ompchamber 3.1.0 → 3.2.0

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 (124) hide show
  1. package/README.md +6 -3
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +279 -280
  4. package/dist/client/static/js/index-01dv383b.css +1 -0
  5. package/dist/client/static/js/{index-8t70jhyj.js → index-1q6165r3.js} +641 -641
  6. package/package.json +1 -1
  7. package/src/cli/lib/args.js +4 -3
  8. package/src/cli/lib/commands/restart.js +7 -6
  9. package/src/cli/lib/commands/stop.js +38 -31
  10. package/src/cli/lib/commands/update.js +3 -2
  11. package/src/cli/lib/output.js +15 -8
  12. package/src/cli/lib/stop-scope.js +23 -0
  13. package/src/cli/lib/stop-scope.test.js +41 -0
  14. package/src/client/components/common/Modal.tsx +14 -4
  15. package/src/client/components/{layout/session-sidebar/SubagentList.tsx → common/subagent-list/index.tsx} +8 -10
  16. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +6 -30
  17. package/src/client/components/layout/session-sidebar/Toolbar.tsx +11 -5
  18. package/src/client/components/layout/session-sidebar/index.tsx +4 -2
  19. package/src/client/components/mobile/LayoutWrapper.tsx +6 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +4 -0
  21. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +66 -29
  22. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +50 -12
  23. package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +21 -1
  24. package/src/client/components/settings/Modal.tsx +35 -6
  25. package/src/client/components/settings/SettingsSidebarList.tsx +1 -1
  26. package/src/client/components/settings/categories/AgentSettings.tsx +45 -28
  27. package/src/client/components/settings/categories/CommandSettings.tsx +45 -28
  28. package/src/client/components/settings/categories/McpSettings.tsx +48 -31
  29. package/src/client/components/settings/categories/ProjectSettings.tsx +34 -19
  30. package/src/client/components/settings/categories/ProviderSettings.tsx +77 -53
  31. package/src/client/components/settings/categories/SkillSettings.tsx +34 -17
  32. package/src/client/components/settings/categories/UsageSettings.tsx +53 -39
  33. package/src/client/components/settings/categories/behavior-settings/Editor.tsx +7 -7
  34. package/src/client/components/settings/categories/mcp-settings/ImportModal.tsx +1 -1
  35. package/src/client/components/settings/categories/mcp-settings/SidebarList.tsx +1 -1
  36. package/src/client/components/settings/categories/project-settings/SidebarList.tsx +1 -1
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +8 -3
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +1 -1
  39. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +9 -4
  40. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +1 -1
  41. package/src/client/components/settings/categories/skill-catalog/AddSourceModal.tsx +1 -1
  42. package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +1 -1
  43. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +1 -1
  44. package/src/client/components/settings/master-detail/index.tsx +68 -0
  45. package/src/client/components/workspace/SearchPanel.tsx +2 -1
  46. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +7 -1
  47. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +2 -1
  48. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +2 -1
  49. package/src/client/components/workspace/chat-timeline/index.tsx +1 -0
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +2 -1
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +2 -1
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +2 -1
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +2 -1
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +2 -1
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +2 -1
  56. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +2 -1
  57. package/src/client/components/workspace/diff-panel/SplitView.tsx +31 -5
  58. package/src/client/components/workspace/diff-panel/Toolbar.tsx +77 -6
  59. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +18 -5
  60. package/src/client/components/workspace/diff-panel/index.tsx +36 -7
  61. package/src/client/components/workspace/editor/Toolbar.tsx +11 -4
  62. package/src/client/components/workspace/git-panel/commit-modal/CommitDiffViewer.tsx +2 -1
  63. package/src/client/hooks/chat/omp/session-list.tsx +31 -1
  64. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +7 -1
  65. package/src/client/hooks/chat/subagent/index.ts +37 -11
  66. package/src/client/hooks/editor/use-file-editor.ts +1 -1
  67. package/src/client/hooks/settings/master-detail.ts +33 -0
  68. package/src/client/hooks/workspace/expanded-sessions.ts +62 -0
  69. package/src/server/lib/bundler/css.test.ts +87 -0
  70. package/src/server/lib/bundler/css.ts +17 -2
  71. package/src/server/lib/bundler/nested-at-rules.test.ts +100 -0
  72. package/src/server/lib/bundler/nested-at-rules.ts +146 -0
  73. package/src/server/lib/db/client.ts +5 -1
  74. package/src/server/lib/fs/git-diff.ts +34 -10
  75. package/src/server/lib/fs/git-repos.ts +81 -0
  76. package/src/server/lib/lifecycle/flock.ts +199 -0
  77. package/src/server/lib/lifecycle/identity.ts +5 -1
  78. package/src/server/lib/lifecycle/launch-mode.test.ts +20 -1
  79. package/src/server/lib/lifecycle/launch-mode.ts +26 -0
  80. package/src/server/lib/lifecycle/port-guard.ts +18 -3
  81. package/src/server/lib/lifecycle/proc/darwin.ts +52 -17
  82. package/src/server/lib/lifecycle/proc/linux.ts +29 -7
  83. package/src/server/lib/lifecycle/proc/ps.ts +7 -0
  84. package/src/server/lib/lifecycle/proc/types.ts +17 -0
  85. package/src/server/lib/lifecycle/proc/win32.ts +5 -0
  86. package/src/server/lib/lifecycle/restart.ts +9 -14
  87. package/src/server/lib/observer-ws.ts +150 -0
  88. package/src/server/lib/omp/rpc/constants.ts +9 -0
  89. package/src/server/lib/omp/rpc/subagent-liveness.ts +2 -12
  90. package/src/server/lib/omp/session/file-scan.ts +7 -1
  91. package/src/server/lib/omp/session/files.ts +95 -96
  92. package/src/server/lib/omp/session/locator.ts +18 -8
  93. package/src/server/lib/omp/session/messages.ts +7 -7
  94. package/src/server/lib/omp/session/scan.ts +108 -0
  95. package/src/server/lib/omp/session/stats.ts +50 -50
  96. package/src/server/lib/omp/session/telemetry/scan.ts +89 -7
  97. package/src/server/lib/omp/session/telemetry-raw.ts +24 -8
  98. package/src/server/lib/omp/session/usage/aggregate.ts +170 -55
  99. package/src/server/lib/terminal/runtime.server.ts +3 -0
  100. package/src/server/lib/terminal/session.server.ts +30 -3
  101. package/src/server/routes/agent/command.test.ts +69 -0
  102. package/src/server/routes/agent/command.ts +14 -0
  103. package/src/server/routes/agent/ws.test.ts +159 -0
  104. package/src/server/routes/agent/ws.ts +22 -97
  105. package/src/server/routes/btw/ws.ts +22 -95
  106. package/src/server/routes/fs/git.ts +61 -70
  107. package/src/server/routes/omp/session-stats.ts +1 -1
  108. package/src/server/routes/telemetry/tokens.ts +6 -4
  109. package/src/server/routes/terminal/run.ts +4 -8
  110. package/src/server/routes/terminal/ws.ts +24 -2
  111. package/src/shared/lib/chat/omp/agent-events.test.ts +33 -1
  112. package/src/shared/lib/chat/omp/agent-events.ts +10 -0
  113. package/src/shared/lib/code/language.ts +146 -0
  114. package/src/shared/lib/code/syntax-highlight.test.ts +2 -1
  115. package/src/shared/lib/code/syntax-highlight.ts +0 -122
  116. package/src/shared/lib/fs/diff-parser.test.ts +5 -5
  117. package/src/shared/lib/fs/diff-parser.ts +67 -42
  118. package/src/shared/lib/omp/subagent/parse.test.ts +88 -0
  119. package/src/shared/lib/omp/subagent/parse.ts +17 -0
  120. package/src/shared/lib/omp/subagent/transcript-client.test.ts +100 -0
  121. package/src/shared/lib/omp/subagent/transcript-client.ts +17 -1
  122. package/src/shared/types/omp/subagent.ts +6 -0
  123. package/dist/client/static/js/index-n4wkqj7t.css +0 -1
  124. package/src/server/lib/lifecycle/restart.test.ts +0 -21
@@ -8,6 +8,8 @@ import { McpImportModal } from '@/client/components/settings/categories/mcp-sett
8
8
  import { LoadingState } from '@/client/components/settings/LoadingState';
9
9
  import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
10
10
  import { useCrudList } from '@/client/hooks/settings/crud-list';
11
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
12
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
11
13
 
12
14
  interface McpSettingsProps {
13
15
  settings: SettingsState;
@@ -17,6 +19,7 @@ interface McpSettingsProps {
17
19
  const GLOBAL_PROJECT: McpProjectOption = { id: 'global', name: 'Global (all projects)', path: '' };
18
20
 
19
21
  export const McpSettings: FunctionComponent<McpSettingsProps> = () => {
22
+ const masterDetail = useSettingsMasterDetail();
20
23
  const { folders } = useSidebarData();
21
24
  const projects = useMemo<McpProjectOption[]>(() => {
22
25
  const bound = folders.flatMap<McpProjectOption>((folder) =>
@@ -98,39 +101,53 @@ export const McpSettings: FunctionComponent<McpSettingsProps> = () => {
98
101
 
99
102
  return (
100
103
  <div className="flex h-full w-full overflow-hidden bg-paper">
101
- <McpSidebarList
102
- servers={servers}
103
- projects={projects}
104
- selectedServerId={isCreatingNew ? null : selectedId}
105
- onSelectServer={select}
106
- onAddNewServer={startCreate}
107
- selectedProject={selectedProject}
108
- onChangeProject={setSelectedProject}
109
- />
110
-
111
- <div className="flex-1 h-full overflow-hidden flex flex-col">
112
- {isCreatingNew ? (
113
- <McpDetailPane
114
- server={emptyServerTemplate}
115
- isNew={true}
116
- onSave={save}
117
- onOpenImportModal={() => setIsImportModalOpen(true)}
118
- />
119
- ) : selectedServer ? (
120
- <McpDetailPane
121
- key={selectedServer.id}
122
- server={selectedServer}
123
- isNew={false}
124
- onSave={save}
125
- onDelete={remove}
126
- onOpenImportModal={() => setIsImportModalOpen(true)}
104
+ <SettingsMasterDetail
105
+ pane={masterDetail.pane}
106
+ onBack={masterDetail.back}
107
+ listLabel="MCP Servers"
108
+ list={
109
+ <McpSidebarList
110
+ servers={servers}
111
+ projects={projects}
112
+ selectedServerId={isCreatingNew ? null : selectedId}
113
+ onSelectServer={(id) => {
114
+ select(id);
115
+ masterDetail.openDetail();
116
+ }}
117
+ onAddNewServer={() => {
118
+ startCreate();
119
+ masterDetail.openDetail();
120
+ }}
121
+ selectedProject={selectedProject}
122
+ onChangeProject={setSelectedProject}
127
123
  />
128
- ) : (
129
- <div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
130
- Select an MCP server or click + to configure one
124
+ }
125
+ detail={
126
+ <div className="flex-1 h-full overflow-hidden flex flex-col">
127
+ {isCreatingNew ? (
128
+ <McpDetailPane
129
+ server={emptyServerTemplate}
130
+ isNew={true}
131
+ onSave={save}
132
+ onOpenImportModal={() => setIsImportModalOpen(true)}
133
+ />
134
+ ) : selectedServer ? (
135
+ <McpDetailPane
136
+ key={selectedServer.id}
137
+ server={selectedServer}
138
+ isNew={false}
139
+ onSave={save}
140
+ onDelete={remove}
141
+ onOpenImportModal={() => setIsImportModalOpen(true)}
142
+ />
143
+ ) : (
144
+ <div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
145
+ Select an MCP server or click + to configure one
146
+ </div>
147
+ )}
131
148
  </div>
132
- )}
133
- </div>
149
+ }
150
+ />
134
151
 
135
152
  <McpImportModal
136
153
  isOpen={isImportModalOpen}
@@ -4,8 +4,11 @@ import { ProjectSidebarList } from '@/client/components/settings/categories/proj
4
4
  import { ProjectDetailsForm } from '@/client/components/settings/categories/project-settings/DetailsForm';
5
5
  import { LoadingState } from '@/client/components/settings/LoadingState';
6
6
  import { useChamberEvent } from '@/client/hooks/ui/window-event';
7
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
8
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
7
9
 
8
10
  export function ProjectSettings() {
11
+ const masterDetail = useSettingsMasterDetail();
9
12
  const [projects, setProjects] = useState<ProjectConfigItem[]>([]);
10
13
  const [availableModels, setAvailableModels] = useState<string[]>([]);
11
14
  const [accentColorOptions, setAccentColorOptions] = useState<AccentColorOption[]>([]);
@@ -94,6 +97,9 @@ export function ProjectSettings() {
94
97
  };
95
98
  setProjects((current) => [...current, newProject]);
96
99
  setSelectedProjectId(newProject.id);
100
+ // On a phone the new project's details are the point of adding it; the
101
+ // list pane would hide the row that was just created.
102
+ masterDetail.openDetail();
97
103
  window.dispatchEvent(new CustomEvent('omp:workspace-updated', { detail: { folderId } }));
98
104
  } catch (error) {
99
105
  console.error('Failed to add project:', error);
@@ -151,26 +157,35 @@ export function ProjectSettings() {
151
157
  }
152
158
 
153
159
  return (
154
- <div className="w-full h-full flex flex-col md:flex-row overflow-hidden bg-paper text-ink">
155
- {/* Left Column: Project list with counter & Add button */}
156
- <ProjectSidebarList
157
- projects={projects}
158
- selectedProjectId={selectedProject?.id || ''}
159
- onSelectProject={setSelectedProjectId}
160
- onAddProject={handleAddProject}
160
+ <div className="w-full h-full flex flex-col overflow-hidden bg-paper text-ink">
161
+ <SettingsMasterDetail
162
+ pane={masterDetail.pane}
163
+ onBack={masterDetail.back}
164
+ listLabel="Projects"
165
+ list={
166
+ <ProjectSidebarList
167
+ projects={projects}
168
+ selectedProjectId={selectedProject?.id || ''}
169
+ onSelectProject={(id) => {
170
+ setSelectedProjectId(id);
171
+ masterDetail.openDetail();
172
+ }}
173
+ onAddProject={handleAddProject}
174
+ />
175
+ }
176
+ detail={
177
+ selectedProject ? (
178
+ <ProjectDetailsForm
179
+ project={selectedProject}
180
+ canDelete={projects.length > 1}
181
+ onUpdateField={handleUpdateField}
182
+ onDeleteProject={handleDeleteProject}
183
+ availableModels={availableModels}
184
+ accentColorOptions={accentColorOptions}
185
+ />
186
+ ) : null
187
+ }
161
188
  />
162
-
163
- {/* Right Column: Project details & controls */}
164
- {selectedProject && (
165
- <ProjectDetailsForm
166
- project={selectedProject}
167
- canDelete={projects.length > 1}
168
- onUpdateField={handleUpdateField}
169
- onDeleteProject={handleDeleteProject}
170
- availableModels={availableModels}
171
- accentColorOptions={accentColorOptions}
172
- />
173
- )}
174
189
  </div>
175
190
  );
176
191
  }
@@ -13,6 +13,8 @@ import { AddModelModal } from '@/client/components/settings/categories/provider-
13
13
  import { DeleteProviderModal } from '@/client/components/settings/categories/provider-settings/DeleteProviderModal';
14
14
  import { providerEmptyReason } from '@/shared/lib/models/provider/dialect';
15
15
  import { configuredProviderSlugs } from '@/shared/lib/models/provider/presets';
16
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
17
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
16
18
 
17
19
  interface ProviderSettingsProps {
18
20
  autoOpenAdd?: boolean;
@@ -23,6 +25,7 @@ export function ProviderSettings({
23
25
  autoOpenAdd = false,
24
26
  onAddModalClose,
25
27
  }: ProviderSettingsProps) {
28
+ const masterDetail = useSettingsMasterDetail();
26
29
  const {
27
30
  providers,
28
31
  connectedProviders,
@@ -66,61 +69,76 @@ export function ProviderSettings({
66
69
  } = useProviderSettings({ autoOpenAdd, onAddModalClose });
67
70
 
68
71
  return (
69
- <div className="w-full h-full flex flex-col md:flex-row overflow-hidden bg-paper text-ink">
70
- <ProviderSidebarList
71
- providers={connectedProviders}
72
- selectedProviderId={selectedProvider?.id || ''}
73
- onSelectProvider={setSelectedProviderId}
74
- onOpenAddModal={() => setIsAddModalOpen(true)}
75
- currentProject={currentProject}
76
- onSelectProject={setCurrentProject}
77
- />
78
-
79
- <div className="flex-1 scrollbar-overlay-container scrollbar-overlay-static p-6 md:p-8 space-y-6">
80
- {selectedProvider ? (
81
- <>
82
- <ProviderHeader provider={selectedProvider} />
72
+ <div className="w-full h-full flex flex-col overflow-hidden bg-paper text-ink">
73
+ <SettingsMasterDetail
74
+ // The "add provider" dialog lives inside the detail pane, so a hidden
75
+ // pane would hide the dialog with it — and `autoOpenAdd` (the model
76
+ // dropdown's "no provider configured" path) opens it without any row
77
+ // having been tapped. While that dialog is open the pane stays up.
78
+ pane={isAddModalOpen ? 'detail' : masterDetail.pane}
79
+ onBack={masterDetail.back}
80
+ listLabel="Providers"
81
+ list={
82
+ <ProviderSidebarList
83
+ providers={connectedProviders}
84
+ selectedProviderId={selectedProvider?.id || ''}
85
+ onSelectProvider={(id) => {
86
+ setSelectedProviderId(id);
87
+ masterDetail.openDetail();
88
+ }}
89
+ onOpenAddModal={() => setIsAddModalOpen(true)}
90
+ currentProject={currentProject}
91
+ onSelectProject={setCurrentProject}
92
+ />
93
+ }
94
+ detail={
95
+ <div className="flex-1 scrollbar-overlay-container scrollbar-overlay-static p-6 md:p-8 space-y-6">
96
+ {selectedProvider ? (
97
+ <>
98
+ <ProviderHeader provider={selectedProvider} />
83
99
 
84
- <ProviderAuthSection
85
- provider={selectedProvider}
86
- onOpenReconnectModal={() => {
87
- // A provider omp signs in through `/login` needs the OAuth
88
- // dialog; one whose endpoint and key live in models.yml (or the
89
- // chamber overlay) is edited in place. Testing the provider ID
90
- // for `omp-auth-` was wrong: a native models.yml entry shares
91
- // that ID prefix once merged, and a keyless local server has no
92
- // login flow to complete at all.
93
- if (selectedProvider.credentialSource === 'omp-auth' && selectedProvider.auth !== 'none') {
94
- setIsLoginModalOpen(true);
95
- } else {
96
- setIsReconnectModalOpen(true);
97
- }
98
- }}
99
- onToggleDisconnect={handleToggleDisconnect}
100
- onToggleDisabled={handleToggleProviderDisabled}
101
- onDelete={() => setIsDeleteModalOpen(true)}
102
- />
100
+ <ProviderAuthSection
101
+ provider={selectedProvider}
102
+ onOpenReconnectModal={() => {
103
+ // A provider omp signs in through `/login` needs the OAuth
104
+ // dialog; one whose endpoint and key live in models.yml (or the
105
+ // chamber overlay) is edited in place. Testing the provider ID
106
+ // for `omp-auth-` was wrong: a native models.yml entry shares
107
+ // that ID prefix once merged, and a keyless local server has no
108
+ // login flow to complete at all.
109
+ if (selectedProvider.credentialSource === 'omp-auth' && selectedProvider.auth !== 'none') {
110
+ setIsLoginModalOpen(true);
111
+ } else {
112
+ setIsReconnectModalOpen(true);
113
+ }
114
+ }}
115
+ onToggleDisconnect={handleToggleDisconnect}
116
+ onToggleDisabled={handleToggleProviderDisabled}
117
+ onDelete={() => setIsDeleteModalOpen(true)}
118
+ />
103
119
 
104
- <ProviderModelsList
105
- models={selectedProvider.models}
106
- emptyReason={providerEmptyReason(selectedProvider)}
107
- onToggleModelVisibility={handleToggleModelVisibility}
108
- onHideAll={handleHideAll}
109
- onShowAll={handleShowAll}
110
- onFetchModels={handleFetchModelsFromList}
111
- onAddModel={() => setIsAddModelModalOpen(true)}
112
- canFetchModels={Boolean(selectedProvider.baseUrl)}
113
- isFetchingModels={isFetchingModels}
114
- onOpenModelConfig={(model) => setConfigModel(model)}
115
- onOpenModelCapabilities={(model) => setCapabilitiesModel(model)}
116
- />
117
- </>
118
- ) : (
119
- <div className="py-12 text-center text-xs text-ink/40">
120
- Select a provider from the sidebar to view connection details
120
+ <ProviderModelsList
121
+ models={selectedProvider.models}
122
+ emptyReason={providerEmptyReason(selectedProvider)}
123
+ onToggleModelVisibility={handleToggleModelVisibility}
124
+ onHideAll={handleHideAll}
125
+ onShowAll={handleShowAll}
126
+ onFetchModels={handleFetchModelsFromList}
127
+ onAddModel={() => setIsAddModelModalOpen(true)}
128
+ canFetchModels={Boolean(selectedProvider.baseUrl)}
129
+ isFetchingModels={isFetchingModels}
130
+ onOpenModelConfig={(model) => setConfigModel(model)}
131
+ onOpenModelCapabilities={(model) => setCapabilitiesModel(model)}
132
+ />
133
+ </>
134
+ ) : (
135
+ <div className="py-12 text-center text-xs text-ink/40">
136
+ Select a provider from the sidebar to view connection details
137
+ </div>
138
+ )}
121
139
  </div>
122
- )}
123
- </div>
140
+ }
141
+ />
124
142
 
125
143
  <ToastStack toasts={toasts} onDismiss={dismissToast} />
126
144
 
@@ -130,7 +148,13 @@ export function ProviderSettings({
130
148
  setIsAddModalOpen(false);
131
149
  onAddModalClose?.();
132
150
  }}
133
- onAddProvider={handleAddProvider}
151
+ onAddProvider={async (provider, options) => {
152
+ await handleAddProvider(provider, options);
153
+ // The row now exists in the list; on a phone the newly registered
154
+ // provider is what the user came to configure, so land on its detail
155
+ // rather than dropping them back on the list.
156
+ masterDetail.openDetail();
157
+ }}
134
158
  presets={availablePresetProviders}
135
159
  existingSlugs={configuredProviderSlugs(providers)}
136
160
  />
@@ -4,6 +4,8 @@ import { SkillSidebarList } from '@/client/components/settings/categories/skill-
4
4
  import { SkillDetailPane } from '@/client/components/settings/categories/skill-settings/DetailPane';
5
5
  import { LoadingState } from '@/client/components/settings/LoadingState';
6
6
  import { useCrudList } from '@/client/hooks/settings/crud-list';
7
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
8
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
7
9
 
8
10
  interface SkillSettingsProps {
9
11
  settings?: SettingsState;
@@ -12,6 +14,7 @@ interface SkillSettingsProps {
12
14
  }
13
15
 
14
16
  export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
17
+ const masterDetail = useSettingsMasterDetail();
15
18
  const [selectedProjectId, setSelectedProjectId] = useState('ompchamber');
16
19
  const { items: skills, selectedId, selected: selectedSkill, isCreatingNew, isLoading, select, startCreate, save, remove } =
17
20
  useCrudList<SkillItem, Partial<SkillItem>>({
@@ -46,23 +49,37 @@ export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
46
49
  }
47
50
 
48
51
  return (
49
- <div className="flex-1 flex flex-col md:flex-row h-full w-full overflow-hidden bg-paper">
50
- <SkillSidebarList
51
- skills={skills}
52
- selectedSkillId={selectedId}
53
- isCreatingNew={isCreatingNew}
54
- onSelectSkill={select}
55
- onAddNewSkill={startCreate}
56
- selectedProject={selectedProjectId}
57
- onSelectProject={setSelectedProjectId}
58
- />
59
-
60
- <SkillDetailPane
61
- skill={selectedSkill ?? null}
62
- isCreatingNew={isCreatingNew}
63
- onSave={save}
64
- onDelete={remove}
65
- onOpenCatalog={onNavigateToCatalog}
52
+ <div className="flex-1 flex flex-col h-full w-full overflow-hidden bg-paper">
53
+ <SettingsMasterDetail
54
+ pane={masterDetail.pane}
55
+ onBack={masterDetail.back}
56
+ listLabel="Skills"
57
+ list={
58
+ <SkillSidebarList
59
+ skills={skills}
60
+ selectedSkillId={selectedId}
61
+ isCreatingNew={isCreatingNew}
62
+ onSelectSkill={(id) => {
63
+ select(id);
64
+ masterDetail.openDetail();
65
+ }}
66
+ onAddNewSkill={() => {
67
+ startCreate();
68
+ masterDetail.openDetail();
69
+ }}
70
+ selectedProject={selectedProjectId}
71
+ onSelectProject={setSelectedProjectId}
72
+ />
73
+ }
74
+ detail={
75
+ <SkillDetailPane
76
+ skill={selectedSkill ?? null}
77
+ isCreatingNew={isCreatingNew}
78
+ onSave={save}
79
+ onDelete={remove}
80
+ onOpenCatalog={onNavigateToCatalog}
81
+ />
82
+ }
66
83
  />
67
84
  </div>
68
85
  );
@@ -5,9 +5,12 @@ import { buildProviders, type UsageProviderId } from '@/client/components/settin
5
5
  import { ProviderDetail } from '@/client/components/settings/categories/usage-settings/ProviderDetail';
6
6
  import { formatDateTime } from '@/client/components/settings/categories/usage-settings/format';
7
7
  import { useUsageReport } from '@/client/hooks/settings/useUsageReport';
8
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
9
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
8
10
 
9
11
  /** Settings → Usage: provider quota, balances, and locally recorded burn. */
10
12
  export function UsageSettings() {
13
+ const masterDetail = useSettingsMasterDetail();
11
14
  const { report, isLoading, error, reload } = useUsageReport();
12
15
  const [selectedProviderId, setSelectedProviderId] = useState<UsageProviderId>('');
13
16
 
@@ -46,47 +49,58 @@ export function UsageSettings() {
46
49
 
47
50
  return (
48
51
  <div className="flex h-full w-full overflow-hidden bg-paper text-xs text-ink">
49
- <UsageSidebarList
50
- report={report}
51
- selectedProviderId={selected?.id ?? ''}
52
- onSelectProvider={setSelectedProviderId}
53
- />
54
-
55
- <div className="flex-1 h-full overflow-hidden flex flex-col">
56
- <div className="px-3.5 py-2.5 border-b border-ink/10 flex items-center justify-between gap-3 flex-shrink-0">
57
- <div className="flex items-center gap-2 min-w-0">
58
- <span className="text-xs font-semibold text-ink/75 truncate">
59
- Updated {formatDateTime(report.generatedAt)}
60
- </span>
61
- {report.isMock && (
62
- <span className="text-[10px] font-mono uppercase tracking-wider bg-ink/5 border border-ink/15 rounded px-1.5 py-0.5 text-ink/60">
63
- Mock
64
- </span>
65
- )}
66
- </div>
67
- <button
68
- type="button"
69
- onClick={() => reload({ force: true })}
70
- disabled={isLoading}
71
- className="p-1.5 bg-paper border border-ink/15 rounded-md hover:bg-ink/5 text-ink/80 transition-colors disabled:opacity-50 flex-shrink-0"
72
- title="Refresh usage"
73
- >
74
- <RefreshCw size={12} className={isLoading ? 'animate-spin text-ink' : ''} />
75
- </button>
76
- </div>
52
+ <SettingsMasterDetail
53
+ pane={masterDetail.pane}
54
+ onBack={masterDetail.back}
55
+ listLabel="Usage"
56
+ list={
57
+ <UsageSidebarList
58
+ report={report}
59
+ selectedProviderId={selected?.id ?? ''}
60
+ onSelectProvider={(id) => {
61
+ setSelectedProviderId(id);
62
+ masterDetail.openDetail();
63
+ }}
64
+ />
65
+ }
66
+ detail={
67
+ <div className="flex-1 h-full overflow-hidden flex flex-col">
68
+ <div className="px-3.5 py-2.5 border-b border-ink/10 flex items-center justify-between gap-3 flex-shrink-0">
69
+ <div className="flex items-center gap-2 min-w-0">
70
+ <span className="text-xs font-semibold text-ink/75 truncate">
71
+ Updated {formatDateTime(report.generatedAt)}
72
+ </span>
73
+ {report.isMock && (
74
+ <span className="text-[10px] font-mono uppercase tracking-wider bg-ink/5 border border-ink/15 rounded px-1.5 py-0.5 text-ink/60">
75
+ Mock
76
+ </span>
77
+ )}
78
+ </div>
79
+ <button
80
+ type="button"
81
+ onClick={() => reload({ force: true })}
82
+ disabled={isLoading}
83
+ className="p-1.5 bg-paper border border-ink/15 rounded-md hover:bg-ink/5 text-ink/80 transition-colors disabled:opacity-50 flex-shrink-0"
84
+ title="Refresh usage"
85
+ >
86
+ <RefreshCw size={12} className={isLoading ? 'animate-spin text-ink' : ''} />
87
+ </button>
88
+ </div>
77
89
 
78
- <div className="flex-1 overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static p-3.5 space-y-4">
79
- {error && <p className="text-error text-xs">{error}</p>}
90
+ <div className="flex-1 overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static p-3.5 space-y-4">
91
+ {error && <p className="text-error text-xs">{error}</p>}
80
92
 
81
- {selected ? (
82
- <ProviderDetail summary={selected.summary} />
83
- ) : (
84
- <p className="text-[11px] text-ink/50 leading-relaxed">
85
- No provider credentials detected. Add an API key in Settings → Providers and it will appear here.
86
- </p>
87
- )}
88
- </div>
89
- </div>
93
+ {selected ? (
94
+ <ProviderDetail summary={selected.summary} />
95
+ ) : (
96
+ <p className="text-[11px] text-ink/50 leading-relaxed">
97
+ No provider credentials detected. Add an API key in Settings → Providers and it will appear here.
98
+ </p>
99
+ )}
100
+ </div>
101
+ </div>
102
+ }
103
+ />
90
104
  </div>
91
105
  );
92
106
  }
@@ -105,24 +105,24 @@ export const BehaviorEditor: FunctionComponent<BehaviorEditorProps> = ({
105
105
  return (
106
106
  <div className="flex-1 flex flex-col h-full bg-paper text-ink p-6 md:p-8 overflow-hidden">
107
107
  {/* Header Bar */}
108
- <div className="flex items-center justify-between pb-4 border-b border-ink/10">
109
- <div className="flex items-center gap-2">
108
+ <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 pb-4 border-b border-ink/10">
109
+ <div className="flex items-center gap-2 min-w-0">
110
110
  <h2 className="text-sm font-bold tracking-tight text-ink flex items-center gap-2">
111
- <span>{copy.title}</span>
112
- <span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded border border-ink/15 text-ink/50 font-medium">
111
+ <span className="truncate">{copy.title}</span>
112
+ <span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded border border-ink/15 text-ink/50 font-medium flex-shrink-0">
113
113
  {copy.badge}
114
114
  </span>
115
115
  </h2>
116
- <div className="group relative">
116
+ <div className="group relative flex-shrink-0">
117
117
  <Info className="w-4 h-4 text-ink/40 cursor-help" />
118
- <div className="absolute left-0 top-6 hidden group-hover:block w-80 p-2.5 rounded-lg bg-ink text-paper text-xs shadow-lg z-20 leading-relaxed pointer-events-none">
118
+ <div className="absolute left-0 top-6 hidden group-hover:block w-80 max-w-[80vw] p-2.5 rounded-lg bg-ink text-paper text-xs shadow-lg z-20 leading-relaxed pointer-events-none">
119
119
  {copy.tooltip}
120
120
  </div>
121
121
  </div>
122
122
  </div>
123
123
 
124
124
  {/* Action Buttons Top */}
125
- <div className="flex items-center gap-2">
125
+ <div className="flex items-center gap-2 flex-shrink-0">
126
126
  <button
127
127
  type="button"
128
128
  onClick={handleCopy}
@@ -100,7 +100,7 @@ export const McpImportModal: FunctionComponent<McpImportModalProps> = ({
100
100
 
101
101
  return (
102
102
  <div className="fixed inset-0 bg-ink/40 backdrop-blur-[2px] z-50 flex items-center justify-center p-4 animate-in fade-in duration-150">
103
- <div className="w-full max-w-lg rounded-lg border border-ink/20 bg-paper text-ink p-5 shadow-2xl space-y-4">
103
+ <div className="w-full max-w-lg rounded-lg border border-ink/20 bg-paper text-ink p-5 shadow-2xl space-y-4 max-h-full overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static">
104
104
  <div className="flex items-center justify-between pb-3 border-b border-ink/10">
105
105
  <div className="flex items-center gap-2">
106
106
  <Code className="w-4 h-4 text-ink/70" />
@@ -31,7 +31,7 @@ export const McpSidebarList: FunctionComponent<McpSidebarListProps> = ({
31
31
  const activeProjectName = projects.find((proj) => proj.path === selectedProject)?.name ?? 'Global (all projects)';
32
32
 
33
33
  return (
34
- <div className="w-56 sm:w-64 border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0 select-none">
34
+ <div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0 select-none">
35
35
  <ProjectSelectorDropdown
36
36
  selectedProject={selectedProject}
37
37
  onChangeProject={onChangeProject}
@@ -39,7 +39,7 @@ export function ProjectSidebarList({
39
39
  onAddProject,
40
40
  }: ProjectSidebarListProps) {
41
41
  return (
42
- <div className="w-56 sm:w-64 border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0">
42
+ <div className="w-full md:w-64 md:border-r border-ink/10 h-full flex flex-col bg-paper/50 flex-shrink-0">
43
43
  {/* List Header */}
44
44
  <div className="px-3.5 py-3 border-b border-ink/10 flex items-center justify-between">
45
45
  <span className="text-xs font-semibold text-ink">
@@ -31,8 +31,13 @@ export function ProviderAuthSection({
31
31
  Authentication
32
32
  </div>
33
33
 
34
- <div className="flex items-center justify-between gap-4">
35
- <div className="flex items-center gap-2">
34
+ {/* Wraps instead of overflowing: this row is a status group plus three
35
+ action buttons, which needs ~330px, while the detail pane is 206px
36
+ at a 768px window (the modal's own two-pane split). Without the wrap
37
+ the buttons ran past the panel and were clipped by the modal's
38
+ `overflow-hidden`. */}
39
+ <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
40
+ <div className="flex items-center gap-2 min-w-0">
36
41
  {isDisabled ? (
37
42
  <>
38
43
  <Ban size={15} className="text-ink/50 flex-shrink-0" strokeWidth={2.4} />
@@ -68,7 +73,7 @@ export function ProviderAuthSection({
68
73
  </div>
69
74
  </div>
70
75
 
71
- <div className="flex items-center gap-2">
76
+ <div className="flex flex-wrap items-center gap-2">
72
77
  <button
73
78
  type="button"
74
79
  onClick={onToggleDisabled}
@@ -57,7 +57,7 @@ export function DeleteProviderModal({
57
57
  aria-label="Confirm provider deletion"
58
58
  >
59
59
  <div
60
- className="bg-paper border border-ink/20 rounded-xl shadow-2xl w-[min(94vw,460px)] overflow-hidden text-ink"
60
+ className="bg-paper border border-ink/20 rounded-xl shadow-2xl w-[min(94vw,460px)] max-h-full overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static text-ink"
61
61
  onClick={(e) => e.stopPropagation()}
62
62
  >
63
63
  <div className="flex items-center gap-3 px-5 py-4 border-b border-ink/10">