chore: adapt the tasks/workflow/teams work to DSH 0.1.7-rc.1 (rebase onto v0.21.1)

Rebases the whole branch onto main's new 0.1.7-rc.1 baseline and lands the
adaptation the new host contract requires: the ui-primitives icon family rename
(Icon<Name><14|16> -> Icon<Name>Regular/Medium, with the 14px defaults pinned
back explicitly) and the StateDot redraw, the rewritten dsh-settings surface
(entry-id namespace, DSH schemastery with collected volatile returns,
configure auto:false), session format v4 (producer-owned source kind plus the
rewritten tool-result shape, parsers accepting both), the client snapshot move
to projectionsBySession.subagentCatalog with the jobs.list route behind it, the
^0.1.7-rc.1 peer floor with vendor and bare-import devDeps, the reduced chunk
set mirrored in all eight places, and the capability hand-backs (read-only
previews, terminal, browser).

Verified: typecheck and lint clean; 119 files / 1200 tests; build with no old
contract residue in the bundles; mount e2e 7/7; the tasks page still passes its
70-assertion real-browser contract.
This commit is contained in:
Menghuan1918
2026-09-27 22:18:19 +08:00
parent b554665bde
commit 2a39a0c8b2
41 changed files with 577 additions and 435 deletions
+5 -5
View File
@@ -15,7 +15,7 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import {
Button, IconChevronUpOutline14, IconCopyOutline16, IconStopFill16, StateDot, Switch, Tag,
Button, IconChevronUpOutlineRegular, IconCopyOutlineRegular, IconStopFillRegular, StateDot, Switch, Tag,
type TagTone,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarJobView } from '../context-types.ts'
@@ -120,7 +120,7 @@ export function JobsDrawer(props: JobsDrawerProps): ReactNode {
<span className={css.jobsBigNum}>{liveCount > 0 ? liveCount : rows.length}</span>
<span className={css.jobsDrawerCount}>{countLabel}</span>
<span className={css.jobsDrawerChev} data-open={open ? 'true' : 'false'} aria-hidden="true">
<IconChevronUpOutline14 size={12} />
<IconChevronUpOutlineRegular size={12} />
</span>
</button>
{!autoOpen && <div className={css.jobsAutoNote}>{t('jobsAutoCollapsed')}</div>}
@@ -174,7 +174,7 @@ export function JobsDrawer(props: JobsDrawerProps): ReactNode {
size="sm"
className={css.jobsKill}
data-armed={armed ? 'true' : 'false'}
icon={<IconStopFill16 size={11} />}
icon={<IconStopFillRegular size={11} />}
aria-label={armed ? t('jobKillConfirm') : t('jobKill')}
title={armed ? t('jobKillConfirm') : t('jobKill')}
disabled={killing}
@@ -288,7 +288,7 @@ export function JobOutputPopoverContent(props: {
<Button
variant="ghost"
size="sm"
icon={<IconCopyOutline16 size={12} />}
icon={<IconCopyOutlineRegular size={12} />}
aria-label={copied ? t('jobCopied') : t('jobCopyOutput')}
title={copied ? t('jobCopied') : t('jobCopyOutput')}
disabled={text === ''}
@@ -343,7 +343,7 @@ export function JobOutputPopoverContent(props: {
variant="outline"
size="sm"
className={armed ? css.jobPopKillArmed : undefined}
icon={<IconStopFill16 size={11} />}
icon={<IconStopFillRegular size={11} />}
disabled={killing}
onClick={() => {
if (armed) void kill()
+1 -17
View File
@@ -73,21 +73,6 @@ import { api } from './api.ts'
import { usePolling } from './use-polling.ts'
import { t } from './locales.ts'
import type { SessionScope } from './api.ts'
import type { ComponentProps, ComponentType } from 'react'
/**
* Cross-line compatibility shim for `ConnectionIndicator`.
*
* 0.1.6-alpha.2 REMOVED the `reconnectLabel` prop (the outage label already
* names the action), while the 0.1.5-rc line still REQUIRES it as the retry
* button's hover/focus action text. The plugin's peer range covers both, so
* the component type is widened once here and the prop is always passed: rc
* renders it, alpha.2 ignores the extra key. Delete this shim (and the prop)
* when the peer floor moves past the rc line.
*/
const ConnectionIndicatorCompat = ConnectionIndicator as unknown as ComponentType<
ComponentProps<typeof ConnectionIndicator> & { reconnectLabel?: string }
>
import type { SidebarTab } from './state.ts'
import css from './SideChatView.module.css'
@@ -768,10 +753,9 @@ export function SideChatView(props: {
</button>
</div>
{connectionState !== undefined && connectionState !== 'connected' && (
<ConnectionIndicatorCompat
<ConnectionIndicator
state={connectionState}
disconnectedLabel={t('sideChatConnDisconnected')}
reconnectLabel={t('sideChatConnReconnect')}
connectingLabel={t('sideChatConnConnecting')}
recoveredLabel={t('sideChatConnRecovered')}
reconnectActionLabel={t('sideChatConnReconnectAction')}
+105 -62
View File
@@ -27,21 +27,24 @@
* descendant count is mono micro-type, and the canvas / tree / board / drawer
* own their own chrome.
*/
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import { useSyncExternalStore } from 'react'
import {
Button, IconRefreshOutline14,
Button, IconRefreshOutlineRegular,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type {
Context,
SidebarJobView,
SidebarSessionList,
SidebarSubagentAddress,
} from '../context-types.ts'
import {
collectBranchIds,
countSubagentDescendants,
isSideThreadSummary,
rootAncestor,
} from './subagent-detect.ts'
import { subagentCatalogs } from './subagent-catalog.ts'
import { treeSessionIds } from './subagent-lineage.ts'
import { type LastActivity } from '../subagent-activity.ts'
import { collectTreeJobs, orderJobs } from './subagent-jobs.ts'
import { api, type TeamsViewResult } from './api.ts'
@@ -59,10 +62,28 @@ import type { SidebarStore } from './state.ts'
import type { WorkflowRunView } from '../workflow-runs.ts'
import legacy from './SubagentView.module.css'
/** Refresh cadence of the live "last text + tool call" lines while a child runs. */
/** Poll cadence of the live "last text + tool call" lines while a child runs. */
const POLL_MS = 3000
/** Poll cadence of the workflow-run and team views while the page is visible. */
const TELEMETRY_POLL_MS = 5000
/** Poll cadence of the background-job lists (one read per tree session). */
const JOBS_POLL_MS = 3000
/**
* The workspace face's "show this conversation" verb. DSH moved child and
* session navigation off `ISessions` onto the service that owns the
* main-view selection, so the mirror is declared here rather than widened in
* `context-types.ts` (the optional old names stay as the fallback).
*/
interface ConversationNavigation {
openSession?(target: SidebarSubagentAddress | string): void
}
/** The per-parent catalog refresh across the two DSH generations. */
interface CatalogRefreshFace {
refreshProjections?(sessionId: string): Promise<void>
refreshSubagents?(sessionId: string): Promise<void>
}
/**
* One shared live-preview poller for the whole tree. At most ONE
@@ -134,6 +155,46 @@ function useTeamView(
return { view, refresh }
}
/**
* The background-job lists of the whole tree, keyed by OWNER session.
*
* DSH 0.1.7 dropped the client session snapshot's `jobsBySession` push mirror,
* so the registry is read through the plugin's own `jobs.list` route — and the
* registry's access fence admits a job to its OWNER session only, so the whole
* tree is one read per tree session, fanned out on each tick. A host without
* the jobs service (503) or one dropped read degrades to an empty set for THAT
* session; the next tick retries.
*/
function useTreeJobs(
byId: SidebarSessionList['byId'],
rootId: string | undefined,
active: boolean,
): Readonly<Record<string, readonly SidebarJobView[]>> {
const treeIds = useMemo(
() => (rootId === undefined ? [] : [...treeSessionIds(byId, rootId)]),
[byId, rootId],
)
const [jobsBySession, setJobsBySession] = useState<Readonly<Record<string, readonly SidebarJobView[]>>>({})
useEffect(() => { setJobsBySession({}) }, [rootId])
const poll = useCallback(async (signal: AbortSignal): Promise<void> => {
const entries = await Promise.all(treeIds.map(async (sessionId): Promise<[string, readonly SidebarJobView[]]> => {
try {
const result = await api.jobsList(sessionId, signal)
return [sessionId, result.jobs]
} catch {
return [sessionId, []]
}
}))
if (!signal.aborted) setJobsBySession(Object.fromEntries(entries))
}, [treeIds])
usePolling(active && treeIds.length > 0, poll, {
intervalMs: JOBS_POLL_MS,
mode: 'self-scheduling',
immediate: true,
})
return jobsBySession
}
/** The open popover of the page (one at a time, anchored). */
type PagePopover =
| { kind: 'node'; nodeId: string; anchor: HTMLElement }
@@ -158,17 +219,20 @@ export function SubagentView(props: {
const { sessionId, active, ctx, store, onOpenChild } = props
const sessions = ctx.sessions
// The same list feed the official catalog consumes (byId lineage + the
// lazy per-parent catalogs). Older DSH snapshots without the subagent seam
// simply leave these surfaces empty — the page degrades to the empty state.
// The same list feed the official catalog consumes. DSH 0.1.7 publishes the
// host-computed `subagentCatalog` projection per session and loads every
// session's projections once per connection, so there is no observe/refresh
// handshake left to run — the page is a pure reader of the snapshot. A host
// without the projection store leaves these surfaces empty and the page
// degrades to its empty state.
const list = useSyncExternalStore(
useMemo(() => (callback: () => void) => sessions.list.subscribe(callback), [sessions]),
useCallback(() => sessions.list.getSnapshot(), [sessions]),
)
const byId = list.byId
// Memoized so the empty-catalog fallback keeps a stable identity — a fresh
// `{}` per render would invalidate every catalog-dependent memo/effect.
const catalogs = useMemo(() => list.subagentsByParent ?? {}, [list.subagentsByParent])
// The projected per-parent catalogs, already folded into the view shape the
// topology consumes (see subagent-catalog.ts).
const catalogs = useMemo(() => subagentCatalogs(list.projectionsBySession), [list.projectionsBySession])
// The topology root: the main agent of the current session's tree.
const rootId = useMemo(() => rootAncestor(byId, sessionId), [byId, sessionId])
@@ -212,48 +276,22 @@ export function SubagentView(props: {
[byId, catalogs, rootId, sessionId, live, runs, teamMembers, teamView, folded],
)
/** Catalog owners currently consuming live membership updates. */
const observedRef = useRef(new Set<string>())
const observe = useCallback((parentSessionId: string, open: boolean): void => {
sessions.setSubagentCatalogOpen?.(parentSessionId, open)
if (open) observedRef.current.add(parentSessionId)
else observedRef.current.delete(parentSessionId)
}, [sessions])
// While the page is visible the topology root consumes live membership; a
// root change (switching to another main agent's tree) or the page hiding
// (tab switched away / panel collapsed) releases everything observed.
useEffect(() => {
if (rootId === undefined || !active) return
observe(rootId, true)
return () => {
// The cleanup must release everything observed AT cleanup time (the set
// mutates as branches open), so reading the ref here is the point.
// eslint-disable-next-line react-hooks/exhaustive-deps
for (const parentSessionId of observedRef.current) {
sessions.setSubagentCatalogOpen?.(parentSessionId, false)
}
observedRef.current.clear()
/**
* Show one conversation in the main view. DSH moved child and session
* navigation OFF `ISessions` (0.1.6's `open` / `openSubagent` are gone from
* the runtime, and the optional calls this page used were silently dead)
* onto the workspace face, which owns the main-view selection. The old
* names stay as the fallback for a host that predates the move.
*/
const openConversation = useCallback((target: SidebarSubagentAddress | string): void => {
const workspace = ctx.get('uiWorkspace') as unknown as ConversationNavigation | undefined
if (typeof workspace?.openSession === 'function') {
workspace.openSession(target)
return
}
}, [rootId, active, observe, sessions])
// Every branch of the always-expanded topology consumes live membership.
const branches = useMemo(() => collectBranchIds(catalogs, rootId), [catalogs, rootId])
useEffect(() => {
if (!active) return
for (const id of branches) {
if (!observedRef.current.has(id)) observe(id, true)
}
}, [branches, active, observe])
// Unobserve everything on unmount (the host stops refreshing unused catalogs).
useEffect(() => () => {
for (const parentSessionId of observedRef.current) {
sessions.setSubagentCatalogOpen?.(parentSessionId, false)
}
observedRef.current.clear()
}, [sessions])
if (typeof target === 'string') sessions.open?.(target)
else sessions.openSubagent?.(target)
}, [ctx, sessions])
const openChild = useCallback((address: SidebarSubagentAddress): void => {
// Notify the shell first: the jump switches the sidebar to the child
@@ -262,24 +300,28 @@ export function SubagentView(props: {
// highlighted) — the README "page stays open" contract.
onOpenChild?.(address)
try {
sessions.openSubagent?.(address)
openConversation(address)
} catch (error) {
console.error('[dsh-better-sidebar] openSubagent failed:', error)
console.error('[dsh-better-sidebar] open subagent failed:', error)
}
}, [sessions, onOpenChild])
}, [openConversation, onOpenChild])
/** Jump back to the main agent (the topology root) from its node. */
const openMain = useCallback((): void => {
if (rootId === undefined) return
try {
sessions.open?.(rootId)
openConversation(rootId)
} catch (error) {
console.error('[dsh-better-sidebar] open session failed:', error)
}
}, [sessions, rootId])
}, [openConversation, rootId])
const refresh = useCallback((parentSessionId: string): void => {
void sessions.refreshSubagents?.(parentSessionId)
// 0.1.7 renamed the per-parent catalog refresh to `refreshProjections`
// (the projection store owns every session's values now); calling the
// removed 0.1.6 name alone would make this button a silent no-op.
const face = sessions as CatalogRefreshFace
void (face.refreshProjections ?? face.refreshSubagents)?.(parentSessionId)
}, [sessions])
/** Jump from the detail window into the node's transcript. */
@@ -316,10 +358,11 @@ export function SubagentView(props: {
)
const agentCount = totals.count + 1
/** The tree's ordered job rows (owner-labeled). */
/** The tree's ordered job rows (owner-labeled, read per tree session). */
const jobsBySession = useTreeJobs(byId, rootId, active)
const jobRows = useMemo(
() => orderJobs(collectTreeJobs(byId, list.jobsBySession, rootId)),
[byId, list.jobsBySession, rootId],
() => orderJobs(collectTreeJobs(byId, jobsBySession, rootId)),
[byId, jobsBySession, rootId],
)
/** Catalogs that failed to load (surfaced as one banner in both modes). */
@@ -445,7 +488,7 @@ export function SubagentView(props: {
variant="ghost"
size="sm"
className={legacy.subagentRefresh}
icon={<IconRefreshOutline14 />}
icon={<IconRefreshOutlineRegular size={14} />}
aria-label={t('refresh')}
title={t('refresh')}
disabled={rootId === undefined}
@@ -474,7 +517,7 @@ export function SubagentView(props: {
variant="outline"
size="sm"
className={legacy.subagentErrorRetry}
icon={<IconRefreshOutline14 />}
icon={<IconRefreshOutlineRegular size={14} />}
onClick={() => { for (const parent of failedParents) refresh(parent) }}
>
{t('retry')}
+15 -9
View File
@@ -17,8 +17,8 @@
import { useEffect, useId, useRef, useState, type ReactNode } from 'react'
import clsx from 'clsx'
import {
Button, IconCheckOutline14, IconCloseOutline16, IconEditOutline16,
IconRefreshOutline14, IconTrashOutline16, Input, MarkdownText, Pill, StateDot, Tag,
Button, IconCheckOutlineRegular, IconCloseOutlineRegular, IconEditOutlineRegular,
IconRefreshOutlineRegular, IconTrashOutlineRegular, Input, MarkdownText, Pill, StateDot, Tag,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarTeamMemberView, SidebarTeamTaskView } from '../context-types.ts'
import { api } from './api.ts'
@@ -133,7 +133,13 @@ function TaskViewBody(props: {
? <div className={css.popHint}>{t('teamTaskNoDescription')}</div>
: (
<MarkdownText
{...markdownTextProps(body, { copyLabel: t('copy'), copiedLabel: t('copied') })}
{...markdownTextProps(body, {
copyLabel: t('copy'),
copiedLabel: t('copied'),
codeLabel: t('codeBlockTitle'),
wrapLabel: t('codeBlockWrap'),
unwrapLabel: t('codeBlockUnwrap'),
})}
/>
)}
</div>
@@ -309,7 +315,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
<Button
variant="ghost"
size="sm"
icon={<IconCloseOutline16 size={12} />}
icon={<IconCloseOutlineRegular size={12} />}
aria-label={t('teamTaskCancel')}
title={t('teamTaskCancel')}
onClick={onClose}
@@ -345,7 +351,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
<Button
variant="primary"
size="sm"
icon={<IconCheckOutline14 size={12} />}
icon={<IconCheckOutlineRegular size={12} />}
disabled={busy || subject.trim() === ''}
onClick={save}
>
@@ -374,7 +380,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
<Button
variant="outline"
size="sm"
icon={<IconEditOutline16 size={12} />}
icon={<IconEditOutlineRegular size={12} />}
onClick={() => { setEditing(true) }}
>
{t('teamTaskEdit')}
@@ -387,7 +393,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
<Button
variant="primary"
size="sm"
icon={<IconRefreshOutline14 size={12} />}
icon={<IconRefreshOutlineRegular size={12} />}
disabled={busy}
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
taskId: task.id, expectedRevision: task.revision, action: 'reopen',
@@ -400,7 +406,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
<Button
variant="primary"
size="sm"
icon={<IconCheckOutline14 size={12} />}
icon={<IconCheckOutlineRegular size={12} />}
disabled={busy}
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
taskId: task.id, expectedRevision: task.revision, action: 'complete',
@@ -413,7 +419,7 @@ export function TaskWindow(props: TaskWindowProps): ReactNode {
variant="outline"
size="sm"
className={clsx(css.taskDanger, armedDelete && css.taskDangerArmed)}
icon={<IconTrashOutline16 size={12} />}
icon={<IconTrashOutlineRegular size={12} />}
disabled={busy}
onClick={() => {
if (!armedDelete) { setArmedDelete(true); return }
+5 -5
View File
@@ -30,7 +30,7 @@ import {
} from 'react'
import clsx from 'clsx'
import {
Button, IconChecklistOutline14, IconFullscreenOutline16, IconLoadingOutline16, IconTreeCorner8x10,
Button, IconChecklistOutlineRegular, IconFullscreenOutlineRegular, IconLoadingOutlineRegular, IconTreeCornerRegular,
StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { TasksAgentNode, TasksFoldNode, TasksNode, TasksWorkflowNode } from './tasks-model.ts'
@@ -110,7 +110,7 @@ export function ViewModeToggle(props: {
<Button
variant="ghost"
size="sm"
icon={toTree ? <IconTreeCorner8x10 /> : <WorkflowGlyph size={13} />}
icon={toTree ? <IconTreeCornerRegular /> : <WorkflowGlyph size={13} />}
aria-label={t(toTree ? 'tasksViewSwitchToTree' : 'tasksViewSwitchToGraph')}
title={t(toTree ? 'tasksViewSwitchToTree' : 'tasksViewSwitchToGraph')}
onClick={() => { onModeChange(toTree ? 'tree' : 'graph') }}
@@ -128,7 +128,7 @@ export function FoldToggleButton(props: { folded: boolean; onToggleFold(): void
variant="ghost"
size="sm"
className={clsx(css.controlBtn, folded && css.controlBtnActive)}
icon={<IconChecklistOutline14 size={13} />}
icon={<IconChecklistOutlineRegular size={13} />}
aria-pressed={folded}
aria-label={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
title={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
@@ -322,7 +322,7 @@ export function TasksGraph(props: TasksGraphProps): ReactNode {
zoom / node clicks fully live underneath. */}
{loading === true && nodes.length === 0 && (
<div className={canvasCss.loading}>
<span className={canvasCss.loadingGlyph} aria-hidden="true"><IconLoadingOutline16 size={12} /></span>
<span className={canvasCss.loadingGlyph} aria-hidden="true"><IconLoadingOutlineRegular size={12} /></span>
{t('loading')}
</div>
)}
@@ -411,7 +411,7 @@ export function TasksGraph(props: TasksGraphProps): ReactNode {
variant="ghost"
size="sm"
className={css.controlBtn}
icon={<IconFullscreenOutline16 size={13} />}
icon={<IconFullscreenOutlineRegular size={13} />}
aria-label={t('tasksZoomFit')}
title={t('tasksZoomFit')}
onClick={refit}
+7 -7
View File
@@ -26,11 +26,11 @@
*/
import type { ReactNode } from 'react'
import clsx from 'clsx'
import { Button, IconRightUpOutline14, StateDot, Tag } from '@deepseek-ai/dsh-client-ui-primitives'
import { Button, IconRightUpOutlineRegular, StateDot, Tag } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarSubagentAddress } from '../context-types.ts'
import type { TasksAgentNode, TasksWorkflowNode } from './tasks-model.ts'
import {
AgentGlyph, WorkflowGlyph, flatten, nodeDotState, taskDotState, taskStatusKey, workflowStatusKey,
AgentGlyph, WorkflowGlyph, flatten, modeLabel, nodeDotState, taskDotState, taskStatusKey, workflowStatusKey,
} from './tasks-shared.tsx'
import { t, type CopyKey } from './locales.ts'
import css from './tasks-graph.module.css'
@@ -78,6 +78,8 @@ export function AgentNodePopover(props: {
onOpenTask(taskId: string, anchor: HTMLElement): void
}): ReactNode {
const { node, onJump, onOpenTask } = props
/** The row's mode word; `unknown` claims no mode and this row is omitted. */
const nodeMode = modeLabel(node.mode)
const liveText = node.live?.text !== undefined ? flatten(node.live.text) : undefined
const liveTool = node.live?.tool !== undefined
? `${node.live.tool.name}${node.live.tool.args === '' ? '' : ` ${node.live.tool.args}`}`
@@ -92,10 +94,8 @@ export function AgentNodePopover(props: {
<PopRow label={t('tasksNodeState')}>
<StateDot state={nodeDotState(node.state)} size={6} /> {t(stateKey(node.state))}
</PopRow>
{node.mode !== undefined && (
<PopRow label={t('tasksNodeMode')}>
{node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable')}
</PopRow>
{nodeMode !== undefined && (
<PopRow label={t('tasksNodeMode')}>{nodeMode}</PopRow>
)}
{node.team !== undefined && (
<PopRow label={t('tasksNodeTeamRole')}>
@@ -141,7 +141,7 @@ export function AgentNodePopover(props: {
<Button
variant="primary"
size="sm"
icon={<IconRightUpOutline14 size={12} />}
icon={<IconRightUpOutlineRegular size={12} />}
onClick={() => { onJump(node) }}
>
{t('tasksNodeJump')}
+2 -2
View File
@@ -17,7 +17,7 @@
*/
import { useCallback, useMemo, useRef, type KeyboardEvent, type ReactNode } from 'react'
import clsx from 'clsx'
import { IconLoadingOutline16, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import { IconLoadingOutlineRegular, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import type { TasksAgentNode, TasksNode, TasksWorkflowNode } from './tasks-model.ts'
import {
AgentGlyph, agentMeta, FoldGlyph, foldPreviews, LiveLine, nodeDotState, TaskLine,
@@ -200,7 +200,7 @@ export function TasksTree(props: TasksTreeProps): ReactNode {
>
{loading === true && roots.length === 0 && (
<div className={css.viewEmptyHint}>
<span className={css.treeGlyph} aria-hidden="true"><IconLoadingOutline16 size={12} /></span>
<span className={css.treeGlyph} aria-hidden="true"><IconLoadingOutlineRegular size={12} /></span>
{t('loading')}
</div>
)}
+11 -11
View File
@@ -20,9 +20,9 @@
*/
import { useMemo, useRef, useState, type ReactNode } from 'react'
import {
Button, IconChecklistOutline14, IconCheckOutline14, IconChevronUpOutline14, IconEditOutline16,
IconEllipsisOutline16, IconPlusOutline16, IconRefreshOutline14, IconTrashOutline16,
IconUserOutline16, Menu, Pill, StateDot, Tag,
Button, IconChecklistOutlineRegular, IconCheckOutlineRegular, IconChevronUpOutlineRegular, IconEditOutlineRegular,
IconEllipsisOutlineRegular, IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular,
IconUserOutlineRegular, Menu, Pill, StateDot, Tag,
type MenuEntry, type TagTone,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarTeamMemberView, SidebarTeamTaskView } from '../context-types.ts'
@@ -66,20 +66,20 @@ function taskMenuItems(
{
id: 'state',
label: t(completed ? 'teamTaskReopen' : 'teamTaskComplete'),
icon: completed ? <IconRefreshOutline14 size={14} /> : <IconCheckOutline14 size={14} />,
icon: completed ? <IconRefreshOutlineRegular size={14} /> : <IconCheckOutlineRegular size={14} />,
},
{ id: 'edit', label: t('teamTaskEdit'), icon: <IconEditOutline16 size={14} /> },
{ id: 'edit', label: t('teamTaskEdit'), icon: <IconEditOutlineRegular size={14} /> },
{
id: 'owner',
label: t('teamTaskOwner'),
icon: <IconUserOutline16 size={14} />,
icon: <IconUserOutlineRegular size={14} />,
submenu: [
{ id: 'owner:', label: t('teamTaskUnowned') },
...teammates.map(member => ({ id: `owner:${member.name}`, label: member.name })),
],
},
{ type: 'separator', id: 'team-task-sep' },
{ id: 'delete', label: t('teamTaskDelete'), icon: <IconTrashOutline16 size={14} /> },
{ id: 'delete', label: t('teamTaskDelete'), icon: <IconTrashOutlineRegular size={14} /> },
]
}
@@ -148,7 +148,7 @@ function TeamTaskRow(props: {
<Button
variant="ghost"
size="sm"
icon={<IconEllipsisOutline16 size={13} />}
icon={<IconEllipsisOutlineRegular size={13} />}
aria-label={`${t('teamTaskActions')} ${task.subject}`}
aria-haspopup="menu"
aria-expanded={menuOpen}
@@ -191,7 +191,7 @@ export function TeamBoard(props: TeamBoardProps): ReactNode {
aria-expanded={!collapsed}
onClick={onToggleCollapsed}
>
<span className={css.teamBoardIcon} aria-hidden="true"><IconChecklistOutline14 size={11} /></span>
<span className={css.teamBoardIcon} aria-hidden="true"><IconChecklistOutlineRegular size={11} /></span>
<span>{t('teamBoard')}</span>
<span className={css.teamBoardCount}>
{t('teamChip', { members: members.length, tasks: live.length })}
@@ -201,7 +201,7 @@ export function TeamBoard(props: TeamBoardProps): ReactNode {
style={{ display: 'inline-flex', transform: collapsed ? undefined : 'rotate(180deg)' }}
aria-hidden="true"
>
<IconChevronUpOutline14 size={12} />
<IconChevronUpOutlineRegular size={12} />
</span>
</button>
{!collapsed && (
@@ -247,7 +247,7 @@ export function TeamBoard(props: TeamBoardProps): ReactNode {
<Button
variant="outline"
size="sm"
icon={<IconPlusOutline16 size={13} />}
icon={<IconPlusOutlineRegular size={13} />}
onClick={(event) => { onOpenTask(undefined, event.currentTarget) }}
>
{t('teamTaskCreate')}
-1
View File
@@ -333,7 +333,6 @@ export const ar: Record<string, string> = {
codeBlockUnwrap: 'بدون التفاف للأسطر',
sideChatBlockWindow: '{shown} من {total} سطرًا',
sideChatConnDisconnected: 'انقطع الاتصال',
sideChatConnReconnect: 'إعادة الاتصال',
sideChatConnConnecting: 'جارٍ إعادة الاتصال…',
sideChatConnRecovered: 'تم استعادة الاتصال',
sideChatConnReconnectAction: 'إعادة الاتصال الآن',
-1
View File
@@ -318,7 +318,6 @@ export const de: Record<string, string> = {
codeBlockUnwrap: 'Zeilen nicht umbrechen',
sideChatBlockWindow: '{shown} von {total} Zeilen',
sideChatConnDisconnected: 'Verbindung verloren',
sideChatConnReconnect: 'Neu verbinden',
sideChatConnConnecting: 'Verbinde neu…',
sideChatConnRecovered: 'Verbindung wiederhergestellt',
sideChatConnReconnectAction: 'Jetzt neu verbinden',
-1
View File
@@ -325,7 +325,6 @@ export const fr: Record<string, string> = {
codeBlockUnwrap: 'Ne pas retourner à la ligne',
sideChatBlockWindow: '{shown} sur {total} lignes',
sideChatConnDisconnected: 'Connexion perdue',
sideChatConnReconnect: 'Reconnecter',
sideChatConnConnecting: 'Reconnexion…',
sideChatConnRecovered: 'Connexion rétablie',
sideChatConnReconnectAction: 'Reconnecter maintenant',
-1
View File
@@ -332,7 +332,6 @@ export const hi: Record<string, string> = {
codeBlockUnwrap: 'पंक्तियाँ न लपेटें',
sideChatBlockWindow: 'कुल {total} में से {shown} पंक्तियाँ',
sideChatConnDisconnected: 'कनेक्शन टूट गया',
sideChatConnReconnect: 'फिर से कनेक्ट करें',
sideChatConnConnecting: 'फिर से कनेक्ट हो रहा है…',
sideChatConnRecovered: 'कनेक्शन बहाल हुआ',
sideChatConnReconnectAction: 'अभी फिर से कनेक्ट करें',
-1
View File
@@ -330,7 +330,6 @@ export const id: Record<string, string> = {
codeBlockUnwrap: 'Jangan bungkus baris',
sideChatBlockWindow: '{shown} dari {total} baris',
sideChatConnDisconnected: 'Koneksi terputus',
sideChatConnReconnect: 'Sambungkan ulang',
sideChatConnConnecting: 'Menyambung ulang…',
sideChatConnRecovered: 'Koneksi dipulihkan',
sideChatConnReconnectAction: 'Sambungkan ulang sekarang',
-1
View File
@@ -323,7 +323,6 @@ export const it: Record<string, string> = {
codeBlockUnwrap: 'Nessun a capo',
sideChatBlockWindow: '{shown} di {total} righe',
sideChatConnDisconnected: 'Connessione persa',
sideChatConnReconnect: 'Riconnetti',
sideChatConnConnecting: 'Riconnessione…',
sideChatConnRecovered: 'Connessione ripristinata',
sideChatConnReconnectAction: 'Riconnetti ora',
-1
View File
@@ -332,7 +332,6 @@ export const ja: Record<string, string> = {
codeBlockUnwrap: '折り返しを無効にする',
sideChatBlockWindow: '全 {total} 行中 {shown} 行を表示',
sideChatConnDisconnected: '接続が切断されました。クリックで再接続',
sideChatConnReconnect: '再接続',
sideChatConnConnecting: '再接続中…',
sideChatConnRecovered: '接続が復旧しました',
sideChatConnReconnectAction: '今すぐ再接続',
-1
View File
@@ -324,7 +324,6 @@ export const ko: Record<string, string> = {
codeBlockUnwrap: '줄 바꿈 해제',
sideChatBlockWindow: '전체 {total}줄 중 {shown}줄 표시',
sideChatConnDisconnected: '연결이 끊겼습니다',
sideChatConnReconnect: '다시 연결',
sideChatConnConnecting: '다시 연결 중…',
sideChatConnRecovered: '연결이 복원되었습니다',
sideChatConnReconnectAction: '지금 다시 연결',
-1
View File
@@ -330,7 +330,6 @@ export const nl: Record<string, string> = {
codeBlockUnwrap: 'Regels niet afbreken',
sideChatBlockWindow: '{shown} van {total} regels',
sideChatConnDisconnected: 'Verbinding verbroken',
sideChatConnReconnect: 'Opnieuw verbinden',
sideChatConnConnecting: 'Opnieuw verbinden…',
sideChatConnRecovered: 'Verbinding hersteld',
sideChatConnReconnectAction: 'Nu opnieuw verbinden',
-1
View File
@@ -334,7 +334,6 @@ export const pl: Record<string, string> = {
codeBlockUnwrap: 'Bez zawijania wierszy',
sideChatBlockWindow: '{shown} z {total} wierszy',
sideChatConnDisconnected: 'Połączenie utracone',
sideChatConnReconnect: 'Połącz ponownie',
sideChatConnConnecting: 'Łączenie ponownie…',
sideChatConnRecovered: 'Połączenie przywrócone',
sideChatConnReconnectAction: 'Połącz ponownie teraz',
-1
View File
@@ -315,7 +315,6 @@ export const pt: Record<string, string> = {
codeBlockUnwrap: 'Não quebrar linhas',
sideChatBlockWindow: '{shown} de {total} linhas',
sideChatConnDisconnected: 'Conexão perdida',
sideChatConnReconnect: 'Reconectar',
sideChatConnConnecting: 'Reconectando…',
sideChatConnRecovered: 'Conexão restabelecida',
sideChatConnReconnectAction: 'Reconectar agora',
-1
View File
@@ -330,7 +330,6 @@ export const ru: Record<string, string> = {
codeBlockUnwrap: 'Не переносить строки',
sideChatBlockWindow: '{shown} из {total} строк',
sideChatConnDisconnected: 'Соединение потеряно',
sideChatConnReconnect: 'Переподключиться',
sideChatConnConnecting: 'Переподключение…',
sideChatConnRecovered: 'Соединение восстановлено',
sideChatConnReconnectAction: 'Переподключиться сейчас',
-1
View File
@@ -315,7 +315,6 @@ export const sv: Record<string, string> = {
codeBlockUnwrap: 'Ingen radbrytning',
sideChatBlockWindow: '{shown} av {total} rader',
sideChatConnDisconnected: 'Anslutningen bröts',
sideChatConnReconnect: 'Anslut igen',
sideChatConnConnecting: 'Ansluter igen…',
sideChatConnRecovered: 'Anslutningen återställd',
sideChatConnReconnectAction: 'Anslut igen nu',
-1
View File
@@ -332,7 +332,6 @@ export const th: Record<string, string> = {
codeBlockUnwrap: 'ไม่ตัดบรรทัด',
sideChatBlockWindow: '{shown} จาก {total} บรรทัด',
sideChatConnDisconnected: 'การเชื่อมต่อขาดหาย',
sideChatConnReconnect: 'เชื่อมต่อใหม่',
sideChatConnConnecting: 'กำลังเชื่อมต่อใหม่…',
sideChatConnRecovered: 'กู้คืนการเชื่อมต่อแล้ว',
sideChatConnReconnectAction: 'เชื่อมต่อใหม่ตอนนี้',
-1
View File
@@ -332,7 +332,6 @@ export const tr: Record<string, string> = {
codeBlockUnwrap: 'Satırları kaydırma',
sideChatBlockWindow: '{total} satırdan {shown} satır',
sideChatConnDisconnected: 'Bağlantı koptu',
sideChatConnReconnect: 'Yeniden bağlan',
sideChatConnConnecting: 'Yeniden bağlanıyor…',
sideChatConnRecovered: 'Bağlantı geri yüklendi',
sideChatConnReconnectAction: 'Şimdi yeniden bağlan',
-1
View File
@@ -332,7 +332,6 @@ export const vi: Record<string, string> = {
codeBlockUnwrap: 'Không ngắt dòng',
sideChatBlockWindow: '{shown} trong {total} dòng',
sideChatConnDisconnected: 'Mất kết nối',
sideChatConnReconnect: 'Kết nối lại',
sideChatConnConnecting: 'Đang kết nối lại…',
sideChatConnRecovered: 'Đã khôi phục kết nối',
sideChatConnReconnectAction: 'Kết nối lại ngay',
-1
View File
@@ -347,7 +347,6 @@ export const zhHK: Record<string, string> = {
codeBlockUnwrap: '取消自動換行',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
sideChatConnDisconnected: '連線已中斷',
sideChatConnReconnect: '重新連線',
sideChatConnConnecting: '正在重新連線…',
sideChatConnRecovered: '連線已恢復',
sideChatConnReconnectAction: '立即重新連線',
-1
View File
@@ -347,7 +347,6 @@ export const zhMO: Record<string, string> = {
codeBlockUnwrap: '取消自動換行',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
sideChatConnDisconnected: '連線已中斷',
sideChatConnReconnect: '重新連線',
sideChatConnConnecting: '正在重新連線…',
sideChatConnRecovered: '連線已恢復',
sideChatConnReconnectAction: '立即重新連線',
-1
View File
@@ -347,7 +347,6 @@ export const zhTW: Record<string, string> = {
codeBlockUnwrap: '取消自動換行',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
sideChatConnDisconnected: '連線已中斷',
sideChatConnReconnect: '重新連線',
sideChatConnConnecting: '正在重新連線…',
sideChatConnRecovered: '連線已恢復',
sideChatConnReconnectAction: '立即重新連線',
-2
View File
@@ -344,7 +344,6 @@ export const zh = {
codeBlockUnwrap: '取消自动换行',
sideChatBlockWindow: '共 {total} 行 · 显示 {shown} 行',
sideChatConnDisconnected: '连接已断开,点击立即重连',
sideChatConnReconnect: '重新连接',
sideChatConnConnecting: '正在重连…',
sideChatConnRecovered: '连接已恢复',
sideChatConnReconnectAction: '立即重连',
@@ -815,7 +814,6 @@ export const en: Record<keyof typeof zh, string> = {
codeBlockUnwrap: 'Do not wrap lines',
sideChatBlockWindow: '{shown} of {total} lines',
sideChatConnDisconnected: 'Disconnected, reconnect now',
sideChatConnReconnect: 'Reconnect',
sideChatConnConnecting: 'Reconnecting…',
sideChatConnRecovered: 'Connection restored',
sideChatConnReconnectAction: 'Reconnect now',
+2 -2
View File
@@ -81,7 +81,7 @@ export function subagentCatalogs(
* @param childSessionId - the child row whose own catalog decides.
*/
export function isKnownLeaf(
catalogs: Readonly<Record<string, SubagentCatalogView>>,
catalogs: Readonly<Record<string, SubagentCatalogView | undefined>>,
childSessionId: string,
): boolean {
const catalog = catalogs[childSessionId]
@@ -97,7 +97,7 @@ export function isKnownLeaf(
* @param childSessionId - the child row being drawn.
*/
export function childActivity(
live: Readonly<Record<string, LastActivity>>,
live: Readonly<Record<string, LastActivity | undefined>>,
childSessionId: string,
): 'running' | 'inactive' {
return live[childSessionId] === undefined ? 'inactive' : 'running'
+23 -20
View File
@@ -1,14 +1,14 @@
/**
* The unified view model of the Tasks page: one ordered node list the graph
* canvas AND the tree mode both render (same data, same fold state). Pure
* derivation over the sessions list feed (byId lineage + per-parent
* catalogs), the live activity map, the folded workflow runs, and the
* optional team view — kept framework-free for node-environment unit tests.
* derivation over the sessions list feed (byId lineage + the host's
* per-session `subagentCatalog` projection views), the live activity map, the
* folded workflow runs, and the optional team view — kept framework-free for
* node-environment unit tests.
*
* Shape rules:
* - agent rows walk the lazy per-parent catalogs in pre-order (the same
* recursion the classic tree used); Side Chat threads and diagnostic rows
* never become nodes;
* - agent rows walk the per-parent catalog views in pre-order (the same
* recursion the classic tree used); Side Chat threads never become nodes;
* - a workflow run hangs under its ORIGIN agent; member agents that already
* exist as the origin's catalog children are RE-PARENTED under the run
* node, members without a catalog row are synthesized from the run's own
@@ -22,14 +22,14 @@
import type {
SidebarSessionSummary,
SidebarSubagentAddress,
SidebarSubagentCatalog,
SidebarSubagentChildEntry,
SidebarSubagentCatalogEntry,
SidebarTeamMemberView,
SidebarTeamTaskView,
} from '../context-types.ts'
import type { LastActivity } from '../subagent-activity.ts'
import type { WorkflowRunView } from '../workflow-runs.ts'
import { isSideThreadSummary } from './subagent-detect.ts'
import { childActivity, isKnownLeaf, type SubagentCatalogView } from './subagent-catalog.ts'
/** Display state of one agent node (drives the dot + fold candidacy). */
export type TasksNodeState = 'running' | 'idle' | 'done' | 'error'
@@ -52,7 +52,8 @@ export interface TasksAgentNode {
label: string
/** The summary's display title when it differs from the label. */
title?: string
mode?: 'one-shot' | 'continuable'
/** The catalog row's mode; `unknown` claims neither and renders nothing. */
mode?: SidebarSubagentCatalogEntry['mode']
state: TasksNodeState
/** The catalog's raw activity word (the secondary line localizes it). */
activity: 'running' | 'inactive'
@@ -103,7 +104,8 @@ export type TasksNode = TasksAgentNode | TasksWorkflowNode | TasksFoldNode
/** Inputs of the model build (all already-resolved client mirrors). */
export interface TasksModelInput {
byId: Readonly<Record<string, SidebarSessionSummary>>
catalogs: Readonly<Record<string, SidebarSubagentCatalog | undefined>>
/** Per-parent catalog views (DSH 0.1.7 projectors, see {@link subagentCatalogs}). */
catalogs: Readonly<Record<string, SubagentCatalogView | undefined>>
rootId: string
currentSessionId: string
live: Readonly<Record<string, LastActivity | undefined>>
@@ -117,7 +119,7 @@ export interface TasksModelInput {
/** Human label of one catalog child (the classic rule). */
function childLabel(
entry: SidebarSubagentChildEntry,
entry: SidebarSubagentCatalogEntry,
summary: SidebarSessionSummary | undefined,
): string {
return entry.label ?? summary?.displayTitle ?? entry.id
@@ -196,26 +198,27 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
const knownAgentIds = new Set<string>()
for (const catalog of Object.values(catalogs)) {
if (catalog?.state !== 'ready') continue
for (const entry of catalog.entries) {
if (entry.kind === 'child') knownAgentIds.add(entry.id)
}
for (const entry of catalog.entries) knownAgentIds.add(entry.id)
}
// The local children lists of `appendChildren` are built lazily per parent,
// so this global set is what makes the guard below order-independent.
/** The agent children of one parent, in catalog order (side threads and
* diagnostics excluded), with workflow runs appended in start order. */
/** The agent children of one parent, in catalog order (side threads
* excluded), with workflow runs appended in start order. */
const appendChildren = (parentId: string): void => {
const catalog = catalogs[parentId]
const entries = catalog?.state === 'ready' ? catalog.entries : []
const agentChildren: TasksAgentNode[] = []
for (const entry of entries) {
if (entry.kind !== 'child') continue
const summary = byId[entry.id]
if (summary !== undefined && isSideThreadSummary(summary)) continue
if (entry.label?.startsWith('Side: ') ?? false) continue
const team = teamOf(entry.id)
const state: TasksNodeState = entry.activity === 'running'
// DSH 0.1.7's catalog row carries the identity only: the live channel
// folds RUNNING children (absence = not running) and a row keeps its
// disclosure unless its own catalog is known-empty.
const activity = childActivity(live, entry.id)
const state: TasksNodeState = activity === 'running'
? 'running'
: team !== undefined ? teamState(team.status) : 'done'
agentChildren.push({
@@ -227,12 +230,12 @@ export function buildTasksModel(input: TasksModelInput): TasksNode[] {
? { title: summary.displayTitle } : {}),
mode: entry.mode,
state,
activity: entry.activity,
activity,
current: entry.id === currentSessionId,
...(live[entry.id] !== undefined ? { live: live[entry.id] } : {}),
...(team !== undefined ? { team } : {}),
...(tasksByNode.get(entry.id) !== undefined ? { tasks: tasksByNode.get(entry.id) } : {}),
hasChildren: entry.hasChildren,
hasChildren: !isKnownLeaf(catalogs, entry.id),
childAddress: { parentSessionId: parentId, childSessionId: entry.id, mode: entry.mode },
})
}
+23 -10
View File
@@ -13,7 +13,7 @@
*/
import type { ReactNode } from 'react'
import {
IconAgentPresetOutline16, IconBranchOutline16, IconChecklistOutline14, IconUserOutline16,
IconAgentPresetOutlineRegular, IconBranchOutlineRegular, IconChecklistOutlineRegular, IconUserOutlineRegular,
type StateDotState,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { LastActivity } from '../subagent-activity.ts'
@@ -75,18 +75,33 @@ export function flatten(text: string): string {
/** The host icon of one agent node (the lead and plain children share one). */
export function AgentGlyph(props: { node: TasksAgentNode; size?: number }): ReactNode {
const size = props.size ?? 11
if (props.node.team?.role === 'teammate') return <IconUserOutline16 size={size} />
return <IconAgentPresetOutline16 size={size} />
if (props.node.team?.role === 'teammate') return <IconUserOutlineRegular size={size} />
return <IconAgentPresetOutlineRegular size={size} />
}
/** The workflow-run glyph. */
export function WorkflowGlyph(props: { size?: number }): ReactNode {
return <IconBranchOutline16 size={props.size ?? 11} />
return <IconBranchOutlineRegular size={props.size ?? 11} />
}
/** The fold aggregate glyph. */
export function FoldGlyph(props: { size?: number }): ReactNode {
return <IconChecklistOutline14 size={props.size ?? 11} />
return <IconChecklistOutlineRegular size={props.size ?? 11} />
}
/**
* The mode word of one catalog row, or undefined when it names no mode. DSH
* 0.1.7 added `unknown` (a child the host's catalog fold kept without a
* readable descriptor): it claims NEITHER mode, so the segment is omitted
* rather than mislabelled.
*/
export function modeLabel(mode: TasksAgentNode['mode']): string | undefined {
switch (mode) {
case 'one-shot': return t('subagentModeOneShot')
case 'continuable': return t('subagentModeContinuable')
case 'unknown':
case undefined: return undefined
}
}
/**
@@ -101,10 +116,8 @@ export function agentMeta(node: TasksAgentNode): string {
if (node.team?.role === 'teammate' && node.team.model !== undefined && node.team.model !== '') {
return `${node.team.model} · ${state}`
}
if (node.mode !== undefined) {
return `${node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable')} · ${state}`
}
return state
const mode = modeLabel(node.mode)
return mode === undefined ? state : `${mode} · ${state}`
}
/** The mono meta line of a workflow run card: status · member tally. */
@@ -167,7 +180,7 @@ export function TaskLine(props: {
props.onOpenTask(primary.id, event.currentTarget)
}}
>
<span className={css.taskGlyph} aria-hidden="true"><IconChecklistOutline14 size={9} /></span>
<span className={css.taskGlyph} aria-hidden="true"><IconChecklistOutlineRegular size={9} /></span>
<span className={css.taskSubject}>{primary.subject}</span>
<span className={css.taskState}>{t(taskStatusKey(primary.status))}</span>
{tasks.length > 1 && <span className={css.taskMore}>{`+${tasks.length - 1}`}</span>}
+19 -19
View File
@@ -7,13 +7,13 @@
*/
import type { ReactNode } from 'react'
import {
IconCodeOutline16,
IconEditOutline16,
IconEllipsisOutline16,
IconGlobeOutline14,
IconPlayOutline16,
IconSearchOutline16,
IconThinkOutline16,
IconCodeOutlineRegular,
IconEditOutlineRegular,
IconEllipsisOutlineRegular,
IconGlobeOutlineRegular,
IconPlayOutlineRegular,
IconSearchOutlineRegular,
IconThinkOutlineRegular,
} from '@deepseek-ai/dsh-client-ui-primitives'
/** One glyph renderer (host icon components accept a pixel size). */
@@ -21,21 +21,21 @@ type Glyph = (size: number) => ReactNode
/** The explicit tool-family map (longest-prefix wins at lookup). */
const TOOL_GLYPHS: ReadonlyArray<readonly [string, Glyph]> = [
['read', size => <IconCodeOutline16 size={size} />],
['write', size => <IconEditOutline16 size={size} />],
['edit', size => <IconEditOutline16 size={size} />],
['glob', size => <IconSearchOutline16 size={size} />],
['grep', size => <IconSearchOutline16 size={size} />],
['search', size => <IconSearchOutline16 size={size} />],
['bash', size => <IconPlayOutline16 size={size} />],
['shell', size => <IconPlayOutline16 size={size} />],
['web', size => <IconGlobeOutline14 size={size} />],
['fetch', size => <IconGlobeOutline14 size={size} />],
['think', size => <IconThinkOutline16 size={size} />],
['read', size => <IconCodeOutlineRegular size={size} />],
['write', size => <IconEditOutlineRegular size={size} />],
['edit', size => <IconEditOutlineRegular size={size} />],
['glob', size => <IconSearchOutlineRegular size={size} />],
['grep', size => <IconSearchOutlineRegular size={size} />],
['search', size => <IconSearchOutlineRegular size={size} />],
['bash', size => <IconPlayOutlineRegular size={size} />],
['shell', size => <IconPlayOutlineRegular size={size} />],
['web', size => <IconGlobeOutlineRegular size={size} />],
['fetch', size => <IconGlobeOutlineRegular size={size} />],
['think', size => <IconThinkOutlineRegular size={size} />],
]
/** The fallback glyph for unmapped tools. */
const FALLBACK_GLYPH: Glyph = size => <IconEllipsisOutline16 size={size} />
const FALLBACK_GLYPH: Glyph = size => <IconEllipsisOutlineRegular size={size} />
/**
* The glyph of one tool name: exact match first, then a prefix match so
+8 -8
View File
@@ -390,10 +390,14 @@ export interface SidebarSessionList {
* this plugin reads is `subagentCatalog`, the direct-child list the 0.1.6
* runtime published as `subagentsByParent`.
*
* Two facts the 0.1.6 snapshot carried are gone and must not be re-read:
* there is no `current` session id (`ctx.sidebarRight.mounted` is the
* sanctioned feed for "which session's seat is on screen"), and there is no
* background-jobs mirror (the `jobs.list` route reads the registry itself).
* Three facts the 0.1.6 snapshot carried are gone and must not be brought
* back: there is no `current` session id (`ctx.sidebarRight.mounted` is the
* sanctioned feed for "which session's seat is on screen"), there is no
* background-jobs mirror (the `jobs.list` route reads the registry itself),
* and there is no per-parent observe handshake — 0.1.7 loads every session's
* projections once per connection, so a catalog surface reads them instead
* of observing and unobserving (0.1.6's `setSubagentCatalogOpen` is deleted,
* not renamed).
*/
projectionsBySession?: Readonly<Record<string, SidebarProjectionSnapshot>>
}
@@ -460,10 +464,6 @@ export interface SidebarSessionsService {
* Resolve an already discovered direct-parent address without opening it.
*/
subagentAddress?(id: string): SidebarSubagentAddress | undefined
/**
* Mark whether a catalog surface is consuming live membership updates.
*/
setSubagentCatalogOpen?(parentSessionId: string, open: boolean): void
/**
* Refresh one direct-child catalog.
*/