7011bbcbc4fe0d6442008148aba850bfee5e5822
339 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7011bbcbc4 | fix: session tab leak, PR icon crash, and PR review prompt scope (#897) | ||
|
|
5b23478323 | feat(web): differentiate pending permission icon from turn finished in sidebar (#882) | ||
|
|
8fee2de145 | fix(web): preserve sidebar scroll position across task switches (#884) | ||
|
|
45a46f9899 |
feat: support tasks without a repository (#850)
* feat: support tasks without a repository Lets users create a task with no repository attached — useful for research, brainstorming, design discussions, or pointing the agent at an existing non-git folder. The create-task dialog gains a 3-mode segmented control (Repo / URL / None); "None" mode replaces the chip row with a folder picker that browses the host filesystem via a new GET /api/v1/fs/list-dir endpoint and either commits an absolute path or falls back to a kandev- managed scratch workspace at <homeDir>/tasks/<workspaceID>/<taskID>/. The worktree executor is hidden from the picker in no-repo mode (it inherently needs a repo); the lifecycle manager also short-circuits worktree mode server-side as a belt-and-suspenders guard. The Changes panel auto-closes for repo-less tasks since it has nothing to show, and the cumulative-diff handler returns null instead of erroring when no base commit exists. Spec + plan added at docs/specs/tasks-without-repositories/. * fix(backend): anchor folder picker to discovery roots (CodeQL path traversal) CodeQL flagged the new GET /api/v1/fs/list-dir handler for piping a user-controlled path straight into os.Stat / os.ReadDir. Restrict the listing to paths inside the configured discovery roots (defaults to $HOME, same allowlist as repository discovery), reject anything outside with ErrPathNotAllowed → HTTP 403, and clip the breadcrumb's "up" button at the root so the picker can't be used to walk above the allowed prefix. Tests cover: rejection of paths outside roots, traversal escapes (../), default-to-first-root, and parent exposure for nested paths. * fix(backend): use os.Root for folder picker (recognized CodeQL sanitizer) Previous attempt anchored to discovery roots via isPathAllowed, but CodeQL's data flow doesn't model that helper as a sanitizer and kept flagging os.Stat / os.ReadDir. Switch to os.Root (Go 1.24+): open the matching discovery root once and run Stat/Open/ReadDir against the relative path within it. The OS itself enforces containment — symlinks and traversal cannot escape — and CodeQL recognizes os.Root as safe. * fix: address PR review feedback - Changes panel auto-close now distinguishes "task loading" from "task has no repos" (returns boolean | null), so the panel isn't permanently removed during the first-render window before task data lands in the store. - Reset noRepository / workspacePath in resetDiscoveryState so reopening the dialog after a None-mode task doesn't carry the flag forward. - Folder-picker breadcrumb: dropped dead `i > 0 && i > 1` condition (always equivalent to `i > 1`); chevrons now render between every segment as expected. - list-dir HTTP handler returns a generic error message instead of forwarding raw OS errors that leak host paths. - Document the dataDir → kandevHome inversion convention next to the scratch-path computation so future readers don't mistake the filepath.Dir call for a fragile derivation. * fix: address CodeRabbit review nitpicks - Trim whitespace on workspace_path before persisting to task metadata so a value of " " doesn't get stored as a non-empty path. - Extract the workspace_path metadata key into models.MetaKeyWorkspacePath alongside the existing MetaKeyAgentProfileID / MetaKeyExecutorProfileID so the set/read sites can't drift apart. - Folder picker: reset cached listing when the popover closes, so reopening reloads from `value` instead of showing the last browsed directory the user navigated away from without committing. - Submit payload: send `workspace_path: undefined` instead of an empty string when the trimmed value is empty, matching the noRepository=false case and keeping the API contract clear. * fix: scratch path lands in kandev home, not its parent Claude caught a real bug — `m.dataDir` is set from `cfg.ResolvedHomeDir()` in cmd/kandev/agents.go, not `ResolvedDataDir()` (the field name is misleading and the comment was wrong). My previous use of `filepath.Dir(m.dataDir)` therefore walked one level above the kandev home, so scratch dirs landed at e.g. `~/tasks/<ws>/<task>` instead of `~/.kandev/tasks/<ws>/<task>` — and at the project root in dev mode where `KANDEV_HOME_DIR=<project>/.kandev-dev`, which is what the user saw on their machine. Use `m.dataDir` directly (it's already the kandev home), document the field's misnamed-but-correct semantics, and tighten the test fixtures. Also: - folder-picker trigger gets `cursor-pointer` (CodeRabbit nitpick). * fix: pass workspacePath through handleCreateWithoutAgent too The "create without agent" path called buildCreateTaskPayload without the new workspacePath field, so picking a folder + clicking "Create without starting" silently dropped the path. Now matches the performCreate (with-agent) path. * fix: satisfy DialogFormState in new-subtask form stub PR #852 (merged after this branch was created) introduced new-subtask-form-state.ts which stubs out a DialogFormState for the subtask dialog. Adding noRepository / setNoRepository / workspacePath / setWorkspacePath to the type made that stub incomplete, breaking the Next.js production build (caught by CI, missed by tsc --noEmit). Add the four inert fields to the stub, alongside the existing fresh- branch inert fields, and pull both groups into named constants to keep the useMemo body under the per-function line cap. * fix: keep toggle-github-url testid as alias on URL-mode button The new source-mode segmented control replaced the previous "or paste a GitHub URL" text link, which had data-testid="toggle-github-url". The existing e2e suite (create-task-github-url.spec.ts and the new subtask.spec.ts from #852) still selects the URL toggle by that testid. Keep "toggle-github-url" as the testid on the URL-mode button so both suites pass without a sweep across all e2e files. Click semantics are unchanged — single click enters URL mode. * test(e2e): update toggle assertion for new source-mode segmented control create-task-github-url.spec.ts:892 was checking the old text-link toggle's two-way behavior with text labels ("or paste a GitHub URL" / "use a workspace repository"). The new segmented control has explicit mode buttons; assert aria-checked instead of label text. The other 9 clicks on `toggle-github-url` in this file still work via the legacy testid alias on the URL-mode button. * refactor: extract useActiveTaskHasRepos to its own hook file dockview-desktop-layout.tsx grew to 609 lines with the new hook + the Changes panel guard, over the 600-line cap documented in CLAUDE.md (lint passed but the convention is real). Move the hook to hooks/domains/kanban/use-active-task-has-repos.ts. File drops to 590 lines. * chore: gitignore .claude/scheduled_tasks.lock runtime file * fix(web): lock chip-row min-height so modal doesn't shift on mode toggle The chip row uses flex-wrap; Repo mode (chip + branch + add button + segmented control) sometimes occupies a different vertical extent than None mode (single folder picker + segmented control), causing the create-task modal to jump a few pixels when toggling between sources. Set min-h-9 so the row always reserves at least one chip-row of height regardless of the active mode. * feat: let folder picker browse anywhere, drop allowed-roots gate The picker is for choosing a starting folder for a repo-less task — users legitimately want /tmp, /var/log/foo, or any directory they have read access to. Coupling it to the repo-discover endpoint's discoveryRoots ($HOME by default) was too tight: paths like /tmp returned 403 "path is not within allowed roots". Decouple ListDirectory from discoveryRoots. The repo-discover endpoint (DiscoverLocalRepositories, ValidateLocalRepositoryPath, LocalRepositoryStatus, ListBranches) stays anchored to the allowlist since picking a repo to import is a workspace-scoped operation; the folder picker trusts the local-process boundary instead. CodeQL is kept happy by routing the FS operations through os.Root opened at "/" — the Go stdlib sanitizer for path injection still recognises it (containment to / is trivially satisfied for every absolute path on the host). --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
0743477d48 |
feat: persist N-entry FIFO message queue (#864)
* feat(orchestrator): persist N-entry FIFO message queue
Replaces the single-slot, latest-wins, in-memory queue with a SQLite-backed
FIFO queue (default cap 10 per session, configurable via
KANDEV_QUEUE_MAX_PER_SESSION). Drain stays one-per-turn so each entry gets its
own user message and `on_turn_*` workflow cycle. Inter-task `message_task_kandev`
returns a structured `queue_full` error with sender attribution when the
target session is saturated, so the calling LLM can decide whether to retry.
Surfaces a new WS action `message.queue.remove` for single-entry deletion;
`message.queue.cancel` now means clear-all. Edit/remove are keyed on the
entry's UUID, returning `entry_not_found` when atomic-take won the drain race.
* feat(web): N-entry queue state, API, and optimistic hook
Replaces the single-slot QueueStatus with an ordered `entries[]` per session
plus capacity meta (`count`, `max`). API exposes typed `QueueFullError` and
`QueueEntryNotFoundError`; the hook surfaces optimistic edit/remove with
drain-race rollback (refetch on `entry_not_found`).
* feat(web): inline ghost-message queue UI with sender chips
Replaces the floating QueuedMessageIndicator overlay with a stack of ghost
messages rendered above the chat input. Each ghost shows a sender chip
(user / agent) and per-entry edit/remove. A small "n/max queued · clear all"
pill summarizes capacity. Inter-task entries are visually distinct and edit
is gated to the original sender.
* fix(queue): address review — auth guard, TOCTOU, error UX
- Add `messagequeue.QueuedByUser` / `QueuedByAgent` constants and centralize
the `queue_full` error code so the WS and MCP handlers stay in sync.
- WS `message.queue.{add,append,update}` default empty user_id to QueuedByUser
so the UpdateContent ownership guard always runs against a non-empty owner;
inter-task entries (`queued_by="agent"`) can no longer be overwritten by
user-initiated edits even when `user_id` is omitted from the payload.
- memoryRepository: hold the mutex through the whole append-or-insert path
(prior split released the lock between tail-check and Insert, allowing two
same-sender callers to race past coalescing). TransferSession now shifts
source positions past the destination's max so transferred entries always
drain after pre-existing destination entries, matching SQLite semantics.
- Frontend: surface edit/clear/remove failures via `sonner` toasts (drain race
→ "message already sent"); exit edit mode on failure instead of leaving the
textarea silently stuck. Hide the remove button on read-only (agent) ghosts.
- `useQueue.clearAll` no longer derives `max` from pre-clear entry count;
resets to `metaMax ?? 0` so the next status_changed event lands authoritative
capacity.
- `senderKindOf` drops the unreachable metadata fallback; route on `queued_by`
alone since `dispatchTaskMessage` always sets it.
- Test loggers now write to stderr per CLAUDE.md "stderr for logs" guideline.
* test(web): align queue e2e selectors with new ghost-message UI
The ghost-message rewrite renamed button titles ("Cancel queued message"
→ "Remove queued message", "Edit message" → "Edit queued message") and
introduced a `data-testid="queued-ghost-list"` container. The
`message-queue.spec.ts` Playwright tests still pointed at the old titles.
* fix(queue): address second review pass — auth, drain race, tests
- WS handlers (`add` / `append` / `update`) now reject `user_id="agent"`
outright, closing the gap a hostile WS client could exploit by
impersonating the inter-task sender identity to overwrite agent
entries (greptile P1).
- WS `update` / `remove` now require `session_id` so the post-mutation
`MessageQueueStatusChanged` broadcast can never be silently skipped.
- `sqliteRepository.TakeHead` checks `RowsAffected()` after DELETE;
when two concurrent take callers race past the busy-timeout the
loser sees 0 affected rows and now returns `nil, nil` instead of
the already-drained message (would otherwise dispatch the same
prompt twice).
- `Service.GetStatus` returns `Entries: []` instead of `nil` on the
error path so the wire shape stays consistent (frontend already
guards but strict consumers wouldn't tolerate the type mismatch).
- New tests: `queue-api.test.ts` covers `rethrowQueueError` mappings
(queue_full / entry_not_found / generic / Error pass-through);
`session-slice.upsert.test.ts` covers `setQueueEntries`,
`removeQueueEntry`, and `clearQueueStatus`. Backend gains an
agent-impersonation handler test.
- `rethrowQueueError` now surfaces `WSError.message` for non-queue
errors instead of stringifying the whole object.
- Cleanup: replace unused `a, c := ...` discards in service_test
with `_, _ = ...`.
* fix(queue): scope update/remove by session_id (cross-session deletion guard)
The MCP `queue_full` error payload deliberately discloses sibling-task
entry IDs (so the calling agent's LLM can reason about the wedge state).
Without a session_id filter on `DeleteByID` / `UpdateContent`, a hostile
agent that received that payload could call `wsRemoveEntry` /
`wsUpdateMessage` against another task's entries and delete or rewrite
them — only the per-entry UUID was being checked.
- `Repository.UpdateContent` and `Repository.DeleteByID` now require
`sessionID` and add `AND session_id = ?` to the SQL guard. The memory
repo mirrors the scope (lookup keyed on session before the entry scan).
- `Service.UpdateMessage` / `Service.RemoveEntry` take `sessionID`; the
WS handlers thread `req.SessionID` through (already validated as
non-empty in the previous fixup).
- TypeScript: `session_id` is now required on `updateQueuedMessage` /
`removeQueuedEntry` params so callers fail at compile time, matching
the backend contract.
- Tests cover the cross-session attack: `RemoveEntry`/`UpdateMessage`
with an attacker-supplied session_id but a real entry id from another
session must return `ErrEntryNotFound` and leave the victim entry
untouched. SQLite repo gets parallel coverage.
* fix(queue): propagate transfer error + tighten WSError narrowing
- `Service.TransferSession` now returns an error so callers can fail
closed instead of silently leaving entries orphaned on the previous
session. The two existing call sites pick the right policy:
`switchSessionForStep` (reusing an existing session) propagates the
error so the workflow switch aborts; the new-session path logs and
continues, since the new session is already committed and rolling
back here would leave a half-switched workflow.
- `asWSError` now also narrows on `message` / `details`, so WS payloads
without a `code` field are still mapped to a meaningful `Error.message`
via `rethrowQueueError` (instead of "[object Object]"). Native
`Error` instances explicitly skip the WS-error branch so they pass
through untouched, preserving the existing pass-through guarantee.
- `queue-api.test.ts`: `expect.assertions(N)` on the catch-only tests
so a missing throw fails the test instead of silently passing.
* fix(queue): check TransferSession error in service tests
The TransferSession signature change in
|
||
|
|
f97ac8bbc1 | feat: streaming agent install, PTY login terminal, docker container UX (#869) | ||
|
|
14c19ea538 |
feat(web): add CI hover popover on PR top-bar button (#846)
* feat(github): plumb CI popover fields + branch protection cache
Adds RequiredReviews/UnresolvedReviewThreads/ChecksTotal/ChecksPassing
to TaskPR + PRStatus. Extends the GraphQL PR query with reviewThreads
to populate UnresolvedReviewThreads on every poll. New branch_protection.go
holds an in-memory TTL cache + BranchProtectionFetcher interface;
PATClient/GHClient implement REST/CLI lookups so SyncTaskPR can resolve
required_approving_review_count for the PR's base branch. SyncTaskPR
preserves existing counts when a lightweight (GraphQL) sync returns 0
so the popover doesn't flap during the polling cycle.
* feat(github): mock controller hooks for CI popover e2e
Extends associateTaskPR to accept review/check counts, required_reviews,
and unresolved_review_threads, and switches to ReplaceTaskPR so re-POSTing
the same task drives the popover live-refresh path. Adds POST /pr-feedback
to seed PRFeedback (checks/reviews/comments) keyed by a deterministic
synthetic head_sha, and PUT /auth-health to flip the mock client's
authenticated flag for the "Reconnect GitHub" branch. Mock client gains
ReplaceCheckRuns/ReplaceReviews/ReplaceComments helpers and an
authenticated field that defaults to true.
* feat(web): CI hover popover on PR top-bar button
Hovering the PR top-bar button now reveals a popover with
Passed/In-Progress/Failed counts, a workflow-rollup list under the
non-empty buckets (with "open on GitHub" + "+ ctx" affordances on
failed rows), a review-state row ("Approved 1 / 2 required"), and
unresolved-comments count. Click on the button keeps the existing
"open PR detail panel" behavior so shipped tests stay green.
PRFeedback is lazy-fetched on hover-open via a new SWR-style hook
backed by a 20-entry LRU cache slice; the popover refetches whenever
the underlying TaskPR's updated_at changes (the WS handler already
applies poll deltas). Bucket logic + workflow grouping live in a
small util with vitest coverage. The aggregate-popover variant for
multi-PR tasks ships but the multi-PR trigger preserves its existing
DropdownMenu so per-row click → addPRPanel still works.
* test(e2e): pr-topbar-popover scenarios A–F covering CI popover
11 specs covering: count rows + zero-bucket hiding, workflow rollup
with (N/M passed) badge under Failed, review row + unresolved comments,
hover open + leave close, failed-row open/+ctx affordances, header
external link, "No checks have started" empty state, "Reconnect GitHub"
auth-lost branch, and live count updates on a re-associate.
Adds api-client helpers (mockGitHubSeedPRFeedback, mockGitHubSetAuthHealth,
extended mockGitHubAssociateTaskPR) and SessionPage popover accessors.
* fix(github): address bot review comments on CI popover
- branch_protection.go: evict stale entries on read so the cache map
cannot grow unbounded over a long-running deployment; rewrite the
comment to drop the inaccurate singleflight claim (no dedup logic
exists, concurrent misses race harmlessly to set()).
- gh_client.go: replace fragile strings.Contains "HTTP 404" with an
isNotFoundErr helper that tolerates the formats current and future
gh CLI versions use ("HTTP 404", "404 Not Found", "status: 404").
- graphql.go: extend reviewThreads with totalCount and attribute the
un-fetched tail to the unresolved bucket so PRs with >100 threads
don't silently undercount in the popover.
- pr-ci-popover.tsx: drop the dead "icon" initializer in PRReviewRow
(every branch overwrites it). Memoize useAddCheckToContext so
PRWorkflowRow's reference equality stays stable across renders.
- pr-topbar-button.tsx: collapse the duplicate JSDoc on
usePopoverInteractions.
* fix(github): address remaining bot review comments + tests
Real correctness fixes:
- Replace the "0/0 means counts omitted" heuristic with an explicit
PRStatus.ChecksPopulated flag so the rich REST sync can clear stale
totals when checks are removed from a PR upstream.
- Refresh tp.BaseBranch from status.PR on each SyncTaskPR so a
retargeted PR's branch-protection lookup keys off the new branch.
Persist base_branch in UpdateTaskPR.
- Cache 403 from /branches/.../protection (token lacks Administration:
Read scope) the same way as 404 — otherwise every poller tick burns
rate-limit quota retrying. Both PATClient and GHClient.
- usePRCIPopover: refetch on every popover-open transition (not just
on updated_at change), via a wasEnabledRef gate. Restores the
intended SWR semantics that the lastSyncedRef-only guard suppressed.
- groupChecksByWorkflow: prefer the URL of a job matching the group's
bucket so clicking a "failed" workflow row navigates to a failing
job, not an earlier passing one.
Tests:
- branch_protection_test.go: cache hit/miss/TTL/eviction, error
not cached, no-rule cached, client-without-fetcher path.
- pat_client_test.go: TestPATClient_FetchBranchProtection table-test
for 200/200-with-null/404/403/500.
- service_test.go: ChecksPopulated preserve-on-zero, accept real
zero, base-branch retarget, change-publish for the new fields.
* refactor(github): trim associateTaskPR + drop unreachable PRAggregatePopover
- mock_controller.go: extract associateTaskPRRequest struct,
buildTaskPRFromRequest, and ensureMockPRForRequest helpers so the
handler is back under the 80-line Go limit.
- pr-ci-popover.tsx: remove unreachable PRAggregatePopover export.
Multi-PR tasks currently use the existing DropdownMenu trigger
(no hover popover), so the aggregate component was dead code.
* fix(github): drop unused anchorRef + tests for is{NotFound,Forbidden}Err
- pr-topbar-button.tsx: anchorRef was leftover from the dropped
click-to-pin onInteractOutside guard. Removed.
- gh_client_test.go: tests for the new isNotFoundErr / isForbiddenErr
helpers covering each accepted format and a few non-matches so a
future gh CLI wording change is caught.
* fix(github): UnresolvedReviewThreadsPopulated guard + drop unreachable nil checks
- service.go: mirror the ChecksPopulated/preserve dance for
UnresolvedReviewThreads. The REST single-PR poller (getPRStatus)
doesn't fetch review threads, so without this guard every REST
sync would clobber the GraphQL-populated value to 0 and emit a
spurious github.task_pr.updated event. The GraphQL path
(convertBatchedPRResult) sets the new flag.
- branch_protection.go: drop unreachable s == nil / fetcher == nil
checks. fetchRequiredReviews is only invoked from SyncTaskPR on a
service constructed via NewService, which always initialises the
cache. After a successful type assertion, the interface value
cannot be nil; the !ok branch already covers nil-interface inputs.
- service_test.go: new TestSyncTaskPR_UnresolvedThreads_PreservesOnLightweightSync
covers the regression. Existing NewFieldsPublishEvent threads-change
case updated to set UnresolvedReviewThreadsPopulated.
* refactor(web): drop CI popover progress bar — last-updated is enough
The animated progress bar was visual noise; the "updated Ns ago"
timestamp tells the user the same thing without the indicator. Footer
also hides entirely until the first fetch lands so an empty popover
doesn't show a useless "" line.
* fix(web): background-sync PRFeedback so popover is fresh on first hover
The on-hover SWR fetch was the only refresh path, so opening the popover
showed the previously-cached value while a new fetch raced behind it —
visibly stale until the refetch landed. The button icon, by contrast,
updates the moment a github.task_pr.updated WS push lands.
Add a usePRFeedbackBackgroundSync hook mounted at the PRTopbarButton
that subscribes to the same pr.updated_at change signal and refetches
PRFeedback into the cache in the background. The popover hook still
fires an on-open refetch as a safety net for the very first hover
(before any sync). No extra polling — fetches are gated on real
TaskPR changes, matching the icon's cadence.
* feat(web): CI status chip in chat status bar + tweak elapsed label
- New <PRStatusChip> renders a compact colored circle in the chat
status bar (left of the TodoIndicator, just above the input). Hover
opens the same PRCIPopover anchored to the chip's top edge so the
card expands upward — matching the user's "open bottom-up" ask. The
chip mounts the background-sync hook so the popover is fresh on
first hover even when the top-bar PR button isn't visible.
- elapsedLabel: "just now" → "updated just now" so the footer reads
the same regardless of how recently the cache was refreshed.
* fix(web): chip color was missing — use static class map for Tailwind JIT
\`getPRStatusColor().replace("text-", "bg-")\` produced class strings at
runtime, which Tailwind's content scanner doesn't pick up — the chip
rendered with no background. Replace with a static lookup so each
literal `bg-*` class appears in the source and gets compiled into the
bundle.
* fix(web): chip uses currentColor SVG so the dot actually renders
The previous bg-* approach relied on Tailwind compiling the literal
class strings. Even with a static map some palette entries (notably
bg-muted-foreground) didn't surface as a usable utility, so the chip
remained transparent. Apply getPRStatusColor's text-* class to the
button wrapper and fill an SVG circle with currentColor — the text-*
utilities are already compiled (PRTaskIcon uses them) so the dot
is guaranteed to render in the right color.
* fix(web): redesign PR status chip — clipboard icon + status glyph
- Move chip to right of TodoIndicator (was left).
- Render as "[clipboard icon] [status glyph]" instead of a bare circle:
passed → green CircleCheck, failed → red CircleX, in_progress →
yellow Loader2 (spinning), merged/neutral → muted dot. Status icons
reuse already-compiled text-* utilities so colors render reliably.
* fix(github): ReviewCount = approved-reviewer count + ReviewCountsPopulated guard
Two bugs feeding the popover's "Approved (0)" misreport:
- Semantic: REST path set ReviewCount to len(reviews), which double-counts
authors who left multiple reviews and includes COMMENTED rows. Replace
with countApprovedReviewers — distinct authors whose latest review
state is APPROVED. GraphQL converter gets the parallel
countApprovedReviewerNodes helper and also populates PendingReviewCount
from ReviewRequests.TotalCount (was unset, so the chip's "(K requested)"
suffix was always missing).
- Clobber: GraphQL polls used to write ReviewCount/PendingReviewCount=0
on every cycle because the converter never set them. Add
ReviewCountsPopulated to PRStatus and gate SyncTaskPR's writes on it
the same way ChecksPopulated already does, so a partial sync path
preserves the persisted values instead of resetting them to zero.
* fix(web): popover nitpicks (icon, review row, indent, badge)
- Chip icon: ClipboardCheck → ListCheck.
- PRReviewRow always renders (drop the early-return). Fresh PRs with
no approvals + no protection rule now show "Awaiting review" instead
of vanishing.
- Indent workflow rows under the In Progress / Failed group headers
(pl-5) so the hierarchy reads more clearly.
- In-progress workflow badge: "0/1 ran" → "1 running". The old phrasing
hid whether the job had even started.
* feat(web): stacked pass-rate progress bar in CI popover
Mirrors TodoIndicator's progress bar at the top of the CI section, but
split into three coloured segments:
green (passed) | yellow (in_progress) | red (failed)
sized proportionally to each bucket's count. The label reads
"Pass rate · K/N (P%)" so the user gets the same at-a-glance summary
TODOs shows. Skipped/cancelled-but-not-failed jobs aren't counted —
the bar always reflects the actionable picture.
* fix(web): right-align review counts + drop chip hover bg
- Move approval/required counts to the right edge of the review row
("Approved" on the left, "1 / 2" on the right), mirroring the
bucket-row layout. The "(K requested)" suffix becomes a "· K requested"
trailing fragment on the right side.
- PRStatusChip: drop hover:bg-accent/50 so the trigger doesn't grow a
background plate when the popover is open.
* fix(web): switch CI chip icon to IconChecklist
* fix(github): clear bot review-comment backlog
Real correctness fixes:
- pr-status-chip: pending checks / pending review now beat
checks_state === "success" in chipStatus precedence — a PR with
CI green but reviewers outstanding is no longer rendered as
"passed".
- branch_protection: drop the unreachable Reset() method.
- pr-topbar-button: stop double-mounting usePRFeedbackBackgroundSync;
the chip already covers it.
- use-pr-ci-popover: dedupe key combines prFeedbackKey + updated_at
so switching tasks with same-timestamp PRs still refetches.
- use-pr-ci-popover: drop stale comment about the removed progress bar.
Refactor:
- client_helpers + graphql: extract reviewSample +
latestReviewStateByAuthor helper so the three near-identical
per-reviewer dedup loops share one implementation.
Tests:
- countApprovedReviewers + reduceReviewSummary unit tests.
- TestSyncTaskPR_ReviewCounts_PreservesOnLightweightSync +
AcceptsRealZero mirror the existing ChecksPopulated guards.
- groupChecksByWorkflow distinct-URL test verifies the failed-job
URL-selection fix.
- branch_protection TTL tests now use testing/synctest — instant
virtual time, no real-sleep flake on CI.
* fix(web): clicking the CI chip no longer closes the popover
HoverCard treats the trigger button as outside the content's bounding
box, so a click on the chip fired Radix's onPointerDownOutside and
auto-closed the popover. Add a triggerRef + onPointerDownOutside guard
on HoverCardContent that filters out clicks landing on the trigger —
clicking the chip is now a no-op while the popover stays open via hover.
|
||
|
|
1733014721 |
feat(web): prettify Kandev MCP tool titles in chat (#858)
* feat(web): prettify Kandev MCP tool titles in chat
Render `create_task_kandev` as "Kandev: Create Task" instead of the
raw underscored tool name. Frontend-only display transform via a
regex-gated helper so non-Kandev tool names (Read, Bash, other MCP
servers, Claude-supplied human titles) pass through unchanged.
* fix(web): update e2e assertions for prettified Kandev tool titles + trim pass-through
- E2E `config-management.spec.ts` asserted raw `*_kandev` names in chat;
update to the new "Kandev: ..." display so Shard 6/10 passes.
- `prettifyToolTitle` now returns `trimmed` instead of `raw` on the
non-match path so whitespace handling is symmetric across both
branches (Greptile review).
* fix(web): expand collapsed tool-call groups in e2e config-management assertions
Once a turn completes, consecutive tool-call messages collapse into a
"{N} tool call(s)" group header, hiding the individual prettified titles
the test asserts against. The previous tests passed by lucky substring
match against the script directives in the user prompt; once
prettification removed `*_kandev` from the chat surface that
coincidence broke. Add an `expandToolCallGroups` helper that clicks
each group header before running the title visibility assertions.
* refactor(web): trim ACRONYMS to only entries used by current Kandev tool catalog
Per "no speculative code" guideline (Claude review): only `mcp` is
produced by the current 29-tool catalog. Remove the unused
id/url/api/ui/vs/ide/cli entries; add new ones when a catalog tool
first needs them.
* fix(web): strip MCP namespace prefix when prettifying Kandev tool titles
Codex passes the tool name as `<server>/<tool>` (e.g.
`kandev/get_task_plan_kandev`), so the bare-suffix regex didn't match
and the raw underscored name leaked through to the chat. Split on
`/` or `__` and prettify the trailing segment, covering Codex's
`server/tool` and Claude's `mcp__server__tool` naming alike.
|
||
|
|
1066ac5b16 |
feat: extend implement plan button with fresh-agent path and server-side plan mode (#832)
* feat: extend implement plan button with fresh-agent path and server-side plan mode - Split-button: primary click = same-session, chevron = fresh agent with clean context window - Forward draft attachments and context files on implement (adds getAttachments() to handle) - Authoritatively clear plan_mode via new session.set_plan_mode WS endpoint, not a message flag - Add on_exit:disable_plan_mode to plan-and-build.yml plan step (mirrors feature-dev.yml) - Drop localStorage plan-mode rehydration; server session metadata is the single source of truth - Delete dead DocumentPanel component (no callers) - Add SetSessionPlanModeByID on orchestrator Service; wire wsSetPlanMode handler * test: fix store isolation in useImplementFresh test + extract duplicate strings - Fix test isolation: reset store directly and clear mocks before rendering - Extract sessionId, appFilePath, appFileName to constants to fix ESLint duplicate-string warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(e2e): cover implement-plan fresh-agent split-button path Adds Playwright coverage for the new chevron menu on the violet Implement button: asserts a brand-new TaskSession is launched on the same task with the planning session's agent profile inherited, and that the planning session is left running in parallel. Second spec verifies the split-button is gated behind plan mode. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: defer plan-mode exit and surface error toast on implement failures Move handlePlanModeChange(false) into the message.add success callback in useImplementPlan so a failed send leaves the plan layout intact for retry. Add error toast to useImplementFresh catch so launchSession failures are visible to the user instead of silently logged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: guard fresh-agent launch on planning session executor_id TaskSession.executor_id is optional, so the fresh-agent path could call launchSession with an undefined executor and silently inherit the workspace default instead of the planning session's executor. Add the field to the early-return guard so the hook no-ops when the planning session is missing either an agent profile or an executor. * feat: mark fresh-agent session as primary and focus it When useImplementFresh launches a new implementation session, the fresh session is now automatically: 1. Set as primary via session.set_primary WS endpoint 2. Focused/selected as the active session in the UI This improves UX by switching the focus to the fresh implementation context immediately rather than keeping the planning session active. Addresses comment in PR #832 from carlosflorencio. - Extract setupFreshSession helper to keep complexity under 15 - Add tests for primary session marking and UI focus - Add test for graceful handling of set_primary failures - Update JSDoc to document the new behavior - Extract test constants to avoid duplicate string warnings * fix(test): replace self-referential test constants `const TASK_ID = TASK_ID;` (and SESS_PLAN, SESS_FRESH) caused a TDZ ReferenceError at module load, so vitest reported `(0 test)` and none of the new fresh-agent coverage actually ran. Replace with literal values so the 13 tests in this file execute again. * fix: drop strict executor_id guard and skip composer clear on missing session_id Two related issues: 1. e2e regression — the planning session in the FE store does not always carry executor_id (the field is omitempty on the backend DTO and isn't populated for fresh task sessions in the WS push path). The earlier guard required it before calling launchSession, so the fresh-agent path silently no-op'd in real flows. Drop executor_id from the guard and only forward it when set; backend resolves it from the agent profile / workspace default when omitted. 2. PR review (Greptile P1) — when launchSession resolves without a session_id, the previous flow still cleared the composer and draft even though no fresh session was activated. Bail out before the clear so the user keeps their typed text and can retry. Tests updated: - replace the no-op-on-missing-executor test with one that asserts the hook still launches and omits the key from the payload - add coverage for the no-session_id branch keeping the composer intact --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
21202ef437 |
fix(web): mobile task switcher sheet skeleton while snapshot loads (#860)
* fix(web): show skeleton in mobile task switcher sheet while snapshot loads
When the user opens the mobile task switcher inside a task page before the
workflow snapshot has hydrated kanban, the sheet rendered "No tasks yet."
instead of a loading state — and stayed that way if the fetch silently
failed.
useWorkflowSnapshot now drives kanban.isLoading, useTasks exposes it as
{ tasks, isLoading }, and the sheet wires that into TaskSwitcher's existing
skeleton. Fetch errors are surfaced via console.warn so an indefinite empty
list isn't silent anymore.
* fix(web): guard snapshot finally on cancel and drop redundant subscription
Skip the workflow-snapshot fetch's finally block when the effect was superseded by a newer workflowId — otherwise the old fetch's settle would clear the loading flag the new effect just set, briefly flashing the empty-state UI mid-switch.
Also drop the redundant kanbanIsLoading subscription in useSheetData (useTasks already encapsulates it) and make the setState mock in use-tasks.test.ts actually mutate state so useWorkflowSnapshot's side effects are exercised by the test.
* fix(web): scope snapshot finally clear to the effect that raised the flag
If kanban already matched the requested workflowId, the effect skipped raising isLoading=true — but its finally still cleared it on settle, collapsing a skeleton a concurrent caller (e.g. workspace switch) had set independently. Track whether this effect raised the flag and only clear in that case.
Also collapse the multi-line useTasks comment per CLAUDE.md style.
* fix(web): suppress superseded snapshot fetch warnings
Skip the catch-handler console.warn when the effect has been cancelled — a stale fetch shouldn't surface noise after a newer workflow has taken over.
* test(web): flush microtasks explicitly in race test
waitFor on a never-true assertion resolves immediately, hiding the intent. Use explicit Promise.resolve() flushes to land the superseded fetch's .then/.finally chain before asserting.
* style(web): collapse multi-line comments per CLAUDE.md
|
||
|
|
40c89cd380 | fix(web): preserve dockview state across task and plan-mode switches (#855) | ||
|
|
9664e13f07 | fix(web): keep "All Workflows" filter on task nav and workflow.created (#854) | ||
|
|
dfcaa24be3 | feat: allow subtasks to target a sibling repository (#852) | ||
|
|
cf3dbc9582 |
fix(web): keep command palette selection on first result (#845)
* fix(web): keep command palette selection on first result * test(web): fix launch session mock response shape * refactor(web): address command palette review feedback * fix(web): align command panel selection setter type |
||
|
|
0d3fce25b4 | fix(web): stop dockview layout corruption when switching between maximized / sessionless tasks (#838) | ||
|
|
7642b984c6 | feat(web): show grid spinner on agent tab while session is working (#836) | ||
|
|
7ec639bf2f |
feat(web): mobile parity (session/terminal/repo pickers + multi-terminal) (#840)
* refactor(web): extract shared use-session-actions hook from SessionTab
Move WS-level session lifecycle handlers (set primary, stop, resume,
delete) into hooks/domains/session/use-session-actions so the same logic
can power both the dockview tab context menu and upcoming mobile
controls. Dockview-specific panel removal stays local in SessionTab via
the onDeleted callback.
* feat(web): humanize task and session state labels
Mobile task sheet was rendering raw enum values like "IN_PROGRESS".
Add formatTaskStateLabel + formatTaskSessionStateLabel helpers and use
them in the mobile task sheet badge. Session state badge in the new
mobile sessions picker also reuses the formatter.
* feat(web): mobile session/terminal/repo pickers + multi-terminal support
Address parity gaps on mobile by introducing per-pane header pills that
open bottom-sheet pickers, scoped to the relevant view rather than
overloading the left task sheet.
Pickers:
- Sessions pill in chat header → bottom sheet with launch / set-primary
/ stop / resume / delete (with confirm). Pill shows agent label +
idx/total badge.
- Terminals pill in terminal header → bottom sheet with add / close
(with confirm) / switch. Replaces an earlier chip-strip prototype.
- Repos pill on the top bar (multi-repo only) → bottom sheet listing
task repositories; switching jumps to a session on that repo.
Each pill renders with a chevron-down icon + count badge so the
trigger reads as a button, not a status badge.
Multi-terminal:
- New useMobileTerminals hook wraps useTerminals and auto-creates a
first shell when the env reports zero.
- MobileTerminalPane multi-mounts PassthroughTerminal instances keyed
by terminalId so switching preserves scrollback. Active xterm
registers itself in a small mobile-active-terminal sender registry;
the existing key-bar reroutes input through xterm.paste() so Ctrl /
^C / arrows reach whichever terminal is currently visible, not just
the per-session default shell.
- Skip the WebGL renderer addon on mobile (new disableWebgl prop on
PassthroughTerminal). The WebGL atlas miscomputed glyph scaling
after the desktop→mobile responsive switch and rendered text at
several times the intended font size; the canvas renderer is correct
and fast enough for shell use.
Refactors:
- Left sheet reverts to a single-purpose task list (the sectioned-hub
variant was rolled back in favour of per-pane pickers).
- session-task-switcher-sheet-hooks extracted from the sheet to keep
it under the file-size lint cap.
* fix(web): address review feedback on mobile pickers + multi-terminal
Bots flagged a real first-mount bug and a handful of smaller items;
addressed each:
- mobile-terminal-pane: track xterm in state instead of a ref so the
active-sender registration effect re-runs once xterm is ready. With
the ref, the effect ran once on mount, found xtermRef.current === null
while PassthroughTerminal was still initializing (notably under the
ResizeObserver fallback path when the container starts at 0×0), and
silently never registered — leaving the key-bar wired to the default
shell instead of the visible terminal.
- send-shell-input: wrap the activeSender call in try/catch so a stale
registry entry (xterm GC'd mid-frame) falls back to the WS path
instead of dropping the keystroke and silently consuming modifiers.
Added tests for the activeSender routing branch (success, modifier
consumption, fallback on throw).
- mobile-sessions-section: remove the dead `showCloseConfirm ? "Delete" : "Delete"`
ternary and the unused prop on SessionActionsMenu.
- use-mobile-terminals: clear the auto-create guard when addTerminal()
rejects so the user gets a retry instead of an empty terminal list.
- mobile-repo-pill: sort taskRepos by position before falling back to
the first repo, so the pill label matches the picker's ordering when
no session is active yet.
- session-task-switcher-sheet-hooks: catch loadTaskSessionsForTask
rejection in handleSelectTask (was an unhandled rejection that left
the sheet open on backend errors), and stabilize useCallback deps in
handleWorkspaceChange by spreading the SheetNavOptions fields so a
fresh literal each render doesn't defeat memoization. Extracted
selectTaskWithoutPrimarySession to keep useSheetActions under the
function-size lint cap.
* fix(web): preserve task.repositories in sheet-hooks kanban mappers
mapSnapshotToKanban (workspace switch) and handleTaskCreated (newly
created task) only carried the first repository_id, dropping the full
TaskRepository array. The mobile repo picker (useTaskRepoCount +
MobileReposSection) reads task.repositories.length, so it silently
disappeared after a workspace switch and for freshly created
multi-repo tasks. Map the array with the same shape the SSR mapper
uses (id, repository_id, base_branch, checkout_branch, position).
Also address a small nit: wrap addTerminal in an arrow at the New
terminal button so the click MouseEvent isn't passed as the (unused)
first argument.
* fix(web): de-dup mobile terminal auto-create + add state-label tests + repo-tap toast
- use-mobile-terminals: move the auto-create-first-shell guard from
per-instance refs to a module-level Set keyed by environmentId.
MobileTerminalPane mounts useMobileTerminals three times for the
same session (the pane itself, the picker pill via
useActiveTerminalPillLabel, and the picker sheet content via
MobileTerminalsList — Vaul keeps it mounted while closed). With per-
instance guards, all three effects ran on first commit and each
fired createUserShell, racing into up to three shells. The shared
Set ensures only the first effect to observe an empty list creates,
with retry-on-failure preserved (delete + next render re-checks).
- mobile-repos-section: tapping a repo with no sessions now toasts a
hint to open the session picker, instead of silently doing nothing.
- state-labels: add tests covering known mappings, null/undefined
fallbacks, and unknown-state passthrough for both formatters.
* fix(web): single useMobileTerminals via context + tests + drop dead export
Address two follow-up review items:
- Lift useMobileTerminals to a single MobileTerminalsProvider owned by
MobileTerminalPane. The picker pill and the picker sheet's list now
read state from useMobileTerminalsContext instead of calling
useMobileTerminals themselves. The previous setup had three
independent useState-backed lists that drifted out of sync after
addTerminal/removeTerminal — the pane could show a stale active
terminal and the pill count could lag until a server WS push
reconciled them. The module-level guard (added in
|
||
|
|
aa6023e421 |
feat(web): per-task color indicator in sidebar (#835)
* feat(web): per-task color indicator in sidebar (localStorage) Adds a color marker to tasks in the left task list for better visual identification. Right-click a task → Color submenu picks one of seven colors (or None to clear). Color renders as a 3px bar on the left edge of the row and persists in localStorage. * fix(web): address review feedback on task color indicator - Filter storage events to TASK_COLORS_STORAGE_KEY so cross-tab writes to unrelated keys no longer wake every TaskItem subscriber. - Cache the parsed color map at module level, invalidated on writes and cross-tab storage events. Avoids re-parsing localStorage on every getSnapshot call (N tasks × N renders). - Drop redundant typeof string check on Object.entries keys. - Dim the colored bar to opacity-60 when not selected so colored rows remain distinguishable from colored-and-selected rows at the left edge. * refactor(web): remove test-only export from task-colors Drop the __resetTaskColorsCacheForTests escape hatch from the production module. Tests now invalidate the cache via a synthesized cross-tab `storage` event, exercising the same listener path the production code uses for real cross-tab updates. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
9fe2cbd874 |
fix: source commit pushed status from git remote, not PR commits (#833)
* fix: source commit pushed status from git remote, not PR commits Commits showed as "not pushed" whenever the branch had no open PR, since the UI derived pushed status by matching local SHAs against the PR's commit list. Sourcing it from the upstream tracking ref in agentctl's GetLog keeps the answer correct without a PR, after rebases, and across multi-repo workspaces. * fix: treat PR-matched commits as pushed alongside the git remote signal mergeCommits now flags a commit as pushed when EITHER the backend's git-sourced flag is true OR a PR commit matches by SHA. The git signal fixes the original "no PR = unpushed" bug; the PR signal preserves the "PR exists ⇒ commit is on remote" assumption that the E2E mocks rely on (and that holds in practice, since GitHub PRs require pushed branches). Also corrects the validation-path comment in markPushedCommits to describe the actual non-branch fall-through. * perf: bound rev-list walk in markPushedCommits to result length Without -n the walk was unbounded — a branch with many local-only commits would traverse full unpushed history per GetLog call. Capping at len(commits) keeps the two walks proportional. rev-list walks newest-first like GetLog, so the cap covers every commit we mark. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
5e3248a66b |
feat(web): pin tasks and drag-to-reorder in sidebar (#829)
Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
bce9fd266a |
feat: multi-question support for ask_user_question_kandev (#828)
* feat(backend): multi-question support for ask_user_question_kandev
Reshape the MCP tool from a single prompt+options pair to a questions[]
array (1-4 entries, single-choice each, custom text fallback). The
clarification HTTP handler gates the response until every question has an
answer, fans out one chat message per question (sharing pending_id, with
question_index/question_total in metadata), and returns a JSON map keyed
by question id back to the agent. The orchestrator resume prompt now
formats multi-line Q1/A1, Q2/A2 summaries when the bundle has more than
one question. Mock-agent gets a clarification-multi scenario and
ask_user_question_test.go covers the new schema + map response. No
back-compat shim — local-first tool, single-PR breaking change.
* feat(web): refresh clarification overlay with multi-question + modern UI
Each per-question chat message gets its own card with a "Question N of M"
progress chip, a ✓ Answered badge once committed, and shadcn-aligned
option buttons with kbd hints. A new useClarificationGroup hook coordinates
the per-question commits (decision A) and posts every answer in a single
batch once the user finishes the bundle; X rejects the whole group. The
single-question case keeps the 1-9 / Esc keyboard shortcuts. Playwright
e2e adds 6 multi-question scenarios (rendering, full happy path, partial
state, mixed option/custom, skip-rejects-bundle, sibling-state) on top of
the existing single-question regression suite.
* fix(web): clarification carousel UX with stepper navigation + race fix
Replaces the vertically stacked card layout with a horizontal stepper:
numbered circle dots up top show step state (active/answered/pending),
the single active question card sits in the middle, and Back / Next /
Submit buttons sit at the bottom. Picking an option auto-advances to the
next step (single-question case auto-submits, no explicit Submit button).
The stepper steps are clickable so users can revise prior answers.
Keyboard shortcuts: 1-9 for option pick, ArrowLeft/Right for navigation,
Esc for skip.
Also fixes a race that surfaced as a 400 BadRequest on the
/api/v1/clarification/:id/respond endpoint when the user clicked an
option before all N messages of a multi-question bundle had streamed in:
findPendingClarificationGroup now gates on metadata.question_total and
returns an empty array until the full bundle has arrived.
Playwright suite expanded from 6 to 12 multi-question scenarios:
stepper rendering, auto-advance, Back, stepper jump, Submit gating,
happy path, mixed custom-text, revise-via-jump, skip-from-mid-step,
number-key shortcuts, Esc skip, first-step Back disabled.
* fix(clarification): address CodeRabbit/Greptile/Claude review comments
Backend:
- Single source of truth for question validation: rename/export
NormalizeAndValidateQuestions and call it from both the HTTP and the
WebSocket-side MCP handlers, so duplicate question ids and bad option
counts can't slip through either path.
- httpRespond now validates that every answer references a real question
id from the bundle and that no id is answered twice (regression tests
added).
- questionsFromMessages reorders by metadata.question_index so the
reconstructed bundle preserves the original question order.
- adapters.CreateClarificationRequestMessages rolls back already-created
messages when a later write in the bundle fails.
- extractQuestionAnswers always returns a JSON map (even on rejection,
with an envelope { rejected, reject_reason }) and simplifyAnswer
preserves both selected_option and custom_text when both are present.
- MCP tool description: rewrite the response example to match the actual
output shape; add minItems / maxItems to the questions and options
arrays in the JSON Schema.
- Test helper: drop the local contains() shim, use strings.Contains.
Frontend:
- useMemo([props]) was a no-op (props is always a fresh ref); split into
[message, messages] so it actually memoizes.
- Drop the dead "expired" SubmitState branch — the backend never returns
410 from this endpoint.
- Remove the asymmetric option-list dimming: the agent now receives both
selected_option and custom_text together, so a half-grey state was
misleading.
- New tests: pending-clarification.test.ts covers
findPendingClarificationGroup including the question_total gate, and
use-clarification-group.test.ts covers the hook's record/submit/skip
flows and the override-merge path used by the single-question case.
* fix(web): treat clarification 409 as success so duplicate submit doesn't stick the overlay
* fix(clarification): unblock agent on persistence failure + nil-guard rejected branch
- Add Store.CancelRequest(pendingID) and call it from both the WebSocket
MCP handler and the HTTP create-request handler when message creation
fails. Without it, the agent's WaitForResponse parked for the full
2-hour MCP timeout while no clarification cards ever reached the user.
Both handlers now return an explicit error response on failure.
- applyAnswersToMessages now guards h.repo == nil in the rejected branch
(parity with sibling expectedQuestionIDs).
- extractQuestionAnswers stubs for unanswered questions in a rejected
bundle now carry { answered: false, rejected: true } so agents reading
per-question payloads see the rejection without inspecting the
envelope.
- Hook test: added 409-as-ok regression coverage for both submit and
skip paths so the previous fix can't drift.
- Store test: covers CancelRequest unblocking a parked WaitForResponse.
* fix: drop unused e2e helper + use channel sync in CancelRequest test
* refactor(web): drop dead 'message' prop from clarification overlay
Both call sites pass 'messages' (the bundle group), so the legacy
single-message prop was unused. Tightening the prop type also lets the
useMemo depend on a single value.
* fix(web): ignore modifier-keyed events in clarification carousel shortcuts
Bare 1..9 / ArrowLeft / ArrowRight / Esc still drive the carousel, but
Cmd/Ctrl+1..9 (browser tab switch) and Alt+ArrowLeft (browser back) now
pass through as the user expects.
|
||
|
|
c64e835612 |
feat: allow moving tasks across workflows (#822)
* feat: allow moving tasks across workflows * fix: address PR review feedback * fix: address remaining PR review feedback * fix: remove unreachable move state event * fix: map task move conflicts correctly * fix: address PR review nitpicks * fix: use neutral task move error message |
||
|
|
3e676e3bb2 |
feat(web): double-click tab to toggle maximize (#823)
* feat(web): double-click tab to toggle maximize Adds a dblclick gesture on dockview tabs that toggles maximize/restore on the containing group, mirroring the existing maximize button. Preview tabs keep their promote-to-pin shortcut: first dblclick pins, then dblclick maximizes. Sidebar group is excluded. * fix(web): add cursor-pointer to tab wrappers for consistency * fix(web): add cursor-pointer to plan-tab wrapper * fix(web): make tab dblclick maximize resilient to layout rebuild Refactor the dblclick-to-maximize handler so it reads the panel's group ID and store state at call time instead of from a render-time closure, and guards against firing while the dockview store is mid-layout-restore. This addresses oscillation when double-clicking a session tab on a task that has multiple sessions: the maximize call now stops event propagation, respects isRestoringLayout, and always uses the live api.group.id, so it remains correct even after dockview rebuilds groups via fromJSON. |
||
|
|
a4e0fc0452 |
feat: attribute cross-task agent messages with a sender badge (#819)
* feat(orchestrator): carry sender_task metadata through user message paths Add WithSenderTask builder to UserMessageMeta and a metadata-aware QueueMessageWithMetadata variant on the message queue. The drain path in event_handlers_agent merges queued metadata into the recorded user message so attribution survives the queue → DB transition. * feat(mcp): attribute message_task_kandev calls to the sending agent The MCP server inside agentctl now injects sender_task_id and sender_session_id from its own server context. The backend handler validates the sender (rejecting missing IDs, self-messages, and unknown tasks), looks up the sender title, wraps the prompt in a <kandev-system> attribution block, and propagates structured metadata across all three delivery paths (queue, created-session, record+prompt). The wrapped content lets the receiving agent identify the peer both live and on ACP session resume; the existing sysprompt strip pipeline hides the wrapper from the UI. * feat(web): render sender-task badge on cross-task agent messages User messages whose metadata carries sender_task_id now render a clickable purple badge above the bubble, linking to the source task. The badge live-resolves the sender's title from the kanban store and falls back to a greyed snapshot when the task isn't loaded (cross-workspace, deleted, etc.). Adds a non-subscribing useTaskById selector so badges don't fan out WS subscriptions per message. * test(e2e): cover cross-task agent message attribution Eight scenarios driven through the mock-agent's MCP script directive: queue path, prompt path, self-message rejection, unknown-target error, badge link target, live title re-resolution after rename, kandev-system wrapper preservation in raw_content, and prompt bodies containing their own kandev-system tags. Adds an updateTaskTitle helper to ApiClient. * fix: address CodeRabbit + Greptile P1 findings on PR #819 - requeueMessage now forwards queuedMsg.Metadata via QueueMessageWithMetadata so sender attribution survives transient delivery failures and retries. - CREATED-session branch in handleMessageTask records the user message AFTER StartCreatedSession succeeds, so a launch failure no longer leaves an orphaned attributed chat row. - SenderTaskBadge: collapse identical displayTitle/fullTitle locals. - E2E: extract waitForTargetIdle helper; replace the 1.5s raw sleep in the collision-check test with the same poll the rest of the suite uses. * fix: address Claude review suggestions on PR #819 - wrapAgentMessage strips </kandev-system> from the embedded sender title before formatting. Without this, a title containing the closing tag would short-circuit the non-greedy strip regex and leak the attribution tail into the visible UI bubble. Metadata snapshot keeps the original title so the badge still displays it. - Add a regression test covering the malicious-title case. - Document the deliberate cross-workspace messaging policy on the sender lookup so future readers don't add a workspace-scope check (Q13 of the feature plan locked this as intentional). |
||
|
|
cb0feea650 | fix(web): dedup topbar PR button against auto-shown PR panel (#812) | ||
|
|
c042050469 | fix: stabilize task switch flow and session recovery (#818) | ||
|
|
5c5e7bf6a7 |
fix(changes-panel): scope commits/diff to live branch divergence; flatten single-repo (#816)
* fix(changes-panel): scope commits/diff to live branch divergence; flatten single-repo Two bugs in the COMMITS panel made it surface commits and file changes that were already on main: 1. The commit list and the cumulative diff both anchored to a stale base. Commit list used `git merge-base HEAD <local-branch>` and the file diff used the session's stored base SHA captured once at session start. On long-lived worktrees the local main ref lags far behind origin/main, so the merge-base lands on an old SHA; once the agent merges main back in, the stored base is also stale. Both end up sweeping in dozens of main commits as if they were the branch's work. 2. `git log <base>..HEAD` follows both parents of a merge commit by default, surfacing main commits brought in via merges from main. Fixes: - agentctl computes merge-base against `origin/<target_branch>` (with a local-ref fallback for unfetched/local-only branches), and uses that for both the commit log and the cumulative diff. Mirrors GitHub's three-dot PR diff semantics — anchor to live divergence with the upstream, not a session-start snapshot. - `git log` now passes `--first-parent` so only the branch's own line of work plus its merge commits show up; commits brought in via merges from main are excluded. Frontend: drop the per-repo "REPOSITORY" sub-header in the COMMITS panel for single-repo workspaces. CommitsRepoGroup gains a `showHeader` prop; when the workspace has one untagged group the section renders flat with the per-repo Push / PR (and Stage all / Commit / Unstage all) buttons lifted into the section header. Multi-repo rendering unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address PR review feedback - Scope --first-parent to the divergence-range path only (Greptile P1). The flag is wrong for open-ended history calls — future activity widgets etc. need the full graph. - Log empty merge-base case in cumulative diff (Greptile P2). Silent fallback to stored base was invisible during diagnostics. - Rename TestRunGitCumulativeDiffForRepo_PrefersOriginRef → TestComputeMergeBase_CorrectAnchorForCumulativeDiff (Claude). Test exercises computeMergeBase, not the runGitCumulativeDiffForRepo conditional; new name reflects what it actually proves. - Add TestGetLog_NoBaseReturnsFullGraph to lock in the --first-parent scoping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address PR review round 2 - Drop dead ActionButtonsSection from changes-panel-timeline.tsx (unused export, no consumers anywhere). Brings the file from 640 → 516 lines, comfortably under the 600-line TS limit Claude flagged. - Extract isSingleRepoGroup helper into lib/group-by-repo.ts and use it in all three sites (CommitsSection, FileListBody, FileListSection) so single-repo detection has one source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a77df053a6 |
refactor(web): drop redundant VCS split button from task top bar (#804)
* refactor(web): drop redundant VCS split button from task top bar All git operations (commit, push, pull, rebase, merge, create PR, force push) are reachable from the Changes panel — its timeline exposes stage/commit and the per-section action row provides push/force-push/create-PR, while the panel header carries the pull/rebase/merge dropdown. The session command palette mirrors the same set for keyboard users. The expanded review dialog keeps its own VcsSplitButton because the changes timeline is hidden behind the modal there. Also removes the now-orphaned 'required' overflow flag (the VCS item was its only caller) and the e2e helper + spec that targeted the removed primary-action button. * test(e2e): drop task-topbar-overflow spec relying on removed VCS button The 1100px viewport test forced overflow by counting on the VCS button's width; with VcsSplitButton gone the scenario no longer holds. Overflow priority logic is already covered by topbar-action-overflow.test.ts. Co-authored-by: João Salavisa <jcfs@users.noreply.github.com> --------- 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 <jcfs@users.noreply.github.com> |
||
|
|
b2dcd29818 |
fix(web): tab session bugs — primary star drift + tab close re-creation (#800)
* fix(web): preserve primarySessionId across kanban.update broadcasts The kanban.update WS handler rebuilt kanban.tasks without primarySessionId / primarySessionState, wiping values set by task.updated. After any board refresh the primary star vanished and selectTaskWithLayout fell into the slower async fallback path. Move task building inside setState so existing values are preserved in both single and multi snapshots. * fix(web): prevent deleted session panel from being re-created handleDelete called removeTaskSession before containerApi.removePanel, which let useAutoSessionTab re-run with effectiveSessionId still pointing at the deleted session and re-create its panel. The setupChatPanelSafetyNet had a matching gap — it re-added a panel for activeSessionId without checking the session still existed. Switch the active session (or clear it) before removeTaskSession so effectiveSessionId updates first. Add guards to useAutoSessionTab and the safety net so neither creates a panel for a session that is no longer in the store. * test(web): add session tab management regression suite Cover the three bugs fixed in this branch end-to-end: deleting a non-active session keeps it gone, deleting the active session switches focus to the remainder, deleting then immediately switching tasks does not resurrect the tab, and the primary star survives both a kanban.update broadcast (triggered via apiClient.moveTask) and a task-switch round trip. * fix(web): hand off deletion to newest remaining session, document multi-snapshot fallback - session-tab.tsx: sort remaining sessions by started_at DESC and pick the newest, instead of relying on the unspecified store ordering. - kanban.ts: comment the multi-snapshot primarySessionId fallback so the double lookup does not look like redundancy. Both per Claude review feedback on #800. * fix(web): preserve explicit-null primary, abort delete on WS failure - kanban.ts: switch the multi-snapshot fallback from `??` to an explicit `=== undefined` check. An explicitly-cleared null primary must NOT be replaced by a stale snapshot value, only an absent key. - session-tab.tsx: wsAction now returns a boolean; handleDelete bails out before mutating the store / dockview when the backend session.delete request fails. Prevents client/server desync from a transient WS failure. - kanban.test.ts: add regression covering the null-vs-undefined merge. Per CodeRabbit and Greptile review on #800. * style(web): drop redundant .slice() in session deletion sort chain filter() already returns a fresh array, so the subsequent sort() does not mutate shared store state. Per Greptile review on #800. |
||
|
|
baaf7d593a | feat(web): polish workbench topbars and task-create dialog (#792) | ||
|
|
bb36cb9e01 |
refactor: move integrations to top-level settings with install-wide configs (#787)
* refactor: move integrations to top-level settings with install-wide configs Jira and Linear configurations are now install-wide singletons rather than per-workspace duplicates, and the GitHub/Jira/Linear settings pages live under /settings/integrations instead of being nested inside each workspace. Issue and PR watchers stay per-workspace via a workspace switcher inside each integration page. Backend ships one-shot migrations that promote the most-recently-updated row and copy its secret onto the new global key. * fix: address review feedback on integrations refactor Fix migration crash on installs whose jira_configs predate the auth-health columns by selecting only guaranteed-present legacy columns and falling back to defaults for the rest. Cover the empty-legacy-table and pre-health-columns upgrade paths with new store tests. On the frontend, refactor useIntegrationEnabled to take string parameters instead of an inline options object so getSnapshot stays referentially stable across renders, and stop dispatching a misleading no-op sync event when localStorage.setItem fails in private mode. * fix: hydrate workspace list on integration settings pages The integration pages wrapped their content in a fresh StateProvider, which created a nested store scope with an empty initial state and shadowed the settings layout's hydrated workspace list. WorkspaceScopedSection then saw zero workspaces and rendered the "Create a workspace to configure this integration" empty state even when the user had workspaces. Drop the redundant StateProvider from the jira and linear pages so they share the layout's store. The github page switches to StateHydrator so it can still seed the GitHub OAuth status without resetting the rest of the store. * refactor(integrations): flatten watcher tables, pick workspace in create dialog Hiding watchers behind a workspace switcher made the list invisible at a glance — a user with multiple workspaces had to flip between each one to see what was configured. Replace the switcher with a flat table that lists every watch across every workspace, with a Workspace column for context, and move workspace selection into the create dialog so the user picks once when the watcher is born. Backend: jira list-issue-watches and github list-review-watches / list-issue-watches accept the workspace_id query param as optional; when omitted, the corresponding store ListAll* method returns rows across every workspace. Per-row mutations still require workspace_id as the IDOR guard. Frontend: useReviewWatches / useIssueWatches / useJiraIssueWatches gain a three-state contract — string for one workspace, undefined for all workspaces, null to skip fetching. Watch tables grow an optional showWorkspace column. Watch dialogs accept an optional workspaceId prop; when absent, the dialog renders a workspace dropdown for the create flow (disabled when editing existing rows so refs aren't orphaned). Github review-watch-dialog crossed the 600-line lint cap so the prompt editor moved to review-watch-prompt-field.tsx. * fix(integrations): make watcher step dropdown reactive across workspace switches Switching to a workspace whose workflows had to be fetched left the workflow step dropdown stuck — the user could pick a workflow but the step Select either showed the previous workflow's steps or appeared inert. The cause was the per-dialog useWorkflowSteps hook deferring its empty-state reset via Promise.resolve().then(() => setSteps([])) to dodge a setState-in-effect lint rule, which let stale steps survive into the next render and gave Radix nothing to anchor the popover to during the in-flight fetch. Hoist the hook into a shared useWorkflowSteps that resets via React's official setState-during-render pattern, exposes a loading flag, and returns a stepPlaceholder helper so the Select can render "Loading steps…" or "No steps in this workflow" instead of an empty popover. The Step Select is now also disabled while loading or when there are no steps, making the state of the dropdown unambiguous. * fix: address PR review feedback on integrations refactor Backend: - linear migration: add column-presence guard mirroring the Jira fix so installs that upgraded from the original Linear schema (without org_slug / last_checked_at / last_ok / last_error) don't crash startup with "no such column"; add TestStore_MigrateLegacyPerWorkspaceTable_PreHealthColumns - jira/handlers.go: drop the now-unused errMsgWorkspaceIDRequired constant that was failing golangci-lint - jira/mock_controller_test.go (new): cover setAuthHealth happy-path and invalid-JSON-rejection so the singleton-shaped payload is locked in Frontend: - useIntegrationAvailable: gate the auth-health probe on loaded && enabled so disabled integrations don't poll /config every 90s - use-workflow-steps: initialize loading to !!workflowId so edit-mode opens show "Loading steps…" instead of "No steps in this workflow" before the first fetch lands - use-workflow-steps.test.ts (new): cover the empty/non-empty workflowId, workflow switch, fetch error, and stepPlaceholder branches - use-linear-enabled.test: tighten the legacy-key migration assertion to pin the migrated value rather than just verifying key cleanup --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
32f098a6fc |
feat: show question icon only for pending input (#782)
* fix: show question icon only for pending input * fix: address pending input review feedback * fix: reduce pending input sidebar complexity * fix: address clarification review comments * test: cover pending clarification hook |
||
|
|
58fb9ffb0f |
fix: dedupe cancel-turn clicks to stop "turn cancelled" cascade (#784)
* fix: dedupe cancel-turn clicks to stop "turn cancelled" cascade The cancel button had no in-flight feedback, so impatient users clicked it repeatedly while the agent was tearing down a slow turn (e.g. a Claude Monitor tool). Each click reached the backend and emitted its own "Turn cancelled by user" message, with the second-and-later calls also lazily creating phantom turns to host those messages. Fix on both sides: SubmitButton now tracks isCancelling locally and disables itself with a spinner while the request is in flight; the orchestrator deduplicates concurrent CancelAgent calls per session via a sync.Map guard so duplicates that slip through return immediately. * fix: address PR review feedback - Test: replace sleep-based polling in cancel-dedupe test with channel synchronization (cancelAgentEntered) per project convention. - UI: switch the cancel re-entrancy guard from state to a ref so isCancelling no longer thrashes the useCallback dep array. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
1394b7103f |
feat: add recent task switcher (#779)
* feat: add recent task switcher * fix: address task switcher review feedback * test: cover recent task cleanup * perf: reduce recent task switcher storage reads |
||
|
|
c853044f1d |
feat(ui): collapse commits and PR changes by default in changes panel (#781)
* feat(ui): collapse commits and PR changes by default in changes panel * fix(e2e): re-expand commits section after remount in squash and focus tests CommitsSection unmounts mid-transition (when both hasStaged and hasCommits are briefly false), so it remounts with defaultCollapsed=true and loses the user's expanded state. Re-expand before assertions that follow such remounts. --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
a510b46e4e |
feat(multi-repo): support tasks spanning multiple repositories (#767)
* feat(task): add task_environment_repos schema and model for multi-repo support
Phase 0 of multi-repository task support: introduces the per-repository
child table and model alongside legacy single-repo fields (dual-write,
no behavior change). Lays the foundation for the orchestrator/lifecycle
refactor in subsequent phases.
- New task_environment_repos table with FK CASCADE on task_environments.
- New task_dir_name column on task_environments (idempotent ALTER).
- TaskEnvironmentRepo model + Repos slice + RepoFor helper on TaskEnvironment.
- CRUD methods + interface extension; env getters preload Repos.
- Idempotent backfill from legacy single-repo task_environments rows.
- Tests cover round-trip, CRUD, preload ordering, embedded create, cascade
delete, and backfill from a legacy-shaped row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(lifecycle): add Repositories list to LaunchRequest and EnvPrepareRequest
Phase 1.1 of multi-repository task support: extends the lifecycle types
with explicit per-repo lists, while keeping the legacy single-repo
top-level fields as the synthesized fallback. No caller behavior changes
yet — preparers still see length-1 specs for single-repo launches.
- LaunchRequest.Repositories []RepoLaunchSpec + RepoSpecs() helper.
- EnvPrepareRequest.Repositories []RepoPrepareSpec + RepoSpecs() helper.
- EnvPrepareResult.Worktrees []RepoWorktreeResult for per-repo outcomes.
- runEnvironmentPreparer forwards req.Repositories into the prepare
request, inheriting the request-level RepoSetupScript when each
per-repo entry has none.
- Tests cover explicit-list precedence, synthesized fallback, and
repo-less requests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree,lifecycle): support multiple worktrees per session
Phases 1.2 and 1.3 of multi-repository task support. The pieces are
intertwined so they ship together — the preparer's per-repo loop
depends on the manager being able to hold one worktree per
(session, repository) pair instead of one per session.
Worktree manager:
- In-memory cache key changes from sessionID to (sessionID|repositoryID).
- New MultiRepoStore optional capability + SQLite implementation:
GetWorktreesBySessionID and GetWorktreeBySessionAndRepository.
- New Manager methods: GetAllBySessionID, GetBySessionAndRepo. Existing
GetBySessionID returns the first match (single-repo callers unchanged).
- Manager.Create's "already exists" check is now repo-scoped so the
second repo's create no longer returns the first repo's worktree.
- All cache write/delete sites updated to the composite key.
WorktreePreparer:
- New prepareMultiRepo path activated when EnvPrepareRequest.Repositories
has >=2 entries; single-repo and repo-less behavior is byte-identical.
- prepareOneRepo runs validate -> sync -> create -> fetch-PR -> per-repo
setup-script for each spec; rolls back already-created worktrees on
any failure.
- Workspace path becomes the task root (parent of any repo subdir);
legacy WorktreeID/WorktreeBranch/MainRepoGitDir mirror Worktrees[0]
for downstream code that hasn't been migrated yet.
- Per-repo setup script runs verbatim (no script-engine resolution
yet — the per-repo placeholder namespace lands in a follow-up).
Tests:
- Manager: GetAllBySessionID, GetBySessionAndRepo (hit + miss),
GetBySessionID single-repo compat, cache key separation.
- Preparer: multi-repo creates one worktree per repo; rollback removes
active worktrees on partial failure; per-repo setup script runs in
each worktree directory and emits its own step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(orchestrator): resolve all task repos and persist per-repo rows on launch
Phase 1.4 of multi-repository task support: the orchestrator now reads
every repository linked to a task, forwards the full list through the
adapter into the lifecycle layer, and persists one TaskEnvironmentRepo
plus one TaskSessionWorktree row per repo when the launch is multi-repo.
Single-repo and repo-less tasks keep their previous code path verbatim
(dual-write — legacy single-repo fields stay populated from the primary).
- New resolveAllRepoInfo / resolveTaskRepoInfo on the executor; primary
is the first by Position.
- buildLaunchAgentRequest takes both the primary repoInfo and the full
list, populating req.Repositories []RepoSpec only when length > 1.
- New RepoSpec / RepoWorktreeResult on the orchestrator's executor
package mirror the lifecycle types so the adapter can translate
between them without leaking lifecycle types into the orchestrator
public API.
- Adapter (cmd/kandev/adapters.go) forwards req.Repositories into
lifecycle.LaunchRequest and surfaces per-repo prepare results back
on LaunchAgentResponse.Worktrees.
- persistTaskEnvironment writes env.Repos in the same create
transaction; persistTaskEnvironmentRepos handles the existing-env
reuse path. TaskEnvironment.TaskDirName is now persisted.
- persistWorktreeAssociation loops over resp.Worktrees in multi-repo,
inserting one row per repo with its own RepositoryID.
- ResumeSession now also populates req.Repositories from task repos
and stamps the existing TaskDirName so the preparer reuses the same
task root.
- repoStore / executorStore interfaces extended with the new repo and
env-repo CRUD methods; mocks updated accordingly.
Tests:
- LaunchPreparedSession with two task repositories populates
req.Repositories in order and keeps legacy single-repo top-level
fields filled from the primary.
- The same launch persists one TaskEnvironment + 2 TaskEnvironmentRepo
+ 2 TaskSessionWorktree rows.
- Single-repo launch still produces zero entries in req.Repositories
and exercises only the legacy fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): git endpoints accept optional repo subpath for multi-repo
Phase 2 of multi-repository task support: every per-call git endpoint
gains an optional `repo` field (form param for GET, JSON for POST) that
selects a sub-directory of the workspace. Empty `repo` = workspace root,
preserving single-repo behavior verbatim.
- Manager.GitOperatorFor(subpath) returns a per-subpath GitOperator
(cached). Validates the subpath against path traversal, absolute
paths, and missing directories.
- New api server helper gitOpForRepo() emits a 400 with a per-operation
GitOperationResult on invalid subpaths.
- Repo field added to: pull, push, rebase, merge, abort, commit,
rename-branch, stage, unstage, discard, create-pr, revert-commit,
show-commit, reset, log, cumulative-diff. (git/status still reads
from the workspace tracker — that endpoint will gain multi-repo
support when the tracker itself is split per repo.)
- Tests cover empty subpath, valid subpath caching, traversal
rejection, and missing-directory rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/comments): scope diff comments by repository for multi-repo tasks
Phase 3 of multi-repository task support: comments now carry an
optional repositoryId so two repos with the same file path render
their own comment threads instead of bleeding together.
- CommentBase.repositoryId (optional, backwards compatible).
- getCommentsForFile gains an optional repositoryId arg; legacy
comments without a repositoryId match any repo filter so persisted
data keeps showing.
- Diff store interface in lib/diff/types.ts mirrors the new arg.
- Tests cover per-repo filter, omitted-arg fallback, and legacy data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(github): per-repo PR association on task PRs and watches
Phase 4 of multi-repository task support: github_task_prs and
github_pr_watches gain a repository_id column so multi-repo tasks can
have one PR per repo without colliding on (task_id) uniqueness.
- Idempotent ALTER TABLE adds repository_id to both tables.
- TaskPR.RepositoryID and PRWatch.RepositoryID model fields.
- New GetTaskPRByRepository and ListTaskPRsByTask lookups; existing
GetTaskPR keeps single-PR semantics for callers not yet migrated.
- ReplaceTaskPR's delete is scoped by repository_id when set so a
multi-repo task replacing one repo's PR no longer wipes the other.
- Tests cover per-repo storage and scoped replace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): foundations for multi-repo task UI
Phase 5 of multi-repository task support: primitives the upcoming UI
work needs without changing any visible behavior yet.
- TaskRepository.checkout_branch surfaces the optional PR head branch
field that the backend already exposes.
- primaryTaskRepository(repos) picks by lowest Position so consumers
match the backend's "primary" semantics instead of `[0]`.
- New useTaskRepositories(taskId) / useTaskPrimaryRepository(taskId)
hooks read the live kanban task and return its repos sorted by
position. Future kanban/diff/UI consumers should use these instead
of poking task.repositories directly.
- Tests cover lowest-position selection and empty/single-repo edge cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): kanban card shows multi-repo "+N" chip and hydrates full repo list
Phase 6 (subset) of multi-repository task support. The kanban now
exposes the per-task repository list to the UI and renders a compact
"+N" badge on cards whose tasks span more than one repository.
- KanbanState.tasks[].repositories slim shape (id, repository_id,
base_branch, checkout_branch, position).
- SSR mapper hydrates the full list and uses primaryTaskRepository()
to pick the position-aware primary instead of [0].
- useKanbanData passes repositories through to the card props.
- KanbanCard renders the primary repo name with a small "+N" chip
when len(repositories) > 1; tooltip names the count.
- useTaskRepositories / useTaskPrimaryRepository hooks return the
slim KanbanTaskRepository shape so multi-repo consumers share one
read path.
Larger UI surfaces (task-create dialog, diff view, file browser,
mobile sheets) still target the primary repo only — they will be
ported as the multi-repo task creation flow lands. No visible change
for single-repo tasks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(task): reject duplicate repositories on multi-repo task create
Phase 8 of multi-repository task support: surface duplicate-repository
input as a clean validation error instead of letting the unique
(task_id, repository_id) constraint surface mid-loop with some rows
already inserted.
- createTaskRepositories tracks seen repository IDs and returns
"repository <id> listed more than once for the task" before any
CreateTaskRepository call.
- Tests cover the rejection path and confirm two distinct repos still
succeed with both rows persisted in Position order.
Repository discovery and per-repo cloning were already covered by
Phase 1.4 (resolveAllRepoInfo loops ensureRepoCloned) and the
existing repository_discovery service is repo-list aware by design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-create): repeatable repository rows for multi-repo task creation
Users can now actually create a multi-repo task: a "+ Add repository"
button below the primary repo + branch selectors expands the form with
additional (repository, base-branch) rows. Each extra row picks from a
filtered repository list that excludes the primary and any other already-
chosen extras, so duplicates are impossible to enter.
- ExtraRepositoryRow type + form-state hook (extraRepositories, add /
remove / update / reset). Lives in its own file so the main state hook
stays under the file-length lint cap.
- ExtraRepositoryRows component renders the rows and the "+ Add" button;
per-row branches are loaded on demand via the existing
useRepositoryBranches hook.
- buildRepositoriesPayload accepts the extras and appends them after the
primary, dropping rows with empty repository_id silently so an
unfinished row does not block submit. The backend's duplicate-repo
validation (Phase 8) catches anything that slips through.
- DialogFormBody is split: CreateModeSelectors (selectors + extra rows)
and useDialogHandlers move to dedicated files, keeping touched files
under the lint thresholds.
- Tests cover the multi-repo payload (ordering, drop-empty, no-primary
recovery, single-repo unchanged) and the existing
store-multi-repo.test.ts gets named constants to satisfy the
no-duplicate-string rule.
Verified: pnpm exec tsc --noEmit clean, vitest 608/608 green,
eslint --max-warnings 0 clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): per-repo workspace tracker for multi-repo git/status
The git/status endpoint now accepts an optional ?repo=<subpath> query
param that selects a sub-directory of the workspace, completing the
per-repo plumbing started in the multi-repo phase. Single-repo callers
keep the workspace-root behavior unchanged.
- Manager.GetWorkspaceTrackerFor(subpath) returns a per-subpath
WorkspaceTracker (lazy + cached). Validation reuses a new
resolveSubpath helper shared with GitOperatorFor, so both code paths
reject path traversal / absolute paths / missing dirs the same way.
- Per-subpath trackers are NOT started (no polling goroutines); the
multi-repo path uses them only for synchronous GetCurrentGitStatus
queries which fall through to getGitStatus when no cache is present.
This keeps per-repo cost cheap.
- handleGitStatus reads c.Query("repo") and routes through the new
resolver, returning a 400 with an error payload on invalid input.
- Tests cover empty subpath (root), valid subpath caching, traversal
rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/review): group file tree by repository for multi-repo task changes
The review file tree now renders per-repo top-level nodes when a task's
changes span multiple repositories, so the same file path in two repos
no longer collapses into a single ambiguous entry.
- ReviewFile.repository_id and ReviewFile.repository_name (optional)
identify which repo each file belongs to. Single-repo callers leave
them unset and the tree falls back to the existing flat grouping.
- buildFileTree splits into buildMultiRepoTree (when 2+ distinct
repository_names are present) and buildFlatTree (the original logic).
Repo roots have isRepoRoot=true and a repositoryId for downstream
filters; their children are full path subtrees per repo.
- collapse() skips repo roots so a repo with a single changed file
still shows its repo header.
- ReviewFileTree renders repo roots with a stronger label, a top
border, and an "N files" count badge; data-testid="repo-root-node"
for E2E coverage. File-level rendering is unchanged.
- Tests cover the flat-tree fallback (no repo info, single repo),
multi-repo grouping, repo-root pinning, and same-filename-different-
repos isolation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-top-bar): "+N" repo chip on multi-repo task header
The task page top-bar breadcrumb now shows a small "+N" chip next to the
primary repository name when the task spans more than one repository,
matching the kanban-card treatment introduced earlier. Hover surfaces the
exact count via tooltip ("frontend (+2 more repos)").
- resolveTaskProps populates repositoryCount from task.repositories.length.
- TaskTopBar threads repositoryCount through to the new
RepositoryBreadcrumb subcomponent.
- The chip carries data-testid="topbar-extra-repo-count" for E2E
selectors.
- TopBarLeft was nearing the function-length / complexity caps; the
breadcrumb extraction restores headroom for future additions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web/task-create): disable "Add repository" when no other repos available
When the user only has the primary repository in their workspace,
clicking "+ Add repository" used to insert a row whose dropdown was
empty — looking like a bug ("no other repos show up"). The button now
disables itself when there are no remaining workspace repositories to
choose from, with a tooltip explaining why ("All workspace repositories
are already added"). The previous "primary not selected" guard is also
surfaced via tooltip.
- ExtraRepositoryRows computes remainingCount = repos − used (primary
+ already-added extras) and gates the Add button on it.
- New computeAddButtonHint helper returns a per-state title string for
the disabled cases.
- Tests cover: dropdown shows the unused workspace repos when opened
(the click path the user reported wasn't actually broken — the gate
was just missing); Add button disabled when only the primary exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(agentctl): workspace tracker falls back to first repo subdir for multi-repo task roots
The "Changes" panel was empty for multi-repo tasks because the
agent's workspace path is the task root — a plain holder directory
that holds one git worktree per repository as siblings. The workspace
tracker tried to run git commands against that bare root and silently
failed, so no git status updates ever reached the UI.
The tracker now detects this at construction: when the configured
workDir isn't itself a git repo, it falls back to the first immediate
child directory that is. The user gets the primary repo's changes in
the panel; cross-repo aggregation is a follow-up.
- preferGitRepoChildIfRootIsBare scans os.ReadDir entries (skipping
hidden / non-directory entries) and returns the first that has a
valid git index. Logs an info line when the rewrite happens so it's
visible in the agentctl log.
- Repo-less tasks (e.g. quick chat) and single-repo tasks keep their
previous behavior verbatim — the fallback only fires when the root
itself fails resolveGitIndexPath.
- Tests cover: fallback when the workdir is a multi-repo holder,
no-fallback for a real git repo, no-fallback when no git child
exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo workspace tracking — Changes panel shows all repos
The agent's workspace tracker is now split per-repository for multi-repo
task roots. Each repo gets its own tracker that polls git independently
and stamps its repository name onto every emitted event. The Changes
panel aggregates files from every repo and groups them under per-repo
headers using the file-tree machinery from Phase 6.
Backend (agentctl):
- GitStatusUpdate / FileListUpdate gain a RepositoryName field. Empty
for single-repo workspaces (legacy clients see no change).
- WorkspaceTracker carries repositoryName and stamps it onto every
emitted status/file-list. Two constructors: NewWorkspaceTracker for
the legacy single-repo path (with the bare-root fallback) and
NewWorkspaceTrackerForRepo for explicit per-repo trackers.
- Manager scans direct child directories for git repos at construction.
When 2+ repos are found, builds one tracker per repo subdir on top of
the root tracker (which stays bound to the bare task root and emits
nothing useful). Single-repo workspaces are unchanged.
- Manager.SubscribeWorkspaceStream / UnsubscribeWorkspaceStream fan out
one client subscription across the root + all per-repo trackers via
AttachWorkspaceStreamSubscriber / DetachWorkspaceStreamSubscriber on
the tracker. The WS handler switched from the single-tracker subscribe
to the manager fan-out.
- Tests cover: per-repo trackers spawn for multi-repo task roots,
single-repo doesn't, both repos emit GitStatusUpdates tagged with
RepositoryName.
Frontend:
- GitStatusEntry / GitStatusData carry repository_name (optional).
- session-runtime gitStatus.byEnvironmentRepo new map keyed by env id
then repository name; setGitStatus routes per-repo updates into it
while still updating the legacy byEnvironmentId for single-repo
consumers. clearGitStatus drops both maps.
- migrateEnvKeyedData migrates the new per-repo map alongside the
existing keys when sessionId resolves to an environmentId.
- WS git-status handler forwards repository_name from event.status.
- New useSessionGitStatusByRepo hook returns sorted [{repo, status}]
pairs for the active session.
- task-changes-panel mergeReviewFiles iterates statusByRepo when
available, stamping repository_name onto every uncommitted file so
the Changes panel groups them under per-repo headers (using the
ReviewFile.repository_name plumbing from Phase 6).
- Tests: per-repo routing, no overwrite on sibling repo update,
legacy single-repo fallback, clear-on-cleanup.
Verified: pnpm exec vitest run 623/623, eslint --max-warnings 0 clean,
tsc --noEmit clean, make test (backend) clean, make lint 0 issues.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers above each repo's files
The Changes panel now renders a sticky "frontend / N files" header above
the diffs from each repository so the user can visually distinguish
where each change lives. Single-repo tasks render unchanged (no header
shown when there's only one unnamed group).
- ReviewDiffList groups its file list by repository_name (preserving the
caller's order within each group), then renders one RepoGroupHeader
per group followed by the existing FileDiffSection list. Header gating
rule: show when len(groups) > 1 OR the single group has a non-empty
repository name.
- Group key uses "repo:path" so a file with the same path in two repos
no longer collides on React's reconciliation.
- groupFilesByRepository + RepoGroupHeader live in a sibling
review-diff-list-groups.tsx file to keep review-diff-list.tsx under
the 600-line lint cap.
- Tests cover: no header for plain (no repo_name) files, one header per
repo for 2+ groups, header shown even for a single named repo, file
paths land under the right group's section.
Verified: vitest 627/627, eslint clean, tsc clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(lifecycle): propagate RepositoryName through PublishGitStatus
The agentctl WorkspaceTracker stamps every per-repo GitStatusUpdate
with RepositoryName, but the lifecycle PublishGitStatus translation
into GitEventPayload was dropping that field. The orchestrator then
forwarded a status with empty repository_name, the frontend store
routed everything into the empty-key bucket, and the Changes panel's
multi-repo grouping never triggered — no header, no per-repo split.
- GitStatusData gains RepositoryName (omitempty so single-repo
payloads stay byte-identical on the wire).
- PublishGitStatus copies update.RepositoryName onto the payload.
- Test asserts the field survives the publish → subscribe round trip
via the in-memory event bus on the GitEvent.<sessionID> subject.
Restart the agentctl process for any running multi-repo task to pick
up the new field; new launches get it automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers in the dockview Changes tab
The Changes tab in the dockview panel (the Files / Changes (N) tab the
user actually interacts with — not the standalone Review dialog) was
still rendering a flat file list, so multi-repo task changes either
collided onto the legacy single-status (showing only one repo's files)
or rendered without grouping.
- FileInfo gains repository_name (optional, single-repo unchanged).
- useSessionGit aggregates files from useSessionGitStatusByRepo when
multi-repo data is present, stamping each FileInfo with its
repository_name. Falls back to the legacy single status for
single-repo workspaces. Aggregation extracted into
aggregateFilesAcrossRepos to keep useSessionGit under the 100-line
function lint cap.
- ChangedFile (changes-panel.tsx + changes-panel-timeline.tsx) carries
repositoryName through to the renderer.
- FileListSection delegates to a new FileListBody that groups rows by
repositoryName; renders a small per-repo subheader when 2+ repos OR
any single named repo is present, otherwise falls back to the
pre-multi-repo flat list (same DOM shape).
- Tests cover: no header for plain (no repo_name) files, headers
per repo for 2+ groups, single named repo still gets a label.
Restart the backend so the per-repo trackers spawn for in-flight
multi-repo tasks; new launches are unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo git ops, unified branch endpoint, chip-style task create
Bundles a string of multi-repo fixes and refactors that came up while
testing the feature end-to-end:
- workspace_path stored on multi-repo TaskEnvironment was one level too
high, so agentctl booted with the wrong WorkDir on cold start; fixed
in resolveTaskEnvWorkspacePath + GetWorkspaceInfoForSession.
- Stage/Unstage/Discard/Commit/Revert/Reset/Amend now thread `repo`
through agentctl client → WS handler → frontend hook → SessionGit
fan-out, so the right git repo is targeted (was running at the task
root, which isn't a git repo for multi-repo tasks).
- agentctl GitLog is multi-repo aware: when no `repo` is specified and
per-repo trackers exist, fans out one log per repo and merges sorted
newest-first with RepositoryName stamped on each commit.
- Per-repo grouping for files (Changes panel) and commits, with
collapsible group headers; commits section defaults to collapsed.
Algorithm extracted into one generic groupByRepositoryName helper
(was copy-pasted three times).
- Task creation dialog rewritten around a unified `fs.repositories[]`
list — no more "primary vs extras" split. Chip-style row with one
pill per repo + branch, "+" to add, x to remove, GitHub URL toggle
inline. Discovered (on-machine) repos appear in any chip's dropdown.
Removes ~6 fields and 3 helpers from the dialog state.
- Branch listing endpoints collapsed to one:
GET /workspaces/:id/branches?repository_id=X | path=Y
Frontend collapses to a single useBranches hook with a tagged-union
source. Drops two HTTP routes, two service methods, two API client
funcs, and one whole hook file.
Tests added across the chain: workspace_path resolver, sort-commits-
by-date, manager poll-mode propagation, RepoSubpaths, chip grouping
+ collapse + per-repo isLatest, group-by-repo helper, dialog payload
builder + chip wiring + discovered-repo flow.
* feat(multi-repo): unified per-repo git controls for all VCS operations
Changes panel and top-bar VCS button render per-repo Stage all, Commit,
Unstage all, Push, Pull, Rebase, Merge, and Create PR uniformly across
single-repo and multi-repo workspaces. Operations route to the correct
git repo subpath; fan-outs collect per-repo results and surface partial
success in the toast layer.
Backend:
- Stamp RepositoryName on Reset/BranchSwitch/FileChange notifications
and forward through orchestrator handlers
- Surface per-repo errors from handleGitLogMultiRepo via PerRepoErrors
Frontend:
- pull/push/rebase/merge/abort/commit fan out via PerRepoOperationResult
- Per-repo group headers always render with action buttons
- Top-bar dropdown lists per-repo Pull/Rebase/Merge entries
- pendingStageFiles keyed per-repo to prevent cross-repo clears
- useRepoDisplayName distinguishes loading from multi-repo
- Commit dialog file summary respects stageAll
- New helpers: changes-panel-helpers.ts, changes-panel-repo-groups.tsx,
use-repo-display-name.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): plumb repo through file/commit/diff endpoints; per-repo PRs and review UI
- backend: thread `repo` subpath through pull/push/rebase/merge/abort, file
content/update, cumulative-diff, and commit-show endpoints; per-repo
fan-out for log + cumulative-diff with each repo's own merge-base; emit
full `repositories` array on task.updated WS payload
- github: store/list TaskPRs as []*TaskPR (per-repo); orchestrator/web store
rekey to `Record<taskId, TaskPR[]>`
- web: aggregate PR icon (count badge + worst-of color, multi-PR tooltip);
branch hover card renders one row per repo with each repo's task base
branch; review dialog merges per-repo file maps with composite keys and
propagates repository_name through diff/expand/revert; commit-detail
panel scopes to the owning repo
- kanban card: chip row of repo names (up to 2 + "+N" overflow, tooltip
lists all)
- task-create dialog: gate non-worktree executors to multi-repo only; move
fresh-branch toggle inline next to the repo chips (icon-only, with
title/aria-label)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(multi-repo): scope PR watches/associations and review UI per repository
Schema: add repository_id to UNIQUE constraints on github_pr_watches and
github_task_prs (with idempotent copy-and-rename migration) so one task
spanning N repos can hold one watch/PR per repo without collisions.
Plumb repository_id through CreatePRWatch/EnsurePRWatch/AssociatePRWithTask
/AssociatePRByURL, fan TriggerPRSync across all per-repo watches, and
disambiguate status sync by (task, owner, repo, pr_number) instead of
"first match". Add Repo to wsCreateFile/Delete/Rename and wsCreatePR/
RenameBranch so file and git ops target the right working tree.
Frontend review dialog: switch reviewedFiles, staleFiles, fileRefs,
selectedFile and comment counts to NUL-separated `${repo}\x00${path}`
keys so same-named files in different repos no longer share state.
useTaskPR consumes the new {prs: TaskPR[]} envelope.
Also: snapshot repoTrackers slice before background refresh in
SetWorkspacePollMode to prevent races with Stop(), and key
mergeCumulativeFiles by NUL-separated (repo, path) to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: apply prettier formatting
Unblock CI prettier check on files modified earlier on this branch
that pre-date the current prettier config.
* fix: address PR review feedback and unblock E2E
Backend:
- ListWorkspaceTaskPRs background sweep now uses TriggerPRSyncAll so
multi-repo tasks refresh every per-repo PR (was only the most recently
updated watch). Caught by claude review on #767.
Frontend (no behaviour change):
- Replace literal NUL byte (0x00) in FILE_KEY_SEP and the inline key
builder in use-review-dialog with the explicit escape so the
source is plain ASCII. The runtime separator is unchanged; this just
removes a footgun that GitHub's diff renders as a visible space.
- Show the auto-resolved branch as a small read-only pill next to the
GitHub URL input so the URL-mode dialog surfaces the branch the task
will run on (previously only the URL was visible).
E2E:
- changes-panel-focus / pr-detection / pr-switcher-changes: expand the
commits-section before asserting on commit text -- the section starts
collapsed since the chip-style refactor.
- repository-selector-scroll: open the popover via repo-chip-trigger
instead of the legacy single repository-selector testid.
* test(e2e): align task-create dialog tests with chip refactor
Updates three more tests that broke when the dialog migrated from a
single repository/branch combobox to per-row repo/branch chips and the
URL-mode toggle relabel:
- create-task.spec.ts (pre-select + remembers-selections): switch
repository-selector / branch-selector lookups to repo-chip-trigger /
branch-chip-trigger.
- create-task-github-url.spec.ts (toggle test): the inverse-state label
is now "use a workspace repository", not the legacy
"or select a repository".
No code changes — purely test alignment with the existing UI.
* fix(web): re-apply workbench-chrome topbar + restore branch-selector behaviour
Two regressions surfaced by the multi-repo merge into main:
1. task-top-bar.tsx had been resolved in our favour at the workbench-chrome
merge (#761), losing the TopbarActionOverflow wiring that the new test
asserts on. Re-applies main's topbar structure (TopbarCluster groups,
TopbarActionOverflow, IntegrationsMenu, MoreToolsMenu, SettingsButton)
on top of our multi-repo additions: repositoryCount + RepositoryBreadcrumb
"+N" chip, gitStatus + isMultiRepo plumbing, and the GitAheadBehindBadges
suppression for multi-repo. GitAheadBehindBadges moved to its own file to
stay under the file-length cap.
2. The chip-style task-create dialog dropped the legacy branch-selector
semantics. Restored:
- Local-executor branch lock: the per-row branch chip is now disabled
when isLocalExecutor and fresh-branch is off. The user's checkout
dictates the branch for local execution; fresh-branch unlocks it.
- Branch override: when locked, the chip shows currentLocalBranch
instead of the value the user picked under a different executor
(so worktree -> local doesn't display a stale "develop" pick).
- Branch refresh: useBranches now exposes a refresh() callback for
id-based sources (calls listRepositoryBranches with refresh=true),
and Pill renders BranchRefreshButton in its popover header when
onRefresh is wired.
- URL-mode branch picker: GitHubUrlSection now renders a real
branch-chip-trigger Pill backed by fs.githubBranches instead of a
read-only span, restoring the "branch selectable in URL mode"
behaviour (URL mode is always remote-clone, so the local-executor
lock does not apply).
E2E spec updated: bulk-rename legacy branch-selector / repository-selector
testids to branch-chip-trigger / repo-chip-trigger.first(). All 13
create-task-branch-selector tests pass locally.
* ci: force workflow re-run
* fix(web): unblock E2E shard 2
Three issues showed up only after the merge with main, which brought back
the E2E shard 2 tests touching git/changes-panel and improve-kandev:
1. Wire the TaskCreateDialog slot props (extraFormSlot, aboveDescriptionSlot,
bottomSlot, descriptionPlaceholder) through to DialogPromptSection. They
were declared on the public API but dropped on the floor before this
change, so Improve Kandev's contributor banner ("@octocat" /
"@kandev-maint") never rendered. Also gate the "or paste a GitHub URL"
toggle on lockedFields.repository so the locked-repo dialog hides it
(matches main's behaviour and the existing test expectation).
2. Revert "commits section starts collapsed" (introduced in
|
||
|
|
6fa38cd62c |
fix(terminal): re-key user-shell RPCs to task_environment_id (#770)
* refactor(backend): re-key user-shell RPCs to task_environment_id user_shell.list/create/stop now take task_environment_id directly instead of session_id with a session→env translation. Sessions in the same task share one shell list, so the env is the natural scope. HTTP route /api/v1/sessions/:id/terminals → /api/v1/environments/:id/terminals. shell.status/subscribe/input (agent passthrough) stay session-keyed. * fix(backend): heal task_sessions.task_environment_id on boot Sessions created via paths that didn't write the FK left shell ops broken — every env-keyed user-shell RPC failed to resolve the env and the frontend silently swallowed the error. The boot heal links each orphan session to its task's existing env (idempotent, runs after backfillTaskEnvironments + healDuplicateTaskEnvironments). * refactor(web): drop session_id from user-shell API, hooks, and slice createUserShell/stopUserShell/fetchTerminals take environmentId. useUserShells takes envId directly (no environmentIdBySessionId translation). useTerminals takes both (env for shell ops, session for tab UX persistence). Slice actions key writes by env id directly, removing the silent no-op when the session→env mapping wasn't set yet. * refactor(web): wire env id through dockview, right-panel, and SSR call sites Dockview "+" menu, terminal scripts dropdown, dev-preview button, right-panel "+" button, Ctrl+\` toggle, watermark, and panel-close cleanup all source the env id from useEnvironmentId() / store and pass it to createUserShell/stopUserShell. Shell-mode passthrough terminal no longer blocks on session id — env id is sufficient. SSR resolves the active session's task_environment_id from the session list and calls fetchTerminals with the env id. * test(e2e): cover env-keyed terminal new-tab flows + WS payload guard New spec exercises the right-panel "+" button and the dockview "+ → Terminal" menu, asserts the new tab connects, and intercepts outgoing user_shell.create WS frames to verify the payload carries task_environment_id and never session_id. A heal-coverage assertion checks every listed session has task_environment_id populated post-boot. * fix(e2e): drop right-panel '+' test, dockview is the only '+' path on desktop The mobile TaskRightPanel SessionTabs aren't rendered at desktop viewports — the dockview '+' menu is the sole new-tab affordance, so testing the mobile path on chromium just timed out clicking a button that never mounted. Consolidate into a single dockview '+ → Terminal' test that asserts the new panel gets the backend-assigned 'Terminal 2' label (proving env-shared shell list) plus the WS-payload regression guard. * fix(e2e): assert dockview terminal-tab count, not 'Terminal 2' label Dockview hardcodes the panel title to 'Terminal' for every tab — only the mobile right-panel SessionTabs surface the backend's 'Terminal N' label. Switch the assertion to count tabs to keep the e2e green. * test(backend): cover SQL NULL branch of healSessionTaskEnvironmentIDs Adds insertSessionWithNullEnvID and TestHealSessionTaskEnvironmentIDs_LinksNullOrphans so the WHERE-clause IS NULL predicate is exercised — the existing tests only seeded empty-string FKs. Also adds LIMIT 1 to the heal subquery as a defensive measure: the unique index already guarantees ≤1 row per task at runtime, but the SQL now reads as deterministic in isolation. * refactor(web): drop dead taskGateOk on agent terminal WS gate Agent passthrough routes by sessionId, not taskId — taskId was only used for logging. Gating the WS effect on taskId forced agent terminals to wait for useSession() to hydrate, adding a refresh-race window that didn't guard any real precondition. connectionKey + canConnect already cover the meaningful preconditions. * fix(web): stamp env id on terminal panels for accurate cleanup addTerminalPanel now bakes environmentId into the panel's params at creation time. The dockview-layout-setup onDidRemovePanel handler prefers the stamped env over re-deriving from the active session, so closing a terminal that was opened under a different task no longer calls stopUserShell with the wrong env (which silently no-ops on the backend and leaks the shell process until session teardown). Falls back to the active session's env for legacy panels persisted before the param existed. * fix(web): keep task id logging out of terminal reconnect deps |
||
|
|
c0ce55ff9d |
fix: unblock terminals stuck on Connecting and heal task_environments (#769)
* fix(gateway): snapshot agentctl status and env id on session subscribe Late-subscribing clients (page reload, task switch, WS reconnect) miss the agentctl_ready and task_environment_id events fired during the original launch. Without them, the env-routed shell terminal gate stays stuck and the panel hangs forever on "Connecting terminal...". - appendSessionStateMessage now carries task_environment_id so the frontend can seed environmentIdBySessionId for the active session. - New appendAgentctlStatusMessage snapshots current agentctl readiness via a bounded Health probe and emits session.agentctl_ready / _starting to flip the client gate out of `undefined`. - GetOrEnsureExecutionForEnvironment now publishes AgentctlStarting to mirror EnsureWorkspaceExecutionForSession and keep gate transitions monotonic. * fix(web): drop shell-mode agentctl gate to unblock env-routed terminals The shell terminal gate required agentctlStatus.isReady to flip to ready before opening the WS, but that event may have fired before the client subscribed (page reload, task switch, local-executor task switch). The lazy create-on-terminal-connect path on the backend never fires either, because the WS never opens — chicken-egg. Backend env handler already waits for remote workspace readiness server-side (waitForRemoteExecutionReadyWithTimeout), so the client just needs the routing keys (env id + session id). Agent-mode terminals still gate on readiness — the agent process must be wired before passthrough I/O is meaningful. Extract computeCanConnect helper to keep PassthroughTerminal under the complexity ceiling. * test: cover terminal-hang on subscribe snapshot and env-routed gating Adds a backend unit test asserting appendSessionStateMessage carries task_environment_id (the deterministic RED for the env-map seed bug), plus E2E scenarios for the user-facing repros: cold load, kanban switch, hard reload, sidebar switch, and a local-executor task switch that used to poison the terminal for every subsequent task. Adds an expectTerminalConnected helper to SessionPage to detect the "Connecting terminal..." overlay. * fix(repo): heal task_environments on boot and enforce one env per task Live DB audit on the user's machine surfaced two corrupt-data classes that manifest as the user-facing "terminal doesn't appear / Failed to fetch terminals: 503" symptoms: - 15 worktree-mode envs with worktree_path set but workspace_path empty. GetOrEnsureExecutionForEnvironment returns ErrSessionWorkspaceNotReady forever, the env terminal handler 503s. - 3 tasks with two task_environments rows (race in lazy create — pairs 4-6 s apart, both ready, distinct workspace_path). healTaskEnvironmentWorkspacePaths backfills workspace_path = worktree_path on the affected rows, and healDuplicateTaskEnvironments collapses each duplicated task to its most recently updated env (re-pointing any sessions still on the loser, then dropping the loser row). ensureTaskEnvironmentTaskUniqueIndex adds a UNIQUE index on task_environments(task_id) so a future race fails loud. Order matters: heal must run first to drop duplicates before the unique index is created. * fix(repo): reject empty workspace_path on worktree envs The orchestrator's persistTaskEnvironment update branch previously left workspace_path untouched, so an env created with empty workspace_path would stay corrupt forever — terminals 503 with ErrSessionWorkspaceNotReady. Tighten the repository boundary: CreateTaskEnvironment refuses worktree-mode inserts without workspace_path, and UpdateTaskEnvironment refuses to clear it back to empty. A future writer regression now fails loud at the boundary instead of producing a row that breaks the env terminal handler. * fix(executor): refresh paths on env update and dedupe by task The persistTaskEnvironment update branch only mutated AgentExecutionID + Status, so envs created earlier with an empty workspace_path stayed permanently broken across launches. Refresh worktree + workspace fields on update via a shared computeWorkspacePath helper used by both branches. Concurrent launches for the same task previously raced inside this function — each saw existingEnv == nil and each called CreateTaskEnvironment, producing duplicate rows ~4-6 s apart in the live DB. Hold a per-task mutex (mirroring sessionLocks) and re-fetch the env inside the critical section so siblings reuse the row the first launch persisted. The repo's unique index on task_environments(task_id) is the durable guarantee; the lock just closes the window before the constraint trips. * test(repo): cover task_environments heal, validation, unique index Repository unit tests for the new heal steps and writer guards: - HealTaskEnvironmentWorkspacePaths_BackfillsEmpty / LeavesPopulatedAlone / Idempotent - HealDuplicateTaskEnvironments_KeepsMostRecent (asserts session re-link to winner) / NoOpWhenSingle - EnsureTaskEnvironmentTaskUniqueIndex_BlocksFutureDuplicates - CreateTaskEnvironment_RejectsEmptyWorkspaceForWorktree / AllowsNonWorktreeWithEmptyWorkspace - UpdateTaskEnvironment_RejectsClearingWorkspaceForWorktree Tests live in the sqlite package so they can exercise the unexported heal* helpers directly, mirroring how the boot path calls them. * fix: address PR review feedback - helpers.go: drop the 2 s blocking Health probe in appendAgentctlStatusMessage. sendSessionData runs in the WS read loop, so the probe was delaying every subscribe/focus ACK by up to 2 s. Use GetWorkspaceStream() != nil as the cached readiness signal — the stream manager only attaches it after Health succeeds, so its presence is a free, non-blocking proxy. Subsequent waitForAgentctlReady events still correct the snapshot if it picks the wrong one. - manager_execution.go: publish AgentctlStarting INSIDE createExecution, before spawning waitForAgentctlReady. The previous order published it after createExecution returned, which let the goroutine race past on fast Health and deliver Ready before Starting. Drop the now-redundant publishes in EnsureWorkspaceExecutionForSession and the env lazy-create path. - task_environment.go: replace the local executorTypeWorktree constant with models.ExecutorTypeWorktree (canonical) so a future rename of the type is caught by the compiler. - base.go: heal_workspace_path now respects the kandev path layout — task-dir-mode envs (.../.kandev/tasks/<name>/<repo>) get Dir(worktree_path) as workspace_path, mirroring computeWorkspacePath in the orchestrator. Plain worktree mode unchanged. - heal_test.go: tighten unique-constraint assertion to require a UNIQUE error string so the test fails loudly if a different DB error masks the constraint violation. Add task-dir-mode heal coverage. |
||
|
|
83e01477df |
refactor: extract shared shapes between jira and linear integrations (#759)
* refactor: extract shared shapes between jira and linear integrations Factors out the duplicated logic between internal/jira + internal/linear (backend) and components/jira + components/linear (frontend) into reusable shared shapes. Follow-up to PR #736. Backend: - New internal/integrations/secretadapter wraps secrets.SecretStore with the upsert API both jira.SecretStore and linear.SecretStore expect; drops both adapter copies from cmd/kandev/services.go. - New internal/integrations/healthpoll owns the 90s auth-health loop via a Prober interface; linear/poller.go is a thin wrapper, jira/poller.go keeps only its (jira-specific) issue-watch loop. - provideServices split into initGitHubService / initJiraService / initLinearService — now 75 lines (was ~125, exceeded the 80-line limit). Frontend: - New hooks/domains/integrations/use-integration-availability.ts and use-integration-enabled.ts; jira/linear hooks reduce to one-line delegates and move from components/<x>/ to hooks/domains/<x>/ per the project convention Greptile flagged on PR #736. - New components/integrations/{auth-status-banner,auth-error-message, validated-popover}.tsx; the four link/import-bar wrappers drop from ~110 lines to ~35 each, and JiraLinkButton picks up the clear-error-on-close behaviour LinearLinkButton already had. Docs: - New 'Adding a new third-party integration' section in CLAUDE.md/AGENTS.md documenting the shared shapes and intentional Jira/Linear divergence (transition vs state, JQL vs filter object, eventBus only on jira). * fix: address PR #759 review feedback - validated-popover: outline-with-label trigger now sets type="button" and honours triggerDisabled, matching ghost-icon (Greptile, Claude, CodeRabbit). - jira-link-button / linear-link-button: move updateTask() into the awaited fetch path so a failed title update surfaces as the popover's inline error instead of an unhandled rejection (CodeRabbit). - use-integration-availability: monotonic requestId race guard prevents a slow earlier probe from overwriting a newer auth result (CodeRabbit). - use-integration-enabled: drop unnecessary async on synchronous init (Claude). - jira/linear poller smoke tests: wrap in testing/synctest instead of time.After(2s) per backend testing convention (CodeRabbit nitpick). Skipped with reasoning on the threads: - secretadapter Set TOCTOU — single-process SQLite + single-writer per workspace; constraint violation on conflict surfaces as an error to the caller, not silent corruption. - healthpoll SetInterval validation — the function isn't called from production code today; a panic on misuse would be a footgun and silent ignore would just hide bugs differently. * fix: address PR #759 round-2 review feedback - jira-settings / linear-settings: drop the local STATUS_REFRESH_MS = 90_000 copies and import the shared INTEGRATION_STATUS_REFRESH_MS so changing the cadence only touches one file (Claude). - healthpoll: rename Poller.ProbeAll → probeAll. The only callers are the loop and the in-package tests, so widening the public API wasn't buying anything (Claude). * fix: invalidate ValidatedPopover in-flight submits on close If the user clicked Submit and dismissed the popover before the request settled, the next time they re-opened it would inherit `loading=true` (the disabled-while-loading submit button was stuck) and a late rejection from the previous session could repopulate `error` in the new one. Track the submit via a `useRef` token, bump it on close, and ignore any result/rejection whose token no longer matches. Also reset `loading` and `error` on close so the next open starts clean. Adds a regression test covering both the late-resolution and late-rejection paths. Addresses CodeRabbit review on PR #759. * ci: trigger workflow re-run GitHub Actions did not dispatch the pull_request workflows on the previous two pushes ( |
||
|
|
396325b2c9 | feat: allow reordering sidebar views (#764) | ||
|
|
5022469187 | fix: refine workbench action button layout (#762) | ||
|
|
68c94dfa3a |
feat: refine workbench chrome (#761)
* docs: define product and design direction * style: establish restrained workbench primitives * refactor: declutter board and task topbars * style: align dockview panel chrome * fix: align task topbar vcs control sizing * refactor: surface integrations navigation * fix: repair integrations and task creation workflow * refactor: include linear in integrations navigation * style: restore familiar workbench tones * fix: add dockview tab breathing room * fix: show configured integration shortcuts * fix: hover open integrations menu * fix: stabilize integrations hover menu * fix: align home and dockview topbars * fix: normalize home topbar controls * fix: compact home topbar height * fix: simplify home chrome and split buttons * fix: move integrations to left chrome * fix: restore home stats shortcut * fix: add dockview tab breathing room * fix: border home stats button * fix: streamline dockview topbar actions * fix: align task split button seam * fix: address review feedback * fix: restore topbar e2e affordances * fix: stabilize workflow e2e isolation * fix: add task topbar action overflow * fix: address PR review feedback |
||
|
|
e16debeaca | fix(preview): server-authoritative session ensure for kanban preview (#760) | ||
|
|
1eae869054 | feat: add improve kandev in-app contribution flow (#740) | ||
|
|
9d275d3009 |
refactor: route user terminals by environment (#758)
* refactor: route user terminals by environment * refactor: remove terminal compatibility shims * fix: address terminal PR review feedback * fix: wait for remote shell readiness * fix: require agent mode for session terminals |
||
|
|
45ee2ff48f |
feat: key terminals + dockview layout to TaskEnvironment (#755)
* feat(backend): key user shells by TaskEnvironmentID Sessions in the same task share a TaskEnvironment. Re-key the user shell runner so terminals are scoped per env, not per session — switching between sessions of the same task now sees the same shell list. - Add TaskEnvironmentID to AgentExecution / LaunchRequest / ExecutorCreateRequest / executor.LaunchAgentRequest, propagated through ToAgentExecution and the orchestrator launch path. - Add Manager.ResolveScopeKey(sessionID) — returns env when execution carries it, falls back to sessionID for legacy paths. - Rename runner param sessionID -> scopeID in interactive_shells.go; map keys are now scopeID:terminalID. - Shell handlers + WS terminal handler resolve sid -> scopeID via ResolveScopeKey before calling the runner. * feat(web): key dockview layout + portals to TaskEnvironmentID Switching between sessions of the same task no longer rebuilds the dockview layout — terminals and panels persist because layouts are env-scoped, not session-scoped. The bottom terminal also reuses a stable sessionId across same-env switches so it doesn't reconnect to a different shell. - Rename layout persistence: setSessionLayout/getSessionLayout/*SessionMaximizeState -> setEnvLayout/getEnvLayout/*EnvMaximizeState (kandev.dockview.env-layout. prefix). - Rename dockview-session-switch.ts -> dockview-env-switch.ts; performSessionSwitch -> performEnvSwitch with oldEnvId/newEnvId/activeSessionId. - dockview-store.ts: currentLayoutSessionId -> currentLayoutEnvId; switchSessionLayout -> switchEnvLayout(oldEnvId, newEnvId, activeSessionId). Same-env switches no-op. Drop _skipLayoutSwitchForSession + consumeSkipFlag — env-keyed action handles it structurally. - panel-portal-manager: PortalEntry.envId; releaseBySession -> releaseByEnv. ENV_SCOPED_COMPONENTS in dockview-desktop-layout (renamed from SESSION_SCOPED). - useEnvSwitchCleanup replaces useSessionSwitchCleanup; tracks env changes only. - All performLayoutSwitch callers (sidebar, dropdown, header, dialog) resolve sid -> envId via environmentIdBySessionId before switching. - bottom-terminal-panel: drop sessionId prop, use useEnvironmentSessionId(). - cleanupTaskStorage now takes envIds[] alongside sessionIds[] for env-scoped layout/maximize cleanup. * test: add coverage for env-keyed terminals + layout Playwright e2e (terminal-env-keyed.spec.ts) — switching between sessions of the same task keeps terminal scrollback intact and asserts both sessions share one task_environment_id. Update the comment in sessionless-task-switch.spec.ts to reference performEnvSwitch. * fix: address env layout PR feedback * test: fix flaky handshake auth check * fix: address env scope review findings |
||
|
|
7b5b3f329d |
feat: add Linear integration (#736)
* feat: add Linear integration
Mirrors the existing Jira integration so a workspace can connect to Linear
with a personal API key, browse and search issues, and start a Kandev task
from any issue. Backend uses Linear's GraphQL API with a workspace-scoped
config + secret store and a 90s auth-health poller; the frontend adds a
settings page, an issue dialog with state transitions, link/import buttons
in the task UI, and a paginated /linear browse page with a Start task flow.
* fix(linear): address PR review feedback
- Route httpListStates through Service.ListStates instead of clientFor
- Extract repeated workspace_id required string into a constant
- Log GetConfig errors during credential resolution instead of dropping them
- Clear popover error state on close in import + link buttons
- Pick the first workflow with steps in the quick task launcher (don't hard-pin to workflows[0])
- Reset useLinearEnabled state on workspace change via tag-based pattern
- Add cursor-pointer to anchor + Link elements per project convention
- Add tests: handlers_test.go, linear-api.test.ts, use-linear-enabled.test.ts, linear-issue-common.test.tsx
- Document internal/jira and internal/linear in CLAUDE.md/AGENTS.md
* fix(linear): address second-round PR review feedback
- Round Linear's fractional state position instead of truncating it via
int(math.Round(s.Position)) so adjacent fractional positions don't collapse
- Force LinearSettings to remount on workspace change with key={id} so the
previous workspace's config / form / testResult / teams cannot bleed into
the next workspace before the new fetches settle
- Broaden the legacy-value useLinearEnabled test to cover "true", "1", "yes",
and "legacy" so a regression to strict value === "true" would fail
* fix(linear): address third-round PR review feedback
- Propagate context.Canceled / context.DeadlineExceeded from ListStates in
GetIssue instead of swallowing them — a caller cancellation must not return
a partial-success response
- Drop the one-off contains() test helper in handlers_test.go and use
strings.Contains directly so the assertion is self-documenting
- Add getLinearIssue URL-shape test that locks identifier-as-path-segment
vs workspaceId-as-query-param so accidentally swapping them would fail
* test(linear): extract repeated literals in linear-api.test.ts
Pulls "ws-1", the API base URL, and the auth-method literal into module-
level constants so the assertions read as the route shape they care about
and the duplicate-strings ESLint rule (currently warn, threshold 4)
stays quiet as the file grows.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
|
||
|
|
fcc7ceb8b6 |
fix(approvals): drop stuck pending permission_request messages (#723)
* fix(web): exclude permission_request from turn-complete safety net The session.turn.completed handler iterated over messages and forced metadata.status="complete" on anything carrying tool_call_id. Permission prompts also carry tool_call_id (for FE pairing with their tool_call), so this loop overwrote the user's "approved" decision on every prompt in the turn — and the FE component re-rendered the Approve/Deny buttons. * fix(backend): protect permission_request rows from tool_call sweeps Three permission_request invariants the prior code violated, mirroring the FE bug fix: - CompletePendingToolCallsForTurn: SQL UPDATE filtered by tool_call_id presence, also catching permission_request rows. Add type filter so the safety net only touches actual tool messages. - GetMessageByToolCallID: returned the oldest row carrying that tool_call_id, which races to the permission_request when timing shifts. Exclude permission_request type so a tool_update can never land on the user's approve/reject row. - applyToolCallMessageUpdate: defensive guard that refuses to overwrite a permission_request, making the invariant explicit at the layer that does the writing. Adds a regression test for CompletePendingToolCallsForTurn covering approved/rejected/pending permission rows. * test(e2e): cover approval-reappear regression with mock-agent scenario Adds a multi-permission mock-agent scenario (three sequential bash tools, each requiring approval) and an E2E test that approves all three and asserts no permission action row remains visible after the agent's turn ends, and that the resolved state survives a reload. Supporting test infrastructure: - data-testid hooks on PermissionActionRow. - permissionActionRows() / permissionApproveButtons() on SessionPage. - backend fixture force-disables AGENTCTL_AUTO_APPROVE_PERMISSIONS=false so a developer's local AUTO_APPROVE=true env doesn't silently bypass the permission UI in tests that exercise the approve/reject flow. * test(backend): cover GetMessageByToolCallID permission_request exclusion Addresses claude-review feedback on the approvals fix: - Adds a regression test that seeds both a permission_request and a tool_call with the same tool_call_id (permission_request created first, so an ORDER BY created_at ASC sweep without the type filter would race to it) and asserts the lookup returns the tool_call row. - Bumps the applyToolCallMessageUpdate invariant guard from Warn to Error: reaching that branch means a caller bug, not a transient condition, and Warn would silently swallow the violation. * fix(e2e): scope auto-approve override to permission-approval test only The previous fixture force-disabled AGENTCTL_AUTO_APPROVE_PERMISSIONS globally, which broke session-isolation.spec.ts and other tests that use scenarios with permission-required tools (e.g. /e2e:read-and-edit) and rely on auto-approve to let the agent finish without UI clicks. Default the env to "true" in the fixture; let permission-approval.spec flip it to "false" via a backend.restart() override in beforeAll, then restore in afterAll. Adds an envOverrides parameter to backend.restart. |
||
|
|
aa70b7dfa4 |
feat(web): mobile terminal key-bar — Ctrl/Shift modify OS-keyboard input + iOS keyboard fixes (#741)
* feat(web): add mobile terminal key-bar with sticky Ctrl modifier Mobile software keyboards lack Ctrl, Esc, Tab, arrows, and shell symbols, so users had no way to interrupt a running command (Ctrl+C), escape an editor, or navigate the shell. Adds a horizontally scrolling key-bar above the virtual keyboard — sticky Ctrl modifier, dedicated Ctrl+C / Ctrl+D buttons, and visualViewport-based docking so the bar tracks the OS keyboard as it opens and closes. * test(web): add E2E coverage for mobile terminal key-bar Nine Playwright tests on the mobile-chrome project cover visibility gating, key emission verified via captured WS frames, the sticky Ctrl state machine, dedicated Ctrl+C / Ctrl+D buttons, keyboard-aware docking, accessibility, and an end-to-end ^C echo to prove the shell path works. Adds a reusable shell.input frame capture helper and a small page-object for the key-bar. * feat(web): shared shell input helpers for the mobile terminal Introduces a small toolkit the next commit will wire into the mobile key-bar: - `refocusXtermTextarea` — re-focuses xterm's hidden textarea after a virtual-key tap so iOS Safari doesn't dismiss the soft keyboard. - `suppressIOSKeyboardAssists` — strips name/autocomplete/data-* hints on the textarea so iOS / 1Password / LastPass / Bitwarden skip the AutoFill suggestion bar. - `useShellModifiersStore` (Zustand) — module-level Ctrl/Shift latch state with toggle (off → latched → sticky → off) and consume actions. - `applyShellModifiers` — pure transform: ctrl + a-z → control byte; shift + tab → CSI Z (reverse tab); single-char only. - `sendShellInput` — single entry point that reads the store, applies the transform, sends over WS, and consumes latched modifiers. `useShellKeySender` and the xterm `onData` path in `shell-terminal.tsx` now route through `sendShellInput` so any future modifier state propagates uniformly. `suppressIOSKeyboardAssists` is wired into `useTerminalInit` immediately after `terminal.open()`. * feat(web): mobile key-bar pivots to OS-keyboard input + pins to keyboard Reworks the mobile terminal key-bar around two insights: 1. The OS soft keyboard already provides letters — duplicating them in-bar was confusing and didn't address the real problem (Ctrl couldn't modify what the user typed via the OS keyboard, because that input went straight to xterm and bypassed React state). 2. iOS Safari `position: fixed; bottom:` drifts during visual-viewport scroll while the keyboard is open. Anchoring via `top` tied to the visual viewport's bottom edge stays glued. Changes: - `MobileTerminalKeybar` reads Ctrl/Shift state from the shared `useShellModifiersStore` and toggles it on tap. The actual chord transform now happens in `sendShellInput`, which both the bar and xterm's `onData` path call — so a Ctrl latch set from the bar modifies the next character typed on the OS keyboard. - Drops `LETTER_KEYS` and `chordChar` from the bar; adds a Shift toggle (Shift+Tab → reverse-tab from the bar; pairs with the modifier transform for OS-keyboard taps). - Latched state now shows the same ring as sticky (with sticky using a stronger `ring-primary`), so the first tap is visibly active. - `touch-action: manipulation` on each button so taps in the `overflow-x-auto` row don't get reinterpreted as scroll. - `useVisualViewportOffset` exports `viewportBottom = offsetTop + height`. When the keyboard is open the bar uses `top: viewportBottom - KEYBAR_HEIGHT_PX` and `bottom: auto`; closed, it keeps the `bottom`-based positioning so it sits above the bottom nav. - `SessionMobileLayout` computes the terminal panel's `paddingBottom` so terminal content's bottom edge sits exactly at the bar's top edge whether the keyboard is up or not. Tests cover the new modifier flow, Shift+Tab, the absence of in-bar letter buttons, and the `top`-anchored positioning when the keyboard is reported as open. * test(web): rewrite mobile key-bar e2e around real user flows The prior e2e leaned on in-bar letter buttons that no longer exist. Rewrites the spec around the journeys a mobile user actually performs: tap ^C to cancel, latch Ctrl + type a letter on the OS keyboard, sticky Ctrl across multiple chords, Shift+Tab from the bar, history nav with the arrow button, Esc, symbol keys, and the iOS positioning fixes (top-anchored when the keyboard opens, tracks visual viewport scroll, terminal panel reserves space for the bar). Adds a `shift` getter to the page object. * style(web): apply prettier formatting * fix(web): keep modifiers armed on dropped sends + tighten input handler Addresses bot review on PR #741: - `sendShellInput` no longer consumes Ctrl/Shift when the WS client isn't available — the chord stays armed so the user can retry once reconnected instead of seeing the latch silently clear. - Adds `send-shell-input.test.ts` with the matrix the helper needs: empty data, ctrl chord + auto-release, sticky retention, shift+tab → CSI Z, shift passthrough on non-tab, and the WS-disconnected no-send/no-consume path. - Removes dead `ctrlChord` export and its tests — the only consumer was its own test, and `applyShellModifiers` covers the same transform end-to-end. - Drops the unused `send` parameter from `useShellInputHandler` (the effect now goes through `sendShellInput` directly), removing a spurious dep that would otherwise re-fire the effect on identity changes. - Documents that `KEY_SEQUENCES.home`/`end` are intentionally readline shortcuts (Ctrl+A/E), not VT Home/End sequences, so future readers don't mistake it for a bug. * refactor(web): split mobile key-bar helpers, tighten null-viewport test Addresses two non-blocking suggestions from PR #741 review: - Move `KeyDef`, `KEYS`, `ModifierButton`, and `KeybarButton` out of `mobile-terminal-keybar.tsx` into a sibling helpers file. The main component drops from 213 to 115 lines, comfortably under the 200-line component convention. - Assert `viewportBottom === 0` in the null-viewport branch of `use-visual-viewport-offset.test.ts` so all three return-shape fields are covered there. |
||
|
|
8fb2e5ba52 | fix(lifecycle): publish agentctl events on workspace restore (#731) |