feat: edit/revert sent messages via server revert API — Closes #56 (#80)

* feat: edit/revert sent messages via server revert API

Wires the mobile client up to the opencode server's session.revert /
session.unrevert endpoints (the same primitive the desktop TUI uses to
edit the last message). Long-press a user message bubble -> "Edit
message" reverts it server-side and prefills the composer with its
text; a banner offers Undo while the revert is pending (it's only
cleaned up server-side on the next prompt). Degrades gracefully with
an alert on older servers that 404 the /revert route.

Closes #56

* fix(revert): address code review findings on edit/revert message flow

- Confirm before overwriting an in-progress composer draft when editing
  a sent message (F2)
- Restore reverted message's file attachments into the composer, not
  just its text (F3)
- Distinguish 401/403 from other revert failures with an accurate
  "Authentication failed" message instead of a generic one (F4)
- Exclude optimistic "temp-" message IDs from the revert cutoff
  comparison so concurrently-sent messages aren't hidden (F9)

---------

Co-authored-by: engineer <engineer@gray-knight-m1.local>
This commit is contained in:
Den
2026-07-17 02:18:05 -07:00
committed by GitHub
parent 0fdfb54d9d
commit b86ffec02f
6 changed files with 257 additions and 7 deletions

View File

@@ -35,6 +35,13 @@ export interface Session {
deletions: number
files: number
}
// Present while a message (and everything after it) is pending revert —
// the server keeps the underlying messages until the next prompt/summarize
// call runs cleanup (or the revert is undone via session.unrevert).
revert?: {
messageID: string
partID?: string
}
}
export interface Message {
@@ -163,6 +170,18 @@ export interface HealthResponse {
const REQUEST_TIMEOUT_MS = 30_000
// Thrown by request() on a non-2xx response. Carries the HTTP status so
// callers can distinguish e.g. 404 (older server, endpoint missing) from
// other failures without parsing the message string.
export class ApiError extends Error {
status: number
constructor(status: number, body: string) {
super(`API Error: ${status} - ${body}`)
this.name = "ApiError"
this.status = status
}
}
function createHeaders(config: ClientConfig): HeadersInit {
return buildRequestHeaders(config)
}
@@ -177,7 +196,7 @@ async function request<T>(config: ClientConfig, path: string, options: RequestIn
if (!response.ok) {
const error = await response.text()
throw new Error(`API Error: ${response.status} - ${error}`)
throw new ApiError(response.status, error)
}
return response.json()
@@ -365,6 +384,20 @@ export function createClient(config: ClientConfig) {
const qs = messageID ? `?messageID=${messageID}` : ""
return request<unknown[]>(config, `/session/${sessionID}/diff${qs}`)
},
// Marks messageID (and everything after it) as pending revert. The
// underlying messages aren't deleted until the next prompt runs
// cleanup, or the revert is undone with unrevert() below.
revert: (sessionID: string, messageID: string, partID?: string) =>
request<Session>(config, `/session/${sessionID}/revert`, {
method: "POST",
body: JSON.stringify(partID ? { messageID, partID } : { messageID }),
}),
unrevert: (sessionID: string) =>
request<Session>(config, `/session/${sessionID}/unrevert`, {
method: "POST",
}),
},
permission: {