@vincemakes/kiso-code 0.1.36 → 0.1.38

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/dist/chat.js CHANGED
@@ -177,9 +177,23 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
177
177
  const turnStart = Date.now();
178
178
  let toolCount = 0;
179
179
  let editCount = 0;
180
+ // W14: the thinking event carries NO timestamp — the CLI wall-clocks
181
+ // the thinking window: it opens at the first thinking event and closes
182
+ // at the first non-thinking event (the fold needs the seconds).
183
+ let thoughtSeconds = 0;
184
+ let thinkingSince = null;
180
185
  try {
181
186
  for await (const ev of run) {
182
187
  last = ev;
188
+ if (ev.type !== "thinking") {
189
+ if (thinkingSince !== null) {
190
+ thoughtSeconds += (Date.now() - thinkingSince) / 1000;
191
+ thinkingSince = null;
192
+ }
193
+ }
194
+ else if (thinkingSince === null) {
195
+ thinkingSince = Date.now();
196
+ }
183
197
  // v2a (the double echo): the interactive echo was already rendered by the
184
198
  // input source — rendering the event again is the double echo.
185
199
  // v2b: DOCKED — the echo lives in the input row (H), NOT the body;
@@ -218,7 +232,18 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
218
232
  break;
219
233
  case "tool_result": {
220
234
  const text = typeof ev.content === "string" ? ev.content : "";
221
- body.toolResult(ev.callId, { content: text, isError: ev.isError });
235
+ // W19: a DENIED call carries its reason — extracted from the
236
+ // result's "[Permission denied] " prefix, keyed on the
237
+ // "denied" tag (the parseChecklist discipline: the tag
238
+ // declares, the prefix confirms). The ToolCell renders the
239
+ // pinned row (full name, target, reason, no timing).
240
+ let reason = null;
241
+ if ((ev.tags ?? []).includes("denied")) {
242
+ const m = /^\[Permission denied\] (.*)$/.exec(text);
243
+ if (m !== null)
244
+ reason = m[1];
245
+ }
246
+ body.toolResult(ev.callId, { content: text, isError: ev.isError, reason });
222
247
  // round 6 (the todo round): a result tagged do-not-compact whose content
223
248
  // follows the checklist shape also renders as the durable
224
249
  // checklist cell (the CLI translates Event → the tui's own
@@ -271,12 +296,19 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
271
296
  // events (zero tokens). The dock's status bar still paints.
272
297
  statusCb?.(usage, estimateCtxRatio(session));
273
298
  const ratio = estimateCtxRatio(session);
299
+ // W14: the turn record closes HERE — before the recap logs, so
300
+ // the commit loop folds the quiet turn's held cells first (the
301
+ // fold line lands above the recap, natural cell order).
302
+ body.endTurn(Math.round(thoughtSeconds));
274
303
  bodyLog(renderRecap({
275
304
  seconds: Math.round((Date.now() - turnStart) / 1000),
276
305
  tools: toolCount,
277
306
  edits: editCount,
278
307
  usage,
279
308
  ctxLeftPct: Number.isFinite(ratio) ? (1 - ratio) * 100 : null,
309
+ // W19: under plan the recap becomes the way-forward row
310
+ // (the /mode hints are the mode's exits).
311
+ mode: getMode(),
280
312
  }));
281
313
  break;
282
314
  }
@@ -436,7 +468,10 @@ export async function chat(session, faux, input, autoCompact) {
436
468
  return;
437
469
  const ratio = estimateCtxRatio(session);
438
470
  const pct = Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
439
- dock.setStatus(`▸ ${getMode()} · /mode to switch · ${agentModel} · ctx left ~${pct}%`);
471
+ // W19: under plan the idle row makes the posture unmistakable
472
+ // the W4 parentheses idiom names the read-only constraint.
473
+ const tier = getMode() === "plan" ? "plan (read-only)" : getMode();
474
+ dock.setStatus(`▸ ${tier} · /mode to switch · ${agentModel} · ctx left ~${pct}%`);
440
475
  };
441
476
  const statusCb = (u, ctx) => {
442
477
  runUsage = u;
@@ -479,6 +514,10 @@ export async function chat(session, faux, input, autoCompact) {
479
514
  }
480
515
  dispatch(line, dispatchCtx);
481
516
  });
517
+ // W15: the expand key — the editor forwards ctrl+r; dispatch runs the
518
+ // chain action (the sentinel's control char marks the key, so a typed
519
+ // "expand" turn is never intercepted).
520
+ input.onExpand(() => dispatch("\x12expand", dispatchCtx));
482
521
  // Recovery first: a session with a dangling pause or uncertain
483
522
  // executions must resolve them BEFORE the REPL accepts new turns —
484
523
  // otherwise the interrupted run dangles while a new one starts.
package/dist/dispatch.js CHANGED
@@ -3,10 +3,10 @@
3
3
  * turns. The bodies moved verbatim from chat()'s closure; chat provides
4
4
  * the context (the chain, the run state, the prompt arming).
5
5
  */
6
- import { escapeTerminal, palette } from "@vincemakes/kiso-tui";
6
+ import { escapeTerminal, kUnit, palette } from "@vincemakes/kiso-tui";
7
7
  import { buildAdapter } from "@vincemakes/kiso-runtime";
8
8
  import { MODES, getMode, setMode } from "./mode.js";
9
- import { agentModel, body, bodyLog, configModels, setAgentModel, setCurrentModelName } from "./state.js";
9
+ import { agentModel, body, bodyLog, configModels, dock, setAgentModel, setCurrentModelName } from "./state.js";
10
10
  import { directWriteProfile, profileAvailable } from "./config.js";
11
11
  /** The ONE dispatcher — slash commands, exit, and turns. The recovery
12
12
  * replay routes through it too — a queued "/last" must never become a
@@ -66,6 +66,35 @@ export function dispatch(line, ctx) {
66
66
  });
67
67
  return;
68
68
  }
69
+ if (trimmed === "\x12expand") {
70
+ // W15: the expand key (ctrl+r) — /last aimed at a chosen cell.
71
+ // The TARGET is picked at press time: a LIVE tool cell toggles
72
+ // IMMEDIATELY in place (the compositor owns those rows and
73
+ // redraws them — the approval pause is exactly when the user
74
+ // reads a cut diff, and the key must answer then, never after
75
+ // the run). A COMMITTED cell can never toggle — history is never
76
+ // rewritten (ADR-0046) — so its expanded block and the empty
77
+ // answer queue on the chain like /last: the block lands as new
78
+ // content after any in-flight turn. The sentinel carries the
79
+ // control char so a typed "expand" turn is never intercepted.
80
+ const r = body.expandNext();
81
+ if (r.kind === "toggled") {
82
+ ctx.input.prompt(); // in place — the frame already repainted
83
+ return;
84
+ }
85
+ const land = async () => {
86
+ if (r.kind === "appended") {
87
+ for (const line of r.lines)
88
+ bodyLog(line);
89
+ }
90
+ else if (r.kind === "none") {
91
+ bodyLog("[nothing to expand]");
92
+ }
93
+ ctx.input.prompt();
94
+ };
95
+ ctx.chainRef.current = ctx.chainRef.current.then(land);
96
+ return;
97
+ }
69
98
  if (trimmed === "/status") {
70
99
  // B area: session id, durable event count, and the ~ context
71
100
  // estimate — all read straight from the live session, nothing
@@ -164,19 +193,76 @@ export function dispatch(line, ctx) {
164
193
  return;
165
194
  }
166
195
  ctx.chainRef.current = ctx.chainRef.current.then(async () => {
196
+ // W18: the compaction indicator — the whole call runs under
197
+ // a live status row (the INDETERMINATE form: the summary is
198
+ // ONE adapter call, no fraction exists — kiso never invents
199
+ // a percentage), with a REAL cancel: esc aborts the signal,
200
+ // which the runtime observes at every phase boundary.
201
+ const abort = new AbortController();
202
+ let compactCancelled = false;
203
+ const onEscape = () => {
204
+ if (abort.signal.aborted)
205
+ return;
206
+ compactCancelled = true;
207
+ abort.abort();
208
+ };
209
+ ctx.input.onEscape(onEscape);
210
+ let compactStart = 0;
211
+ let compactTimer = null;
212
+ // the `as` on the initializer keeps the flow type the full union —
213
+ // onStart fills this during the call, but a closure assignment
214
+ // never re-narrows the outer scope (it would read `never`)
215
+ let compactInfo = null;
216
+ // the ctx estimate BEFORE the summarized event lands (the used
217
+ // fraction — the recap's "ctx 91% → 34%" drops after compacting)
218
+ let ctxBefore = null;
219
+ const compacting = (info) => {
220
+ const text = (elapsed) => `▘ compacting · ${info.rounds} rounds · ~${kUnit(info.tokens)} tokens · ${Math.max(0, elapsed)}s`;
221
+ compactStart = Date.now();
222
+ ctxBefore = Math.round(ctx.estimateCtx() * 100);
223
+ dock.setStatus(text(0), "esc to cancel");
224
+ compactTimer = setInterval(() => {
225
+ dock.setStatus(text(Math.round((Date.now() - compactStart) / 1000)), "esc to cancel");
226
+ }, 1000);
227
+ };
167
228
  try {
168
- const result = await ctx.session.summarize();
229
+ const result = await ctx.session.summarize({
230
+ signal: abort.signal,
231
+ onStart: (info) => {
232
+ compactInfo = info;
233
+ compacting(info);
234
+ },
235
+ });
169
236
  if (result === null) {
170
237
  body.notice("[/compact] nothing to compact — fewer than 5 rounds yet");
171
238
  }
172
239
  else {
173
- body.notice(`[/compact] saved ~${result.savedTokens.toLocaleString("en-US")} tokens`);
240
+ // W18: the settled RECAP replaces the bare saved-token
241
+ // notice — it names what actually happened: the covered
242
+ // rounds, the one summary, the savings, the ctx drop
243
+ // (the estimate BEFORE vs AFTER — the same chars/4
244
+ // proxy the status bar shows, marked ~), and the time.
245
+ const ctxAfter = Math.round(ctx.estimateCtx() * 100);
246
+ const elapsed = compactStart > 0 ? ((Date.now() - compactStart) / 1000).toFixed(1) : "?";
247
+ // a non-null result implies onStart ran — the "?" is
248
+ // reachable only at the type level
249
+ body.notice(`[/compact] ▞ compacted · ${compactInfo?.rounds ?? "?"} rounds → 1 summary · saved ~${kUnit(result.savedTokens)} · ctx ${ctxBefore ?? "?"}% → ${ctxAfter}% · ${elapsed}s`);
174
250
  }
175
251
  }
176
252
  catch (err) {
177
253
  // Honest failure: nothing was persisted, the session
178
254
  // is unchanged (ADR-0044 crash semantics).
179
- body.notice(`[/compact] failed: ${err instanceof Error ? err.message : String(err)}`);
255
+ if (compactCancelled) {
256
+ body.notice("[/compact] cancelled — nothing was persisted");
257
+ }
258
+ else {
259
+ body.notice(`[/compact] failed: ${err instanceof Error ? err.message : String(err)}`);
260
+ }
261
+ }
262
+ finally {
263
+ if (compactTimer !== null)
264
+ clearInterval(compactTimer);
265
+ ctx.paintIdle();
180
266
  }
181
267
  ctx.input.prompt();
182
268
  });
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@
25
25
  import { readFileSync, rmSync } from "node:fs";
26
26
  import { createInterface } from "node:readline";
27
27
  import { join } from "node:path";
28
- import { Body, Editor, PROMPT as EDITOR_PROMPT, bannerLines, palette, renderSessionLine } from "@vincemakes/kiso-tui";
28
+ import { Body, Editor, bannerLines, palette, renderSessionLine } from "@vincemakes/kiso-tui";
29
29
  import { escapeTerminal } from "@vincemakes/kiso-tui";
30
30
  import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
31
31
  import { createFauxProvider } from "@vincemakes/kiso-evals";
@@ -40,24 +40,6 @@ import { resume } from "./resume.js";
40
40
  // The moved exports stay reachable from this entry — the test imports
41
41
  // (project-trust, coding-agent) never change (B4: zero assertion changes).
42
42
  export { applyProjectMerges } from "./trust-ui.js";
43
- /** The banner: the block-letter logo (design fixed). TTY only — pipes, e2e
44
- * drivers, and CI see byte-for-byte the old output; the extensions line
45
- * merges into the third row on TTY and stays a standalone line off-TTY.
46
- * v2a: the logo rows stay dim; the TAGLINE (row 2) is the blue identity
47
- * accent. */
48
- function startupBanner() {
49
- // v3 §01: the banner is block-split — three independent logo rows
50
- // (TOP / tagline / BOTTOM), then TWO info rows (version,
51
- // extensions), each truncated at the window width; < 40 columns
52
- // skips the logo. The historical `[N extensions: names]` text rides
53
- // the extensions row verbatim (the e2e assertions keep matching).
54
- const p = palette();
55
- // A pty without a winsize reports columns = 0 (not undefined) — treat
56
- // it as the default width, never as a 0-column truncation.
57
- const W = process.stdout.columns ?? 0;
58
- const rows = bannerLines(W > 0 ? W : 80, VERSION, bannerExtensionText().replace(/^ · /, ""));
59
- return `${rows.map((r) => `${p.dim}${r}${p.reset}`).join("\n")}\n`;
60
- }
61
43
  /** The v2b behavior, unchanged: readline owns the line, SIGINT, and the
62
44
  * prompt. Only ever constructed when stdin is NOT a TTY. The rl starts
63
45
  * consuming stdin at construction (main), so 'line' events are buffered
@@ -88,6 +70,10 @@ function readlineInput(rl) {
88
70
  onEscape() {
89
71
  /* readline has no bare-Esc semantics — ignored. */
90
72
  },
73
+ onExpand() {
74
+ /* readline has no ctrl+r binding — ignored (W15 rides the
75
+ * editor path only). */
76
+ },
91
77
  question(query, cb) {
92
78
  rl.question(query, cb);
93
79
  },
@@ -130,6 +116,9 @@ function editorInput(editor) {
130
116
  onEscape(cb) {
131
117
  editor.onEscape(cb);
132
118
  },
119
+ onExpand(cb) {
120
+ editor.onExpand(cb);
121
+ },
133
122
  question(query, cb) {
134
123
  editor.question(query, cb);
135
124
  },
@@ -163,8 +152,10 @@ function makeLineInput() {
163
152
  if (process.stdin.isTTY) {
164
153
  const editor = new Editor(() => (dock.active ? dock.redraw() : editor.selfRender()));
165
154
  editor.enter();
166
- const p = palette();
167
- dock.bindInput(() => editor.dockState(), `${p.bold}${EDITOR_PROMPT}${p.reset}`);
155
+ // W6: the box's prompt goes light — "› " (the box already says
156
+ // "input lives here"; the line-mode path keeps the brick ▌, so
157
+ // pipe bytes do not change)
158
+ dock.bindInput(() => editor.dockState(), "› ");
168
159
  dock.bindMenu(() => editor.menuState()); // v3 §04: the slash-command menu
169
160
  return editorInput(editor);
170
161
  }
@@ -187,11 +178,13 @@ function bannerExtensionText() {
187
178
  parts.push(`project: ${projectExtensions.map(label).join(", ")}`);
188
179
  return ` · [${total} extension${total === 1 ? "" : "s"}: ${parts.join(" · ")}]`;
189
180
  }
190
- /** E1: the startup banner line(s) — TTY: logo + merged extensions; off-TTY:
191
- * the historical `[N extensions: ...]` standalone line (zero change). */
192
- function extensionsBanner() {
181
+ /** E1: the startup banner line(s) — TTY: logo + merged extensions + the
182
+ * W5 resume list as a LIVE banner cell (W1: the tier re-derives on
183
+ * resize; the resume list re-gates with the tier); off-TTY: the
184
+ * historical `[N extensions: ...]` standalone line (zero change). */
185
+ function extensionsBanner(resume = []) {
193
186
  if (process.stdout.isTTY) {
194
- bodyLog(startupBanner());
187
+ body.banner(VERSION, bannerExtensionText().replace(/^ · /, ""), resume);
195
188
  return;
196
189
  }
197
190
  const text = bannerExtensionText();
@@ -199,6 +192,19 @@ function extensionsBanner() {
199
192
  return;
200
193
  bodyLog(`${text}\n`);
201
194
  }
195
+ /** W5: the opening-screen resume list — up to 3 recent sessions, newest
196
+ * first, the CURRENT session excluded (it is not something to pick back
197
+ * up). Every field already exists behind the store's SessionMeta (the
198
+ * `kiso sessions` line shows the same data) — only the projection and
199
+ * the sort live here. */
200
+ function recentSessions(id, agent) {
201
+ return agent
202
+ .sessions()
203
+ .filter((m) => m.id !== id)
204
+ .sort((a, b) => b.updatedAt - a.updatedAt)
205
+ .slice(0, 3)
206
+ .map(({ title, events, runs, updatedAt }) => ({ title, events, runs, updatedAt }));
207
+ }
202
208
  /**
203
209
  * A area: the coding-agent system prompt — ONE constant, byte-stable for the
204
210
  * session's lifetime (D area). Kept under ~80 lines; no template engine.
@@ -409,7 +415,7 @@ async function main() {
409
415
  applyConfigMode();
410
416
  const session = await agent.session({ id });
411
417
  bodyLog(`session ${id}\n`);
412
- extensionsBanner();
418
+ extensionsBanner(recentSessions(id, agent));
413
419
  faux = currentFaux;
414
420
  await chat(session, faux, input, resolveAutoCompact(mergedConfig));
415
421
  break;
@@ -439,7 +445,7 @@ async function main() {
439
445
  }
440
446
  case "help": {
441
447
  const p = palette();
442
- console.log(`${p.dim}${bannerLines(80, VERSION, "").join("\n")}${p.reset}\n\n` +
448
+ console.log(`${p.dim}${bannerLines(80, process.stdout.rows ?? 0, VERSION, "").join("\n")}${p.reset}\n\n` +
443
449
  "kiso — the coding agent that survives kill -9\n\n" +
444
450
  " kiso [sessionId] interactive session (default command)\n" +
445
451
  " kiso chat [sessionId] same as above\n" +
@@ -457,7 +463,7 @@ async function main() {
457
463
  const agent = await makeAgent(fauxSkip(id));
458
464
  const session = await agent.session({ id });
459
465
  bodyLog(`session ${id}\n`);
460
- extensionsBanner();
466
+ extensionsBanner(recentSessions(id, agent));
461
467
  await chat(session, faux, input, autoCompactFromEnv());
462
468
  break;
463
469
  }
package/dist/state.d.ts CHANGED
@@ -27,6 +27,9 @@ export interface LineInput {
27
27
  onSigint(cb: () => void): void;
28
28
  onEot(cb: () => void): void;
29
29
  onEscape(cb: () => void): void;
30
+ /** W15: the expand key (ctrl+r) — the chain-level action, never the
31
+ * editor's own interpretation. */
32
+ onExpand(cb: () => void): void;
30
33
  question(query: string, cb: (answer: string) => void): void;
31
34
  cancelQuestion(): void;
32
35
  emitLine(line: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "kiso CLI — the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  "@vincemakes/kiso-provider-openai": "0.1.31",
25
25
  "@vincemakes/kiso-runtime": "0.1.31",
26
26
  "@vincemakes/kiso-tools-node": "0.1.31",
27
- "@vincemakes/kiso-tui": "0.1.36"
27
+ "@vincemakes/kiso-tui": "0.1.38"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.1.2",