@vincemakes/kiso-code 0.9.0 → 0.11.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.
package/dist/chat.js CHANGED
@@ -735,6 +735,19 @@ export async function chat(session, faux, input, autoCompact) {
735
735
  costUsd: spentUsd,
736
736
  }));
737
737
  };
738
+ // TUI2-R2 ⑥ — the BOOT status line. The row is the product's one
739
+ // persistent claim about itself (the tier, how to change it, the model,
740
+ // the context left), and it used to appear after turn ONE: the
741
+ // idle-fresh screen — the screen every session opens on — showed an
742
+ // empty row where all of that belongs.
743
+ //
744
+ // Nothing had to be computed to fix it. paintIdle already had every
745
+ // field at this point: the mode is set before the agent is built, the
746
+ // model is resolved inside it, and an unstarted session's context
747
+ // estimate is a perfectly good 100%. It was simply never called until
748
+ // a turn ended. One call, and the SAME formatter — a boot-time copy of
749
+ // the row would drift from the real one the moment either changed.
750
+ paintIdle();
738
751
  const statusCb = (u, ctx, costUsd) => {
739
752
  runUsage = u;
740
753
  addCost(costUsd ?? null);
package/dist/dispatch.js CHANGED
@@ -3,7 +3,7 @@
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 { contextRows, contextUnavailableRows, escapeTerminal, helpRows, kUnit, palette } from "@vincemakes/kiso-tui";
6
+ import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modelPickView, palette } from "@vincemakes/kiso-tui";
7
7
  import { buildAdapter } from "@vincemakes/kiso-runtime/internal";
8
8
  import { MODES, getMode, setMode } from "./mode.js";
9
9
  import { agentModel, body, bodyLog, configModels, dock, readContextLedger, setAgentModel, setCurrentModelName } from "./state.js";
@@ -56,9 +56,21 @@ export function dispatch(line, ctx) {
56
56
  bodyLog("[no tool call yet]");
57
57
  }
58
58
  else {
59
- bodyLog(`--- ${tool.name} input ---`);
59
+ // TUI2-R2pre ④: the SECTION HEADERS say the act ("--- read
60
+ // input ---") on the interactive SCREEN; the two payloads
61
+ // between them are the RAW input JSON and the RAW result
62
+ // content and are byte-identical either way.
63
+ //
64
+ // The dock check is the round's one conditional, and it is
65
+ // deliberate: /last is a single code path serving two
66
+ // surfaces, and the ruling covers the screen while the pipe
67
+ // is a machine-readable log whose bytes other things (the
68
+ // e2e gates, anyone's script) already depend on. Without the
69
+ // dock, this is that log — so it keeps the call's own name.
70
+ const verb = dock.active ? displayVerb(tool.name) : tool.name;
71
+ bodyLog(`--- ${verb} input ---`);
60
72
  bodyLog(escapeTerminal(JSON.stringify(tool.input, null, 2)));
61
- bodyLog(`--- ${tool.name} output${tool.result.isError ? " (error)" : ""} ---`);
73
+ bodyLog(`--- ${verb} output${tool.result.isError ? " (error)" : ""} ---`);
62
74
  bodyLog(escapeTerminal(tool.result.content));
63
75
  }
64
76
  ctx.input.prompt();
@@ -162,7 +174,39 @@ export function dispatch(line, ctx) {
162
174
  // session's adapter — the NEXT turn uses it (session.setAdapter),
163
175
  // the notice cell leaves the audit line in the body.
164
176
  ctx.chainRef.current = ctx.chainRef.current.then(async () => {
165
- const arg = trimmed.slice(6).trim();
177
+ let arg = trimmed.slice(6).trim();
178
+ // TUI2-R2 ④ — bare /model PICKS. It used to print a list and a
179
+ // sentence telling you to go and edit a JSON file: everything
180
+ // needed to make it a choice was already on screen, and only the
181
+ // choosing was missing. The panel adds the choosing and nothing
182
+ // else — what a switch MEANS is unchanged below, and the printed
183
+ // list survives verbatim wherever there is no panel to draw (a
184
+ // pipe, a dock-less TTY), because that is a machine-readable
185
+ // surface and this round moves no bytes on one.
186
+ if (arg === "" && dock.active && ctx.input.panelAsk !== undefined) {
187
+ const names = Object.keys(configModels);
188
+ const picked = await new Promise((resolve) => {
189
+ ctx.input.panelAsk(modelPickView({
190
+ header: `model — current: ${agentModel}`,
191
+ options: names.map((name) => {
192
+ const profile = configModels[name];
193
+ const marks = [`profile: ${name}`, ...(profileAvailable(profile) ? [] : ["unavailable"]), ...(profile.model === agentModel ? ["current"] : [])];
194
+ return { label: `${profile.kind}/${profile.model}`, note: marks.join(" · ") };
195
+ }),
196
+ typeHint: names.length === 0 ? "type provider/model directly (e.g. openai/deepseek-reasoner)" : "type provider/model directly",
197
+ // the zero-profile copy is TODAY'S, verbatim: the
198
+ // user who sees it is exactly the user who needs
199
+ // the path spelled out
200
+ ...(names.length === 0 ? { emptyNote: "no profiles — define models in ~/.kiso/config.json" } : {}),
201
+ }, ctx.isRunning() ? "▸ run paused" : `▸ ${getMode()}`), (v) => resolve(v.action === "picked" ? v.result : null));
202
+ });
203
+ ctx.paintIdle();
204
+ if (picked === null) {
205
+ ctx.input.prompt();
206
+ return; // esc — nothing switched, nothing said
207
+ }
208
+ arg = "index" in picked ? names[picked.index] : picked.custom;
209
+ }
166
210
  if (arg === "") {
167
211
  bodyLog(`model: ${agentModel}`);
168
212
  const names = Object.keys(configModels);
package/dist/index.js CHANGED
@@ -26,19 +26,20 @@ import { readFileSync, realpathSync, rmSync } from "node:fs";
26
26
  import { createInterface } from "node:readline";
27
27
  import { fileURLToPath } from "node:url";
28
28
  import { join } from "node:path";
29
- import { Body, Editor, bannerLines, escapeTerminal, extensionsBannerText, interactivePrompt, palette, renderSessionLine } from "@vincemakes/kiso-tui";
29
+ import { Body, Editor, bannerLines, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListRow } from "@vincemakes/kiso-tui";
30
30
  import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
31
31
  import { createFauxProvider } from "@vincemakes/kiso-evals";
32
32
  import { createCodingTools } from "@vincemakes/kiso-tools-node";
33
- import { MODES, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
33
+ import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
34
34
  import { builtInLayer } from "./builtin.js";
35
- import { atFiles, body, bodyLog, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, userExtensions, VERSION } from "./state.js";
35
+ import { agentModel, atFiles, body, bodyLog, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setSessionStore, userExtensions, VERSION } from "./state.js";
36
36
  import { askUi, resolveProjectTrust } from "./trust-ui.js";
37
37
  import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
38
38
  import { fauxSkip, readFauxScript } from "./faux-glue.js";
39
- import { autoCompactFromEnv, chat, contextWindowTokens } from "./chat.js";
39
+ import { autoCompactFromEnv, chat, contextWindowTokens, estimateCtxRatio } from "./chat.js";
40
40
  import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
41
41
  import { resume } from "./resume.js";
42
+ import { collectSessionCards } from "./session-cards.js";
42
43
  // The moved exports stay reachable from this entry — the test imports
43
44
  // (project-trust, coding-agent) never change (B4: zero assertion changes).
44
45
  export { applyProjectMerges } from "./trust-ui.js";
@@ -159,6 +160,12 @@ function editorInput(editor) {
159
160
  panelCancel() {
160
161
  editor.cancelPanel();
161
162
  },
163
+ // TUI2-R2 ②: the session picker — the editor owns the keys (the
164
+ // selection walk, the filter, enter/esc), the compositor draws the
165
+ // band, and the id comes back here.
166
+ pick(cards, onPick) {
167
+ editor.beginPick(cards, onPick);
168
+ },
162
169
  // W22: the pending-turn queue — the ↑ pop walk (the keys); the
163
170
  // chips are the compositor's bindQueue (the dock side).
164
171
  bindQueue(state, pop) {
@@ -200,6 +207,7 @@ function makeLineInput() {
200
207
  dock.bindAt(() => editor.atState()); // KC3 §4: the picker's band
201
208
  dock.bindApproval(() => editor.panelState()); // W21: the panel's bound state
202
209
  dock.bindSheet(() => editor.sheetOpen()); // TUI2-R1 (D): the ? keys sheet
210
+ dock.bindPick(() => editor.pickState()); // TUI2-R2 ②: the resume picker's band
203
211
  return editorInput(editor);
204
212
  }
205
213
  return readlineInput(createInterface({ input: process.stdin, output: process.stdout }));
@@ -357,6 +365,9 @@ async function makeAgent(sessionId, input, modelFlag) {
357
365
  if (isFirstRun())
358
366
  scaffoldFirstRun();
359
367
  const store = new SessionStore(sessionsDir());
368
+ // TUI2-R2 ②/③: the navigation surfaces read through THIS store — one
369
+ // store per process, and the picker/listing never write through it.
370
+ setSessionStore(store);
360
371
  // E area: the durable script position — computed AFTER the verdict
361
372
  // (fauxSkip's session-log read is a home read: pre-trust zero-read).
362
373
  const fauxSkipTurns = sessionId === undefined ? 0 : fauxSkip(sessionId);
@@ -442,6 +453,74 @@ async function makeAgent(sessionId, input, modelFlag) {
442
453
  };
443
454
  return createAgent(definition);
444
455
  }
456
+ /** TUI2-R2 ② — the picker's affordance row: the keys, said where the
457
+ * keys are useful. */
458
+ const PICKER_HINT = "↑↓ pick · ⏎ resumes · type filters · esc";
459
+ /**
460
+ * TUI2-R2 ①–③ — the listing's cards. The projection consumes the
461
+ * runtime's own accessors (see session-cards.ts); this is only the
462
+ * plumbing that hands it the store's read side.
463
+ */
464
+ async function sessionCards(agent) {
465
+ const store = sessionStoreRef;
466
+ if (store === null)
467
+ return []; // unreachable: makeAgent builds the store first
468
+ return collectSessionCards(agent, (id) => store.load(id));
469
+ }
470
+ /**
471
+ * TUI2-R2 ② — the resume picker: the band, the keys, the id.
472
+ *
473
+ * The status row carries the picker's own affordance while it is up
474
+ * (a surface teaches its keys where the keys are useful), and the
475
+ * promise settles on the editor's commit — the id the human took, or
476
+ * null when they left. The picker path WRITES NOTHING: the cards are a
477
+ * projection over what is already on disk.
478
+ */
479
+ async function pickSession(agent, input) {
480
+ const cards = await sessionCards(agent);
481
+ if (cards.length === 0) {
482
+ bodyLog("no sessions yet \u2014 `kiso` starts one");
483
+ return null;
484
+ }
485
+ // a dock-less TTY (rows < 4) has no band to draw the picker in, so the
486
+ // honest answer is the usage line this command has always printed.
487
+ if (input.pick === undefined || !dock.active) {
488
+ console.error('usage: kiso resume <sessionId> ["prompt"]');
489
+ process.exit(2);
490
+ }
491
+ dock.setStatus("", PICKER_HINT);
492
+ const picked = await new Promise((resolve) => {
493
+ input.pick(() => cards, resolve);
494
+ });
495
+ dock.setStatus("", null);
496
+ return picked;
497
+ }
498
+ /**
499
+ * TUI2-R2 ⑥ — the BOOT status row.
500
+ *
501
+ * The status line is the product's one persistent claim about itself,
502
+ * and it used to appear after turn ONE: the idle-fresh screen — the
503
+ * screen every session opens on, and the only screen a first-time user
504
+ * sees before deciding whether to type — showed an empty row where the
505
+ * tier, the /mode hint, the model and the remaining context belong.
506
+ *
507
+ * It is painted HERE, at the first moment every field is TRUE: after
508
+ * makeAgent, because that is where the model is resolved. Painting it at
509
+ * dock.enter() — the literally-first frame — would have to name a model
510
+ * nobody had chosen yet, and a status row that guesses is worse than a
511
+ * status row that waits two hundred milliseconds.
512
+ *
513
+ * The meter fields (cache rate, cost) are deliberately absent: an
514
+ * unstarted session has made no requests, and an unmeasured cache is not
515
+ * a 0% cache. chat()'s own paintIdle takes over from here with the same
516
+ * formatter — never a boot-time copy, which would drift from the real
517
+ * row the moment either changed.
518
+ */
519
+ function paintBootStatus(session) {
520
+ if (!dock.active)
521
+ return;
522
+ dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), agentModel, estimateCtxRatio(session)));
523
+ }
445
524
  async function main() {
446
525
  // E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
447
526
  // turns the in-flight stdout/stderr writes into EIO, and node's
@@ -527,11 +606,17 @@ async function main() {
527
606
  bodyLog(`session ${id}\n`);
528
607
  extensionsBanner(recentSessions(id, agent));
529
608
  faux = currentFaux;
609
+ paintBootStatus(session); // TUI2-R2 ⑥: the idle-fresh screen says what it is
530
610
  await chat(session, faux, input, resolveAutoCompact(mergedConfig));
531
611
  break;
532
612
  }
533
613
  case "resume": {
534
- if (!arg) {
614
+ // TUI2-R2 ② — bare `kiso resume` opens the PICKER, but only
615
+ // where there is a human to pick: a pipe keeps today's usage
616
+ // error and today's exit 2, byte for byte. Finding an id used
617
+ // to mean running `kiso sessions` and copying one out by eye;
618
+ // the picker is that step, done by the product.
619
+ if (!arg && !process.stdin.isTTY) {
535
620
  console.error("usage: kiso resume <sessionId> [\"prompt\"]");
536
621
  process.exit(2);
537
622
  }
@@ -541,8 +626,25 @@ async function main() {
541
626
  dock.enter();
542
627
  agent = await makeAgent(arg, input, modelFlag);
543
628
  applyConfigMode();
544
- const session = await agent.session({ id: arg });
629
+ let id = arg;
630
+ if (id === undefined) {
631
+ const picked = await pickSession(agent, input);
632
+ // esc: the human looked and chose not to resume. That is a
633
+ // normal outcome, so it exits 0 with nothing said — never
634
+ // an error, never a session started behind their back.
635
+ if (picked === null)
636
+ break;
637
+ id = picked;
638
+ }
639
+ const session = await agent.session({ id });
545
640
  faux = currentFaux;
641
+ // E area: the durable script position is computed from the
642
+ // session id, and on the picker path the id did not exist when
643
+ // makeAgent ran. Re-arm the scripted adapter at the PICKED
644
+ // session's position so a picked resume continues its script
645
+ // exactly where `kiso resume <id>` would have.
646
+ if (faux && arg === undefined)
647
+ session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
546
648
  await resume(session, prompt, faux, input);
547
649
  break;
548
650
  }
@@ -556,8 +658,23 @@ async function main() {
556
658
  // exists so the gate's ask can be answered; the listing
557
659
  // itself never touches it.
558
660
  agent = await makeAgent(undefined, input, modelFlag);
559
- for (const meta of agent.sessions()) {
560
- console.log(renderSessionLine(meta));
661
+ // TUI2-R2 the same projection the picker renders, printed.
662
+ // The PIPE keeps today's bytes exactly: `kiso sessions` is
663
+ // something scripts read, and a badge column is a TTY-render
664
+ // concern, not a change to a machine interface.
665
+ if (process.stdout.isTTY) {
666
+ const cards = await sessionCards(agent);
667
+ const W = process.stdout.columns ?? 80;
668
+ const col = idColumn(cards);
669
+ const now = Date.now();
670
+ for (const card of cards)
671
+ console.log(sessionListRow(card, W, now, col));
672
+ console.log(sessionListFooter(cards.length, W));
673
+ }
674
+ else {
675
+ for (const meta of agent.sessions()) {
676
+ console.log(renderSessionLine(meta));
677
+ }
561
678
  }
562
679
  break;
563
680
  }
@@ -594,6 +711,7 @@ async function main() {
594
711
  // session surfaced as "[faux mode] the scripted model failed:
595
712
  // <real error>" (a false accusation of the keyless demo).
596
713
  faux = currentFaux;
714
+ paintBootStatus(session); // TUI2-R2 ⑥: the same row on the bare command
597
715
  await chat(session, faux, input, autoCompactFromEnv());
598
716
  break;
599
717
  }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * TUI2-R2 slice ① — the state projection: a session's durable log →
3
+ * the card the picker and the listing both render.
4
+ *
5
+ * The card is a PURE PROJECTION and this file adds no facts. Every field
6
+ * comes from a durable event the product already writes, read through the
7
+ * runtime's OWN accessors:
8
+ *
9
+ * openRunId(records) — recovery's own "is a run still open?"
10
+ * (the ▌ brick: no terminal event)
11
+ * executionLedger(events) — the L2 ledger's own uncertain status
12
+ * (started, no receipt — the crash window)
13
+ * pendingApprovals() — the session's own live-ask accessor
14
+ * (decided/expired/dead-run all excluded)
15
+ * the `terminal` event — its outcome kind, read as written
16
+ *
17
+ * Nothing here re-implements any of them. That is the point: a badge is a
18
+ * claim about what `kiso resume` will do, and the only way it can never
19
+ * lie is for it to be derived from the same facts the resume derives
20
+ * from. A second derivation — however careful — is a second source of
21
+ * truth, and two sources of truth about durability is none.
22
+ *
23
+ * The projection is read-only by construction: it loads and it counts.
24
+ * The R-I-p2 rule (a listing never writes) holds trivially — there is no
25
+ * write call in this file, and the accessors it calls have none either.
26
+ *
27
+ * What a human READS about a card is presentation and lives in the
28
+ * terminal layer (sessionNote — the KC3 §1 split); this file owns the
29
+ * FACTS.
30
+ */
31
+ import { type StoreRecord } from "@vincemakes/kiso-runtime/internal";
32
+ /** The five durable states, in the order the projection resolves them. */
33
+ export type SessionBadge = "uncertain" | "ask" | "interrupted" | "completed" | "failed";
34
+ export interface SessionCard {
35
+ readonly id: string;
36
+ readonly badge: SessionBadge;
37
+ /** The human's unit: one user_input is one turn. (SessionMeta.runs
38
+ * counts runIds, which a resume increments without the human having
39
+ * said anything — that is a different number and not the one a
40
+ * picker row means.) */
41
+ readonly turns: number;
42
+ /** The last record's own stamp — the store's `updatedAt`, never a
43
+ * file mtime (a copied home would lie about every age). */
44
+ readonly updatedAt: number;
45
+ /** The uncertain ledger's size — the ? badge's count. */
46
+ readonly uncertain: number;
47
+ /** The unanswered permission requests — the ◌ badge's count. */
48
+ readonly asks: number;
49
+ /** The terminal outcome's kind, when the run ended; null while it is
50
+ * open. The ✗ note names it rather than inventing one word for six
51
+ * different endings. */
52
+ readonly outcome: string | null;
53
+ }
54
+ /**
55
+ * The projection. `asks` arrives as DATA because its accessor lives on
56
+ * the session object (pendingApprovals) rather than on the record list —
57
+ * passing the count keeps this function pure and unit-testable over a
58
+ * fixture the real writer produced.
59
+ *
60
+ * The precedence, and why it is this one:
61
+ *
62
+ * 1. uncertain — the spec's explicit override. An interrupted run that
63
+ * also holds an undecided side effect cannot resume until a human
64
+ * rules on it, so the badge must show the blocking condition, not
65
+ * the recoverable one.
66
+ * 2. ask — an unanswered question is likewise the thing standing
67
+ * between the session and its own continuation.
68
+ * 3. interrupted — no terminal event: the run was cut mid-flight.
69
+ * 4/5. the terminal's own verdict.
70
+ */
71
+ export declare function projectSessionCard(input: {
72
+ readonly id: string;
73
+ readonly updatedAt: number;
74
+ readonly records: readonly StoreRecord[];
75
+ readonly asks: number;
76
+ }): SessionCard;
77
+ /** The shapes this module needs from the agent — structural, so the
78
+ * projection is testable without standing up a provider. */
79
+ interface CardSource {
80
+ sessions(): readonly {
81
+ readonly id: string;
82
+ readonly updatedAt: number;
83
+ }[];
84
+ session(options: {
85
+ id: string;
86
+ }): Promise<{
87
+ pendingApprovals(): readonly unknown[];
88
+ }>;
89
+ }
90
+ /**
91
+ * The listing's cards, newest first — the order both the picker and
92
+ * `kiso sessions` want (the thing you just left is the thing you are
93
+ * most likely coming back for).
94
+ *
95
+ * The session object exists ONLY to read pendingApprovals(): opening one
96
+ * loads its records and builds an event log, and writes nothing (no
97
+ * lock, no file creation) — the read-only listing rule holds.
98
+ */
99
+ export declare function collectSessionCards(agent: CardSource, load: (id: string) => readonly StoreRecord[]): Promise<SessionCard[]>;
100
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * TUI2-R2 slice ① — the state projection: a session's durable log →
3
+ * the card the picker and the listing both render.
4
+ *
5
+ * The card is a PURE PROJECTION and this file adds no facts. Every field
6
+ * comes from a durable event the product already writes, read through the
7
+ * runtime's OWN accessors:
8
+ *
9
+ * openRunId(records) — recovery's own "is a run still open?"
10
+ * (the ▌ brick: no terminal event)
11
+ * executionLedger(events) — the L2 ledger's own uncertain status
12
+ * (started, no receipt — the crash window)
13
+ * pendingApprovals() — the session's own live-ask accessor
14
+ * (decided/expired/dead-run all excluded)
15
+ * the `terminal` event — its outcome kind, read as written
16
+ *
17
+ * Nothing here re-implements any of them. That is the point: a badge is a
18
+ * claim about what `kiso resume` will do, and the only way it can never
19
+ * lie is for it to be derived from the same facts the resume derives
20
+ * from. A second derivation — however careful — is a second source of
21
+ * truth, and two sources of truth about durability is none.
22
+ *
23
+ * The projection is read-only by construction: it loads and it counts.
24
+ * The R-I-p2 rule (a listing never writes) holds trivially — there is no
25
+ * write call in this file, and the accessors it calls have none either.
26
+ *
27
+ * What a human READS about a card is presentation and lives in the
28
+ * terminal layer (sessionNote — the KC3 §1 split); this file owns the
29
+ * FACTS.
30
+ */
31
+ import { executionLedger, openRunId } from "@vincemakes/kiso-runtime/internal";
32
+ /**
33
+ * The projection. `asks` arrives as DATA because its accessor lives on
34
+ * the session object (pendingApprovals) rather than on the record list —
35
+ * passing the count keeps this function pure and unit-testable over a
36
+ * fixture the real writer produced.
37
+ *
38
+ * The precedence, and why it is this one:
39
+ *
40
+ * 1. uncertain — the spec's explicit override. An interrupted run that
41
+ * also holds an undecided side effect cannot resume until a human
42
+ * rules on it, so the badge must show the blocking condition, not
43
+ * the recoverable one.
44
+ * 2. ask — an unanswered question is likewise the thing standing
45
+ * between the session and its own continuation.
46
+ * 3. interrupted — no terminal event: the run was cut mid-flight.
47
+ * 4/5. the terminal's own verdict.
48
+ */
49
+ export function projectSessionCard(input) {
50
+ const events = input.records.map((r) => r.event);
51
+ // the ledger's own expression — the same one the recovery plan and the
52
+ // session's uncertainExecutions() use, so the three can never disagree
53
+ const uncertain = [...executionLedger(events).values()].filter((r) => r.status === "uncertain").length;
54
+ const open = openRunId(input.records) !== undefined;
55
+ // the LAST terminal is the session's verdict — an older run's ending
56
+ // says nothing about where the session stands now
57
+ let outcome = null;
58
+ for (let i = events.length - 1; i >= 0; i -= 1) {
59
+ const ev = events[i];
60
+ if (ev.type === "terminal") {
61
+ outcome = ev.outcome.kind;
62
+ break;
63
+ }
64
+ }
65
+ const turns = events.filter((e) => e.type === "user_input").length;
66
+ const badge = uncertain > 0
67
+ ? "uncertain"
68
+ : input.asks > 0
69
+ ? "ask"
70
+ : // a log with no terminal at all leaves openRunId undefined only
71
+ // when it is empty; either way nothing durable says it ended
72
+ open || outcome === null
73
+ ? "interrupted"
74
+ : outcome === "completed"
75
+ ? "completed"
76
+ : "failed";
77
+ return { id: input.id, badge, turns, updatedAt: input.updatedAt, uncertain, asks: input.asks, outcome };
78
+ }
79
+ /**
80
+ * The listing's cards, newest first — the order both the picker and
81
+ * `kiso sessions` want (the thing you just left is the thing you are
82
+ * most likely coming back for).
83
+ *
84
+ * The session object exists ONLY to read pendingApprovals(): opening one
85
+ * loads its records and builds an event log, and writes nothing (no
86
+ * lock, no file creation) — the read-only listing rule holds.
87
+ */
88
+ export async function collectSessionCards(agent, load) {
89
+ const cards = [];
90
+ for (const meta of [...agent.sessions()].sort((a, b) => b.updatedAt - a.updatedAt)) {
91
+ const session = await agent.session({ id: meta.id });
92
+ cards.push(projectSessionCard({ id: meta.id, updatedAt: meta.updatedAt, records: load(meta.id), asks: session.pendingApprovals().length }));
93
+ }
94
+ return cards;
95
+ }
package/dist/state.d.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * creates the mutable ones (setBody / setAgentModel / setExtensionLists);
6
6
  * the moved modules read and mutate at call time.
7
7
  */
8
- import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView } from "@vincemakes/kiso-tui";
9
- import type { KisoExtension } from "@vincemakes/kiso-runtime";
8
+ import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SessionCardView } from "@vincemakes/kiso-tui";
9
+ import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
10
10
  /** finding #11: KISO_HOME is the ONE root — every default path derives from
11
11
  * it (sessions, trust, extensions, mcp config, skills). The dedicated
12
12
  * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
@@ -83,6 +83,12 @@ export interface LineInput {
83
83
  panelAsk(view: PanelView, onCommit: (v: PanelVerdict) => void): void;
84
84
  /** W21: cancel the panel — the SIGINT pair to panelAsk. */
85
85
  panelCancel(): void;
86
+ /** TUI2-R2 ②: open the session picker — the editor takes the keys
87
+ * (the selection walk, the filter, enter/esc) and hands back the
88
+ * picked id, or null when the human leaves without picking.
89
+ * OPTIONAL: only the raw-mode editor has it, and the picker only
90
+ * ever opens on a TTY (a pipe has nobody to pick). */
91
+ pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void): void;
86
92
  /** W22: bind the pending-turn queue — the ↑ pop walks the CLI's
87
93
  * live slots (each pop cancels the turn), esc ends the walk after
88
94
  * one more pop. The chips are the compositor's own bindQueue. */
@@ -107,6 +113,16 @@ export declare function setBody(value: Body): void;
107
113
  /** v2d: body output routes through the cell renderer — the single writer.
108
114
  * bodyLog adds the trailing newline; internal newlines are preserved. */
109
115
  export declare function bodyLog(text: string, wrap?: "words"): void;
116
+ /** TUI2-R2 ②/③: the session store makeAgent built — the ONE store per
117
+ * process. The navigation surfaces need its read side (load) to project
118
+ * the badges, and a second store on the same root would be a second
119
+ * lock manager for a job that never writes. */
120
+ export declare let sessionStoreRef: {
121
+ load(id: string): readonly StoreRecord[];
122
+ } | null;
123
+ export declare function setSessionStore(value: {
124
+ load(id: string): readonly StoreRecord[];
125
+ }): void;
110
126
  /** The model name for the status bar — set by makeAgent. */
111
127
  export declare let agentModel: string;
112
128
  export declare function setAgentModel(value: string): void;
package/dist/state.js CHANGED
@@ -163,6 +163,14 @@ export function setBody(value) {
163
163
  export function bodyLog(text, wrap) {
164
164
  body.raw(text.split("\n"), wrap);
165
165
  }
166
+ /** TUI2-R2 ②/③: the session store makeAgent built — the ONE store per
167
+ * process. The navigation surfaces need its read side (load) to project
168
+ * the badges, and a second store on the same root would be a second
169
+ * lock manager for a job that never writes. */
170
+ export let sessionStoreRef = null;
171
+ export function setSessionStore(value) {
172
+ sessionStoreRef = value;
173
+ }
166
174
  /** The model name for the status bar — set by makeAgent. */
167
175
  export let agentModel = "faux";
168
176
  export function setAgentModel(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "kiso CLI — the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,19 +18,19 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.9.0",
22
- "@vincemakes/kiso-core": "0.9.0",
23
- "@vincemakes/kiso-evals": "0.9.0",
24
- "@vincemakes/kiso-mcp-ext": "0.9.0",
25
- "@vincemakes/kiso-provider-anthropic": "0.9.0",
26
- "@vincemakes/kiso-provider-openai": "0.9.0",
27
- "@vincemakes/kiso-runtime": "0.9.0",
28
- "@vincemakes/kiso-skills-ext": "0.9.0",
29
- "@vincemakes/kiso-subagent-ext": "0.9.0",
30
- "@vincemakes/kiso-task-ext": "0.9.0",
31
- "@vincemakes/kiso-tools-node": "0.9.0",
32
- "@vincemakes/kiso-tui": "0.9.0",
33
- "@vincemakes/kiso-tui-cells": "0.9.0"
21
+ "@vincemakes/kiso-ask-ext": "0.11.0",
22
+ "@vincemakes/kiso-core": "0.11.0",
23
+ "@vincemakes/kiso-evals": "0.11.0",
24
+ "@vincemakes/kiso-mcp-ext": "0.11.0",
25
+ "@vincemakes/kiso-provider-anthropic": "0.11.0",
26
+ "@vincemakes/kiso-provider-openai": "0.11.0",
27
+ "@vincemakes/kiso-runtime": "0.11.0",
28
+ "@vincemakes/kiso-skills-ext": "0.11.0",
29
+ "@vincemakes/kiso-subagent-ext": "0.11.0",
30
+ "@vincemakes/kiso-task-ext": "0.11.0",
31
+ "@vincemakes/kiso-tools-node": "0.11.0",
32
+ "@vincemakes/kiso-tui": "0.11.0",
33
+ "@vincemakes/kiso-tui-cells": "0.11.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^26.1.2",