feat(client): workflow-graph Tasks page with teams board and jobs drawer
The Subagent page becomes the Tasks page per the approved Variant-D design: - tasks-model.ts feeds BOTH modes from one pre-order node list: graph canvas (TasksGraph: layered nodes over bezier edges, drag-pan, wheel-zoom-to-cursor, phase frames, bottom-right control cluster) and the classic tree (TasksTree: keyboard navigable) — the view toggle and fold toggle stay reachable in both modes. - Workflow runs (workflows.list) hang under their origin agent; member agents re-parent under the run node, missing ones synthesize from run data; completed leaf agents fold into per-parent aggregate nodes (click or cluster toggle to expand/re-collapse). - Agent Teams (experimental): roster enriches nodes; a header chip opens the shared task board popover with basic CAS operations (complete / reopen / delete / reassign / create / edit); no experimental layer → the whole block hides (structural degradation). - Background jobs move into a bottom drawer that auto-collapses at 8+ agents (manual toggle wins); job output opens as an anchored popover (event replay, never the model's cursor); two-click kill preserved. - Live activity lines render as glyph + tool + args (tool-icons map onto host primitive icons). - Node click jumps to the transcript; ⓘ opens the detail popover; popovers follow the selection-popup dismissal contract (Escape / outside click / anchor off-screen via IntersectionObserver). - tasksViewMode pref (graph default) with a select row in settings; 58 new copy keys across zh/en/ja + 18 third-language dictionaries; dead CSS from the old tree/dock pruned. - Tests: 6 new page-interaction specs (auto-collapse, both-mode toggle, jump, fold, popover); jobs-view suite re-pointed at the popover; prefs/builtins/smoke lock tests updated for the new pref.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* The anchored floating popover of the Tasks page (node details, job output,
|
||||
* workflow run detail, team task editor): a viewport-anchored card portaled
|
||||
* to `document.body`, positioned below its anchor and flipped above when the
|
||||
* viewport bottom would clip it. Dismissal follows the proven
|
||||
* selection-popup contract (fixes upstream issue #425): outside mousedown,
|
||||
* Escape, document hidden, window blur, and the ANCHOR leaving the viewport
|
||||
* (tab switches flip the pane to display:none, which has no DOM event — the
|
||||
* IntersectionObserver geometry signal is the only reliable one).
|
||||
*/
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
/** The popover's visual width (viewport-clamped). */
|
||||
const POP_WIDTH = 264
|
||||
|
||||
export interface AnchoredPopoverProps {
|
||||
/** The anchor element (null = closed). */
|
||||
anchor: HTMLElement | null
|
||||
/** Dismiss (outside click / Escape / anchor off-screen). */
|
||||
onClose(): void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a viewport-anchored popover. The caller owns WHAT is shown inside;
|
||||
* this component owns geometry + dismissal. Re-anchoring while open (a new
|
||||
* anchor) re-measures and keeps the card on screen.
|
||||
*/
|
||||
export function AnchoredPopover(props: AnchoredPopoverProps): ReactNode {
|
||||
const { anchor, onClose, children } = props
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const onCloseRef = useRef(onClose)
|
||||
onCloseRef.current = onClose
|
||||
const [pos, setPos] = useState<{ left: number; top: number } | null>(null)
|
||||
|
||||
// Measure + position once the card is in the DOM (and whenever the anchor
|
||||
// or content changes size). layout effect: no visible jump.
|
||||
useLayoutEffect(() => {
|
||||
if (anchor === null) { setPos(null); return }
|
||||
const card = cardRef.current
|
||||
if (card === null) return
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const width = Math.min(POP_WIDTH, window.innerWidth - 16)
|
||||
const height = card.offsetHeight
|
||||
let top = rect.bottom + 6
|
||||
if (top + height > window.innerHeight - 8) {
|
||||
top = Math.max(8, rect.top - height - 6)
|
||||
}
|
||||
const left = Math.min(Math.max(8, rect.left), window.innerWidth - width - 8)
|
||||
setPos({ left, top })
|
||||
}, [anchor, children])
|
||||
|
||||
// Global dismissal (see the file header for the contract).
|
||||
useEffect(() => {
|
||||
if (anchor === null) return
|
||||
const onMouseDown = (event: MouseEvent): void => {
|
||||
const card = cardRef.current
|
||||
if (card !== null && (card === event.target || card.contains(event.target as Node))) return
|
||||
if (anchor === event.target || anchor.contains(event.target as Node)) return
|
||||
onCloseRef.current()
|
||||
}
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key === 'Escape') onCloseRef.current()
|
||||
}
|
||||
const onHidden = (): void => { if (document.hidden) onCloseRef.current() }
|
||||
const onBlur = (): void => { onCloseRef.current() }
|
||||
document.addEventListener('mousedown', onMouseDown, true)
|
||||
document.addEventListener('keydown', onKeyDown, true)
|
||||
document.addEventListener('visibilitychange', onHidden)
|
||||
window.addEventListener('blur', onBlur)
|
||||
// The anchor leaving the viewport (tab switch / panel collapse) closes.
|
||||
let observer: IntersectionObserver | undefined
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) if (!entry.isIntersecting) onCloseRef.current()
|
||||
}, { threshold: 0 })
|
||||
observer.observe(anchor)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onMouseDown, true)
|
||||
document.removeEventListener('keydown', onKeyDown, true)
|
||||
document.removeEventListener('visibilitychange', onHidden)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
observer?.disconnect()
|
||||
}
|
||||
}, [anchor])
|
||||
|
||||
if (anchor === null) return null
|
||||
return createPortal(
|
||||
<div
|
||||
ref={cardRef}
|
||||
role="dialog"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: pos?.left ?? -9999,
|
||||
top: pos?.top ?? -9999,
|
||||
width: Math.min(POP_WIDTH, window.innerWidth - 16),
|
||||
zIndex: 90,
|
||||
visibility: pos === null ? 'hidden' : 'visible',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* The background-jobs bottom drawer of the Tasks page: the tree's jobs
|
||||
* (owner-labeled, fed by the `session/jobs` push mirror) collapse into a
|
||||
* bottom bar that AUTO-COLLAPSES when the agent count crosses the page's
|
||||
* threshold — the manual toggle always wins afterwards. Job output no
|
||||
* longer docks inside the page: clicking a row opens an anchored popover
|
||||
* (event replay — never the model's job_output cursor). Live rows keep the
|
||||
* two-click-confirm kill.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { IconChevronUpOutline14, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SidebarJobView } from '../context-types.ts'
|
||||
import {
|
||||
formatJobDuration,
|
||||
isJobLive,
|
||||
jobDotState,
|
||||
jobStatusLabel,
|
||||
type TreeJob,
|
||||
} from './subagent-jobs.ts'
|
||||
import { api, type JobOutputResult } from './api.ts'
|
||||
import { IconStopOutline16 } from './icons.tsx'
|
||||
import { t } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** Refresh cadence of an open job-output popover while its job runs. */
|
||||
const JOB_POLL_MS = 2000
|
||||
/** How long the kill button stays armed before it needs re-confirming. */
|
||||
const JOB_KILL_ARM_MS = 3000
|
||||
|
||||
export interface JobsDrawerProps {
|
||||
rows: readonly TreeJob[]
|
||||
/** Agent count of the tree (root included) — the auto-collapse signal. */
|
||||
agentCount: number
|
||||
/** The page is visible (active tab + open panel): skip polling otherwise. */
|
||||
active: boolean
|
||||
/** Open the output popover of one row (anchor = the row's main button). */
|
||||
onOpenOutput(row: TreeJob, anchor: HTMLElement): void
|
||||
}
|
||||
|
||||
/** The agent count at which the drawer starts collapsed. */
|
||||
export const JOBS_DRAWER_COLLAPSE_AT = 8
|
||||
|
||||
export function JobsDrawer(props: JobsDrawerProps): ReactNode {
|
||||
const { rows, agentCount, active, onOpenOutput } = props
|
||||
const autoOpen = agentCount < JOBS_DRAWER_COLLAPSE_AT
|
||||
/** Manual override; undefined = follow the auto rule. */
|
||||
const [manualOpen, setManualOpen] = useState<boolean | undefined>(undefined)
|
||||
const open = manualOpen ?? autoOpen
|
||||
const [armedId, setArmedId] = useState<string | undefined>(undefined)
|
||||
const [killingId, setKillingId] = useState<string | undefined>(undefined)
|
||||
const [killErrorId, setKillErrorId] = useState<string | undefined>(undefined)
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
const liveCount = useMemo(
|
||||
() => rows.reduce((count, row) => count + (isJobLive(row.job) ? 1 : 0), 0),
|
||||
[rows],
|
||||
)
|
||||
const multiOwner = useMemo(
|
||||
() => new Set(rows.map(row => row.ownerSessionId)).size > 1,
|
||||
[rows],
|
||||
)
|
||||
|
||||
// The kill button stays armed only briefly; a stray click must never kill.
|
||||
useEffect(() => {
|
||||
if (armedId === undefined) return
|
||||
const timer = window.setTimeout(() => { setArmedId(undefined) }, JOB_KILL_ARM_MS)
|
||||
return () => { window.clearTimeout(timer) }
|
||||
}, [armedId])
|
||||
|
||||
useEffect(() => {
|
||||
if (liveCount === 0) return
|
||||
setNow(Date.now())
|
||||
const timer = window.setInterval(() => { setNow(Date.now()) }, 1_000)
|
||||
return () => { window.clearInterval(timer) }
|
||||
}, [liveCount])
|
||||
|
||||
const kill = useCallback(async (row: TreeJob): Promise<void> => {
|
||||
setKillingId(row.job.id)
|
||||
setKillErrorId(undefined)
|
||||
try {
|
||||
await api.jobKill({ sessionId: row.ownerSessionId }, row.job.id)
|
||||
} catch {
|
||||
setKillErrorId(row.job.id)
|
||||
} finally {
|
||||
setKillingId(undefined)
|
||||
setArmedId(undefined)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (rows.length === 0) return null
|
||||
|
||||
const countLabel = liveCount > 0
|
||||
? t('jobsCountRunning', { count: rows.length, running: liveCount })
|
||||
: t('jobsCount', { count: rows.length })
|
||||
|
||||
return (
|
||||
<section className={css.jobsDrawer} aria-label={t('jobs')}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.jobsDrawerBar}
|
||||
aria-expanded={open}
|
||||
onClick={() => { setManualOpen(!open) }}
|
||||
>
|
||||
<span>{t('jobs')}</span>
|
||||
<span className={css.jobsDrawerCount}>{countLabel}</span>
|
||||
{!open && !autoOpen && <span className={css.jobsDrawerAuto}>{t('jobsAutoCollapsed')}</span>}
|
||||
<span className={clsx(css.jobsDrawerChevron, open && css.jobsDrawerChevronOpen)} aria-hidden="true">
|
||||
<IconChevronUpOutline14 size={12} />
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className={css.jobsDrawerBody}>
|
||||
{rows.map((row) => {
|
||||
const { job } = row
|
||||
const live = isJobLive(job)
|
||||
const armed = armedId === job.id
|
||||
const killing = killingId === job.id
|
||||
const killFailed = killErrorId === job.id
|
||||
const elapsed = live
|
||||
? now - job.startedAt
|
||||
: (job.finishedAt ?? job.startedAt) - job.startedAt
|
||||
const secondary = [
|
||||
...(multiOwner ? [row.ownerTitle] : []),
|
||||
jobStatusLabel(job.status, t),
|
||||
...(job.detail !== undefined && job.detail !== '' ? [job.detail] : []),
|
||||
formatJobDuration(elapsed, t),
|
||||
].filter(Boolean).join(' · ')
|
||||
return (
|
||||
<div key={job.id} className={clsx(css.jobsRow, !live && css.jobsRowSettled)}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.jobsRowMain}
|
||||
aria-label={`${job.label} ${secondary}`}
|
||||
title={t('jobViewOutput')}
|
||||
onClick={(event) => { onOpenOutput(row, event.currentTarget) }}
|
||||
>
|
||||
<StateDot state={jobDotState(job.status)} size={8} />
|
||||
<span className={css.jobsContent}>
|
||||
<span className={css.jobsLabelLine}>
|
||||
<span className={css.jobsKind}>{job.kind}</span>
|
||||
<span className={css.jobsLabel} title={job.label}>{job.label}</span>
|
||||
</span>
|
||||
<span className={css.jobsSecondary}>{secondary}</span>
|
||||
</span>
|
||||
</button>
|
||||
{job.status === 'running' && (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.jobsKill, armed && css.jobsKillArmed)}
|
||||
aria-label={armed ? t('jobKillConfirm') : t('jobKill')}
|
||||
title={armed ? t('jobKillConfirm') : t('jobKill')}
|
||||
disabled={killing}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
if (armed) void kill(row)
|
||||
else setArmedId(job.id)
|
||||
}}
|
||||
>
|
||||
{armed ? t('jobKillConfirm') : <IconStopOutline16 size={12} />}
|
||||
</button>
|
||||
)}
|
||||
{killFailed && <span className={css.jobsKillError}>{t('jobKillError')}</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The output popover content of one job: the text the MODEL has read so far
|
||||
* (replayed from the owner session's event log), refreshed every
|
||||
* {@link JOB_POLL_MS} while the job runs and the page is visible, pinned to
|
||||
* the newest output like a terminal tail while live.
|
||||
*/
|
||||
export function JobOutputPopoverContent(props: {
|
||||
ownerSessionId: string
|
||||
job: SidebarJobView
|
||||
active: boolean
|
||||
}): ReactNode {
|
||||
const { ownerSessionId, job, active } = props
|
||||
const [state, setState] = useState<'loading' | JobOutputResult | 'error'>('loading')
|
||||
const controllerRef = useRef<AbortController | undefined>(undefined)
|
||||
const preRef = useRef<HTMLPreElement>(null)
|
||||
|
||||
const load = useCallback(async (): Promise<void> => {
|
||||
controllerRef.current?.abort()
|
||||
const controller = new AbortController()
|
||||
controllerRef.current = controller
|
||||
try {
|
||||
const result = await api.jobOutput({ sessionId: ownerSessionId }, job.id, controller.signal)
|
||||
setState(result)
|
||||
} catch {
|
||||
// A newer pull aborted this one, or the wire failed: keep the last
|
||||
// known output; only a popover that never loaded anything shows an error.
|
||||
setState(current => (current === 'loading' ? 'error' : current))
|
||||
}
|
||||
}, [ownerSessionId, job.id])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
if (!active || !isJobLive(job)) return
|
||||
const timer = window.setInterval(() => { void load() }, JOB_POLL_MS)
|
||||
return () => { window.clearInterval(timer) }
|
||||
// isJobLive reads only job.status; whole-job identity churns on every
|
||||
// catalog refresh and must not restart the poll interval.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [load, active, job.status])
|
||||
|
||||
useEffect(() => () => { controllerRef.current?.abort() }, [])
|
||||
|
||||
// Terminal-tail behavior: while the job runs, each refresh pins the view
|
||||
// to the newest output; a settled popover leaves scrolling to the reader.
|
||||
useEffect(() => {
|
||||
if (!isJobLive(job) || typeof state !== 'object' || state.text.length === 0) return
|
||||
const pre = preRef.current
|
||||
if (pre !== null) pre.scrollTop = pre.scrollHeight
|
||||
// Same as the poll effect above: only the status transition matters.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state, job.status])
|
||||
|
||||
return (
|
||||
<div className={css.popCard} style={{ width: 340 }}>
|
||||
<span className={css.popTitle} title={job.label}>
|
||||
<StateDot state={jobDotState(job.status)} size={8} /> {job.label}
|
||||
</span>
|
||||
<span className={css.popHint}>
|
||||
{jobStatusLabel(job.status, t)}
|
||||
{job.detail !== undefined && job.detail !== '' ? ` · ${job.detail}` : ''}
|
||||
</span>
|
||||
{state === 'loading' && <div className={css.popHint}>{t('loading')}</div>}
|
||||
{state === 'error' && <div className={css.popError}>{t('jobOutputError')}</div>}
|
||||
{typeof state === 'object' && (
|
||||
<>
|
||||
{state.text.length > 0
|
||||
? <pre ref={preRef} className={css.popPre}>{state.text}</pre>
|
||||
: state.read
|
||||
? <div className={css.popHint}>{t('jobNoOutput')}</div>
|
||||
: <div className={css.popHint}>{t('jobNotReadYet')}</div>}
|
||||
{state.truncated && <div className={css.popHint}>{t('jobOutputTruncated')}</div>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Subagent page: main-agent topology tree. Row/card chrome mirrors the
|
||||
official ui-subagent catalog row (min-height 50, 7/8/11 padding, radius 8,
|
||||
flex-start alignment, tree connector lines via border-left hairlines). */
|
||||
/* Tasks page shell: header row, empty/error states, and the state dot. The
|
||||
graph canvas / tree rows / popovers / jobs drawer live in
|
||||
tasks-graph.module.css; the old indentation-tree and jobs-dock classes
|
||||
were pruned when the page moved to the unified model. */
|
||||
.subagent {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -52,33 +53,6 @@
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.subagentBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 2px 6px 8px;
|
||||
}
|
||||
|
||||
/* One node card: state dot + label + secondary + (running) live lines. */
|
||||
.subagentRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: 7px 8px 7px 11px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
font: var(--dsw-font-s-14);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.subagentRow:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
@@ -97,102 +71,24 @@
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
}
|
||||
|
||||
.subagentRowDisabled {
|
||||
color: var(--dsw-alias-label-dimmed);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.subagentRowDisabled:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Loading placeholders are disabled but keep the default cursor (official
|
||||
`.loadingRow` distinction from `.disabled`). */
|
||||
.subagentRowLoading {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* The state dot aligns to the first text line (official catalog detail). */
|
||||
.subagentDot {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.subagentContent {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.subagentLabel,
|
||||
.subagentSecondary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subagentLabel {
|
||||
color: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.subagentSecondary {
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* ── Live lines (running cards only) ──────────────────────────────────── */
|
||||
|
||||
.subagentLive {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.subagentLiveTool {
|
||||
flex: none;
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.subagentLiveArgs {
|
||||
min-width: 0;
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subagentLiveText {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* ── Tree connector lines (official catalog recipe) ────────────────────── */
|
||||
|
||||
.subagentNode {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subagentChildren {
|
||||
position: relative;
|
||||
margin-left: 18px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.subagentChildren::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -276,157 +172,25 @@
|
||||
|
||||
/* ── Background-job section (below the topology tree) ────────────────── */
|
||||
|
||||
.jobs {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.jobsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 26px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.jobsTitle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jobsCount {
|
||||
flex: none;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.jobsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* One job row; the whole row lights up on hover, the expandable target
|
||||
stays transparent (the kill button rides on the row's right edge). */
|
||||
.jobsRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.jobsRow:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.jobsRowSettled {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* The row whose output is shown in the bottom dock. */
|
||||
.jobsRowSelected,
|
||||
.jobsRowSelected:hover {
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
}
|
||||
|
||||
.jobsRowMain {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 6px 8px 6px 11px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
font: var(--dsw-font-s-14);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.jobsRowMain:focus-visible {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.jobsDot {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.jobsContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
/* The command line: a kind badge followed by the command itself in the code
|
||||
face, like the terminal's live lines. */
|
||||
.jobsLabelLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jobsKind {
|
||||
flex: none;
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0 5px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 4px;
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
line-height: 14px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.jobsLabel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.jobsSecondary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.jobsKill {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 4px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jobsKill:hover {
|
||||
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);
|
||||
@@ -450,98 +214,13 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.jobsKillError {
|
||||
flex: none;
|
||||
margin-right: 4px;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
/* ── The shared output dock (bottom of the page, sticky like a terminal) ── */
|
||||
|
||||
/* Direct child of the scrolling body, so it docks at the scrollport's bottom
|
||||
edge while the job list scrolls above it; a solid background keeps the
|
||||
tree from showing through. */
|
||||
.jobsPane {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
margin-top: 4px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 8px;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
box-shadow: 0 -6px 12px -8px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.jobsPaneHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 28px;
|
||||
padding: 0 4px 0 10px;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
||||
}
|
||||
|
||||
.jobsPaneDot {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.jobsPaneLabel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.jobsPaneStatus {
|
||||
flex: none;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.jobsPaneClose {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jobsPaneClose:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.jobsPanePre {
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding: 6px 10px;
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.jobsPaneHint {
|
||||
padding: 8px 10px;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.jobsPaneError {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
+358
-763
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* The workflow-graph mode of the Tasks page: the approved Variant D canvas —
|
||||
* layered agent/workflow nodes over bezier edges, pointer-drag pan,
|
||||
* wheel-zoom-to-cursor, per-phase dashed frames, fold aggregate nodes, and a
|
||||
* bottom-right control cluster (view toggle / zoom out / percentage / zoom
|
||||
* in / fit / fold toggle). The cluster lives OUTSIDE the scroll surface so
|
||||
* the view toggle stays reachable in both modes (a lesson of the mockup).
|
||||
*
|
||||
* The layout comes from the pure `layoutTasksGraph`; this component owns
|
||||
* only the transform state and DOM wiring.
|
||||
*/
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { IconChevronUpOutline14, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TasksAgentNode, TasksFoldNode, TasksNode, TasksWorkflowNode } from './tasks-model.ts'
|
||||
import { tasksEdges } from './tasks-model.ts'
|
||||
import { GRAPH_NODE_H, GRAPH_NODE_W, layoutTasksGraph, type GraphBox } from './tasks-graph-layout.ts'
|
||||
import { agentSecondary, foldPreviews, LiveLine, nodeDotState } from './tasks-shared.tsx'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** The zoom bounds of the canvas. */
|
||||
const ZOOM_MIN = 0.15
|
||||
const ZOOM_MAX = 2.5
|
||||
/** Drag-vs-click separation: pointer travel below this stays a click. */
|
||||
const CLICK_TOLERANCE_PX = 4
|
||||
|
||||
/** The workflow run status label key. */
|
||||
function workflowStatusKey(status: TasksWorkflowNode['run']['status']): CopyKey {
|
||||
switch (status) {
|
||||
case 'running': return 'workflowRunning'
|
||||
case 'completed': return 'workflowCompleted'
|
||||
case 'cancelled': return 'workflowCancelled'
|
||||
case 'failed': return 'workflowFailed'
|
||||
}
|
||||
}
|
||||
|
||||
/** The member tally of a run (`done` = every member with an outcome). */
|
||||
function memberTally(run: TasksWorkflowNode['run']): { done: number; total: number } {
|
||||
let done = 0
|
||||
let total = 0
|
||||
for (const phase of run.phases) {
|
||||
for (const member of phase.members) {
|
||||
total += 1
|
||||
if (member.outcome !== undefined) done += 1
|
||||
}
|
||||
}
|
||||
return { done, total }
|
||||
}
|
||||
|
||||
/** One phase frame of a run node (the dashed box behind its members). */
|
||||
interface PhaseFrame {
|
||||
key: string
|
||||
title: string | undefined
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export interface TasksGraphProps {
|
||||
nodes: readonly TasksNode[]
|
||||
folded: boolean
|
||||
/** Re-fit trigger: the topology root id (a tree switch re-centers). */
|
||||
rootId: string | undefined
|
||||
onActivate(node: TasksAgentNode): void
|
||||
onNodeInfo(node: TasksAgentNode, anchor: HTMLElement): void
|
||||
onWorkflowInfo(node: TasksWorkflowNode, anchor: HTMLElement): void
|
||||
onToggleFold(): void
|
||||
mode: 'graph' | 'tree'
|
||||
onModeChange(mode: 'graph' | 'tree'): void
|
||||
}
|
||||
|
||||
/** The shared view-mode toggle button (also used by the tree mode). */
|
||||
export function ViewModeToggle(props: {
|
||||
mode: 'graph' | 'tree'
|
||||
onModeChange(mode: 'graph' | 'tree'): void
|
||||
}): ReactNode {
|
||||
const { mode, onModeChange } = props
|
||||
const toTree = mode === 'graph'
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={css.controlBtn}
|
||||
aria-label={t(toTree ? 'tasksViewSwitchToTree' : 'tasksViewSwitchToGraph')}
|
||||
title={t(toTree ? 'tasksViewSwitchToTree' : 'tasksViewSwitchToGraph')}
|
||||
onClick={() => { onModeChange(toTree ? 'tree' : 'graph') }}
|
||||
>
|
||||
{toTree ? '☰' : '⌗'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/** The fold toggle button (shared by both modes). */
|
||||
export function FoldToggleButton(props: { folded: boolean; onToggleFold(): void }): ReactNode {
|
||||
const { folded, onToggleFold } = props
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.controlBtn, folded && css.controlBtnActive)}
|
||||
aria-pressed={folded}
|
||||
aria-label={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
|
||||
title={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
|
||||
onClick={onToggleFold}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{ display: 'inline-flex', transform: folded ? 'rotate(90deg)' : 'rotate(180deg)' }}
|
||||
>
|
||||
<IconChevronUpOutline14 size={12} />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function TasksGraph(props: TasksGraphProps): ReactNode {
|
||||
const { nodes, folded, rootId, onActivate, onNodeInfo, onWorkflowInfo, onToggleFold, mode, onModeChange } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [tf, setTf] = useState({ x: 0, y: 0, k: 1 })
|
||||
const [dragging, setDragging] = useState(false)
|
||||
/** Pointer travel of the in-flight gesture (click suppression). */
|
||||
const travelRef = useRef(0)
|
||||
const tfRef = useRef(tf)
|
||||
tfRef.current = tf
|
||||
|
||||
const layout = useMemo(() => layoutTasksGraph(nodes), [nodes])
|
||||
const edges = useMemo(() => tasksEdges(nodes), [nodes])
|
||||
const nodeById = useMemo(() => new Map(nodes.map(node => [node.id, node])), [nodes])
|
||||
|
||||
/** Fit the whole canvas into the container (initial mount + ⌂ + reroot). */
|
||||
const fit = useCallback((): void => {
|
||||
const container = containerRef.current
|
||||
if (container === null) return
|
||||
const cw = container.clientWidth
|
||||
const ch = container.clientHeight
|
||||
if (cw === 0 || ch === 0) return
|
||||
const k = Math.min((cw - 40) / layout.width, (ch - 40) / layout.height, 1)
|
||||
setTf({ x: (cw - layout.width * k) / 2, y: 12, k })
|
||||
}, [layout.width, layout.height])
|
||||
|
||||
useLayoutEffect(() => { fit() }, [fit, rootId])
|
||||
|
||||
// Wheel-zoom to cursor (non-passive: the page must not scroll).
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (container === null) return
|
||||
const onWheel = (event: WheelEvent): void => {
|
||||
event.preventDefault()
|
||||
const rect = container.getBoundingClientRect()
|
||||
const mx = event.clientX - rect.left
|
||||
const my = event.clientY - rect.top
|
||||
const { x, y, k } = tfRef.current
|
||||
const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, k * Math.exp(-event.deltaY * 0.0014)))
|
||||
if (next === k) return
|
||||
setTf({
|
||||
x: mx - (mx - x) * (next / k),
|
||||
y: my - (my - y) * (next / k),
|
||||
k: next,
|
||||
})
|
||||
}
|
||||
container.addEventListener('wheel', onWheel, { passive: false })
|
||||
return () => { container.removeEventListener('wheel', onWheel) }
|
||||
}, [])
|
||||
|
||||
const zoomBy = useCallback((factor: number): void => {
|
||||
const container = containerRef.current
|
||||
const { x, y, k } = tfRef.current
|
||||
const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, k * factor))
|
||||
if (next === k || container === null) return
|
||||
const mx = container.clientWidth / 2
|
||||
const my = container.clientHeight / 2
|
||||
setTf({ x: mx - (mx - x) * (next / k), y: my - (my - y) * (next / k), k: next })
|
||||
}, [])
|
||||
|
||||
// Drag-pan: pointer capture on the container, any start point pans.
|
||||
const onPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>): void => {
|
||||
if (event.button !== 0) return
|
||||
travelRef.current = 0
|
||||
const startX = event.clientX
|
||||
const startY = event.clientY
|
||||
const { x, y } = tfRef.current
|
||||
const target = event.currentTarget
|
||||
target.setPointerCapture(event.pointerId)
|
||||
const onMove = (move: globalThis.PointerEvent): void => {
|
||||
const dx = move.clientX - startX
|
||||
const dy = move.clientY - startY
|
||||
travelRef.current = Math.max(travelRef.current, Math.abs(dx) + Math.abs(dy))
|
||||
setTf(current => ({ ...current, x: x + dx, y: y + dy }))
|
||||
if (travelRef.current > CLICK_TOLERANCE_PX) setDragging(true)
|
||||
}
|
||||
const onUp = (): void => {
|
||||
target.removeEventListener('pointermove', onMove)
|
||||
target.removeEventListener('pointerup', onUp)
|
||||
target.removeEventListener('pointercancel', onUp)
|
||||
setDragging(false)
|
||||
}
|
||||
target.addEventListener('pointermove', onMove)
|
||||
target.addEventListener('pointerup', onUp)
|
||||
target.addEventListener('pointercancel', onUp)
|
||||
}, [])
|
||||
|
||||
/** Suppress node clicks that ended a drag gesture. */
|
||||
const clickAllowed = useCallback((): boolean => travelRef.current <= CLICK_TOLERANCE_PX, [])
|
||||
|
||||
// Phase frames: group each run's members by phase and box them.
|
||||
const phaseFrames = useMemo((): PhaseFrame[] => {
|
||||
const frames: PhaseFrame[] = []
|
||||
for (const node of nodes) {
|
||||
if (node.kind !== 'workflow') continue
|
||||
const runNode = node
|
||||
runNode.run.phases.forEach((phase, phaseIndex) => {
|
||||
const boxes: GraphBox[] = []
|
||||
for (const member of phase.members) {
|
||||
const id = member.childId !== '' ? member.childId : `wfmember:${runNode.run.runId}:${member.seq}`
|
||||
const box = layout.boxes.get(id)
|
||||
if (box !== undefined) boxes.push(box)
|
||||
}
|
||||
if (boxes.length === 0) return
|
||||
const x1 = Math.min(...boxes.map(box => box.x)) - 12
|
||||
const y1 = Math.min(...boxes.map(box => box.y)) - 22
|
||||
const x2 = Math.max(...boxes.map(box => box.x + box.w)) + 12
|
||||
const y2 = Math.max(...boxes.map(box => box.y + box.h)) + 12
|
||||
frames.push({
|
||||
key: `${runNode.id}:${phaseIndex}`,
|
||||
title: phase.title,
|
||||
x: x1, y: y1, w: x2 - x1, h: y2 - y1,
|
||||
})
|
||||
})
|
||||
}
|
||||
return frames
|
||||
}, [nodes, layout])
|
||||
|
||||
/** The bezier edge path between two boxes (top-down). */
|
||||
const edgePath = (from: GraphBox, to: GraphBox): string => {
|
||||
const x1 = from.x + from.w / 2
|
||||
const y1 = from.y + from.h
|
||||
const x2 = to.x + to.w / 2
|
||||
const y2 = to.y
|
||||
const midY = (y1 + y2) / 2
|
||||
return `M ${x1} ${y1} C ${x1} ${midY} ${x2} ${midY} ${x2} ${y2}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css.graphView}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(css.canvas, dragging && css.canvasDragging)}
|
||||
role="group"
|
||||
aria-label={t('tasksViewGraph')}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<div
|
||||
className={css.canvasInner}
|
||||
style={{
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
transform: `translate(${tf.x}px, ${tf.y}px) scale(${tf.k})`,
|
||||
}}
|
||||
>
|
||||
<svg className={css.edges} width={layout.width} height={layout.height} aria-hidden="true">
|
||||
{edges.map((edge) => {
|
||||
const from = layout.boxes.get(edge.from)
|
||||
const to = layout.boxes.get(edge.to)
|
||||
if (from === undefined || to === undefined) return null
|
||||
const target = nodeById.get(edge.to)
|
||||
return (
|
||||
<path
|
||||
key={`${edge.from}->${edge.to}`}
|
||||
className={clsx(
|
||||
css.edge,
|
||||
(edge.kind === 'team' || edge.kind === 'workflow') && css.edgeTeam,
|
||||
target?.kind === 'fold' && css.edgeFold,
|
||||
)}
|
||||
d={edgePath(from, to)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
{phaseFrames.map(frame => (
|
||||
<div
|
||||
key={frame.key}
|
||||
className={css.phaseFrame}
|
||||
style={{ left: frame.x, top: frame.y, width: frame.w, height: frame.h }}
|
||||
>
|
||||
<span className={css.phaseLabel}>{frame.title ?? t('workflowPhaseUnnamed')}</span>
|
||||
</div>
|
||||
))}
|
||||
{nodes.map((node) => {
|
||||
const box = layout.boxes.get(node.id)
|
||||
if (box === undefined) return null
|
||||
const style = { left: box.x, top: box.y, width: GRAPH_NODE_W, minHeight: box.h }
|
||||
if (node.kind === 'fold') return renderFoldNode(node, style, onToggleFold, clickAllowed)
|
||||
if (node.kind === 'workflow') {
|
||||
return renderWorkflowNode(node, style, onWorkflowInfo, clickAllowed)
|
||||
}
|
||||
return renderAgentNode(node, style, onActivate, onNodeInfo, clickAllowed)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.controls}>
|
||||
<ViewModeToggle mode={mode} onModeChange={onModeChange} />
|
||||
<FoldToggleButton folded={folded} onToggleFold={onToggleFold} />
|
||||
<button
|
||||
type="button"
|
||||
className={css.controlBtn}
|
||||
aria-label={t('tasksZoomOut')}
|
||||
title={t('tasksZoomOut')}
|
||||
onClick={() => { zoomBy(1 / 1.25) }}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.controlBtn, css.controlPct)}
|
||||
aria-label={t('tasksZoomFit')}
|
||||
title={t('tasksZoomFit')}
|
||||
onClick={fit}
|
||||
>
|
||||
{Math.round(tf.k * 100)}%
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.controlBtn}
|
||||
aria-label={t('tasksZoomIn')}
|
||||
title={t('tasksZoomIn')}
|
||||
onClick={() => { zoomBy(1.25) }}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** One agent node card. */
|
||||
function renderAgentNode(
|
||||
node: TasksAgentNode,
|
||||
style: { left: number; top: number; width: number; minHeight: number },
|
||||
onActivate: (node: TasksAgentNode) => void,
|
||||
onNodeInfo: (node: TasksAgentNode, anchor: HTMLElement) => void,
|
||||
clickAllowed: () => boolean,
|
||||
): ReactNode {
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label={`${node.label} ${agentSecondary(node)}`}
|
||||
aria-current={node.current ? 'true' : undefined}
|
||||
className={clsx(
|
||||
css.node,
|
||||
node.state === 'running' && css.nodeRunning,
|
||||
node.state === 'done' && css.nodeDone,
|
||||
node.state === 'error' && css.nodeError,
|
||||
node.current && css.nodeCurrent,
|
||||
)}
|
||||
style={style}
|
||||
onClick={(event) => {
|
||||
if (!clickAllowed()) return
|
||||
onActivate(node)
|
||||
void event
|
||||
}}
|
||||
>
|
||||
<span className={css.nodeHeader}>
|
||||
<StateDot state={nodeDotState(node.state)} size={8} className={css.nodeDot} />
|
||||
<span className={css.nodeTitle} title={node.label}>{node.label}</span>
|
||||
</span>
|
||||
<span className={css.nodeSecondary} title={agentSecondary(node)}>{agentSecondary(node)}</span>
|
||||
{node.state === 'running' && <LiveLine live={node.live} />}
|
||||
<button
|
||||
type="button"
|
||||
className={css.nodeInfo}
|
||||
aria-label={t('tasksNodeState')}
|
||||
title={t('tasksNodeState')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onNodeInfo(node, event.currentTarget)
|
||||
}}
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** One workflow run node card. */
|
||||
function renderWorkflowNode(
|
||||
node: TasksWorkflowNode,
|
||||
style: { left: number; top: number; width: number; minHeight: number },
|
||||
onWorkflowInfo: (node: TasksWorkflowNode, anchor: HTMLElement) => void,
|
||||
clickAllowed: () => boolean,
|
||||
): ReactNode {
|
||||
const tally = memberTally(node.run)
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label={node.run.name}
|
||||
className={clsx(css.node, css.nodeWorkflow, node.run.status === 'running' && css.nodeRunning)}
|
||||
style={style}
|
||||
onClick={(event) => {
|
||||
if (!clickAllowed()) return
|
||||
onWorkflowInfo(node, event.currentTarget)
|
||||
}}
|
||||
>
|
||||
<span className={css.nodeHeader}>
|
||||
<span aria-hidden="true">▶</span>
|
||||
<span className={css.nodeTitle} title={node.run.name}>{node.run.name}</span>
|
||||
</span>
|
||||
<span className={css.nodeSecondary}>
|
||||
{`${t(workflowStatusKey(node.run.status))} · ${t('workflowMembers', { done: tally.done, total: tally.total })}`}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** One fold aggregate node. */
|
||||
function renderFoldNode(
|
||||
node: TasksFoldNode,
|
||||
style: { left: number; top: number; width: number; minHeight: number },
|
||||
onToggleFold: () => void,
|
||||
clickAllowed: () => boolean,
|
||||
): ReactNode {
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label={`${t('tasksFoldCompleted', { count: node.count })} · ${t('tasksFoldExpand')}`}
|
||||
className={clsx(css.node, css.nodeFold)}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
if (!clickAllowed()) return
|
||||
onToggleFold()
|
||||
}}
|
||||
>
|
||||
<span className={css.nodeTitle}>{t('tasksFoldCompleted', { count: node.count })}</span>
|
||||
<span className={css.nodeSecondary}>{foldPreviews(node.previews)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* The anchored-popover CONTENTS of the Tasks page (geometry and dismissal
|
||||
* live in AnchoredPopover): agent node details with a transcript jump, the
|
||||
* workflow run detail with clickable member rows, and the Agent Teams task
|
||||
* board (roster + basic CAS operations: complete / reopen / delete /
|
||||
* reassign / create / edit). Every mutation passes the task's CURRENT
|
||||
* revision; a conflict shows the note and re-syncs via the parent poller.
|
||||
*/
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type {
|
||||
SidebarSubagentAddress,
|
||||
SidebarTeamMemberView,
|
||||
SidebarTeamTaskView,
|
||||
} from '../context-types.ts'
|
||||
import type { TasksAgentNode, TasksWorkflowNode } from './tasks-model.ts'
|
||||
import { agentSecondary, flatten, nodeDotState } from './tasks-shared.tsx'
|
||||
import { api } from './api.ts'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** The display-state label key. */
|
||||
function stateKey(state: TasksAgentNode['state']): CopyKey {
|
||||
switch (state) {
|
||||
case 'running': return 'tasksStateRunning'
|
||||
case 'idle': return 'tasksStateIdle'
|
||||
case 'done': return 'tasksStateDone'
|
||||
case 'error': return 'tasksStateError'
|
||||
}
|
||||
}
|
||||
|
||||
/** One key/value row of a popover. */
|
||||
function PopRow(props: { label: string; children: ReactNode }): ReactNode {
|
||||
return (
|
||||
<div className={css.popRow}>
|
||||
<span className={css.popKey}>{props.label}</span>
|
||||
<span className={css.popValue}>{props.children}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** The agent node detail popover. */
|
||||
export function AgentNodePopover(props: {
|
||||
node: TasksAgentNode
|
||||
onJump(node: TasksAgentNode): void
|
||||
}): ReactNode {
|
||||
const { node, onJump } = props
|
||||
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}`}`
|
||||
: undefined
|
||||
return (
|
||||
<div className={css.popCard}>
|
||||
<span className={css.popTitle} title={node.label}>{node.label}</span>
|
||||
<PopRow label={t('tasksNodeState')}>
|
||||
<StateDot state={nodeDotState(node.state)} size={8} /> {t(stateKey(node.state))}
|
||||
</PopRow>
|
||||
{node.mode !== undefined && (
|
||||
<PopRow label={t('tasksNodeMode')}>
|
||||
{node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable')}
|
||||
</PopRow>
|
||||
)}
|
||||
{node.team !== undefined && (
|
||||
<>
|
||||
<PopRow label={t('tasksNodeTeamRole')}>
|
||||
{node.team.role === 'lead' ? 'lead' : node.team.name}
|
||||
</PopRow>
|
||||
{node.team.model !== undefined && (
|
||||
<PopRow label={t('tasksNodeModel')}>{node.team.model}</PopRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(liveTool !== undefined || liveText !== undefined) && (
|
||||
<PopRow label={t('tasksNodeActivity')}>{liveTool ?? liveText}</PopRow>
|
||||
)}
|
||||
{node.childAddress !== undefined || node.parentId === undefined ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.popPrimary}
|
||||
onClick={() => { onJump(node) }}
|
||||
>
|
||||
{t('tasksNodeJump')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** The workflow run detail popover (phases with clickable member rows). */
|
||||
export function WorkflowNodePopover(props: {
|
||||
node: TasksWorkflowNode
|
||||
onJumpMember(address: SidebarSubagentAddress): void
|
||||
}): ReactNode {
|
||||
const { node, onJumpMember } = props
|
||||
const { run } = node
|
||||
return (
|
||||
<div className={css.popCard}>
|
||||
<span className={css.popTitle} title={run.name}>▶ {run.name}</span>
|
||||
<PopRow label={t('tasksNodeState')}>
|
||||
{t(run.status === 'running'
|
||||
? 'workflowRunning'
|
||||
: run.status === 'completed'
|
||||
? 'workflowCompleted'
|
||||
: run.status === 'cancelled' ? 'workflowCancelled' : 'workflowFailed')}
|
||||
</PopRow>
|
||||
<div className={css.popSection}>
|
||||
<div className={css.popList}>
|
||||
{run.phases.map((phase, phaseIndex) => (
|
||||
<div key={`${phase.title ?? 'phase'}-${phaseIndex}`}>
|
||||
<div className={clsx(css.popKey, css.popRow)}>
|
||||
{phase.title ?? t('workflowPhaseUnnamed')}
|
||||
</div>
|
||||
{phase.members.map(member => (
|
||||
<div
|
||||
key={member.seq}
|
||||
role={member.childId === '' ? undefined : 'button'}
|
||||
className={css.popListRow}
|
||||
onClick={() => {
|
||||
if (member.childId === '') return
|
||||
onJumpMember({
|
||||
parentSessionId: run.originSessionId,
|
||||
childSessionId: member.childId,
|
||||
mode: 'one-shot',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<StateDot
|
||||
size={8}
|
||||
state={member.outcome === undefined
|
||||
? 'ongoing'
|
||||
: member.outcome === 'completed' ? 'done'
|
||||
: member.outcome === 'failed' ? 'error' : 'idle'}
|
||||
/>
|
||||
<span className={css.popValue} title={member.label}>{member.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** The task status label key. */
|
||||
function taskStatusKey(status: SidebarTeamTaskView['status']): CopyKey {
|
||||
switch (status) {
|
||||
case 'pending': return 'teamTaskPending'
|
||||
case 'in_progress': return 'teamTaskInProgress'
|
||||
case 'completed': return 'teamTaskCompleted'
|
||||
case 'deleted': return 'teamTaskDeleted'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Agent Teams board popover: roster + shared task list with the basic
|
||||
* operations. Poll-driven refresh: a successful mutation (or a conflict)
|
||||
* asks the parent to re-pull `teams.view` immediately.
|
||||
*/
|
||||
export function TeamBoardPopover(props: {
|
||||
rootId: string
|
||||
members: readonly SidebarTeamMemberView[]
|
||||
tasks: readonly SidebarTeamTaskView[]
|
||||
onChanged(): void
|
||||
}): ReactNode {
|
||||
const { rootId, members, tasks, onChanged } = props
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [note, setNote] = useState<string | undefined>(undefined)
|
||||
const [editingId, setEditingId] = useState<string | undefined>(undefined)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [armedDeleteId, setArmedDeleteId] = useState<string | undefined>(undefined)
|
||||
|
||||
/** Run one mutation with the shared busy/conflict handling. */
|
||||
const mutate = async (
|
||||
action: () => Promise<{ ok: true } | { ok: false; error: { code: string; message: string } }>,
|
||||
): Promise<void> => {
|
||||
if (busy) return
|
||||
setBusy(true)
|
||||
setNote(undefined)
|
||||
try {
|
||||
const result = await action()
|
||||
if (!result.ok) {
|
||||
setNote(result.error.code === 'team-task-conflict' ? t('teamTaskConflict') : t('teamTaskError', { message: result.error.message }))
|
||||
}
|
||||
onChanged()
|
||||
} catch (error) {
|
||||
setNote(t('teamTaskError', { message: error instanceof Error ? error.message : String(error) }))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css.popCard} style={{ width: 300 }}>
|
||||
<span className={css.popTitle}>{t('teamBoard')}</span>
|
||||
<PopRow label={t('teamMembers')}>
|
||||
{members.map(member => member.name).join(' · ')}
|
||||
</PopRow>
|
||||
<div className={css.popSection}>
|
||||
<div className={css.popList}>
|
||||
{tasks.length === 0 && <div className={css.popHint}>{t('teamTasksEmpty')}</div>}
|
||||
{tasks.filter(task => task.status !== 'deleted').map(task => (
|
||||
<TeamTaskRow
|
||||
key={task.id}
|
||||
rootId={rootId}
|
||||
task={task}
|
||||
members={members}
|
||||
busy={busy}
|
||||
editing={editingId === task.id}
|
||||
armedDelete={armedDeleteId === task.id}
|
||||
onEdit={(open) => { setEditingId(open ? task.id : undefined) }}
|
||||
onArmDelete={(armed) => { setArmedDeleteId(armed ? task.id : undefined) }}
|
||||
mutate={mutate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{note !== undefined && <div className={css.popNote}>{note}</div>}
|
||||
{creating
|
||||
? (
|
||||
<TeamTaskForm
|
||||
busy={busy}
|
||||
submitLabel={t('teamTaskCreate')}
|
||||
onCancel={() => { setCreating(false) }}
|
||||
onSubmit={(subject, description) => void mutate(async () => {
|
||||
const result = await api.teamsTaskCreate(rootId, { subject, description })
|
||||
if (result.ok) setCreating(false)
|
||||
return result
|
||||
})}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
className={css.popGhost}
|
||||
disabled={busy}
|
||||
onClick={() => { setCreating(true) }}
|
||||
>
|
||||
+ {t('teamTaskCreate')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** One task row of the board (status action + owner select + edit/delete). */
|
||||
function TeamTaskRow(props: {
|
||||
rootId: string
|
||||
task: SidebarTeamTaskView
|
||||
members: readonly SidebarTeamMemberView[]
|
||||
busy: boolean
|
||||
editing: boolean
|
||||
armedDelete: boolean
|
||||
onEdit(open: boolean): void
|
||||
onArmDelete(armed: boolean): void
|
||||
mutate(action: () => Promise<{ ok: true } | { ok: false; error: { code: string; message: string } }>): Promise<void>
|
||||
}): ReactNode {
|
||||
const { rootId, task, members, busy, editing, armedDelete, onEdit, onArmDelete, mutate } = props
|
||||
if (editing) {
|
||||
return (
|
||||
<TeamTaskForm
|
||||
busy={busy}
|
||||
initialSubject={task.subject}
|
||||
initialDescription={task.description}
|
||||
submitLabel={t('teamTaskSave')}
|
||||
onCancel={() => { onEdit(false) }}
|
||||
onSubmit={(subject, description) => void mutate(async () => {
|
||||
const result = await api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id,
|
||||
expectedRevision: task.revision,
|
||||
action: 'edit',
|
||||
subject,
|
||||
description,
|
||||
})
|
||||
if (result.ok) onEdit(false)
|
||||
return result
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={css.popListRow} style={{ cursor: 'default' }}>
|
||||
<StateDot
|
||||
size={8}
|
||||
state={task.status === 'completed' ? 'done' : task.ready ? 'ongoing' : 'warning'}
|
||||
/>
|
||||
<span className={css.popValue} title={task.subject}>
|
||||
{task.subject}
|
||||
{!task.ready && ` · ${t('teamTaskBlocked')}`}
|
||||
</span>
|
||||
<select
|
||||
className={css.popSelect}
|
||||
style={{ width: 'auto', flexShrink: 0 }}
|
||||
aria-label={t('teamTaskOwner')}
|
||||
title={t('teamTaskOwner')}
|
||||
disabled={busy || task.status === 'completed'}
|
||||
value={task.ownerName ?? ''}
|
||||
onChange={(event) => {
|
||||
const owner = event.target.value
|
||||
void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id,
|
||||
expectedRevision: task.revision,
|
||||
action: 'reassign',
|
||||
...(owner === '' ? {} : { owner }),
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<option value="">{t('teamTaskUnowned')}</option>
|
||||
{members.filter(member => member.role === 'teammate').map(member => (
|
||||
<option key={member.id} value={member.name}>{member.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className={css.popActions}>
|
||||
{task.status !== 'completed' && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.popGhost}
|
||||
disabled={busy}
|
||||
title={t('teamTaskComplete')}
|
||||
aria-label={`${t('teamTaskComplete')} ${task.subject}`}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'complete',
|
||||
}))}
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
)}
|
||||
{task.status === 'completed' && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.popGhost}
|
||||
disabled={busy}
|
||||
title={t('teamTaskReopen')}
|
||||
aria-label={`${t('teamTaskReopen')} ${task.subject}`}
|
||||
onClick={() => void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'reopen',
|
||||
}))}
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={css.popGhost}
|
||||
disabled={busy}
|
||||
title={t('teamTaskEdit')}
|
||||
aria-label={`${t('teamTaskEdit')} ${task.subject}`}
|
||||
onClick={() => { onEdit(true) }}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.popGhost, armedDelete && css.popGhostDanger)}
|
||||
disabled={busy}
|
||||
title={armedDelete ? t('teamTaskDeleteConfirm') : t('teamTaskDelete')}
|
||||
aria-label={`${armedDelete ? t('teamTaskDeleteConfirm') : t('teamTaskDelete')} ${task.subject}`}
|
||||
onClick={() => {
|
||||
if (!armedDelete) { onArmDelete(true); return }
|
||||
onArmDelete(false)
|
||||
void mutate(() => api.teamsTaskUpdate(rootId, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'delete',
|
||||
}))
|
||||
}}
|
||||
>
|
||||
{armedDelete ? t('teamTaskDeleteConfirm') : '🗑'}
|
||||
</button>
|
||||
</span>
|
||||
<span className={css.popHint}>{t(taskStatusKey(task.status))}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** The create/edit task form (subject + description). */
|
||||
function TeamTaskForm(props: {
|
||||
busy: boolean
|
||||
initialSubject?: string
|
||||
initialDescription?: string
|
||||
submitLabel: string
|
||||
onCancel(): void
|
||||
onSubmit(subject: string, description: string): void
|
||||
}): ReactNode {
|
||||
const [subject, setSubject] = useState(props.initialSubject ?? '')
|
||||
const [description, setDescription] = useState(props.initialDescription ?? '')
|
||||
return (
|
||||
<div className={css.popSection}>
|
||||
<input
|
||||
className={css.popInput}
|
||||
placeholder={t('teamTaskSubject')}
|
||||
aria-label={t('teamTaskSubject')}
|
||||
value={subject}
|
||||
onChange={(event) => { setSubject(event.target.value) }}
|
||||
/>
|
||||
<input
|
||||
className={css.popInput}
|
||||
placeholder={t('teamTaskDescription')}
|
||||
aria-label={t('teamTaskDescription')}
|
||||
value={description}
|
||||
onChange={(event) => { setDescription(event.target.value) }}
|
||||
/>
|
||||
<div className={css.popActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.popPrimary}
|
||||
disabled={props.busy || subject.trim() === ''}
|
||||
onClick={() => { props.onSubmit(subject.trim(), description.trim()) }}
|
||||
>
|
||||
{props.submitLabel}
|
||||
</button>
|
||||
<button type="button" className={css.popGhost} disabled={props.busy} onClick={props.onCancel}>
|
||||
{t('teamTaskCancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* The tree mode of the Tasks page: the SAME unified model rendered as a
|
||||
* classic indentation tree (keyboard navigable, the official catalog's
|
||||
* arrow-key recipe). Fold aggregates are rows; workflow runs are group rows
|
||||
* whose members indent one level deeper. The control cluster (view toggle +
|
||||
* fold toggle) stays visible bottom-right — the mockup's lesson.
|
||||
*/
|
||||
import { useCallback, useMemo, useRef, type KeyboardEvent, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TasksAgentNode, TasksNode, TasksWorkflowNode } from './tasks-model.ts'
|
||||
import { agentSecondary, foldPreviews, LiveLine, nodeDotState } from './tasks-shared.tsx'
|
||||
import { FoldToggleButton, ViewModeToggle } from './TasksGraph.tsx'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
import legacy from './SubagentView.module.css'
|
||||
|
||||
/** Pixels of indentation per depth level (the classic tree's rhythm). */
|
||||
const INDENT_PX = 18
|
||||
|
||||
/** The workflow run status label key. */
|
||||
function workflowStatusKey(status: TasksWorkflowNode['run']['status']): CopyKey {
|
||||
switch (status) {
|
||||
case 'running': return 'workflowRunning'
|
||||
case 'completed': return 'workflowCompleted'
|
||||
case 'cancelled': return 'workflowCancelled'
|
||||
case 'failed': return 'workflowFailed'
|
||||
}
|
||||
}
|
||||
|
||||
export interface TasksTreeProps {
|
||||
nodes: readonly TasksNode[]
|
||||
folded: boolean
|
||||
onActivate(node: TasksAgentNode): void
|
||||
onNodeInfo(node: TasksAgentNode, anchor: HTMLElement): void
|
||||
onWorkflowInfo(node: TasksWorkflowNode, anchor: HTMLElement): void
|
||||
onToggleFold(): void
|
||||
mode: 'graph' | 'tree'
|
||||
onModeChange(mode: 'graph' | 'tree'): void
|
||||
/** Fallback loading rows while the root catalog hydrates. */
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function TasksTree(props: TasksTreeProps): ReactNode {
|
||||
const {
|
||||
nodes, folded, onActivate, onNodeInfo, onWorkflowInfo, onToggleFold, mode, onModeChange, loading,
|
||||
} = props
|
||||
const bodyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
/** Depth of every node (roots at 0), derived from the parent chain. */
|
||||
const depthById = useMemo(() => {
|
||||
const byId = new Map(nodes.map(node => [node.id, node]))
|
||||
const depths = new Map<string, number>()
|
||||
const depthOf = (node: TasksNode, guard: number): number => {
|
||||
const known = depths.get(node.id)
|
||||
if (known !== undefined) return known
|
||||
if (node.parentId === undefined || guard > 64) {
|
||||
depths.set(node.id, 0)
|
||||
return 0
|
||||
}
|
||||
const parent = byId.get(node.parentId)
|
||||
const depth = parent === undefined ? 0 : depthOf(parent, guard + 1) + 1
|
||||
depths.set(node.id, depth)
|
||||
return depth
|
||||
}
|
||||
for (const node of nodes) depthOf(node, 0)
|
||||
return depths
|
||||
}, [nodes])
|
||||
|
||||
/** Arrow-key navigation over the visible rows (official catalog recipe). */
|
||||
const focusAt = useCallback((index: number): void => {
|
||||
const items = bodyRef.current?.querySelectorAll<HTMLElement>('[data-tasks-row]') ?? []
|
||||
if (items.length === 0) return
|
||||
items[(index + items.length) % items.length]?.focus()
|
||||
}, [])
|
||||
const onTreeKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>): void => {
|
||||
const items = bodyRef.current?.querySelectorAll<HTMLElement>('[data-tasks-row]') ?? []
|
||||
const index = Array.prototype.indexOf.call(items, document.activeElement)
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
focusAt(index + 1)
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
focusAt(index < 0 ? items.length - 1 : index - 1)
|
||||
} else if (event.key === 'Home') {
|
||||
event.preventDefault()
|
||||
focusAt(0)
|
||||
} else if (event.key === 'End') {
|
||||
event.preventDefault()
|
||||
focusAt(items.length - 1)
|
||||
}
|
||||
}, [focusAt])
|
||||
|
||||
const activateOnKey = (event: KeyboardEvent, action: () => void): void => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css.graphView}>
|
||||
<div
|
||||
ref={bodyRef}
|
||||
className={css.treeView}
|
||||
role="tree"
|
||||
aria-label={t('subagent')}
|
||||
onKeyDown={onTreeKeyDown}
|
||||
>
|
||||
{loading === true && (
|
||||
<div className={legacy.subagentEmpty}>{t('loading')}</div>
|
||||
)}
|
||||
{nodes.map((node) => {
|
||||
const depth = depthById.get(node.id) ?? 0
|
||||
const indent = { marginLeft: depth * INDENT_PX }
|
||||
if (node.kind === 'fold') {
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
data-tasks-row
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
aria-level={depth + 1}
|
||||
aria-label={t('tasksFoldExpand')}
|
||||
className={clsx(css.treeRow, css.treeFold)}
|
||||
style={indent}
|
||||
onClick={onToggleFold}
|
||||
onKeyDown={(event) => { activateOnKey(event, onToggleFold) }}
|
||||
>
|
||||
<span className={css.treeContent}>
|
||||
<span className={css.nodeTitle}>{t('tasksFoldCompleted', { count: node.count })}</span>
|
||||
<span className={css.nodeSecondary}>{foldPreviews(node.previews)}</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (node.kind === 'workflow') {
|
||||
const done = node.run.phases.reduce(
|
||||
(sum, phase) => sum + phase.members.filter(member => member.outcome !== undefined).length,
|
||||
0,
|
||||
)
|
||||
const total = node.run.phases.reduce((sum, phase) => sum + phase.members.length, 0)
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
data-tasks-row
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
aria-level={depth + 1}
|
||||
aria-expanded="true"
|
||||
aria-label={node.run.name}
|
||||
className={css.treeRow}
|
||||
style={indent}
|
||||
onClick={(event) => { onWorkflowInfo(node, event.currentTarget) }}
|
||||
onKeyDown={(event) => {
|
||||
activateOnKey(event, () => { onWorkflowInfo(node, event.currentTarget as HTMLElement) })
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true">▶</span>
|
||||
<span className={css.treeContent}>
|
||||
<span className={css.nodeTitle}>{node.run.name}</span>
|
||||
<span className={css.nodeSecondary}>
|
||||
{`${t(workflowStatusKey(node.run.status))} · ${t('workflowMembers', { done, total })}`}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
data-tasks-row
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
aria-level={depth + 1}
|
||||
aria-label={`${node.label} ${agentSecondary(node)}`}
|
||||
aria-current={node.current ? 'true' : undefined}
|
||||
className={clsx(
|
||||
css.treeRow,
|
||||
node.current && css.treeRowActive,
|
||||
node.state === 'done' && css.treeRowDone,
|
||||
)}
|
||||
style={indent}
|
||||
onClick={() => { onActivate(node) }}
|
||||
onKeyDown={(event) => { activateOnKey(event, () => { onActivate(node) }) }}
|
||||
>
|
||||
<StateDot state={nodeDotState(node.state)} className={legacy.subagentDot} />
|
||||
<span className={css.treeContent}>
|
||||
<span className={css.nodeTitle}>{node.label}</span>
|
||||
<span className={css.nodeSecondary}>{agentSecondary(node)}</span>
|
||||
{node.state === 'running' && <LiveLine live={node.live} />}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={css.treeInfo}
|
||||
aria-label={t('tasksNodeState')}
|
||||
title={t('tasksNodeState')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onNodeInfo(node, event.currentTarget)
|
||||
}}
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className={css.controls}>
|
||||
<ViewModeToggle mode={mode} onModeChange={onModeChange} />
|
||||
<FoldToggleButton folded={folded} onToggleFold={onToggleFold} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -137,12 +137,30 @@ export function builtinTabs(): readonly TabDescriptor[] {
|
||||
key: 'autoOpenJobs',
|
||||
title: () => t('settingsJobsTitle'),
|
||||
desc: () => t('settingsJobsDesc'),
|
||||
}, {
|
||||
key: 'tasksViewMode',
|
||||
type: 'select',
|
||||
title: () => t('settingsViewModeTitle'),
|
||||
desc: () => t('settingsViewModeDesc'),
|
||||
options: [
|
||||
{
|
||||
value: 'graph',
|
||||
title: () => t('settingsViewModeGraph'),
|
||||
desc: () => t('settingsViewModeGraphDesc'),
|
||||
},
|
||||
{
|
||||
value: 'tree',
|
||||
title: () => t('settingsViewModeTree'),
|
||||
desc: () => t('settingsViewModeTreeDesc'),
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
component: ({ ctx, scope, visible, onSubagentJump }) => (
|
||||
component: ({ ctx, store, scope, visible, onSubagentJump }) => (
|
||||
<SubagentView
|
||||
sessionId={scope.sessionId}
|
||||
ctx={ctx}
|
||||
store={store}
|
||||
active={visible}
|
||||
onOpenChild={(address) => { onSubagentJump?.(address.childSessionId) }}
|
||||
/>
|
||||
|
||||
@@ -391,6 +391,64 @@ export const ar: Record<string, string> = {
|
||||
jobKill: 'إنهاء',
|
||||
jobKillConfirm: 'انقر مجدداً للتأكيد على الإنهاء',
|
||||
jobKillError: 'فشل الإنهاء',
|
||||
tasksViewGraph: 'مخطط سير العمل',
|
||||
tasksViewTree: 'شجرة',
|
||||
tasksViewSwitchToGraph: 'التبديل إلى مخطط سير العمل',
|
||||
tasksViewSwitchToTree: 'التبديل إلى الشجرة',
|
||||
settingsViewModeTitle: 'العرض الافتراضي لصفحة المهام',
|
||||
settingsViewModeDesc: 'كيف تُعرض صفحة المهام عند الفتح (زر التبديل داخل الصفحة يظل يبدّلها مؤقتًا)',
|
||||
settingsViewModeGraph: 'مخطط سير العمل',
|
||||
settingsViewModeGraphDesc: 'مخطط عُقد متدرج مع تكبير وتحريك',
|
||||
settingsViewModeTree: 'شجرة',
|
||||
settingsViewModeTreeDesc: 'شجرة كلاسيكية بمسافات بادئة، قابلة للتنقل بلوحة المفاتيح',
|
||||
tasksWorkflowMember: 'عضو سير العمل',
|
||||
tasksFoldCompleted: '✓ {count} مكتمل',
|
||||
tasksFoldExpand: 'توسيع العُقد المكتملة',
|
||||
tasksFoldCollapse: 'طي العُقد المكتملة',
|
||||
tasksZoomIn: 'تكبير',
|
||||
tasksZoomOut: 'تصغير',
|
||||
tasksZoomFit: 'ملاءمة اللوحة',
|
||||
tasksNodeJump: 'فتح النسخة',
|
||||
tasksNodeState: 'الحالة',
|
||||
tasksNodeMode: 'الوضع',
|
||||
tasksNodeModel: 'الطراز',
|
||||
tasksNodeTeamRole: 'دور الفريق',
|
||||
tasksNodeActivity: 'آخر نشاط',
|
||||
tasksStateRunning: 'قيد التشغيل',
|
||||
tasksStateIdle: 'خامل',
|
||||
tasksStateDone: 'مكتمل',
|
||||
tasksStateError: 'خطأ',
|
||||
teamBoard: 'لوحة مهام الفريق',
|
||||
teamChip: 'فريق {members} أعضاء · {tasks} مهام',
|
||||
teamMembers: 'الأعضاء',
|
||||
teamTasksEmpty: 'لا مهام مشتركة بعد',
|
||||
teamTaskCreate: 'مهمة جديدة',
|
||||
teamTaskSubject: 'الموضوع',
|
||||
teamTaskDescription: 'الوصف',
|
||||
teamTaskOwner: 'المسؤول',
|
||||
teamTaskUnowned: 'غير مُسندة',
|
||||
teamTaskPending: 'معلّقة',
|
||||
teamTaskInProgress: 'قيد التنفيذ',
|
||||
teamTaskCompleted: 'مكتملة',
|
||||
teamTaskBlocked: 'محجوبة',
|
||||
teamTaskComplete: 'إكمال',
|
||||
teamTaskReopen: 'إعادة فتح',
|
||||
teamTaskDelete: 'حذف',
|
||||
teamTaskDeleteConfirm: 'تأكيد الحذف',
|
||||
teamTaskEdit: 'تحرير',
|
||||
teamTaskSave: 'حفظ',
|
||||
teamTaskCancel: 'إلغاء',
|
||||
teamTaskConflict: 'تم تعديل المهمة من جهة أخرى، تم التحديث',
|
||||
teamTaskError: 'فشلت العملية: {message}',
|
||||
teamTaskDeleted: 'محذوفة',
|
||||
workflowRunning: 'قيد التشغيل',
|
||||
workflowCompleted: 'مكتمل',
|
||||
workflowCancelled: 'أُلغي',
|
||||
workflowFailed: 'فشل',
|
||||
workflowPhaseUnnamed: 'مرحلة بدون اسم',
|
||||
workflowMembers: '{done}/{total} أعضاء',
|
||||
jobsAutoCollapsed: 'تم الطي تلقائيًا (وكلاء كثيرون)',
|
||||
catalogLoadFailed: 'فشل تحميل {count} من الفروع',
|
||||
addPluginsTabCard: 'إضافة إضافات تبويب',
|
||||
addPluginsTabCardDesc: 'تسجيل صفحة شريط جانبي جديدة',
|
||||
addPluginsViewerCard: 'إضافة إضافات معاينة',
|
||||
|
||||
@@ -376,6 +376,64 @@ export const de: Record<string, string> = {
|
||||
jobKill: 'Beenden',
|
||||
jobKillConfirm: 'Zum Bestätigen erneut klicken',
|
||||
jobKillError: 'Beenden fehlgeschlagen',
|
||||
tasksViewGraph: 'Workflow-Diagramm',
|
||||
tasksViewTree: 'Baum',
|
||||
tasksViewSwitchToGraph: 'Zum Workflow-Diagramm wechseln',
|
||||
tasksViewSwitchToTree: 'Zum Baum wechseln',
|
||||
settingsViewModeTitle: 'Standardansicht der Aufgabenseite',
|
||||
settingsViewModeDesc: 'Wie die Aufgabenseite beim Öffnen dargestellt wird (der Umschalter auf der Seite ändert es weiterhin ad hoc)',
|
||||
settingsViewModeGraph: 'Workflow-Diagramm',
|
||||
settingsViewModeGraphDesc: 'Geschichtetes Knotendiagramm mit Zoom und Schwenken',
|
||||
settingsViewModeTree: 'Baum',
|
||||
settingsViewModeTreeDesc: 'Klassischer Einrückungsbaum, per Tastatur navigierbar',
|
||||
tasksWorkflowMember: 'Workflow-Mitglied',
|
||||
tasksFoldCompleted: '✓ {count} abgeschlossen',
|
||||
tasksFoldExpand: 'Abgeschlossene Knoten ausklappen',
|
||||
tasksFoldCollapse: 'Abgeschlossene Knoten einklappen',
|
||||
tasksZoomIn: 'Vergrößern',
|
||||
tasksZoomOut: 'Verkleinern',
|
||||
tasksZoomFit: 'An die Canvas anpassen',
|
||||
tasksNodeJump: 'Transkript öffnen',
|
||||
tasksNodeState: 'Status',
|
||||
tasksNodeMode: 'Modus',
|
||||
tasksNodeModel: 'Modell',
|
||||
tasksNodeTeamRole: 'Teamrolle',
|
||||
tasksNodeActivity: 'Letzte Aktivität',
|
||||
tasksStateRunning: 'Läuft',
|
||||
tasksStateIdle: 'Bereit',
|
||||
tasksStateDone: 'Abgeschlossen',
|
||||
tasksStateError: 'Fehler',
|
||||
teamBoard: 'Team-Aufgabenboard',
|
||||
teamChip: 'Team {members} Mitglieder · {tasks} Aufgaben',
|
||||
teamMembers: 'Mitglieder',
|
||||
teamTasksEmpty: 'Noch keine geteilten Aufgaben',
|
||||
teamTaskCreate: 'Neue Aufgabe',
|
||||
teamTaskSubject: 'Titel',
|
||||
teamTaskDescription: 'Beschreibung',
|
||||
teamTaskOwner: 'Verantwortlich',
|
||||
teamTaskUnowned: 'Nicht übernommen',
|
||||
teamTaskPending: 'Offen',
|
||||
teamTaskInProgress: 'In Arbeit',
|
||||
teamTaskCompleted: 'Abgeschlossen',
|
||||
teamTaskBlocked: 'Blockiert',
|
||||
teamTaskComplete: 'Abschließen',
|
||||
teamTaskReopen: 'Wieder öffnen',
|
||||
teamTaskDelete: 'Löschen',
|
||||
teamTaskDeleteConfirm: 'Löschen bestätigen',
|
||||
teamTaskEdit: 'Bearbeiten',
|
||||
teamTaskSave: 'Speichern',
|
||||
teamTaskCancel: 'Abbrechen',
|
||||
teamTaskConflict: 'Die Aufgabe wurde anderweitig geändert; aktualisiert',
|
||||
teamTaskError: 'Vorgang fehlgeschlagen: {message}',
|
||||
teamTaskDeleted: 'Gelöscht',
|
||||
workflowRunning: 'Läuft',
|
||||
workflowCompleted: 'Abgeschlossen',
|
||||
workflowCancelled: 'Abgebrochen',
|
||||
workflowFailed: 'Fehlgeschlagen',
|
||||
workflowPhaseUnnamed: 'Unbenannte Phase',
|
||||
workflowMembers: '{done}/{total} Mitglieder',
|
||||
jobsAutoCollapsed: 'Automatisch eingeklappt (viele Agenten)',
|
||||
catalogLoadFailed: '{count} Branches konnten nicht geladen werden',
|
||||
addPluginsTabCard: 'Tab-Plugins hinzufügen',
|
||||
addPluginsTabCardDesc: 'Eine neue Seitenleisten-Seite registrieren',
|
||||
addPluginsViewerCard: 'Vorschau-Plugins hinzufügen',
|
||||
|
||||
@@ -383,6 +383,64 @@ export const fr: Record<string, string> = {
|
||||
jobKill: 'Interrompre',
|
||||
jobKillConfirm: 'Cliquez à nouveau pour confirmer l’interruption',
|
||||
jobKillError: 'Échec de l’interruption',
|
||||
tasksViewGraph: 'Diagramme de workflow',
|
||||
tasksViewTree: 'Arbre',
|
||||
tasksViewSwitchToGraph: 'Passer au diagramme de workflow',
|
||||
tasksViewSwitchToTree: 'Passer à l’arbre',
|
||||
settingsViewModeTitle: 'Vue par défaut de la page Tâches',
|
||||
settingsViewModeDesc: 'Comment la page Tâches s’affiche à l’ouverture (le bouton dans la page permet toujours de basculer à la volée)',
|
||||
settingsViewModeGraph: 'Diagramme de workflow',
|
||||
settingsViewModeGraphDesc: 'Graphe de nœuds en couches, avec zoom et panoramique',
|
||||
settingsViewModeTree: 'Arbre',
|
||||
settingsViewModeTreeDesc: 'Arbre à indentation classique, navigable au clavier',
|
||||
tasksWorkflowMember: 'Membre du workflow',
|
||||
tasksFoldCompleted: '✓ {count} terminés',
|
||||
tasksFoldExpand: 'Déplier les nœuds terminés',
|
||||
tasksFoldCollapse: 'Replier les nœuds terminés',
|
||||
tasksZoomIn: 'Zoom avant',
|
||||
tasksZoomOut: 'Zoom arrière',
|
||||
tasksZoomFit: 'Ajuster au canevas',
|
||||
tasksNodeJump: 'Ouvrir la transcription',
|
||||
tasksNodeState: 'État',
|
||||
tasksNodeMode: 'Mode',
|
||||
tasksNodeModel: 'Modèle',
|
||||
tasksNodeTeamRole: 'Rôle dans l’équipe',
|
||||
tasksNodeActivity: 'Dernière activité',
|
||||
tasksStateRunning: 'En cours',
|
||||
tasksStateIdle: 'Inactif',
|
||||
tasksStateDone: 'Terminé',
|
||||
tasksStateError: 'Erreur',
|
||||
teamBoard: 'Tableau des tâches d’équipe',
|
||||
teamChip: 'Équipe {members} membres · {tasks} tâches',
|
||||
teamMembers: 'Membres',
|
||||
teamTasksEmpty: 'Aucune tâche partagée pour l’instant',
|
||||
teamTaskCreate: 'Nouvelle tâche',
|
||||
teamTaskSubject: 'Sujet',
|
||||
teamTaskDescription: 'Description',
|
||||
teamTaskOwner: 'Responsable',
|
||||
teamTaskUnowned: 'Non réclamée',
|
||||
teamTaskPending: 'À faire',
|
||||
teamTaskInProgress: 'En cours',
|
||||
teamTaskCompleted: 'Terminée',
|
||||
teamTaskBlocked: 'Bloquée',
|
||||
teamTaskComplete: 'Terminer',
|
||||
teamTaskReopen: 'Rouvrir',
|
||||
teamTaskDelete: 'Supprimer',
|
||||
teamTaskDeleteConfirm: 'Confirmer la suppression',
|
||||
teamTaskEdit: 'Modifier',
|
||||
teamTaskSave: 'Enregistrer',
|
||||
teamTaskCancel: 'Annuler',
|
||||
teamTaskConflict: 'La tâche a été modifiée ailleurs ; actualisée',
|
||||
teamTaskError: 'Échec de l’opération : {message}',
|
||||
teamTaskDeleted: 'Supprimée',
|
||||
workflowRunning: 'En cours',
|
||||
workflowCompleted: 'Terminé',
|
||||
workflowCancelled: 'Annulé',
|
||||
workflowFailed: 'Échoué',
|
||||
workflowPhaseUnnamed: 'Phase sans nom',
|
||||
workflowMembers: '{done}/{total} membres',
|
||||
jobsAutoCollapsed: 'Replié automatiquement (nombreux agents)',
|
||||
catalogLoadFailed: '{count} branches n’ont pas pu être chargées',
|
||||
addPluginsTabCard: 'Ajouter un plugin d’onglet',
|
||||
addPluginsTabCardDesc: 'Enregistrer une nouvelle page de la barre latérale',
|
||||
addPluginsViewerCard: 'Ajouter un plugin d’aperçu',
|
||||
|
||||
@@ -390,6 +390,64 @@ export const hi: Record<string, string> = {
|
||||
jobKill: 'समाप्त',
|
||||
jobKillConfirm: 'समाप्त की पुष्टि के लिए फिर क्लिक करें',
|
||||
jobKillError: 'समाप्त विफल',
|
||||
tasksViewGraph: 'वर्कफ़्लो ग्राफ़',
|
||||
tasksViewTree: 'ट्री',
|
||||
tasksViewSwitchToGraph: 'वर्कफ़्लो ग्राफ़ पर स्विच करें',
|
||||
tasksViewSwitchToTree: 'ट्री पर स्विच करें',
|
||||
settingsViewModeTitle: 'कार्य पृष्ठ का डिफ़ॉल्ट दृश्य',
|
||||
settingsViewModeDesc: 'कार्य पृष्ठ खुलने पर डिफ़ॉल्ट रूप से कैसे दिखता है (पृष्ठ के अंदर अभी भी अस्थायी रूप से बदला जा सकता है)',
|
||||
settingsViewModeGraph: 'वर्कफ़्लो ग्राफ़',
|
||||
settingsViewModeGraphDesc: 'स्तरित नोड ग्राफ़, ज़ूम और पैन के साथ',
|
||||
settingsViewModeTree: 'ट्री',
|
||||
settingsViewModeTreeDesc: 'क्लासिक इंडेंटेड ट्री, कीबोर्ड से नेविगेट करने योग्य',
|
||||
tasksWorkflowMember: 'वर्कफ़्लो सदस्य',
|
||||
tasksFoldCompleted: '✓ {count} पूर्ण',
|
||||
tasksFoldExpand: 'पूर्ण नोड्स विस्तारित करें',
|
||||
tasksFoldCollapse: 'पूर्ण नोड्स संकुचित करें',
|
||||
tasksZoomIn: 'ज़ूम इन',
|
||||
tasksZoomOut: 'ज़ूम आउट',
|
||||
tasksZoomFit: 'कैनवास में फ़िट करें',
|
||||
tasksNodeJump: 'ट्रांसक्रिप्ट खोलें',
|
||||
tasksNodeState: 'स्थिति',
|
||||
tasksNodeMode: 'मोड',
|
||||
tasksNodeModel: 'मॉडल',
|
||||
tasksNodeTeamRole: 'टीम भूमिका',
|
||||
tasksNodeActivity: 'नवीनतम गतिविधि',
|
||||
tasksStateRunning: 'चल रहा है',
|
||||
tasksStateIdle: 'निष्क्रिय',
|
||||
tasksStateDone: 'पूर्ण',
|
||||
tasksStateError: 'त्रुटि',
|
||||
teamBoard: 'टीम कार्य बोर्ड',
|
||||
teamChip: 'टीम {members} सदस्य · {tasks} कार्य',
|
||||
teamMembers: 'सदस्य',
|
||||
teamTasksEmpty: 'अभी कोई साझा कार्य नहीं',
|
||||
teamTaskCreate: 'नया कार्य',
|
||||
teamTaskSubject: 'विषय',
|
||||
teamTaskDescription: 'विवरण',
|
||||
teamTaskOwner: 'स्वामी',
|
||||
teamTaskUnowned: 'बिना स्वामी',
|
||||
teamTaskPending: 'लंबित',
|
||||
teamTaskInProgress: 'प्रगति पर',
|
||||
teamTaskCompleted: 'पूर्ण',
|
||||
teamTaskBlocked: 'अवरुद्ध',
|
||||
teamTaskComplete: 'पूर्ण करें',
|
||||
teamTaskReopen: 'पुनः खोलें',
|
||||
teamTaskDelete: 'हटाएँ',
|
||||
teamTaskDeleteConfirm: 'हटाने की पुष्टि करें',
|
||||
teamTaskEdit: 'संपादित करें',
|
||||
teamTaskSave: 'सहेजें',
|
||||
teamTaskCancel: 'रद्द करें',
|
||||
teamTaskConflict: 'कार्य दूसरी जगह बदल गया; ताज़ा कर दिया गया',
|
||||
teamTaskError: 'ऑपरेशन विफल: {message}',
|
||||
teamTaskDeleted: 'हटाया गया',
|
||||
workflowRunning: 'चल रहा है',
|
||||
workflowCompleted: 'पूर्ण',
|
||||
workflowCancelled: 'रद्द किया गया',
|
||||
workflowFailed: 'विफल',
|
||||
workflowPhaseUnnamed: 'अनामित चरण',
|
||||
workflowMembers: '{done}/{total} सदस्य',
|
||||
jobsAutoCollapsed: 'एजेंट बहुत सारे, स्वतः संकुचित किया गया',
|
||||
catalogLoadFailed: '{count} ब्रांच लोड होने में विफल',
|
||||
addPluginsTabCard: 'टैब प्लगइन जोड़ें',
|
||||
addPluginsTabCardDesc: 'नया साइडबार पृष्ठ पंजीकृत करें',
|
||||
addPluginsViewerCard: 'पूर्वावलोकन प्लगइन जोड़ें',
|
||||
|
||||
@@ -388,6 +388,64 @@ export const id: Record<string, string> = {
|
||||
jobKill: 'Hentikan',
|
||||
jobKillConfirm: 'Klik lagi untuk mengonfirmasi penghentian',
|
||||
jobKillError: 'Penghentian gagal',
|
||||
tasksViewGraph: 'Graf alur kerja',
|
||||
tasksViewTree: 'Pohon',
|
||||
tasksViewSwitchToGraph: 'Beralih ke graf alur kerja',
|
||||
tasksViewSwitchToTree: 'Beralih ke pohon',
|
||||
settingsViewModeTitle: 'Tampilan default halaman Tasks',
|
||||
settingsViewModeDesc: 'Bagaimana halaman Tasks dirender saat dibuka (tombol di halaman tetap bisa mengalihkannya sementara)',
|
||||
settingsViewModeGraph: 'Graf alur kerja',
|
||||
settingsViewModeGraphDesc: 'Graf node berlapis dengan zoom dan pan',
|
||||
settingsViewModeTree: 'Pohon',
|
||||
settingsViewModeTreeDesc: 'Pohon indentasi klasik, dapat dinavigasi dengan keyboard',
|
||||
tasksWorkflowMember: 'Anggota alur kerja',
|
||||
tasksFoldCompleted: '✓ {count} selesai',
|
||||
tasksFoldExpand: 'Perluas node yang selesai',
|
||||
tasksFoldCollapse: 'Lipat node yang selesai',
|
||||
tasksZoomIn: 'Perbesar',
|
||||
tasksZoomOut: 'Perkecil',
|
||||
tasksZoomFit: 'Sesuaikan ke kanvas',
|
||||
tasksNodeJump: 'Buka transkrip',
|
||||
tasksNodeState: 'Status',
|
||||
tasksNodeMode: 'Mode',
|
||||
tasksNodeModel: 'Model',
|
||||
tasksNodeTeamRole: 'Peran tim',
|
||||
tasksNodeActivity: 'Aktivitas terbaru',
|
||||
tasksStateRunning: 'Berjalan',
|
||||
tasksStateIdle: 'Siaga',
|
||||
tasksStateDone: 'Selesai',
|
||||
tasksStateError: 'Error',
|
||||
teamBoard: 'Papan tugas tim',
|
||||
teamChip: 'Tim {members} anggota · {tasks} tugas',
|
||||
teamMembers: 'Anggota',
|
||||
teamTasksEmpty: 'Belum ada tugas bersama',
|
||||
teamTaskCreate: 'Tugas baru',
|
||||
teamTaskSubject: 'Judul',
|
||||
teamTaskDescription: 'Deskripsi',
|
||||
teamTaskOwner: 'Penanggung jawab',
|
||||
teamTaskUnowned: 'Belum diklaim',
|
||||
teamTaskPending: 'Tertunda',
|
||||
teamTaskInProgress: 'Sedang berjalan',
|
||||
teamTaskCompleted: 'Selesai',
|
||||
teamTaskBlocked: 'Terblokir',
|
||||
teamTaskComplete: 'Selesaikan',
|
||||
teamTaskReopen: 'Buka kembali',
|
||||
teamTaskDelete: 'Hapus',
|
||||
teamTaskDeleteConfirm: 'Konfirmasi hapus',
|
||||
teamTaskEdit: 'Edit',
|
||||
teamTaskSave: 'Simpan',
|
||||
teamTaskCancel: 'Batal',
|
||||
teamTaskConflict: 'Tugas berubah di tempat lain; sudah disegarkan',
|
||||
teamTaskError: 'Operasi gagal: {message}',
|
||||
teamTaskDeleted: 'Terhapus',
|
||||
workflowRunning: 'Berjalan',
|
||||
workflowCompleted: 'Selesai',
|
||||
workflowCancelled: 'Dibatalkan',
|
||||
workflowFailed: 'Gagal',
|
||||
workflowPhaseUnnamed: 'Fase tanpa nama',
|
||||
workflowMembers: '{done}/{total} anggota',
|
||||
jobsAutoCollapsed: 'Dilipat otomatis (agen banyak)',
|
||||
catalogLoadFailed: '{count} cabang gagal dimuat',
|
||||
addPluginsTabCard: 'Tambah plugin tab',
|
||||
addPluginsTabCardDesc: 'Daftarkan halaman sidebar baru',
|
||||
addPluginsViewerCard: 'Tambah plugin pratinjau',
|
||||
|
||||
@@ -381,6 +381,64 @@ export const it: Record<string, string> = {
|
||||
jobKill: 'Termina',
|
||||
jobKillConfirm: 'Faccia di nuovo clic per confermare la terminazione',
|
||||
jobKillError: 'Terminazione non riuscita',
|
||||
tasksViewGraph: 'Grafo del workflow',
|
||||
tasksViewTree: 'Albero',
|
||||
tasksViewSwitchToGraph: 'Passa al grafo del workflow',
|
||||
tasksViewSwitchToTree: 'Passa all’albero',
|
||||
settingsViewModeTitle: 'Vista predefinita della pagina Attività',
|
||||
settingsViewModeDesc: 'Come si apre la pagina Attività (l’interruttore in pagina permette comunque di cambiare al volo)',
|
||||
settingsViewModeGraph: 'Grafo del workflow',
|
||||
settingsViewModeGraphDesc: 'Grafo di nodi a livelli, con zoom e pan',
|
||||
settingsViewModeTree: 'Albero',
|
||||
settingsViewModeTreeDesc: 'Albero a rientri classico, navigabile da tastiera',
|
||||
tasksWorkflowMember: 'Membro del workflow',
|
||||
tasksFoldCompleted: '✓ {count} completati',
|
||||
tasksFoldExpand: 'Espandi i nodi completati',
|
||||
tasksFoldCollapse: 'Comprimi i nodi completati',
|
||||
tasksZoomIn: 'Ingrandisci',
|
||||
tasksZoomOut: 'Riduci',
|
||||
tasksZoomFit: 'Adatta all’area di disegno',
|
||||
tasksNodeJump: 'Apri trascrizione',
|
||||
tasksNodeState: 'Stato',
|
||||
tasksNodeMode: 'Modalità',
|
||||
tasksNodeModel: 'Modello',
|
||||
tasksNodeTeamRole: 'Ruolo nel team',
|
||||
tasksNodeActivity: 'Ultima attività',
|
||||
tasksStateRunning: 'In esecuzione',
|
||||
tasksStateIdle: 'In attesa',
|
||||
tasksStateDone: 'Completato',
|
||||
tasksStateError: 'Errore',
|
||||
teamBoard: 'Bacheca delle attività del team',
|
||||
teamChip: 'Team {members} membri · {tasks} attività',
|
||||
teamMembers: 'Membri',
|
||||
teamTasksEmpty: 'Ancora nessuna attività condivisa',
|
||||
teamTaskCreate: 'Nuova attività',
|
||||
teamTaskSubject: 'Titolo',
|
||||
teamTaskDescription: 'Descrizione',
|
||||
teamTaskOwner: 'Responsabile',
|
||||
teamTaskUnowned: 'Non assegnata',
|
||||
teamTaskPending: 'Da fare',
|
||||
teamTaskInProgress: 'In corso',
|
||||
teamTaskCompleted: 'Completata',
|
||||
teamTaskBlocked: 'Bloccata',
|
||||
teamTaskComplete: 'Completa',
|
||||
teamTaskReopen: 'Riapri',
|
||||
teamTaskDelete: 'Elimina',
|
||||
teamTaskDeleteConfirm: 'Conferma eliminazione',
|
||||
teamTaskEdit: 'Modifica',
|
||||
teamTaskSave: 'Salva',
|
||||
teamTaskCancel: 'Annulla',
|
||||
teamTaskConflict: 'L’attività è stata modificata da altri; aggiornata',
|
||||
teamTaskError: 'Operazione non riuscita: {message}',
|
||||
teamTaskDeleted: 'Eliminata',
|
||||
workflowRunning: 'In corso',
|
||||
workflowCompleted: 'Completato',
|
||||
workflowCancelled: 'Annullato',
|
||||
workflowFailed: 'Non riuscito',
|
||||
workflowPhaseUnnamed: 'Fase senza nome',
|
||||
workflowMembers: '{done}/{total} membri',
|
||||
jobsAutoCollapsed: 'Compresso automaticamente (molti agenti)',
|
||||
catalogLoadFailed: 'Impossibile caricare {count} branch',
|
||||
addPluginsTabCard: 'Aggiungi plugin scheda',
|
||||
addPluginsTabCardDesc: 'Registri una nuova pagina della barra laterale',
|
||||
addPluginsViewerCard: 'Aggiungi plugin di anteprima',
|
||||
|
||||
@@ -390,6 +390,64 @@ export const ja: Record<string, string> = {
|
||||
jobKill: '終了',
|
||||
jobKillConfirm: 'もう一度クリックして終了を確認',
|
||||
jobKillError: '終了に失敗',
|
||||
tasksViewGraph: 'ワークフロー図',
|
||||
tasksViewTree: 'ツリー',
|
||||
tasksViewSwitchToGraph: 'ワークフロー図に切替',
|
||||
tasksViewSwitchToTree: 'ツリーに切替',
|
||||
settingsViewModeTitle: 'タスク管理の既定ビュー',
|
||||
settingsViewModeDesc: 'タスク管理ページを開いたときの表示方法(ページ内で一時切替可能)',
|
||||
settingsViewModeGraph: 'ワークフロー図',
|
||||
settingsViewModeGraphDesc: '階層ノードグラフ(ズーム・パン対応)',
|
||||
settingsViewModeTree: 'ツリー',
|
||||
settingsViewModeTreeDesc: '従来のインデントツリー(キーボード操作可)',
|
||||
tasksWorkflowMember: 'ワークフローメンバー',
|
||||
tasksFoldCompleted: '✓ {count} 件完了',
|
||||
tasksFoldExpand: '完了ノードを展開',
|
||||
tasksFoldCollapse: '完了ノードを折りたたむ',
|
||||
tasksZoomIn: '拡大',
|
||||
tasksZoomOut: '縮小',
|
||||
tasksZoomFit: 'キャンバスに合わせる',
|
||||
tasksNodeJump: 'トランスクリプトを開く',
|
||||
tasksNodeState: '状態',
|
||||
tasksNodeMode: 'モード',
|
||||
tasksNodeModel: 'モデル',
|
||||
tasksNodeTeamRole: 'チーム役割',
|
||||
tasksNodeActivity: '最新アクティビティ',
|
||||
tasksStateRunning: '実行中',
|
||||
tasksStateIdle: '待機',
|
||||
tasksStateDone: '完了',
|
||||
tasksStateError: 'エラー',
|
||||
teamBoard: 'チームタスクボード',
|
||||
teamChip: 'チーム {members} 名 · {tasks} タスク',
|
||||
teamMembers: 'メンバー',
|
||||
teamTasksEmpty: '共有タスクはまだありません',
|
||||
teamTaskCreate: 'タスクを作成',
|
||||
teamTaskSubject: '件名',
|
||||
teamTaskDescription: '説明',
|
||||
teamTaskOwner: '担当',
|
||||
teamTaskUnowned: '未割当',
|
||||
teamTaskPending: '未着手',
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '完了',
|
||||
teamTaskBlocked: 'ブロック',
|
||||
teamTaskComplete: '完了',
|
||||
teamTaskReopen: '再オープン',
|
||||
teamTaskDelete: '削除',
|
||||
teamTaskDeleteConfirm: '削除を確認',
|
||||
teamTaskEdit: '編集',
|
||||
teamTaskSave: '保存',
|
||||
teamTaskCancel: 'キャンセル',
|
||||
teamTaskConflict: '他の場所で変更されました。最新に更新しました',
|
||||
teamTaskError: '操作に失敗: {message}',
|
||||
teamTaskDeleted: '削除済み',
|
||||
workflowRunning: '進行中',
|
||||
workflowCompleted: '完了',
|
||||
workflowCancelled: 'キャンセル',
|
||||
workflowFailed: '失敗',
|
||||
workflowPhaseUnnamed: '無名フェーズ',
|
||||
workflowMembers: '{done}/{total} メンバー',
|
||||
jobsAutoCollapsed: 'エージェント多数のため自動折りたたみ',
|
||||
catalogLoadFailed: '{count} 個のブランチの読み込みに失敗',
|
||||
addPluginsTabCard: 'Tab プラグインを追加',
|
||||
addPluginsTabCardDesc: '新しいサイドバーページを登録',
|
||||
addPluginsViewerCard: 'プレビュープラグインを追加',
|
||||
|
||||
@@ -382,6 +382,64 @@ export const ko: Record<string, string> = {
|
||||
jobKill: '종료',
|
||||
jobKillConfirm: '다시 클릭하여 종료 확인',
|
||||
jobKillError: '종료 실패',
|
||||
tasksViewGraph: '워크플로 그래프',
|
||||
tasksViewTree: '트리',
|
||||
tasksViewSwitchToGraph: '워크플로 그래프로 전환',
|
||||
tasksViewSwitchToTree: '트리로 전환',
|
||||
settingsViewModeTitle: '작업 관리 기본 보기',
|
||||
settingsViewModeDesc: '작업 관리 페이지를 열 때의 기본 표시 방식(페이지 안에서 언제든 임시 전환 가능)',
|
||||
settingsViewModeGraph: '워크플로 그래프',
|
||||
settingsViewModeGraphDesc: '계층형 노드 그래프, 확대/이동 가능',
|
||||
settingsViewModeTree: '트리',
|
||||
settingsViewModeTreeDesc: '클래식 들여쓰기 트리, 키보드 탐색 지원',
|
||||
tasksWorkflowMember: '워크플로 멤버',
|
||||
tasksFoldCompleted: '✓ {count} 완료됨',
|
||||
tasksFoldExpand: '완료된 노드 펼치기',
|
||||
tasksFoldCollapse: '완료된 노드 접기',
|
||||
tasksZoomIn: '확대',
|
||||
tasksZoomOut: '축소',
|
||||
tasksZoomFit: '캔버스에 맞추기',
|
||||
tasksNodeJump: '대화 기록 보기',
|
||||
tasksNodeState: '상태',
|
||||
tasksNodeMode: '모드',
|
||||
tasksNodeModel: '모델',
|
||||
tasksNodeTeamRole: '팀 역할',
|
||||
tasksNodeActivity: '최근 활동',
|
||||
tasksStateRunning: '실행 중',
|
||||
tasksStateIdle: '대기 중',
|
||||
tasksStateDone: '완료됨',
|
||||
tasksStateError: '오류',
|
||||
teamBoard: '팀 작업 보드',
|
||||
teamChip: '팀 멤버 {members}명 · 작업 {tasks}개',
|
||||
teamMembers: '멤버',
|
||||
teamTasksEmpty: '공유 작업이 없습니다',
|
||||
teamTaskCreate: '새 작업',
|
||||
teamTaskSubject: '제목',
|
||||
teamTaskDescription: '설명',
|
||||
teamTaskOwner: '담당자',
|
||||
teamTaskUnowned: '미배정',
|
||||
teamTaskPending: '할 일',
|
||||
teamTaskInProgress: '진행 중',
|
||||
teamTaskCompleted: '완료됨',
|
||||
teamTaskBlocked: '차단됨',
|
||||
teamTaskComplete: '완료',
|
||||
teamTaskReopen: '다시 열기',
|
||||
teamTaskDelete: '삭제',
|
||||
teamTaskDeleteConfirm: '삭제 확인',
|
||||
teamTaskEdit: '편집',
|
||||
teamTaskSave: '저장',
|
||||
teamTaskCancel: '취소',
|
||||
teamTaskConflict: '다른 사람이 작업을 수정해 새로고침했습니다',
|
||||
teamTaskError: '작업 실패: {message}',
|
||||
teamTaskDeleted: '삭제됨',
|
||||
workflowRunning: '진행 중',
|
||||
workflowCompleted: '완료됨',
|
||||
workflowCancelled: '취소됨',
|
||||
workflowFailed: '실패',
|
||||
workflowPhaseUnnamed: '이름 없는 단계',
|
||||
workflowMembers: '{done}/{total} 멤버',
|
||||
jobsAutoCollapsed: '에이전트가 많아 자동으로 접었습니다',
|
||||
catalogLoadFailed: '{count}개 브랜치 로드 실패',
|
||||
addPluginsTabCard: 'Tab 플러그인 추가',
|
||||
addPluginsTabCardDesc: '새 사이드바 페이지 등록',
|
||||
addPluginsViewerCard: '미리보기 플러그인 추가',
|
||||
|
||||
@@ -388,6 +388,64 @@ export const nl: Record<string, string> = {
|
||||
jobKill: 'Beëindigen',
|
||||
jobKillConfirm: 'Klik nogmaals om beëindigen te bevestigen',
|
||||
jobKillError: 'Beëindigen mislukt',
|
||||
tasksViewGraph: 'Workflowgrafiek',
|
||||
tasksViewTree: 'Boom',
|
||||
tasksViewSwitchToGraph: 'Omschakelen naar de workflowgrafiek',
|
||||
tasksViewSwitchToTree: 'Omschakelen naar de boom',
|
||||
settingsViewModeTitle: 'Standaardweergave van de takenpagina',
|
||||
settingsViewModeDesc: 'Hoe de takenpagina bij het openen wordt weergegeven (de schakelaar op de pagina wisselt nog steeds ad hoc)',
|
||||
settingsViewModeGraph: 'Workflowgrafiek',
|
||||
settingsViewModeGraphDesc: 'Gelaagde knoppengrafiek met in- en uitzoomen en pannen',
|
||||
settingsViewModeTree: 'Boom',
|
||||
settingsViewModeTreeDesc: 'Klassieke inspringboom, navigeerbaar met het toetsenbord',
|
||||
tasksWorkflowMember: 'Workflowlid',
|
||||
tasksFoldCompleted: '✓ {count} voltooid',
|
||||
tasksFoldExpand: 'Voltooide knopen uitklappen',
|
||||
tasksFoldCollapse: 'Voltooide knopen inklappen',
|
||||
tasksZoomIn: 'Inzoomen',
|
||||
tasksZoomOut: 'Uitzoomen',
|
||||
tasksZoomFit: 'Passend maken',
|
||||
tasksNodeJump: 'Transcript openen',
|
||||
tasksNodeState: 'Status',
|
||||
tasksNodeMode: 'Modus',
|
||||
tasksNodeModel: 'Model',
|
||||
tasksNodeTeamRole: 'Teamrol',
|
||||
tasksNodeActivity: 'Laatste activiteit',
|
||||
tasksStateRunning: 'Bezig',
|
||||
tasksStateIdle: 'Inactief',
|
||||
tasksStateDone: 'Voltooid',
|
||||
tasksStateError: 'Fout',
|
||||
teamBoard: 'Teamtakenbord',
|
||||
teamChip: 'Team {members} leden · {tasks} taken',
|
||||
teamMembers: 'Leden',
|
||||
teamTasksEmpty: 'Nog geen gedeelde taken',
|
||||
teamTaskCreate: 'Nieuwe taak',
|
||||
teamTaskSubject: 'Onderwerp',
|
||||
teamTaskDescription: 'Beschrijving',
|
||||
teamTaskOwner: 'Verantwoordelijke',
|
||||
teamTaskUnowned: 'Niet opgeëist',
|
||||
teamTaskPending: 'Te doen',
|
||||
teamTaskInProgress: 'Bezig',
|
||||
teamTaskCompleted: 'Voltooid',
|
||||
teamTaskBlocked: 'Geblokkeerd',
|
||||
teamTaskComplete: 'Voltooien',
|
||||
teamTaskReopen: 'Heropenen',
|
||||
teamTaskDelete: 'Verwijderen',
|
||||
teamTaskDeleteConfirm: 'Verwijderen bevestigen',
|
||||
teamTaskEdit: 'Bewerken',
|
||||
teamTaskSave: 'Opslaan',
|
||||
teamTaskCancel: 'Annuleren',
|
||||
teamTaskConflict: 'De taak is elders gewijzigd; vernieuwd',
|
||||
teamTaskError: 'Bewerking mislukt: {message}',
|
||||
teamTaskDeleted: 'Verwijderd',
|
||||
workflowRunning: 'Bezig',
|
||||
workflowCompleted: 'Voltooid',
|
||||
workflowCancelled: 'Geannuleerd',
|
||||
workflowFailed: 'Mislukt',
|
||||
workflowPhaseUnnamed: 'Naamloze fase',
|
||||
workflowMembers: '{done}/{total} leden',
|
||||
jobsAutoCollapsed: 'Automatisch ingeklapt (veel agents)',
|
||||
catalogLoadFailed: '{count} branches konden niet worden geladen',
|
||||
addPluginsTabCard: 'Tabblad-plugins toevoegen',
|
||||
addPluginsTabCardDesc: 'Een nieuwe zijbalkpagina registreren',
|
||||
addPluginsViewerCard: 'Preview-plugins toevoegen',
|
||||
|
||||
@@ -392,6 +392,64 @@ export const pl: Record<string, string> = {
|
||||
jobKill: 'Zabij',
|
||||
jobKillConfirm: 'Kliknij ponownie, aby potwierdzić zabicie',
|
||||
jobKillError: 'Zabicie nie powiodło się',
|
||||
tasksViewGraph: 'Graf przepływu pracy',
|
||||
tasksViewTree: 'Drzewo',
|
||||
tasksViewSwitchToGraph: 'Przełącz na graf przepływu pracy',
|
||||
tasksViewSwitchToTree: 'Przełącz na drzewo',
|
||||
settingsViewModeTitle: 'Domyślny widok strony zadań',
|
||||
settingsViewModeDesc: 'Jak strona zadań renderuje się po otwarciu (przełącznik na stronie nadal działa doraźnie)',
|
||||
settingsViewModeGraph: 'Graf przepływu pracy',
|
||||
settingsViewModeGraphDesc: 'Warstwowy graf węzłów ze zoomowaniem i przesuwaniem',
|
||||
settingsViewModeTree: 'Drzewo',
|
||||
settingsViewModeTreeDesc: 'Klasyczne drzewo wcięć, nawigowalne z klawiatury',
|
||||
tasksWorkflowMember: 'Członek przepływu pracy',
|
||||
tasksFoldCompleted: '✓ {count} ukończonych',
|
||||
tasksFoldExpand: 'Rozwiń ukończone węzły',
|
||||
tasksFoldCollapse: 'Zwiń ukończone węzły',
|
||||
tasksZoomIn: 'Przybliż',
|
||||
tasksZoomOut: 'Oddal',
|
||||
tasksZoomFit: 'Dopasuj do kanwy',
|
||||
tasksNodeJump: 'Otwórz transkrypcję',
|
||||
tasksNodeState: 'Stan',
|
||||
tasksNodeMode: 'Tryb',
|
||||
tasksNodeModel: 'Model',
|
||||
tasksNodeTeamRole: 'Rola w zespole',
|
||||
tasksNodeActivity: 'Ostatnia aktywność',
|
||||
tasksStateRunning: 'Działa',
|
||||
tasksStateIdle: 'Gotowe',
|
||||
tasksStateDone: 'Ukończone',
|
||||
tasksStateError: 'Błąd',
|
||||
teamBoard: 'Tablica zadań zespołu',
|
||||
teamChip: 'Zespół {members} członków · {tasks} zadań',
|
||||
teamMembers: 'Członkowie',
|
||||
teamTasksEmpty: 'Brak wspólnych zadań',
|
||||
teamTaskCreate: 'Nowe zadanie',
|
||||
teamTaskSubject: 'Temat',
|
||||
teamTaskDescription: 'Opis',
|
||||
teamTaskOwner: 'Odpowiedzialny',
|
||||
teamTaskUnowned: 'Nieprzypisane',
|
||||
teamTaskPending: 'Do zrobienia',
|
||||
teamTaskInProgress: 'W toku',
|
||||
teamTaskCompleted: 'Ukończone',
|
||||
teamTaskBlocked: 'Zablokowane',
|
||||
teamTaskComplete: 'Ukończ',
|
||||
teamTaskReopen: 'Otwórz ponownie',
|
||||
teamTaskDelete: 'Usuń',
|
||||
teamTaskDeleteConfirm: 'Potwierdź usunięcie',
|
||||
teamTaskEdit: 'Edytuj',
|
||||
teamTaskSave: 'Zapisz',
|
||||
teamTaskCancel: 'Anuluj',
|
||||
teamTaskConflict: 'Zadanie zostało zmienione przez kogoś innego; odświeżono',
|
||||
teamTaskError: 'Operacja nie powiodła się: {message}',
|
||||
teamTaskDeleted: 'Usunięte',
|
||||
workflowRunning: 'W toku',
|
||||
workflowCompleted: 'Ukończony',
|
||||
workflowCancelled: 'Anulowany',
|
||||
workflowFailed: 'Niepowodzenie',
|
||||
workflowPhaseUnnamed: 'Faza bez nazwy',
|
||||
workflowMembers: '{done}/{total} członków',
|
||||
jobsAutoCollapsed: 'Automatycznie zwinięto (wiele agentów)',
|
||||
catalogLoadFailed: 'Nie udało się załadować {count} gałęzi',
|
||||
addPluginsTabCard: 'Dodaj wtyczki kart',
|
||||
addPluginsTabCardDesc: 'Zarejestruj nową stronę panelu bocznego',
|
||||
addPluginsViewerCard: 'Dodaj wtyczki podglądu',
|
||||
|
||||
@@ -373,6 +373,64 @@ export const pt: Record<string, string> = {
|
||||
jobKill: 'Encerrar',
|
||||
jobKillConfirm: 'Clique novamente para confirmar o encerramento',
|
||||
jobKillError: 'Falha ao encerrar',
|
||||
tasksViewGraph: 'Grafo do workflow',
|
||||
tasksViewTree: 'Árvore',
|
||||
tasksViewSwitchToGraph: 'Mudar para o grafo do workflow',
|
||||
tasksViewSwitchToTree: 'Mudar para a árvore',
|
||||
settingsViewModeTitle: 'Visualização padrão da página de tarefas',
|
||||
settingsViewModeDesc: 'Como a página de tarefas é exibida ao abrir (o seletor na própria página ainda permite alternar pontualmente)',
|
||||
settingsViewModeGraph: 'Grafo do workflow',
|
||||
settingsViewModeGraphDesc: 'Grafo de nós em camadas, com zoom e deslocamento',
|
||||
settingsViewModeTree: 'Árvore',
|
||||
settingsViewModeTreeDesc: 'Árvore com indentação clássica, navegável pelo teclado',
|
||||
tasksWorkflowMember: 'Membro do workflow',
|
||||
tasksFoldCompleted: '✓ {count} concluídos',
|
||||
tasksFoldExpand: 'Expandir os nós concluídos',
|
||||
tasksFoldCollapse: 'Recolher os nós concluídos',
|
||||
tasksZoomIn: 'Aproximar',
|
||||
tasksZoomOut: 'Afastar',
|
||||
tasksZoomFit: 'Ajustar à tela',
|
||||
tasksNodeJump: 'Abrir transcrição',
|
||||
tasksNodeState: 'Estado',
|
||||
tasksNodeMode: 'Modo',
|
||||
tasksNodeModel: 'Modelo',
|
||||
tasksNodeTeamRole: 'Função na equipe',
|
||||
tasksNodeActivity: 'Atividade recente',
|
||||
tasksStateRunning: 'Em execução',
|
||||
tasksStateIdle: 'Em espera',
|
||||
tasksStateDone: 'Concluído',
|
||||
tasksStateError: 'Erro',
|
||||
teamBoard: 'Quadro de tarefas da equipe',
|
||||
teamChip: 'Equipe {members} membros · {tasks} tarefas',
|
||||
teamMembers: 'Membros',
|
||||
teamTasksEmpty: 'Ainda não há tarefas compartilhadas',
|
||||
teamTaskCreate: 'Nova tarefa',
|
||||
teamTaskSubject: 'Assunto',
|
||||
teamTaskDescription: 'Descrição',
|
||||
teamTaskOwner: 'Responsável',
|
||||
teamTaskUnowned: 'Não reivindicada',
|
||||
teamTaskPending: 'Pendente',
|
||||
teamTaskInProgress: 'Em andamento',
|
||||
teamTaskCompleted: 'Concluída',
|
||||
teamTaskBlocked: 'Bloqueada',
|
||||
teamTaskComplete: 'Concluir',
|
||||
teamTaskReopen: 'Reabrir',
|
||||
teamTaskDelete: 'Excluir',
|
||||
teamTaskDeleteConfirm: 'Confirmar exclusão',
|
||||
teamTaskEdit: 'Editar',
|
||||
teamTaskSave: 'Salvar',
|
||||
teamTaskCancel: 'Cancelar',
|
||||
teamTaskConflict: 'A tarefa foi alterada por outra pessoa; atualizada',
|
||||
teamTaskError: 'Operação falhou: {message}',
|
||||
teamTaskDeleted: 'Excluída',
|
||||
workflowRunning: 'Em execução',
|
||||
workflowCompleted: 'Concluído',
|
||||
workflowCancelled: 'Cancelado',
|
||||
workflowFailed: 'Falhou',
|
||||
workflowPhaseUnnamed: 'Fase sem nome',
|
||||
workflowMembers: '{done}/{total} membros',
|
||||
jobsAutoCollapsed: 'Recolhido automaticamente (muitos agentes)',
|
||||
catalogLoadFailed: 'Falha ao carregar {count} branches',
|
||||
addPluginsTabCard: 'Adicionar plugins de aba',
|
||||
addPluginsTabCardDesc: 'Registrar uma nova página da barra lateral',
|
||||
addPluginsViewerCard: 'Adicionar plugins de pré-visualização',
|
||||
|
||||
@@ -388,6 +388,64 @@ export const ru: Record<string, string> = {
|
||||
jobKill: 'Завершить',
|
||||
jobKillConfirm: 'Нажмите ещё раз, чтобы подтвердить завершение',
|
||||
jobKillError: 'Не удалось завершить',
|
||||
tasksViewGraph: 'Граф рабочего процесса',
|
||||
tasksViewTree: 'Дерево',
|
||||
tasksViewSwitchToGraph: 'Переключить на граф рабочего процесса',
|
||||
tasksViewSwitchToTree: 'Переключить на дерево',
|
||||
settingsViewModeTitle: 'Вид страницы задач по умолчанию',
|
||||
settingsViewModeDesc: 'Как отображается страница задач при открытии (переключатель на странице всё ещё меняет её на лету)',
|
||||
settingsViewModeGraph: 'Граф рабочего процесса',
|
||||
settingsViewModeGraphDesc: 'Многоуровневый граф узлов с масштабированием и панорамированием',
|
||||
settingsViewModeTree: 'Дерево',
|
||||
settingsViewModeTreeDesc: 'Классическое дерево с отступами, навигация с клавиатуры',
|
||||
tasksWorkflowMember: 'Участник рабочего процесса',
|
||||
tasksFoldCompleted: '✓ {count} завершено',
|
||||
tasksFoldExpand: 'Развернуть завершённые узлы',
|
||||
tasksFoldCollapse: 'Свернуть завершённые узлы',
|
||||
tasksZoomIn: 'Увеличить',
|
||||
tasksZoomOut: 'Уменьшить',
|
||||
tasksZoomFit: 'По размеру холста',
|
||||
tasksNodeJump: 'Открыть транскрипт',
|
||||
tasksNodeState: 'Состояние',
|
||||
tasksNodeMode: 'Режим',
|
||||
tasksNodeModel: 'Модель',
|
||||
tasksNodeTeamRole: 'Роль в команде',
|
||||
tasksNodeActivity: 'Последняя активность',
|
||||
tasksStateRunning: 'Выполняется',
|
||||
tasksStateIdle: 'Ожидает',
|
||||
tasksStateDone: 'Завершено',
|
||||
tasksStateError: 'Ошибка',
|
||||
teamBoard: 'Доска задач команды',
|
||||
teamChip: 'Команда: {members} участников · {tasks} задач',
|
||||
teamMembers: 'Участники',
|
||||
teamTasksEmpty: 'Общих задач пока нет',
|
||||
teamTaskCreate: 'Новая задача',
|
||||
teamTaskSubject: 'Тема',
|
||||
teamTaskDescription: 'Описание',
|
||||
teamTaskOwner: 'Ответственный',
|
||||
teamTaskUnowned: 'Не занята',
|
||||
teamTaskPending: 'Ожидает',
|
||||
teamTaskInProgress: 'В работе',
|
||||
teamTaskCompleted: 'Завершена',
|
||||
teamTaskBlocked: 'Заблокирована',
|
||||
teamTaskComplete: 'Завершить',
|
||||
teamTaskReopen: 'Открыть снова',
|
||||
teamTaskDelete: 'Удалить',
|
||||
teamTaskDeleteConfirm: 'Подтвердите удаление',
|
||||
teamTaskEdit: 'Изменить',
|
||||
teamTaskSave: 'Сохранить',
|
||||
teamTaskCancel: 'Отмена',
|
||||
teamTaskConflict: 'Задача изменена другим участником, обновлено',
|
||||
teamTaskError: 'Не удалось выполнить: {message}',
|
||||
teamTaskDeleted: 'Удалена',
|
||||
workflowRunning: 'Выполняется',
|
||||
workflowCompleted: 'Завершён',
|
||||
workflowCancelled: 'Отменён',
|
||||
workflowFailed: 'Сбой',
|
||||
workflowPhaseUnnamed: 'Безымянная фаза',
|
||||
workflowMembers: '{done}/{total} участников',
|
||||
jobsAutoCollapsed: 'Свернуто автоматически (много агентов)',
|
||||
catalogLoadFailed: 'Не удалось загрузить ветки: {count}',
|
||||
addPluginsTabCard: 'Добавить плагины вкладок',
|
||||
addPluginsTabCardDesc: 'Зарегистрировать новую страницу боковой панели',
|
||||
addPluginsViewerCard: 'Добавить плагины предпросмотра',
|
||||
|
||||
@@ -373,6 +373,64 @@ export const sv: Record<string, string> = {
|
||||
jobKill: 'Döda',
|
||||
jobKillConfirm: 'Klicka igen för att bekräfta dödande',
|
||||
jobKillError: 'Kunde inte döda',
|
||||
tasksViewGraph: 'Arbetsflödesgraf',
|
||||
tasksViewTree: 'Träd',
|
||||
tasksViewSwitchToGraph: 'Växla till arbetsflödesgrafen',
|
||||
tasksViewSwitchToTree: 'Växla till trädet',
|
||||
settingsViewModeTitle: 'Standardvy för uppgiftssidan',
|
||||
settingsViewModeDesc: 'Hur uppgiftssidan visas vid öppning (växeln på sidan ändrar den fortfarande ad hoc)',
|
||||
settingsViewModeGraph: 'Arbetsflödesgraf',
|
||||
settingsViewModeGraphDesc: 'Lagerindelad nodgraf med zoom och panorering',
|
||||
settingsViewModeTree: 'Träd',
|
||||
settingsViewModeTreeDesc: 'Klassiskt indragsträd, navigerbart med tangentbord',
|
||||
tasksWorkflowMember: 'Arbetsflödesmedlem',
|
||||
tasksFoldCompleted: '✓ {count} klara',
|
||||
tasksFoldExpand: 'Visa färdiga noder',
|
||||
tasksFoldCollapse: 'Dölj färdiga noder',
|
||||
tasksZoomIn: 'Zooma in',
|
||||
tasksZoomOut: 'Zooma ut',
|
||||
tasksZoomFit: 'Anpassa till ytan',
|
||||
tasksNodeJump: 'Öppna transkription',
|
||||
tasksNodeState: 'Status',
|
||||
tasksNodeMode: 'Läge',
|
||||
tasksNodeModel: 'Modell',
|
||||
tasksNodeTeamRole: 'Teamroll',
|
||||
tasksNodeActivity: 'Senaste aktivitet',
|
||||
tasksStateRunning: 'Körs',
|
||||
tasksStateIdle: 'Vilar',
|
||||
tasksStateDone: 'Klar',
|
||||
tasksStateError: 'Fel',
|
||||
teamBoard: 'Teamuppgiftstavla',
|
||||
teamChip: 'Team {members} medlemmar · {tasks} uppgifter',
|
||||
teamMembers: 'Medlemmar',
|
||||
teamTasksEmpty: 'Inga delade uppgifter än',
|
||||
teamTaskCreate: 'Ny uppgift',
|
||||
teamTaskSubject: 'Ämne',
|
||||
teamTaskDescription: 'Beskrivning',
|
||||
teamTaskOwner: 'Ansvarig',
|
||||
teamTaskUnowned: 'Ej tilldelad',
|
||||
teamTaskPending: 'Väntar',
|
||||
teamTaskInProgress: 'Pågår',
|
||||
teamTaskCompleted: 'Klar',
|
||||
teamTaskBlocked: 'Blockerad',
|
||||
teamTaskComplete: 'Färdigställ',
|
||||
teamTaskReopen: 'Öppna igen',
|
||||
teamTaskDelete: 'Ta bort',
|
||||
teamTaskDeleteConfirm: 'Bekräfta borttagning',
|
||||
teamTaskEdit: 'Redigera',
|
||||
teamTaskSave: 'Spara',
|
||||
teamTaskCancel: 'Avbryt',
|
||||
teamTaskConflict: 'Uppgiften ändrades av någon annan, har uppdaterats',
|
||||
teamTaskError: 'Åtgärden misslyckades: {message}',
|
||||
teamTaskDeleted: 'Borttagen',
|
||||
workflowRunning: 'Körs',
|
||||
workflowCompleted: 'Slutförd',
|
||||
workflowCancelled: 'Avbruten',
|
||||
workflowFailed: 'Misslyckad',
|
||||
workflowPhaseUnnamed: 'Namnlös fas',
|
||||
workflowMembers: '{done}/{total} medlemmar',
|
||||
jobsAutoCollapsed: 'Auto-dold (många agenter)',
|
||||
catalogLoadFailed: '{count} grenar kunde inte läsas in',
|
||||
addPluginsTabCard: 'Lägg till flik-plugins',
|
||||
addPluginsTabCardDesc: 'Registrera en ny sidopanelssida',
|
||||
addPluginsViewerCard: 'Lägg till förhandsgransknings-plugins',
|
||||
|
||||
@@ -390,6 +390,64 @@ export const th: Record<string, string> = {
|
||||
jobKill: 'Kill',
|
||||
jobKillConfirm: 'คลิกอีกครั้งเพื่อยืนยันการ kill',
|
||||
jobKillError: 'Kill ล้มเหลว',
|
||||
tasksViewGraph: 'กราฟเวิร์กโฟลว์',
|
||||
tasksViewTree: 'มุมมองต้นไม้',
|
||||
tasksViewSwitchToGraph: 'สลับเป็นกราฟเวิร์กโฟลว์',
|
||||
tasksViewSwitchToTree: 'สลับเป็นมุมมองต้นไม้',
|
||||
settingsViewModeTitle: 'มุมมองเริ่มต้นของหน้างาน',
|
||||
settingsViewModeDesc: 'รูปแบบการแสดงหน้างานเมื่อเปิด (ปุ่มสลับในหน้ายังใช้สลับชั่วคราวได้)',
|
||||
settingsViewModeGraph: 'กราฟเวิร์กโฟลว์',
|
||||
settingsViewModeGraphDesc: 'กราฟโหนดแบบชั้น ซูมและเลื่อนได้',
|
||||
settingsViewModeTree: 'มุมมองต้นไม้',
|
||||
settingsViewModeTreeDesc: 'ต้นไม้แบบเยื้องคลาสสิก นำทางด้วยคีย์บอร์ดได้',
|
||||
tasksWorkflowMember: 'สมาชิกเวิร์กโฟลว์',
|
||||
tasksFoldCompleted: '✓ เสร็จแล้ว {count}',
|
||||
tasksFoldExpand: 'ขยายโหนดที่เสร็จแล้ว',
|
||||
tasksFoldCollapse: 'ย่อโหนดที่เสร็จแล้ว',
|
||||
tasksZoomIn: 'ซูมเข้า',
|
||||
tasksZoomOut: 'ซูมออก',
|
||||
tasksZoomFit: 'พอดีผืนผ้าใบ',
|
||||
tasksNodeJump: 'เปิดบันทึกถอดความ',
|
||||
tasksNodeState: 'สถานะ',
|
||||
tasksNodeMode: 'โหมด',
|
||||
tasksNodeModel: 'โมเดล',
|
||||
tasksNodeTeamRole: 'บทบาทในทีม',
|
||||
tasksNodeActivity: 'กิจกรรมล่าสุด',
|
||||
tasksStateRunning: 'กำลังทำงาน',
|
||||
tasksStateIdle: 'พร้อม',
|
||||
tasksStateDone: 'เสร็จแล้ว',
|
||||
tasksStateError: 'ข้อผิดพลาด',
|
||||
teamBoard: 'บอร์ดงานของทีม',
|
||||
teamChip: 'ทีม {members} สมาชิก · {tasks} งาน',
|
||||
teamMembers: 'สมาชิก',
|
||||
teamTasksEmpty: 'ยังไม่มีงานที่แชร์',
|
||||
teamTaskCreate: 'งานใหม่',
|
||||
teamTaskSubject: 'หัวข้อ',
|
||||
teamTaskDescription: 'คำอธิบาย',
|
||||
teamTaskOwner: 'ผู้รับผิดชอบ',
|
||||
teamTaskUnowned: 'ยังไม่มีผู้รับ',
|
||||
teamTaskPending: 'รอดำเนินการ',
|
||||
teamTaskInProgress: 'กำลังดำเนินการ',
|
||||
teamTaskCompleted: 'เสร็จแล้ว',
|
||||
teamTaskBlocked: 'ถูกบล็อก',
|
||||
teamTaskComplete: 'ทำให้เสร็จ',
|
||||
teamTaskReopen: 'เปิดใหม่',
|
||||
teamTaskDelete: 'ลบ',
|
||||
teamTaskDeleteConfirm: 'ยืนยันการลบ',
|
||||
teamTaskEdit: 'แก้ไข',
|
||||
teamTaskSave: 'บันทึก',
|
||||
teamTaskCancel: 'ยกเลิก',
|
||||
teamTaskConflict: 'งานถูกแก้ไขโดยผู้อื่น รีเฟรชแล้ว',
|
||||
teamTaskError: 'ดำเนินการไม่สำเร็จ: {message}',
|
||||
teamTaskDeleted: 'ลบแล้ว',
|
||||
workflowRunning: 'กำลังทำงาน',
|
||||
workflowCompleted: 'เสร็จสิ้น',
|
||||
workflowCancelled: 'ยกเลิกแล้ว',
|
||||
workflowFailed: 'ล้มเหลว',
|
||||
workflowPhaseUnnamed: 'เฟสไม่มีชื่อ',
|
||||
workflowMembers: '{done}/{total} สมาชิก',
|
||||
jobsAutoCollapsed: 'ย่ออัตโนมัติ (เอเจนต์จำนวนมาก)',
|
||||
catalogLoadFailed: 'โหลดสาขาไม่สำเร็จ {count} รายการ',
|
||||
addPluginsTabCard: 'เพิ่มปลั๊กอิน tab',
|
||||
addPluginsTabCardDesc: 'ลงทะเบียนหน้าแถบด้านข้างใหม่',
|
||||
addPluginsViewerCard: 'เพิ่มปลั๊กอินพรีวิว',
|
||||
|
||||
@@ -390,6 +390,64 @@ export const tr: Record<string, string> = {
|
||||
jobKill: 'Sonlandır',
|
||||
jobKillConfirm: 'Sonlandırmayı onaylamak için tekrar tıklayın',
|
||||
jobKillError: 'Sonlandırma başarısız',
|
||||
tasksViewGraph: 'İş akışı grafiği',
|
||||
tasksViewTree: 'Ağaç',
|
||||
tasksViewSwitchToGraph: 'İş akışı grafiğine geç',
|
||||
tasksViewSwitchToTree: 'Ağaç görünümüne geç',
|
||||
settingsViewModeTitle: 'Görevler sayfası varsayılan görünümü',
|
||||
settingsViewModeDesc: 'Görevler sayfasının açılıştaki görünümü (sayfa içi düğme yine anlık değiştirebilir)',
|
||||
settingsViewModeGraph: 'İş akışı grafiği',
|
||||
settingsViewModeGraphDesc: 'Yakınlaştırma ve kaydırma ile katmanlı düğüm grafiği',
|
||||
settingsViewModeTree: 'Ağaç',
|
||||
settingsViewModeTreeDesc: 'Klasik girintili ağaç, klavye ile gezilebilir',
|
||||
tasksWorkflowMember: 'İş akışı üyesi',
|
||||
tasksFoldCompleted: '✓ {count} tamamlandı',
|
||||
tasksFoldExpand: 'Tamamlanan düğümleri genişlet',
|
||||
tasksFoldCollapse: 'Tamamlanan düğümleri daralt',
|
||||
tasksZoomIn: 'Yakınlaştır',
|
||||
tasksZoomOut: 'Uzaklaştır',
|
||||
tasksZoomFit: 'Tuvale sığdır',
|
||||
tasksNodeJump: 'Transkripti aç',
|
||||
tasksNodeState: 'Durum',
|
||||
tasksNodeMode: 'Mod',
|
||||
tasksNodeModel: 'Model',
|
||||
tasksNodeTeamRole: 'Takım rolü',
|
||||
tasksNodeActivity: 'Son etkinlik',
|
||||
tasksStateRunning: 'Çalışıyor',
|
||||
tasksStateIdle: 'Beklemede',
|
||||
tasksStateDone: 'Tamamlandı',
|
||||
tasksStateError: 'Hata',
|
||||
teamBoard: 'Takım görev panosu',
|
||||
teamChip: 'Takım {members} üye · {tasks} görev',
|
||||
teamMembers: 'Üyeler',
|
||||
teamTasksEmpty: 'Henüz paylaşılan görev yok',
|
||||
teamTaskCreate: 'Yeni görev',
|
||||
teamTaskSubject: 'Konu',
|
||||
teamTaskDescription: 'Açıklama',
|
||||
teamTaskOwner: 'Sahip',
|
||||
teamTaskUnowned: 'Sahipsiz',
|
||||
teamTaskPending: 'Bekliyor',
|
||||
teamTaskInProgress: 'Devam ediyor',
|
||||
teamTaskCompleted: 'Tamamlandı',
|
||||
teamTaskBlocked: 'Engelli',
|
||||
teamTaskComplete: 'Tamamla',
|
||||
teamTaskReopen: 'Yeniden aç',
|
||||
teamTaskDelete: 'Sil',
|
||||
teamTaskDeleteConfirm: 'Silmeyi onayla',
|
||||
teamTaskEdit: 'Düzenle',
|
||||
teamTaskSave: 'Kaydet',
|
||||
teamTaskCancel: 'İptal',
|
||||
teamTaskConflict: 'Görev başkası tarafından değiştirildi, güncellendi',
|
||||
teamTaskError: 'İşlem başarısız: {message}',
|
||||
teamTaskDeleted: 'Silindi',
|
||||
workflowRunning: 'Çalışıyor',
|
||||
workflowCompleted: 'Tamamlandı',
|
||||
workflowCancelled: 'İptal edildi',
|
||||
workflowFailed: 'Başarısız',
|
||||
workflowPhaseUnnamed: 'Adsız aşama',
|
||||
workflowMembers: '{done}/{total} üye',
|
||||
jobsAutoCollapsed: 'Otomatik daraltıldı (çok sayıda ajan)',
|
||||
catalogLoadFailed: '{count} dal yüklenemedi',
|
||||
addPluginsTabCard: 'Sekme eklentileri ekle',
|
||||
addPluginsTabCardDesc: 'Yeni bir kenar çubuğu sayfası kaydet',
|
||||
addPluginsViewerCard: 'Önizleme eklentileri ekle',
|
||||
|
||||
@@ -390,6 +390,64 @@ export const vi: Record<string, string> = {
|
||||
jobKill: 'Hủy',
|
||||
jobKillConfirm: 'Nhấp lại để xác nhận hủy',
|
||||
jobKillError: 'Hủy thất bại',
|
||||
tasksViewGraph: 'Đồ thị quy trình',
|
||||
tasksViewTree: 'Cây',
|
||||
tasksViewSwitchToGraph: 'Chuyển sang đồ thị quy trình',
|
||||
tasksViewSwitchToTree: 'Chuyển sang chế độ cây',
|
||||
settingsViewModeTitle: 'Chế độ xem mặc định của trang tác vụ',
|
||||
settingsViewModeDesc: 'Cách trang tác vụ hiển thị khi mở (nút chuyển trên trang vẫn đổi tùy lúc)',
|
||||
settingsViewModeGraph: 'Đồ thị quy trình',
|
||||
settingsViewModeGraphDesc: 'Đồ thị nút phân tầng với thu phóng và di chuyển',
|
||||
settingsViewModeTree: 'Cây',
|
||||
settingsViewModeTreeDesc: 'Cây thụt lề cổ điển, điều hướng được bằng bàn phím',
|
||||
tasksWorkflowMember: 'Thành viên quy trình',
|
||||
tasksFoldCompleted: '✓ {count} hoàn thành',
|
||||
tasksFoldExpand: 'Mở rộng các nút đã hoàn thành',
|
||||
tasksFoldCollapse: 'Thu gọn các nút đã hoàn thành',
|
||||
tasksZoomIn: 'Phóng to',
|
||||
tasksZoomOut: 'Thu nhỏ',
|
||||
tasksZoomFit: 'Vừa khung vẽ',
|
||||
tasksNodeJump: 'Mở bản ghi hội thoại',
|
||||
tasksNodeState: 'Trạng thái',
|
||||
tasksNodeMode: 'Chế độ',
|
||||
tasksNodeModel: 'Mô hình',
|
||||
tasksNodeTeamRole: 'Vai trò trong nhóm',
|
||||
tasksNodeActivity: 'Hoạt động gần đây',
|
||||
tasksStateRunning: 'Đang chạy',
|
||||
tasksStateIdle: 'Nhàn rỗi',
|
||||
tasksStateDone: 'Hoàn thành',
|
||||
tasksStateError: 'Lỗi',
|
||||
teamBoard: 'Bảng tác vụ nhóm',
|
||||
teamChip: 'Nhóm {members} thành viên · {tasks} tác vụ',
|
||||
teamMembers: 'Thành viên',
|
||||
teamTasksEmpty: 'Chưa có tác vụ dùng chung',
|
||||
teamTaskCreate: 'Tác vụ mới',
|
||||
teamTaskSubject: 'Chủ đề',
|
||||
teamTaskDescription: 'Mô tả',
|
||||
teamTaskOwner: 'Người phụ trách',
|
||||
teamTaskUnowned: 'Chưa nhận',
|
||||
teamTaskPending: 'Chờ xử lý',
|
||||
teamTaskInProgress: 'Đang tiến hành',
|
||||
teamTaskCompleted: 'Hoàn thành',
|
||||
teamTaskBlocked: 'Bị chặn',
|
||||
teamTaskComplete: 'Hoàn tất',
|
||||
teamTaskReopen: 'Mở lại',
|
||||
teamTaskDelete: 'Xóa',
|
||||
teamTaskDeleteConfirm: 'Xác nhận xóa',
|
||||
teamTaskEdit: 'Sửa',
|
||||
teamTaskSave: 'Lưu',
|
||||
teamTaskCancel: 'Hủy',
|
||||
teamTaskConflict: 'Tác vụ đã bị người khác thay đổi, đã làm mới',
|
||||
teamTaskError: 'Thao tác thất bại: {message}',
|
||||
teamTaskDeleted: 'Đã xóa',
|
||||
workflowRunning: 'Đang chạy',
|
||||
workflowCompleted: 'Hoàn thành',
|
||||
workflowCancelled: 'Đã hủy',
|
||||
workflowFailed: 'Thất bại',
|
||||
workflowPhaseUnnamed: 'Giai đoạn chưa đặt tên',
|
||||
workflowMembers: '{done}/{total} thành viên',
|
||||
jobsAutoCollapsed: 'Tự động thu gọn (nhiều agent)',
|
||||
catalogLoadFailed: '{count} nhánh không tải được',
|
||||
addPluginsTabCard: 'Thêm plugin tab',
|
||||
addPluginsTabCardDesc: 'Đăng ký trang thanh bên mới',
|
||||
addPluginsViewerCard: 'Thêm plugin xem trước',
|
||||
|
||||
@@ -405,6 +405,64 @@ export const zhHK: Record<string, string> = {
|
||||
jobKill: '終止',
|
||||
jobKillConfirm: '再次點擊確認終止',
|
||||
jobKillError: '終止失敗',
|
||||
tasksViewGraph: '工作流圖',
|
||||
tasksViewTree: '樹狀圖',
|
||||
tasksViewSwitchToGraph: '切換為工作流圖',
|
||||
tasksViewSwitchToTree: '切換為樹狀圖',
|
||||
settingsViewModeTitle: '任務管理預設檢視',
|
||||
settingsViewModeDesc: '任務管理頁開啟時的預設顯示方式(頁內仍可臨時切換)',
|
||||
settingsViewModeGraph: '工作流圖',
|
||||
settingsViewModeGraphDesc: '分層節點圖,可縮放平移',
|
||||
settingsViewModeTree: '樹狀圖',
|
||||
settingsViewModeTreeDesc: '經典縮排樹,可用鍵盤導航',
|
||||
tasksWorkflowMember: '工作流成員',
|
||||
tasksFoldCompleted: '✓ {count} 已完成',
|
||||
tasksFoldExpand: '展開已完成的節點',
|
||||
tasksFoldCollapse: '收起已完成的節點',
|
||||
tasksZoomIn: '放大',
|
||||
tasksZoomOut: '縮小',
|
||||
tasksZoomFit: '適應畫布',
|
||||
tasksNodeJump: '檢視逐字稿',
|
||||
tasksNodeState: '狀態',
|
||||
tasksNodeMode: '模式',
|
||||
tasksNodeModel: '模型',
|
||||
tasksNodeTeamRole: '團隊角色',
|
||||
tasksNodeActivity: '最近活動',
|
||||
tasksStateRunning: '執行中',
|
||||
tasksStateIdle: '閒置',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出錯',
|
||||
teamBoard: '團隊任務板',
|
||||
teamChip: '團隊 {members} 成員 · {tasks} 任務',
|
||||
teamMembers: '成員',
|
||||
teamTasksEmpty: '暫無共用任務',
|
||||
teamTaskCreate: '新增任務',
|
||||
teamTaskSubject: '標題',
|
||||
teamTaskDescription: '描述',
|
||||
teamTaskOwner: '負責人',
|
||||
teamTaskUnowned: '未認領',
|
||||
teamTaskPending: '待辦',
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '受阻',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重開',
|
||||
teamTaskDelete: '刪除',
|
||||
teamTaskDeleteConfirm: '確認刪除',
|
||||
teamTaskEdit: '編輯',
|
||||
teamTaskSave: '儲存',
|
||||
teamTaskCancel: '取消',
|
||||
teamTaskConflict: '任務已被他人修改,已重新整理',
|
||||
teamTaskError: '操作失敗:{message}',
|
||||
teamTaskDeleted: '已刪除',
|
||||
workflowRunning: '進行中',
|
||||
workflowCompleted: '已完成',
|
||||
workflowCancelled: '已取消',
|
||||
workflowFailed: '失敗',
|
||||
workflowPhaseUnnamed: '未命名階段',
|
||||
workflowMembers: '{done}/{total} 成員',
|
||||
jobsAutoCollapsed: '代理較多,已自動收起',
|
||||
catalogLoadFailed: '{count} 個分支載入失敗',
|
||||
addPluginsTabCard: '新增 Tab 插件',
|
||||
addPluginsTabCardDesc: '註冊新的側邊欄頁面',
|
||||
addPluginsViewerCard: '新增預覽插件',
|
||||
|
||||
@@ -405,6 +405,64 @@ export const zhMO: Record<string, string> = {
|
||||
jobKill: '終止',
|
||||
jobKillConfirm: '再次點擊確認終止',
|
||||
jobKillError: '終止失敗',
|
||||
tasksViewGraph: '工作流圖',
|
||||
tasksViewTree: '樹狀圖',
|
||||
tasksViewSwitchToGraph: '切換為工作流圖',
|
||||
tasksViewSwitchToTree: '切換為樹狀圖',
|
||||
settingsViewModeTitle: '任務管理預設檢視',
|
||||
settingsViewModeDesc: '任務管理頁開啟時的預設顯示方式(頁內仍可臨時切換)',
|
||||
settingsViewModeGraph: '工作流圖',
|
||||
settingsViewModeGraphDesc: '分層節點圖,可縮放平移',
|
||||
settingsViewModeTree: '樹狀圖',
|
||||
settingsViewModeTreeDesc: '經典縮排樹,可用鍵盤導航',
|
||||
tasksWorkflowMember: '工作流成員',
|
||||
tasksFoldCompleted: '✓ {count} 已完成',
|
||||
tasksFoldExpand: '展開已完成的節點',
|
||||
tasksFoldCollapse: '收起已完成的節點',
|
||||
tasksZoomIn: '放大',
|
||||
tasksZoomOut: '縮小',
|
||||
tasksZoomFit: '適應畫布',
|
||||
tasksNodeJump: '檢視逐字稿',
|
||||
tasksNodeState: '狀態',
|
||||
tasksNodeMode: '模式',
|
||||
tasksNodeModel: '模型',
|
||||
tasksNodeTeamRole: '團隊角色',
|
||||
tasksNodeActivity: '最近活動',
|
||||
tasksStateRunning: '執行中',
|
||||
tasksStateIdle: '閒置',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出錯',
|
||||
teamBoard: '團隊任務板',
|
||||
teamChip: '團隊 {members} 成員 · {tasks} 任務',
|
||||
teamMembers: '成員',
|
||||
teamTasksEmpty: '暫無共用任務',
|
||||
teamTaskCreate: '新增任務',
|
||||
teamTaskSubject: '標題',
|
||||
teamTaskDescription: '描述',
|
||||
teamTaskOwner: '負責人',
|
||||
teamTaskUnowned: '未認領',
|
||||
teamTaskPending: '待辦',
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '受阻',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重開',
|
||||
teamTaskDelete: '刪除',
|
||||
teamTaskDeleteConfirm: '確認刪除',
|
||||
teamTaskEdit: '編輯',
|
||||
teamTaskSave: '儲存',
|
||||
teamTaskCancel: '取消',
|
||||
teamTaskConflict: '任務已被他人修改,已重新整理',
|
||||
teamTaskError: '操作失敗:{message}',
|
||||
teamTaskDeleted: '已刪除',
|
||||
workflowRunning: '進行中',
|
||||
workflowCompleted: '已完成',
|
||||
workflowCancelled: '已取消',
|
||||
workflowFailed: '失敗',
|
||||
workflowPhaseUnnamed: '未命名階段',
|
||||
workflowMembers: '{done}/{total} 成員',
|
||||
jobsAutoCollapsed: '代理較多,已自動收起',
|
||||
catalogLoadFailed: '{count} 個分支載入失敗',
|
||||
addPluginsTabCard: '新增 Tab 插件',
|
||||
addPluginsTabCardDesc: '註冊新的側邊欄頁面',
|
||||
addPluginsViewerCard: '新增預覽插件',
|
||||
|
||||
@@ -405,6 +405,64 @@ export const zhTW: Record<string, string> = {
|
||||
jobKill: '終止',
|
||||
jobKillConfirm: '再次點擊確認終止',
|
||||
jobKillError: '終止失敗',
|
||||
tasksViewGraph: '工作流圖',
|
||||
tasksViewTree: '樹狀圖',
|
||||
tasksViewSwitchToGraph: '切換為工作流圖',
|
||||
tasksViewSwitchToTree: '切換為樹狀圖',
|
||||
settingsViewModeTitle: '任務管理預設檢視',
|
||||
settingsViewModeDesc: '任務管理頁開啟時的預設顯示方式(頁內仍可臨時切換)',
|
||||
settingsViewModeGraph: '工作流圖',
|
||||
settingsViewModeGraphDesc: '分層節點圖,可縮放平移',
|
||||
settingsViewModeTree: '樹狀圖',
|
||||
settingsViewModeTreeDesc: '經典縮排樹,可用鍵盤導航',
|
||||
tasksWorkflowMember: '工作流成員',
|
||||
tasksFoldCompleted: '✓ {count} 已完成',
|
||||
tasksFoldExpand: '展開已完成的節點',
|
||||
tasksFoldCollapse: '收起已完成的節點',
|
||||
tasksZoomIn: '放大',
|
||||
tasksZoomOut: '縮小',
|
||||
tasksZoomFit: '適應畫布',
|
||||
tasksNodeJump: '檢視逐字稿',
|
||||
tasksNodeState: '狀態',
|
||||
tasksNodeMode: '模式',
|
||||
tasksNodeModel: '模型',
|
||||
tasksNodeTeamRole: '團隊角色',
|
||||
tasksNodeActivity: '最近活動',
|
||||
tasksStateRunning: '執行中',
|
||||
tasksStateIdle: '閒置',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出錯',
|
||||
teamBoard: '團隊任務板',
|
||||
teamChip: '團隊 {members} 成員 · {tasks} 任務',
|
||||
teamMembers: '成員',
|
||||
teamTasksEmpty: '暫無共用任務',
|
||||
teamTaskCreate: '新增任務',
|
||||
teamTaskSubject: '標題',
|
||||
teamTaskDescription: '描述',
|
||||
teamTaskOwner: '負責人',
|
||||
teamTaskUnowned: '未認領',
|
||||
teamTaskPending: '待辦',
|
||||
teamTaskInProgress: '進行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '受阻',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重開',
|
||||
teamTaskDelete: '刪除',
|
||||
teamTaskDeleteConfirm: '確認刪除',
|
||||
teamTaskEdit: '編輯',
|
||||
teamTaskSave: '儲存',
|
||||
teamTaskCancel: '取消',
|
||||
teamTaskConflict: '任務已被他人修改,已重新整理',
|
||||
teamTaskError: '操作失敗:{message}',
|
||||
teamTaskDeleted: '已刪除',
|
||||
workflowRunning: '進行中',
|
||||
workflowCompleted: '已完成',
|
||||
workflowCancelled: '已取消',
|
||||
workflowFailed: '失敗',
|
||||
workflowPhaseUnnamed: '未命名階段',
|
||||
workflowMembers: '{done}/{total} 成員',
|
||||
jobsAutoCollapsed: '代理較多,已自動收起',
|
||||
catalogLoadFailed: '{count} 個分支載入失敗',
|
||||
addPluginsTabCard: '新增 Tab 插件',
|
||||
addPluginsTabCardDesc: '註冊新的側邊欄頁面',
|
||||
addPluginsViewerCard: '新增預覽插件',
|
||||
|
||||
@@ -402,6 +402,64 @@ export const zh = {
|
||||
jobKill: '终止',
|
||||
jobKillConfirm: '再次点击确认终止',
|
||||
jobKillError: '终止失败',
|
||||
tasksViewGraph: '工作流图',
|
||||
tasksViewTree: '树状图',
|
||||
tasksViewSwitchToGraph: '切换为工作流图',
|
||||
tasksViewSwitchToTree: '切换为树状图',
|
||||
settingsViewModeTitle: '任务管理默认视图',
|
||||
settingsViewModeDesc: '任务管理页打开时的默认展示方式(页内仍可临时切换)',
|
||||
settingsViewModeGraph: '工作流图',
|
||||
settingsViewModeGraphDesc: '分层节点图,可缩放平移',
|
||||
settingsViewModeTree: '树状图',
|
||||
settingsViewModeTreeDesc: '经典缩进树,键盘可导航',
|
||||
tasksWorkflowMember: '工作流成员',
|
||||
tasksFoldCompleted: '✓ {count} 已完成',
|
||||
tasksFoldExpand: '展开已完成的节点',
|
||||
tasksFoldCollapse: '折叠已完成的节点',
|
||||
tasksZoomIn: '放大',
|
||||
tasksZoomOut: '缩小',
|
||||
tasksZoomFit: '适应画布',
|
||||
tasksNodeJump: '查看转录',
|
||||
tasksNodeState: '状态',
|
||||
tasksNodeMode: '模式',
|
||||
tasksNodeModel: '模型',
|
||||
tasksNodeTeamRole: '团队角色',
|
||||
tasksNodeActivity: '最近活动',
|
||||
tasksStateRunning: '运行中',
|
||||
tasksStateIdle: '待命',
|
||||
tasksStateDone: '已完成',
|
||||
tasksStateError: '出错',
|
||||
teamBoard: '团队任务板',
|
||||
teamChip: '团队 {members} 成员 · {tasks} 任务',
|
||||
teamMembers: '成员',
|
||||
teamTasksEmpty: '暂无共享任务',
|
||||
teamTaskCreate: '新建任务',
|
||||
teamTaskSubject: '标题',
|
||||
teamTaskDescription: '描述',
|
||||
teamTaskOwner: '负责人',
|
||||
teamTaskUnowned: '未认领',
|
||||
teamTaskPending: '待办',
|
||||
teamTaskInProgress: '进行中',
|
||||
teamTaskCompleted: '已完成',
|
||||
teamTaskBlocked: '阻塞',
|
||||
teamTaskComplete: '完成',
|
||||
teamTaskReopen: '重开',
|
||||
teamTaskDelete: '删除',
|
||||
teamTaskDeleteConfirm: '确认删除',
|
||||
teamTaskEdit: '编辑',
|
||||
teamTaskSave: '保存',
|
||||
teamTaskCancel: '取消',
|
||||
teamTaskConflict: '任务已被他人修改,已刷新',
|
||||
teamTaskError: '操作失败:{message}',
|
||||
teamTaskDeleted: '已删除',
|
||||
workflowRunning: '进行中',
|
||||
workflowCompleted: '已完成',
|
||||
workflowCancelled: '已取消',
|
||||
workflowFailed: '失败',
|
||||
workflowPhaseUnnamed: '未命名阶段',
|
||||
workflowMembers: '{done}/{total} 成员',
|
||||
jobsAutoCollapsed: '代理较多,已自动折叠',
|
||||
catalogLoadFailed: '{count} 个分支加载失败',
|
||||
addPluginsTabCard: '添加 Tab 插件',
|
||||
addPluginsTabCardDesc: '注册新的侧边栏页面',
|
||||
addPluginsViewerCard: '添加预览插件',
|
||||
@@ -837,6 +895,64 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
jobKill: 'Kill',
|
||||
jobKillConfirm: 'Click again to confirm kill',
|
||||
jobKillError: 'Kill failed',
|
||||
tasksViewGraph: 'Workflow graph',
|
||||
tasksViewTree: 'Tree',
|
||||
tasksViewSwitchToGraph: 'Switch to the workflow graph',
|
||||
tasksViewSwitchToTree: 'Switch to the tree',
|
||||
settingsViewModeTitle: 'Tasks page default view',
|
||||
settingsViewModeDesc: 'How the Tasks page renders on open (the in-page toggle still flips it ad hoc)',
|
||||
settingsViewModeGraph: 'Workflow graph',
|
||||
settingsViewModeGraphDesc: 'Layered node graph with zoom and pan',
|
||||
settingsViewModeTree: 'Tree',
|
||||
settingsViewModeTreeDesc: 'Classic indentation tree, keyboard navigable',
|
||||
tasksWorkflowMember: 'Workflow member',
|
||||
tasksFoldCompleted: '✓ {count} completed',
|
||||
tasksFoldExpand: 'Expand the completed nodes',
|
||||
tasksFoldCollapse: 'Collapse the completed nodes',
|
||||
tasksZoomIn: 'Zoom in',
|
||||
tasksZoomOut: 'Zoom out',
|
||||
tasksZoomFit: 'Fit to canvas',
|
||||
tasksNodeJump: 'Open transcript',
|
||||
tasksNodeState: 'State',
|
||||
tasksNodeMode: 'Mode',
|
||||
tasksNodeModel: 'Model',
|
||||
tasksNodeTeamRole: 'Team role',
|
||||
tasksNodeActivity: 'Latest activity',
|
||||
tasksStateRunning: 'Running',
|
||||
tasksStateIdle: 'Idle',
|
||||
tasksStateDone: 'Done',
|
||||
tasksStateError: 'Error',
|
||||
teamBoard: 'Team task board',
|
||||
teamChip: 'Team {members} members · {tasks} tasks',
|
||||
teamMembers: 'Members',
|
||||
teamTasksEmpty: 'No shared tasks yet',
|
||||
teamTaskCreate: 'New task',
|
||||
teamTaskSubject: 'Subject',
|
||||
teamTaskDescription: 'Description',
|
||||
teamTaskOwner: 'Owner',
|
||||
teamTaskUnowned: 'Unclaimed',
|
||||
teamTaskPending: 'Pending',
|
||||
teamTaskInProgress: 'In progress',
|
||||
teamTaskCompleted: 'Completed',
|
||||
teamTaskBlocked: 'Blocked',
|
||||
teamTaskComplete: 'Complete',
|
||||
teamTaskReopen: 'Reopen',
|
||||
teamTaskDelete: 'Delete',
|
||||
teamTaskDeleteConfirm: 'Confirm delete',
|
||||
teamTaskEdit: 'Edit',
|
||||
teamTaskSave: 'Save',
|
||||
teamTaskCancel: 'Cancel',
|
||||
teamTaskConflict: 'The task changed elsewhere; refreshed',
|
||||
teamTaskError: 'Operation failed: {message}',
|
||||
teamTaskDeleted: 'Deleted',
|
||||
workflowRunning: 'Running',
|
||||
workflowCompleted: 'Completed',
|
||||
workflowCancelled: 'Cancelled',
|
||||
workflowFailed: 'Failed',
|
||||
workflowPhaseUnnamed: 'Unnamed phase',
|
||||
workflowMembers: '{done}/{total} members',
|
||||
jobsAutoCollapsed: 'Auto-collapsed (many agents)',
|
||||
catalogLoadFailed: '{count} branches failed to load',
|
||||
addPluginsTabCard: 'Add tab plugins',
|
||||
addPluginsTabCardDesc: 'Register a new sidebar page',
|
||||
addPluginsViewerCard: 'Add preview plugins',
|
||||
|
||||
@@ -45,6 +45,9 @@ export function parsePrefs(value: unknown): SidebarPrefs {
|
||||
autoOpenJobs: typeof record.autoOpenJobs === 'boolean'
|
||||
? record.autoOpenJobs
|
||||
: SIDEBAR_PREFS_DEFAULTS.autoOpenJobs,
|
||||
tasksViewMode: record.tasksViewMode === 'tree' || record.tasksViewMode === 'graph'
|
||||
? record.tasksViewMode
|
||||
: SIDEBAR_PREFS_DEFAULTS.tasksViewMode,
|
||||
agentOpenTools: typeof record.agentOpenTools === 'boolean'
|
||||
? record.agentOpenTools
|
||||
: SIDEBAR_PREFS_DEFAULTS.agentOpenTools,
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
/**
|
||||
* Styles of the Tasks page's graph canvas, tree rows, anchored popovers, and
|
||||
* the bottom jobs drawer. Skin contract: every color resolves to a
|
||||
* --dsw-alias-* / --dsw-font-* token (guarded by tests/theme.spec.ts); the
|
||||
* hard-projection shadows mix a border token so they follow the theme.
|
||||
*/
|
||||
|
||||
/* ── Graph canvas ──────────────────────────────────────────────────────── */
|
||||
|
||||
.graphView {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.canvasDragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.canvasInner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.edges {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edge {
|
||||
fill: none;
|
||||
stroke: var(--dsw-alias-border-l2);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.edgeTeam,
|
||||
.edgeWorkflow {
|
||||
stroke: var(--dsw-alias-accent);
|
||||
stroke-opacity: 0.65;
|
||||
}
|
||||
|
||||
.edgeFold {
|
||||
stroke-dasharray: 4 3;
|
||||
}
|
||||
|
||||
.phaseFrame {
|
||||
position: absolute;
|
||||
border: 1px dashed var(--dsw-alias-border-l2);
|
||||
border-radius: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.phaseLabel {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 10px;
|
||||
padding: 0 6px;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── Graph nodes ───────────────────────────────────────────────────────── */
|
||||
|
||||
.node {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 7px 9px;
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
border: 1px solid var(--dsw-alias-border-l1);
|
||||
border-radius: 10px;
|
||||
box-shadow: 3px 3px 0 color-mix(in srgb, var(--dsw-alias-border-l1) 55%, transparent);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.nodeRunning {
|
||||
border-color: var(--dsw-alias-accent);
|
||||
}
|
||||
|
||||
.nodeCurrent {
|
||||
box-shadow: 0 0 0 2px var(--dsw-alias-accent), 3px 3px 0 color-mix(in srgb, var(--dsw-alias-border-l1) 55%, transparent);
|
||||
}
|
||||
|
||||
.nodeDone {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.nodeError {
|
||||
border-color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.nodeFold {
|
||||
border-style: dashed;
|
||||
border-color: var(--dsw-alias-border-l2);
|
||||
background: var(--dsw-alias-bg-base);
|
||||
box-shadow: none;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nodeWorkflow {
|
||||
border-color: var(--dsw-alias-accent);
|
||||
}
|
||||
|
||||
.nodeHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nodeDot {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nodeTitle {
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nodeSecondary {
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nodeInfo {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.node:hover .nodeInfo,
|
||||
.nodeInfo:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nodeInfo:hover {
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* ── Live activity lines (icon + tool + args) ──────────────────────────── */
|
||||
|
||||
.liveLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.liveGlyph {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
color: var(--dsw-alias-accent);
|
||||
}
|
||||
|
||||
.liveTool {
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.liveArgs {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.liveText {
|
||||
display: block;
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── Control cluster (bottom-right; visible in BOTH modes) ─────────────── */
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
z-index: 6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding: 3px;
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
border: 1px solid var(--dsw-alias-border-l1);
|
||||
border-radius: 10px;
|
||||
box-shadow: 2px 2px 0 color-mix(in srgb, var(--dsw-alias-border-l1) 45%, transparent);
|
||||
}
|
||||
|
||||
.controlBtn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.controlBtn:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.controlBtnActive {
|
||||
color: var(--dsw-alias-accent);
|
||||
}
|
||||
|
||||
.controlPct {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* ── Tree mode ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.treeView {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 4px 6px 12px;
|
||||
}
|
||||
|
||||
.treeRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 5px 8px;
|
||||
margin-top: 2px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.treeRow:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.treeRow:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.treeRowActive {
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
}
|
||||
|
||||
.treeRowDone {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.treeContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.treeFold {
|
||||
border: 1px dashed var(--dsw-alias-border-l2);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
}
|
||||
|
||||
.treeFold:hover {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.treeInfo {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.treeRow:hover .treeInfo,
|
||||
.treeInfo:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.treeInfo:hover {
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* ── Anchored popover card ─────────────────────────────────────────────── */
|
||||
|
||||
.popCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
background: var(--dsw-alias-bg-layer-2);
|
||||
border: 1px solid var(--dsw-alias-border-l1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 4px 4px 0 color-mix(in srgb, var(--dsw-alias-border-l1) 55%, transparent);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.popTitle {
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.popRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.popKey {
|
||||
flex-shrink: 0;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.popValue {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.popSection {
|
||||
margin-top: 2px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--dsw-alias-border-l1);
|
||||
}
|
||||
|
||||
.popPrimary {
|
||||
align-self: stretch;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--dsw-alias-button-primary-fill);
|
||||
color: var(--dsw-alias-accent-ink);
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popPrimary:hover {
|
||||
background: var(--dsw-alias-button-primary-hover);
|
||||
}
|
||||
|
||||
.popGhost {
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popGhost:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.popGhostDanger {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
border-color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.popActions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.popError {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.popNote {
|
||||
color: var(--dsw-alias-state-warn-label);
|
||||
}
|
||||
|
||||
.popList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.popListRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popListRow:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.popInput,
|
||||
.popSelect {
|
||||
width: 100%;
|
||||
padding: 4px 7px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 7px;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.popInput:focus,
|
||||
.popSelect:focus {
|
||||
outline: none;
|
||||
border-color: var(--dsw-alias-accent);
|
||||
}
|
||||
|
||||
.popPre {
|
||||
margin: 0;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
padding: 6px 8px;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
border: 1px solid var(--dsw-alias-border-l1);
|
||||
border-radius: 8px;
|
||||
font-family: var(--dsw-font-s-14);
|
||||
font-size: var(--dsw-font-xxxs-11-font-size);
|
||||
line-height: var(--dsw-font-xxxs-11-line-height);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.popHint {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* ── Team chip in the header ───────────────────────────────────────────── */
|
||||
|
||||
.teamChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 999px;
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.teamChip:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* ── Jobs drawer ───────────────────────────────────────────────────────── */
|
||||
|
||||
.jobsDrawer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
border-top: 1px solid var(--dsw-alias-border-l1);
|
||||
box-shadow: 0 -3px 0 color-mix(in srgb, var(--dsw-alias-border-l1) 35%, transparent);
|
||||
}
|
||||
|
||||
.jobsDrawerBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xxxs-strong-11);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.jobsDrawerBar:hover {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.jobsDrawerCount {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
}
|
||||
|
||||
.jobsDrawerAuto {
|
||||
margin-left: auto;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
}
|
||||
|
||||
.jobsDrawerChevron {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.jobsDrawerChevronOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.jobsDrawerBody {
|
||||
max-height: 42%;
|
||||
overflow-y: auto;
|
||||
padding: 0 6px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.jobsRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.jobsRowMain {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 5px 6px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.jobsRowMain:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.jobsRowSettled {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.jobsContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.jobsLabelLine {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jobsKind {
|
||||
flex-shrink: 0;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.jobsLabel {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.jobsSecondary {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jobsKill {
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jobsKill:hover {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
border-color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.jobsKillArmed {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
border-color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.jobsKillError {
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Shared presentation bits of the Tasks page's two modes (graph + tree):
|
||||
* the state-dot mapping, the secondary card line, the iconified live
|
||||
* activity row ("glyph + tool + args" — the user-approved format replacing
|
||||
* the old `>read[file]` text), and the fold aggregate caption. Kept in one
|
||||
* module so graph and tree can never drift apart visually.
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import { StateDot, type StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { LastActivity } from '../subagent-activity.ts'
|
||||
import type { TasksAgentNode, TasksNodeState } from './tasks-model.ts'
|
||||
import { toolGlyph } from './tool-icons.tsx'
|
||||
import { t } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** Preview cap of one tool-call argument line. */
|
||||
const ARGS_PREVIEW = 60
|
||||
|
||||
/** The host StateDot semantic of a node display state. */
|
||||
export function nodeDotState(state: TasksNodeState): StateDotState {
|
||||
switch (state) {
|
||||
case 'running': return 'ongoing'
|
||||
case 'idle': return 'idle'
|
||||
case 'done': return 'done'
|
||||
case 'error': return 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** First `limit` characters with an ellipsis when truncated. */
|
||||
export function preview(text: string, limit: number = ARGS_PREVIEW): string {
|
||||
return text.length > limit ? `${text.slice(0, limit)}…` : text
|
||||
}
|
||||
|
||||
/** Collapse whitespace for single-line previews. */
|
||||
export function flatten(text: string): string {
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* The secondary line of an agent node: display title (when it differs from
|
||||
* the durable label) · mode · activity · teammate model. Empty parts drop
|
||||
* out. Synthesized workflow members show their origin instead of a mode.
|
||||
*/
|
||||
export function agentSecondary(node: TasksAgentNode): string {
|
||||
const parts: string[] = []
|
||||
if (node.title !== undefined && node.title !== node.label) parts.push(node.title)
|
||||
if (node.mode !== undefined) {
|
||||
parts.push(node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable'))
|
||||
} else if (node.synthesized === true) {
|
||||
parts.push(t('tasksWorkflowMember'))
|
||||
}
|
||||
parts.push(node.activity === 'running' ? t('subagentRunning') : t('subagentInactive'))
|
||||
if (node.team?.model !== undefined) parts.push(node.team.model)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
/**
|
||||
* The live activity row of a RUNNING agent node: the tool line renders as
|
||||
* "glyph + tool + args" (the approved live-line format), plus the flattened
|
||||
* last text line underneath. A running node with neither reads "thinking…".
|
||||
*/
|
||||
export function LiveLine(props: { live: LastActivity | undefined }): ReactNode {
|
||||
const { live } = props
|
||||
if (live?.text === undefined && live?.tool === undefined) {
|
||||
return <span className={css.liveLine}>{t('subagentThinking')}</span>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{live.tool !== undefined && (
|
||||
<span className={css.liveLine}>
|
||||
<span className={css.liveGlyph} aria-hidden="true">{toolGlyph(live.tool.name)(10)}</span>
|
||||
<span className={css.liveTool}>{live.tool.name}</span>
|
||||
{live.tool.args !== '' && (
|
||||
<span className={css.liveArgs}>{preview(live.tool.args)}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{live.text !== undefined && (
|
||||
<span className={css.liveText}>{flatten(live.text)}</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** The fold aggregate's subtitle: up to two label previews joined by ·. */
|
||||
export function foldPreviews(previews: readonly string[]): string {
|
||||
return previews.join(' · ')
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Tool-call glyphs for the live activity lines of the Tasks page (the
|
||||
* "icon + tool + args" row on running agent nodes). A small explicit map of
|
||||
* the common tool families onto host primitive icons; anything unmapped
|
||||
* gets the generic ellipsis glyph. All icons are the host's own components
|
||||
* (currentColor — zero color literals, the theme contract stays intact).
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
IconCodeOutline16,
|
||||
IconEditOutline16,
|
||||
IconEllipsisOutline16,
|
||||
IconGlobeOutline14,
|
||||
IconPlayOutline16,
|
||||
IconSearchOutline16,
|
||||
IconThinkOutline16,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
|
||||
/** One glyph renderer (host icon components accept a pixel size). */
|
||||
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} />],
|
||||
]
|
||||
|
||||
/** The fallback glyph for unmapped tools. */
|
||||
const FALLBACK_GLYPH: Glyph = size => <IconEllipsisOutline16 size={size} />
|
||||
|
||||
/**
|
||||
* The glyph of one tool name: exact match first, then a prefix match so
|
||||
* `mcp__fs__read_file`-style names still land on their family glyph. The
|
||||
* lookup is case-insensitive.
|
||||
*/
|
||||
export function toolGlyph(toolName: string): Glyph {
|
||||
const name = toolName.toLowerCase()
|
||||
for (const [key, glyph] of TOOL_GLYPHS) {
|
||||
if (name === key) return glyph
|
||||
}
|
||||
for (const [key, glyph] of TOOL_GLYPHS) {
|
||||
if (name.includes(key)) return glyph
|
||||
}
|
||||
return FALLBACK_GLYPH
|
||||
}
|
||||
@@ -80,6 +80,7 @@ export function resolveSidebarConfig(config: SidebarConfig | undefined): Resolve
|
||||
export const PrefsSchema: z<SidebarPrefs> = z.object({
|
||||
autoOpenSubagent: z.boolean().default(true),
|
||||
autoOpenJobs: z.boolean().default(true),
|
||||
tasksViewMode: z.union([z.const('graph'), z.const('tree')]).default('graph'),
|
||||
agentOpenTools: z.boolean().default(false),
|
||||
editorExplorer: z.boolean().default(false),
|
||||
workspaceFence: z.boolean().default(true),
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface SidebarPrefs {
|
||||
* conversation (any new job id, not just the first one).
|
||||
*/
|
||||
autoOpenJobs: boolean
|
||||
/**
|
||||
* The Tasks page's default presentation: the workflow graph canvas or the
|
||||
* classic indentation tree (the in-page toggle still flips it ad hoc).
|
||||
*/
|
||||
tasksViewMode: 'graph' | 'tree'
|
||||
/**
|
||||
* Whether the model-facing `sidebar_open` tool is injected into the
|
||||
* model's toolset — one tool that lets the model actively open a local
|
||||
@@ -151,6 +156,7 @@ export type TitleBarScheme = typeof TITLE_BAR_SCHEMES[number]
|
||||
export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
||||
autoOpenSubagent: true,
|
||||
autoOpenJobs: true,
|
||||
tasksViewMode: 'graph',
|
||||
agentOpenTools: false,
|
||||
editorExplorer: false,
|
||||
workspaceFence: true,
|
||||
|
||||
Reference in New Issue
Block a user