zidane 5.6.14 → 5.7.4
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 +3 -1
- package/dist/{agent-ClkpElCZ.d.ts → agent-BNS2nx_T.d.ts} +535 -15
- package/dist/agent-BNS2nx_T.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +4 -0
- package/dist/chat/pure.js +3 -0
- package/dist/chat.d.ts +31 -661
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -3
- package/dist/chat.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
- package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-DnfNoj16.js +574 -0
- package/dist/edit-utils-DnfNoj16.js.map +1 -0
- package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
- package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
- package/dist/fetch-url-CPxfiXDa.js +518 -0
- package/dist/fetch-url-CPxfiXDa.js.map +1 -0
- package/dist/image-sniff-B7uFSNO1.js +90 -0
- package/dist/image-sniff-B7uFSNO1.js.map +1 -0
- package/dist/{index-CbS75MD3.d.ts → index-CZOwAJIX.d.ts} +2 -2
- package/dist/index-CZOwAJIX.d.ts.map +1 -0
- package/dist/{index-CTDMMdIy.d.ts → index-Ck_AWt8P.d.ts} +3 -4
- package/dist/index-Ck_AWt8P.d.ts.map +1 -0
- package/dist/{index-v3Tzobqr.d.ts → index-KiS7w0dC.d.ts} +3 -3
- package/dist/index-KiS7w0dC.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-DM1UcKeQ.js → interpolate-TySiqKzc.js} +23 -23
- package/dist/{interpolate-DM1UcKeQ.js.map → interpolate-TySiqKzc.js.map} +1 -1
- package/dist/{login-7tHcckmX.js → login-BDeqENSe.js} +7 -58
- package/dist/login-BDeqENSe.js.map +1 -0
- package/dist/{mcp-DGeB7-3D.js → mcp-Kqzz-Rs_.js} +8 -6
- package/dist/mcp-Kqzz-Rs_.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{messages-Dym8S_YH.js → messages-CvRQTdbR.js} +118 -39
- package/dist/messages-CvRQTdbR.js.map +1 -0
- package/dist/{presets-w9Px_aAm.js → presets-JuOnSI-i.js} +2 -2
- package/dist/{presets-w9Px_aAm.js.map → presets-JuOnSI-i.js.map} +1 -1
- package/dist/presets.d.ts +3 -3
- package/dist/presets.js +1 -1
- package/dist/{providers-beXyD9W9.js → providers-h4HJPbbv.js} +485 -31
- package/dist/providers-h4HJPbbv.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BRIsmBSY.js → session-BzLou2_-.js} +2 -2
- package/dist/{session-BRIsmBSY.js.map → session-BzLou2_-.js.map} +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{stats-Lc3zL3RM.js → stats-DAKBEKjc.js} +12 -2
- package/dist/stats-DAKBEKjc.js.map +1 -0
- package/dist/{stdio-loader-EVAF5KlU.js → stdio-loader-Ce68wUmM.js} +4 -4
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +70 -0
- package/dist/tools/fetch-url.d.ts.map +1 -0
- package/dist/tools/fetch-url.js +2 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +190 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/{tools-DhrLrOEr.js → tools-BGtJK0vo.js} +1368 -421
- package/dist/tools-BGtJK0vo.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +1 -1
- package/dist/{turn-operations-UAkOjO-u.js → transcript-anchors-BTSZAPVc.js} +147 -2713
- package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
- package/dist/{transcript-anchors-D0TR6djV.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
- package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
- package/dist/tui.d.ts +58 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1349 -422
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-CCHfR9eC.js +1938 -0
- package/dist/turn-operations-CCHfR9eC.js.map +1 -0
- package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
- package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
- package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
- package/dist/types-BPw_i5vb.js.map +1 -0
- package/dist/{types-KukEp-mi.d.ts → types-CEAMIUXw.d.ts} +1 -1
- package/dist/types-CEAMIUXw.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +3 -3
- package/docs/CHAT.md +53 -6
- package/docs/SKILL.md +3 -0
- package/docs/TUI.md +7 -0
- package/package.json +18 -2
- package/dist/agent-ClkpElCZ.d.ts.map +0 -1
- package/dist/index-CTDMMdIy.d.ts.map +0 -1
- package/dist/index-CbS75MD3.d.ts.map +0 -1
- package/dist/index-v3Tzobqr.d.ts.map +0 -1
- package/dist/login-7tHcckmX.js.map +0 -1
- package/dist/mcp-DGeB7-3D.js.map +0 -1
- package/dist/messages-Dym8S_YH.js.map +0 -1
- package/dist/providers-beXyD9W9.js.map +0 -1
- package/dist/stats-Lc3zL3RM.js.map +0 -1
- package/dist/stdio-loader-EVAF5KlU.js.map +0 -1
- package/dist/tools-DhrLrOEr.js.map +0 -1
- package/dist/transcript-anchors-D0TR6djV.d.ts.map +0 -1
- package/dist/turn-operations-UAkOjO-u.js.map +0 -1
- package/dist/types-KukEp-mi.d.ts.map +0 -1
- package/dist/types-oKPBdCmL.js.map +0 -1
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
//#region src/compact/utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for the compact module — extracted so the runner
|
|
4
|
+
* (`compact.ts`) and the restoration helper (`restore.ts`) don't carry
|
|
5
|
+
* redundant copies of the same low-level math.
|
|
6
|
+
*
|
|
7
|
+
* Kept zero-dependency by design: no Node/Bun imports, no zidane types
|
|
8
|
+
* either. Both `utf8ByteLength` and `estimateTokens` are total
|
|
9
|
+
* functions of a single `string` argument, so they're trivially
|
|
10
|
+
* portable to a worker/browser context if the compact module ever
|
|
11
|
+
* needs to render outside Node.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* UTF-8 byte length, matching `Buffer.byteLength(text, 'utf-8')` but
|
|
15
|
+
* without pulling `node:buffer` for a one-liner. Stable for surrogate
|
|
16
|
+
* pairs — a high+low surrogate pair counts as a single 4-byte sequence
|
|
17
|
+
* (not 2 × 3-byte). Cheap to call in hot loops.
|
|
18
|
+
*
|
|
19
|
+
* Pure. Identical bytes for identical input across every JS runtime.
|
|
20
|
+
*/
|
|
21
|
+
function utf8ByteLength(text) {
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
for (let i = 0; i < text.length; i++) {
|
|
24
|
+
const code = text.charCodeAt(i);
|
|
25
|
+
if (code < 128) bytes += 1;
|
|
26
|
+
else if (code < 2048) bytes += 2;
|
|
27
|
+
else if (code >= 55296 && code <= 56319) {
|
|
28
|
+
bytes += 4;
|
|
29
|
+
i++;
|
|
30
|
+
} else bytes += 3;
|
|
31
|
+
}
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
/** Same constant Claude Code's `CONTEXT_MANAGEMENT.md` documents. */
|
|
35
|
+
const BYTES_PER_TOKEN = 4;
|
|
36
|
+
/**
|
|
37
|
+
* Approximate token count for `text`. Mirrors Claude Code's
|
|
38
|
+
* `BYTES_PER_TOKEN = 4` heuristic — acceptable for budget arithmetic
|
|
39
|
+
* (sizing summarization scopes, enforcing per-file caps in restoration).
|
|
40
|
+
*
|
|
41
|
+
* Accurate to ~10% on English + code. Use a real tokenizer when exact
|
|
42
|
+
* counts matter (cost reporting, hard caps); this is for budget gates
|
|
43
|
+
* where being off by 10% is fine.
|
|
44
|
+
*
|
|
45
|
+
* Pure. Exported so callers (TUI, SDK consumers) can do their own
|
|
46
|
+
* pre-budgeting against the same heuristic the harness uses internally.
|
|
47
|
+
*/
|
|
48
|
+
function estimateTokens(text) {
|
|
49
|
+
return Math.ceil(text.length / 4);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/chat/context-breakdown.ts
|
|
53
|
+
/**
|
|
54
|
+
* Context-usage breakdown — pure transforms.
|
|
55
|
+
*
|
|
56
|
+
* Turns a {@link ContextSnapshot} (the raw pieces the agent assembled for a
|
|
57
|
+
* run: system prompt, wire tools, deferred tools, MCP tools/instructions,
|
|
58
|
+
* skills catalog, ...) plus the real last-turn token total into a categorized
|
|
59
|
+
* {@link ContextBreakdown} the TUI panel / GUI popover render.
|
|
60
|
+
*
|
|
61
|
+
* No React, no node — a browser-context renderer imports these via
|
|
62
|
+
* `zidane/chat/pure`. The snapshot is captured main-side (TUI direct, GUI in
|
|
63
|
+
* `main/chat`) because building it reads provider/agent state; only the pure
|
|
64
|
+
* shaping lives here.
|
|
65
|
+
*
|
|
66
|
+
* Accuracy: per-category counts are estimates when no provider `countTokens`
|
|
67
|
+
* is available — the heuristic ({@link estimateTokens}, ~4 bytes/token) sizes
|
|
68
|
+
* each static segment and the bar is reconciled so it always sums to the real
|
|
69
|
+
* last-turn total (`conversation = realTotal - sum(static)`). When a provider
|
|
70
|
+
* exposes exact counts (Anthropic, OpenAI), the caller passes them in via
|
|
71
|
+
* {@link ContextSnapshot.exact} and the corresponding categories drop their
|
|
72
|
+
* `estimated` flag. Exact buckets (free space, autocompact buffer) are never
|
|
73
|
+
* estimated.
|
|
74
|
+
*/
|
|
75
|
+
const LABELS = {
|
|
76
|
+
systemPrompt: "System prompt",
|
|
77
|
+
rules: "Rules (AGENTS.md)",
|
|
78
|
+
tools: "Tool definitions",
|
|
79
|
+
mcpTools: "MCP tools",
|
|
80
|
+
mcpInstructions: "MCP instructions",
|
|
81
|
+
skills: "Skills",
|
|
82
|
+
subagentDefs: "Subagent definitions",
|
|
83
|
+
conversation: "Conversation",
|
|
84
|
+
mcpToolsDeferred: "MCP tools (deferred)",
|
|
85
|
+
toolsDeferred: "System tools (deferred)",
|
|
86
|
+
autocompactBuffer: "Autocompact buffer",
|
|
87
|
+
freeSpace: "Free space"
|
|
88
|
+
};
|
|
89
|
+
/** Sum the heuristic token sizes of a list of strings. */
|
|
90
|
+
function sumEstimate(parts) {
|
|
91
|
+
let total = 0;
|
|
92
|
+
for (const p of parts) total += estimateTokens(p);
|
|
93
|
+
return total;
|
|
94
|
+
}
|
|
95
|
+
function mcpGroupTokens(group) {
|
|
96
|
+
const items = group.tools.map((t) => ({
|
|
97
|
+
id: `${group.server}:${t.name}`,
|
|
98
|
+
label: t.name,
|
|
99
|
+
tokens: estimateTokens(t.json),
|
|
100
|
+
estimated: true
|
|
101
|
+
}));
|
|
102
|
+
let total = 0;
|
|
103
|
+
for (const it of items) total += it.tokens;
|
|
104
|
+
return {
|
|
105
|
+
total,
|
|
106
|
+
items
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Shape a {@link ContextSnapshot} into a render-ready {@link ContextBreakdown}.
|
|
111
|
+
*
|
|
112
|
+
* Live categories (system prompt, tools, MCP tools/instructions, skills,
|
|
113
|
+
* memory, subagent defs, conversation) sum to `snapshot.used` — `conversation`
|
|
114
|
+
* absorbs the remainder so the bar matches the provider-reported total exactly.
|
|
115
|
+
* Deferred + autocompact + free-space buckets follow.
|
|
116
|
+
*
|
|
117
|
+
* When `snapshot.exact` carries provider counts, the matching categories use
|
|
118
|
+
* them (system, tools) and drop their `estimated` flag; everything else stays
|
|
119
|
+
* on the heuristic.
|
|
120
|
+
*/
|
|
121
|
+
function buildContextBreakdown(snapshot) {
|
|
122
|
+
const exact = snapshot.exact;
|
|
123
|
+
const systemExact = typeof exact?.system === "number";
|
|
124
|
+
const rulesTokens = snapshot.rulesBlock ? estimateTokens(snapshot.rulesBlock) : 0;
|
|
125
|
+
const rulesItems = (snapshot.rulesFiles ?? []).map((f) => ({
|
|
126
|
+
id: f.path,
|
|
127
|
+
label: f.path,
|
|
128
|
+
tokens: 0,
|
|
129
|
+
estimated: false
|
|
130
|
+
}));
|
|
131
|
+
const skillsTokens = snapshot.skillsCatalog ? estimateTokens(snapshot.skillsCatalog) : 0;
|
|
132
|
+
const mcpInstructionsTokens = snapshot.mcpInstructions ? estimateTokens(snapshot.mcpInstructions) : 0;
|
|
133
|
+
const subagentTokens = snapshot.subagentDefs ? estimateTokens(snapshot.subagentDefs) : 0;
|
|
134
|
+
const embeddedSubsections = rulesTokens + skillsTokens + mcpInstructionsTokens + subagentTokens;
|
|
135
|
+
const systemTotal = systemExact ? exact.system : estimateTokens(snapshot.system);
|
|
136
|
+
const baseSystemTokens = Math.max(0, systemTotal - embeddedSubsections);
|
|
137
|
+
const nativeToolsEst = sumEstimate(snapshot.toolsJson);
|
|
138
|
+
const mcpDisclosed = snapshot.mcpGroups.map(mcpGroupTokens);
|
|
139
|
+
const mcpToolsEst = mcpDisclosed.reduce((a, g) => a + g.total, 0);
|
|
140
|
+
let nativeToolsTokens = nativeToolsEst;
|
|
141
|
+
let mcpToolsTokens = mcpToolsEst;
|
|
142
|
+
let toolsEstimated = true;
|
|
143
|
+
if (systemExact && typeof exact?.systemAndTools === "number") {
|
|
144
|
+
const exactToolsTotal = Math.max(0, exact.systemAndTools - exact.system);
|
|
145
|
+
const estTotal = nativeToolsEst + mcpToolsEst;
|
|
146
|
+
if (estTotal > 0) {
|
|
147
|
+
nativeToolsTokens = Math.round(exactToolsTotal * (nativeToolsEst / estTotal));
|
|
148
|
+
mcpToolsTokens = exactToolsTotal - nativeToolsTokens;
|
|
149
|
+
} else {
|
|
150
|
+
nativeToolsTokens = exactToolsTotal;
|
|
151
|
+
mcpToolsTokens = 0;
|
|
152
|
+
}
|
|
153
|
+
toolsEstimated = false;
|
|
154
|
+
}
|
|
155
|
+
const live = [];
|
|
156
|
+
const push = (id, tokens, estimated, items) => {
|
|
157
|
+
if (tokens <= 0 && !items?.length) return;
|
|
158
|
+
live.push({
|
|
159
|
+
id,
|
|
160
|
+
label: LABELS[id],
|
|
161
|
+
tokens: Math.max(0, tokens),
|
|
162
|
+
estimated,
|
|
163
|
+
...items?.length ? { items } : {}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
push("systemPrompt", baseSystemTokens, !systemExact);
|
|
167
|
+
push("rules", rulesTokens, true, rulesItems);
|
|
168
|
+
push("skills", skillsTokens, true);
|
|
169
|
+
push("mcpInstructions", mcpInstructionsTokens, true);
|
|
170
|
+
push("subagentDefs", subagentTokens, true);
|
|
171
|
+
push("tools", nativeToolsTokens, toolsEstimated);
|
|
172
|
+
push("mcpTools", mcpToolsTokens, toolsEstimated, mcpDisclosed.flatMap((g) => g.items));
|
|
173
|
+
const accountedStatic = live.reduce((a, c) => a + c.tokens, 0);
|
|
174
|
+
push("conversation", Math.max(0, snapshot.used - accountedStatic), !(systemExact && !toolsEstimated));
|
|
175
|
+
const deferred = [];
|
|
176
|
+
const deferredNativeTools = sumEstimate(snapshot.deferredToolsJson);
|
|
177
|
+
if (deferredNativeTools > 0) deferred.push({
|
|
178
|
+
id: "toolsDeferred",
|
|
179
|
+
label: LABELS.toolsDeferred,
|
|
180
|
+
tokens: deferredNativeTools,
|
|
181
|
+
estimated: true,
|
|
182
|
+
deferred: true
|
|
183
|
+
});
|
|
184
|
+
const deferredMcp = snapshot.deferredMcpGroups.map(mcpGroupTokens);
|
|
185
|
+
const deferredMcpTotal = deferredMcp.reduce((a, g) => a + g.total, 0);
|
|
186
|
+
if (deferredMcpTotal > 0) deferred.push({
|
|
187
|
+
id: "mcpToolsDeferred",
|
|
188
|
+
label: LABELS.mcpToolsDeferred,
|
|
189
|
+
tokens: deferredMcpTotal,
|
|
190
|
+
estimated: true,
|
|
191
|
+
deferred: true,
|
|
192
|
+
items: deferredMcp.flatMap((g) => g.items)
|
|
193
|
+
});
|
|
194
|
+
if (snapshot.autocompactBuffer > 0) deferred.push({
|
|
195
|
+
id: "autocompactBuffer",
|
|
196
|
+
label: LABELS.autocompactBuffer,
|
|
197
|
+
tokens: snapshot.autocompactBuffer,
|
|
198
|
+
estimated: false,
|
|
199
|
+
deferred: true
|
|
200
|
+
});
|
|
201
|
+
const free = Math.max(0, snapshot.effectiveWindow - snapshot.used);
|
|
202
|
+
deferred.push({
|
|
203
|
+
id: "freeSpace",
|
|
204
|
+
label: LABELS.freeSpace,
|
|
205
|
+
tokens: free,
|
|
206
|
+
estimated: false,
|
|
207
|
+
deferred: true
|
|
208
|
+
});
|
|
209
|
+
const categories = [...live, ...deferred];
|
|
210
|
+
const hasEstimates = live.some((c) => c.estimated && c.tokens > 0);
|
|
211
|
+
const fraction = snapshot.effectiveWindow > 0 ? Math.max(0, Math.min(1, snapshot.used / snapshot.effectiveWindow)) : 0;
|
|
212
|
+
return {
|
|
213
|
+
modelId: snapshot.modelId,
|
|
214
|
+
used: snapshot.used,
|
|
215
|
+
effectiveWindow: snapshot.effectiveWindow,
|
|
216
|
+
fraction,
|
|
217
|
+
categories,
|
|
218
|
+
hasEstimates,
|
|
219
|
+
...snapshot.usage ? { usage: snapshot.usage } : {},
|
|
220
|
+
...snapshot.activeSkills?.length ? { activeSkills: snapshot.activeSkills } : {}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/chat/format.ts
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the user's home directory from env (no static os-module import),
|
|
227
|
+
* so this module stays node-free and importable from a browser-context
|
|
228
|
+
* renderer (Electron's renderer, Vite) via `zidane/chat/pure`. Reads the
|
|
229
|
+
* platform env vars Node/Electron populate; returns '' when unavailable
|
|
230
|
+
* (renderer), in which case `compactPath` falls back to the verbatim path.
|
|
231
|
+
*/
|
|
232
|
+
function resolveHome() {
|
|
233
|
+
const env = globalThis.process?.env;
|
|
234
|
+
return env?.HOME ?? env?.USERPROFILE ?? "";
|
|
235
|
+
}
|
|
236
|
+
/** Compact token formatter — 12_415 → "12.4k", 1_234_567 → "1.23M". */
|
|
237
|
+
function fmtTokens(n) {
|
|
238
|
+
if (n < 1e3) return String(n);
|
|
239
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 2 : 1)}k`;
|
|
240
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
241
|
+
}
|
|
242
|
+
/** Compact relative-time formatter — "just now / 5m / 3h / 2d". */
|
|
243
|
+
function ageString(ts, now = Date.now()) {
|
|
244
|
+
const m = Math.floor((now - ts) / 6e4);
|
|
245
|
+
if (m < 1) return "just now";
|
|
246
|
+
if (m < 60) return `${m}m ago`;
|
|
247
|
+
const h = Math.floor(m / 60);
|
|
248
|
+
if (h < 24) return `${h}h ago`;
|
|
249
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
250
|
+
}
|
|
251
|
+
/** Six-char short form of a session id for headers and lists. */
|
|
252
|
+
function shortId(id) {
|
|
253
|
+
return id.replace(/-/g, "").slice(0, 6);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Single-line preview of a multi-line string, capped at `max` chars and
|
|
257
|
+
* ellipsis-terminated when truncated.
|
|
258
|
+
*
|
|
259
|
+
* Whitespace runs (newlines, tabs, multiple spaces) collapse into one
|
|
260
|
+
* space so the rendered output stays on a single visual row no matter
|
|
261
|
+
* how the input was shaped. Used by every transcript "preview" surface
|
|
262
|
+
* (spawn-start task, `tool: shell (background): <command>`,
|
|
263
|
+
* `<task-notification>` summary line, etc.) — without the whitespace
|
|
264
|
+
* collapse, a 60-char `slice` on a string with an inline `\n\n` paints
|
|
265
|
+
* the second paragraph below the first, producing the visible
|
|
266
|
+
* "preview text spills onto multiple lines" bug (and, downstream,
|
|
267
|
+
* misaligned spawn markers when the wrapped lines collide with
|
|
268
|
+
* other events).
|
|
269
|
+
*
|
|
270
|
+
* Reserves one slot for the `…` so the displayed width is exactly
|
|
271
|
+
* `max` when truncation kicks in.
|
|
272
|
+
*/
|
|
273
|
+
function previewLine(s, max) {
|
|
274
|
+
const single = s.replace(/\s+/g, " ").trim();
|
|
275
|
+
if (single.length <= max) return single;
|
|
276
|
+
return `${single.slice(0, max - 1)}…`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Compact human-readable duration formatter shared by background-task
|
|
280
|
+
* surfaces (the `<task-notification>` summary, the TUI banner, the
|
|
281
|
+
* `shell_kill` tool result, etc.).
|
|
282
|
+
*
|
|
283
|
+
* Format ladder:
|
|
284
|
+
* - `< 1s` → `"Nms"`
|
|
285
|
+
* - `< 10s` → `"N.Ns"` (one decimal)
|
|
286
|
+
* - `< 1m` → `"Ns"` (whole seconds)
|
|
287
|
+
* - `< 1h` → `"NmNs"` / `"Nm"` when seconds round to 0
|
|
288
|
+
* - `≥ 1h` → `"NhNm"` / `"Nh"` when minutes round to 0
|
|
289
|
+
*
|
|
290
|
+
* Single source of truth so a 60s task renders the same across the
|
|
291
|
+
* model-facing XML summary and the user-facing banner. Earlier
|
|
292
|
+
* separate formatters disagreed (XML said `"60.0s"`, banner said `"1m"`)
|
|
293
|
+
* which was confusing to the user reading both side by side.
|
|
294
|
+
*/
|
|
295
|
+
function formatDuration(ms) {
|
|
296
|
+
if (ms < 0) ms = 0;
|
|
297
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
298
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
|
|
299
|
+
const minutes = Math.floor(ms / 6e4);
|
|
300
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
301
|
+
if (minutes < 60) return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
302
|
+
const hours = Math.floor(minutes / 60);
|
|
303
|
+
const remMinutes = minutes % 60;
|
|
304
|
+
return remMinutes > 0 ? `${hours}h${remMinutes}m` : `${hours}h`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Status label for a terminated background task — `"exited <code>"`
|
|
308
|
+
* for natural exits, `"killed"` (with the signal name when known)
|
|
309
|
+
* for our-issued SIGTERMs.
|
|
310
|
+
*
|
|
311
|
+
* Pulled out as its own function so the `<task-notification>` XML
|
|
312
|
+
* summary, the TUI banner header, the `shell_kill` tool result, and
|
|
313
|
+
* future surfaces all read the same string.
|
|
314
|
+
*/
|
|
315
|
+
function formatTaskStatus(info) {
|
|
316
|
+
return info.status === "killed" ? `killed${info.signal ? ` (${info.signal})` : ""}` : `exited ${info.exitCode}`;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* One-line summary of a terminated background task — the shape used by
|
|
320
|
+
* the `<task-notification>` XML's `<summary>` tag AND the TUI banner's
|
|
321
|
+
* `event.text` fallback string. Three dot-separated segments:
|
|
322
|
+
*
|
|
323
|
+
* `<command preview · status · duration>`
|
|
324
|
+
*
|
|
325
|
+
* Centralizes the format so live + replay + wire all agree, and so a
|
|
326
|
+
* future cosmetic tweak (separator glyph, segment ordering) lands in
|
|
327
|
+
* exactly one place.
|
|
328
|
+
*/
|
|
329
|
+
function formatTaskSummary(info, maxCommandChars = 80) {
|
|
330
|
+
return `${previewLine(info.command, maxCommandChars)} · ${formatTaskStatus(info)} · ${formatDuration(info.durationMs)}`;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Compact an absolute path for display: replace the user's `$HOME`
|
|
334
|
+
* prefix with `~` (so `/Users/yael/Code/zidane` → `~/Code/zidane`),
|
|
335
|
+
* and optionally left-truncate with an ellipsis when the result
|
|
336
|
+
* still exceeds `maxWidth` (so the path's *tail* — the part the user
|
|
337
|
+
* recognizes — stays visible: `…/zidane` rather than `/Users/yaeluil…`).
|
|
338
|
+
*
|
|
339
|
+
* `maxWidth` is the maximum *display width* in cells. Omit to skip
|
|
340
|
+
* truncation. Paths outside `$HOME` are returned verbatim modulo
|
|
341
|
+
* truncation. The ellipsis (`…`) counts as one cell.
|
|
342
|
+
*
|
|
343
|
+
* `home` overrides `os.homedir()` for tests; production callers leave
|
|
344
|
+
* it undefined and pay the cheap one-syscall lookup per call.
|
|
345
|
+
*/
|
|
346
|
+
function compactPath(path, maxWidth, home) {
|
|
347
|
+
const h = home ?? resolveHome();
|
|
348
|
+
let display = path;
|
|
349
|
+
if (h) {
|
|
350
|
+
if (path === h) display = "~";
|
|
351
|
+
else if (path.startsWith(`${h}/`)) display = `~${path.slice(h.length)}`;
|
|
352
|
+
}
|
|
353
|
+
if (maxWidth !== void 0 && maxWidth > 1 && display.length > maxWidth) return `…${display.slice(display.length - maxWidth + 1)}`;
|
|
354
|
+
return display;
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/tools/edit-utils.ts
|
|
358
|
+
/**
|
|
359
|
+
* Internal helpers shared between the `edit` and `multi_edit` tools.
|
|
360
|
+
*
|
|
361
|
+
* Not part of the public API — intentionally not re-exported from `tools/index.ts`
|
|
362
|
+
* or the package barrel.
|
|
363
|
+
*/
|
|
364
|
+
/**
|
|
365
|
+
* Count exact (non-overlapping) occurrences of `needle` in `haystack`.
|
|
366
|
+
* Returns 0 for an empty needle — both edit tools reject empty `old_string`
|
|
367
|
+
* up front, so this branch is defensive rather than semantic.
|
|
368
|
+
*/
|
|
369
|
+
function countExactMatches(haystack, needle) {
|
|
370
|
+
if (needle.length === 0) return 0;
|
|
371
|
+
let count = 0;
|
|
372
|
+
let idx = 0;
|
|
373
|
+
while (true) {
|
|
374
|
+
const next = haystack.indexOf(needle, idx);
|
|
375
|
+
if (next === -1) break;
|
|
376
|
+
count++;
|
|
377
|
+
idx = next + needle.length;
|
|
378
|
+
}
|
|
379
|
+
return count;
|
|
380
|
+
}
|
|
381
|
+
/** Map curly quotes (any of the four) to their straight ASCII equivalents. */
|
|
382
|
+
function normalizeQuotes(str) {
|
|
383
|
+
return str.replaceAll("‘", "'").replaceAll("’", "'").replaceAll("“", "\"").replaceAll("”", "\"");
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Substitutions Anthropic's API applies to assistant output before the model
|
|
387
|
+
* sees it. The model emits the sanitized form; the file on disk contains the
|
|
388
|
+
* unsanitized form. We undo the substitutions on `old_string` so the search
|
|
389
|
+
* lands on the actual file contents.
|
|
390
|
+
*
|
|
391
|
+
* Verbatim from `claude-code/tools/FileEditTool/utils.ts`.
|
|
392
|
+
*/
|
|
393
|
+
const DESANITIZATIONS = [
|
|
394
|
+
["<fnr>", "<function_results>"],
|
|
395
|
+
["<n>", "<name>"],
|
|
396
|
+
["</n>", "</name>"],
|
|
397
|
+
["<o>", "<output>"],
|
|
398
|
+
["</o>", "</output>"],
|
|
399
|
+
["<e>", "<error>"],
|
|
400
|
+
["</e>", "</error>"],
|
|
401
|
+
["<s>", "<system>"],
|
|
402
|
+
["</s>", "</system>"],
|
|
403
|
+
["<r>", "<result>"],
|
|
404
|
+
["</r>", "</result>"],
|
|
405
|
+
["< META_START >", "<META_START>"],
|
|
406
|
+
["< META_END >", "<META_END>"],
|
|
407
|
+
["< EOT >", "<EOT>"],
|
|
408
|
+
["< META >", "<META>"],
|
|
409
|
+
["< SOS >", "<SOS>"],
|
|
410
|
+
["\n\nH:", "\n\nHuman:"],
|
|
411
|
+
["\n\nA:", "\n\nAssistant:"]
|
|
412
|
+
];
|
|
413
|
+
/**
|
|
414
|
+
* Apply the SDK desanitization table to a string. Exported so the edit tools
|
|
415
|
+
* can apply it to `new_string` whenever `old_string` matched via a
|
|
416
|
+
* desanitize-class fallback — keeps the file's unsanitized form on disk
|
|
417
|
+
* instead of writing the model's abbreviated form back.
|
|
418
|
+
*/
|
|
419
|
+
function desanitize(s) {
|
|
420
|
+
let out = s;
|
|
421
|
+
for (const [from, to] of DESANITIZATIONS) out = out.replaceAll(from, to);
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Strip line-number prefixes from each line of a needle, used as a recovery
|
|
426
|
+
* fallback when the model pastes a `read_file` chunk verbatim into
|
|
427
|
+
* `old_string` — the on-disk file doesn't carry the metadata prefix.
|
|
428
|
+
*
|
|
429
|
+
* Accepts three separator characters so a model that learned on a different
|
|
430
|
+
* agent stack still works here: `\t` (Claude Code compact, our default),
|
|
431
|
+
* `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one
|
|
432
|
+
* of `\t | →`. The 9-digit ceiling covers files up to ~1B lines without
|
|
433
|
+
* overshooting into legitimate `\d{N}<sep>` content like Markdown table
|
|
434
|
+
* cells with long numeric IDs.
|
|
435
|
+
*/
|
|
436
|
+
const LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
|
|
437
|
+
function stripLineNumberPrefixes(s) {
|
|
438
|
+
return s.replace(LINE_NUMBER_PREFIX_RE, "");
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Search `target` in `normFile` and slice the matching span out of the
|
|
442
|
+
* original `haystack`, counting all non-overlapping occurrences. `normFile`
|
|
443
|
+
* is the haystack with whatever transform (quotes / desanitize / combined)
|
|
444
|
+
* was applied to make the indices align — slicing the original haystack
|
|
445
|
+
* preserves the file's actual typography so `replace_all` writes back the
|
|
446
|
+
* file's form, not the model's.
|
|
447
|
+
*
|
|
448
|
+
* Pre-condition: `normFile.length === haystack.length` (every transform
|
|
449
|
+
* we use is one-to-one). Returns null on miss.
|
|
450
|
+
*/
|
|
451
|
+
function locateAndCount(haystack, normFile, target, via) {
|
|
452
|
+
const idx = normFile.indexOf(target);
|
|
453
|
+
if (idx === -1) return null;
|
|
454
|
+
const actual = haystack.slice(idx, idx + target.length);
|
|
455
|
+
let occ = 0;
|
|
456
|
+
let cursor = 0;
|
|
457
|
+
while (true) {
|
|
458
|
+
const next = normFile.indexOf(target, cursor);
|
|
459
|
+
if (next === -1) break;
|
|
460
|
+
occ++;
|
|
461
|
+
cursor = next + target.length;
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
actual,
|
|
465
|
+
occurrences: occ,
|
|
466
|
+
via
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function resolveOldString(haystack, needle) {
|
|
470
|
+
const exact = countExactMatches(haystack, needle);
|
|
471
|
+
if (exact > 0) return {
|
|
472
|
+
actual: needle,
|
|
473
|
+
occurrences: exact,
|
|
474
|
+
via: "exact"
|
|
475
|
+
};
|
|
476
|
+
const normNeedle = normalizeQuotes(needle);
|
|
477
|
+
const normFile = normalizeQuotes(haystack);
|
|
478
|
+
if (normNeedle !== needle || normFile !== haystack) {
|
|
479
|
+
const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
|
|
480
|
+
if (m) return m;
|
|
481
|
+
}
|
|
482
|
+
const desan = desanitize(needle);
|
|
483
|
+
if (desan !== needle) {
|
|
484
|
+
const desanCount = countExactMatches(haystack, desan);
|
|
485
|
+
if (desanCount > 0) return {
|
|
486
|
+
actual: desan,
|
|
487
|
+
occurrences: desanCount,
|
|
488
|
+
via: "desanitize"
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const combo = desanitize(normNeedle);
|
|
492
|
+
if (combo !== needle) {
|
|
493
|
+
const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
|
|
494
|
+
if (m) return m;
|
|
495
|
+
}
|
|
496
|
+
const stripped = stripLineNumberPrefixes(needle);
|
|
497
|
+
if (stripped !== needle && stripped.trim().length > 0) {
|
|
498
|
+
const count = countExactMatches(haystack, stripped);
|
|
499
|
+
if (count > 0) return {
|
|
500
|
+
actual: stripped,
|
|
501
|
+
occurrences: count,
|
|
502
|
+
via: "line-numbers"
|
|
503
|
+
};
|
|
504
|
+
const strippedNorm = normalizeQuotes(stripped);
|
|
505
|
+
if (strippedNorm !== stripped || normFile !== haystack) {
|
|
506
|
+
const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
|
|
507
|
+
if (m) return m;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Apply the same recovery transforms used to find `old_string` to
|
|
514
|
+
* `new_string`, so the file gets back its native form: desanitize when
|
|
515
|
+
* the model emitted `<n>` for `<name>`, strip line-number prefixes when
|
|
516
|
+
* the match required them, then re-curlify when the match required
|
|
517
|
+
* quote normalization. Shared between `edit` and `multi_edit`.
|
|
518
|
+
*/
|
|
519
|
+
function styleReplacementForVia(replacement, via, actual) {
|
|
520
|
+
let out = replacement;
|
|
521
|
+
if (via === "desanitize" || via === "quotes+desanitize") out = desanitize(out);
|
|
522
|
+
if (via === "line-numbers" || via === "quotes+line-numbers") out = stripLineNumberPrefixes(out);
|
|
523
|
+
if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers") out = preserveQuoteStyle(actual, out);
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* When `old_string` matched via curly-quote normalization, re-style
|
|
528
|
+
* `new_string` so the file's typography is preserved across the edit.
|
|
529
|
+
* Detects whether the matched file region had curly singles, doubles, or
|
|
530
|
+
* both, and applies the matching curlification to the replacement.
|
|
531
|
+
*
|
|
532
|
+
* Apostrophes in contractions (`don't`, `it's`) get the right-single curly
|
|
533
|
+
* quote regardless of opening context — that's the canonical typographer's
|
|
534
|
+
* convention for English. Other quotes use a simple
|
|
535
|
+
* preceded-by-whitespace-or-opening-punctuation heuristic.
|
|
536
|
+
*/
|
|
537
|
+
function preserveQuoteStyle(actual, replacement) {
|
|
538
|
+
const hasDouble = actual.includes("“") || actual.includes("”");
|
|
539
|
+
const hasSingle = actual.includes("‘") || actual.includes("’");
|
|
540
|
+
if (!hasDouble && !hasSingle) return replacement;
|
|
541
|
+
let out = replacement;
|
|
542
|
+
if (hasDouble) out = applyCurly(out, "\"", "“", "”", false);
|
|
543
|
+
if (hasSingle) out = applyCurly(out, "'", "‘", "’", true);
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
function applyCurly(s, straight, left, right, contractionAware) {
|
|
547
|
+
const chars = [...s];
|
|
548
|
+
const result = [];
|
|
549
|
+
for (let i = 0; i < chars.length; i++) {
|
|
550
|
+
if (chars[i] !== straight) {
|
|
551
|
+
result.push(chars[i]);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (contractionAware) {
|
|
555
|
+
const prev = i > 0 ? chars[i - 1] : "";
|
|
556
|
+
const next = i < chars.length - 1 ? chars[i + 1] : "";
|
|
557
|
+
if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
|
|
558
|
+
result.push(right);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
result.push(isOpeningContext(chars, i) ? left : right);
|
|
563
|
+
}
|
|
564
|
+
return result.join("");
|
|
565
|
+
}
|
|
566
|
+
function isOpeningContext(chars, i) {
|
|
567
|
+
if (i === 0) return true;
|
|
568
|
+
const prev = chars[i - 1];
|
|
569
|
+
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "—" || prev === "–";
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
export { compactPath as a, formatTaskStatus as c, shortId as d, buildContextBreakdown as f, utf8ByteLength as h, ageString as i, formatTaskSummary as l, estimateTokens as m, stripLineNumberPrefixes as n, fmtTokens as o, BYTES_PER_TOKEN as p, styleReplacementForVia as r, formatDuration as s, resolveOldString as t, previewLine as u };
|
|
573
|
+
|
|
574
|
+
//# sourceMappingURL=edit-utils-DnfNoj16.js.map
|