@vincemakes/kiso-code 0.39.0 → 0.39.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat.js +8 -0
- package/dist/config.js +14 -0
- package/dist/dispatch.js +71 -7
- package/dist/index.js +6 -0
- package/dist/window-title.d.ts +76 -0
- package/dist/window-title.js +114 -0
- package/package.json +15 -15
package/dist/chat.js
CHANGED
|
@@ -12,6 +12,7 @@ import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
|
12
12
|
import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
13
13
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
14
14
|
import { dispatch, abortBangCommand } from "./dispatch.js";
|
|
15
|
+
import { paintWindowTitle } from "./window-title.js";
|
|
15
16
|
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
16
17
|
import { attachImages } from "./attachments.js";
|
|
17
18
|
import { lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
@@ -781,6 +782,13 @@ submitTurn) {
|
|
|
781
782
|
// construction (ADR-0040).
|
|
782
783
|
switch (ev.type) {
|
|
783
784
|
case "user_input":
|
|
785
|
+
// The window title is re-derived here because THIS is when a
|
|
786
|
+
// session stops being nameless: the tab opened as `kiso —
|
|
787
|
+
// <workspace>` and the first substantive prompt is what gives
|
|
788
|
+
// it a name. Re-derived rather than set, so the opener rule is
|
|
789
|
+
// `sessionTitle`'s and a greeting-first session upgrades when
|
|
790
|
+
// the real prompt lands instead of keeping "hi" forever.
|
|
791
|
+
paintWindowTitle(session.log.all);
|
|
784
792
|
// TV-1B: a system-sourced input is PRODUCT MACHINERY — visible
|
|
785
793
|
// (every durable input renders) but never painted as the
|
|
786
794
|
// user's words. Provenance is honest on screen, not only in
|
package/dist/config.js
CHANGED
|
@@ -102,6 +102,19 @@ export function parseConfig(text, source) {
|
|
|
102
102
|
fail(`models.${name}.promptCaching`, "expected a boolean");
|
|
103
103
|
if (p.streamIdleMs !== undefined && (typeof p.streamIdleMs !== "number" || !Number.isFinite(p.streamIdleMs) || p.streamIdleMs < 0))
|
|
104
104
|
fail(`models.${name}.streamIdleMs`, "expected a non-negative number of milliseconds (0 disables the stream watchdog)");
|
|
105
|
+
// 0.39.2: `contextWindow` was DECLARED on this type, VALIDATED at
|
|
106
|
+
// the top level, and CONSUMED by resolveContextWindow — which
|
|
107
|
+
// prefers it over the global figure, with a comment saying why —
|
|
108
|
+
// and it was never copied here. So it never reached anything: 70
|
|
109
|
+
// of 70 profiles that declared one lost it in this object
|
|
110
|
+
// literal. The cost was not the `ctx ?` on the status row but the
|
|
111
|
+
// compaction threshold, which fell through to the 200k default
|
|
112
|
+
// for every model the registry does not carry — a 1M-window model
|
|
113
|
+
// compacting as though it had a fifth of its window. The
|
|
114
|
+
// documented workaround for exactly that ("set contextWindow in
|
|
115
|
+
// your profile") had never done anything.
|
|
116
|
+
if (p.contextWindow !== undefined && (typeof p.contextWindow !== "number" || !Number.isFinite(p.contextWindow) || p.contextWindow <= 0))
|
|
117
|
+
fail(`models.${name}.contextWindow`, "expected a positive token count");
|
|
105
118
|
models[name] = {
|
|
106
119
|
kind: p.kind,
|
|
107
120
|
model: p.model,
|
|
@@ -109,6 +122,7 @@ export function parseConfig(text, source) {
|
|
|
109
122
|
...(typeof p.baseUrl === "string" ? { baseUrl: p.baseUrl } : {}),
|
|
110
123
|
...(typeof p.promptCaching === "boolean" ? { promptCaching: p.promptCaching } : {}),
|
|
111
124
|
...(typeof p.streamIdleMs === "number" ? { streamIdleMs: p.streamIdleMs } : {}),
|
|
125
|
+
...(typeof p.contextWindow === "number" ? { contextWindow: p.contextWindow } : {}),
|
|
112
126
|
};
|
|
113
127
|
}
|
|
114
128
|
out.models = models;
|
package/dist/dispatch.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* turns. The bodies moved verbatim from chat()'s closure; chat provides
|
|
4
4
|
* the context (the chain, the run state, the prompt arming).
|
|
5
5
|
*/
|
|
6
|
-
import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, palette } from "@vincemakes/kiso-tui";
|
|
6
|
+
import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
7
7
|
import { newSessionId } from "./session-id.js";
|
|
8
8
|
import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
9
9
|
import { MODES, MODE_NOTE, getMode, setMode } from "./mode.js";
|
|
@@ -19,6 +19,17 @@ import { join } from "node:path";
|
|
|
19
19
|
* — kiso never silently drops a level, and now it shows the ones it takes.
|
|
20
20
|
* A model the registry does not know shows `unknown` (nothing is guessed);
|
|
21
21
|
* it must not read `none`, which is a REAL level since the Responses rows. */
|
|
22
|
+
/** A context figure for a NOTICE, in the status row's own form.
|
|
23
|
+
*
|
|
24
|
+
* `displayCtxRatio` returns NaN as its honest "nobody states this model's
|
|
25
|
+
* window", and the status row renders that as `ctx ?`. The `/compact`
|
|
26
|
+
* recap multiplied it and interpolated the result, so the same unknown
|
|
27
|
+
* reached the screen as `ctx NaN% → NaN%`: one value with a contract, and
|
|
28
|
+
* a second consumer that had not read it. The `%` moves inside, because
|
|
29
|
+
* the unknown form does not carry one. */
|
|
30
|
+
function ctxPercent(ratio) {
|
|
31
|
+
return Number.isFinite(ratio) ? `${Math.round(ratio * 100)}%` : "?";
|
|
32
|
+
}
|
|
22
33
|
function effortNote(p) {
|
|
23
34
|
const reasoning = lookupModelMetadata(p.model, p.baseUrl)?.capabilities.reasoning ?? null;
|
|
24
35
|
const effort = reasoning?.effort ?? null;
|
|
@@ -732,7 +743,7 @@ export function dispatch(line, ctx) {
|
|
|
732
743
|
const compacting = (info) => {
|
|
733
744
|
const text = (elapsed) => `▘ compacting · ${info.rounds} rounds · ~${kUnit(info.tokens)} tokens · ${Math.max(0, elapsed)}s`;
|
|
734
745
|
compactStart = Date.now();
|
|
735
|
-
ctxBefore =
|
|
746
|
+
ctxBefore = ctxPercent(ctx.estimateCtx());
|
|
736
747
|
dock.setStatus(text(0), "esc to cancel");
|
|
737
748
|
compactTimer = setInterval(() => {
|
|
738
749
|
dock.setStatus(text(Math.round((Date.now() - compactStart) / 1000)), "esc to cancel");
|
|
@@ -745,6 +756,12 @@ export function dispatch(line, ctx) {
|
|
|
745
756
|
const focus = trimmed.slice(8).trim();
|
|
746
757
|
const result = await ctx.session.summarize({
|
|
747
758
|
signal: abort.signal,
|
|
759
|
+
// 0.39.2: the gesture gets the measured output budget and
|
|
760
|
+
// thinking off where the registry can vouch for turning it
|
|
761
|
+
// off. A fixed 4,000 was failing live sessions with "the
|
|
762
|
+
// summary turn ended with max_tokens"; a complete checkpoint
|
|
763
|
+
// of the reported size measured 18,837.
|
|
764
|
+
manualBudget: true,
|
|
748
765
|
...(focus !== "" ? { focus } : {}),
|
|
749
766
|
onStart: (info) => {
|
|
750
767
|
compactInfo = info;
|
|
@@ -760,11 +777,29 @@ export function dispatch(line, ctx) {
|
|
|
760
777
|
// rounds, the one summary, the savings, the ctx drop
|
|
761
778
|
// (the estimate BEFORE vs AFTER — the same chars/4
|
|
762
779
|
// proxy the status bar shows, marked ~), and the time.
|
|
763
|
-
const ctxAfter =
|
|
764
|
-
const elapsed = compactStart > 0 ? ((Date.now() - compactStart) / 1000)
|
|
780
|
+
const ctxAfter = ctxPercent(ctx.estimateCtx());
|
|
781
|
+
const elapsed = compactStart > 0 ? settledLabel((Date.now() - compactStart) / 1000) : "?s";
|
|
765
782
|
// a non-null result implies onStart ran — the "?" is
|
|
766
783
|
// reachable only at the type level
|
|
767
|
-
body.notice(`[/compact] ✦ compacted · ${compactInfo?.rounds ?? "?"} rounds → 1 summary · saved ~${kUnit(result.savedTokens)} · ctx ${ctxBefore ?? "?"}
|
|
784
|
+
body.notice(`[/compact] ✦ compacted · ${compactInfo?.rounds ?? "?"} rounds → 1 summary · saved ~${kUnit(result.savedTokens)} · ctx ${ctxBefore ?? "?"} → ${ctxAfter} · ${elapsed}`);
|
|
785
|
+
// 0.39.1 — the boundary row, under the recap.
|
|
786
|
+
//
|
|
787
|
+
// `summarized` is appended OFF-LOOP: session.summarize()
|
|
788
|
+
// writes it to the log and persists it, and no hook
|
|
789
|
+
// carries it to a renderer. So the row that marks where
|
|
790
|
+
// the transcript was cut had exactly one producer — a
|
|
791
|
+
// `case "summarized"` in lines.ts whose own comment says
|
|
792
|
+
// it is there "for the switch's completeness only" — and
|
|
793
|
+
// zero callers. The durable log said a compaction
|
|
794
|
+
// happened at seq N; no screen ever did.
|
|
795
|
+
//
|
|
796
|
+
// Rendered THROUGH that case rather than written again
|
|
797
|
+
// here: the line a resumed or replayed view would draw is
|
|
798
|
+
// the line drawn now, by construction, and the row cannot
|
|
799
|
+
// drift into two spellings of one fact.
|
|
800
|
+
const boundary = renderEvent({ type: "summarized", coversToSeq: result.coversToSeq });
|
|
801
|
+
if (boundary.text !== "")
|
|
802
|
+
body.raw(boundary.text.replace(/\n$/, "").split("\n"));
|
|
768
803
|
}
|
|
769
804
|
}
|
|
770
805
|
catch (err) {
|
|
@@ -774,7 +809,22 @@ export function dispatch(line, ctx) {
|
|
|
774
809
|
body.notice("[/compact] cancelled — nothing was persisted");
|
|
775
810
|
}
|
|
776
811
|
else {
|
|
777
|
-
|
|
812
|
+
// 0.39.1 — two repairs on one line. A failure the ADAPTER
|
|
813
|
+
// classified is a plain object, not an Error: `String(err)`
|
|
814
|
+
// rendered it `[object Object]`, which became reachable the
|
|
815
|
+
// moment the adapters started THROWING the
|
|
816
|
+
// transport-failure class instead of stopping with it. And
|
|
817
|
+
// a transport failure is worth saying so about: summarize
|
|
818
|
+
// already retried it once (it has no kernel behind it to do
|
|
819
|
+
// that), so a second failure means the gateway is still
|
|
820
|
+
// dropping the stream — the run is fine and the gesture is
|
|
821
|
+
// worth repeating. A permanent failure is not, and must not
|
|
822
|
+
// be dressed up as one that is.
|
|
823
|
+
const e = err;
|
|
824
|
+
const classified = typeof e.code === "string" && typeof e.message === "string";
|
|
825
|
+
const detail = classified ? String(e.message) : err instanceof Error ? err.message : String(err);
|
|
826
|
+
const again = classified && e.retryable === true ? " — the connection dropped again; /compact is worth another try" : "";
|
|
827
|
+
body.notice(`[/compact] failed: ${escapeTerminal(detail)}${again}`);
|
|
778
828
|
}
|
|
779
829
|
}
|
|
780
830
|
finally {
|
|
@@ -902,7 +952,21 @@ export function dispatch(line, ctx) {
|
|
|
902
952
|
// still submits.
|
|
903
953
|
if (trimmed.startsWith("/") && !trimmed.includes("\n")) {
|
|
904
954
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
905
|
-
|
|
955
|
+
// The word, not the line: a command that EXISTS but was handed an
|
|
956
|
+
// argument it does not take reaches here too, and calling it
|
|
957
|
+
// unknown is a lie the user cannot get past — `/help` lists it,
|
|
958
|
+
// they typed what `/help` said, and kiso answers that there is no
|
|
959
|
+
// such command. (Reported on `/reload extensions`, 0.39.1.) The
|
|
960
|
+
// commands that DO take an argument matched on their own prefix
|
|
961
|
+
// far above, so a known word arriving here means exactly one
|
|
962
|
+
// thing: it takes none. `slashCommandNames` is the help sheet's
|
|
963
|
+
// own table, so this line cannot drift from the list the user was
|
|
964
|
+
// reading when they typed it.
|
|
965
|
+
const word = trimmed.split(/\s+/)[0] ?? trimmed;
|
|
966
|
+
const known = slashCommandNames().includes(word);
|
|
967
|
+
bodyLog(known
|
|
968
|
+
? `${escapeTerminal(word)} takes no arguments — /help says what it does`
|
|
969
|
+
: `unknown command: ${escapeTerminal(word)} — /help lists the commands`);
|
|
906
970
|
ctx.input.prompt();
|
|
907
971
|
});
|
|
908
972
|
return;
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, re
|
|
|
47
47
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
48
48
|
import { tmuxMouseHint } from "./tmux-hint.js";
|
|
49
49
|
import { resume } from "./resume.js";
|
|
50
|
+
import { paintWindowTitle } from "./window-title.js";
|
|
50
51
|
import { resumeTail } from "./resume-tail.js";
|
|
51
52
|
import { armByteTrace } from "./byte-trace.js";
|
|
52
53
|
import { tmpdir, homedir } from "node:os";
|
|
@@ -1125,6 +1126,11 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1125
1126
|
bindRestoredSession(session);
|
|
1126
1127
|
setCurrentModelName(session.model);
|
|
1127
1128
|
paintBootStatus(session);
|
|
1129
|
+
// The terminal's window title, HERE for the same reason the three
|
|
1130
|
+
// lines above are here: this is the one step all three entry points
|
|
1131
|
+
// share (first start, `/resume <id>`, a switch), so a tab can never
|
|
1132
|
+
// be left naming the session the user just left.
|
|
1133
|
+
paintWindowTitle(session.log.all);
|
|
1128
1134
|
const nav = {
|
|
1129
1135
|
sessions: () => agent.sessions().map((m) => m.id),
|
|
1130
1136
|
...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's window title (0.39.1, the owner's call).
|
|
3
|
+
*
|
|
4
|
+
* kiso set none, so a tab read whatever the shell left there — the
|
|
5
|
+
* directory the shell happened to be in and the absolute path of a node
|
|
6
|
+
* binary, identically for every kiso in every tab. The title is the one
|
|
7
|
+
* place a terminal shows which session a tab holds without the user
|
|
8
|
+
* switching to it, and it was the only screen kiso never wrote to.
|
|
9
|
+
*
|
|
10
|
+
* `kiso — <workspace>` at the start, `kiso — <session title> —
|
|
11
|
+
* <workspace>` once the session has one. The session's title is
|
|
12
|
+
* `sessionTitle`'s — the SAME projection the resume picker and `kiso
|
|
13
|
+
* sessions` print, so a tab and the picker can never disagree about what
|
|
14
|
+
* a session is called. That is also why an opener does not become the
|
|
15
|
+
* title: the rule for "substantive" lives in one place and this is a
|
|
16
|
+
* consumer of it, not a second opinion.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT WRITES. OSC 0, BEL-terminated — icon name and window title in
|
|
19
|
+
* one sequence, the form every terminal that supports either accepts. It
|
|
20
|
+
* goes STRAIGHT to stdout, deliberately outside the compositor: a title
|
|
21
|
+
* occupies no cell and no column, so it is not in the width accounting
|
|
22
|
+
* and must not be a cell that the repaint could move, cut, or scroll.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT NEVER WRITES. Anything at all when stdout is not a TTY. An OSC
|
|
25
|
+
* into a pipe is bytes in someone's output, and the pipe-identity gates
|
|
26
|
+
* exist because that has been a real defect before.
|
|
27
|
+
*
|
|
28
|
+
* NO RESTORE ON EXIT. Putting back what was there first requires reading
|
|
29
|
+
* it, which no terminal reliably answers; the shell repaints its own
|
|
30
|
+
* title on the next prompt anyway. A guess written on the way out would
|
|
31
|
+
* be a title kiso invented, which is worse than one it left.
|
|
32
|
+
*
|
|
33
|
+
* AND NO `process.title`. The reference implementation sets one; kiso
|
|
34
|
+
* does not, because two gates in this repo said no and the benefit was
|
|
35
|
+
* that `ps` would print `kiso` instead of a node path.
|
|
36
|
+
*
|
|
37
|
+
* - It overwrites the process's ARGV memory, so a renamed process shows
|
|
38
|
+
* as `kiso` with its arguments simply gone. The subagent's
|
|
39
|
+
* concurrency probe counts live children by matching `chat
|
|
40
|
+
* sub-parent-` in the command column; six renamed children were
|
|
41
|
+
* indistinguishable from each other and from nothing.
|
|
42
|
+
* - Setting it is not free on this platform: 14 ms measured for the
|
|
43
|
+
* first assignment on the machine this was written on, 7 ms for the
|
|
44
|
+
* second. A synchronous stall of that size on the main thread while a
|
|
45
|
+
* 3,000-line paste is arriving wedges the PTY in both directions —
|
|
46
|
+
* the UD-1 byte-exact paste gate deadlocked reproducibly and passed
|
|
47
|
+
* the moment the assignment was removed.
|
|
48
|
+
*
|
|
49
|
+
* The window title is what was asked for; the process name was a detail
|
|
50
|
+
* that came with it, and it costs more than it is worth.
|
|
51
|
+
*/
|
|
52
|
+
import type { Event } from "@vincemakes/kiso-core";
|
|
53
|
+
/** OSC 0 — "set icon name and window title". */
|
|
54
|
+
export declare const OSC_TITLE_PREFIX = "\u001B]0;";
|
|
55
|
+
/** BEL, the terminator the reference form uses (ST works too; BEL is the
|
|
56
|
+
* one older terminals accept without argument). */
|
|
57
|
+
export declare const OSC_TITLE_SUFFIX = "\u0007";
|
|
58
|
+
/**
|
|
59
|
+
* The text of the title, with no IO — the whole rule, testable.
|
|
60
|
+
*
|
|
61
|
+
* Every part is passed through `escapeTerminal` before it can reach the
|
|
62
|
+
* sequence. A title is whatever a human typed or PASTED: a BEL in it
|
|
63
|
+
* would end the sequence early and an ESC would begin another one, and
|
|
64
|
+
* the title would stop being a title and start being commands to the
|
|
65
|
+
* terminal. The same stripper the rendered surfaces use — one definition
|
|
66
|
+
* of "what is safe to put on a terminal", not a second.
|
|
67
|
+
*/
|
|
68
|
+
export declare function windowTitleText(events: readonly Event[], workspace: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Write the title for this session's state. Called where the session is
|
|
71
|
+
* BOUND (which is one place for all three entry points — first start,
|
|
72
|
+
* `/resume <id>`, and a switch) and again as the session's own title
|
|
73
|
+
* becomes knowable, so a tab named before the first prompt is renamed by
|
|
74
|
+
* it rather than keeping a name that was only ever a placeholder.
|
|
75
|
+
*/
|
|
76
|
+
export declare function paintWindowTitle(events: readonly Event[], cwd?: string): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's window title (0.39.1, the owner's call).
|
|
3
|
+
*
|
|
4
|
+
* kiso set none, so a tab read whatever the shell left there — the
|
|
5
|
+
* directory the shell happened to be in and the absolute path of a node
|
|
6
|
+
* binary, identically for every kiso in every tab. The title is the one
|
|
7
|
+
* place a terminal shows which session a tab holds without the user
|
|
8
|
+
* switching to it, and it was the only screen kiso never wrote to.
|
|
9
|
+
*
|
|
10
|
+
* `kiso — <workspace>` at the start, `kiso — <session title> —
|
|
11
|
+
* <workspace>` once the session has one. The session's title is
|
|
12
|
+
* `sessionTitle`'s — the SAME projection the resume picker and `kiso
|
|
13
|
+
* sessions` print, so a tab and the picker can never disagree about what
|
|
14
|
+
* a session is called. That is also why an opener does not become the
|
|
15
|
+
* title: the rule for "substantive" lives in one place and this is a
|
|
16
|
+
* consumer of it, not a second opinion.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT WRITES. OSC 0, BEL-terminated — icon name and window title in
|
|
19
|
+
* one sequence, the form every terminal that supports either accepts. It
|
|
20
|
+
* goes STRAIGHT to stdout, deliberately outside the compositor: a title
|
|
21
|
+
* occupies no cell and no column, so it is not in the width accounting
|
|
22
|
+
* and must not be a cell that the repaint could move, cut, or scroll.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT NEVER WRITES. Anything at all when stdout is not a TTY. An OSC
|
|
25
|
+
* into a pipe is bytes in someone's output, and the pipe-identity gates
|
|
26
|
+
* exist because that has been a real defect before.
|
|
27
|
+
*
|
|
28
|
+
* NO RESTORE ON EXIT. Putting back what was there first requires reading
|
|
29
|
+
* it, which no terminal reliably answers; the shell repaints its own
|
|
30
|
+
* title on the next prompt anyway. A guess written on the way out would
|
|
31
|
+
* be a title kiso invented, which is worse than one it left.
|
|
32
|
+
*
|
|
33
|
+
* AND NO `process.title`. The reference implementation sets one; kiso
|
|
34
|
+
* does not, because two gates in this repo said no and the benefit was
|
|
35
|
+
* that `ps` would print `kiso` instead of a node path.
|
|
36
|
+
*
|
|
37
|
+
* - It overwrites the process's ARGV memory, so a renamed process shows
|
|
38
|
+
* as `kiso` with its arguments simply gone. The subagent's
|
|
39
|
+
* concurrency probe counts live children by matching `chat
|
|
40
|
+
* sub-parent-` in the command column; six renamed children were
|
|
41
|
+
* indistinguishable from each other and from nothing.
|
|
42
|
+
* - Setting it is not free on this platform: 14 ms measured for the
|
|
43
|
+
* first assignment on the machine this was written on, 7 ms for the
|
|
44
|
+
* second. A synchronous stall of that size on the main thread while a
|
|
45
|
+
* 3,000-line paste is arriving wedges the PTY in both directions —
|
|
46
|
+
* the UD-1 byte-exact paste gate deadlocked reproducibly and passed
|
|
47
|
+
* the moment the assignment was removed.
|
|
48
|
+
*
|
|
49
|
+
* The window title is what was asked for; the process name was a detail
|
|
50
|
+
* that came with it, and it costs more than it is worth.
|
|
51
|
+
*/
|
|
52
|
+
import { basename } from "node:path";
|
|
53
|
+
import { sessionTitle } from "@vincemakes/kiso-runtime/internal";
|
|
54
|
+
import { displayWidth, widthCut } from "@vincemakes/kiso-tui-cells/width";
|
|
55
|
+
import { escapeTerminal } from "@vincemakes/kiso-tui-cells/render";
|
|
56
|
+
/** OSC 0 — "set icon name and window title". */
|
|
57
|
+
export const OSC_TITLE_PREFIX = "\u001b]0;";
|
|
58
|
+
/** BEL, the terminator the reference form uses (ST works too; BEL is the
|
|
59
|
+
* one older terminals accept without argument). */
|
|
60
|
+
export const OSC_TITLE_SUFFIX = "\u0007";
|
|
61
|
+
/** The session title's share of the tab, in CELLS. A tab strip gives a
|
|
62
|
+
* title far less than a row, and the workspace — which says WHERE, the
|
|
63
|
+
* thing a human scanning tabs is usually after — must survive the cut. */
|
|
64
|
+
const TITLE_CELLS = 40;
|
|
65
|
+
/** The title a session with no title yet has. `sessionTitle` returns this
|
|
66
|
+
* when nothing has been asked; it is not a label, so it is not shown. */
|
|
67
|
+
const NO_TITLE = "(no prompt)";
|
|
68
|
+
/**
|
|
69
|
+
* The text of the title, with no IO — the whole rule, testable.
|
|
70
|
+
*
|
|
71
|
+
* Every part is passed through `escapeTerminal` before it can reach the
|
|
72
|
+
* sequence. A title is whatever a human typed or PASTED: a BEL in it
|
|
73
|
+
* would end the sequence early and an ESC would begin another one, and
|
|
74
|
+
* the title would stop being a title and start being commands to the
|
|
75
|
+
* terminal. The same stripper the rendered surfaces use — one definition
|
|
76
|
+
* of "what is safe to put on a terminal", not a second.
|
|
77
|
+
*/
|
|
78
|
+
export function windowTitleText(events, workspace) {
|
|
79
|
+
const place = escapeTerminal(workspace);
|
|
80
|
+
// Filtered BEFORE the wrap, so the cost is one object per user turn
|
|
81
|
+
// rather than one per event — this is re-derived on every turn of a
|
|
82
|
+
// session whose log may hold thousands. `sessionTitle` filters again;
|
|
83
|
+
// it is the authority on what counts and this is not a second opinion.
|
|
84
|
+
const inputs = events.filter((e) => e.type === "user_input");
|
|
85
|
+
const raw = sessionTitle(inputs.map((event) => ({ runId: "", ts: 0, event })));
|
|
86
|
+
if (raw === NO_TITLE)
|
|
87
|
+
return `kiso — ${place}`;
|
|
88
|
+
const safe = escapeTerminal(raw);
|
|
89
|
+
// Cut by CELLS, not code points: a title of wide characters counts
|
|
90
|
+
// two columns each, and a count of characters would overflow the tab
|
|
91
|
+
// by as much again. The mark is inside the budget, never added to it.
|
|
92
|
+
const shown = displayWidth(safe) > TITLE_CELLS ? `${widthCut(safe, TITLE_CELLS - 1)}…` : safe;
|
|
93
|
+
return `kiso — ${shown} — ${place}`;
|
|
94
|
+
}
|
|
95
|
+
/** What was last written, so a repaint of the same title writes nothing.
|
|
96
|
+
* The title is re-derived on every bind and every user turn; most of
|
|
97
|
+
* those derive what is already on screen. */
|
|
98
|
+
let shown = null;
|
|
99
|
+
/**
|
|
100
|
+
* Write the title for this session's state. Called where the session is
|
|
101
|
+
* BOUND (which is one place for all three entry points — first start,
|
|
102
|
+
* `/resume <id>`, and a switch) and again as the session's own title
|
|
103
|
+
* becomes knowable, so a tab named before the first prompt is renamed by
|
|
104
|
+
* it rather than keeping a name that was only ever a placeholder.
|
|
105
|
+
*/
|
|
106
|
+
export function paintWindowTitle(events, cwd = process.cwd()) {
|
|
107
|
+
if (process.stdout.isTTY !== true)
|
|
108
|
+
return;
|
|
109
|
+
const text = windowTitleText(events, basename(cwd) || cwd);
|
|
110
|
+
if (text === shown)
|
|
111
|
+
return;
|
|
112
|
+
shown = text;
|
|
113
|
+
process.stdout.write(`${OSC_TITLE_PREFIX}${text}${OSC_TITLE_SUFFIX}`);
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
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.39.
|
|
22
|
-
"@vincemakes/kiso-core": "0.39.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.39.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.39.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.39.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.39.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.39.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.39.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.39.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.39.
|
|
31
|
-
"@vincemakes/kiso-task-ext": "0.39.
|
|
32
|
-
"@vincemakes/kiso-tools-node": "0.39.
|
|
33
|
-
"@vincemakes/kiso-tui": "0.39.
|
|
34
|
-
"@vincemakes/kiso-tui-cells": "0.39.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.39.2",
|
|
22
|
+
"@vincemakes/kiso-core": "0.39.2",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.39.2",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.39.2",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.39.2",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.39.2",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.39.2",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.39.2",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.39.2",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.39.2",
|
|
31
|
+
"@vincemakes/kiso-task-ext": "0.39.2",
|
|
32
|
+
"@vincemakes/kiso-tools-node": "0.39.2",
|
|
33
|
+
"@vincemakes/kiso-tui": "0.39.2",
|
|
34
|
+
"@vincemakes/kiso-tui-cells": "0.39.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.1.2",
|