@vincemakes/kiso-code 0.46.2 → 0.47.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.
@@ -7,13 +7,23 @@
7
7
  * place a terminal shows which session a tab holds without the user
8
8
  * switching to it, and it was the only screen kiso never wrote to.
9
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.
10
+ * Graphite §8.10 — the title is kiso's, the session's name when the
11
+ * person gave it one, and the folder (the card round, owner 2026-10-05):
12
+ *
13
+ * kiso — <folder> ready or working: no mark
14
+ * kiso · needs you — <folder> an approval, a question, an
15
+ * unknown outcome to decide
16
+ * kiso · <name> — <folder> named with `/name`
17
+ *
18
+ * DECLARED REVERSAL of two rulings. The working tab's `✦` (owner,
19
+ * 2026-09-28, §4.1's one declared exception): a tab that changes on every
20
+ * turn is noise, and the owner retired the mark. And 0.39.1's name from
21
+ * the first substantive prompt (`sessionTitle`): a tab says kiso; the
22
+ * resume picker and `kiso sessions` keep showing the derived name. The
23
+ * title changes when the state changes and never on a tick: a ticking
24
+ * title churns tab bars. No bell and no notification — nothing interrupts
25
+ * the person (owner, 2026-09-28). On exit the ready form is written, so a
26
+ * closed session never leaves a waiting mark behind.
17
27
  *
18
28
  * WHAT IT WRITES. OSC 0, BEL-terminated — icon name and window title in
19
29
  * one sequence, the form every terminal that supports either accepts. It
@@ -49,28 +59,38 @@
49
59
  * The window title is what was asked for; the process name was a detail
50
60
  * that came with it, and it costs more than it is worth.
51
61
  */
52
- import type { Event } from "@vincemakes/kiso-core";
53
62
  /** OSC 0 — "set icon name and window title". */
54
63
  export declare const OSC_TITLE_PREFIX = "\u001B]0;";
55
64
  /** BEL, the terminator the reference form uses (ST works too; BEL is the
56
65
  * one older terminals accept without argument). */
57
66
  export declare const OSC_TITLE_SUFFIX = "\u0007";
67
+ /** Graphite §8.10 — what the session is doing, as the title says it. */
68
+ export type TitleState = "ready" | "working" | "needs-you";
58
69
  /**
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.
70
+ * What may reach a title: `escapeTerminal` (the same definition of "safe
71
+ * on a terminal" the rendered surfaces use — a BEL would end the sequence
72
+ * and an ESC would begin another), then the bidi and invisible format
73
+ * code points, which reorder or hide text without being control bytes.
67
74
  */
68
- export declare function windowTitleText(events: readonly Event[], workspace: string): string;
75
+ export declare function sanitizeTitle(text: string): string;
76
+ /** The title for a state, a name (null unless `/name` gave one) and a
77
+ * folder. The name is cut at 40 cells, by CELLS: a wide character counts
78
+ * two. */
79
+ export declare function titleText(state: TitleState, name: string | null, folder: string): string;
80
+ /** The title in a state, with no IO — the whole rule, testable. */
81
+ export declare function windowTitleText(workspace: string, st?: TitleState, named?: string | null): string;
82
+ /** Graphite R3d: the session's `/name` (null when unnamed) — set where the
83
+ * session is bound and when `/name` changes it. */
84
+ export declare function setTitleName(name: string | null): void;
69
85
  /**
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.
86
+ * Write the title for this session. Called where the session is BOUND
87
+ * (one place for all three entry points — first start, `/resume <id>`,
88
+ * and a switch), so a tab never names the folder the user just left.
75
89
  */
76
- export declare function paintWindowTitle(events: readonly Event[], cwd?: string): void;
90
+ export declare function paintWindowTitle(cwd?: string): void;
91
+ /** The session's state changed (§8.10): a turn began or ended, a panel
92
+ * asks the person or has been answered, the process is exiting. Writes
93
+ * only when the title it derives differs from the one shown. */
94
+ export declare function setTitleState(next: TitleState): void;
95
+ /** The state the title shows now — a panel that asks restores it after. */
96
+ export declare function titleState(): TitleState;
@@ -7,13 +7,23 @@
7
7
  * place a terminal shows which session a tab holds without the user
8
8
  * switching to it, and it was the only screen kiso never wrote to.
9
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.
10
+ * Graphite §8.10 — the title is kiso's, the session's name when the
11
+ * person gave it one, and the folder (the card round, owner 2026-10-05):
12
+ *
13
+ * kiso — <folder> ready or working: no mark
14
+ * kiso · needs you — <folder> an approval, a question, an
15
+ * unknown outcome to decide
16
+ * kiso · <name> — <folder> named with `/name`
17
+ *
18
+ * DECLARED REVERSAL of two rulings. The working tab's `✦` (owner,
19
+ * 2026-09-28, §4.1's one declared exception): a tab that changes on every
20
+ * turn is noise, and the owner retired the mark. And 0.39.1's name from
21
+ * the first substantive prompt (`sessionTitle`): a tab says kiso; the
22
+ * resume picker and `kiso sessions` keep showing the derived name. The
23
+ * title changes when the state changes and never on a tick: a ticking
24
+ * title churns tab bars. No bell and no notification — nothing interrupts
25
+ * the person (owner, 2026-09-28). On exit the ready form is written, so a
26
+ * closed session never leaves a waiting mark behind.
17
27
  *
18
28
  * WHAT IT WRITES. OSC 0, BEL-terminated — icon name and window title in
19
29
  * one sequence, the form every terminal that supports either accepts. It
@@ -50,7 +60,6 @@
50
60
  * that came with it, and it costs more than it is worth.
51
61
  */
52
62
  import { basename } from "node:path";
53
- import { sessionTitle } from "@vincemakes/kiso-runtime/internal";
54
63
  import { displayWidth, widthCut } from "@vincemakes/kiso-tui-cells/width";
55
64
  import { escapeTerminal } from "@vincemakes/kiso-tui-cells/render";
56
65
  /** OSC 0 — "set icon name and window title". */
@@ -58,57 +67,76 @@ export const OSC_TITLE_PREFIX = "\u001b]0;";
58
67
  /** BEL, the terminator the reference form uses (ST works too; BEL is the
59
68
  * one older terminals accept without argument). */
60
69
  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. */
70
+ /** The name's share of the tab, in CELLS. A tab strip gives a title far
71
+ * less than a row, and the workspace — which says WHERE, the thing a
72
+ * human scanning tabs is usually after — must survive the cut. */
64
73
  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)";
74
+ /** Bidi controls and invisible format characters: in a title they can
75
+ * reorder or hide what a tab says (U+061C, U+200B–U+200F, U+202A–U+202E,
76
+ * U+2060–U+2069, U+FEFF). */
77
+ const INVISIBLE = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/g;
68
78
  /**
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.
79
+ * What may reach a title: `escapeTerminal` (the same definition of "safe
80
+ * on a terminal" the rendered surfaces use — a BEL would end the sequence
81
+ * and an ESC would begin another), then the bidi and invisible format
82
+ * code points, which reorder or hide text without being control bytes.
77
83
  */
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}`;
84
+ export function sanitizeTitle(text) {
85
+ return escapeTerminal(text).replace(INVISIBLE, "");
86
+ }
87
+ /** The title for a state, a name (null unless `/name` gave one) and a
88
+ * folder. The name is cut at 40 cells, by CELLS: a wide character counts
89
+ * two. */
90
+ export function titleText(state, name, folder) {
91
+ const place = sanitizeTitle(folder);
92
+ const safe = name === null ? "" : sanitizeTitle(name);
93
+ const who = displayWidth(safe) > TITLE_CELLS ? `${widthCut(safe, TITLE_CELLS - 1)}…` : safe;
94
+ const parts = ["kiso", ...(state === "needs-you" ? ["needs you"] : []), ...(who === "" ? [] : [who])];
95
+ return `${parts.join(" · ")} — ${place}`;
96
+ }
97
+ /** The title in a state, with no IO — the whole rule, testable. */
98
+ export function windowTitleText(workspace, st = "ready", named = null) {
99
+ return titleText(st, named, workspace);
94
100
  }
95
101
  /** 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. */
102
+ * The title is re-derived on every bind, every `/name` and every state
103
+ * change; most of those derive what is already on screen. */
98
104
  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)
105
+ let state = "ready";
106
+ let lastCwd = null;
107
+ let lastName = null;
108
+ /** Graphite R3d: the session's `/name` (null when unnamed) — set where the
109
+ * session is bound and when `/name` changes it. */
110
+ export function setTitleName(name) {
111
+ lastName = name;
112
+ write();
113
+ }
114
+ function write() {
115
+ if (process.stdout.isTTY !== true || lastCwd === null)
108
116
  return;
109
- const text = windowTitleText(events, basename(cwd) || cwd);
117
+ const text = windowTitleText(basename(lastCwd) || lastCwd, state, lastName);
110
118
  if (text === shown)
111
119
  return;
112
120
  shown = text;
113
121
  process.stdout.write(`${OSC_TITLE_PREFIX}${text}${OSC_TITLE_SUFFIX}`);
114
122
  }
123
+ /**
124
+ * Write the title for this session. Called where the session is BOUND
125
+ * (one place for all three entry points — first start, `/resume <id>`,
126
+ * and a switch), so a tab never names the folder the user just left.
127
+ */
128
+ export function paintWindowTitle(cwd = process.cwd()) {
129
+ lastCwd = cwd;
130
+ write();
131
+ }
132
+ /** The session's state changed (§8.10): a turn began or ended, a panel
133
+ * asks the person or has been answered, the process is exiting. Writes
134
+ * only when the title it derives differs from the one shown. */
135
+ export function setTitleState(next) {
136
+ state = next;
137
+ write();
138
+ }
139
+ /** The state the title shows now — a panel that asks restores it after. */
140
+ export function titleState() {
141
+ return state;
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.46.2",
3
+ "version": "0.47.0",
4
4
  "description": "kiso-code \u2014 the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,19 +18,19 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.46.2",
22
- "@vincemakes/kiso-core": "0.46.2",
23
- "@vincemakes/kiso-evals": "0.46.2",
24
- "@vincemakes/kiso-mcp-ext": "0.46.2",
25
- "@vincemakes/kiso-provider-anthropic": "0.46.2",
26
- "@vincemakes/kiso-provider-openai": "0.46.2",
27
- "@vincemakes/kiso-provider-openai-responses": "0.46.2",
28
- "@vincemakes/kiso-runtime": "0.46.2",
29
- "@vincemakes/kiso-skills-ext": "0.46.2",
30
- "@vincemakes/kiso-subagent-ext": "0.46.2",
31
- "@vincemakes/kiso-tools-node": "0.46.2",
32
- "@vincemakes/kiso-tui": "0.46.2",
33
- "@vincemakes/kiso-tui-cells": "0.46.2"
21
+ "@vincemakes/kiso-ask-ext": "0.47.0",
22
+ "@vincemakes/kiso-core": "0.47.0",
23
+ "@vincemakes/kiso-evals": "0.47.0",
24
+ "@vincemakes/kiso-mcp-ext": "0.47.0",
25
+ "@vincemakes/kiso-provider-anthropic": "0.47.0",
26
+ "@vincemakes/kiso-provider-openai": "0.47.0",
27
+ "@vincemakes/kiso-provider-openai-responses": "0.47.0",
28
+ "@vincemakes/kiso-runtime": "0.47.0",
29
+ "@vincemakes/kiso-skills-ext": "0.47.0",
30
+ "@vincemakes/kiso-subagent-ext": "0.47.0",
31
+ "@vincemakes/kiso-tools-node": "0.47.0",
32
+ "@vincemakes/kiso-tui": "0.47.0",
33
+ "@vincemakes/kiso-tui-cells": "0.47.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^26.1.2",