@openchambery/web 1.19.7-beta.2 → 1.19.7-beta.20

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 (185) hide show
  1. package/dist/assets/AgentAvatar-DI2zs2WH.js +1 -0
  2. package/dist/assets/AssistantView-C0p-8f2B.js +2 -0
  3. package/dist/assets/DiagramView-CSXRZQ5S.js +1 -0
  4. package/dist/assets/FadeInOnReveal-BWJcUp5P.js +1 -0
  5. package/dist/assets/Icon-4My8YZwt.js +1 -0
  6. package/dist/assets/MarkdownRendererImpl-C6rkoZYH.js +7 -0
  7. package/dist/assets/{MarkstreamRendererImpl-CR0fBG5w.css → MarkstreamRendererImpl-Ca7RATbs.css} +1 -1
  8. package/dist/assets/MarkstreamRendererImpl-CowOCU5W.js +1 -0
  9. package/dist/assets/MobileDetailNavigation-ACMbLTqb.js +1 -0
  10. package/dist/assets/MobileShareBridge-CXR-B1xW.js +4 -0
  11. package/dist/assets/MobileSurface-CKBSKqUH.js +2 -0
  12. package/dist/assets/{MultiRunWindow-khpnWyZO.js → MultiRunWindow-el9_nl25.js} +1 -1
  13. package/dist/assets/OnboardingScreen-BNEtPmvw.js +1 -0
  14. package/dist/assets/OverlayScrollbar-BZB21Yp_.js +2 -0
  15. package/dist/assets/SettingsView-CVSiIRia.js +25 -0
  16. package/dist/assets/{SettingsWindow-Dx5rU9v5.js → SettingsWindow-BXYswtp5.js} +1 -1
  17. package/dist/assets/TerminalView-Bh0IQwa5.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BfuXazW9.js → ToolOutputDialog-XeIJ2etT.js} +3 -3
  19. package/dist/assets/appThemeRegistry-rSdmh4sW.js +2 -0
  20. package/dist/assets/{button-z0GOBtWi.js → button-Or0daT9o.js} +1 -1
  21. package/dist/assets/clipboard-BXxLshVq.js +1 -0
  22. package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-C-kzSGkq.js} +1 -1
  23. package/dist/assets/context-menu-Dpjla5p4.js +1 -0
  24. package/dist/assets/desktopHosts-BzSE0sK7.js +1 -0
  25. package/dist/assets/desktopRelayRestore-BaKjk1kQ.js +1 -0
  26. package/dist/assets/{diagram-CoEqmReY.js → diagram-D0mT3mo1.js} +1 -1
  27. package/dist/assets/{dialog-HCE4eLUW.js → dialog-BkhHBTd4.js} +1 -1
  28. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  29. package/dist/assets/{es-D9KcV5TX.js → es-BHqnSjoZ.js} +1 -1
  30. package/dist/assets/fileQueries-DFY6Kiy1.js +1 -0
  31. package/dist/assets/fileReferenceActions-3pPDAJSl.js +1 -0
  32. package/dist/assets/{fr-DPA9qUl3.js → fr-BfrlJY9i.js} +1 -1
  33. package/dist/assets/{gitApi-DNTMAa3F.js → gitApi-Uvb9w70Y.js} +3 -3
  34. package/dist/assets/{input-BG3VWTIF.js → input-BUsGnzwe.js} +1 -1
  35. package/dist/assets/insertionBoundaries-Bgec5nsP.js +4 -0
  36. package/dist/assets/{ja-Dsif3NoD.js → ja-DP-7qt-x.js} +1 -1
  37. package/dist/assets/{ko-Dixx4Ctg.js → ko-E2B5vGQP.js} +1 -1
  38. package/dist/assets/main-COfQt8Sj.js +169 -0
  39. package/dist/assets/main-PPlpRj1-.js +2 -0
  40. package/dist/assets/markdown-worker-wEFz5Hm_.js +3 -0
  41. package/dist/assets/markdownHeightCache-Bj707yG-.js +2 -0
  42. package/dist/assets/markdownTheme-CVS01wHf.js +2 -0
  43. package/dist/assets/miniChat-EZGRqQZr.js +2 -0
  44. package/dist/assets/mobile-DQNqMo5J.js +2 -0
  45. package/dist/assets/multirun-EViX73vm.js +4 -0
  46. package/dist/assets/outsideFileGrants-BB4GPvCq.js +1 -0
  47. package/dist/assets/path-utils-jGozTg9d.js +1 -0
  48. package/dist/assets/persistence-CSZp1CNB.js +3 -0
  49. package/dist/assets/{pl-Bj78ZGgf.js → pl-r8X5gRM5.js} +1 -1
  50. package/dist/assets/platform-CenlWqxu.js +2 -0
  51. package/dist/assets/projectMeta-Bv1L_nEb.js +3 -0
  52. package/dist/assets/{pt-BR-CFL3xSAf.js → pt-BR-DcpdXCf5.js} +1 -1
  53. package/dist/assets/renderElectronMiniChatApp-Nsq4Wc4s.js +2 -0
  54. package/dist/assets/renderMobileApp-5WJDYaMR.js +28 -0
  55. package/dist/assets/runtimeConfig-DKUQS4iY.js +2 -0
  56. package/dist/assets/runtimeEndpointReset-BMeu9Hqj.js +147 -0
  57. package/dist/assets/sessionChangeSummary-CX2MJIkc.js +1 -0
  58. package/dist/assets/sessionLookup-Sb2HN3G2.js +1 -0
  59. package/dist/assets/sortable-tabs-strip-C5gGnikj.js +1 -0
  60. package/dist/assets/src-Af9cMFKF.css +2 -0
  61. package/dist/assets/{store-DzpbXWX1.js → store-BDFdnk6l.js} +2 -2
  62. package/dist/assets/terminalApi-CIIs9vFx.js +1 -0
  63. package/dist/assets/textarea-CIO34TZz.js +1 -0
  64. package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BSrJYYYc.js} +1 -1
  65. package/dist/assets/{uk-BY60Ub3Z.js → uk-Di0vl9V-.js} +1 -1
  66. package/dist/assets/url-CE_gAfNa.js +1 -0
  67. package/dist/assets/{ChatContainer-CnOrdAgs.js → useAppFontEffects-B_6vc0u3.js} +46 -41
  68. package/dist/assets/useAssistantUIStore-BHacrxe_.js +1 -0
  69. package/dist/assets/useDesktopWindowControlsLayout-B2L5PWHW.js +1 -0
  70. package/dist/assets/useEffectiveDirectory-CBd5vt8N.js +1 -0
  71. package/dist/assets/useFontPreferences-BxOHKuVw.js +1 -0
  72. package/dist/assets/useMobileNavigationStore-CUGaydRW.js +412 -0
  73. package/dist/assets/useSessionAutoCleanup-Clhzc1wn.js +1 -0
  74. package/dist/assets/useUIStore-CqqO11ZI.js +1 -0
  75. package/dist/assets/useWorkerHighlightedLines-C4iQlbPQ.js +7 -0
  76. package/dist/assets/utils-B0mQXN9L.js +1 -0
  77. package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
  78. package/dist/assets/{zh-CN-Cox1LaOW.js → zh-CN-coFCfeQE.js} +1 -1
  79. package/dist/assets/{zh-TW-B6RU0mxa.js → zh-TW-oDRZ1-yb.js} +1 -1
  80. package/dist/index.html +9 -11
  81. package/dist/mini-chat.html +9 -11
  82. package/dist/mobile.html +15 -17
  83. package/package.json +2 -1
  84. package/server/index.js +2 -0
  85. package/server/lib/assistant-delivery-parts.js +3 -1
  86. package/server/lib/assistant-delivery-parts.test.js +11 -0
  87. package/server/lib/assistants/DOCUMENTATION.md +76 -3
  88. package/server/lib/assistants/assign.js +225 -0
  89. package/server/lib/assistants/assign.test.js +118 -0
  90. package/server/lib/assistants/bubbles.js +45 -0
  91. package/server/lib/assistants/bubbles.test.js +36 -0
  92. package/server/lib/assistants/cards.js +148 -0
  93. package/server/lib/assistants/cards.test.js +116 -0
  94. package/server/lib/assistants/contact-store.js +284 -0
  95. package/server/lib/assistants/contact-store.test.js +131 -0
  96. package/server/lib/assistants/contact-tools.js +761 -0
  97. package/server/lib/assistants/contact-tools.test.js +328 -0
  98. package/server/lib/assistants/contracts.js +20 -1
  99. package/server/lib/assistants/harness.js +551 -0
  100. package/server/lib/assistants/harness.test.js +607 -0
  101. package/server/lib/assistants/routes.js +44 -4
  102. package/server/lib/assistants/service.js +930 -17
  103. package/server/lib/assistants/service.test.js +1163 -45
  104. package/server/lib/llm/DOCUMENTATION.md +84 -0
  105. package/server/lib/llm/catalog.js +100 -0
  106. package/server/lib/llm/catalog.test.js +78 -0
  107. package/server/lib/llm/completions.js +151 -0
  108. package/server/lib/llm/completions.test.js +127 -0
  109. package/server/lib/llm/generate.js +570 -0
  110. package/server/lib/llm/generate.test.js +459 -0
  111. package/server/lib/llm/routes.js +47 -0
  112. package/server/lib/llm/temp-directory.js +37 -0
  113. package/server/lib/notifications/APNS.md +4 -4
  114. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  115. package/server/lib/notifications/apns-runtime.js +139 -20
  116. package/server/lib/notifications/apns-runtime.test.js +66 -7
  117. package/server/lib/notifications/apns-titles.js +62 -62
  118. package/server/lib/notifications/apns-titles.test.js +3 -1
  119. package/server/lib/notifications/routes.js +1 -1
  120. package/server/lib/notifications/runtime.js +81 -49
  121. package/server/lib/notifications/runtime.test.js +61 -0
  122. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  123. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  124. package/server/lib/opencode/settings-runtime.js +18 -2
  125. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  126. package/server/lib/session-goal/runtime.js +20 -0
  127. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  128. package/server/lib/session-index/service.js +10 -4
  129. package/server/lib/session-index/service.test.js +37 -0
  130. package/server/lib/session-title/runtime.js +1 -0
  131. package/server/lib/session-title/runtime.test.js +11 -0
  132. package/dist/assets/AgentAvatar-ziUs4sw3.js +0 -1
  133. package/dist/assets/AssistantView-DF4yXFgS.js +0 -1
  134. package/dist/assets/ContextPanel-Cg_uCHzJ.js +0 -285
  135. package/dist/assets/DiagramView-DYh3Zn2y.js +0 -1
  136. package/dist/assets/FadeInOnReveal-BAlsRFJH.js +0 -1
  137. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  138. package/dist/assets/MarkdownRendererImpl-QzUEpBEm.js +0 -7
  139. package/dist/assets/MarkstreamRendererImpl-CXrfeUia.js +0 -1
  140. package/dist/assets/MobileDetailNavigation-DJXEYfq4.js +0 -1
  141. package/dist/assets/MobileShareBridge-CliVo1YH.js +0 -4
  142. package/dist/assets/MobileSurface-9Xh6WQkY.js +0 -2
  143. package/dist/assets/OnboardingScreen-CJYyR54B.js +0 -1
  144. package/dist/assets/OverlayScrollbar-C-jZ8ITG.js +0 -2
  145. package/dist/assets/SettingsView-DyCZehFG.js +0 -25
  146. package/dist/assets/TerminalView-XSALfUaH.js +0 -8
  147. package/dist/assets/appThemeRegistry-DDWrniMM.js +0 -2
  148. package/dist/assets/clipboard-Bz0Zm9VJ.js +0 -1
  149. package/dist/assets/desktop-CfLlmafK.js +0 -1
  150. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  151. package/dist/assets/desktopRelayRestore-LBt0CBOj.js +0 -1
  152. package/dist/assets/fileQueries-Dnn15WsA.js +0 -1
  153. package/dist/assets/insertionBoundaries-DMxuUnL_.js +0 -4
  154. package/dist/assets/main-B9HVtgXE.js +0 -2
  155. package/dist/assets/main-ZNyIs3Sn.js +0 -29
  156. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  157. package/dist/assets/markdownHeightCache-CvUyZT4q.js +0 -1
  158. package/dist/assets/markdownTheme-TdhQOZSN.js +0 -2
  159. package/dist/assets/miniChat-q0T0WGkZ.js +0 -2
  160. package/dist/assets/mobile-DEduvLJj.js +0 -2
  161. package/dist/assets/multirun-DU-wrxlG.js +0 -4
  162. package/dist/assets/persistence-CmjKImz2.js +0 -3
  163. package/dist/assets/platform-skqcMARt.js +0 -2
  164. package/dist/assets/projectMeta-DNWx6Uva.js +0 -3
  165. package/dist/assets/radio-BEonIX1k.js +0 -1
  166. package/dist/assets/renderElectronMiniChatApp--JYWY41L.js +0 -2
  167. package/dist/assets/renderMobileApp-B0Ix_uNo.js +0 -25
  168. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  169. package/dist/assets/runtimeConfig-BTDIG7mX.js +0 -2
  170. package/dist/assets/runtimeEndpointReset-CZQCYtVI.js +0 -4
  171. package/dist/assets/shikiHighlight-C9Cx56Qg.js +0 -1
  172. package/dist/assets/sortable-tabs-strip-Bj8N0VWf.js +0 -1
  173. package/dist/assets/src-2eFAiBLq.css +0 -2
  174. package/dist/assets/terminalApi-CNqqwjfG.js +0 -1
  175. package/dist/assets/url-KeihbE0d.js +0 -1
  176. package/dist/assets/useAppFontEffects-CKLSamae.js +0 -7
  177. package/dist/assets/useAssistantUIStore-DaplPyb0.js +0 -1
  178. package/dist/assets/useDesktopWindowControlsLayout-DJSkFfhT.js +0 -1
  179. package/dist/assets/useEffectiveDirectory-B3rbY1Kv.js +0 -1
  180. package/dist/assets/useFontPreferences-D8emhcNv.js +0 -1
  181. package/dist/assets/useMobileNavigationStore-CAvVUItx.js +0 -412
  182. package/dist/assets/useSessionAutoCleanup-DxNgqJ8s.js +0 -1
  183. package/dist/assets/useUIStore-B7KJS5G7.js +0 -1
  184. package/dist/assets/useWorkerHighlightedLines-DiDF68LR.js +0 -7
  185. package/dist/assets/utils-CTL16t2f.js +0 -1
@@ -273,40 +273,6 @@ export const createNotificationTriggerRuntime = (deps) => {
273
273
  return trimmed.length > 0 ? trimmed : undefined;
274
274
  };
275
275
 
276
- const formatMode = (raw) => {
277
- const value = typeof raw === 'string' ? raw.trim() : '';
278
- const normalized = value.length > 0 ? value : 'agent';
279
- return normalized
280
- .split(/[-_\s]+/)
281
- .filter(Boolean)
282
- .map((token) => token.charAt(0).toUpperCase() + token.slice(1))
283
- .join(' ');
284
- };
285
-
286
- const formatModelId = (raw) => {
287
- const value = typeof raw === 'string' ? raw.trim() : '';
288
- if (!value) {
289
- return 'Assistant';
290
- }
291
-
292
- const tokens = value.split(/[-_]+/).filter(Boolean);
293
- const result = [];
294
- for (let i = 0; i < tokens.length; i += 1) {
295
- const current = tokens[i];
296
- const next = tokens[i + 1];
297
- if (/^\d+$/.test(current) && next && /^\d+$/.test(next)) {
298
- result.push(`${current}.${next}`);
299
- i += 1;
300
- continue;
301
- }
302
- result.push(current);
303
- }
304
-
305
- return result
306
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
307
- .join(' ');
308
- };
309
-
310
276
  // A session with an ACTIVE goal suppresses per-turn ready notifications;
311
277
  // the session-goal runtime sends its own notification when the goal
312
278
  // settles. Fetch failures fall through to normal notification behavior.
@@ -338,6 +304,20 @@ export const createNotificationTriggerRuntime = (deps) => {
338
304
 
339
305
  const sessionId = extractSessionIdFromPayload(payload);
340
306
  const notificationDirectory = extractDirectoryFromPayload(payload);
307
+ if ((payload.type === 'session.updated' || payload.type === 'session.created') && sessionId) {
308
+ const title = payload.properties?.info?.title;
309
+ if (
310
+ typeof title === 'string'
311
+ && title.trim()
312
+ && !(await shouldSkipSystemSessionNotification(sessionId, notificationDirectory))
313
+ ) {
314
+ try {
315
+ await sendLiveActivityEnd?.({ sessionId, title: title.trim() });
316
+ } catch (error) {
317
+ console.warn('[Live Activity] title update failed:', error?.message ?? error);
318
+ }
319
+ }
320
+ }
341
321
  if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
342
322
  const error = payload.properties?.error;
343
323
  const errorText = typeof error?.message === 'string'
@@ -404,13 +384,13 @@ export const createNotificationTriggerRuntime = (deps) => {
404
384
  }
405
385
  lastReadyNotificationAt.set(sessionId, now);
406
386
 
407
- let title = `${formatMode(info?.mode)} agent is ready`;
408
- let body = `${formatModelId(info?.modelID)} completed the task`;
387
+ let title = 'Task completed';
388
+ let body = '';
409
389
  let sessionName = '';
410
390
 
411
391
  try {
412
392
  const templates = settings.notificationTemplates || {};
413
- const completionTemplate = templates.completion || { title: '{agent_name} is ready', message: '{model_name} completed the task' };
393
+ const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
414
394
 
415
395
  const variables = await buildTemplateVariables(payload, sessionId);
416
396
  sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
@@ -430,8 +410,10 @@ export const createNotificationTriggerRuntime = (deps) => {
430
410
  const resolvedBody = resolveNotificationTemplate(completionTemplate.message, variables);
431
411
  if (resolvedTitle) title = resolvedTitle;
432
412
  if (shouldApplyResolvedTemplateMessage(completionTemplate.message, resolvedBody, variables)) body = resolvedBody;
413
+ if (!body) body = sessionName || resolvedBody || 'Session';
433
414
  } catch (error) {
434
415
  console.warn('[Notification] Template resolution failed, using defaults:', error?.message || error);
416
+ if (!body) body = sessionName || 'Session';
435
417
  }
436
418
 
437
419
  if (settings.nativeNotificationsEnabled) {
@@ -492,12 +474,8 @@ export const createNotificationTriggerRuntime = (deps) => {
492
474
  const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
493
475
  const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
494
476
 
495
- let title = /plan\s*mode/i.test(header)
496
- ? 'Switch to plan mode'
497
- : /build\s*agent/i.test(header)
498
- ? 'Switch to build mode'
499
- : header || 'Input needed';
500
- let body = questionText || 'Agent is waiting for your response';
477
+ let title = 'Needs your answer';
478
+ let body = '';
501
479
  let sessionName = '';
502
480
 
503
481
  try {
@@ -506,14 +484,16 @@ export const createNotificationTriggerRuntime = (deps) => {
506
484
  variables.last_message = questionText || header || '';
507
485
 
508
486
  const templates = settings.notificationTemplates || {};
509
- const questionTemplate = templates.question || { title: 'Input needed', message: '{last_message}' };
487
+ const questionTemplate = templates.question || { title: 'Needs your answer', message: '{session_name}' };
510
488
 
511
489
  const resolvedTitle = resolveNotificationTemplate(questionTemplate.title, variables);
512
490
  const resolvedBody = resolveNotificationTemplate(questionTemplate.message, variables);
513
491
  if (resolvedTitle) title = resolvedTitle;
514
492
  if (shouldApplyResolvedTemplateMessage(questionTemplate.message, resolvedBody, variables)) body = resolvedBody;
493
+ if (!body) body = sessionName || resolvedBody || 'Session';
515
494
  } catch (error) {
516
495
  console.warn('[Notification] Question template resolution failed, using defaults:', error?.message || error);
496
+ if (!body) body = sessionName || 'Session';
517
497
  }
518
498
 
519
499
  if (settings.nativeNotificationsEnabled) {
@@ -618,8 +598,8 @@ export const createNotificationTriggerRuntime = (deps) => {
618
598
  ? sessionTitle.trim()
619
599
  : permissionText || 'Agent is waiting for your approval';
620
600
 
621
- let title = 'Permission required';
622
- let body = fallbackMessage;
601
+ let title = 'Needs permission';
602
+ let body = '';
623
603
  let sessionName = '';
624
604
 
625
605
  try {
@@ -628,14 +608,14 @@ export const createNotificationTriggerRuntime = (deps) => {
628
608
  variables.last_message = fallbackMessage;
629
609
 
630
610
  const templates = settings.notificationTemplates || {};
631
- const questionTemplate = templates.question || { title: 'Permission required', message: '{last_message}' };
611
+ const questionTemplate = templates.question || { title: 'Needs permission', message: '{session_name}' };
632
612
 
633
- const resolvedTitle = resolveNotificationTemplate(questionTemplate.title, variables);
634
613
  const resolvedBody = resolveNotificationTemplate(questionTemplate.message, variables);
635
- if (resolvedTitle) title = resolvedTitle;
636
614
  if (shouldApplyResolvedTemplateMessage(questionTemplate.message, resolvedBody, variables)) body = resolvedBody;
615
+ if (!body) body = sessionName || fallbackMessage || 'Session';
637
616
  } catch (error) {
638
617
  console.warn('[Notification] Permission template resolution failed, using defaults:', error?.message || error);
618
+ if (!body) body = sessionName || fallbackMessage || 'Session';
639
619
  }
640
620
 
641
621
  if (settings.nativeNotificationsEnabled) {
@@ -713,6 +693,57 @@ export const createNotificationTriggerRuntime = (deps) => {
713
693
  );
714
694
  };
715
695
 
696
+ const contactNotificationTitle = (name) => {
697
+ if (typeof name !== 'string' || !name.trim()) return 'Assistant';
698
+ const stripped = name.replace(/^(?:\p{Extended_Pictographic}|\p{Regional_Indicator}|[\uFE0F\u200D])+\s*/u, '').trim();
699
+ return stripped || name.trim();
700
+ };
701
+
702
+ const sendContactTurnNotification = async ({ assistantID, name, body, status } = {}) => {
703
+ if (typeof assistantID !== 'string' || !assistantID.trim()) return;
704
+ const settings = await readSettingsFromDisk();
705
+ if (settings.notifyOnCompletion === false) return;
706
+
707
+ const title = contactNotificationTitle(name);
708
+ const rawBody = typeof body === 'string' ? body : '';
709
+ let text = '';
710
+ try {
711
+ text = await prepareNotificationLastMessage({ message: rawBody, settings });
712
+ } catch {
713
+ text = '';
714
+ }
715
+ if (!text) text = status === 'error' ? 'Something went wrong' : 'New message';
716
+ const tag = `contact-${assistantID.trim()}`;
717
+
718
+ if (settings.nativeNotificationsEnabled) {
719
+ const notificationPayload = {
720
+ title,
721
+ body: text,
722
+ tag,
723
+ kind: 'ready',
724
+ assistantID: assistantID.trim(),
725
+ requireHidden: false,
726
+ };
727
+ const desktopNotificationDelivered = emitDesktopNotification(notificationPayload);
728
+ broadcastUiNotification(notificationPayload, { desktopNotificationDelivered });
729
+ }
730
+
731
+ await fanoutPush(
732
+ {
733
+ title,
734
+ body: text,
735
+ tag,
736
+ data: {
737
+ url: `/?assistant=${encodeURIComponent(assistantID.trim())}`,
738
+ assistantID: assistantID.trim(),
739
+ sessionName: title,
740
+ type: 'ready',
741
+ },
742
+ },
743
+ { requireNoSse: true },
744
+ );
745
+ };
746
+
716
747
  return {
717
748
  maybeSendPushForTrigger,
718
749
  setAutoAcceptSession,
@@ -720,5 +751,6 @@ export const createNotificationTriggerRuntime = (deps) => {
720
751
  setGetIsSessionAutoAccepting,
721
752
  clearPendingPushBadge,
722
753
  sendGoalSettlePush,
754
+ sendContactTurnNotification,
723
755
  };
724
756
  };
@@ -199,6 +199,45 @@ const completionPayload = (sessionId = 'ses_root', finish = 'stop') => ({
199
199
  },
200
200
  });
201
201
 
202
+ describe('contact turn notifications', () => {
203
+ it('sends an SMS-style title and body when a contact turn completes', async () => {
204
+ const { runtime, emitDesktopNotification, broadcastUiNotification, sendPushToAllUiSessions } = createRuntime();
205
+ await runtime.sendContactTurnNotification({
206
+ assistantID: 'asst_1',
207
+ name: '大小白',
208
+ body: '我去找一下',
209
+ status: 'complete',
210
+ });
211
+ expect(emitDesktopNotification).toHaveBeenCalledWith(expect.objectContaining({
212
+ title: '大小白',
213
+ body: '我去找一下',
214
+ tag: 'contact-asst_1',
215
+ kind: 'ready',
216
+ assistantID: 'asst_1',
217
+ }));
218
+ expect(broadcastUiNotification).toHaveBeenCalled();
219
+ expect(sendPushToAllUiSessions).toHaveBeenCalledWith(expect.objectContaining({
220
+ title: '大小白',
221
+ body: '我去找一下',
222
+ data: expect.objectContaining({ assistantID: 'asst_1', type: 'ready' }),
223
+ }), { requireNoSse: true });
224
+ });
225
+
226
+ it('skips contact notifications when completion notices are disabled', async () => {
227
+ const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime({
228
+ readSettingsFromDisk: vi.fn(async () => ({ ...defaultSettings, notifyOnCompletion: false })),
229
+ });
230
+ await runtime.sendContactTurnNotification({
231
+ assistantID: 'asst_1',
232
+ name: '大小白',
233
+ body: 'hi',
234
+ status: 'complete',
235
+ });
236
+ expect(emitDesktopNotification).not.toHaveBeenCalled();
237
+ expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
238
+ });
239
+ });
240
+
202
241
  describe('notification trigger live activity end', () => {
203
242
  it('ends the live activity on top-level completion and error', async () => {
204
243
  vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
@@ -232,6 +271,28 @@ describe('notification trigger live activity end', () => {
232
271
  expect(sendApnsToAllUiSessions).toHaveBeenCalledTimes(1);
233
272
  });
234
273
 
274
+ it('does not patch live activity titles for child sessions', async () => {
275
+ vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
276
+ id: 'ses_child',
277
+ parentID: 'ses_parent',
278
+ metadata: {},
279
+ })));
280
+ const { runtime, sendLiveActivityEnd } = createRuntime();
281
+ await runtime.maybeSendPushForTrigger({
282
+ type: 'session.updated',
283
+ properties: {
284
+ directory: '/repo',
285
+ info: {
286
+ id: 'ses_child',
287
+ sessionID: 'ses_child',
288
+ parentID: 'ses_parent',
289
+ title: 'Fixer',
290
+ },
291
+ },
292
+ });
293
+ expect(sendLiveActivityEnd).not.toHaveBeenCalled();
294
+ });
295
+
235
296
  it('suppresses live activity end for child and small-model sessions', async () => {
236
297
  const fetchMock = vi.fn(async (url) => {
237
298
  if (String(url).includes('ses_child')) {
@@ -15,6 +15,7 @@ import { registerScheduledTaskToolRoute } from '../scheduled-tasks/managed-tool-
15
15
  import { registerConversationRoutes } from '../conversations/routes.js';
16
16
  import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';
17
17
  import { registerAssistantRoutes } from '../assistants/routes.js';
18
+ import { registerLlmRoutes } from '../llm/routes.js';
18
19
  import { registerMessageQueueRoutes } from '../message-queue/routes.js';
19
20
  import { registerSkillRoutes } from './skill-routes.js';
20
21
  import { registerPluginRoutes } from './plugin-routes.js';
@@ -126,6 +127,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
126
127
  broadcastGlobalUiEvent,
127
128
  globalMessageStreamHub,
128
129
  getServerId,
130
+ sessionIndexService,
131
+ notifyContactTurnComplete,
129
132
  } = routeDependencies;
130
133
 
131
134
  registerSettingsUtilityRoutes(app, {
@@ -215,6 +218,15 @@ export const createFeatureRoutesRuntime = (dependencies) => {
215
218
  getOpenChamberEventClients,
216
219
  writeSseEvent,
217
220
  });
221
+ // Contact turn lifecycle / bubble deltas — same SSE bus, not revision watermark.
222
+ const broadcastContactTurnEvent = createOpenChamberEventBroadcaster({
223
+ getOpenChamberEventClients,
224
+ writeSseEvent,
225
+ });
226
+ registerLlmRoutes(app, {
227
+ buildOpenCodeUrl,
228
+ getOpenCodeAuthHeaders,
229
+ });
218
230
  assistantRoutesRuntime = registerAssistantRoutes(app, {
219
231
  openchamberDataDir,
220
232
  dbPath: path.join(openchamberDataDir, 'assistants.sqlite'),
@@ -222,12 +234,30 @@ export const createFeatureRoutesRuntime = (dependencies) => {
222
234
  getOpenCodeAuthHeaders,
223
235
  getServerId,
224
236
  getAllowedRoots: () => assistantAllowedRoots,
237
+ listProjects: async () => {
238
+ const settings = await readSettingsFromDiskMigrated();
239
+ return sanitizeProjects(settings?.projects ?? [])
240
+ .filter((project) => project && typeof project.id === 'string' && typeof project.path === 'string')
241
+ .map((project) => ({
242
+ id: project.id,
243
+ path: project.path,
244
+ ...(typeof project.label === 'string' && project.label.trim()
245
+ ? { label: project.label.trim() }
246
+ : {}),
247
+ }));
248
+ },
249
+ listScheduledTasks: (projectID) => projectConfigRuntime.listScheduledTasks(projectID),
250
+ sessionIndexService,
251
+ upsertScheduledTask: (projectID, task) => projectConfigRuntime.upsertScheduledTask(projectID, task),
252
+ syncScheduledTaskProject: (projectID) => scheduledTasksRuntime.syncProject(projectID),
225
253
  refreshAllowedRoots: refreshAssistantAllowedRoots,
226
254
  globalEventHub: globalMessageStreamHub,
227
255
  onRevisionTip: (tip) => broadcastAssistantRevisionTip({
228
256
  type: 'openchamber:assistants-changed',
229
257
  properties: tip,
230
258
  }),
259
+ onContactTurnEvent: (event) => broadcastContactTurnEvent(event),
260
+ onContactTurnComplete: (event) => notifyContactTurnComplete?.(event),
231
261
  });
232
262
  messageQueueRuntime?.setAssistantDeliveryService?.(assistantRoutesRuntime.service);
233
263
 
@@ -3,6 +3,26 @@ import { describe, expect, it, vi } from 'vitest';
3
3
  import { createWorktreeTopologyBroadcaster } from './feature-routes-runtime.js';
4
4
 
5
5
  describe('feature routes runtime composition', () => {
6
+ it('wires scheduled-task upsert into assistant contact tools', async () => {
7
+ const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
8
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*upsertScheduledTask:[\s\S]*projectConfigRuntime\.upsertScheduledTask/);
9
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*syncScheduledTaskProject:[\s\S]*scheduledTasksRuntime\.syncProject/);
10
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listProjects:/);
11
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listScheduledTasks:[\s\S]*projectConfigRuntime\.listScheduledTasks/);
12
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*sessionIndexService/);
13
+ expect(source).toMatch(/label: project\.label\.trim\(\)/);
14
+ });
15
+
16
+ it('fans contact turn events out on the OpenChamber SSE bus separately from revision tips', async () => {
17
+ const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
18
+ expect(source).toMatch(/const broadcastContactTurnEvent = createOpenChamberEventBroadcaster\(/);
19
+ expect(source).toMatch(/onContactTurnEvent: \(event\) => broadcastContactTurnEvent\(event\)/);
20
+ expect(source).toMatch(/onContactTurnComplete: \(event\) => notifyContactTurnComplete\?\.\(event\)/);
21
+ expect(source).toMatch(/onRevisionTip: \(tip\) => broadcastAssistantRevisionTip\(\{\s*type: 'openchamber:assistants-changed'/);
22
+ // Contact turn envelopes stay off the assistants-changed revision watermark path.
23
+ expect(source).not.toMatch(/onRevisionTip:[\s\S]*contact-turn/);
24
+ });
25
+
6
26
  it('registers the managed scheduled-task tool route with its required dependencies', async () => {
7
27
  const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
8
28
  expect(source).toContain("import { registerScheduledTaskToolRoute } from '../scheduled-tasks/managed-tool-route.js';");
@@ -1,6 +1,13 @@
1
1
  import { createProjectIdFromPath } from '../projects/project-id.js';
2
2
 
3
3
  const DEFAULT_NOTIFICATION_TEMPLATES = {
4
+ completion: { title: 'Task completed', message: '{session_name}' },
5
+ error: { title: 'Something went wrong', message: '{session_name}' },
6
+ question: { title: 'Needs your answer', message: '{session_name}' },
7
+ subtask: { title: 'Task completed', message: '{session_name}' },
8
+ };
9
+
10
+ const LEGACY_NOTIFICATION_TEMPLATES = {
4
11
  completion: { title: '{agent_name} is ready', message: '{model_name} completed the task' },
5
12
  error: { title: 'Tool error', message: '{last_message}' },
6
13
  question: { title: 'Input needed', message: '{last_message}' },
@@ -20,6 +27,15 @@ const ensureNotificationTemplateShape = (templates) => {
20
27
  if (!currentEntry || typeof currentEntry.title !== 'string' || typeof currentEntry.message !== 'string') {
21
28
  changed = true;
22
29
  }
30
+ const legacy = LEGACY_NOTIFICATION_TEMPLATES[event];
31
+ const isLegacyDefault = legacy
32
+ && currentTitle === legacy.title
33
+ && currentMessage === legacy.message;
34
+ if (isLegacyDefault) {
35
+ changed = true;
36
+ next[event] = { title: base.title, message: base.message };
37
+ continue;
38
+ }
23
39
  next[event] = { title: currentTitle, message: currentMessage };
24
40
  }
25
41
 
@@ -650,8 +666,8 @@ export const createSettingsRuntime = (deps) => {
650
666
  return { settings, changed: false };
651
667
  }
652
668
 
653
- const defaultLight = 'flexoki-light';
654
- const defaultDark = 'flexoki-dark';
669
+ const defaultLight = 'carbonfox-light';
670
+ const defaultDark = 'vitesse-dark-dark';
655
671
 
656
672
  let nextLightThemeId = hasLight ? settings.lightThemeId : undefined;
657
673
  let nextDarkThemeId = hasDark ? settings.darkThemeId : undefined;
@@ -135,7 +135,10 @@ before touching the filesystem). Rationale: metadata rides every
135
135
  continuation prompt using the last assistant message's
136
136
  provider/model/agent — the goal spends the session's own subscription.
137
137
  4. Settling (`complete`/`blocked`/`budgetLimited`) fires the injected
138
- `emitGoalNotification` so the user hears about it even with the UI closed:
138
+ `emitGoalNotification` so the user hears about it even with the UI closed.
139
+ The same settle also notifies a registered Assistant contact reporter
140
+ (`setAssignedSessionSettleHandler`) so an assigned/watched session updates
141
+ its contact card — read-only, no second scheduler:
139
142
  desktop + UI broadcast + the standard push fanout (web-push with full
140
143
  text; APNs with a generic per-type title and the session name as body).
141
144
  It obeys the notify-on-completion setting. Conversely, while a goal is
@@ -180,6 +183,10 @@ The loop here picks it up from session events like any other goal.
180
183
 
181
184
  ## Limitations
182
185
 
186
+ - TODO(watch): Assistant contact assign skipped a watch tool this slice.
187
+ Goal settle already notifies. Do not invent a second scheduler. A later
188
+ thin contact tool can post a read-only session status card when
189
+ `metadata.openchamber.goal` settles.
183
190
  - Web-server feature: VS Code (extension-only) renders goal state via
184
191
  `session.updated` but does not run the loop.
185
192
  - A goal on a session with no assistant reply yet starts after the first
@@ -286,6 +286,18 @@ export const accountGoalTokenSpend = ({ messages, goal }) => {
286
286
  return { tokensUsed, lastAccountedMessageID };
287
287
  };
288
288
 
289
+ let assignedSessionSettle = null;
290
+
291
+ export const setAssignedSessionSettleHandler = (handler) => {
292
+ assignedSessionSettle = typeof handler === 'function' ? handler : null;
293
+ };
294
+
295
+ const contactStatusFromGoal = (status) => {
296
+ if (status === 'complete' || status === 'budgetLimited') return 'complete';
297
+ if (status === 'blocked') return 'error';
298
+ return null;
299
+ };
300
+
289
301
  export const createSessionGoalRuntime = ({
290
302
  buildOpenCodeUrl,
291
303
  getOpenCodeAuthHeaders,
@@ -401,6 +413,14 @@ export const createSessionGoalRuntime = ({
401
413
  console.warn('[session-goal] notification failed:', error?.message || error);
402
414
  }
403
415
  }
416
+ const contactStatus = contactStatusFromGoal(status);
417
+ if (contactStatus && typeof assignedSessionSettle === 'function') {
418
+ try {
419
+ assignedSessionSettle({ sessionId, directory, status: contactStatus });
420
+ } catch (error) {
421
+ console.warn('[session-goal] assigned session settle failed:', error?.message || error);
422
+ }
423
+ }
404
424
  };
405
425
 
406
426
  const runAudit = async ({ goal, assistantText, directory, lastAssistantInfo }) => {
@@ -15,7 +15,9 @@ Sessions titled `smartfetch-secondary` are temporary SmartFetch model calls;
15
15
  the index excludes them from every snapshot and clears prior summaries when a
16
16
  matching session update arrives. System-owned sessions are excluded by
17
17
  authoritative metadata only: a non-empty
18
- `metadata.openchamber.assistant.assistantID`,
18
+ `metadata.openchamber.assistant.assistantID` (unless
19
+ `metadata.openchamber.assigned.from === 'contact'` — those are visible
20
+ coding workers the Assistant subscribed to),
19
21
  `metadata.openchamber.scheduledTask.taskID`, or
20
22
  `metadata.openchamber.smallModel.purpose`. Title prefixes are human labels and
21
23
  never participate in this filter. Metadata is the ownership/isolation signal;
@@ -10,10 +10,14 @@ const HIDDEN_SESSION_TITLES = new Set(['smartfetch-secondary']);
10
10
  const nonEmptySystemID = (value) => typeof value === 'string' && value.length > 0;
11
11
 
12
12
  /** System sessions are isolated by authoritative metadata only — never by title prefix. */
13
+ const isContactAssignedSession = (openchamber) => openchamber?.assigned?.from === 'contact';
14
+
13
15
  const isSystemSession = (session) => {
14
16
  const openchamber = session?.metadata?.openchamber;
15
17
  if (!openchamber || typeof openchamber !== 'object') return false;
16
- if (nonEmptySystemID(openchamber.assistant?.assistantID)) return true;
18
+ // Contact-assigned workers stay visible in Chat. Only archived Assistant
19
+ // bindings use `openchamber.assistant` as a hide marker.
20
+ if (nonEmptySystemID(openchamber.assistant?.assistantID) && !isContactAssignedSession(openchamber)) return true;
17
21
  if (nonEmptySystemID(openchamber.scheduledTask?.taskID)) return true;
18
22
  if (nonEmptySystemID(openchamber.smallModel?.purpose)) return true;
19
23
  return false;
@@ -52,7 +56,9 @@ const toSummary = (session, fallbackDirectory) => {
52
56
  if (!session || !isVisibleSession(session) || typeof session.id !== 'string' || !session.id) return null;
53
57
  const directory = normalizeDirectory(session.directory ?? session.project?.worktree ?? fallbackDirectory);
54
58
  if (!directory) return null;
55
- const assistant = session.metadata?.openchamber?.assistant;
59
+ const openchamber = session.metadata?.openchamber;
60
+ const assistant = openchamber?.assistant;
61
+ const hideAssistantMarker = isContactAssignedSession(openchamber);
56
62
  return {
57
63
  id: session.id,
58
64
  directory,
@@ -63,8 +69,8 @@ const toSummary = (session, fallbackDirectory) => {
63
69
  archivedAt: toTimestamp(session.time?.archived),
64
70
  parentID: typeof session.parentID === 'string' ? session.parentID : null,
65
71
  hasChildren: typeof session.hasChildren === 'boolean' ? session.hasChildren : null,
66
- assistantID: typeof assistant?.assistantID === 'string' ? assistant.assistantID : null,
67
- assistantName: typeof assistant?.name === 'string' ? assistant.name : null,
72
+ assistantID: hideAssistantMarker || typeof assistant?.assistantID !== 'string' ? null : assistant.assistantID,
73
+ assistantName: hideAssistantMarker || typeof assistant?.name !== 'string' ? null : assistant.name,
68
74
  };
69
75
  };
70
76
 
@@ -100,6 +100,43 @@ describe('Electron session index', () => {
100
100
  service.close();
101
101
  });
102
102
 
103
+ it('keeps contact-assigned worker sessions visible in ordinary sidebar summaries', () => {
104
+ const runtimeRef = { value: 'http://runtime-a.test' };
105
+ const service = createService(runtimeRef);
106
+ const worker = {
107
+ ...session('ses_worker', 100),
108
+ metadata: {
109
+ openchamber: {
110
+ assigned: { from: 'contact', assistantID: 'assistant_1', name: 'A' },
111
+ },
112
+ },
113
+ };
114
+ const legacyWorker = {
115
+ ...session('ses_legacy_worker', 99),
116
+ metadata: {
117
+ openchamber: {
118
+ assistant: { assistantID: 'assistant_1', name: 'A' },
119
+ assigned: { from: 'contact' },
120
+ },
121
+ },
122
+ };
123
+ const hiddenBinding = {
124
+ ...session('ses_binding', 98),
125
+ metadata: { openchamber: { assistant: { assistantID: 'assistant_1', name: 'A' } } },
126
+ };
127
+ service.replaceDirectory({
128
+ directory: '/repo',
129
+ sessions: [worker, legacyWorker, hiddenBinding],
130
+ cursor: null,
131
+ hasMore: false,
132
+ });
133
+ expect(service.snapshot().directories[0].sessions.map((item) => item.id)).toEqual([
134
+ 'ses_worker',
135
+ 'ses_legacy_worker',
136
+ ]);
137
+ service.close();
138
+ });
139
+
103
140
  it('keeps runtime targets isolated in one Electron database', () => {
104
141
  const runtimeRef = { value: 'http://runtime-a.test' };
105
142
  const service = createService(runtimeRef);
@@ -163,6 +163,7 @@ const extractSessionStatus = (payload) => {
163
163
  export const isSystemOwnedSession = (sessionOrInfo) => {
164
164
  const openchamber = sessionOrInfo?.metadata?.openchamber;
165
165
  if (!openchamber || typeof openchamber !== 'object') return false;
166
+ if (openchamber.assigned?.from === 'contact') return false;
166
167
  if (typeof openchamber.assistant?.assistantID === 'string' && openchamber.assistant.assistantID) return true;
167
168
  if (typeof openchamber.scheduledTask?.taskID === 'string' && openchamber.scheduledTask.taskID) return true;
168
169
  if (typeof openchamber.smallModel?.purpose === 'string' && openchamber.smallModel.purpose) return true;
@@ -243,6 +243,17 @@ describe('session-title helpers', () => {
243
243
  metadata: { openchamber: { smallModel: { purpose: '' } } },
244
244
  })).toBe(false);
245
245
  expect(isSystemOwnedSession({ title: 'Ordinary' })).toBe(false);
246
+ expect(isSystemOwnedSession({
247
+ metadata: { openchamber: { assigned: { from: 'contact', assistantID: 'assistant_1' } } },
248
+ })).toBe(false);
249
+ expect(isSystemOwnedSession({
250
+ metadata: {
251
+ openchamber: {
252
+ assistant: { assistantID: 'assistant_1' },
253
+ assigned: { from: 'contact' },
254
+ },
255
+ },
256
+ })).toBe(false);
246
257
  });
247
258
 
248
259
  it('skips title generation and patch for system-owned sessions', async () => {
@@ -1 +0,0 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{nt as e,rt as t}from"./vendor-base-ui-CnHfUmDN.js";import{n}from"./vendor-react-DLPElpoQ.js";import{c as r,l as i}from"./dialog-HCE4eLUW.js";import{t as a}from"./utils-CTL16t2f.js";var o=n();t();var s=e(),c=e=>{let t=(0,o.c)(27),{name:n,emoji:c,size:u,className:d,label:f}=e,p=u===void 0?16:u,m,h,g;if(t[0]!==d||t[1]!==c||t[2]!==n){m=i(n);let e;t[6]===n?e=t[7]:(e=r(n),t[6]=n,t[7]=e),h=e,g=a(`inline-flex aspect-square shrink-0 overflow-hidden rounded-sm`,c&&`items-center justify-center select-none`,m.class,d),t[0]=d,t[1]=c,t[2]=n,t[3]=m,t[4]=h,t[5]=g}else m=t[3],h=t[4],g=t[5];let _=`calc(${p}px * var(--dpt-n, 1))`,v=`calc(${p}px * var(--dpt-n, 1))`,y=`calc(${p}px * var(--dpt-n, 1))`,b=`calc(${p}px * var(--dpt-n, 1))`,x=`calc(${p}px * var(--dpt-n, 1))`,S=`calc(${p}px * var(--dpt-n, 1))`,C=`var(${m.var})`,w;t[8]!==_||t[9]!==v||t[10]!==y||t[11]!==b||t[12]!==x||t[13]!==S||t[14]!==C?(w={width:_,minWidth:v,maxWidth:y,height:b,minHeight:x,maxHeight:S,color:C},t[8]=_,t[9]=v,t[10]=y,t[11]=b,t[12]=x,t[13]=S,t[14]=C,t[15]=w):w=t[15];let T=f?`img`:void 0,E=!f||void 0,D;t[16]!==c||t[17]!==h||t[18]!==p?(D=c?(0,s.jsx)(`span`,{"aria-hidden":`true`,style:{fontSize:`calc(${p*.75}px * var(--dpt-n, 1))`,lineHeight:1},children:c}):(0,s.jsxs)(`svg`,{width:p,height:p,viewBox:`0 0 5 5`,shapeRendering:`crispEdges`,className:`oc-agent-avatar block h-full w-full`,"aria-hidden":`true`,children:[(0,s.jsx)(`rect`,{width:`5`,height:`5`,fill:`currentColor`,opacity:.14}),h.map(l)]}),t[16]=c,t[17]=h,t[18]=p,t[19]=D):D=t[19];let O;return t[20]!==f||t[21]!==w||t[22]!==T||t[23]!==E||t[24]!==D||t[25]!==g?(O=(0,s.jsx)(`span`,{className:g,style:w,role:T,"aria-label":f,"aria-hidden":E,children:D}),t[20]=f,t[21]=w,t[22]=T,t[23]=E,t[24]=D,t[25]=g,t[26]=O):O=t[26],O};function l(e,t){return e.map((e,n)=>e?(0,s.jsx)(`rect`,{x:n,y:t,width:`1`,height:`1`,fill:`currentColor`},`${n}-${t}`):null)}export{c as t};