@openchambery/web 1.18.4-beta.9 → 1.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/assets/{AgentAvatar-DAnelAnB.js → AgentAvatar-DDcjamcJ.js} +1 -1
  2. package/dist/assets/AssistantView-n8llxdCo.js +1 -0
  3. package/dist/assets/{ChatContainer-CSHDg3oS.js → ChatContainer-B7H_fTkm.js} +35 -35
  4. package/dist/assets/{DiagramView-DUciKofo.js → DiagramView-DK5RkG9r.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-QxF1vuqX.js → FadeInOnReveal-By2F9vcI.js} +1 -1
  6. package/dist/assets/{Icon-D-X70irR.js → Icon-DpjYnXxn.js} +1 -1
  7. package/dist/assets/{MarkdownRendererImpl-BjAOpViW.js → MarkdownRendererImpl-DOpja779.js} +1 -1
  8. package/dist/assets/{MobileDetailNavigation-EXZlPK6E.js → MobileDetailNavigation-k1tBlKW5.js} +1 -1
  9. package/dist/assets/{MobileShareBridge-CevrYlt3.js → MobileShareBridge-HxyfCuga.js} +3 -3
  10. package/dist/assets/MobileSurface-BRouCuNF.js +2 -0
  11. package/dist/assets/{MultiRunWindow-CNWuOY4y.js → MultiRunWindow-CwzxTb3p.js} +1 -1
  12. package/dist/assets/{OnboardingScreen-BhSzVmYo.js → OnboardingScreen-BKmuT7AL.js} +1 -1
  13. package/dist/assets/{OverlayScrollbar-CHXk_jvF.js → OverlayScrollbar-Ch3J6ASR.js} +1 -1
  14. package/dist/assets/{SettingsView-YyQ-C-qq.js → SettingsView-Bx9tuWxH.js} +11 -11
  15. package/dist/assets/{SettingsWindow-uUezVVwi.js → SettingsWindow-sVzjzaGh.js} +1 -1
  16. package/dist/assets/{TerminalView-DPto-DoO.js → TerminalView-DSwK1UwL.js} +1 -1
  17. package/dist/assets/{ToolOutputDialog-CgPYRmDX.js → ToolOutputDialog-D9di4LaC.js} +1 -1
  18. package/dist/assets/appThemeRegistry-CN1tLL9G.js +2 -0
  19. package/dist/assets/{button-CwLFtUNT.js → button-C7OXr4ae.js} +1 -1
  20. package/dist/assets/{clipboard-DhOMUCwT.js → clipboard-DWuULPzo.js} +1 -1
  21. package/dist/assets/{collapsible-CP1-_pY6.js → collapsible-DaEya6b6.js} +1 -1
  22. package/dist/assets/{context-menu-BPxTMlbo.js → context-menu-BrLPLxWa.js} +1 -1
  23. package/dist/assets/{desktopHosts-Cr3LE55y.js → desktopHosts-Txp4xOVQ.js} +1 -1
  24. package/dist/assets/desktopRelayRestore-C_FSEHKM.js +1 -0
  25. package/dist/assets/{diagram-rYL80jaS.js → diagram-CHfginjS.js} +1 -1
  26. package/dist/assets/{dialog-BoAAoL5Z.js → dialog-DPS0W9eS.js} +1 -1
  27. package/dist/assets/{es-BXQb1dPR.js → es-BX0Vm3Ac.js} +2 -2
  28. package/dist/assets/fileQueries-BrUKKgPW.js +1 -0
  29. package/dist/assets/{fr-BTyDIvxw.js → fr-CuPzXTYx.js} +2 -2
  30. package/dist/assets/{gitApi-BktvSPdR.js → gitApi-C_tpQa_z.js} +2 -2
  31. package/dist/assets/{input-D52Jicb1.js → input-BzYA603C.js} +1 -1
  32. package/dist/assets/insertionBoundaries-CzVPcHK4.js +1 -0
  33. package/dist/assets/{ja-fcQgtBFy.js → ja-DGtzjrFO.js} +2 -2
  34. package/dist/assets/{ko-CgoVt1eb.js → ko-DZ9Di0je.js} +2 -2
  35. package/dist/assets/{main-DOGudNxM.js → main-BJBQp1tB.js} +2 -2
  36. package/dist/assets/{main-UJICKUVV.js → main-BUGDfAIY.js} +23 -23
  37. package/dist/assets/{markdown-worker-N9d_Z-7s.js → markdown-worker-W06-MhF5.js} +1 -1
  38. package/dist/assets/{markdownTheme-Bl0Sy0Lf.js → markdownTheme-B89dbj3N.js} +1 -1
  39. package/dist/assets/{miniChat-C465ME90.js → miniChat-BLtGuyQZ.js} +2 -2
  40. package/dist/assets/mobile-0Sb3IsCl.js +2 -0
  41. package/dist/assets/multirun-BC-EFVZ0.js +4 -0
  42. package/dist/assets/{persistence-vZ7H8WKh.js → persistence-6sclr7B6.js} +2 -2
  43. package/dist/assets/{pl-DVDnk2fT.js → pl-BuCO3qb1.js} +2 -2
  44. package/dist/assets/{platform-CTaznShj.js → platform-CbeLpKPk.js} +1 -1
  45. package/dist/assets/projectMeta-CYvQd9iC.js +3 -0
  46. package/dist/assets/{pt-BR-ofV_Qcd_.js → pt-BR-Brf4RMsw.js} +2 -2
  47. package/dist/assets/radio-CEt2UjvA.js +1 -0
  48. package/dist/assets/renderElectronMiniChatApp-ByA4BElP.js +2 -0
  49. package/dist/assets/renderMobileApp-upKrXoPH.js +23 -0
  50. package/dist/assets/{runtimeConfig-Dofg9S27.js → runtimeConfig-BAx9ix2e.js} +1 -1
  51. package/dist/assets/runtimeEndpointReset-Dst0Yqyz.js +147 -0
  52. package/dist/assets/{sortable-tabs-strip-N3j5So4j.js → sortable-tabs-strip-nLsPC06f.js} +1 -1
  53. package/dist/assets/src-BDOLh3pc.css +2 -0
  54. package/dist/assets/{store-Hr-xvtDJ.js → store-DQY8Fkb0.js} +3 -3
  55. package/dist/assets/{terminalApi-KDUc5mh2.js → terminalApi-BpPeXWIu.js} +1 -1
  56. package/dist/assets/{tooltip-CaCe-GAm.js → tooltip-D_orJMYd.js} +1 -1
  57. package/dist/assets/{uk-Dzob0yMx.js → uk-C-wmaPTi.js} +2 -2
  58. package/dist/assets/{url-B-FWrnxu.js → url-BsaBG5-1.js} +1 -1
  59. package/dist/assets/useAppFontEffects-Bo__MTWV.js +7 -0
  60. package/dist/assets/{useAssistantUIStore-ifqNyja4.js → useAssistantUIStore-BHsHXCKE.js} +1 -1
  61. package/dist/assets/{useDesktopWindowControlsLayout-CGYSjr_T.js → useDesktopWindowControlsLayout-DkmRLrOT.js} +1 -1
  62. package/dist/assets/{useEffectiveDirectory-BtkZPQZ2.js → useEffectiveDirectory-ByQPsbk0.js} +1 -1
  63. package/dist/assets/{useFontPreferences-D_LFFhFS.js → useFontPreferences-ckzpXQaJ.js} +1 -1
  64. package/dist/assets/useMobileNavigationStore-DLYb8f5L.js +475 -0
  65. package/dist/assets/useSessionAutoCleanup-C5knpuh5.js +1 -0
  66. package/dist/assets/useUIStore-DzwCfRqr.js +1 -0
  67. package/dist/assets/{useWorkerHighlightedLines-Bo9meAHp.js → useWorkerHighlightedLines-Ce2Bs8_N.js} +1 -1
  68. package/dist/assets/{utils-C2P0wcWO.js → utils-B1EK5zKa.js} +1 -1
  69. package/dist/assets/{zh-CN-Sndj2nhS.js → zh-CN-DueRrIql.js} +2 -2
  70. package/dist/assets/{zh-TW-S24a8EMD.js → zh-TW-6875X80V.js} +2 -2
  71. package/dist/index.html +9 -9
  72. package/dist/mini-chat.html +9 -9
  73. package/dist/mobile.html +9 -9
  74. package/package.json +1 -1
  75. package/server/lib/config-sync/config-sync.test.js +52 -0
  76. package/server/lib/config-sync/scripts.js +10 -2
  77. package/server/lib/notifications/DOCUMENTATION.md +2 -1
  78. package/server/lib/notifications/emitter-runtime.js +78 -1
  79. package/server/lib/notifications/emitter-runtime.test.js +94 -1
  80. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -1
  81. package/server/lib/scheduled-tasks/routes.js +51 -8
  82. package/server/lib/session-assist/DOCUMENTATION.md +4 -2
  83. package/server/lib/session-assist/runtime.js +67 -30
  84. package/server/lib/session-assist/runtime.test.js +114 -0
  85. package/server/lib/session-index/DOCUMENTATION.md +9 -3
  86. package/server/lib/session-index/service.js +45 -22
  87. package/server/lib/session-index/service.test.js +81 -0
  88. package/server/lib/session-title/DOCUMENTATION.md +15 -10
  89. package/server/lib/session-title/runtime.js +24 -0
  90. package/server/lib/session-title/runtime.test.js +143 -0
  91. package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
  92. package/server/lib/session-turn-pages/service.js +14 -1
  93. package/server/lib/session-turn-pages/service.test.js +133 -0
  94. package/server/lib/ui-auth/ui-auth.js +3 -1
  95. package/server/lib/ui-auth/ui-auth.test.js +25 -0
  96. package/server/sse-routes.test.js +64 -1
  97. package/dist/assets/AssistantView-CbCe7NJG.js +0 -1
  98. package/dist/assets/MobileSurface-cYYd428z.js +0 -2
  99. package/dist/assets/appThemeRegistry-CPVFQqR9.js +0 -2
  100. package/dist/assets/desktopRelayRestore-VfMK9fdk.js +0 -1
  101. package/dist/assets/fileQueries-CXaw4b1W.js +0 -1
  102. package/dist/assets/insertionBoundaries-Cqk1aLwK.js +0 -1
  103. package/dist/assets/mobile-BHRw1m5c.js +0 -2
  104. package/dist/assets/multirun-BCvIqAk-.js +0 -4
  105. package/dist/assets/projectMeta-Wxow1DQz.js +0 -3
  106. package/dist/assets/radio-CS9DITGQ.js +0 -1
  107. package/dist/assets/renderElectronMiniChatApp-DAIgdBk9.js +0 -2
  108. package/dist/assets/renderMobileApp-BXOtgQHy.js +0 -23
  109. package/dist/assets/runtimeEndpointReset-Cc3yypUQ.js +0 -147
  110. package/dist/assets/src-Dbdlm5x1.css +0 -2
  111. package/dist/assets/useAppFontEffects-BpT_sNyZ.js +0 -7
  112. package/dist/assets/useMobileNavigationStore-BNnnW08z.js +0 -476
  113. package/dist/assets/useSessionAutoCleanup-C5V-GCiC.js +0 -1
  114. package/dist/assets/useUIStore-NfqK-HA7.js +0 -1
@@ -410,6 +410,149 @@ describe('session-title helpers', () => {
410
410
  }
411
411
  });
412
412
 
413
+ it('recovers the fork first-refresh when the fork session.created event was lost', async () => {
414
+ const originalFetch = globalThis.fetch;
415
+ const forkCreated = 100;
416
+ const session = {
417
+ id: 'ses-fork-lost',
418
+ title: 'Original work (fork #1)',
419
+ time: { created: forkCreated },
420
+ metadata: {
421
+ openchamber: {
422
+ titleRefresh: {
423
+ // Inherited from the parent: activity predates the fork.
424
+ activityUpdatedAt: 50,
425
+ lastAutoTitle: 'Original work',
426
+ generatedAt: 40,
427
+ forMessageID: 'assistant-original',
428
+ },
429
+ },
430
+ },
431
+ };
432
+ const messages = [
433
+ { info: { id: 'user-fork', role: 'user' }, parts: [{ type: 'text', text: 'Try a different approach' }] },
434
+ { info: { id: 'assistant-fork', role: 'assistant' }, parts: [{ type: 'text', text: 'Different approach ready' }] },
435
+ ];
436
+ let generationCalls = 0;
437
+
438
+ globalThis.fetch = async (input, init = {}) => {
439
+ const url = String(input);
440
+ if (url.includes('/message')) {
441
+ return new Response(JSON.stringify(messages), { status: 200 });
442
+ }
443
+ if (init.method === 'PATCH') {
444
+ const body = typeof init.body === 'string' ? JSON.parse(init.body) : init.body;
445
+ if (typeof body?.title === 'string') session.title = body.title;
446
+ if (body?.metadata) session.metadata = body.metadata;
447
+ return new Response(JSON.stringify(session), { status: 200 });
448
+ }
449
+ return new Response(JSON.stringify(session), { status: 200 });
450
+ };
451
+
452
+ try {
453
+ const runtime = createSessionTitleRuntime({
454
+ buildOpenCodeUrl: (pathname) => `http://opencode${pathname}`,
455
+ getOpenCodeAuthHeaders: () => ({}),
456
+ getSmallModelService: async () => ({
457
+ generateSmallModelText: async () => {
458
+ generationCalls += 1;
459
+ return { text: 'Different approach', providerID: 'test', modelID: 'test' };
460
+ },
461
+ }),
462
+ now: () => 1_000,
463
+ isTitleRefreshEnabled: () => true,
464
+ });
465
+
466
+ // No session.created — it was lost across an SSE reconnect gap. The first
467
+ // newly-sent fork message lazily re-registers the pending fork entry.
468
+ runtime.processPayload({
469
+ type: 'message.updated',
470
+ properties: { directory: '/repo', info: { sessionID: 'ses-fork-lost', role: 'user', id: 'user-fork', time: { created: 200 } } },
471
+ });
472
+ await new Promise((resolve) => setTimeout(resolve, 25));
473
+
474
+ runtime.processPayload({
475
+ type: 'session.status',
476
+ properties: { sessionID: 'ses-fork-lost', directory: '/repo', status: { type: 'idle' } },
477
+ });
478
+ await new Promise((resolve) => setTimeout(resolve, 25));
479
+
480
+ expect(generationCalls).toBe(1);
481
+ expect(session.title).toBe('Different approach');
482
+ runtime.stop();
483
+ } finally {
484
+ globalThis.fetch = originalFetch;
485
+ }
486
+ });
487
+
488
+ it('does not lazily re-register a fork after its activity already moved past the fork', async () => {
489
+ const originalFetch = globalThis.fetch;
490
+ const forkCreated = 100;
491
+ const session = {
492
+ id: 'ses-fork-advanced',
493
+ title: 'Original work (fork #1)',
494
+ time: { created: forkCreated },
495
+ metadata: {
496
+ openchamber: {
497
+ titleRefresh: {
498
+ // A fork message already advanced activity past the fork time.
499
+ activityUpdatedAt: 300,
500
+ lastAutoTitle: 'Original work',
501
+ generatedAt: 40,
502
+ forMessageID: 'assistant-original',
503
+ },
504
+ },
505
+ },
506
+ };
507
+ let generationCalls = 0;
508
+
509
+ globalThis.fetch = async (input, init = {}) => {
510
+ if (String(input).includes('/message')) {
511
+ return new Response(JSON.stringify([
512
+ { info: { id: 'user-x', role: 'user' }, parts: [{ type: 'text', text: 'later message' }] },
513
+ { info: { id: 'assistant-x', role: 'assistant' }, parts: [{ type: 'text', text: 'later reply' }] },
514
+ ]), { status: 200 });
515
+ }
516
+ if (init.method === 'PATCH') {
517
+ const body = typeof init.body === 'string' ? JSON.parse(init.body) : init.body;
518
+ if (body?.metadata) session.metadata = body.metadata;
519
+ return new Response(JSON.stringify(session), { status: 200 });
520
+ }
521
+ return new Response(JSON.stringify(session), { status: 200 });
522
+ };
523
+
524
+ try {
525
+ const runtime = createSessionTitleRuntime({
526
+ buildOpenCodeUrl: (pathname) => `http://opencode${pathname}`,
527
+ getOpenCodeAuthHeaders: () => ({}),
528
+ getSmallModelService: async () => ({
529
+ generateSmallModelText: async () => {
530
+ generationCalls += 1;
531
+ return { text: 'Should not run', providerID: 'test', modelID: 'test' };
532
+ },
533
+ }),
534
+ now: () => 1_000,
535
+ isTitleRefreshEnabled: () => true,
536
+ });
537
+
538
+ runtime.processPayload({
539
+ type: 'message.updated',
540
+ properties: { directory: '/repo', info: { sessionID: 'ses-fork-advanced', role: 'user', id: 'user-x', time: { created: 400 } } },
541
+ });
542
+ await new Promise((resolve) => setTimeout(resolve, 25));
543
+ runtime.processPayload({
544
+ type: 'session.status',
545
+ properties: { sessionID: 'ses-fork-advanced', directory: '/repo', status: { type: 'idle' } },
546
+ });
547
+ await new Promise((resolve) => setTimeout(resolve, 25));
548
+
549
+ expect(generationCalls).toBe(0);
550
+ runtime.stop();
551
+ } finally {
552
+ globalThis.fetch = originalFetch;
553
+ }
554
+ });
555
+
413
556
  it('refreshes a new session on first idle, then ignores later idle transitions', async () => {
414
557
  const originalFetch = globalThis.fetch;
415
558
  const session = {
@@ -68,7 +68,7 @@ Success `200`:
68
68
  - `records`: chronological oldest → newest, including intermediate non-boundary rows (synthetic, tools, subtask, compaction) inside the window.
69
69
  - `turnCount`: count of authored user boundaries in `records`.
70
70
  - `cursor`: host-owned opaque token for the next `before=` request; `null` when `complete` is true. The token represents the position **just before** the earliest returned authored user (so the client can load older history without overlap).
71
- - `complete`: `upstreamComplete && selected.length === accumulated.length`. True when upstream history is exhausted **and** `selectTurnRecords` did not trim any older scanned rows (nothing left for the client). When the scan window held more than N turns and older rows were trimmed, `complete` stays `false` with a `cursor` so the client can fetch the trimmed history.
71
+ - `complete`: `upstreamComplete && selected.length === accumulated.length`. True when upstream history is exhausted **and** `selectTurnRecords` did not trim any older scanned rows (nothing left for the client). When the scan window held more than N turns and older rows were trimmed, or when upstream stopped on a full page with a missing `nextCursor` (suspicious truncation), `complete` stays `false` with a `cursor` so the client can fetch / retry.
72
72
  - `partsProjection`: `"slim-v1"` on every turn-page response (first packet and prepend). Names the projection applied to `records`; see below. Reconcile responses omit this field and keep full parts.
73
73
 
74
74
  ### Turn-page payload projection
@@ -147,6 +147,7 @@ A message is an authored user turn boundary when:
147
147
 
148
148
  - Upstream pages are chronological **old→new** within each page; **do not reverse**. Older pages are **prepended** while deduping by `info.id` (global order remains old→new).
149
149
  - Continues paging with raw upstream `before` until `turns` authored boundaries are collected or upstream reports no next cursor.
150
+ - **Suspicious truncation:** an upstream page whose `records.length >= scanLimit` (full page) but `nextCursor == null` is treated as a non-authoritative cut (observed OpenCode `x-next-cursor` race), not as end-of-history. The host stops scanning that round with `upstreamComplete=false`, so the response stays `complete=false` and still emits a boundary host cursor the client can retry. A short page (`records.length < scanLimit`) with no cursor, an empty page with no cursor, or an explicit `page.complete === true` remain authoritative exhaustion. Client no-growth handling still bounds retries if the upstream view stays truncated.
150
151
  - Every upstream record must have a non-empty `info.id`; otherwise structured error, no partial `records`, HTTP 502.
151
152
  - Hard scan caps (no partial success): **50 pages** / **5000 messages** → structured error, HTTP 413.
152
153
  - Repeated / stalled cursor, or empty page that still carries a next cursor → structured error, no partial `records`, HTTP 502.
@@ -719,7 +719,14 @@ export const createSessionTurnPageService = ({
719
719
 
720
720
  const rawNext = page.nextCursor;
721
721
  const nextCursor = typeof rawNext === 'string' && rawNext.length > 0 ? rawNext : null;
722
- const pageComplete = page.complete === true || nextCursor == null;
722
+ // Explicit page.complete wins. Otherwise: no nextCursor on a short page
723
+ // is authoritative exhaustion; a *full* page with no nextCursor is a
724
+ // suspicious upstream truncation (x-next-cursor race) — stop scanning
725
+ // but do not mark upstreamComplete so the host still emits a retryable
726
+ // boundary cursor instead of permanently exhausting the client.
727
+ const fullPageNoCursor = nextCursor == null && records.length >= pageLimit;
728
+ const pageComplete = page.complete === true
729
+ || (nextCursor == null && !fullPageNoCursor);
723
730
 
724
731
  if (records.length === 0) {
725
732
  if (nextCursor) {
@@ -795,6 +802,12 @@ export const createSessionTurnPageService = ({
795
802
  break;
796
803
  }
797
804
 
805
+ if (fullPageNoCursor) {
806
+ // Suspicious truncation: keep upstreamComplete=false so the response
807
+ // stays complete=false with a host cursor the client can retry.
808
+ break;
809
+ }
810
+
798
811
  // Enough authored boundaries collected — stop without requiring exhaustion.
799
812
  if (countAuthoredBoundaries(accumulated) >= turnBudget) {
800
813
  break;
@@ -1107,4 +1107,137 @@ describe('createSessionTurnPageService', () => {
1107
1107
  signal: controller.signal,
1108
1108
  }));
1109
1109
  });
1110
+
1111
+ describe('full-page missing nextCursor (suspicious upstream truncation)', () => {
1112
+ it('short page without cursor remains authoritative exhaustion', async () => {
1113
+ // records < scanLimit + no cursor → real end of history.
1114
+ const fetchPage = vi.fn(async () => pageResult([
1115
+ user('msg_u1'),
1116
+ assistant('msg_a1'),
1117
+ user('msg_u2'),
1118
+ assistant('msg_a2'),
1119
+ ], null));
1120
+ const service = createSessionTurnPageService({ fetchPage });
1121
+
1122
+ const result = await service.loadPage({
1123
+ sessionID: 'ses_1',
1124
+ turns: 3,
1125
+ scanLimit: 100,
1126
+ });
1127
+
1128
+ expect(result).toMatchObject({
1129
+ ok: true,
1130
+ complete: true,
1131
+ cursor: null,
1132
+ turnCount: 2,
1133
+ });
1134
+ });
1135
+
1136
+ it('full page without cursor stays incomplete and emits a host cursor', async () => {
1137
+ // Upstream race: page is full (records >= scanLimit) but x-next-cursor is
1138
+ // missing. Must not permanently exhaust — client needs a retryable cursor.
1139
+ const records = [];
1140
+ for (let i = 1; i <= 10; i += 1) {
1141
+ records.push(user(`msg_u${i}`), assistant(`msg_a${i}`));
1142
+ }
1143
+ expect(records.length).toBe(20);
1144
+ const fetchPage = vi.fn(async () => pageResult(records, null));
1145
+ const service = createSessionTurnPageService({ fetchPage });
1146
+
1147
+ const result = await service.loadPage({
1148
+ sessionID: 'ses_1',
1149
+ turns: 3,
1150
+ scanLimit: 20,
1151
+ });
1152
+
1153
+ expect(result.ok).toBe(true);
1154
+ expect(result.complete).toBe(false);
1155
+ expect(typeof result.cursor).toBe('string');
1156
+ expect(result.cursor.startsWith('oc1.')).toBe(true);
1157
+ expect(result.turnCount).toBe(3);
1158
+ expect(decodeHostCursor(result.cursor)).toEqual({
1159
+ ok: true,
1160
+ value: { before: null, boundaryID: 'msg_u8' },
1161
+ });
1162
+ });
1163
+
1164
+ it('page with nextCursor keeps paging (unchanged)', async () => {
1165
+ const pages = new Map([
1166
+ [undefined, pageResult([
1167
+ user('msg_u2'),
1168
+ assistant('msg_a2'),
1169
+ user('msg_u3'),
1170
+ assistant('msg_a3'),
1171
+ ], 'opaque_older')],
1172
+ ['opaque_older', pageResult([
1173
+ user('msg_u1'),
1174
+ assistant('msg_a1'),
1175
+ ], null)],
1176
+ ]);
1177
+ const fetchPage = vi.fn(async ({ before }) => pages.get(before) ?? pageResult([], null));
1178
+ const service = createSessionTurnPageService({ fetchPage });
1179
+
1180
+ const result = await service.loadPage({
1181
+ sessionID: 'ses_1',
1182
+ turns: 3,
1183
+ scanLimit: 100,
1184
+ });
1185
+
1186
+ expect(result).toMatchObject({
1187
+ ok: true,
1188
+ complete: true,
1189
+ cursor: null,
1190
+ turnCount: 3,
1191
+ });
1192
+ expect(fetchPage.mock.calls.length).toBe(2);
1193
+ });
1194
+
1195
+ it('empty page without cursor remains authoritative exhaustion', async () => {
1196
+ const fetchPage = vi.fn(async () => pageResult([], null));
1197
+ const service = createSessionTurnPageService({ fetchPage });
1198
+
1199
+ const result = await service.loadPage({
1200
+ sessionID: 'ses_1',
1201
+ turns: 3,
1202
+ scanLimit: 100,
1203
+ });
1204
+
1205
+ expect(result).toMatchObject({
1206
+ ok: true,
1207
+ complete: true,
1208
+ cursor: null,
1209
+ turnCount: 0,
1210
+ });
1211
+ expect(result.records).toEqual([]);
1212
+ });
1213
+
1214
+ it('explicit page.complete=true on a full page without cursor still exhausts', async () => {
1215
+ // Explicit complete declaration outranks the full-page heuristic.
1216
+ // Exactly turns=budget so selectTurnRecords does not trim — complete
1217
+ // stays true solely because upstreamComplete was set by page.complete.
1218
+ const records = [];
1219
+ for (let i = 1; i <= 10; i += 1) {
1220
+ records.push(user(`msg_u${i}`), assistant(`msg_a${i}`));
1221
+ }
1222
+ const fetchPage = vi.fn(async () => ({
1223
+ records,
1224
+ nextCursor: null,
1225
+ complete: true,
1226
+ }));
1227
+ const service = createSessionTurnPageService({ fetchPage });
1228
+
1229
+ const result = await service.loadPage({
1230
+ sessionID: 'ses_1',
1231
+ turns: 10,
1232
+ scanLimit: 20,
1233
+ });
1234
+
1235
+ expect(result).toMatchObject({
1236
+ ok: true,
1237
+ complete: true,
1238
+ cursor: null,
1239
+ turnCount: 10,
1240
+ });
1241
+ });
1242
+ });
1110
1243
  });
@@ -714,7 +714,9 @@ export const createUiAuth = ({
714
714
  const respondUnauthorized = (req, res) => {
715
715
  res.status(401);
716
716
  const acceptsJson = req.headers.accept?.includes('application/json');
717
- if (acceptsJson || req.path?.startsWith('/api')) {
717
+ // Express strips the mount prefix from req.path (e.g. app.use('/api') turns
718
+ // /api/mcp into /mcp), so resolve the original pathname instead.
719
+ if (acceptsJson || getRequestPathname(req).startsWith('/api')) {
718
720
  res.json({ error: 'UI authentication required', locked: true });
719
721
  } else {
720
722
  res.type('text/plain').send('Authentication required');
@@ -259,6 +259,31 @@ describe('ui auth client credential seam', () => {
259
259
  }
260
260
  });
261
261
 
262
+ it('returns JSON 401 for unauthenticated API requests mounted under a sub-app', async () => {
263
+ const createUiAuth = await loadCreateUiAuth();
264
+ const auth = createUiAuth({ password: 'secret' });
265
+
266
+ // Express strips the mount prefix: app.use('/api') turns /api/mcp into
267
+ // req.path '/mcp'. The unauthorized response must still be identified as an
268
+ // API request (JSON + locked) so clients can react to the auth gate.
269
+ const req = {
270
+ method: 'GET',
271
+ baseUrl: '/api',
272
+ path: '/mcp',
273
+ originalUrl: '/api/mcp',
274
+ url: '/api/mcp',
275
+ headers: {},
276
+ };
277
+ const res = createResponse();
278
+ let called = false;
279
+ await auth.requireAuth(req, res, () => {
280
+ called = true;
281
+ });
282
+ expect(called).toBe(false);
283
+ expect(res.statusCode).toBe(401);
284
+ expect(res.body).toEqual({ error: 'UI authentication required', locked: true });
285
+ });
286
+
262
287
  it('issues desktop client tokens with the UI session expiry', async () => {
263
288
  const createUiAuth = await loadCreateUiAuth();
264
289
  let createClientInput = null;
@@ -52,8 +52,10 @@ const createMockResponse = () => {
52
52
  let body = '';
53
53
  let flushed = false;
54
54
  let bodyFlushCount = 0;
55
+ let destroyed = false;
56
+ let writableEnded = false;
55
57
 
56
- return {
58
+ const res = {
57
59
  on(event, handler) {
58
60
  listeners.set(event, handler);
59
61
  return this;
@@ -80,6 +82,9 @@ const createMockResponse = () => {
80
82
  body += String(chunk);
81
83
  return true;
82
84
  },
85
+ destroy() {
86
+ destroyed = true;
87
+ },
83
88
  status(code) {
84
89
  statusCode = code;
85
90
  return this;
@@ -100,7 +105,21 @@ const createMockResponse = () => {
100
105
  get bodyFlushCount() {
101
106
  return bodyFlushCount;
102
107
  },
108
+ get destroyed() {
109
+ return destroyed;
110
+ },
111
+ set destroyed(value) {
112
+ destroyed = Boolean(value);
113
+ },
114
+ get writableEnded() {
115
+ return writableEnded;
116
+ },
117
+ set writableEnded(value) {
118
+ writableEnded = Boolean(value);
119
+ },
103
120
  };
121
+
122
+ return res;
104
123
  };
105
124
 
106
125
  describe('local SSE routes', () => {
@@ -162,6 +181,7 @@ describe('local SSE routes', () => {
162
181
  getOpenChamberEventClients: () => clients,
163
182
  writeSseEvent(res, payload) {
164
183
  res.write(`data: ${JSON.stringify(payload)}\n\n`);
184
+ return true;
165
185
  },
166
186
  });
167
187
 
@@ -183,4 +203,47 @@ describe('local SSE routes', () => {
183
203
  req.emit('close');
184
204
  expect(clients.has(res)).toBe(false);
185
205
  });
206
+
207
+ it('evicts half-open OpenChamber SSE clients after consecutive paused heartbeats', () => {
208
+ vi.useFakeTimers();
209
+ const { app, getRoute } = createRouteRegistry();
210
+ const clients = new Set();
211
+ let destroyCount = 0;
212
+
213
+ try {
214
+ registerScheduledTaskRoutes(app, {
215
+ getOpenChamberEventClients: () => clients,
216
+ writeSseEvent(res) {
217
+ res.__ssePaused = true;
218
+ return false;
219
+ },
220
+ });
221
+
222
+ const handler = getRoute('GET', '/api/openchamber/events');
223
+ const req = createMockRequest();
224
+ const res = createMockResponse();
225
+ res.destroy = () => {
226
+ destroyCount += 1;
227
+ res.destroyed = true;
228
+ };
229
+
230
+ handler(req, res);
231
+ expect(clients.has(res)).toBe(true);
232
+
233
+ vi.advanceTimersByTime(25_000);
234
+ expect(clients.has(res)).toBe(true);
235
+ expect(destroyCount).toBe(0);
236
+
237
+ vi.advanceTimersByTime(25_000);
238
+ expect(clients.has(res)).toBe(false);
239
+ expect(destroyCount).toBe(1);
240
+ expect(vi.getTimerCount()).toBe(0);
241
+
242
+ // Idempotent cleanup: a late close must not throw or re-destroy.
243
+ req.emit('close');
244
+ expect(destroyCount).toBe(1);
245
+ } finally {
246
+ vi.useRealTimers();
247
+ }
248
+ });
186
249
  });
@@ -1 +0,0 @@
1
- import{o as e,r as t}from"./rolldown-runtime-C0FnF6B9.js";import{A as n,E as r,t as i,w as a}from"./useUIStore-NfqK-HA7.js";import{Ot as o,et as s,rt as c,yt as l}from"./persistence-vZ7H8WKh.js";import{nt as u,rt as d}from"./vendor-base-ui-CnHfUmDN.js";import{r as f}from"./platform-CTaznShj.js";import{n as p}from"./vendor-react-DLPElpoQ.js";import{r as m}from"./vendor-sonner-DVzioF3d.js";import{n as h}from"./vendor-reactuses-core-DDoDDyM9.js";import{n as g}from"./clipboard-DhOMUCwT.js";import{a as _,n as v,s as y}from"./MobileShareBridge-CevrYlt3.js";import{Jr as b,Ka as x,No as S,Qr as C,Ya as w,an as T,bi as E,ca as D,en as O,fn as k,mn as A,o as j,pn as M,vo as N,yo as P,zi as F}from"./useMobileNavigationStore-BNnnW08z.js";import{_ as I,a as L,c as R,d as ee,f as te,g as ne,l as re,n as ie,o as ae,p as oe,r as se,t as z,v as B,x as V,y as ce}from"./MobileDetailNavigation-EXZlPK6E.js";import{i as le,r as ue}from"./useAssistantUIStore-ifqNyja4.js";import{t as de}from"./AgentAvatar-DAnelAnB.js";import{t as fe}from"./utils-C2P0wcWO.js";import{n as pe}from"./Icon-D-X70irR.js";import{a as H,i as me,n as he,o as U,r as W,t as G}from"./dialog-BoAAoL5Z.js";import{t as ge}from"./button-CwLFtUNT.js";import{i as _e}from"./OverlayScrollbar-CHXk_jvF.js";import{I as ve,L as ye,M as be,N as K,q as xe,t as q}from"./ChatContainer-CSHDg3oS.js";import{a as Se,i as Ce,n as we,r as Te,t as Ee}from"./context-menu-BPxTMlbo.js";var De=p(),J=e(d(),1),Y=u(),Oe=e=>{let t=(0,De.c)(46),{assistant:n,sessionID:r,warning:i,surface:a,onRevertMessage:o,onEditMessage:s,pendingUserMessages:c,onPendingUserMessagesMaterialized:l}=e,u=n.effectiveWorkspacePath,d;t[0]!==n.sessionGeneration||t[1]!==r?(d={sessionID:r,sessionGeneration:n.sessionGeneration},t[0]=n.sessionGeneration,t[1]=r,t[2]=d):d=t[2];let p=B(n.id,d,a.active);p.data?.pages;let m;t[3]===p.data?.pages?m=t[4]:(m=p.data?.pages??[],t[3]=p.data?.pages,t[4]=m);let g;t[5]===m?g=t[6]:(g=E(m),t[5]=m,t[6]=g);let _=g,v;if(t[7]!==_){v=new Map;for(let e of _){let t=v.get(e.sessionID);v.set(e.sessionID,t===void 0||t===e.directory?e.directory:null)}t[7]=_,t[8]=v}else v=t[8];let y=v,b;t[9]===p?b=t[10]:(b=async()=>{(p.hasNextPage||p.isFetchNextPageError)&&await p.fetchNextPage()},t[9]=p,t[10]=b);let x=h(b),S=n.mode===`continuous`,C=be(),w;t[11]!==u||t[12]!==y||t[13]!==C||t[14]!==r?(w=(e,t)=>{let n=e===r?u:y.get(e);if(!n||n!==t){M(e,`missing-directory`);return}A(e,t,{phoneShell:!!(C&&!f()),switchToChat:!0})},t[11]=u,t[12]=y,t[13]=C,t[14]=r,t[15]=w):w=t[15];let T=h(w),D;t[16]===S?D=t[17]:(D={...K,forkSession:!1,navigateNestedSession:!1,mutateSession:S},t[16]=S,t[17]=D);let O;t[18]===s?O=t[19]:(O=s?{onEditMessage:s}:{},t[18]=s,t[19]=O);let k;t[20]!==u||t[21]!==o||t[22]!==T||t[23]!==r||t[24]!==a.active||t[25]!==a.surfaceID||t[26]!==D||t[27]!==O?(k={kind:`embedded`,surfaceId:a.surfaceID,sessionId:r,directory:u,active:a.active,capabilities:D,onRevertMessage:o,...O,openSourceSession:T},t[20]=u,t[21]=o,t[22]=T,t[23]=r,t[24]=a.active,t[25]=a.surfaceID,t[26]=D,t[27]=O,t[28]=k):k=t[28];let j=k,N=p.isError||p.isSuccess&&!p.hasNextPage,P=p.isLoading||p.isFetchingNextPage,F;t[29]!==x||t[30]!==N||t[31]!==_||t[32]!==P?(F={entries:_,complete:N,loading:P,fetchPrevious:x},t[29]=x,t[30]=N,t[31]=_,t[32]=P,t[33]=F):F=t[33];let I;t[34]!==u||t[35]!==l||t[36]!==o||t[37]!==c||t[38]!==r||t[39]!==j||t[40]!==a||t[41]!==F||t[42]!==i?(I={sessionId:r,directory:u,composerSurface:a,sessionSurface:j,warning:i,pendingUserMessages:c,onPendingUserMessagesMaterialized:l,assistantHistory:F,onRevertMessage:o},t[34]=u,t[35]=l,t[36]=o,t[37]=c,t[38]=r,t[39]=j,t[40]=a,t[41]=F,t[42]=i,t[43]=I):I=t[43];let L=I,R;return t[44]===L?R=t[45]:(R=(0,Y.jsx)(q,{autoOpenDraft:!1,host:L}),t[44]=L,t[45]=R),R},ke=class extends Error{constructor(){super(`assistant_selection_stale`)}},Ae=(e,t)=>e.assistantID===t.assistantID&&e.transportIdentity===t.transportIdentity&&e.runtimeGeneration===t.runtimeGeneration,je=class{onBusyChange;onError;flight=null;authoritativeIdentity=null;constructor(e,t){this.onBusyChange=e,this.onError=t}activate(e){this.authoritativeIdentity&&!Ae(this.authoritativeIdentity,e)&&this.flight&&this.retire(this.flight),this.authoritativeIdentity=e}enqueue(e,t,n){if(!this.isAuthoritative(e))return Promise.reject(new ke);let r=this.flight;r||(r={identity:e,controller:new AbortController,running:!1,desired:null,waiters:[],activeWaiters:[],idleWaiters:[],retired:!1},this.flight=r),r.desired=t;let i=new Promise((e,t)=>{r.waiters.push({resolve:e,reject:t})});return r.running||(r.running=!0,this.onBusyChange(!0),this.run(r,n)),i}async flush(e){if(!this.isAuthoritative(e))throw new ke;let t=this.flight;t?.running&&await new Promise((e,n)=>t.idleWaiters.push({resolve:e,reject:n}))}assertAuthoritative(e){if(!this.isAuthoritative(e))throw new ke}dispose(){this.flight&&this.retire(this.flight),this.authoritativeIdentity=null}async run(e,t){for(;!e.retired&&e.desired;){let n=e.desired;e.desired=null;let r=e.waiters.splice(0);e.activeWaiters=r;try{if(await t({identity:e.identity,selection:n,signal:e.controller.signal}),!e.retired)for(let e of r)e.resolve()}catch(t){if(!e.retired){this.onError(t);for(let e of r)e.reject(t)}}e.activeWaiters=[]}e.running=!1;for(let t of e.idleWaiters.splice(0))t.resolve();this.flight===e&&this.onBusyChange(!1)}retire(e){e.retired=!0,e.controller.abort();let t=new ke;for(let n of e.activeWaiters.splice(0))n.reject(t);for(let n of e.waiters.splice(0))n.reject(t);for(let n of e.idleWaiters.splice(0))n.reject(t);this.flight===e&&(this.flight=null,this.onBusyChange(!1))}isAuthoritative(e){return this.authoritativeIdentity!==null&&Ae(this.authoritativeIdentity,e)}},X=(e,t)=>{let n=t.providerID??e.providerID,r=t.modelID??e.modelID,i=n!==e.providerID||r!==e.modelID;return{enabled:e.enabled,name:e.name,defaultPrompt:e.defaultPrompt,workspacePath:e.workspacePath,providerID:n,modelID:r,agent:t.agent??null,variant:i?null:t.variant??null,mode:e.mode}},Me=async(e,t,n)=>{n.assertAuthoritative();let r=n.readSnapshot()?.assistants.find(t=>t.id===e.assistantID);if(!r)throw Error(`assistant_unavailable`);try{n.assertAuthoritative(),await n.updateAssistant(r,X(r,t)),n.assertAuthoritative()}catch(i){if(n.assertAuthoritative(),n.signal.aborted)throw new ke;if(!(i instanceof V)||i.code!==`revision_conflict`)throw i;if(n.assertAuthoritative(),r=(await n.ensureSnapshot(n.signal)).assistants.find(t=>t.id===e.assistantID),n.assertAuthoritative(),!r)throw new V(`revision_conflict`,409);n.assertAuthoritative(),await n.updateAssistant(r,X(r,t)),n.assertAuthoritative()}};async function Z(e){await L(e);let t=le.getState().defaultShareAssistant;if(t?.assistantID!==e.id)return;let n=await R();n.serverInstanceID&&t.serverInstanceID===n.serverInstanceID&&le.getState().setDefaultShareAssistant(null)}function Ne({assistant:e,open:t,onOpenChange:n,onDeleted:r}){let{t:i}=g(),[a,o]=J.useState(!1),s=e?_(e.name).displayName||e.name:``,c=h(e=>{a&&!e||n(e)}),l=h(()=>{a||n(!1)}),u=h(async()=>{if(!(!e||a)){o(!0);try{await Z(e),r?.(e.id),n(!1),m.success(i(`assistants.settings.toast.deleted`))}catch{m.error(i(`assistants.settings.toast.deleteFailed`))}finally{o(!1)}}});return(0,Y.jsx)(G,{open:t&&e!==null,onOpenChange:c,children:(0,Y.jsxs)(he,{className:`max-w-md`,children:[(0,Y.jsxs)(H,{children:[(0,Y.jsx)(U,{children:i(`assistants.settings.delete`)}),(0,Y.jsx)(W,{children:i(`assistants.settings.deleteConfirm`,{name:s})})]}),(0,Y.jsxs)(me,{children:[(0,Y.jsx)(ge,{size:`sm`,variant:`ghost`,onClick:l,disabled:a,children:i(`settings.common.actions.cancel`)}),(0,Y.jsx)(ge,{size:`sm`,variant:`destructive`,onClick:u,disabled:a,children:i(`settings.common.actions.delete`)})]})]})})}var Pe=[0,500,1500,4e3],Fe=(e,t,n)=>{let r=!1,i=e.map(e=>e.info.id!==t||e.info.sessionID===n?e:(r=!0,{...e,info:{...e.info,sessionID:n}}));return r?i:e},Ie=e=>{let t=e.refreshDelaysMs??Pe,n=e.sleep??(e=>new Promise(t=>setTimeout(t,e)));(async()=>{for(let r of t){if(r>0&&await n(r),!e.isCurrent())return;try{await e.refresh(e.binding);break}catch{}}})()},Le=e=>`${e.transport}\u0000${e.assistantID}`,Re=e=>({transport:e.transport,assistantID:e.assistantID}),ze=(e,t)=>e.assistantID===t.assistantID&&e.sessionID===t.sessionID&&e.directory===t.directory&&e.sessionGeneration===t.sessionGeneration&&e.transport===t.transport&&e.runtimeGeneration===t.runtimeGeneration,Be=(e,t)=>e.assistantID===t.assistantID&&e.sessionID===t.sessionID&&e.directory===t.directory&&e.sessionGeneration===t.sessionGeneration&&e.messageID===t.messageID&&e.transport===t.transport&&e.runtimeGeneration===t.runtimeGeneration,Ve=()=>{let e=new Map,t=new Map,n=1,r=e=>Le(e),i=async(e,n)=>{let i=r(e),a=t.get(i)??Promise.resolve(),o,s=new Promise(e=>{o=e}),c=a.catch(()=>void 0).then(()=>s);t.set(i,c),await a.catch(()=>void 0);try{return await n()}finally{o(),t.get(i)===c&&t.delete(i)}},a=(t,i)=>{let a=r(Re(t)),o=n++;return e.set(a,{identity:t,rollback:i,token:o}),o},o=t=>e.get(r(t))?.identity,s=t=>e.delete(r(t)),c=async e=>i(e,()=>s(e)),l=async t=>{let n=r(t),i=e.get(n);if(!i)return{kind:`none`};let a=i.token,o=i.identity,s;try{s=await i.rollback()}catch{return{kind:`blocked`,status:`failed`}}let c=e.get(n);return!c||c.token!==a||!Be(c.identity,o)?{kind:`superseded`}:s.status===`rolled-back`||s.status===`conflict`?(e.delete(n),{kind:`cleared`,status:s.status}):{kind:`blocked`,status:s.status}},u=async(t,n)=>{let i=r(t),a=e.get(i);if(!a)return{kind:`none`};if(ze(a.identity,n))return{kind:`match`};let o=a.token,s=a.identity,c=await a.rollback(),l=e.get(i);return!l||l.token!==o||!Be(l.identity,s)?{kind:`superseded`}:c.status===`rolled-back`||c.status===`conflict`?(e.delete(i),{kind:`cleared`,status:c.status}):{kind:`rollback_failed`,retained:!0,status:c.status}};return{register:a,read:o,clear:s,clearExclusive:c,retireCurrent:l,rollbackAndClearIfBindingMismatch:async(e,t)=>i(e,()=>u(e,t)),rollbackAllBestEffort:async t=>{let n=[...e.entries()];await Promise.all(n.map(async([,n])=>{if(t?.excludeTransport!==void 0&&n.identity.transport===t.excludeTransport)return;let a=Re(n.identity);await i(a,async()=>{let t=r(a),i=e.get(t);if(!i||i.token!==n.token)return;let o=i.token,s=i.identity;try{let n=await i.rollback(),r=e.get(t);if(!r||r.token!==o||!Be(r.identity,s))return;(n.status===`rolled-back`||n.status===`conflict`)&&e.delete(t)}catch{}})}))},stageExclusive:async(e,t)=>i(e,async()=>{let n=await l(e);if(n.kind===`blocked`)return{kind:`blocked`,status:n.status};let r=await t();if(!r)return{kind:`skipped`};let i=a(r.identity,r.rollback);return r.protectOnStale?{kind:`stale_protected`,token:i,identity:r.identity}:{kind:`registered`,token:i,identity:r.identity}})}},He=(e,t)=>!e?.attachments?.length||!t?[]:e.attachments.flatMap(e=>{let n=t[e.attachmentRefID];return n?[n]:[]}),Ue=(e,t)=>{let n=t??{};return e.map(e=>({partID:e.partID,text:e.text,...e.synthetic===!0?{synthetic:!0}:{},attachments:e.attachments.flatMap(e=>n[e.attachmentRefID]?[n[e.attachmentRefID]]:[])}))},We=(e,t)=>{if(t.length===0)return[...e];let n=new Map;for(let t of e)n.set(t.partID,t);for(let e of t)n.set(e.partID,e);let r=new Set,i=[];for(let t of e){let e=n.get(t.partID);e&&(i.push(e),r.add(t.partID))}for(let e of t)r.has(e.partID)||(i.push(e),r.add(e.partID));return i},Ge=t({AssistantView:()=>Qe}),Ke={},qe=[],Je=[],Ye=(e,t,n)=>[...e===void 0?[]:[{type:`text`,text:e}],...(t??[]).flatMap(e=>[{type:`text`,text:e.text,...e.synthetic===!0?{synthetic:!0}:{}},...(e.attachments??[]).map(e=>({type:`file`,mime:e.mimeType,url:e.dataUrl}))]),...(n??[]).map(e=>({type:`file`,mime:e.mimeType,url:e.dataUrl}))],Xe=e=>{let t=(0,De.c)(11),{assistant:n,onBack:r}=e,{t:i}=g(),a;t[0]===n?a=t[1]:(a=n?_(n.name):null,t[0]=n,t[1]=a);let o=a,s=n&&o?o.displayName||n.name:``,c;t[2]!==s||t[3]!==i?(c=s||i(`assistants.title`),t[2]=s,t[3]=i,t[4]=c):c=t[4];let l;t[5]===i?l=t[6]:(l=i(`assistants.actions.backToChat`),t[5]=i,t[6]=l);let u;return t[7]!==r||t[8]!==c||t[9]!==l?(u=(0,Y.jsx)(z,{title:c,backAriaLabel:l,onBack:r}),t[7]=r,t[8]=c,t[9]=l,t[10]=u):u=t[10],u},Ze=e=>{let t=(0,De.c)(45),{assistantID:n,displayName:r,avatarEmoji:i,modeLabel:a,selected:o,enabled:s,editLabel:c,deleteLabel:l,onSelect:u,onEdit:d,onDelete:f}=e,[p,m]=J.useState(!1),g;t[0]===u?g=t[1]:(g=()=>u(),t[0]=u,t[1]=g);let _=h(g),v;t[2]===d?v=t[3]:(v=()=>{m(!1),d()},t[2]=d,t[3]=v);let y=h(v),b;t[4]===f?b=t[5]:(b=()=>{m(!1),f()},t[4]=f,t[5]=b);let x=h(b),S;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(S=e=>{e.preventDefault(),m(!0)},t[6]=S):S=t[6];let C=o?`border-border/50 bg-[var(--surface-elevated)]`:`border-transparent hover:bg-interactive-hover`,w=!s&&`opacity-65`,T;t[7]!==C||t[8]!==w?(T=fe(`flex w-full min-h-11 items-center gap-3 rounded-xl border px-3 py-3 text-left outline-none transition-[background-color,border-color,transform,opacity] duration-150 ease-out active:scale-[0.995] focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] motion-reduce:transition-none`,C,w),t[7]=C,t[8]=w,t[9]=T):T=t[9];let E;t[10]!==_||t[11]!==o||t[12]!==T?(E=(0,Y.jsx)(`button`,{type:`button`,role:`option`,"aria-selected":o,onClick:_,onContextMenu:S,className:T}),t[10]=_,t[11]=o,t[12]=T,t[13]=E):E=t[13];let D;t[14]!==n||t[15]!==i||t[16]!==r?(D=(0,Y.jsx)(de,{name:n,emoji:i,size:24,label:r}),t[14]=n,t[15]=i,t[16]=r,t[17]=D):D=t[17];let O;t[18]===r?O=t[19]:(O=(0,Y.jsx)(`span`,{className:`block truncate typography-ui-label font-medium`,children:r}),t[18]=r,t[19]=O);let k;t[20]===a?k=t[21]:(k=(0,Y.jsx)(`span`,{className:`mt-0.5 block truncate typography-micro text-muted-foreground`,children:a}),t[20]=a,t[21]=k);let A;t[22]!==O||t[23]!==k?(A=(0,Y.jsxs)(`span`,{className:`min-w-0 flex-1`,children:[O,k]}),t[22]=O,t[23]=k,t[24]=A):A=t[24];let j;t[25]!==A||t[26]!==E||t[27]!==D?(j=(0,Y.jsxs)(Se,{render:E,children:[D,A]}),t[25]=A,t[26]=E,t[27]=D,t[28]=j):j=t[28];let M;t[29]===Symbol.for(`react.memo_cache_sentinel`)?(M=(0,Y.jsx)(pe,{name:`edit`,className:`size-4`}),t[29]=M):M=t[29];let N;t[30]!==c||t[31]!==y?(N=(0,Y.jsxs)(Te,{onClick:y,children:[M,c]}),t[30]=c,t[31]=y,t[32]=N):N=t[32];let P;t[33]===Symbol.for(`react.memo_cache_sentinel`)?(P=(0,Y.jsx)(Ce,{}),t[33]=P):P=t[33];let F;t[34]===Symbol.for(`react.memo_cache_sentinel`)?(F=(0,Y.jsx)(pe,{name:`delete-bin`,className:`size-4`}),t[34]=F):F=t[34];let I;t[35]!==l||t[36]!==x?(I=(0,Y.jsxs)(Te,{onClick:x,className:`text-[var(--status-error)] focus:text-[var(--status-error)] data-[highlighted]:text-[var(--status-error)] [&_svg]:text-[var(--status-error)]`,children:[F,l]}),t[35]=l,t[36]=x,t[37]=I):I=t[37];let L;t[38]!==N||t[39]!==I?(L=(0,Y.jsxs)(we,{className:`min-w-[10rem]`,children:[N,P,I]}),t[38]=N,t[39]=I,t[40]=L):L=t[40];let R;return t[41]!==p||t[42]!==j||t[43]!==L?(R=(0,Y.jsxs)(Ee,{open:p,onOpenChange:m,children:[j,L]}),t[41]=p,t[42]=j,t[43]=L,t[44]=R):R=t[44],R},Qe=({activeOverride:e,onMobileBack:t})=>{let{t:u}=g(),{isMobile:d}=_e(),f=be(),p=J.useSyncExternalStore(n,r,r),E=a(),A=i(e=>e.activeMainTab===`assistant`),M=e??A,L=i(e=>e.isMobile),R=d||L,z=I(),B=ce(),V=B.data,H=le(e=>e.assistantByTransport[p]??null),me=le(e=>e.selectAssistant),he=le(e=>e.requestCreate),U=V?.assistants.find(e=>e.id===H)??null,W=U?.id??``,G=U?.sessionID??``,K=U?.effectiveWorkspacePath??``,q=J.useMemo(()=>o({transportIdentity:p},`assistant:${W}`),[W,p]),Se=l(q),Ce=S(e=>e.draftAttachmentViews[Se]??Ke),we=S(e=>e.drafts[Se]),Te=J.useMemo(()=>{let e=He(we,Ce);return e.length===0?qe:e},[Ce,we]),Ee=we?.attachments,De=we?.syntheticParts,Ae=J.useMemo(()=>!Ee||!De?``:`${Ee.map(e=>e.attachmentRefID).join(``)}\u0004${De.map(e=>`${e.partID}\u0002${e.attachments.map(e=>e.attachmentRefID).join(``)}`).join(``)}`,[Ee,De]);J.useEffect(()=>{W&&S.getState().hydrateDraftAttachments(q)},[W,q,Ae]);let X=P(K||null,{enabled:!!K&&M}),Z=N(K||null,{enabled:!!K&&M}),Pe=O(G,K||void 0),Le=T(G,K||void 0),ze=k(G),Be=xe(),[Ge,Qe]=J.useState(()=>new Map),$e=J.useRef(0),et=J.useRef(Ve()),tt=Ge.get(W)??Je,[,nt]=J.useState(!1),rt=J.useRef(()=>{});rt.current=()=>{m.error(u(`assistants.composer.selectionSaveFailed`))};let it=J.useRef(null);it.current||=new je(nt,e=>rt.current(e));let at=J.useMemo(()=>({assistantID:W,transportIdentity:p,runtimeGeneration:E}),[W,E,p]),Q=it.current,ot=J.useMemo(()=>({transport:p,assistantID:W}),[W,p]),st=h(async(e,t=p)=>{await et.current.clearExclusive({transport:t,assistantID:e})}),ct=h(e=>{et.current.rollbackAllBestEffort(e).catch(()=>void 0)});J.useEffect(()=>{!H&&V?.assistants[0]&&me(V.assistants[0].id)},[me,H,V?.assistants]),J.useEffect(()=>{B.isSuccess&&H&&!U&&me(V?.assistants[0]?.id??null)},[U,me,H,V?.assistants,B.isSuccess]),J.useEffect(()=>{M&&W&&!G&&ae(W)},[M,W,G]),J.useEffect(()=>{$e.current++,Qe(new Map),ct({excludeTransport:p})},[p,E,ct]),J.useEffect(()=>()=>{$e.current++,ct()},[ct]),J.useEffect(()=>{Q.activate(at)},[Q,at]),J.useEffect(()=>()=>{Q.dispose()},[Q]),J.useEffect(()=>{W&&et.current.rollbackAndClearIfBindingMismatch(ot,{assistantID:W,sessionID:G,directory:K,sessionGeneration:U?.sessionGeneration??-1,transport:p,runtimeGeneration:E}).catch(()=>void 0)},[U?.sessionGeneration,W,K,E,G,ot,p]);let lt=h(e=>at.assistantID?Q.enqueue(at,e,async({identity:e,selection:t,signal:n})=>{await Me(e,t,{readSnapshot:()=>te(void 0,e.transportIdentity),ensureSnapshot:e=>re(e),updateAssistant:ne,signal:n,assertAuthoritative:()=>{if(Q.assertAuthoritative(e),r()!==e.transportIdentity||a()!==e.runtimeGeneration)throw new ke}})}):Promise.reject(Error(`assistant_unavailable`))),ut=J.useMemo(()=>ye(Z.data),[Z.data]),dt=h(e=>{if(!U)return;let t=x(ut,U.agent??void 0,e);if(t){let e=w({providerID:U.providerID,modelID:U.modelID,agent:U.agent},t,ut,X.data??[]),n=e.providerID===U.providerID&&e.modelID===U.modelID;lt({...e,agent:e.agent??void 0,variant:n?U.variant??void 0:void 0})}}),$=h(async(e,t)=>{e.sessionID&&await Be.ensureSessionRenderable(e.sessionID,{directory:e.directory,...t?.force?{force:!0}:{}})}),ft=U?.sessionGeneration;J.useEffect(()=>{!M||!W||!G||!K||ft===void 0||$({sessionID:G,directory:K,sessionGeneration:ft},{force:!0})},[M,W,K,$,ft,G]);let pt=h((e,t)=>{if(t.length===0)return;let n=new Set(t);Qe(t=>{let r=t.get(e);if(!r?.some(e=>n.has(e.info.id)))return t;let i=new Map(t),a=r.filter(e=>!n.has(e.info.id));return a.length>0?i.set(e,a):i.delete(e),i})}),mt=h((e,t,n)=>{Qe(r=>{let i=r.get(e);if(!i)return r;let a=Fe(i,t,n);if(a===i)return r;let o=new Map(r);return o.set(e,a),o})}),ht=h(e=>{pt(W,e)}),gt=h(async e=>{if(!G||!K)throw Error(`assistant_unavailable`);D(G,K).some(t=>t?.id===e)&&(await b(G,e,{directory:K,draftKey:q,restorePrimaryInput:!1}),await st(W))}),_t=h(async(e,t)=>{if(!U||U.mode!==`continuous`)return;if(!G||!K)throw Error(`assistant_unavailable`);if(!D(G,K).some(t=>t?.id===e))return;if(t.info.id!==e||t.info.sessionID!==G||t.info.role!==`user`)throw Error(`The selected user message is unavailable`);let n={assistantID:U.id,sessionID:G,directory:K,sessionGeneration:U.sessionGeneration,messageID:e,transport:p,runtimeGeneration:E},i=Re(n),o=await et.current.stageExclusive(i,async()=>{let i=await C(G,e,t,{directory:K,draftKey:q}),o=r()===n.transport&&a()===n.runtimeGeneration,s=o?te(void 0,n.transport)?.assistants.find(e=>e.id===n.assistantID):void 0,c=!!(s&&s.mode===`continuous`&&s.sessionID===n.sessionID&&s.sessionGeneration===n.sessionGeneration&&(s.effectiveWorkspacePath??``)===n.directory);if(o&&c)return{identity:n,rollback:()=>i.rollback()};let l=await i.rollback();return l.status===`failed`||l.status===`skipped`?{identity:n,rollback:()=>i.rollback(),protectOnStale:!0}:null});if(o.kind===`blocked`)throw Error(`assistant_stage_message_edit_prior_blocked`);if(o.kind===`stale_protected`)throw Error(`assistant_stage_message_edit_rollback_failed`)}),vt=J.useMemo(()=>{let e=F(q,()=>S.getState());return{busy:!1,attachments:Te,addAttachment:t=>e.addLocal(t),removeAttachment:t=>e.removeByAttachmentID(t),clearAttachments:()=>e.clearRootAttachments(),restoreAttachments:t=>e.restoreRootAttachments(t),pendingInput:null,consumePendingInput:()=>null,pendingPreset:null,consumePendingPreset:()=>null,consumeSyntheticParts:()=>{let e=S.getState(),t=e.draftAttachmentViews[l(q)]??Ke,n=e.consumeDraftSyntheticParts(q,y);return n?Ue(n,t):null},restoreSyntheticParts:e=>{(async()=>{let t=S.getState(),n=e.map(e=>({partID:e.partID??s(),text:e.text,attachments:[],...e.synthetic===!0?{synthetic:!0}:{}})),r=We(t.getDraft(q)?.syntheticParts??[],n);t.setDraftSyntheticParts(q,r);for(let r=0;r<n.length;r++){let i=n[r].partID;for(let n of e[r]?.attachments??[])n.source===`server`&&n.dataUrl?t.addDraftDurableAttachment(q,{attachmentID:n.id,filename:n.filename,mimeType:n.mimeType,size:n.size,source:`server`,url:n.dataUrl,serverPath:n.serverPath,partID:i}):await t.addDraftLocalAttachment(q,n.file,{attachmentID:n.id,filename:n.filename,source:n.source===`vscode`?`vscode`:`local`,vscodePath:n.vscodePath,vscodeSource:n.vscodeSource===`selection`?`selection`:void 0,partID:i})}})()},inlineDrafts:[],removeInlineDraft:()=>{},restoreInlineDrafts:()=>{},history:ze,captureRuntime:()=>S.getState().captureDraftRuntime(),getDraft:e=>S.getState().getDraft(e),abortPrompt:{sessionID:null,clear:()=>{}}}},[Te,q,ze]),yt=J.useMemo(()=>{let e=(X.data??[]).find(e=>e.id===U?.providerID)?.models?.find(e=>e.id===U?.modelID);return ve(e)},[U?.modelID,U?.providerID,X.data]),bt=Pe.length>0,xt=J.useMemo(()=>{if(!U||!G||!K)return null;let e={sessionID:G,directory:K,sessionGeneration:U.sessionGeneration};return{kind:`secondary`,surfaceID:`assistant:${U.id}`,active:M,sessionID:G,directory:K,draftKey:q,transportIdentity:p,runtimeGeneration:E,deliveryTarget:{kind:`assistant`,assistantID:U.id},resources:vt,...U.mode===`stateless`?{queueSessionID:`assistant:${U.id}`}:{},selection:{value:{providerID:U.providerID,modelID:U.modelID,agent:U.agent??void 0,variant:U.variant??void 0},catalog:{providers:X.data??[],agents:ut,variants:yt,variantsReady:X.isSuccess,ready:X.isSuccess&&Z.isSuccess,loading:X.isPending||Z.isPending,error:X.isError||Z.isError},change:lt,flush:()=>Q.flush(at)},activity:{phase:Le?.type===`busy`?`busy`:Le?.type===`retry`?`retry`:`idle`,canAbort:Le?.type===`busy`||Le?.type===`retry`},commands:{sessionID:G,hasMessages:bt,hasNewDraft:!1},commandPolicy:e=>e.name!==`fork`&&e.name!==`thread`,backend:{send:async t=>{let n;for(let t of D(e.sessionID,e.directory)){let e=typeof t?.id==`string`?t.id:``;/^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/.test(e)&&(!n||e>n)&&(n=e)}let r=c(`msg`,n);t.options?.commitStagedMessageEdit&&et.current.clear({transport:p,assistantID:U.id});let i=j({messageID:r,sessionID:e.sessionID,providerID:t.providerID??U.providerID,modelID:t.modelID??U.modelID,agent:t.agent??U.agent??void 0,text:t.text,attachments:t.attachments,additionalParts:t.parts});Qe(e=>{let t=new Map(e);return t.set(U.id,[...e.get(U.id)??[],i]),t});let a;try{a=await oe(U.id,e,r,Ye(t.text,t.parts,t.attachments))}catch(e){throw pt(U.id,[r]),e}if(z.data?.serverInstanceID){let e=_(U.name);v({serverInstanceID:z.data.serverInstanceID,assistantID:U.id,name:e.displayName||U.name,avatarSeed:U.id,...e.avatarEmoji?{avatarEmoji:e.avatarEmoji}:{}}).catch(()=>void 0)}a.binding.sessionID&&mt(U.id,r,a.binding.sessionID);let o=$e.current;Ie({binding:a.binding,refresh:t=>$(t,{force:t.sessionID!==e.sessionID||t.sessionGeneration!==e.sessionGeneration}),isCurrent:()=>$e.current===o})},sendQueued:async()=>{throw Error(`assistant-server-queue-required`)},create:async()=>{let e=await ee(U.id);await st(U.id),await $(e,{force:!0})},compact:async()=>{let t=await se(U.id,e);await st(U.id),await $(t.binding,{force:!0})},abort:async()=>{await ie(U.id,e)}},shortcuts:{cycle:dt,new:async()=>{let e=await ee(U.id);await st(U.id),await $(e,{force:!0})},abort:async()=>{await ie(U.id,e)},submit:()=>{}}}},[M,U,z.data?.serverInstanceID,lt,st,dt,K,q,bt,X.data,X.isError,X.isPending,X.isSuccess,Z.isError,Z.isPending,Z.isSuccess,$,mt,pt,vt,E,Q,at,G,Le,p,yt,ut]),St=h(()=>{if(he(),f){f.openSettings();return}let e=i.getState();e.setSettingsPage(`assistants`),e.setSettingsDialogOpen(!0)}),Ct=h(e=>{ue(e,f?{openMobileSettings:f.openSettings}:void 0)}),[wt,Tt]=J.useState(null),Et=h(e=>{Tt(e)}),Dt=h(e=>{e||Tt(null)}),Ot=h(()=>{i.getState().setActiveMainTab(`chat`)}),kt=h(()=>{if(t){t();return}Ot()}),At=(e,t,n,r)=>(0,Y.jsxs)(`div`,{className:`flex h-full min-h-0 flex-col`,children:[R?(0,Y.jsx)(Xe,{assistant:U,onBack:kt}):null,(0,Y.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col items-center justify-center px-6 text-center`,children:[(0,Y.jsx)(pe,{name:e,className:`size-6 text-muted-foreground`}),(0,Y.jsx)(`h1`,{className:`mt-4 typography-ui-header font-semibold`,children:t}),n?(0,Y.jsx)(`p`,{className:`mt-2 max-w-md typography-ui text-muted-foreground`,children:n}):null,r?(0,Y.jsx)(`div`,{className:`mt-5`,children:r}):null]})]});if(z.isPending||B.isPending)return At(`ai-agent`,u(`assistants.state.unavailable`));if(z.isError||!z.data?.supported||!z.data.enabled||!V?.enabled)return At(`cloud-off`,u(`assistants.state.unavailable`));if(!V.assistants.length)return At(`ai-agent`,u(`assistants.onboarding.title`),u(`assistants.onboarding.description`),(0,Y.jsx)(ge,{onClick:St,children:u(`assistants.onboarding.action`)}));if(!U)return At(`ai-agent`,u(`assistants.state.unavailable`));if(!xt)return(0,Y.jsxs)(`div`,{className:`flex h-full min-h-0 flex-col bg-background`,children:[R?(0,Y.jsx)(Xe,{assistant:U,onBack:kt}):null,(0,Y.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6`,"aria-busy":`true`,"aria-label":u(`assistants.state.unavailable`),children:[(0,Y.jsx)(`div`,{className:`size-10 animate-pulse rounded-xl bg-[var(--surface-muted)] motion-reduce:animate-none`}),(0,Y.jsx)(`div`,{className:`h-3.5 w-36 animate-pulse rounded-md bg-[var(--surface-muted)] motion-reduce:animate-none`})]})]});let jt=_(U.name),Mt=U.enabled?B.isError?u(`assistants.state.staleSnapshot`):null:u(`assistants.state.assistantDisabled`);return(0,Y.jsxs)(`div`,{className:`relative flex h-full min-h-0 overflow-hidden bg-background`,"data-presentation":`workspace`,children:[R?null:(0,Y.jsxs)(`section`,{className:`flex h-full min-h-0 w-[clamp(16rem,22vw,20rem)] shrink-0 flex-col overflow-hidden`,children:[(0,Y.jsx)(`header`,{className:`shrink-0 px-4 pb-3 pt-4 sm:px-5`,children:(0,Y.jsx)(`h1`,{className:`truncate typography-ui-label font-semibold text-foreground`,children:u(`assistants.title`)})}),(0,Y.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-3 pb-4 sm:px-4`,role:`listbox`,"aria-label":u(`assistants.listAria`),children:(0,Y.jsx)(`div`,{className:`flex flex-col gap-1 border-t border-border/40 pt-3`,children:V.assistants.map(e=>{let t=e.id===H,n=_(e.name);return(0,Y.jsx)(Ze,{assistantID:e.id,displayName:n.displayName,avatarEmoji:n.avatarEmoji??void 0,modeLabel:e.mode===`stateless`?u(`assistants.mode.stateless`):u(`assistants.mode.continuous`),selected:t,enabled:e.enabled,editLabel:u(`assistants.menu.edit`),deleteLabel:u(`assistants.settings.delete`),onSelect:()=>me(e.id),onEdit:()=>Ct(e.id),onDelete:()=>Et(e)},e.id)})})})]}),(0,Y.jsx)(Ne,{assistant:wt,open:wt!==null,onOpenChange:Dt}),(0,Y.jsxs)(`div`,{className:fe(`flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background`,!R&&`border-l border-border/60`),children:[R?(0,Y.jsx)(Xe,{assistant:U,onBack:kt}):(0,Y.jsxs)(`header`,{className:`flex h-14 shrink-0 items-center gap-3 border-b border-border/40 px-4 sm:px-5`,children:[(0,Y.jsx)(de,{name:U.id,emoji:jt.avatarEmoji,size:24,label:jt.displayName||U.name}),(0,Y.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,Y.jsx)(`div`,{className:`truncate typography-ui-label font-medium`,children:jt.displayName}),(0,Y.jsx)(`div`,{className:`mt-0.5 truncate typography-micro leading-none text-muted-foreground/70`,children:U.mode===`stateless`?u(`assistants.conversation.statelessHint`):u(`assistants.conversation.continuousHint`)})]})]}),(0,Y.jsx)(Oe,{onRevertMessage:gt,onEditMessage:U.mode===`continuous`?_t:void 0,assistant:U,sessionID:G,warning:Mt,surface:xt,pendingUserMessages:tt,onPendingUserMessagesMaterialized:ht})]})]})};export{Ge as n,Ne as r,Qe as t};