repo: push all current workspace changes

This commit is contained in:
Nathan Flurry 2026-03-13 01:12:43 -07:00
parent 252fbdc93b
commit e7dfff5836
29 changed files with 577 additions and 98 deletions

View file

@ -1,5 +1,6 @@
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { createErrorContext, createFoundryLogger } from "@sandbox-agent/foundry-shared";
type Journal = {
entries?: Array<{
@ -11,6 +12,10 @@ type Journal = {
}>;
};
const logger = createFoundryLogger({
service: "foundry-backend-migrations",
});
function padMigrationKey(idx: number): string {
return `m${String(idx).padStart(4, "0")}`;
}
@ -128,8 +133,6 @@ async function main(): Promise<void> {
}
main().catch((error: unknown) => {
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
// eslint-disable-next-line no-console
console.error(message);
logger.error(createErrorContext(error), "generate_actor_migrations_failed");
process.exitCode = 1;
});

View file

@ -1,11 +1,5 @@
import { pino } from "pino";
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
const level = process.env.FOUNDRY_LOG_LEVEL ?? process.env.LOG_LEVEL ?? process.env.RIVET_LOG_LEVEL ?? "info";
export const logger = pino({
level,
base: {
service: "foundry-backend",
},
timestamp: pino.stdTimeFunctions.isoTime,
export const logger = createFoundryLogger({
service: "foundry-backend",
});

View file

@ -1,4 +1,5 @@
import { createHmac, createPrivateKey, createSign, timingSafeEqual } from "node:crypto";
import { logger } from "../logging.js";
export class GitHubAppError extends Error {
readonly status: number;
@ -51,6 +52,10 @@ interface GitHubPageResponse<T> {
nextUrl: string | null;
}
const githubOAuthLogger = logger.child({
scope: "github-oauth",
});
export interface GitHubWebhookEvent {
action?: string;
installation?: { id: number; account?: { login?: string; type?: string; id?: number } | null };
@ -167,13 +172,16 @@ export class GitHubAppClient {
code,
redirect_uri: this.redirectUri,
};
console.log("[github-oauth] exchangeCode request", {
url: `${this.authBaseUrl}/login/oauth/access_token`,
client_id: this.clientId,
redirect_uri: this.redirectUri,
code_length: code.length,
code_prefix: code.slice(0, 6),
});
githubOAuthLogger.debug(
{
url: `${this.authBaseUrl}/login/oauth/access_token`,
clientId: this.clientId,
redirectUri: this.redirectUri,
codeLength: code.length,
codePrefix: code.slice(0, 6),
},
"exchange_code_request",
);
const response = await fetch(`${this.authBaseUrl}/login/oauth/access_token`, {
method: "POST",
@ -185,10 +193,13 @@ export class GitHubAppClient {
});
const responseText = await response.text();
console.log("[github-oauth] exchangeCode response", {
status: response.status,
body: responseText.slice(0, 300),
});
githubOAuthLogger.debug(
{
status: response.status,
bodyPreview: responseText.slice(0, 300),
},
"exchange_code_response",
);
let payload: GitHubTokenResponse;
try {
payload = JSON.parse(responseText) as GitHubTokenResponse;

View file

@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
import { checkBackendHealth } from "@sandbox-agent/foundry-client";
import type { AppConfig } from "@sandbox-agent/foundry-shared";
import { CLI_BUILD_ID } from "../build-id.js";
import { logger } from "../logging.js";
const HEALTH_TIMEOUT_MS = 1_500;
const START_TIMEOUT_MS = 30_000;
@ -237,7 +238,17 @@ async function startBackend(host: string, port: number): Promise<void> {
});
child.on("error", (error) => {
console.error(`failed to launch backend: ${String(error)}`);
logger.error(
{
host,
port,
command: launch.command,
args: launch.args,
errorMessage: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined,
},
"failed_to_launch_backend",
);
});
child.unref();

View file

@ -5,6 +5,7 @@ import { homedir } from "node:os";
import { AgentTypeSchema, CreateTaskInputSchema, type TaskRecord } from "@sandbox-agent/foundry-shared";
import { readBackendMetadata, createBackendClientFromConfig, formatRelativeAge, groupTaskStatus, summarizeTasks } from "@sandbox-agent/foundry-client";
import { ensureBackendRunning, getBackendStatus, parseBackendPort, stopBackend } from "./backend/manager.js";
import { writeStderr, writeStdout } from "./io.js";
import { openEditorForTask } from "./task-editor.js";
import { spawnCreateTmuxWindow } from "./tmux.js";
import { loadConfig, resolveWorkspace, saveConfig } from "./workspace/config.js";
@ -87,7 +88,7 @@ function positionals(args: string[]): string[] {
}
function printUsage(): void {
console.log(`
writeStdout(`
Usage:
hf backend start [--host HOST] [--port PORT]
hf backend stop [--host HOST] [--port PORT]
@ -120,7 +121,7 @@ Tips:
}
function printStatusUsage(): void {
console.log(`
writeStdout(`
Usage:
hf status [--workspace WS] [--json]
@ -146,7 +147,7 @@ JSON Output:
}
function printHistoryUsage(): void {
console.log(`
writeStdout(`
Usage:
hf history [--workspace WS] [--limit N] [--branch NAME] [--task ID] [--json]
@ -195,13 +196,13 @@ 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=true pid=${pid} version=${version}${stale} log=${status.logPath}`);
writeStdout(`running=true pid=${pid} version=${version}${stale} log=${status.logPath}`);
return;
}
if (sub === "stop") {
await stopBackend(host, port);
console.log(`running=false host=${host} port=${port}`);
writeStdout(`running=false host=${host} port=${port}`);
return;
}
@ -210,7 +211,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}`);
writeStdout(`running=${status.running} pid=${pid} version=${version}${stale} host=${host} port=${port} log=${status.logPath}`);
return;
}
@ -224,7 +225,7 @@ async function handleBackend(args: string[]): Promise<void> {
const inspectorUrl = `https://inspect.rivet.dev?u=${encodeURIComponent(managerEndpoint)}`;
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
spawnSync(openCmd, [inspectorUrl], { stdio: "ignore" });
console.log(inspectorUrl);
writeStdout(inspectorUrl);
return;
}
@ -253,7 +254,7 @@ async function handleWorkspace(args: string[]): Promise<void> {
// Backend may not be running yet. Config is already updated.
}
console.log(`workspace=${name}`);
writeStdout(`workspace=${name}`);
}
async function handleList(args: string[]): Promise<void> {
@ -265,12 +266,12 @@ async function handleList(args: string[]): Promise<void> {
const rows = await client.listTasks(workspaceId);
if (format === "json") {
console.log(JSON.stringify(rows, null, 2));
writeStdout(JSON.stringify(rows, null, 2));
return;
}
if (rows.length === 0) {
console.log("no tasks");
writeStdout("no tasks");
return;
}
@ -281,7 +282,7 @@ async function handleList(args: string[]): Promise<void> {
const task = row.task.length > 60 ? `${row.task.slice(0, 57)}...` : row.task;
line += `\t${row.title}\t${task}\t${row.activeSessionId ?? "-"}\t${row.activeSandboxId ?? "-"}`;
}
console.log(line);
writeStdout(line);
}
}
@ -294,7 +295,7 @@ async function handlePush(args: string[]): Promise<void> {
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
const client = createBackendClientFromConfig(config);
await client.runAction(workspaceId, taskId, "push");
console.log("ok");
writeStdout("ok");
}
async function handleSync(args: string[]): Promise<void> {
@ -306,7 +307,7 @@ async function handleSync(args: string[]): Promise<void> {
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
const client = createBackendClientFromConfig(config);
await client.runAction(workspaceId, taskId, "sync");
console.log("ok");
writeStdout("ok");
}
async function handleKill(args: string[]): Promise<void> {
@ -320,15 +321,15 @@ async function handleKill(args: string[]): Promise<void> {
const abandon = hasFlag(args, "--abandon");
if (deleteBranch) {
console.log("info: --delete-branch flag set, branch will be deleted after kill");
writeStdout("info: --delete-branch flag set, branch will be deleted after kill");
}
if (abandon) {
console.log("info: --abandon flag set, Graphite abandon will be attempted");
writeStdout("info: --abandon flag set, Graphite abandon will be attempted");
}
const client = createBackendClientFromConfig(config);
await client.runAction(workspaceId, taskId, "kill");
console.log("ok");
writeStdout("ok");
}
async function handlePrune(args: string[]): Promise<void> {
@ -341,26 +342,26 @@ async function handlePrune(args: string[]): Promise<void> {
const prunable = rows.filter((r) => r.status === "archived" || r.status === "killed");
if (prunable.length === 0) {
console.log("nothing to prune");
writeStdout("nothing to prune");
return;
}
for (const row of prunable) {
const age = formatRelativeAge(row.updatedAt);
console.log(`${dryRun ? "[dry-run] " : ""}${row.taskId}\t${row.branchName}\t${row.status}\t${age}`);
writeStdout(`${dryRun ? "[dry-run] " : ""}${row.taskId}\t${row.branchName}\t${row.status}\t${age}`);
}
if (dryRun) {
console.log(`\n${prunable.length} task(s) would be pruned`);
writeStdout(`\n${prunable.length} task(s) would be pruned`);
return;
}
if (!yes) {
console.log("\nnot yet implemented: auto-pruning requires confirmation");
writeStdout("\nnot yet implemented: auto-pruning requires confirmation");
return;
}
console.log(`\n${prunable.length} task(s) would be pruned (pruning not yet implemented)`);
writeStdout(`\n${prunable.length} task(s) would be pruned (pruning not yet implemented)`);
}
async function handleStatusline(args: string[]): Promise<void> {
@ -375,11 +376,11 @@ async function handleStatusline(args: string[]): Promise<void> {
const errorCount = summary.byStatus.error;
if (format === "claude-code") {
console.log(`hf:${running}R/${idle}I/${errorCount}E`);
writeStdout(`hf:${running}R/${idle}I/${errorCount}E`);
return;
}
console.log(`running=${running} idle=${idle} error=${errorCount}`);
writeStdout(`running=${running} idle=${idle} error=${errorCount}`);
}
async function handleDb(args: string[]): Promise<void> {
@ -387,12 +388,12 @@ async function handleDb(args: string[]): Promise<void> {
if (sub === "path") {
const config = loadConfig();
const dbPath = config.backend.dbPath.replace(/^~/, homedir());
console.log(dbPath);
writeStdout(dbPath);
return;
}
if (sub === "nuke") {
console.log("WARNING: hf db nuke would delete the entire database. This is a placeholder and does not delete anything.");
writeStdout("WARNING: hf db nuke would delete the entire database. This is a placeholder and does not delete anything.");
return;
}
@ -465,12 +466,12 @@ async function handleCreate(args: string[]): Promise<void> {
const switched = await client.switchTask(workspaceId, task.taskId);
const attached = await client.attachTask(workspaceId, task.taskId);
console.log(`Branch: ${task.branchName ?? "-"}`);
console.log(`Task: ${task.taskId}`);
console.log(`Provider: ${task.providerId}`);
console.log(`Session: ${attached.sessionId ?? "none"}`);
console.log(`Target: ${switched.switchTarget || attached.target}`);
console.log(`Title: ${task.title ?? "-"}`);
writeStdout(`Branch: ${task.branchName ?? "-"}`);
writeStdout(`Task: ${task.taskId}`);
writeStdout(`Provider: ${task.providerId}`);
writeStdout(`Session: ${attached.sessionId ?? "none"}`);
writeStdout(`Target: ${switched.switchTarget || attached.target}`);
writeStdout(`Title: ${task.title ?? "-"}`);
const tmuxResult = spawnCreateTmuxWindow({
branchName: task.branchName ?? task.taskId,
@ -479,14 +480,14 @@ async function handleCreate(args: string[]): Promise<void> {
});
if (tmuxResult.created) {
console.log(`Window: created (${task.branchName})`);
writeStdout(`Window: created (${task.branchName})`);
return;
}
console.log("");
console.log(`Run: hf switch ${task.taskId}`);
writeStdout("");
writeStdout(`Run: hf switch ${task.taskId}`);
if ((switched.switchTarget || attached.target).startsWith("/")) {
console.log(`cd ${switched.switchTarget || attached.target}`);
writeStdout(`cd ${switched.switchTarget || attached.target}`);
}
}
@ -510,7 +511,7 @@ async function handleStatus(args: string[]): Promise<void> {
const summary = summarizeTasks(rows);
if (hasFlag(args, "--json")) {
console.log(
writeStdout(
JSON.stringify(
{
workspaceId,
@ -528,16 +529,16 @@ async function handleStatus(args: string[]): Promise<void> {
return;
}
console.log(`workspace=${workspaceId}`);
console.log(`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`);
console.log(`tasks total=${summary.total}`);
console.log(
writeStdout(`workspace=${workspaceId}`);
writeStdout(`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`);
writeStdout(`tasks total=${summary.total}`);
writeStdout(
`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}`)
.join(" ");
console.log(`providers ${providerSummary || "-"}`);
writeStdout(`providers ${providerSummary || "-"}`);
}
async function handleHistory(args: string[]): Promise<void> {
@ -560,12 +561,12 @@ async function handleHistory(args: string[]): Promise<void> {
});
if (hasFlag(args, "--json")) {
console.log(JSON.stringify(rows, null, 2));
writeStdout(JSON.stringify(rows, null, 2));
return;
}
if (rows.length === 0) {
console.log("no events");
writeStdout("no events");
return;
}
@ -576,7 +577,7 @@ async function handleHistory(args: string[]): Promise<void> {
if (payload.length > 120) {
payload = `${payload.slice(0, 117)}...`;
}
console.log(`${ts}\t${row.kind}\t${target}\t${payload}`);
writeStdout(`${ts}\t${row.kind}\t${target}\t${payload}`);
}
}
@ -611,19 +612,19 @@ async function handleSwitchLike(cmd: string, args: string[]): Promise<void> {
if (cmd === "switch") {
const result = await client.switchTask(workspaceId, taskId);
console.log(`cd ${result.switchTarget}`);
writeStdout(`cd ${result.switchTarget}`);
return;
}
if (cmd === "attach") {
const result = await client.attachTask(workspaceId, taskId);
console.log(`target=${result.target} session=${result.sessionId ?? "none"}`);
writeStdout(`target=${result.target} session=${result.sessionId ?? "none"}`);
return;
}
if (cmd === "merge" || cmd === "archive") {
await client.runAction(workspaceId, taskId, cmd);
console.log("ok");
writeStdout("ok");
return;
}
@ -726,6 +727,6 @@ async function main(): Promise<void> {
main().catch((err: unknown) => {
const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
console.error(msg);
writeStderr(msg);
process.exit(1);
});

View file

@ -0,0 +1,7 @@
export function writeStdout(message = ""): void {
process.stdout.write(`${message}\n`);
}
export function writeStderr(message = ""): void {
process.stderr.write(`${message}\n`);
}

View file

@ -0,0 +1,5 @@
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
export const logger = createFoundryLogger({
service: "foundry-cli",
});

View file

@ -2,6 +2,7 @@ import type { AppConfig, TaskRecord } from "@sandbox-agent/foundry-shared";
import { spawnSync } from "node:child_process";
import { createBackendClientFromConfig, filterTasks, formatRelativeAge, groupTaskStatus } from "@sandbox-agent/foundry-client";
import { CLI_BUILD_ID } from "./build-id.js";
import { writeStdout } from "./io.js";
import { resolveTuiTheme, type TuiTheme } from "./theme.js";
interface KeyEventLike {
@ -412,7 +413,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
process.off("SIGTERM", handleSignal);
renderer.destroy();
if (output) {
console.log(output);
writeStdout(output);
}
resolveDone();
};

View file

@ -1,8 +1,21 @@
import { describe, expect, it } from "vitest";
import type { TaskWorkbenchSnapshot, WorkbenchAgentTab, WorkbenchTask, WorkbenchModelId, WorkbenchTranscriptEvent } from "@sandbox-agent/foundry-shared";
import {
createFoundryLogger,
type TaskWorkbenchSnapshot,
type WorkbenchAgentTab,
type WorkbenchTask,
type WorkbenchModelId,
type WorkbenchTranscriptEvent,
} from "@sandbox-agent/foundry-shared";
import { createBackendClient } from "../../src/backend-client.js";
const RUN_WORKBENCH_LOAD_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E === "1";
const logger = createFoundryLogger({
service: "foundry-client-e2e",
bindings: {
suite: "workbench-load",
},
});
function requiredEnv(name: string): string {
const value = process.env[name]?.trim();
@ -269,12 +282,12 @@ describe("e2e(client): workbench load", () => {
const snapshotMetrics = await measureWorkbenchSnapshot(client, workspaceId, 3);
snapshotSeries.push(snapshotMetrics);
console.info(
"[workbench-load-snapshot]",
JSON.stringify({
logger.info(
{
taskIndex: taskIndex + 1,
...snapshotMetrics,
}),
},
"workbench_load_snapshot",
);
}
@ -296,7 +309,7 @@ describe("e2e(client): workbench load", () => {
snapshotTranscriptFinalCount: lastSnapshot.transcriptEventCount,
};
console.info("[workbench-load-summary]", JSON.stringify(summary));
logger.info(summary, "workbench_load_summary");
expect(createTaskLatencies.length).toBe(taskCount);
expect(provisionLatencies.length).toBe(taskCount);

View file

@ -16,6 +16,7 @@
"tsx": "^4"
},
"dependencies": {
"@sandbox-agent/foundry-shared": "workspace:*",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-shell": "^2"
}

View file

@ -2,15 +2,22 @@ import { execSync } from "node:child_process";
import { cpSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(__dirname, "..");
const repoRoot = resolve(desktopRoot, "../../..");
const frontendDist = resolve(desktopRoot, "../frontend/dist");
const destDir = resolve(desktopRoot, "frontend-dist");
const logger = createFoundryLogger({
service: "foundry-desktop-build",
bindings: {
script: "build-frontend",
},
});
function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
console.log(`> ${cmd}`);
logger.info({ command: cmd, cwd: opts?.cwd ?? repoRoot }, "run_command");
execSync(cmd, {
stdio: "inherit",
cwd: opts?.cwd ?? repoRoot,
@ -19,7 +26,7 @@ function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
}
// Step 1: Build the frontend with the desktop-specific backend endpoint
console.log("\n=== Building frontend for desktop ===\n");
logger.info("building_frontend");
run("pnpm --filter @sandbox-agent/foundry-frontend build", {
env: {
VITE_HF_BACKEND_ENDPOINT: "http://127.0.0.1:7741/v1/rivet",
@ -27,7 +34,7 @@ run("pnpm --filter @sandbox-agent/foundry-frontend build", {
});
// Step 2: Copy dist to frontend-dist/
console.log("\n=== Copying frontend build output ===\n");
logger.info({ frontendDist, destDir }, "copying_frontend_dist");
if (existsSync(destDir)) {
rmSync(destDir, { recursive: true });
}
@ -39,4 +46,4 @@ let html = readFileSync(indexPath, "utf-8");
html = html.replace(/<script\s+src="https:\/\/unpkg\.com\/react-scan\/dist\/auto\.global\.js"[^>]*><\/script>\s*/g, "");
writeFileSync(indexPath, html);
console.log("\n=== Frontend build complete ===\n");
logger.info({ indexPath }, "frontend_build_complete");

View file

@ -2,10 +2,17 @@ import { execSync } from "node:child_process";
import { mkdirSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(__dirname, "..");
const sidecarDir = resolve(desktopRoot, "src-tauri/sidecars");
const logger = createFoundryLogger({
service: "foundry-desktop-build",
bindings: {
script: "build-sidecar",
},
});
const isDev = process.argv.includes("--dev");
@ -35,7 +42,7 @@ const targets: Array<{ bunTarget: string; tripleTarget: string }> = isDev
];
function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
console.log(`> ${cmd}`);
logger.info({ command: cmd, cwd: opts?.cwd ?? desktopRoot }, "run_command");
execSync(cmd, {
stdio: "inherit",
cwd: opts?.cwd ?? desktopRoot,
@ -44,7 +51,7 @@ function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
}
// Step 1: Build the backend with tsup
console.log("\n=== Building backend with tsup ===\n");
logger.info("building_backend");
run("pnpm --filter @sandbox-agent/foundry-backend build", {
cwd: resolve(desktopRoot, "../../.."),
});
@ -55,14 +62,14 @@ mkdirSync(sidecarDir, { recursive: true });
const backendEntry = resolve(desktopRoot, "../backend/dist/index.js");
if (!existsSync(backendEntry)) {
console.error(`Backend build output not found at ${backendEntry}`);
logger.error({ backendEntry }, "backend_build_output_not_found");
process.exit(1);
}
for (const { bunTarget, tripleTarget } of targets) {
const outfile = resolve(sidecarDir, `foundry-backend-${tripleTarget}`);
console.log(`\n=== Compiling sidecar for ${tripleTarget} ===\n`);
logger.info({ bunTarget, tripleTarget, outfile }, "compiling_sidecar");
run(`bun build --compile --target ${bunTarget} ${backendEntry} --outfile ${outfile}`);
}
console.log("\n=== Sidecar build complete ===\n");
logger.info({ targets: targets.map((target) => target.tripleTarget) }, "sidecar_build_complete");

View file

@ -1,9 +1,11 @@
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type PointerEvent as ReactPointerEvent } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useStyletron } from "baseui";
import { createErrorContext } from "@sandbox-agent/foundry-shared";
import { PanelLeft, PanelRight } from "lucide-react";
import { useFoundryTokens } from "../app/theme";
import { logger } from "../logging.js";
import { DiffContent } from "./mock-layout/diff-content";
import { MessageList } from "./mock-layout/message-list";
@ -437,7 +439,13 @@ const TranscriptPanel = memo(function TranscriptPanel({
await window.navigator.clipboard.writeText(message.text);
setCopiedMessageId(message.id);
} catch (error) {
console.error("Failed to copy transcript message", error);
logger.error(
{
messageId: message.id,
...createErrorContext(error),
},
"failed_to_copy_transcript_message",
);
}
}, []);
@ -1108,7 +1116,13 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
const { tabId } = await taskWorkbenchClient.addTab({ taskId: activeTask.id });
syncRouteSession(activeTask.id, tabId, true);
} catch (error) {
console.error("failed to auto-create workbench session", error);
logger.error(
{
taskId: activeTask.id,
...createErrorContext(error),
},
"failed_to_auto_create_workbench_session",
);
} finally {
autoCreatingSessionForTaskRef.current.delete(activeTask.id);
}

View file

@ -4,6 +4,8 @@ import { LabelSmall } from "baseui/typography";
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest, PanelRight } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { createErrorContext } from "@sandbox-agent/foundry-shared";
import { logger } from "../../logging.js";
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
import { type FileTreeNode, type Task, diffTabId } from "./view-model";
@ -131,7 +133,13 @@ export const RightSidebar = memo(function RightSidebar({
await window.navigator.clipboard.writeText(path);
} catch (error) {
console.error("Failed to copy file path", error);
logger.error(
{
path,
...createErrorContext(error),
},
"failed_to_copy_file_path",
);
}
}, []);

View file

@ -0,0 +1,5 @@
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
export const logger = createFoundryLogger({
service: "foundry-frontend",
});

View file

@ -11,6 +11,7 @@
"test": "vitest run"
},
"dependencies": {
"pino": "^10.3.1",
"zod": "^4.1.5"
},
"devDependencies": {

View file

@ -1,5 +1,6 @@
export * from "./app-shell.js";
export * from "./contracts.js";
export * from "./config.js";
export * from "./logging.js";
export * from "./workbench.js";
export * from "./workspace.js";

View file

@ -0,0 +1,63 @@
import { pino, type Logger, type LoggerOptions } from "pino";
export interface FoundryLoggerOptions {
service: string;
bindings?: Record<string, unknown>;
level?: string;
}
type ProcessLike = {
env?: Record<string, string | undefined>;
};
function resolveEnvVar(name: string): string | undefined {
const value = (globalThis as { process?: ProcessLike }).process?.env?.[name];
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function defaultLevel(): string {
return resolveEnvVar("FOUNDRY_LOG_LEVEL") ?? resolveEnvVar("LOG_LEVEL") ?? resolveEnvVar("RIVET_LOG_LEVEL") ?? "info";
}
function isBrowserRuntime(): boolean {
return typeof window !== "undefined" && typeof document !== "undefined";
}
export function createFoundryLogger(options: FoundryLoggerOptions): Logger {
const browser = isBrowserRuntime();
const loggerOptions: LoggerOptions = {
level: options.level ?? defaultLevel(),
base: {
service: options.service,
...(options.bindings ?? {}),
},
};
if (browser) {
loggerOptions.browser = {
asObject: true,
};
} else {
loggerOptions.timestamp = pino.stdTimeFunctions.isoTime;
}
return pino(loggerOptions);
}
export function createErrorContext(error: unknown): { errorMessage: string; errorStack?: string } {
if (error instanceof Error) {
return {
errorMessage: error.message,
errorStack: error.stack,
};
}
return {
errorMessage: String(error),
};
}