@openforge-app/plugin-sdk 0.1.0 → 0.2.3
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/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/browserSurfaces.d.ts +78 -0
- package/dist/browserSurfaces.js +17 -0
- package/dist/browserSurfacesTesting.d.ts +60 -0
- package/dist/browserSurfacesTesting.js +240 -0
- package/dist/domain.d.ts +74 -15
- package/dist/domain.js +5 -1
- package/dist/fileIcons.d.ts +6 -0
- package/dist/fileIcons.js +84 -0
- package/dist/frontend.d.ts +5 -2
- package/dist/frontend.js +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/manifest.d.ts +29 -0
- package/dist/manifest.js +37 -0
- package/dist/markdown.d.ts +5 -1
- package/dist/markdown.js +67 -18
- package/dist/openforgePackageMetadataSchema.json +40 -1
- package/dist/pluginIcons.d.ts +7 -0
- package/dist/pluginIcons.js +104 -0
- package/dist/publicUiExports.d.mts +16 -0
- package/dist/publicUiExports.mjs +59 -0
- package/dist/svelteHostRuntimeContract.d.mts +1 -0
- package/dist/svelteHostRuntimeContract.mjs +16 -0
- package/dist/testing/backendServicesFake.d.ts +19 -0
- package/dist/testing/backendServicesFake.js +87 -0
- package/dist/testing/commonApiFake.d.ts +22 -0
- package/dist/testing/commonApiFake.js +274 -0
- package/dist/testing/contracts.d.ts +231 -0
- package/dist/testing/contracts.js +1 -0
- package/dist/testing/frontendContributionFake.d.ts +36 -0
- package/dist/testing/frontendContributionFake.js +195 -0
- package/dist/testing/registryFake.d.ts +40 -0
- package/dist/testing/registryFake.js +124 -0
- package/dist/testing/support.d.ts +40 -0
- package/dist/testing/support.js +242 -0
- package/dist/testing.d.ts +3 -223
- package/dist/testing.js +2 -625
- package/dist/types.d.ts +193 -7
- package/dist/types.js +8 -0
- package/dist/ui/Button.svelte +49 -0
- package/dist/ui/Checkbox.svelte +137 -0
- package/dist/ui/FileTypeIcon.svelte +37 -0
- package/dist/ui/MarkdownContent.svelte +71 -11
- package/dist/ui/Modal.svelte +157 -0
- package/dist/ui/PluginPageHeader.svelte +26 -0
- package/dist/ui/PluginViewState.svelte +76 -0
- package/dist/ui/ResizablePanel.svelte +10 -0
- package/dist/ui/icons/README.md +9 -0
- package/dist/ui/icons/c.svg +1 -0
- package/dist/ui/icons/console.svg +1 -0
- package/dist/ui/icons/cpp.svg +1 -0
- package/dist/ui/icons/csharp.svg +1 -0
- package/dist/ui/icons/css.svg +3 -0
- package/dist/ui/icons/database.svg +1 -0
- package/dist/ui/icons/docker.svg +1 -0
- package/dist/ui/icons/document.svg +4 -0
- package/dist/ui/icons/file.svg +1 -0
- package/dist/ui/icons/folder-open.svg +1 -0
- package/dist/ui/icons/folder.svg +1 -0
- package/dist/ui/icons/git.svg +3 -0
- package/dist/ui/icons/go.svg +1 -0
- package/dist/ui/icons/graphql.svg +20 -0
- package/dist/ui/icons/h.svg +3 -0
- package/dist/ui/icons/html.svg +3 -0
- package/dist/ui/icons/image.svg +3 -0
- package/dist/ui/icons/java.svg +6 -0
- package/dist/ui/icons/javascript.svg +3 -0
- package/dist/ui/icons/json.svg +3 -0
- package/dist/ui/icons/kotlin.svg +1 -0
- package/dist/ui/icons/lock.svg +3 -0
- package/dist/ui/icons/markdown.svg +6 -0
- package/dist/ui/icons/nodejs.svg +6 -0
- package/dist/ui/icons/npm.svg +3 -0
- package/dist/ui/icons/pdf.svg +1 -0
- package/dist/ui/icons/php.svg +1 -0
- package/dist/ui/icons/python.svg +1 -0
- package/dist/ui/icons/react.svg +8 -0
- package/dist/ui/icons/react_ts.svg +8 -0
- package/dist/ui/icons/readme.svg +4 -0
- package/dist/ui/icons/ruby.svg +1 -0
- package/dist/ui/icons/rust.svg +1 -0
- package/dist/ui/icons/sass.svg +3 -0
- package/dist/ui/icons/settings.svg +4 -0
- package/dist/ui/icons/svelte.svg +1 -0
- package/dist/ui/icons/svg.svg +3 -0
- package/dist/ui/icons/swift.svg +1 -0
- package/dist/ui/icons/tune.svg +11 -0
- package/dist/ui/icons/typescript-def.svg +6 -0
- package/dist/ui/icons/typescript.svg +3 -0
- package/dist/ui/icons/vue.svg +1 -0
- package/dist/ui/icons/xml.svg +1 -0
- package/dist/ui/icons/yaml.svg +3 -0
- package/dist/ui/icons/zip.svg +3 -0
- package/dist/vite.js +5 -2
- package/package.json +34 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Koen Van Geert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @openforge-app/plugin-sdk
|
|
2
|
+
|
|
3
|
+
Public SDK for building OpenForge plugins. See [`docs/plugin-authoring.md`](../../docs/plugin-authoring.md) for the complete package, runtime, Svelte-sharing, and CSS-loading contract.
|
|
4
|
+
|
|
5
|
+
Styled frontend plugins must declare Vite/Svelte's emitted CSS artifacts in `package.json#openforge.frontendStyles`:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"openforge": {
|
|
10
|
+
"frontend": "./dist/frontend.js",
|
|
11
|
+
"frontendStyles": ["./dist/plugin.css"]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The host validates, attaches, reloads, and removes these package-relative stylesheets with the frontend plugin lifecycle.
|
|
17
|
+
|
|
18
|
+
## Publishing
|
|
19
|
+
|
|
20
|
+
Automated releases use npm trusted publishing rather than a long-lived npm token. Configure the package's npm Trusted Publisher with repository `koenvg/openforge`, workflow `publish-plugin-sdk.yml`, and environment `npm-publish`. The workflow is the caller npm validates even though the shared publish steps live in `reusable-publish-plugin-sdk.yml`. For a failed tag release, dispatch this workflow manually with the missing `package_version` and disable dry-run.
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
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
|
+
|
|
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.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Disposable } from './types';
|
|
2
|
+
export type BrowserSurfaceErrorCode = 'CAPABILITY_UNAVAILABLE' | 'HOST_UNAVAILABLE' | 'INVALID_TASK' | 'PLUGIN_NOT_ENABLED' | 'INVALID_ID' | 'INVALID_URL' | 'INVALID_BOUNDS' | 'CAPTURE_UNAVAILABLE' | 'CAPTURE_FAILED' | 'SURFACE_ACCESS_DENIED' | 'SURFACE_DESTROYED';
|
|
3
|
+
export declare class BrowserSurfaceError extends Error {
|
|
4
|
+
readonly code: BrowserSurfaceErrorCode;
|
|
5
|
+
constructor(code: BrowserSurfaceErrorCode, message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface BrowserSurfaceNavigationError {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TaskBrowserSurfaceState {
|
|
13
|
+
url: string;
|
|
14
|
+
title: string;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
canGoBack: boolean;
|
|
17
|
+
canGoForward: boolean;
|
|
18
|
+
error: BrowserSurfaceNavigationError | null;
|
|
19
|
+
}
|
|
20
|
+
export interface BrowserSurfaceCapture {
|
|
21
|
+
artifactId: string;
|
|
22
|
+
/** Absolute local PNG path retained for the Task Agent after acknowledgement. */
|
|
23
|
+
absolutePath: string;
|
|
24
|
+
mediaType: 'image/png';
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
url: string;
|
|
28
|
+
title: string;
|
|
29
|
+
capturedAt: string;
|
|
30
|
+
dataUrl: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GetOrCreateBrowserSurfaceRequest {
|
|
33
|
+
taskId: string;
|
|
34
|
+
id: string;
|
|
35
|
+
initialUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface BrowserSurfaceRegion {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
43
|
+
export interface BrowserSurfaceFeedbackSelection {
|
|
44
|
+
region: BrowserSurfaceRegion;
|
|
45
|
+
comment: string;
|
|
46
|
+
/** Host-assigned number rendered on the live page. Older hosts may omit it. */
|
|
47
|
+
annotationNumber?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface BrowserSurfaceVisualFeedback {
|
|
50
|
+
annotationNumber: number;
|
|
51
|
+
url: string;
|
|
52
|
+
region: BrowserSurfaceRegion;
|
|
53
|
+
comment: string;
|
|
54
|
+
}
|
|
55
|
+
export interface TaskBrowserSurfaceController {
|
|
56
|
+
attach(element: HTMLElement): Promise<Disposable>;
|
|
57
|
+
detach(): Promise<void>;
|
|
58
|
+
destroy(): Promise<void>;
|
|
59
|
+
getState(): Promise<TaskBrowserSurfaceState>;
|
|
60
|
+
onStateChanged(handler: (state: TaskBrowserSurfaceState) => void): Disposable;
|
|
61
|
+
navigate(url: string): Promise<TaskBrowserSurfaceState>;
|
|
62
|
+
goBack(): Promise<TaskBrowserSurfaceState>;
|
|
63
|
+
goForward(): Promise<TaskBrowserSurfaceState>;
|
|
64
|
+
reload(): Promise<TaskBrowserSurfaceState>;
|
|
65
|
+
stop(): Promise<TaskBrowserSurfaceState>;
|
|
66
|
+
selectVisibleRegion(): Promise<BrowserSurfaceFeedbackSelection | null>;
|
|
67
|
+
cancelVisibleRegionSelection(): Promise<void>;
|
|
68
|
+
clearVisualFeedback(): Promise<void>;
|
|
69
|
+
replaceVisualFeedback(feedback: readonly BrowserSurfaceVisualFeedback[]): Promise<void>;
|
|
70
|
+
captureExists(artifactId: string): Promise<boolean>;
|
|
71
|
+
captureVisibleViewport(): Promise<BrowserSurfaceCapture>;
|
|
72
|
+
discardCapture(artifactId: string): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
export interface BrowserSurfacesAPI {
|
|
75
|
+
getOrCreate(request: GetOrCreateBrowserSurfaceRequest): Promise<TaskBrowserSurfaceController>;
|
|
76
|
+
resetSession(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export declare function isAllowedBrowserSurfaceUrl(value: string): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class BrowserSurfaceError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'BrowserSurfaceError';
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function isAllowedBrowserSurfaceUrl(value) {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BrowserSurfacesAPI, BrowserSurfaceVisualFeedback, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceState } from './browserSurfaces';
|
|
2
|
+
export interface TestingBrowserSurfaceCalls {
|
|
3
|
+
browserSurfaceGetOrCreate: GetOrCreateBrowserSurfaceRequest[];
|
|
4
|
+
browserSurfaceAttachments: Array<{
|
|
5
|
+
taskId: string;
|
|
6
|
+
id: string;
|
|
7
|
+
element: HTMLElement;
|
|
8
|
+
}>;
|
|
9
|
+
browserSurfaceDetaches: Array<{
|
|
10
|
+
taskId: string;
|
|
11
|
+
id: string;
|
|
12
|
+
}>;
|
|
13
|
+
browserSurfaceDestroys: Array<{
|
|
14
|
+
taskId: string;
|
|
15
|
+
id: string;
|
|
16
|
+
}>;
|
|
17
|
+
browserSurfaceNavigations: Array<{
|
|
18
|
+
taskId: string;
|
|
19
|
+
id: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}>;
|
|
22
|
+
browserSurfaceControls: Array<{
|
|
23
|
+
taskId: string;
|
|
24
|
+
id: string;
|
|
25
|
+
action: 'goBack' | 'goForward' | 'reload' | 'stop';
|
|
26
|
+
}>;
|
|
27
|
+
browserSurfaceSelections: Array<{
|
|
28
|
+
taskId: string;
|
|
29
|
+
id: string;
|
|
30
|
+
}>;
|
|
31
|
+
browserSurfaceFeedbackClears: Array<{
|
|
32
|
+
taskId: string;
|
|
33
|
+
id: string;
|
|
34
|
+
}>;
|
|
35
|
+
browserSurfaceFeedbackReplacements: Array<{
|
|
36
|
+
taskId: string;
|
|
37
|
+
id: string;
|
|
38
|
+
feedback: BrowserSurfaceVisualFeedback[];
|
|
39
|
+
}>;
|
|
40
|
+
browserSurfaceCaptureChecks: Array<{
|
|
41
|
+
taskId: string;
|
|
42
|
+
id: string;
|
|
43
|
+
artifactId: string;
|
|
44
|
+
}>;
|
|
45
|
+
browserSurfaceCaptures: Array<{
|
|
46
|
+
taskId: string;
|
|
47
|
+
id: string;
|
|
48
|
+
}>;
|
|
49
|
+
browserSurfaceCaptureDiscards: Array<{
|
|
50
|
+
taskId: string;
|
|
51
|
+
id: string;
|
|
52
|
+
artifactId: string;
|
|
53
|
+
}>;
|
|
54
|
+
browserSurfaceSessionResets: Array<Record<string, never>>;
|
|
55
|
+
}
|
|
56
|
+
export interface TestingBrowserSurfaces {
|
|
57
|
+
readonly api: BrowserSurfacesAPI;
|
|
58
|
+
setState(taskId: string, id: string, patch: Partial<TaskBrowserSurfaceState>): void;
|
|
59
|
+
}
|
|
60
|
+
export declare function createTestingBrowserSurfaces(calls: TestingBrowserSurfaceCalls): TestingBrowserSurfaces;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { BrowserSurfaceError, isAllowedBrowserSurfaceUrl, } from './browserSurfaces';
|
|
2
|
+
function disposable(dispose) {
|
|
3
|
+
let disposed = false;
|
|
4
|
+
return {
|
|
5
|
+
async dispose() {
|
|
6
|
+
if (disposed)
|
|
7
|
+
return;
|
|
8
|
+
disposed = true;
|
|
9
|
+
await dispose();
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function surfaceKey(taskId, id) {
|
|
14
|
+
return `${taskId}\u0000${id}`;
|
|
15
|
+
}
|
|
16
|
+
function copyState(state) {
|
|
17
|
+
return { ...state, error: state.error ? { ...state.error } : null };
|
|
18
|
+
}
|
|
19
|
+
function validateIdentity(taskId, id) {
|
|
20
|
+
if (!taskId.trim())
|
|
21
|
+
throw new BrowserSurfaceError('INVALID_TASK', 'Task Browser Surface requires a non-empty Task ID');
|
|
22
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id)) {
|
|
23
|
+
throw new BrowserSurfaceError('INVALID_ID', 'Task Browser Surface id must be a stable non-empty local identifier');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class TestingTaskBrowserSurface {
|
|
27
|
+
taskId;
|
|
28
|
+
id;
|
|
29
|
+
calls;
|
|
30
|
+
onDestroyed;
|
|
31
|
+
state;
|
|
32
|
+
history;
|
|
33
|
+
historyIndex = 0;
|
|
34
|
+
listeners = new Set();
|
|
35
|
+
currentAttachment = 0;
|
|
36
|
+
destroyed = false;
|
|
37
|
+
nextAnnotationNumber = 1;
|
|
38
|
+
capturedArtifactIds = new Set();
|
|
39
|
+
constructor(taskId, id, initialUrl, calls, onDestroyed) {
|
|
40
|
+
this.taskId = taskId;
|
|
41
|
+
this.id = id;
|
|
42
|
+
this.calls = calls;
|
|
43
|
+
this.onDestroyed = onDestroyed;
|
|
44
|
+
const url = initialUrl ?? 'about:blank';
|
|
45
|
+
this.history = [url];
|
|
46
|
+
this.state = {
|
|
47
|
+
url,
|
|
48
|
+
title: '',
|
|
49
|
+
loading: false,
|
|
50
|
+
canGoBack: false,
|
|
51
|
+
canGoForward: false,
|
|
52
|
+
error: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async attach(element) {
|
|
56
|
+
this.assertLive();
|
|
57
|
+
const attachment = ++this.currentAttachment;
|
|
58
|
+
this.calls.browserSurfaceAttachments.push({ taskId: this.taskId, id: this.id, element });
|
|
59
|
+
return disposable(async () => {
|
|
60
|
+
if (this.destroyed || attachment !== this.currentAttachment)
|
|
61
|
+
return;
|
|
62
|
+
await this.detach();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async detach() {
|
|
66
|
+
this.assertLive();
|
|
67
|
+
this.currentAttachment += 1;
|
|
68
|
+
this.calls.browserSurfaceDetaches.push({ taskId: this.taskId, id: this.id });
|
|
69
|
+
}
|
|
70
|
+
async destroy() {
|
|
71
|
+
if (this.destroyed)
|
|
72
|
+
return;
|
|
73
|
+
this.destroyed = true;
|
|
74
|
+
this.listeners.clear();
|
|
75
|
+
this.calls.browserSurfaceDestroys.push({ taskId: this.taskId, id: this.id });
|
|
76
|
+
this.onDestroyed();
|
|
77
|
+
}
|
|
78
|
+
async getState() {
|
|
79
|
+
this.assertLive();
|
|
80
|
+
return copyState(this.state);
|
|
81
|
+
}
|
|
82
|
+
onStateChanged(handler) {
|
|
83
|
+
this.assertLive();
|
|
84
|
+
this.listeners.add(handler);
|
|
85
|
+
return disposable(() => { this.listeners.delete(handler); });
|
|
86
|
+
}
|
|
87
|
+
async navigate(url) {
|
|
88
|
+
this.assertLive();
|
|
89
|
+
if (!isAllowedBrowserSurfaceUrl(url)) {
|
|
90
|
+
throw new BrowserSurfaceError('INVALID_URL', 'Task Browser Surfaces only allow HTTP(S) navigation');
|
|
91
|
+
}
|
|
92
|
+
this.calls.browserSurfaceNavigations.push({ taskId: this.taskId, id: this.id, url });
|
|
93
|
+
this.history.splice(this.historyIndex + 1);
|
|
94
|
+
this.history.push(url);
|
|
95
|
+
this.historyIndex = this.history.length - 1;
|
|
96
|
+
this.publish({ url, title: '', error: null });
|
|
97
|
+
return this.getState();
|
|
98
|
+
}
|
|
99
|
+
async goBack() {
|
|
100
|
+
this.assertLive();
|
|
101
|
+
this.calls.browserSurfaceControls.push({ taskId: this.taskId, id: this.id, action: 'goBack' });
|
|
102
|
+
if (this.historyIndex > 0)
|
|
103
|
+
this.historyIndex -= 1;
|
|
104
|
+
this.publish({ url: this.history[this.historyIndex], error: null });
|
|
105
|
+
return this.getState();
|
|
106
|
+
}
|
|
107
|
+
async goForward() {
|
|
108
|
+
this.assertLive();
|
|
109
|
+
this.calls.browserSurfaceControls.push({ taskId: this.taskId, id: this.id, action: 'goForward' });
|
|
110
|
+
if (this.historyIndex < this.history.length - 1)
|
|
111
|
+
this.historyIndex += 1;
|
|
112
|
+
this.publish({ url: this.history[this.historyIndex], error: null });
|
|
113
|
+
return this.getState();
|
|
114
|
+
}
|
|
115
|
+
async reload() {
|
|
116
|
+
this.assertLive();
|
|
117
|
+
this.calls.browserSurfaceControls.push({ taskId: this.taskId, id: this.id, action: 'reload' });
|
|
118
|
+
this.publish({ error: null });
|
|
119
|
+
return this.getState();
|
|
120
|
+
}
|
|
121
|
+
async stop() {
|
|
122
|
+
this.assertLive();
|
|
123
|
+
this.calls.browserSurfaceControls.push({ taskId: this.taskId, id: this.id, action: 'stop' });
|
|
124
|
+
this.publish({ loading: false });
|
|
125
|
+
return this.getState();
|
|
126
|
+
}
|
|
127
|
+
async selectVisibleRegion() {
|
|
128
|
+
this.assertLive();
|
|
129
|
+
this.calls.browserSurfaceSelections.push({ taskId: this.taskId, id: this.id });
|
|
130
|
+
return {
|
|
131
|
+
region: { x: 0.1, y: 0.1, width: 0.4, height: 0.4 },
|
|
132
|
+
comment: 'Example visual feedback',
|
|
133
|
+
annotationNumber: this.nextAnnotationNumber++,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async cancelVisibleRegionSelection() {
|
|
137
|
+
this.assertLive();
|
|
138
|
+
}
|
|
139
|
+
async clearVisualFeedback() {
|
|
140
|
+
this.assertLive();
|
|
141
|
+
this.calls.browserSurfaceFeedbackClears.push({ taskId: this.taskId, id: this.id });
|
|
142
|
+
this.nextAnnotationNumber = 1;
|
|
143
|
+
}
|
|
144
|
+
async replaceVisualFeedback(feedback) {
|
|
145
|
+
this.assertLive();
|
|
146
|
+
this.calls.browserSurfaceFeedbackReplacements.push({
|
|
147
|
+
taskId: this.taskId,
|
|
148
|
+
id: this.id,
|
|
149
|
+
feedback: feedback.map(marker => ({ ...marker, region: { ...marker.region } })),
|
|
150
|
+
});
|
|
151
|
+
this.nextAnnotationNumber = feedback.reduce((maximum, marker) => Math.max(maximum, marker.annotationNumber), 0) + 1;
|
|
152
|
+
}
|
|
153
|
+
async captureExists(artifactId) {
|
|
154
|
+
this.assertLive();
|
|
155
|
+
if (!artifactId.trim())
|
|
156
|
+
throw new BrowserSurfaceError('INVALID_ID', 'Browser Surface capture requires an artifact ID');
|
|
157
|
+
this.calls.browserSurfaceCaptureChecks.push({ taskId: this.taskId, id: this.id, artifactId });
|
|
158
|
+
return this.capturedArtifactIds.has(artifactId);
|
|
159
|
+
}
|
|
160
|
+
async captureVisibleViewport() {
|
|
161
|
+
this.assertLive();
|
|
162
|
+
this.calls.browserSurfaceCaptures.push({ taskId: this.taskId, id: this.id });
|
|
163
|
+
const artifactId = `capture-${this.calls.browserSurfaceCaptures.length}`;
|
|
164
|
+
this.capturedArtifactIds.add(artifactId);
|
|
165
|
+
return {
|
|
166
|
+
artifactId,
|
|
167
|
+
absolutePath: `/tmp/openforge-browser-captures/${this.taskId}/capture-${this.calls.browserSurfaceCaptures.length}.png`,
|
|
168
|
+
mediaType: 'image/png',
|
|
169
|
+
width: 800,
|
|
170
|
+
height: 600,
|
|
171
|
+
url: this.state.url,
|
|
172
|
+
title: this.state.title,
|
|
173
|
+
capturedAt: '2026-01-01T00:00:00.000Z',
|
|
174
|
+
dataUrl: 'data:image/png;base64,iVBORw0KGgo=',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async discardCapture(artifactId) {
|
|
178
|
+
this.assertLive();
|
|
179
|
+
if (!artifactId.trim())
|
|
180
|
+
throw new BrowserSurfaceError('INVALID_ID', 'Browser Surface capture requires an artifact ID');
|
|
181
|
+
this.calls.browserSurfaceCaptureDiscards.push({ taskId: this.taskId, id: this.id, artifactId });
|
|
182
|
+
this.capturedArtifactIds.delete(artifactId);
|
|
183
|
+
}
|
|
184
|
+
setState(patch) {
|
|
185
|
+
this.assertLive();
|
|
186
|
+
this.state = copyState({ ...this.state, ...patch });
|
|
187
|
+
this.notify();
|
|
188
|
+
}
|
|
189
|
+
publish(patch) {
|
|
190
|
+
this.state = {
|
|
191
|
+
...this.state,
|
|
192
|
+
...patch,
|
|
193
|
+
canGoBack: this.historyIndex > 0,
|
|
194
|
+
canGoForward: this.historyIndex < this.history.length - 1,
|
|
195
|
+
};
|
|
196
|
+
this.notify();
|
|
197
|
+
}
|
|
198
|
+
notify() {
|
|
199
|
+
for (const listener of this.listeners)
|
|
200
|
+
listener(copyState(this.state));
|
|
201
|
+
}
|
|
202
|
+
assertLive() {
|
|
203
|
+
if (this.destroyed)
|
|
204
|
+
throw new BrowserSurfaceError('SURFACE_DESTROYED', 'Task Browser Surface has been destroyed');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function createTestingBrowserSurfaces(calls) {
|
|
208
|
+
const surfaces = new Map();
|
|
209
|
+
const api = {
|
|
210
|
+
async getOrCreate(request) {
|
|
211
|
+
const recorded = request.initialUrl === undefined ? { taskId: request.taskId, id: request.id } : { ...request };
|
|
212
|
+
calls.browserSurfaceGetOrCreate.push(recorded);
|
|
213
|
+
validateIdentity(request.taskId, request.id);
|
|
214
|
+
const key = surfaceKey(request.taskId, request.id);
|
|
215
|
+
let surface = surfaces.get(key);
|
|
216
|
+
if (!surface) {
|
|
217
|
+
if (request.initialUrl !== undefined && !isAllowedBrowserSurfaceUrl(request.initialUrl)) {
|
|
218
|
+
throw new BrowserSurfaceError('INVALID_URL', 'Task Browser Surfaces only allow HTTP(S) initial URLs');
|
|
219
|
+
}
|
|
220
|
+
surface = new TestingTaskBrowserSurface(request.taskId, request.id, request.initialUrl, calls, () => surfaces.delete(key));
|
|
221
|
+
surfaces.set(key, surface);
|
|
222
|
+
}
|
|
223
|
+
return surface;
|
|
224
|
+
},
|
|
225
|
+
async resetSession() {
|
|
226
|
+
calls.browserSurfaceSessionResets.push({});
|
|
227
|
+
for (const surface of Array.from(surfaces.values()))
|
|
228
|
+
await surface.destroy();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
api,
|
|
233
|
+
setState(taskId, id, patch) {
|
|
234
|
+
const surface = surfaces.get(surfaceKey(taskId, id));
|
|
235
|
+
if (!surface)
|
|
236
|
+
throw new BrowserSurfaceError('SURFACE_DESTROYED', 'Task Browser Surface is not live');
|
|
237
|
+
surface.setState(patch);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
package/dist/domain.d.ts
CHANGED
|
@@ -23,17 +23,15 @@ export interface Task {
|
|
|
23
23
|
title_source: 'manual' | 'generated' | null;
|
|
24
24
|
/** Unix timestamp for the first generated title write; null means generation has not written yet. */
|
|
25
25
|
title_generated_at: number | null;
|
|
26
|
-
summary: string | null;
|
|
27
26
|
agent: string | null;
|
|
28
27
|
permission_mode: string | null;
|
|
29
28
|
worktree_source: WorktreeSource | null;
|
|
30
29
|
worktree_branch: string | null;
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* agent is not instructed to maintain Handoff Notes.
|
|
31
|
+
* Optional link to the source ticket this task originated from (e.g. a GitHub
|
|
32
|
+
* issue URL or Jira browse link). `null` when no ticket was provided.
|
|
35
33
|
*/
|
|
36
|
-
|
|
34
|
+
source_ticket_url: string | null;
|
|
37
35
|
depends_on: string[];
|
|
38
36
|
project_id: string | null;
|
|
39
37
|
created_at: number;
|
|
@@ -90,6 +88,7 @@ export interface AgentSession {
|
|
|
90
88
|
provider: string;
|
|
91
89
|
claude_session_id: string | null;
|
|
92
90
|
pi_session_id: string | null;
|
|
91
|
+
grok_session_id: string | null;
|
|
93
92
|
}
|
|
94
93
|
export interface ClaudeInstallStatus {
|
|
95
94
|
installed: boolean;
|
|
@@ -114,6 +113,8 @@ export interface PrComment {
|
|
|
114
113
|
file_path: string | null;
|
|
115
114
|
line_number: number | null;
|
|
116
115
|
addressed: number;
|
|
116
|
+
/** 1 when GitHub considers the comment outdated (its diff line changed). */
|
|
117
|
+
outdated: number;
|
|
117
118
|
created_at: number;
|
|
118
119
|
}
|
|
119
120
|
export interface PullRequestInfo {
|
|
@@ -250,12 +251,25 @@ export interface RateLimitNotification {
|
|
|
250
251
|
export interface AgentInfo {
|
|
251
252
|
name: string;
|
|
252
253
|
}
|
|
253
|
-
/** Command info from provider command endpoints — used for / autocomplete */
|
|
254
|
+
/** Command info from provider command endpoints — used for / autocomplete and injectable catalogs */
|
|
254
255
|
export interface CommandInfo {
|
|
255
256
|
name: string;
|
|
256
257
|
description: string | null;
|
|
257
258
|
source: string | null;
|
|
258
259
|
agent: string | null;
|
|
260
|
+
/** Where it comes from: "personal" | "project" | "plugin" | "builtin". Optional enrichment — only claude-code populates it. */
|
|
261
|
+
origin?: string | null;
|
|
262
|
+
/** "auto+manual" | "manual-only" — derived from disable-model-invocation / command semantics */
|
|
263
|
+
triggerMode?: string | null;
|
|
264
|
+
/** false => hidden background skill; injectable catalogs drop it */
|
|
265
|
+
userInvocable?: boolean | null;
|
|
266
|
+
/** e.g. ".claude" | ".agents"; null for builtin/plugin */
|
|
267
|
+
sourceDir?: string | null;
|
|
268
|
+
/** stable on-disk identity under the source skills dir — for a skill, its folder name
|
|
269
|
+
* (a single path component, e.g. "pr-writer"); for a command, the file name. null for builtin */
|
|
270
|
+
sourcePath?: string | null;
|
|
271
|
+
/** full SKILL.md body for a consumer's reading pane; null when there is no source file */
|
|
272
|
+
content?: string | null;
|
|
259
273
|
}
|
|
260
274
|
/** Extended agent info from provider agent endpoints — used for @ autocomplete */
|
|
261
275
|
export interface AutocompleteAgentInfo {
|
|
@@ -294,6 +308,17 @@ export interface ProjectAttention {
|
|
|
294
308
|
unaddressed_comments: number;
|
|
295
309
|
completed_agents: number;
|
|
296
310
|
}
|
|
311
|
+
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';
|
|
312
|
+
/** Backend-owned, Task-only Needs Attention read model. */
|
|
313
|
+
export interface TaskAttentionRow {
|
|
314
|
+
task_id: string;
|
|
315
|
+
project_id: string;
|
|
316
|
+
project_name: string;
|
|
317
|
+
title: string;
|
|
318
|
+
state: TaskAttentionState;
|
|
319
|
+
reason: string;
|
|
320
|
+
activity_at: number;
|
|
321
|
+
}
|
|
297
322
|
export interface WorktreeInfo {
|
|
298
323
|
id: number;
|
|
299
324
|
task_id: string;
|
|
@@ -401,9 +426,13 @@ export interface ReviewPullRequest {
|
|
|
401
426
|
}
|
|
402
427
|
/** GitHub label that suppresses a PR from review counting and grays out its card. */
|
|
403
428
|
export declare const DO_NOT_REVIEW_LABEL = "DO NOT REVIEW";
|
|
404
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* True when the PR carries the "DO NOT REVIEW" label (case-insensitive, trimmed).
|
|
431
|
+
* Tolerates a missing or null `labels` field so a single implementation can back
|
|
432
|
+
* both the plugin PR views and the app-side sort/badge helpers.
|
|
433
|
+
*/
|
|
405
434
|
export declare function hasDoNotReviewLabel(pr: {
|
|
406
|
-
labels
|
|
435
|
+
labels?: PrLabel[] | null;
|
|
407
436
|
}): boolean;
|
|
408
437
|
/** File diff from PR files endpoint */
|
|
409
438
|
export interface PrFileDiff {
|
|
@@ -516,6 +545,43 @@ export interface AgentReviewComment {
|
|
|
516
545
|
created_at: number;
|
|
517
546
|
updated_at: number;
|
|
518
547
|
}
|
|
548
|
+
/** One message in a local "Ask the AI author" Q&A thread. Never sent to GitHub. */
|
|
549
|
+
export interface AiThreadMessage {
|
|
550
|
+
role: 'user' | 'ai';
|
|
551
|
+
body: string;
|
|
552
|
+
created_at: number;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Where a Q&A thread is anchored: a specific diff line, a walkthrough step, or a
|
|
556
|
+
* specific AI review comment (a follow-up like "why did you suggest this?"). The
|
|
557
|
+
* `comment` variant carries the AI comment's id (so its suggestion can be quoted
|
|
558
|
+
* as context) plus the denormalized diff location so it renders inline like a
|
|
559
|
+
* line-anchored thread.
|
|
560
|
+
*/
|
|
561
|
+
export type AiThreadAnchor = {
|
|
562
|
+
type: 'line';
|
|
563
|
+
filename: string;
|
|
564
|
+
line: number;
|
|
565
|
+
side: 'LEFT' | 'RIGHT';
|
|
566
|
+
} | {
|
|
567
|
+
type: 'step';
|
|
568
|
+
step_id: string;
|
|
569
|
+
} | {
|
|
570
|
+
type: 'comment';
|
|
571
|
+
comment_id: number;
|
|
572
|
+
filename: string;
|
|
573
|
+
line: number;
|
|
574
|
+
side: 'LEFT' | 'RIGHT';
|
|
575
|
+
};
|
|
576
|
+
/** A private, per-commit reviewer↔AI conversation, stored locally (never on GitHub). */
|
|
577
|
+
export interface AiThread {
|
|
578
|
+
id: string;
|
|
579
|
+
anchor: AiThreadAnchor;
|
|
580
|
+
status: 'draft' | 'pending' | 'answered' | 'error';
|
|
581
|
+
messages: AiThreadMessage[];
|
|
582
|
+
created_at: number;
|
|
583
|
+
updated_at: number;
|
|
584
|
+
}
|
|
519
585
|
/** App-level view for top-bar navigation */
|
|
520
586
|
export type CoreAppView = 'board' | 'settings' | 'global_settings' | 'files';
|
|
521
587
|
/** App-level view for top-bar navigation */
|
|
@@ -551,13 +617,6 @@ export declare function splitCheckRuns(checks: CheckRunInfo[]): {
|
|
|
551
617
|
visible: CheckRunInfo[];
|
|
552
618
|
passingCount: number;
|
|
553
619
|
};
|
|
554
|
-
export interface Action {
|
|
555
|
-
id: string;
|
|
556
|
-
name: string;
|
|
557
|
-
prompt: string;
|
|
558
|
-
builtin: boolean;
|
|
559
|
-
enabled: boolean;
|
|
560
|
-
}
|
|
561
620
|
export type VoiceInputState = 'idle' | 'recording' | 'transcribing' | 'error';
|
|
562
621
|
export interface TranscriptionResult {
|
|
563
622
|
text: string;
|
package/dist/domain.js
CHANGED
|
@@ -293,7 +293,11 @@ export function preservePullRequestState(oldPr, newPr) {
|
|
|
293
293
|
}
|
|
294
294
|
/** GitHub label that suppresses a PR from review counting and grays out its card. */
|
|
295
295
|
export const DO_NOT_REVIEW_LABEL = 'DO NOT REVIEW';
|
|
296
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* True when the PR carries the "DO NOT REVIEW" label (case-insensitive, trimmed).
|
|
298
|
+
* Tolerates a missing or null `labels` field so a single implementation can back
|
|
299
|
+
* both the plugin PR views and the app-side sort/badge helpers.
|
|
300
|
+
*/
|
|
297
301
|
export function hasDoNotReviewLabel(pr) {
|
|
298
302
|
return (pr.labels ?? []).some((label) => label.name.trim().toUpperCase() === DO_NOT_REVIEW_LABEL);
|
|
299
303
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const DEFAULT_FILE_ICON = "file";
|
|
2
|
+
export declare const DEFAULT_FOLDER_ICON = "folder";
|
|
3
|
+
export declare const DEFAULT_FOLDER_OPEN_ICON = "folder-open";
|
|
4
|
+
export declare function getFileIconName(filename: string): string;
|
|
5
|
+
export declare function getFolderIconName(open: boolean): string;
|
|
6
|
+
export declare const BUNDLED_ICON_NAMES: readonly string[];
|