Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] deb1bdc266 test(plugins): wait for global recovery without an extra restart (#8250)
* test(plugins): observe global recovery without reloading again

* test(plugins): leave time for recovery failure cleanup

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-30 03:38:37 +00:00
Matěj Pešl 9c7925de58 fix(skills): make invocation input portable across harnesses (#8252)
Read invocation input and conversation context without Claude argument substitution. Recognize Pi skill references when forwarding advisor requests.
2026-10-30 03:01:41 +00:00
4351445467 Keep other windows in sync when an agent is deleted (#8248)
* fix(server): broadcast agent removals across logical sessions

* test(server): make subscription fixtures portable on Windows

The newly enabled integration suite assumed /bin/cat and a one-second reply deadline, so Windows CI failed before completing its subscription coverage. Use the running Node executable for echo terminals and let diagnostics finish their bounded external tool probes.

Co-authored-by: loganmeyer <50949542+loganmeyer@users.noreply.github.com>

---------

Co-authored-by: loganmeyer <50949542+loganmeyer@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <paseo-bot[bot]@users.noreply.github.com>
2026-10-30 00:05:34 +00:00
paseo-bot[bot] 9503537041 test(server): exclude loader activity from config responsiveness (#8218)
* test(server): trace recurring config responsiveness failures

* test(server): keep loader messages out of config responsiveness probes

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-29 21:02:58 +00:00
paseo-bot[bot] 1aef1ec662 test(server): isolate timeline fetch responsiveness from runner work (#8197)
* test(server): isolate timeline fetch responsiveness from runner work

Measure the existing cold tail workload in a separate process after fixture collection. Keep the 50ms gate and row assertions, and reproduce unrelated callback CPU contaminating the old measurement.

* test(server): keep timeline probe private and exercise overlapping CPU

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-29 12:21:29 +00:00
946b6e58fd Keep transcript colors in sync after a hidden theme change (#8199)
* fix(app): preserve adaptive theme events while hidden

Read the current scheme through fresh matchMedia lists so hidden runtime
reads do not consume the pending events on listener-owned lists.

Carry Dan Sabin's fix from #7172 into the current combined Unistyles patch,
and cover both transition directions with a real Electron regression in
the existing desktop browser suite.

Co-authored-by: Dan Sabin <dan@freethinking.it>

* test(desktop): run adaptive theme regression on Linux

Electron retains --no-sandbox in argv, so the positional lookup used the script filename as the artifact directory. Read the final argument so the same real Electron regression exercises Linux as well as macOS.

Co-authored-by: Nurşat Bülbül <85260051+Nurshot@users.noreply.github.com>
Co-authored-by: Dan Sabin <dan@freethinking.it>

---------

Co-authored-by: Nurshot <85260051+Nurshot@users.noreply.github.com>
Co-authored-by: Dan Sabin <dan@freethinking.it>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-29 11:16:48 +00:00
20 changed files with 609 additions and 81 deletions
+8
View File
@@ -358,6 +358,14 @@ export to `src`, but browser and Electron Metro resolve its `browser` export to
`lib/module`; CommonJS consumers use `lib/commonjs`. A source-only patch leaves `lib/module`; CommonJS consumers use `lib/commonjs`. A source-only patch leaves
Electron running the old code. Electron running the old code.
The adaptive-theme patch keeps `colorScheme` reads off the media-query lists
owned by Unistyles' change listeners. Chromium defers their events while a window
is hidden; reading `matches` on those same lists can consume the pending change.
CSS variables then follow the OS theme while `withUnistyles` props retain the old
colors. Keep this patch until upstream reads the scheme through separate lists.
The desktop browser E2E checks both transition directions in an isolated Electron
process, because Playwright's focus emulation prevents real hidden-page behavior.
Register every dependency patch in `scripts/postinstall-patches.mjs`. A file in Register every dependency patch in `scripts/postinstall-patches.mjs`. A file in
`patches/` is inert unless that script knows which installed package activates `patches/` is inert unless that script knows which installed package activates
it. it.
@@ -0,0 +1,158 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import electronPath from "electron";
import { build } from "esbuild";
export async function runAdaptiveThemeRegression(artifactDir) {
const { outputFiles } = await build({
stdin: {
resolveDir: fileURLToPath(new URL("../../app", import.meta.url)),
contents: `
import { createElement } from "react";
import { createRoot } from "react-dom/client";
import { StyleSheet, UnistylesRuntime, withUnistyles } from "react-native-unistyles";
StyleSheet.configure({
themes: {
dark: { foreground: "#fafafa", background: "#181b1a" },
light: { foreground: "#1a1a1e", background: "#ffffff" },
},
settings: { adaptiveThemes: true },
});
const Message = withUnistyles(({ color }) =>
createElement("p", { style: { color } }, "Theme-reactive message"));
createRoot(document.getElementById("root")).render(createElement(Message, {
style: { margin: 0 },
uniProps: theme => ({ color: theme.foreground }),
}));
window.readThemeForTest = () => UnistylesRuntime.themeName;
`,
},
bundle: true,
write: false,
format: "iife",
platform: "browser",
alias: { "react-native": "react-native-web" },
define: { __DEV__: "false" },
resolveExtensions: [".web.js", ".js", ".json"],
});
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-adaptive-theme-"));
try {
await fs.writeFile(path.join(directory, "renderer.js"), outputFiles[0].text);
await fs.writeFile(path.join(directory, "main.cjs"), `(${electronRegression.toString()})()`);
// A separate process avoids Playwright's focus emulation, which keeps hidden pages visible.
const env = { ...process.env };
delete env.ELECTRON_RUN_AS_NODE;
const args = [
...(process.platform === "linux" ? ["--no-sandbox"] : []),
path.join(directory, "main.cjs"),
artifactDir,
];
const command = process.platform === "linux" ? "xvfb-run" : electronPath;
const commandArgs =
process.platform === "linux"
? ["-a", "--server-args=-screen 0 1280x800x24", electronPath, ...args]
: args;
await promisify(execFile)(command, commandArgs, {
env,
timeout: 30_000,
});
return JSON.parse(await fs.readFile(path.join(artifactDir, "adaptive-theme.json"), "utf8"));
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
}
async function electronRegression() {
const { app, BrowserWindow, nativeTheme } = require("electron");
const assert = require("node:assert/strict");
const files = require("node:fs");
const paths = require("node:path");
const { setTimeout: delay } = require("node:timers/promises");
app.setPath("userData", paths.join(__dirname, "profile"));
app.on("window-all-closed", () => {});
// Electron keeps Linux's --no-sandbox in argv; the artifact directory is always last.
const artifactDir = process.argv.at(-1);
let window;
const observations = [];
const colors = {
dark: { foreground: "rgb(250, 250, 250)", background: "rgb(24, 27, 26)" },
light: { foreground: "rgb(26, 26, 30)", background: "rgb(255, 255, 255)" },
};
async function read(expression) {
return window.webContents.executeJavaScript(expression);
}
async function expectValue(expression, expected) {
let actual;
for (let attempt = 0; attempt < 50; attempt++) {
actual = await read(expression);
if (actual === expected) return;
await delay(50);
}
assert.equal(actual, expected, expression);
}
try {
await app.whenReady();
if (process.platform === "darwin") {
app.setActivationPolicy("accessory");
app.dock.hide();
}
for (const [from, to] of [
["dark", "light"],
["light", "dark"],
]) {
nativeTheme.themeSource = from;
window = new BrowserWindow({ width: 600, height: 200, show: false, skipTaskbar: true });
await window.loadURL(
"data:text/html," +
encodeURIComponent(`<!doctype html>
<style>body { background: var(--background); } p { font: 20px system-ui; }</style>
<div id="root"></div>`),
);
await read(files.readFileSync(paths.join(__dirname, "renderer.js"), "utf8"));
window.showInactive();
const foreground =
"document.querySelector('p') && getComputedStyle(document.querySelector('p')).color";
const background = "getComputedStyle(document.body).backgroundColor";
await expectValue(foreground, colors[from].foreground);
await expectValue(background, colors[from].background);
window.hide();
await expectValue("document.visibilityState", "hidden");
nativeTheme.themeSource = to;
await expectValue(`matchMedia('(prefers-color-scheme: ${to})').matches`, true);
// Reading the public runtime while hidden must not consume the pending theme notification.
assert.equal(await read("window.readThemeForTest()"), to);
window.showInactive();
await expectValue("document.visibilityState", "visible");
await expectValue(background, colors[to].background);
try {
await expectValue(foreground, colors[to].foreground);
} finally {
files.writeFileSync(
paths.join(artifactDir, `theme-${from}-to-${to}.png`),
(await window.webContents.capturePage()).toPNG(),
);
}
observations.push({
from,
to,
foreground: await read(foreground),
background: await read(background),
});
window.destroy();
window = undefined;
}
files.writeFileSync(
paths.join(artifactDir, "adaptive-theme.json"),
JSON.stringify(observations, null, 2),
);
app.exit(0);
} catch (error) {
console.error(error);
window?.destroy();
app.exit(1);
}
}
+8 -1
View File
@@ -14,6 +14,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import { chromium } from "playwright"; import { chromium } from "playwright";
import { hideDeveloperToasts } from "../../app/e2e/support/developer-ui.mjs"; import { hideDeveloperToasts } from "../../app/e2e/support/developer-ui.mjs";
import { runAppearanceFontSizeRegression } from "./appearance-font-size.electron.mjs"; import { runAppearanceFontSizeRegression } from "./appearance-font-size.electron.mjs";
import { runAdaptiveThemeRegression } from "./appearance-theme.electron.mjs";
import { runSettingsScrollRegression } from "./settings-scroll.electron.mjs"; import { runSettingsScrollRegression } from "./settings-scroll.electron.mjs";
import { runSettingsMemoryRegression } from "./settings-memory.electron.mjs"; import { runSettingsMemoryRegression } from "./settings-memory.electron.mjs";
@@ -1241,6 +1242,7 @@ async function main() {
return; return;
} }
const adaptiveTheme = await runAdaptiveThemeRegression(artifactDir);
await runAppearanceFontSizeRegression(page); await runAppearanceFontSizeRegression(page);
const callerAgentId = await createCallerAgent(daemonPort); const callerAgentId = await createCallerAgent(daemonPort);
@@ -1265,7 +1267,12 @@ async function main() {
artifactDir, artifactDir,
}); });
const pluginLinks = await checkPluginLinks(); const pluginLinks = await checkPluginLinks();
writeJson(path.join(artifactDir, "result.json"), { ...report, settingsMemory, pluginLinks }); writeJson(path.join(artifactDir, "result.json"), {
...report,
settingsMemory,
pluginLinks,
adaptiveTheme,
});
console.log( console.log(
`Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} remained ${report.finalWebContentsId}; viewport, inactive capture, focus continuity, list, snapshot, click, local-page selectors passed.`, `Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} remained ${report.finalWebContentsId}; viewport, inactive capture, focus continuity, list, snapshot, click, local-page selectors passed.`,
); );
+1 -1
View File
@@ -89,7 +89,7 @@
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts", "speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
"test": "npm run test:unit && npm run test:integration", "test": "npm run test:unit && npm run test:integration",
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"", "test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
"test:integration": "vitest run --maxWorkers=1 src/server/git-filename-diffs.e2e.test.ts src/server/daemon-e2e/checkout-diff-content.e2e.test.ts src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/daemon-e2e/terminal-worker-lifecycle.e2e.test.ts src/server/workspace-create-worktree-source.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/daemon-e2e/oversized-message.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts src/server/plugins/lifecycle.e2e.test.ts src/server/connected-hosts/hosts.e2e.test.ts && vitest run --maxWorkers=1 src/server/agent/providers/omp/history.e2e.test.ts src/server/daemon-e2e/git-operations.e2e.test.ts src/server/session.create-agent-worktree-autoarchive.e2e.test.ts src/server/agent/providers/opencode-bridge.local.e2e.test.ts src/server/daemon-e2e/relay-transport.e2e.test.ts -t \"merges into|creates a worktree and auto-archives both|versioned runtime|oversized attachments|connected hosts|password-protected daemon refuses|replays selected OMP history\"", "test:integration": "vitest run --maxWorkers=1 src/server/git-filename-diffs.e2e.test.ts src/server/daemon-e2e/checkout-diff-content.e2e.test.ts src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/daemon-e2e/terminal-worker-lifecycle.e2e.test.ts src/server/workspace-create-worktree-source.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/daemon-e2e/oversized-message.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts src/server/plugins/lifecycle.e2e.test.ts src/server/connected-hosts/hosts.e2e.test.ts src/server/owned-subscriptions.e2e.test.ts && vitest run --maxWorkers=1 src/server/agent/providers/omp/history.e2e.test.ts src/server/daemon-e2e/git-operations.e2e.test.ts src/server/session.create-agent-worktree-autoarchive.e2e.test.ts src/server/agent/providers/opencode-bridge.local.e2e.test.ts src/server/daemon-e2e/relay-transport.e2e.test.ts -t \"merges into|creates a worktree and auto-archives both|versioned runtime|oversized attachments|connected hosts|password-protected daemon refuses|replays selected OMP history\"",
"test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts", "test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts",
"test:integration:all": "npm run test:e2e", "test:integration:all": "npm run test:e2e",
"test:integration:real": "vitest run real.e2e.test.ts", "test:integration:real": "vitest run real.e2e.test.ts",
@@ -8059,6 +8059,60 @@ test("global subscribers receive internal agent events only when they opt in", a
unsubscribeOptedIn(); unsubscribeOptedIn();
}); });
test("agent removal notifications respect scoped subscriptions and internal visibility", () => {
const manager = new AgentManager({ clients: {}, logger });
const removedId = "00000000-0000-4000-8000-000000000901";
const internalId = "00000000-0000-4000-8000-000000000902";
const otherId = "00000000-0000-4000-8000-000000000903";
const publicEvents: AgentManagerEvent[] = [];
const allEvents: AgentManagerEvent[] = [];
const scopedEvents: AgentManagerEvent[] = [];
const internalEvents: AgentManagerEvent[] = [];
const unrelatedEvents: AgentManagerEvent[] = [];
const releases = [
manager.subscribe((event) => publicEvents.push(event), { replayState: false }),
manager.subscribe((event) => allEvents.push(event), {
replayState: false,
includeInternal: true,
}),
manager.subscribe((event) => scopedEvents.push(event), {
replayState: false,
agentId: removedId,
}),
manager.subscribe((event) => internalEvents.push(event), {
replayState: false,
agentId: internalId,
}),
manager.subscribe((event) => unrelatedEvents.push(event), {
replayState: false,
agentId: otherId,
}),
];
try {
manager.notifyAgentRemoved({ id: removedId, workspaceId: "workspace" });
manager.notifyAgentRemoved({ id: internalId, workspaceId: "workspace", internal: true });
const publicRemoval = {
type: "agent_removed",
agentId: removedId,
workspaceId: "workspace",
internal: false,
};
const internalRemoval = {
type: "agent_removed",
agentId: internalId,
workspaceId: "workspace",
internal: true,
};
expect(publicEvents).toEqual([publicRemoval]);
expect(allEvents).toEqual([publicRemoval, internalRemoval]);
expect(scopedEvents).toEqual([publicRemoval]);
expect(internalEvents).toEqual([internalRemoval]);
expect(unrelatedEvents).toEqual([]);
} finally {
for (const release of releases) release();
}
});
test("getAgent returns internal agents by ID", async () => { test("getAgent returns internal agents by ID", async () => {
const internalAgentId = "00000000-0000-4000-8000-000000000107"; const internalAgentId = "00000000-0000-4000-8000-000000000107";
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
@@ -236,6 +236,7 @@ export type {
export type AgentManagerEvent = export type AgentManagerEvent =
| { type: "agent_state"; agent: ManagedAgent } | { type: "agent_state"; agent: ManagedAgent }
| { type: "agent_removed"; agentId: string; workspaceId?: string; internal: boolean }
| { type: "provider_subagent"; event: ProviderSubagentStoreEvent } | { type: "provider_subagent"; event: ProviderSubagentStoreEvent }
| { type: "timeline_replacement"; agentId: string; epoch: string } | { type: "timeline_replacement"; agentId: string; epoch: string }
| { | {
@@ -3519,6 +3520,15 @@ export class AgentManager {
await this.deleteCommittedTimeline(agentId); await this.deleteCommittedTimeline(agentId);
} }
notifyAgentRemoved(agent: Pick<StoredAgentRecord, "id" | "workspaceId" | "internal">): void {
this.dispatch({
type: "agent_removed",
agentId: agent.id,
workspaceId: agent.workspaceId,
internal: agent.internal === true,
});
}
async deleteCommittedTimeline(agentId: string): Promise<void> { async deleteCommittedTimeline(agentId: string): Promise<void> {
await this.durableTimelineStore?.deleteAgent(agentId); await this.durableTimelineStore?.deleteAgent(agentId);
} }
@@ -5376,7 +5386,7 @@ export class AgentManager {
for (const subscriber of this.subscribers) { for (const subscriber of this.subscribers) {
if ( if (
subscriber.agentId && subscriber.agentId &&
event.type === "agent_stream" && (event.type === "agent_stream" || event.type === "agent_removed") &&
subscriber.agentId !== event.agentId subscriber.agentId !== event.agentId
) { ) {
continue; continue;
@@ -5412,6 +5422,7 @@ export class AgentManager {
private eventBelongsToInternalAgent(event: AgentManagerEvent): boolean { private eventBelongsToInternalAgent(event: AgentManagerEvent): boolean {
if (event.type === "agent_state") return event.agent.internal === true; if (event.type === "agent_state") return event.agent.internal === true;
if (event.type === "agent_removed") return event.internal;
if (event.type === "agent_stream") return this.agents.get(event.agentId)?.internal === true; if (event.type === "agent_stream") return this.agents.get(event.agentId)?.internal === true;
if (event.type !== "provider_subagent") return false; if (event.type !== "provider_subagent") return false;
const parentAgentId = const parentAgentId =
@@ -1,4 +1,6 @@
import { measureMainThreadWork } from "../../test-utils/main-thread-work.js"; import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { selectReferenceProjectedTimelinePage } from "./test-utils/timeline-page-reference.js"; import { selectReferenceProjectedTimelinePage } from "./test-utils/timeline-page-reference.js";
import type { AgentTimelineItem } from "./agent-sdk-types.js"; import type { AgentTimelineItem } from "./agent-sdk-types.js";
@@ -251,23 +253,52 @@ it("includes transitive tool updates in a contiguous projected tail", () => {
expect(tail).toMatchObject({ startSeq: 1, endSeq: 6, hasOlder: false }); expect(tail).toMatchObject({ startSeq: 1, endSeq: 6, hasOlder: false });
}); });
async function measureTimelineTail(wholeTurns: boolean) {
const fixture = fileURLToPath(
new URL("./test-utils/timeline-responsiveness.ts", import.meta.url),
);
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
["--expose-gc", "--import", "tsx", fixture, String(wholeTurns)],
{ timeout: 15_000 },
);
process.stderr.write(stderr);
return JSON.parse(stdout) as {
result: ReturnType<InMemoryAgentTimelineStore["fetch"]>;
blockingMs: number;
};
}
it("excludes unrelated test-runner CPU from the timeline fetch budget", async () => {
let unrelatedWork: ReturnType<typeof setImmediate>;
const burnCpu = () => {
const threadCpuUsage: (previous?: NodeJS.CpuUsage) => NodeJS.CpuUsage = Reflect.get(
process,
"threadCpuUsage",
);
const start = threadCpuUsage();
let elapsed: NodeJS.CpuUsage;
do {
elapsed = threadCpuUsage(start);
} while (elapsed.user + elapsed.system < 80_000);
unrelatedWork = setImmediate(burnCpu);
};
unrelatedWork = setImmediate(burnCpu);
try {
const { result: tail, blockingMs } = await measureTimelineTail(false);
expect(tail.rows).toHaveLength(200);
expect(tail.startSeq).toBe(99_801);
expect(tail.endSeq).toBe(100_000);
expect(blockingMs).toBeLessThan(50);
} finally {
clearImmediate(unrelatedWork);
}
});
it.each([false, true])( it.each([false, true])(
"serves tail 200 from 100,000 retained rows without stalling the loop (wholeTurns=%s)", "serves tail 200 from 100,000 retained rows without stalling the loop (wholeTurns=%s)",
async (wholeTurns) => { async (wholeTurns) => {
const store = new InMemoryAgentTimelineStore(); const { result: tail, blockingMs } = await measureTimelineTail(wholeTurns);
store.initialize("large");
for (let index = 0; index < 100_000; index++) {
store.append("large", { type: "user_message", text: String(index) });
}
const { result: tail, blockingMs } = await measureMainThreadWork(
`timeline tail main-thread work (wholeTurns=${wholeTurns})`,
() => {
let page = store.fetch("large", { limit: 200, wholeTurns });
for (let repeat = 1; repeat < 8; repeat++)
page = store.fetch("large", { limit: 200, wholeTurns });
return page;
},
);
expect(tail.rows).toHaveLength(200); expect(tail.rows).toHaveLength(200);
expect(tail.startSeq).toBe(99_801); expect(tail.startSeq).toBe(99_801);
expect(tail.endSeq).toBe(100_000); expect(tail.endSeq).toBe(100_000);
@@ -0,0 +1,25 @@
import { setImmediate as nextTurn } from "node:timers/promises";
import { measureMainThreadWork } from "../../../test-utils/main-thread-work.js";
import { InMemoryAgentTimelineStore } from "../agent-timeline-store.js";
// Measure a cold fetch on its own thread, without Vitest reporting or callbacks.
const wholeTurns = process.argv[2] === "true";
const store = new InMemoryAgentTimelineStore();
store.initialize("large");
for (let index = 0; index < 100_000; index++) {
store.append("large", { type: "user_message", text: String(index) });
}
// Finish fixture allocation and collect its temporary garbage before measuring.
globalThis.gc!();
await nextTurn();
const measured = await measureMainThreadWork(
`timeline tail main-thread work (wholeTurns=${wholeTurns})`,
() => {
let page = store.fetch("large", { limit: 200, wholeTurns });
for (let repeat = 1; repeat < 8; repeat++)
page = store.fetch("large", { limit: 200, wholeTurns });
return page;
},
);
process.stdout.write(JSON.stringify(measured));
@@ -1,7 +1,8 @@
import { MockLoadTestAgentClient } from "./agent/providers/mock-load-test-agent.js"; import { MockLoadTestAgentClient } from "./agent/providers/mock-load-test-agent.js";
import { createPaseoApi } from "@getpaseo/client"; import { createPaseoApi } from "@getpaseo/client";
import type { AgentManager } from "./agent/agent-manager.js";
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import type { z } from "zod"; import type { z } from "zod";
@@ -71,15 +72,18 @@ class SubscriptionPeer {
async request(message: z.input<typeof SessionInboundMessageSchema>): Promise<void> { async request(message: z.input<typeof SessionInboundMessageSchema>): Promise<void> {
this.socket.send(JSON.stringify({ type: "session", message })); this.socket.send(JSON.stringify({ type: "session", message }));
await expect await expect
.poll(() => .poll(
this.frames.some( () =>
(frame) => this.frames.some(
frame.type === "session" && (frame) =>
"payload" in frame.message && frame.type === "session" &&
"requestId" in frame.message.payload && "payload" in frame.message &&
"requestId" in message && "requestId" in frame.message.payload &&
frame.message.payload.requestId === message.requestId, "requestId" in message &&
), frame.message.payload.requestId === message.requestId,
),
// Diagnostics invoke external tools with their own multi-second deadlines.
{ timeout: 10_000 },
) )
.toBe(true); .toBe(true);
} }
@@ -119,6 +123,16 @@ class SubscriptionPeer {
} }
} }
function removedAgentIds(peer: SubscriptionPeer): string[] {
return peer.frames.flatMap((frame) =>
frame.type === "session" &&
frame.message.type === "agent_update" &&
frame.message.payload.kind === "remove"
? [frame.message.payload.agentId]
: [],
);
}
test("a pure list reply belongs only to its source socket in a shared logical session", async () => { test("a pure list reply belongs only to its source socket in a shared logical session", async () => {
const daemon = await createTestPaseoDaemon({ mcpEnabled: false }); const daemon = await createTestPaseoDaemon({ mcpEnabled: false });
const peers: SubscriptionPeer[] = []; const peers: SubscriptionPeer[] = [];
@@ -225,6 +239,96 @@ test("combined filtered agent lists create independent server-owned observations
} }
}); });
test.each([
{
state: "live",
prepare: async (_manager: AgentManager, _agentId: string) => {},
},
{
state: "stored-only",
prepare: (manager: AgentManager, agentId: string) => manager.closeAgent(agentId),
},
])(
"$state agent deletion reaches directory observers across logical sessions without broadcasting its reply",
async ({ prepare }) => {
const daemon = await createTestPaseoDaemon({ mcpEnabled: false });
const admin = new DaemonClient({
url: `ws://127.0.0.1:${daemon.port}/ws`,
appVersion: "0.8.0",
});
const peers: SubscriptionPeer[] = [];
try {
await admin.connect();
const workspace = (
await admin.createWorkspace({ source: { kind: "directory", path: daemon.staticDir } })
).workspace;
if (!workspace) throw new Error("Workspace creation failed");
const removed = await admin.createAgent({
provider: "codex",
cwd: daemon.staticDir,
workspaceId: workspace.id,
});
const survivor = await admin.createAgent({
provider: "codex",
cwd: daemon.staticDir,
workspaceId: workspace.id,
});
const sentinel = path.join(daemon.staticDir, "keep-project-file.txt");
await writeFile(sentinel, "keep this project file\n");
await prepare(daemon.daemon.agentManager, removed.id);
const requester = await SubscriptionPeer.connect(daemon.port, "deletion-origin");
const sibling = await SubscriptionPeer.connect(daemon.port, "deletion-origin");
const observer = await SubscriptionPeer.connect(daemon.port, "deletion-observer");
const legacy = await SubscriptionPeer.connect(daemon.port, "deletion-legacy", {
owned_subscriptions: false,
});
peers.push(requester, sibling, observer, legacy);
for (const [index, peer] of peers.entries()) {
await peer.request({
type: "fetch_agents_request",
requestId: `directory-${index}`,
subscribe: {},
});
const entries = peer.frames.flatMap((frame) =>
frame.type === "session" && frame.message.type === "fetch_agents_response"
? frame.message.payload.entries.map((entry) => entry.agent.id)
: [],
);
expect(entries.sort()).toEqual([removed.id, survivor.id].sort());
peer.frames.length = 0;
}
await requester.request({
type: "delete_agent_request",
requestId: "delete-observed-agent",
agentId: removed.id,
});
await expect
.poll(() => peers.map(removedAgentIds))
.toEqual([[removed.id], [removed.id], [removed.id], [removed.id]]);
const replies = peers.map((peer) =>
peer.frames.flatMap((frame) =>
frame.type === "session" && frame.message.type === "agent_deleted"
? [frame.message.payload]
: [],
),
);
expect(replies).toEqual([
[{ agentId: removed.id, requestId: "delete-observed-agent" }],
[],
[],
[],
]);
expect(await daemon.daemon.agentStorage.get(removed.id)).toBeNull();
expect((await daemon.daemon.agentStorage.get(survivor.id))?.id).toBe(survivor.id);
expect(await readFile(sentinel, "utf8")).toBe("keep this project file\n");
} finally {
for (const peer of peers) peer.close();
await admin.close();
await daemon.close();
}
},
);
test("workspace filters and identical queries coexist until their own release", async () => { test("workspace filters and identical queries coexist until their own release", async () => {
const daemon = await createTestPaseoDaemon({ mcpEnabled: false }); const daemon = await createTestPaseoDaemon({ mcpEnabled: false });
const admin = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.8.0" }); const admin = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.8.0" });
@@ -679,7 +783,8 @@ test("same-workspace terminal directory observers do not share a teardown slot",
peer.frames.length = 0; peer.frames.length = 0;
const result = await admin.createTerminal(daemon.staticDir, "Observed terminal", undefined, { const result = await admin.createTerminal(daemon.staticDir, "Observed terminal", undefined, {
workspaceId: workspace.id, workspaceId: workspace.id,
command: "/bin/cat", command: process.execPath,
args: ["-e", "process.stdin.on('data', data => process.stdout.write(data))"],
}); });
expect(result.error).toBe(null); expect(result.error).toBe(null);
await expect await expect
@@ -728,7 +833,8 @@ test("terminal output has separate server IDs and binary slots, including in a s
).workspace!; ).workspace!;
const result = await admin.createTerminal(daemon.staticDir, "Output ownership", undefined, { const result = await admin.createTerminal(daemon.staticDir, "Output ownership", undefined, {
workspaceId: workspace.id, workspaceId: workspace.id,
command: "/bin/cat", command: process.execPath,
args: ["-e", "process.stdin.on('data', data => process.stdout.write(data))"],
}); });
expect(result.error).toBe(null); expect(result.error).toBe(null);
const idle = await SubscriptionPeer.connect(daemon.port, "terminal-output-session"); const idle = await SubscriptionPeer.connect(daemon.port, "terminal-output-session");
@@ -1423,7 +1529,7 @@ test("request outcomes for import and attention reach only their requesting sock
}); });
const requester = await SubscriptionPeer.connect(daemon.port, "reply-shared"); const requester = await SubscriptionPeer.connect(daemon.port, "reply-shared");
const sibling = await SubscriptionPeer.connect(daemon.port, "reply-shared"); const sibling = await SubscriptionPeer.connect(daemon.port, "reply-shared");
const admin = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); const admin = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.8.0" });
try { try {
await admin.connect(); await admin.connect();
await requester.request({ await requester.request({
@@ -1462,7 +1568,7 @@ test("request outcomes for import and attention reach only their requesting sock
}), }),
); );
const agent = await admin.createAgent({ const agent = await admin.createAgent({
provider: "codex", provider: "mock",
cwd: daemon.staticDir, cwd: daemon.staticDir,
title: "Attention", title: "Attention",
}); });
@@ -1075,23 +1075,29 @@ export default function contribute(plugin: unknown) {
const service = bindTestSessionHost( const service = bindTestSessionHost(
new PluginService(pino({ level: "silent" }), store, "0.4.0"), new PluginService(pino({ level: "silent" }), store, "0.4.0"),
); );
await service.start(); try {
await service.installDirectory({ path: first }); await service.start();
await service.installDirectory({ path: second }); await service.installDirectory({ path: first });
await service.installDirectory({ path: second });
expect(catalogIds(service)).toEqual(["first", "second"]);
await store.patch({ pluginsEnabled: false });
await store.patch({ pluginsEnabled: false }); expect(service.catalog()).toEqual([]);
await expect(service.invokePluginRpc("second", "anything", {})).rejects.toThrow(
expect(service.catalog()).toEqual([]); "Plugin is not available",
await expect(service.invokePluginRpc("second", "anything", {})).rejects.toThrow( );
"Plugin is not available", await store.patch({ pluginsEnabled: true });
); // Observe recovery itself. Reloading here restarts a healthy plugin and
await store.patch({ pluginsEnabled: true }); // would also conceal a failure to restart it after the global enable.
await service.reloadPlugin("first"); await expect
expect((await service.listPlugins()).map(({ id, status }) => ({ id, status }))).toEqual([ .poll(() => service.listPlugins(), { timeout: 10_000 })
{ id: "first", status: "running" }, .toMatchObject([
{ id: "second", status: "running" }, { id: "first", status: "running" },
]); { id: "second", status: "running" },
await service.stopAllPlugins(); ]);
} finally {
await service.stopAllPlugins();
}
}, 20_000); }, 20_000);
it("does not publish an in-flight start after a later global disable", async () => { it("does not publish an in-flight start after a later global disable", async () => {
+22 -10
View File
@@ -1992,6 +1992,24 @@ export class Session {
return; return;
} }
if (event.type === "agent_removed") {
if (event.internal) return;
void this.agentUpdates
.removeAgent(event.agentId)
.then(() => {
if (event.workspaceId)
return this.emitWorkspaceUpdateForWorkspaceId(event.workspaceId);
return undefined;
})
.catch((error) => {
this.sessionLogger.error(
{ err: error, agentId: event.agentId },
"Failed to emit agent removal",
);
});
return;
}
if (event.type === "agent_state") { if (event.type === "agent_state") {
this.sessionLogger.trace( this.sessionLogger.trace(
{ {
@@ -3337,10 +3355,8 @@ export class Session {
private async handleDeleteAgentRequest(agentId: string, requestId: string): Promise<void> { private async handleDeleteAgentRequest(agentId: string, requestId: string): Promise<void> {
this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`); this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
const knownWorkspaceId = const knownAgent =
this.agentManager.getAgent(agentId)?.workspaceId ?? this.agentManager.getAgent(agentId) ?? (await this.agentStorage.get(agentId));
(await this.agentStorage.get(agentId))?.workspaceId ??
null;
// File-backed storage still needs an early delete fence before closeAgent(). // File-backed storage still needs an early delete fence before closeAgent().
beginAgentDeleteIfSupported(this.agentStorage, agentId); beginAgentDeleteIfSupported(this.agentStorage, agentId);
@@ -3361,6 +3377,8 @@ export class Session {
try { try {
await this.agentStorage.remove(agentId); await this.agentStorage.remove(agentId);
await this.agentManager.deleteAgentState(agentId); await this.agentManager.deleteAgentState(agentId);
// Directory removal is daemon-wide; agent_deleted below stays a source-scoped RPC reply.
if (knownAgent) this.agentManager.notifyAgentRemoved(knownAgent);
} catch (error) { } catch (error) {
this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`); this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`);
} }
@@ -3372,12 +3390,6 @@ export class Session {
requestId, requestId,
}, },
}); });
await this.agentUpdates.removeAgent(agentId);
if (knownWorkspaceId) {
await this.emitWorkspaceUpdateForWorkspaceId(knownWorkspaceId);
}
} }
private async handleArchiveAgentRequest(agentId: string, requestId: string): Promise<void> { private async handleArchiveAgentRequest(agentId: string, requestId: string): Promise<void> {
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
import { writeFile, truncate, rm } from "node:fs/promises"; import { writeFile, truncate, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { build } from "esbuild";
import { isPlatform } from "../test-utils/platform.js"; import { isPlatform } from "../test-utils/platform.js";
import { import {
readPaseoConfig, readPaseoConfig,
@@ -20,21 +21,40 @@ import {
writePaseoConfigForEdit, writePaseoConfigForEdit,
} from "./paseo-config-file.js"; } from "./paseo-config-file.js";
let probeDirectory: string;
let probePath: string;
beforeAll(async () => {
probeDirectory = mkdtempSync(join(tmpdir(), "paseo-config-probe-"));
probePath = join(probeDirectory, "config-responsiveness.mjs");
// Compile before starting the measured process. A runtime TypeScript loader
// owns MessagePorts that the responsiveness probe otherwise charges as work.
await build({
entryPoints: [
fileURLToPath(new URL("./test-fixtures/config-responsiveness.ts", import.meta.url)),
],
outfile: probePath,
bundle: true,
platform: "node",
format: "esm",
});
});
afterAll(() => {
rmSync(probeDirectory, { recursive: true, force: true });
});
async function measureConfigRead(directory: string) { async function measureConfigRead(directory: string) {
const fixture = fileURLToPath( const { stdout, stderr } = await promisify(execFile)(process.execPath, [probePath, directory], {
new URL("./test-fixtures/config-responsiveness.ts", import.meta.url), timeout: 15_000,
); });
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
["--import", "tsx", fixture, directory],
{ timeout: 15_000 },
);
process.stderr.write(stderr); process.stderr.write(stderr);
return JSON.parse(stdout) as { return JSON.parse(stdout) as {
result: Awaited<ReturnType<typeof readPaseoConfigForEdit>>; result: Awaited<ReturnType<typeof readPaseoConfigForEdit>>;
blockingMs: number; blockingMs: number;
filesystemRequests: number; filesystemRequests: number;
yields: number; yields: number;
workerMessages: number;
}; };
} }
@@ -84,7 +104,7 @@ describe("paseo config file substrate", () => {
); );
}); });
it("excludes unrelated test-runner CPU from the config read budget", async () => { it("excludes test-runner and loader activity from the config read budget", async () => {
await writeFile(join(tempDir, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } })); await writeFile(join(tempDir, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } }));
const unrelatedWork = new Promise<void>((resolve) => { const unrelatedWork = new Promise<void>((resolve) => {
setImmediate(() => { setImmediate(() => {
@@ -100,8 +120,10 @@ describe("paseo config file substrate", () => {
resolve(); resolve();
}); });
}); });
const { result, blockingMs, filesystemRequests } = await measureConfigRead(tempDir); const { result, blockingMs, filesystemRequests, workerMessages } =
await measureConfigRead(tempDir);
await unrelatedWork; await unrelatedWork;
expect(workerMessages).toBe(0);
expect(result).toEqual({ expect(result).toEqual({
ok: true, ok: true,
config: { worktree: { setup: "npm ci" } }, config: { worktree: { setup: "npm ci" } },
@@ -119,11 +141,13 @@ describe("paseo config file substrate", () => {
worktree: { setup: "npm ci" }, worktree: { setup: "npm ci" },
}; };
await writeFile(join(tempDir, "paseo.json"), JSON.stringify(config)); await writeFile(join(tempDir, "paseo.json"), JSON.stringify(config));
const { result, blockingMs, filesystemRequests, yields } = await measureConfigRead(tempDir); const { result, blockingMs, filesystemRequests, yields, workerMessages } =
await measureConfigRead(tempDir);
expect(result).toEqual({ ok: true, config, revision: await statPaseoConfigPath(tempDir) }); expect(result).toEqual({ ok: true, config, revision: await statPaseoConfigPath(tempDir) });
expect(blockingMs).toBeLessThan(20); expect(blockingMs).toBeLessThan(20);
expect(filesystemRequests).toBeGreaterThan(0); expect(filesystemRequests).toBeGreaterThan(0);
expect(yields).toBeGreaterThan(0); expect(yields).toBeGreaterThan(0);
expect(workerMessages).toBe(0);
}); });
it("rejects a 32MiB config with main-thread work below 20ms", async () => { it("rejects a 32MiB config with main-thread work below 20ms", async () => {
@@ -131,9 +155,11 @@ describe("paseo config file substrate", () => {
join(tempDir, "paseo.json"), join(tempDir, "paseo.json"),
JSON.stringify({ extra: "x".repeat(32 * 1024 * 1024) }), JSON.stringify({ extra: "x".repeat(32 * 1024 * 1024) }),
); );
const { result, blockingMs, filesystemRequests } = await measureConfigRead(tempDir); const { result, blockingMs, filesystemRequests, workerMessages } =
await measureConfigRead(tempDir);
expect(blockingMs).toBeLessThan(20); expect(blockingMs).toBeLessThan(20);
expect(filesystemRequests).toBeGreaterThan(0); expect(filesystemRequests).toBeGreaterThan(0);
expect(workerMessages).toBe(0);
expect(result).toEqual({ ok: false, error: { code: "invalid_project_config" } }); expect(result).toEqual({ ok: false, error: { code: "invalid_project_config" } });
}); });
@@ -1,14 +1,24 @@
import { createHook, executionAsyncResource } from "node:async_hooks";
import { MessagePort } from "node:worker_threads";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { setImmediate as nextTurn } from "node:timers/promises"; import { setImmediate as nextTurn } from "node:timers/promises";
import { measureMainThreadWork } from "../../test-utils/main-thread-work.js"; import { measureMainThreadWork } from "../../test-utils/main-thread-work.js";
import { readPaseoConfigForEdit } from "../paseo-config-file.js"; import { readPaseoConfigForEdit } from "../paseo-config-file.js";
// Keep Vitest transforms, reporting, and unrelated callbacks outside the measured // The parent bundles this fixture before launching plain Node, keeping test
// thread. Read the real file once, without warming the config snapshot or schema. // runner callbacks and runtime loader messages outside the measured thread.
// Read the real file once, without warming the config snapshot or schema.
const directory = process.argv[2]; const directory = process.argv[2];
assert.ok(directory, "Config directory is required"); assert.ok(directory, "Config directory is required");
await nextTurn(); await nextTurn();
let workerMessages = 0;
const activity = createHook({
before() {
if (executionAsyncResource() instanceof MessagePort) workerMessages++;
},
}).enable();
const measured = await measureMainThreadWork("project config main-thread work", () => const measured = await measureMainThreadWork("project config main-thread work", () =>
readPaseoConfigForEdit(directory), readPaseoConfigForEdit(directory),
); );
process.stdout.write(JSON.stringify(measured)); activity.disable();
process.stdout.write(JSON.stringify({ ...measured, workerMessages }));
@@ -486,3 +486,74 @@ diff --git a/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles
...(narrowedProps.style ? { ...(narrowedProps.style ? {
style: styleSecrets.styles style: styleSecrets.styles
} : {}), } : {}),
diff --git a/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js b/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js
index 305fa49..5957aa5 100644
--- a/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js
+++ b/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js
@@ -35,10 +35,17 @@ class UnistylesRuntime {
return this.darkMedia;
}
get colorScheme() {
+ // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers
+ // MediaQueryList change events while the document is hidden and drops the pending event
+ // of any list whose `matches` was read before the document became visible again, which
+ // would silence the adaptive-theme listeners registered on those two lists.
+ if ((0, _utils.isServer)()) {
+ return _types.ColorScheme.Unspecified;
+ }
switch (true) {
- case this.getLightMedia()?.matches:
+ case window.matchMedia('(prefers-color-scheme: light)').matches:
return _types.ColorScheme.Light;
- case this.getDarkMedia()?.matches:
+ case window.matchMedia('(prefers-color-scheme: dark)').matches:
return _types.ColorScheme.Dark;
default:
return _types.ColorScheme.Unspecified;
diff --git a/node_modules/react-native-unistyles/lib/module/web/runtime.js b/node_modules/react-native-unistyles/lib/module/web/runtime.js
index 7f13a06..c487bee 100644
--- a/node_modules/react-native-unistyles/lib/module/web/runtime.js
+++ b/node_modules/react-native-unistyles/lib/module/web/runtime.js
@@ -31,10 +31,17 @@ export class UnistylesRuntime {
return this.darkMedia;
}
get colorScheme() {
+ // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers
+ // MediaQueryList change events while the document is hidden and drops the pending event
+ // of any list whose `matches` was read before the document became visible again, which
+ // would silence the adaptive-theme listeners registered on those two lists.
+ if (isServer()) {
+ return ColorScheme.Unspecified;
+ }
switch (true) {
- case this.getLightMedia()?.matches:
+ case window.matchMedia('(prefers-color-scheme: light)').matches:
return ColorScheme.Light;
- case this.getDarkMedia()?.matches:
+ case window.matchMedia('(prefers-color-scheme: dark)').matches:
return ColorScheme.Dark;
default:
return ColorScheme.Unspecified;
diff --git a/node_modules/react-native-unistyles/src/web/runtime.ts b/node_modules/react-native-unistyles/src/web/runtime.ts
index a16dac0..6519858 100644
--- a/node_modules/react-native-unistyles/src/web/runtime.ts
+++ b/node_modules/react-native-unistyles/src/web/runtime.ts
@@ -41,8 +41,16 @@ export class UnistylesRuntime {
get colorScheme() {
+ // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers
+ // MediaQueryList change events while the document is hidden and drops the pending event
+ // of any list whose `matches` was read before the document became visible again, which
+ // would silence the adaptive-theme listeners registered on those two lists.
+ if (isServer()) {
+ return ColorScheme.Unspecified
+ }
+
switch (true) {
- case this.getLightMedia()?.matches:
+ case window.matchMedia('(prefers-color-scheme: light)').matches:
return ColorScheme.Light
- case this.getDarkMedia()?.matches:
+ case window.matchMedia('(prefers-color-scheme: dark)').matches:
return ColorScheme.Dark
default:
return ColorScheme.Unspecified
+1
View File
@@ -24,6 +24,7 @@ const patchedPackages = [
}, },
// Registry cleanup: remove after react-native-unistyles ships // Registry cleanup: remove after react-native-unistyles ships
// https://github.com/jpudysz/react-native-unistyles/pull/1203. // https://github.com/jpudysz/react-native-unistyles/pull/1203.
// Adaptive themes: keep until colorScheme reads no longer consume listener-owned media changes.
// withUnistyles: also keep the uniProps filter until upstream consumes its control prop. // withUnistyles: also keep the uniProps filter until upstream consumes its control prop.
{ {
nodeModulesPath: "node_modules/react-native-unistyles", nodeModulesPath: "node_modules/react-native-unistyles",
+5 -3
View File
@@ -9,7 +9,7 @@ argument-hint: "[--profile <name>] <question or topic>"
Single agent. Reads the situation you're in. Gives a judgment. You decide what to do — the advisor doesn't drive the work. Single agent. Reads the situation you're in. Gives a judgment. You decide what to do — the advisor doesn't drive the work.
**User's request:** $ARGUMENTS Use the user's request and current conversation as input, including any arguments supplied with the skill invocation. Optional syntax: `[--profile <name>] <question or topic>`.
## Prerequisites ## Prerequisites
@@ -40,13 +40,15 @@ This is analysis only. Do NOT edit, create, or delete any files. Do NOT write co
## Forwarded skills ## Forwarded skills
If `$ARGUMENTS` contains another skill reference — `/unslop`, `/unslop-risk`, `$unslop`, etc. — the user is asking the advisor to run that skill against the current task. Examples: If the user's request contains another skill reference, such as `/unslop`, `/skill:unslop`, or `$unslop`, the user is asking the advisor to run that skill against the current task. Examples:
- `/paseo-advisor /unslop` → advisor runs `/unslop` on the current diff. - `/paseo-advisor /unslop` → advisor runs `/unslop` on the current diff.
- `/paseo-advisor /unslop-risk` → advisor does an unslop-risk review. - `/paseo-advisor /unslop-risk` → advisor does an unslop-risk review.
- `/paseo-advisor $diagnose this build failure` → advisor invokes `/diagnose`. - `/paseo-advisor $diagnose this build failure` → advisor invokes `/diagnose`.
- `/skill:paseo-advisor /skill:unslop` → advisor runs the `unslop` skill on the current diff in Pi.
- `$paseo-advisor $unslop` → advisor runs the `unslop` skill on the current diff in Codex.
Parse the forwarded skill name out of `$ARGUMENTS` (`/<name>` or `$<name>`). In the briefing, tell the advisor explicitly: Parse the forwarded skill name from the user's request (`/<name>`, `/skill:<name>`, or `$<name>`), excluding this skill's own invocation. In the briefing, tell the advisor explicitly:
``` ```
Invoke the `<name>` skill against this task. Load it via the Skill tool before doing anything else. Invoke the `<name>` skill against this task. Load it via the Skill tool before doing anything else.
+1 -1
View File
@@ -8,7 +8,7 @@ user-invocable: true
Two agents from contrasting profiles, fresh context, planning a solution in parallel. Two agents from contrasting profiles, fresh context, planning a solution in parallel.
**User's additional context:** $ARGUMENTS Use the user's request and current conversation as input, including any arguments supplied with the skill invocation.
## Prerequisites ## Prerequisites
+1 -1
View File
@@ -8,7 +8,7 @@ user-invocable: true
Transfer the current task — context, decisions, failed attempts, constraints — to a fresh agent. The receiving agent starts with **zero context**, so the handoff prompt must be a self-contained briefing. Transfer the current task — context, decisions, failed attempts, constraints — to a fresh agent. The receiving agent starts with **zero context**, so the handoff prompt must be a self-contained briefing.
**User's arguments:** $ARGUMENTS Use the user's request and current conversation as input, including any arguments supplied with the skill invocation.
## Prerequisites ## Prerequisites
+1 -1
View File
@@ -7,7 +7,7 @@ description: Answer questions about the Paseo product and app, including setup,
You are helping a user understand, configure, or troubleshoot Paseo itself. Answer their question directly, verify the answer against the current public documentation, and include the relevant documentation link. Do not send the user away to read the docs in place of helping them. You are helping a user understand, configure, or troubleshoot Paseo itself. Answer their question directly, verify the answer against the current public documentation, and include the relevant documentation link. Do not send the user away to read the docs in place of helping them.
**User's question:** $ARGUMENTS Use the user's question and current conversation as input, including any arguments supplied with the skill invocation.
## Use current documentation ## Use current documentation
+1 -1
View File
@@ -7,7 +7,7 @@ description: Build and manage trusted local Paseo plugins. Use when the user ask
Build or manage the requested plugin directly. Use the current public docs to catch contract changes, but keep working from this skill if the network is unavailable. Build or manage the requested plugin directly. Use the current public docs to catch contract changes, but keep working from this skill if the network is unavailable.
**User's request:** $ARGUMENTS Use the user's request and current conversation as input, including any arguments supplied with the skill invocation.
## Check current documentation ## Check current documentation