ompchamber 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +387 -383
  3. package/dist/client/static/js/index-7n838fz5.css +1 -0
  4. package/dist/client/static/js/{index-h05z90mw.js → index-qp2sfmn2.js} +614 -615
  5. package/package.json +1 -1
  6. package/src/cli/lib/commands/status.js +7 -0
  7. package/src/client/components/common/session-actions-menu/index.tsx +201 -0
  8. package/src/client/components/common/session-delete-modal/index.tsx +128 -0
  9. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +18 -0
  10. package/src/client/components/layout/session-sidebar/SessionItem.tsx +109 -99
  11. package/src/client/components/layout/session-sidebar/about-modal/UpdateLog.tsx +50 -0
  12. package/src/client/components/layout/session-sidebar/about-modal/UpdateSection.tsx +12 -17
  13. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +18 -0
  14. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +82 -64
  15. package/src/client/components/settings/ProjectSelectorDropdown.tsx +8 -2
  16. package/src/client/components/settings/SettingsSidebarList.tsx +5 -1
  17. package/src/client/components/settings/categories/CommandSettings.tsx +47 -9
  18. package/src/client/components/settings/categories/SkillCatalogSettings.tsx +23 -43
  19. package/src/client/components/settings/categories/SkillSettings.tsx +51 -7
  20. package/src/client/components/settings/categories/command-settings/DetailPane.tsx +109 -103
  21. package/src/client/components/settings/categories/command-settings/SidebarList.tsx +6 -1
  22. package/src/client/components/settings/categories/skill-settings/DetailPane.tsx +130 -31
  23. package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +9 -3
  24. package/src/client/data/settings/command.ts +0 -40
  25. package/src/client/hooks/settings/workspace-roots.ts +99 -0
  26. package/src/client/hooks/ui/updates/index.ts +178 -0
  27. package/src/client/hooks/workspace/session-delete.ts +94 -0
  28. package/src/server/db.server.ts +41 -11
  29. package/src/server/index.ts +17 -1
  30. package/src/server/lib/btw/purge.server.ts +56 -0
  31. package/src/server/lib/db/client.ts +17 -0
  32. package/src/server/lib/db/compact-chat-overlay.test.ts +121 -0
  33. package/src/server/lib/db/compact-chat-overlay.ts +90 -0
  34. package/src/server/lib/fs/path-segment.test.ts +100 -0
  35. package/src/server/lib/fs/path-segment.ts +45 -0
  36. package/src/server/lib/lifecycle/fd-pressure.test.ts +66 -0
  37. package/src/server/lib/lifecycle/fd-pressure.ts +159 -0
  38. package/src/server/lib/omp/config/commands.test.ts +167 -0
  39. package/src/server/lib/omp/config/commands.ts +195 -0
  40. package/src/server/lib/omp/config/scope.test.ts +49 -0
  41. package/src/server/lib/omp/config/scope.ts +65 -0
  42. package/src/server/lib/omp/config/skills-catalog.ts +35 -3
  43. package/src/server/lib/omp/config/skills.test.ts +105 -0
  44. package/src/server/lib/omp/config/skills.ts +260 -47
  45. package/src/server/lib/omp/rpc/process.ts +22 -14
  46. package/src/server/lib/omp/rpc/session-registry.ts +6 -0
  47. package/src/server/lib/omp/rpc/utility.ts +45 -23
  48. package/src/server/lib/omp/session/delete.server.ts +136 -0
  49. package/src/server/lib/omp/session/merge-stored-anchor.test.ts +85 -0
  50. package/src/server/lib/omp/session/merge-stored-overlay-rows.test.ts +106 -0
  51. package/src/server/lib/omp/session/merge-stored.ts +78 -11
  52. package/src/server/lib/omp/session/reader.ts +13 -0
  53. package/src/server/lib/omp/session/reload.server.test.ts +84 -0
  54. package/src/server/lib/omp/session/reload.server.ts +47 -0
  55. package/src/server/lib/omp/session/subagent-presence.ts +40 -9
  56. package/src/server/lib/terminal/liveness.server.ts +65 -0
  57. package/src/server/lib/terminal/runtime.server.ts +70 -117
  58. package/src/server/lib/terminal/session.server.ts +5 -2
  59. package/src/server/lib/terminal/store.ts +72 -0
  60. package/src/server/lib/updates/apply.ts +62 -33
  61. package/src/server/lib/updates/install.ts +9 -2
  62. package/src/server/lib/updates/omp.ts +37 -9
  63. package/src/server/lib/updates/single-flight.test.ts +61 -0
  64. package/src/server/lib/updates/single-flight.ts +68 -0
  65. package/src/server/routes/chat/session.ts +13 -3
  66. package/src/server/routes/health/probe.ts +8 -0
  67. package/src/server/routes/omp/index.ts +0 -2
  68. package/src/server/routes/sessions/delete.test.ts +84 -0
  69. package/src/server/routes/sessions/delete.ts +83 -0
  70. package/src/server/routes/sessions/index.ts +5 -0
  71. package/src/server/routes/settings/commands.ts +102 -15
  72. package/src/server/routes/settings/skills.ts +165 -113
  73. package/src/server/routes/terminal/sessions.ts +1 -1
  74. package/src/server/routes/updates/apply.ts +101 -14
  75. package/src/shared/lib/chat/composer/client.ts +27 -18
  76. package/src/shared/lib/chat/composer/scope.test.ts +87 -0
  77. package/src/shared/lib/markdown/katex.ts +11 -1
  78. package/src/shared/lib/markdown/marked.ts +68 -13
  79. package/src/shared/lib/markdown/math.test.ts +53 -1
  80. package/src/shared/lib/updates/progress.test.ts +54 -0
  81. package/src/shared/lib/updates/progress.ts +50 -0
  82. package/src/shared/lib/workspace/session-state/store.ts +20 -0
  83. package/src/shared/types/settings/command.ts +15 -2
  84. package/src/shared/types/settings/skill.ts +13 -0
  85. package/dist/client/static/js/index-bsxxt89j.css +0 -1
  86. package/src/client/hooks/ui/updates.ts +0 -108
  87. package/src/server/routes/omp/commands.ts +0 -29
@@ -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 [selectedProjectId, setSelectedProjectId] = useState('ompchamber');
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
- location: skillData.location || 'user',
36
- locationLabel: skillData.locationLabel || 'User / OpenCode',
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
- project: selectedProjectId,
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 from database...</LoadingState>;
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={selectedProjectId}
71
- onSelectProject={setSelectedProjectId}
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 { Bot, Check, Copy, PenTool, Trash2 } from 'lucide-preact';
4
- import type { AgentItem, CommandItem, CommandScope } from '@/shared/types';
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
- const AVAILABLE_MODELS = [
14
- 'Not selected',
15
- 'claude-3-7-sonnet-20250219',
16
- 'claude-3-5-sonnet-20241022',
17
- 'claude-3-5-haiku-20241022',
18
- 'gpt-4o',
19
- 'gpt-4o-mini',
20
- 'gemini-2.5-pro-preview-03-25',
21
- 'deepseek-chat-v3',
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 [availableAgents, setAvailableAgents] = useState<AgentItem[]>([]);
38
+ const [showLineNumbers, setShowLineNumbers] = useState(true);
34
39
 
35
- useEffect(() => {
36
- fetch('/api/settings/agents')
37
- .then(res => res.json())
38
- .then(data => {
39
- if (data?.agents) setAvailableAgents(data.agents);
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
- {formData.isBuiltIn && (
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
- Built-in System Command
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={formData.isBuiltIn}
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={formData.isBuiltIn}
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
- {/* SECTION 2: Execution Context */}
146
- <div className="space-y-3">
147
- <h2 className="text-xs font-bold uppercase tracking-wider text-ink">
148
- Execution Context
149
- </h2>
150
-
151
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
152
- {/* Override Agent */}
153
- <div>
154
- <label className="block text-xs font-semibold text-ink/80 mb-1.5">
155
- Override Agent
156
- </label>
157
- <div className="flex items-center border border-ink/20 hover:border-ink/40 focus-within:border-ink rounded-lg bg-paper transition-colors">
158
- <span className="px-2.5 text-ink/40">
159
- <Bot className="w-3.5 h-3.5" />
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 3: Command Template */}
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
- 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"
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
- 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"
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
- 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"
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
- <textarea
252
- value={formData.template}
253
- onChange={(e) => setFormData({ ...formData, template: e.currentTarget.value })}
254
- rows={7}
255
- placeholder="Enter command template instructions..."
256
- className="w-full flex-1 min-h-[140px] text-xs font-mono p-3 rounded-lg border border-ink/20 hover:border-ink/40 bg-ink/5 focus:outline-none focus:border-ink leading-relaxed transition-colors"
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
- {!formData.isBuiltIn && !isNew && onDelete && (
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
- <button
283
- type="submit"
284
- 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"
285
- >
286
- {isNew ? 'Create Command' : 'Save Command'}
287
- </button>
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="Custom Commands"
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
  )}