@xingular/harness 1.1.1 → 1.2.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 +13 -0
- package/README.md +28 -25
- package/dist/cli.js +6675 -2501
- 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 +15 -1
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +2 -1
- package/dist/sdk/agents/profiles.js +9 -4
- 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/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 +250 -0
- package/dist/sdk/commands/registry.js +47 -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/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- 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/index.d.ts +6 -1
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +29 -12
- 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/inventory.js +4 -3
- 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 -29
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +18 -0
- 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 +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +168 -0
- package/dist/sdk/sessions/contracts.js +59 -1
- 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 +73 -16
- 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 +35 -0
- 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 +202 -17
- package/dist/sdk/sessions/subagentService.d.ts +9 -0
- package/dist/sdk/sessions/subagentService.js +63 -4
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -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 +11 -3
- package/dist/sdk/terminal/controller.js +40 -13
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +154 -54
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
- package/dist/sdk/terminal/subagentMonitor.js +32 -1
- package/dist/sdk/terminal/terminals.d.ts +19 -2
- package/dist/sdk/terminal/terminals.js +245 -35
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +205 -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 +13 -4
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- 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 +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
- 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/checklist.js +1 -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/spawnSubagent.js +4 -3
- 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 +5207 -1883
- package/dist/terminal-pi.js +1658 -271
- package/docs/architecture/02-sessions-and-runs.md +128 -26
- package/docs/architecture/04-prompt.md +50 -12
- package/docs/architecture/06-plugins-and-capabilities.md +24 -7
- package/docs/architecture/07-tools-and-gate.md +104 -24
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +31 -1
- package/docs/architecture/10-server-protocol.md +58 -3
- package/docs/architecture/11-terminal.md +105 -42
- 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 +54 -7
- package/docs/architecture/README.md +3 -1
- 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 +247 -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/README.md +14 -0
- 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/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 +80 -24
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -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 +100 -59
- package/package.json +4 -3
|
@@ -15,6 +15,7 @@ export interface TerminalCommandHost {
|
|
|
15
15
|
control: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>;
|
|
16
16
|
ui: ExtensionUIContext;
|
|
17
17
|
cwd(): string;
|
|
18
|
+
branch(): string | null;
|
|
18
19
|
sessionDirectory(): string;
|
|
19
20
|
refresh(): void;
|
|
20
21
|
changed(): void;
|
|
@@ -43,4 +44,5 @@ export declare function createTerminalCommands(host: TerminalCommandHost): {
|
|
|
43
44
|
completions: () => TerminalCommandCompletion[];
|
|
44
45
|
skills: () => SkillEntry[];
|
|
45
46
|
expandSkill: (text: string) => string;
|
|
47
|
+
legacyAliases: () => string[];
|
|
46
48
|
};
|
|
@@ -1,49 +1,34 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
2
|
import { SessionManager } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { CORE_COMMANDS, coreCommand } from "../../commands/registry.js";
|
|
4
|
+
import { commandRoutes, promptCommandAvailable } from "../../commands/routes.js";
|
|
3
5
|
import { BUILTIN_PLUGIN_REGISTRY } from "../../plugins/builtin.js";
|
|
6
|
+
import { pluginCommandPrefix } from "../../plugins/commandPrefix.js";
|
|
4
7
|
import { PageOverlay } from "./pageOverlay.js";
|
|
5
8
|
import { ModelPicker } from "./modelPicker.js";
|
|
6
9
|
import { TreePicker } from "./treePicker.js";
|
|
7
10
|
import { createCommandPage } from "./commandPages.js";
|
|
8
11
|
import { pageOverlayOptions } from "./dialogGeometry.js";
|
|
12
|
+
import { ResumeSessionsPage } from "./sessionPicker.js";
|
|
9
13
|
import { expandSkillInvocation } from "./skillInvocation.js";
|
|
10
14
|
import { groupTerminalCommands } from "../commandHelp.js";
|
|
11
15
|
import { createBuiltinPluginCommandHandlers } from "./pluginCommands.js";
|
|
12
16
|
export { parseAnnotate } from "./pluginCommands.js";
|
|
13
|
-
const BUILTINS =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ name: 'session', aliases: ['resume'], description: 'Choose a saved session in this account and folder', group: 'Session' },
|
|
20
|
-
{ name: 'history', aliases: ['tree'], description: 'Navigate branches in the current session', group: 'Session' },
|
|
21
|
-
{ name: 'fork', description: 'Fork from an earlier message', group: 'Session' },
|
|
22
|
-
{ name: 'clone', description: 'Clone this branch into a new session', group: 'Session' },
|
|
23
|
-
{ name: 'model', description: 'Choose the model', group: 'Model' },
|
|
24
|
-
{ name: 'effort', description: 'Choose the thinking effort', group: 'Model' },
|
|
25
|
-
{ name: 'thinking', description: 'Toggle transcript thinking visibility: /thinking [show|hide]', group: 'Model' },
|
|
26
|
-
{ name: 'name', description: 'Name this session', group: 'Session' },
|
|
27
|
-
{ name: 'compact', description: 'Compact the context', group: 'Session' },
|
|
28
|
-
{ name: 'copy', description: 'Copy the last reply', group: 'Inspect' },
|
|
29
|
-
{ name: 'export', description: 'Export this branch to HTML or JSONL', group: 'Inspect' },
|
|
30
|
-
{ name: 'reload', description: 'Reload trusted resources and keybindings', group: 'Harness' },
|
|
31
|
-
{ name: 'accounts', description: 'Sign in, switch, check and remove Pi accounts', group: 'Harness' },
|
|
32
|
-
{ name: 'permissions', description: 'Full Permission and per-tool approvals', group: 'Harness' },
|
|
33
|
-
{ name: 'harness', description: 'Browse foundation, variant and all resources', group: 'Harness' },
|
|
34
|
-
{ name: 'skills', description: 'View, inspect and use skills; discover more', group: 'Harness' },
|
|
35
|
-
{ name: 'connections', description: 'Manage local and remote MCP connections', group: 'Harness' },
|
|
36
|
-
{ name: 'mcp', description: 'Open Connections in the Harness browser', group: 'Harness' },
|
|
37
|
-
{ name: 'tools', description: 'Inspect available tools, owners and approvals', group: 'Harness' },
|
|
38
|
-
{ name: 'plugins', description: 'Switch plugins on or off and trust their code', group: 'Harness' },
|
|
39
|
-
{ name: 'hub', description: 'Browse and install reviewed skills, plugins and documents', group: 'Harness' },
|
|
40
|
-
{ name: 'components', description: 'Browse skills, prompts and rules for this Harness scope', group: 'Harness' },
|
|
41
|
-
{ name: 'settings', description: 'Session, terminal and browser settings', group: 'Harness' },
|
|
42
|
-
{ name: 'log', description: "Harness diagnostics and this conversation's run events", group: 'Inspect' },
|
|
43
|
-
{ name: 'hotkeys', description: 'Keyboard controls', group: 'Core' },
|
|
44
|
-
{ name: 'quit', description: 'Restore the terminal and exit', group: 'Core' }
|
|
45
|
-
];
|
|
17
|
+
const BUILTINS = CORE_COMMANDS.map((command) => ({
|
|
18
|
+
name: command.name,
|
|
19
|
+
...('aliases' in command ? { aliases: [...command.aliases] } : {}),
|
|
20
|
+
description: command.description,
|
|
21
|
+
group: command.group
|
|
22
|
+
}));
|
|
46
23
|
const COMMAND_ALIASES = new Map(BUILTINS.flatMap(command => (command.aliases ?? []).map(alias => [alias, command.name])));
|
|
24
|
+
const CORE_PROMPT_NAMES = new Set([...BUILTINS.map(command => command.name), ...COMMAND_ALIASES.keys()]);
|
|
25
|
+
const LEGACY_BUILTIN_COMMAND_ALIASES = new Map([
|
|
26
|
+
['doctor:doctor', 'doctor'], ['plugin:doctor', 'doctor'],
|
|
27
|
+
['worktrees:worktree', 'worktree'], ['plugin:worktree', 'worktree'],
|
|
28
|
+
['history:trajectory', 'trajectory'], ['plugin:trajectory', 'trajectory'],
|
|
29
|
+
['subagents:subagents', 'subagents'], ['plugin:subagents', 'subagents'],
|
|
30
|
+
['subagents:agents', 'agents'], ['plugin:agents', 'agents']
|
|
31
|
+
]);
|
|
47
32
|
export function createTerminalCommands(host) {
|
|
48
33
|
const notify = (text) => host.ui.notify(text, 'info');
|
|
49
34
|
const choose = async (title, items, label) => {
|
|
@@ -70,37 +55,31 @@ export function createTerminalCommands(host) {
|
|
|
70
55
|
}
|
|
71
56
|
const pluginHandlers = createBuiltinPluginCommandHandlers(host, openPage);
|
|
72
57
|
const pluginCommands = BUILTIN_PLUGIN_REGISTRY.commands();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
if (command.namespace === 'unprefixed' && coreNames.has(command.name)) {
|
|
76
|
-
throw new Error(`Required plugin command collides with a core command: ${command.pluginId}:${command.name}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
58
|
+
const builtinPrefixes = new Map(pluginCommands.map(command => [command.prefix, command.pluginId]));
|
|
59
|
+
const prefixCache = new WeakMap();
|
|
79
60
|
const extensionCommands = () => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
return { unique, duplicates };
|
|
61
|
+
return commandRoutes(host.session().extensionRunner.getRegisteredCommands().map(command => {
|
|
62
|
+
const source = command.sourceInfo;
|
|
63
|
+
let prefix = prefixCache.get(source);
|
|
64
|
+
if (!prefix) {
|
|
65
|
+
prefix = pluginCommandPrefix(source);
|
|
66
|
+
prefixCache.set(source, prefix);
|
|
67
|
+
}
|
|
68
|
+
const owner = source.origin === 'package'
|
|
69
|
+
? `package:${source.baseDir ?? source.source}`
|
|
70
|
+
: `extension:${source.path}`;
|
|
71
|
+
return { ...command, prefix, owner, path: source.path };
|
|
72
|
+
}));
|
|
93
73
|
};
|
|
94
|
-
const
|
|
74
|
+
const coreCommandNames = (session) => new Set([
|
|
95
75
|
...BUILTINS.flatMap(command => [command.name, ...(command.aliases ?? [])]),
|
|
96
|
-
...pluginCommands.filter(command => command.namespace === 'unprefixed').map(command => command.name),
|
|
97
76
|
...session.resourceLoader.getPrompts().prompts.map(prompt => prompt.name)
|
|
98
77
|
]);
|
|
99
|
-
const runBuiltinPluginCommand = async (
|
|
78
|
+
const runBuiltinPluginCommand = async (command, argument) => {
|
|
100
79
|
try {
|
|
101
|
-
const handler = pluginHandlers.get(name);
|
|
80
|
+
const handler = pluginHandlers.get(`${command.pluginId}:${command.name}`);
|
|
102
81
|
if (!handler)
|
|
103
|
-
throw new Error(`No terminal handler for built-in plugin command ${name}.`);
|
|
82
|
+
throw new Error(`No terminal handler for built-in plugin command ${command.pluginId}:${command.name}.`);
|
|
104
83
|
await handler(argument);
|
|
105
84
|
host.refresh();
|
|
106
85
|
}
|
|
@@ -108,33 +87,141 @@ export function createTerminalCommands(host) {
|
|
|
108
87
|
host.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
109
88
|
}
|
|
110
89
|
};
|
|
90
|
+
const runExtensionCommand = async (registered, argument, route) => {
|
|
91
|
+
try {
|
|
92
|
+
const command = host.session().extensionRunner.getCommand(registered.invocationName);
|
|
93
|
+
if (!command)
|
|
94
|
+
throw new Error(`Plugin command /${route} is no longer registered. Reload trusted resources.`);
|
|
95
|
+
await command.handler(argument, host.session().extensionRunner.createCommandContext());
|
|
96
|
+
host.refresh();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
host.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const runLegacyPluginCommand = async (name, argument) => {
|
|
103
|
+
const session = host.session();
|
|
104
|
+
if (coreCommandNames(session).has(name)) {
|
|
105
|
+
host.ui.notify(`Use /${name}; /plugin:${name} is unavailable because a core command or prompt already uses that name.`, 'error');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const builtins = BUILTIN_PLUGIN_REGISTRY.commandsNamed(name);
|
|
109
|
+
const extensions = extensionCommands();
|
|
110
|
+
if (builtins.length > 1) {
|
|
111
|
+
host.ui.notify(`Legacy plugin command /plugin:${name} is ambiguous. Use its owner-qualified command.`, 'error');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (builtins.length === 1) {
|
|
115
|
+
const competitors = extensions.byName.get(name) ?? [];
|
|
116
|
+
if (competitors.length) {
|
|
117
|
+
host.ui.notify(`Extension command /plugin:${name} from ${competitors.map(command => command.sourceInfo.path).join(', ')} is unavailable because the name belongs to the built-in ${builtins[0].pluginId} plugin.`, 'error');
|
|
118
|
+
}
|
|
119
|
+
await runBuiltinPluginCommand(builtins[0], argument);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const matches = extensions.byName.get(name) ?? [];
|
|
123
|
+
if (matches.length > 1) {
|
|
124
|
+
host.ui.notify(`Legacy plugin command /plugin:${name} is ambiguous. Registered by: ${matches.map(command => command.sourceInfo.path).join(', ')}. Use its owner-qualified command.`, 'error');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (matches.length === 1) {
|
|
128
|
+
if (extensions.conflictingPrefixes.has(matches[0].prefix)) {
|
|
129
|
+
host.ui.notify(`Plugin prefix /${matches[0].prefix}: is ambiguous or reserved. Owners: ${extensions.conflictingPrefixes.get(matches[0].prefix).join(', ')}.`, 'error');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await runExtensionCommand(matches[0], argument, `plugin:${name}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
host.ui.notify(`Unknown plugin command: /plugin:${name}. Use /help to list the commands.`, 'error');
|
|
136
|
+
};
|
|
137
|
+
const runNamespacedCommand = async (typedName, argument) => {
|
|
138
|
+
const separator = typedName.indexOf(':');
|
|
139
|
+
if (separator < 1)
|
|
140
|
+
return false;
|
|
141
|
+
const prefix = typedName.slice(0, separator);
|
|
142
|
+
const name = typedName.slice(separator + 1);
|
|
143
|
+
if (prefix === 'plugin') {
|
|
144
|
+
await runLegacyPluginCommand(name, argument);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const builtin = name ? BUILTIN_PLUGIN_REGISTRY.command(prefix, name) : BUILTIN_PLUGIN_REGISTRY.defaultCommand(prefix);
|
|
148
|
+
const extensions = extensionCommands();
|
|
149
|
+
const isBuiltinPrefix = builtinPrefixes.has(prefix);
|
|
150
|
+
const isExtensionPrefix = extensions.ownersByPrefix.has(prefix);
|
|
151
|
+
if (!builtin && !isBuiltinPrefix && !isExtensionPrefix)
|
|
152
|
+
return false;
|
|
153
|
+
if (builtin) {
|
|
154
|
+
const competitors = extensions.routes.get(`${prefix}:${builtin.name}`) ?? [];
|
|
155
|
+
if (competitors.length) {
|
|
156
|
+
host.ui.notify(`Extension command /${prefix}:${builtin.name} from ${competitors.map(command => command.sourceInfo.path).join(', ')} is unavailable because the prefix belongs to built-in ${builtin.pluginId}.`, 'error');
|
|
157
|
+
}
|
|
158
|
+
await runBuiltinPluginCommand(builtin, argument);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (extensions.conflictingPrefixes.has(prefix)) {
|
|
162
|
+
host.ui.notify(`Plugin prefix /${prefix}: is ambiguous or reserved. Owners: ${extensions.conflictingPrefixes.get(prefix).join(', ')}.`, 'error');
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (!name) {
|
|
166
|
+
host.ui.notify(`Plugin /${prefix}: has no default command. Use /help to list its commands.`, 'error');
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const matches = extensions.routes.get(`${prefix}:${name}`) ?? [];
|
|
170
|
+
if (matches.length > 1) {
|
|
171
|
+
host.ui.notify(`Plugin command /${prefix}:${name} is ambiguous. Registered by: ${matches.map(command => command.sourceInfo.path).join(', ')}.`, 'error');
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (matches.length === 1) {
|
|
175
|
+
await runExtensionCommand(matches[0], argument, `${prefix}:${name}`);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
host.ui.notify(`Unknown plugin command: /${typedName}. Use /help to list the commands.`, 'error');
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
181
|
+
const legacyAliases = () => {
|
|
182
|
+
const session = host.session();
|
|
183
|
+
const coreNames = coreCommandNames(session);
|
|
184
|
+
const aliases = new Set(LEGACY_BUILTIN_COMMAND_ALIASES.keys());
|
|
185
|
+
for (const command of pluginCommands) {
|
|
186
|
+
if (coreNames.has(command.name))
|
|
187
|
+
continue;
|
|
188
|
+
aliases.add(`plugin:${command.name}`);
|
|
189
|
+
if (command.legacyBare)
|
|
190
|
+
aliases.add(command.name);
|
|
191
|
+
}
|
|
192
|
+
for (const name of extensionCommands().byName.keys()) {
|
|
193
|
+
if (!coreNames.has(name))
|
|
194
|
+
aliases.add(`plugin:${name}`);
|
|
195
|
+
}
|
|
196
|
+
return [...aliases];
|
|
197
|
+
};
|
|
111
198
|
const completions = () => {
|
|
112
199
|
const session = host.session();
|
|
113
200
|
const prompts = session.resourceLoader.getPrompts().prompts;
|
|
114
201
|
const commands = new Map(BUILTINS.map(command => [command.name, command]));
|
|
115
|
-
const bareNames = unprefixedCommandNames(session);
|
|
116
|
-
const namespacedBuiltins = new Set();
|
|
117
202
|
for (const command of pluginCommands) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
203
|
+
const invocation = `${command.prefix}:${command.name}`;
|
|
204
|
+
commands.set(invocation, { name: invocation, description: command.description, group: 'Plugins' });
|
|
205
|
+
}
|
|
206
|
+
for (const module of BUILTIN_PLUGIN_REGISTRY.modules) {
|
|
207
|
+
const defaultCommand = module.defaultCommand && BUILTIN_PLUGIN_REGISTRY.command(module.commandPrefix ?? module.descriptor.id, module.defaultCommand);
|
|
208
|
+
if (defaultCommand) {
|
|
209
|
+
const invocation = `${defaultCommand.prefix}:`;
|
|
210
|
+
commands.set(invocation, { name: invocation, description: defaultCommand.description, group: 'Plugins' });
|
|
127
211
|
}
|
|
128
212
|
}
|
|
129
213
|
const extensions = extensionCommands();
|
|
130
|
-
for (const [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
214
|
+
for (const [route, matches] of extensions.routes) {
|
|
215
|
+
const prefix = route.slice(0, route.indexOf(':'));
|
|
216
|
+
if (matches.length !== 1 || extensions.conflictingPrefixes.has(prefix))
|
|
217
|
+
continue;
|
|
218
|
+
if (BUILTIN_PLUGIN_REGISTRY.command(prefix, matches[0].name))
|
|
219
|
+
continue;
|
|
220
|
+
commands.set(route, { name: route, description: matches[0].description, group: 'Plugins' });
|
|
135
221
|
}
|
|
222
|
+
const extensionNames = new Set(extensions.byName.keys());
|
|
136
223
|
for (const prompt of prompts) {
|
|
137
|
-
if (
|
|
224
|
+
if (promptCommandAvailable(prompt.name, new Set([...commands.keys(), ...COMMAND_ALIASES.keys()]), extensionNames))
|
|
138
225
|
commands.set(prompt.name, { name: prompt.name, description: prompt.description, group: 'Prompts' });
|
|
139
226
|
}
|
|
140
227
|
return [...commands.values()];
|
|
@@ -145,65 +232,34 @@ export function createTerminalCommands(host) {
|
|
|
145
232
|
return false;
|
|
146
233
|
const typedName = match[1];
|
|
147
234
|
const argument = match[2]?.trim() ?? '';
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const collisions = extensions.unique.get(pluginName) ? [extensions.unique.get(pluginName)] : extensions.duplicates.get(pluginName) ?? [];
|
|
159
|
-
if (collisions.length) {
|
|
160
|
-
const owners = collisions.map(command => command.sourceInfo.path).join(', ');
|
|
161
|
-
host.ui.notify(`Extension command /plugin:${pluginName} from ${owners} is unavailable because the name belongs to the built-in ${builtin.pluginId} plugin.`, 'error');
|
|
162
|
-
}
|
|
163
|
-
await runBuiltinPluginCommand(pluginName, argument);
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
const duplicate = extensions.duplicates.get(pluginName);
|
|
167
|
-
if (duplicate) {
|
|
168
|
-
const owners = duplicate.map(command => command.sourceInfo.path).join(', ');
|
|
169
|
-
host.ui.notify(`Plugin command /plugin:${pluginName} is ambiguous. Registered by: ${owners}. Rename or remove one command.`, 'error');
|
|
235
|
+
const legacyBuiltin = LEGACY_BUILTIN_COMMAND_ALIASES.get(typedName);
|
|
236
|
+
if (!legacyBuiltin && await runNamespacedCommand(typedName, argument))
|
|
237
|
+
return true;
|
|
238
|
+
const session = host.session();
|
|
239
|
+
const name = coreCommand(legacyBuiltin ?? typedName)?.name ?? legacyBuiltin ?? typedName;
|
|
240
|
+
const builtin = BUILTINS.find(item => item.name === name);
|
|
241
|
+
if (!builtin && !coreCommandNames(session).has(name)) {
|
|
242
|
+
const legacyBare = BUILTIN_PLUGIN_REGISTRY.commandsNamed(name).filter(command => command.legacyBare);
|
|
243
|
+
if (legacyBare.length > 1) {
|
|
244
|
+
host.ui.notify(`Legacy command /${name} is ambiguous. Use its owner-qualified command.`, 'error');
|
|
170
245
|
return true;
|
|
171
246
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const command = host.session().extensionRunner.getCommand(registered.invocationName);
|
|
176
|
-
if (!command)
|
|
177
|
-
throw new Error(`Plugin command /plugin:${pluginName} is no longer registered. Reload trusted resources.`);
|
|
178
|
-
await command.handler(argument, host.session().extensionRunner.createCommandContext());
|
|
179
|
-
host.refresh();
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
host.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
183
|
-
}
|
|
247
|
+
if (legacyBare.length === 1) {
|
|
248
|
+
await runBuiltinPluginCommand(legacyBare[0], argument);
|
|
184
249
|
return true;
|
|
185
250
|
}
|
|
186
|
-
host.ui.notify(`Unknown plugin command: /${typedName}. Use /help to list the commands.`, 'error');
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
const name = COMMAND_ALIASES.get(typedName) ?? typedName;
|
|
190
|
-
const builtin = BUILTINS.find(item => item.name === name);
|
|
191
|
-
const requiredPluginCommand = BUILTIN_PLUGIN_REGISTRY.command(name);
|
|
192
|
-
if (!builtin && requiredPluginCommand?.namespace === 'unprefixed') {
|
|
193
|
-
await runBuiltinPluginCommand(name, argument);
|
|
194
|
-
return true;
|
|
195
251
|
}
|
|
196
252
|
if (!builtin) {
|
|
197
253
|
// A skill is offered as $name; the typed /skill:name form still reaches the SDK, which expands it.
|
|
198
254
|
if (skills().some(skill => `skill:${skill.name}` === name))
|
|
199
255
|
return false;
|
|
200
|
-
|
|
256
|
+
const prompt = session.resourceLoader.getPrompts().prompts.some(item => item.name === name);
|
|
257
|
+
if (prompt && promptCommandAvailable(name, CORE_PROMPT_NAMES, new Set(extensionCommands().byName.keys())))
|
|
201
258
|
return false;
|
|
202
259
|
host.ui.notify(`Unknown command: /${name}. Use /help to list commands.`, 'error');
|
|
203
260
|
return true;
|
|
204
261
|
}
|
|
205
262
|
try {
|
|
206
|
-
const session = host.session();
|
|
207
263
|
switch (name) {
|
|
208
264
|
case 'help':
|
|
209
265
|
host.showHelp(groupTerminalCommands(completions()));
|
|
@@ -221,7 +277,7 @@ export function createTerminalCommands(host) {
|
|
|
221
277
|
await host.btw(argument);
|
|
222
278
|
break;
|
|
223
279
|
case 'stop': {
|
|
224
|
-
if (
|
|
280
|
+
if (session.isIdle)
|
|
225
281
|
throw new Error('Nothing is running.');
|
|
226
282
|
await host.runtime().interrupt();
|
|
227
283
|
break;
|
|
@@ -232,11 +288,21 @@ export function createTerminalCommands(host) {
|
|
|
232
288
|
case 'session': {
|
|
233
289
|
let path = argument;
|
|
234
290
|
if (!path) {
|
|
235
|
-
const
|
|
236
|
-
|
|
291
|
+
const selected = await host.ui.custom((tui, _theme, _keys, done) => {
|
|
292
|
+
const page = new ResumeSessionsPage({
|
|
293
|
+
theme: () => host.ui.theme,
|
|
294
|
+
cwd: () => host.cwd(),
|
|
295
|
+
branch: () => host.branch(),
|
|
296
|
+
listSessions: signal => SessionManager.list(host.cwd(), host.sessionDirectory(), undefined, signal),
|
|
297
|
+
select: done,
|
|
298
|
+
cancel: () => done(undefined),
|
|
299
|
+
changed: host.changed
|
|
300
|
+
});
|
|
301
|
+
return new PageOverlay({ page, tui, theme: () => host.ui.theme, close: () => done(undefined), changed: host.changed });
|
|
302
|
+
}, { overlay: true, overlayOptions: pageOverlayOptions('fullscreen') });
|
|
237
303
|
if (!selected)
|
|
238
304
|
break;
|
|
239
|
-
path = selected
|
|
305
|
+
path = selected;
|
|
240
306
|
}
|
|
241
307
|
const target = resolve(host.cwd(), path);
|
|
242
308
|
const within = relative(host.sessionDirectory(), target);
|
|
@@ -324,6 +390,21 @@ export function createTerminalCommands(host) {
|
|
|
324
390
|
case 'settings':
|
|
325
391
|
await openPage('settings');
|
|
326
392
|
break;
|
|
393
|
+
case 'doctor':
|
|
394
|
+
await openPage('doctor');
|
|
395
|
+
break;
|
|
396
|
+
case 'worktree':
|
|
397
|
+
await openPage('worktree');
|
|
398
|
+
break;
|
|
399
|
+
case 'trajectory':
|
|
400
|
+
await openPage('trajectory', { runId: argument });
|
|
401
|
+
break;
|
|
402
|
+
case 'subagents':
|
|
403
|
+
await openPage('subagents');
|
|
404
|
+
break;
|
|
405
|
+
case 'agents':
|
|
406
|
+
await openPage('agents');
|
|
407
|
+
break;
|
|
327
408
|
case 'permissions':
|
|
328
409
|
await openPage('permissions');
|
|
329
410
|
break;
|
|
@@ -395,5 +476,5 @@ export function createTerminalCommands(host) {
|
|
|
395
476
|
}
|
|
396
477
|
return true;
|
|
397
478
|
}
|
|
398
|
-
return { execute, completions, skills, expandSkill };
|
|
479
|
+
return { execute, completions, skills, expandSkill, legacyAliases };
|
|
399
480
|
}
|
|
@@ -2,11 +2,20 @@ import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
|
2
2
|
import { type Component, type Focusable, type TuiMouseEventResult } from '@earendil-works/pi-tui';
|
|
3
3
|
import { type TerminalMouseInput } from "./mouse.js";
|
|
4
4
|
export type RowTone = 'success' | 'warning' | 'error' | 'muted' | 'dim' | 'accent';
|
|
5
|
+
export interface ListLabelPart {
|
|
6
|
+
text: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
tone?: RowTone;
|
|
9
|
+
}
|
|
5
10
|
export interface ListRow<T = unknown> {
|
|
6
11
|
id: string;
|
|
7
12
|
label: string;
|
|
13
|
+
/** Optional colored spans for the label; selected rows keep the shared high-contrast text. */
|
|
14
|
+
labelParts?: readonly ListLabelPart[];
|
|
8
15
|
/** Shown at the right edge, in `tone`. */
|
|
9
16
|
detail?: string;
|
|
17
|
+
/** An optional muted second line beneath the label. */
|
|
18
|
+
subtitle?: string;
|
|
10
19
|
tone?: RowTone;
|
|
11
20
|
/** Rows with the same consecutive group share a heading (not shown while searching). */
|
|
12
21
|
group?: string;
|
|
@@ -22,6 +31,8 @@ export interface FilterListOptions<T> {
|
|
|
22
31
|
search?: 'always' | 'never';
|
|
23
32
|
/** Rows the list may use; the search field is part of it. Defaults to what `render` is given. */
|
|
24
33
|
limit?: () => number;
|
|
34
|
+
/** Blank lines between visible rows. */
|
|
35
|
+
rowGap?: number;
|
|
25
36
|
empty?: string;
|
|
26
37
|
noun?: readonly [string, string];
|
|
27
38
|
/** Short context included in the search row while a query is active. */
|
|
@@ -59,6 +70,7 @@ export declare class FilterList<T = unknown> implements Component, Focusable {
|
|
|
59
70
|
handleMouse(event: TerminalMouseInput): TuiMouseEventResult | undefined;
|
|
60
71
|
invalidate(): void;
|
|
61
72
|
private limit;
|
|
73
|
+
private rowGap;
|
|
62
74
|
private mode;
|
|
63
75
|
private canSearch;
|
|
64
76
|
private searchVisible;
|
|
@@ -67,7 +79,8 @@ export declare class FilterList<T = unknown> implements Component, Focusable {
|
|
|
67
79
|
private move;
|
|
68
80
|
handleInput(data: string): boolean;
|
|
69
81
|
private entries;
|
|
70
|
-
private
|
|
82
|
+
private lines;
|
|
83
|
+
private labelParts;
|
|
71
84
|
/** `rows` is the most the list may use, the search row included. */
|
|
72
85
|
render(width: number, rows?: number): string[];
|
|
73
86
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fuzzyFilter, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
1
|
+
import { fuzzyFilter, matchesKey, parseColor, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import { safeText, selectedRow, selectionMuted, selectionText } from "./chrome.js";
|
|
3
3
|
import { SEARCH_FIELD_ROWS, SearchField, searchFieldRowsFor } from "./search.js";
|
|
4
4
|
import { isPrimaryClick } from "./mouse.js";
|
|
@@ -25,7 +25,10 @@ export class FilterList {
|
|
|
25
25
|
get total() { return this.rows.length; }
|
|
26
26
|
get shown() { return this.visible.length; }
|
|
27
27
|
/** Rows the list would use if nothing cut it: the search field, headings and rows. */
|
|
28
|
-
get height() {
|
|
28
|
+
get height() {
|
|
29
|
+
return (this.searchVisible() ? this.renderedSearchRows : 0)
|
|
30
|
+
+ this.entries().reduce((height, entry) => height + (entry.row?.subtitle === undefined ? 1 : 2), 0);
|
|
31
|
+
}
|
|
29
32
|
/** Rows at the top of `render` that a frame must keep when it has to cut the list. */
|
|
30
33
|
get pinnedRows() { return this.searchVisible() ? this.renderedSearchRows : 0; }
|
|
31
34
|
setRows(rows) {
|
|
@@ -61,6 +64,10 @@ export class FilterList {
|
|
|
61
64
|
}
|
|
62
65
|
invalidate() { this.search.invalidate(); }
|
|
63
66
|
limit() { return Math.max(1, Math.floor(this.options.limit?.() ?? Number.POSITIVE_INFINITY)); }
|
|
67
|
+
rowGap() {
|
|
68
|
+
const gap = this.options.rowGap ?? 0;
|
|
69
|
+
return Number.isFinite(gap) ? Math.max(0, Math.floor(gap)) : 0;
|
|
70
|
+
}
|
|
64
71
|
mode() { return this.options.search ?? 'always'; }
|
|
65
72
|
canSearch() { return this.mode() === 'always'; }
|
|
66
73
|
searchVisible() { return this.canSearch() || this.query.length > 0; }
|
|
@@ -68,7 +75,7 @@ export class FilterList {
|
|
|
68
75
|
refilter() {
|
|
69
76
|
const query = this.query.trim();
|
|
70
77
|
this.visible = query
|
|
71
|
-
? fuzzyFilter(this.rows, query, row => row.searchText ?? `${row.label} ${row.detail ?? ''} ${row.group ?? ''}`)
|
|
78
|
+
? fuzzyFilter(this.rows, query, row => row.searchText ?? `${row.label} ${row.subtitle ?? ''} ${row.detail ?? ''} ${row.group ?? ''}`)
|
|
72
79
|
: this.rows;
|
|
73
80
|
if (!this.visible.some(row => row.id === this.selectedId && !row.disabled))
|
|
74
81
|
this.selectedId = this.firstSelectable()?.id;
|
|
@@ -136,15 +143,20 @@ export class FilterList {
|
|
|
136
143
|
entries() {
|
|
137
144
|
const entries = [];
|
|
138
145
|
let last;
|
|
139
|
-
|
|
146
|
+
const gap = this.rowGap();
|
|
147
|
+
for (const [index, row] of this.visible.entries()) {
|
|
140
148
|
if (!this.query && row.group && row.group !== last)
|
|
141
149
|
entries.push({ heading: row.group });
|
|
142
150
|
last = row.group;
|
|
143
151
|
entries.push({ row });
|
|
152
|
+
if (index < this.visible.length - 1) {
|
|
153
|
+
for (let line = 0; line < gap; line += 1)
|
|
154
|
+
entries.push({ spacer: true });
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
return entries;
|
|
146
158
|
}
|
|
147
|
-
|
|
159
|
+
lines(row, width) {
|
|
148
160
|
const theme = this.options.theme();
|
|
149
161
|
const selected = row.id === this.selectedId;
|
|
150
162
|
const detail = row.detail ? safeText(row.detail) : '';
|
|
@@ -153,10 +165,40 @@ export class FilterList {
|
|
|
153
165
|
const label = truncateToWidth(safeText(row.label), Math.max(0, room - (showDetail ? visibleWidth(detail) + 2 : 0)), '…');
|
|
154
166
|
const gap = ' '.repeat(Math.max(0, room - visibleWidth(label) - (showDetail ? visibleWidth(detail) : 0)));
|
|
155
167
|
const tone = (text) => theme.fg(row.tone ?? 'muted', text);
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
const prefix = selected ? '❯ ' : ' ';
|
|
169
|
+
const labelText = selected || row.disabled || !row.labelParts?.length ? label : this.labelParts(row.labelParts, label, theme);
|
|
170
|
+
const main = prefix + label;
|
|
171
|
+
const primary = selected
|
|
172
|
+
? selectedRow(selectionText(theme, main) + gap + (showDetail ? selectionMuted(theme, detail) : ''), width, theme)
|
|
173
|
+
: (row.disabled ? theme.fg('dim', main) : prefix + labelText) + gap + (showDetail ? tone(detail) : '');
|
|
174
|
+
if (row.subtitle === undefined)
|
|
175
|
+
return [primary];
|
|
176
|
+
const subtitle = truncateToWidth(` ${safeText(row.subtitle)}`, width, '…');
|
|
177
|
+
const secondary = selected
|
|
178
|
+
? selectedRow(selectionMuted(theme, subtitle), width, theme)
|
|
179
|
+
: theme.fg(row.disabled ? 'dim' : 'muted', subtitle);
|
|
180
|
+
return [primary, secondary];
|
|
181
|
+
}
|
|
182
|
+
labelParts(parts, visibleLabel, theme) {
|
|
183
|
+
const source = parts.map(part => safeText(part.text)).join('');
|
|
184
|
+
const truncated = visibleWidth(source) > visibleWidth(visibleLabel);
|
|
185
|
+
let remaining = Math.max(0, visibleWidth(visibleLabel) - (truncated ? 1 : 0));
|
|
186
|
+
const output = [];
|
|
187
|
+
for (const part of parts) {
|
|
188
|
+
if (remaining <= 0)
|
|
189
|
+
break;
|
|
190
|
+
const text = safeText(part.text);
|
|
191
|
+
const fitted = truncateToWidth(text, remaining, '');
|
|
192
|
+
if (fitted) {
|
|
193
|
+
output.push(part.color ? theme.style(fitted, { fg: parseColor(part.color) }) : part.tone ? theme.fg(part.tone, fitted) : fitted);
|
|
194
|
+
}
|
|
195
|
+
remaining -= visibleWidth(fitted);
|
|
196
|
+
if (visibleWidth(fitted) < visibleWidth(text))
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (truncated)
|
|
200
|
+
output.push('…');
|
|
201
|
+
return output.join('');
|
|
160
202
|
}
|
|
161
203
|
/** `rows` is the most the list may use, the search row included. */
|
|
162
204
|
render(width, rows) {
|
|
@@ -186,11 +228,51 @@ export class FilterList {
|
|
|
186
228
|
return lines.map(line => truncateToWidth(line, width, ''));
|
|
187
229
|
}
|
|
188
230
|
const at = Math.max(0, entries.findIndex(entry => entry.row?.id === this.selectedId));
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
231
|
+
let start = at;
|
|
232
|
+
let end = at + 1;
|
|
233
|
+
let used = entries[at]?.row?.subtitle === undefined ? 1 : 2;
|
|
234
|
+
let preferBefore = true;
|
|
235
|
+
while (start > 0 || end < entries.length) {
|
|
236
|
+
const before = entries[start - 1];
|
|
237
|
+
const after = entries[end];
|
|
238
|
+
const beforeHeight = before ? (before.row?.subtitle === undefined ? 1 : 2) : Number.POSITIVE_INFINITY;
|
|
239
|
+
const afterHeight = after ? (after.row?.subtitle === undefined ? 1 : 2) : Number.POSITIVE_INFINITY;
|
|
240
|
+
if (preferBefore && before && used + beforeHeight <= budget) {
|
|
241
|
+
start -= 1;
|
|
242
|
+
used += beforeHeight;
|
|
243
|
+
}
|
|
244
|
+
else if (after && used + afterHeight <= budget) {
|
|
245
|
+
end += 1;
|
|
246
|
+
used += afterHeight;
|
|
247
|
+
}
|
|
248
|
+
else if (before && used + beforeHeight <= budget) {
|
|
249
|
+
start -= 1;
|
|
250
|
+
used += beforeHeight;
|
|
251
|
+
}
|
|
252
|
+
else
|
|
253
|
+
break;
|
|
254
|
+
preferBefore = !preferBefore;
|
|
255
|
+
}
|
|
256
|
+
if (entries[start]?.spacer)
|
|
257
|
+
start += 1;
|
|
258
|
+
if (entries[end - 1]?.spacer)
|
|
259
|
+
end -= 1;
|
|
260
|
+
for (const entry of entries.slice(start, end)) {
|
|
261
|
+
if (entry.row) {
|
|
262
|
+
const rendered = this.lines(entry.row, width);
|
|
263
|
+
lines.push(...rendered);
|
|
264
|
+
this.renderedRows.push(...rendered.map(() => entry.row));
|
|
265
|
+
}
|
|
266
|
+
else if (entry.spacer) {
|
|
267
|
+
lines.push('');
|
|
268
|
+
this.renderedRows.push(undefined);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
lines.push(theme.fg('dim', safeText(entry.heading ?? '')));
|
|
272
|
+
this.renderedRows.push(undefined);
|
|
273
|
+
}
|
|
193
274
|
}
|
|
194
|
-
|
|
275
|
+
this.renderedRows = this.renderedRows.slice(0, rowBudget);
|
|
276
|
+
return lines.slice(0, rowBudget).map(line => truncateToWidth(line, width, ''));
|
|
195
277
|
}
|
|
196
278
|
}
|