diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx index 87248a6a8ba..9d1e8541697 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx @@ -3,6 +3,7 @@ import { useSync } from "@tui/context/sync" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import type { TextPart } from "@opencode-ai/sdk/v2" import { Locale } from "@/util/locale" +import { Keybind } from "@/util/keybind" import { DialogMessage } from "./dialog-message" import { useDialog } from "../../ui/dialog" import type { PromptInfo } from "../../component/prompt/history" @@ -19,6 +20,26 @@ export function DialogTimeline(props: { dialog.setSize("large") }) + const keybinds = createMemo(() => { + if (!props.setPrompt) return [] + return [ + { + keybind: Keybind.parse("r")[0], + title: "repeat", + onTrigger: (option: DialogSelectOption) => { + const messageID = option.value + const allParts = sync.data.part[messageID] ?? [] + const relevantParts = allParts + const textPart = relevantParts.find((x) => x.type === "text" && !x.synthetic && !x.ignored) as TextPart | undefined + const input = textPart?.text ?? "" + const parts = relevantParts.filter((x) => x.type === "file" || x.type === "agent") + props.setPrompt!({ input, parts }) + dialog.clear() + }, + }, + ] + }) + const options = createMemo((): DialogSelectOption[] => { const messages = sync.data.message[props.sessionID] ?? [] const result = [] as DialogSelectOption[] @@ -26,7 +47,7 @@ export function DialogTimeline(props: { if (message.role !== "user") continue const part = (sync.data.part[message.id] ?? []).find( (x) => x.type === "text" && !x.synthetic && !x.ignored, - ) as TextPart + ) as TextPart | undefined if (!part) continue result.push({ title: part.text.replace(/\n/g, " "), @@ -43,5 +64,12 @@ export function DialogTimeline(props: { return result }) - return props.onMove(option.value)} title="Timeline" options={options()} /> + return ( + props.onMove(option.value)} + title="Timeline" + options={options()} + keybind={keybinds()} + /> + ) } diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index d44e606746e..3f896bf5816 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -71,10 +71,11 @@ describe("Project.fromDirectory with worktrees", () => { test("should accumulate multiple worktrees in sandboxes", async () => { await using tmp = await tmpdir({ git: true }) - const worktree1 = path.join(tmp.path, "..", "worktree-1") - const worktree2 = path.join(tmp.path, "..", "worktree-2") - await $`git worktree add ${worktree1} -b branch-1`.cwd(tmp.path).quiet() - await $`git worktree add ${worktree2} -b branch-2`.cwd(tmp.path).quiet() + const timestamp = Date.now() + const worktree1 = path.join(tmp.path, "..", `worktree-${timestamp}-1`) + const worktree2 = path.join(tmp.path, "..", `worktree-${timestamp}-2`) + await $`git worktree add ${worktree1} -b branch-${timestamp}-1`.cwd(tmp.path).quiet() + await $`git worktree add ${worktree2} -b branch-${timestamp}-2`.cwd(tmp.path).quiet() await Project.fromDirectory(worktree1) const { project } = await Project.fromDirectory(worktree2)