pi-studio-opencode 0.1.0

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.
Files changed (60) hide show
  1. package/ARCHITECTURE.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/dist/demo-host-pi.d.ts +1 -0
  5. package/dist/demo-host-pi.js +71 -0
  6. package/dist/demo-host-pi.js.map +1 -0
  7. package/dist/demo-host.d.ts +1 -0
  8. package/dist/demo-host.js +154 -0
  9. package/dist/demo-host.js.map +1 -0
  10. package/dist/host-opencode-plugin.d.ts +52 -0
  11. package/dist/host-opencode-plugin.js +396 -0
  12. package/dist/host-opencode-plugin.js.map +1 -0
  13. package/dist/host-opencode.d.ts +154 -0
  14. package/dist/host-opencode.js +627 -0
  15. package/dist/host-opencode.js.map +1 -0
  16. package/dist/host-pi.d.ts +45 -0
  17. package/dist/host-pi.js +258 -0
  18. package/dist/host-pi.js.map +1 -0
  19. package/dist/install-config.d.ts +36 -0
  20. package/dist/install-config.js +136 -0
  21. package/dist/install-config.js.map +1 -0
  22. package/dist/install.d.ts +16 -0
  23. package/dist/install.js +168 -0
  24. package/dist/install.js.map +1 -0
  25. package/dist/launcher.d.ts +2 -0
  26. package/dist/launcher.js +124 -0
  27. package/dist/launcher.js.map +1 -0
  28. package/dist/main.d.ts +1 -0
  29. package/dist/main.js +732 -0
  30. package/dist/main.js.map +1 -0
  31. package/dist/mock-pi-session.d.ts +27 -0
  32. package/dist/mock-pi-session.js +138 -0
  33. package/dist/mock-pi-session.js.map +1 -0
  34. package/dist/open-browser.d.ts +1 -0
  35. package/dist/open-browser.js +29 -0
  36. package/dist/open-browser.js.map +1 -0
  37. package/dist/opencode-plugin.d.ts +3 -0
  38. package/dist/opencode-plugin.js +326 -0
  39. package/dist/opencode-plugin.js.map +1 -0
  40. package/dist/prototype-pdf.d.ts +12 -0
  41. package/dist/prototype-pdf.js +991 -0
  42. package/dist/prototype-pdf.js.map +1 -0
  43. package/dist/prototype-server.d.ts +88 -0
  44. package/dist/prototype-server.js +1002 -0
  45. package/dist/prototype-server.js.map +1 -0
  46. package/dist/prototype-theme.d.ts +36 -0
  47. package/dist/prototype-theme.js +1471 -0
  48. package/dist/prototype-theme.js.map +1 -0
  49. package/dist/studio-core.d.ts +63 -0
  50. package/dist/studio-core.js +251 -0
  51. package/dist/studio-core.js.map +1 -0
  52. package/dist/studio-host-types.d.ts +50 -0
  53. package/dist/studio-host-types.js +14 -0
  54. package/dist/studio-host-types.js.map +1 -0
  55. package/examples/opencode/INSTALL.md +67 -0
  56. package/examples/opencode/opencode.local-path.jsonc +16 -0
  57. package/package.json +68 -0
  58. package/static/prototype.css +1277 -0
  59. package/static/prototype.html +173 -0
  60. package/static/prototype.js +3198 -0
@@ -0,0 +1,122 @@
1
+ # Architecture
2
+
3
+ ## Current goal
4
+
5
+ `pi-studio-opencode` is the first standalone host implementation of **π Studio** outside the original Pi extension.
6
+
7
+ Near-term priority is to ship a genuinely usable opencode-facing Studio and learn from real users before locking in a long-term shared package structure.
8
+
9
+ ## Design philosophy
10
+
11
+ Keep **Studio behavior** as host-neutral as practical, and keep **host integration** as a thin adapter layer.
12
+
13
+ In other words:
14
+
15
+ - shared Studio layers should define what Studio means
16
+ - run / queue steering / stop semantics
17
+ - response history and provenance
18
+ - follow-latest and selection behavior
19
+ - preview and math behavior
20
+ - editor and file-workflow behavior where practical
21
+ - host adapters should define how those semantics are delivered
22
+ - session transport
23
+ - backend queue / stop wiring
24
+ - host-specific metadata
25
+ - host-specific editor integration
26
+
27
+ This repo should avoid drifting into “an opencode app with Studio-like UI”. It should instead remain “a Studio implementation whose first standalone host is opencode”.
28
+
29
+ ## Relationship to `pi-studio`
30
+
31
+ For now, this repo stays **separate** from `pi-studio`.
32
+
33
+ That separation is intentional:
34
+
35
+ - `pi-studio` remains the stable shipped Pi extension
36
+ - `pi-studio-opencode` can evolve faster
37
+ - we should not force an early merge or premature extraction just for architectural neatness
38
+
39
+ `pi-studio` may still be used as a behavioral reference when validating parity, but standalone work should happen here.
40
+
41
+ ## Current no-port opencode bridge
42
+
43
+ The current no-port `/studio` path is acceptable as a **real adapter layer**, but it should be understood as a mix of clean architecture and temporary opencode-specific workarounds.
44
+
45
+ ### Reasonably clean parts
46
+
47
+ - the plugin owns the launch flow for `/studio`
48
+ - the browser UI still talks to a local Studio HTTP server, not directly to opencode internals
49
+ - Studio behavior remains behind the normal host abstraction (`StudioHost`)
50
+ - the plugin-backed host uses `ctx.client` for imperative session operations
51
+ - create / reuse session
52
+ - prompt
53
+ - abort
54
+ - fetch messages
55
+ - `startPrototypeServer(...)` can now accept an injected host factory, so the browser surface is not tightly coupled to one transport
56
+
57
+ ### Current workaround parts
58
+
59
+ - command launch still relies on `command.execute.before` interception and a sentinel throw to cancel normal command execution after Studio has been opened
60
+ - live updates in plain no-port TUI mode rely on plugin event forwarding plus message reconciliation, because browser-side SSE against opencode is not currently reliable there
61
+ - response/user-message binding is therefore adapter logic rather than a single upstream canonical subscription API
62
+ - plugin-load failures currently degrade badly: `/studio` falls back to a normal prompt template, which is confusing and should eventually be guarded more explicitly
63
+
64
+ ### Practical interpretation
65
+
66
+ This is acceptable for now because it avoids much worse options such as TUI scraping, sqlite mutation, or shell-driven key simulation.
67
+
68
+ But it should still be treated as:
69
+
70
+ - a good current integration path
71
+ - not yet proof that upstream opencode APIs are the final ideal surface for Studio
72
+ - something we should simplify if opencode later provides a cleaner command-handled or event-subscription path
73
+
74
+ ## Recommended evolution
75
+
76
+ ### Phase A — ship and learn
77
+
78
+ - make `pi-studio-opencode` useful enough for opencode users to try
79
+ - release preliminary versions early
80
+ - learn what is genuinely shared and what is still host-specific in disguise
81
+
82
+ ### Phase B — extract only the stable shared layers
83
+
84
+ Once the boundaries are clearer from real use, extract the pieces that have actually stabilized, for example:
85
+
86
+ - shared Studio state / provenance logic
87
+ - shared preview / math pipeline
88
+ - shared browser UI behavior
89
+ - shared host type contracts
90
+
91
+ Do **not** force a giant “core” extraction before that boundary is obvious.
92
+
93
+ ### Phase C — test a new Pi-facing layer on top of the shared layers
94
+
95
+ A key validation step should be to build a **new Pi layer** on top of the extracted shared Studio layers.
96
+
97
+ That is the real test of whether the architecture is working.
98
+
99
+ The point is not just to share code abstractly. The point is to stop manually porting fixes between Pi and opencode and to stop keeping both interfaces in sync by hand.
100
+
101
+ If the shared layers are good enough, then the eventual end state could be:
102
+
103
+ - `pi-studio-opencode` = thin opencode-facing layer + shared Studio dependencies
104
+ - `pi-studio` = thin Pi-facing layer + shared Studio dependencies
105
+
106
+ ## Non-goals for now
107
+
108
+ - no premature repo merge with `pi-studio`
109
+ - no forced monorepo yet
110
+ - no promise that the final extracted package is exactly called `studio-core`
111
+ - no rewriting the existing `pi-studio` extension before the shared path proves itself
112
+
113
+ ## Practical rule of thumb
114
+
115
+ When adding or changing behavior here, ask:
116
+
117
+ 1. Is this genuinely **Studio behavior**?
118
+ - keep it host-neutral if possible.
119
+ 2. Is this specifically **how opencode works**?
120
+ - keep it in the opencode adapter layer.
121
+
122
+ That discipline matters more right now than the exact future package layout.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oliver Maclaren
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,108 @@
1
+ # pi-studio-opencode
2
+
3
+ OpenCode plug-in adaptation of the [pi](https://pi.dev/) extension [`pi-studio`](https://github.com/omaclaren/pi-studio).
4
+
5
+ It provides a local browser-based Studio workspace for [OpenCode](https://opencode.ai/), with an editor on the left and response/preview on the right.
6
+
7
+ If you use pi itself, use the original [`pi-studio`](https://github.com/omaclaren/pi-studio). This repo is for OpenCode.
8
+
9
+ ## Status
10
+
11
+ Usable and actively improving.
12
+
13
+ The main goal is a good OpenCode version of Studio rather than full feature parity with `pi-studio`.
14
+
15
+ ## Features
16
+
17
+ - Launch Studio from an active OpenCode session with `/studio`
18
+ - Attach Studio to the same session you are already using
19
+ - Two-pane editor + response/preview workspace
20
+ - Run, queue steering, stop, and browse response history
21
+ - Raw response, rendered response preview, and editor preview
22
+ - Load and save local files
23
+ - Markdown / QMD / LaTeX preview with practical support for:
24
+ - math fallback
25
+ - Quarto-style callouts
26
+ - preview page-break markers
27
+ - local PDF figure preview via `pdf.js`
28
+ - PDF export of the right-pane preview via `pandoc` + `xelatex`
29
+ - Theme-aware Studio UI based on the active OpenCode theme
30
+ - Footer/status info, pane focus controls, and response highlighting inspired by `pi-studio`
31
+
32
+ ## Install
33
+
34
+ Recommended installation:
35
+
36
+ ```bash
37
+ bunx pi-studio-opencode@latest install
38
+ ```
39
+
40
+ That updates your OpenCode config to add:
41
+
42
+ - the `pi-studio-opencode@latest` plugin entry
43
+ - the `/studio` command entry
44
+
45
+ For a project-local install instead of a user-wide one:
46
+
47
+ ```bash
48
+ bunx pi-studio-opencode@latest install --project
49
+ ```
50
+
51
+ Then restart OpenCode and run:
52
+
53
+ ```text
54
+ /studio
55
+ ```
56
+
57
+ ## Manual config
58
+
59
+ If you prefer to edit config yourself, add this to either:
60
+
61
+ - `.opencode/opencode.jsonc`
62
+ - `~/.config/opencode/opencode.json`
63
+ - `~/.config/opencode/opencode.jsonc`
64
+
65
+ ```json
66
+ {
67
+ "$schema": "https://opencode.ai/config.json",
68
+ "plugin": ["pi-studio-opencode@latest"],
69
+ "command": {
70
+ "studio": {
71
+ "template": "Open π Studio for this active opencode session.",
72
+ "description": "Open π Studio attached to the current opencode session"
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## Notes
79
+
80
+ - `/studio` opens a browser-based Studio linked to the current OpenCode session.
81
+ - If `/studio` gets sent to the model as ordinary text, the plug-in probably did not load. Rebuild or reinstall the package, then fully restart OpenCode.
82
+ - If you use `--no-open`, open the full tokenized URL shown by Studio, not just the bare port root.
83
+ - `--base-url`, `--session`, and `--directory` are taken from the active OpenCode session during `/studio`.
84
+ - The Studio UI is external to OpenCode; it is not an embedded pane.
85
+ - Preview and PDF quality depend on local tooling:
86
+ - `pandoc` for preview/PDF workflows
87
+ - `xelatex` for PDF export
88
+
89
+ ## Standalone launcher
90
+
91
+ You can also launch the browser surface directly:
92
+
93
+ ```bash
94
+ pi-studio-opencode --directory "/path/to/project"
95
+ ```
96
+
97
+ or:
98
+
99
+ ```bash
100
+ npx pi-studio-opencode --directory "/path/to/project"
101
+ ```
102
+
103
+ To attach manually to an existing OpenCode server/session:
104
+
105
+ ```bash
106
+ pi-studio-opencode --base-url "http://127.0.0.1:4096" --session "<session-id>" --directory "/path/to/project"
107
+ ```
108
+
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { createPiStudioHost } from "./host-pi.js";
3
+ import { MockPiSession } from "./mock-pi-session.js";
4
+ import { describeStudioHostCapabilities } from "./studio-host-types.js";
5
+ const RUN_PROMPT = [
6
+ "Please reply in one short paragraph.",
7
+ "Say INITIAL_RUN_OK exactly once.",
8
+ ].join("\n");
9
+ const STEER_1 = [
10
+ "Queued steering:",
11
+ "append QUEUE_ONE_OK as a new final sentence.",
12
+ ].join("\n");
13
+ const STEER_2 = [
14
+ "Queued steering 2:",
15
+ "append QUEUE_TWO_OK as a new final sentence.",
16
+ ].join("\n");
17
+ const ABORT_PROMPT = [
18
+ "Write 120 numbered bullet points about event streams.",
19
+ "Do not summarize.",
20
+ ].join("\n");
21
+ function formatState(state) {
22
+ return [
23
+ `backend=${state.backend}`,
24
+ `runState=${state.runState}`,
25
+ `activeChain=${state.activeChainIndex ?? "-"}`,
26
+ `activePrompt=${state.activePromptId ? state.activePromptId.slice(0, 16) : "-"}`,
27
+ `queue=${state.queueLength}`,
28
+ `history=${state.historyCount}`,
29
+ `status=${state.lastBackendStatus ?? "-"}`,
30
+ ].join(" ");
31
+ }
32
+ async function main() {
33
+ const session = new MockPiSession();
34
+ const host = await createPiStudioHost({
35
+ session,
36
+ eventLogger: (line) => console.log(line),
37
+ });
38
+ console.log(`host capabilities: ${describeStudioHostCapabilities(host.getCapabilities())}`);
39
+ const unsubscribe = host.subscribe((state) => {
40
+ console.log(`[state] ${formatState(state)}`);
41
+ });
42
+ try {
43
+ console.log("\n--- pi sketch: native steer semantics ---\n");
44
+ await host.startRun(RUN_PROMPT);
45
+ await sleep(50);
46
+ await host.queueSteer(STEER_1);
47
+ await sleep(50);
48
+ await host.queueSteer(STEER_2);
49
+ await host.waitUntilIdle();
50
+ console.log("History after steering chain:");
51
+ for (const item of host.getHistory()) {
52
+ console.log(`- chain=${item.chainIndex} mode=${item.promptMode} steeringCount=${item.promptSteeringCount} response=${(item.responseText ?? "").replace(/\s+/g, " ")}`);
53
+ }
54
+ console.log("\n--- pi sketch: abort ---\n");
55
+ await host.startRun(ABORT_PROMPT);
56
+ await sleep(50);
57
+ await host.stop();
58
+ await host.waitUntilIdle();
59
+ console.log("Final history:\n");
60
+ console.log(JSON.stringify(host.getHistory(), null, 2));
61
+ }
62
+ finally {
63
+ unsubscribe();
64
+ await host.close();
65
+ }
66
+ }
67
+ void main().catch((error) => {
68
+ console.error(error instanceof Error ? error.stack ?? error.message : error);
69
+ process.exitCode = 1;
70
+ });
71
+ //# sourceMappingURL=demo-host-pi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo-host-pi.js","sourceRoot":"","sources":["../src/demo-host-pi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,154 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { createOpencodeStudioHost } from "./host-opencode.js";
3
+ import { describeStudioHostCapabilities } from "./studio-host-types.js";
4
+ const RUN_PROMPT = [
5
+ "Respond with exactly this plain text and nothing else:",
6
+ "INITIAL_RUN_OK",
7
+ ].join("\n");
8
+ const STEER_1 = [
9
+ "Respond with exactly this plain text and nothing else:",
10
+ "INITIAL_RUN_OK QUEUE_ONE_OK",
11
+ ].join("\n");
12
+ const STEER_2 = [
13
+ "Respond with exactly this plain text and nothing else:",
14
+ "INITIAL_RUN_OK QUEUE_ONE_OK QUEUE_TWO_OK",
15
+ ].join("\n");
16
+ const ABORT_PROMPT = [
17
+ "Write 120 numbered bullet points about event streams.",
18
+ "Do not summarize.",
19
+ ].join("\n");
20
+ function parseArgs(argv) {
21
+ const options = {
22
+ directory: process.cwd(),
23
+ queueDelayMs: 700,
24
+ secondQueueDelayMs: 500,
25
+ abortDelayMs: 500,
26
+ abortTest: true,
27
+ };
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ const next = argv[i + 1];
31
+ if (arg === "--directory" && next) {
32
+ options.directory = next;
33
+ i += 1;
34
+ continue;
35
+ }
36
+ if (arg === "--base-url" && next) {
37
+ options.baseUrl = next;
38
+ i += 1;
39
+ continue;
40
+ }
41
+ if (arg === "--session" && next) {
42
+ options.sessionId = next;
43
+ i += 1;
44
+ continue;
45
+ }
46
+ if (arg === "--title" && next) {
47
+ options.title = next;
48
+ i += 1;
49
+ continue;
50
+ }
51
+ if (arg === "--queue-delay-ms" && next) {
52
+ options.queueDelayMs = Number.parseInt(next, 10);
53
+ i += 1;
54
+ continue;
55
+ }
56
+ if (arg === "--second-queue-delay-ms" && next) {
57
+ options.secondQueueDelayMs = Number.parseInt(next, 10);
58
+ i += 1;
59
+ continue;
60
+ }
61
+ if (arg === "--abort-delay-ms" && next) {
62
+ options.abortDelayMs = Number.parseInt(next, 10);
63
+ i += 1;
64
+ continue;
65
+ }
66
+ if (arg === "--no-abort-test") {
67
+ options.abortTest = false;
68
+ continue;
69
+ }
70
+ if (arg === "--help" || arg === "-h") {
71
+ printUsageAndExit();
72
+ }
73
+ throw new Error(`Unknown argument: ${arg}`);
74
+ }
75
+ return options;
76
+ }
77
+ function printUsageAndExit() {
78
+ console.log(`Usage: npm run host-demo -- [options]
79
+
80
+ Options:
81
+ --directory <path> Project directory / working directory
82
+ --base-url <url> Use an existing opencode server
83
+ --session <id> Reuse an existing session
84
+ --title <title> Title for a newly created session
85
+ --queue-delay-ms <n> Delay before queueing the first steer (default: 700)
86
+ --second-queue-delay-ms <n> Delay before queueing the second steer (default: 500)
87
+ --abort-delay-ms <n> Delay before stop() in the abort demo (default: 500)
88
+ --no-abort-test Skip the abort scenario
89
+ `);
90
+ process.exit(0);
91
+ }
92
+ function formatState(state) {
93
+ return [
94
+ `runState=${state.runState}`,
95
+ `activeChain=${state.activeChainIndex ?? "-"}`,
96
+ `activePrompt=${state.activePromptId ? state.activePromptId.slice(0, 16) : "-"}`,
97
+ `queue=${state.queueLength}`,
98
+ `history=${state.historyCount}`,
99
+ `backend=${state.lastBackendStatus ?? "-"}`,
100
+ ].join(" ");
101
+ }
102
+ async function main() {
103
+ const options = parseArgs(process.argv.slice(2));
104
+ const host = await createOpencodeStudioHost({
105
+ directory: options.directory,
106
+ baseUrl: options.baseUrl,
107
+ sessionId: options.sessionId,
108
+ title: options.title,
109
+ eventLogger: (line) => console.log(line),
110
+ });
111
+ console.log(`host capabilities: ${describeStudioHostCapabilities(host.getCapabilities())}`);
112
+ const unsubscribe = host.subscribe((state) => {
113
+ console.log(`[state] ${formatState(state)}`);
114
+ });
115
+ try {
116
+ console.log("\n--- scenario 1: run + locally queued steers ---\n");
117
+ await host.startRun(RUN_PROMPT);
118
+ await sleep(options.queueDelayMs);
119
+ await host.queueSteer(STEER_1);
120
+ await sleep(options.secondQueueDelayMs);
121
+ await host.queueSteer(STEER_2);
122
+ await host.waitUntilIdle();
123
+ console.log("History after scenario 1:");
124
+ for (const item of host.getHistory()) {
125
+ console.log(`- chain=${item.chainIndex} mode=${item.promptMode} steeringCount=${item.promptSteeringCount} queuedWhileBusy=${item.queuedWhileBusy}`);
126
+ console.log(` response=${(item.responseText ?? "").replace(/\s+/g, " ").slice(0, 140)}`);
127
+ if (item.responseError) {
128
+ console.log(` error=${item.responseError}`);
129
+ }
130
+ }
131
+ if (options.abortTest) {
132
+ console.log("\n--- scenario 2: abort current run ---\n");
133
+ await host.startRun(ABORT_PROMPT);
134
+ await sleep(options.abortDelayMs);
135
+ await host.stop();
136
+ await host.waitUntilIdle();
137
+ const last = host.getHistory().at(-1);
138
+ if (last) {
139
+ console.log(`Abort result: responseId=${last.responseMessageId ?? "(missing)"} error=${last.responseError ?? "(none)"}`);
140
+ }
141
+ }
142
+ console.log("\nFinal history:\n");
143
+ console.log(JSON.stringify(host.getHistory(), null, 2));
144
+ }
145
+ finally {
146
+ unsubscribe();
147
+ await host.close();
148
+ }
149
+ }
150
+ void main().catch((error) => {
151
+ console.error(error instanceof Error ? error.stack ?? error.message : error);
152
+ process.exitCode = 1;
153
+ });
154
+ //# sourceMappingURL=demo-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo-host.js","sourceRoot":"","sources":["../src/demo-host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { Event, OpencodeClient } from "@opencode-ai/sdk";
2
+ import { type OpencodeStudioHostTelemetryEvent } from "./host-opencode.js";
3
+ import type { StudioHost, StudioHostCapabilities, StudioHostHistoryItem, StudioHostListener, StudioHostState } from "./studio-host-types.js";
4
+ export type PluginBackedOpencodeStudioHostOptions = {
5
+ client: OpencodeClient;
6
+ directory: string;
7
+ sessionId?: string;
8
+ title?: string;
9
+ eventLogger?: (line: string) => void;
10
+ telemetryListener?: (event: OpencodeStudioHostTelemetryEvent) => void;
11
+ };
12
+ export declare class PluginBackedOpencodeStudioHost implements StudioHost {
13
+ private readonly options;
14
+ private readonly listeners;
15
+ private readonly baselineMessageIds;
16
+ private readonly matchedUserIds;
17
+ private readonly matchedAssistantIds;
18
+ private readonly partTypesById;
19
+ private readonly messageRolesById;
20
+ private readonly core;
21
+ private readonly client;
22
+ private session;
23
+ private handlingIdle;
24
+ private closed;
25
+ static create(options: PluginBackedOpencodeStudioHostOptions): Promise<PluginBackedOpencodeStudioHost>;
26
+ private constructor();
27
+ getState(): StudioHostState;
28
+ getCapabilities(): StudioHostCapabilities;
29
+ getHistory(): StudioHostHistoryItem[];
30
+ subscribe(listener: StudioHostListener): () => void;
31
+ startRun(prompt: string): Promise<void>;
32
+ queueSteer(prompt: string): Promise<void>;
33
+ stop(): Promise<void>;
34
+ waitUntilIdle(timeoutMs?: number): Promise<void>;
35
+ close(): Promise<void>;
36
+ ingestEvent(event: Event): Promise<void>;
37
+ private initialize;
38
+ private createOrReuseSession;
39
+ private emitModelTelemetryForMessage;
40
+ private handleEvent;
41
+ private handleSessionIdle;
42
+ private dispatchSubmission;
43
+ private finalizeActiveSubmission;
44
+ private bindSubmissionToMessages;
45
+ private reconcileObservedExternalResponses;
46
+ private fetchSessionMessages;
47
+ private emitState;
48
+ private emitTelemetry;
49
+ private fail;
50
+ private assertReady;
51
+ }
52
+ export declare function createPluginBackedOpencodeStudioHost(options: PluginBackedOpencodeStudioHostOptions): Promise<PluginBackedOpencodeStudioHost>;