feat(schedule): add durable after reminders

This commit is contained in:
pku-xht
2026-08-06 03:47:37 +08:00
committed by Tianyi Cui
parent f7e7851e3f
commit 8b69252664
41 changed files with 354 additions and 146 deletions

View File

@@ -24,7 +24,7 @@ The chat view keeps Tool placement but delegates Tool presentation. It passes ea
The chat flow projects consecutive model-retry nodes across retry turns into one stable, muted status row updated to the latest attempt; every retry event remains in the runtime snapshot and session log. Its frontend countdown anchors the scheduled delay to client receipt, avoiding host/browser clock skew, rounds remaining time up to seconds, and has a one-second floor. The latest unresolved retry uses a left-to-right text shimmer. Subsequent turn facts distinguish an attempt that started from one cancelled during backoff, while the Host running bit only controls the live animation; the row then shows a static completed or cancelled label. Normal policy rows show the finite retry maximum; always policy rows show `∞`. Activating the row reveals the latest exact retry delay and failure message. The client runtime removes each failed step's streaming tail before its retry node arrives, while the status remains visible after a later attempt succeeds. An unretried terminal failure renders as a persistent inline status at its turn boundary, showing the display-safe durable message and optional error code without offering an action the Host cannot fulfill; AUTH copy never echoes provider-supplied credential fragments.
Host-presented durable events use the keyed `'conversation.chat.eventview'` seat alongside whole-Tool presentation. The React-free runtime turns a generic `{ presentationKey, view }` sidecar into a `PresentedEventNode`; Chat dispatches on that open key, and a domain UI plugin may register its own row without adding domain vocabulary here. When no registrant is loaded, `GenericEventCard` keeps the presentation key and JSON payload visible in an expandable disclosure rather than dropping the durable event.
Host-presented durable events use the keyed `'conversation.chat.eventview'` seat alongside whole-Tool presentation. The React-free runtime turns a generic event sidecar into a `PresentedEventNode` carrying the durable event type and view; Chat dispatches on that open type, and a domain UI plugin may register its own row without adding domain vocabulary here. When no registrant is loaded, `GenericEventCard` keeps the event type and JSON payload visible in an expandable disclosure rather than dropping the durable event.
`TodoDock` takes the `'conversation.input.dock'` list slot at `order: 0` — before Goal and Queue — and is the plan strip: it reads the host-computed `todos` projection via `useProjection` (standing plan: latest `todo/write` with no later `turn/start`) and renders `TodoPanel`, which takes the plain list, hides itself while the list is empty, and starts collapsed as a header of title plus its own `·`-joined per-status counts (localized, `1 completed · 2 in progress · 1 pending`, zero-count segments omitted). The dock adapter owns selection so the panel stays a pure function of its props. Anything the input-zone composer chain hides (a `conversation.composer` takeover such as ui-question's) hides the whole dock, this strip included. The `todo_write` Tool row belongs to [`ui-tool`](../ui-tool/README.md).

View File

@@ -24,7 +24,7 @@ Think 行默认保持折叠,并在不展开思维链的情况下暴露实时
审批经由本包声明的链接管编辑器:`ApprovalPanel` 注册为按选择器路由的 `'conversation.composer'` 配置项(ui-question 模式),在审批等待未决期间取代 InputBar 占据编辑器(琥珀色条、理由标题、来自运行中调用参数的配对命令行、一次性的拒绝/允许)。`contract/slots.ts` 中的 `PendingApproval` 领域面在运行时 `PendingWait` 载体之上拥有 wire 编码——带审计关联的 `ApprovalResponsePayload` 值;广播的 `approval/resolved` 帧使等待落定并恢复编辑器。运行时 manager 会将所有审批或问题等待通过 `SessionSummary.pendingInteraction` 投影出来,未实例化的 Session 也不例外;`ui-workspace` 负责其侧边栏呈现。未决等待完全离开消息流:问题(ui-question)与审批(ApprovalPanel)都经编辑器接管作答,不再保留只读占位卡。编辑器底行的 Access 席位挂载 `PermissionSelect`,由 host 计算的 `permissions` 投影经标准工具包 `useProjection` 供数(key 缺席即隐藏 chip);chip 打开 Menu 原语下拉,其中 kebab-case 预设名渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission <preset>`,而 `danger-full-access` 在界面中显示为 `Full access`,选择后先打开页面内的 Modal 风险确认。用户勾选确认项前启用按钮始终不可用;取消、Escape、关闭按钮与点击遮罩都不会提交命令。
由 Host presentation 的持久事件使用键控的 `'conversation.chat.eventview'` 座位,与整体 Tool presentation 并行。无 React 的 runtime 会把通用 `{ presentationKey, view }` sidecar 转为 `PresentedEventNode`;Chat 按开放 key 分发,领域 UI 插件无需在本包增加领域词汇即可注册自己的行。没有 registrant 被加载时,`GenericEventCard` 会在可展开 disclosure 中保留可见的 presentation key 与 JSON payload,而不会丢弃该持久事件。
由 Host presentation 的持久事件使用键控的 `'conversation.chat.eventview'` 座位,与整体 Tool presentation 并行。无 React 的 runtime 会把通用事件 sidecar 转为携带持久事件类型与 view 的 `PresentedEventNode`;Chat 按该开放类型分发,领域 UI 插件无需在本包增加领域词汇即可注册自己的行。没有 registrant 被加载时,`GenericEventCard` 会在可展开 disclosure 中保留可见的事件类型与 JSON payload,而不会丢弃该持久事件。
`TodoDock` 以 `order: 0` 占用 `'conversation.input.dock'` 列表 slot(位于 Goal 与 Queue 之前),作为计划条读取 host 计算的 `todos` 投影(站立计划:其后没有更晚 `turn/start` 的最近一次 `todo/write`)并渲染 `TodoPanel`。面板接收纯列表,列表为空时自我隐藏;列表非空时默认折叠,表头显示标题及以 `·` 连接的各状态计数(如 `1 已完成 · 2 进行中 · 1 待处理`,省略零计数)。dock adapter 拥有 selection,因此面板保持为 props 的纯函数。输入区 composer 链隐藏的一切也会隐藏整个 dock。`todo_write` Tool 行属于 [`ui-tool`](../ui-tool/README.md)。

View File

@@ -212,8 +212,8 @@ const CommandRow = memo(function CommandRow({ renderSlot, node, compaction, t }:
)
})
/** One Host-presented durable event: keyed dispatch on its open presentation
* key, with a visible JSON disclosure when no domain renderer is loaded. */
/** One Host-presented durable event: dispatch by durable event type, with a
* visible JSON disclosure when no domain renderer is loaded. */
const EventRow = memo(function EventRow({ renderSlot, node, t }: {
renderSlot: RenderChatSlot
node: PresentedEventNode
@@ -223,7 +223,7 @@ const EventRow = memo(function EventRow({ renderSlot, node, t }: {
return (
<div className={css.callRow}>
{renderSlot('conversation.chat.eventview', owner, {
entryKey: node.presentationKey,
entryKey: node.eventType,
fallback: <GenericEventCard {...owner} t={t} />,
})}
</div>

View File

@@ -1,6 +1,6 @@
// GenericEventCard: the visible fallback for a Host-presented durable event.
// A domain plugin may replace it through the keyed eventview slot; without
// one, the presentation key and JSON sidecar remain inspectable in the flow.
// one, the durable event type and JSON sidecar remain inspectable in the flow.
import { useMemo, useState } from 'react'
import { IconSparkle16 } from '@deepseek-ai/dsh-client-ui-primitives'
@@ -22,7 +22,7 @@ export function GenericEventCard({ node, t }: GenericEventCardProps) {
className={css.root}
icon={<IconSparkle16 size={14} />}
chevronClassName={css.chevron}
title={t('message.presentedEvent', { key: node.presentationKey })}
title={t('message.presentedEvent', { key: node.eventType })}
open={open}
expandable
expandOnRowClick

View File

@@ -110,7 +110,7 @@ const presentedEvent = (seq: number): PresentedEventNode => ({
kind: 'presented-event',
seq,
time: seq * 1_000,
presentationKey: 'schedule/reminder',
eventType: 'schedule/change',
view: { prompt: 'check logs', scheduleId: 'schedule-1' },
})
@@ -969,8 +969,8 @@ describe('ChatView', () => {
return opts?.fallback ?? null
})
const view = render(<h.ChatView {...h.props} />)
expect(calls).toEqual([{ key: 'conversation.chat.eventview', entryKey: 'schedule/reminder' }])
fireEvent.click(view.getByText('事件:schedule/reminder'))
expect(calls).toEqual([{ key: 'conversation.chat.eventview', entryKey: 'schedule/change' }])
fireEvent.click(view.getByText('事件:schedule/change'))
expect(view.getByText(/"prompt": "check logs"/)).toBeTruthy()
expect(view.getByText(/"scheduleId": "schedule-1"/)).toBeTruthy()
})