add conversation tree#2
Open
Broderick-Westrope wants to merge 50 commits into
Open
Conversation
… summarisation model
…r handling, cycle detection, /clone registration
Chain treeParentID through the previous assistant message instead of the user message so multi-step loop iterations form a linear chain (U→A1→A2→A3) rather than siblings under the same user node. The sibling structure caused ancestor-path walks to miss earlier assistant messages, hiding their output in both the TUI and on session reload. Also prevent the message projector from regressing leaf_id when older messages receive metadata updates (cost, token counts) during streaming.
Only chain through lastAssistant when it is newer than lastUser (tool-call continuation within the same turn). When lastUser is newer (new turn), chain through the user message so it stays in the ancestor path.
Skip tool-call loop iterations (assistants whose treeParentID points to another assistant) from the tree display. They are part of the same logical response and not meaningful branch points. Their children still render at the correct depth.
…response Resolve the full continuation chain so the tree preview shows actual output text instead of the first tool call, and selecting an assistant navigates to the tail of the response to preserve all continuations in the ancestor path.
…d tool calls Move leaf_id advancement from the projector (direct DB write, invisible to TUI sync) into updateMessage via patch(), which publishes Session.Event.Updated so the TUI's branch filter stays current. Uses an in-memory guard to avoid redundant patches during streaming. Also tighten the hasToolCalls check to skip completed/errored tool parts. Compaction tails can contain already-processed tool calls that previously prevented the loop from exiting, causing a prefill error when the model received messages ending with an assistant.
Use positional comparison (findLastIndex) instead of lexicographic ULID comparison to determine if the last assistant has responded to the last user. After compaction reordering, the compaction user (new ULID) appears before old tail assistants (old ULIDs), making the ID comparison unreliable. Also remove the now-redundant finish-type check since hasToolCalls already covers the tool-calls case via status filtering. Add unit tests for filterCompacted + break condition covering compaction tails with continuations, completed tool calls, and ULID ordering.
The hasToolCalls filter correctly handles compaction tails (completed/errored tools are excluded), but during live streaming there's a window where finish is set to "tool-calls" before tool parts are materialized. Restore the explicit finish guard alongside the new status filter.
The inArray query works without session_id since ULIDs are globally unique, but adding the constraint follows the consistent pattern and enables the (session_id, time_created, id) composite index.
…anchTo A malformed or stale messageID from the client could corrupt the session's leafID, causing getAncestorPath to return an empty path. Also log a warning when partial summary/fromLeafID inputs are provided.
…ummary clone() was calling MessageV2.get() per message (2 queries each). For 100 messages that's 200 synchronous queries. streamBranch() does it in 2 bulk queries. Also log a warning when treeParentID mapping fails during clone. Same fix applied to BranchSummary.generate() which had the same N+1 pattern.
… fix error status - revert.ts: Change Effect.die to Effect.fail(NotFoundError) so the branch guard is a recoverable typed error instead of an unrecoverable defect. - message-v2.ts: Remove redundant second DB query in getAncestorPath() — the rows are already fetched and ordered, use rows[rows.length - 1]. - handlers/session.ts: Return 500 instead of 400 when branch summary generation fails (the request was valid, the server couldn't produce a result). - groups/session.ts: Align revert endpoint error type to ApiNotFoundError (consistent with the rest of the file; required by the handler fix).
… data Session.messages() was using the flat stream() which returns all messages across all branches. API consumers and internal callers (revert, compaction, summary) now get branch-filtered messages matching the active leafID.
…/expand, subtree delete, branch labels - Reverse sibling sort order (newest first) in tree view walk() - Add filterMode 'on-demand' to DialogSelect: / activates, Esc deactivates - Add collapse/expand with Left/Right arrow keys and ▸/▾ indicators - Add subtree deletion with d key, confirmation prompt, ancestor guard - Add branch labels with l key via DialogPrompt, stored on message schema - New removeSubtree method with single-transaction batch delete - New setLabel method with PATCH route for label persistence - Fix pre-existing memory leak: clean up store.part on message removal - Both Hono and HttpApi routes added with parity
Root cause: when filterActive was false, the filter input was not focused, so nothing in the dialog captured keystrokes — they leaked to the prompt input behind the dialog. This caused d/l keybinds to appear non-functional and typed characters to appear in the prompt. Fix: - Always focus the filter input on mount (even when inactive) - Hide cursor and text color when inactive so it appears unfocused - Prevent all non-escape keys from reaching the input when inactive - Gate onInput handler to reject text when filter is inactive - Walk up treeParentID chain for left-on-collapsed parent navigation to handle skipped continuation assistants
Focused TUI inputs consume character keys before DialogSelect's keybind matching in useKeyboard fires. Arrow keys (left/right) are not consumed by inputs, which is why collapse/expand worked. Fix: move d and l keybind logic from DialogSelect's keybind prop into DialogTree's own useKeyboard handler, which fires regardless of input focus state. Keep keybind entries in the prop array for footer hint display only (no-op onTrigger). Also: - Add selected() to DialogSelectRef for external keybind handlers - Revert always-focus approach (was causing blinking / cursor) - Revert input color changes (not needed without forced focus) - Remove catch-all preventDefault (not needed)
Opentui doesn't re-render visual tree when SolidJS signals change
inside a useKeyboard callback — the <Show when={editingLabel()}>
switch was setting the signal but not causing a visual update.
Fix: use DialogPrompt.show() for label editing and delete confirmation,
which calls dialog.replace() internally. This replaces the entire
dialog stack entry, which opentui handles correctly.
Removes: editingLabel signal, confirmDelete signal, Show nesting,
useKeyboard for y/n confirmation
Adds: DialogPrompt.show() calls in useKeyboard for l and d keys
- After label prompt Enter/Esc or delete confirm/cancel, reopen the tree dialog via reopenTree() instead of leaving the sub-dialog open - Delete keybind hint only shows in footer when the selected node is deletable (not on the current branch ancestor path) - Track selected option via onMove + signal for reactive keybind hints - Extract canDelete helper for reuse between hint and handler - Remove debug logging imports and code
The prompt textarea's onKeyDown handler fires even when blurred in opentui, causing up arrow to load prompt history while the tree dialog is open. Other dialogs (Commands, etc) don't have this issue because their filter input is always focused, capturing arrow keys. Fix: guard onKeyDown with dialog.stack.length > 0 check, matching the existing pattern in the blur effect at line 657.
…n-history # Conflicts: # packages/opencode/src/cli/cmd/run.ts # packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx # packages/opencode/src/cli/cmd/tui/routes/session/index.tsx # packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx # packages/opencode/src/config/keybinds.ts # packages/opencode/src/server/routes/instance/session.ts
…g system, update ACP agent test
When the continuation chain breaks (all assistant chunks get treeParentID pointing to the user message instead of chaining), the tree view rendered each chunk as a separate node. Now only the final assistant sibling is shown; earlier chunks are skipped but their children are still walked to preserve any branches.
…ith tree branching Revert cleanup deleted messages but left leafID pointing at a removed message, so the next user message's treeParentID referenced a ghost node and the entire conversation history disappeared. - cleanup now calls branchTo to reset leafID to the last surviving message - shellImpl re-reads session after cleanup to pick up the new leafID - Timeline dialog (<leader>g) now offers 'Branch from here' instead of 'Revert', wiring through to branchTo for proper tree navigation
…om bar The on-demand filter mode in DialogSelect had no activation mechanism — pressing / did nothing because filterActive was never set to true. Add useKeyboard handler in DialogSelect for / (activate) and Escape (deactivate) when filterMode is on-demand. Move tree keybind hints from the input placeholder to a new hints prop rendered in the bottom bar, matching the action footer pattern used by other dialogs.
Enter while filtering now deactivates filter mode (keeping results) instead of selecting the item. Escape while filtering clears the filter and returns to normal navigation instead of closing the dialog. Uses useBindings for Escape so it takes priority over the parent dialog's close binding.
…ring Escape now clears the input element's internal value so it's empty when filter is re-activated. When filtering, tree indentation, connectors, and fold markers are stripped — options display as a flat list with only the branch/compaction bullet markers preserved.
selectRef.filterActive is a plain getter, not a Solid signal, so createMemo couldn't track it. Use a filterQuery signal updated via the onFilter callback instead. Also notify onFilter when Escape clears the filter so titles revert to tree-prefixed form.
DialogTimeline now accepts leafID and walks the ancestor chain to show only messages on the current branch, matching the session view behavior. Legacy sessions without treeParentID fall back to showing all messages.
When branching from the root user message, onBranch now passes undefined (the root's treeParentID) instead of the message's own ID. branchTo accepts optional messageID and clears leafID to null when absent. The conversation view and timeline distinguish tree sessions with null leafID (show empty) from legacy sessions (show all) by checking whether any message has treeParentID. New messages submitted after branching before root become fresh roots with no parent, so the AI only sees the new message.
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.
No description provided.