@vincemakes/kiso-code 0.40.0 → 0.40.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.
package/dist/chat.d.ts CHANGED
@@ -309,6 +309,7 @@ export type ChatEnd = {
309
309
  } | {
310
310
  readonly next: "switch";
311
311
  readonly id: string;
312
+ readonly lines?: readonly string[];
312
313
  }
313
314
  /** §2.5: the caller REBUILDS the agent and re-enters on the same id.
314
315
  * Not "switch": switching keeps the agent and only changes the id,
@@ -317,6 +318,7 @@ export type ChatEnd = {
317
318
  | {
318
319
  readonly next: "reload";
319
320
  readonly id: string;
321
+ readonly lines?: readonly string[];
320
322
  };
321
323
  /** The session-navigation seam main provides: the OTHER sessions'
322
324
  * ids, and (when a dock is up) the existing picker. */
@@ -326,4 +328,4 @@ export interface ChatNav {
326
328
  readonly route?: (id: string) => SessionRoute | null;
327
329
  readonly pick?: () => Promise<string | null>;
328
330
  }
329
- export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav): Promise<ChatEnd>;
331
+ export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav, seed?: readonly string[]): Promise<ChatEnd>;
package/dist/chat.js CHANGED
@@ -8,6 +8,7 @@ import { readFileSync, statSync } from "node:fs";
8
8
  import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, verifyOfferView, STATUS_GLYPHS, } from "@vincemakes/kiso-tui";
9
9
  import { askView, coldResumeLine, coldResumeView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
10
10
  import { canonicalTargetPath, isProtectedPath, protectedIdentity, shellProgressPath } from "@vincemakes/kiso-tools-node";
11
+ import { queuedSwitchLines } from "./state.js";
11
12
  import { echoText } from "@vincemakes/kiso-tui-cells/render";
12
13
  import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
13
14
  import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
@@ -153,6 +154,12 @@ export function estimateCtxRatio(session) {
153
154
  * is the session's own (the durable profile), never the CLI's memory. */
154
155
  export function statusModelLabel(session) {
155
156
  const effort = session.reasoning?.effort;
157
+ // The owner's dogfood, 2026-09-21 (the second pass): the status row under
158
+ // the composer is the MODEL's, and only the model's — a host belongs where
159
+ // a choice is being made (`/model`'s rows, the switch notice) and where the
160
+ // identity is asked for (`/status`), not in a row that has ~40 columns and
161
+ // cut the host to `deepseek/d…ndcode.ai`. Unnecessary at best, misleading at
162
+ // worst: it read as a path, not as an account.
156
163
  return effort !== undefined && effort !== "default" ? `${agentModel} · ${effort}` : agentModel;
157
164
  }
158
165
  export function displayCtxRatio(session) {
@@ -1164,7 +1171,7 @@ submitTurn) {
1164
1171
  }
1165
1172
  return last;
1166
1173
  }
1167
- export async function chat(session, faux, input, autoCompact, nav) {
1174
+ export async function chat(session, faux, input, autoCompact, nav, seed) {
1168
1175
  // the switch directive — set once by dispatch's /clear or /resume,
1169
1176
  // resolved through the end signal so the final awaits still run
1170
1177
  let switchTo = null;
@@ -1379,7 +1386,15 @@ export async function chat(session, faux, input, autoCompact, nav) {
1379
1386
  // REPL is ready (they are never dropped).
1380
1387
  const chainRef = { current: Promise.resolve() };
1381
1388
  let replReady = false;
1382
- const queuedLines = [];
1389
+ const queuedLines = [...(seed ?? []), ...queuedSwitchLines.splice(0)];
1390
+ // DC-57 (the owner's ruling, 2026-09-21): once the session is LEAVING —
1391
+ // a switch or a reload has been requested — a line that arrives belongs
1392
+ // to the session the person asked for, not to the one they are leaving.
1393
+ // One read can carry several lines (a paste, a pipe, a scripted
1394
+ // driver), and the departing instance used to dispatch them; they are
1395
+ // queued here and replayed by the NEXT chat() entry, exactly as
1396
+ // pre-ready lines are replayed by this one.
1397
+ let leaving = false;
1383
1398
  // B area: user-turn counter for the status line. /last and /think read
1384
1399
  // the body (the ToolCell / ThinkingCell final states).
1385
1400
  let turnNo = 0;
@@ -1565,12 +1580,18 @@ export async function chat(session, faux, input, autoCompact, nav) {
1565
1580
  // the /resume+/clear mini-spec: the switch directive and the
1566
1581
  // session-navigation seam (absent nav = the commands degrade to
1567
1582
  // an honest refusal in dispatch)
1583
+ // DC-57: the dispatch edge asks this before it answers a line, so the
1584
+ // decision is made when the line's segment runs — not when its bytes
1585
+ // arrived, which can be before the switch was even requested.
1586
+ leaving: () => leaving,
1568
1587
  requestSwitch: (id) => {
1569
1588
  switchTo = id;
1589
+ leaving = true;
1570
1590
  resolveEnd();
1571
1591
  },
1572
1592
  requestReload: () => {
1573
1593
  reloadReq = true;
1594
+ leaving = true;
1574
1595
  resolveEnd();
1575
1596
  },
1576
1597
  sessions: () => nav?.sessions() ?? [],
@@ -1603,7 +1624,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
1603
1624
  dispatch(line, dispatchCtx);
1604
1625
  };
1605
1626
  input.onLine((line) => {
1606
- if (!replReady) {
1627
+ if (!replReady || leaving) {
1607
1628
  queuedLines.push(line);
1608
1629
  return;
1609
1630
  }
@@ -1708,8 +1729,8 @@ export async function chat(session, faux, input, autoCompact, nav) {
1708
1729
  // enough: the /compact segment appends nothing of its own.
1709
1730
  await chainRef.current;
1710
1731
  if (switchTo !== null)
1711
- return { next: "switch", id: switchTo };
1732
+ return { next: "switch", id: switchTo, lines: queuedLines.slice() };
1712
1733
  // a switch beats a reload: /resume and /clear are going somewhere else,
1713
1734
  // and the agent they land on is rebuilt by the caller either way.
1714
- return reloadReq ? { next: "reload", id: session.id } : { next: "exit" };
1735
+ return reloadReq ? { next: "reload", id: session.id, lines: queuedLines.slice() } : { next: "exit" };
1715
1736
  }
@@ -37,6 +37,8 @@ export interface DispatchCtx {
37
37
  /** the /resume+/clear mini-spec: end this chat() with a switch to
38
38
  * another session — main re-enters chat there; the editor survives. */
39
39
  readonly requestSwitch: (id: string) => void;
40
+ /** DC-57: true once this entry has been asked to leave — the guard that keeps a departing session from answering a batch's later lines. */
41
+ readonly leaving: () => boolean;
40
42
  /** §2.5: end this chat() with a REBUILD of the agent on the SAME
41
43
  * session — extensions, skills and config are read again. */
42
44
  readonly requestReload: () => void;
package/dist/dispatch.js CHANGED
@@ -9,8 +9,10 @@ import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveRea
9
9
  import { MODES, MODE_NOTE, OFFERED_MODES, getMode, setMode } from "./mode.js";
10
10
  import { clipboardWrite, lastAnswer } from "./clipboard.js";
11
11
  import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
12
- import { agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, setLastBinding } from "./state.js";
12
+ import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, setLastBinding } from "./state.js";
13
13
  import { adapterOptionsFor } from "./auth/adapter-options.js";
14
+ import { profileProviderLabel, providerLabel } from "./provider-label.js";
15
+ import { queuedSwitchLines } from "./state.js";
14
16
  import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner } from "./chat.js";
15
17
  import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
16
18
  import { shellTool } from "@vincemakes/kiso-tools-node";
@@ -190,6 +192,18 @@ function runBang(command, send, ctx) {
190
192
  });
191
193
  }
192
194
  export function dispatch(line, ctx) {
195
+ // DC-57 (the owner's ruling, 2026-09-21): a session that is LEAVING — a
196
+ // switch or a reload has been requested from an earlier segment of the
197
+ // chain — does not answer new lines. They are queued for the session the
198
+ // person asked for and replayed by its `chat()` entry. The guard lives
199
+ // HERE, not only at the input edge: a batch that arrived before the
200
+ // switch was requested still runs its segments after it (the chain is
201
+ // FIFO), which is exactly how a pasted prompt used to land in the
202
+ // departing session.
203
+ if (ctx.leaving()) {
204
+ queuedSwitchLines.push(line);
205
+ return;
206
+ }
193
207
  const trimmed = line.trim();
194
208
  // §2.2 — the `!` gesture is the COMPOSER's alone. A piped session, `-p`
195
209
  // and `--task-file` keep `!` as ordinary text: those lines are content,
@@ -474,6 +488,21 @@ export function dispatch(line, ctx) {
474
488
  bodyLog(`session ${ctx.session.id}`);
475
489
  bodyLog(`${ctx.session.log.all.length} events`);
476
490
  bodyLog(`ctx ${ctxPct}`);
491
+ // The owner, 2026-09-21: /status is where "what am I actually
492
+ // running on" is answered, so the identity is spelled out HERE —
493
+ // the model, the host the request goes to, the profile it came
494
+ // from and the key it spends. The status row stays short; this
495
+ // line is the disambiguation two same-named profiles need.
496
+ //
497
+ // REVIEW (2026-09-21): the profile is named only when what is in
498
+ // hand IS a profile key. `currentModelName` holds whatever the
499
+ // resolution produced — a profile alias after `/model`, a MODEL ID
500
+ // after a switch or a resume — so printing it unconditionally
501
+ // claimed `profile deepseek-v4-flash` for a session that was
502
+ // restored, not switched. (The real fix is to stop overloading that
503
+ // variable; until then, a label that cannot lie.)
504
+ const profileOf = currentProfileName === null ? "" : ` · profile ${currentProfileName}`;
505
+ bodyLog(`model ${agentModel}${providerLabel(agentBaseUrl)}${profileOf}`);
477
506
  ctx.input.prompt();
478
507
  });
479
508
  return;
@@ -567,8 +596,20 @@ export function dispatch(line, ctx) {
567
596
  // the note keeps what tells two rows apart; the levels
568
597
  // left it for the axis below (they were being cut off
569
598
  // the end of the note column at 100 columns).
570
- const marks = [`profile: ${name}`, ...(profileAvailable(profile) ? [] : ["unavailable"]), ...(profile.model === agentModel ? ["current"] : [])];
571
- return { label: `${profile.kind}/${profile.model}`, note: marks.join(" · "), ...effortAxis(profile) };
599
+ // The owner, 2026-09-21: two profiles can name ONE model
600
+ // id and reach two accounts, so the current mark follows
601
+ // the PROFILE the session is on (`currentModelName`), not
602
+ // the model id — the id alone marked both such rows, or
603
+ // neither. The row's own provider rides the label, so two
604
+ // rows that share an id still read differently.
605
+ // 0.40.1 (the owner's dogfood, 2026-09-21): the `profile: <key>` prefix
606
+ // is gone — the row already names the model and its endpoint, and the
607
+ // profile key is what `/model <name>` takes, not what a chooser
608
+ // reading a list needs. What remains is what the chooser CANNOT
609
+ // infer: availability, and which one is live.
610
+ const marks = [...(profileAvailable(profile) ? [] : ["unavailable"]), ...(name === currentProfileName ? ["current"] : [])];
611
+ const host = profileProviderLabel(profile.kind, profile.baseUrl);
612
+ return { label: `${profile.kind}/${profile.model}${host === "" ? "" : ` ${host}`}`, note: marks.join(" · "), ...effortAxis(profile) };
572
613
  }),
573
614
  // PH-1a (finding PH-F4): the example must be a syntax
574
615
  // directWriteProfile actually ACCEPTS — the old
@@ -617,7 +658,8 @@ export function dispatch(line, ctx) {
617
658
  else {
618
659
  for (const name of names) {
619
660
  const p = configModels[name];
620
- bodyLog(` ${name} → ${p.kind}/${p.model} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
661
+ const hostOf = profileProviderLabel(p.kind, p.baseUrl);
662
+ bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
621
663
  }
622
664
  }
623
665
  bodyLog("switch with /model <profile-name|provider/model>");
@@ -713,6 +755,7 @@ export function dispatch(line, ctx) {
713
755
  setLastBinding(binding);
714
756
  setAgentModel(profile.model, profile.baseUrl);
715
757
  setCurrentModelName(arg);
758
+ setCurrentProfileName(direct === null ? null : profName);
716
759
  // §2.5: the ONE source a reload reads for the model — a
717
760
  // switch made here must survive the rebuild.
718
761
  setModelChoice(arg);
@@ -732,7 +775,7 @@ export function dispatch(line, ctx) {
732
775
  // (and the owner's) read `gpt-6-astra · CH 92%`, the new
733
776
  // model beside the previous model's figure.
734
777
  ctx.modelSwitched();
735
- body.notice(`model → ${profName} (${profile.model}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
778
+ body.notice(`model → ${profName} (${profile.model}${providerLabel(profile.baseUrl) === "" ? "" : ` ${providerLabel(profile.baseUrl)}`}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
736
779
  }
737
780
  }
738
781
  }
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ import { floorExtension, isDestructiveCall } from "./floor.js";
45
45
  import { protectedShellExtension } from "./protected-shell.js";
46
46
  import { breakerExtension } from "./breaker.js";
47
47
  import { builtInLayer } from "./builtin.js";
48
- import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setUserProtectedPaths, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, setRetryShown, setNeverInherited, secretEnvNamesOf, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, setFloorOn, floorOn, loadedSkillsCatalog } from "./state.js";
48
+ import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setUserProtectedPaths, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, setRetryShown, setNeverInherited, secretEnvNamesOf, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, setFloorOn, floorOn, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
49
49
  import { maxRetriesFromEnv } from "./retries.js";
50
50
  import { askUi, resolveProjectTrust } from "./trust-ui.js";
51
51
  import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
@@ -468,6 +468,7 @@ function makeLineInput() {
468
468
  // the panel's option rows, so the compositor is what the editor asks
469
469
  // where they are. Neither side computes the other's geometry.
470
470
  editor.bindPanelRows(() => dock.panelOptionRows());
471
+ editor.bindPickWindow(() => dock.visiblePickWindow());
471
472
  dock.bindMenu(() => editor.menuState()); // v3 §04: the slash-command menu
472
473
  editor.bindAtItems(atFiles); // KC3 §5: the file source — listed per OPEN
473
474
  dock.bindAt(() => editor.atState()); // KC3 §4: the picker's band
@@ -785,10 +786,12 @@ async function makeAgent(sessionId, input, modelFlag) {
785
786
  console.log("[faux mode — set ANTHROPIC_API_KEY or OPENAI_API_KEY, or configure models in ~/.kiso/config.json]\n");
786
787
  setCurrentFaux(true);
787
788
  setCurrentModelName("faux");
789
+ setCurrentProfileName(null);
788
790
  }
789
791
  else {
790
792
  setCurrentFaux(false);
791
793
  setCurrentModelName(resolved.name);
794
+ setCurrentProfileName(resolved.name);
792
795
  }
793
796
  setAgentModel(model, resolved?.profile.baseUrl); // v2b: the status bar shows it; OR-1: the endpoint rides along
794
797
  // W21: the extensions array is built ONCE per agent and shared with
@@ -1134,13 +1137,15 @@ function retiredAutoCompact(merged) {
1134
1137
  * threshold got stuck in the first place.
1135
1138
  */
1136
1139
  function bindRestoredSession(session) {
1137
- // 0.40.0: an acknowledged drift says what it did, once — including the
1138
- // owner-ruled reasoning reset, which was silent until now.
1140
+ // 0.40.1 (the owner's ruling of 2026-09-21): a changed binding is neither
1141
+ // blocked nor "acknowledged" — it is recorded as the next revision under
1142
+ // the CURRENT configuration, and this ONE line says which binding and why
1143
+ // (the owner-ruled reasoning reset rides with it, silently until 0.40.0).
1139
1144
  const ack = session.driftAcknowledgement ?? null;
1140
1145
  if (ack !== null) {
1141
1146
  const was = ack.reasoningReset;
1142
1147
  const prior = [was.effort !== "default" ? `effort ${was.effort}` : "", was.thinking !== "default" ? `thinking ${was.thinking}` : ""].filter((s) => s !== "").join(", ");
1143
- bodyLog(`drift acknowledged — now on ${session.model}; reasoning reset to defaults${prior === "" ? "" : ` (was ${prior})`}`);
1148
+ bodyLog(`binding changed — now on ${session.model}: ${ack.reasons.join("; ")}; reasoning reset to defaults${prior === "" ? "" : ` (was ${prior})`}`);
1144
1149
  }
1145
1150
  setAgentModel(session.model, session.baseUrl);
1146
1151
  session.setMicrocompactThreshold(microcompactThresholdFor({
@@ -1261,6 +1266,7 @@ async function reloadAgent(old, id, input, announce = true) {
1261
1266
  setConfiguredWindow(oldCfg.window);
1262
1267
  setCurrentFaux(oldCfg.faux);
1263
1268
  setCurrentModelName(oldCfg.modelName);
1269
+ setCurrentProfileName(null); // the reload snapshot carries no profile name: no mark beats a guessed one
1264
1270
  setModelChoice(oldCfg.choice);
1265
1271
  setAgentModel(oldCfg.agent, oldCfg.endpoint);
1266
1272
  if (oldCfg.delegation === undefined)
@@ -1294,6 +1300,15 @@ async function chatLoop(first, firstId, input, autoCompact) {
1294
1300
  // reload said what it did; repeating the session's opening would read
1295
1301
  // as a new session, which is the one thing it is not.
1296
1302
  let rebuilt = false;
1303
+ // XP-1: the id of the session that IS open — null until the first open
1304
+ // succeeds, and cleared by a refused switch, so one refusal can never
1305
+ // recurse into another.
1306
+ let opened = null;
1307
+ // XP-1: the re-open that follows a refused switch. The session is the one
1308
+ // already on screen, so this step prints neither banner nor tail.
1309
+ let refused = false;
1310
+ /** DC-57: lines that arrived with a switch command belong to the session being asked for. */
1311
+ let seed = [];
1297
1312
  for (;;) {
1298
1313
  // 0.40.0 (the lead's ruling): a session opens in the folder that holds
1299
1314
  // it, never moved — a switch that crosses folders rebuilds the agent
@@ -1318,10 +1333,50 @@ async function chatLoop(first, firstId, input, autoCompact) {
1318
1333
  }
1319
1334
  agent = next;
1320
1335
  }
1321
- const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
1336
+ let session;
1337
+ try {
1338
+ session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
1339
+ }
1340
+ catch (err) {
1341
+ // The profile contract's BLOCKED cases — a sidecar that cannot be
1342
+ // read, an XP-era log without one — belong to the ENTRY: nothing is
1343
+ // on screen yet, and the message says what to do. An in-session
1344
+ // SWITCH is a LINE: it switches NOTHING and the session you were in
1345
+ // stays open. The throw used to escape chatLoop, which killed the
1346
+ // REPL and left the session that was open behind with no log at all
1347
+ // (announced once, then empty and unopenable). Every refusal this
1348
+ // open can throw is treated the same way — the message itself is
1349
+ // what says what to do about it.
1350
+ if (opened === null)
1351
+ throw err;
1352
+ bodyLog(escapeTerminal(err.message));
1353
+ // DC-57's other half + DC-55 (the owner's ruling of 2026-09-21,
1354
+ // option ①): the lines that arrived WITH the refused switch were aimed
1355
+ // at a session that EXISTS and could not be opened. They are NOT run
1356
+ // here — a departing session never answers lines meant for the target —
1357
+ // and not dropped in silence either: what was held is printed, so
1358
+ // nothing vanishes without the person seeing it. (Leaving them in the
1359
+ // queue would hand them to the old session on its next entry, which is
1360
+ // the thing this ruling forbids.)
1361
+ seed = [];
1362
+ const held = queuedSwitchLines.splice(0);
1363
+ if (held.length > 0) {
1364
+ bodyLog(`[${held.length} line${held.length === 1 ? "" : "s"} held back — the switch was refused, so nothing was run]`);
1365
+ for (const line of held)
1366
+ bodyLog(` ${escapeTerminal(line)}`);
1367
+ }
1368
+ id = opened; // the switch is undone
1369
+ opened = null;
1370
+ refused = true;
1371
+ continue;
1372
+ }
1373
+ opened = id;
1322
1374
  if (rebuilt) {
1323
1375
  rebuilt = false;
1324
1376
  }
1377
+ else if (refused) {
1378
+ refused = false; // back in the session that was already on screen
1379
+ }
1325
1380
  else if (prev === null) {
1326
1381
  bodyLog(`session ${id}\n`);
1327
1382
  // REL-0152-D5: a session with history says what that history WAS.
@@ -1379,6 +1434,12 @@ async function chatLoop(first, firstId, input, autoCompact) {
1379
1434
  // the threshold. One step, shared by all three entry points.
1380
1435
  bindRestoredSession(session);
1381
1436
  setCurrentModelName(session.model);
1437
+ // A SWITCH knows only a model id here — nothing that names the profile —
1438
+ // so the mark is cleared rather than guessed. A FRESH start keeps what
1439
+ // makeAgent resolved from `--model <profile>`, which is the one place
1440
+ // the name is known (prev is null only on the first entry).
1441
+ if (prev !== null)
1442
+ setCurrentProfileName(null);
1382
1443
  paintBootStatus(session);
1383
1444
  // The terminal's window title, HERE for the same reason the three
1384
1445
  // lines above are here: this is the one step all three entry points
@@ -1392,9 +1453,12 @@ async function chatLoop(first, firstId, input, autoCompact) {
1392
1453
  route: (sessionId) => routeSession(sessionId),
1393
1454
  ...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
1394
1455
  };
1395
- const end = await chat(session, currentFaux, input, autoCompact, nav);
1456
+ const end = await chat(session, currentFaux, input, autoCompact, nav, seed);
1396
1457
  if (end.next === "exit")
1397
1458
  return;
1459
+ // DC-57: the lines that arrived with the switch command ride INTO the
1460
+ // next entry — they were aimed at the session being asked for.
1461
+ seed = end.lines ?? [];
1398
1462
  if (end.next === "reload") {
1399
1463
  agent = await reloadAgent(agent, id, input);
1400
1464
  rebuilt = true;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The provider's NAME on screen — the host the request will actually go to.
3
+ *
4
+ * The owner, 2026-09-21: switching models must say WHO is being billed. Two
5
+ * profiles can name the same model id and reach two different accounts; the
6
+ * model id alone leaves the person unable to tell which one they are about
7
+ * to spend, which is exactly the mistake a picker exists to prevent.
8
+ *
9
+ * The fact is already in hand: `agentBaseUrl` rides with `agentModel` (one
10
+ * `setAgentModel(model, baseUrl)` call, so the two cannot drift), and a
11
+ * profile's `baseUrl` is what its row shows. This module turns that URL into
12
+ * the one label worth spending columns on — the HOST — with the provider id
13
+ * as the fallback for a binding that names no URL at all.
14
+ *
15
+ * Deliberately not a new rule: `providerIdOf` (auth/credentials) is the CLI's
16
+ * copy of the runtime's origin rule (KNOWN_ORIGINS / resolveContinuationScope),
17
+ * and this function only chooses what to PRINT from the same inputs.
18
+ */
19
+ /** The host of a base URL, without scheme, credentials or any path:
20
+ * `https://api.commandcode.ai/provider/v1` → `commandcode.ai`.
21
+ *
22
+ * `null` when there is no URL to read — never a guess. A URL that does not
23
+ * parse is not invented either: the caller falls back to the provider id. */
24
+ export declare function providerHost(baseUrl?: string): string | null;
25
+ /** `@commandcode.ai` — the provider named beside a model. `""` when the
26
+ * binding names no endpoint (the provider's own default origin), so a row
27
+ * that has nothing to say does not print a stray `@`. */
28
+ export declare function providerLabel(baseUrl?: string): string;
29
+ /** The same label for a PROFILE, which knows its kind as well as its URL: a
30
+ * profile with no baseUrl (a first-party provider) still names itself by
31
+ * providerId, because a row that shows two profiles must tell them apart. */
32
+ export declare function profileProviderLabel(kind: string, baseUrl?: string): string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The provider's NAME on screen — the host the request will actually go to.
3
+ *
4
+ * The owner, 2026-09-21: switching models must say WHO is being billed. Two
5
+ * profiles can name the same model id and reach two different accounts; the
6
+ * model id alone leaves the person unable to tell which one they are about
7
+ * to spend, which is exactly the mistake a picker exists to prevent.
8
+ *
9
+ * The fact is already in hand: `agentBaseUrl` rides with `agentModel` (one
10
+ * `setAgentModel(model, baseUrl)` call, so the two cannot drift), and a
11
+ * profile's `baseUrl` is what its row shows. This module turns that URL into
12
+ * the one label worth spending columns on — the HOST — with the provider id
13
+ * as the fallback for a binding that names no URL at all.
14
+ *
15
+ * Deliberately not a new rule: `providerIdOf` (auth/credentials) is the CLI's
16
+ * copy of the runtime's origin rule (KNOWN_ORIGINS / resolveContinuationScope),
17
+ * and this function only chooses what to PRINT from the same inputs.
18
+ */
19
+ import { providerIdOf } from "./auth/credentials.js";
20
+ /** The host of a base URL, without scheme, credentials or any path:
21
+ * `https://api.commandcode.ai/provider/v1` → `commandcode.ai`.
22
+ *
23
+ * `null` when there is no URL to read — never a guess. A URL that does not
24
+ * parse is not invented either: the caller falls back to the provider id. */
25
+ export function providerHost(baseUrl) {
26
+ if (baseUrl === undefined || baseUrl.trim() === "")
27
+ return null;
28
+ try {
29
+ const url = new URL(baseUrl);
30
+ // REVIEW (2026-09-21): `host`, not `hostname` — the PORT is part of the
31
+ // answer this label exists to give (`localhost:11434` and
32
+ // `localhost:8000` are two different places to spend, and both read as
33
+ // `@localhost` under `hostname`).
34
+ const host = url.host;
35
+ return host === "" ? null : host;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /** `@commandcode.ai` — the provider named beside a model. `""` when the
42
+ * binding names no endpoint (the provider's own default origin), so a row
43
+ * that has nothing to say does not print a stray `@`. */
44
+ export function providerLabel(baseUrl) {
45
+ const host = providerHost(baseUrl);
46
+ return host === null ? "" : `@${host}`;
47
+ }
48
+ /** The same label for a PROFILE, which knows its kind as well as its URL: a
49
+ * profile with no baseUrl (a first-party provider) still names itself by
50
+ * providerId, because a row that shows two profiles must tell them apart. */
51
+ export function profileProviderLabel(kind, baseUrl) {
52
+ return providerLabel(baseUrl) || `@${providerIdOf(kind, baseUrl)}`;
53
+ }
package/dist/state.d.ts CHANGED
@@ -275,6 +275,19 @@ export declare let modelChoice: string | undefined;
275
275
  export declare function setModelChoice(value: string | undefined): void;
276
276
  export declare let currentModelName: string;
277
277
  export declare function setCurrentModelName(value: string): void;
278
+ /** REVIEW (2026-09-21): WHO the binding came from is a different fact from
279
+ * WHICH model it names, and `currentModelName` carried both — a profile alias
280
+ * after `/model`, a model id after a switch, a resume or a reload. Surfaces
281
+ * that want to name the PROFILE ask this one; an unknown profile is null, and
282
+ * nothing guesses a credential identity out of a model name and a host. */
283
+ export declare let currentProfileName: string | null;
284
+ export declare function setCurrentProfileName(value: string | null): void;
285
+ /** DC-57 (the owner's ruling, 2026-09-21): lines that arrived with a switch
286
+ * command — a paste, a pipe, a scripted driver — belong to the session the
287
+ * person asked for. The departing entry queues them here and the next
288
+ * `chat()` drains them into its own replay, so no line is answered by the
289
+ * session being left and none is dropped. */
290
+ export declare const queuedSwitchLines: string[];
278
291
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
279
292
  /** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
280
293
  * config's `floor`, read where the chain is assembled. */
package/dist/state.js CHANGED
@@ -368,6 +368,21 @@ export let currentModelName = "faux";
368
368
  export function setCurrentModelName(value) {
369
369
  currentModelName = value;
370
370
  }
371
+ /** REVIEW (2026-09-21): WHO the binding came from is a different fact from
372
+ * WHICH model it names, and `currentModelName` carried both — a profile alias
373
+ * after `/model`, a model id after a switch, a resume or a reload. Surfaces
374
+ * that want to name the PROFILE ask this one; an unknown profile is null, and
375
+ * nothing guesses a credential identity out of a model name and a host. */
376
+ export let currentProfileName = null;
377
+ export function setCurrentProfileName(value) {
378
+ currentProfileName = value;
379
+ }
380
+ /** DC-57 (the owner's ruling, 2026-09-21): lines that arrived with a switch
381
+ * command — a paste, a pipe, a scripted driver — belong to the session the
382
+ * person asked for. The departing entry queues them here and the next
383
+ * `chat()` drains them into its own replay, so no line is answered by the
384
+ * session being left and none is dropped. */
385
+ export const queuedSwitchLines = [];
371
386
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
372
387
  /** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
373
388
  * config's `floor`, read where the chain is assembled. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.40.0",
3
+ "version": "0.40.1",
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",
@@ -18,20 +18,20 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.40.0",
22
- "@vincemakes/kiso-core": "0.40.0",
23
- "@vincemakes/kiso-evals": "0.40.0",
24
- "@vincemakes/kiso-mcp-ext": "0.40.0",
25
- "@vincemakes/kiso-provider-anthropic": "0.40.0",
26
- "@vincemakes/kiso-provider-openai": "0.40.0",
27
- "@vincemakes/kiso-provider-openai-responses": "0.40.0",
28
- "@vincemakes/kiso-runtime": "0.40.0",
29
- "@vincemakes/kiso-skills-ext": "0.40.0",
30
- "@vincemakes/kiso-subagent-ext": "0.40.0",
31
- "@vincemakes/kiso-task-ext": "0.40.0",
32
- "@vincemakes/kiso-tools-node": "0.40.0",
33
- "@vincemakes/kiso-tui": "0.40.0",
34
- "@vincemakes/kiso-tui-cells": "0.40.0"
21
+ "@vincemakes/kiso-ask-ext": "0.40.1",
22
+ "@vincemakes/kiso-core": "0.40.1",
23
+ "@vincemakes/kiso-evals": "0.40.1",
24
+ "@vincemakes/kiso-mcp-ext": "0.40.1",
25
+ "@vincemakes/kiso-provider-anthropic": "0.40.1",
26
+ "@vincemakes/kiso-provider-openai": "0.40.1",
27
+ "@vincemakes/kiso-provider-openai-responses": "0.40.1",
28
+ "@vincemakes/kiso-runtime": "0.40.1",
29
+ "@vincemakes/kiso-skills-ext": "0.40.1",
30
+ "@vincemakes/kiso-subagent-ext": "0.40.1",
31
+ "@vincemakes/kiso-task-ext": "0.40.1",
32
+ "@vincemakes/kiso-tools-node": "0.40.1",
33
+ "@vincemakes/kiso-tui": "0.40.1",
34
+ "@vincemakes/kiso-tui-cells": "0.40.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.1.2",