@huanlin/dsh-plugin-ya-workspace-sidebar 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4,6 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
7
8
  let react = require("react");
8
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
10
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -58,6 +59,7 @@ window.__ModuleLoader__.load({
58
59
  completed: "完了",
59
60
  collapse: "折りたたむ",
60
61
  expand: "展開",
62
+ resizeRecent: "最近のセッションの高さをドラッグで調整",
61
63
  today: "今日",
62
64
  yesterday: "昨日",
63
65
  date: "{m}月{d}日",
@@ -112,6 +114,7 @@ window.__ModuleLoader__.load({
112
114
  completed: "Abgeschlossen",
113
115
  collapse: "Einklappen",
114
116
  expand: "Ausklappen",
117
+ resizeRecent: "Ziehen, um die Höhe des Bereichs „Letzte Sitzungen“ anzupassen",
115
118
  today: "Heute",
116
119
  yesterday: "Gestern",
117
120
  date: "{d}.{m}.",
@@ -166,6 +169,7 @@ window.__ModuleLoader__.load({
166
169
  completed: "Terminé",
167
170
  collapse: "Réduire",
168
171
  expand: "Développer",
172
+ resizeRecent: "Faire glisser pour redimensionner la zone des sessions récentes",
169
173
  today: "Aujourd’hui",
170
174
  yesterday: "Hier",
171
175
  date: "{d}/{m}",
@@ -220,6 +224,7 @@ window.__ModuleLoader__.load({
220
224
  completed: "Concluído",
221
225
  collapse: "Recolher",
222
226
  expand: "Expandir",
227
+ resizeRecent: "Arraste para redimensionar a área de sessões recentes",
223
228
  today: "Hoje",
224
229
  yesterday: "Ontem",
225
230
  date: "{d}/{m}",
@@ -274,6 +279,7 @@ window.__ModuleLoader__.load({
274
279
  completed: "완료됨",
275
280
  collapse: "접기",
276
281
  expand: "펼치기",
282
+ resizeRecent: "드래그하여 최근 세션 영역 높이 조절",
277
283
  today: "오늘",
278
284
  yesterday: "어제",
279
285
  date: "{m}월 {d}일",
@@ -328,6 +334,7 @@ window.__ModuleLoader__.load({
328
334
  completed: "مكتمل",
329
335
  collapse: "طي",
330
336
  expand: "توسيع",
337
+ resizeRecent: "اسحب لضبط ارتفاع منطقة الجلسات الأخيرة",
331
338
  today: "اليوم",
332
339
  yesterday: "أمس",
333
340
  date: "{m}/{d}",
@@ -382,6 +389,7 @@ window.__ModuleLoader__.load({
382
389
  completed: "पूर्ण",
383
390
  collapse: "संक्षिप्त करें",
384
391
  expand: "विस्तार करें",
392
+ resizeRecent: "हाल के सेशन क्षेत्र की ऊँचाई समायोजित करने के लिए खींचें",
385
393
  today: "आज",
386
394
  yesterday: "कल",
387
395
  date: "{d}/{m}",
@@ -436,6 +444,7 @@ window.__ModuleLoader__.load({
436
444
  completed: "Selesai",
437
445
  collapse: "Ciutkan",
438
446
  expand: "Bentangkan",
447
+ resizeRecent: "Seret untuk mengatur tinggi area sesi terbaru",
439
448
  today: "Hari ini",
440
449
  yesterday: "Kemarin",
441
450
  date: "{d}/{m}",
@@ -490,6 +499,7 @@ window.__ModuleLoader__.load({
490
499
  completed: "Tamamlandı",
491
500
  collapse: "Daralt",
492
501
  expand: "Genişlet",
502
+ resizeRecent: "Son oturumlar alanının yüksekliğini sürükleyerek ayarlayın",
493
503
  today: "Bugün",
494
504
  yesterday: "Dün",
495
505
  date: "{d}.{m}",
@@ -544,6 +554,7 @@ window.__ModuleLoader__.load({
544
554
  completed: "Hoàn tất",
545
555
  collapse: "Thu gọn",
546
556
  expand: "Mở rộng",
557
+ resizeRecent: "Kéo để điều chỉnh chiều cao vùng phiên gần đây",
547
558
  today: "Hôm nay",
548
559
  yesterday: "Hôm qua",
549
560
  date: "{d}/{m}",
@@ -598,6 +609,7 @@ window.__ModuleLoader__.load({
598
609
  completed: "เสร็จสิ้น",
599
610
  collapse: "ย่อ",
600
611
  expand: "ขยาย",
612
+ resizeRecent: "ลากเพื่อปรับความสูงของพื้นที่เซสชันล่าสุด",
601
613
  today: "วันนี้",
602
614
  yesterday: "เมื่อวาน",
603
615
  date: "{d}/{m}",
@@ -652,6 +664,7 @@ window.__ModuleLoader__.load({
652
664
  completed: "Завершён",
653
665
  collapse: "Свернуть",
654
666
  expand: "Развернуть",
667
+ resizeRecent: "Перетащите, чтобы изменить высоту области недавних сессий",
655
668
  today: "Сегодня",
656
669
  yesterday: "Вчера",
657
670
  date: "{d}.{m}",
@@ -706,6 +719,7 @@ window.__ModuleLoader__.load({
706
719
  completed: "Completata",
707
720
  collapse: "Comprimi",
708
721
  expand: "Espandi",
722
+ resizeRecent: "Trascina per ridimensionare l’area delle sessioni recenti",
709
723
  today: "Oggi",
710
724
  yesterday: "Ieri",
711
725
  date: "{d}/{m}",
@@ -760,6 +774,7 @@ window.__ModuleLoader__.load({
760
774
  completed: "Voltooid",
761
775
  collapse: "Inklappen",
762
776
  expand: "Uitklappen",
777
+ resizeRecent: "Sleep om de hoogte van het gebied met recente sessies aan te passen",
763
778
  today: "Vandaag",
764
779
  yesterday: "Gisteren",
765
780
  date: "{d}-{m}",
@@ -814,6 +829,7 @@ window.__ModuleLoader__.load({
814
829
  completed: "Slutförd",
815
830
  collapse: "Fäll ihop",
816
831
  expand: "Fäll ut",
832
+ resizeRecent: "Dra för att justera höjden på området för senaste sessioner",
817
833
  today: "Idag",
818
834
  yesterday: "Igår",
819
835
  date: "{d}/{m}",
@@ -868,6 +884,7 @@ window.__ModuleLoader__.load({
868
884
  completed: "Zakończono",
869
885
  collapse: "Zwiń",
870
886
  expand: "Rozwiń",
887
+ resizeRecent: "Przeciągnij, aby dostosować wysokość obszaru ostatnich sesji",
871
888
  today: "Dzisiaj",
872
889
  yesterday: "Wczoraj",
873
890
  date: "{d}.{m}",
@@ -922,6 +939,7 @@ window.__ModuleLoader__.load({
922
939
  completed: "已完成",
923
940
  collapse: "摺疊",
924
941
  expand: "展開",
942
+ resizeRecent: "拖動調整最近會話區域高度",
925
943
  today: "今天",
926
944
  yesterday: "昨天",
927
945
  date: "{m}月{d}日",
@@ -976,6 +994,7 @@ window.__ModuleLoader__.load({
976
994
  completed: "已完成",
977
995
  collapse: "摺疊",
978
996
  expand: "展開",
997
+ resizeRecent: "拖曳調整最近會話區域高度",
979
998
  today: "今天",
980
999
  yesterday: "昨天",
981
1000
  date: "{m}月{d}日",
@@ -1030,6 +1049,7 @@ window.__ModuleLoader__.load({
1030
1049
  completed: "已完成",
1031
1050
  collapse: "摺疊",
1032
1051
  expand: "展開",
1052
+ resizeRecent: "拖動調整最近會話區域高度",
1033
1053
  today: "今天",
1034
1054
  yesterday: "昨天",
1035
1055
  date: "{m}月{d}日",
@@ -1088,6 +1108,7 @@ window.__ModuleLoader__.load({
1088
1108
  completed: "已完成",
1089
1109
  collapse: "折叠",
1090
1110
  expand: "展开",
1111
+ resizeRecent: "拖动调整最近会话区域高度",
1091
1112
  today: "今天",
1092
1113
  yesterday: "昨天",
1093
1114
  date: "{m}月{d}日",
@@ -1142,6 +1163,7 @@ window.__ModuleLoader__.load({
1142
1163
  completed: "Completed",
1143
1164
  collapse: "Collapse",
1144
1165
  expand: "Expand",
1166
+ resizeRecent: "Drag to resize the recent sessions area",
1145
1167
  today: "Today",
1146
1168
  yesterday: "Yesterday",
1147
1169
  date: "{m}/{d}",
@@ -1149,6 +1171,156 @@ window.__ModuleLoader__.load({
1149
1171
  };
1150
1172
  const NS = "ya-workspace-sidebar";
1151
1173
  //#endregion
1174
+ //#region src/client/navigation.ts
1175
+ /**
1176
+ * Stand-in for the `uiWorkspace` navigation service owned by the official
1177
+ * ui-workspace client entry, which this bundle's patch disables.
1178
+ *
1179
+ * Since DSH v0.1.2-alpha.1 the ui-sidebar, ui-conversation, ui-agent-preset,
1180
+ * and the composed directory-picker client entries all inject `uiWorkspace`;
1181
+ * without a provider their fibers park forever and the whole WebUI boot
1182
+ * deadlocks. This port carries the semantics that moved here from the
1183
+ * pre-split client runtime: reuse-or-create Workspace connection, the
1184
+ * explicit/current/recent New Session fallback, archived-current clearing,
1185
+ * boot auto-selection, and the directory-picking wire calls.
1186
+ */
1187
+ /** Implements Workspace navigation and directory UI operations. */
1188
+ var YaWorkspaceNavigation = class extends _deepseek_ai_cordis.Service {
1189
+ directoryPicker;
1190
+ workspaces;
1191
+ sessions;
1192
+ connecting = /* @__PURE__ */ new Map();
1193
+ /**
1194
+ * @param ctx - Client root Context.
1195
+ * @param directoryPicker - the directory-picking Remote namespace.
1196
+ * @param workspaces - pure Workspace Controller.
1197
+ * @param sessions - pure Session Controller.
1198
+ */
1199
+ constructor(ctx, directoryPicker, workspaces, sessions) {
1200
+ super(ctx, "uiWorkspace");
1201
+ this.directoryPicker = directoryPicker;
1202
+ this.workspaces = workspaces;
1203
+ this.sessions = sessions;
1204
+ ctx.effect(() => this.watchNavigation(), "ya-workspace-sidebar: Workspace navigation policy");
1205
+ }
1206
+ async connectWorkspace(workspaceId) {
1207
+ const workspace = this.workspaces.list.getSnapshot().items.find((item) => item.workspaceId === workspaceId);
1208
+ if (workspace === void 0) throw new Error(`ya-workspace-sidebar: unknown workspace "${workspaceId}"`);
1209
+ const inflight = this.connecting.get(workspaceId);
1210
+ if (inflight !== void 0) return inflight;
1211
+ const archived = this.workspaces.list.getSnapshot().archivedSessionIds;
1212
+ const sessions = this.sessions.list.getSnapshot();
1213
+ for (const id of sessions.ids) {
1214
+ const summary = sessions.byId[id];
1215
+ if (summary !== void 0 && summary.blank && summary.cwd === workspace.path && workspace.sessionIds.includes(summary.id) && !archived.includes(summary.id)) return summary.id;
1216
+ }
1217
+ const attempt = this.sessions.create({ workspaceId }).finally(() => {
1218
+ this.connecting.delete(workspaceId);
1219
+ });
1220
+ this.connecting.set(workspaceId, attempt);
1221
+ return attempt;
1222
+ }
1223
+ startSession(workspaceId) {
1224
+ const workspaces = this.workspaces.list.getSnapshot();
1225
+ const sessions = this.sessions.list.getSnapshot();
1226
+ const current = sessions.current;
1227
+ const currentWorkspaceId = current === void 0 ? void 0 : workspaces.items.find((item) => item.sessionIds.includes(current))?.workspaceId;
1228
+ const recent = workspaces.phase === "ready" && sessions.phase === "ready" ? recentWorkspace(workspaces.items, sessions.byId) : void 0;
1229
+ const target = workspaceId ?? currentWorkspaceId ?? recent;
1230
+ if (target === void 0) {
1231
+ this.sessions.clear();
1232
+ return;
1233
+ }
1234
+ this.connectWorkspace(target).then((sessionId) => {
1235
+ this.sessions.open(sessionId);
1236
+ }, (reason) => {
1237
+ console.warn("ya-workspace-sidebar: new session failed:", reason);
1238
+ });
1239
+ }
1240
+ async archiveSession(sessionId) {
1241
+ await this.workspaces.archiveSession(sessionId);
1242
+ }
1243
+ async pickDirectory() {
1244
+ const result = await this.directoryPicker.pick();
1245
+ if (!result.ok) throw new Error(`directory picker failed: ${result.error.message}`);
1246
+ return result.value;
1247
+ }
1248
+ async listDirectory(path, signal) {
1249
+ const result = await this.directoryPicker.list(path, signal);
1250
+ if (!result.ok) throw new Error(`directory browse failed: ${result.error.code}: ${result.error.message}`);
1251
+ return result.value;
1252
+ }
1253
+ async createDirectory(path, name) {
1254
+ const result = await this.directoryPicker.createDirectory(path, name);
1255
+ if (!result.ok) throw new Error(`directory browse failed: ${result.error.code}: ${result.error.message}`);
1256
+ return result.value;
1257
+ }
1258
+ watchNavigation() {
1259
+ let initial = "waiting";
1260
+ let disposed = false;
1261
+ const reconcile = () => {
1262
+ if (disposed) return;
1263
+ if (this.clearArchivedCurrent()) return;
1264
+ if (initial !== "waiting") return;
1265
+ const workspaces = this.workspaces.list.getSnapshot();
1266
+ const sessions = this.sessions.list.getSnapshot();
1267
+ if (workspaces.phase !== "ready" || sessions.phase !== "ready") return;
1268
+ if (sessions.current !== void 0) {
1269
+ initial = "done";
1270
+ return;
1271
+ }
1272
+ const target = recentWorkspace(workspaces.items, sessions.byId);
1273
+ if (target === void 0) {
1274
+ initial = "done";
1275
+ return;
1276
+ }
1277
+ initial = "connecting";
1278
+ this.connectWorkspace(target).then((sessionId) => {
1279
+ if (disposed) return;
1280
+ if (this.sessions.list.getSnapshot().current === void 0) this.sessions.open(sessionId);
1281
+ initial = "done";
1282
+ }, (reason) => {
1283
+ if (disposed) return;
1284
+ initial = "waiting";
1285
+ console.warn("ya-workspace-sidebar: initial workspace selection failed:", reason);
1286
+ });
1287
+ };
1288
+ const disposeWorkspaces = this.workspaces.list.subscribe(reconcile);
1289
+ const disposeSessions = this.sessions.list.subscribe(reconcile);
1290
+ reconcile();
1291
+ return () => {
1292
+ disposed = true;
1293
+ disposeSessions();
1294
+ disposeWorkspaces();
1295
+ };
1296
+ }
1297
+ /** @returns true when an archived current selection was cleared. */
1298
+ clearArchivedCurrent() {
1299
+ const current = this.sessions.list.getSnapshot().current;
1300
+ if (current === void 0 || !this.workspaces.list.getSnapshot().archivedSessionIds.includes(current)) return false;
1301
+ this.sessions.clear();
1302
+ return true;
1303
+ }
1304
+ };
1305
+ /** Stable tie-breaking follows Host Workspace order. */
1306
+ function recentWorkspace(workspaces, sessions) {
1307
+ let selected;
1308
+ let selectedTime = Number.NEGATIVE_INFINITY;
1309
+ for (const workspace of workspaces) {
1310
+ let latest = Number.NEGATIVE_INFINITY;
1311
+ for (const sessionId of workspace.sessionIds) {
1312
+ const session = sessions[sessionId];
1313
+ if (session !== void 0) latest = Math.max(latest, session.updatedAt);
1314
+ }
1315
+ if (latest === Number.NEGATIVE_INFINITY) latest = Date.parse(workspace.createdAt);
1316
+ if (selected === void 0 || latest > selectedTime) {
1317
+ selected = workspace.workspaceId;
1318
+ selectedTime = latest;
1319
+ }
1320
+ }
1321
+ return selected;
1322
+ }
1323
+ //#endregion
1152
1324
  //#region src/client/styles.ts
1153
1325
  /** One scoped stylesheet injected for the lifetime of the client activation. */
1154
1326
  const CSS = `
@@ -1164,11 +1336,18 @@ body[data-ds-dark-theme] .ya-search { background:var(--dsw-static-neutral-bluish
1164
1336
  .ya-search-input::placeholder { color:var(--dsw-alias-label-tertiary); }
1165
1337
  .ya-search-icon { flex:none; display:inline-flex; border:0; padding:0; color:inherit; background:transparent; }
1166
1338
  .ya-body { flex:1; min-height:0; display:flex; flex-direction:column; overflow:hidden; margin-right:calc(-1 * var(--dsh-sidebar-inline-padding)); padding-right:var(--dsh-sidebar-inline-padding); }
1167
- .ya-recent { flex:none; padding-bottom:8px; border-bottom:1px solid var(--dsw-alias-border-l2); }
1168
- .ya-recent-collapsed { padding-bottom:0; border-bottom-color:transparent; }
1339
+ .ya-recent { flex:none; }
1169
1340
  .ya-recent-list-wrap { display:grid; grid-template-rows:1fr; transition:grid-template-rows 220ms ease-out; }
1170
1341
  .ya-recent-collapsed .ya-recent-list-wrap { grid-template-rows:0fr; }
1171
- .ya-recent-list { display:flex; flex-direction:column; overflow:hidden; min-height:0; }
1342
+ .ya-recent-list { position:relative; overflow-y:auto; overscroll-behavior:contain; min-height:0; max-height:min(280px, 40vh); }
1343
+ .ya-virtual-canvas { position:relative; width:100%; }
1344
+ .ya-virtual-slot { position:absolute; left:0; right:0; height:35px; box-sizing:border-box; padding:1px 0; display:flex; flex-direction:column; justify-content:center; }
1345
+ .ya-virtual-slot .ya-row { margin:0; min-height:33px; }
1346
+ .ya-recent-resizer { position:relative; flex:none; height:9px; margin:1px 0 3px; display:flex; align-items:center; justify-content:center; border-radius:5px; cursor:row-resize; touch-action:none; }
1347
+ .ya-recent-resizer::before { content:''; position:absolute; left:8px; right:8px; top:50%; height:1px; transform:translateY(-0.5px); background:var(--dsw-alias-border-l2); }
1348
+ .ya-recent-resizer::after { content:''; width:26px; height:3px; border-radius:2px; background:var(--dsw-alias-border-l2); transition:background 120ms ease-out; }
1349
+ .ya-recent-resizer:hover::after, .ya-recent-resizer:focus-visible::after, .ya-recent-resizer.ya-resizing::after { background:var(--dsw-alias-label-tertiary); }
1350
+ .ya-recent-resizer.ya-resizing::before { background:var(--dsw-alias-interactive-bg-selected); }
1172
1351
  .ya-block-label { height:26px; display:flex; align-items:center; gap:2px; padding:0 8px; color:var(--dsw-alias-label-tertiary); font-size:12px; font-weight:600; letter-spacing:.02em; text-transform:uppercase; }
1173
1352
  .ya-block-label-toggle { flex:none; width:20px; height:20px; margin-left:auto; border:0; border-radius:6px; padding:0; display:inline-flex; align-items:center; justify-content:center; background:transparent; color:var(--dsw-alias-label-tertiary); cursor:pointer; transition:transform 180ms ease-out; }
1174
1353
  .ya-block-label-toggle:hover { background:var(--dsw-alias-interactive-bg-hover); color:var(--dsw-alias-label-secondary); }
@@ -1191,7 +1370,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1191
1370
  .ya-row:hover .ya-row-actions, .ya-menu-open .ya-row-actions { opacity:1; pointer-events:auto; }
1192
1371
  .ya-status-slot { flex:none; width:16px; height:16px; display:inline-flex; align-items:center; justify-content:center; color:var(--dsw-alias-label-tertiary); }
1193
1372
  .ya-recent .ya-row { min-height:31px; }
1194
- .ya-search-workspace { color:var(--dsw-alias-label-tertiary); font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1373
+ .ya-search-workspace { color:var(--dsw-alias-label-tertiary); font-size:11px; line-height:15px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1195
1374
  .ya-empty, .ya-status { padding:18px 10px; color:var(--dsw-alias-label-tertiary); text-align:center; font-size:13px; }
1196
1375
  .ya-warning { color:var(--dsw-alias-status-warning); }
1197
1376
  .ya-rename-input { width:100%; height:38px; box-sizing:border-box; border:1px solid var(--dsw-alias-border-l2); border-radius:9px; padding:0 11px; background:transparent; color:var(--dsw-alias-label-primary); outline:none; }
@@ -1349,18 +1528,24 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1349
1528
  }
1350
1529
  //#endregion
1351
1530
  //#region src/client/model.ts
1531
+ /** Resolve the visible status-dot kind for one session, if any. */
1532
+ function pendingKindOf(pending, id) {
1533
+ const kind = pending.get(id)?.kind;
1534
+ return kind === "approval" || kind === "plan-review" || kind === "question" ? kind : void 0;
1535
+ }
1352
1536
  /** Navigation key for sessions not accounted to a real workspace. */
1353
1537
  const UNGROUPED = "__ya_ungrouped__";
1354
1538
  function visible(summary, current, archived) {
1355
1539
  return summary.origin !== "subagent" && !archived.has(summary.id) && (!summary.blank || summary.id === current);
1356
1540
  }
1357
- function rowOf(summary, workspaceKey, workspaceTitle) {
1541
+ function rowOf(summary, workspaceKey, workspaceTitle, pending) {
1542
+ const pendingInteraction = pendingKindOf(pending, summary.id);
1358
1543
  return {
1359
1544
  id: summary.id,
1360
1545
  title: summary.blank ? "New Session" : summary.displayTitle,
1361
1546
  blank: summary.blank,
1362
1547
  running: summary.running,
1363
- ...summary.pendingInteraction === void 0 ? {} : { pendingInteraction: summary.pendingInteraction },
1548
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction },
1364
1549
  completed: summary.completed === true,
1365
1550
  updatedAt: summary.updatedAt,
1366
1551
  workspaceKey,
@@ -1378,57 +1563,100 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1378
1563
  return ownerIndex(workspaces).get(sessionId)?.workspaceId ?? "__ya_ungrouped__";
1379
1564
  }
1380
1565
  /** Derive global recent sessions, newest first. */
1381
- function deriveRecent(list, workspaces, archivedSessionIds, limit = 5) {
1566
+ function deriveRecent(list, workspaces, archivedSessionIds, pending, limit = 5) {
1382
1567
  const archived = new Set(archivedSessionIds);
1383
1568
  const owners = ownerIndex(workspaces);
1384
1569
  return list.ids.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && visible(summary, list.current, archived)).sort((a, b) => b.updatedAt - a.updatedAt || String(a.id).localeCompare(String(b.id))).slice(0, limit).map((summary) => {
1385
1570
  const workspace = owners.get(summary.id);
1386
- return rowOf(summary, workspace?.workspaceId ?? "__ya_ungrouped__", workspace?.title ?? "Ungrouped");
1571
+ return rowOf(summary, workspace?.workspaceId ?? "__ya_ungrouped__", workspace?.title ?? "Ungrouped", pending);
1387
1572
  });
1388
1573
  }
1389
- /** Derive first-level real workspaces plus the virtual Ungrouped row. */
1574
+ /**
1575
+ * Windowing math for the recent-sessions virtual list (fixed row stride).
1576
+ *
1577
+ * Clamps `scrollTop` into the valid range, then pads the visible row range
1578
+ * with `overscan` rows on both edges. Before the viewport has been measured
1579
+ * (`viewportHeight <= 0`) it still returns a small head window so the first
1580
+ * paint is never blank.
1581
+ */
1582
+ function virtualWindow(scrollTop, viewportHeight, count, stride = 35, overscan = 4) {
1583
+ if (count <= 0 || stride <= 0) return {
1584
+ start: 0,
1585
+ end: 0
1586
+ };
1587
+ const height = Math.max(0, viewportHeight);
1588
+ const top = Math.min(Math.max(0, scrollTop), Math.max(0, count * stride - height));
1589
+ const first = Math.floor(top / stride);
1590
+ const last = height > 0 ? Math.floor((top + height) / stride) : first;
1591
+ return {
1592
+ start: Math.max(0, first - overscan),
1593
+ end: Math.min(count, last + 1 + overscan)
1594
+ };
1595
+ }
1596
+ function lastUsedOf(timestamps, fallback) {
1597
+ let newest;
1598
+ for (const ts of timestamps) if (newest === void 0 || ts > newest) newest = ts;
1599
+ if (newest !== void 0) return newest;
1600
+ if (fallback === void 0) return void 0;
1601
+ const created = Date.parse(fallback);
1602
+ return Number.isNaN(created) ? void 0 : created;
1603
+ }
1604
+ /** Derive first-level workspaces plus Ungrouped, newest session activity first. */
1390
1605
  function deriveWorkspaces(list, workspaces, archivedSessionIds) {
1391
1606
  const archived = new Set(archivedSessionIds);
1392
1607
  const accounted = /* @__PURE__ */ new Set();
1393
1608
  const result = workspaces.map((workspace) => {
1394
1609
  let count = 0;
1610
+ const timestamps = [];
1395
1611
  for (const id of workspace.sessionIds) {
1396
1612
  accounted.add(id);
1397
1613
  const summary = list.byId[id];
1398
- if (summary !== void 0 && visible(summary, list.current, archived)) count++;
1614
+ if (summary !== void 0 && visible(summary, list.current, archived)) {
1615
+ count++;
1616
+ timestamps.push(summary.updatedAt);
1617
+ }
1399
1618
  }
1619
+ const lastUsedAt = lastUsedOf(timestamps, workspace.createdAt);
1400
1620
  return {
1401
1621
  key: workspace.workspaceId,
1402
1622
  title: workspace.title,
1403
1623
  path: workspace.path,
1404
1624
  createdAt: workspace.createdAt,
1625
+ ...lastUsedAt === void 0 ? {} : { lastUsedAt },
1405
1626
  count,
1406
1627
  real: true
1407
1628
  };
1408
1629
  });
1409
1630
  let ungrouped = 0;
1631
+ const ungroupedTimestamps = [];
1410
1632
  for (const id of list.ids) {
1411
1633
  const summary = list.byId[id];
1412
- if (summary !== void 0 && !accounted.has(id) && visible(summary, list.current, archived)) ungrouped++;
1634
+ if (summary !== void 0 && !accounted.has(id) && visible(summary, list.current, archived)) {
1635
+ ungrouped++;
1636
+ ungroupedTimestamps.push(summary.updatedAt);
1637
+ }
1413
1638
  }
1639
+ const ungroupedLastUsed = lastUsedOf(ungroupedTimestamps);
1414
1640
  result.push({
1415
1641
  key: UNGROUPED,
1416
1642
  title: "Ungrouped",
1417
1643
  count: ungrouped,
1418
- real: false
1644
+ real: false,
1645
+ ...ungroupedLastUsed === void 0 ? {} : { lastUsedAt: ungroupedLastUsed }
1419
1646
  });
1647
+ result.sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0) || String(a.key).localeCompare(String(b.key)));
1420
1648
  return result;
1421
1649
  }
1422
1650
  /** Derive the selected workspace's sessions in its canonical order. */
1423
- function deriveWorkspaceSessions(key, list, workspaces, archivedSessionIds) {
1651
+ function deriveWorkspaceSessions(key, list, workspaces, archivedSessionIds, pending) {
1424
1652
  const archived = new Set(archivedSessionIds);
1425
1653
  if (key === "__ya_ungrouped__") {
1426
1654
  const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
1427
- return list.ids.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && !accounted.has(summary.id) && visible(summary, list.current, archived)).sort((a, b) => b.updatedAt - a.updatedAt || String(a.id).localeCompare(String(b.id))).map((summary) => rowOf(summary, UNGROUPED, "Ungrouped"));
1655
+ return list.ids.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && !accounted.has(summary.id) && visible(summary, list.current, archived)).sort((a, b) => b.updatedAt - a.updatedAt || String(a.id).localeCompare(String(b.id))).map((summary) => rowOf(summary, UNGROUPED, "Ungrouped", pending));
1428
1656
  }
1429
1657
  const workspace = workspaces.find((item) => item.workspaceId === key);
1430
1658
  if (workspace === void 0) return [];
1431
- return workspace.sessionIds.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && visible(summary, list.current, archived)).map((summary) => rowOf(summary, workspace.workspaceId, workspace.title));
1659
+ return workspace.sessionIds.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && visible(summary, list.current, archived)).map((summary) => rowOf(summary, workspace.workspaceId, workspace.title, pending));
1432
1660
  }
1433
1661
  /** Format a local calendar date as `YYYY-MM-DD` (locale-agnostic, no padding surprises). */
1434
1662
  function localDateKey(year, month, day) {
@@ -1440,21 +1668,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1440
1668
  const msB = Date.UTC(b.year, b.month, b.day);
1441
1669
  return Math.round((msA - msB) / 864e5);
1442
1670
  }
1443
- /**
1444
- * Derive the selected real workspace's sessions grouped by local calendar date.
1445
- *
1446
- * - Only real workspaces: `Ungrouped` falls back to {@link deriveWorkspaceSessions}.
1447
- * - Groups are ordered by date descending; rows within a group by `updatedAt` descending.
1448
- * - {@link visible} filter is reused (archived / subagent / blank visibility).
1449
- * - Future timestamps clamp to today's bucket (`dayOffset` 0).
1450
- * - `now` is the reference timestamp for "today"; pass `Date.now()` in production.
1451
- */
1452
- function deriveWorkspaceSessionGroups(key, list, workspaces, archivedSessionIds, now) {
1453
- if (key === "__ya_ungrouped__") return [];
1454
- const workspace = workspaces.find((item) => item.workspaceId === key);
1455
- if (workspace === void 0) return [];
1456
- const archived = new Set(archivedSessionIds);
1457
- const rows = workspace.sessionIds.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && visible(summary, list.current, archived)).map((summary) => rowOf(summary, workspace.workspaceId, workspace.title));
1671
+ function groupByLocalDate(rows, timestampOf, compare, now) {
1458
1672
  if (rows.length === 0) return [];
1459
1673
  const nowDate = new Date(now);
1460
1674
  const today = {
@@ -1464,7 +1678,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1464
1678
  };
1465
1679
  const buckets = /* @__PURE__ */ new Map();
1466
1680
  for (const row of rows) {
1467
- const ts = Math.min(row.updatedAt, now);
1681
+ const ts = Math.min(timestampOf(row), now);
1468
1682
  const d = new Date(ts);
1469
1683
  const date = {
1470
1684
  year: d.getFullYear(),
@@ -1484,7 +1698,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1484
1698
  }
1485
1699
  const groups = [];
1486
1700
  for (const [dateKey, bucket] of buckets) {
1487
- bucket.rows.sort((a, b) => b.updatedAt - a.updatedAt || String(a.id).localeCompare(String(b.id)));
1701
+ bucket.rows.sort(compare);
1488
1702
  groups.push({
1489
1703
  dateKey,
1490
1704
  dayOffset: bucket.dayOffset,
@@ -1494,6 +1708,36 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1494
1708
  groups.sort((a, b) => a.dayOffset - b.dayOffset || a.dateKey.localeCompare(b.dateKey));
1495
1709
  return groups;
1496
1710
  }
1711
+ /**
1712
+ * Derive the selected real workspace's sessions grouped by local calendar date.
1713
+ *
1714
+ * - Only real workspaces: `Ungrouped` falls back to {@link deriveWorkspaceSessions}.
1715
+ * - Groups are ordered by date descending; rows within a group by `updatedAt` descending.
1716
+ * - {@link visible} filter is reused (archived / subagent / blank visibility).
1717
+ * - Future timestamps clamp to today's bucket (`dayOffset` 0).
1718
+ * - `now` is the reference timestamp for "today"; pass `Date.now()` in production.
1719
+ */
1720
+ function deriveWorkspaceSessionGroups(key, list, workspaces, archivedSessionIds, pending, now) {
1721
+ if (key === "__ya_ungrouped__") return [];
1722
+ const workspace = workspaces.find((item) => item.workspaceId === key);
1723
+ if (workspace === void 0) return [];
1724
+ const archived = new Set(archivedSessionIds);
1725
+ return groupByLocalDate(workspace.sessionIds.map((id) => list.byId[id]).filter((summary) => summary !== void 0 && visible(summary, list.current, archived)).map((summary) => rowOf(summary, workspace.workspaceId, workspace.title, pending)), (row) => row.updatedAt, (a, b) => b.updatedAt - a.updatedAt || String(a.id).localeCompare(String(b.id)), now);
1726
+ }
1727
+ /** Group root workspace rows by local calendar date of `lastUsedAt`; undated rows trail with empty `dateKey`. */
1728
+ function deriveWorkspaceGroups(rows, now) {
1729
+ const dated = [];
1730
+ const undated = [];
1731
+ for (const row of rows) if (row.lastUsedAt === void 0) undated.push(row);
1732
+ else dated.push(row);
1733
+ const groups = groupByLocalDate(dated, (row) => row.lastUsedAt ?? 0, (a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0) || String(a.key).localeCompare(String(b.key)), now);
1734
+ if (undated.length > 0) groups.push({
1735
+ dateKey: "",
1736
+ dayOffset: Number.POSITIVE_INFINITY,
1737
+ rows: [...undated]
1738
+ });
1739
+ return groups;
1740
+ }
1497
1741
  /** Case-insensitive local title/workspace matching used beside Host content search. */
1498
1742
  function localMatches(rows, query) {
1499
1743
  const normalized = query.trim().toLocaleLowerCase();
@@ -1537,11 +1781,121 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1537
1781
  listeners.delete(listener);
1538
1782
  };
1539
1783
  }
1784
+ const RECENT_HEIGHT_KEY = "ya-workspace-sidebar:recent-height";
1785
+ const recentHeightListeners = /* @__PURE__ */ new Set();
1786
+ let currentRecentHeight = loadRecentHeight();
1787
+ function clampRecentHeight(height) {
1788
+ return Math.min(640, Math.max(70, Math.round(height)));
1789
+ }
1790
+ /** Read the stored height, falling back to `undefined` on any failure. */
1791
+ function loadRecentHeight() {
1792
+ try {
1793
+ const raw = window.localStorage.getItem(RECENT_HEIGHT_KEY);
1794
+ if (raw === null) return void 0;
1795
+ const parsed = Number(raw);
1796
+ return Number.isFinite(parsed) ? clampRecentHeight(parsed) : void 0;
1797
+ } catch {
1798
+ return;
1799
+ }
1800
+ }
1801
+ /** Persist the height (or clear the entry for `undefined`); failures ignored. */
1802
+ function persistRecentHeight(height) {
1803
+ try {
1804
+ if (height === void 0) window.localStorage.removeItem(RECENT_HEIGHT_KEY);
1805
+ else window.localStorage.setItem(RECENT_HEIGHT_KEY, String(height));
1806
+ } catch {}
1807
+ }
1808
+ /** Current recent-sessions viewport height preference, if the user set one. */
1809
+ function getRecentViewportHeight() {
1810
+ return currentRecentHeight;
1811
+ }
1812
+ /** Store (or clear with `undefined`) the height preference and notify subscribers. */
1813
+ function setRecentViewportHeight(height) {
1814
+ const next = height === void 0 ? void 0 : clampRecentHeight(height);
1815
+ if (next === currentRecentHeight) return;
1816
+ currentRecentHeight = next;
1817
+ persistRecentHeight(next);
1818
+ for (const listener of [...recentHeightListeners]) listener();
1819
+ }
1820
+ /** Subscribe to recent height changes; returns an unsubscribe disposer. */
1821
+ function subscribeRecentViewportHeight(listener) {
1822
+ recentHeightListeners.add(listener);
1823
+ return () => {
1824
+ recentHeightListeners.delete(listener);
1825
+ };
1826
+ }
1827
+ //#endregion
1828
+ //#region src/client/VirtualRecentList.tsx
1829
+ /** Fixed-stride virtual scroller for the global recent-sessions block. */
1830
+ /**
1831
+ * Renders `rows` inside a bounded, scrollable viewport while mounting only
1832
+ * the visible slice (plus overscan). The row stride is fixed
1833
+ * ({@link RECENT_ROW_STRIDE}), so windowing is pure arithmetic — no per-row
1834
+ * measurement — and the scroll position survives session-list updates.
1835
+ */
1836
+ function VirtualRecentList({ rows, renderItem, ariaLabel, viewportRef, maxHeight }) {
1837
+ const viewportRefInternal = (0, react.useRef)(null);
1838
+ const frameRef = (0, react.useRef)(0);
1839
+ const [scrollTop, setScrollTop] = (0, react.useState)(0);
1840
+ const [viewportHeight, setViewportHeight] = (0, react.useState)(0);
1841
+ (0, react.useLayoutEffect)(() => {
1842
+ const el = viewportRefInternal.current;
1843
+ if (el === null) return;
1844
+ const observer = new ResizeObserver((entries) => {
1845
+ const entry = entries[0];
1846
+ if (entry !== void 0) setViewportHeight(entry.contentRect.height);
1847
+ });
1848
+ observer.observe(el);
1849
+ return () => {
1850
+ observer.disconnect();
1851
+ };
1852
+ }, []);
1853
+ (0, react.useEffect)(() => () => {
1854
+ if (frameRef.current !== 0) window.cancelAnimationFrame(frameRef.current);
1855
+ }, []);
1856
+ const handleScroll = () => {
1857
+ if (frameRef.current !== 0) return;
1858
+ frameRef.current = window.requestAnimationFrame(() => {
1859
+ frameRef.current = 0;
1860
+ const el = viewportRefInternal.current;
1861
+ if (el !== null) setScrollTop(el.scrollTop);
1862
+ });
1863
+ };
1864
+ const { start, end } = virtualWindow(scrollTop, viewportHeight, rows.length);
1865
+ const slice = [];
1866
+ for (let index = start; index < end; index += 1) {
1867
+ const row = rows[index];
1868
+ if (row !== void 0) slice.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1869
+ className: "ya-virtual-slot",
1870
+ style: { top: index * 35 },
1871
+ children: renderItem(row)
1872
+ }, row.id));
1873
+ }
1874
+ const attachViewport = (node) => {
1875
+ viewportRefInternal.current = node;
1876
+ viewportRef?.(node);
1877
+ };
1878
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1879
+ ref: attachViewport,
1880
+ className: "ya-recent-list ya-virtual-viewport",
1881
+ role: "tree",
1882
+ "aria-label": ariaLabel,
1883
+ style: maxHeight === void 0 ? void 0 : { maxHeight: `${maxHeight}px` },
1884
+ onScroll: handleScroll,
1885
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1886
+ className: "ya-virtual-canvas",
1887
+ style: { height: rows.length * 35 },
1888
+ children: slice
1889
+ })
1890
+ });
1891
+ }
1540
1892
  //#endregion
1541
1893
  //#region src/client/WorkspaceSidebar.tsx
1542
1894
  /** Two-level workspace/session browser with a persistent global recent block. */
1543
1895
  const SEARCH_DEBOUNCE_MS = 250;
1544
1896
  const SEARCH_MAX = 500;
1897
+ /** Minimum height kept for the workspace browser below the recent block. */
1898
+ const MIN_WORKSPACE = 120;
1545
1899
  function sanitized(value) {
1546
1900
  return value.replaceAll("\0", "").slice(0, SEARCH_MAX);
1547
1901
  }
@@ -1661,7 +2015,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1661
2015
  ]
1662
2016
  });
1663
2017
  }
1664
- function WorkspaceItem({ row, enter, create, rename, remove, t }) {
2018
+ function WorkspaceItem({ row, enter, create, rename, remove, now, t }) {
1665
2019
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1666
2020
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1667
2021
  className: `ya-row ya-workspace-row${menuOpen ? " ya-menu-open" : ""}`,
@@ -1680,9 +2034,9 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1680
2034
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1681
2035
  className: "ya-row-title",
1682
2036
  children: row.real ? row.title : t("ungrouped")
1683
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1684
- className: "ya-row-meta",
1685
- children: t("count", { n: row.count })
2037
+ }), row.lastUsedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2038
+ className: "ya-row-meta ya-row-time",
2039
+ children: relativeTime(row.lastUsedAt, now, t)
1686
2040
  })]
1687
2041
  }), row.path !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1688
2042
  className: "ya-workspace-path",
@@ -1738,23 +2092,27 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1738
2092
  }
1739
2093
  /** Fill `sidebar.workspaces` with the replacement browser. */
1740
2094
  function WorkspaceSidebar(props) {
1741
- const { wide, expandSidebar, useSessions, useWorkspaces, startSession, open, searchSessions, searchResultLimit, renameSession, forkSession, renameWorkspace, deleteWorkspace, archiveSession, createWorkspace, useDirectoryFlow, renderSlot, t } = props;
2095
+ const { wide, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, startSession, open, searchSessions, searchResultLimit, renameSession, forkSession, renameWorkspace, deleteWorkspace, archiveSession, createWorkspace, useDirectoryFlow, renderSlot, t } = props;
1742
2096
  const sessions = useSessions((state) => state);
1743
2097
  const workspaceState = useWorkspaces((state) => state);
1744
2098
  const workspaces = workspaceState.items;
1745
2099
  const archived = workspaceState.archivedSessionIds;
2100
+ const pendingInteractions = useSessionPendingInteraction((state) => state);
1746
2101
  const directoryFlowAvailable = useDirectoryFlow((value) => value);
1747
- const allRows = (0, react.useMemo)(() => deriveRecent(sessions, workspaces, archived, Number.MAX_SAFE_INTEGER), [
2102
+ const allRows = (0, react.useMemo)(() => deriveRecent(sessions, workspaces, archived, pendingInteractions, Number.MAX_SAFE_INTEGER), [
1748
2103
  archived,
2104
+ pendingInteractions,
1749
2105
  sessions,
1750
2106
  workspaces
1751
2107
  ]);
1752
- const recent = allRows.slice(0, 5);
2108
+ const recent = allRows;
1753
2109
  const workspaceRows = (0, react.useMemo)(() => deriveWorkspaces(sessions, workspaces, archived), [
1754
2110
  archived,
1755
2111
  sessions,
1756
2112
  workspaces
1757
2113
  ]);
2114
+ const now = Date.now();
2115
+ const workspaceGroups = (0, react.useMemo)(() => deriveWorkspaceGroups(workspaceRows, now), [workspaceRows, now]);
1758
2116
  const [selectedKey, setSelectedKey] = (0, react.useState)(null);
1759
2117
  const [direction, setDirection] = (0, react.useState)("forward");
1760
2118
  const [hasMounted, setHasMounted] = (0, react.useState)(false);
@@ -1776,15 +2134,15 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1776
2134
  if (selectedKey !== null && selectedKey !== "__ya_ungrouped__" && !workspaces.some((workspace) => workspace.workspaceId === selectedKey)) setSelectedKey(UNGROUPED);
1777
2135
  }, [selectedKey, workspaces]);
1778
2136
  const selectedWorkspace = selectedKey === null || selectedKey === "__ya_ungrouped__" ? void 0 : workspaces.find((workspace) => workspace.workspaceId === selectedKey);
1779
- const now = Date.now();
1780
- const levelGroups = (0, react.useMemo)(() => selectedKey !== null && selectedKey !== "__ya_ungrouped__" ? deriveWorkspaceSessionGroups(selectedKey, sessions, workspaces, archived, now) : [], [
2137
+ const levelGroups = (0, react.useMemo)(() => selectedKey !== null && selectedKey !== "__ya_ungrouped__" ? deriveWorkspaceSessionGroups(selectedKey, sessions, workspaces, archived, pendingInteractions, now) : [], [
1781
2138
  archived,
2139
+ pendingInteractions,
1782
2140
  sessions,
1783
2141
  workspaces,
1784
2142
  selectedKey,
1785
2143
  now
1786
2144
  ]);
1787
- const levelRows = selectedKey === "__ya_ungrouped__" ? deriveWorkspaceSessions(UNGROUPED, sessions, workspaces, archived) : [];
2145
+ const levelRows = selectedKey === "__ya_ungrouped__" ? deriveWorkspaceSessions(UNGROUPED, sessions, workspaces, archived, pendingInteractions) : [];
1788
2146
  const levelEmpty = selectedKey === "__ya_ungrouped__" ? levelRows.length === 0 : levelGroups.every((g) => g.rows.length === 0);
1789
2147
  const [query, setQuery] = (0, react.useState)("");
1790
2148
  const normalizedQuery = sanitized(query).trim();
@@ -1859,6 +2217,92 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
1859
2217
  const [actionMode, setActionModeState] = (0, react.useState)(() => getActionMode());
1860
2218
  const [sessionDeleteTarget, setSessionDeleteTarget] = (0, react.useState)(null);
1861
2219
  (0, react.useEffect)(() => subscribeActionMode(() => setActionModeState(getActionMode())), []);
2220
+ const [recentHeight, setRecentHeight] = (0, react.useState)(() => getRecentViewportHeight());
2221
+ const [recentResizing, setRecentResizing] = (0, react.useState)(false);
2222
+ (0, react.useEffect)(() => subscribeRecentViewportHeight(() => {
2223
+ setRecentHeight(getRecentViewportHeight());
2224
+ }), []);
2225
+ const bodyRef = (0, react.useRef)(null);
2226
+ const recentBlockRef = (0, react.useRef)(null);
2227
+ const breadcrumbRef = (0, react.useRef)(null);
2228
+ const resizerRef = (0, react.useRef)(null);
2229
+ const recentViewportRef = (0, react.useRef)(null);
2230
+ const recentDrag = (0, react.useRef)(null);
2231
+ /** Effective max-height base and the largest height that keeps MIN_WORKSPACE visible. */
2232
+ const measureRecentBounds = () => {
2233
+ const viewport = recentViewportRef.current;
2234
+ const body = bodyRef.current;
2235
+ const resizer = resizerRef.current;
2236
+ if (viewport === null || body === null || resizer === null) return null;
2237
+ const computed = Number.parseFloat(window.getComputedStyle(viewport).maxHeight);
2238
+ const base = Number.isFinite(computed) ? computed : viewport.clientHeight;
2239
+ const breadcrumb = breadcrumbRef.current;
2240
+ const block = recentBlockRef.current;
2241
+ const chrome = (block !== null ? block.offsetHeight : 0) - viewport.clientHeight + (breadcrumb !== null ? breadcrumb.offsetHeight : 0) + resizer.offsetHeight;
2242
+ return {
2243
+ base,
2244
+ max: Math.max(70, Math.round(body.clientHeight - chrome - MIN_WORKSPACE))
2245
+ };
2246
+ };
2247
+ const beginRecentResize = (event) => {
2248
+ if (event.button !== 0 || !event.isPrimary) return;
2249
+ const bounds = measureRecentBounds();
2250
+ if (bounds === null) return;
2251
+ event.preventDefault();
2252
+ recentDrag.current = {
2253
+ pointerId: event.pointerId,
2254
+ startY: event.clientY,
2255
+ startHeight: bounds.base,
2256
+ max: bounds.max,
2257
+ moved: false
2258
+ };
2259
+ resizerRef.current?.setPointerCapture(event.pointerId);
2260
+ document.body.style.cursor = "row-resize";
2261
+ document.body.style.userSelect = "none";
2262
+ setRecentResizing(true);
2263
+ };
2264
+ const moveRecentResize = (event) => {
2265
+ const drag = recentDrag.current;
2266
+ const viewport = recentViewportRef.current;
2267
+ if (drag === null || viewport === null || event.pointerId !== drag.pointerId) return;
2268
+ drag.moved = true;
2269
+ const next = Math.round(Math.min(drag.max, Math.max(70, drag.startHeight + event.clientY - drag.startY)));
2270
+ viewport.style.maxHeight = `${next}px`;
2271
+ };
2272
+ const endRecentResize = (event) => {
2273
+ const drag = recentDrag.current;
2274
+ if (drag === null) return;
2275
+ recentDrag.current = null;
2276
+ const resizer = resizerRef.current;
2277
+ if (resizer !== null && resizer.hasPointerCapture(event.pointerId)) resizer.releasePointerCapture(event.pointerId);
2278
+ document.body.style.cursor = "";
2279
+ document.body.style.userSelect = "";
2280
+ setRecentResizing(false);
2281
+ if (!drag.moved) return;
2282
+ const viewport = recentViewportRef.current;
2283
+ if (viewport === null) return;
2284
+ const written = Number.parseFloat(viewport.style.maxHeight);
2285
+ if (!Number.isFinite(written)) return;
2286
+ const height = Math.round(written);
2287
+ setRecentHeight(height);
2288
+ setRecentViewportHeight(height);
2289
+ };
2290
+ const keyRecentResize = (event) => {
2291
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
2292
+ event.preventDefault();
2293
+ const bounds = measureRecentBounds();
2294
+ if (bounds === null) return;
2295
+ const delta = event.key === "ArrowUp" ? -35 : 35;
2296
+ const next = Math.round(Math.min(bounds.max, Math.max(70, (recentHeight ?? bounds.base) + delta)));
2297
+ setRecentHeight(next);
2298
+ setRecentViewportHeight(next);
2299
+ };
2300
+ const resetRecentHeight = () => {
2301
+ const viewport = recentViewportRef.current;
2302
+ if (viewport !== null) viewport.style.maxHeight = "";
2303
+ setRecentHeight(void 0);
2304
+ setRecentViewportHeight(void 0);
2305
+ };
1862
2306
  const beginWorkspaceRename = (row) => {
1863
2307
  setWorkspaceRename(row);
1864
2308
  setRenameDraft(row.title);
@@ -2035,6 +2479,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2035
2479
  ]
2036
2480
  }),
2037
2481
  wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2482
+ ref: bodyRef,
2038
2483
  className: "ya-body",
2039
2484
  children: normalizedQuery !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2040
2485
  className: "ya-scroll",
@@ -2057,6 +2502,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2057
2502
  ]
2058
2503
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2059
2504
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2505
+ ref: recentBlockRef,
2060
2506
  className: `ya-recent${recentCollapsed ? " ya-recent-collapsed" : ""}`,
2061
2507
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2062
2508
  className: "ya-block-label",
@@ -2076,13 +2522,34 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2076
2522
  children: recent.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2077
2523
  className: "ya-empty",
2078
2524
  children: t("noSessions")
2079
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2080
- className: "ya-recent-list",
2081
- children: recent.map((row) => sessionItem(row, true))
2525
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VirtualRecentList, {
2526
+ rows: recent,
2527
+ ariaLabel: t("recent"),
2528
+ renderItem: (row) => sessionItem(row, true),
2529
+ viewportRef: (node) => {
2530
+ recentViewportRef.current = node;
2531
+ },
2532
+ maxHeight: recentHeight
2082
2533
  })
2083
2534
  })]
2084
2535
  }),
2536
+ !recentCollapsed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2537
+ ref: resizerRef,
2538
+ className: `ya-recent-resizer${recentResizing ? " ya-resizing" : ""}`,
2539
+ role: "separator",
2540
+ "aria-orientation": "horizontal",
2541
+ "aria-label": t("resizeRecent"),
2542
+ "aria-valuenow": recentHeight,
2543
+ tabIndex: 0,
2544
+ onPointerDown: beginRecentResize,
2545
+ onPointerMove: moveRecentResize,
2546
+ onPointerUp: endRecentResize,
2547
+ onPointerCancel: endRecentResize,
2548
+ onDoubleClick: resetRecentHeight,
2549
+ onKeyDown: keyRecentResize
2550
+ }),
2085
2551
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2552
+ ref: breadcrumbRef,
2086
2553
  className: "ya-breadcrumb",
2087
2554
  children: selectedKey === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2088
2555
  className: "ya-crumb",
@@ -2120,7 +2587,11 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2120
2587
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2121
2588
  className: hasMounted ? `ya-level-enter-${direction}` : void 0,
2122
2589
  children: [
2123
- selectedKey === null ? workspaceRows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceItem, {
2590
+ selectedKey === null ? workspaceGroups.flatMap((group) => [...group.dateKey === "" ? [] : [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2591
+ className: "ya-date-group-label",
2592
+ role: "separator",
2593
+ children: dateGroupLabel(group, now, t)
2594
+ }, `ws-group-${group.dateKey}`)], ...group.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceItem, {
2124
2595
  row,
2125
2596
  enter: () => {
2126
2597
  setDirection("forward");
@@ -2136,8 +2607,9 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2136
2607
  setDeleteTarget(row);
2137
2608
  setRenameError(null);
2138
2609
  },
2610
+ now,
2139
2611
  t
2140
- }, row.key)) : selectedKey === "__ya_ungrouped__" ? levelRows.map((row) => sessionItem(row, false)) : levelGroups.flatMap((group) => [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2612
+ }, row.key))]) : selectedKey === "__ya_ungrouped__" ? levelRows.map((row) => sessionItem(row, false)) : levelGroups.flatMap((group) => [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2141
2613
  className: "ya-date-group-label",
2142
2614
  role: "separator",
2143
2615
  children: dateGroupLabel(group, now, t)
@@ -2246,12 +2718,17 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2246
2718
  }
2247
2719
  //#endregion
2248
2720
  //#region src/client/index.ts
2249
- /** Services required by both replacement client entries. */
2721
+ /**
2722
+ * Services required by the replacement client entries. `remote` and
2723
+ * `remote.directoryPicker` feed the uiWorkspace stand-in's directory picking.
2724
+ */
2250
2725
  const inject = [
2251
2726
  "slots",
2252
2727
  "sessions",
2253
2728
  "workspaces",
2254
- "locale"
2729
+ "locale",
2730
+ "remote",
2731
+ "remote.directoryPicker"
2255
2732
  ];
2256
2733
  /** Register the sidebar browser and conversation hero picker. */
2257
2734
  function apply(ctx) {
@@ -2259,8 +2736,21 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2259
2736
  zh,
2260
2737
  en
2261
2738
  }), "ya-workspace-sidebar: dictionaries");
2262
- const betterLocale = ctx.get("betterLocale");
2263
- if (betterLocale) ctx.effect(() => betterLocale.register(NS, dicts), "ya-workspace-sidebar: better-locale override dicts");
2739
+ ctx.effect(() => {
2740
+ let dispose;
2741
+ const sync = () => {
2742
+ dispose?.();
2743
+ dispose = void 0;
2744
+ const store = ctx.get("betterLocale");
2745
+ if (store !== void 0) dispose = store.register(NS, dicts);
2746
+ };
2747
+ sync();
2748
+ const unsubscribe = ctx.locale.subscribe(sync);
2749
+ return () => {
2750
+ unsubscribe();
2751
+ dispose?.();
2752
+ };
2753
+ }, "ya-workspace-sidebar: better-locale override dicts");
2264
2754
  ctx.effect(installStyles, "ya-workspace-sidebar: styles");
2265
2755
  const flowSource = (name) => ({
2266
2756
  getSnapshot: () => ctx.slots.entries(name).length > 0,
@@ -2269,6 +2759,8 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2269
2759
  const sidebarFlow = flowSource("sidebar.workspaces.directoryFlow");
2270
2760
  const pickerFlow = flowSource("conversation.hero.workspace.directoryFlow");
2271
2761
  const createWorkspace = (input) => ctx.workspaces.create(input);
2762
+ const navigation = new YaWorkspaceNavigation(ctx, ctx.remote.directoryPicker, ctx.workspaces, ctx.sessions);
2763
+ ctx.slots.provideRoot({ hooks: { workspaces: ctx.workspaces.list } });
2272
2764
  const searchSessions = async (query, signal) => {
2273
2765
  const result = await ctx.sessions.search(query, signal);
2274
2766
  if (!result.ok) throw new Error(result.error.message);
@@ -2276,7 +2768,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2276
2768
  };
2277
2769
  const sidebarInjected = () => ({
2278
2770
  startSession: (workspaceId) => {
2279
- ctx.workspaces.startSession(workspaceId);
2771
+ navigation.startSession(workspaceId);
2280
2772
  },
2281
2773
  open: (sessionId) => {
2282
2774
  ctx.sessions.open(sessionId);
@@ -2304,7 +2796,7 @@ button.ya-crumb:hover { background:var(--dsw-alias-interactive-bg-hover); color:
2304
2796
  await ctx.workspaces.delete(workspaceId);
2305
2797
  },
2306
2798
  archiveSession: async (sessionId) => {
2307
- await ctx.workspaces.archiveSession(sessionId);
2799
+ await navigation.archiveSession(sessionId);
2308
2800
  },
2309
2801
  insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
2310
2802
  await ctx.workspaces.insertSessionBefore(workspaceId, sessionId, beforeSessionId);