Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/actions/setup-mux/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,3 @@ runs:
if: steps.cache-node-modules.outputs.cache-hit != 'true'
shell: bash
run: bun install --frozen-lockfile

2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g., v1.2.3). If provided, will checkout and release this tag regardless of current branch.'
description: "Tag to release (e.g., v1.2.3). If provided, will checkout and release this tag regardless of current branch."
required: false
type: string

Expand Down
37 changes: 18 additions & 19 deletions .github/workflows/terminal-bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,34 @@ on:
workflow_call:
inputs:
model_name:
description: 'Model to use (e.g., anthropic:claude-sonnet-4-5)'
description: "Model to use (e.g., anthropic:claude-sonnet-4-5)"
required: false
type: string
thinking_level:
description: 'Thinking level (off, low, medium, high)'
description: "Thinking level (off, low, medium, high)"
required: false
type: string
dataset:
description: 'Terminal-Bench dataset to use'
description: "Terminal-Bench dataset to use"
required: false
type: string
default: 'terminal-bench-core==0.1.1'
default: "terminal-bench-core==0.1.1"
concurrency:
description: 'Number of concurrent tasks (--n-concurrent)'
description: "Number of concurrent tasks (--n-concurrent)"
required: false
type: string
default: '4'
default: "4"
livestream:
description: 'Enable livestream mode (verbose output to console)'
description: "Enable livestream mode (verbose output to console)"
required: false
type: boolean
default: false
sample_size:
description: 'Number of random tasks to run (empty = all tasks)'
description: "Number of random tasks to run (empty = all tasks)"
required: false
type: string
extra_args:
description: 'Additional arguments to pass to terminal-bench'
description: "Additional arguments to pass to terminal-bench"
required: false
type: string
secrets:
Expand All @@ -42,34 +42,34 @@ on:
workflow_dispatch:
inputs:
dataset:
description: 'Terminal-Bench dataset to use'
description: "Terminal-Bench dataset to use"
required: false
default: 'terminal-bench-core==0.1.1'
default: "terminal-bench-core==0.1.1"
type: string
concurrency:
description: 'Number of concurrent tasks (--n-concurrent)'
description: "Number of concurrent tasks (--n-concurrent)"
required: false
default: '4'
default: "4"
type: string
livestream:
description: 'Enable livestream mode (verbose output to console)'
description: "Enable livestream mode (verbose output to console)"
required: false
default: false
type: boolean
sample_size:
description: 'Number of random tasks to run (empty = all tasks)'
description: "Number of random tasks to run (empty = all tasks)"
required: false
type: string
model_name:
description: 'Model to use (e.g., anthropic:claude-sonnet-4-5, openai:gpt-5.1-codex)'
description: "Model to use (e.g., anthropic:claude-sonnet-4-5, openai:gpt-5.1-codex)"
required: false
type: string
thinking_level:
description: 'Thinking level (off, low, medium, high)'
description: "Thinking level (off, low, medium, high)"
required: false
type: string
extra_args:
description: 'Additional arguments to pass to terminal-bench'
description: "Additional arguments to pass to terminal-bench"
required: false
type: string

Expand Down Expand Up @@ -147,4 +147,3 @@ jobs:
benchmark.log
if-no-files-found: warn
retention-days: 30

217 changes: 217 additions & 0 deletions .storybook/mocks/orpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Mock ORPC client factory for Storybook stories.
*
* Creates a client that matches the AppRouter interface with configurable mock data.
*/
import type { ORPCClient } from "@/browser/orpc/react";
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import type { ProjectConfig } from "@/node/config";
import type { WorkspaceChatMessage } from "@/common/orpc/types";
import type { ChatStats } from "@/common/types/chatStats";
import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";

export interface MockORPCClientOptions {
projects?: Map<string, ProjectConfig>;
workspaces?: FrontendWorkspaceMetadata[];
/** Per-workspace chat callback. Return messages to emit, or use the callback for streaming. */
onChat?: (workspaceId: string, emit: (msg: WorkspaceChatMessage) => void) => (() => void) | void;
/** Mock for executeBash per workspace */
executeBash?: (
workspaceId: string,
script: string
) => Promise<{ success: true; output: string; exitCode: number; wall_duration_ms: number }>;
}

/**
* Creates a mock ORPC client for Storybook.
*
* Usage:
* ```tsx
* const client = createMockORPCClient({
* projects: new Map([...]),
* workspaces: [...],
* onChat: (wsId, emit) => {
* emit({ type: "caught-up" });
* // optionally return cleanup function
* },
* });
*
* return <AppLoader client={client} />;
* ```
*/
export function createMockORPCClient(options: MockORPCClientOptions = {}): ORPCClient {
const { projects = new Map(), workspaces = [], onChat, executeBash } = options;

const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));

const mockStats: ChatStats = {
consumers: [],
totalTokens: 0,
model: "mock-model",
tokenizerName: "mock-tokenizer",
usageHistory: [],
};

// Cast to ORPCClient - TypeScript can't fully validate the proxy structure
return {
tokenizer: {
countTokens: async () => 0,
countTokensBatch: async (_input: { model: string; texts: string[] }) =>
_input.texts.map(() => 0),
calculateStats: async () => mockStats,
},
server: {
getLaunchProject: async () => null,
},
providers: {
list: async () => [],
getConfig: async () => ({}),
setProviderConfig: async () => ({ success: true, data: undefined }),
setModels: async () => ({ success: true, data: undefined }),
},
general: {
listDirectory: async () => ({ entries: [], hasMore: false }),
ping: async (input: string) => `Pong: ${input}`,
tick: async function* () {
// No-op generator
},
},
projects: {
list: async () => Array.from(projects.entries()),
create: async () => ({
success: true,
data: { projectConfig: { workspaces: [] }, normalizedPath: "/mock/project" },
}),
pickDirectory: async () => null,
listBranches: async () => ({
branches: ["main", "develop"],
recommendedTrunk: "main",
}),
remove: async () => ({ success: true, data: undefined }),
secrets: {
get: async () => [],
update: async () => ({ success: true, data: undefined }),
},
},
workspace: {
list: async () => workspaces,
create: async (input: { projectPath: string; branchName: string }) => ({
success: true,
metadata: {
id: Math.random().toString(36).substring(2, 12),
name: input.branchName,
projectPath: input.projectPath,
projectName: input.projectPath.split("/").pop() ?? "project",
namedWorkspacePath: `/mock/workspace/${input.branchName}`,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
}),
remove: async () => ({ success: true }),
rename: async (input: { workspaceId: string }) => ({
success: true,
data: { newWorkspaceId: input.workspaceId },
}),
fork: async () => ({ success: false, error: "Not implemented in mock" }),
sendMessage: async () => ({ success: true, data: undefined }),
resumeStream: async () => ({ success: true, data: undefined }),
interruptStream: async () => ({ success: true, data: undefined }),
clearQueue: async () => ({ success: true, data: undefined }),
truncateHistory: async () => ({ success: true, data: undefined }),
replaceChatHistory: async () => ({ success: true, data: undefined }),
getInfo: async (input: { workspaceId: string }) =>
workspaceMap.get(input.workspaceId) ?? null,
executeBash: async (input: { workspaceId: string; script: string }) => {
if (executeBash) {
const result = await executeBash(input.workspaceId, input.script);
return { success: true, data: result };
}
return {
success: true,
data: { success: true, output: "", exitCode: 0, wall_duration_ms: 0 },
};
},
onChat: async function* (input: { workspaceId: string }) {
if (!onChat) {
yield { type: "caught-up" } as WorkspaceChatMessage;
return;
}

// Create a queue-based async iterator
const queue: WorkspaceChatMessage[] = [];
let resolveNext: ((msg: WorkspaceChatMessage) => void) | null = null;
let ended = false;

const emit = (msg: WorkspaceChatMessage) => {
if (ended) return;
if (resolveNext) {
const resolve = resolveNext;
resolveNext = null;
resolve(msg);
} else {
queue.push(msg);
}
};

// Call the user's onChat handler
const cleanup = onChat(input.workspaceId, emit);

try {
while (!ended) {
if (queue.length > 0) {
yield queue.shift()!;
} else {
const msg = await new Promise<WorkspaceChatMessage>((resolve) => {
resolveNext = resolve;
});
yield msg;
}
}
} finally {
ended = true;
cleanup?.();
}
},
onMetadata: async function* () {
// Empty generator - no metadata updates in mock
await new Promise(() => {}); // Never resolves, keeps stream open
},
activity: {
list: async () => ({}),
subscribe: async function* () {
await new Promise(() => {}); // Never resolves
},
},
},
window: {
setTitle: async () => undefined,
},
terminal: {
create: async () => ({
sessionId: "mock-session",
workspaceId: "mock-workspace",
cols: 80,
rows: 24,
}),
close: async () => undefined,
resize: async () => undefined,
sendInput: () => undefined,
onOutput: async function* () {
await new Promise(() => {});
},
onExit: async function* () {
await new Promise(() => {});
},
openWindow: async () => undefined,
closeWindow: async () => undefined,
openNative: async () => undefined,
},
update: {
check: async () => undefined,
download: async () => undefined,
install: () => undefined,
onStatus: async function* () {
await new Promise(() => {});
},
},
} as unknown as ORPCClient;
}
14 changes: 13 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import React, { useMemo } from "react";
import type { Preview } from "@storybook/react-vite";
import { ThemeProvider, type ThemeMode } from "../src/browser/contexts/ThemeContext";
import { ORPCProvider } from "../src/browser/orpc/react";
import { createMockORPCClient } from "./mocks/orpc";
import "../src/browser/styles/globals.css";

const preview: Preview = {
Expand All @@ -22,6 +24,16 @@ const preview: Preview = {
theme: "dark",
},
decorators: [
// Global ORPC provider - ensures useORPC works in all stories
(Story) => {
const client = useMemo(() => createMockORPCClient(), []);
return (
<ORPCProvider client={client}>
<Story />
</ORPCProvider>
);
},
// Theme provider
(Story, context) => {
// Default to dark if mode not set (e.g., Chromatic headless browser defaults to light)
const mode = (context.globals.theme as ThemeMode | undefined) ?? "dark";
Expand Down
19 changes: 19 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
modules: "commonjs",
},
],
[
"@babel/preset-typescript",
{
allowDeclareFields: true,
},
],
],
};
Loading