cli-jaw 2.17.41 → 2.17.42

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 (167) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/commands/browser.js +52 -5
  3. package/dist/bin/commands/browser.js.map +1 -1
  4. package/dist/src/agent/args.js +69 -19
  5. package/dist/src/agent/args.js.map +1 -1
  6. package/dist/src/agent/codex-app-events.js +31 -1
  7. package/dist/src/agent/codex-app-events.js.map +1 -1
  8. package/dist/src/agent/cursor-model-inventory.js +140 -0
  9. package/dist/src/agent/cursor-model-inventory.js.map +1 -0
  10. package/dist/src/agent/grok-models.js +80 -0
  11. package/dist/src/agent/grok-models.js.map +1 -0
  12. package/dist/src/browser/actions.js +383 -20
  13. package/dist/src/browser/actions.js.map +1 -1
  14. package/dist/src/browser/grounding-candidate.js +272 -0
  15. package/dist/src/browser/grounding-candidate.js.map +1 -0
  16. package/dist/src/browser/grounding-eval.js +247 -0
  17. package/dist/src/browser/grounding-eval.js.map +1 -0
  18. package/dist/src/browser/image-size.js +63 -0
  19. package/dist/src/browser/image-size.js.map +1 -0
  20. package/dist/src/browser/index.js +1 -0
  21. package/dist/src/browser/index.js.map +1 -1
  22. package/dist/src/browser/occlusion.js +148 -0
  23. package/dist/src/browser/occlusion.js.map +1 -0
  24. package/dist/src/browser/verify-candidate.js +162 -0
  25. package/dist/src/browser/verify-candidate.js.map +1 -0
  26. package/dist/src/browser/vision-input.js +74 -0
  27. package/dist/src/browser/vision-input.js.map +1 -0
  28. package/dist/src/browser/vision-provider.js +75 -0
  29. package/dist/src/browser/vision-provider.js.map +1 -0
  30. package/dist/src/browser/vision.js +360 -47
  31. package/dist/src/browser/vision.js.map +1 -1
  32. package/dist/src/browser/web-ai/candidate-reconcile.js +14 -5
  33. package/dist/src/browser/web-ai/candidate-reconcile.js.map +1 -1
  34. package/dist/src/cli/claude-model-discovery.js +175 -0
  35. package/dist/src/cli/claude-model-discovery.js.map +1 -0
  36. package/dist/src/cli/claude-models.js +46 -0
  37. package/dist/src/cli/claude-models.js.map +1 -1
  38. package/dist/src/cli/registry-live.js +104 -1
  39. package/dist/src/cli/registry-live.js.map +1 -1
  40. package/dist/src/cli/registry.js +28 -5
  41. package/dist/src/cli/registry.js.map +1 -1
  42. package/dist/src/code-mode/host.js +6 -0
  43. package/dist/src/code-mode/host.js.map +1 -1
  44. package/dist/src/code-mode/manager.js +37 -8
  45. package/dist/src/code-mode/manager.js.map +1 -1
  46. package/dist/src/code-mode/providers/catalog.js +53 -1
  47. package/dist/src/code-mode/providers/catalog.js.map +1 -1
  48. package/dist/src/code-mode/providers/codex-app.js +6 -0
  49. package/dist/src/code-mode/providers/codex-app.js.map +1 -1
  50. package/dist/src/code-mode/providers/live-models.js +85 -0
  51. package/dist/src/code-mode/providers/live-models.js.map +1 -0
  52. package/dist/src/code-mode/providers/provider-live-models.js +120 -0
  53. package/dist/src/code-mode/providers/provider-live-models.js.map +1 -0
  54. package/dist/src/code-mode/session.js +37 -0
  55. package/dist/src/code-mode/session.js.map +1 -1
  56. package/dist/src/prompt/templates/a1-system.md +24 -14
  57. package/dist/src/prompt/templates/control-system.md +6 -11
  58. package/dist/src/prompt/templates/employee.md +5 -5
  59. package/dist/src/routes/browser.js +17 -5
  60. package/dist/src/routes/browser.js.map +1 -1
  61. package/package.json +1 -1
  62. package/public/css/activity.css +20 -1
  63. package/public/css/layout.css +7 -3
  64. package/public/dist/.vite/manifest.json +104 -105
  65. package/public/dist/assets/Agent-CQ4EBgY8.js +1 -0
  66. package/public/dist/assets/CodeCanvas-DWsx6sv5.js +2 -0
  67. package/public/dist/assets/DashboardMeta-Dh-tasbY.js +1 -0
  68. package/public/dist/assets/Display-NrqY_IaN.js +1 -0
  69. package/public/dist/assets/{Permissions-BzbnqenZ.js → Permissions-Bh947tUG.js} +1 -1
  70. package/public/dist/assets/Permissions-DzNJCt8K.js +1 -0
  71. package/public/dist/assets/app-BCBnxiGr.css +1 -0
  72. package/public/dist/assets/{app-x5Kray4W.js → app-Q0prKOFx.js} +4 -4
  73. package/public/dist/assets/{bgtask-badge-W9djRQVu.js → bgtask-badge-DAcWaUfS.js} +1 -1
  74. package/public/dist/assets/bgtask-badge-DHA2OQm8.js +1 -0
  75. package/public/dist/assets/{employees--_dmDw_a.js → employees-CWS3Juca.js} +1 -1
  76. package/public/dist/assets/{iframe-renderer-D4BmLtAq.js → iframe-renderer-DCDKBMb7.js} +2 -2
  77. package/public/dist/assets/iframe-renderer-RpvBly2x.js +1 -0
  78. package/public/dist/assets/manager-Nk7-RYPs.css +1 -0
  79. package/public/dist/assets/manager-XkR-DRaQ.js +5 -0
  80. package/public/dist/assets/manager-shortcuts-jsiX-zV6.js +1 -0
  81. package/public/dist/assets/memory-4YH8sz0L.js +1 -0
  82. package/public/dist/assets/{memory-DENQWzM8.js → memory-DhNdKZLO.js} +1 -1
  83. package/public/dist/assets/{message-history-BsrLcGJj.js → message-history-DbRTl1P0.js} +1 -1
  84. package/public/dist/assets/message-history-lVcLpAw8.js +1 -0
  85. package/public/dist/assets/{render-CF6Q1oIh.js → render-DP8SJtdZ.js} +1 -1
  86. package/public/dist/assets/settings--EGy3_B9.js +1 -0
  87. package/public/dist/assets/settings-85n9NjvX.js +1 -0
  88. package/public/dist/assets/{settings-B-C_jdZR.js → settings-C9_D-pn0.js} +32 -32
  89. package/public/dist/assets/settings-core-8wBl1mg-.js +1 -0
  90. package/public/dist/assets/{settings-core-BzAJLsKN.js → settings-core-BcVD2FgV.js} +1 -1
  91. package/public/dist/assets/{settings-embedding-dEMyByVi.js → settings-embedding-BSpq785l.js} +1 -1
  92. package/public/dist/assets/{settings-embedding-BSA42oKM.css → settings-embedding-LIMq5ebM.css} +1 -1
  93. package/public/dist/assets/{sidebar-fFEOpvJl.js → sidebar-BVhLb5Ft.js} +3 -3
  94. package/public/dist/assets/skills-6GFkHTlQ.js +1 -0
  95. package/public/dist/assets/{skills-Da_b06ci.js → skills-X7qxmHka.js} +1 -1
  96. package/public/dist/assets/{slash-commands-BbPUHHF4.js → slash-commands-BnDH4IU_.js} +1 -1
  97. package/public/dist/assets/slash-commands-qu8fPAZ3.js +1 -0
  98. package/public/dist/assets/{trace-drawer-B-QmzUfP.js → trace-drawer-DasaS1z4.js} +1 -1
  99. package/public/dist/assets/{ui-CkRUrIfp.js → ui-BtiBTXng.js} +1 -1
  100. package/public/dist/assets/ui-BucJ1zMA.js +1 -0
  101. package/public/dist/index.html +2 -2
  102. package/public/dist/manager/index.html +3 -3
  103. package/public/dist/settings/index.html +2 -2
  104. package/public/js/features/settings.ts +16 -0
  105. package/public/manager/src/App.tsx +10 -12
  106. package/public/manager/src/AppChrome.tsx +2 -3
  107. package/public/manager/src/InstancePreview.tsx +47 -0
  108. package/public/manager/src/SidebarRailRouter.tsx +34 -30
  109. package/public/manager/src/code/CodeComposer.tsx +31 -6
  110. package/public/manager/src/code/CodeSessionList.tsx +25 -7
  111. package/public/manager/src/code/CodeToastHost.tsx +49 -0
  112. package/public/manager/src/code/CodeTranscript.tsx +136 -26
  113. package/public/manager/src/code/CodeWorkbench.tsx +16 -2
  114. package/public/manager/src/code/ComposerFooter.tsx +130 -46
  115. package/public/manager/src/code/ContextUsageMeter.tsx +72 -0
  116. package/public/manager/src/code/ProviderGlyph.tsx +58 -0
  117. package/public/manager/src/code/code-composer.css +159 -175
  118. package/public/manager/src/code/code-controller-runtime.ts +3 -1
  119. package/public/manager/src/code/code-session-state.ts +7 -1
  120. package/public/manager/src/code/code-toasts.ts +73 -0
  121. package/public/manager/src/code/code-workbench.css +42 -1
  122. package/public/manager/src/code/code.css +130 -15
  123. package/public/manager/src/code/context-usage.ts +74 -0
  124. package/public/manager/src/code/pending-user-item.ts +46 -0
  125. package/public/manager/src/code/session-order.ts +70 -0
  126. package/public/manager/src/code/tool-summary.ts +101 -0
  127. package/public/manager/src/code/use-dictation.ts +130 -0
  128. package/public/manager/src/components/CommandCenter.tsx +4 -8
  129. package/public/manager/src/components/InstanceDetailPanel.tsx +0 -18
  130. package/public/manager/src/components/Workbench.tsx +6 -10
  131. package/public/manager/src/components/WorkbenchHeader.tsx +0 -11
  132. package/public/manager/src/dashboard-settings/dashboard-settings-ui.ts +3 -1
  133. package/public/manager/src/hooks/useDashboardView.ts +38 -38
  134. package/public/manager/src/manager-components.css +0 -8
  135. package/public/manager/src/manager-shortcut-runner.ts +0 -1
  136. package/public/manager/src/manager-shortcuts.ts +0 -2
  137. package/public/manager/src/settings/pages/Agent.tsx +5 -2
  138. package/public/manager/src/settings/pages/DashboardMeta.tsx +20 -1
  139. package/public/manager/src/settings/pages/Permissions.tsx +23 -4
  140. package/public/manager/src/settings/pages/components/agent/PermissionQuickSection.tsx +6 -2
  141. package/public/manager/src/settings/pages/components/agent/agent-save.ts +24 -1
  142. package/public/manager/src/settings/settings-icons.ts +3 -1
  143. package/public/manager/src/settings/settings-registry.ts +3 -3
  144. package/public/manager/src/settings-controls.css +0 -1
  145. package/public/manager/src/settings-standalone.tsx +8 -8
  146. package/public/manager/src/types.ts +0 -1
  147. package/scripts/check-private-boundary.mjs +101 -0
  148. package/scripts/grounding-eval.mjs +223 -0
  149. package/public/dist/assets/Agent-B8ZEWJ6B.js +0 -1
  150. package/public/dist/assets/CodeCanvas-Bj3lTSu6.js +0 -2
  151. package/public/dist/assets/DashboardMeta-CmZavI4H.js +0 -1
  152. package/public/dist/assets/Display-DFkjKcMk.js +0 -1
  153. package/public/dist/assets/Permissions-CJftCO6p.js +0 -1
  154. package/public/dist/assets/app-DVbrIl8f.css +0 -1
  155. package/public/dist/assets/bgtask-badge-CuAc8bAl.js +0 -1
  156. package/public/dist/assets/iframe-renderer-By2WO5bd.js +0 -1
  157. package/public/dist/assets/manager-CxLXWCGi.css +0 -1
  158. package/public/dist/assets/manager-DA3dF3eN.js +0 -5
  159. package/public/dist/assets/manager-shortcuts-CzGkE0ZG.js +0 -1
  160. package/public/dist/assets/memory-thjr2apY.js +0 -1
  161. package/public/dist/assets/message-history-CfU7_P5t.js +0 -1
  162. package/public/dist/assets/settings-DXA5L4DR.js +0 -1
  163. package/public/dist/assets/settings-Dy_IaNVH.js +0 -1
  164. package/public/dist/assets/settings-core-D7sAQqhC.js +0 -1
  165. package/public/dist/assets/skills-BcIievZp.js +0 -1
  166. package/public/dist/assets/slash-commands-B-5Shayl.js +0 -1
  167. package/public/dist/assets/ui-CJLdMfRx.js +0 -1
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Settings</title>
7
7
  <script src="/manager/theme-boot.js"></script>
8
- <script type="module" crossorigin src="/dist/assets/settings-DXA5L4DR.js"></script>
9
- <link rel="stylesheet" crossorigin href="/dist/assets/settings-embedding-BSA42oKM.css">
8
+ <script type="module" crossorigin src="/dist/assets/settings-85n9NjvX.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/dist/assets/settings-embedding-LIMq5ebM.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="settings-root"></div>
@@ -1,4 +1,5 @@
1
1
  // settings.ts — barrel re-export (preserves all import paths)
2
+ import { isLocalPreviewRelayOrigin } from '../preview-parent-origin.js';
2
3
  export { loadSettings, updateSettings, setPerm, onCliChange, saveActiveCliSettings, onFlushCliChange, loadFlushAgentSidebar } from './settings-core.js';
3
4
  export { setTelegram, setForwardAll, setTelegramMentionOnly, saveTelegramSettings } from './settings-telegram.js';
4
5
  export { setDiscord, setDiscordForwardAll, setDiscordAllowBots, setDiscordMentionOnly, saveDiscordSettings } from './settings-discord.js';
@@ -66,12 +67,27 @@ export function initSettingsFrame(): () => void {
66
67
  if (event.data?.type === 'jaw-settings-ready') sendTheme();
67
68
  if (event.data?.type === 'settings:back') closeSettingsPage();
68
69
  };
70
+ // The manager asks this instance to open its own settings page. Two guards live fifteen
71
+ // lines apart on purpose and must NOT be collapsed into one:
72
+ // receive() above talks to our OWN iframe, which is same-origin by construction.
73
+ // relay() below talks to the manager, which under the default origin-port preview
74
+ // transport runs on a different loopback PORT — so a strict origin equality check
75
+ // would silently drop every message. isLocalPreviewRelayOrigin accepts any loopback
76
+ // origin, which is the same predicate the STT and capability relays already use.
77
+ const relay = (event: MessageEvent): void => {
78
+ if (event.source !== window.parent) return;
79
+ if (!isLocalPreviewRelayOrigin(event.origin)) return;
80
+ if (event.data?.type !== 'jaw-preview-settings-open') return;
81
+ toggleSettingsPage(true);
82
+ };
69
83
  frame.addEventListener('load', sendTheme);
70
84
  window.addEventListener('message', receive);
85
+ window.addEventListener('message', relay);
71
86
  sendTheme();
72
87
  return () => {
73
88
  observer.disconnect();
74
89
  frame.removeEventListener('load', sendTheme);
75
90
  window.removeEventListener('message', receive);
91
+ window.removeEventListener('message', relay);
76
92
  };
77
93
  }
@@ -57,7 +57,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
57
57
  const [transitioningPort, setTransitioningPort] = useState<number | null>(null);
58
58
  const [transitionAction, setTransitionAction] = useState<DashboardLifecycleAction | null>(null);
59
59
  const [activeProfileIds, setActiveProfileIds] = useState<string[]>([]);
60
- const { settingsDirty, panelSettingsDirty, dashboardSettingsDirty, setSettingsDirty, onSettingsDirtyChange, onPanelSettingsDirtyChange } = useSettingsDirtyState();
60
+ const { settingsDirty, dashboardSettingsDirty, setSettingsDirty, onSettingsDirtyChange } = useSettingsDirtyState();
61
61
  const [notesDirtyPath, setNotesDirtyPath] = useState<string | null>(null);
62
62
  const [notesSidebarMode, setNotesSidebarMode] = useState<NotesSidebarMode>('files');
63
63
  const [notesSearchFocusToken, setNotesSearchFocusToken] = useState(0);
@@ -194,9 +194,9 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
194
194
  const loaded = await registry.refresh();
195
195
  const ui = loaded.registry.ui;
196
196
  const restored = hydrateInstanceSettings(ui);
197
- view.setSelectedPort(ui.selectedPort); view.setActiveDetailTab(restored.selectedTab); view.setInstanceSettingsOpen(restored.instanceSettingsOpen); view.setSidebarCollapsed(ui.sidebarCollapsed);
197
+ view.setSelectedPort(ui.selectedPort); view.setActiveDetailTab(restored.selectedTab); view.setSidebarCollapsed(ui.sidebarCollapsed);
198
198
  view.setActivityDockCollapsed(ui.activityDockCollapsed); view.setActivityDockHeight(ui.activityDockHeight);
199
- const sidebarMode = normalizeSidebarModeForBuild(readInitialSidebarMode(window.location.search) ?? ui.sidebarMode);
199
+ const sidebarMode = normalizeSidebarModeForBuild(readInitialSidebarMode(window.location.search) ?? restored.sidebarMode); // restored carries the legacy settings request
200
200
  view.setSidebarMode(sidebarMode);
201
201
  if (sidebarMode !== ui.sidebarMode) void saveUi({ sidebarMode });
202
202
  view.setNotesSelectedPath(ui.notesSelectedPath); view.setNotesViewMode(ui.notesViewMode); view.setNotesAuthoringMode(ui.notesAuthoringMode ?? 'plain');
@@ -243,8 +243,8 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
243
243
  setActiveProfileIds(next);
244
244
  void registry.save({ activeProfileFilter: next });
245
245
  }
246
- const { canLeaveDirtySettings, guardSettingsTransition, setInstanceSettingsOpen } = createInstanceSettingsNavigation({
247
- view, settingsDirty, panelSettingsDirty, dashboardSettingsDirty, clearDirty: entry => onSettingsDirtyChange(entry, false), saveUi, selectedPort: selectedInstance?.port ?? null,
246
+ const { canLeaveDirtySettings, guardSettingsTransition, setDashboardSettingsOpen } = createInstanceSettingsNavigation({
247
+ view, settingsDirty, dashboardSettingsDirty, clearDirty: entry => onSettingsDirtyChange(entry, false), saveUi, selectedPort: selectedInstance?.port ?? null,
248
248
  });
249
249
 
250
250
  function handlePreview(instance: DashboardInstance, openDefaultSession = false): void {
@@ -263,7 +263,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
263
263
  void saveUi({ selectedPort: instance.port });
264
264
  }
265
265
  function handleTabChange(tab: DashboardDetailTab): void {
266
- if (tab === 'settings') { setInstanceSettingsOpen(true); return; }
266
+ if (tab === 'settings') { setDashboardSettingsOpen(true); return; } // retired tab -> rail workspace
267
267
  if (view.activeDetailTab === 'preview' && tab !== 'preview') {
268
268
  const port = view.selectedPort;
269
269
  if (port != null) activityUnread.markPortSeen(port);
@@ -385,7 +385,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
385
385
  handleTabChange,
386
386
  setPreviewRefreshKey,
387
387
  handleSidebarToggle,
388
- toggleInstanceSettings: () => setInstanceSettingsOpen(!view.instanceSettingsOpen),
388
+ toggleInstanceSettings: () => setDashboardSettingsOpen(view.sidebarMode !== 'settings'),
389
389
  });
390
390
  }
391
391
  useEffect(() => {
@@ -395,7 +395,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
395
395
  runManagerShortcut(action);
396
396
  });
397
397
  return unsubscribe;
398
- }, [filtered, selectedInstance, view.dashboardShortcutsEnabled, view.sidebarMode, view.activeDetailTab, view.instanceSettingsOpen, settingsDirty, panelSettingsDirty, dashboardSettingsDirty]);
398
+ }, [filtered, selectedInstance, view.dashboardShortcutsEnabled, view.sidebarMode, view.activeDetailTab, settingsDirty, dashboardSettingsDirty]);
399
399
 
400
400
  useEffect(() => {
401
401
  function onKeyDown(event: KeyboardEvent): void {
@@ -416,7 +416,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
416
416
  }
417
417
  document.addEventListener('keydown', onKeyDown);
418
418
  return () => document.removeEventListener('keydown', onKeyDown);
419
- }, [filtered, selectedInstance, view.dashboardShortcutsEnabled, view.dashboardShortcutKeymap, view.sidebarMode, view.activeDetailTab, view.instanceSettingsOpen, settingsDirty, panelSettingsDirty, dashboardSettingsDirty]);
419
+ }, [filtered, selectedInstance, view.dashboardShortcutsEnabled, view.dashboardShortcutKeymap, view.sidebarMode, view.activeDetailTab, settingsDirty, dashboardSettingsDirty]);
420
420
 
421
421
  async function handleLifecycle(action: DashboardLifecycleAction, instance: DashboardInstance): Promise<void> {
422
422
  const lifecycle = instance.lifecycle;
@@ -489,11 +489,9 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
489
489
  instance={selectedInstance}
490
490
  data={data}
491
491
  activeTab={tab}
492
- onSettingsDirtyChange={onPanelSettingsDirtyChange}
493
- onSettingsSaved={() => { if (selectedInstance) void refreshInstance(selectedInstance.port); publishInvalidation({ topics: ['instances'], reason: 'instance:settings-saved', source: 'ui', sourceId: 'app' }); }}
494
492
  onRegistryPatch={(port, patch) => { void registry.save({ instances: { [String(port)]: patch } }).then(() => { load(); publishInvalidation({ topics: ['instances'], reason: 'instance:registry-patched', source: 'ui', sourceId: 'app' }); }); }}
495
493
  />
496
494
  );
497
495
 
498
- return <AppChrome view={view} palette={palette} theme={theme} query={query} loading={loading} showHidden={showHidden} instances={instances} selectedInstance={selectedInstance} data={data} summary={summary} scheduleGroup={scheduleGroup} boardView={boardView} notesModel={notesModel} notesSelectedNote={notesSelectedNote} notesDirtyPath={notesDirtyPath} notesHighlightedPath={notesHighlightedPath} notesSidebarMode={notesSidebarMode} notesSearchFocusToken={notesSearchFocusToken} settingsSection={dashboardSettingsSection} dashboardSettingsUi={dashboardSettingsUi} titleSupport={titleSupport} activityEvents={activityEvents} busyPorts={messageActivity.busyPorts} titlesByPort={messageActivity.titlesByPort} lifecycleMessage={lifecycleMessage} error={error} registryMessage={registry.error || labelEditor.error || managerEvents.error} onPickProject={(port) => void projectPicker.pick(port)} projectPickBusy={projectPicker.busyPort != null} onRefreshPreview={() => setPreviewRefreshKey(key => key + 1)} onInstanceSettingsOpenChange={setInstanceSettingsOpen} onSettingsDirtyChange={onSettingsDirtyChange} detailContent={detailContent} instanceListContent={instanceListContent} drawerProfileFilters={profileChipStrip(profiles)} jawCeoWorkbenchButton={jawCeoBridge.workbenchButton} jawCeoVoiceOverlay={jawCeoBridge.voiceOverlay} jawCeo={jawCeoBridge.ceo} jawCeoVoice={jawCeoBridge.voice} jawCeoOpen={jawCeoBridge.open} jawCeoSelectedPort={activeJawCeoPort} onJawCeoOpenChange={jawCeoBridge.setOpen} onJawCeoOpenWorker={handleOpenJawCeoWorker} previewEnabled={previewEnabled} previewRefreshKey={previewRefreshKey} autoUnloadNotice={autoUnloadNotice} helpOpen={helpOpen} helpTopic={helpTopic} setQuery={setQuery} setShowHidden={setShowHidden} setPreviewEnabled={setPreviewEnabled} setAutoUnloadNotice={setAutoUnloadNotice} setHelpOpen={setHelpOpen} setHelpTopic={setHelpTopic} onOpenHelpTopic={openHelpTopic} setNotesSidebarMode={setNotesSidebarMode} setBoardView={setBoardView} setScheduleGroup={setScheduleGroup} setDashboardSettingsSection={setDashboardSettingsSection} load={load} cycleTheme={cycleTheme} openSelectedInBrowser={openSelectedInBrowser} handleSelectInstance={handleSelectInstance} handleSidebarModeChange={handleSidebarModeChange} handleSidebarToggle={handleSidebarToggle} handleNotesSelectedPathChange={handleNotesSelectedPathChange} openNotesFromPreview={openNotesFromPreview} handleNotesViewModeChange={handleNotesViewModeChange} handleNotesAuthoringModeChange={handleNotesAuthoringModeChange} handleNotesWordWrapChange={handleNotesWordWrapChange} handleNotesVimModeChange={handleNotesVimModeChange} handleNotesTreeWidthChange={handleNotesTreeWidthChange} handleNotesGraphSettingsChange={handleNotesGraphSettingsChange} openNotesSidebarSearch={openNotesSidebarSearch} setNotesDirtyPath={setNotesDirtyPath} handleTabChange={handleTabChange} handleActivityToggle={handleActivityToggle} handleActivityHeight={handleActivityHeight} onDismissLifecycleMessage={() => setLifecycleMessage(null)} handleDashboardSettingsPatch={handleDashboardSettingsPatch} activityUnreadOpenAndMarkSeen={activityUnread.openAndMarkSeen} panelInitialState={panelInitialState} onPanelStateChange={handlePanelStateChange} onShortcutAction={runManagerShortcut} />;
496
+ return <AppChrome view={view} palette={palette} theme={theme} query={query} loading={loading} showHidden={showHidden} instances={instances} selectedInstance={selectedInstance} data={data} summary={summary} scheduleGroup={scheduleGroup} boardView={boardView} notesModel={notesModel} notesSelectedNote={notesSelectedNote} notesDirtyPath={notesDirtyPath} notesHighlightedPath={notesHighlightedPath} notesSidebarMode={notesSidebarMode} notesSearchFocusToken={notesSearchFocusToken} settingsSection={dashboardSettingsSection} dashboardSettingsUi={dashboardSettingsUi} titleSupport={titleSupport} activityEvents={activityEvents} busyPorts={messageActivity.busyPorts} titlesByPort={messageActivity.titlesByPort} lifecycleMessage={lifecycleMessage} error={error} registryMessage={registry.error || labelEditor.error || managerEvents.error} onPickProject={(port) => void projectPicker.pick(port)} projectPickBusy={projectPicker.busyPort != null} onRefreshPreview={() => setPreviewRefreshKey(key => key + 1)} onSettingsDirtyChange={onSettingsDirtyChange} detailContent={detailContent} instanceListContent={instanceListContent} drawerProfileFilters={profileChipStrip(profiles)} jawCeoWorkbenchButton={jawCeoBridge.workbenchButton} jawCeoVoiceOverlay={jawCeoBridge.voiceOverlay} jawCeo={jawCeoBridge.ceo} jawCeoVoice={jawCeoBridge.voice} jawCeoOpen={jawCeoBridge.open} jawCeoSelectedPort={activeJawCeoPort} onJawCeoOpenChange={jawCeoBridge.setOpen} onJawCeoOpenWorker={handleOpenJawCeoWorker} previewEnabled={previewEnabled} previewRefreshKey={previewRefreshKey} autoUnloadNotice={autoUnloadNotice} helpOpen={helpOpen} helpTopic={helpTopic} setQuery={setQuery} setShowHidden={setShowHidden} setPreviewEnabled={setPreviewEnabled} setAutoUnloadNotice={setAutoUnloadNotice} setHelpOpen={setHelpOpen} setHelpTopic={setHelpTopic} onOpenHelpTopic={openHelpTopic} setNotesSidebarMode={setNotesSidebarMode} setBoardView={setBoardView} setScheduleGroup={setScheduleGroup} setDashboardSettingsSection={setDashboardSettingsSection} load={load} cycleTheme={cycleTheme} openSelectedInBrowser={openSelectedInBrowser} handleSelectInstance={handleSelectInstance} handleSidebarModeChange={handleSidebarModeChange} handleSidebarToggle={handleSidebarToggle} handleNotesSelectedPathChange={handleNotesSelectedPathChange} openNotesFromPreview={openNotesFromPreview} handleNotesViewModeChange={handleNotesViewModeChange} handleNotesAuthoringModeChange={handleNotesAuthoringModeChange} handleNotesWordWrapChange={handleNotesWordWrapChange} handleNotesVimModeChange={handleNotesVimModeChange} handleNotesTreeWidthChange={handleNotesTreeWidthChange} handleNotesGraphSettingsChange={handleNotesGraphSettingsChange} openNotesSidebarSearch={openNotesSidebarSearch} setNotesDirtyPath={setNotesDirtyPath} handleTabChange={handleTabChange} handleActivityToggle={handleActivityToggle} handleActivityHeight={handleActivityHeight} onDismissLifecycleMessage={() => setLifecycleMessage(null)} handleDashboardSettingsPatch={handleDashboardSettingsPatch} activityUnreadOpenAndMarkSeen={activityUnread.openAndMarkSeen} panelInitialState={panelInitialState} onPanelStateChange={handlePanelStateChange} onShortcutAction={runManagerShortcut} />;
499
497
  }
@@ -54,8 +54,7 @@ type AppChromeProps = {
54
54
  onPickProject: (port: number) => void;
55
55
  projectPickBusy: boolean;
56
56
  onRefreshPreview: () => void;
57
- onInstanceSettingsOpenChange: (open: boolean) => void;
58
- onSettingsDirtyChange: (entry: 'panel' | 'dashboard', dirty: boolean) => void;
57
+ onSettingsDirtyChange: (entry: 'dashboard', dirty: boolean) => void;
59
58
  detailContent: (tab: DashboardDetailTab) => ReactNode;
60
59
  instanceListContent: ReactNode;
61
60
  drawerProfileFilters: ReactNode;
@@ -148,7 +147,7 @@ export function AppChrome(props: AppChromeProps) {
148
147
  boardView={props.boardView} onBoardViewChange={props.setBoardView} scheduleGroup={props.scheduleGroup} onScheduleGroupChange={props.setScheduleGroup}
149
148
  instances={props.instances} selectedInstance={props.selectedInstance} data={props.data} titlesByPort={props.titlesByPort}
150
149
  busyPorts={props.busyPorts} activeDetailTab={props.view.activeDetailTab} onDetailTabChange={props.handleTabChange}
151
- workbenchHeader={<WorkbenchHeader instance={props.selectedInstance} previewEnabled={props.previewEnabled} onPreviewEnabledChange={props.setPreviewEnabled} onPreviewRefresh={props.onRefreshPreview} onOpenHelpTopic={props.onOpenHelpTopic} onPickProject={props.onPickProject} projectPickBusy={props.projectPickBusy} />} instanceSettingsOpen={props.view.instanceSettingsOpen} onInstanceSettingsOpenChange={props.onInstanceSettingsOpenChange} onSettingsDirtyChange={props.onSettingsDirtyChange} detailContent={props.detailContent} previewEnabled={props.previewEnabled}
150
+ workbenchHeader={<WorkbenchHeader instance={props.selectedInstance} previewEnabled={props.previewEnabled} onPreviewEnabledChange={props.setPreviewEnabled} onPreviewRefresh={props.onRefreshPreview} onOpenHelpTopic={props.onOpenHelpTopic} onPickProject={props.onPickProject} projectPickBusy={props.projectPickBusy} />} onSettingsDirtyChange={props.onSettingsDirtyChange} detailContent={props.detailContent} previewEnabled={props.previewEnabled}
152
151
  previewRefreshKey={props.previewRefreshKey} previewTheme={props.theme.resolved} onOpenNotesFromPreview={props.openNotesFromPreview} lifecycleMessage={props.lifecycleMessage}
153
152
  onDismissLifecycleMessage={props.onDismissLifecycleMessage} instanceListContent={props.instanceListContent} loading={props.loading}
154
153
  jawCeoWorkbenchButton={props.jawCeoWorkbenchButton} jawCeoVoiceOverlay={props.jawCeoVoiceOverlay}
@@ -20,6 +20,9 @@ type InstancePreviewProps = {
20
20
  docPanelCapable?: boolean;
21
21
  previewInsertTextRequest?: PreviewInsertTextRequest | null;
22
22
  onPreviewInsertTextResult?: (id: string, result: PreviewInsertTextResult) => void;
23
+ /** One-shot id; a new value asks the mounted preview to open its own settings page. */
24
+ previewSettingsOpenRequest?: string | null;
25
+ onPreviewSettingsOpenResult?: (id: string, result: PreviewInsertTextResult) => void;
23
26
  };
24
27
 
25
28
  const PREVIEW_IFRAME_SANDBOX = 'allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads';
@@ -149,6 +152,23 @@ function postPreviewSttToggle(frame: HTMLIFrameElement | null, src: string): voi
149
152
  }
150
153
  }
151
154
 
155
+ /**
156
+ * Ask the instance to open ITS OWN settings page. The manager never renders instance settings;
157
+ * it only sends this request, and the instance decides. Fire-and-forget like the STT toggle:
158
+ * there is nothing to await, and the instance owns the outcome.
159
+ */
160
+ function postPreviewSettingsOpen(frame: HTMLIFrameElement | null, src: string): void {
161
+ const targetWindow = frame?.contentWindow;
162
+ if (!targetWindow) return;
163
+ const targetOrigin = previewTargetOrigin(src, frame);
164
+ if (!targetOrigin || targetOrigin === 'null') return;
165
+ try {
166
+ targetWindow.postMessage({ type: 'jaw-preview-settings-open' }, targetOrigin);
167
+ } catch (error) {
168
+ console.warn('[manager-preview] settings open request skipped', error);
169
+ }
170
+ }
171
+
152
172
  function postPreviewVisible(frame: HTMLIFrameElement | null, src: string): void {
153
173
  const targetWindow = frame?.contentWindow;
154
174
  if (!targetWindow) return;
@@ -252,6 +272,7 @@ export function InstancePreview(props: InstancePreviewProps) {
252
272
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
253
273
  const loadedSrcRef = useRef<string | null>(null);
254
274
  const handledInsertRequestRef = useRef<string | null>(null);
275
+ const handledSettingsRequestRef = useRef<string | null>(null);
255
276
  const [pathDropStatus, setPathDropStatus] = useState<string | null>(null);
256
277
  const [folderDragActive, setFolderDragActive] = useState(false);
257
278
  const state = buildPreviewState(
@@ -295,6 +316,32 @@ export function InstancePreview(props: InstancePreviewProps) {
295
316
  .catch(error => finish({ ok: false, error: (error as Error).message }));
296
317
  }, [disabledReason, props.enabled, props.instance, props.onPreviewInsertTextResult, props.previewInsertTextRequest, state.canPreview, state.src]);
297
318
 
319
+ useEffect(() => {
320
+ const id = props.previewSettingsOpenRequest;
321
+ if (!id || handledSettingsRequestRef.current === id) return;
322
+ handledSettingsRequestRef.current = id;
323
+ const finish = (result: PreviewInsertTextResult): void => {
324
+ props.onPreviewSettingsOpenResult?.(id, result);
325
+ };
326
+ // Deliberately independent of props.active: the preview iframe stays mounted when the
327
+ // Preview tab is not showing, so the request still lands. What it cannot survive is an
328
+ // unmounted or still-loading frame, which is what these guards report.
329
+ if (!props.instance?.ok) {
330
+ finish({ ok: false, error: 'selected instance is not online' });
331
+ return;
332
+ }
333
+ if (!props.enabled || !state.canPreview || !state.src) {
334
+ finish({ ok: false, error: disabledReason || 'turn Preview on to open this instance\u2019s settings' });
335
+ return;
336
+ }
337
+ if (loadedSrcRef.current !== state.src) {
338
+ finish({ ok: false, error: 'selected instance preview is still loading' });
339
+ return;
340
+ }
341
+ postPreviewSettingsOpen(iframeRef.current, state.src);
342
+ finish({ ok: true });
343
+ }, [disabledReason, props.enabled, props.instance, props.onPreviewSettingsOpenResult, props.previewSettingsOpenRequest, state.canPreview, state.src]);
344
+
298
345
  const handleFolderPathDrop = useCallback(async (event: React.DragEvent): Promise<void> => {
299
346
  if (!hasFolderPanelDragPayload(event.dataTransfer)) return;
300
347
  event.preventDefault();
@@ -5,7 +5,6 @@ import { InstanceNavigator } from './components/InstanceNavigator';
5
5
  import { MobileNav } from './components/MobileNav';
6
6
  import { SidebarRail } from './components/SidebarRail';
7
7
  import { Workbench } from './components/Workbench';
8
- import { WorkbenchSettingsToggle } from './components/WorkbenchHeader';
9
8
  import { WorkspaceLayout } from './components/WorkspaceLayout';
10
9
  import { lazy } from 'react';
11
10
  import { RightSidebar } from './panels/RightSidebar';
@@ -141,9 +140,7 @@ type Props = {
141
140
  titlesByPort: Record<number, string>;
142
141
  busyPorts: Set<number>;
143
142
  activeDetailTab: DashboardDetailTab;
144
- instanceSettingsOpen: boolean;
145
- onInstanceSettingsOpenChange: (open: boolean) => void;
146
- onSettingsDirtyChange: (entry: 'panel' | 'dashboard', dirty: boolean) => void;
143
+ onSettingsDirtyChange: (entry: 'dashboard', dirty: boolean) => void;
147
144
  onSettingsSaved?: () => void;
148
145
  onDetailTabChange: (tab: DashboardDetailTab) => void;
149
146
  workbenchHeader: ReactNode;
@@ -274,27 +271,13 @@ function renderBottomTabContent(tab: BottomPanelTab, controls: BottomPanelRender
274
271
  }
275
272
 
276
273
  export function SidebarRailRouter(props: Props) {
277
- const panelDirty = useCallback((dirty: boolean) => props.onSettingsDirtyChange('panel', dirty), [props.onSettingsDirtyChange]);
278
274
  const dashboardDirty = useCallback((dirty: boolean) => props.onSettingsDirtyChange('dashboard', dirty), [props.onSettingsDirtyChange]);
279
275
  const selected = props.selectedInstance;
280
276
  const settingsTarget = selected ? { port: selected.port, instanceUrl: selected.url } : {};
281
277
  const managerSettings = { ui: props.dashboardSettingsUi, titleSupport: props.titleSupport, onUiPatch: props.onDashboardSettingsPatch };
282
- const settingsVisible = props.sidebarMode === 'settings' || (props.sidebarMode === 'instances' && props.instanceSettingsOpen);
283
- const returnToOverview = useRef(false);
284
- const closeInstanceSettings = () => {
285
- returnToOverview.current = true;
286
- props.onInstanceSettingsOpenChange(false);
287
- };
288
- useEffect(() => {
289
- // A declined dirty guard keeps settings open and leaves the workspace intact.
290
- if (!props.instanceSettingsOpen && returnToOverview.current) {
291
- returnToOverview.current = false;
292
- props.onDetailTabChange('overview');
293
- }
294
- }, [props.instanceSettingsOpen, props.onDetailTabChange]);
295
- const panelSettings = <SettingsPage onBack={closeInstanceSettings} key={selected?.port ?? 'manager-only'} {...settingsTarget}
296
- manager={managerSettings} scopes={selected ? ['instance', 'manager'] : ['manager']}
297
- onDirtyChange={panelDirty} {...(props.onSettingsSaved ? { onSaved: props.onSettingsSaved } : {})} />;
278
+ // The rail gear is the only settings entry point, and it renders manager scope only.
279
+ // Instance settings belong to the instance; see wp4 for how the manager reaches them.
280
+ const settingsVisible = props.sidebarMode === 'settings';
298
281
  const panelLayout = usePanelLayout();
299
282
  // Per-Files-tab resource state lives in the tab metadata (020 §5 Option B).
300
283
  // The registry's rightFolderRootPath remains the fallback root for tabs
@@ -316,6 +299,11 @@ export function SidebarRailRouter(props: Props) {
316
299
  const [previewInsertTextRequest, setPreviewInsertTextRequest] = useState<PreviewInsertTextRequest | null>(null);
317
300
  const previewInsertSeqRef = useRef(0);
318
301
  const previewInsertResolversRef = useRef(new Map<string, (result: PreviewInsertTextResult) => void>());
302
+ // Instance settings live in the instance. The manager only asks the mounted preview to
303
+ // open its own page; a one-shot id keeps repeat clicks addressable.
304
+ const [previewSettingsOpenRequest, setPreviewSettingsOpenRequest] = useState<string | null>(null);
305
+ const previewSettingsSeqRef = useRef(0);
306
+ const [instanceSettingsNotice, setInstanceSettingsNotice] = useState<string | null>(null);
319
307
  // 030 v2/v3: keep the server registry in sync, deliver shares to the
320
308
  // selected instance's runtime-context, and relay screenshot commands.
321
309
  useEmbeddedBrowserTargetSync(isElectron, props.selectedInstance?.port ?? null);
@@ -355,6 +343,16 @@ export function SidebarRailRouter(props: Props) {
355
343
  resolve(result);
356
344
  }, []);
357
345
 
346
+ const requestInstanceSettings = useCallback((): void => {
347
+ setInstanceSettingsNotice(null);
348
+ previewSettingsSeqRef.current += 1;
349
+ setPreviewSettingsOpenRequest(`settings-open-${previewSettingsSeqRef.current}`);
350
+ }, []);
351
+ const handlePreviewSettingsOpenResult = useCallback((id: string, result: PreviewInsertTextResult): void => {
352
+ setPreviewSettingsOpenRequest(current => current === id ? null : current);
353
+ setInstanceSettingsNotice(result.ok ? null : result.error);
354
+ }, []);
355
+
358
356
  // CEO: handled through right sidebar tab system now. CEO is hidden in v1,
359
357
  // but we keep the overlay console for non-Electron surfaces.
360
358
  const ceoConsoleOpen = !isElectron && props.jawCeoOpen;
@@ -493,10 +491,6 @@ export function SidebarRailRouter(props: Props) {
493
491
  return (
494
492
  <NotesCommandProvider>
495
493
  {props.jawCeoVoiceOverlay}
496
- <WorkbenchSettingsToggle open={settingsVisible} onToggle={() => {
497
- if (props.sidebarMode === 'settings') props.onSidebarModeChange('instances');
498
- else props.onInstanceSettingsOpenChange(!props.instanceSettingsOpen);
499
- }} />
500
494
  <WorkspaceLayout
501
495
  navigatorLabel={props.viewMode === 'code' && props.sidebarMode === 'instances' ? 'Code sessions' : undefined}
502
496
  sidebarCollapsed={settingsVisible || props.sidebarCollapsed}
@@ -598,11 +592,17 @@ export function SidebarRailRouter(props: Props) {
598
592
  <button type="button" className="state-dismiss" aria-label="Dismiss dropped file message" onClick={() => setDropNotice(null)}>X</button>
599
593
  </section>
600
594
  )}
595
+ {instanceSettingsNotice && (
596
+ <section className="state lifecycle-state" role="status">
597
+ <span>{instanceSettingsNotice}</span>
598
+ <button type="button" className="state-dismiss" aria-label="Dismiss instance settings message" onClick={() => setInstanceSettingsNotice(null)}>X</button>
599
+ </section>
600
+ )}
601
601
  <div className="workspace-surface-layer">
602
602
  <WorkspaceSurface active={props.sidebarMode === 'instances' && props.viewMode === 'jaw'}>
603
- <Workbench mode={props.activeDetailTab} onModeChange={props.onDetailTabChange} header={props.workbenchHeader} modeActions={props.jawCeoWorkbenchButton} active={props.sidebarMode === 'instances' && props.viewMode === 'jaw'} settingsOpen={props.instanceSettingsOpen} onSettingsClose={() => props.onInstanceSettingsOpenChange(false)} overview={props.detailContent('overview')} preview={(
604
- <InstancePreview instance={props.selectedInstance} data={props.data} enabled={props.previewEnabled} active={props.sidebarMode === 'instances' && !props.instanceSettingsOpen && props.activeDetailTab === 'preview'} refreshKey={props.previewRefreshKey} theme={props.previewTheme} {...(props.onOpenNotesFromPreview ? { onOpenNotesFromPreview: props.onOpenNotesFromPreview } : {})} onOpenDocFromPreview={handleRightPreviewFile} onPreviewDroppedFiles={handlePreviewDroppedFiles} docPanelCapable={desktopPanelsAvailable} previewInsertTextRequest={previewInsertTextRequest} onPreviewInsertTextResult={handlePreviewInsertTextResult} />
605
- )} logs={props.detailContent('logs')} settings={panelSettings} />
603
+ <Workbench mode={props.activeDetailTab} onModeChange={props.onDetailTabChange} header={props.workbenchHeader} modeActions={(<>{props.jawCeoWorkbenchButton}<button type="button" className="workbench-instance-settings-request" aria-label="Instance settings" title="Open this instance's own settings page" disabled={!props.selectedInstance?.ok} onClick={requestInstanceSettings}>Settings</button></>)} active={props.sidebarMode === 'instances' && props.viewMode === 'jaw'} overview={props.detailContent('overview')} preview={(
604
+ <InstancePreview instance={props.selectedInstance} data={props.data} enabled={props.previewEnabled} active={props.sidebarMode === 'instances' && props.activeDetailTab === 'preview'} refreshKey={props.previewRefreshKey} theme={props.previewTheme} {...(props.onOpenNotesFromPreview ? { onOpenNotesFromPreview: props.onOpenNotesFromPreview } : {})} onOpenDocFromPreview={handleRightPreviewFile} onPreviewDroppedFiles={handlePreviewDroppedFiles} docPanelCapable={desktopPanelsAvailable} previewInsertTextRequest={previewInsertTextRequest} onPreviewInsertTextResult={handlePreviewInsertTextResult} previewSettingsOpenRequest={previewSettingsOpenRequest} onPreviewSettingsOpenResult={handlePreviewSettingsOpenResult} />
605
+ )} logs={props.detailContent('logs')} />
606
606
  </WorkspaceSurface>
607
607
  {props.viewMode === 'code' && props.sidebarMode === 'instances' ? (
608
608
  <WorkspaceSurface active>
@@ -615,10 +615,14 @@ export function SidebarRailRouter(props: Props) {
615
615
  <NotesWorkspace active={props.sidebarMode === 'notes'} selectedPath={props.notesSelectedPath} selectedNote={props.notesSelectedNote} vaultIndex={props.notesModel.index} viewMode={props.notesViewMode} authoringMode={props.notesAuthoringMode} wordWrap={props.notesWordWrap} vimMode={props.notesVimMode} treeWidth={props.notesTreeWidth} notesGraphSettings={props.notesGraphSettings} tagFilter={props.notesModel.tagFilter} onOpenSidebarSearch={props.onOpenNotesSearch} onSelectedPathChange={props.onNotesSelectedPathChange} onDirtyPathChange={props.onNotesDirtyPathChange} onViewModeChange={props.onNotesViewModeChange} onAuthoringModeChange={props.onNotesAuthoringModeChange} onWordWrapChange={props.onNotesWordWrapChange} onVimModeChange={props.onNotesVimModeChange} onTreeWidthChange={props.onNotesTreeWidthChange} onNotesGraphSettingsChange={props.onNotesGraphSettingsChange} onTagSelect={props.notesModel.setTagFilter} onWikiLinkNavigate={props.onNotesSelectedPathChange} />
616
616
  </WorkspaceSurface>
617
617
  <WorkspaceSurface active={props.sidebarMode === 'settings'}>
618
+ {/* Manager scope only. The selected instance is still supplied because
619
+ 'Dashboard meta' edits that instance's manager-registry row and is
620
+ otherwise unreachable; scopes={['manager']} is what keeps every
621
+ instance-owned page out of this surface. */}
618
622
  {props.sidebarMode === 'settings' && <SettingsPage key={selected?.port ?? 'manager-only'} {...settingsTarget}
619
623
  onBack={() => props.onSidebarModeChange('instances')} title="Dashboard settings"
620
624
  initialId={({ display: 'manager-display', activity: 'manager-activity', developer: 'manager-developer', embedding: 'manager-embedding', telegramHub: 'telegram-hub' } as const)[props.settingsSection]}
621
- manager={managerSettings} scopes={selected ? ['instance', 'manager'] : ['manager']} onDirtyChange={dashboardDirty}
625
+ manager={managerSettings} scopes={['manager']} onDirtyChange={dashboardDirty}
622
626
  {...(props.onSettingsSaved ? { onSaved: props.onSettingsSaved } : {})} />}
623
627
  </WorkspaceSurface>
624
628
  <WorkspaceSurface active={props.sidebarMode === 'board'}>
@@ -652,7 +656,7 @@ export function SidebarRailRouter(props: Props) {
652
656
  />
653
657
  )}
654
658
  sidePanel={(!isElectron && ceoConsoleOpen) ? jawCeoPanel : undefined}
655
- mobileNav={<MobileNav activeTab={props.instanceSettingsOpen ? 'settings' : props.activeDetailTab} onOpenInstances={props.onOpenDrawer} onSelectTab={props.onSelectTab} onToggleActivity={props.onToggleActivityFromMobile} />}
659
+ mobileNav={<MobileNav activeTab={props.activeDetailTab} onOpenInstances={props.onOpenDrawer} onSelectTab={props.onSelectTab} onToggleActivity={props.onToggleActivityFromMobile} />}
656
660
  drawer={(
657
661
  <InstanceDrawer open={props.drawerOpen} profileFilters={props.drawerProfileFilters} onClose={props.onCloseDrawer}>
658
662
  {props.instanceListContent}
@@ -1,4 +1,6 @@
1
- import { useRef, useState, type KeyboardEvent } from 'react';
1
+ import { useCallback, useRef, useState, type KeyboardEvent } from 'react';
2
+ import { MicGlyph, SendGlyph, StopGlyph } from './ProviderGlyph';
3
+ import { useDictation } from './use-dictation';
2
4
 
3
5
  type CodeComposerProps = {
4
6
  inputText: string;
@@ -18,6 +20,12 @@ export function CodeComposer(props: CodeComposerProps) {
18
20
  const sending = useRef(false);
19
21
  const cancelling = useRef(false);
20
22
  const [error, setError] = useState<string | null>(null);
23
+ // Dictated text is appended to the draft, never sent on its own.
24
+ const appendDictation = useCallback((text: string) => {
25
+ const current = props.inputText;
26
+ props.onInputChange(current ? `${current.replace(/\s+$/, '')} ${text}` : text);
27
+ }, [props.inputText, props.onInputChange]);
28
+ const dictation = useDictation(appendDictation, props.readOnly);
21
29
  async function submit() {
22
30
  if (!props.canSend || !props.inputText.trim() || sending.current) return;
23
31
  sending.current = true;
@@ -50,10 +58,27 @@ export function CodeComposer(props: CodeComposerProps) {
50
58
  rows={2} readOnly={props.readOnly} />
51
59
  <span className="code-composer-hint">Enter to send · Shift+Enter for a new line</span>
52
60
  </div>
53
- {props.busy ? <button type="button" className="code-composer-send code-composer-stop" aria-label="Stop current turn"
54
- disabled={!props.canStop || props.stopping} onClick={() => void stop()}>{props.stopping ? 'Stopping…' : 'Stop'}</button>
55
- : <button type="button" className="code-composer-send" aria-label="Send prompt"
56
- disabled={!props.canSend || !props.inputText.trim()} onClick={() => void submit()}>{props.pending ? 'Sending…' : 'Send'}</button>}
57
- {error && <div className="code-action-error" role="alert">{error}</div>}
61
+ <div className="code-composer-actions">
62
+ <button type="button" className={`code-composer-icon-button${dictation.status === 'recording' ? ' is-recording' : ''}`}
63
+ aria-label={dictation.status === 'recording' ? 'Stop dictation' : 'Dictation'}
64
+ aria-pressed={dictation.status === 'recording'}
65
+ disabled={dictation.status === 'unsupported' || dictation.status === 'transcribing' || props.readOnly}
66
+ title={dictation.reason} onClick={dictation.toggle}>
67
+ <MicGlyph muted={dictation.status === 'unsupported'} />
68
+ </button>
69
+ {props.busy
70
+ ? <button type="button" className="code-composer-send code-composer-stop" aria-label="Stop current turn"
71
+ title={props.stopping ? 'Stopping' : 'Stop current turn'}
72
+ disabled={!props.canStop || props.stopping} onClick={() => void stop()}><StopGlyph /></button>
73
+ : <button type="button" className="code-composer-send" aria-label="Send prompt"
74
+ title={props.pending ? 'Sending' : 'Send prompt'}
75
+ disabled={!props.canSend || !props.inputText.trim()} onClick={() => void submit()}><SendGlyph /></button>}
76
+ </div>
77
+ {/* An icon button cannot announce progress on its own. */}
78
+ {(props.stopping || props.pending || dictation.status === 'recording' || dictation.status === 'transcribing') &&
79
+ <span className="code-composer-status" role="status">
80
+ {props.stopping ? 'Stopping…' : props.pending ? 'Sending…'
81
+ : dictation.status === 'recording' ? 'Recording…' : 'Transcribing…'}</span>}
82
+ {(error || dictation.error) && <div className="code-action-error" role="alert">{error ?? dictation.error}</div>}
58
83
  </div>;
59
84
  }
@@ -2,6 +2,7 @@ import { useId, useRef, useState } from 'react';
2
2
  import type { CodeSessionInfo } from '../../../../src/code-mode/wire';
3
3
  import type { CodeControllerModel } from './code-controller-types';
4
4
  import { CODE_RUNTIME_LABELS, CODE_SESSION_LABELS, codeCanResume, codeSessionBusy } from './code-types';
5
+ import { codeSessionAttention, codeSessionAttentionLabel, groupCodeSessions } from './session-order';
5
6
 
6
7
  function SessionRow({ session: s, controller: c }: { session: CodeSessionInfo; controller: CodeControllerModel }) {
7
8
  const [renaming, setRenaming] = useState(false);
@@ -14,6 +15,7 @@ function SessionRow({ session: s, controller: c }: { session: CodeSessionInfo; c
14
15
  const active = c.selectedId === s.sessionId;
15
16
  const busy = codeSessionBusy(s);
16
17
  const count = active && c.synced ? c.permissions.length : s.pendingPermissionCount;
18
+ const attention = codeSessionAttention(count);
17
19
  async function action(run: () => Promise<void>, after?: () => void) {
18
20
  if (guard.current) return;
19
21
  guard.current = true; setPending(true); setError(null);
@@ -27,8 +29,13 @@ function SessionRow({ session: s, controller: c }: { session: CodeSessionInfo; c
27
29
  aria-current={active ? 'true' : undefined} onClick={() => void action(() => c.selectSession(s.sessionId))}>
28
30
  <span className="code-session-cwd">{s.title || 'Untitled session'}</span>
29
31
  <span className="code-session-meta" title={s.cwd}>{CODE_RUNTIME_LABELS[s.provider]} · {s.cwd}</span>
30
- <span className={`code-session-status code-session-status-${s.status}`}>{CODE_SESSION_LABELS[s.status]}</span>
31
- <span className="code-session-attention">{count === undefined ? 'Approval status unknown' : count > 0 ? `${count} pending approval${count === 1 ? '' : 's'}` : 'No pending approvals'}</span>
32
+ {/* Ready is the common case and stays silent. A status on every row
33
+ is a status on no row, and it costs the one session that is
34
+ actually doing something its visibility. */}
35
+ <span className={`code-session-status code-session-status-${s.status}`}
36
+ data-quiet={s.status === 'idle' ? 'true' : undefined}>{CODE_SESSION_LABELS[s.status]}</span>
37
+ <span className={`code-session-attention code-session-attention-${attention.kind}`}
38
+ data-quiet={attention.kind === 'none' ? 'true' : undefined}>{codeSessionAttentionLabel(attention)}</span>
32
39
  </button>
33
40
  {renaming ? <form className="code-session-rename" onSubmit={event => {
34
41
  event.preventDefault(); if (title.trim()) void action(() => c.rename(s.sessionId, title.trim()), finishRename);
@@ -63,8 +70,19 @@ export function CodeSessionList({ controller: c }: { controller: CodeControllerM
63
70
  const [paging, setPaging] = useState(false);
64
71
  const pagingRef = useRef(false);
65
72
  const visible = c.sessions.filter(s => `${s.title ?? ''} ${s.cwd} ${CODE_RUNTIME_LABELS[s.provider]}`.toLowerCase().includes(search.toLowerCase()));
66
- const groups = new Map<string, CodeSessionInfo[]>();
67
- for (const s of visible) { const key = grouped ? s.cwd : ''; const rows = groups.get(key) ?? []; rows.push(s); groups.set(key, rows); }
73
+ // Two orderings, deliberately: by workspace when the reader asks for it,
74
+ // otherwise by lifecycle. Neither uses last activity, so answering a prompt
75
+ // in one session does not move it under the reader's cursor -- which is
76
+ // what the server's own last-used ordering would do.
77
+ const groups = grouped
78
+ ? [...visible.reduce((map, s) => {
79
+ const rows = map.get(s.cwd) ?? []; rows.push(s); map.set(s.cwd, rows); return map;
80
+ }, new Map<string, CodeSessionInfo[]>())].map(([cwd, sessions]) => ({ title: cwd, key: cwd, sessions }))
81
+ : groupCodeSessions(visible).map(group => ({
82
+ key: group.section,
83
+ title: group.section === 'archived' ? 'Archived' : '',
84
+ sessions: group.sessions,
85
+ }));
68
86
  async function loadMore() {
69
87
  if (pagingRef.current) return;
70
88
  pagingRef.current = true; setPaging(true); setError(null);
@@ -90,9 +108,9 @@ export function CodeSessionList({ controller: c }: { controller: CodeControllerM
90
108
  <button type="button" className={`code-session-item${c.selectedId === null ? ' active' : ''}`} aria-current={c.selectedId === null ? 'true' : undefined}
91
109
  onClick={c.newSession}><span className="code-session-cwd">New session draft</span><span className="code-session-meta">Preserved unsent draft</span></button>
92
110
  {c.loading && <div className="code-session-list-loading" role="status">Loading sessions…</div>}
93
- {[...groups].map(([cwd, sessions]) => <section className="code-session-group" key={cwd}>
94
- {grouped && <h3 className="code-session-group-title" title={cwd}>{cwd}</h3>}
95
- <ul className="code-session-list-items">{sessions.map(s => <SessionRow key={s.sessionId} session={s} controller={c} />)}</ul>
111
+ {groups.map(group => <section className="code-session-group" key={group.key}>
112
+ {group.title && <h3 className="code-session-group-title" title={group.title}>{group.title}</h3>}
113
+ <ul className="code-session-list-items">{group.sessions.map(s => <SessionRow key={s.sessionId} session={s} controller={c} />)}</ul>
96
114
  </section>)}
97
115
  {!c.loading && !visible.length && <p className="code-session-list-empty">{search ? 'No loaded sessions match. Clear search or load more.' : 'No sessions here. Start with the new session draft.'}</p>}
98
116
  {c.hasMoreSessions && <button type="button" className="code-inline-action" disabled={paging} onClick={() => void loadMore()}>{paging ? 'Loading…' : 'Load more sessions'}</button>}
@@ -0,0 +1,49 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { codeToastAutoDismisses, type CodeToast } from './code-toasts';
3
+
4
+ function ToastCard({ toast, onDismiss }: { toast: CodeToast; onDismiss(id: string): void }) {
5
+ const [held, setHeld] = useState(false);
6
+ const card = useRef<HTMLDivElement>(null);
7
+ useEffect(() => {
8
+ // Held while the reader is pointing at it or has tabbed into it: a
9
+ // notice that expires mid-read is worse than one that lingers, and
10
+ // unmounting a card that owns focus drops the caret to the document
11
+ // body and loses the reader's place in the composer.
12
+ if (held || !codeToastAutoDismisses(toast)) return;
13
+ const timer = setTimeout(() => onDismiss(toast.id), toast.durationMs);
14
+ return () => clearTimeout(timer);
15
+ // The revision restarts the timer when the same notice is pushed again.
16
+ }, [toast.id, toast.revision, toast.durationMs, onDismiss, held]);
17
+ return <div ref={card} className={`code-toast code-toast-${toast.variant}`}
18
+ onPointerEnter={() => setHeld(true)} onPointerLeave={() => setHeld(false)}
19
+ onFocusCapture={() => setHeld(true)}
20
+ onBlurCapture={event => { if (!card.current?.contains(event.relatedTarget as Node | null)) setHeld(false); }}>
21
+ <span className="code-toast-message">{toast.message}</span>
22
+ {/* A short static name: the button sits inside the live region, so a
23
+ label repeating the message would announce the whole notice twice. */}
24
+ <button type="button" className="code-toast-dismiss" aria-label="Dismiss notice"
25
+ onClick={() => onDismiss(toast.id)}>×</button>
26
+ </div>;
27
+ }
28
+
29
+ /**
30
+ * Notices float over the top of the transcript rather than pushing the composer
31
+ * down, so reading one never moves the thing the reader is typing into.
32
+ *
33
+ * Two regions, not one: an assertive live region interrupts, and mixing an
34
+ * alert into a polite region is unreliable because the container's politeness
35
+ * is what applies to an insertion. Errors go in the assertive one.
36
+ */
37
+ export function CodeToastHost({ toasts, onDismiss }: { toasts: readonly CodeToast[]; onDismiss(id: string): void }) {
38
+ // The region is always mounted, even empty. A live region that arrives in
39
+ // the DOM together with its first content is not announced -- assistive tech
40
+ // has to already be observing it when the insertion happens, which is also
41
+ // why it is not hidden while empty. One region, because every notice this
42
+ // surface raises is polite; anything that must interrupt stays inline where
43
+ // it can also stay on screen.
44
+ return <div className="code-toast-host">
45
+ <div className="code-toast-region" role="status">
46
+ {toasts.map(toast => <ToastCard key={toast.id} toast={toast} onDismiss={onDismiss} />)}
47
+ </div>
48
+ </div>;
49
+ }