Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
<LocalProvider>
<KeybindProvider>
<PromptStashProvider>
<DialogProvider>
<CommandProvider>
<PromptHistoryProvider>
<PromptHistoryProvider>
<DialogProvider>
<CommandProvider>
<PromptRefProvider>
<App />
</PromptRefProvider>
</PromptHistoryProvider>
</CommandProvider>
</DialogProvider>
</CommandProvider>
</DialogProvider>
</PromptHistoryProvider>
</PromptStashProvider>
</KeybindProvider>
</LocalProvider>
Expand Down
108 changes: 108 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/dialog-prompt-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
import { createMemo, createSignal, onMount } from "solid-js"
import { usePromptHistory, type PromptInfo } from "./prompt/history"

function formatPrompt(info: PromptInfo, maxLength?: number): string {
const parts = info.parts
.map((part) => {
if (part.type === "text") return part.text
if (part.type === "file") return `@${part.filename || "file"}`
if (part.type === "agent") return `/${part.name}`
return ""
})
.filter(Boolean)
.join(" ")

const text = (parts || info.input).trim().replace(/\n+/g, " ")
if (maxLength && text.length > maxLength) {
return text.slice(0, maxLength) + "…"
}
return text
}

type HistoryOption = DialogSelectOption<number> & {
historyInfo: PromptInfo
}

const TITLE_CHAR_LIMIT = 61
const DESCRIPTION_CHAR_LIMIT = 200

export function DialogPromptHistory(props: { onSelect: (info: PromptInfo) => void }) {
const dialog = useDialog()
const history = usePromptHistory()
const [activeValue, setActiveValue] = createSignal<number>(0)
let selectRef: DialogSelectRef<number> | undefined

const options = createMemo(() => {
const today = new Date().toDateString()
const yesterday = new Date(Date.now() - 86400000).toDateString()
const weekAgo = Date.now() - 7 * 86400000
const currentActiveValue = activeValue()

// Reverse history to show most recent first
return [...history.history].reverse().map((info, index) => {
const timestamp = Date.now() - index * 60000 // Approximate timestamp
const date = new Date(timestamp)
const dateStr = date.toDateString()

let category: string
if (dateStr === today) {
category = "Today"
} else if (dateStr === yesterday) {
category = "Yesterday"
} else if (timestamp > weekAgo) {
category = "This Week"
} else {
category = "Older"
}

const fullPromptText = formatPrompt(info)
const modeIndicator = info.mode === "shell" ? "$ " : ""
const isActive = currentActiveValue === index

const title = modeIndicator + fullPromptText

// For active item with long text, set description to trigger full display (no truncation)
// The description content doesn't matter since title won't be truncated when description exists
const needsExpansion = isActive && fullPromptText.length > TITLE_CHAR_LIMIT
const description = needsExpansion ? " " : undefined

return {
value: index,
title,
description,
category,
footer: info.mode === "shell" ? "shell" : undefined,
historyInfo: info,
} as HistoryOption
})
})

onMount(() => {
dialog.setSize("large")
})

return (
<DialogSelect
title="Prompt History"
options={options()}
ref={(r) => (selectRef = r)}
onMove={(option) => {
setActiveValue(option.value as number)
}}
onFilter={() => {
// When filter changes, first filtered item becomes active
// Get the first filtered item's value
const firstFiltered = selectRef?.filtered[0]
if (firstFiltered) {
setActiveValue(firstFiltered.value)
}
}}
onSelect={(option) => {
props.onSelect((option as HistoryOption).historyInfo)
dialog.clear()
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
})

return {
get history() {
return store.history
},
move(direction: 1 | -1, input: string) {
if (!store.history.length) return undefined
const current = store.history.at(store.index)
Expand Down
38 changes: 38 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
import { DialogAlert } from "../../ui/dialog-alert"
import { DialogPromptHistory } from "../dialog-prompt-history"
import { useToast } from "../../ui/toast"

export type PromptProps = {
Expand Down Expand Up @@ -484,6 +485,25 @@ export function Prompt(props: PromptProps) {
))
},
},
{
title: "Show prompt history",
value: "prompt.history.list",
category: "Prompt",
keybind: "prompt_history_list",
onSelect: (dialog) => {
dialog.replace(() => (
<DialogPromptHistory
onSelect={(item) => {
input.setText(item.input)
setStore("prompt", item)
setStore("mode", item.mode ?? "normal")
restoreExtmarksFromParts(item.parts)
input.cursorOffset = input.plainText.length
}}
/>
))
},
},
])

props.ref?.({
Expand Down Expand Up @@ -614,6 +634,7 @@ export function Prompt(props: PromptProps) {
}
history.append({
...store.prompt,
input: store.prompt.input.trim().replace(/\n+/g, " "),
mode: currentMode,
})
input.extmarks.clear()
Expand Down Expand Up @@ -846,6 +867,23 @@ export function Prompt(props: PromptProps) {
}
if (store.mode === "normal") autocomplete.onKeyDown(e)
if (!autocomplete.visible) {
// Show prompt history modal
if (keybind.match("prompt_history_list", e)) {
dialog.replace(() => (
<DialogPromptHistory
onSelect={(item) => {
input.setText(item.input)
setStore("prompt", item)
setStore("mode", item.mode ?? "normal")
restoreExtmarksFromParts(item.parts)
input.cursorOffset = input.plainText.length
}}
/>
))
e.preventDefault()
return
}

if (
(keybind.match("history_previous", e) && input.cursorOffset === 0) ||
(keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ function Option(props: {
overflow="hidden"
paddingLeft={3}
>
{Locale.truncate(props.title, 61)}
{props.description ? props.title : Locale.truncate(props.title, 61)}
<Show when={props.description}>
<span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
</Show>
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ export namespace Config {
.describe("Delete word backward in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Next history item"),
prompt_history_list: z.string().optional().default("none").describe("Show prompt history modal"),
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
session_parent: z.string().optional().default("<leader>up").describe("Go to parent session"),
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,10 @@ export type KeybindsConfig = {
* Next history item
*/
history_next?: string
/**
* Show prompt history modal
*/
prompt_history_list?: string
/**
* Next child session
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7623,6 +7623,11 @@
"default": "down",
"type": "string"
},
"prompt_history_list": {
"description": "Show prompt history modal",
"default": "none",
"type": "string"
},
"session_child_cycle": {
"description": "Next child session",
"default": "<leader>right",
Expand Down