@openforge-app/plugin-sdk 0.2.10 → 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/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/publicEntrypoints.d.mts +1 -0
- package/dist/publicEntrypoints.mjs +23 -22
- package/dist/publicUiExports.mjs +10 -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/testing/commonApiFake.d.ts +4 -2
- package/dist/testing/commonApiFake.js +247 -4
- 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 +40 -9
- package/dist/ui/MarkdownContent.svelte +46 -0
- package/dist/ui/MermaidDiagramPreview.svelte +216 -0
- package/dist/ui/Modal.svelte +6 -1
- package/dist/ui/PluginSidebarLink.svelte +1 -7
- package/dist/ui/icons/video.svg +3 -0
- package/package.json +14 -5
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();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type MermaidTheme = 'default' | 'dark';
|
|
2
|
+
export declare const MERMAID_DIAGRAM_CLASS = "mermaid-diagram";
|
|
3
|
+
export declare const MERMAID_EXPAND_ACTION_CLASS = "mermaid-diagram-expand";
|
|
4
|
+
export declare function resolveMermaidTheme(doc?: Document): MermaidTheme;
|
|
5
|
+
export interface RenderedMermaidDiagram {
|
|
6
|
+
trigger: HTMLButtonElement;
|
|
7
|
+
wrapper: HTMLDivElement;
|
|
8
|
+
svg: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getRenderedMermaidSvg(wrapper: HTMLDivElement): string | null;
|
|
11
|
+
export declare function getRenderedMermaidDiagram(target: Element): RenderedMermaidDiagram | null;
|
|
12
|
+
export declare function renderMermaidDiagrams(root: HTMLElement, isCurrent?: () => boolean): Promise<void>;
|
|
13
|
+
export declare function observeMermaidTheme(doc: Document, onChange: () => void): () => void;
|
package/dist/mermaid.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { sanitizeMermaidSvg } from './sanitize';
|
|
2
|
+
const MERMAID_CODE_SELECTOR = 'pre > code.language-mermaid';
|
|
3
|
+
export const MERMAID_DIAGRAM_CLASS = 'mermaid-diagram';
|
|
4
|
+
export const MERMAID_EXPAND_ACTION_CLASS = 'mermaid-diagram-expand';
|
|
5
|
+
const MERMAID_FAILURE_CLASS = 'mermaid-diagram-fallback';
|
|
6
|
+
const MERMAID_EXTERNAL_RESOURCE_PATTERN = /(?:\b(?:https?|ftp|file|data|javascript|vbscript):|(?:^|[\s("'=])\/\/|@import|\burl\s*\()/im;
|
|
7
|
+
const MERMAID_ESCAPED_STYLE_PATTERN = /\b(?:classDef|style|linkStyle)\b[^\r\n]*\\/i;
|
|
8
|
+
let diagramId = 0;
|
|
9
|
+
let renderQueue = Promise.resolve();
|
|
10
|
+
const mermaidThemeObservations = new WeakMap();
|
|
11
|
+
function loadMermaid() {
|
|
12
|
+
return import('mermaid').then(module => module.default);
|
|
13
|
+
}
|
|
14
|
+
export function resolveMermaidTheme(doc = document) {
|
|
15
|
+
const themeName = doc.documentElement.getAttribute('data-theme');
|
|
16
|
+
if (themeName)
|
|
17
|
+
return themeName.includes('dark') ? 'dark' : 'default';
|
|
18
|
+
if (doc.defaultView?.matchMedia?.('(prefers-color-scheme: dark)').matches)
|
|
19
|
+
return 'dark';
|
|
20
|
+
return 'default';
|
|
21
|
+
}
|
|
22
|
+
function queueMermaidRender(operation) {
|
|
23
|
+
const result = renderQueue.then(operation, operation);
|
|
24
|
+
renderQueue = result.then(() => undefined, () => undefined);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function assertSafeMermaidSource(source) {
|
|
28
|
+
if (MERMAID_EXTERNAL_RESOURCE_PATTERN.test(source) || MERMAID_ESCAPED_STYLE_PATTERN.test(source)) {
|
|
29
|
+
throw new Error('Mermaid source contains an external resource');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function renderMermaidSvg(source, theme) {
|
|
33
|
+
assertSafeMermaidSource(source);
|
|
34
|
+
return queueMermaidRender(async () => {
|
|
35
|
+
const mermaid = await loadMermaid();
|
|
36
|
+
mermaid.initialize({
|
|
37
|
+
startOnLoad: false,
|
|
38
|
+
securityLevel: 'strict',
|
|
39
|
+
suppressErrorRendering: true,
|
|
40
|
+
maxTextSize: 50_000,
|
|
41
|
+
maxEdges: 500,
|
|
42
|
+
secure: [
|
|
43
|
+
'secure',
|
|
44
|
+
'securityLevel',
|
|
45
|
+
'startOnLoad',
|
|
46
|
+
'suppressErrorRendering',
|
|
47
|
+
'maxTextSize',
|
|
48
|
+
'maxEdges',
|
|
49
|
+
'theme',
|
|
50
|
+
'themeVariables',
|
|
51
|
+
'themeCSS',
|
|
52
|
+
'fontFamily',
|
|
53
|
+
'altFontFamily',
|
|
54
|
+
'dompurifyConfig',
|
|
55
|
+
'htmlLabels',
|
|
56
|
+
'flowchart',
|
|
57
|
+
],
|
|
58
|
+
theme,
|
|
59
|
+
htmlLabels: false,
|
|
60
|
+
flowchart: { htmlLabels: false },
|
|
61
|
+
});
|
|
62
|
+
const { svg } = await mermaid.render(`openforge-mermaid-${++diagramId}`, source);
|
|
63
|
+
return sanitizeMermaidSvg(svg);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function prepareDiagram(code) {
|
|
67
|
+
const fallback = code.parentElement;
|
|
68
|
+
if (!(fallback instanceof HTMLPreElement))
|
|
69
|
+
return null;
|
|
70
|
+
const existingWrapper = fallback.parentElement;
|
|
71
|
+
if (existingWrapper instanceof HTMLDivElement && existingWrapper.classList.contains(MERMAID_DIAGRAM_CLASS)) {
|
|
72
|
+
return { wrapper: existingWrapper, source: code.textContent?.trim() ?? '', fallback };
|
|
73
|
+
}
|
|
74
|
+
const wrapper = document.createElement('div');
|
|
75
|
+
wrapper.className = MERMAID_DIAGRAM_CLASS;
|
|
76
|
+
wrapper.setAttribute('role', 'group');
|
|
77
|
+
wrapper.setAttribute('aria-label', 'Mermaid diagram');
|
|
78
|
+
fallback.before(wrapper);
|
|
79
|
+
wrapper.append(fallback);
|
|
80
|
+
return { wrapper, source: code.textContent?.trim() ?? '', fallback };
|
|
81
|
+
}
|
|
82
|
+
function clearRenderedDiagram(wrapper, fallback) {
|
|
83
|
+
const expandAction = wrapper.querySelector(`:scope > button.${MERMAID_EXPAND_ACTION_CLASS}`);
|
|
84
|
+
for (const child of Array.from(wrapper.children)) {
|
|
85
|
+
if (child !== fallback && child !== expandAction)
|
|
86
|
+
child.remove();
|
|
87
|
+
}
|
|
88
|
+
if (expandAction)
|
|
89
|
+
expandAction.hidden = true;
|
|
90
|
+
fallback.hidden = false;
|
|
91
|
+
wrapper.classList.remove('mermaid-diagram-rendered', MERMAID_FAILURE_CLASS);
|
|
92
|
+
}
|
|
93
|
+
function showRenderFailure(wrapper, fallback) {
|
|
94
|
+
clearRenderedDiagram(wrapper, fallback);
|
|
95
|
+
const message = document.createElement('p');
|
|
96
|
+
message.className = 'mermaid-diagram-error';
|
|
97
|
+
message.setAttribute('role', 'status');
|
|
98
|
+
message.textContent = 'Unable to render Mermaid diagram. Showing source instead.';
|
|
99
|
+
wrapper.prepend(message);
|
|
100
|
+
wrapper.classList.add(MERMAID_FAILURE_CLASS);
|
|
101
|
+
}
|
|
102
|
+
function fitSvgToRenderedContent(svg) {
|
|
103
|
+
if (typeof svg.getBBox !== 'function')
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
const bounds = svg.getBBox();
|
|
107
|
+
if (![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite))
|
|
108
|
+
return;
|
|
109
|
+
if (bounds.width <= 0 || bounds.height <= 0)
|
|
110
|
+
return;
|
|
111
|
+
const padding = 8;
|
|
112
|
+
const width = bounds.width + padding * 2;
|
|
113
|
+
const height = bounds.height + padding * 2;
|
|
114
|
+
svg.setAttribute('viewBox', [
|
|
115
|
+
bounds.x - padding,
|
|
116
|
+
bounds.y - padding,
|
|
117
|
+
width,
|
|
118
|
+
height,
|
|
119
|
+
].join(' '));
|
|
120
|
+
svg.style.maxWidth = `${Math.ceil(width)}px`;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Keep Mermaid's original viewport when the browser cannot measure SVG geometry.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function createDiagramExpandAction(doc) {
|
|
127
|
+
const button = doc.createElement('button');
|
|
128
|
+
button.type = 'button';
|
|
129
|
+
button.className = MERMAID_EXPAND_ACTION_CLASS;
|
|
130
|
+
button.setAttribute('aria-label', 'Expand Mermaid diagram');
|
|
131
|
+
button.title = 'Expand Mermaid diagram';
|
|
132
|
+
button.textContent = 'Expand';
|
|
133
|
+
return button;
|
|
134
|
+
}
|
|
135
|
+
export function getRenderedMermaidSvg(wrapper) {
|
|
136
|
+
if (!wrapper.isConnected || !wrapper.classList.contains('mermaid-diagram-rendered'))
|
|
137
|
+
return null;
|
|
138
|
+
return wrapper.querySelector(':scope > svg')?.outerHTML ?? null;
|
|
139
|
+
}
|
|
140
|
+
export function getRenderedMermaidDiagram(target) {
|
|
141
|
+
const trigger = target.closest(`button.${MERMAID_EXPAND_ACTION_CLASS}`);
|
|
142
|
+
if (!(trigger instanceof HTMLButtonElement))
|
|
143
|
+
return null;
|
|
144
|
+
const wrapper = trigger.closest(`.${MERMAID_DIAGRAM_CLASS}`);
|
|
145
|
+
if (!(wrapper instanceof HTMLDivElement))
|
|
146
|
+
return null;
|
|
147
|
+
const svg = getRenderedMermaidSvg(wrapper);
|
|
148
|
+
return svg ? { trigger, wrapper, svg } : null;
|
|
149
|
+
}
|
|
150
|
+
function showRenderedDiagram(wrapper, fallback, sanitizedSvg) {
|
|
151
|
+
const template = document.createElement('template');
|
|
152
|
+
template.innerHTML = sanitizedSvg;
|
|
153
|
+
const svg = template.content.querySelector('svg');
|
|
154
|
+
if (!svg)
|
|
155
|
+
return false;
|
|
156
|
+
if (!svg.hasAttribute('role'))
|
|
157
|
+
svg.setAttribute('role', 'img');
|
|
158
|
+
if (!svg.hasAttribute('aria-label') && !svg.hasAttribute('aria-labelledby')) {
|
|
159
|
+
svg.setAttribute('aria-label', 'Mermaid diagram');
|
|
160
|
+
}
|
|
161
|
+
clearRenderedDiagram(wrapper, fallback);
|
|
162
|
+
wrapper.insertBefore(template.content, fallback);
|
|
163
|
+
fitSvgToRenderedContent(svg);
|
|
164
|
+
const expandAction = wrapper.querySelector(`:scope > button.${MERMAID_EXPAND_ACTION_CLASS}`)
|
|
165
|
+
?? createDiagramExpandAction(wrapper.ownerDocument);
|
|
166
|
+
expandAction.hidden = false;
|
|
167
|
+
if (!expandAction.parentElement)
|
|
168
|
+
wrapper.insertBefore(expandAction, fallback);
|
|
169
|
+
fallback.hidden = true;
|
|
170
|
+
wrapper.classList.add('mermaid-diagram-rendered');
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
export async function renderMermaidDiagrams(root, isCurrent = () => true) {
|
|
174
|
+
const diagrams = Array.from(root.querySelectorAll(MERMAID_CODE_SELECTOR))
|
|
175
|
+
.map(prepareDiagram)
|
|
176
|
+
.filter((diagram) => diagram !== null);
|
|
177
|
+
const theme = resolveMermaidTheme(root.ownerDocument);
|
|
178
|
+
await Promise.all(diagrams.map(async ({ wrapper, source, fallback }) => {
|
|
179
|
+
clearRenderedDiagram(wrapper, fallback);
|
|
180
|
+
if (!source) {
|
|
181
|
+
showRenderFailure(wrapper, fallback);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const svg = await renderMermaidSvg(source, theme);
|
|
186
|
+
if (!isCurrent() || !wrapper.isConnected)
|
|
187
|
+
return;
|
|
188
|
+
if (!showRenderedDiagram(wrapper, fallback, svg))
|
|
189
|
+
showRenderFailure(wrapper, fallback);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
if (isCurrent() && wrapper.isConnected)
|
|
193
|
+
showRenderFailure(wrapper, fallback);
|
|
194
|
+
}
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
export function observeMermaidTheme(doc, onChange) {
|
|
198
|
+
let observation = mermaidThemeObservations.get(doc);
|
|
199
|
+
if (!observation) {
|
|
200
|
+
const listeners = new Set();
|
|
201
|
+
const notify = () => listeners.forEach(listener => listener());
|
|
202
|
+
const observer = new MutationObserver((mutations) => {
|
|
203
|
+
if (mutations.some(mutation => mutation.attributeName === 'data-theme'))
|
|
204
|
+
notify();
|
|
205
|
+
});
|
|
206
|
+
observer.observe(doc.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
207
|
+
const media = doc.defaultView?.matchMedia?.('(prefers-color-scheme: dark)');
|
|
208
|
+
media?.addEventListener?.('change', notify);
|
|
209
|
+
observation = { listeners, observer, media, notify };
|
|
210
|
+
mermaidThemeObservations.set(doc, observation);
|
|
211
|
+
}
|
|
212
|
+
observation.listeners.add(onChange);
|
|
213
|
+
return () => {
|
|
214
|
+
observation.listeners.delete(onChange);
|
|
215
|
+
if (observation.listeners.size > 0)
|
|
216
|
+
return;
|
|
217
|
+
observation.observer.disconnect();
|
|
218
|
+
observation.media?.removeEventListener?.('change', observation.notify);
|
|
219
|
+
mermaidThemeObservations.delete(doc);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const MIN_MERMAID_ZOOM = 0.25;
|
|
2
|
+
export declare const MAX_MERMAID_ZOOM = 4;
|
|
3
|
+
export declare const MERMAID_ZOOM_STEP = 0.25;
|
|
4
|
+
export interface MermaidSize {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export type MermaidZoomState = {
|
|
9
|
+
mode: 'fit';
|
|
10
|
+
} | {
|
|
11
|
+
mode: 'manual';
|
|
12
|
+
scale: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const FIT_MERMAID_ZOOM: MermaidZoomState;
|
|
15
|
+
export declare function calculateMermaidFitScale(content: MermaidSize, viewport: MermaidSize): number | null;
|
|
16
|
+
export declare function createManualMermaidZoom(scale: number): MermaidZoomState;
|
|
17
|
+
export declare function resolveMermaidZoomScale(state: MermaidZoomState, fitScale: number | null): number;
|
|
18
|
+
export declare function zoomMermaidIn(state: MermaidZoomState, fitScale: number | null): MermaidZoomState;
|
|
19
|
+
export declare function zoomMermaidOut(state: MermaidZoomState, fitScale: number | null): MermaidZoomState;
|
|
20
|
+
export declare function resetMermaidZoom(): MermaidZoomState;
|
|
21
|
+
export declare function canZoomMermaidIn(state: MermaidZoomState, fitScale: number | null): boolean;
|
|
22
|
+
export declare function canZoomMermaidOut(state: MermaidZoomState, fitScale: number | null): boolean;
|
|
23
|
+
export declare function formatMermaidZoomLabel(state: MermaidZoomState, fitScale: number | null): string;
|