@vincemakes/kiso-code 0.46.1 → 0.47.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.
Files changed (43) hide show
  1. package/dist/auth/oauth/chatgpt.js +25 -8
  2. package/dist/auth/oauth/page.d.ts +25 -0
  3. package/dist/auth/oauth/page.js +44 -0
  4. package/dist/chat.d.ts +12 -1
  5. package/dist/chat.js +133 -46
  6. package/dist/coding-prompt.d.ts +7 -0
  7. package/dist/coding-prompt.js +12 -4
  8. package/dist/config.d.ts +7 -3
  9. package/dist/config.js +39 -4
  10. package/dist/create-coding-agent.js +16 -6
  11. package/dist/dispatch.d.ts +2 -0
  12. package/dist/dispatch.js +337 -83
  13. package/dist/floor.d.ts +21 -0
  14. package/dist/floor.js +35 -35
  15. package/dist/git-branch.d.ts +17 -0
  16. package/dist/git-branch.js +69 -0
  17. package/dist/in-run-compaction.d.ts +54 -0
  18. package/dist/in-run-compaction.js +44 -0
  19. package/dist/index.js +106 -45
  20. package/dist/opening.d.ts +48 -0
  21. package/dist/opening.js +43 -0
  22. package/dist/protected-writes.d.ts +8 -0
  23. package/dist/protected-writes.js +12 -0
  24. package/dist/remote-boundary.d.ts +55 -0
  25. package/dist/remote-boundary.js +217 -0
  26. package/dist/replay.d.ts +8 -1
  27. package/dist/replay.js +22 -10
  28. package/dist/resume.js +21 -16
  29. package/dist/session-cards.d.ts +2 -0
  30. package/dist/session-cards.js +3 -3
  31. package/dist/settings.d.ts +19 -0
  32. package/dist/settings.js +34 -5
  33. package/dist/skill-invoke.d.ts +11 -0
  34. package/dist/skill-invoke.js +59 -0
  35. package/dist/state.d.ts +14 -3
  36. package/dist/state.js +29 -3
  37. package/dist/task-notice.d.ts +48 -7
  38. package/dist/task-notice.js +123 -13
  39. package/dist/trust-ui.d.ts +7 -0
  40. package/dist/trust-ui.js +54 -9
  41. package/dist/window-title.d.ts +43 -23
  42. package/dist/window-title.js +78 -50
  43. package/package.json +14 -14
package/dist/floor.d.ts CHANGED
@@ -63,6 +63,27 @@ export declare function innerLines(argv: readonly LooseWord[], home: string): {
63
63
  readonly lines: readonly string[];
64
64
  readonly subshell: boolean;
65
65
  } | null;
66
+ /** How deep command lines inside command lines are followed — `sh -c`,
67
+ * `eval` — and how many are read in all: eval's two readings would
68
+ * otherwise double at every level. */
69
+ export declare const MAX_INNER_DEPTH = 16;
70
+ export declare const MAX_INNER_LINES = 256;
71
+ /** git's global options, read to the subcommand. Shared by the floor and
72
+ * the remote boundary (remote-boundary.ts), which must find the same
73
+ * subcommand the floor does: an option without a value is one word
74
+ * skipped, whatever it is, so `git --no-pager push` reads as push. */
75
+ export interface GitInvocation {
76
+ readonly sub: string | undefined;
77
+ readonly rest: readonly LooseWord[];
78
+ /** B7: several -C compose, each relative to the one before */
79
+ readonly cds: readonly LooseWord[];
80
+ /** B7: --work-tree X IS the tree a whole-tree command works on */
81
+ readonly workTree: LooseWord | undefined;
82
+ /** B7: --git-dir X is a target in itself (space and = forms both) */
83
+ readonly gitDirs: readonly LooseWord[];
84
+ }
85
+ /** `args` is git's argv without `git`. */
86
+ export declare function gitInvocation(args: readonly LooseWord[]): GitInvocation;
66
87
  /**
67
88
  * Would the floor refuse this command line? `workspaceRoot` is where the
68
89
  * shell runs it.
package/dist/floor.js CHANGED
@@ -344,8 +344,8 @@ export function innerLines(argv, home) {
344
344
  /** How deep command lines inside command lines are followed — `sh -c`,
345
345
  * `eval` — and how many are read in all: eval's two readings would
346
346
  * otherwise double at every level. */
347
- const MAX_INNER_DEPTH = 16;
348
- const MAX_INNER_LINES = 256;
347
+ export const MAX_INNER_DEPTH = 16;
348
+ export const MAX_INNER_LINES = 256;
349
349
  const SELECTING = new Set([
350
350
  "-name", "-iname", "-path", "-ipath", "-wholename", "-iwholename", "-regex", "-iregex", "-newer", "-mtime", "-mmin", "-atime",
351
351
  "-amin", "-ctime", "-cmin", "-size", "-empty", "-user", "-group", "-perm", "-samefile", "-links",
@@ -355,6 +355,38 @@ const SELECTING = new Set([
355
355
  const pathShaped = (t) => t === "." || t === "./" || t.startsWith("/") || t.startsWith("~") || t.includes("/") || t.startsWith(":") || (hostDialect().msys && (t.includes("\\") || /^[A-Za-z]:/.test(t)));
356
356
  /** B7: pathspec magic from the top of the repository */
357
357
  const topMagic = (t) => t.startsWith(":/") || t.startsWith(":(top");
358
+ /** `args` is git's argv without `git`. */
359
+ export function gitInvocation(args) {
360
+ let i = 0;
361
+ const cds = [];
362
+ let workTree;
363
+ const gitDirs = [];
364
+ while (i < args.length && args[i].text.startsWith("-")) {
365
+ const t = args[i].text;
366
+ const eq = /^--(work-tree|git-dir)=(.*)$/.exec(t);
367
+ if (eq !== null) {
368
+ const value = { ...args[i], text: eq[2], unknownAt: -1, tilde: eq[2].startsWith("~") };
369
+ if (eq[1] === "work-tree")
370
+ workTree = value;
371
+ else
372
+ gitDirs.push(value);
373
+ }
374
+ if ((t === "-C" || t === "--work-tree" || t === "--git-dir") && i + 1 < args.length) {
375
+ if (t === "-C")
376
+ cds.push(args[i + 1]);
377
+ else if (t === "--work-tree")
378
+ workTree = args[i + 1];
379
+ else
380
+ gitDirs.push(args[i + 1]);
381
+ i += 2;
382
+ }
383
+ else if (t === "-c" || t === "--namespace" || t === "--exec-path")
384
+ i += 2;
385
+ else
386
+ i += 1;
387
+ }
388
+ return { sub: args[i]?.text, rest: args.slice(i + 1), cds, workTree, gitDirs };
389
+ }
358
390
  /** The destructive command's targets, or null when it is not destructive. */
359
391
  function destructiveTargets(argv) {
360
392
  const name = basename(argv[0]?.text ?? "");
@@ -398,39 +430,7 @@ function destructiveTargets(argv) {
398
430
  }
399
431
  if (name !== "git")
400
432
  return null;
401
- let i = 0;
402
- // B7: several -C compose, each relative to the one before
403
- const cds = [];
404
- // B7: --work-tree X IS the tree a whole-tree command works on; --git-dir
405
- // X is a target in itself (space and = forms both)
406
- let workTree;
407
- const gitDirs = [];
408
- while (i < args.length && args[i].text.startsWith("-")) {
409
- const t = args[i].text;
410
- const eq = /^--(work-tree|git-dir)=(.*)$/.exec(t);
411
- if (eq !== null) {
412
- const value = { ...args[i], text: eq[2], unknownAt: -1, tilde: eq[2].startsWith("~") };
413
- if (eq[1] === "work-tree")
414
- workTree = value;
415
- else
416
- gitDirs.push(value);
417
- }
418
- if ((t === "-C" || t === "--work-tree" || t === "--git-dir") && i + 1 < args.length) {
419
- if (t === "-C")
420
- cds.push(args[i + 1]);
421
- else if (t === "--work-tree")
422
- workTree = args[i + 1];
423
- else
424
- gitDirs.push(args[i + 1]);
425
- i += 2;
426
- }
427
- else if (t === "-c" || t === "--namespace" || t === "--exec-path")
428
- i += 2;
429
- else
430
- i += 1;
431
- }
432
- const sub = args[i]?.text;
433
- const rest = args.slice(i + 1);
433
+ const { sub, rest, cds, workTree, gitDirs } = gitInvocation(args);
434
434
  const flags = new Set(rest.filter((a) => a.text.startsWith("-")).map((a) => a.text));
435
435
  const as = (targets) => ({
436
436
  targets: [...targets.map((t) => (t === "repo" && workTree !== undefined ? workTree : t)), ...gitDirs],
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Graphite §8.9 — the branch the status bar names.
3
+ *
4
+ * Read from `.git/HEAD`, walking up from the working directory — no git
5
+ * process is spawned, because the status bar paints on every idle frame.
6
+ * A `.git` FILE (a worktree, a submodule) points at the real git
7
+ * directory with `gitdir: <path>`, and that is followed. A detached HEAD
8
+ * shows its short sha. Outside a repository, or on any read error, there
9
+ * is no branch and the bar says nothing about one.
10
+ *
11
+ * The answer is cached per HEAD file and re-read only when HEAD's mtime
12
+ * moves — a checkout the model runs mid-session is picked up on the next
13
+ * paint, and an idle session reads one stat per paint.
14
+ */
15
+ /** What a HEAD file's contents name: the branch, or a detached sha's head. */
16
+ export declare function branchOf(head: string): string | null;
17
+ export declare function currentBranch(dir: string): string | null;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Graphite §8.9 — the branch the status bar names.
3
+ *
4
+ * Read from `.git/HEAD`, walking up from the working directory — no git
5
+ * process is spawned, because the status bar paints on every idle frame.
6
+ * A `.git` FILE (a worktree, a submodule) points at the real git
7
+ * directory with `gitdir: <path>`, and that is followed. A detached HEAD
8
+ * shows its short sha. Outside a repository, or on any read error, there
9
+ * is no branch and the bar says nothing about one.
10
+ *
11
+ * The answer is cached per HEAD file and re-read only when HEAD's mtime
12
+ * moves — a checkout the model runs mid-session is picked up on the next
13
+ * paint, and an idle session reads one stat per paint.
14
+ */
15
+ import { readFileSync, statSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ const cache = new Map();
18
+ /** The HEAD file of the repository `dir` is inside, or null. */
19
+ function headFile(dir) {
20
+ let d = resolve(dir);
21
+ for (;;) {
22
+ const dotGit = join(d, ".git");
23
+ try {
24
+ const st = statSync(dotGit);
25
+ if (st.isDirectory())
26
+ return join(dotGit, "HEAD");
27
+ if (st.isFile()) {
28
+ const m = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, "utf8"));
29
+ if (m !== null)
30
+ return join(resolve(d, m[1].trim()), "HEAD");
31
+ return null;
32
+ }
33
+ }
34
+ catch {
35
+ // no .git here — keep walking
36
+ }
37
+ const up = dirname(d);
38
+ if (up === d)
39
+ return null;
40
+ d = up;
41
+ }
42
+ }
43
+ /** What a HEAD file's contents name: the branch, or a detached sha's head. */
44
+ export function branchOf(head) {
45
+ const text = head.trim();
46
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(text);
47
+ if (ref !== null)
48
+ return ref[1];
49
+ if (/^[0-9a-f]{7,64}$/i.test(text))
50
+ return text.slice(0, 7);
51
+ return null;
52
+ }
53
+ export function currentBranch(dir) {
54
+ try {
55
+ const head = headFile(dir);
56
+ if (head === null)
57
+ return null;
58
+ const mtimeMs = statSync(head).mtimeMs;
59
+ const hit = cache.get(head);
60
+ if (hit !== undefined && hit.mtimeMs === mtimeMs)
61
+ return hit.branch;
62
+ const branch = branchOf(readFileSync(head, "utf8"));
63
+ cache.set(head, { mtimeMs, branch });
64
+ return branch;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The compaction round (owner, 2026-10-06) — a summary call inside a run,
3
+ * as the screen sees it.
4
+ *
5
+ * The runtime tells the CLI when an in-run summary starts, how far its
6
+ * output has come, and how it ended (`contextPolicy.tiers.onSummary`). The
7
+ * running row reads `current()` on every repaint: while a summary is in
8
+ * flight it is the `compacting · auto` row `/compact` draws, not `working`
9
+ * with a clock that runs while nothing arrives. When the summary is kept,
10
+ * its sizes wait here for the `summarized` event, whose COMPACTED row says
11
+ * them (`takeKept`).
12
+ */
13
+ import type { CompactingProgress } from "@vincemakes/kiso-tui";
14
+ export interface InRunCompaction {
15
+ /** when the summary call started (ms) */
16
+ readonly since: number;
17
+ /** the covered user rounds and their estimated tokens */
18
+ readonly rounds: number;
19
+ readonly tokens: number;
20
+ /** the latest report against the output budget; null before the first */
21
+ progress: CompactingProgress | null;
22
+ }
23
+ type SummaryEvent = {
24
+ readonly phase: "start";
25
+ readonly reason: string;
26
+ readonly info: {
27
+ readonly rounds: number;
28
+ readonly tokens: number;
29
+ };
30
+ } | {
31
+ readonly phase: "progress";
32
+ readonly progress: {
33
+ readonly produced: number;
34
+ readonly budget: number | null;
35
+ readonly reasoningUnseen: boolean;
36
+ };
37
+ } | {
38
+ readonly phase: "end";
39
+ readonly outcome: "kept" | "discarded" | "failed";
40
+ readonly pre: number;
41
+ readonly post: number;
42
+ };
43
+ /** The runtime's callback (create-coding-agent passes it to the tiers). */
44
+ export declare function onInRunSummary(event: SummaryEvent): void;
45
+ /** The summary in flight, or null — what the running row draws. */
46
+ export declare function current(): InRunCompaction | null;
47
+ /** The kept summary's sizes, once: the `summarized` event's row says them. */
48
+ export declare function takeKept(): {
49
+ readonly pre: number;
50
+ readonly post: number;
51
+ } | null;
52
+ /** A run that ends mid-summary (an abort) leaves nothing behind. */
53
+ export declare function clearInRunSummary(): void;
54
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The compaction round (owner, 2026-10-06) — a summary call inside a run,
3
+ * as the screen sees it.
4
+ *
5
+ * The runtime tells the CLI when an in-run summary starts, how far its
6
+ * output has come, and how it ended (`contextPolicy.tiers.onSummary`). The
7
+ * running row reads `current()` on every repaint: while a summary is in
8
+ * flight it is the `compacting · auto` row `/compact` draws, not `working`
9
+ * with a clock that runs while nothing arrives. When the summary is kept,
10
+ * its sizes wait here for the `summarized` event, whose COMPACTED row says
11
+ * them (`takeKept`).
12
+ */
13
+ let live = null;
14
+ let kept = null;
15
+ /** The runtime's callback (create-coding-agent passes it to the tiers). */
16
+ export function onInRunSummary(event) {
17
+ if (event.phase === "start") {
18
+ live = { since: Date.now(), rounds: event.info.rounds, tokens: event.info.tokens, progress: null };
19
+ kept = null;
20
+ }
21
+ else if (event.phase === "progress") {
22
+ if (live !== null)
23
+ live.progress = { produced: event.progress.produced, budget: event.progress.budget, reasoningUnseen: event.progress.reasoningUnseen };
24
+ }
25
+ else {
26
+ live = null;
27
+ kept = event.outcome === "kept" ? { pre: event.pre, post: event.post } : null;
28
+ }
29
+ }
30
+ /** The summary in flight, or null — what the running row draws. */
31
+ export function current() {
32
+ return live;
33
+ }
34
+ /** The kept summary's sizes, once: the `summarized` event's row says them. */
35
+ export function takeKept() {
36
+ const k = kept;
37
+ kept = null;
38
+ return k;
39
+ }
40
+ /** A run that ends mid-summary (an abort) leaves nothing behind. */
41
+ export function clearInRunSummary() {
42
+ live = null;
43
+ kept = null;
44
+ }
package/dist/index.js CHANGED
@@ -30,9 +30,9 @@ import { createInterface } from "node:readline";
30
30
  import { Readable } from "node:stream";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import { basename, join } from "node:path";
33
- import { Body, Editor, PROMPT, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListHeader, sessionListRow, sessionListUnknownLine, slashCommandNames } from "@vincemakes/kiso-tui";
33
+ import { Body, Editor, PROMPT, bannerLines, currentGround, currentGroundRgb, parseOscColor, resolveGround, setGround, escapeTerminal, extensionsBannerText, extensionsFact, idColumn, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListHeader, sessionListRow, sessionListUnknownLine, slashCommandNames } from "@vincemakes/kiso-tui";
34
34
  import { disposeExtensions, SessionStore } from "@vincemakes/kiso-runtime";
35
- import { listSessionSidecars, migrateSummaries, readProfile, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
35
+ import { listSessionSidecars, migrateSummaries, readProfile, readSessionName, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
36
36
  import { skillMenuItems } from "./skill-invoke.js";
37
37
  import { canonicalPath, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
38
38
  import { reverseMigration } from "./session-migration.js";
@@ -43,15 +43,18 @@ import { isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemak
43
43
  import { OFFERED_MODES, applyModeState, envModeLayer, getDontAsk, modeDisplay, parseMode, resolveModeLayers } from "./mode.js";
44
44
  import { activeStoreDir, setActiveStoreDir, agentModel, atFiles, body, bodyLog, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
45
45
  import { fauxSkip, readFauxScript } from "./faux-glue.js";
46
- import { chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
46
+ import { barFor, chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
47
47
  import { preferences, usePreferences } from "./preferences.js";
48
- import { settingsLayers, setChildTurnBudget, stopAllTasks } from "./state.js";
48
+ import { settingsLayers, setChildTurnBudget, stopAllTasks, tasksFor } from "./state.js";
49
+ import { tasksForDisplay } from "./task-notice.js";
49
50
  import { finishChild } from "./child-result.js";
50
51
  import { loadUserConfig, resolveAutoCompact } from "./config.js";
51
52
  import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
52
53
  import { tmuxMouseHint } from "./tmux-hint.js";
53
54
  import { resume } from "./resume.js";
54
- import { paintWindowTitle } from "./window-title.js";
55
+ import { paintWindowTitle, setTitleName, setTitleState } from "./window-title.js";
56
+ import { projectInstructions } from "./coding-prompt.js";
57
+ import { openingFacts } from "./opening.js";
55
58
  import { resumeTail } from "./resume-tail.js";
56
59
  import { replayInto } from "./replay.js";
57
60
  import { armByteTrace } from "./byte-trace.js";
@@ -316,6 +319,15 @@ function editorInput(editor) {
316
319
  panelCancel() {
317
320
  editor.cancelPanel();
318
321
  },
322
+ // Graphite R3e: a read-only sheet over the input (`/status`,
323
+ // `/context`, `/skills`)
324
+ openSheet(rows) {
325
+ editor.openSheet(rows);
326
+ },
327
+ // the sheets round: `/help` opens the command list
328
+ openCommands() {
329
+ editor.openCommands();
330
+ },
319
331
  // TUI2-R2 ②: the session picker — the editor owns the keys (the
320
332
  // selection walk, the filter, enter/esc), the compositor draws the
321
333
  // band, and the id comes back here.
@@ -410,20 +422,30 @@ function makeLineInput() {
410
422
  let osc;
411
423
  let colorScheme;
412
424
  const theme = () => process.env.KISO_THEME ?? userTheme;
425
+ const sameRgb = (a, b) => a === b || (a !== null && b !== null && a.r === b.r && a.g === b.g && a.b === b.b);
413
426
  const rewalk = () => {
414
427
  const next = resolveGround({ theme: theme(), colorScheme, osc, colorfgbg: process.env.COLORFGBG });
428
+ // Graphite §3.4: the colour the terminal reported rides along,
429
+ // whichever rung decided the ground — the surfaces are derived
430
+ // from it when it is of the same kind (graphite.ts decides).
431
+ const rgb = osc === undefined ? null : parseOscColor(osc);
415
432
  // DC-3/DC-14's model: the first frame never waits for a reply.
416
433
  // A reply that changes nothing repaints nothing — which is also
417
434
  // why two answers that AGREE cost one repaint and not two.
418
- if (next === currentGround())
435
+ if (next === currentGround() && sameRgb(rgb, currentGroundRgb()))
419
436
  return;
420
- setGround(next);
437
+ setGround(next, rgb);
421
438
  body.onGroundChange();
422
439
  };
423
440
  setGround(resolveGround({ theme: theme(), colorfgbg: process.env.COLORFGBG }));
424
441
  // the reply, when there is one, re-walks the ladder WITH it — and
425
442
  // `theme` goes in first again, so an explicit answer still wins.
426
443
  editor.onOsc((reply) => {
444
+ // Only the answer to the question asked (OSC 11, the background
445
+ // colour) is the ground: any other OSC a terminal sends would
446
+ // otherwise overwrite it.
447
+ if (!reply.startsWith("11;"))
448
+ return;
427
449
  osc = reply;
428
450
  rewalk();
429
451
  });
@@ -455,6 +477,9 @@ function makeLineInput() {
455
477
  // OR-11 (a): ONE literal. The compositor draws this lead and the
456
478
  // editor measures its rows against it; two copies is how they came
457
479
  // to disagree by two columns in the first place.
480
+ // Graphite §7.8 (owner, 2026-09-29): no prompt glyph — R2's ruling
481
+ // stands again after R1c's `›`; the caret is the terminal's own
482
+ // ink, at column 0, as in 0.44.
458
483
  const COMPOSER_LEAD = "";
459
484
  dock.bindInput(() => editor.dockState(), COMPOSER_LEAD);
460
485
  // …and BOTH renderers are live: the dock draws this row while it is
@@ -470,7 +495,7 @@ function makeLineInput() {
470
495
  editor.bindAtItems(atFiles); // KC3 §5: the file source — listed per OPEN
471
496
  dock.bindAt(() => editor.atState()); // KC3 §4: the picker's band
472
497
  dock.bindApproval(() => editor.panelState()); // W21: the panel's bound state
473
- dock.bindSheet(() => editor.sheetOpen()); // TUI2-R1 (D): the ? keys sheet
498
+ dock.bindSheet(() => editor.sheetContent()); // TUI2-R1 (D): the ? keys sheet; R3e: /status
474
499
  dock.bindPick(() => editor.pickState()); // TUI2-R2 ②: the resume picker's band
475
500
  // R5: the transcript viewer. The editor reports whether it is up and
476
501
  // forwards the commands; the STATE lives in the compositor, because
@@ -496,11 +521,15 @@ function bannerExtensionText() {
496
521
  // terminal layer (extensionsBannerText — a pure function of the three
497
522
  // name lists, including the "(connecting…)" in-flight label). Which
498
523
  // lists exist is the CLI's fact and stays here.
499
- // 0.40.0: with the don't-ask switch on the ask extension is loaded but
500
- // offers no tool (builtin.ts offInDontAsk) — the banner says so, rather
501
- // than listing it as if it could ask.
524
+ return extensionsBannerText(...extensionLists());
525
+ }
526
+ /** The three lists both forms are built from — the pipe's line and the
527
+ * opening's EXTENSIONS row. 0.40.0: with the don't-ask switch on the ask
528
+ * extension is loaded but offers no tool (builtin.ts offInDontAsk) — the
529
+ * banner says so, rather than listing it as if it could ask. */
530
+ function extensionLists() {
502
531
  const builtIn = builtInExtensions.map((e) => (e.name === "ask" && getDontAsk() ? { name: e.name, note: "off in dontAsk" } : e));
503
- return extensionsBannerText(builtIn, userExtensions, projectExtensions);
532
+ return [builtIn, userExtensions, projectExtensions];
504
533
  }
505
534
  /** E1: the startup banner line(s) — TTY: logo + merged extensions + the
506
535
  * W5 resume list as a LIVE banner cell (W1: the tier re-derives on
@@ -549,19 +578,15 @@ async function announceUpdate() {
549
578
  // did, a version check is not a reason to disturb a session.
550
579
  }
551
580
  }
552
- function extensionsBanner(resume = []) {
581
+ function extensionsBanner(sessionId, resumedEvents = 0) {
553
582
  const text = bannerExtensionText();
554
583
  if (!process.stdout.isTTY) {
555
584
  if (text !== "")
556
585
  bodyLog(`${text}\n`);
557
586
  return;
558
587
  }
559
- // R2: the opening answers the three questions a first screen is asked.
560
- // The model and the tier come from the same state the status line reads
561
- // (one source, so the two can never disagree); the workspace is the
562
- // home-relative cwd, because `~/Desktop/devv/kiso` is what a human
563
- // calls the place and `/Users/vinve/Desktop/devv/kiso` is what a
564
- // filesystem calls it.
588
+ // Graphite §7.10: the opening states what loaded. The model, the mode
589
+ // and the folder are the status bar's now (§8.9) and are not repeated.
565
590
  const home = homedir();
566
591
  const cwd = process.cwd();
567
592
  /** DC-49: realpath, falling back to the raw path when it cannot be
@@ -575,15 +600,26 @@ function extensionsBanner(resume = []) {
575
600
  return dir;
576
601
  }
577
602
  };
578
- body.banner(VERSION, text.replace(/^ · /, ""), resume, {
579
- model: agentModel,
580
- mode: modeDisplay(),
581
- cwd: cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd,
582
- // DC-49 — REALPATH on both sides. A symlinked HOME (or a symlinked
583
- // cwd) compares unequal as raw strings while being the same
584
- // directory, and the row would then be absent exactly where it is
585
- // most needed.
586
- homeWorkspace: realOf(cwd) === realOf(home),
603
+ const skills = loadedSkillsCatalog();
604
+ const mcp = loadedExtensions.find((e) => e.name === "mcp");
605
+ body.banner(VERSION, text.replace(/^ · /, ""), [], {
606
+ resumed: resumedEvents > 0,
607
+ facts: openingFacts({
608
+ // the last sweep: the new rows are the dock's; off one the
609
+ // `session <id>` and `[faux mode — …]` lines still print
610
+ sessionId: dock.active ? sessionId : null,
611
+ resumedEvents,
612
+ faux: dock.active && currentFaux,
613
+ rules: projectInstructions(cwd, protectedFiles())?.name ?? null,
614
+ skills: skills === null ? null : { count: skills.entries.length, broken: skills.broken.length },
615
+ mcp: mcp === undefined ? null : { tools: (mcp.tools ?? []).map((t) => t.name), connecting: mcp.connecting === true },
616
+ extensions: dock.active ? extensionsFact(...extensionLists()) : text === "" ? null : { value: text.replace(/^ · /, "") },
617
+ // DC-49 — REALPATH on both sides. A symlinked HOME (or a symlinked
618
+ // cwd) compares unequal as raw strings while being the same
619
+ // directory, and the row would then be absent exactly where it is
620
+ // most needed.
621
+ homeWorkspace: realOf(cwd) === realOf(home),
622
+ }),
587
623
  });
588
624
  }
589
625
  export { workspaceRoot } from "./state.js";
@@ -814,9 +850,10 @@ function bindRestoredSession(session) {
814
850
  function paintBootStatus(session) {
815
851
  if (!dock.active)
816
852
  return;
817
- // DF-0330-F1: the BOOT row gets the budget too — it is the same row, and
818
- // the opening screen is where a long model id is first seen.
819
- dock.setStatus(idleStatus(modeDisplay(), statusModelLabel(session), displayCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
853
+ // Graphite §8.9: the boot row is the status bar — the same builder the
854
+ // idle and running rows use (it used to drop `floor off`, the one fact
855
+ // the boot row most needs when it is true).
856
+ dock.setBar(barFor(session));
820
857
  }
821
858
  /** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
822
859
  * main's finally still runs (dock teardown, agent close, temp cleanup)
@@ -933,7 +970,7 @@ async function reloadAgent(old, id, input, announce = true) {
933
970
  delete process.env.KISO_DELEGATION_CONFIG_JSON;
934
971
  else
935
972
  process.env.KISO_DELEGATION_CONFIG_JSON = oldCfg.delegation;
936
- bodyLog(`[reload] ${err instanceof Error ? err.message : String(err)} — nothing changed, the previous set is still in force`);
973
+ body.notice(`[reload] ${err instanceof Error ? err.message : String(err)} — nothing changed, the previous set is still in force`);
937
974
  return old;
938
975
  }
939
976
  // The new set is built and sound; only now does the old one go.
@@ -948,7 +985,7 @@ async function reloadAgent(old, id, input, announce = true) {
948
985
  }
949
986
  }
950
987
  if (announce)
951
- bodyLog(`[reload] ${loadedExtensions.length} extensions, ${skillCount()} skills — the conversation is unchanged`);
988
+ body.notice(`[reload] ${loadedExtensions.length} extensions, ${skillCount()} skills — the conversation is unchanged`);
952
989
  return next;
953
990
  }
954
991
  async function chatLoop(first, firstId, input, autoCompact) {
@@ -1038,13 +1075,17 @@ async function chatLoop(first, firstId, input, autoCompact) {
1038
1075
  refused = false; // back in the session that was already on screen
1039
1076
  }
1040
1077
  else if (prev === null) {
1041
- bodyLog(`session ${id}\n`);
1078
+ // the last sweep (owner, 2026-10-06): on the terminal the opening's
1079
+ // SESSION row names the id — this line stood above the opening at
1080
+ // column 0. A pipe keeps it.
1081
+ if (!dock.active)
1082
+ bodyLog(`session ${id}\n`);
1042
1083
  // REL-0152-D5: a session with history says what that history WAS.
1043
1084
  // Resuming used to print this one line and drop you at an empty
1044
1085
  // prompt inside a conversation with thousands of events — the
1045
1086
  // durable log was right there and none of it was shown. Empty for
1046
1087
  // a fresh session, so `kiso chat` is byte-identical.
1047
- showResumeTail(session.log.all);
1088
+ showResumeTail(session.log.all, session.id);
1048
1089
  // R2 (owner, 2026-08-27): the resume list is NOT on the opening
1049
1090
  // screen. `/resume` is where you go looking for a session; the
1050
1091
  // opening's job is to say what THIS one is.
@@ -1053,7 +1094,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
1053
1094
  // recent sessions just to draw a badge, and `agent.session()`
1054
1095
  // throws on profile drift — so one drifted session anywhere in the
1055
1096
  // history stopped kiso from starting at all.
1056
- extensionsBanner();
1097
+ extensionsBanner(id, session.log.all.length);
1057
1098
  // OR-9 (owner, 2026-09-09): the update card under the banner, at
1058
1099
  // EVERY start while a newer version is known — §7.10's once-only
1059
1100
  // line is superseded. What the cache knows is painted NOW, with
@@ -1075,8 +1116,8 @@ async function chatLoop(first, firstId, input, autoCompact) {
1075
1116
  void announceUpdate();
1076
1117
  }
1077
1118
  else {
1078
- bodyLog(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
1079
- showResumeTail(session.log.all);
1119
+ body.notice(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
1120
+ showResumeTail(session.log.all, session.id);
1080
1121
  if (currentFaux)
1081
1122
  session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
1082
1123
  }
@@ -1105,7 +1146,9 @@ async function chatLoop(first, firstId, input, autoCompact) {
1105
1146
  // lines above are here: this is the one step all three entry points
1106
1147
  // share (first start, `/resume <id>`, a switch), so a tab can never
1107
1148
  // be left naming the session the user just left.
1108
- paintWindowTitle(session.log.all);
1149
+ // Graphite R3d: the session's own name, when the person gave it one
1150
+ setTitleName(readSessionName(activeStoreDir, session.id));
1151
+ paintWindowTitle();
1109
1152
  const nav = {
1110
1153
  // 0.40.0 dogfood: the ids only — agent.sessions() read every log whole
1111
1154
  // (seconds on the owner's 118 sessions) before /resume could open
@@ -1188,10 +1231,12 @@ async function readSecret(prompt) {
1188
1231
  * there is no viewer and no cell renderer, so the plain text tail stays
1189
1232
  * (DC-51: one call, one cell).
1190
1233
  */
1191
- function showResumeTail(events) {
1234
+ function showResumeTail(events, sessionId) {
1192
1235
  const W = process.stdout.columns ?? 80;
1236
+ // the tasks round: a task notice in the history reads its task's journal
1237
+ // (Amendment 8: a lost task's row names what it ran — from the same list)
1193
1238
  if (dock.active)
1194
- replayInto(body, events, W);
1239
+ replayInto(body, events, W, tasksForDisplay(tasksFor(sessionId)));
1195
1240
  else
1196
1241
  bodyLog(resumeTail(events, W).join("\n"));
1197
1242
  }
@@ -1508,6 +1553,10 @@ async function main() {
1508
1553
  const id = arg ?? newSessionId(sessionsDir());
1509
1554
  // v2b: the dock (TTY only) wraps the whole session — the
1510
1555
  // trust question, the banner, the body, and the input line.
1556
+ // the tasks round (owner, 2026-10-06): the first frames, before the
1557
+ // bar is bound, offer no key ladder (§7.8 retired it) — set while
1558
+ // the dock is not yet up, so nothing is drawn until enter() paints
1559
+ dock.setStatus("", "");
1511
1560
  dock.enter();
1512
1561
  // E area: a resumed session continues the script at its durable
1513
1562
  // position — never restarts it (fauxSkip).
@@ -1532,6 +1581,10 @@ async function main() {
1532
1581
  // argv[4] is the optional prompt; argv[3] is the session id
1533
1582
  // (argv = [node, script, resume, id, prompt?]).
1534
1583
  const prompt = process.argv[4];
1584
+ // the tasks round (owner, 2026-10-06): the first frames, before the
1585
+ // bar is bound, offer no key ladder (§7.8 retired it) — set while
1586
+ // the dock is not yet up, so nothing is drawn until enter() paints
1587
+ dock.setStatus("", "");
1535
1588
  dock.enter();
1536
1589
  const routeLine = arg !== undefined ? enterRouted(arg) : null;
1537
1590
  agent = await createCodingAgent(arg, input, modelFlag);
@@ -1575,7 +1628,7 @@ async function main() {
1575
1628
  session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
1576
1629
  // REL-0152-D5 — the same tail on the explicit-id form. NOT on
1577
1630
  // the -p path above: that one's stdout is a machine's input.
1578
- showResumeTail(session.log.all);
1631
+ showResumeTail(session.log.all, session.id);
1579
1632
  await resume(session, prompt, faux, input);
1580
1633
  break;
1581
1634
  }
@@ -1786,8 +1839,9 @@ async function main() {
1786
1839
  // stopped, so a new user could never learn from the tool
1787
1840
  // itself how to hand it a key.
1788
1841
  const p = palette();
1789
- console.log(`${p.dim}${bannerLines(80, process.stdout.rows ?? 0, VERSION, "").join("\n")}${p.reset}\n\n` +
1790
- "kiso — the coding agent that survives kill -9\n\n" +
1842
+ console.log(
1843
+ // Graphite §7.10: the opening's head says the tagline already
1844
+ `${p.dim}${bannerLines(80, process.stdout.rows ?? 0, VERSION, "").join("\n")}${p.reset}\n\n` +
1791
1845
  " kiso [sessionId] interactive session (default command)\n" +
1792
1846
  " kiso chat [sessionId] same as above\n" +
1793
1847
  " kiso resume pick a session to continue (TTY picker)\n" +
@@ -1818,6 +1872,10 @@ async function main() {
1818
1872
  // A area: no subcommand (or any non-command first argument) IS
1819
1873
  // chat — the first argument is the session id.
1820
1874
  const id = command ?? newSessionId(sessionsDir());
1875
+ // the tasks round (owner, 2026-10-06): the first frames, before the
1876
+ // bar is bound, offer no key ladder (§7.8 retired it) — set while
1877
+ // the dock is not yet up, so nothing is drawn until enter() paints
1878
+ dock.setStatus("", "");
1821
1879
  dock.enter();
1822
1880
  // R-I-p2 (finding R-I-p-2): the bare command passes the SAME
1823
1881
  // input source and model flag as chat/resume — the pre-patch
@@ -1856,8 +1914,11 @@ async function main() {
1856
1914
  // scroll region, the cursor lands at the input line, no broken
1857
1915
  // terminal (kill -9 excepted; `reset` saves it).
1858
1916
  dock.exit();
1917
+ // Graphite §8.10: a closed session never leaves a working or
1918
+ // waiting mark in the tab
1919
+ setTitleState("ready");
1859
1920
  for (const id of tasksAtExit.unconfirmed)
1860
- console.error(`${id}: stop unconfirmed — outcome unknown`);
1921
+ console.error(`${id}: stop unconfirmed — it may still be running`);
1861
1922
  if (tasksAtExit.left.length > 0)
1862
1923
  console.error(`left running: ${tasksAtExit.left.join(", ")} — reopen this session to hear how ${tasksAtExit.left.length === 1 ? "it ends" : "they end"}`);
1863
1924
  // finding #8 (P1): extension dispose runs on the same exit path — a