Merge branch 'main' of github.com:getpaseo/paseo

This commit is contained in:
Mohamed Boudra
2026-07-24 12:48:03 +02:00
13 changed files with 252 additions and 63 deletions
+3 -2
View File
@@ -459,6 +459,7 @@ export function FilePane({
preview,
supportsEditing,
});
const canToggleMarkdownMode = isMarkdown && editable;
const lineCount =
preview?.kind === "text" ? (preview.content ?? "").split("\n").length : undefined;
const errorMessage = getFileErrorMessage(query.error, t("panels.file.failedToLoad"));
@@ -471,8 +472,8 @@ export function FilePane({
preview={preview}
version={version}
filename={getFileNameFromPath(location.path) ?? location.path}
markdownMode={isMarkdown ? markdownMode : undefined}
onMarkdownModeChange={isMarkdown ? setMarkdownMode : undefined}
markdownMode={canToggleMarkdownMode ? markdownMode : undefined}
onMarkdownModeChange={canToggleMarkdownMode ? setMarkdownMode : undefined}
lineCount={lineCount}
editable={editable}
disconnectedMessage={t("workspace.terminal.hostDisconnected")}
+8 -2
View File
@@ -4,6 +4,7 @@ import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpas
import { tryConnectToDaemon } from "../../utils/client.js";
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
import { addJsonOption } from "../../utils/command-options.js";
import { formatPairingInstructions } from "../../output/pairing.js";
interface PairOptions {
home?: string;
@@ -97,6 +98,11 @@ function outputPairingResult(
return;
}
const qrBlock = pairing.qr ? `${pairing.qr}\n` : "";
process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`);
process.stdout.write(
formatPairingInstructions({
url: pairing.url,
qr: pairing.qr,
columns: process.stdout.columns,
}),
);
}
+7 -4
View File
@@ -18,6 +18,7 @@ import {
type DaemonStartOptions,
} from "./daemon/local-daemon.js";
import { tryConnectToDaemon } from "../utils/client.js";
import { formatPairingInstructions } from "../output/pairing.js";
interface OnboardOptions extends DaemonStartOptions {
timeout?: string;
@@ -520,11 +521,13 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
return;
}
renderNote(
pairing.qr ?? "QR is unavailable in this terminal. Use the pairing link below.",
"Scan to pair",
process.stdout.write(
formatPairingInstructions({
url: pairing.url,
qr: pairing.qr,
columns: process.stdout.columns,
}),
);
renderNote(pairing.url, "Pairing link");
printNextSteps(pairing.url, paseoHome, richUi);
if (richUi) {
outro("Paseo is ready!");
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { formatPairingInstructions } from "./pairing.js";
const QR = "\u001b[47m\u001b[30m \n ████ \n \u001b[0m";
const URL = "https://app.paseo.sh/#offer=pairing-offer";
describe("formatPairingInstructions", () => {
it("prints the QR and an unmodified pairing-link line when the terminal is wide enough", () => {
const output = formatPairingInstructions({ qr: QR, url: URL, columns: 7 });
expect(output).toContain(QR);
expect(output.split("\n")).toContain(URL);
});
it("does not print a QR that would reach the terminal edge", () => {
const output = formatPairingInstructions({ qr: QR, url: URL, columns: 6 });
expect(output).not.toContain(QR);
expect(output).toContain("Resize the terminal to at least 7 columns");
expect(output.split("\n")).toContain(URL);
});
it("does not risk printing a QR when terminal width is unknown", () => {
const output = formatPairingInstructions({ qr: QR, url: URL });
expect(output).not.toContain(QR);
expect(output).toContain("terminal width could not be detected");
expect(output.split("\n")).toContain(URL);
});
});
+37
View File
@@ -0,0 +1,37 @@
const ANSI_PATTERN = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, "g");
interface PairingInstructions {
url: string;
qr: string | null;
columns?: number;
}
function visibleWidth(value: string): number {
return Math.max(
...value
.replace(ANSI_PATTERN, "")
.split("\n")
.map((line) => line.length),
);
}
function formatQr(qr: string | null, columns: number | undefined): string {
if (!qr) {
return "QR code is unavailable. Use the pairing link below.";
}
if (columns === undefined) {
return "QR code not shown because terminal width could not be detected.";
}
const width = visibleWidth(qr);
if (columns <= width) {
return `QR code not shown. Resize the terminal to at least ${width + 1} columns, then run this command again.`;
}
return qr;
}
export function formatPairingInstructions({ url, qr, columns }: PairingInstructions): string {
return `\nScan to pair:\n${formatQr(qr, columns)}\n\nPairing link:\n${url}\n`;
}
@@ -277,6 +277,35 @@ describe("OMP agent client and session", () => {
]);
});
test("renders a live system-notice custom message as a synthetic tool call", async () => {
const omp = new OmpHarness();
await omp.start();
await omp.runPrompt("hello OMP", "done");
omp
.runtime()
.acceptCustomMessage(
[
"<system-notice>",
"Background job DocsSmokeTwo has completed.",
'<task-result id="DocsSmokeTwo" agent="explore" status="completed" duration="21.6s">',
"<output>done</output>",
"</task-result>",
"</system-notice>",
].join("\n"),
);
omp.runtime().acceptCustomMessage("plain custom status text");
expect(omp.timeline().filter((item) => item.type === "tool_call")).toMatchObject([
{ callId: "omp-notice:DocsSmokeTwo", name: "task_notification", status: "completed" },
]);
// Non-notice custom messages still fall through as assistant messages.
expect(omp.timeline().filter((item) => item.type === "assistant_message")).toMatchObject([
{ text: "done" },
{ text: "plain custom status text" },
]);
});
test("does not complete a queued model turn from OMP's local-only hint", async () => {
const omp = new OmpHarness();
await omp.start();
@@ -68,6 +68,7 @@ export { formatOmpVersionSupport, resolveOmpDiagnosticPaths } from "./provider-c
import { OmpSubagentCardTracker, type OmpSubagentCardScheduler } from "./subagent-card-tracker.js";
import { shouldDisplayOmpCustomMessage } from "./custom-message.js";
import { getUserMessageText } from "./message-history.js";
import { mapOmpSystemNoticeToToolCall } from "./system-notice.js";
import { materializeProviderImage } from "../provider-image-output.js";
import { OmpCliRuntime } from "./cli-runtime.js";
import { listOmpImportableSessions, readOmpImportSessionConfig } from "./session-descriptor.js";
@@ -2068,12 +2069,14 @@ export class OmpAgentSession implements AgentSession {
if (shouldDisplayOmpCustomMessage(event.message)) {
const text = getUserMessageText(event.message.content);
if (text) {
const advisorItem = mapOmpAdvisorMessageToToolCall(event.message, text);
const item =
mapOmpAdvisorMessageToToolCall(event.message, text) ??
mapOmpSystemNoticeToToolCall(text);
this.emit({
type: "timeline",
provider: this.provider,
turnId,
item: advisorItem ?? { type: "assistant_message", text },
item: item ?? { type: "assistant_message", text },
});
}
}
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import * as QRCode from "qrcode";
import { renderPairingQr } from "./pairing-qr.js";
const ESCAPE = String.fromCharCode(0x1b);
const ANSI_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
describe("renderPairingQr", () => {
it("renders a theme-independent QR code with a four-module quiet zone", async () => {
const url = "https://app.paseo.sh/#offer=test-pairing-offer";
const qr = await renderPairingQr(url);
const styledLines = qr.split("\n");
const visibleLines = qr.replace(ANSI_PATTERN, "").split("\n");
const moduleCount = QRCode.create(url).modules.size;
expect(
styledLines.every(
(line) => line.startsWith(`${ESCAPE}[47m${ESCAPE}[30m`) && line.endsWith(`${ESCAPE}[0m`),
),
).toBe(true);
expect(visibleLines.every((line) => line.length === moduleCount + 8)).toBe(true);
expect(visibleLines.slice(0, 2).every((line) => line.trim() === "")).toBe(true);
expect(visibleLines.slice(-2).every((line) => line.trim() === "")).toBe(true);
expect(visibleLines.every((line) => line.startsWith(" ") && line.endsWith(" "))).toBe(
true,
);
});
});
+9 -41
View File
@@ -1,50 +1,18 @@
import * as QRCode from "qrcode";
import type { QRCodeToStringOptionsTerminal, QRCodeToStringOptionsOther } from "qrcode";
import type { Logger } from "pino";
import type { QRCodeToStringOptionsOther } from "qrcode";
function parseBooleanEnv(value: string | undefined): boolean | undefined {
if (value === undefined) return undefined;
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
return undefined;
}
function shouldPrintPairingQr(): boolean {
const env = parseBooleanEnv(process.env.PASEO_PAIRING_QR);
if (env !== undefined) return env;
return process.stdout.isTTY ?? false;
}
const BLACK_ON_WHITE = "\u001b[47m\u001b[30m";
const RESET_COLORS = "\u001b[0m";
export async function renderPairingQr(url: string): Promise<string> {
const terminalOptions: QRCodeToStringOptionsTerminal = {
type: "terminal",
small: true,
};
const utf8Options: QRCodeToStringOptionsOther = {
type: "utf8",
margin: 4,
};
try {
return await QRCode.toString(url, terminalOptions);
} catch {
return await QRCode.toString(url, utf8Options);
}
}
export async function printPairingQrIfEnabled(args: {
url: string;
logger?: Logger;
}): Promise<void> {
if (!shouldPrintPairingQr()) return;
const qr = await renderPairingQr(args.url);
const out = `\nScan to pair:\n${qr}\n${args.url}\n`;
try {
process.stdout.write(out);
} catch (error) {
args.logger?.debug({ error }, "Failed to print pairing QR");
}
const qr = await QRCode.toString(url, utf8Options);
return qr
.split("\n")
.map((line) => `${BLACK_ON_WHITE}${line}${RESET_COLORS}`)
.join("\n");
}