@openforge-app/plugin-sdk 0.1.0 → 0.2.3

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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/browserSurfaces.d.ts +78 -0
  4. package/dist/browserSurfaces.js +17 -0
  5. package/dist/browserSurfacesTesting.d.ts +60 -0
  6. package/dist/browserSurfacesTesting.js +240 -0
  7. package/dist/domain.d.ts +74 -15
  8. package/dist/domain.js +5 -1
  9. package/dist/fileIcons.d.ts +6 -0
  10. package/dist/fileIcons.js +84 -0
  11. package/dist/frontend.d.ts +5 -2
  12. package/dist/frontend.js +1 -0
  13. package/dist/index.d.ts +5 -3
  14. package/dist/index.js +2 -1
  15. package/dist/manifest.d.ts +29 -0
  16. package/dist/manifest.js +37 -0
  17. package/dist/markdown.d.ts +5 -1
  18. package/dist/markdown.js +67 -18
  19. package/dist/openforgePackageMetadataSchema.json +40 -1
  20. package/dist/pluginIcons.d.ts +7 -0
  21. package/dist/pluginIcons.js +104 -0
  22. package/dist/publicUiExports.d.mts +16 -0
  23. package/dist/publicUiExports.mjs +59 -0
  24. package/dist/svelteHostRuntimeContract.d.mts +1 -0
  25. package/dist/svelteHostRuntimeContract.mjs +16 -0
  26. package/dist/testing/backendServicesFake.d.ts +19 -0
  27. package/dist/testing/backendServicesFake.js +87 -0
  28. package/dist/testing/commonApiFake.d.ts +22 -0
  29. package/dist/testing/commonApiFake.js +274 -0
  30. package/dist/testing/contracts.d.ts +231 -0
  31. package/dist/testing/contracts.js +1 -0
  32. package/dist/testing/frontendContributionFake.d.ts +36 -0
  33. package/dist/testing/frontendContributionFake.js +195 -0
  34. package/dist/testing/registryFake.d.ts +40 -0
  35. package/dist/testing/registryFake.js +124 -0
  36. package/dist/testing/support.d.ts +40 -0
  37. package/dist/testing/support.js +242 -0
  38. package/dist/testing.d.ts +3 -223
  39. package/dist/testing.js +2 -625
  40. package/dist/types.d.ts +193 -7
  41. package/dist/types.js +8 -0
  42. package/dist/ui/Button.svelte +49 -0
  43. package/dist/ui/Checkbox.svelte +137 -0
  44. package/dist/ui/FileTypeIcon.svelte +37 -0
  45. package/dist/ui/MarkdownContent.svelte +71 -11
  46. package/dist/ui/Modal.svelte +157 -0
  47. package/dist/ui/PluginPageHeader.svelte +26 -0
  48. package/dist/ui/PluginViewState.svelte +76 -0
  49. package/dist/ui/ResizablePanel.svelte +10 -0
  50. package/dist/ui/icons/README.md +9 -0
  51. package/dist/ui/icons/c.svg +1 -0
  52. package/dist/ui/icons/console.svg +1 -0
  53. package/dist/ui/icons/cpp.svg +1 -0
  54. package/dist/ui/icons/csharp.svg +1 -0
  55. package/dist/ui/icons/css.svg +3 -0
  56. package/dist/ui/icons/database.svg +1 -0
  57. package/dist/ui/icons/docker.svg +1 -0
  58. package/dist/ui/icons/document.svg +4 -0
  59. package/dist/ui/icons/file.svg +1 -0
  60. package/dist/ui/icons/folder-open.svg +1 -0
  61. package/dist/ui/icons/folder.svg +1 -0
  62. package/dist/ui/icons/git.svg +3 -0
  63. package/dist/ui/icons/go.svg +1 -0
  64. package/dist/ui/icons/graphql.svg +20 -0
  65. package/dist/ui/icons/h.svg +3 -0
  66. package/dist/ui/icons/html.svg +3 -0
  67. package/dist/ui/icons/image.svg +3 -0
  68. package/dist/ui/icons/java.svg +6 -0
  69. package/dist/ui/icons/javascript.svg +3 -0
  70. package/dist/ui/icons/json.svg +3 -0
  71. package/dist/ui/icons/kotlin.svg +1 -0
  72. package/dist/ui/icons/lock.svg +3 -0
  73. package/dist/ui/icons/markdown.svg +6 -0
  74. package/dist/ui/icons/nodejs.svg +6 -0
  75. package/dist/ui/icons/npm.svg +3 -0
  76. package/dist/ui/icons/pdf.svg +1 -0
  77. package/dist/ui/icons/php.svg +1 -0
  78. package/dist/ui/icons/python.svg +1 -0
  79. package/dist/ui/icons/react.svg +8 -0
  80. package/dist/ui/icons/react_ts.svg +8 -0
  81. package/dist/ui/icons/readme.svg +4 -0
  82. package/dist/ui/icons/ruby.svg +1 -0
  83. package/dist/ui/icons/rust.svg +1 -0
  84. package/dist/ui/icons/sass.svg +3 -0
  85. package/dist/ui/icons/settings.svg +4 -0
  86. package/dist/ui/icons/svelte.svg +1 -0
  87. package/dist/ui/icons/svg.svg +3 -0
  88. package/dist/ui/icons/swift.svg +1 -0
  89. package/dist/ui/icons/tune.svg +11 -0
  90. package/dist/ui/icons/typescript-def.svg +6 -0
  91. package/dist/ui/icons/typescript.svg +3 -0
  92. package/dist/ui/icons/vue.svg +1 -0
  93. package/dist/ui/icons/xml.svg +1 -0
  94. package/dist/ui/icons/yaml.svg +3 -0
  95. package/dist/ui/icons/zip.svg +3 -0
  96. package/dist/vite.js +5 -2
  97. package/package.json +34 -7
@@ -26,6 +26,22 @@ export const OPENFORGE_HOST_RUNTIME_SVELTE_SPECIFIERS = Object.freeze(
26
26
  SVELTE_HOST_RUNTIME_MODULES.map(module => module.specifier),
27
27
  )
28
28
 
29
+ const OPENFORGE_HOST_RUNTIME_SVELTE_MODULE_SET = new Set(OPENFORGE_HOST_RUNTIME_SVELTE_SPECIFIERS)
30
+
31
+ /**
32
+ * True for the Svelte host-runtime specifiers the HOST renderer build must externalize.
33
+ *
34
+ * Externalizing these makes the host emit bare `svelte` / `svelte/internal/client` imports
35
+ * that resolve through the injected renderer import map to `plugin://host-runtime/svelte`
36
+ * — the same Svelte instance external plugins load. Host + plugins then share ONE Svelte
37
+ * instance, so mounting an external plugin component from the host tree no longer throws
38
+ * `effect_orphan`. Deliberately Svelte-only: once Svelte is external, host-bundled
39
+ * terminal-runtime resolves Svelte to the shared instance too, so nothing wider is needed.
40
+ */
41
+ export function isOpenForgeHostRuntimeSvelteExternal(id) {
42
+ return OPENFORGE_HOST_RUNTIME_SVELTE_MODULE_SET.has(id)
43
+ }
44
+
29
45
  export const SVELTE_HOST_RUNTIME_IMPORTS = Object.freeze(Object.fromEntries(
30
46
  SVELTE_HOST_RUNTIME_MODULES.map(module => [module.specifier, `${HOST_RUNTIME_SVELTE_BASE_URL}${module.assetPath}`]),
31
47
  ))
@@ -0,0 +1,19 @@
1
+ import type { BackendOpenForgeAPI } from '../types';
2
+ import { type TestingRegistryServices } from './support';
3
+ import type { TestingBackendMethodContribution, TestingBackgroundServiceContribution } from './contracts';
4
+ export declare class TestingBackendServicesFake {
5
+ private readonly services;
6
+ private readonly backendMethods;
7
+ private readonly backgroundServices;
8
+ constructor(services: TestingRegistryServices);
9
+ createApi(): Pick<BackendOpenForgeAPI, 'backend' | 'background'>;
10
+ getSnapshot(): {
11
+ backendMethods: TestingBackendMethodContribution[];
12
+ backgroundServices: TestingBackgroundServiceContribution[];
13
+ };
14
+ captureBackgroundServiceIds(): Set<string>;
15
+ startNewBackgroundServices(existingServices: Set<string>): Promise<void>;
16
+ invokeMethod<TOutput>(method: string, payload?: unknown): Promise<TOutput>;
17
+ private registerBackendMethod;
18
+ private registerBackgroundService;
19
+ }
@@ -0,0 +1,87 @@
1
+ import { assertFunction, createDisposable } from './support';
2
+ export class TestingBackendServicesFake {
3
+ services;
4
+ backendMethods = new Map();
5
+ backgroundServices = new Map();
6
+ constructor(services) {
7
+ this.services = services;
8
+ }
9
+ createApi() {
10
+ return {
11
+ backend: {
12
+ registerMethod: (method, registration) => this.registerBackendMethod(method, registration),
13
+ },
14
+ background: {
15
+ register: (registration) => this.registerBackgroundService(registration),
16
+ },
17
+ };
18
+ }
19
+ getSnapshot() {
20
+ return {
21
+ backendMethods: Array.from(this.backendMethods.values()),
22
+ backgroundServices: Array.from(this.backgroundServices.values()),
23
+ };
24
+ }
25
+ captureBackgroundServiceIds() {
26
+ return new Set(this.backgroundServices.keys());
27
+ }
28
+ async startNewBackgroundServices(existingServices) {
29
+ for (const [key, service] of this.backgroundServices.entries()) {
30
+ if (existingServices.has(key) || service.started)
31
+ continue;
32
+ await service.start();
33
+ service.started = true;
34
+ }
35
+ }
36
+ async invokeMethod(method, payload) {
37
+ const qualifiedId = this.services.localQualifiedId('backend', method);
38
+ this.services.calls.backendInvocations.push({ method, qualifiedId, payload });
39
+ const contribution = this.backendMethods.get(qualifiedId);
40
+ if (!contribution) {
41
+ throw new Error(`Backend method is not registered: ${qualifiedId}`);
42
+ }
43
+ return await contribution.registration.handler(payload);
44
+ }
45
+ registerBackendMethod(method, registration) {
46
+ const qualifiedId = this.services.localQualifiedId('backend', method);
47
+ assertFunction('backend', 'handler', registration.handler);
48
+ this.services.claims.claim('backend', qualifiedId);
49
+ const contribution = {
50
+ id: method.trim(),
51
+ qualifiedId,
52
+ pluginId: this.services.pluginId,
53
+ projectId: this.services.projectId,
54
+ registration,
55
+ };
56
+ this.backendMethods.set(qualifiedId, contribution);
57
+ return createDisposable(() => {
58
+ this.backendMethods.delete(qualifiedId);
59
+ this.services.claims.release('backend', qualifiedId);
60
+ });
61
+ }
62
+ registerBackgroundService(registration) {
63
+ const qualifiedId = this.services.localQualifiedId('background', registration.id);
64
+ if (registration.scope !== 'global' && registration.scope !== 'project' && registration.scope !== 'task') {
65
+ throw new Error('background registration requires scope to be global, project, or task');
66
+ }
67
+ assertFunction('background', 'start', registration.start);
68
+ this.services.claims.claim('background', qualifiedId);
69
+ const contribution = {
70
+ ...registration,
71
+ id: registration.id.trim(),
72
+ qualifiedId,
73
+ pluginId: this.services.pluginId,
74
+ projectId: this.services.projectId,
75
+ started: false,
76
+ };
77
+ this.backgroundServices.set(qualifiedId, contribution);
78
+ return createDisposable(async () => {
79
+ this.backgroundServices.delete(qualifiedId);
80
+ this.services.claims.release('background', qualifiedId);
81
+ if (contribution.started) {
82
+ await contribution.stop?.();
83
+ contribution.started = false;
84
+ }
85
+ });
86
+ }
87
+ }
@@ -0,0 +1,22 @@
1
+ import type { FrontendOpenForgeAPI, OpenForgeCommonAPI } from '../types';
2
+ import { type TestingRegistryServices } from './support';
3
+ import type { TestingCommandContribution, TestingEventListenerContribution } from './contracts';
4
+ export type TestingCommonApi = OpenForgeCommonAPI & Pick<FrontendOpenForgeAPI, 'navigation'>;
5
+ export declare class TestingCommonApiFake {
6
+ private readonly services;
7
+ private readonly commands;
8
+ private readonly eventListeners;
9
+ private readonly eventHandlers;
10
+ private eventListenerSequence;
11
+ constructor(services: TestingRegistryServices);
12
+ createApi(): TestingCommonApi;
13
+ getSnapshot(): {
14
+ commands: TestingCommandContribution[];
15
+ eventListeners: TestingEventListenerContribution[];
16
+ };
17
+ private registerCommand;
18
+ private registerEventListener;
19
+ private invokeCommand;
20
+ private invokeGlobalCommand;
21
+ private emitEvent;
22
+ }
@@ -0,0 +1,274 @@
1
+ import { assertFunction, assertTitle, commandDescriptor, createDisposable, isJsonValue, normalizeAgentCommandMetadata, } from './support';
2
+ export class TestingCommonApiFake {
3
+ services;
4
+ commands = new Map();
5
+ eventListeners = new Map();
6
+ eventHandlers = new Map();
7
+ eventListenerSequence = 0;
8
+ constructor(services) {
9
+ this.services = services;
10
+ }
11
+ createApi() {
12
+ const api = {
13
+ commands: {
14
+ register: (registration) => this.registerCommand(registration),
15
+ invoke: async (id, payload) => this.invokeCommand(id, payload),
16
+ invokeGlobal: async (qualifiedId, payload) => this.invokeGlobalCommand(qualifiedId, payload),
17
+ list: async () => Array.from(this.commands.values()).map(commandDescriptor),
18
+ listCatalog: async () => [],
19
+ },
20
+ events: {
21
+ on: (event, handler) => this.registerEventListener(event, handler, false),
22
+ onGlobal: (qualifiedEvent, handler) => this.registerEventListener(qualifiedEvent, handler, true),
23
+ emit: async (event, payload) => this.emitEvent(event, payload, false),
24
+ emitGlobal: async (qualifiedEvent, payload) => this.emitEvent(qualifiedEvent, payload, true),
25
+ },
26
+ storage: this.services.storage,
27
+ context: {
28
+ getSnapshot: () => this.services.getContextSnapshot(),
29
+ },
30
+ tasks: {
31
+ list: async (request) => {
32
+ const projectId = request?.projectId ?? null;
33
+ const includeDone = request?.includeDone ?? false;
34
+ this.services.calls.taskListRequests.push({ projectId, includeDone });
35
+ return this.services.seededTasks.filter((task) => {
36
+ if (projectId !== null && task.project_id !== projectId)
37
+ return false;
38
+ if (!includeDone && task.status === 'done')
39
+ return false;
40
+ return true;
41
+ });
42
+ },
43
+ get: async () => null,
44
+ create: async (request) => {
45
+ this.services.calls.taskCreations.push(request);
46
+ return {
47
+ id: `mock-task-${this.services.calls.taskCreations.length}`,
48
+ initial_prompt: request.initialPrompt,
49
+ status: 'backlog',
50
+ prompt: null,
51
+ title: null,
52
+ title_source: null,
53
+ title_generated_at: null,
54
+ agent: null,
55
+ permission_mode: null,
56
+ worktree_source: null,
57
+ worktree_branch: null,
58
+ source_ticket_url: null,
59
+ depends_on: request.dependsOn ?? [],
60
+ project_id: request.projectId,
61
+ created_at: 0,
62
+ updated_at: 0,
63
+ };
64
+ },
65
+ // The fake stands in for the host dialog: it records the request and
66
+ // reports a created-but-not-started task, so consumers can assert what
67
+ // they asked for without a UI. Override per test for the other outcomes.
68
+ compose: async (request) => {
69
+ this.services.calls.taskComposes.push(request);
70
+ const task = await api.tasks.create({
71
+ projectId: request.projectId,
72
+ initialPrompt: request.initialPrompt,
73
+ });
74
+ return { task, started: false };
75
+ },
76
+ updateStatus: async (taskId, status) => {
77
+ this.services.calls.taskStatusUpdates.push({ taskId, status });
78
+ },
79
+ listStartPromptContributions: async (projectId) => this.services.startPromptContributions(projectId),
80
+ configureStartPromptContribution: async (request) => {
81
+ this.services.calls.startPromptContributionConfigurations.push(request);
82
+ const contribution = { ...request, ownerPluginId: this.services.pluginId };
83
+ const existing = this.services.startPromptContributions(request.projectId)
84
+ .filter((entry) => entry.id !== request.id
85
+ || (entry.ownerPluginId !== undefined && entry.ownerPluginId !== contribution.ownerPluginId));
86
+ const next = [...existing, contribution].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)
87
+ || a.id.localeCompare(b.id)
88
+ || (a.ownerPluginId ?? '').localeCompare(b.ownerPluginId ?? ''));
89
+ this.services.config.set(`project:${request.projectId}:start_prompt_contributions`, next);
90
+ return next;
91
+ },
92
+ startImplementation: async (request) => {
93
+ this.services.calls.taskImplementationStarts.push(request);
94
+ return {
95
+ taskId: request.taskId,
96
+ workspacePath: '/mock-workspace',
97
+ sessionId: 'mock-session',
98
+ };
99
+ },
100
+ sendFollowUp: async (request) => {
101
+ this.services.calls.taskFollowUps.push(request);
102
+ return {
103
+ taskId: request.taskId,
104
+ sessionId: 'mock-session',
105
+ disposition: 'delivered',
106
+ };
107
+ },
108
+ getWorkspace: async () => null,
109
+ getLatestSession: async () => null,
110
+ },
111
+ projects: {
112
+ list: async () => [],
113
+ get: async () => null,
114
+ },
115
+ fs: {
116
+ readDir: async () => [],
117
+ readFile: async () => ({ type: 'text', content: '', mimeType: null, size: 0 }),
118
+ writeFile: async (request) => {
119
+ this.services.calls.fsWrites.push(request);
120
+ },
121
+ searchFiles: async () => [],
122
+ },
123
+ shell: {
124
+ spawn: async (request) => {
125
+ this.services.calls.shellSpawns.push(request);
126
+ return 0;
127
+ },
128
+ write: async (request) => {
129
+ this.services.calls.shellWrites.push(request);
130
+ },
131
+ resize: async (request) => {
132
+ this.services.calls.shellResizes.push(request);
133
+ },
134
+ kill: async (request) => {
135
+ this.services.calls.shellKills.push(request);
136
+ },
137
+ getBuffer: async (request) => {
138
+ this.services.calls.shellBuffers.push(request);
139
+ return null;
140
+ },
141
+ },
142
+ notifications: {
143
+ notify: async (request) => {
144
+ this.services.calls.notify.push(request);
145
+ },
146
+ },
147
+ attention: {
148
+ listProjects: async () => [],
149
+ },
150
+ system: {
151
+ openUrl: async (url) => {
152
+ this.services.calls.openUrl.push(url);
153
+ },
154
+ writeClipboardText: async (text) => {
155
+ this.services.calls.clipboardWrites.push(text);
156
+ },
157
+ },
158
+ navigation: {
159
+ get: () => this.services.getNavigationSnapshot(),
160
+ navigate: async (request) => {
161
+ this.services.calls.navigationRequests.push(request);
162
+ return this.services.getNavigationSnapshot(request);
163
+ },
164
+ },
165
+ config: {
166
+ get: async (key) => this.services.config.has(`global:${key}`)
167
+ ? this.services.config.get(`global:${key}`)
168
+ : null,
169
+ set: async (key, value) => {
170
+ this.services.config.set(`global:${key}`, value);
171
+ this.services.calls.configWrites.push({ key, value, projectId: null });
172
+ },
173
+ },
174
+ projectConfig: {
175
+ get: async (key, projectId = this.services.projectId ?? '') => this.services.config.has(`project:${projectId}:${key}`)
176
+ ? this.services.config.get(`project:${projectId}:${key}`)
177
+ : null,
178
+ set: async (key, value, projectId = this.services.projectId ?? '') => {
179
+ this.services.config.set(`project:${projectId}:${key}`, value);
180
+ this.services.calls.configWrites.push({ key, value, projectId });
181
+ },
182
+ },
183
+ };
184
+ return api;
185
+ }
186
+ getSnapshot() {
187
+ return {
188
+ commands: Array.from(this.commands.values()),
189
+ eventListeners: Array.from(this.eventListeners.values()),
190
+ };
191
+ }
192
+ registerCommand(registration) {
193
+ const qualifiedId = this.services.localQualifiedId('commands', registration.id);
194
+ assertTitle('commands', registration.title);
195
+ assertFunction('commands', 'handler', registration.handler);
196
+ const agent = normalizeAgentCommandMetadata(registration.agent);
197
+ if (agent && registration.input !== undefined && !isJsonValue(registration.input)) {
198
+ throw new Error('commands registration agent-facing input schema must be a JSON value');
199
+ }
200
+ if (agent && registration.output !== undefined && !isJsonValue(registration.output)) {
201
+ throw new Error('commands registration agent-facing output schema must be a JSON value');
202
+ }
203
+ this.services.claims.claim('commands', qualifiedId);
204
+ const contribution = {
205
+ ...registration,
206
+ agent,
207
+ id: registration.id.trim(),
208
+ title: registration.title.trim(),
209
+ qualifiedId,
210
+ pluginId: this.services.pluginId,
211
+ projectId: this.services.projectId,
212
+ handler: registration.handler,
213
+ };
214
+ this.commands.set(qualifiedId, contribution);
215
+ return createDisposable(() => {
216
+ this.commands.delete(qualifiedId);
217
+ this.services.claims.release('commands', qualifiedId);
218
+ });
219
+ }
220
+ registerEventListener(event, handler, global) {
221
+ const qualifiedId = global ? event : this.services.localQualifiedId('events', event);
222
+ if (qualifiedId.trim().length === 0) {
223
+ throw new Error('events registration requires a non-empty id');
224
+ }
225
+ assertFunction('events', 'handler', handler);
226
+ const handlers = this.eventHandlers.get(qualifiedId) ?? new Set();
227
+ handlers.add(handler);
228
+ this.eventHandlers.set(qualifiedId, handlers);
229
+ const listenerKey = `${qualifiedId}#${++this.eventListenerSequence}`;
230
+ const contribution = {
231
+ id: event,
232
+ qualifiedId,
233
+ pluginId: this.services.pluginId,
234
+ projectId: this.services.projectId,
235
+ handler,
236
+ global,
237
+ };
238
+ this.eventListeners.set(listenerKey, contribution);
239
+ return createDisposable(() => {
240
+ handlers.delete(handler);
241
+ if (handlers.size === 0)
242
+ this.eventHandlers.delete(qualifiedId);
243
+ this.eventListeners.delete(listenerKey);
244
+ });
245
+ }
246
+ async invokeCommand(id, payload) {
247
+ const qualifiedId = this.services.localQualifiedId('commands', id);
248
+ this.services.calls.commandInvocations.push({ id, qualifiedId, payload });
249
+ return this.invokeGlobalCommand(qualifiedId, payload);
250
+ }
251
+ async invokeGlobalCommand(qualifiedId, payload) {
252
+ this.services.calls.globalCommandInvocations.push({ qualifiedId, payload });
253
+ const command = this.commands.get(qualifiedId);
254
+ if (!command)
255
+ throw new Error(`Unknown command: ${qualifiedId}`);
256
+ return await command.handler(payload, {
257
+ taskId: null,
258
+ projectId: command.projectId,
259
+ source: 'plugin',
260
+ });
261
+ }
262
+ async emitEvent(event, payload, global) {
263
+ const qualifiedEvent = global ? event : this.services.localQualifiedId('events', event);
264
+ if (global) {
265
+ this.services.calls.emittedGlobalEvents.push({ qualifiedEvent, payload });
266
+ }
267
+ else {
268
+ this.services.calls.emittedEvents.push({ event, qualifiedEvent, payload });
269
+ }
270
+ for (const handler of Array.from(this.eventHandlers.get(qualifiedEvent) ?? [])) {
271
+ handler(payload);
272
+ }
273
+ }
274
+ }
@@ -0,0 +1,231 @@
1
+ import type { BrowserSurfaceVisualFeedback } from '../browserSurfaces';
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, PluginTaskUISectionRegistration, PluginViewRegistration, ShellSpawnRequest, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskLinkOpenRequest, TaskStartPrefixContext } from '../types';
4
+ import type { Task } from '../domain';
5
+ export type TestingRuntimeScope = 'global' | 'project' | 'task';
6
+ export type TestingRuntimeKind = 'commands' | 'events' | 'views' | 'taskPane' | 'taskUI' | 'settings' | 'backend' | 'background';
7
+ export type TestingMaybePromise<T> = T | Promise<T>;
8
+ export type TestingCommandHandler = (payload?: unknown) => TestingMaybePromise<unknown>;
9
+ export type TestingEventHandler = (payload: unknown) => void;
10
+ export interface TestingOpenForgeApiOptions {
11
+ pluginId?: string;
12
+ projectId?: string | null;
13
+ taskId?: string | null;
14
+ /** The active view key reported by `navigation.get().currentView`. Defaults to `'board'`. */
15
+ viewId?: string;
16
+ packageMetadata?: OpenForgePackageMetadata;
17
+ storage?: PluginStorage;
18
+ /**
19
+ * Tasks returned by `tasks.list`. The mock filters them by the requested
20
+ * `projectId` (when given) and drops `done` tasks unless `includeDone: true`,
21
+ * mirroring the host capability. Defaults to an empty list.
22
+ */
23
+ tasks?: Task[];
24
+ }
25
+ export interface TestingOpenForgeApiCalls {
26
+ commandInvocations: Array<{
27
+ id: string;
28
+ qualifiedId: string;
29
+ payload: unknown;
30
+ }>;
31
+ globalCommandInvocations: Array<{
32
+ qualifiedId: string;
33
+ payload: unknown;
34
+ }>;
35
+ backendInvocations: Array<{
36
+ method: string;
37
+ qualifiedId: string;
38
+ payload: unknown;
39
+ }>;
40
+ emittedEvents: Array<{
41
+ event: string;
42
+ qualifiedEvent: string;
43
+ payload: unknown;
44
+ }>;
45
+ emittedGlobalEvents: Array<{
46
+ qualifiedEvent: string;
47
+ payload: unknown;
48
+ }>;
49
+ openUrl: string[];
50
+ clipboardWrites: string[];
51
+ taskLinkOpenRequests: TaskLinkOpenRequest[];
52
+ navigationRequests: OpenForgeNavigationRequest[];
53
+ notify: NotificationRequest[];
54
+ taskCreations: CreateTaskRequest[];
55
+ taskComposes: ComposeTaskRequest[];
56
+ startPromptContributionConfigurations: ConfigureStartPromptContributionRequest[];
57
+ taskImplementationStarts: StartTaskImplementationRequest[];
58
+ taskFollowUps: SendTaskFollowUpRequest[];
59
+ taskListRequests: Array<{
60
+ projectId: string | null;
61
+ includeDone: boolean;
62
+ }>;
63
+ taskStatusUpdates: Array<{
64
+ taskId: string;
65
+ status: string;
66
+ }>;
67
+ configWrites: Array<{
68
+ key: string;
69
+ value: JsonValue;
70
+ projectId: string | null;
71
+ }>;
72
+ fsWrites: Array<{
73
+ projectId: string;
74
+ path: string;
75
+ content: string;
76
+ }>;
77
+ shellSpawns: ShellSpawnRequest[];
78
+ shellWrites: Array<{
79
+ taskId: string;
80
+ terminalIndex: number;
81
+ data: string;
82
+ }>;
83
+ shellResizes: Array<{
84
+ taskId: string;
85
+ terminalIndex: number;
86
+ cols: number;
87
+ rows: number;
88
+ }>;
89
+ shellKills: Array<{
90
+ taskId: string;
91
+ terminalIndex: number;
92
+ }>;
93
+ shellBuffers: Array<{
94
+ taskId: string;
95
+ terminalIndex: number;
96
+ }>;
97
+ browserSurfaceGetOrCreate: Array<{
98
+ taskId: string;
99
+ id: string;
100
+ initialUrl?: string;
101
+ }>;
102
+ browserSurfaceAttachments: Array<{
103
+ taskId: string;
104
+ id: string;
105
+ element: HTMLElement;
106
+ }>;
107
+ browserSurfaceDetaches: Array<{
108
+ taskId: string;
109
+ id: string;
110
+ }>;
111
+ browserSurfaceDestroys: Array<{
112
+ taskId: string;
113
+ id: string;
114
+ }>;
115
+ browserSurfaceNavigations: Array<{
116
+ taskId: string;
117
+ id: string;
118
+ url: string;
119
+ }>;
120
+ browserSurfaceControls: Array<{
121
+ taskId: string;
122
+ id: string;
123
+ action: 'goBack' | 'goForward' | 'reload' | 'stop';
124
+ }>;
125
+ browserSurfaceSelections: Array<{
126
+ taskId: string;
127
+ id: string;
128
+ }>;
129
+ browserSurfaceFeedbackClears: Array<{
130
+ taskId: string;
131
+ id: string;
132
+ }>;
133
+ browserSurfaceFeedbackReplacements: Array<{
134
+ taskId: string;
135
+ id: string;
136
+ feedback: BrowserSurfaceVisualFeedback[];
137
+ }>;
138
+ browserSurfaceCaptureChecks: Array<{
139
+ taskId: string;
140
+ id: string;
141
+ artifactId: string;
142
+ }>;
143
+ browserSurfaceCaptures: Array<{
144
+ taskId: string;
145
+ id: string;
146
+ }>;
147
+ browserSurfaceCaptureDiscards: Array<{
148
+ taskId: string;
149
+ id: string;
150
+ artifactId: string;
151
+ }>;
152
+ browserSurfaceSessionResets: Array<Record<string, never>>;
153
+ storageGets: Array<{
154
+ scope: TestingRuntimeScope;
155
+ scopeId: string | null;
156
+ key: string;
157
+ }>;
158
+ storageSets: Array<{
159
+ scope: TestingRuntimeScope;
160
+ scopeId: string | null;
161
+ key: string;
162
+ value: JsonValue;
163
+ }>;
164
+ storageDeletes: Array<{
165
+ scope: TestingRuntimeScope;
166
+ scopeId: string | null;
167
+ key: string;
168
+ }>;
169
+ }
170
+ export interface TestingContributionBase {
171
+ id: string;
172
+ qualifiedId: string;
173
+ pluginId: string;
174
+ projectId: string | null;
175
+ }
176
+ export type TestingCommandContribution = TestingContributionBase & CommandRegistration & {
177
+ title: string;
178
+ icon?: string;
179
+ shortcut?: CommandShortcutMetadata;
180
+ handler: TestingCommandHandler;
181
+ };
182
+ export type TestingEventListenerContribution = TestingContributionBase & {
183
+ handler: TestingEventHandler;
184
+ global: boolean;
185
+ };
186
+ export type TestingViewContribution = TestingContributionBase & PluginViewRegistration;
187
+ export type TestingTaskPaneTabContribution = TestingContributionBase & PluginTaskPaneTabRegistration;
188
+ export type TestingTaskUISectionContribution = TestingContributionBase & PluginTaskUISectionRegistration;
189
+ export type TestingSettingsSectionContribution = TestingContributionBase & PluginSettingsSectionRegistration;
190
+ export type TestingBackendMethodContribution = TestingContributionBase & {
191
+ registration: BackendMethodRegistration;
192
+ };
193
+ export type TestingBackgroundServiceContribution = TestingContributionBase & BackgroundServiceRegistration & {
194
+ started: boolean;
195
+ };
196
+ export interface TestingInjectionPointContribution {
197
+ id: string;
198
+ location: InjectionPointLocation;
199
+ }
200
+ export interface TestingTaskStartPrefixProviderContribution {
201
+ id: string;
202
+ title: string;
203
+ order: number;
204
+ provide(context: TaskStartPrefixContext): TestingMaybePromise<string | null>;
205
+ }
206
+ export interface TestingOpenForgeRegistrySnapshot {
207
+ pluginId: string;
208
+ projectId: string | null;
209
+ views: TestingViewContribution[];
210
+ taskPaneTabs: TestingTaskPaneTabContribution[];
211
+ taskUISections: TestingTaskUISectionContribution[];
212
+ settingsSections: TestingSettingsSectionContribution[];
213
+ commands: TestingCommandContribution[];
214
+ eventListeners: TestingEventListenerContribution[];
215
+ backendMethods: TestingBackendMethodContribution[];
216
+ backgroundServices: TestingBackgroundServiceContribution[];
217
+ injectionPoints: TestingInjectionPointContribution[];
218
+ taskStartPrefixProviders: TestingTaskStartPrefixProviderContribution[];
219
+ }
220
+ export type MockFrontendOpenForgeAPI = FrontendOpenForgeAPI & {
221
+ readonly __testing: {
222
+ readonly calls: TestingOpenForgeApiCalls;
223
+ readonly registry: TestingOpenForgeRegistryFake;
224
+ };
225
+ };
226
+ export type MockBackendOpenForgeAPI = BackendOpenForgeAPI & {
227
+ readonly __testing: {
228
+ readonly calls: TestingOpenForgeApiCalls;
229
+ readonly registry: TestingOpenForgeRegistryFake;
230
+ };
231
+ };
@@ -0,0 +1 @@
1
+ export {};