mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
wip(session): register a client-local park-scrollback field (foundation only)
NOT FOR MERGE as-is — the field is declared, registered and persisted, but nothing writes or reads it yet. Preserved so the cadence-split PR does not redo the registry plumbing. parkedScrollbackByTabId is a top-level session field because exportRemoteWorkspaceSession is an explicit allowlist of named top-level fields, so it is omitted from the remote projection for free. A field inside TerminalLayoutSnapshot would ride along instead, because layout entries are copied whole. Five exhaustive 'satisfies Record<keyof WorkspaceSessionState, ...>' constraints reject the field until every registry has an entry, which is why this has no safe partial state and why it belongs in its own PR rather than as a commit on a data-loss fix. Typecheck clean. Still to build: capture routing, the single resolver, the prune cap, and re-pointed e2e coverage.
This commit is contained in:
@@ -55,6 +55,11 @@ export const WORKSPACE_SESSION_FIELD_DISPOSITION = {
|
||||
onRepoRemoval: 'prunedByBespokeRule',
|
||||
onTransfer: 'copiedByBespokeRule'
|
||||
},
|
||||
// Same tab-id keying as the layouts it shadows, so park scrollback follows the same tab ids.
|
||||
parkedScrollbackByTabId: {
|
||||
onRepoRemoval: 'prunedByBespokeRule',
|
||||
onTransfer: 'copiedByBespokeRule'
|
||||
},
|
||||
activeWorktreeIdsOnShutdown: {
|
||||
onRepoRemoval: 'prunedByBespokeRule',
|
||||
onTransfer: 'copiedByBespokeRule'
|
||||
|
||||
@@ -27,6 +27,7 @@ export const WORKSPACE_SESSION_WORKTREE_REFERENCE_KIND = {
|
||||
activeTabId: 'none',
|
||||
tabsByWorktree: 'owner-keyed-row-arrays',
|
||||
terminalLayoutsByTabId: 'none',
|
||||
parkedScrollbackByTabId: 'none',
|
||||
activeWorktreeIdsOnShutdown: 'worktree-id-array',
|
||||
openFilesByWorktree: 'owner-keyed-row-arrays',
|
||||
activeFileIdByWorktree: 'owner-keyed',
|
||||
|
||||
@@ -12,6 +12,7 @@ function createSnapshot(browserUrlHistory: BrowserHistoryEntry[]): WorkspaceSess
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
parkedScrollbackByTabId: {},
|
||||
activeTabIdByWorktree: {},
|
||||
openFiles: [],
|
||||
editorDrafts: {},
|
||||
|
||||
@@ -13,6 +13,7 @@ function createSnapshot(
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
parkedScrollbackByTabId: {},
|
||||
activeTabIdByWorktree: {},
|
||||
openFiles: [],
|
||||
editorDrafts: {},
|
||||
|
||||
@@ -12,6 +12,7 @@ function createSnapshot(
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
parkedScrollbackByTabId: {},
|
||||
activeTabIdByWorktree: {},
|
||||
openFiles: [],
|
||||
editorDrafts: {},
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('SESSION_RELEVANT_FIELDS', () => {
|
||||
tabsByWorktree: true,
|
||||
ptyIdsByTabId: true,
|
||||
terminalLayoutsByTabId: true,
|
||||
parkedScrollbackByTabId: true,
|
||||
activeTabIdByWorktree: true,
|
||||
openFiles: true,
|
||||
editorDrafts: true,
|
||||
|
||||
@@ -34,6 +34,7 @@ export type WorkspaceSessionSnapshot = Pick<
|
||||
| 'tabsByWorktree'
|
||||
| 'ptyIdsByTabId'
|
||||
| 'terminalLayoutsByTabId'
|
||||
| 'parkedScrollbackByTabId'
|
||||
| 'activeTabIdByWorktree'
|
||||
| 'openFiles'
|
||||
| 'editorDrafts'
|
||||
@@ -76,6 +77,7 @@ export const SESSION_RELEVANT_FIELDS = [
|
||||
'tabsByWorktree',
|
||||
'ptyIdsByTabId',
|
||||
'terminalLayoutsByTabId',
|
||||
'parkedScrollbackByTabId',
|
||||
'activeTabIdByWorktree',
|
||||
'openFiles',
|
||||
'editorDrafts',
|
||||
@@ -294,6 +296,7 @@ export function buildWorkspaceSessionPayload(
|
||||
activeTabId: snapshot.activeTabId,
|
||||
tabsByWorktree: buildSanitizedTabsByWorktree(snapshot.tabsByWorktree),
|
||||
terminalLayoutsByTabId: snapshot.terminalLayoutsByTabId,
|
||||
parkedScrollbackByTabId: snapshot.parkedScrollbackByTabId,
|
||||
// Why: session:set fully replaces the persisted object, so dropping this silently disables eager terminal reconnect on restart.
|
||||
activeWorktreeIdsOnShutdown: terminalSessionData.activeWorktreeIdsOnShutdown,
|
||||
activeTabIdByWorktree: snapshot.activeTabIdByWorktree,
|
||||
|
||||
@@ -42,6 +42,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
expandedPaneByTabId: {},
|
||||
canExpandPaneByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
parkedScrollbackByTabId: {},
|
||||
pendingStartupByTabId: {},
|
||||
pendingInitialCwdByTabId: {},
|
||||
pendingSetupSplitByTabId: {},
|
||||
|
||||
@@ -189,6 +189,8 @@ export type TerminalActions = {
|
||||
setTabPaneExpanded: (tabId: string, expanded: boolean) => void
|
||||
setTabCanExpandPane: (tabId: string, canExpand: boolean) => void
|
||||
setTabLayout: (tabId: string, layout: TerminalLayoutSnapshot | null) => void
|
||||
/** Client-local park scrollback for a tab. Never uploaded; read via `resolveLeafScrollback`. */
|
||||
setTabParkedScrollback: (tabId: string, buffersByLeafId: Record<string, string> | null) => void
|
||||
syncPaneDetachPtyOwnership: (args: {
|
||||
detachedLeafId: string
|
||||
detachedPtyId: string | null
|
||||
|
||||
@@ -23,9 +23,26 @@ export function createTerminalLayoutActions(
|
||||
| 'setTabPaneExpanded'
|
||||
| 'setTabCanExpandPane'
|
||||
| 'setTabLayout'
|
||||
| 'setTabParkedScrollback'
|
||||
| 'syncPaneDetachPtyOwnership'
|
||||
> {
|
||||
return {
|
||||
// Why separate from setTabLayout: park scrollback is client-local and must not ride the remote
|
||||
// projection. See WorkspaceSessionState.parkedScrollbackByTabId; read via resolveLeafScrollback.
|
||||
setTabParkedScrollback: (tabId, buffersByLeafId) => {
|
||||
set((s) => {
|
||||
const current = s.parkedScrollbackByTabId ?? {}
|
||||
if (!buffersByLeafId || Object.keys(buffersByLeafId).length === 0) {
|
||||
if (!(tabId in current)) {
|
||||
return s
|
||||
}
|
||||
const next = { ...current }
|
||||
delete next[tabId]
|
||||
return { parkedScrollbackByTabId: next }
|
||||
}
|
||||
return { parkedScrollbackByTabId: { ...current, [tabId]: buffersByLeafId } }
|
||||
})
|
||||
},
|
||||
replaceTerminalLayoutPanePtyId: (tabId, leafId, ptyId) => {
|
||||
set((s) => {
|
||||
const layout = s.terminalLayoutsByTabId[tabId]
|
||||
|
||||
@@ -44,6 +44,9 @@ export type TerminalState = {
|
||||
expandedPaneByTabId: Record<string, boolean>
|
||||
canExpandPaneByTabId: Record<string, boolean>
|
||||
terminalLayoutsByTabId: Record<string, TerminalLayoutSnapshot>
|
||||
/** Client-local park scrollback, tabId -> leafId -> buffer. Never uploaded to a peer; see
|
||||
* WorkspaceSessionState.parkedScrollbackByTabId. Read via `resolveLeafScrollback`, never directly. */
|
||||
parkedScrollbackByTabId: Record<string, Record<string, string>>
|
||||
recentQuickCommandIdByGroup: Record<string, string>
|
||||
/** Runtime-only claim bridging startup payload consumption until terminal hooks mount. */
|
||||
automaticAgentResumeClaimsByTabId: Record<string, AutomaticAgentResumeClaim>
|
||||
|
||||
@@ -44,6 +44,9 @@ export const WORKSPACE_SESSION_FIELD_OWNERSHIP = {
|
||||
activeWorkspaceKey: 'global',
|
||||
activeWorktreeIdsOnShutdown: 'worktreeArray',
|
||||
terminalLayoutsByTabId: 'tabKeyed',
|
||||
// Local-only, never uploaded — but still routed per host so a tab's park scrollback follows its
|
||||
// own partition rather than merging across hosts the way sleepingAgentKeyed rows do.
|
||||
parkedScrollbackByTabId: 'tabKeyed',
|
||||
remoteSessionIdsByTabId: 'tabKeyed',
|
||||
browserPagesByWorkspace: 'browserWorkspaceKeyed',
|
||||
markdownFrontmatterVisible: 'fileKeyed',
|
||||
|
||||
@@ -28,6 +28,7 @@ const PERSISTED_WORKSPACE_SESSION_FIELDS = {
|
||||
activeTabId: true,
|
||||
tabsByWorktree: true,
|
||||
terminalLayoutsByTabId: true,
|
||||
parkedScrollbackByTabId: true,
|
||||
activeWorktreeIdsOnShutdown: true,
|
||||
openFilesByWorktree: true,
|
||||
activeFileIdByWorktree: true,
|
||||
|
||||
@@ -213,6 +213,12 @@ export const workspaceSessionStateSchema: z.ZodType<WorkspaceSessionState> = z.o
|
||||
salvagingRecord(terminalTabIdSchema, terminalLayoutSnapshotSchema),
|
||||
() => ({})
|
||||
),
|
||||
// Client-local park scrollback; see WorkspaceSessionState.parkedScrollbackByTabId for why it is
|
||||
// not a field on the layout snapshot. Optional so an older profile simply carries none.
|
||||
parkedScrollbackByTabId: salvagedOptional(
|
||||
'parkedScrollbackByTabId',
|
||||
salvagingRecord(terminalTabIdSchema, leafStringsSchema)
|
||||
),
|
||||
activeWorktreeIdsOnShutdown: salvagedOptional(
|
||||
'activeWorktreeIdsOnShutdown',
|
||||
salvagingArray(worktreeIdSchema)
|
||||
|
||||
@@ -43,6 +43,26 @@ export type WorkspaceSessionState = {
|
||||
/** Keys may be legacy raw worktree IDs or canonical WorkspaceKey values. */
|
||||
tabsByWorktree: Record<string, TerminalTab[]>
|
||||
terminalLayoutsByTabId: Record<string, TerminalLayoutSnapshot>
|
||||
/**
|
||||
* Client-local scrollback captured when a pane cold-parks, keyed tabId -> leafId -> buffer.
|
||||
*
|
||||
* Why a second home for scrollback rather than `TerminalLayoutSnapshot.buffersByLeafId`: the two
|
||||
* encode different things. `buffersByLeafId` is **shared with peers** — it rides the remote
|
||||
* projection so a second desktop can cold-restore a tab this machine parked. This field is
|
||||
* **local-only**: the ordinary cold park fires every time a workspace is hidden, and shipping
|
||||
* that in a wholesale `replace-session` costs tens of MiB on the common path for a copy no peer
|
||||
* consumes. `exportRemoteWorkspaceSession` is an explicit allowlist of named top-level fields,
|
||||
* so a top-level field is omitted from the upload for free — a field *inside* the layout would
|
||||
* ride along, because layout entries are copied whole.
|
||||
*
|
||||
* Second, weaker-but-real property: the mirrored-tab apply rewrites only `ptyIdsByTabId` and
|
||||
* `terminalLayoutsByTabId`, so scrollback here cannot be wiped by a host inventory frame at all.
|
||||
* Moving it back into the layout for tidiness would reintroduce that wipe.
|
||||
*
|
||||
* Never read this field directly — resolve through `resolveLeafScrollback` so no consumer has to
|
||||
* know which of the two homes a leaf's bytes live in.
|
||||
*/
|
||||
parkedScrollbackByTabId?: Record<string, Record<string, string>>
|
||||
/** Worktree IDs that had at least one tab with a live PTY at shutdown.
|
||||
* Used on startup to eagerly re-spawn PTY processes so the Active filter
|
||||
* works immediately after restart. */
|
||||
|
||||
Reference in New Issue
Block a user