@openforge-app/plugin-sdk 0.2.9 → 0.2.11
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/dist/backend.d.ts +2 -2
- package/dist/collapsibleSectionState.js +30 -12
- package/dist/domain.d.ts +1 -0
- package/dist/domain.js +2 -1
- package/dist/frontend.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +10 -4
- package/dist/markdown.d.ts +9 -0
- package/dist/markdown.js +57 -1
- package/dist/mermaid.d.ts +13 -0
- package/dist/mermaid.js +221 -0
- package/dist/mermaidZoom.d.ts +23 -0
- package/dist/mermaidZoom.js +52 -0
- package/dist/openforgePackageMetadataSchema.json +2 -11
- package/dist/publicEntrypoints.d.mts +27 -0
- package/dist/publicEntrypoints.mjs +104 -0
- package/dist/publicUiExports.mjs +12 -22
- package/dist/registryValidation.d.mts +11 -0
- package/dist/registryValidation.mjs +30 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +131 -0
- package/dist/taskBrowserDevToolsShortcuts.d.ts +12 -0
- package/dist/taskBrowserDevToolsShortcuts.js +20 -0
- package/dist/testing/commonApiFake.js +240 -7
- package/dist/testing/contracts.d.ts +15 -3
- package/dist/testing/frontendContributionFake.d.ts +2 -3
- package/dist/testing/frontendContributionFake.js +0 -32
- package/dist/testing/support.d.ts +5 -2
- package/dist/testing/support.js +12 -1
- package/dist/types.d.ts +91 -20
- package/dist/types.js +1 -1
- package/dist/ui/MarkdownContent.svelte +46 -0
- package/dist/ui/MermaidDiagramPreview.svelte +216 -0
- package/dist/ui/Modal.svelte +39 -4
- package/dist/ui/PluginPageHeader.svelte +11 -4
- package/dist/ui/PluginPageShell.svelte +20 -0
- package/dist/ui/ProjectFileTree.svelte +192 -0
- package/dist/vite.js +7 -17
- package/package.json +14 -6
|
@@ -1,6 +1,104 @@
|
|
|
1
|
-
import { resolveExternalTextFileChunkSize } from '../types.js';
|
|
1
|
+
import { MAX_AGENT_SESSION_PAGE_SIZE, resolveExternalTextFileChunkSize } from '../types.js';
|
|
2
2
|
import { assertFunction, assertTitle, commandDescriptor, createDisposable, isJsonValue, normalizeAgentCommandMetadata, } from './support.js';
|
|
3
3
|
const UTF8_ENCODER = new TextEncoder();
|
|
4
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
5
|
+
function readTestingUserDataDir(files, directoryPath) {
|
|
6
|
+
const prefix = directoryPath ? `${directoryPath}/` : '';
|
|
7
|
+
const entries = new Map();
|
|
8
|
+
for (const [filePath, content] of files) {
|
|
9
|
+
if (!filePath.startsWith(prefix))
|
|
10
|
+
continue;
|
|
11
|
+
const childPath = filePath.slice(prefix.length);
|
|
12
|
+
const separatorIndex = childPath.indexOf('/');
|
|
13
|
+
const name = separatorIndex === -1 ? childPath : childPath.slice(0, separatorIndex);
|
|
14
|
+
if (!name)
|
|
15
|
+
continue;
|
|
16
|
+
entries.set(name, separatorIndex === -1
|
|
17
|
+
? {
|
|
18
|
+
name,
|
|
19
|
+
path: `${prefix}${name}`,
|
|
20
|
+
isDir: false,
|
|
21
|
+
size: UTF8_ENCODER.encode(content).byteLength,
|
|
22
|
+
modifiedAt: null,
|
|
23
|
+
}
|
|
24
|
+
: {
|
|
25
|
+
name,
|
|
26
|
+
path: `${prefix}${name}`,
|
|
27
|
+
isDir: true,
|
|
28
|
+
size: null,
|
|
29
|
+
modifiedAt: null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return [...entries.values()].sort((left, right) => {
|
|
33
|
+
if (left.isDir !== right.isDir)
|
|
34
|
+
return left.isDir ? -1 : 1;
|
|
35
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const TERMINAL_AGENT_SESSION_STATUSES = new Set(['completed', 'failed', 'interrupted']);
|
|
39
|
+
function encodeAgentSessionCursor(payload) {
|
|
40
|
+
const bytes = UTF8_ENCODER.encode(JSON.stringify(payload));
|
|
41
|
+
let binary = '';
|
|
42
|
+
for (const byte of bytes)
|
|
43
|
+
binary += String.fromCharCode(byte);
|
|
44
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
45
|
+
}
|
|
46
|
+
function parseAgentSessionCursor(cursor) {
|
|
47
|
+
try {
|
|
48
|
+
const base64 = cursor.replaceAll('-', '+').replaceAll('_', '/');
|
|
49
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
50
|
+
const binary = atob(padded);
|
|
51
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
52
|
+
const payload = JSON.parse(UTF8_DECODER.decode(bytes));
|
|
53
|
+
const filters = payload.filters;
|
|
54
|
+
if (payload.version !== 1
|
|
55
|
+
|| !Number.isSafeInteger(payload.createdAt)
|
|
56
|
+
|| typeof payload.id !== 'string'
|
|
57
|
+
|| payload.id.length === 0
|
|
58
|
+
|| !filters
|
|
59
|
+
|| typeof filters.provider !== 'string'
|
|
60
|
+
|| !Number.isSafeInteger(filters.startInclusive)
|
|
61
|
+
|| !Number.isSafeInteger(filters.endExclusive)
|
|
62
|
+
|| (filters.taskId !== null && typeof filters.taskId !== 'string')) {
|
|
63
|
+
throw new Error('invalid payload');
|
|
64
|
+
}
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new TypeError('cursor is malformed');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function providerSessionId(session) {
|
|
72
|
+
switch (session.provider) {
|
|
73
|
+
case 'opencode': return session.opencode_session_id;
|
|
74
|
+
case 'claude-code': return session.claude_session_id;
|
|
75
|
+
case 'pi': return session.pi_session_id;
|
|
76
|
+
case 'grok': return session.grok_session_id;
|
|
77
|
+
default: return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function assertNonNegativeSafeInteger(value, name) {
|
|
81
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
82
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function testingExternalFileIdentity(file) {
|
|
86
|
+
return file.identity ?? `${file.root}:${file.path}`;
|
|
87
|
+
}
|
|
88
|
+
function readTestingExternalTextRange(file, startOffsetBytes, maxBytes, expectedIdentity) {
|
|
89
|
+
assertNonNegativeSafeInteger(startOffsetBytes, 'startOffsetBytes');
|
|
90
|
+
if (maxBytes !== undefined)
|
|
91
|
+
assertNonNegativeSafeInteger(maxBytes, 'maxBytes');
|
|
92
|
+
const identity = testingExternalFileIdentity(file);
|
|
93
|
+
if (expectedIdentity !== undefined && expectedIdentity !== identity) {
|
|
94
|
+
throw new Error(`External file identity changed: expected ${expectedIdentity}, received ${identity}`);
|
|
95
|
+
}
|
|
96
|
+
const bytes = UTF8_ENCODER.encode(file.content);
|
|
97
|
+
const endOffsetBytes = maxBytes === undefined
|
|
98
|
+
? bytes.byteLength
|
|
99
|
+
: Math.min(bytes.byteLength, startOffsetBytes + maxBytes);
|
|
100
|
+
return UTF8_DECODER.decode(bytes.slice(startOffsetBytes, endOffsetBytes));
|
|
101
|
+
}
|
|
4
102
|
function* splitExternalTextFile(content, maxBytes) {
|
|
5
103
|
let chunk = '';
|
|
6
104
|
let chunkBytes = 0;
|
|
@@ -45,6 +143,100 @@ export class TestingCommonApiFake {
|
|
|
45
143
|
context: {
|
|
46
144
|
getSnapshot: () => this.services.getContextSnapshot(),
|
|
47
145
|
},
|
|
146
|
+
agentSessions: {
|
|
147
|
+
list: async (request) => {
|
|
148
|
+
if (typeof request.provider !== 'string' || request.provider.trim().length === 0) {
|
|
149
|
+
throw new TypeError('provider must be a non-empty string');
|
|
150
|
+
}
|
|
151
|
+
if (request.taskId !== undefined && (typeof request.taskId !== 'string' || request.taskId.trim().length === 0)) {
|
|
152
|
+
throw new TypeError('taskId must be a non-empty string');
|
|
153
|
+
}
|
|
154
|
+
if (request.cursor !== undefined && (typeof request.cursor !== 'string' || request.cursor.length === 0)) {
|
|
155
|
+
throw new TypeError('cursor must be a non-empty string');
|
|
156
|
+
}
|
|
157
|
+
if (!request.overlaps || typeof request.overlaps !== 'object') {
|
|
158
|
+
throw new TypeError('overlaps must contain startInclusive and endExclusive');
|
|
159
|
+
}
|
|
160
|
+
assertNonNegativeSafeInteger(request.overlaps.startInclusive, 'overlaps.startInclusive');
|
|
161
|
+
assertNonNegativeSafeInteger(request.overlaps.endExclusive, 'overlaps.endExclusive');
|
|
162
|
+
if (request.overlaps.startInclusive >= request.overlaps.endExclusive) {
|
|
163
|
+
throw new RangeError('overlaps must satisfy startInclusive < endExclusive');
|
|
164
|
+
}
|
|
165
|
+
if (!Number.isSafeInteger(request.pageSize)
|
|
166
|
+
|| request.pageSize < 1
|
|
167
|
+
|| request.pageSize > MAX_AGENT_SESSION_PAGE_SIZE) {
|
|
168
|
+
throw new RangeError(`pageSize must be between 1 and ${MAX_AGENT_SESSION_PAGE_SIZE}`);
|
|
169
|
+
}
|
|
170
|
+
const filters = {
|
|
171
|
+
provider: request.provider,
|
|
172
|
+
startInclusive: request.overlaps.startInclusive,
|
|
173
|
+
endExclusive: request.overlaps.endExclusive,
|
|
174
|
+
taskId: request.taskId ?? null,
|
|
175
|
+
};
|
|
176
|
+
const cursor = request.cursor === undefined ? null : parseAgentSessionCursor(request.cursor);
|
|
177
|
+
if (cursor !== null
|
|
178
|
+
&& (cursor.filters.provider !== filters.provider
|
|
179
|
+
|| cursor.filters.startInclusive !== filters.startInclusive
|
|
180
|
+
|| cursor.filters.endExclusive !== filters.endExclusive
|
|
181
|
+
|| cursor.filters.taskId !== filters.taskId)) {
|
|
182
|
+
throw new TypeError('cursor does not match request filters');
|
|
183
|
+
}
|
|
184
|
+
this.services.calls.agentSessionListRequests.push({
|
|
185
|
+
...request,
|
|
186
|
+
overlaps: { ...request.overlaps },
|
|
187
|
+
});
|
|
188
|
+
const taskById = new Map(this.services.seededTasks.map((task) => [task.id, task]));
|
|
189
|
+
const rows = this.services.seededAgentSessions
|
|
190
|
+
.filter((session) => taskById.has(session.ticket_id))
|
|
191
|
+
.filter((session) => session.provider === request.provider)
|
|
192
|
+
.filter((session) => request.taskId === undefined || session.ticket_id === request.taskId)
|
|
193
|
+
.filter((session) => session.created_at < request.overlaps.endExclusive
|
|
194
|
+
&& (!TERMINAL_AGENT_SESSION_STATUSES.has(session.status)
|
|
195
|
+
|| session.updated_at > request.overlaps.startInclusive))
|
|
196
|
+
.filter((session) => cursor === null
|
|
197
|
+
|| session.created_at > cursor.createdAt
|
|
198
|
+
|| (session.created_at === cursor.createdAt && session.id > cursor.id))
|
|
199
|
+
.slice()
|
|
200
|
+
.sort((left, right) => left.created_at - right.created_at
|
|
201
|
+
|| (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
202
|
+
const pageRows = rows.slice(0, request.pageSize);
|
|
203
|
+
const items = pageRows.map((session) => {
|
|
204
|
+
const task = taskById.get(session.ticket_id);
|
|
205
|
+
if (!task)
|
|
206
|
+
throw new Error(`Missing seeded Task for Agent Session ${session.id}`);
|
|
207
|
+
const workspace = this.services.agentSessionWorkspaces[task.id];
|
|
208
|
+
return {
|
|
209
|
+
id: session.id,
|
|
210
|
+
provider: session.provider,
|
|
211
|
+
providerSessionId: providerSessionId(session),
|
|
212
|
+
createdAt: session.created_at,
|
|
213
|
+
updatedAt: session.updated_at,
|
|
214
|
+
task: {
|
|
215
|
+
id: task.id,
|
|
216
|
+
title: task.title?.trim() || task.id,
|
|
217
|
+
status: task.status,
|
|
218
|
+
createdAt: task.created_at,
|
|
219
|
+
updatedAt: task.updated_at,
|
|
220
|
+
},
|
|
221
|
+
workspace: workspace
|
|
222
|
+
? { rootPath: workspace.rootPath, kind: workspace.kind }
|
|
223
|
+
: null,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
const last = pageRows.at(-1);
|
|
227
|
+
return {
|
|
228
|
+
items,
|
|
229
|
+
nextCursor: rows.length > request.pageSize && last
|
|
230
|
+
? encodeAgentSessionCursor({
|
|
231
|
+
version: 1,
|
|
232
|
+
createdAt: last.created_at,
|
|
233
|
+
id: last.id,
|
|
234
|
+
filters,
|
|
235
|
+
})
|
|
236
|
+
: null,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
},
|
|
48
240
|
tasks: {
|
|
49
241
|
list: async (request) => {
|
|
50
242
|
const projectId = request?.projectId ?? null;
|
|
@@ -125,6 +317,16 @@ export class TestingCommonApiFake {
|
|
|
125
317
|
},
|
|
126
318
|
getWorkspace: async () => null,
|
|
127
319
|
getLatestSession: async () => null,
|
|
320
|
+
listSessions: async (request) => {
|
|
321
|
+
this.services.calls.taskSessionListRequests.push({ ...request });
|
|
322
|
+
return this.services.seededAgentSessions
|
|
323
|
+
.map((session, index) => ({ session, index }))
|
|
324
|
+
.filter(({ session }) => session.ticket_id === request.taskId)
|
|
325
|
+
.filter(({ session }) => request.provider === undefined || session.provider === request.provider)
|
|
326
|
+
.filter(({ session }) => request.createdAtOrAfter === undefined || session.created_at >= request.createdAtOrAfter)
|
|
327
|
+
.sort((left, right) => right.session.created_at - left.session.created_at || right.index - left.index)
|
|
328
|
+
.map(({ session }) => session);
|
|
329
|
+
},
|
|
128
330
|
},
|
|
129
331
|
projects: {
|
|
130
332
|
list: async () => [],
|
|
@@ -146,7 +348,6 @@ export class TestingCommonApiFake {
|
|
|
146
348
|
write: async (request) => {
|
|
147
349
|
this.services.calls.shellWrites.push(request);
|
|
148
350
|
},
|
|
149
|
-
writeTerminalQueryResponse: async () => { },
|
|
150
351
|
resize: async (request) => {
|
|
151
352
|
this.services.calls.shellResizes.push(request);
|
|
152
353
|
},
|
|
@@ -211,14 +412,21 @@ export class TestingCommonApiFake {
|
|
|
211
412
|
userData: {
|
|
212
413
|
readDir: async (request = {}) => {
|
|
213
414
|
this.services.calls.fsUserDataReadDirs.push(request);
|
|
214
|
-
return
|
|
415
|
+
return readTestingUserDataDir(this.services.userDataTextFiles, request.path);
|
|
215
416
|
},
|
|
216
417
|
readTextFile: async (request) => {
|
|
217
418
|
this.services.calls.fsUserDataReads.push(request);
|
|
218
|
-
return '';
|
|
419
|
+
return this.services.userDataTextFiles.get(request.path) ?? '';
|
|
219
420
|
},
|
|
220
421
|
writeTextFile: async (request) => {
|
|
221
422
|
this.services.calls.fsUserDataWrites.push(request);
|
|
423
|
+
this.services.userDataTextFiles.set(request.path, request.content);
|
|
424
|
+
},
|
|
425
|
+
appendTextFile: async (request) => {
|
|
426
|
+
this.services.calls.fsUserDataAppends.push(request);
|
|
427
|
+
const content = `${this.services.userDataTextFiles.get(request.path) ?? ''}${request.content}`;
|
|
428
|
+
this.services.userDataTextFiles.set(request.path, content);
|
|
429
|
+
return { sizeBytes: UTF8_ENCODER.encode(content).byteLength };
|
|
222
430
|
},
|
|
223
431
|
},
|
|
224
432
|
external: {
|
|
@@ -230,14 +438,39 @@ export class TestingCommonApiFake {
|
|
|
230
438
|
this.services.calls.fsExternalReads.push(request);
|
|
231
439
|
return '';
|
|
232
440
|
},
|
|
441
|
+
stat: async (request) => {
|
|
442
|
+
this.services.calls.fsExternalStats.push(request);
|
|
443
|
+
const file = this.services.externalTextFiles.find(candidate => candidate.root === request.root && candidate.path === request.path);
|
|
444
|
+
if (!file)
|
|
445
|
+
throw new Error(`External file not found: ${request.root}/${request.path}`);
|
|
446
|
+
return {
|
|
447
|
+
identity: testingExternalFileIdentity(file),
|
|
448
|
+
sizeBytes: UTF8_ENCODER.encode(file.content).byteLength,
|
|
449
|
+
modifiedAtMs: file.modifiedAtMs ?? null,
|
|
450
|
+
};
|
|
451
|
+
},
|
|
233
452
|
readTextFileChunks: (request) => {
|
|
234
453
|
const chunkSizeBytes = resolveExternalTextFileChunkSize(request.chunkSizeBytes);
|
|
235
|
-
const { root, path, signal } = request;
|
|
236
|
-
this.services.calls.fsExternalReadTextFileChunks.push({
|
|
237
|
-
|
|
454
|
+
const { root, path, signal, expectedIdentity, startOffsetBytes = 0, maxBytes, } = request;
|
|
455
|
+
this.services.calls.fsExternalReadTextFileChunks.push({
|
|
456
|
+
root,
|
|
457
|
+
path,
|
|
458
|
+
chunkSizeBytes,
|
|
459
|
+
...(expectedIdentity === undefined ? {} : { expectedIdentity }),
|
|
460
|
+
...(request.startOffsetBytes === undefined ? {} : { startOffsetBytes }),
|
|
461
|
+
...(maxBytes === undefined ? {} : { maxBytes }),
|
|
462
|
+
});
|
|
463
|
+
const file = this.services.externalTextFiles.find(candidate => candidate.root === root && candidate.path === path);
|
|
238
464
|
return (async function* () {
|
|
465
|
+
signal?.throwIfAborted();
|
|
466
|
+
if (!file)
|
|
467
|
+
throw new Error(`External file not found: ${root}/${path}`);
|
|
468
|
+
const content = readTestingExternalTextRange(file, startOffsetBytes, maxBytes, expectedIdentity);
|
|
239
469
|
for (const chunk of splitExternalTextFile(content, chunkSizeBytes)) {
|
|
240
470
|
signal?.throwIfAborted();
|
|
471
|
+
if (expectedIdentity !== undefined && testingExternalFileIdentity(file) !== expectedIdentity) {
|
|
472
|
+
throw new Error(`External file identity changed: expected ${expectedIdentity}, received ${testingExternalFileIdentity(file)}`);
|
|
473
|
+
}
|
|
241
474
|
yield chunk;
|
|
242
475
|
}
|
|
243
476
|
signal?.throwIfAborted();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BrowserSurfaceVisualFeedback } from '../browserSurfaces';
|
|
2
2
|
import type { TestingOpenForgeRegistryFake } from './registryFake';
|
|
3
|
-
import type { BackendMethodRegistration, BackendOpenForgeAPI, BackgroundServiceRegistration, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ConfigureStartPromptContributionRequest, CreateTaskRequest, FrontendOpenForgeAPI, InjectionPointLocation, JsonValue, NotificationRequest, OpenForgeNavigationRequest, OpenForgePackageMetadata, PluginSettingsSectionRegistration, PluginStorage, PluginTaskPaneTabRegistration, PluginReviewRowActionRegistration, PluginTaskUISectionRegistration, PluginViewRegistration, ShellSpawnRequest, SendTaskFollowUpRequest, StartTaskImplementationRequest,
|
|
4
|
-
import type { Task } from '../domain';
|
|
3
|
+
import type { BackendMethodRegistration, BackendOpenForgeAPI, BackgroundServiceRegistration, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ConfigureStartPromptContributionRequest, CreateTaskRequest, FrontendOpenForgeAPI, InjectionPointLocation, AgentSessionWorkspace, ListAgentSessionsRequest, ListTaskSessionsRequest, JsonValue, NotificationRequest, OpenForgeNavigationRequest, OpenForgePackageMetadata, PluginSettingsSectionRegistration, PluginStorage, PluginTaskPaneTabRegistration, PluginReviewRowActionRegistration, PluginTaskUISectionRegistration, PluginViewRegistration, ShellSpawnRequest, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskStartPrefixContext, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadTextFileChunksRequest, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileWriteRequest } from '../types';
|
|
4
|
+
import type { AgentSession, Task } from '../domain';
|
|
5
5
|
export type TestingRuntimeScope = 'global' | 'project' | 'task';
|
|
6
6
|
export type TestingRuntimeKind = 'commands' | 'events' | 'views' | 'taskPane' | 'taskUI' | 'reviewUI' | 'settings' | 'backend' | 'background';
|
|
7
7
|
export type TestingMaybePromise<T> = T | Promise<T>;
|
|
@@ -9,6 +9,9 @@ export type TestingCommandHandler = (payload?: unknown) => TestingMaybePromise<u
|
|
|
9
9
|
export type TestingEventHandler = (payload: unknown) => void;
|
|
10
10
|
export interface TestingExternalTextFile extends ExternalReadFileRequest {
|
|
11
11
|
content: string;
|
|
12
|
+
/** Defaults to a deterministic identity derived from root and path. */
|
|
13
|
+
identity?: string;
|
|
14
|
+
modifiedAtMs?: number | null;
|
|
12
15
|
}
|
|
13
16
|
export type TestingExternalTextFileChunksCall = Omit<ExternalReadTextFileChunksRequest, 'signal' | 'chunkSizeBytes'> & {
|
|
14
17
|
chunkSizeBytes: number;
|
|
@@ -21,12 +24,18 @@ export interface TestingOpenForgeApiOptions {
|
|
|
21
24
|
viewId?: string;
|
|
22
25
|
packageMetadata?: OpenForgePackageMetadata;
|
|
23
26
|
storage?: PluginStorage;
|
|
27
|
+
/** Initial files exposed through `fs.userData`. Defaults to none. */
|
|
28
|
+
userDataTextFiles?: UserDataFileWriteRequest[];
|
|
24
29
|
/**
|
|
25
30
|
* Tasks returned by `tasks.list`. The mock filters them by the requested
|
|
26
31
|
* `projectId` (when given) and drops `done` tasks unless `includeDone: true`,
|
|
27
32
|
* mirroring the host capability. Defaults to an empty list.
|
|
28
33
|
*/
|
|
29
34
|
tasks?: Task[];
|
|
35
|
+
/** Agent Sessions returned by `tasks.listSessions`. Defaults to an empty list. */
|
|
36
|
+
agentSessions?: AgentSession[];
|
|
37
|
+
/** Compact workspace context keyed by Task ID for `agentSessions.list`. Defaults to none. */
|
|
38
|
+
agentSessionWorkspaces?: Readonly<Record<string, AgentSessionWorkspace>>;
|
|
30
39
|
/** UTF-8 files returned by `fs.external.readTextFileChunks`. Defaults to none. */
|
|
31
40
|
externalTextFiles?: TestingExternalTextFile[];
|
|
32
41
|
}
|
|
@@ -56,7 +65,6 @@ export interface TestingOpenForgeApiCalls {
|
|
|
56
65
|
}>;
|
|
57
66
|
openUrl: string[];
|
|
58
67
|
clipboardWrites: string[];
|
|
59
|
-
taskLinkOpenRequests: TaskLinkOpenRequest[];
|
|
60
68
|
navigationRequests: OpenForgeNavigationRequest[];
|
|
61
69
|
notify: NotificationRequest[];
|
|
62
70
|
taskCreations: CreateTaskRequest[];
|
|
@@ -68,6 +76,8 @@ export interface TestingOpenForgeApiCalls {
|
|
|
68
76
|
projectId: string | null;
|
|
69
77
|
includeDone: boolean;
|
|
70
78
|
}>;
|
|
79
|
+
agentSessionListRequests: ListAgentSessionsRequest[];
|
|
80
|
+
taskSessionListRequests: ListTaskSessionsRequest[];
|
|
71
81
|
taskStatusUpdates: Array<{
|
|
72
82
|
taskId: string;
|
|
73
83
|
status: string;
|
|
@@ -85,8 +95,10 @@ export interface TestingOpenForgeApiCalls {
|
|
|
85
95
|
fsUserDataReadDirs: UserDataDirectoryRequest[];
|
|
86
96
|
fsUserDataReads: UserDataFileRequest[];
|
|
87
97
|
fsUserDataWrites: UserDataFileWriteRequest[];
|
|
98
|
+
fsUserDataAppends: UserDataFileWriteRequest[];
|
|
88
99
|
fsExternalReadDirs: ExternalReadDirectoryRequest[];
|
|
89
100
|
fsExternalReads: ExternalReadFileRequest[];
|
|
101
|
+
fsExternalStats: ExternalReadFileRequest[];
|
|
90
102
|
fsExternalReadTextFileChunks: TestingExternalTextFileChunksCall[];
|
|
91
103
|
shellSpawns: ShellSpawnRequest[];
|
|
92
104
|
shellWrites: Array<{
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TaskBrowserSurfaceState } from '../browserSurfaces';
|
|
2
2
|
import type { FrontendOpenForgeAPI } from '../types';
|
|
3
3
|
import { type TestingRegistryServices } from './support.js';
|
|
4
4
|
import type { TestingInjectionPointContribution, TestingTaskStartPrefixProviderContribution, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingReviewRowActionContribution, TestingTaskUISectionContribution, TestingViewContribution } from './contracts';
|
|
5
|
-
type TestingFrontendContributionApi = Pick<FrontendOpenForgeAPI, 'browserSurfaces' | '
|
|
5
|
+
type TestingFrontendContributionApi = Pick<FrontendOpenForgeAPI, 'browserSurfaces' | 'views' | 'taskUI' | 'reviewUI' | 'taskPane' | 'settings' | 'backend' | 'injectionPoints' | 'taskStart'>;
|
|
6
6
|
export declare class TestingFrontendContributionFake {
|
|
7
7
|
private readonly services;
|
|
8
8
|
private readonly invokeBackendMethod;
|
|
@@ -14,7 +14,6 @@ export declare class TestingFrontendContributionFake {
|
|
|
14
14
|
private readonly injectionPoints;
|
|
15
15
|
private readonly taskStartPrefixProviders;
|
|
16
16
|
private readonly browserSurfaces;
|
|
17
|
-
private taskLinkHandler;
|
|
18
17
|
private api;
|
|
19
18
|
constructor(services: TestingRegistryServices, invokeBackendMethod: (method: string, payload?: unknown) => Promise<unknown>);
|
|
20
19
|
createApi(): TestingFrontendContributionApi;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createTestingBrowserSurfaces } from '../browserSurfacesTesting.js';
|
|
2
|
-
import { isAllowedBrowserSurfaceUrl } from '../browserSurfaces.js';
|
|
3
2
|
import { assertFunction, assertTitle, createDisposable, } from './support.js';
|
|
4
3
|
export class TestingFrontendContributionFake {
|
|
5
4
|
services;
|
|
@@ -12,7 +11,6 @@ export class TestingFrontendContributionFake {
|
|
|
12
11
|
injectionPoints = new Map();
|
|
13
12
|
taskStartPrefixProviders = new Map();
|
|
14
13
|
browserSurfaces;
|
|
15
|
-
taskLinkHandler = null;
|
|
16
14
|
api = null;
|
|
17
15
|
constructor(services, invokeBackendMethod) {
|
|
18
16
|
this.services = services;
|
|
@@ -24,36 +22,6 @@ export class TestingFrontendContributionFake {
|
|
|
24
22
|
return this.api;
|
|
25
23
|
const api = {
|
|
26
24
|
browserSurfaces: this.browserSurfaces.api,
|
|
27
|
-
taskLinks: {
|
|
28
|
-
open: async (request) => {
|
|
29
|
-
this.services.calls.taskLinkOpenRequests.push(request);
|
|
30
|
-
if (!isAllowedBrowserSurfaceUrl(request.url)) {
|
|
31
|
-
throw new Error('Task links must use a valid HTTP(S) URL');
|
|
32
|
-
}
|
|
33
|
-
if (this.taskLinkHandler === null) {
|
|
34
|
-
this.services.calls.openUrl.push(request.url);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const result = await this.taskLinkHandler(request);
|
|
38
|
-
if (result === 'declined') {
|
|
39
|
-
this.services.calls.openUrl.push(request.url);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (result !== 'handled') {
|
|
43
|
-
throw new Error(`Task link handler returned an invalid result: ${String(result)}`);
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
registerHandler: (handler) => {
|
|
47
|
-
if (this.taskLinkHandler !== null) {
|
|
48
|
-
throw new Error('A Task link handler is already registered');
|
|
49
|
-
}
|
|
50
|
-
this.taskLinkHandler = handler;
|
|
51
|
-
return createDisposable(() => {
|
|
52
|
-
if (this.taskLinkHandler === handler)
|
|
53
|
-
this.taskLinkHandler = null;
|
|
54
|
-
});
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
25
|
views: {
|
|
58
26
|
register: (registration) => this.registerView(registration),
|
|
59
27
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AgentCommandMetadata, CommandDescriptor, Disposable, JsonValue, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, PluginStorage, StartPromptContribution, SubscriptionSink } from '../types';
|
|
2
|
-
import type { Task } from '../domain';
|
|
1
|
+
import type { AgentSessionWorkspace, AgentCommandMetadata, CommandDescriptor, Disposable, JsonValue, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, PluginStorage, StartPromptContribution, SubscriptionSink } from '../types';
|
|
2
|
+
import type { AgentSession, Task } from '../domain';
|
|
3
3
|
import type { TestingCommandContribution, TestingMaybePromise, TestingExternalTextFile, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingRuntimeKind } from './contracts';
|
|
4
4
|
export declare function createDisposable(dispose: () => TestingMaybePromise<void>): Disposable;
|
|
5
5
|
export declare class TestingSubscriptionSink implements SubscriptionSink {
|
|
@@ -31,7 +31,10 @@ export declare class TestingRegistryServices {
|
|
|
31
31
|
readonly storage: PluginStorage;
|
|
32
32
|
readonly config: Map<string, JsonValue>;
|
|
33
33
|
readonly seededTasks: Task[];
|
|
34
|
+
readonly seededAgentSessions: AgentSession[];
|
|
35
|
+
readonly agentSessionWorkspaces: Readonly<Record<string, AgentSessionWorkspace>>;
|
|
34
36
|
readonly externalTextFiles: TestingExternalTextFile[];
|
|
37
|
+
readonly userDataTextFiles: Map<string, string>;
|
|
35
38
|
readonly claims: TestingContributionClaims;
|
|
36
39
|
constructor(options?: TestingOpenForgeApiOptions);
|
|
37
40
|
localQualifiedId(kind: TestingRuntimeKind, id: string): string;
|
package/dist/testing/support.js
CHANGED
|
@@ -36,7 +36,6 @@ export function createTestingCalls() {
|
|
|
36
36
|
emittedGlobalEvents: [],
|
|
37
37
|
openUrl: [],
|
|
38
38
|
clipboardWrites: [],
|
|
39
|
-
taskLinkOpenRequests: [],
|
|
40
39
|
navigationRequests: [],
|
|
41
40
|
notify: [],
|
|
42
41
|
taskCreations: [],
|
|
@@ -45,14 +44,18 @@ export function createTestingCalls() {
|
|
|
45
44
|
taskImplementationStarts: [],
|
|
46
45
|
taskFollowUps: [],
|
|
47
46
|
taskListRequests: [],
|
|
47
|
+
agentSessionListRequests: [],
|
|
48
|
+
taskSessionListRequests: [],
|
|
48
49
|
taskStatusUpdates: [],
|
|
49
50
|
configWrites: [],
|
|
50
51
|
fsWrites: [],
|
|
51
52
|
fsUserDataReadDirs: [],
|
|
52
53
|
fsUserDataReads: [],
|
|
53
54
|
fsUserDataWrites: [],
|
|
55
|
+
fsUserDataAppends: [],
|
|
54
56
|
fsExternalReadDirs: [],
|
|
55
57
|
fsExternalReads: [],
|
|
58
|
+
fsExternalStats: [],
|
|
56
59
|
fsExternalReadTextFileChunks: [],
|
|
57
60
|
shellSpawns: [],
|
|
58
61
|
shellWrites: [],
|
|
@@ -202,7 +205,10 @@ export class TestingRegistryServices {
|
|
|
202
205
|
storage;
|
|
203
206
|
config = new Map();
|
|
204
207
|
seededTasks;
|
|
208
|
+
seededAgentSessions;
|
|
209
|
+
agentSessionWorkspaces;
|
|
205
210
|
externalTextFiles;
|
|
211
|
+
userDataTextFiles = new Map();
|
|
206
212
|
claims = new TestingContributionClaims();
|
|
207
213
|
constructor(options = {}) {
|
|
208
214
|
this.pluginId = options.pluginId ?? 'test-plugin';
|
|
@@ -218,7 +224,12 @@ export class TestingRegistryServices {
|
|
|
218
224
|
this.calls = createTestingCalls();
|
|
219
225
|
this.storage = options.storage ?? createMemoryPluginStorage(this.calls);
|
|
220
226
|
this.seededTasks = options.tasks ?? [];
|
|
227
|
+
this.seededAgentSessions = options.agentSessions ?? [];
|
|
228
|
+
this.agentSessionWorkspaces = options.agentSessionWorkspaces ?? {};
|
|
221
229
|
this.externalTextFiles = options.externalTextFiles ?? [];
|
|
230
|
+
for (const file of options.userDataTextFiles ?? []) {
|
|
231
|
+
this.userDataTextFiles.set(file.path, file.content);
|
|
232
|
+
}
|
|
222
233
|
}
|
|
223
234
|
localQualifiedId(kind, id) {
|
|
224
235
|
assertLocalId(kind, id);
|