Compare commits

...

3 Commits

Author SHA1 Message Date
Dax Raad b20a31bb8f migration 2026-08-05 01:37:22 -04:00
Brendan Allan f2b3a12910 refactor(app): own rendering contracts (#40375) 2026-08-05 13:30:50 +08:00
Aiden Cline daa998f9c3 fix(core): make xAI OAuth device-only (#40538) 2026-08-04 22:55:31 -05:00
172 changed files with 4130 additions and 13544 deletions
+195 -26
View File
@@ -5,8 +5,27 @@
- Use the `dev` branch database schema and migration registry as the V1 baseline.
- Remove migrations that exist only on the V2 branch.
- Generate one canonical migration from the `dev` schema to the final V2 schema.
- Add explicit data operations to that migration where generated DDL is insufficient.
- Test the migration against a populated database at the exact `dev` schema.
- Keep the canonical migration focused on schema changes and dropping obsolete tables.
- Run the V1 history backfill through an experimental server endpoint invoked by the CLI before it opens the TUI.
- Show committed session progress while the endpoint runs.
Expose `GET /api/experimental/migration/v1` for status and a blocking `POST /api/experimental/migration/v1` to run or
resume the backfill. The status is `required`, `running`, or `completed`. On startup, the CLI checks status first and
renders no migration UI when it is already complete. For required or running status, it shows a spinner and waits for the
blocking POST without a request timeout. While migration runs, poll GET once per second and render completed and total
session counts. GET derives total from all session rows and completed from rows through the stored cursor; the count
advances only after a session transaction commits. The POST returns `{ status: "completed" }`. Do not add a background
job or streaming progress protocol. Interrupted calls resume from the stored cursor.
Initially, only interactive TUI startup performs this check; noninteractive run, ACP, raw API, service, health, version,
and help flows do not trigger the backfill.
Keep migration behavior in Core: status, semaphore, checkpointing, V1 decoding, transformation, and database writes.
Protocol owns the experimental GET/POST contracts, Server handlers delegate to Core, and the interactive CLI owns only
the status check and spinner presentation.
Guard the endpoint with one process-local Effect `Semaphore`. Concurrent callers wait; after the active call completes,
waiting callers acquire the permit, observe the completion key, and return immediately. No distributed lock is required
for the current single elected server process.
## Preserve
@@ -15,20 +34,35 @@ The canonical V1 data remains in its existing tables. In particular, preserve `s
Preserve `workspace` rows and existing `session.workspace_id` values unchanged. The migration must not clear or rebuild
workspace relationships.
Keep the `todo` table and its data unchanged. V2 does not currently migrate todos into another representation, and the
generated migration must not drop the table.
Preserve existing non-null `session.agent` and `session.model` selections. Fill missing values from the latest ordinary
V1 user message ordered by `time_created` and `id`, excluding compaction and subtask-only messages. Copy agent, provider
ID, model ID, and variant, normalizing an absent variant to `default`.
## Truncate
Recompute session usage aggregates from all canonical V1 assistant messages, including compaction or other internal
assistants omitted from the V2 projection. Overwrite session cost and input, output, reasoning, cache-read, and
cache-write token totals with those sums.
Truncate these pre-launch V2 tables before applying schema changes:
Clear persisted `session.revert` state. A staged revert is transient operational state and may refer to omitted projection
rows or unavailable snapshots; it must not resume automatically after upgrading. Preserve the underlying messages,
parts, and file history.
- `event`
- `event_sequence`
- `session_message`
Clear `session.time_compacting`, leave the new `time_suspended` column as `NULL`, and preserve session creation, update,
and archive timestamps. Preserve project `time_initialized`; it is unrelated durable state.
These rows are not canonical V1 data. Truncating `event` before adding the required `event.created` column means the
column needs neither a backfill nor a default. After truncation, rebuild `session_message` from canonical V1 `message`
and `part` rows rather than retaining its pre-launch V2 contents.
Keep the legacy `todo` table and its data physically unchanged, but do not include it in the final V2 Drizzle schema.
After generation, remove the generated `DROP TABLE todo` statement from the canonical migration so the table remains as
unmanaged legacy storage.
## Per-Session Replacement
Do not truncate `event`, `event_sequence`, or `session_message` globally before the backfill. A whole-table delete can
hold SQLite's writer lock long enough to block the running TUI.
Replace each legacy session's V2 state inside that session's checkpointed migration transaction. Delete `event` rows for
the session aggregate, delete its `session_message` rows, rebuild its projection from canonical V1 `message` and `part`
rows, and overwrite its `event_sequence` watermark. If migration of that session fails, all replacements roll back and
the durable cursor remains at the previously committed session. Rows owned by sessions outside the legacy migration set
remain untouched.
## Message Backfill
@@ -36,9 +70,23 @@ Backfill canonical V1 history from `message` and `part` into `session_message`.
the migration. Preserving the V1 tables alone keeps the data safe but does not make existing history visible through the
V2 session APIs, which read `session_message`.
Do not fail the whole migration when a V1 message or part payload cannot be decoded. Skip an undecodable message's V2
projection and log its session and message IDs. Skip an undecodable part while continuing to map its message, and perform
special-message pairing only with decoded rows. Assign sequences after filtering. Leave every malformed source row
untouched in the V1 tables.
Skip and log orphan parts whose source message does not exist and parts with unknown or unsupported types. Continue
migrating the owning message and other valid parts. Include session, message, part ID, and observed type in warnings, and
leave skipped source rows unchanged.
Reuse each V1 `message.id` as the corresponding `session_message.id`. Stable IDs keep the migration deterministic and
avoid rewriting other persisted state that may refer to a message.
For ordinary user and assistant rows, preserve source `message.time_created` and `message.time_updated`. Entirely
synthetic messages preserve their source timestamps, and synthetic rows split from mixed messages use the source user
timestamps. A collapsed compaction uses the compaction user creation time and the later update time of the compaction
user and summary assistant. Keep payload creation/completion times consistent with row timestamps.
Within each session, order V1 messages by `time_created` and then `id`, matching the existing V1 message index. Assign
contiguous `session_message.seq` values starting at `0`.
@@ -46,15 +94,122 @@ Map ordinary V1 messages one-to-one by role. Each ordinary V1 user message becom
V1 assistant message becomes one V2 `assistant` row. Fold the source message's ordered V1 parts into that row's V2
payload.
Keep ordinary messages even when their transformed payload becomes empty after filtering. Preserve an empty V2 user row
with `text: ""` and an empty V2 assistant row with `content: []` so IDs, chronology, and conversation structure remain
stable. Omit only explicitly dropped internal concepts and undecodable messages.
Handle semantic marker parts before applying the ordinary mapping. In particular, a V1 user message containing a
`compaction` part and its paired assistant summary represent one compaction operation, not two ordinary messages. Special
part mappings must be decided explicitly before implementing the backfill.
Do not carry the V1 subtask concept into the V2 projection. Omit user messages containing only `subtask` parts and omit
the paired assistant task-tool messages generated from those markers. For mixed user messages, ignore the `subtask`
parts while preserving ordinary content, and still omit assistant task-tool messages generated by the skipped subtasks.
Keep all source rows unchanged in the V1 `message` and `part` tables.
Map ordinary V1 assistant `text` and `reasoning` parts into the V2 assistant `content` array in part order. Preserve text,
including empty assistant text parts used as structural separators. Map V1 part metadata to optional V2 provider state.
For reasoning, map `time.start` to `time.created` and optional `time.end` to `time.completed`.
Preserve V1 tool parts that are `pending` or `running`, but convert them to terminal V2 tool error states. Preserve the
call ID, tool name, parsed input, metadata, and available start time. Use the assistant message creation time when the V1
state has no start time. Set the error to type `tool.interrupted` with message
`Tool execution was interrupted before V2 migration`. Never resume migrated tool executions.
For a completed V1 tool part, use `callID` as the V2 tool content ID and preserve the tool name and parsed input. Set the
state to `completed`. Convert V1 output into the first text content item and convert stored output attachments into
following file content items with their URI, MIME type, and filename. Preserve state metadata. Map `time.start` to
`time.created` and `time.end` to `time.completed`. When `time.compacted` exists, use
`[Old tool result content cleared]` as the only output and omit attachments.
For a failed V1 tool part, preserve the call ID, tool name, parsed input, metadata, and timestamps, and set the V2 state
to `error`. Convert the V1 error string to a structured error with type `tool.execution`. If V1 metadata contains a string
`output`, preserve it as optional V2 text content. Map `time.start` to `time.created` and `time.end` to `time.completed`.
For an ordinary V1 assistant message, preserve agent, provider ID, model ID, optional variant, creation and completion
times, cost, and input/output/reasoning/cache token counts. Use `default` when the V1 variant is absent. Ignore V1
`tokens.total` because it is derivable and V2 does not persist it.
Use V1 assistant `parentID` only while pairing compactions and skipped subtasks with their originating user messages. Do
not persist it in ordinary V2 assistant rows; V2 uses ordered history rather than user/assistant parent links.
Ignore the optional V1 assistant `structured` output value. V2 has no equivalent top-level assistant field, and visible
text and tool content are migrated separately. Retain the original structured value only in the V1 `message` row.
Ignore V1 assistant `mode` and historical `path` (`cwd` and `root`). Mode is redundant with the preserved assistant
agent, and historical filesystem paths do not belong to the V2 assistant message contract. Retain them only in the V1
`message` row.
For assistant finish reasons, preserve `stop`, `length`, `tool-calls`, `content-filter`, `error`, and `unknown`. Map every
other nonempty V1 finish value to `unknown`, and leave the field absent when V1 omitted it. Do not retain unrecognized raw
finish values in metadata.
Map V1 assistant errors into the current V2 `{ type, message }` storage shape. Normalize Auth, content-filter, context
overflow, structured-output, output-length, aborted, API, and unknown errors to the established V2 string conventions,
preserve the message, and discard V1-only retryability and raw provider details.
Ignore V1 `retry` parts. Do not populate the V2 assistant `retry` field during migration; historical retry state is not
useful enough to preserve. The original retry rows remain in the V1 `part` table.
Do not emit V2 assistant content for V1 `step-start` and `step-finish` parts. Use the first available
`step-start.snapshot` as `assistant.snapshot.start` and the last available `step-finish.snapshot` as
`assistant.snapshot.end`. Continue to source finish, cost, and tokens from the assistant message itself. Ignore step
markers without snapshots.
Do not emit assistant content for standalone V1 `snapshot` or `patch` parts. If no start snapshot came from `step-start`,
use the first standalone snapshot value, then the first patch hash as a final fallback. Only `step-finish.snapshot` may
populate the end snapshot. Merge patch file lists into `assistant.snapshot.files` in first-seen order with duplicates
removed.
V2 follow-up: replace the open `SessionError.Error` string shape with a properly typed persisted error union. This is not
a blocker for the V1 migration, which should target the current storage contract.
V1 synthetic content is represented by user text parts with `synthetic: true`, not by a separate message role. A V1 user
message whose visible text parts are all synthetic should become a V2 `synthetic` message. If a V1 user message mixes
ordinary and synthetic content, preserve the ordinary content in the V2 `user` row and emit the synthetic content as an
adjacent V2 `synthetic` row. Ignore text parts marked `ignored`, matching V1 model-history behavior.
For an ordinary V2 user message, take visible V1 text parts that are neither ignored nor synthetic, preserve part order,
and join their text with `"\n\n"`. Use an empty string when the message contains attachments but no ordinary text.
Ignore the optional V1 user-message `system` override. Do not create a V2 system message or preserve the override in
metadata. The original value remains in the V1 `message` row.
Ignore the optional V1 user-message `tools` map. It represented request-time tool enablement for a historical step and
must not affect future V2 execution. The original value remains in the V1 `message` row.
Ignore the optional V1 user-message `format` field and its schema. It controlled structured-output behavior for a
historical request and must not affect future V2 runs. Preserve visible assistant text normally; retain the original
format only in the V1 `message` row.
Ignore V1 user-message `summary` metadata, including title, body, and diffs. V2 user messages have no equivalent field,
and session-level summary data is already persisted separately. Retain the original summary only in the V1 `message`
row.
Map V1 `agent` parts into the V2 user message's `agents` array in part order. Preserve `name`. When the V1 part has
`source`, map its `value`, `start`, and `end` into the V2 attachment's `mention.text`, `mention.start`, and `mention.end`.
Omit `agents` when there are no agent parts.
Do not read the filesystem or network while migrating V1 file attachments. Attachment migration must be deterministic
from database contents alone. Convert persisted `data:` URLs; represent non-embedded `file:`, HTTP, and other external
URLs with deterministic text rather than fetching them. Keep the original V1 `part` rows unchanged.
For a V1 file backed by a `data:` URL, decode the URL and normalize its payload to base64 for the V2 attachment's `data`.
Preserve `mime` and optional `filename` as `name`. Use a V2 `uri` source with the original URI for a V1 resource source;
otherwise use an `inline` source. When V1 source text metadata exists, map its `value`, `start`, and `end` into the V2
attachment mention. Leave `description` unset and preserve file-part order in the V2 `files` array.
For a non-embedded V1 file, do not create a V2 file attachment. Append
`[Attachment unavailable after migration: <name-or-url> (<mime>)]` to the V2 user text in original part order, separated
by blank lines. Prefer the V1 filename, then resource URI, then part URL for the label. The original URL remains only in
the preserved V1 `part` row.
For a synthetic row split from a mixed user message, derive a generated-looking ID from the source message ID. Preserve
the source ID's 12-character timestamp component and replace its 14-character random component with a deterministic
base-62 encoding of a hash of `v1-synthetic:` plus the source message ID. If that candidate collides with an existing or
derived message ID, deterministically retry with an incrementing salt. Place the synthetic row immediately after its
source user row. Entirely synthetic messages continue to reuse their original message ID.
Use the V1 compaction user message ID as the ID of the collapsed V2 compaction message. This matches V2's use of the
admitted compaction input ID and preserves references to the initiating message.
@@ -64,9 +219,13 @@ serialize the retained V1 tail beginning at `tail_start_id` for `recent`. Use an
retained, and use the compaction user message creation time. Do not emit the paired summary assistant as a separate V2
assistant row.
After rebuilding `session_message`, seed `event_sequence` with one row per migrated session. Set its watermark to that
session's maximum backfilled `session_message.seq`. This prevents new V2 events from reusing sequence numbers or sorting
before migrated history. The `event` table remains empty.
Do not project incomplete or failed V1 compactions into `session_message`. Omit both the internal compaction user marker
and its paired summary assistant when no successful summary was completed. Assign final sequence numbers after filtering
so omitted compactions leave no gaps. Their source rows remain preserved in the V1 `message` and `part` tables.
After rebuilding a session's `session_message`, replace its `event_sequence` watermark with that session's maximum
backfilled `session_message.seq`. This prevents new V2 events from reusing sequence numbers or sorting before migrated
history. The migrated session's prior `event` rows are removed in the same transaction.
## Drop
@@ -74,6 +233,7 @@ Drop these pre-launch V2 tables without preserving or transforming their rows:
- `session_input`
- `session_context_epoch`
- `data_migration`
Do not transfer `session_input` rows into `session_pending`.
@@ -103,16 +263,25 @@ schema.
New nullable session columns, including `fork_session_id`, `fork_boundary`, and `time_suspended`, require no explicit
backfill. Existing rows naturally receive `NULL` when the generated migration adds the columns.
## Verification
## Execution
The canonical migration test should seed representative V1 sessions, messages, parts, todos, projects, accounts,
credentials, permissions, shares, and workspaces. After migration, it should verify:
Store V1 backfill state in `kv`; do not retain a dedicated `data_migration` table. Store the last successfully migrated
session ID under `migration.v1-v2.session.cursor` and write `migration.v1-v2.completed` with value `true` after every
session finishes. Delete the cursor key on completion and return immediately on later calls when the completion key
exists.
- Preserved rows and encoded values remain unchanged.
- Todo rows remain available in the unchanged `todo` table.
- `event` is empty, and stale pre-launch rows are absent from the rebuilt projections.
- Backfilled `session_message` rows represent the canonical V1 `message` and `part` history.
- Each migrated session's `event_sequence` watermark matches its maximum backfilled message sequence.
- Dropped tables no longer exist.
- New tables exist and are empty.
- The final schema has no ungenerated changes.
Absence of the completion key means migration is required, including on a fresh database. Running the endpoint against a
database with no sessions completes immediately and writes the completion key; fresh database initialization does not
seed migration state specially.
Process sessions in stable ID order. Rebuild one session in one transaction, including its `session_message` rows,
session-level backfills, `event_sequence` watermark, and cursor update. If interrupted during a session, that transaction
rolls back and the next endpoint call retries the same session. If it committed, the next call continues after the stored
cursor. Mark the migration complete after the final session and return immediately on later calls.
Process every `session` row, including archived, root, child, and empty sessions, as well as sessions whose messages are
all skipped or internal. Each successfully committed session advances the cursor.
## Testing
Detailed migration test design is deferred until after the canonical migration is implemented.
@@ -9,7 +9,7 @@ import type {
ToolPart,
ToolState,
UserMessage,
} from "@opencode-ai/sdk/v2/client"
} from "../../../src/types"
import type { SessionV1Info, SessionStatus } from "@opencode-ai/client/promise"
import { expect, type Page } from "@playwright/test"
import { Schema } from "effect"
+1
View File
@@ -12,6 +12,7 @@
"./performance/unit/visual-stability.test.ts",
"./reproduction/timeline-suspense/**/*.ts",
"./reproduction/timeline-suspense/**/*.tsx",
"../src/types.ts",
"../src/pages/session/timeline/observe-element-offset.ts",
"./regression/new-session-panel-corner.spec.ts",
"./regression/session-timeline-context-resize.spec.ts",
@@ -1,5 +1,5 @@
import { getFilename } from "@opencode-ai/core/util/path"
import type { Project } from "@opencode-ai/sdk/v2/client"
import type { Project } from "@/types"
import type { SessionInfo } from "@opencode-ai/client/promise"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createMemo, onCleanup } from "solid-js"
+1 -1
View File
@@ -8,7 +8,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { showToast } from "@/utils/toast"
import { extractPromptFromParts } from "@/utils/prompt"
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
import type { TextPart as SDKTextPart } from "@/types"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { useLanguage } from "@/context/language"
@@ -8,7 +8,7 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup,
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/server"
import type { Path } from "@opencode-ai/sdk/v2/client"
import type { Path } from "@/types"
import {
absoluteTreePath,
activeTreeNavigation,
@@ -9,7 +9,7 @@ import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/server"
import { useGlobal } from "@/context/global"
import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
import type { Path } from "@opencode-ai/sdk/v2/client"
import type { Path } from "@/types"
interface DialogSelectDirectoryProps {
title?: string
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { buildFileTreeV2Model, flattenFileTreeV2, flattenLiveFileTreeV2 } from "./file-tree-v2-model"
import type { FileNode } from "@opencode-ai/sdk/v2"
import type { FileNode } from "@/types"
describe("buildFileTreeV2Model", () => {
test("builds a sorted tree and flattens expanded directories", () => {
@@ -1,4 +1,4 @@
import type { FileNode } from "@opencode-ai/sdk/v2"
import type { FileNode } from "@/types"
export type FileTreeV2Model = {
children: ReadonlyMap<string, readonly FileTreeV2Node[]>
+1 -1
View File
@@ -12,7 +12,7 @@ import {
type ParentProps,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import type { FileNode } from "@opencode-ai/sdk/v2"
import type { FileNode } from "@/types"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
+1 -1
View File
@@ -17,7 +17,7 @@ import {
type ParentProps,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import type { FileNode } from "@opencode-ai/sdk/v2"
import type { FileNode } from "@/types"
const MAX_DEPTH = 128
@@ -5,7 +5,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
import type { ReferenceInfo } from "@/types"
import { createEffect, createMemo, on, Show } from "solid-js"
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2"
@@ -1,6 +1,6 @@
// @ts-nocheck
import { createStore } from "solid-js/store"
import type { Todo } from "@opencode-ai/sdk/v2"
import type { Todo } from "@/types"
import { createPromptState } from "@/context/prompt"
import { SessionComposerRegion, createSessionComposerRegionController } from "@/pages/session/composer"
import { createPromptInputHistory, PromptInput } from "./prompt-input"
+1 -1
View File
@@ -81,7 +81,7 @@ import { promptDesignPlaceholder, promptPlaceholder } from "./prompt-input/place
import { createPromptInputTransientState } from "./prompt-input/transient-state"
import { showToast } from "@/utils/toast"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
import type { ReferenceInfo } from "@/types"
export { createPromptInputHistory }
export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission }
@@ -1,5 +1,5 @@
import { getFilename } from "@opencode-ai/core/util/path"
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
import type { AgentPartInput, FilePartInput, Part, TextPartInput } from "@/types"
import type { FileSelection } from "@/context/file"
import { encodeFilePath } from "@/context/file/path"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
@@ -1,4 +1,4 @@
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
import type { Message, Session } from "@/types"
import { showToast } from "@/utils/toast"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Binary } from "@opencode-ai/core/util/binary"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import type { Message, Part } from "@/types"
import { estimateSessionContextBreakdown } from "./session-context-breakdown"
const user = (id: string) => {
@@ -1,4 +1,4 @@
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import type { Message, Part } from "@/types"
export type SessionContextBreakdownKey = "system" | "user" | "assistant" | "tool" | "other"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Message } from "@opencode-ai/sdk/v2/client"
import type { Message } from "@/types"
import { getSessionContext } from "./session-context-metrics"
const assistant = (
@@ -1,4 +1,4 @@
import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client"
import type { AssistantMessage, Message } from "@/types"
type Provider = {
id: string
@@ -10,7 +10,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { File } from "@opencode-ai/session-ui/file"
import { Markdown } from "@opencode-ai/session-ui/markdown"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, UserMessage } from "@/types"
import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers"
import { useSDK } from "@/context/sdk"
@@ -1,4 +1,4 @@
import type { LspStatus } from "@opencode-ai/sdk/v2/client"
import type { LspStatus } from "@/types"
import type { McpServer } from "@opencode-ai/client/promise"
export function hasServiceNeedingAttention(input: { mcp: Array<McpServer["status"]["status"]> }) {
@@ -9,7 +9,7 @@ import { useGlobal } from "@/context/global"
import { ServerConnection, serverName } from "@/context/server"
import { displayName, projectForSession } from "@/pages/layout/helpers"
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
import type { Session } from "@opencode-ai/sdk/v2"
import type { Session } from "@/types"
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
import { TabPreviewPopover } from "./titlebar-tab-popover"
import "./titlebar-tab-nav.css"
@@ -19,7 +19,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
import { showToast } from "@/utils/toast"
import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture"
import { adjacentTabKey, mergeVisibleTabOrder } from "./titlebar-tab-order"
import type { Session } from "@opencode-ai/sdk/v2"
import type { Session } from "@/types"
function SessionTabSlot(props: {
tab: SessionTab
+1 -1
View File
@@ -1,5 +1,5 @@
import { Binary } from "@opencode-ai/core/util/binary"
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, Session } from "@/types"
import { createMemo } from "solid-js"
import { produce, reconcile, type SetStoreFunction } from "solid-js/store"
import type { createServerSdkContext } from "./server-sdk"
@@ -1,4 +1,4 @@
import type { FileContent } from "@opencode-ai/sdk/v2"
import type { FileContent } from "@/types"
const MAX_FILE_CONTENT_ENTRIES = 40
const MAX_FILE_CONTENT_BYTES = 20 * 1024 * 1024
+1 -1
View File
@@ -1,5 +1,5 @@
import { createStore, produce, reconcile } from "solid-js/store"
import type { FileNode } from "@opencode-ai/sdk/v2"
import type { FileNode } from "@/types"
type DirectoryState = {
expanded: boolean
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FileContent } from "@opencode-ai/sdk/v2"
import type { FileContent } from "@/types"
export type FileSelection = {
startLine: number
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FileNode } from "@opencode-ai/sdk/v2"
import type { FileNode } from "@/types"
type WatcherEvent = {
type: string
@@ -1,7 +1,8 @@
import { describe, expect, test } from "bun:test"
import { createStore } from "solid-js/store"
import { QueryClient } from "@tanstack/solid-query"
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
import type { Config, Project } from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise"
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import {
@@ -1,6 +1,5 @@
import type {
Config,
OpencodeClient,
Path,
PermissionRequest,
Project,
@@ -8,7 +7,8 @@ import type {
QuestionRequest,
ReferenceInfo,
Session,
} from "@opencode-ai/sdk/v2/client"
} from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type {
AgentListInput,
AgentListOutput,
@@ -1,7 +1,7 @@
import { createRoot, createSignal, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
import type { VcsInfo } from "@/types"
import {
DIR_IDLE_TTL_MS,
MAX_DIR_STORES,
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@/types"
import { createStore } from "solid-js/store"
import type { State } from "./types"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
@@ -9,7 +9,7 @@ import type {
Session,
SessionStatus,
Todo,
} from "@opencode-ai/sdk/v2/client"
} from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { SessionV2Info } from "@opencode-ai/sdk/v2/client"
import type { SessionV2Info } from "@/types"
import {
applyHomeSessionEvent,
appendHomeSessionEvent,
@@ -1,4 +1,4 @@
import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@opencode-ai/sdk/v2/client"
import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@/types"
import type { QueryClient } from "@tanstack/solid-query"
import { trimSessions } from "./session-trim"
import { pathKey } from "@/utils/path-key"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
@@ -1,4 +1,4 @@
import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, Session } from "@/types"
import { trimSessions } from "./session-trim"
const session = (input: { id: string; parentID?: string; created: number; updated?: number; archived?: number }) =>
@@ -1,4 +1,4 @@
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, Session } from "@/types"
import { cmp } from "./utils"
import { SESSION_RECENT_LIMIT, SESSION_RECENT_WINDOW } from "./types"
@@ -12,7 +12,7 @@ import type {
SessionStatus,
Todo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
} from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import type { CommandInfo, McpResource, McpServer, SessionMessageInfo } from "@opencode-ai/client/promise"
@@ -5,7 +5,7 @@ import type {
PermissionRequest,
ProviderListOutput,
} from "@opencode-ai/client/promise"
import type { Agent, Event, Project, Provider, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
import type { Agent, Event, Project, Provider, ProviderListResponse } from "@/types"
import type { Project as CurrentProject } from "@opencode-ai/client/promise"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
+1 -1
View File
@@ -7,7 +7,7 @@ import { useServerSync } from "./server-sync"
import { useServerSDK } from "./server-sdk"
import { RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server"
import { usePlatform } from "./platform"
import { Project } from "@opencode-ai/sdk/v2"
import type { Project } from "@/types"
import { normalizeProjectInfo } from "./global-sync/utils"
import { Persist, persisted, removePersisted } from "@/utils/persist"
import { pathKey } from "@/utils/path-key"
+1 -1
View File
@@ -9,7 +9,7 @@ import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { decode64 } from "@/utils/base64"
import { EventSessionError } from "@opencode-ai/sdk/v2"
import type { EventSessionError } from "@/types"
import { Persist, persisted } from "@/utils/persist"
import { playSoundById } from "@/utils/sound"
import { useGlobal } from "./global"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, Session } from "@/types"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { autoRespondsPermission, isDirectoryAutoAccepting, sessionAutoAccept } from "./permission-auto-respond"
+1 -1
View File
@@ -1,7 +1,7 @@
import { createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest } from "@/types"
import { Persist, persisted } from "@/utils/persist"
import type { ServerSDK } from "@/context/server-sdk"
import type { ServerSync } from "./server-sync"
+1 -1
View File
@@ -1,5 +1,5 @@
import { checksum } from "@opencode-ai/core/util/encode"
import type { FilePartSource } from "@opencode-ai/sdk/v2/client"
import type { FilePartSource } from "@/types"
import { batch, createMemo, type Accessor } from "solid-js"
import { createStore, type SetStoreFunction } from "solid-js/store"
import type { FileSelection } from "@/context/file"
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import { adaptServerEvent, coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import type { Event } from "@opencode-ai/sdk/v2/client"
import type { Event } from "@/types"
describe("resumeStreamAfterPageShow", () => {
test("restarts a stream only after a back-forward cache restore", () => {
+4 -4
View File
@@ -1,5 +1,5 @@
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import type { Event } from "@opencode-ai/sdk/v2/client"
import type { Event, PermissionRequest } from "@/types"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { makeEventListener } from "@solid-primitives/event-listener"
@@ -18,7 +18,7 @@ const isAbortError = (error: unknown) =>
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
export type ServerEvent = Event & { current?: OpenCodeEvent }
export type ServerEvent = Event & { id?: string; current?: OpenCodeEvent }
type QueuedServerEvent = { directory: string; payload: ServerEvent }
type CurrentDelta = Extract<
OpenCodeEvent,
@@ -41,9 +41,9 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
event.data.source?.type === "tool"
? { messageID: event.data.source.messageID, callID: event.data.source.id }
: undefined,
},
} satisfies PermissionRequest,
current: event,
} as ServerEvent
}
}
return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent
}
@@ -1,7 +1,8 @@
import { describe, expect, test } from "bun:test"
import type { retry } from "@opencode-ai/core/util/retry"
import type { OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise"
import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, Session } from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { createServerSession } from "./server-session"
import type { ServerApi } from "@/utils/server"
+2 -2
View File
@@ -3,14 +3,14 @@ import { retry } from "@opencode-ai/core/util/retry"
import type { OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise"
import type {
Message,
OpencodeClient,
Part,
PermissionRequest,
QuestionRequest,
Session,
SessionStatus,
Todo,
} from "@opencode-ai/sdk/v2/client"
} from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
+2 -2
View File
@@ -1,11 +1,11 @@
import type {
Config,
OpencodeClient,
Path,
Project,
ProviderAuthResponse,
SessionStatus,
} from "@opencode-ai/sdk/v2/client"
} from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path"
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import type { Message, Part } from "@/types"
import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "./sync"
type Text = Extract<Part, { type: "text" }>
+1 -1
View File
@@ -2,7 +2,7 @@ import { Binary } from "@opencode-ai/core/util/binary"
import { createMemo } from "solid-js"
import { useServerSync } from "./server-sync"
import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import type { Message, Part } from "@/types"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createStore, produce } from "solid-js/store"
import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist"
@@ -1,4 +1,4 @@
import type { Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useMarked } from "@opencode-ai/ui/context/marked"
@@ -1,4 +1,4 @@
import type { Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { type Accessor, createMemo, For, Show } from "solid-js"
import { Spinner } from "@opencode-ai/ui/spinner"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
+1 -1
View File
@@ -26,7 +26,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog"
import { getFilename } from "@opencode-ai/core/util/path"
import { Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { createStore, produce, reconcile } from "solid-js/store"
@@ -6,7 +6,7 @@ import {
parseDeepLink,
parseNewSessionDeepLink,
} from "./deep-links"
import { type Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import {
childSessionOnPath,
closeHomeProject,
+1 -1
View File
@@ -1,5 +1,5 @@
import { getFilename } from "@opencode-ai/core/util/path"
import { type Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { pathKey } from "@/utils/path-key"
import type { ServerConnection } from "@/context/server"
import type { HomeProjectSelection } from "@/context/layout"
@@ -1,4 +1,4 @@
import type { Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { Avatar } from "@opencode-ai/ui/avatar"
import { Icon } from "@opencode-ai/ui/icon"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
@@ -14,7 +14,7 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { type Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { type LocalProject } from "@/context/layout"
import { useServerSync, useQueryOptions } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FilePart, Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FilePart, Project, UserMessage, VcsFileDiff } from "@/types"
import { getFilename } from "@opencode-ai/core/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, QuestionRequest, Session } from "@/types"
import { todoDockAtBoundary, todoState } from "./session-composer-state"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
@@ -1,6 +1,6 @@
import { createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import type { PermissionRequest, QuestionRequest, Todo } from "@/types"
import { useParams } from "@solidjs/router"
import { showToast } from "@/utils/toast"
import { useServerSync } from "@/context/server-sync"
@@ -1,5 +1,5 @@
import { For, Show } from "solid-js"
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
import type { PermissionRequest } from "@/types"
import { Button } from "@opencode-ai/ui/button"
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon"
@@ -6,7 +6,7 @@ import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { showToast } from "@/utils/toast"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
import type { QuestionAnswer, QuestionRequest } from "@/types"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { makeEventListener } from "@solid-primitives/event-listener"
@@ -1,4 +1,4 @@
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, QuestionRequest, Session } from "@/types"
function sessionTreeRequest<T>(
session: Session[],
@@ -1,4 +1,4 @@
import type { Todo } from "@opencode-ai/sdk/v2"
import type { Todo } from "@/types"
import { AnimatedNumber } from "@opencode-ai/ui/animated-number"
import { Checkbox } from "@opencode-ai/ui/checkbox"
import { DockTray } from "@opencode-ai/ui/dock-surface"
@@ -1,7 +1,7 @@
// @ts-nocheck
import { createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { Todo } from "@opencode-ai/sdk/v2"
import type { Todo } from "@/types"
import { useServerSync } from "@/context/global-sync"
import { PromptInput } from "@/components/prompt-input"
import { usePrompt } from "@/context/prompt"
@@ -1,6 +1,6 @@
import { createEffect, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { SessionReview } from "@opencode-ai/session-ui/session-review"
import type {
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { UserMessage } from "@opencode-ai/sdk/v2"
import type { UserMessage } from "@/types"
import { resetSessionModel, restorePromptModel, syncPromptModel, syncSessionModel } from "./session-model-helpers"
const message = (input?: { agent?: string; model?: UserMessage["model"] }) =>
@@ -1,4 +1,4 @@
import type { UserMessage } from "@opencode-ai/sdk/v2"
import type { UserMessage } from "@/types"
type Local = {
session: {
@@ -23,7 +23,7 @@ import { Mark } from "@opencode-ai/ui/logo"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -51,7 +51,7 @@ import type {
Part as PartType,
ToolPart,
UserMessage,
} from "@opencode-ai/sdk/v2"
} from "@/types"
import { showToast } from "@/utils/toast"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { AssistantMessage, Message, UserMessage } from "@opencode-ai/sdk/v2"
import type { AssistantMessage, Message, UserMessage } from "@/types"
import { isTimelineReady, loadOlderTimeline, selectUserMessages, selectVisibleUserMessages } from "./model"
const user = (id: string) => ({ id, role: "user" }) as UserMessage
@@ -1,4 +1,4 @@
import type { Message, UserMessage } from "@opencode-ai/sdk/v2"
import type { Message, UserMessage } from "@/types"
import { createMemo, createResource, onCleanup, untrack, type Accessor } from "solid-js"
import { useServerSync } from "@/context/server-sync"
import { useSync } from "@/context/sync"
@@ -1,5 +1,5 @@
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@/types"
import { createMemo, type Accessor } from "solid-js"
import { reuseTimelineRows } from "./row-reconciliation"
import { Timeline, TimelineRow } from "./rows"
@@ -1,6 +1,6 @@
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import type { AssistantMessage, Part, SessionStatus, UserMessage } from "@/types"
import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part"
import { TimelineRow, type SummaryDiff } from "./timeline-row"
import { uniqueSummaryDiffs } from "./summary-diffs"
@@ -215,7 +215,7 @@ export namespace Timeline {
}
if (error) {
const data = error.data?.message
const data = error.data && "message" in error.data ? error.data.message : undefined
rows.push(
new TimelineRow.Error({
userMessageID: userMessage.id,
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff } from "@/types"
import { uniqueSummaryDiffs } from "./summary-diffs"
const diff = (file: string, additions: number) =>
@@ -1,4 +1,4 @@
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff } from "@/types"
import type { SummaryDiff } from "./timeline-row"
export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) {
@@ -1,4 +1,4 @@
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff } from "@/types"
import type { PartGroup } from "@opencode-ai/session-ui/message-part"
import { Data, Equal } from "effect"
@@ -1,6 +1,6 @@
import { useSDK } from "@/context/sdk"
import { Persist, persisted } from "@/utils/persist"
import { SessionStatus } from "@opencode-ai/sdk/v2"
import type { SessionStatus } from "@/types"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useSessionLayout } from "./session-layout"
@@ -15,7 +15,7 @@ import { showToast } from "@/utils/toast"
import { findLast } from "@opencode-ai/core/util/array"
import { createSessionTabs } from "@/pages/session/helpers"
import { extractPromptFromParts } from "@/utils/prompt"
import { UserMessage } from "@opencode-ai/sdk/v2"
import type { UserMessage } from "@/types"
import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionOwnership } from "./session-ownership"
import { useLocal } from "@/context/local"
@@ -1,4 +1,4 @@
import type { UserMessage } from "@opencode-ai/sdk/v2"
import type { UserMessage } from "@/types"
import { useLocation, useNavigate } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { messageIdFromHash } from "./message-id-from-hash"
@@ -1,4 +1,4 @@
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { Kind } from "@/components/file-tree-v2"
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
@@ -1,5 +1,5 @@
import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import {
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
+246
View File
@@ -0,0 +1,246 @@
import type {
Agent,
Config,
Event,
FileContent,
FileNode,
LspStatus,
Path,
PermissionRequest,
Project,
Provider,
ProviderAuthResponse,
ProviderListResponse,
QuestionAnswer,
QuestionRequest,
ReferenceInfo,
Session,
SessionNotFoundError,
SessionStatus,
SessionV2Info,
SnapshotFileDiff,
Todo,
V2SessionListResponse,
VcsFileDiff,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
export type {
Agent,
Config,
Event,
FileContent,
FileNode,
LspStatus,
Path,
PermissionRequest,
Project,
Provider,
ProviderAuthResponse,
ProviderListResponse,
QuestionAnswer,
QuestionRequest,
ReferenceInfo,
Session,
SessionNotFoundError,
SessionStatus,
SessionV2Info,
SnapshotFileDiff,
Todo,
V2SessionListResponse,
VcsFileDiff,
VcsInfo,
}
export type EventSessionError = Extract<Event, { type: "session.error" }>
type MessageError =
| { name: "ProviderAuthError"; data: { providerID: string; message: string } }
| { name: "UnknownError"; data: { message: string; ref?: string } }
| { name: "MessageOutputLengthError"; data: Record<string, unknown> }
| { name: "MessageAbortedError"; data: { message: string } }
| { name: "StructuredOutputError"; data: { message: string; retries: number } }
| { name: "ContextOverflowError"; data: { message: string; responseBody?: string } }
| { name: "ContentFilterError"; data: { message: string } }
| {
name: "APIError"
data: {
message: string
statusCode?: number
isRetryable: boolean
responseHeaders?: Record<string, string>
responseBody?: string
metadata?: Record<string, string>
}
}
export type UserMessage = {
id: string
sessionID: string
role: "user"
time: { created: number }
format?: { type: "text" } | { type: "json_schema"; schema: Record<string, unknown>; retryCount?: number }
summary?: { title?: string; body?: string; diffs: SnapshotFileDiff[] }
agent: string
model: { providerID: string; modelID: string; variant?: string }
system?: string
tools?: Record<string, boolean>
}
export type AssistantMessage = {
id: string
sessionID: string
role: "assistant"
time: { created: number; completed?: number }
error?: MessageError
parentID: string
modelID: string
providerID: string
mode: string
agent: string
path: { cwd: string; root: string }
summary?: boolean
cost: number
tokens: {
total?: number
input: number
output: number
reasoning: number
cache: { read: number; write: number }
}
structured?: unknown
variant?: string
finish?: string
}
export type Message = UserMessage | AssistantMessage
type PartBase = { id: string; sessionID: string; messageID: string }
export type TextPart = PartBase & {
type: "text"
text: string
synthetic?: boolean
ignored?: boolean
time?: { start: number; end?: number }
metadata?: Record<string, unknown>
}
type FilePartSourceText = { value: string; start: number; end: number }
type FileSource = { text: FilePartSourceText; type: "file"; path: string }
type SymbolSource = {
text: FilePartSourceText
type: "symbol"
path: string
range: { start: { line: number; character: number }; end: { line: number; character: number } }
name: string
kind: number
}
type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string }
export type FilePartSource = FileSource | SymbolSource | ResourceSource
export type FilePart = PartBase & {
type: "file"
mime: string
filename?: string
url: string
source?: FilePartSource
}
export type ToolState =
| { status: "pending"; input: Record<string, unknown>; raw: string }
| {
status: "running"
input: Record<string, unknown>
title?: string
metadata?: Record<string, unknown>
time: { start: number }
}
| {
status: "completed"
input: Record<string, unknown>
output: string
title: string
metadata: Record<string, unknown>
time: { start: number; end: number; compacted?: number }
attachments?: FilePart[]
}
| {
status: "error"
input: Record<string, unknown>
error: string
metadata?: Record<string, unknown>
time: { start: number; end: number }
}
export type ToolPart = PartBase & {
type: "tool"
callID: string
tool: string
state: ToolState
metadata?: Record<string, unknown>
}
export type AgentPart = PartBase & {
type: "agent"
name: string
source?: { value: string; start: number; end: number }
}
type ReasoningPart = PartBase & {
type: "reasoning"
text: string
metadata?: Record<string, unknown>
time: { start: number; end?: number }
}
type SubtaskPart = PartBase & {
type: "subtask"
prompt: string
description: string
agent: string
model?: { providerID: string; modelID: string }
command?: string
}
type StepStartPart = PartBase & { type: "step-start"; snapshot?: string }
type StepFinishPart = PartBase & {
type: "step-finish"
reason: string
snapshot?: string
cost: number
tokens: AssistantMessage["tokens"]
}
type SnapshotPart = PartBase & { type: "snapshot"; snapshot: string }
type PatchPart = PartBase & { type: "patch"; hash: string; files: string[] }
type RetryPart = PartBase & {
type: "retry"
attempt: number
error: Extract<MessageError, { name: "APIError" }>
time: { created: number }
}
type CompactionPart = PartBase & {
type: "compaction"
auto: boolean
overflow?: boolean
tail_start_id?: string
}
export type Part =
| TextPart
| SubtaskPart
| ReasoningPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| PatchPart
| AgentPart
| RetryPart
| CompactionPart
export type TextPartInput = Omit<TextPart, "id" | "sessionID" | "messageID"> & { id?: string }
export type FilePartInput = Omit<FilePart, "id" | "sessionID" | "messageID"> & { id?: string }
export type AgentPartInput = Omit<AgentPart, "id" | "sessionID" | "messageID"> & { id?: string }
+2 -2
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { Message } from "@opencode-ai/sdk/v2/client"
import type { Message } from "@/types"
import { diffs, message } from "./diffs"
const item = {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { Message } from "@opencode-ai/sdk/v2/client"
import type { Message } from "@/types"
type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Part } from "@opencode-ai/sdk/v2"
import type { Part } from "@/types"
import { extractPromptFromParts } from "./prompt"
describe("extractPromptFromParts", () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2"
import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@/types"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
type Inline =
+2 -1
View File
@@ -1,6 +1,7 @@
import type { ServerApi } from "./server"
import type { ServerProtocol } from "./server-protocol"
import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } from "@opencode-ai/sdk/v2/client"
import type { AgentPartInput, FilePartInput, Session, TextPartInput } from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type {
Project,
ProjectCurrent,
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { SessionNotFoundError } from "@opencode-ai/sdk/v2/client"
import type { SessionNotFoundError } from "@/types"
import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
import { formatServerError, isSessionNotFoundError, parseReadableConfigInvalidError } from "./server-errors"
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
SessionMessageShell,
SessionMessageUser,
} from "@opencode-ai/client/promise"
import type { AssistantMessage, FilePart, Message, Part, ToolPart, UserMessage } from "@opencode-ai/sdk/v2"
import type { AssistantMessage, FilePart, Message, Part, ToolPart, UserMessage } from "@/types"
import { Option, Schema } from "effect"
const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
+1 -1
View File
@@ -1,5 +1,5 @@
import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise"
import type { Session } from "@opencode-ai/sdk/v2/client"
import type { Session } from "@/types"
import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback"
export function normalizeSessionInfo(input: SessionInfo | Session): Session {
+1 -1
View File
@@ -61,7 +61,7 @@ for (const item of targets) {
name: "parcel-watcher-binding",
setup(build) {
build.onLoad({ filter: /filesystem\/watcher-binding\.ts$/ }, () => ({
contents: `import binding from ${JSON.stringify(parcelWatcherPackage)}; export default () => binding`,
contents: `export default () => require(${JSON.stringify(parcelWatcherPackage)})`,
loader: "js",
}))
},
+3 -6
View File
@@ -8,7 +8,7 @@ import { OPENCODE_CHANNEL, OPENCODE_VERSION } from "./version"
import { AppProcess } from "@opencode-ai/util/process"
import { randomBytes, randomUUID } from "node:crypto"
import path from "node:path"
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
import { Effect, FileSystem, Option, Redacted, Schedule, Schema } from "effect"
import { HttpServer } from "effect/unstable/http"
import { Env } from "./env"
import { ServiceConfig } from "./services/service-config"
@@ -81,7 +81,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
database: {
path:
process.env.OPENCODE_DB ??
(["latest", "beta", "prod"].includes(OPENCODE_CHANNEL) ||
(["latest", "beta", "next", "prod"].includes(OPENCODE_CHANNEL) ||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
? "opencode.db"
@@ -108,9 +108,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
gitbash: process.env.OPENCODE_GIT_BASH_PATH,
},
fs: {
filewatcher: !truthy(
process.env.OPENCODE_FILEWATCHER_DISABLE ?? process.env.OPENCODE_DISABLE_FILEWATCHER,
),
filewatcher: !truthy(process.env.OPENCODE_FILEWATCHER_DISABLE ?? process.env.OPENCODE_DISABLE_FILEWATCHER),
fff:
process.env.OPENCODE_DISABLE_FFF === undefined
? process.platform !== "win32"
@@ -128,7 +126,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
}),
},
).pipe(
Effect.provide(Logger.layer([], { mergeWithExisting: false })),
Effect.catch((error) => {
if (serviceOptions === undefined || port === undefined || !addressInUse(error)) return Effect.fail(error)
return recognizeIncumbent(serviceOptions, hostname, port).pipe(
+41 -6
View File
@@ -283,6 +283,26 @@ export type Endpoint5_26Input = {
}
export type Endpoint5_26Output =
| (
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.created"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: {
readonly sessionID: Session.ID
readonly projectID: Project.ID
readonly location: Location.Ref
readonly subpath?: RelativePath | undefined
readonly parentID?: Session.ID | undefined
readonly slug: string
readonly title?: string | undefined
readonly agent?: Agent.ID | undefined
readonly model?: Model.Ref | undefined
readonly version: string
}
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
@@ -1538,19 +1558,33 @@ export interface DebugApi<E = never> {
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
}
export type Endpoint27_0Input = {
export type Endpoint27_0Output = {
readonly status: "required" | "running" | "completed"
readonly completed: number
readonly total: number
}
export type MigrationV1StatusOperation<E = never> = () => Effect.Effect<Endpoint27_0Output, E>
export type Endpoint27_1Output = { readonly status: "completed" }
export type MigrationV1RunOperation<E = never> = () => Effect.Effect<Endpoint27_1Output, E>
export interface MigrationApi<E = never> {
readonly v1: { readonly status: MigrationV1StatusOperation<E>; readonly run: MigrationV1RunOperation<E> }
}
export type Endpoint28_0Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> }
export type WebsearchProvidersOperation<E = never> = (input?: Endpoint27_0Input) => Effect.Effect<Endpoint27_0Output, E>
export type Endpoint28_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> }
export type WebsearchProvidersOperation<E = never> = (input?: Endpoint28_0Input) => Effect.Effect<Endpoint28_0Output, E>
export type Endpoint27_1Input = {
export type Endpoint28_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly query: string
readonly providerID?: WebSearch.ID | undefined
}
export type Endpoint27_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response }
export type WebsearchQueryOperation<E = never> = (input: Endpoint27_1Input) => Effect.Effect<Endpoint27_1Output, E>
export type Endpoint28_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response }
export type WebsearchQueryOperation<E = never> = (input: Endpoint28_1Input) => Effect.Effect<Endpoint28_1Output, E>
export interface WebsearchApi<E = never> {
readonly providers: WebsearchProvidersOperation<E>
@@ -1585,5 +1619,6 @@ export interface AppApi<E = never> {
readonly projectCopy: ProjectCopyApi<E>
readonly vcs: VcsApi<E>
readonly debug: DebugApi<E>
readonly migration: MigrationApi<E>
readonly websearch: WebsearchApi<E>
}
+23 -10
View File
@@ -215,10 +215,12 @@ import type {
Endpoint26_0Output,
Endpoint26_1Input,
Endpoint26_1Output,
Endpoint27_0Input,
Endpoint27_0Output,
Endpoint27_1Input,
Endpoint27_1Output,
Endpoint28_0Input,
Endpoint28_0Output,
Endpoint28_1Input,
Endpoint28_1Output,
} from "../api/api.js"
import { ClientError } from "./client-error"
@@ -1217,22 +1219,32 @@ const adaptGroup26 = (raw: RawClient["server.debug"]) => ({
location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) },
})
const Endpoint27_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint27_0Input) =>
preserveEffect<Endpoint27_0Output>()(
const Endpoint27_0 = (raw: RawClient["server.migration"]) => () =>
preserveEffect<Endpoint27_0Output>()(raw["migration.v1.status"]({}).pipe(Effect.mapError(mapClientError)))
const Endpoint27_1 = (raw: RawClient["server.migration"]) => () =>
preserveEffect<Endpoint27_1Output>()(raw["migration.v1.run"]({}).pipe(Effect.mapError(mapClientError)))
const adaptGroup27 = (raw: RawClient["server.migration"]) => ({
v1: { status: Endpoint27_0(raw), run: Endpoint27_1(raw) },
})
const Endpoint28_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint28_0Input) =>
preserveEffect<Endpoint28_0Output>()(
raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
)
const Endpoint27_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint27_1Input) =>
preserveEffect<Endpoint27_1Output>()(
const Endpoint28_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint28_1Input) =>
preserveEffect<Endpoint28_1Output>()(
raw["websearch.query"]({
query: { location: input["location"] },
payload: { query: input["query"], providerID: input["providerID"] },
}).pipe(Effect.mapError(mapClientError)),
)
const adaptGroup27 = (raw: RawClient["server.websearch"]) => ({
providers: Endpoint27_0(raw),
query: Endpoint27_1(raw),
const adaptGroup28 = (raw: RawClient["server.websearch"]) => ({
providers: Endpoint28_0(raw),
query: Endpoint28_1(raw),
})
const adaptClient = (raw: RawClient) => ({
@@ -1263,7 +1275,8 @@ const adaptClient = (raw: RawClient) => ({
projectCopy: adaptGroup24(raw["server.projectCopy"]),
vcs: adaptGroup25(raw["server.vcs"]),
debug: adaptGroup26(raw["server.debug"]),
websearch: adaptGroup27(raw["server.websearch"]),
migration: adaptGroup27(raw["server.migration"]),
websearch: adaptGroup28(raw["server.websearch"]),
})
export const make = (options?: { readonly baseUrl?: URL | string }) =>

Some files were not shown because too many files have changed in this diff Show More