Skip to content

Add conversation history drawer and per-conversation code state#45

Open
jlee-kitware wants to merge 24 commits into
masterfrom
conversation-history-panel
Open

Add conversation history drawer and per-conversation code state#45
jlee-kitware wants to merge 24 commits into
masterfrom
conversation-history-panel

Conversation

@jlee-kitware

Copy link
Copy Markdown
Collaborator

History panel (cherry-picked from #35, adapted to current master)

  • New 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.
  • App now uses SinglePageWithDrawerLayout; the panel lives in the left drawer (bound to the existing main_drawer, collapsed by default). The toolbar nav icon toggles it (previously hidden).
  • Adds controllers navigate_to_conversation / toggle_favorite_conversation and state history_sort_order / history_filter_mode / favorited_conversations.

Per-conversation code state (new behavior)

Previously generated_code, code_history, and code_history_pos were 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:

  • Navigating away saves the active (possibly hand-edited) code plus its undo/redo history.
  • Arriving restores that pair's saved state, seeding from the original generated code on first visit.
  • Content-hash keying is robust to index shifts (new generations) and conversation reloads.

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.
  • App instantiates and builds the UI with the drawer layout.
  • Functional check: edit code in pair A, navigate to B, return to A -> A's edit is restored and B keeps independent code/history.

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.
@jlee-kitware jlee-kitware mentioned this pull request Jun 26, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant