@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.
package/dist/replay.js CHANGED
@@ -20,7 +20,8 @@
20
20
  * the viewer — then any uncovered earlier turns folded as above.
21
21
  */
22
22
  import { echoText } from "@vincemakes/kiso-tui-cells/render";
23
- import { taskNoticeLines } from "./task-notice.js";
23
+ import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
24
+ import { taskNoticeLines, taskNoticeRows, taskWhat } from "./task-notice.js";
24
25
  /** How many turns stay on screen in full. */
25
26
  const FULL_TURNS = 2;
26
27
  /** A user turn's opening line, or null when this input does not open one
@@ -48,7 +49,7 @@ export function turnsOf(events) {
48
49
  }
49
50
  /** One turn through the Body — consumeRun's mapping, minus everything that
50
51
  * needs the live process (clocks, tailers, usage, the recap). */
51
- function replayTurn(body, turn, what) {
52
+ function replayTurn(body, turn, tasks) {
52
53
  let thinking = false;
53
54
  let said = false;
54
55
  const results = new Set();
@@ -62,9 +63,17 @@ function replayTurn(body, turn, what) {
62
63
  const ask = askOf(e);
63
64
  if (ask !== null)
64
65
  body.userLine(ask);
65
- else if (e.via?.kind === "tasks")
66
- for (const row of taskNoticeLines(e.via.items, what))
67
- body.notice(row);
66
+ else if (e.via?.kind === "tasks") {
67
+ // the tasks round: the same rows a live session drew, from the
68
+ // journal; the pipe's lines are taskNoticeLines' (Amendment 8: a
69
+ // lost task's line names what it ran)
70
+ const items = e.via.items;
71
+ const what = (taskId) => {
72
+ const t = tasks.find((x) => x.id === taskId);
73
+ return t === undefined ? undefined : taskWhat(t);
74
+ };
75
+ body.metaNotice(taskNoticeLines(items, what).join("\n"), taskNoticeRows(items, tasks));
76
+ }
68
77
  else if (e.source === "system") {
69
78
  body.notice("verification pass");
70
79
  body.notice(` ${typeof e.content === "string" ? e.content : ""}`);
@@ -125,7 +134,7 @@ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
125
134
  * Replay a session's durable events into the body. Returns the number of
126
135
  * turns it found (0: a fresh session — nothing is drawn).
127
136
  */
128
- export function replayInto(body, events, W = 80, what) {
137
+ export function replayInto(body, events, W = 80, tasks = []) {
129
138
  // The latest checkpoint: the turns it covers are what the model will
130
139
  // read as its summary, not as turns.
131
140
  let checkpoint = null;
@@ -141,22 +150,24 @@ export function replayInto(body, events, W = 80, what) {
141
150
  const shown = rest.slice(-FULL_TURNS);
142
151
  const earlier = rest.slice(0, rest.length - shown.length);
143
152
  const label = total > shown.length ? `resuming · ${plural(total, "turn")}, showing the last ${shown.length}` : `resuming · ${plural(total, "turn")}`;
144
- const head = `─── ${label} `;
145
- body.raw([`${head}${"─".repeat(Math.max(1, W - head.length))}`.slice(0, Math.max(1, W))]);
153
+ // Graphite §8.1 (R3b): the resumed history opens like every band — the
154
+ // hairline with its name, `resuming` bold gold, the counts dim (the same
155
+ // words the dim rule carried)
156
+ body.raw([bandHeader(label, Math.max(1, W))]);
146
157
  if (checkpoint !== null) {
147
158
  const summary = checkpoint.summary;
148
159
  body.fold(`checkpoint · summarizes ${plural(covered.length, "earlier turn")} · ctrl+r to read`, () => {
149
160
  for (const t of covered)
150
- replayTurn(body, t, what);
161
+ replayTurn(body, t, tasks);
151
162
  }, summary);
152
163
  }
153
164
  if (earlier.length > 0) {
154
165
  body.fold(`${plural(earlier.length, "earlier turn")} · ctrl+r to read`, () => {
155
166
  for (const t of earlier)
156
- replayTurn(body, t, what);
167
+ replayTurn(body, t, tasks);
157
168
  });
158
169
  }
159
170
  for (const t of shown)
160
- replayTurn(body, t, what);
171
+ replayTurn(body, t, tasks);
161
172
  return total;
162
173
  }
package/dist/resume.js CHANGED
@@ -2,12 +2,12 @@
2
2
  * The ergonomics batch B4 (pure move) — resume: the RECOVERY flow (Area 2/7). The body
3
3
  * moved verbatim from index.ts.
4
4
  */
5
- import { idleStatus, runningStatus } from "@vincemakes/kiso-tui";
6
- import { modeDisplay } from "./mode.js";
7
- import { agentModel, dock, retryOnRow, floorOn } from "./state.js";
5
+ import { STATUS_GLYPHS, workingRow } from "@vincemakes/kiso-tui";
6
+ import { dock, retryOnRow } from "./state.js";
8
7
  import { pendingAsk, resolveUncertains } from "./trust-ui.js";
9
8
  import { failOnFauxExhaustion } from "./faux-glue.js";
10
- import { consumeRun, estimateCtxRatio, startStatusSpinner } from "./chat.js";
9
+ import { setTitleState } from "./window-title.js";
10
+ import { barFor, consumeRun, estimateCtxRatio, startStatusSpinner } from "./chat.js";
11
11
  /**
12
12
  * Resume = the RECOVERY flow (Area 2/7): uncertain executions are decided,
13
13
  * the interrupted run is continued via session.resume() — never faked with
@@ -21,23 +21,26 @@ export async function resume(session, prompt, faux, input) {
21
21
  let turnNo = 0;
22
22
  // v3 §03: the two-state status bar (see chat — same shapes).
23
23
  let runUsage = { in: null, out: null, cache: null, known: false };
24
- let runGlyph = "▖";
24
+ let runGlyph = STATUS_GLYPHS[0];
25
25
  let runStart = Date.now();
26
26
  // KC2 §5: the rows the REPL and this flow used to build separately are
27
- // ONE formatter now — the running row was duplicated verbatim.
28
- const statusCb = (u, ctx) => {
27
+ // ONE formatter now. Graphite §8.7 / §8.9: the running turn's facts ride
28
+ // the live row, the session's the status bar — the chat's own two
29
+ // (barFor), so the recovery flow now spells plan's posture the same way.
30
+ const width = () => (process.stdout.columns > 0 ? process.stdout.columns : 80);
31
+ const statusCb = (u, _ctx) => {
29
32
  runUsage = u;
30
- if (dock.active)
31
- dock.setStatus(runningStatus(runGlyph, runStart, u.out, ctx, null, process.stdout.columns > 0 ? process.stdout.columns : 80, retryOnRow()));
33
+ if (!dock.active)
34
+ return;
35
+ dock.setLive(workingRow(runGlyph, runStart, u.out, null, width(), retryOnRow()));
36
+ dock.setBar(barFor(session, { tokPerSec: null }));
32
37
  };
33
- // the recovery flow prints the BARE mode (chat spells plan's posture) —
34
- // the extraction keeps that difference, it was not asked to settle it.
35
38
  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.
39
- if (dock.active)
40
- dock.setStatus(idleStatus(modeDisplay(), agentModel, estimateCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80, !floorOn));
39
+ setTitleState("ready");
40
+ if (!dock.active)
41
+ return;
42
+ dock.setLive(null);
43
+ dock.setBar(barFor(session, { tokPerSec: null }));
41
44
  };
42
45
  const withRun = async (run) => {
43
46
  currentRun = run;
@@ -47,6 +50,8 @@ export async function resume(session, prompt, faux, input) {
47
50
  runGlyph = g;
48
51
  statusCb(runUsage, estimateCtxRatio(session));
49
52
  });
53
+ statusCb(runUsage, estimateCtxRatio(session)); // the live row from the run's first frame
54
+ setTitleState("working");
50
55
  try {
51
56
  turnNo += 1;
52
57
  const last = await consumeRun(session, run, input, turnNo, faux, statusCb);
@@ -88,6 +88,8 @@ export declare function projectSessionCard(input: {
88
88
  readonly asks: number;
89
89
  readonly workspace?: string | null;
90
90
  readonly profileName?: string | null;
91
+ /** Graphite R3d: the session's `/name`, which wins over the derived title */
92
+ readonly name?: string | null;
91
93
  }): SessionCard;
92
94
  /** The shapes this module needs from the agent — structural, so the
93
95
  * projection is testable without standing up a provider. */
@@ -76,7 +76,7 @@ export function projectSessionCard(input) {
76
76
  : "failed";
77
77
  return {
78
78
  id: input.id,
79
- title: sessionTitle(input.records),
79
+ title: input.name ?? sessionTitle(input.records),
80
80
  badge,
81
81
  turns,
82
82
  updatedAt: input.updatedAt,
@@ -128,11 +128,11 @@ export function cardFromListing(l) {
128
128
  const s = l.summary;
129
129
  const base = { id: l.id, workspace: l.workspace, profileName: l.profileName, ...(l.inferred === true ? { inferred: true } : {}) };
130
130
  if (s === null)
131
- return { ...base, title: l.id, badge: "unknown", turns: null, updatedAt: l.mtime, uncertain: 0, asks: 0, outcome: "no summary" };
131
+ return { ...base, title: l.name ?? l.id, badge: "unknown", turns: null, updatedAt: l.mtime, uncertain: 0, asks: 0, outcome: "no summary" };
132
132
  const badge = s.uncertain > 0 ? "uncertain" : s.asks > 0 ? "ask" : s.state === "open" ? "interrupted" : s.state === "completed" ? "completed" : s.state === null ? "unknown" : "failed";
133
133
  return {
134
134
  ...base,
135
- title: s.title ?? l.id,
135
+ title: l.name ?? s.title ?? l.id,
136
136
  badge,
137
137
  turns: s.turns,
138
138
  updatedAt: s.updatedAt,
@@ -37,4 +37,23 @@ export interface SettingsInput {
37
37
  readonly thinkingRemembered: boolean;
38
38
  readonly version: string;
39
39
  }
40
+ export interface Row {
41
+ readonly name: string;
42
+ readonly value: string;
43
+ readonly from: string;
44
+ readonly change: string;
45
+ /** Graphite P3: the value as a table cell (`on`, `1M`) where `value` is
46
+ * a sentence; absent, the value is already short */
47
+ readonly brief?: string;
48
+ /** Graphite P3: what the sentence said that the cell does not — the
49
+ * panel's opened row carries it */
50
+ readonly means?: string;
51
+ }
52
+ /** Graphite R3e: the settings as data — the /settings panel's rows and
53
+ * the printed form below read the same list. */
54
+ export declare function settingsFacts(i: SettingsInput): Row[];
55
+ /** The printed form: one block per setting — what it is, where it came
56
+ * from and how to change it (a pipe, a dock-less TTY, and a panel row's
57
+ * enter). */
58
+ export declare function settingRow(r: Row, width?: number): string;
40
59
  export declare function settingsRows(i: SettingsInput): string[];
package/dist/settings.js CHANGED
@@ -40,7 +40,7 @@ function dontAskRow(i) {
40
40
  const on = i.dontAsk === true;
41
41
  const resolved = resolveModeLayers(modeLayers(i));
42
42
  const from = (resolved.dontAsk !== "off") === on ? dontAskFrom(i, resolved.from.dontAsk) : "set in this session";
43
- return { name: "don't ask", value: on ? "on — what would ask is refused" : "off", from, change };
43
+ return { name: "don't ask", value: on ? "on — what would ask is refused" : "off", from, change, brief: on ? "on" : "off", ...(on ? { means: "what would ask is refused" } : {}) };
44
44
  }
45
45
  function modelRow(i) {
46
46
  const change = "/model; \"model\" in ~/.kiso/config.json";
@@ -73,7 +73,15 @@ function layered(i, key) {
73
73
  return { value: i.user[key], from: "user config" };
74
74
  return { value: undefined, from: "default" };
75
75
  }
76
- export function settingsRows(i) {
76
+ /** The window's sentence as a cell and the rest (`1M` / `the registry's
77
+ * for this endpoint`) — the window chain words it `<size>, <source>`. */
78
+ function windowCells(window) {
79
+ const cut = window.indexOf(", ");
80
+ return cut < 0 ? {} : { brief: window.slice(0, cut), means: window.slice(cut + 2) };
81
+ }
82
+ /** Graphite R3e: the settings as data — the /settings panel's rows and
83
+ * the printed form below read the same list. */
84
+ export function settingsFacts(i) {
77
85
  const trust = layered(i, "projectTrust");
78
86
  const auto = i.env.KISO_AUTO_COMPACT !== undefined ? { value: i.env.KISO_AUTO_COMPACT, from: "env KISO_AUTO_COMPACT" } : layered(i, "autoCompact");
79
87
  const rows = [
@@ -86,9 +94,18 @@ export function settingsRows(i) {
86
94
  value: i.floorOn ? "on — irrecoverable deletes are refused in every mode" : "off",
87
95
  from: i.user?.floor !== undefined ? "user config" : "default",
88
96
  change: "\"floor\": \"off\" in ~/.kiso/config.json (user config only)",
97
+ brief: i.floorOn ? "on" : "off",
98
+ ...(i.floorOn ? { means: "irrecoverable deletes are refused in every mode" } : {}),
99
+ },
100
+ { name: "window", value: i.window, from: "the window chain (see /status)", change: "\"contextWindow\" on the profile; KISO_CONTEXT_WINDOW for one run", ...windowCells(i.window) },
101
+ {
102
+ name: "compaction",
103
+ value: "in-run: past half the window at a phase end, past 80% at once",
104
+ from: "built in",
105
+ change: "/compact on demand",
106
+ brief: "50% · 80%",
107
+ means: "past half the window at a phase end, past 80% at once",
89
108
  },
90
- { name: "window", value: i.window, from: "the window chain (see /status)", change: "\"contextWindow\" on the profile; KISO_CONTEXT_WINDOW for one run" },
91
- { name: "compaction", value: "in-run: past half the window at a phase end, past 80% at once", from: "built in", change: "/compact on demand" },
92
109
  {
93
110
  name: "auto-compact",
94
111
  value: auto.value === undefined ? "off" : typeof auto.value === "object" ? `at ${auto.value.thresholdRatio} of the window, at a run's start` : String(auto.value),
@@ -101,9 +118,21 @@ export function settingsRows(i) {
101
118
  value: i.thinkingHidden ? "hidden — one line per block" : "shown",
102
119
  from: i.thinkingRemembered ? "ctrl+t (remembered)" : "default",
103
120
  change: "ctrl+t",
121
+ brief: i.thinkingHidden ? "hidden" : "shown",
122
+ ...(i.thinkingHidden ? { means: "one line per block" } : {}),
104
123
  },
105
124
  { name: "version", value: i.version, from: "running", change: "kiso update" },
106
125
  ];
126
+ return rows;
127
+ }
128
+ /** The printed form: one block per setting — what it is, where it came
129
+ * from and how to change it (a pipe, a dock-less TTY, and a panel row's
130
+ * enter). */
131
+ export function settingRow(r, width = r.name.length + 2) {
132
+ return `${r.name.padEnd(width)}${r.value}\n${" ".repeat(width)}from ${r.from} · change: ${r.change}`;
133
+ }
134
+ export function settingsRows(i) {
135
+ const rows = settingsFacts(i);
107
136
  const width = Math.max(...rows.map((r) => r.name.length)) + 2;
108
- return rows.map((r) => `${r.name.padEnd(width)}${r.value}\n${" ".repeat(width)}from ${r.from} · change: ${r.change}`);
137
+ return rows.map((r) => settingRow(r, width));
109
138
  }
@@ -44,6 +44,17 @@ export declare function nearest(name: string, names: readonly string[]): string[
44
44
  * `model only` tag for `user-invocable: false`, then the broken entries
45
45
  * with the loader's own reason. */
46
46
  export declare function skillsRows(catalog: SkillsCatalog | null, sourceOf: (dir: string) => string, userDir: string): string[];
47
+ /**
48
+ * The sheets round (owner, 2026-10-06) — `/skills` on a dock is a sheet
49
+ * over the input, the shape `/status` has (§8.16). The band says how many,
50
+ * how many cannot load, and where they live when that is one place
51
+ * (`skills · 3 · 1 cannot load · ~/.kiso/skills`); each skill is one row,
52
+ * `/name` in ink in a measured column and its description dim, cut by
53
+ * cells; with more than one place, the place ends the row. A skill that
54
+ * cannot load says why, in the failure colour. How to run one is the
55
+ * closing row. On a pipe `/skills` prints `skillsRows`, unchanged.
56
+ */
57
+ export declare function skillsSheetRows(catalog: SkillsCatalog | null, sourceOf: (dir: string) => string, userDir: string, W: number): string[];
47
58
  /**
48
59
  * 0.40.1 (owner's ruling, revoking the 0.41.0 deferral) — the installed
49
60
  * skills as `/` menu entries. The same rule the dispatcher follows, so the
@@ -14,6 +14,9 @@
14
14
  * one answer — the model's index and this door are one list), and every
15
15
  * outcome is data. The dispatcher does the printing and the submitting.
16
16
  */
17
+ import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
18
+ import { cutLine, escapeTerminal, palette } from "@vincemakes/kiso-tui-cells/render";
19
+ import { visibleWidth, widthCut } from "@vincemakes/kiso-tui-cells/width";
17
20
  /**
18
21
  * What a submitted line asks of the skills, if anything. `null` means the
19
22
  * line is not a skill request, and the dispatcher goes on as before — which
@@ -120,6 +123,62 @@ export function skillsRows(catalog, sourceOf, userDir) {
120
123
  rows.push("/<name> [args] or /skill <name> [args] runs one · a built-in command wins a shared name");
121
124
  return rows;
122
125
  }
126
+ /** The last `cells` cells of `text`, by display width. */
127
+ function leftCut(text, cells) {
128
+ const chars = Array.from(text);
129
+ let out = "";
130
+ for (let i = chars.length - 1; i >= 0; i -= 1) {
131
+ if (visibleWidth(chars[i] + out) > cells)
132
+ break;
133
+ out = chars[i] + out;
134
+ }
135
+ return out;
136
+ }
137
+ /**
138
+ * The sheets round (owner, 2026-10-06) — `/skills` on a dock is a sheet
139
+ * over the input, the shape `/status` has (§8.16). The band says how many,
140
+ * how many cannot load, and where they live when that is one place
141
+ * (`skills · 3 · 1 cannot load · ~/.kiso/skills`); each skill is one row,
142
+ * `/name` in ink in a measured column and its description dim, cut by
143
+ * cells; with more than one place, the place ends the row. A skill that
144
+ * cannot load says why, in the failure colour. How to run one is the
145
+ * closing row. On a pipe `/skills` prints `skillsRows`, unchanged.
146
+ */
147
+ export function skillsSheetRows(catalog, sourceOf, userDir, W) {
148
+ const p = palette();
149
+ const entries = catalog?.entries ?? [];
150
+ const broken = catalog?.broken ?? [];
151
+ const close = ` ${p.dim}/<name> runs one \u00b7 a built-in wins its name \u00b7 esc closes${p.reset}`;
152
+ if (entries.length === 0 && broken.length === 0) {
153
+ return [bandHeader("skills \u00b7 none", W), ` ${p.dim}add one as ${escapeTerminal(userDir)}/<name>/SKILL.md${p.reset}`, close].map((r) => cutLine(r, W));
154
+ }
155
+ const places = [...new Set([...entries.map((e) => sourceOf(e.dir)), ...broken.map((b) => sourceOf(b.dir))])];
156
+ const facts = [String(entries.length), ...(broken.length > 0 ? [`${broken.length} cannot load`] : [])];
157
+ // the place is cut from the LEFT when the band cannot hold it (P4's
158
+ // rule for a path in a band name): its end — the folder's own name —
159
+ // and the rule after it stay on screen
160
+ const head = `skills \u00b7 ${facts.join(" \u00b7 ")}`;
161
+ const where = places.length === 1 ? escapeTerminal(places[0]) : "";
162
+ const roomFor = Math.max(0, W - 8 - visibleWidth(head) - 3);
163
+ const shown = where === "" || roomFor < 2 ? "" : visibleWidth(where) <= roomFor ? where : `\u2026${leftCut(where, roomFor - 1)}`;
164
+ const rows = [bandHeader(shown === "" ? head : `${head} \u00b7 ${shown}`, W)];
165
+ const names = [...entries.map((e) => `/${escapeTerminal(e.name)}`), ...broken.map((b) => escapeTerminal(b.dir))];
166
+ const nw = Math.max(...names.map((n) => visibleWidth(n))) + 2;
167
+ const fit = (text, room) => (visibleWidth(text) <= room ? text : `${widthCut(text, Math.max(1, room - 1))}…`);
168
+ const place = (dir) => (places.length > 1 ? ` \u00b7 ${escapeTerminal(sourceOf(dir))}` : "");
169
+ const room = Math.max(1, W - 2 - nw - 1);
170
+ for (const e of entries) {
171
+ const name = `/${escapeTerminal(e.name)}`;
172
+ const what = `${escapeTerminal(e.description)}${e.userInvocable ? "" : " (model only)"}${place(e.dir)}`;
173
+ rows.push(` ${name}${" ".repeat(nw - visibleWidth(name))}${p.dim}${fit(what, room)}${p.reset}`);
174
+ }
175
+ for (const b of broken) {
176
+ const name = escapeTerminal(b.dir);
177
+ rows.push(` ${p.dim}${name}${" ".repeat(nw - visibleWidth(name))}${p.reset}${p.fail}${fit(`cannot load: ${escapeTerminal(b.reason)}${place(b.dir)}`, room)}${p.fgEnd}`);
178
+ }
179
+ rows.push(close);
180
+ return rows.map((r) => cutLine(r, W));
181
+ }
123
182
  /**
124
183
  * 0.40.1 (owner's ruling, revoking the 0.41.0 deferral) — the installed
125
184
  * skills as `/` menu entries. The same rule the dispatcher follows, so the
package/dist/state.d.ts CHANGED
@@ -196,6 +196,13 @@ export interface LineInput {
196
196
  panelAsk(view: PanelView, onCommit: (v: PanelVerdict) => void, opts?: {
197
197
  safer?: () => Promise<SaferAnswer>;
198
198
  }): void;
199
+ /** Graphite R3e: a read-only sheet over the input with the caller's rows
200
+ * (`/status`, `/context`, `/skills`); any key closes it. Absent off a
201
+ * dock. */
202
+ openSheet?(rows: (W: number) => string[]): void;
203
+ /** The sheets round: open the command list, as a typed `/` does
204
+ * (`/help`). Absent off a dock. */
205
+ openCommands?(): void;
199
206
  /** W21: cancel the panel — the SIGINT pair to panelAsk. */
200
207
  panelCancel(): void;
201
208
  /** TUI2-R2 ②: open the session picker — the editor takes the keys
@@ -1,5 +1,6 @@
1
1
  import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
2
2
  import { type TaskInfo } from "@vincemakes/kiso-runtime/internal";
3
+ import type { NoticeMark } from "@vincemakes/kiso-tui";
3
4
  /** The lines a delivery shows: today's `✦ task t1 exited · t2 failed` for
4
5
  * the ordinary items, and one `✦ lost track of …` row per lost task. The
5
6
  * shape is fixed (a design keys on it): the opening `✦ lost track of <id>
@@ -28,3 +29,35 @@ export declare function taskOutput(task: TaskInfo, lines?: number): string[];
28
29
  export declare function taskCounts(tasks: readonly TaskInfo[]): {
29
30
  readonly running: number;
30
31
  };
32
+ /** The tasks round (owner, 2026-10-06) — how a task ended, in one word and
33
+ * its colour: a shell `exited 0` (or `failed 1`), a child `answered` (or
34
+ * `failed`), `stopped` dim, `lost track — may still be running` gold (the
35
+ * main sync, Amendment 8's words; it read `◌ outcome unknown`), `ready`
36
+ * when a long-running command said it was. Read from the task's own journal, so a
37
+ * resumed session says what a live one did; with no journal entry, the
38
+ * delivery's transition word, uncoloured. */
39
+ export declare function taskOutcome(task: TaskInfo | undefined, transition: TaskDeliveryItem["transition"]): {
40
+ readonly word: string;
41
+ readonly tone: NoticeMark["tone"] | null;
42
+ };
43
+ /** The tasks round — a task delivery on the terminal: one meta row per
44
+ * task, `TASK` once and the rows under it unlabelled, each `<id> <how it
45
+ * ended> · <what ran>` with the outcome word marked; a lost task adds
46
+ * where to look (`· /tasks shows it`). The pipe keeps `taskNoticeLines`'
47
+ * lines, byte for byte. */
48
+ export declare function taskNoticeRows(items: readonly TaskDeliveryItem[], tasks: readonly TaskInfo[]): {
49
+ readonly label: string;
50
+ readonly sentence: string;
51
+ readonly mark?: NoticeMark;
52
+ }[];
53
+ /** A session's tasks for display, or none: a journal that cannot be read
54
+ * is reported where it is read on purpose (`/tasks`), never here. */
55
+ export declare function tasksForDisplay(manager: {
56
+ list(): TaskInfo[];
57
+ } | undefined): readonly TaskInfo[];
58
+ /** The tasks round (owner, 2026-10-06) — `/tasks` → show its output, on a
59
+ * dock: a sheet over the input, `/context`'s shape (§8.16). The band
60
+ * names the task, which output it is, and how it ended; the lines at the
61
+ * content edge, each cut to one row; the closing row. On a pipe the
62
+ * printed lines stay. */
63
+ export declare function taskOutputSheetRows(task: TaskInfo, lines: readonly string[], W: number): string[];
@@ -16,6 +16,8 @@ import { closeSync, fstatSync, openSync, readFileSync, readSync } from "node:fs"
16
16
  import { dirname, join } from "node:path";
17
17
  import { readRecords } from "@vincemakes/kiso-runtime/internal";
18
18
  import { elapsedLabel } from "@vincemakes/kiso-tui-cells";
19
+ import { SHEET_CLOSE, bandHeader } from "@vincemakes/kiso-tui-cells/strings";
20
+ import { cutLine, escapeTerminal, palette } from "@vincemakes/kiso-tui-cells/render";
19
21
  /** The lines a delivery shows: today's `✦ task t1 exited · t2 failed` for
20
22
  * the ordinary items, and one `✦ lost track of …` row per lost task. The
21
23
  * shape is fixed (a design keys on it): the opening `✦ lost track of <id>
@@ -121,3 +123,74 @@ export function taskCounts(tasks) {
121
123
  running += 1;
122
124
  return { running };
123
125
  }
126
+ /** The tasks round (owner, 2026-10-06) — how a task ended, in one word and
127
+ * its colour: a shell `exited 0` (or `failed 1`), a child `answered` (or
128
+ * `failed`), `stopped` dim, `lost track — may still be running` gold (the
129
+ * main sync, Amendment 8's words; it read `◌ outcome unknown`), `ready`
130
+ * when a long-running command said it was. Read from the task's own journal, so a
131
+ * resumed session says what a live one did; with no journal entry, the
132
+ * delivery's transition word, uncoloured. */
133
+ export function taskOutcome(task, transition) {
134
+ if (task === undefined)
135
+ return { word: transition, tone: null };
136
+ if (transition === "ready")
137
+ return { word: "ready", tone: "ok" };
138
+ const label = taskStateLabel(task);
139
+ if (label === "exited 0")
140
+ return { word: task.agent !== undefined ? "answered" : "exited 0", tone: "ok" };
141
+ if (label.startsWith("failed"))
142
+ return { word: task.agent !== undefined && label !== "failed to start" ? "failed" : label, tone: "fail" };
143
+ if (task.state.kind === "unknown")
144
+ return { word: label, tone: "gold" };
145
+ return { word: label, tone: null };
146
+ }
147
+ /** The tasks round — a task delivery on the terminal: one meta row per
148
+ * task, `TASK` once and the rows under it unlabelled, each `<id> <how it
149
+ * ended> · <what ran>` with the outcome word marked; a lost task adds
150
+ * where to look (`· /tasks shows it`). The pipe keeps `taskNoticeLines`'
151
+ * lines, byte for byte. */
152
+ export function taskNoticeRows(items, tasks) {
153
+ // (a lost task adds `· /tasks shows it`, before what ran)
154
+ return items.map((item, i) => {
155
+ const task = tasks.find((t) => t.id === item.taskId);
156
+ const { word, tone } = item.transition === "unknown" && task === undefined ? { word: "lost track — may still be running", tone: "gold" } : taskOutcome(task, item.transition);
157
+ const what = task === undefined ? "" : ` · ${taskWhat(task)}`;
158
+ // where to look before what ran: the row is cut at the end
159
+ const where = item.transition === "unknown" ? " · /tasks shows it" : "";
160
+ return { label: i === 0 ? "TASK" : "", sentence: `${item.taskId} ${word}${where}${what}`, ...(tone !== null ? { mark: { text: word, tone } } : {}) };
161
+ });
162
+ }
163
+ /** A session's tasks for display, or none: a journal that cannot be read
164
+ * is reported where it is read on purpose (`/tasks`), never here. */
165
+ export function tasksForDisplay(manager) {
166
+ if (manager === undefined)
167
+ return [];
168
+ try {
169
+ return manager.list();
170
+ }
171
+ catch {
172
+ return [];
173
+ }
174
+ }
175
+ /** The tasks round (owner, 2026-10-06) — `/tasks` → show its output, on a
176
+ * dock: a sheet over the input, `/context`'s shape (§8.16). The band
177
+ * names the task, which output it is, and how it ended; the lines at the
178
+ * content edge, each cut to one row; the closing row. On a pipe the
179
+ * printed lines stay. */
180
+ export function taskOutputSheetRows(task, lines, W) {
181
+ const p = palette();
182
+ const which = task.agent !== undefined ? "its answer" : "its last output";
183
+ const { word } = taskOutcome(task, "exited");
184
+ const rows = [bandHeader(`${task.id} · ${which} · ${task.state.kind === "running" || task.state.kind === "starting" ? taskStateLabel(task) : word}`, W)];
185
+ // the main sync (0.46.2, Amendment 8): a lost task says why, first, in
186
+ // gold — the printed form's `t1 — lost track: …` line
187
+ const why = lostReason(task);
188
+ if (why !== undefined)
189
+ rows.push(` ${p.gold}lost track: ${escapeTerminal(why)}; it may still be running${p.gold === "" ? "" : p.fgEnd}`);
190
+ if (lines.length === 0)
191
+ rows.push(` ${p.dim}nothing yet${p.reset}`);
192
+ for (const line of lines)
193
+ rows.push(` ${p.ink2}${escapeTerminal(line)}${p.ink2 === "" ? "" : p.fgEnd}`);
194
+ rows.push(` ${p.dim}${SHEET_CLOSE}${p.reset}`);
195
+ return rows.map((r) => cutLine(r, W));
196
+ }
@@ -111,4 +111,11 @@ export declare function applyProjectMerges(artifacts: ProjectArtifacts): void;
111
111
  * execution stays uncertain and durable — no rerun/abandoned is
112
112
  * fabricated, round 10). The resolution feedback lands in the body —
113
113
  * the body is the single stdout writer, never a stray console.log. */
114
+ /** Graphite P1b — what an uncertain execution's panel quotes: a shell
115
+ * command verbatim (every line of it), any other call's target as its
116
+ * tool card names it. From the execution record's own input. */
117
+ export declare function uncertainTarget(name: string, input: Readonly<Record<string, unknown>>): string[];
118
+ /** Graphite P1b — an interrupted ask_user's questions, as the model wrote
119
+ * them; empty when the input carries none (the panel then quotes the id). */
120
+ export declare function askedQuestions(input: Readonly<Record<string, unknown>>): string[];
114
121
  export declare function resolveUncertains(session: AgentSession, input: LineInput, isCancelled: () => boolean): Promise<void>;
package/dist/trust-ui.js CHANGED
@@ -9,10 +9,11 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, symlinkS
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
- import { askDeclineAll, askView, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, unansweredAskView } from "@vincemakes/kiso-tui";
12
+ import { askDeclineAll, askView, projectTrustRows, projectTrustView, projectUntrustedNote, toolTarget, uncertainView, unansweredAskView } from "@vincemakes/kiso-tui";
13
13
  import { projectArtifacts, recordTrust, trustFor } from "@vincemakes/kiso-runtime";
14
- import { bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedTempPaths, neverInherited } from "./state.js";
14
+ import { body, bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedTempPaths, neverInherited } from "./state.js";
15
15
  import { guardSavedAllow } from "./protected-writes.js";
16
+ import { setTitleState, titleState } from "./window-title.js";
16
17
  import { mergeDirPrefix } from "./temp-sweep.js";
17
18
  import { loadUserConfig, resolveProjectTrustPolicy } from "./config.js";
18
19
  import { getDontAsk } from "./mode.js";
@@ -50,7 +51,15 @@ opts) {
50
51
  console.log(`[non-interactive — no human to ask: ${view.fallbackQuestion}]`);
51
52
  return Promise.resolve({ action: "deny", reason: "no human to ask" });
52
53
  }
53
- return new Promise((resolve) => {
54
+ // Graphite §8.10: while the person is asked, the title says so; the
55
+ // answer (or the cancel) puts back what it said before
56
+ const before = titleState();
57
+ setTitleState("needs-you");
58
+ return new Promise((settle) => {
59
+ const resolve = (v) => {
60
+ setTitleState(before);
61
+ settle(v);
62
+ };
54
63
  let settled = false;
55
64
  pendingAsk = () => {
56
65
  if (settled)
@@ -151,7 +160,7 @@ export function askUi(input) {
151
160
  // question the model puts is declined, recorded as unanswered,
152
161
  // exactly as a non-interactive session declines it.
153
162
  if (getDontAsk()) {
154
- bodyLog("[dontAsk] the model's question was declined — nothing asks in dontAsk");
163
+ body.notice("[dontAsk] the model's question was declined — nothing asks in dontAsk");
155
164
  return askDeclineAll(spec);
156
165
  }
157
166
  const verdict = await askPanel(input, askView(spec));
@@ -331,15 +340,33 @@ export async function resolveProjectTrust(input) {
331
340
  // carries the question, the args the artifact listing (the same rows
332
341
  // the bodyLog below records, verbatim — the listing still lands in
333
342
  // the scrollback; the panel is a bounded block, the record is not).
334
- bodyLog(`[project .kiso] ${artifacts.root}`);
335
- bodyLog(projectTrustRows(artifacts.files, " ").join("\n")); // DC-51: one call, one cell
336
- const verdict = await askPanel(input, projectTrustView(artifacts.root, artifacts.files));
343
+ // The tasks round (owner, 2026-10-06): on a dock the question lists the
344
+ // files itself, so nothing is printed above it; the record lands after
345
+ // the answer (below). Off a dock the listing is the record, as before.
346
+ if (!dock.active) {
347
+ bodyLog(`[project .kiso] ${artifacts.root}`);
348
+ bodyLog(projectTrustRows(artifacts.files, " ").join("\n")); // DC-51: one call, one cell
349
+ }
350
+ const home = homedir();
351
+ const shownRoot = artifacts.root === home ? "~" : artifacts.root.startsWith(`${home}/`) ? `~${artifacts.root.slice(home.length)}` : artifacts.root;
352
+ const verdict = await askPanel(input, projectTrustView(artifacts.root, artifacts.files, shownRoot));
337
353
  // A cancel is a "no" HERE — refused is sticky, the project does not
338
354
  // load (re-evaluate by deleting the trust line or changing a file).
339
355
  // The non-TTY branch above returned WITHOUT a record so an interactive
340
356
  // run can still decide later.
341
357
  const granted = verdict.action === "allow";
342
358
  recordTrust({ root: artifacts.root, digest: artifacts.digest, decision: granted ? "granted" : "refused" });
359
+ // the record of what was trusted stays in the scrollback: ONE meta row,
360
+ // once the person has answered — `PROJECT trusted · ~/w/.kiso · <files>`
361
+ if (dock.active) {
362
+ const word = granted ? "trusted" : "not trusted";
363
+ const files = projectTrustRows(artifacts.files, "").map((r) => r.trim().replace(/\s{2,}/g, " ")).join(", ");
364
+ // a long root outside home keeps its last two parts — the folder's
365
+ // own name — so the row still has room for what was trusted
366
+ const parts = shownRoot.split("/");
367
+ const where = shownRoot.length > 40 && !shownRoot.startsWith("~") && parts.length > 3 ? `\u2026/${parts.slice(-2).join("/")}` : shownRoot;
368
+ body.metaNotice(`[project .kiso] ${word} \u2014 ${artifacts.root}`, [{ label: "PROJECT", sentence: `${word} \u00b7 ${where} \u00b7 ${files}`, ...(granted ? { mark: { text: word, tone: "ok" } } : {}) }]);
369
+ }
343
370
  if (!granted)
344
371
  return null;
345
372
  applyProjectMerges(artifacts);
@@ -471,6 +498,22 @@ function readdirSyncSafe(dir) {
471
498
  * execution stays uncertain and durable — no rerun/abandoned is
472
499
  * fabricated, round 10). The resolution feedback lands in the body —
473
500
  * the body is the single stdout writer, never a stray console.log. */
501
+ /** Graphite P1b — what an uncertain execution's panel quotes: a shell
502
+ * command verbatim (every line of it), any other call's target as its
503
+ * tool card names it. From the execution record's own input. */
504
+ export function uncertainTarget(name, input) {
505
+ if (name === "shell")
506
+ return String(input.command ?? "").split(/\r\n|\n|\r/);
507
+ return [toolTarget(name, { ...input })];
508
+ }
509
+ /** Graphite P1b — an interrupted ask_user's questions, as the model wrote
510
+ * them; empty when the input carries none (the panel then quotes the id). */
511
+ export function askedQuestions(input) {
512
+ const qs = input.questions;
513
+ if (!Array.isArray(qs))
514
+ return [];
515
+ return qs.flatMap((q) => (typeof q === "object" && q !== null && typeof q.question === "string" ? [q.question] : []));
516
+ }
474
517
  export async function resolveUncertains(session, input, isCancelled) {
475
518
  // The lead's review of #63, B5: under dontAsk nothing asks, so the
476
519
  // uncertain executions are left exactly as a cancel leaves them —
@@ -478,8 +521,10 @@ export async function resolveUncertains(session, input, isCancelled) {
478
521
  // line says what to do. An unattended session must not sit on a panel.
479
522
  if (getDontAsk()) {
480
523
  const n = session.uncertainExecutions().length;
524
+ // Graphite P1b: a notice, so a dock draws it as the UNCERTAIN meta row
525
+ // (notice-meta.ts); a pipe prints the same line it always did
481
526
  if (n > 0)
482
- bodyLog(`[dontAsk] ${n} uncertain execution${n === 1 ? "" : "s"} left unresolved — resolve them in an asking mode`);
527
+ body.notice(`[dontAsk] ${n} uncertain execution${n === 1 ? "" : "s"} left unresolved — resolve them in an asking mode`);
483
528
  return;
484
529
  }
485
530
  for (const uncertain of session.uncertainExecutions()) {
@@ -487,7 +532,7 @@ export async function resolveUncertains(session, input, isCancelled) {
487
532
  // have applied — it is a question nobody answered. The COPY says
488
533
  // so; the mechanism is untouched (allow → the runtime's own rerun
489
534
  // resolution, whose error-fill text this round never edits).
490
- const view = uncertain.name === "ask_user" ? unansweredAskView(uncertain.executionId) : uncertainView(uncertain.name, uncertain.executionId);
535
+ const view = uncertain.name === "ask_user" ? unansweredAskView(uncertain.executionId, askedQuestions(uncertain.input)) : uncertainView(uncertain.name, uncertain.executionId, uncertainTarget(uncertain.name, uncertain.input));
491
536
  const verdict = await askPanel(input, view);
492
537
  if (isCancelled() || verdict.action === "cancel") {
493
538
  // round 10: a cancellation NEVER records a verdict — the execution