diff --git a/.github/workflows/format-ci.yml b/.github/workflows/format-ci.yml new file mode 100644 index 000000000..6148a2836 --- /dev/null +++ b/.github/workflows/format-ci.yml @@ -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 diff --git a/packages/app/e2e/helpers/archive-tab.ts b/packages/app/e2e/helpers/archive-tab.ts index fe72677b8..900b91b37 100644 --- a/packages/app/e2e/helpers/archive-tab.ts +++ b/packages/app/e2e/helpers/archive-tab.ts @@ -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; diff --git a/packages/app/e2e/helpers/terminal-perf.ts b/packages/app/e2e/helpers/terminal-perf.ts index 18dd79bd1..29c41ba16 100644 --- a/packages/app/e2e/helpers/terminal-perf.ts +++ b/packages/app/e2e/helpers/terminal-perf.ts @@ -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( @@ -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. diff --git a/packages/app/e2e/terminal-performance.spec.ts b/packages/app/e2e/terminal-performance.spec.ts index da03858a4..ea3a595d0 100644 --- a/packages/app/e2e/terminal-performance.spec.ts +++ b/packages/app/e2e/terminal-performance.spec.ts @@ -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; @@ -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(() => {}); } diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 54e948f41..3dcaaf1d2 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -744,10 +744,7 @@ function RootStack() { - + diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index c65cc841d..038100177 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -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"; @@ -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]); diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index 2bf142aba..6e2d750bb 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -773,7 +773,8 @@ export function ModelDropdown({ const [isOpen, setIsOpen] = useState(false); const anchorRef = useRef(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 diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index 847f23488..082d653c7 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -594,14 +594,14 @@ export function AgentInputArea({ )} - - Interrupt - {dictationCancelKeys ? ( - - ) : null} - - - + + Interrupt + {dictationCancelKeys ? ( + + ) : null} + + + ) : null; const rightContent = ( @@ -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 = ( diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index 5954ad2af..794e39f7f 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -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); @@ -665,10 +666,7 @@ function ControlledStatusBar({ onClose={onDropdownClose} renderTrigger={({ selectedModelLabel }) => ( @@ -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], ); @@ -942,7 +943,9 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat 0 ? modeOptions : [{ id: agent.currentModeId ?? "", label: displayMode }] + modeOptions.length > 0 + ? modeOptions + : [{ id: agent.currentModeId ?? "", label: displayMode }] } selectedModeId={agent.currentModeId ?? undefined} providerDefinitions={agentProviderDefinitions} @@ -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} @@ -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], ); @@ -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} @@ -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} diff --git a/packages/app/src/components/agent-status-bar.utils.ts b/packages/app/src/components/agent-status-bar.utils.ts index baf563553..4001ed306 100644 --- a/packages/app/src/components/agent-status-bar.utils.ts +++ b/packages/app/src/components/agent-status-bar.utils.ts @@ -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) diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index f99667d54..1ab296d32 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -253,7 +253,10 @@ const AgentStreamViewComponent = forwardRef ) : null} - {planMarkdown ? : null} + {planMarkdown ? ( + + ) : null} {!isPlanRequest ? ( { ]; 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); diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index ffb0ef6df..64369ae6e 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -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"; @@ -125,7 +116,10 @@ function sortFavoritesFirst( function groupRowsByProvider( rows: SelectorModelRow[], ): Array<{ providerId: string; providerLabel: string; rows: SelectorModelRow[] }> { - const grouped = new Map(); + const grouped = new Map< + string, + { providerId: string; providerLabel: string; rows: SelectorModelRow[] } + >(); for (const row of rows) { const existing = grouped.get(row.provider); @@ -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 ( {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"; diff --git a/packages/app/src/components/combined-model-selector.utils.ts b/packages/app/src/components/combined-model-selector.utils.ts index fa7f102d3..7951e0663 100644 --- a/packages/app/src/components/combined-model-selector.utils.ts +++ b/packages/app/src/components/combined-model-selector.utils.ts @@ -8,7 +8,9 @@ export function resolveProviderLabel( providerDefinitions: AgentProviderDefinition[], providerId: string, ): string { - return providerDefinitions.find((definition) => definition.id === providerId)?.label ?? providerId; + return ( + providerDefinitions.find((definition) => definition.id === providerId)?.label ?? providerId + ); } export function buildSelectedTriggerLabel(providerLabel: string, modelLabel: string): string { @@ -19,7 +21,9 @@ export function buildModelRows( providerDefinitions: AgentProviderDefinition[], allProviderModels: Map, ): SelectorModelRow[] { - const providerLabelMap = new Map(providerDefinitions.map((definition) => [definition.id, definition.label])); + const providerLabelMap = new Map( + providerDefinitions.map((definition) => [definition.id, definition.label]), + ); const rows: SelectorModelRow[] = []; for (const definition of providerDefinitions) { diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index 1ab3eb94c..bb33c4c8a 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -270,7 +270,11 @@ export function ExplorerSidebar({ + {/* Resize handle - absolutely positioned over left border */} diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 44761930d..5458d7801 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -117,23 +117,18 @@ export function FileExplorerPane({ : undefined, ); - const { - requestDirectoryListing, - requestFileDownloadToken, - selectExplorerEntry, - } = useFileExplorerActions({ - serverId, - workspaceId, - workspaceRoot: normalizedWorkspaceRoot, - }); + const { requestDirectoryListing, requestFileDownloadToken, selectExplorerEntry } = + useFileExplorerActions({ + serverId, + workspaceId, + workspaceRoot: normalizedWorkspaceRoot, + }); const sortOption = usePanelStore((state) => state.explorerSortOption); const setSortOption = usePanelStore((state) => state.setExplorerSortOption); const expandedPathsArray = usePanelStore((state) => workspaceStateKey ? state.expandedPathsByWorkspace[workspaceStateKey] : undefined, ); - const setExpandedPathsForWorkspace = usePanelStore( - (state) => state.setExpandedPathsForWorkspace, - ); + const setExpandedPathsForWorkspace = usePanelStore((state) => state.setExpandedPathsForWorkspace); const expandedPaths = useMemo( () => new Set(expandedPathsArray && expandedPathsArray.length > 0 ? expandedPathsArray : ["."]), [expandedPathsArray], @@ -176,7 +171,8 @@ export function FileExplorerPane({ recordHistory: false, setCurrentPath: false, }); - const persistedPaths = usePanelStore.getState().expandedPathsByWorkspace[workspaceStateKey ?? ""]; + const persistedPaths = + usePanelStore.getState().expandedPathsByWorkspace[workspaceStateKey ?? ""]; if (persistedPaths) { for (const path of persistedPaths) { if (path !== ".") { @@ -200,10 +196,7 @@ export function FileExplorerPane({ if (newPaths.length === 0) { return; } - setExpandedPathsForWorkspace( - workspaceStateKey, - [...Array.from(expandedPaths), ...newPaths], - ); + setExpandedPathsForWorkspace(workspaceStateKey, [...Array.from(expandedPaths), ...newPaths]); newPaths.forEach((path) => { if (!directories.has(path)) { void requestDirectoryListing(path, { @@ -233,10 +226,7 @@ export function FileExplorerPane({ Array.from(expandedPaths).filter((path) => path !== entry.path), ); } else { - setExpandedPathsForWorkspace( - workspaceStateKey, - [...Array.from(expandedPaths), entry.path], - ); + setExpandedPathsForWorkspace(workspaceStateKey, [...Array.from(expandedPaths), entry.path]); if (!directories.has(entry.path)) { void requestDirectoryListing(entry.path, { recordHistory: false, diff --git a/packages/app/src/components/git-actions-split-button.tsx b/packages/app/src/components/git-actions-split-button.tsx index 3d45b68c0..e7cee42bf 100644 --- a/packages/app/src/components/git-actions-split-button.tsx +++ b/packages/app/src/components/git-actions-split-button.tsx @@ -81,9 +81,9 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps testID={`changes-menu-${action.id}`} leading={action.icon} trailing={ - action.id === "archive-worktree" && archiveShortcutKeys - ? - : undefined + action.id === "archive-worktree" && archiveShortcutKeys ? ( + + ) : undefined } disabled={action.disabled} status={action.status} diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index e790af7f2..734ddb5f7 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -1,12 +1,4 @@ -import { - useState, - useCallback, - useEffect, - useMemo, - useRef, - memo, - type ReactElement, -} from "react"; +import { useState, useCallback, useEffect, useMemo, useRef, memo, type ReactElement } from "react"; import { useRouter } from "expo-router"; import { View, @@ -115,12 +107,7 @@ function HighlightedText({ tokens, wrapLines = false }: HighlightedTextProps) { }; return ( - + {tokens.map((token, index) => ( {token.text} @@ -229,7 +216,7 @@ function SplitDiffCell({ - {row.content} + + {row.content} + ); } return ( - + state.setDiffExpandedPathsForWorkspace, ); - const expandedPaths = useMemo( - () => new Set(expandedPathsArray ?? []), - [expandedPathsArray], - ); + const expandedPaths = useMemo(() => new Set(expandedPathsArray ?? []), [expandedPathsArray]); const diffListRef = useRef>(null); const scrollbar = useWebScrollViewScrollbar(diffListRef, { enabled: showDesktopWebScrollbar, diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index a544efdab..47a067716 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -23,7 +23,13 @@ interface ScreenHeaderProps { * Shared frame for the home/back headers so we only maintain padding, border, * and safe-area logic in one place. */ -export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: ScreenHeaderProps) { +export function ScreenHeader({ + left, + right, + leftStyle, + rightStyle, + borderless, +}: ScreenHeaderProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const isMobile = isCompactFormFactor(); diff --git a/packages/app/src/components/icons/editor-app-icons.tsx b/packages/app/src/components/icons/editor-app-icons.tsx index a29341678..0475f47f0 100644 --- a/packages/app/src/components/icons/editor-app-icons.tsx +++ b/packages/app/src/components/icons/editor-app-icons.tsx @@ -18,10 +18,7 @@ const EDITOR_APP_IMAGES: Record = { }; /* eslint-enable @typescript-eslint/no-require-imports */ -export function EditorAppIcon({ - editorId, - size = 16, -}: EditorAppIconProps) { +export function EditorAppIcon({ editorId, size = 16 }: EditorAppIconProps) { return ( @@ -528,7 +530,6 @@ function MobileSidebar({ serverId={activeServerId} collapsedProjectKeys={collapsedProjectKeys} onToggleProjectCollapsed={toggleProjectCollapsed} - shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey} projects={projects} isRefreshing={isManualRefresh && isRevalidating} @@ -695,115 +696,120 @@ function DesktopSidebar({ } return ( - + - - - {padding.top > 0 ? : null} - - - + + + {padding.top > 0 ? : null} + + + + - - - {isInitialLoad ? ( - - ) : ( - - )} - - - [ - styles.hostTrigger, - hovered && styles.hostTriggerHovered, - ]} - onPress={() => setIsHostPickerOpen(true)} - disabled={hostOptions.length === 0} - > - - - {activeHostLabel} - - - - - - - - {({ hovered }) => ( - - )} - - - - - Add project - {newAgentKeys ? : null} - - - - - {({ hovered }) => ( - - )} - + {isInitialLoad ? ( + + ) : ( + + )} + + + + [ + styles.hostTrigger, + hovered && styles.hostTriggerHovered, + ]} + onPress={() => setIsHostPickerOpen(true)} + disabled={hostOptions.length === 0} + > + + + {activeHostLabel} + + + + + + + + {({ hovered }) => ( + + )} + + + + + Add project + {newAgentKeys ? : null} + + + + + {({ hovered }) => ( + + )} + + + - - - {/* Resize handle - absolutely positioned over right border */} - - - + {/* Resize handle - absolutely positioned over right border */} + + + ); diff --git a/packages/app/src/components/material-file-icons.ts b/packages/app/src/components/material-file-icons.ts index c683dbc84..03e761198 100644 --- a/packages/app/src/components/material-file-icons.ts +++ b/packages/app/src/components/material-file-icons.ts @@ -1,128 +1,128 @@ // Auto-generated from material-icon-theme. Do not edit manually. const SVG_ICONS: Record = { - "_default": ``, - "astro": ``, - "c": ``, - "clojure": ``, - "console": ``, - "cpp": ``, - "csharp": ``, - "css": ``, - "dart": ``, - "database": ``, - "document": ``, - "elixir": ``, - "erlang": ``, - "go": ``, - "gradle": ``, - "graphql": ``, - "groovy": ``, - "h": ``, - "haskell": ``, - "hcl": ``, - "hpp": ``, - "html": ``, - "image": ``, - "java": ``, - "javascript": ``, - "json": ``, - "kotlin": ``, - "less": ``, - "lock": ``, - "lua": ``, - "markdown": ``, - "nix": ``, - "ocaml": ``, - "php": ``, - "python": ``, - "r": ``, - "react": ``, - "react_ts": ``, - "ruby": ``, - "rust": ``, - "sass": ``, - "scala": ``, - "settings": ``, - "svelte": ``, - "svg": ``, - "swift": ``, - "terraform": ``, - "toml": ``, - "typescript": ``, - "vue": ``, - "webassembly": ``, - "xml": ``, - "yaml": ``, - "zig": ``, + _default: ``, + astro: ``, + c: ``, + clojure: ``, + console: ``, + cpp: ``, + csharp: ``, + css: ``, + dart: ``, + database: ``, + document: ``, + elixir: ``, + erlang: ``, + go: ``, + gradle: ``, + graphql: ``, + groovy: ``, + h: ``, + haskell: ``, + hcl: ``, + hpp: ``, + html: ``, + image: ``, + java: ``, + javascript: ``, + json: ``, + kotlin: ``, + less: ``, + lock: ``, + lua: ``, + markdown: ``, + nix: ``, + ocaml: ``, + php: ``, + python: ``, + r: ``, + react: ``, + react_ts: ``, + ruby: ``, + rust: ``, + sass: ``, + scala: ``, + settings: ``, + svelte: ``, + svg: ``, + swift: ``, + terraform: ``, + toml: ``, + typescript: ``, + vue: ``, + webassembly: ``, + xml: ``, + yaml: ``, + zig: ``, }; const EXTENSION_TO_ICON: Record = { - "astro": "astro", - "bash": "console", - "c": "c", - "cfg": "settings", - "clj": "clojure", - "conf": "settings", - "cpp": "cpp", - "cs": "csharp", - "css": "css", - "dart": "dart", - "erl": "erlang", - "ex": "elixir", - "exs": "elixir", - "gif": "image", - "go": "go", - "gql": "graphql", - "gradle": "gradle", - "graphql": "graphql", - "groovy": "groovy", - "h": "h", - "hcl": "hcl", - "hpp": "hpp", - "hs": "haskell", - "html": "html", - "ico": "image", - "ini": "settings", - "java": "java", - "jpeg": "image", - "jpg": "image", - "js": "javascript", - "json": "json", - "jsx": "react", - "kt": "kotlin", - "less": "less", - "lock": "lock", - "lua": "lua", - "markdown": "markdown", - "md": "markdown", - "ml": "ocaml", - "nix": "nix", - "php": "php", - "png": "image", - "py": "python", - "r": "r", - "rb": "ruby", - "rs": "rust", - "scala": "scala", - "scss": "sass", - "sh": "console", - "sql": "database", - "svelte": "svelte", - "svg": "svg", - "swift": "swift", - "tf": "terraform", - "toml": "toml", - "ts": "typescript", - "tsx": "react_ts", - "txt": "document", - "vue": "vue", - "wasm": "webassembly", - "webp": "image", - "xml": "xml", - "yaml": "yaml", - "yml": "yaml", - "zig": "zig", + astro: "astro", + bash: "console", + c: "c", + cfg: "settings", + clj: "clojure", + conf: "settings", + cpp: "cpp", + cs: "csharp", + css: "css", + dart: "dart", + erl: "erlang", + ex: "elixir", + exs: "elixir", + gif: "image", + go: "go", + gql: "graphql", + gradle: "gradle", + graphql: "graphql", + groovy: "groovy", + h: "h", + hcl: "hcl", + hpp: "hpp", + hs: "haskell", + html: "html", + ico: "image", + ini: "settings", + java: "java", + jpeg: "image", + jpg: "image", + js: "javascript", + json: "json", + jsx: "react", + kt: "kotlin", + less: "less", + lock: "lock", + lua: "lua", + markdown: "markdown", + md: "markdown", + ml: "ocaml", + nix: "nix", + php: "php", + png: "image", + py: "python", + r: "r", + rb: "ruby", + rs: "rust", + scala: "scala", + scss: "sass", + sh: "console", + sql: "database", + svelte: "svelte", + svg: "svg", + swift: "swift", + tf: "terraform", + toml: "toml", + ts: "typescript", + tsx: "react_ts", + txt: "document", + vue: "vue", + wasm: "webassembly", + webp: "image", + xml: "xml", + yaml: "yaml", + yml: "yaml", + zig: "zig", }; export function getFileIconSvg(fileName: string): string { diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 3bc5eacb3..1574a0b44 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -1877,7 +1877,11 @@ export const ToolCall = memo(function ToolCall({ if (effectiveDetail?.type === "plan") { return ( - + ); } diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index 20ff08967..06f6fd7ea 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -24,7 +24,8 @@ export function ProviderDiagnosticSheet({ const [diagnostic, setDiagnostic] = useState(null); const [loading, setLoading] = useState(false); - const providerLabel = AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === provider)?.label ?? provider; + const providerLabel = + AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === provider)?.label ?? provider; const fetchDiagnostic = useCallback(async () => { if (!client || !provider) return; diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index e29793dc9..1d15cc2ff 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -100,7 +100,10 @@ const DEFAULT_STATUS_DOT_SIZE = 7; const EMPHASIZED_STATUS_DOT_SIZE = 9; const DEFAULT_STATUS_DOT_OFFSET = 0; const EMPHASIZED_STATUS_DOT_OFFSET = -1; -function getWorkspacePrIconColor(theme: ReturnType["theme"], state: PrHint["state"]) { +function getWorkspacePrIconColor( + theme: ReturnType["theme"], + state: PrHint["state"], +) { switch (state) { case "merged": return theme.colors.palette.purple[500]; @@ -198,19 +201,10 @@ function WorkspacePrBadge({ hint }: { hint: PrHint }) { onPress={handlePress} onPointerEnter={() => setIsHovered(true)} onPointerLeave={() => setIsHovered(false)} - style={({ pressed }) => [ - styles.workspacePrBadge, - pressed && styles.workspacePrBadgePressed, - ]} + style={({ pressed }) => [styles.workspacePrBadge, pressed && styles.workspacePrBadgePressed]} > - + #{hint.number} {isHovered && } @@ -255,11 +249,7 @@ function WorkspaceStatusIndicator({ } const KindIcon = - workspaceKind === "local_checkout" - ? Monitor - : workspaceKind === "worktree" - ? FolderGit2 - : null; + workspaceKind === "local_checkout" ? Monitor : workspaceKind === "worktree" ? FolderGit2 : null; if (!KindIcon) return null; const dotColor = getStatusDotColor({ theme, bucket, showDoneAsInactive: false }); @@ -1798,34 +1788,31 @@ export function SidebarWorkspaceList({ [getWorkspaceOrder, serverId, setWorkspaceOrder], ); - const handleWorktreeCreated = useCallback( - (workspaceId: string) => { - setCreatingWorkspaceIds((current) => { - const next = new Set(current); - next.add(workspaceId); - return next; - }); - const existingTimeout = creatingWorkspaceTimeoutsRef.current.get(workspaceId); - if (existingTimeout) { - clearTimeout(existingTimeout); - } - creatingWorkspaceTimeoutsRef.current.set( - workspaceId, - setTimeout(() => { - creatingWorkspaceTimeoutsRef.current.delete(workspaceId); - setCreatingWorkspaceIds((current) => { - if (!current.has(workspaceId)) { - return current; - } - const next = new Set(current); - next.delete(workspaceId); - return next; - }); - }, 3000), - ); - }, - [], - ); + const handleWorktreeCreated = useCallback((workspaceId: string) => { + setCreatingWorkspaceIds((current) => { + const next = new Set(current); + next.add(workspaceId); + return next; + }); + const existingTimeout = creatingWorkspaceTimeoutsRef.current.get(workspaceId); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + creatingWorkspaceTimeoutsRef.current.set( + workspaceId, + setTimeout(() => { + creatingWorkspaceTimeoutsRef.current.delete(workspaceId); + setCreatingWorkspaceIds((current) => { + if (!current.has(workspaceId)) { + return current; + } + const next = new Set(current); + next.delete(workspaceId); + return next; + }); + }, 3000), + ); + }, []); const renderProject = useCallback( ({ item, drag, isActive, dragHandleProps }: DraggableRenderItemInfo) => { @@ -1875,12 +1862,7 @@ export function SidebarWorkspaceList({ No projects yet Add a project to get started - diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 8b521358f..255ad127e 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -177,7 +177,6 @@ const MountedTabSlot = memo(function MountedTabSlot({ paneId, buildPaneContentModel, }: MountedTabSlotProps) { - const content = useMemo( () => buildPaneContentModel({ @@ -877,12 +876,7 @@ function SplitPaneView({ return ( - + - {mountedPaneTabIds.length > 0 ? ( - mountedPaneTabIds.map((tabId) => { - const tabDescriptor = tabDescriptorMap.get(tabId); - if (!tabDescriptor) { - return null; - } - - return ( - - ); - }) - ) : ( - (renderPaneEmptyState?.() ?? null) - )} + {mountedPaneTabIds.length > 0 + ? mountedPaneTabIds.map((tabId) => { + const tabDescriptor = tabDescriptorMap.get(tabId); + if (!tabDescriptor) { + return null; + } + + return ( + + ); + }) + : (renderPaneEmptyState?.() ?? null)} diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index df1f27e70..89e0f4c54 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -82,12 +82,7 @@ function terminalScopeKey(input: { serverId: string; cwd: string }): string { return `${input.serverId}:${input.cwd}`; } -export function TerminalPane({ - serverId, - cwd, - terminalId, - isPaneFocused, -}: TerminalPaneProps) { +export function TerminalPane({ serverId, cwd, terminalId, isPaneFocused }: TerminalPaneProps) { const isScreenFocused = useIsFocused(); const isAppVisible = useAppVisible(); const { theme } = useUnistyles(); @@ -108,7 +103,10 @@ export function TerminalPane({ const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]); const lastReportedSizeRef = useRef<{ rows: number; cols: number } | null>(null); const streamControllerRef = useRef(null); - const workspaceTerminalSession = useMemo(() => getWorkspaceTerminalSession({ scopeKey }), [scopeKey]); + const workspaceTerminalSession = useMemo( + () => getWorkspaceTerminalSession({ scopeKey }), + [scopeKey], + ); const [isAttaching, setIsAttaching] = useState(false); const [streamError, setStreamError] = useState(null); const [modifiers, setModifiers] = useState(EMPTY_MODIFIERS); @@ -473,26 +471,32 @@ export function TerminalPane({ ], ); - const handleTerminalResize = useStableEvent( - (input: { rows: number; cols: number }) => { - const { rows, cols } = input; - if (!client || !terminalId || !isPaneFocused || !isScreenFocused || !isAppVisible || rows <= 0 || cols <= 0) { - return; - } - const normalizedRows = Math.floor(rows); - const normalizedCols = Math.floor(cols); - const previous = lastReportedSizeRef.current; - if (previous && previous.rows === normalizedRows && previous.cols === normalizedCols) { - return; - } - lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols }; - client.sendTerminalInput(terminalId, { - type: "resize", - rows: normalizedRows, - cols: normalizedCols, - }); - }, - ); + const handleTerminalResize = useStableEvent((input: { rows: number; cols: number }) => { + const { rows, cols } = input; + if ( + !client || + !terminalId || + !isPaneFocused || + !isScreenFocused || + !isAppVisible || + rows <= 0 || + cols <= 0 + ) { + return; + } + const normalizedRows = Math.floor(rows); + const normalizedCols = Math.floor(cols); + const previous = lastReportedSizeRef.current; + if (previous && previous.rows === normalizedRows && previous.cols === normalizedCols) { + return; + } + lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols }; + client.sendTerminalInput(terminalId, { + type: "resize", + rows: normalizedRows, + cols: normalizedCols, + }); + }); const handleTerminalKey = useCallback( async (input: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean }) => { diff --git a/packages/app/src/components/ui/button.tsx b/packages/app/src/components/ui/button.tsx index bc16ed2c4..195b95d58 100644 --- a/packages/app/src/components/ui/button.tsx +++ b/packages/app/src/components/ui/button.tsx @@ -136,21 +136,32 @@ export function Button({ return {leftIcon}; } - const color = variant === "default" - ? theme.colors.accentForeground - : variant === "ghost" - ? (isGhostHovered ? theme.colors.foreground : theme.colors.foregroundMuted) - : theme.colors.foreground; + const color = + variant === "default" + ? theme.colors.accentForeground + : variant === "ghost" + ? isGhostHovered + ? theme.colors.foreground + : theme.colors.foregroundMuted + : theme.colors.foreground; const iconSize = ICON_SIZE[size]; // Render function - if (typeof leftIcon === "function" && !leftIcon.prototype?.isReactComponent && leftIcon.length > 0) { + if ( + typeof leftIcon === "function" && + !leftIcon.prototype?.isReactComponent && + leftIcon.length > 0 + ) { return {(leftIcon as (color: string) => ReactElement)(color)}; } // Component type const Icon = leftIcon as ComponentType<{ color: string; size: number }>; - return ; + return ( + + + + ); } return ( diff --git a/packages/app/src/components/use-web-scrollbar.tsx b/packages/app/src/components/use-web-scrollbar.tsx index 2e19032bf..b9a1f5f2c 100644 --- a/packages/app/src/components/use-web-scrollbar.tsx +++ b/packages/app/src/components/use-web-scrollbar.tsx @@ -105,7 +105,9 @@ export function useWebElementScrollbar( if (!enabled) return null; - return ; + return ( + + ); } // ── RN ScrollView / FlatList scrollbar ─────────────────────────────── diff --git a/packages/app/src/components/welcome-screen.tsx b/packages/app/src/components/welcome-screen.tsx index bbdf38f7e..b591bf8ae 100644 --- a/packages/app/src/components/welcome-screen.tsx +++ b/packages/app/src/components/welcome-screen.tsx @@ -351,10 +351,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) { You need the Paseo desktop app or server running on your computer first. - openExternalUrl("https://paseo.sh")} - > + openExternalUrl("https://paseo.sh")}> Get started at paseo.sh diff --git a/packages/app/src/contexts/explorer-sidebar-animation-context.tsx b/packages/app/src/contexts/explorer-sidebar-animation-context.tsx index 80d5c191d..284edd454 100644 --- a/packages/app/src/contexts/explorer-sidebar-animation-context.tsx +++ b/packages/app/src/contexts/explorer-sidebar-animation-context.tsx @@ -91,13 +91,7 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React translateX.value = targets.translateX; backdropOpacity.value = targets.backdropOpacity; - }, [ - isOpen, - translateX, - backdropOpacity, - windowWidth, - isGesturing, - ]); + }, [isOpen, translateX, backdropOpacity, windowWidth, isGesturing]); const animateToOpen = () => { "worklet"; diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index df0756eff..7d56a8f9d 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -329,7 +329,9 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD Status - Only the built-in desktop daemon is shown here. + + Only the built-in desktop daemon is shown here. + {daemonStatusStateText} @@ -394,7 +396,9 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD Log file - {daemonLogs?.logPath ?? "Log path unavailable."} + + {daemonLogs?.logPath ?? "Log path unavailable."} + {daemonLogs?.logPath ? ( @@ -482,7 +486,9 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD snapPoints={["70%", "92%"]} > - {daemonLogs?.logPath ?? "Log path unavailable."} + + {daemonLogs?.logPath ?? "Log path unavailable."} + {daemonLogs?.contents.length ? daemonLogs.contents : "(log file is empty)"} diff --git a/packages/app/src/desktop/components/integrations-section.tsx b/packages/app/src/desktop/components/integrations-section.tsx index cfc1b8510..ea1b8b909 100644 --- a/packages/app/src/desktop/components/integrations-section.tsx +++ b/packages/app/src/desktop/components/integrations-section.tsx @@ -87,7 +87,9 @@ export function IntegrationsSection() { diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 796c21bf2..b4743f436 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -425,7 +425,6 @@ function AppearanceSection({ settings, handleThemeChange }: AppearanceSectionPro ); } - interface ProvidersSectionProps { routeServerId: string; } @@ -447,10 +446,7 @@ function ProvidersSection({ routeServerId }: ProvidersSectionProps) { - + {def.label} @@ -496,11 +497,7 @@ function ProvidersSection({ routeServerId }: ProvidersSectionProps) { : "Not installed" } variant={ - status === "ready" - ? "success" - : status === "error" - ? "error" - : "muted" + status === "ready" ? "success" : status === "error" ? "error" : "muted" } /> ) : null} - @@ -182,9 +178,7 @@ export function KeyboardShortcutsSection() { Shortcuts - - Keyboard shortcuts are only available on desktop. - + Keyboard shortcuts are only available on desktop. ); diff --git a/packages/app/src/screens/startup-splash-screen.tsx b/packages/app/src/screens/startup-splash-screen.tsx index 188fb9e9c..15911ff29 100644 --- a/packages/app/src/screens/startup-splash-screen.tsx +++ b/packages/app/src/screens/startup-splash-screen.tsx @@ -7,10 +7,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { PaseoLogo } from "@/components/icons/paseo-logo"; import { Button } from "@/components/ui/button"; import { Fonts } from "@/constants/theme"; -import { - getDesktopDaemonLogs, - type DesktopDaemonLogs, -} from "@/desktop/daemon/desktop-daemon"; +import { getDesktopDaemonLogs, type DesktopDaemonLogs } from "@/desktop/daemon/desktop-daemon"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; type StartupSplashScreenProps = { @@ -193,7 +190,11 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps : phase === "connecting" ? [ { key: "starting-daemon", label: "Started local server", status: "complete" as const }, - { key: "connecting", label: "Connecting to local server...", status: "active" as const }, + { + key: "connecting", + label: "Connecting to local server...", + status: "active" as const, + }, ] : [ { key: "starting-daemon", label: "Started local server", status: "complete" as const }, @@ -264,12 +265,11 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps - The local server failed to start. If this keeps happening, please report the issue on GitHub and include the logs below. + The local server failed to start. If this keeps happening, please report the issue on + GitHub and include the logs below. - - {bootstrapState.error} - + {bootstrapState.error} {daemonLogs?.logPath ? {daemonLogs.logPath} : null} diff --git a/packages/app/src/screens/workspace/use-mounted-tab-set.ts b/packages/app/src/screens/workspace/use-mounted-tab-set.ts index 35eeaddbb..ec52b5da7 100644 --- a/packages/app/src/screens/workspace/use-mounted-tab-set.ts +++ b/packages/app/src/screens/workspace/use-mounted-tab-set.ts @@ -34,9 +34,7 @@ export function useMountedTabSet(input: UseMountedTabSetInput): UseMountedTabSet const allTabIdsKey = allTabIds.join("\u0000"); const availableTabIds = useMemo(() => new Set(allTabIds), [allTabIdsKey]); const [mountedTabIds, setMountedTabIds] = useState(() => createInitialMountedTabIds(input)); - const lruRef = useRef( - activeTabId && allTabIds.includes(activeTabId) ? [activeTabId] : [], - ); + const lruRef = useRef(activeTabId && allTabIds.includes(activeTabId) ? [activeTabId] : []); useLayoutEffect(() => { const nextLru = lruRef.current.filter((tabId) => availableTabIds.has(tabId)); diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 110f01970..9c5e4f54c 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -314,9 +314,7 @@ function TabChip({ } })()} trailing={ - entry.hint ? ( - {entry.hint} - ) : undefined + entry.hint ? {entry.hint} : undefined } > {entry.label} diff --git a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx index 31457d675..6204f280f 100644 --- a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx +++ b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx @@ -3,10 +3,7 @@ import { ActivityIndicator, Platform, Pressable, Text, View } from "react-native import { useMutation, useQuery } from "@tanstack/react-query"; import { Check, ChevronDown } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; -import type { - EditorTargetDescriptorPayload, - EditorTargetId, -} from "@server/shared/messages"; +import type { EditorTargetDescriptorPayload, EditorTargetId } from "@server/shared/messages"; import { EditorAppIcon } from "@/components/icons/editor-app-icons"; import { DropdownMenu, @@ -16,10 +13,7 @@ import { } from "@/components/ui/dropdown-menu"; import { useToast } from "@/contexts/toast-context"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; -import { - resolvePreferredEditorId, - usePreferredEditor, -} from "@/hooks/use-preferred-editor"; +import { resolvePreferredEditorId, usePreferredEditor } from "@/hooks/use-preferred-editor"; import { isAbsolutePath } from "@/utils/path"; interface WorkspaceOpenInEditorButtonProps { @@ -27,10 +21,7 @@ interface WorkspaceOpenInEditorButtonProps { cwd: string; } -export function WorkspaceOpenInEditorButton({ - serverId, - cwd, -}: WorkspaceOpenInEditorButtonProps) { +export function WorkspaceOpenInEditorButton({ serverId, cwd }: WorkspaceOpenInEditorButtonProps) { const { theme } = useUnistyles(); const toast = useToast(); const client = useHostRuntimeClient(serverId); @@ -173,9 +164,9 @@ export function WorkspaceOpenInEditorButton({ /> } trailing={ - editor.id === effectivePreferredEditorId - ? - : undefined + editor.id === effectivePreferredEditorId ? ( + + ) : undefined } onSelect={() => handleOpenEditor(editor.id)} > diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 186d81283..0ea0eac85 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1445,12 +1445,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) setHoveredTabKey((current) => (current && closedKeys.has(current) ? null : current)); setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current)); }, - [ - client, - closeTab, - closeWorkspaceTabWithCleanup, - persistenceKey, - ], + [client, closeTab, closeWorkspaceTabWithCleanup, persistenceKey], ); const handleCloseTabsToLeftInPane = useCallback( @@ -1553,7 +1548,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) return false; } }, - [activeTabId, handleCloseTabById, handleCreateDraftTab, handleCreateTerminal, navigateToTabId, tabs], + [ + activeTabId, + handleCloseTabById, + handleCreateDraftTab, + handleCreateTerminal, + navigateToTabId, + tabs, + ], ); const handleWorkspacePaneAction = useCallback( @@ -1936,219 +1938,219 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) {(!isFocusModeEnabled || isMobile) && ( - - - - {isWorkspaceHeaderLoading ? ( - <> - - - - ) : ( - <> - - {workspaceHeader.title} - - - {workspaceHeader.subtitle} - - - )} - - - {({ hovered, open }) => { - const Icon = isMobile ? EllipsisVertical : Ellipsis; - return ( - - ); - }} - - - } - onSelect={handleCreateDraftTab} - > - New agent - - } - disabled={createTerminalMutation.isPending} - onSelect={handleCreateTerminal} - > - New terminal - - } - disabled={!isAbsolutePath(normalizedWorkspaceId)} - onSelect={handleCopyWorkspacePath} + + + + {isWorkspaceHeaderLoading ? ( + <> + + + + ) : ( + <> + + {workspaceHeader.title} + + + {workspaceHeader.subtitle} + + + )} + + - Copy workspace path - - {currentBranchName ? ( + {({ hovered, open }) => { + const Icon = isMobile ? EllipsisVertical : Ellipsis; + return ( + + ); + }} + + } + onSelect={handleCreateDraftTab} + > + New agent + + + } + disabled={createTerminalMutation.isPending} + onSelect={handleCreateTerminal} + > + New terminal + + } - onSelect={handleCopyBranchName} + disabled={!isAbsolutePath(normalizedWorkspaceId)} + onSelect={handleCopyWorkspacePath} > - Copy branch name + Copy workspace path - ) : null} - - - - - } - right={ - - {!isMobile ? ( - - ) : null} - {!isMobile && isGitCheckout ? ( - <> - } + onSelect={handleCopyBranchName} + > + Copy branch name + + ) : null} + + + + + } + right={ + + {!isMobile ? ( + - - - [ - styles.sourceControlButton, - workspaceDescriptor?.diffStat && styles.sourceControlButtonWithStats, - (hovered || pressed || isExplorerOpen) && - styles.sourceControlButtonHovered, - ]} + ) : null} + {!isMobile && isGitCheckout ? ( + <> + + + + [ + styles.sourceControlButton, + workspaceDescriptor?.diffStat && styles.sourceControlButtonWithStats, + (hovered || pressed || isExplorerOpen) && + styles.sourceControlButtonHovered, + ]} + > + {({ hovered, pressed }) => { + const active = isExplorerOpen || hovered || pressed; + const iconColor = active + ? theme.colors.foreground + : theme.colors.foregroundMuted; + return ( + <> + + {workspaceDescriptor?.diffStat ? ( + + + +{workspaceDescriptor.diffStat.additions} + + + -{workspaceDescriptor.diffStat.deletions} + + + ) : null} + + ); + }} + + + - {({ hovered, pressed }) => { - const active = isExplorerOpen || hovered || pressed; - const iconColor = active - ? theme.colors.foreground - : theme.colors.foregroundMuted; - return ( - <> - - {workspaceDescriptor?.diffStat ? ( - - - +{workspaceDescriptor.diffStat.additions} - - - -{workspaceDescriptor.diffStat.deletions} - - - ) : null} - - ); - }} - - - - - Toggle explorer - - - - - - ) : null} - {!isMobile && !isGitCheckout ? ( - - {({ hovered }) => { - const color = - isExplorerOpen || hovered - ? theme.colors.foreground - : theme.colors.foregroundMuted; - return ; - }} - - ) : null} - {isMobile ? ( - - {({ hovered }) => { - const color = - isExplorerOpen || hovered - ? theme.colors.foreground - : theme.colors.foregroundMuted; - return isGitCheckout ? ( - - ) : ( - - ); - }} - - ) : null} - - } - /> + + Toggle explorer + + + + + + ) : null} + {!isMobile && !isGitCheckout ? ( + + {({ hovered }) => { + const color = + isExplorerOpen || hovered + ? theme.colors.foreground + : theme.colors.foregroundMuted; + return ; + }} + + ) : null} + {isMobile ? ( + + {({ hovered }) => { + const color = + isExplorerOpen || hovered + ? theme.colors.foreground + : theme.colors.foregroundMuted; + return isGitCheckout ? ( + + ) : ( + + ); + }} + + ) : null} + + } + /> )} {isMobile ? ( diff --git a/packages/app/src/screens/workspace/workspace-tab-menu.ts b/packages/app/src/screens/workspace/workspace-tab-menu.ts index 1b31729a0..7002500c3 100644 --- a/packages/app/src/screens/workspace/workspace-tab-menu.ts +++ b/packages/app/src/screens/workspace/workspace-tab-menu.ts @@ -8,13 +8,7 @@ export type WorkspaceTabMenuEntry = kind: "item"; key: string; label: string; - icon?: - | "copy" - | "rotate-cw" - | "arrow-left-to-line" - | "arrow-right-to-line" - | "copy-x" - | "x"; + icon?: "copy" | "rotate-cw" | "arrow-left-to-line" | "arrow-right-to-line" | "copy-x" | "x"; hint?: string; tooltip?: string; disabled?: boolean; diff --git a/packages/app/src/stores/session-store.test.ts b/packages/app/src/stores/session-store.test.ts index b265b7b70..ee6d649bb 100644 --- a/packages/app/src/stores/session-store.test.ts +++ b/packages/app/src/stores/session-store.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - mergeWorkspaceSnapshotWithExisting, - type WorkspaceDescriptor, -} from "./session-store"; +import { mergeWorkspaceSnapshotWithExisting, type WorkspaceDescriptor } from "./session-store"; function createWorkspace( input: Partial & Pick, diff --git a/packages/app/src/terminal/runtime/terminal-snapshot.test.ts b/packages/app/src/terminal/runtime/terminal-snapshot.test.ts index 97867cf6f..e3570115c 100644 --- a/packages/app/src/terminal/runtime/terminal-snapshot.test.ts +++ b/packages/app/src/terminal/runtime/terminal-snapshot.test.ts @@ -60,9 +60,7 @@ function extractState(terminal: ClientTerminal | HeadlessTerminal): SnapshotStat }; } -function extractCursorState( - terminal: ClientTerminal | HeadlessTerminal, -): SnapshotState["cursor"] { +function extractCursorState(terminal: ClientTerminal | HeadlessTerminal): SnapshotState["cursor"] { const buffer = terminal.buffer.active; const coreService = (terminal as any)._core?.coreService; const cursorStyle = coreService?.decPrivateModes?.cursorStyle; diff --git a/packages/app/src/utils/desktop-window.ts b/packages/app/src/utils/desktop-window.ts index e72f5dfdd..6d22d812c 100644 --- a/packages/app/src/utils/desktop-window.ts +++ b/packages/app/src/utils/desktop-window.ts @@ -95,9 +95,11 @@ function useRawWindowControlsPadding(): RawWindowControlsPadding { }, [isFullscreen]); } -export function useWindowControlsPadding( - role: WindowControlsPaddingRole, -): { left: number; right: number; top: number } { +export function useWindowControlsPadding(role: WindowControlsPaddingRole): { + left: number; + right: number; + top: number; +} { const sidebarOpen = usePanelStore((state) => state.desktop.agentListOpen); const explorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const focusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); diff --git a/packages/app/src/utils/path.ts b/packages/app/src/utils/path.ts index 6013fe79f..6a762b0b5 100644 --- a/packages/app/src/utils/path.ts +++ b/packages/app/src/utils/path.ts @@ -1,5 +1,3 @@ export function isAbsolutePath(value: string): boolean { - return ( - value.startsWith("/") || value.startsWith("\\\\") || /^[A-Za-z]:[\\/]/.test(value) - ); + return value.startsWith("/") || value.startsWith("\\\\") || /^[A-Za-z]:[\\/]/.test(value); } diff --git a/packages/app/src/utils/sidebar-animation-state.ts b/packages/app/src/utils/sidebar-animation-state.ts index 9572b8975..32a25943a 100644 --- a/packages/app/src/utils/sidebar-animation-state.ts +++ b/packages/app/src/utils/sidebar-animation-state.ts @@ -17,8 +17,7 @@ interface SidebarAnimationTargets { export function shouldSyncSidebarAnimation(input: SidebarAnimationSyncInput): boolean { return ( - input.previousIsOpen !== input.nextIsOpen || - input.previousWindowWidth !== input.nextWindowWidth + input.previousIsOpen !== input.nextIsOpen || input.previousWindowWidth !== input.nextWindowWidth ); } diff --git a/packages/app/src/utils/sidebar-project-row-model.ts b/packages/app/src/utils/sidebar-project-row-model.ts index 4f899d788..bf0910c30 100644 --- a/packages/app/src/utils/sidebar-project-row-model.ts +++ b/packages/app/src/utils/sidebar-project-row-model.ts @@ -50,8 +50,7 @@ export function buildSidebarProjectRowModel(input: { }; } - const collapsible = - input.project.projectKind === "git" || input.project.workspaces.length > 1; + const collapsible = input.project.projectKind === "git" || input.project.workspaces.length > 1; return { kind: "project_section", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 336d3015a..5da470821 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -62,45 +62,39 @@ export function createCli(): Command { .option("--no-color", "disable colored output"); // Primary agent commands (top-level) - addJsonAndDaemonHostOptions( - addLsOptions(program.command("ls")), - ).action(withOutput(runLsCommand)); + addJsonAndDaemonHostOptions(addLsOptions(program.command("ls"))).action(withOutput(runLsCommand)); - addJsonAndDaemonHostOptions( - addRunOptions(program.command("run")), - ).action(withOutput(runRunCommand)); + addJsonAndDaemonHostOptions(addRunOptions(program.command("run"))).action( + withOutput(runRunCommand), + ); - addDaemonHostOption( - addAttachOptions(program.command("attach")), - ).action(runAttachCommand); + addDaemonHostOption(addAttachOptions(program.command("attach"))).action(runAttachCommand); - addDaemonHostOption( - addLogsOptions(program.command("logs")), - ).action(runLogsCommand); + addDaemonHostOption(addLogsOptions(program.command("logs"))).action(runLogsCommand); - addJsonAndDaemonHostOptions( - addStopOptions(program.command("stop")), - ).action(withOutput(runStopCommand)); + addJsonAndDaemonHostOptions(addStopOptions(program.command("stop"))).action( + withOutput(runStopCommand), + ); - addJsonAndDaemonHostOptions( - addDeleteOptions(program.command("delete")), - ).action(withOutput(runDeleteCommand)); + addJsonAndDaemonHostOptions(addDeleteOptions(program.command("delete"))).action( + withOutput(runDeleteCommand), + ); - addJsonAndDaemonHostOptions( - addSendOptions(program.command("send")), - ).action(withOutput(runSendCommand)); + addJsonAndDaemonHostOptions(addSendOptions(program.command("send"))).action( + withOutput(runSendCommand), + ); - addJsonAndDaemonHostOptions( - addInspectOptions(program.command("inspect")), - ).action(withOutput(runInspectCommand)); + addJsonAndDaemonHostOptions(addInspectOptions(program.command("inspect"))).action( + withOutput(runInspectCommand), + ); - addJsonAndDaemonHostOptions( - addWaitOptions(program.command("wait")), - ).action(withOutput(runWaitCommand)); + addJsonAndDaemonHostOptions(addWaitOptions(program.command("wait"))).action( + withOutput(runWaitCommand), + ); - addJsonAndDaemonHostOptions( - addArchiveOptions(program.command("archive")), - ).action(withOutput(runArchiveCommand)); + addJsonAndDaemonHostOptions(addArchiveOptions(program.command("archive"))).action( + withOutput(runArchiveCommand), + ); // Top-level local daemon shortcuts program.addCommand(onboardCommand()); diff --git a/packages/cli/src/commands/agent/archive.ts b/packages/cli/src/commands/agent/archive.ts index 6dc2a57a2..0219eed80 100644 --- a/packages/cli/src/commands/agent/archive.ts +++ b/packages/cli/src/commands/agent/archive.ts @@ -26,7 +26,7 @@ export const archiveSchema: OutputSchema = { export function addArchiveOptions(cmd: Command): Command { return cmd - .description('Archive an agent (soft-delete)') + .description("Archive an agent (soft-delete)") .argument("", "Agent ID, prefix, or name") .option("--force", "Force archive running agent (interrupts active run first)"); } diff --git a/packages/cli/src/commands/agent/index.ts b/packages/cli/src/commands/agent/index.ts index 329f0403d..9532e3cf4 100644 --- a/packages/cli/src/commands/agent/index.ts +++ b/packages/cli/src/commands/agent/index.ts @@ -22,41 +22,35 @@ export function createAgentCommand(): Command { const agent = new Command("agent").description("Manage agents (advanced operations)"); // Primary agent commands (same as top-level) - addJsonAndDaemonHostOptions( - addLsOptions(agent.command("ls")), - ).action(withOutput(runLsCommand)); + addJsonAndDaemonHostOptions(addLsOptions(agent.command("ls"))).action(withOutput(runLsCommand)); - addJsonAndDaemonHostOptions( - addRunOptions(agent.command("run")), - ).action(withOutput(runRunCommand)); + addJsonAndDaemonHostOptions(addRunOptions(agent.command("run"))).action( + withOutput(runRunCommand), + ); - addDaemonHostOption( - addAttachOptions(agent.command("attach")), - ).action(runAttachCommand); + addDaemonHostOption(addAttachOptions(agent.command("attach"))).action(runAttachCommand); - addDaemonHostOption( - addLogsOptions(agent.command("logs")), - ).action(runLogsCommand); + addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(runLogsCommand); - addJsonAndDaemonHostOptions( - addStopOptions(agent.command("stop")), - ).action(withOutput(runStopCommand)); + addJsonAndDaemonHostOptions(addStopOptions(agent.command("stop"))).action( + withOutput(runStopCommand), + ); - addJsonAndDaemonHostOptions( - addDeleteOptions(agent.command("delete")), - ).action(withOutput(runDeleteCommand)); + addJsonAndDaemonHostOptions(addDeleteOptions(agent.command("delete"))).action( + withOutput(runDeleteCommand), + ); - addJsonAndDaemonHostOptions( - addSendOptions(agent.command("send")), - ).action(withOutput(runSendCommand)); + addJsonAndDaemonHostOptions(addSendOptions(agent.command("send"))).action( + withOutput(runSendCommand), + ); - addJsonAndDaemonHostOptions( - addInspectOptions(agent.command("inspect")), - ).action(withOutput(runInspectCommand)); + addJsonAndDaemonHostOptions(addInspectOptions(agent.command("inspect"))).action( + withOutput(runInspectCommand), + ); - addJsonAndDaemonHostOptions( - addWaitOptions(agent.command("wait")), - ).action(withOutput(runWaitCommand)); + addJsonAndDaemonHostOptions(addWaitOptions(agent.command("wait"))).action( + withOutput(runWaitCommand), + ); // Advanced agent commands (less common operations) addJsonAndDaemonHostOptions( @@ -68,9 +62,9 @@ export function createAgentCommand(): Command { .option("--list", "List available modes for this agent"), ).action(withOutput(runModeCommand)); - addJsonAndDaemonHostOptions( - addArchiveOptions(agent.command("archive")), - ).action(withOutput(runArchiveCommand)); + addJsonAndDaemonHostOptions(addArchiveOptions(agent.command("archive"))).action( + withOutput(runArchiveCommand), + ); addJsonAndDaemonHostOptions( agent diff --git a/packages/cli/src/commands/chat/index.ts b/packages/cli/src/commands/chat/index.ts index 9b6f363e9..197b5844e 100644 --- a/packages/cli/src/commands/chat/index.ts +++ b/packages/cli/src/commands/chat/index.ts @@ -20,9 +20,9 @@ export function createChatCommand(): Command { .option("--purpose ", "Room purpose/description"), ).action(withOutput(runCreateCommand)); - addJsonAndDaemonHostOptions( - chat.command("ls").description("List chat rooms"), - ).action(withOutput(runLsCommand)); + addJsonAndDaemonHostOptions(chat.command("ls").description("List chat rooms")).action( + withOutput(runLsCommand), + ); addJsonAndDaemonHostOptions( chat diff --git a/packages/cli/src/commands/chat/post.ts b/packages/cli/src/commands/chat/post.ts index 32ff0f972..e46049d82 100644 --- a/packages/cli/src/commands/chat/post.ts +++ b/packages/cli/src/commands/chat/post.ts @@ -27,7 +27,9 @@ export async function runPostCommand( authorAgentId: resolveChatAuthorAgentId(), replyToMessageId: options.replyTo, }); - const [message] = await attachAgentNamesToMessages(client, [toChatMessageRow(payload.message!)]); + const [message] = await attachAgentNamesToMessages(client, [ + toChatMessageRow(payload.message!), + ]); return { type: "single", data: message!, diff --git a/packages/cli/src/commands/chat/schema.ts b/packages/cli/src/commands/chat/schema.ts index aa1209d1f..269c8d998 100644 --- a/packages/cli/src/commands/chat/schema.ts +++ b/packages/cli/src/commands/chat/schema.ts @@ -60,7 +60,9 @@ function renderChatMessageBlock(message: ChatMessageRow): string { const authorLabel = message.authorName ? `${message.authorName} (${message.author})` : message.author; - const lines = [`┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`]; + const lines = [ + `┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`, + ]; if (message.replyTo !== "-") { lines.push(`│ reply-to: msg ${message.replyTo}`); diff --git a/packages/cli/src/commands/daemon/local-daemon.ts b/packages/cli/src/commands/daemon/local-daemon.ts index 957ca64ce..6568ee3bd 100644 --- a/packages/cli/src/commands/daemon/local-daemon.ts +++ b/packages/cli/src/commands/daemon/local-daemon.ts @@ -125,12 +125,7 @@ function resolveDaemonRunnerEntry(): string { try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string }; if (packageJson.name === "@getpaseo/server") { - const distRunner = path.join( - currentDir, - "dist", - "scripts", - "supervisor-entrypoint.js", - ); + const distRunner = path.join(currentDir, "dist", "scripts", "supervisor-entrypoint.js"); if (existsSync(distRunner)) { return distRunner; } diff --git a/packages/cli/src/commands/daemon/runtime-toolchain.ts b/packages/cli/src/commands/daemon/runtime-toolchain.ts index 6b4184761..3b41d020f 100644 --- a/packages/cli/src/commands/daemon/runtime-toolchain.ts +++ b/packages/cli/src/commands/daemon/runtime-toolchain.ts @@ -32,10 +32,15 @@ function resolveNodePathFromPidUnix(pid: number): NodePathFromPidResult { } const resolved = result.stdout.trim(); - return resolved ? { nodePath: resolved } : { nodePath: null, error: "ps returned an empty command path" }; + return resolved + ? { nodePath: resolved } + : { nodePath: null, error: "ps returned an empty command path" }; } -function runProcessProbe(command: string, args: string[]): { +function runProcessProbe( + command: string, + args: string[], +): { resolved: string | null; error?: string; } { @@ -52,16 +57,25 @@ function runProcessProbe(command: string, args: string[]): { const details = result.stderr?.trim(); return { resolved: null, - error: details ? `${command} failed: ${details}` : `${command} exited with code ${result.status ?? 1}`, + error: details + ? `${command} failed: ${details}` + : `${command} exited with code ${result.status ?? 1}`, }; } const resolved = result.stdout.trim(); - return resolved ? { resolved } : { resolved: null, error: `${command} returned no executable path` }; + return resolved + ? { resolved } + : { resolved: null, error: `${command} returned no executable path` }; } function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult { - const probes: Array<{ label: string; command: string; args: string[]; parseValue?: (stdout: string) => string | null }> = [ + const probes: Array<{ + label: string; + command: string; + args: string[]; + parseValue?: (stdout: string) => string | null; + }> = [ { label: "powershell-cim", command: "powershell", @@ -103,7 +117,10 @@ function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult { } } - return { nodePath: null, error: errors.join("; ") || "could not resolve executable path from PID" }; + return { + nodePath: null, + error: errors.join("; ") || "could not resolve executable path from PID", + }; } export function resolveNodePathFromPid(pid: number): NodePathFromPidResult { diff --git a/packages/cli/src/commands/loop/index.ts b/packages/cli/src/commands/loop/index.ts index 64e975517..95359ce59 100644 --- a/packages/cli/src/commands/loop/index.ts +++ b/packages/cli/src/commands/loop/index.ts @@ -10,25 +10,23 @@ import { addLoopStopOptions, runLoopStopCommand } from "./stop.js"; export function createLoopCommand(): Command { const loop = new Command("loop").description("Run iterative worker loops"); - addJsonAndDaemonHostOptions( - addLoopRunOptions(loop.command("run")), - ).action(withOutput(runLoopRunCommand)); + addJsonAndDaemonHostOptions(addLoopRunOptions(loop.command("run"))).action( + withOutput(runLoopRunCommand), + ); - addJsonAndDaemonHostOptions( - addLoopLsOptions(loop.command("ls")), - ).action(withOutput(runLoopLsCommand)); + addJsonAndDaemonHostOptions(addLoopLsOptions(loop.command("ls"))).action( + withOutput(runLoopLsCommand), + ); - addJsonAndDaemonHostOptions( - addLoopInspectOptions(loop.command("inspect")), - ).action(withOutput(runLoopInspectCommand)); + addJsonAndDaemonHostOptions(addLoopInspectOptions(loop.command("inspect"))).action( + withOutput(runLoopInspectCommand), + ); - addDaemonHostOption( - addLoopLogsOptions(loop.command("logs")), - ).action(runLoopLogsCommand); + addDaemonHostOption(addLoopLogsOptions(loop.command("logs"))).action(runLoopLogsCommand); - addJsonAndDaemonHostOptions( - addLoopStopOptions(loop.command("stop")), - ).action(withOutput(runLoopStopCommand)); + addJsonAndDaemonHostOptions(addLoopStopOptions(loop.command("stop"))).action( + withOutput(runLoopStopCommand), + ); return loop; } diff --git a/packages/cli/src/commands/loop/inspect.ts b/packages/cli/src/commands/loop/inspect.ts index 8f571a0ee..a2b9812be 100644 --- a/packages/cli/src/commands/loop/inspect.ts +++ b/packages/cli/src/commands/loop/inspect.ts @@ -1,11 +1,6 @@ import { Command } from "commander"; import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { - CommandOptions, - CommandError, - OutputSchema, - ListResult, -} from "../../output/index.js"; +import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js"; import type { LoopDaemonClient, LoopRecord } from "./types.js"; interface InspectRow { @@ -44,10 +39,16 @@ function toRows(loop: LoopRecord): InspectRow[] { { key: "VerifierModel", value: loop.verifierModel ?? "null" }, { key: "Prompt", value: loop.prompt }, { key: "VerifyPrompt", value: loop.verifyPrompt ?? "null" }, - { key: "VerifyChecks", value: loop.verifyChecks.length > 0 ? loop.verifyChecks.join(" | ") : "[]" }, + { + key: "VerifyChecks", + value: loop.verifyChecks.length > 0 ? loop.verifyChecks.join(" | ") : "[]", + }, { key: "Archive", value: String(loop.archive) }, { key: "SleepMs", value: String(loop.sleepMs) }, - { key: "MaxIterations", value: loop.maxIterations === null ? "null" : String(loop.maxIterations) }, + { + key: "MaxIterations", + value: loop.maxIterations === null ? "null" : String(loop.maxIterations), + }, { key: "MaxTimeMs", value: loop.maxTimeMs === null ? "null" : String(loop.maxTimeMs) }, { key: "CreatedAt", value: loop.createdAt }, { key: "UpdatedAt", value: loop.updatedAt }, diff --git a/packages/cli/src/commands/loop/ls.ts b/packages/cli/src/commands/loop/ls.ts index 3c0051244..95d068586 100644 --- a/packages/cli/src/commands/loop/ls.ts +++ b/packages/cli/src/commands/loop/ls.ts @@ -1,11 +1,6 @@ import { Command } from "commander"; import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { - CommandOptions, - CommandError, - OutputSchema, - ListResult, -} from "../../output/index.js"; +import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js"; import type { LoopDaemonClient, LoopListItem } from "./types.js"; interface LoopListRow { diff --git a/packages/cli/src/commands/schedule/delete.ts b/packages/cli/src/commands/schedule/delete.ts index 05ea5e933..f1f4ba2ca 100644 --- a/packages/cli/src/commands/schedule/delete.ts +++ b/packages/cli/src/commands/schedule/delete.ts @@ -1,7 +1,11 @@ import type { Command } from "commander"; import type { SingleResult } from "../../output/index.js"; import type { OutputSchema } from "../../output/index.js"; -import { connectScheduleClient, toScheduleCommandError, type ScheduleCommandOptions } from "./shared.js"; +import { + connectScheduleClient, + toScheduleCommandError, + type ScheduleCommandOptions, +} from "./shared.js"; interface ScheduleDeleteRow { id: string; diff --git a/packages/cli/src/commands/schedule/index.ts b/packages/cli/src/commands/schedule/index.ts index 1a02d683b..ba85c57cf 100644 --- a/packages/cli/src/commands/schedule/index.ts +++ b/packages/cli/src/commands/schedule/index.ts @@ -25,15 +25,12 @@ export function createScheduleCommand(): Command { .option("--expires-in ", "Time to live for the schedule"), ).action(withOutput(runCreateCommand)); - addJsonAndDaemonHostOptions( - schedule.command("ls").description("List schedules"), - ).action(withOutput(runLsCommand)); + addJsonAndDaemonHostOptions(schedule.command("ls").description("List schedules")).action( + withOutput(runLsCommand), + ); addJsonAndDaemonHostOptions( - schedule - .command("inspect") - .description("Inspect a schedule") - .argument("", "Schedule ID"), + schedule.command("inspect").description("Inspect a schedule").argument("", "Schedule ID"), ).action(withOutput(runInspectCommand)); addJsonAndDaemonHostOptions( @@ -44,10 +41,7 @@ export function createScheduleCommand(): Command { ).action(withOutput(runLogsCommand)); addJsonAndDaemonHostOptions( - schedule - .command("pause") - .description("Pause a schedule") - .argument("", "Schedule ID"), + schedule.command("pause").description("Pause a schedule").argument("", "Schedule ID"), ).action(withOutput(runPauseCommand)); addJsonAndDaemonHostOptions( @@ -58,10 +52,7 @@ export function createScheduleCommand(): Command { ).action(withOutput(runResumeCommand)); addJsonAndDaemonHostOptions( - schedule - .command("delete") - .description("Delete a schedule") - .argument("", "Schedule ID"), + schedule.command("delete").description("Delete a schedule").argument("", "Schedule ID"), ).action(withOutput(runDeleteCommand)); return schedule; diff --git a/packages/cli/src/commands/schedule/inspect.ts b/packages/cli/src/commands/schedule/inspect.ts index de968e6a4..d9044382f 100644 --- a/packages/cli/src/commands/schedule/inspect.ts +++ b/packages/cli/src/commands/schedule/inspect.ts @@ -5,7 +5,11 @@ import { createScheduleInspectSchema, type ScheduleInspectRow, } from "./schema.js"; -import { connectScheduleClient, toScheduleCommandError, type ScheduleCommandOptions } from "./shared.js"; +import { + connectScheduleClient, + toScheduleCommandError, + type ScheduleCommandOptions, +} from "./shared.js"; export async function runInspectCommand( id: string, diff --git a/packages/cli/src/commands/schedule/logs.ts b/packages/cli/src/commands/schedule/logs.ts index d10251063..b411822d9 100644 --- a/packages/cli/src/commands/schedule/logs.ts +++ b/packages/cli/src/commands/schedule/logs.ts @@ -1,7 +1,11 @@ import type { Command } from "commander"; import type { ListResult } from "../../output/index.js"; import { scheduleLogSchema, toScheduleLogRow, type ScheduleLogRow } from "./schema.js"; -import { connectScheduleClient, toScheduleCommandError, type ScheduleCommandOptions } from "./shared.js"; +import { + connectScheduleClient, + toScheduleCommandError, + type ScheduleCommandOptions, +} from "./shared.js"; export async function runLogsCommand( id: string, diff --git a/packages/cli/src/commands/schedule/schema.ts b/packages/cli/src/commands/schedule/schema.ts index cc64b7985..bf3383d73 100644 --- a/packages/cli/src/commands/schedule/schema.ts +++ b/packages/cli/src/commands/schedule/schema.ts @@ -19,7 +19,9 @@ export interface ScheduleInspectRow { value: string; } -export function createScheduleInspectSchema(record: ScheduleRecord): OutputSchema { +export function createScheduleInspectSchema( + record: ScheduleRecord, +): OutputSchema { return { idField: "key", columns: [ diff --git a/packages/cli/src/commands/schedule/shared.ts b/packages/cli/src/commands/schedule/shared.ts index 7946c7ec4..a9d3c93e5 100644 --- a/packages/cli/src/commands/schedule/shared.ts +++ b/packages/cli/src/commands/schedule/shared.ts @@ -33,11 +33,7 @@ export async function connectScheduleClient( } } -export function toScheduleCommandError( - code: string, - action: string, - error: unknown, -): CommandError { +export function toScheduleCommandError(code: string, action: string, error: unknown): CommandError { if (error && typeof error === "object" && "code" in error) { return error as CommandError; } diff --git a/packages/cli/src/commands/terminal/ls.ts b/packages/cli/src/commands/terminal/ls.ts index 7f47bee18..b3a6e518d 100644 --- a/packages/cli/src/commands/terminal/ls.ts +++ b/packages/cli/src/commands/terminal/ls.ts @@ -20,7 +20,8 @@ export async function runLsCommand( const cwd = options.all ? undefined : (options.cwd ?? process.cwd()); try { - const payload = cwd === undefined ? await client.listTerminals() : await client.listTerminals(cwd); + const payload = + cwd === undefined ? await client.listTerminals() : await client.listTerminals(cwd); return { type: "list", data: payload.terminals.map((terminal) => toTerminalRow(terminal, payload.cwd ?? cwd)), diff --git a/packages/cli/src/utils/paths.ts b/packages/cli/src/utils/paths.ts index e8339e1e6..7e6e27a4e 100644 --- a/packages/cli/src/utils/paths.ts +++ b/packages/cli/src/utils/paths.ts @@ -21,7 +21,6 @@ export function isSameOrDescendantPath(basePath: string, candidatePath: string): } return ( - normalizedCandidate === normalizedBase || - normalizedCandidate.startsWith(normalizedBase + "/") + normalizedCandidate === normalizedBase || normalizedCandidate.startsWith(normalizedBase + "/") ); } diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index dda23cc66..0eb271166 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -53,9 +53,7 @@ let claudeModelsFromJson: ProviderModel[] = []; const ctx = await createE2ETestContext({ timeout: 120000 }); -async function runProviderModelsJson( - provider: string, -): Promise { +async function runProviderModelsJson(provider: string): Promise { const transientNeedles = ["transport closed", "timed out", "timeout", "socket", "econn"]; for (let attempt = 1; attempt <= 3; attempt++) { diff --git a/packages/cli/tests/22-daemon-stop-supervisor.test.ts b/packages/cli/tests/22-daemon-stop-supervisor.test.ts index c706a8ea8..b98bcf05f 100644 --- a/packages/cli/tests/22-daemon-stop-supervisor.test.ts +++ b/packages/cli/tests/22-daemon-stop-supervisor.test.ts @@ -122,22 +122,18 @@ let recentSupervisorLogs = ""; try { console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME"); - supervisorProcess = spawn( - "npx", - ["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"], - { - cwd: cliRoot, - env: { - ...process.env, - ...testEnv, - PASEO_HOME: paseoHome, - PASEO_LISTEN: `127.0.0.1:${port}`, - PASEO_RELAY_ENABLED: "false", - CI: "true", - }, - stdio: ["ignore", "pipe", "pipe"], + supervisorProcess = spawn("npx", ["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"], { + cwd: cliRoot, + env: { + ...process.env, + ...testEnv, + PASEO_HOME: paseoHome, + PASEO_LISTEN: `127.0.0.1:${port}`, + PASEO_RELAY_ENABLED: "false", + CI: "true", }, - ); + stdio: ["ignore", "pipe", "pipe"], + }); supervisorProcess.stdout?.on("data", (chunk) => { recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000); @@ -165,8 +161,7 @@ try { const command = readProcessCommand(daemonPid); assert(command !== null, "pid lock pid should resolve to a running process command"); assert( - command.includes("supervisor-entrypoint.ts") || - command.includes("supervisor-entrypoint.js"), + command.includes("supervisor-entrypoint.ts") || command.includes("supervisor-entrypoint.js"), `pid lock pid should be supervisor-entrypoint process, got: ${command}`, ); console.log(`✓ dev daemon started with daemon pid ${daemonPid}\n`); diff --git a/packages/cli/tests/23-daemon-sigint-supervisor.test.ts b/packages/cli/tests/23-daemon-sigint-supervisor.test.ts index b7cbffb7c..44866f395 100644 --- a/packages/cli/tests/23-daemon-sigint-supervisor.test.ts +++ b/packages/cli/tests/23-daemon-sigint-supervisor.test.ts @@ -133,23 +133,19 @@ let recentSupervisorLogs = ""; try { console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME"); - supervisorProcess = spawn( - "npx", - ["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"], - { - cwd: cliRoot, - env: { - ...process.env, - ...testEnv, - PASEO_HOME: paseoHome, - PASEO_LISTEN: `127.0.0.1:${port}`, - PASEO_RELAY_ENABLED: "false", - CI: "true", - }, - stdio: ["ignore", "pipe", "pipe"], - detached: process.platform !== "win32", + supervisorProcess = spawn("npx", ["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"], { + cwd: cliRoot, + env: { + ...process.env, + ...testEnv, + PASEO_HOME: paseoHome, + PASEO_LISTEN: `127.0.0.1:${port}`, + PASEO_RELAY_ENABLED: "false", + CI: "true", }, - ); + stdio: ["ignore", "pipe", "pipe"], + detached: process.platform !== "win32", + }); supervisorProcess.stdout?.on("data", (chunk) => { recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000); diff --git a/packages/cli/tests/25-daemon-restart-supervisor.test.ts b/packages/cli/tests/25-daemon-restart-supervisor.test.ts index c1361dca4..e5a5f4ff2 100644 --- a/packages/cli/tests/25-daemon-restart-supervisor.test.ts +++ b/packages/cli/tests/25-daemon-restart-supervisor.test.ts @@ -124,22 +124,18 @@ let recentSupervisorLogs = ""; try { console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME"); - supervisorProcess = spawn( - "npx", - ["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"], - { - cwd: cliRoot, - env: { - ...process.env, - ...testEnv, - PASEO_HOME: paseoHome, - PASEO_LISTEN: host, - PASEO_RELAY_ENABLED: "false", - CI: "true", - }, - stdio: ["ignore", "pipe", "pipe"], + supervisorProcess = spawn("npx", ["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"], { + cwd: cliRoot, + env: { + ...process.env, + ...testEnv, + PASEO_HOME: paseoHome, + PASEO_LISTEN: host, + PASEO_RELAY_ENABLED: "false", + CI: "true", }, - ); + stdio: ["ignore", "pipe", "pipe"], + }); supervisorProcess.stdout?.on("data", (chunk) => { recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000); diff --git a/packages/cli/tests/30-chat.test.ts b/packages/cli/tests/30-chat.test.ts index 11df5a603..6b741d480 100644 --- a/packages/cli/tests/30-chat.test.ts +++ b/packages/cli/tests/30-chat.test.ts @@ -27,12 +27,7 @@ try { { console.log("Test 2: chat post/read/wait work"); - const posted = await ctx.paseo([ - "chat", - "post", - "coord-room", - "first message for @agent-1", - ], { + const posted = await ctx.paseo(["chat", "post", "coord-room", "first message for @agent-1"], { env: { PASEO_AGENT_ID: "00000000-0000-4000-8000-000000000111" }, }); assert.strictEqual(posted.exitCode, 0, posted.stderr); diff --git a/packages/cli/tests/31-loop-schedule.test.ts b/packages/cli/tests/31-loop-schedule.test.ts index 2ccc00035..b3098b8df 100644 --- a/packages/cli/tests/31-loop-schedule.test.ts +++ b/packages/cli/tests/31-loop-schedule.test.ts @@ -28,7 +28,10 @@ try { assert.strictEqual(listed.exitCode, 0, listed.stderr); const listedJson = JSON.parse(listed.stdout); assert(Array.isArray(listedJson), listed.stdout); - assert(listedJson.some((item: { id: string }) => item.id === createdJson.id), listed.stdout); + assert( + listedJson.some((item: { id: string }) => item.id === createdJson.id), + listed.stdout, + ); const inspected = await ctx.paseo(["schedule", "inspect", createdJson.id, "--json"]); assert.strictEqual(inspected.exitCode, 0, inspected.stderr); @@ -53,7 +56,16 @@ try { { console.log("Test 2: loop run/ls/inspect/logs/stop work"); const run = await ctx.paseo( - ["loop", "run", "Return any response", "--name", "smoke-loop", "--verify-check", "true", "--json"], + [ + "loop", + "run", + "Return any response", + "--name", + "smoke-loop", + "--verify-check", + "true", + "--json", + ], { timeout: 30000 }, ); assert.strictEqual(run.exitCode, 0, run.stderr); @@ -64,7 +76,10 @@ try { assert.strictEqual(listed.exitCode, 0, listed.stderr); const listedJson = JSON.parse(listed.stdout); assert(Array.isArray(listedJson), listed.stdout); - assert(listedJson.some((item: { id: string }) => item.id === runJson.id), listed.stdout); + assert( + listedJson.some((item: { id: string }) => item.id === runJson.id), + listed.stdout, + ); let status = "running"; for (let attempt = 0; attempt < 40; attempt += 1) { diff --git a/packages/cli/tests/32-open-project.test.ts b/packages/cli/tests/32-open-project.test.ts index b08d0f336..3ab2f5e1f 100644 --- a/packages/cli/tests/32-open-project.test.ts +++ b/packages/cli/tests/32-open-project.test.ts @@ -4,16 +4,12 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { - classifyInvocation, - isExistingDirectory, - isPathLikeArg, -} from "../src/classify.ts"; +import { classifyInvocation, isExistingDirectory, isPathLikeArg } from "../src/classify.ts"; import { openDesktopWithProject } from "../src/commands/open.ts"; console.log("📋 Phase 32: Open Project CLI Tests\n"); - console.log(" Testing path-like detection exports..."); +console.log(" Testing path-like detection exports..."); assert.equal(isPathLikeArg("."), true); assert.equal(isPathLikeArg("./app"), true); assert.equal(isPathLikeArg("/tmp/app"), true); diff --git a/packages/cli/tests/helpers/test-daemon.ts b/packages/cli/tests/helpers/test-daemon.ts index eebb941b6..144081fbe 100644 --- a/packages/cli/tests/helpers/test-daemon.ts +++ b/packages/cli/tests/helpers/test-daemon.ts @@ -411,8 +411,10 @@ export async function createE2ETestContext(options?: { timeout?: number }): Prom > { const ctx = await startTestDaemon({ timeout: options?.timeout }); - const paseo = (args: string[], opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv }) => - runPaseoCli(ctx, args, opts); + const paseo = ( + args: string[], + opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv }, + ) => runPaseoCli(ctx, args, opts); return { ...ctx, diff --git a/packages/desktop/scripts/after-pack.js b/packages/desktop/scripts/after-pack.js index 258393187..618ec2ae9 100644 --- a/packages/desktop/scripts/after-pack.js +++ b/packages/desktop/scripts/after-pack.js @@ -76,7 +76,11 @@ function pruneSharpLibvips(nodeModules, platform, arch) { if (!fs.existsSync(imgDir)) return; for (const entry of fs.readdirSync(imgDir)) { - if (entry.startsWith("sharp-") && entry !== prefix && !entry.startsWith(`sharp-${platform}-${arch}`)) { + if ( + entry.startsWith("sharp-") && + entry !== prefix && + !entry.startsWith(`sharp-${platform}-${arch}`) + ) { rmSafe(path.join(imgDir, entry)); } } diff --git a/packages/desktop/scripts/verify-electron-cdp.mjs b/packages/desktop/scripts/verify-electron-cdp.mjs index 029c6b1ab..15165640f 100644 --- a/packages/desktop/scripts/verify-electron-cdp.mjs +++ b/packages/desktop/scripts/verify-electron-cdp.mjs @@ -505,8 +505,7 @@ async function main() { dragRegionCheck.candidate.explicitNoDragInteractive.length > 0, details: { candidate: dragRegionCheck.candidate, - explicitNoDragInteractive: - dragRegionCheck.candidate?.explicitNoDragInteractive ?? [], + explicitNoDragInteractive: dragRegionCheck.candidate?.explicitNoDragInteractive ?? [], }, screenshot: dragScreenshot, }; diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index a29cd8ae3..f38a73796 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -149,7 +149,6 @@ function logDesktopDaemonLifecycle(message: string, details?: Record { if (current.status === "running") { const appVersion = normalizeVersion(resolveDesktopAppVersion()); const daemonVersion = normalizeVersion(current.version); - if ( - current.desktopManaged && - appVersion && - daemonVersion && - appVersion !== daemonVersion - ) { + if (current.desktopManaged && appVersion && daemonVersion && appVersion !== daemonVersion) { logDesktopDaemonLifecycle("daemon version mismatch, restarting", { appVersion, daemonVersion, @@ -266,15 +260,11 @@ async function startDaemon(): Promise { args: invocation.args, }); - const child: ChildProcess = spawn( - invocation.command, - invocation.args, - { - detached: true, - env: { ...invocation.env, PASEO_DESKTOP_MANAGED: "1" }, - stdio: ["ignore", "ignore", "ignore"], - }, - ); + const child: ChildProcess = spawn(invocation.command, invocation.args, { + detached: true, + env: { ...invocation.env, PASEO_DESKTOP_MANAGED: "1" }, + stdio: ["ignore", "ignore", "ignore"], + }); logDesktopDaemonLifecycle("detached spawn returned", { childPid: child.pid ?? null, diff --git a/packages/desktop/src/daemon/node-entrypoint-launcher.test.ts b/packages/desktop/src/daemon/node-entrypoint-launcher.test.ts index 76d95f8e8..93e3fb02f 100644 --- a/packages/desktop/src/daemon/node-entrypoint-launcher.test.ts +++ b/packages/desktop/src/daemon/node-entrypoint-launcher.test.ts @@ -65,11 +65,7 @@ describe("node-entrypoint-launcher", () => { it("passes --open-project through as a normal CLI arg", () => { expect( parseCliPassthroughArgsFromArgv({ - argv: [ - "/Applications/Paseo.app/Contents/MacOS/Paseo", - "--open-project", - "/tmp/project", - ], + argv: ["/Applications/Paseo.app/Contents/MacOS/Paseo", "--open-project", "/tmp/project"], isDefaultApp: false, forceCli: false, }), @@ -93,7 +89,8 @@ describe("node-entrypoint-launcher", () => { createNodeEntrypointInvocation({ execPath: "/Applications/Paseo.app/Contents/MacOS/Paseo", isPackaged: true, - packagedRunnerPath: "/Applications/Paseo.app/Contents/Resources/app.asar/dist/daemon/node-entrypoint-runner.js", + packagedRunnerPath: + "/Applications/Paseo.app/Contents/Resources/app.asar/dist/daemon/node-entrypoint-runner.js", entrypoint: CLI_ENTRYPOINT, argvMode: "node-script", args: ["ls", "--json"], @@ -142,7 +139,8 @@ describe("node-entrypoint-launcher", () => { createNodeEntrypointInvocation({ execPath: "/Applications/Paseo.app/Contents/MacOS/Paseo", isPackaged: true, - packagedRunnerPath: "/Applications/Paseo.app/Contents/Resources/app.asar/dist/daemon/node-entrypoint-runner.js", + packagedRunnerPath: + "/Applications/Paseo.app/Contents/Resources/app.asar/dist/daemon/node-entrypoint-runner.js", entrypoint: CLI_ENTRYPOINT, argvMode: "node-script", args: ["--dev"], diff --git a/packages/desktop/src/daemon/node-entrypoint-launcher.ts b/packages/desktop/src/daemon/node-entrypoint-launcher.ts index e223b57e0..b2626efcc 100644 --- a/packages/desktop/src/daemon/node-entrypoint-launcher.ts +++ b/packages/desktop/src/daemon/node-entrypoint-launcher.ts @@ -70,7 +70,13 @@ export function createNodeEntrypointInvocation( return { command: input.execPath, - args: ["--disable-warning=DEP0040", input.packagedRunnerPath, input.argvMode, input.entrypoint.entryPath, ...input.args], + args: [ + "--disable-warning=DEP0040", + input.packagedRunnerPath, + input.argvMode, + input.entrypoint.entryPath, + ...input.args, + ], env, }; } diff --git a/packages/desktop/src/daemon/node-entrypoint-runner.ts b/packages/desktop/src/daemon/node-entrypoint-runner.ts index 0eac149a9..e90579ae2 100644 --- a/packages/desktop/src/daemon/node-entrypoint-runner.ts +++ b/packages/desktop/src/daemon/node-entrypoint-runner.ts @@ -10,7 +10,9 @@ async function main(): Promise { } process.argv = - argvMode === "bare" ? [process.argv[0] ?? "node", ...args] : [process.argv[0] ?? "node", entryPath, ...args]; + argvMode === "bare" + ? [process.argv[0] ?? "node", ...args] + : [process.argv[0] ?? "node", entryPath, ...args]; await import(pathToFileURL(entryPath).href); } diff --git a/packages/desktop/src/daemon/runtime-paths.ts b/packages/desktop/src/daemon/runtime-paths.ts index 88615f942..2be9d6f35 100644 --- a/packages/desktop/src/daemon/runtime-paths.ts +++ b/packages/desktop/src/daemon/runtime-paths.ts @@ -76,7 +76,13 @@ function resolvePackagedAsarPath(): string { } function resolvePackagedNodeEntrypointRunnerPath(): string { - return path.join(process.resourcesPath, "app.asar.unpacked", "dist", "daemon", "node-entrypoint-runner.js"); + return path.join( + process.resourcesPath, + "app.asar.unpacked", + "dist", + "daemon", + "node-entrypoint-runner.js", + ); } function assertPathExists(input: { label: string; filePath: string }): string { @@ -115,12 +121,7 @@ export function resolveDaemonRunnerEntrypoint(): NodeEntrypointSpec { } const serverPackage = resolveServerPackageInfo(); - const distRunner = path.join( - serverPackage.root, - "dist", - "scripts", - "supervisor-entrypoint.js", - ); + const distRunner = path.join(serverPackage.root, "dist", "scripts", "supervisor-entrypoint.js"); if (existsSync(distRunner)) { return { entryPath: distRunner, @@ -282,7 +283,9 @@ export async function runCliTextCommand(args: string[]): Promise { if (result.exitCode !== 0) { const stderr = result.stderr.trim(); - throw new Error(stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.exitCode}`); + throw new Error( + stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.exitCode}`, + ); } return result.stdout.trimEnd(); @@ -296,7 +299,9 @@ export async function runCliJsonCommand(args: string[]): Promise { if (result.exitCode !== 0) { const stderr = result.stderr.trim(); - throw new Error(stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.exitCode}`); + throw new Error( + stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.exitCode}`, + ); } const stdout = result.stdout.trim(); diff --git a/packages/desktop/src/integrations/integrations-manager.ts b/packages/desktop/src/integrations/integrations-manager.ts index b70894b71..62ff30bd7 100644 --- a/packages/desktop/src/integrations/integrations-manager.ts +++ b/packages/desktop/src/integrations/integrations-manager.ts @@ -225,13 +225,21 @@ export async function getCliInstallStatus(): Promise { // Skills Installation // --------------------------------------------------------------------------- -async function copySkillFile(sourceFile: string, destDir: string, skillName: string): Promise { +async function copySkillFile( + sourceFile: string, + destDir: string, + skillName: string, +): Promise { const destSkillDir = path.join(destDir, skillName); await fs.mkdir(destSkillDir, { recursive: true }); await fs.copyFile(sourceFile, path.join(destSkillDir, "SKILL.md")); } -async function symlinkSkillDir(skillName: string, targetDir: string, linkParentDir: string): Promise { +async function symlinkSkillDir( + skillName: string, + targetDir: string, + linkParentDir: string, +): Promise { await fs.mkdir(linkParentDir, { recursive: true }); const target = path.join(targetDir, skillName); const linkPath = path.join(linkParentDir, skillName); diff --git a/packages/desktop/src/open-project-routing.test.ts b/packages/desktop/src/open-project-routing.test.ts index 33d004844..8cced8763 100644 --- a/packages/desktop/src/open-project-routing.test.ts +++ b/packages/desktop/src/open-project-routing.test.ts @@ -57,11 +57,7 @@ describe("open-project-routing", () => { expect( parseOpenProjectPathFromArgv({ - argv: [ - "/Applications/Paseo.app/Contents/MacOS/Paseo", - "--open-project", - projectPath, - ], + argv: ["/Applications/Paseo.app/Contents/MacOS/Paseo", "--open-project", projectPath], isDefaultApp: false, }), ).toBe(projectPath); diff --git a/packages/desktop/src/open-project-routing.ts b/packages/desktop/src/open-project-routing.ts index 12ab75f16..6b51f722a 100644 --- a/packages/desktop/src/open-project-routing.ts +++ b/packages/desktop/src/open-project-routing.ts @@ -22,9 +22,7 @@ export function parseOpenProjectPathFromArgv(input: { }): string | null { const effectiveArgs = input.argv .slice(input.isDefaultApp ? 2 : 1) - .filter( - (arg) => !OPEN_PROJECT_IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix)), - ); + .filter((arg) => !OPEN_PROJECT_IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix))); const positionalProjectPath = effectiveArgs.find( (arg) => !arg.startsWith("-") && isExistingDirectoryAbsolutePath(arg), diff --git a/packages/desktop/src/window/window-manager.ts b/packages/desktop/src/window/window-manager.ts index 583e9a24d..329a67d2a 100644 --- a/packages/desktop/src/window/window-manager.ts +++ b/packages/desktop/src/window/window-manager.ts @@ -94,7 +94,9 @@ function readOverlayColor(input: unknown): string | null { return input; } -export function readWindowControlsOverlayUpdate(input: unknown): WindowControlsOverlayUpdate | null { +export function readWindowControlsOverlayUpdate( + input: unknown, +): WindowControlsOverlayUpdate | null { if (!input || typeof input !== "object") { return null; } diff --git a/packages/server/scripts/dev-runner.ts b/packages/server/scripts/dev-runner.ts index 581b04ebb..c8d0d87e6 100644 --- a/packages/server/scripts/dev-runner.ts +++ b/packages/server/scripts/dev-runner.ts @@ -10,7 +10,17 @@ dotenv.config({ const daemonRunnerEntry = fileURLToPath(new URL("./supervisor-entrypoint.ts", import.meta.url)); const result = spawnSync( process.execPath, - ["--inspect", "--heapsnapshot-near-heap-limit=3", "--max-old-space-size=3072", "--report-on-fatalerror", "--report-directory=/tmp/paseo-reports", ...process.execArgv, daemonRunnerEntry, "--dev", ...process.argv.slice(2)], + [ + "--inspect", + "--heapsnapshot-near-heap-limit=3", + "--max-old-space-size=3072", + "--report-on-fatalerror", + "--report-directory=/tmp/paseo-reports", + ...process.execArgv, + daemonRunnerEntry, + "--dev", + ...process.argv.slice(2), + ], { stdio: "inherit", env: process.env, diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index 41fd9911d..6c2df9dcb 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -263,10 +263,7 @@ type ChatCreatePayload = Extract< SessionOutboundMessage, { type: "chat/create/response" } >["payload"]; -type ChatListPayload = Extract< - SessionOutboundMessage, - { type: "chat/list/response" } ->["payload"]; +type ChatListPayload = Extract["payload"]; type ChatInspectPayload = Extract< SessionOutboundMessage, { type: "chat/inspect/response" } @@ -275,38 +272,17 @@ type ChatDeletePayload = Extract< SessionOutboundMessage, { type: "chat/delete/response" } >["payload"]; -type ChatPostPayload = Extract< - SessionOutboundMessage, - { type: "chat/post/response" } ->["payload"]; -type ChatReadPayload = Extract< - SessionOutboundMessage, - { type: "chat/read/response" } ->["payload"]; -type ChatWaitPayload = Extract< - SessionOutboundMessage, - { type: "chat/wait/response" } ->["payload"]; -type LoopRunPayload = Extract< - SessionOutboundMessage, - { type: "loop/run/response" } ->["payload"]; -type LoopListPayload = Extract< - SessionOutboundMessage, - { type: "loop/list/response" } ->["payload"]; +type ChatPostPayload = Extract["payload"]; +type ChatReadPayload = Extract["payload"]; +type ChatWaitPayload = Extract["payload"]; +type LoopRunPayload = Extract["payload"]; +type LoopListPayload = Extract["payload"]; type LoopInspectPayload = Extract< SessionOutboundMessage, { type: "loop/inspect/response" } >["payload"]; -type LoopLogsPayload = Extract< - SessionOutboundMessage, - { type: "loop/logs/response" } ->["payload"]; -type LoopStopPayload = Extract< - SessionOutboundMessage, - { type: "loop/stop/response" } ->["payload"]; +type LoopLogsPayload = Extract["payload"]; +type LoopStopPayload = Extract["payload"]; type ScheduleCreatePayload = Extract< SessionOutboundMessage, { type: "schedule/create/response" } @@ -3261,8 +3237,7 @@ export class DaemonClient { } async loopLogs(options: string | LoopLogsOptions, afterSeq?: number): Promise { - const normalized = - typeof options === "string" ? { id: options, afterSeq } : options; + const normalized = typeof options === "string" ? { id: options, afterSeq } : options; return this.sendCorrelatedSessionRequest({ requestId: normalized.requestId, message: { diff --git a/packages/server/src/poc-commands/commands-poc.test.ts b/packages/server/src/poc-commands/commands-poc.test.ts index 7ca97cef4..23c20030d 100644 --- a/packages/server/src/poc-commands/commands-poc.test.ts +++ b/packages/server/src/poc-commands/commands-poc.test.ts @@ -16,10 +16,7 @@ */ import { describe, expect, test } from "vitest"; -import { - query, - type SDKUserMessage, -} from "@anthropic-ai/claude-agent-sdk"; +import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk"; import { isCommandAvailable } from "../utils/executable.js"; const hasClaudeCredentials = @@ -34,79 +31,87 @@ function createEmptyPrompt(): AsyncGenerator { describe("Claude Agent SDK Commands POC", () => { describe("supportedCommands() API", () => { - test.runIf(canRunClaudeIntegration)("should return an array of SlashCommand objects", async () => { - // Use the pattern from claude-agent.ts: - // Create a query with empty prompt generator for control methods - const emptyPrompt = createEmptyPrompt(); - - const claudeQuery = query({ - prompt: emptyPrompt, - options: { - cwd: process.cwd(), - permissionMode: "plan", - includePartialMessages: false, - settingSources: ["user", "project"], // Required to load skills - }, - }); - - try { - // supportedCommands() is a control method - works without iterating - const commands = await claudeQuery.supportedCommands(); - - // Should be an array - expect(Array.isArray(commands)).toBe(true); - - // Verify structure - if (commands.length > 0) { - const firstCommand = commands[0]; - expect(typeof firstCommand.name).toBe("string"); - expect(typeof firstCommand.description).toBe("string"); - expect(typeof firstCommand.argumentHint).toBe("string"); - expect(firstCommand.name.startsWith("/")).toBe(false); - } - } finally { - if (typeof claudeQuery.return === "function") { - try { - await claudeQuery.return(); - } catch { - // ignore shutdown errors + test.runIf(canRunClaudeIntegration)( + "should return an array of SlashCommand objects", + async () => { + // Use the pattern from claude-agent.ts: + // Create a query with empty prompt generator for control methods + const emptyPrompt = createEmptyPrompt(); + + const claudeQuery = query({ + prompt: emptyPrompt, + options: { + cwd: process.cwd(), + permissionMode: "plan", + includePartialMessages: false, + settingSources: ["user", "project"], // Required to load skills + }, + }); + + try { + // supportedCommands() is a control method - works without iterating + const commands = await claudeQuery.supportedCommands(); + + // Should be an array + expect(Array.isArray(commands)).toBe(true); + + // Verify structure + if (commands.length > 0) { + const firstCommand = commands[0]; + expect(typeof firstCommand.name).toBe("string"); + expect(typeof firstCommand.description).toBe("string"); + expect(typeof firstCommand.argumentHint).toBe("string"); + expect(firstCommand.name.startsWith("/")).toBe(false); + } + } finally { + if (typeof claudeQuery.return === "function") { + try { + await claudeQuery.return(); + } catch { + // ignore shutdown errors + } } } - } - }, 30000); - - test.runIf(canRunClaudeIntegration)("should have valid SlashCommand structure for all commands", async () => { - const emptyPrompt = createEmptyPrompt(); - - const claudeQuery = query({ - prompt: emptyPrompt, - options: { - cwd: process.cwd(), - permissionMode: "plan", - settingSources: ["user", "project"], - }, - }); - - try { - const commands = await claudeQuery.supportedCommands(); - - expect(commands.length).toBeGreaterThan(0); - - // Verify all commands have valid structure - for (const cmd of commands) { - expect(cmd).toHaveProperty("name"); - expect(cmd).toHaveProperty("description"); - expect(cmd).toHaveProperty("argumentHint"); - expect(typeof cmd.name).toBe("string"); - expect(typeof cmd.description).toBe("string"); - expect(typeof cmd.argumentHint).toBe("string"); - expect(cmd.name.length).toBeGreaterThan(0); - expect(cmd.name.startsWith("/")).toBe(false); + }, + 30000, + ); + + test.runIf(canRunClaudeIntegration)( + "should have valid SlashCommand structure for all commands", + async () => { + const emptyPrompt = createEmptyPrompt(); + + const claudeQuery = query({ + prompt: emptyPrompt, + options: { + cwd: process.cwd(), + permissionMode: "plan", + settingSources: ["user", "project"], + }, + }); + + try { + const commands = await claudeQuery.supportedCommands(); + + expect(commands.length).toBeGreaterThan(0); + + // Verify all commands have valid structure + for (const cmd of commands) { + expect(cmd).toHaveProperty("name"); + expect(cmd).toHaveProperty("description"); + expect(cmd).toHaveProperty("argumentHint"); + expect(typeof cmd.name).toBe("string"); + expect(typeof cmd.description).toBe("string"); + expect(typeof cmd.argumentHint).toBe("string"); + expect(cmd.name.length).toBeGreaterThan(0); + expect(cmd.name.startsWith("/")).toBe(false); + } + } finally { + await claudeQuery.return?.(); } - } finally { - await claudeQuery.return?.(); - } - }, 30000); + }, + 30000, + ); }); describe("Command Execution", () => { diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 80161dbb8..042e52704 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -887,7 +887,12 @@ describe("AgentManager", () => { }); await this.gate; if (this.delayedInterrupted) { - this.pushEvent({ type: "turn_canceled", provider: this.provider, reason: "Interrupted", turnId }); + this.pushEvent({ + type: "turn_canceled", + provider: this.provider, + reason: "Interrupted", + turnId, + }); } else { this.pushEvent({ type: "turn_completed", provider: this.provider, turnId }); } @@ -1456,7 +1461,12 @@ describe("AgentManager", () => { this.pushEvent({ type: "turn_started", provider: this.provider, turnId }); if (turnNum === 1) { await allowFirstRunToEnd.promise; - this.pushEvent({ type: "turn_canceled", provider: this.provider, reason: "interrupted", turnId }); + this.pushEvent({ + type: "turn_canceled", + provider: this.provider, + reason: "interrupted", + turnId, + }); } else { await allowSecondRunToEnd.promise; this.pushEvent({ type: "turn_completed", provider: this.provider, turnId }); @@ -1640,7 +1650,7 @@ describe("AgentManager", () => { await secondStartEntered.promise; const replaceGapSnapshot = manager.getAgent(snapshot.id) as - | ({ pendingReplacement: boolean; activeForegroundTurnId: string | null; lifecycle: string }) + | { pendingReplacement: boolean; activeForegroundTurnId: string | null; lifecycle: string } | undefined; expect(replaceGapSnapshot?.pendingReplacement).toBe(false); expect(replaceGapSnapshot?.activeForegroundTurnId).toBeNull(); @@ -1712,14 +1722,22 @@ describe("AgentManager", () => { // Push autonomous events through the session's subscribe() callbacks const autonomousTurnId = "autonomous-turn-1"; - capturedSession!.pushEvent({ type: "turn_started", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_started", + provider: "codex", + turnId: autonomousTurnId, + }); capturedSession!.pushEvent({ type: "timeline", provider: "codex", item: { type: "assistant_message", text: "AUTONOMOUS_PUMP_MESSAGE" }, turnId: autonomousTurnId, }); - capturedSession!.pushEvent({ type: "turn_completed", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_completed", + provider: "codex", + turnId: autonomousTurnId, + }); await settled; const updated = manager.getAgent(snapshot.id); @@ -1789,7 +1807,11 @@ describe("AgentManager", () => { }, { agentId: snapshot.id, replayState: false }, ); - capturedSession.pushEvent({ type: "turn_started", provider: "codex", turnId: "autonomous-cancel-1" }); + capturedSession.pushEvent({ + type: "turn_started", + provider: "codex", + turnId: "autonomous-cancel-1", + }); }); const beforeCancel = manager.getAgent(snapshot.id); @@ -1832,8 +1854,16 @@ describe("AgentManager", () => { const autonomousTurnId = "autonomous-wait-1"; const waitPromise = manager.waitForAgentEvent(snapshot.id, { waitForActive: true }); - capturedSession!.pushEvent({ type: "turn_started", provider: "codex", turnId: autonomousTurnId }); - capturedSession!.pushEvent({ type: "turn_completed", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_started", + provider: "codex", + turnId: autonomousTurnId, + }); + capturedSession!.pushEvent({ + type: "turn_completed", + provider: "codex", + turnId: autonomousTurnId, + }); const result = await waitPromise; expect(result.status).toBe("idle"); @@ -1894,7 +1924,11 @@ describe("AgentManager", () => { await new Promise((resolve) => { const unsub = manager.subscribe( (event) => { - if (event.type === "agent_state" && event.agent.id === snapshot.id && event.agent.lifecycle === "running") { + if ( + event.type === "agent_state" && + event.agent.id === snapshot.id && + event.agent.lifecycle === "running" + ) { unsub(); resolve(); } @@ -1905,14 +1939,22 @@ describe("AgentManager", () => { // Push autonomous events while foreground is active const autonomousTurnId = "autonomous-during-fg-1"; - capturedSession!.pushEvent({ type: "turn_started", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_started", + provider: "codex", + turnId: autonomousTurnId, + }); capturedSession!.pushEvent({ type: "timeline", provider: "codex", item: { type: "assistant_message", text: "AUTONOMOUS_DURING_FOREGROUND" }, turnId: autonomousTurnId, }); - capturedSession!.pushEvent({ type: "turn_completed", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_completed", + provider: "codex", + turnId: autonomousTurnId, + }); releaseForeground.resolve(); const foregroundEvents = await foregroundResults; @@ -1968,7 +2010,11 @@ describe("AgentManager", () => { const settled = new Promise((resolve) => { manager.subscribe( (event) => { - if (event.type === "agent_state" && event.agent.id === snapshot.id && event.agent.lifecycle === "idle") { + if ( + event.type === "agent_state" && + event.agent.id === snapshot.id && + event.agent.lifecycle === "idle" + ) { resolve(); } if (event.type === "agent_stream" && event.agentId === snapshot.id) { @@ -1980,14 +2026,22 @@ describe("AgentManager", () => { }); const autonomousTurnId = "autonomous-isolation-1"; - capturedSession!.pushEvent({ type: "turn_started", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_started", + provider: "codex", + turnId: autonomousTurnId, + }); capturedSession!.pushEvent({ type: "timeline", provider: "codex", item: { type: "assistant_message", text: "EVENT_AFTER_ERROR" }, turnId: autonomousTurnId, }); - capturedSession!.pushEvent({ type: "turn_completed", provider: "codex", turnId: autonomousTurnId }); + capturedSession!.pushEvent({ + type: "turn_completed", + provider: "codex", + turnId: autonomousTurnId, + }); await settled; @@ -2127,7 +2181,9 @@ describe("AgentManager", () => { subscribe(callback: (event: AgentStreamEvent) => void): () => void { this.subs.add(callback); - return () => { this.subs.delete(callback); }; + return () => { + this.subs.delete(callback); + }; } async *streamHistory(): AsyncGenerator {} @@ -2410,7 +2466,12 @@ describe("AgentManager", () => { const turnId = `fail-turn-${attempt}`; setTimeout(() => { this.pushEvent({ type: "turn_started", provider: this.provider, turnId }); - this.pushEvent({ type: "turn_failed", provider: this.provider, error: `boom-${attempt}`, turnId }); + this.pushEvent({ + type: "turn_failed", + provider: this.provider, + error: `boom-${attempt}`, + turnId, + }); }, 0); return { turnId }; } @@ -2537,7 +2598,12 @@ describe("AgentManager", () => { const turnId = "turn-failed-1"; setTimeout(() => { this.pushEvent({ type: "turn_started", provider: this.provider, turnId }); - this.pushEvent({ type: "turn_failed", provider: this.provider, error: "invalid model id", turnId }); + this.pushEvent({ + type: "turn_failed", + provider: this.provider, + error: "invalid model id", + turnId, + }); }, 0); return { turnId }; } @@ -2789,7 +2855,9 @@ describe("AgentManager", () => { subscribe(callback: (event: AgentStreamEvent) => void): () => void { this.subs.add(callback); - return () => { this.subs.delete(callback); }; + return () => { + this.subs.delete(callback); + }; } async *streamHistory(): AsyncGenerator {} @@ -3079,7 +3147,9 @@ describe("AgentManager", () => { } if (event.type === "agent_state" && event.agent.id === snapshot.id) { const fastMode = event.agent.features?.find((feature) => feature.id === "fast_mode"); - seen.push(`state:${event.agent.currentModeId}:${String(fastMode?.type === "toggle" ? fastMode.value : null)}`); + seen.push( + `state:${event.agent.currentModeId}:${String(fastMode?.type === "toggle" ? fastMode.value : null)}`, + ); return; } if (event.type === "agent_stream" && event.event.type === "permission_resolved") { @@ -3127,12 +3197,18 @@ describe("AgentManager", () => { subscribe(callback: (event: AgentStreamEvent) => void): () => void { this.subscribers.add(callback); - return () => { this.subscribers.delete(callback); }; + return () => { + this.subscribers.delete(callback); + }; } private pushEvent(event: AgentStreamEvent): void { for (const cb of this.subscribers) { - try { cb(event); } catch { /* isolation */ } + try { + cb(event); + } catch { + /* isolation */ + } } } diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index f95f29205..3aeec36c1 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1330,10 +1330,7 @@ export class AgentManager { } const pendingRun = this.getPendingForegroundRun(agentId); - if ( - (snapshot.lifecycle === "running" || pendingRun?.started) && - !snapshot.pendingReplacement - ) { + if ((snapshot.lifecycle === "running" || pendingRun?.started) && !snapshot.pendingReplacement) { return; } @@ -1410,11 +1407,7 @@ export class AgentManager { return true; } - if ( - !currentPendingRun && - !current.activeForegroundTurnId && - !current.pendingReplacement - ) { + if (!currentPendingRun && !current.activeForegroundTurnId && !current.pendingReplacement) { finishErr(new Error(`Agent ${agentId} run finished before starting`)); return true; } @@ -1474,8 +1467,7 @@ export class AgentManager { const pendingRun = this.getPendingForegroundRun(agentId); const foregroundTurnId = agent.activeForegroundTurnId; const hasForegroundTurn = Boolean(foregroundTurnId); - const isAutonomousRunning = - agent.lifecycle === "running" && !hasForegroundTurn && !pendingRun; + const isAutonomousRunning = agent.lifecycle === "running" && !hasForegroundTurn && !pendingRun; if (!hasForegroundTurn && !isAutonomousRunning && !pendingRun) { return false; @@ -2098,9 +2090,7 @@ export class AgentManager { }, ): void { const eventTurnId = (event as { turnId?: string }).turnId; - const isForegroundEvent = Boolean( - eventTurnId && agent.activeForegroundTurnId === eventTurnId, - ); + const isForegroundEvent = Boolean(eventTurnId && agent.activeForegroundTurnId === eventTurnId); // Only update timestamp for live events, not history replay if (!options?.fromHistory) { @@ -2137,11 +2127,7 @@ export class AgentManager { } // Suppress user_message echoes for the active foreground turn — // these are already recorded by recordUserMessage(). - if ( - !options?.fromHistory && - event.item.type === "user_message" && - isForegroundEvent - ) { + if (!options?.fromHistory && event.item.type === "user_message" && isForegroundEvent) { const eventMessageId = normalizeMessageId(event.item.messageId); const eventText = event.item.text; if (eventMessageId) { @@ -2525,9 +2511,7 @@ export class AgentManager { } } - private async normalizeConfig( - config: AgentSessionConfig, - ): Promise { + private async normalizeConfig(config: AgentSessionConfig): Promise { const normalized: AgentSessionConfig = { ...config }; // Always resolve cwd to absolute path for consistent history file lookup @@ -2585,5 +2569,4 @@ export class AgentManager { } return agent; } - } diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 795e375a0..99e02e55c 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -311,7 +311,12 @@ export type AgentStreamEvent = } | { type: "turn_canceled"; provider: AgentProvider; reason: string; turnId?: string } | { type: "timeline"; item: AgentTimelineItem; provider: AgentProvider; turnId?: string } - | { type: "permission_requested"; provider: AgentProvider; request: AgentPermissionRequest; turnId?: string } + | { + type: "permission_requested"; + provider: AgentProvider; + request: AgentPermissionRequest; + turnId?: string; + } | { type: "permission_resolved"; provider: AgentProvider; diff --git a/packages/server/src/server/agent/model-catalog.e2e.test.ts b/packages/server/src/server/agent/model-catalog.e2e.test.ts index 0b623d2de..f1297b823 100644 --- a/packages/server/src/server/agent/model-catalog.e2e.test.ts +++ b/packages/server/src/server/agent/model-catalog.e2e.test.ts @@ -16,10 +16,7 @@ function isBinaryInstalled(binary: string): boolean { const hasCodex = isBinaryInstalled("codex"); const hasOpenCode = isBinaryInstalled("opencode"); -function modelMatchesFamily( - model: AgentModelDefinition, - family: "sonnet" | "haiku", -): boolean { +function modelMatchesFamily(model: AgentModelDefinition, family: "sonnet" | "haiku"): boolean { const haystacks = [model.id, model.label, model.description ?? ""].map((value) => value.toLowerCase(), ); diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index 65bccc859..94b183f71 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -52,9 +52,9 @@ const PROVIDER_CLIENT_FACTORIES: Record = { logger, runtimeSettings: runtimeSettings?.copilot, }), - opencode: (logger, runtimeSettings) => - new OpenCodeAgentClient(logger, runtimeSettings?.opencode), - pi: (logger, runtimeSettings) => new PiACPAgentClient({ logger, runtimeSettings: runtimeSettings?.pi }), + opencode: (logger, runtimeSettings) => new OpenCodeAgentClient(logger, runtimeSettings?.opencode), + pi: (logger, runtimeSettings) => + new PiACPAgentClient({ logger, runtimeSettings: runtimeSettings?.pi }), }; function getProviderClientFactory(provider: string): ProviderClientFactory { diff --git a/packages/server/src/server/agent/provider-snapshot-manager.test.ts b/packages/server/src/server/agent/provider-snapshot-manager.test.ts index 4ea80bf3b..da340e574 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.test.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.test.ts @@ -236,9 +236,7 @@ describe("ProviderSnapshotManager", () => { }); manager.refresh("/tmp/project"); - expect(manager.getSnapshot("/tmp/project")).toEqual([ - { provider: "codex", status: "loading" }, - ]); + expect(manager.getSnapshot("/tmp/project")).toEqual([{ provider: "codex", status: "loading" }]); await vi.waitFor(() => { expect(getProviderEntry(manager.getSnapshot("/tmp/project"), "codex")?.models?.[0]?.id).toBe( @@ -302,8 +300,12 @@ describe("ProviderSnapshotManager", () => { manager.getSnapshot("/tmp/project-b"); await vi.waitFor(() => { - expect(getProviderEntry(manager.getSnapshot("/tmp/project-a"), "codex")?.status).toBe("ready"); - expect(getProviderEntry(manager.getSnapshot("/tmp/project-b"), "codex")?.status).toBe("ready"); + expect(getProviderEntry(manager.getSnapshot("/tmp/project-a"), "codex")?.status).toBe( + "ready", + ); + expect(getProviderEntry(manager.getSnapshot("/tmp/project-b"), "codex")?.status).toBe( + "ready", + ); }); expect(getProviderEntry(manager.getSnapshot("/tmp/project-a"), "codex")?.models?.[0]?.id).toBe( @@ -336,19 +338,24 @@ function createRegistry(handles: MockProviderHandle[]): { registry: Object.fromEntries( handles.map((handle) => [handle.definition.id, handle.definition]), ) as Record, - handles: Object.fromEntries( - handles.map((handle) => [handle.definition.id, handle]), - ) as Record, + handles: Object.fromEntries(handles.map((handle) => [handle.definition.id, handle])) as Record< + AgentProvider, + MockProviderHandle + >, }; } function createMockProvider(options: MockProviderOptions): MockProviderHandle { const isAvailable = vi.fn(async () => options.isAvailable?.() ?? true); - const fetchModels = vi.fn(async (listOptions?: { cwd?: string }) => - options.fetchModels?.(listOptions?.cwd) ?? [createModel(options.provider, `${options.provider}-default`)], + const fetchModels = vi.fn( + async (listOptions?: { cwd?: string }) => + options.fetchModels?.(listOptions?.cwd) ?? [ + createModel(options.provider, `${options.provider}-default`), + ], ); - const fetchModes = vi.fn(async (listOptions?: { cwd?: string }) => - options.fetchModes?.(listOptions?.cwd) ?? [createMode(`${options.provider}-mode`)], + const fetchModes = vi.fn( + async (listOptions?: { cwd?: string }) => + options.fetchModes?.(listOptions?.cwd) ?? [createMode(`${options.provider}-mode`)], ); const definition: ProviderDefinition = { diff --git a/packages/server/src/server/agent/provider-snapshot-manager.ts b/packages/server/src/server/agent/provider-snapshot-manager.ts index dded5a2b4..c83358df9 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.ts @@ -3,19 +3,13 @@ import { resolve } from "node:path"; import type { Logger } from "pino"; -import type { - AgentProvider, - ProviderSnapshotEntry, -} from "./agent-sdk-types.js"; +import type { AgentProvider, ProviderSnapshotEntry } from "./agent-sdk-types.js"; import type { ProviderDefinition } from "./provider-registry.js"; import { AGENT_PROVIDER_IDS } from "./provider-manifest.js"; const DEFAULT_CWD_KEY = "__default__"; -type ProviderSnapshotChangeListener = ( - entries: ProviderSnapshotEntry[], - cwd?: string, -) => void; +type ProviderSnapshotChangeListener = (entries: ProviderSnapshotEntry[], cwd?: string) => void; export class ProviderSnapshotManager { private readonly snapshots = new Map>(); @@ -143,7 +137,10 @@ export class ProviderSnapshotManager { status: "error", error: toErrorMessage(error), }); - this.logger.warn({ err: error, provider, cwd: cwdKey }, "Failed to refresh provider snapshot"); + this.logger.warn( + { err: error, provider, cwd: cwdKey }, + "Failed to refresh provider snapshot", + ); this.emitChange(cwdKey); } } diff --git a/packages/server/src/server/agent/providers/__tests__/claude-agent.event-stream.integration.test.ts b/packages/server/src/server/agent/providers/__tests__/claude-agent.event-stream.integration.test.ts index 14c684297..0165e62af 100644 --- a/packages/server/src/server/agent/providers/__tests__/claude-agent.event-stream.integration.test.ts +++ b/packages/server/src/server/agent/providers/__tests__/claude-agent.event-stream.integration.test.ts @@ -55,10 +55,7 @@ function eventsForTurn(events: AgentStreamEvent[], turnId: string): AgentStreamE function userMessagesWithText(events: AgentStreamEvent[], text: string): AgentStreamEvent[] { return events.filter( - (e) => - e.type === "timeline" && - e.item.type === "user_message" && - e.item.text === text, + (e) => e.type === "timeline" && e.item.type === "user_message" && e.item.text === text, ); } @@ -185,12 +182,8 @@ function assertInvariants(events: AgentStreamEvent[], foregroundTurnIds: string[ } // Invariant 4: Autonomous turns have distinct turnIds from foreground turns - const allTurnIds = new Set( - events.filter(hasTurnId).map((e) => e.turnId), - ); - const autonomousTurnIds = [...allTurnIds].filter( - (id) => !foregroundTurnIds.includes(id), - ); + const allTurnIds = new Set(events.filter(hasTurnId).map((e) => e.turnId)); + const autonomousTurnIds = [...allTurnIds].filter((id) => !foregroundTurnIds.includes(id)); for (const autoId of autonomousTurnIds) { expect(foregroundTurnIds).not.toContain(autoId); } @@ -201,300 +194,332 @@ function assertInvariants(events: AgentStreamEvent[], foregroundTurnIds: string[ // --------------------------------------------------------------------------- describe("Agent event stream redesign — integration", () => { - test.skipIf(!canRun)("Test 1: Basic foreground turn", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-basic-" }); - - try { - const { turnId, events } = await startTurnAndCollectEvents( - handle.session, - "respond with just the word hello", - ); - - const turnStarted = events.find( - (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId, - ); - expect(turnStarted).toBeDefined(); - - const terminal = events.find( - (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, - ); - expect(terminal).toBeDefined(); - - assertInvariants(events, [turnId]); - } finally { - await cleanupSession(handle); - } - }, 60_000); - - test.skipIf(!canRun)("Test 2: No duplicate user_messages — THE BUG", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-dedup-" }); - - try { - const { turnId, events } = await startTurnAndCollectEvents(handle.session, "say hi", { - extraMs: 3_000, - }); + test.skipIf(!canRun)( + "Test 1: Basic foreground turn", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-basic-" }); + + try { + const { turnId, events } = await startTurnAndCollectEvents( + handle.session, + "respond with just the word hello", + ); + + const turnStarted = events.find( + (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId, + ); + expect(turnStarted).toBeDefined(); - expect(userMessagesWithText(events, "say hi").length).toBeLessThanOrEqual(1); - - // No turn_started after terminal for the same turnId - const terminalIdx = events.findIndex( - (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, - ); - const staleTurnStarted = events.slice(terminalIdx + 1).filter( - (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId, - ); - expect(staleTurnStarted.length).toBe(0); - - assertInvariants(events, [turnId]); - } finally { - await cleanupSession(handle); - } - }, 60_000); - - test.skipIf(!canRun)("Test 3: Lifecycle doesn't get stuck in running", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-lifecycle-" }); - - try { - const { turnId, events } = await startTurnAndCollectEvents(handle.session, "say hi", { - extraMs: 3_000, - }); + const terminal = events.find( + (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, + ); + expect(terminal).toBeDefined(); - const terminalIdx = events.findIndex( - (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, - ); - const afterTerminal = events.slice(terminalIdx + 1); + assertInvariants(events, [turnId]); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); - // No subsequent turn_started for same turnId - expect( - afterTerminal.filter( - (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId, - ).length, - ).toBe(0); + test.skipIf(!canRun)( + "Test 2: No duplicate user_messages — THE BUG", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-dedup-" }); + + try { + const { turnId, events } = await startTurnAndCollectEvents(handle.session, "say hi", { + extraMs: 3_000, + }); + + expect(userMessagesWithText(events, "say hi").length).toBeLessThanOrEqual(1); + + // No turn_started after terminal for the same turnId + const terminalIdx = events.findIndex( + (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, + ); + const staleTurnStarted = events + .slice(terminalIdx + 1) + .filter((e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId); + expect(staleTurnStarted.length).toBe(0); + + assertInvariants(events, [turnId]); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); - // Any turn_started after terminal must have a different turnId - for (const ts of afterTerminal.filter((e) => e.type === "turn_started" && hasTurnId(e))) { - expect((ts as EventWithTurnId).turnId).not.toBe(turnId); + test.skipIf(!canRun)( + "Test 3: Lifecycle doesn't get stuck in running", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-lifecycle-" }); + + try { + const { turnId, events } = await startTurnAndCollectEvents(handle.session, "say hi", { + extraMs: 3_000, + }); + + const terminalIdx = events.findIndex( + (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, + ); + const afterTerminal = events.slice(terminalIdx + 1); + + // No subsequent turn_started for same turnId + expect( + afterTerminal.filter( + (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId, + ).length, + ).toBe(0); + + // Any turn_started after terminal must have a different turnId + for (const ts of afterTerminal.filter((e) => e.type === "turn_started" && hasTurnId(e))) { + expect((ts as EventWithTurnId).turnId).not.toBe(turnId); + } + + assertInvariants(events, [turnId]); + } finally { + await cleanupSession(handle); } + }, + 60_000, + ); + + test.skipIf(!canRun)( + "Test 4: Autonomous run", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-autonomous-" }); + const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`; + + try { + const { turnId: fgTurnId, events } = await startTurnAndCollectEvents( + handle.session, + [ + "Use the Task tool to start a background sub-agent.", + "In that task, run the Bash command exactly: sleep 3 && echo BACKGROUND_DONE", + "Do not wait for task completion.", + "Reply immediately with exactly: SPAWNED", + `When the background task completes later, reply with exactly: ${autonomousWakeToken}`, + ].join(" "), + { + extraMs: 10_000, + timeoutMs: 60_000, + }, + ); + + const fgTerminalIdx = events.findIndex( + (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === fgTurnId, + ); + const afterForeground = events.slice(fgTerminalIdx + 1); + + // Autonomous turn_started with a different turnId + const autoStarts = afterForeground.filter( + (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId !== fgTurnId, + ) as EventWithTurnId[]; + if (autoStarts.length === 0) { + assertInvariants(events, [fgTurnId]); + return; + } + + const autoTurnId = autoStarts[0]!.turnId; + expect(fgTurnId).not.toBe(autoTurnId); + + // Autonomous turn reaches terminal + expect( + afterForeground.find( + (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === autoTurnId, + ), + ).toBeDefined(); - assertInvariants(events, [turnId]); - } finally { - await cleanupSession(handle); - } - }, 60_000); - - test.skipIf(!canRun)("Test 4: Autonomous run", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-autonomous-" }); - const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`; - - try { - const { turnId: fgTurnId, events } = await startTurnAndCollectEvents( - handle.session, - [ - "Use the Task tool to start a background sub-agent.", - "In that task, run the Bash command exactly: sleep 3 && echo BACKGROUND_DONE", - "Do not wait for task completion.", - "Reply immediately with exactly: SPAWNED", - `When the background task completes later, reply with exactly: ${autonomousWakeToken}`, - ].join(" "), - { - extraMs: 10_000, - timeoutMs: 60_000, - }, - ); - - const fgTerminalIdx = events.findIndex( - (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === fgTurnId, - ); - const afterForeground = events.slice(fgTerminalIdx + 1); - - // Autonomous turn_started with a different turnId - const autoStarts = afterForeground.filter( - (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId !== fgTurnId, - ) as EventWithTurnId[]; - if (autoStarts.length === 0) { assertInvariants(events, [fgTurnId]); - return; + } finally { + await cleanupSession(handle); } + }, + 90_000, + ); + + test.skipIf(!canRun)( + "Test 5: Interruption", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-interrupt-" }); - const autoTurnId = autoStarts[0]!.turnId; - expect(fgTurnId).not.toBe(autoTurnId); - - // Autonomous turn reaches terminal - expect( - afterForeground.find( - (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === autoTurnId, - ), - ).toBeDefined(); - - assertInvariants(events, [fgTurnId]); - } finally { - await cleanupSession(handle); - } - }, 90_000); - - test.skipIf(!canRun)("Test 5: Interruption", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-interrupt-" }); - - try { - let turnId: string | null = null; - const events = await new Promise((resolve, reject) => { - const collected: AgentStreamEvent[] = []; - let interrupted = false; - - const timeout = setTimeout(() => { - unsubscribe(); - reject(new Error("Timed out after 45000ms waiting for terminal event")); - }, 45_000); - - const unsubscribe = handle.session.subscribe((event) => { - collected.push(event); - if (!turnId && event.type === "turn_started" && hasTurnId(event)) { - turnId = event.turnId; - } - - // Once we see turn_started, fire the interrupt - if ( - !interrupted && - turnId && - event.type === "turn_started" && - hasTurnId(event) && - event.turnId === turnId - ) { - interrupted = true; - handle.session.interrupt().catch(() => undefined); - } - - // Resolve when we get a terminal event for this turn - if (turnId && isTerminalEvent(event) && hasTurnId(event) && event.turnId === turnId) { - clearTimeout(timeout); + try { + let turnId: string | null = null; + const events = await new Promise((resolve, reject) => { + const collected: AgentStreamEvent[] = []; + let interrupted = false; + + const timeout = setTimeout(() => { unsubscribe(); - resolve(collected); - } - }); + reject(new Error("Timed out after 45000ms waiting for terminal event")); + }, 45_000); + + const unsubscribe = handle.session.subscribe((event) => { + collected.push(event); + if (!turnId && event.type === "turn_started" && hasTurnId(event)) { + turnId = event.turnId; + } + + // Once we see turn_started, fire the interrupt + if ( + !interrupted && + turnId && + event.type === "turn_started" && + hasTurnId(event) && + event.turnId === turnId + ) { + interrupted = true; + handle.session.interrupt().catch(() => undefined); + } - void handle.session - .startTurn("write a very long essay about the history of computing") - .then((result) => { - if (turnId && turnId !== result.turnId) { + // Resolve when we get a terminal event for this turn + if (turnId && isTerminalEvent(event) && hasTurnId(event) && event.turnId === turnId) { clearTimeout(timeout); unsubscribe(); - reject( - new Error( - `Observed turn_started for ${turnId} but startTurn returned ${result.turnId}`, - ), - ); - return; + resolve(collected); } - turnId = result.turnId; - }) - .catch((error) => { - clearTimeout(timeout); - unsubscribe(); - reject(error); }); - }); - expect(turnId).toBeDefined(); - - // turn_canceled or turn_failed arrives for that turnId - const terminal = events.find( - (e) => - (e.type === "turn_canceled" || e.type === "turn_failed") && - hasTurnId(e) && - e.turnId === turnId, - ); - expect(terminal).toBeDefined(); - - // No further events for that turnId after terminal - const terminalIdx = events.indexOf(terminal!); - expect( - events.slice(terminalIdx + 1).filter((e) => hasTurnId(e) && e.turnId === turnId).length, - ).toBe(0); - - assertInvariants(events, [turnId]); - } finally { - await cleanupSession(handle); - } - }, 60_000); - - test.skipIf(!canRun)("Test 6: Sequential foreground turns", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-sequential-" }); - - try { - const { turnId: turnId1, events: events1 } = await startTurnAndCollectEvents( - handle.session, - "say first", - ); - - const { turnId: turnId2, events: events2 } = await startTurnAndCollectEvents( - handle.session, - "say second", - ); - - const allEvents = [...events1, ...events2]; - - expect(turnId1).not.toBe(turnId2); - - // No events from turn 1 after turn 2 starts - const turn2StartIdx = allEvents.findIndex( - (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId2, - ); - expect( - allEvents.slice(turn2StartIdx + 1).filter((e) => hasTurnId(e) && e.turnId === turnId1) - .length, - ).toBe(0); - - assertInvariants(allEvents, [turnId1, turnId2]); - } finally { - await cleanupSession(handle); - } - }, 90_000); - - test.skipIf(!canRun)("Test 7: Fast-fail", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-fast-fail-" }); - - try { - const { turnId, events } = await startTurnAndCollectEvents(handle.session, "", { - extraMs: 3_000, - }); + void handle.session + .startTurn("write a very long essay about the history of computing") + .then((result) => { + if (turnId && turnId !== result.turnId) { + clearTimeout(timeout); + unsubscribe(); + reject( + new Error( + `Observed turn_started for ${turnId} but startTurn returned ${result.turnId}`, + ), + ); + return; + } + turnId = result.turnId; + }) + .catch((error) => { + clearTimeout(timeout); + unsubscribe(); + reject(error); + }); + }); - // At most one turn_started - expect( - events.filter((e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId) - .length, - ).toBeLessThanOrEqual(1); - - // Terminal present - const terminal = events.find( - (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, - ); - expect(terminal).toBeDefined(); - - // No stale turn_started after terminal - const terminalIdx = events.indexOf(terminal!); - expect( - events.slice(terminalIdx + 1).filter((e) => e.type === "turn_started").length, - ).toBe(0); - - assertInvariants(events, [turnId]); - } finally { - await cleanupSession(handle); - } - }, 60_000); - - test.skipIf(!canRun)("Test 8: User message dedup by text", async () => { - const handle = await createSession({ cwdPrefix: "event-stream-user-dedup-" }); - - try { - const { turnId, events } = await startTurnAndCollectEvents(handle.session, "hello world", { - extraMs: 3_000, - }); + expect(turnId).toBeDefined(); + + // turn_canceled or turn_failed arrives for that turnId + const terminal = events.find( + (e) => + (e.type === "turn_canceled" || e.type === "turn_failed") && + hasTurnId(e) && + e.turnId === turnId, + ); + expect(terminal).toBeDefined(); + + // No further events for that turnId after terminal + const terminalIdx = events.indexOf(terminal!); + expect( + events.slice(terminalIdx + 1).filter((e) => hasTurnId(e) && e.turnId === turnId).length, + ).toBe(0); + + assertInvariants(events, [turnId]); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); + + test.skipIf(!canRun)( + "Test 6: Sequential foreground turns", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-sequential-" }); + + try { + const { turnId: turnId1, events: events1 } = await startTurnAndCollectEvents( + handle.session, + "say first", + ); + + const { turnId: turnId2, events: events2 } = await startTurnAndCollectEvents( + handle.session, + "say second", + ); + + const allEvents = [...events1, ...events2]; + + expect(turnId1).not.toBe(turnId2); + + // No events from turn 1 after turn 2 starts + const turn2StartIdx = allEvents.findIndex( + (e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId2, + ); + expect( + allEvents.slice(turn2StartIdx + 1).filter((e) => hasTurnId(e) && e.turnId === turnId1) + .length, + ).toBe(0); + + assertInvariants(allEvents, [turnId1, turnId2]); + } finally { + await cleanupSession(handle); + } + }, + 90_000, + ); - expect(userMessagesWithText(events, "hello world").length).toBeLessThanOrEqual(1); + test.skipIf(!canRun)( + "Test 7: Fast-fail", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-fast-fail-" }); + + try { + const { turnId, events } = await startTurnAndCollectEvents(handle.session, "", { + extraMs: 3_000, + }); + + // At most one turn_started + expect( + events.filter((e) => e.type === "turn_started" && hasTurnId(e) && e.turnId === turnId) + .length, + ).toBeLessThanOrEqual(1); + + // Terminal present + const terminal = events.find( + (e) => isTerminalEvent(e) && hasTurnId(e) && e.turnId === turnId, + ); + expect(terminal).toBeDefined(); + + // No stale turn_started after terminal + const terminalIdx = events.indexOf(terminal!); + expect(events.slice(terminalIdx + 1).filter((e) => e.type === "turn_started").length).toBe( + 0, + ); + + assertInvariants(events, [turnId]); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); - assertInvariants(events, [turnId]); - } finally { - await cleanupSession(handle); - } - }, 60_000); + test.skipIf(!canRun)( + "Test 8: User message dedup by text", + async () => { + const handle = await createSession({ cwdPrefix: "event-stream-user-dedup-" }); + + try { + const { turnId, events } = await startTurnAndCollectEvents(handle.session, "hello world", { + extraMs: 3_000, + }); + + expect(userMessagesWithText(events, "hello world").length).toBeLessThanOrEqual(1); + + assertInvariants(events, [turnId]); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); }); diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index b686497b5..ced9a1ae0 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -75,23 +75,19 @@ describe("deriveModesFromACP", () => { }); test("falls back to config options when explicit mode state is absent", () => { - const result = deriveModesFromACP( - [{ id: "fallback", label: "Fallback" }], - null, - [ - { - id: "mode", - name: "Mode", - category: "mode", - type: "select", - currentValue: "acceptEdits", - options: [ - { value: "default", name: "Always Ask" }, - { value: "acceptEdits", name: "Accept File Edits" }, - ], - }, - ], - ); + const result = deriveModesFromACP([{ id: "fallback", label: "Fallback" }], null, [ + { + id: "mode", + name: "Mode", + category: "mode", + type: "select", + currentValue: "acceptEdits", + options: [ + { value: "default", name: "Always Ask" }, + { value: "acceptEdits", name: "Accept File Edits" }, + ], + }, + ]); expect(result).toEqual({ modes: [ @@ -103,24 +99,20 @@ describe("deriveModesFromACP", () => { }); test("returns an empty mode list when fallback modes are empty and config only exposes thought levels", () => { - const result = deriveModesFromACP( - [], - null, - [ - { - id: "thought_level", - name: "Thinking", - category: "thought_level", - type: "select", - currentValue: "medium", - options: [ - { value: "low", name: "Low" }, - { value: "medium", name: "Medium" }, - { value: "high", name: "High" }, - ], - }, - ], - ); + const result = deriveModesFromACP([], null, [ + { + id: "thought_level", + name: "Thinking", + category: "thought_level", + type: "select", + currentValue: "medium", + options: [ + { value: "low", name: "Low" }, + { value: "medium", name: "Medium" }, + { value: "high", name: "High" }, + ], + }, + ]); expect(result).toEqual({ modes: [], @@ -131,26 +123,30 @@ describe("deriveModesFromACP", () => { describe("deriveModelDefinitionsFromACP", () => { test("attaches shared thinking options to ACP model state", () => { - const result = deriveModelDefinitionsFromACP("claude-acp", { - availableModels: [ - { modelId: "haiku", name: "Haiku", description: "Fast" }, - { modelId: "sonnet", name: "Sonnet", description: "Balanced" }, - ], - currentModelId: "haiku", - }, [ + const result = deriveModelDefinitionsFromACP( + "claude-acp", { - id: "reasoning", - name: "Reasoning", - category: "thought_level", - type: "select", - currentValue: "medium", - options: [ - { value: "low", name: "Low" }, - { value: "medium", name: "Medium" }, - { value: "high", name: "High" }, + availableModels: [ + { modelId: "haiku", name: "Haiku", description: "Fast" }, + { modelId: "sonnet", name: "Sonnet", description: "Balanced" }, ], + currentModelId: "haiku", }, - ]); + [ + { + id: "reasoning", + name: "Reasoning", + category: "thought_level", + type: "select", + currentValue: "medium", + options: [ + { value: "low", name: "Low" }, + { value: "medium", name: "Medium" }, + { value: "high", name: "High" }, + ], + }, + ], + ); expect(result).toEqual([ { @@ -160,9 +156,27 @@ describe("deriveModelDefinitionsFromACP", () => { description: "Fast", isDefault: true, thinkingOptions: [ - { id: "low", label: "Low", description: undefined, isDefault: false, metadata: undefined }, - { id: "medium", label: "Medium", description: undefined, isDefault: true, metadata: undefined }, - { id: "high", label: "High", description: undefined, isDefault: false, metadata: undefined }, + { + id: "low", + label: "Low", + description: undefined, + isDefault: false, + metadata: undefined, + }, + { + id: "medium", + label: "Medium", + description: undefined, + isDefault: true, + metadata: undefined, + }, + { + id: "high", + label: "High", + description: undefined, + isDefault: false, + metadata: undefined, + }, ], defaultThinkingOptionId: "medium", }, @@ -173,9 +187,27 @@ describe("deriveModelDefinitionsFromACP", () => { description: "Balanced", isDefault: false, thinkingOptions: [ - { id: "low", label: "Low", description: undefined, isDefault: false, metadata: undefined }, - { id: "medium", label: "Medium", description: undefined, isDefault: true, metadata: undefined }, - { id: "high", label: "High", description: undefined, isDefault: false, metadata: undefined }, + { + id: "low", + label: "Low", + description: undefined, + isDefault: false, + metadata: undefined, + }, + { + id: "medium", + label: "Medium", + description: undefined, + isDefault: true, + metadata: undefined, + }, + { + id: "high", + label: "High", + description: undefined, + isDefault: false, + metadata: undefined, + }, ], defaultThinkingOptionId: "medium", }, @@ -562,9 +594,7 @@ describe("ACPAgentSession", () => { thinkingOptionId: null, modeId: "xhigh", })), - getAvailableModes: vi.fn(async () => [ - { id: "xhigh", label: "xhigh" }, - ]), + getAvailableModes: vi.fn(async () => [{ id: "xhigh", label: "xhigh" }]), getCurrentMode: vi.fn(async () => "xhigh"), setMode: vi.fn(), getPendingPermissions: vi.fn(() => []), diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 6b41a3825..3df4239ea 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -1,8 +1,4 @@ -import { - spawn, - type ChildProcess, - type ChildProcessWithoutNullStreams, -} from "node:child_process"; +import { spawn, type ChildProcess, type ChildProcessWithoutNullStreams } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; @@ -107,8 +103,7 @@ const ACP_CLIENT_CAPABILITIES: ACPClientCapabilities = { terminal: true, }; -const COPILOT_AUTOPILOT_MODE = - "https://agentclientprotocol.com/protocol/session-modes#autopilot"; +const COPILOT_AUTOPILOT_MODE = "https://agentclientprotocol.com/protocol/session-modes#autopilot"; type ACPAgentClientOptions = { provider: string; @@ -473,14 +468,15 @@ export class ACPAgentClient implements AgentClient { } } - protected async spawnProcess( - launchEnv?: Record, - ): Promise { + protected async spawnProcess(launchEnv?: Record): Promise { const { command, args } = this.resolveLaunchCommand(); const child = spawn(command, args, { cwd: process.cwd(), env: { - ...applyProviderEnv(process.env as Record, this.runtimeSettings), + ...applyProviderEnv( + process.env as Record, + this.runtimeSettings, + ), ...(launchEnv ?? {}), }, shell: process.platform === "win32", @@ -787,7 +783,10 @@ export class ACPAgentSession implements AgentSession, ACPClient { }; } - async startTurn(prompt: AgentPromptInput, _options?: AgentRunOptions): Promise<{ turnId: string }> { + async startTurn( + prompt: AgentPromptInput, + _options?: AgentRunOptions, + ): Promise<{ turnId: string }> { if (this.closed) { throw new Error(`${this.provider} session is closed`); } @@ -1084,9 +1083,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { this.activeForegroundTurnId = null; } - async requestPermission( - params: RequestPermissionRequest, - ): Promise { + async requestPermission(params: RequestPermissionRequest): Promise { if (shouldAutoApprovePermissionRequest(this.provider, this.currentMode)) { const selectedOption = selectPermissionOption(params.options, { behavior: "allow" }); return selectedOption @@ -1106,12 +1103,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { if (this.toolSnapshotTransformer) { toolSnapshot = this.toolSnapshotTransformer(toolSnapshot); } - const request = mapPermissionRequest( - this.provider, - requestId, - params, - toolSnapshot, - ); + const request = mapPermissionRequest(this.provider, requestId, params, toolSnapshot); const promise = new Promise((resolve, reject) => { this.pendingPermissions.set(requestId, { @@ -1171,11 +1163,16 @@ export class ACPAgentSession implements AgentSession, ACPClient { async createTerminal(params: CreateTerminalRequest): Promise<{ terminalId: string }> { const terminalId = randomUUID(); - const env = Object.fromEntries((params.env ?? []).map((entry: EnvVariable) => [entry.name, entry.value])); + const env = Object.fromEntries( + (params.env ?? []).map((entry: EnvVariable) => [entry.name, entry.value]), + ); const child = spawn(params.command, params.args ?? [], { cwd: params.cwd ?? this.config.cwd, env: { - ...applyProviderEnv(process.env as Record, this.runtimeSettings), + ...applyProviderEnv( + process.env as Record, + this.runtimeSettings, + ), ...env, }, shell: process.platform === "win32", @@ -1201,9 +1198,15 @@ export class ACPAgentSession implements AgentSession, ACPClient { rejectExit, }; - child.stdout.on("data", (chunk: Buffer | string) => appendTerminalOutput(entry, chunk.toString())); - child.stderr.on("data", (chunk: Buffer | string) => appendTerminalOutput(entry, chunk.toString())); - child.once("error", (error) => rejectExit(error instanceof Error ? error : new Error(String(error)))); + child.stdout.on("data", (chunk: Buffer | string) => + appendTerminalOutput(entry, chunk.toString()), + ); + child.stderr.on("data", (chunk: Buffer | string) => + appendTerminalOutput(entry, chunk.toString()), + ); + child.once("error", (error) => + rejectExit(error instanceof Error ? error : new Error(String(error))), + ); child.once("exit", (code, signal) => { const exit = { exitCode: code, signal }; entry.exit = exit; @@ -1258,7 +1261,10 @@ export class ACPAgentSession implements AgentSession, ACPClient { const child = spawn(command, args, { cwd: this.config.cwd, env: { - ...applyProviderEnv(process.env as Record, this.runtimeSettings), + ...applyProviderEnv( + process.env as Record, + this.runtimeSettings, + ), ...(this.launchEnv ?? {}), }, shell: process.platform === "win32", @@ -1448,7 +1454,11 @@ export class ACPAgentSession implements AgentSession, ACPClient { private deriveAvailableModels( models: SessionModelState | null | undefined, ): AgentModelDefinition[] { - const availableModels = deriveModelDefinitionsFromACP(this.provider, models, this.configOptions); + const availableModels = deriveModelDefinitionsFromACP( + this.provider, + models, + this.configOptions, + ); return this.modelTransformer ? this.modelTransformer(availableModels) : availableModels; } @@ -1508,7 +1518,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { } } - private finishTurn(event: Extract): void { + private finishTurn( + event: Extract, + ): void { this.activeForegroundTurnId = null; this.suppressUserEchoMessageId = null; this.suppressUserEchoText = null; @@ -1553,7 +1565,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { return parts.length > 0 ? parts.join(" | ") : undefined; } - private getSelectConfigOption(category: string): Extract | null { + private getSelectConfigOption( + category: string, + ): Extract | null { const option = this.configOptions.find( (entry): entry is Extract => entry.type === "select" && entry.category === category, @@ -1573,7 +1587,12 @@ export class ACPAgentSession implements AgentSession, ACPClient { function flattenSelectOptions( options: Extract["options"], ): Array<{ value: string; name: string; description?: string | null; group?: string }> { - const flattened: Array<{ value: string; name: string; description?: string | null; group?: string }> = []; + const flattened: Array<{ + value: string; + name: string; + description?: string | null; + group?: string; + }> = []; for (const option of options) { if ("value" in option) { flattened.push(option); @@ -1682,7 +1701,9 @@ function extractPromptText(prompt: AgentPromptInput): string { return prompt; } return prompt - .filter((block): block is Extract => block.type === "text") + .filter( + (block): block is Extract => block.type === "text", + ) .map((block) => block.text) .join(""); } @@ -1694,7 +1715,9 @@ function contentBlockToText(content: ContentBlock): string { case "resource_link": return content.title ?? content.uri; case "resource": - return "text" in content.resource ? content.resource.text : `[resource:${content.resource.mimeType ?? "binary"}]`; + return "text" in content.resource + ? content.resource.text + : `[resource:${content.resource.mimeType ?? "binary"}]`; case "image": return "[image]"; case "audio": @@ -1715,8 +1738,8 @@ function mergeToolSnapshot( title: (update.title ?? previous?.title ?? toolCallId) as string, kind: update.kind ?? previous?.kind ?? null, status: update.status ?? previous?.status ?? null, - content: update.content !== undefined ? update.content : previous?.content ?? null, - locations: update.locations !== undefined ? update.locations : previous?.locations ?? null, + content: update.content !== undefined ? update.content : (previous?.content ?? null), + locations: update.locations !== undefined ? update.locations : (previous?.locations ?? null), rawInput: update.rawInput !== undefined ? update.rawInput : previous?.rawInput, rawOutput: update.rawOutput !== undefined ? update.rawOutput : previous?.rawOutput, ...(isFull ? {} : {}), @@ -1783,7 +1806,10 @@ function mapToolStatus(status: ToolCallStatus | null | undefined): ToolCallTimel } } -function mapToolDetail(snapshot: ACPToolSnapshot, terminals: Map): ToolCallDetail { +function mapToolDetail( + snapshot: ACPToolSnapshot, + terminals: Map, +): ToolCallDetail { const firstLocation = snapshot.locations?.[0]?.path; const textContent = extractToolText(snapshot.content); const diffContent = extractDiffContent(snapshot.content); @@ -1811,7 +1837,7 @@ function mapToolDetail(snapshot: ACPToolSnapshot, terminals: Map => item.type === "diff"); + const diff = content?.find( + (item): item is Extract => item.type === "diff", + ); return diff ? { oldText: diff.oldText ?? undefined, newText: diff.newText } : null; } @@ -1993,10 +2021,7 @@ function readRecord(value: unknown): Record | null { : null; } -function readString( - record: Record | null, - keys: string[], -): string | undefined { +function readString(record: Record | null, keys: string[]): string | undefined { if (!record) { return undefined; } @@ -2009,10 +2034,7 @@ function readString( return undefined; } -function readNumber( - record: Record | null, - keys: string[], -): number | undefined { +function readNumber(record: Record | null, keys: string[]): number | undefined { if (!record) { return undefined; } @@ -2061,7 +2083,9 @@ function stringifyUnknown(value: unknown): string | undefined { } } -function coerceSessionConfigMetadata(metadata: AgentMetadata | undefined): Partial { +function coerceSessionConfigMetadata( + metadata: AgentMetadata | undefined, +): Partial { if (!metadata || typeof metadata !== "object") { return {}; } diff --git a/packages/server/src/server/agent/providers/claude-agent.integration.test.ts b/packages/server/src/server/agent/providers/claude-agent.integration.test.ts index 90c7d2ca4..4629b3ebc 100644 --- a/packages/server/src/server/agent/providers/claude-agent.integration.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.integration.test.ts @@ -182,36 +182,40 @@ describe("ClaudeAgentSession integration", () => { } }); - test.runIf(canRunClaudeIntegration)("streams a basic response turn end-to-end", async () => { - const handle = await createSession({ - cwdPrefix: "claude-agent-basic-response-", - }); + test.runIf(canRunClaudeIntegration)( + "streams a basic response turn end-to-end", + async () => { + const handle = await createSession({ + cwdPrefix: "claude-agent-basic-response-", + }); - try { - const events = await collectUntilTerminal( - streamSession(handle.session, "Respond with exactly: HELLO_WORLD"), - ); + try { + const events = await collectUntilTerminal( + streamSession(handle.session, "Respond with exactly: HELLO_WORLD"), + ); - expect(events[0]).toMatchObject({ - type: "turn_started", - provider: "claude", - }); - expect( - events.some( - (event) => - event.type === "timeline" && - event.item.type === "assistant_message" && - compactText(event.item.text).includes("hello_world"), - ), - ).toBe(true); - expect(events.at(-1)).toMatchObject({ - type: "turn_completed", - provider: "claude", - }); - } finally { - await cleanupSession(handle); - } - }, 60_000); + expect(events[0]).toMatchObject({ + type: "turn_started", + provider: "claude", + }); + expect( + events.some( + (event) => + event.type === "timeline" && + event.item.type === "assistant_message" && + compactText(event.item.text).includes("hello_world"), + ), + ).toBe(true); + expect(events.at(-1)).toMatchObject({ + type: "turn_completed", + provider: "claude", + }); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); test.runIf(canRunClaudeIntegration)( "keeps bypassPermissions available after a thinking-option restart", @@ -276,100 +280,104 @@ describe("ClaudeAgentSession integration", () => { 60_000, ); - test.runIf(canRunClaudeIntegration)("runs a real Bash tool call and completes it", async () => { - const handle = await createSession({ - cwdPrefix: "claude-agent-basic-tool-", - }); + test.runIf(canRunClaudeIntegration)( + "runs a real Bash tool call and completes it", + async () => { + const handle = await createSession({ + cwdPrefix: "claude-agent-basic-tool-", + }); + + try { + const events = await collectUntilTerminal( + streamSession( + handle.session, + [ + "Use the Bash tool.", + "Run exactly: echo TOOL_TEST_OUTPUT", + "After the command completes, reply with exactly: TOOL_DONE", + ].join(" "), + ), + ); + + const bashCalls = getToolCalls(events).filter((item) => item.name.toLowerCase() === "bash"); + const completedBashCall = getLatestCompletedBashCall(events); - try { - const events = await collectUntilTerminal( - streamSession( + expect(bashCalls.length).toBeGreaterThan(0); + expect(completedBashCall).toBeDefined(); + expect(completedBashCall?.detail.type).toBe("shell"); + expect( + completedBashCall?.detail.type === "shell" && + completedBashCall.detail.output?.includes("TOOL_TEST_OUTPUT"), + ).toBe(true); + expect(compactText(getAssistantText(events))).toContain("tool_done"); + expect(events.at(-1)).toMatchObject({ + type: "turn_completed", + provider: "claude", + }); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); + + test.runIf(canRunClaudeIntegration)( + "interrupts a running Bash turn and continues on the same query", + async () => { + const handle = await createSession({ + cwdPrefix: "claude-agent-interrupt-continue-", + }); + + try { + const firstStream = streamSession( handle.session, [ "Use the Bash tool.", - "Run exactly: echo TOOL_TEST_OUTPUT", - "After the command completes, reply with exactly: TOOL_DONE", + "Run exactly: sleep 10", + "Do not use a background task.", + "Do not do anything after starting the command.", ].join(" "), - ), - ); - - const bashCalls = getToolCalls(events).filter((item) => item.name.toLowerCase() === "bash"); - const completedBashCall = getLatestCompletedBashCall(events); - - expect(bashCalls.length).toBeGreaterThan(0); - expect(completedBashCall).toBeDefined(); - expect(completedBashCall?.detail.type).toBe("shell"); - expect( - completedBashCall?.detail.type === "shell" && - completedBashCall.detail.output?.includes("TOOL_TEST_OUTPUT"), - ).toBe(true); - expect(compactText(getAssistantText(events))).toContain("tool_done"); - expect(events.at(-1)).toMatchObject({ - type: "turn_completed", - provider: "claude", - }); - } finally { - await cleanupSession(handle); - } - }, 60_000); + ); - test.runIf(canRunClaudeIntegration)( - "interrupts a running Bash turn and continues on the same query", - async () => { - const handle = await createSession({ - cwdPrefix: "claude-agent-interrupt-continue-", - }); + const initialEvents = await collectUntil( + firstStream, + (event) => + event.type === "timeline" && + event.item.type === "tool_call" && + event.item.name.toLowerCase() === "bash", + 45_000, + ); + const firstQuery = getInternalQuery(handle.session); - try { - const firstStream = streamSession( - handle.session, - [ - "Use the Bash tool.", - "Run exactly: sleep 10", - "Do not use a background task.", - "Do not do anything after starting the command.", - ].join(" "), - ); - - const initialEvents = await collectUntil( - firstStream, - (event) => - event.type === "timeline" && - event.item.type === "tool_call" && - event.item.name.toLowerCase() === "bash", - 45_000, - ); - const firstQuery = getInternalQuery(handle.session); - - expect(firstQuery).toBeTruthy(); - - await handle.session.interrupt(); - - const canceledEvents = await collectUntilTerminal(firstStream, { - timeoutMs: 20_000, - }); - const allFirstTurnEvents = [...initialEvents, ...canceledEvents]; - - expect( - allFirstTurnEvents.some( - (event) => event.type === "turn_canceled" && event.provider === "claude", - ), - ).toBe(true); - - const followUpEvents = await collectUntilTerminal( - streamSession(handle.session, "Respond with exactly: AFTER_INTERRUPT_OK"), - ); - const secondQuery = getInternalQuery(handle.session); - - expect(secondQuery).toBe(firstQuery); - expect(compactText(getAssistantText(followUpEvents))).toContain("after_interrupt_ok"); - expect(followUpEvents.at(-1)).toMatchObject({ - type: "turn_completed", - provider: "claude", - }); - } finally { - await cleanupSession(handle); - } + expect(firstQuery).toBeTruthy(); + + await handle.session.interrupt(); + + const canceledEvents = await collectUntilTerminal(firstStream, { + timeoutMs: 20_000, + }); + const allFirstTurnEvents = [...initialEvents, ...canceledEvents]; + + expect( + allFirstTurnEvents.some( + (event) => event.type === "turn_canceled" && event.provider === "claude", + ), + ).toBe(true); + + const followUpEvents = await collectUntilTerminal( + streamSession(handle.session, "Respond with exactly: AFTER_INTERRUPT_OK"), + ); + const secondQuery = getInternalQuery(handle.session); + + expect(secondQuery).toBe(firstQuery); + expect(compactText(getAssistantText(followUpEvents))).toContain("after_interrupt_ok"); + expect(followUpEvents.at(-1)).toMatchObject({ + type: "turn_completed", + provider: "claude", + }); + } finally { + await cleanupSession(handle); + } }, 60_000, ); @@ -377,106 +385,110 @@ describe("ClaudeAgentSession integration", () => { test.runIf(canRunClaudeIntegration)( "creates an autonomous live turn when a background task completes", async () => { - const handle = await createSession({ - cwdPrefix: "claude-agent-autonomous-", - }); - const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`; + const handle = await createSession({ + cwdPrefix: "claude-agent-autonomous-", + }); + const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`; - try { - const foregroundEvents = await collectUntilTerminal( - streamSession( + try { + const foregroundEvents = await collectUntilTerminal( + streamSession( + handle.session, + [ + "Use the Task tool to start a background sub-agent.", + "In that task, run the Bash command exactly: sleep 3 && echo BACKGROUND_DONE", + "Do not wait for task completion.", + "Reply immediately with exactly: SPAWNED", + `When the background task completes later, reply with exactly: ${autonomousWakeToken}`, + ].join(" "), + ), + { timeoutMs: 45_000 }, + ); + + expect(compactText(getAssistantText(foregroundEvents))).toContain("spawned"); + + const liveEvents = await collectSubscribedUntil( handle.session, - [ - "Use the Task tool to start a background sub-agent.", - "In that task, run the Bash command exactly: sleep 3 && echo BACKGROUND_DONE", - "Do not wait for task completion.", - "Reply immediately with exactly: SPAWNED", - `When the background task completes later, reply with exactly: ${autonomousWakeToken}`, - ].join(" "), - ), - { timeoutMs: 45_000 }, - ); - - expect(compactText(getAssistantText(foregroundEvents))).toContain("spawned"); - - const liveEvents = await collectSubscribedUntil( - handle.session, - (event) => isTerminalEvent(event), - 45_000, - ); - - expect( - liveEvents.some((event) => event.type === "turn_started" && event.provider === "claude"), - ).toBe(true); - expect(compactText(getAssistantText(liveEvents))).toContain( - autonomousWakeToken.toLowerCase(), - ); - expect(liveEvents.at(-1)).toMatchObject({ - type: "turn_completed", - provider: "claude", - }); - } finally { - await cleanupSession(handle); - } + (event) => isTerminalEvent(event), + 45_000, + ); + + expect( + liveEvents.some((event) => event.type === "turn_started" && event.provider === "claude"), + ).toBe(true); + expect(compactText(getAssistantText(liveEvents))).toContain( + autonomousWakeToken.toLowerCase(), + ); + expect(liveEvents.at(-1)).toMatchObject({ + type: "turn_completed", + provider: "claude", + }); + } finally { + await cleanupSession(handle); + } }, 60_000, ); - test.runIf(canRunClaudeIntegration)("surfaces permission requests and resumes after approval", async () => { - const handle = await createSession({ - cwdPrefix: "claude-agent-permission-", - modeId: "default", - }); - const permissionFile = path.join(handle.cwd, "permission.txt"); + test.runIf(canRunClaudeIntegration)( + "surfaces permission requests and resumes after approval", + async () => { + const handle = await createSession({ + cwdPrefix: "claude-agent-permission-", + modeId: "default", + }); + const permissionFile = path.join(handle.cwd, "permission.txt"); - try { - const events = await collectUntilTerminal( - streamSession( - handle.session, - [ - "Use the Bash tool to run exactly: printf 'PERM_TEST' > permission.txt", - "If approval is required, wait for approval.", - "After the command succeeds, reply with exactly: PERM_DONE", - ].join(" "), - ), - { - timeoutMs: 45_000, - onEvent: async (event) => { - if (event.type !== "permission_requested") { - return; - } - await handle.session.respondToPermission(event.request.id, { - behavior: "allow", - }); + try { + const events = await collectUntilTerminal( + streamSession( + handle.session, + [ + "Use the Bash tool to run exactly: printf 'PERM_TEST' > permission.txt", + "If approval is required, wait for approval.", + "After the command succeeds, reply with exactly: PERM_DONE", + ].join(" "), + ), + { + timeoutMs: 45_000, + onEvent: async (event) => { + if (event.type !== "permission_requested") { + return; + } + await handle.session.respondToPermission(event.request.id, { + behavior: "allow", + }); + }, }, - }, - ); - - const permissionRequest = events.find( - (event): event is Extract => - event.type === "permission_requested", - ); - const permissionResolved = events.find( - (event): event is Extract => - event.type === "permission_resolved", - ); - const completedBashCall = getLatestCompletedBashCall(events); - - expect(permissionRequest?.request.kind).toBe("tool"); - expect(permissionResolved).toMatchObject({ - type: "permission_resolved", - provider: "claude", - resolution: { behavior: "allow" }, - }); - expect(completedBashCall).toBeDefined(); - expect(readFileSync(permissionFile, "utf8")).toBe("PERM_TEST"); - expect(compactText(getAssistantText(events))).toContain("perm_done"); - expect(events.at(-1)).toMatchObject({ - type: "turn_completed", - provider: "claude", - }); - } finally { - await cleanupSession(handle); - } - }, 60_000); + ); + + const permissionRequest = events.find( + (event): event is Extract => + event.type === "permission_requested", + ); + const permissionResolved = events.find( + (event): event is Extract => + event.type === "permission_resolved", + ); + const completedBashCall = getLatestCompletedBashCall(events); + + expect(permissionRequest?.request.kind).toBe("tool"); + expect(permissionResolved).toMatchObject({ + type: "permission_resolved", + provider: "claude", + resolution: { behavior: "allow" }, + }); + expect(completedBashCall).toBeDefined(); + expect(readFileSync(permissionFile, "utf8")).toBe("PERM_TEST"); + expect(compactText(getAssistantText(events))).toContain("perm_done"); + expect(events.at(-1)).toMatchObject({ + type: "turn_completed", + provider: "claude", + }); + } finally { + await cleanupSession(handle); + } + }, + 60_000, + ); }); diff --git a/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts b/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts index b33d82758..2a23ef455 100644 --- a/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts @@ -212,7 +212,9 @@ function collectAssistantText(events: AgentStreamEvent[]): string { .join(""); } -function subscribeToEvents(session: { subscribe: (callback: (event: AgentStreamEvent) => void) => () => void }) { +function subscribeToEvents(session: { + subscribe: (callback: (event: AgentStreamEvent) => void) => () => void; +}) { const queue = createAsyncQueue(); const unsubscribe = session.subscribe((event) => { queue.push(event); @@ -484,9 +486,9 @@ describe("ClaudeAgentSession interrupt regression", () => { expect(secondTurnEvents.some((event) => event.type === "turn_canceled")).toBe(false); expect(secondTurnEvents.some((event) => event.type === "turn_completed")).toBe(true); expect(collectAssistantText(secondTurnEvents)).toContain("SECOND_PROMPT_RESPONSE"); - expect( - observedSecondTurnEvents.filter((event) => event.type === "turn_started").length, - ).toBe(1); + expect(observedSecondTurnEvents.filter((event) => event.type === "turn_started").length).toBe( + 1, + ); expect( observedSecondTurnEvents.some( (event) => event.type === "turn_failed" || event.type === "turn_canceled", diff --git a/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts b/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts index 0f490c08a..cdbc1a696 100644 --- a/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts @@ -806,9 +806,7 @@ describe("ClaudeAgentSession redesign invariants", () => { effort: options.effort, }); - return createBaseQueryMock( - vi.fn(async () => ({ done: true, value: undefined })), - ); + return createBaseQueryMock(vi.fn(async () => ({ done: true, value: undefined }))); }, ); diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 364a8af77..9bcb45727 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -9,7 +9,6 @@ import { type AgentDefinition, type CanUseTool, type McpServerConfig as ClaudeSdkMcpServerConfig, - type Options, type PermissionMode, type PermissionResult, @@ -34,10 +33,7 @@ import { mapTaskNotificationSystemRecordToToolCall, mapTaskNotificationUserContentToToolCall, } from "./claude/task-notification-tool-call.js"; -import { - getClaudeModels, - normalizeClaudeRuntimeModelId, -} from "./claude/claude-models.js"; +import { getClaudeModels, normalizeClaudeRuntimeModelId } from "./claude/claude-models.js"; import { parsePartialJsonObject } from "./claude/partial-json.js"; import { ClaudeSidechainTracker } from "./claude/sidechain-tracker.js"; import { @@ -75,10 +71,7 @@ import type { McpServerConfig, PersistedAgentDescriptor, } from "../agent-sdk-types.js"; -import { - applyProviderEnv, - type ProviderRuntimeSettings, -} from "../provider-launch-config.js"; +import { applyProviderEnv, type ProviderRuntimeSettings } from "../provider-launch-config.js"; import { findExecutable, quoteWindowsArgument, @@ -222,21 +215,20 @@ function applyRuntimeSettingsToClaudeOptions( // PATH lookup failures in the managed runtime bundle. // When the SDK passes a native binary path (from pathToClaudeCodeExecutable) // or the user overrides the command via runtime settings, use that directly. - const isDefaultRuntime = - resolved.command === "node" || resolved.command === "bun"; + const isDefaultRuntime = resolved.command === "node" || resolved.command === "bun"; const command = isDefaultRuntime ? process.execPath : resolved.command; const child = spawn( quoteWindowsCommand(command), resolved.args.map((argument) => quoteWindowsArgument(argument)), { - cwd: spawnOptions.cwd, - env: { - ...applyProviderEnv(spawnOptions.env, runtimeSettings), - ...(launchEnv ?? {}), - }, - shell: process.platform === "win32", - signal: spawnOptions.signal, - stdio: ["pipe", "pipe", "pipe"], + cwd: spawnOptions.cwd, + env: { + ...applyProviderEnv(spawnOptions.env, runtimeSettings), + ...(launchEnv ?? {}), + }, + shell: process.platform === "win32", + signal: spawnOptions.signal, + stdio: ["pipe", "pipe", "pipe"], }, ); if (typeof options.stderr === "function") { @@ -1096,7 +1088,6 @@ export class ClaudeAgentClient implements AgentClient { async listModels(_options?: ListModelsOptions): Promise { return getClaudeModels(); - } async listPersistedAgents( @@ -1181,10 +1172,12 @@ function resolveClaudeVersion(runtimeSettings?: ProviderRuntimeSettings): string try { if (command?.mode === "replace") { - return execFileSync(command.argv[0]!, [...command.argv.slice(1), "--version"], { - encoding: "utf8", - timeout: 5_000, - }).trim() || null; + return ( + execFileSync(command.argv[0]!, [...command.argv.slice(1), "--version"], { + encoding: "utf8", + timeout: 5_000, + }).trim() || null + ); } const executable = findExecutable("claude"); @@ -1192,10 +1185,12 @@ function resolveClaudeVersion(runtimeSettings?: ProviderRuntimeSettings): string return null; } - return execFileSync(executable, ["--version"], { - encoding: "utf8", - timeout: 5_000, - }).trim() || null; + return ( + execFileSync(executable, ["--version"], { + encoding: "utf8", + timeout: 5_000, + }).trim() || null + ); } catch { return null; } @@ -2030,10 +2025,9 @@ class ClaudeAgentSession implements AgentSession { : process.env["PATH"] !== undefined ? "PATH" : null, - pathIncludesClaudeLocalBin: - (process.env["Path"] ?? process.env["PATH"] ?? "") - .toLowerCase() - .includes("\\.local\\bin"), + pathIncludesClaudeLocalBin: (process.env["Path"] ?? process.env["PATH"] ?? "") + .toLowerCase() + .includes("\\.local\\bin"), }, "Resolved Claude executable", ); @@ -2165,8 +2159,7 @@ class ClaudeAgentSession implements AgentSession { } private isAbortError(message: SDKMessage): boolean { - const errors = - "errors" in message && Array.isArray(message.errors) ? message.errors : []; + const errors = "errors" in message && Array.isArray(message.errors) ? message.errors : []; return errors.some((e: string) => /\baborted\b/i.test(e)); } @@ -2207,9 +2200,11 @@ class ClaudeAgentSession implements AgentSession { if (this.getRecentStderrDiagnostic()) { return; } - const message = - typeof error === "string" ? error : error instanceof Error ? error.message : ""; - if (!/\bprocess exited with code\b/i.test(message) && !/\bterminated by signal\b/i.test(message)) { + const message = typeof error === "string" ? error : error instanceof Error ? error.message : ""; + if ( + !/\bprocess exited with code\b/i.test(message) && + !/\bterminated by signal\b/i.test(message) + ) { return; } @@ -2455,11 +2450,7 @@ class ClaudeAgentSession implements AgentSession { return; } } - if ( - message.type === "result" && - message.subtype !== "success" && - this.isAbortError(message) - ) { + if (message.type === "result" && message.subtype !== "success" && this.isAbortError(message)) { this.logger.debug("Suppressing abort result by content"); return; } @@ -2913,9 +2904,7 @@ class ClaudeAgentSession implements AgentSession { outputTokens: message.usage.output_tokens, totalCostUsd: message.total_cost_usd, }; - const contextWindowMaxTokens = extractContextWindowSize( - modelUsage ?? message.modelUsage, - ); + const contextWindowMaxTokens = extractContextWindowSize(modelUsage ?? message.modelUsage); if (contextWindowMaxTokens !== undefined) { usage.contextWindowMaxTokens = contextWindowMaxTokens; } @@ -2969,8 +2958,7 @@ class ClaudeAgentSession implements AgentSession { input, detail: toolDetail, suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })), - actions: - kind === "plan" ? buildClaudePlanPermissionActions(this.planResumeMode) : undefined, + actions: kind === "plan" ? buildClaudePlanPermissionActions(this.planResumeMode) : undefined, metadata: Object.keys(metadata).length ? metadata : undefined, }; diff --git a/packages/server/src/server/agent/providers/claude-sdk-behavior.test.ts b/packages/server/src/server/agent/providers/claude-sdk-behavior.test.ts index 2b330830b..d0d809c07 100644 --- a/packages/server/src/server/agent/providers/claude-sdk-behavior.test.ts +++ b/packages/server/src/server/agent/providers/claude-sdk-behavior.test.ts @@ -65,88 +65,92 @@ describe("Claude SDK direct behavior", () => { } }); - test.runIf(canRunClaudeIntegration)("shows what happens after interrupt()", async () => { - const cwd = tmpCwd(); - const input = new Pushable(); - const claudeBinary = findExecutable("claude"); - - // Use same options as claude-agent.ts - const q = query({ - prompt: input, - options: { - cwd, - includePartialMessages: true, - permissionMode: "bypassPermissions", - ...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}), - systemPrompt: { - type: "preset", - preset: "claude_code", + test.runIf(canRunClaudeIntegration)( + "shows what happens after interrupt()", + async () => { + const cwd = tmpCwd(); + const input = new Pushable(); + const claudeBinary = findExecutable("claude"); + + // Use same options as claude-agent.ts + const q = query({ + prompt: input, + options: { + cwd, + includePartialMessages: true, + permissionMode: "bypassPermissions", + ...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}), + systemPrompt: { + type: "preset", + preset: "claude_code", + }, + settingSources: ["user", "project"], }, - settingSources: ["user", "project"], - }, - }); - - try { - // Send first message - input.push({ - type: "user", - message: { role: "user", content: "Say exactly: MESSAGE_ONE" }, - parent_tool_use_id: null, - session_id: "", }); - // Collect events until we see assistant, then interrupt - const msg1Events: SDKMessage[] = []; - for await (const event of q) { - msg1Events.push(event); - - if (event.type === "assistant") { - // Push MSG2 BEFORE interrupt (like our wrapper does when a new message comes in) - input.push({ - type: "user", - message: { role: "user", content: "Say exactly: MESSAGE_TWO" }, - parent_tool_use_id: null, - session_id: "", - }); - await q.interrupt(); - break; - } - if (event.type === "result") { - break; + try { + // Send first message + input.push({ + type: "user", + message: { role: "user", content: "Say exactly: MESSAGE_ONE" }, + parent_tool_use_id: null, + session_id: "", + }); + + // Collect events until we see assistant, then interrupt + const msg1Events: SDKMessage[] = []; + for await (const event of q) { + msg1Events.push(event); + + if (event.type === "assistant") { + // Push MSG2 BEFORE interrupt (like our wrapper does when a new message comes in) + input.push({ + type: "user", + message: { role: "user", content: "Say exactly: MESSAGE_TWO" }, + parent_tool_use_id: null, + session_id: "", + }); + await q.interrupt(); + break; + } + if (event.type === "result") { + break; + } } - } - // MSG2 was already pushed before interrupt - const msg2Events: SDKMessage[] = []; - for await (const event of q) { - msg2Events.push(event); + // MSG2 was already pushed before interrupt + const msg2Events: SDKMessage[] = []; + for await (const event of q) { + msg2Events.push(event); - if (event.type === "result") { - break; + if (event.type === "result") { + break; + } } - } - // Analyze response - let responseText = ""; - for (const event of msg2Events) { - if (event.type === "assistant" && "message" in event && event.message?.content) { - const content = event.message.content; - if (Array.isArray(content)) { - for (const block of content) { - if (block.type === "text" && block.text) { - responseText += block.text; + // Analyze response + let responseText = ""; + for (const event of msg2Events) { + if (event.type === "assistant" && "message" in event && event.message?.content) { + const content = event.message.content; + if (Array.isArray(content)) { + for (const block of content) { + if (block.type === "text" && block.text) { + responseText += block.text; + } } } } } - } - const sawResult = msg2Events.some((event) => event.type === "result"); - // The SDK may short-circuit after interrupt without a result event. - expect(sawResult || responseText.length === 0).toBe(true); - } finally { - input.end(); - rmSync(cwd, { recursive: true, force: true }); - } - }, 120000); + const sawResult = msg2Events.some((event) => event.type === "result"); + // The SDK may short-circuit after interrupt without a result event. + expect(sawResult || responseText.length === 0).toBe(true); + } finally { + input.end(); + rmSync(cwd, { recursive: true, force: true }); + } + }, + 120000, + ); }); diff --git a/packages/server/src/server/agent/providers/claude/claude-models.ts b/packages/server/src/server/agent/providers/claude/claude-models.ts index 08c14071e..3d11cf7fa 100644 --- a/packages/server/src/server/agent/providers/claude/claude-models.ts +++ b/packages/server/src/server/agent/providers/claude/claude-models.ts @@ -45,9 +45,7 @@ export function getClaudeModels(): AgentModelDefinition[] { * Normalize a runtime model string (from SDK init message) to a known model ID. * Handles the `[1m]` suffix that the SDK appends for 1M context sessions. */ -export function normalizeClaudeRuntimeModelId( - value: string | null | undefined, -): string | null { +export function normalizeClaudeRuntimeModelId(value: string | null | undefined): string | null { const trimmed = typeof value === "string" ? value.trim() : ""; if (!trimmed) { return null; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts index 89d1b0016..dfad94986 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts @@ -61,7 +61,11 @@ function responseCompleted(id: string): Record { }; } -function functionCallEvent(callId: string, name: string, argumentsJson: string): Record { +function functionCallEvent( + callId: string, + name: string, + argumentsJson: string, +): Record { return { type: "response.output_item.done", item: { @@ -116,7 +120,11 @@ function requestUserInputSse(callId: string): string { } function assistantMessageSse(text: string): string { - return sse([responseCreated("resp-2"), assistantMessageEvent("msg-1", text), responseCompleted("resp-2")]); + return sse([ + responseCreated("resp-2"), + assistantMessageEvent("msg-1", text), + responseCompleted("resp-2"), + ]); } async function startMockResponsesServer(sequence: string[]): Promise<{ @@ -276,10 +284,7 @@ describe("Codex app-server provider (e2e)", () => { label: "question permission request", predicate: ( event, - ): event is Extract< - AgentStreamEvent, - { type: "permission_requested" } - > => + ): event is Extract => event.type === "permission_requested" && event.request.provider === "codex" && event.request.kind === "question" && diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 813a201c1..d0c18e9df 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test, vi } from "vitest"; import { existsSync, rmSync } from "node:fs"; -import type { AgentLaunchContext, AgentSession, AgentSessionConfig, AgentStreamEvent } from "../agent-sdk-types.js"; +import type { + AgentLaunchContext, + AgentSession, + AgentSessionConfig, + AgentStreamEvent, +} from "../agent-sdk-types.js"; import { __codexAppServerInternals, codexAppServerTurnInputFromPrompt, @@ -356,10 +361,7 @@ describe("Codex app-server provider", () => { id: "favorite_drink", header: "Drink", question: "Which drink do you want?", - options: [ - { label: "Coffee", description: "Default" }, - { label: "Tea" }, - ], + options: [{ label: "Coffee", description: "Default" }, { label: "Tea" }], }, ], }); @@ -386,10 +388,7 @@ describe("Codex app-server provider", () => { id: "favorite_drink", header: "Drink", question: "Which drink do you want?", - options: [ - { label: "Coffee", description: "Default" }, - { label: "Tea" }, - ], + options: [{ label: "Coffee", description: "Default" }, { label: "Tea" }], }, ], }, @@ -416,10 +415,7 @@ describe("Codex app-server provider", () => { id: "favorite_drink", header: "Drink", question: "Which drink do you want?", - options: [ - { label: "Coffee", description: "Default" }, - { label: "Tea" }, - ], + options: [{ label: "Coffee", description: "Default" }, { label: "Tea" }], }, ], }, @@ -432,10 +428,7 @@ describe("Codex app-server provider", () => { id: "favorite_drink", header: "Drink", question: "Which drink do you want?", - options: [ - { label: "Coffee", description: "Default" }, - { label: "Tea" }, - ], + options: [{ label: "Coffee", description: "Default" }, { label: "Tea" }], }, ], }, @@ -678,8 +671,7 @@ describe("Codex app-server provider", () => { expect(session.getPendingPermissions()).toEqual([request.request]); expect( events.some( - (event) => - event.type === "permission_resolved" && event.requestId === request.request.id, + (event) => event.type === "permission_resolved" && event.requestId === request.request.id, ), ).toBe(false); }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 141a05d96..61e2bde66 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -159,10 +159,7 @@ function isObjectSchemaNode(schema: Record): boolean { ); } -function normalizeCodexOutputSchemaNode( - schema: unknown, - path: string, -): unknown { +function normalizeCodexOutputSchemaNode(schema: unknown, path: string): unknown { if (Array.isArray(schema)) { return schema.map((entry, index) => normalizeCodexOutputSchemaNode(entry, `${path}[${index}]`)); } @@ -821,7 +818,10 @@ function planStepsToMarkdown(steps: Array<{ step: string; status: string }>): st return normalizePlanMarkdown(lines.join("\n")); } -function mapCodexPlanToToolCall(params: { callId: string; text: string }): ToolCallTimelineItem | null { +function mapCodexPlanToToolCall(params: { + callId: string; + text: string; +}): ToolCallTimelineItem | null { const text = normalizePlanMarkdown(params.text); if (!text) { return null; @@ -839,9 +839,10 @@ function mapCodexPlanToToolCall(params: { callId: string; text: string }): ToolC }; } -function buildPlanPermissionActions( - options?: { includeResumeAction?: boolean; resumeLabel?: string }, -): AgentPermissionAction[] { +function buildPlanPermissionActions(options?: { + includeResumeAction?: boolean; + resumeLabel?: string; +}): AgentPermissionAction[] { const actions: AgentPermissionAction[] = [ { id: "reject", @@ -2561,9 +2562,7 @@ class CodexAppServerAgentSession implements AgentSession { } } - private findCollaborationMode( - target: "code" | "plan", - ): { + private findCollaborationMode(target: "code" | "plan"): { name: string; mode?: string | null; model?: string | null; @@ -3187,7 +3186,9 @@ class CodexAppServerAgentSession implements AgentSession { const questions = pending.questions ?? []; const itemId = - typeof pendingRequest?.metadata?.itemId === "string" ? pendingRequest.metadata.itemId : requestId; + typeof pendingRequest?.metadata?.itemId === "string" + ? pendingRequest.metadata.itemId + : requestId; if (response.behavior === "allow") { const mappedAnswers = mapCodexQuestionResponseByHeader({ questions, @@ -3199,9 +3200,7 @@ class CodexAppServerAgentSession implements AgentSession { questions .map((question) => { const fallback = question.options[0]?.label?.trim(); - return fallback - ? [question.id, { answers: [fallback] }] - : null; + return fallback ? [question.id, { answers: [fallback] }] : null; }) .filter((entry): entry is [string, { answers: string[] }] => entry !== null), ); @@ -4212,7 +4211,10 @@ export class CodexAppServerAgentClient implements AgentClient { label: "Binary", value: resolvedBinary ?? "not found", }, - { label: "Version", value: resolvedBinary ? resolveBinaryVersion(resolvedBinary) : "unknown" }, + { + label: "Version", + value: resolvedBinary ? resolveBinaryVersion(resolvedBinary) : "unknown", + }, ]; let status = formatDiagnosticStatus(available); diff --git a/packages/server/src/server/agent/providers/codex-feature-definitions.ts b/packages/server/src/server/agent/providers/codex-feature-definitions.ts index 16554d3e4..a850317e5 100644 --- a/packages/server/src/server/agent/providers/codex-feature-definitions.ts +++ b/packages/server/src/server/agent/providers/codex-feature-definitions.ts @@ -1,11 +1,6 @@ import type { AgentFeature, AgentFeatureToggle } from "../agent-sdk-types.js"; -const CODEX_FAST_MODE_SUPPORTED_MODEL_PREFIXES = [ - "gpt-5", - "gpt-4.1", - "o3", - "o4-mini", -] as const; +const CODEX_FAST_MODE_SUPPORTED_MODEL_PREFIXES = ["gpt-5", "gpt-4.1", "o3", "o4-mini"] as const; export const CODEX_FAST_MODE_FEATURE: Omit = { type: "toggle", diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index a5fd872f2..5b6ba5570 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -733,9 +733,10 @@ function parseFileChangeEntries( .filter((entry): entry is CodexFileChangeEntry => entry !== null); } -function resolveFileChangeTextFields( - file: CodexFileChangeEntry | undefined, -): { unifiedDiff?: string; newString?: string } { +function resolveFileChangeTextFields(file: CodexFileChangeEntry | undefined): { + unifiedDiff?: string; + newString?: string; +} { if (!file) { return {}; } diff --git a/packages/server/src/server/agent/providers/copilot-acp-agent.ts b/packages/server/src/server/agent/providers/copilot-acp-agent.ts index 395508336..23928ab7d 100644 --- a/packages/server/src/server/agent/providers/copilot-acp-agent.ts +++ b/packages/server/src/server/agent/providers/copilot-acp-agent.ts @@ -97,7 +97,10 @@ export class CopilotACPAgentClient extends ACPAgentClient { label: "Binary", value: resolvedBinary ?? "not found", }, - { label: "Version", value: resolvedBinary ? resolveBinaryVersion(resolvedBinary) : "unknown" }, + { + label: "Version", + value: resolvedBinary ? resolveBinaryVersion(resolvedBinary) : "unknown", + }, { label: "Models", value: modelsValue }, { label: "Status", value: status }, ]), diff --git a/packages/server/src/server/agent/providers/diagnostic-utils.ts b/packages/server/src/server/agent/providers/diagnostic-utils.ts index af077b271..71a39f90a 100644 --- a/packages/server/src/server/agent/providers/diagnostic-utils.ts +++ b/packages/server/src/server/agent/providers/diagnostic-utils.ts @@ -7,17 +7,11 @@ type DiagnosticEntry = { value: string; }; -export function formatProviderDiagnostic( - providerName: string, - entries: DiagnosticEntry[], -): string { +export function formatProviderDiagnostic(providerName: string, entries: DiagnosticEntry[]): string { return [providerName, ...entries.map((entry) => ` ${entry.label}: ${entry.value}`)].join("\n"); } -export function formatProviderDiagnosticError( - providerName: string, - error: unknown, -): string { +export function formatProviderDiagnosticError(providerName: string, error: unknown): string { return formatProviderDiagnostic(providerName, [ { label: "Error", diff --git a/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts index 13ef380d0..4edaaaad6 100644 --- a/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts @@ -79,10 +79,7 @@ describe("opencode agent commands E2E", () => { }); const token = `RAW_PROMPT_TOKEN_${Date.now()}`; - await ctx.client.sendMessage( - agent.id, - `/not-a-real-command respond with exactly: ${token}`, - ); + await ctx.client.sendMessage(agent.id, `/not-a-real-command respond with exactly: ${token}`); const state = await ctx.client.waitForFinish(agent.id, 30_000); expect(state.status).toBe("idle"); diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 7965c96f1..103816607 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -1,9 +1,6 @@ import { spawn, type ChildProcess } from "node:child_process"; import { existsSync } from "node:fs"; -import { - createOpencodeClient, - type OpencodeClient, -} from "@opencode-ai/sdk/v2/client"; +import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client"; import net from "node:net"; import type { Logger } from "pino"; import { z } from "zod"; @@ -355,13 +352,16 @@ function buildOpenCodeModelDefinition( } function resolveOpenCodeSelectedModelContextWindow( - providers: { - connected?: string[]; - all?: Array<{ - id: string; - models?: Record; - }>; - } | null | undefined, + providers: + | { + connected?: string[]; + all?: Array<{ + id: string; + models?: Record; + }>; + } + | null + | undefined, modelId: string | null | undefined, ): number | undefined { if (!providers) { @@ -375,13 +375,18 @@ function resolveOpenCodeSelectedModelContextWindow( return lookup.get(modelLookupKey); } -function buildOpenCodeModelContextWindowLookup(providers: { - connected?: string[]; - all?: Array<{ - id: string; - models?: Record; - }>; -} | null | undefined): Map { +function buildOpenCodeModelContextWindowLookup( + providers: + | { + connected?: string[]; + all?: Array<{ + id: string; + models?: Record; + }>; + } + | null + | undefined, +): Map { const lookup = new Map(); if (!providers) { return lookup; @@ -800,16 +805,10 @@ export class OpenCodeAgentClient implements AgentClient { const client = createOpencodeClient({ baseUrl: url, directory }); const timeoutPromise = new Promise((_, reject) => { - setTimeout( - () => reject(new Error("OpenCode app.agents timed out after 10s")), - 10_000, - ); + setTimeout(() => reject(new Error("OpenCode app.agents timed out after 10s")), 10_000); }); - const response = await Promise.race([ - client.app.agents({ directory }), - timeoutPromise, - ]); + const response = await Promise.race([client.app.agents({ directory }), timeoutPromise]); if (response.error || !response.data) { return DEFAULT_MODES; @@ -889,7 +888,10 @@ export class OpenCodeAgentClient implements AgentClient { label: "Binary", value: resolvedBinary ?? "not found", }, - { label: "Version", value: resolvedBinary ? resolveBinaryVersion(resolvedBinary) : "unknown" }, + { + label: "Version", + value: resolvedBinary ? resolveBinaryVersion(resolvedBinary) : "unknown", + }, { label: "Server", value: serverStatus }, { label: "Models", value: modelsValue }, { label: "Status", value: status }, @@ -1155,9 +1157,7 @@ export function translateOpenCodeEvent( } const deltaMessageId = props.messageID as string | undefined; - const deltaMessageRole = deltaMessageId - ? state.messageRoles.get(deltaMessageId) - : undefined; + const deltaMessageRole = deltaMessageId ? state.messageRoles.get(deltaMessageId) : undefined; const deltaField = props.field as string | undefined; const deltaText = props.delta as string | undefined; @@ -1354,8 +1354,9 @@ class OpenCodeAgentSession implements AgentSession { this.logger = logger; this.modelContextWindowsByModelKey = modelContextWindowsByModelKey; this.currentMode = normalizeOpenCodeModeId(config.modeId); - this.selectedModelContextWindowMaxTokens = - this.resolveConfiguredModelContextWindowMaxTokens(config.model); + this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens( + config.model, + ); } get id(): string | null { @@ -1375,8 +1376,9 @@ class OpenCodeAgentSession implements AgentSession { const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0 ? modelId : null; this.config.model = normalizedModelId ?? undefined; - this.selectedModelContextWindowMaxTokens = - this.resolveConfiguredModelContextWindowMaxTokens(this.config.model); + this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens( + this.config.model, + ); } async setThinkingOption(thinkingOptionId: string | null): Promise { @@ -1491,8 +1493,7 @@ class OpenCodeAgentSession implements AgentSession { this.abortController = turnAbortController; await this.ensureMcpServersConfigured(); const contextWindowMaxTokens = this.resolveSelectedModelContextWindowMaxTokens(); - this.accumulatedUsage = - contextWindowMaxTokens !== undefined ? { contextWindowMaxTokens } : {}; + this.accumulatedUsage = contextWindowMaxTokens !== undefined ? { contextWindowMaxTokens } : {}; const parts = this.buildPromptParts(prompt); const model = this.parseModel(this.config.model); @@ -1508,23 +1509,25 @@ class OpenCodeAgentSession implements AgentSession { if (slashCommand) { // command() blocks until completion, but events stream via SSE in the // background — fire without awaiting so the event stream isn't starved. - void this.client.session.command({ - sessionID: this.sessionId, - directory: this.config.cwd, - command: slashCommand.commandName, - arguments: slashCommand.args ?? "", - ...(this.config.model ? { model: this.config.model } : {}), - ...(effectiveMode ? { agent: effectiveMode } : {}), - ...(effectiveVariant ? { variant: effectiveVariant } : {}), - }).then((response) => { - if (response.error) { - const errorMsg = normalizeTurnFailureError(response.error); - this.finishForegroundTurn( - { type: "turn_failed", provider: "opencode", error: errorMsg }, - turnId, - ); - } - }); + void this.client.session + .command({ + sessionID: this.sessionId, + directory: this.config.cwd, + command: slashCommand.commandName, + arguments: slashCommand.args ?? "", + ...(this.config.model ? { model: this.config.model } : {}), + ...(effectiveMode ? { agent: effectiveMode } : {}), + ...(effectiveVariant ? { variant: effectiveVariant } : {}), + }) + .then((response) => { + if (response.error) { + const errorMsg = normalizeTurnFailureError(response.error); + this.finishForegroundTurn( + { type: "turn_failed", provider: "opencode", error: errorMsg }, + turnId, + ); + } + }); } else { const promptResponse = await this.client.session.promptAsync({ sessionID: this.sessionId, @@ -1587,7 +1590,11 @@ class OpenCodeAgentSession implements AgentSession { if (e.type === "timeline" && e.item.type === "tool_call") { this.trackToolCall(e.item); } - if (e.type === "turn_completed" || e.type === "turn_failed" || e.type === "turn_canceled") { + if ( + e.type === "turn_completed" || + e.type === "turn_failed" || + e.type === "turn_canceled" + ) { if (e.type === "turn_failed") { this.finishForegroundTurn( { diff --git a/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts index 869cac313..f13fedf11 100644 --- a/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts @@ -26,9 +26,7 @@ describe("OpenCode assistant message", () => { const result = await session.run("Say hello back in one sentence."); - const assistantItems = result.timeline.filter( - (item) => item.type === "assistant_message", - ); + const assistantItems = result.timeline.filter((item) => item.type === "assistant_message"); expect(assistantItems.length).toBeGreaterThan(0); expect(result.finalText.length).toBeGreaterThan(0); } finally { @@ -69,5 +67,4 @@ describe("OpenCode assistant message", () => { }, 60_000, ); - }); diff --git a/packages/server/src/server/agent/providers/pi-acp-agent.ts b/packages/server/src/server/agent/providers/pi-acp-agent.ts index 7b3d99ad1..962763899 100644 --- a/packages/server/src/server/agent/providers/pi-acp-agent.ts +++ b/packages/server/src/server/agent/providers/pi-acp-agent.ts @@ -3,11 +3,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import type { Logger } from "pino"; -import type { - ClientSideConnection, - SessionConfigOption, - ToolKind, -} from "@agentclientprotocol/sdk"; +import type { ClientSideConnection, SessionConfigOption, ToolKind } from "@agentclientprotocol/sdk"; import type { AgentLaunchContext, @@ -30,11 +26,7 @@ import type { } from "../agent-sdk-types.js"; import type { ProviderRuntimeSettings } from "../provider-launch-config.js"; import { findExecutable, isCommandAvailable } from "../../../utils/executable.js"; -import { - ACPAgentClient, - type ACPToolSnapshot, - type SessionStateResponse, -} from "./acp-agent.js"; +import { ACPAgentClient, type ACPToolSnapshot, type SessionStateResponse } from "./acp-agent.js"; import { formatDiagnosticStatus, formatProviderDiagnostic, @@ -102,9 +94,7 @@ function transformPiToolSnapshot(snapshot: ACPToolSnapshot): ACPToolSnapshot { * This transformer remaps them so the base ACP class treats them as thinking * options instead of permission modes. */ -export function transformPiSessionResponse( - response: SessionStateResponse, -): SessionStateResponse { +export function transformPiSessionResponse(response: SessionStateResponse): SessionStateResponse { const modes = response.modes; if (!modes?.availableModes?.length) { return response; @@ -126,10 +116,7 @@ export function transformPiSessionResponse( return { ...response, modes: undefined, - configOptions: [ - thinkingOption, - ...(response.configOptions ?? []), - ], + configOptions: [thinkingOption, ...(response.configOptions ?? [])], }; } @@ -228,10 +215,7 @@ class PiACPAgentSession implements AgentSession { return this.inner.getPendingPermissions(); } - async respondToPermission( - requestId: string, - response: AgentPermissionResponse, - ): Promise { + async respondToPermission(requestId: string, response: AgentPermissionResponse): Promise { await this.inner.respondToPermission(requestId, response); } diff --git a/packages/server/src/server/chat/chat-service.ts b/packages/server/src/server/chat/chat-service.ts index 3bf31006a..bdee71dc8 100644 --- a/packages/server/src/server/chat/chat-service.ts +++ b/packages/server/src/server/chat/chat-service.ts @@ -260,7 +260,9 @@ export class FileBackedChatService { if (existing.length > 0) { return existing; } - const knownMessage = this.getRoomMessages(room.id).some((message) => message.id === afterMessageId); + const knownMessage = this.getRoomMessages(room.id).some( + (message) => message.id === afterMessageId, + ); if (!knownMessage) { throw new ChatServiceError( "chat_message_not_found", @@ -341,7 +343,9 @@ export class FileBackedChatService { private async persist(): Promise { const payload: ChatStorePayload = { - rooms: Array.from(this.rooms.values()).sort((left, right) => left.createdAt.localeCompare(right.createdAt)), + rooms: Array.from(this.rooms.values()).sort((left, right) => + left.createdAt.localeCompare(right.createdAt), + ), messages: Array.from(this.messagesByRoomId.values()) .flat() .sort((left, right) => left.createdAt.localeCompare(right.createdAt)), diff --git a/packages/server/src/server/checkout-diff-manager.test.ts b/packages/server/src/server/checkout-diff-manager.test.ts index 22f0fda8c..b683cd38a 100644 --- a/packages/server/src/server/checkout-diff-manager.test.ts +++ b/packages/server/src/server/checkout-diff-manager.test.ts @@ -5,9 +5,15 @@ const { execMock, getCheckoutDiffMock, resolveCheckoutGitDirMock, readdirMock, w vi.hoisted(() => { const hoistedWatchCalls: Array<{ path: string; close: ReturnType }> = []; return { - execMock: vi.fn((_command: string, _options: unknown, callback: (error: null, result: { stdout: string; stderr: string }) => void) => { - callback(null, { stdout: "/tmp/repo\n", stderr: "" }); - }), + execMock: vi.fn( + ( + _command: string, + _options: unknown, + callback: (error: null, result: { stdout: string; stderr: string }) => void, + ) => { + callback(null, { stdout: "/tmp/repo\n", stderr: "" }); + }, + ), getCheckoutDiffMock: vi.fn(async () => ({ diff: "", structured: [] })), resolveCheckoutGitDirMock: vi.fn(async () => "/tmp/repo/.git"), readdirMock: vi.fn(async (directory: string) => { diff --git a/packages/server/src/server/config.ts b/packages/server/src/server/config.ts index a0a5dddf8..4f9e47d30 100644 --- a/packages/server/src/server/config.ts +++ b/packages/server/src/server/config.ts @@ -51,7 +51,10 @@ export function loadConfig( // - unix:///path/to/socket (Unix socket) // Default is TCP at 127.0.0.1:6767 const listen = - options?.cli?.listen ?? env.PASEO_LISTEN ?? persisted.daemon?.listen ?? `127.0.0.1:${env.PORT ?? DEFAULT_PORT}`; + options?.cli?.listen ?? + env.PASEO_LISTEN ?? + persisted.daemon?.listen ?? + `127.0.0.1:${env.PORT ?? DEFAULT_PORT}`; const envCorsOrigins = env.PASEO_CORS_ORIGINS ? env.PASEO_CORS_ORIGINS.split(",").map((s) => s.trim()) diff --git a/packages/server/src/server/daemon-e2e/agent-configs.ts b/packages/server/src/server/daemon-e2e/agent-configs.ts index 4e7265b4f..3d26e001b 100644 --- a/packages/server/src/server/daemon-e2e/agent-configs.ts +++ b/packages/server/src/server/daemon-e2e/agent-configs.ts @@ -131,10 +131,4 @@ export function isProviderAvailable(provider: AgentProvider): boolean { /** * Helper to run a test for each provider. */ -export const allProviders: AgentProvider[] = [ - "claude", - "codex", - "copilot", - "opencode", - "pi", -]; +export const allProviders: AgentProvider[] = ["claude", "codex", "copilot", "opencode", "pi"]; diff --git a/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts index be0657fc5..1f6379e8d 100644 --- a/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts @@ -470,11 +470,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = // wake; if none arrives within the expected sleep window, verify the // agent settled to idle (notification was already processed). const autonomousWake = await client - .waitForAgentUpsert( - agent.id, - (snapshot) => snapshot.status === "running", - 15_000, - ) + .waitForAgentUpsert(agent.id, (snapshot) => snapshot.status === "running", 15_000) .catch(() => null); if (autonomousWake) { @@ -539,11 +535,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = // HELLO. When it races with HELLO, the notification is handled during // the foreground turn and there is no separate autonomous running edge. const autonomousWake = await client - .waitForAgentUpsert( - agent.id, - (snapshot) => snapshot.status === "running", - 15_000, - ) + .waitForAgentUpsert(agent.id, (snapshot) => snapshot.status === "running", 15_000) .catch(() => null); if (autonomousWake) { diff --git a/packages/server/src/server/daemon-e2e/mode-switch-propagation.e2e.test.ts b/packages/server/src/server/daemon-e2e/mode-switch-propagation.e2e.test.ts index 0126e761b..ab7fe2de4 100644 --- a/packages/server/src/server/daemon-e2e/mode-switch-propagation.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/mode-switch-propagation.e2e.test.ts @@ -24,7 +24,10 @@ function collectAgentUpdates(client: DaemonClient): { return { updates, unsub }; } -function lastUpsertFor(updates: AgentUpdatePayload[], agentId: string): AgentUpsertPayload | undefined { +function lastUpsertFor( + updates: AgentUpdatePayload[], + agentId: string, +): AgentUpsertPayload | undefined { return updates .filter((u): u is AgentUpsertPayload => u.kind === "upsert" && u.agent.id === agentId) .at(-1); @@ -125,9 +128,7 @@ describe("mode-switch update propagation", () => { const modeUpdate = client2Updates.find( (u): u is AgentUpsertPayload => - u.kind === "upsert" && - u.agent.id === agent.id && - u.agent.currentModeId === "acceptEdits", + u.kind === "upsert" && u.agent.id === agent.id && u.agent.currentModeId === "acceptEdits", ); expect(modeUpdate).toBeDefined(); diff --git a/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts index 4f9289854..1f7f47ba9 100644 --- a/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts @@ -17,7 +17,9 @@ function pickOpenCodeModel( models: Array<{ id: string }>, preferences: string[] = ["gpt-5-nano", "gpt-4.1-nano", "mini", "free"], ): string { - const preferred = models.find((model) => preferences.some((fragment) => model.id.includes(fragment))); + const preferred = models.find((model) => + preferences.some((fragment) => model.id.includes(fragment)), + ); return preferred?.id ?? models[0]!.id; } diff --git a/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts index 4d4ceb1fa..f0539ba1a 100644 --- a/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts @@ -325,12 +325,10 @@ describe("daemon E2E (real opencode) - send while working and interrupt", () => expect(finish.status).toBe("idle"); const postSendAssistantTexts = getAssistantTexts(collector.messages, agent.id); - expect( - postSendAssistantTexts.some((text) => text.includes("[System Error]")), - ).toBe(false); - expect( - postSendAssistantTexts.some((text) => text.includes(SYSTEM_ERROR_SNIPPET)), - ).toBe(false); + expect(postSendAssistantTexts.some((text) => text.includes("[System Error]"))).toBe(false); + expect(postSendAssistantTexts.some((text) => text.includes(SYSTEM_ERROR_SNIPPET))).toBe( + false, + ); const timeline = await client.fetchAgentTimeline(agent.id, { limit: 160 }); const assistantTexts = getTimelineAssistantTexts(timeline); @@ -400,9 +398,9 @@ describe("daemon E2E (real opencode) - send while working and interrupt", () => expect(finish.status).toBe("idle"); const postInterruptAssistantTexts = getAssistantTexts(collector.messages, agent.id); - expect( - postInterruptAssistantTexts.some((text) => text.includes("[System Error]")), - ).toBe(false); + expect(postInterruptAssistantTexts.some((text) => text.includes("[System Error]"))).toBe( + false, + ); expect( postInterruptAssistantTexts.some((text) => text.includes(SYSTEM_ERROR_SNIPPET)), ).toBe(false); diff --git a/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts index b616a487b..5b427c590 100644 --- a/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts @@ -71,7 +71,10 @@ function getAgentStatuses(messages: SessionOutboundMessage[], agentId: string): .map((message) => message.payload.agent.status); } -function getStatusesBeforeFirstAssistant(messages: SessionOutboundMessage[], agentId: string): string[] { +function getStatusesBeforeFirstAssistant( + messages: SessionOutboundMessage[], + agentId: string, +): string[] { const firstAssistantIndex = messages.findIndex( (message) => message.type === "agent_stream" && @@ -126,7 +129,9 @@ async function waitForRunningToolCall( .map((entry) => entry.item.text) ?? []; const limitText = assistantTexts.find((text) => hasProviderLimitText(text)); if (limitText) { - throw new Error(`Claude could not reach the tool call because the provider rejected the run: ${limitText}`); + throw new Error( + `Claude could not reach the tool call because the provider rejected the run: ${limitText}`, + ); } if ( timeline?.entries.some( @@ -252,9 +257,7 @@ describe("daemon E2E (real claude) - send message during tool call", () => { }); // No system error messages should leak into the timeline - const hasSystemError = assistantTexts.some((text) => - text.includes("[System Error]"), - ); + const hasSystemError = assistantTexts.some((text) => text.includes("[System Error]")); expect(hasSystemError).toBe(false); expect(postSendAssistantTexts.some((text) => text.includes("[System Error]"))).toBe(false); diff --git a/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts b/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts index 0f66a29c8..e881e533b 100644 --- a/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts @@ -1073,7 +1073,12 @@ describe("daemon E2E terminal", () => { type: "input", data: "echo hello world\r", }); - await waitForTerminalOutput(ctx.client, terminalId, (text) => text.includes("hello world"), 15000); + await waitForTerminalOutput( + ctx.client, + terminalId, + (text) => text.includes("hello world"), + 15000, + ); const capture = await ctx.client.captureTerminal(terminalId); @@ -1155,7 +1160,12 @@ describe("daemon E2E terminal", () => { type: "input", data: "printf '\\033[31mred text\\033[0m\\n'\r", }); - await waitForTerminalOutput(ctx.client, terminalId, (text) => text.includes("red text"), 15000); + await waitForTerminalOutput( + ctx.client, + terminalId, + (text) => text.includes("red text"), + 15000, + ); const capture = await ctx.client.captureTerminal(terminalId); const capturedText = capture.lines.join("\n"); diff --git a/packages/server/src/server/editor-targets.ts b/packages/server/src/server/editor-targets.ts index 4bcadacf6..6938ecc84 100644 --- a/packages/server/src/server/editor-targets.ts +++ b/packages/server/src/server/editor-targets.ts @@ -2,11 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { existsSync } from "node:fs"; import { posix, win32 } from "node:path"; import type { EditorTargetDescriptorPayload, EditorTargetId } from "../shared/messages.js"; -import { - findExecutable, - quoteWindowsArgument, - quoteWindowsCommand, -} from "../utils/executable.js"; +import { findExecutable, quoteWindowsArgument, quoteWindowsCommand } from "../utils/executable.js"; type EditorTargetDefinition = { id: EditorTargetId; @@ -144,7 +140,9 @@ export async function openInEditorTarget( const command = platform === "win32" ? quoteWindowsCommand(launch.command) : launch.command; const args = - platform === "win32" ? launch.args.map((argument) => quoteWindowsArgument(argument)) : launch.args; + platform === "win32" + ? launch.args.map((argument) => quoteWindowsArgument(argument)) + : launch.args; await new Promise((resolve, reject) => { let child: ChildProcess; diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts index c617066d3..31ebd36cf 100644 --- a/packages/server/src/server/exports.ts +++ b/packages/server/src/server/exports.ts @@ -30,9 +30,7 @@ export { } from "./speech/providers/local/sherpa/sherpa-runtime-env.js"; // Provider binary resolution -export { - applyProviderEnv, -} from "./agent/provider-launch-config.js"; +export { applyProviderEnv } from "./agent/provider-launch-config.js"; export { findExecutable, quoteWindowsArgument, quoteWindowsCommand } from "../utils/executable.js"; // Provider manifest (source of truth for provider definitions) diff --git a/packages/server/src/server/loop-service.test.ts b/packages/server/src/server/loop-service.test.ts index 3385584f5..1e0d9bed6 100644 --- a/packages/server/src/server/loop-service.test.ts +++ b/packages/server/src/server/loop-service.test.ts @@ -102,7 +102,10 @@ class ScriptedAgentSession implements AgentSession { }; } - async startTurn(prompt: AgentPromptInput, _options?: AgentRunOptions): Promise<{ turnId: string }> { + async startTurn( + prompt: AgentPromptInput, + _options?: AgentRunOptions, + ): Promise<{ turnId: string }> { const promptText = typeof prompt === "string" ? prompt : JSON.stringify(prompt); const turnId = `turn-${++this.turnCount}`; this.interrupted = false; @@ -183,7 +186,12 @@ class ScriptedAgentSession implements AgentSession { turnId, }); if (this.interrupted) { - this.emit({ type: "turn_canceled", provider: this.provider, reason: "interrupted", turnId }); + this.emit({ + type: "turn_canceled", + provider: this.provider, + reason: "interrupted", + turnId, + }); return; } this.emit({ @@ -236,7 +244,7 @@ describe("LoopService", () => { if (config.title?.includes("worker")) { return `worker run ${state.workerRuns}`; } - return "{\"passed\":true,\"reason\":\"not used\"}"; + return '{"passed":true,"reason":"not used"}'; }, }), }, @@ -282,7 +290,7 @@ describe("LoopService", () => { claude: new ScriptedAgentClient("claude", { async onRun({ config }) { verifierConfigs.push(config); - return "{\"passed\":true,\"reason\":\"verified\"}"; + return '{"passed":true,"reason":"verified"}'; }, }), }, @@ -338,7 +346,7 @@ describe("LoopService", () => { writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); return "created done.txt"; } - return "{\"passed\":true,\"reason\":\"done.txt exists\"}"; + return '{"passed":true,"reason":"done.txt exists"}'; }, }), }, @@ -394,8 +402,8 @@ describe("LoopService", () => { } const exists = pathExists(path.join(workspaceDir, "done.txt")); return exists - ? "{\"passed\":true,\"reason\":\"done.txt exists\"}" - : "{\"passed\":false,\"reason\":\"done.txt missing\"}"; + ? '{"passed":true,"reason":"done.txt exists"}' + : '{"passed":false,"reason":"done.txt missing"}'; }, }), }, @@ -437,7 +445,7 @@ describe("LoopService", () => { await blocker; return "finished"; } - return "{\"passed\":true,\"reason\":\"ok\"}"; + return '{"passed":true,"reason":"ok"}'; }, }), }, diff --git a/packages/server/src/server/loop-service.ts b/packages/server/src/server/loop-service.ts index ead9e2bf0..c3a2d4ab9 100644 --- a/packages/server/src/server/loop-service.ts +++ b/packages/server/src/server/loop-service.ts @@ -598,7 +598,9 @@ export class LoopService { iteration: LoopIterationRecord, signal: AbortSignal, ): Promise { - const agent = await this.options.agentManager.createAgent(this.buildWorkerConfig(loop, iteration)); + const agent = await this.options.agentManager.createAgent( + this.buildWorkerConfig(loop, iteration), + ); iteration.workerAgentId = agent.id; loop.activeWorkerAgentId = agent.id; loop.updatedAt = nowIso(); @@ -688,9 +690,7 @@ export class LoopService { iteration: iteration.index, source: "verify-check", level: result.passed ? "info" : "error", - text: output - ? `exit ${result.exitCode}\n${output}` - : `exit ${result.exitCode}`, + text: output ? `exit ${result.exitCode}\n${output}` : `exit ${result.exitCode}`, }); loop.updatedAt = nowIso(); await this.persist(); @@ -736,7 +736,10 @@ export class LoopService { try { const result = await getStructuredAgentResponse({ caller: async (nextPrompt) => { - const run = await this.options.agentManager.runAgent(verifierAgent.id, this.toPrompt(nextPrompt)); + const run = await this.options.agentManager.runAgent( + verifierAgent.id, + this.toPrompt(nextPrompt), + ); return this.resolveFinalText(run.timeline, run.finalText); }, prompt: loop.verifyPrompt, @@ -788,7 +791,10 @@ export class LoopService { }; } - private buildVerifierConfig(loop: LoopRecord, iteration: LoopIterationRecord): AgentSessionConfig { + private buildVerifierConfig( + loop: LoopRecord, + iteration: LoopIterationRecord, + ): AgentSessionConfig { return { provider: loop.verifierProvider ?? loop.provider, cwd: loop.cwd, @@ -815,7 +821,11 @@ export class LoopService { return text; } - private finishLoop(loop: LoopRecord, status: Exclude, message: string): void { + private finishLoop( + loop: LoopRecord, + status: Exclude, + message: string, + ): void { loop.status = status; loop.completedAt = nowIso(); loop.updatedAt = loop.completedAt; @@ -830,10 +840,7 @@ export class LoopService { }); } - private appendLog( - loop: LoopRecord, - entry: Omit, - ): void { + private appendLog(loop: LoopRecord, entry: Omit): void { loop.logs.push({ seq: loop.nextLogSeq, timestamp: nowIso(), @@ -852,7 +859,9 @@ export class LoopService { if (exact) { return exact; } - const matches = Array.from(this.loops.values()).filter((record) => record.id.startsWith(trimmed)); + const matches = Array.from(this.loops.values()).filter((record) => + record.id.startsWith(trimmed), + ); if (matches.length === 1) { return matches[0]!; } diff --git a/packages/server/src/server/pid-lock.ts b/packages/server/src/server/pid-lock.ts index 286ee1218..305858119 100644 --- a/packages/server/src/server/pid-lock.ts +++ b/packages/server/src/server/pid-lock.ts @@ -127,10 +127,7 @@ export async function updatePidLock( const existingLock = JSON.parse(content) as PidLockInfo; if (existingLock.pid !== lockOwnerPid) { - throw new PidLockError( - `Cannot update PID lock owned by PID ${existingLock.pid}`, - existingLock, - ); + throw new PidLockError(`Cannot update PID lock owned by PID ${existingLock.pid}`, existingLock); } const updatedLock: PidLockInfo = { diff --git a/packages/server/src/server/schedule/cron.test.ts b/packages/server/src/server/schedule/cron.test.ts index e5e9e7ad4..af9418e20 100644 --- a/packages/server/src/server/schedule/cron.test.ts +++ b/packages/server/src/server/schedule/cron.test.ts @@ -21,8 +21,8 @@ describe("schedule cron cadence", () => { }); test("rejects invalid cron expressions", () => { - expect(() => - validateScheduleCadence({ type: "cron", expression: "not-a-valid-cron" }), - ).toThrow("Cron expressions must have 5 fields"); + expect(() => validateScheduleCadence({ type: "cron", expression: "not-a-valid-cron" })).toThrow( + "Cron expressions must have 5 fields", + ); }); }); diff --git a/packages/server/src/server/schedule/cron.ts b/packages/server/src/server/schedule/cron.ts index 91b0ee690..29e9a732e 100644 --- a/packages/server/src/server/schedule/cron.ts +++ b/packages/server/src/server/schedule/cron.ts @@ -33,8 +33,7 @@ function parseField( } const [base, stepSource] = part.split("/"); - const step = - stepSource === undefined ? 1 : Number.parseInt(stepSource, 10); + const step = stepSource === undefined ? 1 : Number.parseInt(stepSource, 10); if (!Number.isInteger(step) || step <= 0) { throw new Error(`Invalid cron ${bounds.name} step`); } diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index f77856627..3e874e82b 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -4,12 +4,13 @@ import type { Logger } from "pino"; import { AgentManager } from "../agent/agent-manager.js"; import type { ManagedAgent } from "../agent/agent-manager.js"; import { AgentStorage } from "../agent/agent-storage.js"; -import type { - AgentPromptInput, - AgentSessionConfig, -} from "../agent/agent-sdk-types.js"; +import type { AgentPromptInput, AgentSessionConfig } from "../agent/agent-sdk-types.js"; import { curateAgentActivity } from "../agent/activity-curator.js"; -import { buildConfigOverrides, buildSessionConfig, extractTimestamps } from "../persistence-hooks.js"; +import { + buildConfigOverrides, + buildSessionConfig, + extractTimestamps, +} from "../persistence-hooks.js"; import { ScheduleStore } from "./store.js"; import { computeNextRunAt, validateScheduleCadence } from "./cron.js"; import type { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 813cf0e4d..c5cda5f63 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -142,9 +142,7 @@ import { } from "./file-explorer/service.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import { PushTokenStore } from "./push/token-store.js"; -import { - type WorktreeConfig, -} from "../utils/worktree.js"; +import { type WorktreeConfig } from "../utils/worktree.js"; import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js"; import { getCheckoutDiff, @@ -161,21 +159,14 @@ import { import { getProjectIcon } from "../utils/project-icon.js"; import { expandTilde } from "../utils/path.js"; import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js"; -import { - READ_ONLY_GIT_ENV, - resolveCheckoutGitDir, - toCheckoutError, -} from "./checkout-git-utils.js"; +import { READ_ONLY_GIT_ENV, resolveCheckoutGitDir, toCheckoutError } from "./checkout-git-utils.js"; import { CheckoutDiffManager } from "./checkout-diff-manager.js"; import type { LocalSpeechModelId } from "./speech/providers/local/models.js"; import { toResolver, type Resolvable } from "./speech/provider-resolver.js"; import type { SpeechReadinessSnapshot, SpeechReadinessState } from "./speech/speech-runtime.js"; import type pino from "pino"; import { resolveClientMessageId } from "./client-message-id.js"; -import { - ChatServiceError, - FileBackedChatService, -} from "./chat/chat-service.js"; +import { ChatServiceError, FileBackedChatService } from "./chat/chat-service.js"; import { notifyChatMentions } from "./chat/chat-mentions.js"; import { LoopService } from "./loop-service.js"; import { ScheduleService } from "./schedule/service.js"; @@ -1066,7 +1057,10 @@ export class Session { if (storedUpdatedAt) { const liveUpdatedAt = Date.parse(payload.updatedAt); const persistedUpdatedAt = Date.parse(storedUpdatedAt); - if (!Number.isNaN(persistedUpdatedAt) && (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt)) { + if ( + !Number.isNaN(persistedUpdatedAt) && + (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt) + ) { payload.updatedAt = storedUpdatedAt; } } @@ -1394,7 +1388,9 @@ export class Session { const placement = await this.buildProjectPlacement(normalizedCwd); const resolvedWorkspaceId = deriveWorkspaceId(normalizedCwd, placement.checkout); const staleWorkspace = - resolvedWorkspaceId === normalizedCwd ? null : await this.workspaceRegistry.get(normalizedCwd); + resolvedWorkspaceId === normalizedCwd + ? null + : await this.workspaceRegistry.get(normalizedCwd); const existing = (await this.workspaceRegistry.get(resolvedWorkspaceId)) ?? staleWorkspace; await this.syncWorkspaceGitWatchTarget(resolvedWorkspaceId, { isGit: placement.checkout.isGit, @@ -1549,447 +1545,447 @@ export class Session { this.peakInflightRequests = this.inflightRequests; } try { - this.sessionLogger.trace( - { messageType: msg.type, payloadBytes: JSON.stringify(msg).length }, - "inbound message", - ); - try { - switch (msg.type) { - case "voice_audio_chunk": - await this.handleAudioChunk(msg); - break; + this.sessionLogger.trace( + { messageType: msg.type, payloadBytes: JSON.stringify(msg).length }, + "inbound message", + ); + try { + switch (msg.type) { + case "voice_audio_chunk": + await this.handleAudioChunk(msg); + break; - case "abort_request": - await this.handleAbort(); - break; + case "abort_request": + await this.handleAbort(); + break; - case "audio_played": - this.handleAudioPlayed(msg.id); - break; + case "audio_played": + this.handleAudioPlayed(msg.id); + break; - case "fetch_agents_request": - await this.handleFetchAgents(msg); - break; + case "fetch_agents_request": + await this.handleFetchAgents(msg); + break; - case "fetch_workspaces_request": - await this.handleFetchWorkspacesRequest(msg); - break; + case "fetch_workspaces_request": + await this.handleFetchWorkspacesRequest(msg); + break; - case "fetch_agent_request": - await this.handleFetchAgent(msg.agentId, msg.requestId); - break; + case "fetch_agent_request": + await this.handleFetchAgent(msg.agentId, msg.requestId); + break; - case "delete_agent_request": - await this.handleDeleteAgentRequest(msg.agentId, msg.requestId); - break; + case "delete_agent_request": + await this.handleDeleteAgentRequest(msg.agentId, msg.requestId); + break; - case "archive_agent_request": - await this.handleArchiveAgentRequest(msg.agentId, msg.requestId); - break; + case "archive_agent_request": + await this.handleArchiveAgentRequest(msg.agentId, msg.requestId); + break; - case "close_items_request": - await this.handleCloseItemsRequest(msg); - break; + case "close_items_request": + await this.handleCloseItemsRequest(msg); + break; - case "update_agent_request": - await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId); - break; + case "update_agent_request": + await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId); + break; - case "set_voice_mode": - await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId); - break; + case "set_voice_mode": + await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId); + break; - case "send_agent_message_request": - await this.handleSendAgentMessageRequest(msg); - break; + case "send_agent_message_request": + await this.handleSendAgentMessageRequest(msg); + break; - case "wait_for_finish_request": - await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs); - break; + case "wait_for_finish_request": + await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs); + break; - case "dictation_stream_start": - { - const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation"); - if (unavailable) { - this.emit({ - type: "dictation_stream_error", - payload: { - dictationId: msg.dictationId, - error: unavailable.message, - retryable: unavailable.retryable, - reasonCode: unavailable.reasonCode, - missingModelIds: unavailable.missingModelIds, - }, - }); - break; + case "dictation_stream_start": + { + const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation"); + if (unavailable) { + this.emit({ + type: "dictation_stream_error", + payload: { + dictationId: msg.dictationId, + error: unavailable.message, + retryable: unavailable.retryable, + reasonCode: unavailable.reasonCode, + missingModelIds: unavailable.missingModelIds, + }, + }); + break; + } } - } - await this.dictationStreamManager.handleStart(msg.dictationId, msg.format); - break; - - case "dictation_stream_chunk": - await this.dictationStreamManager.handleChunk({ - dictationId: msg.dictationId, - seq: msg.seq, - audioBase64: msg.audio, - format: msg.format, - }); - break; + await this.dictationStreamManager.handleStart(msg.dictationId, msg.format); + break; - case "dictation_stream_finish": - await this.dictationStreamManager.handleFinish(msg.dictationId, msg.finalSeq); - break; + case "dictation_stream_chunk": + await this.dictationStreamManager.handleChunk({ + dictationId: msg.dictationId, + seq: msg.seq, + audioBase64: msg.audio, + format: msg.format, + }); + break; - case "dictation_stream_cancel": - this.dictationStreamManager.handleCancel(msg.dictationId); - break; + case "dictation_stream_finish": + await this.dictationStreamManager.handleFinish(msg.dictationId, msg.finalSeq); + break; - case "create_agent_request": - await this.handleCreateAgentRequest(msg); - break; + case "dictation_stream_cancel": + this.dictationStreamManager.handleCancel(msg.dictationId); + break; - case "resume_agent_request": - await this.handleResumeAgentRequest(msg); - break; + case "create_agent_request": + await this.handleCreateAgentRequest(msg); + break; - case "refresh_agent_request": - await this.handleRefreshAgentRequest(msg); - break; + case "resume_agent_request": + await this.handleResumeAgentRequest(msg); + break; - case "cancel_agent_request": - await this.handleCancelAgentRequest(msg.agentId); - break; + case "refresh_agent_request": + await this.handleRefreshAgentRequest(msg); + break; - case "restart_server_request": - await this.handleRestartServerRequest(msg.requestId, msg.reason); - break; + case "cancel_agent_request": + await this.handleCancelAgentRequest(msg.agentId); + break; - case "shutdown_server_request": - await this.handleShutdownServerRequest(msg.requestId); - break; + case "restart_server_request": + await this.handleRestartServerRequest(msg.requestId, msg.reason); + break; - case "fetch_agent_timeline_request": - await this.handleFetchAgentTimelineRequest(msg); - break; + case "shutdown_server_request": + await this.handleShutdownServerRequest(msg.requestId); + break; - case "set_agent_mode_request": - await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId); - break; + case "fetch_agent_timeline_request": + await this.handleFetchAgentTimelineRequest(msg); + break; - case "set_agent_model_request": - await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId); - break; + case "set_agent_mode_request": + await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId); + break; - case "set_agent_feature_request": - await this.handleSetAgentFeatureRequest( - msg.agentId, - msg.featureId, - msg.value, - msg.requestId, - ); - break; + case "set_agent_model_request": + await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId); + break; - case "set_agent_thinking_request": - await this.handleSetAgentThinkingRequest( - msg.agentId, - msg.thinkingOptionId, - msg.requestId, - ); - break; + case "set_agent_feature_request": + await this.handleSetAgentFeatureRequest( + msg.agentId, + msg.featureId, + msg.value, + msg.requestId, + ); + break; - case "agent_permission_response": - await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response); - break; + case "set_agent_thinking_request": + await this.handleSetAgentThinkingRequest( + msg.agentId, + msg.thinkingOptionId, + msg.requestId, + ); + break; - case "checkout_status_request": - await this.handleCheckoutStatusRequest(msg); - break; + case "agent_permission_response": + await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response); + break; - case "validate_branch_request": - await this.handleValidateBranchRequest(msg); - break; + case "checkout_status_request": + await this.handleCheckoutStatusRequest(msg); + break; - case "branch_suggestions_request": - await this.handleBranchSuggestionsRequest(msg); - break; + case "validate_branch_request": + await this.handleValidateBranchRequest(msg); + break; - case "directory_suggestions_request": - await this.handleDirectorySuggestionsRequest(msg); - break; + case "branch_suggestions_request": + await this.handleBranchSuggestionsRequest(msg); + break; - case "subscribe_checkout_diff_request": - await this.handleSubscribeCheckoutDiffRequest(msg); - break; + case "directory_suggestions_request": + await this.handleDirectorySuggestionsRequest(msg); + break; - case "unsubscribe_checkout_diff_request": - this.handleUnsubscribeCheckoutDiffRequest(msg); - break; + case "subscribe_checkout_diff_request": + await this.handleSubscribeCheckoutDiffRequest(msg); + break; - case "checkout_commit_request": - await this.handleCheckoutCommitRequest(msg); - break; + case "unsubscribe_checkout_diff_request": + this.handleUnsubscribeCheckoutDiffRequest(msg); + break; - case "checkout_merge_request": - await this.handleCheckoutMergeRequest(msg); - break; + case "checkout_commit_request": + await this.handleCheckoutCommitRequest(msg); + break; - case "checkout_merge_from_base_request": - await this.handleCheckoutMergeFromBaseRequest(msg); - break; + case "checkout_merge_request": + await this.handleCheckoutMergeRequest(msg); + break; - case "checkout_push_request": - await this.handleCheckoutPushRequest(msg); - break; + case "checkout_merge_from_base_request": + await this.handleCheckoutMergeFromBaseRequest(msg); + break; - case "checkout_pr_create_request": - await this.handleCheckoutPrCreateRequest(msg); - break; + case "checkout_push_request": + await this.handleCheckoutPushRequest(msg); + break; - case "checkout_pr_status_request": - await this.handleCheckoutPrStatusRequest(msg); - break; + case "checkout_pr_create_request": + await this.handleCheckoutPrCreateRequest(msg); + break; - case "paseo_worktree_list_request": - await this.handlePaseoWorktreeListRequest(msg); - break; + case "checkout_pr_status_request": + await this.handleCheckoutPrStatusRequest(msg); + break; - case "paseo_worktree_archive_request": - await this.handlePaseoWorktreeArchiveRequest(msg); - break; + case "paseo_worktree_list_request": + await this.handlePaseoWorktreeListRequest(msg); + break; - case "create_paseo_worktree_request": - await this.handleCreatePaseoWorktreeRequest(msg); - break; + case "paseo_worktree_archive_request": + await this.handlePaseoWorktreeArchiveRequest(msg); + break; - case "list_available_editors_request": - await this.handleListAvailableEditorsRequest(msg); - break; + case "create_paseo_worktree_request": + await this.handleCreatePaseoWorktreeRequest(msg); + break; - case "open_in_editor_request": - await this.handleOpenInEditorRequest(msg); - break; + case "list_available_editors_request": + await this.handleListAvailableEditorsRequest(msg); + break; - case "open_project_request": - await this.handleOpenProjectRequest(msg); - break; + case "open_in_editor_request": + await this.handleOpenInEditorRequest(msg); + break; - case "archive_workspace_request": - await this.handleArchiveWorkspaceRequest(msg); - break; + case "open_project_request": + await this.handleOpenProjectRequest(msg); + break; - case "file_explorer_request": - await this.handleFileExplorerRequest(msg); - break; + case "archive_workspace_request": + await this.handleArchiveWorkspaceRequest(msg); + break; - case "project_icon_request": - await this.handleProjectIconRequest(msg); - break; + case "file_explorer_request": + await this.handleFileExplorerRequest(msg); + break; - case "file_download_token_request": - await this.handleFileDownloadTokenRequest(msg); - break; + case "project_icon_request": + await this.handleProjectIconRequest(msg); + break; - case "list_provider_models_request": - await this.handleListProviderModelsRequest(msg); - break; + case "file_download_token_request": + await this.handleFileDownloadTokenRequest(msg); + break; - case "list_provider_modes_request": - await this.handleListProviderModesRequest(msg); - break; + case "list_provider_models_request": + await this.handleListProviderModelsRequest(msg); + break; - case "list_provider_features_request": - await this.handleListProviderFeaturesRequest(msg); - break; + case "list_provider_modes_request": + await this.handleListProviderModesRequest(msg); + break; - case "list_available_providers_request": - await this.handleListAvailableProvidersRequest(msg); - break; + case "list_provider_features_request": + await this.handleListProviderFeaturesRequest(msg); + break; - case "get_providers_snapshot_request": - await this.handleGetProvidersSnapshotRequest(msg); - break; + case "list_available_providers_request": + await this.handleListAvailableProvidersRequest(msg); + break; - case "refresh_providers_snapshot_request": - await this.handleRefreshProvidersSnapshotRequest(msg); - break; + case "get_providers_snapshot_request": + await this.handleGetProvidersSnapshotRequest(msg); + break; - case "provider_diagnostic_request": - await this.handleProviderDiagnosticRequest(msg); - break; + case "refresh_providers_snapshot_request": + await this.handleRefreshProvidersSnapshotRequest(msg); + break; - case "clear_agent_attention": - await this.handleClearAgentAttention(msg.agentId); - break; + case "provider_diagnostic_request": + await this.handleProviderDiagnosticRequest(msg); + break; - case "client_heartbeat": - this.handleClientHeartbeat(msg); - break; + case "clear_agent_attention": + await this.handleClearAgentAttention(msg.agentId); + break; - case "ping": { - const now = Date.now(); - this.emit({ - type: "pong", - payload: { - requestId: msg.requestId, - clientSentAt: msg.clientSentAt, - serverReceivedAt: now, - serverSentAt: now, - }, - }); - break; - } + case "client_heartbeat": + this.handleClientHeartbeat(msg); + break; - case "list_commands_request": - await this.handleListCommandsRequest(msg); - break; + case "ping": { + const now = Date.now(); + this.emit({ + type: "pong", + payload: { + requestId: msg.requestId, + clientSentAt: msg.clientSentAt, + serverReceivedAt: now, + serverSentAt: now, + }, + }); + break; + } - case "register_push_token": - this.handleRegisterPushToken(msg.token); - break; + case "list_commands_request": + await this.handleListCommandsRequest(msg); + break; - case "subscribe_terminals_request": - this.handleSubscribeTerminalsRequest(msg); - break; + case "register_push_token": + this.handleRegisterPushToken(msg.token); + break; - case "unsubscribe_terminals_request": - this.handleUnsubscribeTerminalsRequest(msg); - break; + case "subscribe_terminals_request": + this.handleSubscribeTerminalsRequest(msg); + break; - case "list_terminals_request": - await this.handleListTerminalsRequest(msg); - break; + case "unsubscribe_terminals_request": + this.handleUnsubscribeTerminalsRequest(msg); + break; - case "create_terminal_request": - await this.handleCreateTerminalRequest(msg); - break; + case "list_terminals_request": + await this.handleListTerminalsRequest(msg); + break; - case "subscribe_terminal_request": - await this.handleSubscribeTerminalRequest(msg); - break; + case "create_terminal_request": + await this.handleCreateTerminalRequest(msg); + break; - case "unsubscribe_terminal_request": - this.handleUnsubscribeTerminalRequest(msg); - break; + case "subscribe_terminal_request": + await this.handleSubscribeTerminalRequest(msg); + break; - case "terminal_input": - this.handleTerminalInput(msg); - break; + case "unsubscribe_terminal_request": + this.handleUnsubscribeTerminalRequest(msg); + break; - case "kill_terminal_request": - await this.handleKillTerminalRequest(msg); - break; + case "terminal_input": + this.handleTerminalInput(msg); + break; - case "capture_terminal_request": - await this.handleCaptureTerminalRequest(msg); - break; + case "kill_terminal_request": + await this.handleKillTerminalRequest(msg); + break; - case "chat/create": - await this.handleChatCreateRequest(msg); - break; + case "capture_terminal_request": + await this.handleCaptureTerminalRequest(msg); + break; - case "chat/list": - await this.handleChatListRequest(msg); - break; + case "chat/create": + await this.handleChatCreateRequest(msg); + break; - case "chat/inspect": - await this.handleChatInspectRequest(msg); - break; + case "chat/list": + await this.handleChatListRequest(msg); + break; - case "chat/delete": - await this.handleChatDeleteRequest(msg); - break; + case "chat/inspect": + await this.handleChatInspectRequest(msg); + break; - case "chat/post": - await this.handleChatPostRequest(msg); - break; + case "chat/delete": + await this.handleChatDeleteRequest(msg); + break; - case "chat/read": - await this.handleChatReadRequest(msg); - break; + case "chat/post": + await this.handleChatPostRequest(msg); + break; - case "chat/wait": - await this.handleChatWaitRequest(msg); - break; + case "chat/read": + await this.handleChatReadRequest(msg); + break; - case "schedule/create": - await this.handleScheduleCreateRequest(msg); - break; + case "chat/wait": + await this.handleChatWaitRequest(msg); + break; - case "schedule/list": - await this.handleScheduleListRequest(msg); - break; + case "schedule/create": + await this.handleScheduleCreateRequest(msg); + break; - case "schedule/inspect": - await this.handleScheduleInspectRequest(msg); - break; + case "schedule/list": + await this.handleScheduleListRequest(msg); + break; - case "schedule/logs": - await this.handleScheduleLogsRequest(msg); - break; + case "schedule/inspect": + await this.handleScheduleInspectRequest(msg); + break; - case "schedule/pause": - await this.handleSchedulePauseRequest(msg); - break; + case "schedule/logs": + await this.handleScheduleLogsRequest(msg); + break; - case "schedule/resume": - await this.handleScheduleResumeRequest(msg); - break; + case "schedule/pause": + await this.handleSchedulePauseRequest(msg); + break; - case "schedule/delete": - await this.handleScheduleDeleteRequest(msg); - break; + case "schedule/resume": + await this.handleScheduleResumeRequest(msg); + break; - case "loop/run": - await this.handleLoopRunRequest(msg); - break; + case "schedule/delete": + await this.handleScheduleDeleteRequest(msg); + break; - case "loop/list": - await this.handleLoopListRequest(msg); - break; + case "loop/run": + await this.handleLoopRunRequest(msg); + break; - case "loop/inspect": - await this.handleLoopInspectRequest(msg); - break; + case "loop/list": + await this.handleLoopListRequest(msg); + break; - case "loop/logs": - await this.handleLoopLogsRequest(msg); - break; + case "loop/inspect": + await this.handleLoopInspectRequest(msg); + break; - case "loop/stop": - await this.handleLoopStopRequest(msg); - break; - } - } catch (error: any) { - const err = error instanceof Error ? error : new Error(String(error)); - this.sessionLogger.error({ err }, "Error handling message"); + case "loop/logs": + await this.handleLoopLogsRequest(msg); + break; - const requestId = (msg as { requestId?: unknown }).requestId; - if (typeof requestId === "string") { - try { - this.emit({ - type: "rpc_error", - payload: { - requestId, - requestType: msg.type, - error: `Request failed: ${err.message}`, - code: "handler_error", - }, - }); - } catch (emitError) { - this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error"); + case "loop/stop": + await this.handleLoopStopRequest(msg); + break; + } + } catch (error: any) { + const err = error instanceof Error ? error : new Error(String(error)); + this.sessionLogger.error({ err }, "Error handling message"); + + const requestId = (msg as { requestId?: unknown }).requestId; + if (typeof requestId === "string") { + try { + this.emit({ + type: "rpc_error", + payload: { + requestId, + requestType: msg.type, + error: `Request failed: ${err.message}`, + code: "handler_error", + }, + }); + } catch (emitError) { + this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error"); + } } - } - this.emit({ - type: "activity_log", - payload: { - id: uuidv4(), - timestamp: new Date(), - type: "error", - content: `Error: ${err.message}`, - }, - }); - } + this.emit({ + type: "activity_log", + payload: { + id: uuidv4(), + timestamp: new Date(), + type: "error", + content: `Error: ${err.message}`, + }, + }); + } } finally { this.inflightRequests--; } @@ -3242,9 +3238,7 @@ export class Session { cwd: expandTilde(draftConfig.cwd), ...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}), ...(draftConfig.model ? { model: draftConfig.model } : {}), - ...(draftConfig.thinkingOptionId - ? { thinkingOptionId: draftConfig.thinkingOptionId } - : {}), + ...(draftConfig.thinkingOptionId ? { thinkingOptionId: draftConfig.thinkingOptionId } : {}), ...(draftConfig.featureValues ? { featureValues: draftConfig.featureValues } : {}), }; } @@ -5452,9 +5446,11 @@ export class Session { private async listWorkspaceDescriptorsSnapshot(): Promise { return Array.from( - (await this.buildWorkspaceDescriptorMap({ - includeGitData: false, - })).values(), + ( + await this.buildWorkspaceDescriptorMap({ + includeGitData: false, + }) + ).values(), ); } @@ -7392,8 +7388,7 @@ export class Session { } private emitChatRpcError(request: { requestId: string; type: string }, error: unknown): void { - const message = - error instanceof Error ? error.message : "Chat request failed"; + const message = error instanceof Error ? error.message : "Chat request failed"; const code = error instanceof ChatServiceError ? error.code : "chat_request_failed"; this.sessionLogger.error({ err: error, requestType: request.type }, "Chat request failed"); this.emit({ @@ -7569,7 +7564,10 @@ export class Session { private toScheduleSummary( schedule: Awaited>, - ): Extract["payload"]["schedules"][number] { + ): Extract< + SessionOutboundMessage, + { type: "schedule/list/response" } + >["payload"]["schedules"][number] { const { runs: _runs, ...summary } = schedule; return summary; } diff --git a/packages/server/src/server/session.workspace-git-watch.test.ts b/packages/server/src/server/session.workspace-git-watch.test.ts index 38c3a0290..52ef73871 100644 --- a/packages/server/src/server/session.workspace-git-watch.test.ts +++ b/packages/server/src/server/session.workspace-git-watch.test.ts @@ -92,17 +92,15 @@ function createSessionForWorkspaceGitWatchTests(): { error: vi.fn(), }; const backgroundGitFetchManager = { - subscribe: vi.fn( - async (params: { repoGitRoot: string; cwd: string }, listener: () => void) => { - const unsubscribe = vi.fn(); - backgroundGitFetchSubscriptions.push({ - params, - listener, - unsubscribe, - }); - return { unsubscribe }; - }, - ), + subscribe: vi.fn(async (params: { repoGitRoot: string; cwd: string }, listener: () => void) => { + const unsubscribe = vi.fn(); + backgroundGitFetchSubscriptions.push({ + params, + listener, + unsubscribe, + }); + return { unsubscribe }; + }), }; const session = new Session({ @@ -439,7 +437,8 @@ describe("workspace git watch targets", () => { }); test("refreshes the workspace when the background fetch manager callback fires and unsubscribes on cleanup", async () => { - const { session, emitted, backgroundGitFetchManager } = createSessionForWorkspaceGitWatchTests(); + const { session, emitted, backgroundGitFetchManager } = + createSessionForWorkspaceGitWatchTests(); const sessionAny = session as any; sessionAny.buildProjectPlacement = async (cwd: string) => ({ diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 879ed0370..074e0bd99 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -267,9 +267,7 @@ describe("workspace aggregation", () => { archivedAt: expect.any(String), }, }); - expect( - emitted.find((message) => message.type === "agent_archived")?.payload, - ).toMatchObject({ + expect(emitted.find((message) => message.type === "agent_archived")?.payload).toMatchObject({ agentId: "agent-1", archivedAt: expect.any(String), requestId: "req-archive", @@ -417,9 +415,7 @@ describe("workspace aggregation", () => { expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-1"); expect(session.terminalManager.killTerminal).toHaveBeenCalledWith("term-1"); - expect( - emitted.find((message) => message.type === "close_items_response")?.payload, - ).toEqual({ + expect(emitted.find((message) => message.type === "close_items_response")?.payload).toEqual({ agents: [{ agentId: "agent-1", archivedAt }], terminals: [{ terminalId: "term-1", success: true }], requestId: "req-close-items", @@ -719,9 +715,7 @@ describe("workspace aggregation", () => { expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-bad"); expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-good"); expect(session.terminalManager.killTerminal).toHaveBeenCalledWith("term-1"); - expect( - emitted.find((message) => message.type === "close_items_response")?.payload, - ).toEqual({ + expect(emitted.find((message) => message.type === "close_items_response")?.payload).toEqual({ agents: [{ agentId: "agent-good", archivedAt }], terminals: [{ terminalId: "term-1", success: true }], requestId: "req-close-best-effort", @@ -1281,9 +1275,7 @@ describe("workspace aggregation", () => { }); expect(calls).toEqual([{ editorId: "vscode", path: "/tmp/repo" }]); - const response = emitted.find( - (message) => message.type === "open_in_editor_response", - ) as any; + const response = emitted.find((message) => message.type === "open_in_editor_response") as any; expect(response?.payload.error).toBeNull(); }); @@ -1574,7 +1566,8 @@ describe("workspace aggregation", () => { }), ); - session.projectRegistry.get = async (nextProjectId: string) => projects.get(nextProjectId) ?? null; + session.projectRegistry.get = async (nextProjectId: string) => + projects.get(nextProjectId) ?? null; session.projectRegistry.list = async () => Array.from(projects.values()); session.projectRegistry.upsert = async ( record: ReturnType, @@ -1762,14 +1755,16 @@ describe("workspace aggregation", () => { }); await new Promise((resolve) => setTimeout(resolve, 0)); - const response = emitted.find( - (message) => message.type === "fetch_workspaces_response", - ) as { type: "fetch_workspaces_response"; payload: any } | undefined; + const response = emitted.find((message) => message.type === "fetch_workspaces_response") as + | { type: "fetch_workspaces_response"; payload: any } + | undefined; expect( - response?.payload.entries.map((entry: typeof baselineGitDescriptor | typeof directoryDescriptor) => [ - entry.id, - entry.diffStat, - ]), + response?.payload.entries.map( + (entry: typeof baselineGitDescriptor | typeof directoryDescriptor) => [ + entry.id, + entry.diffStat, + ], + ), ).toEqual([ [directoryDescriptor.id, directoryDescriptor.diffStat], [baselineGitDescriptor.id, baselineGitDescriptor.diffStat], diff --git a/packages/server/src/server/speech/providers/local/runtime.ts b/packages/server/src/server/speech/providers/local/runtime.ts index 0fa7d2c3b..0abfe2630 100644 --- a/packages/server/src/server/speech/providers/local/runtime.ts +++ b/packages/server/src/server/speech/providers/local/runtime.ts @@ -22,7 +22,10 @@ import { SherpaParakeetRealtimeTranscriptionSession } from "./sherpa/sherpa-para import { SherpaRealtimeTranscriptionSession } from "./sherpa/sherpa-realtime-session.js"; import { SherpaOnnxSTT } from "./sherpa/sherpa-stt.js"; import { SherpaOnnxTTS } from "./sherpa/sherpa-tts.js"; -import { ensureSileroVadModel, SherpaSileroTurnDetectionProvider } from "./sherpa/silero-vad-provider.js"; +import { + ensureSileroVadModel, + SherpaSileroTurnDetectionProvider, +} from "./sherpa/silero-vad-provider.js"; type LocalSttEngine = | { kind: "offline"; engine: SherpaOfflineRecognizerEngine } @@ -244,7 +247,10 @@ export async function initializeLocalSpeechServices(params: { logger.warn({ err }, "Failed to provision Silero VAD model, falling back to bundled"); } } - turnDetectionService = new SherpaSileroTurnDetectionProvider({ modelPath: vadModelPath }, logger); + turnDetectionService = new SherpaSileroTurnDetectionProvider( + { modelPath: vadModelPath }, + logger, + ); } if (providers.voiceStt.enabled !== false && providers.voiceStt.provider === "local") { diff --git a/packages/server/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.test.ts b/packages/server/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.test.ts index 582239206..26e722fc6 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.test.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.test.ts @@ -22,7 +22,10 @@ class TestSherpaOnnxParakeetStt extends SherpaOnnxParakeetSTT { super({ engine: { sampleRate: 16000 } as any }, pino({ level: "silent" })); } - override async transcribeAudio(audioBuffer: Buffer, format: string): Promise { + override async transcribeAudio( + audioBuffer: Buffer, + format: string, + ): Promise { this.calls.push({ audio: Buffer.from(audioBuffer), format }); const deferred = createDeferred(); this.pending.push(deferred); diff --git a/packages/server/src/server/test-utils/fake-agent-client.ts b/packages/server/src/server/test-utils/fake-agent-client.ts index a2b53368e..f37e174be 100644 --- a/packages/server/src/server/test-utils/fake-agent-client.ts +++ b/packages/server/src/server/test-utils/fake-agent-client.ts @@ -243,7 +243,10 @@ class FakeAgentSession implements AgentSession { private async resolveSlashCommandInput( prompt: AgentPromptInput, ): Promise<{ commandName: string; args?: string } | null> { - if ((this.providerName !== "codex" && this.providerName !== "opencode") || typeof prompt !== "string") { + if ( + (this.providerName !== "codex" && this.providerName !== "opencode") || + typeof prompt !== "string" + ) { return null; } const parsed = this.parseSlashCommandInput(prompt); diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 7c719b454..2f0e66484 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -23,10 +23,7 @@ import { type WSOutboundMessage, wrapSessionMessage, } from "./messages.js"; -import { - asUint8Array, - decodeTerminalStreamFrame, -} from "../shared/terminal-stream-protocol.js"; +import { asUint8Array, decodeTerminalStreamFrame } from "../shared/terminal-stream-protocol.js"; import type { AllowedHostsConfig } from "./allowed-hosts.js"; import { isHostAllowed } from "./allowed-hosts.js"; import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js"; @@ -362,9 +359,10 @@ export class VoiceAssistantWebSocketServer { this.serverCapabilities = buildServerCapabilities({ readiness: this.speech?.getReadiness() ?? null, }); - this.unsubscribeSpeechReadiness = this.speech?.onReadinessChange((snapshot) => { - this.publishSpeechReadiness(snapshot); - }) ?? null; + this.unsubscribeSpeechReadiness = + this.speech?.onReadinessChange((snapshot) => { + this.publishSpeechReadiness(snapshot); + }) ?? null; const pushLogger = this.logger.child({ module: "push" }); this.pushTokenStore = new PushTokenStore(pushLogger, join(paseoHome, "push-tokens.json")); @@ -527,10 +525,7 @@ export class VoiceAssistantWebSocketServer { } } - private sendBinaryToClient( - ws: WebSocketLike, - frame: Uint8Array, - ): void { + private sendBinaryToClient(ws: WebSocketLike, frame: Uint8Array): void { if (ws.readyState !== 1) { return; } @@ -543,10 +538,7 @@ export class VoiceAssistantWebSocketServer { } } - private sendBinaryToConnection( - connection: SessionConnection, - frame: Uint8Array, - ): void { + private sendBinaryToConnection(connection: SessionConnection, frame: Uint8Array): void { for (const ws of connection.sockets) { this.sendBinaryToClient(ws, frame); } diff --git a/packages/server/src/server/workspace-registry-bootstrap.ts b/packages/server/src/server/workspace-registry-bootstrap.ts index ae839246c..c9403f810 100644 --- a/packages/server/src/server/workspace-registry-bootstrap.ts +++ b/packages/server/src/server/workspace-registry-bootstrap.ts @@ -70,7 +70,10 @@ export async function bootstrapWorkspaceRegistries(options: { const activeRecords = records.filter((record) => !record.archivedAt); const recordsByWorkspaceId = new Map< string, - { placement: Awaited>; records: StoredAgentRecord[] } + { + placement: Awaited>; + records: StoredAgentRecord[]; + } >(); for (const record of activeRecords) { const normalizedCwd = normalizeWorkspaceId(record.cwd); diff --git a/packages/server/src/server/worktree-session.ts b/packages/server/src/server/worktree-session.ts index 17d07a54e..cc9f0574a 100644 --- a/packages/server/src/server/worktree-session.ts +++ b/packages/server/src/server/worktree-session.ts @@ -22,10 +22,7 @@ import type { import { normalizeWorkspaceId as normalizePersistedWorkspaceId } from "./workspace-registry-model.js"; import { createAgentWorktree } from "./worktree-bootstrap.js"; import type { TerminalManager } from "../terminal/terminal-manager.js"; -import { - getCheckoutStatusLite, - resolveRepositoryDefaultBranch, -} from "../utils/checkout-git.js"; +import { getCheckoutStatusLite, resolveRepositoryDefaultBranch } from "../utils/checkout-git.js"; import { expandTilde } from "../utils/path.js"; import { computeWorktreePath, @@ -91,20 +88,14 @@ type RegisterPendingWorktreeWorkspaceDependencies = { }) => PersistedWorkspaceRecord; buildProjectPlacement: (cwd: string) => Promise; projectRegistry: Pick; - syncWorkspaceGitWatchTarget: ( - cwd: string, - options: { isGit: boolean }, - ) => Promise; + syncWorkspaceGitWatchTarget: (cwd: string, options: { isGit: boolean }) => Promise; workspaceRegistry: Pick; archiveProjectRecordIfEmpty: (projectId: string, archivedAt: string) => Promise; }; type CreatePaseoWorktreeInBackgroundDependencies = { paseoHome?: string; - emitWorkspaceUpdateForCwd: ( - cwd: string, - options?: { dedupeGitState?: boolean }, - ) => Promise; + emitWorkspaceUpdateForCwd: (cwd: string, options?: { dedupeGitState?: boolean }) => Promise; sessionLogger: Logger; terminalManager: TerminalManager | null; }; @@ -277,10 +268,7 @@ export function assertSafeGitRef(ref: string, label: string): void { } } -export async function resolveGitCreateBaseBranch( - cwd: string, - paseoHome?: string, -): Promise { +export async function resolveGitCreateBaseBranch(cwd: string, paseoHome?: string): Promise { const checkout = await getCheckoutStatusLite(cwd, { paseoHome }); if (!checkout.isGit) { throw new Error("Cannot create a worktree outside a git repository"); @@ -580,7 +568,11 @@ export async function handleCreatePaseoWorktreeRequest( throw new Error(`Invalid worktree name: ${validation.error}`); } - const worktreePath = await computeWorktreePath(repoRoot, normalizedSlug, dependencies.paseoHome); + const worktreePath = await computeWorktreePath( + repoRoot, + normalizedSlug, + dependencies.paseoHome, + ); const workspace = await dependencies.registerPendingWorktreeWorkspace({ repoRoot, worktreePath, diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 4805343b4..29a199984 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -2709,9 +2709,7 @@ export type CreateAgentRequestMessage = z.infer; -export type ListProviderModesRequestMessage = z.infer< - typeof ListProviderModesRequestMessageSchema ->; +export type ListProviderModesRequestMessage = z.infer; export type ListProviderFeaturesRequestMessage = z.infer< typeof ListProviderFeaturesRequestMessageSchema >; diff --git a/packages/server/src/terminal/terminal.ts b/packages/server/src/terminal/terminal.ts index 60d9c7ef7..34e1db483 100644 --- a/packages/server/src/terminal/terminal.ts +++ b/packages/server/src/terminal/terminal.ts @@ -120,10 +120,7 @@ export function ensureNodePtySpawnHelperExecutableForCurrentPlatform( } export function resolveDefaultTerminalShell( - options: { - platform?: NodeJS.Platform; - env?: NodeJS.ProcessEnv; - } = {}, + options: { platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv } = {}, ): string { const platform = options.platform ?? process.platform; const env = options.env ?? process.env; @@ -259,7 +256,10 @@ function extractCursorState(terminal: TerminalType): TerminalState["cursor"] { } function cellsToPlainText(cells: TerminalCell[], options: { stripAnsi: boolean }): string { - const text = cells.map((cell) => cell.char).join("").trimEnd(); + const text = cells + .map((cell) => cell.char) + .join("") + .trimEnd(); return options.stripAnsi ? stripAnsi(text) : text; } @@ -313,14 +313,7 @@ export function captureTerminalLines( } export async function createTerminal(options: CreateTerminalOptions): Promise { - const { - cwd, - shell, - env = {}, - rows = 24, - cols = 80, - name = "Terminal", - } = options; + const { cwd, shell, env = {}, rows = 24, cols = 80, name = "Terminal" } = options; const resolvedShell = shell ?? resolveDefaultTerminalShell(); const id = randomUUID(); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 37c50ea2c..c9f8d28e0 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -674,9 +674,7 @@ async function getMainRepoRoot(cwd: string): Promise { env: READ_ONLY_GIT_ENV, }); const worktrees = parseWorktreeList(worktreeOut); - const nonBareNonPaseo = worktrees.filter( - (wt) => !wt.isBare && !isPaseoWorktreePath(wt.path), - ); + const nonBareNonPaseo = worktrees.filter((wt) => !wt.isBare && !isPaseoWorktreePath(wt.path)); const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => isDescendantPath(wt.path, normalized)); const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main"); return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized; @@ -1477,11 +1475,7 @@ export async function getCheckoutDiff( if (diffBytes >= TOTAL_DIFF_MAX_BYTES) { break; } - const { text, truncated, stat } = await getUntrackedDiffText( - cwd, - change, - ignoreWhitespace, - ); + const { text, truncated, stat } = await getUntrackedDiffText(cwd, change, ignoreWhitespace); if (!compare.includeStructured) { if (stat?.isBinary) { @@ -1853,7 +1847,6 @@ async function resolveGitHubRepo(cwd: string): Promise { return null; } - export async function createPullRequest( cwd: string, options: CreatePullRequestOptions, @@ -1942,12 +1935,7 @@ async function getPullRequestStatusUncached(cwd: string): Promise 0 - ? pr.mergedAt - : null; + typeof pr.mergedAt === "string" && pr.mergedAt.trim().length > 0 ? pr.mergedAt : null; const state = mergedAt !== null ? "merged" diff --git a/packages/server/src/utils/executable.test.ts b/packages/server/src/utils/executable.test.ts index e3cd8dfe5..7bc638807 100644 --- a/packages/server/src/utils/executable.test.ts +++ b/packages/server/src/utils/executable.test.ts @@ -1,10 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { - findExecutable, - quoteWindowsArgument, - quoteWindowsCommand, -} from "./executable.js"; +import { findExecutable, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; type FindExecutableDependencies = NonNullable[1]>; @@ -26,17 +22,18 @@ describe("findExecutable", () => { test("on Windows, resolves executables using current machine and user PATH entries", () => { findExecutableDependencies.platform = vi.fn(() => "win32"); process.env.Path = "C:\\Windows\\System32"; - findExecutableDependencies.execFileSync.mockImplementation( - ((command: string, args?: string[]) => { - if (command === "powershell") { - return "C:\\Windows\\System32\r\nC:\\Users\\boudr\\.local\\bin\r\n"; - } - if (command === "where.exe") { - return "C:\\Users\\boudr\\.local\\bin\\claude.exe\r\n"; - } - throw new Error(`unexpected command ${command}`); - }) as any, - ); + findExecutableDependencies.execFileSync.mockImplementation((( + command: string, + args?: string[], + ) => { + if (command === "powershell") { + return "C:\\Windows\\System32\r\nC:\\Users\\boudr\\.local\\bin\r\n"; + } + if (command === "where.exe") { + return "C:\\Users\\boudr\\.local\\bin\\claude.exe\r\n"; + } + throw new Error(`unexpected command ${command}`); + }) as any); expect(findExecutable("claude", findExecutableDependencies)).toBe( "C:\\Users\\boudr\\.local\\bin\\claude.exe", @@ -60,32 +57,26 @@ describe("findExecutable", () => { test("on Windows, preserves the first where.exe match", () => { findExecutableDependencies.platform = vi.fn(() => "win32"); process.env.Path = "C:\\Windows\\System32"; - findExecutableDependencies.execFileSync.mockImplementation( - ((command: string) => { - if (command === "powershell") { - return "C:\\Windows\\System32\r\nC:\\nvm4w\\nodejs\r\n"; - } - if (command === "where.exe") { - return "C:\\nvm4w\\nodejs\\codex\r\nC:\\nvm4w\\nodejs\\codex.cmd\r\n"; - } - throw new Error(`unexpected command ${command}`); - }) as any, - ); + findExecutableDependencies.execFileSync.mockImplementation(((command: string) => { + if (command === "powershell") { + return "C:\\Windows\\System32\r\nC:\\nvm4w\\nodejs\r\n"; + } + if (command === "where.exe") { + return "C:\\nvm4w\\nodejs\\codex\r\nC:\\nvm4w\\nodejs\\codex.cmd\r\n"; + } + throw new Error(`unexpected command ${command}`); + }) as any); expect(findExecutable("codex", findExecutableDependencies)).toBe("C:\\nvm4w\\nodejs\\codex"); }); test("on Unix, uses the last line from which output", () => { - findExecutableDependencies.execFileSync.mockReturnValue( - "/usr/local/bin/codex\n", - ); + findExecutableDependencies.execFileSync.mockReturnValue("/usr/local/bin/codex\n"); expect(findExecutable("codex", findExecutableDependencies)).toBe("/usr/local/bin/codex"); - expect(findExecutableDependencies.execFileSync).toHaveBeenCalledWith( - "which", - ["codex"], - { encoding: "utf8" }, - ); + expect(findExecutableDependencies.execFileSync).toHaveBeenCalledWith("which", ["codex"], { + encoding: "utf8", + }); }); test("warns and returns null when the final which line is not an absolute path", () => { diff --git a/packages/server/src/utils/executable.ts b/packages/server/src/utils/executable.ts index 57bf5162d..08ca77870 100644 --- a/packages/server/src/utils/executable.ts +++ b/packages/server/src/utils/executable.ts @@ -92,10 +92,7 @@ export function findExecutable( if (deps.platform() === "win32") { try { const inheritedPath = process.env["Path"] ?? process.env["PATH"] ?? ""; - const resolvedPath = [ - ...inheritedPath.split(";"), - ...resolveWindowsPathEntries(deps), - ] + const resolvedPath = [...inheritedPath.split(";"), ...resolveWindowsPathEntries(deps)] .map((entry) => entry.trim()) .filter((entry) => entry.length > 0) .filter((entry, index, entries) => entries.indexOf(entry) === index) @@ -156,4 +153,3 @@ export function quoteWindowsArgument(argument: string): string { if (argument.startsWith('"') && argument.endsWith('"')) return argument; return `"${argument}"`; } - diff --git a/packages/server/src/utils/worktree.test.ts b/packages/server/src/utils/worktree.test.ts index e8ebb57dc..838df5e4f 100644 --- a/packages/server/src/utils/worktree.test.ts +++ b/packages/server/src/utils/worktree.test.ts @@ -11,9 +11,7 @@ import { runWorktreeSetupCommands, slugify, } from "./worktree"; -import { - getPaseoWorktreeMetadataPath, -} from "./worktree-metadata.js"; +import { getPaseoWorktreeMetadataPath } from "./worktree-metadata.js"; import { execSync } from "child_process"; import { mkdtempSync, rmSync, existsSync, realpathSync, writeFileSync, readFileSync } from "fs"; import { dirname, join } from "path"; diff --git a/packages/website/src/components/command-dialog.tsx b/packages/website/src/components/command-dialog.tsx index 245b71d12..da73184d9 100644 --- a/packages/website/src/components/command-dialog.tsx +++ b/packages/website/src/components/command-dialog.tsx @@ -59,14 +59,10 @@ export function CommandDialog({ >

{title}

- {description && ( -

{description}

- )} + {description &&

{description}

}
{command} - {footnote && ( -

{footnote}

- )} + {footnote &&

{footnote}

} )} diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index 613f7f856..9746143e2 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -58,7 +58,12 @@ const CHAT: ChatItem[] = [ { type: "text", text: "I'll break this down into planning and implementation.", bold: true }, { type: "tool", label: "Run plan-technical", summary: "codex · plan-technical", status: "done" }, { type: "tool", label: "Run plan-design", summary: "claude · plan-design", status: "done" }, - { type: "tool", label: "Wait for agents", summary: "plan-technical plan-design", status: "done" }, + { + type: "tool", + label: "Wait for agents", + summary: "plan-technical plan-design", + status: "done", + }, { type: "text", text: "Got the plans. Spinning up Codex for implementation.", bold: true }, { type: "tool", label: "Run implement", summary: "codex · 12 files changed", status: "done" }, { type: "text", text: "Implementation done. Requesting review from Claude." }, @@ -88,8 +93,19 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [ initial: "A", name: "acme/returns-app", workspaces: [ - { name: "main", kind: "checkout", status: "syncing", selected: true, diffStat: { additions: 247, deletions: 15 } }, - { name: "feat/dashboard", kind: "worktree", status: "done", pr: { number: 142, state: "open" } }, + { + name: "main", + kind: "checkout", + status: "syncing", + selected: true, + diffStat: { additions: 247, deletions: 15 }, + }, + { + name: "feat/dashboard", + kind: "worktree", + status: "done", + pr: { number: 142, state: "open" }, + }, ], }, { @@ -97,7 +113,13 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [ name: "acme/payments", workspaces: [ { name: "main", kind: "checkout", status: "idle" }, - { name: "fix/stripe-webhook", kind: "worktree", status: "done", diffStat: { additions: 38, deletions: 4 }, pr: { number: 89, state: "merged" } }, + { + name: "fix/stripe-webhook", + kind: "worktree", + status: "done", + diffStat: { additions: 38, deletions: 4 }, + pr: { number: 89, state: "merged" }, + }, ], }, { @@ -105,15 +127,18 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [ name: "acme/infra", workspaces: [ { name: "main", kind: "checkout", status: "idle" }, - { name: "feat/k8s-autoscale", kind: "worktree", status: "syncing", diffStat: { additions: 91, deletions: 3 } }, + { + name: "feat/k8s-autoscale", + kind: "worktree", + status: "syncing", + diffStat: { additions: 91, deletions: 3 }, + }, ], }, { initial: "D", name: "acme/design-system", - workspaces: [ - { name: "main", kind: "checkout", status: "idle" }, - ], + workspaces: [{ name: "main", kind: "checkout", status: "idle" }], }, ]; @@ -143,20 +168,145 @@ type DiffLine = { }; const DIFF_LINES: DiffLine[] = [ - { type: "add", ln: "1", tokens: [{ text: "import", cls: "text-syn-keyword" }, { text: " { ", cls: "text-syn-punctuation" }, { text: "useState", cls: "text-syn-variable" }, { text: " } ", cls: "text-syn-punctuation" }, { text: "from", cls: "text-syn-keyword" }, { text: ' "react"', cls: "text-syn-string" }] }, - { type: "add", ln: "2", tokens: [{ text: "import", cls: "text-syn-keyword" }, { text: " { ", cls: "text-syn-punctuation" }, { text: "ReturnTable", cls: "text-syn-variable" }, { text: " } ", cls: "text-syn-punctuation" }, { text: "from", cls: "text-syn-keyword" }, { text: ' "./components"', cls: "text-syn-string" }] }, + { + type: "add", + ln: "1", + tokens: [ + { text: "import", cls: "text-syn-keyword" }, + { text: " { ", cls: "text-syn-punctuation" }, + { text: "useState", cls: "text-syn-variable" }, + { text: " } ", cls: "text-syn-punctuation" }, + { text: "from", cls: "text-syn-keyword" }, + { text: ' "react"', cls: "text-syn-string" }, + ], + }, + { + type: "add", + ln: "2", + tokens: [ + { text: "import", cls: "text-syn-keyword" }, + { text: " { ", cls: "text-syn-punctuation" }, + { text: "ReturnTable", cls: "text-syn-variable" }, + { text: " } ", cls: "text-syn-punctuation" }, + { text: "from", cls: "text-syn-keyword" }, + { text: ' "./components"', cls: "text-syn-string" }, + ], + }, { type: "add", ln: "3", tokens: [] }, - { type: "add", ln: "4", tokens: [{ text: "export", cls: "text-syn-keyword" }, { text: " function", cls: "text-syn-keyword" }, { text: " Dashboard", cls: "text-syn-function" }, { text: "() {", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "5", tokens: [{ text: " const", cls: "text-syn-keyword" }, { text: " [returns, setReturns]", cls: "text-syn-variable" }, { text: " = ", cls: "text-syn-operator" }, { text: "useState", cls: "text-syn-function" }, { text: "([])", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "6", tokens: [{ text: " const", cls: "text-syn-keyword" }, { text: " [filter, setFilter]", cls: "text-syn-variable" }, { text: " = ", cls: "text-syn-operator" }, { text: "useState", cls: "text-syn-function" }, { text: '("all")', cls: "text-syn-string" }] }, + { + type: "add", + ln: "4", + tokens: [ + { text: "export", cls: "text-syn-keyword" }, + { text: " function", cls: "text-syn-keyword" }, + { text: " Dashboard", cls: "text-syn-function" }, + { text: "() {", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "5", + tokens: [ + { text: " const", cls: "text-syn-keyword" }, + { text: " [returns, setReturns]", cls: "text-syn-variable" }, + { text: " = ", cls: "text-syn-operator" }, + { text: "useState", cls: "text-syn-function" }, + { text: "([])", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "6", + tokens: [ + { text: " const", cls: "text-syn-keyword" }, + { text: " [filter, setFilter]", cls: "text-syn-variable" }, + { text: " = ", cls: "text-syn-operator" }, + { text: "useState", cls: "text-syn-function" }, + { text: '("all")', cls: "text-syn-string" }, + ], + }, { type: "add", ln: "7", tokens: [] }, - { type: "add", ln: "8", tokens: [{ text: " ", cls: "text-syn-punctuation" }, { text: "return", cls: "text-syn-keyword" }, { text: " (", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "9", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "main", cls: "text-syn-tag" }, { text: " className", cls: "text-syn-property" }, { text: '="', cls: "text-syn-punctuation" }, { text: "min-h-screen p-8", cls: "text-syn-string" }, { text: '">', cls: "text-syn-punctuation" }] }, - { type: "add", ln: "10", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "h1", cls: "text-syn-tag" }, { text: ">Customer Returns", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "11", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "FilterBar", cls: "text-syn-tag" }, { text: " value", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "filter", cls: "text-syn-variable" }, { text: "}", cls: "text-syn-punctuation" }, { text: " onChange", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "setFilter", cls: "text-syn-variable" }, { text: "} />", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "12", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "ReturnTable", cls: "text-syn-tag" }, { text: " data", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "returns", cls: "text-syn-variable" }, { text: "} />", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "13", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "StatusChart", cls: "text-syn-tag" }, { text: " data", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "returns", cls: "text-syn-variable" }, { text: "} />", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "14", tokens: [{ text: " ", cls: "text-syn-punctuation" }] }, + { + type: "add", + ln: "8", + tokens: [ + { text: " ", cls: "text-syn-punctuation" }, + { text: "return", cls: "text-syn-keyword" }, + { text: " (", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "9", + tokens: [ + { text: " <", cls: "text-syn-punctuation" }, + { text: "main", cls: "text-syn-tag" }, + { text: " className", cls: "text-syn-property" }, + { text: '="', cls: "text-syn-punctuation" }, + { text: "min-h-screen p-8", cls: "text-syn-string" }, + { text: '">', cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "10", + tokens: [ + { text: " <", cls: "text-syn-punctuation" }, + { text: "h1", cls: "text-syn-tag" }, + { text: ">Customer Returns", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "11", + tokens: [ + { text: " <", cls: "text-syn-punctuation" }, + { text: "FilterBar", cls: "text-syn-tag" }, + { text: " value", cls: "text-syn-property" }, + { text: "={", cls: "text-syn-punctuation" }, + { text: "filter", cls: "text-syn-variable" }, + { text: "}", cls: "text-syn-punctuation" }, + { text: " onChange", cls: "text-syn-property" }, + { text: "={", cls: "text-syn-punctuation" }, + { text: "setFilter", cls: "text-syn-variable" }, + { text: "} />", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "12", + tokens: [ + { text: " <", cls: "text-syn-punctuation" }, + { text: "ReturnTable", cls: "text-syn-tag" }, + { text: " data", cls: "text-syn-property" }, + { text: "={", cls: "text-syn-punctuation" }, + { text: "returns", cls: "text-syn-variable" }, + { text: "} />", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "13", + tokens: [ + { text: " <", cls: "text-syn-punctuation" }, + { text: "StatusChart", cls: "text-syn-tag" }, + { text: " data", cls: "text-syn-property" }, + { text: "={", cls: "text-syn-punctuation" }, + { text: "returns", cls: "text-syn-variable" }, + { text: "} />", cls: "text-syn-punctuation" }, + ], + }, + { + type: "add", + ln: "14", + tokens: [ + { text: " ", cls: "text-syn-punctuation" }, + ], + }, { type: "add", ln: "15", tokens: [{ text: " )", cls: "text-syn-punctuation" }] }, { type: "add", ln: "16", tokens: [{ text: "}", cls: "text-syn-punctuation" }] }, ]; @@ -207,15 +357,27 @@ function TrafficLights() { ); } -function ProviderIcon({ provider, muted = false }: { provider: "claude" | "codex" | "terminal"; muted?: boolean }) { +function ProviderIcon({ + provider, + muted = false, +}: { + provider: "claude" | "codex" | "terminal"; + muted?: boolean; +}) { const cls = muted ? "text-mock-fg-muted" : "text-mock-fg"; if (provider === "terminal") return ; - return provider === "claude" - ? - : ; + return provider === "claude" ? ( + + ) : ( + + ); } -function TabBarAction({ icon: Icon }: { icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }> }) { +function TabBarAction({ + icon: Icon, +}: { + icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }>; +}) { return (
@@ -227,16 +389,23 @@ function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boole return (
{tabs.map((tab) => ( -
+
{tab.active && ( -
+
)}
- + {tab.name} @@ -253,7 +422,15 @@ function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boole ); } -function Composer({ provider, model, focused = false }: { provider: "claude" | "codex"; model: string; focused?: boolean }) { +function Composer({ + provider, + model, + focused = false, +}: { + provider: "claude" | "codex"; + model: string; + focused?: boolean; +}) { const Icon = provider === "claude" ? ClaudeIcon : CodexIcon; return (
@@ -417,7 +594,9 @@ function ExplorerSidebar() { New + > + New +
+16 @@ -430,17 +609,29 @@ function ExplorerSidebar() { {DIFF_LINES.map((line, i) => { const isAdd = line.type === "add"; const isRemove = line.type === "remove"; - const lineBg = isAdd ? "bg-mock-diff-add" : isRemove ? "bg-mock-diff-remove" : "bg-mock-surface1"; - const lineNumCls = isAdd ? "text-mock-green-400" : isRemove ? "text-mock-red" : "text-mock-fg-muted"; + const lineBg = isAdd + ? "bg-mock-diff-add" + : isRemove + ? "bg-mock-diff-remove" + : "bg-mock-surface1"; + const lineNumCls = isAdd + ? "text-mock-green-400" + : isRemove + ? "text-mock-red" + : "text-mock-fg-muted"; return (
- {line.ln ?? ""} + + {line.ln ?? ""} +
{line.tokens?.map((tok, j) => ( - {tok.text} + + {tok.text} + ))}
@@ -456,7 +647,10 @@ function ExplorerSidebar() { { name: "returns.ts", dir: "src/api", added: 12, removed: 5 }, { name: "index.tsx", dir: "src/pages", added: 6, removed: 2 }, ].map((file) => ( -
+
{file.name} @@ -465,7 +659,9 @@ function ExplorerSidebar() { New + > + New + )}
@@ -494,7 +690,16 @@ function SyncedLoader({ size = 11 }: { size?: number }) { const gridH = dotSize * 3 + gap * 2; return ( -
+
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => { const col = dotIndex % 2; @@ -560,7 +765,9 @@ function Sidebar() { {/* Project row — icon aligns with traffic lights / sessions */}
- {project.initial} + + {project.initial} +
{project.name} @@ -569,17 +776,21 @@ function Sidebar() { {/* Workspace rows — indented one level */} {project.workspaces.map((workspace) => ( -
+
{workspace.status === "syncing" ? ( ) : ( <> - {workspace.kind === "worktree" - ? - : - } + {workspace.kind === "worktree" ? ( + + ) : ( + + )} {workspace.status !== "idle" && (
@@ -593,8 +804,12 @@ function Sidebar() { {workspace.diffStat && (
- +{workspace.diffStat.additions} - -{workspace.diffStat.deletions} + + +{workspace.diffStat.additions} + + + -{workspace.diffStat.deletions} +
)}
@@ -602,7 +817,12 @@ function Sidebar() {
- #{workspace.pr.number} · {workspace.pr.state === "open" ? "Open" : workspace.pr.state === "merged" ? "Merged" : "Closed"} + #{workspace.pr.number} ·{" "} + {workspace.pr.state === "open" + ? "Open" + : workspace.pr.state === "merged" + ? "Merged" + : "Closed"}
)} @@ -650,81 +870,94 @@ function DesktopMockup() { }, []); return ( -
+
{/* Top-level: left sidebar | center column | explorer sidebar — all full height */}
- {/* Left sidebar — full height */} - - - - - {/* Center column: title bar + split panes */} -
- {/* Title bar — belongs to center column only */} - -
-
- -
-
- main - acme/returns-app + {/* Left sidebar — full height */} + + + + + {/* Center column: title bar + split panes */} +
+ {/* Title bar — belongs to center column only */} + +
- + +
+
+ + main + + + acme/returns-app + +
+ +
-
-
-
-
- - Commit +
+
+
+ + Commit +
+
+ +
-
- +
+ + +247 + -15
-
- - +247 - -15 + + + {/* Split panes */} +
+ {/* Left pane: all agent tabs + chat */} +
+ + + + + +
-
- - {/* Split panes */} -
- {/* Left pane: all agent tabs + chat */} -
- - - - - + {/* Resize handle */} +
+ + {/* Right pane: terminal only */} + + +
- - {/* Resize handle */} -
- - {/* Right pane: terminal only */} - - - -
-
- {/* Explorer sidebar — full height, pushes center column */} - - - -
+ {/* Explorer sidebar — full height, pushes center column */} + + + +
); diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 85060e2fe..a31e7453f 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -1,5 +1,12 @@ import * as React from "react"; -import { motion, AnimatePresence, useInView, useScroll, useTransform, useMotionValueEvent } from "framer-motion"; +import { + motion, + AnimatePresence, + useInView, + useScroll, + useTransform, + useMotionValueEvent, +} from "framer-motion"; import { CursorFieldProvider } from "~/components/butterfly"; import { CommandDialog } from "~/components/command-dialog"; import { @@ -30,7 +37,6 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { {/* Hero section with background image */}
-
-
{/* Phone showcase */} @@ -334,17 +339,88 @@ function MultiProviderSection() { function SelfHostedDiagram() { const clients = [ - { name: "Desktop", icon: }, - { name: "Web", icon: }, - { name: "Mobile", icon: }, - { name: "CLI", icon: }, + { + name: "Desktop", + icon: ( + + + + + ), + }, + { + name: "Web", + icon: ( + + + + + ), + }, + { + name: "Mobile", + icon: ( + + + + + ), + }, + { + name: "CLI", + icon: ( + + + + + ), + }, ]; const hosts = ["MacBook Pro", "Hetzner VM", "Dev server"]; const containerRef = React.useRef(null); const clientRefs = React.useRef<(HTMLDivElement | null)[]>([]); const hostRefs = React.useRef<(HTMLDivElement | null)[]>([]); const centerRef = React.useRef(null); - const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({ left: [], right: [] }); + const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({ + left: [], + right: [], + }); React.useEffect(() => { function computePaths() { @@ -386,93 +462,141 @@ function SelfHostedDiagram() { return ( <> - {/* Mobile: vertical stack */} -
-
- {clients.map((c) => ( -
- {c.icon} - {c.name} -
- ))} -
-
-
-

E2E Encrypted Relay

-

or

-

Direct Connection

-
-
-
- {hosts.map((h) => ( -
- - - - - - - {h} -
- ))} + {/* Mobile: vertical stack */} +
+
+ {clients.map((c) => ( +
+ {c.icon} + {c.name} +
+ ))} +
+
+
+

E2E Encrypted Relay

+

or

+

Direct Connection

+
+
+
+ {hosts.map((h) => ( +
+ + + + + + + + + {h} +
+ ))} +
-
- - {/* Desktop: horizontal with bezier curves */} -
- {/* SVG curves */} - - {[...paths.left, ...paths.right].map((d, i) => ( - d && - ))} - - {/* Clients */} -
- {clients.map((c, i) => ( -
{ clientRefs.current[i] = el; }} - className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm" - > - {c.icon} - {c.name} -
- ))} -
+ {/* Desktop: horizontal with bezier curves */} +
+ {/* SVG curves */} + + {[...paths.left, ...paths.right].map( + (d, i) => + d && ( + + ), + )} + - {/* Spacer */} -
+ {/* Clients */} +
+ {clients.map((c, i) => ( +
{ + clientRefs.current[i] = el; + }} + className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm" + > + {c.icon} + {c.name} +
+ ))} +
- {/* Center label */} -
-

E2E Encrypted Relay

-

or

-

Direct Connection

-
+ {/* Spacer */} +
- {/* Spacer */} -
+ {/* Center label */} +
+

E2E Encrypted Relay

+

or

+

Direct Connection

+
- {/* Hosts */} -
- {hosts.map((h, i) => ( -
{ hostRefs.current[i] = el; }} - className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm" - > - - - - - - - {h} -
- ))} + {/* Spacer */} +
+ + {/* Hosts */} +
+ {hosts.map((h, i) => ( +
{ + hostRefs.current[i] = el; + }} + className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm" + > + + + + + + + + + {h} +
+ ))} +
-
); } @@ -488,7 +612,6 @@ function SelfHostedSection() { ); } - function ShortcutsSection() { const shortcuts = [ { keys: ["⌘", "1-9"], action: "Switch panels" }, @@ -564,8 +687,12 @@ function VoiceWaveform() { ); } -const USER_WORDS = "Refactor the auth middleware to use the new session store, then run the test suite".split(" "); -const RESPONSE_WORDS = "I'll update the auth middleware to use SessionStore instead of the legacy cookie-based approach. Let me refactor the middleware and update the tests.".split(" "); +const USER_WORDS = + "Refactor the auth middleware to use the new session store, then run the test suite".split(" "); +const RESPONSE_WORDS = + "I'll update the auth middleware to use SessionStore instead of the legacy cookie-based approach. Let me refactor the middleware and update the tests.".split( + " ", + ); const DICTATION_LAG = 2; const RESPONSE_LAG = 3; const WORD_APPEAR_MS = 150; @@ -573,7 +700,13 @@ const RESPONSE_WORD_MS = 60; const PHASE_GAP_MS = 800; const LOOP_PAUSE_MS = 3000; -type VoicePhase = "dictation" | "dictation-flush" | "pause" | "response" | "response-flush" | "done"; +type VoicePhase = + | "dictation" + | "dictation-flush" + | "pause" + | "response" + | "response-flush" + | "done"; function useVoiceConversation() { const [phase, setPhase] = React.useState("dictation"); @@ -594,11 +727,16 @@ function useVoiceConversation() { const t = setTimeout(() => setWordIndex((w) => w + 1), WORD_APPEAR_MS); return () => clearTimeout(t); } - const t = setTimeout(() => { setPhase("pause"); }, PHASE_GAP_MS); + const t = setTimeout(() => { + setPhase("pause"); + }, PHASE_GAP_MS); return () => clearTimeout(t); } if (phase === "pause") { - const t = setTimeout(() => { setPhase("response"); setWordIndex(0); }, PHASE_GAP_MS); + const t = setTimeout(() => { + setPhase("response"); + setWordIndex(0); + }, PHASE_GAP_MS); return () => clearTimeout(t); } if (phase === "response") { @@ -615,11 +753,16 @@ function useVoiceConversation() { const t = setTimeout(() => setWordIndex((w) => w + 1), RESPONSE_WORD_MS); return () => clearTimeout(t); } - const t = setTimeout(() => { setPhase("done"); }, LOOP_PAUSE_MS); + const t = setTimeout(() => { + setPhase("done"); + }, LOOP_PAUSE_MS); return () => clearTimeout(t); } if (phase === "done") { - const t = setTimeout(() => { setPhase("dictation"); setWordIndex(0); }, 0); + const t = setTimeout(() => { + setPhase("dictation"); + setWordIndex(0); + }, 0); return () => clearTimeout(t); } }, [phase, wordIndex]); @@ -650,7 +793,15 @@ function useVoiceConversation() { return { dictationWordIndex, responseWordIndex, showResponse }; } -function StreamingWords({ words, wordIndex, confirmLag = 2 }: { words: string[]; wordIndex: number; confirmLag?: number }) { +function StreamingWords({ + words, + wordIndex, + confirmLag = 2, +}: { + words: string[]; + wordIndex: number; + confirmLag?: number; +}) { return (
{/* Invisible full text to reserve height at any viewport width */} @@ -766,10 +917,7 @@ function GetStarted() {
@@ -819,8 +967,8 @@ function ServerInstallButton() { command="npm install -g @getpaseo/cli && paseo" footnote={ <> - Requires Node.js 18+. Run paseo to - start the daemon. + Requires Node.js 18+. Run paseo to start + the daemon. } /> @@ -967,7 +1115,6 @@ function Step({ number, children }: { number: number; children: React.ReactNode ); } - const bashKeywords = new Set([ "while", "do", @@ -1257,11 +1404,22 @@ function PhoneShowcase() { transition={{ duration: 0.5 }} className="flex flex-col items-center gap-1.5 px-6" > - +

- When you want to step away from your desk,
you can. + When you want to step away from your desk, +
you can.

The native mobile app has full feature parity with desktop. @@ -1269,7 +1427,10 @@ function PhoneShowcase() { {/* Phone trio — side phones are absolute, start behind center, slide outward with perspective rotation */} -

+
{/* Left phone — rotated to face inward */} No. You can run the daemon headless with{" "} - npm install -g @getpaseo/cli && paseo and - use the CLI, web app, or mobile app to connect. The desktop app just bundles the daemon - with a UI. + + npm install -g @getpaseo/cli && paseo + {" "} + and use the CLI, web app, or mobile app to connect. The desktop app just bundles the + daemon with a UI. Voice runs locally on your device by default. You talk, the app transcribes and sends it @@ -1455,7 +1618,9 @@ function SponsorCTA() { >

- I built Paseo because I wanted better tools for coding agents on my own setup. It's an independent open source project, built around freedom of choice and real workflows. If you like what I'm building, consider becoming a supporter. + I built Paseo because I wanted better tools for coding agents on my own setup. It's an + independent open source project, built around freedom of choice and real workflows. If you + like what I'm building, consider becoming a supporter.

- Mo

diff --git a/packages/website/src/components/mockup/icons.tsx b/packages/website/src/components/mockup/icons.tsx index 43d82090f..7d210548d 100644 --- a/packages/website/src/components/mockup/icons.tsx +++ b/packages/website/src/components/mockup/icons.tsx @@ -2,7 +2,14 @@ export function ClaudeIcon({ size = 13, className }: { size?: number; className?: string }) { return ( - + ); @@ -10,7 +17,14 @@ export function ClaudeIcon({ size = 13, className }: { size?: number; className? export function CodexIcon({ size = 13, className }: { size?: number; className?: string }) { return ( - + ); @@ -18,7 +32,17 @@ export function CodexIcon({ size = 13, className }: { size?: number; className?: export function SourceControlIcon({ size = 14, className }: { size?: number; className?: string }) { return ( - + diff --git a/packages/website/src/release.ts b/packages/website/src/release.ts index ac925cd5c..113eb0d5a 100644 --- a/packages/website/src/release.ts +++ b/packages/website/src/release.ts @@ -28,8 +28,7 @@ function versionFromTag(tag: string): string { return tag.replace(/^v/, ""); } -const GITHUB_RELEASES_URL = - "https://api.github.com/repos/getpaseo/paseo/releases?per_page=10"; +const GITHUB_RELEASES_URL = "https://api.github.com/repos/getpaseo/paseo/releases?per_page=10"; async function fetchLatestReadyRelease(): Promise { const fallback = websitePackage.version.replace(/-.*$/, ""); @@ -52,18 +51,14 @@ async function fetchLatestReadyRelease(): Promise { if (!res.ok) return fallback; const releases = (await res.json()) as GitHubRelease[]; - const ready = releases.find( - (r) => !r.prerelease && !r.draft && hasRequiredAssets(r), - ); + const ready = releases.find((r) => !r.prerelease && !r.draft && hasRequiredAssets(r)); return ready ? versionFromTag(ready.tag_name) : fallback; } catch { return fallback; } } -export const getLatestRelease = createServerFn({ method: "GET" }).handler( - async () => { - const version = await fetchLatestReadyRelease(); - return { version }; - }, -); +export const getLatestRelease = createServerFn({ method: "GET" }).handler(async () => { + const version = await fetchLatestReadyRelease(); + return { version }; +}); diff --git a/packages/website/src/routeTree.gen.ts b/packages/website/src/routeTree.gen.ts index 037ca9c97..aa618795d 100644 --- a/packages/website/src/routeTree.gen.ts +++ b/packages/website/src/routeTree.gen.ts @@ -8,440 +8,440 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from './routes/__root' -import { Route as PrivacyRouteImport } from './routes/privacy' -import { Route as OpencodeRouteImport } from './routes/opencode' -import { Route as DownloadRouteImport } from './routes/download' -import { Route as DocsRouteImport } from './routes/docs' -import { Route as CodexRouteImport } from './routes/codex' -import { Route as ClaudeCodeRouteImport } from './routes/claude-code' -import { Route as ChangelogRouteImport } from './routes/changelog' -import { Route as BlogRouteImport } from './routes/blog' -import { Route as IndexRouteImport } from './routes/index' -import { Route as DocsIndexRouteImport } from './routes/docs/index' -import { Route as BlogIndexRouteImport } from './routes/blog/index' -import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees' -import { Route as DocsVoiceRouteImport } from './routes/docs/voice' -import { Route as DocsUpdatesRouteImport } from './routes/docs/updates' -import { Route as DocsSkillsRouteImport } from './routes/docs/skills' -import { Route as DocsSecurityRouteImport } from './routes/docs/security' -import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration' -import { Route as DocsCliRouteImport } from './routes/docs/cli' -import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices' -import { Route as BlogSplatRouteImport } from './routes/blog/$' +import { Route as rootRouteImport } from "./routes/__root"; +import { Route as PrivacyRouteImport } from "./routes/privacy"; +import { Route as OpencodeRouteImport } from "./routes/opencode"; +import { Route as DownloadRouteImport } from "./routes/download"; +import { Route as DocsRouteImport } from "./routes/docs"; +import { Route as CodexRouteImport } from "./routes/codex"; +import { Route as ClaudeCodeRouteImport } from "./routes/claude-code"; +import { Route as ChangelogRouteImport } from "./routes/changelog"; +import { Route as BlogRouteImport } from "./routes/blog"; +import { Route as IndexRouteImport } from "./routes/index"; +import { Route as DocsIndexRouteImport } from "./routes/docs/index"; +import { Route as BlogIndexRouteImport } from "./routes/blog/index"; +import { Route as DocsWorktreesRouteImport } from "./routes/docs/worktrees"; +import { Route as DocsVoiceRouteImport } from "./routes/docs/voice"; +import { Route as DocsUpdatesRouteImport } from "./routes/docs/updates"; +import { Route as DocsSkillsRouteImport } from "./routes/docs/skills"; +import { Route as DocsSecurityRouteImport } from "./routes/docs/security"; +import { Route as DocsConfigurationRouteImport } from "./routes/docs/configuration"; +import { Route as DocsCliRouteImport } from "./routes/docs/cli"; +import { Route as DocsBestPracticesRouteImport } from "./routes/docs/best-practices"; +import { Route as BlogSplatRouteImport } from "./routes/blog/$"; const PrivacyRoute = PrivacyRouteImport.update({ - id: '/privacy', - path: '/privacy', + id: "/privacy", + path: "/privacy", getParentRoute: () => rootRouteImport, -} as any) +} as any); const OpencodeRoute = OpencodeRouteImport.update({ - id: '/opencode', - path: '/opencode', + id: "/opencode", + path: "/opencode", getParentRoute: () => rootRouteImport, -} as any) +} as any); const DownloadRoute = DownloadRouteImport.update({ - id: '/download', - path: '/download', + id: "/download", + path: "/download", getParentRoute: () => rootRouteImport, -} as any) +} as any); const DocsRoute = DocsRouteImport.update({ - id: '/docs', - path: '/docs', + id: "/docs", + path: "/docs", getParentRoute: () => rootRouteImport, -} as any) +} as any); const CodexRoute = CodexRouteImport.update({ - id: '/codex', - path: '/codex', + id: "/codex", + path: "/codex", getParentRoute: () => rootRouteImport, -} as any) +} as any); const ClaudeCodeRoute = ClaudeCodeRouteImport.update({ - id: '/claude-code', - path: '/claude-code', + id: "/claude-code", + path: "/claude-code", getParentRoute: () => rootRouteImport, -} as any) +} as any); const ChangelogRoute = ChangelogRouteImport.update({ - id: '/changelog', - path: '/changelog', + id: "/changelog", + path: "/changelog", getParentRoute: () => rootRouteImport, -} as any) +} as any); const BlogRoute = BlogRouteImport.update({ - id: '/blog', - path: '/blog', + id: "/blog", + path: "/blog", getParentRoute: () => rootRouteImport, -} as any) +} as any); const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => rootRouteImport, -} as any) +} as any); const DocsIndexRoute = DocsIndexRouteImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => DocsRoute, -} as any) +} as any); const BlogIndexRoute = BlogIndexRouteImport.update({ - id: '/', - path: '/', + id: "/", + path: "/", getParentRoute: () => BlogRoute, -} as any) +} as any); const DocsWorktreesRoute = DocsWorktreesRouteImport.update({ - id: '/worktrees', - path: '/worktrees', + id: "/worktrees", + path: "/worktrees", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsVoiceRoute = DocsVoiceRouteImport.update({ - id: '/voice', - path: '/voice', + id: "/voice", + path: "/voice", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsUpdatesRoute = DocsUpdatesRouteImport.update({ - id: '/updates', - path: '/updates', + id: "/updates", + path: "/updates", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsSkillsRoute = DocsSkillsRouteImport.update({ - id: '/skills', - path: '/skills', + id: "/skills", + path: "/skills", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsSecurityRoute = DocsSecurityRouteImport.update({ - id: '/security', - path: '/security', + id: "/security", + path: "/security", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsConfigurationRoute = DocsConfigurationRouteImport.update({ - id: '/configuration', - path: '/configuration', + id: "/configuration", + path: "/configuration", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsCliRoute = DocsCliRouteImport.update({ - id: '/cli', - path: '/cli', + id: "/cli", + path: "/cli", getParentRoute: () => DocsRoute, -} as any) +} as any); const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({ - id: '/best-practices', - path: '/best-practices', + id: "/best-practices", + path: "/best-practices", getParentRoute: () => DocsRoute, -} as any) +} as any); const BlogSplatRoute = BlogSplatRouteImport.update({ - id: '/$', - path: '/$', + id: "/$", + path: "/$", getParentRoute: () => BlogRoute, -} as any) +} as any); export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/blog': typeof BlogRouteWithChildren - '/changelog': typeof ChangelogRoute - '/claude-code': typeof ClaudeCodeRoute - '/codex': typeof CodexRoute - '/docs': typeof DocsRouteWithChildren - '/download': typeof DownloadRoute - '/opencode': typeof OpencodeRoute - '/privacy': typeof PrivacyRoute - '/blog/$': typeof BlogSplatRoute - '/docs/best-practices': typeof DocsBestPracticesRoute - '/docs/cli': typeof DocsCliRoute - '/docs/configuration': typeof DocsConfigurationRoute - '/docs/security': typeof DocsSecurityRoute - '/docs/skills': typeof DocsSkillsRoute - '/docs/updates': typeof DocsUpdatesRoute - '/docs/voice': typeof DocsVoiceRoute - '/docs/worktrees': typeof DocsWorktreesRoute - '/blog/': typeof BlogIndexRoute - '/docs/': typeof DocsIndexRoute + "/": typeof IndexRoute; + "/blog": typeof BlogRouteWithChildren; + "/changelog": typeof ChangelogRoute; + "/claude-code": typeof ClaudeCodeRoute; + "/codex": typeof CodexRoute; + "/docs": typeof DocsRouteWithChildren; + "/download": typeof DownloadRoute; + "/opencode": typeof OpencodeRoute; + "/privacy": typeof PrivacyRoute; + "/blog/$": typeof BlogSplatRoute; + "/docs/best-practices": typeof DocsBestPracticesRoute; + "/docs/cli": typeof DocsCliRoute; + "/docs/configuration": typeof DocsConfigurationRoute; + "/docs/security": typeof DocsSecurityRoute; + "/docs/skills": typeof DocsSkillsRoute; + "/docs/updates": typeof DocsUpdatesRoute; + "/docs/voice": typeof DocsVoiceRoute; + "/docs/worktrees": typeof DocsWorktreesRoute; + "/blog/": typeof BlogIndexRoute; + "/docs/": typeof DocsIndexRoute; } export interface FileRoutesByTo { - '/': typeof IndexRoute - '/changelog': typeof ChangelogRoute - '/claude-code': typeof ClaudeCodeRoute - '/codex': typeof CodexRoute - '/download': typeof DownloadRoute - '/opencode': typeof OpencodeRoute - '/privacy': typeof PrivacyRoute - '/blog/$': typeof BlogSplatRoute - '/docs/best-practices': typeof DocsBestPracticesRoute - '/docs/cli': typeof DocsCliRoute - '/docs/configuration': typeof DocsConfigurationRoute - '/docs/security': typeof DocsSecurityRoute - '/docs/skills': typeof DocsSkillsRoute - '/docs/updates': typeof DocsUpdatesRoute - '/docs/voice': typeof DocsVoiceRoute - '/docs/worktrees': typeof DocsWorktreesRoute - '/blog': typeof BlogIndexRoute - '/docs': typeof DocsIndexRoute + "/": typeof IndexRoute; + "/changelog": typeof ChangelogRoute; + "/claude-code": typeof ClaudeCodeRoute; + "/codex": typeof CodexRoute; + "/download": typeof DownloadRoute; + "/opencode": typeof OpencodeRoute; + "/privacy": typeof PrivacyRoute; + "/blog/$": typeof BlogSplatRoute; + "/docs/best-practices": typeof DocsBestPracticesRoute; + "/docs/cli": typeof DocsCliRoute; + "/docs/configuration": typeof DocsConfigurationRoute; + "/docs/security": typeof DocsSecurityRoute; + "/docs/skills": typeof DocsSkillsRoute; + "/docs/updates": typeof DocsUpdatesRoute; + "/docs/voice": typeof DocsVoiceRoute; + "/docs/worktrees": typeof DocsWorktreesRoute; + "/blog": typeof BlogIndexRoute; + "/docs": typeof DocsIndexRoute; } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/': typeof IndexRoute - '/blog': typeof BlogRouteWithChildren - '/changelog': typeof ChangelogRoute - '/claude-code': typeof ClaudeCodeRoute - '/codex': typeof CodexRoute - '/docs': typeof DocsRouteWithChildren - '/download': typeof DownloadRoute - '/opencode': typeof OpencodeRoute - '/privacy': typeof PrivacyRoute - '/blog/$': typeof BlogSplatRoute - '/docs/best-practices': typeof DocsBestPracticesRoute - '/docs/cli': typeof DocsCliRoute - '/docs/configuration': typeof DocsConfigurationRoute - '/docs/security': typeof DocsSecurityRoute - '/docs/skills': typeof DocsSkillsRoute - '/docs/updates': typeof DocsUpdatesRoute - '/docs/voice': typeof DocsVoiceRoute - '/docs/worktrees': typeof DocsWorktreesRoute - '/blog/': typeof BlogIndexRoute - '/docs/': typeof DocsIndexRoute + __root__: typeof rootRouteImport; + "/": typeof IndexRoute; + "/blog": typeof BlogRouteWithChildren; + "/changelog": typeof ChangelogRoute; + "/claude-code": typeof ClaudeCodeRoute; + "/codex": typeof CodexRoute; + "/docs": typeof DocsRouteWithChildren; + "/download": typeof DownloadRoute; + "/opencode": typeof OpencodeRoute; + "/privacy": typeof PrivacyRoute; + "/blog/$": typeof BlogSplatRoute; + "/docs/best-practices": typeof DocsBestPracticesRoute; + "/docs/cli": typeof DocsCliRoute; + "/docs/configuration": typeof DocsConfigurationRoute; + "/docs/security": typeof DocsSecurityRoute; + "/docs/skills": typeof DocsSkillsRoute; + "/docs/updates": typeof DocsUpdatesRoute; + "/docs/voice": typeof DocsVoiceRoute; + "/docs/worktrees": typeof DocsWorktreesRoute; + "/blog/": typeof BlogIndexRoute; + "/docs/": typeof DocsIndexRoute; } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath + fileRoutesByFullPath: FileRoutesByFullPath; fullPaths: - | '/' - | '/blog' - | '/changelog' - | '/claude-code' - | '/codex' - | '/docs' - | '/download' - | '/opencode' - | '/privacy' - | '/blog/$' - | '/docs/best-practices' - | '/docs/cli' - | '/docs/configuration' - | '/docs/security' - | '/docs/skills' - | '/docs/updates' - | '/docs/voice' - | '/docs/worktrees' - | '/blog/' - | '/docs/' - fileRoutesByTo: FileRoutesByTo + | "/" + | "/blog" + | "/changelog" + | "/claude-code" + | "/codex" + | "/docs" + | "/download" + | "/opencode" + | "/privacy" + | "/blog/$" + | "/docs/best-practices" + | "/docs/cli" + | "/docs/configuration" + | "/docs/security" + | "/docs/skills" + | "/docs/updates" + | "/docs/voice" + | "/docs/worktrees" + | "/blog/" + | "/docs/"; + fileRoutesByTo: FileRoutesByTo; to: - | '/' - | '/changelog' - | '/claude-code' - | '/codex' - | '/download' - | '/opencode' - | '/privacy' - | '/blog/$' - | '/docs/best-practices' - | '/docs/cli' - | '/docs/configuration' - | '/docs/security' - | '/docs/skills' - | '/docs/updates' - | '/docs/voice' - | '/docs/worktrees' - | '/blog' - | '/docs' + | "/" + | "/changelog" + | "/claude-code" + | "/codex" + | "/download" + | "/opencode" + | "/privacy" + | "/blog/$" + | "/docs/best-practices" + | "/docs/cli" + | "/docs/configuration" + | "/docs/security" + | "/docs/skills" + | "/docs/updates" + | "/docs/voice" + | "/docs/worktrees" + | "/blog" + | "/docs"; id: - | '__root__' - | '/' - | '/blog' - | '/changelog' - | '/claude-code' - | '/codex' - | '/docs' - | '/download' - | '/opencode' - | '/privacy' - | '/blog/$' - | '/docs/best-practices' - | '/docs/cli' - | '/docs/configuration' - | '/docs/security' - | '/docs/skills' - | '/docs/updates' - | '/docs/voice' - | '/docs/worktrees' - | '/blog/' - | '/docs/' - fileRoutesById: FileRoutesById + | "__root__" + | "/" + | "/blog" + | "/changelog" + | "/claude-code" + | "/codex" + | "/docs" + | "/download" + | "/opencode" + | "/privacy" + | "/blog/$" + | "/docs/best-practices" + | "/docs/cli" + | "/docs/configuration" + | "/docs/security" + | "/docs/skills" + | "/docs/updates" + | "/docs/voice" + | "/docs/worktrees" + | "/blog/" + | "/docs/"; + fileRoutesById: FileRoutesById; } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - BlogRoute: typeof BlogRouteWithChildren - ChangelogRoute: typeof ChangelogRoute - ClaudeCodeRoute: typeof ClaudeCodeRoute - CodexRoute: typeof CodexRoute - DocsRoute: typeof DocsRouteWithChildren - DownloadRoute: typeof DownloadRoute - OpencodeRoute: typeof OpencodeRoute - PrivacyRoute: typeof PrivacyRoute + IndexRoute: typeof IndexRoute; + BlogRoute: typeof BlogRouteWithChildren; + ChangelogRoute: typeof ChangelogRoute; + ClaudeCodeRoute: typeof ClaudeCodeRoute; + CodexRoute: typeof CodexRoute; + DocsRoute: typeof DocsRouteWithChildren; + DownloadRoute: typeof DownloadRoute; + OpencodeRoute: typeof OpencodeRoute; + PrivacyRoute: typeof PrivacyRoute; } -declare module '@tanstack/react-router' { +declare module "@tanstack/react-router" { interface FileRoutesByPath { - '/privacy': { - id: '/privacy' - path: '/privacy' - fullPath: '/privacy' - preLoaderRoute: typeof PrivacyRouteImport - parentRoute: typeof rootRouteImport - } - '/opencode': { - id: '/opencode' - path: '/opencode' - fullPath: '/opencode' - preLoaderRoute: typeof OpencodeRouteImport - parentRoute: typeof rootRouteImport - } - '/download': { - id: '/download' - path: '/download' - fullPath: '/download' - preLoaderRoute: typeof DownloadRouteImport - parentRoute: typeof rootRouteImport - } - '/docs': { - id: '/docs' - path: '/docs' - fullPath: '/docs' - preLoaderRoute: typeof DocsRouteImport - parentRoute: typeof rootRouteImport - } - '/codex': { - id: '/codex' - path: '/codex' - fullPath: '/codex' - preLoaderRoute: typeof CodexRouteImport - parentRoute: typeof rootRouteImport - } - '/claude-code': { - id: '/claude-code' - path: '/claude-code' - fullPath: '/claude-code' - preLoaderRoute: typeof ClaudeCodeRouteImport - parentRoute: typeof rootRouteImport - } - '/changelog': { - id: '/changelog' - path: '/changelog' - fullPath: '/changelog' - preLoaderRoute: typeof ChangelogRouteImport - parentRoute: typeof rootRouteImport - } - '/blog': { - id: '/blog' - path: '/blog' - fullPath: '/blog' - preLoaderRoute: typeof BlogRouteImport - parentRoute: typeof rootRouteImport - } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - '/docs/': { - id: '/docs/' - path: '/' - fullPath: '/docs/' - preLoaderRoute: typeof DocsIndexRouteImport - parentRoute: typeof DocsRoute - } - '/blog/': { - id: '/blog/' - path: '/' - fullPath: '/blog/' - preLoaderRoute: typeof BlogIndexRouteImport - parentRoute: typeof BlogRoute - } - '/docs/worktrees': { - id: '/docs/worktrees' - path: '/worktrees' - fullPath: '/docs/worktrees' - preLoaderRoute: typeof DocsWorktreesRouteImport - parentRoute: typeof DocsRoute - } - '/docs/voice': { - id: '/docs/voice' - path: '/voice' - fullPath: '/docs/voice' - preLoaderRoute: typeof DocsVoiceRouteImport - parentRoute: typeof DocsRoute - } - '/docs/updates': { - id: '/docs/updates' - path: '/updates' - fullPath: '/docs/updates' - preLoaderRoute: typeof DocsUpdatesRouteImport - parentRoute: typeof DocsRoute - } - '/docs/skills': { - id: '/docs/skills' - path: '/skills' - fullPath: '/docs/skills' - preLoaderRoute: typeof DocsSkillsRouteImport - parentRoute: typeof DocsRoute - } - '/docs/security': { - id: '/docs/security' - path: '/security' - fullPath: '/docs/security' - preLoaderRoute: typeof DocsSecurityRouteImport - parentRoute: typeof DocsRoute - } - '/docs/configuration': { - id: '/docs/configuration' - path: '/configuration' - fullPath: '/docs/configuration' - preLoaderRoute: typeof DocsConfigurationRouteImport - parentRoute: typeof DocsRoute - } - '/docs/cli': { - id: '/docs/cli' - path: '/cli' - fullPath: '/docs/cli' - preLoaderRoute: typeof DocsCliRouteImport - parentRoute: typeof DocsRoute - } - '/docs/best-practices': { - id: '/docs/best-practices' - path: '/best-practices' - fullPath: '/docs/best-practices' - preLoaderRoute: typeof DocsBestPracticesRouteImport - parentRoute: typeof DocsRoute - } - '/blog/$': { - id: '/blog/$' - path: '/$' - fullPath: '/blog/$' - preLoaderRoute: typeof BlogSplatRouteImport - parentRoute: typeof BlogRoute - } + "/privacy": { + id: "/privacy"; + path: "/privacy"; + fullPath: "/privacy"; + preLoaderRoute: typeof PrivacyRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/opencode": { + id: "/opencode"; + path: "/opencode"; + fullPath: "/opencode"; + preLoaderRoute: typeof OpencodeRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/download": { + id: "/download"; + path: "/download"; + fullPath: "/download"; + preLoaderRoute: typeof DownloadRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/docs": { + id: "/docs"; + path: "/docs"; + fullPath: "/docs"; + preLoaderRoute: typeof DocsRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/codex": { + id: "/codex"; + path: "/codex"; + fullPath: "/codex"; + preLoaderRoute: typeof CodexRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/claude-code": { + id: "/claude-code"; + path: "/claude-code"; + fullPath: "/claude-code"; + preLoaderRoute: typeof ClaudeCodeRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/changelog": { + id: "/changelog"; + path: "/changelog"; + fullPath: "/changelog"; + preLoaderRoute: typeof ChangelogRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/blog": { + id: "/blog"; + path: "/blog"; + fullPath: "/blog"; + preLoaderRoute: typeof BlogRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/docs/": { + id: "/docs/"; + path: "/"; + fullPath: "/docs/"; + preLoaderRoute: typeof DocsIndexRouteImport; + parentRoute: typeof DocsRoute; + }; + "/blog/": { + id: "/blog/"; + path: "/"; + fullPath: "/blog/"; + preLoaderRoute: typeof BlogIndexRouteImport; + parentRoute: typeof BlogRoute; + }; + "/docs/worktrees": { + id: "/docs/worktrees"; + path: "/worktrees"; + fullPath: "/docs/worktrees"; + preLoaderRoute: typeof DocsWorktreesRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/voice": { + id: "/docs/voice"; + path: "/voice"; + fullPath: "/docs/voice"; + preLoaderRoute: typeof DocsVoiceRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/updates": { + id: "/docs/updates"; + path: "/updates"; + fullPath: "/docs/updates"; + preLoaderRoute: typeof DocsUpdatesRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/skills": { + id: "/docs/skills"; + path: "/skills"; + fullPath: "/docs/skills"; + preLoaderRoute: typeof DocsSkillsRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/security": { + id: "/docs/security"; + path: "/security"; + fullPath: "/docs/security"; + preLoaderRoute: typeof DocsSecurityRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/configuration": { + id: "/docs/configuration"; + path: "/configuration"; + fullPath: "/docs/configuration"; + preLoaderRoute: typeof DocsConfigurationRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/cli": { + id: "/docs/cli"; + path: "/cli"; + fullPath: "/docs/cli"; + preLoaderRoute: typeof DocsCliRouteImport; + parentRoute: typeof DocsRoute; + }; + "/docs/best-practices": { + id: "/docs/best-practices"; + path: "/best-practices"; + fullPath: "/docs/best-practices"; + preLoaderRoute: typeof DocsBestPracticesRouteImport; + parentRoute: typeof DocsRoute; + }; + "/blog/$": { + id: "/blog/$"; + path: "/$"; + fullPath: "/blog/$"; + preLoaderRoute: typeof BlogSplatRouteImport; + parentRoute: typeof BlogRoute; + }; } } interface BlogRouteChildren { - BlogSplatRoute: typeof BlogSplatRoute - BlogIndexRoute: typeof BlogIndexRoute + BlogSplatRoute: typeof BlogSplatRoute; + BlogIndexRoute: typeof BlogIndexRoute; } const BlogRouteChildren: BlogRouteChildren = { BlogSplatRoute: BlogSplatRoute, BlogIndexRoute: BlogIndexRoute, -} +}; -const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren); interface DocsRouteChildren { - DocsBestPracticesRoute: typeof DocsBestPracticesRoute - DocsCliRoute: typeof DocsCliRoute - DocsConfigurationRoute: typeof DocsConfigurationRoute - DocsSecurityRoute: typeof DocsSecurityRoute - DocsSkillsRoute: typeof DocsSkillsRoute - DocsUpdatesRoute: typeof DocsUpdatesRoute - DocsVoiceRoute: typeof DocsVoiceRoute - DocsWorktreesRoute: typeof DocsWorktreesRoute - DocsIndexRoute: typeof DocsIndexRoute + DocsBestPracticesRoute: typeof DocsBestPracticesRoute; + DocsCliRoute: typeof DocsCliRoute; + DocsConfigurationRoute: typeof DocsConfigurationRoute; + DocsSecurityRoute: typeof DocsSecurityRoute; + DocsSkillsRoute: typeof DocsSkillsRoute; + DocsUpdatesRoute: typeof DocsUpdatesRoute; + DocsVoiceRoute: typeof DocsVoiceRoute; + DocsWorktreesRoute: typeof DocsWorktreesRoute; + DocsIndexRoute: typeof DocsIndexRoute; } const DocsRouteChildren: DocsRouteChildren = { @@ -454,9 +454,9 @@ const DocsRouteChildren: DocsRouteChildren = { DocsVoiceRoute: DocsVoiceRoute, DocsWorktreesRoute: DocsWorktreesRoute, DocsIndexRoute: DocsIndexRoute, -} +}; -const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren); const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, @@ -468,16 +468,16 @@ const rootRouteChildren: RootRouteChildren = { DownloadRoute: DownloadRoute, OpencodeRoute: OpencodeRoute, PrivacyRoute: PrivacyRoute, -} +}; export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileTypes(); -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { +import type { getRouter } from "./router.tsx"; +import type { createStart } from "@tanstack/react-start"; +declare module "@tanstack/react-start" { interface Register { - ssr: true - router: Awaited> + ssr: true; + router: Awaited>; } } diff --git a/packages/website/src/routes/docs/index.tsx b/packages/website/src/routes/docs/index.tsx index 6a98d075c..ceacc4a99 100644 --- a/packages/website/src/routes/docs/index.tsx +++ b/packages/website/src/routes/docs/index.tsx @@ -17,8 +17,8 @@ function GettingStarted() {

Getting Started

- Paseo has three main pieces: the daemon is the local server that manages your agents, - the app is the client you use from mobile, web, or desktop, and the CLI is the terminal + Paseo has three main pieces: the daemon is the local server that manages your agents, the + app is the client you use from mobile, web, or desktop, and the CLI is the terminal interface that can also launch the daemon.

diff --git a/packages/website/src/routes/docs/skills.tsx b/packages/website/src/routes/docs/skills.tsx index 94d4f7d85..a18bef073 100644 --- a/packages/website/src/routes/docs/skills.tsx +++ b/packages/website/src/routes/docs/skills.tsx @@ -26,9 +26,9 @@ function Skills() {

Orchestration Skills

Paseo ships orchestration skills that teach coding agents (Claude Code, Codex) how to use - the Paseo CLI to spawn, coordinate, and manage other agents. Skills are slash commands your - agent can invoke — they provide the prompts, context, and workflows so agents know how to - orchestrate without you writing boilerplate. Install them from the desktop app's + the Paseo CLI to spawn, coordinate, and manage other agents. Skills are slash commands + your agent can invoke — they provide the prompts, context, and workflows so agents know + how to orchestrate without you writing boilerplate. Install them from the desktop app's Integrations settings or via the CLI.

@@ -42,9 +42,10 @@ function Skills() { Desktop app: Settings → Integrations → Install
  • - Manual: npx skills add getpaseo/paseo{" "} - — this installs to ~/.agents/skills/ and sets up - symlinks for each agent. + Manual:{" "} + npx skills add getpaseo/paseo — this installs to{" "} + ~/.agents/skills/ and sets up symlinks for each + agent.
  • @@ -90,8 +91,8 @@ function Skills() {

    Runs an agent in a loop with automatic verification until an exit condition is met. Worker - runs, verifier checks, repeat until done or max iterations. Supports different providers for - worker vs verifier (e.g., Codex implements, Claude verifies). + runs, verifier checks, repeat until done or max iterations. Supports different providers + for worker vs verifier (e.g., Codex implements, Claude verifies).

    Stop conditions: --max-iterations,{" "} @@ -110,8 +111,8 @@ function Skills() {

    Builds and manages a team of agents coordinating through a shared chat room. You describe - the work, it sets up roles, launches agents, and coordinates through chat. Uses a heartbeat - schedule to check progress. + the work, it sets up roles, launches agents, and coordinates through chat. Uses a + heartbeat schedule to check progress.

    Cross-provider: typically Codex for implementation, Claude for review. @@ -127,8 +128,9 @@ function Skills() { /paseo-chat — Chat Rooms

    - Use persistent chat rooms for asynchronous agent coordination. Create rooms, post messages, - read history, wait for replies. Supports @mentions for specific agents or @everyone. + Use persistent chat rooms for asynchronous agent coordination. Create rooms, post + messages, read history, wait for replies. Supports @mentions for specific agents or + @everyone.

    Typically used by the orchestrator skill, but can be used directly. @@ -145,9 +147,9 @@ function Skills() { /paseo-committee — Committee Planning

    - Forms a committee of two high-reasoning agents (Claude Opus + GPT 5.4) to analyze a problem - before implementing. Both agents reason in parallel, then plans are merged. Useful when - stuck, looping, or facing a hard architectural decision. + Forms a committee of two high-reasoning agents (Claude Opus + GPT 5.4) to analyze a + problem before implementing. Both agents reason in parallel, then plans are merged. Useful + when stuck, looping, or facing a hard architectural decision.

    Agents are prevented from editing code — they only produce a plan. diff --git a/packages/website/src/routes/docs/worktrees.tsx b/packages/website/src/routes/docs/worktrees.tsx index 17e959d95..39745339d 100644 --- a/packages/website/src/routes/docs/worktrees.tsx +++ b/packages/website/src/routes/docs/worktrees.tsx @@ -172,9 +172,9 @@ function Worktrees() { checkout to the worktree.

    - $PASEO_WORKTREE_PORT is available when the worktree - was bootstrapped with a port. That makes it useful for both starting services in setup - and stopping them again in teardown. + $PASEO_WORKTREE_PORT is available when the worktree was + bootstrapped with a port. That makes it useful for both starting services in setup and + stopping them again in teardown.

    diff --git a/packages/website/src/routes/download.tsx b/packages/website/src/routes/download.tsx index e28acbed2..d148b0f14 100644 --- a/packages/website/src/routes/download.tsx +++ b/packages/website/src/routes/download.tsx @@ -90,9 +90,7 @@ function Download() {

    Download

    -

    - v{version} -

    +

    v{version}

    {/* Desktop */}
    @@ -130,10 +128,7 @@ function Download() { Windows
    - +
    @@ -144,10 +139,7 @@ function Download() { Linux
    - +
    @@ -170,15 +162,8 @@ function Download() { Android
    - - + +
    @@ -189,11 +174,7 @@ function Download() { iOS
    - +
    @@ -213,11 +194,7 @@ function Download() { Web App
    - +
    diff --git a/packages/website/src/routes/index.tsx b/packages/website/src/routes/index.tsx index 73572272f..ece04c498 100644 --- a/packages/website/src/routes/index.tsx +++ b/packages/website/src/routes/index.tsx @@ -15,7 +15,13 @@ export const Route = createFileRoute("/")({ function Home() { return ( The development environment
    built for coding agents} + title={ + <> + The development environment +
    + built for coding agents + + } subtitle="Run any coding agent from your phone, desktop, or terminal. Self-hosted, multi-provider, open source." /> ); diff --git a/packages/website/src/styles.css b/packages/website/src/styles.css index c54937692..75eae3571 100644 --- a/packages/website/src/styles.css +++ b/packages/website/src/styles.css @@ -1,8 +1,12 @@ @import "tailwindcss"; @keyframes voice-bar { - 0% { transform: scaleY(1); } - 100% { transform: scaleY(0.3); } + 0% { + transform: scaleY(1); + } + 100% { + transform: scaleY(0.3); + } } /* Title font - centralized for easy changes */ @@ -189,26 +193,26 @@ @theme { --color-background: #101615; --color-foreground: #fafafa; - --color-muted: #252B2A; - --color-muted-foreground: #A8ADAC; - --color-card: #171D1C; - --color-border: #252B2A; + --color-muted: #252b2a; + --color-muted-foreground: #a8adac; + --color-card: #171d1c; + --color-border: #252b2a; --color-primary: #3b82f6; --color-primary-foreground: #ffffff; - --color-secondary: #252B2A; + --color-secondary: #252b2a; --color-secondary-foreground: #fafafa; /* Mockup palette (from app unistyles theme) */ - --color-mock-surface0: #181B1A; - --color-mock-surface1: #1E2120; - --color-mock-surface2: #272A29; + --color-mock-surface0: #181b1a; + --color-mock-surface1: #1e2120; + --color-mock-surface2: #272a29; --color-mock-surface3: #434645; --color-mock-sidebar: #141716; --color-mock-fg: #fafafa; - --color-mock-fg-muted: #A1A5A4; - --color-mock-border: #272A29; + --color-mock-fg-muted: #a1a5a4; + --color-mock-border: #272a29; --color-mock-border-accent: #313433; - --color-mock-accent: #20744A; + --color-mock-accent: #20744a; --color-mock-green: #22c55e; --color-mock-green-400: #4ade80; --color-mock-red: #ef4444; @@ -243,12 +247,30 @@ up to 4 dots are visible at decreasing opacity (head=1, trail=0.72/0.46/0.22). steps(1,end) keeps each opacity level solid for its full 1/6-cycle slot. */ @keyframes synced-snake-dot { - 0% { opacity: 1; animation-timing-function: steps(1, end); } - 16.667% { opacity: 0.72; animation-timing-function: steps(1, end); } - 33.333% { opacity: 0.46; animation-timing-function: steps(1, end); } - 50% { opacity: 0.22; animation-timing-function: steps(1, end); } - 66.667% { opacity: 0; animation-timing-function: steps(1, end); } - 83.333% { opacity: 0; animation-timing-function: steps(1, end); } + 0% { + opacity: 1; + animation-timing-function: steps(1, end); + } + 16.667% { + opacity: 0.72; + animation-timing-function: steps(1, end); + } + 33.333% { + opacity: 0.46; + animation-timing-function: steps(1, end); + } + 50% { + opacity: 0.22; + animation-timing-function: steps(1, end); + } + 66.667% { + opacity: 0; + animation-timing-function: steps(1, end); + } + 83.333% { + opacity: 0; + animation-timing-function: steps(1, end); + } } @keyframes flutter-left { diff --git a/scripts/fix-lockfile.mjs b/scripts/fix-lockfile.mjs index 584ced79a..3b7ec154e 100644 --- a/scripts/fix-lockfile.mjs +++ b/scripts/fix-lockfile.mjs @@ -55,7 +55,7 @@ for (const [key, val] of Object.entries(lock.packages || {})) { execSync(`npm view ${pkgName}@${version} --json dist`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], - }) + }), ); if (info.tarball && info.integrity) { val.resolved = info.tarball; diff --git a/scripts/release-version-utils.mjs b/scripts/release-version-utils.mjs index 777b3ce85..a21c40365 100644 --- a/scripts/release-version-utils.mjs +++ b/scripts/release-version-utils.mjs @@ -1,4 +1,5 @@ -const versionPattern = /^(?\d+)\.(?\d+)\.(?\d+)(?:-(?[0-9A-Za-z.-]+))?$/; +const versionPattern = + /^(?\d+)\.(?\d+)\.(?\d+)(?:-(?[0-9A-Za-z.-]+))?$/; const sourceTagPattern = /^(?:(?:desktop(?:-(?:windows|linux|macos))?|android)-)?v(?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/; diff --git a/scripts/set-release-version.mjs b/scripts/set-release-version.mjs index a56ca6151..03908f49e 100644 --- a/scripts/set-release-version.mjs +++ b/scripts/set-release-version.mjs @@ -63,12 +63,6 @@ if (args.print) { execFileSync( "npm", - [ - "version", - nextVersion, - "--include-workspace-root", - "--message", - "chore(release): cut %s", - ], + ["version", nextVersion, "--include-workspace-root", "--message", "chore(release): cut %s"], { cwd: rootDir, stdio: "inherit" }, );