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>
114 lines
4.6 KiB
TypeScript
114 lines
4.6 KiB
TypeScript
/**
|
|
* @fileoverview Read My Mind collectors tests (src/readmymind-collectors.ts).
|
|
*
|
|
* `parseTranscriptSignals` runs on JSONL fixtures; `readTranscriptSignals`
|
|
* and `collectWorkspaceSignals` run against real temp files/repos under this
|
|
* test file's temp HOME (no tmux, no network).
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import {
|
|
parseTranscriptSignals,
|
|
readTranscriptSignals,
|
|
collectWorkspaceSignals,
|
|
} from '../src/readmymind-collectors.js';
|
|
|
|
function assistantLine(blocks: unknown[]): string {
|
|
return JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: blocks } });
|
|
}
|
|
|
|
function userToolResultLine(toolUseId: string, isError: boolean): string {
|
|
return JSON.stringify({
|
|
type: 'user',
|
|
message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: toolUseId, is_error: isError }] },
|
|
});
|
|
}
|
|
|
|
describe('parseTranscriptSignals', () => {
|
|
it('keeps the FULL last assistant text, not a snippet', () => {
|
|
const long = 'x'.repeat(4000) + ' THE_END';
|
|
const lines = [
|
|
assistantLine([{ type: 'text', text: 'earlier reply' }]),
|
|
assistantLine([{ type: 'text', text: long }]),
|
|
];
|
|
const signals = parseTranscriptSignals(lines);
|
|
expect(signals.lastAssistantText).toContain('THE_END');
|
|
expect(signals.lastAssistantText!.length).toBeGreaterThan(3000);
|
|
});
|
|
|
|
it('extracts recent tool calls with argument summaries and failure marks', () => {
|
|
const lines = [
|
|
assistantLine([{ type: 'tool_use', id: 't1', name: 'Edit', input: { file_path: 'src/foo.ts' } }]),
|
|
assistantLine([{ type: 'tool_use', id: 't2', name: 'Bash', input: { command: 'npm test' } }]),
|
|
userToolResultLine('t2', true),
|
|
];
|
|
const signals = parseTranscriptSignals(lines);
|
|
expect(signals.recentTools).toEqual([
|
|
{ name: 'Edit', detail: 'src/foo.ts', failed: undefined },
|
|
{ name: 'Bash', detail: 'npm test', failed: true },
|
|
]);
|
|
});
|
|
|
|
it('caps retained tools to the most recent N', () => {
|
|
const lines = Array.from({ length: 15 }, (_, i) =>
|
|
assistantLine([{ type: 'tool_use', id: `t${i}`, name: 'Read', input: { file_path: `f${i}` } }])
|
|
);
|
|
const signals = parseTranscriptSignals(lines);
|
|
expect(signals.recentTools).toHaveLength(10);
|
|
expect(signals.recentTools[0].detail).toBe('f5');
|
|
expect(signals.recentTools[9].detail).toBe('f14');
|
|
});
|
|
|
|
it('skips malformed lines and tool_result-only user entries without text', () => {
|
|
const lines = ['{"type": "assistant", TRUNCATED', '', userToolResultLine('nope', false)];
|
|
const signals = parseTranscriptSignals(lines);
|
|
expect(signals.lastAssistantText).toBeNull();
|
|
expect(signals.recentTools).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('readTranscriptSignals', () => {
|
|
it('reads a real transcript file and returns null for a missing one', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'rmm-transcript-'));
|
|
const file = join(dir, 'session.jsonl');
|
|
writeFileSync(file, [assistantLine([{ type: 'text', text: 'tail reply' }]), ''].join('\n'));
|
|
|
|
const signals = await readTranscriptSignals(file);
|
|
expect(signals?.lastAssistantText).toBe('tail reply');
|
|
|
|
expect(await readTranscriptSignals(join(dir, 'missing.jsonl'))).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('collectWorkspaceSignals', () => {
|
|
it('returns null for a non-git directory', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'rmm-nogit-'));
|
|
expect(await collectWorkspaceSignals(dir)).toBeNull();
|
|
expect(await collectWorkspaceSignals(join(dir, 'does-not-exist'))).toBeNull();
|
|
});
|
|
|
|
it('collects branch, status, commits, and changeset presence from a real repo', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'rmm-git-'));
|
|
const git = (...args: string[]) => execFileSync('git', args, { cwd: dir });
|
|
git('init', '-b', 'main');
|
|
git('config', 'user.email', 'test@example.com');
|
|
git('config', 'user.name', 'Test');
|
|
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
git('add', 'a.txt');
|
|
git('commit', '-m', 'first commit');
|
|
writeFileSync(join(dir, 'b.txt'), 'dirty');
|
|
mkdirSync(join(dir, '.changeset'));
|
|
writeFileSync(join(dir, '.changeset', 'README.md'), 'not a changeset');
|
|
writeFileSync(join(dir, '.changeset', 'blue-cats-run.md'), '---\n"pkg": patch\n---\n');
|
|
|
|
const signals = await collectWorkspaceSignals(dir);
|
|
expect(signals?.branch).toBe('main');
|
|
expect(signals?.statusShort).toContain('b.txt');
|
|
expect(signals?.recentCommits).toContain('first commit');
|
|
expect(signals?.hasChangesets).toBe(true);
|
|
});
|
|
});
|