@xingular/harness 1.0.0 → 1.1.1
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 +33 -5
- package/README.md +70 -19
- package/dist/cli.js +10321 -5785
- package/dist/sdk/agents/builtin.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +34 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/profiles.d.ts +80 -0
- package/dist/sdk/agents/profiles.js +221 -0
- package/dist/sdk/annotations/bridge.d.ts +42 -0
- package/dist/sdk/annotations/bridge.js +264 -0
- package/dist/sdk/annotations/capture.d.ts +116 -0
- package/dist/sdk/annotations/capture.js +308 -0
- package/dist/sdk/annotations/delivery.d.ts +72 -0
- package/dist/sdk/annotations/delivery.js +130 -0
- package/dist/sdk/annotations/inspect.d.ts +10 -0
- package/dist/sdk/annotations/inspect.js +33 -0
- package/dist/sdk/annotations/isolated.d.ts +6 -0
- package/dist/sdk/annotations/isolated.js +51 -0
- package/dist/sdk/annotations/pageScript.d.ts +28 -0
- package/dist/sdk/annotations/pageScript.js +570 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +144 -0
- package/dist/sdk/annotations/queue.d.ts +4 -0
- package/dist/sdk/annotations/queue.js +40 -0
- package/dist/sdk/annotations/serverMethods.d.ts +3 -0
- package/dist/sdk/annotations/serverMethods.js +62 -0
- package/dist/sdk/annotations/store.d.ts +160 -0
- package/dist/sdk/annotations/store.js +386 -0
- package/dist/sdk/browser/index.d.ts +14 -3
- package/dist/sdk/browser/index.js +11 -4
- package/dist/sdk/browser/serverMethods.d.ts +3 -0
- package/dist/sdk/browser/serverMethods.js +7 -0
- package/dist/sdk/browser/session.d.ts +1 -0
- package/dist/sdk/browser/session.js +45 -9
- package/dist/sdk/cli.js +19 -11
- package/dist/sdk/core/bus.js +1 -1
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- package/dist/sdk/doctor/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/harness.d.ts +15 -0
- package/dist/sdk/harness.js +44 -5
- package/dist/sdk/history/recall.js +8 -8
- package/dist/sdk/history/serverMethods.d.ts +3 -0
- package/dist/sdk/history/serverMethods.js +8 -0
- package/dist/sdk/hub/cli.d.ts +1 -1
- package/dist/sdk/hub/cli.js +1 -1
- package/dist/sdk/hub/schema.d.ts +1 -1
- package/dist/sdk/hub/serverMethods.d.ts +3 -0
- package/dist/sdk/hub/serverMethods.js +11 -0
- package/dist/sdk/index.d.ts +10 -0
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/mcp/connections.js +14 -7
- package/dist/sdk/mcp/serverMethods.d.ts +3 -0
- package/dist/sdk/mcp/serverMethods.js +22 -0
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +128 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
- package/dist/sdk/plugins/builtinRegistry.js +49 -0
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- package/dist/sdk/prompt/compile.d.ts +5 -2
- package/dist/sdk/prompt/compile.js +25 -8
- package/dist/sdk/refine/cli.d.ts +1 -1
- package/dist/sdk/refine/cli.js +2 -2
- package/dist/sdk/refine/serverMethods.d.ts +3 -0
- package/dist/sdk/refine/serverMethods.js +20 -0
- package/dist/sdk/server/methods.js +9 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -0
- package/dist/sdk/sessions/contracts.d.ts +17 -0
- package/dist/sdk/sessions/contracts.js +42 -3
- package/dist/sdk/sessions/planMode.d.ts +5 -2
- package/dist/sdk/sessions/planMode.js +12 -3
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +1 -0
- package/dist/sdk/sessions/registry.js +4 -1
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.d.ts +17 -5
- package/dist/sdk/sessions/sessions.js +79 -12
- package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
- package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
- package/dist/sdk/sessions/sideQuestions.js +290 -0
- package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/subagentServerMethods.js +41 -0
- package/dist/sdk/sessions/subagentService.d.ts +13 -2
- package/dist/sdk/sessions/subagentService.js +34 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- package/dist/sdk/terminal/browser.d.ts +62 -0
- package/dist/sdk/terminal/browser.js +41 -0
- package/dist/sdk/terminal/browserControl.d.ts +29 -0
- package/dist/sdk/terminal/browserControl.js +186 -0
- package/dist/sdk/terminal/commandHelp.d.ts +17 -0
- package/dist/sdk/terminal/commandHelp.js +20 -0
- package/dist/sdk/terminal/controller.d.ts +7 -1
- package/dist/sdk/terminal/controller.js +34 -2
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- package/dist/sdk/terminal/piExtension.js +33 -48
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +2 -0
- package/dist/sdk/terminal/standalone.js +50 -10
- package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
- package/dist/sdk/terminal/subagentMonitor.js +121 -0
- package/dist/sdk/terminal/terminals.d.ts +42 -6
- package/dist/sdk/terminal/terminals.js +359 -18
- package/dist/sdk/terminal/trajectory.js +2 -2
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
- package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
- package/dist/sdk/terminal/tui/commandPages.js +54 -6
- package/dist/sdk/terminal/tui/commands.d.ts +8 -4
- package/dist/sdk/terminal/tui/commands.js +181 -58
- package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
- package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
- package/dist/sdk/terminal/tui/dialogs.js +9 -2
- package/dist/sdk/terminal/tui/modelPicker.js +1 -6
- package/dist/sdk/terminal/tui/page.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
- package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
- package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
- package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
- package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
- package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
- package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
- package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
- package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +4 -2
- package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
- package/dist/sdk/terminal/tui/settingsModel.js +32 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
- package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
- package/dist/sdk/terminal/tui/ui.d.ts +2 -0
- package/dist/sdk/terminal/tui/ui.js +1 -0
- package/dist/sdk/tools/askUser.js +26 -8
- package/dist/sdk/tools/checklist.js +2 -2
- package/dist/sdk/tools/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +13 -4
- package/dist/sdk/web/part.d.ts +10 -0
- package/dist/sdk/web/part.js +6 -0
- package/dist/sdk/web/serverMethods.d.ts +3 -0
- package/dist/sdk/web/serverMethods.js +7 -0
- package/dist/sdk/wiki/serverMethods.d.ts +3 -0
- package/dist/sdk/wiki/serverMethods.js +26 -0
- package/dist/sdk/worktrees/cli.d.ts +11 -0
- package/dist/sdk/worktrees/cli.js +146 -0
- package/dist/sdk/worktrees/copy.d.ts +9 -0
- package/dist/sdk/worktrees/copy.js +111 -0
- package/dist/sdk/worktrees/git.d.ts +16 -0
- package/dist/sdk/worktrees/git.js +81 -0
- package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
- package/dist/sdk/worktrees/serverMethods.js +30 -0
- package/dist/sdk/worktrees/service.d.ts +73 -0
- package/dist/sdk/worktrees/service.js +483 -0
- package/dist/sdk/worktrees/setup.d.ts +23 -0
- package/dist/sdk/worktrees/setup.js +65 -0
- package/dist/sdk/worktrees/types.d.ts +91 -0
- package/dist/sdk/worktrees/types.js +1 -0
- package/dist/sdk/world/cli.d.ts +2 -2
- package/dist/sdk/world/cli.js +6 -6
- package/dist/sdk/world/world.d.ts +3 -1
- package/dist/sdk/world/world.js +8 -9
- package/dist/terminal-opentui.js +14540 -7374
- package/dist/terminal-pi.js +285 -666
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +72 -15
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +26 -6
- package/docs/architecture/06-plugins-and-capabilities.md +39 -3
- package/docs/architecture/07-tools-and-gate.md +21 -6
- package/docs/architecture/09-accounts-and-connectors.md +8 -0
- package/docs/architecture/10-server-protocol.md +111 -2
- package/docs/architecture/11-terminal.md +185 -5
- package/docs/architecture/12-storage.md +15 -1
- package/docs/architecture/13-world-tools.md +28 -6
- package/docs/architecture/14-surfaces-and-parity.md +89 -0
- package/docs/architecture/README.md +15 -4
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
- package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
- package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
- package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
- package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
- package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
- package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
- package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
- package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
- package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
- package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
- package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
- package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
- package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
- package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
- package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
- package/docs/architecture/proposals/README.md +25 -0
- package/docs/decisions/ADR-0009-xharness-command.md +6 -5
- package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
- package/docs/decisions/ADR-0015-resource-browser.md +28 -0
- package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
- package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
- package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
- package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
- package/docs/decisions/ADR-0020-page-annotations.md +37 -0
- package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
- package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
- package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
- package/docs/decisions/ADR-0024-xagent-command.md +40 -0
- package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
- package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
- package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
- package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
- package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
- package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
- package/docs/doctor.md +10 -4
- package/docs/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/parity.md +78 -0
- package/docs/releases/1.0.0.md +2 -2
- package/docs/releases/4.0.0.md +46 -0
- package/docs/releases/README.md +20 -15
- package/docs/study/00-index.md +5 -0
- package/docs/study/autonomous-engineering-sources.md +277 -0
- package/docs/study/autonomy-sources.md +147 -0
- package/docs/study/proactive-autonomy.md +408 -0
- package/docs/study/tui-resource-browser.md +78 -0
- package/docs/terminal-tui.md +325 -93
- package/package.json +3 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BUILTIN_PLUGIN_REGISTRY } from "../../plugins/builtin.js";
|
|
2
|
+
import { parseAnnotate } from "./annotateCommand.js";
|
|
3
|
+
/** Terminal adapters for commands declared by Harness-owned feature plugins. */
|
|
4
|
+
export function createBuiltinPluginCommandHandlers(host, openPage) {
|
|
5
|
+
const handlers = new Map([
|
|
6
|
+
['doctor', async () => openPage('doctor')],
|
|
7
|
+
['worktree', async () => openPage('worktree')],
|
|
8
|
+
['trajectory', async (argument) => openPage('trajectory', { runId: argument })],
|
|
9
|
+
['subagents', async () => openPage('subagents')],
|
|
10
|
+
['agents', async () => openPage('agents')],
|
|
11
|
+
['notes', async () => openPage('notes')],
|
|
12
|
+
['annotate', async (argument) => {
|
|
13
|
+
const start = parseAnnotate(argument);
|
|
14
|
+
if (typeof start === 'string')
|
|
15
|
+
host.ui.notify(start, 'error');
|
|
16
|
+
else
|
|
17
|
+
await openPage('annotate', { annotate: start });
|
|
18
|
+
}]
|
|
19
|
+
]);
|
|
20
|
+
const commands = BUILTIN_PLUGIN_REGISTRY.commands();
|
|
21
|
+
for (const command of commands) {
|
|
22
|
+
if (!handlers.has(command.name))
|
|
23
|
+
throw new Error(`No terminal handler for built-in plugin command ${command.pluginId}:${command.name}.`);
|
|
24
|
+
}
|
|
25
|
+
return handlers;
|
|
26
|
+
}
|
|
27
|
+
export { parseAnnotate };
|
|
@@ -4,10 +4,12 @@ export interface TerminalQuestion {
|
|
|
4
4
|
requestId: string;
|
|
5
5
|
question?: string;
|
|
6
6
|
summary?: string;
|
|
7
|
+
uiSketch?: true;
|
|
7
8
|
options: {
|
|
8
9
|
label: string;
|
|
9
10
|
description?: string;
|
|
10
11
|
recommended?: boolean;
|
|
12
|
+
sketch?: string;
|
|
11
13
|
}[];
|
|
12
14
|
multiSelect?: boolean;
|
|
13
15
|
allowText?: boolean;
|
|
@@ -2,7 +2,7 @@ import { createAgentSession, createAgentSessionRuntime, DefaultResourceLoader, M
|
|
|
2
2
|
import { closeSync, existsSync, openSync, readSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import terminalExtension from "../piExtension.js";
|
|
5
|
-
import { planModeTools } from "./planMode.js";
|
|
5
|
+
import { normalModeTools, planModeTools, QUESTION_TOOL_NAMES } from "./planMode.js";
|
|
6
6
|
import { claimTerminalControl, controlCall } from "../control.js";
|
|
7
7
|
import { loadPiAdapter } from "./piAdapter.js";
|
|
8
8
|
import { loadPi } from "../../pi/runtime.js";
|
|
@@ -197,6 +197,7 @@ export async function createTerminalRuntime(args, host) {
|
|
|
197
197
|
...(selected?.model ? { model: selected.model } : {}),
|
|
198
198
|
...(thinkingLevel ? { thinkingLevel } : {})
|
|
199
199
|
});
|
|
200
|
+
created.session.setActiveToolsByName(normalModeTools(created.session.getActiveToolNames()));
|
|
200
201
|
const services = { cwd, agentDir, modelRuntime, settingsManager, resourceLoader, diagnostics: [] };
|
|
201
202
|
host.resources?.(services);
|
|
202
203
|
return { ...created, services, diagnostics: services.diagnostics };
|
|
@@ -296,7 +297,8 @@ export async function createTerminalRuntime(args, host) {
|
|
|
296
297
|
if (!session.isIdle)
|
|
297
298
|
throw new Error('A plan request must wait until the current response settles.');
|
|
298
299
|
const originalTools = session.getActiveToolNames();
|
|
299
|
-
const
|
|
300
|
+
const registeredQuestions = session.getAllTools().map(tool => tool.name).filter(name => QUESTION_TOOL_NAMES.has(name));
|
|
301
|
+
const allowedTools = planModeTools(originalTools, [], registeredQuestions);
|
|
300
302
|
// The session's own model and effort come back once the plan is done.
|
|
301
303
|
const previous = { model: session.model, effort: session.thinkingLevel };
|
|
302
304
|
planRun.allowedTools = new Set(allowedTools);
|
|
@@ -3,10 +3,12 @@ import type { TerminalSettingItem } from "./pages/settingsPage.js";
|
|
|
3
3
|
export interface SettingsModelHost {
|
|
4
4
|
session(): AgentSession;
|
|
5
5
|
ui: ExtensionUIContext;
|
|
6
|
+
control<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
6
7
|
hotkeys?(): string;
|
|
7
8
|
refresh(): void;
|
|
8
9
|
}
|
|
9
10
|
export declare function createSettingsModel(host: SettingsModelHost): {
|
|
10
11
|
items: () => TerminalSettingItem[];
|
|
11
12
|
apply: (id: string) => Promise<void>;
|
|
13
|
+
load: () => Promise<void>;
|
|
12
14
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
const
|
|
1
|
+
const PRIVATE_ACCESS_ID = 'browser.allowPrivate';
|
|
2
|
+
const GROUP_ORDER = ['Theme', 'Display', 'Images', 'Thinking', 'Recovery', 'Editor', 'Queues', 'Commands', 'Terminal', 'Keyboard', 'Compaction', 'Network'];
|
|
2
3
|
export function createSettingsModel(host) {
|
|
4
|
+
let browserSettings = null;
|
|
3
5
|
const booleans = (session) => {
|
|
4
6
|
const settings = session.settingsManager;
|
|
5
7
|
return [
|
|
@@ -40,9 +42,37 @@ export function createSettingsModel(host) {
|
|
|
40
42
|
const split = option.indexOf(':');
|
|
41
43
|
return { id: String(index), tab: locations[index][0], group: locations[index][1], label: split < 0 ? option : option.slice(0, split), value: split < 0 ? '' : option.slice(split + 1).trim(), description: descriptions[index] };
|
|
42
44
|
});
|
|
45
|
+
if (browserSettings)
|
|
46
|
+
result.push({
|
|
47
|
+
id: PRIVATE_ACCESS_ID,
|
|
48
|
+
tab: 'Browser',
|
|
49
|
+
group: 'Network',
|
|
50
|
+
label: 'Private address access',
|
|
51
|
+
value: browserSettings.allowPrivate ? 'On' : 'Off',
|
|
52
|
+
description: 'This device-wide setting affects agent browsing and annotation capture. When on, pages can reach localhost and private networks.'
|
|
53
|
+
});
|
|
43
54
|
return result.sort((a, b) => GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group));
|
|
44
55
|
}
|
|
56
|
+
async function load() {
|
|
57
|
+
const config = await host.control('terminal.browser.config');
|
|
58
|
+
if (!config || typeof config.allowPrivate !== 'boolean')
|
|
59
|
+
throw new Error('The Harness returned invalid browser settings.');
|
|
60
|
+
browserSettings = { allowPrivate: config.allowPrivate };
|
|
61
|
+
}
|
|
45
62
|
async function apply(id) {
|
|
63
|
+
if (id === PRIVATE_ACCESS_ID) {
|
|
64
|
+
if (!browserSettings)
|
|
65
|
+
return;
|
|
66
|
+
const allowPrivate = !browserSettings.allowPrivate;
|
|
67
|
+
if (allowPrivate && !await host.ui.confirm('Allow private network addresses?', 'This device-wide setting lets pages opened by agent browsing and annotation capture reach localhost and private networks.'))
|
|
68
|
+
return;
|
|
69
|
+
const saved = await host.control('terminal.browser.configure', { allowPrivate });
|
|
70
|
+
if (!saved || typeof saved.allowPrivate !== 'boolean')
|
|
71
|
+
throw new Error('The Harness returned invalid browser settings.');
|
|
72
|
+
browserSettings = { allowPrivate: saved.allowPrivate };
|
|
73
|
+
host.refresh();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
46
76
|
const session = host.session();
|
|
47
77
|
const index = Number(id);
|
|
48
78
|
const switches = booleans(session);
|
|
@@ -80,5 +110,5 @@ export function createSettingsModel(host) {
|
|
|
80
110
|
host.ui.notify(host.hotkeys?.() ?? 'Use /help for commands. Keyboard bindings are managed by the terminal application.', 'info');
|
|
81
111
|
host.refresh();
|
|
82
112
|
}
|
|
83
|
-
return { items, apply };
|
|
113
|
+
return { items, apply, load };
|
|
84
114
|
}
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { type Component } from '@earendil-works/pi-tui';
|
|
3
|
-
|
|
4
|
-
export declare const ANSWER_ROWS = 8;
|
|
5
|
-
/**
|
|
6
|
-
* A bounded plain-text excerpt of the main conversation: the last few user and assistant turns, the
|
|
7
|
-
* tools an assistant turn called by name, and whether the reply is still running. Hidden reasoning,
|
|
8
|
-
* tool inputs and results, and images are left out.
|
|
9
|
-
*/
|
|
10
|
-
export declare function sideQuestionSnapshot(messages: readonly unknown[], running: boolean): string;
|
|
11
|
-
/** What the controller needs from a session; a Pi `AgentSession` satisfies it through `createSideSession`. */
|
|
12
|
-
export interface SideSession {
|
|
13
|
-
prompt(text: string): Promise<void>;
|
|
14
|
-
abort(): Promise<void>;
|
|
15
|
-
subscribe(listener: () => void): () => void;
|
|
16
|
-
answer(): string;
|
|
17
|
-
/** Why the provider ended the turn in error, if it did. */
|
|
18
|
-
failure(): string | undefined;
|
|
19
|
-
dispose(): void;
|
|
20
|
-
}
|
|
21
|
-
/** A separate in-memory session on the same account and model, with no tools, extensions, skills or project files. */
|
|
22
|
-
export declare function createSideSession(services: AgentSessionServices, main: AgentSession): Promise<SideSession>;
|
|
3
|
+
import type { SideQuestionContext, SideQuestionRead } from "../../sessions/sideQuestions.js";
|
|
23
4
|
export type SideState = 'thinking' | 'answered' | 'failed';
|
|
5
|
+
type SideQuestionStart = {
|
|
6
|
+
runId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
nativeSessionId: string | null;
|
|
9
|
+
};
|
|
10
|
+
/** Visible native history and tool activity, with hidden reasoning and image bytes omitted. */
|
|
11
|
+
export declare function sideQuestionSnapshot(messages: readonly unknown[], running: boolean): string;
|
|
24
12
|
export interface SideQuestionOptions {
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
context(): SideQuestionContext;
|
|
14
|
+
start(question: string, context: SideQuestionContext): Promise<SideQuestionStart>;
|
|
15
|
+
read(runId: string, afterSequence: number): Promise<SideQuestionRead>;
|
|
16
|
+
interrupt(runId: string): Promise<unknown>;
|
|
17
|
+
copyText(text: string): Promise<void>;
|
|
27
18
|
theme(): Theme;
|
|
28
19
|
/** Puts the dock above the composer, or takes it away. */
|
|
29
20
|
mount(dock: Component | undefined): void;
|
|
@@ -31,23 +22,32 @@ export interface SideQuestionOptions {
|
|
|
31
22
|
}
|
|
32
23
|
export declare class SideQuestion {
|
|
33
24
|
private readonly options;
|
|
34
|
-
private
|
|
35
|
-
private unsubscribe;
|
|
25
|
+
private runId;
|
|
36
26
|
private generation;
|
|
27
|
+
private pollTimer;
|
|
37
28
|
private question;
|
|
38
29
|
private answerText;
|
|
39
30
|
private error;
|
|
40
31
|
private current;
|
|
41
32
|
private startedAt;
|
|
33
|
+
private afterSequence;
|
|
34
|
+
private answerOffset;
|
|
42
35
|
private readonly dock;
|
|
43
36
|
constructor(options: SideQuestionOptions);
|
|
44
37
|
get visible(): boolean;
|
|
45
38
|
get state(): SideState | undefined;
|
|
39
|
+
get answer(): string;
|
|
46
40
|
/** Asks one question, replacing any earlier one. Empty text dismisses. */
|
|
47
41
|
ask(question: string): Promise<void>;
|
|
42
|
+
/** Scrolls the answer while it is longer than the dock. */
|
|
43
|
+
scroll(direction: 'up' | 'down'): void;
|
|
44
|
+
/** Copies the whole side answer, including any lines outside the dock. */
|
|
45
|
+
copyAnswer(): Promise<void>;
|
|
48
46
|
/** Interrupts a running side question and removes the dock. */
|
|
49
47
|
dismiss(): Promise<void>;
|
|
48
|
+
private poll;
|
|
50
49
|
private release;
|
|
51
|
-
/** The dock:
|
|
50
|
+
/** The dock: question, a scrollable answer window, and a hint for browsing or copying. */
|
|
52
51
|
private render;
|
|
53
52
|
}
|
|
53
|
+
export {};
|
|
@@ -1,89 +1,79 @@
|
|
|
1
|
-
// `/btw <question>`: a side question answered by a separate, tool-free session while the main
|
|
2
|
-
// conversation carries on. It lives in a dock above the composer, never enters the transcript, the
|
|
3
|
-
// queue or the run history, and is gone on dismiss, on a session change, or when a new one replaces it.
|
|
4
|
-
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager } from '@earendil-works/pi-coding-agent';
|
|
5
1
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
6
2
|
import { contentWidth, footerRow, neutralRule, padded, safeText } from "./chrome.js";
|
|
7
3
|
import { getWorkingSpinnerFrames, SPINNER_INTERVAL_MS, spinnerFrameAt } from "./workingStatus.js";
|
|
8
|
-
export const SIDE_PROMPT = [
|
|
9
|
-
'You answer one short side question from a person whose main task is still running in another session.',
|
|
10
|
-
'You have no tools: you cannot read or change files, run commands, browse, or call anything, and you must not say you did.',
|
|
11
|
-
'Use the conversation excerpt for context and do not invent what it does not show. Answer briefly and plainly.'
|
|
12
|
-
].join(' ');
|
|
13
|
-
const TURNS = 6;
|
|
14
|
-
const TURN_CHARS = 1_200;
|
|
15
|
-
const TOTAL_CHARS = 6_000;
|
|
16
4
|
const QUESTION_ROWS = 2;
|
|
17
|
-
|
|
5
|
+
const ANSWER_ROWS = 8;
|
|
6
|
+
const MAX_CLIENT_CONTEXT_CHARS = 300_000;
|
|
7
|
+
const POLL_INTERVAL_MS = 160;
|
|
18
8
|
function blocks(content) {
|
|
19
9
|
if (typeof content === 'string')
|
|
20
10
|
return [{ type: 'text', text: content }];
|
|
21
11
|
return Array.isArray(content) ? content.filter((block) => typeof block === 'object' && block !== null) : [];
|
|
22
12
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
function contentText(content) {
|
|
14
|
+
return blocks(content).filter(block => block.type === 'text' && typeof block.text === 'string').map(block => String(block.text)).join('\n').trim();
|
|
15
|
+
}
|
|
16
|
+
function serialized(value) {
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return value;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(value) ?? '';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '[unavailable]';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Visible native history and tool activity, with hidden reasoning and image bytes omitted. */
|
|
29
27
|
export function sideQuestionSnapshot(messages, running) {
|
|
30
|
-
const
|
|
28
|
+
const lines = [];
|
|
31
29
|
for (const message of messages) {
|
|
32
|
-
|
|
33
|
-
if (role !== 'user' && role !== 'assistant')
|
|
34
|
-
continue;
|
|
35
|
-
const parts = blocks(content);
|
|
36
|
-
const text = parts.filter(block => block.type === 'text' && typeof block.text === 'string').map(block => String(block.text)).join('\n').trim();
|
|
37
|
-
const tools = [...new Set(parts.filter(block => block.type === 'toolCall' && typeof block.name === 'string').map(block => String(block.name)))];
|
|
38
|
-
if (!text && !tools.length)
|
|
30
|
+
if (!message || typeof message !== 'object')
|
|
39
31
|
continue;
|
|
40
|
-
|
|
32
|
+
const item = message;
|
|
33
|
+
const parts = blocks(item.content);
|
|
34
|
+
if (item.role === 'user' || item.role === 'assistant') {
|
|
35
|
+
const text = parts.filter(block => block.type === 'text' && typeof block.text === 'string').map(block => String(block.text)).join('\n').trim();
|
|
36
|
+
if (text)
|
|
37
|
+
lines.push(`${item.role === 'user' ? 'User' : 'Assistant'}:\n${safeText(text)}`);
|
|
38
|
+
for (const call of parts.filter(block => block.type === 'toolCall')) {
|
|
39
|
+
const name = typeof call.name === 'string' ? call.name : 'unknown tool';
|
|
40
|
+
lines.push(`Assistant tool call (${name}):\n${safeText(serialized(call.arguments))}`);
|
|
41
|
+
}
|
|
42
|
+
if (parts.some(block => block.type === 'image'))
|
|
43
|
+
lines.push(`${item.role === 'user' ? 'User' : 'Assistant'} attached an image; image bytes are omitted.`);
|
|
44
|
+
}
|
|
45
|
+
else if (item.role === 'toolResult') {
|
|
46
|
+
const name = typeof item.toolName === 'string' ? item.toolName : typeof item.name === 'string' ? item.name : 'unknown tool';
|
|
47
|
+
const output = contentText(item.content);
|
|
48
|
+
lines.push(`Tool result (${name}${typeof item.toolCallId === 'string' ? `, ${item.toolCallId}` : ''}):\n${safeText(output || '(no text result)')}`);
|
|
49
|
+
if (parts.some(block => block.type === 'image'))
|
|
50
|
+
lines.push('Tool result also contained an image; image bytes are omitted.');
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
recent = recent.slice(1);
|
|
45
|
-
const lines = ['Conversation excerpt, oldest first:', ...(recent.length ? recent : ['(nothing yet)'])];
|
|
53
|
+
if (!lines.length)
|
|
54
|
+
lines.push('(No visible conversation messages yet.)');
|
|
46
55
|
if (running)
|
|
47
|
-
lines.push('
|
|
48
|
-
|
|
56
|
+
lines.push('The main reply is still running; its latest message may be unfinished.');
|
|
57
|
+
const full = lines.join('\n\n');
|
|
58
|
+
return full.length > MAX_CLIENT_CONTEXT_CHARS
|
|
59
|
+
? `[Earlier visible context omitted to fit the side-question request.]\n\n${full.slice(-MAX_CLIENT_CONTEXT_CHARS)}`
|
|
60
|
+
: full;
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const model = main.model;
|
|
53
|
-
if (!model || model.id === 'unknown')
|
|
54
|
-
throw new Error('Choose a model before asking a side question.');
|
|
55
|
-
const settingsManager = SettingsManager.inMemory({});
|
|
56
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
57
|
-
cwd: services.cwd, agentDir: services.agentDir, settingsManager,
|
|
58
|
-
noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true, systemPrompt: SIDE_PROMPT
|
|
59
|
-
});
|
|
60
|
-
await resourceLoader.reload();
|
|
61
|
-
const { session } = await createAgentSession({
|
|
62
|
-
cwd: services.cwd, agentDir: services.agentDir, modelRuntime: services.modelRuntime, settingsManager, resourceLoader,
|
|
63
|
-
sessionManager: SessionManager.inMemory(services.cwd), model, thinkingLevel: 'off', noTools: 'all'
|
|
64
|
-
});
|
|
65
|
-
return {
|
|
66
|
-
prompt: text => session.prompt(text),
|
|
67
|
-
abort: () => session.abort(),
|
|
68
|
-
subscribe: listener => session.subscribe(() => listener()),
|
|
69
|
-
answer: () => session.getLastAssistantText() ?? '',
|
|
70
|
-
failure: () => {
|
|
71
|
-
const last = session.messages.at(-1);
|
|
72
|
-
return last?.role === 'assistant' && last.stopReason === 'error' ? last.errorMessage ?? 'The provider returned an error.' : undefined;
|
|
73
|
-
},
|
|
74
|
-
dispose: () => session.dispose()
|
|
75
|
-
};
|
|
62
|
+
function payload(event) {
|
|
63
|
+
return event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
76
64
|
}
|
|
77
65
|
export class SideQuestion {
|
|
78
66
|
options;
|
|
79
|
-
|
|
80
|
-
unsubscribe;
|
|
67
|
+
runId;
|
|
81
68
|
generation = 0;
|
|
69
|
+
pollTimer;
|
|
82
70
|
question = '';
|
|
83
71
|
answerText = '';
|
|
84
72
|
error;
|
|
85
73
|
current;
|
|
86
74
|
startedAt = 0;
|
|
75
|
+
afterSequence = -1;
|
|
76
|
+
answerOffset = 0;
|
|
87
77
|
dock;
|
|
88
78
|
constructor(options) {
|
|
89
79
|
this.options = options;
|
|
@@ -91,6 +81,7 @@ export class SideQuestion {
|
|
|
91
81
|
}
|
|
92
82
|
get visible() { return this.current !== undefined; }
|
|
93
83
|
get state() { return this.current; }
|
|
84
|
+
get answer() { return this.answerText; }
|
|
94
85
|
/** Asks one question, replacing any earlier one. Empty text dismisses. */
|
|
95
86
|
async ask(question) {
|
|
96
87
|
const text = question.trim();
|
|
@@ -98,73 +89,111 @@ export class SideQuestion {
|
|
|
98
89
|
await this.dismiss();
|
|
99
90
|
return;
|
|
100
91
|
}
|
|
101
|
-
await this.release();
|
|
102
92
|
const generation = ++this.generation;
|
|
93
|
+
await this.release();
|
|
103
94
|
this.question = text;
|
|
104
95
|
this.startedAt = Date.now();
|
|
105
96
|
this.answerText = '';
|
|
106
97
|
this.error = undefined;
|
|
98
|
+
this.answerOffset = 0;
|
|
99
|
+
this.afterSequence = -1;
|
|
107
100
|
this.current = 'thinking';
|
|
108
101
|
this.options.mount(this.dock);
|
|
109
102
|
this.options.changed();
|
|
110
103
|
try {
|
|
111
|
-
const
|
|
104
|
+
const started = await this.options.start(text, this.options.context());
|
|
112
105
|
if (generation !== this.generation) {
|
|
113
|
-
|
|
106
|
+
await this.options.interrupt(started.runId).catch(() => undefined);
|
|
114
107
|
return;
|
|
115
108
|
}
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
118
|
-
if (generation !== this.generation)
|
|
119
|
-
return;
|
|
120
|
-
this.answerText = session.answer();
|
|
121
|
-
this.options.changed();
|
|
122
|
-
});
|
|
123
|
-
await session.prompt(`${this.options.snapshot()}\n\nSide question: ${text}`);
|
|
124
|
-
if (generation !== this.generation)
|
|
125
|
-
return;
|
|
126
|
-
this.answerText = session.answer();
|
|
127
|
-
const failure = session.failure();
|
|
128
|
-
if (failure) {
|
|
129
|
-
this.current = 'failed';
|
|
130
|
-
this.error = failure;
|
|
131
|
-
}
|
|
132
|
-
else
|
|
133
|
-
this.current = 'answered';
|
|
109
|
+
this.runId = started.runId;
|
|
110
|
+
void this.poll(generation);
|
|
134
111
|
}
|
|
135
112
|
catch (error) {
|
|
136
113
|
if (generation !== this.generation)
|
|
137
114
|
return;
|
|
138
115
|
this.current = 'failed';
|
|
139
116
|
this.error = error instanceof Error ? error.message : String(error);
|
|
117
|
+
this.options.changed();
|
|
140
118
|
}
|
|
119
|
+
}
|
|
120
|
+
/** Scrolls the answer while it is longer than the dock. */
|
|
121
|
+
scroll(direction) {
|
|
122
|
+
const step = Math.max(1, ANSWER_ROWS - 2);
|
|
123
|
+
this.answerOffset = Math.max(0, this.answerOffset + (direction === 'up' ? -step : step));
|
|
141
124
|
this.options.changed();
|
|
142
125
|
}
|
|
126
|
+
/** Copies the whole side answer, including any lines outside the dock. */
|
|
127
|
+
async copyAnswer() {
|
|
128
|
+
if (this.answerText)
|
|
129
|
+
await this.options.copyText(this.answerText);
|
|
130
|
+
}
|
|
143
131
|
/** Interrupts a running side question and removes the dock. */
|
|
144
132
|
async dismiss() {
|
|
145
133
|
this.generation++;
|
|
146
|
-
this.current = undefined;
|
|
147
134
|
await this.release();
|
|
135
|
+
this.current = undefined;
|
|
148
136
|
this.options.mount(undefined);
|
|
149
137
|
this.options.changed();
|
|
150
138
|
}
|
|
151
|
-
async
|
|
152
|
-
const
|
|
153
|
-
this.
|
|
154
|
-
this.unsubscribe?.();
|
|
155
|
-
this.unsubscribe = undefined;
|
|
156
|
-
if (!session)
|
|
139
|
+
async poll(generation) {
|
|
140
|
+
const runId = this.runId;
|
|
141
|
+
if (!runId || generation !== this.generation)
|
|
157
142
|
return;
|
|
158
143
|
try {
|
|
159
|
-
await
|
|
144
|
+
const result = await this.options.read(runId, this.afterSequence);
|
|
145
|
+
if (generation !== this.generation)
|
|
146
|
+
return;
|
|
147
|
+
for (const event of result.events) {
|
|
148
|
+
this.afterSequence = Math.max(this.afterSequence, event.sequence);
|
|
149
|
+
const data = payload(event);
|
|
150
|
+
if (event.type === 'message.delta' && typeof data.text === 'string')
|
|
151
|
+
this.answerText += data.text;
|
|
152
|
+
if (event.type === 'message.completed' && data.role === 'assistant' && typeof data.text === 'string')
|
|
153
|
+
this.answerText = data.text;
|
|
154
|
+
if (event.type === 'failed')
|
|
155
|
+
this.error = typeof data.message === 'string' ? data.message : 'The side question failed.';
|
|
156
|
+
if (event.type === 'status.changed' && data.status === 'failed')
|
|
157
|
+
this.error = typeof data.reason === 'string' ? data.reason : 'The side question failed.';
|
|
158
|
+
if (event.type === 'status.changed' && data.status === 'interrupted')
|
|
159
|
+
this.error = 'The side question was interrupted.';
|
|
160
|
+
if (event.type === 'completed')
|
|
161
|
+
this.current = 'answered';
|
|
162
|
+
if (event.type === 'failed' || event.type === 'status.changed' && ['failed', 'interrupted'].includes(String(data.status)))
|
|
163
|
+
this.current = 'failed';
|
|
164
|
+
}
|
|
165
|
+
if (result.run.status === 'completed')
|
|
166
|
+
this.current = 'answered';
|
|
167
|
+
if (result.run.status === 'failed' || result.run.status === 'interrupted') {
|
|
168
|
+
this.current = 'failed';
|
|
169
|
+
this.error ??= result.run.terminalReason ?? 'The side question did not complete.';
|
|
170
|
+
}
|
|
171
|
+
this.options.changed();
|
|
172
|
+
if (this.current === 'thinking')
|
|
173
|
+
this.pollTimer = setTimeout(() => { void this.poll(generation); }, POLL_INTERVAL_MS);
|
|
160
174
|
}
|
|
161
|
-
catch {
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (generation !== this.generation)
|
|
177
|
+
return;
|
|
178
|
+
this.current = 'failed';
|
|
179
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
180
|
+
this.options.changed();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async release() {
|
|
184
|
+
if (this.pollTimer)
|
|
185
|
+
clearTimeout(this.pollTimer);
|
|
186
|
+
this.pollTimer = undefined;
|
|
187
|
+
const runId = this.runId;
|
|
188
|
+
this.runId = undefined;
|
|
189
|
+
if (!runId || this.current !== 'thinking')
|
|
190
|
+
return;
|
|
162
191
|
try {
|
|
163
|
-
|
|
192
|
+
await this.options.interrupt(runId);
|
|
164
193
|
}
|
|
165
|
-
catch { /*
|
|
194
|
+
catch { /* The dock can still be dismissed if a run already stopped. */ }
|
|
166
195
|
}
|
|
167
|
-
/** The dock:
|
|
196
|
+
/** The dock: question, a scrollable answer window, and a hint for browsing or copying. */
|
|
168
197
|
render(width) {
|
|
169
198
|
if (width < 1 || !this.current)
|
|
170
199
|
return [];
|
|
@@ -184,14 +213,18 @@ export class SideQuestion {
|
|
|
184
213
|
question[QUESTION_ROWS - 1] = truncateToWidth(question[QUESTION_ROWS - 1], inner - 1, '') + theme.fg('muted', '…');
|
|
185
214
|
const text = this.current === 'failed' ? `${this.answerText ? `${this.answerText}\n` : ''}${this.error ?? 'The side question failed.'}` : this.answerText;
|
|
186
215
|
const wrapped = text ? text.split('\n').flatMap(line => line ? wrapTextWithAnsi(safeText(line), inner) : ['']) : [];
|
|
187
|
-
const
|
|
188
|
-
|
|
216
|
+
const maxOffset = Math.max(0, wrapped.length - ANSWER_ROWS);
|
|
217
|
+
this.answerOffset = Math.min(this.answerOffset, maxOffset);
|
|
218
|
+
const shown = wrapped.slice(this.answerOffset, this.answerOffset + ANSWER_ROWS).map(line => this.current === 'failed' ? theme.fg('error', line) : line);
|
|
219
|
+
const before = this.answerOffset;
|
|
220
|
+
const after = Math.max(0, wrapped.length - before - shown.length);
|
|
221
|
+
const textHint = before || after ? `PgUp/PgDn browse · ${before ? `${before} above` : ''}${before && after ? ' · ' : ''}${after ? `${after} below` : ''} · ` : '';
|
|
189
222
|
return [
|
|
190
223
|
neutralRule(width, theme),
|
|
191
224
|
...question.map(line => padded(line, width)),
|
|
192
225
|
...(shown.length ? ['', ...shown.map(line => padded(line, width))] : []),
|
|
193
226
|
'',
|
|
194
|
-
footerRow(`${
|
|
227
|
+
footerRow(`${textHint}Ctrl+X copy · /btw or Esc dismiss`, width, theme)
|
|
195
228
|
].map(line => truncateToWidth(line, width, ''));
|
|
196
229
|
}
|
|
197
230
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Theme, type AutocompleteProviderFactory, type ExtensionUIContext, type KeybindingsManager, type WorkingIndicatorOptions } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { type AutocompleteProvider, type EditorComponent, type TUI } from '@earendil-works/pi-tui';
|
|
3
|
+
import { type GroupedSelectOption } from "./dialogs.js";
|
|
3
4
|
type EditorFactory = Parameters<ExtensionUIContext['setEditorComponent']>[0];
|
|
4
5
|
type HeaderFactory = Parameters<ExtensionUIContext['setHeader']>[0];
|
|
5
6
|
type FooterFactory = Parameters<ExtensionUIContext['setFooter']>[0];
|
|
@@ -37,6 +38,7 @@ export interface TerminalUiOptions {
|
|
|
37
38
|
export interface TerminalUi extends ExtensionUIContext {
|
|
38
39
|
readonly state: TerminalUiState;
|
|
39
40
|
secretInput: ExtensionUIContext['input'];
|
|
41
|
+
selectGrouped(title: string, options: readonly GroupedSelectOption[], opts?: Parameters<ExtensionUIContext['select']>[2]): Promise<string | undefined>;
|
|
40
42
|
wrapAutocomplete(provider: AutocompleteProvider): AutocompleteProvider;
|
|
41
43
|
reset(): void;
|
|
42
44
|
setThemes(themes: Theme[]): void;
|
|
@@ -22,6 +22,7 @@ export function createTerminalUi(options) {
|
|
|
22
22
|
return {
|
|
23
23
|
state,
|
|
24
24
|
select: (title, items, opts) => dialogs.select(title, items, opts),
|
|
25
|
+
selectGrouped: (title, items, opts) => dialogs.selectGrouped(title, items, opts),
|
|
25
26
|
confirm: (title, message, opts) => dialogs.confirm(title, message, opts),
|
|
26
27
|
input: (title, placeholder, opts) => dialogs.input(title, placeholder, opts),
|
|
27
28
|
secretInput: (title, placeholder, opts) => dialogs.input(title, placeholder, opts, true),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Asking the person a question and waiting for the answer: part of every session.
|
|
2
|
+
import { QUESTION_SKETCH_MAX_LENGTH, QUESTION_SKETCH_PATTERN, questionSketchError } from "../sessions/contracts.js";
|
|
1
3
|
export const ASK_USER = 'ask_user';
|
|
2
4
|
const MAX_OPTIONS = 6;
|
|
3
5
|
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
@@ -16,16 +18,18 @@ export function askUserTool(run) {
|
|
|
16
18
|
return {
|
|
17
19
|
name: ASK_USER,
|
|
18
20
|
label: 'Ask the user',
|
|
19
|
-
description: 'Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 2
|
|
20
|
-
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. Use it when you need a decision, not for small talk.',
|
|
21
|
+
description: 'Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1–2 word label and aim to keep each question to 25 words or fewer. ' +
|
|
22
|
+
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision, not for small talk.',
|
|
21
23
|
parameters: (Type) => Type.Object({
|
|
22
24
|
questions: Type.Array(Type.Object({
|
|
23
|
-
summary: Type.String({ description: 'A concise
|
|
25
|
+
summary: Type.String({ description: 'A concise label of one or two words' }),
|
|
24
26
|
question: Type.String({ description: 'A clear, standalone question; aim for 25 words or fewer' }),
|
|
27
|
+
uiSketch: Type.Optional(Type.Literal(true, { description: 'Enable an ASCII sketch preview; when true, every choice must include a sketch.' })),
|
|
25
28
|
options: Type.Array(Type.Object({
|
|
26
29
|
label: Type.String({ description: 'A short choice; do not add a recommendation suffix to the label' }),
|
|
27
30
|
description: Type.String({ description: 'Required. Briefly explain this choice in 25 words or fewer.' }),
|
|
28
|
-
recommended: Type.Optional(Type.Boolean({ description: 'Whether to show this choice as a visual recommendation; at least one choice per question must be recommended, it is not preselected, and answers use the original label' }))
|
|
31
|
+
recommended: Type.Optional(Type.Boolean({ description: 'Whether to show this choice as a visual recommendation; at least one choice per question must be recommended, it is not preselected, and answers use the original label' })),
|
|
32
|
+
sketch: Type.Optional(Type.String({ minLength: 1, maxLength: QUESTION_SKETCH_MAX_LENGTH, pattern: QUESTION_SKETCH_PATTERN, description: 'Literal ASCII UI wireframe: 1–24 LF-separated lines, at most 80 printable ASCII characters per line. Include the affected element and neighboring UI using structured table/panel borders with fixed, matching outer-edge columns on every row. Required for every choice when uiSketch is true.' }))
|
|
29
33
|
}), {
|
|
30
34
|
minItems: 1,
|
|
31
35
|
maxItems: MAX_OPTIONS,
|
|
@@ -37,26 +41,33 @@ export function askUserTool(run) {
|
|
|
37
41
|
execute: async (input) => {
|
|
38
42
|
if (!Array.isArray(input.questions) || input.questions.length < 1 || input.questions.length > 5)
|
|
39
43
|
return 'Ask between one and five questions.';
|
|
44
|
+
let invalidSketchMode = false;
|
|
45
|
+
let invalidSketchValue = false;
|
|
40
46
|
const questions = input.questions.map((raw) => {
|
|
41
47
|
const item = raw;
|
|
42
48
|
const summary = text(item.summary);
|
|
43
49
|
const question = text(item.question);
|
|
50
|
+
if (item.uiSketch !== undefined && item.uiSketch !== true)
|
|
51
|
+
invalidSketchMode = true;
|
|
44
52
|
const options = (Array.isArray(item.options) ? item.options : [])
|
|
45
53
|
.map((option) => {
|
|
46
54
|
const choice = option;
|
|
55
|
+
if (choice.sketch !== undefined && typeof choice.sketch !== 'string')
|
|
56
|
+
invalidSketchValue = true;
|
|
47
57
|
return {
|
|
48
58
|
label: text(choice.label),
|
|
49
59
|
description: text(choice.description),
|
|
50
|
-
...(choice.recommended === true ? { recommended: true } : {})
|
|
60
|
+
...(choice.recommended === true ? { recommended: true } : {}),
|
|
61
|
+
...(typeof choice.sketch === 'string' ? { sketch: choice.sketch } : {})
|
|
51
62
|
};
|
|
52
63
|
})
|
|
53
64
|
.filter((option) => option.label)
|
|
54
65
|
.slice(0, MAX_OPTIONS);
|
|
55
66
|
const multiSelect = options.length > 1 && item.multiSelect === true;
|
|
56
|
-
return { summary, question, options, multiSelect, allowText: true };
|
|
67
|
+
return { summary, question, options, ...(item.uiSketch === true ? { uiSketch: true } : {}), multiSelect, allowText: true };
|
|
57
68
|
});
|
|
58
|
-
if (questions.some((question) => !question.summary || question.summary.length > 100))
|
|
59
|
-
return 'Each question needs a
|
|
69
|
+
if (questions.some((question) => !question.summary || question.summary.length > 100 || question.summary.split(/\s+/u).filter(Boolean).length > 2))
|
|
70
|
+
return 'Each question needs a concise 1–2 word summary label of no more than 100 characters.';
|
|
60
71
|
if (questions.some((question) => !question.question))
|
|
61
72
|
return 'Every question needs a question sentence.';
|
|
62
73
|
if (questions.some((question) => question.options.length === 0))
|
|
@@ -67,6 +78,13 @@ export function askUserTool(run) {
|
|
|
67
78
|
return 'A single-choice question may recommend at most one option.';
|
|
68
79
|
if (questions.some((question) => question.options.some((option) => !option.description)))
|
|
69
80
|
return 'Every choice needs a short description. Aim for 25 words or fewer.';
|
|
81
|
+
if (invalidSketchMode)
|
|
82
|
+
return 'Set uiSketch to true to enable question sketches.';
|
|
83
|
+
if (invalidSketchValue)
|
|
84
|
+
return 'Each choice sketch must be text.';
|
|
85
|
+
const sketchError = questions.map((question) => questionSketchError(question.uiSketch, question.options)).find(Boolean);
|
|
86
|
+
if (sketchError)
|
|
87
|
+
return sketchError;
|
|
70
88
|
const answers = await run.requestAnswers(questions);
|
|
71
89
|
return questions.map((question, index) => `${question.summary}: ${describeAnswer(answers[index])}`).join('\n');
|
|
72
90
|
}
|