@neuralnomads/codenomad-dev 0.18.0-dev-20260727-269cff64 → 0.18.0-dev-20260804-c16cc005
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/dist/background-processes/manager.js +15 -30
- package/dist/background-processes/manager.test.js +123 -0
- package/dist/index.js +2 -2
- package/dist/server/__tests__/listener-retry.test.js +15 -0
- package/dist/server/http-server.js +7 -7
- package/dist/workspaces/instance-client.js +6 -5
- package/dist/workspaces/instance-client.test.js +149 -0
- package/dist/workspaces/instance-events.js +2 -2
- package/dist/workspaces/loopback.js +8 -0
- package/dist/workspaces/manager.js +24 -8
- package/dist/workspaces/manager.test.js +55 -10
- package/package.json +1 -1
- package/public/assets/FilesTab-PxqWVPyo.js +2 -0
- package/public/assets/{GitChangesTab-COJ_xDva.js → GitChangesTab-CBKL5IdG.js} +1 -1
- package/public/assets/{SplitFilePanel-B5nrgBs3.js → SplitFilePanel-DaqtU5jw.js} +1 -1
- package/public/assets/StatusTab-B-z6yiGA.js +1 -0
- package/public/assets/{align-justify-CKDczyVy.js → align-justify-D8zzAQxs.js} +1 -1
- package/public/assets/{diff-viewer-drQ_Yje8.js → diff-viewer-BU6juksj.js} +1 -1
- package/public/assets/index-7b0Woxv7.js +1 -0
- package/public/assets/{index-Bt9PdFAD.css → index-B7bZsUV8.css} +1 -1
- package/public/assets/index-BnIGFV9i.js +1 -0
- package/public/assets/index-Bwil2k-o.js +1 -0
- package/public/assets/index-Cct6SfHO.js +1 -0
- package/public/assets/index-CfcsvGZu.js +1 -0
- package/public/assets/index-CzxPTjaX.js +1 -0
- package/public/assets/index-DMYv1E9L.js +2 -0
- package/public/assets/index-FXydT52m.js +1 -0
- package/public/assets/index-ZBLywF20.js +1 -0
- package/public/assets/{loading-Cz4dKVP8.js → loading-Bdrg1dVr.js} +1 -1
- package/public/assets/main-oNL0VKfF.js +69 -0
- package/public/assets/{markdown-DUHZyBXo.js → markdown-BwpQkztY.js} +1 -1
- package/public/assets/{tool-call-C7FlzFBf.js → tool-call-BOKqxL44.js} +3 -3
- package/public/assets/unified-picker-DoMT5Ia8.js +1 -0
- package/public/assets/{wrap-text-CS5K8Uhi.js → wrap-text-CJPfwppv.js} +1 -1
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/sw.js +1 -1
- package/public/assets/FilesTab-pl_3_Ppx.js +0 -2
- package/public/assets/StatusTab-rXgEwwYW.js +0 -1
- package/public/assets/index-BymyGk2C.js +0 -1
- package/public/assets/index-CEIEYgCg.js +0 -1
- package/public/assets/index-Cqo6ftJT.js +0 -1
- package/public/assets/index-Cs1fQOLl.js +0 -1
- package/public/assets/index-CwhvpTeE.js +0 -1
- package/public/assets/index-D0xuNxlo.js +0 -2
- package/public/assets/index-D1HW4nyh.js +0 -1
- package/public/assets/index-DBw0YE-2.js +0 -1
- package/public/assets/index-OYNGtp3z.js +0 -1
- package/public/assets/main-BEYytlgD.js +0 -68
- package/public/assets/unified-picker-CpU6qugd.js +0 -1
|
@@ -2,6 +2,7 @@ import { spawn, spawnSync } from "child_process";
|
|
|
2
2
|
import { createWriteStream, existsSync, promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { randomBytes } from "crypto";
|
|
5
|
+
import { createInstanceClient } from "../workspaces/instance-client";
|
|
5
6
|
const ROOT_DIR = ".codenomad/background_processes";
|
|
6
7
|
const INDEX_FILE = "index.json";
|
|
7
8
|
const OUTPUT_FILE = "output.txt";
|
|
@@ -511,38 +512,22 @@ export class BackgroundProcessManager {
|
|
|
511
512
|
const notify = record.notify;
|
|
512
513
|
if (!notify || !record.terminalReason)
|
|
513
514
|
return;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
const port = this.deps.workspaceManager.getInstancePort(workspaceId);
|
|
518
|
-
if (!port) {
|
|
519
|
-
throw new Error("Workspace instance is not ready");
|
|
520
|
-
}
|
|
521
|
-
const targetUrl = `http://127.0.0.1:${port}/session/${encodeURIComponent(notify.sessionID)}/prompt_async`;
|
|
522
|
-
const headers = {
|
|
523
|
-
"content-type": "application/json",
|
|
524
|
-
};
|
|
525
|
-
const authorization = this.deps.workspaceManager.getInstanceAuthorizationHeader(workspaceId);
|
|
526
|
-
if (authorization) {
|
|
527
|
-
headers.authorization = authorization;
|
|
528
|
-
}
|
|
529
|
-
const response = await fetch(targetUrl, {
|
|
530
|
-
method: "POST",
|
|
531
|
-
headers,
|
|
532
|
-
body: JSON.stringify({
|
|
533
|
-
parts: [
|
|
534
|
-
{
|
|
535
|
-
type: "text",
|
|
536
|
-
text: this.buildSyntheticCompletionPrompt(record),
|
|
537
|
-
synthetic: true,
|
|
538
|
-
},
|
|
539
|
-
],
|
|
540
|
-
}),
|
|
515
|
+
const client = createInstanceClient(this.deps.workspaceManager, workspaceId, {
|
|
516
|
+
directory: notify.directory,
|
|
541
517
|
});
|
|
542
|
-
if (!
|
|
543
|
-
|
|
544
|
-
throw new Error(message || `Prompt request failed with ${response.status}`);
|
|
518
|
+
if (!client) {
|
|
519
|
+
throw new Error("Workspace instance is not ready");
|
|
545
520
|
}
|
|
521
|
+
await client.session.promptAsync({
|
|
522
|
+
sessionID: notify.sessionID,
|
|
523
|
+
parts: [
|
|
524
|
+
{
|
|
525
|
+
type: "text",
|
|
526
|
+
text: this.buildSyntheticCompletionPrompt(record),
|
|
527
|
+
synthetic: true,
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
}, { throwOnError: true });
|
|
546
531
|
}
|
|
547
532
|
buildCompletionPrompt(record) {
|
|
548
533
|
const ref = `Background process "${record.title}" (${record.id})`;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { BackgroundProcessManager } from "./manager";
|
|
7
|
+
const WORKSPACE_ID = "ws-test";
|
|
8
|
+
const SESSION_ID = "sess-1";
|
|
9
|
+
const INSTANCE_PORT = 9999;
|
|
10
|
+
const AUTH_HEADER = "Basic test-auth";
|
|
11
|
+
const TERMINAL_TIMEOUT_MS = 3000;
|
|
12
|
+
/**
|
|
13
|
+
* Drives the real {@link BackgroundProcessManager} lifecycle (spawn a
|
|
14
|
+
* fast-exiting command with notify enabled) against a mocked transport, so the
|
|
15
|
+
* migrated `sendCompletionPrompt` path — factory + SDK client + `fetch` — is
|
|
16
|
+
* exercised end to end without touching production wiring.
|
|
17
|
+
*
|
|
18
|
+
* The workspace temp directory is intentionally left in place (under
|
|
19
|
+
* `os.tmpdir()`, OS-reaped): removing it from the test races the manager's
|
|
20
|
+
* asynchronous finalization writes, which intermittently fail with ENOENT.
|
|
21
|
+
*/
|
|
22
|
+
async function runCompletionPrompt(fetchImpl) {
|
|
23
|
+
const requests = [];
|
|
24
|
+
const originalFetch = globalThis.fetch;
|
|
25
|
+
// Captured now but swapped in only inside the try below, so a failure during
|
|
26
|
+
// setup (mkdtemp, manager construction) can't leak the mocked fetch.
|
|
27
|
+
const fetchMock = (async (input, init) => {
|
|
28
|
+
const req = input instanceof Request ? input : new Request(String(input), init);
|
|
29
|
+
requests.push({
|
|
30
|
+
method: req.method,
|
|
31
|
+
url: req.url,
|
|
32
|
+
headers: req.headers,
|
|
33
|
+
body: await req.text(),
|
|
34
|
+
});
|
|
35
|
+
return fetchImpl(input instanceof Request ? input : req, init);
|
|
36
|
+
});
|
|
37
|
+
let warned = false;
|
|
38
|
+
const logger = {
|
|
39
|
+
warn: () => { warned = true; },
|
|
40
|
+
debug: () => { },
|
|
41
|
+
trace: () => { },
|
|
42
|
+
info: () => { },
|
|
43
|
+
error: () => { },
|
|
44
|
+
fatal: () => { },
|
|
45
|
+
isLevelEnabled: () => false,
|
|
46
|
+
level: "info",
|
|
47
|
+
child: () => logger,
|
|
48
|
+
};
|
|
49
|
+
const workspacePath = await fs.mkdtemp(path.join(os.tmpdir(), "bp-test-"));
|
|
50
|
+
// Distinct from the workspace root so the directory-override assertion is
|
|
51
|
+
// discriminating: if `sendCompletionPrompt` stops passing `notify.directory`,
|
|
52
|
+
// the factory would fall back to `workspacePath` and the header check fails.
|
|
53
|
+
const sessionDir = path.join(workspacePath, "session-worktree");
|
|
54
|
+
// Resolve once the manager publishes a terminal (non-running) status update.
|
|
55
|
+
let resolveTerminal = () => { };
|
|
56
|
+
const terminal = new Promise((resolve) => { resolveTerminal = resolve; });
|
|
57
|
+
const eventBus = {
|
|
58
|
+
on: () => { },
|
|
59
|
+
publish: (event) => {
|
|
60
|
+
if (event?.type === "instance.event") {
|
|
61
|
+
const type = event?.event?.type;
|
|
62
|
+
const status = event?.event?.properties?.process?.status;
|
|
63
|
+
if (type === "background.process.removed" || (status && status !== "running"))
|
|
64
|
+
resolveTerminal();
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const workspaceManager = {
|
|
70
|
+
get: () => ({ path: workspacePath }),
|
|
71
|
+
getInstancePort: () => INSTANCE_PORT,
|
|
72
|
+
getInstanceAuthorizationHeader: () => AUTH_HEADER,
|
|
73
|
+
};
|
|
74
|
+
const manager = new BackgroundProcessManager({ workspaceManager, eventBus, logger });
|
|
75
|
+
try {
|
|
76
|
+
globalThis.fetch = fetchMock;
|
|
77
|
+
await manager.start(WORKSPACE_ID, "test-proc", "true", {
|
|
78
|
+
notify: true,
|
|
79
|
+
notification: { sessionID: SESSION_ID, directory: sessionDir },
|
|
80
|
+
});
|
|
81
|
+
// The terminal status update is published at the very end of finalize, so
|
|
82
|
+
// resolving on it is a deterministic completion signal. Fail loudly rather
|
|
83
|
+
// than racing a silent timeout that could mask a hang.
|
|
84
|
+
let timeoutHandle;
|
|
85
|
+
const reachedTerminal = await Promise.race([
|
|
86
|
+
terminal.then(() => true),
|
|
87
|
+
new Promise((resolve) => {
|
|
88
|
+
timeoutHandle = setTimeout(() => resolve(false), TERMINAL_TIMEOUT_MS);
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
if (timeoutHandle)
|
|
92
|
+
clearTimeout(timeoutHandle);
|
|
93
|
+
if (!reachedTerminal) {
|
|
94
|
+
throw new Error("background process did not reach a terminal state in time");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
globalThis.fetch = originalFetch;
|
|
99
|
+
}
|
|
100
|
+
return { requests, warned, directory: sessionDir };
|
|
101
|
+
}
|
|
102
|
+
describe("BackgroundProcessManager.sendCompletionPrompt", () => {
|
|
103
|
+
it("posts the synthetic completion prompt to the instance via the SDK route", async () => {
|
|
104
|
+
const { requests, directory } = await runCompletionPrompt(async () => new Response("{}", { status: 200, headers: { "content-type": "application/json" } }));
|
|
105
|
+
const promptCall = requests.find((r) => r.url.includes("/prompt_async"));
|
|
106
|
+
assert.ok(promptCall, "expected a prompt_async request");
|
|
107
|
+
assert.equal(promptCall.method, "POST");
|
|
108
|
+
assert.equal(promptCall.url, `http://127.0.0.1:${INSTANCE_PORT}/session/${SESSION_ID}/prompt_async`);
|
|
109
|
+
assert.equal(promptCall.headers.get("authorization"), AUTH_HEADER);
|
|
110
|
+
// The prompt is scoped to the session's directory (a POST keeps the
|
|
111
|
+
// directory as a header — the SDK only rewrites header→query for GET/HEAD).
|
|
112
|
+
assert.equal(promptCall.headers.get("x-opencode-directory"), encodeURIComponent(directory));
|
|
113
|
+
const body = JSON.parse(promptCall.body);
|
|
114
|
+
assert.equal(body.parts.length, 1);
|
|
115
|
+
assert.equal(body.parts[0].type, "text");
|
|
116
|
+
assert.equal(body.parts[0].synthetic, true);
|
|
117
|
+
assert.match(body.parts[0].text, /test-proc/);
|
|
118
|
+
});
|
|
119
|
+
it("swallows a failed prompt and logs it without aborting finalization", async () => {
|
|
120
|
+
const { warned } = await runCompletionPrompt(async () => new Response("boom", { status: 500 }));
|
|
121
|
+
assert.equal(warned, true);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -171,8 +171,8 @@ function resolveHost(input) {
|
|
|
171
171
|
}
|
|
172
172
|
return trimmed;
|
|
173
173
|
}
|
|
174
|
-
function programHasArg(argv, flag) {
|
|
175
|
-
return argv.
|
|
174
|
+
export function programHasArg(argv, flag) {
|
|
175
|
+
return argv.some((argument) => argument === flag || argument.startsWith(`${flag}=`));
|
|
176
176
|
}
|
|
177
177
|
async function main() {
|
|
178
178
|
const options = parseCliOptions(process.argv.slice(2));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { programHasArg } from "../../index";
|
|
4
|
+
import { shouldRetryPreferredPort } from "../http-server";
|
|
5
|
+
test("automatic listeners retry Windows reserved ports without masking explicit failures", () => {
|
|
6
|
+
assert.equal(shouldRetryPreferredPort({ code: "EADDRINUSE" }, true, "linux"), true);
|
|
7
|
+
assert.equal(shouldRetryPreferredPort({ code: "EACCES" }, true, "win32"), true);
|
|
8
|
+
assert.equal(shouldRetryPreferredPort({ code: "EACCES" }, true, "linux"), false);
|
|
9
|
+
assert.equal(shouldRetryPreferredPort({ code: "EACCES" }, false, "win32"), false);
|
|
10
|
+
});
|
|
11
|
+
test("explicit listener ports are detected in both supported CLI forms", () => {
|
|
12
|
+
assert.equal(programHasArg(["--http-port", "9899"], "--http-port"), true);
|
|
13
|
+
assert.equal(programHasArg(["--https-port=9898"], "--https-port"), true);
|
|
14
|
+
assert.equal(programHasArg(["--http-porter=9899"], "--http-port"), false);
|
|
15
|
+
});
|
|
@@ -29,6 +29,12 @@ import { BackgroundProcessManager } from "../background-processes/manager";
|
|
|
29
29
|
import { registerAuthRoutes } from "./routes/auth";
|
|
30
30
|
import { sendUnauthorized, wantsHtml } from "../auth/http-auth";
|
|
31
31
|
import { createOpenCodeUpdateService } from "../opencode-update/service";
|
|
32
|
+
export function shouldRetryPreferredPort(error, autoPortRequested, platform = process.platform) {
|
|
33
|
+
if (!autoPortRequested)
|
|
34
|
+
return false;
|
|
35
|
+
const code = error?.code;
|
|
36
|
+
return code === "EADDRINUSE" || (platform === "win32" && code === "EACCES");
|
|
37
|
+
}
|
|
32
38
|
export function createHttpServer(deps) {
|
|
33
39
|
// Fastify's type-level RawServer inference gets noisy when toggling HTTP vs HTTPS.
|
|
34
40
|
// We keep the runtime behavior correct and cast the instance to a generic FastifyInstance.
|
|
@@ -261,18 +267,12 @@ export function createHttpServer(deps) {
|
|
|
261
267
|
};
|
|
262
268
|
const autoPortRequested = deps.bindPort === 0;
|
|
263
269
|
const primaryPort = autoPortRequested ? deps.defaultPort : deps.bindPort;
|
|
264
|
-
const shouldRetryWithEphemeral = (error) => {
|
|
265
|
-
if (!autoPortRequested)
|
|
266
|
-
return false;
|
|
267
|
-
const err = error;
|
|
268
|
-
return Boolean(err && err.code === "EADDRINUSE");
|
|
269
|
-
};
|
|
270
270
|
let listenResult;
|
|
271
271
|
try {
|
|
272
272
|
listenResult = await attemptListen(primaryPort);
|
|
273
273
|
}
|
|
274
274
|
catch (error) {
|
|
275
|
-
if (!
|
|
275
|
+
if (!shouldRetryPreferredPort(error, autoPortRequested)) {
|
|
276
276
|
throw error;
|
|
277
277
|
}
|
|
278
278
|
deps.logger.warn({ err: error, port: primaryPort }, "Preferred port unavailable, retrying on ephemeral port");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
2
|
-
|
|
2
|
+
import { LOOPBACK_HOST } from "./loopback";
|
|
3
3
|
const LOOPBACK_TIMEOUT_MS = 10_000;
|
|
4
4
|
/**
|
|
5
5
|
* Creates an OpenCode SDK client for direct loopback communication with a
|
|
@@ -11,8 +11,8 @@ const LOOPBACK_TIMEOUT_MS = 10_000;
|
|
|
11
11
|
* OpenCode instance directly should use this factory rather than building
|
|
12
12
|
* `http://127.0.0.1:{port}/...` URLs by hand.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* near-instant; a hang indicates a stuck instance.
|
|
14
|
+
* Requests carry a 10-second timeout (configurable via `timeoutMs`) —
|
|
15
|
+
* loopback calls should be near-instant; a hang indicates a stuck instance.
|
|
16
16
|
*
|
|
17
17
|
* The client is cheap to create (object only, no connection); create one per
|
|
18
18
|
* call or cache per instance as needed. Returns `null` when the instance has
|
|
@@ -29,13 +29,14 @@ export function createInstanceClient(workspaceManager, instanceId, options = {})
|
|
|
29
29
|
}
|
|
30
30
|
const workspace = workspaceManager.get(instanceId);
|
|
31
31
|
const timeoutMs = options.timeoutMs ?? LOOPBACK_TIMEOUT_MS;
|
|
32
|
+
const directory = options.directory ?? workspace?.path;
|
|
32
33
|
return createOpencodeClient({
|
|
33
|
-
baseUrl: `http://${
|
|
34
|
+
baseUrl: `http://${LOOPBACK_HOST}:${port}/`,
|
|
34
35
|
headers,
|
|
35
36
|
fetch: (url, init) => fetch(url, {
|
|
36
37
|
...init,
|
|
37
38
|
signal: init?.signal ?? AbortSignal.timeout(timeoutMs),
|
|
38
39
|
}),
|
|
39
|
-
...(
|
|
40
|
+
...(directory ? { directory } : {}),
|
|
40
41
|
});
|
|
41
42
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createInstanceClient } from "./instance-client";
|
|
4
|
+
function makeManager(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
getInstancePort: overrides.getInstancePort ?? (() => undefined),
|
|
7
|
+
getInstanceAuthorizationHeader: overrides.getInstanceAuthorizationHeader ?? (() => undefined),
|
|
8
|
+
get: overrides.get ?? (() => undefined),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Installs a global `fetch` stub that records every outgoing request and
|
|
13
|
+
* answers a minimal healthy JSON body. Returns the capture buffer and a
|
|
14
|
+
* restore function. The stub tolerates both `fetch(url, init)` and
|
|
15
|
+
* `fetch(Request)` invocation styles so it is independent of the SDK's
|
|
16
|
+
* internal call convention.
|
|
17
|
+
*/
|
|
18
|
+
function installRecordingFetch() {
|
|
19
|
+
const requests = [];
|
|
20
|
+
const original = globalThis.fetch;
|
|
21
|
+
globalThis.fetch = (async (input, init) => {
|
|
22
|
+
if (input instanceof Request) {
|
|
23
|
+
requests.push({ url: input.url, headers: new Headers(init?.headers ?? input.headers) });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
requests.push({ url: String(input), headers: new Headers(init?.headers) });
|
|
27
|
+
}
|
|
28
|
+
return new Response(JSON.stringify({ healthy: true }), {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: { "content-type": "application/json" },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return { requests, restore: () => { globalThis.fetch = original; } };
|
|
34
|
+
}
|
|
35
|
+
describe("createInstanceClient", () => {
|
|
36
|
+
it("returns null when the instance has no open port", () => {
|
|
37
|
+
const manager = makeManager({ getInstancePort: () => undefined });
|
|
38
|
+
assert.equal(createInstanceClient(manager, "ws-1"), null);
|
|
39
|
+
});
|
|
40
|
+
it("targets the loopback host and port on outgoing requests", async () => {
|
|
41
|
+
const { requests, restore } = installRecordingFetch();
|
|
42
|
+
try {
|
|
43
|
+
const manager = makeManager({ getInstancePort: () => 4321, get: () => ({ path: "/repo" }) });
|
|
44
|
+
const client = createInstanceClient(manager, "ws-1");
|
|
45
|
+
assert.ok(client, "expected a client when the instance has a port");
|
|
46
|
+
await client.global.health();
|
|
47
|
+
const parsed = new URL(requests[0].url);
|
|
48
|
+
assert.equal(parsed.hostname, "127.0.0.1");
|
|
49
|
+
assert.equal(parsed.port, "4321");
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
restore();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("attaches the authorization header when one is configured", async () => {
|
|
56
|
+
const { requests, restore } = installRecordingFetch();
|
|
57
|
+
try {
|
|
58
|
+
const manager = makeManager({
|
|
59
|
+
getInstancePort: () => 4321,
|
|
60
|
+
getInstanceAuthorizationHeader: () => "Basic abc",
|
|
61
|
+
get: () => ({ path: "/repo" }),
|
|
62
|
+
});
|
|
63
|
+
const client = createInstanceClient(manager, "ws-1");
|
|
64
|
+
await client.global.health();
|
|
65
|
+
assert.equal(requests[0].headers.get("authorization"), "Basic abc");
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
restore();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
it("omits the authorization header when none is configured", async () => {
|
|
72
|
+
const { requests, restore } = installRecordingFetch();
|
|
73
|
+
try {
|
|
74
|
+
const manager = makeManager({ getInstancePort: () => 4321, get: () => ({ path: "/repo" }) });
|
|
75
|
+
const client = createInstanceClient(manager, "ws-1");
|
|
76
|
+
await client.global.health();
|
|
77
|
+
assert.equal(requests[0].headers.get("authorization"), null);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
restore();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
it("scopes requests to the workspace directory", async () => {
|
|
84
|
+
const { requests, restore } = installRecordingFetch();
|
|
85
|
+
try {
|
|
86
|
+
const manager = makeManager({ getInstancePort: () => 4321, get: () => ({ path: "/repo" }) });
|
|
87
|
+
const client = createInstanceClient(manager, "ws-1");
|
|
88
|
+
await client.global.health();
|
|
89
|
+
// GET requests carry directory as a query parameter (see SDK rewrite).
|
|
90
|
+
assert.equal(new URL(requests[0].url).searchParams.get("directory"), "/repo");
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
restore();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
it("does not scope requests when the workspace has no path", async () => {
|
|
97
|
+
const { requests, restore } = installRecordingFetch();
|
|
98
|
+
try {
|
|
99
|
+
const manager = makeManager({ getInstancePort: () => 4321, get: () => undefined });
|
|
100
|
+
const client = createInstanceClient(manager, "ws-1");
|
|
101
|
+
await client.global.health();
|
|
102
|
+
assert.equal(new URL(requests[0].url).searchParams.get("directory"), null);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
restore();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it("honours an explicit directory override over the workspace root", async () => {
|
|
109
|
+
const { requests, restore } = installRecordingFetch();
|
|
110
|
+
try {
|
|
111
|
+
const manager = makeManager({ getInstancePort: () => 4321, get: () => ({ path: "/workspace-root" }) });
|
|
112
|
+
const client = createInstanceClient(manager, "ws-1", {
|
|
113
|
+
directory: "/explicit/session-dir",
|
|
114
|
+
});
|
|
115
|
+
await client.global.health();
|
|
116
|
+
assert.equal(new URL(requests[0].url).searchParams.get("directory"), "/explicit/session-dir");
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
restore();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
it("applies the loopback timeout and aborts a stuck instance", async () => {
|
|
123
|
+
const original = globalThis.fetch;
|
|
124
|
+
// Never resolves on its own; only settles when the passed signal aborts,
|
|
125
|
+
// mirroring how a real fetch honours an AbortSignal. Without the factory
|
|
126
|
+
// timeout this call would hang forever and time the test out.
|
|
127
|
+
globalThis.fetch = (async (_input, init) => {
|
|
128
|
+
return new Promise((_resolve, reject) => {
|
|
129
|
+
const signal = init?.signal;
|
|
130
|
+
if (!signal)
|
|
131
|
+
return;
|
|
132
|
+
if (signal.aborted)
|
|
133
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
134
|
+
else
|
|
135
|
+
signal.addEventListener("abort", () => reject(signal.reason ?? new Error("aborted")));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
const manager = makeManager({ getInstancePort: () => 4321, get: () => ({ path: "/repo" }) });
|
|
140
|
+
const client = createInstanceClient(manager, "ws-1", { timeoutMs: 10 });
|
|
141
|
+
// SDK methods resolve with { error } rather than throwing by default.
|
|
142
|
+
const result = await client.global.health();
|
|
143
|
+
assert.ok(result.error, "expected the stuck-instance call to surface an error");
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
globalThis.fetch = original;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetch } from "undici";
|
|
2
2
|
import { Agent as UndiciAgent } from "undici";
|
|
3
|
-
|
|
3
|
+
import { LOOPBACK_HOST } from "./loopback";
|
|
4
4
|
const STREAM_AGENT = new UndiciAgent({ bodyTimeout: 0, headersTimeout: 0 });
|
|
5
5
|
const RECONNECT_DELAY_MS = 1000;
|
|
6
6
|
export class InstanceEventBridge {
|
|
@@ -70,7 +70,7 @@ export class InstanceEventBridge {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
async consumeStream(workspaceId, port, signal) {
|
|
73
|
-
const url = `http://${
|
|
73
|
+
const url = `http://${LOOPBACK_HOST}:${port}/global/event`;
|
|
74
74
|
const headers = { Accept: "text/event-stream" };
|
|
75
75
|
const authHeader = this.options.workspaceManager.getInstanceAuthorizationHeader(workspaceId);
|
|
76
76
|
if (authHeader) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback host used for direct in-process communication with a running
|
|
3
|
+
* OpenCode workspace instance (the server and the instance share a machine).
|
|
4
|
+
*
|
|
5
|
+
* Shared by the workspace-instance loopback callers so they agree on the host
|
|
6
|
+
* instead of each hardcoding their own `127.0.0.1` literal.
|
|
7
|
+
*/
|
|
8
|
+
export const LOOPBACK_HOST = "127.0.0.1";
|
|
@@ -11,6 +11,7 @@ import { buildOpencodeConfigContent, getCodeNomadPluginUrl, resolveExistingOpenc
|
|
|
11
11
|
import { OPENCODE_SERVER_BASE_URL_ENV, buildOpencodeBasicAuthHeader, OPENCODE_SERVER_PASSWORD_ENV, OPENCODE_SERVER_USERNAME_ENV, resolveOpencodeServerAuth, } from "./opencode-auth";
|
|
12
12
|
import { resolveWorkspaceIdentity } from "./workspace-identity";
|
|
13
13
|
import { parseWslUncPath } from "./spawn";
|
|
14
|
+
import { LOOPBACK_HOST } from "./loopback";
|
|
14
15
|
const STARTUP_STABILITY_DELAY_MS = 1500;
|
|
15
16
|
const DEFAULT_LAUNCH_TIMEOUT_MS = 30_000;
|
|
16
17
|
const ORDINARY_CREATION_OWNER = "";
|
|
@@ -579,6 +580,10 @@ export class WorkspaceManager {
|
|
|
579
580
|
this.exitDuringStartup(params, "exited before becoming ready"),
|
|
580
581
|
]);
|
|
581
582
|
const version = await this.waitForInstanceHealth(params);
|
|
583
|
+
await Promise.race([
|
|
584
|
+
this.validateInstanceConfiguration(params),
|
|
585
|
+
this.exitDuringStartup(params, "exited during configuration validation"),
|
|
586
|
+
]);
|
|
582
587
|
await Promise.race([
|
|
583
588
|
delay(STARTUP_STABILITY_DELAY_MS, undefined, { signal: params.signal }),
|
|
584
589
|
this.exitDuringStartup(params, "exited shortly after start"),
|
|
@@ -606,14 +611,9 @@ export class WorkspaceManager {
|
|
|
606
611
|
});
|
|
607
612
|
}
|
|
608
613
|
async probeInstance(workspaceId, port, signal) {
|
|
609
|
-
const url = `http
|
|
614
|
+
const url = `http://${LOOPBACK_HOST}:${port}/global/health`;
|
|
610
615
|
try {
|
|
611
|
-
const
|
|
612
|
-
const authHeader = this.opencodeAuth.get(workspaceId)?.authorization;
|
|
613
|
-
if (authHeader) {
|
|
614
|
-
headers["Authorization"] = authHeader;
|
|
615
|
-
}
|
|
616
|
-
const response = await fetch(url, { headers, signal });
|
|
616
|
+
const response = await fetch(url, { headers: this.getInstanceRequestHeaders(workspaceId), signal });
|
|
617
617
|
if (!response.ok) {
|
|
618
618
|
const reason = `/global/health returned HTTP ${response.status}`;
|
|
619
619
|
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error");
|
|
@@ -635,6 +635,22 @@ export class WorkspaceManager {
|
|
|
635
635
|
return { ok: false, reason };
|
|
636
636
|
}
|
|
637
637
|
}
|
|
638
|
+
async validateInstanceConfiguration(params) {
|
|
639
|
+
const response = await fetch(`http://${LOOPBACK_HOST}:${params.port}/config`, {
|
|
640
|
+
headers: this.getInstanceRequestHeaders(params.workspaceId),
|
|
641
|
+
signal: params.signal,
|
|
642
|
+
});
|
|
643
|
+
if (response.ok) {
|
|
644
|
+
await response.body?.cancel();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const body = (await response.text()).trim();
|
|
648
|
+
throw new Error(body || `OpenCode /config returned HTTP ${response.status}`);
|
|
649
|
+
}
|
|
650
|
+
getInstanceRequestHeaders(workspaceId) {
|
|
651
|
+
const authorization = this.opencodeAuth.get(workspaceId)?.authorization;
|
|
652
|
+
return authorization ? { Authorization: authorization } : {};
|
|
653
|
+
}
|
|
638
654
|
buildStartupError(workspaceId, phase, exitInfo, lastOutput) {
|
|
639
655
|
const exitDetails = this.describeExit(exitInfo);
|
|
640
656
|
const trimmedOutput = lastOutput.trim();
|
|
@@ -656,7 +672,7 @@ export class WorkspaceManager {
|
|
|
656
672
|
const tryConnect = () => {
|
|
657
673
|
if (settled)
|
|
658
674
|
return;
|
|
659
|
-
const socket = connect({ port, host:
|
|
675
|
+
const socket = connect({ port, host: LOOPBACK_HOST, signal }, () => {
|
|
660
676
|
cleanup();
|
|
661
677
|
socket.end();
|
|
662
678
|
resolve();
|
|
@@ -44,6 +44,7 @@ class ControlledRuntime {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function createHarness(options = {}) {
|
|
47
|
+
const { stubReadiness = true, ...managerOptions } = options;
|
|
47
48
|
const eventBus = new EventBus();
|
|
48
49
|
const runtime = new ControlledRuntime();
|
|
49
50
|
const readiness = deferred();
|
|
@@ -59,17 +60,20 @@ function createHarness(options = {}) {
|
|
|
59
60
|
logger: pino({ level: "silent" }),
|
|
60
61
|
getServerBaseUrl: () => "http://127.0.0.1:4000",
|
|
61
62
|
runtime,
|
|
62
|
-
...
|
|
63
|
+
...managerOptions,
|
|
63
64
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
cancel
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
if (stubReadiness) {
|
|
66
|
+
;
|
|
67
|
+
manager.waitForWorkspaceReadiness = ({ signal }) => Promise.race([
|
|
68
|
+
readiness.promise,
|
|
69
|
+
new Promise((_resolve, reject) => {
|
|
70
|
+
const cancel = () => reject(signal?.reason);
|
|
71
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
72
|
+
if (signal?.aborted)
|
|
73
|
+
cancel();
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
73
77
|
return { manager, runtime, readiness, started, stopped };
|
|
74
78
|
}
|
|
75
79
|
async function createReady(harness) {
|
|
@@ -81,6 +85,47 @@ async function createReady(harness) {
|
|
|
81
85
|
return workspaceId;
|
|
82
86
|
}
|
|
83
87
|
describe("workspace manager lifecycle", () => {
|
|
88
|
+
it("rejects a healthy workspace whose OpenCode configuration is invalid", async () => {
|
|
89
|
+
const originalFetch = globalThis.fetch;
|
|
90
|
+
const requests = [];
|
|
91
|
+
const configError = JSON.stringify({
|
|
92
|
+
name: "ConfigInvalidError",
|
|
93
|
+
data: {
|
|
94
|
+
path: "C:\\Users\\dev\\.config\\opencode\\agents\\invalid.md",
|
|
95
|
+
issues: [{ path: ["tools", "bash"], message: 'Expected boolean, got "ask"' }],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
globalThis.fetch = (async (input) => {
|
|
99
|
+
const url = String(input);
|
|
100
|
+
requests.push(url);
|
|
101
|
+
if (url.includes("/global/health")) {
|
|
102
|
+
return new Response(JSON.stringify({ healthy: true, version: "1.18.5" }), {
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return new Response(configError, { status: 400, headers: { "Content-Type": "application/json" } });
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
const harness = createHarness({ stubReadiness: false });
|
|
110
|
+
harness.manager.waitForPortAvailability = async () => undefined;
|
|
111
|
+
const creation = harness.manager.create(process.cwd());
|
|
112
|
+
const workspaceId = await harness.runtime.launchCalled.promise;
|
|
113
|
+
harness.runtime.resolveLaunch();
|
|
114
|
+
await assert.rejects(creation, (error) => {
|
|
115
|
+
assert.ok(error instanceof Error);
|
|
116
|
+
assert.equal(error.message, configError);
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
assert.deepEqual(requests.map((url) => new URL(url).pathname), ["/global/health", "/config"]);
|
|
120
|
+
assert.equal(new URL(requests[1]).search, "");
|
|
121
|
+
assert.equal(harness.runtime.active.has(workspaceId), false);
|
|
122
|
+
assert.deepEqual(harness.started, []);
|
|
123
|
+
assert.deepEqual(harness.manager.list(), []);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
globalThis.fetch = originalFetch;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
84
129
|
for (const boundary of ["launch", "readiness", "shutdown"]) {
|
|
85
130
|
it(`cancels and cleans a workspace during ${boundary}`, async () => {
|
|
86
131
|
const harness = createHarness();
|
package/package.json
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/monaco-viewer-CiFg18Ak.js","assets/git-diff-vendor-CYS3ApzA.js","assets/fast-diff-vendor-DgdwVvTQ.js","assets/highlight-vendor-8FKMu9os.js","assets/git-diff-vendor-HAZkIolJ.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{a8 as J,m,t as h,i as s,d as u,a as C,u as U,_ as X,f as Y}from"./monaco-viewer-CiFg18Ak.js";import{d as K,b as P,c as $,n as o,S as g,a as w,z as Z,A as p,F as ee}from"./git-diff-vendor-CYS3ApzA.js";import{S as te}from"./SplitFilePanel-DaqtU5jw.js";import{R as V,ay as le,M as ne,az as re,C as ae,e as ie,aA as se}from"./main-oNL0VKfF.js";import{W as oe}from"./wrap-text-CJPfwppv.js";import"./fast-diff-vendor-DgdwVvTQ.js";import"./highlight-vendor-8FKMu9os.js";import"./index-DMYv1E9L.js";var ce=h('<div class="px-2 py-2 border-b border-base"><div class=selector-input-group><div class="flex items-center gap-2 px-3 text-muted"></div><input type=text class=selector-input>'),de=h("<div class=file-list-header><span class=file-list-title></span><span class=file-list-count>"),O=h('<div class="p-3 text-xs text-secondary">'),he=h("<div class=file-list-item><div class=file-list-item-content><div class=file-list-item-path><span class=file-path-text>.."),ue=h('<div class="p-3 text-xs text-error">'),ve=h('<div><div class=file-list-item-content><div class=file-list-item-path><span class=file-path-text></span></div><div class="flex items-center gap-2 shrink-0"><div class=file-list-item-stats><span class="text-[10px] text-secondary"></span></div><button type=button class=git-change-row-action>'),x=h("<div class=file-viewer-empty><span class=file-viewer-empty-text>"),fe=h('<div class="file-viewer-panel flex-1"><div>'),ge=h('<div class="h-full outline-none"tabindex=0>'),we=h("<span>"),be=h("<div class=files-tab-stats><span class=files-tab-stat><span class=files-tab-selected-path><span class=file-path-text>"),Se=h("<button type=button style=margin-inline-start:auto>"),me=h("<button type=button>"),q=h("<button type=button class=files-header-icon-button>"),$e=h("<span class=text-error>");const ye=p(()=>X(()=>import("./monaco-viewer-CiFg18Ak.js").then(e=>e.at),__vite__mapDeps([0,1,2,3,4])).then(e=>({default:e.MonacoFileViewer})));function _e(e){return e?/\.(md|markdown|mdown|mkdn)$/i.test(e):!1}const Te=e=>{const[E,L]=K(""),{isDark:Q}=J(),[N,M]=K(!1);let b;P(()=>{e.browserPath(),L("")});const H=$(()=>[...e.browserEntries()||[]].sort((i,d)=>{const l=i.type==="directory"?0:1,t=d.type==="directory"?0:1;return l!==t?l-t:String(i.name||"").localeCompare(String(d.name||""))})),W=$(()=>E().trim().toLowerCase()),k=$(()=>{const n=W(),i=H();return n?i.filter(d=>String(d.name||"").toLowerCase().includes(n)):i}),y=()=>e.browserLoading()&&e.browserEntries()===null,j=()=>W()?e.t("instanceShell.filesShell.search.empty"):e.t("instanceShell.filesShell.listEmpty"),_=$(()=>_e(e.browserSelectedPath())),S=$(()=>_()&&N());P(()=>{_()||M(!1)});const D=()=>{const n=e.browserSelectedContent();n!=null&&e.onSave(n)},B=async(n,i)=>{i==null||i.stopPropagation();const d=await ie(n);se({message:d?e.t("instanceShell.filesShell.toast.copyPathSuccess"):e.t("instanceShell.filesShell.toast.copyPathError"),variant:d?"success":"error"})};P(()=>{S()&&requestAnimationFrame(()=>b==null?void 0:b.focus())});const T=()=>[(()=>{var n=ce(),i=n.firstChild,d=i.firstChild,l=d.nextSibling;return s(d,o(re,{class:"w-4 h-4"})),l.$$input=t=>L(t.currentTarget.value),w(t=>{var a=e.t("instanceShell.filesShell.search.placeholder"),r=e.t("instanceShell.filesShell.search.ariaLabel");return a!==t.e&&u(l,"placeholder",t.e=a),r!==t.t&&u(l,"aria-label",t.t=r),t},{e:void 0,t:void 0}),w(()=>l.value=E()),n})(),(()=>{var n=de(),i=n.firstChild,d=i.nextSibling;return s(i,()=>e.t("instanceShell.filesShell.fileListTitle")),s(d,()=>k().length),n})(),o(g,{get when(){return e.parentPath()},children:n=>(()=>{var i=he(),d=i.firstChild,l=d.firstChild;return i.$$click=()=>e.onLoadEntries(n()),w(()=>u(l,"title",n())),i})()}),o(g,{get when(){return y()},get children(){var n=O();return s(n,()=>e.t("instanceInfo.loading")),n}}),o(g,{get when(){return m(()=>!e.browserError()&&!y())()&&k().length>0},get fallback(){return m(()=>!y())()?m(()=>!!e.browserError())()?(()=>{var n=ue();return s(n,()=>e.browserError()),n})():(()=>{var n=O();return s(n,j),n})():void 0},get children(){return o(ee,{get each(){return k()},children:n=>(()=>{var i=ve(),d=i.firstChild,l=d.firstChild,t=l.firstChild,a=l.nextSibling,r=a.firstChild,c=r.firstChild,f=r.nextSibling;return i.$$click=()=>{if(n.type==="directory"){e.onLoadEntries(n.path);return}e.onRequestOpenFile(n.path)},s(t,()=>n.name),s(c,()=>n.type),f.$$click=v=>void B(n.path,v),s(f,o(ae,{class:"w-3 h-3"})),w(v=>{var F=`file-list-item ${e.browserSelectedPath()===n.path?"file-list-item-active":""}`,z=n.path,R=n.path,A=e.t("instanceShell.filesShell.actions.copyPath"),I=e.t("instanceShell.filesShell.actions.copyPath");return F!==v.e&&C(i,v.e=F),z!==v.t&&u(i,"title",v.t=z),R!==v.a&&u(l,"title",v.a=R),A!==v.o&&u(f,"title",v.o=A),I!==v.i&&u(f,"aria-label",v.i=I),v},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),i})()})}})],G=n=>{!(n.ctrlKey||n.metaKey)||n.key.toLowerCase()!=="s"||e.browserSelectedSaving()||!e.browserSelectedDirty()||(n.preventDefault(),D())};return m(()=>{const n=()=>e.browserSelectedPath()||e.browserPath(),i=()=>y()?e.t("instanceInfo.loading"):e.t("instanceShell.filesShell.viewerEmpty"),d=()=>(()=>{var l=fe(),t=l.firstChild;return s(t,o(g,{get when(){return e.browserSelectedLoading()},get fallback(){return o(g,{get when(){return e.browserSelectedError()},get fallback(){return o(g,{get when(){return m(()=>!!(e.browserSelectedPath()&&e.browserSelectedContent()!==null))()?{path:e.browserSelectedPath(),content:e.browserSelectedContent()}:null},get fallback(){return(()=>{var a=x(),r=a.firstChild;return s(r,i),a})()},children:a=>o(g,{get when(){return S()},get fallback(){return o(Z,{get fallback(){return(()=>{var r=x(),c=r.firstChild;return s(c,()=>e.t("instanceInfo.loading")),r})()},get children(){return o(ye,{get scopeKey(){return e.scopeKey()},get path(){return a().path},get content(){return a().content},get wordWrap(){return e.wordWrapMode()},get onSave(){return e.onSave},get onContentChange(){return e.onContentChange}})}})},get children(){var r=ge();r.$$mousedown=()=>b==null?void 0:b.focus(),r.$$keydown=G;var c=b;return typeof c=="function"?U(c,r):b=r,s(r,o(ne,{get part(){return{type:"text",text:a().content}},get isDark(){return Q()},escapeRawHtml:!0})),r}})})},children:a=>(()=>{var r=x(),c=r.firstChild;return s(c,a),r})()})},get children(){var a=x(),r=a.firstChild;return s(r,()=>e.t("instanceInfo.loading")),a}})),w(()=>C(t,S()?"file-viewer-content":"file-viewer-content file-viewer-content--monaco")),l})();return o(te,{get header(){return[(()=>{var l=be(),t=l.firstChild,a=t.firstChild,r=a.firstChild;return s(r,n),s(l,o(g,{get when(){return e.browserLoading()},get children(){var c=we();return s(c,()=>e.t("instanceInfo.loading")),c}}),null),s(l,o(g,{get when(){return e.browserError()},children:c=>(()=>{var f=$e();return s(f,c),f})()}),null),w(()=>u(a,"title",n())),l})(),(()=>{var l=Se();return l.$$click=()=>_()&&M(t=>!t),s(l,(()=>{var t=m(()=>!!S());return()=>t()?e.t("instanceShell.filesShell.showSource"):e.t("instanceShell.filesShell.previewMarkdown")})()),w(t=>{var a=`file-viewer-toolbar-button${S()?" active":""}`,r=!_();return a!==t.e&&C(l,t.e=a),r!==t.t&&(l.disabled=t.t=r),t},{e:void 0,t:void 0}),l})(),(()=>{var l=me();return l.$$click=()=>e.onWordWrapModeChange(e.wordWrapMode()==="on"?"off":"on"),s(l,o(oe,{class:"h-4 w-4"})),w(t=>{var a=`file-viewer-toolbar-icon-button${e.wordWrapMode()==="on"?" active":""}`,r=e.wordWrapMode()==="on"?e.t("instanceShell.filesShell.disableWordWrap"):e.t("instanceShell.filesShell.enableWordWrap"),c=e.wordWrapMode()==="on"?e.t("instanceShell.filesShell.disableWordWrap"):e.t("instanceShell.filesShell.enableWordWrap"),f=S();return a!==t.e&&C(l,t.e=a),r!==t.t&&u(l,"title",t.t=r),c!==t.a&&u(l,"aria-label",t.a=c),f!==t.o&&(l.disabled=t.o=f),t},{e:void 0,t:void 0,a:void 0,o:void 0}),l})(),(()=>{var l=q();return l.$$click=D,s(l,o(g,{get when(){return e.browserSelectedSaving()},get fallback(){return o(le,{class:"h-4 w-4"})},get children(){return o(V,{class:"h-4 w-4 animate-spin"})}})),w(t=>{var a=e.t("instanceShell.rightPanel.actions.save")||"Save (Ctrl+S)",r=e.t("instanceShell.rightPanel.actions.save")||"Save",c=e.browserSelectedSaving()||!e.browserSelectedDirty();return a!==t.e&&u(l,"title",t.e=a),r!==t.t&&u(l,"aria-label",t.t=r),c!==t.a&&(l.disabled=t.a=c),t},{e:void 0,t:void 0,a:void 0}),l})(),(()=>{var l=q();return l.$$click=()=>e.onRefresh(),s(l,o(V,{get class(){return`h-4 w-4${e.browserLoading()?" animate-spin":""}`}})),w(t=>{var a=e.t("instanceShell.rightPanel.actions.refresh"),r=e.t("instanceShell.rightPanel.actions.refresh"),c=e.browserLoading();return a!==t.e&&u(l,"title",t.e=a),r!==t.t&&u(l,"aria-label",t.t=r),c!==t.a&&(l.disabled=t.a=c),t},{e:void 0,t:void 0,a:void 0}),l})()]},list:{panel:()=>o(T,{}),overlay:()=>o(T,{})},get viewer(){return d()},get listOpen(){return e.listOpen()},get onToggleList(){return e.onToggleList},get splitWidth(){return e.splitWidth()},get onResizeMouseDown(){return e.onResizeMouseDown},get onResizeTouchStart(){return e.onResizeTouchStart},get isPhoneLayout(){return e.isPhoneLayout()},get overlayAriaLabel(){return e.t("instanceShell.rightPanel.tabs.files")}})})};Y(["input","click","keydown","mousedown"]);export{Te as default};
|