Merge branch 'main' into merge/main-into-dev
# Conflicts: # packages/app/src/components/combined-model-selector.tsx # packages/app/src/components/message-input.tsx # packages/app/src/components/sidebar-workspace-list.tsx # packages/app/src/screens/agent/draft-agent-screen.tsx # packages/app/src/screens/settings-screen.tsx # packages/desktop/src/main.ts # packages/server/src/server/agent/provider-launch-config.ts # packages/server/src/server/session.ts # packages/server/src/server/session.workspaces.test.ts # packages/server/src/shared/messages.ts # packages/server/src/utils/checkout-git.ts
This commit is contained in:
@@ -0,0 +1,428 @@
|
|||||||
|
# Data Model
|
||||||
|
|
||||||
|
Paseo uses **file-based JSON persistence** instead of a traditional database. All data is validated at runtime with Zod schemas and written atomically (write to temp file, then rename). There are no migrations — schemas use optional fields with defaults for forward compatibility.
|
||||||
|
|
||||||
|
All server-side stores live under `$PASEO_HOME` (defaults to `~/.paseo`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory layout
|
||||||
|
|
||||||
|
```
|
||||||
|
$PASEO_HOME/
|
||||||
|
├── config.json # Daemon configuration
|
||||||
|
├── agents/
|
||||||
|
│ └── {project-dir}/
|
||||||
|
│ └── {agentId}.json # One file per agent
|
||||||
|
├── schedules/
|
||||||
|
│ └── {scheduleId}.json # One file per schedule
|
||||||
|
├── chat/
|
||||||
|
│ └── rooms.json # All rooms + messages
|
||||||
|
├── loops/
|
||||||
|
│ └── loops.json # All loop records
|
||||||
|
├── projects/
|
||||||
|
│ ├── projects.json # Project registry
|
||||||
|
│ └── workspaces.json # Workspace registry
|
||||||
|
└── push-tokens.json # Expo push notification tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Agent Record
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/agents/{project-dir}/{agentId}.json`
|
||||||
|
|
||||||
|
Each agent is stored as a separate JSON file, grouped by project directory.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | UUID, primary key |
|
||||||
|
| `provider` | `string` | Agent provider (`"claude"`, `"codex"`, `"opencode"`, etc.) |
|
||||||
|
| `cwd` | `string` | Working directory the agent operates in |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | Creation timestamp |
|
||||||
|
| `updatedAt` | `string` (ISO 8601) | Last update timestamp |
|
||||||
|
| `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp |
|
||||||
|
| `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp |
|
||||||
|
| `title` | `string?` | User-visible title |
|
||||||
|
| `labels` | `Record<string, string>` | Key-value labels (default `{}`) |
|
||||||
|
| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` |
|
||||||
|
| `lastModeId` | `string?` | Last active mode ID |
|
||||||
|
| `config` | `SerializableConfig?` | Agent session configuration (see below) |
|
||||||
|
| `runtimeInfo` | `RuntimeInfo?` | Live runtime state (see below) |
|
||||||
|
| `features` | `AgentFeature[]?` | Provider-reported features (toggles/selects) |
|
||||||
|
| `persistence` | `PersistenceHandle?` | Handle for resuming sessions |
|
||||||
|
| `requiresAttention` | `boolean?` | Whether the agent needs user attention |
|
||||||
|
| `attentionReason` | `"finished" \| "error" \| "permission"?` | Why attention is needed |
|
||||||
|
| `attentionTimestamp` | `string?` (ISO 8601) | When attention was flagged |
|
||||||
|
| `internal` | `boolean?` | Whether this is a system-internal agent (loop workers, etc.) |
|
||||||
|
| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp |
|
||||||
|
|
||||||
|
### Nested: SerializableConfig
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | `string?` | Configured title |
|
||||||
|
| `modeId` | `string?` | Configured mode |
|
||||||
|
| `model` | `string?` | Configured model |
|
||||||
|
| `thinkingOptionId` | `string?` | Thinking/reasoning level |
|
||||||
|
| `featureValues` | `Record<string, unknown>?` | Feature preference overrides |
|
||||||
|
| `extra` | `Record<string, any>?` | Provider-specific config |
|
||||||
|
| `systemPrompt` | `string?` | Custom system prompt |
|
||||||
|
| `mcpServers` | `Record<string, any>?` | MCP server configurations |
|
||||||
|
|
||||||
|
### Nested: RuntimeInfo
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `provider` | `string` | Active provider |
|
||||||
|
| `sessionId` | `string?` | Active session ID |
|
||||||
|
| `model` | `string?` | Active model |
|
||||||
|
| `thinkingOptionId` | `string?` | Active thinking option |
|
||||||
|
| `modeId` | `string?` | Active mode |
|
||||||
|
| `extra` | `Record<string, unknown>?` | Provider-specific runtime data |
|
||||||
|
|
||||||
|
### Nested: PersistenceHandle
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `provider` | `string` | Provider that owns the session |
|
||||||
|
| `sessionId` | `string` | Session ID for resumption |
|
||||||
|
| `nativeHandle` | `any?` | Provider-specific handle (Codex thread ID, Claude resume token, etc.) |
|
||||||
|
| `metadata` | `Record<string, any>?` | Extra metadata |
|
||||||
|
|
||||||
|
### Nested: AgentFeature (discriminated union on `type`)
|
||||||
|
|
||||||
|
**Toggle:**
|
||||||
|
|
||||||
|
| Field | Type |
|
||||||
|
|---|---|
|
||||||
|
| `type` | `"toggle"` |
|
||||||
|
| `id` | `string` |
|
||||||
|
| `label` | `string` |
|
||||||
|
| `description` | `string?` |
|
||||||
|
| `tooltip` | `string?` |
|
||||||
|
| `icon` | `string?` |
|
||||||
|
| `value` | `boolean` |
|
||||||
|
|
||||||
|
**Select:**
|
||||||
|
|
||||||
|
| Field | Type |
|
||||||
|
|---|---|
|
||||||
|
| `type` | `"select"` |
|
||||||
|
| `id` | `string` |
|
||||||
|
| `label` | `string` |
|
||||||
|
| `description` | `string?` |
|
||||||
|
| `tooltip` | `string?` |
|
||||||
|
| `icon` | `string?` |
|
||||||
|
| `value` | `string?` |
|
||||||
|
| `options` | `AgentSelectOption[]` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Daemon Configuration
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/config.json`
|
||||||
|
|
||||||
|
Single file, validated with `PersistedConfigSchema`.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
daemon: {
|
||||||
|
listen: "127.0.0.1:6767",
|
||||||
|
allowedHosts: true | string[],
|
||||||
|
mcp: { enabled: boolean },
|
||||||
|
cors: { allowedOrigins: string[] },
|
||||||
|
relay: { enabled: boolean, endpoint: string, publicEndpoint: string }
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
baseUrl: string
|
||||||
|
},
|
||||||
|
providers: {
|
||||||
|
openai: { apiKey: string },
|
||||||
|
local: { modelsDir: string }
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
providers: {
|
||||||
|
[provider: string]: {
|
||||||
|
command: { mode: "default" } | { mode: "append", args: string[] } | { mode: "replace", argv: string[] },
|
||||||
|
env: Record<string, string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
dictation: { enabled, stt: { provider, model, confidenceThreshold } },
|
||||||
|
voiceMode: { enabled, llm, stt, turnDetection, tts: { provider, model, voice, speakerId, speed } }
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
level, format,
|
||||||
|
console: { level, format },
|
||||||
|
file: { level, path, rotate: { maxSize, maxFiles } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All fields are optional with sensible defaults.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Schedule
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/schedules/{id}.json`
|
||||||
|
|
||||||
|
One file per schedule. ID is 8 hex characters.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | 8-char hex ID |
|
||||||
|
| `name` | `string?` | Human-readable name |
|
||||||
|
| `prompt` | `string` | The prompt to send |
|
||||||
|
| `cadence` | `ScheduleCadence` | Timing (see below) |
|
||||||
|
| `target` | `ScheduleTarget` | What to run (see below) |
|
||||||
|
| `status` | `"active" \| "paused" \| "completed"` | Current state |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | |
|
||||||
|
| `updatedAt` | `string` (ISO 8601) | |
|
||||||
|
| `nextRunAt` | `string?` (ISO 8601) | Next scheduled execution |
|
||||||
|
| `lastRunAt` | `string?` (ISO 8601) | Last execution time |
|
||||||
|
| `pausedAt` | `string?` (ISO 8601) | When paused |
|
||||||
|
| `expiresAt` | `string?` (ISO 8601) | Auto-expire time |
|
||||||
|
| `maxRuns` | `number?` | Max executions before completing |
|
||||||
|
| `runs` | `ScheduleRun[]` | Execution history |
|
||||||
|
|
||||||
|
### Nested: ScheduleCadence (discriminated union on `type`)
|
||||||
|
|
||||||
|
- `{ type: "every", everyMs: number }` — interval in milliseconds
|
||||||
|
- `{ type: "cron", expression: string }` — cron expression
|
||||||
|
|
||||||
|
### Nested: ScheduleTarget (discriminated union on `type`)
|
||||||
|
|
||||||
|
- `{ type: "agent", agentId: string }` — send to existing agent
|
||||||
|
- `{ type: "new-agent", config: { provider, cwd, modeId?, model?, thinkingOptionId?, title?, approvalPolicy?, sandboxMode?, networkAccess?, webSearch?, extra?, systemPrompt?, mcpServers? } }` — create a new agent
|
||||||
|
|
||||||
|
### Nested: ScheduleRun
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | Run ID |
|
||||||
|
| `scheduledFor` | `string` (ISO 8601) | Intended execution time |
|
||||||
|
| `startedAt` | `string` (ISO 8601) | |
|
||||||
|
| `endedAt` | `string?` (ISO 8601) | |
|
||||||
|
| `status` | `"running" \| "succeeded" \| "failed"` | |
|
||||||
|
| `agentId` | `string?` (UUID) | Agent used for this run |
|
||||||
|
| `output` | `string?` | Agent output text |
|
||||||
|
| `error` | `string?` | Error message if failed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Chat
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/chat/rooms.json`
|
||||||
|
|
||||||
|
Single file containing all rooms and messages.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rooms": [ ... ],
|
||||||
|
"messages": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ChatRoom
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` (UUID) | |
|
||||||
|
| `name` | `string` | Unique room name (case-insensitive) |
|
||||||
|
| `purpose` | `string?` | Room description |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | |
|
||||||
|
| `updatedAt` | `string` (ISO 8601) | Updated on each new message |
|
||||||
|
|
||||||
|
### ChatMessage
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` (UUID) | |
|
||||||
|
| `roomId` | `string` | FK to ChatRoom.id |
|
||||||
|
| `authorAgentId` | `string` | Agent ID of the author |
|
||||||
|
| `body` | `string` | Message text (supports `@mentions`) |
|
||||||
|
| `replyToMessageId` | `string?` | FK to another ChatMessage.id |
|
||||||
|
| `mentionAgentIds` | `string[]` | Extracted `@mention` agent IDs |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Loop
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/loops/loops.json`
|
||||||
|
|
||||||
|
Single file containing an array of all loop records.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | 8-char UUID prefix |
|
||||||
|
| `name` | `string?` | Human-readable name |
|
||||||
|
| `prompt` | `string` | Worker prompt |
|
||||||
|
| `cwd` | `string` | Working directory |
|
||||||
|
| `provider` | `string` | Default provider |
|
||||||
|
| `model` | `string?` | Default model |
|
||||||
|
| `workerProvider` | `string?` | Override provider for workers |
|
||||||
|
| `workerModel` | `string?` | Override model for workers |
|
||||||
|
| `verifierProvider` | `string?` | Override provider for verifiers |
|
||||||
|
| `verifierModel` | `string?` | Override model for verifiers |
|
||||||
|
| `verifyPrompt` | `string?` | LLM verification prompt |
|
||||||
|
| `verifyChecks` | `string[]` | Shell commands to run as checks |
|
||||||
|
| `archive` | `boolean` | Whether to archive worker agents after use |
|
||||||
|
| `sleepMs` | `number` | Delay between iterations (ms) |
|
||||||
|
| `maxIterations` | `number?` | Cap on iterations |
|
||||||
|
| `maxTimeMs` | `number?` | Total time budget (ms) |
|
||||||
|
| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | |
|
||||||
|
| `updatedAt` | `string` (ISO 8601) | |
|
||||||
|
| `startedAt` | `string` (ISO 8601) | |
|
||||||
|
| `completedAt` | `string?` (ISO 8601) | |
|
||||||
|
| `stopRequestedAt` | `string?` (ISO 8601) | |
|
||||||
|
| `iterations` | `LoopIteration[]` | |
|
||||||
|
| `logs` | `LoopLogEntry[]` | |
|
||||||
|
| `nextLogSeq` | `number` | Monotonic log sequence counter |
|
||||||
|
| `activeIteration` | `number?` | Currently executing iteration index |
|
||||||
|
| `activeWorkerAgentId` | `string?` | Currently running worker agent |
|
||||||
|
| `activeVerifierAgentId` | `string?` | Currently running verifier agent |
|
||||||
|
|
||||||
|
### Nested: LoopIteration
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `index` | `number` | 1-based iteration index |
|
||||||
|
| `workerAgentId` | `string?` | Agent ID of the worker |
|
||||||
|
| `workerStartedAt` | `string` (ISO 8601) | |
|
||||||
|
| `workerCompletedAt` | `string?` (ISO 8601) | |
|
||||||
|
| `verifierAgentId` | `string?` | Agent ID of the verifier |
|
||||||
|
| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | |
|
||||||
|
| `workerOutcome` | `"completed" \| "failed" \| "canceled"?` | |
|
||||||
|
| `failureReason` | `string?` | |
|
||||||
|
| `verifyChecks` | `LoopVerifyCheckResult[]` | Shell check results |
|
||||||
|
| `verifyPrompt` | `LoopVerifyPromptResult?` | LLM verification result |
|
||||||
|
|
||||||
|
### Nested: LoopLogEntry
|
||||||
|
|
||||||
|
| Field | Type |
|
||||||
|
|---|---|
|
||||||
|
| `seq` | `number` (monotonic) |
|
||||||
|
| `timestamp` | `string` (ISO 8601) |
|
||||||
|
| `iteration` | `number?` |
|
||||||
|
| `source` | `"loop" \| "worker" \| "verifier" \| "verify-check"` |
|
||||||
|
| `level` | `"info" \| "error"` |
|
||||||
|
| `text` | `string` |
|
||||||
|
|
||||||
|
### Nested: LoopVerifyCheckResult
|
||||||
|
|
||||||
|
| Field | Type |
|
||||||
|
|---|---|
|
||||||
|
| `command` | `string` |
|
||||||
|
| `exitCode` | `number` |
|
||||||
|
| `passed` | `boolean` |
|
||||||
|
| `stdout` | `string` |
|
||||||
|
| `stderr` | `string` |
|
||||||
|
| `startedAt` | `string` (ISO 8601) |
|
||||||
|
| `completedAt` | `string` (ISO 8601) |
|
||||||
|
|
||||||
|
### Nested: LoopVerifyPromptResult
|
||||||
|
|
||||||
|
| Field | Type |
|
||||||
|
|---|---|
|
||||||
|
| `passed` | `boolean` |
|
||||||
|
| `reason` | `string` |
|
||||||
|
| `verifierAgentId` | `string?` |
|
||||||
|
| `startedAt` | `string` (ISO 8601) |
|
||||||
|
| `completedAt` | `string` (ISO 8601) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Project Registry
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/projects/projects.json`
|
||||||
|
|
||||||
|
Array of project records.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `projectId` | `string` | Primary key |
|
||||||
|
| `rootPath` | `string` | Filesystem root of the project |
|
||||||
|
| `kind` | `"git" \| "non_git"` | |
|
||||||
|
| `displayName` | `string` | |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | |
|
||||||
|
| `updatedAt` | `string` (ISO 8601) | |
|
||||||
|
| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Workspace Registry
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/projects/workspaces.json`
|
||||||
|
|
||||||
|
Array of workspace records. A workspace is a specific working directory within a project.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `workspaceId` | `string` | Primary key |
|
||||||
|
| `projectId` | `string` | FK to Project.projectId |
|
||||||
|
| `cwd` | `string` | Filesystem path |
|
||||||
|
| `kind` | `"local_checkout" \| "worktree" \| "directory"` | |
|
||||||
|
| `displayName` | `string` | |
|
||||||
|
| `createdAt` | `string` (ISO 8601) | |
|
||||||
|
| `updatedAt` | `string` (ISO 8601) | |
|
||||||
|
| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Push Token Store
|
||||||
|
|
||||||
|
**Path:** `$PASEO_HOME/push-tokens.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tokens": ["ExponentPushToken[...]", ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Simple set of Expo push notification tokens. No schema validation — just an array of strings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client-side stores (App)
|
||||||
|
|
||||||
|
These live in React Native `AsyncStorage` or browser `IndexedDB`, not on the daemon filesystem.
|
||||||
|
|
||||||
|
### Draft Store
|
||||||
|
|
||||||
|
**AsyncStorage key:** `paseo-drafts` (version 2)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
drafts: Record<draftKey, {
|
||||||
|
input: { text: string, images: AttachmentMetadata[] },
|
||||||
|
lifecycle: "active" | "abandoned" | "sent",
|
||||||
|
updatedAt: number, // epoch ms
|
||||||
|
version: number // optimistic concurrency
|
||||||
|
}>,
|
||||||
|
createModalDraft: DraftRecord | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attachment Store (Web)
|
||||||
|
|
||||||
|
**IndexedDB database:** `paseo-attachment-bytes`, object store: `attachments`
|
||||||
|
|
||||||
|
Stores binary attachment blobs keyed by attachment ID.
|
||||||
|
|
||||||
|
### AttachmentMetadata
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | Unique attachment ID |
|
||||||
|
| `mimeType` | `string` | MIME type |
|
||||||
|
| `storageType` | `string` | Storage backend identifier |
|
||||||
|
| `storageKey` | `string` | Key within the storage backend |
|
||||||
|
| `createdAt` | `number` | Epoch ms |
|
||||||
|
| `fileName` | `string?` | Original filename |
|
||||||
|
| `byteSize` | `number?` | Size in bytes |
|
||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
HorizontalScrollProvider,
|
HorizontalScrollProvider,
|
||||||
useHorizontalScrollOptional,
|
useHorizontalScrollOptional,
|
||||||
} from "@/contexts/horizontal-scroll-context";
|
} from "@/contexts/horizontal-scroll-context";
|
||||||
import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout";
|
import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { CommandCenter } from "@/components/command-center";
|
import { CommandCenter } from "@/components/command-center";
|
||||||
import { ProjectPickerModal } from "@/components/project-picker-modal";
|
import { ProjectPickerModal } from "@/components/project-picker-modal";
|
||||||
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
|
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
|
||||||
@@ -374,7 +374,7 @@ function AppContainer({
|
|||||||
const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||||
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
||||||
|
|
||||||
const isCompactLayout = isCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
|
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import type { ReactNode } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
import { Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
||||||
import type { TextInputProps } from "react-native";
|
import type { TextInputProps } from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { getOverlayRoot, OVERLAY_Z } from "../lib/overlay-root";
|
import { getOverlayRoot, OVERLAY_Z } from "../lib/overlay-root";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
@@ -136,7 +137,7 @@ export function AdaptiveModalSheet({
|
|||||||
onFilesDropped,
|
onFilesDropped,
|
||||||
}: AdaptiveModalSheetProps) {
|
}: AdaptiveModalSheetProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const sheetRef = useRef<BottomSheetModal>(null);
|
const sheetRef = useRef<BottomSheetModal>(null);
|
||||||
const dismissingForVisibilityRef = useRef(false);
|
const dismissingForVisibilityRef = useRef(false);
|
||||||
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
|
||||||
@@ -285,7 +286,7 @@ export function AdaptiveModalSheet({
|
|||||||
*/
|
*/
|
||||||
export const AdaptiveTextInput = forwardRef<TextInput, TextInputProps>(
|
export const AdaptiveTextInput = forwardRef<TextInput, TextInputProps>(
|
||||||
function AdaptiveTextInput(props, ref) {
|
function AdaptiveTextInput(props, ref) {
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return <BottomSheetTextInput ref={ref as any} {...props} />;
|
return <BottomSheetTextInput ref={ref as any} {...props} />;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { Alert, Text, TextInput, View } from "react-native";
|
import { Alert, Text, TextInput, View } from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Link2 } from "lucide-react-native";
|
import { Link2 } from "lucide-react-native";
|
||||||
import type { HostProfile } from "@/types/host-connection";
|
import type { HostProfile } from "@/types/host-connection";
|
||||||
import { useHosts, useHostMutations } from "@/runtime/host-runtime";
|
import { useHosts, useHostMutations } from "@/runtime/host-runtime";
|
||||||
@@ -151,7 +152,7 @@ export function AddHostModal({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
const { upsertDirectConnection } = useHostMutations();
|
const { upsertDirectConnection } = useHostMutations();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
const hostInputRef = useRef<TextInput>(null);
|
const hostInputRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { shortenPath } from "@/utils/shorten-path";
|
import { shortenPath } from "@/utils/shorten-path";
|
||||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
@@ -221,7 +222,7 @@ export function AgentList({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
const actionClient = useSessionStore((state) =>
|
const actionClient = useSessionStore((state) =>
|
||||||
actionAgent?.serverId ? (state.sessions[actionAgent.serverId]?.client ?? null) : null,
|
actionAgent?.serverId ? (state.sessions[actionAgent.serverId]?.client ?? null) : null,
|
||||||
|
|||||||
@@ -962,13 +962,14 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void updatePreferences(
|
void updatePreferences(
|
||||||
mergeProviderPreferences({
|
(current) =>
|
||||||
preferences,
|
mergeProviderPreferences({
|
||||||
provider: agent.provider,
|
preferences: current,
|
||||||
updates: {
|
provider: agent.provider,
|
||||||
model: modelId,
|
updates: {
|
||||||
},
|
model: modelId,
|
||||||
}),
|
},
|
||||||
|
}),
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
console.warn("[AgentStatusBar] persist model preference failed", error);
|
console.warn("[AgentStatusBar] persist model preference failed", error);
|
||||||
});
|
});
|
||||||
@@ -978,7 +979,7 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
}}
|
}}
|
||||||
favoriteKeys={favoriteKeys}
|
favoriteKeys={favoriteKeys}
|
||||||
onToggleFavoriteModel={(provider, modelId) => {
|
onToggleFavoriteModel={(provider, modelId) => {
|
||||||
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
|
void updatePreferences((current) => toggleFavoriteModel({ preferences: current, provider, modelId })).catch((error) => {
|
||||||
console.warn("[AgentStatusBar] toggle favorite model failed", error);
|
console.warn("[AgentStatusBar] toggle favorite model failed", error);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -991,16 +992,17 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
const activeModelId = modelSelection.activeModelId;
|
const activeModelId = modelSelection.activeModelId;
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void updatePreferences(
|
void updatePreferences(
|
||||||
mergeProviderPreferences({
|
(current) =>
|
||||||
preferences,
|
mergeProviderPreferences({
|
||||||
provider: agent.provider,
|
preferences: current,
|
||||||
updates: {
|
provider: agent.provider,
|
||||||
model: activeModelId,
|
updates: {
|
||||||
thinkingByModel: {
|
model: activeModelId,
|
||||||
[activeModelId]: thinkingOptionId,
|
thinkingByModel: {
|
||||||
|
[activeModelId]: thinkingOptionId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
console.warn("[AgentStatusBar] persist thinking preference failed", error);
|
console.warn("[AgentStatusBar] persist thinking preference failed", error);
|
||||||
});
|
});
|
||||||
@@ -1014,6 +1016,20 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
void updatePreferences(
|
||||||
|
(current) =>
|
||||||
|
mergeProviderPreferences({
|
||||||
|
preferences: current,
|
||||||
|
provider: agent.provider,
|
||||||
|
updates: {
|
||||||
|
featureValues: {
|
||||||
|
[featureId]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).catch((error) => {
|
||||||
|
console.warn("[AgentStatusBar] persist feature preference failed", error);
|
||||||
|
});
|
||||||
void client.setAgentFeature(agentId, featureId, value).catch((error) => {
|
void client.setAgentFeature(agentId, featureId, value).catch((error) => {
|
||||||
console.warn("[AgentStatusBar] setAgentFeature failed", error);
|
console.warn("[AgentStatusBar] setAgentFeature failed", error);
|
||||||
});
|
});
|
||||||
@@ -1083,7 +1099,7 @@ export function DraftAgentStatusBar({
|
|||||||
onSelect={onSelectProviderAndModel}
|
onSelect={onSelectProviderAndModel}
|
||||||
favoriteKeys={favoriteKeys}
|
favoriteKeys={favoriteKeys}
|
||||||
onToggleFavorite={(provider, modelId) => {
|
onToggleFavorite={(provider, modelId) => {
|
||||||
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
|
void updatePreferences((current) => toggleFavoriteModel({ preferences: current, provider, modelId })).catch((error) => {
|
||||||
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
|
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -1129,7 +1145,7 @@ export function DraftAgentStatusBar({
|
|||||||
isModelLoading={isAllModelsLoading}
|
isModelLoading={isAllModelsLoading}
|
||||||
favoriteKeys={favoriteKeys}
|
favoriteKeys={favoriteKeys}
|
||||||
onToggleFavoriteModel={(provider, modelId) => {
|
onToggleFavoriteModel={(provider, modelId) => {
|
||||||
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
|
void updatePreferences((current) => toggleFavoriteModel({ preferences: current, provider, modelId })).catch((error) => {
|
||||||
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
|
console.warn("[DraftAgentStatusBar] toggle favorite model failed", error);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { View, Text, Pressable, Platform, ActivityIndicator } from "react-native";
|
import { View, Text, Pressable, Platform, ActivityIndicator } from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -110,7 +111,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
const viewportRef = useRef<StreamViewportHandle | null>(null);
|
const viewportRef = useRef<StreamViewportHandle | null>(null);
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const streamRenderStrategy = useMemo(
|
const streamRenderStrategy = useMemo(
|
||||||
() =>
|
() =>
|
||||||
resolveStreamRenderStrategy({
|
resolveStreamRenderStrategy({
|
||||||
@@ -713,7 +714,7 @@ function PermissionRequestCard({
|
|||||||
client: DaemonClient | null;
|
client: DaemonClient | null;
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
const { request } = permission;
|
const { request } = permission;
|
||||||
const isPlanRequest = request.kind === "plan";
|
const isPlanRequest = request.kind === "plan";
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -348,8 +349,7 @@ function ProviderSearchInput({
|
|||||||
}) {
|
}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
const isMobile =
|
const isMobile = useIsCompactFormFactor();
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
|
||||||
const InputComponent = isMobile ? BottomSheetTextInput : TextInput;
|
const InputComponent = isMobile ? BottomSheetTextInput : TextInput;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { View, Pressable, Text, ActivityIndicator, Platform } from "react-native";
|
import { View, Pressable, Text, ActivityIndicator, Platform } from "react-native";
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useShallow } from "zustand/shallow";
|
import { useShallow } from "zustand/shallow";
|
||||||
import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native";
|
import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native";
|
||||||
import Animated from "react-native-reanimated";
|
import Animated from "react-native-reanimated";
|
||||||
@@ -162,10 +163,8 @@ export function Composer({
|
|||||||
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
|
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
|
||||||
const setAgentStreamHead = useSessionStore((state) => state.setAgentStreamHead);
|
const setAgentStreamHead = useSessionStore((state) => state.setAgentStreamHead);
|
||||||
|
|
||||||
const isDesktopWebBreakpoint =
|
const isMobile = useIsCompactFormFactor();
|
||||||
Platform.OS === "web" &&
|
const isDesktopWebBreakpoint = Platform.OS === "web" && !isMobile;
|
||||||
UnistylesRuntime.breakpoint !== "xs" &&
|
|
||||||
UnistylesRuntime.breakpoint !== "sm";
|
|
||||||
const messagePlaceholder = isDesktopWebBreakpoint
|
const messagePlaceholder = isDesktopWebBreakpoint
|
||||||
? DESKTOP_MESSAGE_PLACEHOLDER
|
? DESKTOP_MESSAGE_PLACEHOLDER
|
||||||
: MOBILE_MESSAGE_PLACEHOLDER;
|
: MOBILE_MESSAGE_PLACEHOLDER;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
type ExplorerTab,
|
type ExplorerTab,
|
||||||
} from "@/stores/panel-store";
|
} from "@/stores/panel-store";
|
||||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
import { HEADER_INNER_HEIGHT, isCompactFormFactor } from "@/constants/layout";
|
import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { GitDiffPane } from "./git-diff-pane";
|
import { GitDiffPane } from "./git-diff-pane";
|
||||||
import { FileExplorerPane } from "./file-explorer-pane";
|
import { FileExplorerPane } from "./file-explorer-pane";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
@@ -48,7 +48,7 @@ export function ExplorerSidebar({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isScreenFocused = useIsFocused();
|
const isScreenFocused = useIsFocused();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Gesture } from "react-native-gesture-handler";
|
import { Gesture } from "react-native-gesture-handler";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
cancelAnimation,
|
cancelAnimation,
|
||||||
Easing,
|
Easing,
|
||||||
@@ -89,7 +90,7 @@ export function FileExplorerPane({
|
|||||||
onOpenFile,
|
onOpenFile,
|
||||||
}: FileExplorerPaneProps) {
|
}: FileExplorerPaneProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
||||||
|
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
View,
|
View,
|
||||||
Platform,
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Fonts } from "@/constants/theme";
|
import { Fonts } from "@/constants/theme";
|
||||||
import { useSessionStore, type ExplorerFile } from "@/stores/session-store";
|
import { useSessionStore, type ExplorerFile } from "@/stores/session-store";
|
||||||
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
|
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
|
||||||
@@ -241,7 +242,7 @@ export function FilePane({
|
|||||||
workspaceRoot: string;
|
workspaceRoot: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
}) {
|
}) {
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
||||||
|
|
||||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
TextStyle,
|
TextStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import {
|
import {
|
||||||
AlignJustify,
|
AlignJustify,
|
||||||
Archive,
|
Archive,
|
||||||
@@ -607,7 +608,7 @@ type DiffFlatItem =
|
|||||||
|
|
||||||
export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDiffPaneProps) {
|
export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDiffPaneProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
||||||
const canUseSplitLayout = Platform.OS === "web" && !isMobile;
|
const canUseSplitLayout = Platform.OS === "web" && !isMobile;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PanelLeft } from "lucide-react-native";
|
|||||||
import { ScreenHeader } from "./screen-header";
|
import { ScreenHeader } from "./screen-header";
|
||||||
import { HeaderToggleButton } from "./header-toggle-button";
|
import { HeaderToggleButton } from "./header-toggle-button";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { isCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
|
|
||||||
interface MenuHeaderProps {
|
interface MenuHeaderProps {
|
||||||
@@ -44,7 +44,7 @@ export function SidebarMenuToggle({
|
|||||||
nativeID = "menu-button",
|
nativeID = "menu-button",
|
||||||
}: SidebarMenuToggleProps = {}) {
|
}: SidebarMenuToggleProps = {}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||||
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
|
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
HEADER_INNER_HEIGHT,
|
HEADER_INNER_HEIGHT,
|
||||||
HEADER_INNER_HEIGHT_MOBILE,
|
HEADER_INNER_HEIGHT_MOBILE,
|
||||||
HEADER_TOP_PADDING_MOBILE,
|
HEADER_TOP_PADDING_MOBILE,
|
||||||
isCompactFormFactor,
|
useIsCompactFormFactor,
|
||||||
} from "@/constants/layout";
|
} from "@/constants/layout";
|
||||||
import { useWindowControlsPadding } from "@/utils/desktop-window";
|
import { useWindowControlsPadding } from "@/utils/desktop-window";
|
||||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||||
@@ -26,7 +26,7 @@ interface ScreenHeaderProps {
|
|||||||
export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: ScreenHeaderProps) {
|
export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: ScreenHeaderProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const padding = useWindowControlsPadding("header");
|
const padding = useWindowControlsPadding("header");
|
||||||
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
||||||
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import { formatConnectionStatus } from "@/utils/daemons";
|
|||||||
import {
|
import {
|
||||||
HEADER_INNER_HEIGHT,
|
HEADER_INNER_HEIGHT,
|
||||||
HEADER_INNER_HEIGHT_MOBILE,
|
HEADER_INNER_HEIGHT_MOBILE,
|
||||||
isCompactFormFactor,
|
useIsCompactFormFactor,
|
||||||
} from "@/constants/layout";
|
} from "@/constants/layout";
|
||||||
import {
|
import {
|
||||||
buildHostSessionsRoute,
|
buildHostSessionsRoute,
|
||||||
@@ -118,7 +118,7 @@ export const LeftSidebar = memo(function LeftSidebar({
|
|||||||
|
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isCompactLayout = isCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||||
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ import {
|
|||||||
Scissors,
|
Scissors,
|
||||||
MicVocal,
|
MicVocal,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
cancelAnimation,
|
cancelAnimation,
|
||||||
@@ -1822,7 +1823,7 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
// Check if we're on mobile (use bottom sheet) or desktop (inline expand)
|
// Check if we're on mobile (use bottom sheet) or desktop (inline expand)
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
const effectiveDetail = useMemo<ToolCallDetail | undefined>(() => {
|
const effectiveDetail = useMemo<ToolCallDetail | undefined>(() => {
|
||||||
if (detail) {
|
if (detail) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Alert, Text, View } from "react-native";
|
import { Alert, Text, View } from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Link } from "lucide-react-native";
|
import { Link } from "lucide-react-native";
|
||||||
import type { HostProfile } from "@/types/host-connection";
|
import type { HostProfile } from "@/types/host-connection";
|
||||||
import { useHosts, useHostMutations } from "@/runtime/host-runtime";
|
import { useHosts, useHostMutations } from "@/runtime/host-runtime";
|
||||||
@@ -66,7 +67,7 @@ export function PairLinkModal({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
const { upsertConnectionFromOfferUrl: upsertDaemonFromOfferUrl } = useHostMutations();
|
const { upsertConnectionFromOfferUrl: upsertDaemonFromOfferUrl } = useHostMutations();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
const [offerUrl, setOfferUrl] = useState("");
|
const [offerUrl, setOfferUrl] = useState("");
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { View, Text, TextInput, Pressable, ActivityIndicator, Platform } from "react-native";
|
import { View, Text, TextInput, Pressable, ActivityIndicator, Platform } from "react-native";
|
||||||
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Check, CircleHelp, X } from "lucide-react-native";
|
import { Check, CircleHelp, X } from "lucide-react-native";
|
||||||
import type { PendingPermission } from "@/types/shared";
|
import type { PendingPermission } from "@/types/shared";
|
||||||
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
||||||
@@ -63,7 +64,7 @@ const IS_WEB = Platform.OS === "web";
|
|||||||
|
|
||||||
export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) {
|
export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const questions = parseQuestions(permission.request.input);
|
const questions = parseQuestions(permission.request.input);
|
||||||
|
|
||||||
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
|
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import { NestableScrollContainer } from "react-native-draggable-flatlist";
|
|||||||
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list";
|
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list";
|
||||||
import type { DraggableListDragHandleProps } from "./draggable-list.types";
|
import type { DraggableListDragHandleProps } from "./draggable-list.types";
|
||||||
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
|
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
|
||||||
import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout";
|
import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { projectIconQueryKey } from "@/hooks/use-project-icon-query";
|
import { projectIconQueryKey } from "@/hooks/use-project-icon-query";
|
||||||
import { buildHostNewWorkspaceRoute, parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes";
|
import { buildHostNewWorkspaceRoute, parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
@@ -763,7 +763,7 @@ function ProjectHeaderRow({
|
|||||||
}: ProjectHeaderRowProps) {
|
}: ProjectHeaderRowProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobileBreakpoint = isCompactFormFactor();
|
const isMobileBreakpoint = useIsCompactFormFactor();
|
||||||
const handleBeginWorkspaceSetup = useCallback(() => {
|
const handleBeginWorkspaceSetup = useCallback(() => {
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
return;
|
return;
|
||||||
@@ -771,6 +771,8 @@ function ProjectHeaderRow({
|
|||||||
router.navigate(buildHostNewWorkspaceRoute(serverId, project.iconWorkingDir, { displayName }) as any);
|
router.navigate(buildHostNewWorkspaceRoute(serverId, project.iconWorkingDir, { displayName }) as any);
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
}, [displayName, onWorkspacePress, project.iconWorkingDir, serverId]);
|
}, [displayName, onWorkspacePress, project.iconWorkingDir, serverId]);
|
||||||
|
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
useKeyboardActionHandler({
|
useKeyboardActionHandler({
|
||||||
handlerId: `worktree-new-${project.projectKey}`,
|
handlerId: `worktree-new-${project.projectKey}`,
|
||||||
@@ -1850,7 +1852,7 @@ export function SidebarWorkspaceList({
|
|||||||
listFooterComponent,
|
listFooterComponent,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
}: SidebarWorkspaceListProps) {
|
}: SidebarWorkspaceListProps) {
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const isNative = Platform.OS !== "web";
|
const isNative = Platform.OS !== "web";
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection();
|
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { toXtermTheme } from "@/utils/to-xterm-theme";
|
import { toXtermTheme } from "@/utils/to-xterm-theme";
|
||||||
import TerminalEmulator, { type TerminalEmulatorHandle } from "./terminal-emulator";
|
import TerminalEmulator, { type TerminalEmulatorHandle } from "./terminal-emulator";
|
||||||
import { isCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
|
|
||||||
interface TerminalPaneProps {
|
interface TerminalPaneProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -92,7 +92,7 @@ export function TerminalPane({
|
|||||||
const isAppVisible = useAppVisible();
|
const isAppVisible = useAppVisible();
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]);
|
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]);
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const openAgentList = usePanelStore((state) => state.openAgentList);
|
const openAgentList = usePanelStore((state) => state.openAgentList);
|
||||||
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Animated, Easing, Platform, Text, ToastAndroid, View } from "react-native";
|
import { Animated, Easing, Platform, Text, ToastAndroid, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { AlertTriangle, CheckCircle2 } from "lucide-react-native";
|
import { AlertTriangle, CheckCircle2 } from "lucide-react-native";
|
||||||
import { getOverlayRoot, OVERLAY_Z } from "@/lib/overlay-root";
|
import { getOverlayRoot, OVERLAY_Z } from "@/lib/overlay-root";
|
||||||
import {
|
import {
|
||||||
@@ -181,7 +182,7 @@ export function ToastViewport({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const headerHeight = isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT;
|
const headerHeight = isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT;
|
||||||
const headerTopPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
const headerTopPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
||||||
const topOffset =
|
const topOffset =
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
StatusBar,
|
StatusBar,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
@@ -261,7 +262,7 @@ export function Combobox({
|
|||||||
anchorRef,
|
anchorRef,
|
||||||
children,
|
children,
|
||||||
}: ComboboxProps): ReactElement {
|
}: ComboboxProps): ReactElement {
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
|
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
|
||||||
const isDesktopAboveSearch =
|
const isDesktopAboveSearch =
|
||||||
!isMobile && Platform.OS === "web" && effectiveOptionsPosition === "above-search";
|
!isMobile && Platform.OS === "web" && effectiveOptionsPosition === "above-search";
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Check, CheckCircle } from "lucide-react-native";
|
import { Check, CheckCircle } from "lucide-react-native";
|
||||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
@@ -347,7 +348,7 @@ export function ContextMenuContent({
|
|||||||
testID?: string;
|
testID?: string;
|
||||||
}>): ReactElement | null {
|
}>): ReactElement | null {
|
||||||
const context = useContextMenuContext("ContextMenuContent");
|
const context = useContextMenuContext("ContextMenuContent");
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const useMobileSheet = isMobile && mobileMode === "sheet";
|
const useMobileSheet = isMobile && mobileMode === "sheet";
|
||||||
const { open, setOpen, triggerRef, anchorRect } = context;
|
const { open, setOpen, triggerRef, anchorRect } = context;
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { UnistylesRuntime } from "react-native-unistyles";
|
import { useUnistyles } from "react-native-unistyles";
|
||||||
import { isElectronRuntime, isElectronRuntimeMac } from "@/desktop/host";
|
import { isElectronRuntime, isElectronRuntimeMac } from "@/desktop/host";
|
||||||
|
|
||||||
export const FOOTER_HEIGHT = 75;
|
export const FOOTER_HEIGHT = 75;
|
||||||
@@ -62,16 +62,13 @@ export function getIsElectronRuntime(): boolean {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCompactFormFactor(): boolean {
|
/**
|
||||||
return UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
* Reactive hook — re-renders the component when the breakpoint changes.
|
||||||
}
|
* Always use this instead of reading UnistylesRuntime.breakpoint directly.
|
||||||
|
*/
|
||||||
export function isDesktopFormFactor(): boolean {
|
export function useIsCompactFormFactor(): boolean {
|
||||||
return !isCompactFormFactor();
|
const { rt } = useUnistyles();
|
||||||
}
|
return rt.breakpoint === "xs" || rt.breakpoint === "sm";
|
||||||
|
|
||||||
export function isTouchDesktopFormFactor(): boolean {
|
|
||||||
return Platform.OS !== "web" && isDesktopFormFactor();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SplitContainer relies on dnd-kit and DOM-backed accessibility helpers.
|
// SplitContainer relies on dnd-kit and DOM-backed accessibility helpers.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createContext, useContext, useEffect, useRef, type ReactNode } from "re
|
|||||||
import { useWindowDimensions } from "react-native";
|
import { useWindowDimensions } from "react-native";
|
||||||
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
|
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
|
||||||
import { type GestureType } from "react-native-gesture-handler";
|
import { type GestureType } from "react-native-gesture-handler";
|
||||||
import { isCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import {
|
import {
|
||||||
getRightSidebarAnimationTargets,
|
getRightSidebarAnimationTargets,
|
||||||
@@ -29,7 +29,7 @@ const ExplorerSidebarAnimationContext = createContext<ExplorerSidebarAnimationCo
|
|||||||
|
|
||||||
export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) {
|
export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) {
|
||||||
const { width: windowWidth } = useWindowDimensions();
|
const { width: windowWidth } = useWindowDimensions();
|
||||||
const isCompactLayout = isCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { useWindowDimensions } from "react-native";
|
import { useWindowDimensions } from "react-native";
|
||||||
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
|
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
|
||||||
import { type GestureType } from "react-native-gesture-handler";
|
import { type GestureType } from "react-native-gesture-handler";
|
||||||
import { isCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import {
|
import {
|
||||||
getLeftSidebarAnimationTargets,
|
getLeftSidebarAnimationTargets,
|
||||||
@@ -36,7 +36,7 @@ const SidebarAnimationContext = createContext<SidebarAnimationContextValue | nul
|
|||||||
|
|
||||||
export function SidebarAnimationProvider({ children }: { children: ReactNode }) {
|
export function SidebarAnimationProvider({ children }: { children: ReactNode }) {
|
||||||
const { width: windowWidth } = useWindowDimensions();
|
const { width: windowWidth } = useWindowDimensions();
|
||||||
const isCompactLayout = isCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { invokeDesktopCommand } from "@/desktop/electron/invoke";
|
|||||||
|
|
||||||
export interface DesktopAppUpdateCheckResult {
|
export interface DesktopAppUpdateCheckResult {
|
||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
|
readyToInstall: boolean;
|
||||||
currentVersion: string | null;
|
currentVersion: string | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
body: string | null;
|
body: string | null;
|
||||||
@@ -76,6 +77,7 @@ export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResu
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
hasUpdate: result.hasUpdate === true,
|
hasUpdate: result.hasUpdate === true,
|
||||||
|
readyToInstall: result.readyToInstall === true,
|
||||||
currentVersion: toStringOrNull(result.currentVersion),
|
currentVersion: toStringOrNull(result.currentVersion),
|
||||||
latestVersion: toStringOrNull(result.latestVersion),
|
latestVersion: toStringOrNull(result.latestVersion),
|
||||||
body: toStringOrNull(result.body),
|
body: toStringOrNull(result.body),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function UpdateBanner() {
|
|||||||
|
|
||||||
function getSubtitle(): string {
|
function getSubtitle(): string {
|
||||||
if (isInstalled) return "Restart to use the new version.";
|
if (isInstalled) return "Restart to use the new version.";
|
||||||
if (isInstalling) return "Downloading and installing...";
|
if (isInstalling) return "Installing and restarting...";
|
||||||
if (isError) return errorMessage ?? "Something went wrong.";
|
if (isError) return errorMessage ?? "Something went wrong.";
|
||||||
return `${availableUpdate?.latestVersion ? `v${availableUpdate.latestVersion.replace(/^v/i, "")} is ready` : "A new version is ready"} to install.`;
|
return `${availableUpdate?.latestVersion ? `v${availableUpdate.latestVersion.replace(/^v/i, "")} is ready` : "A new version is ready"} to install.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
checkDesktopAppUpdate,
|
checkDesktopAppUpdate,
|
||||||
formatVersionWithPrefix,
|
formatVersionWithPrefix,
|
||||||
@@ -11,12 +11,15 @@ import {
|
|||||||
export type DesktopAppUpdateStatus =
|
export type DesktopAppUpdateStatus =
|
||||||
| "idle"
|
| "idle"
|
||||||
| "checking"
|
| "checking"
|
||||||
|
| "pending"
|
||||||
| "up-to-date"
|
| "up-to-date"
|
||||||
| "available"
|
| "available"
|
||||||
| "installing"
|
| "installing"
|
||||||
| "installed"
|
| "installed"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
|
const PENDING_RECHECK_MS = 10_000;
|
||||||
|
|
||||||
export interface UseDesktopAppUpdaterReturn {
|
export interface UseDesktopAppUpdaterReturn {
|
||||||
isDesktopApp: boolean;
|
isDesktopApp: boolean;
|
||||||
status: DesktopAppUpdateStatus;
|
status: DesktopAppUpdateStatus;
|
||||||
@@ -56,11 +59,15 @@ function formatStatusText(input: {
|
|||||||
return "App is up to date.";
|
return "App is up to date.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === "pending") {
|
||||||
|
return "We'll let you know when the update is ready.";
|
||||||
|
}
|
||||||
|
|
||||||
if (status === "available") {
|
if (status === "available") {
|
||||||
if (availableUpdate?.latestVersion) {
|
if (availableUpdate?.latestVersion) {
|
||||||
return `Update available: ${formatVersionWithPrefix(availableUpdate.latestVersion)}`;
|
return `Update ready: ${formatVersionWithPrefix(availableUpdate.latestVersion)}`;
|
||||||
}
|
}
|
||||||
return "An app update is available.";
|
return "An app update is ready to install.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "installed") {
|
if (status === "installed") {
|
||||||
@@ -106,9 +113,12 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|||||||
setInstallMessage(null);
|
setInstallMessage(null);
|
||||||
setLastCheckedAt(Date.now());
|
setLastCheckedAt(Date.now());
|
||||||
|
|
||||||
if (result.hasUpdate) {
|
if (result.readyToInstall) {
|
||||||
setAvailableUpdate(result);
|
setAvailableUpdate(result);
|
||||||
setStatus("available");
|
setStatus("available");
|
||||||
|
} else if (result.hasUpdate) {
|
||||||
|
setAvailableUpdate(null);
|
||||||
|
setStatus("pending");
|
||||||
} else {
|
} else {
|
||||||
setAvailableUpdate(null);
|
setAvailableUpdate(null);
|
||||||
setStatus("up-to-date");
|
setStatus("up-to-date");
|
||||||
@@ -133,6 +143,20 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|||||||
[isDesktopApp],
|
[isDesktopApp],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesktopApp || status !== "pending") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
void checkForUpdates({ silent: true });
|
||||||
|
}, PENDING_RECHECK_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [checkForUpdates, isDesktopApp, status]);
|
||||||
|
|
||||||
const installUpdate = useCallback(async () => {
|
const installUpdate = useCallback(async () => {
|
||||||
if (!isDesktopApp) {
|
if (!isDesktopApp) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { resolveFeatureValues } from "./feature-preferences";
|
||||||
|
|
||||||
|
describe("feature-preferences", () => {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
type: "toggle" as const,
|
||||||
|
id: "fast_mode",
|
||||||
|
label: "Fast",
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "toggle" as const,
|
||||||
|
id: "plan_mode",
|
||||||
|
label: "Plan",
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it("restores persisted values for available features", () => {
|
||||||
|
expect(
|
||||||
|
resolveFeatureValues({
|
||||||
|
features,
|
||||||
|
persistedFeatureValues: {
|
||||||
|
fast_mode: true,
|
||||||
|
unknown_feature: true,
|
||||||
|
},
|
||||||
|
localFeatureValues: {},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
fast_mode: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers local values over persisted values", () => {
|
||||||
|
expect(
|
||||||
|
resolveFeatureValues({
|
||||||
|
features,
|
||||||
|
persistedFeatureValues: {
|
||||||
|
fast_mode: true,
|
||||||
|
plan_mode: false,
|
||||||
|
},
|
||||||
|
localFeatureValues: {
|
||||||
|
fast_mode: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
fast_mode: false,
|
||||||
|
plan_mode: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { AgentFeature } from "@server/server/agent/agent-sdk-types";
|
||||||
|
|
||||||
|
export function pruneFeatureValues(
|
||||||
|
featureValues: Record<string, unknown>,
|
||||||
|
features: AgentFeature[],
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const allowedFeatureIds = new Set(features.map((feature) => feature.id));
|
||||||
|
let changed = false;
|
||||||
|
const next: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const [featureId, value] of Object.entries(featureValues)) {
|
||||||
|
if (!allowedFeatureIds.has(featureId)) {
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next[featureId] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? next : featureValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyFeatureValues(
|
||||||
|
features: AgentFeature[],
|
||||||
|
featureValues: Record<string, unknown>,
|
||||||
|
): AgentFeature[] {
|
||||||
|
if (Object.keys(featureValues).length === 0) {
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
return features.map((feature) => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(featureValues, feature.id)) {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
value: featureValues[feature.id],
|
||||||
|
} as AgentFeature;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFeatureValues(args: {
|
||||||
|
features: AgentFeature[];
|
||||||
|
persistedFeatureValues: Record<string, unknown>;
|
||||||
|
localFeatureValues: Record<string, unknown>;
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
const next: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const feature of args.features) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(args.localFeatureValues, feature.id)) {
|
||||||
|
next[feature.id] = args.localFeatureValues[feature.id];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(args.persistedFeatureValues, feature.id)) {
|
||||||
|
next[feature.id] = args.persistedFeatureValues[feature.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
@@ -649,30 +649,29 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
const persistFormPreferences = useCallback(async () => {
|
const persistFormPreferences = useCallback(async () => {
|
||||||
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
||||||
const modelId = resolvedModel?.id ?? formState.model;
|
const modelId = resolvedModel?.id ?? formState.model;
|
||||||
const nextPreferences = mergeProviderPreferences({
|
await updatePreferences((current) =>
|
||||||
preferences: preferences ?? {},
|
mergeProviderPreferences({
|
||||||
provider: formState.provider,
|
preferences: current,
|
||||||
updates: {
|
provider: formState.provider,
|
||||||
model: modelId || undefined,
|
updates: {
|
||||||
mode: formState.modeId || undefined,
|
model: modelId || undefined,
|
||||||
...(modelId && formState.thinkingOptionId
|
mode: formState.modeId || undefined,
|
||||||
? {
|
...(modelId && formState.thinkingOptionId
|
||||||
thinkingByModel: {
|
? {
|
||||||
[modelId]: formState.thinkingOptionId,
|
thinkingByModel: {
|
||||||
},
|
[modelId]: formState.thinkingOptionId,
|
||||||
}
|
},
|
||||||
: {}),
|
}
|
||||||
} satisfies Partial<ProviderPreferences>,
|
: {}),
|
||||||
});
|
} satisfies Partial<ProviderPreferences>,
|
||||||
|
}),
|
||||||
await updatePreferences(nextPreferences);
|
);
|
||||||
}, [
|
}, [
|
||||||
availableModels,
|
availableModels,
|
||||||
formState.model,
|
formState.model,
|
||||||
formState.modeId,
|
formState.modeId,
|
||||||
formState.provider,
|
formState.provider,
|
||||||
formState.thinkingOptionId,
|
formState.thinkingOptionId,
|
||||||
preferences?.providerPreferences,
|
|
||||||
updatePreferences,
|
updatePreferences,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useEffect, useId, useMemo } from "react";
|
import { useCallback, useEffect, useId, useMemo } from "react";
|
||||||
import { UnistylesRuntime } from "react-native-unistyles";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
|
import type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
|
||||||
@@ -60,7 +60,7 @@ export function useCheckoutDiffQuery({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const client = useHostRuntimeClient(serverId);
|
const client = useHostRuntimeClient(serverId);
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
const explorerTab = usePanelStore((state) => state.explorerTab);
|
const explorerTab = usePanelStore((state) => state.explorerTab);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { UnistylesRuntime } from "react-native-unistyles";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import type { CheckoutStatusResponse } from "@server/shared/messages";
|
import type { CheckoutStatusResponse } from "@server/shared/messages";
|
||||||
@@ -32,7 +32,7 @@ function fetchCheckoutStatus(
|
|||||||
export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
|
export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
|
||||||
const client = useHostRuntimeClient(serverId);
|
const client = useHostRuntimeClient(serverId);
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
const explorerTab = usePanelStore((state) => state.explorerTab);
|
const explorerTab = usePanelStore((state) => state.explorerTab);
|
||||||
|
|||||||
@@ -1,50 +1,16 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type {
|
import type {
|
||||||
AgentFeature,
|
|
||||||
AgentProvider,
|
AgentProvider,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
} from "@server/server/agent/agent-sdk-types";
|
} from "@server/server/agent/agent-sdk-types";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
|
import { mergeProviderPreferences, useFormPreferences } from "./use-form-preferences";
|
||||||
function pruneFeatureValues(
|
import {
|
||||||
featureValues: Record<string, unknown>,
|
applyFeatureValues,
|
||||||
features: AgentFeature[],
|
pruneFeatureValues,
|
||||||
): Record<string, unknown> {
|
resolveFeatureValues,
|
||||||
const allowedFeatureIds = new Set(features.map((feature) => feature.id));
|
} from "./feature-preferences";
|
||||||
let changed = false;
|
|
||||||
const next: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
for (const [featureId, value] of Object.entries(featureValues)) {
|
|
||||||
if (!allowedFeatureIds.has(featureId)) {
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
next[featureId] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed ? next : featureValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFeatureValues(
|
|
||||||
features: AgentFeature[],
|
|
||||||
featureValues: Record<string, unknown>,
|
|
||||||
): AgentFeature[] {
|
|
||||||
if (Object.keys(featureValues).length === 0) {
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
return features.map((feature) => {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(featureValues, feature.id)) {
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...feature,
|
|
||||||
value: featureValues[feature.id],
|
|
||||||
} as AgentFeature;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type DraftFeatureConfig = Pick<
|
type DraftFeatureConfig = Pick<
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
@@ -60,10 +26,15 @@ export function useDraftAgentFeatures(input: {
|
|||||||
thinkingOptionId: string | null | undefined;
|
thinkingOptionId: string | null | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { serverId, provider, cwd, modeId, modelId, thinkingOptionId } = input;
|
const { serverId, provider, cwd, modeId, modelId, thinkingOptionId } = input;
|
||||||
const [featureValues, setFeatureValues] = useState<Record<string, unknown>>({});
|
const [localFeatureValues, setLocalFeatureValues] = useState<Record<string, unknown>>({});
|
||||||
const client = useHostRuntimeClient(serverId ?? "");
|
const client = useHostRuntimeClient(serverId ?? "");
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
||||||
|
const { preferences, updatePreferences } = useFormPreferences();
|
||||||
const normalizedCwd = cwd?.trim() || "";
|
const normalizedCwd = cwd?.trim() || "";
|
||||||
|
const persistedFeatureValues = useMemo(
|
||||||
|
() => preferences.providerPreferences?.[provider]?.featureValues ?? {},
|
||||||
|
[preferences.providerPreferences, provider],
|
||||||
|
);
|
||||||
|
|
||||||
const draftConfig = useMemo<DraftFeatureConfig | null>(() => {
|
const draftConfig = useMemo<DraftFeatureConfig | null>(() => {
|
||||||
if (!normalizedCwd) {
|
if (!normalizedCwd) {
|
||||||
@@ -102,28 +73,56 @@ export function useDraftAgentFeatures(input: {
|
|||||||
return payload.features ?? [];
|
return payload.features ?? [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const availableFeatures = featuresQuery.data ?? [];
|
||||||
|
const featureValues = useMemo(
|
||||||
|
() =>
|
||||||
|
resolveFeatureValues({
|
||||||
|
features: availableFeatures,
|
||||||
|
persistedFeatureValues,
|
||||||
|
localFeatureValues,
|
||||||
|
}),
|
||||||
|
[availableFeatures, localFeatureValues, persistedFeatureValues],
|
||||||
|
);
|
||||||
|
|
||||||
const features = useMemo(() => {
|
const features = useMemo(() => {
|
||||||
return applyFeatureValues(featuresQuery.data ?? [], featureValues);
|
return applyFeatureValues(availableFeatures, featureValues);
|
||||||
}, [featureValues, featuresQuery.data]);
|
}, [availableFeatures, featureValues]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const next = pruneFeatureValues(featureValues, features);
|
setLocalFeatureValues({});
|
||||||
if (next !== featureValues) {
|
}, [provider]);
|
||||||
setFeatureValues(next);
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = pruneFeatureValues(localFeatureValues, availableFeatures);
|
||||||
|
if (next !== localFeatureValues) {
|
||||||
|
setLocalFeatureValues(next);
|
||||||
}
|
}
|
||||||
}, [featureValues, features]);
|
}, [availableFeatures, localFeatureValues]);
|
||||||
|
|
||||||
const effectiveFeatureValues = Object.keys(featureValues).length > 0 ? featureValues : undefined;
|
const effectiveFeatureValues = Object.keys(featureValues).length > 0 ? featureValues : undefined;
|
||||||
const setFeatureValue = useCallback((featureId: string, value: unknown) => {
|
const setFeatureValue = useCallback((featureId: string, value: unknown) => {
|
||||||
setFeatureValues((current) => {
|
setLocalFeatureValues((current) => {
|
||||||
if (Object.is(current[featureId], value)) {
|
if (Object.is(current[featureId], value)) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...current, [featureId]: value };
|
return { ...current, [featureId]: value };
|
||||||
});
|
});
|
||||||
}, []);
|
void updatePreferences(
|
||||||
|
(current) =>
|
||||||
|
mergeProviderPreferences({
|
||||||
|
preferences: current,
|
||||||
|
provider,
|
||||||
|
updates: {
|
||||||
|
featureValues: {
|
||||||
|
[featureId]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).catch((error) => {
|
||||||
|
console.warn("[useDraftAgentFeatures] persist feature preference failed", error);
|
||||||
|
});
|
||||||
|
}, [provider, updatePreferences]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
features,
|
features,
|
||||||
|
|||||||
@@ -59,6 +59,41 @@ describe("mergeProviderPreferences", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("merges feature values without dropping existing entries", () => {
|
||||||
|
expect(
|
||||||
|
mergeProviderPreferences({
|
||||||
|
preferences: {
|
||||||
|
provider: "codex",
|
||||||
|
providerPreferences: {
|
||||||
|
codex: {
|
||||||
|
model: "gpt-5.4",
|
||||||
|
featureValues: {
|
||||||
|
fast_mode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: "codex",
|
||||||
|
updates: {
|
||||||
|
featureValues: {
|
||||||
|
plan_mode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
provider: "codex",
|
||||||
|
providerPreferences: {
|
||||||
|
codex: {
|
||||||
|
model: "gpt-5.4",
|
||||||
|
featureValues: {
|
||||||
|
fast_mode: true,
|
||||||
|
plan_mode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("favorite model preferences", () => {
|
describe("favorite model preferences", () => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const providerPreferencesSchema = z.object({
|
|||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
mode: z.string().optional(),
|
mode: z.string().optional(),
|
||||||
thinkingByModel: z.record(z.string()).optional(),
|
thinkingByModel: z.record(z.string()).optional(),
|
||||||
|
featureValues: z.record(z.unknown()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const formPreferencesSchema = z.object({
|
const formPreferencesSchema = z.object({
|
||||||
@@ -53,7 +54,9 @@ async function loadFormPreferences(): Promise<FormPreferences> {
|
|||||||
export interface UseFormPreferencesReturn {
|
export interface UseFormPreferencesReturn {
|
||||||
preferences: FormPreferences;
|
preferences: FormPreferences;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
updatePreferences: (updates: Partial<FormPreferences>) => Promise<void>;
|
updatePreferences: (
|
||||||
|
updates: Partial<FormPreferences> | ((current: FormPreferences) => FormPreferences),
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeProviderPreferences(args: {
|
export function mergeProviderPreferences(args: {
|
||||||
@@ -71,6 +74,13 @@ export function mergeProviderPreferences(args: {
|
|||||||
...existing.thinkingByModel,
|
...existing.thinkingByModel,
|
||||||
...updates.thinkingByModel,
|
...updates.thinkingByModel,
|
||||||
};
|
};
|
||||||
|
const nextFeatureValues =
|
||||||
|
updates.featureValues === undefined
|
||||||
|
? existing.featureValues
|
||||||
|
: {
|
||||||
|
...existing.featureValues,
|
||||||
|
...updates.featureValues,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...preferences,
|
...preferences,
|
||||||
@@ -81,6 +91,7 @@ export function mergeProviderPreferences(args: {
|
|||||||
...existing,
|
...existing,
|
||||||
...updates,
|
...updates,
|
||||||
...(nextThinkingByModel ? { thinkingByModel: nextThinkingByModel } : {}),
|
...(nextThinkingByModel ? { thinkingByModel: nextThinkingByModel } : {}),
|
||||||
|
...(nextFeatureValues ? { featureValues: nextFeatureValues } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -133,11 +144,12 @@ export function useFormPreferences(): UseFormPreferencesReturn {
|
|||||||
const preferences = data ?? DEFAULT_FORM_PREFERENCES;
|
const preferences = data ?? DEFAULT_FORM_PREFERENCES;
|
||||||
|
|
||||||
const updatePreferences = useCallback(
|
const updatePreferences = useCallback(
|
||||||
async (updates: Partial<FormPreferences>) => {
|
async (updates: Partial<FormPreferences> | ((current: FormPreferences) => FormPreferences)) => {
|
||||||
const prev =
|
const prev =
|
||||||
queryClient.getQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY) ??
|
queryClient.getQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY) ??
|
||||||
DEFAULT_FORM_PREFERENCES;
|
DEFAULT_FORM_PREFERENCES;
|
||||||
const next = { ...prev, ...updates };
|
const next =
|
||||||
|
typeof updates === "function" ? updates(prev) : { ...prev, ...updates };
|
||||||
queryClient.setQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY, next);
|
queryClient.setQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY, next);
|
||||||
await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next));
|
await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
} from "@/runtime/host-runtime";
|
} from "@/runtime/host-runtime";
|
||||||
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
|
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store";
|
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store";
|
||||||
import { MAX_CONTENT_WIDTH, isCompactFormFactor } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { WelcomeScreen } from "@/components/welcome-screen";
|
import { WelcomeScreen } from "@/components/welcome-screen";
|
||||||
import type { Agent } from "@/contexts/session-context";
|
import type { Agent } from "@/contexts/session-context";
|
||||||
import { encodeImages } from "@/utils/encode-images";
|
import { encodeImages } from "@/utils/encode-images";
|
||||||
@@ -242,7 +242,7 @@ function DraftAgentScreenContent({
|
|||||||
commandDraftConfig,
|
commandDraftConfig,
|
||||||
statusControls,
|
statusControls,
|
||||||
} = composerState;
|
} = composerState;
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { MenuHeader } from "@/components/headers/menu-header";
|
|||||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { isCompactFormFactor, HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE } from "@/constants/layout";
|
import { useIsCompactFormFactor, HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE } from "@/constants/layout";
|
||||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||||
|
|
||||||
export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
||||||
@@ -17,7 +17,7 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
|||||||
const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
|
const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
|
||||||
const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0);
|
const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0);
|
||||||
|
|
||||||
const isCompactLayout = isCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCompactLayout) {
|
if (!isCompactLayout) {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pc
|
|||||||
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
||||||
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
|
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
|
||||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||||
import { isCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
|
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import { ProviderDiagnosticSheet } from "@/components/provider-diagnostic-sheet";
|
import { ProviderDiagnosticSheet } from "@/components/provider-diagnostic-sheet";
|
||||||
@@ -806,7 +806,7 @@ function DesktopAppUpdateRow() {
|
|||||||
<Text style={styles.aboutHintText}>{statusText}</Text>
|
<Text style={styles.aboutHintText}>{statusText}</Text>
|
||||||
{availableUpdate?.latestVersion ? (
|
{availableUpdate?.latestVersion ? (
|
||||||
<Text style={styles.aboutHintText}>
|
<Text style={styles.aboutHintText}>
|
||||||
New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
Ready to install: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
|
||||||
@@ -1013,7 +1013,7 @@ export default function SettingsScreen() {
|
|||||||
}
|
}
|
||||||
}, [isPlaybackTestRunning, voiceAudioEngine]);
|
}, [isPlaybackTestRunning, voiceAudioEngine]);
|
||||||
|
|
||||||
const isCompactLayout = isCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
const sections = getSettingsSections({ isDesktopApp });
|
const sections = getSettingsSections({ isDesktopApp });
|
||||||
|
|
||||||
const hostsProps: HostsSectionProps = {
|
const hostsProps: HostsSectionProps = {
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ import {
|
|||||||
closeBulkWorkspaceTabs,
|
closeBulkWorkspaceTabs,
|
||||||
} from "@/screens/workspace/workspace-bulk-close";
|
} from "@/screens/workspace/workspace-bulk-close";
|
||||||
import { findAdjacentPane } from "@/utils/split-navigation";
|
import { findAdjacentPane } from "@/utils/split-navigation";
|
||||||
import { isCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||||
|
|
||||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||||
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
||||||
@@ -599,7 +599,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const mainBackgroundColor = theme.colors.surfaceWorkspace;
|
const mainBackgroundColor = theme.colors.surfaceWorkspace;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const isMobile = isCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||||
|
|
||||||
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
|
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm --prefix ../.. run build:daemon && npm run build:main && electron-builder --config electron-builder.yml",
|
"build": "npm --prefix ../.. run build:daemon && npm run build:main && electron-builder --config electron-builder.yml",
|
||||||
"build:main": "tsc -p tsconfig.json",
|
"build:main": "tsc -p tsconfig.json",
|
||||||
"dev": "npm run build:main && wait-on tcp:8081 && electron .",
|
"dev": "./scripts/dev.sh",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
},
|
},
|
||||||
|
|||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
APP_DIR="$(cd "$DESKTOP_DIR/../app" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$DESKTOP_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
# Build the Electron main process
|
||||||
|
npm run build:main
|
||||||
|
|
||||||
|
# Get a random available port for Metro
|
||||||
|
EXPO_PORT=$("$ROOT_DIR/node_modules/.bin/get-port")
|
||||||
|
export EXPO_PORT
|
||||||
|
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo " Paseo Desktop Dev"
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo " Metro: http://localhost:${EXPO_PORT}"
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
# Launch Metro + Electron together, kill both on exit
|
||||||
|
"$ROOT_DIR/node_modules/.bin/concurrently" \
|
||||||
|
--kill-others \
|
||||||
|
--names "metro,electron" \
|
||||||
|
--prefix-colors "magenta,cyan" \
|
||||||
|
"cd '$APP_DIR' && npx expo start --port $EXPO_PORT" \
|
||||||
|
"$ROOT_DIR/node_modules/.bin/wait-on tcp:$EXPO_PORT && EXPO_DEV_URL=http://localhost:$EXPO_PORT electron '$DESKTOP_DIR'"
|
||||||
@@ -7,6 +7,7 @@ import { autoUpdater, type UpdateInfo } from "electron-updater";
|
|||||||
|
|
||||||
export type AppUpdateCheckResult = {
|
export type AppUpdateCheckResult = {
|
||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
|
readyToInstall: boolean;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
body: string | null;
|
body: string | null;
|
||||||
@@ -24,19 +25,84 @@ export type AppUpdateInstallResult = {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let cachedUpdateInfo: UpdateInfo | null = null;
|
let cachedUpdateInfo: UpdateInfo | null = null;
|
||||||
|
let downloadedUpdateVersion: string | null = null;
|
||||||
let downloading = false;
|
let downloading = false;
|
||||||
|
let autoUpdaterConfigured = false;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Configuration
|
// Configuration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function configureAutoUpdater(): void {
|
function configureAutoUpdater(): void {
|
||||||
// Don't auto-download — the user triggers install explicitly.
|
// Download updates in the background and only prompt once they are ready to install.
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = true;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
// Suppress built-in dialogs; the renderer handles UI.
|
// Suppress built-in dialogs; the renderer handles UI.
|
||||||
autoUpdater.autoRunAppAfterInstall = true;
|
autoUpdater.autoRunAppAfterInstall = true;
|
||||||
|
|
||||||
|
if (autoUpdaterConfigured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoUpdaterConfigured = true;
|
||||||
|
|
||||||
|
autoUpdater.on("update-available", (info) => {
|
||||||
|
cachedUpdateInfo = info;
|
||||||
|
downloadedUpdateVersion = null;
|
||||||
|
downloading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
|
cachedUpdateInfo = info;
|
||||||
|
downloadedUpdateVersion = info.version;
|
||||||
|
downloading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-not-available", () => {
|
||||||
|
cachedUpdateInfo = null;
|
||||||
|
downloadedUpdateVersion = null;
|
||||||
|
downloading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("error", (error) => {
|
||||||
|
downloading = false;
|
||||||
|
console.error("[auto-updater] Updater event failed:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReadyToInstallVersion(version: string): boolean {
|
||||||
|
return downloadedUpdateVersion === version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckResult(input: {
|
||||||
|
currentVersion: string;
|
||||||
|
hasUpdate: boolean;
|
||||||
|
readyToInstall: boolean;
|
||||||
|
info?: UpdateInfo | null;
|
||||||
|
}): AppUpdateCheckResult {
|
||||||
|
const { currentVersion, hasUpdate, readyToInstall, info } = input;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUpdate,
|
||||||
|
readyToInstall,
|
||||||
|
currentVersion,
|
||||||
|
latestVersion: info?.version ?? currentVersion,
|
||||||
|
body: typeof info?.releaseNotes === "string" ? info.releaseNotes : null,
|
||||||
|
date: typeof info?.releaseDate === "string" ? info.releaseDate : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleQuitAndInstall(onBeforeQuit?: () => Promise<void>): void {
|
||||||
|
// Use a short delay to allow the renderer to receive the response.
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
if (onBeforeQuit) await onBeforeQuit();
|
||||||
|
autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[auto-updater] quitAndInstall failed:", error);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -45,28 +111,34 @@ function configureAutoUpdater(): void {
|
|||||||
|
|
||||||
export async function checkForAppUpdate(currentVersion: string): Promise<AppUpdateCheckResult> {
|
export async function checkForAppUpdate(currentVersion: string): Promise<AppUpdateCheckResult> {
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
return {
|
return buildCheckResult({
|
||||||
hasUpdate: false,
|
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion: currentVersion,
|
hasUpdate: false,
|
||||||
body: null,
|
readyToInstall: false,
|
||||||
date: null,
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configureAutoUpdater();
|
configureAutoUpdater();
|
||||||
|
|
||||||
|
const cachedVersion = cachedUpdateInfo?.version ?? null;
|
||||||
|
if (cachedVersion && cachedVersion !== currentVersion) {
|
||||||
|
return buildCheckResult({
|
||||||
|
currentVersion,
|
||||||
|
hasUpdate: true,
|
||||||
|
readyToInstall: isReadyToInstallVersion(cachedVersion),
|
||||||
|
info: cachedUpdateInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await autoUpdater.checkForUpdates();
|
const result = await autoUpdater.checkForUpdates();
|
||||||
|
|
||||||
if (!result || !result.updateInfo) {
|
if (!result || !result.updateInfo) {
|
||||||
return {
|
return buildCheckResult({
|
||||||
hasUpdate: false,
|
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion: currentVersion,
|
hasUpdate: false,
|
||||||
body: null,
|
readyToInstall: false,
|
||||||
date: null,
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = result.updateInfo;
|
const info = result.updateInfo;
|
||||||
@@ -75,24 +147,31 @@ export async function checkForAppUpdate(currentVersion: string): Promise<AppUpda
|
|||||||
|
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
cachedUpdateInfo = info;
|
cachedUpdateInfo = info;
|
||||||
|
downloading = !isReadyToInstallVersion(latestVersion);
|
||||||
|
return buildCheckResult({
|
||||||
|
currentVersion,
|
||||||
|
hasUpdate: true,
|
||||||
|
readyToInstall: isReadyToInstallVersion(latestVersion),
|
||||||
|
info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
cachedUpdateInfo = null;
|
||||||
hasUpdate,
|
downloadedUpdateVersion = null;
|
||||||
|
downloading = false;
|
||||||
|
|
||||||
|
return buildCheckResult({
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion,
|
hasUpdate: false,
|
||||||
body: typeof info.releaseNotes === "string" ? info.releaseNotes : null,
|
readyToInstall: false,
|
||||||
date: typeof info.releaseDate === "string" ? info.releaseDate : null,
|
});
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[auto-updater] Failed to check for updates:", error);
|
console.error("[auto-updater] Failed to check for updates:", error);
|
||||||
return {
|
return buildCheckResult({
|
||||||
hasUpdate: false,
|
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion: currentVersion,
|
hasUpdate: false,
|
||||||
body: null,
|
readyToInstall: false,
|
||||||
date: null,
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,14 +187,6 @@ export async function downloadAndInstallUpdate(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloading) {
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
version: currentVersion,
|
|
||||||
message: "Update already in progress.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cachedUpdateInfo) {
|
if (!cachedUpdateInfo) {
|
||||||
return {
|
return {
|
||||||
installed: false,
|
installed: false,
|
||||||
@@ -126,24 +197,35 @@ export async function downloadAndInstallUpdate(
|
|||||||
|
|
||||||
configureAutoUpdater();
|
configureAutoUpdater();
|
||||||
|
|
||||||
|
const readyVersion = cachedUpdateInfo.version;
|
||||||
|
if (isReadyToInstallVersion(readyVersion)) {
|
||||||
|
scheduleQuitAndInstall(onBeforeQuit);
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version: readyVersion,
|
||||||
|
message: "Update downloaded. The app will restart shortly.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloading) {
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
version: currentVersion,
|
||||||
|
message: "Update is still being prepared. Try again in a moment.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
downloading = true;
|
downloading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await autoUpdater.downloadUpdate();
|
await autoUpdater.downloadUpdate();
|
||||||
// quitAndInstall restarts the app with the new version.
|
downloadedUpdateVersion = readyVersion;
|
||||||
// Use a short delay to allow the renderer to receive the response.
|
downloading = false;
|
||||||
setTimeout(async () => {
|
scheduleQuitAndInstall(onBeforeQuit);
|
||||||
try {
|
|
||||||
if (onBeforeQuit) await onBeforeQuit();
|
|
||||||
autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[auto-updater] quitAndInstall failed:", error);
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
installed: true,
|
installed: true,
|
||||||
version: cachedUpdateInfo.version,
|
version: readyVersion,
|
||||||
message: "Update downloaded. The app will restart shortly.",
|
message: "Update downloaded. The app will restart shortly.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ inheritLoginShellEnv();
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
import { app, BrowserWindow, ipcMain, nativeImage, net, protocol } from "electron";
|
import { app, BrowserWindow, ipcMain, nativeImage, net, protocol } from "electron";
|
||||||
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +39,38 @@ const APP_SCHEME = "paseo";
|
|||||||
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
||||||
app.setName("Paseo");
|
app.setName("Paseo");
|
||||||
|
|
||||||
|
// In dev mode, detect git worktrees and isolate each instance so multiple
|
||||||
|
// Electron windows can run side-by-side (separate userData = separate lock).
|
||||||
|
let devWorktreeName: string | null = null;
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
try {
|
||||||
|
const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 3000,
|
||||||
|
}).trim();
|
||||||
|
devWorktreeName = path.basename(topLevel);
|
||||||
|
// Main checkout (e.g. "paseo") gets default userData — only worktrees diverge.
|
||||||
|
const commonDir = path.resolve(
|
||||||
|
topLevel,
|
||||||
|
execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
||||||
|
cwd: topLevel,
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 3000,
|
||||||
|
}).trim(),
|
||||||
|
);
|
||||||
|
const isWorktree = path.resolve(topLevel, ".git") !== commonDir;
|
||||||
|
if (isWorktree) {
|
||||||
|
app.setPath("userData", path.join(app.getPath("appData"), `Paseo-${devWorktreeName}`));
|
||||||
|
log.info("[worktree] isolated userData for worktree:", devWorktreeName);
|
||||||
|
} else {
|
||||||
|
devWorktreeName = null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
devWorktreeName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Allow users to pass Chromium flags via PASEO_ELECTRON_FLAGS for debugging
|
// Allow users to pass Chromium flags via PASEO_ELECTRON_FLAGS for debugging
|
||||||
// rendering issues (e.g. "--disable-gpu --ozone-platform=x11").
|
// rendering issues (e.g. "--disable-gpu --ozone-platform=x11").
|
||||||
// Must run before app.whenReady().
|
// Must run before app.whenReady().
|
||||||
@@ -127,7 +160,9 @@ async function createMainWindow(): Promise<void> {
|
|||||||
const iconPath = getWindowIconPath();
|
const iconPath = getWindowIconPath();
|
||||||
const systemTheme = resolveSystemWindowTheme();
|
const systemTheme = resolveSystemWindowTheme();
|
||||||
|
|
||||||
|
const title = devWorktreeName ? `Paseo (${devWorktreeName})` : "Paseo";
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
|
title,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
show: false,
|
show: false,
|
||||||
@@ -144,6 +179,10 @@ async function createMainWindow(): Promise<void> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (devWorktreeName) {
|
||||||
|
app.dock?.setBadge(devWorktreeName);
|
||||||
|
}
|
||||||
|
|
||||||
setupWindowResizeEvents(mainWindow);
|
setupWindowResizeEvents(mainWindow);
|
||||||
setupDefaultContextMenu(mainWindow);
|
setupDefaultContextMenu(mainWindow);
|
||||||
setupDragDropPrevention(mainWindow);
|
setupDragDropPrevention(mainWindow);
|
||||||
|
|||||||
@@ -18,3 +18,6 @@ PASEO_LISTEN=127.0.0.1:6767
|
|||||||
# Debug recordings (dictation + STT input + TTS output)
|
# Debug recordings (dictation + STT input + TTS output)
|
||||||
# When enabled, recordings are saved under `${cwd}/.debug/recordings/`
|
# When enabled, recordings are saved under `${cwd}/.debug/recordings/`
|
||||||
PASEO_DICTATION_DEBUG=1
|
PASEO_DICTATION_DEBUG=1
|
||||||
|
|
||||||
|
# Enable verbose Claude SDK stream logging (trace-level per-token logs)
|
||||||
|
PASEO_CLAUDE_DEBUG=1
|
||||||
|
|||||||
@@ -1047,6 +1047,8 @@ export function readEventIdentifiers(message: SDKMessage): EventIdentifiers {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const claudeDebug = process.env.PASEO_CLAUDE_DEBUG === "1";
|
||||||
|
|
||||||
export class ClaudeAgentClient implements AgentClient {
|
export class ClaudeAgentClient implements AgentClient {
|
||||||
readonly provider: "claude" = "claude";
|
readonly provider: "claude" = "claude";
|
||||||
readonly capabilities = CLAUDE_CAPABILITIES;
|
readonly capabilities = CLAUDE_CAPABILITIES;
|
||||||
@@ -2453,15 +2455,17 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
while (!this.closed && this.query === activeQuery) {
|
while (!this.closed && this.query === activeQuery) {
|
||||||
try {
|
try {
|
||||||
for await (const message of activeQuery) {
|
for await (const message of activeQuery) {
|
||||||
this.logger.trace(
|
if (claudeDebug) {
|
||||||
{
|
this.logger.trace(
|
||||||
claudeSessionId: this.claudeSessionId,
|
{
|
||||||
messageType: message.type,
|
claudeSessionId: this.claudeSessionId,
|
||||||
messageSubtype: "subtype" in message ? message.subtype : undefined,
|
messageType: message.type,
|
||||||
messageUuid: "uuid" in message ? message.uuid : undefined,
|
messageSubtype: "subtype" in message ? message.subtype : undefined,
|
||||||
},
|
messageUuid: "uuid" in message ? message.uuid : undefined,
|
||||||
"Claude query pump: raw SDK message",
|
},
|
||||||
);
|
"Claude query pump: raw SDK message",
|
||||||
|
);
|
||||||
|
}
|
||||||
consecutiveInterruptAbortRecoveries = 0;
|
consecutiveInterruptAbortRecoveries = 0;
|
||||||
if (await this.handleMissingResumedConversation(message, activeQuery)) {
|
if (await this.handleMissingResumedConversation(message, activeQuery)) {
|
||||||
return;
|
return;
|
||||||
@@ -2537,14 +2541,16 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null;
|
const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null;
|
||||||
const identifiers = readEventIdentifiers(message);
|
const identifiers = readEventIdentifiers(message);
|
||||||
|
|
||||||
this.logger.trace(
|
if (claudeDebug) {
|
||||||
{
|
this.logger.trace(
|
||||||
claudeSessionId: this.claudeSessionId,
|
{
|
||||||
messageType: message.type,
|
claudeSessionId: this.claudeSessionId,
|
||||||
turnId,
|
messageType: message.type,
|
||||||
},
|
turnId,
|
||||||
"Claude query pump: SDK message",
|
},
|
||||||
);
|
"Claude query pump: SDK message",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const messageEvents = this.translateMessageToEvents(message, {
|
const messageEvents = this.translateMessageToEvents(message, {
|
||||||
suppressAssistantText: true,
|
suppressAssistantText: true,
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import {
|
|||||||
} from "./test-utils/index.js";
|
} from "./test-utils/index.js";
|
||||||
import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js";
|
import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js";
|
||||||
import { chunkPcm16, parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js";
|
import { chunkPcm16, parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js";
|
||||||
|
import type {
|
||||||
|
AgentClient,
|
||||||
|
AgentPersistenceHandle,
|
||||||
|
AgentRunResult,
|
||||||
|
AgentSession,
|
||||||
|
AgentSessionConfig,
|
||||||
|
AgentStreamEvent,
|
||||||
|
} from "./agent/agent-sdk-types.js";
|
||||||
|
|
||||||
const openaiApiKey = process.env.OPENAI_API_KEY ?? null;
|
const openaiApiKey = process.env.OPENAI_API_KEY ?? null;
|
||||||
|
|
||||||
@@ -94,6 +102,116 @@ function waitForSignal<T>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NonPersistentReloadSession implements AgentSession {
|
||||||
|
readonly provider = "claude" as const;
|
||||||
|
readonly id = null;
|
||||||
|
readonly capabilities = {
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsSessionPersistence: true,
|
||||||
|
supportsDynamicModes: false,
|
||||||
|
supportsMcpServers: false,
|
||||||
|
supportsReasoningStream: false,
|
||||||
|
supportsToolInvocations: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
constructor(private readonly onClose: () => void) {}
|
||||||
|
|
||||||
|
async run(): Promise<AgentRunResult> {
|
||||||
|
return {
|
||||||
|
sessionId: "non-persistent",
|
||||||
|
finalText: "",
|
||||||
|
timeline: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTurn(): Promise<{ turnId: string }> {
|
||||||
|
return { turnId: "non-persistent-turn" };
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(_callback: (event: AgentStreamEvent) => void): () => void {
|
||||||
|
return () => undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRuntimeInfo() {
|
||||||
|
return {
|
||||||
|
provider: "claude" as const,
|
||||||
|
sessionId: null,
|
||||||
|
model: null,
|
||||||
|
modeId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableModes(): Promise<[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentMode(): Promise<string | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMode(_modeId: string): Promise<void> {}
|
||||||
|
|
||||||
|
getPendingPermissions() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async respondToPermission(): Promise<void> {}
|
||||||
|
|
||||||
|
describePersistence(): AgentPersistenceHandle | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async interrupt(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
this.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NonPersistentReloadClient implements AgentClient {
|
||||||
|
readonly provider = "claude" as const;
|
||||||
|
readonly capabilities = {
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsSessionPersistence: true,
|
||||||
|
supportsDynamicModes: false,
|
||||||
|
supportsMcpServers: false,
|
||||||
|
supportsReasoningStream: false,
|
||||||
|
supportsToolInvocations: false,
|
||||||
|
} as const;
|
||||||
|
createSessionCalls = 0;
|
||||||
|
resumeSessionCalls = 0;
|
||||||
|
closeCalls = 0;
|
||||||
|
|
||||||
|
async isAvailable(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSession(_config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
|
this.createSessionCalls += 1;
|
||||||
|
return new NonPersistentReloadSession(() => {
|
||||||
|
this.closeCalls += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeSession(
|
||||||
|
_handle: AgentPersistenceHandle,
|
||||||
|
_overrides?: Partial<AgentSessionConfig>,
|
||||||
|
): Promise<AgentSession> {
|
||||||
|
this.resumeSessionCalls += 1;
|
||||||
|
return new NonPersistentReloadSession(() => {
|
||||||
|
this.closeCalls += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listModels() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("daemon client E2E", () => {
|
describe("daemon client E2E", () => {
|
||||||
let ctx: DaemonTestContext;
|
let ctx: DaemonTestContext;
|
||||||
|
|
||||||
@@ -281,6 +399,38 @@ describe("daemon client E2E", () => {
|
|||||||
}
|
}
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
|
test("refresh_agent rebuilds a live agent even when it has no persistence handle", async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const client = new NonPersistentReloadClient();
|
||||||
|
const localCtx = await createDaemonTestContext({
|
||||||
|
agentClients: {
|
||||||
|
claude: client,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await localCtx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
provider: "claude",
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.createSessionCalls).toBe(1);
|
||||||
|
expect(client.resumeSessionCalls).toBe(0);
|
||||||
|
expect(client.closeCalls).toBe(0);
|
||||||
|
|
||||||
|
await localCtx.client.refreshAgent(created.id);
|
||||||
|
|
||||||
|
expect(client.createSessionCalls).toBe(2);
|
||||||
|
expect(client.resumeSessionCalls).toBe(0);
|
||||||
|
expect(client.closeCalls).toBe(1);
|
||||||
|
} finally {
|
||||||
|
await localCtx.cleanup();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("resume_agent auto-unarchives archived agents", async () => {
|
test("resume_agent auto-unarchives archived agents", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const LOG_LEVEL_PRIORITIES: Record<LogLevel, number> = {
|
|||||||
|
|
||||||
const DEFAULT_CONSOLE_LEVEL: LogLevel = "info";
|
const DEFAULT_CONSOLE_LEVEL: LogLevel = "info";
|
||||||
const DEFAULT_CONSOLE_FORMAT: LogFormat = "pretty";
|
const DEFAULT_CONSOLE_FORMAT: LogFormat = "pretty";
|
||||||
const DEFAULT_FILE_LEVEL: LogLevel = "trace";
|
const DEFAULT_FILE_LEVEL: LogLevel = "debug";
|
||||||
const DEFAULT_FILE_ROTATE_SIZE = "10m";
|
const DEFAULT_FILE_ROTATE_SIZE = "10m";
|
||||||
const DEFAULT_FILE_ROTATE_MAX_FILES = 2;
|
const DEFAULT_FILE_ROTATE_MAX_FILES = 2;
|
||||||
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
|
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ function clientSupportsAllProviders(appVersion: string | null): boolean {
|
|||||||
function clientSupportsFlexibleEditorIds(appVersion: string | null): boolean {
|
function clientSupportsFlexibleEditorIds(appVersion: string | null): boolean {
|
||||||
return isAppVersionAtLeast(appVersion, MIN_VERSION_FLEXIBLE_EDITOR_IDS);
|
return isAppVersionAtLeast(appVersion, MIN_VERSION_FLEXIBLE_EDITOR_IDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
||||||
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
|
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
|
||||||
const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
|
const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
|
||||||
|
|||||||
@@ -1130,6 +1130,7 @@ export const WorkspaceSetupStatusRequestSchema = z.object({
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// TODO(2026-07): Remove once most clients are on >=0.1.50 and support arbitrary editor ids.
|
// TODO(2026-07): Remove once most clients are on >=0.1.50 and support arbitrary editor ids.
|
||||||
export const LEGACY_EDITOR_TARGET_IDS = [
|
export const LEGACY_EDITOR_TARGET_IDS = [
|
||||||
"cursor",
|
"cursor",
|
||||||
|
|||||||
@@ -1946,6 +1946,7 @@ const ReviewDecisionSchema = z
|
|||||||
type CheckRunNode = z.infer<typeof CheckRunNodeSchema>;
|
type CheckRunNode = z.infer<typeof CheckRunNodeSchema>;
|
||||||
type StatusContextNode = z.infer<typeof StatusContextNodeSchema>;
|
type StatusContextNode = z.infer<typeof StatusContextNodeSchema>;
|
||||||
|
|
||||||
|
|
||||||
async function resolveGhPath(): Promise<string> {
|
async function resolveGhPath(): Promise<string> {
|
||||||
if (cachedGhPath === undefined) {
|
if (cachedGhPath === undefined) {
|
||||||
cachedGhPath = await findExecutable("gh");
|
cachedGhPath = await findExecutable("gh");
|
||||||
|
|||||||
Reference in New Issue
Block a user