#18526 · @Brendonovich · opened Mar 21, 2026 at 1:51 PM UTC · last updated Mar 21, 2026 at 2:15 PM UTC

app: better session id handling

apprefactor
12
+57449117 files

Score breakdown

Impact

0.0

Clarity

0.0

Urgency

0.0

Ease Of Review

0.0

Guidelines

0.0

Readiness

0.0

Size

0.0

Trust

10.0

Traction

0.0

Summary

This large PR, titled "app: better session id handling," provides no description or linked issues. It significantly modifies 17 files related to session components and UI. The specific problem being solved or the nature of "better handling" is completely unclear.

Open in GitHub

Description

No description.

Linked Issues

None.

Comments

No comments.

Changed Files

packages/app/src/app.tsx

+61
@@ -8,10 +8,11 @@ import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { type BaseRouterProps, Navigate, Route, Router, useLocation } from "@solidjs/router"
import { type Duration, Effect } from "effect"
import {
type Component,
createEffect,
createMemo,
createResource,
createSignal,
@@ -114,6 +115,10 @@ function SessionProviders(props: ParentProps) {
}
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
const l = useLocation()
createEffect(() => {
console.log("pathname", l.pathname)
})
return (
<AppShellProviders>
<Suspense fallback={<Loading />}>

packages/app/src/components/prompt-input.tsx

+8957
@@ -1,61 +1,62 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Select } from "@opencode-ai/ui/select"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { type Component, createEffect, createMemo, createSignal, on, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useCommand } from "@/context/command"
import { useComments } from "@/context/comments"
import { type SelectedLineRange, selec

packages/app/src/components/session/session-context-tab.tsx

+1211
@@ -1,21 +1,22 @@
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { checksum } from "@opencode-ai/util/encode"
import { findLast } from "@opencode-ai/util/array"
import { same } from "@/utils/same"
import { Icon } from "@opencode-ai/ui/icon"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { File } from "@opencode-ai/ui/file"
import { Icon } from "@opencode-ai/ui/icon"
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 { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { findLast } from "@opencode-ai/util/array"
import { checksum } from "@opencode-ai/util/encode"
import type { JSX } from "solid-js"
import { createEffect, createMemo, For, on, onCleanup, Show } from "solid-js"
import { useLanguage } from "@/context/la

packages/app/src/pages/session.tsx

+7571
@@ -1,44 +1,44 @@
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Select } from "@opencode-ai/ui/select"
import { Tabs } from "@opencode-ai/ui/tabs"
import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode, checksum } from "@opencode-ai/util/encode"
import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useNavigate, useSearchParams } from "@solidjs/router"
import {
batch,
onCleanup,
Show,
Match,
Switch,
createMemo,
createEffect,
createComputed,
createEffect,
createMemo,
Match,
on,
onCleanup,
onMount,
Show,
Switch,
untrack,
} from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLocal } fr

packages/app/src/pages/session/composer/session-composer-region.tsx

+88
@@ -1,18 +1,19 @@
import { Show, createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
import type { FollowupDraft } from "@/components/prompt-input/submit"
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 type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"

packages/app/src/pages/session/composer/session-composer-state.ts

+711
@@ -1,14 +1,15 @@
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
import { showToast } from "@opencode-ai/ui/toast"
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
import { Optional } from "@/utils/optional"
import { useSessionLayout } from "../session-layout"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
export const todoState = (input: {
@@ -25,12 +26,12 @@ export const todoState = (input: {
const idle = { type: "idle" as const }
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {

packages/app/src/pages/session/composer/session-todo-dock.tsx

+54
@@ -6,10 +6,11 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
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, on, onCleanup } from "solid-js"
import { createEffect, createMemo, Index, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { composerEnabled, composerProbe } from "@/testing/session-composer"
import { useLanguage } from "@/context/language"
import { composerEnabled, composerProbe } from "@/testing/session-composer"
import { useSessionLayout } from "../session-layout"
const doneToken = "\u0000done\u0000"
const totalToken = "\u0000total\u0000"
@@ -40,7 +41,6 @@ function dot(status: Todo["status"]) {
}
export function SessionTodoDock(props: {
sessionID?: string
todos: Todo[]
collapseLabel: string
expandLabel: string
@@ -51,6 +51,7 @@ export function SessionTodoDock(props: {
collapsed: false,
height: 320,
})
const { params } = useSessionLayout()
const toggle = () => setStore("collapsed", (value) => !valu

packages/app/src/pages/session/message-timeline.tsx

+197218
@@ -1,32 +1,32 @@
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { useNavigate } from "@solidjs/router"
import { Popover as KobaltePopover } from "@kobalte/core/popover"
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { Spinner } from "@opencode-ai/ui/spinner"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { Spinner } from "

packages/app/src/pages/session/session-layout.ts

+11
@@ -3,7 +3,7 @@ import { createMemo } from "solid-js"
import { useLayout } from "@/context/layout"
export const useSessionKey = () => {
const params = useParams()
const params = useParams<{ dir: string; id: string & { __brand: "SessionID" } }>()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
return { params, sessionKey }
}

packages/app/src/pages/session/session-side-panel.tsx

+1717
@@ -1,30 +1,31 @@
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 { Tabs } from "@opencode-ai/ui/tabs"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { createMediaQuery } from "@solid-primitives/media"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { closestCenter, DragDropProvider, DragDropSensors, DragOverlay, SortableProvider } from "@thisbeyond/soli

packages/app/src/pages/session/use-session-commands.tsx

+3528
@@ -1,8 +1,15 @@
import { useNavigate } from "@solidjs/router"
import { useCommand, type CommandOption } from "@/context/command"
import type { UserMessage } from "@opencode-ai/sdk/v2"
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 { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/util/array"
import { useNavigate } from "@solidjs/router"
import { DialogFork } from "@/components/dialog-fork"
import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { type CommandOption, useCommand } from "@/context/command"
import { type FileSelection, type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local"
@@ -11,16 +18,10 @@ import { usePrompt } fr

packages/app/src/pages/session/use-session-hash-scroll.ts

+53
@@ -2,10 +2,11 @@ import type { UserMessage } from "@opencode-ai/sdk/v2"
import { useLocation, useNavigate } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { messageIdFromHash } from "./message-id-from-hash"
import { useSessionLayout } from "./session-layout"
export const useSessionHashScroll = (input: {
sessionKey: () => string
sessionID: () => string | undefined
messagesReady: () => boolean
visibleUserMessages: () => UserMessage[]
turnStart: () => number
@@ -28,6 +29,7 @@ export const useSessionHashScroll = (input: {
const location = useLocation()
const navigate = useNavigate()
const { params } = useSessionLayout()
const frames = new Set<number>()
const queue = (fn: () => void) => {
@@ -142,13 +144,13 @@ export const useSessionHashScroll = (input: {
createEffect(() => {
const hash = location.hash
if (!hash) clearing = false
if (!input.sessionID() || !input.messagesReady()) return
if (!params.id || !input.messagesReady()) return
cancel()
queue(() => applyHash("auto"))
})
createEffect(() => {
if (!input.sessionID() || !input.messagesReady()

packages/app/src/utils/optional.ts

+110
@@ -0,0 +1,11 @@
import { dual } from "effect/Function"
export namespace Optional {
export const map = dual<
<I, O>(f: (value: I) => O) => (opt: I | undefined) => O | undefined,
<I, O>(opt: I | undefined, f: (value: I) => O) => O | undefined
>(2, (opt, f) => {
if (opt === undefined) return undefined
return f(opt)
})
}

packages/function/src/api.ts

+22
@@ -1,9 +1,9 @@
import { Hono } from "hono"
import { DurableObject } from "cloudflare:workers"
import { randomUUID } from "node:crypto"
import { jwtVerify, createRemoteJWKSet } from "jose"
import { createAppAuth } from "@octokit/auth-app"
import { Octokit } from "@octokit/rest"
import { Hono } from "hono"
import { createRemoteJWKSet, jwtVerify } from "jose"
import { Resource } from "sst"
type Env = {

packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

+66
@@ -1,15 +1,15 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { createMemo, createResource, createSignal, onMount } from "solid-js"
import { Locale } from "@/util/locale"
import { useKeybind } from "../context/keybind"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename"
import { useKV } from "../context/kv"
import { useSDK } from "../context/sdk"
import { useTheme } from "../context/theme"
import { createDebouncedSignal } from "../util/signal"
import { DialogSessionRename } from "./dialog-session-rename"
import { Spinner } from "./spinner"
export function DialogSessionList() {

packages/opencode/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx

+77
@@ -1,17 +1,17 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { createMemo, createResource, createSignal, onMount } from "solid-js"
import { Locale } from "@/util/locale"
import { useKeybind } from "../../context/keybind"
import { useTheme } from "../../context/theme"
import { useSDK } from "../../context/sdk"
import { DialogSessionRename } from "../dialog-session-rename"
import { useKV } from "../../context/kv"
import { useSDK } from "../../context/sdk"
import { useTheme } from "../../context/theme"
import { useToast } from "../../ui/toast"
import { createDebouncedSignal } from "../../util/signal"
import { DialogSessionRename } from "../dialog-session-rename"
import { Spinner } from "../spinner"
import { useToast } from "../../ui/toast"
export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) {
co

packages/ui/src/components/message-part.tsx

+9146
@@ -1,60 +1,59 @@
import type {
AgentPart,
AssistantMessage,
FilePart,
Message as MessageType,
Part as PartType,
QuestionAnswer,
QuestionInfo,
ReasoningPart,
TextPart,
Todo,
ToolPart,
UserMessage,
} from "@opencode-ai/sdk/v2"
import { checksum } from "@opencode-ai/util/encode"
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
import { useLocation } from "@solidjs/router"
import { animate } from "motion"
import {
Component,
type Component,
createEffect,
createMemo,
createSignal,
For,
Index,
type JSX,
Match,
onCleanup,
onMount,
Show,
Switch,
onCleanup,
Index,
type JSX,
} from "solid-js"
import { createStore } from "solid-js/store"
import stripAnsi from "strip-ansi"
import { Dynamic } from "solid-js/web"
import {
AgentPart,
AssistantMessage,
FilePart,
Message as MessageType,
Part as PartType,
ReasoningPart,
TextPart,
ToolPart,
UserMessage,
Todo,
QuestionAnswer,
QuestionInfo,
} from "@opencode-ai/sdk/v2"
import stripAnsi from "strip-ansi"
import { useData } from "../context"
import { useFileComponent } from "../context