Compare commits

..

3 Commits

Author SHA1 Message Date
Aiden Cline d68de068d0 fix(core): continue after settled tool errors 2026-08-04 23:45:36 -05:00
Aiden Cline a889964d6c refactor(core): keep continuation state private 2026-08-04 23:32:57 -05:00
Aiden Cline e1d53eb588 fix(core): continue interrupted responses 2026-08-04 22:55:23 -05:00
101 changed files with 452 additions and 413 deletions
@@ -9,7 +9,7 @@ import type {
ToolPart,
ToolState,
UserMessage,
} from "../../../src/types"
} from "@opencode-ai/sdk/v2/client"
import type { SessionV1Info, SessionStatus } from "@opencode-ai/client/promise"
import { expect, type Page } from "@playwright/test"
import { Schema } from "effect"
-1
View File
@@ -12,7 +12,6 @@
"./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 "@/types"
import type { Project } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Path } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Path } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { FileNode } from "@opencode-ai/sdk/v2"
describe("buildFileTreeV2Model", () => {
test("builds a sorted tree and flattens expanded directories", () => {
@@ -1,4 +1,4 @@
import type { FileNode } from "@/types"
import type { FileNode } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { FileNode } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { FileNode } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Todo } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
export { createPromptInputHistory }
export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission }
@@ -1,5 +1,5 @@
import { getFilename } from "@opencode-ai/core/util/path"
import type { AgentPartInput, FilePartInput, Part, TextPartInput } from "@/types"
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { estimateSessionContextBreakdown } from "./session-context-breakdown"
const user = (id: string) => {
@@ -1,4 +1,4 @@
import type { Message, Part } from "@/types"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
export type SessionContextBreakdownKey = "system" | "user" | "assistant" | "tool" | "other"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Message } from "@/types"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { getSessionContext } from "./session-context-metrics"
const assistant = (
@@ -1,4 +1,4 @@
import type { AssistantMessage, Message } from "@/types"
import type { AssistantMessage, Message } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers"
import { useSDK } from "@/context/sdk"
@@ -1,4 +1,4 @@
import type { LspStatus } from "@/types"
import type { LspStatus } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Session } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { Session } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { FileContent } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { FileNode } from "@opencode-ai/sdk/v2"
type DirectoryState = {
expanded: boolean
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FileContent } from "@/types"
import type { FileContent } from "@opencode-ai/sdk/v2"
export type FileSelection = {
startLine: number
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FileNode } from "@/types"
import type { FileNode } from "@opencode-ai/sdk/v2"
type WatcherEvent = {
type: string
@@ -1,8 +1,7 @@
import { describe, expect, test } from "bun:test"
import { createStore } from "solid-js/store"
import { QueryClient } from "@tanstack/solid-query"
import type { Config, Project } from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { Config, OpencodeClient, Project } 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,5 +1,6 @@
import type {
Config,
OpencodeClient,
Path,
PermissionRequest,
Project,
@@ -7,8 +8,7 @@ import type {
QuestionRequest,
ReferenceInfo,
Session,
} from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
} 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 "@/types"
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
} from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { SessionV2Info } from "@opencode-ai/sdk/v2/client"
import {
applyHomeSessionEvent,
appendHomeSessionEvent,
@@ -1,4 +1,4 @@
import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@/types"
import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { cmp } from "./utils"
import { SESSION_RECENT_LIMIT, SESSION_RECENT_WINDOW } from "./types"
@@ -12,7 +12,7 @@ import type {
SessionStatus,
Todo,
VcsInfo,
} from "@/types"
} from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Agent, Event, Project, Provider, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
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 type { Project } from "@/types"
import { Project } from "@opencode-ai/sdk/v2"
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 type { EventSessionError } from "@/types"
import { EventSessionError } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { FilePartSource } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Event } from "@opencode-ai/sdk/v2/client"
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, PermissionRequest } from "@/types"
import type { Event } from "@opencode-ai/sdk/v2/client"
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 & { id?: string; current?: OpenCodeEvent }
export type ServerEvent = Event & { 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,8 +1,7 @@
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, Part, Session } from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { Message, OpencodeClient, Part, Session } 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 "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
} 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 "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
} 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 "@/types"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Session } from "@/types"
import type { Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Session } from "@opencode-ai/sdk/v2/client"
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 type { Session } from "@/types"
import { Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import { type Session } from "@opencode-ai/sdk/v2/client"
import {
childSessionOnPath,
closeHomeProject,
+1 -1
View File
@@ -1,5 +1,5 @@
import { getFilename } from "@opencode-ai/core/util/path"
import type { Session } from "@/types"
import { type Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import { type Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { FilePart, Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
function sessionTreeRequest<T>(
session: Session[],
@@ -1,4 +1,4 @@
import type { Todo } from "@/types"
import type { Todo } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { Todo } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
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 "@/types"
} from "@opencode-ai/sdk/v2"
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 "@/types"
import type { AssistantMessage, Message, UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { Message, UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
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 type { AssistantMessage, Part, SessionStatus, UserMessage } from "@/types"
import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
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" in error.data ? error.data.message : undefined
const data = error.data?.message
rows.push(
new TimelineRow.Error({
userMessageID: userMessage.id,
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { SnapshotFileDiff } from "@/types"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import { uniqueSummaryDiffs } from "./summary-diffs"
const diff = (file: string, additions: number) =>
@@ -1,4 +1,4 @@
import type { SnapshotFileDiff } from "@/types"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { SummaryDiff } from "./timeline-row"
export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) {
@@ -1,4 +1,4 @@
import type { SnapshotFileDiff } from "@/types"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
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 type { SessionStatus } from "@/types"
import { SessionStatus } from "@opencode-ai/sdk/v2"
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 type { UserMessage } from "@/types"
import { UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import {
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
-246
View File
@@ -1,246 +0,0 @@
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 "@/types"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { Message } from "@/types"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { diffs, message } from "./diffs"
const item = {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import type { Message } from "@/types"
import type { Message } from "@opencode-ai/sdk/v2/client"
type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Part } from "@/types"
import type { Part } from "@opencode-ai/sdk/v2"
import { extractPromptFromParts } from "./prompt"
describe("extractPromptFromParts", () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@/types"
import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
type Inline =
+1 -2
View File
@@ -1,7 +1,6 @@
import type { ServerApi } from "./server"
import type { ServerProtocol } from "./server-protocol"
import type { AgentPartInput, FilePartInput, Session, TextPartInput } from "@/types"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } 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 "@/types"
import type { SessionNotFoundError } from "@opencode-ai/sdk/v2/client"
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 "@/types"
import type { AssistantMessage, FilePart, Message, Part, ToolPart, UserMessage } from "@opencode-ai/sdk/v2"
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 "@/types"
import type { Session } from "@opencode-ai/sdk/v2/client"
import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback"
export function normalizeSessionInfo(input: SessionInfo | Session): Session {
-3
View File
@@ -7,7 +7,6 @@ import { Agent } from "../agent"
import { Catalog } from "../catalog"
import { Command } from "../command"
import { Config } from "../config"
import { Credential } from "../credential"
import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command"
import { ConfigProviderPlugin } from "../config/plugin/provider"
@@ -68,7 +67,6 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const catalog = yield* Catalog.Service
const command = yield* Command.Service
const config = yield* Config.Service
const credential = yield* Credential.Service
const bus = yield* Bus.Service
const mutation = yield* FileMutation.Service
const formatter = yield* Formatter.Service
@@ -100,7 +98,6 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Catalog.Service, catalog),
Context.make(Command.Service, command),
Context.make(Config.Service, config),
Context.make(Credential.Service, credential),
Context.make(Bus.Service, bus),
Context.make(FileMutation.Service, mutation),
Context.make(Formatter.Service, formatter),
+130 -15
View File
@@ -1,18 +1,30 @@
import { createServer } from "node:http"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Clock, Effect, Option, Schema } from "effect"
import { Clock, Deferred, Effect, Option, Schema } from "effect"
import { App } from "../../app"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { OauthCallbackPage } from "../../oauth/page"
const clientID = "b1a00492-073a-47ea-816f-4c329264a828"
const issuer = "https://auth.x.ai/oauth2"
const deviceGrant = "urn:ietf:params:oauth:grant-type:device_code"
const scope = "openid profile email offline_access grok-cli:access api:access"
const callbackHost = "127.0.0.1"
const callbackPort = 56121
const callbackPath = "/callback"
const redirectURI = `http://${callbackHost}:${callbackPort}${callbackPath}`
const pollingSafetyMargin = 3000
const corsOrigins = new Set(["https://accounts.x.ai", "https://auth.x.ai"])
const browserMethodID = Integration.MethodID.make("browser")
const deviceMethodID = Integration.MethodID.make("device")
type Pkce = {
verifier: string
challenge: string
}
const Token = Schema.Struct({
access_token: Schema.String,
refresh_token: Schema.optional(Schema.String),
@@ -35,12 +47,80 @@ const DeviceError = Schema.Struct({
})
const decodeDeviceError = Schema.decodeUnknownOption(Schema.fromJsonString(DeviceError))
const browser = (app: App.Info) => ({
integrationID: Integration.ID.make("xai"),
method: {
id: browserMethodID,
type: "oauth",
label: "xAI Grok OAuth (SuperGrok Subscription)",
},
authorize: () =>
Effect.gen(function* () {
const pkce = yield* Effect.promise(generatePKCE)
const state = randomString(32)
const code = yield* Deferred.make<string, Error>()
const server = createServer((request, response) => {
const url = new URL(request.url ?? "/", redirectURI)
const origin = request.headers.origin
if (origin && corsOrigins.has(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin)
response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS")
response.setHeader("Access-Control-Allow-Headers", "Content-Type")
response.setHeader("Access-Control-Allow-Private-Network", "true")
response.setHeader("Vary", "Origin")
}
if (request.method === "OPTIONS") {
response.writeHead(204).end()
return
}
if (url.pathname !== callbackPath) {
response.writeHead(404).end("Not found")
return
}
const error = url.searchParams.get("error_description") ?? url.searchParams.get("error")
const value = url.searchParams.get("code")
if (error) {
Effect.runFork(Deferred.fail(code, new Error(error)))
response
.writeHead(400, { "Content-Type": "text/html" })
.end(OauthCallbackPage.error(error, { provider: "xAI" }))
return
}
if (!value || url.searchParams.get("state") !== state) {
const message = value ? "Invalid OAuth state" : "Missing authorization code"
Effect.runFork(Deferred.fail(code, new Error(message)))
response
.writeHead(400, { "Content-Type": "text/html" })
.end(OauthCallbackPage.error(message, { provider: "xAI" }))
return
}
Effect.runFork(Deferred.succeed(code, value))
response.writeHead(200, { "Content-Type": "text/html" }).end(OauthCallbackPage.success({ provider: "xAI" }))
})
yield* Effect.callback<void, Error>((resume) => {
server.once("error", (error) => resume(Effect.fail(error)))
server.listen(callbackPort, callbackHost, () => resume(Effect.void))
})
yield* Effect.addFinalizer(() => Effect.sync(() => server.close()))
return {
mode: "auto" as const,
url: authorizeURL(pkce, state, randomString(32)),
instructions: "Complete authorization in your browser. This window will close automatically.",
callback: Deferred.await(code).pipe(
Effect.flatMap((value) => exchange(value, pkce, app)),
Effect.flatMap((tokens) => credential(browserMethodID, tokens)),
),
}
}),
refresh: (value) => refresh(browserMethodID, Credential.OAuth.make({ ...value, methodID: browserMethodID }), app),
}) satisfies IntegrationOAuthMethodRegistration
const device = (app: App.Info) => ({
integrationID: Integration.ID.make("xai"),
method: {
id: deviceMethodID,
type: "oauth",
label: "SuperGrok Subscription",
label: "xAI Grok OAuth (Headless / Remote / VPS)",
},
authorize: () =>
request(
@@ -48,7 +128,7 @@ const device = (app: App.Info) => ({
{
method: "POST",
headers: headers(app),
body: new URLSearchParams({ client_id: clientID, scope, referrer: "opencode" }).toString(),
body: new URLSearchParams({ client_id: clientID, scope }).toString(),
},
Device,
).pipe(
@@ -73,28 +153,35 @@ const device = (app: App.Info) => ({
export const XAIPlugin = define({
id: "opencode.provider.xai",
effect: Effect.fn(function* (ctx) {
const credentials = yield* Credential.Service
yield* Effect.forEach(
yield* credentials.list(Integration.ID.make("xai")),
(credential) => {
if (credential.value.type !== "oauth" || credential.value.methodID !== browserMethodID) return Effect.void
return credentials.update(credential.id, {
value: Credential.OAuth.make({ ...credential.value, methodID: deviceMethodID }),
})
},
{ discard: true },
)
yield* ctx.integration.transform((draft) => {
draft.update("xai", (integration) => {
integration.name = "xAI"
})
draft.method.update(browser(ctx.app))
draft.method.update(device(ctx.app))
draft.method.update({ integrationID: "xai", method: { type: "key", label: "Manually enter API Key" } })
})
}),
})
function exchange(code: string, pkce: Pkce, app: App.Info) {
return request(
`${issuer}/token`,
{
method: "POST",
headers: headers(app),
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: redirectURI,
client_id: clientID,
code_verifier: pkce.verifier,
}).toString(),
},
Token,
)
}
function refresh(methodID: Integration.MethodID, value: Credential.OAuth, app: App.Info) {
return request(
`${issuer}/token`,
@@ -223,3 +310,31 @@ function positiveSeconds(value: unknown, fallback: number) {
const seconds = Number(value)
return Number.isFinite(seconds) && seconds > 0 ? seconds : fallback
}
async function generatePKCE(): Promise<Pkce> {
const verifier = randomString(64)
const challenge = Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))).toString(
"base64url",
)
return { verifier, challenge }
}
function randomString(length: number) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
return Array.from(crypto.getRandomValues(new Uint8Array(length)), (byte) => chars[byte % chars.length]).join("")
}
function authorizeURL(pkce: Pkce, state: string, nonce: string) {
return `${issuer}/authorize?${new URLSearchParams({
response_type: "code",
client_id: clientID,
redirect_uri: redirectURI,
scope,
code_challenge: pkce.challenge,
code_challenge_method: "S256",
state,
nonce,
plan: "generic",
referrer: "opencode",
}).toString()}`
}
-2
View File
@@ -10,7 +10,6 @@ import { Catalog } from "../catalog"
import { Command } from "../command"
import { Config } from "../config"
import { ConfigPlugin } from "../config/plugin"
import { Credential } from "../credential"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { Bus } from "../bus"
@@ -318,7 +317,6 @@ export const node = makeLocationNode({
Catalog.node,
Command.node,
Config.node,
Credential.node,
Bus.node,
FileMutation.node,
Formatter.node,
+28
View File
@@ -37,6 +37,7 @@ import { SessionUsage } from "../usage"
type CallOutcome = Data.TaggedEnum<{
Completed: { readonly needsContinuation: boolean; readonly step: number }
Retry: { readonly step: number }
Continue: { readonly cause: AIError; readonly error: SessionRunnerRetry.RetryableFailure["error"]; readonly step: number }
Restart: { readonly step: number; readonly recoveredOverflow: boolean }
}>
const CallOutcome = Data.taggedEnum<CallOutcome>()
@@ -91,6 +92,8 @@ const classifyToolExits = (
const TOOLS_INTERRUPTED = { type: "aborted", message: "Tool execution interrupted" } as const
const STEP_INTERRUPTED = { type: "aborted", message: "Step interrupted" } as const
const RESULT_MISSING = { type: "tool.result-missing", message: "Provider did not return a tool result" } as const
const CONTINUE_AFTER_INCOMPLETE_STREAM =
"The previous response was interrupted. Continue from where you left off without repeating completed content."
const layer = Layer.effect(
Service,
@@ -187,6 +190,20 @@ const layer = Layer.effect(
assistantMessageID,
).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry))
if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step }
if (outcome._tag === "Continue") {
yield* retry(
new SessionRunnerRetry.RetryableFailure({
cause: outcome.cause,
error: outcome.error,
step: outcome.step,
}),
).pipe(Pull.catchDone(() => Effect.fail(outcome.cause)))
yield* bus.publish(SessionEvent.Synthetic, {
sessionID,
text: CONTINUE_AFTER_INCOMPLETE_STREAM,
})
assistantMessageID = SessionMessage.ID.create()
}
if (outcome._tag === "Restart") {
if (outcome.recoveredOverflow) recoverOverflow = false
assistantMessageID = SessionMessage.ID.create()
@@ -426,6 +443,17 @@ const layer = Layer.effect(
})
}
const incompleteStream =
llmFailure?.reason._tag === "InvalidProviderOutput" &&
llmFailure.reason.classification === "incomplete-stream"
const toolsAllowContinuation = tools.declines.length === 0 && !tools.interrupted
if (llmError && incompleteStream && record.outputStarted && toolsAllowContinuation)
return CallOutcome.Continue({
cause: llmFailure,
error: llmError,
step: currentStep,
})
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
if (tools.declines.length > 0) return yield* Effect.interrupt
if (tools.interrupted && tools.failure) return yield* Effect.failCause(tools.failure)
+7 -35
View File
@@ -1,6 +1,5 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
@@ -17,52 +16,25 @@ const addPlugin = Effect.fn(function* () {
})
describe("XAIPlugin", () => {
it.effect("registers device OAuth and API key methods", () =>
it.effect("registers browser OAuth, device OAuth, and API key methods", () =>
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const integration = yield* integrations.get(Integration.ID.make("xai"))
expect(integration?.name).toBe("xAI")
expect(integration?.methods).toEqual([
{
id: Integration.MethodID.make("browser"),
type: "oauth",
label: "xAI Grok OAuth (SuperGrok Subscription)",
},
{
id: Integration.MethodID.make("device"),
type: "oauth",
label: "SuperGrok Subscription",
label: "xAI Grok OAuth (Headless / Remote / VPS)",
},
{ type: "key", label: "Manually enter API Key" },
])
}),
)
it.effect("migrates browser OAuth credentials to the device method", () =>
Effect.gen(function* () {
const credentials = yield* Credential.Service
const original = yield* credentials.create({
integrationID: Integration.ID.make("xai"),
label: "personal",
value: Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("browser"),
access: "access",
refresh: "refresh",
expires: 123,
metadata: { account: "account" },
}),
})
yield* addPlugin()
expect(yield* credentials.get(original.id)).toEqual({
...original,
value: Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("device"),
access: "access",
refresh: "refresh",
expires: 123,
metadata: { account: "account" },
}),
})
}),
)
})

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