@vincemakes/kiso-code 0.52.0 → 0.53.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.d.ts CHANGED
@@ -411,5 +411,7 @@ export interface ChatNav {
411
411
  /** 0.40.0: an id outside this project's folder — refused, or opened where it is */
412
412
  readonly route?: (id: string) => SessionRoute | null;
413
413
  readonly pick?: () => Promise<string | null>;
414
+ /** the session's first message was sent: the caller may title the session from it (Session.title) */
415
+ readonly titled?: (firstMessage: string) => void;
414
416
  }
415
417
  export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav, seed?: readonly string[]): Promise<ChatEnd>;
package/dist/chat.js CHANGED
@@ -1386,7 +1386,11 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
1386
1386
  // 0.40.0: a skill turn is not scanned either — its text is a
1387
1387
  // SKILL.md body, and a body that mentions `diagram.png` must not
1388
1388
  // attach a file from the workspace the person never pointed at.
1389
+ input.setGhost?.(null); // a new turn: the last run's follow-up no longer applies
1389
1390
  const content = via !== undefined ? text : attachImages(text, input.attachments?.(), protectedFiles());
1391
+ // the person's first message of the session: a title is made from it beside the run, never in its way
1392
+ if (via === undefined && !session.log.all.some((e) => e.type === "user_input"))
1393
+ nav?.titled?.(text);
1390
1394
  // ADR-0058: a task notice is the runtime's input — source "system"
1391
1395
  const run = via !== undefined ? session.run(content, { via, ...(via.kind === "tasks" ? { source: "system" } : {}) }) : session.run(content);
1392
1396
  currentRun = run;
@@ -1435,6 +1439,15 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
1435
1439
  // round 8: after EVERY turn the prompt is re-armed — the human
1436
1440
  // never types blind after the first turn.
1437
1441
  input.prompt();
1442
+ // the follow-up (owner 2026-11-03): after a run that ended well, the line the person may send next,
1443
+ // asked of the session's own model with the run's own prefix (Session.followUp); shown dim in the
1444
+ // empty box only while no other turn has started or waits; off with the faux model and KISO_FOLLOWUP=off
1445
+ if (last?.type === "terminal" && last.outcome.kind === "completed" && !faux && input.setGhost !== undefined && process.env["KISO_FOLLOWUP"] !== "off" && queued === 0) {
1446
+ void session.followUp({ maxTokens: 200, signal: AbortSignal.timeout(15_000) }).then((line) => {
1447
+ if (turnNo === myTurn && currentRun === null && queued === 0)
1448
+ input.setGhost?.(line === "" ? null : line);
1449
+ }, () => undefined);
1450
+ }
1438
1451
  // the ergonomics batch C8: the opt-in auto-compact — checked AFTER the
1439
1452
  // turn ended (the run's terminal is in the log, the ratio
1440
1453
  // is post-run).
@@ -4,7 +4,9 @@
4
4
  * the context (the chain, the run state, the prompt arming).
5
5
  */
6
6
  import type { SessionRoute } from "./projects.js";
7
- import type { AgentSession } from "@vincemakes/kiso-runtime";
7
+ import { type StoreRecord } from "@vincemakes/kiso-runtime/internal";
8
+ import { type AgentSession } from "@vincemakes/kiso-runtime";
9
+ import type { SessionCardView } from "@vincemakes/kiso-tui";
8
10
  import { type LineInput } from "./state.js";
9
11
  import type { UserInputVia } from "@vincemakes/kiso-core";
10
12
  /** Everything dispatch touches that chat() owns. */
@@ -72,3 +74,6 @@ export declare function parseBang(trimmed: string): {
72
74
  export declare function markCompactWhy(why: string): void;
73
75
  export declare function markAutoCompact(): void;
74
76
  export declare function dispatch(line: string, ctx: DispatchCtx): void;
77
+ /** /fork's rows: each finished run's reply, newest first, by its first line and when it ended; the opened row says the
78
+ * line it answered. The card's id is the seq of the run's terminal, where SessionStore.fork cuts. */
79
+ export declare function replyCards(records: readonly StoreRecord[]): SessionCardView[];
package/dist/dispatch.js CHANGED
@@ -5,11 +5,12 @@
5
5
  */
6
6
  import { STATUS_GLYPHS, contextRows, contextSheetRows, contextUnavailableRows, contextUnavailableSheetRows, displayVerb, escapeTerminal, helpRows, toolTarget, infoSheetRows, kUnit, modePickView, modelPickView, namedPickView, settingsPickView, compactingStatus, liveRow, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
7
7
  import { newSessionId } from "./session-id.js";
8
- import { buildAdapter, lookupModelMetadata, readSessionName, resolveContinuationScope, resolveReasoning, sessionTitle, tiersFor, writeSessionName } from "@vincemakes/kiso-runtime/internal";
8
+ import { buildAdapter, isForkNote, lookupModelMetadata, readSessionName, resolveContinuationScope, resolveReasoning, sessionTitle, tiersFor, writeSessionName } from "@vincemakes/kiso-runtime/internal";
9
+ import { ForkRefusedError, SessionStore } from "@vincemakes/kiso-runtime";
9
10
  import { MODE_LABEL, MODE_NOTE, OFFERED_MODES, applyModeSetting, getDontAsk, getMode, modeDisplay, parseMode, setDontAsk } from "./mode.js";
10
11
  import { clipboardWrite, lastAnswer } from "./clipboard.js";
11
12
  import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
12
- import { activeStoreDir, agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
13
+ import { activeStoreDir, sessionStoreRef, agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
13
14
  import { adapterOptionsFor } from "./auth/adapter-options.js";
14
15
  import { profileProviderLabel, providerLabel } from "./provider-label.js";
15
16
  import { installedVersion, versionStatusLine } from "./stale-version.js";
@@ -1384,6 +1385,44 @@ export function dispatch(line, ctx) {
1384
1385
  ctx.requestReload();
1385
1386
  return;
1386
1387
  }
1388
+ if (trimmed === "/fork") {
1389
+ // owner 2026-11-03: the web app's fork, one rule — a new session from after one of this session's replies
1390
+ // (SessionStore.fork), then the terminal switches to it as /resume does; the replies are picked in /resume's band
1391
+ if (ctx.isRunning()) {
1392
+ body.notice("[/fork] a run is in flight — let it finish (esc stops it), then fork");
1393
+ ctx.input.prompt();
1394
+ return;
1395
+ }
1396
+ ctx.chainRef.current = ctx.chainRef.current.then(async () => {
1397
+ const cards = replyCards(sessionStoreRef?.load(ctx.session.id) ?? []);
1398
+ if (cards.length === 0) {
1399
+ body.notice("no finished reply to fork after yet");
1400
+ ctx.input.prompt();
1401
+ return;
1402
+ }
1403
+ const fork = (afterSeq) => {
1404
+ try {
1405
+ const made = new SessionStore(activeStoreDir).fork(ctx.session.id, { afterSeq });
1406
+ body.notice(`forked "${escapeTerminal(readSessionName(activeStoreDir, made.id) ?? made.id)}" — switching to it`);
1407
+ ctx.requestSwitch(made.id);
1408
+ }
1409
+ catch (error) {
1410
+ body.notice(error instanceof ForkRefusedError ? `[/fork] ${error.message}` : `[/fork] ${error instanceof Error ? error.message : String(error)}`);
1411
+ ctx.input.prompt();
1412
+ }
1413
+ };
1414
+ // no band (a pipe, a dock-less terminal): after the last reply, the sidebar's Fork
1415
+ if (ctx.input.pick === undefined || !dock.active)
1416
+ return fork(Number(cards[0].id));
1417
+ ctx.input.pick(() => cards, (picked) => {
1418
+ if (picked === null)
1419
+ ctx.input.prompt(); // esc: nothing forked, nothing said
1420
+ else
1421
+ fork(Number(picked));
1422
+ }, undefined, "fork");
1423
+ });
1424
+ return;
1425
+ }
1387
1426
  if (trimmed === "/resume" || trimmed.startsWith("/resume ")) {
1388
1427
  // the mini-spec: the in-session door to the durable sessions —
1389
1428
  // /resume <id> switches directly; bare /resume opens the SAME
@@ -1589,3 +1628,27 @@ async function tasksCommand(arg, ctx) {
1589
1628
  else if (action === 1)
1590
1629
  stop(task);
1591
1630
  }
1631
+ /** /fork's rows: each finished run's reply, newest first, by its first line and when it ended; the opened row says the
1632
+ * line it answered. The card's id is the seq of the run's terminal, where SessionStore.fork cuts. */
1633
+ export function replyCards(records) {
1634
+ const cards = [];
1635
+ let asked = "";
1636
+ let reply = "";
1637
+ for (const { event: e, ts } of records) {
1638
+ if (e.type === "user_input" && e.source !== "system") {
1639
+ const content = Array.isArray(e.content) ? e.content.filter((b) => !(b.type === "text" && isForkNote(b.text ?? ""))) : e.content;
1640
+ asked = (typeof content === "string" ? content : content.map((b) => (b.type === "text" ? (b.text ?? "") : "")).join(" ")).trim();
1641
+ reply = "";
1642
+ }
1643
+ else if (e.type === "tool_result")
1644
+ reply = "";
1645
+ else if (e.type === "text_delta")
1646
+ reply += e.text;
1647
+ else if (e.type === "terminal" && e.outcome.kind === "completed") {
1648
+ // the row is how the person recognizes the reply: its first line, inline code read as plain text
1649
+ const first = (reply.split("\n").map((l) => l.trim()).find((l) => l !== "") ?? "(no reply text)").replace(/`([^`]*)`/g, "$1");
1650
+ cards.unshift({ id: String(e.seq), title: first, badge: "completed", turns: null, updatedAt: ts, uncertain: 0, asks: 0, outcome: null, detail: `answered: ${asked.split("\n")[0] ?? ""}` });
1651
+ }
1652
+ }
1653
+ return cards;
1654
+ }
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ import { createRequire } from "node:module";
33
33
  import { basename, dirname, join } from "node:path";
34
34
  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";
35
35
  import { disposeExtensions, SessionStore } from "@vincemakes/kiso-runtime";
36
- import { listSessionSidecars, migrateSummaries, readProfile, readSessionName, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
36
+ import { listSessionSidecars, migrateSummaries, readProfile, readSessionName, summaryMigrationPending, writeSessionName } from "@vincemakes/kiso-runtime/internal";
37
37
  import { skillMenuItems } from "./skill-invoke.js";
38
38
  import { canonicalPath, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
39
39
  import { reverseMigration } from "./session-migration.js";
@@ -332,8 +332,8 @@ function editorInput(editor) {
332
332
  // TUI2-R2 ②: the session picker — the editor owns the keys (the
333
333
  // selection walk, the filter, enter/esc), the compositor draws the
334
334
  // band, and the id comes back here.
335
- pick(cards, onPick, here) {
336
- editor.beginPick(cards, onPick, here);
335
+ pick(cards, onPick, here, kind) {
336
+ editor.beginPick(cards, onPick, here, kind);
337
337
  },
338
338
  // W22: the pending-turn queue — the ↑ pop walk (the keys); the
339
339
  // chips are the compositor's bindQueue (the dock side).
@@ -341,6 +341,9 @@ function editorInput(editor) {
341
341
  editor.bindQueue(state, pop);
342
342
  },
343
343
  // ADR-0057: the steers a stop handed back return to the buffer.
344
+ setGhost(text) {
345
+ editor.setGhost(text);
346
+ },
344
347
  restore(text) {
345
348
  editor.restore(text);
346
349
  },
@@ -1158,6 +1161,30 @@ async function chatLoop(first, firstId, input, autoCompact) {
1158
1161
  sessions: () => agent.sessionIds(),
1159
1162
  route: (sessionId) => routeSession(sessionId),
1160
1163
  ...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
1164
+ // owner 2026-11-03: a session is titled by a summary of its first message, in the background; the person's own
1165
+ // name wins, before or after; off with KISO_AUTO_TITLE=off and with the faux model, and never without a TTY:
1166
+ // a piped chat is a script or a bench leg, where one more request per session is not the person's
1167
+ ...(currentFaux || process.env["KISO_AUTO_TITLE"] === "off" || process.stdin.isTTY !== true
1168
+ ? {}
1169
+ : {
1170
+ titled: (firstMessage) => {
1171
+ // the session and its folder as they are NOW: a /resume or a switch before the title lands must not
1172
+ // send it to another session, nor repaint another session's window title
1173
+ const [titling, mine, dir] = [session, session.id, activeStoreDir];
1174
+ if (readSessionName(dir, mine) !== null)
1175
+ return;
1176
+ void titling.title(firstMessage, { signal: AbortSignal.timeout(15_000) }).then((title) => {
1177
+ if (title === "" || readSessionName(dir, mine) !== null)
1178
+ return;
1179
+ writeSessionName(dir, mine, title);
1180
+ // `id` is the loop's: the session open now (a switch moves it on)
1181
+ if (id !== mine)
1182
+ return;
1183
+ setTitleName(title);
1184
+ paintWindowTitle();
1185
+ }, () => undefined);
1186
+ },
1187
+ }),
1161
1188
  };
1162
1189
  const end = await chat(session, currentFaux, input, autoCompact, nav, seed);
1163
1190
  if (end.next === "exit") {
package/dist/replay.d.ts CHANGED
@@ -19,7 +19,7 @@
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";
22
+ import { type TaskInfo } from "@vincemakes/kiso-runtime/internal";
23
23
  import type { NoticeMark } from "@vincemakes/kiso-tui";
24
24
  /** The Body surface the replay drives — the live run's own mutations. */
25
25
  export interface ReplayBody {
package/dist/replay.js CHANGED
@@ -22,6 +22,7 @@
22
22
  import { echoText } from "@vincemakes/kiso-tui-cells/render";
23
23
  import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
24
24
  import { cappedWaitLine, taskNoticeLines, taskNoticeRows, taskWhat } from "./task-notice.js";
25
+ import { isForkNote } from "@vincemakes/kiso-runtime/internal";
25
26
  /** How many turns stay on screen in full. */
26
27
  const FULL_TURNS = 2;
27
28
  /** A user turn's opening line, or null when this input does not open one
@@ -32,7 +33,9 @@ function askOf(e) {
32
33
  if (e.type !== "user_input" || e.source === "system")
33
34
  return null;
34
35
  const via = e.via;
35
- const text = typeof via?.line === "string" ? via.line : echoText(e.content);
36
+ // a fork's first input carries the fork note before the person's words: the model's, not the line they typed
37
+ const content = Array.isArray(e.content) ? e.content.filter((b) => !(b.type === "text" && isForkNote(b.text ?? ""))) : e.content;
38
+ const text = typeof via?.line === "string" ? via.line : echoText(content);
36
39
  return text.trim() === "" ? null : text;
37
40
  }
38
41
  /** Split events into turns: each starts at a user turn's input; anything
package/dist/state.d.ts CHANGED
@@ -241,7 +241,7 @@ export interface LineInput {
241
241
  * picked id, or null when the human leaves without picking.
242
242
  * OPTIONAL: only the raw-mode editor has it, and the picker only
243
243
  * ever opens on a TTY (a pipe has nobody to pick). */
244
- pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
244
+ pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string, kind?: "resume" | "fork"): void;
245
245
  /** W22: bind the pending-turn queue — the ↑ pop walks the CLI's
246
246
  * live slots (each pop cancels the turn), esc ends the walk after
247
247
  * one more pop. The chips are the compositor's own bindQueue. */
@@ -249,6 +249,9 @@ export interface LineInput {
249
249
  /** ADR-0057: put text back into the editor — the steers a stop handed
250
250
  * back before they landed. The pipe path has no editor; optional. */
251
251
  restore?(text: string): void;
252
+ /** The follow-up: a line the person may send next, dim in the empty box, Tab takes it; null clears it. Optional: the
253
+ * readline input has no box to draw it in. */
254
+ setGhost?(text: string | null): void;
252
255
  /** R3a: cross-session history — seed the recall buffer, register the
253
256
  * append sink. The pipe path has no recall keys; optional. */
254
257
  bindHistory?(seed: readonly string[], persist: (line: string) => void): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "kiso-code \u2014 the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,19 +29,19 @@
29
29
  "test": "vitest run"
30
30
  },
31
31
  "dependencies": {
32
- "@vincemakes/kiso-ask-ext": "0.52.0",
33
- "@vincemakes/kiso-core": "0.52.0",
34
- "@vincemakes/kiso-evals": "0.52.0",
35
- "@vincemakes/kiso-mcp-ext": "0.52.0",
36
- "@vincemakes/kiso-provider-anthropic": "0.52.0",
37
- "@vincemakes/kiso-provider-openai": "0.52.0",
38
- "@vincemakes/kiso-provider-openai-responses": "0.52.0",
39
- "@vincemakes/kiso-runtime": "0.52.0",
40
- "@vincemakes/kiso-skills-ext": "0.52.0",
41
- "@vincemakes/kiso-subagent-ext": "0.52.0",
42
- "@vincemakes/kiso-tools-node": "0.52.0",
43
- "@vincemakes/kiso-tui": "0.52.0",
44
- "@vincemakes/kiso-tui-cells": "0.52.0"
32
+ "@vincemakes/kiso-ask-ext": "0.53.0",
33
+ "@vincemakes/kiso-core": "0.53.0",
34
+ "@vincemakes/kiso-evals": "0.53.0",
35
+ "@vincemakes/kiso-mcp-ext": "0.53.0",
36
+ "@vincemakes/kiso-provider-anthropic": "0.53.0",
37
+ "@vincemakes/kiso-provider-openai": "0.53.0",
38
+ "@vincemakes/kiso-provider-openai-responses": "0.53.0",
39
+ "@vincemakes/kiso-runtime": "0.53.0",
40
+ "@vincemakes/kiso-skills-ext": "0.53.0",
41
+ "@vincemakes/kiso-subagent-ext": "0.53.0",
42
+ "@vincemakes/kiso-tools-node": "0.53.0",
43
+ "@vincemakes/kiso-tui": "0.53.0",
44
+ "@vincemakes/kiso-tui-cells": "0.53.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^26.1.2",