feat(module2): chat with your agent — three canned prompts, then Layers 2/3

Module 2 is now a chat straight to the agent instead of Build & flash.
Removed the "Open your node" links and the Live board feed card.

New AgentChat component: three imperative canned prompts —
- List the I2C devices on the bus
- Count to 100 and print the value once a second in the LED matrix
- Scroll GO CLAWS on the LED matrix
Each sends to the agent (fire-and-forget, cloud) and streams its
activity (tools/flash/reply) back into the transcript. A prompt is marked
done on its first terminal step (flash/response); an error resets it to
retry. Prompts run one at a time.

Once all three have run successfully, Module 2 reveals "What's next" —
the ADD Layer 2 (Skills) + Layer 3 (Policies & failure) capture — and
Proceed gates on all-three-tried AND L2 + L3 filled.

Note: BuildFlash / LiveBoardFeed / ActorMap are now orphaned (kept for
possible reuse).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-22 01:39:25 -07:00
co-authored by Claude Opus 4.8
parent c2ef338888
commit 7780903278
4 changed files with 342 additions and 85 deletions
+41 -12
View File
@@ -13,7 +13,6 @@ vi.mock('@/lib/api', async (orig) => ({
emit = on
return () => {}
},
getNodeStatus: vi.fn().mockResolvedValue({ teamId: 't', online: false }),
sendPrompt: vi.fn().mockResolvedValue(undefined),
}))
@@ -25,6 +24,13 @@ function renderPage() {
)
}
/** Click a canned prompt and let the agent reach a terminal (success) step. */
async function completePrompt(user: ReturnType<typeof userEvent.setup>, id: string) {
await user.click(screen.getByTestId(`prompt-${id}`))
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
await waitFor(() => expect(screen.getByTestId(`prompt-${id}`)).toHaveAttribute('data-state', 'done'))
}
describe('Module2', () => {
beforeEach(() => {
useSession.getState().reset()
@@ -39,31 +45,54 @@ describe('Module2', () => {
).toHaveAttribute('data-state', 'active')
})
it('shows the live board feed and the build & flash panel', () => {
it('is a chat with the three canned prompts — no live feed / build & flash', () => {
renderPage()
expect(screen.getByTestId('live-board-feed')).toBeInTheDocument()
expect(screen.getByTestId('activity-log')).toBeInTheDocument()
expect(screen.getByTestId('agent-chat')).toBeInTheDocument()
expect(screen.getByTestId('prompt-i2c')).toBeInTheDocument()
expect(screen.getByTestId('prompt-count')).toBeInTheDocument()
expect(screen.getByTestId('prompt-scroll')).toBeInTheDocument()
expect(screen.queryByTestId('live-board-feed')).toBeNull()
expect(screen.queryByTestId('activity-log')).toBeNull()
})
it('gates Proceed until the board acted and L2 + L3 are filled', async () => {
it('reveals "what\'s next" (the ADD layers) only after all three prompts succeed', async () => {
const user = userEvent.setup()
renderPage()
expect(screen.queryByTestId('whats-next')).toBeNull()
expect(screen.queryByLabelText(/layer 2/i)).toBeNull()
await completePrompt(user, 'i2c')
await completePrompt(user, 'count')
expect(screen.queryByTestId('whats-next')).toBeNull() // still one to go
await completePrompt(user, 'scroll')
expect(screen.getByTestId('whats-next')).toBeInTheDocument()
expect(screen.getByLabelText(/layer 2/i)).toBeInTheDocument()
})
it('gates Proceed until all prompts ran AND L2 + L3 are filled', async () => {
const user = userEvent.setup()
useSession.getState().setAddLayer('L2', 'escalate on critical')
useSession.getState().setAddLayer('L3', 'drive damper on critical')
renderPage()
const proceed = screen.getByRole('button', { name: /proceed/i })
expect(proceed).toBeDisabled()
// the board acts (via Build & flash) → activity streams from the node
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
await waitFor(() => expect(proceed).toBeEnabled())
await completePrompt(user, 'i2c')
await completePrompt(user, 'count')
await completePrompt(user, 'scroll')
const proceed = screen.getByRole('button', { name: /proceed/i })
expect(proceed).toBeEnabled()
})
it('marks m2 complete on Proceed', async () => {
const user = userEvent.setup()
renderPage()
await completePrompt(user, 'i2c')
await completePrompt(user, 'count')
await completePrompt(user, 'scroll')
await user.type(screen.getByLabelText(/layer 2/i), 'L2 text')
await user.type(screen.getByLabelText(/layer 3/i), 'L3 text')
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
await user.click(await screen.findByRole('button', { name: /proceed/i }))
await user.click(screen.getByRole('button', { name: /proceed/i }))
expect(useSession.getState().phases.m2).toBe(true)
})
})