From 12efbbfa4c49631f8a0201459a0956f78461b355 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:52:51 -0500 Subject: [PATCH] chore: cleanup (#17184) --- .../app/src/components/dialog-select-file.tsx | 8 +- packages/app/src/components/prompt-input.tsx | 66 ++++--------- .../src/components/session-context-usage.tsx | 7 +- .../session/session-context-tab.tsx | 8 +- .../src/components/session/session-header.tsx | 11 +-- packages/app/src/pages/layout.tsx | 96 +++++++------------ packages/app/src/pages/layout/helpers.test.ts | 4 +- packages/app/src/pages/layout/helpers.ts | 6 +- packages/app/src/pages/session.tsx | 15 +-- .../composer/session-composer-region.tsx | 65 ++----------- .../session/composer/session-todo-dock.tsx | 62 ++++-------- packages/app/src/pages/session/file-tabs.tsx | 10 +- .../src/pages/session/message-timeline.tsx | 6 +- .../app/src/pages/session/session-layout.ts | 20 ++++ .../src/pages/session/session-side-panel.tsx | 7 +- .../app/src/pages/session/terminal-panel.tsx | 7 +- .../pages/session/use-session-commands.tsx | 15 +-- 17 files changed, 135 insertions(+), 278 deletions(-) create mode 100644 packages/app/src/pages/session/session-layout.ts diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index b530aff53..9b88cff90 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -6,7 +6,7 @@ import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" import { base64Encode } from "@opencode-ai/util/encode" 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 { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { useGlobalSDK } from "@/context/global-sdk" @@ -14,6 +14,7 @@ import { useGlobalSync } from "@/context/global-sync" import { useLayout } from "@/context/layout" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" +import { useSessionLayout } from "@/pages/session/session-layout" import { decode64 } from "@/utils/base64" import { getRelativeTime } from "@/utils/time" @@ -259,14 +260,11 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const layout = useLayout() const file = useFile() const dialog = useDialog() - const params = useParams() const navigate = useNavigate() const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() + const { params, tabs, view } = useSessionLayout() 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 [grouped, setGrouped] = createSignal(false) const commandEntries = createCommandEntries({ filesOnly, command, language }) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 3ee8f4351..f1a33e75f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -17,7 +17,6 @@ import { } from "@/context/prompt" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" -import { useParams } from "@solidjs/router" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" @@ -37,6 +36,7 @@ import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useSessionLayout } from "@/pages/session/session-layout" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments" import { @@ -102,13 +102,13 @@ export const PromptInput: Component = (props) => { const prompt = usePrompt() const layout = useLayout() const comments = useComments() - const params = useParams() const dialog = useDialog() const providers = useProviders() const command = useCommand() const permission = usePermission() const language = useLanguage() const platform = usePlatform() + const { params, tabs, view } = useSessionLayout() let editorRef!: HTMLDivElement let fileInputRef: HTMLInputElement | undefined let scrollRef!: HTMLDivElement @@ -154,10 +154,6 @@ export const PromptInput: Component = (props) => { 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 sessionID = params.id if (!sessionID) return false @@ -255,6 +251,15 @@ export const PromptInput: Component = (props) => { }) 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(() => { if (store.mode === "shell") return 0 @@ -1275,11 +1280,7 @@ export const PromptInput: Component = (props) => { type="button" variant="ghost" class="size-8 p-0" - style={{ - opacity: buttonsSpring(), - transform: `scale(${0.95 + buttonsSpring() * 0.05})`, - filter: `blur(${(1 - buttonsSpring()) * 2}px)`, - }} + style={buttons()} onClick={pick} disabled={store.mode !== "normal"} tabIndex={store.mode === "normal" ? undefined : -1} @@ -1317,11 +1318,7 @@ export const PromptInput: Component = (props) => { icon={working() ? "stop" : "arrow-up"} variant="primary" class="size-8" - style={{ - opacity: buttonsSpring(), - transform: `scale(${0.95 + buttonsSpring() * 0.05})`, - filter: `blur(${(1 - buttonsSpring()) * 2}px)`, - }} + style={buttons()} aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")} /> @@ -1379,10 +1376,7 @@ export const PromptInput: Component = (props) => { class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0" style={{ padding: "0 4px 0 8px", - opacity: 1 - buttonsSpring(), - transform: `scale(${0.95 + (1 - buttonsSpring()) * 0.05})`, - filter: `blur(${buttonsSpring() * 2}px)`, - "pointer-events": buttonsSpring() < 0.5 ? "auto" : "none", + ...shell(), }} > {language.t("prompt.mode.shell")} @@ -1402,13 +1396,7 @@ export const PromptInput: Component = (props) => { onSelect={local.agent.set} class="capitalize max-w-[160px]" valueClass="truncate text-13-regular" - triggerStyle={{ - height: "28px", - opacity: buttonsSpring(), - transform: `scale(${0.95 + buttonsSpring() * 0.05})`, - filter: `blur(${(1 - buttonsSpring()) * 2}px)`, - "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none", - }} + triggerStyle={control()} variant="ghost" /> @@ -1426,13 +1414,7 @@ export const PromptInput: Component = (props) => { variant="ghost" size="normal" class="min-w-0 max-w-[320px] text-13-regular group" - style={{ - height: "28px", - opacity: buttonsSpring(), - transform: `scale(${0.95 + buttonsSpring() * 0.05})`, - filter: `blur(${(1 - buttonsSpring()) * 2}px)`, - "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none", - }} + style={control()} onClick={() => dialog.show(() => )} > @@ -1461,13 +1443,7 @@ export const PromptInput: Component = (props) => { triggerProps={{ variant: "ghost", size: "normal", - style: { - height: "28px", - opacity: buttonsSpring(), - transform: `scale(${0.95 + buttonsSpring() * 0.05})`, - filter: `blur(${(1 - buttonsSpring()) * 2}px)`, - "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none", - }, + style: control(), class: "min-w-0 max-w-[320px] text-13-regular group", }} > @@ -1499,13 +1475,7 @@ export const PromptInput: Component = (props) => { onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} class="capitalize max-w-[160px]" valueClass="truncate text-13-regular" - triggerStyle={{ - height: "28px", - opacity: buttonsSpring(), - transform: `scale(${0.95 + buttonsSpring() * 0.05})`, - filter: `blur(${(1 - buttonsSpring()) * 2}px)`, - "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none", - }} + triggerStyle={control()} variant="ghost" /> diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 08ae4d319..99e6c13a3 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -2,12 +2,12 @@ import { Match, Show, Switch, createMemo } from "solid-js" import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip" import { ProgressCircle } from "@opencode-ai/ui/progress-circle" import { Button } from "@opencode-ai/ui/button" -import { useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" import { useSync } from "@/context/sync" import { useLanguage } from "@/context/language" import { getSessionContextMetrics } from "@/components/session/session-context-metrics" +import { useSessionLayout } from "@/pages/session/session-layout" interface SessionContextUsageProps { variant?: "button" | "indicator" @@ -27,14 +27,11 @@ function openSessionContext(args: { export function SessionContextUsage(props: SessionContextUsageProps) { const sync = useSync() - const params = useParams() const layout = useLayout() const language = useLanguage() + const { params, tabs, view } = useSessionLayout() 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 usd = createMemo( diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 39eb4b4c0..9aa101bdb 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -1,8 +1,6 @@ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import type { JSX } from "solid-js" -import { useParams } from "@solidjs/router" import { useSync } from "@/context/sync" -import { useLayout } from "@/context/layout" import { checksum } from "@opencode-ai/util/encode" import { findLast } from "@opencode-ai/util/array" 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 type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import { useLanguage } from "@/context/language" +import { useSessionLayout } from "@/pages/session/session-layout" import { getSessionContextMetrics } from "./session-context-metrics" import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" import { createSessionContextFormatter } from "./session-context-format" @@ -91,13 +90,10 @@ const emptyMessages: Message[] = [] const emptyUserMessages: UserMessage[] = [] export function SessionContextTab() { - const params = useParams() const sync = useSync() - const layout = useLayout() 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 messages = createMemo( diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 97f0530e9..9476f8b9b 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -10,7 +10,6 @@ import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/util/path" -import { useParams } from "@solidjs/router" import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" @@ -23,6 +22,7 @@ import { useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { focusTerminalById } from "@/pages/session/helpers" +import { useSessionLayout } from "@/pages/session/session-layout" import { decode64 } from "@/utils/base64" import { Persist, persisted } from "@/utils/persist" import { StatusPopover } from "../status-popover" @@ -225,13 +225,13 @@ function useSessionShare(args: { export function SessionHeader() { const globalSDK = useGlobalSDK() const layout = useLayout() - const params = useParams() const command = useCommand() const server = useServer() const sync = useSync() const platform = usePlatform() const language = useLanguage() const terminal = useTerminal() + const { params, view } = useSessionLayout() const projectDirectory = createMemo(() => decode64(params.dir) ?? "") const project = createMemo(() => { @@ -249,8 +249,6 @@ export function SessionHeader() { const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") 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 [exists, setExists] = createStore>>({ @@ -282,10 +280,7 @@ export function SessionHeader() { Promise.resolve(platform.checkAppExists?.(app.openWith)) .then((value) => Boolean(value)) .catch(() => false) - .then((ok) => { - console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`) - return [app.id, ok] as const - }), + .then((ok) => [app.id, ok] as const), ), ).then((entries) => { setExists(Object.fromEntries(entries) as Partial>) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index fdc011bfb..eb3028101 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2136,6 +2136,41 @@ export default function Layout(props: ParentProps) { ) } + const projects = () => layout.projects.list() + const projectOverlay = () => store.activeProject} /> + const sidebarContent = (mobile?: boolean) => ( + layout.sidebar.opened()} + aimMove={aim.move} + projects={projects} + renderProject={(project) => ( + + )} + 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 ? ( + + ) : ( + + {(project) => } + + ) + } + /> + ) + return (
@@ -2164,38 +2199,7 @@ export default function Layout(props: ParentProps) { arm() }} > -
- layout.sidebar.opened()} - aimMove={aim.move} - projects={() => layout.projects.list()} - renderProject={(project) => ( - - )} - handleDragStart={handleDragStart} - handleDragEnd={handleDragEnd} - handleDragOver={handleDragOver} - openProjectLabel={language.t("command.project.open")} - openProjectKeybind={() => command.keybind("project.open")} - onOpenProject={chooseProject} - renderProjectOverlay={() => ( - 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={() => ( - - {(project) => } - - )} - /> -
+
{sidebarContent()}
setSizing(true)}> e.stopPropagation()} > - layout.sidebar.opened()} - aimMove={aim.move} - projects={() => layout.projects.list()} - renderProject={(project) => ( - - )} - handleDragStart={handleDragStart} - handleDragEnd={handleDragEnd} - handleDragOver={handleDragOver} - openProjectLabel={language.t("command.project.open")} - openProjectKeybind={() => command.keybind("project.open")} - onOpenProject={chooseProject} - renderProjectOverlay={() => ( - 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={() => } - /> + {sidebarContent(true)}
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 916b80214..9dbc6c72d 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -9,10 +9,10 @@ import { import { type Session } from "@opencode-ai/sdk/v2/client" import { displayName, + effectiveWorkspaceOrder, errorMessage, hasProjectPermissions, latestRootSession, - syncWorkspaceOrder, workspaceKey, } from "./helpers" @@ -116,7 +116,7 @@ describe("layout workspace helpers", () => { }) 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"]) }) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 8881b8a48..be4ce9f57 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -8,7 +8,7 @@ export const workspaceKey = (directory: string) => { return directory.replace(/[\\/]+$/, "") } -export function sortSessions(now: number) { +function sortSessions(now: number) { const oneMinuteAgo = now - 60 * 1000 return (a: Session, b: Session) => { 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 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()] } - -export const syncWorkspaceOrder = effectiveWorkspaceOrder diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 1b62b9429..c25463d75 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -20,10 +20,11 @@ import { createStore } from "solid-js/store" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Select } from "@opencode-ai/ui/select" import { createAutoScroll } from "@opencode-ai/ui/hooks" +import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" 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 { useComments } from "@/context/comments" 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 { MessageTimeline } from "@/pages/session/message-timeline" 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 { SessionMobileTabs } from "@/pages/session/session-mobile-tabs" import { SessionSidePanel } from "@/pages/session/session-side-panel" @@ -264,13 +266,13 @@ export default function Page() { const sync = useSync() const dialog = useDialog() const language = useLanguage() - const params = useParams() const navigate = useNavigate() const sdk = useSDK() const prompt = usePrompt() const comments = useComments() const terminal = useTerminal() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() + const { params, sessionKey, tabs, view } = useSessionLayout() createEffect(() => { if (!untrack(() => prompt.ready())) return @@ -298,11 +300,8 @@ export default function Page() { const composer = createSessionComposerState() - 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 view = createMemo(() => layout.view(sessionKey)) createEffect( on( @@ -670,11 +669,7 @@ export default function Page() { const selectionPreview = (path: string, selection: FileSelection) => { const content = file.get(path)?.content?.content if (!content) return undefined - const start = Math.max(1, Math.min(selection.startLine, 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") + return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) } const addCommentToContext = (input: { diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index 08746b51a..964bf18dd 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -1,11 +1,10 @@ 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 { PromptInput } from "@/components/prompt-input" import { useLanguage } from "@/context/language" import { usePrompt } from "@/context/prompt" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" +import { useSessionKey } from "@/pages/session/session-layout" import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock" import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" @@ -27,29 +26,11 @@ export function SessionComposerRegion(props: { onRestore: (id: string) => 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 language = useLanguage() + const { sessionKey } = useSessionKey() - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt) const previewPrompt = () => @@ -69,9 +50,7 @@ export function SessionComposerRegion(props: { setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) }) - const [gate, setGate] = createStore({ - ready: false, - }) + const [ready, setReady] = createSignal(false) let timer: number | undefined let frame: number | undefined @@ -88,17 +67,17 @@ export function SessionComposerRegion(props: { createEffect(() => { sessionKey() - const ready = props.ready + const active = props.ready const delay = 140 clear() - setGate("ready", false) - if (!ready) return + setReady(false) + if (!active) return frame = requestAnimationFrame(() => { frame = undefined timer = window.setTimeout(() => { - setGate("ready", true) + setReady(true) timer = undefined }, delay) }) @@ -106,22 +85,11 @@ export function SessionComposerRegion(props: { onCleanup(clear) - const open = createMemo(() => gate.ready && props.state.dock() && !props.state.closing()) - const config = createMemo(() => - 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 open = createMemo(() => ready() && props.state.dock() && !props.state.closing()) + const progress = useSpring(() => (open() ? 1 : 0), { visualDuration: 0.3, bounce: 0 }) const value = createMemo(() => Math.max(0, Math.min(1, progress()))) 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 lift = createMemo(() => (rolled() ? 18 : 36 * value())) const full = createMemo(() => Math.max(78, height())) @@ -213,19 +181,6 @@ export function SessionComposerRegion(props: { collapseLabel={language.t("session.todo.collapse")} expandLabel={language.t("session.todo.expand")} 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} />
diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx index 04aeb6317..baea51593 100644 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -7,7 +7,6 @@ import { useSpring } from "@opencode-ai/ui/motion-spring" import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" import { Index, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" function dot(status: Todo["status"]) { if (status !== "in_progress") return undefined @@ -39,26 +38,10 @@ export function SessionTodoDock(props: { title: string collapseLabel: string expandLabel: string - 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 + dockProgress: number }) { - const [store, setStore] = createStore({ - collapsed: false, - }) - - const toggle = () => setStore("collapsed", (value) => !value) + const [collapsed, setCollapsed] = createSignal(false) + const toggle = () => setCollapsed((value) => !value) const total = createMemo(() => props.todos.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 config = createMemo(() => - store.collapsed - ? { - 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 collapse = useSpring(() => (collapsed() ? 1 : 0), { visualDuration: 0.3, bounce: 0 }) + const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress))) const shut = createMemo(() => 1 - dock()) const value = createMemo(() => Math.max(0, Math.min(1, collapse()))) 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" aria-label={label()} style={{ - "--tool-motion-odometer-ms": `${props.countDuration ?? 600}ms`, - "--tool-motion-mask": `${props.countMask ?? 18}%`, - "--tool-motion-mask-height": `${props.countMaskHeight ?? 0}px`, - "--tool-motion-spring-ms": `${props.countWidthDuration ?? 560}ms`, + "--tool-motion-odometer-ms": "600ms", + "--tool-motion-mask": "18%", + "--tool-motion-mask-height": "0px", + "--tool-motion-spring-ms": "560ms", opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`, }} > @@ -155,10 +127,10 @@ export function SessionTodoDock(props: { >
0.1, }} @@ -197,7 +169,7 @@ export function SessionTodoDock(props: { opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`, }} > - +
diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 77643789d..4b322368f 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,7 +1,6 @@ import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" -import { useParams } from "@solidjs/router" import type { FileSearchHandle } from "@opencode-ai/ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" 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 { ScrollView } from "@opencode-ai/ui/scroll-view" import { showToast } from "@opencode-ai/ui/toast" -import { useLayout } from "@/context/layout" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { useComments } from "@/context/comments" import { useLanguage } from "@/context/language" import { usePrompt } from "@/context/prompt" import { getSessionHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" function FileCommentMenu(props: { moreLabel: string @@ -53,17 +52,12 @@ function FileCommentMenu(props: { } export function FileTabContent(props: { tab: string }) { - const params = useParams() - const layout = useLayout() const file = useFile() const comments = useComments() const language = useLanguage() const prompt = usePrompt() const fileComponent = useFileComponent() - - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const tabs = createMemo(() => layout.tabs(sessionKey)) - const view = createMemo(() => layout.view(sessionKey)) + const { sessionKey, tabs, view } = useSessionLayout() let scroll: HTMLDivElement | undefined let scrollFrame: number | undefined diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index e64f5a7fd..50f9b452a 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,6 +1,6 @@ import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" 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 { FileIcon } from "@opencode-ai/ui/file-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 { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" +import { useSessionKey } from "@/pages/session/session-layout" import { useSettings } from "@/context/settings" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" @@ -213,16 +214,15 @@ export function MessageTimeline(props: { }) { let touchGesture: number | undefined - const params = useParams() const navigate = useNavigate() const sdk = useSDK() const sync = useSync() const settings = useSettings() const dialog = useDialog() const language = useLanguage() + const { params, sessionKey } = useSessionKey() const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id)) - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionID = createMemo(() => params.id) const sessionMessages = createMemo(() => { const id = sessionID() diff --git a/packages/app/src/pages/session/session-layout.ts b/packages/app/src/pages/session/session-layout.ts new file mode 100644 index 000000000..113411150 --- /dev/null +++ b/packages/app/src/pages/session/session-layout.ts @@ -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)), + } +} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 590f5b6d9..2c499d9f4 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -1,7 +1,6 @@ import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { createMediaQuery } from "@solid-primitives/media" -import { useParams } from "@solidjs/router" import { Tabs } from "@opencode-ai/ui/tabs" import { IconButton } from "@opencode-ai/ui/icon-button" 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 { StickyAddButton } from "@/pages/session/review-tab" import { setSessionHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" export function SessionSidePanel(props: { reviewPanel: () => JSX.Element @@ -34,18 +34,15 @@ export function SessionSidePanel(props: { reviewSnap: boolean size: Sizing }) { - const params = useParams() const layout = useLayout() const sync = useSync() const file = useFile() const language = useLanguage() const command = useCommand() const dialog = useDialog() + const { params, sessionKey, tabs, view } = useSessionLayout() 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 fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 0c6db0b24..c49518656 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,6 +1,5 @@ import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" -import { useParams } from "@solidjs/router" import { Tabs } from "@opencode-ai/ui/tabs" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" 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 { createSizing, focusTerminalById } from "@/pages/session/helpers" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" export function TerminalPanel() { - const params = useParams() const layout = useLayout() const terminal = useTerminal() const language = useLanguage() const command = useCommand() - - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const view = createMemo(() => layout.view(sessionKey)) + const { params, view } = useSessionLayout() const opened = createMemo(() => view().terminal.opened()) const size = createSizing() diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index ea3b5ec57..6799504ca 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -1,7 +1,8 @@ import { createMemo } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" +import { useNavigate } from "@solidjs/router" import { useCommand, type CommandOption } from "@/context/command" 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 { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" @@ -19,6 +20,7 @@ import { showToast } from "@opencode-ai/ui/toast" import { findLast } from "@opencode-ai/util/array" import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" +import { useSessionLayout } from "@/pages/session/session-layout" export type SessionCommandContext = { navigateMessageByOffset: (offset: number) => void @@ -45,12 +47,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sync = useSync() const terminal = useTerminal() const layout = useLayout() - const params = useParams() 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 idle = { type: "idle" as const } @@ -71,11 +70,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const selectionPreview = (path: string, selection: FileSelection) => { const content = file.get(path)?.content?.content if (!content) return undefined - const start = Math.max(1, Math.min(selection.startLine, 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") + return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) } const addSelectionToContext = (path: string, selection: FileSelection) => {