practice polish: no phantom wake, Practice header tag, honest meta

- needsContinuation: a persisted problem-pose ack (terminate:true ended
  the turn deliberately) owes no continuation — reloads after a pose no
  longer wake a phantom turn; error results stay owed like any
  interrupted tool.
- Chat header: practice sessions swap the phase chip for a quiet
  Practice tag (no probe→teach arc applies) and hide the coverage ring
  (practice never extracts — an ever-filling ring would lie).
- ChatSummary gains pendingProblem (activeProblemOf at projection); the
  sidebar meta says 'Problem waiting' for a practice row with a posed
  problem. Kind threads through Chat/[id] page.
- Home picker chip gains Clear (picks were otherwise irremovable).
This commit is contained in:
amosblomqvist
2026-10-04 22:06:22 +03:00
parent e76e42f764
commit 9362baf36e
9 changed files with 101 additions and 7 deletions
+25 -3
View File
@@ -33,7 +33,7 @@ import { PlanBlock, PlanPopup, latestTree } from "@/components/plan-block";
import { PlanMarginalia, planExists } from "@/components/plan-window";
import { MermaidOverlay, SessionMermaidContext } from "@/components/session-overlay";
import { AbortTurnDialog, type AbortMode } from "@/components/abort-turn-dialog";
import { WaypointsIcon } from "lucide-react";
import { DumbbellIcon, WaypointsIcon } from "lucide-react";
import type { PlanBlockData } from "@/src/core";
import { QuestionDock } from "@/components/question-dock";
import { PracticeDock } from "@/components/practice-dock";
@@ -176,10 +176,15 @@ export function Chat({
initialPrompt,
initiallyInFlight,
initialConfigured = false,
kind = "teach",
}: {
chatId: string;
title: string;
initialMessages: LhMessage[];
/** The session kind (practice sessions, owner 2026-10-04): practice
* renders no phase chip (no probe→teach arc applies) and a Practice
* tag rides the header instead. */
kind?: "teach" | "practice";
initialPrompt?: string;
/** A teacher turn is running server-side but attached to no client
* (navigated away mid-turn, returned while in flight). */
@@ -1146,8 +1151,25 @@ export function Chat({
with uncovered vs the chunk threshold, pulses while a run is in
flight, vanishes when the graph is off. Click → the exact
numbers. Before the phase label — the chips cluster right. */}
<CoverageRing chatId={chatId} refreshKey={coverageTick} />
<PhaseChip phase={phases.at(-1) ?? "probe"} />
{/* Practice (owner, 2026-10-04): the coverage ring hides —
practice never extracts, so an ever-filling ring with no
run behind it would lie. */}
{kind !== "practice" && <CoverageRing chatId={chatId} refreshKey={coverageTick} />}
{/* Practice sessions: no phase arc — a quiet Practice tag
instead of the phase chip. */}
{kind === "practice" ? (
<span
data-testid="phase-chip"
data-phase="practice"
aria-hidden="true"
className="pointer-events-none flex shrink-0 select-none items-center gap-1 text-xs font-medium uppercase tracking-wide text-muted-foreground"
>
<DumbbellIcon className="size-3" />
Practice
</span>
) : (
<PhaseChip phase={phases.at(-1) ?? "probe"} />
)}
</TopBar>
<div className="relative flex min-h-0 flex-1">
+11 -1
View File
@@ -187,7 +187,7 @@ export function Home({ initialConfigured }: { initialConfigured: boolean }) {
the chain table as the picker. Quiet, left-aligned above
the composer; teaching mode renders nothing. */}
{mode === "practice" && (
<div className="mb-2 flex justify-start">
<div className="mb-2 flex items-center justify-start gap-1.5">
<button
type="button"
data-testid="practice-props-chip"
@@ -199,6 +199,16 @@ export function Home({ initialConfigured }: { initialConfigured: boolean }) {
? `${practiceProps.length} ${practiceProps.length === 1 ? "proposition" : "propositions"} selected`
: "Pick propositions from the map"}
</button>
{practiceProps.length > 0 && (
<button
type="button"
data-testid="practice-props-clear"
onClick={() => setPracticeProps([])}
className="rounded-full px-2 py-1 font-ui text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
Clear
</button>
)}
</div>
)}
{zeroKeys ? (
+3
View File
@@ -227,6 +227,9 @@ export function timeAgo(iso: string, now = new Date()): string {
* ever wanted. */
function sessionMeta(chat: ChatListItem): string {
if (chat.pendingQuestion) return `Waiting for your answer · ${timeAgo(chat.updatedAt)}`;
if ((chat.kind ?? "teach") === "practice" && chat.pendingProblem) {
return `Problem waiting · ${timeAgo(chat.updatedAt)}`;
}
if (chat.busy) return (chat.kind ?? "teach") === "practice" ? "Practicing…" : "Teacher is working…";
return timeAgo(chat.updatedAt);
}