fix(web): keep change action spinners visible (#3339)

Co-authored-by: Jelloeater's Agent <agent@jello.dev>
Co-authored-by: Henry <nova28@users.noreply.github.com>
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Florêncio
2026-09-04 05:37:31 +00:00
committed by GitHub
parent fa6720f8ca
commit 256c0fc04d
18 changed files with 626 additions and 45 deletions
@@ -202,12 +202,16 @@ function TreeModeFileActionSlot({
{isFinePointer && (
<FileIcon
fileName={name}
className="size-4 transition-opacity group-hover:opacity-0 pointer-events-none"
className={cn(
"size-4 transition-opacity pointer-events-none",
isPending ? "opacity-0" : "group-hover:opacity-0",
)}
/>
)}
<div
className={cn(
isFinePointer &&
!isPending &&
"opacity-0 transition-opacity pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto",
)}
>
@@ -4,7 +4,7 @@ import type { ApiClient } from "../../helpers/api-client";
import { waitForSessionState } from "../../helpers/session";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
import type { Page } from "@playwright/test";
import type { Page, WebSocketRoute } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
@@ -94,6 +94,78 @@ async function openTaskSession(page: Page, title: string): Promise<SessionPage>
return session;
}
type WorktreeFileAction = "worktree.stage" | "worktree.unstage";
type PausedWorktreeFileAction = {
action: WorktreeFileAction;
frame: string;
server: WebSocketRoute;
};
function isWorktreeFileActionFrame(part: string, action: WorktreeFileAction): boolean {
if (!part.trim()) return false;
try {
const frame = JSON.parse(part) as { type?: unknown; action?: unknown };
return frame.type === "request" && frame.action === action;
} catch {
return false;
}
}
function partitionWorktreeFileAction(
message: string | Buffer,
action: WorktreeFileAction,
): { frame: string; passthrough: string | null } | null {
if (typeof message !== "string") return null;
const frames = message.split("\n");
const actionIndex = frames.findIndex((part) => isWorktreeFileActionFrame(part, action));
if (actionIndex === -1) return null;
const passthrough = frames.filter((_, index) => index !== actionIndex).join("\n");
return {
frame: frames[actionIndex]!,
passthrough: passthrough.trim() ? passthrough : null,
};
}
async function pauseNextWorktreeFileAction(page: Page) {
let armedAction: WorktreeFileAction | null = null;
let paused: PausedWorktreeFileAction | null = null;
await page.routeWebSocket(/\/ws$/, (socket) => {
const server = socket.connectToServer();
socket.onMessage((message) => {
const partition = armedAction ? partitionWorktreeFileAction(message, armedAction) : null;
if (partition && armedAction) {
paused = { action: armedAction, frame: partition.frame, server };
armedAction = null;
if (partition.passthrough) server.send(partition.passthrough);
return;
}
server.send(message);
});
server.onMessage((message) => socket.send(message));
});
return {
arm(action: WorktreeFileAction) {
if (armedAction || paused) throw new Error("A worktree file action is already paused");
armedAction = action;
},
async waitForRequest(action: WorktreeFileAction) {
await expect
.poll(() => paused?.action ?? null, {
message: `${action} should reach the paused WebSocket boundary`,
})
.toBe(action);
},
release(action: WorktreeFileAction) {
if (paused?.action !== action) throw new Error(`No paused ${action} request`);
paused.server.send(paused.frame);
paused = null;
},
};
}
/** Create a non-passthrough (standard) agent profile for the mock agent. */
async function createStandardProfile(apiClient: ApiClient, name: string) {
const { agents } = await apiClient.listAgents();
@@ -207,6 +279,102 @@ test.describe("Git Changes Panel", () => {
await expect(session.changes.getByText("test-file.txt")).toBeVisible({ timeout: 15_000 });
});
// @covers AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.1
// @covers AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.2
// @covers AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.3
// @covers AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.4
test("keeps stage and unstage progress visible after pointer leaves", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
const pausedAction = await pauseNextWorktreeFileAction(testPage);
const profile = await createStandardProfile(apiClient, "Git Pending Action Profile");
await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Git Pending Action Test",
profile.id,
{
description: "Testing pending stage and unstage feedback",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
const session = await openTaskSession(testPage, "Git Pending Action Test");
await session.waitForChatIdle();
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const git = new GitHelper(repoDir, {
...process.env,
HOME: backend.tmpDir,
GIT_AUTHOR_NAME: "E2E Test",
GIT_AUTHOR_EMAIL: "e2e@test.local",
GIT_COMMITTER_NAME: "E2E Test",
GIT_COMMITTER_EMAIL: "e2e@test.local",
});
git.createFile("pending-action.txt", "base\n");
git.createFile("pending-sibling.txt", "base\n");
git.stageAll();
git.commit("Add pending action fixtures");
git.modifyFile("pending-action.txt", "base\nchanged\n");
git.modifyFile("pending-sibling.txt", "base\nunchanged pending state\n");
await session.clickTab("Changes");
await expect(session.changes).toBeVisible({ timeout: 10_000 });
await session.expandChangesSection("unstaged-files-section");
const unstagedSection = testPage.getByTestId("unstaged-files-section");
const unstagedRow = unstagedSection.getByTestId("file-row-pending-action.txt");
const siblingRow = unstagedSection.getByTestId("file-row-pending-sibling.txt");
await expect(unstagedRow).toBeVisible();
await expect(siblingRow).toBeVisible();
pausedAction.arm("worktree.stage");
await unstagedRow.hover();
await unstagedRow.getByTitle("Stage file").click();
await pausedAction.waitForRequest("worktree.stage");
await testPage.mouse.move(0, 0);
const pendingStageSlot = unstagedRow.getByTestId("file-row-icon-action-slot");
expect(await unstagedRow.evaluate((row) => row.matches(":hover"))).toBe(false);
await expect(pendingStageSlot.locator(":scope > div")).toHaveCSS("opacity", "1");
await expect(pendingStageSlot.locator(":scope > span[aria-hidden='true']")).toHaveCSS(
"opacity",
"0",
);
await expect(pendingStageSlot.locator("svg.animate-spin")).toBeVisible();
await expect(siblingRow.locator("svg.animate-spin")).toHaveCount(0);
pausedAction.release("worktree.stage");
await session.expandChangesSection("staged-files-section");
const stagedSection = testPage.getByTestId("staged-files-section");
const stagedRow = stagedSection.getByTestId("file-row-pending-action.txt");
await expect(stagedRow).toBeVisible();
pausedAction.arm("worktree.unstage");
await stagedRow.hover();
await stagedRow.getByTitle("Unstage file").click();
await pausedAction.waitForRequest("worktree.unstage");
await testPage.mouse.move(0, 0);
const pendingUnstageSlot = stagedRow.getByTestId("file-row-icon-action-slot");
expect(await stagedRow.evaluate((row) => row.matches(":hover"))).toBe(false);
await expect(pendingUnstageSlot.locator(":scope > div")).toHaveCSS("opacity", "1");
await expect(pendingUnstageSlot.locator(":scope > span[aria-hidden='true']")).toHaveCSS(
"opacity",
"0",
);
await expect(pendingUnstageSlot.locator("svg.animate-spin")).toBeVisible();
pausedAction.release("worktree.unstage");
await expect(unstagedSection.getByTestId("file-row-pending-action.txt")).toBeVisible();
await expect(stagedRow).toHaveCount(0);
});
// @covers AC-PLATFORM-WORKSPACE-GIT-STATUS-001.10
test("shows same path in staged and unstaged sections", async ({
testPage,
@@ -0,0 +1,132 @@
---
created: 2026-09-03
status: implemented
requirements:
- REQ-UI-CHANGES-FILE-ACTION-FEEDBACK-001
system_design:
- ../../specs/ui/system-design/changes-file-action-feedback.md
legacy_specs: []
---
# Implementation Plan: Keep Changes File Action Feedback Visible
## Overview
Make per-file stage and unstage pending feedback override the tree row's idle
hover swap. One focused work order first captures the failure by pausing the
worktree request in Chromium, then makes the minimum shared component change
and verifies the completed stage and unstage transitions.
## Confirmed root cause
- `StageButton` correctly renders `IconLoader2` whenever `FileRow.isPending` is
true.
- In tree mode, `TreeModeFileActionSlot` wraps that button in a fine-pointer
layer whose visibility is controlled only by `group-hover` classes.
- The adjacent `FileIcon` also follows only the inverse hover rule. It does not
consider `isPending`.
- Leaving the row therefore makes the action layer, including its spinner,
transparent and restores the file-type icon even though the repository/path
pending key remains active.
- Coarse-pointer rows do not receive the hover-only classes, so their action
and spinner already remain visible.
## Scope
### In scope
- Give pending state precedence over the fine-pointer file-icon/action hover
swap.
- Keep the existing spinner visible after pointer leave for both stage and
unstage operations.
- Preserve per-repository, per-path pending-state scoping.
- Add deterministic Chromium coverage that holds each WebSocket request open,
leaves the row, and observes the pending and completed states.
- Preserve the existing coarse-pointer action visibility and touch sizing.
### Out of scope
- Git operation dispatch, status polling, WebSocket contracts, and pending-key
lifecycle changes.
- Bulk actions, discard/edit feedback, cancellation, notifications, and new
copy.
- Changes-panel layout, path truncation, navigation, or mobile composition.
## Technical approach
Update `TreeModeFileActionSlot` in
`apps/web/components/task/changes-panel-file-row.tsx` so the fine-pointer idle
hover classes apply only when `isPending` is false. While pending, keep the
file-type icon hidden and the spinner layer visible independent of the row's
hover state. Continue rendering the same `StageButton`; do not introduce
component-local request state.
Extend `apps/web/e2e/tests/git/git-changes-panel.spec.ts` with a narrow
WebSocket transport controller patterned after the existing request-pause
helpers. It shall pause only the armed `worktree.stage` or
`worktree.unstage` frame and forward all other frames. The regression creates a
target file (`pending-action.txt`) and a sibling file (`pending-sibling.txt`),
starts each action from the UI, waits until the request is paused, moves the
pointer to a neutral location, and asserts the existing spinner is still
visible only for the target row before releasing the request and observing the
file's new section.
## Tests
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.1 through .4:** the focused
Chromium scenario proves spinner visibility for both request types after
pointer leave, per-row scoping, and the final staged/unstaged section.
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.5:** the existing
`changes-panel-file-row.test.tsx` coarse-pointer assertion and
`mobile-changes-panel.spec.ts` stage/unstage path remain unchanged and green.
## E2E tests
- Add `keeps stage and unstage progress visible after pointer leaves` to
`apps/web/e2e/tests/git/git-changes-panel.spec.ts` in the Chromium project.
- No new Pixel 5 case is planned because the coarse-pointer branch already
keeps the action slot visible and this work changes neither its markup nor
its interaction. Existing mobile coverage exercises both stage and unstage
controls through the shared row.
## Work orders
- [x] [Task 01: Keep pending file actions visible](task-01-keep-pending-file-actions-visible.md)
## Dependency order
```text
Task 01
```
The browser regression and component correction share one presentation
boundary and proceed sequentially through Red-Green-Refactor.
## Verification results
- RED: the focused Chromium scenario failed after pointer leave because the
pending action layer reached `opacity: 0` while `worktree.stage` remained
paused.
- GREEN: the production-build Chromium scenario passed both the paused stage
and paused unstage flows. A final no-build rerun after test formatting also
passed.
- The existing `changes-panel-file-row.test.tsx` suite passes all 16 tests,
including the coarse-pointer action visibility and touch-size assertion.
- Targeted ESLint, web typecheck, specification lint, and scoped diff checks
pass.
## Risks
- A visibility change that hides the file icon without explicitly revealing
the spinner layer could leave the slot blank after pointer leave. The E2E
assertion observes the rendered spinner, not only class names.
- A WebSocket pause helper can disrupt unrelated live updates if it buffers a
whole message instead of only the matching newline-delimited frame. The
helper must partition and forward unrelated frames immediately.
- Completion can move the row between sections and detach its locator. The
test must reacquire the row from the destination section before testing the
inverse action.
## Open questions
None.
@@ -0,0 +1,118 @@
---
id: "01-keep-pending-file-actions-visible"
title: "Keep pending file actions visible"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-UI-CHANGES-FILE-ACTION-FEEDBACK-001
acceptance_criteria:
- AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.1
- AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.2
- AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.3
- AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.4
- AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.5
system_design:
- ../../specs/ui/system-design/changes-file-action-feedback.md
---
# Task 01: Keep Pending File Actions Visible
## Summary
Keep the tree-mode stage and unstage spinner visible after a fine-pointer user
leaves the acted-on row. Prove the repair by holding each outgoing worktree
request at the browser WebSocket boundary before changing the shared row.
## In scope
- Add a Chromium regression for stage and unstage pending feedback.
- Pause only the armed worktree action frame and forward unrelated WebSocket
traffic.
- Confirm the regression fails because the spinner becomes visually hidden
after pointer leave.
- Make `isPending` override the fine-pointer idle hover swap in
`TreeModeFileActionSlot`.
- Confirm only the acted-on file shows pending feedback and that completion
moves it to the expected staged or unstaged section.
- Preserve the coarse-pointer branch, its always-visible action, and its 44px
target.
## Out of scope
- Changes to Git execution, pending keys, status refresh, or error handling.
- Bulk, discard, edit, commit, or provider-file actions.
- New copy, loading primitives, notifications, cancellation, or progress
estimates.
- Changes-panel layout or responsive composition changes.
## Acceptance
- During an in-flight stage or unstage request, the acted-on row's spinner is
visibly rendered after the pointer leaves, and its file-type icon does not
replace the spinner.
- Releasing each request completes the current Git flow and moves the file to
the section matching its refreshed staged state.
- Idle fine-pointer hover behavior and coarse-pointer action reachability stay
unchanged.
## Verification
Run from the repository root:
```bash
(cd apps/web && rtk pnpm e2e:run tests/git/git-changes-panel.spec.ts -- --grep "keeps stage and unstage progress visible after pointer leaves" --retries=0)
(cd apps/web && rtk pnpm exec eslint components/task/changes-panel-file-row.tsx e2e/tests/git/git-changes-panel.spec.ts)
(cd apps/web && rtk pnpm run typecheck)
rtk python3 scripts/lint-spec-files.py --all
rtk git diff --check -- apps/web docs/specs/ui docs/plans/changes-file-action-feedback
```
Before the production edit, run the exact Playwright scenario and record the
expected spinner-visibility failure. After the edit, rerun the scenario and
then the complete command set above.
## Files likely touched
- `apps/web/components/task/changes-panel-file-row.tsx`
- `apps/web/e2e/tests/git/git-changes-panel.spec.ts`
- `docs/plans/changes-file-action-feedback/plan.md`
- `docs/plans/changes-file-action-feedback/task-01-keep-pending-file-actions-visible.md`
## Dependencies
None.
## Risks
- Newline-delimited WebSocket messages can contain unrelated frames; the pause
controller must forward them instead of delaying the full message.
- The original row can detach after status refresh, so post-completion
assertions must use a locator scoped to the destination section.
## Parallelism
`sequential`
## Inputs
- `REQ-UI-CHANGES-FILE-ACTION-FEEDBACK-001` and its system design.
- `TreeModeFileActionSlot`, `StageButton`, and the existing per-file pending
state in `useSessionGit`.
- The current Chromium Changes-panel scenario and existing WebSocket
request-pause helpers.
## Results
- RED: `pnpm e2e:run tests/git/git-changes-panel.spec.ts -- --grep "keeps stage and unstage progress visible after pointer leaves" --retries=0` failed on the expected rendered-state assertion. After pointer leave, the pending stage layer had computed opacity `0`.
- GREEN: the same production-build command passed after pending state was given
precedence over the tree row's hover swap. The final formatted test passed
again with `--no-build`.
- `changes-panel-file-row.test.tsx` passes 16 tests, including the existing
coarse-pointer visibility and 44px target coverage.
- Targeted ESLint, `pnpm run typecheck`, `scripts/lint-spec-files.py --all`,
and the scoped `git diff --check` command pass.
- No Pixel 5 test was added because the changed conditions execute only for
fine pointers. The shared coarse-pointer branch and existing mobile
stage/unstage path are unchanged.
@@ -529,4 +529,3 @@ write leaves a correct manifest beside a stale status the next run overwrites.
| Release fails partway on config delete | Stops at the failing entity; entities already released stay released and have no manifest row; config retained; retry resumes from the remainder. |
| Manifest names an entity deleted out of band | Manifest entry dropped, run continues. |
| Delete of a nonexistent config | Succeeds with no effect. |
@@ -565,4 +565,3 @@ no step in this order can regress a shipped feature.
- [GitLab workflow sync requirements](../../integrations/requirements/gitlab-workflow-sync.md)
is the shipped provider contract this design reuses and whose write-back
exclusion it follows.
+24 -32
View File
@@ -6,18 +6,12 @@ migration: in_progress
owners:
- kandev
---
# UI
## Scope
UI owns responsive behavior; other systems own behavior/state.
## Map
### Requirements
- [ACP Model Configuration Summary](requirements/acp-model-configuration-summary.md)
- [ACP model summary](requirements/acp-model-configuration-summary.md)
- [ACP Shell Command Output](requirements/acp-shell-command-output.md)
- [Adaptive Kanban](requirements/adaptive-kanban.md)
- [Task Add-Panel PR Submenu](requirements/add-panel-pr-submenu.md)
@@ -30,7 +24,8 @@ UI owns responsive behavior; other systems own behavior/state.
- [Column visibility](requirements/board-step-visibility-filter.md)
- [Inspect annotation submission](requirements/browser-inspect-annotations-save.md)
- [Cancel-turn progress](requirements/cancel-turn-progress.md)
- [Changes File Row Containment](requirements/changes-file-row-containment.md)
- [Action feedback](requirements/changes-file-action-feedback.md)
- [File-row containment](requirements/changes-file-row-containment.md)
- [Changes walkthrough](requirements/changes-walkthrough-toolbar-width.md)
- [Task PR Automation Controls](requirements/ci-pr-automation.md)
- [Merge Queue Recovery Controls](requirements/ci-pr-merge-queue-recovery-controls.md)
@@ -47,7 +42,7 @@ UI owns responsive behavior; other systems own behavior/state.
- [Context Window Reset Freshness](requirements/context-window-reset-freshness.md)
- [Context Window Unmeasured State](requirements/context-window-unmeasured-state.md)
- [Embedded VS Code Executor Availability](requirements/embedded-vscode-executor-availability.md)
- [Embedded VS Code Windows Availability](requirements/embedded-vscode-windows-availability.md)
- [VS Code windows](requirements/embedded-vscode-windows-availability.md)
- [Empty-Turn Notice and Slash-Command Hint](requirements/empty-turn-notice.md)
- [Entity Reference Composer](requirements/entity-reference-composer.md)
- [Executor settings card spacing](requirements/executor-settings-card-spacing.md)
@@ -63,13 +58,13 @@ UI owns responsive behavior; other systems own behavior/state.
- [Repair merge commit details](requirements/merge-commit-details.md)
- [Mermaid Rendering](requirements/mermaid-rendering.md)
- [Message favorite star mobile sizing](requirements/message-favorite-star-mobile-size.md)
- [Message metadata dialog scroll containment](requirements/message-metadata-overflow.md)
- [Metadata dialog](requirements/message-metadata-overflow.md)
- [Queue auto-merge](requirements/message-queue-auto-merge.md)
- [Message queue management](requirements/message-queue-management.md)
- [Merge Enqueued Messages Individually](requirements/message-queue-merge.md)
- [Merge queued](requirements/message-queue-merge.md)
- [Pin the Message Queue Panel](requirements/message-queue-pin.md)
- [Reorder Queued Messages](requirements/message-queue-reorder.md)
- [Control Pending Message Auto-run](requirements/message-queue-run.md)
- [Pending auto-run](requirements/message-queue-run.md)
- [Send Queued Messages Now](requirements/message-queue-send-now.md)
- [Mobile topbar actions](requirements/mobile-quick-chat-topbar.md)
- [Mobile Task Chrome](requirements/mobile-task-chrome.md)
@@ -80,18 +75,18 @@ UI owns responsive behavior; other systems own behavior/state.
- [Responsive PR Detail Header](requirements/pr-detail-header-width.md)
- [Repair PR-only commit details](requirements/pr-only-commit-details.md)
- [PR Task Status Summary](requirements/pr-task-status-summary.md)
- [Pull Request Walkthrough Generation](requirements/pr-walkthrough.md)
- [PR walkthrough](requirements/pr-walkthrough.md)
- [Preview Sprites Transient Retry](requirements/preview-sprites-transient-retry.md)
- [Persistent status motion](requirements/persistent-status-motion.md)
- [Prompt History Panel](requirements/prompt-history-panel.md)
- [Prompt paste: Nerd Font glyphs](requirements/prompt-paste-nerd-font-glyphs.md)
- [Prompt turn duration](requirements/prompt-turn-duration.md)
- [Published Docs Preview Reliability](requirements/published-docs-preview-reliability.md)
- [Docs preview](requirements/published-docs-preview-reliability.md)
- [Quick Chat elevation](requirements/quick-chat-elevation.md)
- [Quick Chat viewport layout](requirements/quick-chat-viewport-layout.md)
- [Quick Chat Idle Dot](requirements/quick-chat-idle-dot.md)
- [Quick Chat and Terminal Tabs](requirements/quick-terminal.md)
- [Relative Last Seen in Account Security](requirements/relative-last-seen.md)
- [Relative last seen](requirements/relative-last-seen.md)
- [Responsive Plan Formatting](requirements/responsive-plan-formatting.md)
- [Resizable Markdown Table Columns](requirements/resizable-markdown-tables.md)
- [Review File Status Cues](requirements/review-file-status.md)
@@ -121,7 +116,7 @@ UI owns responsive behavior; other systems own behavior/state.
- [Task Layout Profiles](requirements/task-layout-profiles.md)
- [Task Agent Tab Reconciliation](requirements/task-agent-tab-reconciliation.md)
- [Threads Deck](requirements/threads-conversation-deck.md)
- [Task Listing Display Preferences](requirements/task-listing-display-preferences.md)
- [Task listing](requirements/task-listing-display-preferences.md)
- [Task transcript history visibility](requirements/task-prompt-transcript-visibility.md)
- [Task Review Shortcut Switcher](requirements/task-review-shortcut.md)
- [Task surface refresh and mobile create](requirements/task-surface-refresh.md)
@@ -134,33 +129,32 @@ UI owns responsive behavior; other systems own behavior/state.
- [Transcript Navigation Settings](requirements/transcript-navigation-settings.md)
- [Voice Mode In Task Behavior](requirements/voice-mode-task-behavior.md)
- [Walkthrough Feedback Controls](requirements/walkthrough-feedback-controls.md)
- [Stable Walkthrough Navigation](requirements/walkthrough-navigation-layout.md)
- [Walkthrough nav](requirements/walkthrough-navigation-layout.md)
- [WebKit Task Dialog Rendering](requirements/webkit-task-dialog-rendering.md)
- [Active workspace first in settings](requirements/workspace-active-first-order.md)
- [WebSocket Connectivity Warning](requirements/ws-connectivity-warning.md)
### Design
- [Clarification submit feedback](system-design/clarification-submit-feedback.md)
- [Growing Dialog Content Containment](system-design/dialog-content-containment.md)
- [Dialog containment](system-design/dialog-content-containment.md)
- [Surface text](system-design/surface-text-hierarchy.md)
- [Agent Todo List Panel](system-design/agent-todo-list-panel.md)
- [App Status Bar](system-design/app-status-bar.md)
- [Changes File Row Containment](system-design/changes-file-row-containment.md)
- [Action feedback](system-design/changes-file-action-feedback.md)
- [File-row containment](system-design/changes-file-row-containment.md)
- [Mention recency](system-design/composer-mention-recency.md)
- [Composer Suggestion Overlays](system-design/composer-suggestion-overlays.md)
- [Compact Workflow Step Navigation](system-design/compact-workflow-step-navigation.md)
- [Kanban Preview Workflow Step Navigation](system-design/kanban-preview-workflow-step-navigation.md)
- [Compact step navigation](system-design/compact-workflow-step-navigation.md)
- [Kanban preview navigation](system-design/kanban-preview-workflow-step-navigation.md)
- [Workflow column visibility (Part 1)](system-design/board-step-visibility-filter-01.md)
- [Workflow column visibility (Part 2)](system-design/board-step-visibility-filter-02.md)
- [Task PR Automation Controls 1](system-design/ci-pr-automation-01.md)
- [Task PR Automation Controls 2](system-design/ci-pr-automation-02.md)
- [Task PR Automation Controls 3](system-design/ci-pr-automation-03.md)
- [Task PR auto 1](system-design/ci-pr-automation-01.md)
- [Task PR auto 2](system-design/ci-pr-automation-02.md)
- [Task PR auto 3](system-design/ci-pr-automation-03.md)
- [Merge Queue Recovery Controls](system-design/ci-pr-merge-queue-recovery-controls.md)
- [Entity Reference Composer](system-design/entity-reference-composer.md)
- [File Tree Keyboard Scope](system-design/file-tree-keyboard-scope.md)
- [File Tree Chat Context](system-design/file-tree-chat-context.md)
- [Kandev MCP Tool Results](system-design/kandev-mcp-tool-results.md)
- [File Tree Chat](system-design/file-tree-chat-context.md)
- [MCP results](system-design/kandev-mcp-tool-results.md)
- [Mobile Task Chrome](system-design/mobile-task-chrome.md)
- [Persistent status motion](system-design/persistent-status-motion.md)
- [Repository Groups](system-design/sidebar-repository-grouping.md)
@@ -180,13 +174,11 @@ UI owns responsive behavior; other systems own behavior/state.
- [Task Agent Tab Reconciliation](system-design/task-agent-tab-reconciliation.md)
- [Threads Deck](system-design/threads-conversation-deck.md)
- [Command-panel Sidebar Task Reveal](system-design/command-panel-sidebar-task-reveal.md)
- [Command Panel Task Activity Icons](system-design/command-panel-task-activity-icons.md)
- [Command-panel activity icons](system-design/command-panel-task-activity-icons.md)
- [Terminal Rendering](system-design/terminal-rendering.md)
- [Terminal Touch Scrolling](system-design/terminal-touch-scrolling.md)
- [Thinking Message Preview](system-design/thinking-message-preview.md)
- [Task Transcript History Visibility](system-design/task-prompt-transcript-visibility.md)
- [Transcript history visibility](system-design/task-prompt-transcript-visibility.md)
- [Transcript Auto-scroll Stability](system-design/transcript-auto-scroll.md)
## Related
- [Platform](../platform/README.md)
@@ -5,7 +5,7 @@ created: 2026-07-15
owners:
- kandev
---
# ACP Model Configuration Summary Requirements
# ACP model summary
## Overview
@@ -0,0 +1,67 @@
---
status: active
system: ui
created: 2026-09-03
owners:
- kandev
---
# Changes File Action Feedback Requirements
## Overview
Working-tree file rows expose stage and unstage actions in the Changes panel.
On fine-pointer layouts, the action shares the file-type icon slot and normally
appears only while the row is hovered. Once an action starts, its progress
feedback must remain visible until the associated request finishes, even when
the pointer leaves the row. The UI system owns this transient presentation
contract; Git operation execution and pending-state lifetime remain owned by
their existing systems.
## Terminology
- **File action slot:** The leading Changes-row position that shows either the
file-type icon, the stage or unstage action, or its pending indicator.
- **Pending file action:** A stage or unstage request whose per-file pending
state has started and has not yet cleared.
## Requirements
### REQ-UI-CHANGES-FILE-ACTION-FEEDBACK-001: Persistent pending file-action feedback
**Intent:** Let users identify which file operation is still running without
keeping the pointer over its row.
**User story:** As a user staging or unstaging a file, I want its progress
indicator to remain visible until the operation finishes, so that I know when
the action is complete.
#### Acceptance criteria
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.1:** When a user starts a stage or
unstage action for one file, that row shall replace its stage or unstage
control with the existing animated pending indicator for the lifetime of the
file's pending state.
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.2:** On a fine-pointer layout, moving
the pointer away from a row with a pending file action shall keep the pending
indicator visible and shall not restore the file-type icon while the action
remains pending.
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.3:** Pending feedback shall remain
scoped to the acted-on repository and file. Other file rows shall retain
their current icons and actions.
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.4:** When the pending state clears,
the row shall return to its current idle presentation or move to the section
that reflects the refreshed staged state.
- **AC-UI-CHANGES-FILE-ACTION-FEEDBACK-001.5:** Coarse-pointer Changes surfaces
shall retain their always-visible, touch-sized file actions and show the same
pending indicator without introducing a hover dependency or reducing the
existing touch target.
## Out of scope
- Changing Git stage or unstage execution, pending-state creation, or
pending-state cleanup.
- Changing bulk stage, bulk unstage, discard, edit, or commit feedback.
- Adding user-facing copy, notifications, cancellation, or progress estimates.
- Changing Changes-panel navigation, grouping, row density, or scroll
ownership.
@@ -0,0 +1,102 @@
---
status: current
system: ui
requirements:
- REQ-UI-CHANGES-FILE-ACTION-FEEDBACK-001
---
# File-action feedback
## Purpose and boundaries
This design keeps the existing per-file stage and unstage spinner visible after
a fine-pointer user leaves the affected Changes row. It changes only the
presentation precedence inside the shared file-action slot. Git operation
dispatch, repository scoping, status refresh, and pending-state cleanup remain
unchanged.
## Requirement mapping
| Requirement | Design section |
| --- | --- |
| `REQ-UI-CHANGES-FILE-ACTION-FEEDBACK-001` | [Components and responsibilities](#components-and-responsibilities), [Visibility precedence](#visibility-precedence), [Responsive behavior](#responsive-behavior), and [Verification](#verification) |
## Components and responsibilities
- `useSessionGit` continues to own `pendingStageFiles`, keyed by repository and
path. It starts the per-file pending state before dispatching
`worktree.stage` or `worktree.unstage` and clears it through the existing
status-refresh and failure paths.
- `changes-panel-tree.tsx` and `changes-panel-timeline.tsx` continue to derive
`FileRow.isPending` from that set. No additional local loading state is added.
- `FileRow` continues to share one implementation across the desktop Changes
panel and the phone Changes surface.
- `TreeModeFileActionSlot` owns the fine-pointer swap between `FileIcon` and
`StageButton`. Pending state takes precedence over the idle hover rule.
- `StageButton` continues to replace either stage or unstage control with the
existing `IconLoader2` spinner while `isPending` is true.
## Visibility precedence
The tree-mode slot has three presentation states:
1. On a fine pointer while idle and not hovered, the file-type icon is visible
and the stage or unstage action is hidden.
2. On a fine pointer while idle and hovered, the icon is hidden and the action
is visible and interactive.
3. While pending, the icon remains hidden and the action layer containing the
spinner remains visible regardless of hover. The pending layer stays
noninteractive through the existing spinner-only branch.
The correction is expressed from `isPending` and the existing pointer mode. It
does not add timers, duplicate request state, pointer listeners, or a second
spinner. When pending state clears, the existing hover rule becomes
authoritative again or the refreshed Git status moves the file to its new
section.
## Responsive behavior
- **Desktop outcome:** the resizable Changes panel keeps the spinner in the
leading file-action slot after pointer leave, so the acted-on file remains
identifiable throughout the operation.
- **Mobile entry point and surface:** the existing Changes bottom-navigation
item opens `MobileChangesPanel`; the shared row keeps its always-visible
coarse-pointer action and 44px target.
- **Nearest shipped exemplar:** the current coarse-pointer branch in
`TreeModeFileActionSlot` already makes the action and pending branch
independent of hover. The desktop correction applies the same pending-state
precedence without changing desktop density.
- **Hierarchy and primary action:** file identity and the stage or unstage
control remain in the existing row. No drawer, route, or new navigation is
introduced.
- **Surface rationale:** this is short, row-scoped request feedback, so keeping
it inline preserves context better than opening a temporary surface.
- **Shared logic and geometry:** both viewport classes consume the same
`isPending` value. Scroll ownership, safe-area handling, path truncation, and
document overflow behavior remain unchanged.
## Failure and recovery
The UI does not infer completion from hover or elapsed time. It follows the
existing `isPending` input. Existing status-refresh and request-failure paths
clear that state; the slot then restores the idle icon/action presentation.
Backend and WebSocket errors remain surfaced through their current paths.
## Verification
- A Chromium Playwright regression pauses the outgoing `worktree.stage`
request, moves the pointer away from the row, and proves that the spinner
remains visible until the request is released and the file moves to the
staged section.
- The same regression pauses `worktree.unstage`, repeats the pointer-leave
assertion, and then proves the file returns to the unstaged section.
- The test intercepts only the selected worktree request at the WebSocket
transport boundary and forwards every unrelated frame.
- Existing coarse-pointer component and Pixel 5 Changes tests continue to prove
that stage and unstage actions are touch-reachable. No new mobile composition
or interaction is introduced.
## Related decisions
None. The change corrects local rendering precedence without changing a
runtime, data, or ownership boundary.
@@ -5,7 +5,7 @@ requirements:
- REQ-UI-CHANGES-FILE-ROW-CONTAINMENT-001
---
# Changes File Row Containment System Design
# File-row containment
## Purpose and boundaries
@@ -7,7 +7,7 @@ created: 2026-06-18
owners:
- tbd
---
# Task PR Automation Controls System Design Part 1
# Task PR automation (part 1)
## Purpose and boundaries
@@ -7,7 +7,7 @@ created: 2026-06-18
owners:
- tbd
---
# Task PR Automation Controls System Design Part 2
# Task PR automation (part 2)
## Purpose and boundaries
@@ -7,7 +7,7 @@ created: 2026-06-18
owners:
- tbd
---
# Task PR Automation Controls System Design Part 3
# Task PR automation (part 3)
## Purpose and boundaries
@@ -5,7 +5,7 @@ requirements:
- REQ-UI-COMMAND-PANEL-TASK-ACTIVITY-001
---
# Command Panel Task Activity Icons System Design
# Command-panel activity icons
## Purpose and boundaries
@@ -5,7 +5,7 @@ requirements:
- REQ-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001
---
# Compact Workflow Step Navigation System Design
# Compact step navigation
## Purpose and boundaries
@@ -5,7 +5,7 @@ requirements:
- REQ-UI-KANDEV-MCP-TOOL-RESULTS-001
---
# Kandev MCP Tool Results System Design
# MCP tool results
## Purpose and boundaries
@@ -8,7 +8,7 @@ owners:
- kandev
---
# Task transcript history visibility system design
# Transcript history visibility
## Purpose and boundaries