fix(ui-chat): derive trigger titles from durable sources
This commit is contained in:
1 parent
6ad7c991bf
commit
daa2387546
13 files changed
+28
-73
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-14-step-process-disclosures.md
|
||||
2026-09-14-step-process-disclosures.md: 2c6b49b96612b7cf6be7d6bff5409b80d1776175
|
||||
2026-09-14-step-process-disclosures.zh.md: 8e008e1a6b6eee251cbfbd07e3dca4ee94fcaba7
|
||||
2026-09-14-step-process-disclosures.md: f13cc17fe9ab95c6b20d05e385280751b41c8d1e
|
||||
2026-09-14-step-process-disclosures.zh.md: d47fc4410bdc023fe5c6a7e661df54fc168524a5
|
||||
@@ -34,7 +34,7 @@ The Chat snapshot builder owns group layout, summaries, and footer eligibility.
|
||||
|
||||
The grouping changes presentation only and adds no Session events or model input. Tool names determine categories; unmatched custom tools use the generic category. Disclosure state is local to the mounted chat view. Closing the whole Turn resets nested process groups and remounts hidden process renderers so tool and detail disclosures start collapsed when reopened. Pagination that changes a group's first member may reset its expansion. Existing per-tool disclosures remain available inside the capped process area.
|
||||
|
||||
Non-human Turn triggers use durable Inbox claims rather than every context injection. Next-turn claims and first-step next-step claims without a competing next-turn claim or human batch input receive independent notice seats above the process control. Policy injections do not qualify. Recorded source plus recognized producer framing supplies the title; missing names use recorded identifiers and unknown status formats stay neutral. User-attributed programmatic requests cannot be distinguished reliably and keep their existing presentation. Notice expansion survives whole-Turn folding.
|
||||
Non-human Turn triggers use durable Inbox claims rather than every context injection. Next-turn claims and first-step next-step claims without a competing next-turn claim or human batch input receive independent notice seats above the process control. Policy injections do not qualify. Durable source-family fields supply the title and icon; presentation never parses producer-authored text for an outcome, and the expanded original content retains that detail. Unknown sources stay neutral. User-attributed programmatic requests cannot be distinguished reliably and keep their existing presentation. Notice expansion survives whole-Turn folding.
|
||||
|
||||
Turn actions are anchored after the recorded Turn end so later reasoning, Tools, and steering stay above the footer. The latest Turn keeps actions visible only when its final content is a response; other endings and historical Turns reveal actions on hover or keyboard focus. The feedback target remains the last durable text response.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Chat 快照构建器负责分组布局、摘要和页脚展示条件。仅内容
|
||||
|
||||
分组仅改变展示,不增加 Session 事件或模型输入。分类由工具名决定,未匹配的自定义工具计入通用类别。展开状态保存在当前挂载的聊天视图中。收起整轮会重置内部过程组,并重新挂载已隐藏的过程 renderer,让工具和详情在重新打开时保持折叠。分页改变分组首个成员时可能重置展开状态。各工具原有的独立折叠仍可在限高过程区内使用。
|
||||
|
||||
非人工轮次触发原因以持久化 Inbox 领取记录为依据。next-turn 领取,以及没有竞争的 next-turn 领取、同批没有人工输入的首步 next-step 领取,在过程控件上方拥有独立提示 seat。策略注入不符合条件。标题使用日志来源及已识别的生产方文本格式;名称缺失时使用记录的标识,未知状态格式保持中性。归因为 user 的程序请求无法可靠区分,保留原有展示。提示展开状态不受整轮折叠影响。
|
||||
非人工轮次触发原因以持久化 Inbox 领取记录为依据。next-turn 领取,以及没有竞争的 next-turn 领取、同批没有人工输入的首步 next-step 领取,在过程控件上方拥有独立提示 seat。策略注入不符合条件。标题和图标使用持久化的来源类别字段;展示层不从生产方正文解析结果,展开后的原始内容保留该信息。未知来源保持中性。归因为 user 的程序请求无法可靠区分,保留原有展示。提示展开状态不受整轮折叠影响。
|
||||
|
||||
轮次操作栏锚定在记录的轮次结束之后,使后续推理、工具和 steering 均位于页脚上方。最后一轮仅在最终内容为回复时常显操作;其他结尾和历史轮次通过悬停或键盘聚焦显示。反馈对象仍为最后一条持久化文字回复。
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
- button "Bad response"
|
||||
- button "Branch into a new conversation"
|
||||
- text: 9/1 {{clock}}
|
||||
- button "Plugin started 9/1 {{clock}}":
|
||||
- text: Plugin started
|
||||
- button "Plugin status updated 9/1 {{clock}}":
|
||||
- text: Plugin status updated
|
||||
- time: 9/1 {{clock}}
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
- button "Background task completed HH:mm":
|
||||
- text: Background task completed
|
||||
- button "Background task updated HH:mm":
|
||||
- text: Background task updated
|
||||
- time: HH:mm
|
||||
@@ -1,5 +1,5 @@
|
||||
- button "Background task completed HH:mm" [expanded]:
|
||||
- text: Background task completed
|
||||
- button "Background task updated HH:mm" [expanded]:
|
||||
- text: Background task updated
|
||||
- time: HH:mm
|
||||
- paragraph: This notification triggered this response.
|
||||
- text: "background job bash-39 (bash: PR merge check) finished [status: completed, exit code 0]. Read its output with job_output."
|
||||
@@ -254,7 +254,7 @@ it('keeps a waking notice above and independent of the turn disclosure', async (
|
||||
const outer = page.locator('[data-turn-process="1"]')
|
||||
await notice.waitFor()
|
||||
expect(await button.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await button.textContent()).toContain('Background task completed')
|
||||
expect(await button.textContent()).toContain('Background task updated')
|
||||
const noticeBox = await notice.boundingBox()
|
||||
const outerBox = await outer.boundingBox()
|
||||
expect(noticeBox!.y + noticeBox!.height).toBeLessThan(outerBox!.y)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md
|
||||
README.md: 7f28c9febc5e4296bd6938a0be143dac47eae5c6
|
||||
README.zh.md: 0fbd5c2c84953bc8105259ab74adabdf96d4fd6e
|
||||
README.md: e892d95b8a5e53bc71304d012ee81eb4b606ef1d
|
||||
README.zh.md: 6704a6e54738633f27062a0f7ec65c83bdb81d05
|
||||
@@ -120,4 +120,4 @@ None.
|
||||
|
||||
**Runtime invariant:** No companion is published. Conversation and Slot registration enforce Chat target consistency.
|
||||
|
||||
Non-human input claimed from the next-turn Inbox appears as an independent trigger notice above the Turn control. A first-step next-step claim also qualifies when that Turn has no next-turn claim or human input in its next-step batch. Ordinary pre-step injections are hidden in every Work details mode; their durable records remain available in trajectory inspection. The compact outlined header shows a source-family icon, title, and subdued time without repeating a subject, and the whole notice highlights on hover. The notice stays visible in every work-details mode, defaults to collapsed, and expands independently of process folding to show a short explanation and the original model-facing content without source metadata. Titles recognize schedules, jobs, goals, agent/team messages, webhooks, and Cordis runner outcomes; task messages use the paper-plane icon, each other recognized source family uses its matching primitive icon, and unknown sources or status formats retain the context-injection fallback. User-attributed SDK and delegated inputs remain user messages because the log does not distinguish their caller.
|
||||
Non-human input claimed from the next-turn Inbox appears as an independent trigger notice above the Turn control. A first-step next-step claim also qualifies when that Turn has no next-turn claim or human input in its next-step batch. Ordinary pre-step injections are hidden in every Work details mode; their durable records remain available in trajectory inspection. The compact outlined header shows a source-family icon, title, and subdued time without repeating a subject, and the whole notice highlights on hover. The notice stays visible in every work-details mode, defaults to collapsed, and expands independently of process folding to show a short explanation and the original model-facing content without source metadata. Titles and icons use durable source-family fields for schedules, jobs, goals, agent/team messages, webhooks, and Cordis runner notices; the UI never infers outcomes from producer-authored text, so the expanded original content remains the outcome authority. Task messages use the paper-plane icon, other recognized source families use their matching primitive icons, and unknown sources retain the context-injection fallback. User-attributed SDK and delegated inputs remain user messages because the log does not distinguish their caller.
|
||||
@@ -120,4 +120,4 @@ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点。没有
|
||||
|
||||
**运行时不变式:** 不发布伴生入口。Conversation 与 slot 注册已经强制 Chat target 一致。
|
||||
|
||||
从 next-turn Inbox 领取的非人工输入在轮次控件上方显示独立触发提示。第一步领取的 next-step 输入在本轮没有 next-turn 领取、且同批没有人工输入时也显示提示。所有工作过程展示模式均隐藏普通 pre-step 注入,其持久化记录仍可在轨迹中查看。紧凑的浅描边标题行只显示来源类别图标、标题与弱化时间,不重复摘要;悬停时整条提示高亮。提示在所有工作过程展示模式下均可见,默认收起,并独立于过程折叠显示简短解释及模型实际收到的原始内容,不显示来源元数据。标题识别定时任务、后台任务、目标、任务或团队消息、Webhook 与 Cordis runner 结果;任务消息使用纸飞机图标,其他已识别来源使用对应的 primitive 图标,未知来源或状态格式使用上下文注入图标作为回退。归因为 user 的 SDK 或委派输入仍作为用户消息展示,因为日志无法区分其调用方。
|
||||
从 next-turn Inbox 领取的非人工输入在轮次控件上方显示独立触发提示。第一步领取的 next-step 输入在本轮没有 next-turn 领取、且同批没有人工输入时也显示提示。所有工作过程展示模式均隐藏普通 pre-step 注入,其持久化记录仍可在轨迹中查看。紧凑的浅描边标题行只显示来源类别图标、标题与弱化时间,不重复摘要;悬停时整条提示高亮。提示在所有工作过程展示模式下均可见,默认收起,并独立于过程折叠显示简短解释及模型实际收到的原始内容,不显示来源元数据。定时任务、后台任务、目标、任务或团队消息、Webhook 与 Cordis runner 通知的标题和图标仅使用持久化的来源类别字段;UI 不从生产方正文推断结果,展开后的原始内容仍是结果信息的依据。任务消息使用纸飞机图标,其他已识别来源使用对应的 primitive 图标,未知来源使用上下文注入图标作为回退。归因为 user 的 SDK 或委派输入仍作为用户消息展示,因为日志无法区分其调用方。
|
||||
@@ -25,7 +25,6 @@ export function turnTriggerDetails(node: ContextMessageNode): {
|
||||
const source = record(node.source)
|
||||
const kind = field(source, 'kind')
|
||||
const plugin = field(source, 'plugin')
|
||||
const text = node.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
|
||||
let title: ChatKey = 'message.trigger.request'
|
||||
let icon: TurnTriggerIcon = 'request'
|
||||
switch (kind) {
|
||||
@@ -45,12 +44,6 @@ export function turnTriggerDetails(node: ContextMessageNode): {
|
||||
case 'subagent-settled': {
|
||||
title = 'message.trigger.subagent'
|
||||
icon = 'subagent'
|
||||
const id = field(source, 'senderSessionId')
|
||||
const prefix = `Background subagent ${id} `
|
||||
const ending = text.startsWith(prefix) ? text.slice(prefix.length).split('\n')[0] : ''
|
||||
if (ending === 'finished and will do no further work unless you send it more.') title = 'message.trigger.subagentDone'
|
||||
if (ending === 'was stopped before it finished.') title = 'message.trigger.subagentStopped'
|
||||
if (ending === 'failed before it finished.') title = 'message.trigger.subagentFailed'
|
||||
break
|
||||
}
|
||||
case 'webhook': {
|
||||
@@ -66,28 +59,9 @@ export function turnTriggerDetails(node: ContextMessageNode): {
|
||||
} else if (plugin === 'tool-jobs') {
|
||||
title = 'message.trigger.job'
|
||||
icon = 'job'
|
||||
const job = /^background job (\S+) \(([^:]+): (.*?)\) finished \[status: ([\w-]+)(?:, ([^\]\n]*))?\]/.exec(text)
|
||||
const status = job?.[4]
|
||||
if (status === 'completed') title = 'message.trigger.jobDone'
|
||||
if (status === 'failed') title = 'message.trigger.jobFailed'
|
||||
if (status === 'stopped' || status === 'aborted' || status === 'killed') title = 'message.trigger.jobStopped'
|
||||
} else if (plugin === 'cordis-host-runner') {
|
||||
title = 'message.trigger.plugin'
|
||||
icon = 'plugin'
|
||||
const success = /^Cordis (run|update) (.+?) completed successfully\./.exec(text)
|
||||
const rejected = /^The user rejected Cordis (run|update) (.+?)\./.exec(text)
|
||||
const failed = /^Cordis (run|update) (.+?) failed after cordis_run/.exec(text)
|
||||
const runtime = /^Cordis (?:Client UI|Host handler) (.+?) failed /.exec(text)
|
||||
?? /^Cordis (?:Host|Client) guard rejected runtime code in (.+?)\.\n/.exec(text + '\n')
|
||||
if (success !== null) {
|
||||
title = success[1] === 'update' ? 'message.trigger.pluginUpdated' : 'message.trigger.pluginDone'
|
||||
} else if (rejected !== null) {
|
||||
title = 'message.trigger.pluginRejected'
|
||||
} else if (failed !== null) {
|
||||
title = 'message.trigger.pluginFailed'
|
||||
} else if (runtime !== null) {
|
||||
title = 'message.trigger.pluginError'
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
|
||||
@@ -83,22 +83,11 @@ export const zh = {
|
||||
'message.trigger.agent': '收到任务消息',
|
||||
'message.trigger.team': '收到团队消息',
|
||||
'message.trigger.subagent': '子任务状态更新',
|
||||
'message.trigger.subagentDone': '子任务完成',
|
||||
'message.trigger.subagentStopped': '子任务已停止',
|
||||
'message.trigger.subagentFailed': '子任务失败',
|
||||
'message.trigger.github': '收到 GitHub 事件',
|
||||
'message.trigger.webhook': '收到外部事件',
|
||||
'message.trigger.schedule': '定时任务',
|
||||
'message.trigger.job': '后台任务状态更新',
|
||||
'message.trigger.jobDone': '后台任务完成',
|
||||
'message.trigger.jobFailed': '后台任务失败',
|
||||
'message.trigger.jobStopped': '后台任务已停止',
|
||||
'message.trigger.plugin': '插件状态更新',
|
||||
'message.trigger.pluginDone': '插件已启动',
|
||||
'message.trigger.pluginUpdated': '插件已更新',
|
||||
'message.trigger.pluginRejected': '插件启动被拒绝',
|
||||
'message.trigger.pluginFailed': '插件启动或更新失败',
|
||||
'message.trigger.pluginError': '插件运行异常',
|
||||
'message.trigger.explanation': '这条通知触发了本轮回复。',
|
||||
'message.turnProcess.worked': '已完成工作',
|
||||
'message.turnProcess.deepDivingFor': '深度求索中,用时{duration}',
|
||||
@@ -227,22 +216,11 @@ export const en = {
|
||||
'message.trigger.agent': 'Task message received',
|
||||
'message.trigger.team': 'Team message received',
|
||||
'message.trigger.subagent': 'Subtask status updated',
|
||||
'message.trigger.subagentDone': 'Subtask completed',
|
||||
'message.trigger.subagentStopped': 'Subtask stopped',
|
||||
'message.trigger.subagentFailed': 'Subtask failed',
|
||||
'message.trigger.github': 'GitHub event received',
|
||||
'message.trigger.webhook': 'External event received',
|
||||
'message.trigger.schedule': 'Scheduled task',
|
||||
'message.trigger.job': 'Background task updated',
|
||||
'message.trigger.jobDone': 'Background task completed',
|
||||
'message.trigger.jobFailed': 'Background task failed',
|
||||
'message.trigger.jobStopped': 'Background task stopped',
|
||||
'message.trigger.plugin': 'Plugin status updated',
|
||||
'message.trigger.pluginDone': 'Plugin started',
|
||||
'message.trigger.pluginUpdated': 'Plugin updated',
|
||||
'message.trigger.pluginRejected': 'Plugin activation rejected',
|
||||
'message.trigger.pluginFailed': 'Plugin activation failed',
|
||||
'message.trigger.pluginError': 'Plugin runtime error',
|
||||
'message.trigger.explanation': 'This notification triggered this response.',
|
||||
'message.turnProcess.worked': 'Worked',
|
||||
'message.turnProcess.deepDivingFor': 'Deep diving for {duration}',
|
||||
|
||||
@@ -8,12 +8,10 @@ function details(source: unknown, text: string) {
|
||||
content: [{ type: 'text', text }], producer: contextProducer(source), form: contextForm(source) })
|
||||
}
|
||||
|
||||
it.each([
|
||||
['completed', 'jobDone'], ['failed', 'jobFailed'], ['killed', 'jobStopped'], ['future', 'job'],
|
||||
])('uses recorded job status %s', (status, key) => {
|
||||
it.each(['completed', 'failed', 'killed', 'future'])('uses the durable job source for status %s', (status) => {
|
||||
expect(details({ kind: 'plugin', plugin: 'tool-jobs' },
|
||||
`background job bash-39 (bash: PR merge check) finished [status: ${status}, exit code 0].`))
|
||||
.toEqual({ title: `message.trigger.${key}`, icon: 'job' })
|
||||
.toEqual({ title: 'message.trigger.job', icon: 'job' })
|
||||
})
|
||||
|
||||
it('classifies schedule and goal triggers', () => {
|
||||
@@ -24,12 +22,17 @@ it('classifies schedule and goal triggers', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Cordis run plug/pkg (r1) completed successfully.', 'pluginDone'],
|
||||
['The user rejected Cordis run plug/pkg (r1).', 'pluginRejected'],
|
||||
['Cordis update plug/pkg (r1) failed after cordis_run returned starting: broken', 'pluginFailed'],
|
||||
['Cordis Client guard rejected runtime code in plug/pkg (r1).', 'pluginError'],
|
||||
])('recognizes runner outcome %s', (text, key) => {
|
||||
expect(details({ kind: 'plugin', plugin: 'cordis-host-runner' }, text).title).toBe(`message.trigger.${key}`)
|
||||
'Cordis run plug/pkg (r1) completed successfully.',
|
||||
'The user rejected Cordis run plug/pkg (r1).',
|
||||
'Cordis update plug/pkg (r1) failed after cordis_run returned starting: broken',
|
||||
'Cordis Client guard rejected runtime code in plug/pkg (r1).',
|
||||
])('uses the durable Cordis source independently of producer text %s', (text) => {
|
||||
expect(details({ kind: 'plugin', plugin: 'cordis-host-runner' }, text).title).toBe('message.trigger.plugin')
|
||||
})
|
||||
|
||||
it('uses the durable subagent source independently of settlement wording', () => {
|
||||
expect(details({ kind: 'subagent-settled', senderSessionId: 'child-1' }, 'future settlement wording'))
|
||||
.toEqual({ title: 'message.trigger.subagent', icon: 'subagent' })
|
||||
})
|
||||
|
||||
it('keeps unknown sources and unrecognized status neutral', () => {
|
||||
|
||||
Reference in new issue
Block a user