From 1b24718906cce73a9ddf9fdaf2e5c6c00394b90a Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 10:25:06 +0800 Subject: [PATCH 01/20] fix(ui): give the task rail's status dot one meaning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dot the rail draws for a session passed through two lossy hops and three disagreeing sources, so it could not say what it meant. Collapse the mapping. `SessionStatus -> SessionStatusTone -> StatusDotVariant` becomes one table from status to Astryx's variant. Two of the seven tones had no distinct outcome at the end of that chain, and the collapse is what let `waiting_for_user` and `blocked` share `warning`: blocked now maps to `error`, so "cannot proceed until you fix a connection" and "holding a question for you" stop rendering alike. Read the running authority. `runningTurnIds` is the runtime's projection of the runs it holds; `session.ts` states why a persisted `status` cannot serve that purpose, and `settledSessionTransientIds` already reads it first over the same list. The row read neither -- a renderer-local streaming set, then the stored `status` -- so a task running under a bot channel or a second window read as idle. The streaming set stays below it, for the gap between this renderer sending a turn and the host reporting it back. Delete what nothing wrote. `review` and `done` were never written by anything, in any version, so no stored record can carry them and no reader of them was reachable; they leave `SESSION_STATUSES`, the wire enum, and the copy tables. `SessionLifecycleStatus` now aliases `SessionStatus` instead of restating it -- three hand-written copies of one enum is how those two values survived in two of them after the last writer went away. Delete the tautological filter. `SessionFilter` had one value left: `archived` moved to Settings in #2985 and `flagged` was never selected, which left a control whose answer is always the same answer. The 「会话」 row that selected it goes with it, as does the branch in `sessionMatchesNavSelection`. A stored filter is dropped rather than validated, which is the migration. Also gone: `SessionStatusPresentation.interactive`, which had no reader, and a doc comment naming a `SessionStatusIcon` and a chat-header badge as the tone matrix's consumers -- neither exists. `status: 'archived'` still duplicates `isArchived`. Consolidating those two rewrites stored rows, so it is #2984's PR 3, not this one. Refs #2984 Generated-by: Claude Code --- .../__tests__/app-shell-session-purge.test.ts | 2 +- .../src/renderer/app-shell-chat-actions.ts | 2 +- .../src/renderer/app-shell-e2e-fixture.ts | 2 +- apps/desktop/src/renderer/app-shell.tsx | 14 +-- .../src/renderer/command-palette-commands.ts | 2 +- apps/desktop/src/renderer/nav-selection.ts | 19 +-- .../src/renderer/session-nav-filter.ts | 22 ++-- .../renderer/session-status-presentation.ts | 39 +++--- apps/desktop/stories/app-shell.stories.tsx | 4 +- .../settings/settings-pages.stories.tsx | 2 +- .../stories/subagent-sessions.stories.tsx | 2 +- .../__tests__/run-session-selection.test.ts | 2 +- packages/core/src/session.ts | 12 +- .../src/protocol/session-continuity.ts | 30 ++--- packages/ui/src/components.tsx | 1 - packages/ui/src/conversation-copy.ts | 4 +- packages/ui/src/nav-selection.ts | 13 +- packages/ui/src/session-history-list.tsx | 114 ++++++++---------- packages/ui/src/session-sidebar-nav.tsx | 10 -- .../ui/src/session-status-presentation.ts | 48 ++++++-- packages/ui/src/shell-controls-copy.ts | 3 - .../ui/stories/session-list-panel.stories.tsx | 15 +-- 22 files changed, 165 insertions(+), 197 deletions(-) diff --git a/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts b/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts index 2ed8dcfd37..95c2258c76 100644 --- a/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts +++ b/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts @@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial = {}): SessionSu isArchived: true, labels: [], hasUnread: false, - status: 'done', + status: 'archived', backend: 'fake', llmConnectionSlug: 'test', connectionLocked: true, diff --git a/apps/desktop/src/renderer/app-shell-chat-actions.ts b/apps/desktop/src/renderer/app-shell-chat-actions.ts index 06dc12950c..83c35ef9a7 100644 --- a/apps/desktop/src/renderer/app-shell-chat-actions.ts +++ b/apps/desktop/src/renderer/app-shell-chat-actions.ts @@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: { showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation); } if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) { - setNavSelection({ section: 'sessions', filter: 'chats' }); + setNavSelection({ section: 'sessions' }); setActiveId(session.id); showOptimisticUserMessage( session.id, diff --git a/apps/desktop/src/renderer/app-shell-e2e-fixture.ts b/apps/desktop/src/renderer/app-shell-e2e-fixture.ts index 1d65d6b556..f21784c742 100644 --- a/apps/desktop/src/renderer/app-shell-e2e-fixture.ts +++ b/apps/desktop/src/renderer/app-shell-e2e-fixture.ts @@ -134,7 +134,7 @@ export function createAppShellE2eFixtureActions(options: { } else if (state.sidebarSection === 'daily-review') { setNavSelection({ section: 'automations', module: 'daily-review' }); } else if (state.sidebarSection === 'sessions') { - setNavSelection({ section: 'sessions', filter: 'chats' }); + setNavSelection({ section: 'sessions' }); } } diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index d71c178d2b..ecfe1e1d5c 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -121,7 +121,7 @@ import { useAppShellTurnPresentation } from './app-shell-turn-view-model'; import { readScrollMotionBehavior } from './scroll-motion-policy'; import { deriveBranchBanner } from './branch-banner'; import { readNavigationState, selectNavigation } from './nav-selection'; -import { sessionMatchesNavSelection } from './session-nav-filter'; +import { sessionMatchesRail } from './session-nav-filter'; import { deriveSessionRevisionNavigation } from './session-revisions'; import { deriveDesktopExecutionBoundarySurface } from './desktop-execution-boundary-surface'; import { useActiveExecutionBoundary } from './use-active-execution-boundary'; @@ -699,11 +699,9 @@ function AppShellContent({ } = useMemo( () => deriveSessionRail(sessions, activeId, (session) => - !hiddenCompanionForkIds.has(session.id) - ? sessionMatchesNavSelection(session, navSelection) - : false, + !hiddenCompanionForkIds.has(session.id) && sessionMatchesRail(session), ), - [sessions, activeId, navSelection, hiddenCompanionForkIds], + [sessions, activeId, hiddenCompanionForkIds], ); // PR-DAILY-REVIEW-MVP-0: bridge for the main Daily Review module. // Memoized so the panel's `useEffect` cleanup keys @@ -1089,7 +1087,7 @@ function AppShellContent({ } function openSessionInChat(sessionId: string, turnId?: string, sequence?: number): void { - setNavSelection({ section: 'sessions', filter: 'chats' }); + setNavSelection({ section: 'sessions' }); setActiveId(sessionId); if (turnId) { setSearchScrollTarget({ sessionId, turnId, sequence, nonce: Date.now() }); @@ -1127,7 +1125,7 @@ function AppShellContent({ * path is unchanged while a half-written message is no longer clobbered. */ const useSkillInChat = useCallback( (_skillId: string, skillName: string) => { - setNavSelection({ section: 'sessions', filter: 'chats' }); + setNavSelection({ section: 'sessions' }); const seed = () => { composerRef.current?.appendText(shellCopy.useSkillPrompt(skillName)); composerRef.current?.focus(); @@ -2494,7 +2492,7 @@ function AppShellContent({ function openNewTaskSurface() { startNewSession(); setNewChatPlanModeActive(false); - setNavSelection({ section: 'sessions', filter: 'chats' }); + setNavSelection({ section: 'sessions' }); setSearchScrollTarget(null); // New-task affordances reset to the empty-state composer; move focus // there so the user can start typing immediately. diff --git a/apps/desktop/src/renderer/command-palette-commands.ts b/apps/desktop/src/renderer/command-palette-commands.ts index 6adf258327..18312694da 100644 --- a/apps/desktop/src/renderer/command-palette-commands.ts +++ b/apps/desktop/src/renderer/command-palette-commands.ts @@ -234,7 +234,7 @@ export function buildCommandList(args: { ...staticCopy('nav:sessions'), Icon: MessageSquare, keywords: [...copy.staticKeywords['nav:sessions']], - run: () => select({ section: 'sessions', filter: 'chats' }), + run: () => select({ section: 'sessions' }), }); cmds.push({ id: 'nav:automations', diff --git a/apps/desktop/src/renderer/nav-selection.ts b/apps/desktop/src/renderer/nav-selection.ts index b0503870c4..942ba3329b 100644 --- a/apps/desktop/src/renderer/nav-selection.ts +++ b/apps/desktop/src/renderer/nav-selection.ts @@ -3,7 +3,6 @@ import type { ExtensionModule, NavModuleMemory, NavSelection, - SessionFilter, } from '@maka/ui'; import { safeLocalStorageGet } from './browser-storage.js'; @@ -19,18 +18,11 @@ const DEFAULT_MODULE_MEMORY: NavModuleMemory = { function defaultNavigationState(): NavigationState { return { - selection: { section: 'sessions', filter: 'chats' }, + selection: { section: 'sessions' }, moduleMemory: { ...DEFAULT_MODULE_MEMORY }, }; } -function isSessionFilter(value: unknown): value is SessionFilter { - // A stored `archived` — written while the rail still had that filter row — - // fails here, and `parseSelection` falls back to the default `chats`. That is - // the migration: the destination it named no longer exists. - return value === 'chats' || value === 'flagged'; -} - function isExtensionModule(value: unknown): value is ExtensionModule { return value === 'skills' || value === 'mcp'; } @@ -41,10 +33,11 @@ function isAutomationModule(value: unknown): value is AutomationModule { function parseSelection(value: unknown): NavSelection | null { if (!value || typeof value !== 'object') return null; - const candidate = value as { section?: unknown; filter?: unknown; module?: unknown }; - if (candidate.section === 'sessions' && isSessionFilter(candidate.filter)) { - return { section: 'sessions', filter: candidate.filter }; - } + const candidate = value as { section?: unknown; module?: unknown }; + // Any stored `filter` is dropped rather than validated: `archived` named a + // destination that moved to Settings (#2985) and `flagged` was never written, + // so every stored value maps to the one session section that exists (#2984). + if (candidate.section === 'sessions') return { section: 'sessions' }; if (candidate.section === 'extensions' && isExtensionModule(candidate.module)) { return { section: 'extensions', module: candidate.module }; } diff --git a/apps/desktop/src/renderer/session-nav-filter.ts b/apps/desktop/src/renderer/session-nav-filter.ts index 2f7b2f73d1..9a6ec31110 100644 --- a/apps/desktop/src/renderer/session-nav-filter.ts +++ b/apps/desktop/src/renderer/session-nav-filter.ts @@ -1,15 +1,13 @@ import type { SessionSummary } from '@maka/core/session'; -import type { NavSelection } from '@maka/ui'; -export function sessionMatchesNavSelection( - session: SessionSummary, - selection: NavSelection, -): boolean { - const filter = selection.section === 'sessions' ? selection.filter : 'chats'; - switch (filter) { - case 'flagged': - return session.isFlagged && !session.isArchived; - case 'chats': - return !session.isArchived; - } +/** + * Which sessions the rail lists. Archived tasks are managed in Settings › 活动 › + * 已归档任务 (#2985), so the rail shows everything else. + * + * This used to switch on `NavSelection.filter`. That filter is gone (#2984): its + * last two values were a destination that moved to Settings and a value nothing + * ever selected, which left one branch reachable — this one. + */ +export function sessionMatchesRail(session: SessionSummary): boolean { + return !session.isArchived; } diff --git a/apps/desktop/src/renderer/session-status-presentation.ts b/apps/desktop/src/renderer/session-status-presentation.ts index f8e8ad4149..d711ce8e8d 100644 --- a/apps/desktop/src/renderer/session-status-presentation.ts +++ b/apps/desktop/src/renderer/session-status-presentation.ts @@ -1,35 +1,26 @@ /** - * Pure presentation helpers for SessionStatus + SessionBlockedReason - * used by the sidebar and chat header. + * Renderer-side presentation helpers for SessionStatus, SessionBlockedReason, + * and failed-turn recovery. * - * Separated from the React component layer so the copy + tone mapping - * can be unit-tested without a DOM, mirroring `session-health-notice.ts` - * pattern. + * Separated from the React component layer so the mapping can be unit-tested + * without a DOM, mirroring the `session-health-notice.ts` pattern. * - * Two contracts enforced here: + * One contract enforced here: **generalized blocked-reason copy** (@kenji + * review). UI labels never expose the raw `SessionBlockedReason` enum string; + * `describeBlockedReason` is the canonical translation, and a new blocked reason + * must extend the core enum AND that matrix together or the `unknown` fallback + * applies. * - * 1. **Generalized blocked-reason copy** (@kenji review): UI labels - * never expose the raw `SessionBlockedReason` enum string. The - * mapping below is the canonical translation. New blocked reasons - * must extend the core enum AND this matrix together, or the - * `unknown` fallback applies. - * - * 2. **Status tone matrix**: each SessionStatus has a single visual - * tone (`accent / warning / destructive / info / success / muted`) - * consumed by both the SessionStatusIcon and the chat-header - * status badge. Aligns with the existing session-health-notice tone - * vocabulary. + * The status → dot mapping itself lives in `@maka/ui`; it is re-exported below + * rather than restated. A second contract used to be documented here — a tone + * matrix "consumed by both the SessionStatusIcon and the chat-header status + * badge" — describing two consumers that do not exist and a tone layer that has + * since been removed (#2984). */ import { SANDBOX_BOUNDARY_RESTART_CLOSURE_CLASS } from '@maka/core/sandbox-boundary'; -import type { SessionBlockedReason, SessionStatus, SessionSummary } from '@maka/core/session'; +import type { SessionBlockedReason, SessionSummary } from '@maka/core/session'; import type { UiLocale } from '@maka/core/ui-locale'; -import { - describeBlockedReason, - presentSessionStatus, - type SessionStatusPresentation, - type SessionStatusTone, -} from '@maka/ui'; import { getDesktopConversationCopy } from './locales/conversation-copy.js'; import { describeSessionErrorReason } from './session-error-presentation.js'; export { presentSessionStatus } from '@maka/ui'; diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx index 1c83c9c237..45cfd3f0f8 100644 --- a/apps/desktop/stories/app-shell.stories.tsx +++ b/apps/desktop/stories/app-shell.stories.tsx @@ -97,7 +97,7 @@ const sidebarSessions: SessionSummary[] = [ makeSession({ id: 'session-active', name: '整理 Storybook 表面覆盖', lastMessageAt: NOW - 14 * 60_000, hasUnread: true, projectId: 'project-maka', cwd: '/workspace/maka-agent/.worktree/storybook' }), makeSession({ id: 'session-waiting', name: '等待权限确认的部署任务', status: 'waiting_for_user', lastMessageAt: NOW - 8 * 60_000, projectId: 'project-docs', cwd: '/workspace/docs' }), makeSession({ id: 'session-pinned', name: 'PR #435 发布风险清单', lastMessageAt: NOW - 76 * 60_000, isFlagged: true, projectId: 'project-maka', cwd: '/workspace/maka-agent' }), - makeSession({ id: 'session-review', name: '已完成的 smoke 回归', status: 'done', lastMessageAt: NOW - 3 * 60 * 60_000, projectId: 'project-archived', cwd: '/workspace/legacy' }), + makeSession({ id: 'session-aborted', name: '中止的 smoke 回归', status: 'aborted', lastMessageAt: NOW - 3 * 60 * 60_000, projectId: 'project-archived', cwd: '/workspace/legacy' }), ]; function project(input: Partial & Pick): ProjectRecord { @@ -386,7 +386,7 @@ function ComposedShell(props: { onWidthChange={noop} minWidth={180} maxWidth={480} - selection={{ section: 'sessions', filter: 'chats' }} + selection={{ section: 'sessions' }} sessions={sidebarRows} activeId={active?.id} groups={viewMode === 'project' ? projectGroups : undefined} diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx index 86ea9cf0ed..6373c74dfa 100644 --- a/apps/desktop/stories/settings/settings-pages.stories.tsx +++ b/apps/desktop/stories/settings/settings-pages.stories.tsx @@ -684,7 +684,7 @@ function archivedTask( isArchived: true, labels: [], hasUnread: false, - status: 'done', + status: 'archived', backend: 'ai-sdk', llmConnectionSlug: 'zai-live', connectionLocked: true, diff --git a/apps/desktop/stories/subagent-sessions.stories.tsx b/apps/desktop/stories/subagent-sessions.stories.tsx index 7d799beac5..faf1dee07c 100644 --- a/apps/desktop/stories/subagent-sessions.stories.tsx +++ b/apps/desktop/stories/subagent-sessions.stories.tsx @@ -357,7 +357,7 @@ function ProductRail(props: { activeSessionId: string }) { return (
{ { sessions: [ session({ id: 'archived', lastMessageAt: 500, isArchived: true }), - session({ id: 'done', lastMessageAt: 500, status: 'done' }), + session({ id: 'blocked', lastMessageAt: 500, status: 'blocked' }), session({ id: 'ask', lastMessageAt: 500, permissionMode: 'ask' }), session({ id: 'missing-time', lastMessageAt: undefined }), session({ id: 'inaccessible', cwd: '/missing', lastMessageAt: 400 }), diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index df6756de19..cbb6a83022 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -28,13 +28,21 @@ import type { SubagentWorkspaceBinding } from './subagent-workspace.js'; export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './explore-agent.js'; +/** + * `archived` is still here and still written by `SessionStore.archive()` + * alongside `isArchived`; consolidating those two onto one authority is its own + * change (#2984, PR 3) because it rewrites stored rows. + * + * `review` and `done` were removed: nothing in the codebase ever wrote them, + * and no stored record can carry them, so the values had no reader that was not + * also dead. Everything the runtime writes is here — `running`, `blocked`, + * `aborted`, `waiting_for_user` — plus `active` as the resting state. + */ export const SESSION_STATUSES = [ 'active', 'running', 'waiting_for_user', 'blocked', - 'review', - 'done', 'archived', 'aborted', ] as const; diff --git a/packages/runtime-host/src/protocol/session-continuity.ts b/packages/runtime-host/src/protocol/session-continuity.ts index 4278b977ba..51819d5f94 100644 --- a/packages/runtime-host/src/protocol/session-continuity.ts +++ b/packages/runtime-host/src/protocol/session-continuity.ts @@ -2,6 +2,7 @@ import { TOOL_ACTIVITY_KINDS, TOOL_OUTPUT_DELTA_MAX_CHARS } from '@maka/core/eve import type { ToolResultPreviewContent } from '@maka/core/events'; import { decodeToolResultPreviewContent } from '@maka/core/tool-result-preview'; import type { ToolActivityKind } from '@maka/core/events'; +import { isSessionStatus, type SessionStatus } from '@maka/core/session'; import { assertExactKeys, requireCount, @@ -41,15 +42,14 @@ export const SESSION_TOOL_NAME_MAX_BYTES = 256; export const SESSION_SUBSCRIPTION_FRAME_MAX_BYTES = 64 * 1024 - 1; export const SESSION_RUNTIME_RESOURCE_PTY_DATA_MAX_BYTES = 48 * 1024; -export type SessionLifecycleStatus = - | 'active' - | 'running' - | 'waiting_for_user' - | 'blocked' - | 'review' - | 'done' - | 'archived' - | 'aborted'; +/** + * The wire status is core's `SessionStatus`, not a restatement of it. It used to + * be a hand-written union here plus a hand-written validator below — three + * copies of one enum, which is how `review` and `done` survived in two of them + * after the last writer went away. `session-catalog.ts` already validates the + * same field with core's `isSessionStatus`. + */ +export type SessionLifecycleStatus = SessionStatus; export interface SessionContinuityIdentity { sessionId: string; @@ -964,17 +964,7 @@ function requireToolActivityKind(value: unknown): ToolActivityKind { } function requireSessionLifecycleStatus(value: unknown): SessionLifecycleStatus { - if ( - value === 'active' || - value === 'running' || - value === 'waiting_for_user' || - value === 'blocked' || - value === 'review' || - value === 'done' || - value === 'archived' || - value === 'aborted' - ) - return value; + if (isSessionStatus(value)) return value; throw invalidProtocolFrame('Invalid Session lifecycle status'); } diff --git a/packages/ui/src/components.tsx b/packages/ui/src/components.tsx index 4764383103..20e0d7c83e 100644 --- a/packages/ui/src/components.tsx +++ b/packages/ui/src/components.tsx @@ -3,7 +3,6 @@ export type { ExtensionModule, NavModuleMemory, NavSelection, - SessionFilter, } from './nav-selection.js'; export { CapabilityAuditStrip } from './capability-audit-strip.js'; export { ModuleHubSelector } from './module-hub-selector.js'; diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 60d094e775..358180675e 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -429,7 +429,7 @@ const CONVERSATION_COPY = { revisionVersionsAriaLabel: '对话版本', revisionVersion: (current, total) => `版本 ${current} / ${total}`, previousRevision: '查看上一版本', nextRevision: '查看下一版本', }, sessions: { - status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', review: '待审核', done: '已完成', archived: '已归档', aborted: '已中止' }, + status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', archived: '已归档', aborted: '已中止' }, blockedReason: { NO_REAL_CONNECTION: '等待配置可用模型连接', auth: '需要重新登录', permission_required: '等待权限确认', tool_failed: '工具调用失败', unknown: '运行中断,可重试' }, listAriaLabel: '对话列表', title: '会话', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多对话`, renameAriaLabel: '重命名对话', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '对话正在流式响应中', staleTitle: '此会话使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '会话已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: '对话操作', pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: '最近', groupByTime: '按时间', groupByProject: '按项目', groupingAriaLabel: '会话分组方式', projectActionsAriaLabel: (name) => `${name} 项目操作`, projectNewTask: '新建任务', projectRename: '重命名', projectArchive: '归档', projectRestore: '恢复', projectRelink: '重新定位', projectUnavailable: '项目目录不可用', archivedProjects: '已归档项目', archivedProjectsAriaLabel: '展开已归档项目', worktreeAriaLabel: 'Git 工作树', promptRailAriaLabel: '按提问跳转', emptyPrompt: '(空提问)', jumpToPrompt: (preview) => `跳到提问:${preview}`, }, @@ -567,7 +567,7 @@ const CONVERSATION_COPY = { revisionVersionsAriaLabel: 'Conversation versions', revisionVersion: (current, total) => `Version ${current} of ${total}`, previousRevision: 'View previous version', nextRevision: 'View next version', }, sessions: { - status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', review: 'Review', done: 'Done', archived: 'Archived', aborted: 'Stopped' }, + status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', archived: 'Archived', aborted: 'Stopped' }, blockedReason: { NO_REAL_CONNECTION: 'Waiting for an available model connection', auth: 'Sign in again', permission_required: 'Waiting for permission', tool_failed: 'Tool call failed', unknown: 'Run interrupted; retry available' }, listAriaLabel: 'Conversation list', title: 'Conversations', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more conversations`, renameAriaLabel: 'Rename conversation', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This conversation is streaming a response', staleTitle: 'This conversation\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale conversation', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: 'Conversation actions', pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Conversation grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, }, diff --git a/packages/ui/src/nav-selection.ts b/packages/ui/src/nav-selection.ts index c7fc3c7836..e0ba44e958 100644 --- a/packages/ui/src/nav-selection.ts +++ b/packages/ui/src/nav-selection.ts @@ -1,13 +1,18 @@ /** - * Archived tasks are not a rail filter. Cleaning them up is management, and it - * lives in Settings › 活动 › 已归档任务; the rail lists what you are working on. + * The rail's session section takes no filter. + * + * It used to carry `'chats' | 'flagged' | 'archived'`. Archived became Settings + * › 活动 › 已归档任务 (#2985) — cleaning tasks up is management, and the rail + * lists what you are working on. `flagged` never had a writer: nothing ever + * selected it, so the branch that filtered on it could not run. What was left + * was a one-value filter, which is the same tautology the 「会话」 row was: a + * control whose answer is always the same answer. */ -export type SessionFilter = 'chats' | 'flagged'; export type ExtensionModule = 'skills' | 'mcp'; export type AutomationModule = 'scheduled-tasks' | 'daily-review'; export type NavSelection = - | { section: 'sessions'; filter: SessionFilter } + | { section: 'sessions' } | { section: 'extensions'; module: ExtensionModule } | { section: 'automations'; module: AutomationModule }; diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index 5daa83c9fb..a85fc3749d 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -600,20 +600,16 @@ const SessionItemMeta = memo(function SessionItemMeta(props: { }) { const locale = useUiLocale(); const copy = getConversationCopy(locale).sessions; - const statusDot = resolveSessionStatusDot(props.session, props.streaming, props.active, locale); + const rowSignal = resolveSessionRowSignal(props.session, props.streaming, props.active, locale); - // Signal (status/unread) and action (MoreMenu) are orthogonal — same as - // project rows (badge + menu). No hover XOR state machine. - const signal = statusDot ? ( + const signal = rowSignal ? ( - ) : shouldShowSessionUnreadDot(props.session, props.streaming, props.active) ? ( - ) : null; return ( @@ -762,14 +758,33 @@ function SessionItemActions(props: { ); } -function resolveSessionStatusDot( +/** + * The row's one signal, resolved from the highest-priority thing true about the + * session: running › waiting for you › blocked › unread. `null` means the row + * draws nothing — idle, archived, and aborted are not states worth a dot. + * + * "Running" is read from `runningTurnIds`, the runtime's projection of the runs + * it is actually holding. The row used to read `streaming` first and then fall + * back to the persisted `status`, and neither is the authority: `session.ts` + * spells out that a stored `status` "can be left behind entirely by a crash", + * and `streaming` only knows about turns THIS renderer sent, so a task running + * under a bot channel or a second window read as idle. `streaming` stays, below + * `runningTurnIds`, for the one thing it is the authority on: the gap between + * this renderer sending a turn and the host reporting it back. + * + * Unread is last because it is the weakest claim on attention — a session that + * is running or holding a question already says something more specific about + * the same unread text. + */ +function resolveSessionRowSignal( session: SessionSummary, streaming: boolean, active: boolean, locale: UiLocale, ): { variant: StatusDotVariant; label: string; isPulsing?: boolean; tooltip?: string } | null { - if (streaming) { - const copy = getConversationCopy(locale).sessions; + const copy = getConversationCopy(locale).sessions; + + if (session.runningTurnIds?.length || streaming) { return { variant: 'accent', label: copy.respondingAriaLabel, @@ -778,64 +793,31 @@ function resolveSessionStatusDot( }; } - const status = session.status; - if (status === 'active' && !active) { - // Idle rows keep a quiet neutral dot (shell-side-nav pattern). - return null; + const { label, variant } = presentSessionStatus(session.status, locale); + if (variant) { + const blockedDetail = + session.status === 'blocked' && session.blockedReason + ? describeBlockedReason(session.blockedReason, locale) + : null; + return { + variant, + label, + // A `running` header with no live run reaching us: either the run ended + // without its status write landing, or this summary came from a mutation + // response, which describes the header alone and omits `runningTurnIds` + // (`session.ts`). Still pulsing — the row should not change shape based on + // which projection delivered it. + isPulsing: session.status === 'running', + tooltip: blockedDetail ? `${label} · ${blockedDetail}` : label, + }; } - if (status === 'active') return null; - - const { label, tone } = presentSessionStatus(status, locale); - const blockedDetail = - status === 'blocked' && session.blockedReason - ? describeBlockedReason(session.blockedReason, locale) - : null; - return { - variant: toneToStatusDotVariant(tone), - label, - isPulsing: status === 'running', - tooltip: blockedDetail ? `${label} · ${blockedDetail}` : label, - }; -} -function toneToStatusDotVariant( - tone: ReturnType['tone'], -): StatusDotVariant { - switch (tone) { - case 'accent': - return 'accent'; - case 'warning': - return 'warning'; - case 'destructive': - return 'error'; - case 'success': - return 'success'; - case 'info': - return 'accent'; - case 'muted': - case 'neutral': - default: - return 'neutral'; + if (!active && session.hasUnread) { + return { variant: 'accent', label: copy.unreadAriaLabel }; } + return null; } -function shouldShowSessionUnreadDot( - session: SessionSummary, - streaming: boolean, - active: boolean, -): boolean { - if (active) return false; - if (!session.hasUnread) return false; - if (streaming) return false; - return !SIDEBAR_UNREAD_SUPPRESSED_STATUSES.has(session.status); -} - -const SIDEBAR_UNREAD_SUPPRESSED_STATUSES = new Set([ - 'running', - 'waiting_for_user', - 'blocked', -]); - interface SessionGroup { id: 'pinned' | 'unpinned'; label: string; diff --git a/packages/ui/src/session-sidebar-nav.tsx b/packages/ui/src/session-sidebar-nav.tsx index f799d3ceb1..b92f0507be 100644 --- a/packages/ui/src/session-sidebar-nav.tsx +++ b/packages/ui/src/session-sidebar-nav.tsx @@ -3,7 +3,6 @@ import { AlertCircle, Blocks, Download, - MessageSquare, Settings, SquarePen, Timer, @@ -29,8 +28,6 @@ export function SessionSidebarNav(props: { const copy = getShellControlsCopy(locale).navigation; const extensionsActive = props.selection.section === 'extensions'; const automationsActive = props.selection.section === 'automations'; - const activeSessionFilter = - props.selection.section === 'sessions' ? props.selection.filter : undefined; const moduleMemory = props.moduleMemory ?? { extensions: 'skills', automations: 'scheduled-tasks' }; const activeScheduledTaskCount = (props.scheduledTasks ?? []).filter( (task) => task.status === 'active', @@ -59,13 +56,6 @@ export function SessionSidebarNav(props: { {props.onImport && ( )} - props.onSelect({ section: 'sessions', filter: 'chats' })} - /> > = { - active: { tone: 'neutral', interactive: true }, running: { tone: 'accent', interactive: true }, waiting_for_user: { tone: 'warning', interactive: true }, blocked: { tone: 'warning', interactive: true }, review: { tone: 'info', interactive: true }, done: { tone: 'success', interactive: true }, archived: { tone: 'muted', interactive: false }, aborted: { tone: 'muted', interactive: false }, +const STATUS_VARIANT: Record = { + active: undefined, + running: 'accent', + waiting_for_user: 'warning', + // `error`, not `warning`: blocked means the task cannot proceed until someone + // fixes a connection, a login, or a permission, while waiting_for_user means + // it is holding a question for you. Sharing one colour made the rail unable to + // say which of the two a row was in. + blocked: 'error', + archived: undefined, + aborted: undefined, }; -export function presentSessionStatus(status: SessionStatus, locale: UiLocale = 'zh'): SessionStatusPresentation { - return { ...STATUS_META[status], label: getConversationCopy(locale).sessions.status[status] }; +export function presentSessionStatus( + status: SessionStatus, + locale: UiLocale = 'zh', +): SessionStatusPresentation { + const variant = STATUS_VARIANT[status]; + return { + label: getConversationCopy(locale).sessions.status[status], + ...(variant ? { variant } : {}), + }; } -export function describeBlockedReason(reason: SessionBlockedReason | undefined, locale: UiLocale = 'zh'): string { +export function describeBlockedReason( + reason: SessionBlockedReason | undefined, + locale: UiLocale = 'zh', +): string { const copy = getConversationCopy(locale).sessions.blockedReason; return reason ? copy[reason] : copy.unknown; } diff --git a/packages/ui/src/shell-controls-copy.ts b/packages/ui/src/shell-controls-copy.ts index 38511e79a7..c719392d0f 100644 --- a/packages/ui/src/shell-controls-copy.ts +++ b/packages/ui/src/shell-controls-copy.ts @@ -8,7 +8,6 @@ type ShellControlsCopy = { mainLabel: string; newTask: string; importSession: string; - conversations: string; /** * Accessible name of the session-group header's new-task trigger * (session-history-list.tsx). Deliberately NOT `newTask`: that copy is @@ -51,7 +50,6 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { mainLabel: '主导航', newTask: '新任务', importSession: '导入会话', - conversations: '会话', groupNewTask: '新建任务', automations: '定时任务', extensions: '扩展', @@ -85,7 +83,6 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { mainLabel: 'Main navigation', newTask: 'New task', importSession: 'Import conversation', - conversations: 'Conversations', groupNewTask: 'New task in group', automations: 'Scheduled tasks', extensions: 'Extensions', diff --git a/packages/ui/stories/session-list-panel.stories.tsx b/packages/ui/stories/session-list-panel.stories.tsx index 4b4c320a1e..307e4788b5 100644 --- a/packages/ui/stories/session-list-panel.stories.tsx +++ b/packages/ui/stories/session-list-panel.stories.tsx @@ -75,7 +75,7 @@ function panelProps(input: { worktreeSessionIds?: SessionListPanelProps['worktreeSessionIds']; }): SessionListPanelProps { return { - selection: input.selection ?? { section: 'sessions', filter: 'chats' }, + selection: input.selection ?? { section: 'sessions' }, sessions: input.sessions, ...(input.activeId ? { activeId: input.activeId } : {}), ...(input.streamingSessionIds ? { streamingSessionIds: input.streamingSessionIds } : {}), @@ -174,18 +174,6 @@ const statusSessions = [ blockedReason: 'auth', lastMessageAt: NOW - 20 * 60 * 1000, }), - makeSession({ - id: 'status-review', - name: '待审核的文件 diff', - status: 'review', - lastMessageAt: NOW - 37 * 60 * 1000, - }), - makeSession({ - id: 'status-done', - name: '已完成的 smoke run', - status: 'done', - lastMessageAt: NOW - 2 * 60 * 60 * 1000, - }), makeSession({ id: 'status-archived', name: '归档的旧实验', @@ -284,7 +272,6 @@ export const PinnedAndRecentSections: Story = { makeSession({ id: 'recent-a', name: '刚结束的 smoke 回归', - status: 'done', lastMessageAt: NOW - 12 * 60 * 1000, }), makeSession({ From 850c34cdef5d323efb532d55907bc57f74c9d2b2 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 10:34:15 +0800 Subject: [PATCH 02/20] refactor(ui): rebuild the task row on two signal slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every task row in the rail now carries exactly two signal slots. Slot 1 is a single status dot, reserved as an 8px gutter even when there is nothing to show so titles stay aligned down the list. Slot 2 holds a compact timestamp at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is no longer permanently mounted next to every title. The dot's meaning is resolved once, in priority order: live run (accent, pulsing) › persisted status › unread (accent, inactive rows only). The live run reads `runningTurnIds` first, matching `settledSessionTransientIds` over the same session list — a persisted `running` status can be left behind by a crash, so it must not outrank the authoritative projection. The stale pill is gone: it duplicated the timestamp it sat next to. The worktree mark falls back to the row tooltip rather than competing for a slot. `RelativeTime` gains `variant="compact"` instead of a second component; a 260px rail row cannot fit the medium-date-plus-time past-horizon fallback. Generated-by: Claude Code --- apps/desktop/src/renderer/styles/sidebar.css | 96 +++++++++++---- packages/ui/src/relative-time.tsx | 22 +++- packages/ui/src/session-history-list.tsx | 117 +++++++------------ 3 files changed, 131 insertions(+), 104 deletions(-) diff --git a/apps/desktop/src/renderer/styles/sidebar.css b/apps/desktop/src/renderer/styles/sidebar.css index 47d438f770..6f8294fbea 100644 --- a/apps/desktop/src/renderer/styles/sidebar.css +++ b/apps/desktop/src/renderer/styles/sidebar.css @@ -42,12 +42,11 @@ * stay in step and an upgrade that restructures the header keeps working. * * Bound on the header, NOT on the section root. SideNavSection wraps the whole - * list body — every row, badge, and pill — in the same element as its title + * list body — every row, badge, and timestamp — in the same element as its title * (session-history-list.tsx), and custom properties inherit, so a rebind on the * root reaches all of it: measured live, the project session-count Badge went - * 12px → 14px, and `.maka-list-row-stale-pill` would have taken a 17.14px line - * box on its 12px font. The header is SideNavSection's first child and holds - * exactly the title and subtitle. + * 12px → 14px. The header is SideNavSection's first child and holds exactly the + * title and subtitle. * * Both halves of the tier move together. Size alone would leave the supporting * leading multiplier on a larger font and land the line box at 23.33px, off the @@ -259,7 +258,8 @@ } /* Fixed trailing column so StatusDot ↔ MoreMenu swaps do not reflow the label - * (hover used to steal ~28px and shove the title left). */ + * (hover used to steal ~28px and shove the title left). Project rows only; task + * rows use .maka-session-row-time below. */ .maka-session-row-trailing { display: inline-flex; align-items: center; @@ -269,6 +269,73 @@ min-height: var(--h-control-sm, 24px); } +/* + * Slot 1: the leading status gutter. + * + * Astryx's StatusDot is a fixed 8px box (StatusDot.tsx), so only the empty case + * needs declaring: a row with no dot must still hold the column, or titles would + * start at a different x depending on whether the task has a state. The dot is + * present or absent; the gutter is not. + */ +.maka-session-row .maka-session-row-signal-empty { + display: block; + width: 8px; + height: 8px; + flex: none; +} + +/* + * Slot 2: the timestamp at rest, the ⋯ menu on hover or keyboard focus. + * + * `visibility`, not `display`, and an absolutely positioned menu: the two never + * occupy layout at the same time and neither can reflow the title. The menu used + * to be mounted visible on every row, which is what made the trailing cluster + * five things wide. + * + * :focus-within covers the keyboard: focus on the row's own button reveals the + * menu, and focus moving into the menu keeps it revealed. + */ +.maka-session-row-time { + display: inline-flex; + align-items: center; + justify-content: flex-end; + flex: none; + min-width: var(--h-control-sm, 24px); + min-height: var(--h-control-sm, 24px); +} + +.maka-session-row-time-label { + color: var(--muted-foreground); + font: var(--typography-caption); + white-space: nowrap; +} + +.maka-session-row > .maka-session-row-action { + opacity: 0; +} + +.maka-session-row:hover > .maka-session-row-action, +.maka-session-row:focus-within > .maka-session-row-action { + opacity: 1; +} + +.maka-session-row:hover .maka-session-row-time, +.maka-session-row:focus-within .maka-session-row-time { + visibility: hidden; +} + +/* Nothing here can reveal the menu later, so it takes the slot outright and the + timestamp yields. */ +@media (hover: none) { + .maka-session-row > .maka-session-row-action { + opacity: 1; + } + + .maka-session-row .maka-session-row-time { + visibility: hidden; + } +} + /* SideNavItem owns each row button and only supports non-interactive * endContent. Action menus occupy a reserved trailing slot visually, but stay * siblings in the DOM so neither control contains the other. */ @@ -312,25 +379,6 @@ background-color: var(--color-overlay-pressed); } -.maka-session-row-trailing-spacer { - display: block; - width: 8px; - height: 8px; -} - -.maka-session-worktree-icon { - flex: 0 0 auto; - color: var(--muted-foreground); -} - -/* #1879: an Astryx `Badge variant="yellow"` now. Box, radius, padding and - caption type all come from the component (20px off `--spacing-5`), so the - only thing product CSS still owns is that it must not be squeezed by the - row's flex layout. */ -.maka-list-row-stale-pill { - flex-shrink: 0; -} - /* ⌘N hint on the 新任务 row — quiet text, no chip chrome. */ /* #1879: no chrome, but still a box, and it had no height of its own — measured, its own leading at 40px took it from 20px to 40px and pushed the diff --git a/packages/ui/src/relative-time.tsx b/packages/ui/src/relative-time.tsx index 83234fe3e7..8bbbf9f963 100644 --- a/packages/ui/src/relative-time.tsx +++ b/packages/ui/src/relative-time.tsx @@ -1,6 +1,10 @@ import { useEffect, useState } from 'react'; import { formatAbsoluteTimestamp } from './chat-display-helpers.js'; -import { formatRelativeTimestamp, nextRelativeRefreshDelay } from '@maka/core/relative-time'; +import { + formatCompactTimestamp, + formatRelativeTimestamp, + nextRelativeRefreshDelay, +} from '@maka/core/relative-time'; import { cn } from './utils.js'; import { useUiLocale } from './locale-context.js'; @@ -11,8 +15,19 @@ import { useUiLocale } from './locale-context.js'; * `nextRelativeRefreshDelay` so we tick every second within the first * minute, every minute within the first hour, then every 10 minutes; * past the 7-day horizon we stop ticking and show the absolute date. + * + * `variant="compact"` swaps the past-horizon fallback for a date-only label + * ("6月20日"), which is what `formatCompactTimestamp` exists for: the wide + * medium-date-plus-time fallback crushes a 260px rail row's title. The ticker is + * the same either way — a visible timestamp inside a memoized row is exactly the + * case that goes stale without it, and the rail's rows are memoized. */ -export function RelativeTime(props: { ts: number; className?: string; suppressTitle?: boolean }) { +export function RelativeTime(props: { + ts: number; + className?: string; + suppressTitle?: boolean; + variant?: 'relative' | 'compact'; +}) { const locale = useUiLocale(); const [, setTick] = useState(0); useEffect(() => { @@ -21,13 +36,14 @@ export function RelativeTime(props: { ts: number; className?: string; suppressTi const id = setTimeout(() => setTick((n) => n + 1), delay); return () => clearTimeout(id); }); + const format = props.variant === 'compact' ? formatCompactTimestamp : formatRelativeTimestamp; return ( ); } diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index a85fc3749d..c7ac0d24f2 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -11,13 +11,11 @@ import { useMountedRef } from './use-mounted-ref.js'; import type { ProjectRecord } from '@maka/core/project'; import type { SessionSummary } from '@maka/core/session'; import type { UiLocale } from '@maka/core/ui-locale'; -import { formatCompactTimestamp } from '@maka/core/relative-time'; import { ICON_SIZE, AlertTriangle, Archive, ArchiveRestore, - FolderGit2, FolderOpen, Pencil, Pin, @@ -27,6 +25,7 @@ import { SquarePen, Trash2, } from './icons.js'; +import { RelativeTime } from './relative-time.js'; import { Badge } from '@astryxdesign/core/Badge'; import { Tooltip } from '@astryxdesign/core/Tooltip'; import { MoreMenu } from '@astryxdesign/core/MoreMenu'; @@ -414,7 +413,8 @@ const SessionNavRow = memo(function SessionNavRow(props: { onStartRename(target: SessionRenameTarget, opener: HTMLElement | null): void; }) { const locale = useUiLocale(); - const metaTitle = formatSessionMeta(props.session, locale); + const copy = getConversationCopy(locale).sessions; + const signal = resolveSessionRowSignal(props.session, props.streaming, props.active, locale); return (
+ ) : ( +
@@ -1241,3 +1275,23 @@ export const ArchivedTasks: Story = { ), }; + +// Real path: 设置 → 导入任务 on a machine that has Codex installed. +export const ImportTasks: Story = { + decorators: [withSettingsBridge], + render: () => , +}; + +// Real path: the same page on a machine with no supported agent — the common +// case, and the one where the source switch and the filter would be chrome +// around nothing. +export const ImportTasksNoSource: Story = { + decorators: [withScopedMakaBridge({ + ...makaBridge, + externalSessions: { + ...makaBridge.externalSessions, + listSources: async () => ({ adapterIds: [] }), + }, + })], + render: () => , +}; diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index ca2dec6c80..487c49eb6e 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -58,6 +58,7 @@ export const SETTINGS_SECTIONS = [ // `maka://settings/
` is a public deep link, so the id names what // the page is rather than the noun it lives under. 'archived-tasks', + 'import-tasks', 'bot-chat', 'search', 'data', diff --git a/packages/ui/src/session-list-panel.tsx b/packages/ui/src/session-list-panel.tsx index 5b3985f9cb..aa60e3c92f 100644 --- a/packages/ui/src/session-list-panel.tsx +++ b/packages/ui/src/session-list-panel.tsx @@ -51,7 +51,6 @@ export function SessionListPanel(props: { updateReminder?: SidebarUpdateReminder; onOpenUpdate?(): void; onNew(): void; - onImport?(): void; rowActions?: SessionRowActions; }) { const copy = getConversationCopy(useUiLocale()).sessions; @@ -145,7 +144,6 @@ export function SessionListPanel(props: { moduleMemory={props.moduleMemory} onSelect={props.onSelect} onNew={props.onNew} - onImport={props.onImport} /> {groupingSwitch} diff --git a/packages/ui/src/session-sidebar-nav.tsx b/packages/ui/src/session-sidebar-nav.tsx index b92f0507be..c134a5d8ee 100644 --- a/packages/ui/src/session-sidebar-nav.tsx +++ b/packages/ui/src/session-sidebar-nav.tsx @@ -6,7 +6,6 @@ import { Settings, SquarePen, Timer, - Upload, } from './icons.js'; import type { NavModuleMemory, NavSelection } from './nav-selection.js'; import { useUiLocale } from './locale-context.js'; @@ -22,7 +21,6 @@ export function SessionSidebarNav(props: { moduleMemory?: NavModuleMemory; onSelect(selection: NavSelection): void; onNew(): void; - onImport?(): void; }) { const locale = useUiLocale(); const copy = getShellControlsCopy(locale).navigation; @@ -53,9 +51,6 @@ export function SessionSidebarNav(props: { onClick={props.onNew} endContent={} /> - {props.onImport && ( - - )} Date: Sat, 15 Aug 2026 10:58:07 +0800 Subject: [PATCH 05/20] =?UTF-8?q?refactor:=20name=20the=20thing=20a=20user?= =?UTF-8?q?=20works=20on=20=E4=BB=BB=E5=8A=A1=20/=20task=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The product called the same object 会话, 对话, session and conversation depending on which file you landed in. Code, protocol and storage keep `session` — that is a durable contract with a wire format behind it — but every string a user reads now says 任务 / task. The rule is what the noun REFERS TO, not the word: - The Maka entity a user opens, renames, archives and searches → 任务 / task. - Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a bot, "可用于对话的模型", "对话记录" as a message log. - Another agent's stored conversations stay 对话 / conversation. 导入任务 lists Codex's conversations and produces Maka tasks; collapsing both onto one word would erase exactly the distinction that page is about. - 侧边对话 stays: it is a chat beside a task, not a task in the rail. Two collisions had to be resolved rather than renamed. The per-task ledger was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos, which is what they always were. And the automations row read "定时任务,N 个 未完成任务", two different meanings of 任务 in one label; the count now says "N 条进行中". The keyboard sheet loses its ←/→ row. It documented cycling between 会话/已标记/已归档, a filter this PR removed along with `SessionFilter`. Generated-by: Claude Code --- .../e2e/composer-skill-invocation.spec.ts | 2 +- apps/desktop/e2e/session-workbar.spec.ts | 8 +- apps/desktop/e2e/settings.spec.ts | 4 +- apps/desktop/e2e/sidebar-project-row.spec.ts | 2 +- apps/desktop/e2e/streaming-remount.spec.ts | 8 +- ...app-shell-session-settings-actions.test.ts | 2 +- .../bot-incoming-project-cwd.test.ts | 2 +- .../src/main/__tests__/thread-search.test.ts | 2 +- apps/desktop/src/main/bot-incoming-main.ts | 10 +- apps/desktop/src/main/capability-snapshot.ts | 2 +- apps/desktop/src/main/chat-readiness.ts | 4 +- .../src/main/computer-use/status-item.ts | 2 +- .../main/e2e-fixture/scenarios-sessions.ts | 2 +- .../main/e2e-fixture/scenarios-settings.ts | 2 +- apps/desktop/src/main/notifications-policy.ts | 2 +- apps/desktop/src/main/search/thread-search.ts | 2 +- apps/desktop/src/preload/preload.ts | 4 +- .../src/renderer/agent-graph-panel.tsx | 4 +- .../src/renderer/locales/conversation-copy.ts | 120 ++++----- .../locales/permission-center-copy.ts | 4 +- .../locales/settings-daily-review-copy.ts | 8 +- .../renderer/locales/settings-data-copy.ts | 4 +- .../renderer/locales/settings-memory-copy.ts | 2 +- .../locales/settings-navigation-copy.ts | 8 +- .../locales/settings-preferences-copy.ts | 12 +- .../locales/settings-projects-copy.ts | 24 +- .../locales/settings-provider-copy.ts | 6 +- .../renderer/locales/settings-shared-copy.ts | 6 +- .../locales/settings-subagents-copy.ts | 8 +- .../renderer/locales/settings-usage-copy.ts | 4 +- .../locales/settings-web-search-copy.ts | 10 +- .../src/renderer/locales/shell-copy.ts | 240 +++++++++--------- .../stories/command-search.stories.tsx | 16 +- .../stories/session-workbar.stories.tsx | 24 +- packages/cli/src/pi-transcript.ts | 2 +- packages/cli/src/pi-tui-runner.ts | 2 +- .../src/server/scheduled-task-coordinator.ts | 4 +- .../src/__tests__/session-manager.test.ts | 4 +- packages/runtime/src/session-manager.ts | 6 +- packages/ui/src/conversation-copy.ts | 62 ++--- packages/ui/src/daily-review-copy.ts | 22 +- packages/ui/src/runtime-resume-copy.ts | 6 +- packages/ui/src/scheduled-task-copy.ts | 12 +- packages/ui/src/session-rename-dialog.tsx | 4 +- packages/ui/src/shared-ui-copy.ts | 32 +-- packages/ui/src/shell-controls-copy.ts | 24 +- packages/ui/stories/model-picker.stories.tsx | 6 +- .../ui/stories/session-list-panel.stories.tsx | 2 +- 48 files changed, 371 insertions(+), 377 deletions(-) diff --git a/apps/desktop/e2e/composer-skill-invocation.spec.ts b/apps/desktop/e2e/composer-skill-invocation.spec.ts index d807d86ea2..b63da95f1d 100644 --- a/apps/desktop/e2e/composer-skill-invocation.spec.ts +++ b/apps/desktop/e2e/composer-skill-invocation.spec.ts @@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({ await expect(workspaceChip).toContainText('Workspace Only'); await page.getByRole('button', { name: '展开侧边栏' }).click(); - const sidebar = page.getByRole('navigation', { name: '对话列表' }); + const sidebar = page.getByRole('navigation', { name: '任务列表' }); await sidebar.getByRole('button', { name: '新任务', exact: true }).click(); await expect(composer).toHaveText(''); diff --git a/apps/desktop/e2e/session-workbar.spec.ts b/apps/desktop/e2e/session-workbar.spec.ts index 1d21a2a232..4151e22bab 100644 --- a/apps/desktop/e2e/session-workbar.spec.ts +++ b/apps/desktop/e2e/session-workbar.spec.ts @@ -8,7 +8,7 @@ async function openGitChanges(page: Page) { await composer.fill('create review session'); await composer.press('Enter'); await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible(); - await page.getByRole('button', { name: '展开会话工作栏' }).click(); + await page.getByRole('button', { name: '展开任务工作栏' }).click(); await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible(); await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click(); return page.getByRole('region', { name: 'Git 变更' }); @@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker', const page = gitReviewWindow.page; const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' }); const panel = await openGitChanges(page); - await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible(); + await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible(); await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0); await page.getByRole('button', { name: '打开工作栏标签' }).click(); const picker = page.getByRole('list', { name: '打开工具' }); await expect(picker).toBeVisible(); - await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click(); - await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click(); + await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click(); + await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click(); await expect(panel).toBeVisible(); await expect(picker).not.toBeVisible(); diff --git a/apps/desktop/e2e/settings.spec.ts b/apps/desktop/e2e/settings.spec.ts index 0b87917d0b..65c675f11c 100644 --- a/apps/desktop/e2e/settings.spec.ts +++ b/apps/desktop/e2e/settings.spec.ts @@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page const identity = page.locator('[data-maka-contract="titlebar-identity"]'); await expect(identity).toBeVisible(); await page.getByRole('button', { name: '展开侧边栏' }).click(); - await identity.getByRole('button', { name: /重命名对话/ }).click(); - await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings'); + await identity.getByRole('button', { name: /重命名任务/ }).click(); + await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings'); // Programmatic activation preserves input focus, matching the macOS // application-menu command that opens Settings before Chromium can blur it. diff --git a/apps/desktop/e2e/sidebar-project-row.spec.ts b/apps/desktop/e2e/sidebar-project-row.spec.ts index 4f8fe7b2de..168fefad0c 100644 --- a/apps/desktop/e2e/sidebar-project-row.spec.ts +++ b/apps/desktop/e2e/sidebar-project-row.spec.ts @@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async ( await page.keyboard.press('Escape'); await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible(); - const sidebar = page.getByRole('navigation', { name: '对话列表' }); + const sidebar = page.getByRole('navigation', { name: '任务列表' }); await sidebar.getByRole('radio', { name: '按项目', exact: true }).click(); const projectRow = sidebar.locator( diff --git a/apps/desktop/e2e/streaming-remount.spec.ts b/apps/desktop/e2e/streaming-remount.spec.ts index 81aa04f385..f4a20d23c3 100644 --- a/apps/desktop/e2e/streaming-remount.spec.ts +++ b/apps/desktop/e2e/streaming-remount.spec.ts @@ -18,11 +18,13 @@ test('remounting a live surface leaves accumulated output settled', async ({ const liveBubble = page.locator('.maka-bubble-streaming'); await expect(liveBubble).toContainText(accumulatedOutput); - const sidebar = page.getByRole('navigation', { name: '对话列表' }); + const sidebar = page.getByRole('navigation', { name: '任务列表' }); await sidebar.getByRole('button', { name: '扩展' }).click(); await expect(page.locator('[data-module="skills"]')).toBeVisible(); await expect(liveBubble).toHaveCount(0); - await sidebar.getByRole('button', { name: '会话', exact: true }).click(); + // Back through the task's own row: the rail's 「会话」 row was a section + // selector for the only section the list has, so it is gone (#2984). + await sidebar.locator('[data-session-id]').first().click(); await expect(liveBubble).toHaveCount(1); await expect(liveBubble).toContainText(accumulatedOutput); @@ -75,7 +77,7 @@ test('returning to a live conversation settles output accumulated while away', a const liveBubble = page.locator('.maka-bubble-streaming'); await expect(liveBubble).toContainText(accumulatedOutput); - const sidebar = page.getByRole('navigation', { name: '对话列表' }); + const sidebar = page.getByRole('navigation', { name: '任务列表' }); await page.getByRole('button', { name: '展开侧边栏' }).click(); await expect(page.locator('[data-agents-page]')).toHaveAttribute( 'data-sidebar-state', diff --git a/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts b/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts index a26297bcf0..760d6a2ea8 100644 --- a/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts +++ b/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts @@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => { assert.deepEqual(harness.successes, [ { - title: '已切换当前会话模型', + title: '已切换当前任务模型', description: 'claude-haiku → claude-opus', }, ]); diff --git a/apps/desktop/src/main/__tests__/bot-incoming-project-cwd.test.ts b/apps/desktop/src/main/__tests__/bot-incoming-project-cwd.test.ts index e0eef7ec61..d874c07fd5 100644 --- a/apps/desktop/src/main/__tests__/bot-incoming-project-cwd.test.ts +++ b/apps/desktop/src/main/__tests__/bot-incoming-project-cwd.test.ts @@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => { } as unknown as BotIncomingMessage); assert.deepEqual(createInput, { - name: 'Telegram 对话', + name: 'Telegram 任务', labels: ['bot', 'telegram'], }); }); diff --git a/apps/desktop/src/main/__tests__/thread-search.test.ts b/apps/desktop/src/main/__tests__/thread-search.test.ts index 456055475d..c5ec7779ba 100644 --- a/apps/desktop/src/main/__tests__/thread-search.test.ts +++ b/apps/desktop/src/main/__tests__/thread-search.test.ts @@ -215,7 +215,7 @@ describe('runThreadSearch', () => { await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)), )[0]!; assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' }); - assert.equal(titleHit.summary, '会话标题'); + assert.equal(titleHit.summary, '任务标题'); assert.equal(titleHit.url, undefined); assert.match(titleHit.snippet ?? '', /\[redacted\]/); assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false); diff --git a/apps/desktop/src/main/bot-incoming-main.ts b/apps/desktop/src/main/bot-incoming-main.ts index c038f7a61b..69a54feeea 100644 --- a/apps/desktop/src/main/bot-incoming-main.ts +++ b/apps/desktop/src/main/bot-incoming-main.ts @@ -183,7 +183,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps): noticeTtlMs: number, ): Promise { if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) { - await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs); + await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs); return undefined; } if (!consumeBotConversationToken(conversationKey)) { @@ -191,7 +191,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps): return undefined; } const sessionId = await deps.sessions.createSession({ - name: `${botDisplayLabel(message.platform)} 对话`, + name: `${botDisplayLabel(message.platform)} 任务`, labels: ['bot', message.platform], }); botConversationSessions.set(conversationKey, sessionId); @@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps): ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS, }; const ack = had - ? '会话已重置,下一条消息会开新对话。' - : '当前没有进行中的对话;下一条消息会开新对话。'; + ? '任务已重置,下一条消息会开新任务。' + : '当前没有进行中的任务;下一条消息会开新任务。'; await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null); return; } @@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps): if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true; await sendTransientBotNotice( message, - 'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。', + 'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。', noticeTtlMs, ); return false; diff --git a/apps/desktop/src/main/capability-snapshot.ts b/apps/desktop/src/main/capability-snapshot.ts index 9c0c94bfe5..4acf8748f6 100644 --- a/apps/desktop/src/main/capability-snapshot.ts +++ b/apps/desktop/src/main/capability-snapshot.ts @@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: { feature: { state: 'partial', source: 'runtime', - reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制', + reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制', }, requiredPermissions: [ { id: 'screen_recording', required: false, status: permissions.screen_recording.status }, diff --git a/apps/desktop/src/main/chat-readiness.ts b/apps/desktop/src/main/chat-readiness.ts index 28174f87c4..36304b6b84 100644 --- a/apps/desktop/src/main/chat-readiness.ts +++ b/apps/desktop/src/main/chat-readiness.ts @@ -132,7 +132,7 @@ function messageForReason( return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`; } case 'fake_backend': - return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。'; + return '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。'; case 'missing_default_connection': case 'connection_missing': // These reasons are handled before we reach isConnectionReady, @@ -147,7 +147,7 @@ export async function assertSessionCanSend( ): Promise { if (header.backend === 'fake') { throw chatConfigurationError( - '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。', + '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。', 'fake_backend', ); } diff --git a/apps/desktop/src/main/computer-use/status-item.ts b/apps/desktop/src/main/computer-use/status-item.ts index 94608adb9c..7a603953cf 100644 --- a/apps/desktop/src/main/computer-use/status-item.ts +++ b/apps/desktop/src/main/computer-use/status-item.ts @@ -114,7 +114,7 @@ const COPY: UiCatalog = { zh: { stopUsing: (appName) => `停止操作 ${appName}`, stopUnnamed: '停止 Computer Use', - empty: '没有正在进行的会话', + empty: '没有正在进行的任务', }, en: { stopUsing: (appName) => `Stop Using ${appName}`, diff --git a/apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts b/apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts index 539e194a66..9045aea86b 100644 --- a/apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts +++ b/apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts @@ -17,7 +17,7 @@ export function longSidebarSessions( return { header: header({ id: sessionId, - name: `会话 ${suffix}`, + name: `任务 ${suffix}`, connection: 'zai-live', model: 'glm-5.1', now, diff --git a/apps/desktop/src/main/e2e-fixture/scenarios-settings.ts b/apps/desktop/src/main/e2e-fixture/scenarios-settings.ts index fc1fd97f29..fc2d855667 100644 --- a/apps/desktop/src/main/e2e-fixture/scenarios-settings.ts +++ b/apps/desktop/src/main/e2e-fixture/scenarios-settings.ts @@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P for (const [index, title] of [ '每日站会前汇总阻塞项', '每月依赖许可证审计', - '季度收尾清点未归档会话', + '季度收尾清点未归档任务', '发布前跑一轮回归', ].entries()) { await create( diff --git a/apps/desktop/src/main/notifications-policy.ts b/apps/desktop/src/main/notifications-policy.ts index 72995d59e2..87a8d88924 100644 --- a/apps/desktop/src/main/notifications-policy.ts +++ b/apps/desktop/src/main/notifications-policy.ts @@ -58,7 +58,7 @@ export interface RunNotificationCopy { */ function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy { if (kind === 'errored') { - return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' }; + return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' }; } return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' }; } diff --git a/apps/desktop/src/main/search/thread-search.ts b/apps/desktop/src/main/search/thread-search.ts index b9dcc31ecf..c8b4e39fd5 100644 --- a/apps/desktop/src/main/search/thread-search.ts +++ b/apps/desktop/src/main/search/thread-search.ts @@ -202,7 +202,7 @@ export async function runThreadSearch( results.push({ source: THREAD_SOURCE, title: session.name, - summary: '会话标题', + summary: '任务标题', snippet, target: { kind: 'thread', diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 8c95941771..4b1bf2ae40 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -421,7 +421,7 @@ function executeWebSearchQuery(input: { return Promise.resolve({ ok: false, reason: 'unsupported_provider', - message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。', + message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。', }); } const query = normalizeWebSearchQuery(input.query); @@ -448,7 +448,7 @@ function executeWebSearchTest(input: { return Promise.resolve({ ok: false, reason: 'unsupported_provider', - message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。', + message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。', }); } const apiKey = webSearchCredentialOverride(input.apiKey); diff --git a/apps/desktop/src/renderer/agent-graph-panel.tsx b/apps/desktop/src/renderer/agent-graph-panel.tsx index 58a4026a91..d6d0f61227 100644 --- a/apps/desktop/src/renderer/agent-graph-panel.tsx +++ b/apps/desktop/src/renderer/agent-graph-panel.tsx @@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy { stopping: '停止中…', stopFailed: '停止 Graph 失败,请重试。', loadFailed: 'Graph 状态刷新失败。', - openSession: '打开子会话', + openSession: '打开子任务', operators: 'Operators', selectedResults: '已选择结果', noOperators: '等待主 Agent 创建 operator…', @@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy { stopping: 'Stopping…', stopFailed: 'Could not stop the graph. Try again.', loadFailed: 'Could not refresh graph state.', - openSession: 'Open child session', + openSession: 'Open child task', operators: 'Operators', selectedResults: 'Selected results', noOperators: 'Waiting for the main agent to create an operator…', diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index 3e9ed6caf9..61aa27268a 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -315,8 +315,8 @@ const ZH_CALL_KIND: CallKindCopy = { semantic_compact: '语义压缩', history_compact: '历史压缩', goal_evaluation: '目标评估', - session_title: '生成会话标题', - session_recap: '会话回顾', + session_title: '生成任务标题', + session_recap: '任务回顾', daily_review: '每日回顾', }; @@ -325,8 +325,8 @@ const EN_CALL_KIND: CallKindCopy = { semantic_compact: 'Semantic compaction', history_compact: 'History compaction', goal_evaluation: 'Goal evaluation', - session_title: 'Session title', - session_recap: 'Session recap', + session_title: 'Task title', + session_recap: 'Task recap', daily_review: 'Daily review', }; @@ -367,7 +367,7 @@ function enDetail(parts: readonly string[]): string { const COPY = { zh: { - actions: { stopFailedTitle: '停止失败', stopFailedFallback: '会话操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新会话列表失败', refreshSessionsFailedFallback: '刷新会话列表失败,请稍后重试。', conversationErrorTitle: '对话出错', conversationErrorFallback: '对话运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新会话 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原对话仍会保留;修改后发送将在新版本中继续', revisionReadyTitle: '可以修改并重发了', revisionReadyDescription: '已回到该消息之前;编辑后发送即可', revisionUnavailableTitle: '暂时无法编辑这条消息', revisionAttachmentsUnsupported: '包含附件的历史消息暂不支持编辑并重发,请复制文字后新建消息。', revisionTransformedTextUnsupported: '通过显式技能发送的历史消息暂不支持编辑并重发,请复制文字后重新选择技能。', revisionDraftAttachmentConflict: 'Composer 中已有待发送附件,请先发送或移除附件,再编辑历史消息。', revisionCommandUnsupported: '修改消息时不能执行 /compact、/side 或编排命令,请取消修改后再试。', revisionAlreadyActive: '已有一条消息正在修改,请先发送或取消当前修改。', revisionCancelLabel: '取消', revisionBannerTitle: '正在修改已发送消息', revisionBannerDetail: '· 发送后创建新版本', revisionUnchanged: '内容没有变化。如需重新回答,请使用“重新生成”。', operationFailedTitle: '操作失败', operationFailedFallback: '对话操作失败,请稍后重试。', attachmentFailedTitle: '添加附件失败', tryAgain: '请稍后重试。', modelReboundTitle: '已切换到可用模型', modelReboundDescription: (modelId) => `原会话使用的连接已不可用${modelId ? ` · ${modelId}` : ''}`, messageReadFailedTitle: '读取对话失败', returnLatest: '返回最新消息' }, + actions: { stopFailedTitle: '停止失败', stopFailedFallback: '任务操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新任务列表失败', refreshSessionsFailedFallback: '刷新任务列表失败,请稍后重试。', conversationErrorTitle: '任务出错', conversationErrorFallback: '任务运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新任务 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原任务仍会保留;修改后发送将在新版本中继续', revisionReadyTitle: '可以修改并重发了', revisionReadyDescription: '已回到该消息之前;编辑后发送即可', revisionUnavailableTitle: '暂时无法编辑这条消息', revisionAttachmentsUnsupported: '包含附件的历史消息暂不支持编辑并重发,请复制文字后新建消息。', revisionTransformedTextUnsupported: '通过显式技能发送的历史消息暂不支持编辑并重发,请复制文字后重新选择技能。', revisionDraftAttachmentConflict: 'Composer 中已有待发送附件,请先发送或移除附件,再编辑历史消息。', revisionCommandUnsupported: '修改消息时不能执行 /compact、/side 或编排命令,请取消修改后再试。', revisionAlreadyActive: '已有一条消息正在修改,请先发送或取消当前修改。', revisionCancelLabel: '取消', revisionBannerTitle: '正在修改已发送消息', revisionBannerDetail: '· 发送后创建新版本', revisionUnchanged: '内容没有变化。如需重新回答,请使用“重新生成”。', operationFailedTitle: '操作失败', operationFailedFallback: '任务操作失败,请稍后重试。', attachmentFailedTitle: '添加附件失败', tryAgain: '请稍后重试。', modelReboundTitle: '已切换到可用模型', modelReboundDescription: (modelId) => `原任务使用的连接已不可用${modelId ? ` · ${modelId}` : ''}`, messageReadFailedTitle: '读取任务失败', returnLatest: '返回最新消息' }, attachments: { tooMany: '附件数量超过 8 个', tooLarge: '附件大小超过 50MB', duplicate: '附件来源重复,请勿重复添加同一文件。' }, model: { fakeBackendLabel: '本地模拟连接', @@ -376,25 +376,25 @@ const COPY = { configurationFallback: '模型连接暂时无法用于发送,请到 设置 · 模型 检查后重试。', configurationReason: { missing_default_connection: '等待配置默认模型。请到 设置 · 模型 添加一个可用模型连接后再发送。', - connection_missing: '该会话依赖的模型连接已删除,请到 设置 · 模型 重新选择或重建连接。', + connection_missing: '该任务依赖的模型连接已删除,请到 设置 · 模型 重新选择或重建连接。', connection_disabled: '当前模型连接已禁用。请到 设置 · 模型 启用或选择其他默认模型。', missing_api_key: '当前模型连接还没有可用凭据。请到 设置 · 模型 补齐 API key 或重新登录后再发送。', missing_model: '当前模型连接还没有可用模型。请到 设置 · 模型 选择默认模型后再发送。', empty_model_list: '当前模型连接没有启用模型。请到 设置 · 模型 添加或启用模型后再发送。', - model_not_enabled: '当前会话选择的模型未启用。请到 设置 · 模型 重新选择可用模型后再发送。', - model_not_chat_capable: '当前会话选择的模型不能用于聊天。请到 设置 · 模型 重新选择支持聊天的模型后再发送。', - fake_backend: '当前会话来自旧的本地模拟连接。请到 设置 · 模型 添加真实模型后新建会话。', + model_not_enabled: '当前任务选择的模型未启用。请到 设置 · 模型 重新选择可用模型后再发送。', + model_not_chat_capable: '当前任务选择的模型不能用于聊天。请到 设置 · 模型 重新选择支持聊天的模型后再发送。', + fake_backend: '当前任务来自旧的本地模拟连接。请到 设置 · 模型 添加真实模型后新建任务。', }, }, - footer: { labels: { regenerate: '重新生成', branch: '分支', copy: '复制', info: '详情' }, pending: '正在处理…', regenerateRunning: '当前回答仍在进行中,结束后再重新生成', regenerateAgain: '已重新生成过,再次点击将创建新的并行回答', regenerate: '让模型重新生成本轮回答', branchRunning: '当前回答仍在进行中,结束后再分支', branchAborted: '从中断前的上下文分支出新对话', branch: '基于此回答的上下文分支出新对话', copy: '复制回答到剪贴板', copyEmpty: '此回答尚无可复制的内容' }, + footer: { labels: { regenerate: '重新生成', branch: '分支', copy: '复制', info: '详情' }, pending: '正在处理…', regenerateRunning: '当前回答仍在进行中,结束后再重新生成', regenerateAgain: '已重新生成过,再次点击将创建新的并行回答', regenerate: '让模型重新生成本轮回答', branchRunning: '当前回答仍在进行中,结束后再分支', branchAborted: '从中断前的上下文分支出新任务', branch: '基于此回答的上下文分支出新任务', copy: '复制回答到剪贴板', copyEmpty: '此回答尚无可复制的内容' }, lineage: { regeneratedFrom: '重新生成自旧回答', regeneratedFromTooltip: '这是重新生成的并行回答,点击查看被保留的旧回答', regeneratedTo: '已重新生成 → 新回答', regeneratedToTooltip: '点击跳转到重新生成的新回答' }, workbar: { - ariaLabel: '会话工作栏', - sectionsAriaLabel: '会话工作栏标签', + ariaLabel: '任务工作栏', + sectionsAriaLabel: '任务工作栏标签', review: '变更', terminal: '终端', terminalNumbered: (index) => `终端 ${index}`, - tasks: '任务', + tasks: '待办', browser: '浏览器', files: '生成文件', inspector: '追踪', @@ -415,11 +415,11 @@ const COPY = { closeToRight: '关闭右侧标签', launcher: { review: '查看当前 Git 工作区变化', - terminal: '查看当前会话的终端运行和实时输出', - tasks: '查看和维护当前会话的任务台账', + terminal: '查看当前任务的终端运行和实时输出', + tasks: '查看和维护这个任务的待办台账', browser: '打开内置浏览器并保留当前页面', - files: '浏览当前会话生成的文件', - inspector: '检查会话调用、工具与耗时记录', + files: '浏览当前任务生成的文件', + inspector: '检查任务调用、工具与耗时记录', sideChat: '在不打断主任务的情况下追问和只读探索', }, }, @@ -427,8 +427,8 @@ const COPY = { ariaLabel: 'Git 变更', empty: '当前 Git 工作区没有变化', emptyHelp: '提交、暂存或修改文件后,变化会显示在这里。', - notGitRepository: '当前会话目录不是 Git 仓库', - workspaceUnavailable: '当前会话目录已不可用', + notGitRepository: '当前任务目录不是 Git 仓库', + workspaceUnavailable: '当前任务目录已不可用', unbornRepository: 'Git 仓库还没有可比较的提交', gitFailed: '无法读取 Git 工作区变化', invalidBaseBranch: '选择的比较分支已不可用', @@ -444,13 +444,13 @@ const COPY = { retry: '重试', }, terminalPanel: { - ariaLabel: '会话终端', - empty: '当前会话还没有终端运行', - emptyHelp: '会话启动终端后会显示在这里。', + ariaLabel: '任务终端', + empty: '当前任务还没有终端运行', + emptyHelp: '任务启动终端后会显示在这里。', loadFailed: '无法读取终端运行', retry: '重试', refresh: '刷新终端', - readOnly: '显示代理和你在当前会话中启动的终端运行', + readOnly: '显示代理和你在当前任务中启动的终端运行', runCount: (count) => `${count} 个终端运行`, newTerminal: '新建终端', commandPlaceholder: '输入命令并回车', @@ -462,7 +462,7 @@ const COPY = { stopFailed: '无法停止终端', }, inspector: { - ariaLabel: '会话追踪', + ariaLabel: '任务追踪', recordFile: '记录文件', copyPath: '复制文件路径', pathCopied: '已复制文件路径', @@ -470,8 +470,8 @@ const COPY = { copyFailedDetail: '剪贴板不可用或被系统拒绝。', loadFailed: '追踪读取失败', retry: '重试', - empty: '这个会话还没有可追踪的活动', - emptyHelp: '会话尚无活动记录。', + empty: '这个任务还没有可追踪的活动', + emptyHelp: '任务尚无活动记录。', costUnavailable: '费用未知', totals: { duration: '总耗时', @@ -537,24 +537,24 @@ const COPY = { confirm: '关闭侧边对话', }, errors: { - forkSetupFailed: '无法创建追问会话,请稍后重试。', + forkSetupFailed: '无法创建追问任务,请稍后重试。', sendRejected: '追问未能开始,请稍后重试。', sendFailed: '追问失败,请稍后重试。', - settlementFailed: '对话已结束,但消息加载失败。请重试或重新打开侧边对话。', + settlementFailed: '任务已结束,但消息加载失败。请重试或重新打开侧边对话。', respondFailed: '响应失败,请稍后重试。', }, }, health: { blocked: { - fake_backend: { label: '会话已过期 · 请先配置真实模型', tooltip: () => '原会话使用旧的本地模拟连接,需要先到 设置 · 模型 添加并启用一个真实模型才能发送。' }, - missing_default_connection: { label: '未配置可用模型', tooltip: () => '当前会话没有可用的模型连接,发送会失败。请到 设置 · 模型 添加并启用一个模型。' }, - connection_missing: { label: '连接已删除', tooltip: () => '此会话依赖的模型连接已被删除,发送会失败。请到 设置 · 模型 检查连接配置。' }, - connection_disabled: { label: '连接已禁用', tooltip: (name) => `会话绑定的连接 "${name}" 已禁用,发送会失败。请到 设置 · 模型 启用它或选择其他连接。` }, + fake_backend: { label: '任务已过期 · 请先配置真实模型', tooltip: () => '原任务使用旧的本地模拟连接,需要先到 设置 · 模型 添加并启用一个真实模型才能发送。' }, + missing_default_connection: { label: '未配置可用模型', tooltip: () => '当前任务没有可用的模型连接,发送会失败。请到 设置 · 模型 添加并启用一个模型。' }, + connection_missing: { label: '连接已删除', tooltip: () => '此任务依赖的模型连接已被删除,发送会失败。请到 设置 · 模型 检查连接配置。' }, + connection_disabled: { label: '连接已禁用', tooltip: (name) => `任务绑定的连接 "${name}" 已禁用,发送会失败。请到 设置 · 模型 启用它或选择其他连接。` }, missing_api_key: { label: '连接缺少密钥', tooltip: (name) => `连接 "${name}" 未填写 API key 或未完成登录,发送会失败。请到 设置 · 模型 补齐凭据。` }, missing_model: { label: '连接未选择模型', tooltip: (name) => `连接 "${name}" 没有默认模型,发送会失败。请到 设置 · 模型 选择一个模型。` }, empty_model_list: { label: '连接没有启用模型', tooltip: (name) => `连接 "${name}" 没有启用任何模型,发送会失败。请到 设置 · 模型 先添加模型。` }, - model_not_enabled: { label: '会话模型未启用', tooltip: (name, model) => `模型 "${model}" 不在连接 "${name}" 的启用列表中,发送会失败。请到 设置 · 模型 重新选择。` }, - model_not_chat_capable: { label: '会话模型不支持聊天', tooltip: (name, model) => `模型 "${model}" 不能用于聊天,发送会失败。请到 设置 · 模型 选择支持聊天的模型。` }, + model_not_enabled: { label: '任务模型未启用', tooltip: (name, model) => `模型 "${model}" 不在连接 "${name}" 的启用列表中,发送会失败。请到 设置 · 模型 重新选择。` }, + model_not_chat_capable: { label: '任务模型不支持聊天', tooltip: (name, model) => `模型 "${model}" 不能用于聊天,发送会失败。请到 设置 · 模型 选择支持聊天的模型。` }, }, reauth: { label: '上次连接测试鉴权失败', tooltip: '最近一次连接测试返回鉴权失败(401 / 403),密钥可能已过期或被吊销。这不会拦截发送,但若发送失败请到 设置 · 模型 重新登录。' }, testError: { label: '上次连接测试失败', tooltip: '最近一次连接测试因网络 / 超时 / 5xx 失败。这不会拦截发送,但若问题持续请到 设置 · 模型 检查 Base URL / 代理。' }, @@ -562,7 +562,7 @@ const COPY = { turnError: { unknown: '未知错误', contextOverflow: '上下文窗口已超出限制', timeout: '请求超时', auth: '鉴权失败', providerBilling: '模型服务计费受限', rateLimit: '触发模型速率限制', network: '网络错误', provider: '模型服务返回错误', stepCap: '达到工具步骤上限', tool: '工具调用失败', permission: '等待权限确认', restarted: '本地应用重启,上一轮没有完成', sandboxBoundaryClosed: '本地应用重启,等待确认的「允许访问工作区以外的内容」请求已按拒绝关闭', recovery: { safeResume: '检查当前状态后,可尝试安全恢复', stepCap: '任务可能尚未完成,可以继续', toolError: '先检查工具结果,再决定是否重试', connection: '先检查模型连接或登录状态', partial: '已保留部分输出,可从这里继续', toolRecord: '工具记录已保留,重试前先看结果', retry: '没有执行工具,可直接重试', sandboxBoundaryClosed: '访问范围没有放开,重试本轮后可重新决定' } }, }, en: { - actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The conversation action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh conversations', refreshSessionsFailedFallback: 'The conversation list could not be refreshed. Try again later.', conversationErrorTitle: 'Conversation error', conversationErrorFallback: 'The conversation run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: 'Branch created', branchCreatedDescription: (name) => `New conversation: ${name}`, revisionStartedTitle: 'Edit draft ready', revisionStartedDescription: 'The original conversation is kept; sending creates a new version', revisionReadyTitle: 'Ready to edit and resend', revisionReadyDescription: 'Rewound to before that message; edit and send when ready', revisionUnavailableTitle: 'This message cannot be edited yet', revisionAttachmentsUnsupported: 'Edit & resend does not yet support historical attachments. Copy the text into a new message instead.', revisionTransformedTextUnsupported: 'Edit & resend does not yet support messages sent with an explicit skill. Copy the text and select the skill again instead.', revisionDraftAttachmentConflict: 'The composer already has pending attachments. Send or remove them before editing a sent message.', revisionCommandUnsupported: 'You cannot run /compact, /side, or orchestration commands while editing a sent message. Cancel the edit first.', revisionAlreadyActive: 'Another message is already being edited. Send or cancel that edit first.', revisionCancelLabel: 'Cancel', revisionBannerTitle: 'Editing sent message', revisionBannerDetail: '· New version on send', revisionUnchanged: 'Nothing changed. Use Regenerate if you only want a new answer.', operationFailedTitle: 'Action failed', operationFailedFallback: 'The conversation action failed. Try again later.', attachmentFailedTitle: 'Failed to add attachment', tryAgain: 'Try again later.', modelReboundTitle: 'Switched to an available model', modelReboundDescription: (modelId) => `The previous connection is unavailable${modelId ? ` · ${modelId}` : ''}`, messageReadFailedTitle: 'Failed to load conversation', returnLatest: 'Return to latest' }, + actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The task action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh tasks', refreshSessionsFailedFallback: 'The task list could not be refreshed. Try again later.', conversationErrorTitle: 'Task error', conversationErrorFallback: 'The task run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: 'Branch created', branchCreatedDescription: (name) => `New task: ${name}`, revisionStartedTitle: 'Edit draft ready', revisionStartedDescription: 'The original task is kept; sending creates a new version', revisionReadyTitle: 'Ready to edit and resend', revisionReadyDescription: 'Rewound to before that message; edit and send when ready', revisionUnavailableTitle: 'This message cannot be edited yet', revisionAttachmentsUnsupported: 'Edit & resend does not yet support historical attachments. Copy the text into a new message instead.', revisionTransformedTextUnsupported: 'Edit & resend does not yet support messages sent with an explicit skill. Copy the text and select the skill again instead.', revisionDraftAttachmentConflict: 'The composer already has pending attachments. Send or remove them before editing a sent message.', revisionCommandUnsupported: 'You cannot run /compact, /side, or orchestration commands while editing a sent message. Cancel the edit first.', revisionAlreadyActive: 'Another message is already being edited. Send or cancel that edit first.', revisionCancelLabel: 'Cancel', revisionBannerTitle: 'Editing sent message', revisionBannerDetail: '· New version on send', revisionUnchanged: 'Nothing changed. Use Regenerate if you only want a new answer.', operationFailedTitle: 'Action failed', operationFailedFallback: 'The task action failed. Try again later.', attachmentFailedTitle: 'Failed to add attachment', tryAgain: 'Try again later.', modelReboundTitle: 'Switched to an available model', modelReboundDescription: (modelId) => `The previous connection is unavailable${modelId ? ` · ${modelId}` : ''}`, messageReadFailedTitle: 'Failed to load task', returnLatest: 'Return to latest' }, attachments: { tooMany: 'You can attach at most 8 files', tooLarge: 'Attachments must be 50 MB or smaller', duplicate: 'This attachment was already added.' }, model: { fakeBackendLabel: 'Local simulation', @@ -571,25 +571,25 @@ const COPY = { configurationFallback: 'This model connection cannot send right now. Check it in Settings · Models and try again.', configurationReason: { missing_default_connection: 'Set a default model in Settings · Models before sending.', - connection_missing: 'The model connection used by this conversation was deleted. Select or create one in Settings · Models.', + connection_missing: 'The model connection used by this task was deleted. Select or create one in Settings · Models.', connection_disabled: 'The current model connection is disabled. Enable it or choose another default in Settings · Models.', missing_api_key: 'The current model connection has no usable credentials. Add an API key or sign in again under Settings · Models.', missing_model: 'The current connection has no usable model. Select a default model in Settings · Models.', empty_model_list: 'The current connection has no enabled models. Add or enable one in Settings · Models.', - model_not_enabled: 'The model selected for this conversation is disabled. Choose an enabled model in Settings · Models.', - model_not_chat_capable: 'The model selected for this conversation cannot chat. Choose a chat-capable model in Settings · Models.', - fake_backend: 'This conversation used the retired local simulation. Add a real model in Settings · Models, then start a new conversation.', + model_not_enabled: 'The model selected for this task is disabled. Choose an enabled model in Settings · Models.', + model_not_chat_capable: 'The model selected for this task cannot chat. Choose a chat-capable model in Settings · Models.', + fake_backend: 'This task used the retired local simulation. Add a real model in Settings · Models, then start a new task.', }, }, - footer: { labels: { regenerate: 'Regenerate', branch: 'Branch', copy: 'Copy', info: 'Details' }, pending: 'Working…', regenerateRunning: 'Wait for the current response to finish before regenerating', regenerateAgain: 'A regenerated response already exists; click again to create another parallel response', regenerate: 'Generate another response to this turn', branchRunning: 'Wait for the current response to finish before branching', branchAborted: 'Branch from the context before the interruption', branch: 'Branch a new conversation from this response', copy: 'Copy response to clipboard', copyEmpty: 'This response has no content to copy' }, + footer: { labels: { regenerate: 'Regenerate', branch: 'Branch', copy: 'Copy', info: 'Details' }, pending: 'Working…', regenerateRunning: 'Wait for the current response to finish before regenerating', regenerateAgain: 'A regenerated response already exists; click again to create another parallel response', regenerate: 'Generate another response to this turn', branchRunning: 'Wait for the current response to finish before branching', branchAborted: 'Branch from the context before the interruption', branch: 'Branch a new task from this response', copy: 'Copy response to clipboard', copyEmpty: 'This response has no content to copy' }, lineage: { regeneratedFrom: 'Regenerated from previous response', regeneratedFromTooltip: 'This is a parallel regenerated response; click to view the retained previous response', regeneratedTo: 'Regenerated → New response', regeneratedToTooltip: 'Jump to the regenerated response' }, workbar: { - ariaLabel: 'Conversation workbar', - sectionsAriaLabel: 'Conversation workbar tabs', + ariaLabel: 'Task workbar', + sectionsAriaLabel: 'Task workbar tabs', review: 'Changes', terminal: 'Terminal', terminalNumbered: (index) => `Terminal ${index}`, - tasks: 'Tasks', + tasks: 'To-do', browser: 'Browser', files: 'Generated files', inspector: 'Trace', @@ -610,10 +610,10 @@ const COPY = { closeToRight: 'Close tabs to the right', launcher: { review: 'View changes in the current Git workspace', - terminal: 'Inspect terminal runs and live output for this conversation', - tasks: 'View and maintain the task ledger for this conversation', + terminal: 'Inspect terminal runs and live output for this task', + tasks: "View and maintain this task's to-do ledger", browser: 'Open the embedded browser and keep the current page', - files: 'Browse files generated by this conversation', + files: 'Browse files generated by this task', inspector: 'Inspect model calls, tools, and timing', sideChat: 'Ask and explore read-only without interrupting the main task', }, @@ -622,8 +622,8 @@ const COPY = { ariaLabel: 'Git changes', empty: 'No changes in the current Git workspace', emptyHelp: 'Committed, staged, and modified files appear here.', - notGitRepository: 'This conversation directory is not a Git repository', - workspaceUnavailable: 'This conversation directory is unavailable', + notGitRepository: 'This task directory is not a Git repository', + workspaceUnavailable: 'This task directory is unavailable', unbornRepository: 'This Git repository has no commit to compare yet', gitFailed: 'Could not read Git workspace changes', invalidBaseBranch: 'The selected comparison branch is unavailable', @@ -641,13 +641,13 @@ const COPY = { retry: 'Retry', }, terminalPanel: { - ariaLabel: 'Conversation terminal', - empty: 'No terminal runs in this conversation yet', + ariaLabel: 'Task terminal', + empty: 'No terminal runs in this task yet', emptyHelp: "The session's terminal appears here once it starts.", loadFailed: 'Could not read terminal runs', retry: 'Retry', refresh: 'Refresh terminal', - readOnly: 'Shows terminal runs started by the agent or you in this conversation', + readOnly: 'Shows terminal runs started by the agent or you in this task', runCount: (count) => `${count} terminal run${count === 1 ? '' : 's'}`, newTerminal: 'New terminal', commandPlaceholder: 'Enter a command and press Enter', @@ -659,7 +659,7 @@ const COPY = { stopFailed: 'Could not stop terminal', }, inspector: { - ariaLabel: 'Session trace', + ariaLabel: 'Task trace', recordFile: 'Record file', copyPath: 'Copy file path', pathCopied: 'File path copied', @@ -667,8 +667,8 @@ const COPY = { copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', loadFailed: 'Could not read the trace', retry: 'Retry', - empty: 'Nothing to trace in this session yet', - emptyHelp: 'No activity recorded for this session yet.', + empty: 'Nothing to trace in this task yet', + emptyHelp: 'No activity recorded for this task yet.', costUnavailable: 'cost unknown', totals: { duration: 'Duration', @@ -736,7 +736,7 @@ const COPY = { confirm: 'Close side chat', }, errors: { - forkSetupFailed: 'Could not create the companion conversation. Please try again.', + forkSetupFailed: 'Could not create the companion task. Please try again.', sendRejected: 'The companion could not start. Please try again.', sendFailed: 'The companion request failed. Please try again.', settlementFailed: 'The run ended, but its messages could not be loaded. Retry or reopen the side chat.', @@ -745,15 +745,15 @@ const COPY = { }, health: { blocked: { - fake_backend: { label: 'Stale conversation · Configure a real model', tooltip: () => 'This conversation used the retired local simulation. Add and enable a real model in Settings · Models before sending.' }, - missing_default_connection: { label: 'No model configured', tooltip: () => 'This conversation has no available model connection. Add and enable one in Settings · Models.' }, - connection_missing: { label: 'Connection deleted', tooltip: () => 'The model connection used by this conversation was deleted. Check Settings · Models.' }, + fake_backend: { label: 'Stale task · Configure a real model', tooltip: () => 'This task used the retired local simulation. Add and enable a real model in Settings · Models before sending.' }, + missing_default_connection: { label: 'No model configured', tooltip: () => 'This task has no available model connection. Add and enable one in Settings · Models.' }, + connection_missing: { label: 'Connection deleted', tooltip: () => 'The model connection used by this task was deleted. Check Settings · Models.' }, connection_disabled: { label: 'Connection disabled', tooltip: (name) => `Connection "${name}" is disabled. Enable it or choose another connection in Settings · Models.` }, missing_api_key: { label: 'Connection credentials missing', tooltip: (name) => `Connection "${name}" has no API key or completed sign-in. Add credentials in Settings · Models.` }, missing_model: { label: 'No model selected', tooltip: (name) => `Connection "${name}" has no default model. Select one in Settings · Models.` }, empty_model_list: { label: 'No models enabled', tooltip: (name) => `Connection "${name}" has no enabled models. Add one in Settings · Models.` }, - model_not_enabled: { label: 'Conversation model disabled', tooltip: (name, model) => `Model "${model}" is not enabled for connection "${name}". Choose another model in Settings · Models.` }, - model_not_chat_capable: { label: 'Conversation model cannot chat', tooltip: (_name, model) => `Model "${model}" cannot be used for chat. Choose a chat-capable model in Settings · Models.` }, + model_not_enabled: { label: 'Task model disabled', tooltip: (name, model) => `Model "${model}" is not enabled for connection "${name}". Choose another model in Settings · Models.` }, + model_not_chat_capable: { label: 'Task model cannot chat', tooltip: (_name, model) => `Model "${model}" cannot be used for chat. Choose a chat-capable model in Settings · Models.` }, }, reauth: { label: 'Last connection test failed authentication', tooltip: 'The latest test returned 401 / 403. Sending is not blocked, but sign in again under Settings · Models if it fails.' }, testError: { label: 'Last connection test failed', tooltip: 'The latest test failed because of a network, timeout, or 5xx error. Sending is not blocked; check Base URL or proxy settings if it persists.' }, diff --git a/apps/desktop/src/renderer/locales/permission-center-copy.ts b/apps/desktop/src/renderer/locales/permission-center-copy.ts index ca5ab108f6..9065b6d841 100644 --- a/apps/desktop/src/renderer/locales/permission-center-copy.ts +++ b/apps/desktop/src/renderer/locales/permission-center-copy.ts @@ -116,7 +116,7 @@ const PERMISSION_CENTER_COPY = { aria: (label) => `${label}能力状态明细`, feature: '功能开关', configuration: '配置', approval: '操作审批', memory: '记忆写入', runtime: '运行态探测', featureStates: { enabled: '已开启', partial: '部分可用', disabled: '已关闭', not_available: '未开放' }, configurationStates: { not_required: '不需要配置', missing: '等待补齐配置', present: '已填写' }, - approvalStates: { not_required: '不需要审批', required_per_action: '每次调用都需审批', required_scoped_lease: '按目标与动作类别授权', pending: '审批挂起', approved: '当前会话已批准', denied: '当前会话已拒绝' }, + approvalStates: { not_required: '不需要审批', required_per_action: '每次调用都需审批', required_scoped_lease: '按目标与动作类别授权', pending: '审批挂起', approved: '当前任务已批准', denied: '当前任务已拒绝' }, memoryStates: { not_applicable: '不涉及记忆写入', disabled: '记忆写入已关闭', draft_required: '需要先草拟 memory 协议', accepted: '记忆写入已接受' }, runtimeStates: { not_available: '尚无运行态探测', not_run: '探测未运行', healthy: '探测通过', degraded: '探测降级' }, }, @@ -163,7 +163,7 @@ const PERMISSION_CENTER_COPY = { aria: (label) => `${label} capability state details`, feature: 'Feature toggle', configuration: 'Configuration', approval: 'Action approval', memory: 'Memory writes', runtime: 'Runtime probe', featureStates: { enabled: 'Enabled', partial: 'Partially available', disabled: 'Disabled', not_available: 'Unavailable' }, configurationStates: { not_required: 'No configuration needed', missing: 'Configuration required', present: 'Configured' }, - approvalStates: { not_required: 'No approval needed', required_per_action: 'Approval required for every call', required_scoped_lease: 'Authorized by target and action category', pending: 'Approval pending', approved: 'Approved for this session', denied: 'Denied for this session' }, + approvalStates: { not_required: 'No approval needed', required_per_action: 'Approval required for every call', required_scoped_lease: 'Authorized by target and action category', pending: 'Approval pending', approved: 'Approved for this task', denied: 'Denied for this task' }, memoryStates: { not_applicable: 'No memory writes', disabled: 'Memory writes disabled', draft_required: 'Draft a memory protocol first', accepted: 'Memory writes accepted' }, runtimeStates: { not_available: 'No runtime probe available', not_run: 'Probe not run', healthy: 'Probe passed', degraded: 'Probe degraded' }, }, diff --git a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts index 24eb1a3233..14328bde9d 100644 --- a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts @@ -22,7 +22,7 @@ export type DailyReviewSettingsCopy = { const SETTINGS_DAILY_REVIEW_COPY = { zh: { - defaultModel: '跟随对话默认', + defaultModel: '跟随任务默认', saveFailed: '保存每日回顾设置失败', aria: '每日回顾', unavailable: '当前版本无法读取每日回顾设置。', @@ -38,10 +38,10 @@ const SETTINGS_DAILY_REVIEW_COPY = { analysisTitle: '分析', analysisDescription: '选择用于生成固定结构报告的模型。', model: '分析模型', - modelHelp: '未指定时跟随当前对话的默认模型。', + modelHelp: '未指定时跟随当前任务的默认模型。', }, en: { - defaultModel: 'Follow conversation default', + defaultModel: 'Follow task default', saveFailed: 'Failed to save Daily Review settings', aria: 'Daily Review', unavailable: 'Daily Review settings are unavailable in this build.', @@ -57,7 +57,7 @@ const SETTINGS_DAILY_REVIEW_COPY = { analysisTitle: 'Analysis', analysisDescription: 'Choose the model used to generate the fixed report structure.', model: 'Analysis model', - modelHelp: 'Follows the current conversation default when unspecified.', + modelHelp: 'Follows the current task default when unspecified.', }, } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/locales/settings-data-copy.ts b/apps/desktop/src/renderer/locales/settings-data-copy.ts index 6aaa64c740..3e72d8c5aa 100644 --- a/apps/desktop/src/renderer/locales/settings-data-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-data-copy.ts @@ -38,7 +38,7 @@ const SETTINGS_DATA_COPY = { exported: '已导出配置', exportedDetail: (items) => `包含:${items.join('、')}`, exportFailed: '导出失败', noCategories: '未选择任何类别', tryAgain: '请稍后重试', imported: '已导入配置', importFailed: '导入失败', invalidFile: '文件无效或版本不受支持。', rows: { - workspace: '工作区路径', workspaceDetail: '会话、设置、凭据和技能文件都存在这个目录下。', loadValueFailed: '载入失败', loading: '正在加载…', + workspace: '工作区路径', workspaceDetail: '任务、设置、凭据和技能文件都存在这个目录下。', loadValueFailed: '载入失败', loading: '正在加载…', history: '输入历史', historyDetail: '上箭头 / 下箭头调出的已发送提示词记录,保存在本机、重启后仍在。清空后无法恢复。', }, actionsAria: '工作区数据操作', opening: '打开中…', openWorkspace: '打开工作区文件夹', copying: '复制中…', copyPath: '复制路径', clearing: '清空中…', clearHistory: '清空输入历史', @@ -65,7 +65,7 @@ const SETTINGS_DATA_COPY = { exported: 'Configuration exported', exportedDetail: (items) => `Included: ${items.join(', ')}`, exportFailed: 'Export failed', noCategories: 'No categories selected', tryAgain: 'Try again later', imported: 'Configuration imported', importFailed: 'Import failed', invalidFile: 'The file is invalid or its version is unsupported.', rows: { - workspace: 'Workspace path', workspaceDetail: 'Sessions, settings, credentials, and skill files are stored in this directory.', loadValueFailed: 'Failed to load', loading: 'Loading…', + workspace: 'Workspace path', workspaceDetail: 'Tasks, settings, credentials, and skill files are stored in this directory.', loadValueFailed: 'Failed to load', loading: 'Loading…', history: 'Input history', historyDetail: 'Previously sent prompts recalled with the Up and Down arrows are kept on this machine and persist across restarts. Clearing them cannot be undone.', }, actionsAria: 'Workspace data actions', opening: 'Opening…', openWorkspace: 'Open workspace folder', copying: 'Copying…', copyPath: 'Copy path', clearing: 'Clearing…', clearHistory: 'Clear input history', diff --git a/apps/desktop/src/renderer/locales/settings-memory-copy.ts b/apps/desktop/src/renderer/locales/settings-memory-copy.ts index 94cbe96f25..512caaabaf 100644 --- a/apps/desktop/src/renderer/locales/settings-memory-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-memory-copy.ts @@ -59,7 +59,7 @@ export type MemorySettingsCopy = { }; const zhText = { - localFile: '本地 MEMORY.md', localFileHelp: '透明 Markdown 文件,保存在当前本机工作区。这里的内容不会自动从聊天里抽取。', enableLocalFile: '启用本地 MEMORY.md', agentReadable: '模型上下文可读取', agentReadableHelp: '默认关闭。开启后,发送消息时会把本地记忆一并提供给模型;隐身模式下仍然不提供。', enableAgentRead: '允许模型上下文读取本地记忆', waitingFile: '等待创建 MEMORY.md', waitingBackup: '等待生成上一版备份', dirty: '有未保存修改', savedDraft: '草稿已保存', backupCandidates: '备份候选', backupCandidatesAria: '本地记忆备份候选列表', opening: '打开中…', open: '打开', restoring: '恢复中…', restore: '恢复', copying: '复制中…', copyReference: '复制引用', backupHelp: '上一版操作会使用最近的候选;这里只显示 metadata,不展示备份正文。', savedAt: '保存于 ', previewPaused: '草稿条目预览暂停', filterAria: '筛选本地记忆', filterPlaceholder: '筛选标题、内容、ID 或标签', clear: '清除', filterEmpty: '没有匹配的记忆条目', filterEmptyHelp: '筛选不会修改 MEMORY.md;清除筛选后会恢复显示全部条目。', activeMemories: '生效记忆', archivedMemories: '已归档记忆', waitingEntry: '等待添加记忆条目', waitingEntryHelp: '在对话里确认记忆,或点右上角「添加记忆」。', manualAddAria: '手动添加本地记忆', manualAdd: '手动添加记忆', manualAddHelp: '填写后立即写入本机 MEMORY.md。', title: '记忆标题', titlePlaceholder: '标题', tags: '记忆标签', tagsPlaceholder: '标签(逗号分隔,可选)', content: '记忆内容', contentPlaceholder: '内容', addDraft: '添加记忆', sensitiveDraft: '草稿含疑似敏感字段', sensitiveDraftHelp: '保存时会先遮蔽疑似 token、API key 或密码,再写入 MEMORY.md。', fileContent: 'MEMORY.md 内容', fileActionsAria: 'MEMORY.md 文件操作', saving: '保存中…', save: '保存', saved: '已保存', openFile: '打开 MEMORY.md', openFolder: '打开所在目录', loading: '载入中…', reload: '重新载入', openPrevious: '打开上一版', copyPath: '复制路径', copyPrevious: '复制上一版引用', resetting: '重置中…', resetBackup: '重置并备份', restorePrevious: '恢复上一版', archiveDraftNotice: '当前归档/恢复操作只更新草稿,保存后才会写入 MEMORY.md。', noMatchEntry: '无匹配条目。', noEntry: '暂无条目。', created: '创建 ', updated: '更新 ', archivedNoPrompt: '已归档,不会提供给模型', activePrompt: '生效条目,发送时会提供给模型', locateDraft: '定位草稿', promptPreview: '模型上下文预览', willInject: '发送时会提供', willNotInject: '当前不会提供', copyContext: '复制上下文', promptPreviewHelp: '这里是发送时会提供给模型的内容;已归档条目不在其中,疑似密钥会遮蔽。', safeModePreview: 'MEMORY.md 过大,当前不会生成模型上下文预览。', emptyPromptPreview: '没有生效记忆会提供给模型。', loadFailed: '载入本地记忆失败', reloaded: '已重新载入 MEMORY.md', reloadDiscarded: '未保存的草稿修改已丢弃。', toggleFailed: '更新本地记忆开关失败', agentReadFailed: '更新模型读取权限失败', saveBlocked: '保存被拦截', safeMode: 'MEMORY.md 内容过大,已进入安全模式。', savedRedacted: '已保存并遮蔽敏感字段', savedFile: '已保存 MEMORY.md', saveFailed: '保存 MEMORY.md 失败', resetDone: '已重置 MEMORY.md', resetDoneDetail: '上一版已保存为备份文件。', resetFailed: '重置 MEMORY.md 失败', noBackup: '没有可恢复备份', noBackupDetail: '保存或重置 MEMORY.md 后才会生成上一版备份。', restoreLatestTitle: '恢复上一版 MEMORY.md?', restoreCandidateTitle: '恢复这个 MEMORY.md 备份?', confirmRestore: '恢复', cancel: '取消', restoredLatest: '已恢复上一版 MEMORY.md', restoredCandidate: '已恢复 MEMORY.md 备份候选', restoredDetail: '恢复前的当前文件已保存为 restore.bak。', restoreFailed: '恢复失败', restoreLatestFailed: '恢复上一版失败', restoreCandidateFailed: '恢复备份失败', openFailed: '打开失败', openPreviousFailed: '打开上一版失败', pathCopied: '已复制路径', copyFailed: '复制失败', copyFailedDetail: '剪贴板不可用或被系统拒绝。', backupReferenceCopied: '已复制上一版引用', entryReferenceCopied: '已复制记忆引用', locateFailed: '无法定位记忆', locateFailedDetail: '当前草稿里找不到这条记忆;请先保存或刷新后重试。', emptyTitle: '标题不能为空', emptyTitleDetail: '给这条记忆起一个短标题。', emptyContent: '内容不能为空', emptyContentDetail: '写下要保留的偏好或事实。', draftOversize: '草稿过大', oversizeDetail: 'MEMORY.md 超出安全上限,请先删减旧内容。', addedDraft: '已添加记忆', addedDraftDetail: '已写入 MEMORY.md。', updateFailed: '无法更新记忆', invalidIdDetail: '这条记忆没有可识别 ID,已停止更新。', archivedDraft: '已在草稿中归档记忆', restoredDraft: '已在草稿中恢复记忆', updateBlocked: '更新被拦截', archived: '已归档记忆', restored: '已恢复记忆', archiveFailed: '归档记忆失败', entryRestoreFailed: '恢复记忆失败', promptCopied: '已复制模型上下文预览', promptCopiedDetail: '使用同一条 prompt 预览和遮蔽路径。', restoreDraftAction: '恢复到草稿', archiveDraftAction: '归档到草稿', restoreAction: '恢复', archiveAction: '归档', + localFile: '本地 MEMORY.md', localFileHelp: '透明 Markdown 文件,保存在当前本机工作区。这里的内容不会自动从聊天里抽取。', enableLocalFile: '启用本地 MEMORY.md', agentReadable: '模型上下文可读取', agentReadableHelp: '默认关闭。开启后,发送消息时会把本地记忆一并提供给模型;隐身模式下仍然不提供。', enableAgentRead: '允许模型上下文读取本地记忆', waitingFile: '等待创建 MEMORY.md', waitingBackup: '等待生成上一版备份', dirty: '有未保存修改', savedDraft: '草稿已保存', backupCandidates: '备份候选', backupCandidatesAria: '本地记忆备份候选列表', opening: '打开中…', open: '打开', restoring: '恢复中…', restore: '恢复', copying: '复制中…', copyReference: '复制引用', backupHelp: '上一版操作会使用最近的候选;这里只显示 metadata,不展示备份正文。', savedAt: '保存于 ', previewPaused: '草稿条目预览暂停', filterAria: '筛选本地记忆', filterPlaceholder: '筛选标题、内容、ID 或标签', clear: '清除', filterEmpty: '没有匹配的记忆条目', filterEmptyHelp: '筛选不会修改 MEMORY.md;清除筛选后会恢复显示全部条目。', activeMemories: '生效记忆', archivedMemories: '已归档记忆', waitingEntry: '等待添加记忆条目', waitingEntryHelp: '在任务里确认记忆,或点右上角「添加记忆」。', manualAddAria: '手动添加本地记忆', manualAdd: '手动添加记忆', manualAddHelp: '填写后立即写入本机 MEMORY.md。', title: '记忆标题', titlePlaceholder: '标题', tags: '记忆标签', tagsPlaceholder: '标签(逗号分隔,可选)', content: '记忆内容', contentPlaceholder: '内容', addDraft: '添加记忆', sensitiveDraft: '草稿含疑似敏感字段', sensitiveDraftHelp: '保存时会先遮蔽疑似 token、API key 或密码,再写入 MEMORY.md。', fileContent: 'MEMORY.md 内容', fileActionsAria: 'MEMORY.md 文件操作', saving: '保存中…', save: '保存', saved: '已保存', openFile: '打开 MEMORY.md', openFolder: '打开所在目录', loading: '载入中…', reload: '重新载入', openPrevious: '打开上一版', copyPath: '复制路径', copyPrevious: '复制上一版引用', resetting: '重置中…', resetBackup: '重置并备份', restorePrevious: '恢复上一版', archiveDraftNotice: '当前归档/恢复操作只更新草稿,保存后才会写入 MEMORY.md。', noMatchEntry: '无匹配条目。', noEntry: '暂无条目。', created: '创建 ', updated: '更新 ', archivedNoPrompt: '已归档,不会提供给模型', activePrompt: '生效条目,发送时会提供给模型', locateDraft: '定位草稿', promptPreview: '模型上下文预览', willInject: '发送时会提供', willNotInject: '当前不会提供', copyContext: '复制上下文', promptPreviewHelp: '这里是发送时会提供给模型的内容;已归档条目不在其中,疑似密钥会遮蔽。', safeModePreview: 'MEMORY.md 过大,当前不会生成模型上下文预览。', emptyPromptPreview: '没有生效记忆会提供给模型。', loadFailed: '载入本地记忆失败', reloaded: '已重新载入 MEMORY.md', reloadDiscarded: '未保存的草稿修改已丢弃。', toggleFailed: '更新本地记忆开关失败', agentReadFailed: '更新模型读取权限失败', saveBlocked: '保存被拦截', safeMode: 'MEMORY.md 内容过大,已进入安全模式。', savedRedacted: '已保存并遮蔽敏感字段', savedFile: '已保存 MEMORY.md', saveFailed: '保存 MEMORY.md 失败', resetDone: '已重置 MEMORY.md', resetDoneDetail: '上一版已保存为备份文件。', resetFailed: '重置 MEMORY.md 失败', noBackup: '没有可恢复备份', noBackupDetail: '保存或重置 MEMORY.md 后才会生成上一版备份。', restoreLatestTitle: '恢复上一版 MEMORY.md?', restoreCandidateTitle: '恢复这个 MEMORY.md 备份?', confirmRestore: '恢复', cancel: '取消', restoredLatest: '已恢复上一版 MEMORY.md', restoredCandidate: '已恢复 MEMORY.md 备份候选', restoredDetail: '恢复前的当前文件已保存为 restore.bak。', restoreFailed: '恢复失败', restoreLatestFailed: '恢复上一版失败', restoreCandidateFailed: '恢复备份失败', openFailed: '打开失败', openPreviousFailed: '打开上一版失败', pathCopied: '已复制路径', copyFailed: '复制失败', copyFailedDetail: '剪贴板不可用或被系统拒绝。', backupReferenceCopied: '已复制上一版引用', entryReferenceCopied: '已复制记忆引用', locateFailed: '无法定位记忆', locateFailedDetail: '当前草稿里找不到这条记忆;请先保存或刷新后重试。', emptyTitle: '标题不能为空', emptyTitleDetail: '给这条记忆起一个短标题。', emptyContent: '内容不能为空', emptyContentDetail: '写下要保留的偏好或事实。', draftOversize: '草稿过大', oversizeDetail: 'MEMORY.md 超出安全上限,请先删减旧内容。', addedDraft: '已添加记忆', addedDraftDetail: '已写入 MEMORY.md。', updateFailed: '无法更新记忆', invalidIdDetail: '这条记忆没有可识别 ID,已停止更新。', archivedDraft: '已在草稿中归档记忆', restoredDraft: '已在草稿中恢复记忆', updateBlocked: '更新被拦截', archived: '已归档记忆', restored: '已恢复记忆', archiveFailed: '归档记忆失败', entryRestoreFailed: '恢复记忆失败', promptCopied: '已复制模型上下文预览', promptCopiedDetail: '使用同一条 prompt 预览和遮蔽路径。', restoreDraftAction: '恢复到草稿', archiveDraftAction: '归档到草稿', restoreAction: '恢复', archiveAction: '归档', } satisfies Record; const enText = { diff --git a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts index 5941456f76..79833e923a 100644 --- a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts @@ -16,7 +16,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { system: '系统', }, sections: { - general: { label: '通用', description: '显示名称与界面语言、隐私与通知、对话默认与网络代理。' }, + general: { label: '通用', description: '显示名称与界面语言、隐私与通知、任务默认与网络代理。' }, appearance: { label: '外观', description: '界面主题与调色板。' }, projects: { label: '工作区', description: '选择 Runtime Host,并管理该 Host 上的项目。' }, models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' }, @@ -25,7 +25,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { 'archived-tasks': { label: '已归档任务', description: '恢复或彻底删除已归档的任务。' }, 'import-tasks': { label: '导入任务', description: '把本机其他 Agent 的对话记录转换成 Maka 任务。' }, memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' }, - 'daily-review': { label: '每日回顾', description: '每天分析本机对话,生成摘要、遗漏提醒和建议。' }, + 'daily-review': { label: '每日回顾', description: '每天分析本机任务,生成摘要、遗漏提醒和建议。' }, 'bot-chat': { label: '远程接入', description: '通过 Telegram、飞书、微信等平台从其他设备与 Maka 对话。' }, search: { label: '联网搜索', description: '联网搜索供应商(如 Tavily)凭据与隐私边界。' }, data: { label: '数据', description: '本地工作区路径、备份与恢复。' }, @@ -42,7 +42,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { system: 'System', }, sections: { - general: { label: 'General', description: 'Display name and interface language, privacy and notifications, conversation defaults, and network proxy.' }, + general: { label: 'General', description: 'Display name and interface language, privacy and notifications, task defaults, and network proxy.' }, appearance: { label: 'Appearance', description: 'Interface theme and color palette.' }, projects: { label: 'Workspace', description: 'Choose the Runtime Host and manage the projects available on it.' }, models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' }, @@ -51,7 +51,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { 'archived-tasks': { label: 'Archived tasks', description: 'Restore or permanently delete archived tasks.' }, 'import-tasks': { label: 'Import tasks', description: 'Convert conversations from another local agent into Maka tasks.' }, memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' }, - 'daily-review': { label: 'Daily Review', description: 'Analyze local conversations for summaries, reminders, and suggestions.' }, + 'daily-review': { label: 'Daily Review', description: 'Analyze local tasks for summaries, reminders, and suggestions.' }, 'bot-chat': { label: 'Remote Access', description: 'Chat with Maka from other devices through Telegram, Feishu, or WeChat.' }, search: { label: 'Web Search', description: 'Credentials and privacy boundaries for providers such as Tavily.' }, data: { label: 'Data', description: 'Local workspace paths, backup, and restore.' }, diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts index a7824cb897..8dd807e29a 100644 --- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts @@ -194,7 +194,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { sections: { identity: '身份', identityHelp: 'Maka 如何称呼你,以及界面语言和回答语气。', privacy: '隐私与通知', privacyHelp: '本地数据的读写范围,以及桌面通知时机。', - chatDefaults: '对话默认', chatDefaultsHelp: '新对话的起始模型、权限模式与思考级别。', + chatDefaults: '任务默认', chatDefaultsHelp: '新任务的起始模型、权限模式与思考级别。', network: '网络', networkHelp: 'AI 模型请求走的网络通道。', theme: '主题', themeHelp: '界面跟随系统,还是固定浅色或深色。', palette: '调色板', paletteHelp: '强调色与画布色调;切换会立即生效并保存在本地。', @@ -220,11 +220,11 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { }, general: { incognito: '隐身模式', incognitoHelp: '开启后暂停本地记忆读写、联网搜索和定时任务触发。', enableIncognito: '启用隐身模式', incognitoFailed: '隐身模式切换失败', notifications: '完成时发送系统通知', notificationsHelp: '窗口不在前台时,在回答完成或出错后发送桌面通知。', notificationsFailed: '通知设置切换失败', workspaceInstructions: '遵循项目指令', workspaceInstructionsHelp: '自动读取每个项目中已有的 AGENTS.md、CLAUDE.md 或 GEMINI.md;文件仍由各自项目管理。', workspaceInstructionsFailed: '项目指令设置切换失败', updateFailed: '设置未生效,请稍后重试。', - defaultModel: '默认模型', defaultModelHelp: '新对话默认使用的模型。', notSet: '未设置', saveDefaultModelFailed: '保存默认模型失败', defaultPermission: '默认权限模式', defaultPermissionHelp: '新对话默认使用的权限模式;可在对话内随时切换。', saveDefaultPermissionFailed: '保存默认权限模式失败', defaultThinking: '默认思考级别', defaultThinkingHelp: '新对话的思考级别;当前模型不支持所选级别时用模型默认。', followModelDefault: '跟随模型默认', saveDefaultThinkingFailed: '保存默认思考级别失败', + defaultModel: '默认模型', defaultModelHelp: '新任务默认使用的模型。', notSet: '未设置', saveDefaultModelFailed: '保存默认模型失败', defaultPermission: '默认权限模式', defaultPermissionHelp: '新任务默认使用的权限模式;可在任务内随时切换。', saveDefaultPermissionFailed: '保存默认权限模式失败', defaultThinking: '默认思考级别', defaultThinkingHelp: '新任务的思考级别;当前模型不支持所选级别时用模型默认。', followModelDefault: '跟随模型默认', saveDefaultThinkingFailed: '保存默认思考级别失败', proxy: '代理服务器', proxyHelp: '为 AI 模型请求配置网络代理', enableProxy: '启用代理服务器', saveNetworkFailed: '保存网络设置失败', proxyProtocol: '代理协议', serverAddress: '服务器地址', port: '端口', proxyAuth: '代理认证', proxyAuthHelp: '需要用户名和密码时开启。', enableProxyAuth: '启用代理认证', username: '用户名', password: '密码', bypassList: '代理白名单', bypassHelp: '这些域名将绕过代理直连,多个用逗号分隔。', autoBypass: (count) => `已自动添加 ${count} 个域名。代理仅作用于 AI 模型请求。`, testing: '测试中…', testCurrent: '测试当前配置', proxyReachable: '代理可达', proxyTestFailed: '代理测试失败', proxyTestError: '代理测试出错', }, about: { - loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制环境信息', pasteHint: '可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', subtitle: '本地优先的 AI 助手 · 桌面端运行环境', privacyLabel: '隐私与安全', privacyTitle: '本地优先 · 隐私默认', privacyPoints: ['所有会话、设置、凭据和 Skill 指令文件都保留在本机工作区。', '模型密钥保存在本机凭据文件内;订阅账号令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在对话内明示授权。', '每个会话都会在本机保留消息、工具调用、权限决策与模式变更记录。'], copying: '复制中…', copyEnvironment: '复制环境信息', copyHelp: '复制当前版本与平台信息以便定位问题;内容不包含工作区路径。', keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看', reportIssueLabel: '报告问题', + loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制环境信息', pasteHint: '可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', subtitle: '本地优先的 AI 助手 · 桌面端运行环境', privacyLabel: '隐私与安全', privacyTitle: '本地优先 · 隐私默认', privacyPoints: ['所有任务、设置、凭据和 Skill 指令文件都保留在本机工作区。', '模型密钥保存在本机凭据文件内;订阅账号令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权。', '每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'], copying: '复制中…', copyEnvironment: '复制环境信息', copyHelp: '复制当前版本与平台信息以便定位问题;内容不包含工作区路径。', keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看', reportIssueLabel: '报告问题', updatesTitle: '软件更新', checkForUpdates: '检查更新', checkingForUpdates: '检查中…', @@ -248,7 +248,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { sections: { identity: 'Identity', identityHelp: 'How Maka addresses you, plus interface language and response tone.', privacy: 'Privacy and notifications', privacyHelp: 'What Maka may read and write locally, and when it notifies you.', - chatDefaults: 'Conversation defaults', chatDefaultsHelp: 'The model, permission mode, and thinking level a new conversation starts on.', + chatDefaults: 'Task defaults', chatDefaultsHelp: 'The model, permission mode, and thinking level a new task starts on.', network: 'Network', networkHelp: 'The network path AI model requests take.', theme: 'Theme', themeHelp: 'Follow the system appearance, or stay on light or dark.', palette: 'Color palette', paletteHelp: 'Accent and canvas colors. Changes apply immediately and are saved locally.', @@ -269,10 +269,10 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { removeErrors: { invalid_id: 'The pet ID is invalid.', remove_failed: 'The local pet pack could not be removed.' }, }, general: { - incognito: 'Incognito mode', incognitoHelp: 'Pause local memory, web search, and scheduled task triggers.', enableIncognito: 'Enable incognito mode', incognitoFailed: 'Could not change incognito mode', notifications: 'Send a system notification when finished', notificationsHelp: 'Notify when a response finishes or fails while the window is in the background.', notificationsFailed: 'Could not change notification settings', workspaceInstructions: 'Follow project instructions', workspaceInstructionsHelp: 'Automatically read existing AGENTS.md, CLAUDE.md, or GEMINI.md files in each project. Manage the files in their respective projects.', workspaceInstructionsFailed: 'Could not change project instruction settings', updateFailed: 'The setting was not applied. Try again later.', defaultModel: 'Default model', defaultModelHelp: 'Model used by new conversations.', notSet: 'Not set', saveDefaultModelFailed: 'Could not save the default model', defaultPermission: 'Default permission mode', defaultPermissionHelp: 'Initial permission mode for new conversations; it can be changed at any time.', saveDefaultPermissionFailed: 'Could not save the default permission mode', defaultThinking: 'Default thinking level', defaultThinkingHelp: 'Thinking level for new conversations; models that do not offer the chosen level use their own default.', followModelDefault: 'Follow model default', saveDefaultThinkingFailed: 'Could not save the default thinking level', proxy: 'Proxy server', proxyHelp: 'Configure a network proxy for AI model requests', enableProxy: 'Enable proxy server', saveNetworkFailed: 'Could not save network settings', proxyProtocol: 'Proxy protocol', serverAddress: 'Server address', port: 'Port', proxyAuth: 'Proxy authentication', proxyAuthHelp: 'Enable this when a username and password are required.', enableProxyAuth: 'Enable proxy authentication', username: 'Username', password: 'Password', bypassList: 'Proxy bypass list', bypassHelp: 'These domains connect directly. Separate multiple domains with commas.', autoBypass: (count) => `${count} ${count === 1 ? 'domain was' : 'domains were'} added automatically. The proxy applies to AI model requests only.`, testing: 'Testing…', testCurrent: 'Test current configuration', proxyReachable: 'Proxy is reachable', proxyTestFailed: 'Proxy test failed', proxyTestError: 'Could not test proxy', + incognito: 'Incognito mode', incognitoHelp: 'Pause local memory, web search, and scheduled task triggers.', enableIncognito: 'Enable incognito mode', incognitoFailed: 'Could not change incognito mode', notifications: 'Send a system notification when finished', notificationsHelp: 'Notify when a response finishes or fails while the window is in the background.', notificationsFailed: 'Could not change notification settings', workspaceInstructions: 'Follow project instructions', workspaceInstructionsHelp: 'Automatically read existing AGENTS.md, CLAUDE.md, or GEMINI.md files in each project. Manage the files in their respective projects.', workspaceInstructionsFailed: 'Could not change project instruction settings', updateFailed: 'The setting was not applied. Try again later.', defaultModel: 'Default model', defaultModelHelp: 'Model used by new tasks.', notSet: 'Not set', saveDefaultModelFailed: 'Could not save the default model', defaultPermission: 'Default permission mode', defaultPermissionHelp: 'Initial permission mode for new tasks; it can be changed at any time.', saveDefaultPermissionFailed: 'Could not save the default permission mode', defaultThinking: 'Default thinking level', defaultThinkingHelp: 'Thinking level for new tasks; models that do not offer the chosen level use their own default.', followModelDefault: 'Follow model default', saveDefaultThinkingFailed: 'Could not save the default thinking level', proxy: 'Proxy server', proxyHelp: 'Configure a network proxy for AI model requests', enableProxy: 'Enable proxy server', saveNetworkFailed: 'Could not save network settings', proxyProtocol: 'Proxy protocol', serverAddress: 'Server address', port: 'Port', proxyAuth: 'Proxy authentication', proxyAuthHelp: 'Enable this when a username and password are required.', enableProxyAuth: 'Enable proxy authentication', username: 'Username', password: 'Password', bypassList: 'Proxy bypass list', bypassHelp: 'These domains connect directly. Separate multiple domains with commas.', autoBypass: (count) => `${count} ${count === 1 ? 'domain was' : 'domains were'} added automatically. The proxy applies to AI model requests only.`, testing: 'Testing…', testCurrent: 'Test current configuration', proxyReachable: 'Proxy is reachable', proxyTestFailed: 'Proxy test failed', proxyTestError: 'Could not test proxy', }, about: { - loadFailed: 'Could not load About information', loading: 'Loading About', unavailable: 'About information is unavailable', copied: 'Environment info copied', pasteHint: 'Paste it directly into an issue report', copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', devBuild: 'Local development build', packagedBuild: 'Release build', subtitle: 'A local-first AI assistant · Desktop runtime', privacyLabel: 'Privacy and security', privacyTitle: 'Local first · Private by default', privacyPoints: ['Conversations, settings, credentials, and Skill instructions stay in the local workspace.', 'Model keys stay in a local credential file; subscription tokens use secure system storage.', 'Maka sends no usage telemetry and contacts a model provider only when you enable it.', 'High-risk tool operations require explicit permission in the conversation.', 'Messages, tool calls, permission decisions, and mode changes are retained locally for each session.'], copying: 'Copying…', copyEnvironment: 'Copy environment info', copyHelp: 'Copy version and platform details to help diagnose an issue. The workspace path is excluded.', keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', reportIssueLabel: 'Report an issue', + loadFailed: 'Could not load About information', loading: 'Loading About', unavailable: 'About information is unavailable', copied: 'Environment info copied', pasteHint: 'Paste it directly into an issue report', copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', devBuild: 'Local development build', packagedBuild: 'Release build', subtitle: 'A local-first AI assistant · Desktop runtime', privacyLabel: 'Privacy and security', privacyTitle: 'Local first · Private by default', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay in the local workspace.', 'Model keys stay in a local credential file; subscription tokens use secure system storage.', 'Maka sends no usage telemetry and contacts a model provider only when you enable it.', 'High-risk tool operations require explicit permission in the task.', 'Messages, tool calls, permission decisions, and mode changes are retained locally for each task.'], copying: 'Copying…', copyEnvironment: 'Copy environment info', copyHelp: 'Copy version and platform details to help diagnose an issue. The workspace path is excluded.', keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', reportIssueLabel: 'Report an issue', updatesTitle: 'Software updates', checkForUpdates: 'Check for updates', checkingForUpdates: 'Checking…', diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts index eb760d6835..8ce3f968d9 100644 --- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts @@ -93,7 +93,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { zh: { runtimeHost: { title: 'Runtime Host', - description: '选择运行会话、自动化和后台工作的 Host。Local 使用这台设备。', + description: '选择运行任务、自动化和后台工作的 Host。Local 使用这台设备。', selected: 'Host', selectedHelp: '切换会立即生效;连接失败时继续使用当前 Host', remoteTitle: '远程 Host', @@ -147,11 +147,11 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { // Says all three layers of the rule in one sentence, because a help line // that only mentions the default would leave the user guessing what // happens before they set one. - sectionHelp: '新对话默认打开此项目;未设置时沿用上次使用的项目。任何对话都能在输入框旁临时切换。', + sectionHelp: '新任务默认打开此项目;未设置时沿用上次使用的项目。任何任务都能在输入框旁临时切换。', addProject: '添加项目', defaultBadge: '默认', setDefault: '设为默认', - setDefaultTitle: '新对话默认打开这个项目', + setDefaultTitle: '新任务默认打开这个项目', setDefaultDisabledTitle: '目录不可用,无法设为默认', setDefaultFailed: '设置默认项目失败', rename: '重命名', @@ -167,20 +167,20 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { remove: '从 Maka 移除', removeConfirmTitle: '从 Maka 移除这个项目?', // The one thing a user actually fears here, stated first and plainly. - removeConfirmBody: '仅从 Maka 的项目列表移除,磁盘上的文件不受影响。该项目下已有的对话会移到"未归属"分组,不会被删除。', + removeConfirmBody: '仅从 Maka 的项目列表移除,磁盘上的文件不受影响。该项目下已有的任务会移到"未归属"分组,不会被删除。', removeConfirm: '移除', removeCancel: '取消', actionFailed: '操作失败', unavailable: '目录不可用', - defaultUnavailable: '原来的默认项目已不可用,新对话暂时沿用上次使用的项目。', + defaultUnavailable: '原来的默认项目已不可用,新任务暂时沿用上次使用的项目。', emptyTitle: '还没有项目', - emptyBody: '添加一个项目目录后,新对话就能默认从它打开,侧边栏也会按项目归类对话。', + emptyBody: '添加一个项目目录后,新任务就能默认从它打开,侧边栏也会按项目归类任务。', moreActions: (projectName: string) => `更多操作:${projectName}`, }, en: { runtimeHost: { title: 'Runtime Host', - description: 'Choose the Host that runs sessions, automations, and background work. Local uses this device.', + description: 'Choose the Host that runs tasks, automations, and background work. Local uses this device.', selected: 'Host', selectedHelp: 'Switches immediately; if connection fails, Desktop keeps using the current Host', remoteTitle: 'Remote Hosts', @@ -233,11 +233,11 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { }, section: 'Workspace', sectionHelp: - 'New conversations open in the default project; without one, they reuse the project you last used. Any conversation can switch next to the input box.', + 'New tasks open in the default project; without one, they reuse the project you last used. Any task can switch next to the input box.', addProject: 'Add project', defaultBadge: 'Default', setDefault: 'Set as default', - setDefaultTitle: 'Open new conversations in this project', + setDefaultTitle: 'Open new tasks in this project', setDefaultDisabledTitle: 'The folder is unavailable, so this cannot be the default', setDefaultFailed: 'Could not set the default project', rename: 'Rename', @@ -251,16 +251,16 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { remove: 'Remove from Maka', removeConfirmTitle: 'Remove this project from Maka?', removeConfirmBody: - 'This only removes it from Maka’s project list; the files on disk are untouched. Conversations under this project move to “Ungrouped” and are not deleted.', + 'This only removes it from Maka’s project list; the files on disk are untouched. Tasks under this project move to “Ungrouped” and are not deleted.', removeConfirm: 'Remove', removeCancel: 'Cancel', actionFailed: 'Action failed', unavailable: 'Folder unavailable', defaultUnavailable: - 'The default project is no longer available, so new conversations reuse the project you last used.', + 'The default project is no longer available, so new tasks reuse the project you last used.', emptyTitle: 'No projects yet', emptyBody: - 'Add a project folder and new conversations can start in it, with the sidebar grouping conversations by project.', + 'Add a project folder and new tasks can start in it, with the sidebar grouping tasks by project.', moreActions: (projectName: string) => `More actions for ${projectName}`, }, } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/locales/settings-provider-copy.ts b/apps/desktop/src/renderer/locales/settings-provider-copy.ts index 254797918d..cb09b62e64 100644 --- a/apps/desktop/src/renderer/locales/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-provider-copy.ts @@ -61,7 +61,7 @@ const zhCopy = { credentials: '连接', dangerZone: '删除连接', deleteRowHelp: '此操作不可撤销。', credentialsHelp: '密钥只保存在本机。', credentialsHelpAccount: '登录令牌只保存在本机。', - modelManagementHelp: '这些模型会出现在对话的模型选择器里。', + modelManagementHelp: '这些模型会出现在任务的模型选择器里。', ...zhCapabilitiesCopy, capabilitiesHelp: '声明每个已启用模型的思考档位、视觉与上下文窗口;保存后生效。', // Row affordances (settings-sidebar 的 InfoRow / ExpandableRow 语言):一行 @@ -89,7 +89,7 @@ const zhCopy = { ? '这会退出本机账号、删除 OAuth 凭据和模型连接;刷新后不会自动重新创建。' : '这会删除模型连接及其本机凭据;如需再次使用,需要重新添加。', isDefault - ? '它当前是默认连接;删除后默认模型会变成未设置,已有对话可能需要重新选择模型。' + ? '它当前是默认连接;删除后默认模型会变成未设置,已有任务可能需要重新选择模型。' : '', ].filter(Boolean).join(' '), connectionSuccess: (name: string) => `连接成功 · ${name}`, connectionFailed: (name: string) => `连接失败 · ${name}`, @@ -118,7 +118,7 @@ const zhCopy = { tabs: { all: '全部', recommended: '推荐', accounts: '账号', plans: '模型计划', api: 'API', aggregators: '聚合服务', local: '本地' }, loadFailed: '载入模型连接失败', loadingAria: '正在加载模型供应商', connections: '模型连接', retry: '点击重试。', empty: '还没有模型连接', - emptyHelp: '从下方选择一种连接方式开始。', default: '默认', setDefault: '设为默认', setDefaultTitle: '让新对话默认使用这个连接', setDefaultPending: '设置中…', setDefaultFailed: '设为默认失败', addHelp: '选择账号登录、模型计划、API、聚合服务或本地运行时。', + emptyHelp: '从下方选择一种连接方式开始。', default: '默认', setDefault: '设为默认', setDefaultTitle: '让新任务默认使用这个连接', setDefaultPending: '设置中…', setDefaultFailed: '设为默认失败', addHelp: '选择账号登录、模型计划、API、聚合服务或本地运行时。', categoriesAria: '模型供应商分类', searchPlaceholder: '搜索服务商', searchAria: '搜索模型服务商', noMatch: '没有匹配的服务商', clearSearch: '清除搜索', createSubtitle: '完成必要配置后,连接会出现在模型页上方。', connection: '模型连接', count: (value: number) => `· ${value}`, connectTitle: (name: string) => `连接 ${name}`, diff --git a/apps/desktop/src/renderer/locales/settings-shared-copy.ts b/apps/desktop/src/renderer/locales/settings-shared-copy.ts index 55d9069064..02589153e4 100644 --- a/apps/desktop/src/renderer/locales/settings-shared-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-shared-copy.ts @@ -72,7 +72,7 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { ready: '就绪', groups: { memorySources: '记忆', - memorySourcesHelp: 'Maka 会在对话中记住你确认过的信息,用于之后的回答。', + memorySourcesHelp: 'Maka 会在任务中记住你确认过的信息,用于之后的回答。', memoryDocument: '记忆文件与备份', memoryDocumentHelp: '记忆保存在本机 MEMORY.md 里;这里可以直接编辑原文或恢复备份。', memoryEntries: '已记住的内容', @@ -82,7 +82,7 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { searchBehavior: '搜索行为', searchBehaviorHelp: '什么时候发起搜索,以及每次取回多少结果。', dataLocation: '数据位置', - dataLocationHelp: '会话、设置、使用统计与凭据都以文件形式存放在本机的这个位置。', + dataLocationHelp: '任务、设置、使用统计与凭据都以文件形式存放在本机的这个位置。', reviewSchedule: '回顾计划', reviewScheduleHelp: '每日回顾的生成时间与使用的模型。', buildInfo: '版本信息', @@ -122,7 +122,7 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { searchBehavior: 'Search behavior', searchBehaviorHelp: 'When a search runs, and how many results it returns.', dataLocation: 'Data location', - dataLocationHelp: 'Conversations, settings, usage statistics, and credentials are stored as files in this location on your machine.', + dataLocationHelp: 'Tasks, settings, usage statistics, and credentials are stored as files in this location on your machine.', reviewSchedule: 'Review schedule', reviewScheduleHelp: 'When the daily review runs, and which model writes it.', buildInfo: 'Build info', diff --git a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts index 03a27da730..3d26b4c94b 100644 --- a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts @@ -114,7 +114,7 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { name: '显示名称', namePlaceholder: '快速代码阅读', id: 'subagent_id', - idDescription: '创建后保持不变,主 Agent 和历史会话会用它识别此配置。', + idDescription: '创建后保持不变,主 Agent 和历史任务会用它识别此配置。', idPlaceholder: 'fast-reader', description: '适用场景', descriptionPlaceholder: '适合快速、低成本地阅读大型仓库', @@ -138,7 +138,7 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { }, remove: { title: (name) => `删除“${name}”?`, - description: '主 Agent 将不再看到这个配置。已创建的子会话不会被删除。', + description: '主 Agent 将不再看到这个配置。已创建的子任务不会被删除。', confirm: '删除', cancel: '取消', }, @@ -195,7 +195,7 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { name: 'Display name', namePlaceholder: 'Fast code reader', id: 'subagent_id', - idDescription: 'Stable after creation. The main agent and session history use it to identify this preset.', + idDescription: 'Stable after creation. The main agent and task history use it to identify this preset.', idPlaceholder: 'fast-reader', description: 'When to use', descriptionPlaceholder: 'Fast, low-cost exploration of large repositories', @@ -219,7 +219,7 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { }, remove: { title: (name) => `Remove “${name}”?`, - description: 'The main agent will no longer see this preset. Existing child sessions are not deleted.', + description: 'The main agent will no longer see this preset. Existing child tasks are not deleted.', confirm: 'Remove', cancel: 'Cancel', }, diff --git a/apps/desktop/src/renderer/locales/settings-usage-copy.ts b/apps/desktop/src/renderer/locales/settings-usage-copy.ts index b1f64dacdd..c4c58c0dc4 100644 --- a/apps/desktop/src/renderer/locales/settings-usage-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-usage-copy.ts @@ -29,7 +29,7 @@ const SETTINGS_USAGE_COPY = { tables: { providersAria: '使用统计供应商统计表', modelsAria: '使用统计模型统计表', toolsAria: '使用统计工具统计表', pricingAria: '使用统计定价配置表', requestsAria: '使用统计请求日志表', providerHeaders: ['供应商', '请求', 'Token', '费用'], modelHeaders: ['模型', '请求', 'Token', '费用'], toolHeaders: ['工具', '调用', '成功', '错误', '平均耗时'], - pricingHeaders: ['供应商', '模型', '输入 / 1M', '输出 / 1M'], requestHeaders: ['时间', '类型', '对象', '会话', 'Token', '费用', '延迟', '状态'], + pricingHeaders: ['供应商', '模型', '输入 / 1M', '输出 / 1M'], requestHeaders: ['时间', '类型', '对象', '任务', 'Token', '费用', '延迟', '状态'], noPricing: '暂无定价覆盖配置', modelKind: '模型', toolKind: '工具', openSession: (label) => `打开 ${label}`, success: '成功', error: '错误', providerEmptyTitle: '暂无供应商用量', providerEmptyBody: '完成一次模型请求后,这里会按供应商聚合请求数、Token 与费用。', modelEmptyTitle: '暂无模型用量', modelEmptyBody: '完成一次模型请求后,这里会按模型聚合请求数、Token 与费用。', @@ -49,7 +49,7 @@ const SETTINGS_USAGE_COPY = { tables: { providersAria: 'Usage by provider', modelsAria: 'Usage by model', toolsAria: 'Usage by tool', pricingAria: 'Usage pricing configuration', requestsAria: 'Usage request log', providerHeaders: ['Provider', 'Requests', 'Tokens', 'Cost'], modelHeaders: ['Model', 'Requests', 'Tokens', 'Cost'], toolHeaders: ['Tool', 'Calls', 'Success', 'Errors', 'Average duration'], - pricingHeaders: ['Provider', 'Model', 'Input / 1M', 'Output / 1M'], requestHeaders: ['Time', 'Type', 'Target', 'Session', 'Tokens', 'Cost', 'Latency', 'Status'], + pricingHeaders: ['Provider', 'Model', 'Input / 1M', 'Output / 1M'], requestHeaders: ['Time', 'Type', 'Target', 'Task', 'Tokens', 'Cost', 'Latency', 'Status'], noPricing: 'No pricing overrides', modelKind: 'Model', toolKind: 'Tool', openSession: (label) => `Open ${label}`, success: 'Success', error: 'Error', providerEmptyTitle: 'No provider usage', providerEmptyBody: 'After a model request, provider request counts, tokens, and costs appear here.', modelEmptyTitle: 'No model usage', modelEmptyBody: 'After a model request, request counts, tokens, and costs appear here by model.', diff --git a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts index 9e1b403c7b..c434795069 100644 --- a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts @@ -24,15 +24,15 @@ const SETTINGS_WEB_SEARCH_COPY = { credentialsCleared: '已清空 Tavily 凭据', credentialsClearedDetail: '联网搜索已自动关闭。', credentialValid: 'Tavily 凭据可用', resultCount: (count) => `返回 ${count} 条结果。`, testFailed: '联网搜索测试失败', testError: '联网搜索测试出错', enabled: '启用联网搜索', enabledHelp: '启用后,Maka 可以在需要最新外部信息时调用所选搜索来源。', provider: '搜索来源', providerHelp: '优先复用当前模型的服务端搜索;不支持时可显式改用 Tavily。', providerModel: '当前模型', providerTavily: 'Tavily', - modelCredential: '主模型原生搜索', modelCredentialHelp: 'Maka 会在每个对话回合开始时,根据当前连接与精确模型决定是否把原生 web_search 注入同一次模型请求。不保存第二份搜索密钥,也不会从设置页另发一次模型调用。', + modelCredential: '主模型原生搜索', modelCredentialHelp: 'Maka 会在每个任务回合开始时,根据当前连接与精确模型决定是否把原生 web_search 注入同一次模型请求。不保存第二份搜索密钥,也不会从设置页另发一次模型调用。', statusAria: '联网搜索凭据状态', lastTest: '最近测试 ', enabledAria: '启用联网搜索', key: 'Tavily 密钥', envKeyHelp: '当前使用环境变量 TAVILY_API_KEY / MAKA_TAVILY_API_KEY;如需改用保存的密钥,请移除环境变量后重启。', savedKeyHelp: '密钥只保存在本机。申请地址:', envPlaceholder: '由环境变量提供', storedPlaceholder: '已保存(输入新密钥可替换)', keyPlaceholder: 'tvly-xxxxxxxx', keyAria: 'Tavily 密钥', actions: '凭据操作', actionsHelp: '保存后可以测试一次真实请求;清空凭据会同步关闭联网搜索。', saving: '保存中…', saveKey: '保存密钥', testing: '测试中…', testKey: '测试凭据', clearing: '清空中…', clearKey: '清空密钥', - testSearch: '测试搜索', testSearchHelp: '发一条真实查询,确认所选联网搜索来源是否配置可用。结果只显示在这里,不写入会话。', queryPlaceholder: '例如:本周 AI 产品发布动态', + testSearch: '测试搜索', testSearchHelp: '发一条真实查询,确认所选联网搜索来源是否配置可用。结果只显示在这里,不写入任务。', queryPlaceholder: '例如:本周 AI 产品发布动态', searching: '搜索中…', search: '搜索', queryFailed: (error) => `查询失败:${error}`, noResults: '没有结果。', resultsAria: '联网搜索真实查询结果', disabledReasons: { noKey: '先配置所选搜索来源', disabled: '先启用联网搜索', noQuery: '输入查询后再搜索' }, - statuses: { valid: '已验证', invalid_credentials: '密钥无效', rate_limited: '服务限流', timeout: '测试超时', network_error: '网络异常', not_configured: '等待配置', untested: '未测试', validEnabled: '已验证 · 已启用', validDisabled: '已验证 · 未启用', unknownEnabled: '未测试 · 已启用', modelEnabled: '已启用 · 按会话模型判定', modelDisabled: '当前模型来源 · 未启用' }, + statuses: { valid: '已验证', invalid_credentials: '密钥无效', rate_limited: '服务限流', timeout: '测试超时', network_error: '网络异常', not_configured: '等待配置', untested: '未测试', validEnabled: '已验证 · 已启用', validDisabled: '已验证 · 未启用', unknownEnabled: '未测试 · 已启用', modelEnabled: '已启用 · 按任务模型判定', modelDisabled: '当前模型来源 · 未启用' }, sources: { model: '来源:当前模型连接', envWithSaved: '来源:环境变量(已保存密钥备用)', env: '来源:环境变量', saved: '来源:本机已保存密钥', none: '来源:未配置' }, errors: { invalid_query: '请输入有效的搜索内容。', incognito_active: '无痕模式下无法使用联网搜索。', not_configured: '所选搜索来源尚未配置完成。', invalid_credentials: '搜索来源拒绝了当前凭据,请更新后重试。', rate_limited: '搜索请求过于频繁,请稍后重试。', network_error: '网络请求失败,请检查网络后重试。', timeout: '搜索请求超时,请重试。', unsupported_provider: '当前模型不支持服务端搜索,或 Maka 尚未实现它的协议;可改用 Tavily。', experimental_disabled: '联网搜索实验功能当前已关闭。' }, }, @@ -46,10 +46,10 @@ const SETTINGS_WEB_SEARCH_COPY = { envKeyHelp: 'Currently using TAVILY_API_KEY / MAKA_TAVILY_API_KEY from the environment. Remove the environment variable and restart to use a saved key.', savedKeyHelp: 'The key is stored only on this machine. Apply at:', envPlaceholder: 'Provided by environment variable', storedPlaceholder: 'Saved (enter a new key to replace)', keyPlaceholder: 'tvly-xxxxxxxx', keyAria: 'Tavily key', actions: 'Credential actions', actionsHelp: 'After saving, test with a real request. Clearing credentials also disables web search.', saving: 'Saving…', saveKey: 'Save key', testing: 'Testing…', testKey: 'Test credentials', clearing: 'Clearing…', clearKey: 'Clear key', - testSearch: 'Test search', testSearchHelp: 'Send a real query to confirm the selected web search source is configured and working. Results appear here only and are not written to the conversation.', queryPlaceholder: 'For example: AI product launches this week', + testSearch: 'Test search', testSearchHelp: 'Send a real query to confirm the selected web search source is configured and working. Results appear here only and are not written to the task.', queryPlaceholder: 'For example: AI product launches this week', searching: 'Searching…', search: 'Search', queryFailed: (error) => `Query failed: ${error}`, noResults: 'No results.', resultsAria: 'Web search live query results', disabledReasons: { noKey: 'Configure the selected search source first', disabled: 'Enable web search first', noQuery: 'Enter a query before searching' }, - statuses: { valid: 'Verified', invalid_credentials: 'Invalid key', rate_limited: 'Rate limited', timeout: 'Test timed out', network_error: 'Network error', not_configured: 'Needs setup', untested: 'Not tested', validEnabled: 'Verified · enabled', validDisabled: 'Verified · disabled', unknownEnabled: 'Not tested · enabled', modelEnabled: 'Enabled · checked per session model', modelDisabled: 'Current model source · disabled' }, + statuses: { valid: 'Verified', invalid_credentials: 'Invalid key', rate_limited: 'Rate limited', timeout: 'Test timed out', network_error: 'Network error', not_configured: 'Needs setup', untested: 'Not tested', validEnabled: 'Verified · enabled', validDisabled: 'Verified · disabled', unknownEnabled: 'Not tested · enabled', modelEnabled: 'Enabled · checked per task model', modelDisabled: 'Current model source · disabled' }, sources: { model: 'Source: current model connection', envWithSaved: 'Source: environment variable (saved key available as backup)', env: 'Source: environment variable', saved: 'Source: key saved on this device', none: 'Source: not configured' }, errors: { invalid_query: 'Enter a valid search query.', incognito_active: 'Web search is unavailable in incognito mode.', not_configured: 'The selected search source is not configured.', invalid_credentials: 'The search provider rejected the current credential. Update it and try again.', rate_limited: 'The search provider is receiving too many requests. Try again later.', network_error: 'The network request failed. Check your connection and try again.', timeout: 'The search request timed out. Try again.', unsupported_provider: 'The current model does not support hosted search, or Maka has not implemented its protocol yet. Select Tavily to continue.', experimental_disabled: 'The experimental web search feature is currently disabled.' }, }, diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts index 03d6a02518..f63c510c1a 100644 --- a/apps/desktop/src/renderer/locales/shell-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-copy.ts @@ -47,7 +47,7 @@ type CommandCopy = { }; const STATIC_COMMAND_KEYWORDS: Record = { - 'action:new-chat': ['new', 'chat', 'start', '新', '建', '对话'], + 'action:new-chat': ['new', 'chat', 'start', '新', '建', '任务'], 'action:side-chat': [ 'side', 'chat', @@ -56,7 +56,7 @@ const STATIC_COMMAND_KEYWORDS: Record = { 'explore', '侧边', '侧聊', - '对话', + '任务', '追问', ], 'action:new-deep-research': ['deep', 'research', 'explore', 'readonly', '研究', '深度', '探索', '只读'], @@ -66,7 +66,7 @@ const STATIC_COMMAND_KEYWORDS: Record = { 'theme:light': ['light', 'theme', '浅色', '主题'], 'theme:dark': ['dark', 'theme', '深色', 'night', '主题'], 'theme:auto': ['auto', 'system', 'theme', '跟随', '系统', '主题'], - 'nav:sessions': ['sessions', 'chats', '会话', '对话', 'left'], + 'nav:sessions': ['sessions', 'chats', '任务', '任务', 'left'], 'nav:automations': ['automations', 'plan', 'task', 'schedule', 'cron', '定时任务', '计划', '提醒'], 'nav:skills': ['skills', '技能'], 'nav:mcp': ['mcp', 'server', 'tools', '扩展', '工具'], @@ -74,7 +74,7 @@ const STATIC_COMMAND_KEYWORDS: Record = { 'diag:open-workspace': ['workspace', 'folder', 'open', 'finder', '工作区', '文件夹', '目录'], 'diag:open-project-folder': ['project', 'folder', 'open', 'finder', '项目', '目录', '文件夹'], 'diag:open-skills': ['skills', 'folder', 'open', 'finder', '技能', '文件夹'], - 'diag:export-conversation': ['export', 'markdown', 'copy', 'conversation', '导出', '对话', '剪贴板', 'md'], + 'diag:export-conversation': ['export', 'markdown', 'copy', 'conversation', '导出', '任务', '剪贴板', 'md'], 'diag:save-conversation-file': [ 'save', 'file', @@ -83,7 +83,7 @@ const STATIC_COMMAND_KEYWORDS: Record = { 'export', '保存', '文件', - '对话', + '任务', '导出', 'md', ], @@ -451,7 +451,7 @@ type ShellCopy = { }; const ZH_STATIC_COMMANDS: Record = { - 'action:new-chat': { label: '新建对话', hint: '开始新的会话', group: '操作' }, + 'action:new-chat': { label: '新建任务', hint: '开始新的任务', group: '操作' }, 'action:side-chat': { label: '打开侧边对话', hint: '⌥⌘S', @@ -472,7 +472,7 @@ const ZH_STATIC_COMMANDS: Record = { 'theme:light': { label: '主题 · 浅色', group: '主题' }, 'theme:dark': { label: '主题 · 深色', group: '主题' }, 'theme:auto': { label: '主题 · 跟随系统', group: '主题' }, - 'nav:sessions': { label: '侧栏 · 会话', group: '导航' }, + 'nav:sessions': { label: '侧栏 · 任务', group: '导航' }, 'nav:automations': { label: '侧栏 · 定时任务', group: '导航' }, 'nav:skills': { label: '打开 · 技能', group: '导航' }, 'nav:mcp': { label: '打开 · MCP', group: '导航' }, @@ -493,12 +493,12 @@ const ZH_STATIC_COMMANDS: Record = { group: '诊断', }, 'diag:export-conversation': { - label: '导出当前对话为 Markdown', + label: '导出当前任务为 Markdown', hint: '复制到剪贴板', group: '诊断', }, 'diag:save-conversation-file': { - label: '保存当前对话为 .md 文件', + label: '保存当前任务为 .md 文件', hint: '用系统保存对话框', group: '诊断', }, @@ -536,8 +536,8 @@ const ZH_STATIC_COMMANDS: Record = { const EN_STATIC_COMMANDS: Record = { 'action:new-chat': { - label: 'New conversation', - hint: 'Start a new conversation', + label: 'New task', + hint: 'Start a new task', group: 'Actions', }, 'action:side-chat': { @@ -568,7 +568,7 @@ const EN_STATIC_COMMANDS: Record = { 'theme:light': { label: 'Theme · Light', group: 'Theme' }, 'theme:dark': { label: 'Theme · Dark', group: 'Theme' }, 'theme:auto': { label: 'Theme · Follow system', group: 'Theme' }, - 'nav:sessions': { label: 'Sidebar · Conversations', group: 'Navigation' }, + 'nav:sessions': { label: 'Sidebar · Tasks', group: 'Navigation' }, 'nav:automations': { label: 'Sidebar · Automations', group: 'Navigation' }, 'nav:skills': { label: 'Open · Skills', group: 'Navigation' }, 'nav:mcp': { label: 'Open · MCP', group: 'Navigation' }, @@ -589,12 +589,12 @@ const EN_STATIC_COMMANDS: Record = { group: 'Diagnostics', }, 'diag:export-conversation': { - label: 'Copy conversation as Markdown', + label: 'Copy task as Markdown', hint: 'Copy to clipboard', group: 'Diagnostics', }, 'diag:save-conversation-file': { - label: 'Save conversation as an .md file', + label: 'Save task as an .md file', hint: 'Use the system save dialog', group: 'Diagnostics', }, @@ -678,14 +678,14 @@ const SHELL_COPY_BY_LOCALE = { skills: 'Skills 文件夹', }, errors: { - messageRead: '对话内容暂时无法读取,请稍后重试。', - messageRefresh: '对话内容暂时无法刷新,请稍后重试。', + messageRead: '任务内容暂时无法读取,请稍后重试。', + messageRefresh: '任务内容暂时无法刷新,请稍后重试。', openPath: (path: string) => `无法打开${path},请稍后重试。`, workspaceUnavailableTitle: '工作目录不可用', workspaceUnavailableDescription: '工作目录不存在或无法访问。请选择有效目录创建新任务。', }, chatActions: { - newConversation: '新建对话', + newConversation: '新建任务', sendFailedTitle: '发送失败', sendFailedFallback: '消息暂时无法发送,请稍后重试。', skillInvocationBlockedTitle: 'Skill 调用失败,消息未发送', @@ -701,10 +701,10 @@ const SHELL_COPY_BY_LOCALE = { too_many_requests: 'Skill 调用请求超过 50 个上限', }, responseFailedTitle: '响应失败', - responseFailedFallback: '会话操作失败,请稍后重试。', - refreshFailedTitle: '刷新对话失败', - sessionStartFailedTitle: '开始对话失败', - sessionStartFailedFallback: '对话暂时无法开始,请稍后重试。', + responseFailedFallback: '任务操作失败,请稍后重试。', + refreshFailedTitle: '刷新任务失败', + sessionStartFailedTitle: '开始任务失败', + sessionStartFailedFallback: '任务暂时无法开始,请稍后重试。', }, projectActions: { currentProject: '当前项目', @@ -749,23 +749,23 @@ const SHELL_COPY_BY_LOCALE = { setDefaultSuccess: (name: string) => `已设为默认 · ${name}`, setDefaultFailedTitle: '切换默认失败', setDefaultFallback: '默认模型暂时无法切换,请稍后重试。', - newConversation: '新建对话', - conversationCopiedTitle: '已复制对话为 Markdown', + newConversation: '新建任务', + conversationCopiedTitle: '已复制任务为 Markdown', lineCount: (lines: number) => `${lines} 行 · 可粘贴到 Notion / Obsidian / GitHub`, copyFailedTitle: '复制失败', clipboardUnavailable: '剪贴板不可用', - conversationSavedTitle: '已保存当前对话', + conversationSavedTitle: '已保存当前任务', saveSummary: (lines: number, fileName: string) => `${lines} 行 · 保存为 ${fileName}`, saveFailedTitle: '保存失败', invalidExport: '导出内容无效', writeFailed: '无法写入选择的位置', - exportFallback: '导出当前对话失败,请稍后重试。', + exportFallback: '导出当前任务失败,请稍后重试。', memoryOpenFailedTitle: '无法打开 MEMORY.md', openFailedTitle: '打开失败', memoryOpenFallback: '无法打开 MEMORY.md,请稍后重试。', today: '今天', reviewCopiedTitle: '已复制今日回顾为 Markdown', - reviewSummary: (sessions: number, requests: number) => `${sessions} 个对话 · ${requests} 个请求`, + reviewSummary: (sessions: number, requests: number) => `${sessions} 个任务 · ${requests} 个请求`, reviewCopyFallback: '今日回顾暂时不可用,或剪贴板被系统拒绝。', reviewPastedTitle: '已追加今日回顾到输入框', reviewCopied: (label: string) => `已复制${label}回顾`, @@ -782,16 +782,16 @@ const SHELL_COPY_BY_LOCALE = { networkTestFallback: '网络代理测试暂时不可用,请稍后重试。', }, sessionRowActions: { - actionFallback: '会话操作失败,请稍后重试。', - flagFailedTitle: '标记会话失败', + actionFallback: '任务操作失败,请稍后重试。', + flagFailedTitle: '标记任务失败', unflagFailedTitle: '取消标记失败', - archiveFailedTitle: '归档会话失败', - unarchiveFailedTitle: '恢复会话失败', - renameFailedTitle: '重命名会话失败', - deleteFailedTitle: '删除会话失败', - currentConversation: '当前会话', + archiveFailedTitle: '归档任务失败', + unarchiveFailedTitle: '恢复任务失败', + renameFailedTitle: '重命名任务失败', + deleteFailedTitle: '删除任务失败', + currentConversation: '当前任务', deleteTitle: (name: string) => `删除 "${name}"`, - deleteDescription: '会话和全部消息会从磁盘上永久移除。该操作不可撤销。', + deleteDescription: '任务和全部消息会从磁盘上永久移除。该操作不可撤销。', deleteLabel: '删除', cancelLabel: '取消', deletedTitle: (name: string) => `已删除 ${name}`, @@ -901,7 +901,7 @@ const SHELL_COPY_BY_LOCALE = { permissionSwitched: (label: string) => `已切到 ${label}`, permissionFailedTitle: '切换权限模式失败', permissionFallback: '权限模式暂时无法切换,请稍后重试。', - modelSwitchedTitle: '已切换当前会话模型', + modelSwitchedTitle: '已切换当前任务模型', modelSwitchedDescription: (from, to) => `${from} → ${to}`, modelFailedTitle: '切换模型失败', modelFallback: '模型暂时无法切换,请稍后重试。', @@ -937,7 +937,7 @@ const SHELL_COPY_BY_LOCALE = { commandPalette: { label: '命令面板', searchLabel: '命令面板搜索', - placeholder: '搜索命令、设置项或会话…', + placeholder: '搜索命令、设置项或任务…', closeLabel: '关闭命令面板', resultsLabel: '命令面板结果', emptyTitle: '没有匹配的命令', @@ -950,7 +950,7 @@ const SHELL_COPY_BY_LOCALE = { settings: '设置', permissions: '权限', connections: '连接', - conversations: '会话', + conversations: '任务', }, staticKeywords: STATIC_COMMAND_KEYWORDS, commands: ZH_STATIC_COMMANDS, @@ -991,7 +991,7 @@ const SHELL_COPY_BY_LOCALE = { rows: [ { keys: ['⌘', 'K'], - description: '打开命令面板(跳会话 / 设置 / 主题等)', + description: '打开命令面板(跳任务 / 设置 / 主题等)', }, { keys: ['?'], description: '打开 / 关闭此快捷键面板' }, { keys: ['⌘', 'N'], description: '新建任务' }, @@ -1008,18 +1008,14 @@ const SHELL_COPY_BY_LOCALE = { ], }, { - heading: '会话列表', + heading: '任务列表', rows: [ - { keys: ['Tab'], description: '在会话与导航之间移动焦点' }, - { keys: ['↑', '↓'], description: '上下移动聚焦的会话' }, + { keys: ['Tab'], description: '在任务与导航之间移动焦点' }, + { keys: ['↑', '↓'], description: '上下移动聚焦的任务' }, { keys: ['Home', 'End'], description: '跳到列表顶部 / 底部' }, - { - keys: ['←', '→'], - description: '在会话 / 已标记 / 已归档之间循环切换', - }, - { keys: ['Enter'], description: '打开聚焦的会话' }, + { keys: ['Enter'], description: '打开聚焦的任务' }, { keys: ['Delete'], description: '弹出删除确认(永远不静默删除)' }, - { keys: ['F'], description: '聚焦会话列表搜索框(按 Esc 清空)' }, + { keys: ['F'], description: '聚焦任务列表搜索框(按 Esc 清空)' }, ], }, { @@ -1033,7 +1029,7 @@ const SHELL_COPY_BY_LOCALE = { heading: '面板调整', rows: [ { keys: ['Tab'], description: '聚焦左右分割条' }, - { keys: ['←', '→'], description: '微调会话列表宽度(±10 px)' }, + { keys: ['←', '→'], description: '微调任务列表宽度(±10 px)' }, { keys: ['Shift', '←', '→'], description: '快速调整(±50 px)' }, { keys: ['Home', 'End'], description: '直接拉到最小 / 最大宽度' }, ], @@ -1042,21 +1038,21 @@ const SHELL_COPY_BY_LOCALE = { }, chrome: { windowActions: '窗口快捷操作', - searchConversations: '搜索对话', + searchConversations: '搜索任务', expandSidebar: '展开侧边栏', collapseSidebar: '收起侧边栏', newTask: '新任务', - expandWorkbar: '展开会话工作栏', - collapseWorkbar: '收起会话工作栏', + expandWorkbar: '展开任务工作栏', + collapseWorkbar: '收起任务工作栏', workspaceActions: '工作区辅助操作', }, app: { - loadingWorkbarLabel: '正在加载会话工作栏', - loadingWorkbar: '正在加载会话工作栏…', + loadingWorkbarLabel: '正在加载任务工作栏', + loadingWorkbar: '正在加载任务工作栏…', useSkillPrompt: (skillName: string) => `使用 ${skillName} 技能:`, - newConversation: '新建对话', + newConversation: '新建任务', compactErrorTitle: '压缩失败', - compactErrorFallback: '对话暂时无法压缩,请稍后重试。', + compactErrorFallback: '任务暂时无法压缩,请稍后重试。', slashCommands: { compact: { name: '压缩上下文', description: '压缩旧历史并保留当前任务' }, graph: { name: '使用 Graph', description: '查看、切换或单次运行 Graph' }, @@ -1064,14 +1060,14 @@ const SHELL_COPY_BY_LOCALE = { swarm: { name: '使用 Swarm', description: '查看、切换或单次运行 Swarm' }, }, sideChatUnavailableTitle: '暂时无法打开侧边对话', - sideChatUnavailableDescription: '请先在主会话中发送一条消息,再使用 /side。', + sideChatUnavailableDescription: '请先在主任务中发送一条消息,再使用 /side。', sideChatContextPendingTitle: '先处理待发送的上下文', sideChatContextPendingDescription: '当前 Composer 还有附件、引用或文件 mention。请先发送或移除它们,再使用 /side。', resumeStartedTitle: '已开始安全恢复', resumeStartedDescription: '正在从最后一个完整执行边界继续', resumeFailedTitle: '恢复失败', - resumeFailedFallback: '无法启动安全恢复,请检查会话状态后重试。', + resumeFailedFallback: '无法启动安全恢复,请检查任务状态后重试。', goalClearFailedTitle: '停止目标失败', goalClearFailedFallback: '目标仍可能继续运行,请立即重试。', appearanceLoadErrorTitle: '载入外观设置失败', @@ -1081,7 +1077,7 @@ const SHELL_COPY_BY_LOCALE = { memoryErrorFallback: '本地记忆状态暂时无法刷新,请稍后重试。', openModelSettings: '打开设置 · 模型', sidebarCollapsed: '侧边栏已收起', - resizeConversationList: '调整对话列表宽度', + resizeConversationList: '调整任务列表宽度', skipErrorTitle: '跳过失败', tryAgainLater: '请稍后重试。', updateInstallFailedTitle: '无法安装更新', @@ -1095,17 +1091,17 @@ const SHELL_COPY_BY_LOCALE = { updateRetryFailedFallback: '请稍后重试,或手动下载最新版本。', loading: '加载中', goToModels: '去模型', - boundaryUnreadableTitle: '暂时读不到这个对话的权限', - boundaryUnreadableDetail: '在读到之前,这里暂时不能输入。可以重试,或先切换到别的对话。', + boundaryUnreadableTitle: '暂时读不到这个任务的权限', + boundaryUnreadableDetail: '在读到之前,这里暂时不能输入。可以重试,或先切换到别的任务。', boundaryUnreadableRetry: '重试', boundaryUnreadableRetrying: '重试中…', permissionModeChanging: '权限模式正在切换,完成后再继续操作。', - permissionModeStreaming: '当前对话正在流式输出,等结束后再切换权限模式。', - permissionModeRunning: '当前对话正在运行,等结束后再切换权限模式。', + permissionModeStreaming: '当前任务正在流式输出,等结束后再切换权限模式。', + permissionModeRunning: '当前任务正在运行,等结束后再切换权限模式。', permissionModeWaiting: '当前有工具调用正在等待确认,处理后再切换权限模式。', planModeChanging: 'Plan Mode 正在切换,完成后再继续操作。', - planModeStreaming: '当前对话正在流式输出,等结束后再切换 Plan Mode。', - planModeRunning: '当前对话正在运行,等结束后再切换 Plan Mode。', + planModeStreaming: '当前任务正在流式输出,等结束后再切换 Plan Mode。', + planModeRunning: '当前任务正在运行,等结束后再切换 Plan Mode。', planModeWaiting: '当前有工具调用正在等待确认,处理后再切换 Plan Mode。', planModeFailedTitle: '切换 Plan Mode 失败', planModeFallback: 'Plan Mode 暂时无法切换,请稍后重试。', @@ -1117,8 +1113,8 @@ const SHELL_COPY_BY_LOCALE = { planModeExecutionActiveTitle: '计划仍在执行', planModeExecutionActiveDescription: '请先中断当前执行,再进入 Plan Mode 调整方案。', swarmModeChanging: 'Swarm Mode 正在切换,完成后再继续操作。', - swarmModeStreaming: '当前对话正在流式输出,等结束后再切换 Swarm Mode。', - swarmModeRunning: '当前对话正在运行,等结束后再切换 Swarm Mode。', + swarmModeStreaming: '当前任务正在流式输出,等结束后再切换 Swarm Mode。', + swarmModeRunning: '当前任务正在运行,等结束后再切换 Swarm Mode。', swarmModeWaiting: '当前有工具调用正在等待确认,处理后再切换 Swarm Mode。', swarmModeFailedTitle: '切换 Swarm Mode 失败', swarmModeFallback: 'Swarm Mode 暂时无法切换,请稍后重试。', @@ -1126,15 +1122,15 @@ const SHELL_COPY_BY_LOCALE = { swarmModeDisabledTitle: 'Swarm Mode 未开启', swarmModeStatusDescription: '使用 /swarm on、/swarm off,或 /swarm <任务> 单次运行。', graphModeChanging: 'Graph Mode 正在切换,完成后再继续操作。', - graphModeStreaming: '当前对话正在流式输出,等结束后再切换 Graph Mode。', - graphModeRunning: '当前对话正在运行,等结束后再切换 Graph Mode。', + graphModeStreaming: '当前任务正在流式输出,等结束后再切换 Graph Mode。', + graphModeRunning: '当前任务正在运行,等结束后再切换 Graph Mode。', graphModeWaiting: '当前有工具调用正在等待确认,处理后再切换 Graph Mode。', graphModeFailedTitle: '切换 Graph Mode 失败', graphModeFallback: 'Graph Mode 暂时无法切换,请稍后重试。', graphModeEnabledTitle: 'Graph Mode 已开启', graphModeDisabledTitle: 'Graph Mode 未开启', graphModeStatusDescription: '使用 /graph on、/graph off,或 /graph <任务> 单次运行。', - resizeWorkbar: '调整会话工作栏宽度', + resizeWorkbar: '调整任务工作栏宽度', }, }, en: { @@ -1146,15 +1142,15 @@ const SHELL_COPY_BY_LOCALE = { skills: 'Skills folder', }, errors: { - messageRead: 'Conversation content is temporarily unavailable. Try again later.', - messageRefresh: 'Conversation content could not be refreshed. Try again later.', + messageRead: 'Task content is temporarily unavailable. Try again later.', + messageRefresh: 'Task content could not be refreshed. Try again later.', openPath: (path: string) => `Could not open the ${path}. Try again later.`, workspaceUnavailableTitle: 'Working directory unavailable', workspaceUnavailableDescription: 'The working directory does not exist or cannot be accessed. Select a valid folder for a new task.', }, chatActions: { - newConversation: 'New conversation', + newConversation: 'New task', sendFailedTitle: 'Message not sent', sendFailedFallback: 'The message could not be sent. Try again later.', skillInvocationBlockedTitle: 'Skill invocation failed; message not sent', @@ -1171,10 +1167,10 @@ const SHELL_COPY_BY_LOCALE = { too_many_requests: 'more than 50 distinct Skill invocation requests', }, responseFailedTitle: 'Response failed', - responseFailedFallback: 'The conversation action failed. Try again later.', - refreshFailedTitle: 'Could not refresh conversation', - sessionStartFailedTitle: 'Could not start conversation', - sessionStartFailedFallback: 'The conversation could not be started. Try again later.', + responseFailedFallback: 'The task action failed. Try again later.', + refreshFailedTitle: 'Could not refresh task', + sessionStartFailedTitle: 'Could not start task', + sessionStartFailedFallback: 'The task could not be started. Try again later.', }, projectActions: { currentProject: 'Current project', @@ -1219,23 +1215,23 @@ const SHELL_COPY_BY_LOCALE = { setDefaultSuccess: (name: string) => `Set as default · ${name}`, setDefaultFailedTitle: 'Could not change default', setDefaultFallback: 'The default model could not be changed. Try again later.', - newConversation: 'New conversation', - conversationCopiedTitle: 'Conversation copied as Markdown', + newConversation: 'New task', + conversationCopiedTitle: 'Task copied as Markdown', lineCount: (lines: number) => `${lines} lines · Ready for Notion / Obsidian / GitHub`, copyFailedTitle: 'Copy failed', clipboardUnavailable: 'Clipboard unavailable', - conversationSavedTitle: 'Conversation saved', + conversationSavedTitle: 'Task saved', saveSummary: (lines: number, fileName: string) => `${lines} lines · Saved as ${fileName}`, saveFailedTitle: 'Save failed', invalidExport: 'The export content is invalid', writeFailed: 'The selected location could not be written', - exportFallback: 'The conversation could not be exported. Try again later.', + exportFallback: 'The task could not be exported. Try again later.', memoryOpenFailedTitle: 'Could not open MEMORY.md', openFailedTitle: 'Open failed', memoryOpenFallback: 'MEMORY.md could not be opened. Try again later.', today: 'Today', reviewCopiedTitle: "Today's review copied as Markdown", - reviewSummary: (sessions: number, requests: number) => `${sessions} conversations · ${requests} requests`, + reviewSummary: (sessions: number, requests: number) => `${sessions} tasks · ${requests} requests`, reviewCopyFallback: "Today's review is unavailable, or the clipboard was denied.", reviewPastedTitle: "Today's review added to the composer", reviewCopied: (label: string) => `${label} review copied`, @@ -1252,17 +1248,17 @@ const SHELL_COPY_BY_LOCALE = { networkTestFallback: 'Network proxy testing is temporarily unavailable. Try again later.', }, sessionRowActions: { - actionFallback: 'The conversation action failed. Try again later.', - flagFailedTitle: 'Could not flag conversation', + actionFallback: 'The task action failed. Try again later.', + flagFailedTitle: 'Could not flag task', unflagFailedTitle: 'Could not remove flag', - archiveFailedTitle: 'Could not archive conversation', - unarchiveFailedTitle: 'Could not restore conversation', - renameFailedTitle: 'Could not rename conversation', - deleteFailedTitle: 'Could not delete conversation', - currentConversation: 'Current conversation', + archiveFailedTitle: 'Could not archive task', + unarchiveFailedTitle: 'Could not restore task', + renameFailedTitle: 'Could not rename task', + deleteFailedTitle: 'Could not delete task', + currentConversation: 'Current task', deleteTitle: (name: string) => `Delete "${name}"`, deleteDescription: - 'The conversation and all of its messages will be permanently removed from disk. This cannot be undone.', + 'The task and all of its messages will be permanently removed from disk. This cannot be undone.', deleteLabel: 'Delete', cancelLabel: 'Cancel', deletedTitle: (name: string) => `Deleted ${name}`, @@ -1373,7 +1369,7 @@ const SHELL_COPY_BY_LOCALE = { permissionSwitched: (label: string) => `Switched to ${label}`, permissionFailedTitle: 'Could not change permission mode', permissionFallback: 'The permission mode could not be changed. Try again later.', - modelSwitchedTitle: 'Conversation model changed', + modelSwitchedTitle: 'Task model changed', modelSwitchedDescription: (from, to) => `${from} → ${to}`, modelFailedTitle: 'Could not change model', modelFallback: 'The model could not be changed. Try again later.', @@ -1409,7 +1405,7 @@ const SHELL_COPY_BY_LOCALE = { commandPalette: { label: 'Command palette', searchLabel: 'Search the command palette', - placeholder: 'Search commands, settings, or conversations…', + placeholder: 'Search commands, settings, or tasks…', closeLabel: 'Close command palette', resultsLabel: 'Command palette results', emptyTitle: 'No matching commands', @@ -1422,7 +1418,7 @@ const SHELL_COPY_BY_LOCALE = { settings: 'Settings', permissions: 'Permissions', connections: 'Connections', - conversations: 'Conversations', + conversations: 'Tasks', }, staticKeywords: STATIC_COMMAND_KEYWORDS, commands: EN_STATIC_COMMANDS, @@ -1469,7 +1465,7 @@ const SHELL_COPY_BY_LOCALE = { rows: [ { keys: ['⌘', 'K'], - description: 'Open the command palette (conversations, Settings, themes, and more)', + description: 'Open the command palette (tasks, Settings, themes, and more)', }, { keys: ['?'], description: 'Open or close this shortcuts panel' }, { keys: ['⌘', 'N'], description: 'Create a new task' }, @@ -1489,32 +1485,28 @@ const SHELL_COPY_BY_LOCALE = { ], }, { - heading: 'Conversation list', + heading: 'Task list', rows: [ { keys: ['Tab'], - description: 'Move focus between conversations and navigation', + description: 'Move focus between tasks and navigation', }, { keys: ['↑', '↓'], - description: 'Move through focused conversations', + description: 'Move through focused tasks', }, { keys: ['Home', 'End'], description: 'Jump to the top or bottom of the list', }, - { - keys: ['←', '→'], - description: 'Cycle through Conversations, Flagged, and Archived', - }, - { keys: ['Enter'], description: 'Open the focused conversation' }, + { keys: ['Enter'], description: 'Open the focused task' }, { keys: ['Delete'], description: 'Open the delete confirmation (never delete silently)', }, { keys: ['F'], - description: 'Focus conversation search (press Esc to clear)', + description: 'Focus task search (press Esc to clear)', }, ], }, @@ -1537,7 +1529,7 @@ const SHELL_COPY_BY_LOCALE = { { keys: ['Tab'], description: 'Focus the left or right splitter' }, { keys: ['←', '→'], - description: 'Adjust conversation-list width (±10 px)', + description: 'Adjust task-list width (±10 px)', }, { keys: ['Shift', '←', '→'], @@ -1553,21 +1545,21 @@ const SHELL_COPY_BY_LOCALE = { }, chrome: { windowActions: 'Window shortcuts', - searchConversations: 'Search conversations', + searchConversations: 'Search tasks', expandSidebar: 'Expand sidebar', collapseSidebar: 'Collapse sidebar', newTask: 'New task', - expandWorkbar: 'Expand conversation workbar', - collapseWorkbar: 'Collapse conversation workbar', + expandWorkbar: 'Expand task workbar', + collapseWorkbar: 'Collapse task workbar', workspaceActions: 'Workspace actions', }, app: { - loadingWorkbarLabel: 'Loading conversation workbar', - loadingWorkbar: 'Loading conversation workbar…', + loadingWorkbarLabel: 'Loading task workbar', + loadingWorkbar: 'Loading task workbar…', useSkillPrompt: (skillName: string) => `Use the ${skillName} skill: `, - newConversation: 'New conversation', + newConversation: 'New task', compactErrorTitle: 'Compaction failed', - compactErrorFallback: 'The conversation could not be compacted. Try again later.', + compactErrorFallback: 'The task could not be compacted. Try again later.', slashCommands: { compact: { name: 'Compact context', description: 'Compact older history while preserving the current task' }, graph: { name: 'Use Graph', description: 'Inspect, switch, or run Graph once' }, @@ -1576,14 +1568,14 @@ const SHELL_COPY_BY_LOCALE = { }, sideChatUnavailableTitle: 'Side chat is not available yet', sideChatUnavailableDescription: - 'Send a message in the main conversation before using /side.', + 'Send a message in the main task before using /side.', sideChatContextPendingTitle: 'Resolve pending context first', sideChatContextPendingDescription: 'The Composer still has attachments, quotes, or file mentions. Send or remove them before using /side.', resumeStartedTitle: 'Safe recovery started', resumeStartedDescription: 'Continuing from the last complete execution boundary', resumeFailedTitle: 'Recovery failed', - resumeFailedFallback: 'Safe recovery could not start. Check the conversation state and try again.', + resumeFailedFallback: 'Safe recovery could not start. Check the task state and try again.', goalClearFailedTitle: 'Could not stop the goal', goalClearFailedFallback: 'The goal may still be running. Try again now.', appearanceLoadErrorTitle: 'Could not load appearance settings', @@ -1593,7 +1585,7 @@ const SHELL_COPY_BY_LOCALE = { memoryErrorFallback: 'Local memory status could not be refreshed. Try again later.', openModelSettings: 'Open Settings · Models', sidebarCollapsed: 'Sidebar is collapsed', - resizeConversationList: 'Resize conversation list', + resizeConversationList: 'Resize task list', skipErrorTitle: 'Could not skip onboarding', tryAgainLater: 'Try again later.', updateInstallFailedTitle: 'Could not install update', @@ -1607,19 +1599,19 @@ const SHELL_COPY_BY_LOCALE = { updateRetryFailedFallback: 'Try again later, or download the latest version manually.', loading: 'Loading', goToModels: 'Go to Models', - boundaryUnreadableTitle: 'Could not read this conversation’s permissions', + boundaryUnreadableTitle: 'Could not read this task’s permissions', boundaryUnreadableDetail: - 'Until they can be read, you cannot type here. Try again, or switch to another conversation.', + 'Until they can be read, you cannot type here. Try again, or switch to another task.', boundaryUnreadableRetry: 'Try again', boundaryUnreadableRetrying: 'Trying again…', permissionModeChanging: 'The permission mode is changing. Wait for it to finish before continuing.', permissionModeStreaming: - 'This conversation is streaming. Wait for it to finish before changing the permission mode.', - permissionModeRunning: 'This conversation is running. Wait for it to finish before changing the permission mode.', + 'This task is streaming. Wait for it to finish before changing the permission mode.', + permissionModeRunning: 'This task is running. Wait for it to finish before changing the permission mode.', permissionModeWaiting: 'A tool call is waiting for confirmation. Respond before changing the permission mode.', planModeChanging: 'Plan Mode is changing. Wait for it to finish before continuing.', - planModeStreaming: 'This conversation is streaming. Wait for it to finish before changing Plan Mode.', - planModeRunning: 'This conversation is running. Wait for it to finish before changing Plan Mode.', + planModeStreaming: 'This task is streaming. Wait for it to finish before changing Plan Mode.', + planModeRunning: 'This task is running. Wait for it to finish before changing Plan Mode.', planModeWaiting: 'A tool call is waiting for confirmation. Respond before changing Plan Mode.', planModeFailedTitle: 'Could not change Plan Mode', planModeFallback: 'Plan Mode could not be changed. Try again later.', @@ -1631,8 +1623,8 @@ const SHELL_COPY_BY_LOCALE = { planModeExecutionActiveTitle: 'The plan is still running', planModeExecutionActiveDescription: 'Interrupt the active execution before entering Plan Mode to revise it.', swarmModeChanging: 'Swarm Mode is changing. Wait for it to finish before continuing.', - swarmModeStreaming: 'This conversation is streaming. Wait for it to finish before changing Swarm Mode.', - swarmModeRunning: 'This conversation is running. Wait for it to finish before changing Swarm Mode.', + swarmModeStreaming: 'This task is streaming. Wait for it to finish before changing Swarm Mode.', + swarmModeRunning: 'This task is running. Wait for it to finish before changing Swarm Mode.', swarmModeWaiting: 'A tool call is waiting for confirmation. Respond before changing Swarm Mode.', swarmModeFailedTitle: 'Could not change Swarm Mode', swarmModeFallback: 'Swarm Mode could not be changed. Try again later.', @@ -1640,15 +1632,15 @@ const SHELL_COPY_BY_LOCALE = { swarmModeDisabledTitle: 'Swarm Mode is off', swarmModeStatusDescription: 'Use /swarm on, /swarm off, or /swarm for one turn.', graphModeChanging: 'Graph Mode is changing. Wait for it to finish before continuing.', - graphModeStreaming: 'This conversation is streaming. Wait for it to finish before changing Graph Mode.', - graphModeRunning: 'This conversation is running. Wait for it to finish before changing Graph Mode.', + graphModeStreaming: 'This task is streaming. Wait for it to finish before changing Graph Mode.', + graphModeRunning: 'This task is running. Wait for it to finish before changing Graph Mode.', graphModeWaiting: 'A tool call is waiting for confirmation. Respond before changing Graph Mode.', graphModeFailedTitle: 'Could not change Graph Mode', graphModeFallback: 'Graph Mode could not be changed. Try again later.', graphModeEnabledTitle: 'Graph Mode is on', graphModeDisabledTitle: 'Graph Mode is off', graphModeStatusDescription: 'Use /graph on, /graph off, or /graph for one turn.', - resizeWorkbar: 'Resize conversation workbar', + resizeWorkbar: 'Resize task workbar', }, }, } satisfies UiCatalog; diff --git a/apps/desktop/stories/command-search.stories.tsx b/apps/desktop/stories/command-search.stories.tsx index dbf2b9559c..1c0c0381e3 100644 --- a/apps/desktop/stories/command-search.stories.tsx +++ b/apps/desktop/stories/command-search.stories.tsx @@ -36,7 +36,7 @@ const threadResults: SearchResult[] = [ { source: 'thread', title: 'Benchmark 结果横评', - summary: '会话 · 今天 10:24', + summary: '任务 · 今天 10:24', snippet: '把 benchmark 输出整理成稳定的对比表,再补一轮 verifier。', target: { kind: 'thread', sessionId: 'session-benchmark', turnId: 'turn-benchmark-table' }, truncated: true, @@ -44,14 +44,14 @@ const threadResults: SearchResult[] = [ { source: 'thread', title: 'Command palette 搜索状态', - summary: '会话 · 昨天 18:42', + summary: '任务 · 昨天 18:42', snippet: 'content search blocked state 要保持 disabled,不能触发关闭。', target: { kind: 'thread', sessionId: 'session-command-search' }, }, { source: 'thread', title: 'Harbor adapter metadata', - summary: '会话 · 周一', + summary: '任务 · 周一', snippet: '确认 provider env passthrough,不要复制本地 adapter。', target: { kind: 'thread', sessionId: 'session-harbor', turnId: 'turn-provider-env' }, }, @@ -61,8 +61,8 @@ const paletteCommands: Command[] = [ { id: 'action:new-chat', kind: 'action', - label: '新建对话', - hint: '开始新的会话', + label: '新建任务', + hint: '开始新的任务', group: '操作', Icon: Plus, keywords: ['new', 'chat', '新建'], @@ -101,7 +101,7 @@ const paletteCommands: Command[] = [ { id: 'diag:export-conversation', kind: 'action', - label: '导出当前对话为 Markdown', + label: '导出当前任务为 Markdown', hint: '复制到剪贴板', group: '诊断', Icon: Download, @@ -113,9 +113,9 @@ const paletteCommands: Command[] = [ kind: 'session', label: '生成本周 benchmark 对比表', hint: '当前', - group: '会话', + group: '任务', Icon: MessageSquare, - keywords: ['benchmark', '会话'], + keywords: ['benchmark', '任务'], run: noop, }, ]; diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index 8e9f7cb81c..0cc16f7032 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -613,7 +613,7 @@ export const ToolPicker: Story = { render: () => , }; -// Real path: 会话工作栏 → 变更, showing the live branch comparison from the +// Real path: 任务工作栏 → 变更, showing the live branch comparison from the // session cwd. The panel is Git-backed; no message or tool-result fixture is // involved in this story. export const Changes: Story = { @@ -621,7 +621,7 @@ export const Changes: Story = { render: () => , }; -// Real path: sidebar → a session → 展开会话工作栏, landing on the tab the app +// Real path: sidebar → a session → 展开任务工作栏, landing on the tab the app // restored. Tasks is the default: an in-progress root, a child claimed and // blocked by a subagent, and the finished ones folded into 最近结束. export const Tasks: Story = { @@ -629,19 +629,19 @@ export const Tasks: Story = { render: () => , }; -// Real path: 会话工作栏 → 任务 on a session whose agent never wrote a task. +// Real path: 任务工作栏 → 任务 on a session whose agent never wrote a task. export const TasksEmpty: Story = { decorators: [bridge({ tasks: [] })], render: () => , }; -// Real path: 会话工作栏 → 任务 when `tasks.list` rejects; 重试 re-runs the read. +// Real path: 任务工作栏 → 任务 when `tasks.list` rejects; 重试 re-runs the read. export const TasksLoadFailed: Story = { decorators: [bridge({ tasksFail: true })], render: () => , }; -// Real path: 会话工作栏 → 文件, on a session whose agent wrote artifacts. The +// Real path: 任务工作栏 → 文件, on a session whose agent wrote artifacts. The // count in the tab is the pane's own filtered total, reported upward. // The pane's empty state renders the same EmptyState as TraceEmpty below, so it // is not a second story. @@ -650,7 +650,7 @@ export const Files: Story = { render: () => , }; -// Real path: 会话工作栏 → 追踪, on a session that has run turns — the overview +// Real path: 任务工作栏 → 追踪, on a session that has run turns — the overview // reads a context budget, token/cache figures and the session's facts off a // retried model call and a post-compaction call, while a turn that failed on a // denied tool sits in the raw record under the coverage notice the projection @@ -660,7 +660,7 @@ export const Trace: Story = { render: () => , }; -// Real path: 会话工作栏 → 追踪 on a long session whose latest call sits near the +// Real path: 任务工作栏 → 追踪 on a long session whose latest call sits near the // top of its window — the tier the context bands and their legend switch to // before a compaction, and the state a reader is most likely to open the tab // for. Same session as Trace, sized differently, so the two read side by side. @@ -669,7 +669,7 @@ export const TraceContextNearLimit: Story = { render: () => , }; -// Real path: 会话工作栏 → 追踪 on a session recorded before tool schemas carried +// Real path: 任务工作栏 → 追踪 on a session recorded before tool schemas carried // a name — the shape of every ledger written prior to #2323. The composition // block still has to show those bytes, as unnamed tools rather than as a // missing category, which is what gating the tool list on the NAMED rows alone @@ -679,7 +679,7 @@ export const TraceUnnamedTools: Story = { render: () => , }; -// Real path: 会话工作栏 → 追踪 when the durable metering record names the latest +// Real path: 任务工作栏 → 追踪 when the durable metering record names the latest // request but its best-effort capture never landed — the composition block has // to SAY so, since an absent section reads as "nothing to explain" and a zero // reads as an empty prompt. @@ -688,21 +688,21 @@ export const TraceCompositionUnrecorded: Story = { render: () => , }; -// Real path: 会话工作栏 → 追踪 on a session that has not run a turn yet — the +// Real path: 任务工作栏 → 追踪 on a session that has not run a turn yet — the // state the task-ledger e2e fixture opens on. export const TraceEmpty: Story = { decorators: [bridge()], render: () => , }; -// Real path: 会话工作栏 → 追踪 when `inspector.trace` reports a failed read (an +// Real path: 任务工作栏 → 追踪 when `inspector.trace` reports a failed read (an // unreadable or partially written run ledger); retry lives on the banner. export const TraceReadFailed: Story = { decorators: [bridge({ traceFail: true })], render: () => , }; -// Real path: 会话工作栏 → 追踪 on a workspace whose path overflows the panel +// Real path: 任务工作栏 → 追踪 on a workspace whose path overflows the panel // — the record-file row keeps its label and copy button, and the path alone // truncates. (The default stories above already show the row with a short // path; this variant pins the truncation contract.) diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts index b79d56041d..5a833de2b5 100644 --- a/packages/cli/src/pi-transcript.ts +++ b/packages/cli/src/pi-transcript.ts @@ -1702,7 +1702,7 @@ function renderWelcomeBlock(width: number): string[] { // `/`. The active model and connection live in the statusline, so the // welcome does not repeat them. const hints: [string, string][] = [ - ['/session', '切换或恢复会话'], + ['/session', '切换或恢复任务'], ['/model', '切换模型'], ['/setup', '配置模型提供商'], ]; diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 9cba1178e4..46fa817b51 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -1397,7 +1397,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { state.entries.push({ kind: 'notice', level: 'info', - text: '已回退到该轮之前(分支为新会话,原会话保留),该轮 prompt 已回填输入框,可修改后重新发送。', + text: '已回退到该轮之前(分支为新任务,原任务保留),该轮 prompt 已回填输入框,可修改后重新发送。', }); requestRender(); }; diff --git a/packages/runtime-host/src/server/scheduled-task-coordinator.ts b/packages/runtime-host/src/server/scheduled-task-coordinator.ts index 0ab4288153..7b2167b771 100644 --- a/packages/runtime-host/src/server/scheduled-task-coordinator.ts +++ b/packages/runtime-host/src/server/scheduled-task-coordinator.ts @@ -579,8 +579,8 @@ export class HostScheduledTaskCoordinator implements ScheduledTaskToolAuthority claim, 'ok', task.effect.kind === 'session_resume' - ? '已在原会话中继续执行。' - : '已启动 Agent 会话执行。', + ? '已在原任务中继续执行。' + : '已启动 Agent 任务执行。', 'fired', execution, ); diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index f242bf0b32..ffc9740e26 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -5043,7 +5043,7 @@ describe('SessionManager permission mode updates', () => { expect(afterFirstRuns.find((run) => run.turnId === 'turn-1')?.status).toBe('completed'); expect(afterFirstRuns.find((run) => run.turnId === 'turn-2')?.status).toBe('running'); - await expectRejects(manager.setPermissionMode(session.id, 'execute'), /当前对话正在运行/); + await expectRejects(manager.setPermissionMode(session.id, 'execute'), /当前任务正在运行/); secondGate.release(); await second.next(); @@ -11894,7 +11894,7 @@ describe('SessionManager permission mode updates', () => { expect((await store.readHeader(session.id)).status).toBe('waiting_for_user'); const [run] = await runStore.listSessionRuns(session.id); expect(run?.status).toBe('waiting_for_user'); - await expectRejects(manager.setPermissionMode(session.id, 'bypass'), /当前对话正在运行/); + await expectRejects(manager.setPermissionMode(session.id, 'bypass'), /当前任务正在运行/); expect((await store.readHeader(session.id)).permissionMode).toBe('ask'); await manager.respondToSandboxBoundary(session.id, { diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index 384cfa049e..0798d22da1 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -1676,7 +1676,7 @@ export class SessionManager { } if (this.runtimeKernel.hasActiveRuns(sessionId)) { - throw new Error('当前对话正在运行,等结束后再切换权限模式。'); + throw new Error('当前任务正在运行,等结束后再切换权限模式。'); } if (previous.status === 'waiting_for_user') { throw new Error('当前有工具调用正在等待确认,处理后再切换权限模式。'); @@ -1709,7 +1709,7 @@ export class SessionManager { kind: 'managed' | 'bypass', ): Promise { if (this.runtimeKernel.hasActiveRuns(sessionId)) { - throw new Error('当前对话正在运行,等结束后再切换沙箱边界。'); + throw new Error('当前任务正在运行,等结束后再切换沙箱边界。'); } const header = await this.deps.store.readHeader(sessionId); if (header.status === 'waiting_for_user') { @@ -1898,7 +1898,7 @@ export class SessionManager { throw new PlanConflictError('Linked child Sessions cannot enter Plan mode'); } if (this.runtimeKernel.hasActiveRuns(sessionId)) { - throw new Error('当前对话正在运行,等结束后再切换协作模式。'); + throw new Error('当前任务正在运行,等结束后再切换协作模式。'); } if (previous.status === 'waiting_for_user') { throw new Error('当前有工具调用正在等待确认,处理后再切换协作模式。'); diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index d9a24ed311..b66ef4f7de 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -324,13 +324,13 @@ export interface ConversationCopy { const CONVERSATION_COPY = { zh: { empty: { - ariaLabel: '开始对话', + ariaLabel: '开始任务', greeting: { morning: '早上好', noon: '中午好', afternoon: '下午好', evening: '晚上好' }, greetingTail: { morning: '清醒的早晨适合理清思路', noon: '专注的午间适合一鼓作气', afternoon: '舒缓的下午适合慢慢推进', evening: '安静的夜晚适合深度思考' }, headlineWithLabel: (greeting, label) => `${greeting} ${label},今天想做点什么?`, headlineFallback: (greeting, tail) => `${greeting},${tail}。`, }, deepResearchEmpty: { - ariaLabel: '深度研究空会话', eyebrow: '深度研究 · 只读探索', title: '先把项目读透,再决定怎么改。', intro: '这个会话固定在只读权限:优先阅读、搜索和分析代码;需要动手实现时,先输出文件、风险和验证命令。', + ariaLabel: '深度研究空任务', eyebrow: '深度研究 · 只读探索', title: '先把项目读透,再决定怎么改。', intro: '这个任务固定在只读权限:优先阅读、搜索和分析代码;需要动手实现时,先输出文件、风险和验证命令。', workflowAriaLabel: '深度研究流程', workflow: DEEP_RESEARCH_WORKFLOW_STEPS, reportAriaLabel: '深度研究输出结构', reportTitle: '输出必须能直接落地', report: DEEP_RESEARCH_REPORT_SECTIONS, scopeAriaLabel: '深度研究范围', scopeTitle: '默认按标准深度研究', scope: DEEP_RESEARCH_SCOPE_OPTIONS, @@ -345,8 +345,8 @@ const CONVERSATION_COPY = { interruptHint: '或点停止中断', addContext: '添加上下文', stagedContext: '附加内容', selectModel: '选择模型', dropToImport: '松开以导入文件内容', addingAttachment: '正在添加附件', addFileOrDirectory: '添加文件或目录', chooseSkill: '选择技能', noSkillsAvailable: '当前没有可用技能', - switchDisabledStreaming: '当前对话正在流式输出,等结束后再切换模型。', switchDisabledRunning: '当前对话正在运行,等结束后再切换模型。', switchDisabledPermission: '当前有工具调用正在等待确认,处理后再切换模型。', - thinkingDisabledStreaming: '当前对话正在流式输出,等结束后再切换思考级别。', thinkingDisabledRunning: '当前对话正在运行,等结束后再切换思考级别。', thinkingDisabledPermission: '当前有工具调用正在等待确认,处理后再切换思考级别。', + switchDisabledStreaming: '当前任务正在流式输出,等结束后再切换模型。', switchDisabledRunning: '当前任务正在运行,等结束后再切换模型。', switchDisabledPermission: '当前有工具调用正在等待确认,处理后再切换模型。', + thinkingDisabledStreaming: '当前任务正在流式输出,等结束后再切换思考级别。', thinkingDisabledRunning: '当前任务正在运行,等结束后再切换思考级别。', thinkingDisabledPermission: '当前有工具调用正在等待确认,处理后再切换思考级别。', planModeLabel: 'Plan', enablePlanMode: '开启 Plan Mode', disablePlanMode: '退出 Plan Mode', planModeOnTitle: 'Plan 模式已启用,点击关闭', swarmModeLabel: 'Swarm', enableSwarmMode: '开启 Swarm Mode', disableSwarmMode: '退出 Swarm Mode', @@ -360,9 +360,9 @@ const CONVERSATION_COPY = { // Short single-token labels — trigger + popout size to content. // Canonical ladder: 默认 / 关 / 低 / 中 / 高 / 超高 (minimal/max when offered). level: { off: '关', minimal: '最少', low: '低', medium: '中', high: '高', xhigh: '超高', max: '最高' }, - switching: '切换中', model: '模型', switchAriaLabel: '切换当前会话模型', + switching: '切换中', model: '模型', switchAriaLabel: '切换当前任务模型', switchWarning: '切换模型可能需要重建服务商提示缓存,使下一次请求更慢或成本更高。', - newChatAriaLabel: (label) => `选择新对话模型,当前 ${label}`, newChatTitle: (label) => `新对话使用的模型:${label}`, + newChatAriaLabel: (label) => `选择新任务模型,当前 ${label}`, newChatTitle: (label) => `新任务使用的模型:${label}`, configureAriaLabel: (label) => `配置模型连接,当前 ${label}`, configureTitle: '配置模型连接', }, permissions: { @@ -381,7 +381,7 @@ const CONVERSATION_COPY = { network: '网络访问', enabled: '已启用', reject: '拒绝', - allowSession: '本会话允许', + allowSession: '本任务允许', }, questions: { other: '其他', otherDescription: '输入一个不同的答案。', otherAriaLabel: '其他答案', otherPlaceholder: '输入你的答案', stop: '停止', stopping: '停止中…', previous: '上一题', submitting: '正在提交…', submit: '提交答案', next: '下一题' }, mentions: { noFiles: '未找到文件', noSkills: '暂无技能', noCommandsOrSkills: '没有匹配的命令或技能', filesAriaLabel: '工作区文件', skillsAriaLabel: '技能', commandsAndSkillsAriaLabel: '命令和技能', commandsGroup: '命令', skillsGroup: 'Skills', loading: '加载中…' }, @@ -393,16 +393,16 @@ const CONVERSATION_COPY = { messages: { you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', workingPhrases: ['正在琢磨…', '正在推敲…', '正在盘算…', '正在钻研…', '正在忙活…', '正在梳理…', '正在打磨…', '正在鼓捣…', '正在酝酿…', '正在攻坚…', '正在权衡…', '正在拾掇…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `${seconds} 秒后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, safeResumePending: '正在验证…', safeResume: '安全恢复', thinking: '深度思考', truncated: '已截断', copied: '已复制', copying: '复制中', copyFailed: '复制失败', copy: '复制', editMessage: '编辑并重发', editMessageDisabledRunning: '当前回答仍在进行中,结束后再编辑', editMessageDisabledAttachments: '包含附件的历史消息暂不支持编辑并重发', editMessageDisabledQuotes: '包含引用的历史消息暂不支持编辑并重发', editMessageDisabledTransformedText: '通过显式技能发送的历史消息暂不支持编辑并重发', userAriaLabel: '你发送的消息', systemAriaLabel: '系统消息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: '本轮回答操作', sourceAriaLabel: '本轮回答的来源', derivativesAriaLabel: '本轮回答的衍生', scheduledTaskTriggered: '定时任务触发', scheduledTaskTitle: (id) => `由定时任务触发 · ${id}`, legacyAutomationTriggered: '旧版自动化(仅历史)', legacyAutomationTitle: (id) => `由旧版自动化触发 · ${id} · 仅保留历史,不会再次执行`, goalContinued: 'Goal 自动继续', goalTitle: (id) => `由 Goal 继续执行 · ${id}`, agentGraphTriggered: 'Agent Graph 自动继续', agentGraphTitle: (graphId) => `由 Agent Graph 调度器触发 · ${graphId}`, - thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的会话日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中断)', abortedByStop: '(已中断 · 由停止按钮触发)', + thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中断)', abortedByStop: '(已中断 · 由停止按钮触发)', }, chat: { - memory: '记忆', memoryAriaLabel: '本地记忆已启用', memoryTitle: '本地 MEMORY.md 已加入 agent 系统提示。点击进入设置 · 记忆管理。', deepResearch: '深度研究', deepResearchAriaLabel: '深度研究,只读探索', deepResearchTitle: '深度研究会话使用只读探索边界:先阅读和分析,默认不改文件。', + memory: '记忆', memoryAriaLabel: '本地记忆已启用', memoryTitle: '本地 MEMORY.md 已加入 agent 系统提示。点击进入设置 · 记忆管理。', deepResearch: '深度研究', deepResearchAriaLabel: '深度研究,只读探索', deepResearchTitle: '深度研究任务使用只读探索边界:先阅读和分析,默认不改文件。', deepResearchProgress: { ariaLabel: '深度研究实时进度', title: '研究进度', - completedSummary: '研究完成 · 原会话保持只读', + completedSummary: '研究完成 · 原任务保持只读', activeSummary: (stage, scope, round) => `${stage} · ${scope} · 第 ${round} 轮`, - handoffTitle: '新建普通任务并填入研究 handoff;不会自动发送,也不会改变原研究会话权限', + handoffTitle: '新建普通任务并填入研究 handoff;不会自动发送,也不会改变原研究任务权限', handoffAction: '在新任务中继续实现', checklistTitle: '检查清单', reportTitle: '报告草稿', @@ -421,27 +421,27 @@ const CONVERSATION_COPY = { }, }, clearGoal: (condition, iteration, max, status) => `自主执行目标进行中:「${condition}」(第 ${iteration}/${max} 轮,${status})。系统每轮后自动续行;点击可清除目标、停止续行。`, clearGoalAriaLabel: (iteration, max) => `清除自主执行目标(已进行 ${iteration}/${max} 轮)`, goalProgress: (iteration, max) => `目标 ${iteration} / ${max}`, goalRunningAriaLabel: '自主目标正在运行', - loadFailed: '对话载入失败', loading: '载入中…', retryLoad: '重试载入', quoteSelection: '引用', askInSidePanel: '在侧栏追问', noMessages: '暂无消息', - branchBeforeInterrupt: '从中断前分支', sessionContextAriaLabel: '会话上下文', sessionLineageAriaLabel: '会话来源', sessionContextMore: (count) => `更多会话上下文(${count})`, - titlebarIdentityAriaLabel: '当前会话', openProjectFolder: (name) => `在文件管理器中打开「${name}」`, openProjectFolderAction: '打开项目文件夹', - openParentSession: (name) => `返回父会话「${name}」`, openParentSessionAction: '打开父会话', - revisionVersionsAriaLabel: '对话版本', revisionVersion: (current, total) => `版本 ${current} / ${total}`, previousRevision: '查看上一版本', nextRevision: '查看下一版本', + loadFailed: '任务载入失败', loading: '载入中…', retryLoad: '重试载入', quoteSelection: '引用', askInSidePanel: '在侧栏追问', noMessages: '暂无消息', + branchBeforeInterrupt: '从中断前分支', sessionContextAriaLabel: '任务上下文', sessionLineageAriaLabel: '任务来源', sessionContextMore: (count) => `更多任务上下文(${count})`, + titlebarIdentityAriaLabel: '当前任务', openProjectFolder: (name) => `在文件管理器中打开「${name}」`, openProjectFolderAction: '打开项目文件夹', + openParentSession: (name) => `返回父任务「${name}」`, openParentSessionAction: '打开父任务', + revisionVersionsAriaLabel: '任务版本', revisionVersion: (current, total) => `版本 ${current} / ${total}`, previousRevision: '查看上一版本', nextRevision: '查看下一版本', }, sessions: { status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', archived: '已归档', aborted: '已中止' }, blockedReason: { NO_REAL_CONNECTION: '等待配置可用模型连接', auth: '需要重新登录', permission_required: '等待权限确认', tool_failed: '工具调用失败', unknown: '运行中断,可重试' }, - listAriaLabel: '对话列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多对话`, renameAriaLabel: '重命名对话', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '对话正在流式响应中', staleTitle: '此会话使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '会话已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: '对话操作', pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: '最近', groupByTime: '按时间', groupByProject: '按项目', groupingAriaLabel: '会话分组方式', projectActionsAriaLabel: (name) => `${name} 项目操作`, projectNewTask: '新建任务', projectRename: '重命名', projectArchive: '归档', projectRestore: '恢复', projectRelink: '重新定位', projectUnavailable: '项目目录不可用', archivedProjects: '已归档项目', archivedProjectsAriaLabel: '展开已归档项目', worktreeAriaLabel: 'Git 工作树', promptRailAriaLabel: '按提问跳转', emptyPrompt: '(空提问)', jumpToPrompt: (preview) => `跳到提问:${preview}`, + listAriaLabel: '任务列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多任务`, renameAriaLabel: '重命名任务', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '任务正在流式响应中', staleTitle: '此任务使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '任务已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: '任务操作', pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: '最近', groupByTime: '按时间', groupByProject: '按项目', groupingAriaLabel: '任务分组方式', projectActionsAriaLabel: (name) => `${name} 项目操作`, projectNewTask: '新建任务', projectRename: '重命名', projectArchive: '归档', projectRestore: '恢复', projectRelink: '重新定位', projectUnavailable: '项目目录不可用', archivedProjects: '已归档项目', archivedProjectsAriaLabel: '展开已归档项目', worktreeAriaLabel: 'Git 工作树', promptRailAriaLabel: '按提问跳转', emptyPrompt: '(空提问)', jumpToPrompt: (preview) => `跳到提问:${preview}`, }, }, en: { empty: { - ariaLabel: 'Start a conversation', + ariaLabel: 'Start a task', greeting: { morning: 'Good morning', noon: 'Good afternoon', afternoon: 'Good afternoon', evening: 'Good evening' }, greetingTail: { morning: 'A clear morning is good for untangling ideas', noon: 'A focused midday is good for a single big push', afternoon: 'A calm afternoon is good for steady progress', evening: 'A quiet evening is good for deep thinking' }, headlineWithLabel: (greeting, label) => `${greeting} ${label} — what shall we tackle today?`, headlineFallback: (greeting, tail) => `${greeting} — ${tail}.`, }, deepResearchEmpty: { - ariaLabel: 'Empty Deep Research conversation', eyebrow: 'Deep Research · Read-only exploration', title: 'Understand the project before deciding what to change.', intro: 'This conversation stays read only: inspect, search, and analyze first. When implementation is needed, report the files, risks, and verification commands.', + ariaLabel: 'Empty Deep Research task', eyebrow: 'Deep Research · Read-only exploration', title: 'Understand the project before deciding what to change.', intro: 'This task stays read only: inspect, search, and analyze first. When implementation is needed, report the files, risks, and verification commands.', workflowAriaLabel: 'Deep Research workflow', workflow: [ { title: 'Find the entry points', body: 'Read the directory layout, configuration, startup path, and test entry points to build a project map.' }, { title: 'Trace the data flow', body: 'Follow key modules through IPC, storage, permissions, and runtime boundaries to the real implementation.' }, @@ -498,9 +498,9 @@ const CONVERSATION_COPY = { model: { thinkingLevel: 'Thinking level', thinkingUnsupported: 'This model does not support thinking-level changes', changeThinkingLevel: 'Change the current model thinking level', defaultLevel: 'Model default', level: { off: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Maximum' }, - switching: 'Switching', model: 'Model', switchAriaLabel: 'Switch model for this conversation', + switching: 'Switching', model: 'Model', switchAriaLabel: 'Switch model for this task', switchWarning: 'Switching may rebuild the provider prompt cache, making the next request slower or more expensive.', - newChatAriaLabel: (label) => `Choose a model for the new conversation, currently ${label}`, newChatTitle: (label) => `Model for the new conversation: ${label}`, + newChatAriaLabel: (label) => `Choose a model for the new task, currently ${label}`, newChatTitle: (label) => `Model for the new task: ${label}`, configureAriaLabel: (label) => `Configure model connections, currently ${label}`, configureTitle: 'Configure model connections', }, permissions: { @@ -519,7 +519,7 @@ const CONVERSATION_COPY = { network: 'Network access', enabled: 'Enabled', reject: 'Reject', - allowSession: 'Allow for this session', + allowSession: 'Allow for this task', }, questions: { other: 'Other', otherDescription: 'Enter a different answer.', otherAriaLabel: 'Other answer', otherPlaceholder: 'Enter your answer', stop: 'Stop', stopping: 'Stopping…', previous: 'Previous', submitting: 'Submitting…', submit: 'Submit answers', next: 'Next' }, mentions: { noFiles: 'No files found', noSkills: 'No skills available', noCommandsOrSkills: 'No matching commands or skills', filesAriaLabel: 'Workspace files', skillsAriaLabel: 'Skills', commandsAndSkillsAriaLabel: 'Commands and skills', commandsGroup: 'Commands', skillsGroup: 'Skills', loading: 'Loading…' }, @@ -531,16 +531,16 @@ const CONVERSATION_COPY = { messages: { you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${seconds}s (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, safeResumePending: 'Checking…', safeResume: 'Safe recovery', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages sent with an explicit skill', userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: 'Response actions', sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this one', scheduledTaskTriggered: 'Triggered by scheduled task', scheduledTaskTitle: (id) => `Triggered by scheduled task · ${id}`, legacyAutomationTriggered: 'Legacy Automation (history only)', legacyAutomationTitle: (id) => `Triggered by legacy Automation · ${id} · Historical only; it will not run again`, goalContinued: 'Continued by Goal', goalTitle: (id) => `Continued by Goal · ${id}`, agentGraphTriggered: 'Continued by Agent Graph', agentGraphTitle: (graphId) => `Triggered by the Agent Graph scheduler · ${graphId}`, - thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted session log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: '(Interrupted)', abortedByStop: '(Interrupted · Stop button)', + thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: '(Interrupted)', abortedByStop: '(Interrupted · Stop button)', }, chat: { memory: 'Memory', memoryAriaLabel: 'Local memory enabled', memoryTitle: 'Local MEMORY.md is included in the agent system prompt. Click to manage it in Settings · Memory.', deepResearch: 'Deep Research', deepResearchAriaLabel: 'Deep Research, read-only exploration', deepResearchTitle: 'Deep Research uses a read-only boundary: inspect and analyze first, without changing files by default.', deepResearchProgress: { ariaLabel: 'Live Deep Research progress', title: 'Research progress', - completedSummary: 'Research complete · Original session remains read-only', + completedSummary: 'Research complete · Original task remains read-only', activeSummary: (stage, scope, round) => `${stage} · ${scope} · Round ${round}`, - handoffTitle: 'Create a normal task with the research handoff. It will not send automatically or change the original research session permissions.', + handoffTitle: 'Create a normal task with the research handoff. It will not send automatically or change the original research task permissions.', handoffAction: 'Continue implementation in a new task', checklistTitle: 'Checklist', reportTitle: 'Report draft', @@ -559,16 +559,16 @@ const CONVERSATION_COPY = { }, }, clearGoal: (condition, iteration, max, status) => `Autonomous goal in progress: “${condition}” (iteration ${iteration}/${max}, ${status}). Maka continues after each iteration; click to clear the goal and stop continuing.`, clearGoalAriaLabel: (iteration, max) => `Clear autonomous goal after ${iteration}/${max} iterations`, goalProgress: (iteration, max) => `Goal ${iteration} of ${max}`, goalRunningAriaLabel: 'Autonomous goal running', - loadFailed: 'Conversation failed to load', loading: 'Loading…', retryLoad: 'Retry', quoteSelection: 'Quote', askInSidePanel: 'Ask in side panel', noMessages: 'No messages yet', - branchBeforeInterrupt: 'Branched before interruption', sessionContextAriaLabel: 'Session context', sessionLineageAriaLabel: 'Session origin', sessionContextMore: (count) => `More session context (${count})`, - titlebarIdentityAriaLabel: 'Current conversation', openProjectFolder: (name) => `Open “${name}” in the file manager`, openProjectFolderAction: 'Open project folder', - openParentSession: (name) => `Return to parent session “${name}”`, openParentSessionAction: 'Open parent session', - revisionVersionsAriaLabel: 'Conversation versions', revisionVersion: (current, total) => `Version ${current} of ${total}`, previousRevision: 'View previous version', nextRevision: 'View next version', + loadFailed: 'Task failed to load', loading: 'Loading…', retryLoad: 'Retry', quoteSelection: 'Quote', askInSidePanel: 'Ask in side panel', noMessages: 'No messages yet', + branchBeforeInterrupt: 'Branched before interruption', sessionContextAriaLabel: 'Task context', sessionLineageAriaLabel: 'Task origin', sessionContextMore: (count) => `More task context (${count})`, + titlebarIdentityAriaLabel: 'Current task', openProjectFolder: (name) => `Open “${name}” in the file manager`, openProjectFolderAction: 'Open project folder', + openParentSession: (name) => `Return to parent task “${name}”`, openParentSessionAction: 'Open parent task', + revisionVersionsAriaLabel: 'Task versions', revisionVersion: (current, total) => `Version ${current} of ${total}`, previousRevision: 'View previous version', nextRevision: 'View next version', }, sessions: { status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', archived: 'Archived', aborted: 'Stopped' }, blockedReason: { NO_REAL_CONNECTION: 'Waiting for an available model connection', auth: 'Sign in again', permission_required: 'Waiting for permission', tool_failed: 'Tool call failed', unknown: 'Run interrupted; retry available' }, - listAriaLabel: 'Conversation list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more conversations`, renameAriaLabel: 'Rename conversation', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This conversation is streaming a response', staleTitle: 'This conversation\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale conversation', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: 'Conversation actions', pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Conversation grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, + listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: 'Task actions', pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Task grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/daily-review-copy.ts b/packages/ui/src/daily-review-copy.ts index 95dfec614f..2e704f6744 100644 --- a/packages/ui/src/daily-review-copy.ts +++ b/packages/ui/src/daily-review-copy.ts @@ -87,14 +87,14 @@ export interface DailyReviewCopy { const DAILY_REVIEW_COPY = { zh: { archive: { - section: { summary: '对话摘要', gaps: '遗漏提醒', usage: '使用洞察', code: '代码建议' }, + section: { summary: '任务摘要', gaps: '遗漏提醒', usage: '使用洞察', code: '代码建议' }, status: { ok: '已生成', no_model: '缺少模型', no_data: '无数据', failed: '生成失败', skipped: '已跳过' }, trigger: { cron: '定时', manual: '手动' }, title: (date, mode) => `${date} · ${mode}`, range: { 1: '单日', 7: '7 天', 30: '30 天' }, generated: (trigger, time) => `${trigger}生成 ${time}`, - sessionCount: (count) => `${count} 对话`, - defaultModel: '默认对话模型', + sessionCount: (count) => `${count} 任务`, + defaultModel: '默认任务模型', opening: '正在打开这份报告…', noContent: '这份报告没有生成正文内容。', noContentHelp: '这一天没有归档内容。', @@ -104,7 +104,7 @@ const DAILY_REVIEW_COPY = { unit: { day: '天', week: '周', month: '月' }, earlier: (unit) => `查看更早一${unit}`, later: (unit) => `查看更晚一${unit}`, }, emptyOverview: { - todayTitle: '等待记录今天活动', rangeTitle: (label) => `${label}无活动`, todayBody: '今天还没有发起对话,也没有调用模型。', rangeBody: (label) => `${label}范围内没有发起对话,也没有调用模型。`, + todayTitle: '等待记录今天活动', rangeTitle: (label) => `${label}无活动`, todayBody: '今天还没有发起任务,也没有调用模型。', rangeBody: (label) => `${label}范围内没有发起任务,也没有调用模型。`, }, export: { ariaLabel: '回顾导出操作', copyTitle: '复制为 Markdown 摘要,方便分享 / 贴到笔记', copying: '复制中…', copy: '复制', appendTitle: '追加到当前输入框草稿', appending: '追加中…', append: '粘到输入框', saveTitle: '保存为 Markdown 文件', saving: '保存中…', save: '保存', @@ -113,23 +113,23 @@ const DAILY_REVIEW_COPY = { title: '每日回顾', generateAnalysis: '生成分析', retryAnalysis: '重新生成', viewAnalysis: '查看分析', backToActivity: '返回活动', timeRange: '时间范围', rangeOptions: [['1', '今日'], ['7', '最近 7 天'], ['30', '最近 30 天']], rangeSwitch: '时间范围切换', }, overview: { - ariaLabel: (label) => `${label}概览`, refreshFailed: (error) => `每日回顾刷新失败:${error}`, retry: '重试', conversations: '对话', requests: '请求', tokens: 'Token', cost: '费用', activeConversations: '活跃对话', + ariaLabel: (label) => `${label}概览`, refreshFailed: (error) => `每日回顾刷新失败:${error}`, retry: '重试', conversations: '任务', requests: '请求', tokens: 'Token', cost: '费用', activeConversations: '活跃任务', }, errorFallback: '每日回顾暂时不可用,请稍后重试。', markdown: { - separator: ':', title: (dayLabel) => `# Maka · 每日回顾 · ${dayLabel}`, conversations: '对话', requests: '请求', tokens: 'Token', cost: '费用', errors: '错误', activeConversations: '活跃对话', modelUsage: '模型使用', toolCalls: '工具调用', requestCount: (count) => `${count} 次`, + separator: ':', title: (dayLabel) => `# Maka · 每日回顾 · ${dayLabel}`, conversations: '任务', requests: '请求', tokens: 'Token', cost: '费用', errors: '错误', activeConversations: '活跃任务', modelUsage: '模型使用', toolCalls: '工具调用', requestCount: (count) => `${count} 次`, }, }, en: { archive: { - section: { summary: 'Conversation summary', gaps: 'Missed items', usage: 'Usage insights', code: 'Code suggestions' }, + section: { summary: 'Task summary', gaps: 'Missed items', usage: 'Usage insights', code: 'Code suggestions' }, status: { ok: 'Generated', no_model: 'Model unavailable', no_data: 'No data', failed: 'Generation failed', skipped: 'Skipped' }, trigger: { cron: 'Scheduled', manual: 'Manual' }, title: (date, mode) => `${date} · ${mode}`, range: { 1: '1 day', 7: '7 days', 30: '30 days' }, generated: (trigger, time) => `${trigger} · ${time}`, sessionCount: (count) => `${count} ${count === 1 ? 'conversation' : 'conversations'}`, - defaultModel: 'Default conversation model', + defaultModel: 'Default task model', opening: 'Opening this report…', noContent: 'This report has no generated content.', noContentHelp: 'Nothing archived for this day.', @@ -139,7 +139,7 @@ const DAILY_REVIEW_COPY = { unit: { day: 'day', week: 'week', month: 'month' }, earlier: (unit) => `View previous ${unit}`, later: (unit) => `View next ${unit}`, }, emptyOverview: { - todayTitle: "Waiting for today's activity", rangeTitle: (label) => `No activity for ${label.toLowerCase()}`, todayBody: 'No conversations or model requests have started today.', rangeBody: (label) => `No conversations or model requests were made during ${label.toLowerCase()}.`, + todayTitle: "Waiting for today's activity", rangeTitle: (label) => `No activity for ${label.toLowerCase()}`, todayBody: 'No conversations or model requests have started today.', rangeBody: (label) => `No tasks or model requests were made during ${label.toLowerCase()}.`, }, export: { ariaLabel: 'Review export actions', copyTitle: 'Copy a Markdown summary to share or add to notes', copying: 'Copying…', copy: 'Copy', appendTitle: 'Append to the current composer draft', appending: 'Appending…', append: 'Add to composer', saveTitle: 'Save as a Markdown file', saving: 'Saving…', save: 'Save', @@ -148,11 +148,11 @@ const DAILY_REVIEW_COPY = { title: 'Daily review', generateAnalysis: 'Generate analysis', retryAnalysis: 'Generate again', viewAnalysis: 'View analysis', backToActivity: 'Back to activity', timeRange: 'Time range', rangeOptions: [['1', 'Today'], ['7', 'Last 7 days'], ['30', 'Last 30 days']], rangeSwitch: 'Change time range', }, overview: { - ariaLabel: (label) => `${label} overview`, refreshFailed: (error) => `Failed to refresh daily review: ${error}`, retry: 'Retry', conversations: 'Conversations', requests: 'Requests', tokens: 'Tokens', cost: 'Cost', activeConversations: 'Active conversations', + ariaLabel: (label) => `${label} overview`, refreshFailed: (error) => `Failed to refresh daily review: ${error}`, retry: 'Retry', conversations: 'Tasks', requests: 'Requests', tokens: 'Tokens', cost: 'Cost', activeConversations: 'Active tasks', }, errorFallback: 'Daily review is temporarily unavailable. Try again later.', markdown: { - separator: ':', title: (dayLabel) => `# Maka · Daily review · ${dayLabel}`, conversations: 'Conversations', requests: 'Requests', tokens: 'Tokens', cost: 'Cost', errors: 'Errors', activeConversations: 'Active conversations', modelUsage: 'Model usage', toolCalls: 'Tool calls', requestCount: (count) => `${count} ${count === 1 ? 'request' : 'requests'}`, + separator: ':', title: (dayLabel) => `# Maka · Daily review · ${dayLabel}`, conversations: 'Tasks', requests: 'Requests', tokens: 'Tokens', cost: 'Cost', errors: 'Errors', activeConversations: 'Active tasks', modelUsage: 'Model usage', toolCalls: 'Tool calls', requestCount: (count) => `${count} ${count === 1 ? 'request' : 'requests'}`, }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/runtime-resume-copy.ts b/packages/ui/src/runtime-resume-copy.ts index a3fa01f358..1385940904 100644 --- a/packages/ui/src/runtime-resume-copy.ts +++ b/packages/ui/src/runtime-resume-copy.ts @@ -38,8 +38,8 @@ const RESUME_PARK_REASON_COPY: Readonly> = { export function resumeParkToastCopy(reasons: readonly string[]): ResumeParkToastCopy { if (reasons.length === 1 && reasons[0] === 'resume_candidate_missing') { return { - title: '没有可恢复的对话', - description: '会话已是最新状态。', + title: '没有可恢复的任务', + description: '任务已是最新状态。', }; } @@ -53,6 +53,6 @@ export function resumeParkToastCopy(reasons: readonly string[]): ResumeParkToast title: '暂时无法安全恢复', description: descriptions.length > 0 ? descriptions.join(' ') - : '当前会话不满足安全恢复条件。', + : '当前任务不满足安全恢复条件。', }; } diff --git a/packages/ui/src/scheduled-task-copy.ts b/packages/ui/src/scheduled-task-copy.ts index 3c285d4467..c94489c743 100644 --- a/packages/ui/src/scheduled-task-copy.ts +++ b/packages/ui/src/scheduled-task-copy.ts @@ -166,7 +166,7 @@ const SCHEDULED_TASK_COPY = { runStatus: { ok: '已触发', blocked: '已阻止', failed: '失败' }, delivery: { local: '本地提醒', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `触发后投递到:${target}` }, form: { - editTitle: '编辑定时任务', createTitle: '新建定时任务', useTemplate: '使用模板', field: { title: '标题', time: '提醒时间', channel: '方式', recurrence: '重复', platform: '平台', cron: 'Cron', chatId: 'Chat ID', note: '备注' }, titlePlaceholder: '例如:明天复盘项目进度', groupSchedule: '频率', groupDelivery: '投递', presetsAriaLabel: '快速设置提醒时间', presets: [['ten-minutes', '10 分钟后'], ['one-hour', '1 小时后'], ['tomorrow-morning', '明天 9 点'], ['next-monday', '下周一 9 点']], recurrenceOptions: [['none', '不重复'], ['daily', '每天'], ['weekly', '每周'], ['monthly', '每月'], ['cron', 'Cron']], deliveryOptions: [['local', '本地提醒'], ['bot', '机器人聊天']], agentRunOption: 'Agent 会话执行', intervalOption: '固定间隔(由 Agent 创建)', cronPlaceholder: '例如 0 9 * * 1-5', chatIdPlaceholder: '例如 Telegram chat_id', deliveryHelp: (providers) => `当前可投递到 ${providers};其它机器人平台不会出现在投递目标里。`, notePlaceholder: '可选:补充需要提醒的上下文', saving: '保存中…', creating: '创建中…', save: '保存', create: '创建', + editTitle: '编辑定时任务', createTitle: '新建定时任务', useTemplate: '使用模板', field: { title: '标题', time: '提醒时间', channel: '方式', recurrence: '重复', platform: '平台', cron: 'Cron', chatId: 'Chat ID', note: '备注' }, titlePlaceholder: '例如:明天复盘项目进度', groupSchedule: '频率', groupDelivery: '投递', presetsAriaLabel: '快速设置提醒时间', presets: [['ten-minutes', '10 分钟后'], ['one-hour', '1 小时后'], ['tomorrow-morning', '明天 9 点'], ['next-monday', '下周一 9 点']], recurrenceOptions: [['none', '不重复'], ['daily', '每天'], ['weekly', '每周'], ['monthly', '每月'], ['cron', 'Cron']], deliveryOptions: [['local', '本地提醒'], ['bot', '机器人聊天']], agentRunOption: 'Agent 任务执行', intervalOption: '固定间隔(由 Agent 创建)', cronPlaceholder: '例如 0 9 * * 1-5', chatIdPlaceholder: '例如 Telegram chat_id', deliveryHelp: (providers) => `当前可投递到 ${providers};其它机器人平台不会出现在投递目标里。`, notePlaceholder: '可选:补充需要提醒的上下文', saving: '保存中…', creating: '创建中…', save: '保存', create: '创建', }, page: { title: '定时任务', refreshing: '正在刷新定时任务', refresh: '刷新定时任务', create: '新建定时任务', keepAwake: '保持系统唤醒', pageSettings: '定时任务页面设置', keepAwakeErrorTitle: '无法更新保持系统唤醒', keepAwakeErrorFallback: '更新保持系统唤醒设置失败,请稍后重试。', viewsAriaLabel: '定时任务视图', tasks: '我的定时任务', runs: '执行记录', filtersAriaLabel: '定时任务筛选', sort: '排序', sortOptions: [['created-desc', '按创建时间倒序'], ['next-run-asc', '按下次触发升序'], ['updated-desc', '按更新时间倒序']], searchLabel: '搜索定时任务', searchPlaceholder: '搜索标题、备注、投递或执行记录…', state: '状态', filterOption: (label, count) => `${label} ${count}`, active: '进行中', all: '全部', range: '范围', rangeOptions: [['day', '今天'], ['week', '近 7 天'], ['month', '近 30 天'], ['all', '全部记录']], searchMatches: (count) => `找到 ${count} 个匹配提醒`, clearSearch: '清除搜索', noSearchTitle: '没有匹配的提醒', noFilterTitle: '当前筛选没有提醒', noSearchBody: '调整搜索词,或切换状态筛选查看其他提醒。', noFilterBody: '切换筛选查看其他状态,或创建新的定时任务。', emptyTitle: '还没有定时任务', emptyBody: '创建一个提醒,让 Maka 在指定时间继续这项工作。', listAriaLabel: '定时任务列表', inspectorOpened: (title) => `已打开「${title}」的任务详情`, edit: '编辑', duplicate: '复制', triggering: '触发中…', triggerNow: '立即触发', snoozing: '延后中…', snooze: '延后 10 分钟', clearing: '清空中…', clearRuns: '清空记录', deleting: '删除中…', delete: '删除', nextRun: (time) => `下次触发:${time}`, recentRun: (time) => `最近 ${time}`, unscheduled: '未安排', noRunsTitle: '暂无执行记录', noRunsBody: '提醒触发、手动执行或投递失败后,会在这里保留最近记录。', showAllTime: '显示全部时间', runsAriaLabel: '定时任务执行记录', activeCount: (count) => `${count} 个进行中`, @@ -182,8 +182,8 @@ const SCHEDULED_TASK_COPY = { runs: '执行记录', noRuns: '这个任务还没有执行记录。', agentSource: 'Agent 定时任务', - agentSourceHint: '到点后,Maka 会使用创建时的执行设置启动新会话。', - agentDelivery: 'Agent 会话执行', + agentSourceHint: '到点后,Maka 会使用创建时的执行设置启动新任务。', + agentDelivery: 'Agent 任务执行', }, }, en: { @@ -201,7 +201,7 @@ const SCHEDULED_TASK_COPY = { runStatus: { ok: 'Triggered', blocked: 'Blocked', failed: 'Failed' }, delivery: { local: 'Local notification', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `Deliver to: ${target}` }, form: { - editTitle: 'Edit scheduled task', createTitle: 'New scheduled task', useTemplate: 'Use template', field: { title: 'Title', time: 'Task time', channel: 'Method', recurrence: 'Repeat', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, titlePlaceholder: 'For example: Review project progress tomorrow', groupSchedule: 'Frequency', groupDelivery: 'Delivery', presetsAriaLabel: 'Quick task times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], agentRunOption: 'Agent session run', intervalOption: 'Fixed interval (created by Agent)', cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this task', saving: 'Saving…', creating: 'Creating…', save: 'Save', create: 'Create', + editTitle: 'Edit scheduled task', createTitle: 'New scheduled task', useTemplate: 'Use template', field: { title: 'Title', time: 'Task time', channel: 'Method', recurrence: 'Repeat', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, titlePlaceholder: 'For example: Review project progress tomorrow', groupSchedule: 'Frequency', groupDelivery: 'Delivery', presetsAriaLabel: 'Quick task times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], agentRunOption: 'Agent task run', intervalOption: 'Fixed interval (created by Agent)', cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this task', saving: 'Saving…', creating: 'Creating…', save: 'Save', create: 'Create', }, page: { title: 'Scheduled tasks', refreshing: 'Refreshing scheduled tasks', refresh: 'Refresh scheduled tasks', create: 'New scheduled task', keepAwake: 'Keep system awake', pageSettings: 'Scheduled task page settings', keepAwakeErrorTitle: 'Could not update Keep system awake', keepAwakeErrorFallback: 'Could not update the Keep system awake setting. Try again later.', viewsAriaLabel: 'Scheduled task views', tasks: 'My scheduled tasks', runs: 'Run history', filtersAriaLabel: 'Scheduled task filters', sort: 'Sort', sortOptions: [['created-desc', 'Newest created first'], ['next-run-asc', 'Next run first'], ['updated-desc', 'Recently updated first']], searchLabel: 'Search scheduled tasks', searchPlaceholder: 'Search titles, notes, delivery, or run history…', state: 'Status', filterOption: (label, count) => `${label} ${count}`, active: 'Active', all: 'All', range: 'Range', rangeOptions: [['day', 'Today'], ['week', 'Last 7 days'], ['month', 'Last 30 days'], ['all', 'All runs']], searchMatches: (count) => `${count} matching ${count === 1 ? 'task' : 'tasks'}`, clearSearch: 'Clear search', noSearchTitle: 'No matching tasks', noFilterTitle: 'No tasks in this filter', noSearchBody: 'Change the search terms or status filter to find other tasks.', noFilterBody: 'Change the filter or create a new scheduled task.', emptyTitle: 'No scheduled tasks yet', emptyBody: 'Create a task so Maka can continue this work at the right time.', listAriaLabel: 'Scheduled task list', inspectorOpened: (title) => `Opened the task details for ${title}`, edit: 'Edit', duplicate: 'Duplicate', triggering: 'Triggering…', triggerNow: 'Trigger now', snoozing: 'Snoozing…', snooze: 'Snooze 10 minutes', clearing: 'Clearing…', clearRuns: 'Clear history', deleting: 'Deleting…', delete: 'Delete', nextRun: (time) => `Next run: ${time}`, recentRun: (time) => `Last run ${time}`, unscheduled: 'Not scheduled', noRunsTitle: 'No run history', noRunsBody: 'Triggered tasks, manual runs, and delivery failures appear here.', showAllTime: 'All time', runsAriaLabel: 'Scheduled task run history', activeCount: (count) => `${count} active`, @@ -218,8 +218,8 @@ const SCHEDULED_TASK_COPY = { noRuns: 'This task has not run yet.', agentSource: 'Agent scheduled task', agentSourceHint: - 'When due, Maka starts a new session using the execution settings captured at creation.', - agentDelivery: 'Agent session run', + 'When due, Maka starts a new task using the execution settings captured at creation.', + agentDelivery: 'Agent task run', }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/session-rename-dialog.tsx b/packages/ui/src/session-rename-dialog.tsx index c63810d6ca..20af51e2de 100644 --- a/packages/ui/src/session-rename-dialog.tsx +++ b/packages/ui/src/session-rename-dialog.tsx @@ -45,8 +45,8 @@ export function SessionRenameDialog(props: { const [name, setName] = useState(target.name); const trimmed = name.trim(); - // The row's own vocabulary: a conversation is 对话 everywhere else in the - // sidebar, and the header doubles as the field's (hidden) label. + // The row's own vocabulary: a session is 任务 everywhere the user can see + // one, and the header doubles as the field's (hidden) label. const title = target.kind === 'project' ? copy.renameProjectTitle : copy.renameAriaLabel; function submit(event: FormEvent) { diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 296b7a52b0..329f87eaa2 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -162,7 +162,7 @@ const SHARED_UI_COPY = { }, automations: { title: '定时任务', - description: '安排定时任务,并回顾本机对话中的工作进展。', + description: '安排定时任务,并回顾本机任务中的工作进展。', selectorLabel: (module) => `定时任务内容:${module}`, scheduledTasks: '定时任务', dailyReview: '每日回顾', @@ -175,20 +175,20 @@ const SHARED_UI_COPY = { loadingAutomations: '正在加载定时任务…', dailyReview: '每日回顾', loadingDailyReview: '正在加载每日回顾…', - dailyReviewDescription: '自动汇总本机对话,生成摘要、遗漏提醒与深度分析;可在设置中开启定时执行。', + dailyReviewDescription: '自动汇总本机任务,生成摘要、遗漏提醒与深度分析;可在设置中开启定时执行。', dailyReviewDisconnectedTitle: '等待连接每日回顾数据', dailyReviewDisconnectedBody: '桌面端数据桥当前未连接。', }, primitives: { loading: '加载中', close: '关闭', resizeHandle: '调整宽度' }, taskLedger: { status: { pending: '待处理', in_progress: '进行中', blocked: '已阻塞', completed: '已完成', failed: '失败', cancelled: '已取消' }, - ariaLabel: '会话任务', - retry: '重新载入任务', - loading: '正在载入任务…', - activeAriaLabel: '活跃会话任务', - empty: '当前会话没有待推进任务', + ariaLabel: '任务待办', + retry: '重新载入待办', + loading: '正在载入待办…', + activeAriaLabel: '进行中的待办', + empty: '这个任务还没有待办', recent: '最近结束', - recentAriaLabel: '最近结束的会话任务', + recentAriaLabel: '最近结束的待办', childAgent: (agentId) => `子代理${agentId ? ` ${agentId}` : ''}`, mainAgent: '主代理', }, @@ -251,7 +251,7 @@ const SHARED_UI_COPY = { }, automations: { title: 'Scheduled tasks', - description: 'Schedule tasks and review progress from local conversations.', + description: 'Schedule tasks and review progress from local tasks.', selectorLabel: (module) => `Scheduled task content: ${module}`, scheduledTasks: 'Scheduled tasks', dailyReview: 'Daily review', @@ -264,20 +264,20 @@ const SHARED_UI_COPY = { loadingAutomations: 'Loading scheduled tasks…', dailyReview: 'Daily review', loadingDailyReview: 'Loading daily review…', - dailyReviewDescription: 'Summarize local conversations into highlights, missed items, and deeper analysis. Scheduled runs can be enabled in Settings.', + dailyReviewDescription: 'Summarize local tasks into highlights, missed items, and deeper analysis. Scheduled runs can be enabled in Settings.', dailyReviewDisconnectedTitle: 'Waiting for daily review data', dailyReviewDisconnectedBody: 'The desktop data bridge is not connected.', }, primitives: { loading: 'Loading', close: 'Close', resizeHandle: 'Resize handle' }, taskLedger: { status: { pending: 'Pending', in_progress: 'In progress', blocked: 'Blocked', completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled' }, - ariaLabel: 'Conversation tasks', - retry: 'Reload tasks', - loading: 'Loading tasks…', - activeAriaLabel: 'Active conversation tasks', - empty: 'This conversation has no active tasks', + ariaLabel: 'To-do list', + retry: 'Reload the to-do list', + loading: 'Loading the to-do list…', + activeAriaLabel: 'In-progress to-dos', + empty: 'This task has no to-dos yet', recent: 'Recently finished', - recentAriaLabel: 'Recently finished conversation tasks', + recentAriaLabel: 'Recently finished to-dos', childAgent: (agentId) => `Child agent${agentId ? ` ${agentId}` : ''}`, mainAgent: 'Main agent', }, diff --git a/packages/ui/src/shell-controls-copy.ts b/packages/ui/src/shell-controls-copy.ts index 681cd9db4f..865862030d 100644 --- a/packages/ui/src/shell-controls-copy.ts +++ b/packages/ui/src/shell-controls-copy.ts @@ -45,22 +45,22 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { settings: '设置', updateDownloaded: (version: string) => `新版本 ${version} 已下载,重启后安装`, updateFailed: (version: string) => `新版本 ${version} 更新失败,点击重试或手动下载`, - pendingTasks: (count: number) => `定时任务,${count} 个未完成任务`, + pendingTasks: (count: number) => `定时任务,${count} 条进行中`, }, search: { title: '搜索', - conversationsLabel: '搜索会话', - placeholder: '搜索会话标题和内容…', + conversationsLabel: '搜索任务', + placeholder: '搜索任务标题和内容…', clearLabel: '清空搜索', statusRegionLabel: '搜索状态和结果', unavailable: '当前环境无法连接搜索后端,请稍后重试。', privacyTitle: '隐私模式已关闭搜索。', - privacyDetail: '关闭隐私模式后可以继续按关键词查找历史对话。', + privacyDetail: '关闭隐私模式后可以继续按关键词查找历史任务。', errorTitle: '搜索暂时无法完成。', errorFallback: '搜索服务需要刷新,请重试。', - introduction: '开始输入以按关键词查找历史对话。结果只包含会话标题和内容文本,不进入网络。', + introduction: '开始输入以按关键词查找历史任务。结果只包含任务标题和内容文本,不进入网络。', searching: '正在搜索…', - empty: '没有匹配的会话标题或内容。换个关键词试试。', + empty: '没有匹配的任务标题或内容。换个关键词试试。', results: (count: number) => `找到 ${count} 条匹配`, truncatedResults: (count: number) => `结果较多,已显示前 ${count} 条`, resultsLabel: '搜索结果', @@ -76,23 +76,23 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { settings: 'Settings', updateDownloaded: (version: string) => `Update ${version} downloaded. Restart to install.`, updateFailed: (version: string) => `Update ${version} failed. Click to retry or download manually.`, - pendingTasks: (count: number) => `Scheduled tasks, ${count} unfinished ${count === 1 ? 'task' : 'tasks'}`, + pendingTasks: (count: number) => `Scheduled tasks, ${count} active`, }, search: { title: 'Search', - conversationsLabel: 'Search conversations', - placeholder: 'Search conversation titles and content…', + conversationsLabel: 'Search tasks', + placeholder: 'Search task titles and content…', clearLabel: 'Clear search', statusRegionLabel: 'Search status and results', unavailable: 'Search is unavailable in the current environment. Try again later.', privacyTitle: 'Search is disabled in privacy mode.', - privacyDetail: 'Turn off privacy mode to search previous conversations by keyword.', + privacyDetail: 'Turn off privacy mode to search previous tasks by keyword.', errorTitle: 'Search could not be completed.', errorFallback: 'Search needs to be refreshed. Try again.', introduction: - 'Start typing to search previous conversations by keyword. Results include local conversation titles and content only and are not sent over the network.', + 'Start typing to search previous tasks by keyword. Results include local task titles and content only and are not sent over the network.', searching: 'Searching…', - empty: 'No matching conversation titles or content. Try another keyword.', + empty: 'No matching task titles or content. Try another keyword.', results: (count: number) => `${count} ${count === 1 ? 'match' : 'matches'}`, truncatedResults: (count: number) => `Many results; showing the first ${count}`, resultsLabel: 'Search results', diff --git a/packages/ui/stories/model-picker.stories.tsx b/packages/ui/stories/model-picker.stories.tsx index 14203e0a03..b1ef2ae229 100644 --- a/packages/ui/stories/model-picker.stories.tsx +++ b/packages/ui/stories/model-picker.stories.tsx @@ -123,7 +123,7 @@ export const ExistingConversation: Story = { ? 'Switching may rebuild the provider prompt cache, making the next request slower or more expensive.' : '切换模型可能需要重建服务商提示缓存,使下一次请求更慢或成本更高。'; const trigger = within(canvasElement).getByRole('button', { - name: /切换当前会话模型|Switch model for this conversation/, + name: /切换当前任务模型|Switch model for this task/, }); const announcement = canvasElement.querySelector('.maka-model-switch-announcement'); await expect(announcement).toHaveAttribute('role', 'status'); @@ -133,7 +133,7 @@ export const ExistingConversation: Story = { await userEvent.hover(trigger); await within(document.body).findByText( - english ? 'Switch model for this conversation' : '切换当前会话模型', + english ? 'Switch model for this task' : '切换当前任务模型', ); await userEvent.unhover(trigger); @@ -188,7 +188,7 @@ export const EmptyConversation: Story = { ), play: async ({ canvasElement }) => { const trigger = within(canvasElement).getByRole('button', { - name: /切换当前会话模型|Switch model for this conversation/, + name: /切换当前任务模型|Switch model for this task/, }); const announcement = canvasElement.querySelector('.maka-model-switch-announcement'); await expect(announcement).toHaveAttribute('role', 'status'); diff --git a/packages/ui/stories/session-list-panel.stories.tsx b/packages/ui/stories/session-list-panel.stories.tsx index 307e4788b5..57bdab52f2 100644 --- a/packages/ui/stories/session-list-panel.stories.tsx +++ b/packages/ui/stories/session-list-panel.stories.tsx @@ -127,7 +127,7 @@ function StoryFrame(props: { activeRow?.querySelector(':scope > button')?.focus({ preventScroll: true }); if (openActiveRowMenu) { menuTimeout = window.setTimeout(() => { - activeRow?.querySelector('[aria-label="对话操作"]')?.click(); + activeRow?.querySelector('[aria-label="任务操作"]')?.click(); }, 0); } }, 0); From 4bfc99d8eb34d0fdbdd418df1b8b289399b3037b Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 11:00:18 +0800 Subject: [PATCH 06/20] test(ui): make the narrow-rail story actually narrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own 260px `resizable` width, so the story clipped a wide rail instead of showing a narrow one — the timestamps it exists to check were outside the frame. It now drives the rail's own width, at 180px, which is the panel's `minWidth` and therefore the narrowest state a user can reach. Generated-by: Claude Code --- packages/ui/stories/session-list-panel.stories.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/ui/stories/session-list-panel.stories.tsx b/packages/ui/stories/session-list-panel.stories.tsx index 57bdab52f2..096ba4de18 100644 --- a/packages/ui/stories/session-list-panel.stories.tsx +++ b/packages/ui/stories/session-list-panel.stories.tsx @@ -69,6 +69,7 @@ function panelProps(input: { activeId?: string; streamingSessionIds?: Set; staleSessionIds?: Set; + width?: number; viewMode?: SessionListPanelProps['viewMode']; groups?: SessionListPanelProps['groups']; projectActions?: SessionListPanelProps['projectActions']; @@ -77,6 +78,10 @@ function panelProps(input: { return { selection: input.selection ?? { section: 'sessions' }, sessions: input.sessions, + // The rail's own width, not just the frame's: SideNav keeps its width in + // `resizable`, so a narrow frame alone only clips a 260px rail instead of + // showing what the narrow one looks like. + ...(input.width === undefined ? {} : { width: input.width }), ...(input.activeId ? { activeId: input.activeId } : {}), ...(input.streamingSessionIds ? { streamingSessionIds: input.streamingSessionIds } : {}), ...(input.staleSessionIds ? { staleSessionIds: input.staleSessionIds } : {}), @@ -209,7 +214,7 @@ const longTitleSessions = [ }), ]; -// Real path: a fresh workspace with no conversations yet — the sidebar list before +// Real path: a fresh workspace with no tasks yet — the rail's list before // anything is created. export const Empty: Story = { render: () => ( @@ -234,12 +239,13 @@ export const ConversationStates: Story = { ), }; -// Real path: a workspace with long conversation titles, with the sidebar dragged to its -// narrow end. +// Real path: a workspace with long task titles, with the rail dragged to its +// narrow end (180px, the panel's own minWidth). export const LongTitlesAndNarrow: Story = { render: () => ( - + Date: Sat, 15 Aug 2026 11:55:01 +0800 Subject: [PATCH 07/20] =?UTF-8?q?fix(ui):=20keep=20the=20rail's=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=20row=20as=20the=20way=20back=20to=20the=20l?= =?UTF-8?q?ist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bb39b330a deleted `SessionFilter` and took the 「会话」 row with it, on the reading that the row only selected a filter whose answer was always the same answer. Half right: its `isSelected` read the dead filter, but its `onClick` selected the SECTION, and it was the only control that did. Without it the rail has three sections and two rows. Collapsed at 48px — the default state (`readSessionListCollapsed`) — the list is not rendered at all, so 扩展 and 定时任务 became one-way doors: the only way back to a running task was 新任务, which answers "show me my tasks" by starting another one. `streaming-remount.spec.ts` walks exactly that path and had been rewritten to click a task row that a collapsed rail does not have. Restore the row as 任务, selected on `section === 'sessions'` and selecting the section with no filter. The filter deletion stands. Refs #2984 Generated-by: Claude Code --- apps/desktop/e2e/streaming-remount.spec.ts | 6 +++--- packages/ui/src/nav-selection.ts | 8 ++++++-- packages/ui/src/session-sidebar-nav.tsx | 19 +++++++++++++++++-- packages/ui/src/shell-controls-copy.ts | 3 +++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/desktop/e2e/streaming-remount.spec.ts b/apps/desktop/e2e/streaming-remount.spec.ts index f4a20d23c3..1e7ff542a1 100644 --- a/apps/desktop/e2e/streaming-remount.spec.ts +++ b/apps/desktop/e2e/streaming-remount.spec.ts @@ -22,9 +22,9 @@ test('remounting a live surface leaves accumulated output settled', async ({ await sidebar.getByRole('button', { name: '扩展' }).click(); await expect(page.locator('[data-module="skills"]')).toBeVisible(); await expect(liveBubble).toHaveCount(0); - // Back through the task's own row: the rail's 「会话」 row was a section - // selector for the only section the list has, so it is gone (#2984). - await sidebar.locator('[data-session-id]').first().click(); + // Back through the rail's 任务 row. The rail is collapsed here, so the task + // rows are not rendered and this section row is the only way back (#2984). + await sidebar.getByRole('button', { name: '任务', exact: true }).click(); await expect(liveBubble).toHaveCount(1); await expect(liveBubble).toContainText(accumulatedOutput); diff --git a/packages/ui/src/nav-selection.ts b/packages/ui/src/nav-selection.ts index e0ba44e958..f9442cf8a1 100644 --- a/packages/ui/src/nav-selection.ts +++ b/packages/ui/src/nav-selection.ts @@ -5,8 +5,12 @@ * › 活动 › 已归档任务 (#2985) — cleaning tasks up is management, and the rail * lists what you are working on. `flagged` never had a writer: nothing ever * selected it, so the branch that filtered on it could not run. What was left - * was a one-value filter, which is the same tautology the 「会话」 row was: a - * control whose answer is always the same answer. + * was a one-value filter: a control whose answer is always the same answer. + * + * The rail's 任务 row survives that deletion. It carried the dead filter, but + * its job was to select this section — it is how you get back here from + * 扩展 or 定时任务, and collapsed at 48px it is the ONLY way, because the list + * itself is not rendered there. */ export type ExtensionModule = 'skills' | 'mcp'; export type AutomationModule = 'scheduled-tasks' | 'daily-review'; diff --git a/packages/ui/src/session-sidebar-nav.tsx b/packages/ui/src/session-sidebar-nav.tsx index c134a5d8ee..0de8e43b33 100644 --- a/packages/ui/src/session-sidebar-nav.tsx +++ b/packages/ui/src/session-sidebar-nav.tsx @@ -3,6 +3,7 @@ import { AlertCircle, Blocks, Download, + MessageSquare, Settings, SquarePen, Timer, @@ -24,6 +25,7 @@ export function SessionSidebarNav(props: { }) { const locale = useUiLocale(); const copy = getShellControlsCopy(locale).navigation; + const sessionsActive = props.selection.section === 'sessions'; const extensionsActive = props.selection.section === 'extensions'; const automationsActive = props.selection.section === 'automations'; const moduleMemory = props.moduleMemory ?? { extensions: 'skills', automations: 'scheduled-tasks' }; @@ -37,10 +39,10 @@ export function SessionSidebarNav(props: { // // SideNavSection, like the footer below, rather than a bare fragment in a // product div: the section is what owns the space BETWEEN nav rows - // (`items` → --spacing-0-5). Handed to `topContent` as a plain div these three + // (`items` → --spacing-0-5). Handed to `topContent` as a plain div these rows // were the only group on the rail outside that authority, so they stacked // edge to edge — invisible expanded, where the label separates the rows, and - // plainly three-icons-as-one-slab at 48px. The header is hidden because the + // plainly icons-as-one-slab at 48px. The header is hidden because the // rail landmark already names the panel; the title stays for a11y. return ( @@ -51,6 +53,19 @@ export function SessionSidebarNav(props: { onClick={props.onNew} endContent={} /> + {/* The way back to the list. Selecting a task row does it too, but only + while the rail is expanded — collapsed, the list is not rendered, so + without this row 扩展 and 定时任务 are one-way doors and the only exit + is 新任务, which answers "show me my tasks" by creating another one. + MessageSquare is the glyph the command palette already draws for a + session (command-palette-commands.ts). */} + props.onSelect({ section: 'sessions' })} + /> Date: Sat, 15 Aug 2026 17:58:28 +0800 Subject: [PATCH 08/20] fix(ui): route task status through the shared semantic layer Deleting `SessionStatusTone` was right; replacing it with a private `SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts` already owns "the one place a status word becomes a colour", so the private table made a second authority, and the two disagreed: a task waiting on a permission prompt drew `error` in the rail while the permission centre drew `attention` for the identical condition. Map the session enum to `StatusSemantic` and let `dotForStatus` pick the colour. `blocked` and `waiting_for_user` are both `attention`, which is what that semantic is defined as -- both are waiting on a person. Giving `blocked` `error` to tell them apart used colour for a distinction colour cannot carry; their labels and `describeBlockedReason` do that. Restore `review` and `done`. They have no writer in current source, but `SESSION_STATUSES` is read back out of storage, and narrowing it is a data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL importer (removed in #2656) passed both values through into real SQLite stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header on an unrecognised status, so one stored row carrying `done` fails an entire catalog page. The migration is its own change. `archived` and `aborted` get their dots back. They were `muted` before, `muted` resolved to a real `neutral` dot, and dropping them to `undefined` was a behaviour change I described as a consequence of collapsing the layer. Refs #2984 Generated-by: Claude Code --- packages/core/src/session.ts | 14 ++-- packages/ui/src/conversation-copy.ts | 4 +- .../ui/src/session-status-presentation.ts | 64 +++++++++++-------- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index cbb6a83022..e9dc928dbd 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -33,16 +33,22 @@ export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './explore-ag * alongside `isArchived`; consolidating those two onto one authority is its own * change (#2984, PR 3) because it rewrites stored rows. * - * `review` and `done` were removed: nothing in the codebase ever wrote them, - * and no stored record can carry them, so the values had no reader that was not - * also dead. Everything the runtime writes is here — `running`, `blocked`, - * `aborted`, `waiting_for_user` — plus `active` as the resting state. + * `review` and `done` have no writer in current source, but they stay: this + * list is read back out of storage, and narrowing it is a data migration, not a + * cleanup. `resolveLegacyStatus` in the JSONL importer (removed in #2656) let + * both values through into real SQLite stores verbatim, and `normalizeSession + * Header` throws on an unrecognised status for the WHOLE header — so one stored + * row carrying `done` fails an entire catalog page, not just its own row. + * Removing them needs a schema migration or a tolerant read, which is its own + * change with its own review. */ export const SESSION_STATUSES = [ 'active', 'running', 'waiting_for_user', 'blocked', + 'review', + 'done', 'archived', 'aborted', ] as const; diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index b66ef4f7de..bf90d1f6b3 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -428,7 +428,7 @@ const CONVERSATION_COPY = { revisionVersionsAriaLabel: '任务版本', revisionVersion: (current, total) => `版本 ${current} / ${total}`, previousRevision: '查看上一版本', nextRevision: '查看下一版本', }, sessions: { - status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', archived: '已归档', aborted: '已中止' }, + status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', review: '待审核', done: '已完成', archived: '已归档', aborted: '已中止' }, blockedReason: { NO_REAL_CONNECTION: '等待配置可用模型连接', auth: '需要重新登录', permission_required: '等待权限确认', tool_failed: '工具调用失败', unknown: '运行中断,可重试' }, listAriaLabel: '任务列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多任务`, renameAriaLabel: '重命名任务', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '任务正在流式响应中', staleTitle: '此任务使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '任务已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: '任务操作', pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: '最近', groupByTime: '按时间', groupByProject: '按项目', groupingAriaLabel: '任务分组方式', projectActionsAriaLabel: (name) => `${name} 项目操作`, projectNewTask: '新建任务', projectRename: '重命名', projectArchive: '归档', projectRestore: '恢复', projectRelink: '重新定位', projectUnavailable: '项目目录不可用', archivedProjects: '已归档项目', archivedProjectsAriaLabel: '展开已归档项目', worktreeAriaLabel: 'Git 工作树', promptRailAriaLabel: '按提问跳转', emptyPrompt: '(空提问)', jumpToPrompt: (preview) => `跳到提问:${preview}`, }, @@ -566,7 +566,7 @@ const CONVERSATION_COPY = { revisionVersionsAriaLabel: 'Task versions', revisionVersion: (current, total) => `Version ${current} of ${total}`, previousRevision: 'View previous version', nextRevision: 'View next version', }, sessions: { - status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', archived: 'Archived', aborted: 'Stopped' }, + status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', review: 'Review', done: 'Done', archived: 'Archived', aborted: 'Stopped' }, blockedReason: { NO_REAL_CONNECTION: 'Waiting for an available model connection', auth: 'Sign in again', permission_required: 'Waiting for permission', tool_failed: 'Tool call failed', unknown: 'Run interrupted; retry available' }, listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: 'Task actions', pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Task grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, }, diff --git a/packages/ui/src/session-status-presentation.ts b/packages/ui/src/session-status-presentation.ts index 8a0ea6c031..9df083d2f4 100644 --- a/packages/ui/src/session-status-presentation.ts +++ b/packages/ui/src/session-status-presentation.ts @@ -1,47 +1,61 @@ import type { SessionBlockedReason, SessionStatus } from '@maka/core/session'; import type { UiLocale } from '@maka/core/ui-locale'; import type { StatusDotVariant } from '@astryxdesign/core/StatusDot'; +import { dotForStatus, type StatusSemantic } from './status-vocabulary.js'; import { getConversationCopy } from './conversation-copy.js'; export interface SessionStatusPresentation { label: string; - /** - * Astryx's own dot variant, reached in one step. - * - * There used to be a `SessionStatusTone` in between — seven names of our own - * that the only caller then mapped onto Astryx's five. Two of those names - * (`info`, `muted`) had no distinct outcome at the end of the chain, and the - * collapse is what let `waiting_for_user` and `blocked` land on the same - * `warning`: they were different tones that stopped being different dots. - * One table, one hop, and the collision is visible in the source. - * - * `undefined` means the row draws no dot at all: an idle session and an - * archived or aborted one are not states the rail interrupts anyone about. - */ variant?: StatusDotVariant; } -const STATUS_VARIANT: Record = { +/** + * What a status MEANS. The colour is not decided here. + * + * There used to be a `SessionStatusTone` in between — seven names of our own + * that the only caller mapped onto Astryx's five. Deleting that layer was + * right; replacing it with a private `SessionStatus -> StatusDotVariant` table + * was not, because `status-vocabulary.ts` already owns the one place a status + * word becomes a colour, and a second table there means the rail and Settings + * can disagree about the same fact. They did: a task waiting on a permission + * prompt drew `error` here while the permission centre drew `attention` for the + * identical condition. + * + * So the session enum maps to `StatusSemantic` and `dotForStatus` picks the + * colour, which also settles what `waiting_for_user` and `blocked` share. + * They are both `attention` — both are "waiting on a person", which is what + * that semantic is defined as. Giving `blocked` `error` to tell the two apart + * was using colour for a distinction colour cannot carry; `error` is reserved + * for "broken now". The two are told apart by their label and, for `blocked`, + * by `describeBlockedReason` in the tooltip. + * + * `undefined` means no dot: `active` is the resting state and the rail does not + * mark a task for being ordinary. Everything else gets one, including + * `archived` and `aborted` — they were `muted` before this change and `muted` + * resolved to a real `neutral` dot, so dropping them to `undefined` was a + * behaviour change, not a consequence of collapsing the layer. Without a dot + * they fell through to the unread branch and an aborted task with unread text + * drew the same accent dot as one that is running. + */ +const STATUS_SEMANTIC: Record = { active: undefined, - running: 'accent', - waiting_for_user: 'warning', - // `error`, not `warning`: blocked means the task cannot proceed until someone - // fixes a connection, a login, or a permission, while waiting_for_user means - // it is holding a question for you. Sharing one colour made the rail unable to - // say which of the two a row was in. - blocked: 'error', - archived: undefined, - aborted: undefined, + running: 'active', + waiting_for_user: 'attention', + blocked: 'attention', + review: 'attention', + done: 'success', + archived: 'neutral', + aborted: 'neutral', }; export function presentSessionStatus( status: SessionStatus, locale: UiLocale = 'zh', ): SessionStatusPresentation { - const variant = STATUS_VARIANT[status]; + const semantic = STATUS_SEMANTIC[status]; return { label: getConversationCopy(locale).sessions.status[status], - ...(variant ? { variant } : {}), + ...(semantic ? { variant: dotForStatus(semantic) } : {}), }; } From b90b60ae125a46d0dafe2e89accb8e19cfcc1044 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 17:58:43 +0800 Subject: [PATCH 09/20] fix(ui): keep the task row's facts when it collapses to two slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The row draws one dot and one trailing slot. That was implemented by collapsing to a single signal inside the resolver, which made the visual element the only carrier of the fact -- so removing the trailing `Badge` removed "stale" itself, not just its pill. `staleTitle` and `staleAriaLabel` were left with no reader at all, the surviving `opacity` cue is cancelled on the selected row by design, and opacity announces nothing. Worktree went the same way: an `aria-label`ed icon inside the button became a `title` on a non-interactive ancestor, which contributes to neither the button's name nor its description. Resolve a LIST of signals in priority order, draw `signals[0]`, and put the rest -- plus worktree and the absolute timestamp -- in one visually hidden span inside the button. Two slots, same pixels, and the facts stop depending on which of them had room. Signals also stop eating each other: `aborted` now keeps its own dot instead of falling through to the unread branch, where an aborted task with unread text drew the same accent dot as a running one. Stale joins the list as `attention`, so a stale task finally has a signal that survives being selected. It is resolved here rather than in `presentSessionStatus` because it is a renderer-derived fact, not a persisted `SessionStatus`. Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop carries that field: Runtime Host's catalog reads storage directly, the wire projection does not list it, and Desktop Main's converter copies persisted fields only -- the identifier appears nowhere in `packages/runtime-host/src` or `apps/desktop/src/main` outside a test fixture. It was a read of something nothing populates, described as reading the authority. `streaming` is what the rail actually has, with the limit that it only knows this renderer's turns; a real live-run projection from the Host is its own change. Also: `pointer-events: none` on the resting ⋯, which sat invisible at `z-index: 1` over the trailing slot and swallowed clicks; and the inner list no longer repeats the rail's own accessible name. Refs #2984 Generated-by: Claude Code --- .../renderer/session-status-presentation.ts | 2 - apps/desktop/src/renderer/styles/sidebar.css | 8 ++ packages/ui/src/session-history-list.tsx | 135 ++++++++++++------ 3 files changed, 100 insertions(+), 45 deletions(-) diff --git a/apps/desktop/src/renderer/session-status-presentation.ts b/apps/desktop/src/renderer/session-status-presentation.ts index d711ce8e8d..757e510925 100644 --- a/apps/desktop/src/renderer/session-status-presentation.ts +++ b/apps/desktop/src/renderer/session-status-presentation.ts @@ -23,8 +23,6 @@ import type { SessionBlockedReason, SessionSummary } from '@maka/core/session'; import type { UiLocale } from '@maka/core/ui-locale'; import { getDesktopConversationCopy } from './locales/conversation-copy.js'; import { describeSessionErrorReason } from './session-error-presentation.js'; -export { presentSessionStatus } from '@maka/ui'; -export { describeBlockedReason } from '@maka/ui'; /** * Session-level "blocked" is only worth interrupting the user when diff --git a/apps/desktop/src/renderer/styles/sidebar.css b/apps/desktop/src/renderer/styles/sidebar.css index c106927d5b..a7a15ab7a1 100644 --- a/apps/desktop/src/renderer/styles/sidebar.css +++ b/apps/desktop/src/renderer/styles/sidebar.css @@ -301,13 +301,20 @@ white-space: nowrap; } +/* `opacity`, not `display: none`, so the trigger keeps its place in the tab + order and in the accessibility tree — that is what makes the menu reachable + without a pointer. `pointer-events: none` goes with it: the button sits + absolutely over the trailing slot at `z-index: 1`, so while it is invisible + it would otherwise still swallow clicks meant for the row underneath. */ .maka-session-row > .maka-session-row-action { opacity: 0; + pointer-events: none; } .maka-session-row:hover > .maka-session-row-action, .maka-session-row:focus-within > .maka-session-row-action { opacity: 1; + pointer-events: auto; } .maka-session-row:hover .maka-session-row-time, @@ -320,6 +327,7 @@ @media (hover: none) { .maka-session-row > .maka-session-row-action { opacity: 1; + pointer-events: auto; } .maka-session-row .maka-session-row-time { diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index a0d20e8ef8..76826138ca 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -25,6 +25,7 @@ import { Trash2, } from './icons.js'; import { RelativeTime } from './relative-time.js'; +import { formatAbsoluteTimestamp } from './chat-display-helpers.js'; import { Badge } from '@astryxdesign/core/Badge'; import { MoreMenu } from '@astryxdesign/core/MoreMenu'; import { @@ -34,6 +35,7 @@ import { import { VStack } from '@astryxdesign/core/Stack'; import { StatusDot, type StatusDotVariant } from '@astryxdesign/core/StatusDot'; import { describeBlockedReason, presentSessionStatus } from './session-status-presentation.js'; +import { dotForStatus } from './status-vocabulary.js'; import { SessionRenameDialog, type SessionRenameTarget } from './session-rename-dialog.js'; import { useUiLocale } from './locale-context.js'; import { getConversationCopy } from './conversation-copy.js'; @@ -96,14 +98,13 @@ export function SessionHistoryList(props: { } } - // Outer SideNav is the sole navigation landmark; this is scroll content only. + // Outer SideNav is the sole navigation landmark and it already carries this + // panel's name; naming this element too put "任务列表" inside "任务列表", + // which is one ambiguous match for anything selecting by that name and no + // extra information for anyone hearing it. It is scroll content and a key + // handler, nothing an assistive tech user needs to be told about separately. return ( -
+
entry.tooltip ?? entry.label), + props.worktree ? copy.worktreeAriaLabel : undefined, + props.session.lastMessageAt + ? formatAbsoluteTimestamp(props.session.lastMessageAt, locale) + : undefined, + ] + .filter((entry): entry is string => Boolean(entry)) + .join(' · '); return (
) : null} + {rowDescription ? ( + {rowDescription} + ) : null} } /> @@ -673,39 +694,50 @@ function SessionItemActions(props: { ); } +interface SessionRowSignal { + variant: StatusDotVariant; + label: string; + isPulsing?: boolean; + tooltip?: string; +} + /** - * The row's one signal, resolved from the highest-priority thing true about the - * session: running › waiting for you › blocked › unread. `null` means the row - * draws nothing — idle, archived, and aborted are not states worth a dot. + * Everything true about the session that is worth saying, in priority order. + * + * The row draws ONE dot — `signals[0]` — but it says all of them. Keeping the + * list is what lets the two visible slots stay two while the row still reaches + * a screen reader with the same facts a sighted user gets from the dot's + * colour, the row's dimming, and the tooltip. Collapsing to a single signal + * inside this function is what previously made the trailing `Badge` the only + * carrier of "stale", so removing the Badge removed the fact. * - * "Running" is read from `runningTurnIds`, the runtime's projection of the runs - * it is actually holding. The row used to read `streaming` first and then fall - * back to the persisted `status`, and neither is the authority: `session.ts` - * spells out that a stored `status` "can be left behind entirely by a crash", - * and `streaming` only knows about turns THIS renderer sent, so a task running - * under a bot channel or a second window read as idle. `streaming` stays, below - * `runningTurnIds`, for the one thing it is the authority on: the gap between - * this renderer sending a turn and the host reporting it back. + * It also stops signals from eating each other. `aborted` used to resolve to no + * dot at all, which dropped the row into the unread branch: an aborted task + * with unread text drew the same accent dot as one that is running. Now it + * draws its own neutral dot and unread is still in the list behind it. * - * Unread is last because it is the weakest claim on attention — a session that - * is running or holding a question already says something more specific about - * the same unread text. + * `streaming` is the only live-run source the rail actually has. It knows only + * about turns THIS renderer sent, which is a real limit — a task running under + * a bot channel or a second window reads as idle here. The fix for that is a + * live-run projection from Runtime Host, which is not in this change; there is + * no `runningTurnIds` on a `SessionSummary` that reaches Desktop, so reading it + * would be reading a field nothing populates. */ -function resolveSessionRowSignal( +function sessionRowSignals( session: SessionSummary, - streaming: boolean, - active: boolean, + options: { streaming: boolean; stale: boolean; active: boolean }, locale: UiLocale, -): { variant: StatusDotVariant; label: string; isPulsing?: boolean; tooltip?: string } | null { +): SessionRowSignal[] { const copy = getConversationCopy(locale).sessions; + const signals: SessionRowSignal[] = []; - if (session.runningTurnIds?.length || streaming) { - return { + if (options.streaming) { + signals.push({ variant: 'accent', label: copy.respondingAriaLabel, isPulsing: true, tooltip: copy.respondingTitle, - }; + }); } const { label, variant } = presentSessionStatus(session.status, locale); @@ -714,23 +746,40 @@ function resolveSessionRowSignal( session.status === 'blocked' && session.blockedReason ? describeBlockedReason(session.blockedReason, locale) : null; - return { + signals.push({ variant, label, - // A `running` header with no live run reaching us: either the run ended - // without its status write landing, or this summary came from a mutation - // response, which describes the header alone and omits `runningTurnIds` - // (`session.ts`). Still pulsing — the row should not change shape based on + // A `running` header with nothing streaming here: the run ended without + // its status write landing, or it is running somewhere this renderer + // cannot see. Still pulsing — the row should not change shape based on // which projection delivered it. isPulsing: session.status === 'running', tooltip: blockedDetail ? `${label} · ${blockedDetail}` : label, - }; + }); + } + + // Unread ranks under both because it is the weakest claim on attention: a + // task that is running or holding a question already says something more + // specific about the same unread text. + if (!options.active && session.hasUnread) { + signals.push({ variant: 'accent', label: copy.unreadAriaLabel }); } - if (!active && session.hasUnread) { - return { variant: 'accent', label: copy.unreadAriaLabel }; + // Stale is a renderer-derived fact, not a persisted status, which is why it + // is resolved here rather than in `presentSessionStatus`. `attention`, not + // `error`: the connection is gone but the task still sends, on the default + // connection. It used to be a trailing `Badge`; the row's dimming is the + // visual now, and dimming is cancelled on the selected row and says nothing + // to assistive tech, so it needs to be in this list either way. + if (options.stale) { + signals.push({ + variant: dotForStatus('attention'), + label: copy.staleAriaLabel, + tooltip: copy.staleTitle, + }); } - return null; + + return signals; } interface SessionGroup { From 550f954a397a84ca4895a78084cb7d12253f4a67 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:00:33 +0800 Subject: [PATCH 10/20] fix(desktop): refresh the catalog when an import's outcome is unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `external-sessions:import` emits `sessions:changed` on success, so a task imported while the user walks away from Settings still reaches the rail on its own -- the comment in `app-shell.tsx` claiming nothing notifies the shell was wrong, and is corrected here. The gap is the other branch. `commit_outcome_unknown` means the Host cannot say whether the task was committed, and that path returned a result without emitting anything. The only trace was the page's own banner, and 导入任务 is a Settings page: leaving it unmounts the banner, which is exactly when someone comes back and imports the same conversation again. Emit there too, with no id, because not knowing which task landed is what the code means. Also return the import promise from `clickAction` instead of `void`-ing it. Astryx's Button awaits it and drops repeat clicks until it settles; discarding it left double-submit to `importingId` alone, one render behind the second click. Refs #2984 Generated-by: Claude Code --- .../runtime-host-external-sessions-ipc-main.test.ts | 9 +++++++-- .../src/main/runtime-host-external-sessions-ipc-main.ts | 7 +++++++ apps/desktop/src/renderer/app-shell.tsx | 8 +++++--- .../src/renderer/settings/import-tasks-settings-page.tsx | 7 ++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-external-sessions-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-external-sessions-ipc-main.test.ts index 718c763d91..8baeb06b63 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-external-sessions-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-external-sessions-ipc-main.test.ts @@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]); }); -test('preserves commit uncertainty as a structured non-retryable IPC result', async () => { +test('an uncertain commit still asks the shell to re-read the catalog', async () => { const events: unknown[] = []; const ipc = ipcHarness(); registerRuntimeHostExternalSessionsIpc( @@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as }), { ok: false, reason: 'commit_outcome_unknown' }, ); - assert.deepEqual(events, []); + // The task may be in the catalog, so the shell has to look. The import page's + // own banner cannot be the only trace: 导入任务 is a Settings page, and the + // moment the user leaves it the banner is unmounted -- which is exactly when + // they come back and import the same conversation again. No id, because not + // knowing which task landed is what `commit_outcome_unknown` means. + assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]); }); test('rejects malformed renderer requests before they reach the Host client', async () => { diff --git a/apps/desktop/src/main/runtime-host-external-sessions-ipc-main.ts b/apps/desktop/src/main/runtime-host-external-sessions-ipc-main.ts index 24fd9a4d32..c74c19bd7b 100644 --- a/apps/desktop/src/main/runtime-host-external-sessions-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-external-sessions-ipc-main.ts @@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc( error.operation === 'external-session.import' && error.code === 'commit_outcome_unknown' ) { + // "Unknown" means the task may well be in the catalog, so tell the + // shell to read it again. Without this, the only trace of a maybe- + // committed import is the banner on the page, and the page is gone the + // moment the user leaves Settings -- which is exactly when they come + // back and import the same conversation a second time. No id: the + // whole point is that we do not know which task, if any, landed. + deps.emitSessionsChanged('created'); return { ok: false, reason: 'commit_outcome_unknown', diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index 5f5c57ed4f..491fa2e6b7 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -3376,9 +3376,11 @@ function AppShellContent({ paletteOpen={paletteOpen} closePalette={closePalette} commandOptions={commandOptions} - /* 导入任务 hands back a task that is not in the catalog yet — nothing - behind `externalSessions.import` notifies the shell — so the shell - seeds it, then leaves Settings and opens it. */ + /* Seeding is for the navigation, not for correctness: the import IPC + already emits `sessions:changed`, so the task reaches the rail on its + own even if the user closes Settings mid-import. Seeding it here just + means `openSessionInChat` has something to open without waiting for + the refresh to land. */ onExternalSessionImported={(session) => { upsertSessionSummary(session); closeSettings(); diff --git a/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx b/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx index 18347c2fbc..bedcf00594 100644 --- a/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx @@ -337,7 +337,12 @@ export function ImportTasksSettingsPage(props: { size="sm" isLoading={importingId === session.id} isDisabled={importingId !== null || uncertainIds.has(session.id)} - clickAction={() => void importConversation(session.id)} + // Returned, not discarded: Astryx's Button awaits a + // promise-returning `clickAction` and drops repeat + // clicks until it settles. `void`-ing it gave that + // guarantee nothing to await, leaving double-submit to + // the `importingId` state alone -- one render behind. + clickAction={() => importConversation(session.id)} label={importingId === session.id ? copy.importing : copy.import} // Every row's button reads 导入; only the accessible // name can say which conversation it imports. From e27572667ebe07bd750756b7eff456039d7ca02a Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:02:44 +0800 Subject: [PATCH 11/20] fix(desktop): re-point the structural gates the surface move broke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting the import dialog left exact-string readers behind. `scripts/ check-astryx-alignment.mjs` read the dialog and its stylesheet by path, so running it exited 1 with ENOENT; the surface inventory still listed both files and had no row for the page that replaced them. The dialog's Item-row guards are not migrated, they are dropped. They checked that a SELECTED row stayed keyboard-reachable -- no parent role stealing Item's native button, no selected-only tabIndex trapping focus. 设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复 does on the archived page. Asserting those smells against the new page would guard a shape it does not have. The button guard and a ListItem import check follow the surface to the page. Inventory rows for the new page and for #2985's archived-tasks page, which was never added. Three runtime-host entries stay missing; they predate this branch. Also read the Daily Review model label from its copy table instead of a literal. The story matched '跟随对话默认', which the rename retired, so it silently found nothing -- and `storybook-visual-smoke.mjs` disables every `play` function, so CI could not report it. Refs #2984 Generated-by: Claude Code --- .../settings/settings-pages.stories.tsx | 12 +++- docs/astryx-surface-file-inventory.md | 2 + docs/astryx-surface-file-inventory.paths | 10 +-- scripts/check-astryx-alignment.mjs | 66 +++---------------- 4 files changed, 28 insertions(+), 62 deletions(-) diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx index 7e26c1e15b..e6bc185b7e 100644 --- a/apps/desktop/stories/settings/settings-pages.stories.tsx +++ b/apps/desktop/stories/settings/settings-pages.stories.tsx @@ -32,6 +32,16 @@ import type { ConnectionsBridge } from '../../src/renderer/settings/providers-pa import type { ProjectRecord } from '@maka/core/project'; import type { ArchivedTasksBridge } from '../../src/renderer/settings/tasks-settings-page'; import { withScopedMakaBridge } from '../maka-bridge'; +import { getDailyReviewSettingsCopy } from '../../src/renderer/locales/settings-daily-review-copy'; + +/** + * Read from the copy table, not typed out again. This selector matched a + * literal '跟随对话默认' that the 任务 rename retired, so it silently found + * nothing — and `scripts/storybook-visual-smoke.mjs` disables every `play` + * function, so CI could not tell us. A story that drives the UI by its visible + * text has to source that text where the UI does. + */ +const DAILY_REVIEW_DEFAULT_MODEL_LABEL = getDailyReviewSettingsCopy('zh').defaultModel; const STORY_PLATFORM = 'darwin' as const; // Fidelity convention (#1433): every story below names the real app path @@ -1101,7 +1111,7 @@ async function waitForStoryCondition(predicate: () => boolean, errorMessage: str async function openDailyReviewModelSelector(canvasElement: HTMLElement): Promise { const selector = await waitForStoryButton( canvasElement, - (candidate) => candidate.textContent?.includes('跟随对话默认') === true, + (candidate) => candidate.textContent?.includes(DAILY_REVIEW_DEFAULT_MODEL_LABEL) === true, ); await userEvent.click(selector); await waitForStoryCondition( diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 4dee6bc1c2..c3747f2945 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -71,6 +71,8 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/settings/data-settings-page.tsx` | settings-page | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned | | `apps/desktop/src/renderer/settings/general-settings-page.tsx` | settings-page | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned | | `apps/desktop/src/renderer/settings/health-center-page.tsx` | settings-page | Banner, Button, Text, VStack | aligned — uses Astryx (Banner, Button, Text, VStack) | aligned | +| `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx` | settings-page | Banner, Button, CheckboxInput, EmptyState, List, ListItem, SegmentedControl, SegmentedControlItem, VStack | aligned — uses Astryx (Banner, Button, CheckboxInput, EmptyState, List, ListItem) | aligned | +| `apps/desktop/src/renderer/settings/tasks-settings-page.tsx` | settings-page | List, ListItem, TextInput | aligned — uses Astryx (List, ListItem, TextInput) | aligned | | `apps/desktop/src/renderer/settings/memory-entry-list.tsx` | settings-module | Button, EmptyState | aligned — uses Astryx (Button, EmptyState) | aligned | | `apps/desktop/src/renderer/settings/memory-settings-page.tsx` | settings-page | Banner, Button, EmptyState | aligned — uses Astryx (Banner, Button, EmptyState) | aligned | | `apps/desktop/src/renderer/settings/memory-settings-sections.tsx` | settings-module | Button | aligned — uses Astryx (Button) | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index 83b265dfcf..af97470735 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -15,7 +15,6 @@ apps/desktop/src/renderer/chat-workbar.tsx apps/desktop/src/renderer/command-palette.tsx apps/desktop/src/renderer/custom-pet-companion.tsx apps/desktop/src/renderer/error-boundary.tsx -apps/desktop/src/renderer/external-session-import-dialog.tsx apps/desktop/src/renderer/keyboard-help.tsx apps/desktop/src/renderer/live-turn-reconciler.tsx apps/desktop/src/renderer/maka-tokens.css @@ -43,6 +42,7 @@ apps/desktop/src/renderer/settings/daily-review-settings-page.tsx apps/desktop/src/renderer/settings/data-settings-page.tsx apps/desktop/src/renderer/settings/general-settings-page.tsx apps/desktop/src/renderer/settings/health-center-page.tsx +apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx apps/desktop/src/renderer/settings/memory-entry-list.tsx apps/desktop/src/renderer/settings/memory-settings-page.tsx apps/desktop/src/renderer/settings/memory-settings-sections.tsx @@ -68,6 +68,7 @@ apps/desktop/src/renderer/settings/settings-section.tsx apps/desktop/src/renderer/settings/settings-skeleton.tsx apps/desktop/src/renderer/settings/settings-surface.tsx apps/desktop/src/renderer/settings/subagent-settings-page.tsx +apps/desktop/src/renderer/settings/tasks-settings-page.tsx apps/desktop/src/renderer/settings/usage-settings-page.tsx apps/desktop/src/renderer/settings/web-search-settings-page.tsx apps/desktop/src/renderer/side-chat-close-confirmation.tsx @@ -84,7 +85,6 @@ apps/desktop/src/renderer/styles/custom-pet-companion.css apps/desktop/src/renderer/styles/daily-review.css apps/desktop/src/renderer/styles/deep-research.css apps/desktop/src/renderer/styles/error.css -apps/desktop/src/renderer/styles/external-session-import.css apps/desktop/src/renderer/styles/help.css apps/desktop/src/renderer/styles/hero.css apps/desktop/src/renderer/styles/interaction-prompts.css @@ -150,9 +150,6 @@ packages/ui/src/model-picker.tsx packages/ui/src/module-hub-selector.tsx packages/ui/src/module-pages.tsx packages/ui/src/permission-mode-menu.tsx -packages/ui/src/scheduled-task-form-dialog.tsx -packages/ui/src/scheduled-task-inspector.tsx -packages/ui/src/scheduled-task-panel.tsx packages/ui/src/primitives/chat.tsx packages/ui/src/primitives/module-page.tsx packages/ui/src/primitives/page-header.tsx @@ -161,6 +158,9 @@ packages/ui/src/prompt-anchor-rail.tsx packages/ui/src/quote-ref-chip.tsx packages/ui/src/relative-time.tsx packages/ui/src/sandbox-boundary-prompt.tsx +packages/ui/src/scheduled-task-form-dialog.tsx +packages/ui/src/scheduled-task-inspector.tsx +packages/ui/src/scheduled-task-panel.tsx packages/ui/src/search-modal.tsx packages/ui/src/session-context-layer.tsx packages/ui/src/session-history-list.tsx diff --git a/scripts/check-astryx-alignment.mjs b/scripts/check-astryx-alignment.mjs index 922e0aa493..9321bf5c37 100644 --- a/scripts/check-astryx-alignment.mjs +++ b/scripts/check-astryx-alignment.mjs @@ -14,7 +14,7 @@ const root = join(fileURLToPath(new URL('..', import.meta.url))); const BUTTON_GUARD_FILES = [ 'packages/ui/src/composer.tsx', 'apps/desktop/src/renderer/session-inspector-panel.tsx', - 'apps/desktop/src/renderer/external-session-import-dialog.tsx', + 'apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx', 'apps/desktop/src/renderer/plan-mode-panel.tsx', 'apps/desktop/src/renderer/session-workbar.tsx', ]; @@ -94,8 +94,7 @@ try { const REQUIRED_IMPORTS = [ ['packages/ui/src/composer.tsx', /Button as UiButton|from '@astryxdesign\/core'/], ['apps/desktop/src/renderer/session-inspector-panel.tsx', /ToggleButton/], - ['apps/desktop/src/renderer/external-session-import-dialog.tsx', /SegmentedControl/], - ['apps/desktop/src/renderer/external-session-import-dialog.tsx', /Item/], + ['apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx', /ListItem/], ['apps/desktop/src/renderer/plan-mode-panel.tsx', /Collapsible/], ]; for (const [rel, re] of REQUIRED_IMPORTS) { @@ -105,59 +104,14 @@ for (const [rel, re] of REQUIRED_IMPORTS) { } } -const importTsx = readFileSync( - join(root, 'apps/desktop/src/renderer/external-session-import-dialog.tsx'), - 'utf8', -); -// Parent-role paths (listitem/option/menuitem) put onClick on the root and -// suppress Item's native button — easy to ship a -1-only tabIndex trap. -if (/role=["']listitem["']/.test(importTsx)) { - failures.push('external-session-import-dialog.tsx: Item role=listitem drops keyboard button'); -} -if (/role=["']option["']/.test(importTsx) || /role=["']listbox["']/.test(importTsx)) { - failures.push( - 'external-session-import-dialog.tsx: avoid listbox/option parent-role path; leave Item without role so it supplies a focusable button', - ); -} -// Every session Item must use onClick (button path) and must not pin tabIndex=-1. -const sessionItemBlocks = [ - ...importTsx.matchAll(/className="maka-external-session-import-row"[\s\S]{0,400}?\/>/g), -]; -if (sessionItemBlocks.length === 0) { - failures.push('external-session-import-dialog.tsx: missing session Item rows'); -} -for (const block of sessionItemBlocks) { - const body = block[0]; - if (!/onClick=/.test(body)) { - failures.push( - 'external-session-import-dialog.tsx: session Item missing onClick (no button path)', - ); - } - if (/tabIndex=\{-1\}|tabIndex=\{selectedId/.test(body)) { - failures.push( - 'external-session-import-dialog.tsx: session Item must not use selected-only tabIndex (null selectedId traps keyboard)', - ); - } -} - -const importCss = readFileSync( - join(root, 'apps/desktop/src/renderer/styles/external-session-import.css'), - 'utf8', -); -if ( - /\.maka-external-session-import-row\[aria-pressed/.test(importCss) && - !/\.maka-external-session-import-row\[aria-selected/.test(importCss) -) { - failures.push( - 'external-session-import.css: selected rows must target aria-selected/aria-current, not aria-pressed', - ); -} -if ( - !/\.maka-external-session-import-row\[aria-selected/.test(importCss) && - !/\.maka-external-session-import-row\[aria-current/.test(importCss) -) { - failures.push('external-session-import.css: missing selected-state selector for Item'); -} +// The import dialog's Item-row guards are gone with the dialog (#2984). They +// checked that a SELECTED row stayed keyboard-reachable: no listitem/option +// parent role stealing Item's native button, no `tabIndex={selectedId ...}` +// trapping focus when nothing is selected, and a selected-state selector on +// aria-selected rather than aria-pressed. 设置 › 活动 › 导入任务 has no +// selection at all — 导入 sits on each row the way 恢复 does on the archived +// page — so there is no selected row to keep reachable. Guarding the new page +// for the same smells would assert a shape it does not have. const invText = readFileSync(join(root, 'docs/astryx-alignment-inventory.md'), 'utf8'); if (/module shell toolbar|module-page-bar.*42|Module shell toolbar CSS/.test(invText)) { From a17a01a2e80a910afc9560bfcb2f90805a3c1272 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:05:30 +0800 Subject: [PATCH 12/20] =?UTF-8?q?refactor:=20finish=20the=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=20rename=20where=20the=20rule=20missed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rename was applied by rule and reviewed by hand, which is the process that produces both halves of this: strings that kept the old noun, and strings that took the new one where the old was right. Same-surface contradictions, which are what a user actually notices: - The to-do panel's error banner said 任务载入失败 next to a button labelled 重新载入待办 -- one `Banner`, two vocabularies. - `chat-readiness` said 当前任务 and then 新建会话 in one sentence. - The bot's help text offered to 清空当前对话开新会话 while the reply to that same command already said 任务已重置. - 定时任务 described itself as 安排定时任务,并回顾本机任务, and its delivery option read Agent 任务执行 -- two meanings of 任务 in one form. - `settlementFailed` called the side chat a 任务 and then a 侧边对话; `forkSetupFailed` called it 追问任务, a third name for one object. Both are 侧边对话 now, matching the en twins. zh/en divergence, where the catalogs share keys but stopped sharing meaning: Daily Review counted 任务 in zh and conversations in en (and its en empty state said conversations on one branch, tasks on the other); the terminal panel said "task" and then "the session's terminal". Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal result chip), `connection-error-copy.ts`, and the CLI's own prose (`allow for session`, `Give this session full access`, `--continue`). Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the import page, 侧边对话, 对话框 for dialog, and `` as a CLI contract. The palette's `nav:sessions` keywords get 会话 and 对话 back -- collapsing both onto 任务 left a duplicate entry and dropped the words a long-time user would still type. Refs #2984 Generated-by: Claude Code --- apps/desktop/src/main/chat-readiness.ts | 4 ++-- apps/desktop/src/renderer/locales/conversation-copy.ts | 8 ++++---- .../src/renderer/locales/external-session-import-copy.ts | 4 ++-- apps/desktop/src/renderer/locales/shell-copy.ts | 2 +- apps/desktop/src/renderer/locales/shell-remaining-copy.ts | 4 ++-- packages/cli/src/__tests__/pi-transcript.test.ts | 2 +- packages/cli/src/__tests__/pi-tui-runner.test.ts | 2 +- packages/cli/src/pi-transcript.ts | 4 ++-- packages/cli/src/pi-tui-runner.ts | 2 +- packages/cli/src/run-command-core.ts | 6 +++--- packages/core/src/bot-events.ts | 4 ++-- packages/core/src/connection-error-copy.ts | 8 ++++---- packages/ui/src/daily-review-copy.ts | 4 ++-- packages/ui/src/scheduled-task-copy.ts | 8 ++++---- packages/ui/src/shared-ui-copy.ts | 4 ++-- packages/ui/src/tool-activity/copy.ts | 4 ++-- 16 files changed, 35 insertions(+), 35 deletions(-) diff --git a/apps/desktop/src/main/chat-readiness.ts b/apps/desktop/src/main/chat-readiness.ts index 36304b6b84..c067b09ed6 100644 --- a/apps/desktop/src/main/chat-readiness.ts +++ b/apps/desktop/src/main/chat-readiness.ts @@ -132,7 +132,7 @@ function messageForReason( return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`; } case 'fake_backend': - return '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。'; + return '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。'; case 'missing_default_connection': case 'connection_missing': // These reasons are handled before we reach isConnectionReady, @@ -147,7 +147,7 @@ export async function assertSessionCanSend( ): Promise { if (header.backend === 'fake') { throw chatConfigurationError( - '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。', + '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。', 'fake_backend', ); } diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index 61aa27268a..33e28af1fe 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -537,10 +537,10 @@ const COPY = { confirm: '关闭侧边对话', }, errors: { - forkSetupFailed: '无法创建追问任务,请稍后重试。', + forkSetupFailed: '无法创建侧边对话,请稍后重试。', sendRejected: '追问未能开始,请稍后重试。', sendFailed: '追问失败,请稍后重试。', - settlementFailed: '任务已结束,但消息加载失败。请重试或重新打开侧边对话。', + settlementFailed: '运行已结束,但消息加载失败。请重试或重新打开侧边对话。', respondFailed: '响应失败,请稍后重试。', }, }, @@ -643,7 +643,7 @@ const COPY = { terminalPanel: { ariaLabel: 'Task terminal', empty: 'No terminal runs in this task yet', - emptyHelp: "The session's terminal appears here once it starts.", + emptyHelp: "This task's terminal appears here once it starts.", loadFailed: 'Could not read terminal runs', retry: 'Retry', refresh: 'Refresh terminal', @@ -736,7 +736,7 @@ const COPY = { confirm: 'Close side chat', }, errors: { - forkSetupFailed: 'Could not create the companion task. Please try again.', + forkSetupFailed: 'Could not open the side chat. Please try again.', sendRejected: 'The companion could not start. Please try again.', sendFailed: 'The companion request failed. Please try again.', settlementFailed: 'The run ended, but its messages could not be loaded. Retry or reopen the side chat.', diff --git a/apps/desktop/src/renderer/locales/external-session-import-copy.ts b/apps/desktop/src/renderer/locales/external-session-import-copy.ts index 8c63a0cd46..b5497a97a8 100644 --- a/apps/desktop/src/renderer/locales/external-session-import-copy.ts +++ b/apps/desktop/src/renderer/locales/external-session-import-copy.ts @@ -44,9 +44,9 @@ const COPY = { emptyTitle: '没有可导入的对话', emptyDescription: '当前来源中没有找到符合条件的根对话。', unavailableTitle: '没有检测到支持的 Agent', - unavailableDescription: 'Maka 会在本机读取 Codex 的会话目录,不会修改其中的文件。', + unavailableDescription: 'Maka 会在本机读取 Codex 的对话目录,不会修改其中的文件。', loadFailedTitle: '无法读取外部对话', - loadFailedFallback: '外部会话目录暂时无法读取,请重试。', + loadFailedFallback: '外部对话目录暂时无法读取,请重试。', retry: '重试', archived: '已归档', loadMore: '加载更多', diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts index f63c510c1a..bd604b6d4e 100644 --- a/apps/desktop/src/renderer/locales/shell-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-copy.ts @@ -66,7 +66,7 @@ const STATIC_COMMAND_KEYWORDS: Record = { 'theme:light': ['light', 'theme', '浅色', '主题'], 'theme:dark': ['dark', 'theme', '深色', 'night', '主题'], 'theme:auto': ['auto', 'system', 'theme', '跟随', '系统', '主题'], - 'nav:sessions': ['sessions', 'chats', '任务', '任务', 'left'], + 'nav:sessions': ['sessions', 'chats', '任务', '会话', '对话', 'left'], 'nav:automations': ['automations', 'plan', 'task', 'schedule', 'cron', '定时任务', '计划', '提醒'], 'nav:skills': ['skills', '技能'], 'nav:mcp': ['mcp', 'server', 'tools', '扩展', '工具'], diff --git a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts index a151b6bf3f..83bd9fd179 100644 --- a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts @@ -55,7 +55,7 @@ const zhCopy = { refreshFailed: "刷新模型连接失败", refreshFallback: "模型连接暂时无法刷新,请稍后重试。", }, - tasks: { loadFailed: "任务载入失败,请重试。" }, + tasks: { loadFailed: "待办载入失败,请重试。" }, projects: { ungrouped: "未归属项目" }, models: { unavailable: "当前不可用" }, overlays: { @@ -131,7 +131,7 @@ const enCopy: ShellRemainingCopy = { refreshFallback: "Model connections are temporarily unavailable. Try again later.", }, - tasks: { loadFailed: "Failed to load tasks. Try again." }, + tasks: { loadFailed: "Failed to load the to-do list. Try again." }, projects: { ungrouped: "No project" }, models: { unavailable: "Currently unavailable" }, overlays: { diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index 2058a0e72f..82d1625964 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -806,7 +806,7 @@ describe('Maka Pi TUI transcript', () => { assert.ok(visibleLines.some((line) => line.includes('Read the user-selected file.'))); assert.ok(visibleLines.some((line) => line.includes('read exact /outside/file.txt'))); assert.ok(visibleLines.some((line) => line.includes('network enabled'))); - assert.ok(visibleLines.some((line) => line.includes('y/Enter allow for session'))); + assert.ok(visibleLines.some((line) => line.includes('y/Enter allow for this task'))); assert.ok(visibleLines.some((line) => line.includes('n/Esc deny'))); assert.ok(visibleLines.every((line) => !line.includes(' a '))); }); diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 8171389df9..eda9977c3c 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3009,7 +3009,7 @@ describe('Maka Pi TUI runner', () => { terminal.input('\r'); // The scan failure is surfaced, not swallowed into an empty list. await waitFor(() => - plainTerminalOutput(terminal.output()).includes('读取外部会话失败:corrupt index'), + plainTerminalOutput(terminal.output()).includes('读取外部对话失败:corrupt index'), ); exitMaka(terminal); diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts index 5a833de2b5..62c084bea4 100644 --- a/packages/cli/src/pi-transcript.ts +++ b/packages/cli/src/pi-transcript.ts @@ -1067,7 +1067,7 @@ function systemNoteText(message: SystemNoteMessage): string | undefined { case 'model_change': return 'Model changed.'; case 'context_compacted': - return 'Context compacted to keep this session within the model window.'; + return 'Context compacted to keep this task within the model window.'; case 'context_compaction_failed_open': return 'Context summary failed; the session continued without a new summary.'; case 'step_limit': @@ -1741,7 +1741,7 @@ function renderSandboxBoundaryPrompt( } lines.push( fitLine( - `${ansi.bold('y')}${ansi.dim('/Enter allow for session')} ${ansi.bold('n')}${ansi.dim('/Esc deny')}`, + `${ansi.bold('y')}${ansi.dim('/Enter allow for this task')} ${ansi.bold('n')}${ansi.dim('/Esc deny')}`, width, ), ); diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 46fa817b51..2e707c7b90 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -1880,7 +1880,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { state.entries.push({ kind: 'notice', level: 'error', - text: `读取外部会话失败:${detail}`, + text: `读取外部对话失败:${detail}`, }); } else { for (const summary of foreignScan.summaries) { diff --git a/packages/cli/src/run-command-core.ts b/packages/cli/src/run-command-core.ts index f174e18912..a47d2881cb 100644 --- a/packages/cli/src/run-command-core.ts +++ b/packages/cli/src/run-command-core.ts @@ -477,9 +477,9 @@ function makaRunHelpText(cliCommand: string): string { ' --thinking off|minimal|low|medium|high|xhigh|max|default', ' --timeout Invocation timeout', ' --max-steps Tool-step cap', - ' --yolo Give this session full access to your files and network', - ' --resume Continue an explicit compatible session', - ' --continue Continue the latest compatible session for cwd', + ' --yolo Give this task full access to your files and network', + ' --resume Continue an explicit compatible task', + ' --continue Continue the latest compatible task for cwd', ' --graph Run this turn in Graph Mode and wait for graph completion', ' -h, --help Show help', ].join('\n'); diff --git a/packages/core/src/bot-events.ts b/packages/core/src/bot-events.ts index c72e5ad957..0d9783abac 100644 --- a/packages/core/src/bot-events.ts +++ b/packages/core/src/bot-events.ts @@ -171,8 +171,8 @@ export function plaintextHelpReply(): string { 'Maka 机器人帮助', '', '· 直接发文字消息就能和 Maka 对话;回复会挂在你的提问下面。', - '· 想清空当前对话开新会话,发:restart / reset / 重置 / 重启 / 新对话。', - '· 群里不响应 plaintext 重置指令(避免一个成员把整群对话清掉)。', + '· 想清空当前任务另起一个,发:restart / reset / 重置 / 重启 / 新对话。', + '· 群里不响应 plaintext 重置指令(避免一个成员把整群的任务清掉)。', '· 长回复会自动拆成多条,第一条挂在你的提问下面。', ].join('\n'); } diff --git a/packages/core/src/connection-error-copy.ts b/packages/core/src/connection-error-copy.ts index 0ab7e9874d..9e376f2100 100644 --- a/packages/core/src/connection-error-copy.ts +++ b/packages/core/src/connection-error-copy.ts @@ -28,15 +28,15 @@ const GENERIC_FIX_COPY = '模型连接暂时无法用于发送,请到 设置 */ const REASON_FIX_COPY: Record = { missing_default_connection: '等待配置默认模型。请到 设置 · 模型 添加一个可用模型连接后再发送。', - connection_missing: '该会话依赖的模型连接已删除,请到 设置 · 模型 重新选择或重建连接。', + connection_missing: '该任务依赖的模型连接已删除,请到 设置 · 模型 重新选择或重建连接。', connection_disabled: '当前模型连接已禁用。请到 设置 · 模型 启用或选择其他默认模型。', missing_api_key: '当前模型连接还没有可用凭据。请到 设置 · 模型 补齐 API key 或重新登录后再发送。', missing_model: '当前模型连接还没有可用模型。请到 设置 · 模型 选择默认模型后再发送。', empty_model_list: '当前模型连接没有启用模型。请到 设置 · 模型 添加或启用模型后再发送。', - model_not_enabled: '当前会话选择的模型未启用。请到 设置 · 模型 重新选择可用模型后再发送。', + model_not_enabled: '当前任务选择的模型未启用。请到 设置 · 模型 重新选择可用模型后再发送。', model_not_chat_capable: - '当前会话选择的模型不能用于聊天。请到 设置 · 模型 重新选择支持聊天的模型后再发送。', - fake_backend: '当前会话来自旧的本地模拟连接。请到 设置 · 模型 添加真实模型后新建会话。', + '当前任务选择的模型不能用于聊天。请到 设置 · 模型 重新选择支持聊天的模型后再发送。', + fake_backend: '当前任务来自旧的本地模拟连接。请到 设置 · 模型 添加真实模型后新建任务。', }; /** diff --git a/packages/ui/src/daily-review-copy.ts b/packages/ui/src/daily-review-copy.ts index 2e704f6744..b160642cca 100644 --- a/packages/ui/src/daily-review-copy.ts +++ b/packages/ui/src/daily-review-copy.ts @@ -128,7 +128,7 @@ const DAILY_REVIEW_COPY = { title: (date, mode) => `${date} · ${mode}`, range: { 1: '1 day', 7: '7 days', 30: '30 days' }, generated: (trigger, time) => `${trigger} · ${time}`, - sessionCount: (count) => `${count} ${count === 1 ? 'conversation' : 'conversations'}`, + sessionCount: (count) => `${count} ${count === 1 ? 'task' : 'tasks'}`, defaultModel: 'Default task model', opening: 'Opening this report…', noContent: 'This report has no generated content.', @@ -139,7 +139,7 @@ const DAILY_REVIEW_COPY = { unit: { day: 'day', week: 'week', month: 'month' }, earlier: (unit) => `View previous ${unit}`, later: (unit) => `View next ${unit}`, }, emptyOverview: { - todayTitle: "Waiting for today's activity", rangeTitle: (label) => `No activity for ${label.toLowerCase()}`, todayBody: 'No conversations or model requests have started today.', rangeBody: (label) => `No tasks or model requests were made during ${label.toLowerCase()}.`, + todayTitle: "Waiting for today's activity", rangeTitle: (label) => `No activity for ${label.toLowerCase()}`, todayBody: 'No tasks or model requests have started today.', rangeBody: (label) => `No tasks or model requests were made during ${label.toLowerCase()}.`, }, export: { ariaLabel: 'Review export actions', copyTitle: 'Copy a Markdown summary to share or add to notes', copying: 'Copying…', copy: 'Copy', appendTitle: 'Append to the current composer draft', appending: 'Appending…', append: 'Add to composer', saveTitle: 'Save as a Markdown file', saving: 'Saving…', save: 'Save', diff --git a/packages/ui/src/scheduled-task-copy.ts b/packages/ui/src/scheduled-task-copy.ts index c94489c743..6cc60242db 100644 --- a/packages/ui/src/scheduled-task-copy.ts +++ b/packages/ui/src/scheduled-task-copy.ts @@ -166,7 +166,7 @@ const SCHEDULED_TASK_COPY = { runStatus: { ok: '已触发', blocked: '已阻止', failed: '失败' }, delivery: { local: '本地提醒', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `触发后投递到:${target}` }, form: { - editTitle: '编辑定时任务', createTitle: '新建定时任务', useTemplate: '使用模板', field: { title: '标题', time: '提醒时间', channel: '方式', recurrence: '重复', platform: '平台', cron: 'Cron', chatId: 'Chat ID', note: '备注' }, titlePlaceholder: '例如:明天复盘项目进度', groupSchedule: '频率', groupDelivery: '投递', presetsAriaLabel: '快速设置提醒时间', presets: [['ten-minutes', '10 分钟后'], ['one-hour', '1 小时后'], ['tomorrow-morning', '明天 9 点'], ['next-monday', '下周一 9 点']], recurrenceOptions: [['none', '不重复'], ['daily', '每天'], ['weekly', '每周'], ['monthly', '每月'], ['cron', 'Cron']], deliveryOptions: [['local', '本地提醒'], ['bot', '机器人聊天']], agentRunOption: 'Agent 任务执行', intervalOption: '固定间隔(由 Agent 创建)', cronPlaceholder: '例如 0 9 * * 1-5', chatIdPlaceholder: '例如 Telegram chat_id', deliveryHelp: (providers) => `当前可投递到 ${providers};其它机器人平台不会出现在投递目标里。`, notePlaceholder: '可选:补充需要提醒的上下文', saving: '保存中…', creating: '创建中…', save: '保存', create: '创建', + editTitle: '编辑定时任务', createTitle: '新建定时任务', useTemplate: '使用模板', field: { title: '标题', time: '提醒时间', channel: '方式', recurrence: '重复', platform: '平台', cron: 'Cron', chatId: 'Chat ID', note: '备注' }, titlePlaceholder: '例如:明天复盘项目进度', groupSchedule: '频率', groupDelivery: '投递', presetsAriaLabel: '快速设置提醒时间', presets: [['ten-minutes', '10 分钟后'], ['one-hour', '1 小时后'], ['tomorrow-morning', '明天 9 点'], ['next-monday', '下周一 9 点']], recurrenceOptions: [['none', '不重复'], ['daily', '每天'], ['weekly', '每周'], ['monthly', '每月'], ['cron', 'Cron']], deliveryOptions: [['local', '本地提醒'], ['bot', '机器人聊天']], agentRunOption: '交给 Agent 执行', intervalOption: '固定间隔(由 Agent 创建)', cronPlaceholder: '例如 0 9 * * 1-5', chatIdPlaceholder: '例如 Telegram chat_id', deliveryHelp: (providers) => `当前可投递到 ${providers};其它机器人平台不会出现在投递目标里。`, notePlaceholder: '可选:补充需要提醒的上下文', saving: '保存中…', creating: '创建中…', save: '保存', create: '创建', }, page: { title: '定时任务', refreshing: '正在刷新定时任务', refresh: '刷新定时任务', create: '新建定时任务', keepAwake: '保持系统唤醒', pageSettings: '定时任务页面设置', keepAwakeErrorTitle: '无法更新保持系统唤醒', keepAwakeErrorFallback: '更新保持系统唤醒设置失败,请稍后重试。', viewsAriaLabel: '定时任务视图', tasks: '我的定时任务', runs: '执行记录', filtersAriaLabel: '定时任务筛选', sort: '排序', sortOptions: [['created-desc', '按创建时间倒序'], ['next-run-asc', '按下次触发升序'], ['updated-desc', '按更新时间倒序']], searchLabel: '搜索定时任务', searchPlaceholder: '搜索标题、备注、投递或执行记录…', state: '状态', filterOption: (label, count) => `${label} ${count}`, active: '进行中', all: '全部', range: '范围', rangeOptions: [['day', '今天'], ['week', '近 7 天'], ['month', '近 30 天'], ['all', '全部记录']], searchMatches: (count) => `找到 ${count} 个匹配提醒`, clearSearch: '清除搜索', noSearchTitle: '没有匹配的提醒', noFilterTitle: '当前筛选没有提醒', noSearchBody: '调整搜索词,或切换状态筛选查看其他提醒。', noFilterBody: '切换筛选查看其他状态,或创建新的定时任务。', emptyTitle: '还没有定时任务', emptyBody: '创建一个提醒,让 Maka 在指定时间继续这项工作。', listAriaLabel: '定时任务列表', inspectorOpened: (title) => `已打开「${title}」的任务详情`, edit: '编辑', duplicate: '复制', triggering: '触发中…', triggerNow: '立即触发', snoozing: '延后中…', snooze: '延后 10 分钟', clearing: '清空中…', clearRuns: '清空记录', deleting: '删除中…', delete: '删除', nextRun: (time) => `下次触发:${time}`, recentRun: (time) => `最近 ${time}`, unscheduled: '未安排', noRunsTitle: '暂无执行记录', noRunsBody: '提醒触发、手动执行或投递失败后,会在这里保留最近记录。', showAllTime: '显示全部时间', runsAriaLabel: '定时任务执行记录', activeCount: (count) => `${count} 个进行中`, @@ -183,7 +183,7 @@ const SCHEDULED_TASK_COPY = { noRuns: '这个任务还没有执行记录。', agentSource: 'Agent 定时任务', agentSourceHint: '到点后,Maka 会使用创建时的执行设置启动新任务。', - agentDelivery: 'Agent 任务执行', + agentDelivery: '交给 Agent 执行', }, }, en: { @@ -201,7 +201,7 @@ const SCHEDULED_TASK_COPY = { runStatus: { ok: 'Triggered', blocked: 'Blocked', failed: 'Failed' }, delivery: { local: 'Local notification', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `Deliver to: ${target}` }, form: { - editTitle: 'Edit scheduled task', createTitle: 'New scheduled task', useTemplate: 'Use template', field: { title: 'Title', time: 'Task time', channel: 'Method', recurrence: 'Repeat', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, titlePlaceholder: 'For example: Review project progress tomorrow', groupSchedule: 'Frequency', groupDelivery: 'Delivery', presetsAriaLabel: 'Quick task times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], agentRunOption: 'Agent task run', intervalOption: 'Fixed interval (created by Agent)', cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this task', saving: 'Saving…', creating: 'Creating…', save: 'Save', create: 'Create', + editTitle: 'Edit scheduled task', createTitle: 'New scheduled task', useTemplate: 'Use template', field: { title: 'Title', time: 'Task time', channel: 'Method', recurrence: 'Repeat', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, titlePlaceholder: 'For example: Review project progress tomorrow', groupSchedule: 'Frequency', groupDelivery: 'Delivery', presetsAriaLabel: 'Quick task times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], agentRunOption: 'Run via the Agent', intervalOption: 'Fixed interval (created by Agent)', cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this task', saving: 'Saving…', creating: 'Creating…', save: 'Save', create: 'Create', }, page: { title: 'Scheduled tasks', refreshing: 'Refreshing scheduled tasks', refresh: 'Refresh scheduled tasks', create: 'New scheduled task', keepAwake: 'Keep system awake', pageSettings: 'Scheduled task page settings', keepAwakeErrorTitle: 'Could not update Keep system awake', keepAwakeErrorFallback: 'Could not update the Keep system awake setting. Try again later.', viewsAriaLabel: 'Scheduled task views', tasks: 'My scheduled tasks', runs: 'Run history', filtersAriaLabel: 'Scheduled task filters', sort: 'Sort', sortOptions: [['created-desc', 'Newest created first'], ['next-run-asc', 'Next run first'], ['updated-desc', 'Recently updated first']], searchLabel: 'Search scheduled tasks', searchPlaceholder: 'Search titles, notes, delivery, or run history…', state: 'Status', filterOption: (label, count) => `${label} ${count}`, active: 'Active', all: 'All', range: 'Range', rangeOptions: [['day', 'Today'], ['week', 'Last 7 days'], ['month', 'Last 30 days'], ['all', 'All runs']], searchMatches: (count) => `${count} matching ${count === 1 ? 'task' : 'tasks'}`, clearSearch: 'Clear search', noSearchTitle: 'No matching tasks', noFilterTitle: 'No tasks in this filter', noSearchBody: 'Change the search terms or status filter to find other tasks.', noFilterBody: 'Change the filter or create a new scheduled task.', emptyTitle: 'No scheduled tasks yet', emptyBody: 'Create a task so Maka can continue this work at the right time.', listAriaLabel: 'Scheduled task list', inspectorOpened: (title) => `Opened the task details for ${title}`, edit: 'Edit', duplicate: 'Duplicate', triggering: 'Triggering…', triggerNow: 'Trigger now', snoozing: 'Snoozing…', snooze: 'Snooze 10 minutes', clearing: 'Clearing…', clearRuns: 'Clear history', deleting: 'Deleting…', delete: 'Delete', nextRun: (time) => `Next run: ${time}`, recentRun: (time) => `Last run ${time}`, unscheduled: 'Not scheduled', noRunsTitle: 'No run history', noRunsBody: 'Triggered tasks, manual runs, and delivery failures appear here.', showAllTime: 'All time', runsAriaLabel: 'Scheduled task run history', activeCount: (count) => `${count} active`, @@ -219,7 +219,7 @@ const SCHEDULED_TASK_COPY = { agentSource: 'Agent scheduled task', agentSourceHint: 'When due, Maka starts a new task using the execution settings captured at creation.', - agentDelivery: 'Agent task run', + agentDelivery: 'Run via the Agent', }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 329f87eaa2..d2a9b6f934 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -162,7 +162,7 @@ const SHARED_UI_COPY = { }, automations: { title: '定时任务', - description: '安排定时任务,并回顾本机任务中的工作进展。', + description: '安排定时运行,并回顾本机任务的工作进展。', selectorLabel: (module) => `定时任务内容:${module}`, scheduledTasks: '定时任务', dailyReview: '每日回顾', @@ -251,7 +251,7 @@ const SHARED_UI_COPY = { }, automations: { title: 'Scheduled tasks', - description: 'Schedule tasks and review progress from local tasks.', + description: 'Schedule recurring runs and review progress across local tasks.', selectorLabel: (module) => `Scheduled task content: ${module}`, scheduledTasks: 'Scheduled tasks', dailyReview: 'Daily review', diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts index 596d766fb1..c5612dc262 100644 --- a/packages/ui/src/tool-activity/copy.ts +++ b/packages/ui/src/tool-activity/copy.ts @@ -192,7 +192,7 @@ const TOOL_ACTIVITY_COPY = { loadTools: { displayName: '加载工具组', loaded: (namespace) => namespace ? `已加载 ${namespace} 工具组` : '已加载工具组', count: (n) => `新增 ${n} 个可用工具:`, footer: '下一步即可调用' }, permissionDenied: '用户已拒绝权限请求', result: { - hiddenLines: (n) => `… 已隐藏 ${n} 行`, ptyFailed: '后台终端交互失败', queued: '已输入', notQueued: '未输入', queuedPreview: (action, preview, bytes) => bytes === undefined ? `${action}:${preview}` : `${action}:${preview}… · 共 ${bytes} 字节`, byteCount: (action, bytes) => `${action} ${bytes} 字节`, resizeNotApplied: (size) => `未调整为 ${size}`, resized: (size) => `已调整为 ${size}`, sizeUnchanged: (size) => `尺寸已是 ${size}`, ptyCompleted: '后台终端交互已完成', terminalUnavailable: '终端输出不可用', noTerminalFrame: '(无可用终端画面)', noOutputYet: '(尚无输出)', noOutput: '(无输出)', exitCode: (code) => `退出码 ${code}`, managedBySource: '由源会话管理', sourceUnavailable: '源会话不可用', running: '运行中', success: '成功', failed: '失败', timedOut: '已超时', cancelled: '已取消', disconnected: '已断开', terminalTruncated: '终端输出已截断', terminalRedacted: '终端输出已脱敏', streamHidden: (stream, n) => `… ${stream} 已隐藏 ${n} 行`, streamsTruncated: (limit) => `输出已截断 · 每路仅展示前 ${limit} 行`, outputTruncated: '输出已截断', outputRedacted: '输出已脱敏', + hiddenLines: (n) => `… 已隐藏 ${n} 行`, ptyFailed: '后台终端交互失败', queued: '已输入', notQueued: '未输入', queuedPreview: (action, preview, bytes) => bytes === undefined ? `${action}:${preview}` : `${action}:${preview}… · 共 ${bytes} 字节`, byteCount: (action, bytes) => `${action} ${bytes} 字节`, resizeNotApplied: (size) => `未调整为 ${size}`, resized: (size) => `已调整为 ${size}`, sizeUnchanged: (size) => `尺寸已是 ${size}`, ptyCompleted: '后台终端交互已完成', terminalUnavailable: '终端输出不可用', noTerminalFrame: '(无可用终端画面)', noOutputYet: '(尚无输出)', noOutput: '(无输出)', exitCode: (code) => `退出码 ${code}`, managedBySource: '由源任务管理', sourceUnavailable: '源任务不可用', running: '运行中', success: '成功', failed: '失败', timedOut: '已超时', cancelled: '已取消', disconnected: '已断开', terminalTruncated: '终端输出已截断', terminalRedacted: '终端输出已脱敏', streamHidden: (stream, n) => `… ${stream} 已隐藏 ${n} 行`, streamsTruncated: (limit) => `输出已截断 · 每路仅展示前 ${limit} 行`, outputTruncated: '输出已截断', outputRedacted: '输出已脱敏', backgroundStatus: { running: '后台运行中', completed: '后台已完成', failed: '后台失败', timed_out: '后台超时', cancelled: '后台已取消', orphaned: '后台任务已断开' }, backgroundUnknown: (status) => `后台 · ${status}`, workflow: { action: '动作', status: '状态', error: '错误', nodes: '节点摘要', diagnostics: '诊断片段' }, webNoResults: '没有结果', webResults: (n) => `${n} 条结果`, credentialSource: { env: '环境变量', settings: '本机已保存 key', missing: '未配置', unknown: '来源未知' }, webFailure: '搜索失败', webSearch: '联网搜索', webGuidance: { env: '请检查 TAVILY_API_KEY / MAKA_TAVILY_API_KEY 后重启。', settings: '请在 设置 · 联网搜索 中更新 Tavily key。', rate_limited: 'Tavily 当前限流,请稍后重试或更换可用凭据。', not_configured: '请先完成联网搜索配置后再重试。', timed_out: '请求超时,请稍后重试。', privacy_mode: '隐私模式下不会发起联网搜索。', unknown: '请检查网络或稍后重试。' }, }, @@ -249,7 +249,7 @@ const TOOL_ACTIVITY_COPY = { loadTools: { displayName: 'Load tools', loaded: (namespace) => namespace ? `Loaded ${namespace} tools` : 'Loaded tools', count: (n) => `Added ${n} available ${n === 1 ? 'tool' : 'tools'}:`, footer: 'Ready to use' }, permissionDenied: 'User denied the permission request', result: { - hiddenLines: (n) => `… ${n} ${n === 1 ? 'line' : 'lines'} hidden`, ptyFailed: 'Background terminal interaction failed', queued: 'Entered', notQueued: 'Not entered', queuedPreview: (action, preview, bytes) => bytes === undefined ? `${action}: ${preview}` : `${action}: ${preview}… · ${bytes} bytes total`, byteCount: (action, bytes) => `${action} ${bytes} bytes`, resizeNotApplied: (size) => `Not resized to ${size}`, resized: (size) => `Resized to ${size}`, sizeUnchanged: (size) => `Size already ${size}`, ptyCompleted: 'Background terminal interaction completed', terminalUnavailable: 'Terminal output unavailable', noTerminalFrame: '(No terminal frame available)', noOutputYet: '(No output yet)', noOutput: '(No output)', exitCode: (code) => `exit code ${code}`, managedBySource: 'Managed by source conversation', sourceUnavailable: 'Source conversation unavailable', running: 'Running', success: 'Succeeded', failed: 'Failed', timedOut: 'Timed out', cancelled: 'Cancelled', disconnected: 'Disconnected', terminalTruncated: 'Terminal output truncated', terminalRedacted: 'Terminal output redacted', streamHidden: (stream, n) => `… ${n} ${stream} ${n === 1 ? 'line' : 'lines'} hidden`, streamsTruncated: (limit) => `Output truncated · showing the first ${limit} lines of each stream`, outputTruncated: 'Output truncated', outputRedacted: 'Output redacted', + hiddenLines: (n) => `… ${n} ${n === 1 ? 'line' : 'lines'} hidden`, ptyFailed: 'Background terminal interaction failed', queued: 'Entered', notQueued: 'Not entered', queuedPreview: (action, preview, bytes) => bytes === undefined ? `${action}: ${preview}` : `${action}: ${preview}… · ${bytes} bytes total`, byteCount: (action, bytes) => `${action} ${bytes} bytes`, resizeNotApplied: (size) => `Not resized to ${size}`, resized: (size) => `Resized to ${size}`, sizeUnchanged: (size) => `Size already ${size}`, ptyCompleted: 'Background terminal interaction completed', terminalUnavailable: 'Terminal output unavailable', noTerminalFrame: '(No terminal frame available)', noOutputYet: '(No output yet)', noOutput: '(No output)', exitCode: (code) => `exit code ${code}`, managedBySource: 'Managed by the source task', sourceUnavailable: 'Source task unavailable', running: 'Running', success: 'Succeeded', failed: 'Failed', timedOut: 'Timed out', cancelled: 'Cancelled', disconnected: 'Disconnected', terminalTruncated: 'Terminal output truncated', terminalRedacted: 'Terminal output redacted', streamHidden: (stream, n) => `… ${n} ${stream} ${n === 1 ? 'line' : 'lines'} hidden`, streamsTruncated: (limit) => `Output truncated · showing the first ${limit} lines of each stream`, outputTruncated: 'Output truncated', outputRedacted: 'Output redacted', backgroundStatus: { running: 'Running in background', completed: 'Background task completed', failed: 'Background task failed', timed_out: 'Background task timed out', cancelled: 'Background task cancelled', orphaned: 'Background task disconnected' }, backgroundUnknown: (status) => `Background · ${status}`, workflow: { action: 'Action', status: 'Status', error: 'Error', nodes: 'Node summary', diagnostics: 'Diagnostic excerpts' }, webNoResults: 'No results', webResults: (n) => `${n} ${n === 1 ? 'result' : 'results'}`, credentialSource: { env: 'Environment variable', settings: 'Locally saved key', missing: 'Not configured', unknown: 'Unknown source' }, webFailure: 'Search failed', webSearch: 'Web search', webGuidance: { env: 'Check TAVILY_API_KEY / MAKA_TAVILY_API_KEY and restart.', settings: 'Update the Tavily key in Settings · Web search.', rate_limited: 'Tavily is rate-limiting requests. Try again later or use another credential.', not_configured: 'Configure web search before retrying.', timed_out: 'The request timed out. Try again later.', privacy_mode: 'Web search is disabled in privacy mode.', unknown: 'Check the network connection or try again later.' }, }, From 971bef3d6d1b284a5cd8515db47f24c2a3fdaf77 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:36:18 +0800 Subject: [PATCH 13/20] fix(ui): route the task row's last two dots through the shared vocabulary `streaming` and `hasUnread` still wrote `'accent'` by hand while the status and stale signals resolved through `dotForStatus`. Half a function deciding its own colours is the drift this change set out to close -- the literal is what let the rail and Settings disagree about the same fact before. Both are `active`: streaming is the system working on it, and unread text is "something happened here" rather than a question waiting on the user, which is the line the two semantics exist to draw. `dotForStatus('active')` is `'accent'`, so nothing moves on screen. Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed the list's own aria-label, and the outer SideNav took that name over when the rail was rebuilt. Refs #2984 Generated-by: Claude Code --- packages/ui/src/session-history-list.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/session-history-list.tsx b/packages/ui/src/session-history-list.tsx index 76826138ca..28d58e1596 100644 --- a/packages/ui/src/session-history-list.tsx +++ b/packages/ui/src/session-history-list.tsx @@ -80,7 +80,6 @@ export function SessionHistoryList(props: { rowActions?: SessionRowActions; }) { const locale = useUiLocale(); - const copy = getConversationCopy(locale).sessions; function handleListKeyDown(event: KeyboardEvent) { if (event.key !== 'Delete' && event.key !== 'Backspace') return; @@ -731,9 +730,13 @@ function sessionRowSignals( const copy = getConversationCopy(locale).sessions; const signals: SessionRowSignal[] = []; + // `active`, through the same vocabulary as everything else here: streaming is + // the system working on it right now, which is what that semantic names. + // Writing `accent` directly would resolve to the identical colour and reopen + // the drift this change closed — half the row's dots deciding for themselves. if (options.streaming) { signals.push({ - variant: 'accent', + variant: dotForStatus('active'), label: copy.respondingAriaLabel, isPulsing: true, tooltip: copy.respondingTitle, @@ -760,9 +763,11 @@ function sessionRowSignals( // Unread ranks under both because it is the weakest claim on attention: a // task that is running or holding a question already says something more - // specific about the same unread text. + // specific about the same unread text. `active` and not `attention`: unread + // text is "something happened here", not a question waiting on the user — + // that distinction is the whole point of the two semantics. if (!options.active && session.hasUnread) { - signals.push({ variant: 'accent', label: copy.unreadAriaLabel }); + signals.push({ variant: dotForStatus('active'), label: copy.unreadAriaLabel }); } // Stale is a renderer-derived fact, not a persisted status, which is why it From 60d2631ba42969079d8b2dbbf898c2944ac7b1e1 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:36:25 +0800 Subject: [PATCH 14/20] docs: say what each session-status-presentation file actually owns Desktop's copy claimed the status-to-dot mapping was "re-exported below" and that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in this branch and neither is in the file; what is left is Desktop-only judgment -- which blocked reasons are worth acting on, and what to offer after a turn fails -- so the header says that instead. The contract the old header carried is real and had nowhere to live after the move: a UI label must never show a raw `SessionBlockedReason`, and a new reason has to extend the core enum and the copy matrix together or it reads as `unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the matrix it constrains is. Refs #2984 Generated-by: Claude Code --- .../renderer/session-status-presentation.ts | 27 +++++++++---------- .../ui/src/session-status-presentation.ts | 7 +++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/renderer/session-status-presentation.ts b/apps/desktop/src/renderer/session-status-presentation.ts index 757e510925..9387048a0c 100644 --- a/apps/desktop/src/renderer/session-status-presentation.ts +++ b/apps/desktop/src/renderer/session-status-presentation.ts @@ -1,21 +1,17 @@ /** - * Renderer-side presentation helpers for SessionStatus, SessionBlockedReason, - * and failed-turn recovery. + * Renderer-side presentation rules that only Desktop has: which blocked reasons + * are worth acting on, and what to offer after a turn fails. * - * Separated from the React component layer so the mapping can be unit-tested + * Separated from the React component layer so the rules can be unit-tested * without a DOM, mirroring the `session-health-notice.ts` pattern. * - * One contract enforced here: **generalized blocked-reason copy** (@kenji - * review). UI labels never expose the raw `SessionBlockedReason` enum string; - * `describeBlockedReason` is the canonical translation, and a new blocked reason - * must extend the core enum AND that matrix together or the `unknown` fallback - * applies. - * - * The status → dot mapping itself lives in `@maka/ui`; it is re-exported below - * rather than restated. A second contract used to be documented here — a tone - * matrix "consumed by both the SessionStatusIcon and the chat-header status - * badge" — describing two consumers that do not exist and a tone layer that has - * since been removed (#2984). + * Turning a `SessionStatus` into a label and a dot, and a `SessionBlockedReason` + * into copy, is NOT here — both live in `@maka/ui`'s file of the same name, + * which is also where the contract that a UI label never shows a raw enum + * identifier is stated and enforced. This file used to re-export those and + * document a tone matrix "consumed by both the SessionStatusIcon and the + * chat-header status badge", naming two consumers that do not exist; the tone + * layer and the re-exports are gone (#2984). */ import { SANDBOX_BOUNDARY_RESTART_CLOSURE_CLASS } from '@maka/core/sandbox-boundary'; @@ -60,7 +56,8 @@ export function normalizeSessionSummaryForDisplay(session: SessionSummary): Sess /** * Generalized Chinese phrasing for a failed turn's `errorClass` - * Mirrors `describeBlockedReason()`; UI must never display the raw enum identifier. + * Mirrors `describeBlockedReason()` in `@maka/ui`, under the same rule: a UI + * label must never display the raw enum identifier. * * Recognized classes are written by the runtime via `classifyError()`, * `classifyHttpStatus()`, and `event.reason` / `event.code`. The set is diff --git a/packages/ui/src/session-status-presentation.ts b/packages/ui/src/session-status-presentation.ts index 9df083d2f4..b99b1c8979 100644 --- a/packages/ui/src/session-status-presentation.ts +++ b/packages/ui/src/session-status-presentation.ts @@ -59,6 +59,13 @@ export function presentSessionStatus( }; } +/** + * The canonical translation of a blocked reason, and the contract that a UI + * label never exposes the raw `SessionBlockedReason` identifier (@kenji review). + * A new reason must extend the core enum AND the copy matrix together, or it + * reads as `unknown` — which is the intended failure, not a silent leak of the + * enum string into the interface. + */ export function describeBlockedReason( reason: SessionBlockedReason | undefined, locale: UiLocale = 'zh', From 74b2e8043df1e2388f09b3237523b922f4fbee64 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:36:32 +0800 Subject: [PATCH 15/20] test: make the restored-task and import fixtures answer like their sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both fixtures modelled states their real source cannot produce, which makes them evidence for nothing. The purge tests built a restored task by flipping `isArchived` alone, leaving `status: 'archived'` behind. `SessionStore.unarchive` writes both fields together, so no stored row looks like that. The sweep only reads `isArchived` today, so no assertion changes -- the point is that the next assertion written against these rows would be checking a state that cannot happen. A `restored` helper names the pair once. The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered the archived conversation while the filter was off and handed 加载更多 the same first page forever, under a comment claiming it demonstrated both controls. It now filters and pages, with a fourth conversation so the default view is a short first page rather than the whole list. Refs #2984 Generated-by: Claude Code --- .../__tests__/app-shell-session-purge.test.ts | 14 ++++++-- .../settings/settings-pages.stories.tsx | 32 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts b/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts index 95c2258c76..8abe78c452 100644 --- a/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts +++ b/apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts @@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial = {}): SessionSu }; } +/** + * A task that left the archive. Both fields move together because that is what + * `SessionStore.unarchive` writes; flipping only `isArchived` would build a row + * the store cannot produce, and the sweep would then be tested against a state + * it will never meet. + */ +function restored(id: string): SessionSummary { + return summary(id, { isArchived: false, status: 'active' }); +} + type SweepHarness = { removed: string[]; cleared: string[]; @@ -128,7 +138,7 @@ describe('purgeSessions', () => { // dialog was up has left it, and a sweep that deleted it anyway would be // acting outside what was agreed to. const h = harness(); - const sessions = [summary('kept', { isArchived: false }), summary('doomed')]; + const sessions = [restored('kept'), summary('doomed')]; const restore = installWindow(h); const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } }); @@ -148,7 +158,7 @@ describe('purgeSessions', () => { const sessions = [summary('first'), summary('second')]; const restore = installWindow(h, { onRemove: (id) => { - if (id === 'first') sessions[1] = summary('second', { isArchived: false }); + if (id === 'first') sessions[1] = restored('second'); }, }); const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } }); diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx index e6bc185b7e..ba99a36175 100644 --- a/apps/desktop/stories/settings/settings-pages.stories.tsx +++ b/apps/desktop/stories/settings/settings-pages.stories.tsx @@ -663,10 +663,24 @@ const makaBridge = { }, // 导入任务 reads another agent's session directory through Desktop Main. The // fixture answers with one source and a short first page so the story shows - // the source switch, the archived filter, and 加载更多 together. + // the source switch, the archived filter, and 加载更多 together. It honours + // `includeArchived` and `cursor` rather than returning one fixed page: + // otherwise the archived row shows while its filter is off and 加载更多 hands + // back the first page forever, which is a control the story cannot be used to + // judge. externalSessions: { listSources: async () => ({ adapterIds: ['codex'] }), - list: async () => ({ sessions: externalConversations, nextCursor: 'page-2' }), + list: async (input: { includeArchived?: boolean; cursor?: string }) => { + const visible = externalConversations.filter( + (conversation) => input.includeArchived || !conversation.archived, + ); + const start = input.cursor === EXTERNAL_SECOND_PAGE ? EXTERNAL_PAGE_SIZE : 0; + const end = start + EXTERNAL_PAGE_SIZE; + return { + sessions: visible.slice(start, end), + nextCursor: end < visible.length ? EXTERNAL_SECOND_PAGE : null, + }; + }, import: async () => ({ ok: false as const }), }, // Appearance mounts CustomPetSettingsSection, which reads and subscribes on @@ -752,6 +766,14 @@ const archivedTaskSessions: SessionSummary[] = [ // 导入任务's rows come from another agent's directory, not from Maka's store: // a source-native id, the cwd it ran in, and whether that agent archived it. +/** + * Two rows a page, so the default view — three unarchived conversations — is + * one short page plus 加载更多, and turning the archived filter on changes both + * the first page and how many pages there are. + */ +const EXTERNAL_PAGE_SIZE = 2; +const EXTERNAL_SECOND_PAGE = 'page-2'; + const externalConversations: ExternalSessionSummary[] = [ { id: 'codex-01930f', @@ -765,6 +787,12 @@ const externalConversations: ExternalSessionSummary[] = [ cwd: '/Users/storybook-fixture-user/workspace/maka-agent', updatedAt: Date.now() - 3 * 60 * 60 * 1000, }, + { + id: 'codex-01930a', + name: 'Reproduce the SQLite lock contention under parallel evals', + cwd: '/Users/storybook-fixture-user/workspace/maka-agent', + updatedAt: Date.now() - 2 * 24 * 60 * 60 * 1000, + }, { id: 'codex-01929c', name: 'Draft the release notes for 0.9.0', From f531b9842c50be1071ee3253cd9a3c05ee41c845 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:36:37 +0800 Subject: [PATCH 16/20] =?UTF-8?q?fix(desktop):=20tidy=20the=20two=20places?= =?UTF-8?q?=20the=20=E4=BB=BB=E5=8A=A1=20rename=20passed=20through=20twice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fake_backend` reaches the user down two paths -- the reason table, and the header check in `assertSessionCanSend`, which never gets far enough to look a connection up. They said the same sentence in two copies, so renaming 会话 to 任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now. The English workspace help came out of the rename as "Any task can switch next to the input box", which makes the task the actor and never names what it switches. It says the project. Refs #2984 Generated-by: Claude Code --- apps/desktop/src/main/chat-readiness.ts | 17 ++++++++++++----- .../renderer/locales/settings-projects-copy.ts | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/chat-readiness.ts b/apps/desktop/src/main/chat-readiness.ts index c067b09ed6..1aa1616225 100644 --- a/apps/desktop/src/main/chat-readiness.ts +++ b/apps/desktop/src/main/chat-readiness.ts @@ -106,6 +106,16 @@ export async function requireReadyConnection( * so the copy stays close to its existing semantics (PR110a refactor * is behavior-preserving — only the judgment moved to core). */ +/** + * Two paths reach `fake_backend`: the reason table below, and the header check + * in `assertSessionCanSend`, which never gets far enough to look a connection + * up. They are the same sentence to the user, so they are the same string here + * — the rename that moved 会话 to 任务 had to be applied twice, which is what a + * second copy costs. + */ +const FAKE_BACKEND_MESSAGE = + '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。'; + function messageForReason( reason: ChatConfigurationReason, connection: LlmConnection, @@ -132,7 +142,7 @@ function messageForReason( return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`; } case 'fake_backend': - return '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。'; + return FAKE_BACKEND_MESSAGE; case 'missing_default_connection': case 'connection_missing': // These reasons are handled before we reach isConnectionReady, @@ -146,10 +156,7 @@ export async function assertSessionCanSend( deps: ReadyConnectionDeps, ): Promise { if (header.backend === 'fake') { - throw chatConfigurationError( - '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。', - 'fake_backend', - ); + throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend'); } await requireReadyConnection(header.llmConnectionSlug, deps, header.model); } diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts index 8ce3f968d9..78f433455a 100644 --- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts @@ -233,7 +233,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { }, section: 'Workspace', sectionHelp: - 'New tasks open in the default project; without one, they reuse the project you last used. Any task can switch next to the input box.', + 'New tasks open in the default project; without one, they reuse the project you last used. You can switch any task to a different project next to the input box.', addProject: 'Add project', defaultBadge: 'Default', setDefault: 'Set as default', From aea07078f72ac30ece7e8bcad6951d0c0b4b7300 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:36:44 +0800 Subject: [PATCH 17/20] chore: regenerate the Astryx surface inventory The Markdown half still listed `external-session-import-dialog.tsx` and `external-session-import.css` after this branch deleted them, because the two halves were edited by hand and `check-astryx-surface-inventory.mjs` only verifies that on-disk files appear in the inventory -- never that inventory rows still exist on disk. It is also not wired into CI, so the drift was invisible from both directions. Running the generator also picks up three files that arrived from main and were never listed: `runtime-host-profiles-section.tsx`, `runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They are unrelated to this branch, but a generated file regenerated in halves is how it drifted in the first place. Refs #2984 Generated-by: Claude Code --- docs/astryx-surface-file-inventory.md | 19 ++++++++++--------- docs/astryx-surface-file-inventory.paths | 3 +++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index c3747f2945..a4707cd456 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -5,7 +5,7 @@ Each row is one on-disk product surface file. Regenerated inventory must stay in Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 183 files — blocker 0, polish 0, aligned 183. +**Totals:** 186 files — blocker 0, polish 0, aligned 186. ## Exclusions (explicit) @@ -43,7 +43,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/command-palette.tsx` | dialog-overlay | EmptyState | aligned — uses Astryx (EmptyState) | aligned | | `apps/desktop/src/renderer/custom-pet-companion.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/error-boundary.tsx` | other | Button, Card | aligned — uses Astryx (Button, Card) | aligned | -| `apps/desktop/src/renderer/external-session-import-dialog.tsx` | dialog-overlay | Banner, Button, CheckboxInput, Dialog, DialogHeader, EmptyState, HStack, Item, Layout, LayoutContent, SegmentedControl, SegmentedControlItem, Spinner, Text, VStack | aligned — uses Astryx (Banner, Button, CheckboxInput, Dialog, DialogHeader, EmptyState, HStack, Item) | aligned | | `apps/desktop/src/renderer/keyboard-help.tsx` | dialog-overlay | Dialog, DialogHeader, Heading, Layout, LayoutContent | aligned — uses Astryx (Dialog, DialogHeader, Heading, Layout, LayoutContent) | aligned | | `apps/desktop/src/renderer/live-turn-reconciler.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/maka-tokens.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | @@ -71,8 +70,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/settings/data-settings-page.tsx` | settings-page | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned | | `apps/desktop/src/renderer/settings/general-settings-page.tsx` | settings-page | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned | | `apps/desktop/src/renderer/settings/health-center-page.tsx` | settings-page | Banner, Button, Text, VStack | aligned — uses Astryx (Banner, Button, Text, VStack) | aligned | -| `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx` | settings-page | Banner, Button, CheckboxInput, EmptyState, List, ListItem, SegmentedControl, SegmentedControlItem, VStack | aligned — uses Astryx (Banner, Button, CheckboxInput, EmptyState, List, ListItem) | aligned | -| `apps/desktop/src/renderer/settings/tasks-settings-page.tsx` | settings-page | List, ListItem, TextInput | aligned — uses Astryx (List, ListItem, TextInput) | aligned | +| `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx` | settings-page | Banner, Button, CheckboxInput, EmptyState, HStack, List, ListItem, SegmentedControl, SegmentedControlItem, Spinner, VStack | aligned — uses Astryx (Banner, Button, CheckboxInput, EmptyState, HStack, List, ListItem, SegmentedControl) | aligned | | `apps/desktop/src/renderer/settings/memory-entry-list.tsx` | settings-module | Button, EmptyState | aligned — uses Astryx (Button, EmptyState) | aligned | | `apps/desktop/src/renderer/settings/memory-settings-page.tsx` | settings-page | Banner, Button, EmptyState | aligned — uses Astryx (Banner, Button, EmptyState) | aligned | | `apps/desktop/src/renderer/settings/memory-settings-sections.tsx` | settings-module | Button | aligned — uses Astryx (Button) | aligned | @@ -89,6 +87,8 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/settings/provider-oauth-section.tsx` | settings-module | Banner, Button, HStack, Text, VStack | aligned — uses Astryx (Banner, Button, HStack, Text, VStack) | aligned | | `apps/desktop/src/renderer/settings/providers-panel.tsx` | settings-module | Badge, Banner, Button, EmptyState, HStack, Heading, List, ListItem, Text, VStack | aligned — uses Astryx (Badge, Banner, Button, EmptyState, HStack, Heading, List, ListItem) | aligned | | `apps/desktop/src/renderer/settings/request-customization-editor.tsx` | settings-module | Button, HStack, IconButton, Text, VStack | aligned — uses Astryx (Button, HStack, IconButton, Text, VStack) | aligned | +| `apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx` | settings-module | Badge, Banner, Button, HStack, List, ListItem, SegmentedControl, SegmentedControlItem, Switch | aligned — uses Astryx (Badge, Banner, Button, HStack, List, ListItem, SegmentedControl, SegmentedControlItem) | aligned | +| `apps/desktop/src/renderer/settings/runtime-host-ssh-terminal-dialog.tsx` | settings-module | Banner, Button, Dialog, DialogHeader, Layout, LayoutContent | aligned — uses Astryx (Banner, Button, Dialog, DialogHeader, Layout, LayoutContent) | aligned | | `apps/desktop/src/renderer/settings/settings-expandable-row.tsx` | settings-module | Button, HStack, Text | aligned — uses Astryx (Button, HStack, Text) | aligned | | `apps/desktop/src/renderer/settings/settings-metric-card.tsx` | settings-module | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/settings/settings-modal.tsx` | settings-page | none | aligned — no raw controls; no Astryx JSX usage | aligned | @@ -98,6 +98,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/settings/settings-skeleton.tsx` | settings-module | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/settings/settings-surface.tsx` | settings-module | Badge, Button, IconButton, Layout, LayoutContent, LayoutHeader, LayoutPanel, SideNav | aligned — uses Astryx (Badge, Button, IconButton, Layout, LayoutContent, LayoutHeader, LayoutPanel, SideNav) | aligned | | `apps/desktop/src/renderer/settings/subagent-settings-page.tsx` | settings-page | Badge, Banner, Button, EmptyState, HStack, IconButton, VStack | aligned — uses Astryx (Badge, Banner, Button, EmptyState, HStack, IconButton, VStack) | aligned | +| `apps/desktop/src/renderer/settings/tasks-settings-page.tsx` | settings-page | Button, EmptyState, HStack, List, ListItem, TextInput | aligned — uses Astryx (Button, EmptyState, HStack, List, ListItem, TextInput) | aligned | | `apps/desktop/src/renderer/settings/usage-settings-page.tsx` | settings-page | Banner, Button, Card, EmptyState, SegmentedControl, SegmentedControlItem, Tab, TabList | aligned — uses Astryx (Banner, Button, Card, EmptyState, SegmentedControl, SegmentedControlItem, Tab, TabList) | aligned | | `apps/desktop/src/renderer/settings/web-search-settings-page.tsx` | settings-page | Banner, Button, EmptyState | aligned — uses Astryx (Banner, Button, EmptyState) | aligned | | `apps/desktop/src/renderer/side-chat-close-confirmation.tsx` | shell-chrome-or-panel | Button, CheckboxInput, Dialog, DialogHeader, HStack, Layout, LayoutContent, Text, VStack | aligned — uses Astryx (Button, CheckboxInput, Dialog, DialogHeader, HStack, Layout, LayoutContent, Text) | aligned | @@ -114,7 +115,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/styles/daily-review.css` | module-hub | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/deep-research.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/error.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | -| `apps/desktop/src/renderer/styles/external-session-import.css` | shell-chrome-or-panel | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/help.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/hero.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/interaction-prompts.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | @@ -146,6 +146,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/styles/settings/provider-editor.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/route.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/rows.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | +| `apps/desktop/src/renderer/styles/settings/runtime-host.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/select.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/theme-preview.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/usage.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | @@ -180,9 +181,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `packages/ui/src/module-hub-selector.tsx` | ui-composition | Tab, TabList | aligned — uses Astryx (Tab, TabList) | aligned | | `packages/ui/src/module-pages.tsx` | ui-composition | EmptyState, Spinner | aligned — uses Astryx (EmptyState, Spinner) | aligned | | `packages/ui/src/permission-mode-menu.tsx` | ui-composition | Selector | aligned — uses Astryx (Selector) | aligned | -| `packages/ui/src/scheduled-task-form-dialog.tsx` | module-hub | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, Selector, Text, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, Selector, Text) | aligned | -| `packages/ui/src/scheduled-task-inspector.tsx` | module-hub | Button, Divider, HStack, Heading, List, ListItem, MetadataList, MetadataListItem, Switch, Text, VStack | aligned — uses Astryx (Button, Divider, HStack, Heading, List, ListItem, MetadataList, MetadataListItem) | aligned | -| `packages/ui/src/scheduled-task-panel.tsx` | module-hub | Button, Divider, EmptyState, List, ListItem, SegmentedControl, SegmentedControlItem, Selector, Text, TextInput, Toolbar | aligned — uses Astryx (Button, Divider, EmptyState, List, ListItem, SegmentedControl, SegmentedControlItem, Selector) | aligned | | `packages/ui/src/primitives/chat.tsx` | primitive | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/primitives/module-page.tsx` | primitive | Dialog, DialogHeader, HStack, Heading, Layout, LayoutContent, LayoutHeader, LayoutPanel, ResizeHandle, Text, VStack | aligned — uses Astryx (Dialog, DialogHeader, HStack, Heading, Layout, LayoutContent, LayoutHeader, LayoutPanel) | aligned | | `packages/ui/src/primitives/page-header.tsx` | primitive | none | aligned — no raw controls; no Astryx JSX usage | aligned | @@ -191,9 +189,12 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `packages/ui/src/quote-ref-chip.tsx` | ui-composition | Button, IconButton | aligned — uses Astryx (Button, IconButton) | aligned | | `packages/ui/src/relative-time.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/sandbox-boundary-prompt.tsx` | ui-composition | Button | aligned — uses Astryx (Button) | aligned | +| `packages/ui/src/scheduled-task-form-dialog.tsx` | module-hub | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, Selector, Text, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, Selector, Text) | aligned | +| `packages/ui/src/scheduled-task-inspector.tsx` | module-hub | Button, Divider, HStack, Heading, List, ListItem, MetadataList, MetadataListItem, Switch, Text, VStack | aligned — uses Astryx (Button, Divider, HStack, Heading, List, ListItem, MetadataList, MetadataListItem) | aligned | +| `packages/ui/src/scheduled-task-panel.tsx` | module-hub | Button, Divider, EmptyState, List, ListItem, SegmentedControl, SegmentedControlItem, Selector, Text, TextInput, Toolbar | aligned — uses Astryx (Button, Divider, EmptyState, List, ListItem, SegmentedControl, SegmentedControlItem, Selector) | aligned | | `packages/ui/src/search-modal.tsx` | dialog-overlay | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/session-context-layer.tsx` | shell-chrome-or-panel | BreadcrumbItem, Breadcrumbs, IconButton, LayoutHeader, Text, Token | aligned — uses Astryx (BreadcrumbItem, Breadcrumbs, IconButton, LayoutHeader, Text, Token) | aligned | -| `packages/ui/src/session-history-list.tsx` | shell-chrome-or-panel | Badge, IconButton, Tooltip, VStack | aligned — uses Astryx (Badge, IconButton, Tooltip, VStack) | aligned | +| `packages/ui/src/session-history-list.tsx` | shell-chrome-or-panel | Badge, VStack | aligned — uses Astryx (Badge, VStack) | aligned | | `packages/ui/src/session-list-panel.tsx` | shell-chrome-or-panel | SegmentedControl, SegmentedControlItem, SideNav | aligned — uses Astryx (SegmentedControl, SegmentedControlItem, SideNav) | aligned | | `packages/ui/src/session-rename-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, TextInput) | aligned | | `packages/ui/src/session-sidebar-nav.tsx` | shell-chrome-or-panel | IconButton, Tooltip | aligned — uses Astryx (IconButton, Tooltip) | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index af97470735..004fd5121e 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -59,6 +59,8 @@ apps/desktop/src/renderer/settings/provider-enabled-model-manager.tsx apps/desktop/src/renderer/settings/provider-oauth-section.tsx apps/desktop/src/renderer/settings/providers-panel.tsx apps/desktop/src/renderer/settings/request-customization-editor.tsx +apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx +apps/desktop/src/renderer/settings/runtime-host-ssh-terminal-dialog.tsx apps/desktop/src/renderer/settings/settings-expandable-row.tsx apps/desktop/src/renderer/settings/settings-metric-card.tsx apps/desktop/src/renderer/settings/settings-modal.tsx @@ -116,6 +118,7 @@ apps/desktop/src/renderer/styles/settings/permission.css apps/desktop/src/renderer/styles/settings/provider-editor.css apps/desktop/src/renderer/styles/settings/route.css apps/desktop/src/renderer/styles/settings/rows.css +apps/desktop/src/renderer/styles/settings/runtime-host.css apps/desktop/src/renderer/styles/settings/select.css apps/desktop/src/renderer/styles/settings/theme-preview.css apps/desktop/src/renderer/styles/settings/usage.css From 4b17102b89a335d073fd82d231daeeda6b8bd97d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 18:50:05 +0800 Subject: [PATCH 18/20] test(ui): show every task status in the story that exists to show them Two gaps, both found by shooting the story for a before/after comparison. `review` and `done` were dropped from `statusSessions` when this branch briefly deleted them from `SESSION_STATUSES`. The enum values and their labels came back; the fixture did not, so the story that covers every status covered six of eight -- and the two missing ones are exactly the two whose colours this change decided on purpose, `attention` and `success`. `StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to 260, so every story that did not pass a width was clipping the rail by 20px. That lands on the trailing slot, which is where this change puts the timestamp -- the stories could not show whether it fits. Stories that want a narrow rail still pass the width to both, which is what the note on `panelProps` is about. Refs #2984 Generated-by: Claude Code --- .../ui/stories/session-list-panel.stories.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/ui/stories/session-list-panel.stories.tsx b/packages/ui/stories/session-list-panel.stories.tsx index 096ba4de18..2a2d9d2c5f 100644 --- a/packages/ui/stories/session-list-panel.stories.tsx +++ b/packages/ui/stories/session-list-panel.stories.tsx @@ -121,7 +121,12 @@ function StoryFrame(props: { focusActiveRow?: boolean; openActiveRowMenu?: boolean; }) { - const { children, width = 240, height = 680, focusActiveRow = false, openActiveRowMenu = false } = props; + // 260 is `SessionListPanel`'s own default width. The frame used to default to + // 240 and clip the rail by 20px in every story that did not pass a width — + // which lands squarely on the trailing slot, so the stories could not show + // whether the timestamp fits. Stories that want a narrow rail pass the width + // to both, as `panelProps` explains. + const { children, width = 260, height = 680, focusActiveRow = false, openActiveRowMenu = false } = props; const ref = useRef(null); useEffect(() => { @@ -179,6 +184,22 @@ const statusSessions = [ blockedReason: 'auth', lastMessageAt: NOW - 20 * 60 * 1000, }), + // `review` and `done` have no writer in current source, but stored rows can + // still carry them (see SESSION_STATUSES) and the rail has to draw them. They + // are also the two colours this change decided on purpose — attention and + // success — so the story that shows every status has to show them. + makeSession({ + id: 'status-review', + name: '待审核的文件 diff', + status: 'review', + lastMessageAt: NOW - 37 * 60 * 1000, + }), + makeSession({ + id: 'status-done', + name: '已完成的 smoke run', + status: 'done', + lastMessageAt: NOW - 2 * 60 * 60 * 1000, + }), makeSession({ id: 'status-archived', name: '归档的旧实验', From 642d5243ff7cd24ec719392c50f6c29bd8233911 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 15 Aug 2026 19:51:31 +0800 Subject: [PATCH 19/20] =?UTF-8?q?refactor(ui):=20drop=20the=20rail's=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expanded, the row selected a section whose content was already on screen directly under it -- the same redundancy as the 会话 list heading this change deleted one row down, and clicking any task row does the same selection. It was kept for the collapsed rail, where the list is not rendered, on the argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong. Collapsed, the rail cannot switch tasks at all, so getting back to any task already means widening it; the titlebar's 展开侧边栏 toggle does that unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with no guard), and `activeId` survives a section change, so the widened rail comes back with the task you left already marked. The row bought one click in a state the user is leaving regardless, and charged a permanent slot for it. `sessions` now has no control of its own on the rail. It is where you are unless you went somewhere, which is why the other two sections light up and this one has nothing to light. `streaming-remount` returns the way the product now offers -- widen, then click the row carrying `aria-current="page"` -- which is a better assertion than the old one anyway: it checks that the task survives the trip, not just that a button exists. Refs #2984 Generated-by: Claude Code --- apps/desktop/e2e/streaming-remount.spec.ts | 13 +++++++-- packages/ui/src/nav-selection.ts | 14 ++++++--- packages/ui/src/session-sidebar-nav.tsx | 34 +++++++--------------- packages/ui/src/shell-controls-copy.ts | 3 -- 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/apps/desktop/e2e/streaming-remount.spec.ts b/apps/desktop/e2e/streaming-remount.spec.ts index 1e7ff542a1..72d6d2aa3b 100644 --- a/apps/desktop/e2e/streaming-remount.spec.ts +++ b/apps/desktop/e2e/streaming-remount.spec.ts @@ -22,9 +22,16 @@ test('remounting a live surface leaves accumulated output settled', async ({ await sidebar.getByRole('button', { name: '扩展' }).click(); await expect(page.locator('[data-module="skills"]')).toBeVisible(); await expect(liveBubble).toHaveCount(0); - // Back through the rail's 任务 row. The rail is collapsed here, so the task - // rows are not rendered and this section row is the only way back (#2984). - await sidebar.getByRole('button', { name: '任务', exact: true }).click(); + // Back the way the product actually offers: the rail is collapsed here, so + // the task rows are not rendered and there is no 任务 row to press (#2984). + // Widening it is the titlebar's job, and the task left behind is still + // `activeId`, so it comes back marked and one click away. + await page.getByRole('button', { name: '展开侧边栏' }).click(); + const currentTaskRow = sidebar.locator( + '[data-maka-contract="session-row"] [aria-current="page"]', + ); + await expect(currentTaskRow).toHaveCount(1); + await currentTaskRow.click(); await expect(liveBubble).toHaveCount(1); await expect(liveBubble).toContainText(accumulatedOutput); diff --git a/packages/ui/src/nav-selection.ts b/packages/ui/src/nav-selection.ts index f9442cf8a1..0642f614f1 100644 --- a/packages/ui/src/nav-selection.ts +++ b/packages/ui/src/nav-selection.ts @@ -7,10 +7,16 @@ * selected it, so the branch that filtered on it could not run. What was left * was a one-value filter: a control whose answer is always the same answer. * - * The rail's 任务 row survives that deletion. It carried the dead filter, but - * its job was to select this section — it is how you get back here from - * 扩展 or 定时任务, and collapsed at 48px it is the ONLY way, because the list - * itself is not rendered there. + * The rail's 任务 row went with it, and nothing replaced it. Selecting this + * section is what clicking a task row already does, so expanded the row sat + * directly above the list that is its own destination. Collapsed the list is + * not rendered — but the rail cannot switch tasks there at all, so coming back + * from 扩展 means widening the rail either way. `activeId` is untouched by a + * section change, so the task you left is still marked when it does. + * + * `sessions` therefore has no control of its own on the rail. It is where you + * are unless you went somewhere, which is why the other two sections light up + * and this one has nothing to light. */ export type ExtensionModule = 'skills' | 'mcp'; export type AutomationModule = 'scheduled-tasks' | 'daily-review'; diff --git a/packages/ui/src/session-sidebar-nav.tsx b/packages/ui/src/session-sidebar-nav.tsx index 0de8e43b33..ed85247e0a 100644 --- a/packages/ui/src/session-sidebar-nav.tsx +++ b/packages/ui/src/session-sidebar-nav.tsx @@ -1,13 +1,5 @@ import type { ScheduledTask } from '@maka/core/scheduled-task'; -import { - AlertCircle, - Blocks, - Download, - MessageSquare, - Settings, - SquarePen, - Timer, -} from './icons.js'; +import { AlertCircle, Blocks, Download, Settings, SquarePen, Timer } from './icons.js'; import type { NavModuleMemory, NavSelection } from './nav-selection.js'; import { useUiLocale } from './locale-context.js'; import { getShellControlsCopy } from './shell-controls-copy.js'; @@ -25,7 +17,6 @@ export function SessionSidebarNav(props: { }) { const locale = useUiLocale(); const copy = getShellControlsCopy(locale).navigation; - const sessionsActive = props.selection.section === 'sessions'; const extensionsActive = props.selection.section === 'extensions'; const automationsActive = props.selection.section === 'automations'; const moduleMemory = props.moduleMemory ?? { extensions: 'skills', automations: 'scheduled-tasks' }; @@ -53,19 +44,16 @@ export function SessionSidebarNav(props: { onClick={props.onNew} endContent={} /> - {/* The way back to the list. Selecting a task row does it too, but only - while the rail is expanded — collapsed, the list is not rendered, so - without this row 扩展 and 定时任务 are one-way doors and the only exit - is 新任务, which answers "show me my tasks" by creating another one. - MessageSquare is the glyph the command palette already draws for a - session (command-palette-commands.ts). */} - props.onSelect({ section: 'sessions' })} - /> + {/* No 任务 row. Expanded, the list below IS that row's destination, and a + control that selects what is already on screen under it is the same + redundancy as the 会话 list heading this change deleted one row down. + Collapsed, the list is not rendered — but the rail cannot switch tasks + there either, so returning from 扩展 already means widening the rail, + which the titlebar's 展开侧边栏 toggle does unconditionally + (app-shell-chrome-actions.tsx) and which lands on a list where the + task you left is still `activeId` and still marked. Adding a row to + save that one click would be paying a permanent slot for a state the + user is leaving anyway. */} Date: Sat, 15 Aug 2026 20:03:08 +0800 Subject: [PATCH 20/20] fix(runtime-host): coalesce a repeat import while the first is still running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving 导入任务 from a dialog into a Settings page removed the only thing stopping one intent from becoming two tasks, and this branch is where that happened. `ExternalSessionImportLifecycle` refused to close the dialog while an import was active; retiring it was described here as dropping a guard against nothing. It was not. The page it was replaced by is one the user may leave mid-import -- the import continues in Desktop Main by design -- and the page's `importingId` dies with it, so coming back and pressing 导入 again issues a second request against a source that is still importing. Nothing downstream deduplicates: `treats repeats as independent copies` is a pinned behaviour, so the second request lands a second task the user now has to tell apart. The guard belongs to the Host, not to the page that asked. Import is a Host operation and the Host is the only party that knows one is running; a client knows about its own requests, which is why a second window or the CLI would have reproduced this with the page's state intact. Concurrent repeats collapse onto the first attempt's promise and both callers get its outcome, success or failure, because it is one operation. Sequential repeats are untouched -- the entry is gone by the time the first settles -- so importing the same conversation again on purpose still makes an independent copy, which the existing test continues to pin. Reported by @M4n5ter in review of #3033. Refs #2984 Generated-by: Claude Code --- .../external-session-coordinator.test.ts | 42 +++++++++++++++++++ .../server/external-session-coordinator.ts | 38 +++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts b/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts index 93604e80f2..6fb46bd51d 100644 --- a/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts @@ -128,6 +128,48 @@ test('imports through the generic importer and treats repeats as independent cop assert.equal(fixture.drainRequests(), 0); }); +test('coalesces a repeat import issued while the first is still running', async () => { + // The surface that asks cannot enforce this. 导入任务 is a Settings page the + // user is free to leave mid-import — the import deliberately continues here — + // and the page's in-flight state dies with it, so coming back and pressing + // 导入 again used to land a second task for one intent. A second window or + // the CLI would have done the same. Nothing is awaited between the two calls + // below, which is exactly that: two requests for one source, both live. + const fixture = coordinatorFixture([adapterFixture()]); + + const [first, second] = await Promise.all([ + fixture.coordinator.handlers['external-session.import']( + { adapterId: 'codex', sourceSessionId: 'source-0' }, + context, + ), + fixture.coordinator.handlers['external-session.import']( + { adapterId: 'codex', sourceSessionId: 'source-0' }, + context, + ), + ]); + + assert.equal(first.ok, true); + assert.equal(second.ok, true); + if (!first.ok || !second.ok) assert.fail('Expected the coalesced import to commit'); + // Same task, and only one of them was ever created. Both callers are told + // about it, so the one that clicked twice still gets taken to the result. + assert.equal(first.result.session.id, second.result.session.id); + assert.equal(fixture.creates.length, 1); + assert.equal(fixture.drainRequests(), 0); + + // Only concurrent repeats collapse. Once the first has settled the source is + // importable again, which is the deliberate second-copy behaviour pinned by + // the test above. + const later = await fixture.coordinator.handlers['external-session.import']( + { adapterId: 'codex', sourceSessionId: 'source-0' }, + context, + ); + assert.equal(later.ok, true); + if (!later.ok) assert.fail('Expected a later repeat to commit its own copy'); + assert.notEqual(later.result.session.id, first.result.session.id); + assert.equal(fixture.creates.length, 2); +}); + test('reports conversion errors before persistence and store uncertainty after entry', async () => { let createAttempts = 0; const conversionFailure = coordinatorFixture( diff --git a/packages/runtime-host/src/server/external-session-coordinator.ts b/packages/runtime-host/src/server/external-session-coordinator.ts index 78a848c24f..c42eb3939d 100644 --- a/packages/runtime-host/src/server/external-session-coordinator.ts +++ b/packages/runtime-host/src/server/external-session-coordinator.ts @@ -65,6 +65,29 @@ export class HostExternalSessionCoordinator { readonly #discardImportedSession: HostExternalSessionCoordinatorOptions['discardImportedSession']; readonly #requestDrain: () => void; + /** + * One import per source at a time, keyed by adapter + source session. + * + * A repeat import is a legitimate request — it makes an independent copy, and + * a test below pins that. A repeat while the first is still running is not: + * it is one intent counted twice, and it lands two tasks the user has to tell + * apart and clean up. + * + * The guard has to be here rather than on the surface that asked. Import is a + * Host operation and the Host is what knows one is running; a client only + * knows about its own. The Settings page that replaced the import dialog can + * be unmounted mid-import by design, and its in-flight state goes with it — + * as would a second window's, or the CLI's. + * + * Coalesced, not rejected: the second caller gets the first one's outcome, + * success or failure, because it is the same operation. Entries are keyed on + * a JSON pair so no separator can be forged out of the ids themselves. + */ + readonly #importsInFlight = new Map< + string, + Promise> + >(); + constructor(options: HostExternalSessionCoordinatorOptions) { this.#adapters = options.adapters; this.#admission = options.admission; @@ -147,6 +170,21 @@ export class HostExternalSessionCoordinator { async importSession( input: ExternalSessionImportInput, + ): Promise> { + const key = JSON.stringify([input.adapterId, input.sourceSessionId]); + const running = this.#importsInFlight.get(key); + if (running) return running; + const attempt = this.#importSession(input); + this.#importsInFlight.set(key, attempt); + try { + return await attempt; + } finally { + this.#importsInFlight.delete(key); + } + } + + async #importSession( + input: ExternalSessionImportInput, ): Promise> { const adapter = this.#adapters.get(input.adapterId); if (!adapter) return importFailure('invalid_request', 'External Session source is unsupported');