Files
paseo/packages/cli/src/commands/worktree/ls.ts
T
Mohamed BoudraandGitHub bb889dae99 feat: stream files as binary frames over WebSocket (#659)
Replace base64-in-JSON file transfers with binary WebSocket frames to
unblock the JS thread when assistant markdown messages contain images.
Receive cost (JSON.parse over multi-MB base64) and persist cost (base64
re-encode to disk) both removed.

- Split daemon-client `exploreFileSystem` into `listDirectory` + `readFile`;
  `readFile` returns `{ bytes, mime, size, modifiedAt }`. Encoding stays
  private to the seam.
- New `packages/server/src/shared/binary-frames/` module hosts terminal
  frames (moved from `terminal-stream-protocol.ts`) and new file-transfer
  opcodes (`FileBegin` / `FileChunk` / `FileEnd`) with request-id correlation.
- Add optional `acceptBinary` to `FileExplorerRequestSchema`. New daemons
  emit binary frames when set; legacy JSON path remains for old clients.
  Compat fallback lives only inside `readFile`.
- New `persistAttachmentFromBytes` writes bytes directly: native via
  `expo-file-system` `File.write(Uint8Array)`, web via Blob -> IndexedDB,
  desktop via new `write_attachment_bytes` IPC. `persistAttachmentFromBase64`
  removed.
2026-05-02 14:02:27 +08:00

132 lines
3.8 KiB
TypeScript

import type { Command } from "commander";
import { homedir } from "node:os";
import { basename, join, sep } from "node:path";
import type { DaemonClient } from "@getpaseo/server";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type { CommandOptions, ListResult, OutputSchema, CommandError } from "../../output/index.js";
/** Worktree list item for display */
export interface WorktreeListItem {
name: string;
branch: string;
cwd: string;
agent: string;
}
/** Shorten home directory in path */
function shortenPath(path: string): string {
const home = process.env.HOME;
if (home && path.startsWith(home)) {
return "~" + path.slice(home.length);
}
return path;
}
/** Extract worktree name from path */
function extractWorktreeName(path: string): string {
return basename(path);
}
export function resolvePaseoHomePath(): string {
return process.env.PASEO_HOME ?? join(homedir(), ".paseo");
}
export function resolvePaseoWorktreesDir(): string {
return join(resolvePaseoHomePath(), "worktrees");
}
function isAgentInManagedWorktree(agentCwd: string): boolean {
const worktreesDir = resolvePaseoWorktreesDir();
return agentCwd === worktreesDir || agentCwd.startsWith(worktreesDir + sep);
}
/** Schema for worktree ls output */
export const worktreeLsSchema: OutputSchema<WorktreeListItem> = {
idField: "name",
columns: [
{ header: "NAME", field: "name", width: 20 },
{ header: "BRANCH", field: "branch", width: 25 },
{ header: "CWD", field: "cwd", width: 45 },
{ header: "AGENT", field: "agent", width: 10 },
],
};
export type WorktreeLsResult = ListResult<WorktreeListItem>;
export interface WorktreeLsOptions extends CommandOptions {
host?: string;
}
export async function runLsCommand(
options: WorktreeLsOptions,
_command: Command,
): Promise<WorktreeLsResult> {
const host = getDaemonHost({ host: options.host });
let client: DaemonClient;
try {
client = await connectToDaemon({ host: options.host });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
// Get worktree list from daemon
const response = await client.getPaseoWorktreeList({});
await client.close();
if (response.error) {
const error: CommandError = {
code: "WORKTREE_LIST_FAILED",
message: `Failed to list worktrees: ${response.error.message}`,
};
throw error;
}
// Build a map of worktree paths to agent IDs
const worktreeAgentMap = new Map<string, string>();
for (const agent of agents) {
if (isAgentInManagedWorktree(agent.cwd)) {
worktreeAgentMap.set(agent.cwd, agent.id.slice(0, 7));
}
}
const items: WorktreeListItem[] = response.worktrees.map((wt) => ({
name: extractWorktreeName(wt.worktreePath),
branch: wt.branchName ?? "-",
cwd: shortenPath(wt.worktreePath),
agent: worktreeAgentMap.get(wt.worktreePath) ?? "-",
}));
return {
type: "list",
data: items,
schema: worktreeLsSchema,
};
} catch (err) {
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "WORKTREE_LIST_FAILED",
message: `Failed to list worktrees: ${message}`,
};
throw error;
}
}