@xingular/harness 1.1.1 → 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 +48 -0
- package/README.md +36 -28
- package/dist/cli.js +10837 -6159
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +18 -4
- package/dist/sdk/agents/planResearch.d.ts +27 -0
- package/dist/sdk/agents/planResearch.js +82 -0
- package/dist/sdk/agents/profiles.d.ts +5 -1
- package/dist/sdk/agents/profiles.js +21 -5
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +258 -0
- package/dist/sdk/commands/registry.js +48 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
- package/dist/sdk/core/workspaceBashTool.js +50 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
- package/dist/sdk/core/workspaceSandbox.js +360 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
- package/dist/sdk/core/workspaceWriteOperations.js +224 -0
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +8 -3
- package/dist/sdk/index.js +4 -1
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +32 -15
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +6 -5
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -31
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +22 -2
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +7 -2
- package/dist/sdk/sessions/continuation.js +11 -6
- package/dist/sdk/sessions/contracts.d.ts +190 -4
- package/dist/sdk/sessions/contracts.js +71 -8
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +74 -17
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +38 -1
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +225 -22
- package/dist/sdk/sessions/subagentService.d.ts +24 -0
- package/dist/sdk/sessions/subagentService.js +225 -26
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +106 -0
- package/dist/sdk/sessions/workSlice.js +329 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +31 -3
- package/dist/sdk/terminal/controller.js +81 -29
- 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.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +88 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +177 -63
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
- package/dist/sdk/terminal/subagentMonitor.js +61 -9
- package/dist/sdk/terminal/terminals.d.ts +26 -2
- package/dist/sdk/terminal/terminals.js +325 -43
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- 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.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +17 -4
- package/dist/sdk/terminal/tui/commands.d.ts +6 -0
- package/dist/sdk/terminal/tui/commands.js +213 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- 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/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +11 -8
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +9204 -4812
- package/dist/terminal-pi.js +1848 -272
- 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 +169 -36
- package/docs/architecture/04-prompt.md +55 -15
- package/docs/architecture/06-plugins-and-capabilities.md +36 -15
- package/docs/architecture/07-tools-and-gate.md +131 -25
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -3
- package/docs/architecture/10-server-protocol.md +71 -9
- package/docs/architecture/11-terminal.md +148 -60
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +58 -8
- package/docs/architecture/README.md +20 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- 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 +24 -6
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- 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/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +83 -27
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +162 -82
- package/package.json +4 -3
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -54
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { sessionEntryToContextMessages } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { parseProposedPlan } from "../tui/planMode.js";
|
|
3
|
+
import { ansiLabel, ansiPlain } from "./ansi.js";
|
|
4
|
+
const MAX_TOOL_OUTPUT = 12_000;
|
|
5
|
+
const OUTPUT_CLIP_MARKER = '\n… output clipped …\n';
|
|
6
|
+
function appendCapped(current, delta) {
|
|
7
|
+
const combined = current + delta;
|
|
8
|
+
if (combined.length <= MAX_TOOL_OUTPUT)
|
|
9
|
+
return combined;
|
|
10
|
+
const available = MAX_TOOL_OUTPUT - OUTPUT_CLIP_MARKER.length;
|
|
11
|
+
const head = Math.ceil(available / 2);
|
|
12
|
+
return `${combined.slice(0, head)}${OUTPUT_CLIP_MARKER}${combined.slice(-(available - head))}`;
|
|
13
|
+
}
|
|
14
|
+
function blocks(content) {
|
|
15
|
+
if (typeof content === 'string')
|
|
16
|
+
return [{ type: 'text', text: content }];
|
|
17
|
+
return Array.isArray(content) ? content.filter((item) => typeof item === 'object' && item !== null) : [];
|
|
18
|
+
}
|
|
19
|
+
function contentText(content) {
|
|
20
|
+
return blocks(content)
|
|
21
|
+
.filter(block => block.type === 'text' && typeof block.text === 'string')
|
|
22
|
+
.map(block => String(block.text))
|
|
23
|
+
.join('\n');
|
|
24
|
+
}
|
|
25
|
+
function withoutBoldMarkers(text) {
|
|
26
|
+
return text.replace(/\*\*(.*?)\*\*/gs, '$1');
|
|
27
|
+
}
|
|
28
|
+
function imageCount(content) {
|
|
29
|
+
return blocks(content).filter(block => block.type === 'image').length;
|
|
30
|
+
}
|
|
31
|
+
function imageContents(content) {
|
|
32
|
+
return blocks(content).flatMap(block => block.type === 'image' && typeof block.data === 'string' && typeof block.mimeType === 'string'
|
|
33
|
+
? [{ data: block.data, mimeType: block.mimeType }]
|
|
34
|
+
: []);
|
|
35
|
+
}
|
|
36
|
+
function stringField(message, name) {
|
|
37
|
+
const value = message[name];
|
|
38
|
+
return typeof value === 'string' ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
function numberField(message, name) {
|
|
41
|
+
const value = message[name];
|
|
42
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
function clip(text, limit = MAX_TOOL_OUTPUT) {
|
|
45
|
+
return text.length > limit ? `${text.slice(0, limit)}\n… ${text.length - limit} more characters` : text;
|
|
46
|
+
}
|
|
47
|
+
function argumentSummary(name, args) {
|
|
48
|
+
const value = (args ?? {});
|
|
49
|
+
const pick = (...names) => {
|
|
50
|
+
for (const key of names) {
|
|
51
|
+
const item = value[key];
|
|
52
|
+
if (typeof item === 'string' && item.trim())
|
|
53
|
+
return item;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
};
|
|
57
|
+
const path = pick('path', 'file_path', 'filePath', 'target');
|
|
58
|
+
const summary = name === 'bash' || name === 'shell'
|
|
59
|
+
? pick('command', 'cmd')
|
|
60
|
+
: name === 'grep'
|
|
61
|
+
? [pick('pattern'), pick('path')].filter(Boolean).join(' ')
|
|
62
|
+
: name === 'find'
|
|
63
|
+
? [pick('pattern'), pick('path')].filter(Boolean).join(' ')
|
|
64
|
+
: name === 'websearch' || name === 'web_search'
|
|
65
|
+
? pick('query')
|
|
66
|
+
: name === 'webfetch' || name === 'web_fetch'
|
|
67
|
+
? pick('url')
|
|
68
|
+
: name === 'spawn_subagent' || name === 'spawnSubagent'
|
|
69
|
+
? [pick('agent'), pick('title', 'brief', 'description', 'prompt')].filter(Boolean).join(' · ')
|
|
70
|
+
: path;
|
|
71
|
+
if (summary)
|
|
72
|
+
return ansiLabel(summary, 100);
|
|
73
|
+
if (!args || typeof args !== 'object')
|
|
74
|
+
return '';
|
|
75
|
+
try {
|
|
76
|
+
return ansiLabel(JSON.stringify(args), 100);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function toolOutput(message) {
|
|
83
|
+
const text = ansiPlain(contentText(message.content));
|
|
84
|
+
const details = message.details;
|
|
85
|
+
if (details && typeof details === 'object') {
|
|
86
|
+
const diff = details.diff;
|
|
87
|
+
if (typeof diff === 'string' && diff.trim())
|
|
88
|
+
return clip(`${text}${text ? '\n' : ''}${diff}`);
|
|
89
|
+
}
|
|
90
|
+
return clip(text);
|
|
91
|
+
}
|
|
92
|
+
const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed']);
|
|
93
|
+
/** The items of a `todo_write` (or pre-rename `checklist_write`) call, or null when they are not a todo list. */
|
|
94
|
+
export function todoEntries(args) {
|
|
95
|
+
const items = args?.items;
|
|
96
|
+
if (!Array.isArray(items))
|
|
97
|
+
return null;
|
|
98
|
+
const entries = [];
|
|
99
|
+
for (const item of items) {
|
|
100
|
+
const entry = item;
|
|
101
|
+
if (!entry || typeof entry.text !== 'string' || !entry.text.trim() || typeof entry.status !== 'string' || !TODO_STATUSES.has(entry.status))
|
|
102
|
+
return null;
|
|
103
|
+
entries.push({ text: ansiLabel(entry.text.trim(), 500), status: entry.status });
|
|
104
|
+
}
|
|
105
|
+
return entries;
|
|
106
|
+
}
|
|
107
|
+
function sameTranscriptItem(left, right) {
|
|
108
|
+
if (left.kind !== right.kind)
|
|
109
|
+
return false;
|
|
110
|
+
if (left.kind === 'todo' && right.kind === 'todo') {
|
|
111
|
+
return left.id === right.id && left.origin === right.origin && left.toolCallId === right.toolCallId && left.status === right.status
|
|
112
|
+
&& JSON.stringify(left.items) === JSON.stringify(right.items);
|
|
113
|
+
}
|
|
114
|
+
const leftFields = Object.entries(left);
|
|
115
|
+
const rightValues = right;
|
|
116
|
+
return leftFields.length === Object.keys(right).length
|
|
117
|
+
&& leftFields.every(([key, value]) => rightValues[key] === value);
|
|
118
|
+
}
|
|
119
|
+
/** Gives each object a stable number, so a cache key can say "this exact tool result". */
|
|
120
|
+
const objectIds = new WeakMap();
|
|
121
|
+
let nextObjectId = 0;
|
|
122
|
+
function objectId(value) {
|
|
123
|
+
let id = objectIds.get(value);
|
|
124
|
+
if (id === undefined)
|
|
125
|
+
objectIds.set(value, id = nextObjectId++);
|
|
126
|
+
return id;
|
|
127
|
+
}
|
|
128
|
+
function compactedHistory(session) {
|
|
129
|
+
const manager = session.sessionManager;
|
|
130
|
+
if (typeof manager?.getBranch !== 'function' || typeof manager.buildSessionProjection !== 'function')
|
|
131
|
+
return null;
|
|
132
|
+
const branch = manager.getBranch();
|
|
133
|
+
const at = branch.findLastIndex(entry => entry.type === 'compaction');
|
|
134
|
+
if (at < 0)
|
|
135
|
+
return null;
|
|
136
|
+
const compactionId = branch[at].id;
|
|
137
|
+
const earlier = new Set(branch.slice(0, at).map(entry => entry.id));
|
|
138
|
+
const projected = manager.buildSessionProjection().entries;
|
|
139
|
+
const first = projected[0];
|
|
140
|
+
if (!first || first.sourceEntry.id !== compactionId || first.messages.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
let head = 0;
|
|
143
|
+
for (const entry of projected) {
|
|
144
|
+
if (entry.sourceEntry.id !== compactionId && !earlier.has(entry.sourceEntry.id))
|
|
145
|
+
break;
|
|
146
|
+
head += entry.messages.length;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
archive: branch.slice(0, at).flatMap(entry => sessionEntryToContextMessages(entry)),
|
|
150
|
+
summaryIndex: first.messages.length - 1,
|
|
151
|
+
head
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export class TranscriptModel {
|
|
155
|
+
session;
|
|
156
|
+
/** Items derived from a finished message, reused while its position and tool results stay the same. */
|
|
157
|
+
messageItems = new WeakMap();
|
|
158
|
+
hideThinking = false;
|
|
159
|
+
showImages = true;
|
|
160
|
+
expanded = false;
|
|
161
|
+
notices = [];
|
|
162
|
+
nextNotice = 0;
|
|
163
|
+
liveBash;
|
|
164
|
+
cachedView;
|
|
165
|
+
cachedItems = new Map();
|
|
166
|
+
/** What a compaction took out of Pi's message list, rebuilt from the session history on bind. */
|
|
167
|
+
history;
|
|
168
|
+
bind(session) {
|
|
169
|
+
this.session = session;
|
|
170
|
+
this.history = undefined;
|
|
171
|
+
this.invalidate();
|
|
172
|
+
}
|
|
173
|
+
clear() {
|
|
174
|
+
this.session = undefined;
|
|
175
|
+
this.history = undefined;
|
|
176
|
+
this.notices = [];
|
|
177
|
+
this.liveBash = undefined;
|
|
178
|
+
this.invalidate();
|
|
179
|
+
}
|
|
180
|
+
// Pi mutates the session messages outside this model, so callers invalidate after runtime events.
|
|
181
|
+
invalidate() {
|
|
182
|
+
this.cachedView = undefined;
|
|
183
|
+
}
|
|
184
|
+
beginBash(command) {
|
|
185
|
+
const messages = (this.session?.messages ?? []);
|
|
186
|
+
this.liveBash = {
|
|
187
|
+
command,
|
|
188
|
+
output: '',
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
previousCount: messages.filter(message => message.role === 'bashExecution').length
|
|
191
|
+
};
|
|
192
|
+
this.invalidate();
|
|
193
|
+
}
|
|
194
|
+
appendBashOutput(id, delta) {
|
|
195
|
+
const live = this.liveBash;
|
|
196
|
+
if (!live || !delta || (live.id !== undefined && live.id !== id))
|
|
197
|
+
return false;
|
|
198
|
+
live.id ??= id;
|
|
199
|
+
live.output = appendCapped(live.output, delta);
|
|
200
|
+
this.invalidate();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
finishBash() {
|
|
204
|
+
this.liveBash = undefined;
|
|
205
|
+
this.invalidate();
|
|
206
|
+
}
|
|
207
|
+
clearNotices() {
|
|
208
|
+
this.notices = [];
|
|
209
|
+
this.invalidate();
|
|
210
|
+
}
|
|
211
|
+
setThinkingHidden(hidden) {
|
|
212
|
+
if (this.hideThinking === hidden)
|
|
213
|
+
return;
|
|
214
|
+
this.hideThinking = hidden;
|
|
215
|
+
this.messageItems = new WeakMap();
|
|
216
|
+
this.invalidate();
|
|
217
|
+
}
|
|
218
|
+
setImagesShown(shown) {
|
|
219
|
+
if (this.showImages === shown)
|
|
220
|
+
return;
|
|
221
|
+
this.showImages = shown;
|
|
222
|
+
this.messageItems = new WeakMap();
|
|
223
|
+
this.invalidate();
|
|
224
|
+
}
|
|
225
|
+
setExpanded(expanded) {
|
|
226
|
+
this.expanded = expanded;
|
|
227
|
+
}
|
|
228
|
+
get isExpanded() {
|
|
229
|
+
return this.expanded;
|
|
230
|
+
}
|
|
231
|
+
notice(text, level = 'info') {
|
|
232
|
+
const lines = text.split(/\r\n?|\n/u).filter(line => line.trim());
|
|
233
|
+
const multiline = lines.length > 1;
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
const sequence = this.nextNotice++;
|
|
236
|
+
this.addNotice(sequence, multiline, { kind: 'notice', id: `notice-${sequence}`, text: line, level });
|
|
237
|
+
}
|
|
238
|
+
this.invalidate();
|
|
239
|
+
}
|
|
240
|
+
commandHelp(groups) {
|
|
241
|
+
const sequence = this.nextNotice++;
|
|
242
|
+
this.addNotice(sequence, true, { kind: 'command-help', id: `command-help-${sequence}`, groups });
|
|
243
|
+
this.invalidate();
|
|
244
|
+
}
|
|
245
|
+
/** A `/todo` snapshot: the list at the moment the command ran, with no tool row. */
|
|
246
|
+
todo(items) {
|
|
247
|
+
const sequence = this.nextNotice++;
|
|
248
|
+
this.addNotice(sequence, true, { kind: 'todo', id: `todo-${sequence}`, origin: 'command', items });
|
|
249
|
+
this.invalidate();
|
|
250
|
+
}
|
|
251
|
+
addNotice(sequence, multiline, item) {
|
|
252
|
+
const messages = this.session?.messages ?? [];
|
|
253
|
+
this.notices.push({ afterMessageCount: messages.length, anchor: messages.at(-1), sequence, multiline, item });
|
|
254
|
+
if (this.notices.length > 60)
|
|
255
|
+
this.notices = this.notices.slice(-60);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Notices in the order they appear, each with the message index it goes before. Compaction
|
|
259
|
+
* replaces the message list with a shorter one, so a notice follows the message it was posted
|
|
260
|
+
* after; a notice whose message was summarized goes right after the compaction card.
|
|
261
|
+
*/
|
|
262
|
+
placedNotices(messages) {
|
|
263
|
+
const indexes = new Map();
|
|
264
|
+
messages.forEach((message, index) => indexes.set(message, index));
|
|
265
|
+
const summary = messages.findIndex(message => message.role === 'compactionSummary');
|
|
266
|
+
return this.notices.map(notice => {
|
|
267
|
+
if (!notice.anchor)
|
|
268
|
+
return { place: 0, notice };
|
|
269
|
+
const index = indexes.get(notice.anchor);
|
|
270
|
+
if (index !== undefined)
|
|
271
|
+
return { place: index + 1, notice };
|
|
272
|
+
return { place: summary >= 0 ? summary + 1 : Math.min(notice.afterMessageCount, messages.length), notice };
|
|
273
|
+
}).sort((left, right) => left.place - right.place || left.notice.sequence - right.notice.sequence);
|
|
274
|
+
}
|
|
275
|
+
view() {
|
|
276
|
+
if (this.cachedView)
|
|
277
|
+
return this.cachedView;
|
|
278
|
+
const session = this.session;
|
|
279
|
+
if (!session)
|
|
280
|
+
return this.cachedView = {
|
|
281
|
+
items: this.stabilize(this.notices.map(notice => notice.item)),
|
|
282
|
+
hasUserMessages: false,
|
|
283
|
+
hasMultilineNotice: this.notices.some(notice => notice.multiline)
|
|
284
|
+
};
|
|
285
|
+
const live = session.messages;
|
|
286
|
+
const messages = this.displayMessages(live);
|
|
287
|
+
const streaming = session.state.streamingMessage;
|
|
288
|
+
const all = streaming && !messages.includes(streaming) ? [...messages, streaming] : messages;
|
|
289
|
+
const pending = session.state.pendingToolCalls ?? new Set();
|
|
290
|
+
const results = new Map();
|
|
291
|
+
for (const message of messages) {
|
|
292
|
+
if (message.role === 'toolResult' && typeof message.toolCallId === 'string')
|
|
293
|
+
results.set(message.toolCallId, message);
|
|
294
|
+
}
|
|
295
|
+
const items = [];
|
|
296
|
+
const bashExecutionCount = live.filter(message => message.role === 'bashExecution').length;
|
|
297
|
+
const notices = this.placedNotices(messages);
|
|
298
|
+
let noticeIndex = 0;
|
|
299
|
+
for (let index = 0; index < all.length; index += 1) {
|
|
300
|
+
while (noticeIndex < notices.length && notices[noticeIndex].place <= index) {
|
|
301
|
+
items.push(notices[noticeIndex++].notice.item);
|
|
302
|
+
}
|
|
303
|
+
const message = all[index];
|
|
304
|
+
if (message === streaming) {
|
|
305
|
+
items.push(...this.messageItemsFor(message, index, true, results, pending));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const key = this.messageKey(message, index, results, pending);
|
|
309
|
+
const cached = this.messageItems.get(message);
|
|
310
|
+
if (cached && cached.key === key) {
|
|
311
|
+
items.push(...cached.items);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const derived = this.messageItemsFor(message, index, false, results, pending);
|
|
315
|
+
this.messageItems.set(message, { key, items: derived });
|
|
316
|
+
items.push(...derived);
|
|
317
|
+
}
|
|
318
|
+
if (this.liveBash && bashExecutionCount === this.liveBash.previousCount) {
|
|
319
|
+
items.push({
|
|
320
|
+
kind: 'bash', id: `live-${this.liveBash.id ?? 'pending'}`, command: this.liveBash.command,
|
|
321
|
+
timestamp: this.liveBash.timestamp, output: ansiPlain(this.liveBash.output), running: true, cancelled: false
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
for (; noticeIndex < notices.length; noticeIndex += 1)
|
|
325
|
+
items.push(notices[noticeIndex].notice.item);
|
|
326
|
+
return this.cachedView = {
|
|
327
|
+
items: this.stabilize(items),
|
|
328
|
+
hasUserMessages: messages.some(message => message.role === 'user'),
|
|
329
|
+
hasMultilineNotice: this.notices.some(notice => notice.multiline)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Pi's list after a compaction starts with the summary and the kept messages. The transcript
|
|
334
|
+
* shows the whole conversation instead: everything before the compaction from the session
|
|
335
|
+
* history, the summary, then what came after.
|
|
336
|
+
*/
|
|
337
|
+
displayMessages(live) {
|
|
338
|
+
if (this.history === undefined) {
|
|
339
|
+
try {
|
|
340
|
+
this.history = this.session ? compactedHistory(this.session) : null;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
this.history = null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const history = this.history;
|
|
347
|
+
if (!history || live.length < history.head || live[history.summaryIndex]?.role !== 'compactionSummary')
|
|
348
|
+
return live;
|
|
349
|
+
return [...history.archive, live[history.summaryIndex], ...live.slice(history.head)];
|
|
350
|
+
}
|
|
351
|
+
/** What a message's items depend on besides the message itself. */
|
|
352
|
+
messageKey(message, index, results, pending) {
|
|
353
|
+
let key = `${index}`;
|
|
354
|
+
if (message.role !== 'assistant')
|
|
355
|
+
return key;
|
|
356
|
+
for (const block of blocks(message.content)) {
|
|
357
|
+
if (block.type !== 'toolCall' || typeof block.id !== 'string')
|
|
358
|
+
continue;
|
|
359
|
+
const result = results.get(block.id);
|
|
360
|
+
key += result ? `|${objectId(result)}` : pending.has(block.id) ? '|r' : '|p';
|
|
361
|
+
}
|
|
362
|
+
return key;
|
|
363
|
+
}
|
|
364
|
+
messageItemsFor(message, index, isStreaming, results, pending) {
|
|
365
|
+
const items = [];
|
|
366
|
+
const role = message.role;
|
|
367
|
+
if (role === 'user') {
|
|
368
|
+
const text = contentText(message.content);
|
|
369
|
+
const images = imageCount(message.content);
|
|
370
|
+
if (!text && !images)
|
|
371
|
+
return items;
|
|
372
|
+
items.push({ kind: 'user', id: `m${index}`, text, images, timestamp: numberField(message, 'timestamp') ?? 0 });
|
|
373
|
+
return items;
|
|
374
|
+
}
|
|
375
|
+
if (role === 'assistant') {
|
|
376
|
+
const model = stringField(message, 'model');
|
|
377
|
+
const content = blocks(message.content);
|
|
378
|
+
let textIndex = 0;
|
|
379
|
+
for (const block of content) {
|
|
380
|
+
if (block.type === 'thinking' && typeof block.thinking === 'string' && !this.hideThinking) {
|
|
381
|
+
const text = block.redacted === true ? '[redacted thinking]' : withoutBoldMarkers(String(block.thinking));
|
|
382
|
+
if (text.trim())
|
|
383
|
+
items.push({ kind: 'thinking', id: `m${index}.t${textIndex++}`, text, streaming: isStreaming });
|
|
384
|
+
}
|
|
385
|
+
else if (block.type === 'text' && typeof block.text === 'string') {
|
|
386
|
+
const id = `m${index}.x${textIndex++}`;
|
|
387
|
+
const timestamp = numberField(message, 'timestamp') ?? 0;
|
|
388
|
+
const plan = parseProposedPlan(block.text);
|
|
389
|
+
if (!plan) {
|
|
390
|
+
if (block.text.trim())
|
|
391
|
+
items.push({ kind: 'assistant', id, text: block.text, model, streaming: isStreaming, timestamp });
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (plan.before)
|
|
395
|
+
items.push({ kind: 'assistant', id: `${id}.b`, text: plan.before, model, streaming: isStreaming, timestamp });
|
|
396
|
+
items.push({ kind: 'plan', id: `${id}.p`, title: plan.title, body: plan.body, streaming: isStreaming && !plan.complete });
|
|
397
|
+
if (plan.after)
|
|
398
|
+
items.push({ kind: 'assistant', id: `${id}.a`, text: plan.after, model, streaming: isStreaming, timestamp });
|
|
399
|
+
}
|
|
400
|
+
else if (block.type === 'toolCall' && typeof block.id === 'string' && typeof block.name === 'string') {
|
|
401
|
+
const result = results.get(block.id);
|
|
402
|
+
const status = result ? (result.isError === true ? 'error' : 'ok') : pending.has(block.id) ? 'running' : 'pending';
|
|
403
|
+
const resultImageCount = result ? imageCount(result.content) : 0;
|
|
404
|
+
const id = `m${index}.c${textIndex++}`;
|
|
405
|
+
const todo = (block.name === 'todo_write' || block.name === 'checklist_write') && result?.isError !== true ? todoEntries(block.arguments) : null;
|
|
406
|
+
if (todo) {
|
|
407
|
+
items.push({ kind: 'todo', id, origin: 'tool', toolCallId: block.id, status, items: todo });
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
items.push({
|
|
411
|
+
kind: 'tool', id, toolCallId: block.id, name: block.name,
|
|
412
|
+
summary: argumentSummary(block.name, block.arguments), status,
|
|
413
|
+
output: result && resultImageCount === 0 ? toolOutput(result) : '',
|
|
414
|
+
durationMs: result ? numberField(result, 'durationMs') : undefined
|
|
415
|
+
});
|
|
416
|
+
if (result && resultImageCount > 0) {
|
|
417
|
+
items.push({
|
|
418
|
+
kind: 'image-result', id: `${id}.image-result`, text: toolOutput(result),
|
|
419
|
+
imageCount: resultImageCount, images: this.showImages ? imageContents(result.content) : [],
|
|
420
|
+
showImages: this.showImages
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const stop = stringField(message, 'stopReason');
|
|
426
|
+
const error = stringField(message, 'errorMessage');
|
|
427
|
+
if (stop === 'error' && error)
|
|
428
|
+
items.push({ kind: 'notice', id: `m${index}.error`, text: error, level: 'error' });
|
|
429
|
+
if (stop === 'aborted')
|
|
430
|
+
items.push({ kind: 'notice', id: `m${index}.aborted`, text: 'Stopped by the person.', level: 'warning' });
|
|
431
|
+
return items;
|
|
432
|
+
}
|
|
433
|
+
if (role === 'bashExecution') {
|
|
434
|
+
const command = stringField(message, 'command') ?? '';
|
|
435
|
+
if (!command)
|
|
436
|
+
return items;
|
|
437
|
+
items.push({
|
|
438
|
+
kind: 'bash', id: `m${index}`, command, timestamp: numberField(message, 'timestamp') ?? 0,
|
|
439
|
+
output: clip(ansiPlain(stringField(message, 'output') ?? '')),
|
|
440
|
+
running: false, exitCode: numberField(message, 'exitCode'), cancelled: message.cancelled === true
|
|
441
|
+
});
|
|
442
|
+
return items;
|
|
443
|
+
}
|
|
444
|
+
if (role === 'custom') {
|
|
445
|
+
if (message.display === false)
|
|
446
|
+
return items;
|
|
447
|
+
const text = contentText(message.content);
|
|
448
|
+
if (!text.trim())
|
|
449
|
+
return items;
|
|
450
|
+
items.push({ kind: 'custom', id: `m${index}`, label: stringField(message, 'customType') ?? 'Extension', text });
|
|
451
|
+
return items;
|
|
452
|
+
}
|
|
453
|
+
if (role === 'compactionSummary') {
|
|
454
|
+
const summary = stringField(message, 'summary') ?? '';
|
|
455
|
+
if (!summary.trim())
|
|
456
|
+
return items;
|
|
457
|
+
items.push({ kind: 'compaction', id: `m${index}`, summary, tokensBefore: numberField(message, 'tokensBefore') });
|
|
458
|
+
return items;
|
|
459
|
+
}
|
|
460
|
+
if (role === 'branchSummary') {
|
|
461
|
+
const text = stringField(message, 'summary') ?? '';
|
|
462
|
+
if (text.trim())
|
|
463
|
+
items.push({ kind: 'summary', id: `m${index}`, label: 'Branch summary', text });
|
|
464
|
+
}
|
|
465
|
+
return items;
|
|
466
|
+
}
|
|
467
|
+
stabilize(items) {
|
|
468
|
+
const stable = items.map(item => {
|
|
469
|
+
const cached = this.cachedItems.get(item.id);
|
|
470
|
+
if (cached && sameTranscriptItem(cached, item))
|
|
471
|
+
return cached;
|
|
472
|
+
this.cachedItems.set(item.id, item);
|
|
473
|
+
return item;
|
|
474
|
+
});
|
|
475
|
+
const currentIds = new Set(stable.map(item => item.id));
|
|
476
|
+
for (const id of this.cachedItems.keys()) {
|
|
477
|
+
if (!currentIds.has(id))
|
|
478
|
+
this.cachedItems.delete(id);
|
|
479
|
+
}
|
|
480
|
+
return stable;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TerminalSubagentActivity } from "../subagentMonitor.js";
|
|
2
2
|
export type SubagentTone = 'success' | 'warning' | 'error' | 'muted' | 'accent';
|
|
3
|
-
/** One roster row: status mark, profile, session name,
|
|
3
|
+
/** One roster row: status mark, profile, session name, todo progress, in that order. */
|
|
4
4
|
export interface SubagentRowView {
|
|
5
5
|
id: string;
|
|
6
6
|
mark: string;
|
|
@@ -10,7 +10,7 @@ export interface SubagentRowView {
|
|
|
10
10
|
/** The profile's colour name (PROP-0029); null for a copy of the parent. */
|
|
11
11
|
color: string | null;
|
|
12
12
|
title: string;
|
|
13
|
-
/** `n of m tasks`, or null when the child wrote no
|
|
13
|
+
/** `n of m tasks`, or null when the child wrote no todo. */
|
|
14
14
|
tasks: string | null;
|
|
15
15
|
}
|
|
16
16
|
export interface SubagentRosterView {
|
|
@@ -18,7 +18,7 @@ export interface SubagentRosterView {
|
|
|
18
18
|
/** `+N more · /subagents` when rows were left out. */
|
|
19
19
|
overflow: string | null;
|
|
20
20
|
}
|
|
21
|
-
export declare function subagentTasks(
|
|
21
|
+
export declare function subagentTasks(todo: TerminalSubagentActivity['todo']): string | null;
|
|
22
22
|
/**
|
|
23
23
|
* The compact live roster shown under the status line: active children first, at most three
|
|
24
24
|
* rows. The session name is cut first when the terminal is narrow.
|
|
@@ -21,24 +21,26 @@ function mark(status, frame) {
|
|
|
21
21
|
case 'running': return { mark: frame, tone: 'warning' };
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
export function subagentTasks(
|
|
25
|
-
return
|
|
24
|
+
export function subagentTasks(todo) {
|
|
25
|
+
return todo && todo.total > 0 ? `${todo.done} of ${todo.total} tasks` : null;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* The compact live roster shown under the status line: active children first, at most three
|
|
29
29
|
* rows. The session name is cut first when the terminal is narrow.
|
|
30
30
|
*/
|
|
31
31
|
export function subagentRoster(items, width, frame = getWorkingSpinnerFrames()[0] ?? '') {
|
|
32
|
-
|
|
32
|
+
// The status line already describes the main agent; its row is only on the /subagents page.
|
|
33
|
+
const children = items.filter(item => item.relation !== 'main');
|
|
34
|
+
if (!children.length || width < 1)
|
|
33
35
|
return null;
|
|
34
|
-
const ordered = [...
|
|
36
|
+
const ordered = [...children].sort((left, right) => {
|
|
35
37
|
const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
|
|
36
38
|
return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
|
|
37
39
|
});
|
|
38
40
|
const rows = ordered.slice(0, MAX_VISIBLE_ROWS).map((item) => {
|
|
39
41
|
const status = mark(item.status, frame);
|
|
40
42
|
const profile = cut(safe(item.profile?.name ?? '') || 'subagent', Math.max(1, Math.min(24, width - 2)));
|
|
41
|
-
const tasks = subagentTasks(item.
|
|
43
|
+
const tasks = subagentTasks(item.todo);
|
|
42
44
|
const used = 2 + visibleWidth(profile) + GAP.length + (tasks ? GAP.length + tasks.length : 0);
|
|
43
45
|
const title = cut(safe(item.title) || 'Subagent', Math.max(1, width - used));
|
|
44
46
|
return { id: item.id, mark: status.mark, tone: status.tone, profile, color: item.profile?.color ?? null, title, tasks };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface OpenTuiTheme {
|
|
2
|
+
background: string;
|
|
3
|
+
panel: string;
|
|
4
|
+
/** The translucent layer that dims the console behind a centered page dialog. */
|
|
5
|
+
scrim: string;
|
|
6
|
+
border: string;
|
|
7
|
+
borderFocus: string;
|
|
8
|
+
text: string;
|
|
9
|
+
muted: string;
|
|
10
|
+
dim: string;
|
|
11
|
+
/** The shared selected-row field and its two text tones. */
|
|
12
|
+
selectionBg: string;
|
|
13
|
+
selectionText: string;
|
|
14
|
+
selectionMuted: string;
|
|
15
|
+
questionSelectedBg: string;
|
|
16
|
+
questionActionBg: string;
|
|
17
|
+
questionActionText: string;
|
|
18
|
+
primary: string;
|
|
19
|
+
accent: string;
|
|
20
|
+
success: string;
|
|
21
|
+
warning: string;
|
|
22
|
+
error: string;
|
|
23
|
+
/** The composer's Bash mode marker and rules; the command body keeps the normal text color. */
|
|
24
|
+
bashMode: string;
|
|
25
|
+
/** The composer's rules while plan mode is on. */
|
|
26
|
+
planMode: string;
|
|
27
|
+
/** Image and pasted-text labels in the composer and in sent messages. */
|
|
28
|
+
attachment: string;
|
|
29
|
+
purple: string;
|
|
30
|
+
cyan: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const opentuiTheme: OpenTuiTheme;
|
|
33
|
+
/** The colour of an agent profile's name (PROP-0029); an unknown name falls back to the accent. */
|
|
34
|
+
export declare function profileColor(theme: OpenTuiTheme, name: string): string;
|
|
35
|
+
export declare function blendHexColor(from: string, to: string, amount: number): string;
|
|
36
|
+
export declare function composerRuleColor(theme: OpenTuiTheme, mode: {
|
|
37
|
+
bash: boolean;
|
|
38
|
+
plan: boolean;
|
|
39
|
+
overlay: boolean;
|
|
40
|
+
}): string;
|
|
41
|
+
export declare function noticeColor(theme: OpenTuiTheme, type: string): string;
|
|
42
|
+
export declare function toolColor(theme: OpenTuiTheme, status: 'running' | 'ok' | 'error' | 'pending'): string;
|
|
43
|
+
export declare function thinkingLevelColor(theme: OpenTuiTheme, level: string): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// OpenCode's terminal palette, carried by the Harness's own product name.
|
|
2
|
+
import { SELECTION_COLORS } from "../tui/selection.js";
|
|
3
|
+
export const opentuiTheme = {
|
|
4
|
+
background: '#0b0b0d',
|
|
5
|
+
panel: '#111114',
|
|
6
|
+
scrim: '#0b0b0d99',
|
|
7
|
+
border: '#2a2a31',
|
|
8
|
+
borderFocus: '#3a3a44',
|
|
9
|
+
text: '#e6e6e9',
|
|
10
|
+
muted: '#8b8b95',
|
|
11
|
+
dim: '#5c5c66',
|
|
12
|
+
selectionBg: SELECTION_COLORS.dark.background,
|
|
13
|
+
selectionText: SELECTION_COLORS.dark.text,
|
|
14
|
+
selectionMuted: SELECTION_COLORS.dark.muted,
|
|
15
|
+
questionSelectedBg: '#193322',
|
|
16
|
+
questionActionBg: '#35754a',
|
|
17
|
+
questionActionText: '#f7fff8',
|
|
18
|
+
primary: '#fab283',
|
|
19
|
+
accent: '#8ab4f8',
|
|
20
|
+
success: '#a6da95',
|
|
21
|
+
warning: '#eed49f',
|
|
22
|
+
error: '#ed8796',
|
|
23
|
+
bashMode: '#00ff88',
|
|
24
|
+
planMode: '#8ab4f8',
|
|
25
|
+
attachment: '#5ea1ff',
|
|
26
|
+
purple: '#c6a0f6',
|
|
27
|
+
cyan: '#8bd5ca'
|
|
28
|
+
};
|
|
29
|
+
/** The colour of an agent profile's name (PROP-0029); an unknown name falls back to the accent. */
|
|
30
|
+
export function profileColor(theme, name) {
|
|
31
|
+
switch (name) {
|
|
32
|
+
case 'blue': return theme.accent;
|
|
33
|
+
case 'green': return theme.success;
|
|
34
|
+
case 'yellow': return theme.warning;
|
|
35
|
+
case 'red': return theme.error;
|
|
36
|
+
case 'magenta': return '#f5bde6';
|
|
37
|
+
case 'cyan': return theme.cyan;
|
|
38
|
+
case 'orange': return theme.primary;
|
|
39
|
+
case 'purple': return theme.purple;
|
|
40
|
+
case 'main': return theme.primary;
|
|
41
|
+
default: return theme.accent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function blendHexColor(from, to, amount) {
|
|
45
|
+
const start = /^#([\da-f]{6})$/iu.exec(from)?.[1];
|
|
46
|
+
const end = /^#([\da-f]{6})$/iu.exec(to)?.[1];
|
|
47
|
+
if (!start || !end)
|
|
48
|
+
return from;
|
|
49
|
+
const weight = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 0;
|
|
50
|
+
const channels = [0, 2, 4].map(offset => {
|
|
51
|
+
const first = Number.parseInt(start.slice(offset, offset + 2), 16);
|
|
52
|
+
const second = Number.parseInt(end.slice(offset, offset + 2), 16);
|
|
53
|
+
return Math.round(first + (second - first) * weight).toString(16).padStart(2, '0');
|
|
54
|
+
});
|
|
55
|
+
return `#${channels.join('')}`;
|
|
56
|
+
}
|
|
57
|
+
export function composerRuleColor(theme, mode) {
|
|
58
|
+
if (mode.bash)
|
|
59
|
+
return theme.bashMode;
|
|
60
|
+
if (mode.plan)
|
|
61
|
+
return theme.planMode;
|
|
62
|
+
return mode.overlay ? theme.border : theme.dim;
|
|
63
|
+
}
|
|
64
|
+
export function noticeColor(theme, type) {
|
|
65
|
+
if (type === 'error')
|
|
66
|
+
return theme.error;
|
|
67
|
+
if (type === 'warning')
|
|
68
|
+
return theme.warning;
|
|
69
|
+
return theme.muted;
|
|
70
|
+
}
|
|
71
|
+
export function toolColor(theme, status) {
|
|
72
|
+
if (status === 'error')
|
|
73
|
+
return theme.error;
|
|
74
|
+
if (status === 'running')
|
|
75
|
+
return theme.warning;
|
|
76
|
+
if (status === 'pending')
|
|
77
|
+
return theme.dim;
|
|
78
|
+
return theme.success;
|
|
79
|
+
}
|
|
80
|
+
export function thinkingLevelColor(theme, level) {
|
|
81
|
+
if (level === 'off')
|
|
82
|
+
return theme.dim;
|
|
83
|
+
if (level === 'minimal' || level === 'low')
|
|
84
|
+
return theme.muted;
|
|
85
|
+
if (level === 'medium')
|
|
86
|
+
return theme.accent;
|
|
87
|
+
return theme.primary;
|
|
88
|
+
}
|