@plannotator/pi-extension 0.27.10 → 0.27.11
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/generated/ai/providers/opencode-sdk.ts +70 -46
- package/generated/ai/types.ts +1 -1
- package/generated/config.ts +29 -0
- package/generated/feedback-archive.ts +500 -0
- package/package.json +1 -1
- package/plannotator-browser.ts +1 -0
- package/plannotator.html +132 -132
- package/review-editor.html +2 -2
- package/server/ai-runtime.ts +12 -5
- package/server/feedback-archive.test.ts +357 -0
- package/server/serverAnnotate.ts +71 -12
- package/server/serverPlan.ts +41 -1
- package/server/serverReview.ts +104 -5
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* OpenCode provider — bridges Plannotator's AI layer with OpenCode's agent server.
|
|
4
4
|
*
|
|
5
|
-
* Uses @opencode-ai/sdk to
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Uses @opencode-ai/sdk to spawn a dedicated `opencode serve` on an
|
|
6
|
+
* OS-assigned port. One server is shared across all sessions of this process,
|
|
7
|
+
* closed on dispose and on process exit. This provider deliberately never
|
|
8
|
+
* attaches to a server it did not spawn: an attached server cannot be cleaned
|
|
9
|
+
* up by us, and opencode's per-directory instances accumulate in it without
|
|
10
|
+
* eviction, so a shared long-lived server grows without bound. The user must
|
|
11
|
+
* have the `opencode` CLI installed and authenticated.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
@@ -59,13 +62,19 @@ export class OpenCodeProvider implements AIProvider {
|
|
|
59
62
|
private server: { url: string; close: () => void } | null = null;
|
|
60
63
|
private client: OpencodeClient | null = null;
|
|
61
64
|
private startPromise: Promise<void> | null = null;
|
|
62
|
-
private
|
|
65
|
+
private exitHandler: (() => void) | null = null;
|
|
66
|
+
/**
|
|
67
|
+
* Bumped by dispose() to invalidate an in-flight doStart: a spawn that
|
|
68
|
+
* completes after its epoch has passed reaps its own server instead of
|
|
69
|
+
* resurrecting a provider the runtime already considers disposed.
|
|
70
|
+
*/
|
|
71
|
+
private startEpoch = 0;
|
|
63
72
|
|
|
64
73
|
constructor(config: OpenCodeConfig) {
|
|
65
74
|
this.config = config;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
/**
|
|
77
|
+
/** Spawn this process's OpenCode server if it is not already running. */
|
|
69
78
|
async ensureServer(): Promise<void> {
|
|
70
79
|
if (this.client) return;
|
|
71
80
|
this.startPromise ??= this.doStart().catch((err) => {
|
|
@@ -76,55 +85,63 @@ export class OpenCodeProvider implements AIProvider {
|
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
private async doStart(): Promise<void> {
|
|
79
|
-
|
|
88
|
+
const epoch = this.startEpoch;
|
|
80
89
|
const { createOpencodeServer, createOpencodeClient } = await getSDK();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
90
|
+
// port 0 asks opencode for an OS-assigned free port (the SDK reads the
|
|
91
|
+
// real URL back from the child's "listening" line), so every Plannotator
|
|
92
|
+
// process gets its own server instead of piling onto a shared default
|
|
93
|
+
// port. An explicitly configured port is still honored verbatim.
|
|
94
|
+
const server: { url: string; close: () => void } = await createOpencodeServer({
|
|
95
|
+
hostname: this.config.hostname ?? "127.0.0.1",
|
|
96
|
+
port: this.config.port ?? 0,
|
|
97
|
+
timeout: 15_000,
|
|
98
|
+
});
|
|
99
|
+
// A SIGINT/SIGTERM death is routed through process.exit() by the CLI, so
|
|
100
|
+
// an "exit" handler is what keeps Ctrl-C from orphaning the spawned
|
|
101
|
+
// `opencode serve` child (server.close() kills it synchronously). The
|
|
102
|
+
// closure captures ITS server — never `this.server`, which a failed
|
|
103
|
+
// retry could have replaced, leaving the first child unreachable by any
|
|
104
|
+
// cleanup. No SIGHUP listener: that would override the ignored
|
|
105
|
+
// disposition `nohup` depends on.
|
|
106
|
+
const exitHandler = () => {
|
|
107
|
+
try {
|
|
108
|
+
server.close();
|
|
109
|
+
} catch {
|
|
110
|
+
// Best effort — the process is exiting either way.
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
process.once("exit", exitHandler);
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} catch (err) {
|
|
94
|
-
const spawnMessage = err instanceof Error ? err.message : String(err);
|
|
95
|
-
if (this.lastAttachError) {
|
|
96
|
-
throw new Error(`${this.lastAttachError}\nFallback startup also failed: ${spawnMessage}`);
|
|
115
|
+
const reap = () => {
|
|
116
|
+
process.removeListener("exit", exitHandler);
|
|
117
|
+
try {
|
|
118
|
+
server.close();
|
|
119
|
+
} catch {
|
|
120
|
+
// Best effort — the child may already be gone.
|
|
97
121
|
}
|
|
98
|
-
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (epoch !== this.startEpoch) {
|
|
125
|
+
// dispose() ran while the spawn was in flight: the runtime no longer
|
|
126
|
+
// wants this provider, so reap the just-spawned server instead of
|
|
127
|
+
// resurrecting a disposed provider with a live child.
|
|
128
|
+
reap();
|
|
129
|
+
throw new Error("OpenCode provider was disposed during startup.");
|
|
99
130
|
}
|
|
100
131
|
|
|
101
|
-
this.client = createOpencodeClient({
|
|
102
|
-
baseUrl: this.server!.url,
|
|
103
|
-
directory: this.config.cwd ?? process.cwd(),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private async tryAttachExistingServer(
|
|
108
|
-
createOpencodeClient: (config?: { baseUrl?: string; directory?: string }) => OpencodeClient,
|
|
109
|
-
): Promise<OpencodeClient | null> {
|
|
110
|
-
const cwd = this.config.cwd ?? process.cwd();
|
|
111
|
-
const baseUrl = `http://${this.config.hostname ?? "127.0.0.1"}:${this.config.port ?? 4096}`;
|
|
112
|
-
const client = createOpencodeClient({
|
|
113
|
-
baseUrl,
|
|
114
|
-
directory: cwd,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
132
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
this.client = createOpencodeClient({
|
|
134
|
+
baseUrl: server.url,
|
|
135
|
+
directory: this.config.cwd ?? process.cwd(),
|
|
121
136
|
});
|
|
122
|
-
return client;
|
|
123
137
|
} catch (err) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
// A post-spawn failure must not leak the child: close it and drop the
|
|
139
|
+
// handler so a retry starts from a clean slate.
|
|
140
|
+
reap();
|
|
141
|
+
throw err;
|
|
127
142
|
}
|
|
143
|
+
this.server = server;
|
|
144
|
+
this.exitHandler = exitHandler;
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
private getClient(): OpencodeClient {
|
|
@@ -199,6 +216,13 @@ export class OpenCodeProvider implements AIProvider {
|
|
|
199
216
|
}
|
|
200
217
|
|
|
201
218
|
dispose(): void {
|
|
219
|
+
// Invalidate any in-flight doStart so a spawn completing after this
|
|
220
|
+
// point reaps itself instead of resurrecting the provider.
|
|
221
|
+
this.startEpoch++;
|
|
222
|
+
if (this.exitHandler) {
|
|
223
|
+
process.removeListener("exit", this.exitHandler);
|
|
224
|
+
this.exitHandler = null;
|
|
225
|
+
}
|
|
202
226
|
if (this.server) {
|
|
203
227
|
this.server.close();
|
|
204
228
|
this.server = null;
|
package/generated/ai/types.ts
CHANGED
|
@@ -316,6 +316,6 @@ export interface OpenCodeConfig extends AIProviderConfig {
|
|
|
316
316
|
type: "opencode-sdk";
|
|
317
317
|
/** Hostname for the OpenCode server. Default: "127.0.0.1". */
|
|
318
318
|
hostname?: string;
|
|
319
|
-
/** Port for the OpenCode server. Default:
|
|
319
|
+
/** Port for the spawned OpenCode server. Default: 0 (OS-assigned free port). */
|
|
320
320
|
port?: number;
|
|
321
321
|
}
|
package/generated/config.ts
CHANGED
|
@@ -181,6 +181,16 @@ export interface PlannotatorConfig {
|
|
|
181
181
|
* annotate sessions fully stateless. Default: true.
|
|
182
182
|
*/
|
|
183
183
|
annotateHistory?: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Durably archive every submitted review under ~/.plannotator/feedback/
|
|
186
|
+
* (or PLANNOTATOR_DATA_DIR): one append-only JSONL record per submission
|
|
187
|
+
* plus a markdown sidecar for the ones that carry content. NOTE: this
|
|
188
|
+
* writes the user's own feedback text and the document/code excerpts it
|
|
189
|
+
* quotes to disk, and nothing prunes the directory. Set to false to never
|
|
190
|
+
* write. Default: true. Annotate-surface records additionally honor
|
|
191
|
+
* `annotateHistory`, so the stateless-annotate promise is unchanged.
|
|
192
|
+
*/
|
|
193
|
+
feedbackHistory?: boolean;
|
|
184
194
|
/**
|
|
185
195
|
* Extra file extensions annotate treats as markdown (#1307), e.g.
|
|
186
196
|
* [".livemd"] for Livebook notebooks. Listed extensions are accepted
|
|
@@ -648,6 +658,25 @@ export function resolveAnnotateHistory(config: PlannotatorConfig): boolean {
|
|
|
648
658
|
return coerceConfigBoolean(config.annotateHistory, true);
|
|
649
659
|
}
|
|
650
660
|
|
|
661
|
+
/**
|
|
662
|
+
* Resolve whether submitted feedback is archived under feedback/.
|
|
663
|
+
*
|
|
664
|
+
* Priority (highest wins):
|
|
665
|
+
* PLANNOTATOR_FEEDBACK_HISTORY env var → config.feedbackHistory → default true
|
|
666
|
+
*
|
|
667
|
+
* Deliberately a separate knob from annotateHistory: that one governs copying
|
|
668
|
+
* ANNOTATED CONTENT into the data dir, this one governs keeping the user's own
|
|
669
|
+
* SUBMISSIONS, and a code-review user must be able to control the second
|
|
670
|
+
* without touching the first. Annotate surfaces honor both.
|
|
671
|
+
*/
|
|
672
|
+
export function resolveFeedbackHistory(config: PlannotatorConfig): boolean {
|
|
673
|
+
const envVal = process.env.PLANNOTATOR_FEEDBACK_HISTORY;
|
|
674
|
+
if (envVal !== undefined) {
|
|
675
|
+
return envVal === "1" || envVal.toLowerCase() === "true";
|
|
676
|
+
}
|
|
677
|
+
return coerceConfigBoolean(config.feedbackHistory, true);
|
|
678
|
+
}
|
|
679
|
+
|
|
651
680
|
/**
|
|
652
681
|
* Resolve whether successful Guided Reviews are persisted to disk.
|
|
653
682
|
*
|