@openforge-app/plugin-sdk 0.2.3 → 0.2.5
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 +1 -7
- package/dist/backend.d.ts +2 -2
- package/dist/context.d.ts +2 -1
- package/dist/context.js +3 -0
- package/dist/domain.d.ts +2 -0
- package/dist/frontend.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +53 -4
- package/dist/manifest.js +18 -1
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +49 -3
- package/dist/openforgePackageMetadataSchema.json +41 -4
- package/dist/pluginIconContract.d.ts +3 -0
- package/dist/pluginIconContract.js +12 -0
- package/dist/pluginIcons.js +1 -6
- package/dist/publicUiExports.mjs +1 -0
- package/dist/testing/commonApiFake.d.ts +2 -1
- package/dist/testing/commonApiFake.js +64 -1
- package/dist/testing/contracts.d.ts +15 -1
- package/dist/testing/frontendContributionFake.js +9 -0
- package/dist/testing/registryFake.js +2 -1
- package/dist/testing/support.d.ts +2 -1
- package/dist/testing/support.js +8 -0
- package/dist/testing.d.ts +1 -1
- package/dist/types.d.ts +74 -3
- package/dist/types.js +52 -0
- package/dist/ui/MarkdownContent.svelte +54 -2
- package/dist/ui/PluginSidebarLink.svelte +54 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @openforge-app/plugin-sdk
|
|
2
2
|
|
|
3
|
-
Public SDK for building OpenForge plugins.
|
|
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, ExternalReadTextFileChunksRequest, ExternalReadFileSystemAPI, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, NavigationAPI, OpenForgeContextChangeHandler, 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, ExternalReadTextFileChunksRequest, ExternalReadFileSystemAPI, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, NavigationAPI, OpenForgeContextChangeHandler, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, };
|
package/dist/context.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Disposable, OpenForgePackageMetadata, OpenForgePluginContext, SubscriptionSink, SupportedOpenForgeApiVersion } from './types';
|
|
1
|
+
import type { Disposable, OpenForgeContextChangeHandler, OpenForgePackageMetadata, OpenForgePluginContext, SubscriptionSink, SupportedOpenForgeApiVersion } from './types';
|
|
2
2
|
declare class SubscriptionSet implements SubscriptionSink {
|
|
3
3
|
private readonly subscriptions;
|
|
4
4
|
add(subscription: Disposable | (() => void)): void;
|
|
@@ -9,6 +9,7 @@ export declare class PluginContextImpl implements OpenForgePluginContext {
|
|
|
9
9
|
readonly apiVersion: SupportedOpenForgeApiVersion;
|
|
10
10
|
readonly packageMetadata: OpenForgePackageMetadata;
|
|
11
11
|
readonly subscriptions: SubscriptionSet;
|
|
12
|
+
onDidChange(_handler: OpenForgeContextChangeHandler): Disposable;
|
|
12
13
|
constructor(pluginId: string, apiVersion: SupportedOpenForgeApiVersion, packageMetadata: OpenForgePackageMetadata);
|
|
13
14
|
}
|
|
14
15
|
export {};
|
package/dist/context.js
CHANGED
|
@@ -21,6 +21,9 @@ export class PluginContextImpl {
|
|
|
21
21
|
apiVersion;
|
|
22
22
|
packageMetadata;
|
|
23
23
|
subscriptions;
|
|
24
|
+
onDidChange(_handler) {
|
|
25
|
+
return { dispose: () => undefined };
|
|
26
|
+
}
|
|
24
27
|
constructor(pluginId, apiVersion, packageMetadata) {
|
|
25
28
|
this.pluginId = pluginId;
|
|
26
29
|
this.apiVersion = apiVersion;
|
package/dist/domain.d.ts
CHANGED
|
@@ -150,6 +150,7 @@ export interface PullRequestInfo {
|
|
|
150
150
|
merge_queue_state: string | null;
|
|
151
151
|
readiness_updated_at: number | null;
|
|
152
152
|
}
|
|
153
|
+
export type PollOutcome = 'completed' | 'missing_github_token' | 'github_token_unavailable' | 'failed' | 'rate_limited';
|
|
153
154
|
export interface PollResult {
|
|
154
155
|
new_comments: number;
|
|
155
156
|
ci_changes: number;
|
|
@@ -158,6 +159,7 @@ export interface PollResult {
|
|
|
158
159
|
errors: number;
|
|
159
160
|
rate_limited: boolean;
|
|
160
161
|
rate_limit_reset_at: number | null;
|
|
162
|
+
outcome: PollOutcome;
|
|
161
163
|
}
|
|
162
164
|
export interface MergeStatusInfo {
|
|
163
165
|
state: string;
|
package/dist/frontend.d.ts
CHANGED
|
@@ -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, OpenForgeContextChangeHandler, 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, OpenForgeContextChangeHandler, 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
|
@@ -2,9 +2,9 @@ export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfac
|
|
|
2
2
|
export type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState, } from './browserSurfaces';
|
|
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
|
-
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';
|
|
7
|
-
export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingEventListenerContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingTaskUISectionContribution, TestingViewContribution, } from './testing';
|
|
5
|
+
export { 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 { AgentCommandDescriptor, AgentCommandMetadata, AgentCommandRuntime, AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, BackendFileSystemAPI, ExternalReadDirectoryRequest, ExternalReadFileRequest, ExternalReadTextFileChunksRequest, ExternalReadFileSystemAPI, FileSystemAPI, UserDataDirectoryRequest, UserDataFileRequest, UserDataFileSystemAPI, UserDataFileWriteRequest, ImplementationRun, 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, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskFollowUpDisposition, TaskFollowUpErrorCode, TaskFollowUpReceipt, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, TasksAPI, ValidationError, } from './types';
|
|
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';
|
|
10
10
|
export type { ProjectFileTreeEntry, ProjectFileTreeItemAccessibility, ProjectFileTreeKeyboardAction, ProjectFileTreeNode, } from './projectFileTree';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
|
|
2
2
|
export { OPENFORGE_PACKAGE_METADATA_SCHEMA, OPENFORGE_PLUGIN_CAPABILITIES, isOpenForgePackageMetadata, isPluginPackageMetadata, isSupportedOpenForgeApiVersion, validateOpenForgePackageMetadata, validatePluginPackageMetadata, } from './manifest';
|
|
3
3
|
export { createMemoryPluginStorage, createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, createTestingCalls, TestingOpenForgeRegistryFake, TestingSubscriptionSink, } from './testing';
|
|
4
|
-
export { TaskFollowUpError, MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
|
|
4
|
+
export { 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';
|
|
5
5
|
export { parseStrictFiniteNumber } from './numberParsing';
|
|
6
6
|
export { buildProjectFileTree, flattenVisibleProjectFileTree, formatProjectFileTreeSize, getProjectFileTreeDepth, getProjectFileTreeItemAccessibility, getProjectFileTreeKeyboardAction, getProjectFileTreeParentPath, hasProjectFileTreeShortcutModifier, projectFileTreePathToId, } from './projectFileTree';
|
|
7
7
|
export { canMergePullRequest, getMergeReadiness, hasMergeConflicts, isClosedUnmergedPullRequest, isMergedPullRequest, isQueuedForMerge, isReadyToMerge, parseCheckRuns, preservePullRequestState, splitCheckRuns, } from './domain';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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
|
+
requires?: undefined;
|
|
34
|
+
enablement: {
|
|
35
|
+
const: string;
|
|
36
|
+
};
|
|
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
|
-
|
|
49
|
-
|
|
75
|
+
oneOf: ({
|
|
76
|
+
properties?: undefined;
|
|
77
|
+
type: string;
|
|
78
|
+
minLength: number;
|
|
79
|
+
pattern: string;
|
|
80
|
+
additionalProperties?: undefined;
|
|
81
|
+
required?: undefined;
|
|
82
|
+
} | {
|
|
83
|
+
minLength?: undefined;
|
|
84
|
+
pattern?: undefined;
|
|
85
|
+
type: string;
|
|
86
|
+
additionalProperties: boolean;
|
|
87
|
+
required: string[];
|
|
88
|
+
properties: {
|
|
89
|
+
type: {
|
|
90
|
+
const: string;
|
|
91
|
+
};
|
|
92
|
+
svg: {
|
|
93
|
+
type: string;
|
|
94
|
+
minLength: number;
|
|
95
|
+
pattern: string;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
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(...
|
|
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
|
}
|
package/dist/markdown.d.ts
CHANGED
|
@@ -2,7 +2,15 @@ export interface RenderMarkdownOptions {
|
|
|
2
2
|
imageBaseUrl?: string | null;
|
|
3
3
|
markdownFilePath?: string | null;
|
|
4
4
|
deferRepositoryImages?: boolean;
|
|
5
|
+
deferRemoteMedia?: boolean;
|
|
5
6
|
}
|
|
7
|
+
/** How a deferred remote URL should be presented once it has been resolved. */
|
|
8
|
+
export interface ResolvedMarkdownMedia {
|
|
9
|
+
url: string;
|
|
10
|
+
kind: 'image' | 'video';
|
|
11
|
+
}
|
|
12
|
+
/** Attribute holding the original URL of a `<img>`/`<a>` awaiting resolution. */
|
|
13
|
+
export declare const MARKDOWN_REMOTE_MEDIA_ATTRIBUTE = "data-markdown-remote-src";
|
|
6
14
|
export declare function resolveMarkdownRepositoryPath(value: string | null, markdownFilePath: string): string | null;
|
|
7
15
|
export declare function getMarkdownRepositoryLinkSuffix(value: string): string;
|
|
8
16
|
export declare function resolveMarkdownImageSrc(src: string | null, imageBaseUrl: string | null | undefined, markdownFilePath?: string): string | null;
|
package/dist/markdown.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import { sanitizeHtml } from './sanitize';
|
|
3
|
+
/** Attribute holding the original URL of a `<img>`/`<a>` awaiting resolution. */
|
|
4
|
+
export const MARKDOWN_REMOTE_MEDIA_ATTRIBUTE = 'data-markdown-remote-src';
|
|
3
5
|
const markedOptions = {
|
|
4
6
|
gfm: true,
|
|
5
7
|
breaks: true,
|
|
@@ -71,13 +73,57 @@ export function resolveMarkdownImageSrc(src, imageBaseUrl, markdownFilePath = ''
|
|
|
71
73
|
return null;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
+
function isRemoteUrl(value) {
|
|
77
|
+
return /^https?:\/\//i.test(value);
|
|
78
|
+
}
|
|
79
|
+
const TEXT_NODE = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Whether nothing but a line break separates `anchor` from the end of its line.
|
|
82
|
+
* GitHub only turns a bare URL into an embedded player when it stands on its own
|
|
83
|
+
* line, so link markup mid-sentence stays a link.
|
|
84
|
+
*/
|
|
85
|
+
function isAtLineBoundary(from, step) {
|
|
86
|
+
let node = from;
|
|
87
|
+
while (node) {
|
|
88
|
+
if (node.nodeType !== TEXT_NODE)
|
|
89
|
+
return node.nodeName === 'BR';
|
|
90
|
+
if ((node.textContent ?? '').trim() !== '')
|
|
91
|
+
return false;
|
|
92
|
+
node = node[step];
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function isBareLinkOnItsOwnLine(anchor, href) {
|
|
97
|
+
return (anchor.textContent ?? '').trim() === href
|
|
98
|
+
&& isAtLineBoundary(anchor.previousSibling, 'previousSibling')
|
|
99
|
+
&& isAtLineBoundary(anchor.nextSibling, 'nextSibling');
|
|
100
|
+
}
|
|
101
|
+
function deferRemoteMediaLinks(root) {
|
|
102
|
+
for (const anchor of root.querySelectorAll('a[href]')) {
|
|
103
|
+
const href = anchor.getAttribute('href')?.trim() ?? '';
|
|
104
|
+
if (!isRemoteUrl(href) || !isBareLinkOnItsOwnLine(anchor, href))
|
|
105
|
+
continue;
|
|
106
|
+
anchor.setAttribute(MARKDOWN_REMOTE_MEDIA_ATTRIBUTE, href);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function prepareMarkdownMediaSources(html, options) {
|
|
110
|
+
if (typeof document === 'undefined' ||
|
|
111
|
+
(!options.imageBaseUrl && !options.deferRepositoryImages && !options.deferRemoteMedia))
|
|
76
112
|
return html;
|
|
77
113
|
const template = document.createElement('template');
|
|
78
114
|
template.innerHTML = html;
|
|
115
|
+
if (options.deferRemoteMedia) {
|
|
116
|
+
deferRemoteMediaLinks(template.content);
|
|
117
|
+
}
|
|
79
118
|
for (const image of template.content.querySelectorAll('img[src]')) {
|
|
80
119
|
const source = image.getAttribute('src')?.trim() ?? '';
|
|
120
|
+
// Hold the remote URL back so the caller can exchange it for one the
|
|
121
|
+
// renderer can actually load, instead of flashing a broken image first.
|
|
122
|
+
if (options.deferRemoteMedia && isRemoteUrl(source)) {
|
|
123
|
+
image.removeAttribute('src');
|
|
124
|
+
image.setAttribute(MARKDOWN_REMOTE_MEDIA_ATTRIBUTE, source);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
81
127
|
const shouldDefer = options.deferRepositoryImages && source.length > 0 && !hasAbsoluteOrSpecialUrl(source);
|
|
82
128
|
if (shouldDefer) {
|
|
83
129
|
image.removeAttribute('src');
|
|
@@ -99,5 +145,5 @@ function prepareMarkdownImageSources(html, options) {
|
|
|
99
145
|
}
|
|
100
146
|
export function renderMarkdownHtml(content, options = {}) {
|
|
101
147
|
const rawHtml = marked.parse(content, markedOptions);
|
|
102
|
-
return sanitizeHtml(
|
|
148
|
+
return sanitizeHtml(prepareMarkdownMediaSources(rawHtml, options));
|
|
103
149
|
}
|
|
@@ -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
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
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,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
|
+
}
|
package/dist/pluginIcons.js
CHANGED
|
@@ -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();
|
package/dist/publicUiExports.mjs
CHANGED
|
@@ -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[];
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
+
import { resolveExternalTextFileChunkSize } from '../types';
|
|
1
2
|
import { assertFunction, assertTitle, commandDescriptor, createDisposable, isJsonValue, normalizeAgentCommandMetadata, } from './support';
|
|
3
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
4
|
+
function* splitExternalTextFile(content, maxBytes) {
|
|
5
|
+
let chunk = '';
|
|
6
|
+
let chunkBytes = 0;
|
|
7
|
+
for (const character of content) {
|
|
8
|
+
const characterBytes = UTF8_ENCODER.encode(character).byteLength;
|
|
9
|
+
if (chunkBytes + characterBytes > maxBytes && chunk.length > 0) {
|
|
10
|
+
yield chunk;
|
|
11
|
+
chunk = '';
|
|
12
|
+
chunkBytes = 0;
|
|
13
|
+
}
|
|
14
|
+
chunk += character;
|
|
15
|
+
chunkBytes += characterBytes;
|
|
16
|
+
}
|
|
17
|
+
if (chunk.length > 0)
|
|
18
|
+
yield chunk;
|
|
19
|
+
}
|
|
2
20
|
export class TestingCommonApiFake {
|
|
3
21
|
services;
|
|
4
22
|
commands = new Map();
|
|
@@ -136,7 +154,7 @@ export class TestingCommonApiFake {
|
|
|
136
154
|
},
|
|
137
155
|
getBuffer: async (request) => {
|
|
138
156
|
this.services.calls.shellBuffers.push(request);
|
|
139
|
-
return null;
|
|
157
|
+
return { buffer: null, isLive: false };
|
|
140
158
|
},
|
|
141
159
|
},
|
|
142
160
|
notifications: {
|
|
@@ -183,6 +201,51 @@ export class TestingCommonApiFake {
|
|
|
183
201
|
};
|
|
184
202
|
return api;
|
|
185
203
|
}
|
|
204
|
+
createBackendApi() {
|
|
205
|
+
const api = this.createApi();
|
|
206
|
+
return {
|
|
207
|
+
...api,
|
|
208
|
+
fs: {
|
|
209
|
+
...api.fs,
|
|
210
|
+
userData: {
|
|
211
|
+
readDir: async (request = {}) => {
|
|
212
|
+
this.services.calls.fsUserDataReadDirs.push(request);
|
|
213
|
+
return [];
|
|
214
|
+
},
|
|
215
|
+
readTextFile: async (request) => {
|
|
216
|
+
this.services.calls.fsUserDataReads.push(request);
|
|
217
|
+
return '';
|
|
218
|
+
},
|
|
219
|
+
writeTextFile: async (request) => {
|
|
220
|
+
this.services.calls.fsUserDataWrites.push(request);
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
external: {
|
|
224
|
+
readDir: async (request) => {
|
|
225
|
+
this.services.calls.fsExternalReadDirs.push(request);
|
|
226
|
+
return [];
|
|
227
|
+
},
|
|
228
|
+
readTextFile: async (request) => {
|
|
229
|
+
this.services.calls.fsExternalReads.push(request);
|
|
230
|
+
return '';
|
|
231
|
+
},
|
|
232
|
+
readTextFileChunks: (request) => {
|
|
233
|
+
const chunkSizeBytes = resolveExternalTextFileChunkSize(request.chunkSizeBytes);
|
|
234
|
+
const { root, path, signal } = request;
|
|
235
|
+
this.services.calls.fsExternalReadTextFileChunks.push({ root, path, chunkSizeBytes });
|
|
236
|
+
const content = this.services.externalTextFiles.find(file => file.root === root && file.path === path)?.content ?? '';
|
|
237
|
+
return (async function* () {
|
|
238
|
+
for (const chunk of splitExternalTextFile(content, chunkSizeBytes)) {
|
|
239
|
+
signal?.throwIfAborted();
|
|
240
|
+
yield chunk;
|
|
241
|
+
}
|
|
242
|
+
signal?.throwIfAborted();
|
|
243
|
+
})();
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
186
249
|
getSnapshot() {
|
|
187
250
|
return {
|
|
188
251
|
commands: Array.from(this.commands.values()),
|
|
@@ -1,12 +1,18 @@
|
|
|
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, ExternalReadTextFileChunksRequest, 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';
|
|
7
7
|
export type TestingMaybePromise<T> = T | Promise<T>;
|
|
8
8
|
export type TestingCommandHandler = (payload?: unknown) => TestingMaybePromise<unknown>;
|
|
9
9
|
export type TestingEventHandler = (payload: unknown) => void;
|
|
10
|
+
export interface TestingExternalTextFile extends ExternalReadFileRequest {
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
export type TestingExternalTextFileChunksCall = Omit<ExternalReadTextFileChunksRequest, 'signal' | 'chunkSizeBytes'> & {
|
|
14
|
+
chunkSizeBytes: number;
|
|
15
|
+
};
|
|
10
16
|
export interface TestingOpenForgeApiOptions {
|
|
11
17
|
pluginId?: string;
|
|
12
18
|
projectId?: string | null;
|
|
@@ -21,6 +27,8 @@ export interface TestingOpenForgeApiOptions {
|
|
|
21
27
|
* mirroring the host capability. Defaults to an empty list.
|
|
22
28
|
*/
|
|
23
29
|
tasks?: Task[];
|
|
30
|
+
/** UTF-8 files returned by `fs.external.readTextFileChunks`. Defaults to none. */
|
|
31
|
+
externalTextFiles?: TestingExternalTextFile[];
|
|
24
32
|
}
|
|
25
33
|
export interface TestingOpenForgeApiCalls {
|
|
26
34
|
commandInvocations: Array<{
|
|
@@ -74,6 +82,12 @@ export interface TestingOpenForgeApiCalls {
|
|
|
74
82
|
path: string;
|
|
75
83
|
content: string;
|
|
76
84
|
}>;
|
|
85
|
+
fsUserDataReadDirs: UserDataDirectoryRequest[];
|
|
86
|
+
fsUserDataReads: UserDataFileRequest[];
|
|
87
|
+
fsUserDataWrites: UserDataFileWriteRequest[];
|
|
88
|
+
fsExternalReadDirs: ExternalReadDirectoryRequest[];
|
|
89
|
+
fsExternalReads: ExternalReadFileRequest[];
|
|
90
|
+
fsExternalReadTextFileChunks: TestingExternalTextFileChunksCall[];
|
|
77
91
|
shellSpawns: ShellSpawnRequest[];
|
|
78
92
|
shellWrites: Array<{
|
|
79
93
|
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.
|
|
61
|
+
...this.commonApi.createBackendApi(),
|
|
62
62
|
...this.backendServices.createApi(),
|
|
63
63
|
__testing: {
|
|
64
64
|
calls: this.calls,
|
|
@@ -104,6 +104,7 @@ export class TestingOpenForgeRegistryFake {
|
|
|
104
104
|
apiVersion: 1,
|
|
105
105
|
packageMetadata: this.packageMetadata,
|
|
106
106
|
subscriptions,
|
|
107
|
+
onDidChange: () => ({ dispose: () => undefined }),
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentCommandMetadata, CommandDescriptor, Disposable, JsonValue, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, PluginStorage, StartPromptContribution, SubscriptionSink } from '../types';
|
|
2
2
|
import type { Task } from '../domain';
|
|
3
|
-
import type { TestingCommandContribution, TestingMaybePromise, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingRuntimeKind } from './contracts';
|
|
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 {
|
|
6
6
|
readonly subscriptions: Disposable[];
|
|
@@ -31,6 +31,7 @@ export declare class TestingRegistryServices {
|
|
|
31
31
|
readonly storage: PluginStorage;
|
|
32
32
|
readonly config: Map<string, JsonValue>;
|
|
33
33
|
readonly seededTasks: Task[];
|
|
34
|
+
readonly externalTextFiles: TestingExternalTextFile[];
|
|
34
35
|
readonly claims: TestingContributionClaims;
|
|
35
36
|
constructor(options?: TestingOpenForgeApiOptions);
|
|
36
37
|
localQualifiedId(kind: TestingRuntimeKind, id: string): string;
|
package/dist/testing/support.js
CHANGED
|
@@ -48,6 +48,12 @@ export function createTestingCalls() {
|
|
|
48
48
|
taskStatusUpdates: [],
|
|
49
49
|
configWrites: [],
|
|
50
50
|
fsWrites: [],
|
|
51
|
+
fsUserDataReadDirs: [],
|
|
52
|
+
fsUserDataReads: [],
|
|
53
|
+
fsUserDataWrites: [],
|
|
54
|
+
fsExternalReadDirs: [],
|
|
55
|
+
fsExternalReads: [],
|
|
56
|
+
fsExternalReadTextFileChunks: [],
|
|
51
57
|
shellSpawns: [],
|
|
52
58
|
shellWrites: [],
|
|
53
59
|
shellResizes: [],
|
|
@@ -196,6 +202,7 @@ export class TestingRegistryServices {
|
|
|
196
202
|
storage;
|
|
197
203
|
config = new Map();
|
|
198
204
|
seededTasks;
|
|
205
|
+
externalTextFiles;
|
|
199
206
|
claims = new TestingContributionClaims();
|
|
200
207
|
constructor(options = {}) {
|
|
201
208
|
this.pluginId = options.pluginId ?? 'test-plugin';
|
|
@@ -211,6 +218,7 @@ export class TestingRegistryServices {
|
|
|
211
218
|
this.calls = createTestingCalls();
|
|
212
219
|
this.storage = options.storage ?? createMemoryPluginStorage(this.calls);
|
|
213
220
|
this.seededTasks = options.tasks ?? [];
|
|
221
|
+
this.externalTextFiles = options.externalTextFiles ?? [];
|
|
214
222
|
}
|
|
215
223
|
localQualifiedId(kind, id) {
|
|
216
224
|
assertLocalId(kind, id);
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, TestingOpenForgeRegistryFake, } from './testing/registryFake';
|
|
2
2
|
export { createMemoryPluginStorage, createTestingCalls, TestingSubscriptionSink, } from './testing/support';
|
|
3
|
-
export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingEventListenerContribution, TestingInjectionPointContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingTaskUISectionContribution, TestingViewContribution, } from './testing/contracts';
|
|
3
|
+
export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingExternalTextFile, TestingExternalTextFileChunksCall, TestingEventListenerContribution, TestingInjectionPointContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingTaskUISectionContribution, TestingViewContribution, } from './testing/contracts';
|
package/dist/types.d.ts
CHANGED
|
@@ -17,13 +17,16 @@ export interface ValidationError {
|
|
|
17
17
|
path: string;
|
|
18
18
|
message: string;
|
|
19
19
|
}
|
|
20
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -54,6 +57,7 @@ export interface OpenForgeContextSnapshot {
|
|
|
54
57
|
projectId: string | null;
|
|
55
58
|
taskId?: string | null;
|
|
56
59
|
}
|
|
60
|
+
export type OpenForgeContextChangeHandler = (snapshot: OpenForgeContextSnapshot) => MaybePromise<void>;
|
|
57
61
|
export interface OpenForgeNavigationSnapshot {
|
|
58
62
|
activeProjectId: string | null;
|
|
59
63
|
currentView: string;
|
|
@@ -85,6 +89,7 @@ export interface OpenForgePluginContext {
|
|
|
85
89
|
apiVersion: SupportedOpenForgeApiVersion;
|
|
86
90
|
packageMetadata: OpenForgePackageMetadata;
|
|
87
91
|
subscriptions: SubscriptionSink;
|
|
92
|
+
onDidChange(handler: OpenForgeContextChangeHandler): Disposable;
|
|
88
93
|
}
|
|
89
94
|
export type FrontendPluginContext = OpenForgePluginContext;
|
|
90
95
|
export type BackendPluginContext = OpenForgePluginContext;
|
|
@@ -188,6 +193,21 @@ export interface PluginViewProps extends Record<string, unknown> {
|
|
|
188
193
|
api: FrontendOpenForgeAPI;
|
|
189
194
|
context: OpenForgeContextSnapshot;
|
|
190
195
|
}
|
|
196
|
+
export interface PluginSidebarViewIdentity {
|
|
197
|
+
pluginId: string;
|
|
198
|
+
id: string;
|
|
199
|
+
qualifiedId: string;
|
|
200
|
+
title: string;
|
|
201
|
+
icon: PluginIcon;
|
|
202
|
+
}
|
|
203
|
+
export interface PluginSidebarNavigationProps extends Record<string, unknown> {
|
|
204
|
+
api: FrontendOpenForgeAPI;
|
|
205
|
+
context: OpenForgeContextSnapshot;
|
|
206
|
+
active: boolean;
|
|
207
|
+
collapsed: boolean;
|
|
208
|
+
view: PluginSidebarViewIdentity;
|
|
209
|
+
onActivate: () => void;
|
|
210
|
+
}
|
|
191
211
|
export interface PluginTaskPaneProps extends Record<string, unknown> {
|
|
192
212
|
api: FrontendOpenForgeAPI;
|
|
193
213
|
context: OpenForgeContextSnapshot;
|
|
@@ -230,6 +250,8 @@ export interface PluginViewRegistration {
|
|
|
230
250
|
order?: number;
|
|
231
251
|
shortcut?: string;
|
|
232
252
|
component: PluginComponentLoader<PluginViewProps> | PluginComponent<PluginViewProps>;
|
|
253
|
+
/** Custom content for a sidebar-placed View's host-owned navigation slot. */
|
|
254
|
+
navigationComponent?: PluginComponentLoader<PluginSidebarNavigationProps> | PluginComponent<PluginSidebarNavigationProps>;
|
|
233
255
|
}
|
|
234
256
|
export interface PluginTaskPaneTabRegistration {
|
|
235
257
|
id: string;
|
|
@@ -353,6 +375,49 @@ export interface FileSystemAPI {
|
|
|
353
375
|
limit?: number;
|
|
354
376
|
}): Promise<string[]>;
|
|
355
377
|
}
|
|
378
|
+
export interface UserDataDirectoryRequest {
|
|
379
|
+
path?: string | null;
|
|
380
|
+
}
|
|
381
|
+
export interface UserDataFileRequest {
|
|
382
|
+
path: string;
|
|
383
|
+
}
|
|
384
|
+
export interface UserDataFileWriteRequest extends UserDataFileRequest {
|
|
385
|
+
content: string;
|
|
386
|
+
}
|
|
387
|
+
export interface ExternalReadDirectoryRequest {
|
|
388
|
+
root: string;
|
|
389
|
+
path?: string | null;
|
|
390
|
+
}
|
|
391
|
+
export interface ExternalReadFileRequest {
|
|
392
|
+
root: string;
|
|
393
|
+
path: string;
|
|
394
|
+
}
|
|
395
|
+
export declare const DEFAULT_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES: number;
|
|
396
|
+
export declare const MIN_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES = 4;
|
|
397
|
+
export declare const MAX_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES: number;
|
|
398
|
+
/** Applies the default external text chunk size and rejects values outside host limits. */
|
|
399
|
+
export declare function resolveExternalTextFileChunkSize(chunkSizeBytes?: number): number;
|
|
400
|
+
export interface ExternalReadTextFileChunksRequest extends ExternalReadFileRequest {
|
|
401
|
+
/** UTF-8 chunks contain at most this many bytes. Defaults to 64 KiB. */
|
|
402
|
+
chunkSizeBytes?: number;
|
|
403
|
+
/** Stops future reads. An in-flight host read may finish, but its result is discarded. */
|
|
404
|
+
signal?: AbortSignal;
|
|
405
|
+
}
|
|
406
|
+
export interface UserDataFileSystemAPI {
|
|
407
|
+
readDir(request?: UserDataDirectoryRequest): Promise<FileEntry[]>;
|
|
408
|
+
readTextFile(request: UserDataFileRequest): Promise<string>;
|
|
409
|
+
writeTextFile(request: UserDataFileWriteRequest): Promise<void>;
|
|
410
|
+
}
|
|
411
|
+
export interface ExternalReadFileSystemAPI {
|
|
412
|
+
readDir(request: ExternalReadDirectoryRequest): Promise<FileEntry[]>;
|
|
413
|
+
readTextFile(request: ExternalReadFileRequest): Promise<string>;
|
|
414
|
+
/** Lazily reads a UTF-8 file without retaining a host file handle between chunks. */
|
|
415
|
+
readTextFileChunks(request: ExternalReadTextFileChunksRequest): AsyncIterable<string>;
|
|
416
|
+
}
|
|
417
|
+
export interface BackendFileSystemAPI extends FileSystemAPI {
|
|
418
|
+
userData: UserDataFileSystemAPI;
|
|
419
|
+
external: ExternalReadFileSystemAPI;
|
|
420
|
+
}
|
|
356
421
|
export type TerminalImageProtocol = 'iterm2';
|
|
357
422
|
export interface ShellSessionRequest {
|
|
358
423
|
taskId: string;
|
|
@@ -371,12 +436,16 @@ export interface ShellResizeRequest extends ShellSessionRequest {
|
|
|
371
436
|
cols: number;
|
|
372
437
|
rows: number;
|
|
373
438
|
}
|
|
439
|
+
export interface PtyBufferState {
|
|
440
|
+
buffer: string | null;
|
|
441
|
+
isLive: boolean;
|
|
442
|
+
}
|
|
374
443
|
export interface ShellAPI {
|
|
375
444
|
spawn(request: ShellSpawnRequest): Promise<number>;
|
|
376
445
|
write(request: ShellWriteRequest): Promise<void>;
|
|
377
446
|
resize(request: ShellResizeRequest): Promise<void>;
|
|
378
447
|
kill(request: ShellSessionRequest): Promise<void>;
|
|
379
|
-
getBuffer(request: ShellSessionRequest): Promise<
|
|
448
|
+
getBuffer(request: ShellSessionRequest): Promise<PtyBufferState>;
|
|
380
449
|
}
|
|
381
450
|
export interface CreateTaskRequest {
|
|
382
451
|
initialPrompt: string;
|
|
@@ -518,6 +587,7 @@ export interface FrontendOpenForgeAPI extends OpenForgeCommonAPI {
|
|
|
518
587
|
taskStart: FrontendTaskStartRegistry;
|
|
519
588
|
}
|
|
520
589
|
export interface BackendOpenForgeAPI extends OpenForgeCommonAPI {
|
|
590
|
+
fs: BackendFileSystemAPI;
|
|
521
591
|
backend: BackendMethodRegistry;
|
|
522
592
|
background: BackgroundServiceRegistry;
|
|
523
593
|
}
|
|
@@ -534,3 +604,4 @@ export declare function parsePluginViewKey(key: PluginViewKey): {
|
|
|
534
604
|
pluginId: string;
|
|
535
605
|
viewId: string;
|
|
536
606
|
};
|
|
607
|
+
export {};
|
package/dist/types.js
CHANGED
|
@@ -10,6 +10,58 @@ 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();
|
|
52
|
+
export const DEFAULT_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES = 64 * 1024;
|
|
53
|
+
export const MIN_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES = 4;
|
|
54
|
+
export const MAX_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
55
|
+
/** Applies the default external text chunk size and rejects values outside host limits. */
|
|
56
|
+
export function resolveExternalTextFileChunkSize(chunkSizeBytes) {
|
|
57
|
+
const size = chunkSizeBytes ?? DEFAULT_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES;
|
|
58
|
+
if (!Number.isInteger(size)
|
|
59
|
+
|| size < MIN_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES
|
|
60
|
+
|| size > MAX_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES) {
|
|
61
|
+
throw new RangeError(`chunkSizeBytes must be an integer between ${MIN_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES} and ${MAX_EXTERNAL_TEXT_FILE_CHUNK_SIZE_BYTES}`);
|
|
62
|
+
}
|
|
63
|
+
return size;
|
|
64
|
+
}
|
|
13
65
|
export class TaskFollowUpError extends Error {
|
|
14
66
|
code;
|
|
15
67
|
constructor(code, message) {
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from 'svelte'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getMarkdownRepositoryLinkSuffix,
|
|
5
|
+
MARKDOWN_REMOTE_MEDIA_ATTRIBUTE,
|
|
6
|
+
renderMarkdownHtml,
|
|
7
|
+
resolveMarkdownRepositoryPath,
|
|
8
|
+
type ResolvedMarkdownMedia,
|
|
9
|
+
} from '../markdown'
|
|
4
10
|
|
|
5
11
|
interface Props {
|
|
6
12
|
content: string
|
|
7
13
|
imageBaseUrl?: string | null
|
|
8
14
|
markdownFilePath?: string | null
|
|
9
15
|
resolveRepositoryImage?: (repositoryPath: string) => Promise<string | null>
|
|
16
|
+
resolveRemoteMedia?: (url: string) => Promise<ResolvedMarkdownMedia | null>
|
|
10
17
|
onOpenRepositoryPath?: (repositoryPath: string, suffix: string) => void | Promise<void>
|
|
11
18
|
onOpenUrl?: (url: string) => void | Promise<void>
|
|
12
19
|
}
|
|
@@ -16,6 +23,7 @@
|
|
|
16
23
|
imageBaseUrl = null,
|
|
17
24
|
markdownFilePath = null,
|
|
18
25
|
resolveRepositoryImage,
|
|
26
|
+
resolveRemoteMedia,
|
|
19
27
|
onOpenRepositoryPath,
|
|
20
28
|
onOpenUrl,
|
|
21
29
|
}: Props = $props()
|
|
@@ -26,6 +34,7 @@
|
|
|
26
34
|
imageBaseUrl: resolveRepositoryImage ? null : imageBaseUrl,
|
|
27
35
|
markdownFilePath,
|
|
28
36
|
deferRepositoryImages: Boolean(resolveRepositoryImage),
|
|
37
|
+
deferRemoteMedia: Boolean(resolveRemoteMedia),
|
|
29
38
|
}))
|
|
30
39
|
|
|
31
40
|
async function resolveRepositoryImages(runId: number) {
|
|
@@ -47,12 +56,55 @@
|
|
|
47
56
|
}))
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
function applyResolvedMedia(element: Element, url: string, resolved: ResolvedMarkdownMedia | null) {
|
|
60
|
+
element.removeAttribute(MARKDOWN_REMOTE_MEDIA_ATTRIBUTE)
|
|
61
|
+
|
|
62
|
+
if (element instanceof HTMLImageElement) {
|
|
63
|
+
element.setAttribute('src', resolved?.url ?? url)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A bare link only becomes a player when the caller says the upload is one;
|
|
68
|
+
// everything else stays the link the Markdown asked for.
|
|
69
|
+
if (resolved?.kind !== 'video') return
|
|
70
|
+
|
|
71
|
+
const video = document.createElement('video')
|
|
72
|
+
video.src = resolved.url
|
|
73
|
+
video.controls = true
|
|
74
|
+
video.preload = 'metadata'
|
|
75
|
+
element.replaceWith(video)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Remote media is held back by the renderer so the caller can swap in a URL
|
|
79
|
+
// this app can load (GitHub only serves upload URLs to a browser session).
|
|
80
|
+
// Anything the caller does not handle falls back to what the Markdown said.
|
|
81
|
+
async function resolveRemoteMediaElements(runId: number) {
|
|
82
|
+
if (!root || !resolveRemoteMedia) return
|
|
83
|
+
|
|
84
|
+
const elements = Array.from(root.querySelectorAll(`[${MARKDOWN_REMOTE_MEDIA_ATTRIBUTE}]`))
|
|
85
|
+
await Promise.all(elements.map(async (element) => {
|
|
86
|
+
const url = element.getAttribute(MARKDOWN_REMOTE_MEDIA_ATTRIBUTE)
|
|
87
|
+
if (!url) return
|
|
88
|
+
|
|
89
|
+
let resolved: ResolvedMarkdownMedia | null = null
|
|
90
|
+
try {
|
|
91
|
+
resolved = await resolveRemoteMedia(url)
|
|
92
|
+
} catch {
|
|
93
|
+
// Fall back to the original URL when resolution is unavailable.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (runId !== imageResolutionId) return
|
|
97
|
+
applyResolvedMedia(element, url, resolved)
|
|
98
|
+
}))
|
|
99
|
+
}
|
|
100
|
+
|
|
50
101
|
$effect(() => {
|
|
51
102
|
const runId = ++imageResolutionId
|
|
52
103
|
void html
|
|
53
|
-
if (!root
|
|
104
|
+
if (!root) return
|
|
54
105
|
|
|
55
106
|
void resolveRepositoryImages(runId)
|
|
107
|
+
void resolveRemoteMediaElements(runId)
|
|
56
108
|
})
|
|
57
109
|
|
|
58
110
|
onDestroy(() => {
|
|
@@ -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
|
+
"version": "0.2.5",
|
|
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": [
|
|
@@ -92,17 +93,17 @@
|
|
|
92
93
|
"test": "vitest run"
|
|
93
94
|
},
|
|
94
95
|
"dependencies": {
|
|
95
|
-
"dompurify": "
|
|
96
|
-
"marked": "
|
|
96
|
+
"dompurify": "catalog:",
|
|
97
|
+
"marked": "catalog:"
|
|
97
98
|
},
|
|
98
99
|
"peerDependencies": {
|
|
99
100
|
"svelte": "^5.0.0"
|
|
100
101
|
},
|
|
101
102
|
"devDependencies": {
|
|
102
|
-
"@types/node": "
|
|
103
|
-
"svelte": "
|
|
104
|
-
"typescript": "
|
|
105
|
-
"vitest": "
|
|
103
|
+
"@types/node": "catalog:pinned",
|
|
104
|
+
"svelte": "catalog:pinned",
|
|
105
|
+
"typescript": "catalog:",
|
|
106
|
+
"vitest": "catalog:",
|
|
106
107
|
"vscode-material-icons": "^0.1.1"
|
|
107
108
|
}
|
|
108
109
|
}
|