@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.
Files changed (33) hide show
  1. package/dist/server/routes/workspaces.js +3 -2
  2. package/dist/workspaces/__tests__/workspace-identity.test.js +203 -0
  3. package/dist/workspaces/manager.js +164 -42
  4. package/dist/workspaces/workspace-identity.js +27 -0
  5. package/package.json +1 -1
  6. package/public/assets/{FilesTab-DWE08QeL.js → FilesTab-CDX_msaa.js} +2 -2
  7. package/public/assets/{GitChangesTab-B0YpYXig.js → GitChangesTab-rcYbXriQ.js} +2 -2
  8. package/public/assets/{SplitFilePanel-r8xyMSwo.js → SplitFilePanel-BC46Ypnw.js} +1 -1
  9. package/public/assets/{StatusTab-CiF7bc-C.js → StatusTab-BdKL8EFK.js} +1 -1
  10. package/public/assets/{align-justify-D7GxsbXt.js → align-justify-uf_OdRBz.js} +1 -1
  11. package/public/assets/{bundle-full-CaqU_-Bu.js → bundle-full-BNEJFEWn.js} +1 -1
  12. package/public/assets/{diff-viewer-BemESP1g.js → diff-viewer-5SSUPiOf.js} +1 -1
  13. package/public/assets/{index-CdOQKtdG.js → index-BCWrXgFI.js} +1 -1
  14. package/public/assets/{index-B5fSJ4Sy.js → index-BFLK8Dqd.js} +2 -2
  15. package/public/assets/{index-DqvdokW8.js → index-BXWJJQEd.js} +1 -1
  16. package/public/assets/{index-CrwkCZNr.js → index-Bg-mj-8s.js} +1 -1
  17. package/public/assets/{index-CuOhJGR_.js → index-C0n-CjpZ.js} +1 -1
  18. package/public/assets/{index-CqPbeD4l.js → index-CJOjWiX-.js} +1 -1
  19. package/public/assets/{index-CNR6iBgw.js → index-Ctv_UxAd.js} +1 -1
  20. package/public/assets/{index-BIJEJ4fH.js → index-D5ajnHwk.js} +1 -1
  21. package/public/assets/{index-Hbpg8uhJ.js → index-DLDznEeH.js} +1 -1
  22. package/public/assets/{index-DosiZsbF.js → index-Djl8_LHh.js} +1 -1
  23. package/public/assets/{index-BzdwA-jE.js → index-nLFYZCXo.js} +1 -1
  24. package/public/assets/{loading-VAuQsLpy.js → loading-BsojmlCC.js} +1 -1
  25. package/public/assets/{main-BcrMO00Y.js → main-BiMrK3qQ.js} +16 -16
  26. package/public/assets/{markdown-DVTO-nx2.js → markdown-CXI6_8-r.js} +3 -3
  27. package/public/assets/{monaco-viewer-B3Jl6a5p.js → monaco-viewer-BJ8dD6do.js} +1 -1
  28. package/public/assets/{tool-call-BtNDaNja.js → tool-call-DiUpuzuW.js} +3 -3
  29. package/public/assets/{unified-picker-CHfDVhMa.js → unified-picker-DWR5mJDE.js} +1 -1
  30. package/public/assets/{wrap-text-SJDACYWw.js → wrap-text-Bb9bhsWo.js} +1 -1
  31. package/public/index.html +3 -3
  32. package/public/loading.html +3 -3
  33. 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 workspace = await deps.workspaceManager.create(body.path, body.name);
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 id = `${Date.now().toString(36)}`;
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.options.eventBus.publish({ type: "workspace.created", workspace: descriptor });
96
- const serverConfig = this.options.settings.getOwner("config", "server");
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
- descriptor.pid = pid;
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 = error instanceof Error ? error.message : String(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.options.eventBus.publish({ type: "workspace.error", workspace: descriptor });
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 (wasRunning) {
160
- await this.runtime.stop(id).catch((error) => {
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 (!wasRunning) {
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 (!workspace.pid) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralnomads/codenomad-dev",
3
- "version": "0.18.0-dev-20260712-c8d4a509",
3
+ "version": "0.18.0-dev-20260713-f34e8071",
4
4
  "description": "CodeNomad Server",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,2 +1,2 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/monaco-viewer-B3Jl6a5p.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-B3Jl6a5p.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-r8xyMSwo.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-BcrMO00Y.js";import{W as oe}from"./wrap-text-SJDACYWw.js";import"./fast-diff-vendor-DgdwVvTQ.js";import"./highlight-vendor-8FKMu9os.js";import"./index-B5fSJ4Sy.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-B3Jl6a5p.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};
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};