From 5159bdec3f85c304e551859e566cdd2211e90e4d Mon Sep 17 00:00:00 2001
From: vlgalib <72434411+vlgalib@users.noreply.github.com>
Date: Wed, 1 Jul 2026 17:07:02 +0100
Subject: [PATCH] feat(web): add grid mode for multi-session workspace
Adds a grid-based workspace layout to the web app, allowing users
to arrange multiple sessions side-by-side (1/2/3/4/6/8 layouts).
Includes:
- SessionGrid component composing multiple SessionView instances
- GridToggle sidebar control for switching layout modes
- CellSessionPicker for assigning sessions to grid cells
- gridMode layout context for per-directory persistence
Co-authored-by: hiai-bobcode contributors
---
packages/app/src/app.tsx | 14 +-
.../session/cell-session-picker.tsx | 113 +++++++++++++
.../src/components/session/grid-toggle.tsx | 67 ++++++++
packages/app/src/components/session/index.ts | 3 +
.../src/components/session/session-grid.tsx | 157 ++++++++++++++++++
.../src/components/session/session-header.tsx | 12 +-
.../components/session/session-providers.tsx | 24 +++
packages/app/src/context/layout.tsx | 61 +++++++
packages/app/src/pages/session.tsx | 78 ++++-----
9 files changed, 482 insertions(+), 47 deletions(-)
create mode 100644 packages/app/src/components/session/cell-session-picker.tsx
create mode 100644 packages/app/src/components/session/grid-toggle.tsx
create mode 100644 packages/app/src/components/session/session-grid.tsx
create mode 100644 packages/app/src/components/session/session-providers.tsx
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index f53cd7c61..d1cabc70a 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -8,7 +8,7 @@ import { Font } from "@mimo-ai/ui/font"
import { Splash } from "@mimo-ai/ui/logo"
import { ThemeProvider } from "@mimo-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta"
-import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
+import { type BaseRouterProps, Navigate, Route, Router, useParams } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect"
import {
@@ -42,6 +42,7 @@ import { ServerConnection, ServerProvider, serverName, useServer } from "@/conte
import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout"
+import { SessionGrid } from "@/components/session"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health"
@@ -55,11 +56,12 @@ if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname))
void loadSession()
}
-const SessionRoute = () => (
-
-
-
-)
+const SessionRoute = () => {
+ const params = useParams<{ dir?: string; id?: string }>()
+ // Layout store keys are base64 dir strings (see layout.sidebar.workspaces usage).
+ // Pass params.dir verbatim so per-directory layout state lines up.
+ return
+}
const SessionIndexRoute = () =>
diff --git a/packages/app/src/components/session/cell-session-picker.tsx b/packages/app/src/components/session/cell-session-picker.tsx
new file mode 100644
index 000000000..c7b148ec6
--- /dev/null
+++ b/packages/app/src/components/session/cell-session-picker.tsx
@@ -0,0 +1,113 @@
+import { createMemo, createSignal, For, Show } from "solid-js"
+import { DropdownMenu } from "@mimo-ai/ui/dropdown-menu"
+import { showToast } from "@mimo-ai/ui/toast"
+import { useGlobalSync } from "@/context/global-sync"
+import { useLayout } from "@/context/layout"
+import { sortedRootSessions } from "@/pages/layout/helpers"
+import { sessionTitle } from "@/utils/session-title"
+import { useSync } from "@/context/sync"
+import { useLanguage } from "@/context/language"
+import { useSDK } from "@/context/sdk"
+
+/**
+ * Empty grid cell — click to open a session in the slot. Lists the
+ * directory's existing sessions (excluding the ones already shown) and adds
+ * the chosen one via `layout.grid.addCell`. The "New session" entry uses the
+ * standard SDK client to create a fresh session in the same directory.
+ *
+ * Scope: same-directory only. Cross-project session listing and per-cell
+ * workspace selection require a future `experimental.session.list` /
+ * `experimental.workspace.list` server endpoint plus a workspace-client pool;
+ * those are intentionally omitted here.
+ */
+export function CellSessionPicker(props: { dir: string; primaryId?: string }) {
+ const layout = useLayout()
+ const globalSync = useGlobalSync()
+ const sdk = useSDK()
+ const sync = useSync()
+ const language = useLanguage()
+ const [store] = globalSync.child(props.dir, { bootstrap: false })
+ const [open, setOpen] = createSignal(false)
+ const [busy, setBusy] = createSignal(false)
+
+ const taken = createMemo(() => {
+ const set = new Set(layout.grid.cellsByID(props.dir)())
+ if (props.primaryId) set.add(props.primaryId)
+ return set
+ })
+
+ const sessions = createMemo(() => {
+ return sortedRootSessions(store, Date.now()).filter((s) => !taken().has(s.id))
+ })
+
+ const createSession = async () => {
+ setBusy(true)
+ try {
+ const result = await sdk.client.session.create().catch(() => undefined)
+ const sessionID = result?.data?.id
+ const directory = result?.data?.directory
+ if (!sessionID) {
+ showToast({
+ variant: "error",
+ title: language.t("common.requestFailed"),
+ description: "Failed to create session",
+ })
+ return
+ }
+ layout.grid.addCell(props.dir, sessionID, {
+ directory,
+ label: "New Session",
+ })
+ // Make sure the new session's events flow into the sync store.
+ void sync.session.sync(sessionID).catch(() => undefined)
+ setOpen(false)
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ return (
+
+
+
+ +
+ Open session
+
+
+
+
+ New
+ void createSession()}>
+ {busy() ? "Creating…" : "New session"}
+
+
+ 0}>
+
+ Existing sessions
+
+ {(s) => (
+ {
+ layout.grid.addCell(props.dir, s.id, { directory: props.dir })
+ setOpen(false)
+ }}
+ >
+ {sessionTitle(s.title)}
+
+ )}
+
+
+
+
+
+
+ No other sessions
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/app/src/components/session/grid-toggle.tsx b/packages/app/src/components/session/grid-toggle.tsx
new file mode 100644
index 000000000..04b6b4ac9
--- /dev/null
+++ b/packages/app/src/components/session/grid-toggle.tsx
@@ -0,0 +1,67 @@
+import { Button } from "@mimo-ai/ui/button"
+import { DropdownMenu } from "@mimo-ai/ui/dropdown-menu"
+import { Icon } from "@mimo-ai/ui/icon"
+import { IconButton } from "@mimo-ai/ui/icon-button"
+import { Tooltip } from "@mimo-ai/ui/tooltip"
+import { Show, createSignal } from "solid-js"
+import { useLayout } from "@/context/layout"
+
+const GRID_MODES = [1, 2, 3, 4, 6, 8] as const
+
+export function GridToggle(props: { dir: string }) {
+ const layout = useLayout()
+ const [open, setOpen] = createSignal(false)
+ const currentMode = () => layout.grid.mode(props.dir)()
+
+ return (
+
+
+
+
+
+
+
+ Grid
+ {GRID_MODES.map((mode) => (
+ {
+ layout.grid.setMode(props.dir, mode)
+ setOpen(false)
+ }}
+ >
+
+
+
+
+
+ {mode}
+
+ ))}
+
+
+ {
+ const active = layout.grid.active(props.dir)()
+ const cells = layout.grid.cells(props.dir)()
+ const isExtra = active && cells.some((c) => c.sessionID === active)
+ if (isExtra) {
+ layout.grid.removeCell(props.dir, active)
+ } else {
+ layout.grid.setMode(props.dir, 1)
+ }
+ setOpen(false)
+ }}
+ >
+ Close session
+
+
+
+
+ )
+}
diff --git a/packages/app/src/components/session/index.ts b/packages/app/src/components/session/index.ts
index 20124b6fd..f6a25aea2 100644
--- a/packages/app/src/components/session/index.ts
+++ b/packages/app/src/components/session/index.ts
@@ -3,3 +3,6 @@ export { SessionContextTab } from "./session-context-tab"
export { SortableTab, FileVisual } from "./session-sortable-tab"
export { SortableTerminalTab } from "./session-sortable-terminal-tab"
export { NewSessionView } from "./session-new-view"
+export { SessionGrid } from "./session-grid"
+export { GridToggle } from "./grid-toggle"
+export { CellSessionPicker } from "./cell-session-picker"
diff --git a/packages/app/src/components/session/session-grid.tsx b/packages/app/src/components/session/session-grid.tsx
new file mode 100644
index 000000000..650005338
--- /dev/null
+++ b/packages/app/src/components/session/session-grid.tsx
@@ -0,0 +1,157 @@
+import { createMemo, ErrorBoundary, For, Show } from "solid-js"
+import { useLayout, type GridCell } from "@/context/layout"
+import Page from "@/pages/session"
+import { SessionProviders } from "@/components/session/session-providers"
+import { CellSessionPicker } from "@/components/session/cell-session-picker"
+
+const GRID_COLS: Record = {
+ 1: "1fr",
+ 2: "1fr 1fr",
+ 3: "1fr 1fr 1fr",
+ 4: "1fr 1fr",
+ 6: "1fr 1fr 1fr",
+ 8: "1fr 1fr 1fr 1fr",
+}
+
+const GRID_ROWS: Record = {
+ 1: "1fr",
+ 2: "1fr",
+ 3: "1fr",
+ 4: "1fr 1fr",
+ 6: "1fr 1fr",
+ 8: "1fr 1fr",
+}
+
+/**
+ * Each cell gets its own session context stack (Terminal/File/Prompt/Comments)
+ * so parallel sessions have independent state and useFile() etc. resolve.
+ * Only the ACTIVE cell renders "full" (header chrome); the rest render
+ * "cell" so the top controls aren't duplicated per cell.
+ */
+function Cell(props: {
+ dir: string
+ cell: GridCell
+ active: boolean
+ onActivate?: () => void
+ onRemove?: () => void
+}) {
+ return (
+ props.onActivate?.()}
+ >
+
+ (
+
+
Session unreachable
+
+
+ {(err as Error).message}
+
+
+
+
+ )}
+ >
+
+
+
+
+ )
+}
+
+/**
+ * SessionGrid composes a CSS grid of session cells for the active directory.
+ * Cell 0 is always the primary (current route) session. Page handles the
+ * new-session state when primaryId is undefined, and its header carries the
+ * Grid toggle. Extra cells fill the remaining grid slots and are added via
+ * the empty-cell picker.
+ *
+ * Layout preferences (mode + cell assignments + active cell) are persisted
+ * per-directory via the layout store.
+ */
+export function SessionGrid(props: { dir: string; primaryId?: string }) {
+ const layout = useLayout()
+ const mode = createMemo(() => layout.grid.mode(props.dir)())
+ const cells = createMemo(() => layout.grid.cells(props.dir)())
+
+ // Cell 0 is always the primary (current route) session. Extra cells
+ // exclude the primary and stay under the slot budget.
+ const extraCells = createMemo(() => {
+ const seen = new Set()
+ return cells()
+ .filter((c) => c.sessionID !== props.primaryId)
+ .filter((c) => {
+ if (seen.has(c.sessionID)) return false
+ seen.add(c.sessionID)
+ return true
+ })
+ .slice(0, Math.max(0, mode() - 1))
+ })
+ const emptyCount = createMemo(() => Math.max(0, mode() - 1 - extraCells().length))
+
+ // Synthesize an ephemeral primary cell so the chrome still works when the
+ // route doesn't carry a session id (new-session state).
+ const primaryCell = createMemo(() => ({
+ id: props.primaryId ?? "primary",
+ sessionID: props.primaryId ?? "",
+ directory: props.dir,
+ mode: "full",
+ label: "",
+ }))
+
+ const activeId = createMemo(() => layout.grid.active(props.dir)() ?? primaryCell().id)
+
+ return (
+ 1}
+ fallback={
+ // mode 1: plain single session (full page + header + Grid toggle).
+
+
+
+ }
+ >
+
+ layout.grid.setActive(props.dir, primaryCell().id)}
+ onRemove={() => layout.grid.setMode(props.dir, 1)}
+ />
+
+ {(cell) => (
+ | layout.grid.setActive(props.dir, cell.id)}
+ onRemove={() => layout.grid.removeCell(props.dir, cell.sessionID)}
+ />
+ )}
+ |
+
+ {() => }
+
+ |
+
+ )
+}
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index a7f8834d6..fe94b6665 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -20,6 +20,7 @@ import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers"
+import { GridToggle } from "./grid-toggle"
import { useSessionLayout } from "@/pages/session/session-layout"
import { messageAgentColor } from "@/utils/agent"
import { decode64 } from "@/utils/base64"
@@ -129,7 +130,8 @@ const showRequestError = (language: ReturnType, err: unknown
})
}
-export function SessionHeader() {
+export function SessionHeader(props: { sessionID?: string } = {}) {
+ const effectiveSessionID = createMemo(() => props.sessionID ?? params.id)
const layout = useLayout()
const command = useCommand()
const server = useServer()
@@ -228,9 +230,10 @@ export function SessionHeader() {
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
)
const opening = createMemo(() => openRequest.app !== undefined)
- const tint = createMemo(() =>
- messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
- )
+ const tint = createMemo(() => {
+ const id = effectiveSessionID()
+ return messageAgentColor(id ? sync.data.message[id] : undefined, sync.data.agent)
+ })
const selectApp = (app: OpenApp) => {
if (!options().some((item) => item.id === app)) return
@@ -431,6 +434,7 @@ export function SessionHeader() {
+
grid import
+ * cycle.
+ */
+export function SessionProviders(props: ParentProps) {
+ return (
+
+
+
+ {props.children}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index 57cbb716e..05a573809 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -55,6 +55,16 @@ export type LocalProject = Partial & { worktree: string; expanded: bool
export type ReviewDiffStyle = "unified" | "split"
+export type GridMode = 1 | 2 | 3 | 4 | 6 | 8
+
+export type GridCell = {
+ id: string
+ sessionID: string
+ directory?: string
+ mode: "full" | "cell"
+ label: string
+}
+
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
touch(key)
seed(key)
@@ -255,6 +265,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
mobileSidebar: {
opened: false,
},
+ gridMode: {} as Record,
+ gridCells: {} as Record,
+ gridActive: {} as Record,
sessionTabs: {} as Record,
sessionView: {} as Record,
handoff: {
@@ -702,6 +715,54 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("mobileSidebar", "opened", (x) => !x)
},
},
+ grid: {
+ mode(directory: string) {
+ return createMemo(() => store.gridMode[directory] ?? 1)
+ },
+ setMode(directory: string, mode: GridMode) {
+ setStore("gridMode", directory, mode)
+ if (mode === 1) {
+ const current = store.gridCells[directory] ?? []
+ if (current.length > 0) setStore("gridCells", directory, [])
+ if (store.gridActive[directory]) setStore("gridActive", directory, undefined as unknown as string)
+ }
+ },
+ active(directory: string) {
+ return createMemo(() => store.gridActive[directory])
+ },
+ setActive(directory: string, id: string) {
+ setStore("gridActive", directory, id)
+ },
+ cells(directory: string) {
+ return createMemo(() => store.gridCells[directory] ?? [])
+ },
+ cellsByID(directory: string) {
+ return createMemo(() => (store.gridCells[directory] ?? []).map((c) => c.sessionID))
+ },
+ setCells(directory: string, cells: GridCell[]) {
+ setStore("gridCells", directory, cells)
+ },
+ addCell(directory: string, sessionID: string, opts?: { directory?: string; label?: string }) {
+ const current = store.gridCells[directory] ?? []
+ if (current.some((c) => c.sessionID === sessionID)) return
+ const cell: GridCell = {
+ id: `${sessionID}-${Date.now()}`,
+ sessionID,
+ directory: opts?.directory ?? directory,
+ mode: "cell",
+ label: opts?.label ?? "",
+ }
+ setStore("gridCells", directory, [...current, cell])
+ },
+ removeCell(directory: string, sessionID: string) {
+ const current = store.gridCells[directory] ?? []
+ setStore(
+ "gridCells",
+ directory,
+ current.filter((c) => c.sessionID !== sessionID),
+ )
+ },
+ },
pendingMessage: {
set(sessionKey: string, messageID: string) {
const at = Date.now()
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 4c7b204b7..cf68937ac 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -318,7 +318,9 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
}
}
-export default function Page() {
+export default function Page(props: { sessionID?: string; mode?: "full" | "cell" } = {}) {
+ const effectiveSessionID = createMemo(() => props.sessionID ?? params.id)
+ const isCellMode = () => props.mode === "cell"
const globalSync = useGlobalSync()
const layout = useLayout()
const local = useLocal()
@@ -338,7 +340,7 @@ export default function Page() {
createEffect(() => {
if (!prompt.ready()) return
untrack(() => {
- if (params.id) return
+ if (effectiveSessionID()) return
const text = searchParams.prompt
if (!text) return
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
@@ -364,7 +366,7 @@ export default function Page() {
createEffect(
on(
- () => params.id,
+ () => effectiveSessionID(),
(id, prev) => {
if (!id) return
if (prev) return
@@ -431,9 +433,9 @@ export default function Page() {
if (!view().reviewPanel.opened()) view().reviewPanel.open()
}
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+ const info = createMemo(() => { const id = effectiveSessionID(); return id ? sync.session.get(id) : undefined })
const isChildSession = createMemo(() => !!info()?.parentID)
- const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
+ const diffs = createMemo(() => { const id = effectiveSessionID(); return id ? list(sync.data.session_diff[id]) : [] })
const canReview = createMemo(() => !!sync.project)
const reviewTab = createMemo(() => isDesktop())
const tabState = createSessionTabs({
@@ -446,19 +448,19 @@ export default function Page() {
const activeTab = tabState.activeTab
const activeFileTab = tabState.activeFileTab
const revertMessageID = createMemo(() => info()?.revert?.messageID)
- const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
+ const messages = createMemo(() => { const id = effectiveSessionID(); return id ? (sync.data.message[id] ?? []) : [] })
const messagesReady = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return true
return sync.data.message[id] !== undefined
})
const historyMore = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return false
return sync.session.history.more(id)
})
const historyLoading = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return false
return sync.session.history.loading(id)
})
@@ -501,7 +503,7 @@ export default function Page() {
createEffect(
on(
- () => ({ dir: params.dir, id: params.id }),
+ () => ({ dir: params.dir, id: effectiveSessionID() }),
(next, prev) => {
if (!prev) return
if (next.dir === prev.dir && next.id === prev.id) return
@@ -756,7 +758,7 @@ export default function Page() {
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
const [sessionSync] = createResource(
- () => [sdk.directory, params.id] as const,
+ () => [sdk.directory, effectiveSessionID()] as const,
([directory, id]) => {
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
@@ -777,7 +779,7 @@ export default function Page() {
refreshFrame = undefined
refreshTimer = window.setTimeout(() => {
refreshTimer = undefined
- if (params.id !== id) return
+ if (effectiveSessionID() !== id) return
untrack(() => {
if (stale) void sync.session.sync(id, { force: true })
})
@@ -791,7 +793,7 @@ export default function Page() {
createEffect(
on(
() => {
- const id = params.id
+ const id = effectiveSessionID()
return [
sdk.directory,
id,
@@ -812,7 +814,7 @@ export default function Page() {
todoFrame = undefined
todoTimer = window.setTimeout(() => {
todoTimer = undefined
- if (sdk.directory !== dir || params.id !== id) return
+ if (sdk.directory !== dir || effectiveSessionID() !== id) return
untrack(() => {
void sync.session.todo(id, cached ? { force: true } : undefined)
})
@@ -990,7 +992,7 @@ export default function Page() {
createEffect(
on(
- () => sync.data.session_status[params.id ?? ""]?.type,
+ () => sync.data.session_status[effectiveSessionID() ?? ""]?.type,
(next, prev) => {
if (next !== "idle" || prev === undefined || prev === "idle") return
refreshVcs()
@@ -1257,7 +1259,7 @@ export default function Page() {
})
createEffect(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return
if (!wantsReview()) return
@@ -1277,7 +1279,7 @@ export default function Page() {
diffTimer = undefined
if (!wants) return
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return
if (!untrack(() => sync.data.session_diff[id] !== undefined)) return
@@ -1404,7 +1406,7 @@ export default function Page() {
)
const historyWindow = createSessionHistoryWindow({
- sessionID: () => params.id,
+ sessionID: () => effectiveSessionID(),
messagesReady,
loaded: () => messages().length,
visibleUserMessages,
@@ -1421,7 +1423,7 @@ export default function Page() {
fillFrame = requestAnimationFrame(() => {
fillFrame = undefined
- if (!params.id || !messagesReady()) return
+ if (!effectiveSessionID() || !messagesReady()) return
if (autoScroll.userScrolled() || historyLoading()) return
const el = scroller
@@ -1437,7 +1439,7 @@ export default function Page() {
on(
() =>
[
- params.id,
+ effectiveSessionID(),
messagesReady(),
historyWindow.turnStart(),
historyMore(),
@@ -1504,13 +1506,13 @@ export default function Page() {
}
const queuedFollowups = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return emptyFollowups
return followup.items[id] ?? emptyFollowups
})
const editingFollowup = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return
return followup.edit[id]
})
@@ -1545,14 +1547,14 @@ export default function Page() {
followupMutation.isPending && followupMutation.variables?.sessionID === sessionID
const sendingFollowup = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return
if (!followupBusy(id)) return
return followupMutation.variables?.id
})
const queueEnabled = createMemo(() => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return false
return settings.general.followup() === "queue" && busy(id) && !composer.blocked() && !isChildSession()
})
@@ -1595,7 +1597,7 @@ export default function Page() {
}
const editFollowup = (id: string) => {
- const sessionID = params.id
+ const sessionID = effectiveSessionID()
if (!sessionID) return
if (followupBusy(sessionID)) return
@@ -1612,7 +1614,7 @@ export default function Page() {
}
const clearFollowupEdit = () => {
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return
setFollowup("edit", id, undefined)
}
@@ -1646,7 +1648,7 @@ export default function Page() {
const restoreMutation = useMutation(() => ({
mutationFn: async (id: string) => {
- const sessionID = params.id
+ const sessionID = effectiveSessionID()
if (!sessionID) return
const next = userMessages().find((item) => item.id > id)
@@ -1694,7 +1696,7 @@ export default function Page() {
}
const restore = (id: string) => {
- if (!params.id || reverting()) return
+ if (!effectiveSessionID() || reverting()) return
return restoreMutation.mutateAsync(id)
}
@@ -1709,7 +1711,7 @@ export default function Page() {
const actions = { revert }
createEffect(() => {
- const sessionID = params.id
+ const sessionID = effectiveSessionID()
if (!sessionID) return
const item = queuedFollowups()[0]
@@ -1748,7 +1750,7 @@ export default function Page() {
const { clearMessageHash, scrollToMessage } = useSessionHashScroll({
sessionKey,
- sessionID: () => params.id,
+ sessionID: () => effectiveSessionID(),
messagesReady,
visibleUserMessages,
historyMore,
@@ -1769,7 +1771,7 @@ export default function Page() {
createEffect(
on(
- () => params.id,
+ () => effectiveSessionID(),
(id) => {
if (!id) requestAnimationFrame(() => inputRef?.focus())
},
@@ -1795,9 +1797,11 @@ export default function Page() {
return (
{sessionSync() ?? ""}
-
+
+
+
-
+
-
+
{
- const id = params.id
+ const id = effectiveSessionID()
if (!id) return
setFollowup("paused", id, true)
},
onSend: (id) => {
- void sendFollowup(params.id!, id, { manual: true })
+ void sendFollowup(effectiveSessionID()!, id, { manual: true })
},
onEdit: editFollowup,
onEditLoaded: clearFollowupEdit,