@vincemakes/kiso-code 0.46.2 → 0.47.1

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.
@@ -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, taskWhatOf } 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,7 +1075,11 @@ 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
@@ -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,7 +1116,7 @@ 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`);
1119
+ body.notice(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
1079
1120
  showResumeTail(session.log.all, session.id);
1080
1121
  if (currentFaux)
1081
1122
  session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
@@ -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
@@ -1190,9 +1233,10 @@ async function readSecret(prompt) {
1190
1233
  */
1191
1234
  function showResumeTail(events, sessionId) {
1192
1235
  const W = process.stdout.columns ?? 80;
1193
- // Amendment 8: a lost task's replayed row names what it ran
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)
1194
1238
  if (dock.active)
1195
- replayInto(body, events, W, taskWhatOf(sessionId));
1239
+ replayInto(body, events, W, tasksForDisplay(tasksFor(sessionId)));
1196
1240
  else
1197
1241
  bodyLog(resumeTail(events, W).join("\n"));
1198
1242
  }
@@ -1509,6 +1553,10 @@ async function main() {
1509
1553
  const id = arg ?? newSessionId(sessionsDir());
1510
1554
  // v2b: the dock (TTY only) wraps the whole session — the
1511
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("", "");
1512
1560
  dock.enter();
1513
1561
  // E area: a resumed session continues the script at its durable
1514
1562
  // position — never restarts it (fauxSkip).
@@ -1533,6 +1581,10 @@ async function main() {
1533
1581
  // argv[4] is the optional prompt; argv[3] is the session id
1534
1582
  // (argv = [node, script, resume, id, prompt?]).
1535
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("", "");
1536
1588
  dock.enter();
1537
1589
  const routeLine = arg !== undefined ? enterRouted(arg) : null;
1538
1590
  agent = await createCodingAgent(arg, input, modelFlag);
@@ -1787,8 +1839,9 @@ async function main() {
1787
1839
  // stopped, so a new user could never learn from the tool
1788
1840
  // itself how to hand it a key.
1789
1841
  const p = palette();
1790
- console.log(`${p.dim}${bannerLines(80, process.stdout.rows ?? 0, VERSION, "").join("\n")}${p.reset}\n\n` +
1791
- "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` +
1792
1845
  " kiso [sessionId] interactive session (default command)\n" +
1793
1846
  " kiso chat [sessionId] same as above\n" +
1794
1847
  " kiso resume pick a session to continue (TTY picker)\n" +
@@ -1819,6 +1872,10 @@ async function main() {
1819
1872
  // A area: no subcommand (or any non-command first argument) IS
1820
1873
  // chat — the first argument is the session id.
1821
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("", "");
1822
1879
  dock.enter();
1823
1880
  // R-I-p2 (finding R-I-p-2): the bare command passes the SAME
1824
1881
  // input source and model flag as chat/resume — the pre-patch
@@ -1857,6 +1914,9 @@ async function main() {
1857
1914
  // scroll region, the cursor lands at the input line, no broken
1858
1915
  // terminal (kill -9 excepted; `reset` saves it).
1859
1916
  dock.exit();
1917
+ // Graphite §8.10: a closed session never leaves a working or
1918
+ // waiting mark in the tab
1919
+ setTitleState("ready");
1860
1920
  for (const id of tasksAtExit.unconfirmed)
1861
1921
  console.error(`${id}: stop unconfirmed — it may still be running`);
1862
1922
  if (tasksAtExit.left.length > 0)
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Graphite §7.10 — what the opening says loaded: the session, the
3
+ * project's rules, the skills, the MCP servers, and the extensions. The
4
+ * model, the mode and the folder are the status bar's (§8.9) and are not
5
+ * repeated here — except the faux model, which the bar names but cannot
6
+ * say how to leave.
7
+ *
8
+ * Pure over what the CLI already holds at startup; the banner lays the
9
+ * facts out and knows nothing of where they came from.
10
+ */
11
+ import type { BannerFact } from "@vincemakes/kiso-tui";
12
+ export interface OpeningInputs {
13
+ /** The session's id — the SESSION row names it (the last sweep: the
14
+ * `session <id>` line above the opening retired on a dock). Null off a
15
+ * dock, where that line still prints: the row keeps its old form. */
16
+ readonly sessionId: string | null;
17
+ /** The events a resumed session opened with; 0 for a new one. */
18
+ readonly resumedEvents: number;
19
+ /** No model resolved and on a dock: the scripted faux model answers
20
+ * (off a dock the `[faux mode — …]` line still prints). */
21
+ readonly faux: boolean;
22
+ /** The instruction file the prompt reads (`projectInstructions`), or null. */
23
+ readonly rules: string | null;
24
+ /** The skills catalog's size, or null when no skills extension loaded. */
25
+ readonly skills: {
26
+ readonly count: number;
27
+ readonly broken: number;
28
+ } | null;
29
+ /** The MCP extension's tools as they stand, or null when it is not loaded. */
30
+ readonly mcp: {
31
+ readonly tools: readonly string[];
32
+ readonly connecting: boolean;
33
+ } | null;
34
+ /** The extensions — `extensionsFact`'s count and names, from the SAME
35
+ * three lists the pipe's `[N extensions: …]` line is built from (so the
36
+ * two still cannot disagree about what loaded, and the 0.40.0 `ask (off
37
+ * in dontAsk)` note stays beside the tier that turns it off); null when
38
+ * nothing loaded. The last sweep (owner, 2026-10-06): the row carried
39
+ * the pipe's bracketed line verbatim — as it still does off a dock
40
+ * (`{ value: <the line> }`). */
41
+ readonly extensions: {
42
+ readonly value: string;
43
+ readonly note?: string;
44
+ } | null;
45
+ /** DC-49 — the workspace is the home directory. */
46
+ readonly homeWorkspace: boolean;
47
+ }
48
+ export declare function openingFacts(o: OpeningInputs): BannerFact[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Graphite §7.10 — what the opening says loaded: the session, the
3
+ * project's rules, the skills, the MCP servers, and the extensions. The
4
+ * model, the mode and the folder are the status bar's (§8.9) and are not
5
+ * repeated here — except the faux model, which the bar names but cannot
6
+ * say how to leave.
7
+ *
8
+ * Pure over what the CLI already holds at startup; the banner lays the
9
+ * facts out and knows nothing of where they came from.
10
+ */
11
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
12
+ export function openingFacts(o) {
13
+ const facts = [];
14
+ if (o.sessionId === null)
15
+ facts.push(o.resumedEvents > 0 ? { label: "SESSION", value: "resumed", note: plural(o.resumedEvents, "event") } : { label: "SESSION", value: "new", note: "resumable after kill -9" });
16
+ else
17
+ facts.push({ label: "SESSION", value: o.sessionId, note: o.resumedEvents > 0 ? `resumed \u00b7 ${plural(o.resumedEvents, "event")}` : "new \u00b7 resumable after kill -9" });
18
+ // the last sweep: no model resolved — the bar says `faux`, and this row
19
+ // says how to leave it (the line that said so was painted over at once)
20
+ // (as short as the RULES note: beside the wordmark a longer note is cut)
21
+ if (o.faux)
22
+ facts.push({ label: "MODEL", value: "faux", note: "set an API key, or add a model to config.json" });
23
+ facts.push(o.rules !== null ? { label: "RULES", value: o.rules } : { label: "RULES", value: "none", note: "an AGENTS.md or CLAUDE.md here is read" });
24
+ if (o.skills !== null) {
25
+ facts.push({ label: "SKILLS", value: String(o.skills.count), note: o.skills.broken > 0 ? `${o.skills.broken} cannot load · /skills` : "/skills lists them" });
26
+ }
27
+ if (o.mcp !== null) {
28
+ // the status tool is the extension's own, not a server's
29
+ const served = o.mcp.tools.filter((t) => t.startsWith("mcp__") && t !== "mcp__status");
30
+ const servers = new Set(served.map((t) => /^mcp__(.+?)__/.exec(t)?.[1] ?? t)).size;
31
+ if (servers === 0)
32
+ facts.push({ label: "MCP", value: o.mcp.connecting ? "connecting…" : "none" });
33
+ else
34
+ facts.push({ label: "MCP", value: plural(servers, "server"), note: `${plural(served.length, "tool")}${o.mcp.connecting ? " · connecting…" : ""}` });
35
+ }
36
+ if (o.extensions !== null)
37
+ facts.push({ label: "EXTENSIONS", value: o.extensions.value, ...(o.extensions.note !== undefined ? { note: o.extensions.note } : {}) });
38
+ // DC-49: STATES the fact and names the remedy; it does not warn — the
39
+ // configuration is allowed (owner, 2026-09-06)
40
+ if (o.homeWorkspace)
41
+ facts.push({ label: "", value: "home directory as workspace — cd into a project to narrow it" });
42
+ return facts;
43
+ }
package/dist/replay.d.ts CHANGED
@@ -19,6 +19,8 @@
19
19
  * summary (what the model sees next) and the turns it covers, read in
20
20
  * the viewer — then any uncovered earlier turns folded as above.
21
21
  */
22
+ import type { TaskInfo } from "@vincemakes/kiso-runtime/internal";
23
+ import type { NoticeMark } from "@vincemakes/kiso-tui";
22
24
  /** The Body surface the replay drives — the live run's own mutations. */
23
25
  export interface ReplayBody {
24
26
  userLine(text: string): void;
@@ -34,6 +36,11 @@ export interface ReplayBody {
34
36
  untimed?: boolean;
35
37
  }): void;
36
38
  notice(text: string): void;
39
+ metaNotice(text: string, rows: readonly {
40
+ readonly label: string;
41
+ readonly sentence: string;
42
+ readonly mark?: NoticeMark;
43
+ }[]): void;
37
44
  endTurn(thoughtSeconds: number): void;
38
45
  fold(label: string, replay: () => void, summary?: string | null): void;
39
46
  raw(lines: string[]): void;
@@ -49,5 +56,5 @@ export declare function turnsOf(events: readonly Ev[]): Ev[][];
49
56
  * Replay a session's durable events into the body. Returns the number of
50
57
  * turns it found (0: a fresh session — nothing is drawn).
51
58
  */
52
- export declare function replayInto(body: ReplayBody, events: readonly Ev[], W?: number, what?: (taskId: string) => string | undefined): number;
59
+ export declare function replayInto(body: ReplayBody, events: readonly Ev[], W?: number, tasks?: readonly TaskInfo[]): number;
53
60
  export {};