@openforge-app/plugin-sdk 0.2.5 → 0.2.7

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;
@@ -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
@@ -117,6 +117,7 @@ export interface PrComment {
117
117
  outdated: number;
118
118
  created_at: number;
119
119
  }
120
+ export type PullRequestMergeMethod = 'merge' | 'squash' | 'rebase';
120
121
  export interface PullRequestInfo {
121
122
  id: number;
122
123
  pr_number: number;
@@ -149,6 +150,9 @@ export interface PullRequestInfo {
149
150
  merge_queue_required: boolean | null;
150
151
  merge_queue_state: string | null;
151
152
  readiness_updated_at: number | null;
153
+ merge_methods_policy_known?: boolean | null;
154
+ allowed_merge_methods?: string | PullRequestMergeMethod[] | null;
155
+ default_merge_method?: PullRequestMergeMethod | null;
152
156
  }
153
157
  export type PollOutcome = 'completed' | 'missing_github_token' | 'github_token_unavailable' | 'failed' | 'rate_limited';
154
158
  export interface PollResult {
@@ -310,7 +314,7 @@ export interface ProjectAttention {
310
314
  unaddressed_comments: number;
311
315
  completed_agents: number;
312
316
  }
313
- 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';
314
318
  /** Backend-owned, Task-only Needs Attention read model. */
315
319
  export interface TaskAttentionRow {
316
320
  task_id: string;
@@ -495,18 +499,6 @@ export interface ReviewSubmission {
495
499
  body: string;
496
500
  comments: ReviewSubmissionComment[];
497
501
  }
498
- /** Self-review comment for task implementation review */
499
- export interface SelfReviewComment {
500
- id: number;
501
- task_id: string;
502
- round: number;
503
- comment_type: string;
504
- file_path: string | null;
505
- line_number: number | null;
506
- body: string;
507
- created_at: number;
508
- archived_at: number | null;
509
- }
510
502
  /** One file referenced by a walkthrough step. `hunk_indexes === null` means the entire file's diff belongs to the step. */
511
503
  export interface PrWalkthroughStepFile {
512
504
  filename: string;
@@ -1,4 +1,4 @@
1
- import type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState } from './browserSurfaces';
1
+ import type { BrowserSurfaceCapture, BrowserDevToolsPanel, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState } from './browserSurfaces';
2
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 & {
@@ -6,5 +6,5 @@ export type MarkedFrontendPlugin<TPlugin extends FrontendPlugin = FrontendPlugin
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, };
9
+ export type { BrowserSurfaceCapture, BrowserDevToolsPanel, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState, };
10
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
@@ -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, 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, 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';
@@ -1,8 +1,9 @@
1
- import { type MergeReadinessAction, type MergeReadinessDetail, type MergeReadinessStatus, type MergeStatusInfo } from './domain';
1
+ import { type MergeReadinessAction, type MergeReadinessDetail, type MergeReadinessStatus, type MergeStatusInfo, type PullRequestMergeMethod } from './domain';
2
2
  export type PrChipSurface = 'compact' | 'detail';
3
3
  export type PrChipVariant = 'success' | 'error' | 'pending' | 'muted' | 'neutral' | 'done' | 'merged' | 'closed';
4
4
  export type PrChipType = 'draft' | 'ci' | 'review' | 'merge';
5
5
  export type PrChipIcon = 'check' | 'cross' | 'clock' | null;
6
+ export declare function getPullRequestMergeActionLabel(method: PullRequestMergeMethod, prNumber?: number): string;
6
7
  export interface PrStatusChipSpec {
7
8
  type: PrChipType;
8
9
  label: string;
@@ -1,4 +1,15 @@
1
1
  import { getMergeReadiness, isClosedUnmergedPullRequest, isMergedPullRequest } from './domain';
2
+ const PULL_REQUEST_MERGE_ACTION_LABELS = {
3
+ merge: 'Create a merge commit',
4
+ squash: 'Squash and merge',
5
+ rebase: 'Rebase and merge',
6
+ };
7
+ export function getPullRequestMergeActionLabel(method, prNumber) {
8
+ const label = PULL_REQUEST_MERGE_ACTION_LABELS[method];
9
+ if (prNumber === undefined)
10
+ return label;
11
+ return method === 'merge' ? `${label} for PR #${prNumber}` : `${label} PR #${prNumber}`;
12
+ }
2
13
  export function getPrStatusChips(pr, surface) {
3
14
  const chips = [];
4
15
  if (pr.draft && pr.state === 'open') {
@@ -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
  /**
@@ -146,6 +146,7 @@ export class TestingCommonApiFake {
146
146
  write: async (request) => {
147
147
  this.services.calls.shellWrites.push(request);
148
148
  },
149
+ writeTerminalQueryResponse: async () => { },
149
150
  resize: async (request) => {
150
151
  this.services.calls.shellResizes.push(request);
151
152
  },
@@ -154,7 +155,7 @@ export class TestingCommonApiFake {
154
155
  },
155
156
  getBuffer: async (request) => {
156
157
  this.services.calls.shellBuffers.push(request);
157
- return { buffer: null, isLive: false };
158
+ return { buffer: null, isLive: false, instanceId: null };
158
159
  },
159
160
  },
160
161
  notifications: {
package/dist/types.d.ts CHANGED
@@ -432,6 +432,10 @@ export interface ShellSpawnRequest extends ShellSessionRequest {
432
432
  export interface ShellWriteRequest extends ShellSessionRequest {
433
433
  data: string;
434
434
  }
435
+ export interface ShellTerminalQueryResponseRequest extends ShellSessionRequest {
436
+ ptyInstanceId: number;
437
+ data: string;
438
+ }
435
439
  export interface ShellResizeRequest extends ShellSessionRequest {
436
440
  cols: number;
437
441
  rows: number;
@@ -439,10 +443,12 @@ export interface ShellResizeRequest extends ShellSessionRequest {
439
443
  export interface PtyBufferState {
440
444
  buffer: string | null;
441
445
  isLive: boolean;
446
+ instanceId: number | null;
442
447
  }
443
448
  export interface ShellAPI {
444
449
  spawn(request: ShellSpawnRequest): Promise<number>;
445
450
  write(request: ShellWriteRequest): Promise<void>;
451
+ writeTerminalQueryResponse(request: ShellTerminalQueryResponseRequest): Promise<void>;
446
452
  resize(request: ShellResizeRequest): Promise<void>;
447
453
  kill(request: ShellSessionRequest): Promise<void>;
448
454
  getBuffer(request: ShellSessionRequest): Promise<PtyBufferState>;
@@ -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.5",
3
+ "version": "0.2.7",
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",
package/dist/context.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import type { Disposable, OpenForgeContextChangeHandler, OpenForgePackageMetadata, OpenForgePluginContext, SubscriptionSink, SupportedOpenForgeApiVersion } from './types';
2
- declare class SubscriptionSet implements SubscriptionSink {
3
- private readonly subscriptions;
4
- add(subscription: Disposable | (() => void)): void;
5
- disposeAll(): Promise<void>;
6
- }
7
- export declare class PluginContextImpl implements OpenForgePluginContext {
8
- readonly pluginId: string;
9
- readonly apiVersion: SupportedOpenForgeApiVersion;
10
- readonly packageMetadata: OpenForgePackageMetadata;
11
- readonly subscriptions: SubscriptionSet;
12
- onDidChange(_handler: OpenForgeContextChangeHandler): Disposable;
13
- constructor(pluginId: string, apiVersion: SupportedOpenForgeApiVersion, packageMetadata: OpenForgePackageMetadata);
14
- }
15
- export {};
package/dist/context.js DELETED
@@ -1,33 +0,0 @@
1
- class SubscriptionSet {
2
- subscriptions = new Set();
3
- add(subscription) {
4
- this.subscriptions.add(subscription);
5
- }
6
- async disposeAll() {
7
- const subscriptions = [...this.subscriptions];
8
- this.subscriptions.clear();
9
- await Promise.all(subscriptions.map(async (subscription) => {
10
- if (typeof subscription === 'function') {
11
- subscription();
12
- }
13
- else {
14
- await subscription.dispose();
15
- }
16
- }));
17
- }
18
- }
19
- export class PluginContextImpl {
20
- pluginId;
21
- apiVersion;
22
- packageMetadata;
23
- subscriptions;
24
- onDidChange(_handler) {
25
- return { dispose: () => undefined };
26
- }
27
- constructor(pluginId, apiVersion, packageMetadata) {
28
- this.pluginId = pluginId;
29
- this.apiVersion = apiVersion;
30
- this.packageMetadata = packageMetadata;
31
- this.subscriptions = new SubscriptionSet();
32
- }
33
- }