@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 +3 -1
- package/dist/chat.js +26 -5
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +48 -5
- package/dist/index.js +70 -6
- package/dist/provider-label.d.ts +32 -0
- package/dist/provider-label.js +53 -0
- package/dist/state.d.ts +13 -0
- package/dist/state.js +15 -0
- package/package.json +15 -15
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
|
}
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -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
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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.
|
|
1138
|
-
//
|
|
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(`
|
|
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
|
-
|
|
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.
|
|
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.
|
|
22
|
-
"@vincemakes/kiso-core": "0.40.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.40.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.40.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.40.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.40.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.40.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.40.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.40.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.40.
|
|
31
|
-
"@vincemakes/kiso-task-ext": "0.40.
|
|
32
|
-
"@vincemakes/kiso-tools-node": "0.40.
|
|
33
|
-
"@vincemakes/kiso-tui": "0.40.
|
|
34
|
-
"@vincemakes/kiso-tui-cells": "0.40.
|
|
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",
|