Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FileProvider>
<PromptProvider>
<CommentsProvider>{props.children}</CommentsProvider>
</PromptProvider>
</FileProvider>
<TerminalProvider>
<FileProvider>
<PromptProvider>
<CommentsProvider>{props.children}</CommentsProvider>
</PromptProvider>
</FileProvider>
</TerminalProvider>
)
}

Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/context/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -378,6 +379,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
const params = useParams()
const cache = new Map<string, TerminalCacheEntry>()
const scope = server.scope()
const dir = createMemo(() => params.dir ?? base64Encode(sdk().directory))

caches.add(cache)
onCleanup(() => caches.delete(cache))
Expand Down Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions packages/app/src/pages/new-session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<div class="relative size-full overflow-hidden flex flex-col">
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
Expand All @@ -73,6 +107,7 @@ export default function NewSessionPage() {
</div>
</div>
</div>
<TerminalPanel />
</div>
)
}
13 changes: 9 additions & 4 deletions packages/app/src/pages/session/session-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
10 changes: 5 additions & 5 deletions packages/app/src/pages/session/terminal-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()

Expand All @@ -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()) ?? []
})

Expand Down
Loading