From 56d2c717b32c778c5ec01977ff788444316ff8ab Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 22:54:36 +0800 Subject: [PATCH] fix(desktop): rebuild the workbar shell on TabList and one open/close control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The right workbar drew its own tab strip: a hand-written `role="tablist"` div of ghost `Button`s with a close `IconButton` beside each, dnd-kit drag reorder, a context menu of move/close verbs, and a preview/pin state no renderer code ever set. It read as a browser tab bar without being one, and the strip's container had no `min-width: 0`, so at the panel's floor the tabs spilled past the edge and pushed [+] and the collapse toggle off screen instead of scrolling. The strip is now Astryx's `TabList`. That settles what the hand-written one could not decide: - `Tab` renders `endContent` inside its own ` - - } - variant="ghost" - size="sm" - className="maka-workbar-tab-close" - onClick={() => props.onClose(props.tab)} - /> - - - - ); -} - function WorkbarLauncher(props: { onOpen: (kind: SessionWorkbarTabKind) => void; sideChatAvailable: boolean; }) { const copy = getDesktopConversationCopy(useUiLocale()).workbar; - const actions: Array<{ - kind: SessionWorkbarTabKind; - label: string; - description: string; - icon: typeof Activity; - shortcut?: string; - disabled?: boolean; - }> = [ - { - kind: 'side-chat', - label: copy.sideChat, - description: copy.launcher.sideChat, - icon: MessageCircleQuestion, - shortcut: 'mod+alt+s', - disabled: !props.sideChatAvailable, - }, - { - kind: 'review', - label: copy.review, - description: copy.launcher.review, - icon: GitBranch, - shortcut: 'ctrl+shift+g', - }, - { - kind: 'terminal', - label: copy.terminal, - description: copy.launcher.terminal, - icon: Terminal, - shortcut: 'ctrl+`', - }, - { - kind: 'browser', - label: copy.browser, - description: copy.launcher.browser, - icon: Globe, - shortcut: 'mod+t', - }, - { - kind: 'files', - label: copy.files, - description: copy.launcher.files, - icon: FolderOpen, - shortcut: 'mod+p', - }, - { - kind: 'tasks', - label: copy.tasks, - description: copy.launcher.tasks, - icon: ListTodo, - }, - { - kind: 'work-board', - label: copy.workBoard, - description: copy.launcher.workBoard, - icon: Clipboard, - }, - { - kind: 'inspector', - label: copy.inspector, - description: copy.launcher.inspector, - icon: Activity, - }, - ]; + // The list is the tool registry, in registry order — icons and shortcuts + // included. This is the one place a face's shortcut is shown, so it is also + // where the shortcuts are learned. return (
@@ -628,19 +344,19 @@ function WorkbarLauncher(props: { density="compact" header={{copy.openTools}} > - {actions.map((action) => ( + {WORKBAR_TOOL_DEFINITIONS.map((definition) => ( } - label={action.label} - description={action.description} + key={definition.kind} + startContent={ + + } + label={faceLabel(definition.kind, copy)} + description={copy.launcher[launcherCopyKey(definition.kind)]} endContent={ - action.shortcut ? ( - - ) : undefined + definition.shortcut ? : undefined } - isDisabled={action.disabled} - onClick={() => props.onOpen(action.kind)} + isDisabled={definition.kind === 'side-chat' && !props.sideChatAvailable} + onClick={() => props.onOpen(definition.kind)} /> ))} @@ -649,6 +365,16 @@ function WorkbarLauncher(props: { ); } +function launcherCopyKey( + kind: SessionWorkbarTabKind, +): keyof WorkbarCopy['launcher'] { + return kind === 'side-chat' + ? 'sideChat' + : kind === 'work-board' + ? 'workBoard' + : kind; +} + export function WorkbarSurface(props: { sessionId: string; projectId?: string | null; @@ -664,18 +390,6 @@ export function WorkbarSurface(props: { placement: SessionWorkbarPlacement, tabs: readonly SessionWorkbarTab[], ) => void; - onReorderTab: ( - placement: SessionWorkbarPlacement, - tabId: string, - targetTabId: string, - ) => void; - onMoveTab: ( - placement: SessionWorkbarPlacement, - tabId: string, - direction: 'left' | 'right', - ) => void; - onMoveTabToPanel: (tabId: string, target: SessionWorkbarPlacement) => void; - onPinTab: (tabId: string) => void; onOpenLauncher: (placement: SessionWorkbarPlacement) => void; onRequestOpenTab: ( placement: SessionWorkbarPlacement, @@ -696,11 +410,6 @@ export function WorkbarSurface(props: { }) { const locale = useUiLocale(); const copy = getDesktopConversationCopy(locale).workbar; - const sessionTodo = useSessionTodo(props.sessionId, { - locale, - loadFailed: copy.todoLoadFailed, - }); - const taskCount = sessionTodoActiveCount(sessionTodo.items); const [artifactCount, setArtifactCount] = useState(0); const placements: SessionWorkbarPlacement[] = ['right', 'bottom']; const positionedTabs = placements.flatMap((placement) => @@ -738,21 +447,17 @@ export function WorkbarSurface(props: { tabs={panel.tabs} activeTabId={showingLauncher ? null : panel.activeTabId} activeSideChatPanelIds={props.activeSideChatPanelIds} - taskCount={taskCount} artifactCount={artifactCount} + sideChatAvailable={props.sourceSession !== undefined} onActivate={(tabId) => props.onActivateTab(placement, tabId)} - onClose={(tab) => props.onCloseTab(placement, tab)} - onCloseTabs={(tabs) => props.onCloseTabs(placement, tabs)} - onReorder={(tabId, targetTabId) => - props.onReorderTab(placement, tabId, targetTabId) - } - onMove={(tabId, direction) => - props.onMoveTab(placement, tabId, direction) + onOpenKind={(kind) => props.onRequestOpenTab(placement, kind)} + onCloseKind={(kind) => + props.onCloseTabs( + placement, + panel.tabs.filter((tab) => tab.kind === kind), + ) } - onMoveToPanel={props.onMoveTabToPanel} - onPin={props.onPinTab} placement={placement} - onOpenLauncher={() => props.onOpenLauncher(placement)} onCollapseRightPanel={ placement === 'right' ? () => props.onDismissPanel('right') @@ -798,15 +503,6 @@ export function WorkbarSurface(props: { /> ); - } else if (tab.kind === 'tasks') { - content = ( - - ); } else if (tab.kind === 'work-board') { content = ( props.onPinTab(tab.id)} className={ tab.kind === 'side-chat' ? 'maka-quote-workbar-panel' : undefined } diff --git a/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx b/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx index 9ca749119f..55e858bf9a 100644 --- a/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx +++ b/apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx @@ -23,9 +23,17 @@ import { IconButton, useUiLocale } from '@maka/ui'; import { PanelRightClose, PanelRightOpen } from '@maka/ui/icons'; import { getShellCopy } from '../../../locales/shell-copy'; -/** Shared titlebar/panel toggle for the Workbar column. */ +/** + * Shared titlebar/panel toggle for the Workbar column. + * + * `md` is the titlebar rail's size, shared with the sidebar and search + * actions it stands beside. In the workbar's own bar it stands beside the + * strip's `sm` tabs and the `sm` `[+]` instead, so that caller passes `sm` — + * three controls in one row have to report one height. + */ export function WorkbarToggle(props: { collapsed: boolean; + size?: 'sm' | 'md'; className?: string; onToggle(): void; }) { @@ -43,7 +51,7 @@ export function WorkbarToggle(props: { /> )} variant="ghost" - size="md" + size={props.size ?? 'md'} className={ props.className ? `maka-titlebar-action ${props.className}` @@ -71,7 +79,9 @@ export function WorkbarTitlebarActions(props: { role="toolbar" aria-label={copy.workspaceActions} > - + {/* `sm`, like the toggle in the workbar's own bar: this is that control, + standing where it stood, so collapsing must not resize it. */} +
); } diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index b4fef1cfa2..4b6280b0bd 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -96,8 +96,6 @@ export interface DesktopConversationCopy { review: string; terminal: string; terminalNumbered(index: number): string; - tasks: string; - todoLoadFailed: string; workBoard: string; browser: string; files: string; @@ -106,21 +104,9 @@ export interface DesktopConversationCopy { sideChatNumbered(index: number): string; openTab: string; openTools: string; - closeTab(label: string): string; - tabMenu(label: string): string; - moveLeft: string; - moveRight: string; - moveToRight: string; - moveToBottom: string; - pinTab: string; - pinTabHint: string; - close: string; - closeOthers: string; - closeToRight: string; launcher: { review: string; terminal: string; - tasks: string; workBoard: string; browser: string; files: string; @@ -486,31 +472,17 @@ const COPY = { review: '变更', terminal: '终端', terminalNumbered: (index) => `终端 ${index}`, - tasks: '待办', - todoLoadFailed: '待办载入失败,请重试。', workBoard: '工作看板', browser: '浏览器', files: '生成文件', inspector: '追踪', sideChat: '侧边对话', sideChatNumbered: (index) => `侧边对话 ${index}`, - openTab: '打开工作栏标签', + openTab: '打开或关闭工作栏的面', openTools: '打开工具', - closeTab: (label) => `关闭${label}`, - tabMenu: (label) => `${label}标签菜单`, - moveLeft: '向左移动', - moveRight: '向右移动', - moveToRight: '移动到右侧面板', - moveToBottom: '移动到底部面板', - pinTab: '固定标签', - pinTabHint: '预览标签,双击或在内容中操作即可固定', - close: '关闭', - closeOthers: '关闭其他标签', - closeToRight: '关闭右侧标签', launcher: { review: '查看当前 Git 工作区变化', terminal: '查看当前任务的终端运行和实时输出', - tasks: '查看和维护这个任务的待办台账', workBoard: '记录和管理暂缓事项', browser: '打开内置浏览器并保留当前页面', files: '浏览当前任务生成的文件', @@ -740,31 +712,17 @@ const COPY = { review: '變更', terminal: '終端', terminalNumbered: (index) => `終端 ${index}`, - tasks: '待辦', - todoLoadFailed: '待辦載入失敗,請重試。', workBoard: '工作看板', browser: '瀏覽器', files: '生成檔案', inspector: '追蹤', sideChat: '側邊對話', sideChatNumbered: (index) => `側邊對話 ${index}`, - openTab: '開啟工作欄標籤', + openTab: '開啟或關閉工作欄的面', openTools: '開啟工具', - closeTab: (label) => `關閉${label}`, - tabMenu: (label) => `${label}標籤選單`, - moveLeft: '向左移動', - moveRight: '向右移動', - moveToRight: '移動到右側面板', - moveToBottom: '移動到底部面板', - pinTab: '固定標籤', - pinTabHint: '預覽標籤,雙擊或在內容中操作即可固定', - close: '關閉', - closeOthers: '關閉其他標籤', - closeToRight: '關閉右側標籤', launcher: { review: '檢視目前 Git 工作區變化', terminal: '檢視目前任務的終端執行和即時輸出', - tasks: '檢視和維護這個任務的待辦臺賬', workBoard: '記錄和管理暫緩事項', browser: '開啟內建瀏覽器並保留目前頁面', files: '瀏覽目前任務生成的檔案', @@ -985,31 +943,17 @@ const COPY = { review: 'Changes', terminal: 'Terminal', terminalNumbered: (index) => `Terminal ${index}`, - tasks: 'To-do', - todoLoadFailed: 'Failed to load the to-do list. Try again.', workBoard: 'Work board', browser: 'Browser', files: 'Generated files', inspector: 'Trace', sideChat: 'Side chat', sideChatNumbered: (index) => `Side chat ${index}`, - openTab: 'Open workbar tab', + openTab: 'Open or close a workbar face', openTools: 'Open tools', - closeTab: (label) => `Close ${label}`, - tabMenu: (label) => `${label} tab menu`, - moveLeft: 'Move left', - moveRight: 'Move right', - moveToRight: 'Move to right panel', - moveToBottom: 'Move to bottom panel', - pinTab: 'Pin tab', - pinTabHint: 'Preview tab. Double-click or interact with its content to pin it', - close: 'Close', - closeOthers: 'Close other tabs', - closeToRight: 'Close tabs to the right', launcher: { review: 'View changes in the current Git workspace', terminal: 'Inspect terminal runs and live output for this task', - tasks: "View and maintain this task's to-do ledger", workBoard: 'Capture and manage deferred work', browser: 'Open the embedded browser and keep the current page', files: 'Browse files generated by this task', diff --git a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts index 9e6ddbf21f..8313458a27 100644 --- a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts +++ b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts @@ -31,7 +31,6 @@ export type DesktopWorkbarBridge = Pick< | 'inspector' | 'sessions' | 'shellRuns' - | 'todo' | 'transcripts' >; @@ -55,7 +54,6 @@ export function createDesktopWorkbarServices( bridge.sessions.subscribeEvents(sessionId, handler), }, terminal: bridge.shellRuns, - todo: bridge.todo, browser: { setActiveSession: (sessionId) => bridge.browser.setActiveSession(sessionId), setViewport: (input) => bridge.browser.setViewport(input), diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 02e5062d64..d07f44133b 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -47,7 +47,6 @@ @import "./styles/settings/select.css" layer(components); @import "./styles/model-switcher.css" layer(components); @import "./styles/chat-header.css" layer(components); -@import "./styles/session-todo-panel.css" layer(components); @import "./styles/work-board.css" layer(components); @import "./styles/plan-mode.css" layer(components); @import "./styles/agent-graph.css" layer(components); diff --git a/apps/desktop/src/renderer/styles/session-todo-panel.css b/apps/desktop/src/renderer/styles/session-todo-panel.css deleted file mode 100644 index f4df8be5ac..0000000000 --- a/apps/desktop/src/renderer/styles/session-todo-panel.css +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* SessionTodoPanel (#4351) — the read-only, flat projection of the Host-owned - current Todo document. It replaced the nested Task Ledger demand chain, so - this sheet was reduced (#4394) to what the flat list actually renders: a - `-panel` scroll container, a `-tree`
    , and one `-row`
  1. per item that - holds a status icon and its content text. The retired four-column, - depth-indented tree row — its `--task-depth` indent, `-group`, - `[data-status]`, `-key`/`-subject`/`-meta`/`-detail`, and `-terminal` - descendants — is gone; none of those elements exist anymore. */ - -.maka-session-todo-panel { - height: 100%; - min-height: 0; - overflow-y: auto; - padding: var(--space-2); -} - -.maka-session-todo-tree { - display: grid; - gap: var(--border-width-hairline); -} - -/* Two children only: the status icon, then content that takes the remaining - width and wraps within it (`minmax(0, 1fr)` lets the text column shrink so - long, unbroken content wraps instead of overflowing the workbar). The - workbar resizes down to SESSION_WORKBAR_MIN_WIDTH (320px) inside an - otherwise wide window, so wrapping — not a width breakpoint — is what keeps - the row readable at every width. If width-responsive styling is ever added - here it should be an `@container` query on `.maka-session-todo-panel` (see - workbar/inspector.css and chat-header.css), not a viewport `@media`. */ -.maka-session-todo-row { - font: var(--maka-text-supporting); - display: grid; - grid-template-columns: auto minmax(0, 1fr); - align-items: center; - gap: var(--space-2); - min-height: 32px; - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-control); - color: var(--muted-foreground); -} - -.maka-session-todo-row > span { - min-width: 0; - overflow-wrap: anywhere; -} - -.maka-session-todo-row:hover { background: var(--state-hover-bg); } - -.maka-session-todo-message { - font: var(--maka-text-supporting); - display: flex; - align-items: center; - justify-content: space-between; - min-height: 32px; - padding: var(--space-1) var(--space-2); - color: var(--muted-foreground); -} - -.maka-session-todo-retry { - display: inline-grid; - width: 28px; - height: 28px; - place-items: center; - border: 0; - border-radius: var(--radius-control); - background: transparent; - color: inherit; -} - -.maka-session-todo-retry:hover { background: var(--state-hover-bg); } - -.maka-session-todo-retry:focus-visible { - outline: var(--focus-ring-width) solid var(--focus-ring); - outline-offset: 1px; -} diff --git a/apps/desktop/src/renderer/styles/workbar/shell.css b/apps/desktop/src/renderer/styles/workbar/shell.css index 444ad1567d..f62b09e407 100644 --- a/apps/desktop/src/renderer/styles/workbar/shell.css +++ b/apps/desktop/src/renderer/styles/workbar/shell.css @@ -128,6 +128,33 @@ align-items: center; min-width: 0; box-sizing: border-box; + /* The rail belongs to the bar, not to the tab strip. `TabList hasDivider` + draws it on the strip's own box, which ends where the tabs do, so it ran + out from under `[+]` and the collapse toggle and read as a line beneath + half a row. This is the second idiom Astryx's `Tab` names for the rail + ("TabList `hasDivider` or a Toolbar with a bottom divider"): the bar + carries it, and hands the tabs `--_tab-indicator-bottom` so the selected + indicator drops through the bar's own gap and lands on it. + + It is an `::after` rather than a border because the bar's height is the + titlebar clearance it stands in; a border would take a pixel out of that + and leave the 28px controls centred on a half pixel. Each placement + declares `--maka-workbar-rail-gap`: the distance from a control's bottom + edge down to the rail, which is what the indicator has to travel. Astryx's + own Toolbar adds a border width on top of that, because its rail is a + border and so sits outside the box the padding measures to; ours is inside + it, so the gap alone lands the indicator's bottom edge on the rail. */ + --_tab-indicator-bottom: calc(-1 * var(--maka-workbar-rail-gap)); +} + +.maka-session-workbar-toolbar::after { + content: ""; + position: absolute; + inset-inline: 0; + inset-block-end: 0; + block-size: var(--border-width); + background: var(--color-border); + pointer-events: none; } .maka-session-workbar[data-placement="right"] > .maka-session-workbar-toolbar { @@ -136,6 +163,17 @@ above that hit-test surface while its empty space remains draggable. */ z-index: calc(var(--z-titlebar) + 1); height: var(--maka-plate-titlebar-clearance); + /* The controls are centred in the bar, so half of what the clearance leaves + over an `sm` control sits under them. */ + --maka-workbar-rail-gap: calc( + (var(--maka-plate-titlebar-clearance) - var(--size-element-sm)) / 2 + ); + /* The right pad is the window titlebar strip's own gutter, not this bar's: + the collapse toggle is one control that moves between the two bands, and + `session-workbar.spec.ts` holds it to the same x in both. Narrowing this to + the bar's own gutter would slide it 16px on every collapse. On top of it, + the caption buttons where the platform draws them on the right (Windows); + macOS puts them on the left, over the sidebar, and reports 0 here. */ padding-inline: var(--space-2) calc(var(--space-6) + var(--maka-titlebar-overlay-right-width)); -webkit-app-region: drag; @@ -149,6 +187,8 @@ .maka-session-workbar[data-placement="bottom"] > .maka-session-workbar-toolbar { padding: var(--space-2) var(--space-4); + /* Height is content here, so the gap under a control is the block padding. */ + --maka-workbar-rail-gap: var(--space-2); } .maka-session-workbar > .maka-lazy-fallback { @@ -158,131 +198,28 @@ .maka-workbar-tab-strip { display: flex; align-items: center; + gap: var(--space-1); width: 100%; min-width: 0; flex: 0 0 auto; + /* The bar's height comes from the titlebar clearance it stands in, never from + the strip: letting the strip set it would move the whole panel the moment + its content changed height. */ + height: 100%; } +/* A flex item defaults to `min-width: auto`, so without this reset the strip + refuses to shrink: it spills past the panel and pushes [+] and the collapse + toggle off the edge instead of scrolling inside itself. */ .maka-workbar-tab-list { - display: flex; - align-items: center; flex: 1 1 auto; - gap: var(--space-1); min-width: 0; - overflow-x: auto; - overscroll-behavior-x: contain; - scroll-padding-inline: var(--space-1); - scrollbar-width: none; -} - -.maka-workbar-tab-list::-webkit-scrollbar { - display: none; -} - -.maka-workbar-tab-context { - flex: 0 0 auto; - min-width: 0; -} - -.maka-workbar-tab { - position: relative; - display: inline-flex; - align-items: center; - flex: 0 0 auto; - min-width: 0; - height: var(--size-element-sm); - border-radius: var(--radius-element); - color: var(--muted-foreground); - touch-action: none; - user-select: none; - transition: - background var(--duration-quick) var(--ease-out-strong), - color var(--duration-quick) var(--ease-out-strong); -} - -.maka-workbar-tab:hover, -.maka-workbar-tab[data-active] { - background: var(--state-hover-bg); - color: var(--foreground); -} - -.maka-workbar-tab[data-active]::after { - position: absolute; - right: var(--space-2); - bottom: -1px; - left: var(--space-2); - height: 2px; - border-radius: var(--radius-full); - background: var(--color-accent); - content: ""; -} - -.maka-workbar-tab[data-dragging] { - z-index: 2; - opacity: 0.72; - box-shadow: var(--shadow-low); -} - -.maka-workbar-tab-icon { - width: var(--icon-control); - height: var(--icon-control); - flex: 0 0 auto; } .maka-workbar-tab-spinner { animation: maka-spin 1s linear infinite; } -.maka-workbar-tab-select { - display: inline-flex; - align-items: center; - gap: var(--space-1); - min-width: 0; - min-height: 0; - height: 100%; - padding: 0 var(--space-1) 0 var(--space-3); - border: 0; - border-radius: 0; - background: transparent; - color: inherit; - cursor: pointer; - font: var(--maka-text-supporting); - box-shadow: none; -} - -.maka-workbar-tab[data-active] .maka-workbar-tab-select { - font-weight: var(--font-weight-semibold); -} - -.maka-workbar-tab-select:focus-visible { - border-radius: var(--radius-element); - outline: var(--focus-ring-width) solid var(--focus-ring); - outline-offset: calc(-1 * var(--focus-ring-width)); -} - -.maka-workbar-tab[data-preview] .maka-workbar-tab-label { - font-style: italic; -} - -.maka-workbar-tab-label { - overflow: hidden; - max-width: 112px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.maka-workbar-tab-close { - position: relative; - z-index: 1; - flex: 0 0 auto; - margin-right: 1px; -} - -.maka-workbar-new-tab { - flex: 0 0 auto; - margin-left: var(--space-1); -} - .maka-workbar-panel-toggle { flex: 0 0 auto; margin-left: var(--space-1); diff --git a/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx b/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx index 3bd8066b29..ffc98ef4ed 100644 --- a/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx +++ b/apps/desktop/stories/accessibility-runtime-surfaces.stories.tsx @@ -95,10 +95,6 @@ function WorkbarToolSurface(props: { kind: 'terminal' | 'browser' | 'files' }) { onActivateTab={noop} onCloseTab={noop} onCloseTabs={noop} - onReorderTab={noop} - onMoveTab={noop} - onMoveTabToPanel={noop} - onPinTab={noop} onOpenLauncher={noop} onRequestOpenTab={noop} confirmBypass={async () => true} diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index 71329da3c1..f12f2f7989 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -24,7 +24,6 @@ import type { ArtifactRecord } from '@maka/core/artifacts'; import type { BrowserState } from '@maka/core/browser'; import type { GitReviewReadResult, GitReviewSnapshot } from '@maka/core/git-review'; import type { SessionSummary } from '@maka/core/session'; -import type { SessionTodoItem } from '@maka/core/session-todo'; import type { SessionTrace } from '@maka/core/session-trace'; import type { ContextDiagnosticsResult } from '@maka/runtime-host/protocol'; import { ToastProvider } from '@maka/ui'; @@ -33,6 +32,7 @@ import { WorkbarSurface } from '../src/renderer/features/workbar/stories'; import { createFakeWorkbarServices, createSessionWorkbarPanelsState, + activateSessionWorkbarTab, createSessionWorkbarTabsState, openStaticSessionWorkbarTab, terminalSessionWorkbarTabId, @@ -147,21 +147,6 @@ const RICH_TERMINAL_BUFFER = [ // ---- ledgers ------------------------------------------------------------- -// The long item is deliberate: it is what proves a long subject still wraps -// instead of pushing the panel sideways. -const tasks: SessionTodoItem[] = [ - { content: '完成会话任务台账升级', status: 'in_progress' }, - { content: '验证 SQLite authority 与并发短 key 分配', status: 'completed' }, - { content: '检查窄窗口下的任务树布局', status: 'pending' }, - { - content: - '核对深层缩进、超长任务描述、owner 与阻塞原因在窄窗口中仍可完整换行且不遮挡后续内容', - status: 'pending', - }, - { content: '同步生命周期文档与边界说明', status: 'pending' }, - { content: '验证 Goal 一次提醒门禁', status: 'completed' }, -]; - const artifacts: ArtifactRecord[] = [ { id: 'artifact-patch', @@ -776,8 +761,6 @@ const unsubscribe = () => () => undefined; * varies, and everything else stays on the populated default. */ function bridge(options: { - tasks?: SessionTodoItem[]; - tasksFail?: boolean; trace?: SessionTrace; traceNextCursor?: string; traceFail?: boolean; @@ -799,13 +782,6 @@ function bridge(options: { } = {}): Decorator { const browserState = options.browserState ?? EMPTY_BROWSER_STATE; const services = createFakeWorkbarServices({ - todo: { - read: async () => { - if (options.tasksFail) throw new Error('读取任务失败'); - return options.tasks ?? tasks; - }, - subscribeChanges: unsubscribe, - }, artifacts: { list: async () => artifacts, readText: async (_sessionId: string, id: string) => ({ ok: true, text: artifactText[id] ?? '' }), @@ -950,6 +926,8 @@ function bridge(options: { */ function Workbar(props: { tab?: SessionWorkbarTabKind; + /** Extra faces opened after `tab`, so the strip can be seen with several. */ + alsoOpen?: readonly Exclude[]; sourceSession?: SessionSummary; /** Overrides the restored column width, the way the resize handle does. */ width?: number; @@ -979,11 +957,22 @@ function Workbar(props: { }, ]; } - const tabsState = tab + const openedFirst = tab ? createSessionWorkbarTabsState([tab], tab.id) : props.tab && props.tab !== 'side-chat' ? openStaticSessionWorkbarTab(emptyTabsState, props.tab) : emptyTabsState; + // Opening a face activates it, so after the extras land the requested face + // is re-activated: the strip shows several tabs with `props.tab` selected + // and the rest unselected, which is the only arrangement where a hovered + // unselected tab can be told apart from the selected one. + const withExtras = (props.alsoOpen ?? []).reduce( + (state, kind) => openStaticSessionWorkbarTab(state, kind), + openedFirst, + ); + const tabsState = openedFirst.activeTabId + ? activateSessionWorkbarTab(withExtras, openedFirst.activeTabId) + : withExtras; return (
    @@ -1006,10 +1000,6 @@ function Workbar(props: { onActivateTab={noop} onCloseTab={noop} onCloseTabs={noop} - onReorderTab={noop} - onMoveTab={noop} - onMoveTabToPanel={noop} - onPinTab={noop} onOpenLauncher={noop} onRequestOpenTab={noop} confirmBypass={async () => true} @@ -1048,6 +1038,26 @@ export const Changes: Story = { render: () => , }; +// Real path: 变更 open, then 浏览器 and 生成文件 opened from [+]. Faces are added to +// the right of the strip and never reordered, so this is what three of them +// look like — one selected, two not, which is the only arrangement where the +// selected marker can be told apart from a hover. (Work Board is not among +// them: this story group's bridge stubs no `workBoard` service, and the panel +// subscribes to it on mount.) +export const SeveralFaces: Story = { + decorators: [bridge()], + render: () => , +}; + +// The same three at the panel's floor, where the strip has to scroll inside +// itself rather than push [+] and the collapse toggle off the edge. +export const SeveralFacesAtColumnFloor: Story = { + decorators: [bridge()], + render: () => ( + + ), +}; + // Real path: 任务工作栏 → 变更 on a session whose branch matches its base. The // panel's own empty state (icon + help), not a spinner and not an error. export const ChangesEmpty: Story = { @@ -1190,26 +1200,6 @@ export const TerminalWriteFailed: Story = { }, }; -// 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 = { - decorators: [bridge()], - render: () => , -}; - -// 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. -export const TasksLoadFailed: Story = { - decorators: [bridge({ tasksFail: true })], - render: () => , -}; - // Storybook cannot host the native WebContentsView, so these pin what the panel // itself draws — chrome and empty state — inside the real workbar shell. export const BrowserEmpty: Story = { diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 6a8def3d1f..274904ffde 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.5.2` (194 component exports). Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 249 files — blocker 0, reimplementation 0, polish 1, aligned 248. +**Totals:** 247 files — blocker 0, reimplementation 0, polish 1, aligned 246. ## Exclusions (explicit) @@ -88,7 +88,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState | aligned — uses Astryx (Banner, EmptyState) | aligned | | `apps/desktop/src/renderer/features/workbar/ui/side-chat-close-confirmation.tsx` | shell-chrome-or-panel | Button, CheckboxInput, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, Text, VStack | aligned — uses Astryx (Button, CheckboxInput, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter) | aligned | | `apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx` | shell-chrome-or-panel | Card, ResizeHandle, Spinner | aligned — uses Astryx (Card, ResizeHandle, Spinner) | aligned | -| `apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx` | shell-chrome-or-panel | Badge, Button, Card, ContextMenu, Heading, Icon, IconButton, Kbd, List, ListItem, Section, Spinner, Tooltip | aligned — uses Astryx (Badge, Button, Card, ContextMenu, Heading, Icon, IconButton, Kbd) | aligned | +| `apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx` | shell-chrome-or-panel | Badge, Card, DropdownMenu, DropdownMenuItem, Heading, Icon, Kbd, List, ListItem, Section, Spinner, Tab, TabList | aligned — uses Astryx (Badge, Card, DropdownMenu, DropdownMenuItem, Heading, Icon, Kbd, List) | aligned | | `apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx` | shell-chrome-or-panel | Icon, IconButton, Tooltip | aligned — uses Astryx (Icon, IconButton, Tooltip) | aligned | | `apps/desktop/src/renderer/keyboard-help.tsx` | dialog-overlay | Dialog, DialogHeader, Heading, Kbd, Layout, LayoutContent | aligned — uses Astryx (Dialog, DialogHeader, Heading, Kbd, Layout, LayoutContent) | aligned | | `apps/desktop/src/renderer/live-turn-reconciler.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | @@ -180,7 +180,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/styles/prompt-suggestions.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/quote-side-panel.css` | shell-chrome-or-panel | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/search-modal.css` | dialog-overlay | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | -| `apps/desktop/src/renderer/styles/session-todo-panel.css` | shell-chrome-or-panel | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/bot.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/settings/connection.css` | settings-module | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | @@ -262,7 +261,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `packages/ui/src/session-rail-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/session-rename-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput) | aligned | | `packages/ui/src/session-sidebar-nav.tsx` | shell-chrome-or-panel | Icon, IconButton, SideNavItem, SideNavSection, Tooltip | aligned — uses Astryx (Icon, IconButton, SideNavItem, SideNavSection, Tooltip) | aligned | -| `packages/ui/src/session-todo-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState, IconButton, Spinner | aligned — uses Astryx (Banner, EmptyState, IconButton, Spinner) | aligned | | `packages/ui/src/skill-inspector.tsx` | shell-chrome-or-panel | Button, Divider, HStack, Heading, MetadataList, MetadataListItem, StackItem, StatusDot, Switch, Text, VStack | aligned — uses Astryx (Button, Divider, HStack, Heading, MetadataList, MetadataListItem, StackItem, StatusDot) | aligned | | `packages/ui/src/skills-panel.tsx` | module-hub | Button, DropdownMenu, DropdownMenuItem, EmptyState, IconButton, List, ListItem, SegmentedControl, SegmentedControlItem, Selector, StatusDot, Text, TextInput, Toolbar | aligned — uses Astryx (Button, DropdownMenu, DropdownMenuItem, EmptyState, IconButton, List, ListItem, SegmentedControl) | aligned | | `packages/ui/src/styles.css` | ui-composition | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index a00a5e0e85..557f6c2ef9 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -151,7 +151,6 @@ apps/desktop/src/renderer/styles/prompt-rail.css apps/desktop/src/renderer/styles/prompt-suggestions.css apps/desktop/src/renderer/styles/quote-side-panel.css apps/desktop/src/renderer/styles/search-modal.css -apps/desktop/src/renderer/styles/session-todo-panel.css apps/desktop/src/renderer/styles/settings.css apps/desktop/src/renderer/styles/settings/bot.css apps/desktop/src/renderer/styles/settings/connection.css @@ -233,7 +232,6 @@ packages/ui/src/session-list-panel.tsx packages/ui/src/session-rail-context.tsx packages/ui/src/session-rename-dialog.tsx packages/ui/src/session-sidebar-nav.tsx -packages/ui/src/session-todo-panel.tsx packages/ui/src/skill-inspector.tsx packages/ui/src/skills-panel.tsx packages/ui/src/styles.css diff --git a/package-lock.json b/package-lock.json index 321c4a5390..872cfd700d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,8 +66,6 @@ "@astryxdesign/core": "0.5.2", "@astryxdesign/theme-neutral": "0.5.2", "@babel/parser": "7.29.7", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", "@maka/ui": "0.1.0", @@ -1667,63 +1665,6 @@ "node": ">=0.1.90" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@earendil-works/pi-tui": { "version": "0.84.4", "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.4.tgz", diff --git a/packages/ui/src/__tests__/session-todo-panel.test.tsx b/packages/ui/src/__tests__/session-todo-panel.test.tsx deleted file mode 100644 index 94ff4f71b2..0000000000 --- a/packages/ui/src/__tests__/session-todo-panel.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { LocaleProvider } from '../locale-context.js'; -import { SessionTodoPanel, sessionTodoActiveCount } from '../session-todo-panel.js'; - -test('renders the Host snapshot as one flat ordered list', () => { - const items = [ - { content: 'First pending item', status: 'pending' as const }, - { content: 'Second completed item', status: 'completed' as const }, - { content: 'Third active item', status: 'in_progress' as const }, - ]; - assert.equal(sessionTodoActiveCount(items), 2); - - const markup = renderToStaticMarkup( - - - , - ); - assert.ok(markup.indexOf('First pending item') < markup.indexOf('Second completed item')); - assert.ok(markup.indexOf('Second completed item') < markup.indexOf('Third active item')); - assert.equal(markup.includes('Task Create'), false); - assert.equal(markup.includes('T1'), false); -}); diff --git a/packages/ui/src/icons.tsx b/packages/ui/src/icons.tsx index 7c9e29a036..31c4b9e326 100644 --- a/packages/ui/src/icons.tsx +++ b/packages/ui/src/icons.tsx @@ -88,6 +88,7 @@ export { Eye, EyeOff, FileCode, + FileDiff, FileEdit, FileImage, FileText, diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 9a31cf091e..267a4c1fff 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -62,7 +62,6 @@ export * from './form-interaction-prompt.js'; export * from './form-interaction-prompt-state.js'; export * from './redact.js'; export * from './thinking-stream.js'; -export * from './session-todo-panel.js'; export * from './toast.js'; export * from './tool-output-stream.js'; export * from './ui.js'; diff --git a/packages/ui/src/session-todo-panel.tsx b/packages/ui/src/session-todo-panel.tsx deleted file mode 100644 index 35989a4116..0000000000 --- a/packages/ui/src/session-todo-panel.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Banner, EmptyState, IconButton, Spinner } from '@astryxdesign/core'; -import type { SessionTodoItem, SessionTodoStatus } from '@maka/core/session-todo'; -import { CheckCircle2, CircleGauge, Clock, ICON_SIZE, ListTodo, RefreshCcw } from './icons.js'; -import { useUiLocale } from './locale-context.js'; -import { getSharedUiCopy } from './shared-ui-copy.js'; - -const STATUS_ICONS = { - pending: Clock, - in_progress: CircleGauge, - completed: CheckCircle2, -} satisfies Record; - -export interface SessionTodoPanelProps { - items: readonly SessionTodoItem[]; - loading?: boolean; - error?: string; - onRetry?: () => void; -} - -export function sessionTodoActiveCount(items: readonly SessionTodoItem[]): number { - return items.filter((item) => item.status !== 'completed').length; -} - -/** Read-only flat projection of the Host-owned current Todo document. */ -export function SessionTodoPanel(props: SessionTodoPanelProps) { - const copy = getSharedUiCopy(useUiLocale()).sessionTodo; - return ( -
    - {props.error ? ( -
    - ); -} diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 098528ce3e..9784398d2f 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -95,13 +95,6 @@ export interface SharedUiCopy { close: string; resizeHandle: string; }; - sessionTodo: { - ariaLabel: string; - retry: string; - loading: string; - activeAriaLabel: string; - empty: string; - }; toast: { notifications: string; closeNotification: string; @@ -192,13 +185,6 @@ const SHARED_UI_COPY = { dailyReviewDisconnectedBody: '桌面端数据桥当前未连接。', }, primitives: { loading: '加载中', close: '关闭', resizeHandle: '调整宽度' }, - sessionTodo: { - ariaLabel: '任务待办', - retry: '重新载入待办', - loading: '正在载入待办…', - activeAriaLabel: '进行中的待办', - empty: '这个任务还没有待办', - }, toast: { notifications: '通知', closeNotification: '关闭通知', confirm: '确定', cancel: '取消' }, stream: { assistantChunkTruncated: '\n[…单条 delta 已截断]\n', assistantTailTruncated: '\n\n[…后续已截断]', thinkingHeadTruncated: '[…已截断早期 reasoning]\n', thinkingChunkTruncated: '\n[…单条 delta 已截断]\n', toolChunkTruncated: '\n[…已截断]\n' }, artifact: { unknownSize: '未知大小' }, @@ -276,13 +262,6 @@ const SHARED_UI_COPY = { dailyReviewDisconnectedBody: '桌面端資料橋目前未連線。', }, primitives: { loading: '載入中', close: '關閉', resizeHandle: '調整寬度' }, - sessionTodo: { - ariaLabel: '任務待辦', - retry: '重新載入待辦', - loading: '正在載入待辦…', - activeAriaLabel: '進行中的待辦', - empty: '這個任務還沒有待辦', - }, toast: { notifications: '通知', closeNotification: '關閉通知', confirm: '確定', cancel: '取消' }, stream: { assistantChunkTruncated: '\n[…單條 delta 已截斷]\n', assistantTailTruncated: '\n\n[…後續已截斷]', thinkingHeadTruncated: '[…已截斷早期 reasoning]\n', thinkingChunkTruncated: '\n[…單條 delta 已截斷]\n', toolChunkTruncated: '\n[…已截斷]\n' }, artifact: { unknownSize: '未知大小' }, @@ -360,13 +339,6 @@ const SHARED_UI_COPY = { dailyReviewDisconnectedBody: 'The desktop data bridge is not connected.', }, primitives: { loading: 'Loading', close: 'Close', resizeHandle: 'Resize handle' }, - sessionTodo: { - 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', - }, toast: { notifications: 'Notifications', closeNotification: 'Close notification', confirm: 'Confirm', cancel: 'Cancel' }, stream: { assistantChunkTruncated: '\n[…single delta truncated]\n', assistantTailTruncated: '\n\n[…remaining output truncated]', thinkingHeadTruncated: '[…earlier reasoning truncated]\n', thinkingChunkTruncated: '\n[…single delta truncated]\n', toolChunkTruncated: '\n[…truncated]\n' }, artifact: { unknownSize: 'Unknown size' },