chore: drive linter to zero, promote warn -> error in CI (#545)
* chore(lint): use Set for LOG_FORMATS membership check
* chore(lint): convert type aliases to interfaces (autofix)
- Remove no-use-before-define rule (conflicts with unistyles ordering)
- Add typescript/consistent-type-definitions: interface
- Run oxlint --fix: 606 type->interface conversions across 268 files
Typecheck green. Warnings: 5432 -> 3787 (-1645).
* chore(lint): clean up relay package warnings
* chore(lint): clean up cli package warnings
* chore(lint): flatten nested ternaries in server (no-nested-ternary)
* chore(lint): escape entities and hoist inline objects in website
* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)
* fix(types): restore Record assignability after type->interface autofix
* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)
* chore(lint): clean up desktop and highlight packages
* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)
* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)
* chore(lint): format keyboard-shortcuts-section
* chore(lint): rename shadowed bindings in server (no-shadow)
Rename inner bindings that shadow outer imports or function params:
- Promise executor `resolve` -> `resolvePromise` (shadowed path `resolve`)
- Method params `options` -> `input`/`target`/`update`/`runOptions`/`opts`/`killOptions`
- Misc loop/destructure renames for `workspaceId`, `scriptNames`, `path`, `query`, `taskNotificationItem`
Mechanical change only; no behavior change.
* fix(types): add index signatures for Record assignability
* chore(lint): extract nested callbacks in server (max-nested-callbacks)
* chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)
Work in progress: 30 of 369 warnings fixed across 23 files.
* chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)
* chore(lint): reduce cyclomatic complexity in server
* chore(lint): hoist inline callbacks in app components (jsx-no-new-function-as-prop)
* chore(lint): parallelize safe await-in-loop in server
* chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)
Finish eliminating jsx-no-new-function-as-prop warnings in
agent-status-bar.tsx and complete refactor of git-diff-pane.tsx
by extracting per-item components and using stable useCallback
handlers.
* chore(lint): parallelize more safe await-in-loop in server
* fix(server): restore microtask ordering in message dispatch and stream event handler
The complexity refactor added async dispatcher chains that inserted extra
microtasks before message handlers ran, and wrapped the stream event switch
in an await that fired between emitState and dispatchStream. Two tests
regressed on both counts. Route to the matching dispatcher synchronously
and skip the await when the stream handler has no async work.
* chore(lint): hoist/memoize inline arrays and objects in app JSX
* chore(lint): parallelize safe awaits in server (no-await-in-loop)
Converts three sequential `for ... await` loops to `Promise.all`:
- OpenCode: configure MCP servers in parallel
- Sherpa: download model files and ensure multiple models concurrently
- Speech runtime: check required model files across models in parallel
* chore(lint): clear website warnings to zero
Resolves all 61 oxlint warnings in packages/website/ by hoisting inline
callbacks/JSX, extracting sub-components to satisfy jsx-max-depth, using
stable data-derived keys, adding explicit button types, flattening nested
ternaries, and removing unused code.
* chore(lint): parallelize more safe awaits in server (no-await-in-loop)
- Workspace reconciliation: archive missing workspaces and orphaned
projects concurrently instead of sequentially.
- Directory suggestions: resolve child directory candidates in parallel
before filtering.
* chore(lint): allow css imports in import/no-unassigned-import, hoist host page styles
* chore(lint): hoist dictation-controls inline arrays
* fix(server): restore microtask ordering in session message dispatch
Converts per-group dispatchers to Promise<void> | undefined. The complexity
refactor broke two tests by inserting extra microtasks between each
dispatcher's await. The new pattern routes synchronously via a nullish-
coalescing chain and awaits only the matching dispatcher's promise.
* chore(lint): flatten nested ternaries in button/volume-meter/autocomplete
* chore(lint): narrow explicit any in misc server files
* chore(lint): flatten nested ternaries in host-runtime/host-page/providers
* chore(lint): clear desktop warnings to zero
Convert polling loops to recursive helpers to avoid no-await-in-loop,
and switch the ws import to the named export so the WebSocket type is
referenced directly.
* chore(lint): remove useless constructors in server tests
* chore(lint): hoist pair-scan inline arrays and objects
Extracts corner style arrays and barcode scanner settings to module
scope, memoizes insets-dependent body/helper text styles.
* chore(lint): hoist inline style arrays in question-form-card
* chore(lint): hoist/memoize inline styles in plan-card
Extracts markdown rule style arrays into dedicated MarkdownInlineText,
MarkdownListItemContent, and MarkdownParagraph subcomponents that
memoize their own style arrays. Memoizes PlanCard container/title/
description styles.
* chore(lint): narrow explicit any in server relay/loader/logger-likes
* chore(lint): hoist SheetBackground combined style
* chore(lint): hoist inline styles in screen-header
* chore(lint): hoist inline styles/objects in menu-header
* chore(lint): hoist top-level inline arrays in diff-viewer
* chore(lint): narrow explicit any in codex-app-server-agent thread items
* chore(lint): reduce max-depth in claude-sdk-behavior test
* chore(lint): memoize inline styles in command-center
* chore(lint): extract helper to reduce max-depth in agent-response-loop
* chore(lint): flatten nested conditions in pi-direct-agent history
* chore(lint): reduce max-depth in claude-agent query pump
* chore(lint): clear cli warnings to zero
Refactor polling loops into recursive helpers to satisfy
no-await-in-loop, fix WebSocket named import, and correct
an absolute-path import in tests/tmp.
* chore(lint): memoize inline styles in context-menu
* chore(lint): narrow explicit any in claude-agent/openai stt
* chore(lint): reduce max-depth in opencode-agent foreground loop
* chore(lint): memoize inline styles in explorer-sidebar
* chore(lint): memoize mobile sidebar styles in explorer-sidebar
* chore(lint): memoize inline styles in agent-list
* chore(lint): reduce relay test no-await-in-loop warnings
Refactor while-loops and for-retry loops in e2e.test.ts and
live-relay.e2e.test.ts into recursive poll/attempt helpers.
Remaining 8 warnings in encrypted-channel.ts concern the custom
Transport interface (on-handler slots, not DOM EventTarget) and a
sequential send loop; both reflect deliberate runtime contracts.
* chore(lint): fix typecheck errors from hoisting refactors
- pair-scan: type BARCODE_SCANNER_SETTINGS as BarcodeSettings
- explorer-sidebar: add missing desktopSidebarStyle useMemo
- sidebar-workspace-list: coerce null dotColor to transparent
- menu-header, e2e.test: formatting
* chore(lint): memoize inline styles in workspace-screen
* chore(lint): memoize inline styles in workspace-desktop-tabs-row
* chore(lint): hoist constant style arrays in desktop-updates-section
* chore(lint): memoize inline styles in combobox
* chore(lint): extract SplitGroupChild to memoize inline styles
* chore(lint): memoize inline styles in dropdown-menu
* chore(lint): memoize inline styles in workspace-hover-card
* chore(lint): memoize inline styles in autocomplete, message-input, composer
* chore(lint): remove explicit any in server (129 warnings)
- session.ts: catch(error: any) -> catch(error) with Error coercion at use
- daemon-client transport: any -> unknown in listener types
- sherpa/onnx loaders: introduce structural native types
- pocket-tts-onnx: typed ONNX session inputs/outputs/tensors
- tests: any -> unknown + named stub types
* chore(lint): hoist inline arrays and objects in app (145 warnings)
- Hoist static style arrays/objects to module-level consts
- Memoize dynamic ones with useMemo and correct deps
- jsx-no-new-array-as-prop: 145 -> 43
- jsx-no-new-object-as-prop: 64 -> 21
* chore(lint): memoize inline styles in _layout
Fixes 3 react-perf/jsx-no-new-object-as-prop warnings in root layout by
memoizing the stack screen options, the agent screen override, and the
GestureHandlerRootView root style.
* chore(lint): memoize inline styles and objects in stream/status panes
Clears 10 react-perf/jsx-no-new warnings across welcome-screen,
file-explorer-pane, terminal-pane, agent-stream-view, and
agent-status-bar by extracting per-item subcomponents, hoisting constant
style tuples, and memoizing derived arrays/objects.
* chore(lint): memoize inline style objects in list/pane components
Clears 6 react-perf/jsx-no-new-object-as-prop warnings across
synced-loader, stream-strategy-web, sortable-inline-list, file-pane, and
both draggable-list platform variants by memoizing or hoisting their
inline style objects.
* chore(lint): hoist test fixture arrays/objects
Clears 14 react-perf/jsx-no-new-* warnings across app test files by
hoisting constant fixtures to module scope or wrapping them in helper
factories so they no longer appear as inline JSX prop values.
* chore(lint): memoize inline styles in message components
Clears 5 react-perf/jsx-no-new-* warnings in message.tsx by memoizing
image source objects, extracting an AssistantMessageBlockContainer for
per-block spacing, and moving todo list item rendering into a
TodoListItemRow subcomponent so each row memoizes its own style arrays.
The remaining 8 warnings in react-native-markdown-display render rules
are inherent to that library's rule API.
* chore(lint): remove unused vitest imports in server tests
* chore(lint): remove unused helpers and vars in server tests
* chore(lint): add explicit returns in then() callbacks in server
* chore(lint): fix no-shadow and no-nested-ternary in server
* chore(lint): flatten nested blocks to satisfy max-depth in server
* chore(lint): extract helpers to satisfy max-depth in server
* chore(lint): parallelize Claude persisted agents lookup
Replace sequential parseClaudeSessionDescriptor loop with Promise.all
to fix no-await-in-loop warning.
* chore(lint): parallelize Linux watch directory traversal
Switch BFS to level-by-level Promise.all over readdir calls, replacing
the sequential pop/await loop that triggered no-await-in-loop.
* chore(lint): parallelize workspace registry bootstrap upserts
Collect per-workspace upsert inputs in a sync pass, then run registry
writes via Promise.all to eliminate no-await-in-loop warnings.
* chore(lint): parallelize test daemon cleanup rm calls
Run the paseoHomeRoot/staticDir removals concurrently in close() and
the startup catch block to fix no-await-in-loop.
* chore(lint): mechanical cleanup in app (unused, shadow, nested-ternary)
Recovers in-flight edits from the app mechanical agent that couldn't be
pushed due to concurrent tree contention. Removes unused helpers and
locals, flattens nested ternaries, narrows a few props where unused.
* chore(lint): parallelize pending permission approvals
approvePendingPermissions now filters and records handled IDs in a
sync pass, then awaits all allowPermission calls concurrently instead
of looping awaits.
* chore(lint): parallelize rebase head-name lookup
Try both rebase backends (rebase-merge, rebase-apply) concurrently
via Promise.all and return the first non-null result.
* chore(lint): no-unused-vars in app
* chore(lint): parallelize worktree terminal bootstrap
Start each bootstrap terminal concurrently via Promise.all. Since every
spec creates an independent terminal and returns a standalone result,
parallelization preserves order while removing two no-await-in-loop
warnings.
* chore(lint): parallelize worktree bootstrap test cleanup and script spawn
Run terminal cleanup across managers and spawnWorkspaceScript for api
and web concurrently to eliminate no-await-in-loop warnings while
preserving semantics.
* chore(lint): prefer-add-event-listener in app
* chore(lint): parallelize script-health-monitor afterEach and spawns
Close all stub TCP servers concurrently and spawn typecheck/api
scripts via Promise.all to drop two no-await-in-loop warnings.
* chore(lint): jsx-no-useless-fragment in app
* chore(lint): parallelize executable PATH probing
* chore(lint): parallelize client and relay transport test cleanup
* chore(lint): parallelize agent storage record file probing
* chore(lint): no-array-index-key in app
* chore(lint): parallelize bootstrap provider availability cleanup
* chore(lint): parallelize Codex rollout file search
* chore(lint): parallelize dictation wav fixture search
* chore(lint): parallelize Claude session id fixture test
* chore(lint): parallelize script health monitor server cleanup
* chore(lint): parallelize Codex skills directory scan
* chore(lint): promise/always-return in app
* chore(lint): no-shadow in use-dictation, hoist icons in splash screen
* chore(lint): no-shadow in composer callbacks
* chore(format): apply oxfmt to 3 server files
* chore(lint): no-explicit-any in stt-manager/chat-mentions tests
* chore(lint): no-shadow in small app files
Rename inner shadowing identifiers in:
- add-host-modal, agent-status-bar, sidebar-workspace-list, terminal-pane
- workspace-setup-dialog, session-context, use-is-local-daemon
- setup-panel, new-workspace-screen, workspace-desktop-tabs-row
* chore(lint): no-explicit-any in claude-agent.test
* chore(lint): no-shadow in small app test/e2e files
* chore(lint): no-explicit-any in workspace-git-service.test
* chore(lint): no-shadow in app test hoisted blocks
* chore(lint): no-explicit-any in websocket-server.runtime-metrics.test
* chore(lint): no-explicit-any in acp-agent.test
* chore(lint): no-shadow in app composer/message-input/e2e helpers
* chore(lint): no-explicit-any in session.workspace-git-watch.test
* chore(lint): no-explicit-any in websocket-server.notifications.test
* chore(lint): no-shadow in sidebar-workspace-list.test, use-pr-pane-data.test
* chore(lint): no-explicit-any in websocket-server.relay-reconnect.test
* chore(lint): no-explicit-any in codex-app-server-agent.test
* chore(lint): no-shadow in composer.test, host-runtime.test, new-workspace-screen.test
* chore(lint): no-explicit-any in codex-app-server-agent.features.test
* chore(lint): hoist jsx-as-prop in app components batch 1
Memoize inline JSX icons passed as props across add-host-modal,
branch-switcher, composer, pair-link-modal, pr-pane, and
sidebar-workspace-list to satisfy react-perf/jsx-no-jsx-as-prop.
* chore(lint): no-explicit-any in session.test
* chore(lint): hoist jsx-as-prop in app components batch 2
Memoize inline JSX passed as props across agent-list, agent-status-bar,
file-explorer-pane, git-actions-split-button, and message to satisfy
react-perf/jsx-no-jsx-as-prop.
* chore(lint): hoist jsx-as-prop in app components batch 3
Memoize inline JSX passed as props across combined-model-selector,
draggable-list.native, provider-diagnostic-sheet, and
workspace-setup-dialog to satisfy react-perf/jsx-no-jsx-as-prop.
* chore(lint): hoist jsx-as-prop in desktop components
Memoize inline JSX passed as props across desktop-permissions-section,
desktop-updates-section, integrations-section, and pair-device-section
to satisfy react-perf/jsx-no-jsx-as-prop.
* chore(lint): no-explicit-any in mcp-server.test
* chore(lint): hoist jsx-as-prop in workspace/new-workspace screens
* chore(lint): hoist jsx-as-prop in remaining screens
* chore(lint): no-explicit-any in session.workspaces.test
* chore(lint): no-explicit-any in snapshot-mutation-ownership.test
* chore(lint): no-explicit-any in dictation-stream-manager.test
* chore(lint): no-explicit-any in relay-transport.e2e.test
* chore(lint): no-explicit-any in worktree-session.test
* chore(lint): no-explicit-any in model-resolver.test
* chore(lint): no-explicit-any in sherpa-parakeet-stt.test
* chore(lint): no-explicit-any in speech-download.e2e.test
* chore(lint): no-explicit-any in script-health-monitor.test
* chore(lint): no-explicit-any in schedule/service.test
* chore(lint): no-explicit-any in persistence-hooks.test
* chore(lint): no-explicit-any in editor-targets.test
* chore(lint): no-explicit-any in send-while-running-stuck-test-utils.test
* chore(lint): no-explicit-any in agent-storage.test
* chore(lint): no-nested-ternary in app batch 1 (18 files)
* chore(lint): no-explicit-any in generate-sherpa-tts-matrix
* chore(lint): no-explicit-any in claude-agent
* chore(lint): no-explicit-any in websocket-server
* chore(lint): no-explicit-any in process-conversation
* chore(lint): no-explicit-any in daemon-client
* chore(lint): no-explicit-any in codex-app-server-agent
* chore(lint): no-nested-ternary in app batch 2 (26 files)
* chore(format): apply oxfmt to server test files
* chore(lint): parallelize agent archiving in test-mcp-inject
* chore(lint): no-explicit-any in small app files (batch 1)
* chore(lint): no-explicit-any in small app files (batch 2)
* chore(lint): no-explicit-any in app (batch 3)
Covers use-web-scrollbar, stream-strategy, dictation-stream-sender test,
terminal-perf helper, checkout-git-actions-store test, and
web-desktop-scrollbar.
* chore(lint): no-explicit-any in app (batch 4)
Covers composer, message-input, tooltip, workspace-desktop-tabs-row,
and the e2e helpers (app, node-ws-factory, terminal-probes).
* chore(lint): no-explicit-any in polyfills/crypto.ts
* chore(lint): no-explicit-any in components/sidebar-workspace-list.tsx
* chore(lint): prefer-array-find over filter().at/pop
Replace filter(pred).at(-1)/pop() patterns with findLast(pred) and
filter(pred)[0] with find(pred) across server and app.
* chore(lint): no-explicit-any in components/message.tsx
* chore(lint): no-explicit-any in components/plan-card.tsx
* chore(lint): no-map-spread replace with Object.assign
Replace { ...obj, override } inside map callbacks with Object.assign({}, obj, { override })
to satisfy oxc/no-map-spread. Preserves copy-on-write semantics.
* chore(lint): no-extraneous-class in test mocks
Add dispose() stubs to xterm addon mocks to satisfy typescript-eslint/no-extraneous-class, and convert static-only Notification mocks to plain objects.
* chore(lint): no-named-as-default use named imports for ws and openai
* chore(lint): jsx-no-new-array-as-prop hoist composed style in volume-display
Memoize the style array so it is not created inline on every render.
* chore(lint): prefer-add-event-listener use Object.assign for non-DOM handlers
These transports (Transport interface, StreamableHTTPServerTransport) use
plain on<event> properties rather than EventTarget, so addEventListener is
not an option. Using Object.assign to set the handlers avoids the lint
false-positive without changing behavior.
* chore(lint): no-multiple-resolved null pendingResolve after settling
Replace boolean-settled guards with a nullable captured resolve/reject.
Each promise callback captures the resolver, nulls it on first use, and
calls it. This preserves the existing single-resolution semantics while
making the no-multiple-resolved rule happy.
* chore(lint): reduce structural complexity in server
Extract nested-callback cleanup helpers in worktree-bootstrap.test. Extract Codex model definition builder to drop list-models complexity below 20.
* chore(lint): extract helpers in agent-activity and session-store-hooks.test
Split tool-call update handling in groupActivities into helpers to clear max-depth. Hoist useWorkspaceFields selector in the hooks test to drop callback nesting.
* chore(lint): rules-of-hooks and exhaustive-deps in agent-stream-view
Move useMemo calls for permission card styles above the early return
so hooks run unconditionally. Add missing dependencies to the inline
path press handler and the stream render callback.
* chore(lint): rules-of-hooks in diff-scroll via optional context hook
Replace try/catch around useExplorerSidebarAnimation with a new
useExplorerSidebarAnimationOptional hook that returns null when the
provider is absent.
* chore(lint): rules-of-hooks and exhaustive-deps in sidebar-workspace-list
Replace conditional useMemo calls with plain inline arrays, capture
creatingWorkspaceTimeoutsRef.current in the effect body before cleanup,
and depend on the full input object in armTimers.
* chore(lint): rules-of-hooks in split-container
Hoist useWorkspaceLayoutStore and useMemo calls above the pane-kind
early return so hooks always run in the same order.
* chore(lint): rules-of-hooks in context-menu and dropdown-menu
Inline the resolvedWidthStyle object inside the content useMemo and move
the early return below the hook so it is always invoked in the same order.
* chore(lint): rules-of-hooks in web-desktop-scrollbar
Move thumbRegionStyle and handleStyle useMemo calls above the visibility
early return.
* chore(lint): reduce complexity in types/stream
Split reduceStreamUpdate timeline branch into per-case helpers (tool call and compaction) to clear complexity and max-depth warnings.
* chore(lint): reduce complexity in diff-highlighter
Extract metadata detection, path extraction, hunk parsing, and content-line push into helpers to drop parseDiff complexity below 20.
* chore(lint): exhaustive-deps in composer and use-attachment-preview-url
Memoize githubSearchItems so the picker callbacks don't see a new array
every render, hoist realtimeVoiceButtonStyle above rightContent useMemo
that depends on it, and add missing style dependencies. Tighten
use-attachment-preview-url to read the attachment via a ref while keying
off stable field primitives.
* chore(lint): reduce complexity in tool-call-detail-state
Extract per-detail helpers for hasMeaningfulToolCallDetail to clear complexity warning.
* chore(lint): reduce complexity in agent-grouping
Split groupAgents into partition, project-activity map, active-project, and inactive-date helpers.
* chore(lint): reduce max-depth in voice-runtime
Extract retireFinishedGroup helper to flatten processPlaybackQueue nesting.
* chore(lint): reduce max-depth in use-branch-switcher
Extract maybeRestoreStashForBranch callback to flatten handleBranchSelect nesting.
* chore(lint): exhaustive-deps in components batch
Address exhaustive-deps warnings across file-explorer-pane, file-pane,
git-diff-pane, message, provider-diagnostic-sheet, stream-strategy-web,
terminal-emulator, terminal-pane, and volume-meter. Memoize values that
otherwise changed every render, read non-reactive values through refs
when the effect intentionally tracks a different key, and add missing
dependencies where they were genuinely absent.
* chore(lint): reduce complexity/max-depth in draft-store
Extract collectQueuedMessageAttachmentIds and collectStreamUserImageIds helpers from runAttachmentGc. Extract buildMigratedDraftRecord helper for migratePersistedState.
* chore(lint): reduce complexity in tooltip and workspace-scripts-button
Extract resolveActualSide and resolveAlignedCoordinate helpers in tooltip. Extract resolveScriptIconColor in workspace-scripts-button.
* chore(lint): exhaustive-deps in contexts and hooks batch
Fixes exhaustive-deps warnings across app contexts and hooks by adding
missing deps, stabilizing derived arrays with useMemo, capturing ref values
at effect entry, and using ref pattern where deps were intentionally omitted.
* chore(lint): final hook-rules in workspace-screen, editor-button, stores, e2e
Memoizes derived arrays (availableEditors, terminals) to stabilize
useMemo deps, adds missing deps (normalizedServerId, normalizedWorkspaceId,
explorerToggleStyle, workspaceIdsKey, eventName), and renames the Playwright
fixture callback parameter from `use` to `provide` so eslint-plugin-react-hooks
doesn't conflate it with React's use() hook.
* chore(lint): reduce complexity/max-nested-callbacks in workspace-tabs-store
Extract migrate function body into migrateWorkspaceTabsState top-level
helper with per-loop sub-helpers (migrateUiTabsForKey, mergeExplicitTabOrder,
mergeLegacyTabOrder, migrateFocusedTabIds) and replace the inline IIFE in
ensureTab with buildNextTabsForEnsure helper.
* chore(lint): reduce complexity in session-store and panel-store
Extract isSessionServerInfoUnchanged helper from updateSessionServerInfo
in session-store. In panel-store split migrate body into per-version
migration helpers (migratePanelV2Explorer, migratePanelV3Explorer,
migratePanelExplorerTabByCheckout, migratePanelDesktopFocusMode) and
a top-level migratePanelState function.
* chore(lint): reduce complexity in keyboard-shortcuts and use-keyboard-shortcuts
Split resolveKeyboardShortcut into resolveInitialChordStep and
resolveAdvancingChordStep helpers with a shared buildMatchFromBinding
factory. Split handleAction's large switch into handleDispatchOnlyAction,
handlePayloadAction, handleSettingsToggle, and handleCommandCenterToggle
helpers, with hasPayloadKey type guard replacing inline payload checks.
* chore(lint): reduce complexity in audio recorder web hooks
Extract assertMicrophoneEnvironment helper from useAudioRecorder start
callback. Split useDictationAudioSource stop callback with
disconnectDictationAudioGraph, stopMediaRecorderIfActive,
finalizeRecorderStoppedPromise, and safeDisconnectNode helpers.
* chore(lint): reduce complexity in use-git-actions
* chore(lint): reduce complexity in use-pr-pane-data
* chore(lint): reduce complexity in use-agent-autocomplete
* chore(lint): reduce complexity in use-agent-screen-state-machine
* chore(lint): reduce complexity in session-stream-reducers
* chore(lint): reduce complexity in desktop-updates-section
* chore(lint): reduce complexity in pair-device-section
* chore(lint): reduce complexity in update-callout-source
* chore(lint): reduce complexity in provider-diagnostic-sheet
* chore(lint): disable no-await-in-loop (sequential-by-necessity cases)
Most remaining no-await-in-loop warnings were in legitimate sequential
patterns: polling loops with sleep, streaming/pagination cursors, shared
mutable state, ordered side effects (audio playback, port allocation).
Parallelizing these would change observable behavior. Rather than force
restructures that add complexity without benefit, disable the rule.
* chore(lint): reduce complexity in setup-panel
Extract helpers (resolveAutoExpandIndex, resolveSetupStatusLabel,
resolveCommandLog, buildCommandRowState) and lift the standalone log
view and top-level error banner into dedicated sub-components to drop
SetupPanel below the complexity limit.
* chore(lint): reduce complexity in AssistantMarkdownImage
Extract error-text resolution into a helper so the image component
drops below the cyclomatic complexity threshold.
* chore(lint): reduce complexity in ProjectLeadingVisual
Extract the status-variant dispatch into a ProjectLeadingVisualStatus
sub-component so the outer function stays below the cyclomatic
complexity limit.
* chore(lint): extract ChatAgentContent agent-building helper
* chore(lint): reduce complexity in ChatAgentContent
Extract the chat-agent selector (selectChatAgentState +
resolveChatAgentFromSession), the agent-shape constructor
(buildChatAgentFromState), and the not-found/error/boot view
dispatch (renderChatAgentNonReadyView) into helpers so both the
selector callback and the component function drop below the
cyclomatic complexity limit.
* chore(lint): extract subcomponents to reduce JSX nesting depth
* chore(lint): reduce complexity in app components (status-bar, explorer, setup-dialog, tool-call-details)
* chore(lint): reduce complexity in app hooks/runtime/stores/misc components
* chore(lint): long-tail cleanup (no-async-endpoint-handlers, unicorn rules, unescaped-entities, unassigned-import allowlist)
* chore(lint): simplify agent useMemo deps to match helper signature
* chore(lint): rename ChatService.postMessage to dispatchMessage
* chore(lint): server cleanup (no-shadow, no-useless-*, no-unmodified-loop, control-regex, require-yield)
* chore(lint): app cleanup (jsx-max-depth, jsx-no-new-*-as-prop, complexity, misleading-regex, no-useless-*)
* chore(lint): e2e cleanup (no-unmodified-loop-condition, max-nested-callbacks)
* chore(lint): workspace-screen complexity + no-empty-pattern cleanup
* chore(lint): use Array.from for defensive snapshot iteration
* chore(lint): final mechanical tail (control-regex, max-depth, useless-expressions, examples cleanup)
* chore(lint): reduce agent-stream-view complexity
* chore(lint): reduce resolveAgentModelSelection complexity
* chore(lint): reduce globalSetup complexity
* chore(format): oxfmt draft-store single-line signature
* chore(lint): reduce GitDiffPane complexity
* chore(lint): reduce MessageInput complexity
* chore(lint): reduce Combobox complexity
* fix(server): bound listLinuxWatchDirectories readdir concurrency
Wraps the per-level readdir traversal in listLinuxWatchDirectories with a
module-level p-limit (default 16, tunable via PASEO_LINUX_WATCH_READDIR_CONCURRENCY).
On broad repos, the previous Promise.all over an entire BFS level could issue
an unbounded number of concurrent readdir calls. Behavior is otherwise
identical: still traverses all non-.git directories and swallows per-directory
readdir failures.
* chore(lint): promote oxlint warnings to errors
- package.json: lint/lint:fix now pass --deny-warnings so any warning fails
- .github/workflows/ci.yml: add lint job running npm run lint
- .oxlintrc.json: disable no-empty-pattern for e2e/fixtures.ts
(Playwright requires the empty object destructure as the first arg)
- packages/app/e2e/fixtures.ts: restore async ({}, provide) signature
- packages/app/e2e/global-setup.ts: oxfmt single-line signature
* fix(test): relay-transport ws mock exports WebSocket named binding
Commit 38efad7d switched relay-transport.ts from default to named import
of ws.WebSocket, but the test mock still only exported default. Updated
vi.mock("ws") to return both default and named WebSocket bindings so the
mocked module matches the new import shape. Classification: test drifted
with production code; test now exports what the production import needs.
* chore(lint): make oxlint error at config level; add Lefthook pre-commit
Replaces --deny-warnings workaround with semantic severity at the config
level, and adds whole-repo pre-commit gates via Lefthook.
- .oxlintrc.json: all enabled categories (correctness, suspicious, perf)
and intentional rule overrides now use "error" instead of "warn".
Removed duplicate "require-await" key (last-wins "off" preserved).
- package.json: lint scripts are plain oxlint / oxlint --fix. Adds
lefthook devDep and "prepare": "lefthook install --force" so hooks
install on npm install even where core.hooksPath is set globally.
- lefthook.yml: pre-commit runs format:check, lint, typecheck in
parallel. Whole-repo gates (no glob filters). Block-only, no
auto-formatting.
* fix: update lockfile signatures and Nix hash
* chore(lint): pre-commit runs formatter in write mode, then lint+typecheck
Switches the pre-commit hook from format:check to format (write) with
stage_fixed: true, so the formatter rewrites files and re-stages them
into the commit. Lint and typecheck then run in parallel against the
formatted tree.
Sequencing uses Lefthook 2.x jobs with a nested group: top-level jobs
run sequentially, so the format job completes before the checks group
starts; within checks, lint and typecheck run in parallel.
CI continues to use format:check, so unformatted code pushed with
--no-verify still fails on the server.
* chore(lint): revert pre-commit to format:check (no stage_fixed)
stage_fixed: true with a whole-repo formatter (oxfmt .) can silently
re-stage the full reformatted file when a developer used git add -p
to stage only a hunk, losing their hunk-level intent. Safer to just
block the commit on unformatted code and let the dev run
`npm run format` themselves.
Reverts to the simple parallel check form: all three commands run
concurrently and are read-only.
* chore(ts7): tsgo compatibility across the monorepo
Preserves tsc 5.9.3 behavior while making TypeScript 7 / tsgo
(@typescript/native-preview 7.0.0-dev.20260423.1) green
package-by-package.
tsconfig:
- server/tsconfig.server.json: drop removed `baseUrl`; rewrite
`@server/*` paths entry to be relative to the tsconfig.
- website/tsconfig.json: drop removed `baseUrl`; rewrite
`~/*` paths entry to be relative.
- expo-two-way-audio/tsconfig.json: replace inherited
moduleResolution=node10 with `bundler`, set `module: esnext`
(required by `bundler`). Matches Metro/Expo runtime behavior
and avoids forcing `.js` extensions in source.
source:
- app/e2e/helpers/terminal-probes.ts: WebSocket subclass `send`
now types its argument as `Parameters<WebSocket["send"]>[0]`
so the subclass signature tracks the platform type across
tsc 5.9 and TS 7 libdom.
- relay/src/crypto.ts: tweetnacl setPRNG callback allocates a
fresh Uint8Array for getRandomValues (owning ArrayBuffer) and
copies into the caller-provided view. TS 7 libdom requires
ArrayBufferView<ArrayBuffer> (not ArrayBufferLike).
All 8 packages green under both tsc --noEmit and tsgo --noEmit.
* fix(app): update os-notifications test mocks for addEventListener
Commit 46731a51 switched attachWebClickHandler from notification.onclick
to notification.addEventListener("click", ...) to satisfy the
prefer-add-event-listener lint rule. The test mocks still asserted
the old onclick property, so two specs failed on CI with
"notification.addEventListener is not a function". Update the three
MockNotification classes to record click listeners through
addEventListener and drive them via clickListeners[0] in the
assertions.
* chore: switch typecheck from tsc to tsgo (@typescript/native-preview)
Installs @typescript/native-preview 7.0.0-dev.20260423.1 as a root
devDependency and swaps every per-workspace `typecheck` script from
`tsc --noEmit` to `tsgo --noEmit`. Build scripts still use `tsc`, so
emit behavior is unchanged. Monorepo typecheck drops from ~28s to
~4.7s wall clock (~6x faster).
expo-two-way-audio previously ran `tsc` without --noEmit for its
typecheck step. tsgo is stricter about rootDir on emit, so the
typecheck script explicitly passes --noEmit here too.
* fix: update lockfile signatures and Nix hash
* fix(app): commit *.css module declaration for CI typecheck
expo generates expo-env.d.ts (which references expo/types where
*.css is declared) locally, but the file is gitignored. CI does a
fresh checkout + install and doesn't run expo, so the declaration
is missing and tsgo rejects the side-effect import
'@xterm/xterm/css/xterm.css' with TS2882. Add a committed
packages/app/global.d.ts with 'declare module "*.css"' so typecheck
is self-contained.
tsc tolerated the missing declaration more loosely; tsgo is stricter.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
github-actions[bot] <github-actions[bot]@users.noreply.github.com>
parent
5b2f3fd830
commit
446dd9cfe6
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
// --- Vector math utilities ---
|
||||
|
||||
@@ -107,10 +107,21 @@ function ButterflyVisual({
|
||||
direction = "right",
|
||||
heading = 0,
|
||||
}: ButterflyVisualProps) {
|
||||
const wingStyle = {
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
};
|
||||
const wingStyle = useMemo(
|
||||
() => ({
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}),
|
||||
[delay, duration],
|
||||
);
|
||||
|
||||
const bodyStyle = useMemo(
|
||||
() => ({
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}),
|
||||
[delay, duration],
|
||||
);
|
||||
|
||||
const scaleX = direction === "left" ? -1 : 1;
|
||||
|
||||
@@ -119,24 +130,28 @@ function ButterflyVisual({
|
||||
// -30° = horizontal, 0° = 30° down (base), +90° = 120° down (nearly straight down)
|
||||
const clampedHeading = Math.max(-30, Math.min(90, heading));
|
||||
|
||||
const svgStyle = useMemo(
|
||||
() => ({
|
||||
filter: "url(#painterly)",
|
||||
transform: `scaleX(${scaleX}) rotateY(25deg) rotate(30deg) rotateZ(${clampedHeading}deg)`,
|
||||
transition: "transform 0.3s ease-out",
|
||||
}),
|
||||
[scaleX, clampedHeading],
|
||||
);
|
||||
|
||||
const farWingStyle = useMemo(
|
||||
() => ({ ...wingStyle, animationDelay: `${delay + 0.08}s` }),
|
||||
[wingStyle, delay],
|
||||
);
|
||||
|
||||
const lowerWingStyle = useMemo(
|
||||
() => ({ ...wingStyle, animationDelay: `${delay + 0.05}s` }),
|
||||
[wingStyle, delay],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="animate-flutter-body"
|
||||
style={{
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 50 40"
|
||||
width={size}
|
||||
height={size * 0.8}
|
||||
style={{
|
||||
filter: "url(#painterly)",
|
||||
transform: `scaleX(${scaleX}) rotateY(25deg) rotate(30deg) rotateZ(${clampedHeading}deg)`,
|
||||
transition: "transform 0.3s ease-out",
|
||||
}}
|
||||
>
|
||||
<div className="animate-flutter-body" style={bodyStyle}>
|
||||
<svg viewBox="0 0 50 40" width={size} height={size * 0.8} style={svgStyle}>
|
||||
<defs>
|
||||
<filter id="painterly" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="2" result="noise" />
|
||||
@@ -153,7 +168,7 @@ function ButterflyVisual({
|
||||
{/* Far side wings (darker, behind, slightly rotated) */}
|
||||
<g
|
||||
className="animate-flutter-left origin-[38px_20px]"
|
||||
style={{ ...wingStyle, animationDelay: `${delay + 0.08}s` }}
|
||||
style={farWingStyle}
|
||||
transform="translate(6, 0) rotate(8, 38, 20)"
|
||||
>
|
||||
<path d="M38 20 Q20 2 8 10 Q0 18 10 28 Q25 32 38 22" fill={darken(color, 50)} />
|
||||
@@ -165,10 +180,7 @@ function ButterflyVisual({
|
||||
<path d="M38 20 Q20 2 8 10 Q0 18 10 28 Q25 32 38 22" fill={color} />
|
||||
</g>
|
||||
|
||||
<g
|
||||
className="animate-flutter-left origin-[38px_20px]"
|
||||
style={{ ...wingStyle, animationDelay: `${delay + 0.05}s` }}
|
||||
>
|
||||
<g className="animate-flutter-left origin-[38px_20px]" style={lowerWingStyle}>
|
||||
<path d="M38 22 Q25 30 18 38 Q28 42 36 34 Q40 28 38 24" fill={color} />
|
||||
</g>
|
||||
</svg>
|
||||
@@ -358,17 +370,18 @@ export function FloatingButterfly({
|
||||
};
|
||||
}, [mounted]);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
transform: `translate(${renderState.x}px, ${renderState.y}px)`,
|
||||
}),
|
||||
[style, renderState.x, renderState.y],
|
||||
);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
...style,
|
||||
transform: `translate(${renderState.x}px, ${renderState.y}px)`,
|
||||
}}
|
||||
>
|
||||
<div ref={elementRef} className="absolute pointer-events-none" style={containerStyle}>
|
||||
<ButterflyVisual
|
||||
size={size}
|
||||
color={color}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { motion, AnimatePresence, type Transition } from "framer-motion";
|
||||
|
||||
const OVERLAY_INITIAL = { opacity: 0 };
|
||||
const OVERLAY_ANIMATE = { opacity: 1 };
|
||||
const OVERLAY_EXIT = { opacity: 0 };
|
||||
const OVERLAY_TRANSITION: Transition = { duration: 0.2 };
|
||||
|
||||
const PANEL_INITIAL = { opacity: 0, scale: 0.95 };
|
||||
const PANEL_ANIMATE = { opacity: 1, scale: 1 };
|
||||
const PANEL_EXIT = { opacity: 0, scale: 0.95 };
|
||||
const PANEL_TRANSITION: Transition = { duration: 0.2, ease: "easeOut" };
|
||||
|
||||
interface CommandDialogProps {
|
||||
trigger: React.ReactNode;
|
||||
@@ -36,25 +46,30 @@ export function CommandDialog({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleToggle = React.useCallback(() => setOpen((prev) => !prev), []);
|
||||
const handleClose = React.useCallback(() => setOpen(false), []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button onClick={() => setOpen(!open)}>{trigger}</button>
|
||||
<button type="button" onClick={handleToggle}>
|
||||
{trigger}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={OVERLAY_INITIAL}
|
||||
animate={OVERLAY_ANIMATE}
|
||||
exit={OVERLAY_EXIT}
|
||||
transition={OVERLAY_TRANSITION}
|
||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
initial={PANEL_INITIAL}
|
||||
animate={PANEL_ANIMATE}
|
||||
exit={PANEL_EXIT}
|
||||
transition={PANEL_TRANSITION}
|
||||
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
@@ -74,11 +89,11 @@ export function CommandDialog({
|
||||
function CodeBlock({ children }: { children: string }) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
function handleCopy() {
|
||||
const handleCopy = React.useCallback(() => {
|
||||
navigator.clipboard.writeText(children);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className="bg-black/30 backdrop-blur-sm rounded-lg p-3 md:p-4 font-mono text-sm flex items-center justify-between gap-2">
|
||||
@@ -87,6 +102,7 @@ function CodeBlock({ children }: { children: string }) {
|
||||
<span className="text-foreground">{children}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
||||
title="Copy to clipboard"
|
||||
|
||||
@@ -27,6 +27,9 @@ import {
|
||||
} from "lucide-react";
|
||||
import { ClaudeIcon, CodexIcon, SourceControlIcon } from "~/components/mockup";
|
||||
|
||||
const USER_BUBBLE_STYLE = { borderRadius: "16px 2px 16px 16px" };
|
||||
const NEW_BADGE_STYLE = { backgroundColor: "rgba(46, 160, 67, 0.2)" };
|
||||
|
||||
// ── Stagger timing ──────────────────────────────────────
|
||||
|
||||
const D = {
|
||||
@@ -73,20 +76,20 @@ const CHAT: ChatItem[] = [
|
||||
|
||||
// ── Sidebar data ────────────────────────────────────────
|
||||
|
||||
type SidebarWorkspace = {
|
||||
interface SidebarWorkspace {
|
||||
name: string;
|
||||
kind: "checkout" | "worktree";
|
||||
status: "running" | "done" | "idle" | "syncing";
|
||||
selected?: boolean;
|
||||
diffStat?: { additions: number; deletions: number };
|
||||
pr?: { number: number; state: "open" | "merged" | "closed" };
|
||||
};
|
||||
}
|
||||
|
||||
type SidebarProject = {
|
||||
interface SidebarProject {
|
||||
initial: string;
|
||||
name: string;
|
||||
workspaces: SidebarWorkspace[];
|
||||
};
|
||||
}
|
||||
|
||||
const SIDEBAR_PROJECTS: SidebarProject[] = [
|
||||
{
|
||||
@@ -144,12 +147,12 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [
|
||||
|
||||
// ── Tab data ─────────────────────────────────────────────
|
||||
|
||||
type TabDef = {
|
||||
interface TabDef {
|
||||
name: string;
|
||||
provider: "claude" | "codex" | "terminal";
|
||||
done: boolean;
|
||||
active?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const TABS: TabDef[] = [
|
||||
{ name: "Orchestrator", provider: "claude", done: false, active: true },
|
||||
@@ -160,12 +163,12 @@ const TABS: TabDef[] = [
|
||||
// ── Diff data ──────────────────────────────────────────
|
||||
|
||||
type DiffLineType = "add" | "remove" | "context" | "header";
|
||||
type DiffLine = {
|
||||
interface DiffLine {
|
||||
type: DiffLineType;
|
||||
ln: string | null;
|
||||
content?: string;
|
||||
tokens?: Array<{ text: string; cls: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
const DIFF_LINES: DiffLine[] = [
|
||||
{
|
||||
@@ -313,36 +316,23 @@ const DIFF_LINES: DiffLine[] = [
|
||||
|
||||
// ── Implementation agent chat data ──────────────────────
|
||||
|
||||
const IMPLEMENT_CHAT: ChatItem[] = [
|
||||
{ type: "text", text: "Starting implementation of the returns dashboard.", bold: true },
|
||||
{ type: "tool", label: "Edit file", summary: "src/pages/dashboard.tsx", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/components/return-table.tsx", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/components/filter-bar.tsx", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/components/status-chart.tsx", status: "done" },
|
||||
{ type: "tool", label: "Run command", summary: "npm run typecheck", status: "done" },
|
||||
{ type: "text", text: "Typecheck passes. Adding API route and data fetching." },
|
||||
{ type: "tool", label: "Edit file", summary: "src/api/returns.ts", status: "done" },
|
||||
{ type: "tool", label: "Edit file", summary: "src/hooks/use-returns.ts", status: "done" },
|
||||
{ type: "tool", label: "Run command", summary: "npm run typecheck", status: "running" },
|
||||
];
|
||||
|
||||
// ── Terminal log lines ─────────────────────────────────
|
||||
|
||||
const TERMINAL_LINES = [
|
||||
{ text: "$ npm run dev", cls: "text-mock-fg" },
|
||||
{ text: "", cls: "" },
|
||||
{ text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" },
|
||||
{ text: "> next dev --turbopack", cls: "text-mock-fg-muted" },
|
||||
{ text: "", cls: "" },
|
||||
{ text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" },
|
||||
{ text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" },
|
||||
{ text: "", cls: "" },
|
||||
{ text: " ✓ Starting...", cls: "text-mock-green" },
|
||||
{ text: " ✓ Ready in 1.2s", cls: "text-mock-green" },
|
||||
{ text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" },
|
||||
{ text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" },
|
||||
{ text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" },
|
||||
{ text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" },
|
||||
{ id: "cmd", text: "$ npm run dev", cls: "text-mock-fg" },
|
||||
{ id: "blank1", text: "", cls: "" },
|
||||
{ id: "pkg", text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" },
|
||||
{ id: "pkg-cmd", text: "> next dev --turbopack", cls: "text-mock-fg-muted" },
|
||||
{ id: "blank2", text: "", cls: "" },
|
||||
{ id: "next-ver", text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" },
|
||||
{ id: "next-local", text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" },
|
||||
{ id: "blank3", text: "", cls: "" },
|
||||
{ id: "starting", text: " ✓ Starting...", cls: "text-mock-green" },
|
||||
{ id: "ready", text: " ✓ Ready in 1.2s", cls: "text-mock-green" },
|
||||
{ id: "compiling-dashboard", text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" },
|
||||
{ id: "compiled-dashboard", text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" },
|
||||
{ id: "compiling-api", text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" },
|
||||
{ id: "compiled-api", text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" },
|
||||
];
|
||||
|
||||
// ── Sub-components ──────────────────────────────────────
|
||||
@@ -468,16 +458,115 @@ function Composer({
|
||||
);
|
||||
}
|
||||
|
||||
function prStateLabel(state: "open" | "merged" | "closed"): string {
|
||||
if (state === "open") return "Open";
|
||||
if (state === "merged") return "Merged";
|
||||
return "Closed";
|
||||
}
|
||||
|
||||
function WorkspaceIcon({ workspace }: { workspace: SidebarWorkspace }) {
|
||||
if (workspace.status === "syncing") return <SyncedLoader size={11} />;
|
||||
const icon =
|
||||
workspace.kind === "worktree" ? (
|
||||
<FolderGit2 size={14} className="text-mock-fg-muted" />
|
||||
) : (
|
||||
<Monitor size={14} className="text-mock-fg-muted" />
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{icon}
|
||||
{workspace.status !== "idle" && (
|
||||
<div className="absolute bottom-0 right-0">
|
||||
<WorkspaceStatusDot status={workspace.status} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarWorkspaceRow({ workspace }: { workspace: SidebarWorkspace }) {
|
||||
return (
|
||||
<div className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}>
|
||||
<div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1">
|
||||
<div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center">
|
||||
<WorkspaceIcon workspace={workspace} />
|
||||
</div>
|
||||
<span className="text-[12px] text-mock-fg/[0.76] font-normal truncate flex-1 min-w-0 leading-[1.4]">
|
||||
{workspace.name}
|
||||
</span>
|
||||
{workspace.diffStat && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<span className="text-[10px] text-mock-green-400 font-normal leading-none">
|
||||
+{workspace.diffStat.additions}
|
||||
</span>
|
||||
<span className="text-[10px] text-mock-red font-normal leading-none">
|
||||
-{workspace.diffStat.deletions}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{workspace.pr && (
|
||||
<div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5">
|
||||
<GitPullRequest size={11} className="text-mock-fg-muted" />
|
||||
<span className="text-[10px] text-mock-fg-muted leading-none truncate">
|
||||
#{workspace.pr.number} · {prStateLabel(workspace.pr.state)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function diffLineBg(type: DiffLineType): string {
|
||||
if (type === "add") return "bg-mock-diff-add";
|
||||
if (type === "remove") return "bg-mock-diff-remove";
|
||||
return "bg-mock-surface1";
|
||||
}
|
||||
|
||||
function diffLineNumCls(type: DiffLineType): string {
|
||||
if (type === "add") return "text-mock-green-400";
|
||||
if (type === "remove") return "text-mock-red";
|
||||
return "text-mock-fg-muted";
|
||||
}
|
||||
|
||||
function DiffLineRow({ line }: { line: DiffLine }) {
|
||||
const lineBg = diffLineBg(line.type);
|
||||
const lineNumCls = diffLineNumCls(line.type);
|
||||
return (
|
||||
<div className={`flex items-stretch ${lineBg}`}>
|
||||
<div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end">
|
||||
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>
|
||||
{line.ln ?? ""}
|
||||
</code>
|
||||
</div>
|
||||
<code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0">
|
||||
{line.tokens?.map((tok) => (
|
||||
<span key={`${tok.cls}:${tok.text}`} className={tok.cls}>
|
||||
{tok.text}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function chatItemKey(item: ChatItem): string {
|
||||
if (item.type === "user") return `user-${item.text}`;
|
||||
if (item.type === "tool") return `tool-${item.label}-${item.summary ?? ""}`;
|
||||
return `text-${item.text}`;
|
||||
}
|
||||
|
||||
function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
return (
|
||||
<div className="flex-1 overflow-hidden px-2 py-2">
|
||||
{items.map((item, i) => {
|
||||
{items.map((item) => {
|
||||
const key = chatItemKey(item);
|
||||
if (item.type === "user") {
|
||||
return (
|
||||
<div key={i} className="flex justify-end py-1">
|
||||
<div key={key} className="flex justify-end py-1">
|
||||
<div
|
||||
className="bg-mock-surface2 px-2.5 py-1.5 max-w-[85%] leading-none"
|
||||
style={{ borderRadius: "16px 2px 16px 16px" }}
|
||||
style={USER_BUBBLE_STYLE}
|
||||
>
|
||||
<span className="text-[11px] text-mock-fg leading-none">{item.text}</span>
|
||||
</div>
|
||||
@@ -488,7 +577,7 @@ function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
const isDone = item.status === "done";
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
key={key}
|
||||
className="flex items-center rounded-lg px-2 py-[1px] -mx-[6px] border border-transparent"
|
||||
>
|
||||
<div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0 mr-1">
|
||||
@@ -514,7 +603,7 @@ function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
}
|
||||
return (
|
||||
<p
|
||||
key={i}
|
||||
key={key}
|
||||
className={`text-[11px] px-2 py-[1px] text-mock-fg leading-[1.5] ${item.bold ? "font-medium" : ""}`}
|
||||
>
|
||||
{item.text}
|
||||
@@ -534,21 +623,12 @@ function ChatArea() {
|
||||
);
|
||||
}
|
||||
|
||||
function ImplementPane() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0">
|
||||
<ChatMessages items={IMPLEMENT_CHAT} />
|
||||
<Composer provider="codex" model="gpt-5.4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TerminalPane() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0">
|
||||
<div className="flex-1 overflow-hidden px-3 py-2">
|
||||
{TERMINAL_LINES.map((line, i) => (
|
||||
<div key={i} className="leading-[1.3]">
|
||||
{TERMINAL_LINES.map((line) => (
|
||||
<div key={line.id} className="leading-[1.3]">
|
||||
<code className={`text-[10px] font-mono ${line.cls} whitespace-pre`}>{line.text}</code>
|
||||
</div>
|
||||
))}
|
||||
@@ -593,7 +673,7 @@ function ExplorerSidebar() {
|
||||
<span className="text-[11px] text-mock-fg-muted truncate min-w-0"> src/pages</span>
|
||||
<span
|
||||
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
|
||||
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
|
||||
style={NEW_BADGE_STYLE}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
@@ -606,37 +686,9 @@ function ExplorerSidebar() {
|
||||
|
||||
{/* Diff lines */}
|
||||
<div className="overflow-hidden bg-mock-surface1">
|
||||
{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";
|
||||
|
||||
return (
|
||||
<div key={i} className={`flex items-stretch ${lineBg}`}>
|
||||
<div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end">
|
||||
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>
|
||||
{line.ln ?? ""}
|
||||
</code>
|
||||
</div>
|
||||
<code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0">
|
||||
{line.tokens?.map((tok, j) => (
|
||||
<span key={j} className={tok.cls}>
|
||||
{tok.text}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{DIFF_LINES.map((line) => (
|
||||
<DiffLineRow key={`${line.type}-${line.ln ?? "blank"}`} line={line} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Collapsed file rows */}
|
||||
@@ -658,7 +710,7 @@ function ExplorerSidebar() {
|
||||
{file.isNew && (
|
||||
<span
|
||||
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
|
||||
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
|
||||
style={NEW_BADGE_STYLE}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
@@ -681,55 +733,74 @@ function ExplorerSidebar() {
|
||||
const DOT_SEQUENCE = [0, 1, 3, 5, 4, 2] as const;
|
||||
const SYNCED_LOADER_DURATION_MS = 950;
|
||||
const DOT_COUNT = 6;
|
||||
const DOT_INDICES = Array.from({ length: DOT_COUNT }, (_, i) => i);
|
||||
const STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step
|
||||
|
||||
interface SyncedLoaderDotProps {
|
||||
dotIndex: number;
|
||||
dotSize: number;
|
||||
gap: number;
|
||||
}
|
||||
|
||||
function SyncedLoaderDot({ dotIndex, dotSize, gap }: SyncedLoaderDotProps) {
|
||||
const col = dotIndex % 2;
|
||||
const row = Math.floor(dotIndex / 2);
|
||||
const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]);
|
||||
const delayMs = -(sequenceIndex * STEP_MS);
|
||||
const style = React.useMemo(
|
||||
() => ({
|
||||
position: "absolute" as const,
|
||||
left: col * (dotSize + gap),
|
||||
top: row * (dotSize + gap),
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#f59e0b",
|
||||
animationName: "synced-snake-dot",
|
||||
animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`,
|
||||
animationTimingFunction: "linear",
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `${delayMs}ms`,
|
||||
}),
|
||||
[col, row, dotSize, gap, delayMs],
|
||||
);
|
||||
return <div style={style} />;
|
||||
}
|
||||
|
||||
function SyncedLoader({ size = 11 }: { size?: number }) {
|
||||
const gap = Math.max(1, Math.round(size * 0.12));
|
||||
const dotSize = Math.max(2, Math.floor((size - gap * 2) / 3));
|
||||
const gridW = dotSize * 2 + gap;
|
||||
const gridH = dotSize * 3 + gap * 2;
|
||||
|
||||
const outerStyle = React.useMemo(
|
||||
() => ({
|
||||
width: size,
|
||||
height: size,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}),
|
||||
[size],
|
||||
);
|
||||
|
||||
const innerStyle = React.useMemo(
|
||||
() => ({ position: "relative" as const, width: gridW, height: gridH }),
|
||||
[gridW, gridH],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative", width: gridW, height: gridH }}>
|
||||
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => {
|
||||
const col = dotIndex % 2;
|
||||
const row = Math.floor(dotIndex / 2);
|
||||
// The snake sequence index for this dot: when is it the "head"?
|
||||
const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]);
|
||||
// Negative delay syncs the animation so this dot is the head at t=0 when sequenceIndex=0.
|
||||
// At t=0, headIndex should be 0, meaning sequenceIndex=0 dot is the head.
|
||||
// Each dot is shifted by sequenceIndex steps back in time.
|
||||
const delayMs = -(sequenceIndex * STEP_MS);
|
||||
return (
|
||||
<div
|
||||
key={dotIndex}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: col * (dotSize + gap),
|
||||
top: row * (dotSize + gap),
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#f59e0b",
|
||||
animationName: "synced-snake-dot",
|
||||
animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`,
|
||||
animationTimingFunction: "linear",
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `${delayMs}ms`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div style={outerStyle}>
|
||||
<div style={innerStyle}>
|
||||
{DOT_INDICES.map((dotIndex) => (
|
||||
<SyncedLoaderDot
|
||||
key={`dot-${dotIndex}`}
|
||||
dotIndex={dotIndex}
|
||||
dotSize={dotSize}
|
||||
gap={gap}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -776,57 +847,7 @@ function Sidebar() {
|
||||
|
||||
{/* Workspace rows — indented one level */}
|
||||
{project.workspaces.map((workspace) => (
|
||||
<div
|
||||
key={workspace.name}
|
||||
className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1">
|
||||
<div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center">
|
||||
{workspace.status === "syncing" ? (
|
||||
<SyncedLoader size={11} />
|
||||
) : (
|
||||
<>
|
||||
{workspace.kind === "worktree" ? (
|
||||
<FolderGit2 size={14} className="text-mock-fg-muted" />
|
||||
) : (
|
||||
<Monitor size={14} className="text-mock-fg-muted" />
|
||||
)}
|
||||
{workspace.status !== "idle" && (
|
||||
<div className="absolute bottom-0 right-0">
|
||||
<WorkspaceStatusDot status={workspace.status} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[12px] text-mock-fg/[0.76] font-normal truncate flex-1 min-w-0 leading-[1.4]">
|
||||
{workspace.name}
|
||||
</span>
|
||||
{workspace.diffStat && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<span className="text-[10px] text-mock-green-400 font-normal leading-none">
|
||||
+{workspace.diffStat.additions}
|
||||
</span>
|
||||
<span className="text-[10px] text-mock-red font-normal leading-none">
|
||||
-{workspace.diffStat.deletions}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{workspace.pr && (
|
||||
<div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5">
|
||||
<GitPullRequest size={11} className="text-mock-fg-muted" />
|
||||
<span className="text-[10px] text-mock-fg-muted leading-none truncate">
|
||||
#{workspace.pr.number} ·{" "}
|
||||
{workspace.pr.state === "open"
|
||||
? "Open"
|
||||
: workspace.pr.state === "merged"
|
||||
? "Merged"
|
||||
: "Closed"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SidebarWorkspaceRow key={workspace.name} workspace={workspace} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -851,6 +872,75 @@ function Sidebar() {
|
||||
);
|
||||
}
|
||||
|
||||
const TERMINAL_TABS: TabDef[] = [
|
||||
{ name: "npm run dev", provider: "terminal", done: false, active: true },
|
||||
];
|
||||
|
||||
function TitleBarLeft() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<PanelLeft size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">main</span>
|
||||
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">
|
||||
acme/returns-app
|
||||
</span>
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Ellipsis size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TitleBarRight() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
|
||||
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-1">
|
||||
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
|
||||
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
|
||||
<ChevronDown size={12} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
|
||||
<SourceControlIcon size={14} className="text-mock-fg-muted" />
|
||||
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
|
||||
<span className="text-[11px] font-normal text-mock-red">-15</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopSplitPanes() {
|
||||
return (
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Left pane: all agent tabs + chat */}
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<motion.div {...fade(D.tabs)}>
|
||||
<PaneTabBar tabs={TABS} focused />
|
||||
</motion.div>
|
||||
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
|
||||
<ChatArea />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div className="w-px bg-mock-border flex-shrink-0" />
|
||||
|
||||
{/* Right pane: terminal only */}
|
||||
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<PaneTabBar tabs={TERMINAL_TABS} />
|
||||
<TerminalPane />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Desktop Mockup ──────────────────────────────────────
|
||||
|
||||
function DesktopMockup() {
|
||||
@@ -869,15 +959,14 @@ function DesktopMockup() {
|
||||
return () => window.removeEventListener("resize", updateScale);
|
||||
}, []);
|
||||
|
||||
const containerStyle = React.useMemo(() => ({ height: `${1200 * (9 / 16) * scale}px` }), [scale]);
|
||||
const innerStyle = React.useMemo(() => ({ width: 1200, transform: `scale(${scale})` }), [scale]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: `${1200 * (9 / 16) * scale}px` }}
|
||||
>
|
||||
<div ref={containerRef} className="w-full overflow-hidden" style={containerStyle}>
|
||||
<div
|
||||
className="mx-auto rounded-xl overflow-hidden border border-mock-border bg-mock-surface0 shadow-[6px_6px_0_rgba(0,0,0,0.4)] origin-top-left"
|
||||
style={{ width: 1200, transform: `scale(${scale})` }}
|
||||
style={innerStyle}
|
||||
>
|
||||
{/* Top-level: left sidebar | center column | explorer sidebar — all full height */}
|
||||
<div className="flex aspect-video">
|
||||
@@ -893,64 +982,11 @@ function DesktopMockup() {
|
||||
{...fade(D.titleBar)}
|
||||
className="flex items-center h-10 px-2 bg-mock-surface0 border-b border-mock-border flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<PanelLeft size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">
|
||||
main
|
||||
</span>
|
||||
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">
|
||||
acme/returns-app
|
||||
</span>
|
||||
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Ellipsis size={14} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
|
||||
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-1">
|
||||
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
|
||||
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
|
||||
<ChevronDown size={12} className="text-mock-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
|
||||
<SourceControlIcon size={14} className="text-mock-fg-muted" />
|
||||
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
|
||||
<span className="text-[11px] font-normal text-mock-red">-15</span>
|
||||
</div>
|
||||
</div>
|
||||
<TitleBarLeft />
|
||||
<TitleBarRight />
|
||||
</motion.div>
|
||||
|
||||
{/* Split panes */}
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Left pane: all agent tabs + chat */}
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<motion.div {...fade(D.tabs)}>
|
||||
<PaneTabBar tabs={TABS} focused />
|
||||
</motion.div>
|
||||
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
|
||||
<ChatArea />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div className="w-px bg-mock-border flex-shrink-0" />
|
||||
|
||||
{/* Right pane: terminal only */}
|
||||
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<PaneTabBar
|
||||
tabs={[{ name: "npm run dev", provider: "terminal", done: false, active: true }]}
|
||||
/>
|
||||
<TerminalPane />
|
||||
</motion.div>
|
||||
</div>
|
||||
<DesktopSplitPanes />
|
||||
</div>
|
||||
|
||||
{/* Explorer sidebar — full height, pushes center column */}
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence, useInView, useScroll, useTransform } from "framer-motion";
|
||||
import {
|
||||
motion,
|
||||
AnimatePresence,
|
||||
useInView,
|
||||
useScroll,
|
||||
useTransform,
|
||||
type Transition,
|
||||
} from "framer-motion";
|
||||
|
||||
// Shared motion presets — hoisted so every JSX site receives the same object
|
||||
// reference and doesn't trigger jsx-no-new-object-as-prop.
|
||||
const FADE_IN_UP = { opacity: 0, y: 20 };
|
||||
const FADE_IN = { opacity: 1, y: 0 };
|
||||
const FADE_IN_UP_TINY = { opacity: 0, y: -10 };
|
||||
const FADE_IN_UP_XL = { opacity: 0, y: 30 };
|
||||
const FADE_IN_UP_40 = { opacity: 0, y: 40 };
|
||||
const FADE_IN_UP_4 = { opacity: 0, y: 4 };
|
||||
const FADE_OUT_UP_4 = { opacity: 0, y: 4 };
|
||||
|
||||
const EASE_OUT_06: Transition = { duration: 0.6, ease: "easeOut" };
|
||||
const EASE_OUT_06_DELAY_015: Transition = { duration: 0.6, delay: 0.15, ease: "easeOut" };
|
||||
const EASE_OUT_06_DELAY_01: Transition = { duration: 0.6, delay: 0.1, ease: "easeOut" };
|
||||
const EASE_OUT_06_DELAY_04: Transition = { duration: 0.6, delay: 0.4, ease: "easeOut" };
|
||||
const EASE_OUT_08_DELAY_05: Transition = { duration: 0.8, delay: 0.5, ease: "easeOut" };
|
||||
const EASE_OUT_05: Transition = { duration: 0.5, ease: "easeOut" };
|
||||
const EASE_OUT_015: Transition = { duration: 0.15, ease: "easeOut" };
|
||||
const DURATION_05: Transition = { duration: 0.5 };
|
||||
|
||||
const VIEWPORT_60 = { once: true, margin: "-60px" };
|
||||
|
||||
const SVG_OVERFLOW_VISIBLE_STYLE = { overflow: "visible" as const };
|
||||
const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 1200 };
|
||||
import { CursorFieldProvider } from "~/components/butterfly";
|
||||
import { CommandDialog } from "~/components/command-dialog";
|
||||
import {
|
||||
@@ -38,9 +69,9 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
||||
|
||||
{/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_40}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_08_DELAY_05}
|
||||
className="relative px-6 md:px-8 pb-8 md:pb-16"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
@@ -203,17 +234,17 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: string })
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_06}
|
||||
className="text-3xl md:text-5xl font-medium tracking-tight"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_06_DELAY_015}
|
||||
className="text-white/70 text-lg leading-relaxed max-w-lg"
|
||||
>
|
||||
{subtitle}
|
||||
@@ -222,23 +253,31 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: string })
|
||||
);
|
||||
}
|
||||
|
||||
const CLAUDE_CODE_BADGE_ICON = <ClaudeCodeIcon className="h-6 w-6" />;
|
||||
const CODEX_BADGE_ICON = <CodexIcon className="h-6 w-6" />;
|
||||
const OPENCODE_BADGE_ICON = <OpenCodeIcon className="h-6 w-6" />;
|
||||
const COPILOT_BADGE_ICON = <CopilotIcon className="h-6 w-6" />;
|
||||
const PI_BADGE_ICON = <PiIcon className="h-6 w-6" />;
|
||||
|
||||
function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
const handleMouseEnter = React.useCallback(() => setHovered(true), []);
|
||||
const handleMouseLeave = React.useCallback(() => setHovered(false), []);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="relative inline-flex items-center justify-center rounded-full p-1.5 text-white/60"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{icon}
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 4 }}
|
||||
transition={{ duration: 0.15, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_4}
|
||||
animate={FADE_IN}
|
||||
exit={FADE_OUT_UP_4}
|
||||
transition={EASE_OUT_015}
|
||||
className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded bg-white text-black text-xs whitespace-nowrap pointer-events-none"
|
||||
>
|
||||
{name}
|
||||
@@ -260,10 +299,10 @@ function FeatureSection({
|
||||
}) {
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_60}
|
||||
transition={EASE_OUT_05}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
@@ -275,22 +314,6 @@ function FeatureSection({
|
||||
);
|
||||
}
|
||||
|
||||
function PrinciplesSection() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-40px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="py-32 px-6 md:px-20 max-w-3xl mx-auto text-center"
|
||||
>
|
||||
<p className="text-2xl md:text-4xl font-medium text-white/90">
|
||||
Here's what's under the hood.
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiProviderSection() {
|
||||
const providers = [
|
||||
{ name: "Claude Code", icon: <ClaudeIcon size={28} /> },
|
||||
@@ -411,6 +434,19 @@ function SelfHostedDiagram() {
|
||||
const clientRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||
const hostRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||
const centerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const setClientRef = React.useCallback(
|
||||
(index: number) => (el: HTMLDivElement | null) => {
|
||||
clientRefs.current[index] = el;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const setHostRef = React.useCallback(
|
||||
(index: number) => (el: HTMLDivElement | null) => {
|
||||
hostRefs.current[index] = el;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({
|
||||
left: [],
|
||||
right: [],
|
||||
@@ -510,13 +546,13 @@ function SelfHostedDiagram() {
|
||||
{/* SVG curves */}
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||
style={{ overflow: "visible" }}
|
||||
style={SVG_OVERFLOW_VISIBLE_STYLE}
|
||||
>
|
||||
{[...paths.left, ...paths.right].map(
|
||||
(d, i) =>
|
||||
(d) =>
|
||||
d && (
|
||||
<path
|
||||
key={i}
|
||||
key={d}
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.25)"
|
||||
@@ -532,9 +568,7 @@ function SelfHostedDiagram() {
|
||||
{clients.map((c, i) => (
|
||||
<div
|
||||
key={c.name}
|
||||
ref={(el) => {
|
||||
clientRefs.current[i] = el;
|
||||
}}
|
||||
ref={setClientRef(i)}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
|
||||
>
|
||||
<span className="text-white/80">{c.icon}</span>
|
||||
@@ -564,9 +598,7 @@ function SelfHostedDiagram() {
|
||||
{hosts.map((h, i) => (
|
||||
<div
|
||||
key={h}
|
||||
ref={(el) => {
|
||||
hostRefs.current[i] = el;
|
||||
}}
|
||||
ref={setHostRef(i)}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
|
||||
>
|
||||
<span className="text-white/80">
|
||||
@@ -696,38 +728,43 @@ function ShortcutsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
interface VoiceBarProps {
|
||||
index: number;
|
||||
barCount: number;
|
||||
}
|
||||
|
||||
function VoiceBar({ index, barCount }: VoiceBarProps) {
|
||||
const style = React.useMemo(() => {
|
||||
const center = barCount / 2;
|
||||
const dist = Math.abs(index - center) / center;
|
||||
const envelope = 1 - dist * dist;
|
||||
const minH = 4;
|
||||
const maxH = 56;
|
||||
const baseH = minH + (maxH - minH) * envelope;
|
||||
const jitter = Math.sin(index * 2.3) * 0.3 + Math.cos(index * 1.7) * 0.2;
|
||||
const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(index * 0.8))));
|
||||
return {
|
||||
height: h,
|
||||
animationName: "voice-bar",
|
||||
animationDuration: `${800 + (index % 5) * 200}ms`,
|
||||
animationTimingFunction: "ease-in-out",
|
||||
animationIterationCount: "infinite",
|
||||
animationDirection: "alternate" as const,
|
||||
animationDelay: `${(index % 7) * 80}ms`,
|
||||
};
|
||||
}, [index, barCount]);
|
||||
return <div className="w-[3px] rounded-full bg-white/30" style={style} />;
|
||||
}
|
||||
|
||||
const VOICE_BAR_COUNT = 48;
|
||||
const VOICE_BAR_INDICES = Array.from({ length: VOICE_BAR_COUNT }, (_, i) => i);
|
||||
|
||||
function VoiceWaveform() {
|
||||
const barCount = 48;
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-[3px] h-16">
|
||||
{Array.from({ length: barCount }).map((_, i) => {
|
||||
// Create a natural-looking waveform envelope — louder in center, quieter at edges
|
||||
const center = barCount / 2;
|
||||
const dist = Math.abs(i - center) / center;
|
||||
const envelope = 1 - dist * dist; // quadratic falloff
|
||||
const minH = 4;
|
||||
const maxH = 56;
|
||||
const baseH = minH + (maxH - minH) * envelope;
|
||||
// Vary per-bar so it doesn't look uniform
|
||||
const jitter = Math.sin(i * 2.3) * 0.3 + Math.cos(i * 1.7) * 0.2;
|
||||
const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(i * 0.8))));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="w-[3px] rounded-full bg-white/30"
|
||||
style={{
|
||||
height: h,
|
||||
animationName: "voice-bar",
|
||||
animationDuration: `${800 + (i % 5) * 200}ms`,
|
||||
animationTimingFunction: "ease-in-out",
|
||||
animationIterationCount: "infinite",
|
||||
animationDirection: "alternate",
|
||||
animationDelay: `${(i % 7) * 80}ms`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{VOICE_BAR_INDICES.map((i) => (
|
||||
<VoiceBar key={`voice-bar-${i}`} index={i} barCount={VOICE_BAR_COUNT} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -838,6 +875,25 @@ function useVoiceConversation() {
|
||||
return { dictationWordIndex, responseWordIndex, showResponse };
|
||||
}
|
||||
|
||||
function makeWordKey(words: string[], i: number): string {
|
||||
const word = words[i];
|
||||
let occurrence = 0;
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (words[j] === word) occurrence++;
|
||||
}
|
||||
return `${word}#${occurrence}`;
|
||||
}
|
||||
|
||||
function WordSpan({ word, confirmed }: { word: string; confirmed: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={`transition-colors duration-300 ${confirmed ? "text-white/90" : "text-white/40"}`}
|
||||
>
|
||||
{word}{" "}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamingWords({
|
||||
words,
|
||||
wordIndex,
|
||||
@@ -858,14 +914,7 @@ function StreamingWords({
|
||||
{words.map((word, i) => {
|
||||
if (i >= wordIndex) return null;
|
||||
const confirmed = i < wordIndex - confirmLag;
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={`transition-colors duration-300 ${confirmed ? "text-white/90" : "text-white/40"}`}
|
||||
>
|
||||
{word}{" "}
|
||||
</span>
|
||||
);
|
||||
return <WordSpan key={makeWordKey(words, i)} word={word} confirmed={confirmed} />;
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
@@ -925,9 +974,9 @@ function LocalVoiceSection() {
|
||||
function GetStarted() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_06_DELAY_04}
|
||||
className="pt-10"
|
||||
>
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
@@ -969,11 +1018,11 @@ function GetStarted() {
|
||||
<div className="flex items-center gap-2 pt-6">
|
||||
<span className="text-xs text-white/40">Supports</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<AgentBadge name="Claude Code" icon={<ClaudeCodeIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Codex" icon={<CodexIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="OpenCode" icon={<OpenCodeIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Copilot" icon={<CopilotIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Pi" icon={<PiIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Claude Code" icon={CLAUDE_CODE_BADGE_ICON} />
|
||||
<AgentBadge name="Codex" icon={CODEX_BADGE_ICON} />
|
||||
<AgentBadge name="OpenCode" icon={OPENCODE_BADGE_ICON} />
|
||||
<AgentBadge name="Copilot" icon={COPILOT_BADGE_ICON} />
|
||||
<AgentBadge name="Pi" icon={PI_BADGE_ICON} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -999,23 +1048,27 @@ function DownloadButton() {
|
||||
);
|
||||
}
|
||||
|
||||
const SERVER_INSTALL_TRIGGER = (
|
||||
<span className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors">
|
||||
<TerminalIcon className="h-5 w-5" />
|
||||
</span>
|
||||
);
|
||||
|
||||
const SERVER_INSTALL_FOOTNOTE = (
|
||||
<>
|
||||
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to start the
|
||||
daemon.
|
||||
</>
|
||||
);
|
||||
|
||||
function ServerInstallButton() {
|
||||
return (
|
||||
<CommandDialog
|
||||
trigger={
|
||||
<span className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors">
|
||||
<TerminalIcon className="h-5 w-5" />
|
||||
</span>
|
||||
}
|
||||
trigger={SERVER_INSTALL_TRIGGER}
|
||||
title="Run agents on a remote machine"
|
||||
description="For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon."
|
||||
command="npm install -g @getpaseo/cli && paseo"
|
||||
footnote={
|
||||
<>
|
||||
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to start
|
||||
the daemon.
|
||||
</>
|
||||
}
|
||||
footnote={SERVER_INSTALL_FOOTNOTE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1105,61 +1158,6 @@ function PiIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
function AppStoreIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 960 960"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M342.277 86.6927C463.326 84.6952 587.87 65.619 705.523 104.97C830.467 143.522 874.012 278.153 872.814 397.105C873.713 481.299 874.012 566.193 858.931 649.19C834.262 804.895 746.172 873.01 590.666 874.608C422.377 880.301 172.489 908.965 104.474 711.012C76.5092 599.452 86.6964 481.1 88.1946 366.843C98.9811 200.75 163.301 90.2882 342.277 86.6927ZM715.411 596.156C758.856 591.362 754.362 524.645 710.816 524.545C610.542 525.244 639.605 550.513 594.462 456.83C577.383 418.778 540.529 337.279 496.085 396.006C479.206 431.062 516.359 464.121 528.844 495.382C569.892 560.6 606.647 628.515 648.494 693.334C667.77 724.495 716.509 696.73 697.333 663.372C685.048 642.298 677.258 619.726 665.773 598.253C682.452 597.854 698.831 598.053 715.411 596.156Z" />
|
||||
<path
|
||||
d="M697.234 663.371C716.41 696.729 667.671 724.494 648.395 693.333C606.548 628.614 569.794 560.699 528.745 495.381C516.161 464.219 479.107 431.161 495.986 396.005C540.43 337.178 577.384 418.776 594.363 456.829C639.506 550.512 610.443 525.243 710.717 524.544C754.263 524.644 758.757 591.361 715.312 596.155C698.732 598.052 682.453 597.852 665.674 598.252C677.159 619.725 684.95 642.297 697.234 663.371Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M474.312 257.679C486.597 230.913 517.059 198.453 545.224 224.92C564.3 242.298 551.316 269.465 538.332 287.242C489.194 363.747 450.242 445.844 405.598 524.845C445.448 528.341 485.598 525.844 525.149 532.835C564.1 539.827 558.907 597.455 519.256 598.353C442.153 601.35 365.049 595.457 287.845 599.652C260.28 597.554 225.024 612.336 203.751 589.065C161.104 516.456 275.761 527.442 317.608 524.546C343.776 499.377 356.659 456.93 377.833 425.769C395.311 394.608 412.39 363.147 429.868 331.986C432.964 322.199 418.982 314.109 415.486 305.12C349.169 230.713 442.153 172.885 474.312 257.679Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M265.471 626.12C284.647 595.758 329.491 609.042 330.39 643.199C325.296 664.872 313.511 684.647 298.53 701.027C275.758 724.997 235.009 703.124 242.5 670.864C246.195 654.485 256.882 640.302 265.471 626.12Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({ number, children }: { number: number; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-white/20 flex items-center justify-center text-xs font-medium">
|
||||
{number}
|
||||
</span>
|
||||
<div className="space-y-2 flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const bashKeywords = new Set([
|
||||
"while",
|
||||
"do",
|
||||
@@ -1174,167 +1172,134 @@ const bashKeywords = new Set([
|
||||
]);
|
||||
const bashCommands = new Set(["paseo", "echo", "jq"]);
|
||||
|
||||
function tokenizeBashComment(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
const end = code.indexOf("\n", i);
|
||||
const comment = end === -1 ? code.slice(i) : code.slice(i, end);
|
||||
return {
|
||||
node: <span className="text-white/30 italic">{comment}</span>,
|
||||
len: comment.length,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeBashDoubleQuoted(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== '"') {
|
||||
if (code[j] === "\\") j++;
|
||||
j++;
|
||||
}
|
||||
const str = code.slice(i, j + 1);
|
||||
return { node: <span className="text-green-400/80">{str}</span>, len: str.length };
|
||||
}
|
||||
|
||||
function tokenizeBashSingleQuoted(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== "'") j++;
|
||||
const str = code.slice(i, j + 1);
|
||||
return { node: <span className="text-green-400/80">{str}</span>, len: str.length };
|
||||
}
|
||||
|
||||
function tokenizeBashDollar(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
if (code[i + 1] === "(") {
|
||||
return { node: <span className="text-amber-300/70">$(</span>, len: 2 };
|
||||
}
|
||||
let j = i + 1;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
return {
|
||||
node: <span className="text-amber-300/70">{code.slice(i, j)}</span>,
|
||||
len: j - i,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeBashFlag(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i;
|
||||
if (code[j + 1] === "-") j++;
|
||||
j++;
|
||||
while (j < code.length && /[\w-]/.test(code[j])) j++;
|
||||
return {
|
||||
node: <span className="text-sky-300/70">{code.slice(i, j)}</span>,
|
||||
len: j - i,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeBashWord(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
let j = i;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
const word = code.slice(i, j);
|
||||
const len = j - i;
|
||||
if (bashKeywords.has(word)) {
|
||||
return { node: <span className="text-purple-400">{word}</span>, len };
|
||||
}
|
||||
if (bashCommands.has(word)) {
|
||||
return { node: <span className="text-white">{word}</span>, len };
|
||||
}
|
||||
return { node: word, len };
|
||||
}
|
||||
|
||||
function isBashFlagStart(code: string, i: number): boolean {
|
||||
return (
|
||||
code[i] === "-" &&
|
||||
(i === 0 || /\s/.test(code[i - 1])) &&
|
||||
i + 1 < code.length &&
|
||||
/[\w-]/.test(code[i + 1])
|
||||
);
|
||||
}
|
||||
|
||||
function isBashCommentStart(code: string, i: number): boolean {
|
||||
return code[i] === "#" && (i === 0 || /[\s(]/.test(code[i - 1]));
|
||||
}
|
||||
|
||||
function tokenizeBashChar(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
const c = code[i];
|
||||
if (c === "|" || (c === "&" && code[i + 1] === "&")) {
|
||||
const op = c === "|" ? "|" : "&&";
|
||||
return { node: <span className="text-white/40">{op}</span>, len: op.length };
|
||||
}
|
||||
if (c === "\\") return { node: <span className="text-white/40">\</span>, len: 1 };
|
||||
if (c === ")") return { node: <span className="text-amber-300/70">)</span>, len: 1 };
|
||||
return { node: c, len: 1 };
|
||||
}
|
||||
|
||||
function nextBashToken(code: string, i: number): { node: React.ReactNode; len: number } {
|
||||
if (isBashCommentStart(code, i)) return tokenizeBashComment(code, i);
|
||||
if (code[i] === '"') return tokenizeBashDoubleQuoted(code, i);
|
||||
if (code[i] === "'") return tokenizeBashSingleQuoted(code, i);
|
||||
if (code[i] === "$") return tokenizeBashDollar(code, i);
|
||||
if (isBashFlagStart(code, i)) return tokenizeBashFlag(code, i);
|
||||
if (/[a-zA-Z_]/.test(code[i])) return tokenizeBashWord(code, i);
|
||||
return tokenizeBashChar(code, i);
|
||||
}
|
||||
|
||||
function highlightBash(code: string): React.ReactNode {
|
||||
const tokens: React.ReactNode[] = [];
|
||||
let i = 0;
|
||||
let key = 0;
|
||||
|
||||
while (i < code.length) {
|
||||
if (code[i] === "#" && (i === 0 || /[\s(]/.test(code[i - 1]))) {
|
||||
const end = code.indexOf("\n", i);
|
||||
const comment = end === -1 ? code.slice(i) : code.slice(i, end);
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white/30 italic">
|
||||
{comment}
|
||||
</span>,
|
||||
);
|
||||
i += comment.length;
|
||||
continue;
|
||||
const { node, len } = nextBashToken(code, i);
|
||||
if (React.isValidElement(node)) {
|
||||
tokens.push(React.cloneElement(node, { key: key++ }));
|
||||
} else {
|
||||
tokens.push(node);
|
||||
}
|
||||
|
||||
if (code[i] === '"') {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== '"') {
|
||||
if (code[j] === "\\") j++;
|
||||
j++;
|
||||
}
|
||||
const str = code.slice(i, j + 1);
|
||||
tokens.push(
|
||||
<span key={key++} className="text-green-400/80">
|
||||
{str}
|
||||
</span>,
|
||||
);
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "'") {
|
||||
let j = i + 1;
|
||||
while (j < code.length && code[j] !== "'") j++;
|
||||
const str = code.slice(i, j + 1);
|
||||
tokens.push(
|
||||
<span key={key++} className="text-green-400/80">
|
||||
{str}
|
||||
</span>,
|
||||
);
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "$") {
|
||||
if (code[i + 1] === "(") {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-amber-300/70">
|
||||
$(
|
||||
</span>,
|
||||
);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
let j = i + 1;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
tokens.push(
|
||||
<span key={key++} className="text-amber-300/70">
|
||||
{code.slice(i, j)}
|
||||
</span>,
|
||||
);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
code[i] === "-" &&
|
||||
(i === 0 || /\s/.test(code[i - 1])) &&
|
||||
i + 1 < code.length &&
|
||||
/[\w-]/.test(code[i + 1])
|
||||
) {
|
||||
let j = i;
|
||||
if (code[j + 1] === "-") j++;
|
||||
j++;
|
||||
while (j < code.length && /[\w-]/.test(code[j])) j++;
|
||||
tokens.push(
|
||||
<span key={key++} className="text-sky-300/70">
|
||||
{code.slice(i, j)}
|
||||
</span>,
|
||||
);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/[a-zA-Z_]/.test(code[i])) {
|
||||
let j = i;
|
||||
while (j < code.length && /\w/.test(code[j])) j++;
|
||||
const word = code.slice(i, j);
|
||||
if (bashKeywords.has(word)) {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-purple-400">
|
||||
{word}
|
||||
</span>,
|
||||
);
|
||||
} else if (bashCommands.has(word)) {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white">
|
||||
{word}
|
||||
</span>,
|
||||
);
|
||||
} else {
|
||||
tokens.push(word);
|
||||
key++;
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "|" || (code[i] === "&" && code[i + 1] === "&")) {
|
||||
const op = code[i] === "|" ? "|" : "&&";
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white/40">
|
||||
{op}
|
||||
</span>,
|
||||
);
|
||||
i += op.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === "\\") {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-white/40">
|
||||
\
|
||||
</span>,
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code[i] === ")") {
|
||||
tokens.push(
|
||||
<span key={key++} className="text-amber-300/70">
|
||||
)
|
||||
</span>,
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
tokens.push(code[i]);
|
||||
i++;
|
||||
i += len;
|
||||
}
|
||||
|
||||
return <>{tokens}</>;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function CLICodeBlock({ children }: { children: string }) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
function handleCopy() {
|
||||
const handleCopy = React.useCallback(() => {
|
||||
navigator.clipboard.writeText(children);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className="relative bg-white/5 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="absolute top-3 right-3 text-white/30 hover:text-white/70 transition-colors p-1"
|
||||
title="Copy to clipboard"
|
||||
@@ -1440,13 +1405,24 @@ function PhoneShowcase() {
|
||||
const leftX = useTransform(scrollYProgress, [0.2, 0.6], [0, -slideDistance]);
|
||||
const rightX = useTransform(scrollYProgress, [0.2, 0.6], [0, slideDistance]);
|
||||
|
||||
const leftPhoneStyle = React.useMemo(
|
||||
() => ({ opacity: sideOpacity, x: leftX, rotateY: -15, scale: 0.97 }),
|
||||
[sideOpacity, leftX],
|
||||
);
|
||||
const rightPhoneStyle = React.useMemo(
|
||||
() => ({ opacity: sideOpacity, x: rightX, rotateY: 15, scale: 0.97 }),
|
||||
[sideOpacity, rightX],
|
||||
);
|
||||
const centerPhoneAnimate = React.useMemo(() => (textInView ? FADE_IN : {}), [textInView]);
|
||||
const textAnimate = React.useMemo(() => (textInView ? FADE_IN : {}), [textInView]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex flex-col items-center pt-4 pb-16 gap-20">
|
||||
{/* Arrow + text */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={textInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5 }}
|
||||
initial={FADE_IN_UP_TINY}
|
||||
animate={textAnimate}
|
||||
transition={DURATION_05}
|
||||
className="flex flex-col items-center gap-1.5 px-6"
|
||||
>
|
||||
<svg
|
||||
@@ -1474,13 +1450,10 @@ function PhoneShowcase() {
|
||||
{/* Phone trio — side phones are absolute, start behind center, slide outward with perspective rotation */}
|
||||
<div
|
||||
className="relative flex items-center justify-center overflow-x-clip w-full"
|
||||
style={{ minHeight: 480, perspective: 1200 }}
|
||||
style={PHONE_PERSPECTIVE_STYLE}
|
||||
>
|
||||
{/* Left phone — rotated to face inward */}
|
||||
<motion.div
|
||||
style={{ opacity: sideOpacity, x: leftX, rotateY: -15, scale: 0.97 }}
|
||||
className="w-[160px] md:w-[240px] absolute"
|
||||
>
|
||||
<motion.div style={leftPhoneStyle} className="w-[160px] md:w-[240px] absolute">
|
||||
<img
|
||||
src="/phone-1.png"
|
||||
alt="Paseo sessions list"
|
||||
@@ -1490,9 +1463,9 @@ function PhoneShowcase() {
|
||||
|
||||
{/* Center phone */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={textInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_XL}
|
||||
animate={centerPhoneAnimate}
|
||||
transition={EASE_OUT_06_DELAY_01}
|
||||
className="w-[220px] md:w-[240px] relative z-10"
|
||||
>
|
||||
<img
|
||||
@@ -1503,10 +1476,7 @@ function PhoneShowcase() {
|
||||
</motion.div>
|
||||
|
||||
{/* Right phone — rotated to face inward */}
|
||||
<motion.div
|
||||
style={{ opacity: sideOpacity, x: rightX, rotateY: 15, scale: 0.97 }}
|
||||
className="w-[160px] md:w-[240px] absolute"
|
||||
>
|
||||
<motion.div style={rightPhoneStyle} className="w-[160px] md:w-[240px] absolute">
|
||||
<img
|
||||
src="/phone-3.png"
|
||||
alt="Paseo diff view"
|
||||
@@ -1518,6 +1488,33 @@ function PhoneShowcase() {
|
||||
);
|
||||
}
|
||||
|
||||
function CLITabButton({
|
||||
title,
|
||||
index,
|
||||
active,
|
||||
onSelect,
|
||||
}: {
|
||||
title: string;
|
||||
index: number;
|
||||
active: boolean;
|
||||
onSelect: (i: number) => void;
|
||||
}) {
|
||||
const handleClick = React.useCallback(() => onSelect(index), [onSelect, index]);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
active
|
||||
? "border-white/40 text-white bg-white/10"
|
||||
: "border-white/15 text-white/50 hover:text-white/80 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CLISection() {
|
||||
const [activeIndex, setActiveIndex] = React.useState(0);
|
||||
const active = cliExamples[activeIndex];
|
||||
@@ -1529,17 +1526,13 @@ function CLISection() {
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{cliExamples.map((example, i) => (
|
||||
<button
|
||||
<CLITabButton
|
||||
key={example.title}
|
||||
onClick={() => setActiveIndex(i)}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
i === activeIndex
|
||||
? "border-white/40 text-white bg-white/10"
|
||||
: "border-white/15 text-white/50 hover:text-white/80 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
{example.title}
|
||||
</button>
|
||||
title={example.title}
|
||||
index={i}
|
||||
active={i === activeIndex}
|
||||
onSelect={setActiveIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1573,10 +1566,10 @@ function CLISection() {
|
||||
function FAQ() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_60}
|
||||
transition={EASE_OUT_05}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h2 className="text-3xl font-medium">FAQ</h2>
|
||||
@@ -1587,8 +1580,8 @@ function FAQ() {
|
||||
speech providers if you configure them.
|
||||
</FAQItem>
|
||||
<FAQItem question="Does my code leave my machine?">
|
||||
Paseo doesn't send your code anywhere. Agents run locally and talk to their own APIs as
|
||||
they normally would. For remote access, you can use the optional{" "}
|
||||
Paseo doesn't send your code anywhere. Agents run locally and talk to their own APIs
|
||||
as they normally would. For remote access, you can use the optional{" "}
|
||||
<a href="/docs/security" className="underline hover:text-white/80">
|
||||
end-to-end encrypted relay
|
||||
</a>
|
||||
@@ -1596,7 +1589,7 @@ function FAQ() {
|
||||
</FAQItem>
|
||||
<FAQItem question="What agents does it support?">
|
||||
Claude Code, Codex, and OpenCode. Each agent runs as its own process using its own CLI.
|
||||
Paseo doesn't modify or wrap their behavior.
|
||||
Paseo doesn't modify or wrap their behavior.
|
||||
</FAQItem>
|
||||
<FAQItem question="Do I need the desktop app?">
|
||||
No. You can run the daemon headless with{" "}
|
||||
@@ -1616,9 +1609,9 @@ function FAQ() {
|
||||
.
|
||||
</FAQItem>
|
||||
<FAQItem question="Can I connect from outside my network?">
|
||||
Yes. You can use the hosted relay (end-to-end encrypted, Paseo can't read your traffic),
|
||||
set up your own tunnel (Tailscale, Cloudflare Tunnel, etc.), or expose the daemon port
|
||||
directly. See{" "}
|
||||
Yes. You can use the hosted relay (end-to-end encrypted, Paseo can't read your
|
||||
traffic), set up your own tunnel (Tailscale, Cloudflare Tunnel, etc.), or expose the
|
||||
daemon port directly. See{" "}
|
||||
<a href="/docs/configuration" className="underline hover:text-white/80">
|
||||
configuration
|
||||
</a>
|
||||
@@ -1626,17 +1619,17 @@ function FAQ() {
|
||||
</FAQItem>
|
||||
<FAQItem question="Do I need git or GitHub?">
|
||||
No. Paseo works in any directory. Worktrees are optional and only relevant if you use git.
|
||||
You can run agents anywhere you'd normally work.
|
||||
You can run agents anywhere you'd normally work.
|
||||
</FAQItem>
|
||||
<FAQItem question="Can I get banned for using Paseo?">
|
||||
<p>We can't make promises on behalf of providers.</p>
|
||||
<p>We can't make promises on behalf of providers.</p>
|
||||
<p>
|
||||
That said, Paseo launches the official first-party CLIs (Claude Code, Codex, OpenCode)
|
||||
as subprocesses. It doesn't extract tokens or call inference APIs directly. From the
|
||||
provider's perspective, usage through Paseo is indistinguishable from running the CLI
|
||||
yourself.
|
||||
as subprocesses. It doesn't extract tokens or call inference APIs directly. From
|
||||
the provider's perspective, usage through Paseo is indistinguishable from running
|
||||
the CLI yourself.
|
||||
</p>
|
||||
<p>I've been using Paseo with all providers for months without issue.</p>
|
||||
<p>I've been using Paseo with all providers for months without issue.</p>
|
||||
</FAQItem>
|
||||
<FAQItem question="How do worktrees work?">
|
||||
When you launch an agent with the worktree option (from the app, desktop, or CLI), Paseo
|
||||
@@ -1655,17 +1648,17 @@ function FAQ() {
|
||||
function SponsorCTA() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_60}
|
||||
transition={EASE_OUT_05}
|
||||
className="rounded-xl bg-white/5 border border-white/10 p-8 md:p-10 text-left space-y-4 max-w-xl mx-auto"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed space-y-3">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>- Mo</p>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,10 @@ interface StarsContext {
|
||||
const ReleaseCtx = createContext<ReleaseContext>({ version: "" });
|
||||
const StarsCtx = createContext<StarsContext>({ stars: "" });
|
||||
|
||||
const PLAUSIBLE_INIT_SCRIPT = {
|
||||
__html: `window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()`,
|
||||
};
|
||||
|
||||
export function useRelease(): ReleaseContext {
|
||||
return useContext(ReleaseCtx);
|
||||
}
|
||||
@@ -67,11 +71,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||
<head>
|
||||
<HeadContent />
|
||||
<script async src="https://plausible.io/js/pa-cKNUoWbeH_Iksb2fh82s3.js" />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()`,
|
||||
}}
|
||||
/>
|
||||
<script dangerouslySetInnerHTML={PLAUSIBLE_INIT_SCRIPT} />
|
||||
</head>
|
||||
<body className="antialiased bg-background text-foreground">
|
||||
{children}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { getPosts, formatDate } from "~/posts";
|
||||
import { pageMeta } from "~/meta";
|
||||
@@ -12,6 +13,34 @@ export const Route = createFileRoute("/blog/")({
|
||||
component: BlogIndex,
|
||||
});
|
||||
|
||||
interface PostRowProps {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
draft?: boolean;
|
||||
}
|
||||
|
||||
function PostRow({ slug, title, date, draft }: PostRowProps) {
|
||||
const params = useMemo(() => ({ _splat: slug }), [slug]);
|
||||
return (
|
||||
<div className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4">
|
||||
<span className="text-lg text-muted-foreground tabular-nums">
|
||||
{formatDate(new Date(date))}
|
||||
</span>
|
||||
<Link
|
||||
to="/blog/$"
|
||||
params={params}
|
||||
className="text-lg text-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{title}
|
||||
{draft && (
|
||||
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">DRAFT</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlogIndex() {
|
||||
const { drafts } = Route.useSearch();
|
||||
const posts = getPosts(drafts);
|
||||
@@ -25,26 +54,13 @@ function BlogIndex() {
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{posts.map(({ slug, frontmatter }) => (
|
||||
<div
|
||||
<PostRow
|
||||
key={slug}
|
||||
className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4"
|
||||
>
|
||||
<span className="text-lg text-muted-foreground tabular-nums">
|
||||
{formatDate(new Date(frontmatter.date))}
|
||||
</span>
|
||||
<Link
|
||||
to="/blog/$"
|
||||
params={{ _splat: slug }}
|
||||
className="text-lg text-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{frontmatter.title}
|
||||
{frontmatter.draft && (
|
||||
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">
|
||||
DRAFT
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
slug={slug}
|
||||
title={frontmatter.title}
|
||||
date={frontmatter.date}
|
||||
draft={frontmatter.draft}
|
||||
/>
|
||||
))}
|
||||
{posts.length === 0 && <p className="text-muted-foreground">No posts yet.</p>}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,10 @@ const navigation = [
|
||||
{ name: "Best practices", href: "/docs/best-practices" },
|
||||
];
|
||||
|
||||
const ACTIVE_OPTIONS_EXACT = { exact: true };
|
||||
const MOBILE_ACTIVE_PROPS = { className: "text-foreground" };
|
||||
const DESKTOP_ACTIVE_PROPS = { className: "bg-muted text-foreground" };
|
||||
|
||||
function DocsLayout() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
@@ -32,9 +36,9 @@ function DocsLayout() {
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={{ exact: true }}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
activeProps={{ className: "text-foreground" }}
|
||||
activeProps={MOBILE_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
@@ -54,9 +58,9 @@ function DocsLayout() {
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={{ exact: true }}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
className="block px-3 py-2 text-sm rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
activeProps={{ className: "bg-muted text-foreground" }}
|
||||
activeProps={DESKTOP_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
|
||||
@@ -17,22 +17,23 @@ function BestPractices() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium font-title mb-4">Best Practices</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
What I've learned from using Paseo daily. Not rules, just patterns that have worked for
|
||||
me.
|
||||
What I've learned from using Paseo daily. Not rules, just patterns that have worked
|
||||
for me.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agents replace typing, not thinking</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Your role has changed. You're no longer the one writing code line by line. You're the one
|
||||
making decisions: what to build, how it should work, what the architecture looks like. The
|
||||
agent executes, but you direct.
|
||||
Your role has changed. You're no longer the one writing code line by line.
|
||||
You're the one making decisions: what to build, how it should work, what the
|
||||
architecture looks like. The agent executes, but you direct.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
You can't just say "implement feature X" and walk away. You still have to do the hard
|
||||
part: deciding what to build, how it fits into the system, what trade-offs to make.
|
||||
Thinking is not optional. At least for now, agents replace the typing, not the thinking.
|
||||
You can't just say "implement feature X" and walk away. You still have to
|
||||
do the hard part: deciding what to build, how it fits into the system, what trade-offs to
|
||||
make. Thinking is not optional. At least for now, agents replace the typing, not the
|
||||
thinking.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -41,16 +42,17 @@ function BestPractices() {
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The agent needs a way to verify its work. TDD is one implementation of this pattern: get
|
||||
the agent to write a failing test, verify it fails for the right reasons, then tell it to
|
||||
make the test pass. The agent can loop on its own because it knows what "done" means.
|
||||
make the test pass. The agent can loop on its own because it knows what "done"
|
||||
means.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Invest in tooling</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
It's not just test runners. For web apps, something like Playwright MCP lets the agent
|
||||
take screenshots and verify UI changes. For a SaaS app I built a CLI that wraps all the
|
||||
business logic so the agent could launch jobs, check statuses, and scrape data without
|
||||
It's not just test runners. For web apps, something like Playwright MCP lets the
|
||||
agent take screenshots and verify UI changes. For a SaaS app I built a CLI that wraps all
|
||||
the business logic so the agent could launch jobs, check statuses, and scrape data without
|
||||
going through the UI.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
@@ -62,22 +64,22 @@ function BestPractices() {
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agents are cheap</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Don't be shy about running multiple agents. Paseo lets you launch agents in isolated
|
||||
Don't be shy about running multiple agents. Paseo lets you launch agents in isolated
|
||||
worktrees. Kick one off with voice while walking, then kick off another. They work
|
||||
independently. You get a notification when they're done.
|
||||
independently. You get a notification when they're done.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Use voice extensively</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
It's much more natural to use voice to communicate ideas and pull them out of your brain.
|
||||
The agent will parse and organize your thoughts better than if you try to write the
|
||||
perfect prompt. You don't need to organize anything. Just talk.
|
||||
It's much more natural to use voice to communicate ideas and pull them out of your
|
||||
brain. The agent will parse and organize your thoughts better than if you try to write the
|
||||
perfect prompt. You don't need to organize anything. Just talk.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Current speech-to-text models are really good. They catch accents, acronyms, technical
|
||||
terms. And even when they don't, the LLM will infer what you meant.
|
||||
terms. And even when they don't, the LLM will infer what you meant.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -85,22 +87,22 @@ function BestPractices() {
|
||||
<h2 className="text-xl font-medium">Understand the type of work</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Sometimes you need to plan: design a spec, verify it, get the agent to follow through.
|
||||
Maybe it takes a couple of agents to work through it. Other times it's conversational:
|
||||
kick off a single agent and start talking, asking questions. Match your approach to the
|
||||
task.
|
||||
Maybe it takes a couple of agents to work through it. Other times it's
|
||||
conversational: kick off a single agent and start talking, asking questions. Match your
|
||||
approach to the task.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Iterate and refactor often</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Don't expect perfect. Expect working. Make it work, make it correct, make it beautiful.
|
||||
Each iteration gets you closer. With tests, refactoring is cheap.
|
||||
Don't expect perfect. Expect working. Make it work, make it correct, make it
|
||||
beautiful. Each iteration gets you closer. With tests, refactoring is cheap.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
I don't let myself add too many features before stopping to refactor. Sometimes I kick off
|
||||
an agent and have it trace code paths, explain dependencies, show me how modules connect.
|
||||
I make mental notes during code review and circle back.
|
||||
I don't let myself add too many features before stopping to refactor. Sometimes I
|
||||
kick off an agent and have it trace code paths, explain dependencies, show me how modules
|
||||
connect. I make mental notes during code review and circle back.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -108,25 +110,25 @@ function BestPractices() {
|
||||
<h2 className="text-xl font-medium">Use agents to check agents</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
If an agent implements something and you ask it to review its own work, it will never find
|
||||
issues. Launch a separate agent with a fresh context to review the first agent's code. It
|
||||
will catch things the first agent missed or glossed over. An agent might say it's done
|
||||
when it's not. Another agent can detect that.
|
||||
issues. Launch a separate agent with a fresh context to review the first agent's
|
||||
code. It will catch things the first agent missed or glossed over. An agent might say
|
||||
it's done when it's not. Another agent can detect that.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Learn your agents' quirks</h2>
|
||||
<h2 className="text-xl font-medium">Learn your agents' quirks</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
People argue about which model is better. That's the wrong question. Each model has
|
||||
People argue about which model is better. That's the wrong question. Each model has
|
||||
strengths and weaknesses. Knowing them is more useful than chasing benchmarks. Benchmarks
|
||||
don't mean anything. You need to try the models yourself to form an opinion.
|
||||
don't mean anything. You need to try the models yourself to form an opinion.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
I use Claude Code as my main driver because it's quick and uses tools well. But sometimes
|
||||
it jumps to conclusions and gives up too easily. Codex is frustratingly slow but goes
|
||||
deep, doesn't stop, and is methodical. It's also stubborn and too serious. These aren't
|
||||
good or bad traits, just differences you learn to work around. Use the right model for the
|
||||
job.
|
||||
I use Claude Code as my main driver because it's quick and uses tools well. But
|
||||
sometimes it jumps to conclusions and gives up too easily. Codex is frustratingly slow but
|
||||
goes deep, doesn't stop, and is methodical. It's also stubborn and too serious.
|
||||
These aren't good or bad traits, just differences you learn to work around. Use the
|
||||
right model for the job.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,9 @@ function CLI() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium font-title mb-4">CLI</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The Paseo CLI lets you manage agents from your terminal. It's the same interface exposed
|
||||
by the daemon's API, so anything you can do in the app you can do from the command line.
|
||||
The Paseo CLI lets you manage agents from your terminal. It's the same interface
|
||||
exposed by the daemon's API, so anything you can do in the app you can do from the
|
||||
command line.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -96,14 +97,14 @@ paseo ls -a -g --json # Full list as JSON`}</pre>
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Streaming output</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Use <code className="font-mono">paseo attach</code> to stream an agent's output in
|
||||
Use <code className="font-mono">paseo attach</code> to stream an agent's output in
|
||||
real-time:
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`paseo attach abc123 # Attach to agent (Ctrl+C to detach)`}</pre>
|
||||
</Code>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Agent IDs can be shortened — <code className="font-mono">abc</code> works if it's
|
||||
Agent IDs can be shortened — <code className="font-mono">abc</code> works if it's
|
||||
unambiguous.
|
||||
</p>
|
||||
</section>
|
||||
@@ -164,7 +165,7 @@ paseo permit deny <id> --all # Deny all pending`}</pre>
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agent modes</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Change an agent's operational mode (provider-specific):
|
||||
Change an agent's operational mode (provider-specific):
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`paseo agent mode <id> --list # Show available modes
|
||||
|
||||
@@ -84,7 +84,7 @@ function GettingStarted() {
|
||||
>
|
||||
GitHub releases page
|
||||
</a>
|
||||
. Open it and you're done.
|
||||
. Open it and you're done.
|
||||
</p>
|
||||
<p className="text-white/60">
|
||||
The desktop app bundles and manages its own daemon automatically, so you do not need a
|
||||
|
||||
@@ -43,12 +43,12 @@ function Providers() {
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-3 list-disc list-inside">
|
||||
<li>
|
||||
<code className="font-mono">claude</code> — Anthropic's Claude Code. Multi-tool
|
||||
<code className="font-mono">claude</code> — Anthropic's Claude Code. Multi-tool
|
||||
assistant with MCP support, streaming, and deep reasoning.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">codex</code> — OpenAI's Codex workspace agent with sandbox
|
||||
controls and optional network access.
|
||||
<code className="font-mono">codex</code> — OpenAI's Codex workspace agent with
|
||||
sandbox controls and optional network access.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">opencode</code> — Open-source coding assistant with
|
||||
@@ -90,14 +90,15 @@ function Providers() {
|
||||
Client Protocol over stdio.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Disable</strong> a provider you don't use.
|
||||
<strong>Disable</strong> a provider you don't use.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Provider IDs must be lowercase alphanumeric with hyphens (
|
||||
<code className="font-mono">/^[a-z][a-z0-9-]*$/</code>). Every custom entry needs{" "}
|
||||
<code className="font-mono">extends</code> (a first-class provider ID or{" "}
|
||||
<code className="font-mono">"acp"</code>) and a <code className="font-mono">label</code>.
|
||||
<code className="font-mono">"acp"</code>) and a{" "}
|
||||
<code className="font-mono">label</code>.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The examples below are a quick tour. The full, up-to-date reference is on GitHub:{" "}
|
||||
@@ -139,7 +140,7 @@ function Providers() {
|
||||
Z.AI exposes GLM models through an Anthropic-compatible endpoint. Point{" "}
|
||||
<code className="font-mono">ANTHROPIC_BASE_URL</code> at their API and use{" "}
|
||||
<code className="font-mono">ANTHROPIC_AUTH_TOKEN</code> for the key. Third-party endpoints
|
||||
don't support Anthropic's server-side tools, so disable{" "}
|
||||
don't support Anthropic's server-side tools, so disable{" "}
|
||||
<code className="font-mono">WebSearch</code>.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
@@ -169,8 +170,8 @@ function Providers() {
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Alibaba Cloud (Qwen) coding plan</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Alibaba's coding plan routes Claude Code to Qwen models via an Anthropic-compatible API.
|
||||
Subscription keys look like <code className="font-mono">sk-sp-...</code> and must be
|
||||
Alibaba's coding plan routes Claude Code to Qwen models via an Anthropic-compatible
|
||||
API. Subscription keys look like <code className="font-mono">sk-sp-...</code> and must be
|
||||
created in the Singapore region.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
@@ -253,8 +254,8 @@ function Providers() {
|
||||
>
|
||||
ACP
|
||||
</a>{" "}
|
||||
over stdio can be added with <code className="font-mono">extends: "acp"</code> and a{" "}
|
||||
<code className="font-mono">command</code>. Paseo spawns the process, sends an{" "}
|
||||
over stdio can be added with <code className="font-mono">extends: "acp"</code>{" "}
|
||||
and a <code className="font-mono">command</code>. Paseo spawns the process, sends an{" "}
|
||||
<code className="font-mono">initialize</code> JSON-RPC request, and the agent reports its
|
||||
capabilities, modes, and models at runtime.
|
||||
</p>
|
||||
|
||||
@@ -83,8 +83,8 @@ function Security() {
|
||||
<code className="font-mono">$PASEO_HOME/daemon-keypair.json</code>
|
||||
</li>
|
||||
<li>
|
||||
When you scan the QR code or click the pairing link, your phone receives the daemon's
|
||||
public key
|
||||
When you scan the QR code or click the pairing link, your phone receives the
|
||||
daemon's public key
|
||||
</li>
|
||||
<li>
|
||||
Your phone sends a handshake message with its own public key. The daemon will not accept
|
||||
@@ -100,15 +100,15 @@ function Security() {
|
||||
message contents, forge messages, or derive encryption keys from observing the handshake.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium mt-6">Why the relay can't attack you</h3>
|
||||
<h3 className="text-lg font-medium mt-6">Why the relay can't attack you</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The daemon requires a valid cryptographic handshake before processing any commands. A
|
||||
compromised relay cannot:
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-white/80">Send commands</strong> — Without your phone's private
|
||||
key, it cannot complete the handshake
|
||||
<strong className="text-white/80">Send commands</strong> — Without your phone's
|
||||
private key, it cannot complete the handshake
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/80">Read your traffic</strong> — All messages are
|
||||
@@ -126,9 +126,9 @@ function Security() {
|
||||
|
||||
<h3 className="text-lg font-medium mt-6">Trust model</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The QR code or pairing link is the trust anchor. It contains the daemon's public key,
|
||||
which is required to establish the encrypted connection. Treat it like a password — don't
|
||||
share it publicly.
|
||||
The QR code or pairing link is the trust anchor. It contains the daemon's public key,
|
||||
which is required to establish the encrypted connection. Treat it like a password —
|
||||
don't share it publicly.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
If you believe a pairing offer has been compromised, restart the daemon to generate a new
|
||||
@@ -226,7 +226,7 @@ function Security() {
|
||||
<code className="font-mono">*.localhost</code>, and all IP addresses
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">['.example.com']</code>: allow{" "}
|
||||
<code className="font-mono">['.example.com']</code>: allow{" "}
|
||||
<code className="font-mono">example.com</code> and any subdomain, plus defaults
|
||||
</li>
|
||||
<li>
|
||||
@@ -244,8 +244,8 @@ function Security() {
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-white/80">Claude Code</strong> — authenticates via Anthropic's
|
||||
OAuth flow, stored in <code className="font-mono">~/.claude/</code>
|
||||
<strong className="text-white/80">Claude Code</strong> — authenticates via
|
||||
Anthropic's OAuth flow, stored in <code className="font-mono">~/.claude/</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/80">Codex</strong> — uses your OpenAI API key or OAuth
|
||||
@@ -267,8 +267,8 @@ function Security() {
|
||||
<h2 className="text-xl font-medium">Recommendations</h2>
|
||||
<ul className="text-white/60 space-y-3 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-white/80">Use the relay</strong> for mobile access — it's the
|
||||
simplest option and all traffic is end-to-end encrypted
|
||||
<strong className="text-white/80">Use the relay</strong> for mobile access — it's
|
||||
the simplest option and all traffic is end-to-end encrypted
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/80">Treat the QR code like a password</strong> — anyone
|
||||
|
||||
@@ -28,8 +28,8 @@ function 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
|
||||
Integrations settings or via the CLI.
|
||||
how to orchestrate without you writing boilerplate. Install them from the desktop
|
||||
app's Integrations settings or via the CLI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,8 @@ function Skills() {
|
||||
command reference so agents know how to run commands.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Not typically invoked directly by users — it's a reference that other skills depend on.
|
||||
Not typically invoked directly by users — it's a reference that other skills depend
|
||||
on.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -71,8 +72,8 @@ function Skills() {
|
||||
</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Hands off your current task to another agent with full context. The receiving agent gets a
|
||||
comprehensive prompt with: task description, relevant files, what's been tried, decisions
|
||||
made, and acceptance criteria.
|
||||
comprehensive prompt with: task description, relevant files, what's been tried,
|
||||
decisions made, and acceptance criteria.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Default provider is Codex. Can specify Claude (sonnet/opus). Supports{" "}
|
||||
|
||||
@@ -59,7 +59,7 @@ function Worktrees() {
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Drop a <code className="font-mono">paseo.json</code> in your repo root. Paseo reads it
|
||||
from the committed version of the base branch you picked, so uncommitted changes in other
|
||||
branches don't apply.
|
||||
branches don't apply.
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`{
|
||||
@@ -113,7 +113,7 @@ function Worktrees() {
|
||||
<code className="font-mono">scripts</code> are named commands you can run inside a
|
||||
worktree on demand. Mark one as a <em>service</em> and Paseo supervises it as a
|
||||
long-running process, assigns it a port, and routes HTTP traffic to it through the
|
||||
daemon's reverse proxy.
|
||||
daemon's reverse proxy.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium mt-4">Plain scripts</h3>
|
||||
@@ -167,9 +167,9 @@ http://<script>.<project>.localhost:<daemon-port>`}</pre>
|
||||
|
||||
<h3 className="text-lg font-medium mt-6">Service-to-service</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Services launched from the same workspace see each other's ports and proxy URLs. Given{" "}
|
||||
<code className="font-mono">web</code> and <code className="font-mono">api</code> above,
|
||||
each process gets:
|
||||
Services launched from the same workspace see each other's ports and proxy URLs.
|
||||
Given <code className="font-mono">web</code> and <code className="font-mono">api</code>{" "}
|
||||
above, each process gets:
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`PASEO_PORT=3000 # this service's port
|
||||
@@ -219,7 +219,7 @@ PASEO_SERVICE_WEB_URL=http://web.my-app.localhost:6767`}</pre>
|
||||
<code className="font-mono">$PASEO_WORKTREE_PATH</code> — the worktree directory
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the worktree's branch
|
||||
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the worktree's branch
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_WORKTREE_PORT</code> — legacy per-worktree port
|
||||
@@ -229,10 +229,10 @@ PASEO_SERVICE_WEB_URL=http://web.my-app.localhost:6767`}</pre>
|
||||
<p className="text-white/60 leading-relaxed">Services additionally get:</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_PORT</code> — this service's assigned port
|
||||
<code className="font-mono">$PASEO_PORT</code> — this service's assigned port
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_URL</code> — this service's proxy URL
|
||||
<code className="font-mono">$PASEO_URL</code> — this service's proxy URL
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_SERVICE_<NAME>_PORT</code> /{" "}
|
||||
|
||||
@@ -26,6 +26,12 @@ export const Route = createFileRoute("/download")({
|
||||
component: Download,
|
||||
});
|
||||
|
||||
const homebrewTrigger = (
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-foreground px-4 py-1.5 text-sm font-medium text-background hover:bg-foreground/85 transition-colors">
|
||||
Homebrew
|
||||
</span>
|
||||
);
|
||||
|
||||
function Download() {
|
||||
const { version } = useRelease();
|
||||
const urls = downloadUrls(version);
|
||||
@@ -110,11 +116,7 @@ function Download() {
|
||||
<DownloadPill href={urls.macAppleSilicon} label="Apple Silicon" />
|
||||
<DownloadPill href={urls.macIntel} label="Intel" />
|
||||
<CommandDialog
|
||||
trigger={
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-foreground px-4 py-1.5 text-sm font-medium text-background hover:bg-foreground/85 transition-colors">
|
||||
Homebrew
|
||||
</span>
|
||||
}
|
||||
trigger={homebrewTrigger}
|
||||
title="Install via Homebrew"
|
||||
command="brew install --cask paseo"
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ function Privacy() {
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">What we collect</h2>
|
||||
<p>Nothing. Paseo runs on your machine and doesn't send us any data.</p>
|
||||
<p>Nothing. Paseo runs on your machine and doesn't send us any data.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
@@ -48,7 +48,8 @@ function Privacy() {
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Analytics and tracking</h2>
|
||||
<p>
|
||||
We don't use analytics, tracking pixels, cookies, or ads. The app doesn't phone home.
|
||||
We don't use analytics, tracking pixels, cookies, or ads. The app doesn't
|
||||
phone home.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -57,7 +58,7 @@ function Privacy() {
|
||||
<p>
|
||||
Paseo wraps agent providers like Claude Code, Codex, and OpenCode. Those tools
|
||||
communicate with their own APIs (Anthropic, OpenAI, etc.) using your credentials.
|
||||
Paseo doesn't manage or intercept those API calls.
|
||||
Paseo doesn't manage or intercept those API calls.
|
||||
</p>
|
||||
<p>
|
||||
If you use voice features with cloud providers (OpenAI speech), your voice data is
|
||||
@@ -66,8 +67,8 @@ function Privacy() {
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">We don't sell your data</h2>
|
||||
<p>We don't have your data to sell. Paseo is self-hosted and local-first.</p>
|
||||
<h2 className="text-xl font-medium text-white">We don't sell your data</h2>
|
||||
<p>We don't have your data to sell. Paseo is self-hosted and local-first.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user