Skip to content
Draft
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
24 changes: 24 additions & 0 deletions .github/workflows/format-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Format CI

on:
pull_request:
branches: [main]
workflow_dispatch:

jobs:
format:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Check formatting
run: npm run format:check -- --reporter=github --max-diagnostics=none
6 changes: 5 additions & 1 deletion packages/app/e2e/helpers/archive-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { pathToFileURL } from "node:url";
import { expect, type Page } from "@playwright/test";
import { buildCreateAgentPreferences, buildSeededHost } from "./daemon-registry";
import { waitForWorkspaceTabsVisible } from "./workspace-tabs";
import { buildHostAgentDetailRoute, buildHostSessionsRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
import {
buildHostAgentDetailRoute,
buildHostSessionsRoute,
buildHostWorkspaceRoute,
} from "@/utils/host-routes";

export type ArchiveTabAgent = {
id: string;
Expand Down
11 changes: 9 additions & 2 deletions packages/app/e2e/helpers/terminal-perf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ function getServerId(): string {
}

async function loadDaemonClientConstructor(): Promise<
new (config: { url: string; clientId: string; clientType: "cli" }) => TerminalPerfDaemonClient
new (config: {
url: string;
clientId: string;
clientType: "cli";
}) => TerminalPerfDaemonClient
> {
const repoRoot = path.resolve(process.cwd(), "../..");
const moduleUrl = pathToFileURL(
Expand Down Expand Up @@ -122,7 +126,10 @@ export async function navigateToTerminal(
await page.goto(workspaceRoute);

// Wait for daemon connection (sidebar shows host label)
await page.getByText("localhost", { exact: true }).first().waitFor({ state: "visible", timeout: 15_000 });
await page
.getByText("localhost", { exact: true })
.first()
.waitFor({ state: "visible", timeout: 15_000 });

// The workspace should now query listTerminals and discover our terminal.
// Click the terminal tab if it auto-appeared, or wait for it.
Expand Down
13 changes: 9 additions & 4 deletions packages/app/e2e/terminal-performance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ test.describe("Terminal wire performance", () => {

await terminal.pressSequentially(`seq 1 ${LINE_COUNT}; echo ${sentinel}\n`, { delay: 0 });

await waitForTerminalContent(page, (text) => text.includes(sentinel), THROUGHPUT_BUDGET_MS + 15_000);
await waitForTerminalContent(
page,
(text) => text.includes(sentinel),
THROUGHPUT_BUDGET_MS + 15_000,
);

const elapsedMs = Date.now() - startMs;

Expand All @@ -81,9 +85,10 @@ test.describe("Terminal wire performance", () => {
`[perf] Throughput: ${report.throughputMBps} MB/s — ${LINE_COUNT} lines in ${elapsedMs}ms`,
);

expect(elapsedMs, `${LINE_COUNT} lines should render within ${THROUGHPUT_BUDGET_MS}ms`).toBeLessThan(
THROUGHPUT_BUDGET_MS,
);
expect(
elapsedMs,
`${LINE_COUNT} lines should render within ${THROUGHPUT_BUDGET_MS}ms`,
).toBeLessThan(THROUGHPUT_BUDGET_MS);
} finally {
await client.killTerminal(terminalId).catch(() => {});
}
Expand Down
5 changes: 1 addition & 4 deletions packages/app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -744,10 +744,7 @@ function RootStack() {
<Stack.Screen name="welcome" />
<Stack.Screen name="settings" />
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
/>
<Stack.Screen name="h/[serverId]/agent/[agentId]" options={{ gestureEnabled: false }} />
<Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/sessions" />
<Stack.Screen name="h/[serverId]/open-project" />
Expand Down
15 changes: 3 additions & 12 deletions packages/app/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { useEffect, useSyncExternalStore } from "react";
import { usePathname, useRouter } from "expo-router";
import { StartupSplashScreen } from "@/screens/startup-splash-screen";
import {
useHostRuntimeBootstrapState,
useStoreReady,
} from "@/app/_layout";
import {
getHostRuntimeStore,
isHostRuntimeConnected,
useHosts,
} from "@/runtime/host-runtime";
import { useHostRuntimeBootstrapState, useStoreReady } from "@/app/_layout";
import { getHostRuntimeStore, isHostRuntimeConnected, useHosts } from "@/runtime/host-runtime";
import { buildHostRootRoute } from "@/utils/host-routes";

const WELCOME_ROUTE = "/welcome";
Expand Down Expand Up @@ -55,9 +48,7 @@ export default function Index() {
return;
}

const targetRoute = anyOnlineServerId
? buildHostRootRoute(anyOnlineServerId)
: WELCOME_ROUTE;
const targetRoute = anyOnlineServerId ? buildHostRootRoute(anyOnlineServerId) : WELCOME_ROUTE;
router.replace(targetRoute as any);
}, [anyOnlineServerId, pathname, router, storeReady]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,8 @@ export function ModelDropdown({
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<View>(null);

const selectedLabel = models.find((model) => model.id === selectedModel)?.label ?? selectedModel ?? "Select model";
const selectedLabel =
models.find((model) => model.id === selectedModel)?.label ?? selectedModel ?? "Select model";
const placeholder = isLoading && models.length === 0 ? "Loading..." : "Select model";
const helperText = error
? undefined
Expand Down
20 changes: 9 additions & 11 deletions packages/app/src/components/agent-input-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -594,14 +594,14 @@ export function AgentInputArea({
)}
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<View style={styles.tooltipRow}>
<Text style={styles.tooltipText}>Interrupt</Text>
{dictationCancelKeys ? (
<Shortcut chord={dictationCancelKeys} style={styles.tooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
<View style={styles.tooltipRow}>
<Text style={styles.tooltipText}>Interrupt</Text>
{dictationCancelKeys ? (
<Shortcut chord={dictationCancelKeys} style={styles.tooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
) : null;

const rightContent = (
Expand Down Expand Up @@ -643,9 +643,7 @@ export function AgentInputArea({
typeof agentState.contextWindowMaxTokens === "number" &&
typeof agentState.contextWindowUsedTokens === "number";
const contextWindowMaxTokens = hasContextWindowMeter ? agentState.contextWindowMaxTokens : null;
const contextWindowUsedTokens = hasContextWindowMeter
? agentState.contextWindowUsedTokens
: null;
const contextWindowUsedTokens = hasContextWindowMeter ? agentState.contextWindowUsedTokens : null;

const beforeVoiceContent = (
<View style={styles.contextWindowMeterSlot}>
Expand Down
46 changes: 29 additions & 17 deletions packages/app/src/components/agent-status-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,8 @@ function ControlledStatusBar({
);
return map;
}, [modelOptions, provider]);
const effectiveProviderDefinitions = providerDefinitions ??
const effectiveProviderDefinitions =
providerDefinitions ??
(PROVIDER_DEFINITION_MAP.has(provider) ? [PROVIDER_DEFINITION_MAP.get(provider)!] : []);
const effectiveAllProviderModels = allProviderModels ?? fallbackAllProviderModels;
const canSelectProviderInModelMenu = canSelectModelProvider ?? (() => true);
Expand Down Expand Up @@ -665,10 +666,7 @@ function ControlledStatusBar({
onClose={onDropdownClose}
renderTrigger={({ selectedModelLabel }) => (
<View
style={[
styles.sheetSelect,
modelDisabled && styles.disabledSheetSelect,
]}
style={[styles.sheetSelect, modelDisabled && styles.disabledSheetSelect]}
pointerEvents="none"
testID="agent-preferences-model"
>
Expand Down Expand Up @@ -923,7 +921,10 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
return (models ?? []).map((model) => ({ id: model.id, label: model.label }));
}, [models]);
const favoriteKeys = useMemo(
() => new Set((preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite))),
() =>
new Set(
(preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)),
),
[preferences.favoriteModels],
);

Expand All @@ -942,7 +943,9 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
<ControlledStatusBar
provider={agent.provider}
modeOptions={
modeOptions.length > 0 ? modeOptions : [{ id: agent.currentModeId ?? "", label: displayMode }]
modeOptions.length > 0
? modeOptions
: [{ id: agent.currentModeId ?? "", label: displayMode }]
}
selectedModeId={agent.currentModeId ?? undefined}
providerDefinitions={agentProviderDefinitions}
Expand Down Expand Up @@ -978,9 +981,11 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
}}
favoriteKeys={favoriteKeys}
onToggleFavoriteModel={(provider, modelId) => {
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
console.warn("[AgentStatusBar] toggle favorite model failed", error);
});
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch(
(error) => {
console.warn("[AgentStatusBar] toggle favorite model failed", error);
},
);
}}
thinkingOptions={thinkingOptions.length > 1 ? thinkingOptions : undefined}
selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined}
Expand Down Expand Up @@ -1064,7 +1069,10 @@ export function DraftAgentStatusBar({
return thinkingOptions.map((option) => ({ id: option.id, label: option.label }));
}, [thinkingOptions]);
const favoriteKeys = useMemo(
() => new Set((preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite))),
() =>
new Set(
(preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)),
),
[preferences.favoriteModels],
);

Expand All @@ -1083,9 +1091,11 @@ export function DraftAgentStatusBar({
onSelect={onSelectProviderAndModel}
favoriteKeys={favoriteKeys}
onToggleFavorite={(provider, modelId) => {
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
});
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch(
(error) => {
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
},
);
}}
isLoading={isAllModelsLoading}
disabled={disabled}
Expand Down Expand Up @@ -1129,9 +1139,11 @@ export function DraftAgentStatusBar({
isModelLoading={isAllModelsLoading}
favoriteKeys={favoriteKeys}
onToggleFavoriteModel={(provider, modelId) => {
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
});
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch(
(error) => {
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
},
);
}}
thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined}
selectedThinkingOptionId={effectiveSelectedThinkingOption}
Expand Down
3 changes: 1 addition & 2 deletions packages/app/src/components/agent-status-bar.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ export function resolveAgentModelSelection(input: {
: null;
const preferredModelId =
runtimeSelectedModel?.id ?? normalizedConfiguredModelId ?? normalizedRuntimeModelId;
const fallbackModel =
models?.find((model) => model.isDefault) ?? models?.[0] ?? null;
const fallbackModel = models?.find((model) => model.isDefault) ?? models?.[0] ?? null;
const selectedModel =
models && preferredModelId
? (models.find((model) => model.id === preferredModelId) ?? fallbackModel ?? null)
Expand Down
9 changes: 7 additions & 2 deletions packages/app/src/components/agent-stream-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
if (item.kind === "user_message" && isToolSequenceItem(belowItem)) {
return looseGap;
}
if ((item.kind === "user_message" || item.kind === "assistant_message") && isToolSequenceItem(belowItem)) {
if (
(item.kind === "user_message" || item.kind === "assistant_message") &&
isToolSequenceItem(belowItem)
) {
return tightGap;
}
if (item.kind === "todo_list" && isToolSequenceItem(belowItem)) {
Expand Down Expand Up @@ -898,7 +901,9 @@ function PermissionRequestCard({
</Text>
) : null}

{planMarkdown ? <PlanCard title="Proposed plan" text={planMarkdown} disableOuterSpacing /> : null}
{planMarkdown ? (
<PlanCard title="Proposed plan" text={planMarkdown} disableOuterSpacing />
) : null}

{!isPlanRequest ? (
<ToolCallDetailsContent
Expand Down
23 changes: 17 additions & 6 deletions packages/app/src/components/combined-model-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,25 @@ describe("combined model selector helpers", () => {
];

it("keeps enough data to search by model and provider name", async () => {
const rows = buildModelRows(providerDefinitions, new Map([
["claude", claudeModels],
["codex", codexModels],
]));
const rows = buildModelRows(
providerDefinitions,
new Map([
["claude", claudeModels],
["codex", codexModels],
]),
);

expect(rows).toEqual([
expect.objectContaining({ providerLabel: "Claude", modelLabel: "Sonnet 4.6", modelId: "sonnet-4.6" }),
expect.objectContaining({ providerLabel: "Codex", modelLabel: "GPT-5.4", modelId: "gpt-5.4" }),
expect.objectContaining({
providerLabel: "Claude",
modelLabel: "Sonnet 4.6",
modelId: "sonnet-4.6",
}),
expect.objectContaining({
providerLabel: "Codex",
modelLabel: "GPT-5.4",
modelId: "gpt-5.4",
}),
]);

expect(matchesSearch(rows[0]!, "claude")).toBe(true);
Expand Down
25 changes: 10 additions & 15 deletions packages/app/src/components/combined-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,8 @@ import {
} from "react-native";
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import {
ArrowLeft,
ChevronDown,
ChevronRight,
Search,
Star,
} from "lucide-react-native";
import type {
AgentModelDefinition,
AgentProvider,
} from "@server/server/agent/agent-sdk-types";
import { ArrowLeft, ChevronDown, ChevronRight, Search, Star } from "lucide-react-native";
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
const IS_WEB = Platform.OS === "web";

Expand Down Expand Up @@ -125,7 +116,10 @@ function sortFavoritesFirst(
function groupRowsByProvider(
rows: SelectorModelRow[],
): Array<{ providerId: string; providerLabel: string; rows: SelectorModelRow[] }> {
const grouped = new Map<string, { providerId: string; providerLabel: string; rows: SelectorModelRow[] }>();
const grouped = new Map<
string,
{ providerId: string; providerLabel: string; rows: SelectorModelRow[] }
>();

for (const row of rows) {
const existing = grouped.get(row.provider);
Expand Down Expand Up @@ -172,8 +166,7 @@ function ModelRow({
[onToggleFavorite, row.modelId, row.provider],
);

const showDescription =
row.description && PROVIDERS_WITH_MODEL_DESCRIPTIONS.has(row.provider);
const showDescription = row.description && PROVIDERS_WITH_MODEL_DESCRIPTIONS.has(row.provider);

return (
<ComboboxItem
Expand Down Expand Up @@ -290,7 +283,9 @@ function GroupedProviderRows({
return (
<View>
{groupedRows.map((group, index) => {
const providerDefinition = providerDefinitions.find((definition) => definition.id === group.providerId);
const providerDefinition = providerDefinitions.find(
(definition) => definition.id === group.providerId,
);
const ProvIcon = getProviderIcon(group.providerId);
const isInline = viewKind === "provider";

Expand Down
Loading