Skip to content
13 changes: 13 additions & 0 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,19 @@ function App() {
},
category: "System",
},
{
title: kv.get("agent_timestamps", "hide") === "show" ? "Hide agent timestamps" : "Show agent timestamps",
value: "session.toggle.agent_timestamps",
category: "System",
slash: {
name: "agent-timestamps",
},
onSelect: (dialog) => {
const current = kv.get("agent_timestamps", "hide")
kv.set("agent_timestamps", current === "show" ? "hide" : "show")
dialog.clear()
},
},
{
title: "Exit the app",
value: "app.exit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,37 @@ export function DialogMessage(props: {
dialog.clear()
},
},
{
title: "Rewind",
value: "session.rewind",
description: "remove selected and later messages",
onSelect: async (dialog) => {
const msg = message()
if (!msg) return

if (props.setPrompt) {
const parts = sync.data.part[msg.id]
const promptInfo = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
props.setPrompt(promptInfo)
}

await sdk.client.session.rewind({
sessionID: props.sessionID,
messageID: msg.id,
})

dialog.clear()
},
},
{
title: "Fork",
value: "session.fork",
Expand Down
20 changes: 20 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const context = createContext<{
conceal: () => boolean
showThinking: () => boolean
showTimestamps: () => boolean
showAgentTimestamps: () => boolean
showDetails: () => boolean
showGenericToolOutput: () => boolean
diffWrapMode: () => "word" | "none"
Expand Down Expand Up @@ -150,6 +151,7 @@ export function Session() {
const [conceal, setConceal] = createSignal(true)
const [showThinking, setShowThinking] = kv.signal("thinking_visibility", true)
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
const [agentTimestamps, setAgentTimestamps] = kv.signal<"hide" | "show">("agent_timestamps", "hide")
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
Expand All @@ -166,6 +168,7 @@ export function Session() {
return false
})
const showTimestamps = createMemo(() => timestamps() === "show")
const showAgentTimestamps = createMemo(() => agentTimestamps() === "show")
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)

const scrollAcceleration = createMemo(() => {
Expand Down Expand Up @@ -562,6 +565,18 @@ export function Session() {
dialog.clear()
},
},
{
title: showAgentTimestamps() ? "Hide agent timestamps" : "Show agent timestamps",
value: "session.toggle.agent_timestamps",
category: "Session",
slash: {
name: "agent-timestamps",
},
onSelect: (dialog) => {
setAgentTimestamps((prev) => (prev === "show" ? "hide" : "show"))
dialog.clear()
},
},
{
title: showThinking() ? "Hide thinking" : "Show thinking",
value: "session.toggle.thinking",
Expand Down Expand Up @@ -987,6 +1002,7 @@ export function Session() {
conceal,
showThinking,
showTimestamps,
showAgentTimestamps,
showDetails,
showGenericToolOutput,
diffWrapMode,
Expand Down Expand Up @@ -1277,6 +1293,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
const local = useLocal()
const { theme } = useTheme()
const sync = useSync()
const ctx = use()
const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])

const final = createMemo(() => {
Expand Down Expand Up @@ -1338,6 +1355,9 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
</span>{" "}
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
<span style={{ fg: theme.textMuted }}> · {props.message.modelID}</span>
<Show when={ctx.showAgentTimestamps()}>
<span style={{ fg: theme.textMuted }}> · {Locale.todayTimeOrDateTime(props.message.time.created)}</span>
</Show>
<Show when={duration()}>
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
</Show>
Expand Down
35 changes: 35 additions & 0 deletions packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,41 @@ export const SessionRoutes = lazy(() =>
return c.json(true)
},
)
.post(
"/:sessionID/rewind",
describeRoute({
summary: "Rewind session",
description:
"Rewind a session to a specific message, removing all messages from that point without reverting file changes.",
operationId: "session.rewind",
responses: {
200: {
description: "Rewound session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
sessionID: z.string(),
}),
),
validator("json", Session.rewind.schema.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const session = await Session.rewind({
sessionID,
...c.req.valid("json"),
})
return c.json(session)
},
)
.post(
"/:sessionID/fork",
describeRoute({
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,27 @@ export namespace Session {
},
)

export const rewind = fn(
z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message"),
}),
async (input) => {
SessionPrompt.assertNotBusy(input.sessionID)
const msgs = await messages({ sessionID: input.sessionID })
for (const msg of msgs) {
if (msg.info.id >= input.messageID) {
Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run())
Bus.publish(MessageV2.Event.Removed, {
sessionID: input.sessionID,
messageID: msg.info.id,
})
}
}
return get(input.sessionID)
},
)

export const fork = fn(
z.object({
sessionID: Identifier.schema("session"),
Expand Down
Loading