From bb67dad07a8c1540032e36f8ed10d76d2b186062 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 30 Jun 2026 15:07:24 -0500 Subject: [PATCH] feat(journey): edit/delete in TUI overlay and desktop star map TUI /journey gets d/e with confirm + $EDITOR; desktop gets a right-click context menu with inline edit modal. Both refresh the graph after mutation. Extract openInEditor into the shared TUI editor helper. --- .../src/app/starmap/node-context-menu.tsx | 159 ++++++++++++++++++ apps/desktop/src/app/starmap/star-map.tsx | 33 ++++ apps/desktop/src/hermes.ts | 32 ++++ ui-tui/src/components/journey.tsx | 127 +++++++++++++- ui-tui/src/lib/editor.ts | 25 ++- 5 files changed, 370 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/src/app/starmap/node-context-menu.tsx diff --git a/apps/desktop/src/app/starmap/node-context-menu.tsx b/apps/desktop/src/app/starmap/node-context-menu.tsx new file mode 100644 index 00000000000..dc7a1bece6c --- /dev/null +++ b/apps/desktop/src/app/starmap/node-context-menu.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react' + +import { Button } from '@/components/ui/button' +import { ConfirmDialog } from '@/components/ui/confirm-dialog' +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Textarea } from '@/components/ui/textarea' +import { deleteLearningNode, editLearningNode, getLearningNode } from '@/hermes' + +export interface NodeMenuTarget { + id: string + kind: 'memory' | 'skill' + label: string + x: number + y: number +} + +interface NodeContextMenuProps { + onChanged: () => void + onClose: () => void + target: NodeMenuTarget | null +} + +interface EditState { + content: string + id: string + label: string +} + +/** Right-click actions for a star-map node: edit (modal) or delete (confirm). */ +export function NodeContextMenu({ onChanged, onClose, target }: NodeContextMenuProps) { + const [editing, setEditing] = useState(null) + const [deleting, setDeleting] = useState<{ id: string; label: string } | null>(null) + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + const noun = target?.kind === 'memory' ? 'memory' : 'skill' + + const openEdit = async () => { + if (!target) { + return + } + + setLoading(true) + setError(null) + try { + const detail = await getLearningNode(target.id) + setEditing({ content: detail.content, id: target.id, label: target.label }) + onClose() + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } finally { + setLoading(false) + } + } + + const save = async () => { + if (!editing) { + return + } + + setSaving(true) + setError(null) + try { + const res = await editLearningNode(editing.id, editing.content) + if (!res.ok) { + throw new Error(res.message) + } + setEditing(null) + onChanged() + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } finally { + setSaving(false) + } + } + + const menuOpen = target && !editing && !deleting + + return ( + <> + {menuOpen ? ( + <> +
e.preventDefault()} /> +
+
{target.label}
+ + +
+ + ) : null} + + !value && !saving && setEditing(null)} open={Boolean(editing)}> + + + Edit {editing?.label} + +