mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-12 10:19:42 +02:00
After a session's first load per page, a tab switch fetched a 1 MiB tail of the server's byte stream. For a fullscreen claude pane (alternate screen, tmux `history_size` 0) that tail is old repaints of one frame, and an idle tab parsed it twice (the cached copy, then the fresh one). Every terminal response now records `paneHistoryLines` per session, and `selectSession` takes `full=1` whenever the last report was 0. For such a pane that capture is the visible frame, a few KB, which is also what a page load already shows. A response without the field forgets the entry, so unknown never counts as empty; a pane that starts keeping history goes back to the tail on the next switch. Shells and the refresh path are unchanged. Measured with real prod payloads through the real client in chromium: median switch 1.26-1.66 s -> 0.10-0.19 s, parse ~1.1-1.5 s -> 2-3 ms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
81 lines
3.4 KiB
TypeScript
81 lines
3.4 KiB
TypeScript
/**
|
|
* @fileoverview What the client remembers about a pane's tmux scrollback.
|
|
*
|
|
* `selectSession` serves a tab switch from the small `full=1` capture when the
|
|
* session's last terminal response reported `paneHistoryLines: 0` (a fullscreen
|
|
* CLI in the alternate screen), instead of the 1 MiB byte tail. The memory that
|
|
* decides it must treat ONLY a reported 0 as empty: a missing field (an older
|
|
* server, a byte-history fallback) has to forget, so the tab switch goes back to
|
|
* the bounded tail rather than trusting a stale answer.
|
|
*
|
|
* Runs the real `_notePaneHistory` / `_clearHistoryTruncation` from app.js in a
|
|
* `vm`. The switch itself is driven end to end in
|
|
* fullscreen-tab-switch-capture.browser.test.ts (browser suite).
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import vm from 'node:vm';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const APP = readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
|
|
|
|
function methodSource(source: string, method: string): string {
|
|
const start = source.search(new RegExp(`^ {2}(?:async )?${method}\\(`, 'm'));
|
|
expect(start, `${method} not found`).toBeGreaterThan(-1);
|
|
const next = /^ {2}(?:async )?[A-Za-z_$][\w$]*\(/m.exec(source.slice(start + 1));
|
|
return next ? source.slice(start, start + 1 + next.index) : source.slice(start);
|
|
}
|
|
|
|
function makeApp() {
|
|
const methods = vm.runInContext(
|
|
`({ ${methodSource(APP, '_notePaneHistory')}, ${methodSource(APP, '_clearHistoryTruncation')} })`,
|
|
vm.createContext({})
|
|
) as Record<string, (...args: unknown[]) => void>;
|
|
return {
|
|
activeSessionId: null,
|
|
_paneHistoryLines: new Map<string, number>(),
|
|
_renderHistoryTruncationBanner() {},
|
|
...methods,
|
|
} as Record<string, any>;
|
|
}
|
|
|
|
describe('_notePaneHistory', () => {
|
|
it('records the count a capture reported, 0 included', () => {
|
|
const app = makeApp();
|
|
app._notePaneHistory('s1', { paneHistoryLines: 0 });
|
|
app._notePaneHistory('s2', { paneHistoryLines: 40000 });
|
|
expect(app._paneHistoryLines.get('s1')).toBe(0);
|
|
expect(app._paneHistoryLines.get('s2')).toBe(40000);
|
|
});
|
|
|
|
it('forgets on a response without a usable count, so unknown never reads as empty', () => {
|
|
const app = makeApp();
|
|
for (const payload of [{}, { paneHistoryLines: null }, { paneHistoryLines: NaN }, { paneHistoryLines: -1 }, null]) {
|
|
app._notePaneHistory('s1', { paneHistoryLines: 0 });
|
|
app._notePaneHistory('s1', payload);
|
|
expect(app._paneHistoryLines.has('s1')).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('ignores a call without a session', () => {
|
|
const app = makeApp();
|
|
app._notePaneHistory(null, { paneHistoryLines: 0 });
|
|
expect(app._paneHistoryLines.size).toBe(0);
|
|
});
|
|
|
|
it('is dropped with the session', () => {
|
|
const app = makeApp();
|
|
app._notePaneHistory('s1', { paneHistoryLines: 0 });
|
|
app._clearHistoryTruncation('s1');
|
|
expect(app._paneHistoryLines.has('s1')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('every terminal response feeds it (static guard)', () => {
|
|
it('the tab switch, the refresh and the history re-pull all record the pane', () => {
|
|
expect(methodSource(APP, 'selectSession')).toContain('this._notePaneHistory?.(sessionId, data);');
|
|
expect(methodSource(APP, '_onSessionNeedsRefresh')).toContain('this._notePaneHistory?.(sessionId, data);');
|
|
expect(methodSource(APP, '_maybeRefetchFullHistory')).toContain('this._notePaneHistory?.(sessionId, payload);');
|
|
});
|
|
});
|