fix(plugins): preserve global chat transcript in compact modal

This commit is contained in:
陈奇 committed 2026-10-27 12:51:07 +00:00
1 parent f8616f5d3a
commit 25e0c834da
5 files changed
+106 -24

No files matched your search

+11 -1
View File
@@ -18,10 +18,20 @@ sha256sum /tmp/netcatty-global-agent.ncpkg
```
The reproducible sample SHA-256 is
`e9457dcced5619ed69cdf4e490ec6d1c04c90f85826daa91f5660213956db299`.
`7f2f0b2a1d37344012a14a50e428fa41a2dfbac09db663d638d6a7f0a57b35af`.
Two independent package builds matched. Build scripts and tests are excluded
from the six-file installed package.
The sample reserves transcript space inside the actual 798-by-548-pixel plugin
viewport measured during native QA. In a scheduled native test environment, run
`npm run test:plugin-global-agent-layout` for the bounded render regression. It
loads the real sample HTML/CSS with synthetic history, including the fixture
notice, and checks transcript scrolling, reachable configuration, and visible
mouse Stop. It does not load the plugin script or start an Agent/provider. This
render check remains pending where the normal browser sandbox cannot start;
do not disable the sandbox to run it. Also verify the installed sample visually
at the 1188-by-848-pixel app window before accepting the viewport correction.
## Native fixture acceptance (parent-scheduled GUI only)
1. Use an isolated synthetic profile, no real Notes, hosts, histories or keys.
+18 -18
View File
@@ -4,28 +4,28 @@
* { box-sizing:border-box; }
html, body { height:100%; overflow:hidden; }
body { margin:0; }
main { max-width:760px; height:100%; min-height:0; padding:20px; margin:auto; display:flex; flex-direction:column; gap:12px; }
header { flex:none; }
.badge { color:hsl(var(--primary)); font-size:11px; font-weight:700; letter-spacing:1.6px; }
h1 { font-size:27px; font-weight:650; letter-spacing:-.6px; margin:8px 0; }
main { max-width:760px; height:100%; min-height:0; padding:12px; margin:auto; display:flex; flex-direction:column; gap:8px; }
p { color:hsl(var(--muted-foreground)); margin:4px 0; }
.card { border:1px solid hsl(var(--border)); border-radius:10px; padding:16px 18px; margin:0; background:hsl(var(--muted)); }
main > .card:not(.response) { flex:0 1 auto; min-height:0; overflow-y:auto; }
label { display:block; font-size:12px; font-weight:600; color:hsl(var(--muted-foreground)); margin-bottom:8px; }
#model { font-weight:600; }
select, textarea { width:100%; background:hsl(var(--background)); color:inherit; border:1px solid hsl(var(--border)); padding:10px 12px; border-radius:7px; font:inherit; }
.card { border:1px solid hsl(var(--border)); border-radius:10px; padding:10px; margin:0; background:hsl(var(--muted)); }
.setup { flex:0 1 auto; min-height:0; overflow-y:auto; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:6px 12px; }
.setup > div { min-width:0; }
.setup .actions, #selection { grid-column:1 / -1; margin:0; }
label { display:block; font-size:12px; font-weight:600; color:hsl(var(--muted-foreground)); margin-bottom:3px; }
select, textarea { width:100%; background:hsl(var(--background)); color:inherit; border:1px solid hsl(var(--border)); padding:6px 10px; border-radius:7px; font:14px/1.5 system-ui,sans-serif; }
select:focus,textarea:focus,button:focus-visible { outline:2px solid hsl(var(--primary)); outline-offset:2px; }
textarea { resize:vertical; min-height:84px; max-height:20vh; }
textarea { display:block; resize:vertical; height:64px; min-height:64px; max-height:16vh; }
form { flex:none; margin:0; }
.actions { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-top:12px; }
button { border:1px solid hsl(var(--border)); border-radius:7px; background:transparent; color:inherit; padding:9px 24px; font:inherit; font-weight:600; cursor:pointer; }
.actions { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:8px; }
button { border:1px solid hsl(var(--border)); border-radius:7px; background:transparent; color:inherit; padding:6px 12px; font:14px/1.5 system-ui,sans-serif; font-weight:600; cursor:pointer; }
#send { border-color:transparent; background:hsl(var(--primary)); color:hsl(var(--primary-foreground)); }
button:disabled { opacity:.42; cursor:default; }
#choose-reference { margin-top:10px; }
#status { margin-left:4px; color:hsl(var(--muted-foreground)); font-size:12px; }
.response { flex:1 1 160px; min-height:0; display:flex; flex-direction:column; overflow:hidden; }
pre { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; white-space:pre-wrap; overflow-wrap:anywhere; font:16px/1.7 system-ui,sans-serif; margin:0; }
.hint,footer { font-size:12px; color:hsl(var(--muted-foreground)); margin-top:8px; }
footer { flex:none; padding-top:8px; }
@media(max-width:520px) { main { padding:22px; } h1 { font-size:23px; } }
.response { flex:1 0 190px; min-height:190px; display:grid; grid-template-columns:auto minmax(0,1fr); grid-template-rows:minmax(0,1fr) auto; column-gap:12px; overflow:hidden; }
pre { grid-column:1 / -1; min-height:0; overflow-y:auto; overscroll-behavior:contain; white-space:pre-wrap; overflow-wrap:anywhere; font:14px/1.6 system-ui,sans-serif; margin:0; }
.hint,footer { font-size:12px; color:hsl(var(--muted-foreground)); }
.response .hint { min-width:0; margin:6px 0 0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.hint:empty { display:none; }
main > #qa { flex:none; margin:0; }
footer { flex:none; margin:0; }
@media(max-width:520px) { .setup { grid-template-columns:minmax(0,1fr); } }
@media (forced-colors: active) { button, select, textarea { border:1px solid ButtonText; } }
+5 -5
View File
@@ -1,8 +1,8 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Global Agent Chat</title><link rel="stylesheet" href="panel.css"></head>
<body><main><header><span class="badge">AGENT PLATFORM EXAMPLE</span><h1>Global Agent Chat</h1><p>A conversation independent of the active terminal or workspace.</p><p id="qa" class="hint"></p></header>
<section class="card"><label for="backend">Configured Agent backend</label><select id="backend" disabled></select><button id="refresh" type="button">Refresh backends</button>
<label for="conversation">Owned conversation</label><select id="conversation"><option value="">New conversation</option></select>
<div class="actions"><button id="targets" type="button">Choose terminals</button><button id="open" type="button" disabled>Open conversation</button></div><p id="selection" class="hint">No targets selected yet.</p></section>
<body><main><p id="qa" class="hint"></p>
<section class="card setup"><div><label for="backend">Configured Agent backend</label><select id="backend" disabled></select></div>
<div><label for="conversation">Owned conversation</label><select id="conversation"><option value="">New conversation</option></select></div>
<div class="actions"><button id="refresh" type="button">Refresh backends</button><button id="targets" type="button">Choose terminals</button><button id="open" type="button" disabled>Open conversation</button></div><p id="selection" class="hint">No targets selected yet.</p></section>
<section class="card response"><pre id="transcript" aria-live="polite"></pre><p id="usage" class="hint"></p><p id="activity" class="hint"></p></section>
<form id="compose"><label for="prompt">Message</label><textarea id="prompt" maxlength="16384" placeholder="Ask about the selected terminals"></textarea><div class="actions"><button id="send" type="submit" disabled>Send</button><button id="stop" type="button" disabled>Stop</button><span id="status" role="status">Loading configured backends…</span></div></form>
<footer>Core owns Agent execution and conversation history. Actions follow this plugin's Allow / Ask / Deny setting. Only Catty and Claude Code MCP mode are eligible in this version; other native modes are unavailable here.</footer></main><script src="panel.js"></script></body></html>
<footer>Actions follow this plugin's Allow / Ask / Deny setting.</footer></main><script src="panel.js"></script></body></html>
+1
View File
@@ -70,6 +70,7 @@
"test:ssh-mfa-models:live": "SSH_MFA_LIVE=1 node --test electron/bridges/sshMfaModels.live.test.cjs",
"test:local-file-drag:electron": "electron scripts/local-file-drag.live.test.cjs",
"test:tray-panel-layout": "electron scripts/tray-panel-layout.live.test.cjs",
"test:plugin-global-agent-layout": "electron scripts/plugin-global-agent-layout.live.test.cjs",
"test:xterm-image-resize": "electron scripts/xterm-image-resize.live.test.cjs",
"test:xterm-foreground-intense": "electron scripts/xterm-foreground-intense.live.test.cjs",
"test:terminal-background-rendering": "electron scripts/terminal-background-rendering.live.test.cjs",
@@ -0,0 +1,71 @@
"use strict";
/* global process, __dirname, clearTimeout, setTimeout, console */
// Render only the sample's HTML/CSS with synthetic text. No plugin, host bridge,
// provider, account configuration or Agent runtime is started by this test.
if (!process.versions.electron) {
require("node:test")("global Agent sample preserves transcript and composer space", {
skip: "run npm run test:plugin-global-agent-layout in the scheduled native QA environment",
}, () => {});
} else {
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { app, BrowserWindow, session } = require("electron");
const tempDirBridge = require("../electron/bridges/tempDirBridge.cjs");
const sample = path.join(__dirname, "../examples/plugins/global-agent");
const userData = fs.mkdtempSync(`${tempDirBridge.getTempFilePath("agent-layout-test")}-`);
app.setPath("userData", userData);
app.on("window-all-closed", () => {});
let win;
const finish = (code) => {
clearTimeout(watchdog);
if (win && !win.isDestroyed()) win.destroy();
fs.rmSync(userData, { recursive: true, force: true });
app.exit(code);
};
const watchdog = setTimeout(() => { console.error("AGENT_LAYOUT_FAIL timeout"); finish(1); }, 15_000);
void app.whenReady().then(async () => {
// Inline the real stylesheet and omit panel.js: even a broken layout has no
// path to execute plugin commands or initiate model/credential requests.
const html = fs.readFileSync(path.join(sample, "panel.html"), "utf8")
.replace('<link rel="stylesheet" href="panel.css">', `<style>${fs.readFileSync(path.join(sample, "panel.css"), "utf8")}</style>`)
.replace('<script src="panel.js"></script>', "")
.replace("<head>", '<head><meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'">');
const isolatedSession = session.fromPartition("agent-layout-synthetic");
isolatedSession.webRequest.onBeforeRequest({ urls: ["http://*/*", "https://*/*"] }, (_details, callback) => callback({ cancel: true }));
win = new BrowserWindow({ show: false, useContentSize: true, width: 798, height: 548,
webPreferences: { session: isolatedSession, sandbox: true, contextIsolation: true, nodeIntegration: false } });
await win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
for (const qa of [false, true]) {
const actual = await win.webContents.executeJavaScript(`(() => {
const element = id => document.getElementById(id);
element('qa').textContent = ${JSON.stringify(qa ? "Isolated deterministic fixture · no real model inference or paid API" : "")};
element('transcript').textContent = 'assistant: ' + 'Synthetic owned conversation. '.repeat(100);
element('usage').textContent = 'Input 12 · Output 6';
element('activity').textContent = 'session.environment';
element('status').textContent = 'Complete';
element('stop').disabled = false;
element('backend').innerHTML = '<option>Synthetic configured Catty</option>';
element('conversation').innerHTML = '<option>Owned synthetic conversation</option>';
const bounds = node => { const r = node.getBoundingClientRect(); return { top:r.top, bottom:r.bottom, height:r.height, width:r.width }; };
const transcript = element('transcript');
const setup = document.querySelector('.setup') || element('backend').closest('section');
return { width:innerWidth, height:innerHeight, transcript:bounds(transcript),
transcriptClient:transcript.clientHeight, transcriptScroll:transcript.scrollHeight,
response:bounds(transcript.closest('section')), compose:bounds(element('compose')), stop:bounds(element('stop')),
setup:bounds(setup), setupOverflow:getComputedStyle(setup).overflowY };
})()`);
const detail = JSON.stringify({ qa, ...actual });
assert.equal(actual.width, 798, detail); assert.equal(actual.height, 548, detail);
assert.ok(actual.response.height >= 180, `usable response card: ${detail}`);
assert.ok(actual.transcriptClient >= 120, `visible conversation text: ${detail}`);
assert.ok(actual.transcriptScroll > actual.transcriptClient, `transcript scrolls independently: ${detail}`);
assert.ok(actual.compose.top >= actual.response.bottom - 1, `composer follows transcript: ${detail}`);
assert.ok(actual.stop.bottom <= actual.height && actual.stop.height >= 30, `mouse Stop stays visible: ${detail}`);
assert.ok(actual.setup.height >= 100 && actual.setupOverflow === "auto", `configuration stays reachable: ${detail}`);
console.log(`AGENT_LAYOUT_PASS ${detail}`);
}
finish(0);
}).catch((error) => { console.error("AGENT_LAYOUT_FAIL", error); finish(1); });
}