@vincemakes/kiso-code 0.45.3 → 0.46.1

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,7 +7,7 @@
7
7
  * Moved verbatim from index.ts's makeAgent (R1, 2026-09-23) and renamed;
8
8
  * the body is not edited beyond the rename and activeStoreDir's setter.
9
9
  */
10
- import { activeStoreDir, setActiveStoreDir, body, bodyLog, codingToolOptions, extensionsDir, floorOn, kisoHome, loadedExtensions, ownSessionsDir, projectRoot, protectedFiles, secretEnvNamesOf, sessionsDir, setAgentModel, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setFloorOn, setMergedConfig, setModelChoice, setNeverInherited, setRetryShown, setSessionStore, setUserProtectedPaths, settingsLayers, workspaceRoot } from "./state.js";
10
+ import { activeStoreDir, childTurnBudget, setActiveStoreDir, body, bodyLog, codingToolOptions, extensionsDir, floorOn, kisoHome, loadedExtensions, ownSessionsDir, projectRoot, protectedFiles, secretEnvNamesOf, sessionsDir, setAgentModel, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setFloorOn, setMergedConfig, setModelChoice, setNeverInherited, setRetryShown, setSessionStore, setUserProtectedPaths, settingsLayers, workspaceRoot } from "./state.js";
11
11
  import { claimProjectDir, projectLayoutActive } from "./projects.js";
12
12
  import { migrationNotice, pendingLegacyIds, planMigration, runMigration } from "./session-migration.js";
13
13
  import { askUi, resolveProjectTrust } from "./trust-ui.js";
@@ -34,6 +34,7 @@ import { createFauxProvider } from "@vincemakes/kiso-evals";
34
34
  import { sweepStaleMergeDirs } from "./temp-sweep.js";
35
35
  import { CODING_TOOL_RULES, composeSystemPrompt } from "./coding-prompt.js";
36
36
  import { createAgent, loadExtensions, loadProjectExtensions, SessionStore } from "@vincemakes/kiso-runtime";
37
+ import { preferences, startingEffort } from "./preferences.js";
37
38
  /** LT-1: the stream watchdog's bound from the environment — a non-negative
38
39
  * number of milliseconds (0 disables it); anything else is ignored. The
39
40
  * PTY rigs use it to trip the watchdog in seconds against a stub that
@@ -265,6 +266,15 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
265
266
  // config profile — a direct provider/model or an env key names none.
266
267
  workspace: workspaceRoot(),
267
268
  ...(resolved !== null && merged.models?.[resolved.name] === resolved.profile ? { profileName: resolved.name } : {}),
269
+ // B1: a NEW session of a config profile starts at the effort last
270
+ // picked for it (the picker's fallback applied); the runtime reads it
271
+ // for a new session only — a resumed one keeps its own
272
+ ...(() => {
273
+ if (resolved === null || merged.models?.[resolved.name] !== resolved.profile)
274
+ return {};
275
+ const effort = startingEffort(resolved.profile, preferences().effort?.[resolved.name]);
276
+ return effort === null ? {} : { reasoning: { thinking: "default", effort } };
277
+ })(),
268
278
  // Area 5: the coding tools are bound to the workspace — every path
269
279
  // they touch is canonicalized inside cwd, escapes are refused.
270
280
  tools: [...createCodingTools(codingToolOptions())], // DC-49 — the options live in state.ts, shared with the `!` command's runner
@@ -314,6 +324,8 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
314
324
  // Modes: the five tiers join at the CHAIN HEAD, before the user/
315
325
  // project extensions (the deny>allow>ask composition keeps a user
316
326
  // deny winning over any mode tier — bypass included).
327
+ // ADR-0058 3d (D6): a background child's budget, its only bound.
328
+ ...(childTurnBudget !== undefined ? { maxTurns: childTurnBudget } : {}),
317
329
  extensions,
318
330
  ...(resolved !== null
319
331
  ? {
@@ -39,6 +39,8 @@ export interface DispatchCtx {
39
39
  readonly requestSwitch: (id: string) => void;
40
40
  /** DC-57: true once this entry has been asked to leave — the guard that keeps a departing session from answering a batch's later lines. */
41
41
  readonly leaving: () => boolean;
42
+ /** ADR-0058 (3e): with live tasks, ask before exiting; true = exit. */
43
+ readonly confirmExit: () => Promise<boolean>;
42
44
  /** §2.5: end this chat() with a REBUILD of the agent on the SAME
43
45
  * session — extensions, skills and config are read again. */
44
46
  readonly requestReload: () => void;
package/dist/dispatch.js CHANGED
@@ -3,21 +3,22 @@
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, compactingStatus, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
6
+ import { STATUS_GLYPHS, contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, namedPickView, compactingStatus, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
7
7
  import { newSessionId } from "./session-id.js";
8
8
  import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
9
- import { MODES, MODE_NOTE, OFFERED_MODES, getMode, setMode } from "./mode.js";
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
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";
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
- import { preferences, setPreference } from "./preferences.js";
16
+ import { preferences, rememberEffort, setPreference } from "./preferences.js";
17
17
  import { 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 } from "./state.js";
20
+ import { queuedSwitchLines, seenUnknownTasks, tasksFor } from "./state.js";
21
+ import { taskOutput, taskRow, taskStateLabel } from "./task-notice.js";
21
22
  import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
22
23
  import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
23
24
  import { shellTool } from "@vincemakes/kiso-tools-node";
@@ -97,7 +98,7 @@ function effortAxis(p) {
97
98
  // the sentence the coordination note asked for: said only when the
98
99
  // cursor could NOT land where the previous selection asked.
99
100
  ...(carried !== undefined && carried !== "default" && carriedIdx < 0 && defaultIdx >= 0
100
- ? { levelNote: `effort ${carried} \u2192 ${levels[defaultIdx]}: the nearest this model supports` }
101
+ ? { levelNote: `effort ${carried} \u2192 ${levels[defaultIdx]}: this model's default (it has no ${carried})` }
101
102
  : {}),
102
103
  };
103
104
  }
@@ -396,6 +397,16 @@ export function dispatch(line, ctx) {
396
397
  });
397
398
  return;
398
399
  }
400
+ if (trimmed === "\x02background") {
401
+ // ADR-0058 (3e) — ctrl+b: the running foreground command(s) to the
402
+ // background, through the session's TaskManager (§6), at once — a
403
+ // steer waits for a command to be 2 s old (D1), the key never does.
404
+ // Off the chain on purpose: it is pressed DURING a run.
405
+ const moved = tasksFor(ctx.session.id)?.detachAll("person") ?? [];
406
+ if (moved.length === 0)
407
+ body.notice("nothing to move to the background");
408
+ return;
409
+ }
399
410
  if (trimmed === "\x14think") {
400
411
  // §2.3 / 0.40.6 — ctrl+t hides thinking (one italic line per block,
401
412
  // the open one included) and shows it again. DC-50's mechanism: one
@@ -510,7 +521,9 @@ export function dispatch(line, ctx) {
510
521
  env: process.env,
511
522
  ...(settingsLayers.modeFlag !== undefined ? { modeFlag: settingsLayers.modeFlag } : {}),
512
523
  ...(settingsLayers.modelFlag !== undefined ? { modelFlag: settingsLayers.modelFlag } : {}),
524
+ dontAskFlag: settingsLayers.dontAskFlag,
513
525
  mode: getMode(),
526
+ dontAsk: getDontAsk(),
514
527
  model: { label: `${agentModel}${providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl))}${profile === null ? "" : ` · profile ${profile}`}`, profile, switched: settingsLayers.modelSwitched },
515
528
  ground: currentGround(),
516
529
  floorOn,
@@ -563,59 +576,89 @@ export function dispatch(line, ctx) {
563
576
  // /mode <name> switches — the notice cell leaves the audit
564
577
  // line in the body, the status bar repaints at once.
565
578
  ctx.chainRef.current = ctx.chainRef.current.then(async () => {
566
- const m = MODES.find((x) => x === trimmed.slice(5).trim());
567
- if (trimmed.slice(5).trim() === "") {
579
+ const arg = trimmed.slice(5).trim();
580
+ // a tier switch, old names included — and when the switch came
581
+ // with the old dontAsk tier and leaves with it, a line says so
582
+ const switchTier = (s) => {
583
+ const wasOn = getDontAsk();
584
+ applyModeSetting(s);
585
+ body.notice(`mode → ${MODE_LABEL[getMode()]}`);
586
+ if (getDontAsk() !== wasOn)
587
+ body.notice(`don't ask → ${getDontAsk() ? "on" : "off"}`);
588
+ ctx.paintIdle();
589
+ };
590
+ if (arg === "") {
568
591
  // DC-36 — BARE /mode PICKS, the way bare /model has since
569
- // TUI2-R2 ④. The five tiers are a CLOSED set, which makes
570
- // this the least defensible place in the product to have
571
- // asked a human to type the answer: everything needed to
572
- // make it a choice was on screen and only the choosing was
573
- // missing.
592
+ // TUI2-R2 ④. The tiers are a CLOSED set, which makes this the
593
+ // least defensible place in the product to have asked a human
594
+ // to type the answer: everything needed to make it a choice
595
+ // was on screen and only the choosing was missing.
574
596
  //
575
- // The printed form survives VERBATIM wherever there is no
576
- // panel to draw — a pipe, a dock-less TTY — because that is
577
- // a machine-readable surface and this round moves no bytes
578
- // on one. `/mode <name>` is untouched.
597
+ // The printed form survives wherever there is no panel to
598
+ // draw — a pipe, a dock-less TTY — because that is a
599
+ // machine-readable surface. `/mode <name>` is untouched.
579
600
  if (dock.active && ctx.input.panelAsk !== undefined) {
580
601
  const current = getMode();
581
602
  const picked = await new Promise((resolve) => {
582
603
  ctx.input.panelAsk(modePickView({
583
- header: `mode — current: ${current}`,
584
- options: OFFERED_MODES.map((name) => ({ label: name, note: [MODE_NOTE[name], ...(name === current ? ["current"] : [])].join(" · ") })),
604
+ // the header names the switch when it is on; the
605
+ // rows are the tiers alone — don't ask is not a tier
606
+ // and is not offered beside them (owner, 2026-09-30)
607
+ header: `mode — current: ${modeDisplay()}`,
608
+ options: OFFERED_MODES.map((name) => ({ label: MODE_LABEL[name], note: [MODE_NOTE[name], ...(name === current ? ["current"] : [])].join(" · ") })),
585
609
  // MP-1 (0.40.7): the cursor opens on the tier in force
586
610
  ...(OFFERED_MODES.indexOf(current) >= 0 ? { initial: OFFERED_MODES.indexOf(current) } : {}),
587
611
  },
588
612
  // DC-12 (design §4): a panel WAITING ON A HUMAN says ❯.
589
- ctx.isRunning() ? "❯ run paused" : `▸ ${current}`), (v) => resolve(v.action === "picked" ? v.result : null));
613
+ ctx.isRunning() ? "❯ run paused" : `▸ ${modeDisplay()}`), (v) => resolve(v.action === "picked" ? v.result : null));
590
614
  });
591
615
  ctx.paintIdle();
592
616
  if (picked === null) {
593
617
  ctx.input.prompt();
594
618
  return; // esc — nothing switched, nothing said
595
619
  }
596
- const chosen = "index" in picked ? OFFERED_MODES[picked.index] : MODES.find((x) => x === picked.custom.trim());
620
+ const chosen = "index" in picked ? (OFFERED_MODES[picked.index] === undefined ? undefined : { mode: OFFERED_MODES[picked.index] }) : parseMode(picked.custom);
597
621
  if (chosen === undefined) {
598
622
  bodyLog(`no such mode: ${"index" in picked ? String(picked.index) : picked.custom.trim()}`);
599
623
  bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
600
624
  }
601
625
  else {
602
- setMode(chosen);
603
- body.notice(`mode → ${chosen}`);
604
- ctx.paintIdle();
626
+ switchTier(chosen);
605
627
  }
606
628
  ctx.input.prompt();
607
629
  return;
608
630
  }
609
631
  bodyLog(`mode ${getMode()}`);
632
+ if (getDontAsk())
633
+ bodyLog("don't ask on");
610
634
  bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
611
635
  }
612
- else if (m === undefined) {
613
- bodyLog(`no such mode: ${trimmed.slice(5).trim()}`);
614
- bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
636
+ else {
637
+ const typed = parseMode(arg);
638
+ if (typed === undefined) {
639
+ bodyLog(`no such mode: ${arg}`);
640
+ bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
641
+ }
642
+ else {
643
+ switchTier(typed);
644
+ }
645
+ }
646
+ ctx.input.prompt();
647
+ });
648
+ return;
649
+ }
650
+ if (trimmed === "/dont-ask" || trimmed.startsWith("/dont-ask ")) {
651
+ // The don't-ask switch on its own — the tier stays. Bare flips it;
652
+ // `on` / `off` set it. A switch set here outlives tier changes.
653
+ ctx.chainRef.current = ctx.chainRef.current.then(async () => {
654
+ const arg = trimmed.slice(9).trim();
655
+ const want = arg === "" ? !getDontAsk() : arg === "on" ? true : arg === "off" ? false : undefined;
656
+ if (want === undefined) {
657
+ bodyLog("usage: /dont-ask [on|off]");
615
658
  }
616
659
  else {
617
- setMode(m);
618
- body.notice(`mode → ${m}`);
660
+ setDontAsk(want);
661
+ body.notice(`don't ask → ${want ? "on" : "off"}`);
619
662
  ctx.paintIdle();
620
663
  }
621
664
  ctx.input.prompt();
@@ -684,7 +727,7 @@ export function dispatch(line, ctx) {
684
727
  // DC-12 (design §4): a panel WAITING ON A HUMAN says ❯. The
685
728
  // else-branch keeps ▸ — it names the current tier, which is
686
729
  // what ▸ means everywhere else.
687
- ctx.isRunning() ? "❯ run paused" : `▸ ${getMode()}`), (v) => {
730
+ ctx.isRunning() ? "❯ run paused" : `▸ ${modeDisplay()}`), (v) => {
688
731
  // OR-7: the level rides beside the result, so a pick
689
732
  // with no second axis is byte-for-byte the old one.
690
733
  if (v.action === "picked")
@@ -824,6 +867,11 @@ export function dispatch(line, ctx) {
824
867
  // §2.5: the ONE source a reload reads for the model — a
825
868
  // switch made here must survive the rebuild.
826
869
  setModelChoice(arg);
870
+ // B1: an explicit effort (typed, or the panel's level) is
871
+ // remembered for the profile — where its next NEW session
872
+ // starts (preferences.json)
873
+ if (direct === null && effortTok !== undefined && reasoning !== undefined)
874
+ rememberEffort(profName, effortTok);
827
875
  // The window a PROFILE states travels with the switch,
828
876
  // like the model id and the endpoint. Leaving it behind
829
877
  // is CTX-1 one field over: the row would show a
@@ -1034,15 +1082,31 @@ export function dispatch(line, ctx) {
1034
1082
  // streams), a pipe's "exit" always arrives mid-run, and the
1035
1083
  // approval panel the fix protects only exists on the dock. An idle
1036
1084
  // exit is immediate, byte-for-byte as before, on every surface.
1085
+ // 3e: with live tasks the dock asks first; staying re-prompts
1086
+ const exitAfterAsking = async () => {
1087
+ if (await ctx.confirmExit())
1088
+ ctx.input.close();
1089
+ else
1090
+ ctx.input.prompt();
1091
+ };
1037
1092
  if (ctx.isRunning() && dock.active) {
1038
1093
  if (trimmed === "exit")
1039
1094
  body.notice("[exit queued — closing after the current run completes]");
1040
- ctx.chainRef.current = ctx.chainRef.current.then(async () => {
1041
- ctx.input.close();
1042
- });
1095
+ ctx.chainRef.current = ctx.chainRef.current.then(exitAfterAsking);
1043
1096
  return;
1044
1097
  }
1045
- ctx.input.close();
1098
+ void exitAfterAsking();
1099
+ return;
1100
+ }
1101
+ if (trimmed === "/tasks" || trimmed.startsWith("/tasks ")) {
1102
+ // ADR-0058 (3e): the session's tasks, for the PERSON. Everything here
1103
+ // is drawn on the screen and nowhere else — no user_input, no event
1104
+ // in the session log, nothing the model sees, no wake.
1105
+ ctx.chainRef.current = ctx.chainRef.current.then(async () => {
1106
+ await tasksCommand(trimmed.slice("/tasks".length).trim(), ctx);
1107
+ ctx.paintIdle();
1108
+ ctx.input.prompt();
1109
+ });
1046
1110
  return;
1047
1111
  }
1048
1112
  if (trimmed === "/clear") {
@@ -1188,3 +1252,76 @@ export function dispatch(line, ctx) {
1188
1252
  // live count rides the status bar (+N queued).
1189
1253
  ctx.submitTurn(line);
1190
1254
  }
1255
+ /** ADR-0058 (3e) — `/tasks`, `/tasks show <id>`, `/tasks stop <id>`. On a
1256
+ * dock the bare command is a pick: a task, then what to do with it. A
1257
+ * stop is REQUESTED here; the row says `stopping` until the journal says
1258
+ * how it ended. Looking at the list counts as having seen its unknown
1259
+ * tasks (the status row stops counting them). */
1260
+ async function tasksCommand(arg, ctx) {
1261
+ const manager = tasksFor(ctx.session.id);
1262
+ if (manager === undefined) {
1263
+ bodyLog("no tasks here — this session has no task manager");
1264
+ return;
1265
+ }
1266
+ let list;
1267
+ try {
1268
+ list = manager.list();
1269
+ }
1270
+ catch (err) {
1271
+ bodyLog(`tasks: ${err.message}`);
1272
+ return;
1273
+ }
1274
+ const show = (t) => {
1275
+ const lines = taskOutput(t);
1276
+ bodyLog(`${t.id} — ${t.agent !== undefined ? "its answer" : "its last output"}${lines.length === 0 ? ": nothing yet" : ""}`);
1277
+ for (const line of lines)
1278
+ bodyLog(` ${line}`);
1279
+ };
1280
+ const stop = (t) => {
1281
+ bodyLog(manager.stop(t.id, "person") ? `${t.id}: stop requested` : `${t.id}: nothing to stop (${taskStateLabel(t)})`);
1282
+ };
1283
+ const [verb, id] = arg.split(/\s+/);
1284
+ if (verb === "show" || verb === "stop") {
1285
+ const t = list.find((x) => x.id === id);
1286
+ if (t === undefined)
1287
+ bodyLog(`no task ${id ?? ""} in this session`);
1288
+ else if (verb === "show")
1289
+ show(t);
1290
+ else
1291
+ stop(t);
1292
+ return;
1293
+ }
1294
+ if (arg !== "") {
1295
+ bodyLog("usage: /tasks · /tasks show <id> · /tasks stop <id>");
1296
+ return;
1297
+ }
1298
+ if (list.length === 0) {
1299
+ bodyLog("no tasks in this session");
1300
+ return;
1301
+ }
1302
+ for (const t of list)
1303
+ if (t.state.kind === "unknown")
1304
+ seenUnknownTasks.add(t.id);
1305
+ if (!dock.active) {
1306
+ for (const t of list) {
1307
+ const r = taskRow(t);
1308
+ bodyLog(`${r.label} ${r.note}`);
1309
+ }
1310
+ return;
1311
+ }
1312
+ const ask = (header, options) => new Promise((resolve) => {
1313
+ ctx.input.panelAsk(namedPickView("tasks", { header, options }, ctx.isRunning() ? "❯ run paused" : "▸ tasks", "which task? (id) "), (v) => resolve(v.action === "picked" && "index" in v.result ? v.result.index : null));
1314
+ });
1315
+ const shown = list.slice(-20); // the newest twenty; /tasks show <id> reaches any
1316
+ const which = await ask(`tasks — ${list.length} in this session`, shown.map((t) => taskRow(t)));
1317
+ const task = which === null ? undefined : shown[which];
1318
+ if (task === undefined)
1319
+ return;
1320
+ const live = task.state.kind === "running" || task.state.kind === "starting";
1321
+ const actions = [task.agent !== undefined ? "show its answer" : "show its output", ...(live ? ["stop it"] : [])];
1322
+ const action = await ask(`${task.id} · ${taskStateLabel(task)} · ${taskRow(task).note}`, actions.map((label) => ({ label })));
1323
+ if (action === 0)
1324
+ show(task);
1325
+ else if (action === 1)
1326
+ stop(task);
1327
+ }
@@ -14,8 +14,10 @@ export interface EmptyScan {
14
14
  export declare function pidAlive(pid: number): boolean;
15
15
  /** Every session in `dirs` with a sidecar and no log. Reads only. */
16
16
  export declare function findEmptySessions(dirs: readonly string[], alive?: (pid: number) => boolean): EmptyScan;
17
- /** The platform's Trash: ~/.Trash on macOS, the freedesktop one elsewhere. */
18
- export declare function defaultTrashRoot(): string;
17
+ /** The platform's Trash: ~/.Trash on macOS, kiso's own folder on Windows
18
+ * (its Recycle Bin is not a folder a program moves files into), the
19
+ * freedesktop one elsewhere. */
20
+ export declare function defaultTrashRoot(kisoHome: string): string;
19
21
  /** Move the sessions into one new folder under `trashRoot`, keeping each
20
22
  * session folder's name; returns that folder. Never deletes: a file that
21
23
  * cannot be renamed across devices is copied, then unlinked. */
@@ -72,10 +72,14 @@ export function findEmptySessions(dirs, alive = pidAlive) {
72
72
  }
73
73
  return { empty, inUse };
74
74
  }
75
- /** The platform's Trash: ~/.Trash on macOS, the freedesktop one elsewhere. */
76
- export function defaultTrashRoot() {
75
+ /** The platform's Trash: ~/.Trash on macOS, kiso's own folder on Windows
76
+ * (its Recycle Bin is not a folder a program moves files into), the
77
+ * freedesktop one elsewhere. */
78
+ export function defaultTrashRoot(kisoHome) {
77
79
  if (process.platform === "darwin")
78
80
  return join(homedir(), ".Trash");
81
+ if (process.platform === "win32")
82
+ return join(kisoHome, "trash");
79
83
  return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "Trash", "files");
80
84
  }
81
85
  /** Move the sessions into one new folder under `trashRoot`, keeping each
@@ -23,6 +23,7 @@
23
23
  import { spawnSync } from "node:child_process";
24
24
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
25
25
  import { join } from "node:path";
26
+ import { launchCommand } from "./launch.js";
26
27
  /**
27
28
  * Run `bin` on `text` and return what came back, or `null` when the edit
28
29
  * failed — in which case `onFailure` has already been told, in one line,
@@ -43,7 +44,9 @@ export function runExternalEditor(bin, root, text, onFailure) {
43
44
  const file = join(dir, "message.md"); // .md so the editor lights it up
44
45
  try {
45
46
  writeFileSync(file, text, { encoding: "utf8", mode: 0o600 });
46
- const r = spawnSync(bin, [file], { stdio: "inherit", shell: false });
47
+ // Windows: an editor is often a .cmd shim (code.cmd) — through cmd.exe
48
+ const l = launchCommand(bin, [file]);
49
+ const r = spawnSync(l.file, [...l.args], { stdio: "inherit", shell: false, ...(l.windowsVerbatimArguments === true ? { windowsVerbatimArguments: true } : {}) });
47
50
  if (r.error !== undefined || (r.status !== null && r.status !== 0)) {
48
51
  onFailure(`[ctrl+g] ${bin} exited ${r.status ?? "abnormally"} — the composer is unchanged`);
49
52
  return null;