minimal-agent 0.5.4 → 0.5.6
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/package.json +1 -1
- package/src/context/archive.js +117 -0
- package/src/context/persistContext.js +33 -1
- package/src/context/sessionPath.js +59 -4
- package/src/context/sessionRegistry.js +104 -0
- package/src/llm/client.js +16 -2
- package/src/loop.js +191 -35
- package/src/prompts/system.js +16 -16
- package/src/tools/read/read.js +5 -1
- package/src/tools/shared/fileState.js +23 -0
- package/src/ui/App.js +155 -11
- package/src/ui/InputBox.js +122 -11
- package/src/ui/LiveArea.js +132 -0
- package/src/ui/MessageList.js +213 -12
- package/src/ui/ProgressPanel.js +54 -0
- package/src/ui/StatusLine.js +21 -2
- package/src/ui/StreamingBlock.js +11 -0
- package/src/ui/ToolStatus.js +2 -2
- package/src/ui/hooks/useChat.js +407 -105
- package/src/ui/hooks/useInputHistory.js +186 -0
- package/src/ui/hooks/useTerminalSize.js +31 -0
- package/src/ui/hooks/useTokenUsage.js +0 -1
- package/src/ui/liveViewport.js +176 -0
package/src/ui/MessageList.js
CHANGED
|
@@ -1,22 +1,222 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================
|
|
4
|
+
* src/ui/MessageList.tsx —— 消息历史展示
|
|
5
|
+
* ------------------------------------------------------------
|
|
6
|
+
* 渲染历史消息(system 不显示)。
|
|
7
|
+
* 使用 Ink 5.x 的 <Static> commit 历史到终端 scrollback:
|
|
8
|
+
* 鼠标滚轮可以自然翻阅,性能也更好(已 commit 的行不会再 rerender)。
|
|
9
|
+
*
|
|
10
|
+
* 流式中的 assistant 文本不再在这里"假消息"显示——
|
|
11
|
+
* streaming 阶段不渲染 live answer,turn 结束后再以正式的
|
|
12
|
+
* assistant Message 进入 history 并被 <Static> commit。
|
|
13
|
+
*
|
|
14
|
+
* banner 也走 Static 流(items[0])。
|
|
15
|
+
* Ink 5 的 Static 约定要求 Static 是兄弟节点中第一个,否则它上方的 live
|
|
16
|
+
* 兄弟会跟 Static commit 一起被重打到 stdout(用户实测看到 banner 被
|
|
17
|
+
* 反复打印 N 次)。把 banner 作为 Static 的第一个 item,banner 自然
|
|
18
|
+
* commit 进 scrollback,启动时打印一次,不再反复出现。
|
|
19
|
+
*
|
|
20
|
+
* v2 新增:
|
|
21
|
+
* - assistant 分支渲染 reasoning_content / reasoning(💭 dim gray);
|
|
22
|
+
* 窄终端(rows<28)折叠 2 行 + "▾ N 行 reasoning 折叠"
|
|
23
|
+
* - tool 分支显示 tool_call_id 末 8 字符作为 [tool result · #abc12345],
|
|
24
|
+
* "▾ 完整 N 行(输入 /show xxx 查看)" 提示用户用命令展开
|
|
25
|
+
* - user 分支前插 turn 头横线 "────── Turn N · 14:32 · 8s · 6 tools ──────",
|
|
26
|
+
* turn 边界扫描结果由 useMemo 一次性算出,挂在 StaticItem 上
|
|
27
|
+
*
|
|
28
|
+
* 样式约定:
|
|
29
|
+
* banner: 青色加粗 minimal-agent + 暗灰色 provider/快捷键提示
|
|
30
|
+
* user: 整条青色 + "> " 前缀(输入;前置 turn 横线,若 turnMeta 非空)
|
|
31
|
+
* assistant: reasoning 暗灰带 💭,content 绿色 ⏺ 锚点 + 默认亮色正文(输出),tool_calls 黄色
|
|
32
|
+
* tool: 暗灰色,折叠(默认只显示前 3 行,末尾提示用 /show 查看)
|
|
33
|
+
* ============================================================
|
|
34
|
+
*/
|
|
35
|
+
import React from 'react';
|
|
36
|
+
import { Box, Static, Text } from 'ink';
|
|
37
|
+
/** 中断标记 user message 的固定内容(loop.ts 写入) */
|
|
38
|
+
const INTERRUPT_USER_CONTENT = '(用户按下了 ESC/Ctrl+C 中断了任务)';
|
|
3
39
|
const MAX_TOOL_PREVIEW_LINES = 3;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
40
|
+
/** 窄终端 reasoning 折叠阈值(行)—— 小于此值时 reasoning 只显示首 2 行 */
|
|
41
|
+
const NARROW_TERMINAL_ROWS = 28;
|
|
42
|
+
/** reasoning 超过此字符数截尾 —— 防撑爆历史 */
|
|
43
|
+
const REASONING_MAX_CHARS = 2000;
|
|
44
|
+
/**
|
|
45
|
+
* 把 assistant message 的两个字符串型 reasoning 字段拼成单字符串。
|
|
46
|
+
* `reasoning_details` 是结构化数组,本期不展开。两个字段任一非空即返回拼接结果;
|
|
47
|
+
* 都空返回 ''。
|
|
48
|
+
*
|
|
49
|
+
* 导出供单测断言。
|
|
50
|
+
*/
|
|
51
|
+
export function combineReasoning(message) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
if (typeof message.reasoning_content === 'string' &&
|
|
54
|
+
message.reasoning_content.length > 0) {
|
|
55
|
+
parts.push(message.reasoning_content);
|
|
56
|
+
}
|
|
57
|
+
if (typeof message.reasoning === 'string' && message.reasoning.length > 0) {
|
|
58
|
+
parts.push(message.reasoning);
|
|
59
|
+
}
|
|
60
|
+
return parts.join('\n');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 扫一遍 history,按 user message 切分 turn,返回每条 message 对应的 turnMeta。
|
|
64
|
+
* 返回数组长度 = history.length,与 history 一一对应:
|
|
65
|
+
* - user message(非中断)→ 该 turn 的 TurnMeta(turnN 递增,>=1)
|
|
66
|
+
* - 中断标记 user / assistant / tool / system → null
|
|
67
|
+
*
|
|
68
|
+
* 算法:
|
|
69
|
+
* 1) 先识别"turn 起点":所有非中断 user message
|
|
70
|
+
* 2) 每个 turn 起点 turnN 从 1 递增
|
|
71
|
+
* 3) turn 结束位置 = 下一个 turn 起点之前(或 history 末尾)
|
|
72
|
+
* 4) durationMs = turn 内最后一条 assistant/tool 的 timestamp - user.timestamp
|
|
73
|
+
* 5) toolsCount = turn 内所有 assistant.tool_calls?.length 累加
|
|
74
|
+
*
|
|
75
|
+
* 导出供单测断言。
|
|
76
|
+
*/
|
|
77
|
+
export function summarizeTurn(history) {
|
|
78
|
+
const result = new Array(history.length).fill(null);
|
|
79
|
+
// 找出所有 turn 起点的 index
|
|
80
|
+
const turnStartIndices = [];
|
|
81
|
+
for (let i = 0; i < history.length; i++) {
|
|
82
|
+
const m = history[i];
|
|
83
|
+
if (m.role === 'user' && m.content !== INTERRUPT_USER_CONTENT) {
|
|
84
|
+
turnStartIndices.push(i);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (let t = 0; t < turnStartIndices.length; t++) {
|
|
88
|
+
const startIdx = turnStartIndices[t];
|
|
89
|
+
const endIdx = t + 1 < turnStartIndices.length
|
|
90
|
+
? turnStartIndices[t + 1] - 1
|
|
91
|
+
: history.length - 1;
|
|
92
|
+
const startMsg = history[startIdx];
|
|
93
|
+
const userTimestamp = startMsg.timestamp ?? Date.now();
|
|
94
|
+
let lastResponseTs = null;
|
|
95
|
+
let toolsCount = 0;
|
|
96
|
+
for (let j = startIdx + 1; j <= endIdx; j++) {
|
|
97
|
+
const m = history[j];
|
|
98
|
+
if (m.role === 'assistant') {
|
|
99
|
+
toolsCount += m.tool_calls?.length ?? 0;
|
|
100
|
+
if (typeof m.timestamp === 'number')
|
|
101
|
+
lastResponseTs = m.timestamp;
|
|
102
|
+
}
|
|
103
|
+
else if (m.role === 'tool') {
|
|
104
|
+
if (typeof m.timestamp === 'number')
|
|
105
|
+
lastResponseTs = m.timestamp;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const durationMs = lastResponseTs !== null ? Math.max(0, lastResponseTs - userTimestamp) : 0;
|
|
109
|
+
result[startIdx] = {
|
|
110
|
+
turnN: t + 1,
|
|
111
|
+
durationMs,
|
|
112
|
+
toolsCount,
|
|
113
|
+
timestamp: userTimestamp,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
8
117
|
}
|
|
9
|
-
|
|
118
|
+
/** 格式化 timestamp 为 HH:MM(本地时区) */
|
|
119
|
+
function formatTime(ts) {
|
|
120
|
+
return new Date(ts).toTimeString().slice(0, 5);
|
|
121
|
+
}
|
|
122
|
+
/** 格式化耗时:<1s / 1.2s / 3m */
|
|
123
|
+
function formatDuration(ms) {
|
|
124
|
+
if (ms < 1000)
|
|
125
|
+
return '<1s';
|
|
126
|
+
if (ms < 60_000)
|
|
127
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
128
|
+
if (ms < 3_600_000)
|
|
129
|
+
return `${Math.floor(ms / 60_000)}m`;
|
|
130
|
+
return `${Math.floor(ms / 3_600_000)}h`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* React.memo 包装。
|
|
134
|
+
*
|
|
135
|
+
* props 浅比较够用:
|
|
136
|
+
* - history 引用变化才需要重算 items(useMemo 依赖正确)
|
|
137
|
+
* - compactGeneration 数字
|
|
138
|
+
* - bannerText / terminalRows 都是基础类型 / 启动后稳定
|
|
139
|
+
*
|
|
140
|
+
* App.tsx 中用 useDeferredValue(chat.history) 把 history 推后到低优先级队列,
|
|
141
|
+
* 配合此 memo,streaming 期间高频 setStreamingText 不再带着 MessageList
|
|
142
|
+
* 重算 items + Static reconcile。
|
|
143
|
+
*/
|
|
144
|
+
export const MessageList = React.memo(function MessageList({ history, compactGeneration, bannerText, terminalRows, }) {
|
|
145
|
+
const items = React.useMemo(() => {
|
|
146
|
+
const turnMetas = summarizeTurn(history);
|
|
147
|
+
return [
|
|
148
|
+
...(bannerText
|
|
149
|
+
? [
|
|
150
|
+
{
|
|
151
|
+
kind: 'banner',
|
|
152
|
+
text: bannerText,
|
|
153
|
+
key: `banner-${bannerText.slice(0, 10)}`,
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
: []),
|
|
157
|
+
...history.map((m, idx) => ({
|
|
158
|
+
kind: 'message',
|
|
159
|
+
msg: m,
|
|
160
|
+
turnMeta: turnMetas[idx],
|
|
161
|
+
// 纯派生 key:有 id 用 id,没有就用 idx 兜底。绝不在 render 期 mutate 消息对象
|
|
162
|
+
// (<Static> 已 commit 的行永不重绘,key 仅在 append 对账时用,idx 已足够稳定)。
|
|
163
|
+
key: m.id ?? `message-${idx}`,
|
|
164
|
+
})),
|
|
165
|
+
];
|
|
166
|
+
}, [bannerText, history]);
|
|
167
|
+
// 窄终端判定(terminalRows 缺失时默认不折叠,保留完整 reasoning)
|
|
168
|
+
const foldReasoning = typeof terminalRows === 'number' && terminalRows < NARROW_TERMINAL_ROWS;
|
|
169
|
+
return (_jsx(Static, { items: items, children: (item) => {
|
|
170
|
+
if (item.kind === 'banner') {
|
|
171
|
+
return _jsx(BannerRow, { text: item.text }, item.key);
|
|
172
|
+
}
|
|
173
|
+
return (_jsx(MessageRow, { message: item.msg, turnMeta: item.turnMeta, foldReasoning: foldReasoning }, item.key));
|
|
174
|
+
} }, compactGeneration));
|
|
175
|
+
});
|
|
176
|
+
function BannerRow({ text }) {
|
|
177
|
+
// text 形如 "minimal-agent · provider/model · /new ... · Ctrl+C 中断"
|
|
178
|
+
// 拆出第一个 token "minimal-agent" 走加粗青色,剩下走暗灰。
|
|
179
|
+
const splitAt = text.indexOf(' ·');
|
|
180
|
+
const head = splitAt >= 0 ? text.slice(0, splitAt) : text;
|
|
181
|
+
const tail = splitAt >= 0 ? text.slice(splitAt) : '';
|
|
182
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: head }), tail.length > 0 && _jsx(Text, { color: "gray", children: tail })] }));
|
|
183
|
+
}
|
|
184
|
+
function MessageRow({ message, turnMeta, foldReasoning, }) {
|
|
10
185
|
switch (message.role) {
|
|
11
|
-
case 'system':
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
186
|
+
case 'system': {
|
|
187
|
+
// v2.x:ephemeral system message 是 UI 临时提示(slash 命令输出等),
|
|
188
|
+
// 用暗灰单行风格显示;正经 system prompt(_ephemeral=undefined)依然 hide。
|
|
189
|
+
const ephemeral = message._ephemeral === true;
|
|
190
|
+
if (!ephemeral)
|
|
191
|
+
return null;
|
|
192
|
+
const lines = message.content.split('\n');
|
|
193
|
+
return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: "gray", dimColor: true, children: line }, `eph-${i}`))) }) }));
|
|
194
|
+
}
|
|
195
|
+
case 'user': {
|
|
196
|
+
const headerLine = turnMeta !== null
|
|
197
|
+
? `────── Turn ${turnMeta.turnN} · ${formatTime(turnMeta.timestamp)} · ${formatDuration(turnMeta.durationMs)} · ${turnMeta.toolsCount} tools ──────`
|
|
198
|
+
: null;
|
|
199
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [headerLine !== null && (_jsx(Text, { color: "gray", dimColor: true, children: headerLine })), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: '> ' }), _jsx(Text, { color: "cyan", children: message.content })] })] }));
|
|
200
|
+
}
|
|
15
201
|
case 'assistant': {
|
|
16
202
|
// assistant 可能只有 tool_calls 没有 content(content 为 null)
|
|
17
203
|
const text = message.content ?? '';
|
|
18
204
|
const tcCount = message.tool_calls?.length ?? 0;
|
|
19
|
-
|
|
205
|
+
// reasoning 拼接
|
|
206
|
+
let reasoningText = combineReasoning(message);
|
|
207
|
+
let truncatedCharsHint = '';
|
|
208
|
+
if (reasoningText.length > REASONING_MAX_CHARS) {
|
|
209
|
+
const omitted = reasoningText.length - REASONING_MAX_CHARS;
|
|
210
|
+
reasoningText = reasoningText.slice(0, REASONING_MAX_CHARS);
|
|
211
|
+
truncatedCharsHint = `...(${omitted} 字省略)`;
|
|
212
|
+
}
|
|
213
|
+
const reasoningLines = reasoningText.length > 0 ? reasoningText.split('\n') : [];
|
|
214
|
+
const visibleReasoningLines = foldReasoning
|
|
215
|
+
? reasoningLines.slice(0, 2)
|
|
216
|
+
: reasoningLines;
|
|
217
|
+
const hiddenReasoningCount = reasoningLines.length - visibleReasoningLines.length;
|
|
218
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [visibleReasoningLines.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [visibleReasoningLines.map((line, i) => (_jsx(Text, { color: "gray", dimColor: true, children: i === 0 ? `💭 ${line}` : ` ${line}` }, `r-${i}`))), hiddenReasoningCount > 0 && (_jsx(Text, { color: "gray", dimColor: true, children: ` ▾ ${hiddenReasoningCount} 行 reasoning 折叠(终端高度 < ${NARROW_TERMINAL_ROWS})` })), truncatedCharsHint.length > 0 && (_jsx(Text, { color: "gray", dimColor: true, children: ` ${truncatedCharsHint}` }))] })), text.length > 0 &&
|
|
219
|
+
text.split('\n').map((line, i) => i === 0 ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: '⏺ ' }), _jsx(Text, { children: line.length > 0 ? line : ' ' })] }, `a-${i}`)) : (_jsx(Text, { children: ` ${line}` }, `a-${i}`))), tcCount > 0 && (_jsx(Text, { color: "yellow", dimColor: true, children: ` └─ 调用了 ${tcCount} 个工具:${(message.tool_calls ?? [])
|
|
20
220
|
.map((tc) => tc.function.name)
|
|
21
221
|
.join(', ')}` }))] }));
|
|
22
222
|
}
|
|
@@ -24,7 +224,8 @@ function MessageRow({ message }) {
|
|
|
24
224
|
const lines = message.content.split('\n');
|
|
25
225
|
const preview = lines.slice(0, MAX_TOOL_PREVIEW_LINES).join('\n');
|
|
26
226
|
const hidden = Math.max(0, lines.length - MAX_TOOL_PREVIEW_LINES);
|
|
27
|
-
|
|
227
|
+
const shortId = message.tool_call_id.slice(-8);
|
|
228
|
+
return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", dimColor: true, children: `[tool result · #${shortId}]` }), _jsx(Text, { color: "gray", children: preview }), hidden > 0 && (_jsx(Text, { color: "gray", dimColor: true, children: `... ▾ 完整 ${lines.length} 行(输入 /show ${shortId} 查看)` }))] }) }));
|
|
28
229
|
}
|
|
29
230
|
}
|
|
30
231
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/** 阶段标签中文映射 */
|
|
4
|
+
const STAGE_LABELS = {
|
|
5
|
+
thinking: '思考中',
|
|
6
|
+
streaming: '回答中',
|
|
7
|
+
tool_executing: '调用工具',
|
|
8
|
+
compacting: '压缩历史',
|
|
9
|
+
};
|
|
10
|
+
/** 工具参数预览单行最大宽度(fallback 时用) */
|
|
11
|
+
const TOOL_ARGS_MAX = 60;
|
|
12
|
+
/** 按字符宽度截断,超长尾部加 …(按字符而非 byte,够用) */
|
|
13
|
+
function truncateTail(s, max) {
|
|
14
|
+
if (s.length <= max)
|
|
15
|
+
return s;
|
|
16
|
+
return `${s.slice(0, max - 1)}…`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 静态指示符:不使用动画 spinner(setState+setInterval 会每 120ms 触发整棵树 rerender,
|
|
20
|
+
* 导致 StreamingBlock / InputBox 等兄弟组件频繁重绘 → 终端追加新行而非原地刷新)。
|
|
21
|
+
* 用固定字符代替,stage_change 事件本身已足够表达"正在工作"。
|
|
22
|
+
*/
|
|
23
|
+
function useSpinnerFrame() {
|
|
24
|
+
return '◉';
|
|
25
|
+
}
|
|
26
|
+
export function ProgressPanel({ progressStage, runningTools, turnTokens, turnElapsedMs, turnStep, pluginProgress, }) {
|
|
27
|
+
// spinner hook 必须无条件调用(React Hooks 规则),早 return 放在 hook 之后
|
|
28
|
+
const spinner = useSpinnerFrame();
|
|
29
|
+
// idle 时整个面板不渲染
|
|
30
|
+
if (progressStage === 'idle') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const stageLabel = STAGE_LABELS[progressStage];
|
|
34
|
+
const elapsedSec = (turnElapsedMs / 1000).toFixed(1);
|
|
35
|
+
const tokensStr = turnTokens.toLocaleString();
|
|
36
|
+
// 头行的"主描述":有工具用 friendly,无工具用 stage label
|
|
37
|
+
let mainDescription;
|
|
38
|
+
if (runningTools.length > 0) {
|
|
39
|
+
const first = runningTools[0];
|
|
40
|
+
mainDescription =
|
|
41
|
+
first.argsFriendly && first.argsFriendly.length > 0
|
|
42
|
+
? first.argsFriendly
|
|
43
|
+
: `${first.toolName}(${truncateTail(first.argsPreview, TOOL_ARGS_MAX)})`;
|
|
44
|
+
if (runningTools.length > 1) {
|
|
45
|
+
mainDescription += ` +${runningTools.length - 1} more`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
mainDescription = stageLabel;
|
|
50
|
+
}
|
|
51
|
+
// 头行尾部的附加信息:耗时 / token / step
|
|
52
|
+
const stepHint = typeof turnStep === 'number' && turnStep > 0 ? ` · step ${turnStep}` : '';
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: [spinner, " "] }), _jsx(Text, { bold: true, children: mainDescription }), _jsx(Text, { color: "gray", children: ` · ${elapsedSec}s · ~${tokensStr} tokens${stepHint}` })] }), pluginProgress && (_jsx(Box, { children: _jsx(Text, { color: "magenta", children: `[plugin:${pluginProgress.pluginId}] ${pluginProgress.current}/${pluginProgress.max ?? '?'}${pluginProgress.message ? ` ${pluginProgress.message}` : ''}` }) }))] }));
|
|
54
|
+
}
|
package/src/ui/StatusLine.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================
|
|
4
|
+
* src/ui/StatusLine.tsx —— 底部状态栏
|
|
5
|
+
* ------------------------------------------------------------
|
|
6
|
+
* 显示:
|
|
7
|
+
* cwd ~/proj · {provider}/{model} · tokens 12.3K / 200K · 距压缩 7.7K
|
|
8
|
+
* 接近压缩阈值时 tokens / 距压缩 变黄;超过阈值时变红。
|
|
9
|
+
* ============================================================
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
2
12
|
import { Box, Text } from 'ink';
|
|
3
13
|
import { homedir } from 'node:os';
|
|
4
14
|
import { sep } from 'node:path';
|
|
5
15
|
import { getWorkingDir } from '../bootstrap/workingDir.js';
|
|
6
16
|
import { useTokenUsage } from './hooks/useTokenUsage.js';
|
|
7
|
-
|
|
17
|
+
/**
|
|
18
|
+
* React.memo 包装。
|
|
19
|
+
*
|
|
20
|
+
* props: provider 引用启动后稳定;history 引用变化(useMemo slice)但内容变化
|
|
21
|
+
* 才需要重算 useTokenUsage —— React.memo 浅比较 history 引用就够了(slice 后引用
|
|
22
|
+
* 变了说明真有新消息);pluginProgress 仅插件循环时更新。
|
|
23
|
+
*
|
|
24
|
+
* 流式 streamingText 高频更新不影响 history 引用,StatusLine 不会被带着 rerender。
|
|
25
|
+
*/
|
|
26
|
+
export const StatusLine = React.memo(function StatusLine({ provider, history, pluginProgress, }) {
|
|
8
27
|
const usage = useTokenUsage(history, provider);
|
|
9
28
|
const ratio = usage.tokens / usage.threshold;
|
|
10
29
|
const color = ratio >= 1 ? 'red' : ratio >= 0.7 ? 'yellow' : 'green';
|
|
11
30
|
const cwdDisplay = shortenPath(getWorkingDir());
|
|
12
31
|
return (_jsxs(Box, { children: [pluginProgress && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "magenta", children: ["\uD83D\uDD04 ", pluginProgress.pluginId, " "] }), _jsxs(Text, { color: "magenta", children: [pluginProgress.current, "/", pluginProgress.max] }), _jsx(Text, { color: "gray", children: ' · ' })] })), _jsx(Text, { color: "cyan", children: "cwd " }), _jsx(Text, { color: "gray", children: cwdDisplay }), _jsx(Text, { color: "gray", children: ' · ' }), _jsxs(Text, { color: "gray", children: [provider.name, "/", provider.model] }), _jsx(Text, { color: "gray", children: ' · ' }), _jsxs(Text, { color: color, children: ["tokens ", fmt(usage.tokens), " / ", fmt(usage.contextWindow)] }), _jsx(Text, { color: "gray", children: ' · ' }), _jsx(Text, { color: color, children: usage.toThreshold > 0 ? `距压缩 ${fmt(usage.toThreshold)}` : `已超阈值(下轮压缩)` })] }));
|
|
13
|
-
}
|
|
32
|
+
});
|
|
14
33
|
/** 把整数格式化为 1.2K / 12.3K / 1.5M */
|
|
15
34
|
function fmt(n) {
|
|
16
35
|
if (n < 1000)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/** streaming text 显示尾部行数上限(超出截尾,老内容滚出窗口) */
|
|
4
|
+
const STREAMING_TAIL_LINES = 20;
|
|
5
|
+
export function StreamingBlock({ progressStage, streamingText, }) {
|
|
6
|
+
if (progressStage !== 'streaming' || streamingText.length === 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const lines = streamingText.split('\n').slice(-STREAMING_TAIL_LINES);
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [lines.map((line, i) => (_jsx(Text, { children: line }, i))), _jsx(Text, { color: "gray", dimColor: true, children: "\u258D" })] }));
|
|
11
|
+
}
|
package/src/ui/ToolStatus.js
CHANGED
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
export function ToolStatus({ status, compacting }) {
|
|
4
4
|
if (compacting) {
|
|
5
|
-
return (_jsx(Box, { children: _jsx(Text, { color: "magenta", bold: true, children: "\uD83D\
|
|
5
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "magenta", bold: true, children: "\uD83D\uDF18 \u6B63\u5728\u538B\u7F29\u5BF9\u8BDD\u5386\u53F2..." }) }));
|
|
6
6
|
}
|
|
7
7
|
if (status) {
|
|
8
|
-
return (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [
|
|
8
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u23F3 ", _jsx(Text, { bold: true, children: status.toolName }), "(", status.argsPreview, ")"] }) }));
|
|
9
9
|
}
|
|
10
10
|
return null;
|
|
11
11
|
}
|