ompchamber 3.5.0 → 3.6.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 (71) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +357 -353
  3. package/dist/client/static/js/index-7n838fz5.css +1 -0
  4. package/dist/client/static/js/{index-w0cem7n5.js → index-qp2sfmn2.js} +591 -592
  5. package/package.json +1 -1
  6. package/src/cli/lib/commands/status.js +7 -0
  7. package/src/client/components/layout/session-sidebar/about-modal/UpdateLog.tsx +50 -0
  8. package/src/client/components/layout/session-sidebar/about-modal/UpdateSection.tsx +12 -17
  9. package/src/client/components/settings/ProjectSelectorDropdown.tsx +8 -2
  10. package/src/client/components/settings/SettingsSidebarList.tsx +5 -1
  11. package/src/client/components/settings/categories/CommandSettings.tsx +47 -9
  12. package/src/client/components/settings/categories/SkillCatalogSettings.tsx +23 -43
  13. package/src/client/components/settings/categories/SkillSettings.tsx +51 -7
  14. package/src/client/components/settings/categories/command-settings/DetailPane.tsx +109 -103
  15. package/src/client/components/settings/categories/command-settings/SidebarList.tsx +6 -1
  16. package/src/client/components/settings/categories/skill-settings/DetailPane.tsx +130 -31
  17. package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +9 -3
  18. package/src/client/data/settings/command.ts +0 -40
  19. package/src/client/hooks/settings/workspace-roots.ts +99 -0
  20. package/src/client/hooks/ui/updates/index.ts +178 -0
  21. package/src/server/db.server.ts +41 -11
  22. package/src/server/index.ts +17 -1
  23. package/src/server/lib/db/client.ts +17 -0
  24. package/src/server/lib/db/compact-chat-overlay.test.ts +121 -0
  25. package/src/server/lib/db/compact-chat-overlay.ts +90 -0
  26. package/src/server/lib/lifecycle/fd-pressure.test.ts +66 -0
  27. package/src/server/lib/lifecycle/fd-pressure.ts +159 -0
  28. package/src/server/lib/omp/config/commands.test.ts +167 -0
  29. package/src/server/lib/omp/config/commands.ts +195 -0
  30. package/src/server/lib/omp/config/scope.test.ts +49 -0
  31. package/src/server/lib/omp/config/scope.ts +65 -0
  32. package/src/server/lib/omp/config/skills-catalog.ts +35 -3
  33. package/src/server/lib/omp/config/skills.test.ts +105 -0
  34. package/src/server/lib/omp/config/skills.ts +260 -47
  35. package/src/server/lib/omp/rpc/process.ts +22 -14
  36. package/src/server/lib/omp/rpc/session-registry.ts +6 -0
  37. package/src/server/lib/omp/rpc/utility.ts +45 -23
  38. package/src/server/lib/omp/session/merge-stored-anchor.test.ts +85 -0
  39. package/src/server/lib/omp/session/merge-stored-overlay-rows.test.ts +106 -0
  40. package/src/server/lib/omp/session/merge-stored.ts +78 -11
  41. package/src/server/lib/omp/session/reload.server.test.ts +84 -0
  42. package/src/server/lib/omp/session/reload.server.ts +47 -0
  43. package/src/server/lib/omp/session/subagent-presence.ts +40 -9
  44. package/src/server/lib/terminal/liveness.server.ts +65 -0
  45. package/src/server/lib/terminal/runtime.server.ts +70 -117
  46. package/src/server/lib/terminal/session.server.ts +5 -2
  47. package/src/server/lib/terminal/store.ts +72 -0
  48. package/src/server/lib/updates/apply.ts +62 -33
  49. package/src/server/lib/updates/install.ts +9 -2
  50. package/src/server/lib/updates/omp.ts +37 -9
  51. package/src/server/lib/updates/single-flight.test.ts +61 -0
  52. package/src/server/lib/updates/single-flight.ts +68 -0
  53. package/src/server/routes/chat/session.ts +13 -3
  54. package/src/server/routes/health/probe.ts +8 -0
  55. package/src/server/routes/omp/index.ts +0 -2
  56. package/src/server/routes/settings/commands.ts +102 -15
  57. package/src/server/routes/settings/skills.ts +165 -113
  58. package/src/server/routes/terminal/sessions.ts +1 -1
  59. package/src/server/routes/updates/apply.ts +101 -14
  60. package/src/shared/lib/chat/composer/client.ts +27 -18
  61. package/src/shared/lib/chat/composer/scope.test.ts +87 -0
  62. package/src/shared/lib/markdown/katex.ts +11 -1
  63. package/src/shared/lib/markdown/marked.ts +68 -13
  64. package/src/shared/lib/markdown/math.test.ts +53 -1
  65. package/src/shared/lib/updates/progress.test.ts +54 -0
  66. package/src/shared/lib/updates/progress.ts +50 -0
  67. package/src/shared/types/settings/command.ts +15 -2
  68. package/src/shared/types/settings/skill.ts +13 -0
  69. package/dist/client/static/js/index-vx0ernrv.css +0 -1
  70. package/src/client/hooks/ui/updates.ts +0 -108
  71. package/src/server/routes/omp/commands.ts +0 -29
@@ -1,45 +1,47 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
  import type { FormEvent, FunctionComponent } from 'preact/compat';
3
- import { Bot, Check, Copy, PenTool, Trash2 } from 'lucide-preact';
4
- import type { AgentItem, CommandItem, CommandScope } from '@/shared/types';
3
+ import { Check, Copy, Eye, EyeOff, Trash2 } from 'lucide-preact';
4
+ import type { CommandItem, CommandScope } from '@/shared/types';
5
5
 
6
6
  interface CommandDetailPaneProps {
7
7
  command: CommandItem;
8
8
  isNew?: boolean;
9
9
  onSave: (updatedCommand: CommandItem) => void;
10
10
  onDelete?: (commandId: string) => void;
11
+ /** Disabled for the project scope: there is no workspace to write it into. */
12
+ projectScopeDisabled?: boolean;
11
13
  }
12
14
 
13
- const AVAILABLE_MODELS = [
14
- 'Not selected',
15
- 'claude-3-7-sonnet-20250219',
16
- 'claude-3-5-sonnet-20241022',
17
- 'claude-3-5-haiku-20241022',
18
- 'gpt-4o',
19
- 'gpt-4o-mini',
20
- 'gemini-2.5-pro-preview-03-25',
21
- 'deepseek-chat-v3',
22
- ];
23
-
15
+ /**
16
+ * One command's detail pane.
17
+ *
18
+ * The three sections mirror what a command IS in oh-my-pi: a name, the scope
19
+ * its file lives at, and a markdown template. The template is the whole
20
+ * behavior — `$ARGUMENTS`, `` !`shell` `` and `@file` are expanded by omp when
21
+ * the command runs — so the editor is the point of the pane, and the body is
22
+ * what a save writes back into the file.
23
+ *
24
+ * A command backed by a file the chamber does not own (a `.claude` or
25
+ * `.agents` command, a builtin, a skill entry) renders read-only: its file
26
+ * belongs to another tool and a save here would rewrite it.
27
+ */
24
28
  export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
25
29
  command,
26
30
  isNew = false,
27
31
  onSave,
28
32
  onDelete,
33
+ projectScopeDisabled = false,
29
34
  }) => {
30
35
  const [formData, setFormData] = useState<CommandItem>(command);
31
36
  const [savedSuccess, setSavedSuccess] = useState(false);
32
37
  const [copiedTemplate, setCopiedTemplate] = useState(false);
33
- const [availableAgents, setAvailableAgents] = useState<AgentItem[]>([]);
38
+ const [showLineNumbers, setShowLineNumbers] = useState(true);
34
39
 
35
- useEffect(() => {
36
- fetch('/api/settings/agents')
37
- .then(res => res.json())
38
- .then(data => {
39
- if (data?.agents) setAvailableAgents(data.agents);
40
- })
41
- .catch(console.error);
42
- }, []);
40
+ // A LIVE command the chamber does not own (a builtin, a `skill:<name>` entry,
41
+ // an extension or a `.claude`/`.agents` file) renders read-only: its file, if
42
+ // it has one, belongs to another tool. A chamber-store row and a brand-new
43
+ // draft have no source and stay editable.
44
+ const readOnly = !isNew && command.source != null && command.managed !== true;
43
45
 
44
46
  useEffect(() => {
45
47
  setFormData(command);
@@ -66,17 +68,37 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
66
68
  }));
67
69
  };
68
70
 
71
+ const lines = formData.template.split('\n');
72
+ const lineCount = Math.max(lines.length, 6);
73
+
69
74
  return (
70
75
  <form onSubmit={handleSave} className="flex-1 flex flex-col h-full scrollbar-overlay-container scrollbar-overlay-static bg-paper text-ink p-6 md:p-8 space-y-6">
76
+ {readOnly && (
77
+ <div className="rounded-lg border border-ink/15 bg-ink/5 px-3 py-2 text-[11px] text-ink/70 leading-relaxed">
78
+ {formData.filePath ? (
79
+ <>
80
+ This command is provided by <span className="font-mono text-ink/80">{formData.source}</span> and is read-only here.
81
+ Edit <span className="font-mono text-ink/80">{formData.filePath}</span> at its source, or create a command of your own
82
+ under <span className="font-mono text-ink/80">~/.omp/agent/commands</span>.
83
+ </>
84
+ ) : (
85
+ <>
86
+ This is a <span className="font-mono text-ink/80">{formData.source ?? 'live'}</span> command built into the agent, not a
87
+ file. It cannot be edited or deleted here — create a command with another name to extend it.
88
+ </>
89
+ )}
90
+ </div>
91
+ )}
92
+
71
93
  {/* SECTION 1: Identity */}
72
94
  <div className="space-y-3">
73
95
  <div className="flex items-center justify-between">
74
96
  <h2 className="text-xs font-bold uppercase tracking-wider text-ink">
75
97
  Identity
76
98
  </h2>
77
- {formData.isBuiltIn && (
99
+ {readOnly && (
78
100
  <span className="text-[10px] font-mono px-2 py-0.5 rounded-md bg-ink/5 border border-ink/10 text-ink/60">
79
- Built-in System Command
101
+ {formData.source ?? 'live'} · read-only
80
102
  </span>
81
103
  )}
82
104
  </div>
@@ -101,7 +123,7 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
101
123
  }
102
124
  required
103
125
  placeholder="command-name"
104
- disabled={formData.isBuiltIn}
126
+ disabled={readOnly}
105
127
  className="w-full text-xs font-mono px-3 py-2 bg-transparent focus:outline-none disabled:opacity-60 text-ink"
106
128
  />
107
129
  </div>
@@ -116,12 +138,12 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
116
138
  onChange={(e) =>
117
139
  setFormData({ ...formData, scope: e.currentTarget.value as CommandScope })
118
140
  }
119
- disabled={formData.isBuiltIn}
141
+ disabled={readOnly || projectScopeDisabled}
142
+ title={projectScopeDisabled ? 'Select a workspace folder to write a project command' : undefined}
120
143
  className="w-full text-xs py-2 px-3 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink disabled:opacity-60 text-ink transition-colors cursor-pointer"
121
144
  >
122
145
  <option value="user">user</option>
123
- <option value="project">project</option>
124
- <option value="system">system</option>
146
+ {!projectScopeDisabled && <option value="project">project</option>}
125
147
  </select>
126
148
  </div>
127
149
  </div>
@@ -133,74 +155,34 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
133
155
  <textarea
134
156
  value={formData.description}
135
157
  onChange={(e) => setFormData({ ...formData, description: e.currentTarget.value })}
158
+ disabled={readOnly}
136
159
  placeholder="What does this command do?"
137
160
  rows={2}
138
- className="w-full text-xs p-3 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink placeholder:text-ink/30 text-ink transition-colors"
161
+ className="w-full text-xs p-3 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink placeholder:text-ink/30 text-ink transition-colors disabled:opacity-60"
139
162
  />
140
163
  </div>
141
- </div>
142
-
143
- <div className="border-t border-ink/10" />
144
164
 
145
- {/* SECTION 2: Execution Context */}
146
- <div className="space-y-3">
147
- <h2 className="text-xs font-bold uppercase tracking-wider text-ink">
148
- Execution Context
149
- </h2>
150
-
151
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
152
- {/* Override Agent */}
153
- <div>
154
- <label className="block text-xs font-semibold text-ink/80 mb-1.5">
155
- Override Agent
156
- </label>
157
- <div className="flex items-center border border-ink/20 hover:border-ink/40 focus-within:border-ink rounded-lg bg-paper transition-colors">
158
- <span className="px-2.5 text-ink/40">
159
- <Bot className="w-3.5 h-3.5" />
160
- </span>
161
- <select
162
- value={formData.overrideAgent || 'Not selected'}
163
- onChange={(e) => setFormData({ ...formData, overrideAgent: e.currentTarget.value })}
164
- className="w-full text-xs py-2 pr-3 bg-transparent focus:outline-none cursor-pointer text-ink"
165
- >
166
- <option value="Not selected">Not selected</option>
167
- {availableAgents.map((agent) => (
168
- <option key={agent.id} value={agent.name}>
169
- @{agent.name}
170
- </option>
171
- ))}
172
- </select>
173
- </div>
174
- </div>
175
-
176
- {/* Override Model */}
177
- <div>
178
- <label className="block text-xs font-semibold text-ink/80 mb-1.5">
179
- Override Model
180
- </label>
181
- <div className="flex items-center border border-ink/20 hover:border-ink/40 focus-within:border-ink rounded-lg bg-paper transition-colors">
182
- <span className="px-2.5 text-ink/40">
183
- <PenTool className="w-3.5 h-3.5" />
184
- </span>
185
- <select
186
- value={formData.overrideModel || 'Not selected'}
187
- onChange={(e) => setFormData({ ...formData, overrideModel: e.currentTarget.value })}
188
- className="w-full text-xs font-mono py-2 pr-3 bg-transparent focus:outline-none cursor-pointer text-ink"
189
- >
190
- {AVAILABLE_MODELS.map((m) => (
191
- <option key={m} value={m}>
192
- {m}
193
- </option>
194
- ))}
195
- </select>
196
- </div>
197
- </div>
165
+ <div>
166
+ <label className="block text-xs font-semibold text-ink/80 mb-1.5">
167
+ Argument Hint
168
+ </label>
169
+ <input
170
+ type="text"
171
+ value={formData.inputHint ?? ''}
172
+ onChange={(e) => setFormData({ ...formData, inputHint: e.currentTarget.value })}
173
+ disabled={readOnly}
174
+ placeholder="[on|off|status]"
175
+ className="w-full text-xs font-mono p-2.5 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink placeholder:text-ink/30 text-ink transition-colors disabled:opacity-60"
176
+ />
177
+ <p className="text-[11px] text-ink/60 mt-1">
178
+ Shown as ghost text after <span className="font-mono">/{formData.name || 'name'}</span> in the composer.
179
+ </p>
198
180
  </div>
199
181
  </div>
200
182
 
201
183
  <div className="border-t border-ink/10" />
202
184
 
203
- {/* SECTION 3: Command Template */}
185
+ {/* SECTION 2: Command Template */}
204
186
  <div className="space-y-2 flex-1 flex flex-col">
205
187
  <div className="flex items-center justify-between">
206
188
  <div>
@@ -212,6 +194,14 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
212
194
  </p>
213
195
  </div>
214
196
  <div className="flex items-center gap-2">
197
+ <button
198
+ type="button"
199
+ onClick={() => setShowLineNumbers(!showLineNumbers)}
200
+ title={showLineNumbers ? 'Hide line numbers' : 'Show line numbers'}
201
+ className="p-1 rounded-md text-ink/50 hover:text-ink hover:bg-ink/5 transition-colors cursor-pointer"
202
+ >
203
+ {showLineNumbers ? <Eye size={15} /> : <EyeOff size={15} />}
204
+ </button>
215
205
  <button
216
206
  type="button"
217
207
  onClick={handleCopyTemplate}
@@ -228,39 +218,53 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
228
218
  <button
229
219
  type="button"
230
220
  onClick={() => handleInsertPlaceholder('$ARGUMENTS')}
231
- className="px-2.5 py-1 text-xs font-mono rounded-md bg-ink/5 hover:bg-ink/10 border border-ink/15 text-ink transition-colors cursor-pointer"
221
+ disabled={readOnly}
222
+ className="px-2.5 py-1 text-xs font-mono rounded-md bg-ink/5 hover:bg-ink/10 border border-ink/15 text-ink transition-colors cursor-pointer disabled:opacity-50"
232
223
  >
233
224
  +$ARGUMENTS
234
225
  </button>
235
226
  <button
236
227
  type="button"
237
228
  onClick={() => handleInsertPlaceholder('!`bun test`')}
238
- className="px-2.5 py-1 text-xs font-mono rounded-md bg-ink/5 hover:bg-ink/10 border border-ink/15 text-ink transition-colors cursor-pointer"
229
+ disabled={readOnly}
230
+ className="px-2.5 py-1 text-xs font-mono rounded-md bg-ink/5 hover:bg-ink/10 border border-ink/15 text-ink transition-colors cursor-pointer disabled:opacity-50"
239
231
  >
240
232
  +!shell
241
233
  </button>
242
234
  <button
243
235
  type="button"
244
236
  onClick={() => handleInsertPlaceholder('@src/App.tsx')}
245
- className="px-2.5 py-1 text-xs font-mono rounded-md bg-ink/5 hover:bg-ink/10 border border-ink/15 text-ink transition-colors cursor-pointer"
237
+ disabled={readOnly}
238
+ className="px-2.5 py-1 text-xs font-mono rounded-md bg-ink/5 hover:bg-ink/10 border border-ink/15 text-ink transition-colors cursor-pointer disabled:opacity-50"
246
239
  >
247
240
  +@filename
248
241
  </button>
249
242
  </div>
250
243
 
251
- <textarea
252
- value={formData.template}
253
- onChange={(e) => setFormData({ ...formData, template: e.currentTarget.value })}
254
- rows={7}
255
- placeholder="Enter command template instructions..."
256
- className="w-full flex-1 min-h-[140px] text-xs font-mono p-3 rounded-lg border border-ink/20 hover:border-ink/40 bg-ink/5 focus:outline-none focus:border-ink leading-relaxed transition-colors"
257
- />
244
+ <div className="flex rounded-lg border border-ink/20 hover:border-ink/40 bg-ink/5 overflow-hidden flex-1 min-h-[140px] font-mono text-xs focus-within:border-ink transition-colors">
245
+ {showLineNumbers && (
246
+ <div className="w-10 py-3 pr-2 select-none text-right text-xs text-ink/30 border-r border-ink/10 bg-ink/[0.02] overflow-hidden leading-relaxed">
247
+ {Array.from({ length: lineCount }).map((_, i) => (
248
+ <div key={i}>{i + 1}</div>
249
+ ))}
250
+ </div>
251
+ )}
252
+ <textarea
253
+ value={formData.template}
254
+ onChange={(e) => setFormData({ ...formData, template: e.currentTarget.value })}
255
+ rows={Math.max(lineCount, 7)}
256
+ disabled={readOnly}
257
+ spellcheck={false}
258
+ placeholder="Enter command template instructions..."
259
+ className="flex-1 p-3 bg-transparent text-ink resize-none focus:outline-none leading-relaxed disabled:opacity-60"
260
+ />
261
+ </div>
258
262
  </div>
259
263
 
260
264
  {/* Footer Controls */}
261
265
  <div className="pt-3 border-t border-ink/10 flex items-center justify-between">
262
266
  <div>
263
- {!formData.isBuiltIn && !isNew && onDelete && (
267
+ {!readOnly && !isNew && onDelete && (
264
268
  <button
265
269
  type="button"
266
270
  onClick={() => onDelete(formData.id)}
@@ -279,12 +283,14 @@ export const CommandDetailPane: FunctionComponent<CommandDetailPaneProps> = ({
279
283
  Saved successfully
280
284
  </span>
281
285
  )}
282
- <button
283
- type="submit"
284
- className="px-4 py-2 rounded-lg bg-ink text-paper text-xs font-medium hover:bg-ink/90 transition-colors shadow-xs cursor-pointer"
285
- >
286
- {isNew ? 'Create Command' : 'Save Command'}
287
- </button>
286
+ {!readOnly && (
287
+ <button
288
+ type="submit"
289
+ className="px-4 py-2 rounded-lg bg-ink text-paper text-xs font-medium hover:bg-ink/90 transition-colors shadow-xs cursor-pointer"
290
+ >
291
+ {isNew ? 'Create Command' : 'Save Command'}
292
+ </button>
293
+ )}
288
294
  </div>
289
295
  </div>
290
296
  </form>
@@ -1,6 +1,7 @@
1
1
  import type { FunctionComponent } from 'preact/compat';
2
2
  import { Terminal } from 'lucide-preact';
3
3
  import type { CommandItem } from '@/shared/types';
4
+ import type { ProjectOption } from '@/client/components/settings/ProjectSelectorDropdown';
4
5
  import { SettingsSidebarList } from '@/client/components/settings/SettingsSidebarList';
5
6
 
6
7
  interface CommandSidebarListProps {
@@ -10,6 +11,8 @@ interface CommandSidebarListProps {
10
11
  onAddNewCommand: () => void;
11
12
  selectedProject: string;
12
13
  onChangeProject: (project: string) => void;
14
+ /** Real workspace folders; defaults to the shared placeholder list. */
15
+ projectOptions?: ProjectOption[];
13
16
  }
14
17
 
15
18
  export const CommandSidebarList: FunctionComponent<CommandSidebarListProps> = ({
@@ -19,6 +22,7 @@ export const CommandSidebarList: FunctionComponent<CommandSidebarListProps> = ({
19
22
  onAddNewCommand,
20
23
  selectedProject,
21
24
  onChangeProject,
25
+ projectOptions,
22
26
  }) => (
23
27
  <SettingsSidebarList
24
28
  items={commands}
@@ -27,11 +31,12 @@ export const CommandSidebarList: FunctionComponent<CommandSidebarListProps> = ({
27
31
  onAddNew={onAddNewCommand}
28
32
  addTitle="Create new command"
29
33
  builtInLabel="Built-in Commands"
30
- customLabel="Custom Commands"
34
+ customLabel="Command Files"
31
35
  formatName={(name) => `/${name}`}
32
36
  nameClassName="font-mono font-semibold text-xs text-ink truncate"
33
37
  selectedProject={selectedProject}
34
38
  onChangeProject={onChangeProject}
39
+ {...(projectOptions ? { projectOptions } : {})}
35
40
  renderIcon={(isBuiltIn) => (
36
41
  <Terminal className={`w-3.5 h-3.5 ${isBuiltIn ? 'text-ink/70' : 'text-ink/60'} flex-shrink-0`} />
37
42
  )}
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
- import { Check, Eye, EyeOff, Info, Save, Sparkles, Trash2, User } from 'lucide-preact';
2
+ import { Check, Eye, EyeOff, Info, RefreshCw, Save, Sparkles, Trash2, User } from 'lucide-preact';
3
3
  import type { SkillItem } from '@/shared/types';
4
4
 
5
5
  interface SkillDetailPaneProps {
@@ -8,6 +8,18 @@ interface SkillDetailPaneProps {
8
8
  onSave: (skillData: Partial<SkillItem>) => void;
9
9
  onDelete?: (id: string) => void;
10
10
  onOpenCatalog?: () => void;
11
+ onReload?: () => void;
12
+ isReloading?: boolean;
13
+ /** Disabled when no workspace is selected: there is no `.omp/skills` to
14
+ * write a project skill into. */
15
+ projectScopeDisabled?: boolean;
16
+ /**
17
+ * Root a NEW skill lands in, from the scope the panel is showing. Without
18
+ * this a create made while a workspace is selected wrote to the user root —
19
+ * the skill then appeared in a list the panel was not showing, and the
20
+ * workspace's own `.omp/skills` never received it.
21
+ */
22
+ defaultLocation?: 'user' | 'project';
11
23
  }
12
24
 
13
25
  export function SkillDetailPane({
@@ -16,33 +28,45 @@ export function SkillDetailPane({
16
28
  onSave,
17
29
  onDelete,
18
30
  onOpenCatalog,
31
+ onReload,
32
+ isReloading = false,
33
+ projectScopeDisabled = false,
34
+ defaultLocation = 'user',
19
35
  }: SkillDetailPaneProps) {
20
36
  const [name, setName] = useState('');
21
37
  const [description, setDescription] = useState('');
22
38
  const [location, setLocation] = useState<'user' | 'project'>('user');
23
- const [locationLabel, setLocationLabel] = useState('User / OpenCode');
39
+ const [locationLabel, setLocationLabel] = useState('User / omp agent');
24
40
  const [instructions, setInstructions] = useState('');
41
+ const [hidden, setHidden] = useState(false);
25
42
  const [savedToast, setSavedToast] = useState(false);
26
43
  const [showLineNumbers, setShowLineNumbers] = useState(true);
27
44
 
45
+ // A skill the chamber does not own (a Claude plugin, a registry package) is
46
+ // shown read-only: its file belongs to another tool, and a save here would
47
+ // rewrite it. `managed` is absent on a brand-new draft, which is editable.
48
+ const readOnly = !isCreatingNew && skill != null && skill.managed !== true;
49
+
28
50
  useEffect(() => {
29
51
  if (isCreatingNew || !skill) {
30
52
  setName('new-skill');
31
53
  setDescription('');
32
- setLocation('user');
33
- setLocationLabel('User / OpenCode');
54
+ setLocation(defaultLocation);
55
+ setLocationLabel(defaultLocation === 'project' ? 'Project / .omp/skills' : 'User / omp agent');
34
56
  setInstructions(`---
35
57
  description: ""
36
58
  ---
37
59
  `);
60
+ setHidden(false);
38
61
  } else {
39
62
  setName(skill.name);
40
63
  setDescription(skill.description);
41
64
  setLocation(skill.location);
42
- setLocationLabel(skill.locationLabel || 'User / OpenCode');
65
+ setLocationLabel(skill.locationLabel || 'User / omp agent');
43
66
  setInstructions(skill.instructions);
67
+ setHidden(skill.hidden === true);
44
68
  }
45
- }, [skill, isCreatingNew]);
69
+ }, [skill, isCreatingNew, defaultLocation]);
46
70
 
47
71
  const handleSave = () => {
48
72
  onSave({
@@ -51,6 +75,7 @@ description: ""
51
75
  location,
52
76
  locationLabel,
53
77
  instructions,
78
+ hidden,
54
79
  });
55
80
  setSavedToast(true);
56
81
  setTimeout(() => setSavedToast(false), 2000);
@@ -68,22 +93,44 @@ description: ""
68
93
  {isCreatingNew ? 'New Skill' : name}
69
94
  </h2>
70
95
  <p className="text-xs text-ink/60 mt-0.5">
71
- {isCreatingNew ? 'Configure a new skill' : 'Configure skill details and prompt rules'}
96
+ {isCreatingNew ? 'Configure a new skill' : `Configure skill details and prompt rules · ${locationLabel}`}
72
97
  </p>
73
98
  </div>
74
99
 
75
- {onOpenCatalog && (
76
- <button
77
- type="button"
78
- onClick={onOpenCatalog}
79
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-ink/20 hover:border-ink/40 text-xs font-medium text-ink hover:bg-ink/5 transition-colors cursor-pointer"
80
- >
81
- <Sparkles size={14} className="text-ink/70" />
82
- <span>Browse Catalog</span>
83
- </button>
84
- )}
100
+ <div className="flex items-center gap-2">
101
+ {onReload && (
102
+ <button
103
+ type="button"
104
+ onClick={onReload}
105
+ disabled={isReloading}
106
+ title="Re-discover skills in the running agents"
107
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-ink/20 hover:border-ink/40 text-xs font-medium text-ink hover:bg-ink/5 transition-colors cursor-pointer disabled:opacity-50"
108
+ >
109
+ <RefreshCw size={14} className={isReloading ? 'animate-spin text-ink/70' : 'text-ink/70'} />
110
+ <span>Reload</span>
111
+ </button>
112
+ )}
113
+ {onOpenCatalog && (
114
+ <button
115
+ type="button"
116
+ onClick={onOpenCatalog}
117
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-ink/20 hover:border-ink/40 text-xs font-medium text-ink hover:bg-ink/5 transition-colors cursor-pointer"
118
+ >
119
+ <Sparkles size={14} className="text-ink/70" />
120
+ <span>Browse Catalog</span>
121
+ </button>
122
+ )}
123
+ </div>
85
124
  </div>
86
125
 
126
+ {readOnly && (
127
+ <div className="rounded-lg border border-ink/15 bg-ink/5 px-3 py-2 text-[11px] text-ink/70 leading-relaxed">
128
+ This skill is provided by <span className="font-mono text-ink/80">{skill?.source}</span> and is read-only here.
129
+ Edit <span className="font-mono text-ink/80">{skill?.filePath}</span> at its source, or install a copy into
130
+ {' '}<span className="font-mono text-ink/80">~/.omp/agent/skills</span> to own it.
131
+ </div>
132
+ )}
133
+
87
134
  {/* Basic Information Section */}
88
135
  <div className="space-y-4">
89
136
  <h3 className="text-xs font-bold uppercase tracking-wider text-ink">
@@ -99,7 +146,8 @@ description: ""
99
146
  <div className="group relative">
100
147
  <Info size={13} className="text-ink/40 cursor-help" />
101
148
  <div className="absolute left-0 top-5 hidden group-hover:block w-64 p-2 rounded-lg bg-ink text-paper text-[11px] shadow-lg z-20 pointer-events-none leading-relaxed">
102
- Unique identifier and runtime execution scope for this skill.
149
+ Unique identifier and runtime execution scope for this skill. A project skill is written under
150
+ {' '}<span className="font-mono">&lt;workspace&gt;/.omp/skills</span>.
103
151
  </div>
104
152
  </div>
105
153
  </div>
@@ -109,14 +157,42 @@ description: ""
109
157
  type="text"
110
158
  value={name}
111
159
  onChange={(e) => setName(e.currentTarget.value)}
160
+ disabled={readOnly}
112
161
  placeholder="skill-name"
113
- className="flex-1 px-3 py-2 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink text-xs font-mono text-ink transition-colors"
162
+ className="flex-1 px-3 py-2 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink text-xs font-mono text-ink transition-colors disabled:opacity-60"
114
163
  />
115
164
  <div className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-ink/20 bg-ink/5 text-xs text-ink/80 flex-shrink-0">
116
165
  <User size={14} className="text-ink/60" />
117
166
  <span>{locationLabel}</span>
118
167
  </div>
119
168
  </div>
169
+
170
+ {/* Scope selector: only a chamber-managed skill can move roots, and
171
+ the project scope needs a workspace to land in. */}
172
+ {!readOnly && (
173
+ <div className="flex items-center gap-2 mt-2">
174
+ {(['user', 'project'] as const).map((scope) => {
175
+ const disabled = scope === 'project' && projectScopeDisabled;
176
+ return (
177
+ <button
178
+ key={scope}
179
+ type="button"
180
+ disabled={disabled}
181
+ title={disabled ? 'Select a workspace folder to write a project skill' : undefined}
182
+ onClick={() => {
183
+ setLocation(scope);
184
+ setLocationLabel(scope === 'user' ? 'User / omp agent' : 'Project / .omp/skills');
185
+ }}
186
+ className={`px-2.5 py-1 rounded-md text-[11px] font-medium transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed ${
187
+ location === scope ? 'bg-ink text-paper' : 'border border-ink/20 text-ink/70 hover:bg-ink/5'
188
+ }`}
189
+ >
190
+ {scope === 'user' ? 'User scope' : 'Project scope'}
191
+ </button>
192
+ );
193
+ })}
194
+ </div>
195
+ )}
120
196
  </div>
121
197
 
122
198
  {/* Description */}
@@ -128,7 +204,8 @@ description: ""
128
204
  <div className="group relative">
129
205
  <Info size={13} className="text-ink/40 cursor-help" />
130
206
  <div className="absolute left-0 top-5 hidden group-hover:block w-64 p-2 rounded-lg bg-ink text-paper text-[11px] shadow-lg z-20 pointer-events-none leading-relaxed">
131
- Short description helping autonomous agents decide when to invoke this skill.
207
+ Short description helping autonomous agents decide when to invoke this skill. Required — omp does not
208
+ load a SKILL.md without one.
132
209
  </div>
133
210
  </div>
134
211
  </div>
@@ -137,10 +214,29 @@ description: ""
137
214
  rows={3}
138
215
  value={description}
139
216
  onChange={(e) => setDescription(e.currentTarget.value)}
217
+ disabled={readOnly}
140
218
  placeholder="Brief description of what this skill does..."
141
- className="w-full px-3 py-2 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink text-xs text-ink transition-colors resize-none leading-relaxed"
219
+ className="w-full px-3 py-2 rounded-lg border border-ink/20 hover:border-ink/40 bg-paper focus:outline-none focus:border-ink text-xs text-ink transition-colors resize-none leading-relaxed disabled:opacity-60"
142
220
  />
143
221
  </div>
222
+
223
+ {/* Model invocation toggle */}
224
+ <label className="flex items-start gap-2 cursor-pointer select-none">
225
+ <input
226
+ type="checkbox"
227
+ checked={hidden}
228
+ disabled={readOnly}
229
+ onChange={(e) => setHidden(e.currentTarget.checked)}
230
+ className="mt-0.5 accent-ink cursor-pointer"
231
+ />
232
+ <span className="text-xs text-ink/80 leading-relaxed">
233
+ Hide from the model&apos;s skill listing
234
+ <span className="block text-[11px] text-ink/50">
235
+ The skill stays runnable via <span className="font-mono">/skill:{name || 'name'}</span>; it is only kept out
236
+ of the automatic skill catalog the model sees.
237
+ </span>
238
+ </span>
239
+ </label>
144
240
  </div>
145
241
 
146
242
  {/* Instructions Section */}
@@ -173,8 +269,9 @@ description: ""
173
269
  rows={Math.max(lineCount, 8)}
174
270
  value={instructions}
175
271
  onChange={(e) => setInstructions(e.currentTarget.value)}
272
+ disabled={readOnly}
176
273
  spellcheck={false}
177
- className="flex-1 p-3 bg-transparent text-ink font-mono text-xs leading-relaxed resize-none focus:outline-none overflow-y-auto"
274
+ className="flex-1 p-3 bg-transparent text-ink font-mono text-xs leading-relaxed resize-none focus:outline-none overflow-y-auto disabled:opacity-60"
178
275
  placeholder="Type markdown instructions..."
179
276
  />
180
277
  </div>
@@ -183,7 +280,7 @@ description: ""
183
280
 
184
281
  {/* Footer Actions */}
185
282
  <div className="pt-4 border-t border-ink/10 flex items-center justify-between">
186
- {!isCreatingNew && skill && onDelete ? (
283
+ {!isCreatingNew && skill && onDelete && !readOnly ? (
187
284
  <button
188
285
  type="button"
189
286
  onClick={() => onDelete(skill.id)}
@@ -196,14 +293,16 @@ description: ""
196
293
  <div />
197
294
  )}
198
295
 
199
- <button
200
- type="button"
201
- onClick={handleSave}
202
- className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-ink text-paper text-xs font-medium hover:bg-ink/90 transition-colors shadow-xs cursor-pointer"
203
- >
204
- {savedToast ? <Check size={14} /> : <Save size={14} />}
205
- <span>{savedToast ? 'Saved' : isCreatingNew ? 'Create Skill' : 'Save Changes'}</span>
206
- </button>
296
+ {!readOnly && (
297
+ <button
298
+ type="button"
299
+ onClick={handleSave}
300
+ className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-ink text-paper text-xs font-medium hover:bg-ink/90 transition-colors shadow-xs cursor-pointer"
301
+ >
302
+ {savedToast ? <Check size={14} /> : <Save size={14} />}
303
+ <span>{savedToast ? 'Saved' : isCreatingNew ? 'Create Skill' : 'Save Changes'}</span>
304
+ </button>
305
+ )}
207
306
  </div>
208
307
  </div>
209
308
  );