Add conversation history drawer and per-conversation code state#45
Open
jlee-kitware wants to merge 24 commits into
Open
Add conversation history drawer and per-conversation code state#45jlee-kitware wants to merge 24 commits into
jlee-kitware wants to merge 24 commits into
Conversation
History panel (cherry-picked and adapted from PR #35): - New ui/layout/conversation_history.py: a browsable list of past conversation pairs with newest/oldest sort and a favorites filter. Clicking a card jumps to that conversation; a heart toggles favorite. - Switch the app to SinglePageWithDrawerLayout and host the panel in the left drawer (bound to existing main_drawer, collapsed by default); the toolbar nav icon now toggles it instead of being hidden. - Controllers navigate_to_conversation and toggle_favorite_conversation, plus history_sort_order / history_filter_mode / favorited_conversations state. Per-conversation code state (new): - generated_code, code_history and code_history_pos were global singletons, so editing code in one conversation and navigating away discarded the edit and bled undo/redo history across conversations. - Add a server-side store keyed by a content hash of each pair. Navigating away saves the active (possibly hand-edited) code and history; arriving restores it, seeding from the original generated code on first visit. Keying by content hash is robust to index shifts and conversation reloads.
Previously code_history only recorded generations and runs, so undo/redo could not step through manual edits made in the editor between runs. Add a debounced @change("generated_code") handler: after a short typing pause it records the current code as a snapshot, coalescing a burst of keystrokes into a single undo step. A content guard pushes a snapshot only when generated_code differs from the active snapshot (code_history[pos]), so programmatic updates (generate, run, undo/redo, per-conversation restore) are ignored and cannot create a feedback loop. Loads are skipped via the existing _conversation_loading flag. Snapshots land in the active conversation's code_history, which is already persisted per pair on navigate-away, so edit-level undo is per-conversation for free.
undo_code/redo_code re-executed the restored version through execute_with_renderer on every step, so each undo/redo replayed the script and rebuilt the scene. Step the editor through code versions only; Run remains the way to execute. After undo/redo generated_code equals the active snapshot, so the debounced edit-snapshot guard correctly skips it.
Track rendered_code (the code shown in the 3D view) and set it on each successful execute_with_renderer. The Run button is disabled while generated_code equals rendered_code, so it lights up only when the editor has something new to apply. Pairs with editor-only undo/redo: stepping to a version that differs from the rendered scene now re-enables Run. Failed runs leave rendered_code unchanged so the user can retry.
Set the conversation history drawer to temporary so it overlays the scene instead of pushing and resizing the render view. This removes the resize churn (and the benign ResizeObserver console notice) that the persistent drawer caused when toggled. The drawer still binds to main_drawer and is toggled by the toolbar nav icon; selecting a conversation now dismisses the overlay.
The drawer from SinglePageWithDrawerLayout defaults to permanent=True, so adding temporary left both flags set and permanent won, the drawer kept reserving layout space and resizing the render view. Clear permanent so the drawer renders as a true temporary overlay (:permanent="false" temporary) and toggling it no longer resizes the view.
Reorder the left column so the generated-code panel is on top and the prompt input (model chips, query box, navigation, Generate) sits at the bottom, matching the familiar bottom-input chat layout. Heights are unchanged (code h-75, prompt h-25); only the order and top margins swap.
Put a New conversation button at the top of the history drawer (Claude-style). It starts a fresh prompt entry: clears the query, editor, explanation, and undo history, enters new-entry mode, and dismisses the overlay, while leaving the conversation history intact. Remove the prev/next arrows that flanked the query box. Browsing now lives in the history drawer (click any turn to jump), and the old right arrow's "new entry" role is replaced by the drawer's New conversation button. The prompt area is now just the query field and Generate, a cleaner chat-like input. The navigate_conversation_left/right controllers remain available for future keyboard shortcuts.
…erver - Drawer is no longer temporary: it is a user-controlled persistent drawer toggled by the toolbar nav icon, staying open until toggled, and no longer auto-closes on selection or New. - Shrink the New conversation button (small, tonal) so it is less bulky. - Silence the benign Chrome "ResizeObserver loop completed with undelivered notifications" notice via a targeted client.Script that ignores only that specific message (and the older "loop limit exceeded") on the error event and console.error, leaving all other errors intact. This notice comes from a ResizeObserver callback (render view / Vuetify components sizing on connect and layout) deferring work to the next frame, not from the drawer, which is why it appeared even with the overlay.
The filled tonal block rendered as a large blue bar. Make it a subtle compact text button (small, density compact, text variant, left-aligned, normal case) so it reads like a quiet sidebar action rather than a banner. Also drop the h-100/flex-column forcing on the panel so its contents sit top-aligned naturally and the list sizes to content (drawer scrolls).
Replace the standalone New conversation button with a compact + icon button sitting alongside the sort and favorites toggles in the panel header, and rename "Conversation History" to "Recents". Same start_new_conversation action, less vertical space, cleaner header.
- Open the Recents drawer by default (main_drawer = True); still toggled by the toolbar nav icon. - Disable the New (+) button when already on a fresh new entry (conversation_index >= conversation_navigation.length), which includes the first-time empty state. New is enabled once you are viewing a generated conversation, so it can branch to a new one; clicking it when already on a blank, ungenerated entry would be a no-op.
Refinement prompts previously regenerated from the model's own previous output, so manual edits in the editor were invisible to the next generation and got discarded (it replaced rather than mutated). Before each refinement query, sync the live editor code (minus the display-only renderer banner) into the <code> block of the most recent assistant turn, so the model mutates what is actually on screen, including hand edits. Guarded to the most recent turn only (not when viewing history or on a new entry), and a no-op when there is no conversation yet.
New conversation cleared the UI but left app.prompt_client.conversation intact, so a "new" conversation silently continued the previous LLM context. Clear the client conversation and its file pointer (otherwise the next query reloads the old file into the fresh conversation), plus the displayed thread, per-conversation code state, and editor, so the new conversation truly starts unrelated to the old one. The prior conversation is discarded from the UI; preserving multiple conversations is the separate sessions model.
Code changes came from two sources (manual edits and LLM generations) and were segmented per turn by a slot layer that swapped code_history on navigation, so the two could clobber each other and edits could be lost. Collapse to a single per-conversation code timeline. Every LLM generation and every manual edit appends a labeled version to one code_history (with a parallel code_history_labels recording origin: the prompt text, "Manual edit", or "Run"). Forward/back (undo/redo) steps through the whole timeline regardless of what produced each version. Turns are anchored to the timeline via per-turn checkpoints (record_turn_checkpoint), so jumping to a turn moves to the version that turn produced; edits made afterward remain further forward on the same timeline and are never discarded. Removes the per-turn slot machinery (_conversation_code_states, _code_states, _pair_key, save_current_code_state) and the save-on-navigate calls. Loaded conversations seed their versions on first visit.
Introduce a sessions layer so the app holds several conversations the user can switch between, instead of one live thread that New wiped. A session bundles a conversation's full state: the LLM message context, the unified code-version timeline (history + labels + position), the per-turn checkpoints, and metadata (title, timestamps, pinned). Exactly one session is active; switching captures the live state into the current session and loads the target, and New archives the current session and starts a fresh one so the rest of the list is preserved. - New controllers/sessions.py: store, capture/restore, new_session, switch_session, touch_current_session, toggle_pin_session, plus ensure_session/refresh_sessions_list. Title derives from the first user prompt; the drawer list sorts pinned-first then most-recent. - initializer.py seeds current_session_id and sessions_list. - vtk_prompt_ui.py creates the initial session at startup, repoints the "+" button to sessions.new_session (archive instead of wipe), and registers switch_session and toggle_pin_session. - generation.py calls touch_current_session after each turn (main and mcp-retry paths) so titles and timestamps update. - conversation_history.py drawer now lists conversations (pin toggle, clickable title, active highlight) rather than the turns of one thread. In-memory only; disk persistence and rename/delete follow.
Move the send action into the prompt field as a small circular arrow (Claude-style) instead of a full-width button below it. The arrow lights up only when there is a prompt to send and is disabled while loading or when a cloud model lacks an API key. The textarea now fills the prompt zone since the button no longer sits beneath it.
Each conversation with content is written as a JSON file under the per-user config dir (~/.config/vtk-prompt/sessions). On startup the app loads all saved sessions, repopulates the Recents drawer, and reopens the most recently updated conversation (state only, no render, since the window is not ready yet). Empty conversations are not written.
…order-on-select Address four issues with the sessions drawer: - Show the active conversation's follow-up prompts: each turn renders as a clickable sub-item under the active conversation, jumping to that point in the thread (was invisible after the drawer switched to listing conversations rather than turns). - Selecting a conversation no longer reorders the list: capture no longer bumps "updated"; only an actual new turn does, so recency reflects use rather than mere viewing. - Restore the sort-order toggle (newest/oldest) and favorites filter, now operating on conversations; refresh_sessions_list honors both, wired via a @change handler. - Fix the send arrow being clipped: the prompt wrapper forced height 100%, overflowing the short card past the chips row and cutting off the arrow. Let the textarea size naturally and center the arrow.
Replace the favorites flag+filter with a pin that floats conversations to the top (pinned first, then by sort order), which keeps important conversations reachable without a separate filter mode. Each conversation row gets a "..." menu with Pin/Unpin, Rename, and Delete; the favorites filter button is removed and the sort toggle stays. - sessions.py: refresh sorts pinned-first; add rename_session (ignores an empty title, persists) and delete_session (removes the file and, if the deleted one was active, opens the next most recent or a fresh session). - Rename and Delete each use a small dialog; controllers confirm_rename_session and confirm_delete_session apply them from dialog state. - initializer.py: dialog state. vtk_prompt_ui.py: the view-change handler now watches only the sort order.
…tion The sent prompt used to linger in the input box, and browsing turns refilled the box with that turn's prompt. Separate the two concerns: the input box (query_text) clears on send, while a new current_prompt holds the prompt to display inline above the explanation, Claude-style. - generation.py captures the prompt into current_prompt and clears query_text as soon as the prompt is captured (before the network call). - conversation._process_conversation_pair sets current_prompt for the viewed turn instead of refilling the input box. - sessions reset clears current_prompt; load_session restores it from the last turn alongside the explanation. - The Explanation panel now renders the prompt (with an account icon) above the explanation text rather than a single read-only textarea.
Closed
Replace the single-turn "Explanation" pane with a running "Conversation" transcript that lists every turn (prompt + response) for the active conversation. The current turn is highlighted, and clicking a turn revisits that step (jumps the code timeline and re-renders), which is more intuitive than the per-turn list that previously lived in the Recents drawer. - build_conversation_navigation enriches each pair with a cleaned prompt and the parsed explanation, so the transcript template stays simple. - content.py: the bottom-right pane is now the transcript (with a pending turn shown while a response generates). - conversation_history.py: the Recents drawer lists conversations only; the per-turn sub-list moved into the transcript.
A turn's checkpoint anchored the code version at generation time, so clicking a turn reverted to its first iteration and manual edits made afterward (which live further along the shared timeline) had to be reached by stepping forward. Jump instead to the last version belonging to the turn: the position just before the next turn's anchor, or the end of history for the most recent turn. Earlier turns still show their own version; the latest turn now restores its newest edit.
The stored conversation interleaves a real prompt with the model's tool calls, tool results, and any retry/format messages before the final answer. Rework build_conversation_navigation to delimit turns by real prompts, pair each with its final response, and collect everything in between as that turn's trace. This also fixes latent mis-pairing whenever tools or retries were used (previously a turn could pair to an empty intermediate message or fragment across retries). Each turn in the Conversation transcript gets a collapsed "Show work" panel (tool name + arguments + result, plus any retries/attempts) when it has one; turns without tool use show nothing extra. The prompt row remains the click target for revisiting a turn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
History panel (cherry-picked from #35, adapted to current master)
conversation_history.py: browsable list of past conversation pairs with newest/oldest sort and a favorites filter. Click a card to jump to that conversation; heart toggles favorite.SinglePageWithDrawerLayout; the panel lives in the left drawer (bound to the existingmain_drawer, collapsed by default). The toolbar nav icon toggles it (previously hidden).navigate_to_conversation/toggle_favorite_conversationand statehistory_sort_order/history_filter_mode/favorited_conversations.Per-conversation code state (new behavior)
Previously
generated_code,code_history, andcode_history_poswere global singletons. Editing code in one conversation then navigating away discarded the edit (navigation re-derived code from the pair's original assistant message), and undo/redo history bled across conversations.Now there is a server-side store keyed by a content hash of each pair:
Triage note
This supersedes the relevant parts of #35. #9 (retries) is already covered by master's client-side retry logic; #36 (code component) is superseded by the merged Monaco editor.
Verification
flake8 src/clean; all touched files byte-compile.