@xingular/harness 1.5.0-unstable → 1.6.0-unstable
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 +93 -0
- package/browser-extension/README.md +32 -0
- package/browser-extension/background.js +612 -0
- package/browser-extension/manifest.json +16 -0
- package/browser-extension/overlay.js +97 -0
- package/browser-extension/popup.css +19 -0
- package/browser-extension/popup.html +30 -0
- package/browser-extension/popup.js +101 -0
- package/browser-extension/site-access.js +63 -0
- package/dist/cli.js +1489 -680
- package/dist/sdk/browser/config.d.ts +3 -0
- package/dist/sdk/browser/config.js +2 -1
- package/dist/sdk/browser/extension/bridge.d.ts +55 -0
- package/dist/sdk/browser/extension/bridge.js +336 -0
- package/dist/sdk/browser/extension/session.d.ts +50 -0
- package/dist/sdk/browser/extension/session.js +321 -0
- package/dist/sdk/browser/index.d.ts +7 -2
- package/dist/sdk/browser/index.js +58 -25
- package/dist/sdk/browser/serverMethods.js +4 -1
- package/dist/sdk/browser/tools.d.ts +99 -2
- package/dist/sdk/browser/tools.js +6 -1
- package/dist/sdk/commands/registry.d.ts +5 -0
- package/dist/sdk/commands/registry.js +1 -0
- package/dist/sdk/harness.d.ts +4 -0
- package/dist/sdk/harness.js +1 -1
- package/dist/sdk/pi/retryPolicy.d.ts +19 -0
- package/dist/sdk/pi/retryPolicy.js +11 -0
- package/dist/sdk/prompt/compile.d.ts +0 -2
- package/dist/sdk/prompt/compile.js +2 -6
- package/dist/sdk/prompt/reporting.d.ts +1 -1
- package/dist/sdk/prompt/reporting.js +30 -13
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/remote/webClient.js +6 -2
- package/dist/sdk/sessions/contracts.d.ts +2 -0
- package/dist/sdk/sessions/contracts.js +5 -1
- package/dist/sdk/sessions/planMode.d.ts +16 -6
- package/dist/sdk/sessions/planMode.js +59 -23
- package/dist/sdk/sessions/runner.js +14 -5
- package/dist/sdk/sessions/workSlice.d.ts +15 -1
- package/dist/sdk/sessions/workSlice.js +17 -9
- package/dist/sdk/terminal/controller.d.ts +6 -0
- package/dist/sdk/terminal/controller.js +44 -5
- package/dist/sdk/terminal/opentui/model.js +3 -1
- package/dist/sdk/terminal/piExtension.js +7 -2
- package/dist/sdk/terminal/stallWatch.d.ts +18 -0
- package/dist/sdk/terminal/stallWatch.js +20 -0
- package/dist/sdk/terminal/subagentMonitor.d.ts +2 -0
- package/dist/sdk/terminal/subagentMonitor.js +4 -1
- package/dist/sdk/terminal/terminals.js +35 -4
- package/dist/sdk/terminal/toolName.d.ts +6 -0
- package/dist/sdk/terminal/toolName.js +12 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +14 -0
- package/dist/sdk/terminal/tui/commands.js +4 -1
- package/dist/sdk/terminal/tui/pages/followUpQueuePage.d.ts +34 -0
- package/dist/sdk/terminal/tui/pages/followUpQueuePage.js +130 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +37 -7
- package/dist/sdk/terminal/tui/runtime.js +2 -1
- package/dist/sdk/terminal/tui/settingsModel.js +88 -7
- package/dist/sdk/tools/askUser.js +2 -2
- package/dist/sdk/world/world.d.ts +12 -2
- package/dist/sdk/world/world.js +42 -4
- package/dist/terminal-opentui.js +1456 -756
- package/dist/terminal-pi.js +62 -31
- package/docs/README.md +1 -0
- package/docs/architecture/02-sessions-and-runs.md +43 -25
- package/docs/architecture/04-prompt.md +23 -22
- package/docs/architecture/10-server-protocol.md +23 -3
- package/docs/architecture/11-terminal.md +41 -29
- package/docs/architecture/12-storage.md +2 -1
- package/docs/architecture/13-world-tools.md +35 -22
- package/docs/architecture/14-surfaces-and-parity.md +24 -20
- package/docs/architecture/README.md +4 -1
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +3 -1
- package/docs/decisions/ADR-0027-connected-sketch-display.md +4 -2
- package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +6 -1
- package/docs/decisions/ADR-0077-plan-runs-may-conclude-no-plan-needed.md +120 -0
- package/docs/decisions/ADR-0078-child-aware-stall-watch.md +46 -0
- package/docs/decisions/ADR-0079-phase-led-reporting-guidance.md +72 -0
- package/docs/decisions/ADR-0080-fixed-width-question-sketch-canvas.md +50 -0
- package/docs/decisions/ADR-0081-no-work-slice-budget-by-default.md +36 -0
- package/docs/decisions/ADR-0082-connected-brave-browser.md +72 -0
- package/docs/parity.md +7 -5
- package/docs/releases/1.5.0-unstable.md +12 -3
- package/docs/releases/1.5.1-unstable.md +24 -0
- package/docs/releases/1.6.0-unstable.md +59 -0
- package/docs/study/00-index.md +17 -1
- package/docs/study/STUDY-0007-claude-code-prompt.md +162 -0
- package/docs/study/STUDY-0008-browser-control.md +281 -0
- package/docs/study/STUDY-0009-claude-code-reporting.md +205 -0
- package/docs/study/sources/claude-code.md +51 -39
- package/docs/terminal-tui.md +46 -18
- package/package.json +2 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { matchesKey } from '@earendil-works/pi-tui';
|
|
2
|
+
import { isPrimaryClick } from "../mouse.js";
|
|
3
|
+
const TABS = [{ id: 'queue', title: 'Follow-up queue' }];
|
|
4
|
+
export class FollowUpQueuePage {
|
|
5
|
+
options;
|
|
6
|
+
presentation = 'centered';
|
|
7
|
+
selected = 0;
|
|
8
|
+
disposed = false;
|
|
9
|
+
closed = false;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
get focused() { return true; }
|
|
14
|
+
set focused(_value) { }
|
|
15
|
+
tabs() { return TABS; }
|
|
16
|
+
activeTab() { return 'queue'; }
|
|
17
|
+
hint() { return '↑↓ select · Enter actions · e edit · s send next · d remove · r return · Esc close'; }
|
|
18
|
+
activate() { this.options.changed(); }
|
|
19
|
+
dispose() {
|
|
20
|
+
if (this.disposed)
|
|
21
|
+
return;
|
|
22
|
+
this.disposed = true;
|
|
23
|
+
this.closeEditor();
|
|
24
|
+
}
|
|
25
|
+
closeEditor() {
|
|
26
|
+
if (this.closed)
|
|
27
|
+
return;
|
|
28
|
+
this.closed = true;
|
|
29
|
+
this.options.editor.close();
|
|
30
|
+
}
|
|
31
|
+
entries() { return this.options.editor.entries(); }
|
|
32
|
+
render(_width, _rows) {
|
|
33
|
+
const entries = this.entries();
|
|
34
|
+
const steering = entries.filter(entry => entry.kind === 'steering');
|
|
35
|
+
const followUp = entries.filter(entry => entry.kind === 'followUp');
|
|
36
|
+
const result = ['Follow-up and steering are shown in FIFO order.', ''];
|
|
37
|
+
for (const [title, group] of [['Steer', steering], ['Follow-up', followUp]]) {
|
|
38
|
+
result.push(title);
|
|
39
|
+
if (!group.length)
|
|
40
|
+
result.push(' (empty)');
|
|
41
|
+
for (const entry of group) {
|
|
42
|
+
const index = entries.indexOf(entry);
|
|
43
|
+
const marker = index === this.selected ? '›' : ' ';
|
|
44
|
+
const kind = entry.kind === 'steering' ? 'Steer' : 'Follow-up';
|
|
45
|
+
result.push(`${marker} ${kind}${entry.hasImages ? ' · images' : ''} ${entry.text.replace(/\s+/g, ' ')}`);
|
|
46
|
+
}
|
|
47
|
+
result.push('');
|
|
48
|
+
}
|
|
49
|
+
if (!entries.length)
|
|
50
|
+
result.push('Queue is empty. Plan, Bash, and deferred inputs are managed elsewhere.');
|
|
51
|
+
else
|
|
52
|
+
result.push('e edit in place · s send next · d remove · r return to composer');
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
async act(action) {
|
|
56
|
+
if (action === 'return') {
|
|
57
|
+
this.options.close();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const entries = this.entries();
|
|
61
|
+
if (!entries.length)
|
|
62
|
+
return;
|
|
63
|
+
const entry = entries[Math.min(this.selected, entries.length - 1)];
|
|
64
|
+
if (action === 'edit')
|
|
65
|
+
await this.options.editor.edit(entry.id);
|
|
66
|
+
if (action === 'send')
|
|
67
|
+
await this.options.editor.sendNext();
|
|
68
|
+
if (action === 'remove')
|
|
69
|
+
this.options.editor.remove(entry.id);
|
|
70
|
+
this.selected = Math.min(this.selected, Math.max(0, this.entries().length - 1));
|
|
71
|
+
this.options.changed();
|
|
72
|
+
}
|
|
73
|
+
handleInput(data) {
|
|
74
|
+
const count = this.entries().length;
|
|
75
|
+
if (matchesKey(data, 'escape'))
|
|
76
|
+
return false;
|
|
77
|
+
if (matchesKey(data, 'up')) {
|
|
78
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
79
|
+
this.options.changed();
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (matchesKey(data, 'down')) {
|
|
83
|
+
this.selected = Math.min(Math.max(0, count - 1), this.selected + 1);
|
|
84
|
+
this.options.changed();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const key = data.toLowerCase();
|
|
88
|
+
if (key === 'e') {
|
|
89
|
+
void this.act('edit');
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (key === 's') {
|
|
93
|
+
void this.act('send');
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (key === 'd' || key === 'delete' || key === '\\x7f') {
|
|
97
|
+
void this.act('remove');
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (key === 'r') {
|
|
101
|
+
void this.act('return');
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (matchesKey(data, 'enter')) {
|
|
105
|
+
void this.act('edit');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
handleMouse(event) {
|
|
111
|
+
if (!isPrimaryClick(event))
|
|
112
|
+
return;
|
|
113
|
+
const entries = this.entries();
|
|
114
|
+
const rendered = this.render(120, 100);
|
|
115
|
+
const line = Math.floor(event.y);
|
|
116
|
+
const displayed = [...entries.filter(entry => entry.kind === 'steering'), ...entries.filter(entry => entry.kind === 'followUp')];
|
|
117
|
+
let index = 0;
|
|
118
|
+
for (let row = 0; row < rendered.length; row++) {
|
|
119
|
+
if (rendered[row].startsWith('› ') || rendered[row].startsWith(' Steer') || rendered[row].startsWith(' Follow-up')) {
|
|
120
|
+
if (row === line) {
|
|
121
|
+
this.selected = entries.indexOf(displayed[index]);
|
|
122
|
+
this.options.changed();
|
|
123
|
+
return { handled: true, render: true };
|
|
124
|
+
}
|
|
125
|
+
index++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
2
|
import { FilterList } from "../list.js";
|
|
3
3
|
import { stripMouseReports } from "../mouseReport.js";
|
|
4
|
+
import { formatWorkingDuration } from "../workingStatus.js";
|
|
4
5
|
const ACTIVE = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
|
|
5
6
|
const payloadOf = (event) => event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
6
7
|
const stringOf = (value) => typeof value === 'string' ? value : undefined;
|
|
@@ -44,10 +45,18 @@ function eventDescription(event) {
|
|
|
44
45
|
return 'Recorded prompt and tool definitions';
|
|
45
46
|
case 'failed':
|
|
46
47
|
return stringOf(payload.message) ?? stringOf(payload.code) ?? 'run failed';
|
|
48
|
+
case 'heartbeat':
|
|
49
|
+
return heartbeatDescription(payload);
|
|
47
50
|
default:
|
|
48
51
|
return stringOf(payload.text) ?? stringOf(payload.name) ?? '';
|
|
49
52
|
}
|
|
50
53
|
}
|
|
54
|
+
/** A beat proves the process lives; what it was inside tells a wait from a wedge. */
|
|
55
|
+
function heartbeatDescription(payload) {
|
|
56
|
+
const tools = Array.isArray(payload.activeTools) ? payload.activeTools.filter((name) => typeof name === 'string') : [];
|
|
57
|
+
const oldest = numberOf(payload.oldestToolMs);
|
|
58
|
+
return tools.length ? `${tools.join(', ')}${oldest === undefined ? '' : ` ${formatWorkingDuration(oldest)}`}` : 'no tool running';
|
|
59
|
+
}
|
|
51
60
|
function usageLine(events) {
|
|
52
61
|
const event = [...events].reverse().find(item => item.type === 'usage.updated');
|
|
53
62
|
if (!event)
|
|
@@ -223,13 +232,34 @@ export class TrajectoryPage {
|
|
|
223
232
|
}));
|
|
224
233
|
}
|
|
225
234
|
eventRows() {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
// A long wait is dozens of identical beats; they fold into one row that shows how long it went on.
|
|
236
|
+
const rows = [];
|
|
237
|
+
let beats = [];
|
|
238
|
+
const flush = () => {
|
|
239
|
+
const last = beats.at(-1);
|
|
240
|
+
if (!last)
|
|
241
|
+
return;
|
|
242
|
+
const count = beats.length > 1 ? `×${beats.length} · ` : '';
|
|
243
|
+
const detail = count + safe(eventDescription(last));
|
|
244
|
+
rows.push({ id: String(last.sequence), label: `${timeOf(last.timestamp)} ${last.type}`, detail, searchText: `${last.timestamp} ${last.type} ${detail}`, data: last });
|
|
245
|
+
beats = [];
|
|
246
|
+
};
|
|
247
|
+
for (const event of this.events) {
|
|
248
|
+
if (event.type === 'heartbeat') {
|
|
249
|
+
beats.push(event);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
flush();
|
|
253
|
+
rows.push({
|
|
254
|
+
id: String(event.sequence),
|
|
255
|
+
label: `${timeOf(event.timestamp)} ${event.type}`,
|
|
256
|
+
detail: safe(eventDescription(event)),
|
|
257
|
+
searchText: `${event.timestamp} ${event.type} ${safe(eventDescription(event))} ${safe(JSON.stringify(payloadOf(event)))}`,
|
|
258
|
+
data: event
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
flush();
|
|
262
|
+
return rows;
|
|
233
263
|
}
|
|
234
264
|
eventJson(event) {
|
|
235
265
|
return JSON.stringify({ payload: event.payload, type: event.type, sequence: event.sequence, timestamp: event.timestamp }, null, 2);
|
|
@@ -2,6 +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 { withTransientRetry } from "../../pi/retryPolicy.js";
|
|
5
6
|
import { GoalController } from "../../sessions/goal.js";
|
|
6
7
|
import { terminalCredentialStore } from "../credentialStore.js";
|
|
7
8
|
import { normalModeTools, normalToolsForBranch, planModeTools, PLAN_TOOLS_ENTRY, QUESTION_TOOL_NAMES } from "./planMode.js";
|
|
@@ -214,7 +215,7 @@ export async function createTerminalRuntime(args, host) {
|
|
|
214
215
|
const factory = async ({ cwd: targetCwd, sessionManager, sessionStartEvent }) => {
|
|
215
216
|
if (realpathSync(targetCwd) !== cwd)
|
|
216
217
|
throw new Error('A native session cannot change this terminal working folder.');
|
|
217
|
-
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
218
|
+
const settingsManager = withTransientRetry(SettingsManager.create(cwd, agentDir));
|
|
218
219
|
const resourceLoader = new DefaultResourceLoader({
|
|
219
220
|
cwd, agentDir,
|
|
220
221
|
// The real settings still control models and tools; empty discovery settings prevent a
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
const BROWSER_BACKEND_ID = 'browser.backend';
|
|
2
|
+
const BROWSER_CONNECTION_ID = 'browser.connection';
|
|
1
3
|
const PRIVATE_ACCESS_ID = 'browser.allowPrivate';
|
|
2
|
-
const GROUP_ORDER = ['Theme', 'Display', 'Images', 'Thinking', 'Recovery', 'Editor', 'Queues', 'Commands', 'Terminal', 'Keyboard', 'Compaction', 'Network'];
|
|
4
|
+
const GROUP_ORDER = ['Theme', 'Display', 'Images', 'Thinking', 'Recovery', 'Editor', 'Queues', 'Commands', 'Terminal', 'Keyboard', 'Compaction', 'Connection', 'Network'];
|
|
3
5
|
export function createSettingsModel(host) {
|
|
4
6
|
let browserSettings = null;
|
|
7
|
+
let extensionSettings = null;
|
|
8
|
+
let pairingCode = null;
|
|
5
9
|
const booleans = (session) => {
|
|
6
10
|
const settings = session.settingsManager;
|
|
7
11
|
return [
|
|
@@ -42,24 +46,101 @@ export function createSettingsModel(host) {
|
|
|
42
46
|
const split = option.indexOf(':');
|
|
43
47
|
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] };
|
|
44
48
|
});
|
|
45
|
-
if (browserSettings)
|
|
49
|
+
if (browserSettings) {
|
|
50
|
+
result.push({
|
|
51
|
+
id: BROWSER_BACKEND_ID,
|
|
52
|
+
tab: 'Browser',
|
|
53
|
+
group: 'Connection',
|
|
54
|
+
label: 'Browser backend',
|
|
55
|
+
value: browserSettings.backend === 'brave' ? 'Brave profile' : 'Isolated browser',
|
|
56
|
+
description: 'Choose the browser the agent controls. Brave uses your existing signed-in profile and only sites you allow in the extension; Incognito is excluded. The extension does not request history or cookie permissions or export cookies. A disconnect never switches to the other backend.'
|
|
57
|
+
});
|
|
58
|
+
result.push({
|
|
59
|
+
id: BROWSER_CONNECTION_ID,
|
|
60
|
+
tab: 'Browser',
|
|
61
|
+
group: 'Connection',
|
|
62
|
+
label: 'Brave extension',
|
|
63
|
+
value: extensionSettings?.state ?? 'Checking',
|
|
64
|
+
description: pairingCode
|
|
65
|
+
? `Enter pairing code ${pairingCode} in the extension popup. It expires in five minutes.`
|
|
66
|
+
: extensionSettings?.state === 'connected'
|
|
67
|
+
? 'Connected to Brave. Use the in-page badge to pause or stop control.'
|
|
68
|
+
: extensionSettings?.paired
|
|
69
|
+
? 'The extension is paired and waiting. Open its toolbar popup to reconnect, or select this row to reset pairing.'
|
|
70
|
+
: 'Select this row to start the loopback bridge and get a one-time pairing code.'
|
|
71
|
+
});
|
|
46
72
|
result.push({
|
|
47
73
|
id: PRIVATE_ACCESS_ID,
|
|
48
74
|
tab: 'Browser',
|
|
49
75
|
group: 'Network',
|
|
50
76
|
label: 'Private address access',
|
|
51
77
|
value: browserSettings.allowPrivate ? 'On' : 'Off',
|
|
52
|
-
description: 'This device-wide setting
|
|
78
|
+
description: 'This device-wide setting lets agent browsing and annotation capture reach localhost and private networks.'
|
|
53
79
|
});
|
|
80
|
+
}
|
|
54
81
|
return result.sort((a, b) => GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group));
|
|
55
82
|
}
|
|
56
83
|
async function load() {
|
|
57
|
-
const config = await
|
|
58
|
-
|
|
84
|
+
const [config, extension] = await Promise.all([
|
|
85
|
+
host.control('terminal.browser.config'),
|
|
86
|
+
host.control('terminal.browser.extension.status')
|
|
87
|
+
]);
|
|
88
|
+
if (!config || typeof config.allowPrivate !== 'boolean' || !['isolated', 'brave'].includes(config.backend))
|
|
59
89
|
throw new Error('The Harness returned invalid browser settings.');
|
|
60
|
-
|
|
90
|
+
if (!extension || typeof extension.connected !== 'boolean' || typeof extension.paired !== 'boolean' || !['stopped', 'pairing', 'waiting', 'connected', 'paused'].includes(extension.state))
|
|
91
|
+
throw new Error('The Harness returned invalid Brave extension status.');
|
|
92
|
+
browserSettings = { backend: config.backend, allowPrivate: config.allowPrivate };
|
|
93
|
+
extensionSettings = extension;
|
|
94
|
+
if (extension.paired)
|
|
95
|
+
pairingCode = null;
|
|
61
96
|
}
|
|
62
97
|
async function apply(id) {
|
|
98
|
+
if (id === BROWSER_BACKEND_ID) {
|
|
99
|
+
if (!browserSettings)
|
|
100
|
+
return;
|
|
101
|
+
const selected = await host.ui.select('Browser backend', ['Isolated browser', 'Brave profile']);
|
|
102
|
+
const backend = selected === 'Brave profile' ? 'brave' : selected === 'Isolated browser' ? 'isolated' : null;
|
|
103
|
+
if (!backend || backend === browserSettings.backend)
|
|
104
|
+
return;
|
|
105
|
+
if (backend === 'brave' && !await host.ui.confirm('Use your Brave profile?', 'The agent can see and control normal tabs on sites you explicitly allow in the extension. Your signed-in pages may contain private information. The extension requires Brave’s debugger permission but does not request history or cookie permissions or export cookies. Incognito is never available, and a disconnect will not switch browsers automatically.'))
|
|
106
|
+
return;
|
|
107
|
+
const saved = await host.control('terminal.browser.configure', { backend });
|
|
108
|
+
if (!saved || saved.backend !== backend)
|
|
109
|
+
throw new Error('The Harness did not save the browser backend.');
|
|
110
|
+
browserSettings = { ...browserSettings, backend: saved.backend };
|
|
111
|
+
extensionSettings = await host.control('terminal.browser.extension.status');
|
|
112
|
+
host.refresh();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (id === BROWSER_CONNECTION_ID) {
|
|
116
|
+
if (!extensionSettings)
|
|
117
|
+
return;
|
|
118
|
+
let start = false;
|
|
119
|
+
if (extensionSettings.paired) {
|
|
120
|
+
const action = await host.ui.select('Brave extension', ['Keep waiting', 'Stop and pair again']);
|
|
121
|
+
if (action === 'Stop and pair again') {
|
|
122
|
+
await host.control('terminal.browser.extension.stop');
|
|
123
|
+
extensionSettings = await host.control('terminal.browser.extension.status');
|
|
124
|
+
start = true;
|
|
125
|
+
}
|
|
126
|
+
else if (action === 'Keep waiting') {
|
|
127
|
+
const result = await host.control('terminal.browser.extension.start');
|
|
128
|
+
extensionSettings = result.status;
|
|
129
|
+
pairingCode = result.pairingCode ?? null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
start = true;
|
|
134
|
+
if (start) {
|
|
135
|
+
const result = await host.control('terminal.browser.extension.start');
|
|
136
|
+
extensionSettings = result.status;
|
|
137
|
+
pairingCode = result.pairingCode ?? null;
|
|
138
|
+
if (pairingCode)
|
|
139
|
+
host.ui.notify(`Pair the Brave extension with code ${pairingCode}.`, 'info');
|
|
140
|
+
}
|
|
141
|
+
host.refresh();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
63
144
|
if (id === PRIVATE_ACCESS_ID) {
|
|
64
145
|
if (!browserSettings)
|
|
65
146
|
return;
|
|
@@ -69,7 +150,7 @@ export function createSettingsModel(host) {
|
|
|
69
150
|
const saved = await host.control('terminal.browser.configure', { allowPrivate });
|
|
70
151
|
if (!saved || typeof saved.allowPrivate !== 'boolean')
|
|
71
152
|
throw new Error('The Harness returned invalid browser settings.');
|
|
72
|
-
browserSettings = { allowPrivate: saved.allowPrivate };
|
|
153
|
+
browserSettings = { ...browserSettings, allowPrivate: saved.allowPrivate };
|
|
73
154
|
host.refresh();
|
|
74
155
|
return;
|
|
75
156
|
}
|
|
@@ -19,7 +19,7 @@ export function askUserTool(run) {
|
|
|
19
19
|
name: ASK_USER,
|
|
20
20
|
label: 'Ask the user',
|
|
21
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
|
|
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 ASCII wireframe on a fixed 80-column canvas. Use the full canvas to show the affected element and relevant neighboring UI with readable labels, controls and spacing; keep rows aligned but omit trailing spaces where convenient. 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 the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked. Never use it to ask for a login, a secret or a credential: give the exact command in the reply instead.',
|
|
23
23
|
parameters: (Type) => Type.Object({
|
|
24
24
|
questions: Type.Array(Type.Object({
|
|
25
25
|
summary: Type.String({ description: 'A concise label of one or two words' }),
|
|
@@ -29,7 +29,7 @@ export function askUserTool(run) {
|
|
|
29
29
|
label: Type.String({ description: 'A short choice; do not add a recommendation suffix to the label' }),
|
|
30
30
|
description: Type.String({ description: 'Required. Briefly explain this choice in 25 words or fewer.' }),
|
|
31
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.' }))
|
|
32
|
+
sketch: Type.Optional(Type.String({ minLength: 1, maxLength: QUESTION_SKETCH_MAX_LENGTH, pattern: QUESTION_SKETCH_PATTERN, description: 'Literal ASCII UI wireframe on a fixed 80-column canvas: 1–24 LF-separated lines, at most 80 printable ASCII characters per line; shorter rows are padded in the terminal preview. 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.' }))
|
|
33
33
|
}), {
|
|
34
34
|
minItems: 1,
|
|
35
35
|
maxItems: MAX_OPTIONS,
|
|
@@ -2,7 +2,8 @@ import type { SecretStore } from "../accounts/secrets.js";
|
|
|
2
2
|
import type { CallerContext } from "../core/caller.js";
|
|
3
3
|
import type { PluginDescriptor } from "../plugins/types.js";
|
|
4
4
|
import type { ActiveRun } from "../sessions/registry.js";
|
|
5
|
-
import type { ToolProvider, ToolSpec } from "../tools/types.js";
|
|
5
|
+
import type { ToolProvider, ToolSession, ToolSpec } from "../tools/types.js";
|
|
6
|
+
import type { BrowserExtensionStart, BrowserExtensionStatus } from "../browser/extension/bridge.js";
|
|
6
7
|
import type { WebRequest } from "../web/safeRequest.js";
|
|
7
8
|
import { type BrowserConfig } from "../browser/config.js";
|
|
8
9
|
import { type BrowserPartOptions } from "../browser/index.js";
|
|
@@ -27,6 +28,10 @@ export interface WorldOptions {
|
|
|
27
28
|
request?: WebRequest;
|
|
28
29
|
/** Replaces the browser's launcher and address check in tests. */
|
|
29
30
|
browser?: Omit<BrowserPartOptions, 'stateDir'> | false;
|
|
31
|
+
/** Enables the loopback extension bridge even when the host supplies its own isolated browser. */
|
|
32
|
+
browserExtension?: {
|
|
33
|
+
port?: number;
|
|
34
|
+
};
|
|
30
35
|
}
|
|
31
36
|
/** A family of world tools, switched as one plugin. */
|
|
32
37
|
export interface WorldPart {
|
|
@@ -59,7 +64,12 @@ export declare class World {
|
|
|
59
64
|
/** Saves or forgets a search service's key. */
|
|
60
65
|
setSearchKey(providerId: string, key: string | null): Promise<void>;
|
|
61
66
|
browserConfig(): BrowserConfig;
|
|
62
|
-
configureBrowser(change: Partial<BrowserConfig>): BrowserConfig
|
|
67
|
+
configureBrowser(change: Partial<BrowserConfig>): Promise<BrowserConfig>;
|
|
68
|
+
browserExtensionStatus(): BrowserExtensionStatus;
|
|
69
|
+
browserExtensionStart(): Promise<BrowserExtensionStart>;
|
|
70
|
+
browserExtensionStop(): Promise<BrowserExtensionStatus>;
|
|
71
|
+
/** Browser tools for a host provider that supplies its own tool registration surface. */
|
|
72
|
+
browserToolsForSession(session: ToolSession): ToolSpec[];
|
|
63
73
|
release(sessionKey: string): Promise<void>;
|
|
64
74
|
close(): Promise<void>;
|
|
65
75
|
}
|
package/dist/sdk/world/world.js
CHANGED
|
@@ -15,8 +15,13 @@ export class World {
|
|
|
15
15
|
browserEngine;
|
|
16
16
|
constructor(options) {
|
|
17
17
|
this.options = options;
|
|
18
|
-
|
|
19
|
-
this.
|
|
18
|
+
const disabled = options.browser === false;
|
|
19
|
+
this.browserEngine = disabled && !options.browserExtension ? null : createBrowserEngine({
|
|
20
|
+
stateDir: options.stateDir,
|
|
21
|
+
...(disabled ? {} : options.browser),
|
|
22
|
+
extensionPort: options.browserExtension?.port
|
|
23
|
+
});
|
|
24
|
+
this.parts.push(...BUILTIN_PLUGIN_REGISTRY.worldParts({ options, setting: (pluginId, key) => this.setting(pluginId, key), browserEngine: disabled ? null : this.browserEngine }));
|
|
20
25
|
}
|
|
21
26
|
add(part) {
|
|
22
27
|
this.parts.push(part);
|
|
@@ -67,8 +72,41 @@ export class World {
|
|
|
67
72
|
browserConfig() {
|
|
68
73
|
return readBrowserConfig(this.options.stateDir);
|
|
69
74
|
}
|
|
70
|
-
configureBrowser(change) {
|
|
71
|
-
|
|
75
|
+
async configureBrowser(change) {
|
|
76
|
+
const before = this.browserConfig();
|
|
77
|
+
const next = writeBrowserConfig(this.options.stateDir, change);
|
|
78
|
+
if (before.backend === 'brave' && next.backend === 'isolated')
|
|
79
|
+
await this.browserEngine?.extension.stop();
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
browserExtensionStatus() {
|
|
83
|
+
if (!this.browserEngine)
|
|
84
|
+
throw invalid('The Brave browser bridge is not enabled by this host.');
|
|
85
|
+
return this.browserEngine.extension.status();
|
|
86
|
+
}
|
|
87
|
+
browserExtensionStart() {
|
|
88
|
+
if (!this.browserEngine)
|
|
89
|
+
return Promise.reject(invalid('The Brave browser bridge is not enabled by this host.'));
|
|
90
|
+
return this.browserEngine.extension.start();
|
|
91
|
+
}
|
|
92
|
+
browserExtensionStop() {
|
|
93
|
+
if (!this.browserEngine)
|
|
94
|
+
return Promise.reject(invalid('The Brave browser bridge is not enabled by this host.'));
|
|
95
|
+
return this.browserEngine.extension.stop();
|
|
96
|
+
}
|
|
97
|
+
/** Browser tools for a host provider that supplies its own tool registration surface. */
|
|
98
|
+
browserToolsForSession(session) {
|
|
99
|
+
if (!this.browserEngine)
|
|
100
|
+
return [];
|
|
101
|
+
const record = findRun(this.options.root(), session.run.runId);
|
|
102
|
+
return this.browserEngine.toolsForSession(session, {
|
|
103
|
+
run: session.run,
|
|
104
|
+
caller: session.caller,
|
|
105
|
+
attended: !UNATTENDED.has(record?.trigger ?? ''),
|
|
106
|
+
subagent: session.depth > 0 || record?.relation === 'subagent',
|
|
107
|
+
sessionKey: session.run.sessionId,
|
|
108
|
+
cwd: session.cwd
|
|
109
|
+
});
|
|
72
110
|
}
|
|
73
111
|
async release(sessionKey) {
|
|
74
112
|
await Promise.all([...this.parts.map((part) => part.release?.(sessionKey)), this.browserEngine?.release(sessionKey)]);
|