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.
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +357 -353
- package/dist/client/static/js/index-7n838fz5.css +1 -0
- package/dist/client/static/js/{index-w0cem7n5.js → index-qp2sfmn2.js} +591 -592
- package/package.json +1 -1
- package/src/cli/lib/commands/status.js +7 -0
- package/src/client/components/layout/session-sidebar/about-modal/UpdateLog.tsx +50 -0
- package/src/client/components/layout/session-sidebar/about-modal/UpdateSection.tsx +12 -17
- package/src/client/components/settings/ProjectSelectorDropdown.tsx +8 -2
- package/src/client/components/settings/SettingsSidebarList.tsx +5 -1
- package/src/client/components/settings/categories/CommandSettings.tsx +47 -9
- package/src/client/components/settings/categories/SkillCatalogSettings.tsx +23 -43
- package/src/client/components/settings/categories/SkillSettings.tsx +51 -7
- package/src/client/components/settings/categories/command-settings/DetailPane.tsx +109 -103
- package/src/client/components/settings/categories/command-settings/SidebarList.tsx +6 -1
- package/src/client/components/settings/categories/skill-settings/DetailPane.tsx +130 -31
- package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +9 -3
- package/src/client/data/settings/command.ts +0 -40
- package/src/client/hooks/settings/workspace-roots.ts +99 -0
- package/src/client/hooks/ui/updates/index.ts +178 -0
- package/src/server/db.server.ts +41 -11
- package/src/server/index.ts +17 -1
- package/src/server/lib/db/client.ts +17 -0
- package/src/server/lib/db/compact-chat-overlay.test.ts +121 -0
- package/src/server/lib/db/compact-chat-overlay.ts +90 -0
- package/src/server/lib/lifecycle/fd-pressure.test.ts +66 -0
- package/src/server/lib/lifecycle/fd-pressure.ts +159 -0
- package/src/server/lib/omp/config/commands.test.ts +167 -0
- package/src/server/lib/omp/config/commands.ts +195 -0
- package/src/server/lib/omp/config/scope.test.ts +49 -0
- package/src/server/lib/omp/config/scope.ts +65 -0
- package/src/server/lib/omp/config/skills-catalog.ts +35 -3
- package/src/server/lib/omp/config/skills.test.ts +105 -0
- package/src/server/lib/omp/config/skills.ts +260 -47
- package/src/server/lib/omp/rpc/process.ts +22 -14
- package/src/server/lib/omp/rpc/session-registry.ts +6 -0
- package/src/server/lib/omp/rpc/utility.ts +45 -23
- package/src/server/lib/omp/session/merge-stored-anchor.test.ts +85 -0
- package/src/server/lib/omp/session/merge-stored-overlay-rows.test.ts +106 -0
- package/src/server/lib/omp/session/merge-stored.ts +78 -11
- package/src/server/lib/omp/session/reload.server.test.ts +84 -0
- package/src/server/lib/omp/session/reload.server.ts +47 -0
- package/src/server/lib/omp/session/subagent-presence.ts +40 -9
- package/src/server/lib/terminal/liveness.server.ts +65 -0
- package/src/server/lib/terminal/runtime.server.ts +70 -117
- package/src/server/lib/terminal/session.server.ts +5 -2
- package/src/server/lib/terminal/store.ts +72 -0
- package/src/server/lib/updates/apply.ts +62 -33
- package/src/server/lib/updates/install.ts +9 -2
- package/src/server/lib/updates/omp.ts +37 -9
- package/src/server/lib/updates/single-flight.test.ts +61 -0
- package/src/server/lib/updates/single-flight.ts +68 -0
- package/src/server/routes/chat/session.ts +13 -3
- package/src/server/routes/health/probe.ts +8 -0
- package/src/server/routes/omp/index.ts +0 -2
- package/src/server/routes/settings/commands.ts +102 -15
- package/src/server/routes/settings/skills.ts +165 -113
- package/src/server/routes/terminal/sessions.ts +1 -1
- package/src/server/routes/updates/apply.ts +101 -14
- package/src/shared/lib/chat/composer/client.ts +27 -18
- package/src/shared/lib/chat/composer/scope.test.ts +87 -0
- package/src/shared/lib/markdown/katex.ts +11 -1
- package/src/shared/lib/markdown/marked.ts +68 -13
- package/src/shared/lib/markdown/math.test.ts +53 -1
- package/src/shared/lib/updates/progress.test.ts +54 -0
- package/src/shared/lib/updates/progress.ts +50 -0
- package/src/shared/types/settings/command.ts +15 -2
- package/src/shared/types/settings/skill.ts +13 -0
- package/dist/client/static/js/index-vx0ernrv.css +0 -1
- package/src/client/hooks/ui/updates.ts +0 -108
- 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 {
|
|
4
|
-
import type {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 [
|
|
38
|
+
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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={
|
|
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={
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
{!
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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="
|
|
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 /
|
|
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(
|
|
33
|
-
setLocationLabel('User /
|
|
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 /
|
|
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' :
|
|
96
|
+
{isCreatingNew ? 'Configure a new skill' : `Configure skill details and prompt rules · ${locationLabel}`}
|
|
72
97
|
</p>
|
|
73
98
|
</div>
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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"><workspace>/.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'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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
);
|