把上一轮验证过的数据层搬到工作图基座上,图/树两种模式共用:
- 新增 src/process-activity.ts:宿主 ui-chat 的 process group 算法移植
(activity() 分类表逐条照抄、liveToolDetail 键优先级与 160 字截断、
counts 计数排序与并列规则、preparing 语义、按 user/reply 边界切范围)。
按本轮设计裁剪掉逐阶段列表(阶段改由徽标 + 颜色表达)。新增一条
偏差:最新范围为空(子代理刚回复完)时回退到上一个「做过事」的范围,
否则已结算卡片的小条只会显示「已完成分析」。
- chatT:从宿主 chat 命名空间读 message.stepProcess.* 措辞(方法调用 +
try/catch 兜底,key 未命中即回退),4 个回归用例钉住「不许解绑 bind」。
- subagents.live 载荷换新:SidebarChildLiveView { running?, text?, summary?,
lastEventTime? },报告树内每个子节点(含已结算)与拓扑根;删除
expand/stages 机制;childActivity → childLive(「不在 map 里 = 未运行」
的读法随旧协议一起作废)。
- tasks-model 消费新形状;LiveLine 与节点详情浮窗改显示合并活动措辞。
- 删除 src/subagent-activity.ts 与 tests/subagent-activity.spec.ts。
验证:typecheck / lint 通过;pnpm test 1252 passed / 9 skipped。
(cherry picked from commit 5e5284f214846b0c366048acbee459f3a7a79456)
116 lines
5.1 KiB
TypeScript
116 lines
5.1 KiB
TypeScript
/**
|
||
* The two labels the Tasks page borrows from the main conversation's merged
|
||
* activity display: the LIVE line ("正在运行命令 · npm run build") and the
|
||
* DONE title for a settled range ("已读取文件、执行了命令").
|
||
*
|
||
* Both are ports of the host's own composition
|
||
* (`dsh-client-ui-chat`'s `ProcessGroupHeader` + `step-process.js`
|
||
* `processTitle`), reading the host's `chat` dictionary through
|
||
* {@link chatT} so the wording — and every language the host ships — matches
|
||
* what the main chat shows for the same work.
|
||
*
|
||
* The translator is injectable for tests; the default is the host bridge. When
|
||
* a key does not resolve (a host that renamed it, or a deployment where the
|
||
* chat target never mounted) the call answers undefined and the card falls
|
||
* back to the tool detail it already has, never to a raw `message.…` key.
|
||
*/
|
||
import type { ProcessActivitySummary } from '../process-activity.ts'
|
||
import { chatT } from './locales.ts'
|
||
|
||
/** A host-namespace translator; undefined means "this key is not available". */
|
||
export type ChatTranslate = (key: string, params?: Record<string, string | number>) => string | undefined
|
||
|
||
/** The host's `' · '` line separator (`message.turnProcess.separator`). */
|
||
function separator(t: ChatTranslate): string {
|
||
return t('message.turnProcess.separator') ?? ' · '
|
||
}
|
||
|
||
/**
|
||
* The live line of one stage: the running category's `…ing` wording plus the
|
||
* running call's detail, or the "thinking" default while nothing is dispatched
|
||
* (exactly the host's `ProcessGroupHeader` label rule).
|
||
*
|
||
* @param summary - the newest stage's summary (absent while the child has none).
|
||
* @param t - host translator (defaults to the `chat` namespace bridge).
|
||
* @returns the line, or undefined when there is nothing to say.
|
||
*/
|
||
export function liveActivityLabel(
|
||
summary: ProcessActivitySummary | undefined,
|
||
t: ChatTranslate = chatT,
|
||
): string | undefined {
|
||
const activity = summary?.running ?? 'thinking'
|
||
const preparing = summary?.preparing === true
|
||
// The host maps its `thinking` fallback onto `prepare.tools` (there is no
|
||
// `prepare.thinking` key).
|
||
const label = preparing
|
||
? t(`message.stepProcess.prepare.${activity === 'thinking' ? 'tools' : activity}`)
|
||
: t(`message.stepProcess.${activity}`)
|
||
if (label === undefined) {
|
||
// No host wording for this category: the detail is the honest fallback.
|
||
return summary?.runningDetail === undefined || summary.runningDetail === ''
|
||
? undefined
|
||
: summary.runningDetail
|
||
}
|
||
const detail = preparing ? '' : summary?.runningDetail ?? ''
|
||
return detail === '' ? label : `${label}${separator(t)}${detail}`
|
||
}
|
||
|
||
/**
|
||
* The last-resort activity text: the ranked categories spelled out as
|
||
* `kind ×count`. It keeps a card informative when the host's `chat` wording is
|
||
* unavailable (renamed keys, or a deployment where the chat target never
|
||
* mounted) without inventing a second vocabulary for the normal case — the
|
||
* category names are the host's own, and they are language-neutral.
|
||
*
|
||
* @param summary - a stage's summary.
|
||
* @returns the counts text, or undefined when the stage called nothing.
|
||
*/
|
||
export function activityCountsText(
|
||
summary: ProcessActivitySummary | undefined,
|
||
): string | undefined {
|
||
const counts = summary?.counts ?? []
|
||
if (counts.length === 0) return undefined
|
||
return counts.map(entry => `${entry.kind} ×${entry.count}`).join(' · ')
|
||
}
|
||
|
||
/**
|
||
* The settled range's title: the top three categories' `done.*` wording joined
|
||
* the way the host joins it (two labels share the "已" prefix; more than three
|
||
* append the "等" suffix). Counts are never printed.
|
||
*
|
||
* @param summary - a stage's summary.
|
||
* @param t - host translator (defaults to the `chat` namespace bridge).
|
||
* @returns the title, or undefined when the host wording is unavailable.
|
||
*/
|
||
export function doneActivityTitle(
|
||
summary: ProcessActivitySummary | undefined,
|
||
t: ChatTranslate = chatT,
|
||
): string | undefined {
|
||
const counts = summary?.counts ?? []
|
||
if (counts.length === 0) return t('message.stepProcess.done.thinking')
|
||
const labels: string[] = []
|
||
for (const { kind } of counts.slice(0, 3)) {
|
||
const label = t(`message.stepProcess.done.${kind}`)
|
||
if (label === undefined) return undefined
|
||
labels.push(label)
|
||
}
|
||
const first = labels[0]
|
||
if (first === undefined) return undefined
|
||
const second = labels[1]
|
||
if (second === undefined) return first
|
||
// The host lowercases every label after the first for latin languages; a
|
||
// Chinese label is unaffected by that.
|
||
const continuation = (label: string): string => label.charAt(0).toLowerCase() + label.slice(1)
|
||
if (labels.length === 2) {
|
||
const prefix = t('message.stepProcess.sharedPrefix') ?? ''
|
||
const stripped = prefix !== '' && first.startsWith(prefix) && second.startsWith(prefix)
|
||
? second.slice(prefix.length)
|
||
: second
|
||
return t('message.stepProcess.joinTwo', { first, second: continuation(stripped) })
|
||
?? `${first}${separator(t)}${continuation(stripped)}`
|
||
}
|
||
const joined = [first, ...labels.slice(1).map(continuation)].join(t('message.stepProcess.comma') ?? ', ')
|
||
if (counts.length > 3) return t('message.stepProcess.more', { title: joined }) ?? joined
|
||
return joined
|
||
}
|