@vincemakes/kiso-code 0.46.1 → 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/auth/oauth/chatgpt.js +25 -8
- package/dist/auth/oauth/page.d.ts +25 -0
- package/dist/auth/oauth/page.js +44 -0
- package/dist/chat.d.ts +12 -1
- package/dist/chat.js +133 -46
- package/dist/coding-prompt.d.ts +7 -0
- package/dist/coding-prompt.js +12 -4
- package/dist/config.d.ts +7 -3
- package/dist/config.js +39 -4
- package/dist/create-coding-agent.js +16 -6
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +337 -83
- package/dist/floor.d.ts +21 -0
- package/dist/floor.js +35 -35
- package/dist/git-branch.d.ts +17 -0
- package/dist/git-branch.js +69 -0
- package/dist/in-run-compaction.d.ts +54 -0
- package/dist/in-run-compaction.js +44 -0
- package/dist/index.js +106 -45
- package/dist/opening.d.ts +48 -0
- package/dist/opening.js +43 -0
- package/dist/protected-writes.d.ts +8 -0
- package/dist/protected-writes.js +12 -0
- package/dist/remote-boundary.d.ts +55 -0
- package/dist/remote-boundary.js +217 -0
- package/dist/replay.d.ts +8 -1
- package/dist/replay.js +22 -10
- package/dist/resume.js +21 -16
- package/dist/session-cards.d.ts +2 -0
- package/dist/session-cards.js +3 -3
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +34 -5
- package/dist/skill-invoke.d.ts +11 -0
- package/dist/skill-invoke.js +59 -0
- package/dist/state.d.ts +14 -3
- package/dist/state.js +29 -3
- package/dist/task-notice.d.ts +48 -7
- package/dist/task-notice.js +123 -13
- package/dist/trust-ui.d.ts +7 -0
- package/dist/trust-ui.js +54 -9
- package/dist/window-title.d.ts +43 -23
- package/dist/window-title.js +78 -50
- package/package.json +14 -14
package/dist/dispatch.js
CHANGED
|
@@ -3,29 +3,30 @@
|
|
|
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 { STATUS_GLYPHS, contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, namedPickView, compactingStatus, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
6
|
+
import { STATUS_GLYPHS, contextRows, contextSheetRows, contextUnavailableRows, contextUnavailableSheetRows, displayVerb, escapeTerminal, helpRows, toolTarget, infoSheetRows, kUnit, modePickView, modelPickView, namedPickView, settingsPickView, compactingStatus, liveRow, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
7
7
|
import { newSessionId } from "./session-id.js";
|
|
8
|
-
import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
8
|
+
import { buildAdapter, lookupModelMetadata, readSessionName, resolveContinuationScope, resolveReasoning, sessionTitle, tiersFor, writeSessionName } from "@vincemakes/kiso-runtime/internal";
|
|
9
9
|
import { MODE_LABEL, MODE_NOTE, OFFERED_MODES, applyModeSetting, getDontAsk, getMode, modeDisplay, parseMode, setDontAsk } from "./mode.js";
|
|
10
10
|
import { clipboardWrite, lastAnswer } from "./clipboard.js";
|
|
11
11
|
import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
|
|
12
|
-
import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
|
|
12
|
+
import { activeStoreDir, agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
|
|
13
13
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
14
14
|
import { profileProviderLabel, providerLabel } from "./provider-label.js";
|
|
15
15
|
import { installedVersion, versionStatusLine } from "./stale-version.js";
|
|
16
16
|
import { preferences, rememberEffort, setPreference } from "./preferences.js";
|
|
17
|
-
import { settingsRows } from "./settings.js";
|
|
17
|
+
import { settingRow, settingsFacts, settingsRows } from "./settings.js";
|
|
18
18
|
import { floorOn, settingsLayers } from "./state.js";
|
|
19
19
|
import { currentGround } from "@vincemakes/kiso-tui-cells/render";
|
|
20
|
-
import { queuedSwitchLines,
|
|
21
|
-
import { taskOutput, taskRow, taskStateLabel } from "./task-notice.js";
|
|
20
|
+
import { queuedSwitchLines, tasksFor } from "./state.js";
|
|
21
|
+
import { lostReason, taskOutput, taskRow, taskStateLabel, taskOutputSheetRows } from "./task-notice.js";
|
|
22
22
|
import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
|
|
23
23
|
import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
|
|
24
24
|
import { shellTool } from "@vincemakes/kiso-tools-node";
|
|
25
25
|
import { dirname, join } from "node:path";
|
|
26
26
|
import { realpathSync } from "node:fs";
|
|
27
27
|
import { homedir } from "node:os";
|
|
28
|
-
import { resolveSkillLine, skillsRows } from "./skill-invoke.js";
|
|
28
|
+
import { resolveSkillLine, skillsRows, skillsSheetRows } from "./skill-invoke.js";
|
|
29
|
+
import { setTitleName, setTitleState } from "./window-title.js";
|
|
29
30
|
/** Where a skill directory really lives. Project and user skills are
|
|
30
31
|
* merged into one scan directory by symlink, so the scan root names a
|
|
31
32
|
* temp directory; the link's target is the answer a person can use. */
|
|
@@ -133,6 +134,17 @@ function signInNote(p) {
|
|
|
133
134
|
}
|
|
134
135
|
return p.apiKeyEnv ?? "no key";
|
|
135
136
|
}
|
|
137
|
+
/** Graphite P3: why a profile cannot run, for its opened row — the words a
|
|
138
|
+
* pick of it prints (`unavailableReason`), without the `model <name>:
|
|
139
|
+
* unavailable —` the row already says and the config aside. */
|
|
140
|
+
function unavailableWhy(name, p) {
|
|
141
|
+
const head = `model ${name}: `;
|
|
142
|
+
const said = unavailableReason(name, p);
|
|
143
|
+
return (said.startsWith(head) ? said.slice(head.length) : said)
|
|
144
|
+
.replace(/^unavailable \u2014 /, "")
|
|
145
|
+
.replace(/ \(configs never store keys, only the env-var name\)$/, "")
|
|
146
|
+
.replaceAll("`", "");
|
|
147
|
+
}
|
|
136
148
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
137
149
|
* replay routes through it too — a queued "/last" must never become a
|
|
138
150
|
* user turn (v2c: the rl lives in main, so lines arrive earlier and the
|
|
@@ -207,7 +219,10 @@ function runBang(command, send, ctx) {
|
|
|
207
219
|
ctx.submitTurn(block);
|
|
208
220
|
return; // submitTurn owns the prompt from here
|
|
209
221
|
}
|
|
210
|
-
|
|
222
|
+
// G3 (R2d): `!!` is the person's command card, `not sent` — its
|
|
223
|
+
// output escaped like every tool's (the raw block this replaced
|
|
224
|
+
// printed the fence's markers and did not escape the output)
|
|
225
|
+
body.bang(command, result.content, result.isError === true);
|
|
211
226
|
}
|
|
212
227
|
finally {
|
|
213
228
|
activeBang = null;
|
|
@@ -215,6 +230,19 @@ function runBang(command, send, ctx) {
|
|
|
215
230
|
ctx.input.prompt();
|
|
216
231
|
});
|
|
217
232
|
}
|
|
233
|
+
/** Graphite R3d: a name is cut where a derived title is (`sessionTitle`). */
|
|
234
|
+
const NAME_MAX = 60;
|
|
235
|
+
/** Graphite §8.7 (R3b): the next /compact was dispatched by kiso, not
|
|
236
|
+
* typed — its row says why: `auto` (the opt-in threshold), or P1b's
|
|
237
|
+
* `cold cache` (a resumed session whose cache expired). Typed, it is
|
|
238
|
+
* `manual`. Read once by the next /compact. */
|
|
239
|
+
let nextCompactWhy = null;
|
|
240
|
+
export function markCompactWhy(why) {
|
|
241
|
+
nextCompactWhy = why;
|
|
242
|
+
}
|
|
243
|
+
export function markAutoCompact() {
|
|
244
|
+
markCompactWhy("auto");
|
|
245
|
+
}
|
|
218
246
|
export function dispatch(line, ctx) {
|
|
219
247
|
// DC-57 (the owner's ruling, 2026-09-21): a session that is LEAVING — a
|
|
220
248
|
// switch or a reload has been requested from an earlier segment of the
|
|
@@ -268,6 +296,18 @@ export function dispatch(line, ctx) {
|
|
|
268
296
|
// between any two elements, so a row-per-call group comes out
|
|
269
297
|
// double-spaced — which is what the owner photographed on the
|
|
270
298
|
// ctrl+o expansion.
|
|
299
|
+
//
|
|
300
|
+
// The sheets round (owner, 2026-10-06): on a dock /help opens the
|
|
301
|
+
// command list — the band a typed `/` opens, every command and
|
|
302
|
+
// installed skill with its description, filtered as you type —
|
|
303
|
+
// so the list you read is the list you pick from (§8.16). Its
|
|
304
|
+
// keys and gestures are the keys sheet's (`?`). Off a dock (a
|
|
305
|
+
// pipe, -p) the table below is what it always printed.
|
|
306
|
+
if (dock.active && ctx.input.openCommands !== undefined) {
|
|
307
|
+
ctx.input.openCommands();
|
|
308
|
+
ctx.input.prompt();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
271
311
|
bodyLog(helpRows().join("\n"), "words");
|
|
272
312
|
ctx.input.prompt();
|
|
273
313
|
});
|
|
@@ -279,10 +319,13 @@ export function dispatch(line, ctx) {
|
|
|
279
319
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
280
320
|
const t = body.lastThinking();
|
|
281
321
|
if (t === null) {
|
|
282
|
-
|
|
322
|
+
body.notice("[no thinking yet]");
|
|
283
323
|
}
|
|
284
324
|
else {
|
|
285
|
-
|
|
325
|
+
// the last sweep (owner, 2026-10-06): on the terminal a THINKING
|
|
326
|
+
// row and the block in thinking's own look; a pipe prints the text
|
|
327
|
+
const lines = t.trim().split("\n").length;
|
|
328
|
+
body.recall(escapeTerminal(t), "THINKING", `the last block \u00b7 ${lines} line${lines === 1 ? "" : "s"}`, [{ text: t, style: "thinking" }]);
|
|
286
329
|
}
|
|
287
330
|
ctx.input.prompt();
|
|
288
331
|
});
|
|
@@ -295,7 +338,7 @@ export function dispatch(line, ctx) {
|
|
|
295
338
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
296
339
|
const tool = body.lastTool();
|
|
297
340
|
if (tool === null) {
|
|
298
|
-
|
|
341
|
+
body.notice("[no tool call yet]");
|
|
299
342
|
}
|
|
300
343
|
else {
|
|
301
344
|
// TUI2-R2pre ④: the SECTION HEADERS say the act ("--- read
|
|
@@ -309,11 +352,21 @@ export function dispatch(line, ctx) {
|
|
|
309
352
|
// is a machine-readable log whose bytes other things (the
|
|
310
353
|
// e2e gates, anyone's script) already depend on. Without the
|
|
311
354
|
// dock, this is that log — so it keeps the call's own name.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
355
|
+
//
|
|
356
|
+
// The last sweep (owner, 2026-10-06): the screen's form is no
|
|
357
|
+
// longer the rules with a display verb — it is a LAST CALL row
|
|
358
|
+
// (the card's verb and target, how much came back) and the two
|
|
359
|
+
// payloads under dim titles, one cell. The pipe keeps the log.
|
|
360
|
+
const input = escapeTerminal(JSON.stringify(tool.input, null, 2));
|
|
361
|
+
const output = escapeTerminal(tool.result.content);
|
|
362
|
+
const pipe = [`--- ${tool.name} input ---`, input, `--- ${tool.name} output${tool.result.isError ? " (error)" : ""} ---`, output].join("\n");
|
|
363
|
+
const n = tool.result.content.replace(/\n+$/, "").split("\n").length;
|
|
364
|
+
const head = `${displayVerb(tool.name).toUpperCase()} ${toolTarget(tool.name, tool.input)}`.trim();
|
|
365
|
+
const said = tool.result.content === "" ? "no output" : `${n} line${n === 1 ? "" : "s"}${tool.result.isError ? " \u00b7 failed" : ""}`;
|
|
366
|
+
body.recall(pipe, "LAST CALL", `${head} \u00b7 ${said}`, [
|
|
367
|
+
{ title: "input", text: input, style: "output" },
|
|
368
|
+
{ title: tool.result.isError ? "output (error)" : "output", text: output, style: "output" },
|
|
369
|
+
]);
|
|
317
370
|
}
|
|
318
371
|
ctx.input.prompt();
|
|
319
372
|
});
|
|
@@ -445,7 +498,7 @@ export function dispatch(line, ctx) {
|
|
|
445
498
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
446
499
|
const answer = lastAnswer(ctx.session.projected());
|
|
447
500
|
if (answer === null) {
|
|
448
|
-
|
|
501
|
+
body.notice("[nothing to copy — no answer yet]");
|
|
449
502
|
}
|
|
450
503
|
else {
|
|
451
504
|
const r = clipboardWrite(answer);
|
|
@@ -454,7 +507,7 @@ export function dispatch(line, ctx) {
|
|
|
454
507
|
// the dock it rides the W18 hint slot (gone at the next
|
|
455
508
|
// keypress); in a pipe it is a line like any other.
|
|
456
509
|
if (dock.active)
|
|
457
|
-
dock.
|
|
510
|
+
dock.flash(r.message);
|
|
458
511
|
else
|
|
459
512
|
bodyLog(`[${r.message}]`);
|
|
460
513
|
}
|
|
@@ -473,13 +526,13 @@ export function dispatch(line, ctx) {
|
|
|
473
526
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
474
527
|
const r = body.rewrap();
|
|
475
528
|
if (r.lines.length === 0) {
|
|
476
|
-
|
|
529
|
+
body.notice("[/rewrap] no prose to re-wrap yet");
|
|
477
530
|
}
|
|
478
531
|
else {
|
|
479
|
-
|
|
532
|
+
body.notice(`--- re-wrapped ${r.blocks} block${r.blocks === 1 ? "" : "s"} at the current width (appended — the history above is unchanged) ---`);
|
|
480
533
|
bodyLog(r.lines.join("\n")); // DC-51: one call, one cell
|
|
481
534
|
if (r.skipped > 0)
|
|
482
|
-
|
|
535
|
+
body.notice(`--- ${r.skipped} earlier block${r.skipped === 1 ? "" : "s"} not re-wrapped (bounded at two screens) ---`);
|
|
483
536
|
}
|
|
484
537
|
ctx.input.prompt();
|
|
485
538
|
});
|
|
@@ -500,8 +553,20 @@ export function dispatch(line, ctx) {
|
|
|
500
553
|
// derivation still does zero I/O and still ignores trace-shaped data.
|
|
501
554
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
502
555
|
const ledger = readContextLedger(ctx.session.id, ctx.contextWindow());
|
|
556
|
+
const reason = "the ledger is written per request \u2014 run a turn, then ask again";
|
|
557
|
+
// the sheets round (owner, 2026-10-06): on a dock /context is a
|
|
558
|
+
// sheet over the input, read and then typed past (§8.16); off one
|
|
559
|
+
// (a pipe, -p) the lines below are what it always printed
|
|
560
|
+
if (dock.active && ctx.input.openSheet !== undefined) {
|
|
561
|
+
const win = ctx.contextWindow();
|
|
562
|
+
const t = win > 0 ? tiersFor(win, 0) : null;
|
|
563
|
+
const tiers = t === null ? null : { soft: t.soft / win, hard: t.hard / win };
|
|
564
|
+
ctx.input.openSheet((W) => (ledger === null ? contextUnavailableSheetRows(reason, W) : contextSheetRows(ledger, tiers, W)));
|
|
565
|
+
ctx.input.prompt();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
503
568
|
for (const row of ledger === null
|
|
504
|
-
? contextUnavailableRows(
|
|
569
|
+
? contextUnavailableRows(reason)
|
|
505
570
|
: contextRows(ledger)) {
|
|
506
571
|
bodyLog(row);
|
|
507
572
|
}
|
|
@@ -515,7 +580,7 @@ export function dispatch(line, ctx) {
|
|
|
515
580
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
516
581
|
const w = statedContextWindow();
|
|
517
582
|
const profile = currentProfileName;
|
|
518
|
-
|
|
583
|
+
const input = {
|
|
519
584
|
user: settingsLayers.user,
|
|
520
585
|
project: settingsLayers.project,
|
|
521
586
|
env: process.env,
|
|
@@ -531,7 +596,128 @@ export function dispatch(line, ctx) {
|
|
|
531
596
|
thinkingHidden: body.thinkingHidden(),
|
|
532
597
|
thinkingRemembered: preferences().thinking !== undefined,
|
|
533
598
|
version: versionStatusLine(installedVersion(), VERSION).replace(/^version /, ""),
|
|
534
|
-
}
|
|
599
|
+
};
|
|
600
|
+
// Graphite R3e (owner, 2026-09-29): on a dock /settings is a panel.
|
|
601
|
+
// The session's own settings change in it — the mode (←→, the same
|
|
602
|
+
// path as /mode), thinking shown/hidden (←→, ctrl+t's path), the
|
|
603
|
+
// model (⏎ opens /model). What lives in a config file shows its
|
|
604
|
+
// value and source, and ⏎ prints how to change it: kiso never
|
|
605
|
+
// writes the person's config (0.40.6's rule stands for those).
|
|
606
|
+
if (dock.active && ctx.input.panelAsk !== undefined) {
|
|
607
|
+
const facts = settingsFacts(input);
|
|
608
|
+
const mode = getMode();
|
|
609
|
+
const hidden = body.thinkingHidden();
|
|
610
|
+
const THINK = ["shown", "hidden"];
|
|
611
|
+
const DONT_ASK = ["off", "on"];
|
|
612
|
+
// Graphite P3 (owner, 2026-10-04): the session's own four first —
|
|
613
|
+
// model, mode, don't ask, thinking — then what lives in a config
|
|
614
|
+
// file. An order, not groups: no caption between them. The printed
|
|
615
|
+
// form keeps its own order.
|
|
616
|
+
const OWN = ["model", "mode", "don't ask", "thinking"];
|
|
617
|
+
const order = [...OWN.flatMap((n) => facts.filter((f) => f.name === n)), ...facts.filter((f) => !OWN.includes(f.name))];
|
|
618
|
+
const options = order.map((f) => {
|
|
619
|
+
const cols = [f.brief ?? f.value, f.from];
|
|
620
|
+
if (f.name === "mode")
|
|
621
|
+
return { label: "mode", cols: [MODE_LABEL[mode] ?? f.value, f.from], levels: OFFERED_MODES.map((m) => MODE_LABEL[m]), axisLabel: "mode", enter: "\u23ce applies", ...(OFFERED_MODES.indexOf(mode) >= 0 ? { level: OFFERED_MODES.indexOf(mode) } : {}) };
|
|
622
|
+
// the switch is the session's own, as /dont-ask is: walked here, never written to a config
|
|
623
|
+
if (f.name === "don't ask")
|
|
624
|
+
return { label: "don't ask", cols, levels: DONT_ASK, level: getDontAsk() ? 1 : 0, axisLabel: "don't ask", enter: "\u23ce applies" };
|
|
625
|
+
if (f.name === "thinking")
|
|
626
|
+
return { label: "thinking", cols, levels: THINK, level: hidden ? 1 : 0, axisLabel: "thinking", enter: "\u23ce applies" };
|
|
627
|
+
if (f.name === "model") {
|
|
628
|
+
const host = providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl)).replace(/^@/, "");
|
|
629
|
+
return { label: "model", cols: [agentModel, f.from], opened: [host, profile === null ? "" : `profile ${profile}`].filter((t) => t !== "").join(" \u00b7 "), enter: "\u23ce opens /model" };
|
|
630
|
+
}
|
|
631
|
+
// a config file's setting: what its value means, and how it changes
|
|
632
|
+
return { label: f.name, cols, opened: [f.means ?? "", `change: ${f.change}`].filter((t) => t !== "").join(" \u00b7 "), enter: "\u23ce prints how" };
|
|
633
|
+
});
|
|
634
|
+
let level;
|
|
635
|
+
const picked = await new Promise((resolve) => {
|
|
636
|
+
ctx.input.panelAsk(settingsPickView({ header: "settings", noun: "", input: "arrows", columns: ["plain", "dim"], levelColumn: 0, ...(ctx.isRunning() ? { facts: "run paused" } : {}), options }, ctx.isRunning() ? "\u276f run paused" : `\u25b8 ${mode}`), (v) => {
|
|
637
|
+
if (v.action === "picked")
|
|
638
|
+
level = v.level;
|
|
639
|
+
resolve(v.action === "picked" ? v.result : null);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
ctx.paintIdle();
|
|
643
|
+
const f = picked !== null && "index" in picked ? order[picked.index] : undefined;
|
|
644
|
+
if (f === undefined) {
|
|
645
|
+
ctx.input.prompt();
|
|
646
|
+
return; // esc — nothing changed, nothing said
|
|
647
|
+
}
|
|
648
|
+
if (f.name === "mode") {
|
|
649
|
+
const chosen = level === undefined ? undefined : OFFERED_MODES[level];
|
|
650
|
+
if (chosen !== undefined && chosen !== mode) {
|
|
651
|
+
const wasOn = getDontAsk();
|
|
652
|
+
applyModeSetting({ mode: chosen });
|
|
653
|
+
body.modeNotice(`mode \u2192 ${MODE_LABEL[chosen]}`, MODE_LABEL[mode], MODE_LABEL[chosen]);
|
|
654
|
+
if (getDontAsk() !== wasOn)
|
|
655
|
+
body.dontAskNotice(`don't ask \u2192 ${getDontAsk() ? "on" : "off"}`, getDontAsk());
|
|
656
|
+
ctx.paintIdle();
|
|
657
|
+
}
|
|
658
|
+
ctx.input.prompt();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (f.name === "don't ask") {
|
|
662
|
+
// /dont-ask's own path: the switch, its row, and nothing written
|
|
663
|
+
const want = level === undefined ? undefined : DONT_ASK[level] === "on";
|
|
664
|
+
if (want !== undefined && want !== getDontAsk()) {
|
|
665
|
+
setDontAsk(want);
|
|
666
|
+
body.dontAskNotice(`don't ask \u2192 ${want ? "on" : "off"}`, want);
|
|
667
|
+
ctx.paintIdle();
|
|
668
|
+
}
|
|
669
|
+
ctx.input.prompt();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (f.name === "thinking") {
|
|
673
|
+
const want = level === undefined ? undefined : THINK[level];
|
|
674
|
+
if (want !== undefined && (want === "hidden") !== hidden) {
|
|
675
|
+
dispatch("\x14think", ctx); // ctrl+t's own path: the toggle, remembered
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
ctx.input.prompt();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (f.name === "model") {
|
|
682
|
+
dispatch("/model", ctx); // the picker's own segment re-arms the prompt
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
bodyLog(settingRow(f), "words");
|
|
686
|
+
ctx.input.prompt();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
bodyLog(settingsRows(input).join("\n\n"), "words");
|
|
690
|
+
ctx.input.prompt();
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (trimmed === "/name" || trimmed.startsWith("/name ")) {
|
|
695
|
+
// Graphite R3d: the person names the session. The name lives in the
|
|
696
|
+
// session's sidecar (the runtime's `name` tenant) — the terminal
|
|
697
|
+
// title, the /resume row and the session list read it, the title
|
|
698
|
+
// derived from the first real line is the fallback. Control and
|
|
699
|
+
// format characters never reach it; it is cut at 60 like a title.
|
|
700
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
701
|
+
const words = trimmed.slice("/name".length).replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
702
|
+
const root = activeStoreDir;
|
|
703
|
+
if (words === "") {
|
|
704
|
+
const named = readSessionName(root, ctx.session.id);
|
|
705
|
+
const derived = sessionTitle(ctx.session.log.all.map((event) => ({ runId: "", ts: 0, event })));
|
|
706
|
+
body.notice(named !== null ? `name: ${escapeTerminal(named)} \u00b7 /name - clears it` : `unnamed \u2014 /resume lists it as "${escapeTerminal(derived)}" \u00b7 /name <words> names it`);
|
|
707
|
+
}
|
|
708
|
+
else if (words === "-") {
|
|
709
|
+
writeSessionName(root, ctx.session.id, null);
|
|
710
|
+
setTitleName(null);
|
|
711
|
+
// the tasks round: the tab says kiso since the card round, so what the
|
|
712
|
+
// first line names again is the session in /resume
|
|
713
|
+
body.notice("name cleared \u2014 /resume lists it by its first line again");
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
const name = [...words].slice(0, NAME_MAX).join("");
|
|
717
|
+
writeSessionName(root, ctx.session.id, name);
|
|
718
|
+
setTitleName(name);
|
|
719
|
+
body.notice(`named "${escapeTerminal(name)}"`);
|
|
720
|
+
}
|
|
535
721
|
ctx.input.prompt();
|
|
536
722
|
});
|
|
537
723
|
return;
|
|
@@ -543,6 +729,28 @@ export function dispatch(line, ctx) {
|
|
|
543
729
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
544
730
|
const ctxRatio = ctx.estimateCtx();
|
|
545
731
|
const ctxPct = Number.isFinite(ctxRatio) ? `~${Math.round(ctxRatio * 100)}%` : "~?";
|
|
732
|
+
// Graphite R3e: on a dock /status is a sheet over the input — the
|
|
733
|
+
// same facts, one per row, read and then typed past; off one (a
|
|
734
|
+
// pipe, -p) the lines below are what it always printed
|
|
735
|
+
if (dock.active && ctx.input.openSheet !== undefined) {
|
|
736
|
+
const w = statedContextWindow();
|
|
737
|
+
const named = readSessionName(activeStoreDir, ctx.session.id);
|
|
738
|
+
const win = ctx.contextWindow();
|
|
739
|
+
const tiers = win > 0 ? tiersFor(win, 0) : null;
|
|
740
|
+
const pal = palette();
|
|
741
|
+
const profileOf = currentProfileName === null ? "" : ` · profile ${currentProfileName}`;
|
|
742
|
+
const facts = [
|
|
743
|
+
{ label: "session", value: `${ctx.session.id}${named === null ? "" : ` · ${named}`} · ${ctx.session.log.all.length} events` },
|
|
744
|
+
{ label: "model", value: `${agentModel}${providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl))}${profileOf}` },
|
|
745
|
+
{ label: "context", value: `${ctxPct} used · ${windowSourceNote(w)}` },
|
|
746
|
+
...(tiers === null ? [] : [{ label: "compaction", value: `past ${Math.round((tiers.soft / win) * 100)}% at a phase end, past ${Math.round((tiers.hard / win) * 100)}% at once · /context for the split` }]),
|
|
747
|
+
{ label: "colour", value: `${pal.tier === null ? "off" : pal.tier === "24bit" ? "24-bit" : "256"} · ground ${currentGround()}` },
|
|
748
|
+
{ label: "version", value: versionStatusLine(installedVersion(), VERSION).replace(/^version /, "") },
|
|
749
|
+
];
|
|
750
|
+
ctx.input.openSheet((W) => infoSheetRows("status", facts, W));
|
|
751
|
+
ctx.input.prompt();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
546
754
|
bodyLog(`session ${ctx.session.id}`);
|
|
547
755
|
bodyLog(`${ctx.session.log.all.length} events`);
|
|
548
756
|
// CW-1: the percentage names its denominator and who stated it — a
|
|
@@ -580,11 +788,15 @@ export function dispatch(line, ctx) {
|
|
|
580
788
|
// a tier switch, old names included — and when the switch came
|
|
581
789
|
// with the old dontAsk tier and leaves with it, a line says so
|
|
582
790
|
const switchTier = (s) => {
|
|
791
|
+
const was = getMode();
|
|
583
792
|
const wasOn = getDontAsk();
|
|
584
793
|
applyModeSetting(s);
|
|
585
|
-
|
|
794
|
+
// Graphite R3e / the main-sync round: the MODE row, and the DON'T
|
|
795
|
+
// ASK row when the switch moved with it, on a terminal; a pipe
|
|
796
|
+
// prints these words as they are
|
|
797
|
+
body.modeNotice(`mode → ${MODE_LABEL[getMode()]}`, MODE_LABEL[was], MODE_LABEL[getMode()]);
|
|
586
798
|
if (getDontAsk() !== wasOn)
|
|
587
|
-
body.
|
|
799
|
+
body.dontAskNotice(`don't ask → ${getDontAsk() ? "on" : "off"}`, getDontAsk());
|
|
588
800
|
ctx.paintIdle();
|
|
589
801
|
};
|
|
590
802
|
if (arg === "") {
|
|
@@ -604,8 +816,16 @@ export function dispatch(line, ctx) {
|
|
|
604
816
|
// the header names the switch when it is on; the
|
|
605
817
|
// rows are the tiers alone — don't ask is not a tier
|
|
606
818
|
// and is not offered beside them (owner, 2026-09-30)
|
|
607
|
-
|
|
608
|
-
|
|
819
|
+
// Graphite P3: the current tier rides the band's name, in
|
|
820
|
+
// the owner's words of 2026-09-30 (`current: full access ·
|
|
821
|
+
// don't ask`) — the row that said it is gone
|
|
822
|
+
header: "mode",
|
|
823
|
+
facts: `current: ${modeDisplay()}${ctx.isRunning() ? " \u00b7 run paused" : ""}`,
|
|
824
|
+
enter: "\u23ce switches",
|
|
825
|
+
// the main-sync design: no `· current` suffix — the cursor
|
|
826
|
+
// opens on the current tier and the band names it; at 80
|
|
827
|
+
// columns the suffix was cut to `· c`
|
|
828
|
+
options: OFFERED_MODES.map((name) => ({ label: MODE_LABEL[name], note: MODE_NOTE[name] })),
|
|
609
829
|
// MP-1 (0.40.7): the cursor opens on the tier in force
|
|
610
830
|
...(OFFERED_MODES.indexOf(current) >= 0 ? { initial: OFFERED_MODES.indexOf(current) } : {}),
|
|
611
831
|
},
|
|
@@ -619,8 +839,7 @@ export function dispatch(line, ctx) {
|
|
|
619
839
|
}
|
|
620
840
|
const chosen = "index" in picked ? (OFFERED_MODES[picked.index] === undefined ? undefined : { mode: OFFERED_MODES[picked.index] }) : parseMode(picked.custom);
|
|
621
841
|
if (chosen === undefined) {
|
|
622
|
-
|
|
623
|
-
bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
|
|
842
|
+
body.notice(`no such mode: ${"index" in picked ? String(picked.index) : picked.custom.trim()}\ntiers: ${OFFERED_MODES.join(" ")}`);
|
|
624
843
|
}
|
|
625
844
|
else {
|
|
626
845
|
switchTier(chosen);
|
|
@@ -636,8 +855,7 @@ export function dispatch(line, ctx) {
|
|
|
636
855
|
else {
|
|
637
856
|
const typed = parseMode(arg);
|
|
638
857
|
if (typed === undefined) {
|
|
639
|
-
|
|
640
|
-
bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
|
|
858
|
+
body.notice(`no such mode: ${arg}\ntiers: ${OFFERED_MODES.join(" ")}`);
|
|
641
859
|
}
|
|
642
860
|
else {
|
|
643
861
|
switchTier(typed);
|
|
@@ -657,8 +875,12 @@ export function dispatch(line, ctx) {
|
|
|
657
875
|
bodyLog("usage: /dont-ask [on|off]");
|
|
658
876
|
}
|
|
659
877
|
else {
|
|
878
|
+
const wasOn = getDontAsk();
|
|
660
879
|
setDontAsk(want);
|
|
661
|
-
|
|
880
|
+
if (want !== wasOn)
|
|
881
|
+
body.dontAskNotice(`don't ask → ${want ? "on" : "off"}`, want);
|
|
882
|
+
else
|
|
883
|
+
body.notice(`don't ask → ${want ? "on" : "off"}`);
|
|
662
884
|
ctx.paintIdle();
|
|
663
885
|
}
|
|
664
886
|
ctx.input.prompt();
|
|
@@ -686,36 +908,43 @@ export function dispatch(line, ctx) {
|
|
|
686
908
|
let pickedLevel;
|
|
687
909
|
const picked = await new Promise((resolve) => {
|
|
688
910
|
ctx.input.panelAsk(modelPickView({
|
|
689
|
-
|
|
911
|
+
// Graphite P3 (owner, 2026-10-04): the band counts the
|
|
912
|
+
// profiles and the rows are a table — the model, its host,
|
|
913
|
+
// and a state word only where there is one (`current`, or
|
|
914
|
+
// why it cannot run). Typing FILTERS, as /resume does: the
|
|
915
|
+
// model, then the host, then the profile's name; a typed
|
|
916
|
+
// `provider/model` that no profile is becomes a row of its
|
|
917
|
+
// own — DECLARED REVERSAL of the `t` row (TUI2-R2 ④) and of
|
|
918
|
+
// the digits here (a digit is a letter of a model name).
|
|
919
|
+
header: "model",
|
|
920
|
+
noun: "profiles",
|
|
921
|
+
...(ctx.isRunning() ? { facts: "run paused" } : {}),
|
|
922
|
+
input: "filter",
|
|
923
|
+
// PH-1a (finding PH-F4): the example is a syntax
|
|
924
|
+
// directWriteProfile ACCEPTS — the fresh install sees it
|
|
925
|
+
filterHint: names.length === 0 ? "type provider/model (e.g. openai-compat/deepseek-reasoner)" : "filter, or type provider/model",
|
|
926
|
+
direct: true,
|
|
927
|
+
enter: "\u23ce switches",
|
|
690
928
|
options: names.map((name) => {
|
|
691
929
|
const profile = configModels[name];
|
|
692
|
-
|
|
693
|
-
//
|
|
694
|
-
// the
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
// the
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
// /settings, where there is room to say it and its source.
|
|
710
|
-
const marks = [...(profileAvailable(profile) ? [] : ["unavailable"]), ...(name === currentProfileName ? ["current"] : [])];
|
|
711
|
-
const host = profileProviderLabel(profile.kind, profile.baseUrl, profile.upstream);
|
|
712
|
-
return { label: `${profile.kind}/${profile.model}${host === "" ? "" : ` ${host}`}`, note: marks.join(" · "), ...effortAxis(profile) };
|
|
930
|
+
const available = profileAvailable(profile);
|
|
931
|
+
// 0.40.1 took the `profile: <key>` prefix off every row and
|
|
932
|
+
// the kind/ prefix rode the label; P3 moves both to the
|
|
933
|
+
// selected row's opened line (owner: the profile name kept
|
|
934
|
+
// there), beside what signs it in. The owner, 2026-09-21:
|
|
935
|
+
// two profiles can name ONE model id, so `current` follows
|
|
936
|
+
// the PROFILE, not the id.
|
|
937
|
+
const host = profileProviderLabel(profile.kind, profile.baseUrl, profile.upstream).replace(/^@/, "");
|
|
938
|
+
const state = name === currentProfileName ? "current" : available ? "" : profile.apiKeyEnv === undefined ? "sign in" : "no key";
|
|
939
|
+
return {
|
|
940
|
+
label: profile.model,
|
|
941
|
+
cols: [host, state],
|
|
942
|
+
opened: available ? `profile ${name} \u00b7 ${profile.kind} \u00b7 ${signInNote(profile)}` : unavailableWhy(name, profile),
|
|
943
|
+
...(available ? {} : { off: true }),
|
|
944
|
+
match: [name],
|
|
945
|
+
...effortAxis(profile),
|
|
946
|
+
};
|
|
713
947
|
}),
|
|
714
|
-
// PH-1a (finding PH-F4): the example must be a syntax
|
|
715
|
-
// directWriteProfile actually ACCEPTS — the old
|
|
716
|
-
// "openai/…" hint failed with "no such model profile"
|
|
717
|
-
// on exactly the fresh-install path that shows it.
|
|
718
|
-
typeHint: names.length === 0 ? "type provider/model directly (e.g. openai-compat/deepseek-reasoner)" : "type provider/model directly",
|
|
719
948
|
// MP-1 (0.40.7): the cursor opens on the session's own
|
|
720
949
|
// profile — opening on row 0 plus one Enter changed who pays
|
|
721
950
|
...(currentProfileName !== null && names.indexOf(currentProfileName) >= 0 ? { initial: names.indexOf(currentProfileName) } : {}),
|
|
@@ -777,7 +1006,7 @@ export function dispatch(line, ctx) {
|
|
|
777
1006
|
const direct = directWriteProfile(profName);
|
|
778
1007
|
const profile = direct ?? configModels[profName];
|
|
779
1008
|
if (profile === undefined) {
|
|
780
|
-
|
|
1009
|
+
body.notice(`no such model profile: ${profName}`);
|
|
781
1010
|
}
|
|
782
1011
|
else if (!profileAvailable(profile)) {
|
|
783
1012
|
// the PROFILE name, not the whole argument: what is
|
|
@@ -912,6 +1141,10 @@ export function dispatch(line, ctx) {
|
|
|
912
1141
|
// model summary — an OFF-LOOP call through the session's own
|
|
913
1142
|
// adapter, so it must never race a running turn: refused
|
|
914
1143
|
// mid-run, with a hint to wait for the turn to end.
|
|
1144
|
+
// Graphite §8.7 (R3b): the row says why — the person asked, or the
|
|
1145
|
+
// opt-in threshold did (`markAutoCompact`, read once here)
|
|
1146
|
+
const compactWhy = nextCompactWhy ?? "manual";
|
|
1147
|
+
nextCompactWhy = null;
|
|
915
1148
|
if (ctx.isRunning()) {
|
|
916
1149
|
body.notice("[/compact] a turn is running — wait for it to finish");
|
|
917
1150
|
return;
|
|
@@ -953,11 +1186,16 @@ export function dispatch(line, ctx) {
|
|
|
953
1186
|
const compacting = (info) => {
|
|
954
1187
|
// 0.40.0: composed by the row seam, not a template here — the row
|
|
955
1188
|
// is where the launch build's progress segment plugs in.
|
|
956
|
-
|
|
957
|
-
|
|
1189
|
+
// Graphite §8.7: compacting is a LIVE state — its row rides above
|
|
1190
|
+
// the composer, the status bar below keeps the session. Composed
|
|
1191
|
+
// against the width the row has (it used to be composed blind).
|
|
1192
|
+
const W = () => (process.stdout.columns > 0 ? process.stdout.columns : 80);
|
|
1193
|
+
const text = (elapsed) => liveRow(compactingStatus(glyph, info.rounds, info.tokens, elapsed, W() - 20, retryOnRow(), progress, compactWhy), ["esc to cancel"], W());
|
|
1194
|
+
repaintCompacting = () => dock.setLive(text(Math.round((Date.now() - compactStart) / 1000)));
|
|
958
1195
|
compactStart = Date.now();
|
|
959
1196
|
ctxBefore = ctxPercent(ctx.estimateCtx());
|
|
960
|
-
dock.
|
|
1197
|
+
dock.setLive(text(0));
|
|
1198
|
+
setTitleState("working"); // §8.10; the idle paint after it says ready again
|
|
961
1199
|
stopSpinner = startStatusSpinner((next) => {
|
|
962
1200
|
glyph = next;
|
|
963
1201
|
repaintCompacting();
|
|
@@ -1153,12 +1391,12 @@ export function dispatch(line, ctx) {
|
|
|
1153
1391
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
1154
1392
|
const route = ctx.route?.(arg) ?? null;
|
|
1155
1393
|
if (route === null || route.kind === "refused") {
|
|
1156
|
-
|
|
1394
|
+
body.notice(route?.line ?? `no such session: ${escapeTerminal(arg)} — /resume lists them`);
|
|
1157
1395
|
ctx.input.prompt();
|
|
1158
1396
|
return;
|
|
1159
1397
|
}
|
|
1160
1398
|
if (route.kind === "elsewhere" && route.line !== null)
|
|
1161
|
-
|
|
1399
|
+
body.notice(route.line);
|
|
1162
1400
|
ctx.requestSwitch(arg);
|
|
1163
1401
|
});
|
|
1164
1402
|
return;
|
|
@@ -1169,7 +1407,7 @@ export function dispatch(line, ctx) {
|
|
|
1169
1407
|
const others = ctx.sessions().filter((id) => id !== ctx.session.id);
|
|
1170
1408
|
if (others.length === 0) {
|
|
1171
1409
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
1172
|
-
|
|
1410
|
+
body.notice("no other sessions — /clear starts a fresh one");
|
|
1173
1411
|
ctx.input.prompt();
|
|
1174
1412
|
});
|
|
1175
1413
|
return;
|
|
@@ -1185,12 +1423,12 @@ export function dispatch(line, ctx) {
|
|
|
1185
1423
|
// session is refused with where to go
|
|
1186
1424
|
const route = ctx.sessions().includes(picked) ? null : (ctx.route?.(picked) ?? null);
|
|
1187
1425
|
if (route?.kind === "refused") {
|
|
1188
|
-
|
|
1426
|
+
body.notice(route.line);
|
|
1189
1427
|
ctx.input.prompt();
|
|
1190
1428
|
return;
|
|
1191
1429
|
}
|
|
1192
1430
|
if (route?.kind === "elsewhere" && route.line !== null)
|
|
1193
|
-
|
|
1431
|
+
body.notice(route.line);
|
|
1194
1432
|
ctx.requestSwitch(picked);
|
|
1195
1433
|
});
|
|
1196
1434
|
return;
|
|
@@ -1214,10 +1452,17 @@ export function dispatch(line, ctx) {
|
|
|
1214
1452
|
return;
|
|
1215
1453
|
}
|
|
1216
1454
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
1217
|
-
|
|
1455
|
+
// the sheets round: on a dock /skills is a sheet over the input
|
|
1456
|
+
// (§8.16); off one it prints the lines it always printed
|
|
1457
|
+
if (skill?.kind === "list" && dock.active && ctx.input.openSheet !== undefined) {
|
|
1458
|
+
const catalog = loadedSkillsCatalog();
|
|
1459
|
+
const userDir = tildePath(join(kisoHome(), "skills"));
|
|
1460
|
+
ctx.input.openSheet((W) => skillsSheetRows(catalog, skillSource, userDir, W));
|
|
1461
|
+
}
|
|
1462
|
+
else if (skill?.kind === "list")
|
|
1218
1463
|
bodyLog(skillsRows(loadedSkillsCatalog(), skillSource, tildePath(join(kisoHome(), "skills"))).map(escapeTerminal).join("\n"));
|
|
1219
1464
|
else if (skill?.kind === "error")
|
|
1220
|
-
|
|
1465
|
+
body.notice(escapeTerminal(skill.message));
|
|
1221
1466
|
ctx.input.prompt();
|
|
1222
1467
|
});
|
|
1223
1468
|
return;
|
|
@@ -1241,7 +1486,7 @@ export function dispatch(line, ctx) {
|
|
|
1241
1486
|
// reading when they typed it.
|
|
1242
1487
|
const word = trimmed.split(/\s+/)[0] ?? trimmed;
|
|
1243
1488
|
const known = slashCommandNames().includes(word);
|
|
1244
|
-
|
|
1489
|
+
body.notice(known
|
|
1245
1490
|
? `${escapeTerminal(word)} takes no arguments — /help says what it does`
|
|
1246
1491
|
: `unknown command: ${escapeTerminal(word)} — /help lists the commands`);
|
|
1247
1492
|
ctx.input.prompt();
|
|
@@ -1255,12 +1500,12 @@ export function dispatch(line, ctx) {
|
|
|
1255
1500
|
/** ADR-0058 (3e) — `/tasks`, `/tasks show <id>`, `/tasks stop <id>`. On a
|
|
1256
1501
|
* dock the bare command is a pick: a task, then what to do with it. A
|
|
1257
1502
|
* stop is REQUESTED here; the row says `stopping` until the journal says
|
|
1258
|
-
* how it ended.
|
|
1259
|
-
*
|
|
1503
|
+
* how it ended. A task kiso lost track of says why in `show`
|
|
1504
|
+
* (Amendment 8). */
|
|
1260
1505
|
async function tasksCommand(arg, ctx) {
|
|
1261
1506
|
const manager = tasksFor(ctx.session.id);
|
|
1262
1507
|
if (manager === undefined) {
|
|
1263
|
-
|
|
1508
|
+
body.notice("no tasks here — this session has no task manager");
|
|
1264
1509
|
return;
|
|
1265
1510
|
}
|
|
1266
1511
|
let list;
|
|
@@ -1268,23 +1513,35 @@ async function tasksCommand(arg, ctx) {
|
|
|
1268
1513
|
list = manager.list();
|
|
1269
1514
|
}
|
|
1270
1515
|
catch (err) {
|
|
1271
|
-
|
|
1516
|
+
body.notice(`tasks: ${err.message}`);
|
|
1272
1517
|
return;
|
|
1273
1518
|
}
|
|
1274
1519
|
const show = (t) => {
|
|
1520
|
+
// the tasks round (owner, 2026-10-06): on a dock a task's output is a
|
|
1521
|
+
// sheet over the input (§8.16) — the newest lines a screen can hold
|
|
1522
|
+
// beside the composer; off one, the lines it always printed
|
|
1523
|
+
if (dock.active && ctx.input.openSheet !== undefined) {
|
|
1524
|
+
const lines = taskOutput(t, 12);
|
|
1525
|
+
ctx.input.openSheet((W) => taskOutputSheetRows(t, lines, W));
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1275
1528
|
const lines = taskOutput(t);
|
|
1529
|
+
// Amendment 8: a lost task says why, before its output
|
|
1530
|
+
const why = lostReason(t);
|
|
1531
|
+
if (why !== undefined)
|
|
1532
|
+
bodyLog(`${t.id} — lost track: ${why}; it may still be running`);
|
|
1276
1533
|
bodyLog(`${t.id} — ${t.agent !== undefined ? "its answer" : "its last output"}${lines.length === 0 ? ": nothing yet" : ""}`);
|
|
1277
1534
|
for (const line of lines)
|
|
1278
1535
|
bodyLog(` ${line}`);
|
|
1279
1536
|
};
|
|
1280
1537
|
const stop = (t) => {
|
|
1281
|
-
|
|
1538
|
+
body.notice(manager.stop(t.id, "person") ? `${t.id}: stop requested` : `${t.id}: nothing to stop (${taskStateLabel(t)})`);
|
|
1282
1539
|
};
|
|
1283
1540
|
const [verb, id] = arg.split(/\s+/);
|
|
1284
1541
|
if (verb === "show" || verb === "stop") {
|
|
1285
1542
|
const t = list.find((x) => x.id === id);
|
|
1286
1543
|
if (t === undefined)
|
|
1287
|
-
|
|
1544
|
+
body.notice(`no task ${id ?? ""} in this session`);
|
|
1288
1545
|
else if (verb === "show")
|
|
1289
1546
|
show(t);
|
|
1290
1547
|
else
|
|
@@ -1292,16 +1549,13 @@ async function tasksCommand(arg, ctx) {
|
|
|
1292
1549
|
return;
|
|
1293
1550
|
}
|
|
1294
1551
|
if (arg !== "") {
|
|
1295
|
-
|
|
1552
|
+
body.notice("usage: /tasks · /tasks show <id> · /tasks stop <id>");
|
|
1296
1553
|
return;
|
|
1297
1554
|
}
|
|
1298
1555
|
if (list.length === 0) {
|
|
1299
|
-
|
|
1556
|
+
body.notice("no tasks in this session");
|
|
1300
1557
|
return;
|
|
1301
1558
|
}
|
|
1302
|
-
for (const t of list)
|
|
1303
|
-
if (t.state.kind === "unknown")
|
|
1304
|
-
seenUnknownTasks.add(t.id);
|
|
1305
1559
|
if (!dock.active) {
|
|
1306
1560
|
for (const t of list) {
|
|
1307
1561
|
const r = taskRow(t);
|