@autopilot-agent/cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -2
- package/dist/activity-animation.d.ts +57 -0
- package/dist/activity-animation.js +145 -0
- package/dist/animation-clock.d.ts +22 -0
- package/dist/animation-clock.js +76 -0
- package/dist/composition-root.d.ts +105 -0
- package/dist/composition-root.js +588 -0
- package/dist/context-report.d.ts +20 -0
- package/dist/context-report.js +174 -0
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +681 -0
- package/dist/design-tokens.d.ts +34 -0
- package/dist/design-tokens.js +34 -0
- package/dist/error-diagnostics.d.ts +8 -0
- package/dist/error-diagnostics.js +62 -0
- package/dist/event-projector.d.ts +26 -0
- package/dist/event-projector.js +333 -0
- package/dist/file-mention-context.d.ts +34 -0
- package/dist/file-mention-context.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/main.d.ts +40 -0
- package/dist/main.js +543 -0
- package/dist/parser.d.ts +17 -0
- package/dist/parser.js +391 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.js +48 -0
- package/dist/scroll-viewport.d.ts +22 -0
- package/dist/scroll-viewport.js +62 -0
- package/dist/termination-format.d.ts +4 -0
- package/dist/termination-format.js +17 -0
- package/dist/timeline-lines.d.ts +38 -0
- package/dist/timeline-lines.js +104 -0
- package/dist/tui/app.d.ts +30 -0
- package/dist/tui/app.js +410 -0
- package/dist/tui/approval.d.ts +19 -0
- package/dist/tui/approval.js +37 -0
- package/dist/tui/brand/brand-header.d.ts +20 -0
- package/dist/tui/brand/brand-header.js +22 -0
- package/dist/tui/brand/intro.d.ts +12 -0
- package/dist/tui/brand/intro.js +88 -0
- package/dist/tui/brand/pixel.d.ts +8 -0
- package/dist/tui/brand/pixel.js +50 -0
- package/dist/tui/constants.d.ts +56 -0
- package/dist/tui/constants.js +57 -0
- package/dist/tui/context.d.ts +11 -0
- package/dist/tui/context.js +12 -0
- package/dist/tui/diff-stats.d.ts +8 -0
- package/dist/tui/diff-stats.js +7 -0
- package/dist/tui/format.d.ts +75 -0
- package/dist/tui/format.js +176 -0
- package/dist/tui/hooks/use-file-mention-index.d.ts +15 -0
- package/dist/tui/hooks/use-file-mention-index.js +59 -0
- package/dist/tui/hooks/use-pickers.d.ts +47 -0
- package/dist/tui/hooks/use-pickers.js +288 -0
- package/dist/tui/hooks/use-prompt-input.d.ts +54 -0
- package/dist/tui/hooks/use-prompt-input.js +258 -0
- package/dist/tui/hooks/use-status-view.d.ts +8 -0
- package/dist/tui/hooks/use-status-view.js +13 -0
- package/dist/tui/hooks/use-viewport.d.ts +22 -0
- package/dist/tui/hooks/use-viewport.js +117 -0
- package/dist/tui/input/file-mention.d.ts +33 -0
- package/dist/tui/input/file-mention.js +112 -0
- package/dist/tui/input/graphemes.d.ts +5 -0
- package/dist/tui/input/graphemes.js +10 -0
- package/dist/tui/input/keys.d.ts +16 -0
- package/dist/tui/input/keys.js +58 -0
- package/dist/tui/input/prompt-layout.d.ts +27 -0
- package/dist/tui/input/prompt-layout.js +84 -0
- package/dist/tui/input-area.d.ts +53 -0
- package/dist/tui/input-area.js +89 -0
- package/dist/tui/jobs.d.ts +10 -0
- package/dist/tui/jobs.js +24 -0
- package/dist/tui/lifecycle.d.ts +23 -0
- package/dist/tui/lifecycle.js +114 -0
- package/dist/tui/markdown/diff.d.ts +8 -0
- package/dist/tui/markdown/diff.js +19 -0
- package/dist/tui/markdown/inline.d.ts +4 -0
- package/dist/tui/markdown/inline.js +32 -0
- package/dist/tui/markdown/markdown.d.ts +17 -0
- package/dist/tui/markdown/markdown.js +117 -0
- package/dist/tui/markdown/table.d.ts +19 -0
- package/dist/tui/markdown/table.js +74 -0
- package/dist/tui/performance-diagnostics.d.ts +7 -0
- package/dist/tui/performance-diagnostics.js +57 -0
- package/dist/tui/pickers/effort.d.ts +10 -0
- package/dist/tui/pickers/effort.js +12 -0
- package/dist/tui/pickers/file-mention.d.ts +11 -0
- package/dist/tui/pickers/file-mention.js +22 -0
- package/dist/tui/pickers/model.d.ts +10 -0
- package/dist/tui/pickers/model.js +12 -0
- package/dist/tui/pickers/permission.d.ts +10 -0
- package/dist/tui/pickers/permission.js +13 -0
- package/dist/tui/pickers/skill.d.ts +13 -0
- package/dist/tui/pickers/skill.js +25 -0
- package/dist/tui/pickers/slash.d.ts +10 -0
- package/dist/tui/pickers/slash.js +11 -0
- package/dist/tui/prompt-input.d.ts +11 -0
- package/dist/tui/prompt-input.js +73 -0
- package/dist/tui/render-mode.d.ts +20 -0
- package/dist/tui/render-mode.js +31 -0
- package/dist/tui/run.d.ts +6 -0
- package/dist/tui/run.js +237 -0
- package/dist/tui/status-line.d.ts +16 -0
- package/dist/tui/status-line.js +37 -0
- package/dist/tui/store.d.ts +4 -0
- package/dist/tui/store.js +344 -0
- package/dist/tui/timeline/live.d.ts +16 -0
- package/dist/tui/timeline/live.js +151 -0
- package/dist/tui/timeline/presentation-timeline.d.ts +14 -0
- package/dist/tui/timeline/presentation-timeline.js +109 -0
- package/dist/tui/timeline/progress.d.ts +12 -0
- package/dist/tui/timeline/progress.js +43 -0
- package/dist/tui/timeline/static-timeline.d.ts +13 -0
- package/dist/tui/timeline/static-timeline.js +121 -0
- package/dist/tui/timeline/tool-tree.d.ts +7 -0
- package/dist/tui/timeline/tool-tree.js +25 -0
- package/dist/tui/timeline/turn.d.ts +19 -0
- package/dist/tui/timeline/turn.js +21 -0
- package/dist/tui/todo-list.d.ts +50 -0
- package/dist/tui/todo-list.js +120 -0
- package/dist/tui/transcript.d.ts +24 -0
- package/dist/tui/transcript.js +34 -0
- package/dist/tui/trust-dialog.d.ts +51 -0
- package/dist/tui/trust-dialog.js +127 -0
- package/dist/tui/types.d.ts +175 -0
- package/dist/tui/types.js +5 -0
- package/dist/tui/viewport/selection.d.ts +27 -0
- package/dist/tui/viewport/selection.js +79 -0
- package/dist/tui/viewport/sources.d.ts +29 -0
- package/dist/tui/viewport/sources.js +585 -0
- package/dist/tui/viewport/viewport.d.ts +12 -0
- package/dist/tui/viewport/viewport.js +87 -0
- package/dist/tui-input-history.d.ts +23 -0
- package/dist/tui-input-history.js +42 -0
- package/dist/tui-input-router.d.ts +24 -0
- package/dist/tui-input-router.js +115 -0
- package/dist/tui.d.ts +15 -0
- package/dist/tui.js +13 -0
- package/dist/types.d.ts +454 -0
- package/dist/types.js +7 -0
- package/package.json +58 -4
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/** 动态时间线的增量缓存与渲染:只更新尾部事件或当前回答块,避免重扫全部事件。 */
|
|
3
|
+
import { memo, useRef } from "react";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
6
|
+
import { TUI_AGENT_LABEL } from "../constants.js";
|
|
7
|
+
import { formatTuiDuration, formatTuiInputConsumed, getTuiApprovalDecisionLabel, isTerminalStatus } from "../format.js";
|
|
8
|
+
import { TuiMarkdown } from "../markdown/markdown.js";
|
|
9
|
+
import { renderTuiToolTreeEvent } from "./tool-tree.js";
|
|
10
|
+
/** 为增量时间线生成稳定键,避免 answer delta 到达时重建历史节点。 */
|
|
11
|
+
function getCliTuiTimelineEventKey(event) {
|
|
12
|
+
const inputId = "inputId" in event ? event.inputId : "";
|
|
13
|
+
const approvalId = "approvalId" in event ? event.approvalId : "";
|
|
14
|
+
return `${event.type}:${event.seq}:${event.turnId}:${inputId}:${approvalId}`;
|
|
15
|
+
}
|
|
16
|
+
/** 判断新事件列表是否只是同一 Turn 的尾部追加。 */
|
|
17
|
+
function isCliTuiTimelineAppendOnly(cache, events) {
|
|
18
|
+
if (cache.eventCount === 0)
|
|
19
|
+
return true;
|
|
20
|
+
return events.length >= cache.eventCount && events[cache.eventCount - 1] === cache.lastEvent;
|
|
21
|
+
}
|
|
22
|
+
/** 应用一条时间线事件,只修改新增尾部或当前回答块。 */
|
|
23
|
+
function appendCliTuiTimelineEvent(cache, event) {
|
|
24
|
+
if (event.type === "answer_delta") {
|
|
25
|
+
if (!event.delta)
|
|
26
|
+
return;
|
|
27
|
+
const lastBlock = cache.blocks[cache.blocks.length - 1];
|
|
28
|
+
if (lastBlock?.kind === "answer") {
|
|
29
|
+
lastBlock.answer += event.delta;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
cache.blocks.push({ kind: "answer", key: "answer:" + String(event.seq), answer: event.delta });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (event.type === "answer_reset") {
|
|
36
|
+
for (let index = cache.blocks.length - 1; index >= 0; index -= 1) {
|
|
37
|
+
if (cache.blocks[index]?.kind === "answer") {
|
|
38
|
+
cache.blocks.splice(index, 1);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (event.type === "tui_approval_submitted") {
|
|
44
|
+
for (const block of cache.blocks) {
|
|
45
|
+
if (block.kind === "event" && block.event.type === "approval_required" && block.event.approvalId === event.approvalId)
|
|
46
|
+
block.hidden = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
cache.blocks.push({ kind: "event", key: getCliTuiTimelineEventKey(event), event });
|
|
50
|
+
}
|
|
51
|
+
/** 首次或 Turn 切换时完整归约时间线;后续更新走尾部增量路径。 */
|
|
52
|
+
function createCliTuiTimelineCache(events) {
|
|
53
|
+
const cache = { eventCount: 0, blocks: [] };
|
|
54
|
+
for (const event of events)
|
|
55
|
+
appendCliTuiTimelineEvent(cache, event);
|
|
56
|
+
cache.eventCount = events.length;
|
|
57
|
+
cache.lastEvent = events[events.length - 1];
|
|
58
|
+
return cache;
|
|
59
|
+
}
|
|
60
|
+
/** 为当前 live Turn 复用已归约的时间线块,避免每个输入字符重扫全部事件。 */
|
|
61
|
+
function useCliTuiTimelineBlocks(events) {
|
|
62
|
+
const cacheRef = useRef(undefined);
|
|
63
|
+
let cache = cacheRef.current;
|
|
64
|
+
if (!cache || !isCliTuiTimelineAppendOnly(cache, events)) {
|
|
65
|
+
cache = createCliTuiTimelineCache(events);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
for (let index = cache.eventCount; index < events.length; index += 1) {
|
|
69
|
+
const event = events[index];
|
|
70
|
+
if (event)
|
|
71
|
+
appendCliTuiTimelineEvent(cache, event);
|
|
72
|
+
}
|
|
73
|
+
cache.eventCount = events.length;
|
|
74
|
+
cache.lastEvent = events[events.length - 1];
|
|
75
|
+
}
|
|
76
|
+
cacheRef.current = cache;
|
|
77
|
+
return cache.blocks;
|
|
78
|
+
}
|
|
79
|
+
/** 渲染一个不再随连续 answer delta 改变的安全事件节点。 */
|
|
80
|
+
function TuiLiveEventBlock(props) {
|
|
81
|
+
const event = props.event;
|
|
82
|
+
if (event.type === "tui_input_consumed") {
|
|
83
|
+
return _jsx(Text, { dimColor: props.useColor, children: formatTuiInputConsumed(event) });
|
|
84
|
+
}
|
|
85
|
+
if (event.type === "tui_approval_submitted") {
|
|
86
|
+
return _jsxs(Text, { dimColor: props.useColor, children: ["Approval submitted \u00B7 ", getTuiApprovalDecisionLabel(event.decision)] });
|
|
87
|
+
}
|
|
88
|
+
if (event.type === "turn_started" || event.type === "reasoning_summary" || event.type === "reasoning_hidden" || event.type === "answer_reset" || event.type === "input_joined" || event.type === "input_consumed" || event.type === "active_skills")
|
|
89
|
+
return null;
|
|
90
|
+
if (event.type === "tool_started" || event.type === "tool_result")
|
|
91
|
+
return renderTuiToolTreeEvent(event, props.useColor);
|
|
92
|
+
if (event.type === "approval_required")
|
|
93
|
+
return null;
|
|
94
|
+
if (event.type === "input_queued")
|
|
95
|
+
return _jsx(Text, { dimColor: props.useColor, children: "Additional instruction queued; waiting for the current step" });
|
|
96
|
+
if (event.type === "todo_updated")
|
|
97
|
+
return null;
|
|
98
|
+
if (event.type === "model_recovery")
|
|
99
|
+
return _jsxs(Text, { dimColor: props.useColor, children: ["Recovering \u00B7 ", event.reason] });
|
|
100
|
+
if (event.type === "context_overflow")
|
|
101
|
+
return _jsx(Text, { children: "Context limit reached; organizing before continuing" });
|
|
102
|
+
if (event.type === "context_compact_started")
|
|
103
|
+
return _jsx(Text, { dimColor: props.useColor, children: "Organizing context" });
|
|
104
|
+
if (event.type === "context_compacted")
|
|
105
|
+
return _jsx(Text, { children: "Context organized" });
|
|
106
|
+
if (event.type === "context_compact_failed")
|
|
107
|
+
return _jsx(Text, { children: "Context organization failed" });
|
|
108
|
+
if (event.type === "context_compact_circuit_open")
|
|
109
|
+
return _jsx(Text, { children: "Context organization temporarily unavailable" });
|
|
110
|
+
if (event.type === "status" && event.status === "failed" && event.reason)
|
|
111
|
+
return _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] });
|
|
112
|
+
if (event.type === "status")
|
|
113
|
+
return null;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** 对历史事件节点启用 memo,使新增 answer delta 不重新执行工具/diff/审批渲染。 */
|
|
117
|
+
const MemoizedTuiLiveEventBlock = memo(TuiLiveEventBlock);
|
|
118
|
+
/** 渲染当前累计回答块;只有最后一个连续回答块会随 delta 更新。 */
|
|
119
|
+
function TuiLiveAnswerBlock(props) {
|
|
120
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: props.useColor ? TUI_ACCENT_COLOR : undefined, children: ["\u2022 ", TUI_AGENT_LABEL] }), props.renderMarkdown
|
|
121
|
+
? _jsx(TuiMarkdown, { content: props.answer, keyPrefix: props.keyPrefix, useColor: props.useColor, maximumWidth: props.maximumWidth })
|
|
122
|
+
: _jsx(Text, { wrap: "wrap", children: props.answer })] }));
|
|
123
|
+
}
|
|
124
|
+
/** 对回答块启用 memo,保持长时间线中已经完成的 Markdown 节点不变。 */
|
|
125
|
+
const MemoizedTuiLiveAnswerBlock = memo(TuiLiveAnswerBlock);
|
|
126
|
+
/** 增量渲染当前 Turn 的时间线块;完整 transcript 由历史树使用 renderTimeline。 */
|
|
127
|
+
function renderTuiLiveTimelineBlock(block, useColor, renderMarkdown, maximumWidth) {
|
|
128
|
+
if (block.kind === "answer") {
|
|
129
|
+
return _jsx(MemoizedTuiLiveAnswerBlock, { answer: block.answer, keyPrefix: block.key, useColor: useColor, renderMarkdown: renderMarkdown, maximumWidth: maximumWidth });
|
|
130
|
+
}
|
|
131
|
+
if (block.hidden)
|
|
132
|
+
return null;
|
|
133
|
+
if (block.event.type === "answer_delta")
|
|
134
|
+
return null;
|
|
135
|
+
return _jsx(MemoizedTuiLiveEventBlock, { event: block.event, useColor: useColor });
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Ink 在动态区超过一屏后会清空终端并从顶部重绘;把已经确定的块交给
|
|
139
|
+
* 历史树后,动态区最多只保留当前回答或未决审批,避免活动期间
|
|
140
|
+
* 的每个事件都把 Windows Terminal 的原生滚动视口拉回顶部。
|
|
141
|
+
*/
|
|
142
|
+
export const TuiLiveTimeline = memo(function TuiLiveTimeline(props) {
|
|
143
|
+
const blocks = useCliTuiTimelineBlocks(props.events);
|
|
144
|
+
const firstAnswerKey = blocks.find(function findFirstAnswer(block) { return block.kind === "answer"; })?.key;
|
|
145
|
+
function renderBlock(block) {
|
|
146
|
+
return (_jsxs(Box, { flexDirection: "column", children: [block.key === firstAnswerKey && props.metrics?.firstAnswerAt !== undefined ? _jsxs(Text, { dimColor: props.useColor, children: ["Thought for ", formatTuiDuration(Math.max(0, props.metrics.firstAnswerAt - props.metrics.startedAt))] }) : null, renderTuiLiveTimelineBlock(block, props.useColor, isTerminalStatus(props.status), props.maximumWidth)] }, block.key));
|
|
147
|
+
}
|
|
148
|
+
return (_jsx(_Fragment, { children: blocks.map(function renderDynamicTimelineBlock(block) {
|
|
149
|
+
return renderBlock(block);
|
|
150
|
+
}) }));
|
|
151
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** 基于共享 Presentation selector 的 CLI 时间线;不读取旧工具 summary、detail 或审批 preview。 */
|
|
2
|
+
import { type AgentPresentationState } from "@autopilot-agent/presentation";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
/** 详情视图参数:expanded 驱动 Todo 卡展开,toolGroupsExpanded 驱动工具分组展开;screen reader 恒展开全部明细。 */
|
|
5
|
+
type TuiDetailView = {
|
|
6
|
+
expanded: boolean;
|
|
7
|
+
screenReader: boolean;
|
|
8
|
+
rowLimit?: number;
|
|
9
|
+
showToggleHint?: boolean;
|
|
10
|
+
toolGroupsExpanded?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** 渲染指定 Turn 的共享 Presentation 时间线;无匹配节点时由调用方回退兼容旧事件。 */
|
|
13
|
+
export declare function renderTuiPresentationTimeline(presentation: AgentPresentationState, turnId: string, useColor: boolean, maximumWidth: number, detailView: TuiDetailView): ReactNode[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** 基于共享 Presentation selector 的 CLI 时间线;不读取旧工具 summary、detail 或审批 preview。 */
|
|
3
|
+
import { getPresentationCopy, selectTimeline, selectToolCalls } from "@autopilot-agent/presentation";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { TUI_ACCENT_BG_COLOR, TUI_ACCENT_COLOR, TUI_CHIP_TEXT_COLOR, TUI_TOOL_ERROR_COLOR } from "../../design-tokens.js";
|
|
6
|
+
import { TUI_AGENT_LABEL } from "../constants.js";
|
|
7
|
+
import { TuiDiffStats } from "../diff-stats.js";
|
|
8
|
+
import { getTuiDiffStats } from "../format.js";
|
|
9
|
+
import { TuiDiffBlock } from "../markdown/diff.js";
|
|
10
|
+
import { TuiMarkdown } from "../markdown/markdown.js";
|
|
11
|
+
import { TuiTodoList } from "../todo-list.js";
|
|
12
|
+
/** 取得 ToolCall 的纯文本状态标志,不依赖颜色表达执行结果。 */
|
|
13
|
+
function getToolStateGlyph(state) {
|
|
14
|
+
if (state === "completed")
|
|
15
|
+
return "•";
|
|
16
|
+
if (state === "denied")
|
|
17
|
+
return "×";
|
|
18
|
+
if (state === "cancelled" || state === "timeout")
|
|
19
|
+
return "⚠";
|
|
20
|
+
if (state === "failed")
|
|
21
|
+
return "×";
|
|
22
|
+
return "✢";
|
|
23
|
+
}
|
|
24
|
+
/** 判断工具是否处于失败类终态;失败步骤文字使用错误红并保留文字标签。 */
|
|
25
|
+
function isFailedToolState(state) {
|
|
26
|
+
return state === "failed" || state === "denied" || state === "cancelled" || state === "timeout";
|
|
27
|
+
}
|
|
28
|
+
/** 取得 Turn 的纯文本终态标志,不把失败、取消和上限混为成功。 */
|
|
29
|
+
function getTurnStateGlyph(state) {
|
|
30
|
+
return state === "completed" ? "✓" : state === "interrupted" || state === "paused" ? "⚠" : "×";
|
|
31
|
+
}
|
|
32
|
+
/** 从 Core 冻结的 Web 明细生成 TUI 安全文案,不读取 query、URL 路径或网页正文。 */
|
|
33
|
+
function getWebToolDetailText(item) {
|
|
34
|
+
if (item.detail?.kind === "web_search") {
|
|
35
|
+
const summary = item.detail.sourceCount === 0 ? "搜索服务未返回结构化引用" : `找到 ${item.detail.sourceCount} 个公开来源`;
|
|
36
|
+
return `${summary}${item.detail.truncated ? " · 结果已截断" : ""}`;
|
|
37
|
+
}
|
|
38
|
+
if (item.detail?.kind === "web_fetch") {
|
|
39
|
+
return `${item.detail.host} · ${item.detail.contentType}${item.detail.truncated ? " · 内容已截断" : ""}`;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** 将一个共享 selector 节点渲染为 Ink 视图,Tool 节点的 React key 始终绑定 callId。 */
|
|
44
|
+
function renderPresentationItem(item, useColor, maximumWidth, detailView, toolCallIndex) {
|
|
45
|
+
if (item.kind === "thinking")
|
|
46
|
+
return _jsxs(Text, { dimColor: useColor, children: ["\u2722 ", item.summary] }, item.id);
|
|
47
|
+
if (item.kind === "tool_group") {
|
|
48
|
+
const summary = (_jsxs(Text, { children: [_jsx(Text, { dimColor: useColor, children: "\u2022 Exploring codebase \u00B7 " }), _jsx(Text, { color: useColor ? TUI_ACCENT_COLOR : undefined, children: item.toolCallIds.length }), _jsx(Text, { dimColor: useColor, children: " operations" })] }, item.id));
|
|
49
|
+
// 分组默认只显示摘要;Ctrl+O 详情态或 screen reader 下展开渲染组内各调用。
|
|
50
|
+
if (!detailView.toolGroupsExpanded && !detailView.screenReader)
|
|
51
|
+
return summary;
|
|
52
|
+
const children = item.toolCallIds
|
|
53
|
+
.map(function findGroupChild(callId) { return toolCallIndex?.get(callId); })
|
|
54
|
+
.filter(function isKnownCall(child) { return child !== undefined; });
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", children: [summary, children.map(function renderGroupChild(child) { return _jsx(Box, { paddingLeft: 2, children: renderPresentationItem(child, useColor, maximumWidth, detailView, toolCallIndex) }, child.callId); })] }, item.id));
|
|
56
|
+
}
|
|
57
|
+
if (item.kind === "tool_call") {
|
|
58
|
+
const title = getPresentationCopy(item.titleKey);
|
|
59
|
+
const jobProgramLabel = item.job?.action === "start" ? item.job.programLabel?.toUpperCase() : undefined;
|
|
60
|
+
const diffStats = getTuiDiffStats(item.diff);
|
|
61
|
+
const detail = getWebToolDetailText(item) ?? [jobProgramLabel ? undefined : item.target, item.resultSummary].filter(Boolean).join(" · ");
|
|
62
|
+
const glyph = getToolStateGlyph(item.state);
|
|
63
|
+
// 徽标 chip 两态:活动与完成步骤都持久使用强调背景 + 深色粗体,失败类终态整行用错误红。
|
|
64
|
+
const head = isFailedToolState(item.state)
|
|
65
|
+
? _jsxs(Text, { color: useColor ? TUI_TOOL_ERROR_COLOR : undefined, children: [glyph, " [", title, "]"] })
|
|
66
|
+
: _jsxs(Text, { children: [_jsxs(Text, { dimColor: useColor, children: [glyph, " "] }), _jsxs(Text, { backgroundColor: useColor ? TUI_ACCENT_BG_COLOR : undefined, color: useColor ? TUI_CHIP_TEXT_COLOR : undefined, bold: useColor || undefined, children: ["[", title, "]"] })] });
|
|
67
|
+
return _jsxs(Box, { flexDirection: "column", children: [head, jobProgramLabel ? _jsxs(Text, { children: [" \u2514\u2500 ", _jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: jobProgramLabel }), detail ? _jsxs(Text, { dimColor: useColor, children: [" \u00B7 ", detail] }) : null] }) : detail || diffStats ? _jsxs(Text, { children: [" \u2514\u2500 ", detail ? _jsxs(Text, { dimColor: useColor, children: [detail, " "] }) : null, diffStats ? _jsx(TuiDiffStats, { ...diffStats, useColor: useColor }) : null] }) : null, item.errorCode ? _jsxs(Text, { dimColor: useColor, children: [" \u2514\u2500 ", item.errorCode] }) : null, item.job?.action === "start" && (item.job.observedStatus === "starting" || item.job.observedStatus === "running") ? _jsx(Text, { dimColor: useColor, children: " /jobs \u67E5\u770B\u540E\u53F0\u4EFB\u52A1" }) : null, item.diff?.text ? _jsx(TuiDiffBlock, { diff: item.diff.text, keyPrefix: item.callId, useColor: useColor }) : null] }, item.callId);
|
|
68
|
+
}
|
|
69
|
+
if (item.kind === "approval") {
|
|
70
|
+
const detail = item.display?.actionPreview ?? item.display?.operation ?? "需要确认";
|
|
71
|
+
return _jsxs(Text, { dimColor: useColor, children: ["\u26A0 ", detail, item.state === "resolved" ? " · 已处理" : ""] }, item.id);
|
|
72
|
+
}
|
|
73
|
+
if (item.kind === "user_question")
|
|
74
|
+
return _jsx(Text, { dimColor: useColor, children: "\u8BF7\u5728\u652F\u6301\u7ED3\u6784\u5316\u56DE\u7B54\u7684 Web \u4E2D\u5904\u7406" }, item.id);
|
|
75
|
+
if (item.kind === "todo")
|
|
76
|
+
return _jsx(TuiTodoList, { item: item, expanded: detailView.expanded, useColor: useColor, screenReader: detailView.screenReader, columns: maximumWidth, rowLimit: detailView.rowLimit, showToggleHint: detailView.showToggleHint }, item.id);
|
|
77
|
+
if (item.kind === "assistant_answer") {
|
|
78
|
+
if (item.channel === "process")
|
|
79
|
+
return _jsx(TuiMarkdown, { content: item.content, keyPrefix: item.id, useColor: useColor, maximumWidth: maximumWidth }, item.id);
|
|
80
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: ["\u2022 ", TUI_AGENT_LABEL] }), _jsx(TuiMarkdown, { content: item.content, keyPrefix: item.id, useColor: useColor, maximumWidth: maximumWidth })] }, item.id);
|
|
81
|
+
}
|
|
82
|
+
if (item.kind === "context_notice")
|
|
83
|
+
return _jsx(Text, { dimColor: useColor, children: item.state === "compacted" ? "Context organized" : `Context ${item.state}` }, item.id);
|
|
84
|
+
if (item.kind === "undo") {
|
|
85
|
+
const isPartial = item.state === "failed" && (item.restoredPaths.length > 0 || item.compensatedPaths.length > 0);
|
|
86
|
+
const title = item.state === "completed" ? "Undo completed" : isPartial ? "Undo incomplete" : "Undo failed";
|
|
87
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: useColor, children: [item.state === "completed" ? "✓" : "⚠", " ", title] }), item.restoredPaths.length > 0 ? _jsxs(Text, { dimColor: useColor, children: [" restored: ", item.restoredPaths.join(", ")] }) : null, item.compensatedPaths.length > 0 ? _jsxs(Text, { dimColor: useColor, children: [" compensated: ", item.compensatedPaths.join(", ")] }) : null, item.conflicts.length > 0 ? _jsxs(Text, { dimColor: useColor, children: [" conflicts (not overwritten): ", item.conflicts.join(", ")] }) : null] }, item.id);
|
|
88
|
+
}
|
|
89
|
+
if (item.kind === "input_notice")
|
|
90
|
+
return _jsxs(Text, { dimColor: useColor, children: ["Input ", item.state, item.content ? ` · ${item.content}` : ""] }, item.id);
|
|
91
|
+
if (item.kind === "turn_summary")
|
|
92
|
+
return _jsxs(Text, { dimColor: useColor, children: [getTurnStateGlyph(item.state), " ", getPresentationCopy(`turn.${item.state}`), " \u00B7 ", item.toolCount, " tools"] }, item.id);
|
|
93
|
+
if (item.kind === "job") {
|
|
94
|
+
if (item.state === "interrupted")
|
|
95
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: useColor, children: "\u26A0 Job interrupted" }), _jsx(Text, { dimColor: useColor, children: " Process cannot be reattached after restart." })] }, item.id);
|
|
96
|
+
return _jsxs(Text, { dimColor: useColor, children: ["Job ", item.state, item.summary ? ` · ${item.summary}` : ""] }, item.id);
|
|
97
|
+
}
|
|
98
|
+
if (item.kind === "capability_notice")
|
|
99
|
+
return _jsx(Text, { dimColor: useColor, children: item.summary ?? item.state }, item.id);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/** 渲染指定 Turn 的共享 Presentation 时间线;无匹配节点时由调用方回退兼容旧事件。 */
|
|
103
|
+
export function renderTuiPresentationTimeline(presentation, turnId, useColor, maximumWidth, detailView) {
|
|
104
|
+
// 构建 callId 索引供展开分组取回组内调用;分组只按 callId 引用,不携带子节点事实。
|
|
105
|
+
const toolCallIndex = new Map(selectToolCalls(presentation, turnId).map(function indexToolCall(call) { return [call.callId, call]; }));
|
|
106
|
+
return selectTimeline(presentation)
|
|
107
|
+
.filter(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === turnId); })
|
|
108
|
+
.map(function renderSharedTimelineItem(item) { return renderPresentationItem(item, useColor, maximumWidth, detailView, toolCallIndex); });
|
|
109
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** 当前思考进度汇总;只统计已发出的 tool_started,避免结果重复计数。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliTurnStatus } from "../../types.js";
|
|
4
|
+
import type { CliTuiTimelineEvent, CliTuiTurnMetrics } from "../types.js";
|
|
5
|
+
/** 汇总 Claude Code 风格的当前思考进度;只统计已发出的 tool_started,避免结果重复计数。 */
|
|
6
|
+
export declare function TuiLiveProgressSummary(props: {
|
|
7
|
+
events: CliTuiTimelineEvent[];
|
|
8
|
+
status: CliTurnStatus;
|
|
9
|
+
metrics?: CliTuiTurnMetrics;
|
|
10
|
+
useColor: boolean;
|
|
11
|
+
activeTodoForm?: string;
|
|
12
|
+
}): ReactNode;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** 当前思考进度汇总;只统计已发出的 tool_started,避免结果重复计数。 */
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Text } from "ink";
|
|
5
|
+
import { formatTuiDuration } from "../format.js";
|
|
6
|
+
/** 汇总 Claude Code 风格的当前思考进度;只统计已发出的 tool_started,避免结果重复计数。 */
|
|
7
|
+
export function TuiLiveProgressSummary(props) {
|
|
8
|
+
const [now, setNow] = useState(() => Date.now());
|
|
9
|
+
const active = props.status === "running" || props.status === "waiting_approval";
|
|
10
|
+
useEffect(function refreshProgressSummary() {
|
|
11
|
+
if (!active)
|
|
12
|
+
return;
|
|
13
|
+
const timer = setInterval(function updateProgressSummaryClock() { setNow(Date.now()); }, 1_000);
|
|
14
|
+
return function stopProgressSummaryClock() { clearInterval(timer); };
|
|
15
|
+
}, [active]);
|
|
16
|
+
if (!active || !props.metrics)
|
|
17
|
+
return null;
|
|
18
|
+
// Todo 的 activeForm 是模型声明的稳定活动文案,优先于宿主猜测的工具动词短语。
|
|
19
|
+
if (props.activeTodoForm) {
|
|
20
|
+
return _jsxs(Text, { dimColor: props.useColor, children: [props.activeTodoForm, " \u00B7 ", formatTuiDuration(Math.max(0, now - props.metrics.startedAt)), "\u2026"] });
|
|
21
|
+
}
|
|
22
|
+
const counts = new Map();
|
|
23
|
+
for (const event of props.events) {
|
|
24
|
+
if (event.type !== "tool_started")
|
|
25
|
+
continue;
|
|
26
|
+
counts.set(event.toolName, (counts.get(event.toolName) ?? 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
const phrases = [];
|
|
29
|
+
const phrase = function makePhrase(toolName, verb, singular) {
|
|
30
|
+
const count = counts.get(toolName) ?? 0;
|
|
31
|
+
if (count === 0)
|
|
32
|
+
return;
|
|
33
|
+
phrases.push(`${verb} ${count} ${count === 1 ? singular : singular + "s"}`);
|
|
34
|
+
};
|
|
35
|
+
phrase("glob_files", "listing", "directory");
|
|
36
|
+
phrase("grep_files", "searching", "code search");
|
|
37
|
+
phrase("read_file", "reading", "file");
|
|
38
|
+
phrase("run_command", "running", "shell command");
|
|
39
|
+
phrase("write_file", "writing", "file");
|
|
40
|
+
phrase("edit_file", "editing", "file");
|
|
41
|
+
const thinking = `Thinking for ${formatTuiDuration(Math.max(0, now - props.metrics.startedAt))}`;
|
|
42
|
+
return _jsxs(Text, { dimColor: props.useColor, children: [thinking, phrases.length > 0 ? `, ${phrases.join(", ")}` : "", "\u2026"] });
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 历史 Turn 时间线:把安全事件聚合为可读时间线行,连续 answer_delta 只显示为一个回答块。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliTurnStatus } from "../../types.js";
|
|
4
|
+
import type { CliTuiTimelineEvent } from "../types.js";
|
|
5
|
+
import type { AgentPresentationState } from "@autopilot-agent/presentation";
|
|
6
|
+
/** 将安全事件聚合为可读的时间线行,连续 answer_delta 只显示为一个回答块。 */
|
|
7
|
+
export declare function renderTimeline(events: CliTuiTimelineEvent[], useColor: boolean, timing?: {
|
|
8
|
+
thoughtDurationMs?: number;
|
|
9
|
+
totalDurationMs?: number;
|
|
10
|
+
presentation?: AgentPresentationState;
|
|
11
|
+
turnId?: string;
|
|
12
|
+
status?: CliTurnStatus;
|
|
13
|
+
}, maximumWidth?: number, screenReader?: boolean): ReactNode[];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_COLOR } from "../../design-tokens.js";
|
|
4
|
+
import { TUI_AGENT_LABEL } from "../constants.js";
|
|
5
|
+
import { formatTuiDuration, formatTuiInputConsumed, getTuiApprovalDecisionLabel, getTuiToolLabel, getTuiTerminationSummary, getTuiTurnFailureReason } from "../format.js";
|
|
6
|
+
import { TuiMarkdown } from "../markdown/markdown.js";
|
|
7
|
+
import { renderTuiToolTreeEvent } from "./tool-tree.js";
|
|
8
|
+
import { renderTuiPresentationTimeline } from "./presentation-timeline.js";
|
|
9
|
+
/** 将安全事件聚合为可读的时间线行,连续 answer_delta 只显示为一个回答块。 */
|
|
10
|
+
export function renderTimeline(events, useColor, timing, maximumWidth = 80, screenReader = false) {
|
|
11
|
+
// 历史区在 Turn 终态只固化一次最终快照;读屏模式继续输出中文状态,不能退化为 glyph-only。
|
|
12
|
+
if (timing?.presentation && timing.turnId && timing.presentation.items.some(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === timing.turnId); })) {
|
|
13
|
+
const nodes = renderTuiPresentationTimeline(timing.presentation, timing.turnId, useColor, maximumWidth, { expanded: true, screenReader });
|
|
14
|
+
const failureReason = getTuiTurnFailureReason(events);
|
|
15
|
+
const terminationSummary = getTuiTerminationSummary(events, timing?.status);
|
|
16
|
+
if (terminationSummary)
|
|
17
|
+
nodes.push(_jsxs(Text, { children: ["\u7EC8\u6001\uFF1A", terminationSummary] }, "turn:termination"));
|
|
18
|
+
else if (failureReason)
|
|
19
|
+
nodes.push(_jsxs(Text, { children: ["\u539F\u56E0\uFF1A", failureReason] }, "turn:failure-reason"));
|
|
20
|
+
if (timing.totalDurationMs !== undefined)
|
|
21
|
+
nodes.push(_jsxs(Text, { dimColor: useColor, children: ["* Cogitated for ", formatTuiDuration(timing.totalDurationMs)] }, "turn:total-duration"));
|
|
22
|
+
return nodes;
|
|
23
|
+
}
|
|
24
|
+
const nodes = [];
|
|
25
|
+
let answer = "";
|
|
26
|
+
let answerSeq = 0;
|
|
27
|
+
let lastAnswerNodeIndex;
|
|
28
|
+
const submittedApprovalIds = new Set(events.flatMap(function readSubmittedApproval(event) {
|
|
29
|
+
return event.type === "tui_approval_submitted" ? [event.approvalId] : [];
|
|
30
|
+
}));
|
|
31
|
+
function flushAnswer() {
|
|
32
|
+
if (!answer)
|
|
33
|
+
return;
|
|
34
|
+
const isFirstAnswer = answerSeq === 0;
|
|
35
|
+
nodes.push(_jsxs(Box, { flexDirection: "column", children: [isFirstAnswer && timing?.thoughtDurationMs !== undefined ? _jsxs(Text, { dimColor: useColor, children: ["Thought for ", formatTuiDuration(timing.thoughtDurationMs)] }) : null, _jsxs(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: ["\u2022 ", TUI_AGENT_LABEL] }), _jsx(TuiMarkdown, { content: answer, keyPrefix: "answer:" + String(answerSeq), useColor: useColor, maximumWidth: maximumWidth })] }, "answer:" + String(answerSeq++)));
|
|
36
|
+
lastAnswerNodeIndex = nodes.length - 1;
|
|
37
|
+
answer = "";
|
|
38
|
+
}
|
|
39
|
+
/** 收到 Core reset/discard 后移除已经固化的暂定回答,避免每个工具步骤都留下重复报告。 */
|
|
40
|
+
function discardAnswer() {
|
|
41
|
+
answer = "";
|
|
42
|
+
if (lastAnswerNodeIndex === undefined)
|
|
43
|
+
return;
|
|
44
|
+
nodes[lastAnswerNodeIndex] = undefined;
|
|
45
|
+
lastAnswerNodeIndex = undefined;
|
|
46
|
+
answerSeq = Math.max(0, answerSeq - 1);
|
|
47
|
+
}
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
if (event.type === "answer_delta") {
|
|
50
|
+
answer += event.delta;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (event.type === "answer_reset") {
|
|
54
|
+
discardAnswer();
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
flushAnswer();
|
|
58
|
+
if (event.type === "tui_input_consumed") {
|
|
59
|
+
nodes.push(_jsx(Text, { dimColor: useColor, children: formatTuiInputConsumed(event) }, String(event.seq) + ":input:" + event.inputId));
|
|
60
|
+
}
|
|
61
|
+
else if (event.type === "tui_approval_submitted") {
|
|
62
|
+
nodes.push(_jsxs(Text, { dimColor: useColor, children: ["Approval submitted \u00B7 ", getTuiApprovalDecisionLabel(event.decision)] }, String(event.seq) + ":approval:" + event.approvalId));
|
|
63
|
+
}
|
|
64
|
+
else if (event.type === "turn_started") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
else if (event.type === "reasoning_summary") {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
else if (event.type === "reasoning_hidden") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
else if (event.type === "tool_started" || event.type === "tool_result") {
|
|
74
|
+
nodes.push(_jsx(Box, { children: renderTuiToolTreeEvent(event, useColor) }, String(event.seq)));
|
|
75
|
+
}
|
|
76
|
+
else if (event.type === "approval_required") {
|
|
77
|
+
if (submittedApprovalIds.has(event.approvalId))
|
|
78
|
+
continue;
|
|
79
|
+
nodes.push(_jsxs(Box, { borderStyle: "single", borderColor: useColor ? TUI_ACCENT_COLOR : undefined, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: useColor ? TUI_ACCENT_COLOR : undefined, children: "Approval required" }), _jsxs(Text, { children: ["\u64CD\u4F5C\uFF1A", getTuiToolLabel(event.toolName)] }), _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] }), _jsx(Text, { dimColor: useColor, children: "[y] \u672C\u6B21 [s] \u672C\u6B21\u4F1A\u8BDD [w] \u6B64\u5DE5\u4F5C\u533A [n] \u62D2\u7EDD" })] }, String(event.seq)));
|
|
80
|
+
}
|
|
81
|
+
else if (event.type === "input_queued") {
|
|
82
|
+
nodes.push(_jsx(Text, { dimColor: useColor, children: "Additional instruction queued; waiting for the current step" }, String(event.seq)));
|
|
83
|
+
}
|
|
84
|
+
else if (event.type === "todo_updated") {
|
|
85
|
+
// Todo 展示由共享 Presentation 的单卡负责,事件流本身不产生重复的静态行。
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
else if (event.type === "input_joined" || event.type === "input_consumed") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
else if (event.type === "model_recovery") {
|
|
92
|
+
nodes.push(_jsxs(Text, { dimColor: useColor, children: ["Recovering \u00B7 ", event.reason] }, String(event.seq)));
|
|
93
|
+
}
|
|
94
|
+
else if (event.type === "context_overflow") {
|
|
95
|
+
nodes.push(_jsx(Text, { children: "Context limit reached; organizing before continuing" }, String(event.seq)));
|
|
96
|
+
}
|
|
97
|
+
else if (event.type === "context_compact_started") {
|
|
98
|
+
nodes.push(_jsx(Text, { dimColor: useColor, children: "Organizing context" }, String(event.seq)));
|
|
99
|
+
}
|
|
100
|
+
else if (event.type === "context_compacted") {
|
|
101
|
+
nodes.push(_jsx(Text, { children: "Context organized" }, String(event.seq)));
|
|
102
|
+
}
|
|
103
|
+
else if (event.type === "context_compact_failed") {
|
|
104
|
+
nodes.push(_jsx(Text, { children: "Context organization failed" }, String(event.seq)));
|
|
105
|
+
}
|
|
106
|
+
else if (event.type === "context_compact_circuit_open") {
|
|
107
|
+
nodes.push(_jsx(Text, { children: "Context organization temporarily unavailable" }, String(event.seq)));
|
|
108
|
+
}
|
|
109
|
+
else if (event.type === "active_skills") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
else if (event.type === "status") {
|
|
113
|
+
if (event.status === "failed" && event.reason)
|
|
114
|
+
nodes.push(_jsxs(Text, { children: ["\u539F\u56E0\uFF1A", event.reason] }, String(event.seq)));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
flushAnswer();
|
|
118
|
+
if (timing?.totalDurationMs !== undefined)
|
|
119
|
+
nodes.push(_jsxs(Text, { dimColor: useColor, children: ["* Cogitated for ", formatTuiDuration(timing.totalDurationMs)] }, "turn:total-duration"));
|
|
120
|
+
return nodes.filter(function isVisibleTimelineNode(node) { return node !== undefined; });
|
|
121
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 工具进度树节点:父节点是徽标 chip 与参数 chip,子节点是结果行与 unified diff。 */
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { CliTuiTimelineEvent } from "../types.js";
|
|
4
|
+
/** 按进度树展示工具:徽标持久强调,参数 chip 与徽标同行并右端截断,结果行保留 └─ 连线与 ✓/✗ 文字标签。 */
|
|
5
|
+
export declare function renderTuiToolTreeEvent(event: Extract<CliTuiTimelineEvent, {
|
|
6
|
+
type: "tool_started" | "tool_result";
|
|
7
|
+
}>, useColor: boolean): ReactNode;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TUI_ACCENT_BG_COLOR, TUI_ACCENT_COLOR, TUI_CHIP_TEXT_COLOR, TUI_TOOL_ERROR_COLOR, TUI_TOOL_SUCCESS_COLOR } from "../../design-tokens.js";
|
|
4
|
+
import { TuiDiffStats } from "../diff-stats.js";
|
|
5
|
+
import { cleanTuiToolSummary, getTuiDiffStats, getTuiToolLabel, getTuiToolLineParts } from "../format.js";
|
|
6
|
+
import { TuiDiffBlock } from "../markdown/diff.js";
|
|
7
|
+
/** 渲染工具徽标 chip:强调背景 + 深色粗体在活动与完成态持久显示;NO_COLOR 退化为纯方括号文本。 */
|
|
8
|
+
function TuiToolBadge(props) {
|
|
9
|
+
if (!props.useColor)
|
|
10
|
+
return _jsx(Text, { children: props.badge });
|
|
11
|
+
return _jsx(Text, { backgroundColor: TUI_ACCENT_BG_COLOR, color: TUI_CHIP_TEXT_COLOR, bold: true, children: props.badge });
|
|
12
|
+
}
|
|
13
|
+
/** 按进度树展示工具:徽标持久强调,参数 chip 与徽标同行并右端截断,结果行保留 └─ 连线与 ✓/✗ 文字标签。 */
|
|
14
|
+
export function renderTuiToolTreeEvent(event, useColor) {
|
|
15
|
+
if (event.type === "tool_started") {
|
|
16
|
+
const detail = cleanTuiToolSummary(event.toolName, event.summary).trim();
|
|
17
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { wrap: "truncate-end", children: ["├─ ", _jsx(TuiToolBadge, { badge: `[${getTuiToolLabel(event.toolName)}]`, useColor: useColor }), detail ? _jsxs(_Fragment, { children: [" ", _jsxs(Text, { color: useColor ? TUI_ACCENT_COLOR : undefined, children: ["[", detail, "]"] })] }) : null] }) }));
|
|
18
|
+
}
|
|
19
|
+
const diffStats = getTuiDiffStats(event.diff ? { text: event.diff } : undefined);
|
|
20
|
+
const parts = getTuiToolLineParts(event.toolName, event.summary, event.status, event.errorCode);
|
|
21
|
+
if (event.status !== "success") {
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: useColor ? TUI_TOOL_ERROR_COLOR : undefined, children: `└─ ${parts.glyph ? `${parts.glyph} ` : ""}${parts.badge}${parts.rest}` }), event.diff ? _jsx(TuiDiffBlock, { diff: event.diff, keyPrefix: String(event.seq), useColor: useColor }) : null] }));
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["└─ ", _jsx(Text, { color: useColor ? TUI_TOOL_SUCCESS_COLOR : undefined, children: parts.glyph }), " ", _jsx(TuiToolBadge, { badge: parts.badge, useColor: useColor }), parts.rest, diffStats ? _jsxs(_Fragment, { children: [" ", _jsx(TuiDiffStats, { ...diffStats, useColor: useColor })] }) : null] }), event.diff ? _jsx(TuiDiffBlock, { diff: event.diff, keyPrefix: String(event.seq), useColor: useColor }) : null] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** 渲染当前 Turn;它会随安全事件更新,完成后整体进入历史树。 */
|
|
2
|
+
import type { CliProjection, CliTurnStatus } from "../../types.js";
|
|
3
|
+
import type { AgentPresentationState } from "@autopilot-agent/presentation";
|
|
4
|
+
import type { CliTuiTimelineEvent, CliTuiTurnMetrics } from "../types.js";
|
|
5
|
+
/** 渲染当前 Turn;memo 保证输入光标更新不会触发 live timeline 组件执行。 */
|
|
6
|
+
export declare const TuiLiveTurn: import("react").NamedExoticComponent<{
|
|
7
|
+
turnId?: string;
|
|
8
|
+
events: CliTuiTimelineEvent[];
|
|
9
|
+
presentation?: AgentPresentationState;
|
|
10
|
+
pendingApproval?: CliProjection["pendingApproval"];
|
|
11
|
+
approvalSelectedIndex: number;
|
|
12
|
+
status: CliTurnStatus;
|
|
13
|
+
metrics?: CliTuiTurnMetrics;
|
|
14
|
+
useColor: boolean;
|
|
15
|
+
screenReader: boolean;
|
|
16
|
+
todoExpanded: boolean;
|
|
17
|
+
todoRowLimit?: number;
|
|
18
|
+
maximumWidth: number;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** 渲染当前 Turn;它会随安全事件更新,完成后整体进入历史树。 */
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { TuiApprovalPrompt } from "../approval.js";
|
|
6
|
+
import { formatTuiDuration, getTuiTerminationSummary, getTuiTurnFailureReason } from "../format.js";
|
|
7
|
+
import { TuiLiveTimeline } from "./live.js";
|
|
8
|
+
import { renderTuiPresentationTimeline } from "./presentation-timeline.js";
|
|
9
|
+
import { TuiLiveProgressSummary } from "./progress.js";
|
|
10
|
+
import { getTuiTodoActiveForm } from "../todo-list.js";
|
|
11
|
+
/** 渲染当前 Turn;memo 保证输入光标更新不会触发 live timeline 组件执行。 */
|
|
12
|
+
export const TuiLiveTurn = memo(function TuiLiveTurn(props) {
|
|
13
|
+
if (!props.turnId || props.events.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const hasPresentation = props.presentation?.items.some(function belongsToTurn(item) { return item.kind === "capability_notice" || ("turnId" in item && item.turnId === props.turnId); }) ?? false;
|
|
16
|
+
const failureReason = getTuiTurnFailureReason(props.events);
|
|
17
|
+
const terminationSummary = getTuiTerminationSummary(props.events, props.status);
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TuiLiveProgressSummary, { events: props.events, status: props.status, metrics: props.metrics, useColor: props.useColor, activeTodoForm: getTuiTodoActiveForm(props.presentation, props.turnId) }), props.presentation && hasPresentation
|
|
19
|
+
? renderTuiPresentationTimeline(props.presentation, props.turnId, props.useColor, props.maximumWidth, { expanded: props.todoExpanded, screenReader: props.screenReader, rowLimit: props.todoRowLimit, showToggleHint: true, toolGroupsExpanded: props.todoExpanded })
|
|
20
|
+
: _jsx(TuiLiveTimeline, { turnId: props.turnId, events: props.events, status: props.status, metrics: props.metrics, useColor: props.useColor, maximumWidth: props.maximumWidth }), terminationSummary ? _jsxs(Text, { children: ["\u7EC8\u6001\uFF1A", terminationSummary] }) : hasPresentation && failureReason ? _jsxs(Text, { children: ["\u539F\u56E0\uFF1A", failureReason] }) : null, props.pendingApproval ? _jsx(TuiApprovalPrompt, { pendingApproval: props.pendingApproval, selectedIndex: props.approvalSelectedIndex, useColor: props.useColor, screenReader: props.screenReader }) : null, props.metrics?.completedAt !== undefined ? _jsxs(Text, { dimColor: props.useColor, children: ["* Cogitated for ", formatTuiDuration(Math.max(0, props.metrics.completedAt - props.metrics.startedAt))] }) : null] }));
|
|
21
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Todo 任务链的 CLI 视图:紧凑/展开两种形态共用同一份结构化行数据,
|
|
3
|
+
* Ink 动态区与备用屏视口都从这里取行,避免两套展示实现。
|
|
4
|
+
*/
|
|
5
|
+
import { type ReactNode } from "react";
|
|
6
|
+
import type { TodoItem } from "@autopilot-agent/core";
|
|
7
|
+
import { type AgentPresentationState, type TodoSnapshotItem } from "@autopilot-agent/presentation";
|
|
8
|
+
/** Todo 行的状态与安全文本;样式由 Ink 或视口 spans 分别映射,hidden 行表示被截断的隐藏计数。 */
|
|
9
|
+
export interface TuiTodoDisplayRow {
|
|
10
|
+
status: TodoItem["status"];
|
|
11
|
+
text: string;
|
|
12
|
+
hidden?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Todo 视图的完整展示数据;紧凑态只有 header,展开态附任务行与隐藏计数。 */
|
|
15
|
+
export interface TuiTodoDisplay {
|
|
16
|
+
header: string;
|
|
17
|
+
activeForm?: string;
|
|
18
|
+
rows: TuiTodoDisplayRow[];
|
|
19
|
+
hidden: number;
|
|
20
|
+
}
|
|
21
|
+
/** 取得 Todo 状态的中文文字标签,供 screen reader 与 NO_COLOR 场景朗读。 */
|
|
22
|
+
export declare function getTuiTodoStatusLabel(status: TodoItem["status"]): string;
|
|
23
|
+
/** 从共享 Presentation 读取当前 Turn 任务链的 in_progress activeForm,作为活动状态的稳定文案。 */
|
|
24
|
+
export declare function getTuiTodoActiveForm(presentation: AgentPresentationState | undefined, turnId: string): string | undefined;
|
|
25
|
+
/** 取得 Todo 状态的纯文本 glyph,颜色不是唯一状态载体。 */
|
|
26
|
+
export declare function getTuiTodoStatusGlyph(status: TodoItem["status"]): string;
|
|
27
|
+
/** 按显示宽度做 grapheme 安全截断,中英文与组合 emoji 不会截出半字。 */
|
|
28
|
+
export declare function truncateTuiTodoText(text: string, maximumWidth: number): string;
|
|
29
|
+
/** 按优先级(in_progress → 最近完成 → pending)在行数上限内挑选可见任务,保持原清单顺序展示。 */
|
|
30
|
+
export declare function orderTuiTodoItems(todos: TodoItem[], limit?: number): {
|
|
31
|
+
shown: TodoItem[];
|
|
32
|
+
hidden: number;
|
|
33
|
+
};
|
|
34
|
+
/** 生成 Todo 视图的共享展示数据;紧凑态一行,展开态为 header + 任务行 + 隐藏计数。 */
|
|
35
|
+
export declare function getTuiTodoDisplay(item: TodoSnapshotItem, options: {
|
|
36
|
+
expanded: boolean;
|
|
37
|
+
screenReader: boolean;
|
|
38
|
+
columns: number;
|
|
39
|
+
rowLimit?: number;
|
|
40
|
+
}): TuiTodoDisplay | undefined;
|
|
41
|
+
/** 渲染 Todo 任务链的 Ink 视图;screen reader 时每行前缀中文状态文字。 */
|
|
42
|
+
export declare function TuiTodoList(props: {
|
|
43
|
+
item: TodoSnapshotItem;
|
|
44
|
+
expanded: boolean;
|
|
45
|
+
useColor: boolean;
|
|
46
|
+
screenReader: boolean;
|
|
47
|
+
columns: number;
|
|
48
|
+
rowLimit?: number;
|
|
49
|
+
showToggleHint?: boolean;
|
|
50
|
+
}): ReactNode;
|