Stabilize SDK mode integration test

This commit is contained in:
Nathan Flurry 2026-03-10 22:37:27 -07:00
parent 24e99ac5e7
commit ec8b6afea9
274 changed files with 5412 additions and 7893 deletions

View file

@ -1,13 +1,5 @@
import * as childProcess from "node:child_process";
import {
closeSync,
existsSync,
mkdirSync,
openSync,
readFileSync,
rmSync,
writeFileSync
} from "node:fs";
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
@ -141,7 +133,7 @@ function removeStateFiles(host: string, port: number): void {
async function checkHealth(host: string, port: number): Promise<boolean> {
return await checkBackendHealth({
endpoint: `http://${host}:${port}/api/rivet`,
timeoutMs: HEALTH_TIMEOUT_MS
timeoutMs: HEALTH_TIMEOUT_MS,
});
}
@ -206,25 +198,14 @@ function resolveLaunchSpec(host: string, port: number): LaunchSpec {
return {
command: resolveBunCommand(),
args: [backendEntry, "start", "--host", host, "--port", String(port)],
cwd: repoRoot
cwd: repoRoot,
};
}
return {
command: "pnpm",
args: [
"--filter",
"@openhandoff/backend",
"exec",
"bun",
"src/index.ts",
"start",
"--host",
host,
"--port",
String(port)
],
cwd: repoRoot
args: ["--filter", "@openhandoff/backend", "exec", "bun", "src/index.ts", "start", "--host", host, "--port", String(port)],
cwd: repoRoot,
};
}
@ -252,7 +233,7 @@ async function startBackend(host: string, port: number): Promise<void> {
cwd: launch.cwd,
detached: true,
stdio: ["ignore", fd, fd],
env: process.env
env: process.env,
});
child.on("error", (error) => {
@ -298,7 +279,7 @@ function findProcessOnPort(port: number): number | null {
const out = childProcess
.execFileSync("lsof", ["-i", `:${port}`, "-t", "-sTCP:LISTEN"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
stdio: ["ignore", "pipe", "ignore"],
})
.trim();
@ -372,7 +353,7 @@ export async function getBackendStatus(host: string, port: number): Promise<Back
pid,
version: readBackendVersion(host, port),
versionCurrent: isVersionCurrent(host, port),
logPath
logPath,
};
}
removeStateFiles(host, port);
@ -384,7 +365,7 @@ export async function getBackendStatus(host: string, port: number): Promise<Back
pid: null,
version: readBackendVersion(host, port),
versionCurrent: isVersionCurrent(host, port),
logPath
logPath,
};
}
@ -393,7 +374,7 @@ export async function getBackendStatus(host: string, port: number): Promise<Back
pid: null,
version: readBackendVersion(host, port),
versionCurrent: false,
logPath
logPath,
};
}

View file

@ -1,7 +1,3 @@
declare const __HF_BUILD_ID__: string | undefined;
export const CLI_BUILD_ID =
typeof __HF_BUILD_ID__ === "string" && __HF_BUILD_ID__.trim().length > 0
? __HF_BUILD_ID__.trim()
: "dev";
export const CLI_BUILD_ID = typeof __HF_BUILD_ID__ === "string" && __HF_BUILD_ID__.trim().length > 0 ? __HF_BUILD_ID__.trim() : "dev";

View file

@ -3,19 +3,8 @@ import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { AgentTypeSchema, CreateHandoffInputSchema, type HandoffRecord } from "@openhandoff/shared";
import {
readBackendMetadata,
createBackendClientFromConfig,
formatRelativeAge,
groupHandoffStatus,
summarizeHandoffs
} from "@openhandoff/client";
import {
ensureBackendRunning,
getBackendStatus,
parseBackendPort,
stopBackend
} from "./backend/manager.js";
import { readBackendMetadata, createBackendClientFromConfig, formatRelativeAge, groupHandoffStatus, summarizeHandoffs } from "@openhandoff/client";
import { ensureBackendRunning, getBackendStatus, parseBackendPort, stopBackend } from "./backend/manager.js";
import { openEditorForTask } from "./task-editor.js";
import { spawnCreateTmuxWindow } from "./tmux.js";
import { loadConfig, resolveWorkspace, saveConfig } from "./workspace/config.js";
@ -26,11 +15,7 @@ async function ensureBunRuntime(): Promise<void> {
}
const preferred = process.env.HF_BUN?.trim();
const candidates = [
preferred,
`${homedir()}/.bun/bin/bun`,
"bun"
].filter((item): item is string => Boolean(item && item.length > 0));
const candidates = [preferred, `${homedir()}/.bun/bin/bun`, "bun"].filter((item): item is string => Boolean(item && item.length > 0));
for (const candidate of candidates) {
const command = candidate;
@ -41,7 +26,7 @@ async function ensureBunRuntime(): Promise<void> {
const child = spawnSync(command, [process.argv[1] ?? "", ...process.argv.slice(2)], {
stdio: "inherit",
env: process.env
env: process.env,
});
if (child.error) {
@ -70,11 +55,7 @@ function hasFlag(args: string[], flag: string): boolean {
return args.includes(flag);
}
function parseIntOption(
value: string | undefined,
fallback: number,
label: string
): number {
function parseIntOption(value: string | undefined, fallback: number, label: string): number {
if (!value) {
return fallback;
}
@ -204,8 +185,8 @@ async function handleBackend(args: string[]): Promise<void> {
backend: {
...config.backend,
host,
port
}
port,
},
};
if (sub === "start") {
@ -229,9 +210,7 @@ async function handleBackend(args: string[]): Promise<void> {
const pid = status.pid ?? "unknown";
const version = status.version ?? "unknown";
const stale = status.running && !status.versionCurrent ? " [outdated]" : "";
console.log(
`running=${status.running} pid=${pid} version=${version}${stale} host=${host} port=${port} log=${status.logPath}`
);
console.log(`running=${status.running} pid=${pid} version=${version}${stale} host=${host} port=${port} log=${status.logPath}`);
return;
}
@ -239,7 +218,7 @@ async function handleBackend(args: string[]): Promise<void> {
await ensureBackendRunning(backendConfig);
const metadata = await readBackendMetadata({
endpoint: `http://${host}:${port}/api/rivet`,
timeoutMs: 4_000
timeoutMs: 4_000,
});
const managerEndpoint = metadata.clientEndpoint ?? `http://${host}:${port}`;
const inspectorUrl = `https://inspect.rivet.dev?u=${encodeURIComponent(managerEndpoint)}`;
@ -424,7 +403,7 @@ async function waitForHandoffReady(
client: ReturnType<typeof createBackendClientFromConfig>,
workspaceId: string,
handoffId: string,
timeoutMs: number
timeoutMs: number,
): Promise<HandoffRecord> {
const start = Date.now();
let delayMs = 250;
@ -478,7 +457,7 @@ async function handleCreate(args: string[]): Promise<void> {
explicitTitle: explicitTitle || undefined,
explicitBranchName: explicitBranchName || undefined,
agentType,
onBranch
onBranch,
});
const created = await client.createHandoff(payload);
@ -496,7 +475,7 @@ async function handleCreate(args: string[]): Promise<void> {
const tmuxResult = spawnCreateTmuxWindow({
branchName: handoff.branchName ?? handoff.handoffId,
targetPath: switched.switchTarget || attached.target,
sessionId: attached.sessionId
sessionId: attached.sessionId,
});
if (tmuxResult.created) {
@ -507,7 +486,7 @@ async function handleCreate(args: string[]): Promise<void> {
console.log("");
console.log(`Run: hf switch ${handoff.handoffId}`);
if ((switched.switchTarget || attached.target).startsWith("/")) {
console.log(`cd ${(switched.switchTarget || attached.target)}`);
console.log(`cd ${switched.switchTarget || attached.target}`);
}
}
@ -539,23 +518,21 @@ async function handleStatus(args: string[]): Promise<void> {
handoffs: {
total: summary.total,
byStatus: summary.byStatus,
byProvider: summary.byProvider
}
byProvider: summary.byProvider,
},
},
null,
2
)
2,
),
);
return;
}
console.log(`workspace=${workspaceId}`);
console.log(
`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`
);
console.log(`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`);
console.log(`handoffs total=${summary.total}`);
console.log(
`status queued=${summary.byStatus.queued} running=${summary.byStatus.running} idle=${summary.byStatus.idle} archived=${summary.byStatus.archived} killed=${summary.byStatus.killed} error=${summary.byStatus.error}`
`status queued=${summary.byStatus.queued} running=${summary.byStatus.running} idle=${summary.byStatus.idle} archived=${summary.byStatus.archived} killed=${summary.byStatus.killed} error=${summary.byStatus.error}`,
);
const providerSummary = Object.entries(summary.byProvider)
.map(([provider, count]) => `${provider}=${count}`)
@ -579,7 +556,7 @@ async function handleHistory(args: string[]): Promise<void> {
workspaceId,
limit,
branch: branch || undefined,
handoffId: handoffId || undefined
handoffId: handoffId || undefined,
});
if (hasFlag(args, "--json")) {
@ -748,7 +725,7 @@ async function main(): Promise<void> {
}
main().catch((err: unknown) => {
const msg = err instanceof Error ? err.stack ?? err.message : String(err);
const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
console.error(msg);
process.exit(1);
});

View file

@ -3,11 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
const DEFAULT_EDITOR_TEMPLATE = [
"# Enter handoff task details below.",
"# Lines starting with # are ignored.",
""
].join("\n");
const DEFAULT_EDITOR_TEMPLATE = ["# Enter handoff task details below.", "# Lines starting with # are ignored.", ""].join("\n");
export function sanitizeEditorTask(input: string): string {
return input

View file

@ -85,7 +85,7 @@ const DEFAULT_THEME: TuiTheme = {
reviewApproved: "#22c55e",
reviewChanges: "#ef4444",
reviewPending: "#eab308",
reviewNone: "#6b7280"
reviewNone: "#6b7280",
};
const OPENCODE_THEME_PACK = opencodeThemePackJson as Record<string, unknown>;
@ -102,7 +102,7 @@ export function resolveTuiTheme(config: AppConfig, baseDir = cwd()): TuiThemeRes
theme: candidate.theme,
name: candidate.name,
source: "openhandoff config",
mode
mode,
};
}
}
@ -121,7 +121,7 @@ export function resolveTuiTheme(config: AppConfig, baseDir = cwd()): TuiThemeRes
theme: DEFAULT_THEME,
name: "opencode-default",
source: "default",
mode
mode,
};
}
@ -150,7 +150,7 @@ function loadOpencodeThemeFromConfig(mode: ThemeMode, baseDir: string): TuiTheme
theme: candidate.theme,
name: candidate.name,
source: `opencode config (${path})`,
mode
mode,
};
}
@ -182,20 +182,15 @@ function loadOpencodeThemeFromState(mode: ThemeMode, baseDir: string): TuiThemeR
theme: candidate.theme,
name: candidate.name,
source: `opencode state (${path})`,
mode
mode,
};
}
function loadFromSpec(
spec: string,
searchDirs: string[],
mode: ThemeMode,
baseDir: string
): ThemeCandidate | null {
function loadFromSpec(spec: string, searchDirs: string[], mode: ThemeMode, baseDir: string): ThemeCandidate | null {
if (isDefaultThemeName(spec)) {
return {
theme: DEFAULT_THEME,
name: "opencode-default"
name: "opencode-default",
};
}
@ -229,7 +224,7 @@ function loadFromSpec(
if (theme) {
return {
theme,
name: spec
name: spec,
};
}
}
@ -253,7 +248,7 @@ function loadThemeFromPath(path: string, mode: ThemeMode): ThemeCandidate | null
}
return {
theme,
name: themeNameFromPath(path)
name: themeNameFromPath(path),
};
} catch {
return null;
@ -269,7 +264,7 @@ function loadThemeFromPath(path: string, mode: ThemeMode): ThemeCandidate | null
if (opencodeTheme) {
return {
theme: opencodeTheme,
name: themeNameFromPath(path)
name: themeNameFromPath(path),
};
}
@ -280,7 +275,7 @@ function loadThemeFromPath(path: string, mode: ThemeMode): ThemeCandidate | null
return {
theme: paletteTheme,
name: themeNameFromPath(path)
name: themeNameFromPath(path),
};
}
@ -292,12 +287,7 @@ function themeNameFromPath(path: string): string {
return base;
}
function themeFromOpencodeValue(
value: unknown,
searchDirs: string[],
mode: ThemeMode,
baseDir: string
): ThemeCandidate | null {
function themeFromOpencodeValue(value: unknown, searchDirs: string[], mode: ThemeMode, baseDir: string): ThemeCandidate | null {
if (typeof value === "string") {
return loadFromSpec(value, searchDirs, mode, baseDir);
}
@ -311,7 +301,7 @@ function themeFromOpencodeValue(
if (theme) {
return {
theme,
name: typeof value.name === "string" ? value.name : "inline"
name: typeof value.name === "string" ? value.name : "inline",
};
}
}
@ -320,7 +310,7 @@ function themeFromOpencodeValue(
if (paletteTheme) {
return {
theme: paletteTheme,
name: typeof value.name === "string" ? value.name : "inline"
name: typeof value.name === "string" ? value.name : "inline",
};
}
@ -382,10 +372,7 @@ function themeFromOpencodeJson(value: unknown, mode: ThemeMode): TuiTheme | null
const info = opencodeColor(themeMap, defs, mode, "info") ?? DEFAULT_THEME.info;
const diffAdd = opencodeColor(themeMap, defs, mode, "diffAdded") ?? success;
const diffDel = opencodeColor(themeMap, defs, mode, "diffRemoved") ?? error;
const diffSep =
opencodeColor(themeMap, defs, mode, "diffContext") ??
opencodeColor(themeMap, defs, mode, "diffHunkHeader") ??
muted;
const diffSep = opencodeColor(themeMap, defs, mode, "diffContext") ?? opencodeColor(themeMap, defs, mode, "diffHunkHeader") ?? muted;
return {
background,
@ -416,7 +403,7 @@ function themeFromOpencodeJson(value: unknown, mode: ThemeMode): TuiTheme | null
reviewApproved: success,
reviewChanges: error,
reviewPending: warning,
reviewNone: muted
reviewNone: muted,
};
}
@ -428,13 +415,7 @@ function opencodeColor(themeMap: JsonObject, defs: JsonObject, mode: ThemeMode,
return resolveOpencodeColor(raw, themeMap, defs, mode, 0);
}
function resolveOpencodeColor(
value: unknown,
themeMap: JsonObject,
defs: JsonObject,
mode: ThemeMode,
depth: number
): string | null {
function resolveOpencodeColor(value: unknown, themeMap: JsonObject, defs: JsonObject, mode: ThemeMode, depth: number): string | null {
if (depth > 12) {
return null;
}
@ -533,7 +514,7 @@ function themeFromAny(value: unknown): TuiTheme | null {
reviewApproved: pick(["review_approved", "approved"], success),
reviewChanges: pick(["review_changes", "changes"], error),
reviewPending: pick(["review_pending", "pending"], warning),
reviewNone: pick(["review_none", "review_unknown"], muted)
reviewNone: pick(["review_none", "review_unknown"], muted),
};
}

View file

@ -20,12 +20,7 @@ export interface SpawnCreateTmuxWindowInput {
export interface SpawnCreateTmuxWindowResult {
created: boolean;
reason:
| "created"
| "not-in-tmux"
| "not-local-path"
| "window-exists"
| "tmux-new-window-failed";
reason: "created" | "not-in-tmux" | "not-local-path" | "window-exists" | "tmux-new-window-failed";
}
function isTmuxSession(): boolean {
@ -63,10 +58,7 @@ function resolveOpencodeBinary(): string {
return "opencode";
}
const bundledCandidates = [
`${homedir()}/.local/share/sandbox-agent/bin/opencode`,
`${homedir()}/.opencode/bin/opencode`
];
const bundledCandidates = [`${homedir()}/.local/share/sandbox-agent/bin/opencode`, `${homedir()}/.opencode/bin/opencode`];
for (const candidate of bundledCandidates) {
if (existsSync(candidate)) {
@ -79,15 +71,7 @@ function resolveOpencodeBinary(): string {
function attachCommand(sessionId: string, targetPath: string, endpoint: string): string {
const opencode = resolveOpencodeBinary();
return [
shellEscape(opencode),
"attach",
shellEscape(endpoint),
"--session",
shellEscape(sessionId),
"--dir",
shellEscape(targetPath)
].join(" ");
return [shellEscape(opencode), "attach", shellEscape(endpoint), "--session", shellEscape(sessionId), "--dir", shellEscape(targetPath)].join(" ");
}
export function stripStatusPrefix(windowName: string): string {
@ -99,11 +83,7 @@ export function stripStatusPrefix(windowName: string): string {
}
export function findTmuxWindowsByBranch(branchName: string): TmuxWindowMatch[] {
const output = spawnSync(
"tmux",
["list-windows", "-a", "-F", "#{session_name}:#{window_id}:#{window_name}"],
{ encoding: "utf8" }
);
const output = spawnSync("tmux", ["list-windows", "-a", "-F", "#{session_name}:#{window_id}:#{window_name}"], { encoding: "utf8" });
if (output.error || output.status !== 0 || !output.stdout) {
return [];
@ -128,16 +108,14 @@ export function findTmuxWindowsByBranch(branchName: string): TmuxWindowMatch[] {
matches.push({
target: `${sessionName}:${windowId}`,
windowName
windowName,
});
}
return matches;
}
export function spawnCreateTmuxWindow(
input: SpawnCreateTmuxWindowInput
): SpawnCreateTmuxWindowResult {
export function spawnCreateTmuxWindow(input: SpawnCreateTmuxWindowInput): SpawnCreateTmuxWindowResult {
if (!isTmuxSession()) {
return { created: false, reason: "not-in-tmux" };
}
@ -154,21 +132,10 @@ export function spawnCreateTmuxWindow(
const endpoint = input.opencodeEndpoint ?? DEFAULT_OPENCODE_ENDPOINT;
let output = "";
try {
output = execFileSync(
"tmux",
[
"new-window",
"-d",
"-P",
"-F",
"#{window_id}",
"-n",
windowName,
"-c",
input.targetPath
],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
);
output = execFileSync("tmux", ["new-window", "-d", "-P", "-F", "#{window_id}", "-n", windowName, "-c", input.targetPath], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
} catch {
return { created: false, reason: "tmux-new-window-failed" };
}
@ -184,11 +151,10 @@ export function spawnCreateTmuxWindow(
// Split left pane horizontally → creates right pane; capture its pane ID
let rightPane: string;
try {
rightPane = execFileSync(
"tmux",
["split-window", "-h", "-P", "-F", "#{pane_id}", "-t", leftPane, "-c", input.targetPath],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
).trim();
rightPane = execFileSync("tmux", ["split-window", "-h", "-P", "-F", "#{pane_id}", "-t", leftPane, "-c", input.targetPath], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
} catch {
return { created: true, reason: "created" };
}
@ -206,13 +172,7 @@ export function spawnCreateTmuxWindow(
// Editor in left pane, agent attach in top-right pane
runTmux(["send-keys", "-t", leftPane, "nvim .", "Enter"]);
runTmux([
"send-keys",
"-t",
rightPane,
attachCommand(input.sessionId, input.targetPath, endpoint),
"Enter"
]);
runTmux(["send-keys", "-t", rightPane, attachCommand(input.sessionId, input.targetPath, endpoint), "Enter"]);
runTmux(["select-pane", "-t", rightPane]);
}

View file

@ -1,11 +1,6 @@
import type { AppConfig, HandoffRecord } from "@openhandoff/shared";
import { spawnSync } from "node:child_process";
import {
createBackendClientFromConfig,
filterHandoffs,
formatRelativeAge,
groupHandoffStatus
} from "@openhandoff/client";
import { createBackendClientFromConfig, filterHandoffs, formatRelativeAge, groupHandoffStatus } from "@openhandoff/client";
import { CLI_BUILD_ID } from "./build-id.js";
import { resolveTuiTheme, type TuiTheme } from "./theme.js";
@ -31,7 +26,7 @@ const HELP_LINES = [
"Esc / Ctrl-C cancel",
"",
"Legend",
"Agent: \u{1F916} running \u{1F4AC} idle \u25CC queued"
"Agent: \u{1F916} running \u{1F4AC} idle \u25CC queued",
];
const COLUMN_WIDTHS = {
@ -41,7 +36,7 @@ const COLUMN_WIDTHS = {
author: 10,
ci: 7,
review: 8,
age: 5
age: 5,
} as const;
interface DisplayRow {
@ -145,15 +140,17 @@ function agentSymbol(status: HandoffRecord["status"]): string {
function toDisplayRow(row: HandoffRecord): DisplayRow {
const conflictPrefix = row.conflictsWithMain === "true" ? "\u26A0 " : "";
const prLabel = row.prUrl
? `#${row.prUrl.match(/\/pull\/(\d+)/)?.[1] ?? "?"}`
: row.prSubmitted ? "sub" : "-";
const prLabel = row.prUrl ? `#${row.prUrl.match(/\/pull\/(\d+)/)?.[1] ?? "?"}` : row.prSubmitted ? "sub" : "-";
const ciLabel = row.ciStatus ?? "-";
const reviewLabel = row.reviewStatus
? row.reviewStatus === "approved" ? "ok"
: row.reviewStatus === "changes_requested" ? "chg"
: row.reviewStatus === "pending" ? "..." : row.reviewStatus
? row.reviewStatus === "approved"
? "ok"
: row.reviewStatus === "changes_requested"
? "chg"
: row.reviewStatus === "pending"
? "..."
: row.reviewStatus
: "-";
return {
@ -164,7 +161,7 @@ function toDisplayRow(row: HandoffRecord): DisplayRow {
author: row.prAuthor ?? "-",
ci: ciLabel,
review: reviewLabel,
age: formatRelativeAge(row.updatedAt)
age: formatRelativeAge(row.updatedAt),
};
}
@ -189,18 +186,12 @@ export function formatRows(
status: string,
searchQuery = "",
showHelp = false,
options: RenderOptions = {}
options: RenderOptions = {},
): string {
const totalWidth = options.width ?? process.stdout.columns ?? 120;
const totalHeight = Math.max(6, options.height ?? process.stdout.rows ?? 24);
const fixedWidth =
COLUMN_WIDTHS.diff +
COLUMN_WIDTHS.agent +
COLUMN_WIDTHS.pr +
COLUMN_WIDTHS.author +
COLUMN_WIDTHS.ci +
COLUMN_WIDTHS.review +
COLUMN_WIDTHS.age;
COLUMN_WIDTHS.diff + COLUMN_WIDTHS.agent + COLUMN_WIDTHS.pr + COLUMN_WIDTHS.author + COLUMN_WIDTHS.ci + COLUMN_WIDTHS.review + COLUMN_WIDTHS.age;
const separators = 7;
const prefixWidth = 2;
const branchWidth = Math.max(20, totalWidth - (fixedWidth + separators + prefixWidth));
@ -208,7 +199,7 @@ export function formatRows(
const branchHeader = searchQuery ? `Branch/PR: ${searchQuery}_` : "Branch/PR (type to filter)";
const header = [
` ${pad(branchHeader, branchWidth)} ${pad("Diff", COLUMN_WIDTHS.diff)} ${pad("Agent", COLUMN_WIDTHS.agent)} ${pad("PR", COLUMN_WIDTHS.pr)} ${pad("Author", COLUMN_WIDTHS.author)} ${pad("CI", COLUMN_WIDTHS.ci)} ${pad("Review", COLUMN_WIDTHS.review)} ${pad("Age", COLUMN_WIDTHS.age)}`,
"-".repeat(Math.max(24, Math.min(totalWidth, 180)))
"-".repeat(Math.max(24, Math.min(totalWidth, 180))),
];
const body =
@ -220,14 +211,7 @@ export function formatRows(
return `${marker}${pad(display.name, branchWidth)} ${pad(display.diff, COLUMN_WIDTHS.diff)} ${pad(display.agent, COLUMN_WIDTHS.agent)} ${pad(display.pr, COLUMN_WIDTHS.pr)} ${pad(display.author, COLUMN_WIDTHS.author)} ${pad(display.ci, COLUMN_WIDTHS.ci)} ${pad(display.review, COLUMN_WIDTHS.review)} ${pad(display.age, COLUMN_WIDTHS.age)}`;
});
const footer = fitLine(
buildFooterLine(
totalWidth,
["Ctrl-H:cheatsheet", `workspace:${workspaceId}`, status],
`v${CLI_BUILD_ID}`
),
totalWidth,
);
const footer = fitLine(buildFooterLine(totalWidth, ["Ctrl-H:cheatsheet", `workspace:${workspaceId}`, status], `v${CLI_BUILD_ID}`), totalWidth);
const contentHeight = totalHeight - 1;
const lines = [...header, ...body].map((line) => fitLine(line, totalWidth));
@ -256,7 +240,10 @@ export function formatRows(
interface OpenTuiLike {
createCliRenderer?: (options?: Record<string, unknown>) => Promise<any>;
TextRenderable?: new (ctx: any, options: { id: string; content: string }) => {
TextRenderable?: new (
ctx: any,
options: { id: string; content: string },
) => {
content: unknown;
fg?: string;
bg?: string;
@ -325,10 +312,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
const core = (await import("@opentui/core")) as OpenTuiLike;
const createCliRenderer = core.createCliRenderer;
const TextRenderable = core.TextRenderable;
const styleApi =
core.fg && core.bg && core.StyledText
? { fg: core.fg, bg: core.bg, StyledText: core.StyledText }
: null;
const styleApi = core.fg && core.bg && core.StyledText ? { fg: core.fg, bg: core.bg, StyledText: core.StyledText } : null;
if (!createCliRenderer || !TextRenderable) {
throw new Error("OpenTUI runtime missing createCliRenderer/TextRenderable exports");
@ -339,7 +323,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
const renderer = await createCliRenderer({ exitOnCtrlC: false });
const text = new TextRenderable(renderer, {
id: "openhandoff-switch",
content: "Loading..."
content: "Loading...",
});
text.fg = themeResolution.theme.text;
text.bg = themeResolution.theme.background;
@ -376,11 +360,9 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
}
const output = formatRows(filteredRows, selected, workspaceId, status, searchQuery, showHelp, {
width: renderer.width ?? process.stdout.columns,
height: renderer.height ?? process.stdout.rows
height: renderer.height ?? process.stdout.rows,
});
text.content = styleApi
? buildStyledContent(output, themeResolution.theme, styleApi)
: output;
text.content = styleApi ? buildStyledContent(output, themeResolution.theme, styleApi) : output;
renderer.requestRender();
};
@ -439,11 +421,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
close();
};
const runActionWithRefresh = async (
label: string,
fn: () => Promise<void>,
success: string
): Promise<void> => {
const runActionWithRefresh = async (label: string, fn: () => Promise<void>, success: string): Promise<void> => {
if (busy) {
return;
}
@ -469,9 +447,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
process.once("SIGINT", handleSignal);
process.once("SIGTERM", handleSignal);
const keyInput = (renderer.keyInput ?? renderer.keyHandler) as
| { on: (name: string, cb: (event: KeyEventLike) => void) => void }
| undefined;
const keyInput = (renderer.keyInput ?? renderer.keyHandler) as { on: (name: string, cb: (event: KeyEventLike) => void) => void } | undefined;
if (!keyInput) {
clearInterval(timer);
@ -577,11 +553,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
if (!row) {
return;
}
void runActionWithRefresh(
`archiving ${row.handoffId}`,
async () => client.runAction(workspaceId, row.handoffId, "archive"),
`archived ${row.handoffId}`
);
void runActionWithRefresh(`archiving ${row.handoffId}`, async () => client.runAction(workspaceId, row.handoffId, "archive"), `archived ${row.handoffId}`);
return;
}
@ -590,11 +562,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
if (!row) {
return;
}
void runActionWithRefresh(
`syncing ${row.handoffId}`,
async () => client.runAction(workspaceId, row.handoffId, "sync"),
`synced ${row.handoffId}`
);
void runActionWithRefresh(`syncing ${row.handoffId}`, async () => client.runAction(workspaceId, row.handoffId, "sync"), `synced ${row.handoffId}`);
return;
}
@ -609,7 +577,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
await client.runAction(workspaceId, row.handoffId, "merge");
await client.runAction(workspaceId, row.handoffId, "archive");
},
`merged+archived ${row.handoffId}`
`merged+archived ${row.handoffId}`,
);
return;
}

View file

@ -7,7 +7,7 @@ import type { ChildProcess } from "node:child_process";
const { spawnMock, execFileSyncMock } = vi.hoisted(() => ({
spawnMock: vi.fn(),
execFileSyncMock: vi.fn()
execFileSyncMock: vi.fn(),
}));
vi.mock("node:child_process", async () => {
@ -15,7 +15,7 @@ vi.mock("node:child_process", async () => {
return {
...actual,
spawn: spawnMock,
execFileSync: execFileSyncMock
execFileSync: execFileSyncMock,
};
});
@ -37,16 +37,16 @@ function healthyMetadataResponse(): { ok: boolean; json: () => Promise<unknown>
json: async () => ({
runtime: "rivetkit",
actorNames: {
workspace: {}
}
})
workspace: {},
},
}),
};
}
function unhealthyMetadataResponse(): { ok: boolean; json: () => Promise<unknown> } {
return {
ok: false,
json: async () => ({})
json: async () => ({}),
};
}
@ -66,11 +66,11 @@ describe("backend manager", () => {
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7
backup_retention_days: 7,
},
providers: {
daytona: { image: "ubuntu:24.04" }
}
daytona: { image: "ubuntu:24.04" },
},
});
beforeEach(() => {
@ -116,7 +116,7 @@ describe("backend manager", () => {
const fakeChild = Object.assign(new EventEmitter(), {
pid: process.pid,
unref: vi.fn()
unref: vi.fn(),
}) as unknown as ChildProcess;
spawnMock.mockReturnValue(fakeChild);
@ -125,14 +125,8 @@ describe("backend manager", () => {
expect(spawnMock).toHaveBeenCalledTimes(1);
const launchCommand = spawnMock.mock.calls[0]?.[0];
const launchArgs = spawnMock.mock.calls[0]?.[1] as string[] | undefined;
expect(
launchCommand === "pnpm" ||
launchCommand === "bun" ||
(typeof launchCommand === "string" && launchCommand.endsWith("/bun"))
).toBe(true);
expect(launchArgs).toEqual(
expect.arrayContaining(["start", "--host", config.backend.host, "--port", String(config.backend.port)])
);
expect(launchCommand === "pnpm" || launchCommand === "bun" || (typeof launchCommand === "string" && launchCommand.endsWith("/bun"))).toBe(true);
expect(launchArgs).toEqual(expect.arrayContaining(["start", "--host", config.backend.host, "--port", String(config.backend.port)]));
if (launchCommand === "pnpm") {
expect(launchArgs).toEqual(expect.arrayContaining(["exec", "bun", "src/index.ts"]));
}
@ -148,9 +142,7 @@ describe("backend manager", () => {
mkdirSync(stateDir, { recursive: true });
writeFileSync(versionPath, "test-build", "utf8");
const fetchMock = vi
.fn<() => Promise<{ ok: boolean; json: () => Promise<unknown> }>>()
.mockResolvedValue(healthyMetadataResponse());
const fetchMock = vi.fn<() => Promise<{ ok: boolean; json: () => Promise<unknown> }>>().mockResolvedValue(healthyMetadataResponse());
globalThis.fetch = fetchMock as unknown as typeof fetch;
await ensureBackendRunning(config);

View file

@ -23,4 +23,3 @@ with more detail
expect(value).toBe("");
});
});

View file

@ -29,11 +29,11 @@ describe("resolveTuiTheme", () => {
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7
backup_retention_days: 7,
},
providers: {
daytona: { image: "ubuntu:24.04" }
}
daytona: { image: "ubuntu:24.04" },
},
});
afterEach(() => {
@ -64,11 +64,7 @@ describe("resolveTuiTheme", () => {
withEnv("XDG_STATE_HOME", stateHome);
withEnv("XDG_CONFIG_HOME", configHome);
mkdirSync(join(stateHome, "opencode"), { recursive: true });
writeFileSync(
join(stateHome, "opencode", "kv.json"),
JSON.stringify({ theme: "gruvbox", theme_mode: "dark" }),
"utf8"
);
writeFileSync(join(stateHome, "opencode", "kv.json"), JSON.stringify({ theme: "gruvbox", theme_mode: "dark" }), "utf8");
const resolution = resolveTuiTheme(baseConfig, tempDir);
@ -85,11 +81,7 @@ describe("resolveTuiTheme", () => {
withEnv("XDG_STATE_HOME", stateHome);
withEnv("XDG_CONFIG_HOME", configHome);
mkdirSync(join(stateHome, "opencode"), { recursive: true });
writeFileSync(
join(stateHome, "opencode", "kv.json"),
JSON.stringify({ theme: "orng", theme_mode: "dark" }),
"utf8"
);
writeFileSync(join(stateHome, "opencode", "kv.json"), JSON.stringify({ theme: "orng", theme_mode: "dark" }), "utf8");
const resolution = resolveTuiTheme(baseConfig, tempDir);

View file

@ -23,8 +23,8 @@ const sample: HandoffRecord = {
switchTarget: "daytona://sandbox-1",
cwd: null,
createdAt: 1,
updatedAt: 1
}
updatedAt: 1,
},
],
agentType: null,
prSubmitted: false,
@ -38,7 +38,7 @@ const sample: HandoffRecord = {
hasUnpushed: null,
parentBranch: null,
createdAt: 1,
updatedAt: 1
updatedAt: 1,
};
describe("formatRows", () => {
@ -60,7 +60,7 @@ describe("formatRows", () => {
it("pins footer to the last terminal row", () => {
const output = formatRows([sample], 0, "default", "ready", "", false, {
width: 80,
height: 12
height: 12,
});
const lines = output.split("\n");
expect(lines).toHaveLength(12);
@ -83,8 +83,8 @@ describe("search", () => {
handoffId: "handoff-2",
branchName: "docs/update-intro",
title: "Docs Intro Refresh",
status: "idle"
}
status: "idle",
},
];
expect(filterHandoffs(rows, "doc")).toHaveLength(1);
expect(filterHandoffs(rows, "h2")).toHaveLength(1);

View file

@ -15,11 +15,11 @@ describe("cli workspace resolution", () => {
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7
backup_retention_days: 7,
},
providers: {
daytona: { image: "ubuntu:24.04" }
}
daytona: { image: "ubuntu:24.04" },
},
});
expect(resolveWorkspace(undefined, config)).toBe("team");

View file

@ -20,7 +20,7 @@ function sourceId(): string {
try {
const raw = execSync("git rev-parse --short HEAD", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (raw.length > 0) {
return raw;
@ -48,7 +48,6 @@ export default defineConfig({
format: ["esm"],
dts: true,
define: {
__HF_BUILD_ID__: JSON.stringify(buildId)
}
__HF_BUILD_ID__: JSON.stringify(buildId),
},
});