mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
feat(pet): /pet style and language, pixel whale body, live pet work (committed as-is; not yet built or tested by the committer)
Pet-model work in progress: /pet style smooth|pixel and /pet language, appearance and live changes, localized strings in all 15 locales, owner and pet_watch updates, DOT_WHALE_PET design note and CHANGELOG entries. nextest for pet/whale/workbar/work_surface, clippy and the PTY tests are still to be run before this is pushed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
3360a6d38d
commit
b0f9cbc4d2
28 files changed
+4354
-802
No files matched your search
@@ -87,6 +87,15 @@ launcher guidance.
|
||||
Motion preferences and unknown usage stay truthful (#6920, #6907),
|
||||
using the shared `codewhale-ratatui` components
|
||||
([#23](https://github.com/codewhale-hq/codewhale-ratatui/pull/23), [#25](https://github.com/codewhale-hq/codewhale-ratatui/pull/25)).
|
||||
- The pet view uses the latest Ratatui particle-whale renderer. Choose a
|
||||
smooth aurora or pixel whale with `/pet style smooth|pixel`, and choose
|
||||
English, Simplified Chinese, Traditional Chinese or Japanese activity words
|
||||
with `/pet language auto|en|zh-Hans|zh-Hant|ja`. Language follows the interface
|
||||
by default; a fixed choice lets you learn another language beside the normal
|
||||
status text. The 980 drones form the same full phrases as the interface,
|
||||
including `读取`, `搜索` and `调用工具`, then return to the whale. The appearance
|
||||
studio saves the same choices. Reduced motion uses a stable pose, and the
|
||||
reply inspector keeps priority on small terminals.
|
||||
|
||||
- A Terminal work dock (`/workbar terminal`) shows the model’s live PTY sessions
|
||||
in the current workspace, with ANSI-styled output, session selection, resize
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Mode mascota activat. Escriu al quadre de missatge; /pet inspect obre les respostes i els agents. /pet off restaura la vista estàndard.",
|
||||
"PetModeOff": "Mode mascota desactivat. La mascota continua vivint al seu company; /pet el torna a activar.",
|
||||
"PetModeReplyTitle": "Resposta",
|
||||
"PetActivityBrowsing": "Utilitzant el navegador",
|
||||
"PetActivityComputer": "Utilitzant l’ordinador",
|
||||
"PetActivityMemory": "Recuperant context",
|
||||
"PetModeMessageHint": "{back} escriure",
|
||||
"PetModePaneHint": "{switch} canviar vista",
|
||||
"PetModeAgentHints": "{select} seleccionar · {open} conversa",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Comença amb el pla. Les accions de risc es revisen automàticament i encara et poden preguntar.",
|
||||
"PlanHandoffKeepPlanning": "Continua planificant",
|
||||
"PlanHandoffKeepPlanningDetail": "Segueix a Plan. No es canvia res.",
|
||||
"PlanHandoffProceed": "Endavant amb el pla."
|
||||
"PlanHandoffProceed": "Endavant amb el pla.",
|
||||
"PetAppearanceLegacy": "La mascota funciona amb una versió anterior. Reinicia-la després d’actualitzar Codewhale per triar l’estil de balena i la llengua del text."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Haustiermodus aktiviert. Schreibe im Nachrichtenfeld; /pet inspect öffnet Antworten und Agenten. /pet off stellt die Standardansicht wieder her.",
|
||||
"PetModeOff": "Haustiermodus aus. Das Haustier lebt in seinem Begleiter weiter; /pet schaltet es wieder ein.",
|
||||
"PetModeReplyTitle": "Antwort",
|
||||
"PetActivityBrowsing": "Browser verwenden",
|
||||
"PetActivityComputer": "Computer bedienen",
|
||||
"PetActivityMemory": "Kontext abrufen",
|
||||
"PetModeMessageHint": "{back} Nachricht schreiben",
|
||||
"PetModePaneHint": "{switch} Ansicht wechseln",
|
||||
"PetModeAgentHints": "{select} auswählen · {open} Verlauf",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Mit dem Plan beginnen. Riskante Aktionen werden automatisch geprüft und können trotzdem nachfragen.",
|
||||
"PlanHandoffKeepPlanning": "Weiter planen",
|
||||
"PlanHandoffKeepPlanningDetail": "In Plan bleiben. Nichts wird geändert.",
|
||||
"PlanHandoffProceed": "Setze den Plan jetzt um."
|
||||
"PlanHandoffProceed": "Setze den Plan jetzt um.",
|
||||
"PetAppearanceLegacy": "Das Haustier verwendet eine ältere Version. Starte es nach dem Codewhale-Update neu, um den Walstil und die Textsprache auszuwählen."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Pet mode on. Write in the message box; /pet inspect opens replies and agents. /pet off restores the standard view.",
|
||||
"PetModeOff": "Pet mode off. The pet keeps living in its companion; /pet turns it back on.",
|
||||
"PetModeReplyTitle": "Reply",
|
||||
"PetActivityBrowsing": "Using the browser",
|
||||
"PetActivityComputer": "Using the computer",
|
||||
"PetActivityMemory": "Retrieving context",
|
||||
"PetModeMessageHint": "{back} message",
|
||||
"PetModePaneHint": "{switch} switch view",
|
||||
"PetModeAgentHints": "{select} select · {open} transcript",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Start on the plan. Risky actions are reviewed automatically and may still ask you.",
|
||||
"PlanHandoffKeepPlanning": "Keep planning",
|
||||
"PlanHandoffKeepPlanningDetail": "Stay in Plan. Nothing is changed.",
|
||||
"PlanHandoffProceed": "Go ahead with the plan."
|
||||
"PlanHandoffProceed": "Go ahead with the plan.",
|
||||
"PetAppearanceLegacy": "The pet companion is running an older version. Restart it after updating Codewhale to use whale styles and text languages."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Modo mascota activado. Escribe en el cuadro de mensaje; /pet inspect abre respuestas y agentes. /pet off restaura la vista estándar.",
|
||||
"PetModeOff": "Modo mascota desactivado. La mascota sigue viviendo en su compañero; /pet lo vuelve a activar.",
|
||||
"PetModeReplyTitle": "Respuesta",
|
||||
"PetActivityBrowsing": "Usando el navegador",
|
||||
"PetActivityComputer": "Usando la computadora",
|
||||
"PetActivityMemory": "Recuperando contexto",
|
||||
"PetModeMessageHint": "{back} escribir",
|
||||
"PetModePaneHint": "{switch} cambiar vista",
|
||||
"PetModeAgentHints": "{select} seleccionar · {open} conversación",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Empieza con el plan. Las acciones riesgosas se revisan automáticamente y aún pueden preguntarte.",
|
||||
"PlanHandoffKeepPlanning": "Seguir planificando",
|
||||
"PlanHandoffKeepPlanningDetail": "Sigue en Plan. No se cambia nada.",
|
||||
"PlanHandoffProceed": "Adelante con el plan."
|
||||
"PlanHandoffProceed": "Adelante con el plan.",
|
||||
"PetAppearanceLegacy": "La mascota usa una versión anterior. Reiníciala después de actualizar Codewhale para elegir el estilo de ballena y el idioma del texto."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Mode animal activé. Écrivez dans le champ de message ; /pet inspect ouvre les réponses et les agents. /pet off rétablit la vue standard.",
|
||||
"PetModeOff": "Mode mascotte désactivé. La mascotte continue de vivre dans son compagnon ; /pet le réactive.",
|
||||
"PetModeReplyTitle": "Réponse",
|
||||
"PetActivityBrowsing": "Utilisation du navigateur",
|
||||
"PetActivityComputer": "Utilisation de l’ordinateur",
|
||||
"PetActivityMemory": "Récupération du contexte",
|
||||
"PetModeMessageHint": "{back} écrire",
|
||||
"PetModePaneHint": "{switch} changer de vue",
|
||||
"PetModeAgentHints": "{select} sélectionner · {open} conversation",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Commencer le plan. Les actions risquées sont examinées automatiquement et peuvent encore demander votre accord.",
|
||||
"PlanHandoffKeepPlanning": "Continuer à planifier",
|
||||
"PlanHandoffKeepPlanningDetail": "Rester en Plan. Rien n'est modifié.",
|
||||
"PlanHandoffProceed": "Allez-y, appliquez le plan."
|
||||
"PlanHandoffProceed": "Allez-y, appliquez le plan.",
|
||||
"PetAppearanceLegacy": "Le compagnon utilise une ancienne version. Redémarrez-le après la mise à jour de Codewhale pour choisir le style de baleine et la langue du texte."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "पेट मोड चालू है। संदेश बॉक्स में लिखें; /pet inspect से उत्तर और एजेंट खोलें। /pet off से सामान्य दृश्य पर लौटें।",
|
||||
"PetModeOff": "पालतू मोड बंद। पालतू अपने साथी में रहता है; /pet से फिर चालू करें।",
|
||||
"PetModeReplyTitle": "उत्तर",
|
||||
"PetActivityBrowsing": "ब्राउज़र का उपयोग",
|
||||
"PetActivityComputer": "कंप्यूटर का उपयोग",
|
||||
"PetActivityMemory": "संदर्भ खोजना",
|
||||
"PetModeMessageHint": "{back} संदेश लिखें",
|
||||
"PetModePaneHint": "{switch} दृश्य बदलें",
|
||||
"PetModeAgentHints": "{select} चुनें · {open} बातचीत",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "योजना पर काम शुरू करें। जोखिम वाले कामों की स्वतः समीक्षा होती है और वे फिर भी आपसे पूछ सकते हैं।",
|
||||
"PlanHandoffKeepPlanning": "योजना बनाना जारी रखें",
|
||||
"PlanHandoffKeepPlanningDetail": "Plan में बने रहें। कुछ भी नहीं बदलता।",
|
||||
"PlanHandoffProceed": "योजना के अनुसार काम शुरू करें।"
|
||||
"PlanHandoffProceed": "योजना के अनुसार काम शुरू करें।",
|
||||
"PetAppearanceLegacy": "पेट पुराने संस्करण पर चल रहा है। व्हेल का रूप और टेक्स्ट की भाषा चुनने के लिए Codewhale अपडेट करने के बाद पेट को फिर से शुरू करें।"
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Mode peliharaan aktif. Tulis di kotak pesan; /pet inspect membuka balasan dan agen. /pet off mengembalikan tampilan biasa.",
|
||||
"PetModeOff": "Mode hewan peliharaan nonaktif. Hewan peliharaan tetap hidup di pendampingnya; /pet mengaktifkannya lagi.",
|
||||
"PetModeReplyTitle": "Balasan",
|
||||
"PetActivityBrowsing": "Menggunakan peramban",
|
||||
"PetActivityComputer": "Mengoperasikan komputer",
|
||||
"PetActivityMemory": "Mengambil konteks",
|
||||
"PetModeMessageHint": "{back} tulis pesan",
|
||||
"PetModePaneHint": "{switch} ganti tampilan",
|
||||
"PetModeAgentHints": "{select} pilih · {open} percakapan",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Mulai kerjakan rencana. Tindakan berisiko ditinjau otomatis dan masih bisa bertanya kepada Anda.",
|
||||
"PlanHandoffKeepPlanning": "Lanjutkan merencanakan",
|
||||
"PlanHandoffKeepPlanningDetail": "Tetap di Plan. Tidak ada yang diubah.",
|
||||
"PlanHandoffProceed": "Silakan jalankan rencananya."
|
||||
"PlanHandoffProceed": "Silakan jalankan rencananya.",
|
||||
"PetAppearanceLegacy": "Pet masih memakai versi lama. Mulai ulang pet setelah memperbarui Codewhale untuk memilih gaya paus dan bahasa teks."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "ペットモードを有効にしました。入力欄からメッセージを送れます。/pet inspect で返信とエージェントを表示し、/pet off で通常表示に戻ります。",
|
||||
"PetModeOff": "ペットモード オフ。ペットはコンパニオンで生き続けます。/pet で再びオンにします。",
|
||||
"PetModeReplyTitle": "返信",
|
||||
"PetActivityBrowsing": "ブラウザーを操作中",
|
||||
"PetActivityComputer": "コンピューターを操作中",
|
||||
"PetActivityMemory": "コンテキストを検索中",
|
||||
"PetModeMessageHint": "{back} メッセージへ",
|
||||
"PetModePaneHint": "{switch} 表示を切替",
|
||||
"PetModeAgentHints": "{select} 選択 · {open} 会話履歴",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "計画に着手します。リスクのある操作は自動でレビューされ、確認を求めることもあります。",
|
||||
"PlanHandoffKeepPlanning": "計画を続ける",
|
||||
"PlanHandoffKeepPlanningDetail": "Plan のままにします。何も変更されません。",
|
||||
"PlanHandoffProceed": "計画どおりに進めてください。"
|
||||
"PlanHandoffProceed": "計画どおりに進めてください。",
|
||||
"PetAppearanceLegacy": "ペットは以前のバージョンで動作しています。Codewhale を更新してペットを再起動すると、クジラのスタイルと文字の言語を選べます。"
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "펫 모드를 켰습니다. 메시지 입력란에 작성하세요. /pet inspect로 답변과 에이전트를 열고 /pet off로 기본 보기로 돌아갑니다.",
|
||||
"PetModeOff": "펫 모드 꺼짐. 펫은 컴패니언에서 계속 살아갑니다. /pet 으로 다시 켭니다.",
|
||||
"PetModeReplyTitle": "답변",
|
||||
"PetActivityBrowsing": "브라우저 사용 중",
|
||||
"PetActivityComputer": "컴퓨터 조작 중",
|
||||
"PetActivityMemory": "컨텍스트 검색 중",
|
||||
"PetModeMessageHint": "{back} 메시지 작성",
|
||||
"PetModePaneHint": "{switch} 보기 전환",
|
||||
"PetModeAgentHints": "{select} 선택 · {open} 대화 기록",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "계획을 시작합니다. 위험한 작업은 자동으로 검토되며 확인을 요청할 수도 있습니다.",
|
||||
"PlanHandoffKeepPlanning": "계속 계획하기",
|
||||
"PlanHandoffKeepPlanningDetail": "Plan 에 머뭅니다. 아무것도 변경되지 않습니다.",
|
||||
"PlanHandoffProceed": "계획대로 진행해 주세요."
|
||||
"PlanHandoffProceed": "계획대로 진행해 주세요.",
|
||||
"PetAppearanceLegacy": "펫이 이전 버전으로 실행 중입니다. Codewhale을 업데이트한 뒤 펫을 다시 시작하면 고래 스타일과 텍스트 언어를 선택할 수 있습니다."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Modo pet ativado. Escreva na caixa de mensagem; /pet inspect abre respostas e agentes. /pet off restaura a visão padrão.",
|
||||
"PetModeOff": "Modo pet desligado. O pet continua vivendo no seu companheiro; /pet liga de novo.",
|
||||
"PetModeReplyTitle": "Resposta",
|
||||
"PetActivityBrowsing": "Usando o navegador",
|
||||
"PetActivityComputer": "Usando o computador",
|
||||
"PetActivityMemory": "Recuperando contexto",
|
||||
"PetModeMessageHint": "{back} escrever",
|
||||
"PetModePaneHint": "{switch} mudar visão",
|
||||
"PetModeAgentHints": "{select} selecionar · {open} conversa",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Começar pelo plano. Ações arriscadas são revisadas automaticamente e ainda podem perguntar a você.",
|
||||
"PlanHandoffKeepPlanning": "Continuar planejando",
|
||||
"PlanHandoffKeepPlanningDetail": "Permanecer em Plan. Nada é alterado.",
|
||||
"PlanHandoffProceed": "Pode seguir com o plano."
|
||||
"PlanHandoffProceed": "Pode seguir com o plano.",
|
||||
"PetAppearanceLegacy": "O pet está usando uma versão anterior. Reinicie-o após atualizar o Codewhale para escolher o estilo da baleia e o idioma do texto."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Режим питомца включён. Пишите в поле сообщения; /pet inspect открывает ответы и агентов. /pet off возвращает обычный вид.",
|
||||
"PetModeOff": "Режим питомца выключен. Питомец продолжает жить в компаньоне; /pet включает его снова.",
|
||||
"PetModeReplyTitle": "Ответ",
|
||||
"PetActivityBrowsing": "Работа в браузере",
|
||||
"PetActivityComputer": "Управление компьютером",
|
||||
"PetActivityMemory": "Поиск контекста",
|
||||
"PetModeMessageHint": "{back} написать",
|
||||
"PetModePaneHint": "{switch} сменить вид",
|
||||
"PetModeAgentHints": "{select} выбрать · {open} переписка",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Начать работу по плану. Рискованные действия проверяются автоматически и всё же могут спросить вас.",
|
||||
"PlanHandoffKeepPlanning": "Продолжить планирование",
|
||||
"PlanHandoffKeepPlanningDetail": "Остаться в Plan. Ничего не меняется.",
|
||||
"PlanHandoffProceed": "Приступайте к плану."
|
||||
"PlanHandoffProceed": "Приступайте к плану.",
|
||||
"PetAppearanceLegacy": "Питомец работает на старой версии. После обновления Codewhale перезапустите его, чтобы выбрать стиль кита и язык текста."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Режим улюбленця ввімкнено. Пишіть у полі повідомлення; /pet inspect відкриває відповіді й агентів. /pet off повертає звичайний вигляд.",
|
||||
"PetModeOff": "Режим улюбленця вимкнено. Улюбленець далі живе в компаньйоні; /pet вмикає його знову.",
|
||||
"PetModeReplyTitle": "Відповідь",
|
||||
"PetActivityBrowsing": "Робота в браузері",
|
||||
"PetActivityComputer": "Керування комп’ютером",
|
||||
"PetActivityMemory": "Пошук контексту",
|
||||
"PetModeMessageHint": "{back} написати",
|
||||
"PetModePaneHint": "{switch} змінити вигляд",
|
||||
"PetModeAgentHints": "{select} вибрати · {open} розмова",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Почати роботу за планом. Ризиковані дії перевіряються автоматично й усе одно можуть запитати вас.",
|
||||
"PlanHandoffKeepPlanning": "Планувати далі",
|
||||
"PlanHandoffKeepPlanningDetail": "Залишитися в Plan. Нічого не змінюється.",
|
||||
"PlanHandoffProceed": "Беріться до плану."
|
||||
"PlanHandoffProceed": "Беріться до плану.",
|
||||
"PetAppearanceLegacy": "Улюбленець працює на старій версії. Після оновлення Codewhale перезапустіть його, щоб вибрати стиль кита й мову тексту."
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
"PetModeOn": "Đã bật chế độ thú cưng. Nhập vào ô tin nhắn; /pet inspect mở phản hồi và tác tử. /pet off trở về giao diện thường.",
|
||||
"PetModeOff": "Đã tắt chế độ thú cưng. Thú cưng vẫn sống trong trình đồng hành; /pet bật lại.",
|
||||
"PetModeReplyTitle": "Phản hồi",
|
||||
"PetActivityBrowsing": "Đang dùng trình duyệt",
|
||||
"PetActivityComputer": "Đang thao tác máy tính",
|
||||
"PetActivityMemory": "Đang truy xuất ngữ cảnh",
|
||||
"PetModeMessageHint": "{back} viết tin nhắn",
|
||||
"PetModePaneHint": "{switch} đổi chế độ xem",
|
||||
"PetModeAgentHints": "{select} chọn · {open} hội thoại",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "Bắt đầu làm theo kế hoạch. Thao tác rủi ro được tự động xem lại và vẫn có thể hỏi bạn.",
|
||||
"PlanHandoffKeepPlanning": "Tiếp tục lập kế hoạch",
|
||||
"PlanHandoffKeepPlanningDetail": "Ở lại Plan. Không có gì thay đổi.",
|
||||
"PlanHandoffProceed": "Hãy làm theo kế hoạch."
|
||||
"PlanHandoffProceed": "Hãy làm theo kế hoạch.",
|
||||
"PetAppearanceLegacy": "Thú cưng đang dùng phiên bản cũ. Khởi động lại sau khi cập nhật Codewhale để chọn kiểu cá voi và ngôn ngữ văn bản."
|
||||
}
|
||||
@@ -43,16 +43,19 @@
|
||||
"ConfigHintPetMode": "启动 Codewhale 时,将动画宠物设为主界面。",
|
||||
"PetModeCopyHint": "{copy} 复制已完成的回复",
|
||||
"PetModeOn": "宠物模式已开启。在消息框中输入;/pet inspect 查看回复与智能体,/pet off 恢复标准视图。",
|
||||
"PetModeOff": "宠物模式已关闭。宠物继续住在伴侣程序里;/pet 重新开启。",
|
||||
"PetModeOff": "宠物模式已关闭,已恢复普通视图。输入 /pet 可重新开启。",
|
||||
"PetModeReplyTitle": "回复",
|
||||
"PetActivityBrowsing": "使用浏览器",
|
||||
"PetActivityComputer": "操作电脑",
|
||||
"PetActivityMemory": "检索上下文",
|
||||
"PetModeMessageHint": "{back} 写消息",
|
||||
"PetModePaneHint": "{switch} 切换视图",
|
||||
"PetModeAgentHints": "{select} 选择 · {open} 对话记录",
|
||||
"PetModeReplyHints": "{scroll} 滚动 · {latest} 最新",
|
||||
"PetModeReplyHints": "{scroll} 滚动 · {latest} 回到最新",
|
||||
"PetModeCompanionHints": "{sound} 声音 · {browser} 外观 · {window} 窗口",
|
||||
"PetModeInspectHint": "{inspect} 回复与智能体 · /pet off 标准视图",
|
||||
"PetModeOnLabel": "宠物模式开",
|
||||
"PetModeOffLabel": "宠物模式关",
|
||||
"PetModeOnLabel": "宠物模式已开启",
|
||||
"PetModeOffLabel": "宠物模式已关闭",
|
||||
"PetViewOpen": "栖息地已打开",
|
||||
"PetViewClosed": "栖息地已关闭",
|
||||
"PetWatchRestored": "/pet 已恢复此会话的栖息地,正在等待新的遥测数据。",
|
||||
@@ -1322,7 +1325,7 @@
|
||||
"CtxInspStablePrefixOnly": "仅稳定前缀",
|
||||
"CtxInspCacheTip": "提示:当当前提供商支持前缀缓存时,稳定前缀区块可被缓存。易变工作集的更改仅会破坏缓存尾部。",
|
||||
"ToolFamilyRead": "读取",
|
||||
"ToolFamilyPatch": "修补",
|
||||
"ToolFamilyPatch": "编辑",
|
||||
"ToolFamilyRun": "运行",
|
||||
"ToolFamilyFind": "搜索",
|
||||
"ToolFamilyDelegate": "智能体",
|
||||
@@ -1330,7 +1333,7 @@
|
||||
"ToolFamilyRlm": "rlm",
|
||||
"ToolFamilyVerify": "验证",
|
||||
"ToolFamilyThink": "思考",
|
||||
"ToolFamilyGeneric": "工具",
|
||||
"ToolFamilyGeneric": "调用工具",
|
||||
"ToolReceiptDone": "完成",
|
||||
"ToolReceiptLinesSingular": "1 行",
|
||||
"ToolReceiptLinesPlural": "{count} 行",
|
||||
@@ -1546,9 +1549,9 @@
|
||||
"PhaseReasoning": "推理中",
|
||||
"PhaseReading": "读取中",
|
||||
"PhaseUsingTool": "使用工具中",
|
||||
"PhaseSubagents": "智能体进行中",
|
||||
"PhaseSubagents": "智能体工作中",
|
||||
"PhaseVerifying": "校验中",
|
||||
"PhaseWaitingOnYou": "需要你",
|
||||
"PhaseWaitingOnYou": "等你处理",
|
||||
"PhaseDone": "完成",
|
||||
"PhaseFailed": "失败",
|
||||
"PhaseFinishing": "收尾中",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "开始执行计划。有风险的操作会自动审核,仍可能向你确认。",
|
||||
"PlanHandoffKeepPlanning": "继续规划",
|
||||
"PlanHandoffKeepPlanningDetail": "留在 Plan。不做任何更改。",
|
||||
"PlanHandoffProceed": "按计划开始执行。"
|
||||
"PlanHandoffProceed": "按计划开始执行。",
|
||||
"PetAppearanceLegacy": "宠物仍在使用旧版本。更新 Codewhale 后重启宠物,即可选择鲸鱼样式和文字语言。"
|
||||
}
|
||||
@@ -43,16 +43,19 @@
|
||||
"ConfigHintPetMode": "啟動 Codewhale 時,將動畫寵物設為主介面。",
|
||||
"PetModeCopyHint": "{copy} 複製已完成的回覆",
|
||||
"PetModeOn": "寵物模式已開啟。在訊息框中輸入;/pet inspect 查看回覆與代理程式,/pet off 恢復標準檢視。",
|
||||
"PetModeOff": "寵物模式已關閉。寵物繼續住在伴侶程式裡;/pet 重新開啟。",
|
||||
"PetModeOff": "寵物模式已關閉,已恢復一般檢視。輸入 /pet 可重新開啟。",
|
||||
"PetModeReplyTitle": "回覆",
|
||||
"PetActivityBrowsing": "使用瀏覽器",
|
||||
"PetActivityComputer": "操作電腦",
|
||||
"PetActivityMemory": "檢索上下文",
|
||||
"PetModeMessageHint": "{back} 寫訊息",
|
||||
"PetModePaneHint": "{switch} 切換檢視",
|
||||
"PetModeAgentHints": "{select} 選取 · {open} 對話紀錄",
|
||||
"PetModeReplyHints": "{scroll} 捲動 · {latest} 最新",
|
||||
"PetModeReplyHints": "{scroll} 捲動 · {latest} 回到最新",
|
||||
"PetModeCompanionHints": "{sound} 音效 · {browser} 外觀 · {window} 視窗",
|
||||
"PetModeInspectHint": "{inspect} 回覆與代理程式 · /pet off 標準檢視",
|
||||
"PetModeOnLabel": "寵物模式開",
|
||||
"PetModeOffLabel": "寵物模式關",
|
||||
"PetModeOnLabel": "寵物模式已開啟",
|
||||
"PetModeOffLabel": "寵物模式已關閉",
|
||||
"PetViewOpen": "棲息地已開啟",
|
||||
"PetViewClosed": "棲息地已關閉",
|
||||
"PetWatchRestored": "/pet 已還原此工作階段的棲息地,正在等待新的遙測資料。",
|
||||
@@ -1320,7 +1323,7 @@
|
||||
"CtxInspStablePrefixOnly": "僅穩定前綴",
|
||||
"CtxInspCacheTip": "提示:當目前的供應商支援前綴快取時,穩定前綴區塊可被快取。易變工作集的更改僅會破壞快取尾部。",
|
||||
"ToolFamilyRead": "讀取",
|
||||
"ToolFamilyPatch": "修補",
|
||||
"ToolFamilyPatch": "編輯",
|
||||
"ToolFamilyRun": "執行",
|
||||
"ToolFamilyFind": "搜尋",
|
||||
"ToolFamilyDelegate": "代理",
|
||||
@@ -1328,7 +1331,7 @@
|
||||
"ToolFamilyRlm": "rlm",
|
||||
"ToolFamilyVerify": "驗證",
|
||||
"ToolFamilyThink": "思考",
|
||||
"ToolFamilyGeneric": "工具",
|
||||
"ToolFamilyGeneric": "呼叫工具",
|
||||
"ToolReceiptDone": "完成",
|
||||
"ToolReceiptLinesSingular": "1 行",
|
||||
"ToolReceiptLinesPlural": "{count} 行",
|
||||
@@ -1544,9 +1547,9 @@
|
||||
"PhaseReasoning": "推理中",
|
||||
"PhaseReading": "讀取中",
|
||||
"PhaseUsingTool": "使用工具中",
|
||||
"PhaseSubagents": "代理進行中",
|
||||
"PhaseSubagents": "代理程式工作中",
|
||||
"PhaseVerifying": "校驗中",
|
||||
"PhaseWaitingOnYou": "需要你",
|
||||
"PhaseWaitingOnYou": "等你處理",
|
||||
"PhaseDone": "完成",
|
||||
"PhaseFailed": "失敗",
|
||||
"PhaseFinishing": "收尾中",
|
||||
@@ -2514,5 +2517,6 @@
|
||||
"PlanHandoffWorkAutoDetail": "開始執行計畫。有風險的操作會自動審核,仍可能向你確認。",
|
||||
"PlanHandoffKeepPlanning": "繼續規劃",
|
||||
"PlanHandoffKeepPlanningDetail": "留在 Plan。不做任何變更。",
|
||||
"PlanHandoffProceed": "依計畫開始執行。"
|
||||
"PlanHandoffProceed": "依計畫開始執行。",
|
||||
"PetAppearanceLegacy": "寵物仍在使用舊版本。更新 Codewhale 後重新啟動寵物,即可選擇鯨魚樣式和文字語言。"
|
||||
}
|
||||
@@ -873,6 +873,10 @@ pub enum MessageId {
|
||||
PetModeOnLabel,
|
||||
PetModeInspectHint,
|
||||
PetModeReplyTitle,
|
||||
PetActivityBrowsing,
|
||||
PetActivityComputer,
|
||||
PetActivityMemory,
|
||||
PetAppearanceLegacy,
|
||||
PetModeMessageHint,
|
||||
PetModePaneHint,
|
||||
PetModeAgentHints,
|
||||
@@ -3502,6 +3506,10 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::PetModeOnLabel,
|
||||
MessageId::PetModeInspectHint,
|
||||
MessageId::PetModeReplyTitle,
|
||||
MessageId::PetActivityBrowsing,
|
||||
MessageId::PetActivityComputer,
|
||||
MessageId::PetActivityMemory,
|
||||
MessageId::PetAppearanceLegacy,
|
||||
MessageId::PetModeMessageHint,
|
||||
MessageId::PetModePaneHint,
|
||||
MessageId::PetModeAgentHints,
|
||||
|
||||
@@ -87,6 +87,15 @@ launcher guidance.
|
||||
Motion preferences and unknown usage stay truthful (#6920, #6907),
|
||||
using the shared `codewhale-ratatui` components
|
||||
([#23](https://github.com/codewhale-hq/codewhale-ratatui/pull/23), [#25](https://github.com/codewhale-hq/codewhale-ratatui/pull/25)).
|
||||
- The pet view uses the latest Ratatui particle-whale renderer. Choose a
|
||||
smooth aurora or pixel whale with `/pet style smooth|pixel`, and choose
|
||||
English, Simplified Chinese, Traditional Chinese or Japanese activity words
|
||||
with `/pet language auto|en|zh-Hans|zh-Hant|ja`. Language follows the interface
|
||||
by default; a fixed choice lets you learn another language beside the normal
|
||||
status text. The 980 drones form the same full phrases as the interface,
|
||||
including `读取`, `搜索` and `调用工具`, then return to the whale. The appearance
|
||||
studio saves the same choices. Reduced motion uses a stable pose, and the
|
||||
reply inspector keeps priority on small terminals.
|
||||
|
||||
- A Terminal work dock (`/workbar terminal`) shows the model’s live PTY sessions
|
||||
in the current workspace, with ANSI-styled output, session selection, resize
|
||||
|
||||
@@ -729,7 +729,7 @@ fn select_pet_mode(app: &mut App, enabled: bool, persist: bool) -> CommandResult
|
||||
/// Escape returns to the same backdrop and draft. Companion verbs keep the
|
||||
/// existing shared simulation, appearance, replay and single audio lease.
|
||||
pub fn pet(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
const USAGE: &str = "Usage: /pet [on|off|inspect|status|appearance|window|source|export|sound on|off|avatar [key]|action [name|live]|view [name|live]]";
|
||||
const USAGE: &str = "Usage: /pet [on|off|inspect|status|appearance|style smooth|pixel|language auto|en|zh-Hans|zh-Hant|ja|window|source|export|sound on|off|avatar [key]|action [name|live]|view [name|live]]";
|
||||
use crate::tui::pet_watch::{self, Control};
|
||||
let words = arg
|
||||
.map(str::trim)
|
||||
@@ -790,6 +790,30 @@ pub fn pet(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
CommandResult::message(format!("/pet {kind} {name}"))
|
||||
}
|
||||
["appearance"] => queued(app, Control::Browser),
|
||||
["style", value] => {
|
||||
let Some(body) = pet_watch::BodyStyle::parse(value) else {
|
||||
return CommandResult::error(USAGE);
|
||||
};
|
||||
queued(
|
||||
app,
|
||||
Control::Appearance {
|
||||
body: Some(body),
|
||||
language: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
["language", value] => {
|
||||
let Some(language) = pet_watch::TextLanguage::parse(value) else {
|
||||
return CommandResult::error(USAGE);
|
||||
};
|
||||
queued(
|
||||
app,
|
||||
Control::Appearance {
|
||||
body: None,
|
||||
language: Some(language),
|
||||
},
|
||||
)
|
||||
}
|
||||
["window"] => queued(app, Control::Window),
|
||||
["source"] => queued(app, Control::Select),
|
||||
["export"] => {
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
//! Presentation preferences never enter the simulation or its replay journal.
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum BodyStyle {
|
||||
#[default]
|
||||
Smooth,
|
||||
Pixel,
|
||||
}
|
||||
impl BodyStyle {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"smooth" => Some(Self::Smooth),
|
||||
"pixel" => Some(Self::Pixel),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TextLanguage {
|
||||
#[default]
|
||||
#[serde(rename = "auto")]
|
||||
Auto,
|
||||
#[serde(rename = "en")]
|
||||
En,
|
||||
#[serde(rename = "zh-Hans")]
|
||||
ZhHans,
|
||||
#[serde(rename = "zh-Hant")]
|
||||
ZhHant,
|
||||
#[serde(rename = "ja")]
|
||||
Ja,
|
||||
}
|
||||
impl TextLanguage {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"auto" => Some(Self::Auto),
|
||||
"en" => Some(Self::En),
|
||||
"zh-hans" => Some(Self::ZhHans),
|
||||
"zh-hant" => Some(Self::ZhHant),
|
||||
"ja" => Some(Self::Ja),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Appearance {
|
||||
@@ -11,6 +53,10 @@ pub struct Appearance {
|
||||
pub dot_scale: f64,
|
||||
pub glow: f64,
|
||||
pub environment: bool,
|
||||
#[serde(default)]
|
||||
pub body_style: BodyStyle,
|
||||
#[serde(default)]
|
||||
pub text_language: TextLanguage,
|
||||
}
|
||||
impl Default for Appearance {
|
||||
fn default() -> Self {
|
||||
@@ -23,6 +69,8 @@ impl Default for Appearance {
|
||||
dot_scale: 1.0,
|
||||
glow: 0.5,
|
||||
environment: true,
|
||||
body_style: BodyStyle::default(),
|
||||
text_language: TextLanguage::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ use crate::{
|
||||
underwater::ShellPhase,
|
||||
},
|
||||
};
|
||||
use codewhale_localization::{MessageId, tr};
|
||||
use codewhale_protocol::engine_owner::OwnerFreshness;
|
||||
use codewhale_localization::{Locale, MessageId, tr};
|
||||
use codewhale_protocol::engine_owner::{OwnerActivityKind, OwnerFreshness, OwnerPresence};
|
||||
use codewhale_ratatui::{
|
||||
AgentCard, Caps, MotionMode, PetMode, PetModeAreas, State, StatusMark, Subagent,
|
||||
AgentCard, Caps, DotWhale, MotionMode, PetMode, PetModeAreas, State, StatusMark, Subagent,
|
||||
SubagentControls, SubagentUsage, SubagentViewWords, Theme,
|
||||
whale_motion::{Activity, Context, Inputs, Presence, Span},
|
||||
};
|
||||
@@ -80,6 +80,37 @@ pub(super) fn agents(app: &App) -> Vec<Subagent<'static>> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn presentation<'a>(
|
||||
raster: Option<&'a super::live::Presentation>,
|
||||
session: Option<&str>,
|
||||
now: Instant,
|
||||
) -> Option<&'a super::live::Presentation> {
|
||||
let source = session.map(super::live::session_source);
|
||||
raster.filter(|r| {
|
||||
r.scene.producer_connected
|
||||
&& now.saturating_duration_since(r.frame_changed) < Duration::from_millis(800)
|
||||
&& Some(r.scene.source.as_str()) == source.as_deref()
|
||||
})
|
||||
}
|
||||
fn activity_words(kind: OwnerActivityKind, locale: Locale) -> std::borrow::Cow<'static, str> {
|
||||
tr(
|
||||
locale,
|
||||
match kind {
|
||||
OwnerActivityKind::Reading => MessageId::ToolFamilyRead,
|
||||
OwnerActivityKind::Editing => MessageId::ToolFamilyPatch,
|
||||
OwnerActivityKind::Searching => MessageId::ToolFamilyFind,
|
||||
OwnerActivityKind::Testing => MessageId::ToolFamilyVerify,
|
||||
OwnerActivityKind::Executing => MessageId::ToolFamilyRun,
|
||||
OwnerActivityKind::Browsing => MessageId::PetActivityBrowsing,
|
||||
OwnerActivityKind::Computer => MessageId::PetActivityComputer,
|
||||
OwnerActivityKind::Memory => MessageId::PetActivityMemory,
|
||||
OwnerActivityKind::Tool => MessageId::ToolFamilyGeneric,
|
||||
OwnerActivityKind::Thinking => MessageId::ToolFamilyThink,
|
||||
OwnerActivityKind::Responding => MessageId::PetModeReplyTitle,
|
||||
OwnerActivityKind::Delegating => MessageId::PhaseSubagents,
|
||||
},
|
||||
)
|
||||
}
|
||||
fn inputs(app: &App, now: Instant) -> Inputs {
|
||||
let presence = match ShellPhase::from_app(app) {
|
||||
ShellPhase::Working | ShellPhase::Verifying => Presence::Working,
|
||||
@@ -88,13 +119,17 @@ fn inputs(app: &App, now: Instant) -> Inputs {
|
||||
ShellPhase::Done => Presence::Done,
|
||||
ShellPhase::Idle | ShellPhase::Failed => Presence::Idle,
|
||||
};
|
||||
let raster = app.pet_watch.raster.as_ref().filter(|r| {
|
||||
r.scene.producer_connected
|
||||
&& now.saturating_duration_since(r.frame_changed) < Duration::from_millis(800)
|
||||
&& Some(r.scene.source.as_str()) == app.current_session_id.as_deref()
|
||||
});
|
||||
let owner = raster.and_then(|r| r.scene.activity.as_ref()).filter(|a| {
|
||||
a.observed && a.freshness == OwnerFreshness::Fresh && a.session_id == app.current_session_id
|
||||
let raster = presentation(
|
||||
app.pet_watch.raster.as_ref(),
|
||||
app.current_session_id.as_deref(),
|
||||
now,
|
||||
);
|
||||
let owner = raster.and_then(|r| {
|
||||
r.scene.activity.as_ref().filter(|a| {
|
||||
a.observed
|
||||
&& a.freshness == OwnerFreshness::Fresh
|
||||
&& a.session_id.as_deref() == Some(r.scene.source.as_str())
|
||||
})
|
||||
});
|
||||
Inputs {
|
||||
presence,
|
||||
@@ -170,6 +205,12 @@ pub(super) fn tick(app: &mut App, visible: bool, now: Instant) {
|
||||
});
|
||||
let motion = motion(app);
|
||||
let theme = theme(app);
|
||||
let prepared = presentation(
|
||||
app.pet_watch.raster.as_ref(),
|
||||
app.current_session_id.as_deref(),
|
||||
now,
|
||||
)
|
||||
.is_some_and(|r| r.scene.materials.is_some());
|
||||
let state = &mut app.pet_watch;
|
||||
state.full.update(
|
||||
app.current_session_id.as_deref(),
|
||||
@@ -183,7 +224,9 @@ pub(super) fn tick(app: &mut App, visible: bool, now: Instant) {
|
||||
state.full_next = None;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
if let Some(interval) = state.full.next_frame_in(&agents, &theme) {
|
||||
if prepared {
|
||||
state.full_next = None;
|
||||
} else if let Some(interval) = state.full.next_frame_in(&agents, &theme) {
|
||||
if state.full_next.is_none_or(|deadline| now >= deadline) {
|
||||
state.full_next = now.checked_add(interval);
|
||||
app.needs_redraw = true;
|
||||
@@ -214,6 +257,29 @@ fn render(frame: &mut Frame, area: Rect, app: &mut App, inspect: bool) {
|
||||
codewhale_palette::chrome_style(&app.ui_theme, toast.level.ink()),
|
||||
)
|
||||
});
|
||||
let now = Instant::now();
|
||||
let prepared = presentation(
|
||||
app.pet_watch.raster.as_ref(),
|
||||
app.current_session_id.as_deref(),
|
||||
now,
|
||||
)
|
||||
.filter(|r| r.scene.materials.is_some());
|
||||
let activity = prepared.and_then(|r| {
|
||||
r.scene.activity.as_ref().filter(|a| {
|
||||
a.observed
|
||||
&& a.freshness == OwnerFreshness::Fresh
|
||||
&& a.session_id.as_deref() == Some(r.scene.source.as_str())
|
||||
})
|
||||
});
|
||||
let words = if matches!(phase, ShellPhase::Working | ShellPhase::Verifying) {
|
||||
activity
|
||||
.filter(|a| a.authoritative_presence == OwnerPresence::Working)
|
||||
.and_then(|a| a.activity_kind)
|
||||
.map(|kind| activity_words(kind, app.ui_locale))
|
||||
.unwrap_or_else(|| phase.label(app.ui_locale))
|
||||
} else {
|
||||
phase.label(app.ui_locale)
|
||||
};
|
||||
let mark = StatusMark::new(match phase {
|
||||
ShellPhase::Working | ShellPhase::Verifying => State::Working,
|
||||
ShellPhase::Approval | ShellPhase::Waiting => State::NeedsYou,
|
||||
@@ -221,7 +287,7 @@ fn render(frame: &mut Frame, area: Rect, app: &mut App, inspect: bool) {
|
||||
ShellPhase::Failed => State::Failed,
|
||||
ShellPhase::Idle | ShellPhase::Typing => State::Ready,
|
||||
})
|
||||
.word(phase.label(app.ui_locale).into_owned());
|
||||
.word(words.clone().into_owned());
|
||||
// ActiveCell is the same live transcript the normal shell reads. Include
|
||||
// streamed replies and terminal errors, without waiting for a successful turn.
|
||||
let response_index = inspect
|
||||
@@ -354,6 +420,26 @@ fn render(frame: &mut Frame, area: Rect, app: &mut App, inspect: bool) {
|
||||
};
|
||||
}
|
||||
let mut view = PetMode::new(&theme, mark);
|
||||
view.dot_whale = prepared.and_then(|r| {
|
||||
let (points, materials, hollow) = if motion.animates() {
|
||||
(
|
||||
&r.scene.points,
|
||||
r.scene.materials.as_ref(),
|
||||
r.scene.style.hollow,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
&r.scene.still.points,
|
||||
r.scene.still.materials.as_ref(),
|
||||
r.scene.still.style.hollow,
|
||||
)
|
||||
};
|
||||
materials.map(|materials| {
|
||||
DotWhale::new(points, materials)
|
||||
.caption(&words)
|
||||
.hollow(hollow)
|
||||
})
|
||||
});
|
||||
view.notice = notice;
|
||||
view.title = "Codewhale".into();
|
||||
view.agents = if inspect { &agents } else { &[] };
|
||||
|
||||
@@ -55,6 +55,8 @@ pub struct Scene {
|
||||
pub still: Pose,
|
||||
#[serde(default)]
|
||||
pub appearance: super::appearance::Appearance,
|
||||
#[serde(default)]
|
||||
pub display_language: Option<String>,
|
||||
/// Owner activity is an overlay on the body, never a reason to drop the
|
||||
/// frame. A long-lived `pet serve` owner can predate this binary (the
|
||||
/// activity shape changed without a frame version bump), so an activity
|
||||
@@ -123,6 +125,7 @@ pub struct Presentation {
|
||||
}
|
||||
#[derive(Clone, Default)]
|
||||
pub struct View {
|
||||
pub locale: String,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub cell_width: f64,
|
||||
@@ -135,6 +138,10 @@ pub struct View {
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub enum Command {
|
||||
Appearance {
|
||||
body: Option<super::BodyStyle>,
|
||||
language: Option<super::TextLanguage>,
|
||||
},
|
||||
Observe(String),
|
||||
Select,
|
||||
Browser,
|
||||
@@ -142,6 +149,7 @@ pub enum Command {
|
||||
Export,
|
||||
}
|
||||
pub enum Notice {
|
||||
AppearanceLegacy,
|
||||
Exported(PathBuf),
|
||||
Message(String),
|
||||
/// The companion cannot be reached: the view thread stopped, or a frame
|
||||
@@ -407,6 +415,17 @@ impl Worker {
|
||||
})
|
||||
}
|
||||
}
|
||||
pub(super) fn session_source(session: &str) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
format!(
|
||||
"session:{}",
|
||||
Sha256::digest(session.as_bytes())
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<String>()
|
||||
)
|
||||
}
|
||||
fn run(
|
||||
rx: mpsc::Receiver<Command>,
|
||||
output: &Mutex<Option<Presentation>>,
|
||||
@@ -414,17 +433,7 @@ fn run(
|
||||
settings: &Mutex<View>,
|
||||
session: Option<String>,
|
||||
) -> io::Result<()> {
|
||||
use sha2::{Digest, Sha256};
|
||||
let source = session.as_ref().map(|s| {
|
||||
format!(
|
||||
"session:{}",
|
||||
Sha256::digest(s.as_bytes())
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
let source = session.as_deref().map(session_source);
|
||||
let mut renderer = graphics::Renderer::default();
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let mut client = Client::connect()?;
|
||||
@@ -438,6 +447,8 @@ fn run(
|
||||
let mut producer_seq = None;
|
||||
let mut sequence = 0u64;
|
||||
let mut action: Option<Value> = None;
|
||||
let mut appearance_patch: Option<(Option<super::BodyStyle>, Option<super::TextLanguage>)> =
|
||||
None;
|
||||
let mut last_action = Instant::now() - Duration::from_secs(1);
|
||||
let mut last_produce = Instant::now();
|
||||
let mut last_audio = Instant::now();
|
||||
@@ -449,6 +460,11 @@ fn run(
|
||||
.clone();
|
||||
match rx.recv_timeout(Duration::from_millis(2)) {
|
||||
Ok(command) => match command {
|
||||
Command::Appearance { body, language } => {
|
||||
let patch = appearance_patch.get_or_insert((None, None));
|
||||
patch.0 = body.or(patch.0);
|
||||
patch.1 = language.or(patch.1);
|
||||
}
|
||||
Command::Observe(text) => {
|
||||
if !events.push(&text)? {
|
||||
// Overflow is a coverage gap, never a truncated batch:
|
||||
@@ -550,6 +566,24 @@ fn run(
|
||||
}
|
||||
}
|
||||
let Some(s) = &scene else { continue };
|
||||
if action.is_none()
|
||||
&& let Some((body, language)) = appearance_patch.take()
|
||||
{
|
||||
if s.display_language.is_none() {
|
||||
let _ = notices.try_send(Notice::AppearanceLegacy);
|
||||
} else {
|
||||
let mut appearance = s.appearance.clone();
|
||||
if let Some(body) = body {
|
||||
appearance.body_style = body;
|
||||
}
|
||||
if let Some(language) = language {
|
||||
appearance.text_language = language;
|
||||
}
|
||||
action = Some(
|
||||
json!({"identity":s.identity,"client":id,"seq":sequence+1,"source_revision":s.source_revision,"action":{"kind":"appearance","appearance":appearance}}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if last_action.elapsed() >= Duration::from_millis(250)
|
||||
&& let Some(pending) = &action
|
||||
{
|
||||
@@ -560,6 +594,7 @@ fn run(
|
||||
sequence += 1;
|
||||
action = None;
|
||||
if selected {
|
||||
fetched = Instant::now() - Duration::from_secs(1);
|
||||
producer_seq = None;
|
||||
events.clear();
|
||||
}
|
||||
@@ -582,7 +617,10 @@ fn run(
|
||||
if seq == 0 {
|
||||
events.clear();
|
||||
}
|
||||
let body = json!({"identity":s.identity,"epoch":s.epoch,"client":id,"source":s.source,"source_revision":s.source_revision,"seq":seq,"waiting":view.waiting,"events":events.events});
|
||||
let mut body = json!({"identity":s.identity,"epoch":s.epoch,"client":id,"source":s.source,"source_revision":s.source_revision,"seq":seq,"waiting":view.waiting,"events":events.events});
|
||||
if s.display_language.is_some() {
|
||||
body["locale"] = json!(view.locale);
|
||||
}
|
||||
producer_seq = client.post("/v1/producer", &body).ok().map(|_| seq);
|
||||
events.clear();
|
||||
last_produce = Instant::now();
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
mod appearance;
|
||||
pub use appearance::{BodyStyle, TextLanguage};
|
||||
mod audio;
|
||||
mod audio_cursor;
|
||||
mod avatars;
|
||||
@@ -36,6 +37,10 @@ mod worker;
|
||||
use live::{Command, Notice, Presentation, Worker};
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Control {
|
||||
Appearance {
|
||||
body: Option<BodyStyle>,
|
||||
language: Option<TextLanguage>,
|
||||
},
|
||||
Sound,
|
||||
Browser,
|
||||
Window,
|
||||
@@ -360,6 +365,9 @@ pub fn command(app: &mut App, control: Control) {
|
||||
}
|
||||
fn apply(state: &mut PetWatch, control: Control, agents: &[codewhale_ratatui::Subagent<'_>]) {
|
||||
match control {
|
||||
Control::Appearance { body, language } => {
|
||||
state.send(Command::Appearance { body, language });
|
||||
}
|
||||
Control::Browser => state.send(Command::Browser),
|
||||
Control::Window => state.send(Command::Window),
|
||||
Control::Select => state.send(Command::Select),
|
||||
@@ -489,7 +497,7 @@ pub fn tick(app: &mut App, now: Instant) {
|
||||
}
|
||||
state.raster = Some(update);
|
||||
state.unavailable = false;
|
||||
if visible && !full::canonical(state) {
|
||||
if visible {
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
@@ -517,11 +525,12 @@ pub fn tick(app: &mut App, now: Instant) {
|
||||
})
|
||||
.unwrap_or((8.0, 16.0));
|
||||
let next = live::View {
|
||||
locale: app.ui_locale.tag().into(),
|
||||
width: a.width.clamp(1, 512),
|
||||
height: a.height.saturating_sub(1).clamp(1, 256),
|
||||
cell_width: cell.0,
|
||||
cell_height: cell.1,
|
||||
motion: motion && !full::canonical(state),
|
||||
motion,
|
||||
pixels: !full::canonical(state)
|
||||
&& matches!(state.avatars.key.as_str(), "" | "whale")
|
||||
&& crate::tui::mark::kitty_graphics_supported()
|
||||
@@ -546,6 +555,10 @@ pub fn tick(app: &mut App, now: Instant) {
|
||||
for notice in notices {
|
||||
app.pet_watch.exporting = false;
|
||||
let (text, level) = match notice {
|
||||
Notice::AppearanceLegacy => (
|
||||
tr(app.ui_locale, MessageId::PetAppearanceLegacy).into_owned(),
|
||||
StatusToastLevel::Warning,
|
||||
),
|
||||
Notice::Exported(path) => (
|
||||
tr(app.ui_locale, MessageId::PetWatchExported)
|
||||
.replace("{path}", &path.display().to_string()),
|
||||
|
||||
@@ -116,6 +116,8 @@ pub struct Producer {
|
||||
pub source_revision: u64,
|
||||
pub seq: u64,
|
||||
pub waiting: bool,
|
||||
#[serde(default)]
|
||||
pub locale: Option<String>,
|
||||
pub events: Vec<Value>,
|
||||
}
|
||||
|
||||
@@ -378,6 +380,7 @@ pub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> {
|
||||
.route("/", get(|| async { Html(include_str!("shared.html")) }))
|
||||
.route("/pet-native.js", get(|| async { ([("content-type", "text/javascript")], include_str!("pet-native.js")) }))
|
||||
.route("/whale-points.tsv", get(|| async { include_str!("../ambient_life/whale-points.tsv") }))
|
||||
.route("/pixel-whale.webp", get(|| async { ([("content-type", "image/webp")], include_bytes!("pixel-whale.webp").as_slice()) }))
|
||||
.route("/v1/frame", get(frame)).route("/v1/action", post(action))
|
||||
.route("/v1/producer", post(producer)).route("/v1/audio", post(audio))
|
||||
.route("/v1/export", get(export)).route("/v1/attach", post(attach))
|
||||
@@ -461,6 +464,7 @@ fn run_world(
|
||||
let initial_time: f64 = context.with(|ctx| ctx.eval("JSON.parse(pet.snapshot()).timeMs"))?;
|
||||
let mut last = origin;
|
||||
let mut last_view = origin;
|
||||
let mut interface_language = "en".to_string();
|
||||
let mut was_active = true;
|
||||
let mut idle_since: Option<Instant> = None;
|
||||
let frame_interval = Duration::from_secs_f64(1.0 / 30.0);
|
||||
@@ -569,7 +573,14 @@ fn run_world(
|
||||
last + Duration::from_secs_f64(count as f64 / 30.0)
|
||||
};
|
||||
if viewed {
|
||||
let text: String = context.with(|ctx| ctx.eval("pet.presentation()"))?;
|
||||
let appearance_json = serde_json::to_string(&saved.appearance)?;
|
||||
let text: String = context.with(|ctx| -> rquickjs::Result<String> {
|
||||
ctx.globals()
|
||||
.set("appearanceJSON", appearance_json.as_str())?;
|
||||
ctx.globals()
|
||||
.set("interfaceLanguage", interface_language.as_str())?;
|
||||
ctx.eval("pet.presentation(JSON.parse(appearanceJSON),interfaceLanguage)")
|
||||
})?;
|
||||
let mut frame: Value = serde_json::from_str(&text)?;
|
||||
frame["version"] = json!(1);
|
||||
frame["identity"] = json!(saved.identity);
|
||||
@@ -725,6 +736,12 @@ fn run_world(
|
||||
|| request.source != saved.source
|
||||
|| request.source_revision != saved.source_revision
|
||||
|| request.events.len() > 64
|
||||
|| request.locale.as_ref().is_some_and(|locale| {
|
||||
locale.len() > 35
|
||||
|| !locale
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'-')
|
||||
})
|
||||
{
|
||||
return Err(
|
||||
"Source changed; attach at the current frame and discard stale input"
|
||||
@@ -732,7 +749,7 @@ fn run_world(
|
||||
);
|
||||
}
|
||||
use sha2::{Digest, Sha256};
|
||||
let hash=Sha256::digest(serde_json::to_vec(&json!({"seq":request.seq,"events":request.events,"waiting":request.waiting})).unwrap()).iter().map(|b|format!("{b:02x}")).collect::<String>();
|
||||
let hash=Sha256::digest(serde_json::to_vec(&json!({"seq":request.seq,"events":request.events,"waiting":request.waiting,"locale":request.locale})).unwrap()).iter().map(|b|format!("{b:02x}")).collect::<String>();
|
||||
if let Some((id, seq, _, prior_hash)) = &producer {
|
||||
if *id != request.client {
|
||||
return Err("This source already has a producer".into());
|
||||
@@ -775,6 +792,14 @@ fn run_world(
|
||||
}
|
||||
producer = Some((request.client, request.seq, Instant::now(), hash));
|
||||
waiting = request.waiting;
|
||||
if let Some(locale) = request.locale {
|
||||
interface_language =
|
||||
if matches!(locale.as_str(), "en" | "zh-Hans" | "zh-Hant" | "ja") {
|
||||
locale
|
||||
} else {
|
||||
"en".into()
|
||||
};
|
||||
}
|
||||
Ok(json!({"seq":request.seq,"cursor":saved.cursor,"durable":false}))
|
||||
})();
|
||||
let _ = reply.send(result);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -5,18 +5,18 @@
|
||||
<style>
|
||||
:root{color-scheme:dark;font:14px system-ui,sans-serif;background:#101619;color:#dae2e2;--line:#344044;--muted:#a4b3b5;--accent:#a3dcd4}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0}button,input,select{font:inherit}button{color:inherit;background:transparent;border:1px solid var(--line);border-radius:5px;padding:9px 13px;cursor:pointer}button:hover{background:#273338}button:disabled{opacity:.5;cursor:wait}button[aria-pressed=true]{border-color:var(--accent);background:#263b3a}button:focus-visible,input:focus-visible,select:focus-visible,canvas:focus-visible{outline:2px solid var(--accent);outline-offset:3px}::selection{background:#98d7cf;color:#142522}a{color:var(--accent);text-underline-offset:4px}header{height:74px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 30px;gap:16px}header strong{font-size:18px;font-weight:550;letter-spacing:-.025em}header span{font-size:12px;color:var(--muted);margin-left:15px}nav{display:flex;gap:8px}main{display:grid;grid-template-columns:minmax(0,1fr) 290px;min-height:calc(100vh - 74px)}.stage{padding:28px;min-width:0}h1{font-size:30px;letter-spacing:-.03em;font-weight:500;margin:0 0 8px}p{color:var(--muted);line-height:1.6;margin:0 0 22px;max-width:68ch}.habitat{position:relative;height:calc(100vh - 282px);min-height:350px;background:#080f15;border:1px solid #374347;border-radius:5px;overflow:hidden}canvas{display:block;width:100%;height:100%;outline:none}.caption{position:absolute;left:25px;bottom:22px;right:25px;display:flex;justify-content:space-between;gap:12px;font:11px ui-monospace,monospace;color:#d1e1e2;pointer-events:none;font-variant-numeric:tabular-nums}.caption small{font:inherit}.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:16px}.controls label{margin:0 8px}.identity{font:10px ui-monospace,monospace;color:var(--muted);margin-top:15px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.settings{border-left:1px solid var(--line);padding:28px 24px;overflow-y:auto;max-height:calc(100vh - 74px);scrollbar-color:#55686a #101619}.settings h2{font-size:16px;font-weight:550;margin:0 0 8px}.settings p{font-size:12px;line-height:1.55;margin-bottom:22px}.presets{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:24px}.presets button{padding:9px;text-align:left;font-size:12px;display:flex;align-items:center;gap:8px}.swatch{width:18px;height:18px;display:inline-grid;place-items:center;border-radius:50%;border:1px solid #788887}.swatch i{width:7px;height:7px;border-radius:50%}.field{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:17px 0;font-size:12px}.field input[type=color]{width:40px;height:26px;padding:0;border:1px solid var(--line);background:none;border-radius:3px;cursor:pointer}.range{display:block}.range span{display:flex;justify-content:space-between;margin-bottom:8px}.range input{width:100%;accent-color:var(--accent)}input[type=checkbox]{accent-color:var(--accent)}input[type=file]{max-width:100%;font-size:11px}output{font-variant-numeric:tabular-nums;color:var(--muted)}.divider{border-top:1px solid var(--line);margin-top:22px;padding-top:8px}.config-actions{display:flex;gap:8px;margin-top:20px}.config-actions button{font-size:11px;padding:8px}.status{min-height:32px;font-size:11px!important;margin-top:16px!important}.result{position:absolute;left:clamp(24px,9vw,160px);right:clamp(24px,9vw,160px);top:50%;max-height:44%;overflow:auto;padding:24px 30px;background:#111b20f2;color:#e4eeec;border-radius:6px;opacity:0;transform:translateY(18px);pointer-events:none;transition:opacity .65s cubic-bezier(.16,1,.3,1),transform .65s cubic-bezier(.16,1,.3,1);scrollbar-color:#65817f #111b20}.result.visible{opacity:1;transform:translateY(0);pointer-events:auto}.result h2{font-size:21px;font-weight:500;letter-spacing:-.02em;margin:0 0 12px}.result p{color:#b9cecc;font-size:14px;line-height:1.7;margin:0 0 14px}.result .label{font:11px ui-monospace,monospace;color:#94beb8;margin:0 0 12px}.result button{font-size:12px}.immersive header,.immersive .settings,.immersive .intro,.immersive .identity{display:none}.immersive main{display:block}.immersive .stage{padding:0}.immersive .habitat{height:calc(100dvh - 64px);min-height:200px;border:0;border-radius:0}.immersive .controls{height:64px;margin:0;padding:10px 22px}.gallery{display:none;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;margin:24px 0}.gallery figure{margin:0;height:205px;position:relative;border-radius:5px;overflow:hidden}.gallery figcaption{position:absolute;left:16px;bottom:14px;font-size:12px}.gallery-mode .gallery{display:grid}.gallery-mode .habitat{display:none}.gallery-mode .stage> .controls{display:none}.gallery-mode .settings{position:sticky;top:0}.connection{margin-top:12px;font-size:11px}.connection a{display:inline-block;margin-top:8px}.work-label{font:11px ui-monospace,monospace;color:var(--muted);margin-left:auto}#finish{display:none}.preview-runLine truncated
|
||||
|
||||
.activity{position:absolute;top:48px;left:30px;right:30px;color:#d4e5e5;pointer-events:none}.activity .eyebrow{font:12px ui-monospace,monospace;opacity:.75;margin-bottom:8px}.activity h2{font-size:clamp(22px,2.8vw,34px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.activity .detail{font:12px ui-monospace,monospace;line-height:1.6;overflow-wrap:anywhere}.activity .parallel{display:block;margin-top:7px;font:12px system-ui,sans-serif}.result.visible~.activity{opacity:1}.action-field{display:block;font-size:12px;margin:18px 0 10px}.action-field select{display:block;width:100%;margin-top:8px;color:inherit;background:#182226;border:1px solid var(--line);padding:9px;border-radius:4px}.settings .behavior-note{font-size:12px;margin-bottom:24px}.caption{font-size:12px}.identity{font-size:11px}.status,.config-actions button,.connection{font-size:12px!important}.gallery figcaption{font-size:13px}@media(max-width:480px){.activity{top:38px;left:22px;right:22px}.activity h2{font-size:23px}.activity .detail{font-size:12px}.caption{font-size:12px}.result.visible~.activity{display:none}.immersive .habitat{height:calc(100dvh - 112px)}}
|
||||
.activity{position:absolute;top:48px;left:30px;right:30px;color:#d4e5e5;pointer-events:none}.activity .phrase{font-size:16px;line-height:1.5;margin-bottom:7px}.activity .eyebrow{font:12px ui-monospace,monospace;opacity:.75;margin-bottom:8px}.activity h2{font-size:clamp(22px,2.8vw,34px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.activity .detail{font:12px ui-monospace,monospace;line-height:1.6;overflow-wrap:anywhere}.activity .parallel{display:block;margin-top:7px;font:12px system-ui,sans-serif}.result.visible~.activity{opacity:1}.action-field{display:block;font-size:12px;margin:18px 0 10px}.action-field select{display:block;width:100%;margin-top:8px;color:inherit;background:#182226;border:1px solid var(--line);padding:9px;border-radius:4px}.settings .behavior-note{font-size:12px;margin-bottom:24px}.caption{font-size:12px}.identity{font-size:11px}.status,.config-actions button,.connection{font-size:12px!important}.gallery figcaption{font-size:13px}@media(max-width:480px){.activity{top:38px;left:22px;right:22px}.activity h2{font-size:23px}.activity .detail{font-size:12px}.caption{font-size:12px}.result.visible~.activity{display:none}.immersive .habitat{height:calc(100dvh - 112px)}}
|
||||
|
||||
/* A reliable backing protects labels even when the two user colors have
|
||||
opposite luminance. Particle colors remain entirely configurable. */
|
||||
.activity{width:max-content;max-width:calc(100% - 60px);padding:12px 15px;background:#10191df2;border-radius:5px;color:#eaf2f0}.activity .eyebrow{margin:0 0 8px;opacity:1;color:#b9ceca}.caption{padding:8px 10px;background:#10191df2;border-radius:4px;color:#eaf2f0}.result:focus{outline:2px solid #a3dcd4;outline-offset:3px}@media(max-width:480px){.activity{max-width:calc(100% - 44px);padding:12px}.activity h2{font-size:22px}}
|
||||
.activity{width:max-content;max-width:calc(100% - 60px);padding:12px 15px;background:#10191df2;border-radius:5px;color:#eaf2f0}.activity .phrase{font-size:16px;line-height:1.5;margin-bottom:7px}.activity .eyebrow{margin:0 0 8px;opacity:1;color:#b9ceca}.caption{padding:8px 10px;background:#10191df2;border-radius:4px;color:#eaf2f0}.result:focus{outline:2px solid #a3dcd4;outline-offset:3px}@media(max-width:480px){.activity{max-width:calc(100% - 44px);padding:12px}.activity h2{font-size:22px}}
|
||||
</style>
|
||||
<header><div><strong>Codewhale pet</strong><span id="connection-label">Shared habitat</span></div><nav aria-label="View options"><button id="compare" aria-pressed="false">Compare looks</button><button id="configure" aria-pressed="true">Appearance</button></nav></header>
|
||||
<main><section class="stage"><div class="intro"><h1>The whale speaks in light.</h1><p>The whole whale flows into a living character: reading scans 读, writing traces 写, and searching sweeps light through 搜. Then every dot gathers back into the whale.</p></div>
|
||||
<div class="habitat"><canvas id="tank" tabindex="0" role="img" aria-label="Codewhale habitat"></canvas><article class="result" id="result" tabindex="-1" aria-live="polite" aria-hidden="true" inert><h2>The work is ready.</h2><div class="label">Preview result · no task was run</div><p>The whale held the space while the task was running. Now the answer comes forward, with the habitat still present behind it.</p><p>In Codewhale, this space will show the completed assistant response from your existing conversation. Your draft, history, and selection stay intact.</p><button id="replay">Replay transition</button></article><div class="activity" id="activity" aria-live="polite" aria-atomic="true"><h2 id="activity-title">A moment between tasks.</h2><div class="eyebrow" id="activity-evidence">Local companion</div><div class="detail" id="activity-detail">Activity unobserved</div><span class="parallel" id="activity-parallel"></span></div><div class="caption"><strong id="caption">Connecting…</strong><small id="clock"></small></div></div>
|
||||
<main><section class="stage"><div class="intro"><h1>The whale speaks in light.</h1><p>Choose a whale style and a language for its large activity words. Its pixels and drones form the same phrases Codewhale uses, then return to the whale.</p></div>
|
||||
<div class="habitat"><canvas id="tank" tabindex="0" role="img" aria-label="Codewhale habitat"></canvas><article class="result" id="result" tabindex="-1" aria-live="polite" aria-hidden="true" inert><h2>The work is ready.</h2><div class="label">Preview result · no task was run</div><p>The whale held the space while the task was running. Now the answer comes forward, with the habitat still present behind it.</p><p>In Codewhale, this space will show the completed assistant response from your existing conversation. Your draft, history, and selection stay intact.</p><button id="replay">Replay transition</button></article><div class="activity" id="activity" aria-live="polite" aria-atomic="true"><h2 id="activity-title">A moment between tasks.</h2><div class="phrase" id="activity-phrase" hidden></div><div class="eyebrow" id="activity-evidence">Local companion</div><div class="detail" id="activity-detail">Activity unobserved</div><span class="parallel" id="activity-parallel"></span></div><div class="caption"><strong id="caption">Connecting…</strong><small id="clock"></small></div></div>
|
||||
<div class="gallery" id="gallery" aria-label="The same whale across eight appearances"></div>
|
||||
<div class="controls"><button id="focus">Focus</button><button id="pulse">Pulse</button><button id="sound" aria-pressed="false">Sound off</button><label><input id="still" type="checkbox"> Still</label><button id="expand">Full habitat</button><button id="work">Preview work → result</button><button id="finish">Finish preview now</button><button id="return-live" hidden>Return to live pet</button><span class="work-label" id="work-label">Escape returns</span></div><div class="identity" id="identity"></div></section>
|
||||
<aside class="settings" aria-label="Appearance settings"><h2>Behavior</h2><label class="action-field">Try a preview action<select id="action-preview"><option value="thinking">Thinking</option><option value="reading">Reading files</option><option value="searching">Searching</option><option value="editing">Editing files</option><option value="executing">Running a command</option><option value="testing">Running tests</option><option value="browsing">Using the browser</option><option value="computer">Controlling the desktop</option><option value="memory">Retrieving context</option><option value="tool">Using a tool</option><option value="delegating">Three parallel agents</option><option value="waiting">Waiting for you</option><option value="responding">Writing a response</option><option value="error">An operation failed</option><option value="unknown">No telemetry</option></select></label><p class="behavior-note">Preview actions are simulated. All 980 dots become one large character, alive with light and motion, before returning to the filled whale. Live activity comes from the session owner.</p><h2>Appearance</h2><p>Presets are starting points. Every color is yours to change.</p><div class="presets" id="presets"></div>
|
||||
<aside class="settings" aria-label="Appearance settings"><h2>Behavior</h2><label class="action-field">Try a preview action<select id="action-preview"><option value="thinking">Thinking</option><option value="reading">Reading files</option><option value="searching">Searching</option><option value="editing">Editing files</option><option value="executing">Running a command</option><option value="testing">Running tests</option><option value="browsing">Using the browser</option><option value="computer">Controlling the desktop</option><option value="memory">Retrieving context</option><option value="tool">Using a tool</option><option value="delegating">Three parallel agents</option><option value="waiting">Waiting for you</option><option value="responding">Writing a response</option><option value="error">An operation failed</option><option value="unknown">No telemetry</option></select></label><p class="behavior-note">Preview actions are simulated. All 980 dots form complete activity phrases, alive with light and motion, before returning to the filled whale. Live activity comes from the session owner.</p><h2>Whale</h2><label class="action-field">Whale style<select id="bodyStyle"><option value="smooth">Smooth aurora</option><option value="pixel">Pixel whale</option></select></label><label class="action-field">Large-text language<select id="textLanguage"><option value="auto">Follow Codewhale interface</option><option value="en">English</option><option value="zh-Hans">简体中文 · Simplified Chinese</option><option value="zh-Hant">繁體中文 · Traditional Chinese</option><option value="ja">日本語 · Japanese</option></select></label><p class="behavior-note">Try another language to learn its activity phrases. Regular status text stays in the interface language. Other interface languages currently use English for the large text.</p><h2>Appearance</h2><p>Presets are starting points. Every color is yours to change.</p><div class="presets" id="presets"></div>
|
||||
<label class="field">Background <input type="color" id="background" value="#080f15"></label><label class="field">Upper light <input type="color" id="backgroundTop" value="#182d38"></label><label class="field">Particle color <input type="color" id="particle" value="#87dddb"></label><label class="field"><span>Colors follow activity</span><input type="checkbox" id="eventColors" checked></label>
|
||||
<label class="field range"><span>Brightness <output id="brightness-value"></output></span><input type="range" id="brightness" min="0.25" max="2" step="0.05" value="1.25"></label><label class="field range"><span>Dot size <output id="dotScale-value"></output></span><input type="range" id="dotScale" min="0.65" max="1.8" step="0.05" value="1"></label><label class="field range"><span>Glow <output id="glow-value"></output></span><input type="range" id="glow" min="0" max="1" step="0.05" value="0.5"></label><label class="field"><span>Water and horizon</span><input type="checkbox" id="environment" checked></label>
|
||||
<div class="config-actions"><button id="export-config">Save appearance</button><button id="import-config">Load appearance</button><input id="config-file" type="file" accept="application/json,.json" hidden></div><p class="status" id="message" role="status">Opening the habitat…</p><div class="connection"><button id="save">Save pet replay</button><br><a href="pet.html" id="join">Join a live pet from the standalone viewer</a></div></aside></main>
|
||||
@@ -25,20 +25,23 @@
|
||||
'use strict';
|
||||
const $=id=>document.getElementById(id), canvas=$('tank');
|
||||
const rgb=h=>h.match(/[a-f\d]{2}/gi).map(n=>parseInt(n,16)),hex=c=>'#'+c.map(n=>n.toString(16).padStart(2,'0')).join('');
|
||||
const base={background:[8,15,21],backgroundTop:[24,45,56],particle:[135,221,219],eventColors:true,brightness:1.25,dotScale:1,glow:.5,environment:true};
|
||||
const base={background:[8,15,21],backgroundTop:[24,45,56],particle:[135,221,219],eventColors:true,brightness:1.25,dotScale:1,glow:.5,environment:true,bodyStyle:'smooth',textLanguage:'auto'};
|
||||
const presets=[['Ocean','#080f15','#182d38','#87dddb'],['Chalk','#e8edee','#fbfdfb','#285967'],['Graphite','#141518','#28292e','#d9e0e3'],['Linen','#e9e1d2','#f8f2e7','#79573a'],['Forest','#0c1916','#20382d','#c5db9b'],['Plum','#201a29','#3b2e4b','#e7b8d0'],['Ember','#211610','#49291b','#edb177'],['Cobalt','#101934','#263b60','#b2d7f2']].map(([name,b,t,p])=>({name,...base,background:rgb(b),backgroundTop:rgb(t),particle:rgb(p),eventColors:false,brightness:name==='Chalk'||name==='Linen'?1.8:1.35}));
|
||||
let appearance={...base}, selected='Ocean', current, previous, lastTickAt=0, received=0, client=crypto.randomUUID(), seq=0, pending=null, nextAppearance=null, busy=false, sound=false;
|
||||
let preview=new URLSearchParams(location.search).has('preview'), pet, points, demoTick=0, demoRunning=false, demoDone=false, actionChoice='thinking', actionTick=0, lastDraw=0, accumulator=0, galleryTick=0, previewSaved=false;
|
||||
const pixelWhale=new Image();pixelWhale.src='pixel-whale.webp';
|
||||
const pixelTints=new Map();
|
||||
function pixelSource(a){if(a.eventColors)return pixelWhale;const key=hex(a.particle);if(pixelTints.has(key))return pixelTints.get(key);const c=document.createElement('canvas');c.width=c.height=512;const ctx=c.getContext('2d');ctx.imageSmoothingEnabled=false;ctx.drawImage(pixelWhale,0,0,512,512);ctx.globalCompositeOperation='source-in';ctx.fillStyle=key;ctx.fillRect(0,0,512,512);if(pixelTints.size>=16)pixelTints.clear();pixelTints.set(key,c);return c}
|
||||
const media=matchMedia('(prefers-reduced-motion: reduce)');$('still').checked=media.matches;media.onchange=()=>{$('still').checked=media.matches};
|
||||
function valid(a){return a&&Object.keys(base).every(k=>k in a)&&['background','backgroundTop','particle'].every(k=>Array.isArray(a[k])&&a[k].length===3&&a[k].every(n=>Number.isInteger(n)&&n>=0&&n<=255))&&typeof a.eventColors==='boolean'&&typeof a.environment==='boolean'&&[['brightness',.25,2],['dotScale',.65,1.8],['glow',0,1]].every(([k,lo,hi])=>Number.isFinite(a[k])&&a[k]>=lo&&a[k]<=hi)}
|
||||
function clean(a){if(!valid(a))throw Error('This appearance has invalid colors or control ranges.');return Object.fromEntries(Object.keys(base).map(k=>[k,a[k]]))}
|
||||
function valid(a){return a&&Object.keys(base).filter(k=>!['bodyStyle','textLanguage'].includes(k)).every(k=>k in a)&&['background','backgroundTop','particle'].every(k=>Array.isArray(a[k])&&a[k].length===3&&a[k].every(n=>Number.isInteger(n)&&n>=0&&n<=255))&&typeof a.eventColors==='boolean'&&typeof a.environment==='boolean'&&['smooth','pixel'].includes(a.bodyStyle??'smooth')&&['auto','en','zh-Hans','zh-Hant','ja'].includes(a.textLanguage??'auto')&&[['brightness',.25,2],['dotScale',.65,1.8],['glow',0,1]].every(([k,lo,hi])=>Number.isFinite(a[k])&&a[k]>=lo&&a[k]<=hi)}
|
||||
function clean(a){if(!valid(a))throw Error('This appearance has invalid colors or control ranges.');return Object.fromEntries(Object.keys(base).map(k=>[k,a[k]??base[k]]))}
|
||||
try{const saved=JSON.parse(localStorage.getItem('codewhale-pet-appearance-v1'));if(valid(saved))appearance=clean(saved)}catch{}
|
||||
function presetName(a){return presets.find(p=>JSON.stringify(clean(p))===JSON.stringify(clean(a)))?.name||'Custom'}
|
||||
function presetName(a){const palette=v=>Object.fromEntries(Object.entries(clean(v)).filter(([k])=>!['bodyStyle','textLanguage'].includes(k)));return presets.find(p=>JSON.stringify(palette(p))===JSON.stringify(palette(a)))?.name||'Custom'}
|
||||
selected=presetName(appearance);
|
||||
function syncControls(){for(const k of ['background','backgroundTop','particle'])$(k).value=hex(appearance[k]);for(const k of ['eventColors','environment'])$(k).checked=appearance[k];for(const k of ['brightness','dotScale','glow']){$(k).value=appearance[k];$(k+'-value').value=k==='glow'?Math.round(appearance[k]*100)+'%':appearance[k].toFixed(2)+'×'}for(const b of $('presets').children)b.setAttribute('aria-pressed',String(b.textContent===selected));}
|
||||
function syncControls(){for(const k of ['bodyStyle','textLanguage'])$(k).value=appearance[k];for(const k of ['background','backgroundTop','particle'])$(k).value=hex(appearance[k]);for(const k of ['eventColors','environment'])$(k).checked=appearance[k];for(const k of ['brightness','dotScale','glow']){$(k).value=appearance[k];$(k+'-value').value=k==='glow'?Math.round(appearance[k]*100)+'%':appearance[k].toFixed(2)+'×'}for(const b of $('presets').children)b.setAttribute('aria-pressed',String(b.textContent===selected));}
|
||||
function saveAppearance(){try{localStorage.setItem('codewhale-pet-appearance-v1',JSON.stringify(appearance))}catch{}if(preview){$('message').textContent='Appearance saved in this preview. Export it to keep a portable copy.'}else{nextAppearance=clean(appearance);$('message').textContent='Saving appearance to the companion…';void flushAction()}}
|
||||
for(const a of presets){const b=document.createElement('button');b.type='button';b.setAttribute('aria-pressed','false');const sw=document.createElement('span');sw.className='swatch';sw.style.background=hex(a.background);const dot=document.createElement('i');dot.style.background=hex(a.particle);sw.append(dot);b.append(sw,document.createTextNode(a.name));b.onclick=()=>{appearance=clean(a);selected=a.name;syncControls();saveAppearance()};$('presets').append(b);const f=document.createElement('figure'),c=document.createElement('canvas'),label=document.createElement('figcaption');c.setAttribute('role','img');c.setAttribute('aria-label',a.name+' appearance');label.textContent=a.name;label.style.color=readable(a.background);f.append(c,label);$('gallery').append(f)}
|
||||
for(const key of Object.keys(base))$(key).oninput=()=>{appearance[key]=['background','backgroundTop','particle'].includes(key)?rgb($(key).value):['eventColors','environment'].includes(key)?$(key).checked:Number($(key).value);if(key==='particle')appearance.eventColors=false;selected='Custom';syncControls();saveAppearance()};
|
||||
for(const a of presets){const b=document.createElement('button');b.type='button';b.setAttribute('aria-pressed','false');const sw=document.createElement('span');sw.className='swatch';sw.style.background=hex(a.background);const dot=document.createElement('i');dot.style.background=hex(a.particle);sw.append(dot);b.append(sw,document.createTextNode(a.name));b.onclick=()=>{appearance=clean({...a,bodyStyle:appearance.bodyStyle,textLanguage:appearance.textLanguage});selected=a.name;syncControls();saveAppearance()};$('presets').append(b);const f=document.createElement('figure'),c=document.createElement('canvas'),label=document.createElement('figcaption');c.setAttribute('role','img');c.setAttribute('aria-label',a.name+' appearance');label.textContent=a.name;label.style.color=readable(a.background);f.append(c,label);$('gallery').append(f)}
|
||||
for(const key of Object.keys(base))$(key).oninput=()=>{appearance[key]=['bodyStyle','textLanguage'].includes(key)?$(key).value:['background','backgroundTop','particle'].includes(key)?rgb($(key).value):['eventColors','environment'].includes(key)?$(key).checked:Number($(key).value);if(key==='particle')appearance.eventColors=false;if(!['bodyStyle','textLanguage'].includes(key))selected='Custom';syncControls();saveAppearance()};
|
||||
syncControls();
|
||||
async function request(path,body,headers={}){const r=await fetch(path,{method:body===undefined?'GET':'POST',headers:{'Content-Type':'application/json',...headers},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(2500)});if(!r.ok){let e;try{e=await r.json()}catch{}const error=Error(e?.error||'Local pet unavailable. Reopen from Codewhale or use the preview.');error.rejected=r.status===409&&!error.message.includes('storage');throw error}return r.json()}
|
||||
async function flushAction(){if(busy||!current||preview)return;if(!pending&&nextAppearance){pending={identity:current.identity,client,seq:seq+1,source_revision:current.sourceRevision,action:{kind:'appearance',appearance:nextAppearance}};nextAppearance=null}if(!pending)return;busy=true;try{await request('/v1/action',pending);seq=pending.seq;pending=null;$('message').textContent='Appearance and interactions saved by the shared companion.'}catch(e){if(e.rejected)pending=null;$('message').textContent=e.message}finally{busy=false}}
|
||||
@@ -69,7 +72,7 @@ const previewActions={reading:'read_file',searching:'grep_files',editing:'apply_
|
||||
const workPhases=['thinking','reading','searching','browsing','computer','memory','tool','delegating','editing','executing','testing','waiting','responding'];
|
||||
function beginAction(kind,at){const emit=e=>pet.observeEngine(JSON.stringify(e),at);emit({event:'turn_started',turn_id:'preview-turn-'+(++previewTurn)});if(operationKinds.includes(kind))emit({event:'operation_activity_started',span_id:'preview-op-'+(++previewSpan),activity_kind:kind,action_id:previewActions[kind]});else if(kind==='waiting')emit({event:'user_input_required',id:'preview-question'});else if(kind==='delegating'){for(let i=0;i<3;i++)emit({event:'agent_spawned',id:'preview-worker-'+i})}else if(kind==='thinking'||kind==='responding')emit({event:kind==='thinking'?'thinking_started':'message_started',index:0});else if(kind==='error'){const span='preview-op-'+(++previewSpan);emit({event:'operation_activity_started',span_id:span,activity_kind:'executing',action_id:'exec_shell'});emit({event:'operation_activity_completed',span_id:span,activity_kind:'executing',action_id:'exec_shell',outcome:'failed'})}actionTick=0}
|
||||
$('action-preview').onchange=async()=>{try{if(!preview)await makePreview();demoRunning=false;demoDone=false;actionChoice=$('action-preview').value;beginAction(actionChoice,demoTick*1000/30);showResult(false);document.body.classList.remove('preview-running')}catch(e){$('message').textContent=e.message}};
|
||||
function advancePreview(){const at=demoTick*1000/30;if(!demoDone){if(demoRunning){const phase=Math.min(workPhases.length-1,Math.floor(demoTick/180));if(demoTick%180===0){actionChoice=workPhases[phase];$('action-preview').value=actionChoice;beginAction(actionChoice,at)}}else if(demoTick===0)beginAction(actionChoice,at);if(actionTick%6===0){const emit=e=>pet.observeEngine(JSON.stringify(e),at);if(operationKinds.includes(actionChoice))emit({event:'tool_call_heartbeat'});else if(actionChoice==='delegating'){for(let i=0;i<3;i++)emit({event:'agent_progress',id:'preview-worker-'+i,worker_status:'running'})}else if(actionChoice==='thinking'||actionChoice==='responding')emit({event:'response_delta',index:0,channel:actionChoice==='thinking'?'reasoning':'text'});}}actionTick++;pet.advanceEngine(++demoTick*1000/30,true,!demoDone&&actionChoice==='waiting');if(demoRunning&&demoTick>=180*workPhases.length)finishWork();previous=current;current=JSON.parse(pet.presentation());lastTickAt=received=performance.now();current.producerConnected=!demoDone&&actionChoice!=='unknown';current.epoch='preview';}
|
||||
function advancePreview(){const at=demoTick*1000/30;if(!demoDone){if(demoRunning){const phase=Math.min(workPhases.length-1,Math.floor(demoTick/180));if(demoTick%180===0){actionChoice=workPhases[phase];$('action-preview').value=actionChoice;beginAction(actionChoice,at)}}else if(demoTick===0)beginAction(actionChoice,at);if(actionTick%6===0){const emit=e=>pet.observeEngine(JSON.stringify(e),at);if(operationKinds.includes(actionChoice))emit({event:'tool_call_heartbeat'});else if(actionChoice==='delegating'){for(let i=0;i<3;i++)emit({event:'agent_progress',id:'preview-worker-'+i,worker_status:'running'})}else if(actionChoice==='thinking'||actionChoice==='responding')emit({event:'response_delta',index:0,channel:actionChoice==='thinking'?'reasoning':'text'});}}actionTick++;pet.advanceEngine(++demoTick*1000/30,true,!demoDone&&actionChoice==='waiting');if(demoRunning&&demoTick>=180*workPhases.length)finishWork();previous=current;current=JSON.parse(pet.presentation(appearance,navigator.language));lastTickAt=received=performance.now();current.producerConnected=!demoDone&&actionChoice!=='unknown';current.epoch='preview';}
|
||||
function readable(c){return c[0]*.2126+c[1]*.7152+c[2]*.0722>145?'#28383d':'#d4e5e5'}
|
||||
function paint(c,f,a,now,small=false){const w=c.clientWidth,h=c.clientHeight,d=small?1:Math.min(devicePixelRatio||1,2);if(!w||!h)return;if(c.width!==Math.round(w*d)||c.height!==Math.round(h*d)){c.width=Math.round(w*d);c.height=Math.round(h*d)}const ctx=c.getContext('2d');ctx.setTransform(d,0,0,d,0,0);const gradient=ctx.createRadialGradient(w*.5,0,0,w*.5,h*.18,w*.9);gradient.addColorStop(0,hex(a.backgroundTop));gradient.addColorStop(1,hex(a.background));ctx.fillStyle=gradient;ctx.fillRect(0,0,w,h);if(!f)return;const still=$('still').checked,pose=still?f.still:f,state=pose.state,style=pose.style,stale=!preview&&(now-lastTickAt>800||now-received>800),t=still?0:f.timeMs/1000;
|
||||
if(a.environment){ctx.strokeStyle=readable(a.background);ctx.globalAlpha=.12;ctx.lineWidth=.7;ctx.beginPath();ctx.moveTo(20,26);ctx.lineTo(w-20,26);ctx.stroke();for(let lane=0;lane<6;lane++){ctx.globalAlpha=.025;ctx.beginPath();for(let x=0;x<=w;x+=8){const y=h*.85+Math.sin(x/110+lane+t*.18)*7+lane*5;x?ctx.lineTo(x,y):ctx.moveTo(x,y)}ctx.stroke()}}ctx.globalAlpha=1;
|
||||
@@ -77,10 +80,11 @@ const complete=demoDone&&!small,scale=Math.min(w*.44,h*(complete?.18:.34)),ox=w*
|
||||
const light=a.background[0]*.2126+a.background[1]*.7152+a.background[2]*.0722>145;
|
||||
const radius=Math.max(.6,Math.min(w,h)*.0022)*a.dotScale,hollow=stale||!f.producerConnected||state.observed<.92;
|
||||
const fallback=[style.r,style.g,style.b,style.alpha],local=preview||small;
|
||||
const pixel=pose.bodyStyle==='pixel',morph=pose.morph??1;if(pixel&&pixelWhale.complete&&pixelWhale.naturalWidth){ctx.globalAlpha=(1-morph)*Math.min(1,a.brightness)*(hollow?.45:1)*(stale?.55:1);ctx.imageSmoothingEnabled=false;ctx.filter=hollow?'grayscale(1)':'none';ctx.drawImage(pixelSource(a),ox-scale*.8341,oy-scale*.8341,scale*1.6682,scale*1.6682);ctx.filter='none';ctx.globalAlpha=1;}
|
||||
for(let i=0;i<pose.points.length;i++){const q=pose.points[i],p=previous?.points[i],x=ox+(p&&!still?p[0]+(q[0]-p[0])*mix:q[0])*scale,y=oy+(p&&!still?p[1]+(q[1]-p[1])*mix:q[1])*scale,m=pose.materials?.[i]||fallback;
|
||||
let color=m.slice(0,3),alpha=m[3];if(local){if(!a.eventColors)color=a.particle;else if(light){const peak=Math.max(...color,1);color=color.map(v=>Math.round(v*Math.min(1,115/peak)))}alpha=Math.min(1,alpha*a.brightness/(preview?1:f.appearance?.brightness||1))}if(hollow)alpha=Math.min(alpha,.48);if(stale)alpha*=.55;
|
||||
let color=m.slice(0,3),alpha=m[3];if(local){if(!a.eventColors)color=a.particle;else if(light){const peak=Math.max(...color,1);color=color.map(v=>Math.round(v*Math.min(1,115/peak)))}alpha=Math.min(1,alpha*a.brightness/(preview?1:f.appearance?.brightness||1))}if(pixel&&pixelWhale.complete&&pixelWhale.naturalWidth)alpha*=morph;if(hollow)alpha=Math.min(alpha,.48);if(stale)alpha*=.55;
|
||||
const material='rgb('+color.map(Math.round).join(',')+')',depth=.8+.2*Math.min(1,alpha);ctx.fillStyle=ctx.strokeStyle=material;ctx.globalAlpha=alpha;ctx.lineWidth=Math.max(.7,radius*.6);ctx.beginPath();ctx.arc(x,y,radius*depth,0,Math.PI*2);hollow?ctx.stroke():ctx.fill();if(!hollow&&!light&&a.glow>0&&i%5===0){ctx.globalAlpha=alpha*.035*a.glow;ctx.beginPath();ctx.arc(x,y,radius*(1+a.glow*4),0,Math.PI*2);ctx.fill()}}ctx.globalAlpha=1;}
|
||||
function draw(now){requestAnimationFrame(draw);if(document.hidden)return;const dt=lastDraw?Math.min(.1,(now-lastDraw)/1000):0;lastDraw=now;if(preview&&pet){accumulator+=dt;let count=0;while(accumulator>=1/30&&count++<3){advancePreview();accumulator-=1/30}}if(document.body.classList.contains('gallery-mode')){if(++galleryTick%3===0)Array.from($('gallery').querySelectorAll('canvas')).forEach((c,i)=>paint(c,current,presets[i],now,true))}else paint(canvas,current,appearance,now);if(current){const p=$('still').checked?current.still:current,activity=current.activity,c=cue(activity),known=c&&(preview||now-lastTickAt<800),title=demoDone?'Settling back into the habitat.':known?c.label:'A moment between tasks.',detail=demoDone?'Preview complete':known?((preview?'Simulated action':'Observed action')+(c.actionId?' · '+c.actionId:'')):'Activity unobserved',evidence=preview?'Simulated preview':known?'Observed in Codewhale':'Local companion';for(const [id,text] of [['activity-title',title],['activity-detail',detail],['activity-evidence',evidence]])if($(id).textContent!==text)$(id).textContent=text;$('activity-parallel').textContent=known&&activity.parallelAgentCount?activity.parallelAgentCount+' parallel agent'+(activity.parallelAgentCount===1?'':'s')+' active':'';$('caption').textContent=(preview?'Preview · ':'')+'Codewhale'+(current.state.observed<.92?' · unobserved':'')+(demoDone?' · complete':'');$('clock').textContent=Math.floor(current.timeMs/60000)+':'+String(Math.floor(current.timeMs/1000)%60).padStart(2,'0');canvas.setAttribute('aria-label',title+'; '+detail+'; '+$('caption').textContent+'; '+(selected==='Custom'?'custom':selected)+' appearance')}}
|
||||
function draw(now){requestAnimationFrame(draw);if(document.hidden)return;const dt=lastDraw?Math.min(.1,(now-lastDraw)/1000):0;lastDraw=now;if(preview&&pet){accumulator+=dt;let count=0;while(accumulator>=1/30&&count++<3){advancePreview();accumulator-=1/30}}if(document.body.classList.contains('gallery-mode')){if(++galleryTick%3===0)Array.from($('gallery').querySelectorAll('canvas')).forEach((c,i)=>paint(c,current,presets[i],now,true))}else paint(canvas,current,appearance,now);if(current){const p=$('still').checked?current.still:current,activity=current.activity,c=cue(activity),known=c&&(preview||now-lastTickAt<800),title=demoDone?'Settling back into the habitat.':known?c.label:'A moment between tasks.',detail=demoDone?'Preview complete':known?((preview?'Simulated action':'Observed action')+(c.actionId?' · '+c.actionId:'')):'Activity unobserved',evidence=preview?'Simulated preview':known?'Observed in Codewhale':'Local companion';for(const [id,text] of [['activity-title',title],['activity-detail',detail],['activity-evidence',evidence]])if($(id).textContent!==text)$(id).textContent=text;const phrase=$('activity-phrase');phrase.hidden=!known||!current.displayPhrase;phrase.lang=current.displayLanguage||'en';if(phrase.textContent!==(current.displayPhrase||''))phrase.textContent=current.displayPhrase||'';$('activity-parallel').textContent=known&&activity.parallelAgentCount?activity.parallelAgentCount+' parallel agent'+(activity.parallelAgentCount===1?'':'s')+' active':'';$('caption').textContent=(preview?'Preview · ':'')+'Codewhale'+(current.state.observed<.92?' · unobserved':'')+(demoDone?' · complete':'');$('clock').textContent=Math.floor(current.timeMs/60000)+':'+String(Math.floor(current.timeMs/1000)%60).padStart(2,'0');canvas.setAttribute('aria-label',title+'; '+detail+'; '+$('caption').textContent+'; '+(current.displayPhrase?current.displayPhrase+'; '+current.displayLanguage+'; ':'')+(selected==='Custom'?'custom':selected)+' appearance')}}
|
||||
async function connect(){if(preview){await makePreview();return}const token=location.hash.slice(1);history.replaceState(null,'',location.pathname);if(token){if(!/^[a-f0-9]{64}$/i.test(token))throw Error('Invalid local connection');await request('/v1/attach',{}, {Authorization:'Bearer '+token})}$('join').hidden=true;$('message').textContent='One live pet. Appearance changes are shared; preview work stays isolated.';await poll()}
|
||||
requestAnimationFrame(draw);connect().catch(e=>{$('message').textContent=e.message});
|
||||
</script></html>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The original 980-point whale remains the canonical body of one persistent
|
||||
world. Its owner now prepares a living-glyph presentation from admitted Engine
|
||||
activity: the dots form readable Chinese characters and moving tools, then
|
||||
activity: the dots form the selected language’s existing activity phrases, then
|
||||
return to the whale. This presentation uses the existing world clock; it does
|
||||
not replace the simulation, recording format or score. Original recordings
|
||||
retain their expression-version behavior.
|
||||
@@ -33,33 +33,65 @@ on that clock. It does not step the world, consume randomness or modify the
|
||||
canonical checkpoint. The fixed `[-1, 1]` envelope keeps scale stable across
|
||||
forms; consumers fit that envelope rather than fitting each changing shape.
|
||||
|
||||
The presentation samples 980 interior points from the existing filled pet mark,
|
||||
preserving its eye, mouth and curled negative space at home. During activity,
|
||||
all 980 points become the prepared activity character at the whale's full
|
||||
footprint; tool use is the two-character `工具`.
|
||||
The smooth presentation samples 980 interior points from the existing filled
|
||||
pet mark, preserving its eye, mouth and curled negative space at home. The
|
||||
optional pixel presentation derives 980 colored drone positions from the
|
||||
approved 192-cell pixel whale. The browser paints its lossless pixel mark at
|
||||
rest, fading it into those drones as they travel into words; terminal views
|
||||
paint the same prepared drone geometry. Both styles share the world and clock. During activity,
|
||||
all 980 points become the prepared activity phrase in the whale's fixed
|
||||
footprint. Chinese phrases of up to three characters use one line; longer
|
||||
phrases use two lines. English wraps at spaces; Japanese wraps at phrase
|
||||
boundaries. Every language keeps the same envelope. Ordinary captions retain
|
||||
the full meaning in the interface language.
|
||||
Every occupied glyph-mask pixel receives a particle, preserving stroke detail
|
||||
in compact views. No separate tool icon overlays the character.
|
||||
|
||||
An 8.4-second cycle returns all 980 points to the filled whale between
|
||||
expressions, using the presentation's admission timestamp rather than its
|
||||
heartbeats. Completion holds its character for 800 ms and settles into the
|
||||
whale within 1.7 seconds. The
|
||||
materials stay within an ocean palette, with warm glyph accents for waiting or
|
||||
observed failure. Still presents the full-size character during activity and
|
||||
whale within 1.7 seconds. The materials stay within an ocean palette, with warm glyph accents for waiting or
|
||||
observed failure. Still presents the full-size phrase during activity and
|
||||
the filled whale at rest, without the cycle or swim.
|
||||
`build-dot-glyphs.py --whale-body` reproducibly derives the
|
||||
interior sampling from the pinned alpha mask in `pet/public/whale.png`.
|
||||
|
||||
The glyphs are `读 写 搜 试 行 览 控 记 工具 思 答 协 待 成`: reading, editing,
|
||||
searching, testing, executing, browsing, computer use, memory, tool,
|
||||
thinking, responding, delegating, waiting and completion. Flowing ink and light
|
||||
express the action within the character: reading scans rows, searching moves a
|
||||
The generator reads the existing tool and phase labels from the English,
|
||||
Simplified Chinese, Traditional Chinese and Japanese locale packs, using the
|
||||
same typed activity mapping as the terminal caption. The Simplified Chinese
|
||||
phrases are: `读取 编辑 搜索 验证 运行 使用浏览器 操作电脑 检索上下文
|
||||
调用工具 思考 回复 智能体工作中 等你处理 完成`. Updating that locale and regenerating
|
||||
the masks keeps the pet's wording consistent with the interface. Labels never
|
||||
fall back to a single-character abbreviation. Flowing ink and light
|
||||
express the action within the phrase: reading scans rows, searching moves a
|
||||
spotlight, writing traces across strokes, memory draws light inward and
|
||||
delegating runs concurrent bands. Glyph masks in `dot-glyphs.ts` are generated
|
||||
by `pet/scripts/build-dot-glyphs.py`; font provenance and license are in
|
||||
`pet/assets/dot-glyphs-LICENSE.txt`. Captions carry the meaning independently of
|
||||
color, motion and the viewer's ability to read a small glyph.
|
||||
|
||||
Whale style and large-text language are independent saved appearance settings.
|
||||
`/pet style smooth|pixel` selects the body; `/pet language auto|en|zh-Hans|zh-Hant|ja`
|
||||
selects the phrases. The appearance studio exposes the same choices. `auto`
|
||||
follows the active producer’s interface locale; other interface languages use
|
||||
English large text until masks are available. An English reader can select
|
||||
Chinese or Japanese large text while keeping an English caption for its meaning.
|
||||
These preferences do not change the Engine language, session, score, checkpoint,
|
||||
recording or audio. Old appearance files default to smooth and auto. An older
|
||||
running owner keeps its existing producer contract; style and language changes
|
||||
report that the companion needs to be updated and restarted.
|
||||
|
||||
`pet/assets/pixel-whale.svg` is the approved exploration 34 (192-cell grid),
|
||||
with the same aurora palette and silhouette as the smooth brand mark. Its
|
||||
SHA-256 is `9d3742fbf325aedc18435d7b15e89178b8684ccbb40cd55a32e60bed156de700`.
|
||||
`build-dot-glyphs.py --pixel-body` verifies that source and derives the drone
|
||||
positions. The lossless transparent 1024px WebP is generated from that SVG
|
||||
with ImageMagick: `magick -background none -density 384 pet/assets/pixel-whale.svg
|
||||
-filter point -resize 1024x1024 -define webp:lossless=true pet/public/pixel-whale.webp`.
|
||||
`build-pet.mjs` checks that the embedded terminal image and native cores match
|
||||
their canonical sources. Custom particle colors also tint the browser’s pixel
|
||||
mark; selecting a color preset preserves style and language.
|
||||
|
||||
The Engine admits `activity_kind` and optional `action_id` on operation start
|
||||
and completion. The projection exposes `activityKind` and `actionId`, including
|
||||
active spans and the latest failed-tool receipt. An action ID is a bounded
|
||||
@@ -77,7 +109,11 @@ apply the appearance settings again. A carrier may still dim a stale/offline
|
||||
view and adapt ink to terminal capabilities. Reduced motion selects the owner's
|
||||
`still` pose rather than running a second simulation or pausing mid-morph.
|
||||
|
||||
GPUI and the Ratatui `DotWhale` component consume prepared geometry. The product
|
||||
GPUI and the Ratatui `DotWhale` component consume prepared geometry. The 0.10.2
|
||||
terminal's `PetMode` now passes its fresh, session-bound prepared pose into
|
||||
`DotWhale`; reduced motion selects the prepared Still pose. A missing or stale
|
||||
pose falls back to the native whale, with the current localized shell status.
|
||||
The normal caption and streamed-reply inspector remain available. The product
|
||||
web app has a guarded prepared-frame adapter and an opt-in particle painter,
|
||||
but its Runtime/control-plane transport is not wired to those frames yet. The
|
||||
owner's authenticated loopback browser companion is a separate connected
|
||||
|
||||
Reference in new issue
Block a user