Commit Graph

273 Commits

Author SHA1 Message Date
Carlos Florêncio 6ffb157d7e fix: user workflow deadlock (#677) 2026-04-23 09:54:06 +01:00
João Salavisa c11a6bbfd2 feat(github): add issue watcher with task creation and auto-cleanup (#672)
* feat(github): add issue watcher with task creation and auto-cleanup

Add a GitHub Issue Watcher feature that polls for new issues matching
configurable filters and automatically creates tasks for them. This
complements the existing PR Review Watcher with the same patterns.

Backend:
- Issue watch CRUD (models, store, service, controller, WS handlers)
- Poller integration with configurable intervals
- Event-driven task creation via orchestrator
- Repository resolution and worktree support for issue tasks
- Auto-cleanup of tasks for closed issues (CleanupClosedIssueTasks)
- GetIssueState client method across all implementations
- Default branch auto-detection from cloned repo HEAD

Frontend:
- Issue watch settings UI (table, dialog, placeholders)
- Issue link icon on task cards (green circle-dot)
- API client, store slice, and hook for issue watches
- SSR and WS hydration of issue metadata

* fix: resolve golangci-lint errors (goconst, nestif, dupl) and prettier formatting

* fix(github): fix issue watch update shape, trigger key, and label quoting

- httpUpdateIssueWatch now returns the full IssueWatch object instead of
  {updated:true} so the frontend store correctly refreshes after edits
- httpTriggerIssueWatch returns new_issues_found to match TriggerIssueResponse
  type; was returning new_issues so the toast count was always 0
- buildIssueFilter uses conditional quoting instead of fmt.Sprintf %q to
  avoid Go escape sequences in GitHub search qualifiers

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* fix: resolve eslint warnings (max-lines-per-function, no-nested-ternary)

* test(github): add issue watcher tests and fix review feedback

- Add TestInterpolateIssuePrompt (4 cases: empty/all/none/partial)
- Add TestCreateIssueTask (3 cases: skip/reserve+assign/release on fail)
- Add TestShouldDeleteIssueTask (4 cases: open/closed+sessions/closed/error)
- Fix WS response key new_issues -> new_issues_found
- Fix fmt.Printf -> fmt.Fprintf(os.Stderr) in store.go
- Add CleanupClosedIssueTasks call in WS trigger handler

* fix: address PR review feedback and CI failures

- Split test files to fix revive file-length-limit (service_test.go,
  event_handlers_github_test.go)
- Add session checker guard in CleanupClosedIssueTasks
- Normalize poll intervals on UpdateIssueWatch
- Add cleanup call in TriggerAllIssueChecks loop
- Log cleanup errors in controller and WS handler
- Prefer refs/remotes/origin/HEAD in detectGitDefaultBranch
- Add aria-label to issue task icon
- Fix TypeScript type errors: add linkedIssues to GitHubSlice,
  fix hook exports, remove invalid imports

* fix: split test files exceeding revive limit and clean up minor issues

- Split service_test.go (883→779 lines): move TestTriggerPRSync_* to
  service_pr_sync_test.go
- Split event_handlers_github_test.go (806→677 lines): move
  TestCreateReviewTask_* and helpers to event_handlers_github_review_test.go
- Remove redundant s.taskSessionChecker nil-check inside shouldDeleteIssueTask
  (early guard at CleanupClosedIssueTasks already ensures it is non-nil)
- Remove unreachable dead guard in detectAndPersistDefaultBranch
  (repo.DefaultBranch is always "" at that call site)
- Fix bogus #0 issue badge in task-session-sidebar.tsx when issueNumber is absent

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* fix: address Claude review — poll clamp and prompt injection

- Handle v <= 0 in UpdateIssueWatch poll interval (fall back to default)
- Remove {{issue.body}} from default template to prevent prompt injection
- Update test expected output for new default template

* fix: remove {{issue.body}} from frontend DEFAULT_ISSUE_WATCH_PROMPT

Mirrors the backend fix — the UI pre-fills the prompt so the backend
fallback was never reached. The placeholder stays in
ISSUE_WATCH_PLACEHOLDERS for explicit opt-in.

* feat(github): add isIssueWatch sidebar filter dimension

Add isIssueWatch boolean to KanbanState, TaskSwitcherItem, and sidebar
filter registry — mirroring the isPRReview pattern. Issue-watch tasks
(created from issue_watch_id in metadata) can now be explicitly filtered
in the sidebar, just like PR review tasks.

Files changed:
- sidebar-view-types: add isIssueWatch dimension
- filter-dimension-registry: add Issue watch filter meta
- apply-view: add isIssueWatch extractor
- kanban types: add isIssueWatch field
- TaskSwitcherItem: add isIssueWatch field
- use-all-workflow-snapshots: derive isIssueWatch from metadata
- ssr/mapper: derive isIssueWatch from metadata
- task-session-sidebar: pass isIssueWatch, extract helpers to fix complexity

* fix: correct deleteIssueWatch response type to match backend

Backend httpDeleteIssueWatch returns {"deleted": true}; the frontend
type was { success: boolean } — a mismatch with no functional impact
(return value is ignored) but wrong per TypeScript contract.

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* refactor: extract metadata helpers to shared lib/metadata-utils.ts

- Move isPRReviewFromMetadata, isIssueWatchFromMetadata, and
  issueFieldsFromMetadata to a shared module to eliminate duplicate
  definitions (sonarjs/no-identical-functions).
- Fix deleteIssueWatch response type: { success: boolean } -> { deleted: boolean }
  to match the actual backend response.

* style: fix prettier formatting

* test(web): add tests for isIssueWatch filter dimension and metadata-utils

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: fix prettier formatting in test files

* fix(github): cascade-delete issue watch tasks on watch deletion and add isPRReview to SSR mapper

- DeleteIssueWatch now wraps in a transaction and deletes related
  github_issue_watch_tasks rows before removing the watch; prevents
  orphaned dedup rows accumulating after a watch is deleted
- snapshotToState in mapper.ts now also sets isPRReview from metadata,
  matching the mapping already done in use-all-workflow-snapshots.ts

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* style: extract duplicate string constant in metadata-utils test

* style: extract repeated WS error strings into constants in handlers.go

goconst min-occurrences: 3 — 'invalid payload' (6x) and 'id required' (4x)
both exceed the threshold. Extract to package-level constants.

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* style: extract repeated issue key strings into constants in event_handlers_github.go

goconst min-occurrences: 3 — 'issue_number' (9x) and 'issue_watch_id' (5x)
both newly introduced in this PR exceed the threshold.

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* style: fix prettier formatting in metadata-utils test

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:32:46 +01:00
Carlos Florêncio cb7546e323 fix(web): prevent kanban topbar search from overlapping right buttons (#661) 2026-04-20 23:22:34 +01:00
Carlos Florêncio d81baf3fca fix(web): stop "Preparing workspace" flashing on step move and refresh stale chats (#663) 2026-04-20 22:48:07 +01:00
Carlos Florêncio 6909fe6854 feat(web): session tabs on kanban preview panel (#648) 2026-04-20 22:31:09 +01:00
Carlos Florêncio f4b5abef1a fix(web): show repo name instead of full path in task sidebar (#652) 2026-04-20 20:04:32 +01:00
José Almeida d209d2ce2a feat(web): sidebar filter UX polish — align ops, group steps by workflow (#647)
* fix(web): align operator column across sidebar filter rows

Dimension select was `flex-1`, which made the op column drift horizontally
between rows — especially on boolean dimensions where the value input is
omitted. Fix by pinning the dimension and op selects to fixed widths so
only the value column flexes, keeping the middle "is/in/not in" aligned
across every row.

* feat(web): group workflow steps by workflow in sidebar filter dropdowns

The Workflow step filter previously flattened steps from every workflow
into one unlabelled list, making it impossible to tell which workflow a
step belonged to. Tag each step option with its workflow name and render
the single- and multi-select dropdowns with a labelled group + separator
per workflow. Other enum dimensions stay flat (backward-compatible via
an optional `group` field on the option).

* refactor(web): extract shared helper for sidebar filter option groups

Both the single- and multi-select value dropdowns built group buckets
with the same consecutive-key loop, which implicitly assumed options of
the same group arrived back-to-back. Extract a `buildOptionGroups`
helper backed by a Map so the adjacency assumption goes away and the
rendering call sites shrink to one line each. Also include the stable
`option.value` in the cmdk `CommandItem` value so same-titled steps
inside one workflow don't collide on identity/search.

* fix(web): truncate sidebar filter single-select value within popover

The single-select value trigger and the free-text input were `flex-1`
without `min-w-0`, so a long value (e.g. repository slug) pushed the
row wider than the popover and bled the remove button past the right
edge. The multi-select trigger already had `min-w-0`, which is why the
"in" operator rendered correctly. Add `min-w-0` to both so the flex
items can shrink and the built-in SelectValue `line-clamp-1` ellipsis
engages.
2026-04-20 19:32:36 +01:00
Carlos Florêncio e1f0632325 fix(web): anchor PR detail panel to session group on auto-open (#646) 2026-04-20 18:52:45 +01:00
José Almeida f6f45b5777 feat: add filter/group/sort and saved views to task sidebar (#644)
* feat(web): add sidebar view types and apply-view engine

Introduces the typed representation of a sidebar view (filter clauses,
sort spec, group key) and the pure engine that applies a view to a list
of tasks to produce a filtered, sorted, grouped result.

* feat(web): add built-in sidebar view presets

Provides the four shipped presets (All tasks, No PR reviews, Active,
Archived). The 'No PR reviews' preset filters out PR-watcher tasks that
would otherwise crowd the sidebar.

* feat(web): persist sidebar views in ui store

Adds sidebar view state to the ui slice with draft semantics for saved
views, localStorage persistence of active view + user-created views, and
root store wiring.

* feat(web): add filter dimension registry and clause editor

Registry of filterable task dimensions (status, workspace, workflow, task
type, etc.) plus the primitive UI pieces — clause editor, sort picker,
group picker, and a hook that supplies operator and value options for a
selected dimension.

* feat(web): add sidebar filter bar, popover, and view manager

Composes the filter primitives into the user-facing surfaces: the top
filter bar, the popover that hosts clause editing and sort/group
selection, the chips that switch between views, and the manager for
saving, renaming, and deleting user views.

* refactor(web): extract NewTaskButton component

Pulls the Create Task button and its command-palette registration into a
standalone memoized component in preparation for reuse across sidebar
surfaces.

* feat(web): integrate filter/view system into task sidebar

Wires the filter bar, view chips, and apply-view engine into the task
sidebar (desktop and mobile sheet). Updates task-switcher to consume the
grouped list from the engine and adds mock data for tests and previews.

* test(web): add e2e coverage for sidebar filter/view system

Page object for the filter popover plus Playwright specs covering built-in
presets, clause editing, sort/group, saved-view draft semantics, and
persistence across reloads.

* refactor(web): trim sidebar built-in views to a single seeded default

Drop the "No PR reviews", "Active", and "Archived" built-in view presets
and the isBuiltIn protection flag. The sidebar now seeds a single "All
tasks" view on first load; users can rename, overwrite, or delete it like
any other view. Deletion is guarded against removing the last remaining
view so the user can never lock themselves out.

* fix(web): humanize executor type labels in sidebar filter and grouping

The executor-type filter dropdown and group-by label were rendering raw
backend identifiers like "worktree" and "local_docker". Route both
through the existing getExecutorLabel() helper so the sidebar shows
"Worktree" and "Local Docker" like the rest of the app.

* feat(web): replace "Has PR" filter with "Is PR review"

The old dimension matched any task with an associated PR, including
tasks a user manually linked. The new dimension isolates tasks created
by the GitHub PR-review watcher specifically, which is what the filter
is useful for — hiding a wall of auto-generated review tasks.

The flag is derived from the review_watch_id metadata key the backend
already stamps onto watcher-created tasks, threaded through the kanban
snapshot and into the sidebar TaskSwitcherItem. A small migration drops
unknown dimensions (including the old "hasPR") from stored views so the
popover does not crash when reading legacy data.

* fix(web): tighten sidebar filter popover layout

Reduce the popover width (w-96 → w-[22rem]), trim section padding
(p-3 → p-2.5) and label margins (mb-2 → mb-1.5), and drop the
"No filters applied." placeholder. Extract a shared SectionLabel to
keep the three section headers visually aligned.

* feat(web): show workflow step color dot in sidebar filter dropdown

Surface the step color on the workflow-step filter's value dropdown so
each option renders with the same coloured circle used elsewhere (task
switcher, kanban column, command panel). Adds an optional color field
on filter value options; only workflow-step options populate it today.

* feat(web): replace cramped in/not-in filter editor with a multiselect dropdown

The previous "in/not in" value input rendered selected values as tiny
10px pills inside a fixed-height row that overflowed and truncated as
the list of options grew. Replace it with a combobox-style multiselect
(Popover + Command) that shows a compact summary ("N selected" or the
single label with its color dot) and opens a searchable, scrollable
list with check indicators on click.

* fix(web): show workflow and step names (not UUIDs) in sidebar grouping

Group-by Workflow and Workflow Step rendered raw UUID strings as group
headers because `TaskSwitcherItem` did not carry the resolved names.
Pass `workflowName` and `workflowStepTitle` through `toSidebarItem` and
prefer them in the group extractors.

* fix(web): drop redundant true/false selector on boolean sidebar filters

Boolean dimensions (PR review, Archived, Has diff) previously exposed
both an operator (is / is not) and a value selector (true / false),
which gives four user inputs for two real states. Hide the value input
for boolean clauses and always store `true`; negation is expressed via
the operator. Rename "Is PR review" to "PR review" since the dimension
label no longer needs to read as a question.

* refactor(web): drop Sort by Repository and relabel State to Status

Sorting by repository duplicates what Group by Repository already
offers, and "State" leaks a backend term into the picker. Remove the
repository sort key (comparator, type, picker option) and rename the
state option to "Status". Stored views referencing the removed key are
migrated back to `state` at load time so the popover does not crash.

* fix(web): tighten sidebar filter popover section spacing

The FILTERS, SORT, and GROUP BY section labels each sat above their
controls with both padded containers (p-2.5) and a 6px bottom margin,
leaving visible dead space above each header. Reduce container padding
to p-2 and label margin to mb-1 so sections read as a compact stack.

* chore(web): apply prettier formatting drift in sidebar-filter area

* revert(web): undo store.ts prettier reformat to stay under max-lines cap

The packed single-line UIA declaration in store.ts is load-bearing for
the 600-line ESLint cap: the previous chore commit unpacked it to 9
lines and pushed the file to 609 lines, turning the max-lines warning
into a lint failure. Restore the packed layout (the /* prettier-ignore */
comment is there for exactly this reason).

* refactor(web): label boolean filter ops as Show/Hide instead of is/is not

* fix(web): pull section labels flush to sidebar filter popover borders

* refactor(web): inline rename and save-as into sidebar view header row

* refactor(web): split ViewHeaderRow into mode, display, and action subcomponents

* feat(backend): persist sidebar views in user settings

Adds a SidebarViews field to UserSettings (stored in the existing
users.settings JSON blob) plus validation in the service layer:
views are capped at 50, ids and names must be non-empty and unique.

* feat(web): hydrate and sync sidebar views to backend

Moves the source of truth for saved sidebar views from localStorage
to the user settings endpoint. Views hydrate from SSR on first load;
create/rename/overwrite/delete/duplicate/toggle-collapsed all PATCH
the backend with optimistic updates and revert-on-failure. The
active view id and draft remain per-device in localStorage. A
sync bridge surfaces PATCH errors as toasts and runs a one-time
migration to upload any pre-existing local views.

As a side effect, localStorage writes and PATCH side effects are
moved out of immer producers to avoid proxy-serialization risk.

* chore(web): apply prettier to store.ts sidebar actions block

* fix(web): run migrateView on SSR-hydrated sidebar views

Defensively drop clauses with unknown dimensions and reset stale sort
keys when views come from the backend, matching the localStorage load
path. Also notes the default-state module and sidebar views in the
store structure docs.

* docs: note default-state module and sidebar views in store structure

* fix(web): type sidebar view storage wrappers properly

Drop the as-unknown-as-SidebarView and as-never casts at call sites by
making the local-storage getters/setters accept the domain type and do
the JsonValue cast once inside the wrapper.

* refactor(web): stop PATCHing on sidebar group collapse toggles

collapsedGroups is per-device visual state; routing it through
mutateViews made every expand/collapse click fire a user-settings
PATCH. Keep it local (in-memory + localStorage cache); the next real
view mutation picks up whatever state is current.

* fix(web): skip sidebar view migration when settings hydrated from server

After SSR hydration the in-memory views are already the backend's
truth, so migrating re-POSTs the same data every page load. Gate the
one-shot migration on userSettings.loaded — only run if the server
didn't answer and the slice fell back to localStorage.
2026-04-20 13:44:24 +01:00
Carlos Florêncio 31140c8b9f fix(web): follow workflow step session switches in chat UI (#625) 2026-04-20 09:26:53 +01:00
Carlos Florêncio 4b55a69145 feat(web): vscode-style preview tabs for files, diffs, and commits (#622) 2026-04-19 17:39:07 +01:00
Carlos Florêncio 66f0cc2a16 fix(web): persist agent profile mode through bulk-edit save (#626) 2026-04-19 11:34:43 +01:00
Carlos Florêncio 1f7e4dbb5e fix: stream setup script output and keep prepare panel on failure (#607) 2026-04-19 11:08:18 +01:00
José Almeida 80eacf9e1c fix(web): use UUID fallback for attachments on non-secure contexts (#640) 2026-04-18 15:29:57 +01:00
Carlos Florêncio 5b04c9c675 perf: focus-gated git polling to reduce CPU on retained worktrees (#610) 2026-04-18 11:33:04 +01:00
José Almeida f63a9d5dc5 fix(web): wrap long paths in Discard Changes dialog (#620)
Long file paths could overflow the AlertDialog horizontally, pushing
the action buttons outside the visible area. Add break-words on the
description and overflow-wrap:anywhere on the filename span so paths
wrap mid-string instead of forcing the dialog wider than its max-w.
2026-04-18 10:32:24 +01:00
Francisco Magalhães cd46516cef fix: ux consistency on archiving and deleting tasks (#627) 2026-04-17 23:20:10 +01:00
João Salavisa f440007efc feat: add confirmation dialog before archiving tasks (#621) 2026-04-17 18:27:10 +01:00
Carlos Florêncio 60614a78a9 fix(web): close file diff tab when uncommitted change is undone (#618) 2026-04-17 16:46:08 +01:00
José Almeida 6cbf050fa1 fix: prevent agentctl OOM from unbounded diff generation in workspace tracker (#598)
* 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>
2026-04-17 15:16:18 +01:00
João Salavisa 4640bed408 fix(web): validate activeSessionId belongs to activeTaskId before use (#614) 2026-04-17 02:35:12 +01:00
Carlos Florêncio 99a8407dd8 fix: close clarification overlay when agent moves on (#608) 2026-04-16 15:58:01 +01:00
Carlos Florêncio ce55577e6d feat(web): render short tool-call output inline (#604) 2026-04-16 11:37:15 +01:00
Carlos Florêncio eaba25b80e fix(web): stop panels from opening in the left sidebar group (#603) 2026-04-16 11:36:46 +01:00
Carlos Florêncio 836b48956b fix(web): align top-bar right button heights (#601) 2026-04-16 11:04:23 +01:00
Carlos Florêncio a222854422 feat: associate agent profiles with workflows and steps (#597) 2026-04-16 10:14:45 +01:00
João Salavisa f74de1c642 fix: improve plan comment formatting to match code review style (#600)
* 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.
2026-04-16 02:57:41 +01:00
Luan Martins 59d7270056 fix(web): disable resume and show agent selector when profile is deleted (#578) 2026-04-15 22:31:02 +01:00
Carlos Florêncio d3945b00ce fix(web): read session state at call time in comment run to prevent stale queue (#588) 2026-04-15 19:55:57 +01:00
Carlos Florêncio 204fac1cca feat: acp-first profiles, models, and modes (#566) 2026-04-15 13:22:32 +01:00
José Almeida c08d68c0fe refactor: re-key dockview panel state by environmentId instead of sessionId (#491)
* 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)
2026-04-14 11:44:38 +01:00
José Almeida 2b8513f902 feat: add multi-select and drag-to-move for file tree and changes panel (#490)
* 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
2026-04-11 17:43:21 +01:00
Carlos Florêncio 5ff930c6e0 fix(web): keep file tree and terminal waiting through long prepare (#564) 2026-04-09 12:49:47 +01:00
Carlos Florêncio a8092fd0a9 fix(web): sort nested file tree folders before files (#562) 2026-04-08 19:41:07 +01:00
Carlos Florêncio 41b405c64f fix(backend): prevent task reorder during silent session resume (#555) 2026-04-08 07:55:24 +01:00
Carlos Florêncio 61e5d5cce9 feat(web): redesign task sidebar with repo-grouped layout and diff stats (#550) 2026-04-07 09:12:17 +01:00
Carlos Florêncio a44489640b fix: changed branch and auto focus changes panel (#549) 2026-04-06 18:59:37 +01:00
Carlos Florêncio 52d6f7e9bf fix(web): persist PR panel dismissal across page refreshes (#547) 2026-04-06 13:52:53 +01:00
Carlos Florêncio 6d81734c66 fix(web): reset stale topbar branch when navigating between tasks (#546) 2026-04-06 12:52:42 +01:00
Carlos Florêncio f9ca582036 fix: compare ahead/behind counts against base branch instead of remote tracking branch (#544) 2026-04-06 09:51:58 +01:00
José Almeida 8e50fe90e9 feat(web): right-click context menu to move sidebar tasks between steps (#492)
* 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
2026-04-06 00:57:01 +01:00
Carlos Florêncio 82eb59d91e fix: disable plan mode when moving to next workflow step (#525) 2026-04-03 15:07:46 +01:00
Carlos Florêncio 2238ef0825 feat: prioritize local changes above PR files in changes panel (#528) 2026-04-03 14:28:28 +01:00
Carlos Florêncio dbe778e4e8 feat: auto-show PR details panel when task has associated PR (#517) 2026-04-02 00:08:35 +01:00
Carlos Florêncio beb42acc29 feat: expose hidden keybindings in settings for user customization (#521) 2026-04-01 23:29:49 +01:00
Carlos Florêncio 1587203c2e fix(web): add max-height and scrollbar to queue message editor textarea (#519) 2026-04-01 23:03:20 +01:00
Carlos Florêncio 6fea6277a8 fix: hide start agent button during preparation and fix auto-start race condition on step move (#516) 2026-04-01 23:00:05 +01:00
Carlos Florêncio 5fb1c12364 fix: stabilize diff viewer fileRefs to prevent auto-scroll on background updates (#513) 2026-04-01 22:53:47 +01:00
Carlos Florêncio f4723ebcae feat: sort tasks by creation date in kanban and sidebar (#511) 2026-03-31 23:31:18 +01:00
Carlos Florêncio c732675f33 fix(github): workspace-scoped PR data loading with cache and singleflight (#509) 2026-03-31 22:56:09 +01:00