diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 10703c127561..8042383b2f54 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -41,7 +41,7 @@ "lint:fix": "eslint src/ electron/ --fix", "fmt": "prettier --write 'src/**/*.{ts,tsx}' 'electron/**/*.{js,cjs}' 'vite.config.ts'", "fix": "npm run lint:fix && npm run fmt", - "test:ui": "vitest run --environment jsdom", + "test:ui": "vitest run --environment jsdom src", "preview": "node scripts/assert-root-install.cjs && vite preview --host 127.0.0.1 --port 4174" }, "dependencies": { @@ -84,7 +84,7 @@ "react": "^19.2.5", "react-arborist": "^3.5.0", "react-dom": "^19.2.5", - "react-router-dom": "^7.14.2", + "react-router-dom": "7.17.0", "react-shiki": "^0.9.3", "remark-math": "^6.0.0", "shiki": "^4.0.2", @@ -125,7 +125,7 @@ "rcedit": "^5.0.2", "typescript": "^6.0.3", "vite": "^8.0.10", - "vitest": "^4.1.5", + "vitest": "^4.1.6", "wait-on": "^9.0.5" }, "build": { diff --git a/apps/desktop/src/components/pane-shell/pane-shell.tsx b/apps/desktop/src/components/pane-shell/pane-shell.tsx index a3f6719ee54b..75fb487021ca 100644 --- a/apps/desktop/src/components/pane-shell/pane-shell.tsx +++ b/apps/desktop/src/components/pane-shell/pane-shell.tsx @@ -130,7 +130,7 @@ function trackForPane(pane: CollectedPane, states: Record { + it('formats the minimum source/conflict/correction footer for an important answer', () => { + const markdown = formatCompanyBrainTrace({ + changedAfterCorrection: [{ after: 'Qigawa spelling is canonical', before: 'Kigawa spelling' }], + conflictWinner: 'Obsidian canon note beats stale model memory', + sources: ['Obsidian: BBBB/Canon.md', 'session correction from Dave'] + }) + + expect(markdown).toContain('### Company Brain trace') + expect(markdown).toContain('- Source: Obsidian: BBBB/Canon.md') + expect(markdown).toContain('- Conflict winner: Obsidian canon note beats stale model memory') + expect(markdown).toContain('- Changed after correction: Kigawa spelling → Qigawa spelling is canonical') + expect(answerHasCompleteCompanyBrainTrace(markdown)).toBe(true) + }) + + it('parses trace footers and de-duplicates semicolon-separated sources', () => { + const trace = parseCompanyBrainTrace(`Answer body. + +### Company Brain trace +- Sources: memory:BBBB; memory:BBBB; Obsidian:Canon +- Conflict winner: Dave correction wins over stale memory +- Changed after correction: old → new +`) + + expect(trace).toEqual({ + changedAfterCorrection: [{ after: 'new', before: 'old' }], + conflictWinner: 'Dave correction wins over stale memory', + sources: ['memory:BBBB', 'Obsidian:Canon'] + }) + }) + + it('reports missing trace fields so the UI/gateway can gate important answers later', () => { + expect(missingCompanyBrainTraceFields(null)).toEqual(['source', 'conflictWinner', 'changedAfterCorrection']) + expect( + missingCompanyBrainTraceFields( + parseCompanyBrainTrace(`### Company Brain trace +- Source: session_search:abc +`) + ) + ).toEqual(['conflictWinner', 'changedAfterCorrection']) + expect(answerHasCompleteCompanyBrainTrace('plain answer')).toBe(false) + }) +}) diff --git a/apps/desktop/src/lib/company-brain-trace.ts b/apps/desktop/src/lib/company-brain-trace.ts new file mode 100644 index 000000000000..2fa558a5ca3f --- /dev/null +++ b/apps/desktop/src/lib/company-brain-trace.ts @@ -0,0 +1,108 @@ +export interface CompanyBrainCorrection { + after: string + before?: string +} + +export interface CompanyBrainTrace { + changedAfterCorrection?: CompanyBrainCorrection[] + conflictWinner?: string + sources: string[] +} + +export type CompanyBrainTraceField = 'source' | 'conflictWinner' | 'changedAfterCorrection' + +const TRACE_HEADING_RE = /^#{2,6}\s*Company Brain trace\s*$/im +const TRACE_BLOCK_RE = /(?:^|\n)#{2,6}\s*Company Brain trace\s*\n(?[\s\S]*?)(?=\n#{1,6}\s|\n?$)/i +const BULLET_RE = /^\s*[-*]\s*(?