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:
@@ -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()
|
||||
|
||||
@@ -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
@@ -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')}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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
@@ -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')}
|
||||
|
||||
@@ -333,7 +333,6 @@ export const ar: Record<string, string> = {
|
||||
codeBlockUnwrap: 'بدون التفاف للأسطر',
|
||||
sideChatBlockWindow: '{shown} من {total} سطرًا',
|
||||
sideChatConnDisconnected: 'انقطع الاتصال',
|
||||
sideChatConnReconnect: 'إعادة الاتصال',
|
||||
sideChatConnConnecting: 'جارٍ إعادة الاتصال…',
|
||||
sideChatConnRecovered: 'تم استعادة الاتصال',
|
||||
sideChatConnReconnectAction: 'إعادة الاتصال الآن',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -332,7 +332,6 @@ export const hi: Record<string, string> = {
|
||||
codeBlockUnwrap: 'पंक्तियाँ न लपेटें',
|
||||
sideChatBlockWindow: 'कुल {total} में से {shown} पंक्तियाँ',
|
||||
sideChatConnDisconnected: 'कनेक्शन टूट गया',
|
||||
sideChatConnReconnect: 'फिर से कनेक्ट करें',
|
||||
sideChatConnConnecting: 'फिर से कनेक्ट हो रहा है…',
|
||||
sideChatConnRecovered: 'कनेक्शन बहाल हुआ',
|
||||
sideChatConnReconnectAction: 'अभी फिर से कनेक्ट करें',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -332,7 +332,6 @@ export const ja: Record<string, string> = {
|
||||
codeBlockUnwrap: '折り返しを無効にする',
|
||||
sideChatBlockWindow: '全 {total} 行中 {shown} 行を表示',
|
||||
sideChatConnDisconnected: '接続が切断されました。クリックで再接続',
|
||||
sideChatConnReconnect: '再接続',
|
||||
sideChatConnConnecting: '再接続中…',
|
||||
sideChatConnRecovered: '接続が復旧しました',
|
||||
sideChatConnReconnectAction: '今すぐ再接続',
|
||||
|
||||
@@ -324,7 +324,6 @@ export const ko: Record<string, string> = {
|
||||
codeBlockUnwrap: '줄 바꿈 해제',
|
||||
sideChatBlockWindow: '전체 {total}줄 중 {shown}줄 표시',
|
||||
sideChatConnDisconnected: '연결이 끊겼습니다',
|
||||
sideChatConnReconnect: '다시 연결',
|
||||
sideChatConnConnecting: '다시 연결 중…',
|
||||
sideChatConnRecovered: '연결이 복원되었습니다',
|
||||
sideChatConnReconnectAction: '지금 다시 연결',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -330,7 +330,6 @@ export const ru: Record<string, string> = {
|
||||
codeBlockUnwrap: 'Не переносить строки',
|
||||
sideChatBlockWindow: '{shown} из {total} строк',
|
||||
sideChatConnDisconnected: 'Соединение потеряно',
|
||||
sideChatConnReconnect: 'Переподключиться',
|
||||
sideChatConnConnecting: 'Переподключение…',
|
||||
sideChatConnRecovered: 'Соединение восстановлено',
|
||||
sideChatConnReconnectAction: 'Переподключиться сейчас',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -332,7 +332,6 @@ export const th: Record<string, string> = {
|
||||
codeBlockUnwrap: 'ไม่ตัดบรรทัด',
|
||||
sideChatBlockWindow: '{shown} จาก {total} บรรทัด',
|
||||
sideChatConnDisconnected: 'การเชื่อมต่อขาดหาย',
|
||||
sideChatConnReconnect: 'เชื่อมต่อใหม่',
|
||||
sideChatConnConnecting: 'กำลังเชื่อมต่อใหม่…',
|
||||
sideChatConnRecovered: 'กู้คืนการเชื่อมต่อแล้ว',
|
||||
sideChatConnReconnectAction: 'เชื่อมต่อใหม่ตอนนี้',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -347,7 +347,6 @@ export const zhHK: Record<string, string> = {
|
||||
codeBlockUnwrap: '取消自動換行',
|
||||
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
|
||||
sideChatConnDisconnected: '連線已中斷',
|
||||
sideChatConnReconnect: '重新連線',
|
||||
sideChatConnConnecting: '正在重新連線…',
|
||||
sideChatConnRecovered: '連線已恢復',
|
||||
sideChatConnReconnectAction: '立即重新連線',
|
||||
|
||||
@@ -347,7 +347,6 @@ export const zhMO: Record<string, string> = {
|
||||
codeBlockUnwrap: '取消自動換行',
|
||||
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
|
||||
sideChatConnDisconnected: '連線已中斷',
|
||||
sideChatConnReconnect: '重新連線',
|
||||
sideChatConnConnecting: '正在重新連線…',
|
||||
sideChatConnRecovered: '連線已恢復',
|
||||
sideChatConnReconnectAction: '立即重新連線',
|
||||
|
||||
@@ -347,7 +347,6 @@ export const zhTW: Record<string, string> = {
|
||||
codeBlockUnwrap: '取消自動換行',
|
||||
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
|
||||
sideChatConnDisconnected: '連線已中斷',
|
||||
sideChatConnReconnect: '重新連線',
|
||||
sideChatConnConnecting: '正在重新連線…',
|
||||
sideChatConnRecovered: '連線已恢復',
|
||||
sideChatConnReconnectAction: '立即重新連線',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user