summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/pages/session.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-15 09:34:00 -0600
committerAdam <[email protected]>2025-12-15 10:22:04 -0600
commit5cf6a1343c6ca088bd2b586197faf7fe58961290 (patch)
treed8001631005d2f4791bfe3a0dd3a0b21003a2516 /packages/desktop/src/pages/session.tsx
parent44d6c5780d41616bf29a749020c9d7f98895407f (diff)
downloadopencode-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.tsx148
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