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:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-10-11 02:12:40 -07:00
1 parent 3360a6d38d
commit b0f9cbc4d2
28 files changed
+4354 -802

No files matched your search

+9
View File
@@ -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
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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 अपडेट करने के बाद पेट को फिर से शुरू करें।"
}
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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 を更新してペットを再起動すると、クジラのスタイルと文字の言語を選べます。"
}
+5 -1
View File
@@ -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을 업데이트한 뒤 펫을 다시 시작하면 고래 스타일과 텍스트 언어를 선택할 수 있습니다."
}
+5 -1
View File
@@ -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."
}
+5 -1
View File
@@ -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 перезапустите его, чтобы выбрать стиль кита и язык текста."
}
+5 -1
View File
@@ -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 перезапустіть його, щоб вибрати стиль кита й мову тексту."
}
+5 -1
View File
@@ -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."
}
+13 -9
View File
@@ -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 后重启宠物,即可选择鲸鱼样式和文字语言。"
}
+13 -9
View File
@@ -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 後重新啟動寵物,即可選擇鯨魚樣式和文字語言。"
}
+8
View File
@@ -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,
+9
View File
@@ -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(),
}
}
}
+98 -12
View File
@@ -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 { &[] };
+50 -12
View File
@@ -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();
+15 -2
View File
@@ -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()),
+27 -2
View File
@@ -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

+19 -15
View File
@@ -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>
+49 -13
View File
@@ -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