From 8b5d55a2576fbd3e585f9aaa946e908f1b310112 Mon Sep 17 00:00:00 2001 From: Lucas1479 Date: Thu, 22 Oct 2026 16:46:06 +1300 Subject: [PATCH 1/8] fix(gui): inline chat rename, Markdown lists and console translations Rename chats inline in the history rail. Electron does not implement window.prompt, so double-click rename threw; the test stub hid it. Restore Markdown list markers removed by Tailwind preflight and stop preserving source newlines in assistant Markdown, which added blank lines between blocks. The assistant avatar falls back to the active role's initial instead of a fixed K. System messages and errors use theme tokens. Translate the remaining console text in Chat, Work activity, artifacts and the capability overview. Capability details become i18n templates with detailVars, and implementation labels are translated part by part, which retires five whole-string keys. Remove the unused ModelBar and SessionList. Tests cover conditional t() keys, every capability projection in a configuration grid, and visible text on host elements. Co-Authored-By: Claude Opus 5.5 --- electron/src/renderer/activeCharacter.tsx | 5 + .../src/renderer/components/BackendPage.tsx | 4 +- .../renderer/components/CapabilitiesPanel.tsx | 26 ++- .../components/ChatAvatarSettings.tsx | 4 +- .../src/renderer/components/ChatBubble.tsx | 14 +- electron/src/renderer/components/ChatPage.tsx | 37 ++--- .../renderer/components/ChatSessionRail.tsx | 81 ++++++++-- .../components/ChatWorkActivityCard.tsx | 16 +- electron/src/renderer/components/ModelBar.tsx | 62 ------- .../renderer/components/ProjectAppsPanel.tsx | 48 +++--- .../src/renderer/components/SessionList.tsx | 54 ------- .../renderer/components/WorkFilesPanel.tsx | 26 +-- .../components/sceneCapabilityProjection.ts | 25 ++- electron/src/renderer/locales/zh-CN.json | 152 +++++++++++++++++- electron/src/renderer/styles/index.css | 37 ++++- electron/tests/characterWorkspace.test.mjs | 2 +- electron/tests/chatSelectionFeedback.test.mjs | 85 +++++++++- electron/tests/i18nCatalog.test.mjs | 110 ++++++++++++- tests/test_session_display_boundary.py | 4 +- 19 files changed, 556 insertions(+), 236 deletions(-) delete mode 100644 electron/src/renderer/components/ModelBar.tsx delete mode 100644 electron/src/renderer/components/SessionList.tsx diff --git a/electron/src/renderer/activeCharacter.tsx b/electron/src/renderer/activeCharacter.tsx index 125751eb..4abe518f 100644 --- a/electron/src/renderer/activeCharacter.tsx +++ b/electron/src/renderer/activeCharacter.tsx @@ -40,3 +40,8 @@ export function useActiveCharacterSnapshot(connected: boolean, send: Send) { export function useActiveCharacter() { return useContext(ActiveCharacterContext) } + +/** Avatar fallback for the assistant: the active role's first character, or A for Amadeus. */ +export function characterInitial(character: ActiveCharacter | null): string { + return Array.from(character?.short_name || '')[0] || 'A' +} diff --git a/electron/src/renderer/components/BackendPage.tsx b/electron/src/renderer/components/BackendPage.tsx index 2d1dbdb8..727846fe 100644 --- a/electron/src/renderer/components/BackendPage.tsx +++ b/electron/src/renderer/components/BackendPage.tsx @@ -196,8 +196,8 @@ export default function BackendPage({ projections, send, subscribe, connected, r {(renderStatus || wallpaperStatus) && (
- {renderStatus ?
Render: {renderStatus}
: null} - {wallpaperStatus ?
Wallpaper: {wallpaperStatus}
: null} + {renderStatus ?
{t('Render')}: {renderStatus}
: null} + {wallpaperStatus ?
{t('Wallpaper')}: {wallpaperStatus}
: null}
)} diff --git a/electron/src/renderer/components/CapabilitiesPanel.tsx b/electron/src/renderer/components/CapabilitiesPanel.tsx index 534fa2af..622411ac 100644 --- a/electron/src/renderer/components/CapabilitiesPanel.tsx +++ b/electron/src/renderer/components/CapabilitiesPanel.tsx @@ -93,6 +93,18 @@ function statusLabel(state: SceneCapabilityState, explicit?: string): string { return explicit || STATE_LABELS[state] } +type Translate = ReturnType['t'] + +// Implementation labels join independent parts with " · "; each part is its own term. +function translateParts(t: Translate, value: string): string { + return value.split(' · ').map(part => t(part)).join(' · ') +} + +// Detail variables are terms too ("on demand", "full screen"); identifiers stay as written. +function translateDetail(t: Translate, detail: string, vars?: Record): string { + return t(detail, Object.fromEntries(Object.entries(vars || {}).map(([key, value]) => [key, t(value)]))) +} + function compactStatusLabel(state: SceneCapabilityState): string { if (state === 'attention') return 'Needs attention' if (state === 'inactive') return 'Off' @@ -188,7 +200,7 @@ function Overview({ capabilities, onOpenSection }: Props) {
{t('Implementation')} - {t(capability.implementation)} + {translateParts(t, capability.implementation)}
@@ -239,7 +251,7 @@ function SceneStatus({ capabilities, onOpenSection }: Props) {
{t(diagnostic.capability)} - {t(diagnostic.status)} · {t(diagnostic.detail)} + {t(diagnostic.status)} · {diagnostic.detail}
@@ -289,24 +301,24 @@ function SceneStatus({ capabilities, onOpenSection }: Props) {
{items.map(({ capability, usage }) => ( -
+
{t(capability.label)} - {t(usage.detail)} + {translateDetail(t, usage.detail, usage.detailVars)}
-
+
diff --git a/electron/src/renderer/components/ChatAvatarSettings.tsx b/electron/src/renderer/components/ChatAvatarSettings.tsx index 96345b00..ad3a4489 100644 --- a/electron/src/renderer/components/ChatAvatarSettings.tsx +++ b/electron/src/renderer/components/ChatAvatarSettings.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { useI18n } from '../i18n' import FluentIcon from './FluentIcon' -import { useActiveCharacter } from '../activeCharacter' +import { characterInitial, useActiveCharacter } from '../activeCharacter' type ChatAvatarRole = 'user' | 'assistant' type ChatAvatars = { user: string; assistant: string } @@ -73,7 +73,7 @@ export default function ChatAvatarSettings() { { role: 'user', title: t('Your avatar'), detail: 'Shown beside your messages', fallback: 'U' }, { role: 'assistant', title: activeCharacter ? t('{name} avatar', { name: activeCharacter.short_name }) : t('Assistant avatar'), - detail: 'Shown beside Amadeus responses', fallback: Array.from(activeCharacter?.short_name || '')[0] || 'A' }, + detail: 'Shown beside Amadeus responses', fallback: characterInitial(activeCharacter) }, ] return ( diff --git a/electron/src/renderer/components/ChatBubble.tsx b/electron/src/renderer/components/ChatBubble.tsx index 11f9671c..8eaa532f 100644 --- a/electron/src/renderer/components/ChatBubble.tsx +++ b/electron/src/renderer/components/ChatBubble.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react' import ReactMarkdown from 'react-markdown' +import { characterInitial, useActiveCharacter } from '../activeCharacter' +import { useI18n } from '../i18n' interface Props { role: 'user' | 'assistant' | 'system' @@ -15,6 +17,8 @@ interface Props { const DOT_PHASES = ['●', '● ●', '● ● ●', '● ●'] export default function ChatBubble({ role, text, translation = '', streaming, onStreamIdle, userAvatar = '', assistantAvatar = '', hasPreviousMessage }: Props) { + const { t } = useI18n() + const activeCharacter = useActiveCharacter() const [display, setDisplay] = useState(text) const [dotPhase, setDotPhase] = useState(0) const [cursorVisible, setCursorVisible] = useState(true) @@ -65,7 +69,7 @@ export default function ChatBubble({ role, text, translation = '', streaming, on className="flex justify-center px-[18px]" style={{ marginTop: hasPreviousMessage ? 10 : 0 }} > - + {text}
@@ -98,7 +102,7 @@ export default function ChatBubble({ role, text, translation = '', streaming, on > {avatar ? - : isUser ? 'U' : 'K'} + : isUser ? 'U' : characterInitial(activeCharacter)}
{/* Keep short messages compact while long messages wrap within the chat lane. */} @@ -110,8 +114,10 @@ export default function ChatBubble({ role, text, translation = '', streaming, on maxWidth: '70%', }} > + {/* User text is shown verbatim; assistant Markdown lays out its own blocks, so + preserving its newlines would add blank lines between paragraphs and lists. */}
{display}} {!isUser && translation ? (
[...prev, { role: 'system', - text: persisted + text: t(persisted ? 'Default model saved for the next backend start; the current conversation still uses the previous model.' - : 'Model switch failed; wait for the current reply to finish and try again.', + : 'Model switch failed; wait for the current reply to finish and try again.'), }]) } - }, [send]) + }, [send, t]) const handleNewSession = useCallback(async () => { try { @@ -1252,8 +1252,9 @@ export default function ChatPage({ send, subscribe, connected, renderActive, ren } }, [activeSession, loadSession, selectSession, sessions]) - const handleRenameSession = useCallback(async (id: string, currentTitle: string) => { - const title = window.prompt('Rename session', currentTitle)?.trim() + // The rail edits titles inline; Electron does not implement window.prompt. + const handleRenameSession = useCallback(async (id: string, nextTitle: string) => { + const title = nextTitle.trim() if (!title) return try { await runSessionSelection({ pending: 0 }, () => {}, @@ -1286,12 +1287,12 @@ export default function ChatPage({ send, subscribe, connected, renderActive, ren const visualButtonDisabled = !connected || !canUseMultimodal const visualButtonTitle = !canUseMultimodal - ? 'Select an image-capable model, such as DeepSeek Flash, OpenAI, or Gemini' + ? t('Select an image-capable model, such as DeepSeek Flash, OpenAI, or Gemini') : visionVideoMode - ? 'Global vision watching is on. Hold to turn it off. Right-click to choose a window.' + ? t('Global vision watching is on. Hold to turn it off. Right-click to choose a window.') : pendingVisualAttachment - ? `${pendingVisualAttachment.name} will be sent with the next message. Click to choose another image; hold for watching; right-click for window.` - : 'Choose an image for the next message. Hold for watching; right-click for window.' + ? t('{name} will be sent with the next message. Click to choose another image; hold for watching; right-click for window.', { name: pendingVisualAttachment.name }) + : t('Choose an image for the next message. Hold for watching; right-click for window.') const activitiesByTurn = useMemo(() => { const grouped = new Map() @@ -1433,7 +1434,7 @@ export default function ChatPage({ send, subscribe, connected, renderActive, ren className="group flex items-center shrink-0" aria-label={t('Current chat project')} title={activeContext?.projectId - ? `This chat is bound to ${activeContext.projectName}` + ? t('This chat is bound to {name}', { name: activeContext.projectName }) : t('This is a default Draft chat')} style={{ height: 28, @@ -1551,7 +1552,7 @@ export default function ChatPage({ send, subscribe, connected, renderActive, ren ))}
{attentionError && ( -

{attentionError}

+

{attentionError}

)}
)} @@ -1589,7 +1590,7 @@ export default function ChatPage({ send, subscribe, connected, renderActive, ren > - Interacting with Amadeus about {artifactContext.title} + {t('Interacting with Amadeus about')} {artifactContext.title}