Skip to content

feat: root-as-head stack rendering with tree expansion#70

Draft
comp615 wants to merge 7 commits intoblock:mainfrom
comp615:ccroom/stack-tree-rendering
Draft

feat: root-as-head stack rendering with tree expansion#70
comp615 wants to merge 7 commits intoblock:mainfrom
comp615:ccroom/stack-tree-rendering

Conversation

@comp615
Copy link
Contributor

@comp615 comp615 commented Feb 23, 2026

This PR description was generated by Amp.

Problem

The stack toggle in the main thread list groups handoff-related threads but has three issues:

  1. Unstable head: The most recently updated thread becomes the stack head, which shifts as new children are added or threads are resumed
  2. Flat expansion: Expanding a stack shows all other members sorted by date in a flat list, losing the tree structure
  3. Forks are invisible: Given root → [branch-a → leaf-a, branch-b], expansion shows a flat interleaved list with no indication of which branch a thread belongs to

Before (flat list)

leaf-b (Jan 5)          ← head (most recent, shifts unpredictably)
  leaf-a (Jan 4)        ← flat, no structure
  branch-b (Jan 3)
  branch-a (Jan 2)
  root (Jan 1)

After (tree)

root (Jan 1) [5 threads · active Jan 5]     ← stable identity, sorted by most recent activity
  ├── branch-b (Jan 3)
  ├── branch-a (Jan 2)
  │   └── leaf-a (Jan 4)
  └── leaf-b (Jan 5)

Screenshot (after): 9-thread stack expanded as an indented tree with depth-based connector lines:

Stack tree expansion

Solution

Commits follow dependency order:

  1. feat: root-as-head stack rendering with tree expansion — Core behavior change: root thread is now the stack head, lastActiveDate drives sort position, expanded stacks render as indented trees via StackTree.tsx, DFS-ordered descendants field replaces ancestors
  2. refactor: extract buildHandoffGraph to shared/utils and remove deprecated ancestors alias — Deduplicates the handoff graph-building logic (threadMap + parentToChildren + childToParent) into buildHandoffGraph() in shared/utils.ts, used by both threadStacks.ts (client) and threadChain.ts (server). Removes the deprecated ancestors alias from ThreadStack.

Key changes

Data model (shared/types.ts):

  • ThreadStack.ancestorsThreadStack.descendants (tree-ordered via DFS)
  • Added ThreadStack.lastActiveDate (most recent lastUpdatedDate across all members)

Shared utility (shared/utils.ts):

  • New buildHandoffGraph(threads){ threadMap, childToParent, parentToChildren } — extracted from duplicated code in threadStacks.ts and threadChain.ts

Stack building (src/utils/threadStacks.ts):

  • Head selection: root (no parent in stack) instead of most-recently-updated
  • descendants built via DFS from root's children, sorted by lastUpdatedDate desc at each level
  • Entries re-sorted by lastActiveDate desc so active stacks float to the top
  • New getLastActiveThread() helper for kanban column placement

Tree rendering (src/components/ThreadList/StackTree.tsx):

  • New recursive component that walks topology.parentToChildren from root
  • Each node renders as a ThreadRow with stackDepth prop for indentation
  • Children sorted by lastUpdatedDate desc for deterministic ordering

ThreadRow (src/components/ThreadList/ThreadRow.tsx, types.ts):

  • Added stackDepth prop for depth-based indentation (marginLeft: (depth-1) * 16px)
  • Added displayLastUpdated prop so the head row shows lastActiveDate (most recent activity) rather than the root's own last update

Kanban + Detail Card views: Updated to use descendants, topology, and getLastActiveThread() for correct column placement and tree-structured expansion.

Server (server/lib/threadChain.ts): Now uses buildHandoffGraph() from shared utils instead of inline graph construction.

React Strict Mode Fix

During development, discovered a bug where mutable Set objects were passed as props and mutated during render in StackTree. React strict mode double-renders components, so the second render pass would see the already-mutated Set and skip all nodes. Fix: create new Set instances instead of mutating shared state.

Dependencies

Depends on PR #59 (ccroom/fork-topology-data-model) which introduced ThreadStackTopology and fork-aware chain traversal.

Sibling PR: PR #69 (ccroom/full-tree-chain-display) — full-tree rendering in the chain discovery panel.

Verification

  • pnpm typecheck passes (0 errors)
  • All 256 tests pass (pnpm test)
  • pnpm lint clean

AI Tool Validation

Copy and paste this prompt into your AI tool (Amp, Cursor, etc.):

Checkout comp615/thread-manager-for-amp branch ccroom/stack-tree-rendering and verify:

1. In src/utils/threadStacks.ts, confirm buildThreadStacks() uses the root thread
   (member with no parent) as the stack head, not the most recently updated thread.

2. In src/utils/threadStacks.ts, confirm entries are re-sorted by lastActiveDate
   (max lastUpdatedDate across all stack members) so active stacks float to top.

3. In src/components/ThreadList/StackTree.tsx, confirm the recursive tree renderer
   walks topology.parentToChildren from rootId and passes stackDepth to ThreadRow
   for indentation.

4. In shared/utils.ts, confirm buildHandoffGraph() builds threadMap, childToParent,
   and parentToChildren from threads' handoffParentId field.

5. Run `pnpm test` and confirm all tests pass.

comp615 and others added 7 commits February 22, 2026 13:35
- Thread.handoffChildIds: string[] replaces singular handoffChildId
  (legacy field kept for backward compat, set to last child seen)
- ThreadChain.descendantsTree: recursive ThreadChainNode[] for branching
  (flat descendants[] kept for UI compat)
- ThreadStack.topology: adjacency maps (rootId, childToParent, parentToChildren)
- Fix threadChain.ts: was reading thread.relationships which getThreads()
  never populates; now uses handoffParentId/handoffChildIds
- All new fields are optional/additive; no UI changes needed

Amp-Thread-ID: https://ampcode.com/threads/T-019c869d-7987-71df-99a5-e64a57114bee
Co-authored-by: Amp <amp@ampcode.com>
- threadStacks.test.ts: assert topology adjacency maps on 'multiple children'
  and 'fan-out' tests (rootId, parentToChildren forks, childToParent edges)
- threadChain.test.ts: new test file with 6 tests covering linear chain,
  fork descendants tree, mid-chain queries, unknown thread, and
  flat-vs-tree consistency

Amp-Thread-ID: https://ampcode.com/threads/T-019c869d-7987-71df-99a5-e64a57114bee
Co-authored-by: Amp <amp@ampcode.com>
- Remove ThreadChain.descendants (flat ChainThread[])
- Make ThreadChain.descendantsTree required (was optional)
- Drop flattenTree() from threadChain.ts (no longer needed)
- Update ThreadChainContent.tsx: recursive DescendantNodes renderer
  with depth-based indentation and GitFork icon for branches
- Update useThreadDiscovery.ts: chainCount uses descendantsTree.length
- Update tests to assert on tree structure only

Amp-Thread-ID: https://ampcode.com/threads/T-019c869d-7987-71df-99a5-e64a57114bee
Co-authored-by: Amp <amp@ampcode.com>
The plural handoffChildIds (added in PR block#59) fully replaces it.
Removed from Thread interface and threadCrud assignment.

Amp-Thread-ID: https://ampcode.com/threads/T-019c8702-28e9-73ba-b5e4-496c1dc11651
Co-authored-by: Amp <amp@ampcode.com>
- buildThreadStacks() now uses root (no parent) as head instead of most-recent
- Added lastActiveDate to ThreadStack for sort ordering
- Added descendants field (DFS tree-ordered) alongside deprecated ancestors
- Re-sort entries by lastActiveDate so active stacks float to top
- New StackTree component renders expanded stacks as indented tree via topology
- ThreadRow supports stackDepth prop for depth-based indentation
- Stack head Updated column shows most recently active thread's time
- KanbanView uses getLastActiveThread() for column placement and time display
- Removed mutable visited Set from StackTree render (React strict mode fix)
- All three views (table, kanban, cards) use tree rendering with depth
- 258 tests pass, typecheck + lint clean

Amp-Thread-ID: https://ampcode.com/threads/T-019c8b24-9f53-7038-a667-56ab9bee122f
Co-authored-by: Amp <amp@ampcode.com>
…ated ancestors alias

- Extract duplicated graph-building logic (threadMap, childToParent,
  parentToChildren) from threadChain.ts and threadStacks.ts into a
  shared buildHandoffGraph() helper in shared/utils.ts
- Remove deprecated 'ancestors' field from ThreadStack type (was an
  alias for 'descendants')

Amp-Thread-ID: https://ampcode.com/threads/T-019c8b82-ed31-7099-b07a-2fc28341ad52
Co-authored-by: Amp <amp@ampcode.com>
Copy link
Collaborator

@jom-sq jom-sq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i technically put the newest one on top on purpose at the time but yeah im fine with this!

@comp615
Copy link
Contributor Author

comp615 commented Feb 23, 2026

i technically put the newest one on top on purpose at the time but yeah im fine with this!

@jom-sq yeah this one I feel the most awkward / unsure about. I think it both makes sense to have the newest on top, but also to see the tree to follow the thread as it were...maybe we chat live on this I do not feel as strongly about this one since the view vs sort is quirky

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.

2 participants