@qilin-agent/client-ui-workspace 0.0.0-stage → 3.1.3

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,3698 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@qilin-agent/client-ui-workspace",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _qilin_agent_client_store = require("@qilin-agent/client-store");
8
+ let _qilin_agent_kylin = require("@qilin-agent/kylin");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react = require("react");
11
+ let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
12
+ //#region lib/types/client/shortcuts.js
13
+ /**
14
+ * Create the private browser request source shared by commands and controls.
15
+ * @returns observable state and its complete mutation callbacks.
16
+ */
17
+ function createWorkspaceShortcutControls() {
18
+ const state = (0, _qilin_agent_client_store.createSnapshotStore)({
19
+ searchRequest: 0,
20
+ addRequested: false,
21
+ directoryBusy: false,
22
+ renameTarget: null
23
+ });
24
+ return {
25
+ state,
26
+ search: () => {
27
+ state.set({
28
+ ...state.getSnapshot(),
29
+ searchRequest: state.getSnapshot().searchRequest + 1
30
+ });
31
+ },
32
+ add: () => {
33
+ state.set(state.getSnapshot().directoryBusy ? state.getSnapshot() : {
34
+ ...state.getSnapshot(),
35
+ addRequested: true
36
+ });
37
+ },
38
+ closeAdd: () => {
39
+ state.set({
40
+ ...state.getSnapshot(),
41
+ addRequested: false
42
+ });
43
+ },
44
+ directoryBusy: (busy) => {
45
+ state.set({
46
+ ...state.getSnapshot(),
47
+ directoryBusy: busy
48
+ });
49
+ },
50
+ rename: (sessionId, currentTitle) => {
51
+ state.set({
52
+ ...state.getSnapshot(),
53
+ renameTarget: {
54
+ sessionId,
55
+ currentTitle
56
+ }
57
+ });
58
+ },
59
+ closeRename: () => {
60
+ state.set({
61
+ ...state.getSnapshot(),
62
+ renameTarget: null
63
+ });
64
+ }
65
+ };
66
+ }
67
+ /**
68
+ * Register navigation commands against the existing workspace owner.
69
+ * @param ctx - plugin context with the shortcut, locale, and model services.
70
+ * @param navigation - session creation and forking from the pointer controls' navigation service.
71
+ * @param controls - browser-owned opening requests.
72
+ * @param archiveSession - shared archive action, including running-work confirmation and notices.
73
+ */
74
+ function installWorkspaceShortcuts(ctx, navigation, controls, archiveSession) {
75
+ const t = ctx.locale.bind("workspace");
76
+ const current = () => Object.values(ctx.sessions.list.getSnapshot().byId).find((row) => (row.retainedBy.mainView ?? 0) > 0);
77
+ const addReason = () => ctx.slots.entries("sidebar.workspaces.directoryFlow").length === 0 ? t("shortcut.noPicker") : controls.state.getSnapshot().directoryBusy ? t("shortcut.directoryBusy") : null;
78
+ const register = (id, label, aliases, code, modifiers, webModifiers, resolve) => {
79
+ ctx.effect(() => ctx.shortcuts.register({
80
+ id,
81
+ label,
82
+ aliases,
83
+ defaults: {
84
+ "desktop:macos": {
85
+ code,
86
+ modifiers
87
+ },
88
+ "desktop:windows": {
89
+ code,
90
+ modifiers
91
+ },
92
+ "desktop:linux": {
93
+ code,
94
+ modifiers
95
+ },
96
+ "web:macos": {
97
+ code,
98
+ modifiers: webModifiers
99
+ },
100
+ "web:windows": {
101
+ code,
102
+ modifiers: webModifiers
103
+ }
104
+ },
105
+ regions: ["page", "editable"],
106
+ modals: [],
107
+ resolve
108
+ }), `ui-workspace: ${id}`);
109
+ };
110
+ register("session.new", () => t("session.new"), ["new session", "new chat"], "KeyN", ["primary"], ["primary", "alt"], () => ({
111
+ status: "handled",
112
+ run: () => {
113
+ navigation.startSession();
114
+ }
115
+ }));
116
+ register("session.search", () => t("search.sessions.aria"), ["search sessions"], "KeyK", ["primary"], ["primary", "alt"], () => ({
117
+ status: "handled",
118
+ run: controls.search
119
+ }));
120
+ register("workspace.add", () => t("workspace.add"), ["add workspace", "open folder"], "KeyO", ["primary"], ["primary", "alt"], () => {
121
+ const reason = addReason();
122
+ return reason === null ? {
123
+ status: "handled",
124
+ run: controls.add
125
+ } : {
126
+ status: "blocked",
127
+ reason
128
+ };
129
+ });
130
+ register("session.rename", () => t("rename.session.title"), ["rename session"], "KeyR", ["primary", "alt"], ["primary", "shift"], () => {
131
+ const target = current();
132
+ return target === void 0 ? {
133
+ status: "blocked",
134
+ reason: t("shortcut.noSession")
135
+ } : {
136
+ status: "handled",
137
+ run: () => {
138
+ controls.rename(target.id, target.displayTitle);
139
+ }
140
+ };
141
+ });
142
+ register("session.fork", () => t("menu.fork"), ["fork session"], "KeyF", ["primary", "alt"], ["primary", "shift"], () => {
143
+ const target = current();
144
+ if (target === void 0) return {
145
+ status: "blocked",
146
+ reason: t("shortcut.noSession")
147
+ };
148
+ if (target.blank) return {
149
+ status: "blocked",
150
+ reason: t("shortcut.noCompletedTurn")
151
+ };
152
+ return {
153
+ status: "handled",
154
+ run: () => {
155
+ navigation.forkSession(target.id).catch((error) => {
156
+ if (!(error instanceof Error && error.name === "SessionForkError" && error.rpcError.code === "session/fork-unavailable")) console.warn("session fork rejected:", error);
157
+ });
158
+ }
159
+ };
160
+ });
161
+ register("session.archive", () => t("menu.archiveSession"), ["archive session"], "KeyA", ["primary", "shift"], ["primary", "alt"], () => {
162
+ const target = current();
163
+ return target === void 0 ? {
164
+ status: "blocked",
165
+ reason: t("shortcut.noSession")
166
+ } : {
167
+ status: "handled",
168
+ run: () => {
169
+ archiveSession(target.id);
170
+ }
171
+ };
172
+ });
173
+ }
174
+ //#endregion
175
+ //#region ../../util/workspace-path/src/index.ts
176
+ /** Whether a path uses a Windows drive or UNC prefix. */
177
+ function isWindowsStylePath(value) {
178
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
179
+ }
180
+ /**
181
+ * Abbreviate a POSIX home directory for display.
182
+ * @param path - Absolute or already-short display path.
183
+ * @param home - Host account home; absent skips abbreviation.
184
+ * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
185
+ */
186
+ function abbreviateHomePath(path, home) {
187
+ if (home === void 0 || home === "") return path;
188
+ if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path;
189
+ const root = home.replace(/\/+$/, "");
190
+ if (root === "" || root === "/") return path;
191
+ if (path.replace(/\/+$/, "") === root) return "~";
192
+ if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`;
193
+ return path;
194
+ }
195
+ /**
196
+ * Read the final non-empty segment of a Workspace path for display.
197
+ * Workspace-label surfaces use this helper instead of deriving another basename.
198
+ * @param path - Workspace directory path using POSIX or Windows separators.
199
+ * @returns the final segment, or an empty string for a separator-only path.
200
+ */
201
+ function workspaceTitleOf(path) {
202
+ const trimmed = path.replace(/[/\\]+$/, "");
203
+ const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
204
+ return trimmed.slice(separator + 1);
205
+ }
206
+ //#endregion
207
+ //#region lib/types/client/subagent-lineage.js
208
+ /** UI Workspace-owned projection of descendant counts from Session summaries. */
209
+ /**
210
+ * Index uninterrupted subagent descendants under each ancestor.
211
+ * @param summaries - Session summaries keyed by id.
212
+ * @returns descendant totals keyed by possible parent id.
213
+ */
214
+ function indexSubagentDescendants(summaries) {
215
+ const indexed = /* @__PURE__ */ new Map();
216
+ for (const descendant of Object.values(summaries)) {
217
+ if (descendant.origin !== "subagent") continue;
218
+ const seen = /* @__PURE__ */ new Set();
219
+ let current = descendant;
220
+ while (current?.origin === "subagent" && current.parentId !== void 0 && !seen.has(current.id)) {
221
+ seen.add(current.id);
222
+ const aggregate = indexed.get(current.parentId);
223
+ if (aggregate === void 0) indexed.set(current.parentId, {
224
+ count: 1,
225
+ runningCount: descendant.running ? 1 : 0
226
+ });
227
+ else {
228
+ aggregate.count += 1;
229
+ if (descendant.running) aggregate.runningCount += 1;
230
+ }
231
+ current = summaries[current.parentId];
232
+ }
233
+ }
234
+ return indexed;
235
+ }
236
+ /**
237
+ * Resolve the Workspace browser group that owns one Session.
238
+ * @param workspaces - authoritative Workspace membership.
239
+ * @param sessionId - Session whose browser group is required.
240
+ * @returns owning Workspace id, or {@link UNGROUPED_KEY} when no Workspace accounts for it.
241
+ */
242
+ function owningGroupKey(workspaces, sessionId) {
243
+ return workspaces.find((workspace) => workspace.sessionIds.includes(sessionId))?.workspaceId ?? "";
244
+ }
245
+ function mainSessionId(list) {
246
+ return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
247
+ }
248
+ /**
249
+ * Directory display label: basename of the path (both separators accepted).
250
+ * Ungrouped-bucket fallback for surfaces without a workspace title.
251
+ * @param cwd - directory path, or undefined for the ungrouped bucket.
252
+ * @returns basename, the raw cwd when it has no basename, or an empty ungrouped marker.
253
+ */
254
+ function workspaceLabel(cwd) {
255
+ if (cwd === void 0 || cwd === "") return "";
256
+ const base = workspaceTitleOf(cwd);
257
+ return base !== "" ? base : cwd;
258
+ }
259
+ /**
260
+ * Project known account members by current Session recency.
261
+ * @param sessionIds - authoritative account membership.
262
+ * @param summaries - current Session summaries; members without a summary are omitted until it arrives.
263
+ * @returns known members newest first, with Session identity as the deterministic tie-break.
264
+ */
265
+ function orderByRecency(sessionIds, summaries) {
266
+ return sessionIds.flatMap((id) => {
267
+ const summary = summaries[id];
268
+ return summary === void 0 ? [] : [{
269
+ id,
270
+ updatedAt: summary.updatedAt
271
+ }];
272
+ }).sort((a, b) => {
273
+ if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt;
274
+ return a.id < b.id ? -1 : 1;
275
+ }).map((member) => member.id);
276
+ }
277
+ /**
278
+ * Reconcile a browser-local manual order with current account membership.
279
+ * @param memberIds - authoritative account membership.
280
+ * @param savedOrder - previously saved browser-local order.
281
+ * @param summaries - current Session summaries used to append newly known members by recency.
282
+ * @returns retained saved slots followed by newly known members; departed members and unknown new members are omitted.
283
+ */
284
+ function reconcileManualOrder(memberIds, savedOrder, summaries) {
285
+ const members = new Map(memberIds.map((id) => [id, id]));
286
+ const included = /* @__PURE__ */ new Set();
287
+ const ordered = [];
288
+ for (const key of savedOrder ?? []) {
289
+ const id = members.get(key);
290
+ if (id === void 0 || included.has(key)) continue;
291
+ ordered.push(id);
292
+ included.add(key);
293
+ }
294
+ for (const id of orderByRecency(memberIds, summaries)) {
295
+ if (included.has(id)) continue;
296
+ ordered.push(id);
297
+ included.add(id);
298
+ }
299
+ return ordered;
300
+ }
301
+ /**
302
+ * Keep the selected provisional New Session ahead of either base order.
303
+ * @param order - recency or reconciled manual order.
304
+ * @param currentBlank - selected blank Session in this account, when present.
305
+ * @returns a copy with the selected blank first and no duplicate slot.
306
+ */
307
+ function pinCurrentBlank(order, currentBlank) {
308
+ if (currentBlank === void 0) return [...order];
309
+ return [currentBlank, ...order.filter((id) => id !== currentBlank)];
310
+ }
311
+ /**
312
+ * Ordinary sessions are visible; among blank sessions, only the current one
313
+ * is visible. Subagent children use their parent header catalog; archived
314
+ * sessions are visible nowhere, while their accounting slots remain so
315
+ * unarchiving restores position.
316
+ */
317
+ function sessionVisible(session, current, archived) {
318
+ return session.origin !== "subagent" && !archived.has(session.id) && (!session.blank || session.id === current);
319
+ }
320
+ /**
321
+ * A blank session is the selected Workspace's provisional New Session row;
322
+ * its canonical title never enters search (blank rows are query-excluded)
323
+ * and the renderer localizes its display label.
324
+ */
325
+ function sessionTitle(session) {
326
+ return session.blank ? "" : session.displayTitle;
327
+ }
328
+ /** Build one group without projecting session lineage into presentation. */
329
+ function buildGroup(key, workspaceId, cwd, createdAt, label, members) {
330
+ return {
331
+ key,
332
+ workspaceId,
333
+ cwd,
334
+ createdAt,
335
+ label,
336
+ sessions: [...members]
337
+ };
338
+ }
339
+ /** Apply a stored Ungrouped order and append newly loose Sessions by recency. */
340
+ function orderedUngrouped(members, stored, summaries) {
341
+ const byId = new Map(members.map((session) => [session.id, session]));
342
+ return (stored === void 0 ? orderByRecency(members.map((session) => session.id), summaries) : reconcileManualOrder(members.map((session) => session.id), stored, summaries)).flatMap((id) => {
343
+ const session = byId.get(id);
344
+ /* v8 ignore next -- ids are projected exclusively from the members used to build byId. */
345
+ return session === void 0 ? [] : [session];
346
+ });
347
+ }
348
+ /**
349
+ * Group Sessions by Workspace: one group per caller-ordered entity, with
350
+ * members resolved from caller-ordered sessionIds. Sessions outside every
351
+ * Workspace trail in the browser-local Ungrouped order, which falls back to
352
+ * recency before that order is initialized.
353
+ */
354
+ function groupByWorkspace(list, workspaces, archived, ungroupedOrder) {
355
+ const current = mainSessionId(list);
356
+ const groups = [];
357
+ const accounted = /* @__PURE__ */ new Set();
358
+ for (const workspace of workspaces) {
359
+ const members = [];
360
+ for (const id of workspace.sessionIds) {
361
+ const summary = list.byId[id];
362
+ if (summary === void 0) continue;
363
+ accounted.add(id);
364
+ if (!sessionVisible(summary, current, archived)) continue;
365
+ members.push(summary);
366
+ }
367
+ groups.push(buildGroup(workspace.workspaceId, workspace.workspaceId, workspace.path, Date.parse(workspace.createdAt), workspace.title, members));
368
+ }
369
+ const stray = list.ids.map((id) => list.byId[id]).filter((s) => s !== void 0 && !accounted.has(s.id) && sessionVisible(s, current, archived));
370
+ if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, "", orderedUngrouped(stray, ungroupedOrder, list.byId)));
371
+ return groups;
372
+ }
373
+ /** Keep navigation presentation independent from domain-owned interaction objects. */
374
+ function visiblePendingKind(kind) {
375
+ switch (kind) {
376
+ case "approval":
377
+ case "plan-review":
378
+ case "question": return kind;
379
+ default: return;
380
+ }
381
+ }
382
+ function sessionNode(s, descendants, statuses, pinned) {
383
+ const status = statuses.get(s.id);
384
+ const pendingInteraction = visiblePendingKind(status?.pendingInteraction?.kind);
385
+ return {
386
+ id: s.id,
387
+ title: sessionTitle(s),
388
+ blank: s.blank,
389
+ running: status?.running ?? s.running,
390
+ runningSubagentCount: descendants.get(s.id)?.runningCount ?? 0,
391
+ completed: status?.completionUnread === true,
392
+ updatedAt: s.updatedAt,
393
+ pinned: pinned.has(s.id),
394
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction }
395
+ };
396
+ }
397
+ /**
398
+ * Front pinned rows without changing either partition's caller order, so the
399
+ * provisional New Session placeholder's own position is untouched.
400
+ * @param members - caller-ordered rows.
401
+ * @param pinned - registry-global pin set.
402
+ * @returns pinned rows first, then the rest.
403
+ */
404
+ function sectionMembers(members, pinned) {
405
+ const leading = [];
406
+ const rest = [];
407
+ for (const member of members) if (pinned.has(member.id)) leading.push(member);
408
+ else rest.push(member);
409
+ return [...leading, ...rest];
410
+ }
411
+ /**
412
+ * Derive the workspace browser groups with every session as a top-level row.
413
+ *
414
+ * Every group shows; sessions populate under expanded groups in the selected
415
+ * local order. Blank sessions are excluded except for the selected
416
+ * provisional New Session row; archived sessions are excluded everywhere.
417
+ * Content search lives outside this derivation
418
+ * (see {@link deriveSearchResults}).
419
+ * @param list - sessions list snapshot (`mainView` retention feeds containsCurrent).
420
+ * @param workspaces - real Workspaces in Host group order with caller-projected Session order.
421
+ * @param rowState - registry-global pin and archive membership.
422
+ * @param statuses - unified UI status by Session.
423
+ * @param view - local expansion arrays.
424
+ * @returns group sections in render order.
425
+ */
426
+ function deriveGroups(list, workspaces, rowState, statuses, view) {
427
+ const archived = new Set(rowState.archivedSessionIds);
428
+ const pinned = new Set(rowState.pinnedSessionIds);
429
+ const expandedGroups = new Set(view.expandedGroups);
430
+ const descendants = indexSubagentDescendants(list.byId);
431
+ const current = mainSessionId(list);
432
+ const currentGroup = current === void 0 ? void 0 : owningGroupKey(workspaces, current);
433
+ const groups = [];
434
+ for (const g of groupByWorkspace(list, workspaces, archived, view.ungroupedOrder)) {
435
+ const expanded = expandedGroups.has(g.key);
436
+ groups.push({
437
+ key: g.key,
438
+ workspaceId: g.workspaceId,
439
+ cwd: g.cwd,
440
+ createdAt: g.createdAt,
441
+ label: g.label,
442
+ sessionCount: g.sessions.length,
443
+ expanded,
444
+ containsCurrent: g.key === currentGroup,
445
+ sessions: expanded ? sectionMembers(g.sessions, pinned).map((session) => sessionNode(session, descendants, statuses, pinned)) : []
446
+ });
447
+ }
448
+ return groups;
449
+ }
450
+ /**
451
+ * Select flat-list members without deriving row presentation or ordering.
452
+ * @param list - sessions list snapshot.
453
+ * @param rowState - registry-global pin and archive membership.
454
+ * @returns known visible Session ids in list order, including ordinary forks and only the current blank.
455
+ */
456
+ function visibleSessionIds(list, rowState) {
457
+ return memberIds(list, new Set(rowState.archivedSessionIds));
458
+ }
459
+ /**
460
+ * Select complete flat-list membership, independently of archive visibility.
461
+ * The pin write-back reconciles this account, so an archived Session keeps its
462
+ * slot and no pin silently drops it.
463
+ * @param list - sessions list snapshot.
464
+ * @returns known ordinary Session ids, including archived ones.
465
+ */
466
+ function sessionMemberIds(list) {
467
+ return memberIds(list, /* @__PURE__ */ new Set());
468
+ }
469
+ function memberIds(list, archived) {
470
+ const current = mainSessionId(list);
471
+ return list.ids.filter((id) => {
472
+ const s = list.byId[id];
473
+ return s !== void 0 && sessionVisible(s, current, archived);
474
+ });
475
+ }
476
+ /**
477
+ * Derive flat rows from the browser's ordered visible Session ids.
478
+ * @param list - sessions list snapshot used to select the ids.
479
+ * @param sessionIds - known visible members in caller order, including any pinned blank.
480
+ * @param pinnedSessionIds - registry-global pin set; pinned rows lead after the blank.
481
+ * @param statuses - unified UI status by Session.
482
+ * @returns flat rows with pinned rows fronted and current status indicators.
483
+ */
484
+ function deriveFlat(list, sessionIds, pinnedSessionIds, statuses) {
485
+ const descendants = indexSubagentDescendants(list.byId);
486
+ const pinned = new Set(pinnedSessionIds);
487
+ return sectionMembers(sessionIds.map((id) => list.byId[id]), pinned).map((session) => sessionNode(session, descendants, statuses, pinned));
488
+ }
489
+ /**
490
+ * Merge immediate title/Workspace substring matches with ranked Host content
491
+ * matches. Local rows lead newest-first, content-only rows retain backend
492
+ * order, and duplicate sessions receive the backend snippet in place.
493
+ * @param list - session metadata authority.
494
+ * @param workspaces - Workspace membership and display labels.
495
+ * @param query - caller text; surrounding whitespace is ignored.
496
+ * @param rowState - registry-global archive membership (members never match).
497
+ * @param statuses - unified UI status by Session.
498
+ * @param content - ranked Host content-search page.
499
+ * @param limit - protocol-owned maximum merged row count.
500
+ * @returns bounded deduplicated flat rows and a refine-query hint bit.
501
+ */
502
+ function deriveSearchResults(list, workspaces, query, rowState, statuses, content, limit) {
503
+ const q = query.trim().toLowerCase();
504
+ if (q === "") return {
505
+ items: [],
506
+ hasMore: false
507
+ };
508
+ const archived = new Set(rowState.archivedSessionIds);
509
+ const descendants = indexSubagentDescendants(list.byId);
510
+ const current = mainSessionId(list);
511
+ const workspaceBySession = /* @__PURE__ */ new Map();
512
+ for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) if (!workspaceBySession.has(sessionId)) workspaceBySession.set(sessionId, workspace.title);
513
+ const labelOf = (summary) => workspaceBySession.get(summary.id) ?? workspaceLabel(summary.cwd);
514
+ const contentBySession = /* @__PURE__ */ new Map();
515
+ for (const item of content.items) if (!contentBySession.has(item.sessionId)) contentBySession.set(item.sessionId, item);
516
+ const local = [];
517
+ for (const id of list.ids) {
518
+ const summary = list.byId[id];
519
+ if (summary === void 0 || summary.blank || !sessionVisible(summary, current, archived)) continue;
520
+ if (sessionTitle(summary).toLowerCase().includes(q) || labelOf(summary).toLowerCase().includes(q)) local.push(summary);
521
+ }
522
+ const localById = new Map(local.map((summary) => [summary.id, summary]));
523
+ const orderedLocal = orderByRecency(local.map((summary) => summary.id), list.byId).map((id) => localById.get(id));
524
+ const ordered = [];
525
+ const included = /* @__PURE__ */ new Set();
526
+ const include = (summary) => {
527
+ if (included.has(summary.id)) return;
528
+ included.add(summary.id);
529
+ ordered.push(summary);
530
+ };
531
+ for (const summary of orderedLocal) include(summary);
532
+ for (const item of content.items) {
533
+ const summary = list.byId[item.sessionId];
534
+ if (summary !== void 0 && !summary.blank && sessionVisible(summary, current, archived)) include(summary);
535
+ }
536
+ return {
537
+ items: ordered.slice(0, limit).map((summary) => {
538
+ const match = contentBySession.get(summary.id);
539
+ const status = statuses.get(summary.id);
540
+ const pendingInteraction = visiblePendingKind(status?.pendingInteraction?.kind);
541
+ return {
542
+ id: summary.id,
543
+ title: sessionTitle(summary),
544
+ workspace: labelOf(summary),
545
+ running: status?.running ?? summary.running,
546
+ runningSubagentCount: descendants.get(summary.id)?.runningCount ?? 0,
547
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction },
548
+ completed: status?.completionUnread === true,
549
+ ...match === void 0 ? {} : { snippet: match.snippet }
550
+ };
551
+ }),
552
+ hasMore: content.hasMore || ordered.length > limit
553
+ };
554
+ }
555
+ /** Normalize separators for comparison without interpreting POSIX backslashes as separators. */
556
+ function folderPath(path) {
557
+ return (/^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") ? path.replaceAll("\\", "/") : path).replace(/\/+$/, "");
558
+ }
559
+ /**
560
+ * Find the nearest registered ancestor, excluding the Workspace directory itself.
561
+ * Paths use Host spelling; matching is case-sensitive, like Workspace identity.
562
+ * @param path - Workspace directory.
563
+ * @param parents - registered Workspace directory paths.
564
+ * @returns the owning parent path, or undefined when no parent contains the Workspace.
565
+ */
566
+ function owningParentFolder(path, parents) {
567
+ const child = folderPath(path);
568
+ let owner;
569
+ let length = -1;
570
+ for (const parent of parents) {
571
+ const root = folderPath(parent);
572
+ if (root.length > length && child !== root && child.startsWith(`${root}/`)) {
573
+ owner = parent;
574
+ length = root.length;
575
+ }
576
+ }
577
+ return owner;
578
+ }
579
+ //#endregion
580
+ //#region lib/types/client/stores.js
581
+ /**
582
+ * The workspace browser's viewing store: the session-list grouping mode,
583
+ * persisted across reloads. Module level exports the factory only (a
584
+ * module-level handle would pin the store identity across plugin reloads);
585
+ * register() receives the factory and the browser derives its PropsStore
586
+ * share from the return type.
587
+ */
588
+ /** Browser-local order account for the hierarchy-free flat Session list. */
589
+ const FLAT_SESSION_ORDER_KEY = "__flat_session_order__";
590
+ /** Copy read-only projections into the persisted mutable store representation. */
591
+ function copySessionOrders(orders) {
592
+ return Object.fromEntries(Object.entries(orders).map(([key, order]) => [key, [...order]]));
593
+ }
594
+ /**
595
+ * Create the workspace browser viewing store handle.
596
+ * @returns the store handle (spec + type + identity + factory in one).
597
+ */
598
+ function createWorkspaceViewStore() {
599
+ return (0, _qilin_agent_client_store.defineStore)({
600
+ init: () => ({
601
+ groupBy: "workspace",
602
+ orderBy: "updated",
603
+ groupExpansion: {},
604
+ sessionOrderByAccount: {}
605
+ }),
606
+ persist: "qilin.workspace.view.v5",
607
+ actions: {
608
+ setGroupBy: (d, mode) => {
609
+ d.groupBy = mode;
610
+ },
611
+ setOrderBy: (d, mode, initialOrders) => {
612
+ if (mode === d.orderBy) return;
613
+ d.sessionOrderByAccount = mode === "manual" ? copySessionOrders(initialOrders) : {};
614
+ d.orderBy = mode;
615
+ },
616
+ setGroupExpanded: (d, key, expanded) => {
617
+ d.groupExpansion[key] = expanded;
618
+ },
619
+ retainAccountKeys: (d, workspaceKeys) => {
620
+ const retained = new Set(workspaceKeys);
621
+ d.groupExpansion = Object.fromEntries(Object.entries(d.groupExpansion).filter(([key]) => retained.has(key)));
622
+ d.sessionOrderByAccount = Object.fromEntries(Object.entries(d.sessionOrderByAccount).filter(([key]) => retained.has(key)));
623
+ delete d.sessionUpdatedAtByAccount;
624
+ },
625
+ syncSessionOrders: (d, orders) => {
626
+ if (d.orderBy !== "manual") return;
627
+ Object.assign(d.sessionOrderByAccount, copySessionOrders(orders));
628
+ },
629
+ setSessionOrder: (d, accountKey, order, initialOrders) => {
630
+ if (d.orderBy === "updated") d.sessionOrderByAccount = copySessionOrders(initialOrders);
631
+ d.orderBy = "manual";
632
+ d.sessionOrderByAccount[accountKey] = [...order];
633
+ },
634
+ pinSessionOrder: (d, sessionId, accountKeys, source) => {
635
+ const selected = new Set(accountKeys);
636
+ d.sessionOrderByAccount = Object.fromEntries(Object.entries(source.members).map(([key, members]) => {
637
+ const order = reconcileManualOrder(members, d.sessionOrderByAccount[key], source.summaries);
638
+ return [key, selected.has(key) ? [sessionId, ...order.filter((id) => id !== sessionId)] : order];
639
+ }));
640
+ }
641
+ }
642
+ });
643
+ }
644
+ //#endregion
645
+ //#region lib/types/client/pin-order.js
646
+ /**
647
+ * Every account's complete membership: each Workspace, Ungrouped, and the flat list.
648
+ * @param workspaces - current Host Workspaces.
649
+ * @param list - current Session list snapshot.
650
+ * @returns membership per account key plus the summaries a reconciliation orders by.
651
+ */
652
+ function pinOrderSource(workspaces, list) {
653
+ const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
654
+ return {
655
+ members: Object.fromEntries([
656
+ ...workspaces.map((workspace) => [workspace.workspaceId, workspace.sessionIds]),
657
+ ["", list.ids.filter((id) => list.byId[id] !== void 0 && !accounted.has(id))],
658
+ [FLAT_SESSION_ORDER_KEY, sessionMemberIds(list)]
659
+ ]),
660
+ summaries: list.byId
661
+ };
662
+ }
663
+ /**
664
+ * The accounts a pinned Session leads: its group (or Ungrouped) and the flat list.
665
+ * @param workspaces - current Host Workspaces.
666
+ * @param sessionId - the Session being pinned.
667
+ * @returns the account keys a pin write fronts.
668
+ */
669
+ function pinOrderAccounts(workspaces, sessionId) {
670
+ return [owningGroupKey(workspaces, sessionId), FLAT_SESSION_ORDER_KEY];
671
+ }
672
+ //#endregion
673
+ //#region lib/types/client/navigation.js
674
+ /** Workspace archive and directory UI capability. */
675
+ /** Structured directory failure exposed to directory UI consumers. */
676
+ var DirectoryBrowseError = class extends Error {
677
+ rpcError;
678
+ name = "DirectoryBrowseError";
679
+ /** @param rpcError - Host directory business failure. */
680
+ constructor(rpcError) {
681
+ super(`directory browse failed: ${rpcError.code}: ${rpcError.message}`);
682
+ this.rpcError = rpcError;
683
+ }
684
+ };
685
+ /** Implements Workspace archive and directory UI operations. */
686
+ var UiWorkspaceService = class extends _qilin_agent_kylin.Service {
687
+ directoryPicker;
688
+ agentPresets;
689
+ workbench;
690
+ workspaces;
691
+ sessions;
692
+ view;
693
+ connecting = /* @__PURE__ */ new Map();
694
+ lifetime = new AbortController();
695
+ /**
696
+ * The persisted main-pane selection. Published as the `mainSelection` root
697
+ * standard source, because the Session Controller owns the catalog and leaves
698
+ * view selection to navigation.
699
+ */
700
+ selection = (0, _qilin_agent_client_store.createSnapshotStore)({}, { persist: { name: "qilin.sessions.current" } });
701
+ mainReference;
702
+ /**
703
+ * @param ctx - Client root Context.
704
+ * @param directoryPicker - the directory-picking Remote namespace.
705
+ * @param agentPresets - the preset-switch caller, for the blank-session
706
+ * tag rebind (D3: a blank session may switch presets; a started one may not).
707
+ * @param workbench - the tag state owner, supplying each new task's preset
708
+ * and the visibility fold the reuse rule checks.
709
+ * @param workspaces - pure Workspace Controller.
710
+ * @param sessions - pure Session Controller.
711
+ * @param view - the browser's viewing-store write set, which owns saved Session order.
712
+ */
713
+ constructor(ctx, directoryPicker, agentPresets, workbench, workspaces, sessions, view) {
714
+ super(ctx, "uiWorkspace");
715
+ this.directoryPicker = directoryPicker;
716
+ this.agentPresets = agentPresets;
717
+ this.workbench = workbench;
718
+ this.workspaces = workspaces;
719
+ this.sessions = sessions;
720
+ this.view = view;
721
+ ctx.effect(() => {
722
+ const stop = this.watchNavigation();
723
+ return () => {
724
+ stop();
725
+ this.lifetime.abort();
726
+ const reference = this.mainReference;
727
+ this.mainReference = void 0;
728
+ reference?.release();
729
+ };
730
+ }, "ui-workspace: Workspace navigation policy");
731
+ }
732
+ async connectWorkspace(workspaceId) {
733
+ const workspace = this.workspaces.list.getSnapshot().items.find((item) => item.workspaceId === workspaceId);
734
+ if (workspace === void 0) throw new Error(`uiWorkspace.connectWorkspace: unknown workspace ${workspaceId}`);
735
+ const inflight = this.connecting.get(workspaceId);
736
+ if (inflight !== void 0) return inflight;
737
+ const archived = this.workspaces.list.getSnapshot().archivedSessionIds;
738
+ const sessions = this.sessions.list.getSnapshot();
739
+ for (const id of sessions.ids) {
740
+ const summary = sessions.byId[id];
741
+ if (summary !== void 0 && summary.blank && summary.cwd === workspace.path && workspace.sessionIds.includes(summary.id) && !archived.includes(summary.id)) {
742
+ this.rebindBlankToActiveTag(id);
743
+ return summary.id;
744
+ }
745
+ }
746
+ const attempt = this.sessions.create({
747
+ workspaceId,
748
+ agentPreset: this.workbench.presetFor(this.workbench.state.getSnapshot().active)
749
+ }).finally(() => {
750
+ this.connecting.delete(workspaceId);
751
+ });
752
+ this.connecting.set(workspaceId, attempt);
753
+ return attempt;
754
+ }
755
+ /**
756
+ * Rebind every blank session of the current Workspace to the now-active
757
+ * tag's preset. Blank sessions carried over from the other tag would
758
+ * otherwise sit hidden in this tag's filtered list; a non-blank session
759
+ * stays exactly as it is (the Host refuses non-blank switches anyway).
760
+ */
761
+ rebindBlanksAfterTagSwitch() {
762
+ const current = this.mainReference?.sessionId;
763
+ const workspace = current === void 0 ? void 0 : this.workspaces.list.getSnapshot().items.find((item) => item.sessionIds.includes(current));
764
+ if (workspace === void 0) return;
765
+ for (const id of workspace.sessionIds) if (this.sessions.list.getSnapshot().byId[id]?.blank === true) this.rebindBlankToActiveTag(id);
766
+ }
767
+ /**
768
+ * Switch one blank session to the active tag's preset when the preset it
769
+ * was created under does not show under this tag. Fire-and-forget: the
770
+ * session stays usable either way, and a `locked` verdict means it engaged
771
+ * mid-rebind, so its own preset stands.
772
+ * @param id - the blank session to align.
773
+ */
774
+ rebindBlankToActiveTag(id) {
775
+ const summary = this.sessions.list.getSnapshot().byId[id];
776
+ if (summary === void 0) return;
777
+ const tag = this.workbench.state.getSnapshot().active;
778
+ if (this.workbench.shows(summary.projectionValues?.agentPreset, tag)) return;
779
+ this.agentPresets.select(id, this.workbench.presetFor(tag)).catch((reason) => {
780
+ console.warn(`blank session ${id} kept its own preset:`, reason);
781
+ });
782
+ }
783
+ adoptBlankSessionPreset(presetId) {
784
+ const current = this.mainReference?.sessionId;
785
+ if (current === void 0) return;
786
+ if (this.sessions.list.getSnapshot().byId[current]?.blank !== true) return;
787
+ this.agentPresets.select(current, presetId).catch((reason) => {
788
+ console.warn(`blank session ${current} kept its own preset:`, reason);
789
+ });
790
+ }
791
+ openSession(target) {
792
+ this.replaceMain(target, this.lifetime.signal);
793
+ }
794
+ async openWorkspace(workspaceId, beforeOpen) {
795
+ const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
796
+ const sessionId = await this.connectWorkspace(workspaceId);
797
+ if (navigation.aborted) return;
798
+ this.replaceMain(sessionId, navigation, beforeOpen);
799
+ }
800
+ async forkSession(sessionId) {
801
+ const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
802
+ const childId = await this.sessions.fork({
803
+ sessionId,
804
+ increaseTitle: true
805
+ });
806
+ if (!navigation.aborted) this.replaceMain(childId, navigation);
807
+ }
808
+ startSession(workspaceId) {
809
+ const workspace = this.workspaces.list.getSnapshot();
810
+ const sessions = this.sessions.list.getSnapshot();
811
+ const current = this.mainReference?.sessionId;
812
+ const currentWorkspaceId = current === void 0 ? void 0 : workspace.items.find((item) => item.sessionIds.includes(current))?.workspaceId;
813
+ const recent = workspace.phase === "ready" && sessions.phase === "ready" ? recentWorkspace(workspace.items, sessions.byId) : void 0;
814
+ const target = workspaceId ?? currentWorkspaceId ?? recent;
815
+ if (target === void 0) {
816
+ this.clearMain();
817
+ return;
818
+ }
819
+ this.openWorkspace(target).catch((reason) => {
820
+ console.warn("new session failed:", reason);
821
+ });
822
+ }
823
+ async archiveSession(sessionId) {
824
+ await this.workspaces.archiveSession(sessionId);
825
+ if (this.mainReference?.sessionId === sessionId) this.clearMain();
826
+ }
827
+ async unarchiveSession(sessionId) {
828
+ await this.workspaces.unarchiveSession(sessionId);
829
+ }
830
+ async pinSession(sessionId) {
831
+ await this.workspaces.pinSession(sessionId);
832
+ const { items } = this.workspaces.list.getSnapshot();
833
+ this.view.pinSessionOrder(sessionId, pinOrderAccounts(items, sessionId), pinOrderSource(items, this.sessions.list.getSnapshot()));
834
+ }
835
+ async unpinSession(sessionId) {
836
+ await this.workspaces.unpinSession(sessionId);
837
+ }
838
+ async pickDirectory() {
839
+ const result = await this.directoryPicker.pick();
840
+ if (!result.ok) throw new Error(`directory picker failed: ${result.error.message}`);
841
+ return result.value;
842
+ }
843
+ async listDirectory(path, signal) {
844
+ const result = await this.directoryPicker.list(path, signal);
845
+ if (!result.ok) throw new DirectoryBrowseError(result.error);
846
+ return result.value;
847
+ }
848
+ async createDirectory(path, name) {
849
+ const result = await this.directoryPicker.createDirectory(path, name);
850
+ if (!result.ok) throw new DirectoryBrowseError(result.error);
851
+ return result.value;
852
+ }
853
+ watchNavigation() {
854
+ let initial = "waiting";
855
+ const reconcile = () => {
856
+ if (this.lifetime.signal.aborted) return;
857
+ if (this.clearArchivedCurrent()) return;
858
+ if (initial !== "waiting") return;
859
+ const workspace = this.workspaces.list.getSnapshot();
860
+ const sessions = this.sessions.list.getSnapshot();
861
+ if (workspace.phase !== "ready" || sessions.phase !== "ready") return;
862
+ if (this.mainReference !== void 0) {
863
+ initial = "done";
864
+ return;
865
+ }
866
+ const saved = this.selection.getSnapshot();
867
+ const savedTarget = saved.subagentAddress ?? (saved.sessionId !== void 0 && sessions.byId[saved.sessionId] !== void 0 ? saved.sessionId : void 0);
868
+ if (savedTarget !== void 0) {
869
+ initial = "connecting";
870
+ try {
871
+ if (saved.subagentAddress !== void 0) this.sessions.refreshProjections(saved.subagentAddress.parentSessionId);
872
+ this.openSession(savedTarget);
873
+ initial = "done";
874
+ } catch (reason) {
875
+ initial = "waiting";
876
+ console.warn("initial Session restoration failed:", reason);
877
+ }
878
+ return;
879
+ }
880
+ const target = recentWorkspace(workspace.items, sessions.byId);
881
+ if (target === void 0) {
882
+ initial = "done";
883
+ return;
884
+ }
885
+ initial = "connecting";
886
+ this.connectWorkspace(target).then((sessionId) => {
887
+ if (this.mainReference === void 0) this.openSession(sessionId);
888
+ }).then(() => {
889
+ initial = "done";
890
+ }, (reason) => {
891
+ if (this.lifetime.signal.aborted) return;
892
+ initial = "waiting";
893
+ console.warn("initial workspace selection failed:", reason);
894
+ });
895
+ };
896
+ const disposeWorkspaces = this.workspaces.list.subscribe(reconcile);
897
+ const disposeSessions = this.sessions.list.subscribe(reconcile);
898
+ reconcile();
899
+ return () => {
900
+ this.lifetime.abort();
901
+ disposeSessions();
902
+ disposeWorkspaces();
903
+ };
904
+ }
905
+ /** @returns true when an archived current selection was cleared. */
906
+ clearArchivedCurrent() {
907
+ const current = this.mainReference?.sessionId;
908
+ if (current === void 0 || !this.workspaces.list.getSnapshot().archivedSessionIds.includes(current)) return false;
909
+ this.clearMain();
910
+ return true;
911
+ }
912
+ clearMain() {
913
+ const previous = this.mainReference;
914
+ this.mainReference = void 0;
915
+ this.selection.set({});
916
+ previous?.release();
917
+ this.ctx.layout.selectPanel(null);
918
+ }
919
+ replaceMain(target, signal, beforeOpen) {
920
+ signal.throwIfAborted();
921
+ const reference = this.sessions.retain(target, { source: "mainView" });
922
+ try {
923
+ signal.throwIfAborted();
924
+ beforeOpen?.(reference.sessionId);
925
+ if (signal.aborted) {
926
+ reference.release();
927
+ return;
928
+ }
929
+ const subagentAddress = typeof target === "string" ? this.sessions.subagentAddress(reference.sessionId) : target;
930
+ this.selection.set({
931
+ sessionId: reference.sessionId,
932
+ ...subagentAddress === void 0 ? {} : { subagentAddress }
933
+ });
934
+ } catch (error) {
935
+ reference.release();
936
+ throw error;
937
+ }
938
+ const previous = this.mainReference;
939
+ this.mainReference = reference;
940
+ previous?.release();
941
+ this.sessions.refreshProjections(reference.sessionId);
942
+ this.ctx.layout.selectPanel(null);
943
+ }
944
+ };
945
+ /** Stable tie-breaking follows Host Workspace order. */
946
+ function recentWorkspace(workspaces, sessions) {
947
+ let selected;
948
+ let selectedTime = Number.NEGATIVE_INFINITY;
949
+ for (const workspace of workspaces) {
950
+ let latest = Number.NEGATIVE_INFINITY;
951
+ for (const sessionId of workspace.sessionIds) {
952
+ const session = sessions[sessionId];
953
+ if (session !== void 0) latest = Math.max(latest, session.updatedAt);
954
+ }
955
+ if (latest === Number.NEGATIVE_INFINITY) latest = Date.parse(workspace.createdAt);
956
+ if (selected === void 0 || latest > selectedTime) {
957
+ selected = workspace.workspaceId;
958
+ selectedTime = latest;
959
+ }
960
+ }
961
+ return selected;
962
+ }
963
+ //#endregion
964
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/AgentPresetChip.module.css.mjs
965
+ const css$5 = "._2_v9Qa_chip{max-width:min(100%,360px);min-height:28px;color:var(--qilin-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:16px;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}._2_v9Qa_chip:hover,._2_v9Qa_chip[aria-expanded=true]{background:var(--qilin-alias-interactive-bg-hover)}._2_v9Qa_icon{color:var(--qilin-alias-label-primary);flex:none}._2_v9Qa_label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._2_v9Qa_chevron{color:var(--qilin-alias-label-caption);flex:none}._2_v9Qa_item{flex-direction:column;gap:2px;min-width:0;display:flex}._2_v9Qa_itemName{font-weight:500}._2_v9Qa_itemDesc{color:var(--qilin-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}._2_v9Qa_menuAnchor{z-index:10}";
966
+ const tagId$5 = "@qilin-agent/client-ui-workspace/AgentPresetChip.module.css";
967
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
968
+ const tag = document.createElement("style");
969
+ tag.dataset.plugin = "@qilin-agent/client-ui-workspace";
970
+ tag.dataset.pluginCss = tagId$5;
971
+ tag.textContent = css$5;
972
+ document.head.appendChild(tag);
973
+ }
974
+ var AgentPresetChip_module_css_default = {
975
+ "chevron": "_2_v9Qa_chevron",
976
+ "chip": "_2_v9Qa_chip",
977
+ "icon": "_2_v9Qa_icon",
978
+ "item": "_2_v9Qa_item",
979
+ "itemDesc": "_2_v9Qa_itemDesc",
980
+ "itemName": "_2_v9Qa_itemName",
981
+ "label": "_2_v9Qa_label",
982
+ "menuAnchor": "_2_v9Qa_menuAnchor"
983
+ };
984
+ //#endregion
985
+ //#region lib/types/client/AgentPresetChip.js
986
+ /**
987
+ * The hero run-mode chip, beside the workspace picker on the new-task screen:
988
+ * which preset the next task in this workbench tag composes from (D3). The
989
+ * general and coding tags carry their own defaults (standard / ptc); the chip
990
+ * exists so the creator preset stays one pick away. The choice is remembered
991
+ * per tag through the workbench state, and the on-screen blank session is
992
+ * rebound immediately so the staged task already carries it.
993
+ *
994
+ * The chip hides while the roster is unavailable, failed, or offers fewer
995
+ * than two choices for the tag — a selector over one entry is noise.
996
+ */
997
+ /**
998
+ * Render the run-mode chip.
999
+ * @param props - the bound workbench and roster selector hooks, the tag
1000
+ * vocabulary and pick actions, and the locale seat.
1001
+ * @returns the chip, or null when there is nothing to choose.
1002
+ */
1003
+ function AgentPresetChip({ useWorkbench, useRoster, tagChoices, pick, t }) {
1004
+ const workbench = useWorkbench((snapshot) => snapshot);
1005
+ const roster = useRoster((snapshot) => snapshot);
1006
+ const [open, setOpen] = (0, react.useState)(false);
1007
+ const tag = workbench.active;
1008
+ const options = (0, react.useMemo)(() => {
1009
+ const byId = new Map(roster.presets.map((preset) => [preset.id, preset]));
1010
+ const shipped = tagChoices(tag).map((id) => byId.get(id)).filter((preset) => preset !== void 0);
1011
+ const known = new Set([...tagChoices("general"), ...tagChoices("coding")]);
1012
+ const custom = roster.presets.filter((preset) => !known.has(preset.id));
1013
+ return [...shipped, ...custom];
1014
+ }, [
1015
+ roster.presets,
1016
+ tag,
1017
+ tagChoices
1018
+ ]);
1019
+ if (roster.status !== "ready" || options.length < 2) return null;
1020
+ const currentId = workbench.presets[tag];
1021
+ const currentName = options.find((option) => option.id === currentId)?.name ?? currentId;
1022
+ return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
1023
+ open,
1024
+ onClose: () => {
1025
+ setOpen(false);
1026
+ },
1027
+ items: options.map((option) => ({
1028
+ id: option.id,
1029
+ label: (0, react_jsx_runtime.jsxs)("span", {
1030
+ className: AgentPresetChip_module_css_default.item,
1031
+ children: [(0, react_jsx_runtime.jsx)("span", {
1032
+ className: AgentPresetChip_module_css_default.itemName,
1033
+ children: option.name ?? option.id
1034
+ }), option.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
1035
+ className: AgentPresetChip_module_css_default.itemDesc,
1036
+ children: option.description
1037
+ })]
1038
+ })
1039
+ })),
1040
+ selectedId: currentId,
1041
+ onSelect: (id) => {
1042
+ setOpen(false);
1043
+ pick(id);
1044
+ },
1045
+ align: "start",
1046
+ portal: true,
1047
+ className: AgentPresetChip_module_css_default.menuAnchor,
1048
+ anchor: (0, react_jsx_runtime.jsxs)("button", {
1049
+ type: "button",
1050
+ className: AgentPresetChip_module_css_default.chip,
1051
+ "aria-label": t("preset.aria"),
1052
+ "aria-haspopup": "menu",
1053
+ "aria-expanded": open,
1054
+ title: t("preset.hint"),
1055
+ onClick: () => {
1056
+ setOpen((value) => !value);
1057
+ },
1058
+ children: [
1059
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconAgentPresetOutline16, { className: AgentPresetChip_module_css_default.icon }),
1060
+ (0, react_jsx_runtime.jsx)("span", {
1061
+ className: AgentPresetChip_module_css_default.label,
1062
+ children: currentName
1063
+ }),
1064
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, { className: AgentPresetChip_module_css_default.chevron })
1065
+ ]
1066
+ })
1067
+ });
1068
+ }
1069
+ //#endregion
1070
+ //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
1071
+ function r(e) {
1072
+ var t, f, n = "";
1073
+ if ("string" == typeof e || "number" == typeof e) n += e;
1074
+ else if ("object" == typeof e) if (Array.isArray(e)) {
1075
+ var o = e.length;
1076
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
1077
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
1078
+ return n;
1079
+ }
1080
+ function clsx() {
1081
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
1082
+ return n;
1083
+ }
1084
+ //#endregion
1085
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/rows/WorkbenchSwitch.module.css.mjs
1086
+ const css$4 = ".n6Fpha_switch{align-items:center;gap:6px;display:inline-flex}.n6Fpha_tab{height:28px;color:var(--qilin-alias-label-secondary);font-size:var(--dsh-content-font-size-secondary,13px);corner-shape:round;cursor:pointer;transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out);background:0 0;border:1px solid #0000;border-radius:999px;flex:none;justify-content:center;align-items:center;gap:5px;padding:0 8px;line-height:1;display:inline-flex}.n6Fpha_tab:hover{background:var(--qilin-alias-interactive-bg-hover)}.n6Fpha_activePill{color:var(--qilin-alias-brand-primary-new-colorprimary-new-color);border-color:currentColor;padding:0 11px}.n6Fpha_inactiveIcon{width:28px;padding:0}.n6Fpha_label{white-space:nowrap}";
1087
+ const tagId$4 = "@qilin-agent/client-ui-workspace/WorkbenchSwitch.module.css";
1088
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
1089
+ const tag = document.createElement("style");
1090
+ tag.dataset.plugin = "@qilin-agent/client-ui-workspace";
1091
+ tag.dataset.pluginCss = tagId$4;
1092
+ tag.textContent = css$4;
1093
+ document.head.appendChild(tag);
1094
+ }
1095
+ var WorkbenchSwitch_module_css_default = {
1096
+ "activePill": "n6Fpha_activePill",
1097
+ "inactiveIcon": "n6Fpha_inactiveIcon",
1098
+ "label": "n6Fpha_label",
1099
+ "switch": "n6Fpha_switch",
1100
+ "tab": "n6Fpha_tab"
1101
+ };
1102
+ //#endregion
1103
+ //#region lib/types/client/rows/WorkbenchSwitch.js
1104
+ /** Constant visual order: coding first (left), general second (right). */
1105
+ const ENTRIES = ["coding", "general"];
1106
+ /**
1107
+ * Render the two-tag switch. Both tags stay mounted and reachable — only the
1108
+ * label of the inactive one is hidden — so the tab semantics of the previous
1109
+ * segmented control carry over: roving tab stop on the selected tag,
1110
+ * Left/Right to move selection.
1111
+ * @param props.value - the active tag.
1112
+ * @param props.onChange - selection requested by click or Left/Right.
1113
+ * @param props.labels - localized tag names, keyed by tag; the inactive
1114
+ * button's accessible name (its label is hidden).
1115
+ * @param props.ariaLabel - localized accessible name for the tab list.
1116
+ * @param props.panelId - DOM id of the panel the tabs control.
1117
+ * @returns the tab list, without its panel.
1118
+ */
1119
+ function WorkbenchSwitch({ value, onChange, labels, ariaLabel, panelId }) {
1120
+ const onKeyDown = (event, index) => {
1121
+ let next;
1122
+ switch (event.key) {
1123
+ case "ArrowLeft":
1124
+ next = (index + ENTRIES.length - 1) % ENTRIES.length;
1125
+ break;
1126
+ case "ArrowRight":
1127
+ next = (index + 1) % ENTRIES.length;
1128
+ break;
1129
+ default: return;
1130
+ }
1131
+ event.preventDefault();
1132
+ event.stopPropagation();
1133
+ const tablist = event.currentTarget.parentElement;
1134
+ const nextTag = ENTRIES[next];
1135
+ /* v8 ignore next -- the event comes from a mounted direct child and next is bounded by the fixed pair. */
1136
+ if (tablist === null || nextTag === void 0) return;
1137
+ tablist.querySelectorAll("[role=\"tab\"]").item(next).focus();
1138
+ onChange(nextTag);
1139
+ };
1140
+ return (0, react_jsx_runtime.jsx)("div", {
1141
+ role: "tablist",
1142
+ "aria-label": ariaLabel,
1143
+ className: WorkbenchSwitch_module_css_default.switch,
1144
+ children: ENTRIES.map((tag, index) => {
1145
+ const active = value === tag;
1146
+ return (0, react_jsx_runtime.jsxs)("button", {
1147
+ type: "button",
1148
+ role: "tab",
1149
+ id: `workbench-tab-${tag}`,
1150
+ "aria-selected": active,
1151
+ "aria-controls": panelId,
1152
+ "aria-label": labels[tag],
1153
+ tabIndex: active ? 0 : -1,
1154
+ title: labels[tag],
1155
+ className: clsx(WorkbenchSwitch_module_css_default.tab, active ? WorkbenchSwitch_module_css_default.activePill : WorkbenchSwitch_module_css_default.inactiveIcon),
1156
+ onClick: () => {
1157
+ onChange(tag);
1158
+ },
1159
+ onKeyDown: (event) => {
1160
+ onKeyDown(event, index);
1161
+ },
1162
+ children: [tag === "coding" ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconTerminalOutline16, { size: 14 }) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconSparkle16, { size: 14 }), active && (0, react_jsx_runtime.jsx)("span", {
1163
+ className: WorkbenchSwitch_module_css_default.label,
1164
+ children: labels[tag]
1165
+ })]
1166
+ }, tag);
1167
+ })
1168
+ });
1169
+ }
1170
+ //#endregion
1171
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/WorkbenchSwitchSeat.module.css.mjs
1172
+ const css$3 = ".ASIh6a_seat{flex:none;padding:0 12px 8px}";
1173
+ const tagId$3 = "@qilin-agent/client-ui-workspace/WorkbenchSwitchSeat.module.css";
1174
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1175
+ const tag = document.createElement("style");
1176
+ tag.dataset.plugin = "@qilin-agent/client-ui-workspace";
1177
+ tag.dataset.pluginCss = tagId$3;
1178
+ tag.textContent = css$3;
1179
+ document.head.appendChild(tag);
1180
+ }
1181
+ var WorkbenchSwitchSeat_module_css_default = { "seat": "ASIh6a_seat" };
1182
+ //#endregion
1183
+ //#region lib/types/client/WorkbenchSwitchSeat.js
1184
+ /**
1185
+ * Render the switch seat.
1186
+ * @param props - shell owner share (wide), the bound workbench selector hook,
1187
+ * the switch callback, and the locale seat.
1188
+ * @returns the switch row, or null on the rail.
1189
+ */
1190
+ function WorkbenchSwitchSeat({ wide, useWorkbench, onWorkbenchSwitch, t }) {
1191
+ const workbench = useWorkbench((state) => state);
1192
+ if (!wide) return null;
1193
+ return (0, react_jsx_runtime.jsx)("div", {
1194
+ className: WorkbenchSwitchSeat_module_css_default.seat,
1195
+ children: (0, react_jsx_runtime.jsx)(WorkbenchSwitch, {
1196
+ value: workbench.active,
1197
+ onChange: onWorkbenchSwitch,
1198
+ labels: {
1199
+ general: t("workbench.general"),
1200
+ coding: t("workbench.coding")
1201
+ },
1202
+ ariaLabel: t("workbench.tabs.aria"),
1203
+ panelId: "workbench-list-panel"
1204
+ })
1205
+ });
1206
+ }
1207
+ //#endregion
1208
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/rows/Rows.module.css.mjs
1209
+ const css$2 = ".RrwzeW_projectRow,.RrwzeW_sessionRow{padding:0 8px;cursor:pointer;user-select:none;color:var(--qilin-alias-label-primary);border-radius:8px;align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}.RrwzeW_projectRow:hover,.RrwzeW_sessionRow:hover,.RrwzeW_sessionRow.RrwzeW_selected{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_searchResultRow{box-sizing:border-box;cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--qilin-alias-label-primary);background:0 0;border:none;border-radius:8px;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}.RrwzeW_searchResultRow:hover,.RrwzeW_searchResultRow.RrwzeW_selected{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_searchResultHeading{align-items:center;min-width:0;display:flex}.RrwzeW_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}.RrwzeW_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}.RrwzeW_searchResultWorkspace,.RrwzeW_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}.RrwzeW_searchResultWorkspace{max-width:40%;color:var(--qilin-alias-label-tertiary);flex:none}.RrwzeW_searchResultSnippet{min-width:0;color:var(--qilin-alias-label-secondary);flex:1}.RrwzeW_projectRow{box-sizing:border-box;align-items:center;height:34px}.RrwzeW_projectRow .RrwzeW_rowActions{height:20px}.RrwzeW_sessionRow{height:32px;animation:RrwzeW_row-in .15s var(--ds-ease-in-out);gap:0}.RrwzeW_sessionRow .RrwzeW_title{margin:0 6px 0 4px}@keyframes RrwzeW_row-in{0%{opacity:0}}.RrwzeW_slot{width:16px;height:20px;color:var(--qilin-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.RrwzeW_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.RrwzeW_folderActive{color:var(--qilin-alias-state-business-primary)}.RrwzeW_projectRow .RrwzeW_chevron{display:none}.RrwzeW_projectRow:hover .RrwzeW_chevron{display:inline-flex}.RrwzeW_projectRow:hover .RrwzeW_folder{display:none}.RrwzeW_arrow{transition:transform .15s var(--ds-ease-in-out)}.RrwzeW_arrowOpen{transform:rotate(90deg)}.RrwzeW_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.RrwzeW_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}.RrwzeW_renameInput{border:.5px solid var(--qilin-alias-border-l4);background:var(--qilin-alias-button-elevated-fill);min-width:0;color:inherit;border-radius:4px;outline:none;padding:0 2px;font-size:14px;line-height:20px}.RrwzeW_sessionRow .RrwzeW_title{scroll-behavior:smooth;flex:1}@media (hover:hover){.RrwzeW_sessionRow:hover .RrwzeW_title{text-overflow:clip}}.RrwzeW_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--qilin-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}.RrwzeW_time{color:var(--qilin-alias-label-tertiary);flex:none;font-size:12px;line-height:20px}.RrwzeW_dot{flex:none}.RrwzeW_pinIndicator{width:16px;height:20px;color:var(--qilin-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}.RrwzeW_sessionRow:hover .RrwzeW_pinIndicator,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_pinIndicator{display:none}.RrwzeW_rowActions{flex:none;align-items:center;gap:12px;display:none}.RrwzeW_projectRow:hover .RrwzeW_rowActions,.RrwzeW_sessionRow:hover .RrwzeW_rowActions,.RrwzeW_projectRow.RrwzeW_menuOpen .RrwzeW_rowActions,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_rowActions{display:inline-flex}.RrwzeW_sessionRow:hover .RrwzeW_time,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_time{display:none}.RrwzeW_projectRow.RrwzeW_menuOpen,.RrwzeW_sessionRow.RrwzeW_menuOpen{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_sessionRow.RrwzeW_dropBefore,.RrwzeW_sessionRow.RrwzeW_dropAfter{position:relative}.RrwzeW_sessionRow.RrwzeW_dropBefore:before,.RrwzeW_sessionRow.RrwzeW_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--qilin-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}.RrwzeW_sessionRow.RrwzeW_dropBefore:before{top:-7px}.RrwzeW_sessionRow.RrwzeW_dropAfter:after{bottom:-7px}.RrwzeW_hoverContent{flex-direction:column;gap:8px;display:flex}.RrwzeW_hoverTitle{color:#fff;overflow-wrap:break-word;font-size:14px;line-height:20px}.RrwzeW_hoverPath{color:#cfd3d6;word-break:break-all;font-size:12px;line-height:16px}.RrwzeW_hoverTime{color:#cfd3d6;font-size:12px;line-height:16px}.RrwzeW_hoverStatus{color:#adb2b8;align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}.RrwzeW_iconButton{cursor:pointer;width:16px;height:16px;color:var(--qilin-alias-label-tertiary);background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.RrwzeW_iconButton:hover{color:var(--qilin-alias-label-primary)}.RrwzeW_chevron{color:var(--qilin-alias-label-caption)}@media (prefers-reduced-motion:reduce){.RrwzeW_sessionRow,.RrwzeW_arrow{transition:none;animation:none}.RrwzeW_sessionRow .RrwzeW_title{scroll-behavior:auto}}";
1210
+ const tagId$2 = "@qilin-agent/client-ui-workspace/Rows.module.css";
1211
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
1212
+ const tag = document.createElement("style");
1213
+ tag.dataset.plugin = "@qilin-agent/client-ui-workspace";
1214
+ tag.dataset.pluginCss = tagId$2;
1215
+ tag.textContent = css$2;
1216
+ document.head.appendChild(tag);
1217
+ }
1218
+ var Rows_module_css_default = {
1219
+ "arrow": "RrwzeW_arrow",
1220
+ "arrowOpen": "RrwzeW_arrowOpen",
1221
+ "chevron": "RrwzeW_chevron",
1222
+ "dot": "RrwzeW_dot",
1223
+ "dropAfter": "RrwzeW_dropAfter",
1224
+ "dropBefore": "RrwzeW_dropBefore",
1225
+ "folder": "RrwzeW_folder",
1226
+ "folderActive": "RrwzeW_folderActive",
1227
+ "hoverContent": "RrwzeW_hoverContent",
1228
+ "hoverPath": "RrwzeW_hoverPath",
1229
+ "hoverStatus": "RrwzeW_hoverStatus",
1230
+ "hoverTime": "RrwzeW_hoverTime",
1231
+ "hoverTitle": "RrwzeW_hoverTitle",
1232
+ "iconButton": "RrwzeW_iconButton",
1233
+ "menuOpen": "RrwzeW_menuOpen",
1234
+ "meta": "RrwzeW_meta",
1235
+ "pinIndicator": "RrwzeW_pinIndicator",
1236
+ "projectRow": "RrwzeW_projectRow",
1237
+ "projectText": "RrwzeW_projectText",
1238
+ "renameInput": "RrwzeW_renameInput",
1239
+ "row-in": "RrwzeW_row-in",
1240
+ "rowActions": "RrwzeW_rowActions",
1241
+ "searchResultHeading": "RrwzeW_searchResultHeading",
1242
+ "searchResultMeta": "RrwzeW_searchResultMeta",
1243
+ "searchResultRow": "RrwzeW_searchResultRow",
1244
+ "searchResultSnippet": "RrwzeW_searchResultSnippet",
1245
+ "searchResultTitle": "RrwzeW_searchResultTitle",
1246
+ "searchResultWorkspace": "RrwzeW_searchResultWorkspace",
1247
+ "selected": "RrwzeW_selected",
1248
+ "sessionRow": "RrwzeW_sessionRow",
1249
+ "slot": "RrwzeW_slot",
1250
+ "time": "RrwzeW_time",
1251
+ "title": "RrwzeW_title",
1252
+ "visuallyHidden": "RrwzeW_visuallyHidden"
1253
+ };
1254
+ //#endregion
1255
+ //#region lib/types/client/rows/Rows.js
1256
+ /**
1257
+ * Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
1258
+ * all data and callbacks arrive via props. Hover swaps (folder->chevron,
1259
+ * time->ellipsis, action buttons) are CSS-only, and a session row's clipped
1260
+ * title is scrolled programmatically while the row is hovered. Row ... menus are
1261
+ * visual-only except workspace Rename/Delete and session Rename/Fork/Archive; the
1262
+ * session and workspace hover cards are suppressed while a menu is open.
1263
+ */
1264
+ /** Row display title: blank rows show the localized New Session label. */
1265
+ function displayTitle(node, t) {
1266
+ return node.blank ? t("session.new") : node.title || t("session.untitled");
1267
+ }
1268
+ /**
1269
+ * Reveal a title wider than its one-line cell while its row is hovered: the
1270
+ * title clips its own text, so the far edge (a fork's incremented title, for
1271
+ * example) is reachable by scrolling the element to its end. Leaving returns it
1272
+ * to the start in one step, because the resting ellipsis and the narrowed cell
1273
+ * would otherwise meet the text while it travelled back. A title that fits has
1274
+ * no scroll range to move, and the stylesheet decides whether either move
1275
+ * glides or jumps.
1276
+ * @param title - the row's clipping title element.
1277
+ * @param revealed - whether the pointer is on the row.
1278
+ */
1279
+ function revealClippedTitle(title, revealed) {
1280
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
1281
+ if (title === null) return;
1282
+ if (revealed) {
1283
+ title.scrollLeft = title.scrollWidth - title.clientWidth;
1284
+ return;
1285
+ }
1286
+ if (typeof title.scrollTo === "function") title.scrollTo({
1287
+ left: 0,
1288
+ behavior: "instant"
1289
+ });
1290
+ else title.scrollLeft = 0;
1291
+ }
1292
+ /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
1293
+ function timeLabel(updatedAt, now, t) {
1294
+ const { unit, n } = (0, _qilin_agent_client_ui_primitives.relativeTime)(updatedAt, now);
1295
+ return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
1296
+ }
1297
+ /** Hover-card variant: distances wrap in the ago template; the now bucket stays bare (no "now ago"). */
1298
+ function hoverTimeLabel(updatedAt, now, t) {
1299
+ const { unit, n } = (0, _qilin_agent_client_ui_primitives.relativeTime)(updatedAt, now);
1300
+ return unit === "now" ? t("time.now") : t("time.ago", { t: t(`time.${unit}`, { n }) });
1301
+ }
1302
+ /**
1303
+ * Absolute creation time through the dictionary's date template (the message
1304
+ * clock pattern): `toLocaleString` would follow the browser language, not the
1305
+ * app locale, and produce mixed-language text after a switch.
1306
+ */
1307
+ function createdLabel(createdAt, t) {
1308
+ const d = new Date(createdAt);
1309
+ const pad2 = (v) => String(v).padStart(2, "0");
1310
+ return t("hover.created", { time: `${t("date.ymd", {
1311
+ y: d.getFullYear(),
1312
+ m: d.getMonth() + 1,
1313
+ d: d.getDate()
1314
+ })} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` });
1315
+ }
1316
+ /** Hover-card body: workspace title, display directory path, absolute creation time. */
1317
+ function WorkspaceHoverContent({ label, cwd, createdAt, t }) {
1318
+ return (0, react_jsx_runtime.jsxs)("div", {
1319
+ className: Rows_module_css_default.hoverContent,
1320
+ children: [
1321
+ (0, react_jsx_runtime.jsx)("div", {
1322
+ className: Rows_module_css_default.hoverTitle,
1323
+ children: label
1324
+ }),
1325
+ (0, react_jsx_runtime.jsx)("div", {
1326
+ className: Rows_module_css_default.hoverPath,
1327
+ children: cwd
1328
+ }),
1329
+ (0, react_jsx_runtime.jsx)("div", {
1330
+ className: Rows_module_css_default.hoverTime,
1331
+ children: createdLabel(createdAt, t)
1332
+ })
1333
+ ]
1334
+ });
1335
+ }
1336
+ /** Pointer-position half of a row (insert line above or below). */
1337
+ function rowHalf(e) {
1338
+ const rect = e.currentTarget.getBoundingClientRect();
1339
+ return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1340
+ }
1341
+ /**
1342
+ * Project (workspace) header row: folder + title;
1343
+ * hover reveals the chevron and create button, and dwelling on a real
1344
+ * Workspace shows its hover card (the ungrouped bucket has none).
1345
+ * `containsCurrent` arrives on the node (derivation fact, no renderer scan).
1346
+ * @param props.group - derived group node.
1347
+ * @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed.
1348
+ * @param props.onToggle - expand/collapse the group.
1349
+ * @param props.onCreate - start a frontend Session inside this Workspace.
1350
+ * @param props.drag - optional workspace-row drag wiring.
1351
+ * @param props.home - host account home for POSIX hover-path abbreviation.
1352
+ * @param props.t - the browser root's locale seat.
1353
+ * @returns the row element.
1354
+ */
1355
+ function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, t }) {
1356
+ const row = group;
1357
+ const label = row.workspaceId === void 0 ? t("group.ungrouped") : row.label;
1358
+ const active = containsCurrentDescendant || group.expanded && group.containsCurrent;
1359
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1360
+ const workspaceMenuItems = [{
1361
+ id: "rename",
1362
+ label: t("rename"),
1363
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEditOutline16, {})
1364
+ }, {
1365
+ id: "delete",
1366
+ label: t("delete.workspace"),
1367
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconTrashOutline16, {}),
1368
+ danger: true
1369
+ }];
1370
+ const ownRow = (0, react_jsx_runtime.jsxs)("div", {
1371
+ className: clsx(Rows_module_css_default.projectRow, menuOpen && Rows_module_css_default.menuOpen),
1372
+ role: "treeitem",
1373
+ "aria-expanded": row.expanded,
1374
+ onClick: onToggle,
1375
+ draggable: drag !== void 0,
1376
+ onDragStart: drag === void 0 ? void 0 : (e) => {
1377
+ e.dataTransfer.effectAllowed = "move";
1378
+ e.dataTransfer.setData("text/plain", row.key);
1379
+ drag.start();
1380
+ },
1381
+ onDragEnd: drag?.end,
1382
+ children: [
1383
+ (0, react_jsx_runtime.jsx)("span", {
1384
+ className: clsx(Rows_module_css_default.slot, Rows_module_css_default.folder, active && Rows_module_css_default.folderActive),
1385
+ children: row.expanded ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconFolderOpen16, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconFolderClose16, {})
1386
+ }),
1387
+ (0, react_jsx_runtime.jsx)("span", {
1388
+ className: clsx(Rows_module_css_default.slot, Rows_module_css_default.chevron),
1389
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconTriangleRightFill14, { className: clsx(Rows_module_css_default.arrow, row.expanded && Rows_module_css_default.arrowOpen) })
1390
+ }),
1391
+ (0, react_jsx_runtime.jsx)("span", {
1392
+ className: Rows_module_css_default.projectText,
1393
+ children: (0, react_jsx_runtime.jsx)("span", {
1394
+ className: Rows_module_css_default.title,
1395
+ children: label
1396
+ })
1397
+ }),
1398
+ (0, react_jsx_runtime.jsxs)("span", {
1399
+ className: Rows_module_css_default.rowActions,
1400
+ children: [actions !== void 0 && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
1401
+ open: menuOpen,
1402
+ onClose: () => {
1403
+ setMenuOpen(false);
1404
+ },
1405
+ items: workspaceMenuItems,
1406
+ onSelect: (id) => {
1407
+ setMenuOpen(false);
1408
+ /* v8 ignore next -- Menu can emit only the rename and delete rows supplied above. */
1409
+ if (id !== "rename" && id !== "delete") return;
1410
+ if (id === "rename") actions.rename();
1411
+ else actions.delete();
1412
+ },
1413
+ portal: true,
1414
+ closeOnPointerLeave: true,
1415
+ anchor: (0, react_jsx_runtime.jsx)("button", {
1416
+ type: "button",
1417
+ className: Rows_module_css_default.iconButton,
1418
+ "aria-label": t("actions.workspace.aria", { name: label }),
1419
+ onClick: (e) => {
1420
+ e.stopPropagation();
1421
+ setMenuOpen((v) => !v);
1422
+ },
1423
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEllipsisOutline16, {})
1424
+ })
1425
+ }), (0, react_jsx_runtime.jsx)("button", {
1426
+ type: "button",
1427
+ className: Rows_module_css_default.iconButton,
1428
+ "aria-label": t("actions.newSession.aria", { name: label }),
1429
+ onClick: (e) => {
1430
+ e.stopPropagation();
1431
+ onCreate();
1432
+ },
1433
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPlusOutline16, {})
1434
+ })]
1435
+ })
1436
+ ]
1437
+ });
1438
+ if (row.createdAt === void 0) return ownRow;
1439
+ return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.HoverCard, {
1440
+ anchor: ownRow,
1441
+ content: (0, react_jsx_runtime.jsx)(WorkspaceHoverContent, {
1442
+ label: row.label,
1443
+ cwd: row.cwd === void 0 ? void 0 : abbreviateHomePath(row.cwd, home),
1444
+ createdAt: row.createdAt,
1445
+ t
1446
+ }),
1447
+ disabled: menuOpen,
1448
+ copyText: row.cwd,
1449
+ copyLabel: t("copy"),
1450
+ copiedLabel: t("hover.copied")
1451
+ });
1452
+ }
1453
+ /* v8 ignore next 3 -- closed-union backstop; only reached if the status is forged */
1454
+ function assertNever(value) {
1455
+ throw new Error(`unknown pending interaction: ${String(value)}`);
1456
+ }
1457
+ /**
1458
+ * Session status presentation; pending interaction is primary and live activity
1459
+ * outranks completion reminders.
1460
+ */
1461
+ function sessionStatuses(node, t) {
1462
+ const subagents = node.runningSubagentCount === 0 ? void 0 : {
1463
+ state: "ongoing",
1464
+ label: t(node.runningSubagentCount === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: node.runningSubagentCount })
1465
+ };
1466
+ let pending;
1467
+ switch (node.pendingInteraction) {
1468
+ case "approval":
1469
+ pending = {
1470
+ state: "warning",
1471
+ label: t("status.waitingApproval")
1472
+ };
1473
+ break;
1474
+ case "plan-review":
1475
+ pending = {
1476
+ state: "warning",
1477
+ label: t("status.planReview")
1478
+ };
1479
+ break;
1480
+ case "question":
1481
+ pending = {
1482
+ state: "warning",
1483
+ label: t("status.waitingAnswer")
1484
+ };
1485
+ break;
1486
+ case void 0: break;
1487
+ /* v8 ignore next -- closed PendingInteractionStatus union */
1488
+ default: return assertNever(node.pendingInteraction);
1489
+ }
1490
+ if (pending !== void 0) return subagents === void 0 ? [pending] : [pending, subagents];
1491
+ if (node.running) {
1492
+ const primary = {
1493
+ state: "ongoing",
1494
+ label: t("status.running")
1495
+ };
1496
+ return subagents === void 0 ? [primary] : [primary, subagents];
1497
+ }
1498
+ if (subagents !== void 0) return [subagents];
1499
+ if (node.completed) return [{
1500
+ state: "done",
1501
+ label: t("status.completed")
1502
+ }];
1503
+ return [{
1504
+ state: "done",
1505
+ label: t("status.idle")
1506
+ }];
1507
+ }
1508
+ /** Primary status dot plus every status's screen-reader label, shared by the search and session rows. */
1509
+ function SessionStatusDots({ statuses }) {
1510
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, { state: statuses[0].state }), statuses.map((status) => (0, react_jsx_runtime.jsx)("span", {
1511
+ className: Rows_module_css_default.visuallyHidden,
1512
+ children: status.label
1513
+ }, status.label))] });
1514
+ }
1515
+ /** Non-interactive pinned-row marker; the enclosing row remains the only action. */
1516
+ function PinnedIndicator({ t }) {
1517
+ const label = t("row.pinned");
1518
+ return (0, react_jsx_runtime.jsx)("span", {
1519
+ className: Rows_module_css_default.pinIndicator,
1520
+ role: "img",
1521
+ "aria-label": label,
1522
+ title: label,
1523
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinFillRegular, { size: 14 })
1524
+ });
1525
+ }
1526
+ /**
1527
+ * Hover-card body: full title, relative time, the Session's own scheduled-task
1528
+ * section, and every relevant live status. The seat section sits above the
1529
+ * status lines so they stay the card's trailing status line.
1530
+ */
1531
+ function SessionHoverContent({ node, now, renderSlot, t }) {
1532
+ const statuses = sessionStatuses(node, t);
1533
+ return (0, react_jsx_runtime.jsxs)("div", {
1534
+ className: Rows_module_css_default.hoverContent,
1535
+ children: [
1536
+ (0, react_jsx_runtime.jsx)("div", {
1537
+ className: Rows_module_css_default.hoverTitle,
1538
+ children: displayTitle(node, t)
1539
+ }),
1540
+ !node.blank && (0, react_jsx_runtime.jsx)("div", {
1541
+ className: Rows_module_css_default.hoverTime,
1542
+ children: hoverTimeLabel(node.updatedAt, now, t)
1543
+ }),
1544
+ renderSlot("sidebar.session.row.hover", { sessionId: node.id }),
1545
+ statuses.map((status) => (0, react_jsx_runtime.jsxs)("div", {
1546
+ className: Rows_module_css_default.hoverStatus,
1547
+ children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, { state: status.state }), (0, react_jsx_runtime.jsx)("span", { children: status.label })]
1548
+ }, status.label))
1549
+ ]
1550
+ });
1551
+ }
1552
+ /**
1553
+ * One flat search result: title, Workspace context, and optional content
1554
+ * excerpt. Search navigation opens the session only; it does not address an
1555
+ * event inside the conversation.
1556
+ * @param props.result - merged local/content search row.
1557
+ * @param props.currentId - selected session id.
1558
+ * @param props.onOpen - open the selected session.
1559
+ * @param props.t - Workspace-browser translation seat.
1560
+ * @returns the result button.
1561
+ */
1562
+ function SearchResultItem({ result, currentId, onOpen, t }) {
1563
+ const selected = result.id === currentId;
1564
+ const statuses = sessionStatuses(result, t);
1565
+ const primaryStatus = statuses[0];
1566
+ return (0, react_jsx_runtime.jsxs)("button", {
1567
+ type: "button",
1568
+ className: clsx(Rows_module_css_default.searchResultRow, selected && Rows_module_css_default.selected),
1569
+ role: "treeitem",
1570
+ "aria-selected": selected,
1571
+ onClick: () => {
1572
+ onOpen(result.id);
1573
+ },
1574
+ children: [(0, react_jsx_runtime.jsxs)("span", {
1575
+ className: Rows_module_css_default.searchResultHeading,
1576
+ children: [(0, react_jsx_runtime.jsx)("span", {
1577
+ className: Rows_module_css_default.slot,
1578
+ children: (primaryStatus.state !== "done" || result.completed) && (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
1579
+ }), (0, react_jsx_runtime.jsx)("span", {
1580
+ className: Rows_module_css_default.searchResultTitle,
1581
+ children: result.title || t("session.untitled")
1582
+ })]
1583
+ }), (0, react_jsx_runtime.jsxs)("span", {
1584
+ className: Rows_module_css_default.searchResultMeta,
1585
+ children: [(0, react_jsx_runtime.jsx)("span", {
1586
+ className: Rows_module_css_default.searchResultWorkspace,
1587
+ children: result.workspace || t("group.ungrouped")
1588
+ }), result.snippet !== void 0 && (0, react_jsx_runtime.jsx)("span", {
1589
+ className: Rows_module_css_default.searchResultSnippet,
1590
+ children: result.snippet
1591
+ })]
1592
+ })]
1593
+ });
1594
+ }
1595
+ /**
1596
+ * One top-level 34px session row: status dot (pending user interaction outranks
1597
+ * own or descendant activity), title, relative time, and the row actions menu.
1598
+ * @param props.node - derived session node.
1599
+ * @param props.currentId - selected session id (row highlight).
1600
+ * @param props.now - epoch ms for relative-time formatting.
1601
+ * @param props.onOpen - open a session by id.
1602
+ * @param props.onRename - open the session rename dialog (id + current title).
1603
+ * @param props.onFork - fork a session at its last completed turn.
1604
+ * @param props.onArchive - archive a session by id.
1605
+ * @param props.onPin - pin a session by id (leading its group and the flat list).
1606
+ * @param props.onUnpin - drop a session's pin.
1607
+ * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
1608
+ * @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
1609
+ * @param props.t - the browser root's locale seat.
1610
+ * @returns the session row.
1611
+ */
1612
+ function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork, onArchive, onPin, onUnpin, onReveal, drag, renderSlot, t }) {
1613
+ const row = node;
1614
+ const title = displayTitle(node, t);
1615
+ const selected = node.id === currentId;
1616
+ const statuses = sessionStatuses(node, t);
1617
+ const showStatus = statuses[0].state !== "done" || row.completed;
1618
+ const draggable = drag !== void 0 && !row.blank;
1619
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1620
+ const rowRef = (0, react.useRef)(null);
1621
+ const titleRef = (0, react.useRef)(null);
1622
+ (0, react.useEffect)(() => {
1623
+ if (onReveal === void 0) return;
1624
+ rowRef.current?.scrollIntoView({ block: "nearest" });
1625
+ onReveal();
1626
+ }, [onReveal]);
1627
+ const sessionMenuItems = [
1628
+ {
1629
+ id: "pin",
1630
+ label: t(row.pinned ? "menu.unpinSession" : "menu.pinSession"),
1631
+ icon: row.pinned ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinFillRegular, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinOutlineRegular, {})
1632
+ },
1633
+ {
1634
+ id: "rename",
1635
+ label: t("rename"),
1636
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEditOutline16, {})
1637
+ },
1638
+ {
1639
+ id: "fork",
1640
+ label: t("menu.fork"),
1641
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconBranchOutline16, {})
1642
+ },
1643
+ {
1644
+ id: "archive",
1645
+ label: t("menu.archiveSession"),
1646
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconArchiveOutline20, { size: 16 })
1647
+ }
1648
+ ];
1649
+ return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.HoverCard, {
1650
+ anchor: (0, react_jsx_runtime.jsxs)("div", {
1651
+ ref: rowRef,
1652
+ className: clsx(Rows_module_css_default.sessionRow, selected && Rows_module_css_default.selected, menuOpen && Rows_module_css_default.menuOpen, drag?.marker === "before" && Rows_module_css_default.dropBefore, drag?.marker === "after" && Rows_module_css_default.dropAfter),
1653
+ role: "treeitem",
1654
+ "aria-selected": selected,
1655
+ onClick: () => {
1656
+ onOpen(node.id);
1657
+ },
1658
+ onPointerEnter: () => {
1659
+ revealClippedTitle(titleRef.current, true);
1660
+ },
1661
+ onPointerLeave: () => {
1662
+ revealClippedTitle(titleRef.current, false);
1663
+ },
1664
+ draggable,
1665
+ onDragStart: drag === void 0 || row.blank ? void 0 : (e) => {
1666
+ e.dataTransfer.effectAllowed = "move";
1667
+ e.dataTransfer.setData("text/plain", node.id);
1668
+ drag.start();
1669
+ },
1670
+ onDragEnd: drag === void 0 || row.blank ? void 0 : drag.end,
1671
+ onDragOver: drag === void 0 ? void 0 : (e) => {
1672
+ if (!drag.active) return;
1673
+ e.preventDefault();
1674
+ e.dataTransfer.dropEffect = "move";
1675
+ drag.hover(rowHalf(e));
1676
+ },
1677
+ onDrop: drag === void 0 ? void 0 : (e) => {
1678
+ if (!drag.active) return;
1679
+ e.preventDefault();
1680
+ drag.drop(rowHalf(e));
1681
+ },
1682
+ children: [
1683
+ (0, react_jsx_runtime.jsx)("span", {
1684
+ className: Rows_module_css_default.slot,
1685
+ children: !row.blank && (showStatus ? (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses }) : renderSlot("sidebar.session.row.leading", { sessionId: node.id }))
1686
+ }),
1687
+ (0, react_jsx_runtime.jsx)("span", {
1688
+ ref: titleRef,
1689
+ className: Rows_module_css_default.title,
1690
+ children: title
1691
+ }),
1692
+ !row.blank && (0, react_jsx_runtime.jsx)("span", {
1693
+ className: Rows_module_css_default.time,
1694
+ children: timeLabel(row.updatedAt, now, t)
1695
+ }),
1696
+ row.pinned && !row.blank && (0, react_jsx_runtime.jsx)(PinnedIndicator, { t }),
1697
+ !row.blank && (0, react_jsx_runtime.jsxs)("span", {
1698
+ className: Rows_module_css_default.rowActions,
1699
+ children: [(0, react_jsx_runtime.jsx)("button", {
1700
+ type: "button",
1701
+ className: Rows_module_css_default.iconButton,
1702
+ "aria-label": t(row.pinned ? "actions.unpin" : "actions.pin"),
1703
+ onClick: (e) => {
1704
+ e.stopPropagation();
1705
+ (row.pinned ? onUnpin : onPin)(node.id);
1706
+ },
1707
+ children: row.pinned ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinFillRegular, { size: 14 }) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinOutlineRegular, { size: 14 })
1708
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
1709
+ open: menuOpen,
1710
+ onClose: () => {
1711
+ setMenuOpen(false);
1712
+ },
1713
+ items: sessionMenuItems,
1714
+ onSelect: (id) => {
1715
+ setMenuOpen(false);
1716
+ if (id === "pin") (row.pinned ? onUnpin : onPin)(node.id);
1717
+ if (id === "rename") onRename(node.id, row.title);
1718
+ if (id === "fork") onFork(node.id);
1719
+ if (id === "archive") onArchive(node.id);
1720
+ },
1721
+ portal: true,
1722
+ closeOnPointerLeave: true,
1723
+ anchor: (0, react_jsx_runtime.jsx)("button", {
1724
+ type: "button",
1725
+ className: Rows_module_css_default.iconButton,
1726
+ "aria-label": t("actions.session.aria", { name: title }),
1727
+ onClick: (e) => {
1728
+ e.stopPropagation();
1729
+ setMenuOpen((v) => !v);
1730
+ },
1731
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEllipsisOutline16, {})
1732
+ })
1733
+ })]
1734
+ })
1735
+ ]
1736
+ }),
1737
+ content: (0, react_jsx_runtime.jsx)(SessionHoverContent, {
1738
+ node,
1739
+ now,
1740
+ renderSlot,
1741
+ t
1742
+ }),
1743
+ disabled: menuOpen || drag?.active === true,
1744
+ copyText: row.blank || row.title === "" ? void 0 : row.title,
1745
+ copyLabel: t("copy"),
1746
+ copiedLabel: t("hover.copied")
1747
+ });
1748
+ }
1749
+ //#endregion
1750
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/WorkspacePicker.module.css.mjs
1751
+ const css$1 = ".QOlntq_modalAction{min-width:72px}.QOlntq_modalError,.QOlntq_menuStatus{margin-top:8px;font-size:12px;line-height:18px}.QOlntq_modalError{color:var(--qilin-alias-state-error-primary)}.QOlntq_menuStatus{color:var(--qilin-alias-label-secondary)}";
1752
+ const tagId$1 = "@qilin-agent/client-ui-workspace/WorkspacePicker.module.css";
1753
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1754
+ const tag = document.createElement("style");
1755
+ tag.dataset.plugin = "@qilin-agent/client-ui-workspace";
1756
+ tag.dataset.pluginCss = tagId$1;
1757
+ tag.textContent = css$1;
1758
+ document.head.appendChild(tag);
1759
+ }
1760
+ var WorkspacePicker_module_css_default = {
1761
+ "menuStatus": "QOlntq_menuStatus",
1762
+ "modalAction": "QOlntq_modalAction",
1763
+ "modalError": "QOlntq_modalError"
1764
+ };
1765
+ //#endregion
1766
+ //#region lib/types/client/WorkspacePicker.js
1767
+ const ADD_WORKSPACE = "::add-workspace";
1768
+ /**
1769
+ * Render the pick menu plus the adoption error dialog.
1770
+ * @param props - owner-controlled flow props.
1771
+ * @returns menu + dialog elements.
1772
+ */
1773
+ function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly = false, side = "bottom", selectedId }) {
1774
+ const workspaceSnapshot = useWorkspaces((state) => state);
1775
+ const workspaces = workspaceSnapshot.items;
1776
+ const getAnchorRect = (0, react.useCallback)(() => anchorRef?.current?.getBoundingClientRect() ?? null, [anchorRef]);
1777
+ const [errorOpen, setErrorOpen] = (0, react.useState)(false);
1778
+ const [modalError, setModalError] = (0, react.useState)(null);
1779
+ const [flowOpen, setFlowOpen] = (0, react.useState)(false);
1780
+ const [pickingFolder, setPickingFolder] = (0, react.useState)(false);
1781
+ const flowBusy = flowOpen || pickingFolder;
1782
+ const flowAvailable = useDirectoryFlow((occupied) => occupied);
1783
+ (0, react.useEffect)(() => {
1784
+ if (flowOpen && !flowAvailable) setFlowOpen(false);
1785
+ }, [flowOpen, flowAvailable]);
1786
+ const addEntries = flowAvailable ? [{
1787
+ id: ADD_WORKSPACE,
1788
+ label: t("menu.addWorkspace"),
1789
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPlusOutline16, { size: 16 }),
1790
+ disabled: flowBusy
1791
+ }] : [];
1792
+ const pinAdd = !addOnly && workspaces.length > 0;
1793
+ const items = pinAdd ? workspaces.map((workspace) => ({
1794
+ id: workspace.workspaceId,
1795
+ label: workspace.title,
1796
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconFolderClose16, { size: 16 }),
1797
+ disabled: flowBusy
1798
+ })) : addEntries;
1799
+ const menuIsEmpty = items.length === 0;
1800
+ const closeModal = () => {
1801
+ setErrorOpen(false);
1802
+ setModalError(null);
1803
+ };
1804
+ /** Adopt a picked directory; failures land in the folder-error dialog (Choose again reopens the flow). */
1805
+ const adoptDirectory = (path) => createWorkspace({ path }).then((workspace) => {
1806
+ setFlowOpen(false);
1807
+ onPick(workspace.workspaceId);
1808
+ }).catch((reason) => {
1809
+ setModalError(reason instanceof Error ? reason.message : String(reason));
1810
+ setFlowOpen(false);
1811
+ setErrorOpen(true);
1812
+ });
1813
+ const openDirectoryFlow = (0, react.useCallback)(() => {
1814
+ onClose();
1815
+ setErrorOpen(false);
1816
+ setModalError(null);
1817
+ setFlowOpen(true);
1818
+ }, [onClose]);
1819
+ const listSettled = addOnly || workspaceSnapshot.phase === "ready";
1820
+ const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1;
1821
+ (0, react.useEffect)(() => {
1822
+ if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow();
1823
+ }, [
1824
+ open,
1825
+ addIsTheOnlyEntry,
1826
+ flowBusy,
1827
+ openDirectoryFlow
1828
+ ]);
1829
+ /** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
1830
+ const flowOwner = {
1831
+ open: flowOpen,
1832
+ busy: pickingFolder,
1833
+ onPicked: (path) => {
1834
+ setPickingFolder(true);
1835
+ adoptDirectory(path).finally(() => {
1836
+ setPickingFolder(false);
1837
+ });
1838
+ },
1839
+ onCancel: () => {
1840
+ setFlowOpen(false);
1841
+ },
1842
+ onError: (message) => {
1843
+ setFlowOpen(false);
1844
+ setModalError(message);
1845
+ setErrorOpen(true);
1846
+ }
1847
+ };
1848
+ const handleSelect = (id) => {
1849
+ if (id === ADD_WORKSPACE) {
1850
+ openDirectoryFlow();
1851
+ return;
1852
+ }
1853
+ onPick(id);
1854
+ };
1855
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1856
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
1857
+ open: open && !addIsTheOnlyEntry && !menuIsEmpty,
1858
+ anchor: null,
1859
+ items,
1860
+ ...pinAdd ? { footer: addEntries } : {},
1861
+ selectedId,
1862
+ onSelect: handleSelect,
1863
+ onClose,
1864
+ side,
1865
+ portal: true,
1866
+ getAnchorRect
1867
+ }),
1868
+ open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === "pending" && (0, react_jsx_runtime.jsx)("div", {
1869
+ className: WorkspacePicker_module_css_default.menuStatus,
1870
+ role: "status",
1871
+ children: t("picker.loading")
1872
+ }),
1873
+ renderDirectoryFlow(flowOwner),
1874
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Modal, {
1875
+ open: errorOpen,
1876
+ onClose: closeModal,
1877
+ closeLabel: t("close"),
1878
+ title: t("folderError.title"),
1879
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
1880
+ variant: "outline",
1881
+ className: WorkspacePicker_module_css_default.modalAction,
1882
+ onClick: closeModal,
1883
+ children: t("cancel")
1884
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
1885
+ variant: "primary",
1886
+ className: WorkspacePicker_module_css_default.modalAction,
1887
+ disabled: !flowAvailable,
1888
+ onClick: openDirectoryFlow,
1889
+ children: t("folderError.retry")
1890
+ })] }),
1891
+ children: (0, react_jsx_runtime.jsx)("div", {
1892
+ className: WorkspacePicker_module_css_default.modalError,
1893
+ role: "alert",
1894
+ children: modalError
1895
+ })
1896
+ })
1897
+ ] });
1898
+ }
1899
+ /**
1900
+ * The conversation empty-state registration: adapts the owner share to the
1901
+ * core flow (all state and semantics live in the flow / the owner).
1902
+ * @param props - empty-state slot props (owner share + injected creation callback).
1903
+ * @returns the flow element.
1904
+ */
1905
+ function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t }) {
1906
+ return (0, react_jsx_runtime.jsx)(WorkspacePickFlow, {
1907
+ t,
1908
+ open,
1909
+ anchorRef,
1910
+ useWorkspaces,
1911
+ createWorkspace,
1912
+ useDirectoryFlow,
1913
+ renderDirectoryFlow: (owner) => renderSlot("conversation.hero.workspace.directoryFlow", owner),
1914
+ selectedId,
1915
+ onPick,
1916
+ onClose
1917
+ });
1918
+ }
1919
+ //#endregion
1920
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css.mjs
1921
+ const css = ".CO5s3G_root{--qilin-session-list-edge-inset:var(--qilin-sidebar-inline-padding);--qilin-session-list-scrollbar-width:8px;--qilin-session-list-scrollbar-offset:2px;box-sizing:border-box;min-height:0;padding-right:var(--qilin-session-list-edge-inset);flex-direction:column;flex:1;display:flex}.CO5s3G_root.CO5s3G_rail{padding-right:0}.CO5s3G_iconButton{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--qilin-alias-label-secondary);background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.CO5s3G_iconButton:focus-visible,.CO5s3G_searchButton:focus-visible,.CO5s3G_clearButton:focus-visible{outline:var(--qilin-focus-ring-width) solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:-2px}.CO5s3G_iconButton:hover{background:var(--qilin-alias-interactive-bg-hover)}.CO5s3G_sectionHeader{box-sizing:border-box;height:36px;color:var(--qilin-alias-label-tertiary);border-radius:12px;flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;display:flex;overflow:hidden}.CO5s3G_root:not(.CO5s3G_rail) .CO5s3G_sectionHeader{margin-top:2px;margin-right:-4px}.CO5s3G_sectionLabel{white-space:nowrap;opacity:1;visibility:visible;min-width:0;max-width:45%;transition:max-width .18s var(--ds-ease-in-out), margin-right .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;line-height:20px;overflow:hidden}.CO5s3G_sectionLabelHidden{opacity:0;visibility:hidden;max-width:0;margin-right:-4px;transition-delay:0s,0s,0s,0s,.18s;transform:translate(-4px)}.CO5s3G_searchSlot{box-sizing:border-box;min-width:0;max-width:28px;transition:max-width .18s var(--ds-ease-in-out), padding-left .18s var(--ds-ease-in-out);flex:1;align-items:center;margin-left:auto;padding-left:0;display:flex}.CO5s3G_searchSlotExpanded{max-width:100%;padding-left:0}.CO5s3G_headerActions{opacity:1;visibility:visible;max-width:60px;transition:max-width .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;align-items:center;gap:4px;display:flex;overflow:hidden}.CO5s3G_headerActionsHidden{opacity:0;visibility:hidden;pointer-events:none;max-width:0;transition-delay:0s,0s,0s,.18s;transform:translate(4px)}.CO5s3G_search{box-sizing:border-box;corner-shape:round;cursor:text;width:100%;height:28px;color:var(--qilin-alias-label-secondary);transition:width .18s var(--ds-ease-in-out), padding .18s var(--ds-ease-in-out), border-color .18s var(--ds-ease-in-out), background-color .18s var(--ds-ease-in-out);background:0 0;border:none;border-radius:50%;flex:none;align-items:center;gap:0;margin:0;padding:0;display:flex;overflow:hidden}.CO5s3G_searchExpanded{border:.5px solid var(--qilin-alias-border-l4);width:calc(100% + 4px);height:30px;color:var(--qilin-alias-label-caption);background:0 0;border-radius:10px;margin-inline:-2px;padding:0 4px 0 0}.CO5s3G_searchButton{corner-shape:round;cursor:pointer;width:28px;height:28px;color:inherit;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.CO5s3G_searchExpanded .CO5s3G_searchButton{width:28px;height:30px}.CO5s3G_searchButton:hover{background:var(--qilin-alias-interactive-bg-hover)}.CO5s3G_searchExpanded .CO5s3G_searchButton:hover{background:0 0}.CO5s3G_searchInput{opacity:0;pointer-events:none;width:0;min-width:0;color:var(--qilin-alias-label-primary);transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;font-size:13px;line-height:18px}.CO5s3G_searchExpanded .CO5s3G_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.CO5s3G_searchInput::placeholder{color:var(--qilin-alias-label-tertiary)}.CO5s3G_clearButton{corner-shape:round;cursor:pointer;width:24px;height:24px;color:var(--qilin-alias-label-secondary);background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.CO5s3G_clearButton:hover{background:var(--qilin-alias-interactive-bg-hover)}.CO5s3G_rail .CO5s3G_sectionHeader{justify-content:flex-start;gap:0;margin-bottom:12px;padding-left:0}.CO5s3G_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--qilin-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;overflow:visible}.CO5s3G_rail .CO5s3G_listArea{margin-left:0;margin-right:0;padding-left:0}.CO5s3G_treeBody{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.CO5s3G_fade{left:0;right:var(--qilin-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--qilin-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}[data-platform=darwin] .CO5s3G_fade{display:none}.CO5s3G_wide{animation:CO5s3G_wide-in .2s var(--ds-ease-in-out)}@keyframes CO5s3G_wide-in{0%{opacity:0}}.CO5s3G_list{min-height:0;margin-left:-4px;margin-right:var(--qilin-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--qilin-session-list-edge-inset) - var(--qilin-session-list-scrollbar-width) - var(--qilin-session-list-scrollbar-offset));scrollbar-gutter:stable;flex:1;padding-bottom:16px;overflow-y:auto}.CO5s3G_flatList>*+*,.CO5s3G_searchTree>[role=treeitem]+[role=treeitem],.CO5s3G_groupSection>*+*{margin-top:2px}.CO5s3G_searchStatus,.CO5s3G_searchWarning{color:var(--qilin-alias-label-tertiary);padding:10px 12px;font-size:12px;line-height:18px}.CO5s3G_searchWarning{color:var(--qilin-alias-label-secondary)}.CO5s3G_groupSection{position:relative}.CO5s3G_groupSection+.CO5s3G_groupSection{margin-top:4px}.CO5s3G_listTopDropIndicator,.CO5s3G_workspaceDropBefore:before,.CO5s3G_workspaceDropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--qilin-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:0}.CO5s3G_listTopDropIndicator{top:-8px;left:0;right:var(--qilin-session-list-edge-inset)}.CO5s3G_listTopDropActive>.CO5s3G_workspaceDropBefore:first-child:before{display:none}.CO5s3G_workspaceDropBefore:before{top:-8px}.CO5s3G_workspaceDropAfter:after{bottom:-8px}.CO5s3G_sessionOverflowButton{width:100%;height:28px;padding:0 12px 0 calc(28px + var(--qilin-workspace-indent,0px));cursor:pointer;text-align:left;color:var(--qilin-alias-label-tertiary);background:0 0;border:none;border-radius:8px;font-size:12px}.CO5s3G_groupSection>.CO5s3G_sessionOverflowButton{margin-top:0}.CO5s3G_sessionOverflowButton:hover{color:var(--qilin-alias-label-secondary);background:0 0}.CO5s3G_empty{color:var(--qilin-alias-label-tertiary);padding:16px 12px;font-size:13px}.CO5s3G_renameInput{box-sizing:border-box;border:.5px solid var(--qilin-alias-border-l4);width:100%;height:44px;color:var(--qilin-alias-label-primary);background:0 0;border-radius:22px;outline:none;padding:7px 14px;font-size:14px;font-weight:400;line-height:22px}.CO5s3G_renameInput:disabled{color:var(--qilin-alias-label-dimmed)}.CO5s3G_renameError{color:var(--qilin-alias-state-error-primary);margin-top:8px;font-size:12px;line-height:18px}.CO5s3G_deleteAction:not(:disabled){color:var(--qilin-alias-state-error-primary)}.CO5s3G_deleteStatus{color:var(--qilin-alias-label-secondary);font-size:12px;line-height:18px}@media (prefers-reduced-motion:reduce){.CO5s3G_wide{animation:none}.CO5s3G_search,.CO5s3G_sectionLabel,.CO5s3G_searchSlot,.CO5s3G_searchInput,.CO5s3G_headerActions{transition:none}}";
1922
+ const tagId = "@qilin-agent/client-ui-workspace/WorkspaceBrowser.module.css";
1923
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1924
+ const tag = document.createElement("style");
1925
+ tag.dataset.plugin = "@qilin-agent/client-ui-workspace";
1926
+ tag.dataset.pluginCss = tagId;
1927
+ tag.textContent = css;
1928
+ document.head.appendChild(tag);
1929
+ }
1930
+ var WorkspaceBrowser_module_css_default = {
1931
+ "clearButton": "CO5s3G_clearButton",
1932
+ "deleteAction": "CO5s3G_deleteAction",
1933
+ "deleteStatus": "CO5s3G_deleteStatus",
1934
+ "empty": "CO5s3G_empty",
1935
+ "fade": "CO5s3G_fade",
1936
+ "flatList": "CO5s3G_flatList",
1937
+ "groupSection": "CO5s3G_groupSection",
1938
+ "headerActions": "CO5s3G_headerActions",
1939
+ "headerActionsHidden": "CO5s3G_headerActionsHidden",
1940
+ "iconButton": "CO5s3G_iconButton",
1941
+ "list": "CO5s3G_list",
1942
+ "listArea": "CO5s3G_listArea",
1943
+ "listTopDropActive": "CO5s3G_listTopDropActive",
1944
+ "listTopDropIndicator": "CO5s3G_listTopDropIndicator",
1945
+ "rail": "CO5s3G_rail",
1946
+ "renameError": "CO5s3G_renameError",
1947
+ "renameInput": "CO5s3G_renameInput",
1948
+ "root": "CO5s3G_root",
1949
+ "search": "CO5s3G_search",
1950
+ "searchButton": "CO5s3G_searchButton",
1951
+ "searchExpanded": "CO5s3G_searchExpanded",
1952
+ "searchInput": "CO5s3G_searchInput",
1953
+ "searchSlot": "CO5s3G_searchSlot",
1954
+ "searchSlotExpanded": "CO5s3G_searchSlotExpanded",
1955
+ "searchStatus": "CO5s3G_searchStatus",
1956
+ "searchTree": "CO5s3G_searchTree",
1957
+ "searchWarning": "CO5s3G_searchWarning",
1958
+ "sectionHeader": "CO5s3G_sectionHeader",
1959
+ "sectionLabel": "CO5s3G_sectionLabel",
1960
+ "sectionLabelHidden": "CO5s3G_sectionLabelHidden",
1961
+ "sessionOverflowButton": "CO5s3G_sessionOverflowButton",
1962
+ "treeBody": "CO5s3G_treeBody",
1963
+ "wide": "CO5s3G_wide",
1964
+ "wide-in": "CO5s3G_wide-in",
1965
+ "workspaceDropAfter": "CO5s3G_workspaceDropAfter",
1966
+ "workspaceDropBefore": "CO5s3G_workspaceDropBefore"
1967
+ };
1968
+ //#endregion
1969
+ //#region lib/types/client/rows/WorkspaceBrowser.js
1970
+ /**
1971
+ * The workspace/session browsing region filling the sidebar shell's
1972
+ * `sidebar.workspaces` hole: section header (title + view options + add
1973
+ * workspace), search, the grouped tree or flat list, and the workspace
1974
+ * dialogs. Wide state renders the full browser; rail state renders no region
1975
+ * control of its own, so the search and add requests each expand the column
1976
+ * through the owner share and act once it is wide. Adding is the header
1977
+ * button's one action, so it raises the directory flow with no menu in
1978
+ * between; the flow and its error dialog live in WorkspacePicker (same package
1979
+ * — direct composition, no slot between them).
1980
+ */
1981
+ /**
1982
+ * Column slide length (--ds-transition-duration-slow): the rail-expanded search
1983
+ * focus and the rail-expanded add picker both wait it out — focus() forces a
1984
+ * synchronous layout that would jank the slide, and the picker's anchor button
1985
+ * mounts only once the same slide has made the column wide.
1986
+ */
1987
+ const EXPAND_SLIDE_MS = 300;
1988
+ /** Pause between the latest keystroke and a Host content-search request. */
1989
+ const SEARCH_DEBOUNCE_MS = 250;
1990
+ /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
1991
+ const SEARCH_QUERY_MAX_CODE_UNITS = 500;
1992
+ /** Session rows visible per Workspace before the local overflow control. */
1993
+ const COLLAPSED_SESSION_LIMIT = 5;
1994
+ /** Fold one Workspace without charging its provisional New Session against the ordinary-row limit. */
1995
+ function collapsedSessionRows(sessions) {
1996
+ let ordinaryCount = 0;
1997
+ const rows = sessions.filter((session) => {
1998
+ if (session.blank) return true;
1999
+ if (ordinaryCount >= COLLAPSED_SESSION_LIMIT) return false;
2000
+ ordinaryCount += 1;
2001
+ return true;
2002
+ });
2003
+ return {
2004
+ rows,
2005
+ hiddenCount: sessions.length - rows.length
2006
+ };
2007
+ }
2008
+ /** Keep controlled input and RPC payload inside the session.search wire contract. */
2009
+ function sanitizeSearchQuery(value) {
2010
+ const withoutNul = value.replaceAll("\0", "");
2011
+ if (withoutNul.length <= SEARCH_QUERY_MAX_CODE_UNITS) return withoutNul;
2012
+ let end = SEARCH_QUERY_MAX_CODE_UNITS;
2013
+ const last = withoutNul.charCodeAt(end - 1);
2014
+ const next = withoutNul.charCodeAt(end);
2015
+ if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
2016
+ return withoutNul.slice(0, end);
2017
+ }
2018
+ /** Immutable membership toggle for the local expand-all array. */
2019
+ function toggled(list, key) {
2020
+ return list.includes(key) ? list.filter((k) => k !== key) : [...list, key];
2021
+ }
2022
+ /**
2023
+ * Accept the native drag at document level while a row drag is active: row
2024
+ * hover still owns the insertion marker, and releasing outside the list must
2025
+ * not be rendered as a rejected drop before dragend commits that last marker.
2026
+ */
2027
+ function useNativeDragAcceptance(active) {
2028
+ (0, react.useEffect)(() => {
2029
+ if (!active) return;
2030
+ const acceptDrag = (event) => {
2031
+ event.preventDefault();
2032
+ if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "move";
2033
+ };
2034
+ const acceptDrop = (event) => {
2035
+ event.preventDefault();
2036
+ };
2037
+ document.addEventListener("dragover", acceptDrag);
2038
+ document.addEventListener("drop", acceptDrop);
2039
+ return () => {
2040
+ document.removeEventListener("dragover", acceptDrag);
2041
+ document.removeEventListener("drop", acceptDrop);
2042
+ };
2043
+ }, [active]);
2044
+ }
2045
+ /** Grouping and ordering menu; own open state so it resets with the wide chrome. */
2046
+ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }) {
2047
+ const [open, setOpen] = (0, react.useState)(false);
2048
+ return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
2049
+ open,
2050
+ onClose: () => {
2051
+ setOpen(false);
2052
+ },
2053
+ items: [
2054
+ {
2055
+ type: "label",
2056
+ id: "group-by",
2057
+ text: t("groupBy.label")
2058
+ },
2059
+ {
2060
+ id: "workspace",
2061
+ label: t("groupBy.workspace")
2062
+ },
2063
+ {
2064
+ id: "workspace-tree",
2065
+ label: t("groupBy.workspaceTree")
2066
+ },
2067
+ {
2068
+ id: "flat",
2069
+ label: t("groupBy.flat")
2070
+ },
2071
+ {
2072
+ type: "separator",
2073
+ id: "order-by-separator"
2074
+ },
2075
+ {
2076
+ type: "label",
2077
+ id: "order-by",
2078
+ text: t("orderBy.label")
2079
+ },
2080
+ {
2081
+ id: "manual",
2082
+ label: t("orderBy.manual")
2083
+ },
2084
+ {
2085
+ id: "updated",
2086
+ label: t("orderBy.updated")
2087
+ }
2088
+ ],
2089
+ selectedIds: [groupBy, orderBy],
2090
+ onSelect: (id) => {
2091
+ if (id === "workspace" || id === "workspace-tree" || id === "flat") onGroupPick(id);
2092
+ else if (id === "manual" || id === "updated") onOrderPick(id);
2093
+ setOpen(false);
2094
+ },
2095
+ align: "end",
2096
+ dense: true,
2097
+ portal: true,
2098
+ anchor: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
2099
+ label: t("viewOptions.label"),
2100
+ side: "bottom",
2101
+ delayMs: 500,
2102
+ children: (0, react_jsx_runtime.jsx)("button", {
2103
+ type: "button",
2104
+ className: clsx(WorkspaceBrowser_module_css_default.iconButton, WorkspaceBrowser_module_css_default.wide),
2105
+ "aria-label": t("viewOptions.label"),
2106
+ onClick: () => {
2107
+ setOpen((v) => !v);
2108
+ },
2109
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPersonalizationOutline16, {})
2110
+ })
2111
+ })
2112
+ });
2113
+ }
2114
+ /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
2115
+ function workspaceGroupHalf(e) {
2116
+ const rect = e.currentTarget.getBoundingClientRect();
2117
+ return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
2118
+ }
2119
+ /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
2120
+ function SessionTree({ list, useSessionStatus, startSession, open, forkSession, workspaces, ungroupedSessionIds, archivedSessionIds, pinnedSessionIds, onSessionPin, onSessionUnpin, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, renderSlot, revealSessionId, onSessionRevealed }) {
2121
+ const panelActive = usePanelInfo((info) => info.activePanelId !== null);
2122
+ const statuses = useSessionStatus((s) => s);
2123
+ const current = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2124
+ const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
2125
+ const [expandedSessionGroups, setExpandedSessionGroups] = (0, react.useState)([]);
2126
+ const [drag, setDrag] = (0, react.useState)(null);
2127
+ const sessionDropCommitted = (0, react.useRef)(false);
2128
+ const [workspaceDrag, setWorkspaceDrag] = (0, react.useState)(null);
2129
+ const workspaceDropCommitted = (0, react.useRef)(false);
2130
+ useNativeDragAcceptance(drag !== null || workspaceDrag !== null);
2131
+ const currentGroup = current === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, current);
2132
+ (0, react.useEffect)(() => {
2133
+ if (current === void 0 || currentGroup === void 0 || Object.hasOwn(groupExpansion, currentGroup)) return;
2134
+ setGroupExpanded(currentGroup, true);
2135
+ }, [
2136
+ current,
2137
+ currentGroup,
2138
+ setGroupExpanded,
2139
+ groupExpansion
2140
+ ]);
2141
+ const parents = (0, react.useMemo)(() => {
2142
+ if (!nestWorkspaces) return /* @__PURE__ */ new Map();
2143
+ const keysByPath = new Map(workspaces.map((workspace) => [workspace.path, workspace.workspaceId]));
2144
+ const paths = [...keysByPath.keys()];
2145
+ return new Map(workspaces.map((workspace) => {
2146
+ const path = owningParentFolder(workspace.path, paths);
2147
+ return [workspace.workspaceId, path === void 0 ? void 0 : keysByPath.get(path)];
2148
+ }));
2149
+ }, [nestWorkspaces, workspaces]);
2150
+ const currentAncestors = (0, react.useMemo)(() => {
2151
+ const keys = /* @__PURE__ */ new Set();
2152
+ for (let key = currentGroup === void 0 ? void 0 : parents.get(currentGroup); key !== void 0; key = parents.get(key)) keys.add(key);
2153
+ return keys;
2154
+ }, [currentGroup, parents]);
2155
+ const expandedGroups = (0, react.useMemo)(() => {
2156
+ const ancestorKeys = new Set(parents.values());
2157
+ return [...workspaces.map((workspace) => workspace.workspaceId), ""].filter((key) => groupExpansion[key] ?? ancestorKeys.has(key));
2158
+ }, [
2159
+ groupExpansion,
2160
+ parents,
2161
+ workspaces
2162
+ ]);
2163
+ const groups = (0, react.useMemo)(() => deriveGroups(list, workspaces, {
2164
+ pinnedSessionIds,
2165
+ archivedSessionIds
2166
+ }, statuses, {
2167
+ expandedGroups,
2168
+ ungroupedOrder: ungroupedSessionIds
2169
+ }), [
2170
+ list,
2171
+ workspaces,
2172
+ pinnedSessionIds,
2173
+ archivedSessionIds,
2174
+ statuses,
2175
+ expandedGroups,
2176
+ ungroupedSessionIds
2177
+ ]);
2178
+ (0, react.useEffect)(() => {
2179
+ for (let key = revealGroup; key !== void 0; key = parents.get(key)) if (groupExpansion[key] === false || key === revealGroup && groupExpansion[key] !== true) setGroupExpanded(key, true);
2180
+ }, [
2181
+ groupExpansion,
2182
+ parents,
2183
+ revealGroup,
2184
+ setGroupExpanded
2185
+ ]);
2186
+ (0, react.useEffect)(() => {
2187
+ if (revealSessionId === void 0 || revealGroup === void 0) return;
2188
+ const group = groups.find((candidate) => candidate.key === revealGroup);
2189
+ if (group === void 0 || !group.expanded || !group.sessions.some((row) => row.id === revealSessionId)) return;
2190
+ if (collapsedSessionRows(group.sessions).rows.some((row) => row.id === revealSessionId)) return;
2191
+ setExpandedSessionGroups((keys) => keys.includes(revealGroup) ? keys : [...keys, revealGroup]);
2192
+ }, [
2193
+ groups,
2194
+ revealGroup,
2195
+ revealSessionId
2196
+ ]);
2197
+ const now = Date.now();
2198
+ const commitSessionDrag = (activeDrag, over) => {
2199
+ if (sessionDropCommitted.current) return;
2200
+ sessionDropCommitted.current = true;
2201
+ setDrag(null);
2202
+ const group = groups.find((candidate) => candidate.key === activeDrag.accountKey);
2203
+ if (group === void 0) return;
2204
+ const sessionsExpanded = expandedSessionGroups.includes(group.key);
2205
+ const renderedSessions = sessionsExpanded ? group.sessions : collapsedSessionRows(group.sessions).rows;
2206
+ const targetIndex = renderedSessions.findIndex((session) => session.id === over.id);
2207
+ if (targetIndex === -1) return;
2208
+ const sourceIndex = renderedSessions.findIndex((session) => session.id === activeDrag.sessionId);
2209
+ if (over.id === activeDrag.sessionId) return;
2210
+ const withoutSource = renderedSessions.filter((session) => session.id !== activeDrag.sessionId);
2211
+ const targetWithoutSourceIndex = withoutSource.findIndex((session) => session.id === over.id);
2212
+ if (targetWithoutSourceIndex === -1) return;
2213
+ const visibleInsertAt = over.half === "before" ? targetWithoutSourceIndex : targetWithoutSourceIndex + 1;
2214
+ if (sourceIndex !== -1 && visibleInsertAt === sourceIndex) return;
2215
+ const accountSessionIds = activeDrag.accountKey === "" ? ungroupedSessionIds : workspaces.find((workspace) => workspace.workspaceId === activeDrag.accountKey)?.sessionIds;
2216
+ if (accountSessionIds === void 0 || !accountSessionIds.includes(activeDrag.sessionId)) return;
2217
+ const nextOrder = accountSessionIds.filter((id) => id !== activeDrag.sessionId);
2218
+ let anchor;
2219
+ if (sessionsExpanded) anchor = over.half === "before" ? over.id : renderedSessions[targetIndex + 1]?.id;
2220
+ else {
2221
+ const previousVisible = withoutSource[visibleInsertAt - 1]?.id;
2222
+ if (previousVisible === void 0) anchor = nextOrder[0];
2223
+ else {
2224
+ const previousIndex = nextOrder.indexOf(previousVisible);
2225
+ if (previousIndex === -1) return;
2226
+ anchor = nextOrder[previousIndex + 1];
2227
+ }
2228
+ }
2229
+ const insertAt = anchor === void 0 ? nextOrder.length : nextOrder.indexOf(anchor);
2230
+ nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId);
2231
+ if (!sessionsExpanded && sourceIndex !== -1) {
2232
+ const nodes = new Map(group.sessions.map((node) => [node.id, node]));
2233
+ if (!collapsedSessionRows(nextOrder.flatMap((id) => {
2234
+ const node = nodes.get(id);
2235
+ return node === void 0 ? [] : [node];
2236
+ })).rows.some((node) => node.id === activeDrag.sessionId)) return;
2237
+ }
2238
+ const currentBlank = group.sessions.find((node) => node.blank)?.id;
2239
+ setSessionOrder(activeDrag.accountKey, pinCurrentBlank(nextOrder, currentBlank));
2240
+ };
2241
+ const commitWorkspaceDrag = (activeDrag, over) => {
2242
+ if (workspaceDropCommitted.current) return;
2243
+ workspaceDropCommitted.current = true;
2244
+ setWorkspaceDrag(null);
2245
+ const owner = parents.get(activeDrag.workspaceId);
2246
+ const siblings = workspaces.filter((workspace) => parents.get(workspace.workspaceId) === owner);
2247
+ const rowIndex = siblings.findIndex((workspace) => workspace.workspaceId === over.id);
2248
+ if (rowIndex === -1) return;
2249
+ const anchor = over.half === "before" ? over.id : siblings[rowIndex + 1]?.workspaceId;
2250
+ if (anchor === activeDrag.workspaceId) return;
2251
+ const sourceIndex = siblings.findIndex((workspace) => workspace.workspaceId === activeDrag.workspaceId);
2252
+ const anchorIndex = anchor === void 0 ? siblings.length : siblings.findIndex((workspace) => workspace.workspaceId === anchor);
2253
+ if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return;
2254
+ insertWorkspaceBefore(activeDrag.workspaceId, anchor).catch((reason) => {
2255
+ console.warn("workspace reorder rejected:", reason);
2256
+ });
2257
+ };
2258
+ const childrenByParent = (0, react.useMemo)(() => {
2259
+ const children = /* @__PURE__ */ new Map();
2260
+ for (const group of groups) {
2261
+ const parent = parents.get(group.key);
2262
+ const siblings = children.get(parent);
2263
+ if (siblings === void 0) children.set(parent, [group]);
2264
+ else siblings.push(group);
2265
+ }
2266
+ return children;
2267
+ }, [groups, parents]);
2268
+ const rootGroups = childrenByParent.get(void 0) ?? [];
2269
+ const workspaceDropAtListStart = rootGroups[0]?.workspaceId !== void 0 && workspaceDrag?.over?.id === rootGroups[0].workspaceId && workspaceDrag.over.half === "before";
2270
+ const renderGroup = (group, depth) => {
2271
+ const workspaceId = group.workspaceId;
2272
+ const children = childrenByParent.get(group.key) ?? [];
2273
+ const compatibleDrag = workspaceDrag !== null && parents.get(workspaceDrag.workspaceId) === parents.get(group.key);
2274
+ const collapsed = collapsedSessionRows(group.sessions);
2275
+ const sessionsExpanded = expandedSessionGroups.includes(group.key);
2276
+ const workspaceMarker = workspaceId !== void 0 && workspaceDrag?.over?.id === workspaceId ? workspaceDrag.over.half : null;
2277
+ const workspaceDragProps = workspaceId === void 0 ? void 0 : {
2278
+ start: () => {
2279
+ workspaceDropCommitted.current = false;
2280
+ setWorkspaceDrag({
2281
+ workspaceId,
2282
+ over: null
2283
+ });
2284
+ },
2285
+ end: () => {
2286
+ if (workspaceDrag?.over !== null && workspaceDrag?.over !== void 0) commitWorkspaceDrag(workspaceDrag, workspaceDrag.over);
2287
+ else setWorkspaceDrag(null);
2288
+ workspaceDropCommitted.current = false;
2289
+ }
2290
+ };
2291
+ const hoverWorkspace = workspaceId === void 0 || !compatibleDrag ? void 0 : (half) => {
2292
+ setWorkspaceDrag((active) => active === null ? active : {
2293
+ ...active,
2294
+ over: {
2295
+ id: workspaceId,
2296
+ half
2297
+ }
2298
+ });
2299
+ };
2300
+ const dropWorkspace = workspaceId === void 0 || !compatibleDrag ? void 0 : (half) => {
2301
+ commitWorkspaceDrag(workspaceDrag, {
2302
+ id: workspaceId,
2303
+ half
2304
+ });
2305
+ };
2306
+ return (0, react_jsx_runtime.jsxs)("div", {
2307
+ style: { "--dsh-workspace-indent": `${depth * 12}px` },
2308
+ className: clsx(WorkspaceBrowser_module_css_default.groupSection, workspaceMarker === "before" && WorkspaceBrowser_module_css_default.workspaceDropBefore, workspaceMarker === "after" && WorkspaceBrowser_module_css_default.workspaceDropAfter),
2309
+ onDragOver: workspaceDrag === null ? void 0 : (e) => {
2310
+ e.preventDefault();
2311
+ if (hoverWorkspace === void 0 && parents.get(group.key) !== void 0) return;
2312
+ e.stopPropagation();
2313
+ if (hoverWorkspace === void 0) {
2314
+ e.dataTransfer.dropEffect = "none";
2315
+ if (workspaceDrag.over !== null) setWorkspaceDrag({
2316
+ ...workspaceDrag,
2317
+ over: null
2318
+ });
2319
+ } else {
2320
+ e.dataTransfer.dropEffect = "move";
2321
+ hoverWorkspace(workspaceGroupHalf(e));
2322
+ }
2323
+ },
2324
+ onDrop: workspaceDrag === null ? void 0 : (e) => {
2325
+ e.preventDefault();
2326
+ if (dropWorkspace === void 0 && parents.get(group.key) !== void 0) return;
2327
+ e.stopPropagation();
2328
+ if (dropWorkspace === void 0) {
2329
+ workspaceDropCommitted.current = true;
2330
+ setWorkspaceDrag(null);
2331
+ } else dropWorkspace(workspaceGroupHalf(e));
2332
+ },
2333
+ children: [
2334
+ (0, react_jsx_runtime.jsx)(ProjectRowItem, {
2335
+ group,
2336
+ containsCurrentDescendant: currentAncestors.has(group.key),
2337
+ home,
2338
+ t,
2339
+ onToggle: () => {
2340
+ if (group.expanded) setExpandedSessionGroups((keys) => keys.filter((key) => key !== group.key));
2341
+ setGroupExpanded(group.key, !group.expanded);
2342
+ },
2343
+ onCreate: () => {
2344
+ if (group.workspaceId !== void 0) {
2345
+ setGroupExpanded(group.key, true);
2346
+ startSession(group.workspaceId);
2347
+ }
2348
+ },
2349
+ drag: workspaceDragProps,
2350
+ actions: group.workspaceId === void 0 ? void 0 : {
2351
+ rename: () => {
2352
+ /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
2353
+ if (group.workspaceId !== void 0) onRenameRequest(group.workspaceId, group.label);
2354
+ },
2355
+ delete: () => {
2356
+ /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
2357
+ if (group.workspaceId !== void 0) onDeleteRequest(group.workspaceId, group.label);
2358
+ }
2359
+ }
2360
+ }),
2361
+ group.expanded && children.length > 0 && (0, react_jsx_runtime.jsx)("div", {
2362
+ role: "group",
2363
+ children: children.map((child) => renderGroup(child, depth + 1))
2364
+ }),
2365
+ (sessionsExpanded ? group.sessions : collapsed.rows).map((node) => {
2366
+ const sameGroupDrag = drag !== null && drag.accountKey === group.key;
2367
+ const normalizeHalf = (half) => node.blank ? "after" : half;
2368
+ const dragProps = {
2369
+ start: () => {
2370
+ sessionDropCommitted.current = false;
2371
+ setDrag({
2372
+ accountKey: group.key,
2373
+ sessionId: node.id,
2374
+ over: null
2375
+ });
2376
+ },
2377
+ active: sameGroupDrag,
2378
+ marker: sameGroupDrag && drag.over?.id === node.id ? drag.over.half : null,
2379
+ hover: (half) => {
2380
+ /* v8 ignore next -- narrowing guard: Rows gates hover on `active`, which is false while the drag state is null. */
2381
+ setDrag((d) => d === null ? d : {
2382
+ ...d,
2383
+ over: {
2384
+ id: node.id,
2385
+ half: normalizeHalf(half)
2386
+ }
2387
+ });
2388
+ },
2389
+ drop: (half) => {
2390
+ /* v8 ignore next -- narrowing guard: Rows gates drop on `active`, which is false while the drag state is null. */
2391
+ if (drag === null) return;
2392
+ commitSessionDrag(drag, {
2393
+ id: node.id,
2394
+ half: normalizeHalf(half)
2395
+ });
2396
+ },
2397
+ end: () => {
2398
+ if (drag?.over !== null && drag?.over !== void 0) commitSessionDrag(drag, drag.over);
2399
+ else setDrag(null);
2400
+ sessionDropCommitted.current = false;
2401
+ }
2402
+ };
2403
+ return (0, react_jsx_runtime.jsx)(SessionNodeItem, {
2404
+ node,
2405
+ currentId: current,
2406
+ now,
2407
+ onOpen: open,
2408
+ onRename: onSessionRename,
2409
+ onFork: forkSession,
2410
+ onArchive: onSessionArchive,
2411
+ onPin: onSessionPin,
2412
+ onUnpin: onSessionUnpin,
2413
+ onReveal: node.id === revealSessionId && group.key === revealGroup ? () => {
2414
+ onSessionRevealed(node.id);
2415
+ } : void 0,
2416
+ drag: dragProps,
2417
+ renderSlot,
2418
+ t
2419
+ }, node.id);
2420
+ }),
2421
+ collapsed.hiddenCount > 0 && (0, react_jsx_runtime.jsx)("button", {
2422
+ type: "button",
2423
+ className: WorkspaceBrowser_module_css_default.sessionOverflowButton,
2424
+ "aria-expanded": sessionsExpanded,
2425
+ onClick: () => {
2426
+ setExpandedSessionGroups((keys) => toggled(keys, group.key));
2427
+ },
2428
+ children: sessionsExpanded ? t("sessions.collapse") : t("sessions.expand", { n: collapsed.hiddenCount })
2429
+ })
2430
+ ]
2431
+ }, group.key);
2432
+ };
2433
+ return (0, react_jsx_runtime.jsxs)("div", {
2434
+ className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
2435
+ children: [
2436
+ workspaceDropAtListStart && (0, react_jsx_runtime.jsx)("span", {
2437
+ className: WorkspaceBrowser_module_css_default.listTopDropIndicator,
2438
+ "aria-hidden": "true"
2439
+ }),
2440
+ (0, react_jsx_runtime.jsxs)("div", {
2441
+ className: clsx(WorkspaceBrowser_module_css_default.list, workspaceDropAtListStart && WorkspaceBrowser_module_css_default.listTopDropActive),
2442
+ role: "tree",
2443
+ "aria-label": t("section.sessions"),
2444
+ children: [groups.length === 0 && (0, react_jsx_runtime.jsx)("div", {
2445
+ className: WorkspaceBrowser_module_css_default.empty,
2446
+ children: t("empty.none")
2447
+ }), rootGroups.map((group) => renderGroup(group, 0))]
2448
+ }),
2449
+ (0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.fade })
2450
+ ]
2451
+ });
2452
+ }
2453
+ /** The flat "In one list" body: every session is one draggable top-level row. */
2454
+ function FlatList({ list, sessionIds, pinnedSessionIds, useSessionStatus, open, forkSession, onSessionRename, onSessionArchive, onSessionPin, onSessionUnpin, usePanelInfo, setSessionOrder, revealSessionId, onSessionRevealed, t, renderSlot }) {
2455
+ const panelActive = usePanelInfo((info) => info.activePanelId !== null);
2456
+ const statuses = useSessionStatus((s) => s);
2457
+ const rows = (0, react.useMemo)(() => deriveFlat(list, sessionIds, pinnedSessionIds, statuses), [
2458
+ list,
2459
+ sessionIds,
2460
+ pinnedSessionIds,
2461
+ statuses
2462
+ ]);
2463
+ const [drag, setDrag] = (0, react.useState)(null);
2464
+ const dropCommitted = (0, react.useRef)(false);
2465
+ useNativeDragAcceptance(drag !== null);
2466
+ const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2467
+ const commitDrag = (activeDrag, over) => {
2468
+ if (dropCommitted.current) return;
2469
+ dropCommitted.current = true;
2470
+ setDrag(null);
2471
+ const targetIndex = rows.findIndex((row) => row.id === over.id);
2472
+ if (targetIndex === -1) return;
2473
+ const anchor = over.half === "before" ? over.id : rows[targetIndex + 1]?.id;
2474
+ if (anchor === activeDrag.sessionId) return;
2475
+ const sourceIndex = rows.findIndex((row) => row.id === activeDrag.sessionId);
2476
+ const anchorIndex = anchor === void 0 ? rows.length : rows.findIndex((row) => row.id === anchor);
2477
+ if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return;
2478
+ const nextOrder = rows.map((row) => row.id).filter((id) => id !== activeDrag.sessionId);
2479
+ const insertAt = anchor === void 0 ? nextOrder.length : nextOrder.indexOf(anchor);
2480
+ nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId);
2481
+ const currentBlank = rows.find((node) => node.blank)?.id;
2482
+ setSessionOrder(FLAT_SESSION_ORDER_KEY, pinCurrentBlank(nextOrder, currentBlank));
2483
+ };
2484
+ const now = Date.now();
2485
+ return (0, react_jsx_runtime.jsxs)("div", {
2486
+ className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
2487
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2488
+ className: clsx(WorkspaceBrowser_module_css_default.list, WorkspaceBrowser_module_css_default.flatList),
2489
+ role: "tree",
2490
+ "aria-label": t("section.sessions"),
2491
+ children: [rows.length === 0 && (0, react_jsx_runtime.jsx)("div", {
2492
+ className: WorkspaceBrowser_module_css_default.empty,
2493
+ children: t("empty.none")
2494
+ }), rows.map((node) => {
2495
+ const active = drag !== null;
2496
+ const normalizeHalf = (half) => node.blank ? "after" : half;
2497
+ return (0, react_jsx_runtime.jsx)(SessionNodeItem, {
2498
+ node,
2499
+ currentId,
2500
+ now,
2501
+ onOpen: open,
2502
+ onRename: onSessionRename,
2503
+ onFork: forkSession,
2504
+ onArchive: onSessionArchive,
2505
+ onPin: onSessionPin,
2506
+ onUnpin: onSessionUnpin,
2507
+ onReveal: node.id === revealSessionId ? () => {
2508
+ onSessionRevealed(node.id);
2509
+ } : void 0,
2510
+ renderSlot,
2511
+ drag: {
2512
+ start: () => {
2513
+ dropCommitted.current = false;
2514
+ setDrag({
2515
+ accountKey: FLAT_SESSION_ORDER_KEY,
2516
+ sessionId: node.id,
2517
+ over: null
2518
+ });
2519
+ },
2520
+ active,
2521
+ marker: active && drag.over?.id === node.id ? drag.over.half : null,
2522
+ hover: (half) => {
2523
+ setDrag((current) => current === null ? current : {
2524
+ ...current,
2525
+ over: {
2526
+ id: node.id,
2527
+ half: normalizeHalf(half)
2528
+ }
2529
+ });
2530
+ },
2531
+ drop: (half) => {
2532
+ if (drag !== null) commitDrag(drag, {
2533
+ id: node.id,
2534
+ half: normalizeHalf(half)
2535
+ });
2536
+ },
2537
+ end: () => {
2538
+ if (drag?.over !== null && drag?.over !== void 0) commitDrag(drag, drag.over);
2539
+ else setDrag(null);
2540
+ dropCommitted.current = false;
2541
+ }
2542
+ },
2543
+ t
2544
+ }, node.id);
2545
+ })]
2546
+ }), (0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.fade })]
2547
+ });
2548
+ }
2549
+ /** Flat search body: local metadata matches plus the current Host result page. */
2550
+ function SearchResults({ useSessions, useSessionStatus, open, workspaces, archivedSessionIds, query, remote, resultLimit, usePanelInfo, t }) {
2551
+ const panelActive = usePanelInfo((info) => info.activePanelId !== null);
2552
+ const list = useSessions((s) => s);
2553
+ const statuses = useSessionStatus((s) => s);
2554
+ const currentRemote = remote.query === query ? remote : {
2555
+ query,
2556
+ status: "loading",
2557
+ items: [],
2558
+ hasMore: false
2559
+ };
2560
+ const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, { archivedSessionIds }, statuses, currentRemote, resultLimit), [
2561
+ list,
2562
+ workspaces,
2563
+ query,
2564
+ archivedSessionIds,
2565
+ statuses,
2566
+ currentRemote,
2567
+ resultLimit
2568
+ ]);
2569
+ const pending = currentRemote.status === "loading";
2570
+ const failed = currentRemote.status === "error";
2571
+ const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2572
+ return (0, react_jsx_runtime.jsxs)("div", {
2573
+ className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
2574
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2575
+ className: WorkspaceBrowser_module_css_default.list,
2576
+ children: [
2577
+ (0, react_jsx_runtime.jsx)("div", {
2578
+ className: WorkspaceBrowser_module_css_default.searchTree,
2579
+ role: "tree",
2580
+ "aria-label": t("search.results.aria"),
2581
+ children: results.items.map((result) => (0, react_jsx_runtime.jsx)(SearchResultItem, {
2582
+ result,
2583
+ currentId,
2584
+ onOpen: open,
2585
+ t
2586
+ }, result.id))
2587
+ }),
2588
+ pending && (0, react_jsx_runtime.jsx)("div", {
2589
+ className: WorkspaceBrowser_module_css_default.searchStatus,
2590
+ role: "status",
2591
+ children: t("search.pending")
2592
+ }),
2593
+ failed && (0, react_jsx_runtime.jsx)("div", {
2594
+ className: WorkspaceBrowser_module_css_default.searchWarning,
2595
+ role: "status",
2596
+ children: t("search.unavailable")
2597
+ }),
2598
+ !pending && results.items.length === 0 && (0, react_jsx_runtime.jsx)("div", {
2599
+ className: WorkspaceBrowser_module_css_default.empty,
2600
+ children: t("search.noMatches")
2601
+ }),
2602
+ results.hasMore && (0, react_jsx_runtime.jsx)("div", {
2603
+ className: WorkspaceBrowser_module_css_default.searchStatus,
2604
+ children: t("search.hasMore", { n: resultLimit })
2605
+ })
2606
+ ]
2607
+ }), (0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.fade })]
2608
+ });
2609
+ }
2610
+ /**
2611
+ * Render the browsing region.
2612
+ * @param props - composed slot props (shell owner share + store + injected actions).
2613
+ * @returns the region element tree.
2614
+ */
2615
+ function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, pinSession, unpinSession, createWorkspace, searchSessions, searchResultLimit, requestSearch, requestAddWorkspace, closeAddWorkspace, closeRenameRequest, setDirectoryBusy, useDirectoryFlow, useHostInfo, useWorkspaceShortcuts, useShortcuts, renderSlot, t }) {
2616
+ const home = useHostInfo((info) => info.home);
2617
+ const list = useSessions((state) => state);
2618
+ const workspaces = useWorkspaces((state) => state.items);
2619
+ const workspacePhase = useWorkspaces((state) => state.phase);
2620
+ const workspaceStreamState = useWorkspaces((state) => state.state);
2621
+ const archivedSessionIds = useWorkspaces((state) => state.archivedSessionIds);
2622
+ const pinnedSessionIds = useWorkspaces((state) => state.pinnedSessionIds);
2623
+ const directoryFlowAvailable = useDirectoryFlow((occupied) => occupied);
2624
+ const groupBy = useStore((s) => s.groupBy);
2625
+ const orderBy = useStore((s) => s.orderBy);
2626
+ const groupExpansion = useStore((s) => s.groupExpansion);
2627
+ const sessionOrderByAccount = useStore((s) => s.sessionOrderByAccount);
2628
+ const workspaceReady = workspacePhase === "ready" && workspaceStreamState !== "loading";
2629
+ const mainSessionId = Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2630
+ const currentBlank = mainSessionId !== void 0 && list.byId[mainSessionId]?.blank === true ? mainSessionId : void 0;
2631
+ const ungroupedMemberIds = (0, react.useMemo)(() => {
2632
+ const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
2633
+ return list.ids.filter((id) => list.byId[id] !== void 0 && !accounted.has(id));
2634
+ }, [list, workspaces]);
2635
+ const flatMemberIds = (0, react.useMemo)(() => visibleSessionIds(list, { archivedSessionIds }), [archivedSessionIds, list]);
2636
+ const orderedWorkspaces = (0, react.useMemo)(() => workspaces.map((workspace) => {
2637
+ const memberIds = workspace.sessionIds;
2638
+ const baseOrder = orderBy === "updated" ? orderByRecency(memberIds, list.byId) : reconcileManualOrder(memberIds, sessionOrderByAccount[workspace.workspaceId], list.byId);
2639
+ return {
2640
+ ...workspace,
2641
+ sessionIds: pinCurrentBlank(baseOrder, currentBlank !== void 0 && memberIds.includes(currentBlank) ? currentBlank : void 0)
2642
+ };
2643
+ }), [
2644
+ currentBlank,
2645
+ list.byId,
2646
+ orderBy,
2647
+ sessionOrderByAccount,
2648
+ workspaces
2649
+ ]);
2650
+ const orderedUngroupedSessionIds = (0, react.useMemo)(() => {
2651
+ return pinCurrentBlank(orderBy === "updated" ? orderByRecency(ungroupedMemberIds, list.byId) : reconcileManualOrder(ungroupedMemberIds, sessionOrderByAccount[""], list.byId), currentBlank !== void 0 && ungroupedMemberIds.includes(currentBlank) ? currentBlank : void 0);
2652
+ }, [
2653
+ currentBlank,
2654
+ list.byId,
2655
+ orderBy,
2656
+ sessionOrderByAccount,
2657
+ ungroupedMemberIds
2658
+ ]);
2659
+ const orderedFlatSessionIds = (0, react.useMemo)(() => {
2660
+ return pinCurrentBlank(orderBy === "updated" ? orderByRecency(flatMemberIds, list.byId) : reconcileManualOrder(flatMemberIds, sessionOrderByAccount[FLAT_SESSION_ORDER_KEY], list.byId), currentBlank !== void 0 && flatMemberIds.includes(currentBlank) ? currentBlank : void 0);
2661
+ }, [
2662
+ currentBlank,
2663
+ flatMemberIds,
2664
+ list.byId,
2665
+ orderBy,
2666
+ sessionOrderByAccount
2667
+ ]);
2668
+ const activeSessionOrders = (0, react.useMemo)(() => Object.fromEntries([
2669
+ ...orderedWorkspaces.map((workspace) => [workspace.workspaceId, workspace.sessionIds]),
2670
+ ["", orderedUngroupedSessionIds],
2671
+ [FLAT_SESSION_ORDER_KEY, orderedFlatSessionIds]
2672
+ ]), [
2673
+ orderedFlatSessionIds,
2674
+ orderedUngroupedSessionIds,
2675
+ orderedWorkspaces
2676
+ ]);
2677
+ (0, react.useEffect)(() => {
2678
+ if (workspacePhase !== "ready") return;
2679
+ actions.retainAccountKeys([
2680
+ "",
2681
+ FLAT_SESSION_ORDER_KEY,
2682
+ ...workspaces.map((workspace) => workspace.workspaceId)
2683
+ ]);
2684
+ }, [
2685
+ actions.retainAccountKeys,
2686
+ workspacePhase,
2687
+ workspaces
2688
+ ]);
2689
+ (0, react.useEffect)(() => {
2690
+ if (list.phase !== "ready" || workspaceReady || orderBy !== "manual" || currentBlank === void 0) return;
2691
+ const changed = {};
2692
+ for (const [key, ids] of Object.entries(activeSessionOrders)) {
2693
+ if (key !== "__flat_session_order__" && workspacePhase !== "ready") continue;
2694
+ const saved = sessionOrderByAccount[key] ?? [];
2695
+ if (ids[0] !== currentBlank || saved[0] === currentBlank) continue;
2696
+ changed[key] = [currentBlank, ...saved.filter((id) => id !== currentBlank)];
2697
+ }
2698
+ if (Object.keys(changed).length > 0) actions.syncSessionOrders(changed);
2699
+ }, [
2700
+ actions.syncSessionOrders,
2701
+ activeSessionOrders,
2702
+ currentBlank,
2703
+ list.phase,
2704
+ orderBy,
2705
+ sessionOrderByAccount,
2706
+ workspacePhase,
2707
+ workspaceReady
2708
+ ]);
2709
+ (0, react.useEffect)(() => {
2710
+ if (list.phase !== "ready" || !workspaceReady || orderBy !== "manual") return;
2711
+ const changed = Object.fromEntries(Object.entries(activeSessionOrders).filter(([key, ids]) => {
2712
+ const saved = sessionOrderByAccount[key];
2713
+ return saved === void 0 || saved.length !== ids.length || ids.some((id, index) => id !== saved[index]);
2714
+ }));
2715
+ if (Object.keys(changed).length > 0) actions.syncSessionOrders(changed);
2716
+ }, [
2717
+ actions.syncSessionOrders,
2718
+ activeSessionOrders,
2719
+ list.phase,
2720
+ orderBy,
2721
+ sessionOrderByAccount,
2722
+ workspaceReady
2723
+ ]);
2724
+ const saveSessionOrder = (accountKey, order) => {
2725
+ actions.setSessionOrder(accountKey, order, activeSessionOrders);
2726
+ };
2727
+ const [query, setQuery] = (0, react.useState)("");
2728
+ const [searchExpanded, setSearchExpanded] = (0, react.useState)(false);
2729
+ const [revealSessionId, setRevealSessionId] = (0, react.useState)(void 0);
2730
+ const normalizedQuery = sanitizeSearchQuery(query).trim();
2731
+ const [remoteSearch, setRemoteSearch] = (0, react.useState)({
2732
+ query: "",
2733
+ status: "idle",
2734
+ items: [],
2735
+ hasMore: false
2736
+ });
2737
+ const searchRoot = (0, react.useRef)(null);
2738
+ const searchInput = (0, react.useRef)(null);
2739
+ const [wsPickerOpen, setWsPickerOpen] = (0, react.useState)(false);
2740
+ const [addOnExpand, setAddOnExpand] = (0, react.useState)(false);
2741
+ const wsPlusRef = (0, react.useRef)(null);
2742
+ const composingRef = (0, react.useRef)(false);
2743
+ const openSearchResult = (sessionId) => {
2744
+ setRevealSessionId(sessionId);
2745
+ setQuery("");
2746
+ setSearchExpanded(false);
2747
+ open(sessionId);
2748
+ };
2749
+ const acknowledgeSessionReveal = (sessionId) => {
2750
+ setRevealSessionId((current) => current === sessionId ? void 0 : current);
2751
+ };
2752
+ (0, react.useEffect)(() => {
2753
+ if (normalizedQuery !== "") setRevealSessionId(void 0);
2754
+ }, [normalizedQuery]);
2755
+ const [searchOnExpand, setSearchOnExpand] = (0, react.useState)(false);
2756
+ (0, react.useEffect)(() => {
2757
+ if (wide && searchOnExpand) {
2758
+ const timer = window.setTimeout(() => {
2759
+ searchInput.current?.focus({ preventScroll: true });
2760
+ setSearchOnExpand(false);
2761
+ }, EXPAND_SLIDE_MS);
2762
+ return () => {
2763
+ window.clearTimeout(timer);
2764
+ };
2765
+ }
2766
+ }, [wide, searchOnExpand]);
2767
+ (0, react.useEffect)(() => {
2768
+ if (!wide || !searchExpanded || searchOnExpand) return;
2769
+ searchInput.current?.focus({ preventScroll: true });
2770
+ }, [
2771
+ wide,
2772
+ searchExpanded,
2773
+ searchOnExpand
2774
+ ]);
2775
+ (0, react.useEffect)(() => {
2776
+ if (!wide || !searchExpanded || searchOnExpand) return;
2777
+ const onClick = (event) => {
2778
+ if (!(event.target instanceof Node) || searchRoot.current?.contains(event.target) === true) return;
2779
+ searchInput.current?.blur();
2780
+ if (normalizedQuery !== "") return;
2781
+ setSearchExpanded(false);
2782
+ };
2783
+ document.addEventListener("click", onClick);
2784
+ return () => {
2785
+ document.removeEventListener("click", onClick);
2786
+ };
2787
+ }, [
2788
+ normalizedQuery,
2789
+ wide,
2790
+ searchExpanded,
2791
+ searchOnExpand
2792
+ ]);
2793
+ (0, react.useEffect)(() => {
2794
+ if (normalizedQuery === "") {
2795
+ setRemoteSearch({
2796
+ query: "",
2797
+ status: "idle",
2798
+ items: [],
2799
+ hasMore: false
2800
+ });
2801
+ return;
2802
+ }
2803
+ const controller = new AbortController();
2804
+ setRemoteSearch({
2805
+ query: normalizedQuery,
2806
+ status: "loading",
2807
+ items: [],
2808
+ hasMore: false
2809
+ });
2810
+ const timer = window.setTimeout(() => {
2811
+ searchSessions(normalizedQuery, controller.signal).then((result) => {
2812
+ if (controller.signal.aborted) return;
2813
+ setRemoteSearch({
2814
+ query: normalizedQuery,
2815
+ status: "ready",
2816
+ items: result.items,
2817
+ hasMore: result.hasMore
2818
+ });
2819
+ }).catch(() => {
2820
+ if (controller.signal.aborted) return;
2821
+ setRemoteSearch({
2822
+ query: normalizedQuery,
2823
+ status: "error",
2824
+ items: [],
2825
+ hasMore: false
2826
+ });
2827
+ });
2828
+ }, SEARCH_DEBOUNCE_MS);
2829
+ return () => {
2830
+ window.clearTimeout(timer);
2831
+ controller.abort();
2832
+ };
2833
+ }, [normalizedQuery, searchSessions]);
2834
+ const [renameTarget, setRenameTarget] = (0, react.useState)(null);
2835
+ const [renameDraft, setRenameDraft] = (0, react.useState)("");
2836
+ const [renaming, setRenaming] = (0, react.useState)(false);
2837
+ const [renameError, setRenameError] = (0, react.useState)(null);
2838
+ const renameTrimmed = renameDraft.trim();
2839
+ const renameDuplicate = renameTarget !== null && renameTrimmed !== "" && renameTrimmed !== renameTarget.currentTitle && workspaces.some((w) => w.title === renameTrimmed);
2840
+ const renameBlocked = renaming || renameTrimmed === "" || renameTarget === null || renameTrimmed === renameTarget.currentTitle || renameDuplicate;
2841
+ const closeRename = () => {
2842
+ if (renaming) return;
2843
+ setRenameTarget(null);
2844
+ setRenameError(null);
2845
+ };
2846
+ const confirmRename = () => {
2847
+ if (renameBlocked) return;
2848
+ setRenaming(true);
2849
+ setRenameError(null);
2850
+ renameWorkspace(renameTarget.workspaceId, renameTrimmed).then(() => {
2851
+ setRenaming(false);
2852
+ setRenameTarget(null);
2853
+ }).catch((reason) => {
2854
+ setRenaming(false);
2855
+ setRenameError(reason instanceof Error ? reason.message : String(reason));
2856
+ });
2857
+ };
2858
+ const [sessionRenameTarget, setSessionRenameTarget] = (0, react.useState)(null);
2859
+ const [sessionRenameDraft, setSessionRenameDraft] = (0, react.useState)("");
2860
+ const [sessionRenaming, setSessionRenaming] = (0, react.useState)(false);
2861
+ const [sessionRenameError, setSessionRenameError] = (0, react.useState)(null);
2862
+ const sessionRenameTrimmed = sessionRenameDraft.trim();
2863
+ const sessionRenameBlocked = sessionRenaming || sessionRenameTrimmed === "" || sessionRenameTarget === null;
2864
+ const closeSessionRename = () => {
2865
+ if (sessionRenaming) return;
2866
+ setSessionRenameTarget(null);
2867
+ setSessionRenameError(null);
2868
+ };
2869
+ const confirmSessionRename = () => {
2870
+ if (sessionRenameBlocked) return;
2871
+ setSessionRenaming(true);
2872
+ setSessionRenameError(null);
2873
+ renameSession(sessionRenameTarget.sessionId, sessionRenameTrimmed).then(() => {
2874
+ setSessionRenaming(false);
2875
+ setSessionRenameTarget(null);
2876
+ }).catch((reason) => {
2877
+ setSessionRenaming(false);
2878
+ setSessionRenameError(reason instanceof Error ? reason.message : String(reason));
2879
+ });
2880
+ };
2881
+ const onSessionRename = (sessionId, currentTitle) => {
2882
+ setSessionRenameTarget({
2883
+ sessionId,
2884
+ currentTitle
2885
+ });
2886
+ setSessionRenameDraft(currentTitle);
2887
+ setSessionRenameError(null);
2888
+ };
2889
+ const shortcutState = useWorkspaceShortcuts((state) => state);
2890
+ const addShortcut = useShortcuts((rows) => rows.find((row) => row.id === "workspace.add"));
2891
+ const searchShortcut = useShortcuts((rows) => rows.find((row) => row.id === "session.search"));
2892
+ (0, react.useEffect)(() => {
2893
+ if (shortcutState.searchRequest === 0) return;
2894
+ closeAddWorkspace();
2895
+ setSearchExpanded(true);
2896
+ if (!wide) {
2897
+ setSearchOnExpand(true);
2898
+ expandSidebar();
2899
+ } else searchInput.current?.focus({ preventScroll: true });
2900
+ }, [shortcutState.searchRequest]);
2901
+ (0, react.useEffect)(() => {
2902
+ if (!shortcutState.addRequested) return;
2903
+ closeAddWorkspace();
2904
+ if (!wide) {
2905
+ setAddOnExpand(true);
2906
+ expandSidebar();
2907
+ return;
2908
+ }
2909
+ setWsPickerOpen(true);
2910
+ }, [closeAddWorkspace, shortcutState.addRequested]);
2911
+ (0, react.useEffect)(() => {
2912
+ if (!wide || !addOnExpand) return;
2913
+ const timer = window.setTimeout(() => {
2914
+ setWsPickerOpen(true);
2915
+ setAddOnExpand(false);
2916
+ }, EXPAND_SLIDE_MS);
2917
+ return () => {
2918
+ window.clearTimeout(timer);
2919
+ };
2920
+ }, [wide, addOnExpand]);
2921
+ (0, react.useEffect)(() => {
2922
+ const target = shortcutState.renameTarget;
2923
+ if (target === null) return;
2924
+ closeRenameRequest();
2925
+ setSessionRenameTarget({
2926
+ sessionId: target.sessionId,
2927
+ currentTitle: target.currentTitle
2928
+ });
2929
+ setSessionRenameDraft(target.currentTitle);
2930
+ setSessionRenameError(null);
2931
+ }, [closeRenameRequest, shortcutState.renameTarget]);
2932
+ (0, react.useEffect)(() => {
2933
+ setDirectoryBusy(wsPickerOpen);
2934
+ }, [setDirectoryBusy, wsPickerOpen]);
2935
+ const onSessionArchive = (sessionId) => {
2936
+ archiveSession(sessionId).catch((reason) => {
2937
+ console.warn("session archive rejected:", reason);
2938
+ });
2939
+ };
2940
+ const onSessionPin = (sessionId) => {
2941
+ pinSession(sessionId).catch((reason) => {
2942
+ console.warn("session pin rejected:", reason);
2943
+ });
2944
+ };
2945
+ const onSessionUnpin = (sessionId) => {
2946
+ unpinSession(sessionId).catch((reason) => {
2947
+ console.warn("session unpin rejected:", reason);
2948
+ });
2949
+ };
2950
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
2951
+ const [deleting, setDeleting] = (0, react.useState)(false);
2952
+ const [deleteCommittedId, setDeleteCommittedId] = (0, react.useState)(null);
2953
+ const [deleteError, setDeleteError] = (0, react.useState)(null);
2954
+ (0, react.useEffect)(() => {
2955
+ if (deleteCommittedId === null || workspaces.some((workspace) => workspace.workspaceId === deleteCommittedId)) return;
2956
+ setDeleting(false);
2957
+ setDeleteCommittedId(null);
2958
+ setDeleteTarget(null);
2959
+ }, [deleteCommittedId, workspaces]);
2960
+ const closeDelete = () => {
2961
+ if (deleting) return;
2962
+ setDeleteTarget(null);
2963
+ setDeleteError(null);
2964
+ };
2965
+ const confirmDelete = () => {
2966
+ /* v8 ignore next -- the Modal is absent without a target and its button is disabled while deleting. */
2967
+ if (deleting || deleteTarget === null) return;
2968
+ setDeleting(true);
2969
+ setDeleteCommittedId(null);
2970
+ setDeleteError(null);
2971
+ deleteWorkspace(deleteTarget.workspaceId).then(() => {
2972
+ setDeleteCommittedId(deleteTarget.workspaceId);
2973
+ }).catch((reason) => {
2974
+ setDeleting(false);
2975
+ setDeleteError(reason instanceof Error ? reason.message : String(reason));
2976
+ });
2977
+ };
2978
+ return (0, react_jsx_runtime.jsxs)("div", {
2979
+ className: clsx(WorkspaceBrowser_module_css_default.root, !wide && WorkspaceBrowser_module_css_default.rail),
2980
+ children: [
2981
+ (0, react_jsx_runtime.jsxs)("div", {
2982
+ className: WorkspaceBrowser_module_css_default.sectionHeader,
2983
+ children: [
2984
+ wide && (0, react_jsx_runtime.jsx)("span", {
2985
+ className: clsx(WorkspaceBrowser_module_css_default.sectionLabel, WorkspaceBrowser_module_css_default.wide, searchExpanded && WorkspaceBrowser_module_css_default.sectionLabelHidden),
2986
+ children: groupBy === "flat" ? t("section.sessions") : t("section.workspaces")
2987
+ }),
2988
+ wide && (0, react_jsx_runtime.jsx)("div", {
2989
+ className: clsx(WorkspaceBrowser_module_css_default.searchSlot, searchExpanded && WorkspaceBrowser_module_css_default.searchSlotExpanded),
2990
+ children: (0, react_jsx_runtime.jsxs)("div", {
2991
+ ref: searchRoot,
2992
+ className: clsx(WorkspaceBrowser_module_css_default.search, searchExpanded && WorkspaceBrowser_module_css_default.searchExpanded),
2993
+ onClick: () => {
2994
+ setWsPickerOpen(false);
2995
+ setSearchExpanded(true);
2996
+ searchInput.current?.focus();
2997
+ requestSearch();
2998
+ },
2999
+ children: [
3000
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
3001
+ label: t("search"),
3002
+ shortcutKeys: searchShortcut?.keys,
3003
+ side: "bottom",
3004
+ delayMs: 500,
3005
+ disabled: searchExpanded,
3006
+ children: (0, react_jsx_runtime.jsx)("button", {
3007
+ type: "button",
3008
+ className: WorkspaceBrowser_module_css_default.searchButton,
3009
+ "aria-label": t("search.sessions.aria"),
3010
+ "aria-keyshortcuts": searchShortcut?.aria,
3011
+ "aria-expanded": searchExpanded,
3012
+ onClick: () => {
3013
+ setWsPickerOpen(false);
3014
+ setSearchExpanded(true);
3015
+ requestSearch();
3016
+ },
3017
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconSearchOutline16, { size: searchExpanded ? 11 : 14 })
3018
+ })
3019
+ }),
3020
+ (0, react_jsx_runtime.jsx)("input", {
3021
+ ref: searchInput,
3022
+ className: WorkspaceBrowser_module_css_default.searchInput,
3023
+ type: "text",
3024
+ placeholder: t("search.placeholder"),
3025
+ maxLength: SEARCH_QUERY_MAX_CODE_UNITS,
3026
+ value: query,
3027
+ tabIndex: searchExpanded ? 0 : -1,
3028
+ onChange: (e) => {
3029
+ setQuery(sanitizeSearchQuery(e.target.value));
3030
+ },
3031
+ onKeyDown: (e) => {
3032
+ if (e.key !== "Escape") return;
3033
+ setQuery("");
3034
+ setSearchExpanded(false);
3035
+ }
3036
+ }),
3037
+ searchExpanded && (0, react_jsx_runtime.jsx)("button", {
3038
+ type: "button",
3039
+ className: WorkspaceBrowser_module_css_default.clearButton,
3040
+ "aria-label": t("search.clear"),
3041
+ onClick: (e) => {
3042
+ e.stopPropagation();
3043
+ setQuery("");
3044
+ setSearchExpanded(false);
3045
+ },
3046
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCloseFill14, {})
3047
+ })
3048
+ ]
3049
+ })
3050
+ }),
3051
+ wide && (0, react_jsx_runtime.jsxs)("div", {
3052
+ className: clsx(WorkspaceBrowser_module_css_default.headerActions, searchExpanded && WorkspaceBrowser_module_css_default.headerActionsHidden),
3053
+ children: [(0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
3054
+ groupBy,
3055
+ orderBy,
3056
+ onGroupPick: (mode) => {
3057
+ actions.setGroupBy(mode);
3058
+ },
3059
+ onOrderPick: (mode) => {
3060
+ actions.setOrderBy(mode, activeSessionOrders);
3061
+ },
3062
+ t
3063
+ }), directoryFlowAvailable && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
3064
+ label: t("workspace.add"),
3065
+ shortcutKeys: addShortcut?.keys,
3066
+ side: "bottom",
3067
+ delayMs: 500,
3068
+ children: (0, react_jsx_runtime.jsx)("button", {
3069
+ ref: wsPlusRef,
3070
+ type: "button",
3071
+ className: WorkspaceBrowser_module_css_default.iconButton,
3072
+ "aria-label": t("workspace.add"),
3073
+ "aria-keyshortcuts": addShortcut?.aria,
3074
+ onClick: () => {
3075
+ requestAddWorkspace();
3076
+ },
3077
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconProjectAddOutline16, { size: 16 })
3078
+ })
3079
+ })]
3080
+ }),
3081
+ (0, react_jsx_runtime.jsx)(WorkspacePickFlow, {
3082
+ t,
3083
+ open: wsPickerOpen,
3084
+ anchorRef: wsPlusRef,
3085
+ useWorkspaces,
3086
+ createWorkspace,
3087
+ useDirectoryFlow,
3088
+ renderDirectoryFlow: (owner) => renderSlot("sidebar.workspaces.directoryFlow", owner),
3089
+ addOnly: true,
3090
+ side: "right",
3091
+ onPick: (workspaceId) => {
3092
+ setWsPickerOpen(false);
3093
+ startSession(workspaceId);
3094
+ },
3095
+ onClose: () => {
3096
+ setWsPickerOpen(false);
3097
+ }
3098
+ })
3099
+ ]
3100
+ }),
3101
+ (0, react_jsx_runtime.jsx)("div", {
3102
+ id: "workbench-list-panel",
3103
+ className: WorkspaceBrowser_module_css_default.listArea,
3104
+ children: wide && (normalizedQuery !== "" ? (0, react_jsx_runtime.jsx)(SearchResults, {
3105
+ usePanelInfo,
3106
+ useSessions,
3107
+ useSessionStatus,
3108
+ open: openSearchResult,
3109
+ workspaces,
3110
+ archivedSessionIds,
3111
+ query: normalizedQuery,
3112
+ remote: remoteSearch,
3113
+ resultLimit: searchResultLimit,
3114
+ t
3115
+ }) : groupBy === "flat" ? (0, react_jsx_runtime.jsx)(FlatList, {
3116
+ usePanelInfo,
3117
+ list,
3118
+ sessionIds: orderedFlatSessionIds,
3119
+ useSessionStatus,
3120
+ open,
3121
+ forkSession,
3122
+ onSessionRename,
3123
+ onSessionArchive,
3124
+ onSessionPin,
3125
+ onSessionUnpin,
3126
+ pinnedSessionIds,
3127
+ setSessionOrder: saveSessionOrder,
3128
+ revealSessionId,
3129
+ onSessionRevealed: acknowledgeSessionReveal,
3130
+ renderSlot,
3131
+ t
3132
+ }) : (0, react_jsx_runtime.jsx)(SessionTree, {
3133
+ usePanelInfo,
3134
+ list,
3135
+ useSessionStatus,
3136
+ renderSlot,
3137
+ onSessionRename,
3138
+ onSessionArchive,
3139
+ onSessionPin,
3140
+ onSessionUnpin,
3141
+ forkSession,
3142
+ workspaces: orderedWorkspaces,
3143
+ ungroupedSessionIds: orderedUngroupedSessionIds,
3144
+ workspaceReady,
3145
+ nestWorkspaces: groupBy === "workspace-tree",
3146
+ groupExpansion,
3147
+ setGroupExpanded: actions.setGroupExpanded,
3148
+ setSessionOrder: saveSessionOrder,
3149
+ archivedSessionIds,
3150
+ pinnedSessionIds,
3151
+ startSession,
3152
+ open,
3153
+ insertWorkspaceBefore,
3154
+ revealSessionId,
3155
+ onSessionRevealed: acknowledgeSessionReveal,
3156
+ home,
3157
+ t,
3158
+ onRenameRequest: (workspaceId, currentTitle) => {
3159
+ setRenameTarget({
3160
+ workspaceId,
3161
+ currentTitle
3162
+ });
3163
+ setRenameDraft(currentTitle);
3164
+ setRenameError(null);
3165
+ },
3166
+ onDeleteRequest: (workspaceId, title) => {
3167
+ setDeleteTarget({
3168
+ workspaceId,
3169
+ title
3170
+ });
3171
+ setDeleteError(null);
3172
+ }
3173
+ }))
3174
+ }),
3175
+ (0, react_jsx_runtime.jsxs)(_qilin_agent_client_ui_primitives.Modal, {
3176
+ open: renameTarget !== null,
3177
+ onClose: closeRename,
3178
+ closeLabel: t("close"),
3179
+ title: t("rename.workspace.title"),
3180
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
3181
+ variant: "outline",
3182
+ disabled: renaming,
3183
+ onClick: closeRename,
3184
+ children: t("cancel")
3185
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
3186
+ variant: "primary",
3187
+ disabled: renameBlocked,
3188
+ onClick: confirmRename,
3189
+ children: t("rename")
3190
+ })] }),
3191
+ children: [
3192
+ (0, react_jsx_runtime.jsx)("input", {
3193
+ className: WorkspaceBrowser_module_css_default.renameInput,
3194
+ value: renameDraft,
3195
+ "aria-label": t("field.workspaceName"),
3196
+ autoFocus: true,
3197
+ disabled: renaming,
3198
+ onFocus: (e) => {
3199
+ e.target.select();
3200
+ },
3201
+ onChange: (e) => {
3202
+ setRenameDraft(e.target.value);
3203
+ setRenameError(null);
3204
+ },
3205
+ onCompositionStart: () => {
3206
+ composingRef.current = true;
3207
+ },
3208
+ onCompositionEnd: () => {
3209
+ composingRef.current = false;
3210
+ },
3211
+ onKeyDown: (e) => {
3212
+ if (e.key === "Enter" && !composingRef.current) {
3213
+ e.preventDefault();
3214
+ confirmRename();
3215
+ }
3216
+ }
3217
+ }),
3218
+ renameDuplicate && (0, react_jsx_runtime.jsx)("div", {
3219
+ className: WorkspaceBrowser_module_css_default.renameError,
3220
+ role: "alert",
3221
+ children: t("conflict.named", { name: renameTrimmed })
3222
+ }),
3223
+ renameError !== null && (0, react_jsx_runtime.jsx)("div", {
3224
+ className: WorkspaceBrowser_module_css_default.renameError,
3225
+ role: "alert",
3226
+ children: renameError
3227
+ })
3228
+ ]
3229
+ }),
3230
+ (0, react_jsx_runtime.jsxs)(_qilin_agent_client_ui_primitives.Modal, {
3231
+ open: sessionRenameTarget !== null,
3232
+ onClose: closeSessionRename,
3233
+ closeLabel: t("close"),
3234
+ title: t("rename.session.title"),
3235
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
3236
+ variant: "outline",
3237
+ disabled: sessionRenaming,
3238
+ onClick: closeSessionRename,
3239
+ children: t("cancel")
3240
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
3241
+ variant: "primary",
3242
+ disabled: sessionRenameBlocked,
3243
+ onClick: confirmSessionRename,
3244
+ children: t("rename")
3245
+ })] }),
3246
+ children: [(0, react_jsx_runtime.jsx)("input", {
3247
+ className: WorkspaceBrowser_module_css_default.renameInput,
3248
+ value: sessionRenameDraft,
3249
+ "aria-label": t("field.sessionName"),
3250
+ autoFocus: true,
3251
+ disabled: sessionRenaming,
3252
+ onFocus: (e) => {
3253
+ e.target.select();
3254
+ },
3255
+ onChange: (e) => {
3256
+ setSessionRenameDraft(e.target.value);
3257
+ setSessionRenameError(null);
3258
+ },
3259
+ onCompositionStart: () => {
3260
+ composingRef.current = true;
3261
+ },
3262
+ onCompositionEnd: () => {
3263
+ composingRef.current = false;
3264
+ },
3265
+ onKeyDown: (e) => {
3266
+ if (e.key === "Enter" && !composingRef.current) {
3267
+ e.preventDefault();
3268
+ confirmSessionRename();
3269
+ }
3270
+ }
3271
+ }), sessionRenameError !== null && (0, react_jsx_runtime.jsx)("div", {
3272
+ className: WorkspaceBrowser_module_css_default.renameError,
3273
+ role: "alert",
3274
+ children: sessionRenameError
3275
+ })]
3276
+ }),
3277
+ (0, react_jsx_runtime.jsxs)(_qilin_agent_client_ui_primitives.Modal, {
3278
+ open: deleteTarget !== null,
3279
+ onClose: closeDelete,
3280
+ closeLabel: t("close"),
3281
+ title: t("delete.workspace"),
3282
+ ...deleteTarget === null ? {} : { description: t("delete.desc", { name: deleteTarget.title }) },
3283
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
3284
+ variant: "outline",
3285
+ disabled: deleting,
3286
+ onClick: closeDelete,
3287
+ children: t("cancel")
3288
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
3289
+ variant: "outline",
3290
+ className: WorkspaceBrowser_module_css_default.deleteAction,
3291
+ disabled: deleting,
3292
+ onClick: confirmDelete,
3293
+ children: t("delete.workspace")
3294
+ })] }),
3295
+ children: [deleting && (0, react_jsx_runtime.jsx)("div", {
3296
+ className: WorkspaceBrowser_module_css_default.deleteStatus,
3297
+ role: "status",
3298
+ children: t("delete.pending")
3299
+ }), deleteError !== null && (0, react_jsx_runtime.jsx)("div", {
3300
+ className: WorkspaceBrowser_module_css_default.renameError,
3301
+ role: "alert",
3302
+ children: deleteError
3303
+ })]
3304
+ })
3305
+ ]
3306
+ });
3307
+ }
3308
+ //#endregion
3309
+ //#region lib/types/client/locales.js
3310
+ /**
3311
+ * `workspace` namespace dictionaries: the browsing region (section header,
3312
+ * search, tree rows, dialogs) and the pick/add flow. Runtime failure
3313
+ * messages (wire error strings) pass through untranslated by policy.
3314
+ */
3315
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3316
+ const zh = {
3317
+ "group.ungrouped": "未分组",
3318
+ "session.new": "新会话",
3319
+ "session.untitled": "未命名",
3320
+ "shortcut.noSession": "请先选择一个会话",
3321
+ "shortcut.noPicker": "目录选择器不可用",
3322
+ "shortcut.directoryBusy": "正在选择或添加工作区",
3323
+ "shortcut.noCompletedTurn": "当前会话没有已结束的轮次",
3324
+ "section.workspaces": "工作区",
3325
+ "section.sessions": "会话",
3326
+ "workbench.tabs.aria": "切换工作台",
3327
+ "workbench.general": "通用",
3328
+ "workbench.coding": "编码",
3329
+ "preset.aria": "运行模式",
3330
+ "preset.hint": "选择新任务使用的运行模式",
3331
+ "viewOptions.label": "视图选项",
3332
+ "groupBy.label": "分组方式",
3333
+ "groupBy.workspace": "按工作区",
3334
+ "groupBy.workspaceTree": "按工作区树",
3335
+ "groupBy.flat": "单列表",
3336
+ "orderBy.label": "排序方式",
3337
+ "orderBy.manual": "手动排序",
3338
+ "orderBy.updated": "最近更新",
3339
+ "sessions.expand": "展开其余 {n} 个会话",
3340
+ "sessions.collapse": "收起",
3341
+ "empty.none": "暂无会话",
3342
+ "empty.noMatches": "无匹配结果",
3343
+ "workspace.add": "添加工作区",
3344
+ "search.sessions.aria": "搜索会话",
3345
+ "search.placeholder": "搜索会话…",
3346
+ "search.clear": "清除搜索",
3347
+ "search.results.aria": "搜索结果",
3348
+ "search.pending": "正在搜索会话历史…",
3349
+ "search.unavailable": "内容搜索暂不可用,仅显示名称匹配。",
3350
+ "search.noMatches": "无匹配会话",
3351
+ "search.hasMore": "仅显示前 {n} 条结果,请缩小搜索范围。",
3352
+ "menu.addWorkspace": "添加工作区…",
3353
+ "picker.loading": "正在加载工作区…",
3354
+ "conflict.named": "已存在名为“{name}”的工作区。",
3355
+ "folderError.title": "无法打开文件夹",
3356
+ "folderError.retry": "重新选择",
3357
+ "rename": "重命名",
3358
+ "rename.workspace.title": "重命名工作区",
3359
+ "rename.session.title": "重命名会话",
3360
+ "field.workspaceName": "工作区名称",
3361
+ "field.sessionName": "会话名称",
3362
+ "delete.workspace": "删除工作区",
3363
+ "delete.desc": "将把“{name}”从工作区列表中移除。文件夹与会话记录会保留,其会话将显示在“未分组”下。",
3364
+ "delete.pending": "正在删除工作区…",
3365
+ "menu.pinSession": "置顶会话",
3366
+ "menu.unpinSession": "取消置顶",
3367
+ "menu.fork": "分叉会话",
3368
+ "menu.archiveSession": "归档会话",
3369
+ "row.pinned": "已置顶",
3370
+ "sessions.count.one": "{n} 个会话",
3371
+ "sessions.count.other": "{n} 个会话",
3372
+ "actions.workspace.aria": "工作区“{name}”的操作",
3373
+ "actions.session.aria": "会话“{name}”的操作",
3374
+ "actions.newSession.aria": "在“{name}”中新建会话",
3375
+ "actions.pin": "置顶会话",
3376
+ "actions.unpin": "取消置顶",
3377
+ "status.running": "进行中",
3378
+ "status.subagentsRunning.one": "{n} 个子代理运行中",
3379
+ "status.subagentsRunning.other": "{n} 个子代理运行中",
3380
+ "status.idle": "空闲",
3381
+ "status.waitingApproval": "等待审批",
3382
+ "status.planReview": "计划待审",
3383
+ "status.waitingAnswer": "等待回答",
3384
+ "status.completed": "已完成",
3385
+ "hover.created": "创建于 {time}",
3386
+ "hover.copied": "已复制",
3387
+ "date.ymd": "{y}年{m}月{d}日",
3388
+ "time.now": "刚刚",
3389
+ "time.minutes": "{n}分钟",
3390
+ "time.hours": "{n}小时",
3391
+ "time.days": "{n}天",
3392
+ "time.months": "{n}个月",
3393
+ "time.years": "{n}年",
3394
+ "time.ago": "{t}前"
3395
+ };
3396
+ /** English dictionary, checked complete against the zh key set. */
3397
+ const en = {
3398
+ "group.ungrouped": "Ungrouped",
3399
+ "session.new": "New Session",
3400
+ "session.untitled": "Untitled",
3401
+ "shortcut.noSession": "Select a session first",
3402
+ "shortcut.noPicker": "Directory picker unavailable",
3403
+ "shortcut.directoryBusy": "Selecting or adding a workspace",
3404
+ "shortcut.noCompletedTurn": "This session has no completed turn",
3405
+ "section.workspaces": "Workspaces",
3406
+ "section.sessions": "Sessions",
3407
+ "workbench.tabs.aria": "Switch workbench",
3408
+ "workbench.general": "General",
3409
+ "workbench.coding": "Coding",
3410
+ "preset.aria": "Run mode",
3411
+ "preset.hint": "Choose the run mode for new tasks",
3412
+ "viewOptions.label": "View options",
3413
+ "groupBy.label": "Group by",
3414
+ "groupBy.workspace": "WorkSpace",
3415
+ "groupBy.workspaceTree": "Workspace Tree",
3416
+ "groupBy.flat": "In one list",
3417
+ "orderBy.label": "Order by",
3418
+ "orderBy.manual": "Manual",
3419
+ "orderBy.updated": "Last updated",
3420
+ "sessions.expand": "Show {n} more sessions",
3421
+ "sessions.collapse": "Show less",
3422
+ "empty.none": "No sessions yet",
3423
+ "empty.noMatches": "No matches",
3424
+ "workspace.add": "Add workspace",
3425
+ "search.sessions.aria": "Search sessions",
3426
+ "search.placeholder": "Search sessions...",
3427
+ "search.clear": "Clear search",
3428
+ "search.results.aria": "Search results",
3429
+ "search.pending": "Searching session history…",
3430
+ "search.unavailable": "Content search is temporarily unavailable. Showing name matches.",
3431
+ "search.noMatches": "No matching sessions",
3432
+ "search.hasMore": "Showing the first {n} results. Narrow your search.",
3433
+ "menu.addWorkspace": "Add workspace…",
3434
+ "picker.loading": "Loading workspaces…",
3435
+ "conflict.named": "A workspace named “{name}” already exists.",
3436
+ "folderError.title": "Couldn’t open folder",
3437
+ "folderError.retry": "Choose again",
3438
+ "rename": "Rename",
3439
+ "rename.workspace.title": "Rename workspace",
3440
+ "rename.session.title": "Rename session",
3441
+ "field.workspaceName": "Workspace name",
3442
+ "field.sessionName": "Session name",
3443
+ "delete.workspace": "Delete workspace",
3444
+ "delete.desc": "This removes “{name}” from the workspace list. The folder and session logs will be kept. Its sessions will appear under Ungrouped.",
3445
+ "delete.pending": "Deleting workspace…",
3446
+ "menu.pinSession": "Pin session",
3447
+ "menu.unpinSession": "Unpin session",
3448
+ "menu.fork": "Fork session",
3449
+ "menu.archiveSession": "Archive session",
3450
+ "row.pinned": "Pinned",
3451
+ "sessions.count.one": "{n} session",
3452
+ "sessions.count.other": "{n} sessions",
3453
+ "actions.workspace.aria": "Workspace actions for {name}",
3454
+ "actions.session.aria": "Session actions for {name}",
3455
+ "actions.newSession.aria": "New session in {name}",
3456
+ "actions.pin": "Pin",
3457
+ "actions.unpin": "Unpin",
3458
+ "status.running": "Running",
3459
+ "status.subagentsRunning.one": "{n} subagent running",
3460
+ "status.subagentsRunning.other": "{n} subagents running",
3461
+ "status.idle": "Idle",
3462
+ "status.waitingApproval": "Waiting for approval",
3463
+ "status.planReview": "Plan awaiting review",
3464
+ "status.waitingAnswer": "Waiting for answer",
3465
+ "status.completed": "Completed",
3466
+ "hover.created": "Created {time}",
3467
+ "hover.copied": "Copied",
3468
+ "date.ymd": "{y}-{m}-{d}",
3469
+ "time.now": "now",
3470
+ "time.minutes": "{n}min",
3471
+ "time.hours": "{n}h",
3472
+ "time.days": "{n}d",
3473
+ "time.months": "{n}mo",
3474
+ "time.years": "{n}y",
3475
+ "time.ago": "{t} ago"
3476
+ };
3477
+ //#endregion
3478
+ //#region lib/types/client/index.js
3479
+ /** Dictionary namespace owned by this plugin. */
3480
+ const NS = "workspace";
3481
+ /**
3482
+ * Required services (cordis fiber inject). The target slots are declared by
3483
+ * the ui-sidebar / ui-conversation applies, whose activation order relative
3484
+ * to this one is NOT constrained: qilin.client.inject edges are informational
3485
+ * (loading/prefetch metadata, never apply sequencing) and neither owner
3486
+ * provides a waitable service. apply therefore depends on each slot
3487
+ * declaration through `slots.inject()` instead of assuming order.
3488
+ */
3489
+ const inject = [
3490
+ "slots",
3491
+ "sessions",
3492
+ "workspaces",
3493
+ "locale",
3494
+ "remote",
3495
+ "remote.directoryPicker",
3496
+ "remote.agentPresets",
3497
+ "workbench",
3498
+ "layout",
3499
+ "shortcuts"
3500
+ ];
3501
+ /**
3502
+ * Register the browser and picker once their slot declarations are on the
3503
+ * ledger. Inject factories return plain callbacks; data reads use the
3504
+ * framework's global hooks.
3505
+ * @param ctx - client root context.
3506
+ */
3507
+ function apply(ctx) {
3508
+ const sessions = ctx.get("sessions");
3509
+ const workspaces = ctx.get("workspaces");
3510
+ const workbench = ctx.get("workbench");
3511
+ const viewHandle = createWorkspaceViewStore();
3512
+ const viewInstance = viewHandle.create();
3513
+ const viewStore = {
3514
+ ...viewHandle,
3515
+ create: () => viewInstance
3516
+ };
3517
+ const uiWorkspace = new UiWorkspaceService(ctx, ctx.remote.directoryPicker, ctx.remote.agentPresets, workbench, workspaces, sessions, viewInstance.actions);
3518
+ ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } });
3519
+ ctx.effect(() => ctx.locale.register(NS, {
3520
+ zh,
3521
+ en
3522
+ }), "ui-workspace: dictionaries");
3523
+ const shortcutControls = createWorkspaceShortcutControls();
3524
+ const onWorkbenchSwitch = (tag) => {
3525
+ workbench.setActive(tag);
3526
+ uiWorkspace.rebindBlanksAfterTagSwitch();
3527
+ };
3528
+ const searchSessions = async (query, signal) => {
3529
+ const result = await sessions.search(query, signal);
3530
+ if (!result.ok) throw new Error(result.error.message);
3531
+ return result.value;
3532
+ };
3533
+ const flowSource = (hole) => ({
3534
+ getSnapshot: () => ctx.slots.entries(hole).length > 0,
3535
+ subscribe: (listener) => ctx.slots.subscribe(hole, listener)
3536
+ });
3537
+ const browserFlowSource = flowSource("sidebar.workspaces.directoryFlow");
3538
+ const hostInfo = {
3539
+ getSnapshot: () => ctx.remote.$host,
3540
+ subscribe: (listener) => ctx.on("connection/reset", listener)
3541
+ };
3542
+ const pickerFlowSource = flowSource("conversation.hero.workspace.directoryFlow");
3543
+ const openSession = (sessionId) => {
3544
+ uiWorkspace.openSession(sessionId);
3545
+ };
3546
+ const browserInjected = () => ({
3547
+ startSession: (workspaceId) => {
3548
+ uiWorkspace.startSession(workspaceId);
3549
+ },
3550
+ open: openSession,
3551
+ searchSessions,
3552
+ searchResultLimit: sessions.searchResultLimit,
3553
+ renameSession: async (sessionId, title) => {
3554
+ const result = await sessions.using(sessionId, { source: "workspaceOperation" }, (reference) => reference.binding.session.rename(title));
3555
+ if (!result.ok) throw new Error(result.error.message);
3556
+ },
3557
+ forkSession: (sessionId) => {
3558
+ uiWorkspace.forkSession(sessionId).catch(() => {});
3559
+ },
3560
+ renameWorkspace: async (workspaceId, title) => {
3561
+ await workspaces.rename(workspaceId, title);
3562
+ },
3563
+ deleteWorkspace: async (workspaceId) => {
3564
+ await workspaces.delete(workspaceId);
3565
+ },
3566
+ insertWorkspaceBefore: async (workspaceId, beforeWorkspaceId) => {
3567
+ await workspaces.insertBefore(workspaceId, beforeWorkspaceId);
3568
+ },
3569
+ archiveSession: async (sessionId) => {
3570
+ await uiWorkspace.archiveSession(sessionId);
3571
+ },
3572
+ pinSession: (sessionId) => uiWorkspace.pinSession(sessionId),
3573
+ unpinSession: (sessionId) => uiWorkspace.unpinSession(sessionId),
3574
+ createWorkspace: (input) => workspaces.create(input),
3575
+ requestSearch: shortcutControls.search,
3576
+ requestAddWorkspace: shortcutControls.add,
3577
+ closeAddWorkspace: shortcutControls.closeAdd,
3578
+ closeRenameRequest: shortcutControls.closeRename,
3579
+ setDirectoryBusy: shortcutControls.directoryBusy,
3580
+ hooks: {
3581
+ directoryFlow: browserFlowSource,
3582
+ hostInfo,
3583
+ workspaceShortcuts: shortcutControls.state,
3584
+ shortcuts: ctx.shortcuts.catalog
3585
+ }
3586
+ });
3587
+ installWorkspaceShortcuts(ctx, uiWorkspace, shortcutControls, (sessionId) => {
3588
+ uiWorkspace.archiveSession(sessionId);
3589
+ });
3590
+ const pickerInjected = () => ({
3591
+ createWorkspace: (input) => workspaces.create(input),
3592
+ hooks: { directoryFlow: pickerFlowSource }
3593
+ });
3594
+ ctx.slots.inject("sidebar.workbench", () => ctx.slots.register({
3595
+ name: "sidebar.workbench",
3596
+ inject: () => ({
3597
+ hooks: { workbench: workbench.state },
3598
+ onWorkbenchSwitch
3599
+ }),
3600
+ locale: NS
3601
+ }, WorkbenchSwitchSeat));
3602
+ ctx.slots.inject("sidebar.workspaces", () => ctx.slots.register({
3603
+ name: "sidebar.workspaces",
3604
+ children: {
3605
+ "sidebar.workspaces.directoryFlow": {
3606
+ kind: "single",
3607
+ scope: "root"
3608
+ },
3609
+ "sidebar.session.row.leading": {
3610
+ kind: "list",
3611
+ scope: "root"
3612
+ },
3613
+ "sidebar.session.row.hover": {
3614
+ kind: "list",
3615
+ scope: "root"
3616
+ }
3617
+ },
3618
+ store: viewStore,
3619
+ inject: browserInjected,
3620
+ locale: NS
3621
+ }, WorkspaceBrowser));
3622
+ ctx.slots.inject("conversation.hero.workspace", () => ctx.slots.register({
3623
+ name: "conversation.hero.workspace",
3624
+ children: { "conversation.hero.workspace.directoryFlow": {
3625
+ kind: "single",
3626
+ scope: "root"
3627
+ } },
3628
+ inject: pickerInjected,
3629
+ locale: NS
3630
+ }, WorkspacePicker));
3631
+ const rosterStore = (0, _qilin_agent_client_store.createSnapshotStore)({
3632
+ status: "idle",
3633
+ presets: []
3634
+ });
3635
+ let rosterGeneration = 0;
3636
+ const loadRoster = () => {
3637
+ const generation = ++rosterGeneration;
3638
+ ctx.remote.agentPresets.list().then((result) => {
3639
+ if (generation !== rosterGeneration) return;
3640
+ if (!result.ok && result.error.code === "gateway/invocation-unavailable") {
3641
+ rosterStore.set({
3642
+ status: "ready",
3643
+ presets: []
3644
+ });
3645
+ return;
3646
+ }
3647
+ if (!result.ok) {
3648
+ rosterStore.set({
3649
+ status: "failed",
3650
+ presets: []
3651
+ });
3652
+ return;
3653
+ }
3654
+ rosterStore.set({
3655
+ status: "ready",
3656
+ presets: result.value.presets.filter((preset) => preset.broken === void 0).map((preset) => ({
3657
+ id: preset.id,
3658
+ ...preset.name === void 0 ? {} : { name: preset.name },
3659
+ ...preset.description === void 0 ? {} : { description: preset.description }
3660
+ }))
3661
+ });
3662
+ }).catch(() => {
3663
+ if (generation === rosterGeneration) rosterStore.set({
3664
+ status: "failed",
3665
+ presets: []
3666
+ });
3667
+ });
3668
+ };
3669
+ ctx.effect(() => {
3670
+ loadRoster();
3671
+ return ctx.on("connection/reset", loadRoster);
3672
+ }, "ui-workspace: preset roster refresh");
3673
+ const pickPreset = (presetId) => {
3674
+ workbench.setPresetFor(workbench.state.getSnapshot().active, presetId);
3675
+ uiWorkspace.adoptBlankSessionPreset(presetId);
3676
+ };
3677
+ ctx.slots.inject("conversation.hero.agentPreset", () => ctx.slots.register({
3678
+ name: "conversation.hero.agentPreset",
3679
+ priority: -1,
3680
+ inject: () => ({
3681
+ hooks: {
3682
+ workbench: workbench.state,
3683
+ roster: rosterStore
3684
+ },
3685
+ tagChoices: (tag) => workbench.tagChoices(tag),
3686
+ pick: pickPreset
3687
+ }),
3688
+ locale: NS
3689
+ }, AgentPresetChip));
3690
+ }
3691
+ //#endregion
3692
+ exports.apply = apply;
3693
+ exports.inject = inject;
3694
+ return module.exports;
3695
+ }
3696
+ });
3697
+
3698
+ //# sourceMappingURL=client.js.map