@openforge-app/plugin-sdk 0.2.3 → 0.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @openforge-app/plugin-sdk
2
2
 
3
- Public SDK for building OpenForge plugins. See [`docs/plugin-authoring.md`](../../docs/plugin-authoring.md) for the complete package, runtime, Svelte-sharing, and CSS-loading contract.
3
+ Public SDK for building OpenForge plugins. [`docs/plugin-authoring.md`](../../docs/plugin-authoring.md) is the canonical guide for the package, runtime, Svelte-sharing, and CSS-loading contract.
4
4
 
5
5
  Styled frontend plugins must declare Vite/Svelte's emitted CSS artifacts in `package.json#openforge.frontendStyles`:
6
6
 
@@ -24,9 +24,3 @@ Automated releases use npm trusted publishing rather than a long-lived npm token
24
24
  `@openforge-app/plugin-sdk` is licensed under the MIT License. Plugin authors may use it for personal, internal, open source, or commercial plugin development, including redistribution and modification under the MIT terms.
25
25
 
26
26
  This MIT license applies to the SDK package only. The OpenForge desktop application is licensed separately under the repository root `LICENSE` and is source-available/proprietary; the app may not be commercially resold or redistributed without permission.
27
-
28
- ## Current contract
29
-
30
- Version `0.2.1` publishes the API-v1 contract used by the OpenForge host, including agent-facing Plugin Command metadata, `PluginCommandInvocationContext`, and the `@openforge-app/plugin-sdk/testing` helpers. Legacy handoff-summary declarations (`Task.summary` and `tasks.updateSummary`) are no longer part of the contract.
31
-
32
- Desktop releases publish the SDK in the same workflow using the desktop git tag's semver; the manual publish workflow uses the checked-in package version. Both paths run `pnpm check:contract`, which packs the package, verifies all exported files, and compiles the current authoring contract against the tarball before publication.
package/dist/backend.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import type { BackendMethodRegistration, BackendMethodRegistry, BackendOpenForgeAPI, BackendPlugin, BackendPluginContext, BackgroundServiceRegistration, BackgroundServiceRegistry, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot } from './types';
1
+ import type { BackendFileSystemAPI, BackendMethodRegistration, BackendMethodRegistry, BackendOpenForgeAPI, BackendPlugin, BackendPluginContext, BackgroundServiceRegistration, BackgroundServiceRegistry, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadFileSystemAPI, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot } from './types';
2
2
  export declare function defineBackendPlugin<const TPlugin extends BackendPlugin>(plugin: TPlugin): TPlugin;
3
- export type { BackendMethodRegistration, BackendMethodRegistry, BackendOpenForgeAPI, BackendPlugin, BackendPluginContext, BackgroundServiceRegistration, BackgroundServiceRegistry, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, };
3
+ export type { BackendFileSystemAPI, BackendMethodRegistration, BackendMethodRegistry, BackendOpenForgeAPI, BackendPlugin, BackendPluginContext, BackgroundServiceRegistration, BackgroundServiceRegistry, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadFileSystemAPI, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, };
@@ -1,5 +1,5 @@
1
1
  import type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState } from './browserSurfaces';
2
- import type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendInjectionPointRegistry, FrontendTaskStartRegistry, FrontendTaskUIRegistry, FrontendViewRegistry, InjectionPointLocation, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, PluginIcon, PluginInjectionPointProps, PluginInjectionPointRegistration, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsSectionScope, PluginStorageScope, PluginSvgIcon, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginTaskUISectionProps, PluginTaskUISectionRegistration, PluginViewProps, PluginViewRegistration, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol } from './types';
2
+ import type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendInjectionPointRegistry, FrontendTaskStartRegistry, FrontendTaskUIRegistry, FrontendViewRegistry, InjectionPointLocation, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, PluginIcon, PluginSidebarNavigationProps, PluginSidebarViewIdentity, PluginInjectionPointProps, PluginInjectionPointRegistration, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsSectionScope, PluginStorageScope, PluginSvgIcon, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginTaskUISectionProps, PluginTaskUISectionRegistration, PluginViewProps, PluginViewRegistration, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, PtyBufferState, TerminalImageProtocol } from './types';
3
3
  export declare const OPENFORGE_FRONTEND_PLUGIN_MARKER = "__openforgeFrontendPlugin";
4
4
  export type MarkedFrontendPlugin<TPlugin extends FrontendPlugin = FrontendPlugin> = TPlugin & {
5
5
  readonly [OPENFORGE_FRONTEND_PLUGIN_MARKER]: true;
@@ -7,4 +7,4 @@ export type MarkedFrontendPlugin<TPlugin extends FrontendPlugin = FrontendPlugin
7
7
  export declare function defineFrontendPlugin<const TPlugin extends FrontendPlugin>(plugin: TPlugin): MarkedFrontendPlugin<TPlugin>;
8
8
  export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
9
9
  export type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState, };
10
- export type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendInjectionPointRegistry, FrontendTaskStartRegistry, FrontendTaskUIRegistry, FrontendViewRegistry, InjectionPointLocation, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, PluginIcon, PluginInjectionPointProps, PluginInjectionPointRegistration, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsSectionScope, PluginStorageScope, PluginSvgIcon, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginTaskUISectionProps, PluginTaskUISectionRegistration, PluginViewProps, PluginViewRegistration, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol, };
10
+ export type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendInjectionPointRegistry, FrontendTaskStartRegistry, FrontendTaskUIRegistry, FrontendViewRegistry, InjectionPointLocation, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, PluginIcon, PluginSidebarNavigationProps, PluginSidebarViewIdentity, PluginInjectionPointProps, PluginInjectionPointRegistration, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsSectionScope, PluginStorageScope, PluginSvgIcon, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginTaskUISectionProps, PluginTaskUISectionRegistration, PluginViewProps, PluginViewRegistration, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, PtyBufferState, TerminalImageProtocol, };
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 { TaskFollowUpError, MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
6
- export type { AgentCommandDescriptor, AgentCommandMetadata, AgentCommandRuntime, AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, FileSystemAPI, ImplementationRun, InjectionPointLocation, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, OpenForgePluginCapability, OpenForgePluginContext, OpenForgePluginPackageJson, PluginCommandInvocationContext, PluginCommandInvocationSource, PluginComponentLoader, PluginComponentModule, PluginEntry, PluginIcon, PluginInjectionPointProps, PluginSettingsSectionProps, PluginState, PluginSvgIcon, PluginTaskPaneProps, PluginTaskUISectionProps, PluginViewProps, PluginStorage, PluginStorageScope, PluginViewKey, ProjectsAPI, ShellAPI, ShellResizeRequest, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskFollowUpDisposition, TaskFollowUpErrorCode, TaskFollowUpReceipt, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, TasksAPI, ValidationError, } from './types';
6
+ export type { AgentCommandDescriptor, AgentCommandMetadata, AgentCommandRuntime, AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, BackendFileSystemAPI, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadFileSystemAPI, FileSystemAPI, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, ImplementationRun, InjectionPointLocation, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, 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, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskFollowUpDisposition, TaskFollowUpErrorCode, TaskFollowUpReceipt, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, TasksAPI, ValidationError, } from './types';
7
7
  export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, 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';
@@ -11,7 +11,7 @@ export declare const OPENFORGE_PACKAGE_METADATA_SCHEMA: {
11
11
  dependentRequired: {
12
12
  frontendStyles: string[];
13
13
  };
14
- allOf: {
14
+ allOf: ({
15
15
  if: {
16
16
  properties: {
17
17
  requires: {
@@ -19,13 +19,35 @@ export declare const OPENFORGE_PACKAGE_METADATA_SCHEMA: {
19
19
  const: string;
20
20
  };
21
21
  };
22
+ enablement?: undefined;
22
23
  };
23
24
  required: string[];
24
25
  };
25
26
  then: {
26
27
  required: string[];
28
+ properties?: undefined;
27
29
  };
28
- }[];
30
+ } | {
31
+ if: {
32
+ properties: {
33
+ enablement: {
34
+ const: string;
35
+ };
36
+ requires?: undefined;
37
+ };
38
+ required: string[];
39
+ };
40
+ then: {
41
+ properties: {
42
+ requires: {
43
+ contains: {
44
+ const: string;
45
+ };
46
+ };
47
+ };
48
+ required: string[];
49
+ };
50
+ })[];
29
51
  properties: {
30
52
  id: {
31
53
  type: string;
@@ -44,9 +66,36 @@ export declare const OPENFORGE_PACKAGE_METADATA_SCHEMA: {
44
66
  type: string;
45
67
  minLength: number;
46
68
  };
69
+ enablement: {
70
+ enum: string[];
71
+ default: string;
72
+ description: string;
73
+ };
47
74
  icon: {
48
- type: string;
49
- minLength: number;
75
+ oneOf: ({
76
+ type: string;
77
+ minLength: number;
78
+ pattern: string;
79
+ additionalProperties?: undefined;
80
+ required?: undefined;
81
+ properties?: undefined;
82
+ } | {
83
+ type: string;
84
+ additionalProperties: boolean;
85
+ required: string[];
86
+ properties: {
87
+ type: {
88
+ const: string;
89
+ };
90
+ svg: {
91
+ type: string;
92
+ minLength: number;
93
+ pattern: string;
94
+ };
95
+ };
96
+ minLength?: undefined;
97
+ pattern?: undefined;
98
+ })[];
50
99
  description: string;
51
100
  };
52
101
  frontend: {
package/dist/manifest.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import packageMetadataSchemaData from './openforgePackageMetadataSchema.json';
2
+ import { isPluginIconName, isPluginSvgIcon } from './pluginIconContract';
2
3
  import { SUPPORTED_OPENFORGE_API_VERSIONS } from './types';
3
4
  export const OPENFORGE_PACKAGE_METADATA_SCHEMA = packageMetadataSchemaData;
4
5
  export const OPENFORGE_PLUGIN_CAPABILITIES = packageMetadataSchemaData.properties.requires.items.enum;
@@ -27,6 +28,18 @@ function validateOptionalString(value, path) {
27
28
  }
28
29
  return [];
29
30
  }
31
+ function validateEnablement(value) {
32
+ if (value === undefined || value === 'app' || value === 'project') {
33
+ return [];
34
+ }
35
+ return [{ path: 'enablement', message: 'Must be "app" or "project"' }];
36
+ }
37
+ function validatePluginIcon(value) {
38
+ if (value === undefined || isPluginIconName(value) || isPluginSvgIcon(value)) {
39
+ return [];
40
+ }
41
+ return [{ path: 'icon', message: 'Must be a non-empty Lucide icon name or { type: "svg", svg }' }];
42
+ }
30
43
  function validateFrontendStyles(value) {
31
44
  if (value === undefined)
32
45
  return [];
@@ -97,7 +110,8 @@ export function validateOpenForgePackageMetadata(data) {
97
110
  errors.push(...validateApiVersion(data.apiVersion));
98
111
  errors.push(...validateRequiredString(data.displayName, 'displayName'));
99
112
  errors.push(...validateRequiredString(data.description, 'description'));
100
- errors.push(...validateOptionalString(data.icon, 'icon'));
113
+ errors.push(...validateEnablement(data.enablement));
114
+ errors.push(...validatePluginIcon(data.icon));
101
115
  errors.push(...validateOptionalString(data.frontend, 'frontend'));
102
116
  errors.push(...validateFrontendStyles(data.frontendStyles));
103
117
  if (data.frontendStyles !== undefined && !isNonEmptyString(data.frontend)) {
@@ -105,6 +119,9 @@ export function validateOpenForgePackageMetadata(data) {
105
119
  }
106
120
  errors.push(...validateOptionalString(data.backend, 'backend'));
107
121
  errors.push(...validateRequires(data.requires));
122
+ if (data.enablement === 'app' && (!Array.isArray(data.requires) || !data.requires.includes('appEnablement'))) {
123
+ errors.push({ path: 'requires', message: 'App enablement requires the appEnablement capability' });
124
+ }
108
125
  if (Array.isArray(data.requires) && data.requires.includes('browserSurfaces') && !isNonEmptyString(data.frontend)) {
109
126
  errors.push({ path: 'requires', message: 'browserSurfaces capability requires a frontend entry' });
110
127
  }
@@ -27,6 +27,18 @@
27
27
  "required": ["requires"]
28
28
  },
29
29
  "then": { "required": ["frontend"] }
30
+ },
31
+ {
32
+ "if": {
33
+ "properties": { "enablement": { "const": "app" } },
34
+ "required": ["enablement"]
35
+ },
36
+ "then": {
37
+ "properties": {
38
+ "requires": { "contains": { "const": "appEnablement" } }
39
+ },
40
+ "required": ["requires"]
41
+ }
30
42
  }
31
43
  ],
32
44
  "properties": {
@@ -47,10 +59,33 @@
47
59
  "type": "string",
48
60
  "minLength": 1
49
61
  },
62
+ "enablement": {
63
+ "enum": ["app", "project"],
64
+ "default": "project",
65
+ "description": "Lifecycle ownership for this plugin package. Omission means project-owned enablement."
66
+ },
50
67
  "icon": {
51
- "type": "string",
52
- "minLength": 1,
53
- "description": "Semantic OpenForge icon key or package asset reference."
68
+ "oneOf": [
69
+ {
70
+ "type": "string",
71
+ "minLength": 1,
72
+ "pattern": "\\S"
73
+ },
74
+ {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["type", "svg"],
78
+ "properties": {
79
+ "type": { "const": "svg" },
80
+ "svg": {
81
+ "type": "string",
82
+ "minLength": 1,
83
+ "pattern": "\\S"
84
+ }
85
+ }
86
+ }
87
+ ],
88
+ "description": "Lucide icon name in kebab-case or inline SVG geometry using the PluginIcon contract."
54
89
  },
55
90
  "frontend": {
56
91
  "type": "string",
@@ -101,7 +136,9 @@
101
136
  "config",
102
137
  "projectConfig",
103
138
  "browserSurfaces",
104
- "taskLinks"
139
+ "taskLinks",
140
+ "appEnablement",
141
+ "customSidebarNavigation"
105
142
  ]
106
143
  }
107
144
  }
@@ -0,0 +1,3 @@
1
+ import type { PluginSvgIcon } from './types';
2
+ export declare function isPluginIconName(value: unknown): value is string;
3
+ export declare function isPluginSvgIcon(value: unknown): value is PluginSvgIcon;
@@ -0,0 +1,12 @@
1
+ export function isPluginIconName(value) {
2
+ return typeof value === 'string' && value.trim().length > 0;
3
+ }
4
+ export function isPluginSvgIcon(value) {
5
+ if (value === null || typeof value !== 'object')
6
+ return false;
7
+ const icon = value;
8
+ return Object.keys(icon).length === 2
9
+ && icon.type === 'svg'
10
+ && typeof icon.svg === 'string'
11
+ && icon.svg.trim().length > 0;
12
+ }
@@ -1,4 +1,5 @@
1
1
  import DOMPurify from 'dompurify';
2
+ import { isPluginSvgIcon } from './pluginIconContract';
2
3
  export const MAX_PLUGIN_SVG_ICON_CHARACTERS = 10_000;
3
4
  const ALLOWED_SVG_TAGS = ['svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon'];
4
5
  const ALLOWED_SVG_ATTRIBUTES = [
@@ -33,12 +34,6 @@ const ALLOWED_SVG_ATTRIBUTES = [
33
34
  'points',
34
35
  ];
35
36
  const SAFE_PAINT_VALUE = /^(?:none|currentColor|transparent|#[0-9a-f]{3,8}|[a-z]+|(?:rgb|rgba|hsl|hsla)\([\d\s.,%+\-/]+\))$/iu;
36
- function isPluginSvgIcon(value) {
37
- return value !== null
38
- && typeof value === 'object'
39
- && value.type === 'svg'
40
- && typeof value.svg === 'string';
41
- }
42
37
  function parseSvgRoot(markup) {
43
38
  const template = document.createElement('template');
44
39
  template.innerHTML = markup.trim();
@@ -8,6 +8,7 @@ const PUBLIC_UI_COMPONENT_NAMES = Object.freeze([
8
8
  'Modal',
9
9
  'PluginPageHeader',
10
10
  'PluginViewState',
11
+ 'PluginSidebarLink',
11
12
  'FileTypeIcon',
12
13
  ])
13
14
 
@@ -1,4 +1,4 @@
1
- import type { FrontendOpenForgeAPI, OpenForgeCommonAPI } from '../types';
1
+ import type { BackendOpenForgeAPI, FrontendOpenForgeAPI, OpenForgeCommonAPI } from '../types';
2
2
  import { type TestingRegistryServices } from './support';
3
3
  import type { TestingCommandContribution, TestingEventListenerContribution } from './contracts';
4
4
  export type TestingCommonApi = OpenForgeCommonAPI & Pick<FrontendOpenForgeAPI, 'navigation'>;
@@ -10,6 +10,7 @@ export declare class TestingCommonApiFake {
10
10
  private eventListenerSequence;
11
11
  constructor(services: TestingRegistryServices);
12
12
  createApi(): TestingCommonApi;
13
+ createBackendApi(): TestingCommonApi & Pick<BackendOpenForgeAPI, 'fs'>;
13
14
  getSnapshot(): {
14
15
  commands: TestingCommandContribution[];
15
16
  eventListeners: TestingEventListenerContribution[];
@@ -136,7 +136,7 @@ export class TestingCommonApiFake {
136
136
  },
137
137
  getBuffer: async (request) => {
138
138
  this.services.calls.shellBuffers.push(request);
139
- return null;
139
+ return { buffer: null, isLive: false };
140
140
  },
141
141
  },
142
142
  notifications: {
@@ -183,6 +183,38 @@ export class TestingCommonApiFake {
183
183
  };
184
184
  return api;
185
185
  }
186
+ createBackendApi() {
187
+ const api = this.createApi();
188
+ return {
189
+ ...api,
190
+ fs: {
191
+ ...api.fs,
192
+ userData: {
193
+ readDir: async (request = {}) => {
194
+ this.services.calls.fsUserDataReadDirs.push(request);
195
+ return [];
196
+ },
197
+ readTextFile: async (request) => {
198
+ this.services.calls.fsUserDataReads.push(request);
199
+ return '';
200
+ },
201
+ writeTextFile: async (request) => {
202
+ this.services.calls.fsUserDataWrites.push(request);
203
+ },
204
+ },
205
+ external: {
206
+ readDir: async (request) => {
207
+ this.services.calls.fsExternalReadDirs.push(request);
208
+ return [];
209
+ },
210
+ readTextFile: async (request) => {
211
+ this.services.calls.fsExternalReads.push(request);
212
+ return '';
213
+ },
214
+ },
215
+ },
216
+ };
217
+ }
186
218
  getSnapshot() {
187
219
  return {
188
220
  commands: Array.from(this.commands.values()),
@@ -1,6 +1,6 @@
1
1
  import type { BrowserSurfaceVisualFeedback } from '../browserSurfaces';
2
2
  import type { TestingOpenForgeRegistryFake } from './registryFake';
3
- import type { BackendMethodRegistration, BackendOpenForgeAPI, BackgroundServiceRegistration, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ConfigureStartPromptContributionRequest, CreateTaskRequest, FrontendOpenForgeAPI, InjectionPointLocation, JsonValue, NotificationRequest, OpenForgeNavigationRequest, OpenForgePackageMetadata, PluginSettingsSectionRegistration, PluginStorage, PluginTaskPaneTabRegistration, PluginTaskUISectionRegistration, PluginViewRegistration, ShellSpawnRequest, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskLinkOpenRequest, TaskStartPrefixContext } from '../types';
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, ExternalReadDirectoryRequest, ExternalReadFileRequest, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileWriteRequest } from '../types';
4
4
  import type { Task } from '../domain';
5
5
  export type TestingRuntimeScope = 'global' | 'project' | 'task';
6
6
  export type TestingRuntimeKind = 'commands' | 'events' | 'views' | 'taskPane' | 'taskUI' | 'settings' | 'backend' | 'background';
@@ -74,6 +74,11 @@ export interface TestingOpenForgeApiCalls {
74
74
  path: string;
75
75
  content: string;
76
76
  }>;
77
+ fsUserDataReadDirs: UserDataDirectoryRequest[];
78
+ fsUserDataReads: UserDataFileRequest[];
79
+ fsUserDataWrites: UserDataFileWriteRequest[];
80
+ fsExternalReadDirs: ExternalReadDirectoryRequest[];
81
+ fsExternalReads: ExternalReadFileRequest[];
77
82
  shellSpawns: ShellSpawnRequest[];
78
83
  shellWrites: Array<{
79
84
  taskId: string;
@@ -102,6 +102,15 @@ export class TestingFrontendContributionFake {
102
102
  const qualifiedId = this.services.localQualifiedId('views', registration.id);
103
103
  assertTitle('views', registration.title);
104
104
  assertFunction('views', 'component', registration.component);
105
+ if (registration.navigationComponent !== undefined) {
106
+ assertFunction('views', 'navigationComponent', registration.navigationComponent);
107
+ if (registration.placement !== 'sidebar') {
108
+ throw new Error('views registration custom navigation requires sidebar placement');
109
+ }
110
+ if (!this.services.packageMetadata.requires?.includes('customSidebarNavigation')) {
111
+ throw new Error('views registration custom navigation requires the customSidebarNavigation capability');
112
+ }
113
+ }
105
114
  this.services.claims.claim('views', qualifiedId);
106
115
  const contribution = {
107
116
  ...registration,
@@ -58,7 +58,7 @@ export class TestingOpenForgeRegistryFake {
58
58
  if (this.cachedBackendApi)
59
59
  return this.cachedBackendApi;
60
60
  const api = {
61
- ...this.commonApi.createApi(),
61
+ ...this.commonApi.createBackendApi(),
62
62
  ...this.backendServices.createApi(),
63
63
  __testing: {
64
64
  calls: this.calls,
@@ -48,6 +48,11 @@ export function createTestingCalls() {
48
48
  taskStatusUpdates: [],
49
49
  configWrites: [],
50
50
  fsWrites: [],
51
+ fsUserDataReadDirs: [],
52
+ fsUserDataReads: [],
53
+ fsUserDataWrites: [],
54
+ fsExternalReadDirs: [],
55
+ fsExternalReads: [],
51
56
  shellSpawns: [],
52
57
  shellWrites: [],
53
58
  shellResizes: [],
package/dist/types.d.ts CHANGED
@@ -17,13 +17,16 @@ export interface ValidationError {
17
17
  path: string;
18
18
  message: string;
19
19
  }
20
- export type OpenForgePluginCapability = 'commands' | 'events' | 'views' | 'taskPane' | 'taskStart' | 'settings' | 'background' | 'backend' | 'storage' | 'context' | 'navigation' | 'tasks' | 'projects' | 'fs' | 'shell' | 'notifications' | 'attention' | 'system.openUrl' | 'system.writeClipboardText' | 'config' | 'projectConfig' | 'browserSurfaces' | 'taskLinks';
20
+ declare const OPENFORGE_PLUGIN_CAPABILITY_TYPE_MEMBERS: readonly ["commands", "events", "views", "injectionPoints", "taskPane", "taskStart", "settings", "background", "backend", "storage", "context", "navigation", "tasks", "projects", "fs", "shell", "notifications", "attention", "system.openUrl", "system.writeClipboardText", "config", "projectConfig", "browserSurfaces", "taskLinks", "appEnablement", "customSidebarNavigation"];
21
+ export type OpenForgePluginCapability = (typeof OPENFORGE_PLUGIN_CAPABILITY_TYPE_MEMBERS)[number];
21
22
  export interface OpenForgePackageMetadata {
22
23
  id: string;
23
24
  apiVersion: SupportedOpenForgeApiVersion;
24
25
  displayName: string;
25
26
  description: string;
26
- icon?: string;
27
+ /** Defaults to project-owned lifecycle when omitted. */
28
+ enablement?: 'app' | 'project';
29
+ icon?: PluginIcon;
27
30
  frontend?: string;
28
31
  frontendStyles?: string[];
29
32
  backend?: string;
@@ -188,6 +191,21 @@ export interface PluginViewProps extends Record<string, unknown> {
188
191
  api: FrontendOpenForgeAPI;
189
192
  context: OpenForgeContextSnapshot;
190
193
  }
194
+ export interface PluginSidebarViewIdentity {
195
+ pluginId: string;
196
+ id: string;
197
+ qualifiedId: string;
198
+ title: string;
199
+ icon: PluginIcon;
200
+ }
201
+ export interface PluginSidebarNavigationProps extends Record<string, unknown> {
202
+ api: FrontendOpenForgeAPI;
203
+ context: OpenForgeContextSnapshot;
204
+ active: boolean;
205
+ collapsed: boolean;
206
+ view: PluginSidebarViewIdentity;
207
+ onActivate: () => void;
208
+ }
191
209
  export interface PluginTaskPaneProps extends Record<string, unknown> {
192
210
  api: FrontendOpenForgeAPI;
193
211
  context: OpenForgeContextSnapshot;
@@ -230,6 +248,8 @@ export interface PluginViewRegistration {
230
248
  order?: number;
231
249
  shortcut?: string;
232
250
  component: PluginComponentLoader<PluginViewProps> | PluginComponent<PluginViewProps>;
251
+ /** Custom content for a sidebar-placed View's host-owned navigation slot. */
252
+ navigationComponent?: PluginComponentLoader<PluginSidebarNavigationProps> | PluginComponent<PluginSidebarNavigationProps>;
233
253
  }
234
254
  export interface PluginTaskPaneTabRegistration {
235
255
  id: string;
@@ -353,6 +373,36 @@ export interface FileSystemAPI {
353
373
  limit?: number;
354
374
  }): Promise<string[]>;
355
375
  }
376
+ export interface UserDataDirectoryRequest {
377
+ path?: string | null;
378
+ }
379
+ export interface UserDataFileRequest {
380
+ path: string;
381
+ }
382
+ export interface UserDataFileWriteRequest extends UserDataFileRequest {
383
+ content: string;
384
+ }
385
+ export interface ExternalReadDirectoryRequest {
386
+ root: string;
387
+ path?: string | null;
388
+ }
389
+ export interface ExternalReadFileRequest {
390
+ root: string;
391
+ path: string;
392
+ }
393
+ export interface UserDataFileSystemAPI {
394
+ readDir(request?: UserDataDirectoryRequest): Promise<FileEntry[]>;
395
+ readTextFile(request: UserDataFileRequest): Promise<string>;
396
+ writeTextFile(request: UserDataFileWriteRequest): Promise<void>;
397
+ }
398
+ export interface ExternalReadFileSystemAPI {
399
+ readDir(request: ExternalReadDirectoryRequest): Promise<FileEntry[]>;
400
+ readTextFile(request: ExternalReadFileRequest): Promise<string>;
401
+ }
402
+ export interface BackendFileSystemAPI extends FileSystemAPI {
403
+ userData: UserDataFileSystemAPI;
404
+ external: ExternalReadFileSystemAPI;
405
+ }
356
406
  export type TerminalImageProtocol = 'iterm2';
357
407
  export interface ShellSessionRequest {
358
408
  taskId: string;
@@ -371,12 +421,16 @@ export interface ShellResizeRequest extends ShellSessionRequest {
371
421
  cols: number;
372
422
  rows: number;
373
423
  }
424
+ export interface PtyBufferState {
425
+ buffer: string | null;
426
+ isLive: boolean;
427
+ }
374
428
  export interface ShellAPI {
375
429
  spawn(request: ShellSpawnRequest): Promise<number>;
376
430
  write(request: ShellWriteRequest): Promise<void>;
377
431
  resize(request: ShellResizeRequest): Promise<void>;
378
432
  kill(request: ShellSessionRequest): Promise<void>;
379
- getBuffer(request: ShellSessionRequest): Promise<string | null>;
433
+ getBuffer(request: ShellSessionRequest): Promise<PtyBufferState>;
380
434
  }
381
435
  export interface CreateTaskRequest {
382
436
  initialPrompt: string;
@@ -518,6 +572,7 @@ export interface FrontendOpenForgeAPI extends OpenForgeCommonAPI {
518
572
  taskStart: FrontendTaskStartRegistry;
519
573
  }
520
574
  export interface BackendOpenForgeAPI extends OpenForgeCommonAPI {
575
+ fs: BackendFileSystemAPI;
521
576
  backend: BackendMethodRegistry;
522
577
  background: BackgroundServiceRegistry;
523
578
  }
@@ -534,3 +589,4 @@ export declare function parsePluginViewKey(key: PluginViewKey): {
534
589
  pluginId: string;
535
590
  viewId: string;
536
591
  };
592
+ export {};
package/dist/types.js CHANGED
@@ -10,6 +10,45 @@ export const SUPPORTED_OPENFORGE_API_VERSIONS = Object.freeze(readSupportedOpenF
10
10
  export const OPENFORGE_PLUGIN_API_VERSION = SUPPORTED_OPENFORGE_API_VERSIONS[0];
11
11
  export const MIN_SUPPORTED_API_VERSION = Math.min(...SUPPORTED_OPENFORGE_API_VERSIONS);
12
12
  export const MAX_SUPPORTED_API_VERSION = Math.max(...SUPPORTED_OPENFORGE_API_VERSIONS);
13
+ const OPENFORGE_PLUGIN_CAPABILITY_TYPE_MEMBERS = [
14
+ 'commands',
15
+ 'events',
16
+ 'views',
17
+ 'injectionPoints',
18
+ 'taskPane',
19
+ 'taskStart',
20
+ 'settings',
21
+ 'background',
22
+ 'backend',
23
+ 'storage',
24
+ 'context',
25
+ 'navigation',
26
+ 'tasks',
27
+ 'projects',
28
+ 'fs',
29
+ 'shell',
30
+ 'notifications',
31
+ 'attention',
32
+ 'system.openUrl',
33
+ 'system.writeClipboardText',
34
+ 'config',
35
+ 'projectConfig',
36
+ 'browserSurfaces',
37
+ 'taskLinks',
38
+ 'appEnablement',
39
+ 'customSidebarNavigation',
40
+ ];
41
+ function assertOpenForgePluginCapabilitiesMatchSchema() {
42
+ const schemaCapabilities = packageMetadataSchemaData.properties.requires.items.enum;
43
+ if (!Array.isArray(schemaCapabilities) || !schemaCapabilities.every((capability) => typeof capability === 'string')) {
44
+ throw new Error('openforgePackageMetadataSchema.json properties.requires.items.enum must contain only strings');
45
+ }
46
+ if (schemaCapabilities.length !== OPENFORGE_PLUGIN_CAPABILITY_TYPE_MEMBERS.length
47
+ || schemaCapabilities.some((capability, index) => capability !== OPENFORGE_PLUGIN_CAPABILITY_TYPE_MEMBERS[index])) {
48
+ throw new Error('OpenForgePluginCapability must match openforgePackageMetadataSchema.json properties.requires.items.enum');
49
+ }
50
+ }
51
+ assertOpenForgePluginCapabilitiesMatchSchema();
13
52
  export class TaskFollowUpError extends Error {
14
53
  code;
15
54
  constructor(code, message) {
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ interface Props {
5
+ accessibleName: string
6
+ active: boolean
7
+ collapsed: boolean
8
+ onActivate: () => void
9
+ leading?: Snippet
10
+ label?: Snippet
11
+ trailing?: Snippet
12
+ class?: string
13
+ }
14
+
15
+ let {
16
+ accessibleName,
17
+ active,
18
+ collapsed,
19
+ onActivate,
20
+ leading,
21
+ label,
22
+ trailing,
23
+ class: className,
24
+ }: Props = $props()
25
+
26
+ function handleKeydown(event: KeyboardEvent): void {
27
+ if (event.key !== 'Enter' && event.key !== ' ') return
28
+ event.preventDefault()
29
+ onActivate()
30
+ }
31
+ </script>
32
+
33
+ <button
34
+ type="button"
35
+ class={[
36
+ 'relative mx-2 flex min-h-11 w-auto 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
+ collapsed ? 'justify-center px-0' : 'px-3',
38
+ active ? 'bg-primary/10 text-primary' : 'text-base-content/55 hover:bg-base-200 hover:text-base-content',
39
+ className,
40
+ ]}
41
+ title={collapsed ? accessibleName : undefined}
42
+ aria-label={accessibleName}
43
+ aria-current={active ? 'page' : undefined}
44
+ onclick={onActivate}
45
+ onkeydown={handleKeydown}
46
+ >
47
+ {#if leading}
48
+ <span class="relative shrink-0">{@render leading()}</span>
49
+ {/if}
50
+ {#if !collapsed}
51
+ {#if label}<span class="min-w-0 text-sm font-medium">{@render label()}</span>{/if}
52
+ {#if trailing}<span class="ml-auto shrink-0">{@render trailing()}</span>{/if}
53
+ {/if}
54
+ </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openforge-app/plugin-sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -71,6 +71,7 @@
71
71
  "./ui/Modal.svelte": "./dist/ui/Modal.svelte",
72
72
  "./ui/PluginPageHeader.svelte": "./dist/ui/PluginPageHeader.svelte",
73
73
  "./ui/PluginViewState.svelte": "./dist/ui/PluginViewState.svelte",
74
+ "./ui/PluginSidebarLink.svelte": "./dist/ui/PluginSidebarLink.svelte",
74
75
  "./ui/FileTypeIcon.svelte": "./dist/ui/FileTypeIcon.svelte"
75
76
  },
76
77
  "files": [