mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* feat(native-chat): edit a queued message in place on the host A queued card's Edit used to copy its text into the composer and delete the card, so the message lost its place, an agent-mail card lost its sender, and while a question or approval replaced the composer the text vanished. The execution host now edits the card itself: - agentSession.queuedMessageUpdate replaces only the card's text, as a compare-and-set on the body fingerprint the client last saw. It keeps the id, position, sender, kind, send mode and attachments; a command card is not editable. A resent Save answers from the card as it stands (unchanged or changed), so a lost answer never overwrites newer text. - agentSession.queuedMessageEditHold acquires, renews and releases an in-memory edit lease per caller and edit. While one is live, automatic delivery stops at the card (earlier cards still send, later ones wait); an explicit Send still overrides. Leases expire after two minutes on their own, re-derive validity from the card on every read, and die with the conversation. Release uses cleanup admission. - The published card carries an optional editHeld marker, never `paused`. - Both methods sit behind the new agent-session.queued-message-edit.v1 capability, independent of the dark mid-turn queue capability, and are on the mobile allowlist for the phone follow-up. * feat(native-chat): edit a queued card inline on desktop Edit message now opens an editor inside the queued card instead of moving the text into the composer. The card keeps its place and sender, and the editor stays usable while a question or approval holds the composer's slot (it does not lose focus to the prompt card). - Enter or Cmd/Ctrl+Enter saves, Shift+Enter is a newline, Escape cancels; IME composition keys never save or cancel. Save and Cancel replace the card's own actions while it is edited; one editor per pane. - The editor holds the host's edit lease, renewing every 30 seconds and taking it again after a lapse. Lease failures are only logged: they never block typing, Save or Cancel. - If the card is sent or removed elsewhere mid-edit, a clean edit just closes; changed text goes into the chat box after any existing draft, with one toast. A Save that lands and then sends counts as success. - Against a host without agent-session.queued-message-edit.v1 the card offers no Edit (no copy-and-delete fallback); an empty overflow menu is hidden. * fix(native-chat): name every queued-edit notice in its switch * fix(native-chat): keep queued-edit typing safe and stop disabling Send elsewhere - Save with an unreadable answer (dropped for a moved runtime fence, or lost) now reads the published card: saved text shown -> close; card gone -> the sent-as-saved / chat-box path; otherwise keep the editor quietly. - Send and Steer stay enabled on a card edited on another device; the published editHeld marker is gone (no reader left). Automatic delivery still stops at an edited card on the host. - Save answering changed moves the typing to the chat box and reopens the editor on the newer text. Notices say when the chat box is hidden by a question or approval. - Closing the pane or switching chats with unsaved typing appends it to that chat's box quietly. - Lease renewal no longer publishes, so an open editor is not idle-sweep activity. - The consume compare-and-set also matches the body fingerprint. - Saving unchanged text keeps the original blocks. * fix(native-chat): keep Agent in the Japanese queued-edit notices, as the catalog does * fix(native-chat): close a queued edit its own Save already landed, wake the queue for a pruned lease, keep typing across a reload A Save whose answer was lost now closes once the card shows the saved text, and a card that went out with exactly the typed text closes quietly instead of copying it to the chat box. A renew that prunes another editor's lapsed lease wakes the queue in place of the timer it cancels. A reload, which unmounts nothing, hands unsaved typing to the chat box like closing the pane. * refactor(native-chat): move the queued-edit notices to their own module * fix(native-chat): unref the edit-lease timer in a way the phone's typecheck accepts The lease module reaches the mobile test program through journal-store, where setTimeout answers a number, so `timer.unref?.()` failed mobile's tests typecheck ratchet. * test(cross-version-wire): call the queued-edit methods in both skew directions The manifest must name every agentSession method; the two edit methods reach their host methods on a new build and are only called by clients on a host advertising agent-session.queued-message-edit.v1. * test(rpc): count the two queued-edit methods on the structured surface * fix(native-chat): lay out the inline queued-message editor as a header, field and Save footer While a card is edited, its row shows a pencil, "Editing message", the sender and an X that cancels; the field at full width; and a footer with the key hint and Save. The card's queue icon and its status line are hidden until editing ends. * fix(native-chat): show the Enter key on the queued-message editor's Save button The editor footer's muted key hint is gone. Save now carries the same inline key badge as the new-workspace composer's Create button, extracted into a shared ButtonKeyHint so both buttons render one badge.
82 lines
4.5 KiB
TypeScript
82 lines
4.5 KiB
TypeScript
// The queued-message part of the agent-session wire: the published draft cards,
|
|
// the queue's pause beside them, and the draft mutations' answers.
|
|
|
|
import type { UnreadAgentSessionFailureFact } from './agent-session-failure'
|
|
import type { AgentJournalMessageItem } from './agent-session-journal-types'
|
|
|
|
/** The draft could not be converted into a send; an explicit Send retries it. */
|
|
export const QUEUED_MESSAGE_PAUSED_SEND_FAILED = 'send_failed' as const
|
|
|
|
export type AgentSessionQueuedMessagePausedReason = typeof QUEUED_MESSAGE_PAUSED_SEND_FAILED
|
|
|
|
/** The whole queue is paused and sends nothing on its own: 'stopped' — the user interrupted
|
|
* ("Queue paused because you interrupted"). Resume (`agentSession.queuedMessagesResume`), or any
|
|
* turn starting, lifts it; Send-now on one card sends that card and leaves the rest paused until
|
|
* its turn starts. A /clear or a reopen also holds cards until the chat's next turn, but nothing
|
|
* runs then, so neither is published. A client treats an unknown reason as a plain pause, so a
|
|
* newer host can add one. */
|
|
export type AgentSessionQueuePause = { reason: 'stopped' }
|
|
|
|
/** What rides beside a frame's `queuedMessages`, published together with the list. */
|
|
export type AgentSessionQueuePublicationFields = {
|
|
/** Null when the queue sends on its own. */
|
|
queuePause?: AgentSessionQueuePause | null
|
|
/** The card the queue sends next once nothing runs, null while anything holds the queue.
|
|
* Absent from an older host, read as null. */
|
|
nextQueuedMessageId?: string | null
|
|
}
|
|
|
|
export type AgentSessionQueuedMessagesResumeResult = {
|
|
/** False when nothing was paused, and on a replay of an already-run Resume. */
|
|
resumed: boolean
|
|
}
|
|
|
|
/** One draft the host holds for this conversation, published whole-list on the
|
|
* subscribe stream and on history pages. Text-only v1. */
|
|
export type AgentSessionQueuedMessage = {
|
|
messageId: string
|
|
position: number
|
|
body: AgentJournalMessageItem
|
|
state: 'waiting' | 'returned'
|
|
/** This one card is held, whatever the queue's pause: its conversion failed. */
|
|
paused?: true
|
|
/** Why it is held, as a marker the client localizes: 'send_failed' ("couldn't send"); only an
|
|
* explicit Send releases it. A client must treat an unknown marker as a plain hold, so a newer
|
|
* host can add one. The queue-level pause is `queuePause`, published beside the list. */
|
|
pausedReason?: AgentSessionQueuedMessagePausedReason
|
|
/** A returned card's refusal: the `reason` and `rejection` pair its submission settled with.
|
|
* Only a failure returns a card; a draft a Stop or restart took back waits again. Clients classify it from `returnedRejection` (falling back to `returnedReason` when a host
|
|
* wrote no fact) exactly as they classify a rejected submission's `rejection`, e.g.
|
|
* `classifyDispatchRejection({ reason: returnedReason, rejection: returnedRejection })`. */
|
|
returnedReason?: string | null
|
|
returnedRejection?: UnreadAgentSessionFailureFact
|
|
}
|
|
|
|
/** No body: the card leaving the published list IS the outcome, so a lost
|
|
* answer needs no re-ask and no text ever rides the wire back. */
|
|
export type AgentSessionQueuedMessageDeleteResult =
|
|
| { deleted: true; messageId: string }
|
|
/** `dispatched` means it already became a submission; `missing` covers a
|
|
* pruned tombstone. Replays answer from tombstone receipts. */
|
|
| { deleted: false; messageId: string; disposition: 'dispatched' | 'withdrawn' | 'missing' }
|
|
|
|
/** `agentSession.queuedMessageUpdate` (`agent-session.queued-message-edit.v1`). `unchanged`: the
|
|
* card already reads as asked, which is also how a resent Save whose answer was lost succeeds.
|
|
* `changed`: someone else's edit landed first; nothing was overwritten. */
|
|
export type AgentSessionQueuedMessageUpdateResult =
|
|
| { status: 'updated' | 'unchanged'; messageId: string; fingerprint: string }
|
|
| { status: 'changed' | 'not-editable'; messageId: string }
|
|
| { status: 'gone'; messageId: string; disposition: 'dispatched' | 'withdrawn' | 'missing' }
|
|
|
|
/** `agentSession.queuedMessageEditHold`: one editor's lease, keyed by the authenticated caller and
|
|
* `editId`. It expires on its own; renewing extends it, and release frees only this editor's. */
|
|
export type AgentSessionQueuedMessageEditHoldParams = {
|
|
sessionId: string
|
|
messageId: string
|
|
editId: string
|
|
} & ({ action: 'acquire'; expectedBodyFingerprint: string } | { action: 'renew' | 'release' })
|
|
|
|
export type AgentSessionQueuedMessageEditHoldResult =
|
|
| { status: 'held'; fingerprint: string; leaseDurationMs: number; remainingMs: number }
|
|
| { status: 'released' | 'expired' | 'changed' | 'gone' | 'not-editable' }
|