chore(shared-model): bring stranded node-UI demo into the feature branch (merge-order fix)#1162
Conversation
…'s shared AI model (demo MVP)
Adds a self-contained, self-discovering chat modal that lets a context-graph
MEMBER chat with a curator's shared AI model. The completion runs ON THE
CURATOR'S node; the member node only proxies via its existing OpenAI-compatible
route POST /api/context-graph/:id/model/v1/chat/completions.
- api.ts: new invokeSharedModelChat(cg, messages, { model? }) helper. Uses the
existing authHeaders() machinery (same-origin Bearer NODE token). Returns
choices[0].message.content; on a non-2xx it unwraps the OpenAI-shaped error
body { error: { message } } (an object — the generic post() helper would
stringify it to [object Object]) and throws Error(error.message) so denials
(403 "requester is not a member…", quota) surface as readable text.
- CuratorModelChatModal.tsx: new modal mirroring ShareProjectModal's chrome
(v10-* classes, useModalDismiss). Cross-CG + self-discovering: lists the
member's CGs via useMyContextGraphs, reads each grant via getModelGrant, keeps
the enabled ones in a "<CG name> · <modelId>" dropdown (friendly empty state
when none). In-memory chat (list + textarea + Send), a "thinking…" state while
awaiting the single buffered reply, an inline error bubble for denials, and a
header banner indicating the chat runs on the curator's shared model. No
streaming, no tool-calls, no persistence.
- Header.tsx: single global launch point — a chat-bubble icon button in the
header actions opens the modal (cross-CG, so global rather than per-project).
The modal is mounted only while open so its discovery hook doesn't poll on
every Header mount.
- Tests: invoke-shared-model-chat.test.ts pins the 200→content and
403→error.message contract against a mocked fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| setInput(''); | ||
| setSending(true); | ||
| try { | ||
| const reply = await invokeSharedModelChat(selectedCg, outbound, { |
There was a problem hiding this comment.
🔴 Bug: This request is not scoped to the currently selected context graph. If the user switches the dropdown while invokeSharedModelChat() is in flight, the selectedCg effect clears the session, but the old response/error still appends into the newly selected graph's conversation. Capture the graph/request id before awaiting and ignore late completions when selection changes, or disable graph switching until the send settles.
| * persistence. | ||
| */ | ||
| export function CuratorModelChatModal({ open, onClose }: CuratorModelChatModalProps) { | ||
| const { myCgs, cgsLoading } = useMyContextGraphs(); |
There was a problem hiding this comment.
🟡 Issue: useMyContextGraphs() applies the local "hidden from sidebar" filter. Using it here means hiding a context graph in the sidebar also makes its shared model undiscoverable, even though the user is still a member and this modal is meant to be a cross-context-graph surface. Consider sourcing membership without the hidden-ids filter (or add an opt-out for this hook) for the model picker.
Why
The stacked PRs were merged in the wrong order:
feat/llm-sharing-module✓feat/shared-model-m1-fixesinstead of being retargeted tofeat/llm-sharing-module.Net effect: the node-UI demo is stranded in
feat/shared-model-m1-fixesand never reached the feature branch (feat/llm-sharing-module, the base of #1157).What this does
Merges
feat/shared-model-m1-fixes→feat/llm-sharing-module. The M1 fixes are already present in the base (via #1158), so the only delta is the UI demo:packages/node-ui/src/ui/components/Modals/CuratorModelChatModal.tsx(new)packages/node-ui/src/ui/components/Shell/Header.tsx(launch button)packages/node-ui/src/ui/api.ts(invokeSharedModelChat)packages/node-ui/test/invoke-shared-model-chat.test.ts(new)Verified locally: clean merge, no conflicts, node-ui-only diff. Does not touch #1157→main.
🤖 Generated with Claude Code