mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
fix(sidebar): Hide sleeping keeps a paired server's active workspaces (dark) (#30624)
* fix(sidebar): Hide sleeping reads a paired server's own terminal report (dark) With ORCA_SIDEBAR_INPUTS=main, main's activity domain now lists a paired server's rows: their terminals and agents as the server's worktree.ps reports them, plus the browser and chat tabs this desktop's saved session holds there. A server out of contact keeps live terminals unverifiable. Switch-off builds are unchanged. * fix(sidebar): keep server activity out of the published paired rows Review: a strict field on paired rows could drop a whole row for an older reader; the activity domain already carries it.
This commit is contained in:
@@ -119,7 +119,9 @@ describe('PairedServerRowsProducer', () => {
|
||||
|
||||
it('populates from a server in contact once its change stream is ready', async () => {
|
||||
const { hosts, call, watches } = fakeHosts([{ id: 'box', live: true }])
|
||||
call.mockResolvedValue(psReply([row('repo::/a', { workspaceKind: 'git' })]))
|
||||
call.mockResolvedValue(
|
||||
psReply([row('repo::/a', { workspaceKind: 'git', liveTerminalCount: 1, status: 'active' })])
|
||||
)
|
||||
const producer = new PairedServerRowsProducer({
|
||||
hasDemand: () => true,
|
||||
hostLabelOverrides: () => new Map()
|
||||
@@ -158,7 +160,9 @@ describe('PairedServerRowsProducer', () => {
|
||||
isPinned: false,
|
||||
isMainWorktree: false,
|
||||
lineage: null,
|
||||
workspaceStatus: 'in-progress'
|
||||
workspaceStatus: 'in-progress',
|
||||
// The server's own terminals and agents.
|
||||
activity: { terminal: 'live', agent: null }
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -405,7 +409,8 @@ describe('PairedServerRowsProducer', () => {
|
||||
isPinned: false,
|
||||
isMainWorktree: false,
|
||||
lineage: null,
|
||||
workspaceStatus: 'in-progress'
|
||||
workspaceStatus: 'in-progress',
|
||||
activity: { terminal: null, agent: null }
|
||||
}
|
||||
])
|
||||
expect(rowsOf(producer, 'runtime:two')?.rows.map((entry) => [entry.key, entry.kind])).toEqual([
|
||||
|
||||
@@ -14,6 +14,8 @@ import type { RepoIconDataUrls } from './repo-icon-data-urls'
|
||||
import type { SidebarInputsFeedDomain } from './sidebar-inputs-feed'
|
||||
import { readDesktopSidebarFacts } from './desktop-sidebar-facts'
|
||||
import { buildPairedServerReviewFacts } from '../../../shared/sidebar-list/workspace-review-facts'
|
||||
import { buildPairedServerActivityFacts } from '../../../shared/sidebar-list/paired-server-activity-facts'
|
||||
import { parseExecutionHostId } from '../../../shared/execution-host'
|
||||
|
||||
/** This host's store and connection registries, read the way sidebarInputs.desktopFacts reads them. */
|
||||
export function readRuntimeDesktopSidebarFacts(store: RuntimeStore): DesktopSidebarFacts {
|
||||
@@ -70,8 +72,27 @@ export function sidebarInputsFeedDomains(
|
||||
return [
|
||||
{
|
||||
name: 'activity',
|
||||
read: () => facts.getWorkspaceActivityFacts().facts,
|
||||
watch: (changed) => facts.workspaceActivityFacts.revision.onChange(changed)
|
||||
read: () => ({
|
||||
// A paired server's rows too; this host's own rows win an id both list.
|
||||
...buildPairedServerActivityFacts(
|
||||
pairedServerRows.snapshot().servers,
|
||||
(hostId) => sources.store()?.getWorkspaceSession?.(hostId) ?? null
|
||||
),
|
||||
...facts.getWorkspaceActivityFacts().facts
|
||||
}),
|
||||
watch: (changed) => {
|
||||
const detaches = [
|
||||
facts.workspaceActivityFacts.revision.onChange(changed),
|
||||
pairedServerRows.revision.onChange(changed),
|
||||
// This desktop's saved session for a paired server holds its browser and chat tabs there.
|
||||
sources.store()?.onWorkspaceSessionWritten?.((hostId) => {
|
||||
if (hostId === null || parseExecutionHostId(hostId)?.kind === 'runtime') {
|
||||
changed()
|
||||
}
|
||||
})
|
||||
]
|
||||
return () => detaches.forEach((detach) => detach?.())
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'temporal',
|
||||
@@ -107,7 +128,10 @@ export function sidebarInputsFeedDomains(
|
||||
Object.fromEntries(
|
||||
pairedServerRows
|
||||
.snapshot()
|
||||
.servers.flatMap(({ rows }) => rows.map((row) => [row.key, row]))
|
||||
// Activity goes out in its own domain, so a terminal count does not resend the row.
|
||||
.servers.flatMap(({ rows }) =>
|
||||
rows.map(({ activity: _activity, ...row }) => [row.key, row])
|
||||
)
|
||||
),
|
||||
watch: (changed) => pairedServerRows.revision.onChange(changed)
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
SidebarFeedView
|
||||
} from '../../../../shared/sidebar-list/sidebar-feed-rows'
|
||||
import type { SmartStampFields } from '../../../../shared/sidebar-list/sort-stamps'
|
||||
import { buildPairedServerActivityFacts } from '../../../../shared/sidebar-list/paired-server-activity-facts'
|
||||
import { GOLDEN_NOW, folder, group, lineage } from './sidebar-list-golden-factories.test-fixture'
|
||||
import { sidebarWorkspaceRowFilters } from './sidebar-filter-actions'
|
||||
import {
|
||||
@@ -130,6 +131,51 @@ export function parityActiveState(
|
||||
return { activeWorktreeId: worktreeId, activeWorkspaceExecutionHostId: hostId }
|
||||
}
|
||||
|
||||
const SERVER_WORKTREE_ID = 'api::/w/api-y'
|
||||
|
||||
/** A desktop terminal in a server workspace: the PTY is the server's, which reports it. */
|
||||
export function parityServerTerminalState(): Partial<AppState> {
|
||||
return {
|
||||
tabsByWorktree: {
|
||||
[SERVER_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'tab-server',
|
||||
ptyId: 'pty-server',
|
||||
worktreeId: SERVER_WORKTREE_ID,
|
||||
title: 'Terminal 1',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: GOLDEN_NOW
|
||||
}
|
||||
]
|
||||
},
|
||||
ptyIdsByTabId: { 'tab-server': ['pty-server'] }
|
||||
}
|
||||
}
|
||||
|
||||
/** A desktop browser tab in a server workspace: the server never sees it. */
|
||||
export function parityServerBrowserTabState(): Partial<AppState> {
|
||||
return {
|
||||
browserTabsByWorktree: {
|
||||
[SERVER_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'browser-server',
|
||||
worktreeId: SERVER_WORKTREE_ID,
|
||||
url: 'about:blank',
|
||||
title: '',
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: GOLDEN_NOW
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed domains main would publish for this store, read through the window-side projections
|
||||
* the shadow comparison held equal to main's producers before the window read main's feed.
|
||||
@@ -137,7 +183,13 @@ export function parityActiveState(
|
||||
export function feedDomainsFromWindowState(state: AppState): SidebarInputsFeedDomain[] {
|
||||
const worktrees = getAllWorktreesFromState(state)
|
||||
const servers = readWindowPairedServerHosts(state, new Set([SERVER_HOST_ID]))
|
||||
const windowActivity = readWindowWorkspaceActivityFacts(state, GOLDEN_NOW)
|
||||
const serverRows = readWindowPairedServerRows(state, new Set(servers.keys()))
|
||||
// The server reports the terminals and agents the desktop runs in its workspaces.
|
||||
const reportedRows = [...serverRows.values()].map((row) => {
|
||||
const facts = windowActivity.get(row.worktreeId)
|
||||
return { ...row, activity: { terminal: facts?.terminal ?? null, agent: facts?.agent ?? null } }
|
||||
})
|
||||
const domain = (name: string, read: () => Record<string, unknown>): SidebarInputsFeedDomain => ({
|
||||
name,
|
||||
read,
|
||||
@@ -154,11 +206,17 @@ export function feedDomainsFromWindowState(state: AppState): SidebarInputsFeedDo
|
||||
})
|
||||
)
|
||||
),
|
||||
domain('activity', () =>
|
||||
Object.fromEntries(
|
||||
toLocallyOwnedRows(state, readWindowWorkspaceActivityFacts(state, GOLDEN_NOW))
|
||||
)
|
||||
),
|
||||
domain('activity', () => ({
|
||||
// The window's browser and chat tabs, as its saved session for the server holds them.
|
||||
...buildPairedServerActivityFacts(
|
||||
[{ hostId: SERVER_HOST_ID, freshness: 'live', rows: reportedRows }],
|
||||
() => ({
|
||||
browserTabsByWorktree: state.browserTabsByWorktree,
|
||||
unifiedTabs: state.unifiedTabsByWorktree
|
||||
})
|
||||
),
|
||||
...Object.fromEntries(toLocallyOwnedRows(state, windowActivity))
|
||||
})),
|
||||
domain('review', () => ({
|
||||
...buildPairedServerReviewFacts(
|
||||
[{ hostId: SERVER_HOST_ID, rows: [...serverRows.values()] }],
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
* and agent-send reveal; its imported-worktrees and new-external-worktrees cards; its create
|
||||
* preflight; host-header health and detail.
|
||||
* Known gaps the phone shows differently, so the fixture leaves them out: a creation a paired
|
||||
* server runs; a paired server's activity under "Hide sleeping" (the window reads its own tabs);
|
||||
* a PR unlinked on a server workspace; an SSH workspace reached through a paired server.
|
||||
* server runs; a PR unlinked on a server workspace; an SSH workspace reached through a paired
|
||||
* server; a server terminal only another client opened (the server counts it, the window does not).
|
||||
*/
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, renderHook } from '@testing-library/react'
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
import {
|
||||
feedDomainsFromWindowState,
|
||||
parityActiveState,
|
||||
parityServerBrowserTabState,
|
||||
parityServerTerminalState,
|
||||
parityWorldOverrides,
|
||||
phoneInputsFromWindowState,
|
||||
projectParityRows
|
||||
@@ -243,6 +245,15 @@ describe('sidebar list parity: desktop window and feed reader', () => {
|
||||
{ showSleepingWorkspaces: false, ...goldenLiveTerminalState() },
|
||||
/^ITEM .*\|orca::\/w\/feat-a /
|
||||
],
|
||||
// The server reports the desktop's terminal; only the desktop knows its browser tab.
|
||||
'hide sleeping with a paired server terminal': [
|
||||
{ showSleepingWorkspaces: false, ...parityServerTerminalState() },
|
||||
/^ITEM .*runtime:env-1\|api::\/w\/api-y /
|
||||
],
|
||||
'hide sleeping with a browser tab in a paired server workspace': [
|
||||
{ showSleepingWorkspaces: false, ...parityServerBrowserTabState() },
|
||||
/^ITEM .*runtime:env-1\|api::\/w\/api-y /
|
||||
],
|
||||
'hide default branch': [{ hideDefaultBranchWorkspace: true }, /^ITEM .*api-y /],
|
||||
// The same id on two hosts: only the SSH copy is open.
|
||||
'open on one of two hosts': [
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ExecutionHostId } from '../execution-host'
|
||||
import type { WorkspaceSessionState } from '../workspace-session-state-types'
|
||||
import { buildPairedServerActivityFacts } from './paired-server-activity-facts'
|
||||
import { normalizePairedServerRow, type PairedServerWorkspaceRow } from './paired-server-rows'
|
||||
|
||||
const HOST = 'runtime:env-1'
|
||||
|
||||
function serverRow(worktreeId: string, fields: Record<string, unknown> = {}) {
|
||||
const row = normalizePairedServerRow(HOST, {
|
||||
worktreeId,
|
||||
repoId: 'repo',
|
||||
path: `/srv/${worktreeId}`,
|
||||
branch: worktreeId,
|
||||
liveTerminalCount: 0,
|
||||
status: 'inactive',
|
||||
...fields
|
||||
})
|
||||
if (!row) {
|
||||
throw new Error('row did not normalize')
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
function build(
|
||||
rows: PairedServerWorkspaceRow[],
|
||||
options: {
|
||||
freshness?: 'live' | 'unverifiable'
|
||||
session?: Pick<WorkspaceSessionState, 'browserTabsByWorktree' | 'unifiedTabs'>
|
||||
} = {}
|
||||
) {
|
||||
const asked: ExecutionHostId[] = []
|
||||
const facts = buildPairedServerActivityFacts(
|
||||
[{ hostId: HOST, freshness: options.freshness ?? 'live', rows }],
|
||||
(hostId) => {
|
||||
asked.push(hostId)
|
||||
return options.session ?? null
|
||||
}
|
||||
)
|
||||
return { facts, asked }
|
||||
}
|
||||
|
||||
describe('paired server activity facts', () => {
|
||||
it("reads the server's own terminals and agents off its rows", () => {
|
||||
const { facts } = build([
|
||||
serverRow('live', { liveTerminalCount: 2, status: 'active' }),
|
||||
serverRow('lost', { unverifiableTerminalCount: 1 }),
|
||||
serverRow('agent', { status: 'working', workingMode: 'monitoring' }),
|
||||
serverRow('asks', { liveTerminalCount: 1, status: 'permission' }),
|
||||
serverRow('idle', { status: 'done' })
|
||||
])
|
||||
expect(facts).toEqual({
|
||||
live: { terminal: 'live', browser: false, structuredChat: false, agent: null },
|
||||
lost: { terminal: 'unverifiable', browser: false, structuredChat: false, agent: null },
|
||||
agent: { terminal: null, browser: false, structuredChat: false, agent: 'monitoring' },
|
||||
asks: { terminal: 'live', browser: false, structuredChat: false, agent: 'permission' }
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a server out of contact unverifiable, never asleep', () => {
|
||||
const { facts } = build([serverRow('live', { liveTerminalCount: 1, status: 'active' })], {
|
||||
freshness: 'unverifiable'
|
||||
})
|
||||
expect(facts.live?.terminal).toBe('unverifiable')
|
||||
})
|
||||
|
||||
it("adds the browser tabs this desktop's session holds in a server workspace", () => {
|
||||
const { facts, asked } = build([serverRow('browsing')], {
|
||||
session: {
|
||||
browserTabsByWorktree: {
|
||||
browsing: [
|
||||
{
|
||||
id: 'b1',
|
||||
worktreeId: 'browsing',
|
||||
url: 'about:blank',
|
||||
title: '',
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
unifiedTabs: {}
|
||||
}
|
||||
})
|
||||
expect(asked).toEqual([HOST])
|
||||
expect(facts.browsing).toEqual({
|
||||
terminal: null,
|
||||
browser: true,
|
||||
structuredChat: false,
|
||||
agent: null
|
||||
})
|
||||
})
|
||||
|
||||
it('an older server that sends no counts reports nothing', () => {
|
||||
const row = normalizePairedServerRow(HOST, { worktreeId: 'old', repoId: 'repo' })
|
||||
expect(row?.activity).toEqual({ terminal: null, agent: null })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
parseExecutionHostId,
|
||||
toRuntimeExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../execution-host'
|
||||
import type { WorkspaceSessionState } from '../workspace-session-state-types'
|
||||
import { getWorkspaceSessionTabActivity } from '../workspace-session-tab-activity'
|
||||
import { worktreeIdentityKey } from '../worktree/id'
|
||||
import type { PairedServerRowsSnapshot } from './paired-server-rows'
|
||||
import { isIdleWorkspaceActivity, type WorkspaceActivityFacts } from './workspace-activity-facts'
|
||||
|
||||
/**
|
||||
* A paired server's rows under the server's bare ids, like the review facts: its terminals and
|
||||
* agents as the server reports them, and the browser and chat tabs this desktop's session holds
|
||||
* there, which only the desktop knows. Rows from a server out of contact keep their terminals
|
||||
* unverifiable, never gone (docs/reference/ssh-execution-boundary.md).
|
||||
*/
|
||||
export function buildPairedServerActivityFacts(
|
||||
servers: readonly Pick<
|
||||
PairedServerRowsSnapshot['servers'][number],
|
||||
'hostId' | 'freshness' | 'rows'
|
||||
>[],
|
||||
sessionOf: (
|
||||
hostId: ExecutionHostId
|
||||
) => Pick<WorkspaceSessionState, 'browserTabsByWorktree' | 'unifiedTabs'> | null
|
||||
): Record<string, WorkspaceActivityFacts> {
|
||||
const facts: Record<string, WorkspaceActivityFacts> = {}
|
||||
for (const server of servers) {
|
||||
const parsed = parseExecutionHostId(server.hostId)
|
||||
if (parsed?.kind !== 'runtime') {
|
||||
continue
|
||||
}
|
||||
const tabs = getWorkspaceSessionTabActivity(
|
||||
sessionOf(toRuntimeExecutionHostId(parsed.environmentId))
|
||||
)
|
||||
const browser = new Set([...tabs.browser].map(worktreeIdentityKey))
|
||||
const structuredChat = new Set([...tabs.structuredChat].map(worktreeIdentityKey))
|
||||
for (const row of server.rows) {
|
||||
const reported = row.activity
|
||||
const key = worktreeIdentityKey(row.worktreeId)
|
||||
const fact: WorkspaceActivityFacts = {
|
||||
terminal:
|
||||
reported?.terminal === 'live' && server.freshness !== 'live'
|
||||
? 'unverifiable'
|
||||
: (reported?.terminal ?? null),
|
||||
browser: browser.has(key),
|
||||
structuredChat: structuredChat.has(key),
|
||||
agent: reported?.agent ?? null
|
||||
}
|
||||
if (!isIdleWorkspaceActivity(fact)) {
|
||||
facts[row.worktreeId] = fact
|
||||
}
|
||||
}
|
||||
}
|
||||
return facts
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { openEnum } from '../zod-salvage'
|
||||
import { DEFAULT_WORKSPACE_STATUS_ID } from '../workspace-statuses'
|
||||
import { SMART_TIERS } from './sort-stamps'
|
||||
import { WorkspaceRowSchema } from './workspace-rows'
|
||||
import { HostReportedActivitySchema, type HostReportedActivity } from './workspace-activity-facts'
|
||||
|
||||
/** One paired-server workspace as the sidebar lists it, keyed by its host-qualified id. */
|
||||
const PairedServerWorkspaceRowSchema = z
|
||||
@@ -41,6 +42,10 @@ const PairedServerWorkspaceRowSchema = z
|
||||
smartAt: true
|
||||
}).partial().shape
|
||||
)
|
||||
.extend({
|
||||
/** The server's own terminals and agents there; older desktops omit it. */
|
||||
activity: HostReportedActivitySchema.optional().catch(undefined)
|
||||
})
|
||||
|
||||
/** See PairedServerHost; health and compatibility stay open strings, as a reader only compares them. */
|
||||
const PairedServerHostFactsSchema = z.object({
|
||||
@@ -96,9 +101,26 @@ const ServerWorktreePsRowSchema = z.object({
|
||||
detachedHead: z.literal(true).optional().catch(undefined),
|
||||
lastActivityAt: z.number().optional().catch(undefined),
|
||||
smartTier: z.enum(SMART_TIERS).optional().catch(undefined),
|
||||
smartAt: z.number().optional().catch(undefined)
|
||||
smartAt: z.number().optional().catch(undefined),
|
||||
liveTerminalCount: z.number().optional().catch(undefined),
|
||||
unverifiableTerminalCount: z.number().optional().catch(undefined),
|
||||
status: z.string().optional().catch(undefined),
|
||||
workingMode: z.string().optional().catch(undefined)
|
||||
})
|
||||
|
||||
function reportedActivity(row: z.infer<typeof ServerWorktreePsRowSchema>): HostReportedActivity {
|
||||
const working = row.workingMode === 'monitoring' ? 'monitoring' : 'working'
|
||||
return {
|
||||
terminal:
|
||||
(row.liveTerminalCount ?? 0) > 0
|
||||
? 'live'
|
||||
: (row.unverifiableTerminalCount ?? 0) > 0
|
||||
? 'unverifiable'
|
||||
: null,
|
||||
agent: row.status === 'permission' ? 'permission' : row.status === 'working' ? working : null
|
||||
}
|
||||
}
|
||||
|
||||
/** A server's own `worktree.ps` row, with defaults for what older servers omit; null without an id. */
|
||||
export function normalizePairedServerRow(
|
||||
hostId: ExecutionHostId,
|
||||
@@ -116,6 +138,10 @@ export function normalizePairedServerRow(
|
||||
parentWorktreeId,
|
||||
lineageWorktreeInstanceId,
|
||||
parentWorktreeInstanceId,
|
||||
liveTerminalCount: _liveTerminalCount,
|
||||
unverifiableTerminalCount: _unverifiableTerminalCount,
|
||||
status: _status,
|
||||
workingMode: _workingMode,
|
||||
...fields
|
||||
} = parsed.data
|
||||
return {
|
||||
@@ -132,6 +158,7 @@ export function normalizePairedServerRow(
|
||||
parentWorktreeInstanceId
|
||||
}
|
||||
: null,
|
||||
recentAt: lastActivityAt
|
||||
recentAt: lastActivityAt,
|
||||
activity: reportedActivity(parsed.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,17 @@ export const WorkspaceActivityFactsSnapshotSchema = z.object({
|
||||
facts: z.record(z.string(), WorkspaceActivityFactsSchema)
|
||||
})
|
||||
|
||||
/** What a paired server reports for one of its own workspaces; the tabs are its clients' to add. */
|
||||
export const HostReportedActivitySchema = WorkspaceActivityFactsSchema.pick({
|
||||
terminal: true,
|
||||
agent: true
|
||||
})
|
||||
|
||||
export type LiveAgentWorktreeStatus = z.infer<typeof LiveAgentWorktreeStatusSchema>
|
||||
export type WorkspaceActivityFacts = z.infer<typeof WorkspaceActivityFactsSchema>
|
||||
export type WorkspaceTerminalLiveness = NonNullable<WorkspaceActivityFacts['terminal']>
|
||||
export type WorkspaceActivityFactsSnapshot = z.infer<typeof WorkspaceActivityFactsSnapshotSchema>
|
||||
export type HostReportedActivity = z.infer<typeof HostReportedActivitySchema>
|
||||
|
||||
export const IDLE_WORKSPACE_ACTIVITY: WorkspaceActivityFacts = {
|
||||
terminal: null,
|
||||
|
||||
@@ -2,25 +2,37 @@ import type { WorkspaceSessionState } from './workspace-session-state-types'
|
||||
import { isStructuredTab } from './structured-agent-session-tab'
|
||||
import { normalizeWorkspaceSessionKeyToWorkspaceId } from './workspace-scope'
|
||||
|
||||
type SessionTabs = Pick<WorkspaceSessionState, 'browserTabsByWorktree' | 'unifiedTabs'>
|
||||
|
||||
/**
|
||||
* Workspaces a saved session holds open browser or native-chat tabs for: the two signals of the
|
||||
* desktop sidebar's activity rule (worktree-activity-state.ts) that live in the client's session
|
||||
* rather than on the execution host. Live terminals and agents are the host's to report. Reads
|
||||
* the saved session's keys; the renderer's live-state twin is getWorktreeIdsWithStructuredChat.
|
||||
*/
|
||||
export function getWorkspaceSessionTabActivityIds(
|
||||
session: Pick<WorkspaceSessionState, 'browserTabsByWorktree' | 'unifiedTabs'> | null | undefined
|
||||
): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
export function getWorkspaceSessionTabActivity(session: SessionTabs | null | undefined): {
|
||||
browser: Set<string>
|
||||
structuredChat: Set<string>
|
||||
} {
|
||||
const browser = new Set<string>()
|
||||
const structuredChat = new Set<string>()
|
||||
for (const [key, tabs] of Object.entries(session?.browserTabsByWorktree ?? {})) {
|
||||
if (tabs.length > 0) {
|
||||
ids.add(normalizeWorkspaceSessionKeyToWorkspaceId(key))
|
||||
browser.add(normalizeWorkspaceSessionKeyToWorkspaceId(key))
|
||||
}
|
||||
}
|
||||
for (const [key, tabs] of Object.entries(session?.unifiedTabs ?? {})) {
|
||||
if (tabs.some(isStructuredTab)) {
|
||||
ids.add(normalizeWorkspaceSessionKeyToWorkspaceId(key))
|
||||
structuredChat.add(normalizeWorkspaceSessionKeyToWorkspaceId(key))
|
||||
}
|
||||
}
|
||||
return ids
|
||||
return { browser, structuredChat }
|
||||
}
|
||||
|
||||
/** Either kind of getWorkspaceSessionTabActivity. */
|
||||
export function getWorkspaceSessionTabActivityIds(
|
||||
session: SessionTabs | null | undefined
|
||||
): Set<string> {
|
||||
const { browser, structuredChat } = getWorkspaceSessionTabActivity(session)
|
||||
return new Set([...browser, ...structuredChat])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user