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
12 changes: 1 addition & 11 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|MultiEdit|Write|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "bunx prettier --write \"$1\" 1>/dev/null 2>/dev/null || true"
}
]
}
]
"PostToolUse": []
}
}
1 change: 0 additions & 1 deletion .github/actions/setup-mux/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,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

2 changes: 1 addition & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "path";

const config: StorybookConfig = {
stories: ["../src/browser/**/*.stories.@(ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-docs", "@storybook/addon-interactions"],
addons: ["@storybook/addon-links", "@storybook/addon-docs"],
framework: {
name: "@storybook/react-vite",
options: {},
Expand Down
206 changes: 206 additions & 0 deletions .storybook/mocks/orpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* Mock ORPC client factory for Storybook stories.
*
* Creates a client that matches the AppRouter interface with configurable mock data.
*/
import type { APIClient } from "@/browser/contexts/API";
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";
import { createAsyncMessageQueue } from "@/common/utils/asyncMessageQueue";

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 }>;
/** Provider configuration (API keys, base URLs, etc.) */
providersConfig?: Record<string, { apiKeySet: boolean; baseUrl?: string; models?: string[] }>;
/** List of available provider names */
providersList?: string[];
}

/**
* 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 = {}): APIClient {
const {
projects = new Map(),
workspaces = [],
onChat,
executeBash,
providersConfig = {},
providersList = [],
} = 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 () => providersList,
getConfig: async () => providersConfig,
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", "feature/new-feature"],
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;
}

const { push, iterate, end } = createAsyncMessageQueue<WorkspaceChatMessage>();

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

try {
yield* iterate();
} finally {
end();
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 APIClient;
}
1 change: 1 addition & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const preview: Preview = {
theme: "dark",
},
decorators: [
// 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
25 changes: 25 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
modules: "commonjs",
},
],
[
"@babel/preset-typescript",
{
allowDeclareFields: true,
},
],
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
};
Loading