practice fix — creation is graph-domain: cluster picker, nodeIds-only API (owner ruling 2026-09-29)
Owner ruling: 'problem sets always take in nodes — graph objects' —
practice is graph-domain, not chat-domain. No session-created sets.
API: POST /api/practice/sets takes {nodeIds, params, title?} ONLY —
the chatId branch, UnknownChatError, and the any-provenance filter are
deleted; the selector is nodeIds (readNodes filtered to the selection,
unknown ids drop, empty live intersection → 400 empty_selection);
title optional from the UI (the picker's cluster label), dated default
stays. chatStore leaves the pipeline deps entirely (labels resolve
store-side in the clusters service).
GET /api/graph/clusters (new): the birth-session groups the graph view
renders — graphSnapshot's clusters verbatim (same ordering, same
MIN_CLUSTER_SIZE presentation filter), mapped to {key, kind
chat|notes, label (null when the naming record is gone — honest,
never guessed), nodeIds, nodeCount}; corrupt/empty graph → empty
list. GraphClusterGroup contract in core/contracts/graph.ts (client-
safe — the dialog imports it).
practice-dialog: session picker → cluster picker (rows show label +
proposition count, notes clusters marked); Create posts the cluster's
nodeIds + label as title + params. 400 empty-selection copy reworded
for the vanished-nodes edge ('Nothing practicable is left in that
cluster — the graph changed.'); 503 deep link unchanged. Preselected
mode gone — the dialog is the only creation entry for now (the graph-
view cluster entry comes later with free-form selection).
Sidebar: session right-click 'Practice this session's propositions'
removed (item + state + dialog wiring); the /practice nav row stays.
Tests: pipeline — chatId/any-provenance tests die, nodeIds selection
pins added (unknown-id filtering, dated-default title, title hint
rides, empty/vanished → EmptySelectionError); render — cluster picker
pins (label+count rows, notes kind, unlabeled cluster muted, empty
graph line), preselected pins removed; 16 render + 13 pipeline.
Docs: architecture.md §7 API+UI paragraph rewritten (graph-domain
ruling, /api/graph/clusters, cluster picker as the only entry, no
404-unknown-chat); README creation-flow line updated.
1130/1130, tsc 0, lint 0, build 0.
This commit is contained in:
@@ -9,7 +9,6 @@ import type { ChatDeleteMode } from "@/src/core";
|
||||
import { GraduationCapIcon, PanelLeftCloseIcon, PanelLeftOpenIcon, PlusIcon, SearchIcon, SettingsIcon } from "lucide-react";
|
||||
import { ActivitySection } from "@/components/activity-dock";
|
||||
import { CoverageMenuBlock } from "@/components/extraction-coverage";
|
||||
import { PracticeDialog } from "@/components/practice-dialog";
|
||||
import { LhMark } from "@/components/lh-mark";
|
||||
import { BrailleSpinner } from "@/components/braille-spinner";
|
||||
import { KeyboardShortcuts } from "@/components/keyboard-shortcuts";
|
||||
@@ -398,9 +397,6 @@ function SessionsSidebar() {
|
||||
const ctxAnchorRef = useRef<HTMLElement | null>(null);
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState<ChatListItem | null>(null);
|
||||
// Practice slice 5 (2026-09-29): the right-click "Practice this session's
|
||||
// propositions" opens the creation dialog PRESELECTED on that session.
|
||||
const [practiceChat, setPracticeChat] = useState<{ chatId: string; title: string } | null>(null);
|
||||
|
||||
const openContext = (e: React.MouseEvent<HTMLButtonElement>, chat: ChatListItem) => {
|
||||
e.preventDefault(); // suppress the native menu
|
||||
@@ -736,21 +732,6 @@ function SessionsSidebar() {
|
||||
Delete…
|
||||
</button>
|
||||
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
|
||||
{/* Practice (slice 5): the session's propositions → a problem set.
|
||||
Sits ABOVE the coverage block — an action like Extract, not
|
||||
management like Rename/Delete. */}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="ctx-practice"
|
||||
onClick={() => {
|
||||
const target = chats?.find((c) => c.id === ctxChatId);
|
||||
if (target) setPracticeChat({ chatId: target.id, title: target.title });
|
||||
setCtxChatId(null);
|
||||
}}
|
||||
className="flex items-center rounded-lg px-2.5 py-1.5 text-left text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
Practice this session's propositions
|
||||
</button>
|
||||
<CoverageMenuBlock chatId={ctxChatId} />
|
||||
</MenuShell>
|
||||
)}
|
||||
@@ -762,11 +743,6 @@ function SessionsSidebar() {
|
||||
onClose={() => setDeleting(null)}
|
||||
onDeleted={onDeleted}
|
||||
/>
|
||||
{/* The preselected practice dialog (slice 5) — portaled by the
|
||||
component itself, here for state locality like the delete dialog. */}
|
||||
{practiceChat && (
|
||||
<PracticeDialog preselected={practiceChat} onClose={() => setPracticeChat(null)} />
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user