(name: string, params: any, cwd: string)
| 76 | } |
| 77 | |
| 78 | /** Dispatch a dashboard action by name. Unknown actions are rejected. */ |
| 79 | export async function dispatchAction(name: string, params: any, cwd: string): Promise<ActionResult> { |
| 80 | try { |
| 81 | switch (name) { |
| 82 | case 'config.set': { |
| 83 | const v = validateConfigSet(String(params?.key ?? ''), params?.value); |
| 84 | if (!v.ok) return { ok: false, message: v.error }; |
| 85 | await writeConfigKnob(String(params.key), v.coerced); |
| 86 | return { ok: true, message: `Set ${params.key} = ${v.coerced}. Takes effect on the next run.` }; |
| 87 | } |
| 88 | case 'model.set': { |
| 89 | // role: main (default) | subagent | vision | all — "all" points every role at one model, |
| 90 | // which is all you need when that model has vision (no separate vision model required). |
| 91 | const model = String(params?.model ?? '').trim(); |
| 92 | const role = String(params?.role ?? 'main').trim(); |
| 93 | if (!model) return { ok: false, message: 'Pick a model.' }; |
| 94 | if (!['main', 'subagent', 'vision', 'all'].includes(role)) return { ok: false, message: `Unknown role "${role}".` }; |
| 95 | const { inferProvider } = await import('../llm/role-resolver.js'); |
| 96 | const { looksVisionCapable } = await import('../setup/model-detector.js'); |
| 97 | const provider = inferProvider(model); |
| 98 | const seesImages = looksVisionCapable(model); |
| 99 | if (role === 'main' || role === 'all') await writeConfigKnob('defaults.model', model); |
| 100 | if (role === 'subagent' || role === 'all') { await writeConfigKnob('roles.subagent.model', model); await writeConfigKnob('roles.subagent.provider', provider); } |
| 101 | if (role === 'vision' || role === 'all') { await writeConfigKnob('roles.vision.model', model); await writeConfigKnob('roles.vision.provider', provider); } |
| 102 | const visionNote = role === 'vision' && !seesImages |
| 103 | ? ' ⚠️ this model does not look vision-capable — screenshots may fail.' |
| 104 | : seesImages && role !== 'subagent' ? ' 👁 has vision — no separate vision model needed.' : ''; |
| 105 | const label = role === 'all' ? 'ALL roles (main + subagent + vision)' : role === 'main' ? 'Default model' : `${role} model`; |
| 106 | return { ok: true, message: `${label} → ${model}.${visionNote} Takes effect on the next run.` }; |
| 107 | } |
| 108 | case 'recall.query': { |
| 109 | const q = String(params?.query ?? '').trim(); |
| 110 | if (!q) return { ok: false, message: 'Type what to recall.' }; |
| 111 | const { rankApproaches } = await import('../context/approach-recall.js'); |
| 112 | const { renderApproachDiffs } = await import('../context/approach-diff.js'); |
| 113 | const { getSessionStore } = await import('../session/store.js'); |
| 114 | const store = getSessionStore(); |
| 115 | const worklog = (() => { try { return store.getWorklog(cwd, 100).map((w: any) => ({ kind: 'worklog' as const, text: w.entry, when: String(w.created_at ?? '').slice(0, 10), at: w.created_at, detail: w.kind })); } catch { return []; } })(); |
| 116 | const episodes = await (async () => { |
| 117 | try { const { readEpisodes } = await import('../context/episodic-memory.js'); return (await readEpisodes(cwd)).map((e: any) => ({ kind: 'episode' as const, text: `${e.prompt} ${e.summary}`, when: String(e.ts ?? '').slice(0, 10), at: e.ts, files: e.filesChanged, detail: e.summary, verified: e.verified })); } |
| 118 | catch { return []; } |
| 119 | })(); |
| 120 | const facts = (() => { try { return store.getFactsForCwd(cwd, 200).map((f: string) => ({ kind: 'fact' as const, text: f, when: '', detail: 'fact' })); } catch { return []; } })(); |
| 121 | const receipts = await (async () => { |
| 122 | try { |
| 123 | const { getScheduleStore } = await import('../schedule/store.js'); |
| 124 | const { parseMaintainScope } = await import('../schedule/recipes.js'); |
| 125 | const sched = getScheduleStore(); |
| 126 | const out: any[] = []; |
| 127 | for (const s of sched.list().filter((s: any) => s.recipe === 'maintain' && s.cwd === cwd)) { |
| 128 | const scope = parseMaintainScope(s.prompt).scope; |
| 129 | for (const r of sched.recentRuns(s.id, 50)) { |
| 130 | if (!r.receipt) continue; |
| 131 | try { |
| 132 | const rc = JSON.parse(r.receipt); |
| 133 | out.push({ kind: 'receipt' as const, at: r.started_at, when: String(r.started_at ?? '').slice(0, 10), text: `maintain ${scope}: ${rc.summary || rc.reason || rc.status}`, files: Array.isArray(rc.filesChanged) ? rc.filesChanged : [], detail: scope, verified: rc.status === 'opened' ? true : rc.status === 'blocked' || rc.status === 'failed' ? false : undefined }); |
| 134 | } catch { /* skip */ } |
| 135 | } |
no test coverage detected