From 5bf021fa07403ec9b0abcecac42ce27206bbea95 Mon Sep 17 00:00:00 2001 From: anduimagui <78056086+anduimagui@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:39:17 +0100 Subject: [PATCH] fix(app): enable terminal on draft sessions --- packages/app/src/app.tsx | 14 ++++--- packages/app/src/context/terminal.tsx | 6 ++- packages/app/src/pages/new-session.tsx | 39 ++++++++++++++++++- .../app/src/pages/session/session-layout.ts | 13 +++++-- .../app/src/pages/session/terminal-panel.tsx | 10 ++--- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 75f0c6b44465..d951180124a3 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -248,15 +248,17 @@ function SessionProviders(props: ParentProps) { ) } -// The draft page only renders the prompt composer, so it drops TerminalProvider. +// The draft page renders the prompt composer plus workspace terminal support. // FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. function DraftProviders(props: ParentProps) { return ( - - - {props.children} - - + + + + {props.children} + + + ) } diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index d1aa61c4ce57..a51c31dc1956 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -8,6 +8,7 @@ import { useServer } from "./server" import { defaultTitle, titleNumber } from "./terminal-title" import { Persist, persisted, removePersisted } from "@/utils/persist" import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope" +import { base64Encode } from "@opencode-ai/core/util/encode" export type LocalPTY = { id: string @@ -378,6 +379,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont const params = useParams() const cache = new Map() const scope = server.scope() + const dir = createMemo(() => params.dir ?? base64Encode(sdk().directory)) caches.add(cache) onCleanup(() => caches.delete(cache)) @@ -421,11 +423,11 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont return entry.value } - const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope)) + const workspace = createMemo(() => loadWorkspace(dir(), params.id, scope)) createEffect( on( - () => ({ dir: params.dir, id: params.id, scope }), + () => ({ dir: dir(), id: params.id, scope }), (next, prev) => { if (!prev?.dir) return if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx index 39ec2f251b3d..24f366fa65e8 100644 --- a/packages/app/src/pages/new-session.tsx +++ b/packages/app/src/pages/new-session.tsx @@ -2,22 +2,32 @@ import { createEffect, createMemo, onMount, untrack } from "solid-js" import { createStore } from "solid-js/store" import { useSearchParams } from "@solidjs/router" import { NewSessionDesignView } from "@/components/session" +import { useCommand } from "@/context/command" import { useComments } from "@/context/comments" +import { useLanguage } from "@/context/language" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" +import { useSessionLayout } from "@/pages/session/session-layout" +import { TerminalPanel } from "@/pages/session/terminal-panel" +import { useTerminal } from "@/context/terminal" /** * The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt - * composer for a brand-new session — no terminal, review pane, file tree, or message - * timeline. Submitting promotes the draft into a real session (see prompt-input/submit). + * composer for a brand-new session plus the workspace terminal. It does not render + * the review pane, file tree, or message timeline. Submitting promotes the draft + * into a real session (see prompt-input/submit). */ export default function NewSessionPage() { + const command = useCommand() + const language = useLanguage() const prompt = usePrompt() const sdk = useSDK() const sync = useSync() const comments = useComments() + const terminal = useTerminal() + const { view } = useSessionLayout() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() let inputRef: HTMLDivElement | undefined @@ -49,6 +59,30 @@ export default function NewSessionPage() { requestAnimationFrame(() => inputRef?.focus()) }) + const openTerminal = () => { + if (terminal.all().length > 0) terminal.new() + view().terminal.open() + } + + command.register("new-session", () => [ + { + id: "terminal.toggle", + title: language.t("command.terminal.toggle"), + category: language.t("command.category.view"), + keybind: "ctrl+`", + slash: "terminal", + onSelect: () => view().terminal.toggle(), + }, + { + id: "terminal.new", + title: language.t("command.terminal.new"), + description: language.t("command.terminal.new.description"), + category: language.t("command.category.terminal"), + keybind: "ctrl+alt+t", + onSelect: openTerminal, + }, + ]) + return ( @@ -73,6 +107,7 @@ export default function NewSessionPage() { + ) } diff --git a/packages/app/src/pages/session/session-layout.ts b/packages/app/src/pages/session/session-layout.ts index 82be2bf5cdb1..45915d76b6c0 100644 --- a/packages/app/src/pages/session/session-layout.ts +++ b/packages/app/src/pages/session/session-layout.ts @@ -2,22 +2,27 @@ import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" import { useLayout } from "@/context/layout" import { useServer } from "@/context/server" +import { useSDK } from "@/context/sdk" +import { base64Encode } from "@opencode-ai/core/util/encode" import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope" export const useSessionKey = () => { const params = useParams() const server = useServer() + const sdk = useSDK() const scope = createMemo(() => server.scope()) - const workspaceKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(params.dir))) - const sessionKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(params.dir, params.id))) - return { params, sessionKey, workspaceKey } + const dir = createMemo(() => params.dir ?? base64Encode(sdk().directory)) + const workspaceKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(dir()))) + const sessionKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(dir(), params.id))) + return { params, dir, sessionKey, workspaceKey } } export const useSessionLayout = () => { const layout = useLayout() - const { params, sessionKey, workspaceKey } = useSessionKey() + const { params, dir, sessionKey, workspaceKey } = useSessionKey() return { params, + dir, sessionKey, workspaceKey, tabs: createMemo(() => layout.tabs(sessionKey)), diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 6943a9a3ec9c..37f2848c62c0 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -28,7 +28,7 @@ export function TerminalPanel() { const language = useLanguage() const command = useCommand() const settings = useSettings() - const { params, workspaceKey, view } = useSessionLayout() + const { dir, workspaceKey, view } = useSessionLayout() const opened = createMemo(() => view().terminal.opened()) const size = createSizing() @@ -122,8 +122,8 @@ export function TerminalPanel() { }) createEffect(() => { - const dir = params.dir - if (!dir) return + const routeDir = dir() + if (!routeDir) return if (!terminal.ready()) return language.locale() @@ -140,8 +140,8 @@ export function TerminalPanel() { }) const handoff = createMemo(() => { - const dir = params.dir - if (!dir) return [] + const routeDir = dir() + if (!routeDir) return [] return getTerminalHandoff(workspaceKey()) ?? [] })