Просмотр исходного кода

refactor(app): use native v2 types (#40608)

Brendan Allan 1 неделя назад
Родитель
Сommit
25c2150baf
43 измененных файлов с 134 добавлено и 215 удалено
  1. 1 1
      packages/app/src/components/prompt-input-v2.tsx
  2. 1 1
      packages/app/src/components/prompt-input.tsx
  3. 5 2
      packages/app/src/components/prompt-input/build-request-parts.ts
  4. 4 5
      packages/app/src/context/global-sync/bootstrap.ts
  5. 6 5
      packages/app/src/context/global-sync/event-reducer.test.ts
  6. 1 4
      packages/app/src/context/global-sync/event-reducer.ts
  7. 3 5
      packages/app/src/context/global-sync/home-session-index.test.ts
  8. 7 6
      packages/app/src/context/global-sync/home-session-index.ts
  9. 2 1
      packages/app/src/context/global-sync/session-cache.test.ts
  10. 2 1
      packages/app/src/context/global-sync/session-cache.ts
  11. 2 1
      packages/app/src/context/global-sync/session-trim.test.ts
  12. 2 1
      packages/app/src/context/global-sync/session-trim.ts
  13. 7 5
      packages/app/src/context/global-sync/types.ts
  14. 1 25
      packages/app/src/context/global-sync/utils.test.ts
  15. 1 18
      packages/app/src/context/global-sync/utils.ts
  16. 3 3
      packages/app/src/context/notification.tsx
  17. 2 1
      packages/app/src/context/permission-auto-respond.test.ts
  18. 2 5
      packages/app/src/context/permission.tsx
  19. 5 9
      packages/app/src/context/server-sdk.test.ts
  20. 1 20
      packages/app/src/context/server-sdk.tsx
  21. 1 4
      packages/app/src/context/server-session.ts
  22. 2 2
      packages/app/src/context/server-sync.tsx
  23. 4 3
      packages/app/src/pages/session.tsx
  24. 2 1
      packages/app/src/pages/session/composer/session-composer-state.test.ts
  25. 2 1
      packages/app/src/pages/session/composer/session-composer-state.ts
  26. 4 4
      packages/app/src/pages/session/composer/session-permission-dock.tsx
  27. 1 1
      packages/app/src/pages/session/composer/session-question-dock.tsx
  28. 2 1
      packages/app/src/pages/session/composer/session-request-tree.ts
  29. 1 2
      packages/app/src/pages/session/review-tab.tsx
  30. 2 3
      packages/app/src/pages/session/session-side-panel.tsx
  31. 2 2
      packages/app/src/pages/session/timeline/projection.ts
  32. 2 2
      packages/app/src/pages/session/timeline/rows.ts
  33. 5 6
      packages/app/src/pages/session/timeline/summary-diffs.test.ts
  34. 3 3
      packages/app/src/pages/session/timeline/summary-diffs.ts
  35. 2 2
      packages/app/src/pages/session/timeline/timeline-row.ts
  36. 1 1
      packages/app/src/pages/session/usage-exceeded-dialogs.tsx
  37. 10 4
      packages/app/src/pages/session/v2/review-diff-kinds.test.ts
  38. 2 3
      packages/app/src/pages/session/v2/review-diff-kinds.ts
  39. 1 2
      packages/app/src/pages/session/v2/review-panel-v2.tsx
  40. 24 44
      packages/app/src/types.ts
  41. 1 2
      packages/app/src/utils/diffs.test.ts
  42. 1 2
      packages/app/src/utils/diffs.ts
  43. 1 1
      packages/app/src/utils/server-errors.test.ts

+ 1 - 1
packages/app/src/components/prompt-input-v2.tsx

@@ -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/client/promise"
 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 - 1
packages/app/src/components/prompt-input.tsx

@@ -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/client/promise"
 
 export { createPromptInputHistory }
 export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission }

+ 5 - 2
packages/app/src/components/prompt-input/build-request-parts.ts

@@ -1,12 +1,15 @@
 import { getFilename } from "@opencode-ai/core/util/path"
-import type { AgentPartInput, FilePartInput, Part, TextPartInput } from "@/types"
+import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@/types"
 import type { FileSelection } from "@/context/file"
 import { encodeFilePath } from "@/context/file/path"
 import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
 import { Identifier } from "@/utils/id"
 import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note"
 
-type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string }
+type PromptRequestPart =
+  | (Omit<TextPart, "id" | "sessionID" | "messageID"> & { id: string })
+  | (Omit<FilePart, "id" | "sessionID" | "messageID"> & { id: string })
+  | (Omit<MessageAgentPart, "id" | "sessionID" | "messageID"> & { id: string })
 
 type ContextFile = {
   key: string

+ 4 - 5
packages/app/src/context/global-sync/bootstrap.ts

@@ -1,11 +1,8 @@
 import type {
   Config,
   Path,
-  PermissionRequest,
   Project,
   ProviderAuthResponse,
-  QuestionRequest,
-  ReferenceInfo,
   Session,
 } from "@/types"
 import type {
@@ -17,11 +14,14 @@ import type {
   CommandListOutput,
   LocationGetInput,
   LocationGetOutput,
+  PermissionRequest,
   ProjectCurrentInput,
   ProjectCurrentOutput,
   ProjectListOutput,
   ReferenceListInput,
   ReferenceListOutput,
+  ReferenceInfo,
+  QuestionRequest,
   SessionApi,
 } from "@opencode-ai/client/promise"
 import { showToast } from "@/utils/toast"
@@ -34,7 +34,6 @@ import type { ServerSession } from "../server-session"
 import {
   cmp,
   normalizeAgentList,
-  normalizePermissionRequest,
   normalizeProjectInfo,
   normalizeProviderList,
 } from "./utils"
@@ -368,7 +367,7 @@ export async function bootstrapDirectory(input: {
         retry(() =>
           input.api.permission.request
             .list({ location: { directory: input.directory } })
-            .then((result) => result.data.map(normalizePermissionRequest))
+            .then((result) => result.data)
             .then((permissions) => {
             const ids = permissions.map((permission) => permission.sessionID)
             const grouped = groupBySession(

+ 6 - 5
packages/app/src/context/global-sync/event-reducer.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
-import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@/types"
+import type { Message, Part, Project, Session } from "@/types"
+import type { PermissionRequest, QuestionRequest } from "@opencode-ai/client/promise"
 import { createStore } from "solid-js/store"
 import type { State } from "./types"
 import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
@@ -38,10 +39,10 @@ const permissionRequest = (id: string, sessionID: string, title = id) =>
   ({
     id,
     sessionID,
-    permission: title,
-    patterns: ["*"],
+    action: title,
+    resources: ["*"],
     metadata: {},
-    always: [],
+    save: [],
   }) as PermissionRequest
 
 const questionRequest = (id: string, sessionID: string, title = id) =>
@@ -512,7 +513,7 @@ describe("applyDirectoryEvent", () => {
       directory: "/tmp",
       loadLsp() {},
     })
-    expect(store.permission[sessionID]?.find((x) => x.id === "perm_2")?.permission).toBe("updated")
+    expect(store.permission[sessionID]?.find((x) => x.id === "perm_2")?.action).toBe("updated")
 
     applyDirectoryEvent({
       event: { type: "permission.replied", properties: { sessionID, requestID: "perm_2" } },

+ 1 - 4
packages/app/src/context/global-sync/event-reducer.ts

@@ -3,14 +3,11 @@ import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/
 import type {
   Message,
   Part,
-  PermissionRequest,
   Project,
-  QuestionRequest,
   Session,
-  SessionStatus,
   Todo,
 } from "@/types"
-import type { FileDiffInfo } from "@opencode-ai/client/promise"
+import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
 import type { State, VcsCache } from "./types"
 import { trimSessions } from "./session-trim"
 import { dropSessionCaches } from "./session-cache"

+ 3 - 5
packages/app/src/context/global-sync/home-session-index.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import type { SessionV2Info } from "@/types"
+import type { SessionInfo } from "@opencode-ai/client/promise"
 import {
   applyHomeSessionEvent,
   appendHomeSessionEvent,
@@ -48,9 +48,7 @@ describe("Home V2 session index", () => {
         calls.push({ input, signal: options.signal })
         if (!("cursor" in input)) {
           return {
-            data: Array.from({ length: HOME_V2_SESSION_PAGE_LIMIT }, (_, index) =>
-              session({ id: `page-1-${index}` }),
-            ),
+            data: Array.from({ length: HOME_V2_SESSION_PAGE_LIMIT }, (_, index) => session({ id: `page-1-${index}` })),
             cursor: { next: "next-page" },
           }
         }
@@ -74,7 +72,7 @@ describe("Home V2 session index", () => {
     const activeNull = {
       ...session({ id: "active-null", updated: 20 }),
       time: { created: 1, updated: 20, archived: null },
-    } as unknown as SessionV2Info
+    } as unknown as SessionInfo
     const result = parseHomeSessionIndex([
       session({ id: "root", updated: 30 }),
       activeNull,

+ 7 - 6
packages/app/src/context/global-sync/home-session-index.ts

@@ -1,4 +1,5 @@
-import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@/types"
+import type { Event, Session } from "@/types"
+import type { SessionInfo, SessionsResponse } from "@opencode-ai/client/promise"
 import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback"
 import type { QueryClient } from "@tanstack/solid-query"
 import { trimSessions } from "./session-trim"
@@ -26,11 +27,11 @@ export async function loadHomeSessionIndex(
   list: (
     input: { limit: number; order: "desc"; cursor?: string },
     options: { signal?: AbortSignal },
-  ) => Promise<V2SessionListResponse>,
+  ) => Promise<SessionsResponse>,
   eventSequence = 0,
   signal?: AbortSignal,
 ) {
-  const data: SessionV2Info[] = []
+  const data: SessionInfo[] = []
   let cursor: string | undefined
 
   for (;;) {
@@ -126,7 +127,7 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
 // multiple directories. A bounded page could omit an old session updated today.
 // Once released, use client.v2.project.list() and client.v2.session.list({
 // parentID: null, order: "desc" }), then remove this adapter and its V1 fields.
-export function parseHomeSessionIndex(sessions: SessionV2Info[]): Session[] {
+export function parseHomeSessionIndex(sessions: SessionInfo[]): Session[] {
   return sessions.flatMap((item) => {
     if (item.parentID || typeof item.time.archived === "number") return []
     return [toLegacySummary(item)]
@@ -141,7 +142,7 @@ export function retainHomeSessions(sessions: Session[], limit: number, now: numb
 export function applyHomeSessionEvent(sessions: Session[], event: HomeSessionEvent) {
   const info = event.properties.info
   const index = sessions.findIndex((session) => session.id === info.id)
-    if (event.type === "session.deleted" || info.parentID || typeof info.time.archived === "number") {
+  if (event.type === "session.deleted" || info.parentID || typeof info.time.archived === "number") {
     if (index === -1) return sessions
     return sessions.toSpliced(index, 1)
   }
@@ -150,7 +151,7 @@ export function applyHomeSessionEvent(sessions: Session[], event: HomeSessionEve
   return sessions.with(index, info)
 }
 
-function toLegacySummary(session: SessionV2Info): Session {
+function toLegacySummary(session: SessionInfo): Session {
   return {
     id: session.id,
     slug: session.id,

+ 2 - 1
packages/app/src/context/global-sync/session-cache.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
-import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@/types"
+import type { Message, Part, Todo } from "@/types"
+import type { PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
 

+ 2 - 1
packages/app/src/context/global-sync/session-cache.ts

@@ -1,4 +1,5 @@
-import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@/types"
+import type { Message, Part, Todo } from "@/types"
+import type { PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import type { SessionMessageInfo } from "@opencode-ai/client/promise"
 

+ 2 - 1
packages/app/src/context/global-sync/session-trim.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
-import type { PermissionRequest, Session } from "@/types"
+import type { Session } from "@/types"
+import type { PermissionRequest } from "@opencode-ai/client/promise"
 import { trimSessions } from "./session-trim"
 
 const session = (input: { id: string; parentID?: string; created: number; updated?: number; archived?: number }) =>

+ 2 - 1
packages/app/src/context/global-sync/session-trim.ts

@@ -1,4 +1,5 @@
-import type { PermissionRequest, Session } from "@/types"
+import type { Session } from "@/types"
+import type { PermissionRequest } from "@opencode-ai/client/promise"
 import { cmp } from "./utils"
 import { SESSION_RECENT_LIMIT, SESSION_RECENT_WINDOW } from "./types"
 

+ 7 - 5
packages/app/src/context/global-sync/types.ts

@@ -5,15 +5,17 @@ import type {
   Message,
   Part,
   Path,
-  PermissionRequest,
-  QuestionRequest,
-  ReferenceInfo,
   Session,
-  SessionStatus,
   Todo,
   VcsInfo,
 } from "@/types"
-import type { FileDiffInfo } from "@opencode-ai/client/promise"
+import type {
+  FileDiffInfo,
+  PermissionRequest,
+  QuestionRequest,
+  ReferenceInfo,
+  SessionStatus,
+} from "@opencode-ai/client/promise"
 import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 import type { CommandInfo, McpResource, McpServer, SessionMessageInfo } from "@opencode-ai/client/promise"
 import type { Accessor } from "solid-js"

+ 1 - 25
packages/app/src/context/global-sync/utils.test.ts

@@ -5,7 +5,7 @@ import type {
   ModelListOutput,
   ProviderListOutput,
 } from "@opencode-ai/client/promise"
-import { directoryKey, normalizeAgentList, normalizePermissionRequest, normalizeProviderList } from "./utils"
+import { directoryKey, normalizeAgentList, normalizeProviderList } from "./utils"
 
 describe("normalizeAgentList", () => {
   test("adapts current agents to the app agent shape", () => {
@@ -43,30 +43,6 @@ describe("normalizeAgentList", () => {
   })
 })
 
-describe("normalizePermissionRequest", () => {
-  test("adapts the current permission request to app state", () => {
-    expect(
-      normalizePermissionRequest({
-        id: "permission-1",
-        sessionID: "session-1",
-        action: "read",
-        resources: ["README.md"],
-        save: ["*.md"],
-        metadata: { path: "README.md" },
-        source: { type: "tool", messageID: "message-1", id: "call-1" },
-      }),
-    ).toEqual({
-      id: "permission-1",
-      sessionID: "session-1",
-      permission: "read",
-      patterns: ["README.md"],
-      always: ["*.md"],
-      metadata: { path: "README.md" },
-      tool: { messageID: "message-1", callID: "call-1" },
-    })
-  })
-})
-
 describe("normalizeProviderList", () => {
   test("groups current models into the app provider catalog", () => {
     const result = normalizeProviderList(

+ 1 - 18
packages/app/src/context/global-sync/utils.ts

@@ -2,10 +2,9 @@ import type {
   AgentListOutput,
   ModelDefaultOutput,
   ModelListOutput,
-  PermissionRequest,
   ProviderListOutput,
 } from "@opencode-ai/client/promise"
-import type { Agent, Event, Project, Provider, ProviderListResponse } from "@/types"
+import type { Agent, 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"
@@ -36,22 +35,6 @@ export function normalizeAgentList(input: AgentListOutput["data"] | Agent[]): Ag
   }))
 }
 
-type LegacyPermissionRequest = Extract<Event, { type: "permission.asked" }>["properties"]
-
-export function normalizePermissionRequest(input: PermissionRequest | LegacyPermissionRequest): LegacyPermissionRequest {
-  if ("permission" in input) return input
-  return {
-    id: input.id,
-    sessionID: input.sessionID,
-    permission: input.action,
-    patterns: input.resources,
-    always: input.save ?? [],
-    metadata: input.metadata ?? {},
-    tool:
-      input.source?.type === "tool" ? { messageID: input.source.messageID, callID: input.source.id } : undefined,
-  }
-}
-
 export function normalizeProviderList(
   providers: ProviderListOutput["data"] | ProviderListResponse,
   models?: ModelListOutput["data"],

+ 3 - 3
packages/app/src/context/notification.tsx

@@ -360,7 +360,7 @@ function createServerNotificationState(input: {
 
   const handleSessionError = (
     directory: string,
-    event: { properties: { sessionID?: string; error?: EventSessionError["properties"]["error"] } },
+    event: { properties: EventSessionError["properties"] },
     time: number,
   ) => {
     const sessionID = event.properties.sessionID
@@ -372,7 +372,7 @@ function createServerNotificationState(input: {
         void playSoundById(settings.sounds.errors())
       }
 
-      const error = "error" in event.properties ? event.properties.error : undefined
+      const error = event.properties.error
       append({
         directory,
         time,
@@ -393,7 +393,7 @@ function createServerNotificationState(input: {
 
   const unsub = serverSDK().event.listen((e) => {
     const event = e.details
-    if (event.type !== "session.idle" && event.type !== "session.error") return
+    if (event.type !== "session.idle" && event.type !== "session.execution.failed") return
 
     const directory = e.name
     const time = Date.now()

+ 2 - 1
packages/app/src/context/permission-auto-respond.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
-import type { PermissionRequest, Session } from "@/types"
+import type { Session } from "@/types"
+import type { PermissionRequest } from "@opencode-ai/client/promise"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { autoRespondsPermission, isDirectoryAutoAccepting, sessionAutoAccept } from "./permission-auto-respond"
 

+ 2 - 5
packages/app/src/context/permission.tsx

@@ -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/client/promise"
 import { Persist, persisted } from "@/utils/persist"
 import type { ServerSDK } from "@/context/server-sdk"
 import type { ServerSync } from "./server-sync"
@@ -13,7 +13,6 @@ import { type DraftTab, useTabs } from "./tabs"
 import { useSettings } from "./settings"
 import { requireServerKey } from "@/utils/session-route"
 import type { ServerScope } from "@/utils/server-scope"
-import { normalizePermissionRequest } from "./global-sync/utils"
 import {
   acceptKey,
   directoryAcceptKey,
@@ -256,9 +255,7 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
   }
 
   const list = async (directory: string) => {
-    return input.sdk.api.permission.request
-      .list({ location: { directory } })
-      .then((result) => result.data.map(normalizePermissionRequest))
+    return input.sdk.api.permission.request.list({ location: { directory } }).then((result) => result.data)
   }
 
   function respondOnce(permission: PermissionRequest, directory?: string) {

+ 5 - 9
packages/app/src/context/server-sdk.test.ts

@@ -15,7 +15,7 @@ describe("resumeStreamAfterPageShow", () => {
 })
 
 describe("adaptServerEvent", () => {
-  test("preserves current events while adapting permission requests for existing consumers", () => {
+  test("preserves current permission requests", () => {
     const current = {
       id: "evt_1",
       created: 1,
@@ -35,9 +35,9 @@ describe("adaptServerEvent", () => {
       properties: {
         id: "perm_1",
         sessionID: "ses_1",
-        permission: "read",
-        patterns: ["src/**"],
-        tool: { messageID: "msg_1", callID: "call_1" },
+        action: "read",
+        resources: ["src/**"],
+        source: { type: "tool", messageID: "msg_1", id: "call_1" },
       },
       current,
     })
@@ -87,11 +87,7 @@ describe("current event buffering", () => {
   test("preserves boundaries between distinct delta streams", () => {
     const events = [delta("evt_1", "a"), delta("evt_2", "b", 1), delta("evt_3", "c")]
 
-    expect(coalesceServerEvents(events).map((event) => event.payload.current?.id)).toEqual([
-      "evt_1",
-      "evt_2",
-      "evt_3",
-    ])
+    expect(coalesceServerEvents(events).map((event) => event.payload.current?.id)).toEqual(["evt_1", "evt_2", "evt_3"])
   })
 
   test("preserves current event order when enqueuing", () => {

+ 1 - 20
packages/app/src/context/server-sdk.tsx

@@ -1,5 +1,5 @@
 import type { OpenCodeEvent } from "@opencode-ai/client/promise"
-import type { Event, PermissionRequest } from "@/types"
+import type { Event } from "@/types"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createGlobalEmitter } from "@solid-primitives/event-bus"
 import { makeEventListener } from "@solid-primitives/event-listener"
@@ -24,25 +24,6 @@ type CurrentDelta = Extract<
 >
 
 export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
-  if (event.type === "permission.asked") {
-    return {
-      id: event.id,
-      type: "permission.asked",
-      properties: {
-        id: event.data.id,
-        sessionID: event.data.sessionID,
-        permission: event.data.action,
-        patterns: event.data.resources,
-        always: event.data.save ?? [],
-        metadata: event.data.metadata ?? {},
-        tool:
-          event.data.source?.type === "tool"
-            ? { messageID: event.data.source.messageID, callID: event.data.source.id }
-            : undefined,
-      } satisfies PermissionRequest,
-      current: event,
-    }
-  }
   return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent
 }
 

+ 1 - 4
packages/app/src/context/server-session.ts

@@ -4,13 +4,10 @@ import type { OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai
 import type {
   Message,
   Part,
-  PermissionRequest,
-  QuestionRequest,
   Session,
-  SessionStatus,
   Todo,
 } from "@/types"
-import type { FileDiffInfo } from "@opencode-ai/client/promise"
+import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
 import { batch } from "solid-js"
 import { createStore, produce, reconcile } from "solid-js/store"
 import { rootSession } from "@/utils/session-route"

+ 2 - 2
packages/app/src/context/server-sync.tsx

@@ -3,7 +3,6 @@ import type {
   Path,
   Project,
   ProviderAuthResponse,
-  SessionStatus,
 } from "@/types"
 import { showToast } from "@/utils/toast"
 import { getFilename } from "@opencode-ai/core/util/path"
@@ -55,6 +54,7 @@ import type {
   McpResourceCatalogOutput,
   McpServer,
   SessionActiveOutput,
+  SessionStatus,
 } from "@opencode-ai/client/promise"
 import { toggleMcp } from "./global-sync/mcp"
 import { createServerSession, type ServerSession } from "./server-session"
@@ -476,7 +476,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
 
     if (event.current) session.applyV2(event.current)
     session.apply(event)
-    if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") {
+    if (event.type === "session.created" || event.type === "session.deleted") {
       if ("info" in event.properties) homeSessions.apply(event as Parameters<typeof homeSessions.apply>[0])
     }
     homeSessions.refresh(event.type)

+ 4 - 3
packages/app/src/pages/session.tsx

@@ -1,4 +1,5 @@
-import type { FilePart, Project, UserMessage, VcsFileDiff } from "@/types"
+import type { FilePart, Project, UserMessage } from "@/types"
+import type { FileDiffInfo } from "@opencode-ai/client/promise"
 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"
@@ -718,13 +719,13 @@ export default function Page() {
     if (reviewMode() === "git" || reviewMode() === "branch") return !vcsQuery.isPending
     return true
   }
-  const loadReviewDiff = async (file: string, version?: number): Promise<VcsFileDiff | undefined> => {
+  const loadReviewDiff = async (file: string, version?: number): Promise<FileDiffInfo | undefined> => {
     const mode = vcsMode()
     if (!mode) return
     const root = reviewRootDirectory(sync().project?.worktree ?? sdk().directory)
     const directory = reviewDiffDirectory(root, file)
     const source = reviewDiffs().find((diff) => diff.file === file)
-    const valid = (diff: VcsFileDiff | undefined) => {
+    const valid = (diff: FileDiffInfo | undefined) => {
       if (!diff || !source) return
       if (diff.additions !== source.additions || diff.deletions !== source.deletions) return
       if (reviewDiffNeedsLoad(diff)) return

+ 2 - 1
packages/app/src/pages/session/composer/session-composer-state.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
-import type { PermissionRequest, QuestionRequest, Session } from "@/types"
+import type { Session } from "@/types"
+import type { PermissionRequest, QuestionRequest } from "@opencode-ai/client/promise"
 import { todoDockAtBoundary, todoState } from "./session-composer-state"
 import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
 

+ 2 - 1
packages/app/src/pages/session/composer/session-composer-state.ts

@@ -1,6 +1,7 @@
 import { createEffect, createMemo, on, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
-import type { PermissionRequest, QuestionRequest, Todo } from "@/types"
+import type { Todo } from "@/types"
+import type { PermissionRequest, QuestionRequest } from "@opencode-ai/client/promise"
 import { useParams } from "@solidjs/router"
 import { showToast } from "@/utils/toast"
 import { useServerSync } from "@/context/server-sync"

+ 4 - 4
packages/app/src/pages/session/composer/session-permission-dock.tsx

@@ -1,5 +1,5 @@
 import { For, Show } from "solid-js"
-import type { PermissionRequest } from "@/types"
+import type { PermissionRequest } from "@opencode-ai/client/promise"
 import { Button } from "@opencode-ai/ui/button"
 import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
 import { Icon } from "@opencode-ai/ui/icon"
@@ -13,7 +13,7 @@ export function SessionPermissionDock(props: {
   const language = useLanguage()
 
   const toolDescription = () => {
-    const key = `settings.permissions.tool.${props.request.permission}.description`
+    const key = `settings.permissions.tool.${props.request.action}.description`
     const value = language.t(key as Parameters<typeof language.t>[0])
     if (value === key) return ""
     return value
@@ -59,11 +59,11 @@ export function SessionPermissionDock(props: {
         </div>
       </Show>
 
-      <Show when={props.request.patterns.length > 0}>
+      <Show when={props.request.resources.length > 0}>
         <div data-slot="permission-row">
           <span data-slot="permission-spacer" aria-hidden="true" />
           <div data-slot="permission-patterns">
-            <For each={props.request.patterns}>
+            <For each={props.request.resources}>
               {(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
             </For>
           </div>

+ 1 - 1
packages/app/src/pages/session/composer/session-question-dock.tsx

@@ -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/client/promise"
 import { useLanguage } from "@/context/language"
 import { useSDK } from "@/context/sdk"
 import { makeEventListener } from "@solid-primitives/event-listener"

+ 2 - 1
packages/app/src/pages/session/composer/session-request-tree.ts

@@ -1,4 +1,5 @@
-import type { PermissionRequest, QuestionRequest, Session } from "@/types"
+import type { Session } from "@/types"
+import type { PermissionRequest, QuestionRequest } from "@opencode-ai/client/promise"
 
 function sessionTreeRequest<T>(
   session: Session[],

+ 1 - 2
packages/app/src/pages/session/review-tab.tsx

@@ -1,6 +1,5 @@
 import { createEffect, onCleanup, type JSX } from "solid-js"
 import { makeEventListener } from "@solid-primitives/event-listener"
-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 {
@@ -15,7 +14,7 @@ import type { LineComment } from "@/context/comments"
 
 export type DiffStyle = "unified" | "split"
 
-type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
+type ReviewDiff = FileDiffInfo
 
 export interface SessionReviewTabProps {
   title?: JSX.Element

+ 2 - 3
packages/app/src/pages/session/session-side-panel.tsx

@@ -23,7 +23,6 @@ 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 { FileDiffInfo } from "@opencode-ai/client/promise"
 import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -57,8 +56,8 @@ import { setSessionHandoff } from "@/pages/session/handoff"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab"
 
-type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
-type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff
+type ReviewDiff = FileDiffInfo
+type RenderDiff = FileDiffInfo
 
 function renderDiff(value: ReviewDiff): value is RenderDiff {
   return typeof value.file === "string"

+ 2 - 2
packages/app/src/pages/session/timeline/projection.ts

@@ -1,5 +1,5 @@
-import type { SessionMessageInfo } from "@opencode-ai/client/promise"
-import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@/types"
+import type { SessionMessageInfo, SessionStatus } from "@opencode-ai/client/promise"
+import type { AssistantMessage, Message, Part, UserMessage } from "@/types"
 import { createMemo, type Accessor } from "solid-js"
 import { reuseTimelineRows } from "./row-reconciliation"
 import { Timeline, TimelineRow } from "./rows"

+ 2 - 2
packages/app/src/pages/session/timeline/rows.ts

@@ -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 type { SessionMessageInfo, SessionStatus } from "@opencode-ai/client/promise"
+import type { AssistantMessage, Part, 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"

+ 5 - 6
packages/app/src/pages/session/timeline/summary-diffs.test.ts

@@ -1,25 +1,24 @@
 import { describe, expect, test } from "bun:test"
-import type { SnapshotFileDiff } from "@/types"
+import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import { uniqueSummaryDiffs } from "./summary-diffs"
 
 const diff = (file: string, additions: number) =>
   ({
     file,
+    patch: "",
     additions,
     deletions: 0,
-  }) satisfies SnapshotFileDiff
+    status: "modified",
+  }) satisfies FileDiffInfo
 
 describe("uniqueSummaryDiffs", () => {
   test("drops entries without files and preserves unique input", () => {
     const alpha = diff("alpha.ts", 1)
     const beta = diff("beta.ts", 1)
-    const invalid = { additions: 1, deletions: 0 } satisfies SnapshotFileDiff
-
     expect(uniqueSummaryDiffs(undefined)).toEqual([])
     expect(uniqueSummaryDiffs([])).toEqual([])
-    expect(uniqueSummaryDiffs([invalid])).toEqual([])
 
-    const result = uniqueSummaryDiffs([alpha, invalid, beta])
+    const result = uniqueSummaryDiffs([alpha, beta])
     expect(result).toEqual([alpha, beta])
     expect(result[0]).toBe(alpha)
     expect(result[1]).toBe(beta)

+ 3 - 3
packages/app/src/pages/session/timeline/summary-diffs.ts

@@ -1,7 +1,7 @@
-import type { SnapshotFileDiff } from "@/types"
+import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import type { SummaryDiff } from "./timeline-row"
 
-export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) {
+export function uniqueSummaryDiffs(diffs: FileDiffInfo[] | undefined) {
   const files = new Set<string>()
   return (diffs ?? [])
     .reduceRight<SummaryDiff[]>((result, diff) => {
@@ -15,6 +15,6 @@ export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) {
     .reverse()
 }
 
-function isSummaryDiff(diff: SnapshotFileDiff): diff is SummaryDiff {
+function isSummaryDiff(diff: FileDiffInfo): diff is SummaryDiff {
   return typeof diff.file === "string"
 }

+ 2 - 2
packages/app/src/pages/session/timeline/timeline-row.ts

@@ -1,8 +1,8 @@
-import type { SnapshotFileDiff } from "@/types"
+import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import type { PartGroup } from "@opencode-ai/session-ui/message-part"
 import { Data, Equal } from "effect"
 
-export type SummaryDiff = SnapshotFileDiff & { file: string }
+export type SummaryDiff = FileDiffInfo
 
 export namespace TimelineRow {
   export class TurnGap extends Data.TaggedClass("TurnGap")<{

+ 1 - 1
packages/app/src/pages/session/usage-exceeded-dialogs.tsx

@@ -1,6 +1,6 @@
 import { useSDK } from "@/context/sdk"
 import { Persist, persisted } from "@/utils/persist"
-import type { SessionStatus } from "@/types"
+import type { SessionStatus } from "@opencode-ai/client/promise"
 import { onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { useSessionLayout } from "./session-layout"

+ 10 - 4
packages/app/src/pages/session/v2/review-diff-kinds.test.ts

@@ -4,8 +4,8 @@ import { filterReviewFiles, reviewDiffDirectory, reviewDiffKinds, reviewDiffNeed
 describe("reviewDiffKinds", () => {
   test("maps file and directory kinds", () => {
     const kinds = reviewDiffKinds([
-      { file: "src/a.ts", additions: 1, deletions: 0, status: "added" },
-      { file: "src/b.ts", additions: 0, deletions: 2, status: "deleted" },
+      { file: "src/a.ts", patch: "", additions: 1, deletions: 0, status: "added" },
+      { file: "src/b.ts", patch: "", additions: 0, deletions: 2, status: "deleted" },
     ])
 
     expect(kinds.get("src/a.ts")).toBe("add")
@@ -14,7 +14,9 @@ describe("reviewDiffKinds", () => {
   })
 
   test("normalizes file and directory paths", () => {
-    const kinds = reviewDiffKinds([{ file: "\\src//lib/a.ts/", additions: 1, deletions: 1, status: "modified" }])
+    const kinds = reviewDiffKinds([
+      { file: "\\src//lib/a.ts/", patch: "", additions: 1, deletions: 1, status: "modified" },
+    ])
 
     expect(kinds.get("src/lib/a.ts")).toBe("mix")
     expect(kinds.get("src/lib")).toBe("mix")
@@ -36,6 +38,7 @@ describe("reviewDiffNeedsLoad", () => {
         file: "src/a.ts",
         additions: 1,
         deletions: 0,
+        status: "modified",
         patch: "diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts",
       }),
     ).toBe(true)
@@ -47,10 +50,13 @@ describe("reviewDiffNeedsLoad", () => {
         file: "src/a.ts",
         additions: 1,
         deletions: 0,
+        status: "modified",
         patch: "@@ -0,0 +1 @@\n+value",
       }),
     ).toBe(false)
-    expect(reviewDiffNeedsLoad({ file: "empty.txt", additions: 0, deletions: 0 })).toBe(false)
+    expect(
+      reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" }),
+    ).toBe(false)
   })
 })
 

+ 2 - 3
packages/app/src/pages/session/v2/review-diff-kinds.ts

@@ -1,15 +1,14 @@
-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"
 
-export type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff
+export type RenderDiff = FileDiffInfo
 
 export function normalizePath(p: string) {
   return normalizeFileTreeV2Path(p)
 }
 
-export function filterRenderableDiff(value: FileDiffInfo | SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
+export function filterRenderableDiff(value: FileDiffInfo): value is RenderDiff {
   return typeof value.file === "string"
 }
 

+ 1 - 2
packages/app/src/pages/session/v2/review-panel-v2.tsx

@@ -1,5 +1,4 @@
 import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js"
-import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import {
   SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
@@ -31,7 +30,7 @@ import {
 import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
 import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
 
-type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
+type ReviewDiff = FileDiffInfo
 
 export type ReviewPanelV2Props = {
   title?: JSX.Element

+ 24 - 44
packages/app/src/types.ts

@@ -1,34 +1,32 @@
 import type {
   EventSubscribeOutput,
   FileDiffInfo,
-  FileDiffLegacyInfo,
   ProjectListOutput,
-  QuestionAnswer,
-  QuestionInfo,
-  QuestionRequest,
-  ReferenceInfo,
-  SessionInfo,
-  SessionNotFoundError,
-  SessionStatus,
-  SessionV1Info,
-  SessionsResponse,
 } from "@opencode-ai/client/promise"
 import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 
-export type {
-  QuestionAnswer,
-  QuestionRequest,
-  ReferenceInfo,
-  SessionNotFoundError,
-  SessionStatus,
-}
-
 export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string }
-export type Session = Omit<SessionV1Info, "title"> & { title: string }
-export type SessionV2Info = SessionInfo
-export type V2SessionListResponse = SessionsResponse
-export type SnapshotFileDiff = FileDiffLegacyInfo
-export type VcsFileDiff = FileDiffInfo
+export type Session = {
+  id: string
+  slug: string
+  projectID: string
+  workspaceID?: string
+  directory: string
+  path?: string
+  parentID?: string
+  summary?: { additions: number; deletions: number; files: number; diffs?: FileDiffInfo[] }
+  cost?: number
+  tokens?: { input: number; output: number; reasoning: number; cache: { read: number; write: number } }
+  share?: { url: string }
+  title: string
+  agent?: string
+  model?: { id: string; providerID: string; variant?: string }
+  version: string
+  metadata?: Record<string, unknown>
+  time: { created: number; updated: number; compacting?: number; archived?: number }
+  permission?: Array<{ permission: string; pattern: string; action: "allow" | "deny" | "ask" }>
+  revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string }
+}
 
 type CurrentEvent = EventSubscribeOutput extends infer Item
   ? Item extends { type: infer Type extends string; data: infer Data }
@@ -36,21 +34,9 @@ type CurrentEvent = EventSubscribeOutput extends infer Item
     : never
   : never
 
-export type Event =
-  | Exclude<CurrentEvent, { type: "permission.asked" }>
-  | { type: "permission.asked"; properties: PermissionRequest }
+export type Event = CurrentEvent
 
-export type EventSessionError = Extract<Event, { type: "session.error" }>
-
-export type PermissionRequest = {
-  id: string
-  sessionID: string
-  permission: string
-  patterns: string[]
-  metadata: Record<string, unknown>
-  always: string[]
-  tool?: { messageID: string; callID: string }
-}
+export type EventSessionError = Extract<Event, { type: "session.execution.failed" }>
 
 type MessageError =
   | { name: "ProviderAuthError"; data: { providerID: string; message: string } }
@@ -78,7 +64,7 @@ export type UserMessage = {
   role: "user"
   time: { created: number }
   format?: { type: "text" } | { type: "json_schema"; schema: Record<string, unknown>; retryCount?: number }
-  summary?: { title?: string; body?: string; diffs: SnapshotFileDiff[] }
+  summary?: { title?: string; body?: string; diffs: FileDiffInfo[] }
   agent: string
   model: { providerID: string; modelID: string; variant?: string }
   system?: string
@@ -330,9 +316,3 @@ export type Config = {
   experimental?: Record<string, unknown>
   [key: string]: unknown
 }
-
-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 }
-
-export type Question = QuestionInfo

+ 1 - 2
packages/app/src/utils/diffs.test.ts

@@ -1,5 +1,4 @@
 import { describe, expect, test } from "bun:test"
-import type { SnapshotFileDiff } from "@/types"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import type { Message } from "@/types"
 import { diffs, message } from "./diffs"
@@ -10,7 +9,7 @@ const item = {
   additions: 1,
   deletions: 1,
   status: "modified",
-} satisfies FileDiffInfo & SnapshotFileDiff
+} satisfies FileDiffInfo
 
 describe("diffs", () => {
   test("keeps valid arrays", () => {

+ 1 - 2
packages/app/src/utils/diffs.ts

@@ -1,8 +1,7 @@
-import type { SnapshotFileDiff, VcsFileDiff } from "@/types"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import type { Message } from "@/types"
 
-type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
+type Diff = FileDiffInfo
 
 function diff(value: unknown): value is Diff {
   if (!value || typeof value !== "object" || Array.isArray(value)) return false

+ 1 - 1
packages/app/src/utils/server-errors.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import type { SessionNotFoundError } from "@/types"
+import type { SessionNotFoundError } from "@opencode-ai/client/promise"
 import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
 import { formatServerError, isSessionNotFoundError, parseReadableConfigInvalidError } from "./server-errors"