mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
fix(smart-sort): correct liveness check + freshness gate from review
- smart-sort.ts cold-start: switch from `tab.ptyId` (wake-hint) to `tabHasLivePty(ptyIdsByTabId, tab.id)` so slept tabs after restart don't skip the persisted-sortOrder fallback. - smart-attention.ts: only suppress the title-heuristic fallback for panes whose hook entry is fresh, so stale hooks no longer hide a live `permission` title behind a dead entry. - WorktreeList.tsx: drop dead `prCache` subscription for smart mode (the new comparator doesn't read it) and refresh the now-stale cold-start comment. - smart-sort.test.ts: pin the slept-tab cold-start path; correct a misleading comment about a not-actually-dropped heuristic. - docs: align telemetry property names with the shipped schema and drop stale `WorktreeJumpPalette.tsx` line-number references. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -563,10 +563,10 @@ const WorktreeList = React.memo(function WorktreeList() {
|
||||
|
||||
const cardProps = useAppStore((s) => s.worktreeCardProperties)
|
||||
|
||||
// PR cache is needed for PR-status grouping, smart sorting, and when the
|
||||
// PR card property is visible.
|
||||
// PR cache is needed for PR-status grouping and when the PR card property
|
||||
// is visible.
|
||||
const prCache = useAppStore((s) =>
|
||||
groupBy === 'pr-status' || sortBy === 'smart' || cardProps.includes('pr') ? s.prCache : null
|
||||
groupBy === 'pr-status' || cardProps.includes('pr') ? s.prCache : null
|
||||
)
|
||||
|
||||
const sortEpoch = useAppStore((s) => s.sortEpoch)
|
||||
@@ -645,13 +645,14 @@ const WorktreeList = React.memo(function WorktreeList() {
|
||||
(worktree) => !worktree.isArchived
|
||||
)
|
||||
|
||||
// Why cold-start detection: the smart score is dominated by ephemeral
|
||||
// signals (running jobs +60, live terminals +12, needs attention +35)
|
||||
// that vanish after restart. Recomputing the smart score on cold start
|
||||
// produces a shuffled ordering because those signals are gone while
|
||||
// persistent ones (unread, linked PR) survive — changing relative ranks.
|
||||
// Instead, restore the pre-shutdown order from the persisted sortOrder
|
||||
// snapshot, and switch to the live smart score once PTYs start spawning.
|
||||
// Why cold-start detection: smart-class resolution depends on the
|
||||
// agent-status snapshot (agentStatusByPaneKey) hydrating from the hook
|
||||
// server, which lands asynchronously after launch. Running the warm
|
||||
// comparator before that arrives would collapse every worktree to Class 4
|
||||
// and shuffle the sidebar against the comparator's tiebreakers. Restore
|
||||
// the pre-shutdown order from the persisted sortOrder snapshot until any
|
||||
// live PTY appears, then switch to the live class layer. See Edge case 8
|
||||
// in docs/smart-worktree-order-redesign.md.
|
||||
if (sortBy === 'smart' && !sessionHasHadPty.current) {
|
||||
// Why: `tabHasLivePty` (over `ptyIdsByTabId`) is the source of truth for
|
||||
// liveness — slept terminals retain `tab.ptyId` as a wake hint, so reading
|
||||
|
||||
@@ -268,6 +268,13 @@ export function buildAttentionByWorktree(
|
||||
if (hookEntries) {
|
||||
for (const entry of hookEntries) {
|
||||
panes.push({ kind: 'hook', entry })
|
||||
// Why: only fresh hook entries should suppress the title-heuristic
|
||||
// fallback for their pane. A stale hook is filtered out by
|
||||
// resolveAttention; if we marked its pane as "hook-covered" we'd hide
|
||||
// the live title behind a dead entry and drop the worktree to Class 4.
|
||||
if (!isExplicitAgentStatusFresh(entry, now, AGENT_STATUS_STALE_AFTER_MS)) {
|
||||
continue
|
||||
}
|
||||
const paneId = paneIdFromPaneKey(entry.paneKey)
|
||||
if (paneId !== null) {
|
||||
hookPaneIds.add(paneId)
|
||||
|
||||
@@ -451,6 +451,20 @@ describe('sortWorktreesSmart — cold start fallback', () => {
|
||||
expect(sorted.map((w) => w.id)).toEqual(['b', 'a'])
|
||||
})
|
||||
|
||||
it('treats slept tabs (tab.ptyId without live entry) as cold start', () => {
|
||||
// Why: tab.ptyId is the wake-hint sessionId preserved under sleep — not a
|
||||
// liveness signal. With slept tabs but no live PTYs, sortWorktreesSmart
|
||||
// must fall back to persisted sortOrder.
|
||||
const a = makeWorktree({ id: 'a', sortOrder: 1, displayName: 'a' })
|
||||
const b = makeWorktree({ id: 'b', sortOrder: 2, displayName: 'b' })
|
||||
const tabsByWorktree = {
|
||||
[a.id]: [makeTab({ id: 'ta', worktreeId: a.id, ptyId: 'wake-hint' })]
|
||||
}
|
||||
// ptyIdsByTabId is empty — slept tab has wake-hint ptyId but no live entry.
|
||||
const sorted = sortWorktreesSmart([a, b], tabsByWorktree, repoMap, {}, {}, {})
|
||||
expect(sorted.map((w) => w.id)).toEqual(['b', 'a'])
|
||||
})
|
||||
|
||||
it('uses the smart comparator once a PTY is alive', () => {
|
||||
const blocked = makeWorktree({ id: 'blocked', displayName: 'Blocked', sortOrder: 0 })
|
||||
const done = makeWorktree({ id: 'done', displayName: 'Done', sortOrder: 100 })
|
||||
@@ -487,8 +501,8 @@ describe('sortWorktreesSmart — cold start fallback', () => {
|
||||
|
||||
describe('sortWorktreesSmart — palette caller regression', () => {
|
||||
// Why: WorktreeJumpPalette routes typed queries through sortWorktreesSmart.
|
||||
// This test pins that the palette path uses the class layer (not the
|
||||
// dropped terminal-title heuristic) when threading agentStatusByPaneKey.
|
||||
// This test pins that the palette path uses the class layer (not just the
|
||||
// recent-activity fallback) when threading agentStatusByPaneKey.
|
||||
it('palette ranks blocked above working when both flow through sortWorktreesSmart', () => {
|
||||
const blocked = makeWorktree({ id: 'blocked', displayName: 'A-Blocked' })
|
||||
const working = makeWorktree({ id: 'working', displayName: 'B-Working' })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Worktree, Repo, TerminalTab } from '../../../../shared/types'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
|
||||
import { IDLE, buildAttentionByWorktree, type WorktreeAttention } from './smart-attention'
|
||||
|
||||
type SortBy = 'name' | 'smart' | 'recent' | 'repo'
|
||||
@@ -122,9 +123,12 @@ export function sortWorktreesSmart(
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string>>,
|
||||
ptyIdsByTabId: Record<string, string[]>
|
||||
): Worktree[] {
|
||||
// Why: `tabHasLivePty` (over `ptyIdsByTabId`) is the source of truth for
|
||||
// liveness — slept terminals retain `tab.ptyId` as a wake hint, so reading
|
||||
// it directly would falsely keep cold-start ordering off after restart.
|
||||
const hasAnyLivePty = Object.values(tabsByWorktree)
|
||||
.flat()
|
||||
.some((t) => t.ptyId)
|
||||
.some((tab) => tabHasLivePty(ptyIdsByTabId, tab.id))
|
||||
|
||||
if (!hasAnyLivePty) {
|
||||
// Cold start: use persisted sortOrder snapshot until the agent-status
|
||||
|
||||
Reference in New Issue
Block a user