mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
The feature as pitched in docs/readmymind-plan.md: pressing the header
brain button predicts the prompt you were about to type, from the case's
intent profile plus everything the session already knows.
Backend:
- readmymind-context.ts: pure budgeted context assembler (9 ranked
sources: pending approval dialog, user goals, last assistant turn tail,
recent prompts, tool activity, git workspace signals, away context,
sibling sessions, rethink state; 30 KB budget, whole-section drop from
the bottom of the ranking, trust tiers stated in the prompt)
- readmymind-collectors.ts: transcript tail reader (the live watcher
keeps only a 500-char snippet) and git signal collection (execFile,
2s timeout, skipped for remote-SSH cases)
- readmymind-predictor.ts: one-shot claude -p in a throwaway tmux
session, opus by default (readMyMindModel setting), strict JSON
contract with 1-3 suggestions (continue / verify / redirect), newline
stripping, 90s timeout; mutable singleton so route tests can stub it
- POST /api/sessions/:id/readmymind: claude-mode only (400), one
prediction in flight per session (409 CONFLICT), rethink body
{ steer, rejected }; ownership via findSessionOrFail
Frontend:
- readmymind-ui.js (loadorder 11.3): header brain button, marker-hidden
until readMyMindEnabled is ON, desktop only (phone key is phase 3);
modal with editable suggestion + rationale and Send / Insert /
Rethink / Dismiss; suggestion text rendered via value/textContent only
and nothing ever auto-sends
- App Settings -> Panels checkbox for readMyMindEnabled; en + zh-CN
strings
Verified end to end against a live isolated instance: transcript
capture, a real opus prediction grounded in the stated goals, rethink
steering, the 409, and the browser modal incl. Insert leaving the text
unsubmitted on the composer. 41 new unit/route tests; full test:ci
sweep green (4680 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.9 KiB
TypeScript
74 lines
2.9 KiB
TypeScript
/**
|
|
* @fileoverview Read My Mind predictor output-contract tests
|
|
* (src/readmymind-predictor.ts).
|
|
*
|
|
* Pure `parsePredictionOutput` tests only: the spawn/poll runner is exercised
|
|
* through the stubbed singleton in the route tests, never by really spawning
|
|
* tmux under vitest.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { parsePredictionOutput } from '../src/readmymind-predictor.js';
|
|
|
|
const VALID = JSON.stringify({
|
|
suggestions: [
|
|
{ prompt: 'run the tests', why: 'the assistant just finished a fix', kind: 'verify' },
|
|
{ prompt: 'COM', why: 'changesets are pending', kind: 'continue' },
|
|
],
|
|
});
|
|
|
|
describe('parsePredictionOutput', () => {
|
|
it('parses the strict contract', () => {
|
|
const suggestions = parsePredictionOutput(VALID);
|
|
expect(suggestions).toHaveLength(2);
|
|
expect(suggestions[0]).toEqual({
|
|
prompt: 'run the tests',
|
|
why: 'the assistant just finished a fix',
|
|
kind: 'verify',
|
|
});
|
|
});
|
|
|
|
it('tolerates fenced or prosed wrapping around the JSON object', () => {
|
|
expect(parsePredictionOutput('```json\n' + VALID + '\n```')).toHaveLength(2);
|
|
expect(parsePredictionOutput('Here you go:\n' + VALID)).toHaveLength(2);
|
|
});
|
|
|
|
it('throws cleanly on garbage', () => {
|
|
expect(() => parsePredictionOutput('no json here at all')).toThrow(/no JSON object/);
|
|
expect(() => parsePredictionOutput('{ "definitely": not json }')).toThrow(/malformed JSON/);
|
|
});
|
|
|
|
it('throws on a shape mismatch, never a half-suggestion', () => {
|
|
expect(() => parsePredictionOutput('{"suggestions": []}')).toThrow(/contract/);
|
|
expect(() => parsePredictionOutput('{"ideas": ["x"]}')).toThrow(/contract/);
|
|
expect(() => parsePredictionOutput(JSON.stringify({ suggestions: [{ prompt: 'x', kind: 'guess' }] }))).toThrow(
|
|
/contract/
|
|
);
|
|
const four = { suggestions: Array(4).fill({ prompt: 'x', kind: 'continue' }) };
|
|
expect(() => parsePredictionOutput(JSON.stringify(four))).toThrow(/contract/);
|
|
});
|
|
|
|
it('collapses embedded newlines to single-line prompts (multi-line breaks Ink)', () => {
|
|
const out = parsePredictionOutput(
|
|
JSON.stringify({ suggestions: [{ prompt: 'fix the bug\nthen run tests', kind: 'continue' }] })
|
|
);
|
|
expect(out[0].prompt).toBe('fix the bug then run tests');
|
|
});
|
|
|
|
it('defaults a missing why and drops empty prompts', () => {
|
|
const out = parsePredictionOutput(JSON.stringify({ suggestions: [{ prompt: 'ok', kind: 'continue' }] }));
|
|
expect(out[0].why).toBe('');
|
|
|
|
expect(() =>
|
|
parsePredictionOutput(JSON.stringify({ suggestions: [{ prompt: ' \n ', kind: 'continue' }] }))
|
|
).toThrow(/empty/);
|
|
});
|
|
|
|
it('bounds runaway fields instead of failing them', () => {
|
|
const out = parsePredictionOutput(
|
|
JSON.stringify({ suggestions: [{ prompt: 'p'.repeat(5000), why: 'w'.repeat(5000), kind: 'redirect' }] })
|
|
);
|
|
expect(out[0].prompt.length).toBe(1000);
|
|
expect(out[0].why.length).toBe(300);
|
|
});
|
|
});
|