From 7bc1c934868c6ce383935b541ad1b7b336cd7c76 Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 3 Aug 2026 17:01:04 +0100 Subject: [PATCH] feat: evolve investigation into guided conversation --- .claude/working-rules.md | 38 ++++++++ app/globals.css | 21 +++++ components/reasoning-workspace.jsx | 134 ++++++++++++----------------- tests/ui/scenario-form.test.jsx | 99 +++++++++++---------- 4 files changed, 169 insertions(+), 123 deletions(-) diff --git a/.claude/working-rules.md b/.claude/working-rules.md index 3ac6d2a..fb42dd6 100644 --- a/.claude/working-rules.md +++ b/.claude/working-rules.md @@ -66,6 +66,44 @@ Use a focused commit message. Do not merge or tag unless explicitly requested. +## Non-narration rule + +Claude Code must act as an implementation agent, not narrate its internal +debugging process. + +When tests fail: + +1. inspect the focused failure; +2. make the smallest justified edit; +3. rerun the focused test; +4. repeat until passing or genuinely blocked. + +Do not print or explain intermediate reasoning. + +Never print: + +- rendered HTML; +- full JSON; +- full graph objects; +- large diffs; +- long stack traces; +- repeated interpretations of the same failure. + +Prefer: + +tool → edit → focused test → concise report + +The final chat response must be under 1,000 words and normally contain only: + +- branch; +- commit hash; +- files changed; +- behaviour changed; +- tests; +- lint/build; +- remaining limitation; +- git status. + ## Response discipline At the end of a task, normally report only: diff --git a/app/globals.css b/app/globals.css index e417d16..dbb837d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,8 +7,29 @@ to { transform: rotate(360deg); } } +@keyframes fadeIn { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +.investigation-card { + animation: fadeIn 0.4s ease-out both; +} + +.investigation-card:nth-child(2) { + animation-delay: 0.08s; +} + +.investigation-card:nth-child(3) { + animation-delay: 0.16s; +} + @media (prefers-reduced-motion: reduce) { [style*="animation:spin"] { animation: none !important; } + + .investigation-card { + animation: none; + } } diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 640a9ce..c48eab0 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -63,7 +63,7 @@ function ActivitySpinner() { function SituationCard({ centralStatement }) { if (!centralStatement) return null; return ( -
+

Your situation

@@ -76,9 +76,9 @@ function SituationCard({ centralStatement }) { function CurrentUnderstanding({ currentSummary }) { if (currentSummary) { return ( -
+

- Current understanding + What we've established

{currentSummary}

@@ -86,7 +86,7 @@ function CurrentUnderstanding({ currentSummary }) { } return ( -
+

We have started to separate what is known from what still needs checking.

@@ -94,110 +94,87 @@ function CurrentUnderstanding({ currentSummary }) { ); } -// ── Current focus card ──────────────────────────────────────── -function CurrentFocus({ graph }) { - if (!graph?.activeUnknownNodeId || !graph.nodes?.length) return null; - - const activeNode = graph.nodes.find( - (n) => n.id === graph.activeUnknownNodeId - ); - if (!activeNode) return null; - - // Find the unknown label that maps to activeUnknownNodeId from selectedQuestion or nodes - const statusText = - activeNode.status === "resolved" ? "Answered" : "Under investigation"; - - return ( -
-

- What we are working out -

-

{activeNode.label}

- {activeNode.description && activeNode.description !== activeNode.label && ( -

Why it matters: {activeNode.description}

- )} - - {statusText} - -
- ); -} - -// ── Next question card (prominent) ──────────────────────────── -function NextQuestionCard({ selectedQuestion }) { +// ── Current investigation card (prominent) ───────────────────── +function CurrentInvestigationCard({ selectedQuestion }) { if (!selectedQuestion) return null; const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question; if (!q) return null; return ( -
+

- Next question + Current investigation

{q}

); } -// ── Reasoning progress card ──────────────────────────────────── -function ReasoningProgress({ graph }) { +// ── Investigation progress card ────────────────────────────── +function InvestigationProgress({ graph, noQuestionReason: rwNoQuestionReason }) { if (!graph?.nodes?.length) return null; + const resolvedIds = new Set(graph.resolvedNodeIds || []); const unknowns = graph.nodes.filter((n) => n.kind === "unknown"); - const remainingCount = unknowns.filter((u) => u.status !== "resolved").length; + const remainingCount = unknowns.filter( + (u) => u.status !== "resolved" && !resolvedIds.has(u.id), + ).length; + const activeNode = graph.activeUnknownNodeId + ? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId) + : null; return ( -
-

- Reasoning progress -

- {remainingCount > 0 ? ( -

- We have identified {remainingCount} area{remainingCount === 1 ? "" : "s"} that still need investigation. +

+ {remainingCount > 0 && !rwNoQuestionReason ? ( +

+ We are still building confidence about your situation.{" "} + {remainingCount === 1 + ? "One area remains." + : `${remainingCount} areas remain.`}

) : ( -

+

All areas under investigation are now complete.

)} - {graph.activeUnknownNodeId && (() => { - const activeNode = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId); - if (!activeNode) return null; - return ( - <> -

- Current focus -

-

{activeNode.label}

- {activeNode.description && activeNode.description !== activeNode.label && ( -

Why this matters: {activeNode.description}

- )} - - ); - })()} - {!graph.activeUnknownNodeId && remainingCount === 0 && ( -

There is no active area of investigation at the moment.

+ {activeNode && ( + <> +

+ Current focus +

+

{activeNode.label}

+ {activeNode.description && activeNode.description !== activeNode.label && ( +

Why it matters: {activeNode.description}

+ )} + + )} + {!activeNode && remainingCount === 0 && ( +

There is nothing further to investigate at this time.

)}
); } -// ── No question state ───────────────────────────────────────── -function NoQuestionMessage({ noQuestionReason }) { - let message = "There is no next question at the moment."; +// ── Investigation complete state ─────────────────────────────── +function InvestigationCompleteMessage({ noQuestionReason }) { + let message = "We have established enough for now."; if (noQuestionReason) { const reason = String(noQuestionReason); - if (reason.toLowerCase().includes("satisfied") || reason.toLowerCase().includes("complete")) { - message += " The situation has been fully investigated."; + if ( + reason.toLowerCase().includes("resolved") || + reason.toLowerCase().includes("satisfied") || + reason.toLowerCase().includes("complete") + ) { + message = "You have provided enough information. The situation has been fully investigated."; } else if (reason.toLowerCase().includes("insufficient")) { - message += " We need more information to determine the next step."; + message = "There is not yet enough evidence to guide the next step. Your original situation will remain our focus when new information becomes available."; } else { - message += " " + reason; + message = reason; } } return ( -
+

{message}

); @@ -313,22 +290,21 @@ export default function ReasoningWorkspace({
) : ( <> - {/* When analysis succeeded but there's no question to answer */} + {/* When investigation has nothing further to ask */} {status === "success" && !canAnswer && graph && ( - + )} {graph && } {graph && } - {graph && } - {canAnswer && } - {graph && } + {canAnswer && } + {graph && } {/* ── Answer form ──────────────────────────────── */} {canAnswer && (