mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-10 16:21:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5040355156 |
No files matched your search
@@ -877,6 +877,7 @@ jobs:
|
||||
--bare -p --permission-mode dontAsk \
|
||||
--settings "$HOME/.claude/settings.json" \
|
||||
--allowedTools "Read" "Bash(web-search *)" "Bash(web-fetch *)" \
|
||||
"Bash($research_dir/web-search *)" "Bash($research_dir/web-fetch *)" \
|
||||
--disallowedTools "WebSearch" "WebFetch" "Edit" "Write" "NotebookEdit" \
|
||||
--output-format stream-json --verbose --model "$AI_MODEL" \
|
||||
"$prompt"
|
||||
@@ -1695,6 +1696,7 @@ jobs:
|
||||
--bare -p --permission-mode dontAsk \
|
||||
--settings "$HOME/.claude/settings.json" \
|
||||
--allowedTools "Read" "Bash(web-search *)" "Bash(web-fetch *)" \
|
||||
"Bash($research_dir/web-search *)" "Bash($research_dir/web-fetch *)" \
|
||||
--disallowedTools "WebSearch" "WebFetch" "Edit" "Write" "NotebookEdit" \
|
||||
--output-format stream-json --verbose --model "$AI_MODEL" \
|
||||
"$prompt"
|
||||
|
||||
@@ -252,8 +252,6 @@ import {
|
||||
shouldStartTerminalBackend,
|
||||
} from "./terminal/restoredSessionGate";
|
||||
import {
|
||||
alignTerminalViewportScroll,
|
||||
createSynchronizedOutputFitScheduler,
|
||||
AUTO_RUN_SNIPPET_LINE_DELAY_MS,
|
||||
forceSyncRenderAfterResize,
|
||||
MAX_CONNECTION_LOG_DATA_CHARS,
|
||||
@@ -2276,7 +2274,6 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
clearHibernateRetry();
|
||||
clearAutoReconnect();
|
||||
void cleanupSession();
|
||||
synchronizedFitSchedulerRef.current?.dispose();
|
||||
disposeRuntimeOnly();
|
||||
};
|
||||
|
||||
@@ -2870,12 +2867,6 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
});
|
||||
}, [reuseConnectionFromSessionId, sessionId, terminalBackend]);
|
||||
|
||||
const synchronizedFitSchedulerRef = useRef<ReturnType<typeof createSynchronizedOutputFitScheduler> | null>(null);
|
||||
if (!synchronizedFitSchedulerRef.current) {
|
||||
synchronizedFitSchedulerRef.current = createSynchronizedOutputFitScheduler();
|
||||
}
|
||||
useEffect(() => () => synchronizedFitSchedulerRef.current?.dispose(), []);
|
||||
|
||||
type SafeFitOptions = { force?: boolean; requireVisible?: boolean; immediate?: boolean; allowHidden?: boolean };
|
||||
const pendingWriteSafeFitRef = useRef<{
|
||||
term: XTerm;
|
||||
@@ -2973,14 +2964,6 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the frozen buffer intact until synchronized output finishes.
|
||||
// Re-enter safeFit to use the latest size, buffer and reading position.
|
||||
if (synchronizedFitSchedulerRef.current?.defer(term, () => {
|
||||
if (termRef.current === term) {
|
||||
safeFitRef.current({ ...options, force: true, immediate: true });
|
||||
}
|
||||
})) return;
|
||||
|
||||
const buffer = term.buffer.active;
|
||||
const wasPinnedToBottom = buffer.viewportY >= buffer.baseY;
|
||||
const savedViewportY = buffer.viewportY;
|
||||
@@ -3005,10 +2988,6 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
forceSyncRenderAfterResize(term);
|
||||
}
|
||||
|
||||
// Reflow changes the buffer row before xterm updates its scroll range.
|
||||
// Align first so the relative restore below does not apply that delta twice.
|
||||
alignTerminalViewportScroll(term);
|
||||
|
||||
// Preserve scroll position across resize (superset/Tabby pattern).
|
||||
if (wasPinnedToBottom) {
|
||||
term.scrollToBottom();
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import type { Terminal as XTerm } from "@xterm/xterm";
|
||||
import { alignTerminalViewportScroll } from "./terminalHelpers";
|
||||
|
||||
type ViewportSpy = {
|
||||
scrollToLine: (line: number, disableSmoothScroll?: boolean) => void;
|
||||
_sync?: () => void;
|
||||
calls: Array<{ line: number; disableSmoothScroll?: boolean }>;
|
||||
};
|
||||
|
||||
const createTerm = (
|
||||
viewportY: number,
|
||||
viewport?: ViewportSpy | null,
|
||||
): XTerm => ({
|
||||
buffer: { active: { viewportY } },
|
||||
_core: viewport === null ? undefined : { _viewport: viewport },
|
||||
}) as unknown as XTerm;
|
||||
|
||||
test("alignTerminalViewportScroll snaps the viewport back to the buffer row without smooth scrolling", () => {
|
||||
const calls: Array<{ line: number; disableSmoothScroll?: boolean }> = [];
|
||||
const viewport: ViewportSpy = {
|
||||
scrollToLine: (line, disableSmoothScroll) => {
|
||||
calls.push({ line, disableSmoothScroll });
|
||||
},
|
||||
calls,
|
||||
};
|
||||
const term = createTerm(247, viewport);
|
||||
|
||||
alignTerminalViewportScroll(term);
|
||||
|
||||
assert.deepEqual(calls, [{ line: 247, disableSmoothScroll: true }]);
|
||||
});
|
||||
|
||||
test("alignTerminalViewportScroll syncs viewport dimensions before setting the position", () => {
|
||||
const calls: Array<{ line: number; disableSmoothScroll?: boolean }> = [];
|
||||
const order: string[] = [];
|
||||
const viewport: ViewportSpy = {
|
||||
_sync: () => {
|
||||
order.push("sync");
|
||||
},
|
||||
scrollToLine: (line, disableSmoothScroll) => {
|
||||
order.push("scrollToLine");
|
||||
calls.push({ line, disableSmoothScroll });
|
||||
},
|
||||
calls,
|
||||
};
|
||||
const term = createTerm(247, viewport);
|
||||
|
||||
alignTerminalViewportScroll(term);
|
||||
|
||||
assert.deepEqual(order, ["sync", "scrollToLine"]);
|
||||
assert.deepEqual(calls, [{ line: 247, disableSmoothScroll: true }]);
|
||||
});
|
||||
|
||||
test("alignTerminalViewportScroll survives a failing dimension sync", () => {
|
||||
const calls: Array<{ line: number; disableSmoothScroll?: boolean }> = [];
|
||||
const viewport: ViewportSpy = {
|
||||
_sync: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
scrollToLine: (line, disableSmoothScroll) => {
|
||||
calls.push({ line, disableSmoothScroll });
|
||||
},
|
||||
calls,
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => alignTerminalViewportScroll(createTerm(5, viewport)));
|
||||
assert.deepEqual(calls, [{ line: 5, disableSmoothScroll: true }]);
|
||||
});
|
||||
|
||||
test("alignTerminalViewportScroll is a no-op when the private viewport is unavailable", () => {
|
||||
const term = createTerm(10, null);
|
||||
|
||||
assert.doesNotThrow(() => alignTerminalViewportScroll(term));
|
||||
});
|
||||
|
||||
test("alignTerminalViewportScroll survives a viewport without scrollToLine", () => {
|
||||
const term = {
|
||||
buffer: { active: { viewportY: 5 } },
|
||||
_core: { _viewport: {} },
|
||||
} as unknown as XTerm;
|
||||
|
||||
assert.doesNotThrow(() => alignTerminalViewportScroll(term));
|
||||
});
|
||||
|
||||
test("alignTerminalViewportScroll swallows viewport errors instead of breaking the fit", () => {
|
||||
const term = {
|
||||
buffer: { active: { viewportY: 5 } },
|
||||
_core: {
|
||||
_viewport: {
|
||||
scrollToLine: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as XTerm;
|
||||
|
||||
assert.doesNotThrow(() => alignTerminalViewportScroll(term));
|
||||
});
|
||||
@@ -539,84 +539,3 @@ export function forceSyncRenderAfterResize(term: XTerm): void {
|
||||
logger.warn("Sync render after resize failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
type XTermWithPrivateViewport = XTerm & {
|
||||
_core?: {
|
||||
_viewport?: {
|
||||
scrollToLine?: (line: number, disableSmoothScroll?: boolean) => void;
|
||||
_sync?: () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-align the DOM scroll position with the buffer's viewport row.
|
||||
*
|
||||
* xterm's reflow adjusts the buffer's viewport row (ydisp) during resize, but
|
||||
* the scrollable viewport keeps its stale pixel offset. Any subsequent
|
||||
* relative scroll (wheel, scrollToLine) then applies its delta twice — once
|
||||
* against the buffer and once against the stale DOM offset — drifting the
|
||||
* reading position (all the way to the top while shrinking, #3299). Snapping
|
||||
* the viewport back to the buffer row before a relative restore removes the
|
||||
* desync.
|
||||
*/
|
||||
export function alignTerminalViewportScroll(term: XTerm): void {
|
||||
const viewport = (term as XTermWithPrivateViewport)._core?._viewport;
|
||||
const scrollToLine = viewport?.scrollToLine;
|
||||
if (typeof scrollToLine !== "function") return;
|
||||
|
||||
// After a resize, xterm only refreshes the viewport's scroll dimensions on
|
||||
// its queued render callback. Setting a scroll position against the stale
|
||||
// dimensions gets clamped to the old maximum while xterm records the
|
||||
// requested row, so the queued sync then assumes the position was already
|
||||
// applied and the DOM offset stays stale — the next wheel scroll jumps
|
||||
// upward by the resize delta. Sync the dimensions now, before positioning.
|
||||
// If synchronized output (DECSET 2026) is active, _sync() above is a no-op
|
||||
// that merely defers DOM scroll updates until the mode ends; positioning
|
||||
// here would still record the requested row as _latestYDisp against the
|
||||
// stale dimensions, and the deferred sync would then see
|
||||
// ydisp === _latestYDisp and skip repositioning, leaving a stale DOM
|
||||
// offset. Leave positioning to that deferred sync instead: after reflow the
|
||||
// buffer's ydisp differs from the recorded _latestYDisp, so it repositions
|
||||
// with fresh dimensions on its own.
|
||||
if (typeof viewport._sync === "function") {
|
||||
try {
|
||||
viewport._sync.call(viewport);
|
||||
} catch (err) {
|
||||
logger.warn("Sync viewport dimensions after resize failed", err);
|
||||
}
|
||||
}
|
||||
if (term.modes?.synchronizedOutputMode) return;
|
||||
|
||||
try {
|
||||
scrollToLine.call(viewport, term.buffer.active.viewportY, true);
|
||||
} catch (err) {
|
||||
logger.warn("Align viewport scroll after resize failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
/** Defer the whole fit while xterm keeps the visible frame frozen (DECSET 2026). */
|
||||
export function createSynchronizedOutputFitScheduler() {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const dispose = () => {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
return {
|
||||
dispose,
|
||||
defer(term: XTerm, fit: () => void): boolean {
|
||||
dispose();
|
||||
if (!term.modes.synchronizedOutputMode) return false;
|
||||
// Arms xterm's own synchronized-output timeout even if the program
|
||||
// only enabled the mode without writing visible content afterward.
|
||||
term.refresh(0, Math.max(0, term.rows - 1));
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
// The caller re-enters safeFit and checks the mode again, including
|
||||
// when another synchronized frame started before this retry.
|
||||
fit();
|
||||
}, 32);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/* global __dirname, process, setTimeout, clearTimeout, console */
|
||||
// Opt-in real Claude Code permission check; all model traffic stays on loopback.
|
||||
// Usage: node scripts/fixtures/research-permission-probe.cjs [claude executable]
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { prepareAiCliSettings } = require('../ai-automation.cjs');
|
||||
const repository = path.resolve(__dirname, '../..');
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'netcatty-research-permission-'));
|
||||
const settings = path.join(root, 'settings.json');
|
||||
prepareAiCliSettings({ configPath: settings, denyWeb: true, allowBrave: true, allowWrites: false });
|
||||
for (const name of ['web-search', 'web-fetch', 'unrelated-helper']) {
|
||||
fs.writeFileSync(path.join(root, name), '#!/bin/sh\nprintf RESEARCH_FIXTURE_OK\n', { mode: 0o755 });
|
||||
}
|
||||
const workflow = fs.readFileSync(path.join(repository, '.github/workflows/ai-automation.yml'), 'utf8');
|
||||
const blocks = [...workflow.matchAll(/--allowedTools "Read" "Bash\(web-search \*\)"([\s\S]*?)--disallowedTools/g)];
|
||||
assert.equal(blocks.length, 2, 'classification and follow-up research must both be checked');
|
||||
let command;
|
||||
const server = http.createServer((req, res) => {
|
||||
let raw = '';
|
||||
req.on('data', data => { raw += data; });
|
||||
req.on('end', () => {
|
||||
const body = JSON.parse(raw || '{}');
|
||||
if (req.url.includes('count_tokens')) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end('{"input_tokens":10}');
|
||||
return;
|
||||
}
|
||||
const finished = (body.messages || []).some(message => Array.isArray(message.content)
|
||||
&& message.content.some(item => item.type === 'tool_result'));
|
||||
const block = finished ? { type: 'text', text: 'DONE' }
|
||||
: { type: 'tool_use', id: 'tool_fixture', name: 'Bash', input: { command } };
|
||||
const message = { id: 'msg_fixture', type: 'message', role: 'assistant', model: body.model,
|
||||
content: [block], stop_reason: finished ? 'end_turn' : 'tool_use', stop_sequence: null,
|
||||
usage: { input_tokens: 20, output_tokens: 10 } };
|
||||
if (!body.stream) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify(message));
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
const emit = (type, data) => res.write(`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`);
|
||||
emit('message_start', { message: { ...message, content: [], stop_reason: null } });
|
||||
emit('content_block_start', { index: 0, content_block: finished
|
||||
? { type: 'text', text: '' } : { ...block, input: {} } });
|
||||
emit('content_block_delta', { index: 0, delta: finished
|
||||
? { type: 'text_delta', text: 'DONE' }
|
||||
: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) } });
|
||||
emit('content_block_stop', { index: 0 });
|
||||
emit('message_delta', { delta: { stop_reason: message.stop_reason, stop_sequence: null }, usage: { output_tokens: 10 } });
|
||||
emit('message_stop', {});
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
async function check(index, helper, expectRun, baseline = false) {
|
||||
const allowed = [...blocks[index][0].split('--disallowedTools')[0].matchAll(/"([^"]+)"/g)]
|
||||
.map(match => match[1].replaceAll('$research_dir', root))
|
||||
.filter(rule => !baseline || !rule.includes(root));
|
||||
command = `${path.join(root, helper)} smoke 2>&1 | head -40`;
|
||||
const args = ['--bare', '-p', '--permission-mode', 'dontAsk', '--settings', settings,
|
||||
'--allowedTools', ...allowed, '--disallowedTools', 'WebSearch', 'WebFetch', 'Edit', 'Write', 'NotebookEdit',
|
||||
'--output-format', 'stream-json', '--verbose', '--model', 'claude-sonnet-4-6', 'Run the approved helper once.'];
|
||||
const env = { PATH: `${root}${path.delimiter}${process.env.PATH}`, HOME: process.env.HOME,
|
||||
TMPDIR: process.env.TMPDIR, CLAUDE_CONFIG_DIR: path.join(root, 'config'),
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${server.address().port}`, ANTHROPIC_AUTH_TOKEN: 'synthetic-local-fixture',
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' };
|
||||
let output = '', errors = '';
|
||||
const code = await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.argv[2] || 'claude', args, { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
const timeout = setTimeout(() => child.kill('SIGTERM'), 20000);
|
||||
child.stdout.on('data', data => { output += data; });
|
||||
child.stderr.on('data', data => { errors += data; });
|
||||
child.once('error', error => { clearTimeout(timeout); reject(error); });
|
||||
child.once('exit', value => { clearTimeout(timeout); resolve(value); });
|
||||
});
|
||||
assert.equal(code, 0, errors);
|
||||
const events = output.trim().split('\n').map(line => JSON.parse(line));
|
||||
const result = events.findLast(event => event.type === 'result');
|
||||
assert.ok(result, 'Claude must finish the model/tool turn');
|
||||
const ran = events.some(event => Array.isArray(event.message?.content)
|
||||
&& event.message.content.some(item => item.type === 'tool_result'
|
||||
&& JSON.stringify(item.content).includes('RESEARCH_FIXTURE_OK') && !item.is_error));
|
||||
assert.equal(ran, expectRun, `${index}:${helper}:baseline=${baseline}`);
|
||||
assert.equal((result.permission_denials || []).length, expectRun ? 0 : 1);
|
||||
console.log(JSON.stringify({ route: index === 0 ? 'classification' : 'follow-up', helper, baseline, ran }));
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
for (const index of [0, 1]) {
|
||||
await check(index, 'web-search', false, true);
|
||||
await check(index, 'web-search', true);
|
||||
await check(index, 'web-fetch', true);
|
||||
await check(index, 'unrelated-helper', false);
|
||||
}
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
})().catch(error => { console.error(error.message); process.exitCode = 1; });
|
||||
@@ -1,111 +0,0 @@
|
||||
"use strict";
|
||||
/* global process, __dirname, console */
|
||||
if (!process.versions.electron) {
|
||||
require("node:test")("fit preserves terminal reading position", {
|
||||
skip: "run with Electron for real xterm viewport coverage",
|
||||
}, () => {});
|
||||
} else {
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const esbuild = require("esbuild");
|
||||
const temp = require("../electron/bridges/tempDirBridge.cjs");
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const userData = fs.mkdtempSync(`${temp.getTempFilePath("fit-scroll")}-`);
|
||||
app.setPath("userData", userData);
|
||||
app.on("window-all-closed", () => {});
|
||||
const source = fs.readFileSync(path.join(root, "components/Terminal.tsx"), "utf8");
|
||||
const start = source.indexOf(" const safeFit = (options?: SafeFitOptions) => {");
|
||||
const end = source.indexOf(" const prevIsResizingRef", start);
|
||||
if (start < 0 || end < 0) throw new Error("Cannot extract production safeFit");
|
||||
const safeFitSource = esbuild.transformSync(source.slice(start, end), { loader: "ts" }).code;
|
||||
const bundle = esbuild.buildSync({
|
||||
stdin: {
|
||||
contents: 'export {Terminal} from "@xterm/xterm"; export {alignTerminalViewportScroll, forceSyncRenderAfterResize, createSynchronizedOutputFitScheduler} from "./components/terminal/terminalHelpers";',
|
||||
loader: "ts", resolveDir: root,
|
||||
},
|
||||
bundle: true, format: "cjs", platform: "browser", write: false,
|
||||
define: { "import.meta.env.DEV": "false", "import.meta.env.PROD": "true", "import.meta": "{}" },
|
||||
}).outputFiles[0].text;
|
||||
let win;
|
||||
const cleanup = code => {
|
||||
win?.destroy();
|
||||
fs.rmSync(userData, { recursive: true, force: true });
|
||||
app.exit(code);
|
||||
};
|
||||
void app.whenReady().then(async () => {
|
||||
win = new BrowserWindow({
|
||||
show: true, width: 1000, height: 700,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false, sandbox: false, backgroundThrottling: false },
|
||||
});
|
||||
await win.loadURL("data:text/html,<body style='background:%23111;color:white'><h3>Issue 3299: resize reading position</h3><div id='terminal'></div>");
|
||||
await win.webContents.insertCSS(fs.readFileSync(require.resolve("@xterm/xterm/css/xterm.css"), "utf8"));
|
||||
const result = await win.webContents.executeJavaScript(`(async () => {
|
||||
const assert = require('node:assert/strict');
|
||||
const loaded = {exports:{}};
|
||||
((module,exports)=>{${bundle}})(loaded,loaded.exports);
|
||||
const {Terminal,forceSyncRenderAfterResize,createSynchronizedOutputFitScheduler} = loaded.exports;
|
||||
const alignTerminalViewportScroll = process.env.NETCATTY_FIT_BASELINE ? ()=>{} : loaded.exports.alignTerminalViewportScroll;
|
||||
const wait = ms => new Promise(resolve=>setTimeout(resolve,ms));
|
||||
const term = new Terminal({cols:80,rows:25,scrollback:1000,fontSize:14,allowProposedApi:true,smoothScrollDuration:0});
|
||||
const el = document.getElementById('terminal');
|
||||
el.style.cssText='width:900px;height:560px'; term.open(el);
|
||||
const write = data => new Promise(resolve=>term.write(data,resolve));
|
||||
const ref = current => ({current});
|
||||
const termRef=ref(term), containerRef=ref(el), isRendererActiveRef=ref(true), lastFittedSizeRef=ref(null);
|
||||
const autocompleteRepositionRef=ref(null), xtermRuntimeRef=ref(null), pendingWriteSafeFitRef=ref(null);
|
||||
const synchronizedFitSchedulerRef=ref(createSynchronizedOutputFitScheduler());
|
||||
let dimensions={cols:80,rows:25};
|
||||
const fitAddonRef=ref({proposeDimensions:()=>dimensions});
|
||||
const hasPendingTerminalWrites=()=>false;
|
||||
const XTERM_PERFORMANCE_CONFIG={resize:{useRAF:false}};
|
||||
const logger={warn:(message,error)=>{throw error || new Error(message)}};
|
||||
${safeFitSource}
|
||||
const safeFitRef=ref(safeFit);
|
||||
const fit=(cols,rows)=>{dimensions={cols,rows};safeFit({force:true,immediate:true});};
|
||||
const domRow=()=>term._core._viewport._scrollableElement.getScrollPosition().scrollTop / term._core._renderService.dimensions.css.cell.height;
|
||||
const check=(expected,label)=>{
|
||||
assert.equal(term.buffer.active.viewportY,expected,label+' buffer');
|
||||
assert.ok(Math.abs(domRow()-expected)<0.01,label+' DOM expected '+expected+' actual '+domRow());
|
||||
};
|
||||
await write(Array.from({length:200},(_,i)=>'line '+i+' '+('A B repeated '.repeat(7))).join('\\r\\n'));
|
||||
await wait(100); term.scrollToLine(70); await wait(50);
|
||||
fit(80,16); await wait(80); check(70,'shrink');
|
||||
fit(80,30); await wait(80); check(70,'grow');
|
||||
for(let rows=29;rows>=12;rows--){fit(80,rows);await wait(20);check(70,'drag '+rows);}
|
||||
term.scrollToBottom();await wait(50);fit(80,8);await wait(80);check(term.buffer.active.baseY,'pinned shrink');
|
||||
term.scrollLines(-1);await wait(50);check(term.buffer.active.baseY-1,'first scroll after shrink');
|
||||
term.scrollToLine(70);await wait(50);
|
||||
const oldSize=[term.cols,term.rows];
|
||||
await write('\\x1b[?2026h');fit(100,20);fit(120,24);await wait(80);
|
||||
assert.deepEqual([term.cols,term.rows],oldSize,'no reflow inside synchronized frame');
|
||||
term.scrollLines(-3);await wait(40);const selected=term.buffer.active.viewportY;
|
||||
await write('\\x1b[?2026l');await wait(120);
|
||||
assert.deepEqual([term.cols,term.rows],[120,24]);check(Math.min(selected,term.buffer.active.baseY),'user selected position');
|
||||
await write('\\x1b[?2026h');fit(90,18);await write('\\x1b[?2026l\\x1b[?2026h');await wait(70);
|
||||
assert.equal(term.cols,120,'a new synchronized frame delays retry');
|
||||
await write('\\x1b[?2026l');await wait(100);check(Math.min(selected,term.buffer.active.baseY),'mode reentry');
|
||||
await write('\\r\\n'+Array.from({length:1200},()=> 'A B repeated').join('\\r\\n'));
|
||||
await wait(80);term.scrollToLine(100);await wait(30);
|
||||
await write('\\x1b[?2026h');fit(130,22);await write('\\r\\n'.repeat(5));
|
||||
const afterTrim=term.buffer.active.viewportY;
|
||||
await write('\\x1b[?2026l');await wait(100);check(afterTrim,'scrollback trim');
|
||||
await write('\\x1b[?2026h\\x1b[?1049h');fit(100,20);
|
||||
await write('alternate output\\x1b[?1049l\\x1b[?2026l');await wait(100);
|
||||
check(Math.min(afterTrim,term.buffer.active.baseY),'alternate buffer round trip');
|
||||
await write('\\x1b[?2026h');fit(110,21);await wait(1400);
|
||||
assert.equal(term.modes.synchronizedOutputMode,false,'xterm timeout releases output');
|
||||
assert.equal(term.cols,110,'fit retried after output timeout');
|
||||
const beforeDispose=term.cols;
|
||||
await write('\\x1b[?2026h');fit(140,25);synchronizedFitSchedulerRef.current.dispose();
|
||||
await write('\\x1b[?2026l');await wait(100);assert.equal(term.cols,beforeDispose,'teardown cancels retry');
|
||||
assert.ok(el.querySelector('.xterm-screen').getBoundingClientRect().height > 0,'real rendered terminal');
|
||||
return {passed:['shrink','grow','18 drag steps','bottom and next scroll','synchronized repeated spaced text','user scroll','mode reentry','full scrollback trim','alternate buffer','output timeout','cleanup'],viewportY:term.buffer.active.viewportY,domRow:domRow()};
|
||||
})()`);
|
||||
console.log(JSON.stringify(result));
|
||||
if (process.env.NETCATTY_FIT_SCREENSHOT) {
|
||||
fs.writeFileSync(process.env.NETCATTY_FIT_SCREENSHOT, (await win.webContents.capturePage()).toPNG());
|
||||
}
|
||||
cleanup(0);
|
||||
}).catch(error=>{console.error(error);cleanup(1);});
|
||||
}
|
||||
Reference in new issue
Block a user