chore: reformat codebase with oxfmt

Apply prettier-compatible formatting across the repo to match the
incoming oxfmt configuration. Mechanical reformat only — no logic
changes. Covers YAML quote normalization, package.json key sorting,
Markdown/TOML formatting, and minor TS whitespace tweaks.
This commit is contained in:
Mohamed Boudra
2026-04-23 21:44:48 +07:00
parent 9b25f8fc36
commit ce4b0d54c7
81 changed files with 1171 additions and 1009 deletions
@@ -92,9 +92,7 @@ type DaemonClientConfig = {
};
async function loadDaemonClientConstructor(): Promise<
new (
config: DaemonClientConfig,
) => DaemonClientInstance
new (config: DaemonClientConfig) => DaemonClientInstance
> {
const repoRoot = path.resolve(__dirname, "../../../../");
const moduleUrl = pathToFileURL(
+1 -3
View File
@@ -81,9 +81,7 @@ type ArchiveTabDaemonClientConfig = {
};
async function loadDaemonClientConstructor(): Promise<
new (
config: ArchiveTabDaemonClientConfig,
) => ArchiveTabDaemonClient
new (config: ArchiveTabDaemonClientConfig) => ArchiveTabDaemonClient
> {
const repoRoot = path.resolve(__dirname, "../../../../");
const moduleUrl = pathToFileURL(
+1 -3
View File
@@ -49,9 +49,7 @@ function getDaemonWsUrl(): string {
}
async function loadDaemonClientConstructor(): Promise<
new (
config: NewWorkspaceDaemonClientConfig,
) => NewWorkspaceDaemonClient
new (config: NewWorkspaceDaemonClientConfig) => NewWorkspaceDaemonClient
> {
const repoRoot = path.resolve(__dirname, "../../../../");
const moduleUrl = pathToFileURL(
+1 -3
View File
@@ -63,9 +63,7 @@ type TerminalPerfDaemonClientConfig = {
};
async function loadDaemonClientConstructor(): Promise<
new (
config: TerminalPerfDaemonClientConfig,
) => TerminalPerfDaemonClient
new (config: TerminalPerfDaemonClientConfig) => TerminalPerfDaemonClient
> {
const repoRoot = path.resolve(__dirname, "../../../../");
const moduleUrl = pathToFileURL(
@@ -24,4 +24,3 @@ appId: sh.paseo
commands:
- tapOn:
point: "50%,20%"
-1
View File
@@ -3,4 +3,3 @@ appId: sh.paseo
- launchApp:
clearState: true
- runFlow: flows/dev-client.yaml
+1 -1
View File
@@ -1,8 +1,8 @@
{
"name": "@getpaseo/app",
"main": "index.ts",
"version": "0.1.62",
"private": true,
"main": "index.ts",
"scripts": {
"start": "APP_VARIANT=development expo start",
"reset-project": "node ./scripts/reset-project.js",
+15 -14
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="%LANG_ISO_CODE%">
<head>
<meta charset="utf-8" />
@@ -21,8 +21,9 @@
}
/* Prevent white flash before React mounts */
@media (prefers-color-scheme: dark) {
html, body {
background-color: #181B1A;
html,
body {
background-color: #181b1a;
}
}
/* These styles make the root element full-height */
@@ -38,17 +39,17 @@
input,
textarea,
select,
[role='button'],
[role='link'],
[role='textbox'],
[role='combobox'],
[role='tab'],
[role='switch'],
[role='checkbox'],
[role='slider'],
[role='menuitem'],
[role="button"],
[role="link"],
[role="textbox"],
[role="combobox"],
[role="tab"],
[role="switch"],
[role="checkbox"],
[role="slider"],
[role="menuitem"],
[tabindex],
[contenteditable='true'] {
[contenteditable="true"] {
-webkit-app-region: no-drag !important;
}
@@ -59,7 +60,7 @@
outline: none;
}
*:focus-visible {
outline: 2px solid #20744A;
outline: 2px solid #20744a;
outline-offset: 2px;
}
</style>
+2 -3
View File
@@ -7,9 +7,8 @@ let attachmentStorePromise: Promise<AttachmentStore> | null = null;
async function createAttachmentStore(): Promise<AttachmentStore> {
if (isWeb) {
if (isElectronRuntime()) {
const { createDesktopAttachmentStore } = await import(
"../desktop/attachments/desktop-attachment-store"
);
const { createDesktopAttachmentStore } =
await import("../desktop/attachments/desktop-attachment-store");
return createDesktopAttachmentStore();
}
@@ -82,13 +82,7 @@ vi.mock("expo-image", () => ({
vi.mock("react-native", async () => {
const actual = await vi.importActual<Record<string, unknown>>("react-native");
const Modal = ({
visible = true,
children,
}: {
visible?: boolean;
children?: React.ReactNode;
}) =>
const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) =>
visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null;
return { ...actual, Modal };
});
@@ -285,13 +285,7 @@ vi.mock("expo-image", () => ({
vi.mock("react-native", async () => {
const actual = await vi.importActual<Record<string, unknown>>("react-native");
const Modal = ({
visible = true,
children,
}: {
visible?: boolean;
children?: React.ReactNode;
}) =>
const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) =>
visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null;
return { ...actual, Modal };
});
@@ -1954,8 +1954,8 @@ export function SidebarWorkspaceList({
select: toProjectIconDataUri,
enabled: Boolean(
getHostRuntimeStore().getClient(request.serverId) &&
isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) &&
request.cwd,
isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) &&
request.cwd,
),
staleTime: Infinity,
gcTime: 1000 * 60 * 60,
+11 -12
View File
@@ -134,18 +134,17 @@ interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout"> {
tabDropPreview: TabDropPreview | null;
}
interface SplitPaneViewProps
extends Omit<
SplitNodeViewProps,
| "node"
| "workspaceKey"
| "focusedPaneId"
| "activeDragTabId"
| "showDropZones"
| "dropPreview"
| "onMoveTabToPane"
| "onResizeSplit"
> {
interface SplitPaneViewProps extends Omit<
SplitNodeViewProps,
| "node"
| "workspaceKey"
| "focusedPaneId"
| "activeDragTabId"
| "showDropZones"
| "dropPreview"
| "onMoveTabToPane"
| "onResizeSplit"
> {
pane: SplitPane;
uiTabs: WorkspaceTab[];
isFocused: boolean;
@@ -237,23 +237,24 @@ describe("providers snapshot hook cache scope", () => {
it.each([
{ name: "missing", entries: [] },
{ name: "loading", entries: [codexEntry("loading")] },
])("ensures a selected provider snapshot on selector open when it is $name", async ({
entries,
}) => {
enableProvidersSnapshot();
mockClient.getProvidersSnapshot
.mockResolvedValueOnce(providersSnapshot(entries))
.mockResolvedValueOnce(providersSnapshot([codexEntry("ready", [readyCodexModel])]));
])(
"ensures a selected provider snapshot on selector open when it is $name",
async ({ entries }) => {
enableProvidersSnapshot();
mockClient.getProvidersSnapshot
.mockResolvedValueOnce(providersSnapshot(entries))
.mockResolvedValueOnce(providersSnapshot([codexEntry("ready", [readyCodexModel])]));
const { result } = renderProvidersSnapshotHook();
const { result } = renderProvidersSnapshotHook();
await waitForSnapshotEntries(result, entries);
await openSelectorForSelectedProvider(result);
await waitForSnapshotReads(2);
await waitForSnapshotEntries(result, entries);
await openSelectorForSelectedProvider(result);
await waitForSnapshotReads(2);
expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({});
expect(mockClient.refreshProvidersSnapshot).not.toHaveBeenCalled();
});
expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({});
expect(mockClient.refreshProvidersSnapshot).not.toHaveBeenCalled();
},
);
it("does not ensure a selected provider snapshot on selector open when the provider is ready with no models", async () => {
enableProvidersSnapshot();
+1 -1
View File
@@ -51,7 +51,7 @@ export function resolveKeyboardFocusScope(input: {
candidates.some((element) =>
Boolean(
element.closest("[data-testid='command-center-panel']") ||
element.closest("[data-testid='command-center-input']"),
element.closest("[data-testid='command-center-input']"),
),
)
) {
@@ -289,23 +289,19 @@ describe("keyboard-shortcuts", () => {
},
];
it.each(matchingCases)("$name", ({
event,
context,
action,
payload,
preventDefault,
stopPropagation,
}) => {
expectShortcutResolution({
event,
context,
action,
...(payload !== undefined ? { payload } : {}),
...(preventDefault !== undefined ? { preventDefault } : {}),
...(stopPropagation !== undefined ? { stopPropagation } : {}),
});
});
it.each(matchingCases)(
"$name",
({ event, context, action, payload, preventDefault, stopPropagation }) => {
expectShortcutResolution({
event,
context,
action,
...(payload !== undefined ? { payload } : {}),
...(preventDefault !== undefined ? { preventDefault } : {}),
...(stopPropagation !== undefined ? { stopPropagation } : {}),
});
},
);
const nonMatchingCases: NonMatchingShortcutCase[] = [
{
+2 -7
View File
@@ -491,10 +491,7 @@ export class HostRuntimeController {
private probeRequestVersion = 0;
private probeCycleInFlight: Promise<void> | null = null;
constructor(input: {
host: HostProfile;
deps?: HostRuntimeControllerDeps;
}) {
constructor(input: { host: HostProfile; deps?: HostRuntimeControllerDeps }) {
this.host = input.host;
this.deps = input.deps ?? createDefaultDeps();
this.connectionMachineState = {
@@ -1155,9 +1152,7 @@ export class HostRuntimeStore {
private storageLoaded = false;
private bootstrapAttempted = false;
constructor(input?: {
deps?: HostRuntimeControllerDeps;
}) {
constructor(input?: { deps?: HostRuntimeControllerDeps }) {
this.deps = input?.deps ?? createDefaultDeps();
}
@@ -230,11 +230,11 @@ export function WorkspaceDraftAgentTab({
const isReadyForPendingAutoSubmit = Boolean(
pendingAutoSubmit &&
draftInput.isHydrated &&
workspaceDirectory &&
client &&
!isSubmitting &&
!composerState.isModelLoading,
draftInput.isHydrated &&
workspaceDirectory &&
client &&
!isSubmitting &&
!composerState.isModelLoading,
);
const autoSubmitKeyRef = useRef<string | null>(null);
useEffect(() => {
@@ -36,10 +36,10 @@ export function hasMeaningfulToolCallDetail(detail: ToolCallDetail | undefined):
case "search":
return Boolean(
detail.query.trim().length > 0 ||
detail.content ||
(detail.filePaths && detail.filePaths.length > 0) ||
(detail.webResults && detail.webResults.length > 0) ||
(detail.annotations && detail.annotations.length > 0),
detail.content ||
(detail.filePaths && detail.filePaths.length > 0) ||
(detail.webResults && detail.webResults.length > 0) ||
(detail.annotations && detail.annotations.length > 0),
);
case "fetch":
return Boolean(detail.url || detail.result || detail.codeText);
+4 -4
View File
@@ -2,17 +2,17 @@
"name": "@getpaseo/cli",
"version": "0.1.62",
"description": "Paseo CLI - control your AI coding agents from the command line",
"type": "module",
"bin": {
"paseo": "bin/paseo"
},
"files": [
"bin",
"dist"
],
"type": "module",
"publishConfig": {
"access": "public"
},
"bin": {
"paseo": "bin/paseo"
},
"scripts": {
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
"prepack": "npm run build",
+11 -11
View File
@@ -3,6 +3,16 @@
"version": "0.1.62",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"homepage": "https://paseo.sh",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Mohamed Boudra",
"email": "hello@moboudra.com"
},
"repository": {
"type": "git",
"url": "https://github.com/getpaseo/paseo.git"
},
"main": "dist/main.js",
"scripts": {
"build": "npm --prefix ../.. run build:daemon && npm run build:main && electron-builder --config electron-builder.yml",
@@ -27,15 +37,5 @@
"typescript": "5.9.3",
"unzip-crx-3": "^0.2.0",
"wait-on": "8.0.5"
},
"homepage": "https://paseo.sh",
"repository": {
"type": "git",
"url": "https://github.com/getpaseo/paseo.git"
},
"author": {
"name": "Mohamed Boudra",
"email": "hello@moboudra.com"
},
"license": "AGPL-3.0-or-later"
}
}
+12 -12
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
https://www.contributor-covenant.org/translations.
@@ -18,18 +18,20 @@ This example showcases the basic functions of the expo-two-way-audio module:
In the `examples/basic-usage` directory:
1. Install dependencies:
```sh
npm install
```
```sh
npm install
```
2. Run the app:
```sh
# Run iOS
npm run ios
# Run Android
npm run android
```
```sh
# Run iOS
npm run ios
# Run Android
npm run android
```
## Notes
@@ -1,6 +1,7 @@
{
"name": "expo-two-way-audio-example",
"version": "1.0.0",
"private": true,
"main": "expo/AppEntry.js",
"scripts": {
"start": "expo start",
@@ -11,17 +12,16 @@
"dependencies": {
"buffer": "^6.0.3",
"expo": "^52.0.0",
"react": "18.3.1",
"react-native": "0.76.2",
"expo-build-properties": "~0.13.1",
"expo-dev-client": "~5.0.3",
"expo-build-properties": "~0.13.1"
"react": "18.3.1",
"react-native": "0.76.2"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.3.12",
"typescript": "^5.1.3"
},
"private": true,
"expo": {
"autolinking": {
"nativeModulesDir": "../.."
@@ -1,6 +1,7 @@
{
"name": "expo-two-way-audio-flow-example",
"version": "1.0.0",
"private": true,
"main": "expo/AppEntry.js",
"scripts": {
"start": "expo start",
@@ -25,7 +26,6 @@
"@types/react": "~18.3.12",
"typescript": "^5.1.3"
},
"private": true,
"expo": {
"autolinking": {
"nativeModulesDir": "../.."
+6 -6
View File
@@ -1,10 +1,10 @@
{
"name": "@getpaseo/highlight",
"version": "0.1.62",
"files": [
"dist"
],
"type": "module",
"publishConfig": {
"access": "public"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
@@ -14,9 +14,9 @@
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
"prepack": "npm run build",
+5 -5
View File
@@ -2,14 +2,11 @@
"name": "@getpaseo/relay",
"version": "0.1.62",
"description": "Paseo relay for bridging daemon and client connections",
"type": "module",
"publishConfig": {
"access": "public"
},
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"type": "module",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -30,6 +27,9 @@
"default": "./src/cloudflare-adapter.ts"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
"prepack": "npm run build",
+146 -139
View File
@@ -199,169 +199,176 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
}
}, SHUTDOWN_TIMEOUT_MS);
it("full flow: daemon and client exchange encrypted messages through relay", {
timeout: 90_000,
}, async () => {
const serverId = "test-session-" + Date.now();
const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2);
it(
"full flow: daemon and client exchange encrypted messages through relay",
{
timeout: 90_000,
},
async () => {
const serverId = "test-session-" + Date.now();
const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2);
// === DAEMON SIDE ===
// Generate keypair (public key goes in QR)
const daemonKeyPair = await generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey);
// === DAEMON SIDE ===
// Generate keypair (public key goes in QR)
const daemonKeyPair = await generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey);
// QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
// QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
// Daemon connects to relay as "server" control role
const daemonControlWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
);
// Daemon connects to relay as "server" control role
const daemonControlWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
);
await new Promise<void>((resolve, reject) => {
daemonControlWs.on("open", resolve);
daemonControlWs.on("error", reject);
});
await new Promise<void>((resolve, reject) => {
daemonControlWs.on("open", resolve);
daemonControlWs.on("error", reject);
});
// === CLIENT SIDE ===
// Client scans QR, gets daemon's public key and session ID
// Client generates own keypair
const clientKeyPair = await generateKeyPair();
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey);
// === CLIENT SIDE ===
// Client scans QR, gets daemon's public key and session ID
// Client generates own keypair
const clientKeyPair = await generateKeyPair();
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey);
// Client imports daemon's public key and derives shared secret
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
// Client imports daemon's public key and derives shared secret
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
const waitForClientSeen = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("timed out waiting for connected")), 5000);
const onMessage = (raw: unknown) => {
try {
const text =
typeof raw === "string"
? raw
: raw && typeof (raw as any).toString === "function"
? (raw as any).toString()
: "";
const msg = JSON.parse(text);
if (msg?.type === "connected" && msg.connectionId === connectionId) {
clearTimeout(timeout);
daemonControlWs.off("message", onMessage);
resolve();
return;
const waitForClientSeen = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("timed out waiting for connected")),
5000,
);
const onMessage = (raw: unknown) => {
try {
const text =
typeof raw === "string"
? raw
: raw && typeof (raw as any).toString === "function"
? (raw as any).toString()
: "";
const msg = JSON.parse(text);
if (msg?.type === "connected" && msg.connectionId === connectionId) {
clearTimeout(timeout);
daemonControlWs.off("message", onMessage);
resolve();
return;
}
if (
msg?.type === "sync" &&
Array.isArray(msg.connectionIds) &&
msg.connectionIds.includes(connectionId)
) {
clearTimeout(timeout);
daemonControlWs.off("message", onMessage);
resolve();
}
} catch {
// ignore
}
if (
msg?.type === "sync" &&
Array.isArray(msg.connectionIds) &&
msg.connectionIds.includes(connectionId)
) {
clearTimeout(timeout);
daemonControlWs.off("message", onMessage);
resolve();
}
} catch {
// ignore
}
};
daemonControlWs.on("message", onMessage);
});
};
daemonControlWs.on("message", onMessage);
});
// Client connects to relay as "client" role (must include connectionId)
const clientWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`,
);
// Client connects to relay as "client" role (must include connectionId)
const clientWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`,
);
await new Promise<void>((resolve, reject) => {
clientWs.on("open", resolve);
clientWs.on("error", reject);
});
await new Promise<void>((resolve, reject) => {
clientWs.on("open", resolve);
clientWs.on("error", reject);
});
await waitForClientSeen;
await waitForClientSeen;
const daemonWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`,
);
await new Promise<void>((resolve, reject) => {
daemonWs.on("open", resolve);
daemonWs.on("error", reject);
});
const daemonWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`,
);
await new Promise<void>((resolve, reject) => {
daemonWs.on("open", resolve);
daemonWs.on("error", reject);
});
// Client sends hello with its public key (this message is NOT encrypted - it's the handshake)
const helloMsg = JSON.stringify({ type: "hello", key: clientPubKeyB64 });
clientWs.send(helloMsg);
// Client sends hello with its public key (this message is NOT encrypted - it's the handshake)
const helloMsg = JSON.stringify({ type: "hello", key: clientPubKeyB64 });
clientWs.send(helloMsg);
// === DAEMON RECEIVES HELLO ===
const daemonReceivedHello = await new Promise<string>((resolve) => {
daemonWs.once("message", (data) => resolve(data.toString()));
});
// === DAEMON RECEIVES HELLO ===
const daemonReceivedHello = await new Promise<string>((resolve) => {
daemonWs.once("message", (data) => resolve(data.toString()));
});
const hello = JSON.parse(daemonReceivedHello);
expect(hello.type).toBe("hello");
expect(hello.key).toBe(clientPubKeyB64);
const hello = JSON.parse(daemonReceivedHello);
expect(hello.type).toBe("hello");
expect(hello.key).toBe(clientPubKeyB64);
// Daemon imports client's public key and derives shared secret
const clientPubKeyOnDaemon = await importPublicKey(hello.key);
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
// Daemon imports client's public key and derives shared secret
const clientPubKeyOnDaemon = await importPublicKey(hello.key);
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
// === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages ===
// === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages ===
// Daemon sends encrypted "ready" message
const readyPlaintext = JSON.stringify({ type: "ready" });
const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext);
daemonWs.send(Buffer.from(readyCiphertext));
// Daemon sends encrypted "ready" message
const readyPlaintext = JSON.stringify({ type: "ready" });
const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext);
daemonWs.send(Buffer.from(readyCiphertext));
// Client receives and decrypts
const clientReceivedReady = await new Promise<Buffer>((resolve) => {
clientWs.once("message", (data) => resolve(data as Buffer));
});
const decryptedReady = await decrypt(
clientSharedKey,
clientReceivedReady.buffer.slice(
clientReceivedReady.byteOffset,
clientReceivedReady.byteOffset + clientReceivedReady.byteLength,
),
);
expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" });
// Client receives and decrypts
const clientReceivedReady = await new Promise<Buffer>((resolve) => {
clientWs.once("message", (data) => resolve(data as Buffer));
});
const decryptedReady = await decrypt(
clientSharedKey,
clientReceivedReady.buffer.slice(
clientReceivedReady.byteOffset,
clientReceivedReady.byteOffset + clientReceivedReady.byteLength,
),
);
expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" });
// Client sends encrypted message
const clientMessage = "Hello from client!";
const clientCiphertext = await encrypt(clientSharedKey, clientMessage);
clientWs.send(Buffer.from(clientCiphertext));
// Client sends encrypted message
const clientMessage = "Hello from client!";
const clientCiphertext = await encrypt(clientSharedKey, clientMessage);
clientWs.send(Buffer.from(clientCiphertext));
// Daemon receives and decrypts
const daemonReceivedMsg = await new Promise<Buffer>((resolve) => {
daemonWs.once("message", (data) => resolve(data as Buffer));
});
const decryptedClientMsg = await decrypt(
daemonSharedKey,
daemonReceivedMsg.buffer.slice(
daemonReceivedMsg.byteOffset,
daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength,
),
);
expect(decryptedClientMsg).toBe(clientMessage);
// Daemon receives and decrypts
const daemonReceivedMsg = await new Promise<Buffer>((resolve) => {
daemonWs.once("message", (data) => resolve(data as Buffer));
});
const decryptedClientMsg = await decrypt(
daemonSharedKey,
daemonReceivedMsg.buffer.slice(
daemonReceivedMsg.byteOffset,
daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength,
),
);
expect(decryptedClientMsg).toBe(clientMessage);
// Daemon sends encrypted response
const daemonMessage = "Hello from daemon!";
const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage);
daemonWs.send(Buffer.from(daemonCiphertext));
// Daemon sends encrypted response
const daemonMessage = "Hello from daemon!";
const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage);
daemonWs.send(Buffer.from(daemonCiphertext));
// Client receives and decrypts
const clientReceivedMsg = await new Promise<Buffer>((resolve) => {
clientWs.once("message", (data) => resolve(data as Buffer));
});
const decryptedDaemonMsg = await decrypt(
clientSharedKey,
clientReceivedMsg.buffer.slice(
clientReceivedMsg.byteOffset,
clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength,
),
);
expect(decryptedDaemonMsg).toBe(daemonMessage);
// Client receives and decrypts
const clientReceivedMsg = await new Promise<Buffer>((resolve) => {
clientWs.once("message", (data) => resolve(data as Buffer));
});
const decryptedDaemonMsg = await decrypt(
clientSharedKey,
clientReceivedMsg.buffer.slice(
clientReceivedMsg.byteOffset,
clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength,
),
);
expect(decryptedDaemonMsg).toBe(daemonMessage);
// Cleanup
daemonWs.close();
clientWs.close();
});
// Cleanup
daemonWs.close();
clientWs.close();
},
);
it("relay only sees opaque bytes after handshake", { timeout: 90_000 }, async () => {
const serverId = "opaque-test-" + Date.now();
+1 -5
View File
@@ -1,8 +1,4 @@
export {
createClientChannel,
createDaemonChannel,
EncryptedChannel,
} from "./encrypted-channel.js";
export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js";
export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
export {
+2 -9
View File
@@ -1,7 +1,4 @@
export type {
ConnectionRole,
RelaySessionAttachment,
} from "./types.js";
export type { ConnectionRole, RelaySessionAttachment } from "./types.js";
export {
generateKeyPair,
@@ -12,9 +9,5 @@ export {
decrypt,
} from "./crypto.js";
export {
createClientChannel,
createDaemonChannel,
EncryptedChannel,
} from "./encrypted-channel.js";
export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js";
export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
+1 -3
View File
@@ -3,9 +3,7 @@ account_id = "10ed39a1dbf316e30abd0c409bed40d6"
main = "src/cloudflare-adapter.ts"
compatibility_date = "2024-12-01"
routes = [
{ pattern = "relay.paseo.sh", custom_domain = true }
]
routes = [{ pattern = "relay.paseo.sh", custom_domain = true }]
[observability]
enabled = true
+31 -10
View File
@@ -11,6 +11,7 @@ Paseo is a mobile + CLI app for monitoring and controlling local AI coding agent
## Build / Lint / Test Commands
### Root (monorepo)
```bash
npm run dev # Start daemon + Expo in Tmux
npm run build:daemon # Build: highlight + relay + server + cli
@@ -20,6 +21,7 @@ npm run format # Format with Biome (in-place)
```
### Server package (`packages/server`)
```bash
npm run dev # Start dev daemon (tsx watch)
npm run build # Build lib + scripts to dist/
@@ -45,6 +47,7 @@ npm run test:ui # Vitest UI at localhost:51204
```
### Other useful commands
```bash
npm run build --workspace=@getpaseo/relay # Rebuild relay before daemon
npm run build --workspace=@getpaseo/server # Rebuild server
@@ -58,13 +61,22 @@ npm run cli -- daemon status # Check daemon status
## Code Style
### Biome (formatting only, no linting)
```json
{ "indentStyle": "space", "indentWidth": 2, "lineWidth": 100, "quoteStyle": "double", "trailingCommas": "all", "semicolons": "always" }
{
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"quoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always"
}
```
### TypeScript
- **Fully strict** — no `any`, no implicit `any`
- **`interface`** over `type`** when possible
- **`interface`** over `type`\*\* when possible
- **`function` declarations** over arrow function assignments
- **Named types** — no complex inline types in public signatures
- **Object parameters** — use single object param when >1 argument
@@ -72,15 +84,18 @@ npm run cli -- daemon status # Check daemon status
- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true`
### Imports
- Use path alias `@server/*` in server package (maps to `./src/`)
- No barrel `index.ts` re-exports — they create unnecessary indirection
### Naming
- Files: `kebab-case.ts` named after the main export (`create-tool-call.ts`)
- Tests: collocated with implementation (`thing.test.ts`)
- No prefixes like `RpcX`, `DbX`, `UiX` — keep one canonical type per concept
### Error Handling
- **Fail explicitly** — throw instead of silently returning defaults
- **Typed domain errors** — extend `Error` with structured metadata
@@ -97,10 +112,16 @@ class TimeoutError extends Error {
```
### State Design
Discriminated unions over bags of booleans/optionals:
```typescript
// Bad
interface FetchState { isLoading: boolean; error?: Error; data?: Data }
interface FetchState {
isLoading: boolean;
error?: Error;
data?: Data;
}
// Good
type FetchState =
@@ -168,10 +189,10 @@ npm run db:query -- "SELECT * FROM agent_timeline_rows..."
## Relevant Docs
| File | What it covers |
|---|---|
| [../CLAUDE.md](../CLAUDE.md) | Repository overview, critical rules, quick start |
| [../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) | System design, WebSocket protocol, data flow |
| [../docs/CODING_STANDARDS.md](../docs/CODING_STANDARDS.md) | Type hygiene, error handling, React patterns |
| [../docs/TESTING.md](../docs/TESTING.md) | TDD workflow, determinism, real deps over mocks |
| [../SECURITY.md](../SECURITY.md) | Relay threat model, E2E encryption |
| File | What it covers |
| ---------------------------------------------------------- | ------------------------------------------------ |
| [../CLAUDE.md](../CLAUDE.md) | Repository overview, critical rules, quick start |
| [../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) | System design, WebSocket protocol, data flow |
| [../docs/CODING_STANDARDS.md](../docs/CODING_STANDARDS.md) | Type hygiene, error handling, React patterns |
| [../docs/TESTING.md](../docs/TESTING.md) | TDD workflow, determinism, real deps over mocks |
| [../SECURITY.md](../SECURITY.md) | Relay threat model, E2E encryption |
+4
View File
@@ -49,15 +49,18 @@ npm start
## Project Status
**✅ Completed** (Phases 1-2):
- Package setup and configuration
- Express server with WebSocket
- React UI with Vite
- WebSocket client with ping/pong testing
**⏳ In Progress** (Phase 3):
- Terminal control (tmux integration)
**📋 Planned** (Phases 4-9):
- LLM integration (OpenAI GPT-4)
- Agent orchestrator
- Speech-to-Text (Deepgram)
@@ -95,6 +98,7 @@ PASEO_HOME=~/.paseo-blue PASEO_LISTEN=127.0.0.1:7777 npm run dev
## Testing
Currently manual testing via:
1. Start servers: `npm run dev`
2. Open http://localhost:5173
3. Test WebSocket connection (green status indicator)
+5 -5
View File
@@ -2,11 +2,6 @@
"name": "@getpaseo/server",
"version": "0.1.62",
"description": "Paseo backend server",
"type": "module",
"publishConfig": {
"access": "public"
},
"types": "./dist/server/server/exports.d.ts",
"files": [
"dist/server",
"dist/src",
@@ -15,6 +10,8 @@
"README.md",
".env.example"
],
"type": "module",
"types": "./dist/server/server/exports.d.ts",
"exports": {
".": {
"types": "./dist/server/server/exports.d.ts",
@@ -27,6 +24,9 @@
"default": "./dist/server/utils/tool-call-parsers.js"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development node --import tsx scripts/dev-runner.ts",
"dev:tsx": "cross-env NODE_ENV=development tsx watch --ignore '**/*.timestamp-*' src/server/index.ts",
+4 -5
View File
@@ -232,11 +232,10 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
labels?: Record<string, string>;
}
export interface CreatePaseoWorktreeInput
extends Pick<
CreatePaseoWorktreeRequest,
"cwd" | "worktreeSlug" | "attachments" | "refName" | "action" | "githubPrNumber"
> {}
export interface CreatePaseoWorktreeInput extends Pick<
CreatePaseoWorktreeRequest,
"cwd" | "worktreeSlug" | "attachments" | "refName" | "action" | "githubPrNumber"
> {}
type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
type SubscribeCheckoutDiffPayload = Extract<
@@ -130,9 +130,7 @@ function buildZodValidator<T>(schema: z.ZodTypeAny, schemaName: string): SchemaV
function buildJsonSchemaValidator<T>(schema: JsonSchema): SchemaValidator<T> {
const AjvConstructor = Ajv as unknown as {
new (
options?: AjvOptions,
): {
new (options?: AjvOptions): {
compile: (input: JsonSchema) => ((value: unknown) => boolean) & {
errors?: ErrorObject[] | null;
};
@@ -1203,9 +1203,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
? (
await Promise.all(
terminalManager.listDirectories().map(async (directory) =>
(
await terminalManager.getTerminals(directory)
).map((terminal) => ({
(await terminalManager.getTerminals(directory)).map((terminal) => ({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
@@ -8,4 +8,3 @@
* This file is kept as a stub to document the migration.
* It can be deleted once the refactoring is confirmed working.
*/
@@ -49,9 +49,7 @@ export class ClaudeSidechainTracker {
private readonly activeSidechains = new Map<string, SubAgentActivityState>();
private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
constructor(input: {
getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
}) {
constructor(input: { getToolInput: (toolUseId: string) => AgentMetadata | null | undefined }) {
this.getToolInput = input.getToolInput;
}
@@ -291,11 +291,7 @@ export class MockLoadTestAgentSession implements AgentSession {
private modeId: string | null;
private modelId: string | null;
constructor(options: {
config: AgentSessionConfig;
sessionId: string;
logger?: Logger;
}) {
constructor(options: { config: AgentSessionConfig; sessionId: string; logger?: Logger }) {
this.id = options.sessionId;
this.logger = options.logger;
this.modeId = options.config.modeId ?? MOCK_LOAD_TEST_MODE_ID;
@@ -192,26 +192,23 @@ describe.runIf(process.platform === "win32")("Windows provider launch parity", (
});
});
test.each(
providerLaunchCases,
)("$provider launches a cmd shim from a path with spaces through spawnProcess", async ({
binaryName,
args,
shell,
}) => {
const fixture = makeFixture(binaryName, args);
test.each(providerLaunchCases)(
"$provider launches a cmd shim from a path with spaces through spawnProcess",
async ({ binaryName, args, shell }) => {
const fixture = makeFixture(binaryName, args);
const result = await runProviderFixture({
command: fixture.shim,
args: fixture.expectedArgs,
expectedArgs: fixture.expectedArgs,
shell,
});
const result = await runProviderFixture({
command: fixture.shim,
args: fixture.expectedArgs,
expectedArgs: fixture.expectedArgs,
shell,
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
});
expect(result.error).toBeNull();
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.stderr).toBe("");
expect(result.stdout.trim()).toBe("ARGV_OK");
},
);
});
@@ -78,52 +78,51 @@ describe("daemon E2E", () => {
await ctx.cleanup();
}, 60000);
describe.each([
"claude",
"codex",
"opencode",
] as const)("live model switching (%s)", (provider) => {
const shouldRun =
provider === "claude" ||
(provider === "codex" && hasCodex) ||
(provider === "opencode" && hasOpenCode);
describe.each(["claude", "codex", "opencode"] as const)(
"live model switching (%s)",
(provider) => {
const shouldRun =
provider === "claude" ||
(provider === "codex" && hasCodex) ||
(provider === "opencode" && hasOpenCode);
test.runIf(shouldRun)(
"updates agent model without restarting",
async () => {
const cwd = tmpCwd();
try {
const modelList = await ctx.client.listProviderModels(provider);
if (!modelList.models || modelList.models.length === 0) {
throw new Error(`No models returned for provider ${provider}`);
test.runIf(shouldRun)(
"updates agent model without restarting",
async () => {
const cwd = tmpCwd();
try {
const modelList = await ctx.client.listProviderModels(provider);
if (!modelList.models || modelList.models.length === 0) {
throw new Error(`No models returned for provider ${provider}`);
}
const [modelA, modelB] = pickTwoDistinctModels(modelList.models);
const agent = await ctx.client.createAgent({
provider,
cwd,
title: `Model Switch (${provider})`,
model: modelA,
});
const startIndex = messages.length;
await ctx.client.setAgentModel(agent.id, modelB);
const updated = await waitForAgentUpdate(
messages,
startIndex,
(a) => a.id === agent.id && a.model === modelB,
{ timeoutMs: 20000 },
);
expect(updated.model).toBe(modelB);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
const [modelA, modelB] = pickTwoDistinctModels(modelList.models);
const agent = await ctx.client.createAgent({
provider,
cwd,
title: `Model Switch (${provider})`,
model: modelA,
});
const startIndex = messages.length;
await ctx.client.setAgentModel(agent.id, modelB);
const updated = await waitForAgentUpdate(
messages,
startIndex,
(a) => a.id === agent.id && a.model === modelB,
{ timeoutMs: 20000 },
);
expect(updated.model).toBe(modelB);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
},
180000,
);
});
},
180000,
);
},
);
test("live thinking switching works for Claude (off -> on)", async () => {
const cwd = tmpCwd();
+1 -5
View File
@@ -35,11 +35,7 @@ export {
type ProviderOverride,
type ProviderProfileModel,
} from "./agent/provider-launch-config.js";
export {
findExecutable,
quoteWindowsArgument,
quoteWindowsCommand,
} from "../utils/executable.js";
export { findExecutable, quoteWindowsArgument, quoteWindowsCommand } from "../utils/executable.js";
export { execCommand, spawnProcess } from "../utils/spawn.js";
// Provider manifest (source of truth for provider definitions)
@@ -1776,9 +1776,9 @@ describe("workspace aggregation", () => {
rmSync(tempDir, { recursive: true, force: true });
}
const response = emitted.find((message) => message.type === "create_paseo_worktree_response") as
| { type: "create_paseo_worktree_response"; payload: any }
| undefined;
const response = emitted.find(
(message) => message.type === "create_paseo_worktree_response",
) as { type: "create_paseo_worktree_response"; payload: any } | undefined;
expect(response?.payload.error).toBeNull();
expect(response?.payload.workspace).toMatchObject({
@@ -7,8 +7,5 @@ export {
type DaemonEvent,
type DaemonEventHandler,
} from "./daemon-client.js";
export {
createDaemonTestContext,
type DaemonTestContext,
} from "./daemon-test-context.js";
export { createDaemonTestContext, type DaemonTestContext } from "./daemon-test-context.js";
export { useTempClaudeConfigDir } from "./claude-config.js";
@@ -172,20 +172,19 @@ describe("pull request timeline message schemas", () => {
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "x" });
});
test.each([
"rate_limited",
"",
undefined,
])("normalizes malformed error kind %j to unknown", (kind) => {
const parsed = PullRequestTimelineResponseSchema.parse({
type: "pull_request_timeline_response",
payload: {
error: { kind },
},
});
test.each(["rate_limited", "", undefined])(
"normalizes malformed error kind %j to unknown",
(kind) => {
const parsed = PullRequestTimelineResponseSchema.parse({
type: "pull_request_timeline_response",
payload: {
error: { kind },
},
});
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" });
});
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" });
},
);
});
describe("checkout PR status compatibility", () => {
+1 -1
View File
@@ -6,7 +6,7 @@ main = "@tanstack/react-start/server-entry"
routes = [
{ pattern = "paseo.sh", custom_domain = true },
{ pattern = "www.paseo.sh", custom_domain = true }
{ pattern = "www.paseo.sh", custom_domain = true },
]
[observability]