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
|
@@ -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
|
/>
|
|
@@ -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
|
)}
|