fix(tui): address onboarding review feedback

This commit is contained in:
NI0317
2026-07-31 17:47:07 +08:00
parent 63404c7a31
commit 346d1e4c74
23 changed files with 212 additions and 221 deletions

View File

@@ -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/ui/tui/README.md
README.md: c1ab57410469d649fd589c5038903324c60343cb
README.zh.md: 7c92136398f7a84213712d06f843a29afd8f1277
README.md: 86e54ab76d07e32fad93965fcbb585d2b5fdfe06
README.zh.md: a8d072a82c9cff8db7cfe436957a9c1391fb8d8f

View File

@@ -10,7 +10,7 @@ Interactive terminals on macOS, Linux, and Windows are supported. Windows uses p
This package owns interactive terminal presentation and input only. It injects `agents`, [`commands`](../commands/README.md), `llm`, `systemPrompt`, `tokenMeter`, `tools`, and `userInteraction`, optionally reads a `skills` service (present only when one is mounted), then drives an agent created or resumed by app or developer code. Agent lifecycle, persistence, and the model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries.
After terminal startup succeeds, the package provides the terminal-local `ctx.tui` extension service. A plugin that injects it can call `openOverlay()` with a component factory and constrained layout options; the host exposes the viewport, semantic theme (including terminal-safe DeepSeek `brand` treatment), display-text escaping, redraw, close, and a lifetime signal, but not the pi-tui tree, terminal, focus controller, or overlay handle. The package exports semantic-key, ANSI-wrap, truncation, and visible-width primitives for extension components, keeping the underlying renderer dependency inside `dsh-tui`. Plugin overlays, the model selector, and user questions share one FIFO modal queue. Each request is an effect of the calling plugin fiber, so unload removes queued work or closes visible work before cleanup settles; terminal shutdown unloads dependents before stopping pi-tui. Overlay state is not logged or replayed. Component code is trusted and may render ANSI styling, but must pass untrusted text through `host.display()`. The [interactive-extension Agent Note](../../../.agents/notes/implemented/architecture/2026-07-22-tui-interactive-extension-service.md) owns the boundary and rejected alternatives.
After terminal startup succeeds, the package provides the terminal-local `ctx.tui` extension service. A plugin that injects it can call `openOverlay()` with a component factory and constrained layout options; the host exposes the viewport, semantic theme (including terminal-safe DeepSeek `brand` treatment), display-text escaping, redraw, close, and a lifetime signal, but not the pi-tui tree, terminal, focus controller, or overlay handle. Plugin overlays, the model selector, and user questions share one FIFO modal queue. Each request is an effect of the calling plugin fiber, so unload removes queued work or closes visible work before cleanup settles; terminal shutdown unloads dependents before stopping pi-tui. Overlay state is not logged or replayed. Component code is trusted and may render ANSI styling, but must pass untrusted text through `host.display()`. The [interactive-extension Agent Note](../../../.agents/notes/implemented/architecture/2026-07-22-tui-interactive-extension-service.md) owns the boundary and rejected alternatives.
The TUI rebuilds resumed history from the append-origin session events, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the standing `todo/write` plan above the editor (cleared on the next `turn/start`), and presents `ctx.userInteraction` questions in a wide bottom-left keyboard panel with progress, numbered options, and aligned descriptions. The latest logged session title becomes the header subtitle, with `welcome` before a title exists, and the terminal window title becomes `<session title> — <configured title>`. A durable `llm/retry` event retracts the failed step's live chunks and renders the scheduled retry count, delay, and failure in the transcript; success, exhaustion, and cancellation then settle through ordinary session events. The footer totals each logged model step's usage once, including failed attempts, while treating committed-message usage as a fallback for logs without a usage chunk. Its idle view compares token-meter pressure with `ctx.llm.resolveModelInfo()` context for the current route, displays `context unknown` when the adapter has no capacity metadata, and also shows tool-card mode plus the current model and any explicitly selected reasoning effort; while the agent runs, an elapsed working indicator and `esc interrupt` replace that summary. A surface replacement never rewrites the rendered transcript: the conversation it shadows stays readable, and a landed compaction checkpoint adds one dim `… earlier context was compacted …` marker at its log position, so the terminal reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies — a pruned tool result, a regenerated assistant message — render nothing.

View File

@@ -10,7 +10,7 @@ DeepSeek Harness agent智能体的交互式终端入口基于 [`@earend
本包package只持有交互式终端展示和输入。它注入 `agents`、[`commands`](../commands/README.md)、`llm``systemPrompt``tokenMeter``tools``userInteraction`,可选读取 `skills` 服务(仅在已挂载时存在),然后驱动由 app 或开发者代码创建或恢复的 agent。Agent 生命周期、持久化与模型侧 [`ask_user_question`](../tool-ask-user/README.md) 工具仍是独立组合项。
终端成功启动后,本包会提供终端本地的 `ctx.tui` 扩展服务。注入该服务的插件可以使用组件工厂和受限布局选项调用 `openOverlay()`;宿主会公开 viewport、语义化主题包括终端安全的 DeepSeek `brand` 样式)、显示文本转义、重绘、关闭和生命周期信号,但不公开 pi-tui 树、终端、焦点控制器或 overlay 句柄。本包还为扩展组件导出语义按键、ANSI 换行、截断和可见宽度原语,使底层 renderer 依赖始终留在 `dsh-tui` 内。插件 overlay、模型选择器和用户问题共用一个 FIFO 模态队列。每个请求都是调用方插件 fiber 的 effect因此卸载会移除排队工作或在清理结算前关闭可见工作终端关闭会先卸载依赖项再停止 pi-tui。Overlay 状态不会记录或回放。组件代码受信任,可以渲染 ANSI 样式,但必须通过 `host.display()` 处理不受信任文本。[交互式扩展 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-22-tui-interactive-extension-service.md)持有该边界和未采用的替代方案。
终端成功启动后,本包会提供终端本地的 `ctx.tui` 扩展服务。注入该服务的插件可以使用组件工厂和受限布局选项调用 `openOverlay()`;宿主会公开 viewport、语义化主题包括终端安全的 DeepSeek `brand` 样式)、显示文本转义、重绘、关闭和生命周期信号,但不公开 pi-tui 树、终端、焦点控制器或 overlay 句柄。插件 overlay、模型选择器和用户问题共用一个 FIFO 模态队列。每个请求都是调用方插件 fiber 的 effect因此卸载会移除排队工作或在清理结算前关闭可见工作终端关闭会先卸载依赖项再停止 pi-tui。Overlay 状态不会记录或回放。组件代码受信任,可以渲染 ANSI 样式,但必须通过 `host.display()` 处理不受信任文本。[交互式扩展 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-22-tui-interactive-extension-service.md)持有该边界和未采用的替代方案。
TUI 从追加来源的会话事件重建已恢复历史,渲染 Markdown 响应与 reasoning将每个工具的 `presentCall` / `presentResult` 意图应用到终端、diff 或通用卡片,把站立的 `todo/write` 计划保留在编辑器上方(下一个 `turn/start` 时清空),并在左下方宽键盘面板中展示 `ctx.userInteraction` 问题,包含进度、编号选项和对齐说明。最新记录的会话标题成为 header 副标题;标题不存在时使用 `welcome`,终端窗口标题则变为 `<session title> — <configured title>`。持久 `llm/retry` 事件会撤回失败步骤的实时 chunk并在 transcript文本记录中渲染计划重试次数、延迟和失败成功、耗尽与取消随后通过普通会话事件结算。Footer 会对每个已记录模型步骤的用量只计一次,包括失败尝试;对于没有用量 chunk 的日志,以已提交消息的用量回退。其空闲视图会将 token-meter 压力与 `ctx.llm.resolveModelInfo()` 为当前路由返回的上下文容量进行比较;适配器没有容量元数据时显示 `context unknown`并显示工具卡片模式、当前模型以及任何显式选择的推理强度。Agent 运行时,这些摘要会替换为已经过工作时间指示器和 `esc interrupt`。表层替换从不重写已渲染的 transcript被它遮蔽的对话仍可阅读而已落地的压缩compaction检查点会在其日志位置添加一行暗色 `… earlier context was compacted …` 标记,因此终端报告的是模型从何处起不再看到那段历史,而不是把它抹掉。仅供模型使用的替换副本——被裁剪的工具结果、重新生成的 assistant 消息——不渲染任何内容。

View File

@@ -1,59 +0,0 @@
/**
* Terminal-safe primitives for components mounted through the TUI extension service.
*
* Extensions use these wrappers instead of importing the underlying renderer, so
* `ctx.tui` remains the owner of key decoding, ANSI wrapping, and column width.
* @module @deepseek-ai/dsh-tui/extension-primitives
*/
import {
Key,
matchesKey,
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
type KeyId,
} from '@earendil-works/pi-tui'
/** Key identifiers accepted by TUI extension components. */
export const TuiKey = Key
/**
* Test whether terminal input matches one semantic key.
* @param data - Raw terminal input delivered to the component.
* @param key - Semantic key identifier to match.
* @returns Whether the input encodes the requested key.
*/
export function matchesTuiKey(data: string, key: KeyId): boolean {
return matchesKey(data, key)
}
/**
* Measure terminal columns after ignoring ANSI control sequences.
* @param value - Styled or plain terminal text.
* @returns Visible terminal-column width.
*/
export function tuiVisibleWidth(value: string): number {
return visibleWidth(value)
}
/**
* Wrap styled terminal text without splitting ANSI sequences.
* @param value - Styled or plain terminal text.
* @param width - Maximum visible columns per line.
* @returns Wrapped lines preserving active ANSI styling.
*/
export function wrapTuiText(value: string, width: number): string[] {
return wrapTextWithAnsi(value, width)
}
/**
* Truncate styled terminal text to a visible-column limit.
* @param value - Styled or plain terminal text.
* @param width - Maximum visible columns.
* @param ellipsis - Suffix used when truncation occurs.
* @returns Text whose visible width does not exceed the limit.
*/
export function truncateTuiText(value: string, width: number, ellipsis = ''): string {
return truncateToWidth(value, width, ellipsis)
}

View File

@@ -141,13 +141,6 @@ import { WorkspaceFileSearch } from './chat/file-autocomplete.ts'
export { TuiPromptService } from './prompt.ts'
export { renderSkillInvocation } from './chat/skill-invocation.ts'
export {
matchesTuiKey,
truncateTuiText,
TuiKey,
tuiVisibleWidth,
wrapTuiText,
} from './extension/primitives.ts'
export type { TuiResumeHost, TuiRuntime } from './runtime.ts'
export {
resolveTuiConfig,

View File

@@ -17,13 +17,6 @@ import {
TuiOverlayManager,
type TuiOverlayDriver,
} from '../src/extension/overlay-manager.ts'
import {
matchesTuiKey,
truncateTuiText,
TuiKey,
tuiVisibleWidth,
wrapTuiText,
} from '../src/index.ts'
const theme: TuiTheme = Object.freeze({
text: (value: string) => `text:${value}`,
@@ -37,17 +30,6 @@ const theme: TuiTheme = Object.freeze({
bold: (value: string) => `bold:${value}`,
})
describe('TUI extension terminal primitives', () => {
it('owns semantic keys, ANSI-safe wrapping, truncation, and visible width', () => {
expect(matchesTuiKey('\r', TuiKey.enter)).toBe(true)
expect(tuiVisibleWidth('\x1b[34m鲸鱼\x1b[39m')).toBe(4)
const truncated = truncateTuiText('鲸鱼欢迎', 6)
expect(truncated).toContain('鲸鱼欢')
expect(tuiVisibleWidth(truncated)).toBe(6)
expect(wrapTuiText('\x1b[34m鲸鱼欢迎\x1b[39m', 4)).toHaveLength(2)
})
})
interface ShownOverlay {
component: Component
options: TuiOverlayOptions | undefined