chore: cleanup (#17184)

This commit is contained in:
Adam
2026-03-12 08:52:51 -05:00
committed by GitHub
parent 13402529ce
commit 12efbbfa4c
17 changed files with 135 additions and 278 deletions

View File

@@ -6,7 +6,7 @@ import { Keybind } from "@opencode-ai/ui/keybind"
import { List } from "@opencode-ai/ui/list" import { List } from "@opencode-ai/ui/list"
import { base64Encode } from "@opencode-ai/util/encode" import { base64Encode } from "@opencode-ai/util/encode"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSDK } from "@/context/global-sdk"
@@ -14,6 +14,7 @@ import { useGlobalSync } from "@/context/global-sync"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file" import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSessionLayout } from "@/pages/session/session-layout"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { getRelativeTime } from "@/utils/time" import { getRelativeTime } from "@/utils/time"
@@ -259,14 +260,11 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const layout = useLayout() const layout = useLayout()
const file = useFile() const file = useFile()
const dialog = useDialog() const dialog = useDialog()
const params = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const { params, tabs, view } = useSessionLayout()
const filesOnly = () => props.mode === "files" const filesOnly = () => props.mode === "files"
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const state = { cleanup: undefined as (() => void) | void, committed: false } const state = { cleanup: undefined as (() => void) | void, committed: false }
const [grouped, setGrouped] = createSignal(false) const [grouped, setGrouped] = createSignal(false)
const commandEntries = createCommandEntries({ filesOnly, command, language }) const commandEntries = createCommandEntries({ filesOnly, command, language })

View File

@@ -17,7 +17,6 @@ import {
} from "@/context/prompt" } from "@/context/prompt"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useParams } from "@solidjs/router"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
@@ -37,6 +36,7 @@ import { Persist, persisted } from "@/utils/persist"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useSessionLayout } from "@/pages/session/session-layout"
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments" import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
import { import {
@@ -102,13 +102,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const prompt = usePrompt() const prompt = usePrompt()
const layout = useLayout() const layout = useLayout()
const comments = useComments() const comments = useComments()
const params = useParams()
const dialog = useDialog() const dialog = useDialog()
const providers = useProviders() const providers = useProviders()
const command = useCommand() const command = useCommand()
const permission = usePermission() const permission = usePermission()
const language = useLanguage() const language = useLanguage()
const platform = usePlatform() const platform = usePlatform()
const { params, tabs, view } = useSessionLayout()
let editorRef!: HTMLDivElement let editorRef!: HTMLDivElement
let fileInputRef: HTMLInputElement | undefined let fileInputRef: HTMLInputElement | undefined
let scrollRef!: HTMLDivElement let scrollRef!: HTMLDivElement
@@ -154,10 +154,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
requestAnimationFrame(scrollCursorIntoView) requestAnimationFrame(scrollCursorIntoView)
} }
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const commentInReview = (path: string) => { const commentInReview = (path: string) => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return false if (!sessionID) return false
@@ -255,6 +251,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}) })
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
const motion = (value: number) => ({
opacity: value,
transform: `scale(${0.95 + value * 0.05})`,
filter: `blur(${(1 - value) * 2}px)`,
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
})
const buttons = createMemo(() => motion(buttonsSpring()))
const shell = createMemo(() => motion(1 - buttonsSpring()))
const control = createMemo(() => ({ height: "28px", ...buttons() }))
const commentCount = createMemo(() => { const commentCount = createMemo(() => {
if (store.mode === "shell") return 0 if (store.mode === "shell") return 0
@@ -1275,11 +1280,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button" type="button"
variant="ghost" variant="ghost"
class="size-8 p-0" class="size-8 p-0"
style={{ style={buttons()}
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
onClick={pick} onClick={pick}
disabled={store.mode !== "normal"} disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1} tabIndex={store.mode === "normal" ? undefined : -1}
@@ -1317,11 +1318,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
icon={working() ? "stop" : "arrow-up"} icon={working() ? "stop" : "arrow-up"}
variant="primary" variant="primary"
class="size-8" class="size-8"
style={{ style={buttons()}
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")} aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/> />
</Tooltip> </Tooltip>
@@ -1379,10 +1376,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0" class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
style={{ style={{
padding: "0 4px 0 8px", padding: "0 4px 0 8px",
opacity: 1 - buttonsSpring(), ...shell(),
transform: `scale(${0.95 + (1 - buttonsSpring()) * 0.05})`,
filter: `blur(${buttonsSpring() * 2}px)`,
"pointer-events": buttonsSpring() < 0.5 ? "auto" : "none",
}} }}
> >
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span> <span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
@@ -1402,13 +1396,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onSelect={local.agent.set} onSelect={local.agent.set}
class="capitalize max-w-[160px]" class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular" valueClass="truncate text-13-regular"
triggerStyle={{ triggerStyle={control()}
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
variant="ghost" variant="ghost"
/> />
</TooltipKeybind> </TooltipKeybind>
@@ -1426,13 +1414,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost" variant="ghost"
size="normal" size="normal"
class="min-w-0 max-w-[320px] text-13-regular group" class="min-w-0 max-w-[320px] text-13-regular group"
style={{ style={control()}
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)} onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
> >
<Show when={local.model.current()?.provider?.id}> <Show when={local.model.current()?.provider?.id}>
@@ -1461,13 +1443,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
triggerProps={{ triggerProps={{
variant: "ghost", variant: "ghost",
size: "normal", size: "normal",
style: { style: control(),
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
},
class: "min-w-0 max-w-[320px] text-13-regular group", class: "min-w-0 max-w-[320px] text-13-regular group",
}} }}
> >
@@ -1499,13 +1475,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
class="capitalize max-w-[160px]" class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular" valueClass="truncate text-13-regular"
triggerStyle={{ triggerStyle={control()}
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
variant="ghost" variant="ghost"
/> />
</TooltipKeybind> </TooltipKeybind>

View File

@@ -2,12 +2,12 @@ import { Match, Show, Switch, createMemo } from "solid-js"
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip" import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle" import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { getSessionContextMetrics } from "@/components/session/session-context-metrics" import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
import { useSessionLayout } from "@/pages/session/session-layout"
interface SessionContextUsageProps { interface SessionContextUsageProps {
variant?: "button" | "indicator" variant?: "button" | "indicator"
@@ -27,14 +27,11 @@ function openSessionContext(args: {
export function SessionContextUsage(props: SessionContextUsageProps) { export function SessionContextUsage(props: SessionContextUsageProps) {
const sync = useSync() const sync = useSync()
const params = useParams()
const layout = useLayout() const layout = useLayout()
const language = useLanguage() const language = useLanguage()
const { params, tabs, view } = useSessionLayout()
const variant = createMemo(() => props.variant ?? "button") const variant = createMemo(() => props.variant ?? "button")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const usd = createMemo( const usd = createMemo(

View File

@@ -1,8 +1,6 @@
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
import type { JSX } from "solid-js" import type { JSX } from "solid-js"
import { useParams } from "@solidjs/router"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useLayout } from "@/context/layout"
import { checksum } from "@opencode-ai/util/encode" import { checksum } from "@opencode-ai/util/encode"
import { findLast } from "@opencode-ai/util/array" import { findLast } from "@opencode-ai/util/array"
import { same } from "@/utils/same" import { same } from "@/utils/same"
@@ -14,6 +12,7 @@ import { Markdown } from "@opencode-ai/ui/markdown"
import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSessionLayout } from "@/pages/session/session-layout"
import { getSessionContextMetrics } from "./session-context-metrics" import { getSessionContextMetrics } from "./session-context-metrics"
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
import { createSessionContextFormatter } from "./session-context-format" import { createSessionContextFormatter } from "./session-context-format"
@@ -91,13 +90,10 @@ const emptyMessages: Message[] = []
const emptyUserMessages: UserMessage[] = [] const emptyUserMessages: UserMessage[] = []
export function SessionContextTab() { export function SessionContextTab() {
const params = useParams()
const sync = useSync() const sync = useSync()
const layout = useLayout()
const language = useLanguage() const language = useLanguage()
const { params, view } = useSessionLayout()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const messages = createMemo( const messages = createMemo(

View File

@@ -10,7 +10,6 @@ import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { useParams } from "@solidjs/router"
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web" import { Portal } from "solid-js/web"
@@ -23,6 +22,7 @@ import { useServer } from "@/context/server"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal" import { useTerminal } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers" import { focusTerminalById } from "@/pages/session/helpers"
import { useSessionLayout } from "@/pages/session/session-layout"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { StatusPopover } from "../status-popover" import { StatusPopover } from "../status-popover"
@@ -225,13 +225,13 @@ function useSessionShare(args: {
export function SessionHeader() { export function SessionHeader() {
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const layout = useLayout() const layout = useLayout()
const params = useParams()
const command = useCommand() const command = useCommand()
const server = useServer() const server = useServer()
const sync = useSync() const sync = useSync()
const platform = usePlatform() const platform = usePlatform()
const language = useLanguage() const language = useLanguage()
const terminal = useTerminal() const terminal = useTerminal()
const { params, view } = useSessionLayout()
const projectDirectory = createMemo(() => decode64(params.dir) ?? "") const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
const project = createMemo(() => { const project = createMemo(() => {
@@ -249,8 +249,6 @@ export function SessionHeader() {
const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const showShare = createMemo(() => shareEnabled() && !!params.id) const showShare = createMemo(() => shareEnabled() && !!params.id)
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey))
const os = createMemo(() => detectOS(platform)) const os = createMemo(() => detectOS(platform))
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
@@ -282,10 +280,7 @@ export function SessionHeader() {
Promise.resolve(platform.checkAppExists?.(app.openWith)) Promise.resolve(platform.checkAppExists?.(app.openWith))
.then((value) => Boolean(value)) .then((value) => Boolean(value))
.catch(() => false) .catch(() => false)
.then((ok) => { .then((ok) => [app.id, ok] as const),
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
return [app.id, ok] as const
}),
), ),
).then((entries) => { ).then((entries) => {
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>) setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)

View File

@@ -2136,6 +2136,41 @@ export default function Layout(props: ParentProps) {
) )
} }
const projects = () => layout.projects.list()
const projectOverlay = () => <ProjectDragOverlay projects={projects} activeProject={() => store.activeProject} />
const sidebarContent = (mobile?: boolean) => (
<SidebarContent
mobile={mobile}
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={projects}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile={mobile} />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={projectOverlay}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() =>
mobile ? (
<SidebarPanel project={currentProject()} mobile />
) : (
<Show when={currentProject()} keyed>
{(project) => <SidebarPanel project={project} merged />}
</Show>
)
}
/>
)
return ( return (
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Titlebar /> <Titlebar />
@@ -2164,38 +2199,7 @@ export default function Layout(props: ParentProps) {
arm() arm()
}} }}
> >
<div class="@container w-full h-full contain-strict"> <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
<SidebarContent
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={() => layout.projects.list()}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={() => (
<ProjectDragOverlay
projects={() => layout.projects.list()}
activeProject={() => store.activeProject}
/>
)}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => (
<Show when={currentProject()} keyed>
{(project) => <SidebarPanel project={project} merged />}
</Show>
)}
/>
</div>
<Show when={layout.sidebar.opened()}> <Show when={layout.sidebar.opened()}>
<div onPointerDown={() => setSizing(true)}> <div onPointerDown={() => setSizing(true)}>
<ResizeHandle <ResizeHandle
@@ -2242,33 +2246,7 @@ export default function Layout(props: ParentProps) {
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<SidebarContent {sidebarContent(true)}
mobile
opened={() => layout.sidebar.opened()}
aimMove={aim.move}
projects={() => layout.projects.list()}
renderProject={(project) => (
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile />
)}
handleDragStart={handleDragStart}
handleDragEnd={handleDragEnd}
handleDragOver={handleDragOver}
openProjectLabel={language.t("command.project.open")}
openProjectKeybind={() => command.keybind("project.open")}
onOpenProject={chooseProject}
renderProjectOverlay={() => (
<ProjectDragOverlay
projects={() => layout.projects.list()}
activeProject={() => store.activeProject}
/>
)}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => <SidebarPanel project={currentProject()} mobile />}
/>
</nav> </nav>
</div> </div>

View File

@@ -9,10 +9,10 @@ import {
import { type Session } from "@opencode-ai/sdk/v2/client" import { type Session } from "@opencode-ai/sdk/v2/client"
import { import {
displayName, displayName,
effectiveWorkspaceOrder,
errorMessage, errorMessage,
hasProjectPermissions, hasProjectPermissions,
latestRootSession, latestRootSession,
syncWorkspaceOrder,
workspaceKey, workspaceKey,
} from "./helpers" } from "./helpers"
@@ -116,7 +116,7 @@ describe("layout workspace helpers", () => {
}) })
test("keeps local first while preserving known order", () => { test("keeps local first while preserving known order", () => {
const result = syncWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"]) const result = effectiveWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"])
expect(result).toEqual(["/root", "/c", "/b"]) expect(result).toEqual(["/root", "/c", "/b"])
}) })

View File

@@ -8,7 +8,7 @@ export const workspaceKey = (directory: string) => {
return directory.replace(/[\\/]+$/, "") return directory.replace(/[\\/]+$/, "")
} }
export function sortSessions(now: number) { function sortSessions(now: number) {
const oneMinuteAgo = now - 60 * 1000 const oneMinuteAgo = now - 60 * 1000
return (a: Session, b: Session) => { return (a: Session, b: Session) => {
const aUpdated = a.time.updated ?? a.time.created const aUpdated = a.time.updated ?? a.time.created
@@ -22,7 +22,7 @@ export function sortSessions(now: number) {
} }
} }
export const isRootVisibleSession = (session: Session, directory: string) => const isRootVisibleSession = (session: Session, directory: string) =>
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) => export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
@@ -90,5 +90,3 @@ export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted
return [...result, ...live.values()] return [...result, ...live.values()]
} }
export const syncWorkspaceOrder = effectiveWorkspaceOrder

View File

@@ -20,10 +20,11 @@ import { createStore } from "solid-js/store"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Select } from "@opencode-ai/ui/select" import { Select } from "@opencode-ai/ui/select"
import { createAutoScroll } from "@opencode-ai/ui/hooks" import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode, checksum } from "@opencode-ai/util/encode" import { base64Encode, checksum } from "@opencode-ai/util/encode"
import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { useNavigate, useSearchParams } from "@solidjs/router"
import { NewSessionView, SessionHeader } from "@/components/session" import { NewSessionView, SessionHeader } from "@/components/session"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
@@ -37,6 +38,7 @@ import { createSessionComposerState, SessionComposerRegion } from "@/pages/sessi
import { createOpenReviewFile, createSizing, focusTerminalById } from "@/pages/session/helpers" import { createOpenReviewFile, createSizing, focusTerminalById } from "@/pages/session/helpers"
import { MessageTimeline } from "@/pages/session/message-timeline" import { MessageTimeline } from "@/pages/session/message-timeline"
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { useSessionLayout } from "@/pages/session/session-layout"
import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers" import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs" import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
import { SessionSidePanel } from "@/pages/session/session-side-panel" import { SessionSidePanel } from "@/pages/session/session-side-panel"
@@ -264,13 +266,13 @@ export default function Page() {
const sync = useSync() const sync = useSync()
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const params = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const sdk = useSDK() const sdk = useSDK()
const prompt = usePrompt() const prompt = usePrompt()
const comments = useComments() const comments = useComments()
const terminal = useTerminal() const terminal = useTerminal()
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
const { params, sessionKey, tabs, view } = useSessionLayout()
createEffect(() => { createEffect(() => {
if (!untrack(() => prompt.ready())) return if (!untrack(() => prompt.ready())) return
@@ -298,11 +300,8 @@ export default function Page() {
const composer = createSessionComposerState() const composer = createSessionComposerState()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const workspaceKey = createMemo(() => params.dir ?? "") const workspaceKey = createMemo(() => params.dir ?? "")
const workspaceTabs = createMemo(() => layout.tabs(workspaceKey)) const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
createEffect( createEffect(
on( on(
@@ -670,11 +669,7 @@ export default function Page() {
const selectionPreview = (path: string, selection: FileSelection) => { const selectionPreview = (path: string, selection: FileSelection) => {
const content = file.get(path)?.content?.content const content = file.get(path)?.content?.content
if (!content) return undefined if (!content) return undefined
const start = Math.max(1, Math.min(selection.startLine, selection.endLine)) return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine })
const end = Math.max(selection.startLine, selection.endLine)
const lines = content.split("\n").slice(start - 1, end)
if (lines.length === 0) return undefined
return lines.slice(0, 2).join("\n")
} }
const addCommentToContext = (input: { const addCommentToContext = (input: {

View File

@@ -1,11 +1,10 @@
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useParams } from "@solidjs/router"
import { useSpring } from "@opencode-ai/ui/motion-spring" import { useSpring } from "@opencode-ai/ui/motion-spring"
import { PromptInput } from "@/components/prompt-input" import { PromptInput } from "@/components/prompt-input"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
import { useSessionKey } from "@/pages/session/session-layout"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock" import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
@@ -27,29 +26,11 @@ export function SessionComposerRegion(props: {
onRestore: (id: string) => void onRestore: (id: string) => void
} }
setPromptDockRef: (el: HTMLDivElement) => void setPromptDockRef: (el: HTMLDivElement) => void
visualDuration?: number
bounce?: number
dockOpenVisualDuration?: number
dockOpenBounce?: number
dockCloseVisualDuration?: number
dockCloseBounce?: number
drawerExpandVisualDuration?: number
drawerExpandBounce?: number
drawerCollapseVisualDuration?: number
drawerCollapseBounce?: number
subtitleDuration?: number
subtitleTravel?: number
subtitleEdge?: number
countDuration?: number
countMask?: number
countMaskHeight?: number
countWidthDuration?: number
}) { }) {
const params = useParams()
const prompt = usePrompt() const prompt = usePrompt()
const language = useLanguage() const language = useLanguage()
const { sessionKey } = useSessionKey()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt) const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt)
const previewPrompt = () => const previewPrompt = () =>
@@ -69,9 +50,7 @@ export function SessionComposerRegion(props: {
setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
}) })
const [gate, setGate] = createStore({ const [ready, setReady] = createSignal(false)
ready: false,
})
let timer: number | undefined let timer: number | undefined
let frame: number | undefined let frame: number | undefined
@@ -88,17 +67,17 @@ export function SessionComposerRegion(props: {
createEffect(() => { createEffect(() => {
sessionKey() sessionKey()
const ready = props.ready const active = props.ready
const delay = 140 const delay = 140
clear() clear()
setGate("ready", false) setReady(false)
if (!ready) return if (!active) return
frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => {
frame = undefined frame = undefined
timer = window.setTimeout(() => { timer = window.setTimeout(() => {
setGate("ready", true) setReady(true)
timer = undefined timer = undefined
}, delay) }, delay)
}) })
@@ -106,22 +85,11 @@ export function SessionComposerRegion(props: {
onCleanup(clear) onCleanup(clear)
const open = createMemo(() => gate.ready && props.state.dock() && !props.state.closing()) const open = createMemo(() => ready() && props.state.dock() && !props.state.closing())
const config = createMemo(() => const progress = useSpring(() => (open() ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
open()
? {
visualDuration: props.dockOpenVisualDuration ?? props.visualDuration ?? 0.3,
bounce: props.dockOpenBounce ?? props.bounce ?? 0,
}
: {
visualDuration: props.dockCloseVisualDuration ?? props.visualDuration ?? 0.3,
bounce: props.dockCloseBounce ?? props.bounce ?? 0,
},
)
const progress = useSpring(() => (open() ? 1 : 0), config)
const value = createMemo(() => Math.max(0, Math.min(1, progress()))) const value = createMemo(() => Math.max(0, Math.min(1, progress())))
const [height, setHeight] = createSignal(320) const [height, setHeight] = createSignal(320)
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001) const dock = createMemo(() => (ready() && props.state.dock()) || value() > 0.001)
const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined)) const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
const lift = createMemo(() => (rolled() ? 18 : 36 * value())) const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
const full = createMemo(() => Math.max(78, height())) const full = createMemo(() => Math.max(78, height()))
@@ -213,19 +181,6 @@ export function SessionComposerRegion(props: {
collapseLabel={language.t("session.todo.collapse")} collapseLabel={language.t("session.todo.collapse")}
expandLabel={language.t("session.todo.expand")} expandLabel={language.t("session.todo.expand")}
dockProgress={value()} dockProgress={value()}
visualDuration={props.visualDuration}
bounce={props.bounce}
expandVisualDuration={props.drawerExpandVisualDuration}
expandBounce={props.drawerExpandBounce}
collapseVisualDuration={props.drawerCollapseVisualDuration}
collapseBounce={props.drawerCollapseBounce}
subtitleDuration={props.subtitleDuration}
subtitleTravel={props.subtitleTravel}
subtitleEdge={props.subtitleEdge}
countDuration={props.countDuration}
countMask={props.countMask}
countMaskHeight={props.countMaskHeight}
countWidthDuration={props.countWidthDuration}
/> />
</div> </div>
</div> </div>

View File

@@ -7,7 +7,6 @@ import { useSpring } from "@opencode-ai/ui/motion-spring"
import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
import { Index, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" import { Index, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
function dot(status: Todo["status"]) { function dot(status: Todo["status"]) {
if (status !== "in_progress") return undefined if (status !== "in_progress") return undefined
@@ -39,26 +38,10 @@ export function SessionTodoDock(props: {
title: string title: string
collapseLabel: string collapseLabel: string
expandLabel: string expandLabel: string
dockProgress?: number dockProgress: number
visualDuration?: number
bounce?: number
expandVisualDuration?: number
expandBounce?: number
collapseVisualDuration?: number
collapseBounce?: number
subtitleDuration?: number
subtitleTravel?: number
subtitleEdge?: number
countDuration?: number
countMask?: number
countMaskHeight?: number
countWidthDuration?: number
}) { }) {
const [store, setStore] = createStore({ const [collapsed, setCollapsed] = createSignal(false)
collapsed: false, const toggle = () => setCollapsed((value) => !value)
})
const toggle = () => setStore("collapsed", (value) => !value)
const total = createMemo(() => props.todos.length) const total = createMemo(() => props.todos.length)
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length) const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
@@ -73,19 +56,8 @@ export function SessionTodoDock(props: {
) )
const preview = createMemo(() => active()?.content ?? "") const preview = createMemo(() => active()?.content ?? "")
const config = createMemo(() => const collapse = useSpring(() => (collapsed() ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
store.collapsed const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
? {
visualDuration: props.collapseVisualDuration ?? props.visualDuration ?? 0.3,
bounce: props.collapseBounce ?? props.bounce ?? 0,
}
: {
visualDuration: props.expandVisualDuration ?? props.visualDuration ?? 0.3,
bounce: props.expandBounce ?? props.bounce ?? 0,
},
)
const collapse = useSpring(() => (store.collapsed ? 1 : 0), config)
const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress ?? 1)))
const shut = createMemo(() => 1 - dock()) const shut = createMemo(() => 1 - dock())
const value = createMemo(() => Math.max(0, Math.min(1, collapse()))) const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
const hide = createMemo(() => Math.max(value(), shut())) const hide = createMemo(() => Math.max(value(), shut()))
@@ -133,10 +105,10 @@ export function SessionTodoDock(props: {
class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 whitespace-nowrap overflow-visible" class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 whitespace-nowrap overflow-visible"
aria-label={label()} aria-label={label()}
style={{ style={{
"--tool-motion-odometer-ms": `${props.countDuration ?? 600}ms`, "--tool-motion-odometer-ms": "600ms",
"--tool-motion-mask": `${props.countMask ?? 18}%`, "--tool-motion-mask": "18%",
"--tool-motion-mask-height": `${props.countMaskHeight ?? 0}px`, "--tool-motion-mask-height": "0px",
"--tool-motion-spring-ms": `${props.countWidthDuration ?? 560}ms`, "--tool-motion-spring-ms": "560ms",
opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`, opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`,
}} }}
> >
@@ -155,10 +127,10 @@ export function SessionTodoDock(props: {
> >
<TextReveal <TextReveal
class="text-14-regular text-text-base cursor-default" class="text-14-regular text-text-base cursor-default"
text={store.collapsed ? preview() : undefined} text={collapsed() ? preview() : undefined}
duration={props.subtitleDuration ?? 600} duration={600}
travel={props.subtitleTravel ?? 25} travel={25}
edge={props.subtitleEdge ?? 17} edge={17}
spring="cubic-bezier(0.34, 1, 0.64, 1)" spring="cubic-bezier(0.34, 1, 0.64, 1)"
springSoft="cubic-bezier(0.34, 1, 0.64, 1)" springSoft="cubic-bezier(0.34, 1, 0.64, 1)"
growOnly growOnly
@@ -168,7 +140,7 @@ export function SessionTodoDock(props: {
<div class="ml-auto"> <div class="ml-auto">
<IconButton <IconButton
data-action="session-todo-toggle-button" data-action="session-todo-toggle-button"
data-collapsed={store.collapsed ? "true" : "false"} data-collapsed={collapsed() ? "true" : "false"}
icon="chevron-down" icon="chevron-down"
size="normal" size="normal"
variant="ghost" variant="ghost"
@@ -181,14 +153,14 @@ export function SessionTodoDock(props: {
event.stopPropagation() event.stopPropagation()
toggle() toggle()
}} }}
aria-label={store.collapsed ? props.expandLabel : props.collapseLabel} aria-label={collapsed() ? props.expandLabel : props.collapseLabel}
/> />
</div> </div>
</div> </div>
<div <div
data-slot="session-todo-list" data-slot="session-todo-list"
aria-hidden={store.collapsed || off()} aria-hidden={collapsed() || off()}
classList={{ classList={{
"pointer-events-none": hide() > 0.1, "pointer-events-none": hide() > 0.1,
}} }}
@@ -197,7 +169,7 @@ export function SessionTodoDock(props: {
opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`, opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`,
}} }}
> >
<TodoList todos={props.todos} open={!store.collapsed} /> <TodoList todos={props.todos} open={!collapsed()} />
</div> </div>
</div> </div>
</DockTray> </DockTray>

View File

@@ -1,7 +1,6 @@
import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js" import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { useParams } from "@solidjs/router"
import type { FileSearchHandle } from "@opencode-ai/ui/file" import type { FileSearchHandle } from "@opencode-ai/ui/file"
import { useFileComponent } from "@opencode-ai/ui/context/file" import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
@@ -12,12 +11,12 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { useLayout } from "@/context/layout"
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { getSessionHandoff } from "@/pages/session/handoff" import { getSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
function FileCommentMenu(props: { function FileCommentMenu(props: {
moreLabel: string moreLabel: string
@@ -53,17 +52,12 @@ function FileCommentMenu(props: {
} }
export function FileTabContent(props: { tab: string }) { export function FileTabContent(props: { tab: string }) {
const params = useParams()
const layout = useLayout()
const file = useFile() const file = useFile()
const comments = useComments() const comments = useComments()
const language = useLanguage() const language = useLanguage()
const prompt = usePrompt() const prompt = usePrompt()
const fileComponent = useFileComponent() const fileComponent = useFileComponent()
const { sessionKey, tabs, view } = useSessionLayout()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
let scroll: HTMLDivElement | undefined let scroll: HTMLDivElement | undefined
let scrollFrame: number | undefined let scrollFrame: number | undefined

View File

@@ -1,6 +1,6 @@
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { FileIcon } from "@opencode-ai/ui/file-icon" import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
@@ -19,6 +19,7 @@ import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/
import { SessionContextUsage } from "@/components/session-context-usage" import { SessionContextUsage } from "@/components/session-context-usage"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSessionKey } from "@/pages/session/session-layout"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
@@ -213,16 +214,15 @@ export function MessageTimeline(props: {
}) { }) {
let touchGesture: number | undefined let touchGesture: number | undefined
const params = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const settings = useSettings() const settings = useSettings()
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const { params, sessionKey } = useSessionKey()
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id)) const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const sessionID = createMemo(() => params.id) const sessionID = createMemo(() => params.id)
const sessionMessages = createMemo(() => { const sessionMessages = createMemo(() => {
const id = sessionID() const id = sessionID()

View File

@@ -0,0 +1,20 @@
import { useParams } from "@solidjs/router"
import { createMemo } from "solid-js"
import { useLayout } from "@/context/layout"
export const useSessionKey = () => {
const params = useParams()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
return { params, sessionKey }
}
export const useSessionLayout = () => {
const layout = useLayout()
const { params, sessionKey } = useSessionKey()
return {
params,
sessionKey,
tabs: createMemo(() => layout.tabs(sessionKey)),
view: createMemo(() => layout.view(sessionKey)),
}
}

View File

@@ -1,7 +1,6 @@
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
import { useParams } from "@solidjs/router"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
@@ -26,6 +25,7 @@ import { FileTabContent } from "@/pages/session/file-tabs"
import { createOpenSessionFileTab, getTabReorderIndex, type Sizing } from "@/pages/session/helpers" import { createOpenSessionFileTab, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
import { StickyAddButton } from "@/pages/session/review-tab" import { StickyAddButton } from "@/pages/session/review-tab"
import { setSessionHandoff } from "@/pages/session/handoff" import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
export function SessionSidePanel(props: { export function SessionSidePanel(props: {
reviewPanel: () => JSX.Element reviewPanel: () => JSX.Element
@@ -34,18 +34,15 @@ export function SessionSidePanel(props: {
reviewSnap: boolean reviewSnap: boolean
size: Sizing size: Sizing
}) { }) {
const params = useParams()
const layout = useLayout() const layout = useLayout()
const sync = useSync() const sync = useSync()
const file = useFile() const file = useFile()
const language = useLanguage() const language = useLanguage()
const command = useCommand() const command = useCommand()
const dialog = useDialog() const dialog = useDialog()
const { params, sessionKey, tabs, view } = useSessionLayout()
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())

View File

@@ -1,6 +1,5 @@
import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js" import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useParams } from "@solidjs/router"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -18,16 +17,14 @@ import { useTerminal, type LocalPTY } from "@/context/terminal"
import { terminalTabLabel } from "@/pages/session/terminal-label" import { terminalTabLabel } from "@/pages/session/terminal-label"
import { createSizing, focusTerminalById } from "@/pages/session/helpers" import { createSizing, focusTerminalById } from "@/pages/session/helpers"
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
export function TerminalPanel() { export function TerminalPanel() {
const params = useParams()
const layout = useLayout() const layout = useLayout()
const terminal = useTerminal() const terminal = useTerminal()
const language = useLanguage() const language = useLanguage()
const command = useCommand() const command = useCommand()
const { params, view } = useSessionLayout()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey))
const opened = createMemo(() => view().terminal.opened()) const opened = createMemo(() => view().terminal.opened())
const size = createSizing() const size = createSizing()

View File

@@ -1,7 +1,8 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { useCommand, type CommandOption } from "@/context/command" import { useCommand, type CommandOption } from "@/context/command"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
@@ -19,6 +20,7 @@ import { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/util/array" import { findLast } from "@opencode-ai/util/array"
import { extractPromptFromParts } from "@/utils/prompt" import { extractPromptFromParts } from "@/utils/prompt"
import { UserMessage } from "@opencode-ai/sdk/v2" import { UserMessage } from "@opencode-ai/sdk/v2"
import { useSessionLayout } from "@/pages/session/session-layout"
export type SessionCommandContext = { export type SessionCommandContext = {
navigateMessageByOffset: (offset: number) => void navigateMessageByOffset: (offset: number) => void
@@ -45,12 +47,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sync = useSync() const sync = useSync()
const terminal = useTerminal() const terminal = useTerminal()
const layout = useLayout() const layout = useLayout()
const params = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const { params, tabs, view } = useSessionLayout()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const idle = { type: "idle" as const } const idle = { type: "idle" as const }
@@ -71,11 +70,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const selectionPreview = (path: string, selection: FileSelection) => { const selectionPreview = (path: string, selection: FileSelection) => {
const content = file.get(path)?.content?.content const content = file.get(path)?.content?.content
if (!content) return undefined if (!content) return undefined
const start = Math.max(1, Math.min(selection.startLine, selection.endLine)) return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine })
const end = Math.max(selection.startLine, selection.endLine)
const lines = content.split("\n").slice(start - 1, end)
if (lines.length === 0) return undefined
return lines.slice(0, 2).join("\n")
} }
const addSelectionToContext = (path: string, selection: FileSelection) => { const addSelectionToContext = (path: string, selection: FileSelection) => {