Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .agents/skills/ade-perf-lanes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Performance practices for ADE's Lanes tab. Read before editing
measured UI audit proves a better one.
metadata:
author: ade-autoresearch
version: 0.2.0
version: 0.2.2
status: active
---

Expand Down Expand Up @@ -89,3 +89,27 @@ Use this as engineering guidance for keeping the Lanes tab fast while adding fea
- **Apply when**: New lane metadata or appearance handlers update color/name/description without changing branch state.
- **Avoid**: Bare `refreshLanes()` after appearance-only updates.
- **Verification**: Real UI manage-dialog trace `lanes-refresh-light-20260511` showed `ade.lanes.updateAppearance` at 1 ms followed by an unnecessary `ade.lanes.listSnapshots` at 308 ms. The lightweight path uses `refreshLanes({ includeStatus: false })` instead.

### Gate Stack Graph agent rosters behind visibility
- **Why it helped**: Lanes loaded per-lane agent rosters even while Stack Graph was closed. With 30 lanes, initial `/lanes` load fanned out `agentChat.list({ laneId })` and `sessions.list({ laneId })` for every lane before the user opened the graph.
- **Apply when**: A closed Lanes surface computes per-lane chat/session/agent data that is only rendered inside Stack Graph.
- **Avoid**: Fetching every lane's agent roster on page load to keep a closed dropdown warm.
- **Verification**: `lanes-20260531-1421-baseline` Lanes nav spent 87 `ade.localRuntime.callAction` calls / 25.3 s total IPC time. After gating rosters until Stack Graph opens, `lanes-20260531-1421-after1` dropped Lanes nav to 28 `callAction` calls / 13.3 s. Stack Graph then paid the roster cost on demand: 63 calls / 450 ms in the open segment.

### Filter chat session lists before applying caps
- **Why it helped**: `agentChat.listSessions()` loaded the newest 500 terminal sessions and then filtered to chat rows. Many newer CLI or shell sessions could push older chats out of the capped result, making chat panes look empty or stale even though persisted chats existed.
- **Apply when**: A session list is intended to show a specific tool family, provider, or surface and the underlying table also stores high-volume shell/run-owned sessions.
- **Avoid**: Applying a global `limit` before the meaningful filter, or widening limits as a substitute for the right query.
- **Verification**: `fix(chat): filter chat session lists before caps` adds `toolTypes` to `sessionService.list`, uses it from `agentChatService.listSessions`, preserves legacy chat rows inferred from resume commands, and covers a regression with 505 newer shell sessions hiding an older chat.

### Refresh visible linked PRs opportunistically
- **Why it helped**: Lanes PR badges and attached-PR status depended on the broad PR poller, so mergeability/check/review state could sit stale for about a minute after opening or switching lanes.
- **Apply when**: A Lanes surface needs attached PR status for lanes already visible in the grid. Refresh only linked, stale PR ids for visible lanes, dedupe them, cap the batch, and track recent request timestamps so scrolling or layout churn cannot stampede GitHub.
- **Avoid**: Kicking the global PR poller, refreshing GitHub-only PR rows without lane links, or widening the full PR refresh cadence to make one visible grid feel fresher.
- **Verification**: `perf(lanes): refresh visible PR status opportunistically` refreshes at most 4 stale visible linked PRs after a 260 ms debounce with a 15 s stale/request gate, merges returned summaries into Lanes tags, and covers selection/dedupe/cap behavior in `LanesPage.test.ts`.

### Pause minimized and delayed Git Actions effects
- **Why it helped**: Minimized pane bodies were CSS-hidden but still mounted, so Git Actions kept diff/stash/sync/conflict loads, auto-rebase status, sync-status polling, event subscriptions, and commit-history requests alive in the background. Multiple visible lanes could also mount Git Actions panes at once.
- **Apply when**: A pane body owns timers, subscriptions, local Git reads, PR/AI/runtime status, or history loads. Pass pane minimized state into the render path, make child effects explicitly inactive while minimized, and stagger non-primary visible pane mounts so only the immediate lane warms eagerly.
- **Avoid**: Treating visual collapse as inactive, or adding a single page-level throttle while hidden pane components keep their own timers running.
- **Verification**: `perf(lanes): pause minimized git actions panes` adds `PaneConfig.renderChildren`, passes `active={!minimized}` to `LaneGitActionsPane` and `CommitTimeline`, staggers inline Git Actions bodies by visible-lane order, and covers inactive/active transitions in component tests. A real Electron `/lanes` segment, `lanes-minimized-git-idle` in `lanes-20260531-1421-throttles-after3`, kept the Git Actions pane minimized for 33.4 s with no slow Git Actions status/history IPC; main/browser p95 CPU was 0.15% and renderer tab p95 was 0.05%.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ ADE_DEV_RUNTIME_SOCKET_PATH=/tmp/my-ade-dev.sock npm run dev:runtime
ADE_DESKTOP_BRIDGE_SOCKET_PATH=/tmp/my-bridge.sock npm run dev:desktop
```

When launching ADE desktop dev through ADE App Control from a running Alpha/Beta
ADE window, use an absolute lane cwd and clear packaged-channel environment
variables inherited from the host app. Otherwise the dev Electron app can reuse
the Alpha/Beta profile and lose the single-instance lock instead of opening the
lane build:

```bash
ade --socket app-control launch --force \
--cwd "/path/to/ADE/.ade/worktrees/<lane>" \
--command "sh -lc 'ADE_PACKAGE_CHANNEL= ADE_DESKTOP_APP_NAME= ADE_DESKTOP_BRIDGE_SOCKET_PATH=/tmp/ade-desktop-bridge-<lane>.sock npm run dev:desktop -- --socket /tmp/ade-runtime-<lane>.sock'" \
--text
```

To test auto-runtime creation, use the default dev commands after stopping the dev runtime:

```bash
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6055,6 +6055,11 @@ app.whenReady().then(async () => {
}
return ctx;
},
getResourceUsageContexts: () => {
const contexts = new Set<AppContext>(projectContexts.values());
contexts.add(getActiveContext());
return Array.from(contexts);
},
getSyncService: () => {
return getMobileSyncService();
},
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/src/main/services/chat/agentChatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,9 @@ function createMockSessionService() {
if (typeof opts?.status === "string") {
rows = rows.filter((row) => row.status === opts.status);
}
if (Array.isArray(opts?.toolTypes) && opts.toolTypes.length > 0) {
rows = rows.filter((row) => opts.toolTypes.includes(row.toolType));
}
rows = rows.sort((a, b) => String(b.startedAt ?? "").localeCompare(String(a.startedAt ?? "")));
if (opts?.limit === null) return rows;
const limit = typeof opts?.limit === "number" ? opts.limit : 200;
Expand Down Expand Up @@ -3697,6 +3700,34 @@ describe("createAgentChatService", () => {
expect(sessions[0]!.provider).toBe("opencode");
});

it("lists chat sessions even when newer shell sessions exceed the terminal list cap", async () => {
const { service, sessionService } = createService();

const chat = await service.createSession({
laneId: "lane-1",
provider: "codex",
model: "gpt-5-codex",
});

for (let i = 0; i < 505; i++) {
sessionService.create({
sessionId: `shell-session-${i}`,
laneId: "lane-1",
toolType: "shell",
title: `Shell ${i}`,
startedAt: new Date(Date.UTC(2026, 2, 17, 0, 10, i)).toISOString(),
});
}

const sessions = await service.listSessions("lane-1");
expect(sessions.map((session) => session.sessionId)).toContain(chat.id);
expect(sessionService.list).toHaveBeenLastCalledWith(expect.objectContaining({
laneId: "lane-1",
limit: 500,
toolTypes: expect.arrayContaining(["codex-chat", "claude-chat", "opencode-chat", "cursor", "droid-chat"]),
}));
});

it("excludes identity sessions by default", async () => {
const { service } = createService();

Expand Down
25 changes: 16 additions & 9 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2548,16 +2548,19 @@ function describeCodexModel(value: string): string | null {
return null;
}

const CHAT_SESSION_TOOL_TYPES = [
"codex-chat",
"claude-chat",
"opencode-chat",
"cursor",
"droid-chat",
] satisfies TerminalToolType[];
type ChatSessionToolType = (typeof CHAT_SESSION_TOOL_TYPES)[number];

function isChatToolType(
toolType: TerminalToolType | null | undefined,
): toolType is "codex-chat" | "claude-chat" | "opencode-chat" | "cursor" | "droid-chat" {
return (
toolType === "codex-chat"
|| toolType === "claude-chat"
|| toolType === "opencode-chat"
|| toolType === "cursor"
|| toolType === "droid-chat"
);
): toolType is ChatSessionToolType {
return toolType != null && CHAT_SESSION_TOOL_TYPES.includes(toolType as ChatSessionToolType);
}

function providerFromToolType(toolType: TerminalToolType | null | undefined): AgentChatProvider {
Expand Down Expand Up @@ -21785,7 +21788,11 @@ export function createAgentChatService(args: {
laneId?: string,
options?: { includeIdentity?: boolean; includeAutomation?: boolean; includeArchived?: boolean },
): Promise<AgentChatSessionSummary[]> => {
const rows = sessionService.list({ ...(laneId ? { laneId } : {}), limit: 500 });
const rows = sessionService.list({
...(laneId ? { laneId } : {}),
limit: 500,
toolTypes: CHAT_SESSION_TOOL_TYPES,
});
const chatRows = rows.filter((row) => isChatToolType(row.toolType));
const includeIdentity = options?.includeIdentity === true;
const includeAutomation = options?.includeAutomation === true;
Expand Down
Loading
Loading