@xingular/harness 1.2.0 → 1.3.1-unstable.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 +63 -0
- package/README.md +26 -17
- package/builtin/skills/plugin-authoring/SKILL.md +34 -0
- package/dist/cli.js +7362 -6239
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +16 -4
- package/dist/sdk/agents/profiles.js +75 -12
- package/dist/sdk/annotations/bridge.d.ts +2 -0
- package/dist/sdk/annotations/bridge.js +4 -0
- package/dist/sdk/annotations/capture.d.ts +44 -2
- package/dist/sdk/annotations/capture.js +104 -3
- package/dist/sdk/annotations/delivery.d.ts +24 -17
- package/dist/sdk/annotations/delivery.js +82 -49
- package/dist/sdk/annotations/inspect.js +7 -6
- package/dist/sdk/annotations/pageScript.js +4 -4
- package/dist/sdk/annotations/part.d.ts +0 -2
- package/dist/sdk/annotations/part.js +4 -33
- package/dist/sdk/annotations/queue.d.ts +2 -2
- package/dist/sdk/annotations/queue.js +12 -7
- package/dist/sdk/annotations/serverMethods.js +11 -7
- package/dist/sdk/annotations/store.d.ts +13 -21
- package/dist/sdk/annotations/store.js +47 -37
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/list.js +2 -0
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/links.d.ts +51 -0
- package/dist/sdk/core/links.js +155 -0
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/harness.d.ts +5 -0
- package/dist/sdk/harness.js +11 -6
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/plugins/builtin.js +14 -9
- package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
- package/dist/sdk/plugins/builtinRegistry.js +9 -3
- package/dist/sdk/plugins/discover.d.ts +7 -1
- package/dist/sdk/plugins/discover.js +41 -2
- package/dist/sdk/plugins/identity.js +1 -1
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +3 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +21 -4
- package/dist/sdk/plugins/plugins.d.ts +2 -0
- package/dist/sdk/plugins/plugins.js +1 -0
- package/dist/sdk/plugins/proposals.d.ts +17 -0
- package/dist/sdk/plugins/proposals.js +147 -0
- package/dist/sdk/plugins/types.d.ts +1 -1
- package/dist/sdk/plugins/types.js +1 -1
- package/dist/sdk/prompt/compile.d.ts +13 -4
- package/dist/sdk/prompt/compile.js +46 -20
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/server/methods.js +14 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessionState.d.ts +13 -0
- package/dist/sdk/sessions/sessionState.js +31 -0
- package/dist/sdk/sessions/sessions.d.ts +7 -0
- package/dist/sdk/sessions/sessions.js +73 -15
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +195 -55
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
- package/dist/sdk/terminal/opentui/theme.js +2 -0
- package/dist/sdk/terminal/piExtension.js +98 -15
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +16 -0
- package/dist/sdk/terminal/terminals.js +130 -26
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
- package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
- package/dist/sdk/terminal/tui/annotateSend.js +40 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +16 -11
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +12 -2
- package/dist/sdk/terminal/tui/dialogs.js +1 -0
- package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
- package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
- package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
- package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
- package/dist/sdk/tools/defaultSkills.d.ts +4 -0
- package/dist/sdk/tools/defaultSkills.js +20 -0
- package/dist/sdk/tools/definitions.d.ts +3 -1
- package/dist/sdk/tools/definitions.js +17 -1
- package/dist/sdk/tools/dispatch.js +12 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/sessionName.d.ts +9 -0
- package/dist/sdk/tools/sessionName.js +58 -11
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/sdk/world/world.d.ts +0 -2
- package/dist/terminal-opentui.js +3331 -1584
- package/dist/terminal-pi.js +421 -133
- package/docs/README.md +3 -0
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +73 -36
- package/docs/architecture/03-variants.md +1 -3
- package/docs/architecture/04-prompt.md +24 -13
- package/docs/architecture/06-plugins-and-capabilities.md +46 -10
- package/docs/architecture/07-tools-and-gate.md +49 -8
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +68 -16
- package/docs/architecture/11-terminal.md +94 -45
- package/docs/architecture/12-storage.md +1 -0
- package/docs/architecture/13-world-tools.md +8 -6
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +20 -18
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
- package/docs/architecture/proposals/README.md +11 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
- package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
- package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
- package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +16 -13
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/1.3.1-unstable.1.md +8 -0
- package/docs/releases/README.md +9 -6
- package/docs/terminal-tui.md +127 -65
- package/package.json +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
- package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
1
|
+
import { Input, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import { FilterList } from "../list.js";
|
|
3
|
-
|
|
3
|
+
import { normalizeAddress, normalizeDevTools } from "../annotateCommand.js";
|
|
4
|
+
import { describeSend, sendUnsent, unsent } from "../annotateSend.js";
|
|
5
|
+
const TABS = [{ id: 'annotate', title: 'Annotate' }, { id: 'all', title: 'All annotations' }];
|
|
6
|
+
const FIELDS = {
|
|
7
|
+
address: { label: 'Page address', placeholder: 'http://localhost:5173' },
|
|
8
|
+
port: { label: 'DevTools port', placeholder: '9222' },
|
|
9
|
+
path: { label: 'App to start', placeholder: '/Applications/MyApp.app' }
|
|
10
|
+
};
|
|
4
11
|
export const ANNOTATE_ACTIONS = {
|
|
5
12
|
reveal: 'Show it in the window',
|
|
6
|
-
send: 'Send
|
|
7
|
-
confirm: 'Confirm
|
|
8
|
-
queue: 'Ask the agent to fix it (add to queue)',
|
|
9
|
-
dequeue: 'Take it out of the queue',
|
|
13
|
+
send: 'Send it now',
|
|
14
|
+
confirm: 'Confirm and send it (it came from the page)',
|
|
10
15
|
edit: 'Edit the comment',
|
|
11
16
|
resolve: 'Resolve',
|
|
17
|
+
reopen: 'Reopen',
|
|
18
|
+
share: 'Share it with this session',
|
|
19
|
+
unshare: 'Stop sharing it with this session',
|
|
12
20
|
delete: 'Delete'
|
|
13
21
|
};
|
|
14
22
|
const clip = (text, size) => (text.length > size ? `${text.slice(0, size - 1)}…` : text);
|
|
@@ -19,45 +27,84 @@ export function describeTarget(target) {
|
|
|
19
27
|
return `<${target.tag}> ${clip(target.name || target.text || target.selector, 40)}`;
|
|
20
28
|
return 'whole page';
|
|
21
29
|
}
|
|
22
|
-
/** Which
|
|
30
|
+
/** Which group of the Annotate tab a note belongs in, for this session; null when it is not shown there. */
|
|
23
31
|
export function section(note, sessionId) {
|
|
24
|
-
if (note.status === 'resolved')
|
|
32
|
+
if (note.status === 'resolved' || !note.delivery || note.delivery.sessionId !== sessionId)
|
|
25
33
|
return null;
|
|
26
|
-
|
|
27
|
-
if (delivery && delivery.sessionId === sessionId)
|
|
28
|
-
return delivery.state === 'queued' ? 'Queue' : 'Sent and fixed';
|
|
29
|
-
if (!delivery)
|
|
30
|
-
return 'Saved';
|
|
31
|
-
return null;
|
|
34
|
+
return note.delivery.state === 'pending' ? 'Unsent' : 'Sent';
|
|
32
35
|
}
|
|
33
36
|
export class AnnotatePage {
|
|
34
37
|
options;
|
|
35
38
|
presentation = 'fullscreen';
|
|
39
|
+
tab;
|
|
40
|
+
view = 'notes';
|
|
41
|
+
field = null;
|
|
36
42
|
notes;
|
|
37
43
|
sessionId = null;
|
|
38
44
|
target = null;
|
|
45
|
+
apps = null;
|
|
39
46
|
bridgeSnippet = null;
|
|
40
47
|
error;
|
|
41
48
|
busy = false;
|
|
42
49
|
disposed = false;
|
|
43
50
|
polling = false;
|
|
51
|
+
focusedValue = false;
|
|
44
52
|
timer = null;
|
|
45
53
|
refreshes = 0;
|
|
46
|
-
|
|
54
|
+
listTop = 0;
|
|
55
|
+
noteList;
|
|
56
|
+
appList;
|
|
57
|
+
allList;
|
|
47
58
|
constructor(options) {
|
|
48
59
|
this.options = options;
|
|
49
|
-
this.
|
|
50
|
-
|
|
60
|
+
this.tab = options.initialTab ?? 'annotate';
|
|
61
|
+
this.noteList = new FilterList({
|
|
62
|
+
theme: options.theme, search: 'never', noun: ['annotation', 'annotations'],
|
|
63
|
+
empty: 'No annotations yet. Press i and click a component in the window.',
|
|
64
|
+
onActivate: row => { if (row.data)
|
|
65
|
+
void this.noteMenu(row.data.note, 'annotate'); }
|
|
66
|
+
});
|
|
67
|
+
this.appList = new FilterList({
|
|
68
|
+
theme: options.theme, search: 'never', noun: ['choice', 'choices'],
|
|
51
69
|
onActivate: row => { if (row.data)
|
|
52
|
-
void this.
|
|
70
|
+
void this.chooseApp(row.data); }
|
|
53
71
|
});
|
|
72
|
+
this.allList = new FilterList({
|
|
73
|
+
theme: options.theme, search: 'always', noun: ['annotation', 'annotations'], empty: 'No web annotations yet',
|
|
74
|
+
onActivate: row => { if (row.data)
|
|
75
|
+
void this.noteMenu(row.data.note, 'all'); }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
get focused() { return this.focusedValue; }
|
|
79
|
+
set focused(value) {
|
|
80
|
+
this.focusedValue = value;
|
|
81
|
+
this.syncFocus();
|
|
82
|
+
}
|
|
83
|
+
syncFocus() {
|
|
84
|
+
const value = this.focusedValue;
|
|
85
|
+
this.noteList.focused = value && this.tab === 'annotate' && this.view === 'notes' && !this.field;
|
|
86
|
+
this.appList.focused = value && this.tab === 'annotate' && this.view === 'apps' && !this.field;
|
|
87
|
+
this.allList.focused = value && this.tab === 'all';
|
|
88
|
+
if (this.field)
|
|
89
|
+
this.field.input.focused = value && this.tab === 'annotate';
|
|
54
90
|
}
|
|
55
|
-
get focused() { return this.list.focused; }
|
|
56
|
-
set focused(value) { this.list.focused = value; }
|
|
57
91
|
tabs() { return TABS; }
|
|
58
|
-
activeTab() { return
|
|
92
|
+
activeTab() { return this.tab; }
|
|
93
|
+
selectTab(id) {
|
|
94
|
+
if (id !== 'annotate' && id !== 'all')
|
|
95
|
+
return;
|
|
96
|
+
this.tab = id;
|
|
97
|
+
this.syncFocus();
|
|
98
|
+
this.options.changed();
|
|
99
|
+
}
|
|
59
100
|
hint() {
|
|
60
|
-
|
|
101
|
+
if (this.tab === 'all')
|
|
102
|
+
return 'Enter choose · type to search · Tab Annotate · Esc close';
|
|
103
|
+
if (this.field)
|
|
104
|
+
return 'Enter go · Esc cancel';
|
|
105
|
+
if (this.view === 'apps')
|
|
106
|
+
return 'Enter choose · r look again · Esc back';
|
|
107
|
+
return `${this.target?.inspecting ? 'i stop inspecting' : 'i inspect'} · s send unsent · o page · a app · t window · Enter note · Tab all · Esc close`;
|
|
61
108
|
}
|
|
62
109
|
activate() {
|
|
63
110
|
void this.start();
|
|
@@ -70,32 +117,158 @@ export class AnnotatePage {
|
|
|
70
117
|
void this.options.call('terminal.annotations.stopInspect').catch(() => undefined);
|
|
71
118
|
}
|
|
72
119
|
async start() {
|
|
73
|
-
const { call } = this.options;
|
|
74
120
|
const start = this.options.start;
|
|
75
|
-
await this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return start.kind === 'bridge' && !this.target?.windows.length ? 'Load the bridge script in the app (shown at the top), then reload the app.' : undefined;
|
|
93
|
-
});
|
|
121
|
+
await this.reload();
|
|
122
|
+
if (start.kind === 'web' && !start.url) {
|
|
123
|
+
// The capture target outlives the page: reopening returns to the window still open.
|
|
124
|
+
if (!await this.restore())
|
|
125
|
+
this.openField('address');
|
|
126
|
+
}
|
|
127
|
+
else if (start.kind === 'apps')
|
|
128
|
+
await this.showApps();
|
|
129
|
+
else
|
|
130
|
+
await this.attempt(async () => {
|
|
131
|
+
if (start.kind === 'web')
|
|
132
|
+
await this.openPage(start.url);
|
|
133
|
+
else if (start.kind === 'cdp')
|
|
134
|
+
await this.attachApp(start.endpoint, start.name);
|
|
135
|
+
else if (start.kind === 'bridge')
|
|
136
|
+
return this.attachBridge(start.name);
|
|
137
|
+
});
|
|
94
138
|
this.schedule();
|
|
95
139
|
}
|
|
140
|
+
/** Shows the target that is still open, with the inspector on again; false when no window is open. */
|
|
141
|
+
async restore() {
|
|
142
|
+
try {
|
|
143
|
+
await this.refreshTarget();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const target = this.target;
|
|
149
|
+
if (!target || (target.kind === 'web' && !target.windows.length))
|
|
150
|
+
return false;
|
|
151
|
+
this.view = 'notes';
|
|
152
|
+
this.bridgeSnippet = target.bridge?.snippet ?? null;
|
|
153
|
+
this.syncFocus();
|
|
154
|
+
if (target.windows.length)
|
|
155
|
+
await this.toggleInspect(true).catch(() => undefined);
|
|
156
|
+
this.options.changed();
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
// ---- The target: a page, an app over its DevTools port, or the page bridge ----------------------
|
|
96
160
|
async refreshTarget() {
|
|
97
161
|
this.target = await this.options.call('terminal.annotations.target');
|
|
98
162
|
}
|
|
163
|
+
async openPage(url) {
|
|
164
|
+
await this.options.call('terminal.annotations.open', { url });
|
|
165
|
+
await this.refreshTarget();
|
|
166
|
+
await this.toggleInspect(true);
|
|
167
|
+
}
|
|
168
|
+
async attachApp(endpoint, name) {
|
|
169
|
+
await this.options.call('terminal.annotations.attach', { kind: 'cdp', endpoint, ...(name ? { name } : {}) });
|
|
170
|
+
await this.refreshTarget();
|
|
171
|
+
this.view = 'notes';
|
|
172
|
+
this.syncFocus();
|
|
173
|
+
await this.toggleInspect(true);
|
|
174
|
+
}
|
|
175
|
+
async attachBridge(name) {
|
|
176
|
+
const attached = await this.options.call('terminal.annotations.attach', { kind: 'bridge', ...(name ? { name } : {}) });
|
|
177
|
+
this.bridgeSnippet = attached.bridge?.snippet ?? null;
|
|
178
|
+
await this.refreshTarget();
|
|
179
|
+
this.view = 'notes';
|
|
180
|
+
this.syncFocus();
|
|
181
|
+
if (this.target?.windows.length) {
|
|
182
|
+
await this.toggleInspect(true);
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return 'Load the bridge script in the app (shown at the top), then reload the app.';
|
|
186
|
+
}
|
|
187
|
+
openField(kind) {
|
|
188
|
+
const input = new Input({ prompt: '', placeholder: FIELDS[kind].placeholder, placeholderStyle: text => this.options.theme().fg('dim', text) });
|
|
189
|
+
this.field = { kind, input };
|
|
190
|
+
this.syncFocus();
|
|
191
|
+
this.options.changed();
|
|
192
|
+
}
|
|
193
|
+
closeField() {
|
|
194
|
+
this.field = null;
|
|
195
|
+
this.syncFocus();
|
|
196
|
+
this.options.changed();
|
|
197
|
+
}
|
|
198
|
+
submitField() {
|
|
199
|
+
const field = this.field;
|
|
200
|
+
if (!field)
|
|
201
|
+
return;
|
|
202
|
+
const value = field.input.getValue().trim();
|
|
203
|
+
if (!value)
|
|
204
|
+
return;
|
|
205
|
+
this.closeField();
|
|
206
|
+
void this.attempt(async () => {
|
|
207
|
+
if (field.kind === 'address')
|
|
208
|
+
return this.openPage(normalizeAddress(value));
|
|
209
|
+
if (field.kind === 'port')
|
|
210
|
+
return this.attachApp(normalizeDevTools(value));
|
|
211
|
+
const launched = await this.options.call('terminal.annotations.launch', { path: value });
|
|
212
|
+
await this.refreshTarget();
|
|
213
|
+
this.view = 'notes';
|
|
214
|
+
this.syncFocus();
|
|
215
|
+
await this.toggleInspect(true);
|
|
216
|
+
return `Started ${launched.app?.name ?? 'the app'} and attached to its windows.`;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async showApps() {
|
|
220
|
+
this.view = 'apps';
|
|
221
|
+
this.syncFocus();
|
|
222
|
+
this.appList.setRows(this.appRows());
|
|
223
|
+
this.options.changed();
|
|
224
|
+
try {
|
|
225
|
+
this.apps = (await this.options.call('terminal.annotations.discover')).apps;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
this.apps = [];
|
|
229
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
230
|
+
}
|
|
231
|
+
if (this.disposed)
|
|
232
|
+
return;
|
|
233
|
+
this.appList.setRows(this.appRows());
|
|
234
|
+
// The first app found is the likely choice.
|
|
235
|
+
const first = this.apps[0];
|
|
236
|
+
this.appList.select(first ? `app:${first.port}` : 'launch');
|
|
237
|
+
this.options.changed();
|
|
238
|
+
}
|
|
239
|
+
appRows() {
|
|
240
|
+
const rows = [];
|
|
241
|
+
const group = 'Apps with a DevTools port on this computer';
|
|
242
|
+
if (!this.apps)
|
|
243
|
+
rows.push({ id: 'looking', label: 'Looking for apps…', group, disabled: true });
|
|
244
|
+
else if (!this.apps.length)
|
|
245
|
+
rows.push({ id: 'none', label: 'None found on ports 9222–9230', group, detail: 'start one below', disabled: true, tone: 'dim' });
|
|
246
|
+
for (const app of this.apps ?? []) {
|
|
247
|
+
rows.push({
|
|
248
|
+
id: `app:${app.port}`, label: app.name, group, tone: 'accent', data: { kind: 'app', app },
|
|
249
|
+
detail: `${app.browser} · port ${app.port} · ${app.windows} window${app.windows === 1 ? '' : 's'}`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
rows.push({ id: 'launch', label: 'Start an app with a DevTools port…', group: 'Other ways', detail: 'Electron or Chromium app', data: { kind: 'launch' } }, { id: 'port', label: 'Type a DevTools port…', group: 'Other ways', detail: 'an app started with --remote-debugging-port', data: { kind: 'port' } }, { id: 'bridge', label: 'Use the page bridge…', group: 'Other ways', detail: 'a web view with no DevTools port', data: { kind: 'bridge' } }, { id: 'web', label: 'Annotate a web page instead…', group: 'Other ways', detail: 'opens a browser window', data: { kind: 'web' } });
|
|
253
|
+
return rows;
|
|
254
|
+
}
|
|
255
|
+
chooseApp(row) {
|
|
256
|
+
if (row.kind === 'port')
|
|
257
|
+
return this.openField('port');
|
|
258
|
+
if (row.kind === 'launch')
|
|
259
|
+
return this.openField('path');
|
|
260
|
+
if (row.kind === 'web') {
|
|
261
|
+
this.view = 'notes';
|
|
262
|
+
return this.openField('address');
|
|
263
|
+
}
|
|
264
|
+
if (row.kind === 'bridge')
|
|
265
|
+
return this.attempt(() => this.attachBridge());
|
|
266
|
+
return this.attempt(async () => {
|
|
267
|
+
await this.attachApp(row.app.endpoint, row.app.name);
|
|
268
|
+
return `Attached to ${row.app.name}.`;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// ---- Notes ------------------------------------------------------------------------------------
|
|
99
272
|
async reload() {
|
|
100
273
|
try {
|
|
101
274
|
const result = await this.options.call('terminal.annotations.list');
|
|
@@ -108,30 +281,38 @@ export class AnnotatePage {
|
|
|
108
281
|
}
|
|
109
282
|
if (this.disposed)
|
|
110
283
|
return;
|
|
111
|
-
this.
|
|
284
|
+
this.noteList.setRows(this.rows());
|
|
285
|
+
this.allList.setRows(this.allRows());
|
|
112
286
|
this.options.changed();
|
|
113
287
|
}
|
|
288
|
+
/** The Annotate tab: this session's unsent and sent annotations. */
|
|
114
289
|
rows() {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return rows;
|
|
290
|
+
const waiting = unsent(this.notes ?? [], this.sessionId);
|
|
291
|
+
const sent = (this.notes ?? []).filter(note => section(note, this.sessionId) === 'Sent');
|
|
292
|
+
const row = (note, group) => {
|
|
293
|
+
const delivery = note.delivery;
|
|
294
|
+
const state = delivery.state === 'sent' ? `sent · ${describeTarget(note.target)}`
|
|
295
|
+
: `${describeTarget(note.target)}${delivery.confirm ? ' · confirm before sending' : delivery.autoSend ? ' · sends when the agent finishes' : ''}`;
|
|
296
|
+
return {
|
|
297
|
+
id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group, detail: state,
|
|
298
|
+
tone: delivery.state === 'sent' ? 'muted' : delivery.confirm ? 'warning' : 'accent',
|
|
299
|
+
data: { kind: 'note', note }
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
return [...waiting.map(note => row(note, `Unsent (${waiting.length})`)), ...sent.map(note => row(note, 'Sent'))];
|
|
132
303
|
}
|
|
133
|
-
|
|
134
|
-
return
|
|
304
|
+
shared(note) {
|
|
305
|
+
return note.grants.some(grant => grant.principal.sessionId && grant.principal.sessionId === this.sessionId);
|
|
306
|
+
}
|
|
307
|
+
/** The All annotations tab: every saved annotation. */
|
|
308
|
+
allRows() {
|
|
309
|
+
return (this.notes ?? []).map(note => ({
|
|
310
|
+
id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group: 'Annotations',
|
|
311
|
+
detail: `${describeTarget(note.target)} · ${note.status}${this.shared(note) ? ' · shared here' : ''}`,
|
|
312
|
+
tone: note.status === 'resolved' ? 'dim' : this.shared(note) ? 'success' : 'muted',
|
|
313
|
+
searchText: `${note.body} ${note.source.url} ${note.source.title} ${note.tags.join(' ')}`,
|
|
314
|
+
data: { kind: 'note', note }
|
|
315
|
+
}));
|
|
135
316
|
}
|
|
136
317
|
/** Polls the inspector for the person's next comment, and the notes every few seconds. */
|
|
137
318
|
schedule() {
|
|
@@ -184,9 +365,12 @@ export class AnnotatePage {
|
|
|
184
365
|
await this.refreshTarget();
|
|
185
366
|
this.options.changed();
|
|
186
367
|
}
|
|
368
|
+
/** Runs one action at a time; a key pressed meanwhile is answered, never dropped silently. */
|
|
187
369
|
async attempt(work) {
|
|
188
|
-
if (this.busy)
|
|
370
|
+
if (this.busy) {
|
|
371
|
+
this.options.ui.notify('Still working on the last step. Try again in a moment.', 'info');
|
|
189
372
|
return;
|
|
373
|
+
}
|
|
190
374
|
this.busy = true;
|
|
191
375
|
try {
|
|
192
376
|
const message = await work();
|
|
@@ -202,21 +386,8 @@ export class AnnotatePage {
|
|
|
202
386
|
}
|
|
203
387
|
await this.reload();
|
|
204
388
|
}
|
|
205
|
-
|
|
206
|
-
return this.attempt(
|
|
207
|
-
const waiting = this.queued();
|
|
208
|
-
if (!waiting.length)
|
|
209
|
-
return 'The queue is empty.';
|
|
210
|
-
const unconfirmed = waiting.filter(note => note.delivery?.confirm);
|
|
211
|
-
if (unconfirmed.length) {
|
|
212
|
-
if (!await this.options.ui.confirm(`Send ${waiting.length} note(s) to the agent?`, `${unconfirmed.length} came from the page's own scripts. Check that you wrote them.`))
|
|
213
|
-
return;
|
|
214
|
-
for (const note of unconfirmed)
|
|
215
|
-
await this.options.call('terminal.annotations.confirm', { id: note.id, expectedVersion: note.version });
|
|
216
|
-
}
|
|
217
|
-
const result = await this.options.call('terminal.annotations.send');
|
|
218
|
-
return result.state === 'sent' ? `Sent ${result.ids.length} note(s) to the agent.` : result.state === 'busy' ? 'The agent is working; the queue goes out when it finishes.' : 'Nothing was sent.';
|
|
219
|
-
});
|
|
389
|
+
sendAll() {
|
|
390
|
+
return this.attempt(() => sendUnsent(this.options.call, this.options.ui));
|
|
220
391
|
}
|
|
221
392
|
chooseWindow() {
|
|
222
393
|
return this.attempt(async () => {
|
|
@@ -233,16 +404,24 @@ export class AnnotatePage {
|
|
|
233
404
|
await this.refreshTarget();
|
|
234
405
|
});
|
|
235
406
|
}
|
|
236
|
-
async noteMenu(note) {
|
|
407
|
+
async noteMenu(note, from) {
|
|
237
408
|
const { call, ui } = this.options;
|
|
238
|
-
const
|
|
239
|
-
const choices =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
409
|
+
const pendingHere = section(note, this.sessionId) === 'Unsent';
|
|
410
|
+
const choices = from === 'annotate'
|
|
411
|
+
? [
|
|
412
|
+
ANNOTATE_ACTIONS.reveal,
|
|
413
|
+
...(pendingHere ? [note.delivery?.confirm ? ANNOTATE_ACTIONS.confirm : ANNOTATE_ACTIONS.send] : []),
|
|
414
|
+
ANNOTATE_ACTIONS.edit,
|
|
415
|
+
ANNOTATE_ACTIONS.resolve,
|
|
416
|
+
ANNOTATE_ACTIONS.delete
|
|
417
|
+
]
|
|
418
|
+
: [
|
|
419
|
+
ANNOTATE_ACTIONS.reveal,
|
|
420
|
+
ANNOTATE_ACTIONS.edit,
|
|
421
|
+
note.status === 'open' ? ANNOTATE_ACTIONS.resolve : ANNOTATE_ACTIONS.reopen,
|
|
422
|
+
...(note.status === 'open' ? [this.shared(note) ? ANNOTATE_ACTIONS.unshare : ANNOTATE_ACTIONS.share] : []),
|
|
423
|
+
ANNOTATE_ACTIONS.delete
|
|
424
|
+
];
|
|
246
425
|
const choice = await ui.select(`${clip(note.source.app?.name ?? note.source.title ?? note.source.url, 50)} · ${describeTarget(note.target)}`, choices);
|
|
247
426
|
const id = note.id;
|
|
248
427
|
if (choice === ANNOTATE_ACTIONS.reveal)
|
|
@@ -250,21 +429,11 @@ export class AnnotatePage {
|
|
|
250
429
|
const result = await call('terminal.annotations.reveal', { id });
|
|
251
430
|
return result.state === 'matched' ? 'Marked in the window.' : `The target is ${result.state}${result.reason ? `: ${result.reason}` : ''}.`;
|
|
252
431
|
});
|
|
253
|
-
if (choice === ANNOTATE_ACTIONS.queue)
|
|
254
|
-
return this.attempt(async () => {
|
|
255
|
-
await call('terminal.annotations.queue', { id, expectedVersion: note.version });
|
|
256
|
-
return 'Added to the queue.';
|
|
257
|
-
});
|
|
258
|
-
if (choice === ANNOTATE_ACTIONS.dequeue)
|
|
259
|
-
return this.attempt(async () => {
|
|
260
|
-
await call('terminal.annotations.dequeue', { id, expectedVersion: note.version });
|
|
261
|
-
});
|
|
262
432
|
if (choice === ANNOTATE_ACTIONS.confirm || choice === ANNOTATE_ACTIONS.send)
|
|
263
433
|
return this.attempt(async () => {
|
|
264
434
|
if (choice === ANNOTATE_ACTIONS.confirm)
|
|
265
435
|
await call('terminal.annotations.confirm', { id, expectedVersion: note.version });
|
|
266
|
-
|
|
267
|
-
return result.state === 'sent' ? 'Sent to the agent.' : result.state === 'busy' ? 'It goes to the agent when it finishes.' : undefined;
|
|
436
|
+
return describeSend(await call('terminal.annotations.send', { ids: [id] }));
|
|
268
437
|
});
|
|
269
438
|
if (choice === ANNOTATE_ACTIONS.edit)
|
|
270
439
|
return this.attempt(async () => {
|
|
@@ -273,58 +442,116 @@ export class AnnotatePage {
|
|
|
273
442
|
return;
|
|
274
443
|
await call('terminal.annotations.update', { id, expectedVersion: note.version, patch: { body } });
|
|
275
444
|
});
|
|
276
|
-
if (choice === ANNOTATE_ACTIONS.resolve)
|
|
445
|
+
if (choice === ANNOTATE_ACTIONS.resolve || choice === ANNOTATE_ACTIONS.reopen)
|
|
446
|
+
return this.attempt(async () => {
|
|
447
|
+
await call('terminal.annotations.update', { id, expectedVersion: note.version, patch: { status: choice === ANNOTATE_ACTIONS.resolve ? 'resolved' : 'open' } });
|
|
448
|
+
return choice === ANNOTATE_ACTIONS.resolve ? 'Resolved.' : 'Reopened.';
|
|
449
|
+
});
|
|
450
|
+
if (choice === ANNOTATE_ACTIONS.share || choice === ANNOTATE_ACTIONS.unshare)
|
|
277
451
|
return this.attempt(async () => {
|
|
278
|
-
await call('terminal.annotations.
|
|
279
|
-
return '
|
|
452
|
+
await call(choice === ANNOTATE_ACTIONS.share ? 'terminal.annotations.share' : 'terminal.annotations.unshare', { id });
|
|
453
|
+
return choice === ANNOTATE_ACTIONS.share ? 'Shared with this session. It is unsent until you send it.' : 'No longer shared with this session.';
|
|
280
454
|
});
|
|
281
455
|
if (choice === ANNOTATE_ACTIONS.delete)
|
|
282
456
|
return this.attempt(async () => {
|
|
283
|
-
if (!await ui.confirm('Delete this
|
|
457
|
+
if (!await ui.confirm('Delete this annotation?', 'It cannot be restored.'))
|
|
284
458
|
return;
|
|
285
459
|
await call('terminal.annotations.delete', { id, expectedVersion: note.version });
|
|
286
460
|
});
|
|
287
461
|
}
|
|
462
|
+
// ---- Input and rendering -------------------------------------------------------------------------
|
|
463
|
+
currentList() {
|
|
464
|
+
if (this.tab === 'all')
|
|
465
|
+
return this.allList;
|
|
466
|
+
return this.view === 'apps' ? this.appList : this.noteList;
|
|
467
|
+
}
|
|
288
468
|
handleMouse(event) {
|
|
289
|
-
return this.
|
|
469
|
+
return this.currentList().handleMouse({ ...event, y: event.y - this.listTop });
|
|
290
470
|
}
|
|
291
471
|
handleInput(data) {
|
|
472
|
+
if (this.tab === 'all') {
|
|
473
|
+
if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
|
|
474
|
+
this.selectTab('annotate');
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if (matchesKey(data, 'escape'))
|
|
478
|
+
return this.allList.query ? this.allList.handleInput(data) : false;
|
|
479
|
+
return this.allList.handleInput(data);
|
|
480
|
+
}
|
|
481
|
+
if (this.field) {
|
|
482
|
+
if (matchesKey(data, 'escape')) {
|
|
483
|
+
this.closeField();
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (matchesKey(data, 'enter')) {
|
|
487
|
+
this.submitField();
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
this.field.input.handleInput(data);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
|
|
494
|
+
this.selectTab('all');
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (this.view === 'apps') {
|
|
498
|
+
if (matchesKey(data, 'escape')) {
|
|
499
|
+
this.view = 'notes';
|
|
500
|
+
this.syncFocus();
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (data === 'r') {
|
|
504
|
+
void this.showApps();
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return this.appList.handleInput(data);
|
|
508
|
+
}
|
|
292
509
|
if (data === 'i') {
|
|
293
510
|
void this.attempt(() => this.toggleInspect(!this.target?.inspecting));
|
|
294
511
|
return true;
|
|
295
512
|
}
|
|
296
513
|
if (data === 's') {
|
|
297
|
-
void this.
|
|
514
|
+
void this.sendAll();
|
|
298
515
|
return true;
|
|
299
516
|
}
|
|
300
517
|
if (data === 't') {
|
|
301
518
|
void this.chooseWindow();
|
|
302
519
|
return true;
|
|
303
520
|
}
|
|
304
|
-
if (data === '
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
521
|
+
if (data === 'o') {
|
|
522
|
+
this.openField('address');
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (data === 'a') {
|
|
526
|
+
void this.showApps();
|
|
308
527
|
return true;
|
|
309
528
|
}
|
|
310
|
-
return this.
|
|
529
|
+
return this.noteList.handleInput(data);
|
|
311
530
|
}
|
|
312
531
|
header(width) {
|
|
313
532
|
const theme = this.options.theme();
|
|
314
533
|
const target = this.target;
|
|
315
534
|
const current = target?.windows.find(window => window.current);
|
|
316
|
-
const kind = !target ? '
|
|
535
|
+
const kind = !target ? 'No window yet' : target.kind === 'web' ? 'Browser window' : target.kind === 'cdp' ? `App · ${target.app?.name ?? ''} (DevTools)` : `App · ${target.app?.name ?? ''} (page bridge)`;
|
|
317
536
|
const lines = [
|
|
318
537
|
`${theme.bold(kind)}${current ? theme.fg('muted', ` · ${clip(current.title || current.url, 40)} · ${clip(current.url, 60)}`) : ''}`,
|
|
319
|
-
target
|
|
320
|
-
? theme.fg('
|
|
321
|
-
:
|
|
538
|
+
!target
|
|
539
|
+
? theme.fg('dim', 'Type a page address, or press a to annotate a desktop app.')
|
|
540
|
+
: target.inspecting
|
|
541
|
+
? theme.fg('success', '● Inspecting: hover a component in the window and click it. The agent can read every annotation.')
|
|
542
|
+
: theme.fg('dim', '○ Inspector off. Press i to turn it on.')
|
|
322
543
|
];
|
|
323
544
|
if (this.bridgeSnippet && target?.kind === 'bridge') {
|
|
324
545
|
lines.push(theme.fg(target.windows.length ? 'dim' : 'warning', `Load in the app while developing: ${this.bridgeSnippet}`));
|
|
325
546
|
}
|
|
326
547
|
if (this.error)
|
|
327
548
|
lines.push(theme.fg('error', this.error));
|
|
549
|
+
if (this.field) {
|
|
550
|
+
const { label } = FIELDS[this.field.kind];
|
|
551
|
+
const prefix = `${theme.fg('accent', '›')} ${theme.bold(label)} `;
|
|
552
|
+
const room = Math.max(1, width - visibleWidth(prefix));
|
|
553
|
+
lines.push('', prefix + (this.field.input.render(room)[0] ?? ''));
|
|
554
|
+
}
|
|
328
555
|
lines.push('');
|
|
329
556
|
return lines.map(line => truncateToWidth(line, width, ''));
|
|
330
557
|
}
|
|
@@ -332,10 +559,17 @@ export class AnnotatePage {
|
|
|
332
559
|
const theme = this.options.theme();
|
|
333
560
|
if (width < 1 || rows < 1)
|
|
334
561
|
return [];
|
|
562
|
+
if (this.tab === 'all') {
|
|
563
|
+
this.listTop = 0;
|
|
564
|
+
if (!this.notes)
|
|
565
|
+
return [theme.fg('dim', this.error ? `Could not read web annotations: ${this.error}` : 'Reading web annotations…')];
|
|
566
|
+
return this.allList.render(width, rows).slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
567
|
+
}
|
|
335
568
|
const header = this.header(width);
|
|
569
|
+
this.listTop = header.length;
|
|
336
570
|
if (!this.notes)
|
|
337
|
-
return [...header, theme.fg('dim', 'Reading
|
|
338
|
-
const body = this.
|
|
571
|
+
return [...header, theme.fg('dim', 'Reading annotations…')].slice(0, rows);
|
|
572
|
+
const body = this.currentList().render(width, Math.max(1, rows - header.length));
|
|
339
573
|
return [...header, ...body].slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
340
574
|
}
|
|
341
575
|
}
|
|
@@ -194,10 +194,11 @@ export class BrowserPage {
|
|
|
194
194
|
if (row.hub) {
|
|
195
195
|
const entry = row.hub;
|
|
196
196
|
const installed = this.snapshot?.hub?.installed.some(receipt => receipt.entry.id === entry.id && receipt.entry.version === entry.version);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
if (entry.delivery.kind !== 'pi-package')
|
|
198
|
+
actions.push({ id: 'preview', label: 'Preview reviewed content', run: async () => {
|
|
199
|
+
const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
|
|
200
|
+
this.showContent(page.content + (page.done ? '' : '\n\n[Preview limited to first page.]'));
|
|
201
|
+
} });
|
|
201
202
|
if (!installed)
|
|
202
203
|
actions.push({ id: 'install', label: `Install ${entry.version}`, run: async () => {
|
|
203
204
|
if (!await this.options.ui.confirm(`Install ${entry.title}?`, `${entry.description}\nPublisher: ${entry.publisher}\nLicense: ${entry.license}\nSource: ${entry.sourceUrl}\nVersion: ${entry.version}\nInstallation does not grant trust, enablement, credentials or permissions.`))
|