summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages/session/composer
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-11 16:34:48 -0500
committerGitHub <[email protected]>2026-03-11 21:34:48 +0000
commitfbd9b7cf4fb27433a066a10a197c7edb420336d1 (patch)
tree39697e059b99369f370682a0f68da037adfe1016 /packages/app/src/pages/session/composer
parent58f45ae22b0faeba4a98910b0d3b42146846462b (diff)
downloadopencode-fbd9b7cf4fb27433a066a10a197c7edb420336d1.tar.gz
opencode-fbd9b7cf4fb27433a066a10a197c7edb420336d1.zip
feat(app): restore to message and fork session (#17092)
Diffstat (limited to 'packages/app/src/pages/session/composer')
-rw-r--r--packages/app/src/pages/session/composer/session-composer-region.tsx40
-rw-r--r--packages/app/src/pages/session/composer/session-revert-dock.tsx92
2 files changed, 128 insertions, 4 deletions
diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx
index 93ea3d465..08746b51a 100644
--- a/packages/app/src/pages/session/composer/session-composer-region.tsx
+++ b/packages/app/src/pages/session/composer/session-composer-region.tsx
@@ -8,6 +8,7 @@ import { usePrompt } from "@/context/prompt"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
+import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
@@ -20,6 +21,11 @@ export function SessionComposerRegion(props: {
onNewSessionWorktreeReset: () => void
onSubmit: () => void
onResponseSubmit: () => void
+ revert?: {
+ items: { id: string; text: string }[]
+ restoring?: string
+ onRestore: (id: string) => void
+ }
setPromptDockRef: (el: HTMLDivElement) => void
visualDuration?: number
bounce?: number
@@ -116,6 +122,8 @@ export function SessionComposerRegion(props: {
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
const [height, setHeight] = createSignal(320)
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
+ const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
+ const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
const full = createMemo(() => Math.max(78, height()))
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
@@ -170,9 +178,22 @@ export function SessionComposerRegion(props: {
<Show
when={prompt.ready()}
fallback={
- <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
- {handoffPrompt() || language.t("prompt.loading")}
- </div>
+ <>
+ <Show when={rolled()} keyed>
+ {(revert) => (
+ <div class="pb-2">
+ <SessionRevertDock
+ items={revert.items}
+ restoring={revert.restoring}
+ onRestore={revert.onRestore}
+ />
+ </div>
+ )}
+ </Show>
+ <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
+ {handoffPrompt() || language.t("prompt.loading")}
+ </div>
+ </>
}
>
<Show when={dock()}>
@@ -209,12 +230,23 @@ export function SessionComposerRegion(props: {
</div>
</div>
</Show>
+ <Show when={rolled()} keyed>
+ {(revert) => (
+ <div
+ style={{
+ "margin-top": `${-36 * value()}px`,
+ }}
+ >
+ <SessionRevertDock items={revert.items} restoring={revert.restoring} onRestore={revert.onRestore} />
+ </div>
+ )}
+ </Show>
<div
classList={{
"relative z-10": true,
}}
style={{
- "margin-top": `${-36 * value()}px`,
+ "margin-top": `${-lift()}px`,
}}
>
<PromptInput
diff --git a/packages/app/src/pages/session/composer/session-revert-dock.tsx b/packages/app/src/pages/session/composer/session-revert-dock.tsx
new file mode 100644
index 000000000..ce7644b67
--- /dev/null
+++ b/packages/app/src/pages/session/composer/session-revert-dock.tsx
@@ -0,0 +1,92 @@
+import { For, Show, createMemo } from "solid-js"
+import { createStore } from "solid-js/store"
+import { Button } from "@opencode-ai/ui/button"
+import { DockTray } from "@opencode-ai/ui/dock-surface"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { useLanguage } from "@/context/language"
+
+export function SessionRevertDock(props: {
+ items: { id: string; text: string }[]
+ restoring?: string
+ onRestore: (id: string) => void
+}) {
+ const language = useLanguage()
+ const [store, setStore] = createStore({
+ collapsed: false,
+ })
+
+ const toggle = () => setStore("collapsed", (value) => !value)
+ const total = createMemo(() => props.items.length)
+ const label = createMemo(() =>
+ language.t(total() === 1 ? "session.revertDock.summary.one" : "session.revertDock.summary.other", {
+ count: total(),
+ }),
+ )
+ const preview = createMemo(() => props.items[0]?.text ?? "")
+
+ return (
+ <DockTray data-component="session-revert-dock">
+ <div
+ class="pl-3 pr-2 py-2 flex items-center gap-2"
+ role="button"
+ tabIndex={0}
+ onClick={toggle}
+ onKeyDown={(event) => {
+ if (event.key !== "Enter" && event.key !== " ") return
+ event.preventDefault()
+ toggle()
+ }}
+ >
+ <span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
+ <Show when={store.collapsed && preview()}>
+ <span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
+ </Show>
+ <div class="ml-auto shrink-0">
+ <IconButton
+ data-collapsed={store.collapsed ? "true" : "false"}
+ icon="chevron-down"
+ size="normal"
+ variant="ghost"
+ style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ }}
+ onClick={(event) => {
+ event.stopPropagation()
+ toggle()
+ }}
+ aria-label={
+ store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
+ }
+ />
+ </div>
+ </div>
+
+ <Show when={store.collapsed}>
+ <div class="h-5" aria-hidden="true" />
+ </Show>
+
+ <Show when={!store.collapsed}>
+ <div class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
+ <For each={props.items}>
+ {(item) => (
+ <div class="flex items-center gap-2 min-w-0 rounded-[10px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
+ <span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
+ <Button
+ size="small"
+ variant="secondary"
+ class="shrink-0"
+ disabled={!!props.restoring}
+ onClick={() => props.onRestore(item.id)}
+ >
+ {language.t("session.revertDock.restore")}
+ </Button>
+ </div>
+ )}
+ </For>
+ </div>
+ </Show>
+ </DockTray>
+ )
+}