@neuralnomads/codenomad-dev 0.18.0-dev-20260712-c8d4a509 → 0.18.0-dev-20260713-f34e8071
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/server/routes/workspaces.js +3 -2
- package/dist/workspaces/__tests__/workspace-identity.test.js +203 -0
- package/dist/workspaces/manager.js +164 -42
- package/dist/workspaces/workspace-identity.js +27 -0
- package/package.json +1 -1
- package/public/assets/{FilesTab-DWE08QeL.js → FilesTab-CDX_msaa.js} +2 -2
- package/public/assets/{GitChangesTab-B0YpYXig.js → GitChangesTab-rcYbXriQ.js} +2 -2
- package/public/assets/{SplitFilePanel-r8xyMSwo.js → SplitFilePanel-BC46Ypnw.js} +1 -1
- package/public/assets/{StatusTab-CiF7bc-C.js → StatusTab-BdKL8EFK.js} +1 -1
- package/public/assets/{align-justify-D7GxsbXt.js → align-justify-uf_OdRBz.js} +1 -1
- package/public/assets/{bundle-full-CaqU_-Bu.js → bundle-full-BNEJFEWn.js} +1 -1
- package/public/assets/{diff-viewer-BemESP1g.js → diff-viewer-5SSUPiOf.js} +1 -1
- package/public/assets/{index-CdOQKtdG.js → index-BCWrXgFI.js} +1 -1
- package/public/assets/{index-B5fSJ4Sy.js → index-BFLK8Dqd.js} +2 -2
- package/public/assets/{index-DqvdokW8.js → index-BXWJJQEd.js} +1 -1
- package/public/assets/{index-CrwkCZNr.js → index-Bg-mj-8s.js} +1 -1
- package/public/assets/{index-CuOhJGR_.js → index-C0n-CjpZ.js} +1 -1
- package/public/assets/{index-CqPbeD4l.js → index-CJOjWiX-.js} +1 -1
- package/public/assets/{index-CNR6iBgw.js → index-Ctv_UxAd.js} +1 -1
- package/public/assets/{index-BIJEJ4fH.js → index-D5ajnHwk.js} +1 -1
- package/public/assets/{index-Hbpg8uhJ.js → index-DLDznEeH.js} +1 -1
- package/public/assets/{index-DosiZsbF.js → index-Djl8_LHh.js} +1 -1
- package/public/assets/{index-BzdwA-jE.js → index-nLFYZCXo.js} +1 -1
- package/public/assets/{loading-VAuQsLpy.js → loading-BsojmlCC.js} +1 -1
- package/public/assets/{main-BcrMO00Y.js → main-BiMrK3qQ.js} +16 -16
- package/public/assets/{markdown-DVTO-nx2.js → markdown-CXI6_8-r.js} +3 -3
- package/public/assets/{monaco-viewer-B3Jl6a5p.js → monaco-viewer-BJ8dD6do.js} +1 -1
- package/public/assets/{tool-call-BtNDaNja.js → tool-call-DiUpuzuW.js} +3 -3
- package/public/assets/{unified-picker-CHfDVhMa.js → unified-picker-DWR5mJDE.js} +1 -1
- package/public/assets/{wrap-text-SJDACYWw.js → wrap-text-Bb9bhsWo.js} +1 -1
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/sw.js +1 -1
|
@@ -7,6 +7,7 @@ import { resolveWorktreeDirectory } from "../../workspaces/worktree-directory";
|
|
|
7
7
|
const WorkspaceCreateSchema = z.object({
|
|
8
8
|
path: z.string(),
|
|
9
9
|
name: z.string().optional(),
|
|
10
|
+
forceNew: z.boolean().optional(),
|
|
10
11
|
});
|
|
11
12
|
const WorkspaceCloneSchema = z.object({
|
|
12
13
|
repositoryUrl: z.string().trim().min(1, "Repository URL is required"),
|
|
@@ -51,9 +52,9 @@ export function registerWorkspaceRoutes(app, deps) {
|
|
|
51
52
|
app.post("/api/workspaces", async (request, reply) => {
|
|
52
53
|
try {
|
|
53
54
|
const body = WorkspaceCreateSchema.parse(request.body ?? {});
|
|
54
|
-
const
|
|
55
|
+
const result = await deps.workspaceManager.create(body.path, body.name, { forceNew: body.forceNew });
|
|
55
56
|
reply.code(201);
|
|
56
|
-
return workspace;
|
|
57
|
+
return result.created ? result.workspace : { ...result.workspace, reused: true };
|
|
57
58
|
}
|
|
58
59
|
catch (error) {
|
|
59
60
|
request.log.error({ err: error }, "Failed to create workspace");
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
|
+
import pino from "pino";
|
|
7
|
+
import { EventBus } from "../../events/bus";
|
|
8
|
+
import { WorkspaceManager } from "../manager";
|
|
9
|
+
import { normalizeWorkspaceIdentityPath, resolveWorkspaceIdentity } from "../workspace-identity";
|
|
10
|
+
const temporaryDirectories = [];
|
|
11
|
+
function deferred() {
|
|
12
|
+
let resolve;
|
|
13
|
+
let reject;
|
|
14
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
15
|
+
resolve = resolvePromise;
|
|
16
|
+
reject = rejectPromise;
|
|
17
|
+
});
|
|
18
|
+
return { promise, resolve, reject };
|
|
19
|
+
}
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
22
|
+
});
|
|
23
|
+
async function createLinkedWorkspace() {
|
|
24
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "codenomad-workspace-identity-"));
|
|
25
|
+
temporaryDirectories.push(root);
|
|
26
|
+
const target = path.join(root, "target");
|
|
27
|
+
const link = path.join(root, "link");
|
|
28
|
+
await mkdir(target);
|
|
29
|
+
await symlink(target, link, process.platform === "win32" ? "junction" : "dir");
|
|
30
|
+
return { root, target, link };
|
|
31
|
+
}
|
|
32
|
+
function createManager(rootDir) {
|
|
33
|
+
const logger = pino({ level: "silent" });
|
|
34
|
+
const manager = new WorkspaceManager({
|
|
35
|
+
rootDir,
|
|
36
|
+
settings: { getOwner: () => ({ environmentVariables: {} }) },
|
|
37
|
+
binaryResolver: {
|
|
38
|
+
resolveDefault: () => ({ path: process.execPath, label: "Node.js", version: process.version }),
|
|
39
|
+
},
|
|
40
|
+
eventBus: new EventBus(logger),
|
|
41
|
+
logger,
|
|
42
|
+
getServerBaseUrl: () => "http://127.0.0.1:3000",
|
|
43
|
+
});
|
|
44
|
+
const internal = manager;
|
|
45
|
+
internal.runtime.launch = async () => ({
|
|
46
|
+
pid: 123,
|
|
47
|
+
port: 4321,
|
|
48
|
+
exitPromise: new Promise(() => { }),
|
|
49
|
+
getLastOutput: () => "",
|
|
50
|
+
});
|
|
51
|
+
internal.waitForWorkspaceReadiness = async () => undefined;
|
|
52
|
+
return manager;
|
|
53
|
+
}
|
|
54
|
+
describe("workspace identity", () => {
|
|
55
|
+
it("normalizes Windows drive and UNC paths without affecting POSIX case", () => {
|
|
56
|
+
assert.equal(normalizeWorkspaceIdentityPath("C:\\Projects\\CodeNomad\\", "win32"), "c:\\projects\\codenomad\\");
|
|
57
|
+
assert.equal(normalizeWorkspaceIdentityPath(String.raw `\\Server\Share\Repo`, "win32"), String.raw `\\server\share\repo`);
|
|
58
|
+
assert.equal(normalizeWorkspaceIdentityPath("/Projects/CodeNomad/", "linux"), "/Projects/CodeNomad/");
|
|
59
|
+
});
|
|
60
|
+
it("resolves a symlink and its target to the same canonical launch path", async () => {
|
|
61
|
+
const { root, target, link } = await createLinkedWorkspace();
|
|
62
|
+
const targetResult = await resolveWorkspaceIdentity(target, root);
|
|
63
|
+
const linkResult = await resolveWorkspaceIdentity(link, root);
|
|
64
|
+
assert.equal(linkResult.identityKey, targetResult.identityKey);
|
|
65
|
+
assert.equal(linkResult.workspacePath, targetResult.workspacePath);
|
|
66
|
+
assert.notEqual(linkResult.workspacePath, path.normalize(link));
|
|
67
|
+
});
|
|
68
|
+
it("falls back to a normalized absolute identity when realpath fails", async () => {
|
|
69
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "codenomad-workspace-missing-"));
|
|
70
|
+
temporaryDirectories.push(root);
|
|
71
|
+
const result = await resolveWorkspaceIdentity("missing", root);
|
|
72
|
+
const expected = path.resolve(root, "missing");
|
|
73
|
+
assert.equal(result.workspacePath, expected);
|
|
74
|
+
assert.equal(result.identityKey, normalizeWorkspaceIdentityPath(expected));
|
|
75
|
+
});
|
|
76
|
+
it("atomically reuses an active workspace reached through a symlink", async () => {
|
|
77
|
+
const { root, target, link } = await createLinkedWorkspace();
|
|
78
|
+
const manager = createManager(root);
|
|
79
|
+
const [targetResult, linkResult] = await Promise.all([manager.create(target), manager.create(link)]);
|
|
80
|
+
assert.equal(Number(targetResult.created) + Number(linkResult.created), 1);
|
|
81
|
+
assert.equal(targetResult.workspace.id, linkResult.workspace.id);
|
|
82
|
+
assert.equal(manager.list().length, 1);
|
|
83
|
+
});
|
|
84
|
+
it("shares an in-flight startup between canonical aliases", async () => {
|
|
85
|
+
const { root, target, link } = await createLinkedWorkspace();
|
|
86
|
+
const manager = createManager(root);
|
|
87
|
+
let launches = 0;
|
|
88
|
+
manager.runtime.launch = async () => {
|
|
89
|
+
launches += 1;
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
91
|
+
return {
|
|
92
|
+
pid: 123,
|
|
93
|
+
port: 4321,
|
|
94
|
+
exitPromise: new Promise(() => { }),
|
|
95
|
+
getLastOutput: () => "",
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const [first, second] = await Promise.all([manager.create(target), manager.create(link)]);
|
|
99
|
+
assert.equal(launches, 1);
|
|
100
|
+
assert.equal(Number(first.created) + Number(second.created), 1);
|
|
101
|
+
assert.equal(first.workspace.id, second.workspace.id);
|
|
102
|
+
assert.equal(first.workspace.status, "ready");
|
|
103
|
+
assert.equal(second.workspace.status, "ready");
|
|
104
|
+
});
|
|
105
|
+
it("releases a failed identity reservation so creation can be retried", async () => {
|
|
106
|
+
const { root, target, link } = await createLinkedWorkspace();
|
|
107
|
+
const manager = createManager(root);
|
|
108
|
+
let launches = 0;
|
|
109
|
+
manager.runtime.launch = async () => {
|
|
110
|
+
launches += 1;
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
112
|
+
throw new Error("launch failed");
|
|
113
|
+
};
|
|
114
|
+
const failed = await Promise.allSettled([manager.create(target), manager.create(link)]);
|
|
115
|
+
assert.deepEqual(failed.map((result) => result.status), ["rejected", "rejected"]);
|
|
116
|
+
assert.equal(launches, 1);
|
|
117
|
+
manager.runtime.launch = async () => ({
|
|
118
|
+
pid: 456,
|
|
119
|
+
port: 5432,
|
|
120
|
+
exitPromise: new Promise(() => { }),
|
|
121
|
+
getLastOutput: () => "",
|
|
122
|
+
});
|
|
123
|
+
const retry = await manager.create(target);
|
|
124
|
+
assert.equal(retry.created, true);
|
|
125
|
+
assert.equal(retry.workspace.status, "ready");
|
|
126
|
+
});
|
|
127
|
+
it("allows an explicit second workspace for the same canonical path", async () => {
|
|
128
|
+
const { root, target, link } = await createLinkedWorkspace();
|
|
129
|
+
const manager = createManager(root);
|
|
130
|
+
const [first, second] = await Promise.all([
|
|
131
|
+
manager.create(target, undefined, { forceNew: true }),
|
|
132
|
+
manager.create(link, undefined, { forceNew: true }),
|
|
133
|
+
]);
|
|
134
|
+
assert.equal(first.created, true);
|
|
135
|
+
assert.equal(second.created, true);
|
|
136
|
+
assert.notEqual(first.workspace.id, second.workspace.id);
|
|
137
|
+
assert.equal(manager.list().length, 2);
|
|
138
|
+
});
|
|
139
|
+
it("keeps the canonical reservation when a forced duplicate is deleted", async () => {
|
|
140
|
+
const { root, target, link } = await createLinkedWorkspace();
|
|
141
|
+
const manager = createManager(root);
|
|
142
|
+
const normalLaunch = deferred();
|
|
143
|
+
const forcedLaunch = deferred();
|
|
144
|
+
let launches = 0;
|
|
145
|
+
manager.runtime.launch = () => {
|
|
146
|
+
launches += 1;
|
|
147
|
+
return launches === 1 ? normalLaunch.promise : forcedLaunch.promise;
|
|
148
|
+
};
|
|
149
|
+
const first = manager.create(target);
|
|
150
|
+
while (manager.list().length < 1) {
|
|
151
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
152
|
+
}
|
|
153
|
+
const forced = manager.create(link, undefined, { forceNew: true });
|
|
154
|
+
const forcedRejected = assert.rejects(forced, /Workspace creation cancelled/);
|
|
155
|
+
while (manager.list().length < 2) {
|
|
156
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
157
|
+
}
|
|
158
|
+
const forcedWorkspace = manager.list().find((workspace) => workspace.id !== manager.list()[0].id);
|
|
159
|
+
await manager.delete(forcedWorkspace.id);
|
|
160
|
+
const reused = manager.create(link);
|
|
161
|
+
normalLaunch.resolve({ pid: 123, port: 4321, exitPromise: new Promise(() => { }), getLastOutput: () => "" });
|
|
162
|
+
forcedLaunch.resolve({ pid: 456, port: 5432, exitPromise: new Promise(() => { }), getLastOutput: () => "" });
|
|
163
|
+
const [firstResult, reusedResult] = await Promise.all([first, reused]);
|
|
164
|
+
await forcedRejected;
|
|
165
|
+
assert.equal(launches, 2);
|
|
166
|
+
assert.equal(firstResult.workspace.id, reusedResult.workspace.id);
|
|
167
|
+
assert.equal(reusedResult.created, false);
|
|
168
|
+
});
|
|
169
|
+
it("cancels an in-flight startup when its workspace is deleted", async () => {
|
|
170
|
+
const { root, target } = await createLinkedWorkspace();
|
|
171
|
+
const manager = createManager(root);
|
|
172
|
+
const launch = deferred();
|
|
173
|
+
manager.runtime.launch = () => launch.promise;
|
|
174
|
+
const events = [];
|
|
175
|
+
manager.options.eventBus.onEvent((event) => events.push(event));
|
|
176
|
+
const creation = manager.create(target);
|
|
177
|
+
const creationRejected = assert.rejects(creation, /Workspace creation cancelled/);
|
|
178
|
+
while (manager.list().length === 0) {
|
|
179
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
180
|
+
}
|
|
181
|
+
const workspaceId = manager.list()[0].id;
|
|
182
|
+
await manager.delete(workspaceId);
|
|
183
|
+
launch.resolve({ pid: 123, port: 4321, exitPromise: new Promise(() => { }), getLastOutput: () => "" });
|
|
184
|
+
await creationRejected;
|
|
185
|
+
assert.equal(manager.get(workspaceId), undefined);
|
|
186
|
+
assert.equal(events.filter((event) => event.type === "workspace.stopped" && event.workspaceId === workspaceId).length, 1);
|
|
187
|
+
});
|
|
188
|
+
it("waits for and cancels forced creations during shutdown", async () => {
|
|
189
|
+
const { root, target } = await createLinkedWorkspace();
|
|
190
|
+
const manager = createManager(root);
|
|
191
|
+
const launch = deferred();
|
|
192
|
+
manager.runtime.launch = () => launch.promise;
|
|
193
|
+
const creation = manager.create(target, undefined, { forceNew: true });
|
|
194
|
+
const creationRejected = assert.rejects(creation, /Workspace creation cancelled/);
|
|
195
|
+
while (manager.list().length === 0) {
|
|
196
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
197
|
+
}
|
|
198
|
+
const shutdown = manager.shutdown();
|
|
199
|
+
launch.resolve({ pid: 123, port: 4321, exitPromise: new Promise(() => { }), getLastOutput: () => "" });
|
|
200
|
+
await Promise.all([creationRejected, shutdown]);
|
|
201
|
+
assert.equal(manager.list().length, 0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { spawnSync } from "child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { connect } from "net";
|
|
4
5
|
import { FileSystemBrowser } from "../filesystem/browser";
|
|
5
6
|
import { searchWorkspaceFiles } from "../filesystem/search";
|
|
@@ -7,11 +8,18 @@ import { clearWorkspaceSearchCache } from "../filesystem/search-cache";
|
|
|
7
8
|
import { WorkspaceRuntime } from "./runtime";
|
|
8
9
|
import { buildOpencodeConfigContent, getCodeNomadPluginUrl, resolveExistingOpencodeConfigContent, } from "../opencode-plugin.js";
|
|
9
10
|
import { OPENCODE_SERVER_BASE_URL_ENV, buildOpencodeBasicAuthHeader, OPENCODE_SERVER_PASSWORD_ENV, OPENCODE_SERVER_USERNAME_ENV, resolveOpencodeServerAuth, } from "./opencode-auth";
|
|
11
|
+
import { resolveWorkspaceIdentity } from "./workspace-identity";
|
|
10
12
|
const STARTUP_STABILITY_DELAY_MS = 1500;
|
|
11
13
|
export class WorkspaceManager {
|
|
12
14
|
constructor(options) {
|
|
13
15
|
this.options = options;
|
|
14
16
|
this.workspaces = new Map();
|
|
17
|
+
this.workspaceIdentities = new Map();
|
|
18
|
+
this.pendingWorkspaceCreations = new Map();
|
|
19
|
+
this.pendingWorkspaceOwners = new Map();
|
|
20
|
+
this.activeWorkspaceCreations = new Set();
|
|
21
|
+
this.cancelledWorkspaceCreations = new Set();
|
|
22
|
+
this.shuttingDown = false;
|
|
15
23
|
this.opencodeAuth = new Map();
|
|
16
24
|
this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger);
|
|
17
25
|
this.codeNomadPluginUrl = getCodeNomadPluginUrl();
|
|
@@ -28,6 +36,11 @@ export class WorkspaceManager {
|
|
|
28
36
|
getInstanceAuthorizationHeader(id) {
|
|
29
37
|
return this.opencodeAuth.get(id)?.authorization;
|
|
30
38
|
}
|
|
39
|
+
findReadyWorkspaceByIdentity(identityKey) {
|
|
40
|
+
return Array.from(this.workspaces.values()).find((workspace) => {
|
|
41
|
+
return workspace.status === "ready" && this.workspaceIdentities.get(workspace.id) === identityKey;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
31
44
|
listFiles(workspaceId, relativePath = ".") {
|
|
32
45
|
const workspace = this.requireWorkspace(workspaceId);
|
|
33
46
|
const browser = new FileSystemBrowser({ rootDir: workspace.path });
|
|
@@ -71,11 +84,55 @@ export class WorkspaceManager {
|
|
|
71
84
|
const browser = new FileSystemBrowser({ rootDir: directory });
|
|
72
85
|
browser.writeFile(relativePath, contents);
|
|
73
86
|
}
|
|
74
|
-
async create(folder, name) {
|
|
75
|
-
const
|
|
87
|
+
async create(folder, name, options) {
|
|
88
|
+
const { workspacePath, identityKey } = await resolveWorkspaceIdentity(folder, this.options.rootDir);
|
|
89
|
+
if (this.shuttingDown) {
|
|
90
|
+
throw new Error("Workspace manager is shutting down");
|
|
91
|
+
}
|
|
92
|
+
if (options?.forceNew) {
|
|
93
|
+
return this.trackWorkspaceCreation(this.createResolvedWorkspace(workspacePath, identityKey, name));
|
|
94
|
+
}
|
|
95
|
+
const existing = this.findReadyWorkspaceByIdentity(identityKey);
|
|
96
|
+
if (existing) {
|
|
97
|
+
this.options.logger.info({ workspaceId: existing.id, folder: workspacePath }, "Reusing existing workspace");
|
|
98
|
+
return { workspace: existing, created: false };
|
|
99
|
+
}
|
|
100
|
+
const pending = this.pendingWorkspaceCreations.get(identityKey);
|
|
101
|
+
if (pending) {
|
|
102
|
+
const result = await pending;
|
|
103
|
+
return { workspace: result.workspace, created: false };
|
|
104
|
+
}
|
|
105
|
+
const creation = this.trackWorkspaceCreation(this.createResolvedWorkspace(workspacePath, identityKey, name, (workspaceId) => {
|
|
106
|
+
this.pendingWorkspaceOwners.set(identityKey, workspaceId);
|
|
107
|
+
}));
|
|
108
|
+
this.pendingWorkspaceCreations.set(identityKey, creation);
|
|
109
|
+
try {
|
|
110
|
+
return await creation;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
if (this.pendingWorkspaceCreations.get(identityKey) === creation) {
|
|
114
|
+
this.pendingWorkspaceCreations.delete(identityKey);
|
|
115
|
+
this.pendingWorkspaceOwners.delete(identityKey);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
trackWorkspaceCreation(creation) {
|
|
120
|
+
let tracked;
|
|
121
|
+
tracked = (async () => {
|
|
122
|
+
try {
|
|
123
|
+
return await creation;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
this.activeWorkspaceCreations.delete(tracked);
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
this.activeWorkspaceCreations.add(tracked);
|
|
130
|
+
return tracked;
|
|
131
|
+
}
|
|
132
|
+
async createResolvedWorkspace(workspacePath, identityKey, name, onReserved) {
|
|
133
|
+
const id = randomUUID();
|
|
76
134
|
const binary = this.options.binaryResolver.resolveDefault();
|
|
77
135
|
const resolvedBinaryPath = this.resolveBinaryPath(binary.path);
|
|
78
|
-
const workspacePath = path.isAbsolute(folder) ? folder : path.resolve(this.options.rootDir, folder);
|
|
79
136
|
clearWorkspaceSearchCache(workspacePath);
|
|
80
137
|
this.options.logger.info({ workspaceId: id, folder: workspacePath, binary: resolvedBinaryPath }, "Creating workspace");
|
|
81
138
|
const proxyPath = `/workspaces/${id}/instance`;
|
|
@@ -92,35 +149,38 @@ export class WorkspaceManager {
|
|
|
92
149
|
updatedAt: new Date().toISOString(),
|
|
93
150
|
};
|
|
94
151
|
this.workspaces.set(id, descriptor);
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
const envVars = serverConfig?.environmentVariables;
|
|
98
|
-
const userEnvironment = envVars && typeof envVars === "object" && !Array.isArray(envVars) ? envVars : {};
|
|
99
|
-
const opencodeConfigContent = buildOpencodeConfigContent(resolveExistingOpencodeConfigContent(userEnvironment), this.codeNomadPluginUrl);
|
|
100
|
-
const serverBaseUrl = this.options.getServerBaseUrl();
|
|
101
|
-
const normalizedServerBaseUrl = serverBaseUrl.replace(/\/+$/, "");
|
|
102
|
-
const { username: opencodeUsername, password: opencodePassword } = resolveOpencodeServerAuth({
|
|
103
|
-
userEnvironment,
|
|
104
|
-
processEnv: process.env,
|
|
105
|
-
});
|
|
106
|
-
const authorization = buildOpencodeBasicAuthHeader({ username: opencodeUsername, password: opencodePassword });
|
|
107
|
-
if (!authorization) {
|
|
108
|
-
throw new Error("Failed to build OpenCode auth header");
|
|
109
|
-
}
|
|
110
|
-
this.opencodeAuth.set(id, { username: opencodeUsername, password: opencodePassword, authorization });
|
|
111
|
-
const environment = {
|
|
112
|
-
...userEnvironment,
|
|
113
|
-
OPENCODE_CONFIG_CONTENT: opencodeConfigContent,
|
|
114
|
-
OPENCODE_EXPERIMENTAL_WORKSPACES: "true",
|
|
115
|
-
CODENOMAD_INSTANCE_ID: id,
|
|
116
|
-
CODENOMAD_BASE_URL: serverBaseUrl,
|
|
117
|
-
...(this.options.nodeExtraCaCertsPath ? { NODE_EXTRA_CA_CERTS: this.options.nodeExtraCaCertsPath } : {}),
|
|
118
|
-
[OPENCODE_SERVER_BASE_URL_ENV]: `${normalizedServerBaseUrl}${proxyPath}`,
|
|
119
|
-
[OPENCODE_SERVER_USERNAME_ENV]: opencodeUsername,
|
|
120
|
-
[OPENCODE_SERVER_PASSWORD_ENV]: opencodePassword,
|
|
121
|
-
};
|
|
122
|
-
const logLevel = serverConfig?.logLevel;
|
|
152
|
+
this.workspaceIdentities.set(id, identityKey);
|
|
153
|
+
onReserved?.(id);
|
|
123
154
|
try {
|
|
155
|
+
this.options.eventBus.publish({ type: "workspace.created", workspace: descriptor });
|
|
156
|
+
const serverConfig = this.options.settings.getOwner("config", "server");
|
|
157
|
+
const envVars = serverConfig?.environmentVariables;
|
|
158
|
+
const userEnvironment = envVars && typeof envVars === "object" && !Array.isArray(envVars) ? envVars : {};
|
|
159
|
+
const opencodeConfigContent = buildOpencodeConfigContent(resolveExistingOpencodeConfigContent(userEnvironment), this.codeNomadPluginUrl);
|
|
160
|
+
const serverBaseUrl = this.options.getServerBaseUrl();
|
|
161
|
+
const normalizedServerBaseUrl = serverBaseUrl.replace(/\/+$/, "");
|
|
162
|
+
const { username: opencodeUsername, password: opencodePassword } = resolveOpencodeServerAuth({
|
|
163
|
+
userEnvironment,
|
|
164
|
+
processEnv: process.env,
|
|
165
|
+
});
|
|
166
|
+
const authorization = buildOpencodeBasicAuthHeader({ username: opencodeUsername, password: opencodePassword });
|
|
167
|
+
if (!authorization) {
|
|
168
|
+
throw new Error("Failed to build OpenCode auth header");
|
|
169
|
+
}
|
|
170
|
+
this.opencodeAuth.set(id, { username: opencodeUsername, password: opencodePassword, authorization });
|
|
171
|
+
const environment = {
|
|
172
|
+
...userEnvironment,
|
|
173
|
+
OPENCODE_CONFIG_CONTENT: opencodeConfigContent,
|
|
174
|
+
OPENCODE_EXPERIMENTAL_WORKSPACES: "true",
|
|
175
|
+
CODENOMAD_INSTANCE_ID: id,
|
|
176
|
+
CODENOMAD_BASE_URL: serverBaseUrl,
|
|
177
|
+
...(this.options.nodeExtraCaCertsPath ? { NODE_EXTRA_CA_CERTS: this.options.nodeExtraCaCertsPath } : {}),
|
|
178
|
+
[OPENCODE_SERVER_BASE_URL_ENV]: `${normalizedServerBaseUrl}${proxyPath}`,
|
|
179
|
+
[OPENCODE_SERVER_USERNAME_ENV]: opencodeUsername,
|
|
180
|
+
[OPENCODE_SERVER_PASSWORD_ENV]: opencodePassword,
|
|
181
|
+
};
|
|
182
|
+
const logLevel = serverConfig?.logLevel;
|
|
183
|
+
this.throwIfWorkspaceCreationCancelled(id);
|
|
124
184
|
const { pid, port, exitPromise, getLastOutput } = await this.runtime.launch({
|
|
125
185
|
workspaceId: id,
|
|
126
186
|
folder: workspacePath,
|
|
@@ -129,24 +189,42 @@ export class WorkspaceManager {
|
|
|
129
189
|
logLevel,
|
|
130
190
|
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
|
131
191
|
});
|
|
192
|
+
descriptor.pid = pid;
|
|
193
|
+
descriptor.port = port;
|
|
194
|
+
this.throwIfWorkspaceCreationCancelled(id);
|
|
132
195
|
const runtimeVersion = await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput });
|
|
133
196
|
if (runtimeVersion) {
|
|
134
197
|
descriptor.binaryVersion = runtimeVersion;
|
|
135
198
|
}
|
|
136
|
-
|
|
137
|
-
descriptor.port = port;
|
|
199
|
+
this.throwIfWorkspaceCreationCancelled(id);
|
|
138
200
|
descriptor.status = "ready";
|
|
139
201
|
descriptor.updatedAt = new Date().toISOString();
|
|
140
202
|
this.options.eventBus.publish({ type: "workspace.started", workspace: descriptor });
|
|
141
203
|
this.options.logger.info({ workspaceId: id, port }, "Workspace ready");
|
|
142
|
-
return descriptor;
|
|
204
|
+
return { workspace: descriptor, created: true };
|
|
143
205
|
}
|
|
144
206
|
catch (error) {
|
|
207
|
+
let stopFailure;
|
|
208
|
+
await this.runtime.stop(id).catch((stopError) => {
|
|
209
|
+
stopFailure = stopError;
|
|
210
|
+
this.options.logger.warn({ workspaceId: id, err: stopError }, "Failed to stop workspace after startup error");
|
|
211
|
+
});
|
|
212
|
+
const cancelled = this.cancelledWorkspaceCreations.delete(id) || !this.workspaces.has(id);
|
|
213
|
+
if (cancelled && !stopFailure) {
|
|
214
|
+
throw new Error("Workspace creation cancelled");
|
|
215
|
+
}
|
|
145
216
|
descriptor.status = "error";
|
|
146
|
-
descriptor.error =
|
|
217
|
+
descriptor.error = stopFailure instanceof Error
|
|
218
|
+
? `Workspace startup failed and its process could not be stopped: ${stopFailure.message}`
|
|
219
|
+
: error instanceof Error ? error.message : String(error);
|
|
147
220
|
descriptor.updatedAt = new Date().toISOString();
|
|
148
|
-
this.
|
|
221
|
+
if (this.workspaces.has(id)) {
|
|
222
|
+
this.options.eventBus.publish({ type: "workspace.error", workspace: descriptor });
|
|
223
|
+
}
|
|
149
224
|
this.options.logger.error({ workspaceId: id, err: error }, "Workspace failed to start");
|
|
225
|
+
if (cancelled) {
|
|
226
|
+
throw new Error(descriptor.error);
|
|
227
|
+
}
|
|
150
228
|
throw error;
|
|
151
229
|
}
|
|
152
230
|
}
|
|
@@ -155,37 +233,76 @@ export class WorkspaceManager {
|
|
|
155
233
|
if (!workspace)
|
|
156
234
|
return undefined;
|
|
157
235
|
this.options.logger.info({ workspaceId: id }, "Stopping workspace");
|
|
236
|
+
const wasStarting = workspace.status === "starting";
|
|
158
237
|
const wasRunning = Boolean(workspace.pid);
|
|
159
|
-
if (
|
|
160
|
-
|
|
238
|
+
if (wasStarting) {
|
|
239
|
+
this.cancelledWorkspaceCreations.add(id);
|
|
240
|
+
}
|
|
241
|
+
if (wasStarting || wasRunning) {
|
|
242
|
+
try {
|
|
243
|
+
await this.runtime.stop(id);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
161
246
|
this.options.logger.warn({ workspaceId: id, err: error }, "Failed to stop workspace process cleanly");
|
|
162
|
-
|
|
247
|
+
workspace.status = "error";
|
|
248
|
+
workspace.error = error instanceof Error ? error.message : String(error);
|
|
249
|
+
workspace.updatedAt = new Date().toISOString();
|
|
250
|
+
this.options.eventBus.publish({ type: "workspace.error", workspace });
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const identityKey = this.workspaceIdentities.get(id);
|
|
255
|
+
if (identityKey && this.pendingWorkspaceOwners.get(identityKey) === id) {
|
|
256
|
+
this.pendingWorkspaceCreations.delete(identityKey);
|
|
257
|
+
this.pendingWorkspaceOwners.delete(identityKey);
|
|
163
258
|
}
|
|
259
|
+
const stoppedEventPublished = workspace.status === "stopped";
|
|
164
260
|
this.workspaces.delete(id);
|
|
261
|
+
this.workspaceIdentities.delete(id);
|
|
165
262
|
this.opencodeAuth.delete(id);
|
|
263
|
+
if (!wasStarting) {
|
|
264
|
+
this.cancelledWorkspaceCreations.delete(id);
|
|
265
|
+
}
|
|
166
266
|
clearWorkspaceSearchCache(workspace.path);
|
|
167
|
-
if (!
|
|
267
|
+
if (!stoppedEventPublished) {
|
|
168
268
|
this.options.eventBus.publish({ type: "workspace.stopped", workspaceId: id });
|
|
169
269
|
}
|
|
170
270
|
return workspace;
|
|
171
271
|
}
|
|
172
272
|
async shutdown() {
|
|
273
|
+
this.shuttingDown = true;
|
|
173
274
|
this.options.logger.info("Shutting down all workspaces");
|
|
174
275
|
const stopTasks = [];
|
|
175
276
|
for (const [id, workspace] of this.workspaces) {
|
|
176
|
-
if (
|
|
277
|
+
if (workspace.status === "starting") {
|
|
278
|
+
this.cancelledWorkspaceCreations.add(id);
|
|
279
|
+
}
|
|
280
|
+
if (!workspace.pid && workspace.status !== "starting") {
|
|
177
281
|
this.options.logger.debug({ workspaceId: id }, "Workspace already stopped");
|
|
178
282
|
continue;
|
|
179
283
|
}
|
|
180
284
|
this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown");
|
|
181
285
|
stopTasks.push(this.runtime.stop(id).catch((error) => {
|
|
182
286
|
this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown");
|
|
287
|
+
throw error;
|
|
183
288
|
}));
|
|
184
289
|
}
|
|
185
290
|
if (stopTasks.length > 0) {
|
|
186
|
-
await Promise.allSettled(stopTasks);
|
|
291
|
+
const stopResults = await Promise.allSettled(stopTasks);
|
|
292
|
+
const failedStop = stopResults.find((result) => result.status === "rejected");
|
|
293
|
+
if (failedStop) {
|
|
294
|
+
throw failedStop.reason;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (this.activeWorkspaceCreations.size > 0) {
|
|
298
|
+
await Promise.allSettled(this.activeWorkspaceCreations);
|
|
187
299
|
}
|
|
188
300
|
this.workspaces.clear();
|
|
301
|
+
this.workspaceIdentities.clear();
|
|
302
|
+
this.pendingWorkspaceCreations.clear();
|
|
303
|
+
this.pendingWorkspaceOwners.clear();
|
|
304
|
+
this.activeWorkspaceCreations.clear();
|
|
305
|
+
this.cancelledWorkspaceCreations.clear();
|
|
189
306
|
this.opencodeAuth.clear();
|
|
190
307
|
this.options.logger.info("All workspaces cleared");
|
|
191
308
|
}
|
|
@@ -196,6 +313,11 @@ export class WorkspaceManager {
|
|
|
196
313
|
}
|
|
197
314
|
return workspace;
|
|
198
315
|
}
|
|
316
|
+
throwIfWorkspaceCreationCancelled(id) {
|
|
317
|
+
if (this.shuttingDown || this.cancelledWorkspaceCreations.has(id) || !this.workspaces.has(id)) {
|
|
318
|
+
throw new Error("Workspace creation cancelled");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
199
321
|
resolveBinaryPath(identifier) {
|
|
200
322
|
if (!identifier) {
|
|
201
323
|
return identifier;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function normalizeWorkspaceIdentityPath(value, platform = process.platform) {
|
|
4
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
5
|
+
const normalized = pathApi.normalize(value);
|
|
6
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
7
|
+
}
|
|
8
|
+
export async function resolveWorkspaceIdentity(folder, rootDir) {
|
|
9
|
+
const submittedPath = path.isAbsolute(folder) ? path.normalize(folder) : path.resolve(rootDir, folder);
|
|
10
|
+
try {
|
|
11
|
+
const workspacePath = path.normalize(await realpath(submittedPath));
|
|
12
|
+
const metadata = await stat(workspacePath, { bigint: true });
|
|
13
|
+
return {
|
|
14
|
+
workspacePath,
|
|
15
|
+
identityKey: metadata.ino > 0n
|
|
16
|
+
? `fs:${metadata.dev.toString()}:${metadata.ino.toString()}`
|
|
17
|
+
: normalizeWorkspaceIdentityPath(workspacePath),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Preserve the existing launch behavior when the path cannot be resolved yet.
|
|
22
|
+
return {
|
|
23
|
+
workspacePath: submittedPath,
|
|
24
|
+
identityKey: normalizeWorkspaceIdentityPath(submittedPath),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/monaco-viewer-
|
|
2
|
-
import{a6 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-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/monaco-viewer-BJ8dD6do.js","assets/git-diff-vendor-CSgooKT_.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{a6 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-BJ8dD6do.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-CSgooKT_.js";import{S as te}from"./SplitFilePanel-BC46Ypnw.js";import{R as O,O as le,M as re,Q as ne,C as ae,e as ie,T as se}from"./main-BiMrK3qQ.js";import{W as oe}from"./wrap-text-Bb9bhsWo.js";import"./fast-diff-vendor-DgdwVvTQ.js";import"./highlight-vendor-8FKMu9os.js";import"./index-BFLK8Dqd.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>"),V=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-BJ8dD6do.js").then(e=>e.ar),__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 De=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 r=W(),i=H();return r?i.filter(d=>String(d.name||"").toLowerCase().includes(r)):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 T=()=>{const r=e.browserSelectedContent();r!=null&&e.onSave(r)},B=async(r,i)=>{i==null||i.stopPropagation();const d=await ie(r);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 D=()=>[(()=>{var r=ce(),i=r.firstChild,d=i.firstChild,l=d.nextSibling;return s(d,o(ne,{class:"w-4 h-4"})),l.$$input=t=>L(t.currentTarget.value),w(t=>{var a=e.t("instanceShell.filesShell.search.placeholder"),n=e.t("instanceShell.filesShell.search.ariaLabel");return a!==t.e&&u(l,"placeholder",t.e=a),n!==t.t&&u(l,"aria-label",t.t=n),t},{e:void 0,t:void 0}),w(()=>l.value=E()),r})(),(()=>{var r=de(),i=r.firstChild,d=i.nextSibling;return s(i,()=>e.t("instanceShell.filesShell.fileListTitle")),s(d,()=>k().length),r})(),o(g,{get when(){return e.parentPath()},children:r=>(()=>{var i=he(),d=i.firstChild,l=d.firstChild;return i.$$click=()=>e.onLoadEntries(r()),w(()=>u(l,"title",r())),i})()}),o(g,{get when(){return y()},get children(){var r=V();return s(r,()=>e.t("instanceInfo.loading")),r}}),o(g,{get when(){return m(()=>!e.browserError()&&!y())()&&k().length>0},get fallback(){return m(()=>!y())()?m(()=>!!e.browserError())()?(()=>{var r=ue();return s(r,()=>e.browserError()),r})():(()=>{var r=V();return s(r,j),r})():void 0},get children(){return o(ee,{get each(){return k()},children:r=>(()=>{var i=ve(),d=i.firstChild,l=d.firstChild,t=l.firstChild,a=l.nextSibling,n=a.firstChild,c=n.firstChild,f=n.nextSibling;return i.$$click=()=>{if(r.type==="directory"){e.onLoadEntries(r.path);return}e.onRequestOpenFile(r.path)},s(t,()=>r.name),s(c,()=>r.type),f.$$click=v=>void B(r.path,v),s(f,o(ae,{class:"w-3 h-3"})),w(v=>{var F=`file-list-item ${e.browserSelectedPath()===r.path?"file-list-item-active":""}`,z=r.path,R=r.path,I=e.t("instanceShell.filesShell.actions.copyPath"),A=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),I!==v.o&&u(f,"title",v.o=I),A!==v.i&&u(f,"aria-label",v.i=A),v},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),i})()})}})],G=r=>{!(r.ctrlKey||r.metaKey)||r.key.toLowerCase()!=="s"||e.browserSelectedSaving()||!e.browserSelectedDirty()||(r.preventDefault(),T())};return m(()=>{const r=()=>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(),n=a.firstChild;return s(n,i),a})()},children:a=>o(g,{get when(){return S()},get fallback(){return o(Z,{get fallback(){return(()=>{var n=x(),c=n.firstChild;return s(c,()=>e.t("instanceInfo.loading")),n})()},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 n=ge();n.$$mousedown=()=>b==null?void 0:b.focus(),n.$$keydown=G;var c=b;return typeof c=="function"?U(c,n):b=n,s(n,o(re,{get part(){return{type:"text",text:a().content}},get isDark(){return q()},escapeRawHtml:!0})),n}})})},children:a=>(()=>{var n=x(),c=n.firstChild;return s(c,a),n})()})},get children(){var a=x(),n=a.firstChild;return s(n,()=>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,n=a.firstChild;return s(n,r),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",r())),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":""}`,n=!_();return a!==t.e&&C(l,t.e=a),n!==t.t&&(l.disabled=t.t=n),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":""}`,n=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),n!==t.t&&u(l,"title",t.t=n),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=T,s(l,o(g,{get when(){return e.browserSelectedSaving()},get fallback(){return o(le,{class:"h-4 w-4"})},get children(){return o(O,{class:"h-4 w-4 animate-spin"})}})),w(t=>{var a=e.t("instanceShell.rightPanel.actions.save")||"Save (Ctrl+S)",n=e.t("instanceShell.rightPanel.actions.save")||"Save",c=e.browserSelectedSaving()||!e.browserSelectedDirty();return a!==t.e&&u(l,"title",t.e=a),n!==t.t&&u(l,"aria-label",t.t=n),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(O,{get class(){return`h-4 w-4${e.browserLoading()?" animate-spin":""}`}})),w(t=>{var a=e.t("instanceShell.rightPanel.actions.refresh"),n=e.t("instanceShell.rightPanel.actions.refresh"),c=e.browserLoading();return a!==t.e&&u(l,"title",t.e=a),n!==t.t&&u(l,"aria-label",t.t=n),c!==t.a&&(l.disabled=t.a=c),t},{e:void 0,t:void 0,a:void 0}),l})()]},list:{panel:()=>o(D,{}),overlay:()=>o(D,{})},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{De as default};
|