diff options
| author | Adam <[email protected]> | 2025-12-15 09:34:00 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-15 10:22:04 -0600 |
| commit | 5cf6a1343c6ca088bd2b586197faf7fe58961290 (patch) | |
| tree | d8001631005d2f4791bfe3a0dd3a0b21003a2516 /packages/desktop/src/pages/session.tsx | |
| parent | 44d6c5780d41616bf29a749020c9d7f98895407f (diff) | |
| download | opencode-5cf6a1343c6ca088bd2b586197faf7fe58961290.tar.gz opencode-5cf6a1343c6ca088bd2b586197faf7fe58961290.zip | |
wip(desktop): progress
Diffstat (limited to 'packages/desktop/src/pages/session.tsx')
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 148 |
1 files changed, 108 insertions, 40 deletions
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 05a9e8a1d..11056a598 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect, on } from "solid-js" import { useLocal, type LocalFile } from "@/context/local" import { createStore } from "solid-js/store" import { PromptInput } from "@/components/prompt-input" @@ -38,6 +38,9 @@ import { DialogSelectModel } from "@/components/dialog-select-model" import { useCommand } from "@/context/command" import { useNavigate, useParams } from "@solidjs/router" import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2" +import { useSDK } from "@/context/sdk" +import { usePrompt } from "@/context/prompt" +import { extractPromptFromParts } from "@/utils/prompt" export default function Page() { const layout = useLayout() @@ -48,45 +51,56 @@ export default function Page() { const command = useCommand() const params = useParams() const navigate = useNavigate() + const sdk = useSDK() + const prompt = usePrompt() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + const revertMessageID = createMemo(() => info()?.revert?.messageID) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const userMessages = createMemo(() => messages() .filter((m) => m.role === "user") .sort((a, b) => a.id.localeCompare(b.id)), ) - const lastUserMessage = createMemo(() => userMessages()?.at(-1)) + // Visible user messages excludes reverted messages (those >= revertMessageID) + const visibleUserMessages = createMemo(() => { + const revert = revertMessageID() + if (!revert) return userMessages() + return userMessages().filter((m) => m.id < revert) + }) + const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1)) const [messageStore, setMessageStore] = createStore<{ messageId?: string }>({}) const activeMessage = createMemo(() => { if (!messageStore.messageId) return lastUserMessage() - return userMessages()?.find((m) => m.id === messageStore.messageId) + // If the stored message is no longer visible (e.g., was reverted), fall back to last visible + const found = visibleUserMessages()?.find((m) => m.id === messageStore.messageId) + return found ?? lastUserMessage() }) const setActiveMessage = (message: UserMessage | undefined) => { setMessageStore("messageId", message?.id) } function navigateMessageByOffset(offset: number) { - const messages = userMessages() - if (messages.length === 0) return + const msgs = visibleUserMessages() + if (msgs.length === 0) return const current = activeMessage() - const currentIndex = current ? messages.findIndex((m) => m.id === current.id) : -1 + const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1 let targetIndex: number if (currentIndex === -1) { - targetIndex = offset > 0 ? 0 : messages.length - 1 + targetIndex = offset > 0 ? 0 : msgs.length - 1 } else { targetIndex = currentIndex + offset } - if (targetIndex < 0 || targetIndex >= messages.length) return + if (targetIndex < 0 || targetIndex >= msgs.length) return - setActiveMessage(messages[targetIndex]) + setActiveMessage(msgs[targetIndex]) } const last = createMemo( @@ -131,6 +145,24 @@ export default function Page() { } }) + // Auto-navigate to new messages when they're added + // This handles the case after undo + submit where we want to see the new message + // We track the last message ID and only navigate when a NEW message is added (ID increases) + createEffect( + on( + () => visibleUserMessages().at(-1)?.id, + (lastId, prevLastId) => { + // Only navigate if a new message was added (lastId is greater/newer than previous) + if (lastId && prevLastId && lastId > prevLastId) { + setMessageStore("messageId", undefined) + } + }, + { defer: true }, + ), + ) + + const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" }) + command.register(() => [ { id: "session.new", @@ -226,6 +258,66 @@ export default function Page() { slash: "agent", onSelect: () => local.agent.move(1), }, + { + id: "session.undo", + title: "Undo", + description: "Undo the last message", + category: "Session", + keybind: "mod+z", + slash: "undo", + disabled: !params.id || visibleUserMessages().length === 0, + onSelect: async () => { + const sessionID = params.id + if (!sessionID) return + if (status()?.type !== "idle") { + await sdk.client.session.abort({ sessionID }).catch(() => {}) + } + const revert = info()?.revert?.messageID + // Find the last user message that's not already reverted + const message = userMessages().findLast((x) => !revert || x.id < revert) + if (!message) return + await sdk.client.session.revert({ sessionID, messageID: message.id }) + // Restore the prompt from the reverted message + const parts = sync.data.part[message.id] + if (parts) { + const restored = extractPromptFromParts(parts) + prompt.set(restored) + } + // Navigate to the message before the reverted one (which will be the new last visible message) + const priorMessage = userMessages().findLast((x) => x.id < message.id) + setActiveMessage(priorMessage) + }, + }, + { + id: "session.redo", + title: "Redo", + description: "Redo the last undone message", + category: "Session", + keybind: "mod+shift+z", + slash: "redo", + disabled: !params.id || !info()?.revert?.messageID, + onSelect: async () => { + const sessionID = params.id + if (!sessionID) return + const revertMessageID = info()?.revert?.messageID + if (!revertMessageID) return + const nextMessage = userMessages().find((x) => x.id > revertMessageID) + if (!nextMessage) { + // Full unrevert - restore all messages and navigate to last + await sdk.client.session.unrevert({ sessionID }) + prompt.reset() + // Navigate to the last message (the one that was at the revert point) + const lastMsg = userMessages().findLast((x) => x.id >= revertMessageID) + setActiveMessage(lastMsg) + return + } + // Partial redo - move forward to next message + await sdk.client.session.revert({ sessionID, messageID: nextMessage.id }) + // Navigate to the message before the new revert point + const priorMsg = userMessages().findLast((x) => x.id < nextMessage.id) + setActiveMessage(priorMsg) + }, + }, ]) const handleKeyDown = (event: KeyboardEvent) => { @@ -548,7 +640,7 @@ export default function Page() { <Match when={params.id}> <div class="flex items-start justify-start h-full min-h-0"> <SessionMessageRail - messages={userMessages()} + messages={visibleUserMessages()} current={activeMessage()} onMessageSelect={setActiveMessage} wide={wide()} @@ -556,7 +648,7 @@ export default function Page() { <Show when={activeMessage()}> <SessionTurn sessionID={params.id!} - messageID={activeMessage()?.id!} + messageID={activeMessage()!.id} stepsExpanded={store.stepsExpanded} onStepsExpandedChange={(expanded) => setStore("stepsExpanded", expanded)} classes={{ @@ -564,7 +656,11 @@ export default function Page() { content: "pb-20", container: "w-full " + - (wide() ? "max-w-146 mx-auto px-6" : userMessages().length > 1 ? "pr-6 pl-18" : "px-6"), + (wide() + ? "max-w-146 mx-auto px-6" + : visibleUserMessages().length > 1 + ? "pr-6 pl-18" + : "px-6"), }} /> </Show> @@ -718,34 +814,6 @@ export default function Page() { /> </div> </Show> - <div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto"> - {/* <FileTree path="" onFileClick={ handleTabClick} /> */} - </div> - <div class="hidden shrink-0 w-56 p-2"> - <Show - when={local.file.changes().length} - fallback={<div class="px-2 text-xs text-text-muted">No changes</div>} - > - <ul class=""> - <For each={local.file.changes()}> - {(path) => ( - <li> - <button - onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })} - class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element" - > - <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" /> - <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span> - <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0"> - {getDirectory(path)} - </span> - </button> - </li> - )} - </For> - </ul> - </Show> - </div> </div> <Show when={layout.terminal.opened()}> <div |
