Ver código fonte

Feat/clickable subtask (#6846)

OpeOginni 1 mês atrás
pai
commit
8996185f3b

+ 57 - 12
packages/app/src/components/session/session-header.tsx

@@ -35,7 +35,12 @@ export function SessionHeader() {
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
 
 
   const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
   const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
-  const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+  const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
+  const parentSession = createMemo(() => {
+    const current = currentSession()
+    if (!current?.parentID) return undefined
+    return sync.data.session.find((s) => s.id === current.parentID)
+  })
   const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
   const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
   const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
   const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
 
 
@@ -45,6 +50,8 @@ export function SessionHeader() {
 
 
   function navigateToSession(session: Session | undefined) {
   function navigateToSession(session: Session | undefined) {
     if (!session) return
     if (!session) return
+    // Only navigate if we're actually changing to a different session
+    if (session.id === params.id) return
     navigate(`/${params.dir}/session/${session.id}`)
     navigate(`/${params.dir}/session/${session.id}`)
   }
   }
 
 
@@ -79,18 +86,56 @@ export function SessionHeader() {
               </Select>
               </Select>
               <div class="text-text-weaker">/</div>
               <div class="text-text-weaker">/</div>
             </div>
             </div>
-            <Select
-              options={sessions()}
-              current={currentSession()}
-              placeholder="New session"
-              label={(x) => x.title}
-              value={(x) => x.id}
-              onSelect={navigateToSession}
-              class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
-              variant="ghost"
-            />
+            <Show
+              when={parentSession()}
+              fallback={
+                <>
+                  <Select
+                    options={sessions()}
+                    current={currentSession()}
+                    placeholder="New session"
+                    label={(x) => x.title}
+                    value={(x) => x.id}
+                    onSelect={navigateToSession}
+                    class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
+                    variant="ghost"
+                  />
+                </>
+              }
+            >
+              <div class="flex items-center gap-2 min-w-0">
+                <Select
+                    options={sessions()}
+                    current={parentSession()}
+                    placeholder="Back to parent session"
+                    label={(x) => x.title}
+                    value={(x) => x.id}
+                    onSelect={(session) => {
+                      // Only navigate if selecting a different session than current parent
+                      const currentParent = parentSession()
+                      if (session && currentParent && session.id !== currentParent.id) {
+                        navigateToSession(session)
+                      }
+                    }}
+                    class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
+                    variant="ghost"
+                  />
+                <div class="text-text-weaker">/</div>
+                <div class="flex items-center gap-1.5 min-w-0">
+                  <Tooltip value="Back to parent session">
+                    <button
+                      type="button"
+                      class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
+                      onClick={() => navigateToSession(parentSession())}
+                    >
+                      <Icon name="arrow-left" size="small" class="text-icon-base" />
+                    </button>
+                  </Tooltip>
+                </div>
+              </div>
+            </Show>
           </div>
           </div>
-          <Show when={currentSession()}>
+          <Show when={currentSession() && !parentSession()}>
             <TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
             <TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
               <IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
               <IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
             </TooltipKeybind>
             </TooltipKeybind>

+ 12 - 2
packages/app/src/pages/directory-layout.tsx

@@ -1,5 +1,5 @@
 import { createMemo, Show, type ParentProps } from "solid-js"
 import { createMemo, Show, type ParentProps } from "solid-js"
-import { useParams } from "@solidjs/router"
+import { useNavigate, useParams } from "@solidjs/router"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SyncProvider, useSync } from "@/context/sync"
 import { SyncProvider, useSync } from "@/context/sync"
 import { LocalProvider } from "@/context/local"
 import { LocalProvider } from "@/context/local"
@@ -10,6 +10,7 @@ import { iife } from "@opencode-ai/util/iife"
 
 
 export default function Layout(props: ParentProps) {
 export default function Layout(props: ParentProps) {
   const params = useParams()
   const params = useParams()
+  const navigate = useNavigate()
   const directory = createMemo(() => {
   const directory = createMemo(() => {
     return base64Decode(params.dir!)
     return base64Decode(params.dir!)
   })
   })
@@ -26,8 +27,17 @@ export default function Layout(props: ParentProps) {
               response: "once" | "always" | "reject"
               response: "once" | "always" | "reject"
             }) => sdk.client.permission.respond(input)
             }) => sdk.client.permission.respond(input)
 
 
+            const navigateToSession = (sessionID: string) => {
+              navigate(`/${params.dir}/session/${sessionID}`)
+            }
+
             return (
             return (
-              <DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
+              <DataProvider
+                data={sync.data}
+                directory={directory()}
+                onPermissionRespond={respond}
+                onNavigateToSession={navigateToSession}
+              >
                 <LocalProvider>{props.children}</LocalProvider>
                 <LocalProvider>{props.children}</LocalProvider>
               </DataProvider>
               </DataProvider>
             )
             )

+ 10 - 0
packages/ui/src/components/basic-tool.css

@@ -68,6 +68,16 @@
     line-height: var(--line-height-large);
     line-height: var(--line-height-large);
     letter-spacing: var(--letter-spacing-normal);
     letter-spacing: var(--letter-spacing-normal);
     color: var(--text-weak);
     color: var(--text-weak);
+
+    &.clickable {
+      cursor: pointer;
+      text-decoration: underline;
+      transition: color 0.15s ease;
+
+      &:hover {
+        color: var(--text-base);
+      }
+    }
   }
   }
 
 
   [data-slot="basic-tool-tool-arg"] {
   [data-slot="basic-tool-tool-arg"] {

+ 8 - 0
packages/ui/src/components/basic-tool.tsx

@@ -25,6 +25,7 @@ export interface BasicToolProps {
   hideDetails?: boolean
   hideDetails?: boolean
   defaultOpen?: boolean
   defaultOpen?: boolean
   forceOpen?: boolean
   forceOpen?: boolean
+  onSubtitleClick?: () => void
 }
 }
 
 
 export function BasicTool(props: BasicToolProps) {
 export function BasicTool(props: BasicToolProps) {
@@ -59,6 +60,13 @@ export function BasicTool(props: BasicToolProps) {
                             data-slot="basic-tool-tool-subtitle"
                             data-slot="basic-tool-tool-subtitle"
                             classList={{
                             classList={{
                               [trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
                               [trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
+                              clickable: !!props.onSubtitleClick,
+                            }}
+                            onClick={(e) => {
+                              if (props.onSubtitleClick) {
+                                e.stopPropagation()
+                                props.onSubtitleClick()
+                              }
                             }}
                             }}
                           >
                           >
                             {trigger().subtitle}
                             {trigger().subtitle}

+ 9 - 0
packages/ui/src/components/message-part.tsx

@@ -759,6 +759,13 @@ ToolRegistry.register({
       })
       })
     }
     }
 
 
+    const handleSubtitleClick = () => {
+      const sessionId = childSessionId()
+      if (sessionId && data.navigateToSession) {
+        data.navigateToSession(sessionId)
+      }
+    }
+
     const renderChildToolPart = () => {
     const renderChildToolPart = () => {
       const toolData = childToolPart()
       const toolData = childToolPart()
       if (!toolData) return null
       if (!toolData) return null
@@ -797,6 +804,7 @@ ToolRegistry.register({
                       titleClass: "capitalize",
                       titleClass: "capitalize",
                       subtitle: props.input.description,
                       subtitle: props.input.description,
                     }}
                     }}
+                    onSubtitleClick={handleSubtitleClick}
                   />
                   />
                 }
                 }
               >
               >
@@ -826,6 +834,7 @@ ToolRegistry.register({
                 titleClass: "capitalize",
                 titleClass: "capitalize",
                 subtitle: props.input.description,
                 subtitle: props.input.description,
               }}
               }}
+              onSubtitleClick={handleSubtitleClick}
             >
             >
               <div
               <div
                 ref={autoScroll.scrollRef}
                 ref={autoScroll.scrollRef}

+ 9 - 1
packages/ui/src/context/data.tsx

@@ -30,9 +30,16 @@ export type PermissionRespondFn = (input: {
   response: "once" | "always" | "reject"
   response: "once" | "always" | "reject"
 }) => void
 }) => void
 
 
+export type NavigateToSessionFn = (sessionID: string) => void
+
 export const { use: useData, provider: DataProvider } = createSimpleContext({
 export const { use: useData, provider: DataProvider } = createSimpleContext({
   name: "Data",
   name: "Data",
-  init: (props: { data: Data; directory: string; onPermissionRespond?: PermissionRespondFn }) => {
+  init: (props: {
+    data: Data
+    directory: string
+    onPermissionRespond?: PermissionRespondFn
+    onNavigateToSession?: NavigateToSessionFn
+  }) => {
     return {
     return {
       get store() {
       get store() {
         return props.data
         return props.data
@@ -41,6 +48,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
         return props.directory
         return props.directory
       },
       },
       respondToPermission: props.onPermissionRespond,
       respondToPermission: props.onPermissionRespond,
+      navigateToSession: props.onNavigateToSession,
     }
     }
   },
   },
 })
 })