Compare commits

...
Author SHA1 Message Date
Mohamed Boudra ea7602b968 fix(app): copy complete assistant turns on iOS
Completed turns can span the rendered history and live-head segments. Preserve that boundary in the footer host so Copy turn includes assistant text from both segments.
2026-07-22 11:44:37 +00:00
Mohamed Boudra 68993b7ab3 fix(desktop): preserve Windows terminal hook smoke command
The outer batch shell expanded the hook variable and control operators before terminal send-keys received them. Encode the probe for PowerShell so expansion happens inside the packaged terminal.
2026-07-22 13:16:27 +02:00
Mohamed Boudra 246c07fba5 fix(cli): make new workspace creation explicit (#2315)
Agent callers now stay in their current workspace unless --new-workspace explicitly requests a separate local or worktree workspace.
2026-07-22 18:26:40 +08:00
Mohamed Boudra 0afcc96370 docs(website): correct provider usage guidance 2026-07-22 12:25:01 +02:00
Mohamed Boudra 3a8d08ba91 Refactor changelog entries and improve descriptions 2026-07-22 12:25:01 +02:00
paseo-ai[bot] f22a8ea8e0 fix: update lockfile signatures and Nix hash [skip ci] 2026-07-22 09:05:28 +00:00
17 changed files with 267 additions and 130 deletions
+5 -5
View File
@@ -5,8 +5,6 @@
### Added
- Edit files directly in the web and desktop apps ([#2270](https://github.com/getpaseo/paseo/pull/2270), [#2309](https://github.com/getpaseo/paseo/pull/2309))
- Automate local and worktree-backed workspaces through the CLI and agent tools ([#2186](https://github.com/getpaseo/paseo/pull/2186))
- Start a new workspace from a pasted pull request or merge request link ([#2290](https://github.com/getpaseo/paseo/pull/2290))
- Switch models from the Command Center for active agents and new drafts ([#2147](https://github.com/getpaseo/paseo/pull/2147) by [@kedrzu](https://github.com/kedrzu))
- Configure workspace service ports with a fixed range or external allocator ([#2165](https://github.com/getpaseo/paseo/pull/2165) by [@mcowger](https://github.com/mcowger))
- Search keyboard shortcuts by action, note, or key combination ([#2160](https://github.com/getpaseo/paseo/pull/2160))
@@ -15,9 +13,11 @@
### Improved
- Workspace and chat sync uses less data without losing or reordering history ([#2028](https://github.com/getpaseo/paseo/pull/2028), [#2185](https://github.com/getpaseo/paseo/pull/2185), [#2196](https://github.com/getpaseo/paseo/pull/2196), [#2206](https://github.com/getpaseo/paseo/pull/2206), [#2259](https://github.com/getpaseo/paseo/pull/2259), [#2263](https://github.com/getpaseo/paseo/pull/2263))
- Added folders remain independent projects across nested repositories, worktrees, and filesystem mounts ([#2098](https://github.com/getpaseo/paseo/pull/2098), [#2187](https://github.com/getpaseo/paseo/pull/2187))
- Idle agents release provider resources automatically and resume when needed ([#2203](https://github.com/getpaseo/paseo/pull/2203), [#2209](https://github.com/getpaseo/paseo/pull/2209))
- Improved parity of CLI and MCP tools for workspace, agent and schedule management ([#2186](https://github.com/getpaseo/paseo/pull/2186))
- Pasted PR/MR links in the composer become auto-selected as a checkout option ([#2290](https://github.com/getpaseo/paseo/pull/2290))
- Projects, workspaces and chat syncing is more efficient ([#2028](https://github.com/getpaseo/paseo/pull/2028), [#2185](https://github.com/getpaseo/paseo/pull/2185), [#2196](https://github.com/getpaseo/paseo/pull/2196), [#2206](https://github.com/getpaseo/paseo/pull/2206), [#2259](https://github.com/getpaseo/paseo/pull/2259), [#2263](https://github.com/getpaseo/paseo/pull/2263))
- Make project creation more explicit ([#2098](https://github.com/getpaseo/paseo/pull/2098), [#2187](https://github.com/getpaseo/paseo/pull/2187))
- Idle agents release processes automatically and resume when needed ([#2203](https://github.com/getpaseo/paseo/pull/2203), [#2209](https://github.com/getpaseo/paseo/pull/2209))
- New Claude and Codex agents default to safer automatic approval modes when supported ([#2213](https://github.com/getpaseo/paseo/pull/2213))
- Oh My Pi now supports Max thinking in imported and new sessions ([#2191](https://github.com/getpaseo/paseo/pull/2191) by [@mvanhorn](https://github.com/mvanhorn))
- Commit history now includes recent pushed and base-branch commits ([#2312](https://github.com/getpaseo/paseo/pull/2312))
+1 -1
View File
@@ -383,7 +383,7 @@ install.
Use `npm run cli` to run the in-repo CLI from source (`npx tsx packages/cli/src/index.ts`). The script wraps the CLI with `scripts/dev-home.sh`, so it automatically uses this checkout's `.dev/paseo-home` and dev daemon endpoint unless you pass an explicit override. The globally installed `paseo` binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use `npm run cli` when you want to talk to the CLI you are editing.
Canonical automation uses `paseo workspace create/ls/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --isolation local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases.
Canonical automation uses `paseo workspace create/ls/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases.
```bash
npm run cli -- ls -a -g # List all agents globally
+1 -1
View File
@@ -1 +1 @@
sha256-prXQpWLMwN1JO2tGhGDie/Ud9vnmUSF44581Mm7R7eA=
sha256-Ys/R5+0O3SDYeCSd7hatTuCwV9S0kIO8vhmPKGZsvn0=
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { TurnTiming } from "@/timeline/turn-time";
import type { StreamItem } from "@/types/stream";
import {
collectAssistantTurnContentForStreamRenderStrategy,
orderHeadForStreamRenderStrategy,
orderTailForStreamRenderStrategy,
type StreamStrategy,
@@ -310,6 +311,34 @@ describe("layoutStream", () => {
expect(footerOwners(layout)).toEqual([assistant.id]);
});
it.each(["web", "android"] as const)(
"copies every assistant block when a completed turn spans history and live head on %s",
(platform) => {
const strategy = strategyFor(platform);
const layout = layoutFor({
platform,
tail: [
userMessage("u1", 1),
assistantMessage("first block", 2, { groupId: "turn-1", index: 0 }),
assistantMessage("second block", 3, { groupId: "turn-1", index: 1 }),
],
head: [assistantMessage("final block", 4, { groupId: "turn-1", index: 2 })],
});
const footer = layout.auxiliaryTurnFooter;
expect(footer).not.toBeNull();
expect(
collectAssistantTurnContentForStreamRenderStrategy({
strategy,
items: footer!.items,
startIndex: footer!.startIndex,
precedingItems: footer!.precedingItems,
precedingStartIndex: footer!.precedingStartIndex,
}),
).toBe("first block\n\nsecond block\n\nfinal block");
},
);
it.each(["web", "android"] as const)(
"places inline footer after trailing visible tool rows before the next user on %s",
(platform) => {
+13
View File
@@ -10,6 +10,8 @@ export interface TurnFooterHost {
items: StreamItem[];
timing?: TurnTiming;
startIndex: number;
precedingItems: StreamItem[] | null;
precedingStartIndex: number | null;
}
export interface StreamLayoutItem {
@@ -66,12 +68,16 @@ function createTurnFooterHost(input: {
items: StreamItem[];
index: number;
timingByAssistantId: Map<string, TurnTiming>;
precedingItems?: StreamItem[] | null;
precedingStartIndex?: number | null;
}): TurnFooterHost {
return {
itemId: input.item.id,
items: input.items,
timing: input.timingByAssistantId.get(input.item.id),
startIndex: input.index,
precedingItems: input.precedingItems ?? null,
precedingStartIndex: input.precedingStartIndex ?? null,
};
}
@@ -141,6 +147,11 @@ function resolveAuxiliaryTurnFooter(input: StreamLayoutInput): TurnFooterHost |
items: assistant.items,
index: assistant.index,
timingByAssistantId: input.timingByAssistantId,
precedingItems: footerItems === input.liveHead ? input.history : null,
precedingStartIndex:
footerItems === input.liveHead
? input.strategy.getHistoryLiveBoundaryIndex(input.history)
: null,
});
}
@@ -174,6 +185,8 @@ function resolveCompletedFooter(input: {
items: assistant.items,
index: assistant.index,
timingByAssistantId: input.timingByAssistantId,
precedingItems: input.boundaryAboveItems,
precedingStartIndex: input.boundaryAboveIndex,
});
}
+18 -1
View File
@@ -272,8 +272,25 @@ export function collectAssistantTurnContentForStreamRenderStrategy(params: {
strategy: StreamStrategy;
items: StreamItem[];
startIndex: number;
precedingItems?: StreamItem[] | null;
precedingStartIndex?: number | null;
}): string {
return params.strategy.collectAssistantTurnContent(params.items, params.startIndex);
const content = params.strategy.collectAssistantTurnContent(params.items, params.startIndex);
if (!params.precedingItems || params.precedingStartIndex == null) {
return content;
}
const precedingContent = params.strategy.collectAssistantTurnContent(
params.precedingItems,
params.precedingStartIndex,
);
if (!precedingContent) {
return content;
}
if (!content) {
return precedingContent;
}
return `${precedingContent}\n\n${content}`;
}
export function isNearBottomForStreamRenderStrategy(
+15 -27
View File
@@ -3,8 +3,6 @@ import { View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import type { Theme } from "@/styles/theme";
import type { TurnTiming } from "@/timeline/turn-time";
import type { StreamItem } from "@/types/stream";
import {
collectAssistantTurnContentForStreamRenderStrategy,
type StreamStrategy,
@@ -62,9 +60,7 @@ export const TurnFooter = memo(function TurnFooter({
return (
<CompletedTurnFooterRow
strategy={strategy}
items={host.items}
timing={host.timing}
startIndex={host.startIndex}
host={host}
supportsTimelineCursor={supportsTimelineCursor}
onForkAssistantTurn={onForkAssistantTurn}
/>
@@ -73,16 +69,12 @@ export const TurnFooter = memo(function TurnFooter({
export const CompletedTurnFooterRow = memo(function CompletedTurnFooterRow({
strategy,
items,
timing,
startIndex,
host,
supportsTimelineCursor,
onForkAssistantTurn,
}: {
strategy: TurnContentStrategy;
items: StreamItem[];
timing?: TurnTiming;
startIndex: number;
host: TurnFooterHost;
supportsTimelineCursor: boolean;
onForkAssistantTurn?: AssistantTurnForkHandler;
}) {
@@ -90,9 +82,7 @@ export const CompletedTurnFooterRow = memo(function CompletedTurnFooterRow({
<TurnFooterRow>
<CompletedTurnFooter
strategy={strategy}
items={items}
timing={timing}
startIndex={startIndex}
host={host}
supportsTimelineCursor={supportsTimelineCursor}
onForkAssistantTurn={onForkAssistantTurn}
/>
@@ -133,16 +123,12 @@ function RunningTurnFooter({ inFlightTurnStartedAt }: { inFlightTurnStartedAt: D
function CompletedTurnFooter({
strategy,
items,
timing,
startIndex,
host,
supportsTimelineCursor,
onForkAssistantTurn,
}: {
strategy: TurnContentStrategy;
items: StreamItem[];
timing?: TurnTiming;
startIndex: number;
host: TurnFooterHost;
supportsTimelineCursor: boolean;
onForkAssistantTurn?: AssistantTurnForkHandler;
}) {
@@ -150,14 +136,16 @@ function CompletedTurnFooter({
() =>
collectAssistantTurnContentForStreamRenderStrategy({
strategy,
items,
startIndex,
items: host.items,
startIndex: host.startIndex,
precedingItems: host.precedingItems,
precedingStartIndex: host.precedingStartIndex,
}),
[strategy, items, startIndex],
[strategy, host],
);
const boundary = resolveAssistantTurnForkBoundary({
items,
startIndex,
items: host.items,
startIndex: host.startIndex,
supportsTimelineCursor,
});
const handleFork = useCallback(
@@ -173,8 +161,8 @@ function CompletedTurnFooter({
<View style={stylesheet.turnFooterSlot}>
<AssistantTurnFooter
getContent={getContent}
completedAt={timing?.completedAt}
durationMs={timing?.durationMs}
completedAt={host.timing?.completedAt}
durationMs={host.timing?.durationMs}
onFork={boundary && onForkAssistantTurn ? handleFork : undefined}
/>
</View>
+1 -3
View File
@@ -153,9 +153,7 @@ function renderStreamItemWithTurnFooter(input: {
const footer = footerHost ? (
<CompletedTurnFooterRow
strategy={input.strategy}
items={footerHost.items}
timing={footerHost.timing}
startIndex={footerHost.startIndex}
host={footerHost}
supportsTimelineCursor={input.supportsTimelineCursor}
onForkAssistantTurn={input.onForkAssistantTurn}
/>
+16 -3
View File
@@ -10,10 +10,23 @@ describe("canonical CLI surface", () => {
expect(help).not.toContain("worktree");
});
it("hides legacy run worktree syntax", () => {
it("names explicit workspace creation without exposing older syntax", () => {
const run = createCli().commands.find((command) => command.name() === "run");
expect(run?.helpInformation()).toContain("--isolation <local|worktree>");
expect(run?.helpInformation()).not.toContain("--worktree <name>");
const help = run?.helpInformation();
expect(help).toContain("--new-workspace <local|worktree>");
expect(help).not.toContain("--isolation");
expect(help).not.toContain("--worktree <name>");
});
it("offers the worktree creation options on run", () => {
const run = createCli().commands.find((command) => command.name() === "run");
const help = run?.helpInformation();
expect(help).toContain("--worktree-mode <mode>");
expect(help).toContain("--worktree-slug <slug>");
expect(help).toContain("--new-branch <name>");
expect(help).toContain("--branch <name>");
expect(help).toContain("--pr-number <n>");
expect(help).toContain("--forge <forge>");
});
it("uses background for execution and reserves detach for ownership", () => {
+22 -8
View File
@@ -72,23 +72,37 @@ describe("runRunCommand option validation", () => {
});
}
it("rejects --isolation combined with --workspace", async () => {
it("rejects --new-workspace combined with --workspace", async () => {
await expectInvalidOptions(
{ isolation: "worktree", workspace: "ws-1" },
/--isolation and --workspace cannot be combined/,
{ newWorkspace: "worktree", workspace: "ws-1" },
/--new-workspace and --workspace cannot be combined/,
);
});
it("allows explicit worktree isolation through validation", async () => {
// Explicit isolation with no --workspace
it("allows explicit worktree workspace creation through validation", async () => {
// Explicit workspace creation with no --workspace
// must clear validation. It still fails later (provider resolution), which
// is enough to prove the new guard did not reject it.
await expect(
runRunCommand("do something", { isolation: "worktree", provider: undefined }, {} as never),
runRunCommand("do something", { newWorkspace: "worktree", provider: undefined }, {} as never),
).rejects.not.toMatchObject({ code: "INVALID_OPTIONS" });
});
it("rejects unknown workspace isolation", async () => {
await expectInvalidOptions({ isolation: "container" }, /Unsupported workspace isolation/);
it("rejects unknown new workspace kinds", async () => {
await expectInvalidOptions({ newWorkspace: "container" }, /Unsupported new workspace kind/);
});
it("rejects two workspace creation flags", async () => {
await expectInvalidOptions(
{ newWorkspace: "local", worktree: "legacy-slug" },
/--new-workspace and --worktree cannot be combined/,
);
});
it("rejects an unknown worktree creation mode before connecting", async () => {
await expectInvalidOptions(
{ newWorkspace: "worktree", worktreeMode: "container" },
/Unsupported worktree mode/,
);
});
});
+98 -38
View File
@@ -14,6 +14,7 @@ import { lookup } from "mime-types";
import { parseDuration } from "../../utils/duration.js";
import { collectMultiple } from "../../utils/command-options.js";
import { resolveProviderAndModel } from "../../utils/provider-model.js";
import { buildWorkspaceSource } from "../workspace/create.js";
export { resolveProviderAndModel } from "../../utils/provider-model.js";
@@ -38,12 +39,21 @@ export function addRunOptions(cmd: Command): Command {
)
.option("--thinking <id>", "Thinking option ID to use for this run")
.option("--mode <mode>", "Provider-specific mode (e.g., plan, default, bypass)")
.option("--isolation <local|worktree>", "Create a new workspace with this isolation")
.option("--new-workspace <local|worktree>", "Create a separate local or worktree workspace")
.addOption(new Option("--worktree <name>", "Legacy workspace isolation alias").hideHelp())
.option("--base <branch>", "Base branch for an isolated workspace")
.option(
"--worktree-mode <mode>",
"Worktree mode: branch-off, checkout-branch, or checkout-pr",
)
.option("--worktree-slug <slug>", "Managed worktree path slug")
.option("--new-branch <name>", "New branch name for branch-off mode")
.option("--base <ref>", "Base ref for branch-off mode")
.option("--branch <name>", "Existing branch for checkout-branch mode")
.option("--pr-number <n>", "Pull request or change request number for checkout-pr mode")
.option("--forge <forge>", "Forge for checkout-pr mode")
.option(
"--workspace <id>",
"Run in an existing workspace (default: a new workspace is created per run; falls back to $PASEO_WORKSPACE_ID)",
"Run in an existing workspace (defaults to the caller workspace when agent-scoped)",
)
.option(
"--image <path>",
@@ -105,9 +115,15 @@ export interface AgentRunOptions extends CommandOptions {
model?: string;
thinking?: string;
mode?: string;
isolation?: string;
newWorkspace?: string;
worktree?: string;
worktreeMode?: string;
worktreeSlug?: string;
newBranch?: string;
base?: string;
branch?: string;
prNumber?: string;
forge?: string;
workspace?: string;
image?: string[];
cwd?: string;
@@ -117,6 +133,25 @@ export interface AgentRunOptions extends CommandOptions {
outputSchema?: string;
}
function resolveNewWorkspaceKind(options: AgentRunOptions): string | undefined {
return options.newWorkspace ?? (options.worktree ? "worktree" : undefined);
}
function buildRunWorkspaceSource(options: AgentRunOptions, cwd: string) {
const newWorkspace = resolveNewWorkspaceKind(options) ?? "local";
return buildWorkspaceSource({
isolation: newWorkspace,
path: cwd,
mode: options.worktreeMode,
worktreeSlug: options.worktreeSlug ?? options.worktree,
newBranch: options.newBranch,
base: options.base,
branch: options.branch,
prNumber: options.prNumber,
forge: options.forge,
});
}
function toRunResult(
agent: AgentSnapshotPayload,
statusOverride?: AgentRunResult["status"],
@@ -269,37 +304,61 @@ function structuredRunSchema(output: Record<string, unknown>): OutputSchema<Agen
};
}
function validateRunOptions(prompt: string, options: AgentRunOptions, outputSchema: unknown): void {
if (!prompt || prompt.trim().length === 0) {
function validateRunWorkspaceOptions(options: AgentRunOptions): void {
const newWorkspace = resolveNewWorkspaceKind(options);
if (
options.newWorkspace &&
options.newWorkspace !== "local" &&
options.newWorkspace !== "worktree"
) {
throw {
code: "MISSING_PROMPT",
message: "A prompt is required",
details: "Usage: paseo agent run [options] <prompt>",
code: "INVALID_OPTIONS",
message: `Unsupported new workspace kind: ${options.newWorkspace}`,
details: "Use --new-workspace local or --new-workspace worktree",
} satisfies CommandError;
}
const createsIsolatedWorkspace = options.isolation === "worktree" || Boolean(options.worktree);
if (options.isolation && options.isolation !== "local" && options.isolation !== "worktree") {
if (options.newWorkspace && options.worktree) {
throw {
code: "INVALID_OPTIONS",
message: `Unsupported workspace isolation: ${options.isolation}`,
details: "Use --isolation local or --isolation worktree",
message: "--new-workspace and --worktree cannot be combined",
details: "Use --new-workspace worktree and the supported worktree options",
} satisfies CommandError;
}
if (options.base && !createsIsolatedWorkspace) {
const hasWorktreeCreationOptions = [
options.worktreeMode,
options.worktreeSlug,
options.newBranch,
options.base,
options.branch,
options.prNumber,
options.forge,
].some((value) => value !== undefined);
if (hasWorktreeCreationOptions && newWorkspace !== "worktree") {
throw {
code: "INVALID_OPTIONS",
message: "--base can only be used with --isolation worktree",
details: "Usage: paseo agent run --isolation worktree --base <branch> <prompt>",
message: "Worktree options require --new-workspace worktree",
details: "Usage: paseo run --new-workspace worktree [worktree options] <prompt>",
} satisfies CommandError;
}
if (options.isolation && options.workspace) {
if (newWorkspace === "worktree") {
try {
buildRunWorkspaceSource(options, options.cwd ?? process.cwd());
} catch (error) {
throw {
code: "INVALID_OPTIONS",
message: error instanceof Error ? error.message : String(error),
} satisfies CommandError;
}
}
if (options.newWorkspace && options.workspace) {
throw {
code: "INVALID_OPTIONS",
message: "--isolation and --workspace cannot be combined",
details: "Select an existing workspace or create a new one with an isolation choice",
message: "--new-workspace and --workspace cannot be combined",
details: "Select an existing workspace or explicitly create a new one",
} satisfies CommandError;
}
@@ -309,9 +368,21 @@ function validateRunOptions(prompt: string, options: AgentRunOptions, outputSche
throw {
code: "INVALID_OPTIONS",
message: "--worktree and --workspace cannot be combined",
details: "Use --isolation worktree instead of the legacy --worktree flag",
details: "Use --new-workspace worktree instead of the legacy --worktree flag",
} satisfies CommandError;
}
}
function validateRunOptions(prompt: string, options: AgentRunOptions, outputSchema: unknown): void {
if (!prompt || prompt.trim().length === 0) {
throw {
code: "MISSING_PROMPT",
message: "A prompt is required",
details: "Usage: paseo agent run [options] <prompt>",
} satisfies CommandError;
}
validateRunWorkspaceOptions(options);
if (outputSchema && runsInBackground(options)) {
throw {
@@ -465,27 +536,25 @@ export async function resolveExistingRunWorkspace(
// 1. --workspace <id> -> run in that existing workspace
// 2. $PASEO_AGENT_ID -> daemon resolves the caller's workspace
// 3. $PASEO_WORKSPACE_ID -> exported by workspace terminals
// 4. --isolation <kind> -> mint a new workspace with explicit isolation
// 4. --new-workspace <kind> -> mint a new workspace explicitly
// 5. bare run -> mint a new local-backed workspace for cwd
async function resolveRunWorkspace(
client: ConnectedDaemonClient,
options: AgentRunOptions,
cwd: string,
): Promise<RunWorkspace> {
const requestedIsolation = options.isolation ?? (options.worktree ? "worktree" : undefined);
const explicit = requestedIsolation ? undefined : options.workspace?.trim();
const newWorkspace = resolveNewWorkspaceKind(options);
const explicit = newWorkspace ? undefined : options.workspace?.trim();
if (explicit) {
console.error(`Using workspace ${explicit}`);
return resolveExistingRunWorkspace(client, explicit);
}
if (!requestedIsolation && resolveRunCallerAgentId()) {
if (!newWorkspace && resolveRunCallerAgentId()) {
return { cwd };
}
const ambientWorkspaceId = requestedIsolation
? undefined
: process.env.PASEO_WORKSPACE_ID?.trim();
const ambientWorkspaceId = newWorkspace ? undefined : process.env.PASEO_WORKSPACE_ID?.trim();
if (ambientWorkspaceId) {
console.error(`Using workspace ${ambientWorkspaceId}`);
return resolveExistingRunWorkspace(client, ambientWorkspaceId);
@@ -493,17 +562,8 @@ async function resolveRunWorkspace(
// TODO: thread the run `prompt` as firstAgentContext so workspace-level
// title/branch generation picks up the task description (U8/U6 deferred).
const result =
requestedIsolation === "worktree"
? await client.createWorkspace({
source: {
kind: "worktree",
cwd,
worktreeSlug: options.worktree,
baseBranch: options.base,
},
})
: await client.createWorkspace({ source: { kind: "directory", path: cwd } });
const source = buildRunWorkspaceSource(options, cwd);
const result = await client.createWorkspace({ source });
if (!result.workspace) {
throw {
+24
View File
@@ -58,6 +58,8 @@ try {
assert(result.stdout.includes("--title"), "help should mention --title option");
assert(result.stdout.includes("--provider"), "help should mention --provider option");
assert(result.stdout.includes("--mode"), "help should mention --mode option");
assert(result.stdout.includes("--new-workspace"), "help should mention --new-workspace option");
assert(!result.stdout.includes("--isolation"), "help should not mention --isolation");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
assert(result.stdout.includes("--output-schema"), "help should mention --output-schema option");
assert(result.stdout.includes("--host"), "help should mention --host option");
@@ -244,6 +246,28 @@ try {
assert(output.includes("unknown option"), "should report unknown option for --ui");
console.log("✓ run --ui is rejected\n");
}
// Test 15: run --new-workspace is accepted
{
console.log("Test 15: run --new-workspace is accepted");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --new-workspace local "test prompt"`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --new-workspace");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ run --new-workspace is accepted\n");
}
// Test 16: run --isolation is rejected (unreleased flag removed)
{
console.log("Test 16: run --isolation is rejected");
const result =
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --isolation local "test prompt"`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail for removed --isolation flag");
const output = result.stdout + result.stderr;
assert(output.includes("unknown option"), "should report unknown option for --isolation");
console.log("✓ run --isolation is rejected\n");
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true });
@@ -130,7 +130,13 @@ function shellQuoteCliArg(value) {
function getTerminalHookSmokeCommand(marker) {
if (process.platform === "win32") {
return `"%PASEO_HOOK_CLI%" hooks codex Stop && echo ${marker}`;
const script = [
"& $env:PASEO_HOOK_CLI hooks codex Stop",
"if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }",
`Write-Output '${marker}'`,
].join("; ");
const encodedScript = Buffer.from(script, "utf16le").toString("base64");
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodedScript}`;
}
return `"$PASEO_HOOK_CLI" hooks codex Stop && echo ${marker}`;
+11 -39
View File
@@ -1,6 +1,6 @@
---
title: Claude Code
description: How Paseo runs Claude Code and how Anthropic's usage policy applies.
description: Run Claude Code in Paseo using your existing Claude plan.
nav: Claude Code
order: 23
category: Providers
@@ -8,54 +8,26 @@ category: Providers
# Claude Code
Paseo runs Claude Code through the official `claude` CLI using the same Claude Agent SDK that Claude Desktop uses internally.
Paseo runs Claude Code through the official `claude` CLI using the Claude Agent SDK.
## Anthropic's June 15, 2026 policy change
## Does Claude Code cost extra in Paseo?
Starting June 15, 2026, Anthropic splits Claude Code usage into two buckets. This is documented in their official support article: ["Use the Claude Agent SDK with your Claude plan"](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan).
No. Claude Code usage in Paseo counts against your normal Claude plan limits. It does not require a separate pool of Agent SDK credits.
**Interactive usage** — draws from your main subscription limits:
You still need a Claude plan that includes Claude Code, and your plan's usual usage limits apply.
- Claude Code in your terminal
- Claude Code in VS Code, JetBrains, and other IDEs
- Claude Desktop (Anthropic's own app)
- Claude chat on web, mobile, and desktop
## Getting started
**Programmatic usage** — draws from a separate monthly credit pool:
Install and sign in to the Claude Code CLI on the machine running Paseo. Paseo uses that existing installation and account when you start a Claude Code agent.
- `claude -p` (non-interactive/scripting mode)
- Claude Agent SDK usage in your own projects
- Claude Code GitHub Actions integration
- Third-party apps that authenticate through the Agent SDK
## Use Claude Code in the Paseo terminal
Credit amounts per month: Pro ($20), Max 5x ($100), Max 20x ($200), Team Standard ($20/seat), Team Premium ($100/seat). Credits don't roll over. After they run out, additional usage flows to pay-as-you-go API rates (if you have usage credits enabled).
Claude Code also works great inside the Paseo terminal. If you prefer the standard CLI experience, open a terminal in your workspace and run `claude` as usual.
## Where Paseo fits
Paseo uses the **Claude Agent SDK** to run Claude Code — the exact same mechanism Claude Desktop uses under the hood. Claude Desktop is whitelisted by Anthropic and counts as interactive usage. Paseo is not.
Even though the usage is practically interactive — you type prompts, review output, and approve tool calls in real time — Anthropic classifies Paseo as "programmatic usage" because it is a third-party app that authenticates through the Agent SDK.
## What this means for you
- Your **interactive** Claude Code usage (terminal, IDE, Claude Desktop) continues to draw from your main subscription limits, unchanged.
- Your **Paseo chat** usage draws from the separate Agent SDK monthly credits.
- Using Claude Code inside a Paseo terminal draws from your main subscription limits, same as any terminal.
## You can still use the terminal
Paseo has first-class terminal support. You can run Claude Code in your terminal exactly as you always have, and Paseo will still give you:
- **Worktree management** — create and switch git worktrees from the app
- **Remote access** — connect to your daemon from mobile or web while your terminal session runs locally
- **Git diffs** — review changes in a visual diff viewer
- **GitHub integration** — commit, push, open PRs, watch checks, and merge from the app
- **Agent supervision** — see all running agents, send follow-up prompts, and review output history
- **Relay** — access your terminal session from anywhere without exposing your machine
You can use the terminal from Paseo's desktop, web, or mobile app while keeping access to your workspace, git changes, and other Paseo tools.
## See also
- [Anthropic: Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan)
- [Custom providers](/docs/custom-providers), for custom binaries, third-party endpoints, or multiple Claude profiles.
- [Supported providers](/docs/supported-providers), for other agents you can run alongside Claude Code.
- [Custom providers](/docs/custom-providers), for custom binaries, third-party endpoints, or multiple Claude profiles.
- [Paseo vs Claude Desktop](/alternatives/claude-desktop), for a feature comparison.
+4 -2
View File
@@ -31,13 +31,15 @@ Use `paseo run` to start a new agent with a task:
paseo run "implement user authentication"
paseo run --provider codex "refactor the API layer"
paseo run --background "run the focused test suite"
paseo run --isolation worktree --base main "implement feature X"
paseo run --new-workspace worktree --worktree-mode branch-off --new-branch feature/x --base main "implement feature X"
paseo run --workspace <workspace-id> "review the current diff"
paseo run --output-schema schema.json "extract release notes"
paseo run --output-schema '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}' "summarize release notes"
```
From a human shell, a bare `paseo run` creates a new local workspace for the current directory. Use `--workspace <id>` to add the agent to an existing workspace, or `--isolation worktree` to create a new workspace backed by an isolated git worktree.
From a human shell, a bare `paseo run` creates a new local workspace for the current directory. Use `--workspace <id>` to add the agent to an existing workspace, or `--new-workspace local|worktree` to explicitly create a separate workspace for the run.
Worktree creation accepts `--worktree-mode branch-off|checkout-branch|checkout-pr` plus the matching `--new-branch`/`--base`, `--branch`, or `--pr-number`/`--forge` options. Use `--worktree-slug` to choose the managed directory slug.
When an existing Paseo agent runs the same command, Paseo recognizes it through `PASEO_AGENT_ID`. Without explicit placement, the new agent becomes its subagent in the same workspace. `--workspace` can place that subagent elsewhere without changing its parent.
+1 -1
View File
@@ -256,4 +256,4 @@ paseo run --workspace <workspace-id> "implement auth"
paseo workspace archive <workspace-id>
```
For the common case, `paseo run --isolation worktree --base main "implement auth"` creates both the workspace and its first agent.
For the common case, `paseo run --new-workspace worktree --worktree-mode branch-off --new-branch feature/auth --base main "implement auth"` creates both the workspace and its first agent.
+1
View File
@@ -104,6 +104,7 @@ paseo workspace create --isolation worktree --mode branch-off --new-branch fix-x
paseo workspace create --isolation worktree --mode checkout-branch --branch existing-work
paseo workspace create --isolation worktree --mode checkout-pr --pr-number 42
paseo run --provider codex/gpt-5.4 --mode full-access --workspace <workspace-id> "<prompt>"
paseo run --provider codex/gpt-5.4 --mode full-access --new-workspace worktree --worktree-mode branch-off --new-branch fix-x --base main "<prompt>"
paseo send <agent-id> "<follow-up>"
paseo ls
paseo schedule create --cron "*/15 * * * *" "ping main build"