fix(plugins): preserve global chat transcript in compact modal
This commit is contained in:
1 parent
f8616f5d3a
commit
25e0c834da
5 files changed
+106
-24
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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; } }
|
||||
@@ -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>
|
||||
@@ -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); });
|
||||
}
|
||||
Reference in new issue
Block a user