mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 21:23:04 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7602b968 | ||
|
|
68993b7ab3 | ||
|
|
246c07fba5 | ||
|
|
0afcc96370 | ||
|
|
3a8d08ba91 | ||
|
|
f22a8ea8e0 |
+5
-5
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user