tycono 0.3.13-beta.6 → 0.3.13
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/bin/cli.js +1 -2
- package/package.json +1 -1
- package/src/api/src/create-app.ts +2 -0
- package/src/api/src/routes/execute.ts +9 -6
- package/src/api/src/routes/presets.ts +35 -0
- package/src/api/src/services/preset-loader.ts +149 -0
- package/src/shared/types.ts +78 -0
- package/src/tui/api.ts +17 -0
- package/src/tui/components/PanelMode.tsx +0 -15
- package/src/tui/hooks/useCommand.ts +26 -2
package/bin/cli.js
CHANGED
|
@@ -10,9 +10,8 @@ if (!process.env.__TYCONO_HEAP_SET && !process.execArgv.some(a => a.includes('ma
|
|
|
10
10
|
process.env.__TYCONO_HEAP_SET = '1';
|
|
11
11
|
try {
|
|
12
12
|
execFileSync(process.execPath, [
|
|
13
|
-
'--max-old-space-size=
|
|
13
|
+
'--max-old-space-size=8192',
|
|
14
14
|
'--expose-gc',
|
|
15
|
-
'--heapsnapshot-near-heap-limit=1',
|
|
16
15
|
...process.execArgv,
|
|
17
16
|
fileURLToPath(import.meta.url),
|
|
18
17
|
...process.argv.slice(2),
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import { engineRouter } from './routes/engine.js';
|
|
|
17
17
|
import { sessionsRouter } from './routes/sessions.js';
|
|
18
18
|
import { setupRouter } from './routes/setup.js';
|
|
19
19
|
import { skillsRouter } from './routes/skills.js';
|
|
20
|
+
import { presetsRouter } from './routes/presets.js';
|
|
20
21
|
|
|
21
22
|
export function createApp() {
|
|
22
23
|
const app = express();
|
|
@@ -53,6 +54,7 @@ export function createApp() {
|
|
|
53
54
|
app.use('/api/engine', engineRouter);
|
|
54
55
|
app.use('/api/sessions', sessionsRouter);
|
|
55
56
|
app.use('/api/skills', skillsRouter);
|
|
57
|
+
app.use('/api/presets', presetsRouter);
|
|
56
58
|
|
|
57
59
|
app.get('/api/health', (_req, res) => {
|
|
58
60
|
res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
|
|
@@ -27,6 +27,9 @@ executionManager.onExecutionCreated((exec) => {
|
|
|
27
27
|
waveMultiplexer.onExecutionCreated(exec);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
// OOM fix: wave recovery runs once, not on every 5s poll
|
|
31
|
+
let waveRecoveryDone = false;
|
|
32
|
+
|
|
30
33
|
/* ─── Runner — lazy, re-created when engine changes ── */
|
|
31
34
|
|
|
32
35
|
function getRunner() {
|
|
@@ -50,15 +53,15 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
50
53
|
|
|
51
54
|
// ── /api/waves/active — restore active waves after refresh ──
|
|
52
55
|
if (method === 'GET' && url === '/api/waves/active') {
|
|
53
|
-
// Recovery: rebuild wave→session mapping from session-store
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
if (
|
|
56
|
+
// Recovery: rebuild wave→session mapping from session-store (ONE TIME ONLY)
|
|
57
|
+
// Previous bug: recovery ran on EVERY poll (5s) because getActiveWaves()
|
|
58
|
+
// returns empty for done executions → recovery loop → OOM
|
|
59
|
+
if (!waveRecoveryDone) {
|
|
60
|
+
waveRecoveryDone = true;
|
|
57
61
|
const allSessions = listSessions();
|
|
58
62
|
let recovered = 0;
|
|
59
63
|
for (const ses of allSessions) {
|
|
60
64
|
if (!ses.waveId) continue;
|
|
61
|
-
// Only recover CEO sessions for wave display (team sessions loaded on demand)
|
|
62
65
|
if (ses.roleId !== 'ceo') continue;
|
|
63
66
|
const exec = executionManager.getActiveExecution(ses.id);
|
|
64
67
|
if (exec) {
|
|
@@ -67,7 +70,7 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
if (recovered > 0) {
|
|
70
|
-
console.log(`[WaveRecovery] Recovered ${recovered}
|
|
73
|
+
console.log(`[WaveRecovery] Recovered ${recovered} sessions (one-time)`);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presets.ts — Preset API routes
|
|
3
|
+
*
|
|
4
|
+
* GET /api/presets — list all preset summaries
|
|
5
|
+
* GET /api/presets/:id — get full preset detail
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
9
|
+
import { getPresetSummaries, getPresetById } from '../services/preset-loader.js';
|
|
10
|
+
|
|
11
|
+
export const presetsRouter = Router();
|
|
12
|
+
|
|
13
|
+
/** GET /api/presets — list preset summaries for TUI */
|
|
14
|
+
presetsRouter.get('/', (_req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const summaries = getPresetSummaries(COMPANY_ROOT);
|
|
17
|
+
res.json(summaries);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
res.status(500).json({ error: 'Failed to load presets' });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** GET /api/presets/:id — get full preset detail */
|
|
24
|
+
presetsRouter.get('/:id', (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const preset = getPresetById(COMPANY_ROOT, req.params.id);
|
|
27
|
+
if (!preset) {
|
|
28
|
+
res.status(404).json({ error: `Preset not found: ${req.params.id}` });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
res.json(preset.definition);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
res.status(500).json({ error: 'Failed to load preset' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset-loader.ts — Load presets from company/presets/
|
|
3
|
+
*
|
|
4
|
+
* Scans company/presets/ for:
|
|
5
|
+
* - _default.yaml (auto-generated from existing roles/)
|
|
6
|
+
* - {name}/preset.yaml (installed presets with roles/skills/knowledge)
|
|
7
|
+
*
|
|
8
|
+
* Returns PresetSummary[] for TUI display and full LoadedPreset for wave creation.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import type { PresetDefinition, LoadedPreset, PresetSummary } from '../../../shared/types.js';
|
|
14
|
+
|
|
15
|
+
const PRESETS_DIR = 'company/presets';
|
|
16
|
+
const DEFAULT_PRESET_FILE = '_default.yaml';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a default preset definition from existing roles/ directory.
|
|
20
|
+
* This is generated on-the-fly — no need to persist _default.yaml.
|
|
21
|
+
*/
|
|
22
|
+
function buildDefaultPreset(companyRoot: string): LoadedPreset {
|
|
23
|
+
const rolesDir = path.join(companyRoot, 'roles');
|
|
24
|
+
const roles: string[] = [];
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(rolesDir)) {
|
|
27
|
+
const entries = fs.readdirSync(rolesDir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.isDirectory()) continue;
|
|
30
|
+
const yamlPath = path.join(rolesDir, entry.name, 'role.yaml');
|
|
31
|
+
if (fs.existsSync(yamlPath)) {
|
|
32
|
+
roles.push(entry.name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
definition: {
|
|
39
|
+
spec: 'preset/v1',
|
|
40
|
+
id: 'default',
|
|
41
|
+
name: 'Default Team',
|
|
42
|
+
tagline: 'Your current team',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
roles,
|
|
45
|
+
},
|
|
46
|
+
path: null,
|
|
47
|
+
isDefault: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load a single preset from a directory containing preset.yaml.
|
|
53
|
+
*/
|
|
54
|
+
function loadPresetFromDir(presetDir: string): LoadedPreset | null {
|
|
55
|
+
const yamlPath = path.join(presetDir, 'preset.yaml');
|
|
56
|
+
if (!fs.existsSync(yamlPath)) return null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as PresetDefinition;
|
|
60
|
+
if (!raw.id || !raw.name || !Array.isArray(raw.roles)) return null;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
definition: {
|
|
64
|
+
spec: raw.spec || 'preset/v1',
|
|
65
|
+
id: raw.id,
|
|
66
|
+
name: raw.name,
|
|
67
|
+
tagline: raw.tagline,
|
|
68
|
+
version: raw.version || '1.0.0',
|
|
69
|
+
description: raw.description,
|
|
70
|
+
author: raw.author,
|
|
71
|
+
category: raw.category,
|
|
72
|
+
industry: raw.industry,
|
|
73
|
+
stage: raw.stage,
|
|
74
|
+
use_case: raw.use_case,
|
|
75
|
+
roles: raw.roles,
|
|
76
|
+
knowledge_docs: raw.knowledge_docs,
|
|
77
|
+
skills_count: raw.skills_count,
|
|
78
|
+
pricing: raw.pricing,
|
|
79
|
+
tags: raw.tags,
|
|
80
|
+
languages: raw.languages,
|
|
81
|
+
stats: raw.stats,
|
|
82
|
+
wave_scoped: raw.wave_scoped,
|
|
83
|
+
},
|
|
84
|
+
path: presetDir,
|
|
85
|
+
isDefault: false,
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load all presets from company/presets/ + auto-generated default.
|
|
94
|
+
* Returns [default, ...installed] — default is always first.
|
|
95
|
+
*/
|
|
96
|
+
export function loadPresets(companyRoot: string): LoadedPreset[] {
|
|
97
|
+
const presets: LoadedPreset[] = [];
|
|
98
|
+
|
|
99
|
+
// 1. Default preset (always present)
|
|
100
|
+
const defaultPreset = buildDefaultPreset(companyRoot);
|
|
101
|
+
|
|
102
|
+
// Check if _default.yaml exists with overrides
|
|
103
|
+
const defaultYamlPath = path.join(companyRoot, PRESETS_DIR, DEFAULT_PRESET_FILE);
|
|
104
|
+
if (fs.existsSync(defaultYamlPath)) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = YAML.parse(fs.readFileSync(defaultYamlPath, 'utf-8')) as Partial<PresetDefinition>;
|
|
107
|
+
if (raw.name) defaultPreset.definition.name = raw.name;
|
|
108
|
+
if (raw.tagline) defaultPreset.definition.tagline = raw.tagline;
|
|
109
|
+
if (raw.description) defaultPreset.definition.description = raw.description;
|
|
110
|
+
} catch { /* ignore malformed _default.yaml */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
presets.push(defaultPreset);
|
|
114
|
+
|
|
115
|
+
// 2. Installed presets from company/presets/{name}/preset.yaml
|
|
116
|
+
const presetsDir = path.join(companyRoot, PRESETS_DIR);
|
|
117
|
+
if (fs.existsSync(presetsDir)) {
|
|
118
|
+
const entries = fs.readdirSync(presetsDir, { withFileTypes: true });
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.isDirectory()) continue;
|
|
121
|
+
const preset = loadPresetFromDir(path.join(presetsDir, entry.name));
|
|
122
|
+
if (preset) presets.push(preset);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return presets;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get preset summaries for TUI display.
|
|
131
|
+
*/
|
|
132
|
+
export function getPresetSummaries(companyRoot: string): PresetSummary[] {
|
|
133
|
+
return loadPresets(companyRoot).map(p => ({
|
|
134
|
+
id: p.definition.id,
|
|
135
|
+
name: p.definition.name,
|
|
136
|
+
description: p.definition.description ?? p.definition.tagline,
|
|
137
|
+
rolesCount: p.definition.roles.length,
|
|
138
|
+
roles: p.definition.roles,
|
|
139
|
+
isDefault: p.isDefault,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Find a specific preset by ID.
|
|
145
|
+
*/
|
|
146
|
+
export function getPresetById(companyRoot: string, presetId: string): LoadedPreset | null {
|
|
147
|
+
const presets = loadPresets(companyRoot);
|
|
148
|
+
return presets.find(p => p.definition.id === presetId) ?? null;
|
|
149
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -144,3 +144,81 @@ export function eventTypeToMessageStatus(eventType: ActivityEventType): MessageS
|
|
|
144
144
|
|
|
145
145
|
/** TeamStatus — Role별 현재 상태 + 작업 내용 (context-assembler, runner에서 공유) */
|
|
146
146
|
export type TeamStatus = Record<string, { status: RoleStatus; task?: string }>;
|
|
147
|
+
|
|
148
|
+
/* ═══════════════════════════════════════════════
|
|
149
|
+
* Preset — Wave-scoped team configuration
|
|
150
|
+
* ═══════════════════════════════════════════════ */
|
|
151
|
+
|
|
152
|
+
/** preset.yaml 스키마 */
|
|
153
|
+
export interface PresetDefinition {
|
|
154
|
+
/** Spec version (e.g. "preset/v1") */
|
|
155
|
+
spec: string;
|
|
156
|
+
/** Unique identifier (directory name) */
|
|
157
|
+
id: string;
|
|
158
|
+
/** Display name */
|
|
159
|
+
name: string;
|
|
160
|
+
/** Short tagline */
|
|
161
|
+
tagline?: string;
|
|
162
|
+
/** Version string */
|
|
163
|
+
version: string;
|
|
164
|
+
/** Full description */
|
|
165
|
+
description?: string;
|
|
166
|
+
/** Author info */
|
|
167
|
+
author?: {
|
|
168
|
+
id: string;
|
|
169
|
+
name: string;
|
|
170
|
+
verified?: boolean;
|
|
171
|
+
};
|
|
172
|
+
/** Category / classification */
|
|
173
|
+
category?: string;
|
|
174
|
+
industry?: string;
|
|
175
|
+
stage?: string;
|
|
176
|
+
use_case?: string[];
|
|
177
|
+
/** Role IDs included in this preset */
|
|
178
|
+
roles: string[];
|
|
179
|
+
/** Counts */
|
|
180
|
+
knowledge_docs?: number;
|
|
181
|
+
skills_count?: number;
|
|
182
|
+
/** Pricing */
|
|
183
|
+
pricing?: {
|
|
184
|
+
type: 'one-time' | 'subscription';
|
|
185
|
+
price: number;
|
|
186
|
+
wave_scoped_tier?: string;
|
|
187
|
+
};
|
|
188
|
+
/** Tags for search */
|
|
189
|
+
tags?: string[];
|
|
190
|
+
languages?: string[];
|
|
191
|
+
/** Stats (marketplace) */
|
|
192
|
+
stats?: {
|
|
193
|
+
installs: number;
|
|
194
|
+
rating: number;
|
|
195
|
+
reviews: number;
|
|
196
|
+
waves_used: number;
|
|
197
|
+
};
|
|
198
|
+
/** Wave-scoped recommendations */
|
|
199
|
+
wave_scoped?: {
|
|
200
|
+
recommended_tasks?: string[];
|
|
201
|
+
task_keywords?: string[];
|
|
202
|
+
avg_wave_duration?: string;
|
|
203
|
+
complexity?: string;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Loaded preset with resolved path info */
|
|
208
|
+
export interface LoadedPreset {
|
|
209
|
+
definition: PresetDefinition;
|
|
210
|
+
/** Absolute path to preset directory (or null for _default) */
|
|
211
|
+
path: string | null;
|
|
212
|
+
/** Whether this is the _default preset */
|
|
213
|
+
isDefault: boolean;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Preset summary for TUI display */
|
|
217
|
+
export interface PresetSummary {
|
|
218
|
+
id: string;
|
|
219
|
+
name: string;
|
|
220
|
+
description?: string;
|
|
221
|
+
rolesCount: number;
|
|
222
|
+
roles: string[];
|
|
223
|
+
isDefault: boolean;
|
|
224
|
+
}
|
package/src/tui/api.ts
CHANGED
|
@@ -136,6 +136,7 @@ export async function fetchExecStatus(): Promise<ExecStatus> {
|
|
|
136
136
|
export async function dispatchWave(directive?: string, options?: {
|
|
137
137
|
targetRoles?: string[];
|
|
138
138
|
continuous?: boolean;
|
|
139
|
+
preset?: string;
|
|
139
140
|
}): Promise<WaveResponse> {
|
|
140
141
|
return fetchJson<WaveResponse>('/api/jobs', {
|
|
141
142
|
method: 'POST',
|
|
@@ -144,6 +145,7 @@ export async function dispatchWave(directive?: string, options?: {
|
|
|
144
145
|
directive: directive ?? '',
|
|
145
146
|
targetRoles: options?.targetRoles,
|
|
146
147
|
continuous: options?.continuous ?? false,
|
|
148
|
+
preset: options?.preset,
|
|
147
149
|
},
|
|
148
150
|
});
|
|
149
151
|
}
|
|
@@ -211,6 +213,21 @@ export async function cleanupSessions(): Promise<{ cleaned: number; remaining: n
|
|
|
211
213
|
return fetchJson<{ cleaned: number; remaining: number }>('/api/active-sessions/cleanup', { method: 'POST' });
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
/* ─── Presets ─── */
|
|
217
|
+
|
|
218
|
+
export interface PresetSummary {
|
|
219
|
+
id: string;
|
|
220
|
+
name: string;
|
|
221
|
+
description?: string;
|
|
222
|
+
rolesCount: number;
|
|
223
|
+
roles: string[];
|
|
224
|
+
isDefault: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function fetchPresets(): Promise<PresetSummary[]> {
|
|
228
|
+
return fetchJson<PresetSummary[]>('/api/presets');
|
|
229
|
+
}
|
|
230
|
+
|
|
214
231
|
/* ─── Knowledge docs ─── */
|
|
215
232
|
|
|
216
233
|
export interface KnowledgeDoc {
|
|
@@ -139,26 +139,11 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// OOM debug: track render count
|
|
143
|
-
let panelRenderCount = 0;
|
|
144
|
-
|
|
145
142
|
export const PanelMode: React.FC<PanelModeProps> = ({
|
|
146
143
|
tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
|
|
147
144
|
streamStatus, waveId, activeSessions, allSessions, companyRoot, waves,
|
|
148
145
|
focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
|
|
149
146
|
}) => {
|
|
150
|
-
panelRenderCount++;
|
|
151
|
-
if (panelRenderCount % 100 === 0) {
|
|
152
|
-
const mem = process.memoryUsage();
|
|
153
|
-
console.error(`[PanelMode] render #${panelRenderCount} heap=${Math.round(mem.heapUsed/1024/1024)}MB events=${events.length}`);
|
|
154
|
-
}
|
|
155
|
-
if (panelRenderCount > 1000) {
|
|
156
|
-
console.error(`[PanelMode] ⛔ RENDER LOOP DETECTED: ${panelRenderCount} renders. Bailing out.`);
|
|
157
|
-
onEscape(); // Force back to command mode
|
|
158
|
-
panelRenderCount = 0;
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
147
|
const [termHeight, setTermHeight] = useState(process.stdout.rows || 30);
|
|
163
148
|
const [rightTab, setRightTab] = useState<RightTab>('stream');
|
|
164
149
|
const [docsFilter, setDocsFilter] = useState<DocsFilter>('all');
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { useCallback } from 'react';
|
|
19
|
-
import { dispatchWave, sendDirective, stopWave, fetchJson, killSession, cleanupSessions, fetchActiveSessions } from '../api';
|
|
19
|
+
import { dispatchWave, sendDirective, stopWave, fetchJson, killSession, cleanupSessions, fetchActiveSessions, fetchPresets } from '../api';
|
|
20
|
+
import type { PresetSummary } from '../api';
|
|
20
21
|
|
|
21
22
|
export interface WaveInfo {
|
|
22
23
|
waveId: string;
|
|
@@ -25,9 +26,10 @@ export interface WaveInfo {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface CommandResult {
|
|
28
|
-
type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports' | 'sessions' | 'cleanup' | 'docs' | 'read_file' | 'open_file';
|
|
29
|
+
type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports' | 'sessions' | 'cleanup' | 'docs' | 'read_file' | 'open_file' | 'preset_list' | 'preset_select';
|
|
29
30
|
message: string;
|
|
30
31
|
waveId?: string;
|
|
32
|
+
presets?: PresetSummary[];
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
async function postAssign(roleId: string, task: string): Promise<{ waveId?: string }> {
|
|
@@ -80,6 +82,15 @@ export function useCommand(options: UseCommandOptions) {
|
|
|
80
82
|
|
|
81
83
|
case 'new': {
|
|
82
84
|
const directive = args || undefined;
|
|
85
|
+
if (!directive) {
|
|
86
|
+
// No args → show preset selection UI
|
|
87
|
+
try {
|
|
88
|
+
const presets = await fetchPresets();
|
|
89
|
+
return { type: 'preset_select', message: '', presets };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { type: 'error', message: `Failed to load presets: ${err instanceof Error ? err.message : 'unknown'}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
83
94
|
try {
|
|
84
95
|
const result = await dispatchWave(directive);
|
|
85
96
|
onWaveCreated(result.waveId, directive ?? '');
|
|
@@ -168,6 +179,19 @@ export function useCommand(options: UseCommandOptions) {
|
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
|
|
182
|
+
case 'preset': {
|
|
183
|
+
const subCmd = args.split(/\s+/)[0]?.toLowerCase() || 'list';
|
|
184
|
+
if (subCmd === 'list' || !subCmd) {
|
|
185
|
+
try {
|
|
186
|
+
const presets = await fetchPresets();
|
|
187
|
+
return { type: 'preset_list', message: '', presets };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return { type: 'error', message: `Failed to load presets: ${err instanceof Error ? err.message : 'unknown'}` };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { type: 'error', message: `Unknown preset command: ${subCmd}. Try: /preset list` };
|
|
193
|
+
}
|
|
194
|
+
|
|
171
195
|
case 'roles':
|
|
172
196
|
onShowPanel();
|
|
173
197
|
return { type: 'panel', message: '' };
|