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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
|
|
@@ -47,6 +47,13 @@ function printInstance(entry) {
|
|
|
47
47
|
log(` launch: ${entry.launchMode ?? 'unknown'}${entry.source ? ` (${entry.source})` : ''}`);
|
|
48
48
|
log(` uptime: ${formatUptime(entry.startedAt)}`);
|
|
49
49
|
log(` health: ${formatHealth(entry.health)}`);
|
|
50
|
+
// Descriptor pressure, when the server can measure it. A dev server climbs
|
|
51
|
+
// toward 10,240 by holding the client module graph across rebuilds, and past
|
|
52
|
+
// that point every `Bun.spawn` in the process fails at once (the terminal
|
|
53
|
+
// shell, the omp child, git) — this line is the warning that arrives before
|
|
54
|
+
// the breakage instead of after it.
|
|
55
|
+
const fds = entry.health?.fds;
|
|
56
|
+
if (fds) log(` fds: ${fds.open}/${fds.limit}${fds.nearCliff ? ' (near the spawn limit — restart this instance)' : ''}`);
|
|
50
57
|
log(` log: ${entry.logFile}`);
|
|
51
58
|
}
|
|
52
59
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { Loader2, Terminal } from 'lucide-preact';
|
|
3
|
+
import { updateLogText, type UpdateLogState } from '@/shared/lib/updates/progress';
|
|
4
|
+
|
|
5
|
+
interface UpdateLogProps {
|
|
6
|
+
state: UpdateLogState;
|
|
7
|
+
/** A run is in flight: the label says so and the tail keeps following it. */
|
|
8
|
+
active: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The update's own output, as it arrives.
|
|
13
|
+
*
|
|
14
|
+
* A live install prints the commands it runs and what they answered, and that
|
|
15
|
+
* is the only honest progress signal available — there is no percentage to
|
|
16
|
+
* report. It follows the tail while the run is active so the newest line stays
|
|
17
|
+
* on screen, and stays readable afterwards, because the failure that matters
|
|
18
|
+
* explains itself in the last few lines.
|
|
19
|
+
*/
|
|
20
|
+
export function UpdateLog({ state, active }: UpdateLogProps) {
|
|
21
|
+
const scroller = useRef<HTMLPreElement>(null);
|
|
22
|
+
const text = updateLogText(state);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const element = scroller.current;
|
|
26
|
+
if (element) element.scrollTop = element.scrollHeight;
|
|
27
|
+
}, [text]);
|
|
28
|
+
|
|
29
|
+
if (!text && !active) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="mt-2.5">
|
|
33
|
+
<div className="mb-1 flex items-center gap-1.5 text-[10px] text-ink/50">
|
|
34
|
+
{active ? (
|
|
35
|
+
<Loader2 size={11} className="animate-spin motion-reduce:animate-none" />
|
|
36
|
+
) : (
|
|
37
|
+
<Terminal size={11} />
|
|
38
|
+
)}
|
|
39
|
+
<span>{active ? 'Updating…' : 'Update output'}</span>
|
|
40
|
+
{state.truncated && <span className="text-ink/35">· older lines trimmed</span>}
|
|
41
|
+
</div>
|
|
42
|
+
<pre
|
|
43
|
+
ref={scroller}
|
|
44
|
+
className="max-h-40 overflow-auto rounded-md border border-ink/10 bg-canvas px-2 py-1.5 font-mono text-[10px] leading-4 whitespace-pre-wrap break-words text-ink/70 select-text"
|
|
45
|
+
>
|
|
46
|
+
{text || '…'}
|
|
47
|
+
</pre>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AlertCircle, Check, Download, Loader2, RefreshCw } from 'lucide-preact';
|
|
2
2
|
import type { UpdateTarget, UpdateTargetInfo } from '@/shared/types/updates';
|
|
3
3
|
import type { UseUpdatesResult } from '@/client/hooks/ui/updates';
|
|
4
|
+
import { UpdateLog } from '@/client/components/layout/session-sidebar/about-modal/UpdateLog';
|
|
4
5
|
|
|
5
6
|
interface UpdateSectionProps {
|
|
6
7
|
updates: UseUpdatesResult;
|
|
@@ -62,23 +63,13 @@ export function UpdateSection({ updates, onToast }: UpdateSectionProps) {
|
|
|
62
63
|
const result = await updates.apply(target);
|
|
63
64
|
if (!onToast) return;
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// the command to run — action required, not a success.
|
|
73
|
-
onToast(result.message, result.success ? 'success' : 'error');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (result.success) {
|
|
78
|
-
onToast(`${result.message} A restart may be needed.`, 'success');
|
|
79
|
-
} else {
|
|
80
|
-
onToast(result.message, 'error');
|
|
81
|
-
}
|
|
66
|
+
// A null result means the run was aborted with the panel (an unmount, a
|
|
67
|
+
// page tear-down): there is nobody left to tell.
|
|
68
|
+
if (!result) return;
|
|
69
|
+
|
|
70
|
+
// `manual` means the environment cannot self-update and the message is the
|
|
71
|
+
// command to run — action required, not a success.
|
|
72
|
+
onToast(result.message, result.success && !result.manual ? 'success' : 'error');
|
|
82
73
|
};
|
|
83
74
|
|
|
84
75
|
const showResult = Boolean(updates.info && !updates.checking && !updates.error);
|
|
@@ -90,6 +81,10 @@ export function UpdateSection({ updates, onToast }: UpdateSectionProps) {
|
|
|
90
81
|
{updates.info && <span className="font-mono text-[9px] text-ink/40">checked</span>}
|
|
91
82
|
</div>
|
|
92
83
|
|
|
84
|
+
{updates.progress && (
|
|
85
|
+
<UpdateLog state={updates.progress} active={updates.applying !== null} />
|
|
86
|
+
)}
|
|
87
|
+
|
|
93
88
|
<div className="mt-2.5">
|
|
94
89
|
{updates.checking && (
|
|
95
90
|
<button type="button" disabled className={`${actionClassName} w-full justify-center`}>
|
|
@@ -22,8 +22,14 @@ export const PROJECT_OPTIONS: ProjectOption[] = [
|
|
|
22
22
|
|
|
23
23
|
const optionValue = (option: ProjectOption): string => option.value ?? option.id;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Trigger label for a selected value. A caller that stores the option's VALUE
|
|
27
|
+
* (the skills/commands panels store a project path, not an id) must still get
|
|
28
|
+
* the option's human label rather than the raw path it selected.
|
|
29
|
+
*/
|
|
30
|
+
const defaultTriggerLabel = (options: ProjectOption[], selectedProject: string): string =>
|
|
31
|
+
options.find((option) => option.id === selectedProject || optionValue(option) === selectedProject)?.name
|
|
32
|
+
?? selectedProject;
|
|
27
33
|
|
|
28
34
|
interface ProjectSelectorDropdownProps {
|
|
29
35
|
selectedProject: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
2
|
import type { FunctionComponent } from 'preact/compat';
|
|
3
3
|
import { Plus } from 'lucide-preact';
|
|
4
|
-
import { ProjectSelectorDropdown } from '@/client/components/settings/ProjectSelectorDropdown';
|
|
4
|
+
import { ProjectSelectorDropdown, type ProjectOption } from '@/client/components/settings/ProjectSelectorDropdown';
|
|
5
5
|
|
|
6
6
|
export interface SettingsSidebarItem {
|
|
7
7
|
id: string;
|
|
@@ -24,6 +24,8 @@ interface SettingsSidebarListProps {
|
|
|
24
24
|
renderIcon: (isBuiltIn: boolean) => ComponentChildren;
|
|
25
25
|
selectedProject: string;
|
|
26
26
|
onChangeProject: (project: string) => void;
|
|
27
|
+
/** Real workspace folders; defaults to the shared placeholder list. */
|
|
28
|
+
projectOptions?: ProjectOption[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -43,6 +45,7 @@ export const SettingsSidebarList: FunctionComponent<SettingsSidebarListProps> =
|
|
|
43
45
|
renderIcon,
|
|
44
46
|
selectedProject,
|
|
45
47
|
onChangeProject,
|
|
48
|
+
projectOptions,
|
|
46
49
|
}) => {
|
|
47
50
|
const builtInItems = items.filter((item) => item.isBuiltIn);
|
|
48
51
|
const customItems = items.filter((item) => !item.isBuiltIn);
|
|
@@ -81,6 +84,7 @@ export const SettingsSidebarList: FunctionComponent<SettingsSidebarListProps> =
|
|
|
81
84
|
<ProjectSelectorDropdown
|
|
82
85
|
selectedProject={selectedProject}
|
|
83
86
|
onChangeProject={onChangeProject}
|
|
87
|
+
{...(projectOptions ? { options: projectOptions } : {})}
|
|
84
88
|
/>
|
|
85
89
|
|
|
86
90
|
<div className="px-3.5 py-2.5 border-b border-ink/10 flex items-center justify-between">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'preact/hooks';
|
|
1
|
+
import { useRef, useState } from 'preact/hooks';
|
|
2
2
|
import type { FunctionComponent } from 'preact/compat';
|
|
3
3
|
import type { CommandItem, SettingsState } from '@/shared/types';
|
|
4
4
|
import { CommandSidebarList } from '@/client/components/settings/categories/command-settings/SidebarList';
|
|
@@ -7,18 +7,40 @@ import { LoadingState } from '@/client/components/settings/LoadingState';
|
|
|
7
7
|
import { useCrudList } from '@/client/hooks/settings/crud-list';
|
|
8
8
|
import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
|
|
9
9
|
import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
|
|
10
|
+
import { GLOBAL_SCOPE_ID, useWorkspaceRoots } from '@/client/hooks/settings/workspace-roots';
|
|
10
11
|
|
|
11
12
|
interface CommandSettingsProps {
|
|
12
13
|
settings: SettingsState;
|
|
13
14
|
onUpdate: (settings: SettingsState) => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The Commands panel, scoped to one root at a time.
|
|
19
|
+
*
|
|
20
|
+
* The list is omp's own `get_available_commands` for the selected scope: the
|
|
21
|
+
* builtins, the `skill:<name>` entries, extension and TypeScript commands, and
|
|
22
|
+
* every markdown command file omp discovers — the user-level
|
|
23
|
+
* `~/.omp/agent/commands`, `~/.claude/commands`, … plus the selected
|
|
24
|
+
* workspace's `.omp/commands`, `.claude/commands`, `.agents/commands` and
|
|
25
|
+
* `.github/prompts`. The server resolves each live name against those roots so
|
|
26
|
+
* the pane can tell a file command (editable, and the file is the truth) from
|
|
27
|
+
* a builtin (read-only), and reports the scope each one lives at.
|
|
28
|
+
*/
|
|
16
29
|
export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
|
|
17
30
|
const masterDetail = useSettingsMasterDetail();
|
|
18
|
-
const
|
|
31
|
+
const roots = useWorkspaceRoots();
|
|
32
|
+
const [selectedRootId, setSelectedRootId] = useState<string>(GLOBAL_SCOPE_ID);
|
|
33
|
+
// The name a command had when it was selected: a rename has to travel so the
|
|
34
|
+
// file is moved rather than left behind as a second command. Read from a ref
|
|
35
|
+
// because the save body is built outside the list controller's own state.
|
|
36
|
+
const selectedNameRef = useRef<string | null>(null);
|
|
37
|
+
const query = roots.queryFor(selectedRootId);
|
|
38
|
+
const isWorkspaceScope = roots.isWorkspace(selectedRootId);
|
|
39
|
+
|
|
19
40
|
const { items: commands, selectedId, selected: selectedCommand, isCreatingNew, isLoading, select, startCreate, save, remove } =
|
|
20
41
|
useCrudList<CommandItem>({
|
|
21
42
|
endpoint: '/api/settings/commands',
|
|
43
|
+
query,
|
|
22
44
|
listKey: 'commands',
|
|
23
45
|
bodyKey: 'command',
|
|
24
46
|
messages: {
|
|
@@ -27,7 +49,20 @@ export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
|
|
|
27
49
|
delete: 'Failed to delete command via API:',
|
|
28
50
|
},
|
|
29
51
|
cacheKey: 'command',
|
|
30
|
-
|
|
52
|
+
// A live command's id is derived from its name; the rename has to travel
|
|
53
|
+
// so the file is moved rather than duplicated.
|
|
54
|
+
buildBody: (target) => ({
|
|
55
|
+
command: target,
|
|
56
|
+
...roots.scopeBodyFor(selectedRootId),
|
|
57
|
+
...(selectedNameRef.current ? { previousName: selectedNameRef.current } : {}),
|
|
58
|
+
}),
|
|
59
|
+
buildDeleteQuery: (id) => `?id=${encodeURIComponent(id)}&${query.slice(1)}`,
|
|
60
|
+
buildNew: (updated) => ({
|
|
61
|
+
...updated,
|
|
62
|
+
id: `cmd-${Date.now()}`,
|
|
63
|
+
isBuiltIn: false,
|
|
64
|
+
scope: isWorkspaceScope ? 'project' : 'user',
|
|
65
|
+
}),
|
|
31
66
|
buildUpdate: (updated) => updated,
|
|
32
67
|
isDeleteError: (data) => Boolean((data as { error?: string } | null)?.error),
|
|
33
68
|
});
|
|
@@ -36,15 +71,13 @@ export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
|
|
|
36
71
|
id: `new-${Date.now()}`,
|
|
37
72
|
name: 'new-command',
|
|
38
73
|
description: '',
|
|
39
|
-
scope: 'user',
|
|
40
|
-
overrideAgent: 'Not selected',
|
|
41
|
-
overrideModel: 'Not selected',
|
|
74
|
+
scope: isWorkspaceScope ? 'project' : 'user',
|
|
42
75
|
template: 'Execute task: $ARGUMENTS',
|
|
43
76
|
isBuiltIn: false,
|
|
44
77
|
};
|
|
45
78
|
|
|
46
79
|
if (isLoading) {
|
|
47
|
-
return <LoadingState>Loading commands
|
|
80
|
+
return <LoadingState>Loading commands…</LoadingState>;
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
return (
|
|
@@ -58,15 +91,18 @@ export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
|
|
|
58
91
|
commands={commands}
|
|
59
92
|
selectedCommandId={isCreatingNew ? null : selectedId}
|
|
60
93
|
onSelectCommand={(id) => {
|
|
94
|
+
selectedNameRef.current = commands.find((command) => command.id === id)?.name ?? null;
|
|
61
95
|
select(id);
|
|
62
96
|
masterDetail.openDetail();
|
|
63
97
|
}}
|
|
64
98
|
onAddNewCommand={() => {
|
|
99
|
+
selectedNameRef.current = null;
|
|
65
100
|
startCreate();
|
|
66
101
|
masterDetail.openDetail();
|
|
67
102
|
}}
|
|
68
|
-
selectedProject={
|
|
69
|
-
onChangeProject={
|
|
103
|
+
selectedProject={selectedRootId}
|
|
104
|
+
onChangeProject={setSelectedRootId}
|
|
105
|
+
projectOptions={roots.options}
|
|
70
106
|
/>
|
|
71
107
|
}
|
|
72
108
|
detail={
|
|
@@ -76,6 +112,7 @@ export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
|
|
|
76
112
|
command={emptyCommandTemplate}
|
|
77
113
|
isNew={true}
|
|
78
114
|
onSave={save}
|
|
115
|
+
projectScopeDisabled={!isWorkspaceScope}
|
|
79
116
|
/>
|
|
80
117
|
) : selectedCommand ? (
|
|
81
118
|
<CommandDetailPane
|
|
@@ -84,6 +121,7 @@ export const CommandSettings: FunctionComponent<CommandSettingsProps> = () => {
|
|
|
84
121
|
isNew={false}
|
|
85
122
|
onSave={save}
|
|
86
123
|
onDelete={remove}
|
|
124
|
+
projectScopeDisabled={!isWorkspaceScope}
|
|
87
125
|
/>
|
|
88
126
|
) : (
|
|
89
127
|
<div className="flex-1 flex items-center justify-center text-xs font-mono text-ink/40">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
2
|
import { Check, Download, ExternalLink, GitBranch, Plus, RefreshCw, Search, Star } from 'lucide-preact';
|
|
3
|
-
import type { CatalogSkillItem, SkillCatalogSource
|
|
3
|
+
import type { CatalogSkillItem, SkillCatalogSource } from '@/shared/types';
|
|
4
4
|
import { AddSourceModal } from '@/client/components/settings/categories/skill-catalog/AddSourceModal';
|
|
5
5
|
import { invalidateComposerCache } from '@/shared/lib/chat/composer/client';
|
|
6
6
|
import { useSkillCatalog } from '@/client/hooks/settings/skill-catalog';
|
|
@@ -31,49 +31,29 @@ export function SkillCatalogSettings() {
|
|
|
31
31
|
const handleInstallToggle = (skill: CatalogSkillItem) => {
|
|
32
32
|
const existing = userSkills.find((s) => s.name === skill.name);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
showToast(`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
id: `skill-cat-${Date.now()}`,
|
|
53
|
-
name: skill.name,
|
|
54
|
-
description: skill.description,
|
|
55
|
-
location: 'user',
|
|
56
|
-
locationLabel: 'User / OpenCode',
|
|
57
|
-
instructions: skill.instructions || `---\ndescription: "${skill.description}"\n---\n\n## Instructions\nFollow standard prompt protocol for ${skill.name}.`,
|
|
58
|
-
project: 'ompchamber',
|
|
59
|
-
isInstalledFromCatalog: true,
|
|
60
|
-
catalogSource: skill.sourceId,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
fetch('/api/settings/skills', {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: { 'Content-Type': 'application/json' },
|
|
66
|
-
body: JSON.stringify({ skill: newSkill }),
|
|
34
|
+
// Both directions go through the same endpoint: an install runs the skills
|
|
35
|
+
// CLI (which is what puts the skill where omp reads it), an uninstall runs
|
|
36
|
+
// its remove counterpart. Deleting only the chamber's own record used to
|
|
37
|
+
// leave the skill installed and running.
|
|
38
|
+
fetch('/api/settings/skills', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({ type: 'install_toggle', skill, install: !existing }),
|
|
42
|
+
})
|
|
43
|
+
.then((res) => res.json())
|
|
44
|
+
.then((data) => {
|
|
45
|
+
if (data?.error) {
|
|
46
|
+
showToast(`Failed: ${data.error}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(data?.skills)) setUserSkills(data.skills);
|
|
50
|
+
invalidateComposerCache('command');
|
|
51
|
+
showToast(existing ? `Uninstalled skill "${skill.name}"` : `Installed skill "${skill.name}"`);
|
|
67
52
|
})
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
showToast(`Installed skill "${skill.name}"`);
|
|
73
|
-
invalidateComposerCache('command');
|
|
74
|
-
})
|
|
75
|
-
.catch(console.error);
|
|
76
|
-
}
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
console.error(error);
|
|
55
|
+
showToast(`Failed to ${existing ? 'uninstall' : 'install'} "${skill.name}"`);
|
|
56
|
+
});
|
|
77
57
|
};
|
|
78
58
|
|
|
79
59
|
const handleAddSource = (newSource: SkillCatalogSource) => {
|
|
@@ -6,6 +6,7 @@ import { LoadingState } from '@/client/components/settings/LoadingState';
|
|
|
6
6
|
import { useCrudList } from '@/client/hooks/settings/crud-list';
|
|
7
7
|
import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
|
|
8
8
|
import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
|
|
9
|
+
import { GLOBAL_SCOPE_ID, useWorkspaceRoots } from '@/client/hooks/settings/workspace-roots';
|
|
9
10
|
|
|
10
11
|
interface SkillSettingsProps {
|
|
11
12
|
settings?: SettingsState;
|
|
@@ -13,12 +14,31 @@ interface SkillSettingsProps {
|
|
|
13
14
|
onNavigateToCatalog?: () => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The Skills panel, scoped to one root at a time.
|
|
19
|
+
*
|
|
20
|
+
* The picker selects WHICH omp scope is read: a workspace root answers with the
|
|
21
|
+
* skills an omp session started there loads (its `.omp/skills`, `.claude`,
|
|
22
|
+
* `.agents`, `.github`, `.codex` entries plus the user-level ones), while the
|
|
23
|
+
* user scope answers for the agent dir alone. That is not a client-side filter
|
|
24
|
+
* — the server passes the scope's cwd to `omp skill list`, because omp's
|
|
25
|
+
* project walk-up is what decides the list.
|
|
26
|
+
*
|
|
27
|
+
* A write lands in the scope that is selected: a project skill under
|
|
28
|
+
* `<workspace>/.omp/skills`, a user skill under `~/.omp/agent/skills`.
|
|
29
|
+
*/
|
|
16
30
|
export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
|
|
17
31
|
const masterDetail = useSettingsMasterDetail();
|
|
18
|
-
const
|
|
32
|
+
const roots = useWorkspaceRoots();
|
|
33
|
+
const [selectedRootId, setSelectedRootId] = useState<string>(GLOBAL_SCOPE_ID);
|
|
34
|
+
const [isReloading, setIsReloading] = useState(false);
|
|
35
|
+
const query = roots.queryFor(selectedRootId);
|
|
36
|
+
const isWorkspaceScope = roots.isWorkspace(selectedRootId);
|
|
37
|
+
|
|
19
38
|
const { items: skills, selectedId, selected: selectedSkill, isCreatingNew, isLoading, select, startCreate, save, remove } =
|
|
20
39
|
useCrudList<SkillItem, Partial<SkillItem>>({
|
|
21
40
|
endpoint: '/api/settings/skills',
|
|
41
|
+
query,
|
|
22
42
|
listKey: 'skills',
|
|
23
43
|
bodyKey: 'skill',
|
|
24
44
|
messages: {
|
|
@@ -28,14 +48,22 @@ export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
|
|
|
28
48
|
},
|
|
29
49
|
cacheKey: 'command',
|
|
30
50
|
fallbackToFirst: false,
|
|
51
|
+
// The write carries the scope it was made in, because the draft's own
|
|
52
|
+
// location decides which `.omp/skills` root it lands in.
|
|
53
|
+
buildBody: (target) => ({ skill: target, ...roots.scopeBodyFor(selectedRootId) }),
|
|
54
|
+
buildDeleteQuery: (id) => `?id=${encodeURIComponent(id)}&${query.slice(1)}`,
|
|
31
55
|
buildNew: (skillData) => ({
|
|
32
56
|
id: `skill-${Date.now()}`,
|
|
33
57
|
name: skillData.name || 'new-skill',
|
|
34
58
|
description: skillData.description || '',
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
// A new skill defaults to the scope the panel is showing: creating a
|
|
60
|
+
// project skill while the user scope is selected would write outside
|
|
61
|
+
// the list they are looking at.
|
|
62
|
+
location: skillData.location || (isWorkspaceScope ? 'project' : 'user'),
|
|
63
|
+
locationLabel: skillData.locationLabel || (isWorkspaceScope ? 'Project / .omp/skills' : 'User / omp agent'),
|
|
37
64
|
instructions: skillData.instructions || '',
|
|
38
|
-
|
|
65
|
+
hidden: skillData.hidden === true,
|
|
66
|
+
project: 'omp',
|
|
39
67
|
}),
|
|
40
68
|
buildUpdate: (skillData, selected, selectedId) => ({
|
|
41
69
|
...(selected || { id: selectedId || `skill-${Date.now()}` }),
|
|
@@ -44,8 +72,19 @@ export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
|
|
|
44
72
|
isDeleteError: (data) => Boolean((data as { error?: string } | null)?.error),
|
|
45
73
|
});
|
|
46
74
|
|
|
75
|
+
const reload = () => {
|
|
76
|
+
setIsReloading(true);
|
|
77
|
+
fetch('/api/settings/skills', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ type: 'reload', ...roots.scopeBodyFor(selectedRootId) }),
|
|
81
|
+
})
|
|
82
|
+
.catch((error) => console.error('Failed to reload plugins:', error))
|
|
83
|
+
.finally(() => setIsReloading(false));
|
|
84
|
+
};
|
|
85
|
+
|
|
47
86
|
if (isLoading) {
|
|
48
|
-
return <LoadingState>Loading skills
|
|
87
|
+
return <LoadingState>Loading skills…</LoadingState>;
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
return (
|
|
@@ -67,8 +106,9 @@ export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
|
|
|
67
106
|
startCreate();
|
|
68
107
|
masterDetail.openDetail();
|
|
69
108
|
}}
|
|
70
|
-
selectedProject={
|
|
71
|
-
onSelectProject={
|
|
109
|
+
selectedProject={selectedRootId}
|
|
110
|
+
onSelectProject={setSelectedRootId}
|
|
111
|
+
projectOptions={roots.options}
|
|
72
112
|
/>
|
|
73
113
|
}
|
|
74
114
|
detail={
|
|
@@ -78,6 +118,10 @@ export function SkillSettings({ onNavigateToCatalog }: SkillSettingsProps) {
|
|
|
78
118
|
onSave={save}
|
|
79
119
|
onDelete={remove}
|
|
80
120
|
onOpenCatalog={onNavigateToCatalog}
|
|
121
|
+
onReload={reload}
|
|
122
|
+
isReloading={isReloading}
|
|
123
|
+
projectScopeDisabled={!isWorkspaceScope}
|
|
124
|
+
defaultLocation={isWorkspaceScope ? 'project' : 'user'}
|
|
81
125
|
/>
|
|
82
126
|
}
|
|
83
127
|
/>
|