* fix: prevent agentctl OOM from unbounded diff generation in workspace tracker
Add per-file size cap (10MB), binary detection (null-byte in 8KB),
diff output cap (256KB), and total budget (2MB) to workspace tracker
diff enrichment. Strip diff content from event bus path. Surface skip
reasons in frontend. Add pprof endpoints to agentctl.
* fix: address PR review feedback
Close stdout pipe on cmd.Start failure to prevent fd leak.
Limit untracked file read to maxDiffOutputSize (256KB) instead of
maxDiffFileSize (10MB) to reduce peak allocation from ~20MB to ~512KB.
* fix: add filepath.Clean to os.Stat call to satisfy CodeQL path-injection check
* fix: address second round of PR review feedback
Extract all DiffSkipReason strings as named constants. Add diff_skip_reason
to UncommittedFile type removing unsafe cast. Add unit tests for
isBinaryContent, totalDiffBytes, capDiffOutput truncation, and all
enrichUntrackedFileDiffs skip-reason paths.
* fix: use bytes.IndexByte for null-byte check clarity
* fix: show truncation banner when diff has partial content
* fix: revert diff stripping from event bus — frontend needs diffs from this path
E2E tests confirmed the frontend receives git status diffs through the
event bus (agentctl → backend WS → event bus → gateway → frontend).
Stripping diffs broke the changes panel. The per-file size caps in
workspace_git_diff.go already bound the data.
* fix: extract renderDiffContent to keep FileDiffSection under 100-line lint limit
* fix: correct onCommentRun type in renderDiffContent helper
* fix: format long destructuring line for prettier
* fix: move diffSkipReasonLabel to types.ts to keep review-diff-list under 600 lines
Co-authored-by: José Almeida <undefined@users.noreply.github.com>
* fix: coerce optional forceLoad to boolean for renderDiffContent
* fix: shorten className and comment to stay within prettier printWidth
* fix: apply prettier formatting to review-diff-list
* fix: correct off-by-one in untracked file line count
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: José Almeida <undefined@users.noreply.github.com>
* fix: improve plan comment formatting to match code review style
- Update formatPlanCommentsAsMarkdown to use code blocks and blockquotes
- Remove duplicate header in chat-input-area.tsx
- Update buildDocumentContext to use consistent formatting
- Fixes duplicate '### Plan Comments' headers and extra separators
* fix: add tests and fix multiline blockquote handling
- Add format.test.ts for formatPlanCommentsAsMarkdown and related functions
- Fix multiline comment text to properly prefix each line with >
- Addresses review feedback about missing tests and broken blockquotes
* fix: export and reuse toBlockquote helper to avoid duplication
* fix: update E2E test to match new plan comment format
The test was expecting 'Comment 1:' which was the old bullet-style format.
Now looks for 'Plan Comments' header instead.
* refactor: re-key dockview panel state by environmentId instead of sessionId
- Remove terminal from SESSION_SCOPED_COMPONENTS (uses useEnvironmentSessionId internally)
- Convert ChangesContent and file-editor-panel to useEnvironmentSessionId()
- Re-key sessionCommits store from bySessionId to byEnvironmentId
- Re-key useCumulativeDiff cache by environmentId
- Update WS handler to resolve envKey before invalidating cumulative diff cache
- Update comments across portal manager, session switch, and layout files
* fix: revert useEnvironmentSessionId in file-editor-panel
file-editor is session-scoped and ephemeral — portal is destroyed on
session switch, so useEnvironmentSessionId() has no practical effect.
* fix: address CodeRabbit review comments
- Pass appStore to setupPortalCleanup so terminal tab close still calls
stopUserShell via activeSessionId fallback (entry.sessionId is now
undefined for global panels)
- Add request version guard to useCumulativeDiff to discard stale
responses after environment switches
* fix: scope activeSessionId fallback to terminal only in portal cleanup
vscode is session-scoped and always has entry.sessionId — the
activeSessionId fallback should only apply to terminal (now global).
* fix: migrate stranded env-keyed data on registerSessionEnvironment
When registerSessionEnvironment(sessionId, environmentId) is called,
any data previously stored under the fallback sessionId key is now
migrated to the proper environmentId key. This prevents data from
"vanishing" when selectors start reading by environmentId after the
mapping is registered.
Applies to: sessionCommits, gitStatus, shell outputs/statuses,
userShells.
* fix: extract migrateEnvKeyedData helper to fix lint max-lines
* fix: address CodeRabbit review comments
- Always delete orphaned sessionId key in migrateEnvKeyedData even when
environmentId key already exists (prevents stale accumulation)
- Reset loading state when switching to an envKey with warm cache to
avoid brief spinner flash
* fix: always reset loading on envKey switch in useCumulativeDiff
Prevents stuck loading state on rapid A→B→A env switches where the
stale in-flight response is discarded by version guard but loading
is never reset.
* fix: move sync effect before fetch effect in useCumulativeDiff
React 18 batches effects in declaration order. With the sync effect
after the fetch effect, setLoading(false) from the sync overrode
setLoading(true) from the fetch on cold-cache env switches. Moving
the sync effect first ensures the fetch effect's setLoading(true)
wins the batch.
* test: add migrateEnvKeyedData coverage for registerSessionEnvironment
Tests 4 cases: normal migration, don't-clobber existing data,
no-op when sessionId equals environmentId, and partial sub-key
migration.
* fix: close stale-diff gaps in useCumulativeDiff sync effect
- Bump requestVersionRef on envKey change so in-flight fetches for
the previous envKey are discarded
- Clear loadingState[envKey] so the fetch effect isn't blocked on
re-entry (A→B→A scenario)
* feat: add multi-select and drag-to-move for file tree and changes panel
Add Shift-click range and Ctrl/Cmd-click toggle selection to the file
tree and staged/unstaged changes sections. Includes bulk actions
(delete, stage, unstage, discard), drag-and-drop file moving into
directories, keyboard shortcuts (Escape, Ctrl+A), and E2E tests.
* fix: move bulk action components to file-row to fix max-lines lint
* style: apply prettier formatting to all changed files
* fix: bulk delete all selected items and update anchor on stale shift-click
* fix: address CodeRabbit review comments
- Keep original file-row-* data-testid for backwards compat
- Remove eager onClearSelection from bulk actions (auto-pruned by hook)
- Variant-scope bulk-discard/bulk-unstage test ids
- Remove index.lock deletion from E2E GitHelper retry loop
* fix: fix E2E tests — create files after session open, remove DnD test
- Create files AFTER opening session so WS detects git status changes
- Add proper git env vars (GIT_AUTHOR_NAME, etc.) matching existing tests
- Use element.press("Escape") instead of page keyboard for proper focus
- Remove drag-to-directory test (Playwright dragTo doesn't support HTML5
dataTransfer events needed by our DnD implementation)
* fix: rename E2E task titles to avoid ambiguous 'Changes' tab selector
* fix: make file list focusable for Escape key handler in E2E tests
* fix: improve selection styling and ignore right-clicks in selection handler
- Use text-accent-foreground on selected rows for dark mode readability
- Override child span colors with [&_span]:text-accent-foreground
- Use hover:bg-accent/80 instead of hover:bg-muted on selected items
- Skip selection handling on right-click (button === 2) so context menu
works without clearing multi-selection
* fix: plain click should not select — only Ctrl/Cmd+click and Shift+click
Plain click now clears any selection and performs the original action
(open file / toggle folder / open diff). Selection only happens with
modifier keys. handleClick returns boolean to indicate whether the
click was consumed by selection logic.
* fix: fix multi-select bugs and expand E2E test coverage
Bug fixes:
- Change isSelected from boolean to function prop so children compute
their own selection state (fixes nested file selection)
- Inline TreeNodeRow JSX into TreeNodeItem so ContextMenuTrigger asChild
can attach refs directly (fixes right-click context menu)
- Set lastClickedRef on plain click so shift-click has a valid anchor
- Add click-outside handler to clear selection
E2E tests expanded to 14 file-tree tests + 3 changes-panel tests:
- Ctrl-click select, toggle, multi-select
- Plain click clears selection
- Shift-click range after plain click and ctrl-click anchors
- Directory selection and nested file selection
- Right-click context menu (single + bulk delete)
- Click outside clears selection
- Escape clears selection
* fix: move AlertDialog outside ContextMenu to fix delete action
The AlertDialog wrapping the ContextMenu caused Radix focus management
to intercept context menu item selection, making Delete trigger Rename.
Moving AlertDialog to a sibling of ContextMenu fixes the conflict.
* fix: reorder context menu — Delete first, Rename second
Radix ContextMenu auto-focuses the first item on open, which caused
accidental Rename triggers. Putting Delete first (the more common
action for selection) avoids this.
* fix: don't clear selection when clicking context menu or dialog
The click-outside handler was clearing selection when clicking on
context menu items or confirmation dialog buttons (rendered in portals
outside the tree container). Now skips clearing for elements with
role=menu, role=alertdialog, or role=dialog.
Also extended the bulk delete E2E test to verify both files are
actually removed after confirming the dialog.
* fix: hide file count in delete dialog when folder children not loaded
When a folder hasn't been expanded, its children aren't loaded yet so
countFilesInTree returns 0. Instead of showing "0 files", hide the
count entirely and just show "delete <folder name>".
* fix: show confirmation dialog for bulk discard in changes panel
Bulk discard was calling git.discard() directly, bypassing the
confirmation dialog. Now routes through handleBulkDiscardClick which
opens the DiscardDialog with a bulk message ("discard changes to N
files") before executing.
* fix: address remaining CodeRabbit review comments
- Deduplicate filenames on move: when moving files with identical names
to the same directory, append "(1)", "(2)" etc. to avoid collisions.
Both tree update and rename API calls use the same deduplication.
- Replace void operator with .catch(() => {}) for bulk stage/unstage
to prevent unhandled promise rejections.
- Add document-level dragend listener as safety net to clear stuck drag
state if the element's onDragEnd doesn't fire (Escape, out-of-window).
* test: add unit tests for file tree move deduplication and visible paths
* fix: address all remaining PR review comments
- Move onSelectionChange out of React state updater (side effect fix)
- Stabilize keyboard listener deps (pass callbacks not object)
- Remove unnecessary useMemo in BulkActionBar
- Fix ghost-restore on partial bulk delete (remove all at once)
- Fix blank filename on single-file bulk discard
- Add aria-selected and role=treeitem for accessibility
- Extract computeNextSelection params into object (max-params lint)
* fix: add missing bulk action props to WorkingTreeProps type
WorkingTreeSections referenced onBulkStage, onBulkUnstage, and
onBulkDiscard but WorkingTreeProps Pick didn't include them, causing
a TypeScript build failure.
* fix: address final review comments
- Add unit tests for computeNextSelection (8 tests covering ctrl-click,
shift-click range, stale anchor, reverse range, shift+ctrl extend)
- Extract shared E2E helpers (GitHelper, makeGitEnv, openTaskSession,
createStandardProfile) to e2e/helpers/git-helper.ts
- Fix misleading comment in bulk delete (says "re-insert only failed"
but implementation restores full snapshot)
* fix: use Promise.all for bulk delete to prevent partial rollback
Individual .catch() handlers could restore already-deleted files
if a later delete failed. Now uses Promise.all so rollback only
happens if any delete rejects.
* test: add renderHook tests for useMultiSelect hook behaviors
- Stale-path pruning: items shrink → selected paths auto-pruned
- Plain click: returns false, clears existing selection
- selectAll/clearSelection: work correctly, fire onSelectionChange
- Install @testing-library/react for renderHook support
* fix: handle false returns from onDeleteFile in bulk delete Promise.all
* chore: remove unused @testing-library/react-hooks dependency
* feat(web): add right-click context menu to move sidebar tasks between workflow steps
* fix: scope Move to submenu to task's own workflow steps
* fix: surgical rollback and disable actions during delete
* fix: wrap TaskItem in div for ContextMenuTrigger asChild ref forwarding
* refactor: unify right-click and 3-dot menus into single context menu
- Delete task-item-menu.tsx (DropdownMenu) — no longer needed
- TaskItem's 3-dot button now dispatches contextmenu event to trigger
the wrapping ContextMenu
- TaskItemWithContextMenu tracks open state and passes menuOpen to
TaskItem for hover visibility
- Add Duplicate item to the context menu
- Fix step color dots using block display
* fix: use Tailwind class for step color dots instead of inline style
* fix: disable Duplicate menu item until handler is implemented
* fix: defer move-to-step to let context menu close before optimistic update
* fix: defer move-to-step to let context menu close before optimistic update
* feat(vcs): add commit body/description field to commit dialog
Add an optional description textarea below the commit title in both
desktop and mobile commit dialogs, with a dedicated AI generate button
for the description. Add builtin-commit-description utility agent and
E2E test.
* style: fix prettier formatting in mobile git controls and E2E test
* fix(e2e): use specific testid selectors for commit dialog inputs
The commit dialog now has two textboxes (title + body), so
getByRole("textbox") is ambiguous. Use getByTestId selectors
and add exact match for the Commit button.
* fix(ui): add cursor-pointer to mobile commit button
* fix(ui): remove "(optional)" from description label and "Generate" text from button
* fix(ui): match PR dialog style for commit description generate button
* feat(vcs): add AI generate button for PR title
Add builtin-pr-title utility agent and wire up a generate button
inside the PR title input, matching the commit title pattern.
Extract shared dialog field components into vcs-dialog-fields.tsx
to stay within the 600-line file limit.
* refactor(ui): unify generate buttons as icon-only sparkles inside inputs
Move AI generate buttons inside both title inputs and description
textareas (top-right corner) for a consistent look. Remove the
"Generate" label variant — all buttons are now icon-only sparkles.
* style: fix prettier formatting in utility agent generator hook
* fix(a11y): add aria-labels to generate buttons and PR title input
* ci: retrigger E2E tests
* fix(e2e): improve commit dialog button click reliability
Add exact match and enabled check before clicking the dialog
Commit button, increase close timeout.
* ci: retrigger checks
* fix(e2e): improve pre-hooks test commit button reliability
Add exact match, enabled check, and longer dialog timeout to match
the commit-body test pattern.
* fix(ui): prevent dialog field overflow and improve commit description prompt
- Add min-w-0 to relative wrappers in commit and PR dialog fields to
prevent text inputs/textareas from overflowing the dialog bounds
- Rewrite commit description prompt to use system-role framing that
prevents weaker models from adding conversational preamble
* feat(web): add collapsible sections to changes panel
Sections (Unstaged, Staged, Commits, PR Changes, PR Commits) can now
be collapsed/expanded by clicking the section header or chevron arrow.
Includes Playwright E2E test validating collapse and expand behavior.
* fix(web): add aria-expanded to collapsible section toggles
Addresses CodeRabbit review: adds aria-expanded attribute to section
toggle buttons for screen reader support, and asserts the attribute
in the E2E test.
* fix(web): render section toggle as button only when collapsible
Non-collapsible sections keep a plain <span> label to avoid
ambiguous getByRole("button") matches in E2E tests (e.g. "Commit"
action button vs "Commits (0)" section header).
* fix(web): use exact match for Commit button in pre-hooks E2E test
The collapsible "Commits" section toggle button now matches
getByRole("button", { name: "Commit" }) via substring. Use exact
matching to only target the action button.