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.
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +387 -383
- package/dist/client/static/js/index-7n838fz5.css +1 -0
- package/dist/client/static/js/{index-h05z90mw.js → index-qp2sfmn2.js} +614 -615
- package/package.json +1 -1
- package/src/cli/lib/commands/status.js +7 -0
- package/src/client/components/common/session-actions-menu/index.tsx +201 -0
- package/src/client/components/common/session-delete-modal/index.tsx +128 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +18 -0
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +109 -99
- 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/mobile/mobile-session-sidebar/Item.tsx +18 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +82 -64
- 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/client/hooks/workspace/session-delete.ts +94 -0
- package/src/server/db.server.ts +41 -11
- package/src/server/index.ts +17 -1
- package/src/server/lib/btw/purge.server.ts +56 -0
- 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/fs/path-segment.test.ts +100 -0
- package/src/server/lib/fs/path-segment.ts +45 -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/delete.server.ts +136 -0
- 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/reader.ts +13 -0
- 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/sessions/delete.test.ts +84 -0
- package/src/server/routes/sessions/delete.ts +83 -0
- package/src/server/routes/sessions/index.ts +5 -0
- 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/lib/workspace/session-state/store.ts +20 -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-bsxxt89j.css +0 -1
- package/src/client/hooks/ui/updates.ts +0 -108
- package/src/server/routes/omp/commands.ts +0 -29
|
@@ -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`}>
|
|
@@ -7,6 +7,8 @@ import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-optio
|
|
|
7
7
|
import { useShowMore } from '@/client/hooks/ui/show-more';
|
|
8
8
|
import { useExpandedSessions } from '@/client/hooks/workspace/expanded-sessions';
|
|
9
9
|
import { useWorkspaceFolderActions } from '@/client/hooks/workspace/workspace-folder-actions';
|
|
10
|
+
import { useSessionDelete } from '@/client/hooks/workspace/session-delete';
|
|
11
|
+
import { SessionDeleteModal } from '@/client/components/common/session-delete-modal';
|
|
10
12
|
import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
|
|
11
13
|
import { relativeTimeAgo } from '@/shared/lib/workspace/relative-time';
|
|
12
14
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
@@ -53,6 +55,7 @@ export function MobileSessionCategory({
|
|
|
53
55
|
handleArchive,
|
|
54
56
|
handleRename,
|
|
55
57
|
} = useWorkspaceFolderActions(folder, refresh);
|
|
58
|
+
const sessionDelete = useSessionDelete();
|
|
56
59
|
|
|
57
60
|
useOnClickOutside(menuRef, () => {
|
|
58
61
|
setShowMenu(false);
|
|
@@ -195,6 +198,13 @@ export function MobileSessionCategory({
|
|
|
195
198
|
onToggleExpand={() => toggleSession(sessionKey)}
|
|
196
199
|
onSelect={() => onSelectSession(session.id)}
|
|
197
200
|
onArchive={() => handleArchive(session)}
|
|
201
|
+
onDelete={
|
|
202
|
+
// A pending `new-…` chat has no transcript anywhere yet —
|
|
203
|
+
// there is nothing to delete, and the server refuses it.
|
|
204
|
+
sessionKey.startsWith('new-')
|
|
205
|
+
? undefined
|
|
206
|
+
: () => sessionDelete.requestDelete(session)
|
|
207
|
+
}
|
|
198
208
|
onRename={sessionKey.startsWith('new-') ? undefined : (name) => void handleRename(session, name)}
|
|
199
209
|
/>
|
|
200
210
|
{isRosterOpen && (
|
|
@@ -216,6 +226,14 @@ export function MobileSessionCategory({
|
|
|
216
226
|
)}
|
|
217
227
|
</div>
|
|
218
228
|
)}
|
|
229
|
+
|
|
230
|
+
<SessionDeleteModal
|
|
231
|
+
session={sessionDelete.pending}
|
|
232
|
+
isDeleting={sessionDelete.isDeleting}
|
|
233
|
+
error={sessionDelete.error}
|
|
234
|
+
onClose={sessionDelete.cancelDelete}
|
|
235
|
+
onConfirm={() => void sessionDelete.confirmDelete()}
|
|
236
|
+
/>
|
|
219
237
|
</div>
|
|
220
238
|
);
|
|
221
239
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Check, ChevronDown, ChevronRight, CircleQuestionMark, Loader2, MoreHorizontal } from 'lucide-preact';
|
|
2
2
|
import type { SessionItemData } from '@/shared/types';
|
|
3
3
|
import { useInlineRename } from '@/client/hooks/ui/inline-rename';
|
|
4
|
+
import { SessionActionsMenu, useRowMenu } from '@/client/components/common/session-actions-menu';
|
|
4
5
|
|
|
5
6
|
export interface MobileSessionRowProps {
|
|
6
7
|
session: SessionItemData;
|
|
@@ -16,6 +17,9 @@ export interface MobileSessionRowProps {
|
|
|
16
17
|
onToggleExpand?: () => void;
|
|
17
18
|
onSelect: () => void;
|
|
18
19
|
onArchive: () => void;
|
|
20
|
+
/** Omitted for a session that cannot be deleted yet (a pending `new-…` chat,
|
|
21
|
+
* which has no transcript anywhere). */
|
|
22
|
+
onDelete?: () => void;
|
|
19
23
|
onRename?: (name: string) => void;
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -30,6 +34,7 @@ export function MobileSessionRow({
|
|
|
30
34
|
onToggleExpand,
|
|
31
35
|
onSelect,
|
|
32
36
|
onArchive,
|
|
37
|
+
onDelete,
|
|
33
38
|
onRename,
|
|
34
39
|
}: MobileSessionRowProps) {
|
|
35
40
|
const {
|
|
@@ -41,6 +46,9 @@ export function MobileSessionRow({
|
|
|
41
46
|
handleKeyDown,
|
|
42
47
|
handleBlur,
|
|
43
48
|
} = useInlineRename(session.title, onRename);
|
|
49
|
+
// One trigger for every row action; also reachable by long-press contextmenu.
|
|
50
|
+
const menu = useRowMenu();
|
|
51
|
+
const hasActions = Boolean(onRename || onArchive || onDelete);
|
|
44
52
|
|
|
45
53
|
// The roster toggle is the row's LAST element, so its glyph lands in the same
|
|
46
54
|
// column as the folder header's expand chevron — one vertical line of
|
|
@@ -71,76 +79,86 @@ export function MobileSessionRow({
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
return (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
type="button"
|
|
81
|
-
onClick={onSelect}
|
|
82
|
-
className="flex-1 text-left pl-2 pr-3 py-2 flex items-center justify-between min-w-0"
|
|
82
|
+
<>
|
|
83
|
+
<div
|
|
84
|
+
onContextMenu={hasActions ? menu.openAtCursor : undefined}
|
|
85
|
+
className={`w-full rounded-lg flex items-center transition-colors ${
|
|
86
|
+
isActive ? 'bg-ink/10 font-medium text-ink' : 'hover:bg-ink/5 text-ink/85'
|
|
87
|
+
}`}
|
|
83
88
|
>
|
|
84
|
-
<div className="flex items-center min-w-0 pr-2">
|
|
85
|
-
{/* Status slot, 16px at the same x as the folder header's icon slot,
|
|
86
|
-
so the left column reads as one line. Waiting on an answer
|
|
87
|
-
outranks the run spinner — the spinner says work is happening,
|
|
88
|
-
the question mark says it is YOUR turn (desktop parity). */}
|
|
89
|
-
<span className="w-4 flex-shrink-0 flex items-center justify-center">
|
|
90
|
-
{awaitingInput ? (
|
|
91
|
-
<CircleQuestionMark size={13} className="text-ink/70 animate-pulse" />
|
|
92
|
-
) : status === 'stream' ? (
|
|
93
|
-
<Loader2 size={13} className="text-ink/50 animate-spin" />
|
|
94
|
-
) : status ? (
|
|
95
|
-
<Check size={13} className="text-ink/50" />
|
|
96
|
-
) : null}
|
|
97
|
-
</span>
|
|
98
|
-
<span className="text-xs truncate leading-snug ml-2">{title}</span>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<span className="text-[11px] text-ink/45 font-mono flex-shrink-0 ml-2">{timeAgo ?? ''}</span>
|
|
102
|
-
</button>
|
|
103
|
-
|
|
104
|
-
{onRename && (
|
|
105
89
|
<button
|
|
106
90
|
type="button"
|
|
107
|
-
onClick={
|
|
108
|
-
|
|
109
|
-
className="flex-shrink-0 p-2 text-ink/35 hover:text-ink rounded-lg cursor-pointer"
|
|
91
|
+
onClick={onSelect}
|
|
92
|
+
className="flex-1 text-left pl-2 pr-3 py-2 flex items-center justify-between min-w-0"
|
|
110
93
|
>
|
|
111
|
-
<
|
|
94
|
+
<div className="flex items-center min-w-0 pr-2">
|
|
95
|
+
{/* Status slot, 16px at the same x as the folder header's icon slot,
|
|
96
|
+
so the left column reads as one line. Waiting on an answer
|
|
97
|
+
outranks the run spinner — the spinner says work is happening,
|
|
98
|
+
the question mark says it is YOUR turn (desktop parity). */}
|
|
99
|
+
<span className="w-4 flex-shrink-0 flex items-center justify-center">
|
|
100
|
+
{awaitingInput ? (
|
|
101
|
+
<CircleQuestionMark size={13} className="text-ink/70 animate-pulse" />
|
|
102
|
+
) : status === 'stream' ? (
|
|
103
|
+
<Loader2 size={13} className="text-ink/50 animate-spin" />
|
|
104
|
+
) : status ? (
|
|
105
|
+
<Check size={13} className="text-ink/50" />
|
|
106
|
+
) : null}
|
|
107
|
+
</span>
|
|
108
|
+
<span className="text-xs truncate leading-snug ml-2">{title}</span>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<span className="text-[11px] text-ink/45 font-mono flex-shrink-0 ml-2">{timeAgo ?? ''}</span>
|
|
112
112
|
</button>
|
|
113
|
-
)}
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
{/* One trigger for rename / archive / delete, always visible: a touch
|
|
115
|
+
surface has no hover, and the always-on action buttons this replaced
|
|
116
|
+
consumed a third of a phone's row width.
|
|
117
|
+
`mr-1` only when the row has NO roster toggle — it reserves that
|
|
118
|
+
column so the trailing glyph's centre stays 19px from the row's right
|
|
119
|
+
edge whether the last element is this trigger or the chevron. Without
|
|
120
|
+
it the glyphs step in and out from row to row. */}
|
|
121
|
+
{hasActions && (
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={menu.openBelow}
|
|
125
|
+
title="Session actions"
|
|
126
|
+
aria-label="Session actions"
|
|
127
|
+
aria-haspopup="menu"
|
|
128
|
+
aria-expanded={menu.anchor !== null}
|
|
129
|
+
className={`flex-shrink-0 p-2 text-ink/35 hover:text-ink rounded-lg cursor-pointer ${showChevron ? '' : 'mr-1'}`}
|
|
130
|
+
>
|
|
131
|
+
<MoreHorizontal size={16} />
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
{/* Last element, so its glyph sits in the folder header's chevron column.
|
|
136
|
+
`p-1.5 mr-1.5` centres a 14px glyph on that column while keeping a
|
|
137
|
+
touch-sized tap target. */}
|
|
138
|
+
{showChevron && (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={onToggleExpand}
|
|
142
|
+
title={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
|
|
143
|
+
aria-expanded={isExpanded}
|
|
144
|
+
aria-label={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
|
|
145
|
+
className="flex-shrink-0 p-1.5 mr-1.5 flex items-center justify-center text-ink/50 hover:text-ink rounded-lg cursor-pointer"
|
|
146
|
+
>
|
|
147
|
+
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{menu.anchor && (
|
|
153
|
+
<SessionActionsMenu
|
|
154
|
+
anchor={menu.anchor}
|
|
155
|
+
isArchived={session.is_archived === 1}
|
|
156
|
+
onRename={onRename ? startRename : undefined}
|
|
157
|
+
onArchive={onArchive}
|
|
158
|
+
onDelete={onDelete}
|
|
159
|
+
onClose={menu.close}
|
|
160
|
+
/>
|
|
143
161
|
)}
|
|
144
|
-
|
|
162
|
+
</>
|
|
145
163
|
);
|
|
146
164
|
}
|
|
@@ -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) => {
|