mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
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 <help@stably.ai>
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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': {
|
||||
|
||||
Reference in New Issue
Block a user