@openforge-app/plugin-sdk 0.2.11 → 0.3.0
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/README.md +220 -8
- package/dist/domain.d.ts +50 -1
- package/dist/fileIcons.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/markdown.js +7 -1
- package/dist/testing/commonApiFake.d.ts +4 -2
- package/dist/testing/commonApiFake.js +213 -3
- package/dist/testing/contracts.d.ts +20 -1
- package/dist/testing/registryFake.d.ts +2 -1
- package/dist/testing/registryFake.js +3 -0
- package/dist/testing/support.d.ts +3 -1
- package/dist/testing/support.js +7 -0
- package/dist/types.d.ts +29 -8
- package/dist/ui/PluginSidebarLink.svelte +1 -7
- package/dist/ui/icons/video.svg +3 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,26 +1,238 @@
|
|
|
1
1
|
# @openforge-app/plugin-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript SDK for building trusted OpenForge plugins. It includes API version 1 types, frontend and backend entry-point helpers, package metadata validation, testing fakes, Vite helpers, and shared Svelte components.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
OpenForge plugins run trusted code. Install plugins only from authors you trust.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @openforge-app/plugin-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Frontend plugins also use the Svelte 5 runtime supplied by OpenForge. Add Svelte as a peer dependency and as a development dependency in the plugin package:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add -D svelte@^5 @sveltejs/vite-plugin-svelte vite
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"svelte": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Package metadata
|
|
28
|
+
|
|
29
|
+
Declare the plugin in `package.json#openforge` and publish every built file named by the metadata:
|
|
6
30
|
|
|
7
31
|
```json
|
|
8
32
|
{
|
|
33
|
+
"name": "@acme/openforge-notes",
|
|
34
|
+
"version": "1.0.0",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"files": ["dist"],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@openforge-app/plugin-sdk": "latest"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"svelte": "^5.0.0"
|
|
42
|
+
},
|
|
9
43
|
"openforge": {
|
|
44
|
+
"id": "acme.notes",
|
|
45
|
+
"apiVersion": 1,
|
|
46
|
+
"displayName": "Notes",
|
|
47
|
+
"description": "Project notes",
|
|
48
|
+
"icon": "notebook-text",
|
|
10
49
|
"frontend": "./dist/frontend.js",
|
|
11
|
-
"frontendStyles": ["./dist/plugin.css"]
|
|
50
|
+
"frontendStyles": ["./dist/plugin.css"],
|
|
51
|
+
"backend": "./dist/backend.mjs",
|
|
52
|
+
"requires": ["views", "storage", "backend"]
|
|
12
53
|
}
|
|
13
54
|
}
|
|
14
55
|
```
|
|
15
56
|
|
|
16
|
-
|
|
57
|
+
`openforge.apiVersion` controls compatibility with the OpenForge host. It is separate from the npm package version. The current host contract uses API version `1`.
|
|
58
|
+
|
|
59
|
+
OpenForge loads built artifacts from the installed package. It does not compile plugin source during installation.
|
|
60
|
+
|
|
61
|
+
## Frontend entry point
|
|
62
|
+
|
|
63
|
+
Use `@openforge-app/plugin-sdk/frontend` for renderer contributions such as views, task UI, settings, commands, and events:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { defineFrontendPlugin } from '@openforge-app/plugin-sdk/frontend'
|
|
67
|
+
|
|
68
|
+
export default defineFrontendPlugin({
|
|
69
|
+
activate(openforge, context) {
|
|
70
|
+
context.subscriptions.add(openforge.views.register({
|
|
71
|
+
id: 'notes',
|
|
72
|
+
title: 'Notes',
|
|
73
|
+
icon: 'notebook-text',
|
|
74
|
+
placement: 'rail',
|
|
75
|
+
component: () => import('./NotesView.svelte'),
|
|
76
|
+
}))
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Add every registration and cleanup handle to `context.subscriptions`. OpenForge disposes them when it deactivates or reloads the plugin.
|
|
82
|
+
|
|
83
|
+
Task reads support project-filtered invalidation subscriptions. Notifications may be coalesced and contain identity plus a reason, not a Task snapshot. Repeat only the bounded read your view needs:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export default defineFrontendPlugin({
|
|
87
|
+
activate(openforge, context) {
|
|
88
|
+
const projectId = openforge.context.getSnapshot().projectId
|
|
89
|
+
if (!projectId) return
|
|
90
|
+
|
|
91
|
+
context.subscriptions.add(openforge.tasks.onDidChange(projectId, (event) => {
|
|
92
|
+
const refresh = event.taskId
|
|
93
|
+
? openforge.tasks.detail(event.projectId, event.taskId)
|
|
94
|
+
: openforge.tasks.active(event.projectId)
|
|
95
|
+
void refresh.then((result) => {
|
|
96
|
+
// Update plugin-owned state from the bounded result.
|
|
97
|
+
}).catch(console.error)
|
|
98
|
+
}))
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The reason is one of `created`, `updated`, `completed`, `attention`, or `execution`. Add the returned disposable to `context.subscriptions`; explicit disposal, deactivation, reload, and uninstall then stop delivery.
|
|
104
|
+
|
|
105
|
+
Frontend bundles must share Svelte with the host. Use the SDK's Vite external helper:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
109
|
+
import { openforgePluginViteExternals } from '@openforge-app/plugin-sdk/vite'
|
|
110
|
+
import { defineConfig } from 'vite'
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
plugins: [svelte()],
|
|
114
|
+
build: {
|
|
115
|
+
lib: {
|
|
116
|
+
entry: 'src/frontend.ts',
|
|
117
|
+
formats: ['es'],
|
|
118
|
+
fileName: 'frontend',
|
|
119
|
+
},
|
|
120
|
+
rollupOptions: {
|
|
121
|
+
external: openforgePluginViteExternals,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Svelte library builds emit CSS separately. List each emitted CSS file in `openforge.frontendStyles`; otherwise OpenForge will not load it.
|
|
128
|
+
|
|
129
|
+
## Backend entry point
|
|
130
|
+
|
|
131
|
+
Use `@openforge-app/plugin-sdk/backend` for Node dependencies, plugin-local RPC methods, and background services:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { defineBackendPlugin } from '@openforge-app/plugin-sdk/backend'
|
|
135
|
+
|
|
136
|
+
export default defineBackendPlugin({
|
|
137
|
+
activate(openforge, context) {
|
|
138
|
+
context.subscriptions.add(openforge.backend.registerMethod('ping', {
|
|
139
|
+
input: { type: 'object', additionalProperties: false },
|
|
140
|
+
output: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
required: ['ok'],
|
|
143
|
+
properties: { ok: { type: 'boolean' } },
|
|
144
|
+
},
|
|
145
|
+
handler: async () => ({ ok: true }),
|
|
146
|
+
}))
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Backend plugins run in the trusted Node plugin host. They cannot register Svelte views, task UI, settings sections, or frontend navigation.
|
|
152
|
+
|
|
153
|
+
## Testing
|
|
154
|
+
|
|
155
|
+
`@openforge-app/plugin-sdk/testing` provides mock APIs and a registry fake for registration, capability, storage, and cleanup tests:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { describe, expect, it } from 'vitest'
|
|
159
|
+
import { createOpenForgeRegistryFake } from '@openforge-app/plugin-sdk/testing'
|
|
160
|
+
import plugin from '../src/frontend'
|
|
161
|
+
|
|
162
|
+
describe('frontend plugin', () => {
|
|
163
|
+
it('registers its view', async () => {
|
|
164
|
+
const registry = createOpenForgeRegistryFake({
|
|
165
|
+
pluginId: 'acme.notes',
|
|
166
|
+
projectId: 'P-1',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
await registry.activateFrontend(plugin)
|
|
170
|
+
|
|
171
|
+
expect(registry.snapshot.views).toMatchObject([
|
|
172
|
+
{ id: 'notes', qualifiedId: 'acme.notes.notes', title: 'Notes' },
|
|
173
|
+
])
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Use `registry.emitTaskChange(...)` to drive invalidation behavior without importing host events or renderer stores:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
registry.emitTaskChange({
|
|
182
|
+
projectId: 'P-1',
|
|
183
|
+
taskId: 'T-42',
|
|
184
|
+
reason: 'updated',
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Public entry points
|
|
189
|
+
|
|
190
|
+
The package exports these modules:
|
|
191
|
+
|
|
192
|
+
| Import | Purpose |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `@openforge-app/plugin-sdk` | Shared types, metadata validation, constants, and domain helpers |
|
|
195
|
+
| `@openforge-app/plugin-sdk/frontend` | Frontend plugin definition and renderer types |
|
|
196
|
+
| `@openforge-app/plugin-sdk/backend` | Backend plugin definition and Node-host types |
|
|
197
|
+
| `@openforge-app/plugin-sdk/testing` | Mock APIs, storage, subscriptions, and registry fakes |
|
|
198
|
+
| `@openforge-app/plugin-sdk/vite` | Host-runtime externals for plugin builds |
|
|
199
|
+
| `@openforge-app/plugin-sdk/package-metadata-schema.json` | JSON Schema for `package.json#openforge` |
|
|
200
|
+
| `@openforge-app/plugin-sdk/domain` | Shared OpenForge domain types |
|
|
201
|
+
| `@openforge-app/plugin-sdk/prStatusPresentation` | Pull request status presentation helpers |
|
|
202
|
+
| `@openforge-app/plugin-sdk/markdown` | Markdown rendering helpers |
|
|
203
|
+
| `@openforge-app/plugin-sdk/numberParsing` | Numeric parsing helpers |
|
|
204
|
+
| `@openforge-app/plugin-sdk/projectFileTree` | Project file tree helpers |
|
|
205
|
+
| `@openforge-app/plugin-sdk/sanitize` | HTML and SVG sanitization helpers |
|
|
206
|
+
| `@openforge-app/plugin-sdk/pluginIcons` | Plugin icon validation and sanitization |
|
|
207
|
+
| `@openforge-app/plugin-sdk/fileIcons` | File-type icon lookup helpers |
|
|
208
|
+
| `@openforge-app/plugin-sdk/collapsibleSectionState` | Persisted collapse-state helpers |
|
|
209
|
+
| `@openforge-app/plugin-sdk/taskBrowserDevToolsShortcuts` | Task Browser DevTools shortcut helpers |
|
|
210
|
+
|
|
211
|
+
Shared Svelte components use explicit imports. The package exports:
|
|
212
|
+
|
|
213
|
+
- `@openforge-app/plugin-sdk/ui/Button.svelte`
|
|
214
|
+
- `@openforge-app/plugin-sdk/ui/Checkbox.svelte`
|
|
215
|
+
- `@openforge-app/plugin-sdk/ui/CollapsibleSection.svelte`
|
|
216
|
+
- `@openforge-app/plugin-sdk/ui/FileTypeIcon.svelte`
|
|
217
|
+
- `@openforge-app/plugin-sdk/ui/MarkdownContent.svelte`
|
|
218
|
+
- `@openforge-app/plugin-sdk/ui/Modal.svelte`
|
|
219
|
+
- `@openforge-app/plugin-sdk/ui/PluginPageHeader.svelte`
|
|
220
|
+
- `@openforge-app/plugin-sdk/ui/PluginPageShell.svelte`
|
|
221
|
+
- `@openforge-app/plugin-sdk/ui/PluginSidebarLink.svelte`
|
|
222
|
+
- `@openforge-app/plugin-sdk/ui/PluginViewState.svelte`
|
|
223
|
+
- `@openforge-app/plugin-sdk/ui/ProjectFileTree.svelte`
|
|
224
|
+
- `@openforge-app/plugin-sdk/ui/ResizablePanel.svelte`
|
|
225
|
+
Use only documented package exports. Do not import OpenForge renderer stores, Electron or preload APIs, Rust internals, app IPC wrappers, or files under this package's `src/` directory.
|
|
226
|
+
|
|
227
|
+
## Documentation
|
|
17
228
|
|
|
18
|
-
|
|
229
|
+
- [Plugin authoring guide](https://github.com/koenvg/openforge/blob/main/docs/plugin-authoring.md)
|
|
230
|
+
- [SDK reference](https://github.com/koenvg/openforge/blob/main/docs/plugins/sdk-reference.md)
|
|
19
231
|
|
|
20
|
-
|
|
232
|
+
The authoring guide documents package metadata, capabilities, frontend and backend runtime boundaries, storage, task APIs, browser surfaces, and CSS loading. The SDK reference lists the public types and shared UI components.
|
|
21
233
|
|
|
22
234
|
## License
|
|
23
235
|
|
|
24
|
-
|
|
236
|
+
The SDK is available under the [MIT License](./LICENSE). Plugins may use, modify, and redistribute it under those terms, including in commercial and private plugins.
|
|
25
237
|
|
|
26
|
-
|
|
238
|
+
The OpenForge desktop application has a separate license. The SDK license does not grant permission to commercially resell or redistribute the app.
|
package/dist/domain.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export type WritableBoardStatus = Exclude<BoardStatus, 'done'>;
|
|
|
14
14
|
export type WorktreeSource = 'newBranchFromMain' | 'existingBranch' | 'disabled';
|
|
15
15
|
export interface Task {
|
|
16
16
|
id: string;
|
|
17
|
+
/** @deprecated Use `TaskDetail.prompt` in the bounded Task read APIs. */
|
|
17
18
|
initial_prompt: string;
|
|
18
19
|
status: BoardStatus;
|
|
20
|
+
/** @deprecated Legacy execution override retained for API version 1 compatibility. */
|
|
19
21
|
prompt: string | null;
|
|
20
22
|
/** Explicit display title; null means fall back to the prompt-derived title. */
|
|
21
23
|
title: string | null;
|
|
@@ -37,6 +39,53 @@ export interface Task {
|
|
|
37
39
|
created_at: number;
|
|
38
40
|
updated_at: number;
|
|
39
41
|
}
|
|
42
|
+
export interface TaskLabel {
|
|
43
|
+
id: number;
|
|
44
|
+
projectId: string;
|
|
45
|
+
name: string;
|
|
46
|
+
}
|
|
47
|
+
export interface TaskReference {
|
|
48
|
+
id: string;
|
|
49
|
+
status: BoardStatus;
|
|
50
|
+
projectId: string;
|
|
51
|
+
title: string;
|
|
52
|
+
dependsOn: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface TaskSummary extends TaskReference {
|
|
55
|
+
createdAt: number;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
promptPreview: string;
|
|
58
|
+
labels: TaskLabel[];
|
|
59
|
+
sourceTicketUrl: string | null;
|
|
60
|
+
}
|
|
61
|
+
export interface TaskDetail extends TaskSummary {
|
|
62
|
+
prompt: string;
|
|
63
|
+
agent: string | null;
|
|
64
|
+
permissionMode: string | null;
|
|
65
|
+
worktreeSource: WorktreeSource | null;
|
|
66
|
+
worktreeBranch: string | null;
|
|
67
|
+
titleSource: 'manual' | 'generated' | null;
|
|
68
|
+
titleGeneratedAt: number | null;
|
|
69
|
+
}
|
|
70
|
+
export interface ActiveTasks {
|
|
71
|
+
tasks: TaskDetail[];
|
|
72
|
+
related: TaskReference[];
|
|
73
|
+
}
|
|
74
|
+
export interface CompletedTaskQuery {
|
|
75
|
+
/** At most 200 Unicode characters. ASCII case-insensitive; non-ASCII casing is exact. */
|
|
76
|
+
search?: string;
|
|
77
|
+
/** At most 20 Task Label names; each trimmed name is at most 40 Unicode characters. */
|
|
78
|
+
labels?: string[];
|
|
79
|
+
cursor?: string | null;
|
|
80
|
+
}
|
|
81
|
+
export interface CompletedTaskPage {
|
|
82
|
+
tasks: TaskSummary[];
|
|
83
|
+
nextCursor: string | null;
|
|
84
|
+
}
|
|
85
|
+
export interface TaskRead {
|
|
86
|
+
task: TaskDetail;
|
|
87
|
+
related: TaskReference[];
|
|
88
|
+
}
|
|
40
89
|
export interface GitBranchInfo {
|
|
41
90
|
name: string;
|
|
42
91
|
is_current: boolean;
|
|
@@ -601,7 +650,7 @@ export interface FileEntry {
|
|
|
601
650
|
}
|
|
602
651
|
/** File content with type information */
|
|
603
652
|
export interface FileContent {
|
|
604
|
-
type: 'text' | 'image' | 'binary' | 'document' | 'large-file';
|
|
653
|
+
type: 'text' | 'image' | 'video' | 'binary' | 'document' | 'large-file';
|
|
605
654
|
content: string;
|
|
606
655
|
mimeType: string | null;
|
|
607
656
|
size: number;
|
package/dist/fileIcons.js
CHANGED
|
@@ -35,6 +35,7 @@ const EXTENSION_ICONS = {
|
|
|
35
35
|
svg: 'svg',
|
|
36
36
|
png: 'image', jpg: 'image', jpeg: 'image', gif: 'image',
|
|
37
37
|
webp: 'image', bmp: 'image', ico: 'image', avif: 'image',
|
|
38
|
+
mp4: 'video', m4v: 'video', webm: 'video', ogv: 'video', ogg: 'video', mov: 'video',
|
|
38
39
|
pdf: 'pdf',
|
|
39
40
|
zip: 'zip', tar: 'zip', gz: 'zip', tgz: 'zip', rar: 'zip', '7z': 'zip',
|
|
40
41
|
lock: 'lock',
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSur
|
|
|
3
3
|
export { OPENFORGE_PACKAGE_METADATA_SCHEMA, OPENFORGE_PLUGIN_CAPABILITIES, isOpenForgePackageMetadata, isPluginPackageMetadata, isSupportedOpenForgeApiVersion, validateOpenForgePackageMetadata, validatePluginPackageMetadata, } from './manifest';
|
|
4
4
|
export { createMemoryPluginStorage, createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, createTestingCalls, TestingOpenForgeRegistryFake, TestingSubscriptionSink, } from './testing';
|
|
5
5
|
export { MAX_AGENT_SESSION_PAGE_SIZE, DEFAULT_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES, MAX_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES, MIN_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES, TaskFollowUpError, MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, resolveExternalTextFileChunkSize, makePluginViewKey, parsePluginViewKey, } from './types';
|
|
6
|
-
export type { AgentSessionCursor, AgentSessionOverlap, AgentSessionsAPI, AgentSessionSummary, AgentSessionSummaryPage, AgentSessionTaskSummary, AgentSessionWorkspace, AgentCommandDescriptor, AgentCommandMetadata, AgentCommandRuntime, AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, BackendFileSystemAPI, ExternalFileMetadata, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadTextFileChunksRequest, ExternalReadFileSystemAPI, FileSystemAPI, UserDataDirectoryRequest, UserDataFileAppendResult, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, ImplementationRun, ListAgentSessionsRequest, ListTaskSessionsRequest, InjectionPointLocation, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, OpenForgeContextChangeHandler, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, OpenForgePluginCapability, OpenForgePluginContext, OpenForgePluginPackageJson, PluginCommandInvocationContext, PluginCommandInvocationSource, PluginComponentLoader, PluginComponentModule, PluginEntry, PluginIcon, PluginSidebarNavigationProps, PluginSidebarViewIdentity, PluginInjectionPointProps, PluginSettingsSectionProps, PluginState, PluginSvgIcon, PluginTaskPaneProps, PluginTaskUISectionProps, PluginViewProps, PluginStorage, PluginStorageScope, PluginViewKey, ProjectsAPI, PtyBufferState, ShellAPI, ShellResizeRequest, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol, TerminalViewSnapshot, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskFollowUpDisposition, TaskFollowUpErrorCode, TaskFollowUpReceipt, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TasksAPI, ValidationError, } from './types';
|
|
6
|
+
export type { AgentSessionCursor, AgentSessionOverlap, AgentSessionsAPI, AgentSessionSummary, AgentSessionSummaryPage, AgentSessionTaskSummary, AgentSessionWorkspace, AgentCommandDescriptor, AgentCommandMetadata, AgentCommandRuntime, AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, BackendFileSystemAPI, ExternalFileMetadata, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadTextFileChunksRequest, ExternalReadFileSystemAPI, FileSystemAPI, UserDataDirectoryRequest, UserDataFileAppendResult, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, ImplementationRun, ListAgentSessionsRequest, ListTaskSessionsRequest, InjectionPointLocation, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, OpenForgeContextChangeHandler, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, OpenForgePluginCapability, OpenForgePluginContext, OpenForgePluginPackageJson, PluginCommandInvocationContext, PluginCommandInvocationSource, PluginComponentLoader, PluginComponentModule, PluginEntry, PluginIcon, PluginSidebarNavigationProps, PluginSidebarViewIdentity, PluginInjectionPointProps, PluginSettingsSectionProps, PluginState, PluginSvgIcon, PluginTaskPaneProps, PluginTaskUISectionProps, PluginViewProps, PluginStorage, PluginStorageScope, PluginViewKey, ProjectsAPI, PtyBufferState, ShellAPI, ShellResizeRequest, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol, TerminalViewSnapshot, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskFollowUpDisposition, TaskFollowUpErrorCode, TaskFollowUpReceipt, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TaskChangeEvent, TaskChangeReason, TaskOperationsAPI, TasksAPI, ValidationError, } from './types';
|
|
7
7
|
export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingExternalTextFile, TestingExternalTextFileChunksCall, TestingEventListenerContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingTaskUISectionContribution, TestingViewContribution, } from './testing';
|
|
8
8
|
export { parseStrictFiniteNumber } from './numberParsing';
|
|
9
9
|
export { buildProjectFileTree, flattenVisibleProjectFileTree, formatProjectFileTreeSize, getProjectFileTreeDepth, getProjectFileTreeItemAccessibility, getProjectFileTreeKeyboardAction, getProjectFileTreeParentPath, hasProjectFileTreeShortcutModifier, projectFileTreePathToId, } from './projectFileTree';
|
package/dist/markdown.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { marked } from 'marked';
|
|
1
|
+
import { marked, Renderer } from 'marked';
|
|
2
2
|
import { sanitizeHtml } from './sanitize';
|
|
3
3
|
/** Attribute holding the original URL of a `<img>`/`<a>` awaiting resolution. */
|
|
4
4
|
export const MARKDOWN_REMOTE_MEDIA_ATTRIBUTE = 'data-markdown-remote-src';
|
|
5
|
+
class MarkdownRenderer extends Renderer {
|
|
6
|
+
table(token) {
|
|
7
|
+
return `<div class="markdown-table-scroll">${super.table(token)}</div>`;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
5
10
|
const markedOptions = {
|
|
6
11
|
gfm: true,
|
|
7
12
|
breaks: true,
|
|
13
|
+
renderer: new MarkdownRenderer(),
|
|
8
14
|
};
|
|
9
15
|
const RENDERED_MARKDOWN_CACHE_CAPACITY = 100;
|
|
10
16
|
const renderedMarkdownCache = new Map();
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import type { BackendOpenForgeAPI, FrontendOpenForgeAPI, OpenForgeCommonAPI } from '../types';
|
|
1
|
+
import type { BackendOpenForgeAPI, FrontendOpenForgeAPI, TaskChangeEvent, OpenForgeCommonAPI } from '../types';
|
|
2
2
|
import { type TestingRegistryServices } from './support.js';
|
|
3
3
|
import type { TestingCommandContribution, TestingEventListenerContribution } from './contracts';
|
|
4
|
-
export type TestingCommonApi = OpenForgeCommonAPI & Pick<FrontendOpenForgeAPI, 'navigation'>;
|
|
4
|
+
export type TestingCommonApi = Omit<OpenForgeCommonAPI, 'tasks'> & Pick<FrontendOpenForgeAPI, 'tasks' | 'navigation'>;
|
|
5
5
|
export declare class TestingCommonApiFake {
|
|
6
6
|
private readonly services;
|
|
7
7
|
private readonly commands;
|
|
8
8
|
private readonly eventListeners;
|
|
9
9
|
private readonly eventHandlers;
|
|
10
|
+
private readonly taskChangeHandlers;
|
|
10
11
|
private eventListenerSequence;
|
|
11
12
|
constructor(services: TestingRegistryServices);
|
|
13
|
+
emitTaskChange(event: TaskChangeEvent): void;
|
|
12
14
|
createApi(): TestingCommonApi;
|
|
13
15
|
createBackendApi(): TestingCommonApi & Pick<BackendOpenForgeAPI, 'fs'>;
|
|
14
16
|
getSnapshot(): {
|
|
@@ -115,15 +115,172 @@ function* splitExternalTextFile(content, maxBytes) {
|
|
|
115
115
|
if (chunk.length > 0)
|
|
116
116
|
yield chunk;
|
|
117
117
|
}
|
|
118
|
+
function isTestingImageReferenceDefinition(line) {
|
|
119
|
+
const separator = line.indexOf(':');
|
|
120
|
+
if (separator < 0)
|
|
121
|
+
return false;
|
|
122
|
+
const marker = line.slice(0, separator);
|
|
123
|
+
const value = line.slice(separator + 1);
|
|
124
|
+
const imageNumber = marker.startsWith('[image#') && marker.endsWith(']')
|
|
125
|
+
? marker.slice('[image#'.length, -1)
|
|
126
|
+
: null;
|
|
127
|
+
return imageNumber !== null
|
|
128
|
+
&& imageNumber.length > 0
|
|
129
|
+
&& /^\d+$/u.test(imageNumber)
|
|
130
|
+
&& value.trimStart().startsWith('data:image/')
|
|
131
|
+
&& value.includes(';base64,');
|
|
132
|
+
}
|
|
133
|
+
function testingTaskPromptPreview(task) {
|
|
134
|
+
const lines = task.initial_prompt
|
|
135
|
+
.split(/\r?\n/u)
|
|
136
|
+
.filter(line => !isTestingImageReferenceDefinition(line));
|
|
137
|
+
while (lines.at(-1)?.trim() === '')
|
|
138
|
+
lines.pop();
|
|
139
|
+
return [...lines.join('\n')].slice(0, 120).join('');
|
|
140
|
+
}
|
|
141
|
+
function testingTaskTitle(task, preview) {
|
|
142
|
+
const explicitTitle = task.title?.trim();
|
|
143
|
+
if (explicitTitle)
|
|
144
|
+
return explicitTitle;
|
|
145
|
+
const fallback = preview.split(/\r?\n/u).map(line => line.trim()).find(Boolean) || task.id;
|
|
146
|
+
return [...fallback].slice(0, 120).join('');
|
|
147
|
+
}
|
|
148
|
+
function taskReference(task) {
|
|
149
|
+
const preview = testingTaskPromptPreview(task);
|
|
150
|
+
if (!task.project_id)
|
|
151
|
+
throw new Error(`Task ${task.id} must belong to a project`);
|
|
152
|
+
return {
|
|
153
|
+
id: task.id,
|
|
154
|
+
status: task.status,
|
|
155
|
+
projectId: task.project_id,
|
|
156
|
+
title: testingTaskTitle(task, preview),
|
|
157
|
+
dependsOn: [...task.depends_on],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function taskSummary(task, labels = []) {
|
|
161
|
+
return {
|
|
162
|
+
...taskReference(task),
|
|
163
|
+
createdAt: task.created_at,
|
|
164
|
+
updatedAt: task.updated_at,
|
|
165
|
+
promptPreview: testingTaskPromptPreview(task),
|
|
166
|
+
labels: [...labels],
|
|
167
|
+
sourceTicketUrl: task.source_ticket_url,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function taskDetail(task, labels = []) {
|
|
171
|
+
return {
|
|
172
|
+
...taskSummary(task, labels),
|
|
173
|
+
prompt: task.initial_prompt,
|
|
174
|
+
agent: task.agent,
|
|
175
|
+
permissionMode: task.permission_mode,
|
|
176
|
+
worktreeSource: task.worktree_source,
|
|
177
|
+
worktreeBranch: task.worktree_branch,
|
|
178
|
+
titleSource: task.title_source,
|
|
179
|
+
titleGeneratedAt: task.title_generated_at,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function asciiLowercase(value) {
|
|
183
|
+
return value.replace(/[A-Z]/gu, character => character.toLowerCase());
|
|
184
|
+
}
|
|
185
|
+
function compareCompletedTasks(left, right) {
|
|
186
|
+
if (left.updated_at !== right.updated_at)
|
|
187
|
+
return right.updated_at - left.updated_at;
|
|
188
|
+
if (left.id === right.id)
|
|
189
|
+
return 0;
|
|
190
|
+
return left.id > right.id ? -1 : 1;
|
|
191
|
+
}
|
|
192
|
+
function testingCompletedTaskScope(projectId, query) {
|
|
193
|
+
return {
|
|
194
|
+
projectId,
|
|
195
|
+
search: asciiLowercase(query.search?.trim() ?? ''),
|
|
196
|
+
labels: [...new Set((query.labels ?? [])
|
|
197
|
+
.map(name => name.trim().toLowerCase())
|
|
198
|
+
.filter(Boolean))].sort(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function encodeTestingCompletedTaskCursor(cursor) {
|
|
202
|
+
return `testing:${encodeURIComponent(JSON.stringify(cursor))}`;
|
|
203
|
+
}
|
|
204
|
+
function decodeTestingCompletedTaskCursor(encoded, scope) {
|
|
205
|
+
try {
|
|
206
|
+
if (!encoded.startsWith('testing:'))
|
|
207
|
+
throw new Error('wrong cursor format');
|
|
208
|
+
const cursor = JSON.parse(decodeURIComponent(encoded.slice('testing:'.length)));
|
|
209
|
+
if (cursor.version !== 1
|
|
210
|
+
|| !Number.isSafeInteger(cursor.updatedAt)
|
|
211
|
+
|| typeof cursor.id !== 'string'
|
|
212
|
+
|| JSON.stringify(cursor.scope) !== JSON.stringify(scope)) {
|
|
213
|
+
throw new Error('invalid cursor payload');
|
|
214
|
+
}
|
|
215
|
+
return cursor;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new Error('Invalid Task cursor');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function listTestingCompletedTasks(allTasks, projectId, query = {}, labelsByTaskId = new Map()) {
|
|
222
|
+
if (!projectId.trim())
|
|
223
|
+
throw new RangeError('projectId is required');
|
|
224
|
+
const submittedLabels = query.labels ?? [];
|
|
225
|
+
if (submittedLabels.length > 20) {
|
|
226
|
+
throw new RangeError('Completed Task reads support at most 20 Task Label filters');
|
|
227
|
+
}
|
|
228
|
+
if (submittedLabels.some(name => [...name.trim()].length > 40)) {
|
|
229
|
+
throw new RangeError('Completed Task Label filters must be 40 characters or fewer');
|
|
230
|
+
}
|
|
231
|
+
if ([...(query.search?.trim() ?? '')].length > 200) {
|
|
232
|
+
throw new RangeError('Completed Task search must be 200 characters or fewer');
|
|
233
|
+
}
|
|
234
|
+
const scope = testingCompletedTaskScope(projectId, query);
|
|
235
|
+
const labels = new Set(scope.labels);
|
|
236
|
+
const cursor = query.cursor ? decodeTestingCompletedTaskCursor(query.cursor, scope) : null;
|
|
237
|
+
const matching = allTasks
|
|
238
|
+
.filter(task => task.status === 'done' && task.project_id === projectId)
|
|
239
|
+
.filter(task => {
|
|
240
|
+
const summary = taskSummary(task, labelsByTaskId.get(task.id));
|
|
241
|
+
return !scope.search || [summary.id, summary.title, summary.promptPreview]
|
|
242
|
+
.some(value => asciiLowercase(value).includes(scope.search));
|
|
243
|
+
})
|
|
244
|
+
.filter(task => {
|
|
245
|
+
if (labels.size === 0)
|
|
246
|
+
return true;
|
|
247
|
+
const names = taskSummary(task, labelsByTaskId.get(task.id))
|
|
248
|
+
.labels.map(label => label.name.toLowerCase());
|
|
249
|
+
return [...labels].every(label => names.includes(label));
|
|
250
|
+
})
|
|
251
|
+
.sort(compareCompletedTasks);
|
|
252
|
+
const remaining = cursor
|
|
253
|
+
? matching.filter(task => task.updated_at < cursor.updatedAt
|
|
254
|
+
|| (task.updated_at === cursor.updatedAt && task.id < cursor.id))
|
|
255
|
+
: matching;
|
|
256
|
+
const pageTasks = remaining.slice(0, 50);
|
|
257
|
+
const tasks = pageTasks.map(task => taskSummary(task, labelsByTaskId.get(task.id)));
|
|
258
|
+
const last = pageTasks.at(-1);
|
|
259
|
+
const nextCursor = remaining.length > 50 && last
|
|
260
|
+
? encodeTestingCompletedTaskCursor({
|
|
261
|
+
version: 1,
|
|
262
|
+
scope,
|
|
263
|
+
updatedAt: last.updated_at,
|
|
264
|
+
id: last.id,
|
|
265
|
+
})
|
|
266
|
+
: null;
|
|
267
|
+
return { tasks, nextCursor };
|
|
268
|
+
}
|
|
118
269
|
export class TestingCommonApiFake {
|
|
119
270
|
services;
|
|
120
271
|
commands = new Map();
|
|
121
272
|
eventListeners = new Map();
|
|
122
273
|
eventHandlers = new Map();
|
|
274
|
+
taskChangeHandlers = new Map();
|
|
123
275
|
eventListenerSequence = 0;
|
|
124
276
|
constructor(services) {
|
|
125
277
|
this.services = services;
|
|
126
278
|
}
|
|
279
|
+
emitTaskChange(event) {
|
|
280
|
+
for (const handler of this.taskChangeHandlers.get(event.projectId) ?? []) {
|
|
281
|
+
handler(event);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
127
284
|
createApi() {
|
|
128
285
|
const api = {
|
|
129
286
|
commands: {
|
|
@@ -238,6 +395,16 @@ export class TestingCommonApiFake {
|
|
|
238
395
|
},
|
|
239
396
|
},
|
|
240
397
|
tasks: {
|
|
398
|
+
onDidChange: (projectId, handler) => {
|
|
399
|
+
const handlers = this.taskChangeHandlers.get(projectId) ?? new Set();
|
|
400
|
+
handlers.add(handler);
|
|
401
|
+
this.taskChangeHandlers.set(projectId, handlers);
|
|
402
|
+
return createDisposable(() => {
|
|
403
|
+
handlers.delete(handler);
|
|
404
|
+
if (handlers.size === 0)
|
|
405
|
+
this.taskChangeHandlers.delete(projectId);
|
|
406
|
+
});
|
|
407
|
+
},
|
|
241
408
|
list: async (request) => {
|
|
242
409
|
const projectId = request?.projectId ?? null;
|
|
243
410
|
const includeDone = request?.includeDone ?? false;
|
|
@@ -245,12 +412,54 @@ export class TestingCommonApiFake {
|
|
|
245
412
|
return this.services.seededTasks.filter((task) => {
|
|
246
413
|
if (projectId !== null && task.project_id !== projectId)
|
|
247
414
|
return false;
|
|
248
|
-
if (!includeDone && task.status === 'done')
|
|
415
|
+
if (projectId !== null && !includeDone && task.status === 'done')
|
|
249
416
|
return false;
|
|
250
417
|
return true;
|
|
251
418
|
});
|
|
252
419
|
},
|
|
253
|
-
get: async () => null,
|
|
420
|
+
get: async (taskId) => this.services.seededTasks.find(task => task.id === taskId) ?? null,
|
|
421
|
+
active: async (projectId) => {
|
|
422
|
+
this.services.calls.taskActiveRequests.push({ projectId });
|
|
423
|
+
const activeTasks = this.services.seededTasks.filter(task => task.project_id === projectId && task.status !== 'done');
|
|
424
|
+
const activeIds = new Set(activeTasks.map(task => task.id));
|
|
425
|
+
const relatedIds = new Set();
|
|
426
|
+
for (const task of this.services.seededTasks) {
|
|
427
|
+
if (activeIds.has(task.id)) {
|
|
428
|
+
for (const dependencyId of task.depends_on)
|
|
429
|
+
relatedIds.add(dependencyId);
|
|
430
|
+
}
|
|
431
|
+
else if (task.depends_on.some(dependencyId => activeIds.has(dependencyId))) {
|
|
432
|
+
relatedIds.add(task.id);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
tasks: activeTasks.map(task => taskDetail(task, this.services.seededTaskLabelAssignments.get(task.id))),
|
|
437
|
+
related: this.services.seededTasks
|
|
438
|
+
.filter(task => relatedIds.has(task.id) && !activeIds.has(task.id))
|
|
439
|
+
.map(taskReference),
|
|
440
|
+
};
|
|
441
|
+
},
|
|
442
|
+
completed: async (projectId, query = {}) => {
|
|
443
|
+
this.services.calls.taskCompletedRequests.push({ projectId, ...query });
|
|
444
|
+
return listTestingCompletedTasks(this.services.seededTasks, projectId, query, this.services.seededTaskLabelAssignments);
|
|
445
|
+
},
|
|
446
|
+
detail: async (projectId, taskId) => {
|
|
447
|
+
this.services.calls.taskDetailRequests.push({ projectId, taskId });
|
|
448
|
+
const task = this.services.seededTasks.find(candidate => candidate.id === taskId && candidate.project_id === projectId);
|
|
449
|
+
if (!task)
|
|
450
|
+
return null;
|
|
451
|
+
const relatedIds = new Set(task.depends_on);
|
|
452
|
+
for (const candidate of this.services.seededTasks) {
|
|
453
|
+
if (candidate.depends_on.includes(taskId))
|
|
454
|
+
relatedIds.add(candidate.id);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
task: taskDetail(task, this.services.seededTaskLabelAssignments.get(task.id)),
|
|
458
|
+
related: this.services.seededTasks
|
|
459
|
+
.filter(candidate => relatedIds.has(candidate.id))
|
|
460
|
+
.map(taskReference),
|
|
461
|
+
};
|
|
462
|
+
},
|
|
254
463
|
create: async (request) => {
|
|
255
464
|
this.services.calls.taskCreations.push(request);
|
|
256
465
|
return {
|
|
@@ -334,7 +543,8 @@ export class TestingCommonApiFake {
|
|
|
334
543
|
},
|
|
335
544
|
fs: {
|
|
336
545
|
readDir: async () => [],
|
|
337
|
-
readFile: async (
|
|
546
|
+
readFile: async ({ path }) => this.services.projectFileContents[path]
|
|
547
|
+
?? { type: 'text', content: '', mimeType: null, size: 0 },
|
|
338
548
|
writeFile: async (request) => {
|
|
339
549
|
this.services.calls.fsWrites.push(request);
|
|
340
550
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { BrowserSurfaceVisualFeedback } from '../browserSurfaces';
|
|
2
|
+
import type { CompletedTaskQuery } from '../domain';
|
|
2
3
|
import type { TestingOpenForgeRegistryFake } from './registryFake';
|
|
3
4
|
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
|
+
import type { AgentSession, FileContent, Task, TaskLabel } from '../domain';
|
|
5
6
|
export type TestingRuntimeScope = 'global' | 'project' | 'task';
|
|
6
7
|
export type TestingRuntimeKind = 'commands' | 'events' | 'views' | 'taskPane' | 'taskUI' | 'reviewUI' | 'settings' | 'backend' | 'background';
|
|
7
8
|
export type TestingMaybePromise<T> = T | Promise<T>;
|
|
@@ -16,6 +17,10 @@ export interface TestingExternalTextFile extends ExternalReadFileRequest {
|
|
|
16
17
|
export type TestingExternalTextFileChunksCall = Omit<ExternalReadTextFileChunksRequest, 'signal' | 'chunkSizeBytes'> & {
|
|
17
18
|
chunkSizeBytes: number;
|
|
18
19
|
};
|
|
20
|
+
export interface TestingTaskLabelAssignment {
|
|
21
|
+
taskId: string;
|
|
22
|
+
labels: TaskLabel[];
|
|
23
|
+
}
|
|
19
24
|
export interface TestingOpenForgeApiOptions {
|
|
20
25
|
pluginId?: string;
|
|
21
26
|
projectId?: string | null;
|
|
@@ -26,12 +31,16 @@ export interface TestingOpenForgeApiOptions {
|
|
|
26
31
|
storage?: PluginStorage;
|
|
27
32
|
/** Initial files exposed through `fs.userData`. Defaults to none. */
|
|
28
33
|
userDataTextFiles?: UserDataFileWriteRequest[];
|
|
34
|
+
/** File contents returned by `fs.readFile`, keyed by project-relative path. */
|
|
35
|
+
projectFileContents?: Readonly<Record<string, FileContent>>;
|
|
29
36
|
/**
|
|
30
37
|
* Tasks returned by `tasks.list`. The mock filters them by the requested
|
|
31
38
|
* `projectId` (when given) and drops `done` tasks unless `includeDone: true`,
|
|
32
39
|
* mirroring the host capability. Defaults to an empty list.
|
|
33
40
|
*/
|
|
34
41
|
tasks?: Task[];
|
|
42
|
+
/** Task Label assignments used by canonical Task projections. */
|
|
43
|
+
taskLabelAssignments?: TestingTaskLabelAssignment[];
|
|
35
44
|
/** Agent Sessions returned by `tasks.listSessions`. Defaults to an empty list. */
|
|
36
45
|
agentSessions?: AgentSession[];
|
|
37
46
|
/** Compact workspace context keyed by Task ID for `agentSessions.list`. Defaults to none. */
|
|
@@ -76,6 +85,16 @@ export interface TestingOpenForgeApiCalls {
|
|
|
76
85
|
projectId: string | null;
|
|
77
86
|
includeDone: boolean;
|
|
78
87
|
}>;
|
|
88
|
+
taskActiveRequests: Array<{
|
|
89
|
+
projectId: string;
|
|
90
|
+
}>;
|
|
91
|
+
taskCompletedRequests: Array<{
|
|
92
|
+
projectId: string;
|
|
93
|
+
} & CompletedTaskQuery>;
|
|
94
|
+
taskDetailRequests: Array<{
|
|
95
|
+
projectId: string;
|
|
96
|
+
taskId: string;
|
|
97
|
+
}>;
|
|
79
98
|
agentSessionListRequests: ListAgentSessionsRequest[];
|
|
80
99
|
taskSessionListRequests: ListTaskSessionsRequest[];
|
|
81
100
|
taskStatusUpdates: Array<{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TaskBrowserSurfaceState } from '../browserSurfaces';
|
|
2
|
-
import type { BackendPlugin, BackendPluginContext, FrontendPlugin, FrontendPluginContext, OpenForgePackageMetadata, PluginStorage } from '../types';
|
|
2
|
+
import type { BackendPlugin, BackendPluginContext, FrontendPlugin, FrontendPluginContext, OpenForgePackageMetadata, TaskChangeEvent, PluginStorage } from '../types';
|
|
3
3
|
import type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot } from './contracts';
|
|
4
4
|
import { TestingSubscriptionSink } from './support.js';
|
|
5
5
|
export declare class TestingOpenForgeRegistryFake {
|
|
@@ -28,6 +28,7 @@ export declare class TestingOpenForgeRegistryFake {
|
|
|
28
28
|
createBackendContext(): BackendPluginContext;
|
|
29
29
|
activateFrontend(plugin: FrontendPlugin): Promise<void>;
|
|
30
30
|
activateBackend(plugin: BackendPlugin): Promise<void>;
|
|
31
|
+
emitTaskChange(event: TaskChangeEvent): void;
|
|
31
32
|
setBrowserSurfaceState(taskId: string, id: string, patch: Partial<TaskBrowserSurfaceState>): void;
|
|
32
33
|
disposeAll(): Promise<void>;
|
|
33
34
|
getSnapshot(): TestingOpenForgeRegistrySnapshot;
|
|
@@ -82,6 +82,9 @@ export class TestingOpenForgeRegistryFake {
|
|
|
82
82
|
await plugin.activate(this.backendApi, this.createBackendContext());
|
|
83
83
|
await this.backendServices.startNewBackgroundServices(existingServices);
|
|
84
84
|
}
|
|
85
|
+
emitTaskChange(event) {
|
|
86
|
+
this.commonApi.emitTaskChange(event);
|
|
87
|
+
}
|
|
85
88
|
setBrowserSurfaceState(taskId, id, patch) {
|
|
86
89
|
this.frontendContributions.setBrowserSurfaceState(taskId, id, patch);
|
|
87
90
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentSessionWorkspace, AgentCommandMetadata, CommandDescriptor, Disposable, JsonValue, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, PluginStorage, StartPromptContribution, SubscriptionSink } from '../types';
|
|
2
|
-
import type { AgentSession, Task } from '../domain';
|
|
2
|
+
import type { AgentSession, FileContent, Task, TaskLabel } 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,10 +31,12 @@ export declare class TestingRegistryServices {
|
|
|
31
31
|
readonly storage: PluginStorage;
|
|
32
32
|
readonly config: Map<string, JsonValue>;
|
|
33
33
|
readonly seededTasks: Task[];
|
|
34
|
+
readonly seededTaskLabelAssignments: Map<string, TaskLabel[]>;
|
|
34
35
|
readonly seededAgentSessions: AgentSession[];
|
|
35
36
|
readonly agentSessionWorkspaces: Readonly<Record<string, AgentSessionWorkspace>>;
|
|
36
37
|
readonly externalTextFiles: TestingExternalTextFile[];
|
|
37
38
|
readonly userDataTextFiles: Map<string, string>;
|
|
39
|
+
readonly projectFileContents: Readonly<Record<string, FileContent>>;
|
|
38
40
|
readonly claims: TestingContributionClaims;
|
|
39
41
|
constructor(options?: TestingOpenForgeApiOptions);
|
|
40
42
|
localQualifiedId(kind: TestingRuntimeKind, id: string): string;
|
package/dist/testing/support.js
CHANGED
|
@@ -44,6 +44,9 @@ export function createTestingCalls() {
|
|
|
44
44
|
taskImplementationStarts: [],
|
|
45
45
|
taskFollowUps: [],
|
|
46
46
|
taskListRequests: [],
|
|
47
|
+
taskActiveRequests: [],
|
|
48
|
+
taskCompletedRequests: [],
|
|
49
|
+
taskDetailRequests: [],
|
|
47
50
|
agentSessionListRequests: [],
|
|
48
51
|
taskSessionListRequests: [],
|
|
49
52
|
taskStatusUpdates: [],
|
|
@@ -205,10 +208,12 @@ export class TestingRegistryServices {
|
|
|
205
208
|
storage;
|
|
206
209
|
config = new Map();
|
|
207
210
|
seededTasks;
|
|
211
|
+
seededTaskLabelAssignments;
|
|
208
212
|
seededAgentSessions;
|
|
209
213
|
agentSessionWorkspaces;
|
|
210
214
|
externalTextFiles;
|
|
211
215
|
userDataTextFiles = new Map();
|
|
216
|
+
projectFileContents;
|
|
212
217
|
claims = new TestingContributionClaims();
|
|
213
218
|
constructor(options = {}) {
|
|
214
219
|
this.pluginId = options.pluginId ?? 'test-plugin';
|
|
@@ -224,9 +229,11 @@ export class TestingRegistryServices {
|
|
|
224
229
|
this.calls = createTestingCalls();
|
|
225
230
|
this.storage = options.storage ?? createMemoryPluginStorage(this.calls);
|
|
226
231
|
this.seededTasks = options.tasks ?? [];
|
|
232
|
+
this.seededTaskLabelAssignments = new Map((options.taskLabelAssignments ?? []).map(assignment => [assignment.taskId, assignment.labels]));
|
|
227
233
|
this.seededAgentSessions = options.agentSessions ?? [];
|
|
228
234
|
this.agentSessionWorkspaces = options.agentSessionWorkspaces ?? {};
|
|
229
235
|
this.externalTextFiles = options.externalTextFiles ?? [];
|
|
236
|
+
this.projectFileContents = options.projectFileContents ?? {};
|
|
230
237
|
for (const file of options.userDataTextFiles ?? []) {
|
|
231
238
|
this.userDataTextFiles.set(file.path, file.content);
|
|
232
239
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Component } from 'svelte';
|
|
2
2
|
import type { BrowserSurfacesAPI } from './browserSurfaces';
|
|
3
|
-
import type { BoardStatus, AgentSession, CommandInfo, FileContent, FileEntry, Project, ProjectAttention, ReviewPullRequest, Task, TaskWorkspaceInfo, WorktreeSource, WritableBoardStatus } from './domain';
|
|
3
|
+
import type { BoardStatus, AgentSession, CommandInfo, FileContent, FileEntry, Project, ProjectAttention, ActiveTasks, CompletedTaskPage, CompletedTaskQuery, TaskRead, TaskDetail, ReviewPullRequest, Task, TaskWorkspaceInfo, WorktreeSource, WritableBoardStatus } from './domain';
|
|
4
4
|
export type SupportedOpenForgeApiVersion = 1;
|
|
5
5
|
export declare const SUPPORTED_OPENFORGE_API_VERSIONS: readonly [1, ...1[]];
|
|
6
6
|
export declare const OPENFORGE_PLUGIN_API_VERSION: SupportedOpenForgeApiVersion;
|
|
@@ -202,6 +202,7 @@ export interface PluginTaskPaneProps extends Record<string, unknown> {
|
|
|
202
202
|
api: FrontendOpenForgeAPI;
|
|
203
203
|
context: OpenForgeContextSnapshot;
|
|
204
204
|
taskId: string;
|
|
205
|
+
task: TaskDetail;
|
|
205
206
|
projectId: string | null;
|
|
206
207
|
}
|
|
207
208
|
export interface PluginTaskUISectionProps extends PluginTaskPaneProps {
|
|
@@ -609,19 +610,31 @@ export interface AgentSessionSummaryPage {
|
|
|
609
610
|
export interface AgentSessionsAPI {
|
|
610
611
|
list(request: ListAgentSessionsRequest): Promise<AgentSessionSummaryPage>;
|
|
611
612
|
}
|
|
612
|
-
export
|
|
613
|
+
export type TaskChangeReason = 'created' | 'updated' | 'completed' | 'attention' | 'execution';
|
|
614
|
+
/**
|
|
615
|
+
* A coalescible signal that one or more bounded Task reads may now be stale.
|
|
616
|
+
* It never contains a Task snapshot.
|
|
617
|
+
*/
|
|
618
|
+
export interface TaskChangeEvent {
|
|
619
|
+
projectId: string;
|
|
620
|
+
taskId: string | null;
|
|
621
|
+
reason: TaskChangeReason;
|
|
622
|
+
}
|
|
623
|
+
export interface TaskOperationsAPI {
|
|
613
624
|
/**
|
|
614
|
-
* Lists
|
|
615
|
-
*
|
|
616
|
-
* `
|
|
617
|
-
* unscoped listing (no `projectId`) always returns all states, so
|
|
618
|
-
* `includeDone` only affects the project-scoped path.
|
|
625
|
+
* Lists legacy Task rows. Project-scoped reads exclude Completed Tasks unless
|
|
626
|
+
* `includeDone` is true; unscoped reads preserve the complete legacy array.
|
|
627
|
+
* @deprecated Use `active`, `completed`, or `detail`. Removed in version 2.
|
|
619
628
|
*/
|
|
620
629
|
list(request?: {
|
|
621
630
|
projectId?: string | null;
|
|
622
631
|
includeDone?: boolean;
|
|
623
632
|
}): Promise<Task[]>;
|
|
633
|
+
/** @deprecated Use `detail`. Removed in version 2. */
|
|
624
634
|
get(taskId: string): Promise<Task | null>;
|
|
635
|
+
active(projectId: string): Promise<ActiveTasks>;
|
|
636
|
+
completed(projectId: string, query?: CompletedTaskQuery): Promise<CompletedTaskPage>;
|
|
637
|
+
detail(projectId: string, taskId: string): Promise<TaskRead | null>;
|
|
625
638
|
create(request: CreateTaskRequest): Promise<Task>;
|
|
626
639
|
/**
|
|
627
640
|
* Opens the host's create-task dialog pre-filled, letting the user edit the
|
|
@@ -642,6 +655,13 @@ export interface TasksAPI {
|
|
|
642
655
|
/** Returns matching Agent Sessions newest first. */
|
|
643
656
|
listSessions(request: ListTaskSessionsRequest): Promise<AgentSession[]>;
|
|
644
657
|
}
|
|
658
|
+
export interface TasksAPI extends TaskOperationsAPI {
|
|
659
|
+
/**
|
|
660
|
+
* Subscribes to coalescible Task invalidations for one Project.
|
|
661
|
+
* Repeat the relevant bounded read instead of treating events as snapshots.
|
|
662
|
+
*/
|
|
663
|
+
onDidChange(projectId: string, handler: (event: TaskChangeEvent) => void): Disposable;
|
|
664
|
+
}
|
|
645
665
|
export interface ProjectsAPI {
|
|
646
666
|
list(): Promise<Project[]>;
|
|
647
667
|
get(projectId: string): Promise<Project | null>;
|
|
@@ -673,7 +693,7 @@ export interface OpenForgeCommonAPI {
|
|
|
673
693
|
getSnapshot(): OpenForgeContextSnapshot;
|
|
674
694
|
};
|
|
675
695
|
agentSessions: AgentSessionsAPI;
|
|
676
|
-
tasks:
|
|
696
|
+
tasks: TaskOperationsAPI;
|
|
677
697
|
projects: ProjectsAPI;
|
|
678
698
|
fs: FileSystemAPI;
|
|
679
699
|
shell: ShellAPI;
|
|
@@ -684,6 +704,7 @@ export interface OpenForgeCommonAPI {
|
|
|
684
704
|
projectConfig: KeyValueConfigAPI;
|
|
685
705
|
}
|
|
686
706
|
export interface FrontendOpenForgeAPI extends OpenForgeCommonAPI {
|
|
707
|
+
tasks: TasksAPI;
|
|
687
708
|
browserSurfaces: BrowserSurfacesAPI;
|
|
688
709
|
navigation: NavigationAPI;
|
|
689
710
|
views: FrontendViewRegistry;
|
|
@@ -23,17 +23,12 @@
|
|
|
23
23
|
class: className,
|
|
24
24
|
}: Props = $props()
|
|
25
25
|
|
|
26
|
-
function handleKeydown(event: KeyboardEvent): void {
|
|
27
|
-
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
28
|
-
event.preventDefault()
|
|
29
|
-
onActivate()
|
|
30
|
-
}
|
|
31
26
|
</script>
|
|
32
27
|
|
|
33
28
|
<button
|
|
34
29
|
type="button"
|
|
35
30
|
class={[
|
|
36
|
-
'relative mx-2 flex min-h-11 w-
|
|
31
|
+
'relative mx-2 flex min-h-11 w-[calc(100%_-_1rem)] items-center rounded-lg gap-3 py-2.5 cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary',
|
|
37
32
|
collapsed ? 'justify-center px-0' : 'px-3',
|
|
38
33
|
active ? 'bg-primary/10 text-primary' : 'text-base-content/55 hover:bg-base-200 hover:text-base-content',
|
|
39
34
|
className,
|
|
@@ -42,7 +37,6 @@
|
|
|
42
37
|
aria-label={accessibleName}
|
|
43
38
|
aria-current={active ? 'page' : undefined}
|
|
44
39
|
onclick={onActivate}
|
|
45
|
-
onkeydown={handleKeydown}
|
|
46
40
|
>
|
|
47
41
|
{#if leading}
|
|
48
42
|
<span class="relative shrink-0">{@render leading()}</span>
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openforge-app/plugin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "TypeScript SDK for building trusted OpenForge frontend and backend plugins.",
|
|
5
|
+
"homepage": "https://github.com/koenvg/openforge/blob/main/docs/plugin-authoring.md",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"openforge",
|
|
8
|
+
"plugin",
|
|
9
|
+
"sdk",
|
|
10
|
+
"svelte"
|
|
11
|
+
],
|
|
4
12
|
"license": "MIT",
|
|
5
13
|
"repository": {
|
|
6
14
|
"type": "git",
|