@xingular/harness 1.2.0 → 1.3.1-unstable.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +10 -5
  3. package/dist/cli.js +5560 -5056
  4. package/dist/sdk/agents/builtin.js +3 -3
  5. package/dist/sdk/agents/planResearch.d.ts +23 -1
  6. package/dist/sdk/agents/planResearch.js +71 -8
  7. package/dist/sdk/agents/profiles.d.ts +4 -1
  8. package/dist/sdk/agents/profiles.js +17 -6
  9. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  10. package/dist/sdk/commands/registry.d.ts +11 -3
  11. package/dist/sdk/commands/registry.js +3 -2
  12. package/dist/sdk/core/shell.d.ts +20 -0
  13. package/dist/sdk/core/shell.js +73 -0
  14. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  15. package/dist/sdk/core/workspaceBashTool.js +16 -2
  16. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  17. package/dist/sdk/core/workspaceSandbox.js +52 -12
  18. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  19. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  20. package/dist/sdk/doctor/checks.js +71 -12
  21. package/dist/sdk/hub/hub.js +5 -3
  22. package/dist/sdk/index.d.ts +2 -2
  23. package/dist/sdk/index.js +1 -1
  24. package/dist/sdk/plugins/builtin.js +3 -3
  25. package/dist/sdk/plugins/install.d.ts +6 -2
  26. package/dist/sdk/plugins/install.js +29 -5
  27. package/dist/sdk/plugins/inventory.js +2 -2
  28. package/dist/sdk/prompt/compile.d.ts +2 -2
  29. package/dist/sdk/prompt/compile.js +14 -16
  30. package/dist/sdk/server/methods.js +4 -2
  31. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  32. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  33. package/dist/sdk/sessions/continuation.d.ts +4 -2
  34. package/dist/sdk/sessions/continuation.js +7 -6
  35. package/dist/sdk/sessions/contracts.d.ts +22 -4
  36. package/dist/sdk/sessions/contracts.js +12 -7
  37. package/dist/sdk/sessions/planMode.js +2 -2
  38. package/dist/sdk/sessions/runStore.js +3 -1
  39. package/dist/sdk/sessions/runner.js +2 -2
  40. package/dist/sdk/sessions/sessions.js +29 -11
  41. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  42. package/dist/sdk/sessions/subagentService.js +194 -54
  43. package/dist/sdk/sessions/subagents.d.ts +10 -1
  44. package/dist/sdk/sessions/subagents.js +4 -0
  45. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  46. package/dist/sdk/sessions/workSlice.js +77 -45
  47. package/dist/sdk/terminal/controller.d.ts +20 -0
  48. package/dist/sdk/terminal/controller.js +43 -18
  49. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  50. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  51. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  52. package/dist/sdk/terminal/opentui/model.js +482 -0
  53. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  54. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  55. package/dist/sdk/terminal/opentui/theme.js +1 -0
  56. package/dist/sdk/terminal/piExtension.js +28 -14
  57. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  58. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  59. package/dist/sdk/terminal/terminals.d.ts +7 -0
  60. package/dist/sdk/terminal/terminals.js +86 -14
  61. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  62. package/dist/sdk/terminal/trajectory.js +8 -0
  63. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  64. package/dist/sdk/terminal/tui/chrome.js +13 -0
  65. package/dist/sdk/terminal/tui/commandPages.js +14 -4
  66. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  67. package/dist/sdk/terminal/tui/commands.js +8 -0
  68. package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
  69. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  70. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  71. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  72. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
  73. package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
  74. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  75. package/dist/sdk/tools/sessionName.d.ts +2 -0
  76. package/dist/sdk/tools/sessionName.js +5 -3
  77. package/dist/sdk/tools/spawnSubagent.js +9 -7
  78. package/dist/sdk/tools/todo.d.ts +15 -0
  79. package/dist/sdk/tools/todo.js +64 -0
  80. package/dist/terminal-opentui.js +1416 -348
  81. package/dist/terminal-pi.js +315 -126
  82. package/docs/architecture/00-overview.md +1 -1
  83. package/docs/architecture/01-extension-points.md +1 -1
  84. package/docs/architecture/02-sessions-and-runs.md +65 -34
  85. package/docs/architecture/04-prompt.md +8 -6
  86. package/docs/architecture/06-plugins-and-capabilities.md +10 -6
  87. package/docs/architecture/07-tools-and-gate.md +31 -5
  88. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  89. package/docs/architecture/10-server-protocol.md +16 -9
  90. package/docs/architecture/11-terminal.md +51 -26
  91. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  92. package/docs/architecture/README.md +18 -17
  93. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  94. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  95. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  96. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  97. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  98. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  99. package/docs/architecture/proposals/README.md +10 -6
  100. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  101. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  102. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  103. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  104. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  105. package/docs/doctor.md +14 -1
  106. package/docs/parity.md +9 -9
  107. package/docs/releases/1.3.0.md +21 -0
  108. package/docs/releases/README.md +2 -2
  109. package/docs/terminal-tui.md +85 -46
  110. package/package.json +1 -1
  111. package/dist/sdk/tools/checklist.d.ts +0 -9
  112. package/dist/sdk/tools/checklist.js +0 -55
@@ -12,7 +12,7 @@ import { execFile as execFile2, spawn as spawn4 } from "child_process";
12
12
  import { existsSync as existsSync11 } from "fs";
13
13
  import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
14
14
  import { tmpdir as tmpdir3 } from "os";
15
- import { join as join20 } from "path";
15
+ import { join as join21 } from "path";
16
16
  import { randomUUID as randomUUID4 } from "crypto";
17
17
  import { promisify as promisify2 } from "util";
18
18
  import {
@@ -20,7 +20,7 @@ import {
20
20
  initTheme,
21
21
  resizeImage
22
22
  } from "@earendil-works/pi-coding-agent";
23
- import { matchesKey as matchesKey16 } from "@earendil-works/pi-tui";
23
+ import { matchesKey as matchesKey18 } from "@earendil-works/pi-tui";
24
24
 
25
25
  // src/terminal/client.ts
26
26
  import { createConnection } from "net";
@@ -334,9 +334,20 @@ function pageFrame({ title, meta, body, footer, width, rows, theme, headerLine }
334
334
  footerRow(footer, width, theme)
335
335
  ].slice(0, rows);
336
336
  }
337
+ function inspectorListWidth(width) {
338
+ return width < 88 ? undefined : Math.max(32, Math.floor((width - 3) * 0.4));
339
+ }
340
+ function splitColumns(left, right, leftWidth, width, rows, theme) {
341
+ const rightWidth = Math.max(0, width - leftWidth - 3);
342
+ return Array.from({ length: Math.max(0, Math.trunc(rows)) }, (_, index) => {
343
+ const leftLine = truncateToWidth(left[index] ?? "", leftWidth, "");
344
+ const rightLine = truncateToWidth(right[index] ?? "", rightWidth, "");
345
+ return leftLine + " ".repeat(Math.max(0, leftWidth - visibleWidth(leftLine))) + theme.fg("dim", " \u2502 ") + rightLine;
346
+ });
347
+ }
337
348
 
338
349
  // src/terminal/tui/commands.ts
339
- import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
350
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve } from "path";
340
351
  import { SessionManager } from "@earendil-works/pi-coding-agent";
341
352
 
342
353
  // src/commands/registry.ts
@@ -357,6 +368,7 @@ var CORE_COMMANDS = [
357
368
  { name: "compact", args: "[instructions]", description: "Compact the context", group: "Session", kind: "session", methods: ["sessions.run"] },
358
369
  { name: "copy", description: "Copy the last reply", group: "Inspect", kind: "local", methods: ["sessions.lastReply"] },
359
370
  { name: "export", args: "[path.html|path.jsonl]", description: "Export this branch to HTML or JSONL", group: "Inspect", kind: "session", methods: ["sessions.export", "sessions.run"] },
371
+ { name: "todo", aliases: ["checklist"], description: "Show the current todo list", group: "Inspect", kind: "session", methods: ["runs.todo"] },
360
372
  { name: "trajectory", args: "[run-id]", description: "Inspect recorded runs for this session", group: "Inspect", kind: "page", methods: ["runs.list", "runs.events"] },
361
373
  { name: "reload", description: "Reload trusted resources and keybindings", group: "Harness", kind: "session", methods: ["sessions.reload"] },
362
374
  { name: "accounts", description: "Sign in, switch, check and remove Pi accounts", group: "Harness", kind: "page", methods: ["accounts.list"] },
@@ -366,8 +378,8 @@ var CORE_COMMANDS = [
366
378
  { name: "connections", description: "Manage local and remote MCP connections", group: "Harness", kind: "page" },
367
379
  { name: "mcp", description: "Open Connections in the Harness browser", group: "Harness", kind: "page" },
368
380
  { name: "tools", description: "Inspect available tools, owners and approvals", group: "Harness", kind: "page" },
369
- { name: "plugins", description: "Switch plugins on or off and trust their code", group: "Harness", kind: "page", methods: ["plugins.inventory"] },
370
- { name: "hub", description: "Browse and install reviewed skills, plugins and documents", group: "Harness", kind: "page" },
381
+ { name: "plugins", description: "Manage local plugins, packages, trust and settings", group: "Harness", kind: "page", methods: ["plugins.list", "plugins.attach", "plugins.detach", "plugins.trust", "plugins.untrust", "plugins.configure", "plugins.install", "plugins.update", "plugins.remove", "hub.list"] },
382
+ { name: "hub", description: "Browse the Community catalog and manage Hub installs", group: "Harness", kind: "page", methods: ["hub.list", "hub.install", "hub.uninstall"] },
371
383
  { name: "components", description: "Browse skills, prompts and rules for this Harness scope", group: "Harness", kind: "page" },
372
384
  { name: "settings", description: "Session, terminal and browser settings", group: "Harness", kind: "page" },
373
385
  { name: "doctor", description: "Check account, model, tools and runtime readiness", group: "Harness", kind: "page", methods: ["doctor.start", "doctor.read"] },
@@ -922,6 +934,7 @@ var runEventTypeSchema = z.enum([
922
934
  "extension.notice",
923
935
  "reaction.added",
924
936
  "agent.message",
937
+ "todo.updated",
925
938
  "checklist.updated",
926
939
  "checkpoint",
927
940
  "heartbeat",
@@ -1060,9 +1073,9 @@ var extensionNoticePayload = z.object({
1060
1073
  text: z.string(),
1061
1074
  extension: z.string().optional()
1062
1075
  });
1063
- var checklistItemSchema = z.object({ text: z.string().min(1).max(500), status: z.enum(["pending", "in_progress", "completed"]) }).strict();
1064
- var MAX_CHECKLIST_ITEMS = 50;
1065
- var checklistPayload = z.object({ items: z.array(checklistItemSchema).max(MAX_CHECKLIST_ITEMS) }).strict();
1076
+ var todoItemSchema = z.object({ text: z.string().min(1).max(500), status: z.enum(["pending", "in_progress", "completed"]) }).strict();
1077
+ var MAX_TODO_ITEMS = 50;
1078
+ var todoPayload = z.object({ items: z.array(todoItemSchema).max(MAX_TODO_ITEMS) }).strict();
1066
1079
  var MAX_CHECKPOINT_SUMMARY = 1200;
1067
1080
  var MAX_CHECKPOINT_DONE = 20;
1068
1081
  var MAX_CHECKPOINT_EVIDENCE = 5;
@@ -1184,7 +1197,8 @@ var runEventPayloadSchemas = {
1184
1197
  "extension.notice": extensionNoticePayload,
1185
1198
  "reaction.added": reactionAddedPayload,
1186
1199
  "agent.message": agentMessagePayload,
1187
- "checklist.updated": checklistPayload,
1200
+ "todo.updated": todoPayload,
1201
+ "checklist.updated": todoPayload,
1188
1202
  checkpoint: checkpointPayload,
1189
1203
  heartbeat: heartbeatPayload,
1190
1204
  "artifact.added": artifactPayload,
@@ -1239,7 +1253,9 @@ var runRecordSchema = z.object({
1239
1253
  variantScope: text.optional(),
1240
1254
  variantVersion: text.optional(),
1241
1255
  profile: text.optional(),
1242
- title: text.optional()
1256
+ title: text.optional(),
1257
+ model: text.optional(),
1258
+ effort: text.optional()
1243
1259
  });
1244
1260
  function upgradeRunRecord(raw) {
1245
1261
  if (!raw || typeof raw !== "object")
@@ -1255,43 +1271,45 @@ function isActiveStatus(status) {
1255
1271
  return ACTIVE_STATUSES.includes(status);
1256
1272
  }
1257
1273
 
1258
- // src/tools/checklist.ts
1259
- var CHECKLIST_WRITE = "checklist_write";
1274
+ // src/tools/todo.ts
1275
+ var TODO_WRITE = "todo_write";
1276
+ var TODO_GUIDANCE = "If the work has several distinct steps or will take more than one tool call, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. " + "Before your final reply, write one final update: mark every fully completed step completed and leave unfinished or blocked steps accurate. Never mark unfinished work completed. " + "The person follows your work from it, and a subagent\u2019s todo list shows as its progress in the person\u2019s list of subagents.";
1277
+ var TODO_EVENTS = new Set(["todo.updated", "checklist.updated"]);
1260
1278
  var MARK = { pending: "[ ]", in_progress: "[~]", completed: "[x]" };
1261
- function describeChecklist(items) {
1279
+ function describeTodo(items) {
1262
1280
  if (!items.length)
1263
- return "The checklist is empty.";
1281
+ return "The todo list is empty.";
1264
1282
  return items.map((item) => `${MARK[item.status]} ${item.text}`).join(`
1265
1283
  `);
1266
1284
  }
1267
- function checklistTool(run) {
1285
+ function todoTool(run) {
1268
1286
  return {
1269
- name: CHECKLIST_WRITE,
1270
- label: "Write the checklist",
1271
- description: "Keep a checklist of the steps of the current work. The user watches this list to follow your progress, so write it early and keep it current. Send the whole list every time; it replaces the previous one. " + "Use it whenever the work has several distinct steps or will take several tool calls, when the user gives several things to do, and when a subagent brief asks for more than one thing. Do not use it for a single, simple step or for conversation. " + "Statuses: pending, in_progress, completed. Keep exactly one step in_progress while you work, mark a step completed as soon as it is done (not in batches), and add steps you discover. " + "Before your final response, write one final checklist update: mark every fully completed step completed, and leave unfinished or blocked steps in their accurate pending or in_progress status. Never mark unfinished work completed. " + "Only mark a step completed when it is fully done; if something blocks it, keep it in_progress and add a step for the blocker.",
1287
+ name: TODO_WRITE,
1288
+ label: "Write the todo list",
1289
+ description: `Keep a todo list of the steps of the current work. The user watches this list to follow your progress. ${TODO_GUIDANCE} ` + "Send the whole list every time; it replaces the previous one. Do not use it for a single, simple step or for conversation. " + "Statuses: pending, in_progress, completed. Keep exactly one step in_progress while you work, mark a step completed as soon as it is done (not in batches), and add steps you discover. " + "Only mark a step completed when it is fully done; if something blocks it, keep it in_progress and add a step for the blocker.",
1272
1290
  parameters: (Type) => Type.Object({
1273
1291
  items: Type.Array(Type.Object({
1274
1292
  text: Type.String({ description: "One step, as a short imperative line" }),
1275
1293
  status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")])
1276
- }), { maxItems: MAX_CHECKLIST_ITEMS })
1294
+ }), { maxItems: MAX_TODO_ITEMS })
1277
1295
  }),
1278
1296
  readOnly: true,
1279
1297
  planModeSafe: () => true,
1280
1298
  execute: async (input) => {
1281
1299
  if (!Array.isArray(input.items))
1282
- return "Send items: the whole checklist as an array.";
1283
- if (input.items.length > MAX_CHECKLIST_ITEMS)
1284
- return `Keep the checklist to ${MAX_CHECKLIST_ITEMS} steps or fewer.`;
1300
+ return "Send items: the whole todo list as an array.";
1301
+ if (input.items.length > MAX_TODO_ITEMS)
1302
+ return `Keep the todo list to ${MAX_TODO_ITEMS} steps or fewer.`;
1285
1303
  const items = [];
1286
1304
  for (const raw of input.items) {
1287
1305
  const entry = raw;
1288
- const parsed = checklistItemSchema.safeParse({ text: typeof entry?.text === "string" ? entry.text.trim() : entry?.text, status: entry?.status });
1306
+ const parsed = todoItemSchema.safeParse({ text: typeof entry?.text === "string" ? entry.text.trim() : entry?.text, status: entry?.status });
1289
1307
  if (!parsed.success)
1290
1308
  return "Every step needs text of 1 to 500 characters and a status of pending, in_progress or completed.";
1291
1309
  items.push(parsed.data);
1292
1310
  }
1293
- run.emit("checklist.updated", { items });
1294
- return describeChecklist(items);
1311
+ run.emit("todo.updated", { items });
1312
+ return describeTodo(items);
1295
1313
  }
1296
1314
  };
1297
1315
  }
@@ -1411,11 +1429,12 @@ function checkpointTool(run, slice) {
1411
1429
 
1412
1430
  // src/tools/sessionName.ts
1413
1431
  var SESSION_NAME = "session_name";
1432
+ var SESSION_GUIDANCE = "If the request needs tools or changes state \u2014 not a greeting, thanks, or a short conversational reply \u2014 call session_name once before your first other tool call, naming the work from the request. " + "Use one short imperative sentence of at most six words. Revise the name only when the overall objective changes; keep the current name when it does not.";
1414
1433
  function sessionNameTool(current) {
1415
1434
  return {
1416
1435
  name: SESSION_NAME,
1417
1436
  label: "Name the session",
1418
- description: "Set or revise this session\u2019s name. Name meaningful work early, then revise the name when the overall objective changes; greetings alone need no name. " + 'Use one short imperative sentence describing the objective, at most six words, such as "Add web browser integrations". ' + "You may revise a manually assigned name. Keep a useful current name when the objective is unchanged. This only names your own session.",
1437
+ description: `Set or revise this session\u2019s name. ${SESSION_GUIDANCE} ` + 'For example, "Add web browser integrations". You may revise a manually assigned name. This only names your own session.',
1419
1438
  parameters: (Type) => Type.Object({ name: Type.String({ description: "One short sentence describing the objective, at most six words" }) }),
1420
1439
  planModeSafe: () => true,
1421
1440
  execute: async (input) => {
@@ -1445,7 +1464,7 @@ function clip(text, limit) {
1445
1464
  return charCount(text) <= limit ? text : `${takeChars(text, limit)}\u2026`;
1446
1465
  }
1447
1466
  function renderCompactionCheckpoint(input) {
1448
- if (!input.checklist.length && !input.planInstructions)
1467
+ if (!input.todo.length && !input.planInstructions)
1449
1468
  return null;
1450
1469
  const parts = [
1451
1470
  "This is current state from the active run after context compaction, not a new user request. Continue the same request. Do not infer that omitted details or decisions were settled.",
@@ -1456,20 +1475,20 @@ ${clip(input.goal.trim(), 1000)}` : "",
1456
1475
 
1457
1476
  ${clip(input.planInstructions, 4000)}` : ""
1458
1477
  ].filter(Boolean);
1459
- if (input.checklist.length) {
1460
- const active = input.checklist.filter((item) => item.status !== "completed");
1461
- const completed = input.checklist.filter((item) => item.status === "completed");
1462
- const checklist = [
1478
+ if (input.todo.length) {
1479
+ const active = input.todo.filter((item) => item.status !== "completed");
1480
+ const completed = input.todo.filter((item) => item.status === "completed");
1481
+ const todo = [
1463
1482
  active.length ? `### In progress and pending
1464
- ${clip(describeChecklist(active), 2500)}` : "",
1483
+ ${clip(describeTodo(active), 2500)}` : "",
1465
1484
  completed.length ? `### Completed (${completed.length})
1466
- ${clip(describeChecklist(completed), 500)}` : ""
1485
+ ${clip(describeTodo(completed), 500)}` : ""
1467
1486
  ].filter(Boolean).join(`
1468
1487
 
1469
1488
  `);
1470
- parts.push(`## Current checklist
1489
+ parts.push(`## Current todo list
1471
1490
 
1472
- ${checklist}`);
1491
+ ${todo}`);
1473
1492
  }
1474
1493
  const body = clip(parts.join(`
1475
1494
 
@@ -1815,9 +1834,9 @@ function snapshot(root, runId) {
1815
1834
  // src/sessions/workSlice.ts
1816
1835
  var DEFAULT_SLICE_POLICY = {
1817
1836
  enabled: true,
1818
- maxToolCalls: 160,
1819
- maxMinutes: 40,
1820
- maxNewTokens: 320000,
1837
+ maxToolCalls: 320,
1838
+ maxMinutes: 60,
1839
+ maxNewTokens: 0,
1821
1840
  maxSlices: 8,
1822
1841
  continue: true
1823
1842
  };
@@ -1909,37 +1928,57 @@ function canonical(value, depth = 0) {
1909
1928
  }
1910
1929
  var signature = (name, input) => `${name}\x00${createHash("sha256").update(canonical(input ?? null)).digest("hex")}`;
1911
1930
  var clip2 = (value, limit) => value.length <= limit ? value : `${value.slice(0, limit - 1)}\u2026`;
1912
- function handoffRequest(limit, state) {
1913
- return clip2(`This reply reached its work-slice limit (${limit}; step ${state.steps}). Call checkpoint_write now with what is done, the single next step, anything blocking, the files you touched and the checks that matter. Then tell the user in a few lines where you stopped. Do not call other tools.`, 600);
1931
+ function handoffRequest(limit, state, checkpointAvailable = true) {
1932
+ const handoff = checkpointAvailable ? "Call checkpoint_write now with what is done, the single next step, anything blocking, the files you touched and the checks that matter. Then tell the user in a few lines where you stopped." : "Stop using tools and return a concise partial research report with findings, blockers and the next useful research step. The Harness records an evidence-derived checkpoint.";
1933
+ return clip2(`This reply reached its work-slice limit (${limit}; step ${state.steps}). ${handoff} Do not call other tools.`, 600);
1914
1934
  }
1915
- function stalledNote(reason) {
1916
- return clip2(`You are repeating work: ${reason}. Do not repeat it. Choose a different approach, read what the earlier attempt returned, or call checkpoint_write with the blocker so the next slice can decide.`, 600);
1935
+ function stalledNote(reason, checkpointAvailable = true) {
1936
+ const handoff = checkpointAvailable ? "Choose a different approach, read what the earlier attempt returned, or call checkpoint_write with the blocker so the next slice can decide." : "Do not use more tools. Return concise findings and blockers in your final response; the Harness records an evidence-derived checkpoint.";
1937
+ return clip2(`You are repeating work: ${reason}. Do not repeat it. ${handoff}`, 600);
1917
1938
  }
1918
1939
  function workSliceGuard(options) {
1919
1940
  const { policy, state } = options;
1920
1941
  const calls = new Map;
1942
+ const checkCalls = new Set;
1921
1943
  const edits = new Map;
1922
1944
  const failures = new Map;
1923
- let stepsSinceProgress = 0;
1924
1945
  let nudged = false;
1946
+ const checkpointAvailable = () => typeof options.checkpointAvailable === "function" ? options.checkpointAvailable() : options.checkpointAvailable !== false;
1925
1947
  const resetRepetition = () => {
1926
1948
  calls.clear();
1949
+ checkCalls.clear();
1927
1950
  edits.clear();
1928
1951
  failures.clear();
1929
- stepsSinceProgress = 0;
1930
1952
  nudged = false;
1931
1953
  };
1932
1954
  const end = (kind, limit) => {
1933
1955
  state.endedBy = kind;
1934
1956
  state.endedByLimit = limit;
1935
1957
  options.onEnd?.(kind, limit);
1936
- const reason = kind === "stuck" ? stalledNote(limit) : kind === "compaction-thrash" ? handoffRequest("three context compactions in one reply", state) : handoffRequest(limit, state);
1958
+ const canCheckpoint = checkpointAvailable();
1959
+ const reason = kind === "stuck" ? stalledNote(limit, canCheckpoint) : kind === "compaction-thrash" ? handoffRequest("three context compactions in one reply", state, canCheckpoint) : handoffRequest(limit, state, canCheckpoint);
1937
1960
  return { block: true, terminate: true, reason };
1938
1961
  };
1962
+ const nudge = (reason) => {
1963
+ if (nudged)
1964
+ return end("stuck", reason);
1965
+ nudged = true;
1966
+ const canCheckpoint = checkpointAvailable();
1967
+ if (!canCheckpoint) {
1968
+ state.endedBy = "stuck";
1969
+ state.endedByLimit = reason;
1970
+ }
1971
+ return { block: true, reason: stalledNote(reason, canCheckpoint) };
1972
+ };
1939
1973
  const repeated = (name, input) => {
1940
1974
  if (POLLING_TOOLS.has(name))
1941
1975
  return null;
1942
1976
  const key = signature(name, input);
1977
+ if (name === "bash") {
1978
+ const command = input && typeof input === "object" ? String(input.command ?? "") : "";
1979
+ if (command && looksLikeCheck(command))
1980
+ checkCalls.add(key);
1981
+ }
1943
1982
  const count = (calls.get(key) ?? 0) + 1;
1944
1983
  calls.set(key, count);
1945
1984
  if (count < 3)
@@ -1969,9 +2008,19 @@ function workSliceGuard(options) {
1969
2008
  pi.on("tool_result", (event) => {
1970
2009
  if (event.toolName === "checkpoint_write" && !event.isError) {
1971
2010
  state.checkpointWritten = true;
1972
- stepsSinceProgress = 0;
1973
2011
  nudged = false;
1974
2012
  }
2013
+ if ((event.toolName === "edit" || event.toolName === "write") && event.isError === false) {
2014
+ const path = event.input && typeof event.input === "object" ? String(event.input.path ?? "") : "";
2015
+ if (path) {
2016
+ edits.set(path, (edits.get(path) ?? 0) + 1);
2017
+ failures.clear();
2018
+ for (const key of checkCalls)
2019
+ calls.delete(key);
2020
+ checkCalls.clear();
2021
+ nudged = false;
2022
+ }
2023
+ }
1975
2024
  if (event.toolName !== "bash")
1976
2025
  return;
1977
2026
  const command = event.input && typeof event.input === "object" ? String(event.input.command ?? "") : "";
@@ -1984,16 +2033,18 @@ function workSliceGuard(options) {
1984
2033
  failures.delete(command);
1985
2034
  edits.clear();
1986
2035
  calls.clear();
1987
- stepsSinceProgress = 0;
2036
+ checkCalls.clear();
1988
2037
  nudged = false;
1989
2038
  });
1990
2039
  pi.on("tool_call", (event) => {
1991
2040
  const name = event.toolName;
1992
2041
  state.steps += 1;
1993
2042
  state.lastTool = name;
1994
- const handoff = HANDOFF_TOOLS.has(name);
2043
+ const canCheckpoint = checkpointAvailable();
2044
+ const handoff = canCheckpoint && HANDOFF_TOOLS.has(name);
1995
2045
  if (state.endedBy) {
1996
- return handoff ? undefined : { block: true, terminate: true, reason: "This reply is over. Its checkpoint is recorded; nothing else runs." };
2046
+ const reason = state.checkpointWritten ? "This reply is over. Its checkpoint is recorded; nothing else runs." : "This reply is over. The Harness will record an evidence-derived checkpoint; nothing else runs.";
2047
+ return handoff ? undefined : { block: true, terminate: true, reason };
1997
2048
  }
1998
2049
  if (state.compactions >= 3) {
1999
2050
  if (handoff) {
@@ -2008,38 +2059,27 @@ function workSliceGuard(options) {
2008
2059
  state.endedBy = "budget";
2009
2060
  state.endedByLimit = limit;
2010
2061
  state.refusedAt ??= state.steps;
2011
- stepsSinceProgress = 0;
2012
- return handoff ? undefined : { block: true, reason: handoffRequest(limit, state) };
2062
+ return handoff ? undefined : { block: true, reason: handoffRequest(limit, state, canCheckpoint) };
2013
2063
  }
2014
2064
  if (handoff)
2015
2065
  return;
2066
+ if (name === "todo_write")
2067
+ return;
2016
2068
  const edit = name === "edit" || name === "write";
2017
2069
  const path = edit && event.input && typeof event.input === "object" ? String(event.input.path ?? "") : "";
2018
2070
  if (edit && path) {
2019
- const count = (edits.get(path) ?? 0) + 1;
2020
- edits.set(path, count);
2021
- stepsSinceProgress = 0;
2071
+ const count = edits.get(path) ?? 0;
2022
2072
  if (count >= 4) {
2023
2073
  const reason = `${path} was edited ${count} times with no passing check between`;
2024
- if (nudged)
2025
- return end("stuck", reason);
2026
- nudged = true;
2027
- return { block: true, reason: stalledNote(reason) };
2074
+ return nudge(reason);
2028
2075
  }
2029
- } else {
2030
- stepsSinceProgress += 1;
2031
2076
  }
2032
2077
  const repeat = repeated(name, event.input);
2033
2078
  const failure = failing(name, event.input);
2034
- const noProgress = stepsSinceProgress >= 25 ? `${stepsSinceProgress} steps with no file change and no passing check` : null;
2035
- const reason = repeat ?? failure ?? noProgress;
2079
+ const reason = repeat ?? failure;
2036
2080
  if (!reason)
2037
2081
  return;
2038
- if (nudged)
2039
- return end("stuck", reason);
2040
- nudged = true;
2041
- stepsSinceProgress = 0;
2042
- return { block: true, reason: stalledNote(reason) };
2082
+ return nudge(reason);
2043
2083
  });
2044
2084
  };
2045
2085
  }
@@ -2119,20 +2159,22 @@ function subagentTools(service, parentRunId) {
2119
2159
  return [{
2120
2160
  name: SPAWN_SUBAGENT,
2121
2161
  label: "Start a subagent",
2122
- description: "Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children.",
2162
+ description: "Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. The optional tools field only narrows the profile and parent intersection; in plan mode, omit it unless you need a stricter subset. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children. To resume a child that stopped before finishing (interrupted or failed), pass continue with its run ID and no title, agent or tools: it reopens that child's own conversation with the same profile and tools, and brief becomes an optional extra instruction.",
2123
2163
  planModeSafe: (input) => service.planSpawnRefusal(parentRunId, input) === null,
2124
2164
  parameters: (Type) => Type.Object({
2125
- title: Type.String({ description: `A short imperative title for the roster, at most ${MAX_SUBAGENT_TITLE_WORDS} words, such as "Map the auth flow"` }),
2126
- brief: Type.String({ description: "A self-contained task: goal, relevant context or paths, non-goals, expected result, and acceptance or verification. The child cannot see this conversation." }),
2165
+ title: Type.Optional(Type.String({ description: `Required for a new child: a short imperative title for the roster, at most ${MAX_SUBAGENT_TITLE_WORDS} words, such as "Map the auth flow"` })),
2166
+ brief: Type.Optional(Type.String({ description: "Required for a new child: a self-contained task: goal, relevant context or paths, non-goals, expected result, and acceptance or verification. The child cannot see this conversation. With continue, an optional extra instruction." })),
2167
+ continue: Type.Optional(Type.String({ description: "The run ID of one of your subagents that stopped before finishing, to continue it from where it stopped instead of starting a fresh child" })),
2127
2168
  agent: Type.Optional(Type.String({ description: "The name of a specialist profile from your Subagents list; omit only when no listed profile fits (omitting starts an unprofiled copy)" })),
2128
- tools: Type.Optional(Type.Array(Type.String())),
2169
+ tools: Type.Optional(Type.Array(Type.String({ description: "Only request a stricter subset of the profile and parent tools. In plan mode, omit this field to inherit the safe available research intersection." }))),
2129
2170
  wait: Type.Optional(Type.Boolean({ description: "Wait for the child and return its result; default false" }))
2130
2171
  }),
2131
2172
  execute: execute(async (input) => {
2132
- const title = subagentTitle(input.title);
2133
- if (!title)
2173
+ const resume = input.continue === undefined ? undefined : input.continue;
2174
+ const title = resume === undefined ? subagentTitle(input.title) : undefined;
2175
+ if (resume === undefined && !title)
2134
2176
  throw invalid(`Give the subagent a title of at most ${MAX_SUBAGENT_TITLE_WORDS} words, with no control characters.`);
2135
- const child = await service.spawn({ parentRunId, brief: input.brief, title, agent: input.agent, tools: input.tools, wait: input.wait });
2177
+ const child = await service.spawn({ parentRunId, brief: input.brief, title, agent: input.agent, tools: input.tools, wait: input.wait, ...resume === undefined ? {} : { continue: resume } });
2136
2178
  if (input.wait === true)
2137
2179
  return reported(parentRunId, service.results({ parentRunId, runIds: [child.runId] }), [child.runId]);
2138
2180
  return { text: JSON.stringify(child), details: { [SUBAGENT_RECEIPT]: { version: 1, ownerRunId: parentRunId, spawned: [child.runId] } } };
@@ -4925,7 +4967,7 @@ var PLAN_RESEARCH_TOOLS = new Set([
4925
4967
  "grep",
4926
4968
  "find",
4927
4969
  "ls",
4928
- "checklist_write",
4970
+ "todo_write",
4929
4971
  "session_name",
4930
4972
  "history_search",
4931
4973
  "history_read",
@@ -4955,7 +4997,7 @@ var PLAN_RESEARCH_EVIDENCE_TOOLS = new Set([
4955
4997
 
4956
4998
  // src/agents/builtin.ts
4957
4999
  var RECALL = ["history_search", "history_read", "trajectory_search", "trajectory_read", "wiki_search", "wiki_read", "wiki_list", "skill_load"];
4958
- var PROGRESS = ["checklist_write", "session_name"];
5000
+ var PROGRESS = ["todo_write", "session_name"];
4959
5001
  var WEB = ["web_search", "web_fetch"];
4960
5002
  var BUILTIN_PROFILES = [
4961
5003
  {
@@ -4965,7 +5007,7 @@ var BUILTIN_PROFILES = [
4965
5007
  instructions: [
4966
5008
  "You carry out one piece of work from your brief, end to end.",
4967
5009
  "- Read the code you change first and follow the conventions you find.",
4968
- "- Keep a checklist with checklist_write when the brief has more than one step, and name your session with session_name.",
5010
+ "- Keep a todo list with todo_write when the brief has more than one step, and name your session with session_name.",
4969
5011
  "- Make the change, then check it: run the relevant tests, type checks or commands.",
4970
5012
  "- Stay inside the brief. If something outside it blocks you, stop and say so instead of widening the change.",
4971
5013
  "- Finish with a short report: what you changed (files), how you checked it, and anything left open."
@@ -4984,7 +5026,7 @@ var BUILTIN_PROFILES = [
4984
5026
  instructions: [
4985
5027
  "You answer a question about the code, files or history in your brief. You never change anything.",
4986
5028
  "- Search broadly first, then read the parts that matter.",
4987
- "- Keep a checklist with checklist_write when the search has several parts, and name your session with session_name.",
5029
+ "- Keep a todo list with todo_write when the search has several parts, and name your session with session_name.",
4988
5030
  "- Cite what you found as path:line.",
4989
5031
  "- Return conclusions, not file dumps: what you found, where, and how sure you are."
4990
5032
  ].join(`
@@ -5016,6 +5058,8 @@ var BUILTIN_PROFILES = [
5016
5058
 
5017
5059
  // src/agents/profiles.ts
5018
5060
  var PROFILE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
5061
+ var RESERVED_PROFILE_NAMES = ["main"];
5062
+ var reservedRefusal = (name) => `${name} is reserved for the main agent; choose another profile name.`;
5019
5063
  var PROFILE_COLORS = ["blue", "green", "yellow", "red", "magenta", "cyan", "orange", "purple"];
5020
5064
  function projectRoot(cwd) {
5021
5065
  let dir = cwd;
@@ -5067,6 +5111,8 @@ function formatVariant(variant) {
5067
5111
  function parseProfile(text, name, source, path) {
5068
5112
  if (!PROFILE_NAME.test(name))
5069
5113
  throw invalid("A profile name uses lowercase letters, digits, - and _, and starts with a letter or digit.");
5114
+ if (RESERVED_PROFILE_NAMES.includes(name))
5115
+ throw invalid(reservedRefusal(name));
5070
5116
  const parsed = parse(text);
5071
5117
  if (!parsed)
5072
5118
  throw invalid("The profile needs a frontmatter block between --- lines.");
@@ -5131,7 +5177,7 @@ function variantDefinitions(variantDir) {
5131
5177
  if (!folder || !existsSync7(folder))
5132
5178
  return [];
5133
5179
  return readdirSync5(folder).sort().flatMap((name) => {
5134
- const definition = subagentOf(variantDir, name);
5180
+ const definition = RESERVED_PROFILE_NAMES.includes(name) ? null : subagentOf(variantDir, name);
5135
5181
  if (!definition)
5136
5182
  return [];
5137
5183
  return [{
@@ -5189,6 +5235,8 @@ function createProfile(root, cwd, input) {
5189
5235
  const name = typeof input.name === "string" ? input.name.trim() : "";
5190
5236
  if (!PROFILE_NAME.test(name))
5191
5237
  throw invalid("A profile name uses lowercase letters, digits, - and _, and starts with a letter or digit.");
5238
+ if (RESERVED_PROFILE_NAMES.includes(name))
5239
+ throw invalid(reservedRefusal(name));
5192
5240
  if (input.scope !== "global" && input.scope !== "project")
5193
5241
  throw invalid("scope must be global or project.");
5194
5242
  const dir = scopeDir(root, cwd, input.scope);
@@ -5278,6 +5326,72 @@ function sideQuestionServerMethods(harness) {
5278
5326
  };
5279
5327
  }
5280
5328
 
5329
+ // src/core/path.ts
5330
+ import { execFileSync } from "child_process";
5331
+ import { accessSync, constants, statSync as statSync4 } from "fs";
5332
+ import { homedir } from "os";
5333
+ import { delimiter, extname, isAbsolute as isAbsolute2, join as join11 } from "path";
5334
+ var resolved = null;
5335
+ function searchPath() {
5336
+ const platform = process.platform;
5337
+ const pathKey = pathVariableName(process.env);
5338
+ const inherited = process.env[pathKey] ?? "";
5339
+ if (resolved?.platform === platform && resolved.inherited === inherited)
5340
+ return resolved.path;
5341
+ const separator = pathSeparator(platform);
5342
+ const entries = [];
5343
+ if (platform !== "win32") {
5344
+ try {
5345
+ const output = execFileSync(process.env.SHELL || "/bin/zsh", ["-ilc", 'printf %s "$PATH"'], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
5346
+ entries.push(...(output.split(`
5347
+ `).filter(Boolean).at(-1) ?? "").split(separator));
5348
+ } catch {}
5349
+ }
5350
+ entries.push(...inherited.split(separator));
5351
+ if (platform !== "win32") {
5352
+ const home = homedir();
5353
+ if (home)
5354
+ entries.push(...[".local/bin", ".bun/bin", ".npm-global/bin", ".cargo/bin"].map((folder) => join11(home, folder)));
5355
+ entries.push("/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin");
5356
+ }
5357
+ const path = [...new Set(entries.filter(Boolean))].join(separator);
5358
+ resolved = { platform, inherited, path };
5359
+ return path;
5360
+ }
5361
+ function pathVariableName(environment) {
5362
+ return Object.keys(environment).find((name) => name.toLowerCase() === "path") ?? "PATH";
5363
+ }
5364
+ function pathSeparator(platform) {
5365
+ return platform === "win32" ? ";" : delimiter;
5366
+ }
5367
+ function isExecutable(path) {
5368
+ try {
5369
+ accessSync(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
5370
+ return statSync4(path).isFile();
5371
+ } catch {
5372
+ return false;
5373
+ }
5374
+ }
5375
+ function findBinary(name) {
5376
+ const platform = process.platform;
5377
+ const suffixes = platform === "win32" && !extname(name) ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [];
5378
+ const names = [name, ...suffixes.map((suffix) => `${name}${suffix}`)];
5379
+ if (isAbsolute2(name)) {
5380
+ for (const candidate of names)
5381
+ if (isExecutable(candidate))
5382
+ return candidate;
5383
+ return null;
5384
+ }
5385
+ for (const directory of searchPath().split(pathSeparator(platform))) {
5386
+ for (const candidateName of names) {
5387
+ const candidate = join11(directory, candidateName);
5388
+ if (isExecutable(candidate))
5389
+ return candidate;
5390
+ }
5391
+ }
5392
+ return null;
5393
+ }
5394
+
5281
5395
  // src/mcp/types.ts
5282
5396
  import { z as z2 } from "zod";
5283
5397
  var MCP_OAUTH_PORT = 47823;
@@ -5417,7 +5531,7 @@ function connectedAppServerMethods(harness) {
5417
5531
 
5418
5532
  // src/tools/dispatch.ts
5419
5533
  import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync6 } from "fs";
5420
- import { join as join11 } from "path";
5534
+ import { join as join12 } from "path";
5421
5535
 
5422
5536
  // src/accounts/store.ts
5423
5537
  function scrub(text) {
@@ -5680,7 +5794,7 @@ ${tail}` : ""}`;
5680
5794
  }
5681
5795
  }
5682
5796
  function loadSkill(variantDir, name) {
5683
- const skills = variantDir && existsSync8(join11(variantDir, "skills")) ? readdirSync6(join11(variantDir, "skills"), { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync8(join11(variantDir, "skills", entry.name, "SKILL.md"))).map((entry) => entry.name).sort() : [];
5797
+ const skills = variantDir && existsSync8(join12(variantDir, "skills")) ? readdirSync6(join12(variantDir, "skills"), { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync8(join12(variantDir, "skills", entry.name, "SKILL.md"))).map((entry) => entry.name).sort() : [];
5684
5798
  const wanted = name.trim().replace(/^skills\//, "").replace(/`/g, "");
5685
5799
  if (!wanted)
5686
5800
  return skills.length ? `Your skills: ${skills.join(", ")}. Load one with skill_load and its name.` : "You have no skills.";
@@ -5688,11 +5802,11 @@ function loadSkill(variantDir, name) {
5688
5802
  throw invalid("A skill name is lowercase letters, digits, _ or -.");
5689
5803
  if (!skills.includes(wanted))
5690
5804
  return `There is no skill named ${wanted}.${skills.length ? ` Your skills: ${skills.join(", ")}.` : ""}`;
5691
- const dir = join11(variantDir, "skills", wanted);
5692
- const helpers = existsSync8(join11(dir, "scripts")) ? readdirSync6(join11(dir, "scripts")) : [];
5693
- return `${readFileSync6(join11(dir, "SKILL.md"), "utf8")}${helpers.length ? `
5805
+ const dir = join12(variantDir, "skills", wanted);
5806
+ const helpers = existsSync8(join12(dir, "scripts")) ? readdirSync6(join12(dir, "scripts")) : [];
5807
+ return `${readFileSync6(join12(dir, "SKILL.md"), "utf8")}${helpers.length ? `
5694
5808
 
5695
- (This skill's helpers are in ${join11(dir, "scripts")}: ${helpers.join(", ")}.)` : ""}`;
5809
+ (This skill's helpers are in ${join12(dir, "scripts")}: ${helpers.join(", ")}.)` : ""}`;
5696
5810
  }
5697
5811
 
5698
5812
  // src/tools/harnessTools.ts
@@ -5896,7 +6010,7 @@ var WIKI_PLUGIN = {
5896
6010
  };
5897
6011
  var HISTORY_PLUGIN = { id: "history", title: "History", description: "Search earlier conversations and inspect recorded run trajectories.", required: "Earlier runs are how an agent picks up where work stopped.", tools: tools([...RECALL_TOOLS, ...TRAJECTORY_TOOLS]), toolGroups: ["History"] };
5898
6012
  var ASK_USER_PLUGIN = { id: "ask-user", title: "Ask the user", description: "Lets the agent stop and ask a question with choices.", required: "An agent must be able to stop and ask you.", tools: [{ name: ASK_USER }] };
5899
- var CHECKLIST_PLUGIN = { id: "checklist", title: "Checklist", description: "Lets the agent keep a checklist of the steps of its work, recorded with the run so progress can be shown and picked up again.", required: "Progress of longer work is kept through it.", tools: [{ name: CHECKLIST_WRITE, description: "Write the checklist" }] };
6013
+ var TODO_PLUGIN = { id: "todo", title: "Todo", description: "Lets the agent keep a todo list of the steps of its work, recorded with the run so progress can be shown and picked up again.", required: "Progress of longer work is kept through it.", tools: [{ name: TODO_WRITE, description: "Write the todo list" }] };
5900
6014
  var CHECKPOINT_PLUGIN = { id: "checkpoint", title: "Checkpoint", description: "Lets a bounded reply record a handoff at a work-slice boundary so the next slice starts from it.", required: "A reply that stops at its budget leaves the state the next one resumes from.", tools: [{ name: CHECKPOINT_WRITE, description: "Write the handoff checkpoint" }] };
5901
6015
  var CONNECTED_APPS_PLUGIN = {
5902
6016
  id: "connected-apps",
@@ -5945,7 +6059,7 @@ var BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
5945
6059
  serverMethods: historyServerMethods
5946
6060
  },
5947
6061
  { descriptor: ASK_USER_PLUGIN, toolOrder: 40, tools: (context) => context.harnessTools && context.allowedTools.includes(ASK_USER) ? [askUserTool(context.run)] : [] },
5948
- { descriptor: CHECKLIST_PLUGIN, toolOrder: 50, tools: (context) => context.harnessTools ? [checklistTool(context.run)] : [] },
6062
+ { descriptor: TODO_PLUGIN, toolOrder: 50, tools: (context) => context.harnessTools ? [todoTool(context.run)] : [] },
5949
6063
  { descriptor: CHECKPOINT_PLUGIN, toolOrder: 51, tools: (context) => context.harnessTools && context.allowedTools.includes(CHECKPOINT_WRITE) ? [checkpointTool(context.run, () => sliceOf(context.env.root, context.runId))] : [] },
5950
6064
  {
5951
6065
  descriptor: CONNECTED_APPS_PLUGIN,
@@ -6027,10 +6141,10 @@ function promptCommandAvailable(name, taken, extensionCommands = new Set) {
6027
6141
  }
6028
6142
 
6029
6143
  // src/plugins/commandPrefix.ts
6030
- import { basename as basename3, dirname as dirname4, extname } from "path";
6144
+ import { basename as basename3, dirname as dirname4, extname as extname2 } from "path";
6031
6145
 
6032
6146
  // src/plugins/manifest.ts
6033
- import { basename as basename2, join as join12 } from "path";
6147
+ import { basename as basename2, join as join13 } from "path";
6034
6148
  import { z as z3 } from "zod";
6035
6149
  var connectorTemplateSchema = z3.object({
6036
6150
  id: z3.string().regex(/^[a-z0-9-]+$/),
@@ -6067,7 +6181,7 @@ function inside(path) {
6067
6181
  }
6068
6182
  }
6069
6183
  function packageManifest(packageRoot) {
6070
- const manifest = readJsonOr(join12(packageRoot, "package.json"), {});
6184
+ const manifest = readJsonOr(join13(packageRoot, "package.json"), {});
6071
6185
  const own = manifest.harness && typeof manifest.harness === "object" && !Array.isArray(manifest.harness) ? manifest.harness : {};
6072
6186
  return {
6073
6187
  name: typeof manifest.name === "string" && manifest.name ? manifest.name : basename2(packageRoot),
@@ -6099,7 +6213,7 @@ function pluginCommandPrefix(source) {
6099
6213
  const synthetic = /^<[^:>]+:([^>]+)>$/.exec(source.path);
6100
6214
  if (synthetic)
6101
6215
  return slug(synthetic[1]);
6102
- const file = basename3(source.path, extname(source.path));
6216
+ const file = basename3(source.path, extname2(source.path));
6103
6217
  return slug(file === "index" ? basename3(dirname4(source.path)) : file);
6104
6218
  }
6105
6219
 
@@ -6284,20 +6398,23 @@ class PageOverlay {
6284
6398
  contentWidth: contentWidth(width),
6285
6399
  centered
6286
6400
  };
6287
- const escapeHint = page.ownsNavigation ? "Esc back/close" : "Esc close";
6288
- const fullFooter = page.ownsNavigation ? `${escapeHint} \xB7 ${page.hint()}` : [
6289
- page.hint(),
6401
+ const pageHint = page.hint();
6402
+ const returnsToPage = pageHint.split(" \xB7 ").includes("Esc back");
6403
+ const escapeHint = page.ownsNavigation ? "Esc back/close" : returnsToPage ? "Esc back" : "Esc close";
6404
+ const fullFooter = page.ownsNavigation ? `${escapeHint} \xB7 ${pageHint}` : [
6405
+ pageHint,
6290
6406
  tabs.length > 1 ? "Tab/Shift+Tab sections" : "",
6291
- escapeHint
6407
+ returnsToPage ? "" : escapeHint
6292
6408
  ].filter(Boolean).join(" \xB7 ");
6293
6409
  const footerWidth = Math.max(0, width - CHROME_PAD * 2);
6294
- const primaryHint = page.hint().split(" \xB7 ")[0] ?? "";
6410
+ const primaryHint = pageHint.split(" \xB7 ")[0] ?? "";
6295
6411
  const compactFooter = [
6296
6412
  primaryHint,
6297
6413
  !page.ownsNavigation && tabs.length > 1 ? "Tab/\u21E7Tab" : "",
6298
6414
  escapeHint
6299
6415
  ].filter(Boolean).join(" \xB7 ");
6300
- const footer = visibleWidth2(fullFooter) <= footerWidth ? fullFooter : compactFooter;
6416
+ const navigationFooter = [!page.ownsNavigation && tabs.length > 1 ? "Tab/\u21E7Tab" : "", escapeHint].filter(Boolean).join(" \xB7 ");
6417
+ const footer = visibleWidth2(fullFooter) <= footerWidth ? fullFooter : visibleWidth2(compactFooter) <= footerWidth ? compactFooter : visibleWidth2(navigationFooter) <= footerWidth ? navigationFooter : escapeHint;
6301
6418
  return pageFrame({
6302
6419
  title: "",
6303
6420
  headerLine: tabStrip(tabs, page.activeTab(), inner, theme, "top", this.hoveredTab),
@@ -9002,12 +9119,13 @@ class BrowserPage {
9002
9119
  if (row.hub) {
9003
9120
  const entry = row.hub;
9004
9121
  const installed = this.snapshot?.hub?.installed.some((receipt) => receipt.entry.id === entry.id && receipt.entry.version === entry.version);
9005
- actions.push({ id: "preview", label: "Preview reviewed content", run: async () => {
9006
- const page = await this.options.call("terminal.browser.hubPreview", { id: entry.id, version: entry.version });
9007
- this.showContent(page.content + (page.done ? "" : `
9122
+ if (entry.delivery.kind !== "pi-package")
9123
+ actions.push({ id: "preview", label: "Preview reviewed content", run: async () => {
9124
+ const page = await this.options.call("terminal.browser.hubPreview", { id: entry.id, version: entry.version });
9125
+ this.showContent(page.content + (page.done ? "" : `
9008
9126
 
9009
9127
  [Preview limited to first page.]`));
9010
- } });
9128
+ } });
9011
9129
  if (!installed)
9012
9130
  actions.push({ id: "install", label: `Install ${entry.version}`, run: async () => {
9013
9131
  if (!await this.options.ui.confirm(`Install ${entry.title}?`, `${entry.description}
@@ -9585,8 +9703,760 @@ The resource is saved for review. Installing grants no attachment, code trust or
9585
9703
  }
9586
9704
  }
9587
9705
 
9706
+ // src/plugins/surfaces.ts
9707
+ function surfaceBadge(surfaces) {
9708
+ if (!surfaces)
9709
+ return "";
9710
+ return surfaces.terminal === "unavailable" ? "app only" : "better in the app";
9711
+ }
9712
+ function describePlugin(plugin) {
9713
+ const description = plugin.description || "No description.";
9714
+ return plugin.surfaces ? `${description} ${plugin.surfaces.note}` : description;
9715
+ }
9716
+
9717
+ // src/terminal/tui/pages/pluginsPage.ts
9718
+ import { matchesKey as matchesKey9, truncateToWidth as truncateToWidth9, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
9719
+ var SECTIONS2 = [
9720
+ { id: "plugins", title: "Plugins" },
9721
+ { id: "trust", title: "Trust" },
9722
+ { id: "packages", title: "Packages" }
9723
+ ];
9724
+ var SETTINGS_ROW = "settings:";
9725
+ var PACKAGE_ROW = "package:";
9726
+ var INSTALL_PACKAGE = "action:install-package";
9727
+ var isSwitch = (setting) => setting.min === 0 && setting.max === 1;
9728
+ var settingValueText = (setting) => isSwitch(setting) ? setting.value === 1 ? "on" : "off" : `${setting.value.toLocaleString("en-US")}${setting.value === setting.default ? " \xB7 default" : ""}`;
9729
+ var settingsOf = (plugin) => plugin.settings ?? [];
9730
+
9731
+ class PluginsPage {
9732
+ options;
9733
+ presentation = "fullscreen";
9734
+ plugins;
9735
+ section = "plugins";
9736
+ projectId = null;
9737
+ hubManagedIds = new Set;
9738
+ error;
9739
+ restart = false;
9740
+ busy = false;
9741
+ disposed = false;
9742
+ list;
9743
+ pointerLayout;
9744
+ settingsFor = null;
9745
+ saved = false;
9746
+ settingsList;
9747
+ constructor(options) {
9748
+ this.options = options;
9749
+ this.list = new FilterList({
9750
+ theme: options.theme,
9751
+ search: "always",
9752
+ noun: ["item", "items"],
9753
+ empty: "There is nothing to switch or trust here yet.",
9754
+ onActivate: (row) => {
9755
+ this.open(row.id);
9756
+ }
9757
+ });
9758
+ this.settingsList = new FilterList({
9759
+ theme: options.theme,
9760
+ search: "always",
9761
+ noun: ["setting", "settings"],
9762
+ empty: "This plugin has no settings.",
9763
+ onActivate: (row) => {
9764
+ this.change(row.id);
9765
+ },
9766
+ onCancel: () => this.back()
9767
+ });
9768
+ }
9769
+ get focused() {
9770
+ return this.list.focused;
9771
+ }
9772
+ set focused(value) {
9773
+ this.list.focused = value;
9774
+ this.settingsList.focused = value;
9775
+ }
9776
+ activate() {
9777
+ this.reload();
9778
+ }
9779
+ dispose() {
9780
+ this.disposed = true;
9781
+ }
9782
+ tabs() {
9783
+ const plugin = this.settingsPlugin();
9784
+ return plugin ? [{ id: "settings", title: `Plugins \u203A ${safeText(plugin.title)}` }] : SECTIONS2;
9785
+ }
9786
+ activeTab() {
9787
+ return this.settingsPlugin() ? "settings" : this.section;
9788
+ }
9789
+ hint() {
9790
+ if (this.settingsPlugin())
9791
+ return this.settingsList.query ? "Enter change \xB7 Esc clear" : "Enter change \xB7 Esc back";
9792
+ return this.list.query ? "Enter actions \xB7 Esc clear" : "Enter actions \xB7 type to search";
9793
+ }
9794
+ settingsPlugin() {
9795
+ return this.settingsFor ? this.plugins?.find((plugin) => plugin.id === this.settingsFor) : undefined;
9796
+ }
9797
+ async reload() {
9798
+ try {
9799
+ const { call } = this.options;
9800
+ this.projectId = (await call("plugins.session").catch(() => ({ projectId: null }))).projectId;
9801
+ this.plugins = await call("plugins.list");
9802
+ this.error = undefined;
9803
+ this.hubManagedIds = new Set;
9804
+ try {
9805
+ const overview = await call("hub.list");
9806
+ this.hubManagedIds = new Set(overview.installed.filter((receipt) => receipt.entry.delivery.kind === "pi-package").flatMap((receipt) => receipt.resourceIds));
9807
+ } catch {}
9808
+ } catch (error) {
9809
+ this.error = error instanceof Error ? error.message : String(error);
9810
+ }
9811
+ if (this.disposed)
9812
+ return;
9813
+ this.fill();
9814
+ this.options.changed();
9815
+ }
9816
+ fill() {
9817
+ if (this.settingsFor && !this.settingsPlugin())
9818
+ this.settingsFor = null;
9819
+ this.list.setRows(this.rows());
9820
+ const plugin = this.settingsPlugin();
9821
+ if (plugin)
9822
+ this.settingsList.setRows(settingsOf(plugin).map((setting) => ({
9823
+ id: setting.key,
9824
+ label: safeText(setting.label),
9825
+ detail: settingValueText(setting),
9826
+ tone: isSwitch(setting) ? setting.value === 1 ? "success" : "dim" : "muted",
9827
+ searchText: `${setting.label} ${setting.description}`
9828
+ })));
9829
+ }
9830
+ rows() {
9831
+ if (this.section === "packages")
9832
+ return [
9833
+ ...(this.plugins ?? []).filter((plugin) => plugin.install).map((plugin) => {
9834
+ const source = plugin.install.kind === "folder" ? "local folder" : "package";
9835
+ const hub = this.hubManagedIds.has(plugin.id);
9836
+ return { id: `${PACKAGE_ROW}${plugin.id}`, group: safeText(plugin.title), label: "Manage package", detail: `${source} \xB7 ${hub ? "Hub" : "local"} \xB7 ${plugin.version ?? plugin.install.source}`, tone: "muted", searchText: `${plugin.title} ${plugin.install.source} ${plugin.version ?? ""} package ${hub ? "Hub" : "local"}` };
9837
+ }),
9838
+ { id: INSTALL_PACKAGE, group: "Packages", label: "Install package\u2026", detail: "Folder, npm or Git", tone: "accent", searchText: "install add package plugin npm Git folder" }
9839
+ ];
9840
+ const menu = this.menu();
9841
+ const out = [];
9842
+ const placed = new Set;
9843
+ const settingsRow = (plugin) => {
9844
+ const count = settingsOf(plugin).length;
9845
+ placed.add(plugin.id);
9846
+ return { id: `${SETTINGS_ROW}${plugin.id}`, group: safeText(plugin.title), label: "Settings", detail: `${count} ${count === 1 ? "setting" : "settings"} \u203A`, tone: "muted", searchText: `${plugin.title} settings ${settingsOf(plugin).map((setting) => setting.label).join(" ")}` };
9847
+ };
9848
+ menu.forEach((row, index) => {
9849
+ out.push(this.toRow(row));
9850
+ const last = menu[index + 1]?.plugin.id !== row.plugin.id;
9851
+ if (this.section === "plugins" && last && settingsOf(row.plugin).length)
9852
+ out.push(settingsRow(row.plugin));
9853
+ });
9854
+ if (this.section === "plugins") {
9855
+ for (const plugin of this.plugins ?? [])
9856
+ if (settingsOf(plugin).length && !placed.has(plugin.id))
9857
+ out.push(settingsRow(plugin));
9858
+ }
9859
+ return out;
9860
+ }
9861
+ menu() {
9862
+ const rows = menuRows(this.plugins ?? [], this.projectId);
9863
+ if (this.section === "trust")
9864
+ return rows.filter((row) => row.whole && Boolean(row.plugin.code));
9865
+ if (this.section === "packages")
9866
+ return [];
9867
+ return rows.filter((row) => rowActions(row, this.projectId).some((action) => action.startsWith("Switch ")));
9868
+ }
9869
+ key(row) {
9870
+ return `${row.whole ? "plugin" : "item"}:${row.id}`;
9871
+ }
9872
+ toRow(row) {
9873
+ const kind = row.whole ? row.note : row.note.replace(/ in .*$/, "");
9874
+ return {
9875
+ id: this.key(row),
9876
+ group: safeText(row.plugin.title),
9877
+ label: row.whole ? "Whole plugin" : safeText(row.name),
9878
+ detail: `${row.on ? "on" : "off"}${kind ? ` \xB7 ${kind}` : ""}${row.whole && surfaceBadge(row.plugin.surfaces) ? ` \xB7 ${surfaceBadge(row.plugin.surfaces)}` : ""}`,
9879
+ tone: row.on ? "success" : "dim",
9880
+ searchText: `${row.name} ${row.plugin.title} ${row.note}`
9881
+ };
9882
+ }
9883
+ async open(id) {
9884
+ if (id.startsWith(SETTINGS_ROW)) {
9885
+ this.settingsFor = id.slice(SETTINGS_ROW.length);
9886
+ this.saved = false;
9887
+ this.fill();
9888
+ this.options.changed();
9889
+ return;
9890
+ }
9891
+ if (id === INSTALL_PACKAGE) {
9892
+ await this.installPackage();
9893
+ return;
9894
+ }
9895
+ if (id.startsWith(PACKAGE_ROW)) {
9896
+ const plugin = this.plugins?.find((candidate) => candidate.id === id.slice(PACKAGE_ROW.length));
9897
+ if (plugin?.install)
9898
+ await this.managePackage(plugin);
9899
+ return;
9900
+ }
9901
+ const row = this.menu().find((candidate) => this.key(candidate) === id);
9902
+ if (!row || this.busy)
9903
+ return;
9904
+ this.busy = true;
9905
+ try {
9906
+ const action = await this.options.ui.select(`${row.plugin.title}: ${describePlugin(row.plugin)}`, rowActions(row, this.projectId));
9907
+ if (action && action !== BACK3 && await applyPluginAction(this.options.ui, this.options.call, row, action))
9908
+ this.restart = true;
9909
+ } finally {
9910
+ this.busy = false;
9911
+ }
9912
+ await this.reload();
9913
+ }
9914
+ async installPackage() {
9915
+ if (this.busy)
9916
+ return;
9917
+ this.busy = true;
9918
+ try {
9919
+ const source = await this.options.ui.input("Install local plugin package", "npm:package@version, Git address, or absolute folder path");
9920
+ if (!source?.trim())
9921
+ return;
9922
+ const confirmed = await this.options.ui.confirm("Install package?", `${source.trim()} will be installed without running package code. It arrives switched off and code remains untrusted until reviewed.`);
9923
+ if (!confirmed)
9924
+ return;
9925
+ await this.options.call("plugins.install", { source: source.trim() });
9926
+ this.restart = true;
9927
+ this.options.ui.notify("Installed switched off. Changes apply when this session restarts.", "info");
9928
+ await this.reload();
9929
+ } catch (error) {
9930
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), "error");
9931
+ } finally {
9932
+ this.busy = false;
9933
+ if (!this.disposed)
9934
+ this.options.changed();
9935
+ }
9936
+ }
9937
+ async managePackage(plugin) {
9938
+ if (this.busy || !plugin.install)
9939
+ return;
9940
+ const hubManaged = this.hubManagedIds.has(plugin.id);
9941
+ const actions = [
9942
+ ...hubManaged || plugin.install.kind === "folder" ? [] : ["Update package"],
9943
+ hubManaged ? "Uninstall Hub version" : "Remove package",
9944
+ BACK3
9945
+ ];
9946
+ this.busy = true;
9947
+ try {
9948
+ const action = await this.options.ui.select(`${plugin.title}: ${plugin.install.source}`, actions);
9949
+ if (!action || action === BACK3)
9950
+ return;
9951
+ if (action === "Update package") {
9952
+ const confirmed = await this.options.ui.confirm(`Update ${plugin.title}?`, "Fetches a newer package version without running its scripts. Changed code remains subject to trust review.");
9953
+ if (!confirmed)
9954
+ return;
9955
+ await this.options.call("plugins.update", { pluginId: plugin.id });
9956
+ this.options.ui.notify("Updated. Changes apply when this session restarts.", "info");
9957
+ } else {
9958
+ const title = hubManaged ? `Uninstall ${plugin.title} from Hub?` : `Remove ${plugin.title}?`;
9959
+ const message = hubManaged ? "This removes the exact Hub receipt and only its recorded package resources." : "This removes the installed package. A local folder is moved to the Harness trash.";
9960
+ if (!await this.options.ui.confirm(title, message))
9961
+ return;
9962
+ await this.options.call("plugins.remove", { pluginId: plugin.id });
9963
+ this.options.ui.notify(hubManaged ? "Hub package uninstalled." : "Package removed.", "info");
9964
+ }
9965
+ this.restart = true;
9966
+ await this.reload();
9967
+ } catch (error) {
9968
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), "error");
9969
+ } finally {
9970
+ this.busy = false;
9971
+ if (!this.disposed)
9972
+ this.options.changed();
9973
+ }
9974
+ }
9975
+ back() {
9976
+ this.settingsFor = null;
9977
+ this.saved = false;
9978
+ this.fill();
9979
+ }
9980
+ async change(key) {
9981
+ const plugin = this.settingsPlugin();
9982
+ const setting = plugin && settingsOf(plugin).find((candidate) => candidate.key === key);
9983
+ if (!plugin || !setting || this.busy)
9984
+ return;
9985
+ this.busy = true;
9986
+ try {
9987
+ let value;
9988
+ if (isSwitch(setting))
9989
+ value = setting.value === 1 ? 0 : 1;
9990
+ else {
9991
+ const answer = await this.options.ui.input(`${setting.label} (${setting.min.toLocaleString("en-US")}\u2013${setting.max.toLocaleString("en-US")})`, String(setting.value));
9992
+ if (answer === undefined || !answer.trim())
9993
+ return;
9994
+ const typed = answer.trim().replace(/[,_]/g, "");
9995
+ value = Number(typed);
9996
+ if (!/^-?\d+$/.test(typed) || value < setting.min || value > setting.max) {
9997
+ this.options.ui.notify(`${setting.label} must be a whole number from ${setting.min.toLocaleString("en-US")} to ${setting.max.toLocaleString("en-US")}.`, "error");
9998
+ return;
9999
+ }
10000
+ }
10001
+ const fresh = await this.options.call("plugins.configure", { pluginId: plugin.id, settings: { [setting.key]: value } });
10002
+ if (Array.isArray(fresh))
10003
+ this.plugins = fresh;
10004
+ else
10005
+ await this.reload();
10006
+ this.saved = true;
10007
+ this.fill();
10008
+ } catch (error) {
10009
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), "error");
10010
+ } finally {
10011
+ this.busy = false;
10012
+ if (!this.disposed)
10013
+ this.options.changed();
10014
+ }
10015
+ }
10016
+ handleMouse(event) {
10017
+ const layout = this.pointerLayout;
10018
+ if (!layout)
10019
+ return;
10020
+ if (event.x < 0 || event.x >= layout.width || event.y < layout.top || event.y >= layout.top + layout.rows)
10021
+ return { handled: true, render: false };
10022
+ return layout.list.handleMouse({ ...event, y: event.y - layout.top });
10023
+ }
10024
+ handleInput(data) {
10025
+ if (this.settingsPlugin()) {
10026
+ if (matchesKey9(data, "tab") || matchesKey9(data, "shift+tab"))
10027
+ return true;
10028
+ return this.settingsList.handleInput(data);
10029
+ }
10030
+ if (matchesKey9(data, "tab") || matchesKey9(data, "shift+tab")) {
10031
+ if (!this.list.query) {
10032
+ const direction = matchesKey9(data, "shift+tab") ? -1 : 1;
10033
+ const current = SECTIONS2.findIndex((section) => section.id === this.section);
10034
+ this.section = SECTIONS2[(current + direction + SECTIONS2.length) % SECTIONS2.length].id;
10035
+ this.fill();
10036
+ }
10037
+ return true;
10038
+ }
10039
+ return this.list.handleInput(data);
10040
+ }
10041
+ selectedPlugin() {
10042
+ const selectedId = this.list.selected?.id;
10043
+ return selectedId?.startsWith(SETTINGS_ROW) ? this.plugins?.find((candidate) => `${SETTINGS_ROW}${candidate.id}` === selectedId) : selectedId?.startsWith(PACKAGE_ROW) ? this.plugins?.find((candidate) => `${PACKAGE_ROW}${candidate.id}` === selectedId) : this.menu().find((candidate) => this.key(candidate) === selectedId)?.plugin;
10044
+ }
10045
+ pluginInspector(plugin, width, rows) {
10046
+ const theme = this.options.theme();
10047
+ if (width < 1 || rows < 1)
10048
+ return [];
10049
+ if (!plugin) {
10050
+ const action = this.list.selected?.id === INSTALL_PACKAGE;
10051
+ return [theme.fg("muted", action ? "Add a package from npm, Git, or a local folder." : "Select a plugin to inspect it.")].slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10052
+ }
10053
+ const selectedId = this.list.selected?.id;
10054
+ const row = this.menu().find((candidate) => this.key(candidate) === selectedId);
10055
+ const title = row ? row.whole ? row.plugin.title : row.name : plugin.title;
10056
+ const lines = [theme.bold(safeText(title))];
10057
+ if (title !== plugin.title)
10058
+ lines.push(theme.fg("dim", `Part of ${safeText(plugin.title)}`));
10059
+ lines.push("", ...wrapTextWithAnsi4(safeText(describePlugin(plugin)), width));
10060
+ if (row) {
10061
+ const status = this.section === "trust" && row.whole ? plugin.trust === "trusted" ? "Code trusted" : "Code not trusted" : row.on ? "Enabled" : "Disabled";
10062
+ lines.push("", theme.fg(row.on ? "success" : "dim", status));
10063
+ }
10064
+ if (plugin.code) {
10065
+ lines.push(theme.fg("muted", `Code trust: ${plugin.trust === "trusted" ? "trusted" : "not trusted"}`));
10066
+ lines.push(theme.fg("muted", `Fingerprint: ${plugin.code.hash.slice(0, 12)}`));
10067
+ }
10068
+ if (plugin.install) {
10069
+ lines.push("", theme.fg("muted", `Package: ${this.hubManagedIds.has(plugin.id) ? "Hub receipt" : "local"}`));
10070
+ lines.push(...wrapTextWithAnsi4(theme.fg("dim", safeText(plugin.install.source)), width));
10071
+ }
10072
+ if (settingsOf(plugin).length)
10073
+ lines.push("", theme.fg("muted", `${settingsOf(plugin).length} settings`));
10074
+ return lines.slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10075
+ }
10076
+ render(width, rows) {
10077
+ const theme = this.options.theme();
10078
+ if (width < 1 || rows < 1)
10079
+ return [];
10080
+ if (!this.plugins) {
10081
+ this.pointerLayout = undefined;
10082
+ return [theme.fg(this.error ? "error" : "dim", this.error ? `Could not read plugins: ${this.error}` : "Reading plugins\u2026")];
10083
+ }
10084
+ const plugin = this.settingsPlugin();
10085
+ if (plugin)
10086
+ return this.renderSettings(plugin, width, rows);
10087
+ const note = this.restart ? [theme.fg("warning", "Saved. Changes apply when this session restarts.")] : [];
10088
+ const paneWidth = inspectorListWidth(width);
10089
+ if (paneWidth !== undefined) {
10090
+ const bodyRows = Math.max(0, rows - note.length);
10091
+ const rightWidth = Math.max(0, width - paneWidth - 3);
10092
+ const listLines = this.list.render(paneWidth, bodyRows);
10093
+ this.pointerLayout = { list: this.list, width: paneWidth, top: note.length, rows: listLines.length };
10094
+ return [...note, ...splitColumns(listLines, this.pluginInspector(this.selectedPlugin(), rightWidth, bodyRows), paneWidth, width, bodyRows, theme)].slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10095
+ }
10096
+ const selected = this.selectedPlugin();
10097
+ const detail = selected && rows >= 8 ? wrapTextWithAnsi4(safeText(describePlugin(selected)), width).slice(0, 3) : [];
10098
+ const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
10099
+ const listLines = this.list.render(width, listRows);
10100
+ this.pointerLayout = { list: this.list, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
10101
+ const lines = [...note, ...listLines];
10102
+ if (detail.length)
10103
+ lines.push("", ...detail.map((line) => theme.fg("muted", line)));
10104
+ return lines.slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10105
+ }
10106
+ renderSettings(plugin, width, rows) {
10107
+ const theme = this.options.theme();
10108
+ const setting = settingsOf(plugin).find((candidate) => candidate.key === this.settingsList.selected?.id);
10109
+ const range = setting && !isSwitch(setting) ? [`From ${setting.min.toLocaleString("en-US")} to ${setting.max.toLocaleString("en-US")} \xB7 default ${setting.default.toLocaleString("en-US")}`] : [];
10110
+ const note = this.saved ? [theme.fg("success", "Saved.")] : [];
10111
+ const paneWidth = inspectorListWidth(width);
10112
+ if (paneWidth !== undefined) {
10113
+ const bodyRows = Math.max(0, rows - note.length);
10114
+ const rightWidth = Math.max(0, width - paneWidth - 3);
10115
+ const listLines = this.settingsList.render(paneWidth, bodyRows);
10116
+ const details = setting ? [theme.bold(safeText(setting.label)), "", ...wrapTextWithAnsi4(safeText(setting.description), rightWidth), "", ...range.map((line) => theme.fg("muted", line)), theme.fg("dim", `Current value: ${settingValueText(setting)}`)] : [theme.fg("muted", "Select a setting to review its value and description.")];
10117
+ this.pointerLayout = { list: this.settingsList, width: paneWidth, top: note.length, rows: listLines.length };
10118
+ return [...note, ...splitColumns(listLines, details, paneWidth, width, bodyRows, theme)].slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10119
+ }
10120
+ const detail = setting && rows >= 8 ? [...wrapTextWithAnsi4(safeText(setting.description), width).slice(0, 3), ...range] : [];
10121
+ const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
10122
+ const listLines = this.settingsList.render(width, listRows);
10123
+ this.pointerLayout = { list: this.settingsList, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
10124
+ const lines = [...note, ...listLines];
10125
+ if (detail.length)
10126
+ lines.push("", ...detail.map((line) => theme.fg("muted", line)));
10127
+ return lines.slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10128
+ }
10129
+ }
10130
+
10131
+ // src/terminal/tui/pages/hubPage.ts
10132
+ import { matchesKey as matchesKey10, truncateToWidth as truncateToWidth10, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
10133
+
10134
+ // src/terminal/hubMenu.ts
10135
+ function hubSummary(entry) {
10136
+ return `${entry.description}
10137
+
10138
+ Publisher: ${entry.publisher}
10139
+ License: ${entry.license}
10140
+ Source: ${entry.sourceUrl}
10141
+
10142
+ ${entry.delivery.kind === "document" ? "Saved as data for a compatible application. Nothing is created or enabled." : "Installed switched off. Code remains untrusted and approvals remain unchanged."}`;
10143
+ }
10144
+ async function installHubEntry(ui, call, entry) {
10145
+ if (!await ui.confirm(`Install ${entry.title} ${entry.version}?`, hubSummary(entry)))
10146
+ return;
10147
+ try {
10148
+ const receipt = await call("hub.install", { id: entry.id, version: entry.version });
10149
+ ui.notify(receipt.document ? "Saved in this library for a compatible application." : "Installed, switched off. Use /plugins to review and attach it. Changes apply after restarting the session.", "info");
10150
+ } catch (error) {
10151
+ ui.notify(error instanceof Error ? error.message : String(error), "error");
10152
+ }
10153
+ }
10154
+
10155
+ // src/terminal/tui/pages/hubPage.ts
10156
+ var SECTIONS3 = [
10157
+ { id: "catalog", title: "Catalog" },
10158
+ { id: "installed", title: "Installed" }
10159
+ ];
10160
+ var REFRESH = "action:refresh";
10161
+ var installedId = (id, version) => `installed:${id}@${version}`;
10162
+
10163
+ class HubPage {
10164
+ options;
10165
+ presentation = "fullscreen";
10166
+ catalog;
10167
+ section = "catalog";
10168
+ error;
10169
+ refreshError;
10170
+ refreshing = false;
10171
+ previewTitle;
10172
+ previewText;
10173
+ previewOffset = 0;
10174
+ previewRows = 1;
10175
+ busy = false;
10176
+ disposed = false;
10177
+ list;
10178
+ pointerLayout;
10179
+ constructor(options) {
10180
+ this.options = options;
10181
+ this.list = new FilterList({
10182
+ theme: options.theme,
10183
+ search: "always",
10184
+ noun: ["entry", "entries"],
10185
+ empty: "No Community entries have been published yet.",
10186
+ onActivate: (row) => {
10187
+ this.activateRow(row.id);
10188
+ }
10189
+ });
10190
+ }
10191
+ get focused() {
10192
+ return this.list.focused;
10193
+ }
10194
+ set focused(value) {
10195
+ this.list.focused = value;
10196
+ }
10197
+ activate() {
10198
+ this.refreshOnOpen();
10199
+ }
10200
+ dispose() {
10201
+ this.disposed = true;
10202
+ }
10203
+ tabs() {
10204
+ return SECTIONS3;
10205
+ }
10206
+ activeTab() {
10207
+ return this.section;
10208
+ }
10209
+ hint() {
10210
+ if (this.previewText !== undefined)
10211
+ return "\u2191\u2193 scroll \xB7 PgUp/PgDn page \xB7 Esc back";
10212
+ if (this.list.query)
10213
+ return "Enter open \xB7 Esc clear";
10214
+ if (this.section === "installed")
10215
+ return "Enter manage receipt \xB7 type to search";
10216
+ const selectedId = this.list.selected?.id;
10217
+ const entry = this.catalog?.entries.find((candidate) => candidate.channel === "community" && candidate.id + "@" + candidate.version === selectedId);
10218
+ if (entry?.delivery.kind === "pi-package")
10219
+ return "Enter install exact version \xB7 type to search";
10220
+ return entry ? "Enter preview or install \xB7 type to search" : "Enter refresh catalog \xB7 type to search";
10221
+ }
10222
+ async refreshOnOpen() {
10223
+ await this.load(false);
10224
+ if (!this.disposed)
10225
+ await this.load(true);
10226
+ }
10227
+ async load(refresh) {
10228
+ if (refresh) {
10229
+ this.refreshing = true;
10230
+ this.options.changed();
10231
+ }
10232
+ try {
10233
+ const catalog = await this.options.call("hub.list", refresh ? { refresh: true } : {});
10234
+ this.catalog = catalog;
10235
+ this.error = undefined;
10236
+ if (refresh) {
10237
+ this.refreshError = catalog.error ?? undefined;
10238
+ if (catalog.error)
10239
+ this.options.ui.notify(`${catalog.error} Using ${catalog.origin} content.`, "warning");
10240
+ } else if (catalog.error) {
10241
+ this.refreshError = catalog.error;
10242
+ }
10243
+ } catch (error) {
10244
+ const message = error instanceof Error ? error.message : String(error);
10245
+ if (refresh)
10246
+ this.refreshError = message;
10247
+ else if (!this.catalog)
10248
+ this.error = message;
10249
+ } finally {
10250
+ this.refreshing = false;
10251
+ }
10252
+ if (this.disposed)
10253
+ return;
10254
+ this.fill();
10255
+ this.options.changed();
10256
+ }
10257
+ fill() {
10258
+ this.list.setRows(this.rows());
10259
+ }
10260
+ rows() {
10261
+ if (this.section === "installed") {
10262
+ const receipts = this.catalog?.installed ?? [];
10263
+ return [
10264
+ ...receipts.map((receipt) => ({
10265
+ id: installedId(receipt.entry.id, receipt.entry.version),
10266
+ label: safeText(receipt.entry.title),
10267
+ group: safeText(receipt.entry.category),
10268
+ detail: `${receipt.entry.channel} \xB7 ${receipt.state ?? "installed"} \xB7 ${receipt.entry.version}`,
10269
+ tone: receipt.state === "installed" ? "success" : "warning",
10270
+ searchText: `${receipt.entry.title} ${receipt.entry.category} ${receipt.entry.id} ${receipt.entry.publisher} ${receipt.entry.channel} ${receipt.state ?? ""}`
10271
+ })),
10272
+ { id: REFRESH, label: "\u27F3 Refresh catalog", group: "Actions", tone: "accent" }
10273
+ ];
10274
+ }
10275
+ const entries = (this.catalog?.entries ?? []).filter((entry) => entry.channel === "community").sort((left, right) => left.category.localeCompare(right.category));
10276
+ return [
10277
+ ...entries.map((entry) => ({
10278
+ id: entry.id + "@" + entry.version,
10279
+ label: safeText(entry.title),
10280
+ group: safeText(entry.category),
10281
+ detail: entry.version,
10282
+ tone: "muted",
10283
+ searchText: `${entry.title} ${entry.category} ${entry.id} ${entry.publisher} ${entry.tags.join(" ")}`
10284
+ })),
10285
+ { id: REFRESH, label: "\u27F3 Refresh catalog", group: "Actions", tone: "accent" }
10286
+ ];
10287
+ }
10288
+ async activateRow(id) {
10289
+ if (this.busy)
10290
+ return;
10291
+ this.busy = true;
10292
+ try {
10293
+ if (id === REFRESH) {
10294
+ await this.load(true);
10295
+ return;
10296
+ }
10297
+ if (id.startsWith("installed:")) {
10298
+ const receipt = this.catalog?.installed.find((candidate) => installedId(candidate.entry.id, candidate.entry.version) === id);
10299
+ if (receipt)
10300
+ await this.manageReceipt(receipt);
10301
+ return;
10302
+ }
10303
+ const entry = this.catalog?.entries.find((candidate) => candidate.channel === "community" && candidate.id + "@" + candidate.version === id);
10304
+ if (entry) {
10305
+ const installAction = `Install exact version ${entry.version}`;
10306
+ const actions = entry.delivery.kind === "pi-package" ? [installAction, "Back"] : ["Preview reviewed content", installAction, "Back"];
10307
+ const action = await this.options.ui.select(`${safeText(entry.title)} \xB7 ${entry.version}`, actions);
10308
+ if (action === "Preview reviewed content")
10309
+ await this.previewEntry(entry);
10310
+ else if (action === installAction) {
10311
+ await installHubEntry(this.options.ui, this.options.call, entry);
10312
+ await this.load(false);
10313
+ }
10314
+ }
10315
+ } finally {
10316
+ this.busy = false;
10317
+ }
10318
+ }
10319
+ async previewEntry(entry) {
10320
+ if (entry.delivery.kind === "pi-package")
10321
+ return;
10322
+ try {
10323
+ const page = await this.options.call("terminal.browser.hubPreview", { id: entry.id, version: entry.version });
10324
+ this.previewTitle = safeText(entry.title);
10325
+ this.previewText = page.content + (page.done ? "" : `
10326
+
10327
+ [Preview limited to first page.]`);
10328
+ this.previewOffset = 0;
10329
+ this.pointerLayout = undefined;
10330
+ this.options.changed();
10331
+ } catch (error) {
10332
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), "error");
10333
+ }
10334
+ }
10335
+ async manageReceipt(receipt) {
10336
+ const action = await this.options.ui.select(`${receipt.entry.title} \xB7 ${receipt.entry.channel} \xB7 ${receipt.entry.version}`, ["Uninstall this version", "Back"]);
10337
+ if (action !== "Uninstall this version")
10338
+ return;
10339
+ const confirmed = await this.options.ui.confirm(`Uninstall ${receipt.entry.title}?`, `This removes only resources recorded by the ${receipt.entry.channel} receipt ${receipt.entry.id}@${receipt.entry.version} and revokes their attachment choices.`);
10340
+ if (!confirmed)
10341
+ return;
10342
+ try {
10343
+ await this.options.call("hub.uninstall", { id: receipt.entry.id, version: receipt.entry.version });
10344
+ this.options.ui.notify(`Uninstalled ${receipt.entry.title} ${receipt.entry.version}.`, "info");
10345
+ await this.load(false);
10346
+ } catch (error) {
10347
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), "error");
10348
+ }
10349
+ }
10350
+ status() {
10351
+ const catalog = this.catalog;
10352
+ const error = this.refreshError ?? catalog.error;
10353
+ const lastRefresh = catalog.refreshedAt ? ` \xB7 last refreshed ${catalog.refreshedAt}` : "";
10354
+ if (this.refreshing)
10355
+ return { text: `${catalog.origin} catalog \xB7 refreshing\u2026${lastRefresh}`, tone: "muted" };
10356
+ if (error)
10357
+ return { text: `${catalog.origin} catalog \xB7 refresh failed: ${error}${lastRefresh}`, tone: "warning" };
10358
+ return { text: `${catalog.origin} catalog${catalog.refreshedAt ? ` \xB7 refreshed ${catalog.refreshedAt}` : ""}`, tone: "dim" };
10359
+ }
10360
+ handleMouse(event) {
10361
+ if (this.previewText !== undefined)
10362
+ return { handled: true, render: false };
10363
+ const layout = this.pointerLayout;
10364
+ if (!layout)
10365
+ return;
10366
+ if (event.x < 0 || event.x >= layout.width || event.y < layout.top || event.y >= layout.top + layout.rows)
10367
+ return { handled: true, render: false };
10368
+ return this.list.handleMouse({ ...event, y: event.y - layout.top });
10369
+ }
10370
+ handleInput(data) {
10371
+ if (this.previewText !== undefined) {
10372
+ if (matchesKey10(data, "escape")) {
10373
+ this.previewTitle = undefined;
10374
+ this.previewText = undefined;
10375
+ this.previewOffset = 0;
10376
+ this.options.changed();
10377
+ return true;
10378
+ }
10379
+ const page = Math.max(1, this.previewRows);
10380
+ const delta = matchesKey10(data, "down") ? 1 : matchesKey10(data, "up") ? -1 : matchesKey10(data, "pageDown") ? page : matchesKey10(data, "pageUp") ? -page : 0;
10381
+ if (matchesKey10(data, "home"))
10382
+ this.previewOffset = 0;
10383
+ else if (matchesKey10(data, "end"))
10384
+ this.previewOffset = Number.MAX_SAFE_INTEGER;
10385
+ else
10386
+ this.previewOffset += delta;
10387
+ this.options.changed();
10388
+ return true;
10389
+ }
10390
+ if (matchesKey10(data, "tab") || matchesKey10(data, "shift+tab")) {
10391
+ if (!this.list.query) {
10392
+ this.section = this.section === "catalog" ? "installed" : "catalog";
10393
+ this.fill();
10394
+ }
10395
+ return true;
10396
+ }
10397
+ return this.list.handleInput(data);
10398
+ }
10399
+ entryInspector(entry, installed, width, rows) {
10400
+ const theme = this.options.theme();
10401
+ if (width < 1 || rows < 1)
10402
+ return [];
10403
+ if (!entry)
10404
+ return [theme.fg("muted", "Select an entry to review its publisher, license, and version.")].slice(0, rows);
10405
+ const lines = [theme.bold(safeText(entry.title)), "", ...wrapTextWithAnsi5(safeText(entry.description), width), ""];
10406
+ lines.push(`Category: ${safeText(entry.category)}`, `Publisher: ${safeText(entry.publisher)}`, `License: ${safeText(entry.license)}`, `Version: ${safeText(entry.version)}`, `Channel: ${safeText(entry.channel)}`, `ID: ${safeText(entry.id)}`);
10407
+ lines.push("", ...wrapTextWithAnsi5(theme.fg("dim", `Source: ${safeText(entry.sourceUrl)}`), width));
10408
+ if (installed)
10409
+ lines.push("", `Installed: ${safeText(installed.installedAt)}`, `Receipt: ${safeText(installed.state ?? "installed")} \xB7 ${safeText(installed.entry.channel)}`);
10410
+ if (entry.delivery.kind === "pi-package")
10411
+ lines.push("", theme.fg("warning", "Package code stays switched off and untrusted until reviewed."));
10412
+ return lines.slice(0, rows).map((line) => truncateToWidth10(line, width, ""));
10413
+ }
10414
+ render(width, rows) {
10415
+ const theme = this.options.theme();
10416
+ if (width < 1 || rows < 1)
10417
+ return [];
10418
+ if (this.previewText !== undefined) {
10419
+ const content = this.previewText.split(`
10420
+ `).flatMap((line) => wrapTextWithAnsi5(safeText(line), width));
10421
+ this.previewRows = Math.max(1, rows - 1);
10422
+ this.previewOffset = Math.min(this.previewOffset, Math.max(0, content.length - this.previewRows));
10423
+ const header = theme.bold(`Preview \xB7 ${this.previewTitle ?? "Hub entry"} \xB7 ${this.previewOffset + 1}/${Math.max(1, content.length)}`);
10424
+ return [header, ...content.slice(this.previewOffset, this.previewOffset + this.previewRows)].slice(0, rows).map((line) => truncateToWidth10(line, width, ""));
10425
+ }
10426
+ if (!this.catalog) {
10427
+ this.pointerLayout = undefined;
10428
+ return [theme.fg(this.error ? "error" : "dim", this.error ? "Could not read the catalog: " + safeText(this.error) : "Reading the catalog\u2026")];
10429
+ }
10430
+ const selectedId = this.list.selected?.id;
10431
+ const installed = this.section === "installed" ? this.catalog.installed.find((receipt) => installedId(receipt.entry.id, receipt.entry.version) === selectedId) : undefined;
10432
+ const entry = installed?.entry ?? this.catalog.entries.find((candidate) => candidate.channel === "community" && candidate.id + "@" + candidate.version === selectedId);
10433
+ const status = this.status();
10434
+ const statusLine = theme.fg(status.tone, truncateToWidth10(safeText(status.text), width, ""));
10435
+ const paneWidth = inspectorListWidth(width);
10436
+ if (paneWidth !== undefined) {
10437
+ const bodyRows = Math.max(0, rows - 1);
10438
+ const rightWidth = Math.max(0, width - paneWidth - 3);
10439
+ const listLines = this.list.render(paneWidth, bodyRows);
10440
+ this.pointerLayout = { width: paneWidth, top: 1, rows: listLines.length };
10441
+ return [statusLine, ...splitColumns(listLines, this.entryInspector(entry, installed, rightWidth, bodyRows), paneWidth, width, bodyRows, theme)].slice(0, rows).map((line) => truncateToWidth10(line, width, ""));
10442
+ }
10443
+ const detail = entry && rows >= 9 ? [
10444
+ ...wrapTextWithAnsi5(safeText(entry.description), width).slice(0, 3),
10445
+ `${entry.publisher} \xB7 ${entry.license} \xB7 ${entry.id}`,
10446
+ ...installed ? [`Installed ${installed.installedAt} \xB7 ${installed.entry.channel} \xB7 ${installed.state ?? "installed"}`] : []
10447
+ ] : [];
10448
+ const detailHeight = detail.length ? detail.length + 1 : 0;
10449
+ const listLines = this.list.render(width, Math.max(1, rows - 1 - detailHeight));
10450
+ this.pointerLayout = { width, top: 1, rows: Math.min(listLines.length, Math.max(0, rows - 1)) };
10451
+ const lines = [statusLine, ...listLines];
10452
+ if (detail.length)
10453
+ lines.push("", ...detail.map((line) => theme.fg("muted", safeText(line))));
10454
+ return lines.slice(0, rows).map((line) => truncateToWidth10(line, width, ""));
10455
+ }
10456
+ }
10457
+
9588
10458
  // src/terminal/tui/pages/logPage.ts
9589
- import { matchesKey as matchesKey9, stripTerminalSequences as stripTerminalSequences3, truncateToWidth as truncateToWidth9, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
10459
+ import { matchesKey as matchesKey11, stripTerminalSequences as stripTerminalSequences3, truncateToWidth as truncateToWidth11, wrapTextWithAnsi as wrapTextWithAnsi6 } from "@earendil-works/pi-tui";
9590
10460
  var payloadOf = (event) => event.payload && typeof event.payload === "object" ? event.payload : {};
9591
10461
  var stringOf = (value) => typeof value === "string" ? value : undefined;
9592
10462
  var numberOf = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -9739,25 +10609,25 @@ class LogPage {
9739
10609
  }
9740
10610
  handleInput(data) {
9741
10611
  const page = Math.max(1, this.rows - 1);
9742
- if (matchesKey9(data, "tab"))
10612
+ if (matchesKey11(data, "tab"))
9743
10613
  this.changeSource(1);
9744
- else if (matchesKey9(data, "shift+tab"))
10614
+ else if (matchesKey11(data, "shift+tab"))
9745
10615
  this.changeSource(-1);
9746
- else if (matchesKey9(data, "home")) {
10616
+ else if (matchesKey11(data, "home")) {
9747
10617
  this.offset = 0;
9748
10618
  this.atEnd = false;
9749
- } else if (matchesKey9(data, "end"))
10619
+ } else if (matchesKey11(data, "end"))
9750
10620
  this.atEnd = true;
9751
- else if (matchesKey9(data, "up")) {
10621
+ else if (matchesKey11(data, "up")) {
9752
10622
  this.offset = Math.max(0, this.currentOffset() - 1);
9753
10623
  this.atEnd = false;
9754
- } else if (matchesKey9(data, "down")) {
10624
+ } else if (matchesKey11(data, "down")) {
9755
10625
  this.offset = this.currentOffset() + 1;
9756
10626
  this.atEnd = false;
9757
- } else if (matchesKey9(data, "pageUp")) {
10627
+ } else if (matchesKey11(data, "pageUp")) {
9758
10628
  this.offset = Math.max(0, this.currentOffset() - page);
9759
10629
  this.atEnd = false;
9760
- } else if (matchesKey9(data, "pageDown")) {
10630
+ } else if (matchesKey11(data, "pageDown")) {
9761
10631
  this.offset = this.currentOffset() + page;
9762
10632
  this.atEnd = false;
9763
10633
  } else
@@ -9774,7 +10644,7 @@ class LogPage {
9774
10644
  const bodyRows = Math.max(0, rows - 2);
9775
10645
  this.rows = Math.max(1, bodyRows);
9776
10646
  const lines = safe(section.text).split(`
9777
- `).flatMap((line) => line ? wrapTextWithAnsi4(line, width) : [""]);
10647
+ `).flatMap((line) => line ? wrapTextWithAnsi6(line, width) : [""]);
9778
10648
  this.maxOffset = Math.max(0, lines.length - bodyRows);
9779
10649
  if (this.atEnd)
9780
10650
  this.offset = this.maxOffset;
@@ -9782,13 +10652,13 @@ class LogPage {
9782
10652
  this.offset = Math.max(0, Math.min(this.maxOffset, this.offset));
9783
10653
  const last = Math.min(lines.length, this.offset + bodyRows);
9784
10654
  const position = theme.fg("dim", `lines ${lines.length ? this.offset + 1 : 0}-${last}/${lines.length}`);
9785
- return [position, ...lines.slice(this.offset, last)].slice(0, rows).map((line) => truncateToWidth9(line, width, ""));
10655
+ return [position, ...lines.slice(this.offset, last)].slice(0, rows).map((line) => truncateToWidth11(line, width, ""));
9786
10656
  }
9787
10657
  }
9788
10658
 
9789
10659
  // src/terminal/tui/pages/permissionsPage.ts
9790
- import { matchesKey as matchesKey10, truncateToWidth as truncateToWidth10, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
9791
- var SECTIONS2 = [
10660
+ import { matchesKey as matchesKey12, truncateToWidth as truncateToWidth12, wrapTextWithAnsi as wrapTextWithAnsi7 } from "@earendil-works/pi-tui";
10661
+ var SECTIONS4 = [
9792
10662
  { id: "overview", title: "Overview" },
9793
10663
  { id: "tools", title: "Tools" }
9794
10664
  ];
@@ -9832,7 +10702,7 @@ class PermissionsPage {
9832
10702
  this.disposed = true;
9833
10703
  }
9834
10704
  tabs() {
9835
- return SECTIONS2;
10705
+ return SECTIONS4;
9836
10706
  }
9837
10707
  activeTab() {
9838
10708
  return this.section;
@@ -9908,7 +10778,7 @@ class PermissionsPage {
9908
10778
  return this.list.handleMouse(event);
9909
10779
  }
9910
10780
  handleInput(data) {
9911
- if (matchesKey10(data, "tab") || matchesKey10(data, "shift+tab")) {
10781
+ if (matchesKey12(data, "tab") || matchesKey12(data, "shift+tab")) {
9912
10782
  if (!this.list.query) {
9913
10783
  this.section = this.section === "overview" ? "tools" : "overview";
9914
10784
  this.fill();
@@ -9923,16 +10793,16 @@ class PermissionsPage {
9923
10793
  return [];
9924
10794
  if (!this.state)
9925
10795
  return [theme.fg(this.error ? "error" : "dim", this.error ? `Could not read permissions: ${this.error}` : "Reading permissions\u2026")];
9926
- const note = this.section === "overview" && rows >= 8 ? wrapTextWithAnsi5(NOTE, width).slice(0, 2) : [];
10796
+ const note = this.section === "overview" && rows >= 8 ? wrapTextWithAnsi7(NOTE, width).slice(0, 2) : [];
9927
10797
  const lines = this.list.render(width, Math.max(1, rows - (note.length ? note.length + 1 : 0)));
9928
10798
  if (note.length)
9929
10799
  lines.push("", ...note.map((line) => theme.fg("dim", line)));
9930
- return lines.slice(0, rows).map((line) => truncateToWidth10(line, width, ""));
10800
+ return lines.slice(0, rows).map((line) => truncateToWidth12(line, width, ""));
9931
10801
  }
9932
10802
  }
9933
10803
 
9934
10804
  // src/terminal/tui/pages/settingsPage.ts
9935
- import { matchesKey as matchesKey11, truncateToWidth as truncateToWidth11, wrapTextWithAnsi as wrapTextWithAnsi6 } from "@earendil-works/pi-tui";
10805
+ import { matchesKey as matchesKey13, truncateToWidth as truncateToWidth13, wrapTextWithAnsi as wrapTextWithAnsi8 } from "@earendil-works/pi-tui";
9936
10806
  var CATEGORIES = [
9937
10807
  { id: "appearance", title: "Appearance" },
9938
10808
  { id: "model", title: "Model" },
@@ -10020,11 +10890,11 @@ class SettingsPage {
10020
10890
  return this.list.handleMouse(event);
10021
10891
  }
10022
10892
  handleInput(data) {
10023
- if (matchesKey11(data, "tab") || matchesKey11(data, "shift+tab")) {
10893
+ if (matchesKey13(data, "tab") || matchesKey13(data, "shift+tab")) {
10024
10894
  const categories = this.categories();
10025
10895
  if (!this.list.query && categories.length > 1) {
10026
10896
  const at = Math.max(0, categories.findIndex((category) => category.id === this.category));
10027
- this.category = categories[(at + (matchesKey11(data, "shift+tab") ? -1 : 1) + categories.length) % categories.length].id;
10897
+ this.category = categories[(at + (matchesKey13(data, "shift+tab") ? -1 : 1) + categories.length) % categories.length].id;
10028
10898
  this.fill();
10029
10899
  }
10030
10900
  return true;
@@ -10045,16 +10915,16 @@ class SettingsPage {
10045
10915
  const lines = this.list.render(width, Math.max(2, rows - 1 - reserved));
10046
10916
  if (descriptionRows) {
10047
10917
  lines.push("");
10048
- const wrapped = description ? wrapTextWithAnsi6(safeText(description), Math.max(1, width)) : [];
10918
+ const wrapped = description ? wrapTextWithAnsi8(safeText(description), Math.max(1, width)) : [];
10049
10919
  for (let index = 0;index < descriptionRows; index++)
10050
10920
  lines.push(theme.fg("muted", wrapped[index] ?? ""));
10051
10921
  }
10052
- return lines.slice(0, rows).map((line) => truncateToWidth11(line, width, ""));
10922
+ return lines.slice(0, rows).map((line) => truncateToWidth13(line, width, ""));
10053
10923
  }
10054
10924
  }
10055
10925
 
10056
10926
  // src/terminal/tui/pages/trajectoryPage.ts
10057
- import { matchesKey as matchesKey12, stripTerminalSequences as stripTerminalSequences4, truncateToWidth as truncateToWidth12, visibleWidth as visibleWidth9, wrapTextWithAnsi as wrapTextWithAnsi7 } from "@earendil-works/pi-tui";
10927
+ import { matchesKey as matchesKey14, stripTerminalSequences as stripTerminalSequences4, truncateToWidth as truncateToWidth14, visibleWidth as visibleWidth9, wrapTextWithAnsi as wrapTextWithAnsi9 } from "@earendil-works/pi-tui";
10058
10928
  var ACTIVE = new Set(["queued", "running", "waiting_for_approval", "waiting_for_input"]);
10059
10929
  var payloadOf2 = (event) => event.payload && typeof event.payload === "object" ? event.payload : {};
10060
10930
  var stringOf2 = (value) => typeof value === "string" ? value : undefined;
@@ -10115,7 +10985,7 @@ function durationLine(run) {
10115
10985
  return `${(Math.max(0, end - start) / 1000).toFixed(1)} s \xB7 ${timeOf2(run.startedAt)}`;
10116
10986
  }
10117
10987
  var bySequence = (left, right) => left.sequence - right.sequence;
10118
- var SECTIONS3 = [
10988
+ var SECTIONS5 = [
10119
10989
  { id: "runs", title: "Runs" },
10120
10990
  { id: "events", title: "Events" }
10121
10991
  ];
@@ -10178,7 +11048,7 @@ class TrajectoryPage {
10178
11048
  this.eventList.focused = value;
10179
11049
  }
10180
11050
  tabs() {
10181
- return SECTIONS3;
11051
+ return SECTIONS5;
10182
11052
  }
10183
11053
  activeTab() {
10184
11054
  return this.section;
@@ -10386,10 +11256,10 @@ class TrajectoryPage {
10386
11256
  return this.eventList.handleMouse({ ...event, y: event.y - this.listStart });
10387
11257
  }
10388
11258
  handleInput(data) {
10389
- if (matchesKey12(data, "tab") || matchesKey12(data, "shift+tab")) {
10390
- const at = SECTIONS3.findIndex((section) => section.id === this.section);
10391
- const delta = matchesKey12(data, "shift+tab") ? -1 : 1;
10392
- this.section = SECTIONS3[(at + delta + SECTIONS3.length) % SECTIONS3.length].id;
11259
+ if (matchesKey14(data, "tab") || matchesKey14(data, "shift+tab")) {
11260
+ const at = SECTIONS5.findIndex((section) => section.id === this.section);
11261
+ const delta = matchesKey14(data, "shift+tab") ? -1 : 1;
11262
+ this.section = SECTIONS5[(at + delta + SECTIONS5.length) % SECTIONS5.length].id;
10393
11263
  return true;
10394
11264
  }
10395
11265
  if (this.section === "runs")
@@ -10402,11 +11272,11 @@ class TrajectoryPage {
10402
11272
  this.changeRun(-1);
10403
11273
  return true;
10404
11274
  }
10405
- if (matchesKey12(data, "pageUp")) {
11275
+ if (matchesKey14(data, "pageUp")) {
10406
11276
  this.detailOffset = Math.max(0, this.detailOffset - 8);
10407
11277
  return true;
10408
11278
  }
10409
- if (matchesKey12(data, "pageDown")) {
11279
+ if (matchesKey14(data, "pageDown")) {
10410
11280
  this.detailOffset += 8;
10411
11281
  return true;
10412
11282
  }
@@ -10439,7 +11309,7 @@ class TrajectoryPage {
10439
11309
  lines.push(theme.fg("dim", this.run.goal ? `Prompt: ${this.run.goal.replace(/\s+/g, " ").slice(0, 140)}` : "Prompt text unavailable"));
10440
11310
  if (this.error)
10441
11311
  lines.push(theme.fg("error", `Read error: ${this.error}`));
10442
- return lines.slice(0, Math.min(rows, 5)).map((line) => truncateToWidth12(line, width, ""));
11312
+ return lines.slice(0, Math.min(rows, 5)).map((line) => truncateToWidth14(line, width, ""));
10443
11313
  }
10444
11314
  detailLines(width, rows) {
10445
11315
  if (rows < 1)
@@ -10447,10 +11317,10 @@ class TrajectoryPage {
10447
11317
  const event = this.selectedEvent;
10448
11318
  const theme = this.options.theme();
10449
11319
  const heading = theme.bold(theme.fg("accent", event ? `Event JSON \xB7 ${event.type} \xB7 #${event.sequence}` : "Event JSON"));
10450
- const json = event ? wrapTextWithAnsi7(safe2(this.eventJson(event)), Math.max(1, width)) : [this.events.length ? "No event selected" : this.loading ? "Loading recorded events\u2026" : "No recorded events"];
11320
+ const json = event ? wrapTextWithAnsi9(safe2(this.eventJson(event)), Math.max(1, width)) : [this.events.length ? "No event selected" : this.loading ? "Loading recorded events\u2026" : "No recorded events"];
10451
11321
  const visibleRows = Math.max(0, rows - 1);
10452
11322
  this.detailOffset = Math.min(this.detailOffset, Math.max(0, json.length - visibleRows));
10453
- return [heading, ...json.slice(this.detailOffset, this.detailOffset + visibleRows).map((line) => this.options.theme().fg("dim", line))].slice(0, rows).map((line) => truncateToWidth12(line, width, ""));
11323
+ return [heading, ...json.slice(this.detailOffset, this.detailOffset + visibleRows).map((line) => this.options.theme().fg("dim", line))].slice(0, rows).map((line) => truncateToWidth14(line, width, ""));
10454
11324
  }
10455
11325
  render(width, rows) {
10456
11326
  const theme = this.options.theme();
@@ -10466,7 +11336,7 @@ class TrajectoryPage {
10466
11336
  const list = this.runList.render(width, Math.max(0, rows - lines.length));
10467
11337
  this.listEnd = this.listStart + list.length;
10468
11338
  lines.push(...list);
10469
- return lines.slice(0, rows).map((line) => truncateToWidth12(line, width, ""));
11339
+ return lines.slice(0, rows).map((line) => truncateToWidth14(line, width, ""));
10470
11340
  }
10471
11341
  if (!this.run) {
10472
11342
  this.listStart = this.listEnd = 0;
@@ -10491,7 +11361,7 @@ class TrajectoryPage {
10491
11361
  this.listEnd = this.listStart + events.length - 1;
10492
11362
  this.detailsStart = paneStart;
10493
11363
  const pane = Array.from({ length: Math.max(events.length, details.length) }, (_, index) => {
10494
- const left = truncateToWidth12(events[index] ?? "", leftWidth, "");
11364
+ const left = truncateToWidth14(events[index] ?? "", leftWidth, "");
10495
11365
  const gap = " ".repeat(Math.max(0, leftWidth - visibleWidth9(left)));
10496
11366
  return left + gap + theme.fg("dim", " \u2502 ") + (details[index] ?? "");
10497
11367
  });
@@ -10511,12 +11381,12 @@ class TrajectoryPage {
10511
11381
  const detailRows = Math.max(0, rows - lines.length);
10512
11382
  lines.push(...this.detailLines(width, detailRows));
10513
11383
  }
10514
- return lines.slice(0, rows).map((line) => truncateToWidth12(line, width, ""));
11384
+ return lines.slice(0, rows).map((line) => truncateToWidth14(line, width, ""));
10515
11385
  }
10516
11386
  }
10517
11387
 
10518
11388
  // src/terminal/tui/pages/worktreesPage.ts
10519
- import { truncateToWidth as truncateToWidth13, wrapTextWithAnsi as wrapTextWithAnsi8 } from "@earendil-works/pi-tui";
11389
+ import { truncateToWidth as truncateToWidth15, wrapTextWithAnsi as wrapTextWithAnsi10 } from "@earendil-works/pi-tui";
10520
11390
  var TABS = [{ id: "worktrees", title: "Worktrees" }];
10521
11391
  var WORKTREE_ACTIONS = {
10522
11392
  switch: "Switch this terminal here (new conversation)",
@@ -10735,15 +11605,15 @@ class WorktreesPage {
10735
11605
  if (!this.overview)
10736
11606
  return [theme.fg("dim", "Reading worktrees\u2026")];
10737
11607
  if (!this.overview.repo) {
10738
- return wrapTextWithAnsi8(`Worktrees need a git repository. ${this.overview.reason ?? ""}`.trim(), width).slice(0, rows).map((line) => theme.fg("dim", line));
11608
+ return wrapTextWithAnsi10(`Worktrees need a git repository. ${this.overview.reason ?? ""}`.trim(), width).slice(0, rows).map((line) => theme.fg("dim", line));
10739
11609
  }
10740
- const header = truncateToWidth13(theme.fg("dim", `Repository ${this.overview.repo}`), width, "");
10741
- return [header, "", ...this.list.render(width, Math.max(1, rows - 2))].slice(0, rows).map((line) => truncateToWidth13(line, width, ""));
11610
+ const header = truncateToWidth15(theme.fg("dim", `Repository ${this.overview.repo}`), width, "");
11611
+ return [header, "", ...this.list.render(width, Math.max(1, rows - 2))].slice(0, rows).map((line) => truncateToWidth15(line, width, ""));
10742
11612
  }
10743
11613
  }
10744
11614
 
10745
11615
  // src/terminal/tui/pages/notesPage.ts
10746
- import { truncateToWidth as truncateToWidth14 } from "@earendil-works/pi-tui";
11616
+ import { truncateToWidth as truncateToWidth16 } from "@earendil-works/pi-tui";
10747
11617
  var TABS2 = [{ id: "notes", title: "Web annotations" }];
10748
11618
  var NOTE_KINDS = { page: "The whole page", text: "Text I select in the window", element: "An element I click in the window" };
10749
11619
  var NOTE_ACTIONS = {
@@ -10958,12 +11828,12 @@ class NotesPage {
10958
11828
  return [theme.fg("error", `Could not read web annotations: ${this.error}`)];
10959
11829
  if (!this.notes)
10960
11830
  return [theme.fg("dim", "Reading web annotations\u2026")];
10961
- return this.list.render(width, rows).slice(0, rows).map((line) => truncateToWidth14(line, width, ""));
11831
+ return this.list.render(width, rows).slice(0, rows).map((line) => truncateToWidth16(line, width, ""));
10962
11832
  }
10963
11833
  }
10964
11834
 
10965
11835
  // src/terminal/tui/pages/annotatePage.ts
10966
- import { truncateToWidth as truncateToWidth15 } from "@earendil-works/pi-tui";
11836
+ import { truncateToWidth as truncateToWidth17 } from "@earendil-works/pi-tui";
10967
11837
  var TABS3 = [{ id: "annotate", title: "Annotate" }];
10968
11838
  var ANNOTATE_ACTIONS = {
10969
11839
  reveal: "Show it in the window",
@@ -11295,7 +12165,7 @@ class AnnotatePage {
11295
12165
  if (this.error)
11296
12166
  lines.push(theme.fg("error", this.error));
11297
12167
  lines.push("");
11298
- return lines.map((line) => truncateToWidth15(line, width, ""));
12168
+ return lines.map((line) => truncateToWidth17(line, width, ""));
11299
12169
  }
11300
12170
  render(width, rows) {
11301
12171
  const theme = this.options.theme();
@@ -11305,12 +12175,12 @@ class AnnotatePage {
11305
12175
  if (!this.notes)
11306
12176
  return [...header, theme.fg("dim", "Reading page notes\u2026")].slice(0, rows);
11307
12177
  const body = this.list.render(width, Math.max(1, rows - header.length));
11308
- return [...header, ...body].slice(0, rows).map((line) => truncateToWidth15(line, width, ""));
12178
+ return [...header, ...body].slice(0, rows).map((line) => truncateToWidth17(line, width, ""));
11309
12179
  }
11310
12180
  }
11311
12181
 
11312
12182
  // src/terminal/tui/pages/subagentsPage.ts
11313
- import { matchesKey as matchesKey13, stripTerminalSequences as stripTerminalSequences7, truncateToWidth as truncateToWidth18, wrapTextWithAnsi as wrapTextWithAnsi9 } from "@earendil-works/pi-tui";
12183
+ import { matchesKey as matchesKey15, stripTerminalSequences as stripTerminalSequences7, truncateToWidth as truncateToWidth20, wrapTextWithAnsi as wrapTextWithAnsi11 } from "@earendil-works/pi-tui";
11314
12184
 
11315
12185
  // src/terminal/subagentMonitor.ts
11316
12186
  var ACTIVE_SUBAGENT_STATUSES = new Set(["queued", "running", "waiting_for_approval", "waiting_for_input"]);
@@ -11373,6 +12243,8 @@ function profileColor(theme, name) {
11373
12243
  return theme.primary;
11374
12244
  case "purple":
11375
12245
  return theme.purple;
12246
+ case "main":
12247
+ return theme.primary;
11376
12248
  default:
11377
12249
  return theme.accent;
11378
12250
  }
@@ -11415,7 +12287,7 @@ function toolColor(theme, status) {
11415
12287
  }
11416
12288
 
11417
12289
  // src/terminal/opentui/subagentsWidget.ts
11418
- import { stripTerminalSequences as stripTerminalSequences5, truncateToWidth as truncateToWidth16, visibleWidth as visibleWidth10 } from "@earendil-works/pi-tui";
12290
+ import { stripTerminalSequences as stripTerminalSequences5, truncateToWidth as truncateToWidth18, visibleWidth as visibleWidth10 } from "@earendil-works/pi-tui";
11419
12291
 
11420
12292
  // src/terminal/tui/workingStatus.ts
11421
12293
  var WORKING_VERBS = [
@@ -11647,7 +12519,7 @@ function formatWorkingDuration(durationMs) {
11647
12519
  var MAX_VISIBLE_ROWS = 3;
11648
12520
  var GAP = " ";
11649
12521
  function cut(text, width) {
11650
- return stripTerminalSequences5(truncateToWidth16(text, width, "\u2026"));
12522
+ return stripTerminalSequences5(truncateToWidth18(text, width, "\u2026"));
11651
12523
  }
11652
12524
  function safe3(text) {
11653
12525
  return stripTerminalSequences5(text).replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim();
@@ -11669,20 +12541,21 @@ function mark2(status, frame) {
11669
12541
  return { mark: frame, tone: "warning" };
11670
12542
  }
11671
12543
  }
11672
- function subagentTasks(checklist) {
11673
- return checklist && checklist.total > 0 ? `${checklist.done} of ${checklist.total} tasks` : null;
12544
+ function subagentTasks(todo) {
12545
+ return todo && todo.total > 0 ? `${todo.done} of ${todo.total} tasks` : null;
11674
12546
  }
11675
12547
  function subagentRoster(items, width, frame = getWorkingSpinnerFrames()[0] ?? "") {
11676
- if (!items.length || width < 1)
12548
+ const children = items.filter((item) => item.relation !== "main");
12549
+ if (!children.length || width < 1)
11677
12550
  return null;
11678
- const ordered = [...items].sort((left, right) => {
12551
+ const ordered = [...children].sort((left, right) => {
11679
12552
  const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
11680
12553
  return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
11681
12554
  });
11682
12555
  const rows = ordered.slice(0, MAX_VISIBLE_ROWS).map((item) => {
11683
12556
  const status = mark2(item.status, frame);
11684
12557
  const profile = cut(safe3(item.profile?.name ?? "") || "subagent", Math.max(1, Math.min(24, width - 2)));
11685
- const tasks = subagentTasks(item.checklist);
12558
+ const tasks = subagentTasks(item.todo);
11686
12559
  const used = 2 + visibleWidth10(profile) + GAP.length + (tasks ? GAP.length + tasks.length : 0);
11687
12560
  const title = cut(safe3(item.title) || "Subagent", Math.max(1, width - used));
11688
12561
  return { id: item.id, mark: status.mark, tone: status.tone, profile, color: item.profile?.color ?? null, title, tasks };
@@ -11693,7 +12566,7 @@ function subagentRoster(items, width, frame = getWorkingSpinnerFrames()[0] ?? ""
11693
12566
 
11694
12567
  // src/terminal/tui/status.ts
11695
12568
  import { basename as basename4, sep as sep3 } from "path";
11696
- import { parseColor as parseColor3, stripTerminalSequences as stripTerminalSequences6, truncateToWidth as truncateToWidth17, visibleWidth as visibleWidth11 } from "@earendil-works/pi-tui";
12569
+ import { parseColor as parseColor3, stripTerminalSequences as stripTerminalSequences6, truncateToWidth as truncateToWidth19, visibleWidth as visibleWidth11 } from "@earendil-works/pi-tui";
11697
12570
 
11698
12571
  // src/terminal/tui/theme.ts
11699
12572
  import { Theme } from "@earendil-works/pi-coding-agent";
@@ -11953,9 +12826,9 @@ function rowSubtitle(item, width) {
11953
12826
  const available = Math.max(1, width - 2);
11954
12827
  const verbose = width >= 96;
11955
12828
  const account = clean2(item.accountLabel ?? "") || "\u2014";
11956
- const model = truncateToWidth18(effectiveModel(item), 24, "\u2026");
12829
+ const model = truncateToWidth20(effectiveModel(item), 24, "\u2026");
11957
12830
  const details = verbose ? [`acct ${account}`, `model ${model}`, `eff ${item.effort ?? "\u2014"}`, usageSummary(item)].join(" \xB7 ") : [`a:${account}`, `m:${model}`, `e:${item.effort ?? "\u2014"}`, usageSummary(item)].join(" \xB7 ");
11958
- return truncateToWidth18(details, available, "\u2026");
12831
+ return truncateToWidth20(details, available, "\u2026");
11959
12832
  }
11960
12833
 
11961
12834
  class SubagentsPage {
@@ -11989,8 +12862,9 @@ class SubagentsPage {
11989
12862
  }
11990
12863
  rows(width = 80, now = this.options.now?.() ?? Date.now()) {
11991
12864
  const ordered = [...this.options.snapshot()].sort((left, right) => {
12865
+ const main = Number(right.relation === "main") - Number(left.relation === "main");
11992
12866
  const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
11993
- return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
12867
+ return main || active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
11994
12868
  });
11995
12869
  return ordered.map((item) => {
11996
12870
  const profile = clean2(item.profile?.name ?? "") || "subagent";
@@ -12015,31 +12889,32 @@ class SubagentsPage {
12015
12889
  render(width, rows) {
12016
12890
  const items = this.rows(width, this.options.now?.() ?? Date.now());
12017
12891
  this.list.setRows(items);
12018
- const active = items.filter((item) => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
12892
+ const children = items.filter((item) => item.data?.relation !== "main");
12893
+ const active = children.filter((item) => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
12019
12894
  const selected = this.list.selected?.data;
12020
- const header = headerRow("Subagents", `${active} active \xB7 ${items.length} total`, width, this.options.theme());
12895
+ const header = headerRow("Subagents", `${active} active \xB7 ${children.length} total`, width, this.options.theme());
12021
12896
  const bodyRows = Math.max(0, rows - 7);
12022
12897
  const listRows = Math.max(1, Math.min(this.list.height, Math.floor(bodyRows * 0.6)));
12023
12898
  const output = [header, "", ...this.list.render(width, listRows), ""];
12024
12899
  if (selected) {
12025
12900
  const theme = this.options.theme();
12026
12901
  const detailIndent = " ".repeat(CHROME_PAD * 2);
12027
- const detailLines = (text) => wrapTextWithAnsi9(theme.fg("text", clean2(text)), Math.max(1, width - detailIndent.length)).map((line) => `${detailIndent}${line}`);
12902
+ const detailLines = (text) => wrapTextWithAnsi11(theme.fg("text", clean2(text)), Math.max(1, width - detailIndent.length)).map((line) => `${detailIndent}${line}`);
12028
12903
  output.push(neutralRule(width, theme));
12029
12904
  output.push(padded(theme.fg("muted", `Profile: ${clean2(selected.profile?.name ?? "") || "copy of the parent"}`), width));
12030
12905
  output.push(padded(theme.fg("accent", "Task"), width));
12031
- output.push(...detailLines(selected.goal || "Subagent"));
12032
- output.push(padded(theme.fg("muted", `Status: ${[selected.status.replaceAll("_", " "), subagentTasks(selected.checklist), terminalSubagentDuration(selected)].filter(Boolean).join(" \xB7 ")}`), width));
12906
+ output.push(...detailLines(selected.goal || (selected.relation === "main" ? "Main agent" : "Subagent")));
12907
+ output.push(padded(theme.fg("muted", `Status: ${[selected.status.replaceAll("_", " "), subagentTasks(selected.todo), terminalSubagentDuration(selected)].filter(Boolean).join(" \xB7 ")}`), width));
12033
12908
  output.push(padded(theme.fg("accent", "Current activity"), width));
12034
12909
  output.push(...detailLines(selected.activity));
12035
12910
  } else {
12036
12911
  output.push(padded(this.options.theme().fg("muted", "The selected subagent\u2019s task and current activity appear here."), width));
12037
12912
  }
12038
- return output.slice(0, Math.max(0, rows)).map((line) => truncateToWidth18(line, width, ""));
12913
+ return output.slice(0, Math.max(0, rows)).map((line) => truncateToWidth20(line, width, ""));
12039
12914
  }
12040
12915
  handleInput(data) {
12041
12916
  this.list.setRows(this.rows());
12042
- if (matchesKey13(data, "enter"))
12917
+ if (matchesKey15(data, "enter"))
12043
12918
  return this.list.handleInput(data);
12044
12919
  if (this.list.handleInput(data)) {
12045
12920
  this.options.changed();
@@ -12050,7 +12925,7 @@ class SubagentsPage {
12050
12925
  }
12051
12926
 
12052
12927
  // src/terminal/tui/pages/agentsPage.ts
12053
- import { stripTerminalSequences as stripTerminalSequences8, truncateToWidth as truncateToWidth19, wrapTextWithAnsi as wrapTextWithAnsi10 } from "@earendil-works/pi-tui";
12928
+ import { stripTerminalSequences as stripTerminalSequences8, truncateToWidth as truncateToWidth21, wrapTextWithAnsi as wrapTextWithAnsi12 } from "@earendil-works/pi-tui";
12054
12929
  var AGENT_ACTIONS = {
12055
12930
  edit: "Edit in your editor",
12056
12931
  copy: "Copy to a new profile",
@@ -12244,7 +13119,7 @@ class AgentsPage {
12244
13119
  const profile = selected.profile;
12245
13120
  const field = (label, value) => padded(`${theme.fg("muted", `${label}: `)}${theme.fg("text", clean3(value))}`, width);
12246
13121
  output.push(padded(theme.fg("accent", `${profile.name} \xB7 ${profile.color}`), width));
12247
- output.push(...wrapTextWithAnsi10(theme.fg("text", clean3(profile.description)), textWidth));
13122
+ output.push(...wrapTextWithAnsi12(theme.fg("text", clean3(profile.description)), textWidth));
12248
13123
  output.push(field("Tools", profile.tools?.join(", ") ?? "all of the parent's"));
12249
13124
  output.push(field("Plugins", profile.plugins?.join(", ") ?? "all of the parent's"));
12250
13125
  output.push(field("Variant", formatVariant(profile.variant) ?? "the parent's"));
@@ -12252,7 +13127,7 @@ class AgentsPage {
12252
13127
  output.push(field("File", profile.path ?? "built in"));
12253
13128
  output.push(padded(theme.fg("accent", "Instructions"), width));
12254
13129
  output.push(...profile.instructions.split(`
12255
- `).flatMap((line) => wrapTextWithAnsi10(theme.fg("muted", clean3(line)), textWidth)));
13130
+ `).flatMap((line) => wrapTextWithAnsi12(theme.fg("muted", clean3(line)), textWidth)));
12256
13131
  } else if (selected?.kind === "problem") {
12257
13132
  output.push(padded(theme.fg("error", selected.problem.error), width));
12258
13133
  output.push(padded(theme.fg("muted", selected.problem.path), width));
@@ -12260,12 +13135,12 @@ class AgentsPage {
12260
13135
  const dirs = this.overview.dirs;
12261
13136
  output.push(padded(theme.fg("muted", `Your profiles live in ${dirs.global}${dirs.project ? ` and ${dirs.project}` : ""}. A project profile wins over a global one, and both over a built-in one.`), width));
12262
13137
  }
12263
- return output.slice(0, rows).map((line) => truncateToWidth19(line, width, ""));
13138
+ return output.slice(0, rows).map((line) => truncateToWidth21(line, width, ""));
12264
13139
  }
12265
13140
  }
12266
13141
 
12267
13142
  // src/terminal/tui/pages/usagePage.ts
12268
- import { matchesKey as matchesKey14, stripTerminalSequences as stripTerminalSequences9, truncateToWidth as truncateToWidth20, visibleWidth as visibleWidth12, wrapTextWithAnsi as wrapTextWithAnsi11 } from "@earendil-works/pi-tui";
13143
+ import { matchesKey as matchesKey16, stripTerminalSequences as stripTerminalSequences9, truncateToWidth as truncateToWidth22, visibleWidth as visibleWidth12, wrapTextWithAnsi as wrapTextWithAnsi13 } from "@earendil-works/pi-tui";
12269
13144
  var PERIODS = [
12270
13145
  { key: "1", period: "today", label: "today" },
12271
13146
  { key: "2", period: "7d", label: "7d" },
@@ -12378,7 +13253,7 @@ function bar(percent, width, theme, rejected = false) {
12378
13253
  function spread(left, right, width) {
12379
13254
  if (!right)
12380
13255
  return left;
12381
- const rightText = truncateToWidth20(right, Math.max(0, width - visibleWidth12(left) - 2), "");
13256
+ const rightText = truncateToWidth22(right, Math.max(0, width - visibleWidth12(left) - 2), "");
12382
13257
  return left + " ".repeat(Math.max(1, width - visibleWidth12(left) - visibleWidth12(rightText))) + rightText;
12383
13258
  }
12384
13259
 
@@ -12472,17 +13347,17 @@ class UsagePage {
12472
13347
  }
12473
13348
  handleInput(data) {
12474
13349
  const page = Math.max(1, this.scrollRows - 1);
12475
- if (matchesKey14(data, "up"))
13350
+ if (matchesKey16(data, "up"))
12476
13351
  this.offset = Math.max(0, this.offset - 1);
12477
- else if (matchesKey14(data, "down"))
13352
+ else if (matchesKey16(data, "down"))
12478
13353
  this.offset = Math.min(this.maxOffset, this.offset + 1);
12479
- else if (matchesKey14(data, "pageUp"))
13354
+ else if (matchesKey16(data, "pageUp"))
12480
13355
  this.offset = Math.max(0, this.offset - page);
12481
- else if (matchesKey14(data, "pageDown"))
13356
+ else if (matchesKey16(data, "pageDown"))
12482
13357
  this.offset = Math.min(this.maxOffset, this.offset + page);
12483
- else if (matchesKey14(data, "home"))
13358
+ else if (matchesKey16(data, "home"))
12484
13359
  this.offset = 0;
12485
- else if (matchesKey14(data, "end"))
13360
+ else if (matchesKey16(data, "end"))
12486
13361
  this.offset = this.maxOffset;
12487
13362
  else if (data === "r") {
12488
13363
  if (this.tab === "limits")
@@ -12509,7 +13384,7 @@ class UsagePage {
12509
13384
  const scope = SCOPES2.find((entry) => entry.scope === summary.scope)?.label ?? summary.scope;
12510
13385
  const period = PERIODS.find((entry) => entry.period === summary.period)?.label ?? summary.period;
12511
13386
  header.push(theme.bold(theme.fg("accent", `${scope} \xB7 ${period}`)));
12512
- header.push(...wrapTextWithAnsi11(theme.fg("muted", usageLine2(summary.totals)), width));
13387
+ header.push(...wrapTextWithAnsi13(theme.fg("muted", usageLine2(summary.totals)), width));
12513
13388
  const details = [
12514
13389
  summary.totals.cacheHitPercent === null ? null : `${summary.totals.cacheHitPercent}% cache hits`,
12515
13390
  `TTFT ${duration(summary.totals.avgTtftMs)}`,
@@ -12517,9 +13392,9 @@ class UsagePage {
12517
13392
  summary.totals.estimatedCostUsd ? `${money(summary.totals.estimatedCostUsd)} estimated` : null
12518
13393
  ].filter((entry) => Boolean(entry));
12519
13394
  if (details.length)
12520
- header.push(...wrapTextWithAnsi11(theme.fg("dim", details.join(" \xB7 ")), width));
13395
+ header.push(...wrapTextWithAnsi13(theme.fg("dim", details.join(" \xB7 ")), width));
12521
13396
  if (this.error)
12522
- header.push(...wrapTextWithAnsi11(theme.fg("error", this.error), width));
13397
+ header.push(...wrapTextWithAnsi13(theme.fg("error", this.error), width));
12523
13398
  const body = [""];
12524
13399
  const total = tokensOf(summary.totals);
12525
13400
  const gap = () => {
@@ -12531,7 +13406,7 @@ class UsagePage {
12531
13406
  const spark = summary.byDay.slice(-14).map((day) => BARS[Math.min(BARS.length - 1, Math.round((day.inputTokens + day.cacheReadTokens + day.outputTokens) / max * (BARS.length - 1)))]).join("");
12532
13407
  body.push(theme.fg("accent", spark));
12533
13408
  for (const day of summary.byDay.slice(-3).reverse()) {
12534
- body.push(...wrapTextWithAnsi11(theme.fg("muted", ` ${day.day} ${count(day.inputTokens + day.cacheReadTokens + day.outputTokens)} tokens \xB7 ${count(day.requests)} requests \xB7 ${money(day.costUsd)}`), width));
13409
+ body.push(...wrapTextWithAnsi13(theme.fg("muted", ` ${day.day} ${count(day.inputTokens + day.cacheReadTokens + day.outputTokens)} tokens \xB7 ${count(day.requests)} requests \xB7 ${money(day.costUsd)}`), width));
12535
13410
  }
12536
13411
  }
12537
13412
  const section = (rows) => {
@@ -12540,7 +13415,7 @@ class UsagePage {
12540
13415
  gap();
12541
13416
  for (const row of rows) {
12542
13417
  const line = ` ${oneLine2(row.label)} ${share2(tokensOf(row), total)} ${count(tokensOf(row))} tokens \xB7 ${count(row.requests)} requests \xB7 ${money(row.costUsd)}`;
12543
- body.push(...wrapTextWithAnsi11(theme.fg("muted", line), width));
13418
+ body.push(...wrapTextWithAnsi13(theme.fg("muted", line), width));
12544
13419
  }
12545
13420
  };
12546
13421
  section(summary.byModel);
@@ -12561,7 +13436,7 @@ class UsagePage {
12561
13436
  return { header, body: [] };
12562
13437
  }
12563
13438
  if (this.error)
12564
- header.push(...wrapTextWithAnsi11(theme.fg("error", this.error), width));
13439
+ header.push(...wrapTextWithAnsi13(theme.fg("error", this.error), width));
12565
13440
  const body = [];
12566
13441
  const barWidth = Math.max(8, Math.min(16, Math.floor(width / 6)));
12567
13442
  for (const account of report.accounts) {
@@ -12577,11 +13452,11 @@ class UsagePage {
12577
13452
  const source = provider.source === "provider" ? "" : provider.source;
12578
13453
  const meta = [source, ago(provider.observedAt)].filter(Boolean).join(" \xB7 ");
12579
13454
  const tone = provider.source === "unavailable" ? "warning" : "muted";
12580
- body.push(...wrapTextWithAnsi11(theme.fg(tone, ` ${provider.provider}${meta ? ` \xB7 ${meta}` : ""}${provider.reason ? ` \xB7 ${oneLine2(provider.reason)}` : ""}`), width));
13455
+ body.push(...wrapTextWithAnsi13(theme.fg(tone, ` ${provider.provider}${meta ? ` \xB7 ${meta}` : ""}${provider.reason ? ` \xB7 ${oneLine2(provider.reason)}` : ""}`), width));
12581
13456
  for (const limit of provider.limits) {
12582
13457
  body.push("");
12583
13458
  const reset = until(limit.resetsAt);
12584
- body.push(...wrapTextWithAnsi11(spread(theme.bold(` ${oneLine2(limit.label)}`), theme.fg("dim", reset), width), width));
13459
+ body.push(...wrapTextWithAnsi13(spread(theme.bold(` ${oneLine2(limit.label)}`), theme.fg("dim", reset), width), width));
12585
13460
  const used = limit.usedPercent ?? null;
12586
13461
  const remainingPercent = limit.remainingPercent ?? (used === null ? limit.remaining !== null && limit.remaining !== undefined && limit.limit !== null && limit.limit !== undefined && limit.limit > 0 ? Math.max(0, Math.min(100, limit.remaining / limit.limit * 100)) : null : Math.max(0, Math.min(100, 100 - used)));
12587
13462
  const rejected = limit.status === "rejected";
@@ -12589,7 +13464,7 @@ class UsagePage {
12589
13464
  const absolute = limit.remaining !== null && limit.remaining !== undefined ? `${limit.unit === "usd" ? money(limit.remaining) : limit.unit === "currency" ? `${limit.remaining.toFixed(2)} ${limit.detail ?? ""}`.trim() : count(limit.remaining)} left` : limit.limit !== null && limit.limit !== undefined ? `limit ${count(limit.limit)}${limit.unit && limit.unit !== "percent" ? ` ${limit.unit}` : ""}` : "";
12590
13465
  const value = remainingPercent === null ? absolute || "\u2014" : `${Math.round(remainingPercent)}% left`;
12591
13466
  const suffix = remainingPercent !== null && absolute ? ` ${theme.fg("dim", absolute)}` : "";
12592
- body.push(...wrapTextWithAnsi11(` ${bar(remainingPercent, barWidth, theme, rejected)} ${theme.fg(tone, value)}${suffix}`, width));
13467
+ body.push(...wrapTextWithAnsi13(` ${bar(remainingPercent, barWidth, theme, rejected)} ${theme.fg(tone, value)}${suffix}`, width));
12593
13468
  }
12594
13469
  }
12595
13470
  }
@@ -12599,7 +13474,7 @@ class UsagePage {
12599
13474
  if (local) {
12600
13475
  if (body.length)
12601
13476
  body.push("");
12602
- body.push(...wrapTextWithAnsi11(theme.fg("dim", `7d \xB7 ${count(local.inputTokens + local.cacheReadTokens + local.outputTokens)} tokens \xB7 ${count(local.requests)} requests \xB7 ${money(local.costUsd)}`), width));
13477
+ body.push(...wrapTextWithAnsi13(theme.fg("dim", `7d \xB7 ${count(local.inputTokens + local.cacheReadTokens + local.outputTokens)} tokens \xB7 ${count(local.requests)} requests \xB7 ${money(local.costUsd)}`), width));
12603
13478
  }
12604
13479
  return { header, body };
12605
13480
  }
@@ -12612,7 +13487,7 @@ class UsagePage {
12612
13487
  this.scrollRows = Math.max(1, bodyRows);
12613
13488
  this.maxOffset = Math.max(0, body.length - bodyRows);
12614
13489
  this.offset = Math.max(0, Math.min(this.offset, this.maxOffset));
12615
- return [...header, ...body.slice(this.offset, this.offset + bodyRows)].slice(0, rows).map((line) => truncateToWidth20(line, width, ""));
13490
+ return [...header, ...body.slice(this.offset, this.offset + bodyRows)].slice(0, rows).map((line) => truncateToWidth22(line, width, ""));
12616
13491
  }
12617
13492
  }
12618
13493
 
@@ -12786,19 +13661,31 @@ function createCommandPage(host, id, options = {}) {
12786
13661
  notify,
12787
13662
  changed: host.changed
12788
13663
  });
13664
+ case "plugins":
13665
+ return new PluginsPage({
13666
+ theme,
13667
+ call: host.control,
13668
+ changed: host.changed,
13669
+ ui: { select: host.ui.select.bind(host.ui), confirm: host.ui.confirm.bind(host.ui), notify, input: host.ui.input.bind(host.ui) }
13670
+ });
13671
+ case "hub":
13672
+ return new HubPage({
13673
+ theme,
13674
+ call: host.control,
13675
+ changed: host.changed,
13676
+ ui: { select: host.ui.select.bind(host.ui), confirm: host.ui.confirm.bind(host.ui), notify }
13677
+ });
12789
13678
  case "harness":
12790
13679
  case "skills":
12791
13680
  case "connections":
12792
13681
  case "tools":
12793
- case "plugins":
12794
- case "hub":
12795
13682
  case "components":
12796
13683
  return new BrowserPage({
12797
13684
  theme,
12798
13685
  call: host.control,
12799
13686
  changed: host.changed,
12800
- section: id === "harness" || id === "hub" ? "overview" : id === "components" ? "library" : id,
12801
- source: id === "hub" ? "discover" : "installed",
13687
+ section: id === "harness" ? "overview" : id === "components" ? "library" : id,
13688
+ source: "installed",
12802
13689
  ui: {
12803
13690
  select: host.ui.select.bind(host.ui),
12804
13691
  confirm: host.ui.confirm.bind(host.ui),
@@ -13410,7 +14297,7 @@ function createTerminalCommands(host) {
13410
14297
  }
13411
14298
  const target = resolve(host.cwd(), path);
13412
14299
  const within = relative2(host.sessionDirectory(), target);
13413
- if (within === ".." || within.startsWith("../") || within.startsWith("..\\") || isAbsolute2(within)) {
14300
+ if (within === ".." || within.startsWith("../") || within.startsWith("..\\") || isAbsolute3(within)) {
13414
14301
  throw new Error("Resume sessions from this account history folder.");
13415
14302
  }
13416
14303
  await host.runtime().switchSession(target);
@@ -13519,6 +14406,14 @@ function createTerminalCommands(host) {
13519
14406
  case "trajectory":
13520
14407
  await openPage("trajectory", { runId: argument });
13521
14408
  break;
14409
+ case "todo": {
14410
+ const items = await host.todo();
14411
+ if (!items.length)
14412
+ notify("No todo list in this reply yet. The agent writes one with todo_write for multi-step work.");
14413
+ else
14414
+ host.showTodo(items);
14415
+ break;
14416
+ }
13522
14417
  case "subagents":
13523
14418
  await openPage("subagents");
13524
14419
  break;
@@ -13609,7 +14504,7 @@ var PLAN_READ_ONLY_TOOLS = new Set([
13609
14504
  "find",
13610
14505
  "ls",
13611
14506
  SESSION_NAME,
13612
- CHECKLIST_WRITE,
14507
+ TODO_WRITE,
13613
14508
  ASK_USER,
13614
14509
  "ask_user_question",
13615
14510
  GET_CAPABILITIES,
@@ -13737,7 +14632,7 @@ function parseProposedPlan(text) {
13737
14632
  };
13738
14633
  }
13739
14634
  // src/terminal/tui/dialogs.ts
13740
- import { Editor, Input as Input2, matchesKey as matchesKey15, stripTerminalSequences as stripTerminalSequences11, truncateToWidth as truncateToWidth21, visibleWidth as visibleWidth13, wrapTextWithAnsi as wrapTextWithAnsi12 } from "@earendil-works/pi-tui";
14635
+ import { Editor, Input as Input2, matchesKey as matchesKey17, stripTerminalSequences as stripTerminalSequences11, truncateToWidth as truncateToWidth23, visibleWidth as visibleWidth13, wrapTextWithAnsi as wrapTextWithAnsi14 } from "@earendil-works/pi-tui";
13741
14636
  function selectTheme(theme) {
13742
14637
  return {
13743
14638
  selectedPrefix: (text) => selectionText(theme, text),
@@ -13810,7 +14705,7 @@ class SearchableSelect {
13810
14705
  return this.list.handleMouse(event);
13811
14706
  }
13812
14707
  handleInput(data) {
13813
- if (matchesKey15(data, "ctrl+c")) {
14708
+ if (matchesKey17(data, "ctrl+c")) {
13814
14709
  this.cancel();
13815
14710
  return;
13816
14711
  }
@@ -13827,7 +14722,7 @@ class SearchableSelect {
13827
14722
  this.list.handleInput(data === "j" ? "\x1B[B" : "\x1B[A");
13828
14723
  }
13829
14724
  render(width) {
13830
- return this.list.render(Math.max(4, width), this.windowRows() + this.searchRows()).map((line) => truncateToWidth21(line, width, ""));
14725
+ return this.list.render(Math.max(4, width), this.windowRows() + this.searchRows()).map((line) => truncateToWidth23(line, width, ""));
13831
14726
  }
13832
14727
  }
13833
14728
 
@@ -13883,8 +14778,8 @@ class DialogPanel {
13883
14778
  return mouseAware.handleMouse?.({ ...event, y: row });
13884
14779
  }
13885
14780
  handleInput(data) {
13886
- if (this.detailTotal > this.detailRows && (matchesKey15(data, "pageUp") || matchesKey15(data, "pageDown"))) {
13887
- const delta = matchesKey15(data, "pageUp") ? -1 : 1;
14781
+ if (this.detailTotal > this.detailRows && (matchesKey17(data, "pageUp") || matchesKey17(data, "pageDown"))) {
14782
+ const delta = matchesKey17(data, "pageUp") ? -1 : 1;
13888
14783
  this.detailOffset = Math.max(0, Math.min(this.detailTotal - this.detailRows, this.detailOffset + delta * Math.max(1, this.detailRows)));
13889
14784
  return;
13890
14785
  }
@@ -13895,7 +14790,7 @@ class DialogPanel {
13895
14790
  return [];
13896
14791
  const theme = this.theme();
13897
14792
  const inner = this.centered ? centerDialogContentWidth(width) : contentWidth(width);
13898
- const details = this.message ? wrapTextWithAnsi12(stripTerminalSequences11(this.message).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "").replace(/\t/g, " "), inner) : [];
14793
+ const details = this.message ? wrapTextWithAnsi14(stripTerminalSequences11(this.message).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "").replace(/\t/g, " "), inner) : [];
13899
14794
  const content = this.content.render(inner);
13900
14795
  const frameRows = this.centered ? CENTER_DIALOG_PAD * 2 + 2 : CHROME_ROWS;
13901
14796
  const maxHeight = this.maxRows ? Math.max(3, Math.floor(this.maxRows())) : details.length + content.length + frameRows;
@@ -14042,9 +14937,9 @@ class TerminalDialogs {
14042
14937
  editor.focused = value;
14043
14938
  },
14044
14939
  invalidate: () => editor.invalidate(),
14045
- render: (width) => editor.render(Math.max(4, width)).map((line) => truncateToWidth21(line, width, "")),
14940
+ render: (width) => editor.render(Math.max(4, width)).map((line) => truncateToWidth23(line, width, "")),
14046
14941
  handleInput: (data) => {
14047
- if (matchesKey15(data, "escape") || matchesKey15(data, "ctrl+c"))
14942
+ if (matchesKey17(data, "escape") || matchesKey17(data, "ctrl+c"))
14048
14943
  done(undefined);
14049
14944
  else
14050
14945
  editor.handleInput(data);
@@ -14114,7 +15009,7 @@ class TerminalDialogs {
14114
15009
 
14115
15010
  // src/terminal/tui/keybindings.ts
14116
15011
  import { readFileSync as readFileSync7 } from "fs";
14117
- import { join as join13 } from "path";
15012
+ import { join as join14 } from "path";
14118
15013
  import { KeybindingsManager, TUI_KEYBINDINGS } from "@earendil-works/pi-tui";
14119
15014
  var actions = {
14120
15015
  "app.commands": "ctrl+k",
@@ -14171,7 +15066,7 @@ var HARNESS_KEYBINDINGS = {
14171
15066
  function readBindings(agentDir) {
14172
15067
  let bindings = {};
14173
15068
  try {
14174
- const source = JSON.parse(readFileSync7(join13(agentDir, "keybindings.json"), "utf8"));
15069
+ const source = JSON.parse(readFileSync7(join14(agentDir, "keybindings.json"), "utf8"));
14175
15070
  if (source && typeof source === "object" && !Array.isArray(source)) {
14176
15071
  bindings = Object.fromEntries(Object.entries(source).filter(([, value]) => typeof value === "string" || Array.isArray(value) && value.every((key) => typeof key === "string")));
14177
15072
  }
@@ -14186,11 +15081,11 @@ function terminalKeybindings(agentDir) {
14186
15081
  }
14187
15082
 
14188
15083
  // src/terminal/tui/piAdapter.ts
14189
- import { join as join15 } from "path";
15084
+ import { join as join16 } from "path";
14190
15085
  import { pathToFileURL as pathToFileURL2 } from "url";
14191
15086
 
14192
15087
  // src/pi/runtime.ts
14193
- import { join as join14 } from "path";
15088
+ import { join as join15 } from "path";
14194
15089
  import { pathToFileURL } from "url";
14195
15090
  var PINNED_PI_VERSION = "1.4.0";
14196
15091
  var HTTP_DISPATCHER_CHUNK = "dist/bundle/chunks/chunk-MM3LKT73.js";
@@ -14219,7 +15114,7 @@ function createPiLoader(importPi, importDispatcher) {
14219
15114
  return loading;
14220
15115
  };
14221
15116
  }
14222
- var loadPi = createPiLoader(() => import("@earendil-works/pi-coding-agent"), async (packageDir) => await import(pathToFileURL(join14(packageDir, HTTP_DISPATCHER_CHUNK)).href));
15117
+ var loadPi = createPiLoader(() => import("@earendil-works/pi-coding-agent"), async (packageDir) => await import(pathToFileURL(join15(packageDir, HTTP_DISPATCHER_CHUNK)).href));
14223
15118
 
14224
15119
  // src/terminal/tui/piAdapter.ts
14225
15120
  var adapter;
@@ -14234,10 +15129,10 @@ function loadPiAdapter() {
14234
15129
  assertPiAdapterVersion(pi.VERSION);
14235
15130
  const root = pi.getPackageDir();
14236
15131
  const [theme, renderers, clipboard, images] = await Promise.all([
14237
- import(pathToFileURL2(join15(root, "dist/modes/interactive/theme/theme.js")).href),
14238
- import(pathToFileURL2(join15(root, "dist/core/tools/renderers/index.js")).href),
14239
- import(pathToFileURL2(join15(root, "dist/utils/clipboard.js")).href),
14240
- import(pathToFileURL2(join15(root, "dist/utils/clipboard-image.js")).href)
15132
+ import(pathToFileURL2(join16(root, "dist/modes/interactive/theme/theme.js")).href),
15133
+ import(pathToFileURL2(join16(root, "dist/core/tools/renderers/index.js")).href),
15134
+ import(pathToFileURL2(join16(root, "dist/utils/clipboard.js")).href),
15135
+ import(pathToFileURL2(join16(root, "dist/utils/clipboard-image.js")).href)
14241
15136
  ]);
14242
15137
  if (typeof theme.setThemeInstance !== "function" || typeof renderers.withBuiltInRenderers !== "function" || typeof clipboard.readClipboardText !== "function" || typeof clipboard.readClipboardFilePaths !== "function" || typeof images.readClipboardImage !== "function" || typeof images.extensionForImageMimeType !== "function") {
14243
15138
  throw new Error("The pinned Pi terminal adapter no longer exposes its required presentation hooks.");
@@ -14271,10 +15166,10 @@ import {
14271
15166
  SettingsManager
14272
15167
  } from "@earendil-works/pi-coding-agent";
14273
15168
  import { closeSync as closeSync2, existsSync as existsSync10, openSync as openSync2, readSync, realpathSync as realpathSync4 } from "fs";
14274
- import { isAbsolute as isAbsolute4, join as join18, relative as relative4, resolve as resolve4, sep as sep5 } from "path";
15169
+ import { isAbsolute as isAbsolute5, join as join19, relative as relative4, resolve as resolve4, sep as sep5 } from "path";
14275
15170
 
14276
15171
  // src/terminal/piExtension.ts
14277
- import { createBashTool, createEditTool, createWriteTool } from "@earendil-works/pi-coding-agent";
15172
+ import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from "@earendil-works/pi-coding-agent";
14278
15173
  import { Type as Type2 } from "typebox";
14279
15174
 
14280
15175
  // src/core/commandBudget.ts
@@ -14324,13 +15219,13 @@ async function withCommandBudget(run, abort, seconds) {
14324
15219
 
14325
15220
  // src/sessions/compactionCheckpoint.ts
14326
15221
  var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
14327
- function validatedChecklist(value) {
14328
- if (!Array.isArray(value) || value.length > MAX_CHECKLIST_ITEMS)
15222
+ function validatedTodo(value) {
15223
+ if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS)
14329
15224
  return null;
14330
15225
  const items = [];
14331
15226
  for (const valueItem of value) {
14332
15227
  const item = valueItem;
14333
- const parsed = checklistItemSchema.safeParse({
15228
+ const parsed = todoItemSchema.safeParse({
14334
15229
  text: typeof item?.text === "string" ? item.text.trim() : item?.text,
14335
15230
  status: item?.status
14336
15231
  });
@@ -14343,12 +15238,12 @@ function validatedChecklist(value) {
14343
15238
  function compactionCheckpoint(options = {}) {
14344
15239
  return (pi) => {
14345
15240
  let goal = "";
14346
- let checklist = validatedChecklist(options.initialChecklist ?? []) ?? [];
15241
+ let todo = validatedTodo(options.initialTodo ?? []) ?? [];
14347
15242
  let lastCompactionKey = null;
14348
15243
  let compacted = false;
14349
15244
  const reset = () => {
14350
15245
  goal = "";
14351
- checklist = validatedChecklist(options.initialChecklist ?? []) ?? [];
15246
+ todo = validatedTodo(options.initialTodo ?? []) ?? [];
14352
15247
  lastCompactionKey = null;
14353
15248
  compacted = false;
14354
15249
  };
@@ -14356,16 +15251,16 @@ function compactionCheckpoint(options = {}) {
14356
15251
  if (event.messageType !== "user")
14357
15252
  return;
14358
15253
  goal = event.prompt;
14359
- checklist = validatedChecklist(options.initialChecklist ?? []) ?? [];
15254
+ todo = validatedTodo(options.initialTodo ?? []) ?? [];
14360
15255
  lastCompactionKey = null;
14361
15256
  compacted = false;
14362
15257
  });
14363
15258
  pi.on("tool_result", (event) => {
14364
- if (event.toolName !== CHECKLIST_WRITE || event.isError)
15259
+ if (event.toolName !== TODO_WRITE || event.isError)
14365
15260
  return;
14366
- const items = validatedChecklist(event.input.items);
15261
+ const items = validatedTodo(event.input.items);
14367
15262
  if (items)
14368
- checklist = items;
15263
+ todo = items;
14369
15264
  });
14370
15265
  pi.on("context", (event) => {
14371
15266
  const summary = event.messages.findLast((message) => message.role === "compactionSummary");
@@ -14380,7 +15275,7 @@ function compactionCheckpoint(options = {}) {
14380
15275
  }
14381
15276
  if (!compacted)
14382
15277
  return;
14383
- const content = renderCompactionCheckpoint({ goal, checklist, planInstructions });
15278
+ const content = renderCompactionCheckpoint({ goal, todo, planInstructions });
14384
15279
  if (!content)
14385
15280
  return;
14386
15281
  return {
@@ -14439,26 +15334,26 @@ function parseInitialBashCommand(input) {
14439
15334
  }
14440
15335
 
14441
15336
  // src/core/workspace.ts
14442
- import { lstatSync as lstatSync2, realpathSync, statSync as statSync4 } from "fs";
14443
- import { homedir } from "os";
14444
- import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute3, join as join16, relative as relative3, resolve as resolve2, sep as sep4 } from "path";
15337
+ import { lstatSync as lstatSync2, realpathSync, statSync as statSync5 } from "fs";
15338
+ import { homedir as homedir2 } from "os";
15339
+ import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute4, join as join17, relative as relative3, resolve as resolve2, sep as sep4 } from "path";
14445
15340
  function resolveWritePath(candidate, cwd = process.cwd()) {
14446
- const expanded = candidate === "~" ? homedir() : candidate.startsWith("~/") || candidate.startsWith("~\\") ? join16(homedir(), candidate.slice(2)) : candidate;
14447
- const absoluteCandidate = isAbsolute3(expanded) ? resolve2(expanded) : resolve2(cwd, expanded);
15341
+ const expanded = candidate === "~" ? homedir2() : candidate.startsWith("~/") || candidate.startsWith("~\\") ? join17(homedir2(), candidate.slice(2)) : candidate;
15342
+ const absoluteCandidate = isAbsolute4(expanded) ? resolve2(expanded) : resolve2(cwd, expanded);
14448
15343
  return canonicalizeTarget(absoluteCandidate);
14449
15344
  }
14450
15345
  function isWithinWorkspace(workspaceRoot, canonicalCandidate) {
14451
15346
  const canonicalRoot = realpathSync(workspaceRoot);
14452
- if (!statSync4(canonicalRoot).isDirectory())
15347
+ if (!statSync5(canonicalRoot).isDirectory())
14453
15348
  throw invalid("The workspace root must be a directory.");
14454
15349
  const fromRoot = relative3(canonicalRoot, canonicalCandidate);
14455
- return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep4}`) && !isAbsolute3(fromRoot);
15350
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep4}`) && !isAbsolute4(fromRoot);
14456
15351
  }
14457
15352
  function verifyWorkspaceRoot(workspaceRoot) {
14458
15353
  const canonicalRoot = realpathSync(workspaceRoot);
14459
15354
  if (dirname5(canonicalRoot) === canonicalRoot)
14460
15355
  throw invalid("The filesystem root cannot be used as an active project workspace.");
14461
- if (canonicalRoot !== resolve2(workspaceRoot) || !statSync4(canonicalRoot).isDirectory()) {
15356
+ if (canonicalRoot !== resolve2(workspaceRoot) || !statSync5(canonicalRoot).isDirectory()) {
14462
15357
  throw invalid("The active project workspace changed after this run began.");
14463
15358
  }
14464
15359
  return canonicalRoot;
@@ -14508,23 +15403,93 @@ function inspectWorkspaceWrite(workspaceRoot, cwd, tool, input) {
14508
15403
  // src/core/workspaceBashTool.ts
14509
15404
  import { Type } from "typebox";
14510
15405
 
15406
+ // src/core/shell.ts
15407
+ import { accessSync as accessSync2, constants as constants2, statSync as statSync6 } from "fs";
15408
+ import { win32 } from "path";
15409
+ var POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"];
15410
+ var POWERSHELL_UTF8 = `try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}
15411
+ `;
15412
+ var cached2;
15413
+ function hostShell() {
15414
+ if (cached2 === undefined)
15415
+ cached2 = resolveShell(process.platform);
15416
+ return cached2;
15417
+ }
15418
+ function resolveShell(platform, lookup = {}) {
15419
+ const env = lookup.env ?? process.env;
15420
+ const exists = lookup.exists ?? isFile;
15421
+ const onPath = lookup.onPath ?? findBinary;
15422
+ if (platform !== "win32") {
15423
+ const executable = ["/bin/bash", "/usr/bin/bash"].find((candidate) => exists(candidate));
15424
+ return executable ? { kind: "bash", executable, args: ["-c"] } : null;
15425
+ }
15426
+ const gitBash = [env.ProgramFiles, env["ProgramFiles(x86)"]].filter((folder) => Boolean(folder)).map((folder) => win32.join(folder, "Git", "bin", "bash.exe")).find((candidate) => exists(candidate));
15427
+ if (gitBash)
15428
+ return { kind: "bash", executable: gitBash, args: ["-c"] };
15429
+ const bashOnPath = onPath("bash.exe");
15430
+ if (bashOnPath && !isWslLauncher(bashOnPath, env))
15431
+ return { kind: "bash", executable: bashOnPath, args: ["-c"] };
15432
+ const existing = (folder, ...parts) => {
15433
+ if (!folder)
15434
+ return null;
15435
+ const candidate = win32.join(folder, ...parts);
15436
+ return exists(candidate) ? candidate : null;
15437
+ };
15438
+ const powershell = onPath("pwsh.exe") ?? existing(env.ProgramFiles, "PowerShell", "7", "pwsh.exe") ?? onPath("powershell.exe") ?? existing(env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15439
+ return powershell ? { kind: "powershell", executable: powershell, args: POWERSHELL_ARGS } : null;
15440
+ }
15441
+ function shellArguments(shell, command) {
15442
+ return [...shell.args, shell.kind === "powershell" ? `${POWERSHELL_UTF8}${command}` : command];
15443
+ }
15444
+ function shellLabel(shell) {
15445
+ if (shell.kind === "powershell")
15446
+ return "PowerShell";
15447
+ return /\\git\\/iu.test(shell.executable) ? "Git Bash" : "Bash";
15448
+ }
15449
+ function isWslLauncher(path, env) {
15450
+ const normalized = win32.normalize(path).toLowerCase();
15451
+ const systemRoot = env.SystemRoot;
15452
+ if (systemRoot) {
15453
+ return ["System32", "Sysnative"].some((folder) => normalized === win32.join(systemRoot, folder, "bash.exe").toLowerCase());
15454
+ }
15455
+ return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/u.test(normalized);
15456
+ }
15457
+ function isFile(path) {
15458
+ try {
15459
+ accessSync2(path, constants2.F_OK);
15460
+ return statSync6(path).isFile();
15461
+ } catch {
15462
+ return false;
15463
+ }
15464
+ }
15465
+
14511
15466
  // src/core/workspaceSandbox.ts
14512
15467
  import { AsyncLocalStorage } from "async_hooks";
14513
15468
  import { spawn } from "child_process";
14514
15469
  import { randomUUID as randomUUID2 } from "crypto";
14515
- import { accessSync, constants, existsSync as existsSync9, lstatSync as lstatSync3, mkdtempSync, realpathSync as realpathSync2, readdirSync as readdirSync7, rmSync as rmSync6, statSync as statSync5 } from "fs";
15470
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync9, lstatSync as lstatSync3, mkdtempSync, realpathSync as realpathSync2, readdirSync as readdirSync7, rmSync as rmSync6, statSync as statSync7 } from "fs";
14516
15471
  import { tmpdir } from "os";
14517
- import { join as join17 } from "path";
15472
+ import { join as join18 } from "path";
15473
+ import { spawnSync } from "child_process";
14518
15474
  var MARKER_PREFIX = "__XINGULAR_WORKSPACE_SANDBOX_STARTED_";
14519
15475
  var bashWriteScope = new AsyncLocalStorage;
14520
15476
  function runWithWorkspaceWriteScope(scope, action) {
14521
15477
  return bashWriteScope.run(scope, action);
14522
15478
  }
15479
+ var WINDOWS_UNSANDBOXED_REASON = "Windows has no workspace write-boundary sandbox, so this command would run without one. It will not run until this one-call request is approved.";
15480
+ var SANDBOX_BACKENDS = {
15481
+ darwin: ["/usr/bin/sandbox-exec"],
15482
+ linux: ["/usr/bin/bwrap", "/bin/bwrap"]
15483
+ };
15484
+ function workspaceSandboxBackend(platform = process.platform) {
15485
+ const candidates = SANDBOX_BACKENDS[platform];
15486
+ return candidates ? systemExecutable(candidates) : null;
15487
+ }
14523
15488
  function systemExecutable(candidates) {
14524
15489
  for (const candidate of candidates) {
14525
15490
  try {
14526
- accessSync(candidate, constants.X_OK);
14527
- if (statSync5(candidate).isFile())
15491
+ accessSync3(candidate, constants3.X_OK);
15492
+ if (statSync7(candidate).isFile())
14528
15493
  return candidate;
14529
15494
  } catch {}
14530
15495
  }
@@ -14536,6 +15501,16 @@ function shellPath() {
14536
15501
  throw new Error("The workspace write-boundary sandbox requires Bash, but no Bash executable was found.");
14537
15502
  return shell;
14538
15503
  }
15504
+ function requireShell(resolve) {
15505
+ const shell = resolve();
15506
+ if (!shell) {
15507
+ throw new Error(process.platform === "win32" ? "No shell was found: neither Git Bash nor PowerShell is available. Bash was not run." : "No Bash executable was found. Bash was not run.");
15508
+ }
15509
+ return shell;
15510
+ }
15511
+ function unconfinedCommand(shell, command) {
15512
+ return { executable: shell.executable, args: shellArguments(shell, command), marker: "" };
15513
+ }
14539
15514
  function sbplString(value) {
14540
15515
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll(`
14541
15516
  `, "\\n")}"`;
@@ -14579,13 +15554,13 @@ ${command}`;
14579
15554
  return readOnlySandboxCommand(platform, realpathSync2(readOnlyTempRoot), cwd, shell, wrappedCommand, marker);
14580
15555
  const writeRoots = [...new Set([workspaceRoot, ...additionalWriteRoots].map((root) => realpathSync2(root)))].sort((left, right) => left.length - right.length);
14581
15556
  if (platform === "darwin") {
14582
- const executable = systemExecutable(["/usr/bin/sandbox-exec"]);
15557
+ const executable = workspaceSandboxBackend(platform);
14583
15558
  if (!executable)
14584
15559
  throw new Error("Workspace write-boundary sandbox unavailable: sandbox-exec was not found. Bash was not run.");
14585
15560
  return { executable, args: ["-p", macSandboxProfile(writeRoots), shell, "-c", wrappedCommand], marker };
14586
15561
  }
14587
15562
  if (platform === "linux") {
14588
- const executable = systemExecutable(["/usr/bin/bwrap", "/bin/bwrap"]);
15563
+ const executable = workspaceSandboxBackend(platform);
14589
15564
  if (!executable)
14590
15565
  throw new Error("Workspace write-boundary sandbox unavailable: install bubblewrap (bwrap). Bash was not run.");
14591
15566
  return {
@@ -14615,13 +15590,13 @@ ${command}`;
14615
15590
  }
14616
15591
  function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand, marker) {
14617
15592
  if (platform === "darwin") {
14618
- const executable = systemExecutable(["/usr/bin/sandbox-exec"]);
15593
+ const executable = workspaceSandboxBackend(platform);
14619
15594
  if (!executable)
14620
15595
  throw new Error("Read-only Bash sandbox unavailable: sandbox-exec was not found. Bash was not run.");
14621
15596
  return { executable, args: ["-p", macReadOnlyProfile(tempRoot), shell, "-c", wrappedCommand], marker };
14622
15597
  }
14623
15598
  if (platform === "linux") {
14624
- const executable = systemExecutable(["/usr/bin/bwrap", "/bin/bwrap"]);
15599
+ const executable = workspaceSandboxBackend(platform);
14625
15600
  if (!executable)
14626
15601
  throw new Error("Read-only Bash sandbox unavailable: install bubblewrap (bwrap). Bash was not run.");
14627
15602
  return {
@@ -14656,7 +15631,11 @@ function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand,
14656
15631
  throw new Error(`Read-only Bash sandbox unavailable on ${platform}. Bash was not run.`);
14657
15632
  }
14658
15633
  function killProcess(child) {
14659
- if (child.pid && process.platform !== "win32") {
15634
+ if (child.pid && process.platform === "win32") {
15635
+ spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
15636
+ return;
15637
+ }
15638
+ if (child.pid) {
14660
15639
  try {
14661
15640
  process.kill(-child.pid, "SIGKILL");
14662
15641
  return;
@@ -14793,9 +15772,12 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
14793
15772
  if (!isWithinWorkspace(root, canonicalCwd))
14794
15773
  throw new Error("The Bash working directory is outside the active project workspace.");
14795
15774
  const scope = options.workspaceWriteScope?.() ?? bashWriteScope.getStore() ?? "workspace";
15775
+ const platform = options.platform ?? process.platform;
15776
+ const shell = options.shell ?? hostShell;
14796
15777
  if (scope === "read-only") {
14797
- const platform = options.platform ?? process.platform;
14798
- const tempRoot = realpathSync2(mkdtempSync(join17(tmpdir(), "harness-read-only-bash-")));
15778
+ if (platform === "win32")
15779
+ throw new Error("Plan-mode Bash needs a read-only sandbox, which Windows does not have. Bash was not run.");
15780
+ const tempRoot = realpathSync2(mkdtempSync(join18(tmpdir(), "harness-read-only-bash-")));
14799
15781
  try {
14800
15782
  const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command, [], tempRoot);
14801
15783
  const env = { ...execOptions.env ?? process.env, TMPDIR: tempRoot };
@@ -14812,11 +15794,12 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
14812
15794
  bypass = false;
14813
15795
  }
14814
15796
  if (bypass) {
14815
- const result = await runProcess({ executable: shellPath(), args: ["-c", command], marker: "" }, canonicalCwd, execOptions, null);
15797
+ const result = await runProcess(unconfinedCommand(requireShell(shell), command), canonicalCwd, execOptions, null);
14816
15798
  return { exitCode: result.exitCode };
14817
15799
  }
14818
- if (scope === "outside") {
14819
- const reason = "This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved.";
15800
+ if (scope === "outside" || platform === "win32") {
15801
+ const resolved = requireShell(shell);
15802
+ const reason = scope === "outside" ? "This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved." : WINDOWS_UNSANDBOXED_REASON;
14820
15803
  let approved = false;
14821
15804
  try {
14822
15805
  approved = await options.requestWorkspaceWriteScope?.({ command, cwd: canonicalCwd, workspaceRoot: root, reason }) ?? false;
@@ -14825,14 +15808,14 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
14825
15808
  }
14826
15809
  if (!approved)
14827
15810
  throw new Error(`${reason} Bash was not run.`);
14828
- execOptions.onData(Buffer.from(`
15811
+ if (scope === "outside")
15812
+ execOptions.onData(Buffer.from(`
14829
15813
  [Outside-workspace writes approved for this command; running once.]
14830
15814
  `));
14831
- const result = await runProcess({ executable: shellPath(), args: ["-c", command], marker: "" }, canonicalCwd, execOptions, null);
15815
+ const result = await runProcess(unconfinedCommand(resolved, command), canonicalCwd, execOptions, null);
14832
15816
  return { exitCode: result.exitCode };
14833
15817
  }
14834
15818
  assertNoExternalHardlinks(root);
14835
- const platform = options.platform ?? process.platform;
14836
15819
  const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command);
14837
15820
  const result = await runProcess(spec, canonicalCwd, execOptions, spec.marker);
14838
15821
  return { exitCode: result.exitCode };
@@ -14841,12 +15824,17 @@ function createWorkspaceBashOperations(workspaceRoot, options = {}) {
14841
15824
  }
14842
15825
 
14843
15826
  // src/core/workspaceBashTool.ts
14844
- function withWorkspaceWriteScope(tool) {
15827
+ function createWorkspaceShellTool(pi, cwd, operations, shell = hostShell(), platform = process.platform) {
15828
+ const native = shell?.kind === "powershell" ? { ...pi.createPowerShellTool(cwd, { operations }), name: "bash" } : pi.createBashTool(cwd, { operations });
15829
+ const note = platform === "win32" && shell ? ` On this machine commands run in ${shellLabel(shell)}. Windows has no workspace sandbox, so each command asks for approval unless Full Permission is on, and plan mode cannot run commands.` : "";
15830
+ return withWorkspaceWriteScope(native, note);
15831
+ }
15832
+ function withWorkspaceWriteScope(tool, note = "") {
14845
15833
  const native = tool;
14846
15834
  const execute = native.execute.bind(tool);
14847
15835
  return {
14848
15836
  ...tool,
14849
- description: `${native.description} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
15837
+ description: `${native.description}${note} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
14850
15838
  parameters: Type.Object({
14851
15839
  command: Type.String({ description: "Shell command to execute" }),
14852
15840
  timeout: Type.Optional(Type.Number({ description: `Timeout in seconds (default ${DEFAULT_COMMAND_TIMEOUT_SECONDS})` })),
@@ -14873,7 +15861,7 @@ function withWorkspaceWriteScope(tool) {
14873
15861
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
14874
15862
  import { spawn as spawn2 } from "child_process";
14875
15863
  import { createHash as createHash2 } from "crypto";
14876
- import { constants as fsConstants, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
15864
+ import { constants as fsConstants, realpathSync as realpathSync3, statSync as statSync8 } from "fs";
14877
15865
  import { access, readFile } from "fs/promises";
14878
15866
  import { dirname as dirname6, resolve as resolve3 } from "path";
14879
15867
  var writeAuthorization = new AsyncLocalStorage2;
@@ -14922,6 +15910,23 @@ try {
14922
15910
  throw error
14923
15911
  }
14924
15912
  `;
15913
+ function atomicWriteLaunch(platform, root, additionalRoots) {
15914
+ if (platform === "win32") {
15915
+ const env = {};
15916
+ for (const name of ["SystemRoot", "windir", "TEMP", "TMP", "USERPROFILE"]) {
15917
+ if (process.env[name])
15918
+ env[name] = process.env[name];
15919
+ }
15920
+ return { executable: process.execPath, args: ["-e", atomicWriteScript], env };
15921
+ }
15922
+ const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
15923
+ const spec = workspaceSandboxCommand(platform, root, root, command, additionalRoots);
15924
+ return {
15925
+ executable: spec.executable,
15926
+ args: spec.args,
15927
+ env: { PATH: "/usr/bin:/bin", ...process.env.HOME ? { HOME: process.env.HOME } : {} }
15928
+ };
15929
+ }
14925
15930
  function shellQuote(value) {
14926
15931
  return `'${value.replaceAll("'", "'\\''")}'`;
14927
15932
  }
@@ -14930,7 +15935,7 @@ function nearestExistingDirectory(path) {
14930
15935
  while (true) {
14931
15936
  try {
14932
15937
  const canonical = realpathSync3(current);
14933
- if (!statSync6(canonical).isDirectory())
15938
+ if (!statSync8(canonical).isDirectory())
14934
15939
  throw new Error(`Write scope is not a directory: ${canonical}`);
14935
15940
  return canonical;
14936
15941
  } catch (error) {
@@ -14962,17 +15967,12 @@ async function runAtomicWrite(authorization, path, content) {
14962
15967
  throw new Error("The write authorization no longer matches the active workspace target.");
14963
15968
  }
14964
15969
  const additionalRoots = outside ? [nearestExistingDirectory(canonicalPath)] : [];
14965
- const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
14966
- const spec = workspaceSandboxCommand(process.platform, root, root, command, additionalRoots);
14967
- const env = {
14968
- PATH: "/usr/bin:/bin",
14969
- ...process.env.HOME ? { HOME: process.env.HOME } : {}
14970
- };
15970
+ const launch = atomicWriteLaunch(process.platform, root, additionalRoots);
14971
15971
  await new Promise((resolvePromise, rejectPromise) => {
14972
- const child = spawn2(spec.executable, spec.args, {
15972
+ const child = spawn2(launch.executable, launch.args, {
14973
15973
  cwd: root,
14974
15974
  detached: process.platform !== "win32",
14975
- env,
15975
+ env: launch.env,
14976
15976
  stdio: ["pipe", "pipe", "pipe"],
14977
15977
  windowsHide: true
14978
15978
  });
@@ -15095,6 +16095,7 @@ async function piExtension_default(pi, planRun, getSession) {
15095
16095
  let context = null;
15096
16096
  let nativeSubagents = false;
15097
16097
  let subagentParent = false;
16098
+ let stoppedChildren = null;
15098
16099
  let timer = null;
15099
16100
  let polling = false;
15100
16101
  let failure;
@@ -15179,14 +16180,14 @@ async function piExtension_default(pi, planRun, getSession) {
15179
16180
  },
15180
16181
  workspaceWriteScope: () => planRun?.active ? "read-only" : undefined
15181
16182
  });
15182
- pi.registerTool(withWorkspaceWriteScope(createBashTool(process.cwd(), { operations: workspaceBash })));
16183
+ pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
15183
16184
  pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
15184
16185
  pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
15185
16186
  compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
15186
16187
  const slicePolicy = parseWorkSlice(undefined);
15187
16188
  const sliceState = createSliceState(slicePolicy);
15188
16189
  if (slicePolicy.enabled)
15189
- workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
16190
+ workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
15190
16191
  pi.on("context", (event) => {
15191
16192
  const messages = pruneToolImages(event.messages);
15192
16193
  return messages === event.messages ? undefined : { messages };
@@ -15195,7 +16196,7 @@ async function piExtension_default(pi, planRun, getSession) {
15195
16196
  context = ctx;
15196
16197
  if (timer)
15197
16198
  clearInterval(timer);
15198
- await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile() });
16199
+ await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
15199
16200
  timer = setInterval(() => {
15200
16201
  if (polling || !context)
15201
16202
  return;
@@ -15223,15 +16224,19 @@ async function piExtension_default(pi, planRun, getSession) {
15223
16224
  usage = emptyUsage();
15224
16225
  reasoningTokens = undefined;
15225
16226
  costReported = false;
15226
- const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
16227
+ const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "", model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel?.() ?? null });
15227
16228
  workspaceRoot = typeof begun.workspaceRoot === "string" ? begun.workspaceRoot : null;
15228
16229
  if (!workspaceRoot)
15229
16230
  throw new Error("The active project workspace could not be resolved; the turn was not started.");
16231
+ pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
16232
+ stoppedChildren = null;
15230
16233
  if (nativeSubagents) {
15231
- pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
15232
16234
  const snapshot = await controlCall("terminal.subagents.register", { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), plan: planRun?.active === true, history: subagentHistory(ctx.sessionManager.getBranch()) });
15233
16235
  if (planRun && Array.isArray(snapshot.connectors))
15234
16236
  planRun.connectors = snapshot.connectors;
16237
+ if (planRun && snapshot.subagentProfiles)
16238
+ planRun.subagentProfiles = snapshot.subagentProfiles;
16239
+ stoppedChildren = snapshot.notice ?? null;
15235
16240
  subagentParent = true;
15236
16241
  }
15237
16242
  if (planRun && event.messageType === "user") {
@@ -15294,6 +16299,12 @@ async function piExtension_default(pi, planRun, getSession) {
15294
16299
  const instruction = refusal ? `The user's initial Bash command was not run. Reason: ${refusal}. Do not claim that it ran; explain the limitation and ask how to proceed.` : "The user\u2019s initial Bash command has already run. Read its native Bash result and explain what it shows. Do not run the same command again unless the user explicitly asks.";
15295
16300
  return { message: { customType: "xingular-initial-bash", content: instruction, display: false, details: {} } };
15296
16301
  });
16302
+ pi.on("before_agent_start", async () => {
16303
+ const notice = stoppedChildren;
16304
+ stoppedChildren = null;
16305
+ if (notice)
16306
+ return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
16307
+ });
15297
16308
  const deliverChildren = async (event, ctx, settle) => {
15298
16309
  if (!nativeSubagents || settle && event.outcome !== "completed" || ctx.signal?.aborted)
15299
16310
  return;
@@ -15467,17 +16478,17 @@ async function piExtension_default(pi, planRun, getSession) {
15467
16478
  }
15468
16479
  });
15469
16480
  let recorded = Promise.resolve();
15470
- const checklist = checklistTool({ emit: (_type, payload) => {
15471
- recorded = terminalCall("checklist", payload);
16481
+ const todo = todoTool({ emit: (_type, payload) => {
16482
+ recorded = terminalCall("todo", payload);
15472
16483
  } });
15473
16484
  pi.registerTool({
15474
- name: checklist.name,
15475
- label: checklist.label,
15476
- description: checklist.description,
15477
- parameters: Type2.Unsafe(checklist.parameters(Type2)),
16485
+ name: todo.name,
16486
+ label: todo.label,
16487
+ description: todo.description,
16488
+ parameters: Type2.Unsafe(todo.parameters(Type2)),
15478
16489
  async execute(_id, input) {
15479
16490
  recorded = Promise.resolve();
15480
- const result = await checklist.execute(input);
16491
+ const result = await todo.execute(input);
15481
16492
  await recorded;
15482
16493
  return { content: [{ type: "text", text: typeof result === "string" ? result : result.text }], details: undefined };
15483
16494
  }
@@ -15698,7 +16709,7 @@ function parseTerminalArguments(args) {
15698
16709
  }
15699
16710
  function assertTerminalHistoryPath(path, sessionDirectory) {
15700
16711
  const within = relative4(realpathSync4(sessionDirectory), realpathSync4(path));
15701
- if (!within || within === ".." || within.startsWith(`..${sep5}`) || isAbsolute4(within)) {
16712
+ if (!within || within === ".." || within.startsWith(`..${sep5}`) || isAbsolute5(within)) {
15702
16713
  throw new Error("Resume only from this terminal history folder. Use /import for another native session file in this working folder.");
15703
16714
  }
15704
16715
  }
@@ -15726,7 +16737,7 @@ function assertTerminalSessionScope(path, cwd, cwdOverride) {
15726
16737
  } catch {
15727
16738
  throw new Error("This file does not have a readable native session header.");
15728
16739
  }
15729
- if (header.type !== "session" || !header.cwd || !isAbsolute4(header.cwd)) {
16740
+ if (header.type !== "session" || !header.cwd || !isAbsolute5(header.cwd)) {
15730
16741
  throw new Error("The native session must record its original absolute working folder.");
15731
16742
  }
15732
16743
  if (realpathSync4(header.cwd) !== realpathSync4(cwd)) {
@@ -15737,7 +16748,7 @@ async function createTerminalRuntime(args, host) {
15737
16748
  const prepared = parseTerminalArguments(args);
15738
16749
  const cwd = realpathSync4(process.cwd());
15739
16750
  const agentDirValue = process.env.PI_CODING_AGENT_DIR;
15740
- if (!agentDirValue || !isAbsolute4(agentDirValue))
16751
+ if (!agentDirValue || !isAbsolute5(agentDirValue))
15741
16752
  throw new Error("The Harness terminal requires its isolated account directory.");
15742
16753
  const agentDir = realpathSync4(agentDirValue);
15743
16754
  const sessionDirectory = realpathSync4(resolve4(cwd, prepared.sessionDir));
@@ -15751,7 +16762,7 @@ async function createTerminalRuntime(args, host) {
15751
16762
  const identity = await controlCall("accounts.session").catch(() => null);
15752
16763
  const account = identity && typeof identity.accountId === "string" && typeof identity.label === "string" ? Object.freeze({ accountId: identity.accountId, label: identity.label }) : null;
15753
16764
  await loadPiAdapter();
15754
- const modelRuntime = await ModelRuntime.create({ authPath: join18(agentDir, "auth.json"), modelsPath: join18(agentDir, "models.json") });
16765
+ const modelRuntime = await ModelRuntime.create({ authPath: join19(agentDir, "auth.json"), modelsPath: join19(agentDir, "models.json") });
15755
16766
  observeProviderResponses(modelRuntime, (provider, response) => {
15756
16767
  const headers = limitHeaders(response.headers);
15757
16768
  if (!Object.keys(headers).length)
@@ -16143,7 +17154,7 @@ async function createTerminalRuntime(args, host) {
16143
17154
  }
16144
17155
 
16145
17156
  // src/terminal/tui/sideQuestion.ts
16146
- import { truncateToWidth as truncateToWidth22, visibleWidth as visibleWidth14, wrapTextWithAnsi as wrapTextWithAnsi13 } from "@earendil-works/pi-tui";
17157
+ import { truncateToWidth as truncateToWidth24, visibleWidth as visibleWidth14, wrapTextWithAnsi as wrapTextWithAnsi15 } from "@earendil-works/pi-tui";
16147
17158
  var QUESTION_ROWS = 2;
16148
17159
  var ANSWER_ROWS = 8;
16149
17160
  var MAX_CLIENT_CONTEXT_CHARS = 300000;
@@ -16352,17 +17363,17 @@ class SideQuestion {
16352
17363
  const spinner = this.current === "thinking" ? spinnerFrameAt(Date.now() - this.startedAt, getWorkingSpinnerFrames(process.env.TERM), SPINNER_INTERVAL_MS) : "";
16353
17364
  const marker = `${spinner ? `${spinner} ` : ""}/btw`;
16354
17365
  const markerWidth = visibleWidth14(marker) + 1;
16355
- const asked = wrapTextWithAnsi13(safeText(this.question), Math.max(1, inner - markerWidth));
17366
+ const asked = wrapTextWithAnsi15(safeText(this.question), Math.max(1, inner - markerWidth));
16356
17367
  const question = asked.slice(0, QUESTION_ROWS).map((line, index) => {
16357
17368
  const prefix = index === 0 ? `${theme.bold(theme.fg("accent", marker))} ` : " ".repeat(markerWidth);
16358
17369
  return prefix + theme.fg("muted", line);
16359
17370
  });
16360
17371
  if (asked.length > QUESTION_ROWS)
16361
- question[QUESTION_ROWS - 1] = truncateToWidth22(question[QUESTION_ROWS - 1], inner - 1, "") + theme.fg("muted", "\u2026");
17372
+ question[QUESTION_ROWS - 1] = truncateToWidth24(question[QUESTION_ROWS - 1], inner - 1, "") + theme.fg("muted", "\u2026");
16362
17373
  const text = this.current === "failed" ? `${this.answerText ? `${this.answerText}
16363
17374
  ` : ""}${this.error ?? "The side question failed."}` : this.answerText;
16364
17375
  const wrapped = text ? text.split(`
16365
- `).flatMap((line) => line ? wrapTextWithAnsi13(safeText(line), inner) : [""]) : [];
17376
+ `).flatMap((line) => line ? wrapTextWithAnsi15(safeText(line), inner) : [""]) : [];
16366
17377
  const maxOffset = Math.max(0, wrapped.length - ANSWER_ROWS);
16367
17378
  this.answerOffset = Math.min(this.answerOffset, maxOffset);
16368
17379
  const shown = wrapped.slice(this.answerOffset, this.answerOffset + ANSWER_ROWS).map((line) => this.current === "failed" ? theme.fg("error", line) : line);
@@ -16375,7 +17386,7 @@ class SideQuestion {
16375
17386
  ...shown.length ? ["", ...shown.map((line) => padded(line, width))] : [],
16376
17387
  "",
16377
17388
  footerRow(`${textHint}Ctrl+X copy \xB7 /btw or Esc dismiss`, width, theme)
16378
- ].map((line) => truncateToWidth22(line, width, ""));
17389
+ ].map((line) => truncateToWidth24(line, width, ""));
16379
17390
  }
16380
17391
  }
16381
17392
 
@@ -16578,7 +17589,7 @@ import { execFile } from "child_process";
16578
17589
  import { chmod, mkdtemp, open, rm, stat as stat2 } from "fs/promises";
16579
17590
  import { randomUUID as randomUUID3 } from "crypto";
16580
17591
  import { tmpdir as tmpdir2 } from "os";
16581
- import { basename as basename6, join as join19 } from "path";
17592
+ import { basename as basename6, join as join20 } from "path";
16582
17593
  import { promisify } from "util";
16583
17594
  var exec = promisify(execFile);
16584
17595
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
@@ -16611,14 +17622,14 @@ async function stageClipboardFiles(paths, options = {}) {
16611
17622
  return { paths };
16612
17623
  let directory;
16613
17624
  try {
16614
- directory = await mkdtemp(join19(options.tempRoot ?? tmpdir2(), "harness-clipboard-"));
17625
+ directory = await mkdtemp(join20(options.tempRoot ?? tmpdir2(), "harness-clipboard-"));
16615
17626
  const stagedPaths = [];
16616
17627
  for (const [index, path] of paths.entries()) {
16617
17628
  if (!isHeicPath(path)) {
16618
17629
  stagedPaths.push(path);
16619
17630
  continue;
16620
17631
  }
16621
- const output = join19(directory, `${index}-${randomUUID3()}.png`);
17632
+ const output = join20(directory, `${index}-${randomUUID3()}.png`);
16622
17633
  try {
16623
17634
  await (options.convert ?? convertWithSips)(path, output);
16624
17635
  await chmod(output, 384);
@@ -17154,7 +18165,7 @@ function asTui(host) {
17154
18165
 
17155
18166
  // src/terminal/opentui/attachments.ts
17156
18167
  import { fileURLToPath } from "url";
17157
- import { extname as extname2 } from "path";
18168
+ import { extname as extname3 } from "path";
17158
18169
  var LARGE_PASTE_LINES = 3;
17159
18170
  var LARGE_PASTE_CHARACTERS = 800;
17160
18171
  var IMAGE_EXTENSIONS = {
@@ -17167,7 +18178,7 @@ var IMAGE_EXTENSIONS = {
17167
18178
  ".heif": "image/heif"
17168
18179
  };
17169
18180
  function imageMimeTypeForPath(path) {
17170
- return IMAGE_EXTENSIONS[extname2(path).toLowerCase()];
18181
+ return IMAGE_EXTENSIONS[extname3(path).toLowerCase()];
17171
18182
  }
17172
18183
  function lineCount(text) {
17173
18184
  return text.split(/\r\n|\r|\n/u).length;
@@ -17372,15 +18383,15 @@ function toolOutput(message) {
17372
18383
  }
17373
18384
  return clip6(text);
17374
18385
  }
17375
- var CHECKLIST_STATUSES = new Set(["pending", "in_progress", "completed"]);
17376
- function checklistEntries(args) {
18386
+ var TODO_STATUSES = new Set(["pending", "in_progress", "completed"]);
18387
+ function todoEntries(args) {
17377
18388
  const items = args?.items;
17378
18389
  if (!Array.isArray(items))
17379
18390
  return null;
17380
18391
  const entries = [];
17381
18392
  for (const item of items) {
17382
18393
  const entry = item;
17383
- if (!entry || typeof entry.text !== "string" || !entry.text.trim() || typeof entry.status !== "string" || !CHECKLIST_STATUSES.has(entry.status))
18394
+ if (!entry || typeof entry.text !== "string" || !entry.text.trim() || typeof entry.status !== "string" || !TODO_STATUSES.has(entry.status))
17384
18395
  return null;
17385
18396
  entries.push({ text: ansiLabel(entry.text.trim(), 500), status: entry.status });
17386
18397
  }
@@ -17389,8 +18400,8 @@ function checklistEntries(args) {
17389
18400
  function sameTranscriptItem(left, right) {
17390
18401
  if (left.kind !== right.kind)
17391
18402
  return false;
17392
- if (left.kind === "checklist" && right.kind === "checklist") {
17393
- return left.id === right.id && left.toolCallId === right.toolCallId && left.status === right.status && JSON.stringify(left.items) === JSON.stringify(right.items);
18403
+ if (left.kind === "todo" && right.kind === "todo") {
18404
+ return left.id === right.id && left.origin === right.origin && left.toolCallId === right.toolCallId && left.status === right.status && JSON.stringify(left.items) === JSON.stringify(right.items);
17394
18405
  }
17395
18406
  const leftFields = Object.entries(left);
17396
18407
  const rightValues = right;
@@ -17519,6 +18530,11 @@ class TranscriptModel {
17519
18530
  this.addNotice(sequence, true, { kind: "command-help", id: `command-help-${sequence}`, groups });
17520
18531
  this.invalidate();
17521
18532
  }
18533
+ todo(items) {
18534
+ const sequence = this.nextNotice++;
18535
+ this.addNotice(sequence, true, { kind: "todo", id: `todo-${sequence}`, origin: "command", items });
18536
+ this.invalidate();
18537
+ }
17522
18538
  addNotice(sequence, multiline, item) {
17523
18539
  const messages = this.session?.messages ?? [];
17524
18540
  this.notices.push({ afterMessageCount: messages.length, anchor: messages.at(-1), sequence, multiline, item });
@@ -17664,9 +18680,9 @@ class TranscriptModel {
17664
18680
  const status = result ? result.isError === true ? "error" : "ok" : pending.has(block.id) ? "running" : "pending";
17665
18681
  const resultImageCount = result ? imageCount(result.content) : 0;
17666
18682
  const id = `m${index}.c${textIndex++}`;
17667
- const checklist = block.name === "checklist_write" && result?.isError !== true ? checklistEntries(block.arguments) : null;
17668
- if (checklist) {
17669
- items.push({ kind: "checklist", id, toolCallId: block.id, status, items: checklist });
18683
+ const todo = (block.name === "todo_write" || block.name === "checklist_write") && result?.isError !== true ? todoEntries(block.arguments) : null;
18684
+ if (todo) {
18685
+ items.push({ kind: "todo", id, origin: "tool", toolCallId: block.id, status, items: todo });
17670
18686
  continue;
17671
18687
  }
17672
18688
  items.push({
@@ -17782,7 +18798,7 @@ function matchPalette(items, filter) {
17782
18798
 
17783
18799
  // src/terminal/opentui/pathCompletion.ts
17784
18800
  import { readdir } from "fs/promises";
17785
- import { homedir as homedir2 } from "os";
18801
+ import { homedir as homedir3 } from "os";
17786
18802
  import { resolve as resolve5 } from "path";
17787
18803
  function consume(character, state) {
17788
18804
  if (state.escaped) {
@@ -17847,7 +18863,7 @@ async function completeFilePaths(text, cursorOffset, cwd, limit = 8) {
17847
18863
  const slash = Math.max(typedPrefix.lastIndexOf("/"), typedPrefix.lastIndexOf("\\"));
17848
18864
  const parent = slash < 0 ? "" : typedPrefix.slice(0, slash + 1);
17849
18865
  const namePrefix = typedPrefix.slice(slash + 1);
17850
- const diskParent = parent.startsWith("~/") ? resolve5(homedir2(), parent.slice(2)) : resolve5(cwd, parent || ".");
18866
+ const diskParent = parent.startsWith("~/") ? resolve5(homedir3(), parent.slice(2)) : resolve5(cwd, parent || ".");
17851
18867
  const separator = parent.endsWith("\\") ? "\\" : "/";
17852
18868
  const caseSensitive = process.platform !== "win32";
17853
18869
  const compare = (value) => caseSensitive ? value : value.toLocaleLowerCase();
@@ -18289,6 +19305,7 @@ class OpenTuiApplication {
18289
19305
  ui;
18290
19306
  commands;
18291
19307
  slashCommandNames;
19308
+ skillNames;
18292
19309
  sideQuestion;
18293
19310
  keybindings;
18294
19311
  themeValue;
@@ -18455,6 +19472,10 @@ class OpenTuiApplication {
18455
19472
  this.transcript.commandHelp(groups);
18456
19473
  this.change();
18457
19474
  }
19475
+ showTodo(items) {
19476
+ this.transcript.todo(items);
19477
+ this.change();
19478
+ }
18458
19479
  showToast(message) {
18459
19480
  if (this.stopped)
18460
19481
  return;
@@ -18622,6 +19643,8 @@ class OpenTuiApplication {
18622
19643
  control: (method, params) => controlCall(method, params ?? {}),
18623
19644
  changed: this.change,
18624
19645
  showHelp: (groups) => this.showHelp(groups),
19646
+ todo: async () => (await controlCall("terminal.todo")).todo,
19647
+ showTodo: (items) => this.showTodo(items),
18625
19648
  logs: {
18626
19649
  read: () => readHarnessLogSnapshot(() => controlCall("terminal.logs.info"), (snapshotSize, offset) => controlCall("terminal.logs.read", { snapshotSize, offset }))
18627
19650
  },
@@ -18640,9 +19663,11 @@ class OpenTuiApplication {
18640
19663
  this.keybindings?.reload();
18641
19664
  this.resetActiveWorkingStatus();
18642
19665
  this.refresh();
19666
+ this.refreshSubagents(true);
18643
19667
  }));
18644
19668
  this.initialized = true;
18645
19669
  this.refresh();
19670
+ this.refreshSubagents(true);
18646
19671
  let lastSubagentPoll = 0;
18647
19672
  const tick = setInterval(() => {
18648
19673
  if (this.startedAt !== null || this.compacting || this.sideQuestion?.state === "thinking")
@@ -18714,8 +19739,9 @@ class OpenTuiApplication {
18714
19739
  this.subagentsPolling = true;
18715
19740
  try {
18716
19741
  if (!settled && !this.session.getActiveToolNames().includes("spawn_subagent")) {
18717
- if (this.subagents.length) {
18718
- this.subagents = [];
19742
+ const main = this.subagents.filter((item) => item.relation === "main");
19743
+ if (main.length !== this.subagents.length) {
19744
+ this.subagents = main;
18719
19745
  this.change();
18720
19746
  }
18721
19747
  return;
@@ -18800,6 +19826,7 @@ class OpenTuiApplication {
18800
19826
  }
18801
19827
  refresh() {
18802
19828
  this.slashCommandNames = undefined;
19829
+ this.skillNames = undefined;
18803
19830
  if (!this.runtime)
18804
19831
  return;
18805
19832
  this.ui?.setThemes(this.session.resourceLoader.getThemes().themes);
@@ -18811,6 +19838,7 @@ class OpenTuiApplication {
18811
19838
  }
18812
19839
  resetPresentation() {
18813
19840
  this.slashCommandNames = undefined;
19841
+ this.skillNames = undefined;
18814
19842
  this.resetHistoryBrowse();
18815
19843
  this.invalidateFileCompletion();
18816
19844
  this.sideQuestion?.dismiss();
@@ -18841,6 +19869,17 @@ class OpenTuiApplication {
18841
19869
  this.mountEditor(undefined);
18842
19870
  this.change();
18843
19871
  }
19872
+ currentSkillNames() {
19873
+ if (this.skillNames)
19874
+ return this.skillNames;
19875
+ if (!this.commands)
19876
+ return [];
19877
+ try {
19878
+ return this.skillNames = this.commands.skills().map((skill) => skill.name);
19879
+ } catch {
19880
+ return this.skillNames = [];
19881
+ }
19882
+ }
18844
19883
  currentSlashCommandNames() {
18845
19884
  if (this.slashCommandNames)
18846
19885
  return this.slashCommandNames;
@@ -18853,8 +19892,8 @@ class OpenTuiApplication {
18853
19892
  for (const alias of command.aliases ?? [])
18854
19893
  names.add(alias);
18855
19894
  }
18856
- for (const skill of this.commands.skills())
18857
- names.add(`skill:${skill.name}`);
19895
+ for (const skill of this.currentSkillNames())
19896
+ names.add(`skill:${skill}`);
18858
19897
  for (const alias of this.commands.legacyAliases())
18859
19898
  names.add(alias);
18860
19899
  } catch {}
@@ -19229,7 +20268,7 @@ class OpenTuiApplication {
19229
20268
  this.session.setThinkingLevel(next);
19230
20269
  }
19231
20270
  extensionShortcut(data) {
19232
- const shortcut = [...this.session.extensionRunner.getShortcuts(this.keybindings.getResolvedBindings()).entries()].find(([key]) => matchesKey16(data, key));
20271
+ const shortcut = [...this.session.extensionRunner.getShortcuts(this.keybindings.getResolvedBindings()).entries()].find(([key]) => matchesKey18(data, key));
19233
20272
  if (!shortcut)
19234
20273
  return false;
19235
20274
  this.attempt(() => shortcut[1].handler(this.session.extensionRunner.createContext()));
@@ -19821,6 +20860,28 @@ class OpenTuiApplication {
19821
20860
  return true;
19822
20861
  if (this.questionDock && ["note", "answer"].includes(this.questionDock.view.focus))
19823
20862
  return false;
20863
+ const palette = this.paletteView();
20864
+ const choice = this.planChoice.view;
20865
+ if (choice && !palette) {
20866
+ const action = this.planChoice.handle(key, this.draft === "");
20867
+ if (action === "implement") {
20868
+ this.implementPlan(choice.title);
20869
+ return true;
20870
+ }
20871
+ if (action === "edit") {
20872
+ this.editPlan();
20873
+ return true;
20874
+ }
20875
+ if (action === "dismiss") {
20876
+ this.planChoice.clear();
20877
+ this.change();
20878
+ return true;
20879
+ }
20880
+ if (action === "moved") {
20881
+ this.change();
20882
+ return true;
20883
+ }
20884
+ }
19824
20885
  if (!this.paletteOpen && key.ctrl && key.name === "up") {
19825
20886
  this.historyPrevious(key.cursorOffset);
19826
20887
  return true;
@@ -19849,7 +20910,6 @@ class OpenTuiApplication {
19849
20910
  this.moveComposerCursor(this.draft.length);
19850
20911
  return true;
19851
20912
  }
19852
- const palette = this.paletteView();
19853
20913
  if (palette) {
19854
20914
  if (key.name === "up") {
19855
20915
  this.paletteIndex = (palette.index - 1 + palette.items.length) % palette.items.length;
@@ -19874,27 +20934,6 @@ class OpenTuiApplication {
19874
20934
  return true;
19875
20935
  }
19876
20936
  }
19877
- const choice = this.planChoice.view;
19878
- if (choice && !palette) {
19879
- const action = this.planChoice.handle(key, this.draft === "");
19880
- if (action === "implement") {
19881
- this.implementPlan(choice.title);
19882
- return true;
19883
- }
19884
- if (action === "edit") {
19885
- this.editPlan();
19886
- return true;
19887
- }
19888
- if (action === "dismiss") {
19889
- this.planChoice.clear();
19890
- this.change();
19891
- return true;
19892
- }
19893
- if (action === "moved") {
19894
- this.change();
19895
- return true;
19896
- }
19897
- }
19898
20937
  if (!this.paletteOpen && !this.bashMode && this.draft.trim() === "" && data.startsWith("!")) {
19899
20938
  this.enterBashMode(data.slice(1));
19900
20939
  return true;
@@ -20020,7 +21059,7 @@ class OpenTuiApplication {
20020
21059
  "Ctrl+O expand details \xB7 Ctrl+T thinking",
20021
21060
  "Ctrl+L model",
20022
21061
  "Ctrl+N new session",
20023
- "\u2191/\u2193 at draft start/end browse history; line edges jump to characters \xB7 Ctrl+\u2191/\u2193 anywhere",
21062
+ "\u2191/\u2193 at either draft endpoint browse older/newer history \xB7 Ctrl+\u2191/\u2193 anywhere",
20024
21063
  "Enter send or queue \xB7 Cmd+Enter steer \xB7 Esc dismisses btw or stops reply",
20025
21064
  "Ctrl+C clear or quit \xB7 Ctrl+D quit",
20026
21065
  "PgUp/PgDn transcript"
@@ -20103,6 +21142,7 @@ class OpenTuiApplication {
20103
21142
  items: transcript.items,
20104
21143
  hasUserMessages: transcript.hasUserMessages,
20105
21144
  slashCommandNames: this.currentSlashCommandNames(),
21145
+ skillNames: this.currentSkillNames(),
20106
21146
  draft: this.paletteOpen ? this.paletteFilter : this.draft,
20107
21147
  draftEpoch: this.draftEpoch,
20108
21148
  draftCursorOffset: this.draftCursorOffset,
@@ -20246,8 +21286,8 @@ class OpenTuiApplication {
20246
21286
  }
20247
21287
  }
20248
21288
  async externalEditor() {
20249
- const directory = await mkdtemp2(join20(tmpdir3(), "harness-editor-"));
20250
- const file = join20(directory, "prompt.md");
21289
+ const directory = await mkdtemp2(join21(tmpdir3(), "harness-editor-"));
21290
+ const file = join21(directory, "prompt.md");
20251
21291
  try {
20252
21292
  await writeFile(file, this.customEditor?.getExpandedText?.() ?? this.draft, { mode: 384 });
20253
21293
  await this.editFile(file);
@@ -21353,22 +22393,33 @@ function bufferOffset(text, index) {
21353
22393
  offset += visibleWidth15(line) + (position < lines.length - 1 ? 1 : 0);
21354
22394
  return offset;
21355
22395
  }
21356
- function syncSlashCommandMark(target, names, typeId, styleId) {
22396
+ function leadingSkillInvocationRange(text, names) {
22397
+ const match = /^(\s*)\$(\S+)/u.exec(text);
22398
+ if (!match || !names.includes(match[2]))
22399
+ return;
22400
+ return { start: match[1].length, end: match[0].length };
22401
+ }
22402
+ function syncTextMark(target, range, typeId, styleId) {
21357
22403
  const marks = target.extmarks.getAllForTypeId(typeId);
21358
- const command = leadingSlashCommandRange(target.plainText, names);
21359
- if (!command) {
22404
+ if (!range) {
21360
22405
  for (const mark of marks)
21361
22406
  target.extmarks.delete(mark.id);
21362
22407
  return;
21363
22408
  }
21364
- const start = bufferOffset(target.plainText, command.start);
21365
- const end = start + visibleWidth15(target.plainText.slice(command.start, command.end));
22409
+ const start = bufferOffset(target.plainText, range.start);
22410
+ const end = start + visibleWidth15(target.plainText.slice(range.start, range.end));
21366
22411
  if (marks.length === 1 && marks[0].start === start && marks[0].end === end)
21367
22412
  return;
21368
22413
  for (const mark of marks)
21369
22414
  target.extmarks.delete(mark.id);
21370
22415
  target.extmarks.create({ start, end, virtual: true, styleId, typeId });
21371
22416
  }
22417
+ function syncSlashCommandMark(target, names, typeId, styleId) {
22418
+ syncTextMark(target, leadingSlashCommandRange(target.plainText, names), typeId, styleId);
22419
+ }
22420
+ function syncSkillInvocationMark(target, names, typeId, styleId) {
22421
+ syncTextMark(target, leadingSkillInvocationRange(target.plainText, names), typeId, styleId);
22422
+ }
21372
22423
  function syncAttachmentMarks(target, labels, typeId, styleId) {
21373
22424
  const byId = new Map(labels.map((label) => [label.id, label]));
21374
22425
  const marks = target.extmarks.getAllForTypeId(typeId);
@@ -21431,7 +22482,7 @@ function statusBarText(state) {
21431
22482
  }
21432
22483
 
21433
22484
  // src/terminal/opentui/workingLine.ts
21434
- import { stripTerminalSequences as stripTerminalSequences13, truncateToWidth as truncateToWidth23, visibleWidth as visibleWidth16 } from "@earendil-works/pi-tui";
22485
+ import { stripTerminalSequences as stripTerminalSequences13, truncateToWidth as truncateToWidth25, visibleWidth as visibleWidth16 } from "@earendil-works/pi-tui";
21435
22486
  var SHIMMER_INTERVAL_MS = 120;
21436
22487
  var SHIMMER_RADIUS = 2;
21437
22488
  function workingLineParts(state, width) {
@@ -21462,7 +22513,7 @@ function workingLineParts(state, width) {
21462
22513
  return { prefix, label, suffix };
21463
22514
  let remaining = Math.max(0, Math.floor(width));
21464
22515
  const fit = (text) => {
21465
- const fitted = stripTerminalSequences13(truncateToWidth23(text, remaining, "\u2026", false));
22516
+ const fitted = stripTerminalSequences13(truncateToWidth25(text, remaining, "\u2026", false));
21466
22517
  remaining = Math.max(0, remaining - visibleWidth16(fitted));
21467
22518
  return fitted;
21468
22519
  };
@@ -21656,21 +22707,21 @@ function toolStatusIcon(status) {
21656
22707
  return "\u25CB";
21657
22708
  return "\u2713";
21658
22709
  }
21659
- function checklistMark(status) {
22710
+ function todoMark(status) {
21660
22711
  if (status === "completed")
21661
22712
  return "\u2713";
21662
22713
  if (status === "in_progress")
21663
22714
  return "\u25CF";
21664
22715
  return "\u25CB";
21665
22716
  }
21666
- function checklistMarkColor(t, status) {
22717
+ function todoMarkColor(t, status) {
21667
22718
  if (status === "completed")
21668
22719
  return t.success;
21669
22720
  if (status === "in_progress")
21670
22721
  return t.warning;
21671
22722
  return t.dim;
21672
22723
  }
21673
- function checklistProgress(done, total) {
22724
+ function todoProgress(done, total) {
21674
22725
  return total ? `${done} of ${total} done` : "empty";
21675
22726
  }
21676
22727
  function labelledRule(label, width) {
@@ -22027,7 +23078,7 @@ function TranscriptItemView(props) {
22027
23078
  return _el$23;
22028
23079
  })();
22029
23080
  }
22030
- if (item.kind === "checklist") {
23081
+ if (item.kind === "todo") {
22031
23082
  const done = () => item.items.filter((entry) => entry.status === "completed").length;
22032
23083
  return (() => {
22033
23084
  var _el$27 = _$createElement2("box"), _el$28 = _$createElement2("text"), _el$29 = _$createElement2("box");
@@ -22037,12 +23088,19 @@ function TranscriptItemView(props) {
22037
23088
  _$setProp2(_el$27, "paddingLeft", 1);
22038
23089
  _$setProp2(_el$27, "flexShrink", 0);
22039
23090
  _$setProp2(_el$28, "wrapMode", "none");
22040
- _$insert2(_el$28, _$createComponent2(Span, {
22041
- get fg() {
22042
- return toolColor(t, item.status);
23091
+ _$insert2(_el$28, _$createComponent2(Show2, {
23092
+ get when() {
23093
+ return _$memo2(() => item.origin === "tool")() && item.status;
22043
23094
  },
22044
23095
  get children() {
22045
- return toolStatusIcon(item.status);
23096
+ return _$createComponent2(Span, {
23097
+ get fg() {
23098
+ return toolColor(t, item.status);
23099
+ },
23100
+ get children() {
23101
+ return toolStatusIcon(item.status);
23102
+ }
23103
+ });
22046
23104
  }
22047
23105
  }), null);
22048
23106
  _$insert2(_el$28, _$createComponent2(Span, {
@@ -22052,14 +23110,14 @@ function TranscriptItemView(props) {
22052
23110
  get attributes() {
22053
23111
  return TextAttributes2.BOLD;
22054
23112
  },
22055
- children: " Checklist"
23113
+ children: " Todo"
22056
23114
  }), null);
22057
23115
  _$insert2(_el$28, _$createComponent2(Span, {
22058
23116
  get fg() {
22059
23117
  return t.muted;
22060
23118
  },
22061
23119
  get children() {
22062
- return [" \xB7 ", _$memo2(() => checklistProgress(done(), item.items.length))];
23120
+ return [" \xB7 ", _$memo2(() => todoProgress(done(), item.items.length))];
22063
23121
  }
22064
23122
  }), null);
22065
23123
  _$setProp2(_el$29, "flexDirection", "column");
@@ -22084,10 +23142,10 @@ function TranscriptItemView(props) {
22084
23142
  _$setProp2(_el$32, "wrapMode", "none");
22085
23143
  _$insert2(_el$32, _$createComponent2(Span, {
22086
23144
  get fg() {
22087
- return checklistMarkColor(t, entry.status);
23145
+ return todoMarkColor(t, entry.status);
22088
23146
  },
22089
23147
  get children() {
22090
- return checklistMark(entry.status);
23148
+ return todoMark(entry.status);
22091
23149
  }
22092
23150
  }));
22093
23151
  _$setProp2(_el$33, "width", 1);
@@ -22768,6 +23826,7 @@ function WorkingLine(props) {
22768
23826
  }
22769
23827
  var ATTACHMENT_MARK = "attachment";
22770
23828
  var SLASH_COMMAND_MARK = "slash-command";
23829
+ var SKILL_INVOCATION_MARK = "skill-invocation";
22771
23830
  function pastedText(bytes) {
22772
23831
  return new TextDecoder().decode(bytes).replace(/\x1b\[[0-?]*[ -/]*[@-~]/gu, "").replace(/\r\n?/gu, `
22773
23832
  `);
@@ -22786,6 +23845,11 @@ function syncMarks(area, view, syntax) {
22786
23845
  const typeId = area.extmarks.getTypeId(SLASH_COMMAND_MARK) ?? area.extmarks.registerType(SLASH_COMMAND_MARK);
22787
23846
  syncSlashCommandMark(area, view.bashMode ? [] : view.slashCommandNames, typeId, commandStyleId);
22788
23847
  }
23848
+ const skillStyleId = syntax.getStyleId(SKILL_INVOCATION_MARK);
23849
+ if (skillStyleId !== null) {
23850
+ const typeId = area.extmarks.getTypeId(SKILL_INVOCATION_MARK) ?? area.extmarks.registerType(SKILL_INVOCATION_MARK);
23851
+ syncSkillInvocationMark(area, view.bashMode ? [] : view.skillNames, typeId, skillStyleId);
23852
+ }
22789
23853
  return changed;
22790
23854
  }
22791
23855
  function subagentToneColor(t, tone) {
@@ -22895,6 +23959,7 @@ function Prompt(props) {
22895
23959
  createEffect2(() => {
22896
23960
  view().attachments;
22897
23961
  view().slashCommandNames;
23962
+ view().skillNames;
22898
23963
  view().bashMode;
22899
23964
  if (syncing)
22900
23965
  return;
@@ -22930,7 +23995,7 @@ function Prompt(props) {
22930
23995
  const plan = () => view().planMode;
22931
23996
  const composerFocused = () => {
22932
23997
  const current = view();
22933
- return !current.overlay && !current.customEditor;
23998
+ return !current.overlay && !current.customEditor && !current.planChoice;
22934
23999
  };
22935
24000
  const composerColor = () => composerRuleColor(theme2, {
22936
24001
  bash: bash(),
@@ -23539,6 +24604,9 @@ function OpenTuiRoot(props) {
23539
24604
  },
23540
24605
  [SLASH_COMMAND_MARK]: {
23541
24606
  fg: theme2.accent
24607
+ },
24608
+ [SKILL_INVOCATION_MARK]: {
24609
+ fg: theme2.accent
23542
24610
  }
23543
24611
  }, renderer.nativeScene);
23544
24612
  onCleanup2(() => {
@@ -23661,7 +24729,7 @@ function OpenTuiRoot(props) {
23661
24729
  });
23662
24730
  const composerBlocked = createMemo2(() => {
23663
24731
  const current = view();
23664
- return Boolean(current.overlay || current.customEditor || current.questionDock);
24732
+ return Boolean(current.overlay || current.customEditor || current.questionDock || current.planChoice);
23665
24733
  });
23666
24734
  createEffect2(on2(composerBlocked, (blocked) => {
23667
24735
  if (!prompt)