Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/dashboard-mode-checkboxes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@gemstack/framework': minor
---

feat(framework): show the active Open Loop modes as read-only checkboxes on the dashboard (#272)

When a run builds under a domain preset, the dashboard now renders a Modes panel with the run's modes as checkboxes ([x] technical / [ ] autopilot), so the policy driving the build is visible beside the stack and loop panels. Backed by a new `modes` framework event (`OPEN_LOOP_MODES` is the canonical mode ordering, shared with the meta-select router); a run with no preset emits nothing and shows no panel. The event persists with the run, so `--resume` rehydrates the panel too.
21 changes: 21 additions & 0 deletions packages/framework/src/dashboard/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export function dashboardHtml(title: string, stoppable = false): string {
font-weight: 600; }
.badge.grade { background: #14371f; color: #67d98f; }
.badge.proto { background: #2a2320; color: #f0a35e; }
#modes li { display: flex; align-items: center; gap: 8px; color: #b7c0d0; }
#modes .box { color: #6ea8fe; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
#modes li.off { color: #5c657a; }
#modes li.off .box { color: #3a4256; }
#activity { grid-column: 1 / -1; }
#log { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; color: #96a0b3;
max-height: 320px; overflow-y: auto; white-space: pre-wrap; }
Expand Down Expand Up @@ -95,6 +99,10 @@ export function dashboardHtml(title: string, stoppable = false): string {
<div id="grade" class="muted">building…</div>
<ul id="passes"></ul>
</section>
<section id="modes-panel" hidden>
<h2>Modes</h2>
<ul id="modes"></ul>
</section>
<section id="deploy-panel">
<h2>Deploy</h2>
<div id="deploy" class="muted">not decided yet</div>
Expand Down Expand Up @@ -198,6 +206,18 @@ function renderRationale(e) {
row.className = 'alt'; row.innerHTML = '<b>' + esc(a.option) + '</b> \\u2014 ' + esc(a.whyNot);
});
}
function renderModes(all, active) {
const on = new Set(active || []);
const ul = $('modes'); ul.innerHTML = '';
for (const m of (all || [])) {
const checked = on.has(m);
const li = document.createElement('li');
if (!checked) li.className = 'off';
li.innerHTML = '<span class="box">' + (checked ? '[x]' : '[ ]') + '</span> ' + esc(m);
ul.appendChild(li);
}
$('modes-panel').hidden = false;
}
function setSessionLink(sessionId, sessionLink) {
const el = $('session-link');
if (sessionLink) {
Expand Down Expand Up @@ -229,6 +249,7 @@ function onEvent(fe) {
}
else if (fe.kind === 'bootstrap') bootstrap(fe.event);
else if (fe.kind === 'driver') driver(fe.event);
else if (fe.kind === 'modes') renderModes(fe.all, fe.active);
else if (fe.kind === 'log') log(fe.message);
else if (fe.kind === 'end') {
ended = true;
Expand Down
3 changes: 3 additions & 0 deletions packages/framework/src/dashboard/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ test('dashboard serves the HTML page with the title', async () => {
assert.equal(status, 200)
assert.match(body, /My Framework/)
assert.match(body, /new EventSource\('\/events'\)/)
// The Modes panel + its renderer ship in the page (#272), hidden until a modes event.
assert.match(body, /id="modes-panel" hidden/)
assert.match(body, /function renderModes/)
} finally {
await dash.close()
}
Expand Down
12 changes: 12 additions & 0 deletions packages/framework/src/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { strict as assert } from 'node:assert'
import { test } from 'node:test'
import {
OPEN_LOOP_MODES,
SESSION_ID_PLACEHOLDER,
formatFrameworkEvent,
hasSessionIdPlaceholder,
Expand Down Expand Up @@ -35,6 +36,17 @@ test('formatFrameworkEvent renders a preview line', () => {
)
})

test('formatFrameworkEvent renders modes as checkboxes (#272)', () => {
assert.equal(
formatFrameworkEvent({ kind: 'modes', all: ['autopilot', 'technical'], active: ['technical'] }),
' modes: [ ] autopilot [x] technical',
)
})

test('OPEN_LOOP_MODES is the canonical mode ordering', () => {
assert.deepEqual([...OPEN_LOOP_MODES], ['autopilot', 'technical'])
})

test('formatFrameworkEvent distinguishes finished / stopped / failed (#218)', () => {
assert.equal(formatFrameworkEvent({ kind: 'end', ok: true }), '✓ finished')
assert.equal(formatFrameworkEvent({ kind: 'end', ok: false, stopped: true }), '■ stopped')
Expand Down
18 changes: 18 additions & 0 deletions packages/framework/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export type FrameworkEvent =
| { kind: 'preview'; url: string; command: string }
/** A framework-level log line. */
| { kind: 'log'; message: string }
/**
* The run's active Open Loop modes (#272), emitted once when a domain preset is
* in effect. `all` is every mode the run knows about (stable order); `active` is
* the subset switched on for this run. The dashboard renders them as read-only
* checkboxes so the policy driving the build is visible.
*/
| { kind: 'modes'; all: readonly string[]; active: readonly string[] }
/**
* The run finished. `ok` is false when it threw. `stopped` marks the common,
* non-error case where the user interrupted it (the dashboard Stop button /
Expand All @@ -50,6 +57,10 @@ export function formatFrameworkEvent(event: FrameworkEvent): string {
return `▶ your app is running at ${event.url}`
case 'log':
return ` ${event.message}`
case 'modes': {
const shown = event.all.map(m => `${event.active.includes(m) ? '[x]' : '[ ]'} ${m}`).join(' ')
return ` modes: ${shown}`
}
case 'driver':
return formatDriverEvent(event.event)
case 'bootstrap':
Expand Down Expand Up @@ -102,6 +113,13 @@ function truncate(text: string, max = 100): string {
return flat.length > max ? flat.slice(0, max - 1) + '…' : flat
}

/**
* The Open Loop modes a run can activate, in the order the dashboard shows them.
* The single source of truth for both the mode checkboxes (#272) and the
* meta-select router's validation ({@link import('./meta-select.js').META_SELECT_MODES}).
*/
export const OPEN_LOOP_MODES = ['autopilot', 'technical'] as const

/**
* The placeholder a `--session-link` template uses for the real session id.
* Remote Control does not expose a URL you can build from a session id, so we
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export {
resolveSessionLink,
hasSessionIdPlaceholder,
SESSION_ID_PLACEHOLDER,
OPEN_LOOP_MODES,
} from './events.js'
export { startDashboard, dashboardHtml, type Dashboard, type DashboardOptions } from './dashboard/index.js'
export {
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/src/meta-select.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DomainPreset } from '@gemstack/ai-autopilot'
import type { DriverSession } from './driver/index.js'
import { OPEN_LOOP_MODES } from './events.js'

/**
* AI meta-select (#204, Rom's "meta-meta prompt"): before a run, infer which Open
Expand All @@ -16,7 +17,7 @@ import type { DriverSession } from './driver/index.js'
*/

/** The Open Loop modes a run can activate. Mirrors the CLI's `--autopilot` / `--technical`. */
export const META_SELECT_MODES = ['autopilot', 'technical'] as const
export const META_SELECT_MODES = OPEN_LOOP_MODES

/** The system framing for the meta-select turn: a tiny, fast classifier, not a builder. */
export const META_SELECT_SYSTEM =
Expand Down
20 changes: 20 additions & 0 deletions packages/framework/src/preset-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,30 @@ test('a domain preset frames the run and is narrated', async () => {
assert.ok(log, 'domain preset is logged')
assert.match((log as { message: string }).message, /modes: technical/)

// The active modes are emitted for the dashboard checkboxes (#272): both known
// modes, with only the active one on.
const modes = events.find(e => e.kind === 'modes')
assert.deepEqual(modes, { kind: 'modes', all: ['autopilot', 'technical'], active: ['technical'] })

// The run still completes normally.
assert.equal(result.productionGrade, true)
})

test('no modes event is emitted without a domain preset (#272)', async () => {
const events: FrameworkEvent[] = []
await runFramework({
intent: FAKE_INTENT,
driver: fakeDriver(),
cwd: '/tmp/ws',
signals: FAKE_SIGNALS,
onEvent: e => events.push(e),
})
assert.equal(
events.some(e => e.kind === 'modes'),
false,
)
})

test('the preset loop is materialized against the driver and runs its chain', async () => {
const { driver } = recordingDriver()
const { loop } = await runFramework({
Expand Down
4 changes: 3 additions & 1 deletion packages/framework/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import type { Driver, DriverEvent, DriverSession } from './driver/index.js'
import { memoryFraming, type LoadedMemory } from './memory.js'
import { decideDeploy, deployWith, domainLoopChecklist, driverArchitect, driverBuild, driverChecklist, driverImprove, driverLoopPrompts } from './steps.js'
import { hasSessionIdPlaceholder, resolveSessionLink, type FrameworkEvent } from './events.js'
import { hasSessionIdPlaceholder, OPEN_LOOP_MODES, resolveSessionLink, type FrameworkEvent } from './events.js'

/**
* The framework's default full-fledged pass budget. Higher than ai-autopilot's
Expand Down Expand Up @@ -289,6 +289,8 @@ export async function runFramework(opts: RunFrameworkOptions): Promise<RunFramew
kind: 'log',
message: `Domain preset: ${domainPreset.title}${modeNote}; ${domainPreset.loops.length}-loop review policy in effect`,
})
// Surface the run's active modes as read-only checkboxes on the dashboard (#272).
emit({ kind: 'modes', all: OPEN_LOOP_MODES, active: opts.modes ?? [] })
}

// Watch the black box for its real session id (the {type:'result'} event) and
Expand Down
Loading