Merge remote-tracking branch 'origin/main' into brennanb2025/multi-window-monitor-support

This commit is contained in:
brennanb2025
2026-06-13 13:15:15 -07:00
4 changed files with 99 additions and 52 deletions
+75 -50
View File
@@ -97,6 +97,7 @@ import { LINEAR_WRITE_BODY_CAP } from '../../shared/linear-agent-access'
import type { FeatureInteractionId } from '../../shared/feature-interactions'
import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry'
import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, splitWorktreeId } from '../../shared/worktree-id'
import { parsePtySessionId } from '../../shared/pty-session-id-format'
import { clampLinearIssueListLimit } from '../../shared/linear-issue-read-limits'
import { isFolderRepo } from '../../shared/repo-kind'
import { DEFAULT_WORKSPACE_STATUS_ID } from '../../shared/workspace-statuses'
@@ -14912,7 +14913,12 @@ export class OrcaRuntimeService {
// handler, works even for daemon terminals), and (2) the PTY foreground process
// + output quiescence. The poll self-cancels when the primary OSC path fires.
private startTuiIdleFallbackPoll(waiter: TerminalWaiter, leaf: RuntimeLeafRecord): void {
let foregroundPollInFlight = false
waiter.pollInterval = setInterval(async () => {
if (!waiter.pollInterval) {
return
}
let startedForegroundPoll = false
try {
if (leaf.lastAgentStatus === 'idle') {
if (waiter.pollInterval) {
@@ -14963,7 +14969,14 @@ export class OrcaRuntimeService {
}
// Foreground process fallback: if the daemon/local provider can report
// the process and it's a non-shell with quiet output, treat as idle.
if (leaf.lastAgentStatus === null && leaf.ptyId && this.ptyController) {
if (
leaf.lastAgentStatus === null &&
leaf.ptyId &&
this.ptyController &&
!foregroundPollInFlight
) {
foregroundPollInFlight = true
startedForegroundPoll = true
const fg = await this.ptyController.getForegroundProcess(leaf.ptyId)
if (fg && !isShellProcess(fg)) {
const quietMs = leaf.lastOutputAt ? Date.now() - leaf.lastOutputAt : 0
@@ -14978,12 +14991,21 @@ export class OrcaRuntimeService {
}
} catch {
// Swallow transient PTY inspection errors and keep polling.
} finally {
if (startedForegroundPoll) {
foregroundPollInFlight = false
}
}
}, TUI_IDLE_POLL_INTERVAL_MS)
}
private startPtyTuiIdleFallbackPoll(waiter: TerminalWaiter, pty: RuntimePtyWorktreeRecord): void {
let foregroundPollInFlight = false
waiter.pollInterval = setInterval(async () => {
if (!waiter.pollInterval) {
return
}
let startedForegroundPoll = false
try {
if (pty.lastAgentStatus === 'idle') {
if (waiter.pollInterval) {
@@ -15019,7 +15041,9 @@ export class OrcaRuntimeService {
this.resolveWaiter(waiter, buildPtyTerminalWaitResult(waiter.handle, 'tui-idle', pty))
return
}
if (pty.lastAgentStatus === null && this.ptyController) {
if (pty.lastAgentStatus === null && this.ptyController && !foregroundPollInFlight) {
foregroundPollInFlight = true
startedForegroundPoll = true
const fg = await this.ptyController.getForegroundProcess(pty.ptyId)
if (fg && !isShellProcess(fg)) {
const quietMs = pty.lastOutputAt ? Date.now() - pty.lastOutputAt : 0
@@ -15034,6 +15058,10 @@ export class OrcaRuntimeService {
}
} catch {
// Swallow transient PTY inspection errors and keep polling.
} finally {
if (startedForegroundPoll) {
foregroundPollInFlight = false
}
}
}, TUI_IDLE_POLL_INTERVAL_MS)
}
@@ -17034,18 +17062,13 @@ function withTimeoutResult<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<{ ok: true; value: T } | { ok: false }> {
let timeout: ReturnType<typeof setTimeout> | null = null
return new Promise<{ ok: true; value: T } | { ok: false }>((resolve) => {
timeout = setTimeout(() => resolve({ ok: false }), timeoutMs)
promise.then(
(value) => resolve({ ok: true, value }),
() => resolve({ ok: false })
)
}).finally(() => {
if (timeout) {
clearTimeout(timeout)
return withTimeout(
promise.then((value) => ({ ok: true, value }) as const),
timeoutMs,
{
ok: false
}
})
)
}
export function appendRecentPtyOutput(previous: string | undefined, data: string): string {
@@ -17669,13 +17692,7 @@ function buildTerminalWaitResult(
condition: RuntimeTerminalWaitCondition,
leaf: RuntimeLeafRecord
): RuntimeTerminalWait {
return {
handle,
condition,
satisfied: true,
status: getTerminalState(leaf),
exitCode: leaf.lastExitCode
}
return buildTerminalWait(handle, condition, getTerminalState(leaf), leaf.lastExitCode)
}
function buildTerminalWaitBlockedResult(
@@ -17684,14 +17701,13 @@ function buildTerminalWaitBlockedResult(
leaf: RuntimeLeafRecord,
blockedReason: RuntimeTerminalWaitBlockedReason
): RuntimeTerminalWait {
return {
return buildTerminalWait(
handle,
condition,
satisfied: false,
status: getTerminalState(leaf),
exitCode: leaf.lastExitCode,
getTerminalState(leaf),
leaf.lastExitCode,
blockedReason
}
)
}
function buildPtyTerminalWaitResult(
@@ -17699,13 +17715,7 @@ function buildPtyTerminalWaitResult(
condition: RuntimeTerminalWaitCondition,
pty: RuntimePtyWorktreeRecord
): RuntimeTerminalWait {
return {
handle,
condition,
satisfied: true,
status: pty.connected ? 'running' : pty.lastExitCode !== null ? 'exited' : 'unknown',
exitCode: pty.lastExitCode
}
return buildTerminalWait(handle, condition, getPtyTerminalState(pty), pty.lastExitCode)
}
function buildPtyTerminalWaitBlockedResult(
@@ -17713,17 +17723,37 @@ function buildPtyTerminalWaitBlockedResult(
condition: RuntimeTerminalWaitCondition,
pty: RuntimePtyWorktreeRecord,
blockedReason: RuntimeTerminalWaitBlockedReason
): RuntimeTerminalWait {
return buildTerminalWait(
handle,
condition,
getPtyTerminalState(pty),
pty.lastExitCode,
blockedReason
)
}
function buildTerminalWait(
handle: string,
condition: RuntimeTerminalWaitCondition,
status: RuntimeTerminalState,
exitCode: number | null,
blockedReason?: RuntimeTerminalWaitBlockedReason
): RuntimeTerminalWait {
return {
handle,
condition,
satisfied: false,
status: pty.connected ? 'running' : pty.lastExitCode !== null ? 'exited' : 'unknown',
exitCode: pty.lastExitCode,
blockedReason
satisfied: blockedReason === undefined,
status,
exitCode,
...(blockedReason ? { blockedReason } : {})
}
}
function getPtyTerminalState(pty: RuntimePtyWorktreeRecord): RuntimeTerminalState {
return pty.connected ? 'running' : pty.lastExitCode !== null ? 'exited' : 'unknown'
}
function branchSelectorMatches(branch: string, selector: string): boolean {
// Why: Git worktree data can report local branches as either `refs/heads/foo`
// or `foo` depending on which plumbing path produced the record. Orca's
@@ -17741,12 +17771,7 @@ function normalizeBranchRef(branch: string): string {
}
function inferWorktreeIdFromPtyId(ptyId: string): string | null {
const separatorIndex = ptyId.lastIndexOf('@@')
if (separatorIndex <= 0) {
return null
}
const worktreeId = ptyId.slice(0, separatorIndex)
return parseRuntimeWorktreeId(worktreeId) ? worktreeId : null
return parsePtySessionId(ptyId).worktreeId
}
function parseRuntimeWorktreeId(
@@ -17791,13 +17816,7 @@ function getLeafWorktreeStatus(
]
const latestTitle = getLatestAgentCandidateTitle(...titleCandidates)
const detected = latestTitle ? detectAgentStatusFromTitle(latestTitle) : leaf.lastAgentStatus
if (detected === 'permission') {
return 'permission'
}
if (detected === 'working') {
return 'working'
}
return leaf.ptyId ? 'active' : 'inactive'
return getDetectedWorktreeStatus(detected, leaf.ptyId !== null)
}
function classifyLatestAgentTitle(
@@ -17855,7 +17874,13 @@ function getLatestAgentCandidateTitle(
}
function getSavedTabWorktreeStatus(title: string, hasPty: boolean): RuntimeWorktreeStatus {
const detected = detectAgentStatusFromTitle(title)
return getDetectedWorktreeStatus(detectAgentStatusFromTitle(title), hasPty)
}
function getDetectedWorktreeStatus(
detected: AgentStatus | null,
hasPty: boolean
): RuntimeWorktreeStatus {
if (detected === 'permission') {
return 'permission'
}
@@ -61,6 +61,7 @@ import {
} from './workspace-delete-quick-action'
import { DetachedHeadBadge } from '@/components/DetachedHeadBadge'
import { getWorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display'
import { getFlushWorktreeCardPaddingLeft } from './worktree-list-indentation'
import { translate } from '@/i18n/i18n'
import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope'
@@ -793,8 +794,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
// aligned with the pre-inset layout and the repo header hierarchy.
const cardStyle = flushSurface
? {
paddingLeft:
contentIndent > 0 ? `max(0.125rem, calc(${contentIndent}px - 0.625rem))` : '0.125rem'
paddingLeft: getFlushWorktreeCardPaddingLeft(contentIndent)
}
: contentIndent > 0
? { paddingLeft: `calc(0.125rem + ${contentIndent}px)` }
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
WORKTREE_SECTION_HEADER_PADDING_LEFT,
getFlushWorktreeCardPaddingLeft,
getProjectGroupHeaderPaddingLeft,
getWorktreeCardContentIndent,
getWorktreeCardSurfaceInset
@@ -50,4 +51,12 @@ describe('worktree list indentation', () => {
it('does not inset card surfaces outside grouped views', () => {
expect(getWorktreeCardSurfaceInset({ isGrouped: false, groupDepth: 4 })).toBe(0)
})
it('pulls flush card content back by the tuned inset gap', () => {
expect(getFlushWorktreeCardPaddingLeft(20)).toBe('max(2px, calc(20px - 4px))')
})
it('keeps flush card content off the sidebar edge without indentation', () => {
expect(getFlushWorktreeCardPaddingLeft(0)).toBe('2px')
})
})
@@ -2,6 +2,13 @@ export const SIDEBAR_TREE_INDENT = 18
// Why: project-grouped cards need to read as children even after the card
// surface inset is subtracted, while lineage rows keep the base tree step.
const PROJECT_WORKTREE_CARD_EXTRA_INDENT = 2
// Why: flush cards span the full row, so their content is pulled back from the
// raw tree indent to sit under the group header. A smaller pullback nudges
// content rightward for clearer nesting; this is the knob to tune that gap.
export const FLUSH_CARD_CONTENT_PULLBACK = 4
// Why: even at zero indent a flush card keeps this minimal left inset so its
// surface never sits hard against the sidebar edge.
export const FLUSH_CARD_MIN_CONTENT_INSET = 2
// Why: grouped workspace cards should move their surface inward without using
// the full tree step, preserving the existing compact child-card rhythm.
const GROUPED_WORKTREE_CARD_SURFACE_INDENT = 14
@@ -39,3 +46,9 @@ export function getWorktreeCardSurfaceInset(args: {
}): number {
return args.isGrouped ? clampDepth(args.groupDepth) * GROUPED_WORKTREE_CARD_SURFACE_INDENT : 0
}
export function getFlushWorktreeCardPaddingLeft(contentIndent: number): string {
return contentIndent > 0
? `max(${FLUSH_CARD_MIN_CONTENT_INSET}px, calc(${contentIndent}px - ${FLUSH_CARD_CONTENT_PULLBACK}px))`
: `${FLUSH_CARD_MIN_CONTENT_INSET}px`
}