ompchamber 3.1.1 → 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 (109) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +279 -280
  3. package/dist/client/static/js/index-01dv383b.css +1 -0
  4. package/dist/client/static/js/{index-qhzsybkn.js → index-1q6165r3.js} +641 -641
  5. package/package.json +1 -1
  6. package/src/cli/lib/output.js +15 -8
  7. package/src/client/components/common/Modal.tsx +14 -4
  8. package/src/client/components/{layout/session-sidebar/SubagentList.tsx → common/subagent-list/index.tsx} +8 -10
  9. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +6 -30
  10. package/src/client/components/layout/session-sidebar/Toolbar.tsx +11 -5
  11. package/src/client/components/layout/session-sidebar/index.tsx +4 -2
  12. package/src/client/components/mobile/LayoutWrapper.tsx +6 -0
  13. package/src/client/components/mobile/SessionSidebar.tsx +4 -0
  14. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +66 -29
  15. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +50 -12
  16. package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +21 -1
  17. package/src/client/components/settings/Modal.tsx +35 -6
  18. package/src/client/components/settings/SettingsSidebarList.tsx +1 -1
  19. package/src/client/components/settings/categories/AgentSettings.tsx +45 -28
  20. package/src/client/components/settings/categories/CommandSettings.tsx +45 -28
  21. package/src/client/components/settings/categories/McpSettings.tsx +48 -31
  22. package/src/client/components/settings/categories/ProjectSettings.tsx +34 -19
  23. package/src/client/components/settings/categories/ProviderSettings.tsx +77 -53
  24. package/src/client/components/settings/categories/SkillSettings.tsx +34 -17
  25. package/src/client/components/settings/categories/UsageSettings.tsx +53 -39
  26. package/src/client/components/settings/categories/behavior-settings/Editor.tsx +7 -7
  27. package/src/client/components/settings/categories/mcp-settings/ImportModal.tsx +1 -1
  28. package/src/client/components/settings/categories/mcp-settings/SidebarList.tsx +1 -1
  29. package/src/client/components/settings/categories/project-settings/SidebarList.tsx +1 -1
  30. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +8 -3
  31. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +1 -1
  32. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +9 -4
  33. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +1 -1
  34. package/src/client/components/settings/categories/skill-catalog/AddSourceModal.tsx +1 -1
  35. package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +1 -1
  36. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +1 -1
  37. package/src/client/components/settings/master-detail/index.tsx +68 -0
  38. package/src/client/components/workspace/SearchPanel.tsx +2 -1
  39. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +7 -1
  40. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +2 -1
  41. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +2 -1
  42. package/src/client/components/workspace/chat-timeline/index.tsx +1 -0
  43. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +2 -1
  44. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +2 -1
  45. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +2 -1
  46. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +2 -1
  47. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +2 -1
  48. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +2 -1
  49. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +2 -1
  50. package/src/client/components/workspace/diff-panel/SplitView.tsx +31 -5
  51. package/src/client/components/workspace/diff-panel/Toolbar.tsx +77 -6
  52. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +18 -5
  53. package/src/client/components/workspace/diff-panel/index.tsx +36 -7
  54. package/src/client/components/workspace/editor/Toolbar.tsx +11 -4
  55. package/src/client/components/workspace/git-panel/commit-modal/CommitDiffViewer.tsx +2 -1
  56. package/src/client/hooks/chat/omp/session-list.tsx +31 -1
  57. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +7 -1
  58. package/src/client/hooks/chat/subagent/index.ts +37 -11
  59. package/src/client/hooks/editor/use-file-editor.ts +1 -1
  60. package/src/client/hooks/settings/master-detail.ts +33 -0
  61. package/src/client/hooks/workspace/expanded-sessions.ts +62 -0
  62. package/src/server/lib/db/client.ts +5 -1
  63. package/src/server/lib/fs/git-diff.ts +34 -10
  64. package/src/server/lib/fs/git-repos.ts +81 -0
  65. package/src/server/lib/lifecycle/flock.ts +199 -0
  66. package/src/server/lib/lifecycle/identity.ts +5 -1
  67. package/src/server/lib/lifecycle/port-guard.ts +18 -3
  68. package/src/server/lib/lifecycle/proc/darwin.ts +52 -17
  69. package/src/server/lib/lifecycle/proc/linux.ts +29 -7
  70. package/src/server/lib/lifecycle/proc/ps.ts +7 -0
  71. package/src/server/lib/lifecycle/proc/types.ts +17 -0
  72. package/src/server/lib/lifecycle/proc/win32.ts +5 -0
  73. package/src/server/lib/observer-ws.ts +150 -0
  74. package/src/server/lib/omp/rpc/constants.ts +9 -0
  75. package/src/server/lib/omp/rpc/subagent-liveness.ts +2 -12
  76. package/src/server/lib/omp/session/file-scan.ts +7 -1
  77. package/src/server/lib/omp/session/files.ts +95 -96
  78. package/src/server/lib/omp/session/locator.ts +18 -8
  79. package/src/server/lib/omp/session/messages.ts +7 -7
  80. package/src/server/lib/omp/session/scan.ts +108 -0
  81. package/src/server/lib/omp/session/stats.ts +50 -50
  82. package/src/server/lib/omp/session/telemetry/scan.ts +89 -7
  83. package/src/server/lib/omp/session/telemetry-raw.ts +24 -8
  84. package/src/server/lib/omp/session/usage/aggregate.ts +170 -55
  85. package/src/server/lib/terminal/runtime.server.ts +3 -0
  86. package/src/server/lib/terminal/session.server.ts +30 -3
  87. package/src/server/routes/agent/command.test.ts +69 -0
  88. package/src/server/routes/agent/command.ts +14 -0
  89. package/src/server/routes/agent/ws.test.ts +159 -0
  90. package/src/server/routes/agent/ws.ts +22 -97
  91. package/src/server/routes/btw/ws.ts +22 -95
  92. package/src/server/routes/fs/git.ts +61 -70
  93. package/src/server/routes/omp/session-stats.ts +1 -1
  94. package/src/server/routes/telemetry/tokens.ts +6 -4
  95. package/src/server/routes/terminal/run.ts +4 -8
  96. package/src/server/routes/terminal/ws.ts +24 -2
  97. package/src/shared/lib/chat/omp/agent-events.test.ts +33 -1
  98. package/src/shared/lib/chat/omp/agent-events.ts +10 -0
  99. package/src/shared/lib/code/language.ts +146 -0
  100. package/src/shared/lib/code/syntax-highlight.test.ts +2 -1
  101. package/src/shared/lib/code/syntax-highlight.ts +0 -122
  102. package/src/shared/lib/fs/diff-parser.test.ts +5 -5
  103. package/src/shared/lib/fs/diff-parser.ts +67 -42
  104. package/src/shared/lib/omp/subagent/parse.test.ts +88 -0
  105. package/src/shared/lib/omp/subagent/parse.ts +17 -0
  106. package/src/shared/lib/omp/subagent/transcript-client.test.ts +100 -0
  107. package/src/shared/lib/omp/subagent/transcript-client.ts +17 -1
  108. package/src/shared/types/omp/subagent.ts +6 -0
  109. package/dist/client/static/js/index-5yfre1kw.css +0 -1
@@ -84,7 +84,11 @@ export function SettingsModal({
84
84
  if (isOpen) {
85
85
  setActiveCategory(initialCategory);
86
86
  setAutoOpenAdd(autoOpenAddProvider);
87
- setIsMobileDrilled(false);
87
+ // `autoOpenAdd` asks for the provider dialog, which a category renders
88
+ // inside its own detail pane. On a phone the modal would otherwise open
89
+ // inside a `display: none` subtree and never be seen, so the request also
90
+ // lands the user on the category screen it belongs to.
91
+ setIsMobileDrilled(autoOpenAddProvider);
88
92
  setSearchQuery('');
89
93
  }
90
94
  }, [isOpen, initialCategory, autoOpenAddProvider]);
@@ -206,8 +210,19 @@ export function SettingsModal({
206
210
 
207
211
  {/* DESKTOP & MOBILE DETAIL VIEW */}
208
212
  <div className={`flex-1 flex-col h-full bg-paper overflow-hidden ${isMobileDrilled ? 'flex' : 'hidden md:flex'}`}>
209
- {/* Header Bar */}
210
- <div className="h-16 px-6 border-b border-ink/10 flex items-center justify-between flex-shrink-0 bg-paper">
213
+ {/* Header Bar. The height and padding carry the safe-area insets on a
214
+ phone: the modal is full-screen below `md`, so without them the
215
+ back button and the title sit under the status bar / notch, the
216
+ same way the mobile chrome (header, editor, diff) already avoids. */}
217
+ <div
218
+ className="px-6 border-b border-ink/10 flex items-center justify-between flex-shrink-0 bg-paper"
219
+ style={{
220
+ height: 'calc(4rem + env(safe-area-inset-top, 0px))',
221
+ paddingTop: 'env(safe-area-inset-top, 0px)',
222
+ paddingLeft: 'max(1.5rem, env(safe-area-inset-left, 0px))',
223
+ paddingRight: 'max(1.5rem, env(safe-area-inset-right, 0px))',
224
+ }}
225
+ >
211
226
  <div className="flex items-center space-x-3 min-w-0">
212
227
  {/* Mobile Back Button */}
213
228
  <button
@@ -241,7 +256,10 @@ export function SettingsModal({
241
256
  </div>
242
257
 
243
258
  {/* Scrollable Settings Body - Full Width across all categories */}
244
- <div className={`flex-1 scrollbar-overlay-container scrollbar-overlay-static w-full ${['projects', 'providers', 'agents', 'behavior', 'commands', 'mcp', 'skills', 'skills-catalog', 'usage'].includes(activeCategory) ? 'p-0 flex flex-col' : 'p-6 md:p-8 flex flex-col'}`}>
259
+ <div
260
+ className={`flex-1 scrollbar-overlay-container scrollbar-overlay-static w-full ${['projects', 'providers', 'agents', 'behavior', 'commands', 'mcp', 'skills', 'skills-catalog', 'usage'].includes(activeCategory) ? 'p-0 flex flex-col' : 'p-6 md:p-8 flex flex-col'}`}
261
+ style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}
262
+ >
245
263
  <div className="w-full h-full flex-1 flex flex-col">
246
264
  {renderCategoryContent()}
247
265
  </div>
@@ -250,7 +268,15 @@ export function SettingsModal({
250
268
 
251
269
  {/* MOBILE CATEGORY LIST VIEW (When not drilled into detail) */}
252
270
  <div className={`flex-1 flex-col h-full bg-canvas md:hidden ${isMobileDrilled ? 'hidden' : 'flex'}`}>
253
- <div className="h-14 px-4 border-b border-ink/10 flex items-center justify-between flex-shrink-0 bg-canvas">
271
+ <div
272
+ className="px-4 border-b border-ink/10 flex items-center justify-between flex-shrink-0 bg-canvas"
273
+ style={{
274
+ height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
275
+ paddingTop: 'env(safe-area-inset-top, 0px)',
276
+ paddingLeft: 'max(1rem, env(safe-area-inset-left, 0px))',
277
+ paddingRight: 'max(1rem, env(safe-area-inset-right, 0px))',
278
+ }}
279
+ >
254
280
  <span className="font-bold text-sm tracking-tight text-ink">Settings</span>
255
281
  <button
256
282
  type="button"
@@ -262,7 +288,10 @@ export function SettingsModal({
262
288
  </button>
263
289
  </div>
264
290
 
265
- <div className="flex-1 overflow-hidden">
291
+ <div
292
+ className="flex-1 overflow-hidden"
293
+ style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }}
294
+ >
266
295
  <SettingsSidebar
267
296
  activeCategory={activeCategory}
268
297
  onSelectCategory={handleSelectCategory}
@@ -77,7 +77,7 @@ export const SettingsSidebarList: FunctionComponent<SettingsSidebarListProps> =
77
77
  };
78
78
 
79
79
  return (
80
- <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">
80
+ <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">
81
81
  <ProjectSelectorDropdown
82
82
  selectedProject={selectedProject}
83
83
  onChangeProject={onChangeProject}
@@ -5,6 +5,8 @@ import { AgentSidebarList } from '@/client/components/settings/categories/agent-
5
5
  import { AgentDetailPane } from '@/client/components/settings/categories/agent-settings/DetailPane';
6
6
  import { LoadingState } from '@/client/components/settings/LoadingState';
7
7
  import { useCrudList } from '@/client/hooks/settings/crud-list';
8
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
9
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
8
10
 
9
11
  interface AgentSettingsProps {
10
12
  settings: SettingsState;
@@ -12,6 +14,7 @@ interface AgentSettingsProps {
12
14
  }
13
15
 
14
16
  export const AgentSettings: FunctionComponent<AgentSettingsProps> = () => {
17
+ const masterDetail = useSettingsMasterDetail();
15
18
  const [selectedProject, setSelectedProject] = useState('ompchamber');
16
19
  const { items: agents, selectedId, selected: selectedAgent, isCreatingNew, isLoading, select, startCreate, save, remove } =
17
20
  useCrudList<AgentItem>({
@@ -49,36 +52,50 @@ export const AgentSettings: FunctionComponent<AgentSettingsProps> = () => {
49
52
 
50
53
  return (
51
54
  <div className="flex h-full w-full overflow-hidden bg-paper">
52
- <AgentSidebarList
53
- agents={agents}
54
- selectedAgentId={isCreatingNew ? null : selectedId}
55
- onSelectAgent={select}
56
- onAddNewAgent={startCreate}
57
- selectedProject={selectedProject}
58
- onChangeProject={setSelectedProject}
59
- />
60
-
61
- <div className="flex-1 h-full overflow-hidden flex flex-col">
62
- {isCreatingNew ? (
63
- <AgentDetailPane
64
- agent={emptyAgentTemplate}
65
- isNew={true}
66
- onSave={save}
55
+ <SettingsMasterDetail
56
+ pane={masterDetail.pane}
57
+ onBack={masterDetail.back}
58
+ listLabel="Agents"
59
+ list={
60
+ <AgentSidebarList
61
+ agents={agents}
62
+ selectedAgentId={isCreatingNew ? null : selectedId}
63
+ onSelectAgent={(id) => {
64
+ select(id);
65
+ masterDetail.openDetail();
66
+ }}
67
+ onAddNewAgent={() => {
68
+ startCreate();
69
+ masterDetail.openDetail();
70
+ }}
71
+ selectedProject={selectedProject}
72
+ onChangeProject={setSelectedProject}
67
73
  />
68
- ) : selectedAgent ? (
69
- <AgentDetailPane
70
- key={selectedAgent.id}
71
- agent={selectedAgent}
72
- isNew={false}
73
- onSave={save}
74
- onDelete={remove}
75
- />
76
- ) : (
77
- <div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
78
- Select an agent or click + to create one
74
+ }
75
+ detail={
76
+ <div className="flex-1 h-full overflow-hidden flex flex-col">
77
+ {isCreatingNew ? (
78
+ <AgentDetailPane
79
+ agent={emptyAgentTemplate}
80
+ isNew={true}
81
+ onSave={save}
82
+ />
83
+ ) : selectedAgent ? (
84
+ <AgentDetailPane
85
+ key={selectedAgent.id}
86
+ agent={selectedAgent}
87
+ isNew={false}
88
+ onSave={save}
89
+ onDelete={remove}
90
+ />
91
+ ) : (
92
+ <div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
93
+ Select an agent or click + to create one
94
+ </div>
95
+ )}
79
96
  </div>
80
- )}
81
- </div>
97
+ }
98
+ />
82
99
  </div>
83
100
  );
84
101
  };
@@ -5,6 +5,8 @@ import { CommandSidebarList } from '@/client/components/settings/categories/comm
5
5
  import { CommandDetailPane } from '@/client/components/settings/categories/command-settings/DetailPane';
6
6
  import { LoadingState } from '@/client/components/settings/LoadingState';
7
7
  import { useCrudList } from '@/client/hooks/settings/crud-list';
8
+ import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
9
+ import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
8
10
 
9
11
  interface CommandSettingsProps {
10
12
  settings: SettingsState;
@@ -12,6 +14,7 @@ interface CommandSettingsProps {
12
14
  }
13
15
 
14
16
  export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
17
+ const masterDetail = useSettingsMasterDetail();
15
18
  const [selectedProject, setSelectedProject] = useState('ompchamber');
16
19
  const { items: commands, selectedId, selected: selectedCommand, isCreatingNew, isLoading, select, startCreate, save, remove } =
17
20
  useCrudList<CommandItem>({
@@ -46,36 +49,50 @@ export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
46
49
 
47
50
  return (
48
51
  <div className="flex h-full w-full overflow-hidden bg-paper">
49
- <CommandSidebarList
50
- commands={commands}
51
- selectedCommandId={isCreatingNew ? null : selectedId}
52
- onSelectCommand={select}
53
- onAddNewCommand={startCreate}
54
- selectedProject={selectedProject}
55
- onChangeProject={setSelectedProject}
56
- />
57
-
58
- <div className="flex-1 h-full overflow-hidden flex flex-col">
59
- {isCreatingNew ? (
60
- <CommandDetailPane
61
- command={emptyCommandTemplate}
62
- isNew={true}
63
- onSave={save}
52
+ <SettingsMasterDetail
53
+ pane={masterDetail.pane}
54
+ onBack={masterDetail.back}
55
+ listLabel="Commands"
56
+ list={
57
+ <CommandSidebarList
58
+ commands={commands}
59
+ selectedCommandId={isCreatingNew ? null : selectedId}
60
+ onSelectCommand={(id) => {
61
+ select(id);
62
+ masterDetail.openDetail();
63
+ }}
64
+ onAddNewCommand={() => {
65
+ startCreate();
66
+ masterDetail.openDetail();
67
+ }}
68
+ selectedProject={selectedProject}
69
+ onChangeProject={setSelectedProject}
64
70
  />
65
- ) : selectedCommand ? (
66
- <CommandDetailPane
67
- key={selectedCommand.id}
68
- command={selectedCommand}
69
- isNew={false}
70
- onSave={save}
71
- onDelete={remove}
72
- />
73
- ) : (
74
- <div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
75
- Select a command or click + to create one
71
+ }
72
+ detail={
73
+ <div className="flex-1 h-full overflow-hidden flex flex-col">
74
+ {isCreatingNew ? (
75
+ <CommandDetailPane
76
+ command={emptyCommandTemplate}
77
+ isNew={true}
78
+ onSave={save}
79
+ />
80
+ ) : selectedCommand ? (
81
+ <CommandDetailPane
82
+ key={selectedCommand.id}
83
+ command={selectedCommand}
84
+ isNew={false}
85
+ onSave={save}
86
+ onDelete={remove}
87
+ />
88
+ ) : (
89
+ <div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
90
+ Select a command or click + to create one
91
+ </div>
92
+ )}
76
93
  </div>
77
- )}
78
- </div>
94
+ }
95
+ />
79
96
  </div>
80
97
  );
81
98
  };
@@ -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
  />