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
@@ -9,9 +9,6 @@ type WorkbenchProps = {
9
9
  overview: ReactNode;
10
10
  preview: ReactNode;
11
11
  logs: ReactNode;
12
- settings: ReactNode;
13
- settingsOpen: boolean;
14
- onSettingsClose: () => void;
15
12
  active: boolean;
16
13
  };
17
14
 
@@ -23,8 +20,8 @@ function modeLabel(mode: DashboardDetailTab): string {
23
20
 
24
21
  export function Workbench(props: WorkbenchProps) {
25
22
  return (
26
- <section className={`workbench workbench-${props.mode}`} data-instance-settings-open={props.settingsOpen} aria-label="Selected instance workbench">
27
- <div className="workbench-header" hidden={props.settingsOpen}>
23
+ <section className={`workbench workbench-${props.mode}`} aria-label="Selected instance workbench">
24
+ <div className="workbench-header">
28
25
  {props.header}
29
26
  <div className="workbench-mode-bar">
30
27
  <div className="workbench-mode-tabs" role="tablist" aria-label="Workbench modes"
@@ -56,7 +53,7 @@ export function Workbench(props: WorkbenchProps) {
56
53
  </div>
57
54
  </div>
58
55
  <div className="workbench-body">
59
- {!props.settingsOpen && props.mode === 'overview' && (
56
+ {props.mode === 'overview' && (
60
57
  <div key="overview" id="workbench-panel-overview" role="tabpanel" aria-labelledby="workbench-tab-overview" className="workbench-panel workbench-panel-overview">{props.overview}</div>
61
58
  )}
62
59
  <div
@@ -65,16 +62,15 @@ export function Workbench(props: WorkbenchProps) {
65
62
  role="tabpanel"
66
63
  aria-labelledby="workbench-tab-preview"
67
64
  className="workbench-panel workbench-panel-preview"
68
- hidden={props.settingsOpen || props.mode !== 'preview'}
69
- aria-hidden={props.settingsOpen || props.mode !== 'preview'}
65
+ hidden={props.mode !== 'preview'}
66
+ aria-hidden={props.mode !== 'preview'}
70
67
  data-preview-host="persistent"
71
68
  >
72
69
  {props.preview}
73
70
  </div>
74
- {!props.settingsOpen && props.mode === 'logs' && (
71
+ {props.mode === 'logs' && (
75
72
  <div key="logs" id="workbench-panel-logs" role="tabpanel" aria-labelledby="workbench-tab-logs" className="workbench-panel workbench-panel-logs">{props.logs}</div>
76
73
  )}
77
- {props.settingsOpen && <div id="workbench-instance-settings" className="workbench-settings-page">{props.settings}</div>}
78
74
  </div>
79
75
  </section>
80
76
  );
@@ -107,14 +107,3 @@ export function WorkbenchHeader(props: WorkbenchHeaderProps) {
107
107
  </div>
108
108
  );
109
109
  }
110
-
111
- export function WorkbenchSettingsToggle(props: { open: boolean; onToggle: () => void }) {
112
- const [slot, setSlot] = useState<HTMLElement | null>(null);
113
- useEffect(() => { setSlot(document.getElementById('command-settings-slot')); }, []);
114
- if (!slot) return null;
115
- return createPortal(<button type="button" className="workbench-settings-toggle" aria-label="Instance settings"
116
- title="Instance settings" aria-pressed={props.open} aria-controls="workbench-instance-settings"
117
- onClick={props.onToggle}>
118
- <span aria-hidden="true" dangerouslySetInnerHTML={{ __html: icon('settings', 16) }} />
119
- </button>, slot);
120
- }
@@ -11,7 +11,9 @@ export function dashboardSettingsUiFromView(
11
11
  return {
12
12
  selectedPort: view.selectedPort,
13
13
  selectedTab: view.activeDetailTab,
14
- instanceSettingsOpen: view.instanceSettingsOpen,
14
+ // Retained in the schema for one version so older clients and PATCH callers do not
15
+ // 400, but the rail owns settings now and nothing opens a Workbench panel.
16
+ instanceSettingsOpen: false,
15
17
  sidebarCollapsed: view.sidebarCollapsed,
16
18
  activityDockCollapsed: view.activityDockCollapsed,
17
19
  activityDockHeight: view.activityDockHeight,
@@ -6,7 +6,6 @@ import type { DashboardDetailTab, DashboardDiffMode, DashboardDiffRootPolicy, Da
6
6
  export function useDashboardView() {
7
7
  const [selectedPort, setSelectedPort] = useState<number | null>(null);
8
8
  const [activeDetailTab, setActiveDetailTab] = useState<DashboardDetailTab>('overview');
9
- const [instanceSettingsOpen, setInstanceSettingsOpen] = useState(false);
10
9
  const [drawerOpen, setDrawerOpen] = useState(false);
11
10
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
12
11
  const [activityDockCollapsed, setActivityDockCollapsed] = useState(false);
@@ -42,7 +41,6 @@ export function useDashboardView() {
42
41
  return {
43
42
  selectedPort,
44
43
  setSelectedPort,
45
- instanceSettingsOpen, setInstanceSettingsOpen,
46
44
  activeDetailTab,
47
45
  setActiveDetailTab,
48
46
  drawerOpen,
@@ -102,19 +100,26 @@ export function useDashboardView() {
102
100
  };
103
101
  }
104
102
 
105
- export function hydrateInstanceSettings(ui: Pick<DashboardRegistryUi, 'selectedTab'> & { instanceSettingsOpen?: unknown }) {
103
+ /**
104
+ * The in-Workbench instance settings panel is gone; the rail owns settings now. A registry
105
+ * written by an older client can still carry `instanceSettingsOpen: true` or the retired
106
+ * `selectedTab: 'settings'`, so map either onto the rail's sidebar mode rather than dropping
107
+ * the user on a workspace whose settings tab no longer exists. The field stays accepted by
108
+ * the server normalizer for one version; nothing writes it any more.
109
+ */
110
+ export function hydrateInstanceSettings(ui: Pick<DashboardRegistryUi, 'selectedTab' | 'sidebarMode'> & { instanceSettingsOpen?: unknown }) {
111
+ const legacySettingsRequest = ui.selectedTab === 'settings' || ui.instanceSettingsOpen === true;
106
112
  return { selectedTab: ui.selectedTab === 'settings' ? 'overview' as const : ui.selectedTab,
107
- instanceSettingsOpen: ui.selectedTab === 'settings' || ui.instanceSettingsOpen === true };
113
+ sidebarMode: legacySettingsRequest ? 'settings' as const : ui.sidebarMode };
108
114
  }
109
115
 
110
116
  type SettingsNavigation = {
111
- view: Pick<ReturnType<typeof useDashboardView>, 'instanceSettingsOpen' | 'activeDetailTab' | 'sidebarMode' |
112
- 'setSelectedPort' | 'setInstanceSettingsOpen' | 'setSidebarMode' | 'setViewMode' | 'setDrawerOpen'>;
117
+ view: Pick<ReturnType<typeof useDashboardView>, 'activeDetailTab' | 'sidebarMode' |
118
+ 'setSelectedPort' | 'setSidebarMode' | 'setViewMode' | 'setDrawerOpen'>;
113
119
  selectedPort: number | null;
114
120
  settingsDirty: boolean;
115
- panelSettingsDirty: boolean;
116
121
  dashboardSettingsDirty: boolean;
117
- clearDirty: (entry: 'panel' | 'dashboard') => void;
122
+ clearDirty: (entry: 'dashboard') => void;
118
123
  saveUi: (patch: Partial<DashboardRegistryUi>) => Promise<void>;
119
124
  confirmDiscard?: () => boolean;
120
125
  };
@@ -125,49 +130,44 @@ export function createInstanceSettingsNavigation(args: SettingsNavigation) {
125
130
  (args.confirmDiscard ?? (() => window.confirm('Discard unsaved Settings changes?')))();
126
131
  }
127
132
  function guardSettingsTransition(target: Partial<{
128
- sidebarMode: DashboardSidebarMode; selectedPort: number | null; instanceSettingsOpen: boolean;
133
+ sidebarMode: DashboardSidebarMode; selectedPort: number | null;
129
134
  }>): boolean {
130
135
  const portChanged = target.selectedPort !== undefined && target.selectedPort !== selectedPort;
131
136
  const leavingDashboard = view.sidebarMode === 'settings' && (target.sidebarMode ?? view.sidebarMode) !== 'settings';
132
- const closingPanel = view.instanceSettingsOpen && !(target.instanceSettingsOpen ?? view.instanceSettingsOpen);
133
- const departing = (['panel', 'dashboard'] as const).filter(entry =>
134
- portChanged || (entry === 'panel' ? closingPanel : leavingDashboard));
135
- const dirty = departing.some(entry => entry === 'panel' ? args.panelSettingsDirty : args.dashboardSettingsDirty);
136
- if (dirty && !(args.confirmDiscard ?? (() => window.confirm('Discard unsaved Settings changes?')))()) return false;
137
- for (const entry of departing) args.clearDirty(entry);
137
+ // The rail workspace is the only settings surface, so there is one draft to guard.
138
+ const departing = portChanged || leavingDashboard;
139
+ if (!departing) return true;
140
+ if (args.dashboardSettingsDirty
141
+ && !(args.confirmDiscard ?? (() => window.confirm('Discard unsaved Settings changes?')))()) return false;
142
+ args.clearDirty('dashboard');
138
143
  return true;
139
144
  }
140
- function setInstanceSettingsOpen(open: boolean, port = selectedPort): void {
141
- if (!guardSettingsTransition({ selectedPort: port, instanceSettingsOpen: open,
142
- sidebarMode: open ? 'instances' : view.sidebarMode })) return;
143
- if (open) {
144
- view.setSelectedPort(port); view.setSidebarMode('instances');
145
- view.setViewMode('jaw'); view.setDrawerOpen(false);
146
- }
147
- view.setInstanceSettingsOpen(open);
148
- void args.saveUi({ instanceSettingsOpen: open,
149
- selectedTab: view.activeDetailTab === 'settings' ? 'overview' : view.activeDetailTab,
150
- ...(open ? { selectedPort: port, sidebarMode: 'instances' as const } : {}) });
145
+ /** Opens or closes the rail's manager settings workspace, honouring the dirty guard. */
146
+ function setDashboardSettingsOpen(open: boolean): void {
147
+ const sidebarMode = open ? 'settings' as const : 'instances' as const;
148
+ if (view.sidebarMode === sidebarMode) return;
149
+ if (!guardSettingsTransition({ sidebarMode })) return;
150
+ if (open) { view.setViewMode('jaw'); view.setDrawerOpen(false); }
151
+ view.setSidebarMode(sidebarMode);
152
+ void args.saveUi({ sidebarMode,
153
+ selectedTab: view.activeDetailTab === 'settings' ? 'overview' : view.activeDetailTab });
151
154
  }
152
- return { canLeaveDirtySettings, guardSettingsTransition, setInstanceSettingsOpen };
155
+ return { canLeaveDirtySettings, guardSettingsTransition, setDashboardSettingsOpen };
153
156
  }
154
157
 
155
- // Both entry points may remain mounted: a clean hidden Shell cannot clear its peer.
156
- export function settingsDirtyAfter(current: { panel: boolean; dashboard: boolean },
157
- entry: 'panel' | 'dashboard', dirty: boolean) {
158
+ // One settings surface remains, so there is one draft. The entry name is kept so the
159
+ // dirty-store call sites read the same as before.
160
+ export function settingsDirtyAfter(current: { dashboard: boolean },
161
+ entry: 'dashboard', dirty: boolean) {
158
162
  return { ...current, [entry]: dirty };
159
163
  }
160
164
  export function useSettingsDirtyState() {
161
- const [entries, setEntries] = useState({ panel: false, dashboard: false });
162
- const onSettingsDirtyChange = useCallback((entry: 'panel' | 'dashboard', dirty: boolean) => {
165
+ const [entries, setEntries] = useState({ dashboard: false });
166
+ const onSettingsDirtyChange = useCallback((entry: 'dashboard', dirty: boolean) => {
163
167
  setEntries(current => settingsDirtyAfter(current, entry, dirty));
164
168
  }, []);
165
- // SettingsShell keys its dirty notification and unmount cleanup to this callback.
166
- const onPanelSettingsDirtyChange = useCallback((dirty: boolean) => {
167
- onSettingsDirtyChange('panel', dirty);
168
- }, [onSettingsDirtyChange]);
169
169
  const setSettingsDirty = useCallback((dirty: boolean) => {
170
- setEntries({ panel: dirty, dashboard: dirty });
170
+ setEntries({ dashboard: dirty });
171
171
  }, []);
172
- return { settingsDirty: entries.panel || entries.dashboard, panelSettingsDirty: entries.panel, dashboardSettingsDirty: entries.dashboard, setSettingsDirty, onSettingsDirtyChange, onPanelSettingsDirtyChange };
172
+ return { settingsDirty: entries.dashboard, dashboardSettingsDirty: entries.dashboard, setSettingsDirty, onSettingsDirtyChange };
173
173
  }
@@ -1297,15 +1297,7 @@ button.instance-group-header[aria-expanded="false"] .instance-group-chevron {
1297
1297
  .drawer-body { min-height: 0; overflow: auto; }
1298
1298
 
1299
1299
  /* Settings replaces the workspace while retaining the preview host. */
1300
- .workbench[data-instance-settings-open="true"] { grid-template-rows: minmax(0, 1fr); }
1301
1300
  .workbench .workbench-header[hidden], #manager-sidebar-list[hidden] { display: none; }
1302
- .workbench[data-instance-settings-open="true"] .workbench-body { display: grid; min-height: 0; padding: 0; }
1303
- .workbench-settings-page { min-width: 0; min-height: 0; height: 100%; }
1304
- .workbench-settings-toggle { min-width: 32px; min-height: 32px; display: inline-flex; align-items: center; justify-content: center; }
1305
- .workbench-settings-toggle svg { width: 16px; height: 16px; stroke-width: 1.8; }
1306
- .workbench-settings-toggle[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); }
1307
- .workbench-settings-toggle:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
1308
- @media (pointer: coarse) { .workbench-settings-toggle { min-width: 44px; min-height: 44px; } }
1309
1301
 
1310
1302
  @media (max-width: 639px) {
1311
1303
  .command-center .command-title { display: none; }
@@ -76,7 +76,6 @@ export function runManagerShortcut(action: DashboardShortcutAction, deps: Manage
76
76
  if (action === 'switchTab1') { deps.handleTabChange('overview'); return; }
77
77
  if (action === 'switchTab2') { deps.handleTabChange('preview'); return; }
78
78
  if (action === 'switchTab3') { deps.handleTabChange('logs'); return; }
79
- if (action === 'switchTab4') { deps.handleTabChange('settings'); return; }
80
79
  if (action === 'previousTab' || action === 'nextTab') {
81
80
  const tabs: DashboardDetailTab[] = ['overview', 'preview', 'logs'];
82
81
  const idx = tabs.indexOf(deps.activeDetailTab);
@@ -17,7 +17,6 @@ export const MANAGER_SHORTCUT_ACTIONS: DashboardShortcutAction[] = [
17
17
  'switchTab1',
18
18
  'switchTab2',
19
19
  'switchTab3',
20
- 'switchTab4',
21
20
  'previousTab',
22
21
  'nextTab',
23
22
  'browserReload',
@@ -57,7 +56,6 @@ export const DEFAULT_MANAGER_SHORTCUT_KEYMAP: DashboardShortcutKeymap = {
57
56
  switchTab1: 'Meta+1',
58
57
  switchTab2: 'Meta+2',
59
58
  switchTab3: 'Meta+3',
60
- switchTab4: 'Meta+4',
61
59
  previousTab: 'Meta+Shift+[',
62
60
  nextTab: 'Meta+Shift+]',
63
61
  browserReload: 'Meta+R',
@@ -124,7 +124,7 @@ type RuntimeDraft = {
124
124
  model: string;
125
125
  effort: string;
126
126
  workingDir: string;
127
- permissions: 'auto' | string[];
127
+ permissions: 'auto' | 'safe' | string[];
128
128
  };
129
129
 
130
130
  export default function Agent({ port, client, dirty, registerSave }: SettingsPageProps) {
@@ -284,7 +284,10 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
284
284
  model: runtimeModelFor(cli, state.data.perCli, state.data.activeOverrides),
285
285
  effort: runtimeEffortFor(cli, state.data.perCli, state.data.activeOverrides),
286
286
  workingDir: state.data.workingDir || '',
287
- permissions: permissions.mode === 'custom' ? permissions.tokens : 'auto',
287
+ // Preserve 'safe' rather than collapsing it: an untouched Safe instance must not
288
+ // become Auto (YOLO) as a side effect of saving something unrelated on this page.
289
+ permissions: permissions.mode === 'custom' ? permissions.tokens
290
+ : permissions.mode === 'safe' ? 'safe' : 'auto',
288
291
  });
289
292
  }, [cliMeta, state]);
290
293
 
@@ -95,7 +95,26 @@ export type DashboardMetaProps = SettingsPageProps & {
95
95
  managerClient?: ManagerClient;
96
96
  };
97
97
 
98
- export default function DashboardMeta({
98
+ /**
99
+ * No selected instance. SettingsShell passes `port={port ?? 0}`, and this page writes to the
100
+ * MANAGER registry keyed by port, so rendering the form here would let a save PATCH
101
+ * /api/dashboard/registry under instance key "0". This is a separate component rather than an
102
+ * early return because the form below calls hooks before its first return: an early return
103
+ * above them breaks the rules of hooks, and one below them has already issued the fetch.
104
+ */
105
+ function DashboardMetaEmpty() {
106
+ return (
107
+ <p className="settings-empty" role="status">
108
+ Select an instance to edit its dashboard metadata.
109
+ </p>
110
+ );
111
+ }
112
+
113
+ export default function DashboardMeta(props: DashboardMetaProps) {
114
+ return props.port ? <DashboardMetaForm {...props} /> : <DashboardMetaEmpty />;
115
+ }
116
+
117
+ function DashboardMetaForm({
99
118
  port,
100
119
  client,
101
120
  dirty,
@@ -16,7 +16,7 @@ import {
16
16
  } from './page-shell';
17
17
  import { InlineWarn } from './components/InlineWarn';
18
18
 
19
- export type PermissionMode = 'auto' | 'custom';
19
+ export type PermissionMode = 'auto' | 'safe' | 'custom';
20
20
 
21
21
  type PermissionsSnapshot = {
22
22
  permissions?: 'auto' | string[] | unknown;
@@ -58,9 +58,13 @@ export function parsePermissionsValue(
58
58
  value: unknown,
59
59
  ):
60
60
  | { mode: 'auto' }
61
+ | { mode: 'safe' }
61
62
  | { mode: 'custom'; tokens: string[] }
62
63
  | { mode: 'unknown' } {
63
64
  if (isPermissionsAuto(value)) return { mode: 'auto' };
65
+ // 'safe' is a real stored policy — the Classic sidebar is its only writer today. Reporting
66
+ // it as 'unknown' is what let callers collapse it to 'auto', silently widening permissions.
67
+ if (value === 'safe') return { mode: 'safe' };
64
68
  if (Array.isArray(value)) {
65
69
  const tokens = value
66
70
  .filter((t): t is string => typeof t === 'string')
@@ -102,6 +106,7 @@ export function isAllowlistValid(tokens: ReadonlyArray<string>): boolean {
102
106
 
103
107
  const MODE_OPTIONS = [
104
108
  { value: 'auto', label: 'Auto (YOLO)' },
109
+ { value: 'safe', label: 'Safe' },
105
110
  { value: 'custom', label: 'Custom — explicit allowlist below' },
106
111
  ];
107
112
 
@@ -125,7 +130,10 @@ export default function Permissions({ port, client, dirty, registerSave }: Setti
125
130
  setMode('custom');
126
131
  setTokens(parsed.tokens);
127
132
  } else {
128
- setMode('auto');
133
+ // A Safe instance must read as Safe. Showing it as Auto (YOLO) hides the policy the
134
+ // user is about to change and reports 'auto' as the original, so a round trip
135
+ // through Custom and back writes a widened policy the user never chose.
136
+ setMode(parsed.mode === 'safe' ? 'safe' : 'auto');
129
137
  setTokens([]);
130
138
  }
131
139
  }, [state]);
@@ -141,9 +149,10 @@ export default function Permissions({ port, client, dirty, registerSave }: Setti
141
149
  [dirty],
142
150
  );
143
151
 
144
- const originalSerialized = useMemo<'auto' | string[]>(() => {
152
+ const originalSerialized = useMemo<'auto' | 'safe' | string[]>(() => {
145
153
  if (!original) return 'auto';
146
154
  if (original.mode === 'custom') return original.tokens;
155
+ if (original.mode === 'safe') return 'safe';
147
156
  return 'auto';
148
157
  }, [original]);
149
158
 
@@ -159,6 +168,16 @@ export default function Permissions({ port, client, dirty, registerSave }: Setti
159
168
  });
160
169
  return;
161
170
  }
171
+ if (next === 'safe') {
172
+ setMode('safe');
173
+ setTokens([]);
174
+ writeEntry({
175
+ value: 'safe',
176
+ original: originalSerialized,
177
+ valid: true,
178
+ });
179
+ return;
180
+ }
162
181
  if (next === 'custom') {
163
182
  setMode('custom');
164
183
  const seed =
@@ -204,7 +223,7 @@ export default function Permissions({ port, client, dirty, registerSave }: Setti
204
223
  setMode('custom');
205
224
  setTokens(parsed.tokens);
206
225
  } else {
207
- setMode('auto');
226
+ setMode(parsed.mode === 'safe' ? 'safe' : 'auto');
208
227
  setTokens([]);
209
228
  }
210
229
  setData(fresh);
@@ -10,17 +10,20 @@ import {
10
10
  type PermissionQuickSectionProps = {
11
11
  value: unknown;
12
12
  configuredValue: unknown;
13
- onChange(next: 'auto' | string[]): void;
13
+ onChange(next: 'auto' | 'safe' | string[]): void;
14
14
  };
15
15
 
16
16
  const MODE_OPTIONS = [
17
17
  { value: 'auto', label: 'Auto (YOLO)' },
18
+ { value: 'safe', label: 'Safe' },
18
19
  { value: 'custom', label: 'Custom allowlist' },
19
20
  ];
20
21
 
21
22
  export function PermissionQuickSection({ value, configuredValue, onChange }: PermissionQuickSectionProps) {
22
23
  const parsed = parsePermissionsValue(value);
23
- const mode = parsed.mode === 'custom' ? 'custom' : 'auto';
24
+ // Never fall through to 'auto': a Safe instance shown as Auto (YOLO) means the user cannot
25
+ // see the permission they are about to lose when they touch this control.
26
+ const mode = parsed.mode === 'custom' ? 'custom' : parsed.mode === 'safe' ? 'safe' : 'auto';
24
27
  const tokens = parsed.mode === 'custom' ? parsed.tokens : [];
25
28
  const summary = parsed.mode === 'custom'
26
29
  ? `Editor draft: ${tokens.length} explicit token${tokens.length === 1 ? '' : 's'}`
@@ -38,6 +41,7 @@ export function PermissionQuickSection({ value, configuredValue, onChange }: Per
38
41
  options={MODE_OPTIONS}
39
42
  onChange={(next) => {
40
43
  if (next === 'auto') onChange('auto');
44
+ else if (next === 'safe') onChange('safe');
41
45
  else onChange(tokens.length > 0 && isAllowlistValid(tokens) ? tokens : seedAutoAllowlist(null));
42
46
  }}
43
47
  />
@@ -3,7 +3,9 @@ import { expandPatch } from '../../path-utils';
3
3
  import {
4
4
  applyRuntimeEmployeesDiff,
5
5
  runtimeEmployeesHaveErrors,
6
+ unwrapRuntimeEmployees,
6
7
  type RuntimeEmployeeRecord,
8
+ type RuntimeEmployeesResponse,
7
9
  } from './runtime-employees-helpers';
8
10
 
9
11
  export type AgentSettingsSnapshot = {
@@ -62,7 +64,28 @@ export async function saveAgentRuntime(options: {
62
64
  }
63
65
  try {
64
66
  if (runtimeEmployeesNext) {
65
- await applyRuntimeEmployeesDiff(client, employeeOriginal, runtimeEmployeesNext);
67
+ // The Classic sidebar writes employees immediately and independently, so by save
68
+ // time the server can hold rows this page never saw. Two things follow, and only
69
+ // doing the first is worse than doing neither.
70
+ //
71
+ // Re-read so the diff is computed against reality rather than a stale snapshot.
72
+ // Then restrict deletion to rows this page actually knew about: a row that appeared
73
+ // on the server while the page was open is absent from the draft, which the plain
74
+ // diff reads as "the user removed it" and deletes. It was never the user's to
75
+ // remove. Additions and updates still come from the draft as the user expressed
76
+ // them.
77
+ //
78
+ // A failed re-read falls back to the snapshot rather than blocking the save: the
79
+ // stale diff is still the caller's explicit intent, and refusing to save would
80
+ // discard it.
81
+ let baseline = employeeOriginal;
82
+ try {
83
+ const server = unwrapRuntimeEmployees(await client.get<RuntimeEmployeesResponse>('/api/employees'));
84
+ const knownToThisPage = new Set(employeeOriginal.map(row => row.id));
85
+ const draftIds = new Set(runtimeEmployeesNext.map(row => row.id));
86
+ baseline = server.filter(row => knownToThisPage.has(row.id) || draftIds.has(row.id));
87
+ } catch { /* keep the snapshot baseline */ }
88
+ await applyRuntimeEmployeesDiff(client, baseline, runtimeEmployeesNext);
66
89
  }
67
90
  } catch (err: unknown) {
68
91
  throw new Error(`runtime employees save failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -10,6 +10,8 @@ const SETTINGS_ICONS: Readonly<Record<string, IconName>> = {
10
10
  'advanced-export': 'download', employees: 'people',
11
11
  'manager-display': 'monitor', 'manager-activity': 'heartPulse',
12
12
  'manager-developer': 'tool', 'manager-embedding': 'search',
13
- 'telegram-hub': 'radio', 'dashboard-meta': 'unknown',
13
+ // 'unknown' resolves through this table rather than the 'settings' fallback, so it rendered
14
+ // a literal question mark. The page edits an instance's label, group, favorite and notes.
15
+ 'telegram-hub': 'radio', 'dashboard-meta': 'pencil',
14
16
  };
15
17
  export function settingsIcon(id: string): IconName { return SETTINGS_ICONS[id] ?? 'settings'; }
@@ -3,7 +3,7 @@ import type { ComponentType } from 'react';
3
3
  import type { DashboardLocale } from '../types';
4
4
  import { DASHBOARD_SETTINGS_COPY } from '../dashboard-settings/DashboardSettingsSidebar';
5
5
  type Entry = { id: SettingsCategoryId; scope: SettingsScope; group: SettingsCategoryGroup;
6
- label: string | ((locale: DashboardLocale) => string); requiresInstance?: boolean; hidden?: boolean; load: () => Promise<{ default: ComponentType<SettingsPageProps> }> };
6
+ label: string | ((locale: DashboardLocale) => string); hidden?: boolean; load: () => Promise<{ default: ComponentType<SettingsPageProps> }> };
7
7
  export const SETTINGS_REGISTRY: readonly Entry[] = [
8
8
  { id:'agent', scope:'instance', group:'runtime', label:'Agent', load:()=>import('./pages/Agent') },
9
9
  { id:'model', scope:'instance', group:'runtime', label:'Model defaults', load:()=>import('./pages/ModelProvider') },
@@ -27,9 +27,9 @@ export const SETTINGS_REGISTRY: readonly Entry[] = [
27
27
  { id:'manager-developer', scope:'manager', group:'advanced', label:locale=>DASHBOARD_SETTINGS_COPY[locale].sections.developer.label, load:()=>import('./pages/manager/Developer') },
28
28
  { id:'manager-embedding', scope:'manager', group:'automation', label:locale=>DASHBOARD_SETTINGS_COPY[locale].sections.embedding.label, load:()=>import('./pages/manager/Embedding') },
29
29
  { id:'telegram-hub', scope:'manager', group:'channels', label:locale=>DASHBOARD_SETTINGS_COPY[locale].sections.telegramHub.label, load:()=>import('./pages/manager/TelegramHub') },
30
- { id:'dashboard-meta', scope:'manager', requiresInstance:true, group:'advanced', label:'Dashboard meta', load:()=>import('./pages/DashboardMeta') },
30
+ { id:'dashboard-meta', scope:'manager', group:'advanced', label:'Dashboard meta', load:()=>import('./pages/DashboardMeta') },
31
31
  ];
32
32
  export const entriesForScopes = (scopes: readonly SettingsScope[], hasInstance: boolean, locale: DashboardLocale) =>
33
33
  SETTINGS_REGISTRY.filter(entry => !entry.hidden && scopes.includes(entry.scope)
34
- && (hasInstance || (entry.scope !== 'instance' && !entry.requiresInstance)))
34
+ && (hasInstance || entry.scope !== 'instance'))
35
35
  .map(entry => ({...entry, label:typeof entry.label==='function' ? entry.label(locale) : entry.label}));
@@ -224,7 +224,6 @@
224
224
  .settings-shell .settings-field-error { grid-column:1 / -1; }
225
225
  .settings-shell .settings-select-menu { left:auto; right:0; width:min(100%,560px); }
226
226
  .settings-shell .settings-field :is(input:not([type=checkbox]),textarea), .settings-shell .settings-select-trigger { background:var(--surface-raised); box-shadow:none; }
227
- .workbench-settings-toggle:focus-visible,
228
227
  .settings-shell :focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
229
228
  @container (max-width:719px) {
230
229
  .settings-shell .settings-field, .settings-shell .dashboard-settings-row { grid-template-columns:minmax(0,1fr); row-gap:12px; }
@@ -13,7 +13,6 @@ import './settings-embedding.css';
13
13
 
14
14
  const proxyMatch = location.pathname.match(/^\/i\/(\d+)\//);
15
15
  const apiBase = proxyMatch ? `/i/${proxyMatch[1]}` : '';
16
- const bootstrapPort = (window as Window & { __JAW_PORT?: unknown }).__JAW_PORT;
17
16
  const host = document.getElementById('settings-root');
18
17
  if (!host) throw new Error('Invalid settings host');
19
18
 
@@ -53,13 +52,14 @@ const client = createDirectClient(0, { base: apiBase, getHeaders: async () => {
53
52
  return token ? { Authorization: `Bearer ${token}` } : {};
54
53
  } });
55
54
  let snapshot: { port?: unknown } | null = null;
56
- if (validPort(bootstrapPort) === null) {
57
- try {
58
- const result = await client.get<{ port?: unknown; data?: { port?: unknown } }>('/api/settings');
59
- snapshot = result.data ?? result;
60
- } catch { /* Location is the final fallback; pages report their own API failures. */ }
61
- }
62
- const port = validPort(bootstrapPort) ?? validPort(snapshot?.port) ?? validPort(location.port);
55
+ // There used to be a `window.__JAW_PORT` bootstrap fast path here. Nothing in the tree ever
56
+ // assigned it, so it always resolved to null and this fetch ran regardless; keeping it implied
57
+ // a handshake that did not exist.
58
+ try {
59
+ const result = await client.get<{ port?: unknown; data?: { port?: unknown } }>('/api/settings');
60
+ snapshot = result.data ?? result;
61
+ } catch { /* Location is the final fallback; pages report their own API failures. */ }
62
+ const port = validPort(snapshot?.port) ?? validPort(location.port);
63
63
  if (port === null) {
64
64
  host.setAttribute('role', 'alert');
65
65
  host.textContent = 'Cannot resolve instance settings port';
@@ -30,7 +30,6 @@ export type DashboardShortcutAction =
30
30
  | 'switchTab1'
31
31
  | 'switchTab2'
32
32
  | 'switchTab3'
33
- | 'switchTab4'
34
33
  | 'previousTab'
35
34
  | 'nextTab'
36
35
  | 'browserReload'