@openforge-app/plugin-sdk 0.2.6 → 0.2.8

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.
@@ -9,12 +9,14 @@ export interface BrowserSurfaceNavigationError {
9
9
  message: string;
10
10
  url: string;
11
11
  }
12
+ export type BrowserDevToolsPanel = 'elements' | 'console';
12
13
  export interface TaskBrowserSurfaceState {
13
14
  url: string;
14
15
  title: string;
15
16
  loading: boolean;
16
17
  canGoBack: boolean;
17
18
  canGoForward: boolean;
19
+ devToolsOpen: boolean;
18
20
  error: BrowserSurfaceNavigationError | null;
19
21
  }
20
22
  export interface BrowserSurfaceCapture {
@@ -63,6 +65,8 @@ export interface TaskBrowserSurfaceController {
63
65
  goForward(): Promise<TaskBrowserSurfaceState>;
64
66
  reload(): Promise<TaskBrowserSurfaceState>;
65
67
  stop(): Promise<TaskBrowserSurfaceState>;
68
+ openDevTools(panel?: BrowserDevToolsPanel): Promise<TaskBrowserSurfaceState>;
69
+ closeDevTools(): Promise<TaskBrowserSurfaceState>;
66
70
  selectVisibleRegion(): Promise<BrowserSurfaceFeedbackSelection | null>;
67
71
  cancelVisibleRegionSelection(): Promise<void>;
68
72
  clearVisualFeedback(): Promise<void>;
@@ -1,4 +1,4 @@
1
- import type { BrowserSurfacesAPI, BrowserSurfaceVisualFeedback, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceState } from './browserSurfaces';
1
+ import type { BrowserSurfacesAPI, BrowserDevToolsPanel, BrowserSurfaceVisualFeedback, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceState } from './browserSurfaces';
2
2
  export interface TestingBrowserSurfaceCalls {
3
3
  browserSurfaceGetOrCreate: GetOrCreateBrowserSurfaceRequest[];
4
4
  browserSurfaceAttachments: Array<{
@@ -22,7 +22,8 @@ export interface TestingBrowserSurfaceCalls {
22
22
  browserSurfaceControls: Array<{
23
23
  taskId: string;
24
24
  id: string;
25
- action: 'goBack' | 'goForward' | 'reload' | 'stop';
25
+ action: 'goBack' | 'goForward' | 'reload' | 'stop' | 'openDevTools' | 'closeDevTools';
26
+ panel?: BrowserDevToolsPanel;
26
27
  }>;
27
28
  browserSurfaceSelections: Array<{
28
29
  taskId: string;
@@ -1,4 +1,4 @@
1
- import { BrowserSurfaceError, isAllowedBrowserSurfaceUrl, } from './browserSurfaces';
1
+ import { BrowserSurfaceError, isAllowedBrowserSurfaceUrl, } from './browserSurfaces.js';
2
2
  function disposable(dispose) {
3
3
  let disposed = false;
4
4
  return {
@@ -49,6 +49,7 @@ class TestingTaskBrowserSurface {
49
49
  loading: false,
50
50
  canGoBack: false,
51
51
  canGoForward: false,
52
+ devToolsOpen: false,
52
53
  error: null,
53
54
  };
54
55
  }
@@ -124,6 +125,23 @@ class TestingTaskBrowserSurface {
124
125
  this.publish({ loading: false });
125
126
  return this.getState();
126
127
  }
128
+ async openDevTools(panel) {
129
+ this.assertLive();
130
+ this.calls.browserSurfaceControls.push({
131
+ taskId: this.taskId,
132
+ id: this.id,
133
+ action: 'openDevTools',
134
+ ...(panel ? { panel } : {}),
135
+ });
136
+ this.publish({ devToolsOpen: true });
137
+ return this.getState();
138
+ }
139
+ async closeDevTools() {
140
+ this.assertLive();
141
+ this.calls.browserSurfaceControls.push({ taskId: this.taskId, id: this.id, action: 'closeDevTools' });
142
+ this.publish({ devToolsOpen: false });
143
+ return this.getState();
144
+ }
127
145
  async selectVisibleRegion() {
128
146
  this.assertLive();
129
147
  this.calls.browserSurfaceSelections.push({ taskId: this.taskId, id: this.id });
@@ -0,0 +1,10 @@
1
+ export type CollapsedSectionsState = Record<string, boolean>;
2
+ export declare const COLLAPSED_SECTIONS_STORAGE_KEY = "openforge.infoPanelSectionCollapse.v1";
3
+ export declare function pluginSectionKey(pluginId: string, sectionKey: string): string;
4
+ export declare const collapsedSections: {
5
+ subscribe: (this: void, run: import("svelte/store").Subscriber<CollapsedSectionsState>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
6
+ };
7
+ export declare function isSectionCollapsed(state: CollapsedSectionsState, key: string): boolean;
8
+ export declare function setSectionCollapsed(key: string, collapsed: boolean): void;
9
+ export declare function toggleSection(key: string): void;
10
+ export declare function clearCollapsedSections(): void;
@@ -0,0 +1,81 @@
1
+ import { writable } from 'svelte/store';
2
+ // The store started life in the host app as src/lib/infoPanelSectionState.ts. The key is
3
+ // deliberately unchanged: renaming it would reset every user's collapsed sections.
4
+ export const COLLAPSED_SECTIONS_STORAGE_KEY = 'openforge.infoPanelSectionCollapse.v1';
5
+ export function pluginSectionKey(pluginId, sectionKey) {
6
+ return `plugin:${pluginId}:${sectionKey}`;
7
+ }
8
+ function getLocalStorage() {
9
+ try {
10
+ return globalThis.localStorage ?? null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function readPersisted() {
17
+ const storage = getLocalStorage();
18
+ if (storage === null)
19
+ return {};
20
+ try {
21
+ const rawValue = storage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY);
22
+ if (rawValue === null)
23
+ return {};
24
+ const parsed = JSON.parse(rawValue);
25
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
26
+ return {};
27
+ const state = {};
28
+ for (const [key, value] of Object.entries(parsed)) {
29
+ if (value === true)
30
+ state[key] = true;
31
+ }
32
+ return state;
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
38
+ function writePersisted(state) {
39
+ const storage = getLocalStorage();
40
+ if (storage === null)
41
+ return;
42
+ try {
43
+ // Only persist collapsed sections; expanded is the default and needs no entry.
44
+ const collapsedOnly = {};
45
+ for (const [key, value] of Object.entries(state)) {
46
+ if (value === true)
47
+ collapsedOnly[key] = true;
48
+ }
49
+ if (Object.keys(collapsedOnly).length === 0) {
50
+ storage.removeItem(COLLAPSED_SECTIONS_STORAGE_KEY);
51
+ return;
52
+ }
53
+ storage.setItem(COLLAPSED_SECTIONS_STORAGE_KEY, JSON.stringify(collapsedOnly));
54
+ }
55
+ catch {
56
+ // Persistence is best effort and must not block the section from rendering.
57
+ }
58
+ }
59
+ const { subscribe, set, update } = writable(readPersisted());
60
+ export const collapsedSections = { subscribe };
61
+ export function isSectionCollapsed(state, key) {
62
+ return state[key] === true;
63
+ }
64
+ export function setSectionCollapsed(key, collapsed) {
65
+ update((current) => {
66
+ const next = { ...current, [key]: collapsed };
67
+ writePersisted(next);
68
+ return next;
69
+ });
70
+ }
71
+ export function toggleSection(key) {
72
+ update((current) => {
73
+ const next = { ...current, [key]: !isSectionCollapsed(current, key) };
74
+ writePersisted(next);
75
+ return next;
76
+ });
77
+ }
78
+ export function clearCollapsedSections() {
79
+ writePersisted({});
80
+ set({});
81
+ }
package/dist/domain.d.ts CHANGED
@@ -314,7 +314,7 @@ export interface ProjectAttention {
314
314
  unaddressed_comments: number;
315
315
  completed_agents: number;
316
316
  }
317
- export type TaskAttentionState = 'idle' | 'needs-input' | 'paused' | 'agent-done' | 'failed' | 'interrupted' | 'pr-draft' | 'pr-open' | 'ci-failed' | 'changes-requested' | 'unaddressed-comments' | 'ready-to-merge' | 'ready-to-enqueue' | 'pr-queued' | 'pr-merged' | 'pr-closed' | 'ci-running' | 'review-pending' | 'merge-conflict';
317
+ export type TaskAttentionState = 'active' | 'backlog' | 'idle' | 'needs-input' | 'paused' | 'agent-done' | 'failed' | 'interrupted' | 'pr-draft' | 'pr-open' | 'ci-failed' | 'changes-requested' | 'unaddressed-comments' | 'ready-to-merge' | 'ready-to-enqueue' | 'pr-queued' | 'pr-merged' | 'pr-closed' | 'ci-running' | 'review-pending' | 'merge-conflict';
318
318
  /** Backend-owned, Task-only Needs Attention read model. */
319
319
  export interface TaskAttentionRow {
320
320
  task_id: string;
@@ -499,18 +499,6 @@ export interface ReviewSubmission {
499
499
  body: string;
500
500
  comments: ReviewSubmissionComment[];
501
501
  }
502
- /** Self-review comment for task implementation review */
503
- export interface SelfReviewComment {
504
- id: number;
505
- task_id: string;
506
- round: number;
507
- comment_type: string;
508
- file_path: string | null;
509
- line_number: number | null;
510
- body: string;
511
- created_at: number;
512
- archived_at: number | null;
513
- }
514
502
  /** One file referenced by a walkthrough step. `hunk_indexes === null` means the entire file's diff belongs to the step. */
515
503
  export interface PrWalkthroughStepFile {
516
504
  filename: string;
@@ -1,10 +1,10 @@
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, 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';
1
+ import type { BrowserSurfaceCapture, BrowserDevToolsPanel, 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, 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, TerminalViewSnapshot } 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;
6
6
  };
7
7
  export declare function defineFrontendPlugin<const TPlugin extends FrontendPlugin>(plugin: TPlugin): MarkedFrontendPlugin<TPlugin>;
8
8
  export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
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, 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, };
9
+ export type { BrowserSurfaceCapture, BrowserDevToolsPanel, 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, 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, TerminalViewSnapshot, };
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 { 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, ShellTerminalQueryResponseRequest, 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, 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, TerminalViewSnapshot, ShellSessionRequest, ShellSpawnRequest, ShellTerminalQueryResponseRequest, 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, 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,6 +10,7 @@ const PUBLIC_UI_COMPONENT_NAMES = Object.freeze([
10
10
  'PluginViewState',
11
11
  'PluginSidebarLink',
12
12
  'FileTypeIcon',
13
+ 'CollapsibleSection',
13
14
  ])
14
15
 
15
16
  /**
@@ -1,5 +1,5 @@
1
1
  import type { BackendOpenForgeAPI } from '../types';
2
- import { type TestingRegistryServices } from './support';
2
+ import { type TestingRegistryServices } from './support.js';
3
3
  import type { TestingBackendMethodContribution, TestingBackgroundServiceContribution } from './contracts';
4
4
  export declare class TestingBackendServicesFake {
5
5
  private readonly services;
@@ -1,4 +1,4 @@
1
- import { assertFunction, createDisposable } from './support';
1
+ import { assertFunction, createDisposable } from './support.js';
2
2
  export class TestingBackendServicesFake {
3
3
  services;
4
4
  backendMethods = new Map();
@@ -1,5 +1,5 @@
1
1
  import type { BackendOpenForgeAPI, FrontendOpenForgeAPI, OpenForgeCommonAPI } from '../types';
2
- import { type TestingRegistryServices } from './support';
2
+ import { type TestingRegistryServices } from './support.js';
3
3
  import type { TestingCommandContribution, TestingEventListenerContribution } from './contracts';
4
4
  export type TestingCommonApi = OpenForgeCommonAPI & Pick<FrontendOpenForgeAPI, 'navigation'>;
5
5
  export declare class TestingCommonApiFake {
@@ -1,5 +1,5 @@
1
- import { resolveExternalTextFileChunkSize } from '../types';
2
- import { assertFunction, assertTitle, commandDescriptor, createDisposable, isJsonValue, normalizeAgentCommandMetadata, } from './support';
1
+ import { resolveExternalTextFileChunkSize } from '../types.js';
2
+ import { assertFunction, assertTitle, commandDescriptor, createDisposable, isJsonValue, normalizeAgentCommandMetadata, } from './support.js';
3
3
  const UTF8_ENCODER = new TextEncoder();
4
4
  function* splitExternalTextFile(content, maxBytes) {
5
5
  let chunk = '';
@@ -1,6 +1,6 @@
1
- import { type TaskBrowserSurfaceState } from '../browserSurfaces';
1
+ import { type TaskBrowserSurfaceState } from '../browserSurfaces.js';
2
2
  import type { FrontendOpenForgeAPI } from '../types';
3
- import { type TestingRegistryServices } from './support';
3
+ import { type TestingRegistryServices } from './support.js';
4
4
  import type { TestingInjectionPointContribution, TestingTaskStartPrefixProviderContribution, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingTaskUISectionContribution, TestingViewContribution } from './contracts';
5
5
  type TestingFrontendContributionApi = Pick<FrontendOpenForgeAPI, 'browserSurfaces' | 'taskLinks' | 'views' | 'taskUI' | 'taskPane' | 'settings' | 'backend' | 'injectionPoints' | 'taskStart'>;
6
6
  export declare class TestingFrontendContributionFake {
@@ -1,6 +1,6 @@
1
- import { createTestingBrowserSurfaces } from '../browserSurfacesTesting';
2
- import { isAllowedBrowserSurfaceUrl } from '../browserSurfaces';
3
- import { assertFunction, assertTitle, createDisposable, } from './support';
1
+ import { createTestingBrowserSurfaces } from '../browserSurfacesTesting.js';
2
+ import { isAllowedBrowserSurfaceUrl } from '../browserSurfaces.js';
3
+ import { assertFunction, assertTitle, createDisposable, } from './support.js';
4
4
  export class TestingFrontendContributionFake {
5
5
  services;
6
6
  invokeBackendMethod;
@@ -1,7 +1,7 @@
1
1
  import type { TaskBrowserSurfaceState } from '../browserSurfaces';
2
2
  import type { BackendPlugin, BackendPluginContext, FrontendPlugin, FrontendPluginContext, OpenForgePackageMetadata, PluginStorage } from '../types';
3
3
  import type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot } from './contracts';
4
- import { TestingSubscriptionSink } from './support';
4
+ import { TestingSubscriptionSink } from './support.js';
5
5
  export declare class TestingOpenForgeRegistryFake {
6
6
  readonly pluginId: string;
7
7
  readonly projectId: string | null;
@@ -1,7 +1,7 @@
1
- import { TestingBackendServicesFake } from './backendServicesFake';
2
- import { TestingCommonApiFake } from './commonApiFake';
3
- import { TestingFrontendContributionFake } from './frontendContributionFake';
4
- import { TestingRegistryServices, TestingSubscriptionSink, } from './support';
1
+ import { TestingBackendServicesFake } from './backendServicesFake.js';
2
+ import { TestingCommonApiFake } from './commonApiFake.js';
3
+ import { TestingFrontendContributionFake } from './frontendContributionFake.js';
4
+ import { TestingRegistryServices, TestingSubscriptionSink, } from './support.js';
5
5
  export class TestingOpenForgeRegistryFake {
6
6
  pluginId;
7
7
  projectId;
package/dist/testing.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, TestingOpenForgeRegistryFake, } from './testing/registryFake';
2
- export { createMemoryPluginStorage, createTestingCalls, TestingSubscriptionSink, } from './testing/support';
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';
1
+ export { createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, TestingOpenForgeRegistryFake, } from './testing/registryFake.js';
2
+ export { createMemoryPluginStorage, createTestingCalls, TestingSubscriptionSink, } from './testing/support.js';
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.js';
package/dist/testing.js CHANGED
@@ -1,2 +1,2 @@
1
- export { createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, TestingOpenForgeRegistryFake, } from './testing/registryFake';
2
- export { createMemoryPluginStorage, createTestingCalls, TestingSubscriptionSink, } from './testing/support';
1
+ export { createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, TestingOpenForgeRegistryFake, } from './testing/registryFake.js';
2
+ export { createMemoryPluginStorage, createTestingCalls, TestingSubscriptionSink, } from './testing/support.js';
package/dist/types.d.ts CHANGED
@@ -440,8 +440,15 @@ export interface ShellResizeRequest extends ShellSessionRequest {
440
440
  cols: number;
441
441
  rows: number;
442
442
  }
443
+ export interface TerminalViewSnapshot {
444
+ instanceId: number;
445
+ watermark: number;
446
+ data: string;
447
+ }
443
448
  export interface PtyBufferState {
449
+ authority?: 'xterm-authoritative' | 'ghostty-authoritative';
444
450
  buffer: string | null;
451
+ snapshot?: TerminalViewSnapshot | null;
445
452
  isLive: boolean;
446
453
  instanceId: number | null;
447
454
  }
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- import packageMetadataSchemaData from './openforgePackageMetadataSchema.json';
1
+ import packageMetadataSchemaData from './openforgePackageMetadataSchema.json' with { type: 'json' };
2
2
  function readSupportedOpenForgeApiVersions() {
3
3
  const versions = packageMetadataSchemaData.properties.apiVersion.enum;
4
4
  if (!Array.isArray(versions) || versions.length === 0 || !versions.every((version) => typeof version === 'number' && Number.isInteger(version))) {
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { collapsedSections, isSectionCollapsed, toggleSection } from '../collapsibleSectionState'
4
+
5
+ interface Props {
6
+ // Stable, global key used to persist the collapsed/expanded state. Plugins must
7
+ // build this with `pluginSectionKey(pluginId, key)` so two plugins cannot collide.
8
+ sectionKey: string
9
+ title: string
10
+ // aria-label for the section landmark (defaults to the title).
11
+ label?: string
12
+ // data-task-info-card value (defaults to the section key).
13
+ cardId?: string
14
+ ariaLive?: 'polite' | 'off'
15
+ // A 14px glyph identifying the section, drawn between the disclosure caret and the
16
+ // title. Keep it decorative: the host hides it from assistive tech so it cannot
17
+ // pollute the toggle's accessible name.
18
+ icon?: Snippet
19
+ // Optional per-section controls (refresh, edit, counts). Rendered as a sibling of
20
+ // the toggle button — never nested inside it — so nested <button>s stay valid.
21
+ actions?: Snippet
22
+ children: Snippet
23
+ }
24
+
25
+ let { sectionKey, title, label, cardId, ariaLive, icon, actions, children }: Props = $props()
26
+
27
+ let collapsed = $derived(isSectionCollapsed($collapsedSections, sectionKey))
28
+ let contentId = $derived(`info-section-${sectionKey}`)
29
+ </script>
30
+
31
+ <!-- `--section-inset` is the distance from the card edge to the caret. Hosts override it
32
+ per surface (the task inspector pushes it out to 1.5rem); the header and the body
33
+ both read it so they can never drift apart. `--section-caret-column` is the caret
34
+ plus the gap after it, so body content lines up with the icon and title rather than
35
+ starting under the caret. Keep it equal to the caret's `w-3` plus the header `gap-2`. -->
36
+ <section
37
+ data-task-info-card={cardId ?? sectionKey}
38
+ data-card-sizing="natural"
39
+ class="rounded-lg border border-base-300/70 bg-base-100 overflow-hidden shrink-0 [--section-inset:0.75rem] [--section-caret-column:1.25rem]"
40
+ aria-label={label ?? title}
41
+ aria-live={ariaLive}
42
+ >
43
+ <div class="flex items-stretch {collapsed ? '' : 'border-b border-base-300/70'}">
44
+ <h3 class="m-0 min-w-0 flex-1">
45
+ <button
46
+ type="button"
47
+ class="flex w-full items-center gap-2 px-[var(--section-inset)] py-2 text-left text-sm font-semibold text-base-content hover:bg-base-200/40 focus-visible:ring-2 focus-visible:ring-primary rounded"
48
+ aria-expanded={!collapsed}
49
+ aria-controls={contentId}
50
+ onclick={() => toggleSection(sectionKey)}
51
+ >
52
+ <!-- Fixed-width caret column so every section title starts at the same x,
53
+ including the single-row cards that render a blank column instead. -->
54
+ <span
55
+ class="w-3 shrink-0 text-center text-[0.7rem] leading-none text-base-content/40 transition-transform duration-150 {collapsed ? '-rotate-90' : ''}"
56
+ aria-hidden="true"
57
+ >▾</span>
58
+ {#if icon}
59
+ <span class="flex shrink-0 items-center text-base-content/50" aria-hidden="true">{@render icon()}</span>
60
+ {/if}
61
+ <span class="truncate">{title}</span>
62
+ </button>
63
+ </h3>
64
+ {#if actions}
65
+ <div class="flex shrink-0 items-center gap-2 pr-2">
66
+ {@render actions()}
67
+ </div>
68
+ {/if}
69
+ </div>
70
+
71
+ {#if !collapsed}
72
+ <div
73
+ id={contentId}
74
+ class="pl-[calc(var(--section-inset)_+_var(--section-caret-column))] pr-[var(--section-inset)]"
75
+ >
76
+ {@render children()}
77
+ </div>
78
+ {/if}
79
+ </section>
@@ -8,6 +8,12 @@
8
8
  type ResolvedMarkdownMedia,
9
9
  } from '../markdown'
10
10
 
11
+ interface MarkdownImageOpenRequest {
12
+ src: string
13
+ alt: string
14
+ openLink?: () => void
15
+ }
16
+
11
17
  interface Props {
12
18
  content: string
13
19
  imageBaseUrl?: string | null
@@ -16,6 +22,7 @@
16
22
  resolveRemoteMedia?: (url: string) => Promise<ResolvedMarkdownMedia | null>
17
23
  onOpenRepositoryPath?: (repositoryPath: string, suffix: string) => void | Promise<void>
18
24
  onOpenUrl?: (url: string) => void | Promise<void>
25
+ onOpenImage?: (request: MarkdownImageOpenRequest) => void
19
26
  }
20
27
 
21
28
  let {
@@ -26,6 +33,7 @@
26
33
  resolveRemoteMedia,
27
34
  onOpenRepositoryPath,
28
35
  onOpenUrl,
36
+ onOpenImage,
29
37
  }: Props = $props()
30
38
 
31
39
  let root = $state<HTMLDivElement | null>(null)
@@ -37,6 +45,35 @@
37
45
  deferRemoteMedia: Boolean(resolveRemoteMedia),
38
46
  }))
39
47
 
48
+ function imageTrigger(image: HTMLImageElement): HTMLElement {
49
+ return image.closest('a') ?? image
50
+ }
51
+
52
+ function updateInteractiveImage(image: HTMLImageElement) {
53
+ const trigger = imageTrigger(image)
54
+ if (!onOpenImage || !image.getAttribute('src')) {
55
+ if (trigger.dataset.markdownImageTrigger === 'true') {
56
+ trigger.removeAttribute('role')
57
+ trigger.removeAttribute('tabindex')
58
+ trigger.removeAttribute('aria-label')
59
+ delete trigger.dataset.markdownImageTrigger
60
+ }
61
+ image.classList.remove('cursor-zoom-in')
62
+ return
63
+ }
64
+
65
+ const imageName = image.alt.trim()
66
+ trigger.setAttribute('role', 'button')
67
+ if (trigger === image) trigger.setAttribute('tabindex', '0')
68
+ trigger.setAttribute('aria-label', imageName ? `Open ${imageName} image` : 'Open image preview')
69
+ trigger.dataset.markdownImageTrigger = 'true'
70
+ image.classList.add('cursor-zoom-in')
71
+ }
72
+
73
+ function updateInteractiveImages() {
74
+ if (!root) return
75
+ root.querySelectorAll('img').forEach(updateInteractiveImage)
76
+ }
40
77
  async function resolveRepositoryImages(runId: number) {
41
78
  if (!root || !markdownFilePath || !resolveRepositoryImage) return
42
79
 
@@ -50,6 +87,7 @@
50
87
  if (runId !== imageResolutionId || !resolvedSrc) return
51
88
  image.setAttribute('src', resolvedSrc)
52
89
  image.removeAttribute('data-markdown-repository-path')
90
+ updateInteractiveImage(image)
53
91
  } catch {
54
92
  // Leave the image inert when an asset is missing or cannot be previewed.
55
93
  }
@@ -61,6 +99,7 @@
61
99
 
62
100
  if (element instanceof HTMLImageElement) {
63
101
  element.setAttribute('src', resolved?.url ?? url)
102
+ updateInteractiveImage(element)
64
103
  return
65
104
  }
66
105
 
@@ -102,6 +141,7 @@
102
141
  const runId = ++imageResolutionId
103
142
  void html
104
143
  if (!root) return
144
+ updateInteractiveImages()
105
145
 
106
146
  void resolveRepositoryImages(runId)
107
147
  void resolveRemoteMediaElements(runId)
@@ -111,17 +151,10 @@
111
151
  imageResolutionId++
112
152
  })
113
153
 
114
- function handleClick(event: MouseEvent) {
115
- if (!(event.target instanceof Element)) return
116
-
117
- const anchor = event.target.closest('a')
118
- const href = anchor?.getAttribute('href')
119
- if (!anchor || !href) return
120
-
154
+ function openMarkdownLink(href: string, absoluteHref: string) {
121
155
  if (markdownFilePath) {
122
156
  const repositoryPath = resolveMarkdownRepositoryPath(href, markdownFilePath)
123
157
  if (repositoryPath) {
124
- event.preventDefault()
125
158
  void onOpenRepositoryPath?.(repositoryPath, getMarkdownRepositoryLinkSuffix(href))
126
159
  return
127
160
  }
@@ -129,14 +162,62 @@
129
162
 
130
163
  if (href.startsWith('#')) return
131
164
 
132
- event.preventDefault()
133
- if (onOpenUrl && anchor.href) {
134
- void onOpenUrl(href.startsWith('//') ? `https:${href}` : anchor.href)
165
+ if (onOpenUrl && absoluteHref) {
166
+ void onOpenUrl(href.startsWith('//') ? `https:${href}` : absoluteHref)
135
167
  }
136
168
  }
169
+
170
+ function openImage(image: HTMLImageElement) {
171
+ const src = image.getAttribute('src')
172
+ if (!onOpenImage || !src) return
173
+
174
+ const anchor = image.closest('a')
175
+ const href = anchor?.getAttribute('href')
176
+ onOpenImage({
177
+ src,
178
+ alt: image.alt,
179
+ openLink: href ? () => openMarkdownLink(href, anchor?.href ?? '') : undefined,
180
+ })
181
+ }
182
+
183
+ function findEventImage(target: Element): HTMLImageElement | null {
184
+ const image = target.closest('img')
185
+ if (image instanceof HTMLImageElement) return image
186
+
187
+ return target.closest('a')?.querySelector('img') ?? null
188
+ }
189
+
190
+ function handleClick(event: MouseEvent) {
191
+ if (!(event.target instanceof Element)) return
192
+
193
+ const image = findEventImage(event.target)
194
+ if (image && onOpenImage && image.getAttribute('src')) {
195
+ event.preventDefault()
196
+ imageTrigger(image).focus()
197
+ openImage(image)
198
+ return
199
+ }
200
+
201
+ const anchor = event.target.closest('a')
202
+ const href = anchor?.getAttribute('href')
203
+ if (!anchor || !href) return
204
+
205
+ if (!href.startsWith('#')) event.preventDefault()
206
+ openMarkdownLink(href, anchor.href)
207
+ }
208
+
209
+ function handleKeydown(event: KeyboardEvent) {
210
+ if (event.key !== 'Enter' && event.key !== ' ') return
211
+ if (!(event.target instanceof Element)) return
212
+
213
+ const image = findEventImage(event.target)
214
+ if (!image) return
215
+
216
+ event.preventDefault()
217
+ openImage(image)
218
+ }
137
219
  </script>
138
220
 
139
- <!-- svelte-ignore a11y_click_events_have_key_events -->
140
- <div bind:this={root} role="presentation" class="markdown-body" onclick={handleClick}>
221
+ <div bind:this={root} role="presentation" class="markdown-body" onclick={handleClick} onkeydown={handleKeydown}>
141
222
  {@html html}
142
223
  </div>
package/dist/vite.js CHANGED
@@ -39,6 +39,7 @@ const OPENFORGE_PLUGIN_SDK_SOURCE_ENTRYPOINTS = Object.freeze([
39
39
  ['@openforge-app/plugin-sdk/sanitize', 'packages/plugin-sdk/src/sanitize.ts'],
40
40
  ['@openforge-app/plugin-sdk/pluginIcons', 'packages/plugin-sdk/src/pluginIcons.ts'],
41
41
  ['@openforge-app/plugin-sdk/fileIcons', 'packages/plugin-sdk/src/fileIcons.ts'],
42
+ ['@openforge-app/plugin-sdk/collapsibleSectionState', 'packages/plugin-sdk/src/collapsibleSectionState.ts'],
42
43
  ...OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS.map(({ importSpecifier, workspaceSourcePath }) => [importSpecifier, workspaceSourcePath]),
43
44
  ['@openforge-app/plugin-sdk', 'packages/plugin-sdk/src/index.ts'],
44
45
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openforge-app/plugin-sdk",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,6 +64,10 @@
64
64
  "types": "./dist/fileIcons.d.ts",
65
65
  "default": "./dist/fileIcons.js"
66
66
  },
67
+ "./collapsibleSectionState": {
68
+ "types": "./dist/collapsibleSectionState.d.ts",
69
+ "default": "./dist/collapsibleSectionState.js"
70
+ },
67
71
  "./ui/Button.svelte": "./dist/ui/Button.svelte",
68
72
  "./ui/Checkbox.svelte": "./dist/ui/Checkbox.svelte",
69
73
  "./ui/MarkdownContent.svelte": "./dist/ui/MarkdownContent.svelte",
@@ -72,7 +76,8 @@
72
76
  "./ui/PluginPageHeader.svelte": "./dist/ui/PluginPageHeader.svelte",
73
77
  "./ui/PluginViewState.svelte": "./dist/ui/PluginViewState.svelte",
74
78
  "./ui/PluginSidebarLink.svelte": "./dist/ui/PluginSidebarLink.svelte",
75
- "./ui/FileTypeIcon.svelte": "./dist/ui/FileTypeIcon.svelte"
79
+ "./ui/FileTypeIcon.svelte": "./dist/ui/FileTypeIcon.svelte",
80
+ "./ui/CollapsibleSection.svelte": "./dist/ui/CollapsibleSection.svelte"
76
81
  },
77
82
  "files": [
78
83
  "dist",