* fix(team): propagate cross-provider member args to lead subprocess
When a team has an anthropic lead and codex teammates, the lead was
launched without --settings {"codex":{"forced_login_method":"chatgpt"}}.
buildInheritedCliFlags in the lead had nothing to pass to the codex
teammate, which started without knowing the required auth method and
crashed with "no CODEX_API_KEY or OPENAI_API_KEY configured".
Adds buildCrossProviderMemberArgs that collects provider launch args for
member providers that differ from the primary, and merges them into the
lead's launch args so they propagate to teammates via buildInheritedCliFlags.
* fix: log cross-provider env resolution failures instead of silently swallowing
Final step of the virtualization plan. Turns the virtualized render
path on in production behind a row-count threshold, and adds regression
tests covering every gate.
- `VIRTUALIZATION_ROW_THRESHOLD = 60`. Short lists stay on the direct
render path (no wrapper, no position: absolute, no measurement
churn). Above the threshold the virtualizer takes over. Threshold is
sized so conversations under ~one session of activity don't pay the
virtualization cost; it activates once scrolling through a longer
history.
- `shouldVirtualize` now requires `renderRows.length >= threshold` in
addition to the existing opt-in and scroll-ref checks.
- `MessagesPanel` opts into virtualization for every layout it wires
(inline / sidebar / bottom-sheet). The internal threshold then
decides when to actually enable it, so callers don't need per-layout
heuristics.
- Tests: adds a new `ActivityTimeline virtualization threshold` block
covering (a) below-threshold list stays on the direct path,
(b) no viewport → direct path regardless of count, (c) above
threshold + viewport with `virtualizationEnabled` flips to the
virtualized render path (simulated by clicking "show all" past
pagination).
With this in, #70 → #74 combine to deliver:
- correct IntersectionObserver roots in scroll containers
- atomic render rows with stable keys
- windowed rendering with DOM-measured scrollMargin and measureElement
- auto-on when the cost of direct rendering actually shows up
Fifth step of the virtualization plan. Two small, coupled changes that
make the virtualized path stable without a merged-ref helper.
- Attach `rowVirtualizer.measureElement` to the existing virtualizer
wrapper div. Because the wrapper carries no padding or margin, its
bounding box matches the inner row, so the observer ref (which stays
on the inner AnimatedHeightReveal node) and the measure ref (on the
outer wrapper) address the same effective height. No merged ref
callback is needed.
- Suppress mount-based entry animation inside the virtualized path.
The virtualizer mounts and unmounts rows as the user scrolls them in
and out; without this, the "new item" fade would replay every time
an older row re-entered the viewport. `renderTimelineRow` now takes
an optional `suppressEntryAnimation` flag and forwards `isNew=false`
to both `LeadThoughtsGroupRow` and `MemoizedMessageRowWithObserver`
when set. The direct render path is unchanged.
Still dormant in this release — `viewport.virtualizationEnabled` stays
false at every call site. PR #6 adds the threshold gate, tests, and
opt-in wiring.
Fourth step of the virtualization plan. Adds `useVirtualizer` wiring
with a DOM-measured `scrollMargin`, gated behind
`viewport.virtualizationEnabled`. Dormant in this release — no caller
flips the flag yet — so behavior is unchanged.
- Imports `useVirtualizer` from `@tanstack/react-virtual`. Fixed
per-kind estimates (`ROW_SIZE_ESTIMATES`) drive `estimateSize`. Keys
come from `row.key`, so row identity matches the renderRows model.
- `shouldVirtualize` requires all of: contract says enabled, a scroll
element ref is present, and there is at least one row. Otherwise the
render falls back to the direct `renderRows.map(...)` path from PR
#72.
- Measures `scrollMargin` via `ResizeObserver` on both the scroll
element and the timeline root, plus `scroll` and `resize` listeners,
all rAF-batched. Avoids hand-summed heights that drift when
composer/status/padding change.
- Virtualized path renders an absolute-positioned list inside a sized
container (`height = getTotalSize()`). `translateY` subtracts
`scrollMargin` so rows align to the timeline's own origin rather
than the scroll container's top.
This PR intentionally does *not* enable `measureElement` (PR #5) or
flip `virtualizationEnabled` for any layout (PR #6) — both rely on
this wiring landing first.
Third step of the virtualization plan. Pure refactor — no UI change, no
virtualization yet. Prepares the timeline for row-level windowing.
- Introduces `TimelineRow`, a discriminated union of `session-separator`,
`lead-thought-group` (pinned and non-pinned), `compaction-divider`,
and `message-row`. Each row maps 1:1 to a single visual element.
- Adds a `renderRows` useMemo that walks `timelineItems` once and emits
atomic rows, hoisting session separators out of the Fragment bundle
that used to pair them with their owning item. This is the shape a
windowing layer needs: each row measurable and addressable
independently.
- Extracts a `renderTimelineRow(row)` helper that switches on `row.kind`
and returns the same JSX the previous inline render produced. Logic
per kind is identical — keys, memoization, collapse props, pinned
thought "live" semantics — so there is no visual diff.
- The render body collapses from two blocks (pinned + `.slice().map()`)
into a single `renderRows.map(renderTimelineRow)` call.
Follow-ups will virtualize `renderRows` with measured row heights and
tighten observer/animation wiring; pagination, collapse state, zebra
striping, and `groupTimelineItems` are untouched.
Second step of the virtualization plan. No virtualization yet. This PR
makes IntersectionObserver-based visibility tracking correct inside
scroll containers (sidebar, bottom-sheet), which is a prerequisite for
virtualizing the timeline.
- Introduces `TimelineViewport` — a grouped contract passed as a single
`viewport` prop on `ActivityTimeline`. Holds `scrollElementRef`,
`observerRoot`, `scrollMargin`, and `virtualizationEnabled`.
- `MessageRowWithObserver` and `LeadThoughtsGroupRow` now create their
`IntersectionObserver` with `root = observerRoot?.current ?? null`
instead of defaulting to the document viewport. Unread marking now
fires when rows enter their real scroll parent.
- `MessagesPanel` resolves the active scroll owner from `position`
(inline from parent ref, sidebar from `sidebarScrollRef`, bottom-sheet
from `bottomSheetScrollRef`) and passes it into ActivityTimeline.
- Tests: stubs `IntersectionObserver` to capture `options.root` and
asserts null when no viewport is passed, and the provided element when
`viewport.observerRoot` is set.
`scrollMargin` and `virtualizationEnabled` are included in the contract
but not consumed yet — they land in follow-up PRs (#4/#5).
- Introduced caching mechanism with expiration for message feeds to improve performance.
- Added logging for cache expiration events to aid in debugging.
- Updated MessagesPanel to reopen search bar when participant filters are active.
- Added test cases for handling tmux server errors and message panel behavior with filters.
First step toward virtualizing ActivityTimeline. Makes the real scroll
container observable per layout, without changing behavior.
- `TeamDetailView` forwards `contentRef` to `MessagesPanel` as
`inlineScrollContainerRef`. `MessagesPanel` accepts it as an optional
prop (unused in this release) so a follow-up can wire the inline
viewport to virtualization consumers.
- `MessagesPanel` creates `bottomSheetScrollRef` and passes it to
`Sheet.Content scrollRef`. react-modal-sheet merges it with its
internal scroll ref, so the element stays the same DOM node; this
only exposes it to us.
- Sidebar already owns `sidebarScrollRef`; no change there.
Behavior is unchanged — this only exposes refs for the follow-up that
will thread a viewport contract into ActivityTimeline.
- Integrated pending replies state management for team members.
- Updated TeamDetailView to initialize pending replies from state.
- Added logic to refresh team messages and member activity on tab focus.
- Improved UI components by increasing dialog content width for better layout.
- Enhanced member draft rows with avatar support for better visual representation.
- Implemented reconciliation logic for pending replies based on message history.
- Updated tests to cover new functionality and ensure reliability.
- Bumped pnpm version to 10.33.0 in package.json.
- Added existing members to EditTeamDialog for better context.
- Improved buildMemberDraftColorMap to reserve colors for existing members and predict colors for new drafts.
- Added tests to ensure color assignment logic works correctly for existing and new members.
Replace the per-item backward scan that located the most recent session
anchor with a single forward pass via useMemo.
Before: for every timeline item the render loop walked backward until
it found a lead-thought anchor, so N items produced up to N * N anchor
lookups on every render pass.
After: a single O(n) sweep builds previousSessionAnchorByIndex; render
time lookup is O(1). getItemSessionAnchorId is hoisted to module scope
so it is not recreated per render.
Behavior is unchanged. The three existing separator tests still pass,
and four new cases cover three-session transitions, long runs of
non-anchor items between thought groups, consecutive same-session
thoughts, and single-item lists.
- Updated `getClaudeLogs` method to support asynchronous fetching of logs.
- Introduced new interfaces for retained logs and transcript cache entries.
- Added logic to retain and retrieve Claude logs even after cleanup of live runs.
- Implemented fallback mechanism to use persisted transcripts when no live run exists.
- Updated tests to cover new log retention and retrieval scenarios.
Remove the SessionDetail.messages stripping and related cache-size
change per maintainer feedback. The session-detail optimization will
follow separately after PR #58 lands with the right architectural
pattern (lightweight snapshot + separate endpoints).
This PR now contains only:
- progressPayload helpers (buildProgressLogsTail, buildProgressAssistantOutput)
- cap applied to emitLogsProgress, updateProgress, stall warning, retry error
- throttle raised 300ms -> 1000ms
- tests for the progress payload behavior
Autofix-only change. The OOM-fix commit inserted the progressPayload
import into the wrong position relative to AutoResumeService /
idleNotificationMainProcessSemantics, which failed the
simple-import-sort ESLint rule enforced by CI.
Users with long-running teams (37+ tasks, 10+ agents for an hour) were
hitting constant renderer crashes (issue #36). Two hot paths were
serializing unbounded histories across IPC on every tick:
- Provisioning progress: emitLogsProgress and updateProgress both
joined the full provisioningOutputParts array (~20 event-driven call
sites) plus the full CLI log tail, then fanned that out to the
renderer. After an hour, each tick shipped multi-megabyte payloads
and Zustand OOM'd on the immutable state clone.
- Session detail cache: SessionDetail.messages (the raw parsed JSONL)
was being cached and returned over IPC/HTTP even though the renderer
only reads session/chunks/processes/metrics. This roughly doubled
the per-entry cache footprint on large sessions.
Fixes:
- Add progressPayload helpers that cap the log tail to 200 lines and
assistant output to the last 20 parts; empty/whitespace joins
collapse to undefined so the noop guard is explicit rather than
coincidental.
- Apply the cap inside emitLogsProgress, updateProgress, and the two
inline emission paths (stall warning, retry error). Throttle the
log-progress tick 300ms -> 1000ms so Zustand can keep up.
- Add stripSessionDetailMessages and call it at every SessionDetail
production site that crosses IPC/HTTP (both sessions.ts routes,
both cache stores).
- Raise MAX_CACHE_SESSIONS 5 -> 20 now that the per-entry SessionDetail
footprint is bounded. Previously 5 forced constant re-parsing on
every session switch.
Tests: 15 new unit tests covering the helpers (tail slicing, empty
parts, whitespace-only parts, non-mutation of inputs).
- Added support for live runtime model metadata in team provisioning.
- Implemented functions to extract and manage CLI flag values for team members.
- Updated member specifications to include effective models based on provider defaults.
- Enhanced UI dialogs to check selected providers in parallel, improving responsiveness.
- Added tests for handling model unavailability during team bootstrap and launch processes.
- Introduced CLI_INSTALLER_VERIFY_PROVIDER_MODELS IPC channel for on-demand model verification.
- Implemented handler for verifying provider models in the CliInstallerService.
- Enhanced CLI installation status management with model verification state and availability.
- Updated related components to support model verification feedback in the UI.
- Updated components in the agent-graph renderer to utilize context hooks instead of the store for accessing team data.
- Introduced `useGraphActivityContext` and `useGraphMemberPopoverContext` hooks to streamline data management.
- Refactored `GraphBlockingEdgePopover`, `GraphNodePopover`, and `GraphTaskCard` components for improved performance and readability.
- Enhanced imports in `MemberDetailDialog` for better organization.
- Added stable slot layout support in various components, enhancing the layout and interaction of nodes.
- Updated TypeScript configuration to include new paths for the agent-graph package.
- Refactored layout logic in activity lanes and kanban to accommodate stable slot assignments.
- Enhanced GraphView and GraphControls to support sidebar visibility toggling and owner slot drop handling.
- Introduced new types for layout management in GraphDataPort and related files.
- Updated README to include stable slot layout documentation.
* fix(team): resolve stuck "reconciling" state and skip resume when teammates never spawned
Addresses #54.
When a team launch fails to bootstrap teammates, the team gets stuck showing
"Last launch is still reconciling" indefinitely, and retrying with --resume
reconnects the lead but does not re-spawn the dead teammates. The only
workaround was enabling "Clear context (fresh session)", which loses the
lead's prior conversation context.
Two root causes addressed:
1. createPersistedLaunchSnapshot counted members still in 'starting' state
(agentToolAccepted=false) as 'pending' regardless of launchPhase. When
launchPhase was 'finished' with never-spawned members, the aggregate
state stayed as 'partial_pending' forever, rendered as "still reconciling".
Fix: when launchPhase != 'active', promote such members to
'failed_to_start' so the aggregate becomes 'partial_failure'
("Launch failed partway"), which correctly signals a terminal state.
2. TeamProvisioningService._launchTeamInner always used --resume when a
previous leadSessionId existed, even if the previous launch had no
teammates successfully spawned. The CLI's deterministic reconnect path
restores lead context but does not re-spawn dead teammates, so the team
stays broken across relaunches. Fix: before adding --resume, read the
persisted launch state. If every expected teammate is 'starting' (never
spawned) or 'failed_to_start', skip --resume so the CLI performs a full
fresh bootstrap that spawns all teammates.
Verified manually on Linux: a team stuck in "still reconciling" correctly
transitions to "failed partway" after the first fix, and the next Launch
(without "Clear context") fully bootstraps and brings teammates online.
* fix(team): narrow skip resume to never-spawned teammates
---------
Co-authored-by: 777genius <quantjumppro@gmail.com>
P1: Poller no longer overwrites nextCursor/hasMore — those belong
to the "Load older" flow. Both poller and loadOlder now dedup
messages by messageId or timestamp+from fingerprint.
P1: Cursor is now compound (timestamp|messageId) with stable
tie-breaking sort. Messages sharing the same timestamp at page
boundaries are no longer lost.
P2: getMessagesPage now applies the same enrichment as getTeamData:
leadSessionId propagation and slash-command-result annotation.
P3: Added 3 tests for getMessagesPage covering pagination, cursor
stability with same-timestamp messages, and slash command annotation.
Two different interval sets with the same length would produce
the same cache key, returning stale results. Serialize each
interval's startedAt~completedAt into the key.
capTimerMap was cancelling oldest pending throttle/debounce timers
when the Map exceeded a size limit. Since the schedulers don't
replay dropped keys, this silently lost refresh callbacks — leaving
session/team state stale after high-volume file change events.
These Maps are self-cleaning (entries delete themselves when their
callback fires) and hold ~100 bytes per entry. Even 200 entries
is negligible memory. Removing the cap fixes the data freshness
issue without any memory concern.
Returning messages: [] broke the slash command annotation test and
any code relying on getTeamData.messages (notifications, dedup).
Keep a small batch (50 newest) in getTeamData for compatibility.
Full message history is available via getMessagesPage() API.
New cursor-based IPC endpoint for the messages timeline panel:
- team:getMessagesPage(teamName, { beforeTimestamp?, limit? })
returns { messages, nextCursor, hasMore }
- Cursor is timestamp-based — stable under live message insertion
- Default page size 50, max 200
getTeamData no longer includes messages in its response, eliminating
the ~1MB messages payload from every team refresh. Messages are now
fetched independently by MessagesPanel.
MessagesPanel changes:
- Fetches initial page on mount via getMessagesPage API
- Auto-refreshes newest page every 5s when team is alive
- "Load older messages" button for pagination
- Falls back to prop messages if API fails (graceful degradation)
The TeamDataWorkerClient logged console.warn when the worker file
was not found, which is expected during tests (no build output).
The test setup treats unexpected warnings as failures.
Downgrade to logger.debug for the "not found" message and remove
the eager warning from resolveWorkerPath().
- Move endRefreshing() outside the !cancelled guard in finally block
so it always runs even when the effect is torn down mid-refresh.
- Only call load() when isTabActive or on first load — prevents
unnecessary fetches when a hidden tab's effect re-runs.
Critical:
- findLogsForTask: call worker directly instead of wrapping in
wrapTeamHandler, so failures propagate to catch and trigger the
main-thread fallback correctly.
- TeamDataWorkerClient: scope error/exit handlers to the specific
worker instance to prevent a stale worker's exit event from
rejecting pending requests on a replacement worker (race condition).
Major:
- TeamDataWorkerClient: validate teamName and taskId before forwarding
to worker thread (input sanitization).
- team-data-worker: include status, since, and intervals length in
cache key to prevent stale results after filter changes.
- team-data-worker: move logsInFlight.delete() into .finally() so
rejected lookups don't poison the in-flight map permanently.
- MemberLogsTab: reset refreshCountRef and refreshing state in effect
cleanup to prevent the refresh indicator from latching on permanently
when the effect tears down mid-refresh (e.g. on tab switch).
The user inbox (user.json) contains real teammate-to-user messages
generated by Claude Code CLI. Filtering it as a system inbox was
incorrect — it broke message aggregation for user-directed messages.
Only the broadcast inbox (*.json) needs to be excluded since '*'
is not a valid member name and causes a phantom member in the UI.
The broadcast inbox file (inboxes/*.json) was being parsed by
listInboxNames() as a member named "*", which appeared in the UI
as a phantom team member. Since "*" fails the MEMBER_NAME_PATTERN
validation, it could not be removed through the UI.
Filter system inbox names (*, user) from listInboxNames() so they
are not treated as real team members.
Main process — worker thread for team data:
- New team-data-worker thread handles getTeamData and findLogsForTask,
isolating heavy file I/O (scanning 300+ subagent JSONL files) from
Electron's main event loop. getTeamData dropped from ~2000ms on the
main thread to ~110ms via the worker.
- Worker-side dedup and 10s result cache for findLogsForTask prevents
redundant scans when the same task is queried multiple times.
- Discovery cache TTL raised from 5s to 30s — avoids re-scanning the
entire project directory on every call.
- Message cap at 200 in TeamDataService to keep IPC payloads under 1MB
(was sending 2200+ messages / ~3MB, stalling Chromium IPC serialization).
- IPC handlers fall back to main-thread execution if the worker is
unavailable (graceful degradation).
Renderer — useShallow and memoization (55 files):
- Added useShallow to store selectors across 55 renderer files. Batched
individual useStore() calls (e.g. 17 calls in ExtensionStoreView,
10 in ConnectionSection) into single useShallow selectors, cutting
unnecessary re-render checks on every store update.
- MemberLogsTab: three 5-second polling intervals now pause when the
parent tab is hidden (display:none). Previously 5 hidden tabs × 3
intervals = 15 polling timers firing continuously.
- KanbanColumn wrapped in React.memo to skip re-renders when props
haven't changed.
- MemberList: memoized activeMembers/removedMembers/colorMap; replaced
O(n×m) per-member task scan with a pre-computed reviewer map.
- Bounded timer Maps in store initialization to prevent unbounded growth
of debounce/throttle tracking maps during long sessions.
- Eliminated the enrichMemberBranches method from TeamDataService to simplify member branch enrichment logic.
- Updated TeamDetailView to utilize live branch tracking for both lead and member worktrees, enhancing the accuracy of displayed member branches.
- Adjusted various references to ensure membersWithLiveBranches is used consistently across the component.