minimal-agent 0.5.5 → 0.6.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.
@@ -24,7 +24,6 @@ export function useTokenUsage(messages, provider) {
24
24
  tokens,
25
25
  threshold,
26
26
  contextWindow: provider.contextWindow,
27
- percentageOfWindow: tokens / provider.contextWindow,
28
27
  toThreshold: threshold - tokens,
29
28
  };
30
29
  }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * ============================================================
3
+ * src/ui/liveViewport.ts —— LiveArea 有界多行 live 区的宽度安全工具(纯函数)
4
+ * ------------------------------------------------------------
5
+ * ★ 两条硬不变量(根治 Windows 终端"思维链刷屏",缺一不可):
6
+ *
7
+ * Ink 默认 render 在主屏 scrollback 上用 log-update 维护**底部 live 区**:
8
+ * 每帧 `eraseLines(上一帧 \n 数)`(光标上移逐行清)再重写。两个坑都会让上移清除"漏擦":
9
+ *
10
+ * ┌─ INV-1 每行物理宽度 < 终端宽 ──────────────────────────────┐
11
+ * │ stock ink `log-update.js` 用 `output.split('\n').length` 记账, │
12
+ * │ **完全不补偿满宽行被终端自动折行**。若某行物理宽触到终端最后一列, │
13
+ * │ 终端自己折一行 → 实际物理行 > ink 记的 \n 数 → eraseLines 擦少 → │
14
+ * │ 它上面的行每帧 strand 进 scrollback = 刷屏。 │
15
+ * │ ∴ live 区每一行都按**我们自己的** displayWidth(CJK/全角/emoji=2, │
16
+ * │ 宁宽不窄)预钳到 ≤ cols - LIVE_RIGHT_MARGIN 再交给 ink。不依赖 │
17
+ * │ ink 的字宽测量 → 终端永不折行 → \n 记账 == 物理行 → 精确擦除。 │
18
+ * └────────────────────────────────────────────────────────┘
19
+ * ┌─ INV-2 live 区总高度 < 终端行数 ──────────────────────────┐
20
+ * │ `ink.js` onRender:一旦 outputHeight >= stdout.rows 会**全屏 │
21
+ * │ clearTerminal 重写**(含整个 static),灾难性。∴ 流式内容预览必须 │
22
+ * │ 有界(取尾部最近 N 行,N 自适应终端高度,见 LiveArea.contentMaxLines)。│
23
+ * └────────────────────────────────────────────────────────┘
24
+ *
25
+ * 完整思维链 / 完整答案 / 完整工具结果一律走 `<Static>`(write-once,永不
26
+ * 参与 log-update → 天然 strand-proof、高度不受限)。live 区只放**有界**的过程预览。
27
+ *
28
+ * ★ 中文/emoji 宽度(关键,别退化成按码点切):
29
+ * CJK / 全角 / emoji 1 码点 = 2 列。宽度必须按 charWidth 累加,不能按 length,
30
+ * 否则这些字符实际占 2× 列宽被算窄、钳不住 → 终端折行 → INV-1 失守、刷屏复发。
31
+ * ============================================================
32
+ */
33
+ /** live 行右侧安全余量(列):钳到 cols - 此值,吃掉光标在"恰好满宽"处的自动折行边界。 */
34
+ export const LIVE_RIGHT_MARGIN = 2;
35
+ /** 剥离 ANSI/CSI 控制序列(含 ESC 引导符)。流式文本理论上没有,做防御性清理。 */
36
+ const ANSI_RE = new RegExp('\\u001b\\[[0-9;?]*[ -/]*[@-~]', 'g');
37
+ /**
38
+ * 单个 Unicode 码点的终端显示宽度:CJK / 全角 / emoji = 2,其余 = 1。
39
+ *
40
+ * 设计目标是做终端实际宽度的**上界估计**(宁可算宽、不可算窄):算宽只会让行
41
+ * 提前一两列截断(无害),算窄则会让钳制失效、终端折行(= 刷屏复发,INV-1 失守)。
42
+ *
43
+ * 覆盖:
44
+ * - 常见 East Asian Ambiguous 标点(…—–·“”‘’ 等):CJK 终端按全角(2)渲染,
45
+ * 这些是中文文本里最容易把行顶宽的"隐形 2 列"字符,统一按 2 计。
46
+ * - Emoji / 符号 / dingbat(💭✶ 等):终端普遍按全角(2)渲染。注意 charWidth 是
47
+ * 逐码点求和,ZWJ/变体序列会被算成各组件之和(偏大 = 安全方向)。
48
+ * - Hangul Jamo / CJK 部首笔画符号标点 / 假名 / CJK 统一 / 谚文音节 /
49
+ * 兼容表意 / 竖排标点 / 全角 ASCII 与符号 / CJK 扩展 B+(星平面)。
50
+ */
51
+ export function charWidth(cp) {
52
+ // 常见 ambiguous-width 标点:CJK 终端按 2 列渲染,统一上界为 2 防折行
53
+ if (cp === 0x00b7 || // ·
54
+ cp === 0x2013 || // –
55
+ cp === 0x2014 || // —
56
+ cp === 0x2018 || // ‘
57
+ cp === 0x2019 || // ’
58
+ cp === 0x201c || // “
59
+ cp === 0x201d || // ”
60
+ cp === 0x2026 || // …
61
+ cp === 0x203b // ※
62
+ ) {
63
+ return 2;
64
+ }
65
+ // Emoji / 符号 / dingbat:终端普遍全角渲染(含 💭 与 spinner ✶✷✸✹✺)。上界=2,算宽无害。
66
+ if ((cp >= 0x2600 && cp <= 0x27bf) || // Misc Symbols + Dingbats(含 ✶✷✸✹✺)
67
+ (cp >= 0x2b00 && cp <= 0x2bff) || // Misc Symbols and Arrows
68
+ (cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors(VS16 强制全角)
69
+ (cp >= 0x1f000 && cp <= 0x1faff) // Emoji 主区(含 💭)
70
+ ) {
71
+ return 2;
72
+ }
73
+ if (cp >= 0x1100 &&
74
+ (cp <= 0x115f ||
75
+ (cp >= 0x2e80 && cp <= 0x303e) ||
76
+ (cp >= 0x3041 && cp <= 0xa4cf) ||
77
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
78
+ (cp >= 0xf900 && cp <= 0xfaff) ||
79
+ (cp >= 0xfe10 && cp <= 0xfe6f) ||
80
+ (cp >= 0xff00 && cp <= 0xff60) ||
81
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
82
+ (cp >= 0x20000 && cp <= 0x3fffd))) {
83
+ return 2;
84
+ }
85
+ return 1;
86
+ }
87
+ /** 一段文本的显示宽度(按 charWidth 累加;不破坏 surrogate pair)。 */
88
+ export function displayWidth(text) {
89
+ let w = 0;
90
+ for (const ch of text) {
91
+ w += charWidth(ch.codePointAt(0) ?? 0);
92
+ }
93
+ return w;
94
+ }
95
+ /**
96
+ * 标准化流式文本:CRLF/CR → LF、剥离 ANSI、收敛尾部多余空行。
97
+ */
98
+ export function normalizeLiveText(text) {
99
+ return text
100
+ .replace(/\r\n?/g, '\n')
101
+ .replace(ANSI_RE, '')
102
+ .replace(/\n+$/g, '');
103
+ }
104
+ /**
105
+ * 取**前缀**:保留从头开始、显示宽度 ≤ maxWidth 的最长前缀(不破坏 surrogate pair)。
106
+ * 用于状态行的单行宽度钳制(保住最重要的行首 glyph/阶段,多余的尾部丢弃)。
107
+ */
108
+ export function clampWidthStart(text, maxWidth) {
109
+ if (maxWidth <= 0)
110
+ return '';
111
+ let w = 0;
112
+ let out = '';
113
+ for (const ch of text) {
114
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
115
+ if (w + cw > maxWidth)
116
+ break;
117
+ out += ch;
118
+ w += cw;
119
+ }
120
+ return out;
121
+ }
122
+ /**
123
+ * 把(可能多行的)流式文本硬换行成**显示行数组**,每行显示宽度 ≤ maxWidth(满足 INV-1)。
124
+ *
125
+ * 规则:
126
+ * - 先按原始 `\n` 切段落(保留空行 → 输出空字符串行)。
127
+ * - 段落内贪心累加:超过 maxWidth 时,优先在**最近一个空格**处断行(word-aware,
128
+ * 英文不切断单词);没有空格(如中文)则按字符硬断。
129
+ * - 全程按 charWidth(CJK/全角/emoji=2)计宽,绝不按 length。
130
+ *
131
+ * 这是"打字机多行"的核心:reasoning / 答案的长文本在这里被切成等宽窄行,
132
+ * 再由 LiveArea 取尾部最近 N 行(满足 INV-2)。
133
+ *
134
+ * 导出供单测断言。
135
+ */
136
+ export function wrapToDisplayLines(text, maxWidth) {
137
+ if (maxWidth <= 0)
138
+ return [];
139
+ const normalized = normalizeLiveText(text);
140
+ const out = [];
141
+ for (const para of normalized.split('\n')) {
142
+ if (para.length === 0) {
143
+ out.push('');
144
+ continue;
145
+ }
146
+ let line = '';
147
+ let lineWidth = 0;
148
+ let lastSpace = -1; // line 内最近一个空格的下标(断行机会)
149
+ for (const ch of para) {
150
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
151
+ // 超宽且当前行非空 → 先断行再放入 ch
152
+ if (lineWidth + cw > maxWidth && line.length > 0) {
153
+ if (lastSpace >= 0 && lastSpace < line.length - 1) {
154
+ // 在最近空格处断:head 不含该空格,余下部分(不含空格)顺延到下一行
155
+ const head = line.slice(0, lastSpace);
156
+ const rest = line.slice(lastSpace + 1);
157
+ out.push(head);
158
+ line = rest;
159
+ lineWidth = displayWidth(rest);
160
+ }
161
+ else {
162
+ out.push(line);
163
+ line = '';
164
+ lineWidth = 0;
165
+ }
166
+ lastSpace = -1;
167
+ }
168
+ line += ch;
169
+ lineWidth += cw;
170
+ if (ch === ' ')
171
+ lastSpace = line.length - 1;
172
+ }
173
+ out.push(line);
174
+ }
175
+ return out;
176
+ }
@@ -73,7 +73,7 @@
73
73
  "prompt": { "type": "string" },
74
74
  "output_schema": {
75
75
  "type": "object",
76
- "description": "⚠ ADR-05 新增:LLM 输出 JSON Schema 强约束,走 provider-native structured output。仅 llm/skill 节点生效;详见 docs/05-advanced-workflow-integration.md。"
76
+ "description": "⚠ ADR-05 新增:LLM 输出 JSON Schema 强约束,走 provider-native structured output。仅 llm 节点生效(skill 节点 loader 拒绝);详见 docs/workflow-design.md。"
77
77
  },
78
78
  "fork": {
79
79
  "type": "array",
@@ -105,7 +105,7 @@
105
105
  "hint": { "type": "string" }
106
106
  }
107
107
  },
108
- "description": "⚠ ADR-05 新增:llm/skill 节点声明可用的上下文文件清单(path + hint),runner 拼进 prompt 并开放 Read 子循环,不 inline 内容。详见 docs/05-advanced-workflow-integration.md。"
108
+ "description": "⚠ ADR-05 新增:仅 llm 节点声明可用的上下文文件清单(path + hint),runner 拼进 prompt 并开放 Read 子循环,不 inline 内容(skill 节点 loader 拒绝)。详见 docs/workflow-design.md。"
109
109
  },
110
110
  "allowed_tools": {
111
111
  "type": "array",