@larose/pi-web 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +50 -0
  3. package/THIRD_PARTY_LICENSES.md +40 -0
  4. package/dist/client/home.js +1619 -0
  5. package/dist/client/session.js +3703 -0
  6. package/dist/server/api.js +485 -0
  7. package/dist/server/cli.js +51 -0
  8. package/dist/server/directory-browser.js +104 -0
  9. package/dist/server/errors.js +10 -0
  10. package/dist/server/event-buffer.js +40 -0
  11. package/dist/server/extension-ui.js +245 -0
  12. package/dist/server/git-workspaces.js +559 -0
  13. package/dist/server/runtime-registry.js +703 -0
  14. package/dist/server/server.js +190 -0
  15. package/dist/server/session-repository.js +374 -0
  16. package/package.json +46 -0
  17. package/public/home.html +139 -0
  18. package/public/session.html +144 -0
  19. package/public/styles.css +2463 -0
  20. package/screenshots/home.png +0 -0
  21. package/screenshots/session.png +0 -0
  22. package/src/client/display-title.ts +36 -0
  23. package/src/client/event-stream.ts +194 -0
  24. package/src/client/home.ts +1575 -0
  25. package/src/client/markdown.ts +98 -0
  26. package/src/client/message-queue.ts +67 -0
  27. package/src/client/path-combobox.ts +271 -0
  28. package/src/client/session.ts +2174 -0
  29. package/src/client/shared.ts +99 -0
  30. package/src/client/slash-completion.ts +184 -0
  31. package/src/client/transcript-activity.ts +188 -0
  32. package/src/client/usage-format.ts +156 -0
  33. package/src/client/workspace-browser.ts +36 -0
  34. package/src/server/api.ts +652 -0
  35. package/src/server/cli.ts +63 -0
  36. package/src/server/directory-browser.ts +137 -0
  37. package/src/server/errors.ts +11 -0
  38. package/src/server/event-buffer.ts +59 -0
  39. package/src/server/extension-ui.ts +359 -0
  40. package/src/server/git-workspaces.ts +750 -0
  41. package/src/server/runtime-registry.ts +943 -0
  42. package/src/server/server.ts +248 -0
  43. package/src/server/session-repository.ts +488 -0
@@ -0,0 +1,1619 @@
1
+ // src/client/display-title.ts
2
+ var UNNAMED_SESSION_TITLE = "Unnamed session";
3
+ var MAX_FALLBACK_TITLE_CHARS = 60;
4
+ function normalizedFirstSentence(value) {
5
+ const normalized = value.replace(/\s+/gu, " ").trim();
6
+ const sentenceEnd = /[.!?。!?](?=\s|$)/u.exec(normalized);
7
+ return sentenceEnd ? normalized.slice(0, sentenceEnd.index + sentenceEnd[0].length) : normalized;
8
+ }
9
+ function boundedTitle(value) {
10
+ const characters = Array.from(value);
11
+ if (characters.length <= MAX_FALLBACK_TITLE_CHARS) {
12
+ return value;
13
+ }
14
+ const prefix = characters.slice(0, MAX_FALLBACK_TITLE_CHARS - 1).join("").trimEnd();
15
+ return `${prefix}\u2026`;
16
+ }
17
+ function displaySessionTitle(source) {
18
+ const nativeName = source.name?.trim();
19
+ if (nativeName) {
20
+ return nativeName;
21
+ }
22
+ const firstSentence = normalizedFirstSentence(source.firstMessage ?? "");
23
+ return firstSentence ? boundedTitle(firstSentence) : UNNAMED_SESSION_TITLE;
24
+ }
25
+
26
+ // src/client/path-combobox.ts
27
+ var nextComboboxId = 0;
28
+ function uniqueSuggestions(suggestions) {
29
+ const values = /* @__PURE__ */ new Set();
30
+ return suggestions.filter((suggestion) => {
31
+ if (values.has(suggestion.value)) {
32
+ return false;
33
+ }
34
+ values.add(suggestion.value);
35
+ return true;
36
+ });
37
+ }
38
+ var PathCombobox = class {
39
+ options;
40
+ optionIdPrefix;
41
+ suggestions = [];
42
+ activeIndex = -1;
43
+ generation = 0;
44
+ request = null;
45
+ openState = false;
46
+ loading = false;
47
+ disabled = false;
48
+ constructor(options) {
49
+ this.options = options;
50
+ this.optionIdPrefix = `path-combobox-option-${++nextComboboxId}`;
51
+ options.input.setAttribute("role", "combobox");
52
+ options.input.setAttribute("aria-autocomplete", "list");
53
+ options.input.setAttribute("aria-controls", options.list.id);
54
+ options.input.setAttribute("aria-expanded", "false");
55
+ options.input.setAttribute("aria-haspopup", "listbox");
56
+ options.toggle.setAttribute("aria-controls", options.list.id);
57
+ options.list.setAttribute("role", "listbox");
58
+ options.list.setAttribute("aria-label", "Directory suggestions");
59
+ options.input.addEventListener("focus", () => this.open());
60
+ options.input.addEventListener("click", () => {
61
+ const end = options.input.value.length;
62
+ options.input.setSelectionRange(end, end);
63
+ });
64
+ options.input.addEventListener("input", () => {
65
+ this.activeIndex = -1;
66
+ this.openState = true;
67
+ this.render();
68
+ void this.refresh();
69
+ });
70
+ options.input.addEventListener("keydown", (event) => this.handleKeydown(event));
71
+ options.input.addEventListener("blur", () => {
72
+ window.setTimeout(() => this.close(), 0);
73
+ });
74
+ options.toggle.addEventListener("click", () => {
75
+ if (this.disabled) {
76
+ return;
77
+ }
78
+ if (this.openState) {
79
+ this.close();
80
+ } else {
81
+ options.input.focus();
82
+ if (!this.openState) {
83
+ this.open();
84
+ }
85
+ }
86
+ });
87
+ }
88
+ setValue(value) {
89
+ this.options.input.value = value;
90
+ }
91
+ value() {
92
+ return this.options.input.value;
93
+ }
94
+ setDisabled(disabled) {
95
+ this.disabled = disabled;
96
+ this.options.input.disabled = disabled;
97
+ this.options.toggle.disabled = disabled;
98
+ if (disabled) {
99
+ this.close();
100
+ }
101
+ }
102
+ refreshShortcuts() {
103
+ if (this.openState) {
104
+ void this.refresh();
105
+ }
106
+ }
107
+ close() {
108
+ this.openState = false;
109
+ this.activeIndex = -1;
110
+ this.request?.abort();
111
+ this.request = null;
112
+ this.options.input.removeAttribute("aria-activedescendant");
113
+ this.render();
114
+ }
115
+ open() {
116
+ if (this.disabled) {
117
+ return;
118
+ }
119
+ this.openState = true;
120
+ this.render();
121
+ void this.refresh();
122
+ }
123
+ async refresh() {
124
+ const generation = ++this.generation;
125
+ this.request?.abort();
126
+ const request = new AbortController();
127
+ this.request = request;
128
+ const shortcuts = this.options.shortcuts();
129
+ const value = this.options.input.value.trim();
130
+ this.loading = Boolean(value);
131
+ this.suggestions = shortcuts;
132
+ this.options.status.textContent = this.loading ? "Loading folders\u2026" : "";
133
+ this.render();
134
+ if (!value) {
135
+ this.loading = false;
136
+ this.options.status.textContent = shortcuts.length > 0 ? `${shortcuts.length} recent paths` : "Enter a path";
137
+ return;
138
+ }
139
+ try {
140
+ const loaded = await this.options.load(value, request.signal);
141
+ if (generation !== this.generation || request.signal.aborted) {
142
+ return;
143
+ }
144
+ this.suggestions = uniqueSuggestions([...shortcuts, ...loaded]);
145
+ this.options.status.textContent = this.suggestions.length > 0 ? `${this.suggestions.length} paths available` : "No matching directories";
146
+ } catch (error) {
147
+ if (generation !== this.generation || request.signal.aborted) {
148
+ return;
149
+ }
150
+ this.suggestions = shortcuts;
151
+ this.options.status.textContent = error instanceof Error ? error.message : "Could not browse directories";
152
+ } finally {
153
+ if (generation === this.generation) {
154
+ this.loading = false;
155
+ this.request = null;
156
+ this.render();
157
+ }
158
+ }
159
+ }
160
+ render() {
161
+ this.options.input.setAttribute("aria-expanded", String(this.openState));
162
+ this.options.input.setAttribute("aria-busy", String(this.loading));
163
+ this.options.toggle.setAttribute("aria-expanded", String(this.openState));
164
+ this.options.list.hidden = !this.openState;
165
+ if (!this.openState) {
166
+ this.options.list.replaceChildren();
167
+ return;
168
+ }
169
+ const fragment = document.createDocumentFragment();
170
+ let previousSource = null;
171
+ for (const [index, suggestion] of this.suggestions.entries()) {
172
+ if (suggestion.source !== previousSource) {
173
+ const heading = document.createElement("li");
174
+ heading.className = "path-combobox-heading";
175
+ heading.textContent = suggestion.source === "recent" ? "Recent" : "Folders";
176
+ heading.setAttribute("role", "presentation");
177
+ fragment.append(heading);
178
+ previousSource = suggestion.source;
179
+ }
180
+ const item = document.createElement("li");
181
+ item.className = "path-combobox-option";
182
+ item.id = `${this.optionIdPrefix}-${index}`;
183
+ item.setAttribute("role", "option");
184
+ item.setAttribute("aria-selected", String(index === this.activeIndex));
185
+ item.append(Object.assign(document.createElement("strong"), { textContent: suggestion.label }));
186
+ if (suggestion.detail) {
187
+ item.append(Object.assign(document.createElement("span"), { textContent: suggestion.detail }));
188
+ }
189
+ item.addEventListener("mousedown", (event) => event.preventDefault());
190
+ item.addEventListener("click", () => this.choose(index));
191
+ fragment.append(item);
192
+ }
193
+ if (this.suggestions.length === 0 && !this.loading) {
194
+ const empty = document.createElement("li");
195
+ empty.className = "path-combobox-empty";
196
+ empty.textContent = "No directories found";
197
+ empty.setAttribute("role", "presentation");
198
+ fragment.append(empty);
199
+ }
200
+ this.options.list.replaceChildren(fragment);
201
+ }
202
+ choose(index) {
203
+ const suggestion = this.suggestions[index];
204
+ if (!suggestion) {
205
+ return;
206
+ }
207
+ this.options.input.value = suggestion.value;
208
+ this.close();
209
+ void this.options.commit(suggestion.value);
210
+ }
211
+ moveActive(offset) {
212
+ if (this.suggestions.length === 0) {
213
+ return;
214
+ }
215
+ this.activeIndex = this.activeIndex < 0 ? offset > 0 ? 0 : this.suggestions.length - 1 : (this.activeIndex + offset + this.suggestions.length) % this.suggestions.length;
216
+ this.options.input.setAttribute("aria-activedescendant", `${this.optionIdPrefix}-${this.activeIndex}`);
217
+ this.render();
218
+ document.getElementById(`${this.optionIdPrefix}-${this.activeIndex}`)?.scrollIntoView({ block: "nearest" });
219
+ }
220
+ handleKeydown(event) {
221
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
222
+ event.preventDefault();
223
+ if (!this.openState) {
224
+ this.open();
225
+ }
226
+ this.moveActive(event.key === "ArrowDown" ? 1 : -1);
227
+ return;
228
+ }
229
+ if (event.key === "Escape" && this.openState) {
230
+ event.preventDefault();
231
+ this.close();
232
+ return;
233
+ }
234
+ if (event.key === "Enter") {
235
+ event.preventDefault();
236
+ if (this.openState && this.activeIndex >= 0) {
237
+ this.choose(this.activeIndex);
238
+ } else {
239
+ this.close();
240
+ void this.options.commit(this.options.input.value);
241
+ }
242
+ }
243
+ }
244
+ };
245
+
246
+ // src/client/shared.ts
247
+ function requiredElement(selector, root = document) {
248
+ const element = root.querySelector(selector);
249
+ if (!element) {
250
+ throw new Error(`Missing required element: ${selector}`);
251
+ }
252
+ return element;
253
+ }
254
+ function textElement(tag, className, text) {
255
+ const element = document.createElement(tag);
256
+ element.className = className;
257
+ element.textContent = text;
258
+ return element;
259
+ }
260
+ function readableError(error) {
261
+ return error instanceof Error ? error.message : String(error);
262
+ }
263
+ async function api(path, options) {
264
+ const response = await fetch(path, options);
265
+ const body = await response.json().catch(() => void 0);
266
+ if (!response.ok) {
267
+ throw new Error(body?.error?.message ?? `Request failed (${response.status})`);
268
+ }
269
+ return body;
270
+ }
271
+ function sessionPath(id) {
272
+ return `/sessions/${encodeURIComponent(id)}`;
273
+ }
274
+
275
+ // src/client/workspace-browser.ts
276
+ function repositoryName(root) {
277
+ const normalized = root.replace(/[\\/]+$/, "");
278
+ return normalized.split(/[\\/]/).pop() || root;
279
+ }
280
+ function displayRelativePath(relativePath) {
281
+ return relativePath === "." ? "(root)" : relativePath;
282
+ }
283
+ function relativePathWithin(root, candidate) {
284
+ const separator = root.includes("\\") && !root.includes("/") ? "\\" : "/";
285
+ const normalizedRoot = root.replaceAll("\\", "/").replace(/\/+$/, "") || "/";
286
+ const normalizedCandidate = candidate.replaceAll("\\", "/").replace(/\/+$/, "") || "/";
287
+ const caseInsensitive = /^[A-Za-z]:/.test(normalizedRoot);
288
+ const comparableRoot = caseInsensitive ? normalizedRoot.toLocaleLowerCase() : normalizedRoot;
289
+ const comparableCandidate = caseInsensitive ? normalizedCandidate.toLocaleLowerCase() : normalizedCandidate;
290
+ if (comparableCandidate === comparableRoot) {
291
+ return ".";
292
+ }
293
+ const rootPrefix = comparableRoot === "/" ? "/" : `${comparableRoot}/`;
294
+ if (!comparableCandidate.startsWith(rootPrefix)) {
295
+ return null;
296
+ }
297
+ return normalizedCandidate.slice(rootPrefix.length).replaceAll("/", separator);
298
+ }
299
+ function worktreeTargetPath(context, name) {
300
+ const root = context.repositoryRoot.replace(/[\\/]$/, "");
301
+ const separator = root.includes("\\") && !root.includes("/") ? "\\" : "/";
302
+ const relativeCwd = context.relativeCwd === "." ? "" : `${separator}${context.relativeCwd}`;
303
+ return `${root}${separator}.pi${separator}worktrees${separator}${name || "<name>"}${relativeCwd}`;
304
+ }
305
+
306
+ // src/client/home.ts
307
+ var elements = {
308
+ appNotifications: requiredElement("[data-app-notifications]"),
309
+ checkoutFeedback: requiredElement("[data-workspace-checkout-feedback]"),
310
+ checkoutSelect: requiredElement("[data-workspace-checkout]"),
311
+ createSubmit: requiredElement("[data-create-submit]"),
312
+ gitStatus: requiredElement("[data-git-status]"),
313
+ kindRepository: requiredElement("[data-workspace-kind-repository]"),
314
+ kindStandalone: requiredElement("[data-workspace-kind-standalone]"),
315
+ latestSessionList: requiredElement("[data-latest-session-list]"),
316
+ newSession: requiredElement("[data-new-session]"),
317
+ newSessionCancel: requiredElement("[data-new-session-cancel]"),
318
+ newSessionToggle: requiredElement("[data-new-session-toggle]"),
319
+ newWorktreeMode: requiredElement("[data-new-worktree-mode]"),
320
+ recentWorkspace: requiredElement("[data-recent-workspace]"),
321
+ relativeFeedback: requiredElement("[data-workspace-relative-feedback]"),
322
+ relativeInput: requiredElement("[data-workspace-relative-path]"),
323
+ relativeList: requiredElement("[data-workspace-relative-options]"),
324
+ relativeStatus: requiredElement("[data-workspace-relative-status]"),
325
+ relativeToggle: requiredElement("[data-workspace-relative-toggle]"),
326
+ repositoryFeedback: requiredElement("[data-repository-path-feedback]"),
327
+ repositoryFields: requiredElement("[data-repository-fields]"),
328
+ repositoryInput: requiredElement("[data-repository-path]"),
329
+ repositoryList: requiredElement("[data-repository-path-options]"),
330
+ repositoryStatus: requiredElement("[data-repository-path-status]"),
331
+ repositoryToggle: requiredElement("[data-repository-path-toggle]"),
332
+ sessionList: requiredElement("[data-session-list]"),
333
+ standaloneFeedback: requiredElement("[data-standalone-path-feedback]"),
334
+ standaloneFields: requiredElement("[data-standalone-fields]"),
335
+ standaloneInput: requiredElement("[data-standalone-path]"),
336
+ standaloneList: requiredElement("[data-standalone-path-options]"),
337
+ standaloneStatus: requiredElement("[data-standalone-path-status]"),
338
+ standaloneToggle: requiredElement("[data-standalone-path-toggle]"),
339
+ worktreeAvailability: requiredElement("[data-worktree-availability]"),
340
+ worktreeCancel: requiredElement("[data-worktree-cancel]"),
341
+ worktreeName: requiredElement("[data-worktree-name]"),
342
+ worktreeStart: requiredElement("[data-worktree-start]"),
343
+ worktreeSubmit: requiredElement("[data-worktree-submit]"),
344
+ worktreeTarget: requiredElement("[data-worktree-target]")
345
+ };
346
+ var listing = { groups: [], knownCwds: [], activeSessionIds: [], runningSessionIds: [] };
347
+ var workspaceKind = "repository";
348
+ var repositorySelection = null;
349
+ var standaloneSelection = null;
350
+ var inspectionGeneration = 0;
351
+ var workspacePending = false;
352
+ var pendingRecentWorkspaceValue = null;
353
+ var recentWorkspaceOptionsSignature = "";
354
+ var newSessionOpen = false;
355
+ var workspaceInitialized = false;
356
+ var newWorktreeOpen = false;
357
+ var creatingSession = false;
358
+ var contextualCreationCwd = null;
359
+ var repositoryWorktreeEditorRoot = null;
360
+ var repositoryWorktreeName = "";
361
+ var repositoryWorktreeCreationRoot = null;
362
+ var appNotifications = /* @__PURE__ */ new Map();
363
+ var deletingSessionIds = /* @__PURE__ */ new Set();
364
+ var removingWorktreeRoots = /* @__PURE__ */ new Set();
365
+ function formatTime(value) {
366
+ const date = new Date(value);
367
+ if (Number.isNaN(date.getTime())) {
368
+ return "";
369
+ }
370
+ return new Intl.DateTimeFormat(void 0, {
371
+ month: "short",
372
+ day: "numeric",
373
+ hour: "numeric",
374
+ minute: "2-digit"
375
+ }).format(date);
376
+ }
377
+ function clearAppStatus(key) {
378
+ const notification = appNotifications.get(key);
379
+ if (!notification) {
380
+ return;
381
+ }
382
+ if (notification.dismissal !== void 0) {
383
+ window.clearTimeout(notification.dismissal);
384
+ }
385
+ notification.element.remove();
386
+ appNotifications.delete(key);
387
+ elements.appNotifications.hidden = appNotifications.size === 0;
388
+ }
389
+ function createAppNotification(key) {
390
+ const element = document.createElement("div");
391
+ element.className = "app-notification";
392
+ const message = document.createElement("p");
393
+ message.className = "app-notification-message";
394
+ message.setAttribute("aria-atomic", "true");
395
+ const dismiss = document.createElement("button");
396
+ dismiss.className = "app-notification-dismiss";
397
+ dismiss.type = "button";
398
+ dismiss.textContent = "\xD7";
399
+ dismiss.setAttribute("aria-label", "Dismiss notification");
400
+ dismiss.addEventListener("click", () => clearAppStatus(key));
401
+ element.append(message, dismiss);
402
+ elements.appNotifications.prepend(element);
403
+ elements.appNotifications.hidden = false;
404
+ const notification = { dismissal: void 0, element, generation: 0, message, dismiss };
405
+ appNotifications.set(key, notification);
406
+ return notification;
407
+ }
408
+ function setAppStatus(key, message, tone) {
409
+ if (!message) {
410
+ clearAppStatus(key);
411
+ return;
412
+ }
413
+ const notification = appNotifications.get(key) ?? createAppNotification(key);
414
+ const generation = ++notification.generation;
415
+ if (notification.dismissal !== void 0) {
416
+ window.clearTimeout(notification.dismissal);
417
+ notification.dismissal = void 0;
418
+ }
419
+ notification.element.dataset.tone = tone;
420
+ notification.dismiss.hidden = false;
421
+ notification.message.setAttribute("role", tone === "error" ? "alert" : "status");
422
+ notification.message.setAttribute("aria-live", tone === "error" ? "assertive" : "polite");
423
+ notification.message.textContent = message;
424
+ if (tone === "success") {
425
+ notification.dismissal = window.setTimeout(() => {
426
+ notification.dismissal = void 0;
427
+ if (generation === notification.generation) {
428
+ clearAppStatus(key);
429
+ }
430
+ }, 1e4);
431
+ }
432
+ }
433
+ function setGitStatus(message, isError = false) {
434
+ elements.gitStatus.textContent = message;
435
+ elements.gitStatus.classList.toggle("error", isError);
436
+ }
437
+ function feedbackElement(control) {
438
+ if (control === "checkout") {
439
+ return elements.checkoutFeedback;
440
+ }
441
+ if (control === "relative") {
442
+ return elements.relativeFeedback;
443
+ }
444
+ if (control === "repository") {
445
+ return elements.repositoryFeedback;
446
+ }
447
+ return elements.standaloneFeedback;
448
+ }
449
+ function setFeedback(control, message = "", isError = false) {
450
+ const element = feedbackElement(control);
451
+ element.textContent = message || "\xA0";
452
+ element.classList.toggle("error", isError);
453
+ }
454
+ function clearFeedback() {
455
+ setFeedback("checkout");
456
+ setFeedback("relative");
457
+ setFeedback("repository");
458
+ setFeedback("standalone");
459
+ }
460
+ function worktreeHeadLabel(worktree) {
461
+ return worktree.head.type === "branch" ? worktree.head.name : `detached ${worktree.head.shortCommit}`;
462
+ }
463
+ function checkoutLabel(worktree) {
464
+ return worktree.isLinkedWorktree ? "Worktree" : "Primary checkout";
465
+ }
466
+ function compareText(left, right) {
467
+ const folded = left.toLocaleLowerCase().localeCompare(right.toLocaleLowerCase());
468
+ return folded || left.localeCompare(right);
469
+ }
470
+ function modifiedTimestamp(session) {
471
+ const timestamp = Date.parse(session.modified);
472
+ return Number.isNaN(timestamp) ? 0 : timestamp;
473
+ }
474
+ function compareLatestSessions(left, right) {
475
+ return modifiedTimestamp(right) - modifiedTimestamp(left) || left.id.localeCompare(right.id);
476
+ }
477
+ function compareSessionsByTitle(left, right) {
478
+ const title = compareText(displaySessionTitle(left), displaySessionTitle(right));
479
+ return title || modifiedTimestamp(right) - modifiedTimestamp(left) || left.id.localeCompare(right.id);
480
+ }
481
+ function compareWorktrees(left, right) {
482
+ if (left.isLinkedWorktree !== right.isLinkedWorktree) {
483
+ return left.isLinkedWorktree ? 1 : -1;
484
+ }
485
+ return compareText(worktreeHeadLabel(left), worktreeHeadLabel(right)) || compareText(left.worktreeRoot, right.worktreeRoot);
486
+ }
487
+ function compareRelativePaths(left, right) {
488
+ if (left === "." || right === ".") {
489
+ return left === "." ? -1 : 1;
490
+ }
491
+ return compareText(left, right);
492
+ }
493
+ function option(value, label) {
494
+ const element = document.createElement("option");
495
+ element.value = value;
496
+ element.textContent = label;
497
+ return element;
498
+ }
499
+ function pathSeparator(path) {
500
+ return path.includes("\\") && !path.includes("/") ? "\\" : "/";
501
+ }
502
+ function trimTrailingSeparators(path) {
503
+ if (/^[A-Za-z]:[\\/]?$/.test(path)) {
504
+ return path;
505
+ }
506
+ return path.replace(/[\\/]+$/, "");
507
+ }
508
+ function joinServerPath(root, relativePath) {
509
+ if (!relativePath || relativePath === ".") {
510
+ return root;
511
+ }
512
+ return `${trimTrailingSeparators(root)}${pathSeparator(root)}${relativePath}`;
513
+ }
514
+ function isAbsoluteServerPath(path) {
515
+ return path.startsWith("/") || path.startsWith("\\\\") || /^[A-Za-z]:[\\/]/.test(path);
516
+ }
517
+ function normalizeRelativePath(value, root) {
518
+ const trimmed = value.trim();
519
+ if (!trimmed || trimmed === "." || trimmed.toLocaleLowerCase() === "(root)") {
520
+ return ".";
521
+ }
522
+ if (isAbsoluteServerPath(trimmed)) {
523
+ throw new Error("Working directory must be relative to the selected checkout.");
524
+ }
525
+ const segments = trimmed.split(/[\\/]+/).filter((segment) => segment && segment !== ".");
526
+ if (segments.includes("..")) {
527
+ throw new Error("Working directory cannot leave the selected checkout.");
528
+ }
529
+ return segments.join(pathSeparator(root)) || ".";
530
+ }
531
+ async function requestInspection(cwd) {
532
+ return api("/api/git-workspaces/inspect", {
533
+ method: "POST",
534
+ headers: { "content-type": "application/json" },
535
+ body: JSON.stringify({ cwd })
536
+ });
537
+ }
538
+ async function browseDirectories(path, signal) {
539
+ return api("/api/directories", {
540
+ method: "POST",
541
+ headers: { "content-type": "application/json" },
542
+ body: JSON.stringify({ path }),
543
+ signal
544
+ });
545
+ }
546
+ function recentWorkspaceValue() {
547
+ if (workspaceKind === "repository" && repositorySelection) {
548
+ return `repository:${repositorySelection.repositoryRoot}`;
549
+ }
550
+ if (workspaceKind === "standalone" && standaloneSelection) {
551
+ return `standalone:${standaloneSelection.cwd}`;
552
+ }
553
+ return "";
554
+ }
555
+ function renderRecentWorkspaces() {
556
+ const choices = listing.groups.map(
557
+ (group) => group.type === "repository" ? {
558
+ value: `repository:${group.repositoryRoot}`,
559
+ label: `Git \xB7 ${repositoryName(group.repositoryRoot)} \u2014 ${group.repositoryRoot}`
560
+ } : {
561
+ value: `standalone:${group.cwd}`,
562
+ label: `Directory \xB7 ${repositoryName(group.cwd)} \u2014 ${group.cwd}`
563
+ }
564
+ );
565
+ const placeholder = listing.groups.length > 0 ? "Choose a recent workspace\u2026" : "No recent workspaces";
566
+ const signature = JSON.stringify({ placeholder, choices });
567
+ if (signature !== recentWorkspaceOptionsSignature) {
568
+ elements.recentWorkspace.replaceChildren(
569
+ option("", placeholder),
570
+ ...choices.map((choice) => option(choice.value, choice.label))
571
+ );
572
+ recentWorkspaceOptionsSignature = signature;
573
+ }
574
+ elements.recentWorkspace.value = pendingRecentWorkspaceValue ?? recentWorkspaceValue();
575
+ elements.recentWorkspace.disabled = creatingSession || listing.groups.length === 0;
576
+ }
577
+ function relativeShortcuts() {
578
+ const selection = repositorySelection;
579
+ if (!selection) {
580
+ return [];
581
+ }
582
+ const paths = ["."];
583
+ const group = listing.groups.find(
584
+ (candidate) => candidate.type === "repository" && candidate.repositoryRoot === selection.repositoryRoot
585
+ );
586
+ if (group) {
587
+ for (const session of group.sessions) {
588
+ const relativeCwd = session.gitContext?.relativeCwd;
589
+ if (relativeCwd && !paths.includes(relativeCwd)) {
590
+ paths.push(relativeCwd);
591
+ }
592
+ }
593
+ }
594
+ return paths.map((path) => ({
595
+ value: displayRelativePath(path),
596
+ label: displayRelativePath(path),
597
+ detail: path === "." ? "Checkout root" : "Recent working directory",
598
+ source: "recent"
599
+ }));
600
+ }
601
+ function absoluteDirectorySuggestions(listing2) {
602
+ return listing2.directories.map((directory) => ({
603
+ value: directory.path,
604
+ label: directory.name,
605
+ detail: directory.path,
606
+ source: "server"
607
+ }));
608
+ }
609
+ function primaryWorktree(inspection) {
610
+ return inspection.worktrees.find((worktree) => !worktree.isLinkedWorktree) ?? inspection.worktrees[0] ?? null;
611
+ }
612
+ function closeWorktreeEditor() {
613
+ newWorktreeOpen = false;
614
+ elements.worktreeName.value = "";
615
+ }
616
+ function currentCwd() {
617
+ return workspaceKind === "repository" ? repositorySelection?.cwd ?? "" : standaloneSelection?.cwd ?? "";
618
+ }
619
+ function renderCheckout() {
620
+ const selection = repositorySelection;
621
+ elements.checkoutSelect.replaceChildren();
622
+ if (!selection) {
623
+ elements.checkoutSelect.append(option("", "Choose a repository first"));
624
+ return;
625
+ }
626
+ for (const worktree of [...selection.repositoryInspection.worktrees].sort(compareWorktrees)) {
627
+ const label = worktree.isLinkedWorktree ? `${worktreeHeadLabel(worktree)} \u2014 ${checkoutLabel(worktree)}` : checkoutLabel(worktree);
628
+ elements.checkoutSelect.append(option(worktree.worktreeRoot, label));
629
+ }
630
+ elements.checkoutSelect.value = selection.worktreeRoot;
631
+ }
632
+ function renderWorktreeMode() {
633
+ const inspection = repositorySelection?.selectedInspection;
634
+ const repositoryActive = workspaceKind === "repository" && Boolean(repositorySelection);
635
+ elements.newWorktreeMode.hidden = !newWorktreeOpen || !repositoryActive;
636
+ elements.worktreeName.required = newWorktreeOpen && repositoryActive;
637
+ elements.worktreeStart.hidden = !repositoryActive || newWorktreeOpen;
638
+ elements.worktreeStart.disabled = workspacePending || !inspection?.creation.available;
639
+ elements.worktreeStart.setAttribute("aria-expanded", String(newWorktreeOpen));
640
+ elements.worktreeAvailability.textContent = inspection?.creation.reason ?? "";
641
+ elements.worktreeAvailability.hidden = !repositoryActive || newWorktreeOpen || Boolean(inspection?.creation.available) || !inspection?.creation.reason;
642
+ elements.createSubmit.hidden = newWorktreeOpen && repositoryActive;
643
+ elements.worktreeSubmit.hidden = !newWorktreeOpen || !repositoryActive;
644
+ }
645
+ function renderWorktreeTarget() {
646
+ const context = repositorySelection?.selectedInspection.context;
647
+ elements.worktreeTarget.textContent = context ? worktreeTargetPath(context, elements.worktreeName.value.trim()) : "";
648
+ }
649
+ function renderNewSessionDisclosure() {
650
+ elements.newSession.hidden = !newSessionOpen;
651
+ elements.newSessionToggle.setAttribute("aria-expanded", String(newSessionOpen));
652
+ elements.newSessionToggle.disabled = creatingSession;
653
+ elements.newSessionCancel.disabled = creatingSession;
654
+ }
655
+ function renderWorkspaceControls() {
656
+ const repositoryActive = workspaceKind === "repository";
657
+ elements.kindRepository.checked = repositoryActive;
658
+ elements.kindStandalone.checked = !repositoryActive;
659
+ elements.repositoryFields.hidden = !repositoryActive;
660
+ elements.standaloneFields.hidden = repositoryActive;
661
+ elements.repositoryInput.required = repositoryActive;
662
+ elements.relativeInput.required = repositoryActive;
663
+ elements.standaloneInput.required = !repositoryActive;
664
+ renderRecentWorkspaces();
665
+ renderCheckout();
666
+ elements.checkoutSelect.disabled = workspacePending || !repositorySelection;
667
+ repositoryCombobox.setDisabled(workspacePending);
668
+ relativeCombobox.setDisabled(workspacePending || !repositorySelection);
669
+ standaloneCombobox.setDisabled(workspacePending);
670
+ renderWorktreeMode();
671
+ renderWorktreeTarget();
672
+ const validSelection = Boolean(currentCwd());
673
+ elements.createSubmit.disabled = creatingSession || workspacePending || !validSelection;
674
+ elements.worktreeSubmit.disabled = creatingSession || workspacePending || !repositorySelection?.selectedInspection.creation.available;
675
+ elements.newSession.setAttribute("aria-busy", String(creatingSession || workspacePending));
676
+ renderNewSessionDisclosure();
677
+ }
678
+ function applyRepositorySelection(repositoryInspection, selectedInspection) {
679
+ const context = selectedInspection.context;
680
+ if (!context) {
681
+ throw new Error("This directory is not inside a Git repository.");
682
+ }
683
+ repositorySelection = {
684
+ repositoryRoot: context.repositoryRoot,
685
+ repositoryInspection,
686
+ selectedInspection,
687
+ worktreeRoot: context.worktreeRoot,
688
+ relativeCwd: context.relativeCwd,
689
+ cwd: selectedInspection.cwd
690
+ };
691
+ workspaceKind = "repository";
692
+ elements.repositoryInput.value = context.repositoryRoot;
693
+ elements.relativeInput.value = displayRelativePath(context.relativeCwd);
694
+ closeWorktreeEditor();
695
+ }
696
+ async function inspectRepositoryPath(path, preserveDetectedPath = false) {
697
+ const requested = path.trim();
698
+ const generation = ++inspectionGeneration;
699
+ if (!requested) {
700
+ setFeedback("repository", "Enter an absolute repository path.", true);
701
+ return false;
702
+ }
703
+ if (!preserveDetectedPath) {
704
+ repositorySelection = null;
705
+ elements.relativeInput.value = "(root)";
706
+ closeWorktreeEditor();
707
+ setFeedback("checkout");
708
+ setFeedback("relative");
709
+ }
710
+ workspacePending = true;
711
+ setFeedback("repository", "Inspecting repository\u2026");
712
+ setGitStatus("Inspecting repository\u2026");
713
+ renderWorkspaceControls();
714
+ try {
715
+ const detectedInspection = await requestInspection(requested);
716
+ const detectedContext = detectedInspection.context;
717
+ if (!detectedContext) {
718
+ throw new Error("This directory is not inside a Git repository.");
719
+ }
720
+ const repositoryInspection = detectedInspection.cwd === detectedContext.repositoryRoot ? detectedInspection : await requestInspection(detectedContext.repositoryRoot);
721
+ if (generation !== inspectionGeneration) {
722
+ return false;
723
+ }
724
+ if (!repositoryInspection.context || repositoryInspection.context.repositoryRoot !== detectedContext.repositoryRoot) {
725
+ throw new Error("The repository checkout could not be inspected.");
726
+ }
727
+ let selectedInspection = detectedInspection;
728
+ if (!preserveDetectedPath) {
729
+ const primary = primaryWorktree(repositoryInspection);
730
+ if (!primary) {
731
+ throw new Error("No usable checkout was found for this repository.");
732
+ }
733
+ selectedInspection = primary.cwd === repositoryInspection.cwd ? repositoryInspection : await requestInspection(primary.cwd);
734
+ }
735
+ if (generation !== inspectionGeneration) {
736
+ return false;
737
+ }
738
+ applyRepositorySelection(repositoryInspection, selectedInspection);
739
+ clearFeedback();
740
+ setFeedback("repository", detectedContext.repositoryRoot);
741
+ setGitStatus(`Git repository selected: ${detectedContext.repositoryRoot}.`);
742
+ return true;
743
+ } catch (error) {
744
+ if (generation !== inspectionGeneration) {
745
+ return false;
746
+ }
747
+ setFeedback("repository", readableError(error), true);
748
+ setGitStatus(readableError(error), true);
749
+ return false;
750
+ } finally {
751
+ if (generation === inspectionGeneration) {
752
+ workspacePending = false;
753
+ renderWorkspaceControls();
754
+ }
755
+ }
756
+ }
757
+ async function inspectStandalonePath(path) {
758
+ const requested = path.trim();
759
+ const generation = ++inspectionGeneration;
760
+ if (!requested) {
761
+ setFeedback("standalone", "Enter an absolute directory path.", true);
762
+ return false;
763
+ }
764
+ standaloneSelection = null;
765
+ closeWorktreeEditor();
766
+ workspacePending = true;
767
+ setFeedback("standalone", "Inspecting directory\u2026");
768
+ setGitStatus("Inspecting directory\u2026");
769
+ renderWorkspaceControls();
770
+ try {
771
+ const inspection = await requestInspection(requested);
772
+ if (generation !== inspectionGeneration) {
773
+ return false;
774
+ }
775
+ if (inspection.context) {
776
+ const repositoryInspection = inspection.cwd === inspection.context.repositoryRoot ? inspection : await requestInspection(inspection.context.repositoryRoot);
777
+ if (generation !== inspectionGeneration) {
778
+ return false;
779
+ }
780
+ applyRepositorySelection(repositoryInspection, inspection);
781
+ clearFeedback();
782
+ setFeedback("repository", "Git repository detected. Switched from standalone directory.");
783
+ setGitStatus(`Git repository detected: ${inspection.context.repositoryRoot}.`);
784
+ return true;
785
+ }
786
+ standaloneSelection = { inspection, cwd: inspection.cwd };
787
+ workspaceKind = "standalone";
788
+ elements.standaloneInput.value = inspection.cwd;
789
+ closeWorktreeEditor();
790
+ clearFeedback();
791
+ setFeedback("standalone", inspection.cwd);
792
+ setGitStatus(`Standalone directory selected: ${inspection.cwd}.`);
793
+ return true;
794
+ } catch (error) {
795
+ if (generation !== inspectionGeneration) {
796
+ return false;
797
+ }
798
+ setFeedback("standalone", readableError(error), true);
799
+ setGitStatus(readableError(error), true);
800
+ return false;
801
+ } finally {
802
+ if (generation === inspectionGeneration) {
803
+ workspacePending = false;
804
+ renderWorkspaceControls();
805
+ }
806
+ }
807
+ }
808
+ async function inspectRelativePath(value) {
809
+ const selection = repositorySelection;
810
+ if (!selection) {
811
+ return false;
812
+ }
813
+ let relativeCwd;
814
+ try {
815
+ relativeCwd = normalizeRelativePath(value, selection.worktreeRoot);
816
+ } catch (error) {
817
+ setFeedback("relative", readableError(error), true);
818
+ return false;
819
+ }
820
+ const generation = ++inspectionGeneration;
821
+ workspacePending = true;
822
+ setFeedback("relative", "Inspecting working directory\u2026");
823
+ renderWorkspaceControls();
824
+ try {
825
+ const inspection = await requestInspection(joinServerPath(selection.worktreeRoot, relativeCwd));
826
+ const context = inspection.context;
827
+ if (!context || context.repositoryRoot !== selection.repositoryRoot || context.worktreeRoot !== selection.worktreeRoot) {
828
+ throw new Error("Working directory must be inside the selected checkout.");
829
+ }
830
+ if (generation !== inspectionGeneration) {
831
+ return false;
832
+ }
833
+ repositorySelection = {
834
+ ...selection,
835
+ selectedInspection: inspection,
836
+ relativeCwd: context.relativeCwd,
837
+ cwd: inspection.cwd
838
+ };
839
+ elements.relativeInput.value = displayRelativePath(context.relativeCwd);
840
+ closeWorktreeEditor();
841
+ setFeedback("relative", inspection.cwd);
842
+ setGitStatus(`Working directory selected: ${inspection.cwd}.`);
843
+ return true;
844
+ } catch (error) {
845
+ if (generation !== inspectionGeneration) {
846
+ return false;
847
+ }
848
+ setFeedback("relative", readableError(error), true);
849
+ setGitStatus(readableError(error), true);
850
+ return false;
851
+ } finally {
852
+ if (generation === inspectionGeneration) {
853
+ workspacePending = false;
854
+ renderWorkspaceControls();
855
+ }
856
+ }
857
+ }
858
+ async function selectCheckout() {
859
+ const selection = repositorySelection;
860
+ const worktree = selection?.repositoryInspection.worktrees.find(
861
+ (candidate) => candidate.worktreeRoot === elements.checkoutSelect.value
862
+ );
863
+ if (!selection || !worktree) {
864
+ return;
865
+ }
866
+ const generation = ++inspectionGeneration;
867
+ workspacePending = true;
868
+ closeWorktreeEditor();
869
+ elements.relativeInput.value = "(root)";
870
+ repositorySelection = {
871
+ ...selection,
872
+ worktreeRoot: worktree.worktreeRoot,
873
+ relativeCwd: ".",
874
+ cwd: worktree.worktreeRoot
875
+ };
876
+ setFeedback("relative");
877
+ setFeedback("checkout", "Inspecting checkout\u2026");
878
+ renderWorkspaceControls();
879
+ try {
880
+ const inspection = await requestInspection(worktree.worktreeRoot);
881
+ if (generation !== inspectionGeneration) {
882
+ return;
883
+ }
884
+ const context = inspection.context;
885
+ if (!context || context.repositoryRoot !== selection.repositoryRoot || context.worktreeRoot !== worktree.worktreeRoot) {
886
+ throw new Error("The selected checkout could not be inspected.");
887
+ }
888
+ repositorySelection = {
889
+ ...selection,
890
+ selectedInspection: inspection,
891
+ worktreeRoot: context.worktreeRoot,
892
+ relativeCwd: ".",
893
+ cwd: inspection.cwd
894
+ };
895
+ elements.relativeInput.value = "(root)";
896
+ setFeedback("checkout", inspection.cwd);
897
+ setGitStatus(`Checkout selected: ${context.worktreeRoot}.`);
898
+ } catch (error) {
899
+ if (generation !== inspectionGeneration) {
900
+ return;
901
+ }
902
+ repositorySelection = selection;
903
+ elements.relativeInput.value = displayRelativePath(selection.relativeCwd);
904
+ setFeedback("checkout", readableError(error), true);
905
+ setGitStatus(readableError(error), true);
906
+ } finally {
907
+ if (generation === inspectionGeneration) {
908
+ workspacePending = false;
909
+ renderWorkspaceControls();
910
+ }
911
+ }
912
+ }
913
+ var repositoryCombobox = new PathCombobox({
914
+ input: elements.repositoryInput,
915
+ list: elements.repositoryList,
916
+ status: elements.repositoryStatus,
917
+ toggle: elements.repositoryToggle,
918
+ shortcuts: () => [],
919
+ load: async (value, signal) => absoluteDirectorySuggestions(await browseDirectories(value, signal)),
920
+ commit: (value) => inspectRepositoryPath(value)
921
+ });
922
+ var standaloneCombobox = new PathCombobox({
923
+ input: elements.standaloneInput,
924
+ list: elements.standaloneList,
925
+ status: elements.standaloneStatus,
926
+ toggle: elements.standaloneToggle,
927
+ shortcuts: () => [],
928
+ load: async (value, signal) => absoluteDirectorySuggestions(await browseDirectories(value, signal)),
929
+ commit: (value) => inspectStandalonePath(value)
930
+ });
931
+ var relativeCombobox = new PathCombobox({
932
+ input: elements.relativeInput,
933
+ list: elements.relativeList,
934
+ status: elements.relativeStatus,
935
+ toggle: elements.relativeToggle,
936
+ shortcuts: relativeShortcuts,
937
+ load: async (value, signal) => {
938
+ const selection = repositorySelection;
939
+ if (!selection) {
940
+ return [];
941
+ }
942
+ const normalized = normalizeRelativePath(value, selection.worktreeRoot);
943
+ const response = await browseDirectories(joinServerPath(selection.worktreeRoot, normalized), signal);
944
+ return response.directories.flatMap((directory) => {
945
+ const relative = relativePathWithin(selection.worktreeRoot, directory.path);
946
+ return relative ? [
947
+ {
948
+ value: displayRelativePath(relative),
949
+ label: displayRelativePath(relative),
950
+ detail: directory.path,
951
+ source: "server"
952
+ }
953
+ ] : [];
954
+ });
955
+ },
956
+ commit: (value) => inspectRelativePath(value)
957
+ });
958
+ function sessionDeleteButton(session) {
959
+ const running = listing.runningSessionIds.includes(session.id);
960
+ const title = displaySessionTitle(session);
961
+ const button = document.createElement("button");
962
+ button.className = "session-delete destructive";
963
+ button.type = "button";
964
+ button.textContent = "Delete";
965
+ button.title = running ? "Wait for the session to finish before deleting it" : "Permanently delete session";
966
+ button.setAttribute("aria-label", `Permanently delete session ${title}`);
967
+ button.disabled = running || deletingSessionIds.has(session.id);
968
+ button.addEventListener("click", () => void deleteSession(session));
969
+ return button;
970
+ }
971
+ function renderSessionCard(session) {
972
+ const running = listing.runningSessionIds.includes(session.id);
973
+ const title = displaySessionTitle(session);
974
+ const card = document.createElement("div");
975
+ card.className = "session-card";
976
+ const link = document.createElement("a");
977
+ link.className = "session-button";
978
+ link.href = sessionPath(session.id);
979
+ link.title = title;
980
+ const dot = document.createElement("span");
981
+ dot.className = `running-dot${running ? " active" : ""}`;
982
+ dot.setAttribute("aria-label", running ? "Running" : "Idle");
983
+ const copy = document.createElement("span");
984
+ copy.className = "session-copy";
985
+ copy.append(
986
+ textElement("strong", "", title),
987
+ textElement("span", "session-meta", `${formatTime(session.modified)} \xB7 ${session.messageCount} messages`)
988
+ );
989
+ link.append(dot, copy);
990
+ card.append(link, sessionDeleteButton(session));
991
+ return card;
992
+ }
993
+ function contextualCreateButton(cwd) {
994
+ const button = document.createElement("button");
995
+ button.className = "contextual-session-create";
996
+ button.type = "button";
997
+ button.textContent = contextualCreationCwd === cwd ? "Creating\u2026" : "+ New session";
998
+ button.title = `Create a new session in ${cwd}`;
999
+ button.setAttribute("aria-label", `Create a new session in ${cwd}`);
1000
+ button.disabled = creatingSession;
1001
+ button.addEventListener("click", () => void createContextualSession(cwd));
1002
+ return button;
1003
+ }
1004
+ function repositoryWorktreeTarget(repositoryRoot, name) {
1005
+ const separator = pathSeparator(repositoryRoot);
1006
+ return `${trimTrailingSeparators(repositoryRoot)}${separator}.pi${separator}worktrees${separator}${name || "<name>"}`;
1007
+ }
1008
+ function closeRepositoryWorktreeEditor(editorId) {
1009
+ repositoryWorktreeEditorRoot = null;
1010
+ repositoryWorktreeName = "";
1011
+ renderSessions();
1012
+ document.getElementById(`${editorId}-toggle`)?.focus();
1013
+ }
1014
+ function repositoryWorktreeButton(group, editorId) {
1015
+ const button = document.createElement("button");
1016
+ button.id = `${editorId}-toggle`;
1017
+ button.className = "repository-worktree-start";
1018
+ button.type = "button";
1019
+ button.textContent = "+ New worktree";
1020
+ button.title = `Create a new worktree and session in ${group.repositoryRoot}`;
1021
+ button.setAttribute("aria-label", `Create a new worktree and session in ${group.repositoryRoot}`);
1022
+ button.setAttribute("aria-controls", editorId);
1023
+ button.setAttribute("aria-expanded", String(repositoryWorktreeEditorRoot === group.repositoryRoot));
1024
+ button.disabled = creatingSession;
1025
+ button.addEventListener("click", () => {
1026
+ if (repositoryWorktreeEditorRoot === group.repositoryRoot) {
1027
+ document.querySelector(`#${editorId} input`)?.focus();
1028
+ return;
1029
+ }
1030
+ repositoryWorktreeEditorRoot = group.repositoryRoot;
1031
+ repositoryWorktreeName = "";
1032
+ renderSessions();
1033
+ document.querySelector(`#${editorId} input`)?.focus();
1034
+ });
1035
+ return button;
1036
+ }
1037
+ function renderRepositoryWorktreeEditor(group, editorId) {
1038
+ const form = document.createElement("form");
1039
+ form.id = editorId;
1040
+ form.className = "repository-worktree-editor";
1041
+ form.setAttribute("aria-busy", String(repositoryWorktreeCreationRoot === group.repositoryRoot));
1042
+ const header = document.createElement("header");
1043
+ const headingId = `${editorId}-heading`;
1044
+ const heading = textElement("h4", "", "New worktree");
1045
+ heading.id = headingId;
1046
+ form.setAttribute("aria-labelledby", headingId);
1047
+ const cancel = document.createElement("button");
1048
+ cancel.className = "quiet-action";
1049
+ cancel.type = "button";
1050
+ cancel.textContent = "Cancel";
1051
+ cancel.disabled = creatingSession;
1052
+ cancel.addEventListener("click", () => closeRepositoryWorktreeEditor(editorId));
1053
+ header.append(heading, cancel);
1054
+ const label = document.createElement("label");
1055
+ const inputId = `${editorId}-name`;
1056
+ label.htmlFor = inputId;
1057
+ label.textContent = "Worktree and branch name";
1058
+ const input = document.createElement("input");
1059
+ input.id = inputId;
1060
+ input.name = "worktreeName";
1061
+ input.autocomplete = "off";
1062
+ input.spellcheck = false;
1063
+ input.required = true;
1064
+ input.disabled = creatingSession;
1065
+ input.value = repositoryWorktreeName;
1066
+ input.addEventListener("input", () => {
1067
+ repositoryWorktreeName = input.value;
1068
+ target.textContent = repositoryWorktreeTarget(group.repositoryRoot, repositoryWorktreeName.trim());
1069
+ });
1070
+ input.addEventListener("keydown", (event) => {
1071
+ if (event.key !== "Escape" || creatingSession) {
1072
+ return;
1073
+ }
1074
+ event.preventDefault();
1075
+ closeRepositoryWorktreeEditor(editorId);
1076
+ });
1077
+ const preview = document.createElement("p");
1078
+ preview.className = "worktree-target";
1079
+ preview.append("Create at ");
1080
+ const target = document.createElement("span");
1081
+ target.textContent = repositoryWorktreeTarget(group.repositoryRoot, repositoryWorktreeName.trim());
1082
+ preview.append(target);
1083
+ const submit = document.createElement("button");
1084
+ submit.className = "session-create-submit";
1085
+ submit.type = "submit";
1086
+ submit.textContent = repositoryWorktreeCreationRoot === group.repositoryRoot ? "Creating\u2026" : "Create worktree & session";
1087
+ submit.disabled = creatingSession;
1088
+ form.append(header, label, input, preview, submit);
1089
+ form.addEventListener("submit", (event) => {
1090
+ event.preventDefault();
1091
+ void createSessionAt(group.repositoryRoot, {
1092
+ worktreeName: repositoryWorktreeName,
1093
+ repositoryEditorRoot: group.repositoryRoot
1094
+ });
1095
+ });
1096
+ return form;
1097
+ }
1098
+ function renderGroupHeading(name, path, id, action) {
1099
+ const header = document.createElement("header");
1100
+ header.className = "session-group-header";
1101
+ const identity = document.createElement("div");
1102
+ identity.className = "session-group-identity";
1103
+ const title = document.createElement("h3");
1104
+ title.id = id;
1105
+ title.title = path;
1106
+ title.append(document.createTextNode(path.slice(0, Math.max(0, path.length - name.length))));
1107
+ title.append(textElement("strong", "", name));
1108
+ identity.append(title);
1109
+ header.append(identity);
1110
+ if (action) {
1111
+ header.append(action);
1112
+ }
1113
+ return header;
1114
+ }
1115
+ function worktreeCwdLabel(group, worktree, relativeCwd) {
1116
+ if (!worktree.isLinkedWorktree) {
1117
+ return displayRelativePath(relativeCwd);
1118
+ }
1119
+ const relativeWorktree = relativePathWithin(group.repositoryRoot, worktree.worktreeRoot);
1120
+ const worktreePath = relativeWorktree && relativeWorktree !== "." ? relativeWorktree : worktree.worktreeRoot;
1121
+ return relativeCwd === "." ? worktreePath : joinServerPath(worktreePath, relativeCwd);
1122
+ }
1123
+ function renderWorktree(group, worktree) {
1124
+ const container = document.createElement("section");
1125
+ container.className = "worktree-group";
1126
+ const sessions = group.sessions.filter((session) => session.workspaceContext?.worktreeRoot === worktree.worktreeRoot);
1127
+ const cwdHeading = (relativeCwd, includeDelete) => {
1128
+ const cwd = joinServerPath(worktree.worktreeRoot, relativeCwd);
1129
+ const header = document.createElement("header");
1130
+ header.className = "worktree-header";
1131
+ const identity = document.createElement("div");
1132
+ identity.className = "worktree-identity";
1133
+ identity.title = cwd;
1134
+ identity.append(textElement("h5", "relative-cwd-heading", worktreeCwdLabel(group, worktree, relativeCwd)));
1135
+ const context = document.createElement("div");
1136
+ context.className = "worktree-heading-context";
1137
+ context.append(identity, contextualCreateButton(cwd));
1138
+ header.append(context);
1139
+ if (includeDelete) {
1140
+ const active = sessions.some((session) => listing.activeSessionIds.includes(session.id));
1141
+ const removalPending = removingWorktreeRoots.size > 0;
1142
+ const remove = document.createElement("button");
1143
+ remove.className = "worktree-delete";
1144
+ remove.type = "button";
1145
+ remove.textContent = removingWorktreeRoots.has(worktree.worktreeRoot) ? "Deleting\u2026" : "Delete worktree";
1146
+ remove.disabled = active || removalPending;
1147
+ remove.title = active ? "Wait for active sessions in this worktree to close before deleting it" : `Delete linked worktree at ${worktree.worktreeRoot}`;
1148
+ remove.setAttribute("aria-label", `Delete linked worktree ${worktreeHeadLabel(worktree)}`);
1149
+ remove.addEventListener("click", () => void removeWorktree(group, worktree));
1150
+ header.append(remove);
1151
+ }
1152
+ return header;
1153
+ };
1154
+ const cards = document.createElement("div");
1155
+ cards.className = "worktree-sessions";
1156
+ if (sessions.length === 0) {
1157
+ cards.append(
1158
+ cwdHeading(".", worktree.isLinkedWorktree),
1159
+ textElement("p", "worktree-empty", "No saved sessions in this worktree.")
1160
+ );
1161
+ } else {
1162
+ const sessionsByRelativeCwd = /* @__PURE__ */ new Map();
1163
+ for (const session of sessions) {
1164
+ const relativeCwd = session.workspaceContext?.relativeCwd ?? ".";
1165
+ const relativeSessions = sessionsByRelativeCwd.get(relativeCwd) ?? [];
1166
+ relativeSessions.push(session);
1167
+ sessionsByRelativeCwd.set(relativeCwd, relativeSessions);
1168
+ }
1169
+ const relativeGroups = [...sessionsByRelativeCwd].sort(([left], [right]) => compareRelativePaths(left, right));
1170
+ for (const [index, [relativeCwd, relativeSessions]] of relativeGroups.entries()) {
1171
+ const relativeGroup = document.createElement("section");
1172
+ relativeGroup.className = "relative-cwd-group";
1173
+ relativeGroup.append(
1174
+ cwdHeading(relativeCwd, worktree.isLinkedWorktree && index === 0),
1175
+ ...relativeSessions.sort(compareSessionsByTitle).map((session) => renderSessionCard(session))
1176
+ );
1177
+ cards.append(relativeGroup);
1178
+ }
1179
+ }
1180
+ container.append(cards);
1181
+ return container;
1182
+ }
1183
+ function allSessions() {
1184
+ return listing.groups.flatMap((group) => group.sessions);
1185
+ }
1186
+ function renderLatestSession(session) {
1187
+ const running = listing.runningSessionIds.includes(session.id);
1188
+ const title = displaySessionTitle(session);
1189
+ const row = document.createElement("div");
1190
+ row.className = "latest-session-card";
1191
+ const link = document.createElement("a");
1192
+ link.className = "latest-session-button";
1193
+ link.href = sessionPath(session.id);
1194
+ link.title = title;
1195
+ const dot = document.createElement("span");
1196
+ dot.className = `running-dot${running ? " active" : ""}`;
1197
+ dot.setAttribute("aria-label", running ? "Running" : "Idle");
1198
+ const copy = document.createElement("span");
1199
+ copy.className = "session-copy";
1200
+ copy.append(
1201
+ textElement("strong", "", title),
1202
+ textElement("span", "session-meta", `${formatTime(session.modified)} \xB7 ${session.messageCount} messages`),
1203
+ textElement("span", "latest-session-location", session.cwd)
1204
+ );
1205
+ link.append(dot, copy);
1206
+ row.append(link, sessionDeleteButton(session));
1207
+ return row;
1208
+ }
1209
+ function renderSessions() {
1210
+ const sessions = allSessions();
1211
+ const latest = sessions.sort(compareLatestSessions).slice(0, 5);
1212
+ elements.latestSessionList.replaceChildren(
1213
+ ...latest.length > 0 ? latest.map((session) => renderLatestSession(session)) : [textElement("p", "empty-list latest-empty", "No saved sessions yet.")]
1214
+ );
1215
+ const groups = document.createDocumentFragment();
1216
+ const orderedGroups = [...listing.groups].sort((left, right) => compareText(left.cwd, right.cwd));
1217
+ for (const [index, group] of orderedGroups.entries()) {
1218
+ const section = document.createElement("section");
1219
+ section.className = `session-group ${group.type}`;
1220
+ const headingId = `session-group-${index}`;
1221
+ section.setAttribute("aria-labelledby", headingId);
1222
+ if (group.type === "repository") {
1223
+ const editorId = `${headingId}-new-worktree`;
1224
+ section.append(
1225
+ renderGroupHeading(
1226
+ repositoryName(group.repositoryRoot),
1227
+ group.repositoryRoot,
1228
+ headingId,
1229
+ repositoryWorktreeButton(group, editorId)
1230
+ )
1231
+ );
1232
+ if (repositoryWorktreeEditorRoot === group.repositoryRoot) {
1233
+ section.append(renderRepositoryWorktreeEditor(group, editorId));
1234
+ }
1235
+ const worktrees = document.createElement("div");
1236
+ worktrees.className = "worktree-list";
1237
+ worktrees.append(
1238
+ ...[...group.worktrees].sort(compareWorktrees).map((worktree) => renderWorktree(group, worktree))
1239
+ );
1240
+ section.append(worktrees);
1241
+ } else {
1242
+ section.append(
1243
+ renderGroupHeading(repositoryName(group.cwd), group.cwd, headingId, contextualCreateButton(group.cwd))
1244
+ );
1245
+ const cards = document.createElement("div");
1246
+ cards.className = "standalone-sessions";
1247
+ cards.append(...[...group.sessions].sort(compareSessionsByTitle).map((session) => renderSessionCard(session)));
1248
+ section.append(cards);
1249
+ }
1250
+ groups.append(section);
1251
+ }
1252
+ if (orderedGroups.length === 0) {
1253
+ groups.append(textElement("p", "empty-list", "No saved sessions yet. Create one above to get started."));
1254
+ }
1255
+ elements.sessionList.replaceChildren(groups);
1256
+ renderRecentWorkspaces();
1257
+ relativeCombobox.refreshShortcuts();
1258
+ }
1259
+ async function refreshSessions(showStatus = true, statusKey = "session-listing") {
1260
+ try {
1261
+ listing = await api("/api/sessions");
1262
+ renderSessions();
1263
+ if (showStatus) {
1264
+ clearAppStatus(statusKey);
1265
+ }
1266
+ return true;
1267
+ } catch (error) {
1268
+ setAppStatus(statusKey, readableError(error), "error");
1269
+ return false;
1270
+ }
1271
+ }
1272
+ async function refreshRepositorySelectionAfterRemoval(repositoryRoot, removedWorktreeRoot) {
1273
+ const selection = repositorySelection;
1274
+ if (!selection || selection.repositoryRoot !== repositoryRoot) {
1275
+ return;
1276
+ }
1277
+ const repositoryInspection = await requestInspection(repositoryRoot);
1278
+ if (!repositoryInspection.context) {
1279
+ repositorySelection = null;
1280
+ renderWorkspaceControls();
1281
+ return;
1282
+ }
1283
+ if (selection.worktreeRoot !== removedWorktreeRoot) {
1284
+ repositorySelection = { ...selection, repositoryInspection };
1285
+ renderWorkspaceControls();
1286
+ return;
1287
+ }
1288
+ const fallback = primaryWorktree(repositoryInspection);
1289
+ if (!fallback) {
1290
+ repositorySelection = null;
1291
+ renderWorkspaceControls();
1292
+ return;
1293
+ }
1294
+ const selectedInspection = fallback.cwd === repositoryInspection.cwd ? repositoryInspection : await requestInspection(fallback.cwd);
1295
+ applyRepositorySelection(repositoryInspection, selectedInspection);
1296
+ renderWorkspaceControls();
1297
+ }
1298
+ async function removeWorktree(group, worktree) {
1299
+ if (!worktree.isLinkedWorktree || removingWorktreeRoots.size > 0) {
1300
+ return;
1301
+ }
1302
+ const label = worktreeHeadLabel(worktree);
1303
+ const statusKey = `worktree-removal:${worktree.worktreeRoot}`;
1304
+ const confirmed = confirm(
1305
+ `Delete linked worktree \u201C${label}\u201D?
1306
+
1307
+ This deletes the checkout at ${worktree.worktreeRoot}. Git will refuse if it contains modified or untracked files. Git branches and saved Pi sessions are kept.`
1308
+ );
1309
+ if (!confirmed) {
1310
+ return;
1311
+ }
1312
+ removingWorktreeRoots.add(worktree.worktreeRoot);
1313
+ renderSessions();
1314
+ try {
1315
+ await api("/api/git-workspaces/worktrees", {
1316
+ method: "DELETE",
1317
+ headers: { "content-type": "application/json" },
1318
+ body: JSON.stringify({ cwd: group.repositoryRoot, worktreeRoot: worktree.worktreeRoot })
1319
+ });
1320
+ const [sessionsRefreshed] = await Promise.all([
1321
+ refreshSessions(false, statusKey),
1322
+ refreshRepositorySelectionAfterRemoval(group.repositoryRoot, worktree.worktreeRoot)
1323
+ ]);
1324
+ if (sessionsRefreshed) {
1325
+ setAppStatus(statusKey, `Deleted linked worktree at ${worktree.worktreeRoot}.`, "success");
1326
+ }
1327
+ } catch (error) {
1328
+ setAppStatus(statusKey, readableError(error), "error");
1329
+ } finally {
1330
+ removingWorktreeRoots.delete(worktree.worktreeRoot);
1331
+ renderSessions();
1332
+ }
1333
+ }
1334
+ async function deleteSession(session) {
1335
+ const title = displaySessionTitle(session);
1336
+ const statusKey = `session-deletion:${session.id}`;
1337
+ if (!confirm(`Permanently delete \u201C${title}\u201D? This cannot be undone.`)) {
1338
+ return;
1339
+ }
1340
+ deletingSessionIds.add(session.id);
1341
+ renderSessions();
1342
+ try {
1343
+ await api(`/api/sessions/${encodeURIComponent(session.id)}`, { method: "DELETE" });
1344
+ if (await refreshSessions(false, statusKey)) {
1345
+ setAppStatus(statusKey, `Session \u201C${title}\u201D deleted`, "success");
1346
+ }
1347
+ } catch (error) {
1348
+ setAppStatus(statusKey, readableError(error), "error");
1349
+ } finally {
1350
+ deletingSessionIds.delete(session.id);
1351
+ renderSessions();
1352
+ }
1353
+ }
1354
+ async function commitEditedWorkspace() {
1355
+ if (workspaceKind === "standalone") {
1356
+ return elements.standaloneInput.value.trim() === standaloneSelection?.cwd ? true : inspectStandalonePath(elements.standaloneInput.value);
1357
+ }
1358
+ const requestedRepository = elements.repositoryInput.value.trim();
1359
+ if (requestedRepository !== repositorySelection?.repositoryRoot) {
1360
+ return inspectRepositoryPath(requestedRepository);
1361
+ }
1362
+ if (elements.relativeInput.value.trim() !== displayRelativePath(repositorySelection?.relativeCwd ?? ".")) {
1363
+ return inspectRelativePath(elements.relativeInput.value);
1364
+ }
1365
+ return true;
1366
+ }
1367
+ async function createSessionAt(cwd, options = {}) {
1368
+ if (creatingSession) {
1369
+ return;
1370
+ }
1371
+ creatingSession = true;
1372
+ contextualCreationCwd = options.contextualCwd ?? null;
1373
+ repositoryWorktreeCreationRoot = options.repositoryEditorRoot ?? null;
1374
+ renderWorkspaceControls();
1375
+ renderSessions();
1376
+ try {
1377
+ const body = options.worktreeName ? { cwd, worktreeName: options.worktreeName } : { cwd };
1378
+ const response = await api("/api/sessions", {
1379
+ method: "POST",
1380
+ headers: { "content-type": "application/json" },
1381
+ body: JSON.stringify(body)
1382
+ });
1383
+ location.assign(sessionPath(response.session.id));
1384
+ } catch (error) {
1385
+ creatingSession = false;
1386
+ contextualCreationCwd = null;
1387
+ repositoryWorktreeCreationRoot = null;
1388
+ renderWorkspaceControls();
1389
+ renderSessions();
1390
+ setAppStatus("session-creation", readableError(error), "error");
1391
+ }
1392
+ }
1393
+ async function createContextualSession(cwd) {
1394
+ await createSessionAt(cwd, { contextualCwd: cwd });
1395
+ }
1396
+ async function createSession() {
1397
+ if (creatingSession || workspacePending) {
1398
+ return;
1399
+ }
1400
+ if (!await commitEditedWorkspace()) {
1401
+ return;
1402
+ }
1403
+ const createWorktree = newWorktreeOpen && workspaceKind === "repository";
1404
+ const cwd = currentCwd();
1405
+ if (!cwd || !elements.newSession.reportValidity()) {
1406
+ return;
1407
+ }
1408
+ await createSessionAt(cwd, createWorktree ? { worktreeName: elements.worktreeName.value } : {});
1409
+ }
1410
+ function sourceSessionId() {
1411
+ const match = /^\/sessions\/([^/]+)\/new\/?$/.exec(location.pathname);
1412
+ if (!match?.[1]) {
1413
+ return null;
1414
+ }
1415
+ try {
1416
+ return decodeURIComponent(match[1]);
1417
+ } catch {
1418
+ return null;
1419
+ }
1420
+ }
1421
+ async function initializeWorkspace() {
1422
+ if (workspaceInitialized) {
1423
+ return;
1424
+ }
1425
+ workspaceInitialized = true;
1426
+ let initialCwd = "";
1427
+ let initialCwdFromSession = false;
1428
+ const sourceId = sourceSessionId();
1429
+ if (sourceId) {
1430
+ try {
1431
+ const response = await api(`/api/sessions/${encodeURIComponent(sourceId)}`);
1432
+ initialCwd = response.session.cwd;
1433
+ initialCwdFromSession = true;
1434
+ } catch (error) {
1435
+ setAppStatus("source-session", readableError(error), "error");
1436
+ }
1437
+ }
1438
+ if (!initialCwd) {
1439
+ initialCwd = listing.knownCwds[0] ?? "";
1440
+ }
1441
+ if (!initialCwd) {
1442
+ renderWorkspaceControls();
1443
+ return;
1444
+ }
1445
+ const initialGeneration = ++inspectionGeneration;
1446
+ try {
1447
+ const inspection = await requestInspection(initialCwd);
1448
+ if (initialGeneration === inspectionGeneration && inspection.context) {
1449
+ elements.repositoryInput.value = inspection.context.repositoryRoot;
1450
+ await inspectRepositoryPath(initialCwd, initialCwdFromSession);
1451
+ } else if (initialGeneration === inspectionGeneration) {
1452
+ workspaceKind = "standalone";
1453
+ elements.standaloneInput.value = initialCwd;
1454
+ await inspectStandalonePath(initialCwd);
1455
+ }
1456
+ } catch (error) {
1457
+ if (initialGeneration === inspectionGeneration) {
1458
+ setAppStatus("initial-workspace", readableError(error), "error");
1459
+ }
1460
+ }
1461
+ renderWorkspaceControls();
1462
+ }
1463
+ async function openNewSession(focusForm) {
1464
+ newSessionOpen = true;
1465
+ renderNewSessionDisclosure();
1466
+ await initializeWorkspace();
1467
+ if (focusForm) {
1468
+ const target = elements.recentWorkspace.disabled ? workspaceKind === "repository" ? elements.repositoryInput : elements.standaloneInput : elements.recentWorkspace;
1469
+ target.focus();
1470
+ }
1471
+ }
1472
+ function closeNewSession() {
1473
+ newSessionOpen = false;
1474
+ closeWorktreeEditor();
1475
+ renderWorkspaceControls();
1476
+ }
1477
+ function changeWorkspaceKind(kind) {
1478
+ ++inspectionGeneration;
1479
+ workspacePending = false;
1480
+ pendingRecentWorkspaceValue = null;
1481
+ workspaceKind = kind;
1482
+ repositorySelection = null;
1483
+ standaloneSelection = null;
1484
+ elements.repositoryInput.value = "";
1485
+ elements.relativeInput.value = "(root)";
1486
+ elements.standaloneInput.value = "";
1487
+ closeWorktreeEditor();
1488
+ clearFeedback();
1489
+ setGitStatus("");
1490
+ renderWorkspaceControls();
1491
+ }
1492
+ async function selectRecentWorkspace() {
1493
+ const value = elements.recentWorkspace.value;
1494
+ if (!value) {
1495
+ pendingRecentWorkspaceValue = null;
1496
+ return;
1497
+ }
1498
+ pendingRecentWorkspaceValue = value;
1499
+ repositorySelection = null;
1500
+ standaloneSelection = null;
1501
+ elements.repositoryInput.value = "";
1502
+ elements.relativeInput.value = "(root)";
1503
+ elements.standaloneInput.value = "";
1504
+ closeWorktreeEditor();
1505
+ try {
1506
+ if (value.startsWith("repository:")) {
1507
+ const repositoryRoot = value.slice("repository:".length);
1508
+ workspaceKind = "repository";
1509
+ elements.repositoryInput.value = repositoryRoot;
1510
+ clearFeedback();
1511
+ renderWorkspaceControls();
1512
+ await inspectRepositoryPath(repositoryRoot, false);
1513
+ return;
1514
+ }
1515
+ if (value.startsWith("standalone:")) {
1516
+ const cwd = value.slice("standalone:".length);
1517
+ workspaceKind = "standalone";
1518
+ elements.standaloneInput.value = cwd;
1519
+ clearFeedback();
1520
+ renderWorkspaceControls();
1521
+ await inspectStandalonePath(cwd);
1522
+ }
1523
+ } finally {
1524
+ if (pendingRecentWorkspaceValue === value) {
1525
+ pendingRecentWorkspaceValue = null;
1526
+ renderRecentWorkspaces();
1527
+ }
1528
+ }
1529
+ }
1530
+ elements.newSession.addEventListener("submit", (event) => {
1531
+ event.preventDefault();
1532
+ void createSession();
1533
+ });
1534
+ elements.newSessionToggle.addEventListener("click", () => void openNewSession(true));
1535
+ elements.newSessionCancel.addEventListener("click", () => {
1536
+ closeNewSession();
1537
+ elements.newSessionToggle.focus();
1538
+ });
1539
+ elements.kindRepository.addEventListener("change", () => {
1540
+ if (elements.kindRepository.checked) {
1541
+ changeWorkspaceKind("repository");
1542
+ }
1543
+ });
1544
+ elements.kindStandalone.addEventListener("change", () => {
1545
+ if (elements.kindStandalone.checked) {
1546
+ changeWorkspaceKind("standalone");
1547
+ }
1548
+ });
1549
+ elements.recentWorkspace.addEventListener("change", () => void selectRecentWorkspace());
1550
+ elements.repositoryInput.addEventListener("input", () => {
1551
+ if (elements.repositoryInput.value.trim() !== repositorySelection?.repositoryRoot) {
1552
+ ++inspectionGeneration;
1553
+ workspacePending = false;
1554
+ pendingRecentWorkspaceValue = null;
1555
+ repositorySelection = null;
1556
+ elements.relativeInput.value = "(root)";
1557
+ elements.recentWorkspace.value = "";
1558
+ closeWorktreeEditor();
1559
+ setFeedback("checkout");
1560
+ setFeedback("relative");
1561
+ renderWorkspaceControls();
1562
+ }
1563
+ });
1564
+ elements.standaloneInput.addEventListener("input", () => {
1565
+ if (elements.standaloneInput.value.trim() !== standaloneSelection?.cwd) {
1566
+ ++inspectionGeneration;
1567
+ workspacePending = false;
1568
+ pendingRecentWorkspaceValue = null;
1569
+ standaloneSelection = null;
1570
+ elements.recentWorkspace.value = "";
1571
+ closeWorktreeEditor();
1572
+ renderWorkspaceControls();
1573
+ }
1574
+ });
1575
+ elements.relativeInput.addEventListener("input", () => {
1576
+ if (newWorktreeOpen) {
1577
+ closeWorktreeEditor();
1578
+ renderWorkspaceControls();
1579
+ }
1580
+ });
1581
+ elements.checkoutSelect.addEventListener("change", () => void selectCheckout());
1582
+ elements.worktreeStart.addEventListener("click", () => {
1583
+ if (!repositorySelection?.selectedInspection.creation.available) {
1584
+ return;
1585
+ }
1586
+ newWorktreeOpen = true;
1587
+ elements.worktreeName.value = "";
1588
+ renderWorkspaceControls();
1589
+ elements.worktreeName.focus();
1590
+ });
1591
+ elements.worktreeCancel.addEventListener("click", () => {
1592
+ closeWorktreeEditor();
1593
+ renderWorkspaceControls();
1594
+ elements.worktreeStart.focus();
1595
+ });
1596
+ elements.worktreeName.addEventListener("input", renderWorktreeTarget);
1597
+ elements.worktreeName.addEventListener("keydown", (event) => {
1598
+ if (event.key !== "Escape") {
1599
+ return;
1600
+ }
1601
+ event.preventDefault();
1602
+ elements.worktreeCancel.click();
1603
+ });
1604
+ document.addEventListener("visibilitychange", () => {
1605
+ if (document.visibilityState === "visible") {
1606
+ void refreshSessions(false);
1607
+ }
1608
+ });
1609
+ clearFeedback();
1610
+ renderWorkspaceControls();
1611
+ await refreshSessions();
1612
+ if (sourceSessionId()) {
1613
+ await openNewSession(false);
1614
+ }
1615
+ setInterval(() => {
1616
+ if (document.visibilityState === "visible") {
1617
+ void refreshSessions(false);
1618
+ }
1619
+ }, 3e4);