@pellux/goodvibes-agent 1.0.17 → 1.0.18
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 +6 -0
- package/README.md +2 -2
- package/dist/package/main.js +313 -86
- package/docs/README.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/tools-and-commands.md +5 -5
- package/package.json +1 -1
- package/src/tools/agent-harness-keybinding-metadata.ts +83 -23
- package/src/tools/agent-harness-panel-metadata.ts +84 -14
- package/src/tools/agent-harness-tool-schema.ts +5 -5
- package/src/tools/agent-harness-tool.ts +6 -6
- package/src/tools/agent-harness-ui-surface-metadata.ts +75 -11
- package/src/version.ts +1 -1
package/docs/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Important baseline constraints:
|
|
|
22
22
|
- Agent depends on `@pellux/goodvibes-sdk@0.33.35`.
|
|
23
23
|
- Agent connects to a GoodVibes host owned outside this product.
|
|
24
24
|
- Agent does not start, stop, restart, install, uninstall, or own the connected GoodVibes host.
|
|
25
|
-
- User-facing Agent workspace actions, single workspace-action lookup, built-in panels, modal/overlay/picker UI surfaces, named operator surfaces, top-level CLI mirrors, fixed shortcuts, configurable keybindings, slash commands, single-command lookup with policy metadata, model tools and individual model tool schemas, settings and single-setting lookup, local registries, connected-host capability boundaries, single connected-host capability inspection, and live connected-host readiness are model-visible through Agent-owned tools. Visible UI routing includes the command browser, reasoning-effort picker, live process output, runtime activity, settings, workspaces, and pickers the user can open. Local memory, note, persona, skill, and routine create editors can also run through confirmed workspace actions. Mutations and visible UI routing remain explicit and confirmation-gated, and ambiguous
|
|
25
|
+
- User-facing Agent workspace actions, single workspace-action lookup, built-in panels and single-panel lookup, modal/overlay/picker UI surfaces and single-surface lookup, named operator surfaces, top-level CLI mirrors, fixed shortcuts, configurable keybindings and single-keybinding lookup, slash commands, single-command lookup with policy metadata, model tools and individual model tool schemas, settings and single-setting lookup, local registries, connected-host capability boundaries, single connected-host capability inspection, and live connected-host readiness are model-visible through Agent-owned tools. Visible UI routing includes the command browser, reasoning-effort picker, live process output, runtime activity, settings, workspaces, and pickers the user can open. Local memory, note, persona, skill, and routine create editors can also run through confirmed workspace actions. Mutations and visible UI routing remain explicit and confirmation-gated, and ambiguous lookup matches are refused with candidates instead of guessed.
|
|
26
26
|
- Agent Knowledge uses only `/api/goodvibes-agent/knowledge/*`; there is no default knowledge or non-Agent product fallback.
|
|
27
27
|
- Agent supports isolated Agent homes with `GOODVIBES_AGENT_HOME=<path>` and named profile homes with `goodvibes-agent profiles create <name> --template <starter> --yes` plus `--agent-profile <name>`.
|
|
28
28
|
- Agent supports connected-host URL overrides with `--runtime-url http://host:port` or `GOODVIBES_AGENT_RUNTIME_URL=http://host:port`; these only change the Agent connection target.
|
package/docs/getting-started.md
CHANGED
|
@@ -44,7 +44,7 @@ After setup has been applied once, the TUI opens directly into the Agent operato
|
|
|
44
44
|
|
|
45
45
|
Press `/` inside the Agent workspace to search every workspace action by name, category, command, or detail. Use that finder before reaching for shell commands; CLI subcommands are scriptable mirrors of these TUI workflows.
|
|
46
46
|
|
|
47
|
-
The model can inspect and use the same harness surface through Agent-owned tools. `agent_harness` exposes workspace action discovery, single workspace-action inspection by action id, command, or lookup text, built-in panel discovery/routing, modal/overlay/picker UI surface discovery/routing, top-level CLI mirror discovery, fixed shortcut and configurable keybinding discovery, keybinding mutation
|
|
47
|
+
The model can inspect and use the same harness surface through Agent-owned tools. `agent_harness` exposes workspace action discovery, single workspace-action inspection by action id, command, or lookup text, built-in panel discovery/routing by panel id or lookup text, modal/overlay/picker UI surface discovery/routing by surface id or lookup text, top-level CLI mirror discovery, fixed shortcut and configurable keybinding discovery, keybinding inspection/mutation by action id or lookup text, slash-command discovery, single slash-command inspection by typed command or lookup text, command policy metadata, command execution with confirmation, model tool discovery and single-tool schema inspection, settings inspection/mutation by key or lookup text, connected-host capability inventory, single connected-host capability inspection, and live connected-host readiness posture. CLI mirror modes are read-only catalog/parser inspection and point the model to the matching in-process tool, workspace action, setting mode, or slash-command mirror. Panel modes expose catalog/open state and route visible panel/workspace changes through the current Agent shell bridge with confirmation. UI surface modes expose help, shortcuts, command browser, conversation search, prompt-history search, slash-command mode, file picker, block actions, context, runtime activity, live process output, bookmarks, model/provider/reasoning-effort pickers, TTS provider/voice pickers, session/profile pickers, the panel-picker compatibility route, security/knowledge/subscription operator surfaces, settings, MCP workspace, onboarding, and Agent workspace entrypoints; `open_ui_surface` is confirmation-gated and only performs visible shell navigation. Shortcut modes expose the fixed runtime/editor shortcuts and the live keybindings table; confirmed keybinding edits write the same `keybindings.json` file the user edits and reload the runtime manager. Workspace action discovery can include editor field schemas with `includeParameters:true`, including starter-template defaults for profile creation and selected-routine defaults for routine schedule promotion when `recordId` is supplied; single-action lookup uses the same user-facing search fields and returns lookup metadata plus the editor schema; single-panel, single-surface, single-keybinding, and single-setting lookup use their catalog search fields, return lookup metadata, and refuse ambiguous route or mutation requests with candidates. Selection-based local workspace actions accept a local `recordId`, so the model can use the same note promotion and local registry flows as the TUI. Direct local create editors for memory, notes, personas, skills, and routines can execute from submitted fields through `run_workspace_action` and `agent_local_registry` with confirmation. First-class model tools cover the main product workflows directly: Agent Knowledge, Agent Knowledge ingest, Agent-local memory/notes/personas/skills/routines, operator actions, notifications, channel sends, reminders, generated media, and work plans.
|
|
48
48
|
|
|
49
49
|
Use the Artifacts area for concrete files and generated output: attach images to prompts, export conversations or saved sessions, ingest local source files and URL lists into Agent Knowledge, import bookmarks or browser history, inspect source records, and generate media artifacts from confirmed prompts.
|
|
50
50
|
|
|
@@ -122,7 +122,7 @@ Use `/channels` inside the TUI for channel readiness and exact confirmed sends.
|
|
|
122
122
|
|
|
123
123
|
The main assistant conversation can perform narrow confirmed operator actions when the user explicitly asks for a specific target: approve/deny/cancel one approval, run/pause/resume one automation job, cancel/retry one automation run, or run one schedule. Those calls use only public connected-host routes, require confirmation, and do not create, edit, or delete automation definitions.
|
|
124
124
|
|
|
125
|
-
When the main assistant conversation needs to change Agent settings, change Agent keybindings, open a visible UI surface, or run a user-facing harness action, it should use `agent_harness` rather than generic SDK context/settings tools.
|
|
125
|
+
When the main assistant conversation needs to change Agent settings, change Agent keybindings, open a visible UI surface, or run a user-facing harness action, it should use `agent_harness` rather than generic SDK context/settings tools. Panel, UI surface, keybinding, and setting descriptors report lookup metadata when resolved from `target` or `query`; ambiguous route or mutation requests return candidates instead of guessing. Setting descriptors also report writability, workspace visibility, and lock reasons. Keybinding descriptors report fixed shortcuts, resolved bindings, defaults, custom state, and config path; UI surface descriptors report the visible shell opener, preferred model route, and confirmation policy; secret-backed settings store raw values through the secret manager; connected-host lifecycle settings stay read-only in Agent; and destructive local record deletion requires explicit confirmation.
|
|
126
126
|
|
|
127
127
|
## Connected GoodVibes Host
|
|
128
128
|
|
|
@@ -43,20 +43,20 @@ Host-management and coding-first commands that would imply connected-host lifecy
|
|
|
43
43
|
|
|
44
44
|
The main Agent model has an Agent-owned harness bridge rather than generic SDK settings/context control. Use these model tools as the supported parity layer:
|
|
45
45
|
|
|
46
|
-
- `agent_harness`: inspect workspace categories/actions and one workspace action by action id, command, or lookup text, inspect built-in panels, inspect modal/overlay/picker UI surfaces and named operator surfaces, inspect top-level CLI mirrors, inspect fixed shortcuts and configurable keybindings, inspect slash commands and one slash command by typed command or lookup text with policy metadata, run concrete workspace or slash-command mirrors with confirmation, inspect model tools or one model tool schema, inspect or change Agent settings
|
|
46
|
+
- `agent_harness`: inspect workspace categories/actions and one workspace action by action id, command, or lookup text, inspect built-in panels and one panel by id or lookup text, inspect modal/overlay/picker UI surfaces and named operator surfaces by id or lookup text, inspect top-level CLI mirrors, inspect fixed shortcuts and configurable keybindings, inspect or change one keybinding by action id or lookup text, inspect slash commands and one slash command by typed command or lookup text with policy metadata, run concrete workspace or slash-command mirrors with confirmation, inspect model tools or one model tool schema, inspect or change Agent settings by exact key or lookup text, inspect connected-host capability boundaries or one connected-host capability, and inspect live connected-host readiness.
|
|
47
47
|
- `agent_local_registry`: inspect and maintain Agent-local memory, notes, personas, skills, skill bundles, and routines. Deleting local records requires `confirm:true` and `explicitUserRequest`.
|
|
48
48
|
- `agent_knowledge` and `agent_knowledge_ingest`: ask/search and ingest into the isolated Agent Knowledge segment.
|
|
49
49
|
- `agent_operator_briefing` and `agent_operator_action`: inspect connected work/approval/automation posture, or run exact confirmed approval/automation actions.
|
|
50
50
|
- `agent_work_plan`: keep the visible Agent-local work plan current from the conversation.
|
|
51
51
|
- `agent_channel_send`, `agent_notify`, `agent_reminder_schedule`, and `agent_media_generate`: perform confirmed external delivery, notification, reminder, or media actions when the user explicitly asks.
|
|
52
52
|
|
|
53
|
-
`agent_harness` discovery modes are read-only. `summary` reports the model access map; `panels`
|
|
53
|
+
`agent_harness` discovery modes are read-only. `summary` reports the model access map; `panels` lists the built-in panel catalog, and `panel` resolves one panel by `panelId`, `target`, or `query` with current open/focused state plus its matching Agent workspace route; `ui_surfaces` lists modal, overlay, picker, and workspace entrypoints, and `ui_surface` resolves one by `surfaceId`, `target`, or `query` with shell-opener availability and preferred model routes; `cli_commands` and `cli_command` return top-level package CLI mirror metadata, parser output, blocked command tokens, and preferred in-process model routes; `shortcuts` returns fixed runtime/editor shortcuts plus configurable keybindings; `keybindings` lists the live resolved keybinding table, and `keybinding`, `set_keybinding`, and `reset_keybinding` resolve one action by `actionId`, `target`, `key`, or `query` with default bindings, custom state, config path, and lookup metadata; `commands` lists slash-command descriptions; `command` returns one slash-command detail by `command`, `commandName`, `target`, or `query`, including parsed arguments plus effect/confirmation/preferred-tool/boundary policy metadata; `workspace_actions` lists Agent workspace actions and can inline editor field schemas with `includeParameters:true`; `workspace_action` returns one action by `actionId`, `command`, `target`, or `query`, using the same user-facing action-search fields and returning lookup metadata plus its editor schema; `tools` lists model tool definitions and can inline JSON schemas with `includeParameters:true`; `tool` returns one model tool schema by `toolName`, `target`, or `query`; `settings` returns setting descriptors plus setting policy; `get_setting`, `set_setting`, and `reset_setting` resolve one setting by `key`, `target`, or `query`, return lookup metadata on success, and refuse ambiguous matches with candidate settings; `connected_host` returns the connected-host route families, allowed capabilities, blocked capabilities, and first-class tool availability; `connected_host_capability` returns one allowed or blocked connected-host capability by `capabilityId`, `target`, or `query` with related route families and boundary text; and `connected_host_status` performs a live read-only check of the connected-host status and Agent Knowledge status routes and reports endpoint bindings, token posture, SDK compatibility, route readiness, and findings without printing token values.
|
|
54
54
|
|
|
55
|
-
`open_ui_surface` is a confirmation-gated visible navigation mode for the same shell surfaces the user can open: Agent workspace, settings, MCP workspace, model/provider/reasoning-effort pickers, TTS provider/voice pickers, session/profile pickers, the panel-picker compatibility route, security/knowledge/subscription operator surfaces, conversation search, prompt-history search, slash-command mode, command browser, file picker, block actions, bookmarks, context inspector, runtime activity monitor, live process output, help, shortcuts, and onboarding. It does not perform hidden operations; use first-class model tools, settings modes, workspace actions, or confirmed slash-command mirrors for actual state changes.
|
|
55
|
+
`open_ui_surface` is a confirmation-gated visible navigation mode for the same shell surfaces the user can open: Agent workspace, settings, MCP workspace, model/provider/reasoning-effort pickers, TTS provider/voice pickers, session/profile pickers, the panel-picker compatibility route, security/knowledge/subscription operator surfaces, conversation search, prompt-history search, slash-command mode, command browser, file picker, block actions, bookmarks, context inspector, runtime activity monitor, live process output, help, shortcuts, and onboarding. It does not perform hidden operations; use first-class model tools, settings modes, workspace actions, or confirmed slash-command mirrors for actual state changes. Ambiguous UI surface lookup text is refused with candidate surfaces instead of routed.
|
|
56
56
|
|
|
57
|
-
`set_keybinding` and `reset_keybinding` are confirmation-gated control modes. They write the same Agent `keybindings.json` file exposed by `/keybindings`, reload the runtime keybinding manager, and leave fixed runtime/editor shortcuts read-only.
|
|
57
|
+
`set_keybinding` and `reset_keybinding` are confirmation-gated control modes. They write the same Agent `keybindings.json` file exposed by `/keybindings`, reload the runtime keybinding manager, and leave fixed runtime/editor shortcuts read-only. Ambiguous keybinding lookup text is refused with candidate actions instead of guessed.
|
|
58
58
|
|
|
59
|
-
`open_panel` is a confirmation-gated UI routing mode. It hands a visible panel/workspace route to the current Agent shell bridge and does not mutate connected-host lifecycle, listener posture, or external accounts.
|
|
59
|
+
`open_panel` is a confirmation-gated UI routing mode. It hands a visible panel/workspace route to the current Agent shell bridge and does not mutate connected-host lifecycle, listener posture, or external accounts. Ambiguous panel lookup text is refused with candidate panels instead of routed.
|
|
60
60
|
|
|
61
61
|
CLI mirror modes are catalog and parser inspection only. When the user asks the model to operate from inside the main conversation, use the returned first-class model tool, workspace action, setting mode, or confirmed slash-command mirror instead of launching a hidden nested `goodvibes-agent` process.
|
|
62
62
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,7 @@ const FIXED_SHORTCUTS: readonly Record<string, string>[] = [
|
|
|
23
23
|
interface HarnessKeybindingArgs {
|
|
24
24
|
readonly query?: unknown;
|
|
25
25
|
readonly actionId?: unknown;
|
|
26
|
+
readonly target?: unknown;
|
|
26
27
|
readonly key?: unknown;
|
|
27
28
|
readonly fields?: unknown;
|
|
28
29
|
readonly combo?: unknown;
|
|
@@ -33,6 +34,24 @@ interface HarnessKeybindingArgs {
|
|
|
33
34
|
|
|
34
35
|
type KeybindingsOverrideFile = Record<string, unknown>;
|
|
35
36
|
|
|
37
|
+
interface KeybindingLookup {
|
|
38
|
+
readonly source: 'actionId' | 'target' | 'key' | 'query';
|
|
39
|
+
readonly input: string;
|
|
40
|
+
readonly resolvedBy: 'action' | 'case-insensitive-action' | 'search';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type KeybindingResolution =
|
|
44
|
+
| {
|
|
45
|
+
readonly status: 'found';
|
|
46
|
+
readonly action: KeyAction;
|
|
47
|
+
readonly lookup: KeybindingLookup;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
readonly status: 'ambiguous';
|
|
51
|
+
readonly input: string;
|
|
52
|
+
readonly candidates: readonly Record<string, unknown>[];
|
|
53
|
+
};
|
|
54
|
+
|
|
36
55
|
function readString(value: unknown): string {
|
|
37
56
|
return typeof value === 'string' ? value.trim() : '';
|
|
38
57
|
}
|
|
@@ -58,11 +77,6 @@ function isKeyAction(action: string): action is KeyAction {
|
|
|
58
77
|
return Object.hasOwn(ACTION_DESCRIPTIONS, action);
|
|
59
78
|
}
|
|
60
79
|
|
|
61
|
-
function actionFromArgs(args: HarnessKeybindingArgs): KeyAction | null {
|
|
62
|
-
const action = readString(args.actionId || args.key || args.query);
|
|
63
|
-
return action && isKeyAction(action) ? action : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
80
|
function readBoolean(value: unknown): boolean | undefined {
|
|
67
81
|
if (typeof value === 'boolean') return value;
|
|
68
82
|
if (typeof value !== 'string') return undefined;
|
|
@@ -138,22 +152,63 @@ function describeCombo(manager: KeybindingsManager, combo: KeyCombo): Record<str
|
|
|
138
152
|
};
|
|
139
153
|
}
|
|
140
154
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
function keybindingLookupFromArgs(args: HarnessKeybindingArgs): { readonly source: KeybindingLookup['source']; readonly input: string } | null {
|
|
156
|
+
const actionId = readString(args.actionId);
|
|
157
|
+
if (actionId) return { source: 'actionId', input: actionId };
|
|
158
|
+
const target = readString(args.target);
|
|
159
|
+
if (target) return { source: 'target', input: target };
|
|
160
|
+
const key = readString(args.key);
|
|
161
|
+
if (key) return { source: 'key', input: key };
|
|
162
|
+
const query = readString(args.query);
|
|
163
|
+
if (query) return { source: 'query', input: query };
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function bindingSearchText(manager: KeybindingsManager, entry: { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] }): string {
|
|
168
|
+
return [
|
|
144
169
|
entry.action,
|
|
145
170
|
entry.description,
|
|
146
171
|
...entry.combos.map((combo) => comboFingerprint(combo)),
|
|
172
|
+
...entry.combos.map((combo) => manager.formatCombo(combo)),
|
|
147
173
|
].join('\n').toLowerCase();
|
|
148
|
-
return haystack.includes(query.toLowerCase());
|
|
149
174
|
}
|
|
150
175
|
|
|
151
|
-
function
|
|
176
|
+
function bindingCandidate(manager: KeybindingsManager, entry: { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] }): Record<string, unknown> {
|
|
177
|
+
return {
|
|
178
|
+
action: entry.action,
|
|
179
|
+
description: entry.description,
|
|
180
|
+
labels: entry.combos.map((combo) => manager.formatCombo(combo)),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function bindingMatches(manager: KeybindingsManager, entry: { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] }, query: string): boolean {
|
|
185
|
+
if (!query) return true;
|
|
186
|
+
return bindingSearchText(manager, entry).includes(query.toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): KeybindingResolution | null {
|
|
190
|
+
const manager = requireKeybindingsManager(context);
|
|
191
|
+
const lookup = keybindingLookupFromArgs(args);
|
|
192
|
+
if (!lookup) return null;
|
|
193
|
+
const entries = manager.getAll();
|
|
194
|
+
if (isKeyAction(lookup.input)) return { status: 'found', action: lookup.input, lookup: { ...lookup, resolvedBy: 'action' } };
|
|
195
|
+
const inputLower = lookup.input.toLowerCase();
|
|
196
|
+
const ciActions = entries.filter((entry) => entry.action.toLowerCase() === inputLower);
|
|
197
|
+
if (ciActions.length === 1) return { status: 'found', action: ciActions[0]!.action, lookup: { ...lookup, resolvedBy: 'case-insensitive-action' } };
|
|
198
|
+
if (ciActions.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: ciActions.map((entry) => bindingCandidate(manager, entry)).slice(0, 8) };
|
|
199
|
+
const matches = entries.filter((entry) => bindingMatches(manager, entry, lookup.input));
|
|
200
|
+
if (matches.length === 1) return { status: 'found', action: matches[0]!.action, lookup: { ...lookup, resolvedBy: 'search' } };
|
|
201
|
+
if (matches.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: matches.map((entry) => bindingCandidate(manager, entry)).slice(0, 8) };
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function describeBinding(manager: KeybindingsManager, action: KeyAction, combos: KeyCombo[], lookup?: KeybindingLookup): Record<string, unknown> {
|
|
152
206
|
const defaults = DEFAULT_KEYBINDINGS[action];
|
|
153
207
|
const customized = !combosEqual(combos, defaults);
|
|
154
208
|
return {
|
|
155
209
|
action,
|
|
156
210
|
description: ACTION_DESCRIPTIONS[action],
|
|
211
|
+
...(lookup ? { lookup } : {}),
|
|
157
212
|
bindings: combos.map((combo) => describeCombo(manager, combo)),
|
|
158
213
|
labels: combos.map((combo) => manager.formatCombo(combo)),
|
|
159
214
|
defaultBindings: defaults.map((combo) => describeCombo(manager, combo)),
|
|
@@ -205,7 +260,7 @@ export function listHarnessKeybindings(context: CommandContext, args: HarnessKey
|
|
|
205
260
|
const manager = requireKeybindingsManager(context);
|
|
206
261
|
const query = readString(args.query);
|
|
207
262
|
const entries = manager.getAll()
|
|
208
|
-
.filter((entry) => bindingMatches(entry, query))
|
|
263
|
+
.filter((entry) => bindingMatches(manager, entry, query))
|
|
209
264
|
.slice(0, readLimit(args.limit, 200))
|
|
210
265
|
.map((entry) => describeBinding(manager, entry.action, entry.combos));
|
|
211
266
|
return {
|
|
@@ -219,44 +274,49 @@ export function listHarnessKeybindings(context: CommandContext, args: HarnessKey
|
|
|
219
274
|
|
|
220
275
|
export function describeHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> | null {
|
|
221
276
|
const manager = requireKeybindingsManager(context);
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
277
|
+
const resolved = resolveHarnessKeybinding(context, args);
|
|
278
|
+
if (resolved?.status === 'ambiguous') return { status: 'ambiguous', input: resolved.input, candidates: resolved.candidates };
|
|
279
|
+
if (!resolved) return null;
|
|
280
|
+
const entry = manager.getAll().find((candidate) => candidate.action === resolved.action);
|
|
225
281
|
return entry ? {
|
|
226
282
|
configPath: manager.getConfigPath(),
|
|
227
|
-
...describeBinding(manager, entry.action, entry.combos),
|
|
283
|
+
...describeBinding(manager, entry.action, entry.combos, resolved.lookup),
|
|
228
284
|
} : null;
|
|
229
285
|
}
|
|
230
286
|
|
|
231
287
|
export function setHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> {
|
|
232
288
|
const manager = requireKeybindingsManager(context);
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
289
|
+
const resolved = resolveHarnessKeybinding(context, args);
|
|
290
|
+
if (resolved?.status === 'ambiguous') throw new Error(`Ambiguous keybinding action ${resolved.input}. Candidates: ${JSON.stringify(resolved.candidates)}`);
|
|
291
|
+
if (!resolved) throw new Error('set_keybinding requires a valid keybinding action id, target, or query.');
|
|
235
292
|
const combos = combosFromArgs(args);
|
|
236
293
|
const configPath = manager.getConfigPath();
|
|
237
294
|
const overrides = readOverrideFile(configPath);
|
|
238
|
-
overrides[action] = combos.length === 1 ? combos[0] : combos;
|
|
295
|
+
overrides[resolved.action] = combos.length === 1 ? combos[0] : combos;
|
|
239
296
|
writeOverrideFile(configPath, overrides);
|
|
240
297
|
manager.loadFromDisk();
|
|
241
298
|
return {
|
|
242
299
|
status: 'updated',
|
|
243
300
|
configPath,
|
|
244
|
-
keybinding: describeHarnessKeybinding(context, { actionId: action }),
|
|
301
|
+
keybinding: describeHarnessKeybinding(context, { actionId: resolved.action }),
|
|
302
|
+
lookup: resolved.lookup,
|
|
245
303
|
};
|
|
246
304
|
}
|
|
247
305
|
|
|
248
306
|
export function resetHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> {
|
|
249
307
|
const manager = requireKeybindingsManager(context);
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
308
|
+
const resolved = resolveHarnessKeybinding(context, args);
|
|
309
|
+
if (resolved?.status === 'ambiguous') throw new Error(`Ambiguous keybinding action ${resolved.input}. Candidates: ${JSON.stringify(resolved.candidates)}`);
|
|
310
|
+
if (!resolved) throw new Error('reset_keybinding requires a valid keybinding action id, target, or query.');
|
|
252
311
|
const configPath = manager.getConfigPath();
|
|
253
312
|
const overrides = readOverrideFile(configPath);
|
|
254
|
-
delete overrides[action];
|
|
313
|
+
delete overrides[resolved.action];
|
|
255
314
|
writeOverrideFile(configPath, overrides);
|
|
256
315
|
manager.loadFromDisk();
|
|
257
316
|
return {
|
|
258
317
|
status: 'reset',
|
|
259
318
|
configPath,
|
|
260
|
-
keybinding: describeHarnessKeybinding(context, { actionId: action }),
|
|
319
|
+
keybinding: describeHarnessKeybinding(context, { actionId: resolved.action }),
|
|
320
|
+
lookup: resolved.lookup,
|
|
261
321
|
};
|
|
262
322
|
}
|
|
@@ -5,11 +5,30 @@ import type { PanelRegistration } from '../panels/types.ts';
|
|
|
5
5
|
export interface AgentHarnessPanelArgs {
|
|
6
6
|
readonly query?: unknown;
|
|
7
7
|
readonly panelId?: unknown;
|
|
8
|
+
readonly target?: unknown;
|
|
8
9
|
readonly category?: unknown;
|
|
9
10
|
readonly limit?: unknown;
|
|
10
11
|
readonly pane?: unknown;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
interface PanelLookup {
|
|
15
|
+
readonly source: 'panelId' | 'target' | 'query';
|
|
16
|
+
readonly input: string;
|
|
17
|
+
readonly resolvedBy: 'id' | 'case-insensitive-id' | 'name' | 'case-insensitive-name' | 'search';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type PanelResolution =
|
|
21
|
+
| {
|
|
22
|
+
readonly status: 'found';
|
|
23
|
+
readonly registration: PanelRegistration;
|
|
24
|
+
readonly lookup: PanelLookup;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
readonly status: 'ambiguous';
|
|
28
|
+
readonly input: string;
|
|
29
|
+
readonly candidates: readonly Record<string, unknown>[];
|
|
30
|
+
};
|
|
31
|
+
|
|
13
32
|
function readString(value: unknown): string {
|
|
14
33
|
return typeof value === 'string' ? value.trim() : '';
|
|
15
34
|
}
|
|
@@ -32,10 +51,59 @@ function panelMatches(panel: Record<string, unknown>, query: string): boolean {
|
|
|
32
51
|
panel.category,
|
|
33
52
|
panel.description,
|
|
34
53
|
panel.workspaceRoute,
|
|
35
|
-
].map((value) => String(value ?? '')).join('\n').toLowerCase().includes(query.toLowerCase());
|
|
54
|
+
].map((value) => typeof value === 'object' ? JSON.stringify(value) : String(value ?? '')).join('\n').toLowerCase().includes(query.toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function panelLookupFromArgs(args: AgentHarnessPanelArgs): { readonly source: PanelLookup['source']; readonly input: string } | null {
|
|
58
|
+
const panelId = readString(args.panelId);
|
|
59
|
+
if (panelId) return { source: 'panelId', input: panelId };
|
|
60
|
+
const target = readString(args.target);
|
|
61
|
+
if (target) return { source: 'target', input: target };
|
|
62
|
+
const query = readString(args.query);
|
|
63
|
+
if (query) return { source: 'query', input: query };
|
|
64
|
+
return null;
|
|
36
65
|
}
|
|
37
66
|
|
|
38
|
-
function
|
|
67
|
+
function panelCandidate(registration: PanelRegistration): Record<string, unknown> {
|
|
68
|
+
return {
|
|
69
|
+
id: registration.id,
|
|
70
|
+
name: registration.name,
|
|
71
|
+
category: registration.category,
|
|
72
|
+
description: registration.description,
|
|
73
|
+
workspaceRoute: {
|
|
74
|
+
categoryId: agentWorkspaceCategoryForPanel(registration.id),
|
|
75
|
+
command: agentWorkspaceCommandForPanel(registration.id),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveHarnessPanel(context: CommandContext, args: AgentHarnessPanelArgs): PanelResolution | null {
|
|
81
|
+
const manager = panelManager(context);
|
|
82
|
+
const lookup = panelLookupFromArgs(args);
|
|
83
|
+
if (!manager || !lookup) return null;
|
|
84
|
+
const registrations = manager.getRegisteredTypes();
|
|
85
|
+
const exactId = registrations.find((panel) => panel.id === lookup.input);
|
|
86
|
+
if (exactId) return { status: 'found', registration: exactId, lookup: { ...lookup, resolvedBy: 'id' } };
|
|
87
|
+
const inputLower = lookup.input.toLowerCase();
|
|
88
|
+
const exactName = registrations.find((panel) => panel.name === lookup.input);
|
|
89
|
+
if (exactName) return { status: 'found', registration: exactName, lookup: { ...lookup, resolvedBy: 'name' } };
|
|
90
|
+
const ciId = registrations.filter((panel) => panel.id.toLowerCase() === inputLower);
|
|
91
|
+
if (ciId.length === 1) return { status: 'found', registration: ciId[0]!, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } };
|
|
92
|
+
if (ciId.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: ciId.map(panelCandidate).slice(0, 8) };
|
|
93
|
+
const ciName = registrations.filter((panel) => panel.name.toLowerCase() === inputLower);
|
|
94
|
+
if (ciName.length === 1) return { status: 'found', registration: ciName[0]!, lookup: { ...lookup, resolvedBy: 'case-insensitive-name' } };
|
|
95
|
+
if (ciName.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: ciName.map(panelCandidate).slice(0, 8) };
|
|
96
|
+
const category = readString(args.category);
|
|
97
|
+
const search = registrations.filter((registration) => {
|
|
98
|
+
if (category && registration.category !== category) return false;
|
|
99
|
+
return panelMatches(panelCandidate(registration), lookup.input);
|
|
100
|
+
});
|
|
101
|
+
if (search.length === 1) return { status: 'found', registration: search[0]!, lookup: { ...lookup, resolvedBy: 'search' } };
|
|
102
|
+
if (search.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: search.map(panelCandidate).slice(0, 8) };
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function describePanelRegistration(context: CommandContext, registration: PanelRegistration, lookup?: PanelLookup): Record<string, unknown> {
|
|
39
107
|
const manager = panelManager(context);
|
|
40
108
|
const openPanel = manager?.getPanel(registration.id) ?? null;
|
|
41
109
|
const pane = manager?.getPaneOf(registration.id) ?? null;
|
|
@@ -46,6 +114,7 @@ function describePanelRegistration(context: CommandContext, registration: PanelR
|
|
|
46
114
|
icon: registration.icon,
|
|
47
115
|
category: registration.category,
|
|
48
116
|
description: registration.description,
|
|
117
|
+
...(lookup ? { lookup } : {}),
|
|
49
118
|
preload: registration.preload === true,
|
|
50
119
|
open: openPanel !== null,
|
|
51
120
|
pane,
|
|
@@ -82,26 +151,27 @@ export function listHarnessPanels(context: CommandContext, args: AgentHarnessPan
|
|
|
82
151
|
}
|
|
83
152
|
|
|
84
153
|
export function describeHarnessPanel(context: CommandContext, args: AgentHarnessPanelArgs): Record<string, unknown> | null {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|| panel.name.toLowerCase() === panelId.toLowerCase()
|
|
92
|
-
));
|
|
93
|
-
return registration ? describePanelRegistration(context, registration) : null;
|
|
154
|
+
const resolved = resolveHarnessPanel(context, args);
|
|
155
|
+
if (resolved?.status === 'found') return describePanelRegistration(context, resolved.registration, resolved.lookup);
|
|
156
|
+
if (resolved?.status === 'ambiguous') {
|
|
157
|
+
return { status: 'ambiguous', input: resolved.input, candidates: resolved.candidates };
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
94
160
|
}
|
|
95
161
|
|
|
96
162
|
export function openHarnessPanel(context: CommandContext, args: AgentHarnessPanelArgs): Record<string, unknown> {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
163
|
+
const resolved = resolveHarnessPanel(context, args);
|
|
164
|
+
if (resolved?.status === 'ambiguous') {
|
|
165
|
+
return { status: 'ambiguous_panel', input: resolved.input, candidates: resolved.candidates };
|
|
166
|
+
}
|
|
167
|
+
if (!resolved) {
|
|
99
168
|
return {
|
|
100
169
|
status: 'unknown_panel',
|
|
101
|
-
panelId: readString(args.panelId || args.query) || '<missing>',
|
|
170
|
+
panelId: readString(args.panelId || args.target || args.query) || '<missing>',
|
|
102
171
|
availablePanels: listHarnessPanels(context, { limit: 50 }).map((entry) => entry.id),
|
|
103
172
|
};
|
|
104
173
|
}
|
|
174
|
+
const panel = describePanelRegistration(context, resolved.registration, resolved.lookup);
|
|
105
175
|
const requestedPane = readString(args.pane);
|
|
106
176
|
const pane = requestedPane === 'bottom' || requestedPane === 'top' ? requestedPane : undefined;
|
|
107
177
|
if (!context.showPanel) {
|
|
@@ -23,7 +23,7 @@ export const AGENT_HARNESS_PARAMETER_PROPERTIES = {
|
|
|
23
23
|
},
|
|
24
24
|
query: {
|
|
25
25
|
type: 'string',
|
|
26
|
-
description: 'Search text for command, workspace action, setting,
|
|
26
|
+
description: 'Search text for command, panel, UI surface, keybinding, workspace action, setting, or tool catalogs; also slash-command, setting, panel, UI surface, or keybinding lookup for single-item modes.',
|
|
27
27
|
},
|
|
28
28
|
command: {
|
|
29
29
|
type: 'string',
|
|
@@ -48,15 +48,15 @@ export const AGENT_HARNESS_PARAMETER_PROPERTIES = {
|
|
|
48
48
|
},
|
|
49
49
|
surfaceId: {
|
|
50
50
|
type: 'string',
|
|
51
|
-
description: 'UI surface id for ui_surface or open_ui_surface modes.',
|
|
51
|
+
description: 'UI surface id for ui_surface or open_ui_surface modes. target or query can also look up one UI surface when the exact id is not known.',
|
|
52
52
|
},
|
|
53
53
|
panelId: {
|
|
54
54
|
type: 'string',
|
|
55
|
-
description: 'Built-in panel id for panel or open_panel modes.',
|
|
55
|
+
description: 'Built-in panel id for panel or open_panel modes. target or query can also look up one panel when the exact id is not known.',
|
|
56
56
|
},
|
|
57
57
|
actionId: {
|
|
58
58
|
type: 'string',
|
|
59
|
-
description: 'Agent workspace action id for workspace_action or run_workspace_action, or keybinding action id for keybinding/set_keybinding/reset_keybinding.',
|
|
59
|
+
description: 'Agent workspace action id for workspace_action or run_workspace_action, or keybinding action id for keybinding/set_keybinding/reset_keybinding. target or query can also look up one keybinding action.',
|
|
60
60
|
},
|
|
61
61
|
fields: {
|
|
62
62
|
type: 'object',
|
|
@@ -90,7 +90,7 @@ export const AGENT_HARNESS_PARAMETER_PROPERTIES = {
|
|
|
90
90
|
},
|
|
91
91
|
target: {
|
|
92
92
|
type: 'string',
|
|
93
|
-
description: 'Optional lookup target, such as a model-picker target, workspace action id/search text, slash command root or invocation, setting key/search text, model tool name, or connected-host capability id.',
|
|
93
|
+
description: 'Optional lookup target, such as a model-picker target, panel id/search text, UI surface id/search text, workspace action id/search text, slash command root or invocation, setting key/search text, keybinding action/search text, model tool name, or connected-host capability id.',
|
|
94
94
|
},
|
|
95
95
|
capabilityId: {
|
|
96
96
|
type: 'string',
|
|
@@ -595,9 +595,9 @@ export function createAgentHarnessTool(deps: AgentHarnessToolDeps): Tool {
|
|
|
595
595
|
tools: deps.toolRegistry.getToolDefinitions().length,
|
|
596
596
|
modelAccess: {
|
|
597
597
|
cliCommands: 'Use mode:"cli_commands" and mode:"cli_command" to inspect package CLI mirrors and their preferred in-process model routes. CLI modes are discovery-only.',
|
|
598
|
-
panels: 'Use mode:"panels" and mode:"panel" to inspect built-in panel catalog/open state; use mode:"open_panel" with confirm:true plus explicitUserRequest to route a visible panel/workspace change.',
|
|
599
|
-
uiSurfaces: 'Use mode:"ui_surfaces" and mode:"ui_surface" to inspect modal/overlay/picker/workspace surfaces; use mode:"open_ui_surface" with confirm:true plus explicitUserRequest to route visible UI navigation.',
|
|
600
|
-
shortcuts: 'Use mode:"shortcuts" to inspect fixed shortcuts plus configurable keybindings. Use mode:"set_keybinding" and mode:"reset_keybinding" with confirm:true plus explicitUserRequest to edit the same config file the user edits.',
|
|
598
|
+
panels: 'Use mode:"panels" to list and mode:"panel" with panelId, target, or query to inspect built-in panel catalog/open state; use mode:"open_panel" with confirm:true plus explicitUserRequest to route a visible panel/workspace change.',
|
|
599
|
+
uiSurfaces: 'Use mode:"ui_surfaces" to list and mode:"ui_surface" with surfaceId, target, or query to inspect modal/overlay/picker/workspace surfaces; use mode:"open_ui_surface" with confirm:true plus explicitUserRequest to route visible UI navigation.',
|
|
600
|
+
shortcuts: 'Use mode:"shortcuts" to inspect fixed shortcuts plus configurable keybindings. Use mode:"keybinding" with actionId, target, key, or query; use mode:"set_keybinding" and mode:"reset_keybinding" with confirm:true plus explicitUserRequest to edit the same config file the user edits.',
|
|
601
601
|
slashCommands: 'Use mode:"commands" to list slash commands and mode:"command" with command, commandName, target, or query to inspect one command; use mode:"run_command" with confirm:true plus explicitUserRequest to execute.',
|
|
602
602
|
workspace: 'Use mode:"workspace_actions" to list and mode:"workspace_action" with actionId, command, target, or query for one action and editor schema; set includeParameters:true on workspace_actions to inline editor schemas.',
|
|
603
603
|
settings: 'Use mode:"settings" to list and mode:"get_setting" with key, target, or query for one setting. Use mode:"set_setting" or mode:"reset_setting" with key, target, or query plus confirm:true and explicitUserRequest.',
|
|
@@ -633,7 +633,7 @@ export function createAgentHarnessTool(deps: AgentHarnessToolDeps): Tool {
|
|
|
633
633
|
}
|
|
634
634
|
if (args.mode === 'panel') {
|
|
635
635
|
const panel = describeHarnessPanel(deps.commandContext, args);
|
|
636
|
-
return panel ? output(panel) : error(`Unknown panel ${readString(args.panelId || args.query) || '<missing>'}.`);
|
|
636
|
+
return panel ? output(panel) : error(`Unknown panel ${readString(args.panelId || args.target || args.query) || '<missing>'}.`);
|
|
637
637
|
}
|
|
638
638
|
if (args.mode === 'open_panel') {
|
|
639
639
|
const confirmationError = requireConfirmedAction(args, 'Panel routing');
|
|
@@ -646,7 +646,7 @@ export function createAgentHarnessTool(deps: AgentHarnessToolDeps): Tool {
|
|
|
646
646
|
}
|
|
647
647
|
if (args.mode === 'ui_surface') {
|
|
648
648
|
const surface = describeHarnessUiSurface(deps.commandContext, args);
|
|
649
|
-
return surface ? output(surface) : error(`Unknown UI surface ${readString(args.surfaceId || args.query) || '<missing>'}.`);
|
|
649
|
+
return surface ? output(surface) : error(`Unknown UI surface ${readString(args.surfaceId || args.query || args.target) || '<missing>'}.`);
|
|
650
650
|
}
|
|
651
651
|
if (args.mode === 'open_ui_surface') {
|
|
652
652
|
const confirmationError = requireConfirmedAction(args, 'UI surface routing');
|
|
@@ -657,7 +657,7 @@ export function createAgentHarnessTool(deps: AgentHarnessToolDeps): Tool {
|
|
|
657
657
|
if (args.mode === 'keybindings') return output(listHarnessKeybindings(deps.commandContext, args));
|
|
658
658
|
if (args.mode === 'keybinding') {
|
|
659
659
|
const binding = describeHarnessKeybinding(deps.commandContext, args);
|
|
660
|
-
return binding ? output(binding) : error(`Unknown keybinding action ${readString(args.actionId || args.key || args.query) || '<missing>'}.`);
|
|
660
|
+
return binding ? output(binding) : error(`Unknown keybinding action ${readString(args.actionId || args.target || args.key || args.query) || '<missing>'}.`);
|
|
661
661
|
}
|
|
662
662
|
if (args.mode === 'set_keybinding') {
|
|
663
663
|
const confirmationError = requireConfirmedAction(args, 'Keybinding mutation');
|
|
@@ -27,6 +27,24 @@ interface UiSurfaceDefinition {
|
|
|
27
27
|
readonly open: (context: CommandContext, args: AgentHarnessUiSurfaceArgs) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
interface UiSurfaceLookup {
|
|
31
|
+
readonly source: 'surfaceId' | 'target' | 'query';
|
|
32
|
+
readonly input: string;
|
|
33
|
+
readonly resolvedBy: 'id' | 'case-insensitive-id' | 'label' | 'case-insensitive-label' | 'search';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type UiSurfaceResolution =
|
|
37
|
+
| {
|
|
38
|
+
readonly status: 'found';
|
|
39
|
+
readonly surface: UiSurfaceDefinition;
|
|
40
|
+
readonly lookup: UiSurfaceLookup;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
readonly status: 'ambiguous';
|
|
44
|
+
readonly input: string;
|
|
45
|
+
readonly candidates: readonly Record<string, unknown>[];
|
|
46
|
+
};
|
|
47
|
+
|
|
30
48
|
function readString(value: unknown): string {
|
|
31
49
|
return typeof value === 'string' ? value.trim() : '';
|
|
32
50
|
}
|
|
@@ -668,7 +686,17 @@ function surfaceMatches(surface: Record<string, unknown>, query: string): boolea
|
|
|
668
686
|
].map((value) => String(value ?? '')).join('\n').toLowerCase().includes(query.toLowerCase());
|
|
669
687
|
}
|
|
670
688
|
|
|
671
|
-
function
|
|
689
|
+
function surfaceLookupFromArgs(args: AgentHarnessUiSurfaceArgs): { readonly source: UiSurfaceLookup['source']; readonly input: string } | null {
|
|
690
|
+
const surfaceId = readString(args.surfaceId);
|
|
691
|
+
if (surfaceId) return { source: 'surfaceId', input: surfaceId };
|
|
692
|
+
const query = readString(args.query);
|
|
693
|
+
if (query) return { source: 'query', input: query };
|
|
694
|
+
const target = readString(args.target);
|
|
695
|
+
if (target) return { source: 'target', input: target };
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function surfaceCandidate(surface: UiSurfaceDefinition): Record<string, unknown> {
|
|
672
700
|
return {
|
|
673
701
|
id: surface.id,
|
|
674
702
|
label: surface.label,
|
|
@@ -676,6 +704,38 @@ function describeSurface(context: CommandContext, surface: UiSurfaceDefinition):
|
|
|
676
704
|
summary: surface.summary,
|
|
677
705
|
command: surface.command,
|
|
678
706
|
preferredModelRoute: surface.preferredModelRoute,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function resolveHarnessUiSurface(args: AgentHarnessUiSurfaceArgs): UiSurfaceResolution | null {
|
|
711
|
+
const lookup = surfaceLookupFromArgs(args);
|
|
712
|
+
if (!lookup) return null;
|
|
713
|
+
const exactId = UI_SURFACES.find((surface) => surface.id === lookup.input);
|
|
714
|
+
if (exactId) return { status: 'found', surface: exactId, lookup: { ...lookup, resolvedBy: 'id' } };
|
|
715
|
+
const exactLabel = UI_SURFACES.find((surface) => surface.label === lookup.input);
|
|
716
|
+
if (exactLabel) return { status: 'found', surface: exactLabel, lookup: { ...lookup, resolvedBy: 'label' } };
|
|
717
|
+
const inputLower = lookup.input.toLowerCase();
|
|
718
|
+
const ciId = UI_SURFACES.filter((surface) => surface.id.toLowerCase() === inputLower);
|
|
719
|
+
if (ciId.length === 1) return { status: 'found', surface: ciId[0]!, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } };
|
|
720
|
+
if (ciId.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: ciId.map(surfaceCandidate).slice(0, 8) };
|
|
721
|
+
const ciLabel = UI_SURFACES.filter((surface) => surface.label.toLowerCase() === inputLower);
|
|
722
|
+
if (ciLabel.length === 1) return { status: 'found', surface: ciLabel[0]!, lookup: { ...lookup, resolvedBy: 'case-insensitive-label' } };
|
|
723
|
+
if (ciLabel.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: ciLabel.map(surfaceCandidate).slice(0, 8) };
|
|
724
|
+
const search = UI_SURFACES.filter((surface) => surfaceMatches(surfaceCandidate(surface), lookup.input));
|
|
725
|
+
if (search.length === 1) return { status: 'found', surface: search[0]!, lookup: { ...lookup, resolvedBy: 'search' } };
|
|
726
|
+
if (search.length > 1) return { status: 'ambiguous', input: lookup.input, candidates: search.map(surfaceCandidate).slice(0, 8) };
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function describeSurface(context: CommandContext, surface: UiSurfaceDefinition, lookup?: UiSurfaceLookup): Record<string, unknown> {
|
|
731
|
+
return {
|
|
732
|
+
id: surface.id,
|
|
733
|
+
label: surface.label,
|
|
734
|
+
kind: surface.kind,
|
|
735
|
+
summary: surface.summary,
|
|
736
|
+
command: surface.command,
|
|
737
|
+
...(lookup ? { lookup } : {}),
|
|
738
|
+
preferredModelRoute: surface.preferredModelRoute,
|
|
679
739
|
parameters: surface.parameters ?? [],
|
|
680
740
|
available: surface.available(context),
|
|
681
741
|
policy: {
|
|
@@ -700,25 +760,29 @@ export function listHarnessUiSurfaces(context: CommandContext, args: AgentHarnes
|
|
|
700
760
|
}
|
|
701
761
|
|
|
702
762
|
export function describeHarnessUiSurface(context: CommandContext, args: AgentHarnessUiSurfaceArgs): Record<string, unknown> | null {
|
|
703
|
-
const
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
763
|
+
const resolved = resolveHarnessUiSurface(args);
|
|
764
|
+
if (resolved?.status === 'found') return describeSurface(context, resolved.surface, resolved.lookup);
|
|
765
|
+
if (resolved?.status === 'ambiguous') {
|
|
766
|
+
return { status: 'ambiguous', input: resolved.input, candidates: resolved.candidates };
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
707
769
|
}
|
|
708
770
|
|
|
709
771
|
export async function openHarnessUiSurface(context: CommandContext, args: AgentHarnessUiSurfaceArgs): Promise<Record<string, unknown>> {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
772
|
+
const resolved = resolveHarnessUiSurface(args);
|
|
773
|
+
if (resolved?.status === 'ambiguous') {
|
|
774
|
+
return { status: 'ambiguous_ui_surface', input: resolved.input, candidates: resolved.candidates };
|
|
775
|
+
}
|
|
776
|
+
if (!resolved) {
|
|
713
777
|
return {
|
|
714
778
|
status: 'unknown_ui_surface',
|
|
715
|
-
surfaceId: surfaceId || '<missing>',
|
|
779
|
+
surfaceId: readString(args.surfaceId || args.query || args.target) || '<missing>',
|
|
716
780
|
availableSurfaces: UI_SURFACES.map((entry) => entry.id),
|
|
717
781
|
};
|
|
718
782
|
}
|
|
719
|
-
const routed = await surface.open(context, args);
|
|
783
|
+
const routed = await resolved.surface.open(context, args);
|
|
720
784
|
return {
|
|
721
785
|
...routed,
|
|
722
|
-
descriptor: describeSurface(context, surface),
|
|
786
|
+
descriptor: describeSurface(context, resolved.surface, resolved.lookup),
|
|
723
787
|
};
|
|
724
788
|
}
|