From 3aaff6ffe2a20466ab2938888b32b520c433d583 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 27 Jul 2026 16:59:05 +0800 Subject: [PATCH] refactor(app): establish v2 session controller --- packages/app/src/pages/session.tsx | 81 ++++++-------- .../pages/session/session-controller.test.ts | 71 ++++++++++++ .../src/pages/session/session-controller.ts | 101 ++++++++++++++++++ .../app/src/pages/session/session-domain.ts | 19 ++++ .../src/pages/session/timeline/controller.tsx | 40 ++++--- .../session/timeline/message-timeline.tsx | 5 +- .../app/src/pages/session/timeline/model.ts | 56 ++++------ .../pages/session/use-session-commands.tsx | 66 +++++------- 8 files changed, 285 insertions(+), 154 deletions(-) create mode 100644 packages/app/src/pages/session/session-controller.test.ts create mode 100644 packages/app/src/pages/session/session-controller.ts create mode 100644 packages/app/src/pages/session/session-domain.ts diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c6e3a5fcbb..3f68ddec90 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -57,7 +57,7 @@ import { ServerConnection, serverName, useServer } from "@/context/server" import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" -import { TerminalProvider, useTerminal } from "@/context/terminal" +import { TerminalProvider } from "@/context/terminal" import { PromptInput } from "@/components/prompt-input" import { PromptInputV2Composer, usePromptInputV2Controller } from "@/components/prompt-input-v2" import { useSettingsCommand } from "@/components/settings-dialog" @@ -70,11 +70,11 @@ import { createSessionComposerRegionController, SessionComposerRegion, } from "@/pages/session/composer" -import { createOpenReviewFile, createSessionTabs, createSizing, shouldShowFileTree } from "@/pages/session/helpers" +import { createOpenReviewFile, createSizing, shouldShowFileTree } from "@/pages/session/helpers" import { MessageTimeline } from "@/pages/session/timeline/message-timeline" import { createTimelineModel } from "@/pages/session/timeline/model" import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" -import { useSessionLayout } from "@/pages/session/session-layout" +import { createSessionController } from "@/pages/session/session-controller" import { restorePromptModel, syncPromptModel, syncSessionModel } from "@/pages/session/session-model-helpers" import { clampSessionPanelWidth, @@ -101,7 +101,6 @@ import { extractPromptFromParts } from "@/utils/prompt" import { formatServerError, isLocalSessionNotFoundError, isSessionNotFoundError } from "@/utils/server-errors" import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs" -import { createSessionOwnership } from "./session/session-ownership" import { createSessionLineage } from "./session/session-lineage" type FollowupItem = FollowupDraft & { id: string } @@ -366,15 +365,28 @@ export default function Page() { const prompt = usePrompt() const comments = useComments() const command = useCommand() - const terminal = useTerminal() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() const location = useLocation() const navigate = useNavigate() - const { params, sessionKey, workspaceKey, tabs, view } = useSessionLayout() + const isDesktop = createMediaQuery("(min-width: 768px)") + const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns()) + const canReview = createMemo(() => !!sync().project) + const session = createSessionController({ + review: isDesktop, + hasReview: canReview, + fileBrowser: (sessionID) => newSessionDesign() && isDesktop() && !!sessionID, + }) + const params = session.identity.params + const sessionKey = session.identity.sessionKey + const workspaceKey = session.identity.workspaceKey + const tabs = session.layout.tabs + const view = session.layout.view const reviewMode = () => view().review.mode() ?? "git" const reviewFile = () => view().review.file() - const sessionOwnership = createSessionOwnership(sessionKey) - const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns()) + const sessionOwnership = session.ownership + const info = session.data.info + const isChildSession = session.data.isChild + const revertMessageID = session.data.revertMessageID createEffect(() => { if (!prompt.ready()) return @@ -433,8 +445,8 @@ export default function Page() { const current = tabs().tabs() if (current.all.length > 0 || current.active) return - const all = normalizeTabs(from.all) - const active = from.active ? normalizeTab(from.active) : undefined + const all = session.tabs.normalizeAll(from.all) + const active = from.active ? session.tabs.normalize(from.active) : undefined tabs().setAll(all) tabs().setActive(active && all.includes(active) ? active : all[0]) @@ -445,7 +457,6 @@ export default function Page() { ), ) - const isDesktop = createMediaQuery("(min-width: 768px)") const size = createSizing() const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const desktopV2ReviewOpen = createMemo(() => newSessionDesign() && desktopReviewOpen() && !!params.id) @@ -510,46 +521,16 @@ export default function Page() { }), ) - function normalizeTab(tab: string) { - if (!tab.startsWith("file://")) return tab - return file.tab(tab) - } - - function normalizeTabs(list: string[]) { - const seen = new Set() - const next: string[] = [] - for (const item of list) { - const value = normalizeTab(item) - if (seen.has(value)) continue - seen.add(value) - next.push(value) - } - return next - } - const openReviewPanel = () => { if (!view().reviewPanel.opened()) view().reviewPanel.open() } - const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) - const isChildSession = createMemo(() => !!info()?.parentID) - const canReview = createMemo(() => !!sync().project) - const reviewTab = createMemo(() => isDesktop()) - const tabState = createSessionTabs({ - tabs, - pathFromTab: file.pathFromTab, - normalizeTab, - review: reviewTab, - hasReview: canReview, - }) - const activeTab = tabState.activeTab - const activeFileTab = tabState.activeFileTab - const revertMessageID = createMemo(() => info()?.revert?.messageID) - const timeline = createTimelineModel({ sessionID: () => params.id, revertMessageID }) + const activeTab = session.tabs.activeTab + const activeFileTab = session.tabs.activeFileTab + const timeline = createTimelineModel({ session }) const historyLoading = timeline.history.loading const historyMore = timeline.history.more const lastUserMessage = timeline.lastUserMessage - const messages = timeline.messages const messagesReady = timeline.ready const sessionSync = timeline.resource const userMessages = timeline.userMessages @@ -1138,11 +1119,10 @@ export default function Page() { useComposerCommands() useSessionCommands({ + session, navigateMessageByOffset, setActiveMessage, focusInput, - review: reviewTab, - fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id, }) command.register("session-palette", () => [ { @@ -1692,8 +1672,6 @@ export default function Page() { }) } - const merge = (next: NonNullable>, target = sync()) => target.session.remember(next) - const roll = (sessionID: string, next: NonNullable>["revert"], target = sync()) => { const session = target.session.get(sessionID) if (!session) return @@ -1754,7 +1732,7 @@ export default function Page() { const queueEnabled = createMemo(() => { const id = params.id if (!id) return false - return settings.general.followup() === "queue" && busy(id) && !composer.blocked() && !isChildSession() + return settings.general.followup() === "queue" && session.data.working() && !composer.blocked() && !isChildSession() }) const followupText = (item: FollowupDraft) => { @@ -1932,7 +1910,7 @@ export default function Page() { if (followup.paused[sessionID]) return if (isChildSession()) return if (composer.blocked()) return - if (busy(sessionID)) return + if (session.data.working()) return void sendFollowup(sessionID, item.id) }) @@ -2080,6 +2058,7 @@ export default function Page() { {(_id) => ( { - const id = info()?.parentID + const id = session.data.parentID() if (!id) return navigate( params.serverKey diff --git a/packages/app/src/pages/session/session-controller.test.ts b/packages/app/src/pages/session/session-controller.test.ts new file mode 100644 index 0000000000..73ea8227fe --- /dev/null +++ b/packages/app/src/pages/session/session-controller.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test" +import type { AssistantMessage, Message, UserMessage } from "@opencode-ai/sdk/v2" +import { createRoot, createSignal } from "solid-js" +import { + normalizeSessionTab, + normalizeSessionTabs, + selectSessionUserMessages, + selectVisibleSessionUserMessages, +} from "./session-domain" +import { createSessionOwnership } from "./session-ownership" + +const user = (id: string): UserMessage => ({ + id, + sessionID: "session", + role: "user", + time: { created: 0 }, + agent: "build", + model: { providerID: "provider", modelID: "model" }, +}) + +const assistant: AssistantMessage = { + id: "msg_2", + sessionID: "session", + role: "assistant", + time: { created: 0 }, + parentID: "msg_1", + modelID: "model", + providerID: "provider", + mode: "build", + agent: "build", + path: { cwd: "/workspace", root: "/workspace" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, +} + +describe("session controller invariants", () => { + test("normalizes file tabs once while preserving non-file tabs and order", () => { + const normalize = (tab: string) => normalizeSessionTab(tab, (value) => value.toLowerCase()) + + expect(normalizeSessionTabs(["review", "file://SRC/A.TS", "file://src/a.ts", "context"], normalize)).toEqual([ + "review", + "file://src/a.ts", + "context", + ]) + }) + + test("selects user history strictly before the revert boundary", () => { + const messages: Message[] = [user("msg_1"), assistant, user("msg_3"), user("msg_5")] + const users = selectSessionUserMessages(messages) + + expect(users.map((message) => message.id)).toEqual(["msg_1", "msg_3", "msg_5"]) + expect(selectVisibleSessionUserMessages(users, "msg_3").map((message) => message.id)).toEqual(["msg_1"]) + expect(selectVisibleSessionUserMessages(users)).toBe(users) + }) + + test("rejects work captured by a previous session", () => { + createRoot((dispose) => { + const [key, setKey] = createSignal("session-a") + const ownership = createSessionOwnership(key) + const captured = ownership.capture() + let ran = false + + setKey("session-b") + + expect(captured.current()).toBe(false) + expect(captured.run(() => (ran = true))).toBeUndefined() + expect(ran).toBe(false) + dispose() + }) + }) +}) diff --git a/packages/app/src/pages/session/session-controller.ts b/packages/app/src/pages/session/session-controller.ts new file mode 100644 index 0000000000..f9c1a7a3b9 --- /dev/null +++ b/packages/app/src/pages/session/session-controller.ts @@ -0,0 +1,101 @@ +import type { Message, UserMessage } from "@opencode-ai/sdk/v2" +import { createMemo, type Accessor } from "solid-js" +import { useFile } from "@/context/file" +import { useSync } from "@/context/sync" +import { same } from "@/utils/same" +import { createSessionTabs } from "./helpers" +import { + normalizeSessionTab, + normalizeSessionTabs, + selectSessionUserMessages, + selectVisibleSessionUserMessages, +} from "./session-domain" +import { useSessionLayout } from "./session-layout" +import { createSessionOwnership } from "./session-ownership" + +const emptyMessages: Message[] = [] +const emptyUserMessages: UserMessage[] = [] +const idle = { type: "idle" as const } + +export function createSessionController(input: { + review?: Accessor + hasReview?: Accessor + fileBrowser?: (sessionID: string | undefined) => boolean +}) { + const file = useFile() + const sync = useSync() + const layout = useSessionLayout() + const sessionID = createMemo(() => layout.params.id) + const info = createMemo(() => { + const id = sessionID() + return id ? sync().session.get(id) : undefined + }) + const parentID = createMemo(() => info()?.parentID) + const parent = createMemo(() => { + const id = parentID() + return id ? sync().session.get(id) : undefined + }) + const status = createMemo(() => { + const id = sessionID() + return id ? (sync().data.session_status[id] ?? idle) : idle + }) + const messages = createMemo(() => { + const id = sessionID() + return id ? (sync().data.message[id] ?? emptyMessages) : emptyMessages + }) + const userMessages = createMemo(() => selectSessionUserMessages(messages()), emptyUserMessages, { equals: same }) + const revertMessageID = createMemo(() => info()?.revert?.messageID) + const visibleUserMessages = createMemo( + () => selectVisibleSessionUserMessages(userMessages(), revertMessageID()), + emptyUserMessages, + { equals: same }, + ) + const normalizeTab = (tab: string) => normalizeSessionTab(tab, file.tab) + const tabs = createSessionTabs({ + tabs: layout.tabs, + pathFromTab: file.pathFromTab, + normalizeTab, + review: input.review, + hasReview: input.hasReview, + fileBrowser: input.fileBrowser ? () => input.fileBrowser?.(sessionID()) ?? false : undefined, + }) + + return { + identity: { + params: layout.params, + sessionID, + sessionKey: layout.sessionKey, + workspaceKey: layout.workspaceKey, + }, + data: { + info, + parent, + parentID, + isChild: createMemo(() => !!parentID()), + status, + working: createMemo(() => { + const id = sessionID() + return id ? sync().data.session_working(id) : false + }), + revertMessageID, + }, + history: { + messages, + userMessages, + visibleUserMessages, + lastUserMessage: createMemo(() => visibleUserMessages().at(-1)), + }, + layout: { + tabs: layout.tabs, + view: layout.view, + }, + ownership: createSessionOwnership(layout.sessionKey), + tabs: { + ...tabs, + normalize: normalizeTab, + normalizeAll: (values: string[]) => normalizeSessionTabs(values, normalizeTab), + }, + } +} + +export type SessionController = ReturnType diff --git a/packages/app/src/pages/session/session-domain.ts b/packages/app/src/pages/session/session-domain.ts new file mode 100644 index 0000000000..f5b56770ca --- /dev/null +++ b/packages/app/src/pages/session/session-domain.ts @@ -0,0 +1,19 @@ +import type { Message, UserMessage } from "@opencode-ai/sdk/v2" + +export function normalizeSessionTab(tab: string, normalizeFileTab: (tab: string) => string) { + if (!tab.startsWith("file://")) return tab + return normalizeFileTab(tab) +} + +export function normalizeSessionTabs(tabs: string[], normalize: (tab: string) => string) { + return [...new Set(tabs.map(normalize))] +} + +export function selectSessionUserMessages(messages: Message[]) { + return messages.filter((message): message is UserMessage => message.role === "user") +} + +export function selectVisibleSessionUserMessages(messages: UserMessage[], revertMessageID?: string) { + if (!revertMessageID) return messages + return messages.filter((message) => message.id < revertMessageID) +} diff --git a/packages/app/src/pages/session/timeline/controller.tsx b/packages/app/src/pages/session/timeline/controller.tsx index 775be85243..2d227d720e 100644 --- a/packages/app/src/pages/session/timeline/controller.tsx +++ b/packages/app/src/pages/session/timeline/controller.tsx @@ -15,7 +15,7 @@ import { useSettings } from "@/context/settings" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" -import { useSessionKey } from "@/pages/session/session-layout" +import type { SessionController } from "@/pages/session/session-controller" import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" import { sessionTitle } from "@/utils/session-title" import { showToast } from "@/utils/toast" @@ -24,8 +24,6 @@ import { createTimelineProjection } from "./projection" const emptyMessages: Message[] = [] const emptyParts: Part[] = [] -const idle = { type: "idle" as const } - const taskDescription = (part: Part, sessionID: string): string | undefined => { if (part.type !== "tool" || part.tool !== "task") return undefined const metadata = "metadata" in part.state ? part.state.metadata : undefined @@ -35,7 +33,16 @@ const taskDescription = (part: Part, sessionID: string): string | undefined => { return undefined } -export function createTimelineController(input: { userMessages: Accessor }) { +export type TimelineSessionSource = { + identity: Pick + data: Pick + history: Pick +} + +export function createTimelineController(input: { + session: TimelineSessionSource + userMessages: Accessor +}) { const navigate = useNavigate() const serverSDK = useServerSDK() const sdk = useSDK() @@ -45,14 +52,11 @@ export function createTimelineController(input: { userMessages: Accessor params.id) - const status = createMemo(() => { - const id = sessionID() - if (!id) return idle - return sync().data.session_status[id] ?? idle - }) - const messages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) + const params = input.session.identity.params + const sessionKey = input.session.identity.sessionKey + const sessionID = input.session.identity.sessionID + const status = input.session.data.status + const messages = input.session.history.messages const projectedMessages = createMemo(() => { const id = sessionID() if (!id) return [] @@ -61,19 +65,13 @@ export function createTimelineController(input: { userMessages: Accessor message.id < boundary) : projected }) - const info = createMemo(() => { - const id = sessionID() - return id ? sync().session.get(id) : undefined - }) + const info = input.session.data.info const titleValue = createMemo(() => info()?.title) const titleLabel = createMemo(() => sessionTitle(titleValue())) const shareUrl = createMemo(() => info()?.share?.url) const shareEnabled = createMemo(() => sync().data.config.share !== "disabled") - const parentID = createMemo(() => info()?.parentID) - const parent = createMemo(() => { - const id = parentID() - return id ? sync().session.get(id) : undefined - }) + const parentID = input.session.data.parentID + const parent = input.session.data.parent const parentMessages = createMemo(() => { const id = parentID() return id ? (sync().data.message[id] ?? emptyMessages) : emptyMessages diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index e8de78477c..d2ee7271a8 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -53,7 +53,7 @@ import { scheduleConnectedMeasure } from "./measure" import { observeElementOffsetReconnectAware } from "./observe-element-offset" import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows" import { filterVirtualIndexes } from "./virtual-items" -import { createTimelineController, type TimelineController } from "./controller" +import { createTimelineController, type TimelineController, type TimelineSessionSource } from "./controller" const emptyTools: ToolPart[] = [] const emptyAssistantMessages: AssistantMessage[] = [] @@ -205,6 +205,7 @@ function TimelineDiffView(props: { diff: SummaryDiff }) { } type MessageTimelineProps = { + session: TimelineSessionSource actions?: UserActions scroll: { overflow: boolean; bottom: boolean; jump: boolean } onResumeScroll: () => void @@ -227,7 +228,7 @@ type MessageTimelineProps = { } export function MessageTimeline(props: MessageTimelineProps) { - const controller = createTimelineController({ userMessages: () => props.userMessages }) + const controller = createTimelineController({ session: props.session, userMessages: () => props.userMessages }) return ( ) diff --git a/packages/app/src/pages/session/timeline/model.ts b/packages/app/src/pages/session/timeline/model.ts index 7eebee6080..0a5fe5ef94 100644 --- a/packages/app/src/pages/session/timeline/model.ts +++ b/packages/app/src/pages/session/timeline/model.ts @@ -1,23 +1,24 @@ -import type { Message, UserMessage } from "@opencode-ai/sdk/v2" +import type { Message } from "@opencode-ai/sdk/v2" import { createMemo, createResource, onCleanup, untrack, type Accessor } from "solid-js" import { useServerSync } from "@/context/server-sync" import { useSync } from "@/context/sync" -import { same } from "@/utils/same" +import type { SessionController } from "../session-controller" + +export { + selectSessionUserMessages as selectUserMessages, + selectVisibleSessionUserMessages as selectVisibleUserMessages, +} from "../session-domain" -const emptyUserMessages: UserMessage[] = [] const sessionFreshness = 15_000 -export function createTimelineModel(input: { - sessionID: Accessor - revertMessageID: Accessor -}) { +export function createTimelineModel(input: { session: Pick }) { const serverSync = useServerSync() const sync = useSync() let refreshFrame: number | undefined let refreshTimer: number | undefined const [resource] = createResource( - () => input.sessionID(), + () => input.session.identity.sessionID(), (id) => { clearRefresh() if (!id) return @@ -29,7 +30,7 @@ export function createTimelineModel(input: { refreshFrame = undefined refreshTimer = window.setTimeout(() => { refreshTimer = undefined - if (input.sessionID() !== id) return + if (input.session.identity.sessionID() !== id) return untrack(() => { if (stale) void sync().session.sync(id, { force: true }) }) @@ -39,33 +40,21 @@ export function createTimelineModel(input: { return sync().session.sync(id) }, ) - const messages = createMemo(() => { - const id = input.sessionID() - return id ? (sync().data.message[id] ?? []) : [] - }) const ready = createMemo(() => { - const id = input.sessionID() + const id = input.session.identity.sessionID() return !id || isTimelineReady(sync().data.message[id], serverSync().session.history.loading(id)) }) - const userMessages = createMemo(() => selectUserMessages(messages()), emptyUserMessages, { equals: same }) - const visibleUserMessages = createMemo( - () => { - return selectVisibleUserMessages(userMessages(), input.revertMessageID()) - }, - emptyUserMessages, - { equals: same }, - ) const more = createMemo(() => { - const id = input.sessionID() + const id = input.session.identity.sessionID() return id ? sync().session.history.more(id) : false }) const loading = createMemo(() => { - const id = input.sessionID() + const id = input.session.identity.sessionID() return id ? sync().session.history.loading(id) : false }) const loadOlder = async (options?: { before?: () => void; after?: (done: boolean) => void }) => { return loadOlderTimeline({ - sessionID: input.sessionID, + sessionID: input.session.identity.sessionID, more, loading, loadMore: (sessionID) => sync().session.history.loadMore(sessionID), @@ -78,12 +67,12 @@ export function createTimelineModel(input: { return { history: { loadOlder, loading, more }, - lastUserMessage: createMemo(() => visibleUserMessages().at(-1)), - messages, + lastUserMessage: input.session.history.lastUserMessage, + messages: input.session.history.messages, ready, resource, - userMessages, - visibleUserMessages, + userMessages: input.session.history.userMessages, + visibleUserMessages: input.session.history.visibleUserMessages, } function clearRefresh() { @@ -94,19 +83,10 @@ export function createTimelineModel(input: { } } -export function selectUserMessages(messages: Message[]) { - return messages.filter((message): message is UserMessage => message.role === "user") -} - export function isTimelineReady(messages: Message[] | undefined, loading: boolean) { return messages !== undefined && (messages.some((message) => message.role === "user") || !loading) } -export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) { - if (!revertMessageID) return messages - return messages.filter((message) => message.id < revertMessageID) -} - export async function loadOlderTimeline(input: { sessionID: Accessor more: Accessor diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 12dd96a5e6..28a45a6daa 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -13,19 +13,25 @@ import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@/utils/toast" import { findLast } from "@opencode-ai/core/util/array" -import { createSessionTabs } from "@/pages/session/helpers" import { extractPromptFromParts } from "@/utils/prompt" -import { UserMessage } from "@opencode-ai/sdk/v2" -import { useSessionLayout } from "@/pages/session/session-layout" -import { createSessionOwnership } from "./session-ownership" +import type { UserMessage } from "@opencode-ai/sdk/v2" import { useLocal } from "@/context/local" +import type { SessionController } from "./session-controller" + +type SessionCommandSource = { + identity: SessionController["identity"] + data: Pick + history: Pick + layout: SessionController["layout"] + ownership: SessionController["ownership"] + tabs: Pick +} export type SessionCommandContext = { + session: SessionCommandSource navigateMessageByOffset: (offset: number) => void setActiveMessage: (message: UserMessage | undefined) => void focusInput: () => void - review?: () => boolean - fileBrowser?: () => boolean } const withCategory = (category: string) => { @@ -49,15 +55,17 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const layout = useLayout() const local = useLocal() const navigate = useNavigate() - const { params, sessionKey, tabs, view } = useSessionLayout() - const sessionOwnership = createSessionOwnership(sessionKey) + const params = actions.session.identity.params + const tabs = actions.session.layout.tabs + const view = actions.session.layout.view + const sessionOwnership = actions.session.ownership const openDialog = async (load: () => Promise, show: (value: T) => void) => { const owner = sessionOwnership.capture() const value = await load() owner.run(() => show(value)) } const runCommand = async (input: { - owner: ReturnType["capture"]> + owner: ReturnType prompt: T request: () => Promise updatePrompt: (prompt: T) => void @@ -68,39 +76,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => { input.owner.run(input.updateViewport) } - const info = () => { - const id = params.id - if (!id) return - return sync().session.get(id) - } - const hasReview = () => !!params.id - const normalizeTab = (tab: string) => { - if (!tab.startsWith("file://")) return tab - return file.tab(tab) - } - const tabState = createSessionTabs({ - tabs, - pathFromTab: file.pathFromTab, - normalizeTab, - review: actions.review, - hasReview, - fileBrowser: actions.fileBrowser, - }) - const activeFileTab = tabState.activeFileTab - const closableTab = tabState.closableTab + const info = actions.session.data.info + const activeFileTab = actions.session.tabs.activeFileTab + const closableTab = actions.session.tabs.closableTab const shown = settings.visibility.fileTree - const messages = () => { - const id = params.id - if (!id) return [] - return sync().data.message[id] ?? [] - } - const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[] - const visibleUserMessages = () => { - const revert = info()?.revert?.messageID - if (!revert) return userMessages() - return userMessages().filter((m) => m.id < revert) - } + const userMessages = actions.session.history.userMessages + const visibleUserMessages = actions.session.history.visibleUserMessages const showAllFiles = () => { if (layout.fileTree.tab() !== "changes") return @@ -309,7 +291,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const session = sdk().api.session const directory = sdk().directory const promptSession = prompt.capture() - const revert = info()?.revert?.messageID + const revert = actions.session.data.revertMessageID() const messages = userMessages() const message = findLast(messages, (x) => !revert || x.id < revert) if (!message) return @@ -338,7 +320,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const messages = userMessages() const promptSession = prompt.capture() - const revertMessageID = info()?.revert?.messageID + const revertMessageID = actions.session.data.revertMessageID() if (!revertMessageID) return const next = messages.find((x) => x.id > revertMessageID)