mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
Merge remote-tracking branch 'origin/main' into brennanb2025/multi-window-monitor-support
This commit is contained in:
@@ -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`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user