@vincemakes/kiso-code 0.9.0 → 0.10.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 +13 -0
- package/dist/dispatch.js +48 -4
- package/dist/index.js +126 -8
- package/dist/session-cards.d.ts +100 -0
- package/dist/session-cards.js +95 -0
- package/dist/state.d.ts +18 -2
- package/dist/state.js +8 -0
- package/package.json +14 -14
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
|
-
|
|
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(`--- ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
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.
|
|
3
|
+
"version": "0.10.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.
|
|
22
|
-
"@vincemakes/kiso-core": "0.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
27
|
-
"@vincemakes/kiso-runtime": "0.
|
|
28
|
-
"@vincemakes/kiso-skills-ext": "0.
|
|
29
|
-
"@vincemakes/kiso-subagent-ext": "0.
|
|
30
|
-
"@vincemakes/kiso-task-ext": "0.
|
|
31
|
-
"@vincemakes/kiso-tools-node": "0.
|
|
32
|
-
"@vincemakes/kiso-tui": "0.
|
|
33
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.10.0",
|
|
22
|
+
"@vincemakes/kiso-core": "0.10.0",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.10.0",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.10.0",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.10.0",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.10.0",
|
|
27
|
+
"@vincemakes/kiso-runtime": "0.10.0",
|
|
28
|
+
"@vincemakes/kiso-skills-ext": "0.10.0",
|
|
29
|
+
"@vincemakes/kiso-subagent-ext": "0.10.0",
|
|
30
|
+
"@vincemakes/kiso-task-ext": "0.10.0",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.10.0",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.10.0",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.10.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.1.2",
|