mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 03:27:44 +00:00
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec
255 lines
9.2 KiB
TypeScript
255 lines
9.2 KiB
TypeScript
import type { RuntimeSyncedTab, RuntimeWorktreePsSummary } from '../../shared/runtime-types'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
|
import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
|
|
import { parsePaneKey } from '../../shared/stable-pane-id'
|
|
import { getWorkspaceSessionTabActivityIds } from '../../shared/workspace-session-tab-activity'
|
|
import type { RuntimeStore } from './runtime-store-contract'
|
|
import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records'
|
|
import type { RuntimeWorktreePsSummaryLookup } from './runtime-worktree-summary-paths'
|
|
import {
|
|
getLatestPtyTitle,
|
|
getLeafDisplayRecord,
|
|
getLeafWorktreeStatus,
|
|
getPtyDisplayRecord,
|
|
getSavedTabWorktreeStatus,
|
|
maxTimestamp,
|
|
mergeWorktreeSummaryStatus,
|
|
type TitleDisplayClear
|
|
} from './runtime-worktree-status-projection'
|
|
import { runtimeWorktreeIdsEqual } from './runtime-worktree-path-identity'
|
|
|
|
export type RuntimeWorkingTerminalEvidence = {
|
|
paneKey: string | null
|
|
ptyId: string | null
|
|
tabId: string | null
|
|
}
|
|
|
|
export function applyRuntimeWorktreePsTerminalActivity(args: {
|
|
freshPtyLiveness: ReadonlySet<string> | null
|
|
/** Filled with every PTY this pass counts as live. */
|
|
countedPtyIds: Set<string>
|
|
leaves: Iterable<RuntimeLeafRecord>
|
|
ptysById: ReadonlyMap<string, RuntimePtyWorktreeRecord>
|
|
tabs: ReadonlyMap<string, RuntimeSyncedTab>
|
|
session: WorkspaceSessionState | null | undefined
|
|
getPaneKey: (leaf: RuntimeLeafRecord) => string
|
|
getTitleDisplayClear: (ptyId: string) => TitleDisplayClear | null
|
|
getSummary: RuntimeWorktreePsSummaryLookup
|
|
}): Map<string, RuntimeWorkingTerminalEvidence[]> {
|
|
const workingEvidence = new Map<string, RuntimeWorkingTerminalEvidence[]>()
|
|
const savedTabOwnerById = new Map<string, { worktreeId: string; title: string }>()
|
|
for (const [worktreeId, tabs] of Object.entries(args.session?.tabsByWorktree ?? {})) {
|
|
for (const tab of tabs) {
|
|
savedTabOwnerById.set(tab.id, { worktreeId, title: tab.title })
|
|
}
|
|
}
|
|
const savedLayoutTabIdByPtyId = new Map<string, string>()
|
|
for (const [tabId, layout] of Object.entries(args.session?.terminalLayoutsByTabId ?? {})) {
|
|
for (const ptyId of Object.values(layout?.ptyIdsByLeafId ?? {})) {
|
|
if (ptyId) {
|
|
savedLayoutTabIdByPtyId.set(ptyId, tabId)
|
|
}
|
|
}
|
|
}
|
|
const countedPtyIds = args.countedPtyIds
|
|
for (const leaf of args.leaves) {
|
|
if (
|
|
!leaf.ptyId ||
|
|
!leaf.connected ||
|
|
(args.freshPtyLiveness !== null && !args.freshPtyLiveness.has(leaf.ptyId))
|
|
) {
|
|
continue
|
|
}
|
|
const freshOwner = args.ptysById.get(leaf.ptyId)
|
|
if (
|
|
args.freshPtyLiveness !== null &&
|
|
freshOwner?.connected &&
|
|
!runtimeWorktreeIdsEqual(freshOwner.worktreeId, leaf.worktreeId)
|
|
) {
|
|
continue
|
|
}
|
|
const summary = args.getSummary(leaf.worktreeId)
|
|
if (!summary) {
|
|
continue
|
|
}
|
|
countedPtyIds.add(leaf.ptyId)
|
|
summary.hasHostSidebarActivity = true
|
|
const previousLastOutputAt = summary.lastOutputAt
|
|
summary.liveTerminalCount += 1
|
|
summary.hasAttachedPty = true
|
|
summary.lastOutputAt = maxTimestamp(summary.lastOutputAt, leaf.lastOutputAt)
|
|
const leafStatus = getLeafWorktreeStatus(
|
|
getLeafDisplayRecord(leaf, args.getTitleDisplayClear(leaf.ptyId)),
|
|
args.tabs.get(leaf.tabId)?.title ?? null
|
|
)
|
|
if (leafStatus === 'working') {
|
|
addWorkingTerminalEvidence(workingEvidence, summary.worktreeId, {
|
|
paneKey: args.getPaneKey(leaf),
|
|
ptyId: leaf.ptyId,
|
|
tabId: leaf.tabId
|
|
})
|
|
}
|
|
mergeWorktreeSummaryStatus(summary, leafStatus)
|
|
if (
|
|
leaf.preview &&
|
|
(summary.preview.length === 0 || (leaf.lastOutputAt ?? -1) >= (previousLastOutputAt ?? -1))
|
|
) {
|
|
summary.preview = leaf.preview
|
|
}
|
|
}
|
|
for (const pty of args.ptysById.values()) {
|
|
if (
|
|
!pty.connected ||
|
|
countedPtyIds.has(pty.ptyId) ||
|
|
(args.freshPtyLiveness !== null && !args.freshPtyLiveness.has(pty.ptyId))
|
|
) {
|
|
continue
|
|
}
|
|
const persistedTabId = savedLayoutTabIdByPtyId.get(pty.ptyId)
|
|
const displayTitle = getLatestPtyTitle(
|
|
getPtyDisplayRecord(pty, args.getTitleDisplayClear(pty.ptyId))
|
|
)
|
|
let owner = persistedTabId ? savedTabOwnerById.get(persistedTabId) : undefined
|
|
if (args.freshPtyLiveness !== null) {
|
|
owner = { worktreeId: pty.worktreeId, title: owner?.title ?? displayTitle ?? '' }
|
|
}
|
|
if (!owner && persistedTabId && pty.tabId === persistedTabId) {
|
|
owner = { worktreeId: pty.worktreeId, title: displayTitle ?? '' }
|
|
}
|
|
const pane = parsePaneKey(pty.paneKey ?? '')
|
|
const hasExplicitOwner =
|
|
pty.tabId !== null && pane?.tabId === pty.tabId && pane.leafId.length > 0
|
|
const savedOwner = pty.tabId ? savedTabOwnerById.get(pty.tabId) : undefined
|
|
const hasSavedLayout =
|
|
pty.tabId !== null && Object.hasOwn(args.session?.terminalLayoutsByTabId ?? {}, pty.tabId)
|
|
if (!owner && hasExplicitOwner && !hasSavedLayout) {
|
|
owner = {
|
|
worktreeId: savedOwner?.worktreeId ?? pty.worktreeId,
|
|
title: savedOwner?.title ?? displayTitle ?? ''
|
|
}
|
|
}
|
|
if (!owner) {
|
|
continue
|
|
}
|
|
const summary = args.getSummary(owner.worktreeId)
|
|
if (!summary) {
|
|
continue
|
|
}
|
|
countedPtyIds.add(pty.ptyId)
|
|
const previousLastOutputAt = summary.lastOutputAt
|
|
summary.liveTerminalCount += 1
|
|
summary.hasAttachedPty = true
|
|
summary.hasHostSidebarActivity = true
|
|
summary.lastOutputAt = maxTimestamp(summary.lastOutputAt, pty.lastOutputAt)
|
|
const ptyStatus = getSavedTabWorktreeStatus(owner.title, true)
|
|
if (ptyStatus === 'working') {
|
|
addWorkingTerminalEvidence(workingEvidence, summary.worktreeId, {
|
|
paneKey: pty.paneKey,
|
|
ptyId: pty.ptyId,
|
|
tabId: pty.tabId ?? persistedTabId ?? null
|
|
})
|
|
}
|
|
mergeWorktreeSummaryStatus(summary, ptyStatus)
|
|
if (
|
|
pty.preview &&
|
|
(summary.preview.length === 0 || (pty.lastOutputAt ?? -1) >= (previousLastOutputAt ?? -1))
|
|
) {
|
|
summary.preview = pty.preview
|
|
}
|
|
}
|
|
return workingEvidence
|
|
}
|
|
|
|
function addWorkingTerminalEvidence(
|
|
evidenceByWorktreeId: Map<string, RuntimeWorkingTerminalEvidence[]>,
|
|
worktreeId: string,
|
|
evidence: RuntimeWorkingTerminalEvidence
|
|
): void {
|
|
const existing = evidenceByWorktreeId.get(worktreeId)
|
|
if (existing) {
|
|
existing.push(evidence)
|
|
} else {
|
|
evidenceByWorktreeId.set(worktreeId, [evidence])
|
|
}
|
|
}
|
|
|
|
export function applyRuntimeWorktreePsSessionActivity(args: {
|
|
store: RuntimeStore | null
|
|
summaries: Map<string, RuntimeWorktreePsSummary>
|
|
repoById: ReadonlyMap<string, Repo>
|
|
ptysById: ReadonlyMap<string, RuntimePtyWorktreeRecord>
|
|
tabs: ReadonlyMap<string, RuntimeSyncedTab>
|
|
/** Non-minting: a listing must not issue handles, only recognise the ones already bound. */
|
|
getTerminalHandlesForPty: (ptyId: string) => readonly string[]
|
|
getSummary: RuntimeWorktreePsSummaryLookup
|
|
}): {
|
|
mirroredWorktreeIdByTabId: Map<string, string>
|
|
connectedPtyEvidence: {
|
|
tabIds: Set<string>
|
|
paneKeys: Set<string>
|
|
ptyIdByTerminalHandle: Map<string, string>
|
|
}
|
|
} {
|
|
const mirroredWorktreeIdByTabId = new Map<string, string>()
|
|
const sessionsByHostId = new Map<ExecutionHostId, WorkspaceSessionState>()
|
|
for (const summary of args.summaries.values()) {
|
|
const repo = args.repoById.get(summary.repoId)
|
|
const session = args.store?.getWorkspaceSession?.(repo ? getRepoExecutionHostId(repo) : 'local')
|
|
if (session) {
|
|
sessionsByHostId.set(repo ? getRepoExecutionHostId(repo) : 'local', session)
|
|
}
|
|
}
|
|
for (const session of sessionsByHostId.values()) {
|
|
for (const [worktreeId, tabs] of Object.entries(session.tabsByWorktree ?? {})) {
|
|
for (const tab of tabs) {
|
|
mirroredWorktreeIdByTabId.set(tab.id, worktreeId)
|
|
}
|
|
if (tabs.length === 0) {
|
|
continue
|
|
}
|
|
const summary = args.getSummary(worktreeId)
|
|
if (summary && tabs.some((tab) => tab.ptyId && args.ptysById.get(tab.ptyId)?.connected)) {
|
|
summary.hasHostSidebarActivity = true
|
|
}
|
|
}
|
|
for (const worktreeId of getWorkspaceSessionTabActivityIds(session)) {
|
|
const summary = args.getSummary(worktreeId)
|
|
if (summary) {
|
|
summary.hasHostSidebarActivity = true
|
|
}
|
|
}
|
|
if (session.activeWorktreeId) {
|
|
const summary = args.getSummary(session.activeWorktreeId)
|
|
if (summary) {
|
|
summary.isActive = true
|
|
}
|
|
}
|
|
}
|
|
for (const [tabId, tab] of args.tabs) {
|
|
if (!mirroredWorktreeIdByTabId.has(tabId)) {
|
|
mirroredWorktreeIdByTabId.set(tabId, tab.worktreeId)
|
|
}
|
|
}
|
|
const connectedPtyEvidence = {
|
|
tabIds: new Set<string>(),
|
|
paneKeys: new Set<string>(),
|
|
ptyIdByTerminalHandle: new Map<string, string>()
|
|
}
|
|
for (const pty of args.ptysById.values()) {
|
|
if (!pty.connected) {
|
|
continue
|
|
}
|
|
if (pty.tabId) {
|
|
connectedPtyEvidence.tabIds.add(pty.tabId)
|
|
}
|
|
if (pty.paneKey) {
|
|
connectedPtyEvidence.paneKeys.add(pty.paneKey)
|
|
}
|
|
for (const terminalHandle of args.getTerminalHandlesForPty(pty.ptyId)) {
|
|
connectedPtyEvidence.ptyIdByTerminalHandle.set(terminalHandle, pty.ptyId)
|
|
}
|
|
}
|
|
return { mirroredWorktreeIdByTabId, connectedPtyEvidence }
|
|
}
|