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.
- package/README.md +3 -3
- package/dist/bin/commands/browser.js +52 -5
- package/dist/bin/commands/browser.js.map +1 -1
- package/dist/src/agent/args.js +69 -19
- package/dist/src/agent/args.js.map +1 -1
- package/dist/src/agent/codex-app-events.js +31 -1
- package/dist/src/agent/codex-app-events.js.map +1 -1
- package/dist/src/agent/cursor-model-inventory.js +140 -0
- package/dist/src/agent/cursor-model-inventory.js.map +1 -0
- package/dist/src/agent/grok-models.js +80 -0
- package/dist/src/agent/grok-models.js.map +1 -0
- package/dist/src/browser/actions.js +383 -20
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/grounding-candidate.js +272 -0
- package/dist/src/browser/grounding-candidate.js.map +1 -0
- package/dist/src/browser/grounding-eval.js +247 -0
- package/dist/src/browser/grounding-eval.js.map +1 -0
- package/dist/src/browser/image-size.js +63 -0
- package/dist/src/browser/image-size.js.map +1 -0
- package/dist/src/browser/index.js +1 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/occlusion.js +148 -0
- package/dist/src/browser/occlusion.js.map +1 -0
- package/dist/src/browser/verify-candidate.js +162 -0
- package/dist/src/browser/verify-candidate.js.map +1 -0
- package/dist/src/browser/vision-input.js +74 -0
- package/dist/src/browser/vision-input.js.map +1 -0
- package/dist/src/browser/vision-provider.js +75 -0
- package/dist/src/browser/vision-provider.js.map +1 -0
- package/dist/src/browser/vision.js +360 -47
- package/dist/src/browser/vision.js.map +1 -1
- package/dist/src/browser/web-ai/candidate-reconcile.js +14 -5
- package/dist/src/browser/web-ai/candidate-reconcile.js.map +1 -1
- package/dist/src/cli/claude-model-discovery.js +175 -0
- package/dist/src/cli/claude-model-discovery.js.map +1 -0
- package/dist/src/cli/claude-models.js +46 -0
- package/dist/src/cli/claude-models.js.map +1 -1
- package/dist/src/cli/registry-live.js +104 -1
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/cli/registry.js +28 -5
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/code-mode/host.js +6 -0
- package/dist/src/code-mode/host.js.map +1 -1
- package/dist/src/code-mode/manager.js +37 -8
- package/dist/src/code-mode/manager.js.map +1 -1
- package/dist/src/code-mode/providers/catalog.js +53 -1
- package/dist/src/code-mode/providers/catalog.js.map +1 -1
- package/dist/src/code-mode/providers/codex-app.js +6 -0
- package/dist/src/code-mode/providers/codex-app.js.map +1 -1
- package/dist/src/code-mode/providers/live-models.js +85 -0
- package/dist/src/code-mode/providers/live-models.js.map +1 -0
- package/dist/src/code-mode/providers/provider-live-models.js +120 -0
- package/dist/src/code-mode/providers/provider-live-models.js.map +1 -0
- package/dist/src/code-mode/session.js +37 -0
- package/dist/src/code-mode/session.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +24 -14
- package/dist/src/prompt/templates/control-system.md +6 -11
- package/dist/src/prompt/templates/employee.md +5 -5
- package/dist/src/routes/browser.js +17 -5
- package/dist/src/routes/browser.js.map +1 -1
- package/package.json +1 -1
- package/public/css/activity.css +20 -1
- package/public/css/layout.css +7 -3
- package/public/dist/.vite/manifest.json +104 -105
- package/public/dist/assets/Agent-CQ4EBgY8.js +1 -0
- package/public/dist/assets/CodeCanvas-DWsx6sv5.js +2 -0
- package/public/dist/assets/DashboardMeta-Dh-tasbY.js +1 -0
- package/public/dist/assets/Display-NrqY_IaN.js +1 -0
- package/public/dist/assets/{Permissions-BzbnqenZ.js → Permissions-Bh947tUG.js} +1 -1
- package/public/dist/assets/Permissions-DzNJCt8K.js +1 -0
- package/public/dist/assets/app-BCBnxiGr.css +1 -0
- package/public/dist/assets/{app-x5Kray4W.js → app-Q0prKOFx.js} +4 -4
- package/public/dist/assets/{bgtask-badge-W9djRQVu.js → bgtask-badge-DAcWaUfS.js} +1 -1
- package/public/dist/assets/bgtask-badge-DHA2OQm8.js +1 -0
- package/public/dist/assets/{employees--_dmDw_a.js → employees-CWS3Juca.js} +1 -1
- package/public/dist/assets/{iframe-renderer-D4BmLtAq.js → iframe-renderer-DCDKBMb7.js} +2 -2
- package/public/dist/assets/iframe-renderer-RpvBly2x.js +1 -0
- package/public/dist/assets/manager-Nk7-RYPs.css +1 -0
- package/public/dist/assets/manager-XkR-DRaQ.js +5 -0
- package/public/dist/assets/manager-shortcuts-jsiX-zV6.js +1 -0
- package/public/dist/assets/memory-4YH8sz0L.js +1 -0
- package/public/dist/assets/{memory-DENQWzM8.js → memory-DhNdKZLO.js} +1 -1
- package/public/dist/assets/{message-history-BsrLcGJj.js → message-history-DbRTl1P0.js} +1 -1
- package/public/dist/assets/message-history-lVcLpAw8.js +1 -0
- package/public/dist/assets/{render-CF6Q1oIh.js → render-DP8SJtdZ.js} +1 -1
- package/public/dist/assets/settings--EGy3_B9.js +1 -0
- package/public/dist/assets/settings-85n9NjvX.js +1 -0
- package/public/dist/assets/{settings-B-C_jdZR.js → settings-C9_D-pn0.js} +32 -32
- package/public/dist/assets/settings-core-8wBl1mg-.js +1 -0
- package/public/dist/assets/{settings-core-BzAJLsKN.js → settings-core-BcVD2FgV.js} +1 -1
- package/public/dist/assets/{settings-embedding-dEMyByVi.js → settings-embedding-BSpq785l.js} +1 -1
- package/public/dist/assets/{settings-embedding-BSA42oKM.css → settings-embedding-LIMq5ebM.css} +1 -1
- package/public/dist/assets/{sidebar-fFEOpvJl.js → sidebar-BVhLb5Ft.js} +3 -3
- package/public/dist/assets/skills-6GFkHTlQ.js +1 -0
- package/public/dist/assets/{skills-Da_b06ci.js → skills-X7qxmHka.js} +1 -1
- package/public/dist/assets/{slash-commands-BbPUHHF4.js → slash-commands-BnDH4IU_.js} +1 -1
- package/public/dist/assets/slash-commands-qu8fPAZ3.js +1 -0
- package/public/dist/assets/{trace-drawer-B-QmzUfP.js → trace-drawer-DasaS1z4.js} +1 -1
- package/public/dist/assets/{ui-CkRUrIfp.js → ui-BtiBTXng.js} +1 -1
- package/public/dist/assets/ui-BucJ1zMA.js +1 -0
- package/public/dist/index.html +2 -2
- package/public/dist/manager/index.html +3 -3
- package/public/dist/settings/index.html +2 -2
- package/public/js/features/settings.ts +16 -0
- package/public/manager/src/App.tsx +10 -12
- package/public/manager/src/AppChrome.tsx +2 -3
- package/public/manager/src/InstancePreview.tsx +47 -0
- package/public/manager/src/SidebarRailRouter.tsx +34 -30
- package/public/manager/src/code/CodeComposer.tsx +31 -6
- package/public/manager/src/code/CodeSessionList.tsx +25 -7
- package/public/manager/src/code/CodeToastHost.tsx +49 -0
- package/public/manager/src/code/CodeTranscript.tsx +136 -26
- package/public/manager/src/code/CodeWorkbench.tsx +16 -2
- package/public/manager/src/code/ComposerFooter.tsx +130 -46
- package/public/manager/src/code/ContextUsageMeter.tsx +72 -0
- package/public/manager/src/code/ProviderGlyph.tsx +58 -0
- package/public/manager/src/code/code-composer.css +159 -175
- package/public/manager/src/code/code-controller-runtime.ts +3 -1
- package/public/manager/src/code/code-session-state.ts +7 -1
- package/public/manager/src/code/code-toasts.ts +73 -0
- package/public/manager/src/code/code-workbench.css +42 -1
- package/public/manager/src/code/code.css +130 -15
- package/public/manager/src/code/context-usage.ts +74 -0
- package/public/manager/src/code/pending-user-item.ts +46 -0
- package/public/manager/src/code/session-order.ts +70 -0
- package/public/manager/src/code/tool-summary.ts +101 -0
- package/public/manager/src/code/use-dictation.ts +130 -0
- package/public/manager/src/components/CommandCenter.tsx +4 -8
- package/public/manager/src/components/InstanceDetailPanel.tsx +0 -18
- package/public/manager/src/components/Workbench.tsx +6 -10
- package/public/manager/src/components/WorkbenchHeader.tsx +0 -11
- package/public/manager/src/dashboard-settings/dashboard-settings-ui.ts +3 -1
- package/public/manager/src/hooks/useDashboardView.ts +38 -38
- package/public/manager/src/manager-components.css +0 -8
- package/public/manager/src/manager-shortcut-runner.ts +0 -1
- package/public/manager/src/manager-shortcuts.ts +0 -2
- package/public/manager/src/settings/pages/Agent.tsx +5 -2
- package/public/manager/src/settings/pages/DashboardMeta.tsx +20 -1
- package/public/manager/src/settings/pages/Permissions.tsx +23 -4
- package/public/manager/src/settings/pages/components/agent/PermissionQuickSection.tsx +6 -2
- package/public/manager/src/settings/pages/components/agent/agent-save.ts +24 -1
- package/public/manager/src/settings/settings-icons.ts +3 -1
- package/public/manager/src/settings/settings-registry.ts +3 -3
- package/public/manager/src/settings-controls.css +0 -1
- package/public/manager/src/settings-standalone.tsx +8 -8
- package/public/manager/src/types.ts +0 -1
- package/scripts/check-private-boundary.mjs +101 -0
- package/scripts/grounding-eval.mjs +223 -0
- package/public/dist/assets/Agent-B8ZEWJ6B.js +0 -1
- package/public/dist/assets/CodeCanvas-Bj3lTSu6.js +0 -2
- package/public/dist/assets/DashboardMeta-CmZavI4H.js +0 -1
- package/public/dist/assets/Display-DFkjKcMk.js +0 -1
- package/public/dist/assets/Permissions-CJftCO6p.js +0 -1
- package/public/dist/assets/app-DVbrIl8f.css +0 -1
- package/public/dist/assets/bgtask-badge-CuAc8bAl.js +0 -1
- package/public/dist/assets/iframe-renderer-By2WO5bd.js +0 -1
- package/public/dist/assets/manager-CxLXWCGi.css +0 -1
- package/public/dist/assets/manager-DA3dF3eN.js +0 -5
- package/public/dist/assets/manager-shortcuts-CzGkE0ZG.js +0 -1
- package/public/dist/assets/memory-thjr2apY.js +0 -1
- package/public/dist/assets/message-history-CfU7_P5t.js +0 -1
- package/public/dist/assets/settings-DXA5L4DR.js +0 -1
- package/public/dist/assets/settings-Dy_IaNVH.js +0 -1
- package/public/dist/assets/settings-core-D7sAQqhC.js +0 -1
- package/public/dist/assets/skills-BcIievZp.js +0 -1
- package/public/dist/assets/slash-commands-B-5Shayl.js +0 -1
- 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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/dist/assets/settings-embedding-
|
|
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,
|
|
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.
|
|
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) ??
|
|
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,
|
|
247
|
-
view, settingsDirty,
|
|
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') {
|
|
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: () =>
|
|
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,
|
|
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,
|
|
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)}
|
|
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
|
-
|
|
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} />}
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
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'}
|
|
604
|
-
<InstancePreview instance={props.selectedInstance} data={props.data} enabled={props.previewEnabled} active={props.sidebarMode === 'instances' &&
|
|
605
|
-
)} logs={props.detailContent('logs')}
|
|
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={
|
|
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.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
{
|
|
94
|
-
{
|
|
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
|
+
}
|