@vincemakes/kiso-code 0.1.30 → 0.1.32
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 +8 -8
- package/dist/body.d.ts +0 -121
- package/dist/body.js +0 -491
- package/dist/dock.d.ts +0 -75
- package/dist/dock.js +0 -176
- package/dist/editor.d.ts +0 -75
- package/dist/editor.js +0 -444
- package/dist/render.d.ts +0 -135
- package/dist/render.js +0 -325
package/dist/render.js
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event rendering for the terminal. Pure (testable): given events, produce
|
|
3
|
-
* the lines a human sees. Colors are raw ANSI — no dependencies.
|
|
4
|
-
*/
|
|
5
|
-
import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
|
|
6
|
-
export const COLOR_ON = { blue: "\x1b[38;5;75m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", bg: "\x1b[48;5;237m", reset: "\x1b[0m" };
|
|
7
|
-
export const COLOR_OFF = { blue: "", dim: "", red: "", green: "", bg: "", reset: "" };
|
|
8
|
-
export function palette() {
|
|
9
|
-
return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* E 组/八: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
13
|
-
* reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
|
|
14
|
-
* bidi overrides. The kiso colors are applied by render, not by the data.
|
|
15
|
-
* EVERY externally-sourced string must pass through this before any output.
|
|
16
|
-
*/
|
|
17
|
-
export function escapeTerminal(text) {
|
|
18
|
-
// eslint-disable-next-line no-control-regex
|
|
19
|
-
return text
|
|
20
|
-
.replace(/[\u0000-\u0008\u000d\u000e-\u001f\u007f]/g, "") // C0 (keeps only \t and \n)
|
|
21
|
-
.replace(/\u001b/g, "") // ESC
|
|
22
|
-
.replace(/[\u0080-\u009f]/g, "") // C1
|
|
23
|
-
.replace(/[\u202a-\u202e\u2066-\u2069]/g, ""); // bidi
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* 八/十: the path the human is asked to approve is the CANONICAL one the
|
|
27
|
-
* tool will actually touch — the tools' OWN resolution (deepest existing
|
|
28
|
-
* ancestor realpath'd, the not-yet-existing tail re-appended), so a file
|
|
29
|
-
* to be created under a symlinked directory shows the REAL target, and
|
|
30
|
-
* the UI and the tool share ONE resolution (canonicalTargetPath).
|
|
31
|
-
*/
|
|
32
|
-
export const canonicalPath = canonicalTargetPath;
|
|
33
|
-
/**
|
|
34
|
-
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
35
|
-
* current line); `newline` says whether the line is complete.
|
|
36
|
-
*
|
|
37
|
-
* 自举 P1: `prevThinking` marks a thinking delta that continues the SAME
|
|
38
|
-
* block — it renders appended to the segment, without the … prefix. The
|
|
39
|
-
* consumer closes the segment with a newline at the next non-thinking
|
|
40
|
-
* event.
|
|
41
|
-
*/
|
|
42
|
-
/**
|
|
43
|
-
* v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
|
|
44
|
-
* " (… /think shows full)" marker when the block is longer. The consumer
|
|
45
|
-
* buffers the block's deltas, renders this at the block's end, and keeps
|
|
46
|
-
* the full text for /think. Pipes get the same fold — the content
|
|
47
|
-
* strategy is presentation-independent.
|
|
48
|
-
*/
|
|
49
|
-
export function foldThinking(block) {
|
|
50
|
-
const p = palette();
|
|
51
|
-
const trimmed = escapeTerminal(block.trim());
|
|
52
|
-
const truncated = trimmed.length > 100;
|
|
53
|
-
return `${p.dim}…${trimmed.slice(0, 100)}${truncated ? ` (${block.length} chars · /think)` : ""}${p.reset}\n`;
|
|
54
|
-
}
|
|
55
|
-
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
56
|
-
export function foldResult(content) {
|
|
57
|
-
const flat = content.replaceAll("\n", " ");
|
|
58
|
-
const truncated = flat.length > 160;
|
|
59
|
-
return `${escapeTerminal(flat.slice(0, 160))}${truncated ? " (/last for full)" : ""}`;
|
|
60
|
-
}
|
|
61
|
-
export function renderEvent(ev, prevThinking = false) {
|
|
62
|
-
const p = palette();
|
|
63
|
-
switch (ev.type) {
|
|
64
|
-
case "user_input":
|
|
65
|
-
// v2a: blue (the identity accent — the interactive prompt echoes
|
|
66
|
-
// itself; this render is the REPLAY path).
|
|
67
|
-
return { text: `${p.blue}you> ${escapeTerminal(typeof ev.content === "string" ? ev.content : "(content)")}${p.reset}\n`, newline: true, prompt: false };
|
|
68
|
-
case "text_delta":
|
|
69
|
-
return { text: escapeTerminal(ev.text), newline: false, prompt: false };
|
|
70
|
-
case "text_end":
|
|
71
|
-
return { text: "\n", newline: true, prompt: false };
|
|
72
|
-
case "thinking":
|
|
73
|
-
// 自举 P1/v2b: the CONSUMER buffers each thinking block and folds
|
|
74
|
-
// it to ONE dim line (foldThinking); this render is the generic
|
|
75
|
-
// path for tests. The full block goes to /think.
|
|
76
|
-
return {
|
|
77
|
-
text: foldThinking(ev.text),
|
|
78
|
-
newline: true,
|
|
79
|
-
prompt: false,
|
|
80
|
-
};
|
|
81
|
-
case "tool_call_end":
|
|
82
|
-
// v2a: plain — the call line is information, not decoration.
|
|
83
|
-
return {
|
|
84
|
-
text: `→ ${escapeTerminal(ev.name)}(${ev.input ? escapeTerminal(JSON.stringify(ev.input).slice(0, 200)) : ""})\n`,
|
|
85
|
-
newline: true,
|
|
86
|
-
prompt: false,
|
|
87
|
-
};
|
|
88
|
-
case "tool_execution_started":
|
|
89
|
-
return { text: `${p.dim} running…${p.reset}\n`, newline: true, prompt: false };
|
|
90
|
-
case "tool_execution_succeeded":
|
|
91
|
-
return { text: ` ok\n`, newline: true, prompt: false }; // v2a: plain — success is not an accent
|
|
92
|
-
case "tool_execution_failed":
|
|
93
|
-
return { text: `${p.red} failed: ${escapeTerminal(ev.error.slice(0, 160))}${p.reset}\n`, newline: true, prompt: false };
|
|
94
|
-
case "tool_result": {
|
|
95
|
-
const content = typeof ev.content === "string" ? ev.content : ev.content.map((b) => (b.type === "text" ? b.text : "(image)")).join("");
|
|
96
|
-
return {
|
|
97
|
-
// v2b: the echo truncates at 160 chars + a /last hint — the
|
|
98
|
-
// full content stays in the event stream.
|
|
99
|
-
text: `${p.dim}${ev.isError ? p.red : p.dim} [result${ev.isError ? " ✗" : ""}] ${foldResult(content)}${p.reset}\n`,
|
|
100
|
-
newline: true,
|
|
101
|
-
prompt: false,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
case "permission_requested":
|
|
105
|
-
// 八: the tool NAME is model text — escaped like everything else.
|
|
106
|
-
return {
|
|
107
|
-
text: `⏸ ${escapeTerminal(ev.name)} needs approval ${p.dim}${approvalDetail(ev.name, ev.input)}${p.reset} `,
|
|
108
|
-
newline: false,
|
|
109
|
-
prompt: true,
|
|
110
|
-
};
|
|
111
|
-
case "permission_decided":
|
|
112
|
-
// v2a: verdicts are plain — neither success accents nor errors.
|
|
113
|
-
return {
|
|
114
|
-
text: ` ${ev.decision === "approved" ? "approved" : "denied"}${ev.reason ? `: ${escapeTerminal(ev.reason)}` : ""}\n`,
|
|
115
|
-
newline: true,
|
|
116
|
-
prompt: false,
|
|
117
|
-
};
|
|
118
|
-
case "terminal": {
|
|
119
|
-
const outcome = ev.outcome;
|
|
120
|
-
const label = outcome.kind === "completed"
|
|
121
|
-
? `done` // v2a: plain — the ✓ mark is the success accent
|
|
122
|
-
: outcome.kind === "aborted"
|
|
123
|
-
? `aborted (${outcome.by})`
|
|
124
|
-
: `${p.red}${outcome.kind}${p.reset}${"error" in outcome && "message" in outcome.error ? `: ${escapeTerminal(outcome.error.message.slice(0, 200))}` : ""}`;
|
|
125
|
-
return { text: `\n${label}\n`, newline: true, prompt: false };
|
|
126
|
-
}
|
|
127
|
-
case "compacted":
|
|
128
|
-
return { text: `${p.dim} [compacted ${ev.cleared.length} results]${p.reset}\n`, newline: true, prompt: false };
|
|
129
|
-
case "uncertain_pending":
|
|
130
|
-
return {
|
|
131
|
-
text: `${p.red}⚠ ${escapeTerminal(ev.name)} failed (${ev.executionId}): ${escapeTerminal(ev.error.slice(0, 160))}${p.reset}\n`,
|
|
132
|
-
newline: true,
|
|
133
|
-
prompt: false,
|
|
134
|
-
};
|
|
135
|
-
default:
|
|
136
|
-
return { text: "", newline: false, prompt: false };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* The approval prompt detail (Area 5/八): the human must be able to see
|
|
141
|
-
* EVERYTHING they are approving. The shell command is shown in full; the
|
|
142
|
-
* path is the CANONICAL one the tool will touch; write/edit show the FULL
|
|
143
|
-
* content (never a truncated tail that hides a dangerous payload). The
|
|
144
|
-
* decision is bound to the complete input via the decisionId.
|
|
145
|
-
*/
|
|
146
|
-
function approvalDetail(name, input) {
|
|
147
|
-
if (name === "shell") {
|
|
148
|
-
return `\n $ ${escapeTerminal(String(input.command ?? ""))}`;
|
|
149
|
-
}
|
|
150
|
-
if (name === "write_file") {
|
|
151
|
-
const content = String(input.content ?? "");
|
|
152
|
-
return `\n ${escapeTerminal(canonicalPath(String(input.path ?? "?")))}\n ${escapeTerminal(content)}`;
|
|
153
|
-
}
|
|
154
|
-
if (name === "edit_file") {
|
|
155
|
-
return `\n ${escapeTerminal(canonicalPath(String(input.path ?? "?")))}\n replace: ${escapeTerminal(String(input.search ?? ""))}\n with: ${escapeTerminal(String(input.replace ?? ""))}`;
|
|
156
|
-
}
|
|
157
|
-
return `\n ${escapeTerminal(JSON.stringify(input))}`;
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* B 区: one-line summary of a completed tool call, e.g.
|
|
161
|
-
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
|
162
|
-
* ✗ shell npm test (exit 1)
|
|
163
|
-
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
164
|
-
* code; failures (isError) are ✗. Pure and deterministic.
|
|
165
|
-
*/
|
|
166
|
-
export function renderToolSummary(name, input, result) {
|
|
167
|
-
// v2a: ✓ is a blue identity accent; ✗ stays red.
|
|
168
|
-
const p = palette();
|
|
169
|
-
const mark = result.isError ? `${p.red}✗${p.reset}` : `${p.blue}✓${p.reset}`;
|
|
170
|
-
const shortName = name.replace("_file", "");
|
|
171
|
-
const detail = toolSummaryDetail(name, input, result);
|
|
172
|
-
return `${mark} ${escapeTerminal(`${shortName} ${detail}`)}`;
|
|
173
|
-
}
|
|
174
|
-
function toolSummaryDetail(name, input, result) {
|
|
175
|
-
// Line count without the phantom empty line after a trailing newline.
|
|
176
|
-
const lines = (text) => {
|
|
177
|
-
if (text === "")
|
|
178
|
-
return 0;
|
|
179
|
-
const parts = text.split("\n");
|
|
180
|
-
return parts[parts.length - 1] === "" ? parts.length - 1 : parts.length;
|
|
181
|
-
};
|
|
182
|
-
switch (name) {
|
|
183
|
-
case "read_file": {
|
|
184
|
-
const path = String(input.path ?? "?");
|
|
185
|
-
const count = lines(String(result.content));
|
|
186
|
-
return `${path} (${count} line${count === 1 ? "" : "s"})`;
|
|
187
|
-
}
|
|
188
|
-
case "write_file": {
|
|
189
|
-
const path = String(input.path ?? "?");
|
|
190
|
-
const count = lines(String(input.content ?? ""));
|
|
191
|
-
return `${path} (+${count})`;
|
|
192
|
-
}
|
|
193
|
-
case "edit_file": {
|
|
194
|
-
const path = String(input.path ?? "?");
|
|
195
|
-
const removed = lines(String(input.search ?? ""));
|
|
196
|
-
const added = lines(String(input.replace ?? ""));
|
|
197
|
-
return `${path} (+${added} -${removed})`;
|
|
198
|
-
}
|
|
199
|
-
case "shell": {
|
|
200
|
-
const command = String(input.command ?? "?");
|
|
201
|
-
const exit = exitCodeOf(result);
|
|
202
|
-
return `${command} (exit ${exit})`;
|
|
203
|
-
}
|
|
204
|
-
case "list_dir":
|
|
205
|
-
return String(input.path ?? "(root)");
|
|
206
|
-
default:
|
|
207
|
-
return String(input.path ?? input.command ?? "");
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/** The exit code of a shell result: parsed from the failure text, 0 on success. */
|
|
211
|
-
function exitCodeOf(result) {
|
|
212
|
-
if (!result.isError)
|
|
213
|
-
return 0;
|
|
214
|
-
const m = /exit (\d+)/.exec(result.content);
|
|
215
|
-
return m !== null ? Number(m[1]) : 1;
|
|
216
|
-
}
|
|
217
|
-
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
218
|
-
export function kUnit(value) {
|
|
219
|
-
if (value === null)
|
|
220
|
-
return "?";
|
|
221
|
-
if (value >= 1000)
|
|
222
|
-
return `${(value / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
223
|
-
return String(value);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* B 区/v2a: the one-line status bar after a terminal, e.g.
|
|
227
|
-
* [turn 3 · in 12.4k out 1.8k · cache 9.2k · ctx ~14%]
|
|
228
|
-
* 降噪: unknown fields are OMITTED ENTIRELY (有什么显什么); a fully unknown
|
|
229
|
-
* usage → null (the caller prints nothing); faux mode → [turn N · faux].
|
|
230
|
-
* All data comes from usage events; ctx is the approximate estimate
|
|
231
|
-
* passed in (chars/4 vs the window), marked with ~.
|
|
232
|
-
*/
|
|
233
|
-
export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
|
|
234
|
-
if (faux)
|
|
235
|
-
return `[turn ${turn} · faux]`;
|
|
236
|
-
if (!usage.known)
|
|
237
|
-
return null; // everything unknown — nothing worth showing
|
|
238
|
-
const parts = [];
|
|
239
|
-
if (usage.in !== null || usage.out !== null) {
|
|
240
|
-
const seg = `${usage.in !== null ? `in ${kUnit(usage.in)}` : ""}${usage.in !== null && usage.out !== null ? " " : ""}${usage.out !== null ? `out ${kUnit(usage.out)}` : ""}`;
|
|
241
|
-
parts.push(seg);
|
|
242
|
-
}
|
|
243
|
-
if (usage.cache !== null)
|
|
244
|
-
parts.push(`cache ${kUnit(usage.cache)}`);
|
|
245
|
-
if (Number.isFinite(ctxRatio))
|
|
246
|
-
parts.push(`ctx ~${Math.round(ctxRatio * 100)}%`);
|
|
247
|
-
if (parts.length === 0)
|
|
248
|
-
return null;
|
|
249
|
-
return `[turn ${turn} · ${parts.join(" · ")}]`;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* v2a rhythm — the exact bytes after a terminal event: the status line
|
|
253
|
-
* hugs the terminal (有什么显什么 — omitted when there is nothing to show),
|
|
254
|
-
* then EXACTLY one blank line before the next prompt. The consumer prints
|
|
255
|
-
* this verbatim; the render tests pin the sequence.
|
|
256
|
-
*/
|
|
257
|
-
export function renderTerminalGap(statusLine) {
|
|
258
|
-
return `${statusLine === null ? "" : `${statusLine}\n`}\n`;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* v3 §01 — the banner, block-split. The logo is three INDEPENDENT rows
|
|
262
|
-
* (TOP / the tagline / BOTTOM), then TWO info rows (version,
|
|
263
|
-
* extensions). Every row truncates at the terminal width with a " (+N)"
|
|
264
|
-
* marker (N = the hidden display width); a window narrower than 40
|
|
265
|
-
* columns skips the logo entirely — only the info rows. Pure.
|
|
266
|
-
*/
|
|
267
|
-
export const TAGLINE = "the coding agent that survives kill -9";
|
|
268
|
-
const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", TAGLINE, "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
|
|
269
|
-
/** Display width of a row (1-cell ASCII; 2-cell CJK/wide). */
|
|
270
|
-
function displayW(row) {
|
|
271
|
-
let w = 0;
|
|
272
|
-
for (let i = 0; i < row.length; i += 1) {
|
|
273
|
-
const cp = row.codePointAt(i);
|
|
274
|
-
w += cp > 0xff ? 2 : 1;
|
|
275
|
-
}
|
|
276
|
-
return w;
|
|
277
|
-
}
|
|
278
|
-
/** v3 §01: truncate a row at `width`, marking the hidden span " (+N)". */
|
|
279
|
-
export function truncateRow(row, width) {
|
|
280
|
-
if (displayW(row) <= width)
|
|
281
|
-
return row;
|
|
282
|
-
const cut = Math.max(0, width - 4);
|
|
283
|
-
let w = 0;
|
|
284
|
-
let i = 0;
|
|
285
|
-
for (; i < row.length; i += 1) {
|
|
286
|
-
const cw = row.codePointAt(i) > 0xff ? 2 : 1;
|
|
287
|
-
if (w + cw > cut)
|
|
288
|
-
break;
|
|
289
|
-
w += cw;
|
|
290
|
-
}
|
|
291
|
-
return `${row.slice(0, i)} (+${displayW(row) - w})`;
|
|
292
|
-
}
|
|
293
|
-
/** v3 §01: the banner lines for a width W — logo (skipped under 40
|
|
294
|
-
* columns) + version + extensions. */
|
|
295
|
-
export function bannerLines(W, version, extensionsText) {
|
|
296
|
-
const rows = [];
|
|
297
|
-
if (W >= 40)
|
|
298
|
-
for (const r of LOGO_ROWS)
|
|
299
|
-
rows.push(truncateRow(r, W));
|
|
300
|
-
rows.push(truncateRow(`v${version}`, W));
|
|
301
|
-
if (extensionsText !== "")
|
|
302
|
-
rows.push(truncateRow(extensionsText, W));
|
|
303
|
-
return rows;
|
|
304
|
-
}
|
|
305
|
-
export function renderRecap(s) {
|
|
306
|
-
const p = palette();
|
|
307
|
-
const parts = [`${s.seconds}s`, `${s.tools} tool${s.tools === 1 ? "" : "s"}${s.edits > 0 ? ` (${s.edits} edit${s.edits === 1 ? "" : "s"})` : ""}`];
|
|
308
|
-
if (s.usage.known) {
|
|
309
|
-
const seg = `${s.usage.in !== null ? `in ${kUnit(s.usage.in)}` : ""}${s.usage.in !== null && s.usage.out !== null ? " " : ""}${s.usage.out !== null ? `out ${kUnit(s.usage.out)}` : ""}`;
|
|
310
|
-
if (seg !== "")
|
|
311
|
-
parts.push(seg);
|
|
312
|
-
if (s.usage.cache !== null && s.usage.in !== null && s.usage.in > 0) {
|
|
313
|
-
parts.push(`cache ${Math.round((s.usage.cache / s.usage.in) * 100)}%`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (s.ctxLeftPct !== null)
|
|
317
|
-
parts.push(`ctx left ~${Math.round(s.ctxLeftPct)}%`);
|
|
318
|
-
return `${p.blue}▞${p.reset} ${parts.join(" · ")}\n`;
|
|
319
|
-
}
|
|
320
|
-
/** One-line summary of a session, for `kiso sessions`. */
|
|
321
|
-
export function renderSessionLine(meta) {
|
|
322
|
-
const when = meta.updatedAt ? new Date(meta.updatedAt).toISOString().slice(0, 16) : "—";
|
|
323
|
-
// 八: the title is the user's first prompt — model/user text, escaped.
|
|
324
|
-
return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
|
|
325
|
-
}
|