dsh-worktree-switch 0.0.0-stage → 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.
package/lib/client.js ADDED
@@ -0,0 +1,1137 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-worktree-switch", factory: function (require) {
2
+ var module = { exports: {} };
3
+ var exports = module.exports;
4
+
5
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
+ let react = require("react");
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
10
+ // src/client/Chip.tsx
11
+ /**
12
+ * The New Session switch.
13
+ *
14
+ * The slot it occupies (`conversation.input.left`) renders in the composer of
15
+ * every open session, and gives its occupant no `variant`; the blank-session
16
+ * hero is instead recognised from the session itself, exactly as the shell
17
+ * recognises it: a session whose list summary says blank and whose history is
18
+ * open. Outside that state this component renders nothing at all — the spec's
19
+ * "no greyed remnant" — and it releases the composer block it raised.
20
+ * @module dsh-worktree-switch/client/Chip
21
+ */
22
+ /**
23
+ * The line the switch shows.
24
+ * @param state - current state.
25
+ * @param t - bound translate.
26
+ * @returns visible label.
27
+ */
28
+ function chipLabel(state, t) {
29
+ switch (state.phase) {
30
+ case "checking": return t("chip.checking");
31
+ case "off": return t("chip.off");
32
+ case "creating": return t("chip.creating");
33
+ case "reverting": return t("chip.reverting");
34
+ case "ready": return t("chip.ready", { name: state.worktree?.name ?? "" });
35
+ case "failed": return t("chip.failed");
36
+ case "disabled": return t("chip.disabled");
37
+ }
38
+ }
39
+ /**
40
+ * The hover text: why the switch is where it is, and what the copy is.
41
+ * @param state - current state.
42
+ * @param t - bound translate.
43
+ * @returns tooltip text.
44
+ */
45
+ function chipHint(state, t) {
46
+ switch (state.phase) {
47
+ case "checking": return t("chip.hint.checking");
48
+ case "off": return t("chip.hint.off");
49
+ case "creating": return t("chip.hint.creating");
50
+ case "reverting": return t("chip.hint.reverting");
51
+ case "ready": {
52
+ const parts = [t("chip.hint.ready", {
53
+ branch: state.worktree?.branch ?? "",
54
+ path: state.worktree?.path ?? ""
55
+ })];
56
+ if (!state.intent) parts.push(t("chip.hint.intentOff", { name: state.worktree?.name ?? "" }));
57
+ if (state.sourceDirty > 0) parts.push(t("chip.hint.dirty", { count: state.sourceDirty }));
58
+ if (state.error !== null) parts.push(state.error);
59
+ return parts.join(" ");
60
+ }
61
+ case "failed": {
62
+ const base = t("chip.hint.failed");
63
+ return state.error === null ? base : `${base} ${state.error}`;
64
+ }
65
+ case "disabled": return t("chip.hint.disabled");
66
+ }
67
+ }
68
+ /**
69
+ * Render the Worktree switch.
70
+ * @param props - slot runtime props, the binding face, and the locale seat.
71
+ * @returns the switch, or null outside the blank-session hero.
72
+ */
73
+ function WorktreeChip(props) {
74
+ const { useSession, useToggle, ensure, setIntent, retry, block, t } = props;
75
+ const hero = useSession((session) => session.blank && session.openState === "open");
76
+ const state = useToggle((current) => current);
77
+ (0, react.useEffect)(() => {
78
+ if (hero) ensure();
79
+ }, [ensure, hero]);
80
+ (0, react.useEffect)(() => {
81
+ if (!hero) {
82
+ block(null);
83
+ return;
84
+ }
85
+ const reason = state.phase === "checking" ? t("chip.blocked.checking") : state.phase === "creating" ? t("chip.blocked.creating") : state.phase === "reverting" ? t("chip.blocked.reverting") : state.phase === "failed" && state.intent ? t("chip.blocked.failed") : null;
86
+ block(reason);
87
+ }, [
88
+ block,
89
+ hero,
90
+ state.intent,
91
+ state.phase,
92
+ t
93
+ ]);
94
+ (0, react.useEffect)(() => () => {
95
+ block(null);
96
+ }, [block]);
97
+ if (!hero) return null;
98
+ const hint = chipHint(state, t);
99
+ const locked = state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting";
100
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
101
+ className: "dsh-wt-chip",
102
+ "data-phase": state.phase,
103
+ "data-intent": state.intent ? "on" : "off",
104
+ "data-hint": hint,
105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
106
+ label: hint,
107
+ side: "top",
108
+ delayMs: 200,
109
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
110
+ className: "dsh-wt-anchor",
111
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
112
+ type: "button",
113
+ role: "switch",
114
+ "aria-checked": state.intent,
115
+ "aria-label": t("chip.label"),
116
+ className: "dsh-wt-switch",
117
+ "data-phase": state.phase,
118
+ disabled: locked,
119
+ onClick: () => {
120
+ setIntent(!state.intent);
121
+ },
122
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
123
+ className: "dsh-wt-dot",
124
+ "aria-hidden": "true"
125
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
126
+ className: "dsh-wt-label",
127
+ children: chipLabel(state, t)
128
+ })]
129
+ })
130
+ })
131
+ }), state.phase === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
132
+ type: "button",
133
+ className: "dsh-wt-retry",
134
+ onClick: () => {
135
+ retry();
136
+ },
137
+ children: t("chip.retry")
138
+ }) : null]
139
+ });
140
+ }
141
+ // src/client/Panel.tsx
142
+ /**
143
+ * The Worktrees panel and its sidebar entry.
144
+ *
145
+ * The page is a plain read of every workspace's pool plus one explicit,
146
+ * twice-confirmed destructive action. Discarding is the only write the plugin
147
+ * offers here: nothing merges, nothing is reclaimed automatically, and a copy
148
+ * with uncommitted changes is refused by the host, not hidden by a disabled
149
+ * button alone.
150
+ * @module dsh-worktree-switch/client/Panel
151
+ */
152
+ /** How long an armed discard stays armed, in milliseconds. */
153
+ const ARM_TIMEOUT_MS = 5e3;
154
+ /** Render one copy's creation time without depending on the test machine's locale. */
155
+ function formatTime(milliseconds) {
156
+ if (milliseconds <= 0) return "—";
157
+ return new Date(milliseconds).toISOString().slice(0, 16).replace("T", " ");
158
+ }
159
+ /** The key that identifies one discard row. */
160
+ function rowKey(group, item) {
161
+ return `${group.path}\u0000${item.name}`;
162
+ }
163
+ /** One project's copies. */
164
+ function ProjectSection(props) {
165
+ const { group, state, armed, t, onArm, onDiscard } = props;
166
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
167
+ className: "dsh-wt-project",
168
+ "data-project": group.path,
169
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
170
+ className: "dsh-wt-project-head",
171
+ children: [
172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: group.title }),
173
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
174
+ className: "dsh-wt-path",
175
+ children: group.path
176
+ }),
177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
178
+ className: "dsh-wt-count",
179
+ children: t("panel.count", { count: group.items.length })
180
+ })
181
+ ]
182
+ }), group.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
183
+ className: "dsh-wt-error",
184
+ role: "alert",
185
+ children: group.error
186
+ }) : !group.supported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
187
+ className: "dsh-wt-muted",
188
+ children: t("panel.notRepository")
189
+ }) : group.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
190
+ className: "dsh-wt-muted",
191
+ children: t("panel.empty")
192
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
193
+ className: "dsh-wt-rows",
194
+ children: group.items.map((item) => {
195
+ const key = rowKey(group, item);
196
+ const isArmed = armed === key;
197
+ const busy = state.busy === key;
198
+ const dirty = item.changedFiles > 0;
199
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
200
+ className: "dsh-wt-row",
201
+ "data-worktree": item.name,
202
+ "data-dirty": dirty ? "true" : "false",
203
+ children: [
204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
205
+ className: "dsh-wt-name",
206
+ children: item.name
207
+ }),
208
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
209
+ className: "dsh-wt-branch",
210
+ children: [
211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
212
+ className: "dsh-wt-muted",
213
+ children: t("panel.branch")
214
+ }),
215
+ " ",
216
+ item.branch
217
+ ]
218
+ }),
219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
220
+ className: "dsh-wt-row-path",
221
+ children: item.path
222
+ }),
223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
224
+ className: "dsh-wt-created",
225
+ children: [
226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
227
+ className: "dsh-wt-muted",
228
+ children: t("panel.created")
229
+ }),
230
+ " ",
231
+ formatTime(item.createdAt)
232
+ ]
233
+ }),
234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
235
+ className: "dsh-wt-changes",
236
+ "data-changes": item.changedFiles,
237
+ children: dirty ? t("panel.changesCount", { count: item.changedFiles }) : t("panel.clean")
238
+ }),
239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
240
+ label: dirty ? t("panel.discardDirty") : t("panel.discardHint"),
241
+ side: "top",
242
+ delayMs: 200,
243
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
244
+ className: "dsh-wt-anchor",
245
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
246
+ type: "button",
247
+ className: "dsh-wt-discard",
248
+ "data-armed": isArmed ? "true" : "false",
249
+ disabled: dirty || busy || !group.supported,
250
+ onClick: () => {
251
+ if (isArmed) {
252
+ onDiscard(group, item);
253
+ return;
254
+ }
255
+ onArm(key);
256
+ },
257
+ children: busy ? "…" : isArmed ? t("panel.discardAgain") : t("panel.discard")
258
+ })
259
+ })
260
+ })
261
+ ]
262
+ }, key);
263
+ })
264
+ })]
265
+ });
266
+ }
267
+ /**
268
+ * Render the Worktrees panel.
269
+ * @param props - runtime props, the panel face, and the locale seat.
270
+ * @returns the page.
271
+ */
272
+ function WorktreePanel(props) {
273
+ const { useWorktrees, refresh, discard, close, t } = props;
274
+ const state = useWorktrees((current) => current);
275
+ const [armed, setArmed] = (0, react.useState)(null);
276
+ const timer = (0, react.useRef)(null);
277
+ (0, react.useEffect)(() => {
278
+ refresh();
279
+ }, [refresh]);
280
+ (0, react.useEffect)(() => () => {
281
+ if (timer.current !== null) window.clearTimeout(timer.current);
282
+ }, []);
283
+ const arm = (key) => {
284
+ if (timer.current !== null) window.clearTimeout(timer.current);
285
+ setArmed(key);
286
+ timer.current = window.setTimeout(() => {
287
+ setArmed(null);
288
+ }, ARM_TIMEOUT_MS);
289
+ };
290
+ const onDiscard = (group, item) => {
291
+ if (timer.current !== null) window.clearTimeout(timer.current);
292
+ setArmed(null);
293
+ discard(group.path, item.name);
294
+ };
295
+ const total = state.groups.reduce((sum, group) => sum + group.items.length, 0);
296
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
297
+ className: "dsh-wt-panel",
298
+ "data-loading": state.loading ? "true" : "false",
299
+ children: [
300
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
301
+ className: "dsh-wt-panel-head",
302
+ children: [
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
304
+ type: "button",
305
+ className: "dsh-wt-back",
306
+ onClick: () => {
307
+ close();
308
+ },
309
+ children: t("panel.back")
310
+ }),
311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
312
+ className: "dsh-wt-titles",
313
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("panel.title") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("panel.subtitle") })]
314
+ }),
315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
316
+ className: "dsh-wt-count",
317
+ children: t("panel.count", { count: total })
318
+ }),
319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
320
+ type: "button",
321
+ className: "dsh-wt-refresh",
322
+ onClick: () => {
323
+ refresh();
324
+ },
325
+ children: t("panel.refresh")
326
+ })
327
+ ]
328
+ }),
329
+ state.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
330
+ className: "dsh-wt-error",
331
+ role: "alert",
332
+ children: t("panel.error", { message: state.error })
333
+ }) : null,
334
+ state.loading && state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
335
+ className: "dsh-wt-muted",
336
+ role: "status",
337
+ children: t("panel.loading")
338
+ }) : state.groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
339
+ className: "dsh-wt-muted",
340
+ children: t("panel.workspaceEmpty")
341
+ }) : state.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectSection, {
342
+ group,
343
+ state,
344
+ armed,
345
+ t,
346
+ onArm: arm,
347
+ onDiscard
348
+ }, group.path))
349
+ ]
350
+ });
351
+ }
352
+ /**
353
+ * Render the sidebar entry that opens the panel.
354
+ * @param props - sidebar owner props, the open action, and the locale seat.
355
+ * @returns the icon button.
356
+ */
357
+ function WorktreePanelIcon(props) {
358
+ const { size, active, open, t } = props;
359
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
360
+ label: t("sidebar.label"),
361
+ side: "right",
362
+ delayMs: 300,
363
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
364
+ className: "dsh-wt-anchor",
365
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
366
+ type: "button",
367
+ className: "dsh-wt-panel-icon",
368
+ "data-active": active ? "true" : "false",
369
+ "aria-label": t("sidebar.label"),
370
+ "aria-pressed": active,
371
+ onClick: () => {
372
+ open();
373
+ },
374
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
375
+ width: Math.max(12, Math.round(size * .6)),
376
+ height: Math.max(12, Math.round(size * .6)),
377
+ viewBox: "0 0 16 16",
378
+ "aria-hidden": "true",
379
+ fill: "none",
380
+ stroke: "currentColor",
381
+ strokeWidth: "1.4",
382
+ strokeLinecap: "round",
383
+ children: [
384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 2.5v7A2.5 2.5 0 0 0 6.5 12H11" }),
385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
386
+ cx: "4",
387
+ cy: "2.5",
388
+ r: "1.5"
389
+ }),
390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
391
+ cx: "4",
392
+ cy: "12.5",
393
+ r: "1.5"
394
+ }),
395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
396
+ cx: "11",
397
+ cy: "10.5",
398
+ r: "1.5"
399
+ }),
400
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
401
+ d: "M8.5 6.5h2.5v2",
402
+ strokeDasharray: "1.6 2"
403
+ })
404
+ ]
405
+ })
406
+ })
407
+ })
408
+ });
409
+ }
410
+ // src/client/locales.ts
411
+ /** English dictionary; its keys are the namespace's key domain. */
412
+ const en = {
413
+ "chip.label": "Worktree copy for this session",
414
+ "chip.checking": "Worktree…",
415
+ "chip.off": "Worktree off",
416
+ "chip.creating": "Creating worktree…",
417
+ "chip.reverting": "Leaving worktree…",
418
+ "chip.ready": "Worktree: {name}",
419
+ "chip.failed": "Worktree failed",
420
+ "chip.retry": "Retry",
421
+ "chip.disabled": "Worktree unavailable",
422
+ "chip.hint.checking": "Checking whether this project can be isolated…",
423
+ "chip.hint.off": "New sessions in this project start in the project directory.",
424
+ "chip.hint.on": "This session runs in a copy of the project; the source checkout is untouched.",
425
+ "chip.hint.creating": "Creating a checkout inside the project…",
426
+ "chip.hint.reverting": "Moving this session back to the project directory…",
427
+ "chip.hint.ready": "Branch {branch} at {path}",
428
+ "chip.hint.failed": "The copy was not created, so this session is still in the project directory.",
429
+ "chip.hint.disabled": "This project is not a Git repository, so there is nothing to copy.",
430
+ "chip.hint.dirty": "The source checkout has {count} uncommitted change(s); the copy starts from the last commit.",
431
+ "chip.hint.intentOff": "Turning off could not leave {name}; this session still runs in it.",
432
+ "chip.blocked.checking": "Checking whether this session gets a worktree…",
433
+ "chip.blocked.creating": "Creating the worktree before the first prompt…",
434
+ "chip.blocked.reverting": "Leaving the worktree before the first prompt…",
435
+ "chip.blocked.failed": "The worktree could not be created. Retry, turn the switch off, or discard the failed copy first.",
436
+ "panel.title": "Worktrees",
437
+ "panel.subtitle": "Copies created by the Worktree switch: one checkout and one branch each. Discard removes both.",
438
+ "panel.refresh": "Refresh",
439
+ "panel.back": "Back",
440
+ "panel.loading": "Reading worktrees…",
441
+ "panel.empty": "No worktrees here yet.",
442
+ "panel.workspaceEmpty": "No projects yet. Open a session to give this panel something to read.",
443
+ "panel.error": "Could not read worktrees: {message}",
444
+ "panel.notRepository": "Not a Git repository.",
445
+ "panel.name": "Name",
446
+ "panel.branch": "Branch",
447
+ "panel.created": "Created",
448
+ "panel.changes": "Changes",
449
+ "panel.changesCount": "{count} uncommitted",
450
+ "panel.clean": "Clean",
451
+ "panel.discard": "Discard",
452
+ "panel.discardAgain": "Discard now",
453
+ "panel.discardHint": "Deletes the checkout and its branch.",
454
+ "panel.discardDirty": "Commit or discard its changes first.",
455
+ "panel.count": "{count} copies",
456
+ "sidebar.label": "Worktrees"
457
+ };
458
+ /** Chinese dictionary: every key of {@link en}. */
459
+ const zh = {
460
+ "chip.label": "本会话使用 Worktree 副本",
461
+ "chip.checking": "Worktree…",
462
+ "chip.off": "Worktree 已关",
463
+ "chip.creating": "正在创建 Worktree…",
464
+ "chip.reverting": "正在离开 Worktree…",
465
+ "chip.ready": "Worktree:{name}",
466
+ "chip.failed": "Worktree 创建失败",
467
+ "chip.retry": "重试",
468
+ "chip.disabled": "Worktree 不可用",
469
+ "chip.hint.checking": "正在检查这个项目能否隔离…",
470
+ "chip.hint.off": "这个项目的新会话直接在项目目录里开始。",
471
+ "chip.hint.on": "本会话在项目的一份副本里运行,源仓库不受影响。",
472
+ "chip.hint.creating": "正在项目里创建检出…",
473
+ "chip.hint.reverting": "正在把本会话切回项目目录…",
474
+ "chip.hint.ready": "分支 {branch},位于 {path}",
475
+ "chip.hint.failed": "副本没有创建成功,本会话仍然在项目目录里。",
476
+ "chip.hint.disabled": "这个项目不是 Git 仓库,没有可复制的内容。",
477
+ "chip.hint.dirty": "源仓库有 {count} 处未提交改动;副本会从最后一次提交开始。",
478
+ "chip.hint.intentOff": "关闭时没能离开 {name},本会话仍在副本里运行。",
479
+ "chip.blocked.checking": "正在检查本会话是否需要 Worktree…",
480
+ "chip.blocked.creating": "正在创建 Worktree,副本就绪前暂停发送…",
481
+ "chip.blocked.reverting": "正在离开 Worktree,切回项目目录前暂停发送…",
482
+ "chip.blocked.failed": "Worktree 创建失败。请重试、关闭开关,或先丢弃失败的副本。",
483
+ "panel.title": "Worktree",
484
+ "panel.subtitle": "Worktree 开关创建的副本:每份副本是一个检出加一个分支,丢弃会同时删掉两者。",
485
+ "panel.refresh": "刷新",
486
+ "panel.back": "返回",
487
+ "panel.loading": "正在读取 Worktree…",
488
+ "panel.empty": "这里还没有 Worktree。",
489
+ "panel.workspaceEmpty": "还没有项目。打开一个会话,这里才有可读的内容。",
490
+ "panel.error": "读取 Worktree 失败:{message}",
491
+ "panel.notRepository": "不是 Git 仓库。",
492
+ "panel.name": "名称",
493
+ "panel.branch": "分支",
494
+ "panel.created": "创建时间",
495
+ "panel.changes": "改动",
496
+ "panel.changesCount": "{count} 处未提交",
497
+ "panel.clean": "干净",
498
+ "panel.discard": "丢弃",
499
+ "panel.discardAgain": "确认丢弃",
500
+ "panel.discardHint": "删除检出目录和它的分支。",
501
+ "panel.discardDirty": "请先提交或丢弃其中的改动。",
502
+ "panel.count": "{count} 份副本",
503
+ "sidebar.label": "Worktree"
504
+ };
505
+ // src/client/intent.ts
506
+ /**
507
+ * The remembered per-project choice: "this project always isolates" and
508
+ * "this project never isolates" both need to survive a reload, and nothing
509
+ * else about the plugin does.
510
+ *
511
+ * localStorage is the right home for it. The choice is a property of one
512
+ * browser's user, not of the repository or the session log, and the two
513
+ * alternatives (a host endpoint, the settings service) would add a round trip
514
+ * to a decision that must be available before the switch can render.
515
+ * @module dsh-worktree-switch/client/intent
516
+ */
517
+ /** One storage key holds the whole map, so a project deletion cannot orphan entries. */
518
+ const STORAGE_KEY = "dsh-worktree-switch:intent";
519
+ /** Read the map, tolerating absent or corrupt storage. */
520
+ function readMap() {
521
+ try {
522
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
523
+ if (raw === null || raw === void 0) return {};
524
+ const parsed = JSON.parse(raw);
525
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
526
+ const entries = Object.entries(parsed).filter(([, value]) => typeof value === "boolean");
527
+ return Object.fromEntries(entries);
528
+ } catch {
529
+ return {};
530
+ }
531
+ }
532
+ /**
533
+ * Read the remembered choice for one project.
534
+ * @param project - absolute repository root.
535
+ * @returns the remembered value, or undefined when the project has none.
536
+ */
537
+ function readIntent(project) {
538
+ return readMap()[project];
539
+ }
540
+ /**
541
+ * Remember one project's choice.
542
+ * @param project - absolute repository root.
543
+ * @param enabled - the value to remember.
544
+ */
545
+ function writeIntent(project, enabled) {
546
+ try {
547
+ const next = {
548
+ ...readMap(),
549
+ [project]: enabled
550
+ };
551
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
552
+ } catch {}
553
+ }
554
+ // src/client/rpc.ts
555
+ /** The channel every plugin route lives on. */
556
+ const CHANNEL = "/api";
557
+ /** This plugin's endpoint prefix. */
558
+ const PREFIX = "dsh-worktree-switch";
559
+ /**
560
+ * Call one host verb.
561
+ * @param ctx - browser Cordis context providing `connection`.
562
+ * @param endpoint - verb name without the plugin prefix.
563
+ * @param payload - JSON payload.
564
+ * @param signal - optional caller cancellation.
565
+ * @returns the verb's value.
566
+ * @throws {Error} carrying the host failure's message when the verb refused.
567
+ */
568
+ async function callHost(ctx, endpoint, payload, signal) {
569
+ const connection = ctx.get("connection");
570
+ if (connection === void 0) throw new Error("worktree switch: the connection service is not mounted");
571
+ const result = await connection.rpc.call(CHANNEL, `${PREFIX}/${endpoint}`, payload, signal);
572
+ if (result === null || typeof result !== "object" || !("ok" in result)) throw new Error(`worktree switch: malformed reply from ${endpoint}`);
573
+ if (!result.ok) throw new Error(result.error.message);
574
+ return result.value;
575
+ }
576
+ // src/client/state.ts
577
+ /** The initial state: nothing is known until the host answers. */
578
+ const INITIAL = {
579
+ phase: "checking",
580
+ intent: false,
581
+ worktree: null,
582
+ repositoryRoot: null,
583
+ sourceDirty: 0,
584
+ error: null
585
+ };
586
+ /** A thrown value's message, without leaking `undefined`. */
587
+ function messageOf(error) {
588
+ if (error instanceof Error) return error.message;
589
+ const text = String(error);
590
+ return text === "" ? "unknown error" : text;
591
+ }
592
+ /**
593
+ * One controller per plugin instance: it caches the state of every session the
594
+ * switch has rendered for, and owns their composer blocks.
595
+ */
596
+ var ToggleController = class {
597
+ ctx;
598
+ bindings = /* @__PURE__ */ new Map();
599
+ lifetime = new AbortController();
600
+ /**
601
+ * @param ctx - browser Cordis context.
602
+ */
603
+ constructor(ctx) {
604
+ this.ctx = ctx;
605
+ }
606
+ /**
607
+ * The binding for one session, created on first use.
608
+ * @param sessionId - the session whose switch is rendering.
609
+ * @returns the stable binding.
610
+ */
611
+ binding(sessionId) {
612
+ const key = String(sessionId);
613
+ const existing = this.bindings.get(key);
614
+ if (existing !== void 0) return existing;
615
+ const binding = new InternalBinding(this.ctx, key, this.lifetime.signal);
616
+ this.bindings.set(key, binding.face);
617
+ return binding.face;
618
+ }
619
+ /** Clear every composer block this controller raised. */
620
+ dispose() {
621
+ this.lifetime.abort(/* @__PURE__ */ new Error("worktree switch unloaded"));
622
+ for (const binding of this.bindings.values()) binding.block(null);
623
+ this.bindings.clear();
624
+ }
625
+ };
626
+ /** The concrete per-session state machine behind one {@link ChipBinding}. */
627
+ var InternalBinding = class {
628
+ ctx;
629
+ sessionId;
630
+ signal;
631
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(INITIAL);
632
+ started = false;
633
+ /** The project whose remembered choice is in play; null until status answers. */
634
+ project = null;
635
+ /** A create is in flight; a second one would race the same copy. */
636
+ creating = false;
637
+ /** The release in flight, shared by callers that ask to leave at once. */
638
+ releasing = null;
639
+ face;
640
+ /**
641
+ * @param ctx - browser Cordis context.
642
+ * @param sessionId - wire session identity.
643
+ * @param signal - plugin lifetime cancellation.
644
+ */
645
+ constructor(ctx, sessionId, signal) {
646
+ this.ctx = ctx;
647
+ this.sessionId = sessionId;
648
+ this.signal = signal;
649
+ this.face = {
650
+ hooks: { toggle: this.store },
651
+ ensure: () => {
652
+ this.ensure();
653
+ },
654
+ setIntent: (next) => {
655
+ this.setIntent(next);
656
+ },
657
+ retry: () => {
658
+ this.retry();
659
+ },
660
+ block: (reason) => {
661
+ this.block(reason);
662
+ }
663
+ };
664
+ }
665
+ /** Run the first status round trip at most once. */
666
+ ensure() {
667
+ if (this.started) return;
668
+ this.started = true;
669
+ this.check();
670
+ }
671
+ /** Ask the host for the project's facts and act on the remembered choice. */
672
+ async check() {
673
+ try {
674
+ const status = await callHost(this.ctx, "status", { sessionId: this.sessionId }, this.signal);
675
+ if (!status.supported) {
676
+ this.store.set({
677
+ ...this.store.getSnapshot(),
678
+ phase: "disabled",
679
+ intent: status.defaultEnabled,
680
+ error: null
681
+ });
682
+ return;
683
+ }
684
+ this.project = status.repositoryRoot;
685
+ const intent = readIntent(status.repositoryRoot) ?? status.defaultEnabled;
686
+ const base = {
687
+ phase: "off",
688
+ intent,
689
+ worktree: status.sessionWorktree,
690
+ repositoryRoot: status.repositoryRoot,
691
+ sourceDirty: status.sourceDirty,
692
+ error: null
693
+ };
694
+ if (status.sessionWorktree !== null) {
695
+ this.store.set({
696
+ ...base,
697
+ phase: "ready"
698
+ });
699
+ if (!intent) this.release();
700
+ return;
701
+ }
702
+ if (!intent) {
703
+ this.store.set(base);
704
+ return;
705
+ }
706
+ this.store.set(base);
707
+ await this.create();
708
+ } catch (error) {
709
+ if (this.signal.aborted) return;
710
+ this.store.set({
711
+ ...this.store.getSnapshot(),
712
+ phase: "failed",
713
+ error: messageOf(error)
714
+ });
715
+ }
716
+ }
717
+ /** Create the copy for a session the switch says should have one. */
718
+ async create() {
719
+ if (this.creating) return;
720
+ const before = this.store.getSnapshot();
721
+ if (before.repositoryRoot === null || !before.intent) return;
722
+ this.creating = true;
723
+ this.store.set({
724
+ ...before,
725
+ phase: "creating",
726
+ error: null
727
+ });
728
+ try {
729
+ const prepared = await callHost(this.ctx, "prepare", { sessionId: this.sessionId }, this.signal);
730
+ const now = this.store.getSnapshot();
731
+ if (!now.intent) {
732
+ if (now.phase === "reverting") this.release();
733
+ return;
734
+ }
735
+ this.store.set({
736
+ ...now,
737
+ phase: "ready",
738
+ worktree: prepared.worktree,
739
+ repositoryRoot: prepared.repositoryRoot,
740
+ sourceDirty: prepared.sourceDirty,
741
+ error: null
742
+ });
743
+ } catch (error) {
744
+ if (this.signal.aborted) return;
745
+ const now = this.store.getSnapshot();
746
+ if (!now.intent) {
747
+ if (now.phase === "reverting") this.release();
748
+ return;
749
+ }
750
+ this.store.set({
751
+ ...now,
752
+ phase: "failed",
753
+ error: messageOf(error)
754
+ });
755
+ } finally {
756
+ this.creating = false;
757
+ }
758
+ }
759
+ /**
760
+ * Leave the copy this session runs in, so "off" means the project directory.
761
+ * Concurrent callers share one round trip.
762
+ * @returns completion of the leave.
763
+ */
764
+ release() {
765
+ const running = this.releasing;
766
+ if (running !== null) return running;
767
+ const operation = this.leave().finally(() => {
768
+ if (this.releasing === operation) this.releasing = null;
769
+ });
770
+ this.releasing = operation;
771
+ return operation;
772
+ }
773
+ /** Ask the host to undo the copy and settle the switch on what it reported. */
774
+ async leave() {
775
+ const before = this.store.getSnapshot();
776
+ this.store.set({
777
+ ...before,
778
+ phase: "reverting",
779
+ error: null
780
+ });
781
+ try {
782
+ await callHost(this.ctx, "release", { sessionId: this.sessionId }, this.signal);
783
+ if (this.signal.aborted) return;
784
+ const now = this.store.getSnapshot();
785
+ this.store.set({
786
+ ...now,
787
+ phase: "off",
788
+ intent: false,
789
+ worktree: null,
790
+ error: null
791
+ });
792
+ } catch (error) {
793
+ if (this.signal.aborted) return;
794
+ const now = this.store.getSnapshot();
795
+ this.store.set({
796
+ ...now,
797
+ phase: now.worktree === null ? "off" : "ready",
798
+ intent: false,
799
+ error: messageOf(error)
800
+ });
801
+ }
802
+ }
803
+ /** Persist and apply one switch move. */
804
+ setIntent(next) {
805
+ const state = this.store.getSnapshot();
806
+ if (state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting") return;
807
+ if (this.project !== null) writeIntent(this.project, next);
808
+ if (!next) {
809
+ if (state.intent === next) return;
810
+ this.store.set({
811
+ ...state,
812
+ intent: false,
813
+ error: null
814
+ });
815
+ this.release();
816
+ return;
817
+ }
818
+ if (state.intent === next) return;
819
+ this.store.set({
820
+ ...state,
821
+ intent: true,
822
+ error: null
823
+ });
824
+ if (state.phase === "off" || state.phase === "failed") this.create();
825
+ }
826
+ /** Re-run the host round trip with the switch on. */
827
+ retry() {
828
+ const state = this.store.getSnapshot();
829
+ if (state.phase !== "failed") return;
830
+ if (this.project !== null) writeIntent(this.project, true);
831
+ this.store.set({
832
+ ...state,
833
+ intent: true,
834
+ phase: "off",
835
+ error: null
836
+ });
837
+ this.create();
838
+ }
839
+ /** Block or release this session's composer through the public registry. */
840
+ block(reason) {
841
+ const conversation = this.ctx.get("conversation");
842
+ if (conversation === void 0) return;
843
+ conversation.blocks.set(this.sessionId, reason === null ? void 0 : { reason });
844
+ }
845
+ };
846
+ /**
847
+ * Read every workspace's pool and discard copies from the panel.
848
+ */
849
+ var PanelController = class {
850
+ ctx;
851
+ leave;
852
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
853
+ loading: true,
854
+ groups: [],
855
+ error: null,
856
+ busy: null
857
+ });
858
+ lifetime = new AbortController();
859
+ /**
860
+ * @param ctx - browser Cordis context.
861
+ * @param leave - called by the panel's Back action.
862
+ */
863
+ constructor(ctx, leave) {
864
+ this.ctx = ctx;
865
+ this.leave = leave;
866
+ }
867
+ /** The stable face handed to the panel component. */
868
+ face = {
869
+ hooks: { worktrees: this.store },
870
+ refresh: () => {
871
+ this.refresh();
872
+ },
873
+ discard: (project, name) => {
874
+ this.discard(project, name);
875
+ },
876
+ close: () => {
877
+ this.leave();
878
+ }
879
+ };
880
+ /** Read every workspace's pool; projects that fail stay visible with their error. */
881
+ refresh() {
882
+ this.run();
883
+ }
884
+ async run() {
885
+ const workspaces = this.ctx.get("workspaces");
886
+ if (workspaces === void 0) {
887
+ this.store.set({
888
+ loading: false,
889
+ groups: [],
890
+ error: "the workspace service is not mounted",
891
+ busy: null
892
+ });
893
+ return;
894
+ }
895
+ const items = workspaces.list.getSnapshot().items;
896
+ this.store.set({
897
+ ...this.store.getSnapshot(),
898
+ loading: true,
899
+ error: null
900
+ });
901
+ const groups = [];
902
+ for (const workspace of items) try {
903
+ const listed = await callHost(this.ctx, "list", { path: workspace.path }, this.lifetime.signal);
904
+ groups.push(listed.supported ? {
905
+ path: workspace.path,
906
+ title: workspace.title,
907
+ supported: true,
908
+ items: listed.items,
909
+ error: null
910
+ } : {
911
+ path: workspace.path,
912
+ title: workspace.title,
913
+ supported: false,
914
+ items: [],
915
+ error: null
916
+ });
917
+ } catch (error) {
918
+ if (this.lifetime.signal.aborted) return;
919
+ groups.push({
920
+ path: workspace.path,
921
+ title: workspace.title,
922
+ supported: true,
923
+ items: [],
924
+ error: messageOf(error)
925
+ });
926
+ }
927
+ if (this.lifetime.signal.aborted) return;
928
+ this.store.set({
929
+ loading: false,
930
+ groups,
931
+ error: null,
932
+ busy: null
933
+ });
934
+ }
935
+ async discard(project, name) {
936
+ const key = `${project}\u0000${name}`;
937
+ this.store.set({
938
+ ...this.store.getSnapshot(),
939
+ busy: key,
940
+ error: null
941
+ });
942
+ try {
943
+ await callHost(this.ctx, "discard", {
944
+ path: project,
945
+ name
946
+ }, this.lifetime.signal);
947
+ this.store.set({
948
+ ...this.store.getSnapshot(),
949
+ busy: null
950
+ });
951
+ await this.run();
952
+ } catch (error) {
953
+ if (this.lifetime.signal.aborted) return;
954
+ this.store.set({
955
+ ...this.store.getSnapshot(),
956
+ busy: null,
957
+ error: messageOf(error)
958
+ });
959
+ }
960
+ }
961
+ /** Abort in-flight reads when the plugin unloads. */
962
+ dispose() {
963
+ this.lifetime.abort(/* @__PURE__ */ new Error("worktree switch unloaded"));
964
+ }
965
+ };
966
+ // src/client/styles.ts
967
+ /**
968
+ * The plugin's own stylesheet, injected once per page.
969
+ *
970
+ * Surfaces already own typography and colour; this sheet only lays out the
971
+ * switch and the panel and borrows `currentColor` for every line, so it stays
972
+ * legible in both themes without owning a palette.
973
+ * @module dsh-worktree-switch/client/styles
974
+ */
975
+ /** Marker attribute identifying this plugin's style element. */
976
+ const STYLE_ATTRIBUTE = "data-dsh-worktree-switch-style";
977
+ /** The whole sheet. */
978
+ const CSS = `
979
+ .dsh-wt-chip { display: inline-flex; align-items: center; gap: 6px; }
980
+ .dsh-wt-anchor { display: inline-flex; }
981
+ .dsh-wt-switch {
982
+ display: inline-flex; align-items: center; gap: 6px;
983
+ padding: 2px 8px; border-radius: 999px;
984
+ border: 1px solid color-mix(in oklab, currentColor 24%, transparent);
985
+ background: transparent; color: inherit; font: inherit; font-size: 12px;
986
+ line-height: 20px; cursor: pointer; white-space: nowrap;
987
+ }
988
+ .dsh-wt-switch:hover:not(:disabled) { border-color: color-mix(in oklab, currentColor 44%, transparent); }
989
+ .dsh-wt-switch:disabled { cursor: default; opacity: .72; }
990
+ .dsh-wt-switch[data-phase="failed"] { border-color: color-mix(in oklab, #e5534b 55%, transparent); }
991
+ .dsh-wt-switch:focus-visible { outline: 2px solid color-mix(in oklab, currentColor 45%, transparent); outline-offset: 1px; }
992
+ .dsh-wt-dot {
993
+ width: 6px; height: 6px; border-radius: 999px;
994
+ background: color-mix(in oklab, currentColor 40%, transparent);
995
+ }
996
+ .dsh-wt-chip[data-intent="on"] .dsh-wt-dot { background: #2da44e; }
997
+ .dsh-wt-chip[data-phase="failed"] .dsh-wt-dot { background: #e5534b; }
998
+ .dsh-wt-chip[data-phase="creating"] .dsh-wt-dot { background: #bf8700; }
999
+ .dsh-wt-label { max-width: 240px; overflow: hidden; text-overflow: ellipsis; }
1000
+ .dsh-wt-retry {
1001
+ border: none; background: transparent; color: inherit; font: inherit; font-size: 12px;
1002
+ text-decoration: underline; cursor: pointer; padding: 0 2px;
1003
+ }
1004
+ .dsh-wt-panel {
1005
+ display: flex; flex-direction: column; gap: 16px; padding: 20px 28px 40px;
1006
+ color: inherit; font: inherit; overflow: auto; height: 100%;
1007
+ }
1008
+ .dsh-wt-panel-head { display: flex; align-items: center; gap: 14px; }
1009
+ .dsh-wt-titles { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
1010
+ .dsh-wt-titles h2 { margin: 0; font-size: 16px; font-weight: 600; }
1011
+ .dsh-wt-titles p { margin: 0; font-size: 12px; opacity: .72; }
1012
+ .dsh-wt-back, .dsh-wt-refresh, .dsh-wt-discard {
1013
+ border: 1px solid color-mix(in oklab, currentColor 24%, transparent);
1014
+ background: transparent; color: inherit; font: inherit; font-size: 12px;
1015
+ border-radius: 6px; padding: 6px 12px; cursor: pointer;
1016
+ }
1017
+ .dsh-wt-back:hover, .dsh-wt-refresh:hover, .dsh-wt-discard:hover:not(:disabled) {
1018
+ border-color: color-mix(in oklab, currentColor 44%, transparent);
1019
+ }
1020
+ .dsh-wt-discard:disabled { opacity: .6; cursor: default; }
1021
+ .dsh-wt-discard[data-armed="true"] {
1022
+ border-color: color-mix(in oklab, #e5534b 60%, transparent);
1023
+ color: #e5534b;
1024
+ }
1025
+ .dsh-wt-project { display: flex; flex-direction: column; gap: 8px; }
1026
+ .dsh-wt-project-head { display: flex; align-items: baseline; gap: 10px; }
1027
+ .dsh-wt-project-head h3 { margin: 0; font-size: 13px; font-weight: 600; }
1028
+ .dsh-wt-count { font-size: 11px; opacity: .64; }
1029
+ .dsh-wt-path, .dsh-wt-row-path { font-size: 11px; opacity: .64; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1030
+ .dsh-wt-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
1031
+ .dsh-wt-row {
1032
+ display: grid; align-items: center; gap: 12px;
1033
+ grid-template-columns: minmax(120px, 1.2fr) minmax(120px, 1fr) minmax(160px, 1.6fr) auto auto auto;
1034
+ padding: 8px 12px; border-radius: 8px;
1035
+ border: 1px solid color-mix(in oklab, currentColor 12%, transparent);
1036
+ }
1037
+ .dsh-wt-name { font-weight: 600; }
1038
+ .dsh-wt-branch, .dsh-wt-created, .dsh-wt-changes { font-size: 12px; }
1039
+ .dsh-wt-changes[data-changes]:not([data-changes="0"]) { color: #bf8700; }
1040
+ .dsh-wt-muted { opacity: .64; font-size: 12px; }
1041
+ .dsh-wt-error { color: #e5534b; font-size: 12px; margin: 0; }
1042
+ .dsh-wt-panel-icon {
1043
+ display: inline-flex; align-items: center; justify-content: center;
1044
+ width: 28px; height: 28px; border-radius: 8px; border: none; background: transparent;
1045
+ color: inherit; opacity: .68; cursor: pointer; padding: 0;
1046
+ }
1047
+ .dsh-wt-panel-icon:hover { opacity: 1; background: color-mix(in oklab, currentColor 8%, transparent); }
1048
+ .dsh-wt-panel-icon[data-active="true"] { opacity: 1; background: color-mix(in oklab, currentColor 12%, transparent); }
1049
+ `;
1050
+ /**
1051
+ * Install the stylesheet if this page has not installed it yet.
1052
+ * @returns a disposer that removes the element this call created.
1053
+ */
1054
+ function installStyles() {
1055
+ if (typeof document === "undefined") return () => {};
1056
+ if (document.querySelector(`style[${STYLE_ATTRIBUTE}]`) !== null) return () => {};
1057
+ const style = document.createElement("style");
1058
+ style.setAttribute(STYLE_ATTRIBUTE, "");
1059
+ style.textContent = CSS;
1060
+ document.head.appendChild(style);
1061
+ return () => {
1062
+ style.remove();
1063
+ };
1064
+ }
1065
+ // src/client/index.tsx
1066
+ /** Browser module id: package name, bundle row id, plugin id. */
1067
+ const name = "dsh-worktree-switch";
1068
+ /** Locale namespace this plugin owns. */
1069
+ const NS = "worktreeToggle";
1070
+ /** Keyed `main` panel id; the sidebar entry's list id is the same string. */
1071
+ const PANEL_ID = "dsh-worktree-switch";
1072
+ /**
1073
+ * Client services this plugin cannot work without.
1074
+ *
1075
+ * `conversation` is how the switch stops a prompt while a copy it promised is
1076
+ * not ready; `workspaces` is how the panel finds the projects to list.
1077
+ */
1078
+ const inject = [
1079
+ "conversation",
1080
+ "connection",
1081
+ "layout",
1082
+ "locale",
1083
+ "slots",
1084
+ "workspaces"
1085
+ ];
1086
+ /**
1087
+ * Mount the browser half.
1088
+ * @param ctx - browser Cordis context.
1089
+ */
1090
+ function apply(ctx) {
1091
+ ctx.effect(() => ctx.locale.register(NS, {
1092
+ en,
1093
+ zh
1094
+ }), "dsh-worktree-switch: dictionaries");
1095
+ ctx.effect(installStyles, "dsh-worktree-switch: styles");
1096
+ const t = ctx.locale.bind(NS);
1097
+ const toggles = new ToggleController(ctx);
1098
+ const panels = new PanelController(ctx, () => {
1099
+ ctx.layout.selectPanel(null);
1100
+ });
1101
+ ctx.effect(() => () => {
1102
+ toggles.dispose();
1103
+ panels.dispose();
1104
+ }, "dsh-worktree-switch: controllers");
1105
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
1106
+ name: "conversation.input.left",
1107
+ id: PANEL_ID,
1108
+ order: 40,
1109
+ locale: NS,
1110
+ inject: (sessionId) => toggles.binding(sessionId)
1111
+ }, WorktreeChip));
1112
+ ctx.slots.inject("main", () => ctx.slots.register({
1113
+ name: "main",
1114
+ key: PANEL_ID,
1115
+ locale: NS,
1116
+ inject: () => panels.face
1117
+ }, WorktreePanel));
1118
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
1119
+ name: "sidebar.panellist",
1120
+ id: PANEL_ID,
1121
+ order: 30,
1122
+ locale: NS,
1123
+ label: () => t("sidebar.label"),
1124
+ inject: () => ({ open: () => {
1125
+ ctx.layout.selectPanel(PANEL_ID);
1126
+ } })
1127
+ }, WorktreePanelIcon));
1128
+ }
1129
+ exports.NS = NS;
1130
+ exports.PANEL_ID = PANEL_ID;
1131
+ exports.apply = apply;
1132
+ exports.inject = inject;
1133
+ exports.name = name;
1134
+
1135
+
1136
+ return module.exports;
1137
+ } });