Merge branch 'dev' into docs-export
commit
f49834f301
|
|
@ -1220,7 +1220,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
})
|
})
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
if (session) {
|
||||||
|
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||||
|
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,14 @@ type SessionTabs = {
|
||||||
type SessionView = {
|
type SessionView = {
|
||||||
scroll: Record<string, SessionScroll>
|
scroll: Record<string, SessionScroll>
|
||||||
reviewOpen?: string[]
|
reviewOpen?: string[]
|
||||||
|
pendingMessage?: string
|
||||||
|
pendingMessageAt?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabHandoff = {
|
||||||
|
dir: string
|
||||||
|
id: string
|
||||||
|
at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
|
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
|
||||||
|
|
@ -115,10 +123,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
},
|
},
|
||||||
sessionTabs: {} as Record<string, SessionTabs>,
|
sessionTabs: {} as Record<string, SessionTabs>,
|
||||||
sessionView: {} as Record<string, SessionView>,
|
sessionView: {} as Record<string, SessionView>,
|
||||||
|
handoff: {
|
||||||
|
tabs: undefined as TabHandoff | undefined,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const MAX_SESSION_KEYS = 50
|
const MAX_SESSION_KEYS = 50
|
||||||
|
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
|
||||||
const meta = { active: undefined as string | undefined, pruned: false }
|
const meta = { active: undefined as string | undefined, pruned: false }
|
||||||
const used = new Map<string, number>()
|
const used = new Map<string, number>()
|
||||||
|
|
||||||
|
|
@ -411,6 +423,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
|
handoff: {
|
||||||
|
tabs: createMemo(() => store.handoff?.tabs),
|
||||||
|
setTabs(dir: string, id: string) {
|
||||||
|
setStore("handoff", "tabs", { dir, id, at: Date.now() })
|
||||||
|
},
|
||||||
|
clearTabs() {
|
||||||
|
if (!store.handoff?.tabs) return
|
||||||
|
setStore("handoff", "tabs", undefined)
|
||||||
|
},
|
||||||
|
},
|
||||||
projects: {
|
projects: {
|
||||||
list,
|
list,
|
||||||
open(directory: string) {
|
open(directory: string) {
|
||||||
|
|
@ -536,6 +558,49 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
setStore("mobileSidebar", "opened", (x) => !x)
|
setStore("mobileSidebar", "opened", (x) => !x)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pendingMessage: {
|
||||||
|
set(sessionKey: string, messageID: string) {
|
||||||
|
const at = Date.now()
|
||||||
|
touch(sessionKey)
|
||||||
|
const current = store.sessionView[sessionKey]
|
||||||
|
if (!current) {
|
||||||
|
setStore("sessionView", sessionKey, {
|
||||||
|
scroll: {},
|
||||||
|
pendingMessage: messageID,
|
||||||
|
pendingMessageAt: at,
|
||||||
|
})
|
||||||
|
prune(meta.active ?? sessionKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
"sessionView",
|
||||||
|
sessionKey,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.pendingMessage = messageID
|
||||||
|
draft.pendingMessageAt = at
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
consume(sessionKey: string) {
|
||||||
|
const current = store.sessionView[sessionKey]
|
||||||
|
const message = current?.pendingMessage
|
||||||
|
const at = current?.pendingMessageAt
|
||||||
|
if (!message || !at) return
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
"sessionView",
|
||||||
|
sessionKey,
|
||||||
|
produce((draft) => {
|
||||||
|
delete draft.pendingMessage
|
||||||
|
delete draft.pendingMessageAt
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Date.now() - at > PENDING_MESSAGE_TTL_MS) return
|
||||||
|
return message
|
||||||
|
},
|
||||||
|
},
|
||||||
view(sessionKey: string | Accessor<string>) {
|
view(sessionKey: string | Accessor<string>) {
|
||||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1000,69 +1000,6 @@ export default function Layout(props: ParentProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSession(session: Session) {
|
|
||||||
const [store, setStore] = globalSync.child(session.directory)
|
|
||||||
const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
|
||||||
const index = sessions.findIndex((s) => s.id === session.id)
|
|
||||||
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
|
||||||
|
|
||||||
const result = await globalSDK.client.session
|
|
||||||
.delete({ directory: session.directory, sessionID: session.id })
|
|
||||||
.then((x) => x.data)
|
|
||||||
.catch((err) => {
|
|
||||||
showToast({
|
|
||||||
title: language.t("session.delete.failed.title"),
|
|
||||||
description: errorMessage(err),
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
const removed = new Set<string>([session.id])
|
|
||||||
|
|
||||||
const byParent = new Map<string, string[]>()
|
|
||||||
for (const item of draft.session) {
|
|
||||||
const parentID = item.parentID
|
|
||||||
if (!parentID) continue
|
|
||||||
const existing = byParent.get(parentID)
|
|
||||||
if (existing) {
|
|
||||||
existing.push(item.id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byParent.set(parentID, [item.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack = [session.id]
|
|
||||||
while (stack.length) {
|
|
||||||
const parentID = stack.pop()
|
|
||||||
if (!parentID) continue
|
|
||||||
|
|
||||||
const children = byParent.get(parentID)
|
|
||||||
if (!children) continue
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
if (removed.has(child)) continue
|
|
||||||
removed.add(child)
|
|
||||||
stack.push(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (session.id === params.id) {
|
|
||||||
if (nextSession) {
|
|
||||||
navigate(`/${params.dir}/session/${nextSession.id}`)
|
|
||||||
} else {
|
|
||||||
navigate(`/${params.dir}/session`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
command.register(() => {
|
command.register(() => {
|
||||||
const commands: CommandOption[] = [
|
const commands: CommandOption[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -1316,15 +1253,6 @@ export default function Layout(props: ParentProps) {
|
||||||
globalSync.project.meta(project.worktree, { name })
|
globalSync.project.meta(project.worktree, { name })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameSession(session: Session, next: string) {
|
|
||||||
if (next === session.title) return
|
|
||||||
await globalSDK.client.session.update({
|
|
||||||
directory: session.directory,
|
|
||||||
sessionID: session.id,
|
|
||||||
title: next,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||||
const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
|
const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
|
||||||
if (current === next) return
|
if (current === next) return
|
||||||
|
|
@ -1475,33 +1403,6 @@ export default function Layout(props: ParentProps) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDeleteSession(props: { session: Session }) {
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteSession(props.session)
|
|
||||||
dialog.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog title={language.t("session.delete.title")} fit>
|
|
||||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<span class="text-14-regular text-text-strong">
|
|
||||||
{language.t("session.delete.confirm", { name: props.session.title })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" size="large" onClick={handleDelete}>
|
|
||||||
{language.t("session.delete.button")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDeleteWorkspace(props: { root: string; directory: string }) {
|
function DialogDeleteWorkspace(props: { root: string; directory: string }) {
|
||||||
const name = createMemo(() => getFilename(props.directory))
|
const name = createMemo(() => getFilename(props.directory))
|
||||||
const [data, setData] = createStore({
|
const [data, setData] = createStore({
|
||||||
|
|
@ -1855,10 +1756,6 @@ export default function Layout(props: ParentProps) {
|
||||||
const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded())
|
const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded())
|
||||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||||
const isActive = createMemo(() => props.session.id === params.id)
|
const isActive = createMemo(() => props.session.id === params.id)
|
||||||
const [menu, setMenu] = createStore({
|
|
||||||
open: false,
|
|
||||||
pendingRename: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||||
const cancelHoverPrefetch = () => {
|
const cancelHoverPrefetch = () => {
|
||||||
|
|
@ -1885,7 +1782,7 @@ export default function Layout(props: ParentProps) {
|
||||||
const item = (
|
const item = (
|
||||||
<A
|
<A
|
||||||
href={`${props.slug}/session/${props.session.id}`}
|
href={`${props.slug}/session/${props.session.id}`}
|
||||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menu.open ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onPointerEnter={scheduleHoverPrefetch}
|
onPointerEnter={scheduleHoverPrefetch}
|
||||||
onPointerLeave={cancelHoverPrefetch}
|
onPointerLeave={cancelHoverPrefetch}
|
||||||
onMouseEnter={scheduleHoverPrefetch}
|
onMouseEnter={scheduleHoverPrefetch}
|
||||||
|
|
@ -1917,14 +1814,9 @@ export default function Layout(props: ParentProps) {
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<InlineEditor
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
id={`session:${props.session.id}`}
|
{props.session.title}
|
||||||
value={() => props.session.title}
|
</span>
|
||||||
onSave={(next) => renameSession(props.session, next)}
|
|
||||||
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
||||||
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
||||||
stopPropagation
|
|
||||||
/>
|
|
||||||
<Show when={props.session.summary}>
|
<Show when={props.session.summary}>
|
||||||
{(summary) => (
|
{(summary) => (
|
||||||
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||||
|
|
@ -1972,7 +1864,10 @@ export default function Layout(props: ParentProps) {
|
||||||
getLabel={messageLabel}
|
getLabel={messageLabel}
|
||||||
onMessageSelect={(message) => {
|
onMessageSelect={(message) => {
|
||||||
if (!isActive()) {
|
if (!isActive()) {
|
||||||
sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
|
layout.pendingMessage.set(
|
||||||
|
`${base64Encode(props.session.directory)}/${props.session.id}`,
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
navigate(`${props.slug}/session/${props.session.id}`)
|
navigate(`${props.slug}/session/${props.session.id}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1989,49 +1884,25 @@ export default function Layout(props: ParentProps) {
|
||||||
<div
|
<div
|
||||||
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100 pointer-events-auto": menu.open,
|
"opacity-100 pointer-events-auto": !!props.mobile,
|
||||||
"opacity-0 pointer-events-none": !menu.open,
|
"opacity-0 pointer-events-none": !props.mobile,
|
||||||
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||||
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu modal={!sidebarHovering()} open={menu.open} onOpenChange={(open) => setMenu("open", open)}>
|
<Tooltip value={language.t("common.archive")} placement="top">
|
||||||
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
<IconButton
|
||||||
<DropdownMenu.Trigger
|
icon="archive"
|
||||||
as={IconButton}
|
variant="ghost"
|
||||||
icon="dot-grid"
|
class="size-6 rounded-md"
|
||||||
variant="ghost"
|
aria-label={language.t("common.archive")}
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
onClick={(event) => {
|
||||||
aria-label={language.t("common.moreOptions")}
|
event.preventDefault()
|
||||||
/>
|
event.stopPropagation()
|
||||||
</Tooltip>
|
void archiveSession(props.session)
|
||||||
<DropdownMenu.Portal mount={!props.mobile ? state.nav : undefined}>
|
}}
|
||||||
<DropdownMenu.Content
|
/>
|
||||||
onCloseAutoFocus={(event) => {
|
</Tooltip>
|
||||||
if (!menu.pendingRename) return
|
|
||||||
event.preventDefault()
|
|
||||||
setMenu("pendingRename", false)
|
|
||||||
openEditor(`session:${props.session.id}`, props.session.title)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
setMenu("pendingRename", true)
|
|
||||||
setMenu("open", false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onSelect={() => archiveSession(props.session)}>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession session={props.session} />)}>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,16 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||||
|
|
@ -73,10 +76,31 @@ import { same } from "@/utils/same"
|
||||||
|
|
||||||
type DiffStyle = "unified" | "split"
|
type DiffStyle = "unified" | "split"
|
||||||
|
|
||||||
|
type HandoffSession = {
|
||||||
|
prompt: string
|
||||||
|
files: Record<string, SelectedLineRange | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
const HANDOFF_MAX = 40
|
||||||
|
|
||||||
const handoff = {
|
const handoff = {
|
||||||
prompt: "",
|
session: new Map<string, HandoffSession>(),
|
||||||
terminals: [] as string[],
|
terminal: new Map<string, string[]>(),
|
||||||
files: {} as Record<string, SelectedLineRange | null>,
|
}
|
||||||
|
|
||||||
|
const touch = <K, V>(map: Map<K, V>, key: K, value: V) => {
|
||||||
|
map.delete(key)
|
||||||
|
map.set(key, value)
|
||||||
|
while (map.size > HANDOFF_MAX) {
|
||||||
|
const first = map.keys().next().value
|
||||||
|
if (first === undefined) return
|
||||||
|
map.delete(first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
|
||||||
|
const prev = handoff.session.get(key) ?? { prompt: "", files: {} }
|
||||||
|
touch(handoff.session, key, { ...prev, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionReviewTabProps {
|
interface SessionReviewTabProps {
|
||||||
|
|
@ -280,9 +304,47 @@ export default function Page() {
|
||||||
.finally(() => setUi("responding", false))
|
.finally(() => setUi("responding", false))
|
||||||
}
|
}
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
|
const workspaceKey = createMemo(() => params.dir ?? "")
|
||||||
|
const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => params.id,
|
||||||
|
(id, prev) => {
|
||||||
|
if (!id) return
|
||||||
|
if (prev) return
|
||||||
|
|
||||||
|
const pending = layout.handoff.tabs()
|
||||||
|
if (!pending) return
|
||||||
|
if (Date.now() - pending.at > 60_000) {
|
||||||
|
layout.handoff.clearTabs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.id !== id) return
|
||||||
|
layout.handoff.clearTabs()
|
||||||
|
if (pending.dir !== (params.dir ?? "")) return
|
||||||
|
|
||||||
|
const from = workspaceTabs().tabs()
|
||||||
|
if (from.all.length === 0 && !from.active) return
|
||||||
|
|
||||||
|
const current = tabs().tabs()
|
||||||
|
if (current.all.length > 0 || current.active) return
|
||||||
|
|
||||||
|
const all = normalizeTabs(from.all)
|
||||||
|
const active = from.active ? normalizeTab(from.active) : undefined
|
||||||
|
tabs().setAll(all)
|
||||||
|
tabs().setActive(active && all.includes(active) ? active : all[0])
|
||||||
|
|
||||||
|
workspaceTabs().setAll([])
|
||||||
|
workspaceTabs().setActive(undefined)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
|
|
@ -398,6 +460,213 @@ export default function Page() {
|
||||||
if (!id) return false
|
if (!id) return false
|
||||||
return sync.session.history.loading(id)
|
return sync.session.history.loading(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [title, setTitle] = createStore({
|
||||||
|
draft: "",
|
||||||
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
menuOpen: false,
|
||||||
|
pendingRename: false,
|
||||||
|
})
|
||||||
|
let titleRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
const errorMessage = (err: unknown) => {
|
||||||
|
if (err && typeof err === "object" && "data" in err) {
|
||||||
|
const data = (err as { data?: { message?: string } }).data
|
||||||
|
if (data?.message) return data.message
|
||||||
|
}
|
||||||
|
if (err instanceof Error) return err.message
|
||||||
|
return language.t("common.requestFailed")
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => params.id,
|
||||||
|
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const openTitleEditor = () => {
|
||||||
|
if (!params.id) return
|
||||||
|
setTitle({ editing: true, draft: info()?.title ?? "" })
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
titleRef?.focus()
|
||||||
|
titleRef?.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeTitleEditor = () => {
|
||||||
|
if (title.saving) return
|
||||||
|
setTitle({ editing: false, saving: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveTitleEditor = async () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
if (title.saving) return
|
||||||
|
|
||||||
|
const next = title.draft.trim()
|
||||||
|
if (!next || next === (info()?.title ?? "")) {
|
||||||
|
setTitle({ editing: false, saving: false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle("saving", true)
|
||||||
|
await sdk.client.session
|
||||||
|
.update({ sessionID, title: next })
|
||||||
|
.then(() => {
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const index = draft.session.findIndex((s) => s.id === sessionID)
|
||||||
|
if (index !== -1) draft.session[index].title = next
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
setTitle({ editing: false, saving: false })
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setTitle("saving", false)
|
||||||
|
showToast({
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function archiveSession(sessionID: string) {
|
||||||
|
const session = sync.session.get(sessionID)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const sessions = sync.data.session ?? []
|
||||||
|
const index = sessions.findIndex((s) => s.id === sessionID)
|
||||||
|
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||||
|
|
||||||
|
await sdk.client.session
|
||||||
|
.update({ sessionID, time: { archived: Date.now() } })
|
||||||
|
.then(() => {
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const index = draft.session.findIndex((s) => s.id === sessionID)
|
||||||
|
if (index !== -1) draft.session.splice(index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (params.id !== sessionID) return
|
||||||
|
if (session.parentID) {
|
||||||
|
navigate(`/${params.dir}/session/${session.parentID}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (nextSession) {
|
||||||
|
navigate(`/${params.dir}/session/${nextSession.id}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigate(`/${params.dir}/session`)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
showToast({
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSession(sessionID: string) {
|
||||||
|
const session = sync.session.get(sessionID)
|
||||||
|
if (!session) return false
|
||||||
|
|
||||||
|
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
||||||
|
const index = sessions.findIndex((s) => s.id === sessionID)
|
||||||
|
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||||
|
|
||||||
|
const result = await sdk.client.session
|
||||||
|
.delete({ sessionID })
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch((err) => {
|
||||||
|
showToast({
|
||||||
|
title: language.t("session.delete.failed.title"),
|
||||||
|
description: errorMessage(err),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) return false
|
||||||
|
|
||||||
|
sync.set(
|
||||||
|
produce((draft) => {
|
||||||
|
const removed = new Set<string>([sessionID])
|
||||||
|
|
||||||
|
const byParent = new Map<string, string[]>()
|
||||||
|
for (const item of draft.session) {
|
||||||
|
const parentID = item.parentID
|
||||||
|
if (!parentID) continue
|
||||||
|
const existing = byParent.get(parentID)
|
||||||
|
if (existing) {
|
||||||
|
existing.push(item.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byParent.set(parentID, [item.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = [sessionID]
|
||||||
|
while (stack.length) {
|
||||||
|
const parentID = stack.pop()
|
||||||
|
if (!parentID) continue
|
||||||
|
|
||||||
|
const children = byParent.get(parentID)
|
||||||
|
if (!children) continue
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
if (removed.has(child)) continue
|
||||||
|
removed.add(child)
|
||||||
|
stack.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (params.id !== sessionID) return true
|
||||||
|
if (session.parentID) {
|
||||||
|
navigate(`/${params.dir}/session/${session.parentID}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (nextSession) {
|
||||||
|
navigate(`/${params.dir}/session/${nextSession.id}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
navigate(`/${params.dir}/session`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDeleteSession(props: { sessionID: string }) {
|
||||||
|
const title = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await deleteSession(props.sessionID)
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog title={language.t("session.delete.title")} fit>
|
||||||
|
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-14-regular text-text-strong">
|
||||||
|
{language.t("session.delete.confirm", { name: title() })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" size="large" onClick={handleDelete}>
|
||||||
|
{language.t("session.delete.button")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const emptyUserMessages: UserMessage[] = []
|
const emptyUserMessages: UserMessage[] = []
|
||||||
const userMessages = createMemo(
|
const userMessages = createMemo(
|
||||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||||
|
|
@ -545,8 +814,10 @@ export default function Page() {
|
||||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!params.id) return
|
sdk.directory
|
||||||
sync.session.sync(params.id)
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
|
sync.session.sync(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -614,10 +885,22 @@ export default function Page() {
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => params.id,
|
sessionKey,
|
||||||
() => {
|
() => {
|
||||||
setStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
setStore("expanded", {})
|
setStore("expanded", {})
|
||||||
|
setUi("autoCreated", false)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => params.dir,
|
||||||
|
(dir) => {
|
||||||
|
if (!dir) return
|
||||||
|
setStore("newSessionWorktree", "main")
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
|
|
@ -1125,12 +1408,15 @@ export default function Page() {
|
||||||
activeDiff: undefined as string | undefined,
|
activeDiff: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const reviewScroll = () => tree.reviewScroll
|
createEffect(
|
||||||
const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
|
on(
|
||||||
const pendingDiff = () => tree.pendingDiff
|
sessionKey,
|
||||||
const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
|
() => {
|
||||||
const activeDiff = () => tree.activeDiff
|
setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined })
|
||||||
const setActiveDiff = (value: string | undefined) => setTree("activeDiff", value)
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const showAllFiles = () => {
|
const showAllFiles = () => {
|
||||||
if (fileTreeTab() !== "changes") return
|
if (fileTreeTab() !== "changes") return
|
||||||
|
|
@ -1151,8 +1437,8 @@ export default function Page() {
|
||||||
view={view}
|
view={view}
|
||||||
diffStyle={layout.review.diffStyle()}
|
diffStyle={layout.review.diffStyle()}
|
||||||
onDiffStyleChange={layout.review.setDiffStyle}
|
onDiffStyleChange={layout.review.setDiffStyle}
|
||||||
onScrollRef={setReviewScroll}
|
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||||
focusedFile={activeDiff()}
|
focusedFile={tree.activeDiff}
|
||||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
comments={comments.all()}
|
comments={comments.all()}
|
||||||
focusedComment={comments.focus()}
|
focusedComment={comments.focus()}
|
||||||
|
|
@ -1202,7 +1488,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewDiffTop = (path: string) => {
|
const reviewDiffTop = (path: string) => {
|
||||||
const root = reviewScroll()
|
const root = tree.reviewScroll
|
||||||
if (!root) return
|
if (!root) return
|
||||||
|
|
||||||
const id = reviewDiffId(path)
|
const id = reviewDiffId(path)
|
||||||
|
|
@ -1218,7 +1504,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToReviewDiff = (path: string) => {
|
const scrollToReviewDiff = (path: string) => {
|
||||||
const root = reviewScroll()
|
const root = tree.reviewScroll
|
||||||
if (!root) return false
|
if (!root) return false
|
||||||
|
|
||||||
const top = reviewDiffTop(path)
|
const top = reviewDiffTop(path)
|
||||||
|
|
@ -1232,24 +1518,23 @@ export default function Page() {
|
||||||
const focusReviewDiff = (path: string) => {
|
const focusReviewDiff = (path: string) => {
|
||||||
const current = view().review.open() ?? []
|
const current = view().review.open() ?? []
|
||||||
if (!current.includes(path)) view().review.setOpen([...current, path])
|
if (!current.includes(path)) view().review.setOpen([...current, path])
|
||||||
setActiveDiff(path)
|
setTree({ activeDiff: path, pendingDiff: path })
|
||||||
setPendingDiff(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const pending = pendingDiff()
|
const pending = tree.pendingDiff
|
||||||
if (!pending) return
|
if (!pending) return
|
||||||
if (!reviewScroll()) return
|
if (!tree.reviewScroll) return
|
||||||
if (!diffsReady()) return
|
if (!diffsReady()) return
|
||||||
|
|
||||||
const attempt = (count: number) => {
|
const attempt = (count: number) => {
|
||||||
if (pendingDiff() !== pending) return
|
if (tree.pendingDiff !== pending) return
|
||||||
if (count > 60) {
|
if (count > 60) {
|
||||||
setPendingDiff(undefined)
|
setTree("pendingDiff", undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = reviewScroll()
|
const root = tree.reviewScroll
|
||||||
if (!root) {
|
if (!root) {
|
||||||
requestAnimationFrame(() => attempt(count + 1))
|
requestAnimationFrame(() => attempt(count + 1))
|
||||||
return
|
return
|
||||||
|
|
@ -1267,7 +1552,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(root.scrollTop - top) <= 1) {
|
if (Math.abs(root.scrollTop - top) <= 1) {
|
||||||
setPendingDiff(undefined)
|
setTree("pendingDiff", undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1310,13 +1595,17 @@ export default function Page() {
|
||||||
void sync.session.diff(id)
|
void sync.session.diff(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let treeDir: string | undefined
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
const dir = sdk.directory
|
||||||
if (!isDesktop()) return
|
if (!isDesktop()) return
|
||||||
if (!layout.fileTree.opened()) return
|
if (!layout.fileTree.opened()) return
|
||||||
if (sync.status === "loading") return
|
if (sync.status === "loading") return
|
||||||
|
|
||||||
fileTreeTab()
|
fileTreeTab()
|
||||||
void file.tree.list("")
|
const refresh = treeDir !== dir
|
||||||
|
treeDir = dir
|
||||||
|
void (refresh ? file.tree.refresh("") : file.tree.list(""))
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoScroll = createAutoScroll({
|
const autoScroll = createAutoScroll({
|
||||||
|
|
@ -1351,6 +1640,18 @@ export default function Page() {
|
||||||
let scrollSpyFrame: number | undefined
|
let scrollSpyFrame: number | undefined
|
||||||
let scrollSpyTarget: HTMLDivElement | undefined
|
let scrollSpyTarget: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
sessionKey,
|
||||||
|
() => {
|
||||||
|
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
||||||
|
scrollSpyFrame = undefined
|
||||||
|
scrollSpyTarget = undefined
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const anchor = (id: string) => `message-${id}`
|
const anchor = (id: string) => `message-${id}`
|
||||||
|
|
||||||
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
||||||
|
|
@ -1465,20 +1766,14 @@ export default function Page() {
|
||||||
window.history.replaceState(null, "", `#${anchor(id)}`)
|
window.history.replaceState(null, "", `#${anchor(id)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
const sessionID = params.id
|
on(sessionKey, (key) => {
|
||||||
if (!sessionID) return
|
if (!params.id) return
|
||||||
const raw = sessionStorage.getItem("opencode.pendingMessage")
|
const messageID = layout.pendingMessage.consume(key)
|
||||||
if (!raw) return
|
if (!messageID) return
|
||||||
const parts = raw.split("|")
|
setUi("pendingMessage", messageID)
|
||||||
const pendingSessionID = parts[0]
|
}),
|
||||||
const messageID = parts[1]
|
)
|
||||||
if (!pendingSessionID || !messageID) return
|
|
||||||
if (pendingSessionID !== sessionID) return
|
|
||||||
|
|
||||||
sessionStorage.removeItem("opencode.pendingMessage")
|
|
||||||
setUi("pendingMessage", messageID)
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
||||||
const root = scroller
|
const root = scroller
|
||||||
|
|
@ -1692,7 +1987,7 @@ export default function Page() {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!prompt.ready()) return
|
if (!prompt.ready()) return
|
||||||
handoff.prompt = previewPrompt()
|
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -1712,20 +2007,22 @@ export default function Page() {
|
||||||
return language.t("terminal.title")
|
return language.t("terminal.title")
|
||||||
}
|
}
|
||||||
|
|
||||||
handoff.terminals = terminal.all().map(label)
|
touch(handoff.terminal, params.dir!, terminal.all().map(label))
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!file.ready()) return
|
if (!file.ready()) return
|
||||||
handoff.files = Object.fromEntries(
|
setSessionHandoff(sessionKey(), {
|
||||||
tabs()
|
files: Object.fromEntries(
|
||||||
.all()
|
tabs()
|
||||||
.flatMap((tab) => {
|
.all()
|
||||||
const path = file.pathFromTab(tab)
|
.flatMap((tab) => {
|
||||||
if (!path) return []
|
const path = file.pathFromTab(tab)
|
||||||
return [[path, file.selectedLines(path) ?? null] as const]
|
if (!path) return []
|
||||||
}),
|
return [[path, file.selectedLines(path) ?? null] as const]
|
||||||
)
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|
@ -1801,7 +2098,7 @@ export default function Page() {
|
||||||
diffs={diffs}
|
diffs={diffs}
|
||||||
view={view}
|
view={view}
|
||||||
diffStyle="unified"
|
diffStyle="unified"
|
||||||
focusedFile={activeDiff()}
|
focusedFile={tree.activeDiff}
|
||||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||||
comments={comments.all()}
|
comments={comments.all()}
|
||||||
focusedComment={comments.focus()}
|
focusedComment={comments.focus()}
|
||||||
|
|
@ -1954,20 +2251,108 @@ export default function Page() {
|
||||||
centered(),
|
centered(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="h-10 flex items-center gap-1">
|
<div class="h-10 w-full flex items-center justify-between gap-2">
|
||||||
<Show when={info()?.parentID}>
|
<div class="flex items-center gap-1 min-w-0 flex-1">
|
||||||
<IconButton
|
<Show when={info()?.parentID}>
|
||||||
tabIndex={-1}
|
<IconButton
|
||||||
icon="arrow-left"
|
tabIndex={-1}
|
||||||
variant="ghost"
|
icon="arrow-left"
|
||||||
onClick={() => {
|
variant="ghost"
|
||||||
navigate(`/${params.dir}/session/${info()?.parentID}`)
|
onClick={() => {
|
||||||
}}
|
navigate(`/${params.dir}/session/${info()?.parentID}`)
|
||||||
aria-label={language.t("common.goBack")}
|
}}
|
||||||
/>
|
aria-label={language.t("common.goBack")}
|
||||||
</Show>
|
/>
|
||||||
<Show when={info()?.title}>
|
</Show>
|
||||||
<h1 class="text-16-medium text-text-strong truncate">{info()?.title}</h1>
|
<Show when={info()?.title || title.editing}>
|
||||||
|
<Show
|
||||||
|
when={title.editing}
|
||||||
|
fallback={
|
||||||
|
<h1
|
||||||
|
class="text-16-medium text-text-strong truncate min-w-0"
|
||||||
|
onDblClick={openTitleEditor}
|
||||||
|
>
|
||||||
|
{info()?.title}
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InlineInput
|
||||||
|
ref={(el) => {
|
||||||
|
titleRef = el
|
||||||
|
}}
|
||||||
|
value={title.draft}
|
||||||
|
disabled={title.saving}
|
||||||
|
class="text-16-medium text-text-strong grow-1 min-w-0"
|
||||||
|
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
void saveTitleEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
closeTitleEditor()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => closeTitleEditor()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={params.id}>
|
||||||
|
{(id) => (
|
||||||
|
<div class="shrink-0 flex items-center">
|
||||||
|
<DropdownMenu
|
||||||
|
open={title.menuOpen}
|
||||||
|
onOpenChange={(open) => setTitle("menuOpen", open)}
|
||||||
|
>
|
||||||
|
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="dot-grid"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (!title.pendingRename) return
|
||||||
|
event.preventDefault()
|
||||||
|
setTitle("pendingRename", false)
|
||||||
|
openTitleEditor()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setTitle({ pendingRename: true, menuOpen: false })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("common.rename")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("common.archive")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("common.delete")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2147,7 +2532,7 @@ export default function Page() {
|
||||||
when={prompt.ready()}
|
when={prompt.ready()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
||||||
{handoff.prompt || language.t("prompt.loading")}
|
{handoff.session.get(sessionKey())?.prompt || language.t("prompt.loading")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -2398,7 +2783,7 @@ export default function Page() {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return null
|
if (!p) return null
|
||||||
if (file.ready()) return file.selectedLines(p) ?? null
|
if (file.ready()) return file.selectedLines(p) ?? null
|
||||||
return handoff.files[p] ?? null
|
return handoff.session.get(sessionKey())?.files[p] ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
let wrap: HTMLDivElement | undefined
|
let wrap: HTMLDivElement | undefined
|
||||||
|
|
@ -2892,7 +3277,7 @@ export default function Page() {
|
||||||
allowed={diffFiles()}
|
allowed={diffFiles()}
|
||||||
kinds={kinds()}
|
kinds={kinds()}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
active={activeDiff()}
|
active={tree.activeDiff}
|
||||||
onFileClick={(node) => focusReviewDiff(node.path)}
|
onFileClick={(node) => focusReviewDiff(node.path)}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -2952,7 +3337,7 @@ export default function Page() {
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex flex-col h-full pointer-events-none">
|
<div class="flex flex-col h-full pointer-events-none">
|
||||||
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
|
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
|
||||||
<For each={handoff.terminals}>
|
<For each={handoff.terminal.get(params.dir!) ?? []}>
|
||||||
{(title) => (
|
{(title) => (
|
||||||
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
|
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
|
||||||
{title}
|
{title}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue