ompchamber 3.4.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 (87) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +387 -383
  3. package/dist/client/static/js/index-7n838fz5.css +1 -0
  4. package/dist/client/static/js/{index-h05z90mw.js → index-qp2sfmn2.js} +614 -615
  5. package/package.json +1 -1
  6. package/src/cli/lib/commands/status.js +7 -0
  7. package/src/client/components/common/session-actions-menu/index.tsx +201 -0
  8. package/src/client/components/common/session-delete-modal/index.tsx +128 -0
  9. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +18 -0
  10. package/src/client/components/layout/session-sidebar/SessionItem.tsx +109 -99
  11. package/src/client/components/layout/session-sidebar/about-modal/UpdateLog.tsx +50 -0
  12. package/src/client/components/layout/session-sidebar/about-modal/UpdateSection.tsx +12 -17
  13. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +18 -0
  14. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +82 -64
  15. package/src/client/components/settings/ProjectSelectorDropdown.tsx +8 -2
  16. package/src/client/components/settings/SettingsSidebarList.tsx +5 -1
  17. package/src/client/components/settings/categories/CommandSettings.tsx +47 -9
  18. package/src/client/components/settings/categories/SkillCatalogSettings.tsx +23 -43
  19. package/src/client/components/settings/categories/SkillSettings.tsx +51 -7
  20. package/src/client/components/settings/categories/command-settings/DetailPane.tsx +109 -103
  21. package/src/client/components/settings/categories/command-settings/SidebarList.tsx +6 -1
  22. package/src/client/components/settings/categories/skill-settings/DetailPane.tsx +130 -31
  23. package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +9 -3
  24. package/src/client/data/settings/command.ts +0 -40
  25. package/src/client/hooks/settings/workspace-roots.ts +99 -0
  26. package/src/client/hooks/ui/updates/index.ts +178 -0
  27. package/src/client/hooks/workspace/session-delete.ts +94 -0
  28. package/src/server/db.server.ts +41 -11
  29. package/src/server/index.ts +17 -1
  30. package/src/server/lib/btw/purge.server.ts +56 -0
  31. package/src/server/lib/db/client.ts +17 -0
  32. package/src/server/lib/db/compact-chat-overlay.test.ts +121 -0
  33. package/src/server/lib/db/compact-chat-overlay.ts +90 -0
  34. package/src/server/lib/fs/path-segment.test.ts +100 -0
  35. package/src/server/lib/fs/path-segment.ts +45 -0
  36. package/src/server/lib/lifecycle/fd-pressure.test.ts +66 -0
  37. package/src/server/lib/lifecycle/fd-pressure.ts +159 -0
  38. package/src/server/lib/omp/config/commands.test.ts +167 -0
  39. package/src/server/lib/omp/config/commands.ts +195 -0
  40. package/src/server/lib/omp/config/scope.test.ts +49 -0
  41. package/src/server/lib/omp/config/scope.ts +65 -0
  42. package/src/server/lib/omp/config/skills-catalog.ts +35 -3
  43. package/src/server/lib/omp/config/skills.test.ts +105 -0
  44. package/src/server/lib/omp/config/skills.ts +260 -47
  45. package/src/server/lib/omp/rpc/process.ts +22 -14
  46. package/src/server/lib/omp/rpc/session-registry.ts +6 -0
  47. package/src/server/lib/omp/rpc/utility.ts +45 -23
  48. package/src/server/lib/omp/session/delete.server.ts +136 -0
  49. package/src/server/lib/omp/session/merge-stored-anchor.test.ts +85 -0
  50. package/src/server/lib/omp/session/merge-stored-overlay-rows.test.ts +106 -0
  51. package/src/server/lib/omp/session/merge-stored.ts +78 -11
  52. package/src/server/lib/omp/session/reader.ts +13 -0
  53. package/src/server/lib/omp/session/reload.server.test.ts +84 -0
  54. package/src/server/lib/omp/session/reload.server.ts +47 -0
  55. package/src/server/lib/omp/session/subagent-presence.ts +40 -9
  56. package/src/server/lib/terminal/liveness.server.ts +65 -0
  57. package/src/server/lib/terminal/runtime.server.ts +70 -117
  58. package/src/server/lib/terminal/session.server.ts +5 -2
  59. package/src/server/lib/terminal/store.ts +72 -0
  60. package/src/server/lib/updates/apply.ts +62 -33
  61. package/src/server/lib/updates/install.ts +9 -2
  62. package/src/server/lib/updates/omp.ts +37 -9
  63. package/src/server/lib/updates/single-flight.test.ts +61 -0
  64. package/src/server/lib/updates/single-flight.ts +68 -0
  65. package/src/server/routes/chat/session.ts +13 -3
  66. package/src/server/routes/health/probe.ts +8 -0
  67. package/src/server/routes/omp/index.ts +0 -2
  68. package/src/server/routes/sessions/delete.test.ts +84 -0
  69. package/src/server/routes/sessions/delete.ts +83 -0
  70. package/src/server/routes/sessions/index.ts +5 -0
  71. package/src/server/routes/settings/commands.ts +102 -15
  72. package/src/server/routes/settings/skills.ts +165 -113
  73. package/src/server/routes/terminal/sessions.ts +1 -1
  74. package/src/server/routes/updates/apply.ts +101 -14
  75. package/src/shared/lib/chat/composer/client.ts +27 -18
  76. package/src/shared/lib/chat/composer/scope.test.ts +87 -0
  77. package/src/shared/lib/markdown/katex.ts +11 -1
  78. package/src/shared/lib/markdown/marked.ts +68 -13
  79. package/src/shared/lib/markdown/math.test.ts +53 -1
  80. package/src/shared/lib/updates/progress.test.ts +54 -0
  81. package/src/shared/lib/updates/progress.ts +50 -0
  82. package/src/shared/lib/workspace/session-state/store.ts +20 -0
  83. package/src/shared/types/settings/command.ts +15 -2
  84. package/src/shared/types/settings/skill.ts +13 -0
  85. package/dist/client/static/js/index-bsxxt89j.css +0 -1
  86. package/src/client/hooks/ui/updates.ts +0 -108
  87. package/src/server/routes/omp/commands.ts +0 -29
@@ -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
  );
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'preact/hooks';
2
2
  import { ChevronDown, ChevronRight, Plus } from 'lucide-preact';
3
3
  import type { SkillItem } from '@/shared/types';
4
- import { ProjectSelectorDropdown } from '@/client/components/settings/ProjectSelectorDropdown';
4
+ import { ProjectSelectorDropdown, type ProjectOption } from '@/client/components/settings/ProjectSelectorDropdown';
5
5
 
6
6
  interface SkillSidebarListProps {
7
7
  skills: SkillItem[];
@@ -11,6 +11,8 @@ interface SkillSidebarListProps {
11
11
  onAddNewSkill: () => void;
12
12
  selectedProject: string;
13
13
  onSelectProject: (project: string) => void;
14
+ /** Real workspace folders; defaults to the shared placeholder list. */
15
+ projectOptions?: ProjectOption[];
14
16
  }
15
17
 
16
18
  export function SkillSidebarList({
@@ -21,6 +23,7 @@ export function SkillSidebarList({
21
23
  onAddNewSkill,
22
24
  selectedProject,
23
25
  onSelectProject,
26
+ projectOptions,
24
27
  }: SkillSidebarListProps) {
25
28
  const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
26
29
 
@@ -55,8 +58,9 @@ export function SkillSidebarList({
55
58
  variant="compact"
56
59
  selectedProject={selectedProject}
57
60
  onChangeProject={onSelectProject}
61
+ {...(projectOptions ? { options: projectOptions } : {})}
58
62
  getTriggerLabel={(options, value) =>
59
- options.find((option) => option.id === value)?.label || value
63
+ options.find((option) => option.id === value || option.value === value)?.label || value
60
64
  }
61
65
  />
62
66
 
@@ -77,8 +81,10 @@ export function SkillSidebarList({
77
81
 
78
82
  {/* Skills List */}
79
83
  <div className="flex-1 scrollbar-overlay-container scrollbar-overlay-static p-1.5 space-y-2">
84
+ {/* The picker above names the scope; the list is exactly that scope's
85
+ skills, so a second scope claim here would only be able to lie. */}
80
86
  <div className="px-2 pt-1 pb-0.5 text-[10px] font-bold text-ink/50 uppercase tracking-wider">
81
- USER SKILLS
87
+ SKILLS
82
88
  </div>
83
89
 
84
90
  {/* Temporary "New Skill" indicator item */}
@@ -7,8 +7,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
7
7
  name: 'init',
8
8
  description: 'guided AGENTS.md setup',
9
9
  scope: 'system',
10
- overrideAgent: 'Not selected',
11
- overrideModel: 'Not selected',
12
10
  template: `Initialize AGENTS.md instructions for this repository following guided prompts. Context: $ARGUMENTS`,
13
11
  isBuiltIn: true,
14
12
  },
@@ -17,8 +15,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
17
15
  name: 'review',
18
16
  description: 'review changes [commit|branch|working]',
19
17
  scope: 'system',
20
- overrideAgent: 'code-reviewer',
21
- overrideModel: 'Not selected',
22
18
  template: `Review the current changes in $ARGUMENTS and summarize bugs, formatting issues, and test considerations.\n!\`git diff --stat\``,
23
19
  isBuiltIn: true,
24
20
  },
@@ -28,8 +24,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
28
24
  name: 'debugging',
29
25
  description: 'MUST USE for any real runtime debugging session',
30
26
  scope: 'user',
31
- overrideAgent: 'coder',
32
- overrideModel: 'Not selected',
33
27
  template: `Analyze the runtime error log and isolate the root failure:\n!\`bun run test\`\nFix the issue and verify: $ARGUMENTS`,
34
28
  },
35
29
  {
@@ -37,8 +31,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
37
31
  name: 'frontend',
38
32
  description: 'MUST USE for frontend/web UI/UX development',
39
33
  scope: 'user',
40
- overrideAgent: 'frontend-specialist',
41
- overrideModel: 'Not selected',
42
34
  template: `Implement clean responsive UI component following Tailwind and theme variable specifications.\nTarget file: @$ARGUMENTS`,
43
35
  },
44
36
  {
@@ -46,8 +38,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
46
38
  name: 'git-master',
47
39
  description: 'MUST USE for ANY git operations and commit conventions',
48
40
  scope: 'user',
49
- overrideAgent: 'git-sync',
50
- overrideModel: 'Not selected',
51
41
  template: `Inspect git status and generate clean conventional commit message:\n!\`git status -s\`\nDetails: $ARGUMENTS`,
52
42
  },
53
43
  {
@@ -55,8 +45,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
55
45
  name: 'goal',
56
46
  description: '(builtin) Set, show, pause, resume session goal',
57
47
  scope: 'user',
58
- overrideAgent: 'Sisyphus - ultraworker',
59
- overrideModel: 'Not selected',
60
48
  template: `Manage current goal state: $ARGUMENTS`,
61
49
  },
62
50
  {
@@ -64,8 +52,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
64
52
  name: 'handoff',
65
53
  description: '(builtin) Create a detailed context handoff report',
66
54
  scope: 'user',
67
- overrideAgent: 'general',
68
- overrideModel: 'Not selected',
69
55
  template: `Summarize work in progress, architectural decisions, and open items for handoff to next agent session.\n$ARGUMENTS`,
70
56
  },
71
57
  {
@@ -73,8 +59,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
73
59
  name: 'hyperplan',
74
60
  description: '(builtin) Adversarial multi-agent plan builder',
75
61
  scope: 'user',
76
- overrideAgent: 'Prometheus - Plan Builder',
77
- overrideModel: 'Not selected',
78
62
  template: `Construct a detailed plan with review gates and adversarial critiques for $ARGUMENTS.`,
79
63
  },
80
64
  {
@@ -82,8 +66,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
82
66
  name: 'init-deep',
83
67
  description: '(builtin) Initialize hierarchical AGENTS.md setup',
84
68
  scope: 'user',
85
- overrideAgent: 'architect',
86
- overrideModel: 'Not selected',
87
69
  template: `Analyze directory structure and construct domain-specific AGENTS.md files for subpackages: $ARGUMENTS`,
88
70
  },
89
71
  {
@@ -91,8 +73,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
91
73
  name: 'playwright',
92
74
  description: 'MUST USE for any browser-related testing',
93
75
  scope: 'user',
94
- overrideAgent: 'e2e-runner',
95
- overrideModel: 'Not selected',
96
76
  template: `Run playwright tests for scenario: $ARGUMENTS\n!\`bunx playwright test\``,
97
77
  },
98
78
  {
@@ -100,8 +80,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
100
80
  name: 'pty-open-background-spy',
101
81
  description: 'Open PTY Sessions Web Interface',
102
82
  scope: 'user',
103
- overrideAgent: 'devops-runner',
104
- overrideModel: 'Not selected',
105
83
  template: `Launch PTY background session monitor with target: $ARGUMENTS`,
106
84
  },
107
85
  {
@@ -109,8 +87,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
109
87
  name: 'pty-show-server-url',
110
88
  description: 'Show PTY Sessions Web Interface URL',
111
89
  scope: 'user',
112
- overrideAgent: 'devops-runner',
113
- overrideModel: 'Not selected',
114
90
  template: `Output the active PTY session web socket endpoint and UI URL.`,
115
91
  },
116
92
  {
@@ -118,8 +94,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
118
94
  name: 'refactor',
119
95
  description: '(builtin) Intelligent refactoring coordinator',
120
96
  scope: 'user',
121
- overrideAgent: 'refactor-pro',
122
- overrideModel: 'Not selected',
123
97
  template: `Refactor module @$ARGUMENTS into decoupled single-responsibility files without changing public behavior. Keep files under 350 lines.`,
124
98
  },
125
99
  {
@@ -127,8 +101,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
127
101
  name: 'benchmark',
128
102
  description: 'Run performance benchmark suites and record timings',
129
103
  scope: 'user',
130
- overrideAgent: 'performance-tuner',
131
- overrideModel: 'Not selected',
132
104
  template: `Execute benchmark suite on target: $ARGUMENTS`,
133
105
  },
134
106
  {
@@ -136,8 +108,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
136
108
  name: 'bundle-analyze',
137
109
  description: 'Inspect vendor bundle size and dynamic chunk splitting',
138
110
  scope: 'user',
139
- overrideAgent: 'bundle-optimizer',
140
- overrideModel: 'Not selected',
141
111
  template: `Run build analyzer and report chunk weight statistics: !\`bun run build\``,
142
112
  },
143
113
  {
@@ -145,8 +115,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
145
115
  name: 'db-migrate',
146
116
  description: 'Review and execute database migrations safely',
147
117
  scope: 'user',
148
- overrideAgent: 'database-analyzer',
149
- overrideModel: 'Not selected',
150
118
  template: `Review pending schema migrations and verify backward compatibility: $ARGUMENTS`,
151
119
  },
152
120
  {
@@ -154,8 +122,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
154
122
  name: 'doc-generate',
155
123
  description: 'Generate API documentation and usage guides',
156
124
  scope: 'user',
157
- overrideAgent: 'doc-writer',
158
- overrideModel: 'Not selected',
159
125
  template: `Draft comprehensive technical documentation for module @$ARGUMENTS`,
160
126
  },
161
127
  {
@@ -163,8 +129,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
163
129
  name: 'security-scan',
164
130
  description: 'Run static security analysis and check dependency vulnerabilities',
165
131
  scope: 'user',
166
- overrideAgent: 'security-auditor',
167
- overrideModel: 'Not selected',
168
132
  template: `Scan repository for leaked secrets and outdated dependencies: !\`npm audit\``,
169
133
  },
170
134
  {
@@ -172,8 +136,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
172
136
  name: 'test-unit',
173
137
  description: 'Run unit test suite with coverage report',
174
138
  scope: 'user',
175
- overrideAgent: 'test-engineer',
176
- overrideModel: 'Not selected',
177
139
  template: `Run test suite with coverage: !\`bun test\` - Focus: $ARGUMENTS`,
178
140
  },
179
141
  {
@@ -181,8 +143,6 @@ export const DEFAULT_COMMANDS_LIST: CommandItem[] = [
181
143
  name: 'typecheck',
182
144
  description: 'Run strict TypeScript compiler verification',
183
145
  scope: 'user',
184
- overrideAgent: 'lint-fixer',
185
- overrideModel: 'Not selected',
186
146
  template: `Run strict typecheck without emitting files: !\`bunx tsc --noEmit\``,
187
147
  },
188
148
  ];
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { useEffect, useState } from 'preact/hooks';
7
+ import type { ProjectOption } from '@/client/components/settings/ProjectSelectorDropdown';
8
+
9
+ interface ProjectConfigItem {
10
+ id: string;
11
+ name: string;
12
+ path?: string;
13
+ }
14
+
15
+ export interface WorkspaceRoots {
16
+ options: ProjectOption[];
17
+ /** Workspace root for a selected option id, or null for the user scope. */
18
+ rootFor: (optionId: string) => string | null;
19
+ /** Query string (with leading `?`) for a read scoped to this option. */
20
+ queryFor: (optionId: string) => string;
21
+ /** Extra body fields scoping a write to this option. */
22
+ scopeBodyFor: (optionId: string) => { root?: string; scope?: string };
23
+ /** True when this option is a real workspace (the project scope exists). */
24
+ isWorkspace: (optionId: string) => boolean;
25
+ }
26
+
27
+ /** Option id for the user-level (no workspace) scope. */
28
+ export const GLOBAL_SCOPE_ID = 'global';
29
+
30
+ const USER_OPTION: ProjectOption = {
31
+ id: GLOBAL_SCOPE_ID,
32
+ name: 'global',
33
+ label: 'User (all projects)',
34
+ value: '',
35
+ };
36
+
37
+ /**
38
+ * The workspace folders the skills/commands panels can be scoped to.
39
+ *
40
+ * These are the real registered folders (`/api/settings/projects`), not the
41
+ * hardcoded placeholder list the panels used to offer: a panel scoped to a
42
+ * made-up project name sent a root the server could not resolve, so the picker
43
+ * silently did nothing. The `value` of each option is the folder's project
44
+ * path, which is what the API takes as `?root=`.
45
+ *
46
+ * The USER scope is `?scope=user` rather than "no root": omp resolves
47
+ * project-scope skills and commands from its process cwd, so an unscoped read
48
+ * answered for whatever directory the server happened to run in. `scope=user`
49
+ * pins that read to the agent dir, which is the only cwd whose inventory is
50
+ * the user's own.
51
+ */
52
+ export function useWorkspaceRoots(): WorkspaceRoots {
53
+ const [options, setOptions] = useState<ProjectOption[]>([USER_OPTION]);
54
+
55
+ useEffect(() => {
56
+ let active = true;
57
+ fetch('/api/settings/projects')
58
+ .then((res) => res.json())
59
+ .then((data: { projects?: ProjectConfigItem[] }) => {
60
+ if (!active) return;
61
+ const folders = (data.projects ?? [])
62
+ .filter((project) => typeof project.path === 'string' && project.path)
63
+ .map((project) => ({
64
+ id: project.id,
65
+ name: project.name,
66
+ label: project.name,
67
+ value: project.path as string,
68
+ }));
69
+ setOptions([...folders, USER_OPTION]);
70
+ })
71
+ .catch((error) => console.error('Failed to load workspace folders:', error));
72
+ return () => {
73
+ active = false;
74
+ };
75
+ }, []);
76
+
77
+ // The dropdown reports the option's VALUE (a project path) while a panel may
78
+ // hold either that or the option id, so both resolve to the same root. An
79
+ // id-only lookup silently answered the user scope for every workspace.
80
+ const rootFor = (optionId: string): string | null => {
81
+ const option = options.find((candidate) => candidate.id === optionId || candidate.value === optionId);
82
+ const value = option?.value ?? '';
83
+ return value || null;
84
+ };
85
+
86
+ const queryFor = (optionId: string): string => {
87
+ const root = rootFor(optionId);
88
+ return root ? `?root=${encodeURIComponent(root)}` : '?scope=user';
89
+ };
90
+
91
+ const scopeBodyFor = (optionId: string): { root?: string; scope?: string } => {
92
+ const root = rootFor(optionId);
93
+ return root ? { root } : { scope: 'user' };
94
+ };
95
+
96
+ const isWorkspace = (optionId: string): boolean => rootFor(optionId) !== null;
97
+
98
+ return { options, rootFor, queryFor, scopeBodyFor, isWorkspace };
99
+ }