mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
Merge pull request #551 from nwparker/cloud/v3-02-restart-history
fix(cloud): V3-02 a woken cloud pane keeps its pre-sleep history across an app restart
This commit is contained in:
@@ -69,6 +69,60 @@ describe('a shallower replay never shrinks a cloud pane (N-02)', () => {
|
||||
expect(after.filter((row) => row === '05000 line')).toHaveLength(1)
|
||||
})
|
||||
|
||||
// V3-02: asleep, woken, then the app restarted. Everything above the divider lived only in this
|
||||
// pane; the host's (new) terminal has just what came after the wake.
|
||||
it('keeps the output from before a sleep when the host replay after a restart has none of it', async () => {
|
||||
const terminal = new Terminal({ cols: 80, rows: 10, scrollback: 6000, allowProposedApi: true })
|
||||
const divider = '── Slept 10:46–10:52 AM · processes stopped ──'
|
||||
const postWake = ['$ seq 5001 5003', '5001', '5002', '5003', 'N02W-POSTWAKE']
|
||||
await write(terminal, `${[...lines(1, 300), divider, ...postWake].join('\r\n')}\r\n$ `)
|
||||
|
||||
const kept = await shallowReplay(terminal, `${postWake.join('\r\n')}\r\n$ `)
|
||||
|
||||
expect(kept).toBe(true)
|
||||
const after = rows(terminal)
|
||||
expect(after[0]).toBe('00001 line')
|
||||
expect(after).toContain(divider)
|
||||
expect(after.filter((row) => row === 'N02W-POSTWAKE')).toHaveLength(1)
|
||||
expect(after.indexOf(divider)).toBeLessThan(after.indexOf('5001'))
|
||||
})
|
||||
|
||||
it('keeps the output from before a sleep when the restarted host shows only a fresh prompt', async () => {
|
||||
const terminal = new Terminal({ cols: 80, rows: 10, scrollback: 6000, allowProposedApi: true })
|
||||
const divider = '── Slept 10:46–10:52 AM · processes stopped ──'
|
||||
await write(terminal, `${[...lines(1, 40), divider].join('\r\n')}\r\n$ `)
|
||||
|
||||
const kept = await shallowReplay(terminal, '$ ')
|
||||
|
||||
expect(kept).toBe(true)
|
||||
const after = rows(terminal)
|
||||
expect(after[0]).toBe('00001 line')
|
||||
expect(after.at(-2)).toBe(divider)
|
||||
expect(after.at(-1)).toBe('$')
|
||||
})
|
||||
|
||||
it('two sleeps before the restart: keeps everything up to the last divider, nothing doubled', async () => {
|
||||
const terminal = new Terminal({ cols: 80, rows: 10, scrollback: 6000, allowProposedApi: true })
|
||||
const first = '── Slept 9:00–9:30 AM · processes stopped ──'
|
||||
const second = '── Slept 10:46–10:52 AM · processes stopped ──'
|
||||
const postWake = ['$ echo after-second', 'after-second']
|
||||
await write(
|
||||
terminal,
|
||||
`${[...lines(1, 50), first, ...lines(51, 80), second, ...postWake].join('\r\n')}\r\n$ `
|
||||
)
|
||||
|
||||
const kept = await shallowReplay(terminal, `${postWake.join('\r\n')}\r\n$ `)
|
||||
|
||||
expect(kept).toBe(true)
|
||||
const after = rows(terminal)
|
||||
expect(after[0]).toBe('00001 line')
|
||||
expect(after.filter((row) => row === first)).toHaveLength(1)
|
||||
expect(after.filter((row) => row === second)).toHaveLength(1)
|
||||
expect(after.filter((row) => row === '00080 line')).toHaveLength(1)
|
||||
expect(after.filter((row) => row === 'after-second')).toHaveLength(1)
|
||||
expect(after.indexOf(first)).toBeLessThan(after.indexOf(second))
|
||||
})
|
||||
|
||||
it('leaves the replay alone when it does not overlap what the pane held', async () => {
|
||||
const terminal = new Terminal({ cols: 80, rows: 10, scrollback: 6000, allowProposedApi: true })
|
||||
await write(terminal, `${lines(1, 3000).join('\r\n')}\r\n$ `)
|
||||
@@ -83,6 +137,8 @@ describe('a shallower replay never shrinks a cloud pane (N-02)', () => {
|
||||
const before = ['a', 'b', 'c', 'd', 'e', 'f', 'g'].join('\r\n')
|
||||
expect(mergeDeeperHistory(before, ['tail-of-d', 'e', 'f', 'g'].join('\r\n'))).toBe(before)
|
||||
expect(mergeDeeperHistory(before, ['x', 'y', 'z', 'w'].join('\r\n'))).toBeNull()
|
||||
expect(mergeDeeperHistory(['a', 'b'].join('\r\n'), ['a', 'b', 'c', 'd'].join('\r\n'))).toBeNull()
|
||||
expect(
|
||||
mergeDeeperHistory(['a', 'b'].join('\r\n'), ['a', 'b', 'c', 'd'].join('\r\n'))
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,6 +14,11 @@ import {
|
||||
const ANCHOR_LINES = 3
|
||||
const SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g')
|
||||
|
||||
/** The wake divider ("── Slept 2:02–2:31 PM ──" in any of its forms, or a bare rule). */
|
||||
function isWakeDivider(line: string | undefined): boolean {
|
||||
return /^──( .+ ──|─*)$/.test(plain(line))
|
||||
}
|
||||
|
||||
function plain(line: string | undefined): string {
|
||||
return (line ?? '').replace(SGR, '').trimEnd()
|
||||
}
|
||||
@@ -27,6 +32,16 @@ function plain(line: string | undefined): string {
|
||||
export function mergeDeeperHistory(paneCopy: string, replayHistory: string): string | null {
|
||||
const before = paneCopy.split('\r\n')
|
||||
const after = replayHistory.split('\r\n')
|
||||
// V3-02: output from before a sleep lives only in this pane (sleep stopped the host's processes).
|
||||
// A host replay never carries the wake divider, so everything up to it is kept, then the host's.
|
||||
let dividerAt = before.length - 1
|
||||
while (dividerAt >= 0 && !isWakeDivider(before[dividerAt])) {
|
||||
dividerAt--
|
||||
}
|
||||
if (dividerAt !== -1 && !after.some(isWakeDivider)) {
|
||||
const hostLines = after.length === 1 && plain(after[0]) === '' ? [] : after
|
||||
return [...before.slice(0, dividerAt + 1), ...hostLines].join('\r\n')
|
||||
}
|
||||
if (after.length < ANCHOR_LINES + 1 || before.length <= after.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ type PendingHistory = {
|
||||
type PreReplaySnapshot = { ptyId: string; at: number; aboveScreen: string; screen: string }
|
||||
const preReplay = new Map<string, PreReplaySnapshot>()
|
||||
/** The host's subscribe/recovery replay keeps this many rows of history. */
|
||||
const HOST_REPLAY_ROWS = 1000
|
||||
/** A sleep that follows a resubscribe within this long uses what the pane held before it. */
|
||||
const PRE_REPLAY_MAX_AGE_MS = 10 * 60_000
|
||||
|
||||
@@ -219,7 +218,8 @@ export function noteBeforeCloudReplay(
|
||||
return null
|
||||
}
|
||||
const normal = terminal.buffer?.normal
|
||||
if (!normal || normal.length - terminal.rows <= HOST_REPLAY_ROWS) {
|
||||
// Any scrollback: a pane woken from sleep can hold output the host never had (V3-02), at any depth.
|
||||
if (!normal || normal.baseY <= 0) {
|
||||
return null
|
||||
}
|
||||
return (async () => {
|
||||
|
||||
Reference in New Issue
Block a user