@vincemakes/kiso-code 0.32.1 → 0.33.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/auth/credentials.js +39 -9
- package/dist/auth/refresh.js +31 -4
- package/dist/byte-trace.js +13 -2
- package/dist/chat.d.ts +1 -1
- package/dist/chat.js +46 -6
- package/dist/external-editor.js +7 -2
- package/dist/first-run.js +8 -3
- package/dist/index.js +22 -4
- package/dist/resume-tail.js +17 -2
- package/dist/resume.js +4 -1
- package/dist/tmux-hint.d.ts +22 -0
- package/dist/tmux-hint.js +26 -0
- package/package.json +15 -15
package/dist/auth/credentials.js
CHANGED
|
@@ -45,22 +45,52 @@ const ORIGIN_TO_PROVIDER = {
|
|
|
45
45
|
* openai-CHAT profiles, and the subscription backend serves only the
|
|
46
46
|
* Responses dialect. */
|
|
47
47
|
const CHATGPT_ORIGIN = "https://chatgpt.com";
|
|
48
|
+
/** R1 — the vendors' OWN origins. A profile whose baseUrl points anywhere
|
|
49
|
+
* else authenticates with `apiKeyEnv` and nothing else, however familiar
|
|
50
|
+
* the dialect it speaks. */
|
|
51
|
+
const ANTHROPIC_ORIGIN = "https://api.anthropic.com";
|
|
52
|
+
const OPENAI_ORIGIN = "https://api.openai.com";
|
|
48
53
|
/** The provider identity a profile's credential is stored under, or null
|
|
49
54
|
* for an origin nobody recognizes (custom: env var only). */
|
|
50
55
|
export function providerIdOf(kind, baseUrl) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// R1 — A STORED CREDENTIAL NEVER LEAVES THE VENDOR'S OWN ORIGIN.
|
|
57
|
+
//
|
|
58
|
+
// This returned `anthropic` for every anthropic-kind profile and
|
|
59
|
+
// `openai` for every non-ChatGPT Responses profile, whatever the
|
|
60
|
+
// baseUrl said. `authForProfile` takes the stored credential BEFORE
|
|
61
|
+
// the profile's `apiKeyEnv`, so a profile pointing at a proxy — with
|
|
62
|
+
// an explicit key named for that proxy — sent THE VENDOR'S STORED KEY
|
|
63
|
+
// to the proxy and never read the env var at all. Reproduced against a
|
|
64
|
+
// local capture in the 2026-09-10 external review.
|
|
65
|
+
//
|
|
66
|
+
// OR-1's reasoning is superseded for this question, and the sentence it
|
|
67
|
+
// turned on is where it went wrong: "a Responses endpoint that is
|
|
68
|
+
// neither is still OpenAI's API shape behind a proxy, and its
|
|
69
|
+
// credential is OpenAI's". The DIALECT is OpenAI's; the CREDENTIAL is
|
|
70
|
+
// not. Who speaks the protocol and who should be paid to answer are
|
|
71
|
+
// different questions, and only the second one decides where a secret
|
|
72
|
+
// may go.
|
|
73
|
+
//
|
|
74
|
+
// The compat kind was already right — an unrecognized origin resolves
|
|
75
|
+
// to null and the env var pays — so this makes the other two agree with
|
|
76
|
+
// the one that had the rule.
|
|
77
|
+
const origin = originOf(baseUrl);
|
|
78
|
+
if (kind === "anthropic") {
|
|
79
|
+
if (baseUrl === undefined)
|
|
80
|
+
return "anthropic"; // the vendor's own default endpoint
|
|
81
|
+
return origin === ANTHROPIC_ORIGIN ? "anthropic" : null;
|
|
82
|
+
}
|
|
83
|
+
if (kind === "openai-responses") {
|
|
84
|
+
if (baseUrl === undefined)
|
|
85
|
+
return "openai"; // the vendor's own default endpoint
|
|
86
|
+
if (origin === CHATGPT_ORIGIN)
|
|
87
|
+
return "chatgpt";
|
|
88
|
+
return origin === OPENAI_ORIGIN ? "openai" : null;
|
|
89
|
+
}
|
|
59
90
|
if (kind !== "openai-compat")
|
|
60
91
|
return null;
|
|
61
92
|
if (baseUrl === undefined)
|
|
62
93
|
return "openai";
|
|
63
|
-
const origin = originOf(baseUrl);
|
|
64
94
|
return origin === null ? null : (ORIGIN_TO_PROVIDER[origin] ?? null);
|
|
65
95
|
}
|
|
66
96
|
function originOf(baseUrl) {
|
package/dist/auth/refresh.js
CHANGED
|
@@ -29,12 +29,39 @@ export async function ensureFresh(providerId, flow, path, now = Date.now(), toke
|
|
|
29
29
|
if (refreshed === undefined)
|
|
30
30
|
throw new AuthError(`signed out of ${providerId} (the token could not be refreshed: ${error?.message ?? "unknown"}) — run \`kiso login ${providerId}\``);
|
|
31
31
|
const written = refreshed;
|
|
32
|
+
// R2a — THE WRITE-BACK MAY NOT RESURRECT WHAT THE HUMAN REMOVED.
|
|
33
|
+
//
|
|
34
|
+
// `flow.refresh` is awaited outside the store lock, so anything can
|
|
35
|
+
// happen to the file while it is on the wire. The old condition kept
|
|
36
|
+
// only a LATER-EXPIRING entry, which asks the wrong question: an entry
|
|
37
|
+
// deleted by `kiso logout` is not later-expiring, it is ABSENT, so the
|
|
38
|
+
// branch fell through and wrote the old credential back — a sign-out
|
|
39
|
+
// the human performed, undone by a network round trip they never saw.
|
|
40
|
+
// A new login of another type was overwritten the same way: an api-key
|
|
41
|
+
// entry is not an oauth entry with a later expiry either.
|
|
42
|
+
//
|
|
43
|
+
// The question that is actually being asked is "is this still the
|
|
44
|
+
// generation I began from?", so that is what is compared: the refresh
|
|
45
|
+
// token and the expiry captured BEFORE the await. Absent, a different
|
|
46
|
+
// type, or a different generation — including the concurrent refresh
|
|
47
|
+
// the old condition was written for — all leave the file alone.
|
|
48
|
+
const began = current;
|
|
32
49
|
modifyAuthFile((file) => {
|
|
33
50
|
const latest = file.credentials[providerId];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
if (latest === undefined)
|
|
52
|
+
return file; // logged out while we were on the wire
|
|
53
|
+
if (latest.type !== "oauth")
|
|
54
|
+
return file; // replaced by a login of another type
|
|
55
|
+
if (latest.refresh !== began.refresh || latest.expires !== began.expires)
|
|
56
|
+
return file; // a different generation
|
|
37
57
|
return { version: 1, credentials: { ...file.credentials, [providerId]: written } };
|
|
38
58
|
}, path);
|
|
39
|
-
|
|
59
|
+
// What the FILE holds, not what this refresh produced: if the write was
|
|
60
|
+
// declined the caller must be told the truth on disk, and an absent
|
|
61
|
+
// entry is the not-signed-in error rather than an undefined cast to a
|
|
62
|
+
// Credential.
|
|
63
|
+
const after = readAuthFile(path).credentials[providerId];
|
|
64
|
+
if (after === undefined)
|
|
65
|
+
throw new AuthError(`not signed in to ${providerId}: run \`kiso login ${providerId}\``);
|
|
66
|
+
return after;
|
|
40
67
|
}
|
package/dist/byte-trace.js
CHANGED
|
@@ -41,7 +41,13 @@ function line(dir, data) {
|
|
|
41
41
|
return;
|
|
42
42
|
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
43
43
|
try {
|
|
44
|
-
|
|
44
|
+
// R6b: 0600 at CREATION. This file is every keystroke and every
|
|
45
|
+
// paste, base64 — the session log plus the things that never became
|
|
46
|
+
// a turn, including anything typed and deleted. It is opt-in
|
|
47
|
+
// debugging, which makes it easy to leave behind and easy to forget
|
|
48
|
+
// is there; it is the most sensitive file kiso writes and it was the
|
|
49
|
+
// least protected. An existing file keeps its mode (R6's rule).
|
|
50
|
+
appendFileSync(path, `${JSON.stringify({ ms: Date.now() - start, dir, n: buf.length, b: buf.toString("base64") })}\n`, { mode: 0o600 });
|
|
45
51
|
}
|
|
46
52
|
catch {
|
|
47
53
|
// a trace that fails must never take the session with it
|
|
@@ -65,7 +71,12 @@ export function armByteTrace() {
|
|
|
65
71
|
path = target;
|
|
66
72
|
start = Date.now();
|
|
67
73
|
try {
|
|
68
|
-
|
|
74
|
+
// R6b: the mode belongs HERE, where the file is created. The append
|
|
75
|
+
// below states it too, but `appendFileSync`'s mode only applies when
|
|
76
|
+
// it creates the file — and this truncating write always gets there
|
|
77
|
+
// first, so without this the trace was born 0644 and the append
|
|
78
|
+
// never had a chance to say otherwise.
|
|
79
|
+
writeFileSync(path, "", { mode: 0o600 });
|
|
69
80
|
}
|
|
70
81
|
catch {
|
|
71
82
|
path = null;
|
package/dist/chat.d.ts
CHANGED
|
@@ -183,7 +183,7 @@ export declare function parseSaferOptions(text: string): SaferOption[] | null;
|
|
|
183
183
|
* task file has none. 50 is named here; a finding moves it, never a stray
|
|
184
184
|
* edit. */
|
|
185
185
|
export declare const CHECKPOINT_TURNS = 50;
|
|
186
|
-
export declare function consumeRun(session: AgentSession, run: Run, input: LineInput, turnNo: number, faux: boolean, statusCb: ((usage: RunUsage, ctxRatio: number, costUsd?: number | null) => void) | null,
|
|
186
|
+
export declare function consumeRun(session: AgentSession, run: Run, input: LineInput, turnNo: number, faux: boolean, statusCb: ((usage: RunUsage, ctxRatio: number, costUsd?: number | null, tokPerSec?: number | null) => void) | null,
|
|
187
187
|
/** W21: the amend words ("Yes + feedback") ride the NEXT user turn —
|
|
188
188
|
* threaded from chat's submitTurn; absent in the recovery flow
|
|
189
189
|
* (resume) where a dropped amend is noticed instead. */
|
package/dist/chat.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* estimates. All bodies moved verbatim from index.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, statSync } from "node:fs";
|
|
8
|
-
import { escapeTerminal, cacheHitPct, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, verifyOfferView, STATUS_GLYPHS, } from "@vincemakes/kiso-tui";
|
|
8
|
+
import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, verifyOfferView, STATUS_GLYPHS, } from "@vincemakes/kiso-tui";
|
|
9
9
|
import { askView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
10
|
import { canonicalTargetPath, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
|
+
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
11
12
|
import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
12
13
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
13
14
|
import { dispatch, abortBangCommand } from "./dispatch.js";
|
|
@@ -580,6 +581,10 @@ export async function consumeRun(session, run, input, turnNo, faux, statusCb,
|
|
|
580
581
|
submitTurn) {
|
|
581
582
|
let last;
|
|
582
583
|
let usage = { in: null, out: null, cache: null, known: false };
|
|
584
|
+
// TPS-1: the decode clock, armed by the FIRST streamed event of a call
|
|
585
|
+
// and read at that call's usage event. Per CALL, not per turn — a turn
|
|
586
|
+
// with three model calls reports the third, and each one times itself.
|
|
587
|
+
let callFirstEventAt = null;
|
|
583
588
|
let prevTotal = null;
|
|
584
589
|
let missed = null;
|
|
585
590
|
// v3 §02: the recap line derives ENTIRELY from the local event stream
|
|
@@ -638,9 +643,12 @@ submitTurn) {
|
|
|
638
643
|
body.notice(` ${typeof ev.content === "string" ? ev.content : ""}`);
|
|
639
644
|
break;
|
|
640
645
|
}
|
|
641
|
-
|
|
646
|
+
// DC-60: a turn that carries an image is a content ARRAY — its words
|
|
647
|
+
// still echo, the image as a mark; an empty chip was the bug.
|
|
648
|
+
body.userLine(echoText(ev.content));
|
|
642
649
|
break;
|
|
643
650
|
case "thinking":
|
|
651
|
+
callFirstEventAt ??= Date.now(); // TPS-1: reasoning IS decoding
|
|
644
652
|
body.thinkingAppend(ev.text);
|
|
645
653
|
break;
|
|
646
654
|
case "tool_call_end":
|
|
@@ -691,6 +699,7 @@ submitTurn) {
|
|
|
691
699
|
break;
|
|
692
700
|
}
|
|
693
701
|
case "text_delta":
|
|
702
|
+
callFirstEventAt ??= Date.now(); // TPS-1
|
|
694
703
|
body.textAppend(ev.text);
|
|
695
704
|
break;
|
|
696
705
|
case "text_end":
|
|
@@ -712,7 +721,12 @@ submitTurn) {
|
|
|
712
721
|
missed = delta.missed;
|
|
713
722
|
// TUI2-R1 (E): the request's canonical cost rides the same
|
|
714
723
|
// callback the usage does — one settled request, one addition.
|
|
715
|
-
|
|
724
|
+
// TPS-1: the rate of the call that just settled. The clock is
|
|
725
|
+
// cleared here rather than at the turn boundary, so a turn's
|
|
726
|
+
// second call is timed from ITS own first event.
|
|
727
|
+
const tokPerSec = callFirstEventAt === null ? null : decodeRate(ev.outputTokens, Date.now() - callFirstEventAt);
|
|
728
|
+
callFirstEventAt = null;
|
|
729
|
+
statusCb?.(usage, displayCtxRatio(session), delta.costUsd, tokPerSec);
|
|
716
730
|
break;
|
|
717
731
|
}
|
|
718
732
|
case "uncertain_pending":
|
|
@@ -962,6 +976,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
962
976
|
// rotates every 200ms; the idle state returns after the run.
|
|
963
977
|
runStart = Date.now();
|
|
964
978
|
runUsage = { in: null, out: null, cache: null, known: false };
|
|
979
|
+
lastTokPerSec = null; // TPS-1: nothing has settled in THIS turn yet
|
|
965
980
|
const stopSpinner = startStatusSpinner((g) => {
|
|
966
981
|
runGlyph = g;
|
|
967
982
|
paintRunning();
|
|
@@ -1153,11 +1168,25 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1153
1168
|
let spentUsd = null;
|
|
1154
1169
|
let runGlyph = "▖";
|
|
1155
1170
|
let runStart = Date.now();
|
|
1171
|
+
// TPS-1: the decode rate of the last SETTLED call. One variable serves
|
|
1172
|
+
// both rows because they want the same number at different moments;
|
|
1173
|
+
// only the reset points differ, and they are all on the ENTRY side —
|
|
1174
|
+
// cleared when a turn starts (so the running row never shows the
|
|
1175
|
+
// previous turn's figure while a new one is in flight) and when the
|
|
1176
|
+
// binding changes (DF-0311-F1: a model that has not run has no
|
|
1177
|
+
// measurement), never at a turn's END, so the idle row can carry the
|
|
1178
|
+
// last call of the last turn.
|
|
1179
|
+
let lastTokPerSec = null;
|
|
1180
|
+
// DF-0330-F1: the terminal's own width, read at paint time so a resize
|
|
1181
|
+
// is honoured. winsize reports 0 columns in some hosts and 0 is not
|
|
1182
|
+
// nullish, so the guard is `> 0` rather than `??` (the same shape the
|
|
1183
|
+
// compositor's own width call uses).
|
|
1184
|
+
const rowWidth = () => (process.stdout.columns > 0 ? process.stdout.columns : 80);
|
|
1156
1185
|
// KC2 §5: the STATE (the glyph, the run's start, the usage, the dock)
|
|
1157
1186
|
// stays here; the ROW's text is the tui's status formatter.
|
|
1158
1187
|
const paintRunning = () => {
|
|
1159
1188
|
if (dock.active)
|
|
1160
|
-
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session)));
|
|
1189
|
+
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec));
|
|
1161
1190
|
};
|
|
1162
1191
|
// W19: under plan the idle row makes the posture unmistakable — the W4
|
|
1163
1192
|
// parentheses idiom names the read-only constraint. The tier is the
|
|
@@ -1171,7 +1200,12 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1171
1200
|
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), statusModelLabel(session), displayCtxRatio(session), {
|
|
1172
1201
|
cacheHitPct: cacheHitPct(runUsage),
|
|
1173
1202
|
costUsd: spentUsd,
|
|
1174
|
-
|
|
1203
|
+
tokPerSec: lastTokPerSec,
|
|
1204
|
+
},
|
|
1205
|
+
// DF-0330-F1: the row's budget. Without it the row is composed
|
|
1206
|
+
// blind and invariant ① cuts whatever sits last — which is how
|
|
1207
|
+
// a measured rate went missing at 100 columns.
|
|
1208
|
+
rowWidth()));
|
|
1175
1209
|
};
|
|
1176
1210
|
// TUI2-R2 ⑥ — the BOOT status line. The row is the product's one
|
|
1177
1211
|
// persistent claim about itself (the tier, how to change it, the model,
|
|
@@ -1195,11 +1229,16 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1195
1229
|
// binding's, and the row does not render it anyway.
|
|
1196
1230
|
const modelSwitched = () => {
|
|
1197
1231
|
runUsage = { in: null, out: null, cache: null, known: false };
|
|
1232
|
+
lastTokPerSec = null; // TPS-1: the rate belonged to the old binding
|
|
1198
1233
|
paintIdle();
|
|
1199
1234
|
};
|
|
1200
|
-
const statusCb = (u, ctx, costUsd) => {
|
|
1235
|
+
const statusCb = (u, ctx, costUsd, tokPerSec) => {
|
|
1201
1236
|
runUsage = u;
|
|
1202
1237
|
addCost(costUsd ?? null);
|
|
1238
|
+
// TPS-1: a call that could not be measured leaves the row as it was
|
|
1239
|
+
// rather than blanking a figure the previous call earned.
|
|
1240
|
+
if (tokPerSec != null)
|
|
1241
|
+
lastTokPerSec = tokPerSec;
|
|
1203
1242
|
paintRunning();
|
|
1204
1243
|
};
|
|
1205
1244
|
// TUI2-R1 (E): the canonical cost of one settled request, added to the
|
|
@@ -1354,6 +1393,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1354
1393
|
// spent, reported as seconds it did.
|
|
1355
1394
|
runStart = Date.now();
|
|
1356
1395
|
runUsage = { in: null, out: null, cache: null, known: false };
|
|
1396
|
+
lastTokPerSec = null; // TPS-1: nothing has settled in THIS turn yet
|
|
1357
1397
|
const last = await consumeRun(session, recoveryRun, input, turnNo, faux, statusCb, submitTurn);
|
|
1358
1398
|
currentRun = null;
|
|
1359
1399
|
failOnFauxExhaustion(last, faux, input);
|
package/dist/external-editor.js
CHANGED
|
@@ -33,11 +33,16 @@ import { join } from "node:path";
|
|
|
33
33
|
export function runExternalEditor(bin, root, text, onFailure) {
|
|
34
34
|
// per INVOCATION, not per run: a directory per call is how the tree
|
|
35
35
|
// grew 400,000 of them once.
|
|
36
|
-
|
|
36
|
+
// R6b: the draft is the human's own words on their way to an editor.
|
|
37
|
+
// `mkdtemp` already gives its own directory 0700, so the file was
|
|
38
|
+
// shielded by WHERE IT SITS rather than by what it is — and the root
|
|
39
|
+
// above it was created with no mode at all. Both stated now, so the
|
|
40
|
+
// shield is the file's own and does not depend on a neighbour's default.
|
|
41
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
37
42
|
const dir = mkdtempSync(join(root, "compose-"));
|
|
38
43
|
const file = join(dir, "message.md"); // .md so the editor lights it up
|
|
39
44
|
try {
|
|
40
|
-
writeFileSync(file, text, "utf8");
|
|
45
|
+
writeFileSync(file, text, { encoding: "utf8", mode: 0o600 });
|
|
41
46
|
const r = spawnSync(bin, [file], { stdio: "inherit", shell: false });
|
|
42
47
|
if (r.error !== undefined || (r.status !== null && r.status !== 0)) {
|
|
43
48
|
onFailure(`[ctrl+g] ${bin} exited ${r.status ?? "abnormally"} — the composer is unchanged`);
|
package/dist/first-run.js
CHANGED
|
@@ -29,9 +29,14 @@ export function isFirstRun() {
|
|
|
29
29
|
* first-run sequence is evidenced by the FILES, not an announcement. */
|
|
30
30
|
export function scaffoldFirstRun() {
|
|
31
31
|
const home = kisoHome();
|
|
32
|
-
|
|
32
|
+
// R6: the home is PRIVATE from its first moment. This is usually the
|
|
33
|
+
// first thing that creates it, so the mode set here is the one every
|
|
34
|
+
// later writer inherits by finding the directory already there. The
|
|
35
|
+
// config may name an env var and the sentinel is empty, but the
|
|
36
|
+
// directory itself will hold session logs and the history.
|
|
37
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
33
38
|
const config = join(home, "config.json");
|
|
34
39
|
if (!existsSync(config))
|
|
35
|
-
writeFileSync(config, CONFIG_SCAFFOLD, "utf8");
|
|
36
|
-
writeFileSync(join(home, SENTINEL), "", "utf8");
|
|
40
|
+
writeFileSync(config, CONFIG_SCAFFOLD, { encoding: "utf8", mode: 0o600 });
|
|
41
|
+
writeFileSync(join(home, SENTINEL), "", { encoding: "utf8", mode: 0o600 });
|
|
37
42
|
}
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ import { chat, contextWindowTokens, displayCtxRatio, statusModelLabel } from "./
|
|
|
45
45
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
46
46
|
import { oauthTokenThunk } from "./auth/token.js";
|
|
47
47
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
48
|
+
import { tmuxMouseHint } from "./tmux-hint.js";
|
|
48
49
|
import { resume } from "./resume.js";
|
|
49
50
|
import { resumeTail } from "./resume-tail.js";
|
|
50
51
|
import { armByteTrace } from "./byte-trace.js";
|
|
@@ -833,7 +834,9 @@ async function pickSession(agent, input) {
|
|
|
833
834
|
function paintBootStatus(session) {
|
|
834
835
|
if (!dock.active)
|
|
835
836
|
return;
|
|
836
|
-
|
|
837
|
+
// DF-0330-F1: the BOOT row gets the budget too — it is the same row, and
|
|
838
|
+
// the opening screen is where a long model id is first seen.
|
|
839
|
+
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), statusModelLabel(session), displayCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
|
|
837
840
|
}
|
|
838
841
|
/** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
|
|
839
842
|
* main's finally still runs (dock teardown, agent close, temp cleanup)
|
|
@@ -1004,6 +1007,14 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1004
1007
|
const known = knownUpdate(updateDeps());
|
|
1005
1008
|
if (known !== null)
|
|
1006
1009
|
paintUpdateCard(known);
|
|
1010
|
+
// DC-59: under tmux without `mouse on`, say why a scroll walks the
|
|
1011
|
+
// history — once, at start, only when it is knowable.
|
|
1012
|
+
const tmuxHint = tmuxMouseHint(process.env, () => {
|
|
1013
|
+
const r = spawnSync("tmux", ["show", "-gv", "mouse"], { encoding: "utf8", timeout: 500 });
|
|
1014
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
1015
|
+
});
|
|
1016
|
+
if (tmuxHint !== null)
|
|
1017
|
+
body.notice(tmuxHint);
|
|
1007
1018
|
void announceUpdate();
|
|
1008
1019
|
}
|
|
1009
1020
|
else {
|
|
@@ -1232,15 +1243,22 @@ async function main() {
|
|
|
1232
1243
|
let seed = [];
|
|
1233
1244
|
try {
|
|
1234
1245
|
seed = readFileSync(historyPath, "utf8").split("\n").filter((l) => l !== "").slice(-500);
|
|
1235
|
-
|
|
1246
|
+
// R6: the rewrite keeps the file private. It only runs when the
|
|
1247
|
+
// file already existed, so this is a trim of the user's own
|
|
1248
|
+
// history rather than a creation — the mode is stated so the trim
|
|
1249
|
+
// cannot widen it.
|
|
1250
|
+
writeFileSync(historyPath, seed.length > 0 ? seed.join("\n") + "\n" : "", { mode: 0o600 });
|
|
1236
1251
|
}
|
|
1237
1252
|
catch {
|
|
1238
1253
|
// no file yet, or unreadable — start empty
|
|
1239
1254
|
}
|
|
1240
1255
|
input.bindHistory(seed, (line) => {
|
|
1241
1256
|
try {
|
|
1242
|
-
|
|
1243
|
-
|
|
1257
|
+
// R6: every submitted line lands here, so the history is as
|
|
1258
|
+
// private as the session logs — 0700 home, 0600 file, at
|
|
1259
|
+
// CREATION. An existing file keeps its mode (not migrated).
|
|
1260
|
+
mkdirSync(kisoHome(), { recursive: true, mode: 0o700 });
|
|
1261
|
+
appendFileSync(historyPath, line.replaceAll("\n", " ") + "\n", { mode: 0o600 });
|
|
1244
1262
|
}
|
|
1245
1263
|
catch {
|
|
1246
1264
|
// best-effort — a full disk never breaks a submit
|
package/dist/resume-tail.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* renders text. It writes nothing, decides nothing, and cannot disagree
|
|
16
16
|
* with the session because it holds no state of its own.
|
|
17
17
|
*/
|
|
18
|
+
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
18
19
|
/** How much of the tail is worth showing. Two turns is enough to
|
|
19
20
|
* recognise a conversation and short enough that resuming does not
|
|
20
21
|
* bury the prompt. */
|
|
@@ -34,10 +35,24 @@ export function resumeTail(events, W = 80) {
|
|
|
34
35
|
let reply = "";
|
|
35
36
|
for (const ev of events) {
|
|
36
37
|
const e = ev;
|
|
37
|
-
if (e.type === "user_input"
|
|
38
|
+
if (e.type === "user_input") {
|
|
39
|
+
// R7b: a turn carrying an IMAGE is a content ARRAY, and this kept
|
|
40
|
+
// only strings — so such a turn was skipped entirely. Skipping an
|
|
41
|
+
// ASK does not merely lose it: the reply that followed attached to
|
|
42
|
+
// the PREVIOUS ask, and the resumed tail showed an answer under a
|
|
43
|
+
// question it did not answer. Worse than showing nothing.
|
|
44
|
+
//
|
|
45
|
+
// Projected with the LIVE ECHO's own function, so the tail and the
|
|
46
|
+
// echo cannot drift: cli → tui-cells is the right direction. The
|
|
47
|
+
// RUNTIME keeps its own `contentText` (R7) because it cannot
|
|
48
|
+
// depend on tui-cells — two implementations, one per direction,
|
|
49
|
+
// agreeing on "(image)".
|
|
50
|
+
const text = echoText(e.content).trim();
|
|
51
|
+
if (text === "")
|
|
52
|
+
continue;
|
|
38
53
|
if (ask !== null)
|
|
39
54
|
turns.push({ ask, reply });
|
|
40
|
-
ask =
|
|
55
|
+
ask = text;
|
|
41
56
|
reply = "";
|
|
42
57
|
}
|
|
43
58
|
else if (e.type === "text_delta" && typeof e.text === "string" && ask !== null) {
|
package/dist/resume.js
CHANGED
|
@@ -33,8 +33,11 @@ export async function resume(session, prompt, faux, input) {
|
|
|
33
33
|
// the recovery flow prints the BARE mode (chat spells plan's posture) —
|
|
34
34
|
// the extraction keeps that difference, it was not asked to settle it.
|
|
35
35
|
const paintIdle = () => {
|
|
36
|
+
// DF-0330-F1: the recovery flow paints the same row, so it gets the
|
|
37
|
+
// same budget. It has no meter, so nothing here can be dropped that
|
|
38
|
+
// was not already at risk of being cut.
|
|
36
39
|
if (dock.active)
|
|
37
|
-
dock.setStatus(idleStatus(getMode(), agentModel, estimateCtxRatio(session)));
|
|
40
|
+
dock.setStatus(idleStatus(getMode(), agentModel, estimateCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
|
|
38
41
|
};
|
|
39
42
|
const withRun = async (run) => {
|
|
40
43
|
currentRun = run;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-59 (0.32.2) — the one thing kiso can say about scrolling under tmux.
|
|
3
|
+
*
|
|
4
|
+
* Under tmux without `mouse on`, the terminal turns wheel and trackpad
|
|
5
|
+
* scrolling into arrow keys for tmux's alternate screen and tmux passes
|
|
6
|
+
* them to the pane: a scroll over the idle composer walks the history
|
|
7
|
+
* (TMUX-F1). The editor's burst guard collapses a wheel NOTCH — three or
|
|
8
|
+
* more identical arrows in one read — but a smooth trackpad stream arrives
|
|
9
|
+
* one arrow per read and is indistinguishable from a hand. With `mouse on`
|
|
10
|
+
* tmux owns the wheel and kiso receives nothing (measured). So kiso names
|
|
11
|
+
* the cause once at start when it is knowable, and never guesses: no
|
|
12
|
+
* `$TMUX`, no question asked; the option unreadable, no hint.
|
|
13
|
+
*
|
|
14
|
+
* PURE on purpose: the decision takes the env and a reader; the spawn that
|
|
15
|
+
* asks tmux lives with the CLI's other spawns (this module's tests are
|
|
16
|
+
* unit tests and must stay so — the pty-manifest gate classifies by import
|
|
17
|
+
* closure).
|
|
18
|
+
*/
|
|
19
|
+
export declare const TMUX_MOUSE_HINT = "under tmux without `mouse on`, trackpad and wheel scrolling arrive as arrow keys and walk the composer history \u2014 add `set -g mouse on` to ~/.tmux.conf";
|
|
20
|
+
/** The hint, or null. `show` reads tmux's global `mouse` option ("on" /
|
|
21
|
+
* "off"), or null when tmux cannot be asked. */
|
|
22
|
+
export declare function tmuxMouseHint(env: Readonly<Record<string, string | undefined>>, show: () => string | null): string | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-59 (0.32.2) — the one thing kiso can say about scrolling under tmux.
|
|
3
|
+
*
|
|
4
|
+
* Under tmux without `mouse on`, the terminal turns wheel and trackpad
|
|
5
|
+
* scrolling into arrow keys for tmux's alternate screen and tmux passes
|
|
6
|
+
* them to the pane: a scroll over the idle composer walks the history
|
|
7
|
+
* (TMUX-F1). The editor's burst guard collapses a wheel NOTCH — three or
|
|
8
|
+
* more identical arrows in one read — but a smooth trackpad stream arrives
|
|
9
|
+
* one arrow per read and is indistinguishable from a hand. With `mouse on`
|
|
10
|
+
* tmux owns the wheel and kiso receives nothing (measured). So kiso names
|
|
11
|
+
* the cause once at start when it is knowable, and never guesses: no
|
|
12
|
+
* `$TMUX`, no question asked; the option unreadable, no hint.
|
|
13
|
+
*
|
|
14
|
+
* PURE on purpose: the decision takes the env and a reader; the spawn that
|
|
15
|
+
* asks tmux lives with the CLI's other spawns (this module's tests are
|
|
16
|
+
* unit tests and must stay so — the pty-manifest gate classifies by import
|
|
17
|
+
* closure).
|
|
18
|
+
*/
|
|
19
|
+
export const TMUX_MOUSE_HINT = "under tmux without `mouse on`, trackpad and wheel scrolling arrive as arrow keys and walk the composer history — add `set -g mouse on` to ~/.tmux.conf";
|
|
20
|
+
/** The hint, or null. `show` reads tmux's global `mouse` option ("on" /
|
|
21
|
+
* "off"), or null when tmux cannot be asked. */
|
|
22
|
+
export function tmuxMouseHint(env, show) {
|
|
23
|
+
if (env.TMUX === undefined || env.TMUX === "")
|
|
24
|
+
return null;
|
|
25
|
+
return show() === "off" ? TMUX_MOUSE_HINT : null;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "kiso-code — 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.
|
|
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-provider-openai-responses": "0.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.
|
|
31
|
-
"@vincemakes/kiso-task-ext": "0.
|
|
32
|
-
"@vincemakes/kiso-tools-node": "0.
|
|
33
|
-
"@vincemakes/kiso-tui": "0.
|
|
34
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.33.0",
|
|
22
|
+
"@vincemakes/kiso-core": "0.33.0",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.33.0",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.33.0",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.33.0",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.33.0",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.33.0",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.33.0",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.33.0",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.33.0",
|
|
31
|
+
"@vincemakes/kiso-task-ext": "0.33.0",
|
|
32
|
+
"@vincemakes/kiso-tools-node": "0.33.0",
|
|
33
|
+
"@vincemakes/kiso-tui": "0.33.0",
|
|
34
|
+
"@vincemakes/kiso-tui-cells": "0.33.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.1.2",
|