diff --git a/client/package.json b/client/package.json index 45f6c4632..9502bc439 100644 --- a/client/package.json +++ b/client/package.json @@ -32,6 +32,7 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", + "marked": "^17.0.5", "partysocket": "^1.1.16", "solid-js": "^1.9.0", "solid-sonner": "^0.2.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index de1748e4a..e335571f5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,6 +20,8 @@ import ClaudeTranscriptDialog from "./ClaudeTranscriptDialog"; import ModalDialog, { refocusTerminal } from "./ModalDialog"; import Dialog from "@corvu/dialog"; import EmptyState from "./EmptyState"; +import PlanPane from "./PlanPane"; +import Resizable from "@corvu/resizable"; import CloseConfirm, { type CloseConfirmTarget } from "./CloseConfirm"; import { createCommands } from "./commands"; import { exportSessionAsPdf } from "./exportSessionAsPdf"; @@ -35,6 +37,7 @@ import { useShortcuts } from "./useShortcuts"; import { useSubPanel } from "./useSubPanel"; import { useColorScheme } from "./useColorScheme"; import { useTips } from "./useTips"; +import { usePlans } from "./usePlans"; const App: Component = () => { const { preferences, updatePreferences } = useServerState(); @@ -107,6 +110,15 @@ const App: Component = () => { const [searchOpen, setSearchOpen] = createSignal(false); createEffect(on(store.activeId, () => setSearchOpen(false), { defer: true })); + const { + activePlanPath, + planName, + planContent, + isPlanContentLoading, + addFeedback, + removeFeedback, + } = usePlans({ activeMeta: store.activeMeta }); + const { initTipTriggers, startupTips, setStartupTips } = useTips(); initTipTriggers({ terminalIds: store.terminalIds }); @@ -375,46 +387,121 @@ const App: Component = () => { /> {/* min-w-0: override flex min-width:auto so terminal area shrinks below canvas intrinsic size */}
-
+ + Connecting... +
+ } + > + + void session.handleRestoreSession()} + /> + + + {(id) => ( + + void crud.handleCreateSubTerminal(parentId, cwd) + } + onCloseTerminal={closeTerminal} + activeMeta={store.activeMeta()} + scrollLockEnabled={scrollLock()} + /> + )} + + +
+ } > - - Connecting... + {/* Plan pane open — resizable horizontal split: terminal left, plan right */} + + +
+ + Connecting... +
+ } + > + + {(id) => ( + + void crud.handleCreateSubTerminal(parentId, cwd) + } + onCloseTerminal={closeTerminal} + activeMeta={store.activeMeta()} + scrollLockEnabled={scrollLock()} + /> + )} + +
- } - > - - void session.handleRestoreSession()} - /> - - - {(id) => ( - - void crud.handleCreateSubTerminal(parentId, cwd) + + + + { + const id = store.activeId(); + if (id) { + // \r = Enter in PTY (carriage return, not newline) + void client.terminal.sendInput({ + id, + data: text + "\r", + }); } - onCloseTerminal={closeTerminal} - activeMeta={store.activeMeta()} - scrollLockEnabled={scrollLock()} - /> - )} - - - + }} + /> + + + diff --git a/client/src/PlanPane.tsx b/client/src/PlanPane.tsx new file mode 100644 index 000000000..124b23814 --- /dev/null +++ b/client/src/PlanPane.tsx @@ -0,0 +1,310 @@ +/** PlanPane — renders a plan file as full markdown with text-selection commenting. + * + * Select any text in the rendered plan to leave inline feedback. Feedback is + * written back to the plan file as blockquotes referencing the selected text, + * and rendered inline within the markdown (not in a separate section). + * + * Composed from independent modules: + * - planMarkdown.ts — markdown rendering + line annotation + feedback restyling + * - usePlanChangeHighlight.ts — change detection + DOM highlight animation */ + +import { + type Component, + Show, + createSignal, + createMemo, + onCleanup, +} from "solid-js"; +import { toast } from "solid-sonner"; +import type { PlanContent } from "kolu-common"; +import { renderPlanMarkdown, findLineFromNode } from "./planMarkdown"; +import { usePlanChangeHighlight } from "./usePlanChangeHighlight"; + +/** Selection popover state. */ +interface SelectionState { + text: string; + /** Source line number from the markdown file (via data-line attribute). */ + sourceLine: number; + top: number; + left: number; +} + +/** Floating popover that appears on text selection. */ +const SelectionPopover: Component<{ + selection: SelectionState; + onSubmit: (selectedText: string, sourceLine: number, comment: string) => void; + onDismiss: () => void; +}> = (props) => { + const [comment, setComment] = createSignal(""); + + function handleSubmit() { + const text = comment().trim(); + if (!text) return; + props.onSubmit(props.selection.text, props.selection.sourceLine, text); + setComment(""); + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleSubmit(); + } + if (e.key === "Escape") { + props.onDismiss(); + } + } + + return ( +
+
+ Re: «{props.selection.text.slice(0, 60)} + {props.selection.text.length > 60 ? "…" : ""}» +
+