From c723f1b744dcd072c6cbdd0295d7f6f9725ea0f9 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Tue, 5 May 2026 15:31:16 -0700 Subject: [PATCH] fix(sidebar): bound create-grace floor to its window; stamp createdAt on CLI-created worktrees Two follow-ups to the Recent-sort grace-window fix: 1. effectiveRecentActivity bounded to the grace window. Previously it returned max(lastActivityAt, createdAt + CREATE_GRACE_MS) regardless of `now`, so a worktree created days ago and never touched would keep ranking as `createdAt + 5min` forever, masking genuinely fresher worktrees. Now the floor only applies while `now < createdAt + CREATE_GRACE_MS`; afterwards it's plain lastActivityAt. Comparator threads `now` into both call sites. 2. createManagedWorktree (CLI/runtime path) now stamps createdAt alongside lastActivityAt, matching createLocalWorktree / createRemoteWorktree. Without this, a worktree created via the CLI missed the grace window entirely and could be outranked by ambient PTY bumps on other worktrees in the seconds after creation. Co-authored-by: Orca --- src/main/runtime/orca-runtime.test.ts | 47 +++++++++++++++++++ src/main/runtime/orca-runtime.ts | 8 +++- .../src/components/sidebar/smart-sort.test.ts | 18 +++++-- .../src/components/sidebar/smart-sort.ts | 22 ++++++--- 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 3fb54c5a637..015101514ba 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -1062,6 +1062,53 @@ describe('OrcaRuntimeService', () => { expect(activateWorktree).toHaveBeenCalledWith('repo-1', expect.any(String), undefined) }) + it('stamps createdAt alongside lastActivityAt so CLI-created worktrees get the Recent-sort grace window', async () => { + // Why: parity with createLocalWorktree / createRemoteWorktree. Without + // createdAt, ambient PTY bumps in OTHER worktrees during the few seconds + // after creation can push the new worktree below them in Recent sort. + const runtime = new OrcaRuntimeService(store) + runtime.setNotifier({ + worktreesChanged: vi.fn(), + reposChanged: vi.fn(), + activateWorktree: vi.fn(), + createTerminal: vi.fn(), + splitTerminal: vi.fn(), + renameTerminal: vi.fn(), + focusTerminal: vi.fn(), + closeTerminal: vi.fn(), + sleepWorktree: vi.fn(), + terminalFitOverrideChanged: vi.fn() + }) + runtime.attachWindow(1) + + computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-grace') + ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-grace') + vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: {} }) + vi.mocked(listWorktrees).mockResolvedValueOnce([ + { + path: '/tmp/workspaces/runtime-grace', + head: 'def', + branch: 'runtime-grace', + isBare: false, + isMainWorktree: false + } + ]) + + const before = Date.now() + const result = await runtime.createManagedWorktree({ + repoSelector: 'id:repo-1', + name: 'runtime-grace' + }) + const after = Date.now() + + expect(result.worktree.createdAt).toBeDefined() + expect(result.worktree.createdAt).toBeGreaterThanOrEqual(before) + expect(result.worktree.createdAt).toBeLessThanOrEqual(after) + // Both fields must be stamped from the same `now` so the grace-window + // math (max(lastActivityAt, createdAt + GRACE_MS)) is well-defined. + expect(result.worktree.createdAt).toBe(result.worktree.lastActivityAt) + }) + it('skips archive hooks for CLI worktree removal by default', async () => { const runtime = new OrcaRuntimeService(store) vi.mocked(getEffectiveHooks).mockReturnValue({ diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index be4210e28f7..c912f239158 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -2099,8 +2099,14 @@ export class OrcaRuntimeService { } const worktreeId = `${repo.id}::${created.path}` + const now = Date.now() const meta = this.store.setWorktreeMeta(worktreeId, { - lastActivityAt: Date.now(), + lastActivityAt: now, + // See createRemoteWorktree: createdAt grants the new worktree a grace + // window in Recent sort so ambient PTY bumps in OTHER worktrees can't + // push it down before the user has had a chance to notice it. Smart-sort + // uses max(lastActivityAt, createdAt + CREATE_GRACE_MS). + createdAt: now, ...(shouldSetDisplayName(requestedName, branchName, sanitizedName) ? { displayName: requestedName } : {}), diff --git a/src/renderer/src/components/sidebar/smart-sort.test.ts b/src/renderer/src/components/sidebar/smart-sort.test.ts index 91a3a3cd511..b56f9c2c0db 100644 --- a/src/renderer/src/components/sidebar/smart-sort.test.ts +++ b/src/renderer/src/components/sidebar/smart-sort.test.ts @@ -522,12 +522,12 @@ describe('buildWorktreeComparator — recent (lastActivityAt)', () => { describe('effectiveRecentActivity — create-grace floor', () => { it('returns lastActivityAt when createdAt is absent', () => { const wt = makeWorktree({ id: 'old', lastActivityAt: 12345 }) - expect(effectiveRecentActivity(wt)).toBe(12345) + expect(effectiveRecentActivity(wt, NOW)).toBe(12345) }) it('returns createdAt + CREATE_GRACE_MS when grace window exceeds lastActivityAt', () => { const wt = makeWorktree({ id: 'fresh', lastActivityAt: NOW, createdAt: NOW }) - expect(effectiveRecentActivity(wt)).toBe(NOW + CREATE_GRACE_MS) + expect(effectiveRecentActivity(wt, NOW)).toBe(NOW + CREATE_GRACE_MS) }) it('returns lastActivityAt when grace window has elapsed', () => { @@ -536,7 +536,7 @@ describe('effectiveRecentActivity — create-grace floor', () => { createdAt: NOW - CREATE_GRACE_MS - 60_000, lastActivityAt: NOW - 1000 }) - expect(effectiveRecentActivity(wt)).toBe(NOW - 1000) + expect(effectiveRecentActivity(wt, NOW)).toBe(NOW - 1000) }) it('returns lastActivityAt when real activity has surpassed the grace floor', () => { @@ -545,7 +545,17 @@ describe('effectiveRecentActivity — create-grace floor', () => { const createdAt = NOW - 3 * 60 * 1000 const wt = makeWorktree({ id: 'used', createdAt, lastActivityAt: NOW - 60_000 }) // createdAt + GRACE_MS = NOW + 2min, which exceeds lastActivityAt (NOW - 1min). - expect(effectiveRecentActivity(wt)).toBe(createdAt + CREATE_GRACE_MS) + expect(effectiveRecentActivity(wt, NOW)).toBe(createdAt + CREATE_GRACE_MS) + }) + + it('returns lastActivityAt once the grace window has elapsed even when no other activity has occurred', () => { + // Bug-fix case: a worktree created days ago that was never touched after + // creation. Without the time-bound check, the floor would still apply and + // the worktree would rank as `createdAt + 5min` forever, masking truly + // fresher worktrees. + const createdAt = NOW - CREATE_GRACE_MS - 1 + const wt = makeWorktree({ id: 'untouched', createdAt, lastActivityAt: createdAt }) + expect(effectiveRecentActivity(wt, NOW)).toBe(createdAt) }) }) diff --git a/src/renderer/src/components/sidebar/smart-sort.ts b/src/renderer/src/components/sidebar/smart-sort.ts index 1c310759941..54867cef86c 100644 --- a/src/renderer/src/components/sidebar/smart-sort.ts +++ b/src/renderer/src/components/sidebar/smart-sort.ts @@ -21,13 +21,20 @@ export const CREATE_GRACE_MS = 5 * 60 * 1000 /** * Rank a worktree in Recent sort using `lastActivityAt`, but with a floor of - * `createdAt + CREATE_GRACE_MS` while that window is active. Returns - * `lastActivityAt` unchanged for worktrees without `createdAt` (discovered on - * disk, or persisted before this field existed). + * `createdAt + CREATE_GRACE_MS` *only during* the grace window (i.e. while + * `now < createdAt + CREATE_GRACE_MS`). Once the window has elapsed, returns + * `lastActivityAt` unchanged. Returns `lastActivityAt` unchanged for worktrees + * without `createdAt` (discovered on disk, or persisted before this field + * existed). */ -export function effectiveRecentActivity(worktree: Worktree): number { +export function effectiveRecentActivity(worktree: Worktree, now: number): number { const { lastActivityAt, createdAt } = worktree - if (createdAt === undefined) { + // Why bound by now: a worktree with createdAt set but no subsequent activity + // should not retain artificially-high recency forever; the floor exists to + // absorb the noisy creation window only. Without this bound, a worktree + // created days ago and never touched would keep ranking as if its activity + // were `createdAt + 5min`, masking truly fresher worktrees indefinitely. + if (createdAt === undefined || now >= createdAt + CREATE_GRACE_MS) { return lastActivityAt } return Math.max(lastActivityAt, createdAt + CREATE_GRACE_MS) @@ -301,7 +308,8 @@ export function buildWorktreeComparator( ) return ( scoreB - scoreA || - effectiveRecentActivity(smartB.worktree) - effectiveRecentActivity(smartA.worktree) || + effectiveRecentActivity(smartB.worktree, now) - + effectiveRecentActivity(smartA.worktree, now) || a.displayName.localeCompare(b.displayName) ) } @@ -318,7 +326,7 @@ export function buildWorktreeComparator( // signal — bumped by bumpWorktreeActivity (PTY spawn, background // events) and by meaningful meta edits (comment, isUnread). return ( - effectiveRecentActivity(b) - effectiveRecentActivity(a) || + effectiveRecentActivity(b, now) - effectiveRecentActivity(a, now) || a.displayName.localeCompare(b.displayName) ) case 'repo': {