Explorar el Código

feat(tui): show project copy in session list (#31421)

James Long hace 2 meses
padre
commit
ffcb45d7c9

+ 8 - 3
packages/core/src/project.ts

@@ -36,7 +36,12 @@ export const DirectoriesInput = Schema.Struct({
 }).annotate({ identifier: "Project.DirectoriesInput" })
 export type DirectoriesInput = typeof DirectoriesInput.Type
 
-export const Directories = Schema.Array(AbsolutePath).annotate({ identifier: "Project.Directories" })
+export const Directories = Schema.Array(
+  Schema.Struct({
+    directory: AbsolutePath,
+    type: Schema.Literals(["main", "root", "git_worktree"]),
+  }),
+).annotate({ identifier: "Project.Directories" })
 export type Directories = typeof Directories.Type
 
 export interface Interface {
@@ -73,13 +78,13 @@ export const layer = Layer.effect(
 
     const directories = Effect.fn("Project.directories")(function* (input: DirectoriesInput) {
       const rows = yield* db
-        .select({ directory: ProjectDirectoryTable.directory })
+        .select({ directory: ProjectDirectoryTable.directory, type: ProjectDirectoryTable.type })
         .from(ProjectDirectoryTable)
         .where(eq(ProjectDirectoryTable.project_id, input.projectID))
         .orderBy(desc(ProjectDirectoryTable.time_created), asc(ProjectDirectoryTable.directory))
         .all()
         .pipe(Effect.orDie)
-      return rows.map((row) => AbsolutePath.make(row.directory))
+      return rows.map((row) => ({ directory: AbsolutePath.make(row.directory), type: row.type }))
     })
 
     const cached = Effect.fnUntraced(function* (dir: string) {

+ 7 - 5
packages/core/test/project.test.ts

@@ -59,9 +59,11 @@ describe("Project directories schemas", () => {
           projectID: ProjectV2.ID.make("project"),
         },
       )
-      expect(Schema.decodeUnknownSync(ProjectV2.Directories)([AbsolutePath.make("/tmp/project")])).toEqual([
-        AbsolutePath.make("/tmp/project"),
-      ])
+      expect(
+        Schema.decodeUnknownSync(ProjectV2.Directories)([
+          { directory: AbsolutePath.make("/tmp/project"), type: "main" },
+        ]),
+      ).toEqual([{ directory: AbsolutePath.make("/tmp/project"), type: "main" }])
     }),
   )
 
@@ -90,8 +92,8 @@ describe("Project directories schemas", () => {
         .pipe(Effect.orDie)
 
       expect(yield* project.directories({ projectID })).toEqual([
-        AbsolutePath.make("/repo/z"),
-        AbsolutePath.make("/repo/a"),
+        { directory: AbsolutePath.make("/repo/z"), type: "root" },
+        { directory: AbsolutePath.make("/repo/a"), type: "main" },
       ])
     }),
   )

+ 11 - 3
packages/opencode/test/server/project-copy.test.ts

@@ -34,6 +34,8 @@ function json<T>(response: HttpClientResponse.HttpClientResponse) {
 }
 
 describe("project directories and copies endpoints", () => {
+  type ProjectDirectory = { directory: string; type: "main" | "root" | "git_worktree" }
+
   it.instance(
     "lists directories and manages git worktree copies",
     () =>
@@ -51,7 +53,7 @@ describe("project directories and copies endpoints", () => {
 
         const initial = yield* request(test.directory, `${base}/directories`)
         expect(initial.status).toBe(200)
-        expect(yield* json<string[]>(initial)).toEqual([test.directory])
+        expect(yield* json<ProjectDirectory[]>(initial)).toEqual([{ directory: test.directory, type: "main" }])
 
         const create = yield* request(test.directory, copies, {
           method: "POST",
@@ -63,7 +65,10 @@ describe("project directories and copies endpoints", () => {
         expect(created.directory).toBe(createdDirectory)
 
         const listed = yield* request(test.directory, `${base}/directories`)
-        expect(yield* json<string[]>(listed)).toContain(created.directory)
+        expect(yield* json<ProjectDirectory[]>(listed)).toContainEqual({
+          directory: created.directory,
+          type: "git_worktree",
+        })
 
         yield* Effect.promise(() => Bun.write(path.join(created.directory, "dirty.txt"), "dirty"))
 
@@ -94,7 +99,10 @@ describe("project directories and copies endpoints", () => {
         })
         expect(refresh.status).toBe(204)
         const refreshed = yield* request(test.directory, `${base}/directories`)
-        expect((yield* json<string[]>(refreshed)).length).toBe(2)
+        expect(yield* json<ProjectDirectory[]>(refreshed)).toEqual([
+          { directory: externalDirectory, type: "git_worktree" },
+          { directory: test.directory, type: "main" },
+        ])
       }),
     { git: true },
   )

+ 4 - 1
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -3743,7 +3743,10 @@ export type ConfigV2ExperimentalPolicy = {
   resource: string
 }
 
-export type ProjectDirectories = Array<string>
+export type ProjectDirectories = Array<{
+  directory: string
+  type: "main" | "root" | "git_worktree"
+}>
 
 export type ProjectCopyCopy = {
   directory: string

+ 12 - 1
packages/sdk/openapi.json

@@ -23864,7 +23864,18 @@
       "ProjectDirectories": {
         "type": "array",
         "items": {
-          "type": "string"
+          "type": "object",
+          "properties": {
+            "directory": {
+              "type": "string"
+            },
+            "type": {
+              "type": "string",
+              "enum": ["main", "root", "git_worktree"]
+            }
+          },
+          "required": ["directory", "type"],
+          "additionalProperties": false
         }
       },
       "ProjectCopyCopy": {

+ 1 - 1
packages/tui/src/component/dialog-move-session.tsx

@@ -63,7 +63,7 @@ export function DialogMoveSession(props: {
       try {
         await sdk.client.experimental.projectCopy.refresh({ projectID }, { throwOnError: true })
         const directories = await sdk.client.project.directories({ projectID }, { throwOnError: true })
-        return directories.data ?? []
+        return directories.data?.map((item) => item.directory) ?? []
       } finally {
         setWorking(false)
       }

+ 9 - 21
packages/tui/src/component/dialog-session-list.tsx

@@ -2,13 +2,13 @@ import { useDialog } from "../ui/dialog"
 import { DialogSelect } from "../ui/dialog-select"
 import { useRoute } from "../context/route"
 import { useSync } from "../context/sync"
-import { createMemo, createResource, createSignal, onMount, type JSX } from "solid-js"
+import { createMemo, createResource, createSignal, onMount } from "solid-js"
+import path from "path"
 import { Locale } from "../util/locale"
 import { useProject } from "../context/project"
 import { useTheme } from "../context/theme"
 import { useSDK } from "../context/sdk"
 import { useLocal } from "../context/local"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { DialogSessionRename } from "./dialog-session-rename"
 import { createDebouncedSignal } from "../util/signal"
 import { useToast } from "../ui/toast"
@@ -16,7 +16,6 @@ import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } fr
 import { Spinner } from "./spinner"
 import { errorMessage } from "../util/error"
 import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
-import { WorkspaceLabel } from "./workspace-label"
 import { useCommandShortcut } from "../keymap"
 
 export function DialogSessionList() {
@@ -170,24 +169,13 @@ export function DialogSessionList() {
     function buildOption(id: string, category: string) {
       const x = sessionMap.get(id)
       if (!x) return undefined
-      const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
-
-      let footer: JSX.Element | string = ""
-      if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
-        if (x.workspaceID) {
-          footer = workspace ? (
-            <WorkspaceLabel
-              type={workspace.type}
-              name={workspace.name}
-              status={project.workspace.status(x.workspaceID) ?? "error"}
-            />
-          ) : (
-            <WorkspaceLabel type="unknown" name={x.workspaceID} status="error" />
-          )
-        }
-      } else {
-        footer = Locale.time(x.time.updated)
-      }
+      const directory = x.path
+        ? x.directory.endsWith(x.path)
+          ? x.directory.slice(0, -x.path.length).replace(/\/$/, "")
+          : undefined
+        : x.directory
+      const footer =
+        directory && directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""
 
       const isDeleting = toDelete() === x.id
       const status = sync.data.session_status?.[x.id]

+ 7 - 2
packages/tui/src/context/project.tsx

@@ -23,6 +23,7 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
       project: {
         id: undefined as string | undefined,
         worktree: undefined as string | undefined,
+        mainDir: undefined as string | undefined,
       },
       instance: {
         path: defaultPath,
@@ -36,15 +37,19 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
 
     async function sync() {
       const workspace = store.workspace.current
-      const [path, project] = await Promise.all([
+      const [instancePath, project] = await Promise.all([
         sdk.client.path.get({ workspace }),
         sdk.client.project.current({ workspace }),
       ])
+      const directories = project.data?.id
+        ? await sdk.client.project.directories({ projectID: project.data.id, workspace })
+        : undefined
 
       batch(() => {
-        setStore("instance", "path", reconcile(path.data || defaultPath))
+        setStore("instance", "path", reconcile(instancePath.data || defaultPath))
         setStore("project", "id", project.data?.id)
         setStore("project", "worktree", project.data?.worktree)
+        setStore("project", "mainDir", directories?.data?.find((item) => item.type === "main")?.directory)
       })
     }