@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.
- package/CHANGELOG.md +35 -0
- package/README.md +10 -5
- package/dist/cli.js +5560 -5056
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +4 -1
- package/dist/sdk/agents/profiles.js +17 -6
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/plugins/builtin.js +3 -3
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +2 -2
- package/dist/sdk/prompt/compile.d.ts +2 -2
- package/dist/sdk/prompt/compile.js +14 -16
- package/dist/sdk/server/methods.js +4 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.js +29 -11
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +194 -54
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +28 -14
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +7 -0
- package/dist/sdk/terminal/terminals.js +86 -14
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.js +14 -4
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +8 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/terminal-opentui.js +1416 -348
- package/dist/terminal-pi.js +315 -126
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +65 -34
- package/docs/architecture/04-prompt.md +8 -6
- package/docs/architecture/06-plugins-and-capabilities.md +10 -6
- package/docs/architecture/07-tools-and-gate.md +31 -5
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +16 -9
- package/docs/architecture/11-terminal.md +51 -26
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +18 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/README.md +10 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +9 -9
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/README.md +2 -2
- package/docs/terminal-tui.md +85 -46
- package/package.json +1 -1
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
package/dist/terminal-opentui.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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: "
|
|
370
|
-
{ name: "hub", description: "Browse
|
|
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
|
|
1064
|
-
var
|
|
1065
|
-
var
|
|
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
|
-
"
|
|
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/
|
|
1259
|
-
var
|
|
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
|
|
1279
|
+
function describeTodo(items) {
|
|
1262
1280
|
if (!items.length)
|
|
1263
|
-
return "The
|
|
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
|
|
1285
|
+
function todoTool(run) {
|
|
1268
1286
|
return {
|
|
1269
|
-
name:
|
|
1270
|
-
label: "Write the
|
|
1271
|
-
description:
|
|
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:
|
|
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
|
|
1283
|
-
if (input.items.length >
|
|
1284
|
-
return `Keep the
|
|
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 =
|
|
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("
|
|
1294
|
-
return
|
|
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:
|
|
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.
|
|
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.
|
|
1460
|
-
const active = input.
|
|
1461
|
-
const completed = input.
|
|
1462
|
-
const
|
|
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(
|
|
1483
|
+
${clip(describeTodo(active), 2500)}` : "",
|
|
1465
1484
|
completed.length ? `### Completed (${completed.length})
|
|
1466
|
-
${clip(
|
|
1485
|
+
${clip(describeTodo(completed), 500)}` : ""
|
|
1467
1486
|
].filter(Boolean).join(`
|
|
1468
1487
|
|
|
1469
1488
|
`);
|
|
1470
|
-
parts.push(`## Current
|
|
1489
|
+
parts.push(`## Current todo list
|
|
1471
1490
|
|
|
1472
|
-
${
|
|
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:
|
|
1819
|
-
maxMinutes:
|
|
1820
|
-
maxNewTokens:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2043
|
+
const canCheckpoint = checkpointAvailable();
|
|
2044
|
+
const handoff = canCheckpoint && HANDOFF_TOOLS.has(name);
|
|
1995
2045
|
if (state.endedBy) {
|
|
1996
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2035
|
-
const reason = repeat ?? failure ?? noProgress;
|
|
2079
|
+
const reason = repeat ?? failure;
|
|
2036
2080
|
if (!reason)
|
|
2037
2081
|
return;
|
|
2038
|
-
|
|
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: `
|
|
2126
|
-
brief: Type.String({ description: "
|
|
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
|
|
2133
|
-
|
|
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
|
-
"
|
|
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 = ["
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
5692
|
-
const helpers = existsSync8(
|
|
5693
|
-
return `${readFileSync6(
|
|
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 ${
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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,
|
|
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
|
|
6288
|
-
const
|
|
6289
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
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
|
|
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 (
|
|
10612
|
+
if (matchesKey11(data, "tab"))
|
|
9743
10613
|
this.changeSource(1);
|
|
9744
|
-
else if (
|
|
10614
|
+
else if (matchesKey11(data, "shift+tab"))
|
|
9745
10615
|
this.changeSource(-1);
|
|
9746
|
-
else if (
|
|
10616
|
+
else if (matchesKey11(data, "home")) {
|
|
9747
10617
|
this.offset = 0;
|
|
9748
10618
|
this.atEnd = false;
|
|
9749
|
-
} else if (
|
|
10619
|
+
} else if (matchesKey11(data, "end"))
|
|
9750
10620
|
this.atEnd = true;
|
|
9751
|
-
else if (
|
|
10621
|
+
else if (matchesKey11(data, "up")) {
|
|
9752
10622
|
this.offset = Math.max(0, this.currentOffset() - 1);
|
|
9753
10623
|
this.atEnd = false;
|
|
9754
|
-
} else if (
|
|
10624
|
+
} else if (matchesKey11(data, "down")) {
|
|
9755
10625
|
this.offset = this.currentOffset() + 1;
|
|
9756
10626
|
this.atEnd = false;
|
|
9757
|
-
} else if (
|
|
10627
|
+
} else if (matchesKey11(data, "pageUp")) {
|
|
9758
10628
|
this.offset = Math.max(0, this.currentOffset() - page);
|
|
9759
10629
|
this.atEnd = false;
|
|
9760
|
-
} else if (
|
|
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 ?
|
|
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) =>
|
|
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
|
|
9791
|
-
var
|
|
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
|
|
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 (
|
|
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 ?
|
|
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) =>
|
|
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
|
|
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 (
|
|
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 + (
|
|
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 ?
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
10390
|
-
const at =
|
|
10391
|
-
const delta =
|
|
10392
|
-
this.section =
|
|
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 (
|
|
11275
|
+
if (matchesKey14(data, "pageUp")) {
|
|
10406
11276
|
this.detailOffset = Math.max(0, this.detailOffset - 8);
|
|
10407
11277
|
return true;
|
|
10408
11278
|
}
|
|
10409
|
-
if (
|
|
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) =>
|
|
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 ?
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
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
|
|
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
|
|
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 =
|
|
10741
|
-
return [header, "", ...this.list.render(width, Math.max(1, rows - 2))].slice(0, rows).map((line) =>
|
|
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
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
11673
|
-
return
|
|
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
|
-
|
|
12548
|
+
const children = items.filter((item) => item.relation !== "main");
|
|
12549
|
+
if (!children.length || width < 1)
|
|
11677
12550
|
return null;
|
|
11678
|
-
const ordered = [...
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 ${
|
|
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) =>
|
|
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.
|
|
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) =>
|
|
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 (
|
|
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
|
|
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(...
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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 =
|
|
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 (
|
|
13350
|
+
if (matchesKey16(data, "up"))
|
|
12476
13351
|
this.offset = Math.max(0, this.offset - 1);
|
|
12477
|
-
else if (
|
|
13352
|
+
else if (matchesKey16(data, "down"))
|
|
12478
13353
|
this.offset = Math.min(this.maxOffset, this.offset + 1);
|
|
12479
|
-
else if (
|
|
13354
|
+
else if (matchesKey16(data, "pageUp"))
|
|
12480
13355
|
this.offset = Math.max(0, this.offset - page);
|
|
12481
|
-
else if (
|
|
13356
|
+
else if (matchesKey16(data, "pageDown"))
|
|
12482
13357
|
this.offset = Math.min(this.maxOffset, this.offset + page);
|
|
12483
|
-
else if (
|
|
13358
|
+
else if (matchesKey16(data, "home"))
|
|
12484
13359
|
this.offset = 0;
|
|
12485
|
-
else if (
|
|
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(...
|
|
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(...
|
|
13395
|
+
header.push(...wrapTextWithAnsi13(theme.fg("dim", details.join(" \xB7 ")), width));
|
|
12521
13396
|
if (this.error)
|
|
12522
|
-
header.push(...
|
|
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(...
|
|
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(...
|
|
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(...
|
|
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(...
|
|
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(...
|
|
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(...
|
|
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(...
|
|
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) =>
|
|
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"
|
|
12801
|
-
source:
|
|
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("..\\") ||
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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) =>
|
|
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 && (
|
|
13887
|
-
const delta =
|
|
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 ?
|
|
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) =>
|
|
14940
|
+
render: (width) => editor.render(Math.max(4, width)).map((line) => truncateToWidth23(line, width, "")),
|
|
14046
14941
|
handleInput: (data) => {
|
|
14047
|
-
if (
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
14238
|
-
import(pathToFileURL2(
|
|
14239
|
-
import(pathToFileURL2(
|
|
14240
|
-
import(pathToFileURL2(
|
|
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
|
|
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
|
|
14328
|
-
if (!Array.isArray(value) || value.length >
|
|
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 =
|
|
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
|
|
15241
|
+
let todo = validatedTodo(options.initialTodo ?? []) ?? [];
|
|
14347
15242
|
let lastCompactionKey = null;
|
|
14348
15243
|
let compacted = false;
|
|
14349
15244
|
const reset = () => {
|
|
14350
15245
|
goal = "";
|
|
14351
|
-
|
|
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
|
-
|
|
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 !==
|
|
15259
|
+
if (event.toolName !== TODO_WRITE || event.isError)
|
|
14365
15260
|
return;
|
|
14366
|
-
const items =
|
|
15261
|
+
const items = validatedTodo(event.input.items);
|
|
14367
15262
|
if (items)
|
|
14368
|
-
|
|
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,
|
|
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
|
|
14443
|
-
import { homedir } from "os";
|
|
14444
|
-
import { basename as basename5, dirname as dirname5, isAbsolute as
|
|
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 === "~" ?
|
|
14447
|
-
const absoluteCandidate =
|
|
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 (!
|
|
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}`) && !
|
|
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) || !
|
|
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
|
|
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
|
|
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
|
-
|
|
14527
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
14798
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
15471
|
-
recorded = terminalCall("
|
|
16481
|
+
const todo = todoTool({ emit: (_type, payload) => {
|
|
16482
|
+
recorded = terminalCall("todo", payload);
|
|
15472
16483
|
} });
|
|
15473
16484
|
pi.registerTool({
|
|
15474
|
-
name:
|
|
15475
|
-
label:
|
|
15476
|
-
description:
|
|
15477
|
-
parameters: Type2.Unsafe(
|
|
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
|
|
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}`) ||
|
|
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 || !
|
|
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 || !
|
|
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:
|
|
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
|
|
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 =
|
|
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] =
|
|
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 ?
|
|
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) =>
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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[
|
|
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
|
|
17376
|
-
function
|
|
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" || !
|
|
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 === "
|
|
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
|
|
17668
|
-
if (
|
|
17669
|
-
items.push({ kind: "
|
|
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
|
|
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(
|
|
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
|
-
|
|
18718
|
-
|
|
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.
|
|
18857
|
-
names.add(`skill:${skill
|
|
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]) =>
|
|
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
|
|
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(
|
|
20250
|
-
const file =
|
|
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
|
|
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
|
-
|
|
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,
|
|
21365
|
-
const end = start + visibleWidth15(target.plainText.slice(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 === "
|
|
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(
|
|
22041
|
-
get
|
|
22042
|
-
return
|
|
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
|
|
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: "
|
|
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(() =>
|
|
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
|
|
23145
|
+
return todoMarkColor(t, entry.status);
|
|
22088
23146
|
},
|
|
22089
23147
|
get children() {
|
|
22090
|
-
return
|
|
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)
|