From c9137998df76a94ef2548aea6a0c8caaef99c938 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:48:18 -0400 Subject: [PATCH 1/3] multiple fixes thorughout the git actiosn pane and also chat runtime --- .../main/services/chat/agentChatService.ts | 2 +- .../main/services/git/gitOperationsService.ts | 13 ++ .../src/main/services/github/githubService.ts | 13 +- .../src/main/services/ipc/registerIpc.ts | 17 ++ .../src/main/services/prs/prService.ts | 30 ++- apps/desktop/src/preload/global.d.ts | 5 + apps/desktop/src/preload/preload.ts | 7 + .../src/renderer/components/app/TabNav.tsx | 4 +- .../src/renderer/components/app/TopBar.tsx | 4 +- .../components/chat/chatStatusVisuals.tsx | 2 +- .../components/lanes/LaneGitActionsPane.tsx | 190 +++++++++++------- .../components/lanes/LaneTerminalsPanel.tsx | 4 +- .../renderer/components/lanes/LanesPage.tsx | 2 +- .../renderer/components/prs/CreatePrModal.tsx | 21 +- apps/desktop/src/renderer/index.css | 8 + .../src/renderer/lib/terminalAttention.ts | 2 +- apps/desktop/src/shared/ipc.ts | 3 + 17 files changed, 235 insertions(+), 92 deletions(-) diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index 1957c1e25..3ff346752 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -8954,7 +8954,7 @@ export function createAgentChatService(args: { } const nextProvider: AgentChatProvider = resolveProviderGroupForModel(descriptor); - const nextModel = descriptor.isCliWrapped ? descriptor.shortId : descriptor.id; + const nextModel = descriptor.isCliWrapped ? descriptor.sdkModelId : descriptor.id; const previousModelId = managed.session.modelId ?? resolveModelIdFromStoredValue(managed.session.model, managed.session.provider) ?? managed.session.model; diff --git a/apps/desktop/src/main/services/git/gitOperationsService.ts b/apps/desktop/src/main/services/git/gitOperationsService.ts index 0b03679c3..dcc7e82b2 100644 --- a/apps/desktop/src/main/services/git/gitOperationsService.ts +++ b/apps/desktop/src/main/services/git/gitOperationsService.ts @@ -770,6 +770,19 @@ export function createGitOperationsService({ return action; }, + async stashClear(args: { laneId: string }): Promise { + const { action } = await runLaneOperation({ + laneId: args.laneId, + kind: "git_stash_clear", + reason: "stash_clear", + metadata: {}, + fn: async (lane) => { + await runGitOrThrow(["stash", "clear"], { cwd: lane.worktreePath, timeoutMs: 15_000 }); + } + }); + return action; + }, + async fetch(args: { laneId: string }): Promise { const { action } = await runLaneOperation({ laneId: args.laneId, diff --git a/apps/desktop/src/main/services/github/githubService.ts b/apps/desktop/src/main/services/github/githubService.ts index 191b86bb4..1d2775263 100644 --- a/apps/desktop/src/main/services/github/githubService.ts +++ b/apps/desktop/src/main/services/github/githubService.ts @@ -258,17 +258,26 @@ export function createGithubService({ const message = (data && typeof data === "object" && !Array.isArray(data) ? asString((data as any).message) : "") || `GitHub API request failed (HTTP ${response.status})`; + let detail = ""; + if (data && typeof data === "object" && !Array.isArray(data) && Array.isArray((data as any).errors)) { + const errorMessages = ((data as any).errors as any[]) + .map((e) => (typeof e === "object" && e && typeof e.message === "string" ? e.message : null)) + .filter(Boolean); + if (errorMessages.length > 0) { + detail = ": " + errorMessages.join("; "); + } + } const rateRemaining = response.headers.get("x-ratelimit-remaining"); const rateReset = response.headers.get("x-ratelimit-reset"); if (rateRemaining === "0" && rateReset) { const resetAtMs = Number(rateReset) * 1000; const err = new Error( - `${message} (rate limit exceeded; resets at ${new Date(resetAtMs).toLocaleString()})` + `${message}${detail} (rate limit exceeded; resets at ${new Date(resetAtMs).toLocaleString()})` ); (err as any).rateLimitResetAtMs = resetAtMs; throw err; } - throw new Error(message); + throw new Error(message + detail); } // Cache ETag for future conditional requests diff --git a/apps/desktop/src/main/services/ipc/registerIpc.ts b/apps/desktop/src/main/services/ipc/registerIpc.ts index 301cf14d1..cd61fcc17 100644 --- a/apps/desktop/src/main/services/ipc/registerIpc.ts +++ b/apps/desktop/src/main/services/ipc/registerIpc.ts @@ -177,6 +177,8 @@ import type { AgentChatSessionCapabilities, AgentChatSessionCapabilitiesArgs, AgentChatSteerArgs, + AgentChatCancelSteerArgs, + AgentChatEditSteerArgs, AgentChatUnifiedPermissionMode, AgentChatUpdateSessionArgs, AgentChatSlashCommand, @@ -3738,6 +3740,16 @@ export function registerIpc({ await ctx.agentChatService.steer(arg); }); + ipcMain.handle(IPC.agentChatCancelSteer, async (_event, arg: AgentChatCancelSteerArgs): Promise => { + const ctx = getCtx(); + await ctx.agentChatService.cancelSteer(arg); + }); + + ipcMain.handle(IPC.agentChatEditSteer, async (_event, arg: AgentChatEditSteerArgs): Promise => { + const ctx = getCtx(); + await ctx.agentChatService.editSteer(arg); + }); + ipcMain.handle(IPC.agentChatInterrupt, async (_event, arg: AgentChatInterruptArgs): Promise => { const ctx = getCtx(); await ctx.agentChatService.interrupt(arg); @@ -4096,6 +4108,11 @@ export function registerIpc({ return ctx.gitService.stashDrop(arg); }); + ipcMain.handle(IPC.gitStashClear, async (_event, arg: { laneId: string }): Promise => { + const ctx = getCtx(); + return ctx.gitService.stashClear(arg); + }); + ipcMain.handle(IPC.gitFetch, async (_event, arg: { laneId: string }): Promise => { const ctx = getCtx(); return ctx.gitService.fetch(arg); diff --git a/apps/desktop/src/main/services/prs/prService.ts b/apps/desktop/src/main/services/prs/prService.ts index 21cc1d90b..e87cd71e4 100644 --- a/apps/desktop/src/main/services/prs/prService.ts +++ b/apps/desktop/src/main/services/prs/prService.ts @@ -1966,17 +1966,25 @@ export function createPrService({ } const createdAt = nowIso(); - const created = await githubService.apiRequest({ - method: "POST", - path: `/repos/${repo.owner}/${repo.name}/pulls`, - body: { - title: args.title, - head: headBranch, - base: baseBranch, - body: args.body, - draft: Boolean(args.draft) - } - }); + let created: { data: any; response: Response | null }; + try { + created = await githubService.apiRequest({ + method: "POST", + path: `/repos/${repo.owner}/${repo.name}/pulls`, + body: { + title: args.title, + head: headBranch, + base: baseBranch, + body: args.body, + draft: Boolean(args.draft) + } + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error( + `Failed to create pull request for "${headBranch}" → "${baseBranch}": ${msg}` + ); + } const pr = created.data; const prNumber = Number(pr?.number); diff --git a/apps/desktop/src/preload/global.d.ts b/apps/desktop/src/preload/global.d.ts index 27734342e..29edf6687 100644 --- a/apps/desktop/src/preload/global.d.ts +++ b/apps/desktop/src/preload/global.d.ts @@ -82,6 +82,8 @@ import type { AgentChatSessionCapabilitiesArgs, AgentChatSessionSummary, AgentChatSteerArgs, + AgentChatCancelSteerArgs, + AgentChatEditSteerArgs, AgentChatSubagentSnapshot, AgentChatSubagentListArgs, AgentChatUpdateSessionArgs, @@ -825,6 +827,8 @@ declare global { handoff: (args: AgentChatHandoffArgs) => Promise; send: (args: AgentChatSendArgs) => Promise; steer: (args: AgentChatSteerArgs) => Promise; + cancelSteer: (args: AgentChatCancelSteerArgs) => Promise; + editSteer: (args: AgentChatEditSteerArgs) => Promise; interrupt: (args: AgentChatInterruptArgs) => Promise; resume: (args: AgentChatResumeArgs) => Promise; approve: (args: AgentChatApproveArgs) => Promise; @@ -895,6 +899,7 @@ declare global { stashApply: (args: GitStashRefArgs) => Promise; stashPop: (args: GitStashRefArgs) => Promise; stashDrop: (args: GitStashRefArgs) => Promise; + stashClear: (args: { laneId: string }) => Promise; fetch: (args: { laneId: string }) => Promise; pull: (args: { laneId: string }) => Promise; getSyncStatus: (args: { laneId: string }) => Promise; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index e405e75ce..f9c5f6168 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -241,6 +241,8 @@ import type { AgentChatSessionCapabilitiesArgs, AgentChatSessionSummary, AgentChatSteerArgs, + AgentChatCancelSteerArgs, + AgentChatEditSteerArgs, AgentChatSubagentSnapshot, AgentChatSubagentListArgs, AgentChatUpdateSessionArgs, @@ -1120,6 +1122,10 @@ contextBridge.exposeInMainWorld("ade", { ipcRenderer.invoke(IPC.agentChatSend, args), steer: async (args: AgentChatSteerArgs): Promise => ipcRenderer.invoke(IPC.agentChatSteer, args), + cancelSteer: async (args: AgentChatCancelSteerArgs): Promise => + ipcRenderer.invoke(IPC.agentChatCancelSteer, args), + editSteer: async (args: AgentChatEditSteerArgs): Promise => + ipcRenderer.invoke(IPC.agentChatEditSteer, args), interrupt: async (args: AgentChatInterruptArgs): Promise => ipcRenderer.invoke(IPC.agentChatInterrupt, args), resume: async (args: AgentChatResumeArgs): Promise => @@ -1237,6 +1243,7 @@ contextBridge.exposeInMainWorld("ade", { stashApply: async (args: GitStashRefArgs): Promise => ipcRenderer.invoke(IPC.gitStashApply, args), stashPop: async (args: GitStashRefArgs): Promise => ipcRenderer.invoke(IPC.gitStashPop, args), stashDrop: async (args: GitStashRefArgs): Promise => ipcRenderer.invoke(IPC.gitStashDrop, args), + stashClear: async (args: { laneId: string }): Promise => ipcRenderer.invoke(IPC.gitStashClear, args), fetch: async (args: { laneId: string }): Promise => ipcRenderer.invoke(IPC.gitFetch, args), pull: async (args: { laneId: string }): Promise => ipcRenderer.invoke(IPC.gitPull, args), getSyncStatus: async (args: { laneId: string }): Promise => diff --git a/apps/desktop/src/renderer/components/app/TabNav.tsx b/apps/desktop/src/renderer/components/app/TabNav.tsx index 78d4ce13f..77fc85616 100644 --- a/apps/desktop/src/renderer/components/app/TabNav.tsx +++ b/apps/desktop/src/renderer/components/app/TabNav.tsx @@ -115,10 +115,10 @@ export function TabNav() { : "All active terminals running" } className={cn( - "absolute -right-1 -top-1 ade-status-dot animate-spin", + "absolute -right-1 -top-1 ade-status-dot", terminalAttention.indicator === "running-needs-attention" ? "ade-status-dot-warning" - : "ade-status-dot-active", + : "ade-status-dot-active animate-spin", )} /> ) : null} diff --git a/apps/desktop/src/renderer/components/app/TopBar.tsx b/apps/desktop/src/renderer/components/app/TopBar.tsx index c700f0ba1..61badd1fb 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.tsx @@ -351,10 +351,10 @@ export function TopBar() { : `${terminalAttention.runningCount} running terminal${terminalAttention.runningCount === 1 ? "" : "s"}` } className={cn( - "ade-status-dot h-1.5 w-1.5 shrink-0 animate-pulse", + "ade-status-dot h-1.5 w-1.5 shrink-0", indicator === "running-needs-attention" ? "ade-status-dot-warning" - : "ade-status-dot-active" + : "ade-status-dot-active animate-pulse" )} /> ) : null} diff --git a/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx b/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx index 75ecc94ba..f6acb7c19 100644 --- a/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx +++ b/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx @@ -39,7 +39,7 @@ export function ChatStatusGlyph({ case "failed": return ; case "waiting": - return ; + return ; case "working": return ; } diff --git a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx index d47add419..7914f7772 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx @@ -1275,6 +1275,7 @@ export function LaneGitActionsPane({ + )} + + }} + > + SAVE CHANGES + + {stashes.length === 0 ? (
- Use stash when you want to clear the worktree without committing. Hover the buttons for the git details. + Save your in-progress changes without committing. You can restore them later.
) : (
{stashes.slice(0, responsiveMode === "wide" ? 2 : 3).map((stash) => (
-
-
- {stash.subject || stash.ref} -
-
- {stash.ref} · {formatRelativeTime(stash.createdAt)} +
+
+
+ {stash.subject || stash.ref} +
+
+ {stash.ref} · {formatRelativeTime(stash.createdAt)} +
+ + + +
+
+ Restore removes entry. Copy to Worktree keeps it. Delete discards permanently.
- - -
))} {stashes.length > (responsiveMode === "wide" ? 2 : 3) ? ( diff --git a/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx b/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx index 88751f690..e2c4620bb 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx @@ -22,7 +22,7 @@ const tabTrigger = function statusDotCls(indicator: ReturnType): string { if (indicator === "running-active") return "border-2 border-emerald-500 border-t-transparent bg-transparent"; - if (indicator === "running-needs-attention") return "border-2 border-amber-400 border-t-transparent bg-transparent"; + if (indicator === "running-needs-attention") return "bg-amber-400"; return "bg-red-500"; } @@ -321,7 +321,7 @@ export function LaneTerminalsPanel({ overrideLaneId }: { overrideLaneId?: string runtimeState: s.runtimeState }); const dotClass = statusDotCls(indicator); - const dotSpin = !profileColor && indicator !== "ended"; + const dotSpin = !profileColor && indicator === "running-active"; return ( NEW LANE {addLaneDropdownOpen ? ( -
+
@@ -1652,7 +1654,7 @@ export function LaneGitActionsPane({
) : (
- {stashes.slice(0, responsiveMode === "wide" ? 2 : 3).map((stash) => ( + {stashes.slice(0, maxVisibleStashes).map((stash) => (
))} - {stashes.length > (responsiveMode === "wide" ? 2 : 3) ? ( + {stashes.length > maxVisibleStashes ? (
- +{stashes.length - (responsiveMode === "wide" ? 2 : 3)} more stash entr{stashes.length - (responsiveMode === "wide" ? 2 : 3) === 1 ? "y" : "ies"}. + +{stashes.length - maxVisibleStashes} more stash entr{stashes.length - maxVisibleStashes === 1 ? "y" : "ies"}.
) : null}
diff --git a/apps/desktop/src/renderer/lib/terminalAttention.test.ts b/apps/desktop/src/renderer/lib/terminalAttention.test.ts index aa8686b2f..8f73d65c2 100644 --- a/apps/desktop/src/renderer/lib/terminalAttention.test.ts +++ b/apps/desktop/src/renderer/lib/terminalAttention.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { runningSessionNeedsAttention, sessionIndicatorState } from "./terminalAttention"; +import { runningSessionNeedsAttention, sessionIndicatorState, sessionStatusDot } from "./terminalAttention"; describe("terminalAttention", () => { it("does not treat a plain shell prompt as awaiting user input", () => { @@ -21,4 +21,36 @@ describe("terminalAttention", () => { }), ).toBe("running-needs-attention"); }); + + describe("sessionStatusDot", () => { + it("returns a spinning emerald dot for a running active session", () => { + const dot = sessionStatusDot({ + status: "running", + lastOutputPreview: "building project...", + }); + expect(dot.spinning).toBe(true); + expect(dot.cls).toContain("emerald"); + expect(dot.label).toBe("Running"); + }); + + it("returns a solid (non-spinning) amber dot for a running needs-attention session", () => { + const dot = sessionStatusDot({ + status: "running", + lastOutputPreview: "Confirm continue? (y/n)", + }); + expect(dot.spinning).toBe(false); + expect(dot.cls).toContain("amber"); + expect(dot.label).toBe("Awaiting input"); + }); + + it("returns a solid red dot for an ended session", () => { + const dot = sessionStatusDot({ + status: "exited", + lastOutputPreview: "Process exited with code 0", + }); + expect(dot.spinning).toBe(false); + expect(dot.cls).toContain("red"); + expect(dot.label).toBe("Ended"); + }); + }); }); diff --git a/docs/architecture/AI_INTEGRATION.md b/docs/architecture/AI_INTEGRATION.md index 26a74aa0c..cf864f350 100644 --- a/docs/architecture/AI_INTEGRATION.md +++ b/docs/architecture/AI_INTEGRATION.md @@ -1482,6 +1482,8 @@ interface AgentChatService { createSession(laneId: string, provider: "codex" | "claude" | "unified", model: string, modelId: string, opts?: CreateSessionOpts): Promise; sendMessage(sessionId: string, text: string, attachments?: FileRef[]): AsyncIterable; steer(sessionId: string, text: string): Promise; + cancelSteer(sessionId: string): Promise; + editSteer(sessionId: string, text: string): Promise; interrupt(sessionId: string): Promise; resumeSession(sessionId: string): Promise; listSessions(laneId?: string): Promise; @@ -1576,6 +1578,8 @@ send({ method: "initialized", params: {} }); | `createSession()` | `thread/start` | Params: `model`, `cwd` (lane worktree), `approvalPolicy`, `sandbox` | | `sendMessage()` | `turn/start` | Input array: `[{ type: "text", text }, ...attachments]` | | `steer()` | `turn/steer` | Appends to in-flight turn; cannot change model/sandbox | +| `cancelSteer()` | N/A | Cancel a pending steer before it is delivered | +| `editSteer()` | N/A | Replace pending steer text before it is delivered | | `interrupt()` | `turn/interrupt` | Turn completes with `status: "interrupted"` | | `resumeSession()` | `thread/resume` | Params: `threadId`, optional `personality` | | `listSessions()` | `thread/list` | Filter by `cwd` to scope to lane | diff --git a/docs/architecture/DATA_MODEL.md b/docs/architecture/DATA_MODEL.md index e7f1f0b7d..ce665c807 100644 --- a/docs/architecture/DATA_MODEL.md +++ b/docs/architecture/DATA_MODEL.md @@ -289,7 +289,7 @@ Records every significant operation (git commands, pack updates, etc.) with timi Known `kind` values: - `git_stage`, `git_unstage`, `git_discard`, `git_restore_staged` - `git_commit`, `git_commit_amend`, `git_revert`, `git_cherry_pick` -- `git_stash_push`, `git_stash_apply`, `git_stash_pop`, `git_stash_drop` +- `git_stash_push`, `git_stash_apply`, `git_stash_pop`, `git_stash_drop`, `git_stash_clear` - `git_fetch`, `git_sync_merge`, `git_sync_rebase` - `git_push`, `git_push_force_with_lease` - `pack_update_lane`, `pack_update_project` diff --git a/docs/architecture/GIT_ENGINE.md b/docs/architecture/GIT_ENGINE.md index da65add2b..4beafda73 100644 --- a/docs/architecture/GIT_ENGINE.md +++ b/docs/architecture/GIT_ENGINE.md @@ -178,6 +178,7 @@ The `discardFile` operation checks whether the file is untracked (via `git statu | `stashApply` | `git stash apply ` | Apply stash without removing it | | `stashPop` | `git stash pop ` | Apply and remove stash | | `stashDrop` | `git stash drop ` | Remove stash without applying | +| `stashClear` | `git stash clear` | Remove all stash entries | #### Sync Operations @@ -330,7 +331,7 @@ Live orchestrator/MCP context exports do not wait for those compatibility refres - Lane status computation (dirty, ahead, behind) - File operations: stage, unstage, discard, restore staged - Commit operations: commit, amend, revert, cherry-pick -- Stash operations: push, list, apply, pop, drop +- Stash operations: push, list, apply, pop, drop, clear - Sync operations: fetch, merge sync, rebase sync - Push with automatic upstream setup and force-with-lease option - Recent commits listing with parsed metadata diff --git a/docs/architecture/MULTI_DEVICE_SYNC.md b/docs/architecture/MULTI_DEVICE_SYNC.md index 9d29a8578..e2bb87e58 100644 --- a/docs/architecture/MULTI_DEVICE_SYNC.md +++ b/docs/architecture/MULTI_DEVICE_SYNC.md @@ -278,7 +278,7 @@ Rule for future workstreams: - **State-only operations** (create lane metadata, update settings): write locally, cr-sqlite syncs. - **Execution operations** (create worktree, run terminal command, create PR): send command to host via WebSocket. Host executes, state changes sync back. -The remote command service (`syncRemoteCommandService`) registers named actions that controllers can invoke. Agent chat commands include `chat.create`, `chat.send`, `chat.interrupt`, `chat.steer`, `chat.approve`, `chat.respondToInput`, `chat.resume`, `chat.updateSession`, `chat.dispose`, and `chat.models`. All chat commands are viewer-allowed and queueable, enabling full remote chat session control from companion devices. +The remote command service (`syncRemoteCommandService`) registers named actions that controllers can invoke. Agent chat commands include `chat.create`, `chat.send`, `chat.interrupt`, `chat.steer`, `chat.cancelSteer`, `chat.editSteer`, `chat.approve`, `chat.respondToInput`, `chat.resume`, `chat.updateSession`, `chat.dispose`, and `chat.models`. All chat commands are viewer-allowed and queueable, enabling full remote chat session control from companion devices. --- diff --git a/docs/features/LANES.md b/docs/features/LANES.md index d5feccaa6..856f5e754 100644 --- a/docs/features/LANES.md +++ b/docs/features/LANES.md @@ -143,7 +143,7 @@ The center pane is the main working area. The LanesPage uses `PaneTilingLayout` **Sub-panes** within the center area (via PaneTilingLayout): - **Diff pane** (`LaneDiffPane`): Git diff viewer with Monaco side-by-side diffs, per-file stage/unstage/discard, commit diff viewing -- **Git actions pane** (`LaneGitActionsPane`): Commit, stash, fetch, sync (merge/rebase), push operations with recent commits list, rebase button for stacked lanes +- **Git actions pane** (`LaneGitActionsPane`): Commit, stash, fetch, sync (merge/rebase), push operations with recent commits list, rebase button for stacked lanes. The Pull button flashes and shows a behind-count badge when the lane is behind its upstream. Stash buttons use user-friendly labels (Save Changes, Restore, Copy to Worktree, Delete) with inline explanations. The Save Changes button is disabled when there are no working tree changes. A Clear All button (with numeric confirmation) appears when stashes exist. - **Terminals pane** (`LaneTerminalsPanel`): Embedded terminal sessions with tab/tiling views, quick-launch profiles, session delta cards - **Work pane** (`LaneWorkPane`): Embedded terminal sessions and agent chat view (terminal/chat toggle) - **Stack pane** (`LaneStackPane`): Stack chain visualization and management @@ -228,7 +228,7 @@ Each section shows a file list with change type indicators: - Amend checkbox (future) **Advanced git controls**: -- **Stash**: Push (with optional message), Pop, Apply, Drop, List stashes +- **Stash**: Save Changes (push with optional message), Restore (pop), Copy to Worktree (apply), Delete (drop), Clear All (with numeric confirmation), List stashes - **Fetch**: Fetch from remote (all refs or specific) - **Sync**: Merge or Rebase with upstream/base ref - **Push**: Push to remote, with force-with-lease option @@ -269,7 +269,7 @@ The inspector is a collapsible sidebar on the right edge of the Lanes tab. It pr |---------|---------------| | `laneService` | CRUD operations for lanes. Creates/removes worktrees via git. Computes lane status by aggregating dirty state, ahead/behind, and other signals. Manages lane metadata in the database. Supports primary, worktree, and attached lane types. Provides rebase (recursive rebase with remote tracking branch resolution for primary parents, dirty-worktree guard), reparent, stack chain, appearance management, and `createFromUnstaged` (stash-based unstaged change rescue to a new child lane with automatic rollback on failure). | | `rebaseSuggestionService` | Monitors stacked lanes for parent-advanced state. Generates rebase suggestions with dismiss/defer lifecycle. Emits real-time suggestion events to the renderer. | -| `gitService` | All git operations: stage, unstage, discard, commit, stash, fetch, sync (merge/rebase), push, conflict state detection (merge/rebase in-progress, continue, abort). Operates on a specified worktree path. Returns structured results with success/failure and output. | +| `gitService` | All git operations: stage, unstage, discard, commit, stash (push, list, apply, pop, drop, clear), fetch, sync (merge/rebase), push, conflict state detection (merge/rebase in-progress, continue, abort). Operates on a specified worktree path. Returns structured results with success/failure and output. | | `diffService` | Computes working tree diffs (unstaged changes) and index diffs (staged changes). Per-file diff content for the Monaco viewer. Handles binary file detection and large file truncation. | | `operationService` | Records all git operations with before/after SHA transitions. Provides an audit trail for every action taken in a lane. Used by the History tab. | | `laneProxyService` | Per-lane hostname reverse proxy. Routes HTTP traffic by Host header to the correct lane's dev server. Manages proxy lifecycle, route registration, and preview URL generation. | @@ -330,6 +330,7 @@ The inspector is a collapsible sidebar on the right edge of the Lanes tab. It pr | `ade.git.stashApply` | `(args: { worktreePath: string, index?: number }) => GitActionResult` | Apply a stash without removing it | | `ade.git.stashPop` | `(args: { worktreePath: string, index?: number }) => GitActionResult` | Apply and remove a stash | | `ade.git.stashDrop` | `(args: { worktreePath: string, index?: number }) => GitActionResult` | Remove a stash entry | +| `ade.git.stashClear` | `(args: { laneId: string }) => GitActionResult` | Remove all stash entries | | `ade.git.fetch` | `(args: { worktreePath: string, remote?: string }) => GitActionResult` | Fetch from remote | | `ade.git.sync` | `(args: { worktreePath: string, strategy: 'merge' \| 'rebase', ref?: string }) => GitActionResult` | Merge or rebase with upstream | | `ade.git.push` | `(args: { worktreePath: string, forceWithLease?: boolean }) => GitActionResult` | Push to remote | diff --git a/docs/features/PULL_REQUESTS.md b/docs/features/PULL_REQUESTS.md index efb34c3b5..d14580052 100644 --- a/docs/features/PULL_REQUESTS.md +++ b/docs/features/PULL_REQUESTS.md @@ -122,7 +122,7 @@ Notification messages emitted by the polling service are now generic (not PR-spe After a successful GitHub merge, cleanup operations (branch deletion, group membership removal, lane archiving, base branch fetch, cache invalidation, rebase-needs scan) are wrapped in an outer try-catch so that a failure in any cleanup step does not mask the successful merge. The operation is marked as succeeded regardless, with a `cleanupError` metadata field when something went wrong. Individual cleanup failures are logged as warnings. -The Create PR modal validates branch names against invalid git ref characters before submission. +The Create PR modal validates branch names against invalid git ref characters before submission. All three PR creation modes (normal, queue, integration) display lane warning panels alongside the draft checkbox, surfacing rebase needs and other lane health issues before submission. Error messages from failed GitHub API calls are cleaned of internal IPC prefixes before display, and the PR service wraps creation failures with contextual head/base branch information for clearer diagnostics. GitHub API error responses now extract nested error detail messages (from the `errors` array in the response body) and append them to the thrown error, improving visibility of issues such as duplicate PR or branch protection failures. The PR detail pane renders markdown body content with `rehype-sanitize` applied after `rehype-raw`, stripping potentially unsafe HTML from PR descriptions fetched from GitHub. diff --git a/docs/features/TERMINALS_AND_SESSIONS.md b/docs/features/TERMINALS_AND_SESSIONS.md index 5841651ac..c926faa6a 100644 --- a/docs/features/TERMINALS_AND_SESSIONS.md +++ b/docs/features/TERMINALS_AND_SESSIONS.md @@ -106,3 +106,15 @@ The current terminals and sessions feature follows these rules: - avoid perpetual idle polling when there are no live sessions to watch That preserves ADE's session-awareness while making the session system a lighter dependency for the rest of the UI. + +--- + +## Terminal status indicators + +Terminal session status dots use distinct visual treatments per state: + +- **Running (active)**: Spinning emerald ring (border spinner animation). Used in the tab nav, top bar, and terminal panel tabs. +- **Running (needs attention)**: Solid amber dot (no animation). Indicates the terminal is awaiting user input. The top bar variant pulses; the tab nav variant does not animate. +- **Ended**: Solid red dot (no animation). + +The `sessionStatusDot()` helper in `terminalAttention.ts` and `sessionIndicatorState()` produce the dot class and spinning flag. The chat status glyph for "waiting" state renders a static check-circle icon rather than a spinner, distinguishing idle-waiting from active-working. From d6af52bc554bc76b4e0e6761cb89889a362b09a1 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:22:42 -0400 Subject: [PATCH 3/3] fixing review comments and ci issues --- .../main/services/chat/agentChatService.ts | 12 +++++++ .../src/main/services/ipc/registerIpc.ts | 31 ++++++++++++++++--- .../src/main/services/prs/prService.test.ts | 2 +- .../chat/AgentChatMessageList.test.tsx | 5 +-- .../components/chat/chatStatusVisuals.tsx | 4 +-- .../components/lanes/LaneTerminalsPanel.tsx | 2 +- apps/desktop/src/renderer/index.css | 3 +- .../renderer/lib/terminalAttention.test.ts | 2 +- apps/desktop/src/shared/types/chat.ts | 9 ++++++ 9 files changed, 58 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index 3ff346752..0f590284d 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -49,6 +49,7 @@ import { } from "../../../shared/types"; import type { AgentChatApprovalDecision, + AgentChatCancelSteerArgs, AgentChatClaudePermissionMode, AgentChatCompletionReport, AgentChatCodexApprovalPolicy, @@ -56,6 +57,7 @@ import type { AgentChatCodexSandbox, AgentChatCreateArgs, AgentChatDisposeArgs, + AgentChatEditSteerArgs, AgentChatExecutionMode, AgentChatEvent, AgentChatEventEnvelope, @@ -8355,6 +8357,14 @@ export function createAgentChatService(args: { await executePreparedSendMessage(preparedSteer); }; + const cancelSteer = async ({ sessionId }: AgentChatCancelSteerArgs): Promise => { + await interrupt({ sessionId }); + }; + + const editSteer = async ({ sessionId, text }: AgentChatEditSteerArgs): Promise => { + await steer({ sessionId, text }); + }; + const interrupt = async ({ sessionId }: AgentChatInterruptArgs): Promise => { const managed = ensureManagedSession(sessionId); @@ -9349,6 +9359,8 @@ export function createAgentChatService(args: { sendMessage, runSessionTurn, steer, + cancelSteer, + editSteer, interrupt, resumeSession, listSessions, diff --git a/apps/desktop/src/main/services/ipc/registerIpc.ts b/apps/desktop/src/main/services/ipc/registerIpc.ts index cd61fcc17..1aace2a6f 100644 --- a/apps/desktop/src/main/services/ipc/registerIpc.ts +++ b/apps/desktop/src/main/services/ipc/registerIpc.ts @@ -3538,6 +3538,29 @@ export function registerIpc({ return { laneId: record.laneId }; }; + const parseAgentChatCancelSteerArgs = ( + value: unknown, + ): AgentChatCancelSteerArgs => { + const record = requireRecord(value, "Agent chat cancel steer request"); + if (typeof record.sessionId !== "string" || !record.sessionId.trim()) { + throw new Error("Agent chat cancel steer sessionId must be a non-empty string"); + } + return { sessionId: record.sessionId }; + }; + + const parseAgentChatEditSteerArgs = ( + value: unknown, + ): AgentChatEditSteerArgs => { + const record = requireRecord(value, "Agent chat edit steer request"); + if (typeof record.sessionId !== "string" || !record.sessionId.trim()) { + throw new Error("Agent chat edit steer sessionId must be a non-empty string"); + } + if (typeof record.text !== "string") { + throw new Error("Agent chat edit steer text must be a string"); + } + return { sessionId: record.sessionId, text: record.text }; + }; + ipcMain.handle(IPC.lanesOAuthGetStatus, async () => { const ctx = getCtx(); return ctx.oauthRedirectService?.getStatus() ?? { @@ -3740,14 +3763,14 @@ export function registerIpc({ await ctx.agentChatService.steer(arg); }); - ipcMain.handle(IPC.agentChatCancelSteer, async (_event, arg: AgentChatCancelSteerArgs): Promise => { + ipcMain.handle(IPC.agentChatCancelSteer, async (_event, arg: unknown): Promise => { const ctx = getCtx(); - await ctx.agentChatService.cancelSteer(arg); + await ctx.agentChatService.cancelSteer(parseAgentChatCancelSteerArgs(arg)); }); - ipcMain.handle(IPC.agentChatEditSteer, async (_event, arg: AgentChatEditSteerArgs): Promise => { + ipcMain.handle(IPC.agentChatEditSteer, async (_event, arg: unknown): Promise => { const ctx = getCtx(); - await ctx.agentChatService.editSteer(arg); + await ctx.agentChatService.editSteer(parseAgentChatEditSteerArgs(arg)); }); ipcMain.handle(IPC.agentChatInterrupt, async (_event, arg: AgentChatInterruptArgs): Promise => { diff --git a/apps/desktop/src/main/services/prs/prService.test.ts b/apps/desktop/src/main/services/prs/prService.test.ts index 842f7573b..a9dd82b06 100644 --- a/apps/desktop/src/main/services/prs/prService.test.ts +++ b/apps/desktop/src/main/services/prs/prService.test.ts @@ -285,7 +285,7 @@ describe("prService.createFromLane", () => { } // All subsequent GETs (fetchPr, checks, reviews, comments, files, actions) return { - data: args.path.includes("/pulls/99") && !args.path.includes("/") + data: args.path.endsWith("/pulls/99") ? { number: 99, html_url: "https://github.com/test-owner/test-repo/pull/99", diff --git a/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx b/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx index 9a9bd0870..f4fcd692c 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx @@ -611,7 +611,7 @@ describe("AgentChatMessageList transcript rendering", () => { expect(screen.getByTestId("location").textContent).toBe("/files::{\"laneId\":\"lane-123\"}"); }); - it("renders ask-user requests with an amber waiting spinner", () => { + it("renders ask-user requests with an amber waiting icon", () => { const view = renderMessageList([ { sessionId: "session-1", @@ -631,7 +631,8 @@ describe("AgentChatMessageList transcript rendering", () => { ]); expect(screen.getByText("Needs Input")).toBeTruthy(); - expect(view.container.querySelector(".animate-spin.text-amber-400")).toBeTruthy(); + expect(view.container.querySelector("svg.text-amber-400")).toBeTruthy(); + expect(view.container.querySelector(".animate-spin.text-amber-400")).toBeFalsy(); }); it("labels provider chats as Codex and preserves explicit assistant labels", () => { diff --git a/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx b/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx index f6acb7c19..489b2e06a 100644 --- a/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx +++ b/apps/desktop/src/renderer/components/chat/chatStatusVisuals.tsx @@ -1,4 +1,4 @@ -import { CheckCircle, SpinnerGap, XCircle } from "@phosphor-icons/react"; +import { CheckCircle, Clock, SpinnerGap, XCircle } from "@phosphor-icons/react"; import { cn } from "../ui/cn"; export type ChatStatusVisualState = "working" | "waiting" | "completed" | "failed"; @@ -39,7 +39,7 @@ export function ChatStatusGlyph({ case "failed": return ; case "waiting": - return ; + return ; case "working": return ; } diff --git a/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx b/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx index e2c4620bb..e8bd46faf 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx @@ -22,7 +22,7 @@ const tabTrigger = function statusDotCls(indicator: ReturnType): string { if (indicator === "running-active") return "border-2 border-emerald-500 border-t-transparent bg-transparent"; - if (indicator === "running-needs-attention") return "bg-amber-400"; + if (indicator === "running-needs-attention") return "bg-amber-300"; return "bg-red-500"; } diff --git a/apps/desktop/src/renderer/index.css b/apps/desktop/src/renderer/index.css index b88a79580..858cfd132 100644 --- a/apps/desktop/src/renderer/index.css +++ b/apps/desktop/src/renderer/index.css @@ -1789,7 +1789,8 @@ button:active, [role="button"]:active { .ade-glow-pulse-red, .ade-glow-pulse-amber, .ade-conflict-breathe, - .ade-float { + .ade-float, + .pull-btn-flash { animation: none !important; } /* Dropdown item stagger — instant appearance under reduced motion */ diff --git a/apps/desktop/src/renderer/lib/terminalAttention.test.ts b/apps/desktop/src/renderer/lib/terminalAttention.test.ts index 8f73d65c2..61092af3e 100644 --- a/apps/desktop/src/renderer/lib/terminalAttention.test.ts +++ b/apps/desktop/src/renderer/lib/terminalAttention.test.ts @@ -45,7 +45,7 @@ describe("terminalAttention", () => { it("returns a solid red dot for an ended session", () => { const dot = sessionStatusDot({ - status: "exited", + status: "completed", lastOutputPreview: "Process exited with code 0", }); expect(dot.spinning).toBe(false); diff --git a/apps/desktop/src/shared/types/chat.ts b/apps/desktop/src/shared/types/chat.ts index 6f3ce145d..aa8aad761 100644 --- a/apps/desktop/src/shared/types/chat.ts +++ b/apps/desktop/src/shared/types/chat.ts @@ -580,6 +580,15 @@ export type AgentChatSteerArgs = { text: string; }; +export type AgentChatCancelSteerArgs = { + sessionId: string; +}; + +export type AgentChatEditSteerArgs = { + sessionId: string; + text: string; +}; + export type AgentChatInterruptArgs = { sessionId: string; };