feat(bridge): establish local pi status bridge
Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
test("renders every Pi RPC extension UI method without replying to fire-and-forget requests", async () => {
|
||||
const source = await readFile("ui/src/App.tsx", "utf8");
|
||||
for (const method of [
|
||||
"select",
|
||||
"confirm",
|
||||
"input",
|
||||
"editor",
|
||||
"notify",
|
||||
"setStatus",
|
||||
"setWidget",
|
||||
"setTitle",
|
||||
"set_editor_text",
|
||||
]) {
|
||||
assert.match(source, new RegExp(`case "${method}"`));
|
||||
}
|
||||
assert.match(source, /setPendingExtension\(extension\)/);
|
||||
assert.doesNotMatch(source, /This extension UI is unsupported/);
|
||||
assert.match(source, /function stripAnsi/);
|
||||
assert.match(source, /stripAnsi\(extension\.statusText\)/);
|
||||
assert.match(source, /className="pi-controls"/);
|
||||
assert.match(source, /aria-label="Model and thinking controls"/);
|
||||
assert.doesNotMatch(source, /<details className="pi-controls">/);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/event\.ctrlKey && event\.key\.toLowerCase\(\) === "c"/,
|
||||
);
|
||||
assert.match(source, /getCurrentWindow\(\)\s*\.hide\(\)\s*\.catch/);
|
||||
assert.match(
|
||||
source,
|
||||
/window\.addEventListener\("keydown", handleKeydown, true\)/,
|
||||
);
|
||||
assert.match(source, /useLayoutEffect/);
|
||||
assert.match(source, /transcript\.scrollTop = transcript\.scrollHeight/);
|
||||
assert.match(source, /requestAnimationFrame\(scrollToLatest\)/);
|
||||
assert.match(source, /const loadGeneration = useRef\(0\)/);
|
||||
assert.match(
|
||||
source,
|
||||
/const lastEventSequence = useRef<Record<string, number>>/,
|
||||
);
|
||||
assert.match(source, /generation !== loadGeneration\.current/);
|
||||
assert.match(source, /current\.phase !== "working"/);
|
||||
assert.match(source, /bridgeEvent\.seq <= \(lastEventSequence\.current/);
|
||||
assert.match(source, /\.toFixed\(1\)/);
|
||||
assert.match(source, /formatTokens\(stats\.tokens\.input\)/);
|
||||
assert.match(source, /formatTokens\(stats\.tokens\.output\)/);
|
||||
assert.match(source, /formatTokens\(cacheTokens\)/);
|
||||
assert.match(source, /function updateWorkProgress/);
|
||||
assert.match(
|
||||
source,
|
||||
/className=\{`work-progress composer-progress \$\{workProgress\.phase\}`\}/,
|
||||
);
|
||||
assert.match(source, /event\.type === "tool"/);
|
||||
assert.match(source, /event\.type === "queue"/);
|
||||
assert.match(source, /composer-progress/);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/<section className=\{`work-progress \$\{workProgress\.phase\}`\}/,
|
||||
);
|
||||
});
|
||||
|
||||
test("makes the window draggable from the non-interactive header", async () => {
|
||||
const [component, styles, capability] = await Promise.all([
|
||||
readFile("ui/src/App.tsx", "utf8"),
|
||||
readFile("ui/src/App.css", "utf8"),
|
||||
readFile("ui/src-tauri/capabilities/default.json", "utf8"),
|
||||
]);
|
||||
assert.match(component, /startDragging\(\)/);
|
||||
assert.match(
|
||||
component,
|
||||
/closest\(\s*"button, input, select, textarea",?\s*\)/,
|
||||
);
|
||||
assert.match(styles, /\.workflow-header \{[\s\S]*cursor: grab/);
|
||||
assert.match(capability, /core:window:allow-start-dragging/);
|
||||
});
|
||||
|
||||
test("opens local follow-up choices for /model and /resume", async () => {
|
||||
const [component, styles] = await Promise.all([
|
||||
readFile("ui/src/App.tsx", "utf8"),
|
||||
readFile("ui/src/App.css", "utf8"),
|
||||
]);
|
||||
assert.match(component, /const controlCommands: Command\[\]/);
|
||||
assert.match(component, /followUp: "model"/);
|
||||
assert.match(component, /followUp: "resume"/);
|
||||
assert.match(component, /followUp: "thinking"/);
|
||||
assert.match(component, /const nativeCommand = controlCommands\.find/);
|
||||
assert.match(component, /function chooseThinking/);
|
||||
assert.match(component, /function openCommandFollowUp/);
|
||||
assert.match(component, /function chooseModel/);
|
||||
assert.match(component, /className="command-followup"/);
|
||||
assert.match(component, /New session/);
|
||||
assert.match(styles, /\.command-followup \{/);
|
||||
assert.match(styles, /\.command-choice-list \{/);
|
||||
});
|
||||
|
||||
test("shows bridge-managed directory status and sessions for the opened directory", async () => {
|
||||
const [component, styles] = await Promise.all([
|
||||
readFile("ui/src/App.tsx", "utf8"),
|
||||
readFile("ui/src/App.css", "utf8"),
|
||||
]);
|
||||
assert.match(
|
||||
component,
|
||||
/invoke<\{ directories: Directory\[\] \}>\("list_directories"\)/,
|
||||
);
|
||||
assert.match(
|
||||
component,
|
||||
/invoke<\{ sessions: Session\[\] \}>\("list_sessions"/,
|
||||
);
|
||||
assert.match(component, /function chooseDirectory/);
|
||||
assert.match(component, /function startNewSession/);
|
||||
assert.match(component, /function switchSession/);
|
||||
assert.match(component, /className="session-panel"/);
|
||||
assert.match(component, /New session/);
|
||||
assert.match(component, /state\.isStreaming/);
|
||||
assert.match(styles, /\.session-panel \{/);
|
||||
assert.match(styles, /\.session-list \{/);
|
||||
});
|
||||
|
||||
test("renders todo-plugin state and compact context/token status", async () => {
|
||||
const [component, styles] = await Promise.all([
|
||||
readFile("ui/src/App.tsx", "utf8"),
|
||||
readFile("ui/src/App.css", "utf8"),
|
||||
]);
|
||||
assert.match(component, /function currentTodos/);
|
||||
assert.match(component, /message\.toolName === "todo"/);
|
||||
assert.match(component, /className="todos-pane"/);
|
||||
assert.match(component, /className="workflow-main"/);
|
||||
assert.match(component, /ctx \{formatTokens\(contextTokens\)\}/);
|
||||
assert.match(component, /tok \{formatTokens\(stats\.tokens\.total\)\}/);
|
||||
assert.match(styles, /\.workflow-main \{/);
|
||||
assert.match(styles, /\.todos-pane \{/);
|
||||
assert.match(styles, /\.status-metric \{/);
|
||||
});
|
||||
|
||||
test("shows an optimistic prompt until the transcript receives it", async () => {
|
||||
const [component, styles] = await Promise.all([
|
||||
readFile("ui/src/App.tsx", "utf8"),
|
||||
readFile("ui/src/App.css", "utf8"),
|
||||
]);
|
||||
assert.match(component, /type PendingSubmission/);
|
||||
assert.match(
|
||||
component,
|
||||
/const \[pendingSubmissions, setPendingSubmissions\]/,
|
||||
);
|
||||
assert.match(component, /setStatus\("Sending prompt to Pi…"\)/);
|
||||
assert.match(component, /phase: "sending"/);
|
||||
assert.match(component, /phase: "sent"/);
|
||||
assert.match(component, /Sent · waiting for Pi/);
|
||||
assert.match(component, /receivedMessages\.some/);
|
||||
assert.match(component, /pending-message/);
|
||||
assert.match(styles, /\.pending-message \{/);
|
||||
assert.match(styles, /\.pending-message\.sending/);
|
||||
});
|
||||
|
||||
test("distinguishes user and assistant messages", async () => {
|
||||
const styles = await readFile("ui/src/App.css", "utf8");
|
||||
assert.match(styles, /\.message\.user \{/);
|
||||
assert.match(styles, /background: #192536/);
|
||||
assert.match(styles, /\.message\.assistant \{/);
|
||||
assert.match(styles, /background: #1a241a/);
|
||||
});
|
||||
|
||||
test("uses a subtle focus treatment for the prompt editor", async () => {
|
||||
const styles = await readFile("ui/src/App.css", "utf8");
|
||||
assert.match(styles, /\.composer textarea:focus \{/);
|
||||
assert.match(styles, /outline: none/);
|
||||
assert.match(styles, /border-color: #6f8b56/);
|
||||
});
|
||||
|
||||
test("submits the composer with Enter and preserves Shift+Enter for newlines", async () => {
|
||||
const source = await readFile("ui/src/App.tsx", "utf8");
|
||||
assert.match(source, /onKeyDown=\{\(event\) => \{/);
|
||||
assert.match(source, /event\.key !== "Enter"/);
|
||||
assert.match(source, /event\.shiftKey/);
|
||||
assert.match(source, /event\.nativeEvent\.isComposing/);
|
||||
assert.match(source, /event\.preventDefault\(\);\s*void submit\(\);/);
|
||||
assert.match(source, /Enter to send · Shift\+Enter for newline/);
|
||||
});
|
||||
|
||||
test("uses a compact workflow layout that keeps the composer and transcript stable", async () => {
|
||||
const [component, styles] = await Promise.all([
|
||||
readFile("ui/src/App.tsx", "utf8"),
|
||||
readFile("ui/src/App.css", "utf8"),
|
||||
]);
|
||||
assert.match(component, /className="workflow-header"/);
|
||||
assert.match(component, /className="workflow"/);
|
||||
assert.match(component, /className="workflow-footer"/);
|
||||
assert.match(component, /className="composer-stack"/);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.workflow \{[\s\S]*grid-template-rows: minmax\(0, 1fr\) auto/,
|
||||
);
|
||||
assert.match(styles, /\.transcript,\s*\.settings \{[\s\S]*overflow: auto/);
|
||||
assert.match(
|
||||
component,
|
||||
/className=\{`transcript \$\{pendingExtension \? "with-extension" : ""\} \$\{workProgress\.phase\}`\}/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.transcript\.with-extension \{[\s\S]*padding-bottom: 112px/,
|
||||
);
|
||||
assert.match(styles, /\.transcript\.working \{/);
|
||||
assert.match(styles, /conic-gradient\(/);
|
||||
assert.match(styles, /transcript-border-orbit/);
|
||||
assert.match(styles, /prefers-reduced-motion: reduce/);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.workflow-main \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) 210px/,
|
||||
);
|
||||
assert.match(styles, /\.pi-controls \{[\s\S]*padding: 4px 6px/);
|
||||
});
|
||||
Reference in New Issue
Block a user