mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
* fix(native-chat): list not-sent rows at their journal places The shared projection appended rows the host recorded but never delivered after every other row, leaving the reader to sort them. A reader that draws the list as given showed an old not-sent message under newer turns. Merge them in at their journal places, after the stop rows are built, so the list is already in order. The desktop sorts by the same order, so what it draws is unchanged. * refactor(native-chat): move the recorded-rejection line into shared words The desktop's words for a message the host recorded and then rejected now live beside the other rejection words, returning notice parts each client renders its own way, so the phone can say the same thing. The desktop calls it with the same inputs; its words are unchanged. * refactor(native-chat): one projection behaviour for rejected rows Every reader now draws a message the host recorded and then rejected where it was sent, marked not sent, so the rejectedInPlace option goes. The phone was its last off caller apart from the host's conversation outline, which now leaves not-sent rows out itself; a test proves the outline is the same as if the rejected message had never been sent. * fix(mobile): show a not-sent message in place with the reason under it The phone now draws a message the host recorded and then rejected where it was sent, as the desktop does, with one small line under the bubble in the desktop's words. The line is built from the journal the view already receives; when a start-failure or sign-in row already gives the reason, it says only that the message was not sent. The load-earlier header moves to its own file to keep the view under its line limit. * fix(mobile): a send the host recorded is never handed back When the send's own reply said the host recorded the message and then rejected it, the phone put the text back in the composer with a banner, though the host and the desktop keep it in the chat. A recorded send now settles like any other: its row draws it in place as not sent (or its kept card holds it), and the composer stays empty. The banner's failure-fact plumbing, fed only by that branch, goes with it; the transcript states the sign-in guidance once. * fix(mobile): keep each structured send on the shared optimistic list Once a not-sent row stays in the chat, matching a pending bubble by its text breaks: resending the same words after a not-sent message never reached the count the bubble waited for, so a stray copy stayed, and an acknowledgement lost on that resend showed a false "Delivery unconfirmed". A structured send is now one record, kept by the structured session under the id it is sent with. Three parts match the desktop's sender: the id, the drawing (the shared projection draws it at the tail until the host's row under that id lands, in the same pass) and the settle rule (the host's own record of it, a submission, a card, or a card's hand-off, drops it by the one rule the desktop now shares). The rest stays the phone's own: the bubble shows once the host accepts the send, not from the tap; a send whose answer was lost is held undrawn and says "Delivery unconfirmed" only if its deadline passes first, instead of going back to the composer; records are kept whether or not a chat view is open; and one send in flight is the composer's rule, not the sender's. Its photo is kept under the row id it shares with the host's row, so it shows on the bubble, the landed row and a not-sent row. The drafts' text matching, with its older queued-card matching, now serves only terminal-backed chats, whose transcripts carry no ids. * fix(native-chat): merge not-sent rows by the transcript order alone The merge kept a rule for rows with no journal place that no caller reaches and that the desktop's sort does not have; drop it, and say what merging after the stop rows actually keeps. A test pins that the list the phone draws as given is already in the desktop's sorted order, with a moved stop row, a retry run, a cut-turn notice, a held card and two not-sent rows. * fix(mobile): draw the not-sent line as the desktop does Red and capped to the bubble's width, like the desktop's line and the phone's own returned-card caption, and only when there is a notice: the row had a second, unreachable source of words. A send a Stop took back is no failure, so it no longer rebuilds the notices on every streaming frame.
106 lines
4.4 KiB
TypeScript
106 lines
4.4 KiB
TypeScript
// The words for a message the host rejected, shared by the desktop, the phone and the queue's
|
|
// returned cards.
|
|
|
|
import {
|
|
agentSessionWriteNoticeEnglish,
|
|
agentSessionWriteNotDoneParts
|
|
} from './agent-session-refusal-notice'
|
|
import type { AgentSessionWriteNoticePart } from './agent-session-write-notice-copy'
|
|
import {
|
|
readWholeAgentSessionFailureFact,
|
|
type AgentSessionFailureFact
|
|
} from './agent-session-failure'
|
|
import type { AgentSessionFailureWordsContext } from './agent-session-failure-words'
|
|
import type { AgentJournalSubmission } from './agent-session-journal-types'
|
|
import { agentSessionFailureStatedByRow } from './agent-session-visible-failures'
|
|
import { classifyDispatchRejection } from './structured-agent-session-dispatch-rejection'
|
|
|
|
/**
|
|
* What to put on screen for a rejection.
|
|
*
|
|
* A content rejection's reason is the provider explaining itself, so it is shown
|
|
* verbatim — "Claude does not support the image type .bmp" is the whole answer and
|
|
* a generic string would throw it away. A transport rejection's reason is an
|
|
* internal marker; printing it put `provider_write_failed: broken pipe` in front of
|
|
* users, which names nothing they can act on. That case gets copy that says what
|
|
* happened, and on the phone that the message can be sent again — which it can,
|
|
* because the frame provably never left, so a resend cannot duplicate.
|
|
*
|
|
* The null default claims no cause and no next step, because at that point we know
|
|
* neither: all it asserts is the one thing every rejection shares.
|
|
*
|
|
* Exported because the phone needs the same copy: the rule about
|
|
* which reasons a person may read is a property of the reason, not of the queue.
|
|
*/
|
|
export function structuredAgentSessionRejectionNotice(
|
|
reason: string | null,
|
|
write: 'send' | 'composer-send'
|
|
): string {
|
|
return agentSessionWriteNoticeEnglish(structuredAgentSessionRejectionParts(reason, write))
|
|
}
|
|
|
|
export function structuredAgentSessionRejectionParts(
|
|
reason: string | null,
|
|
write: 'send' | 'composer-send',
|
|
/** The host's typed fact, which decides when the row carried one. */
|
|
fact?: AgentSessionFailureFact,
|
|
context: AgentSessionFailureWordsContext = {}
|
|
): AgentSessionWriteNoticePart[] {
|
|
if (fact) {
|
|
return rejectionFactParts(write, fact, context)
|
|
}
|
|
if (reason === null) {
|
|
return ['notDoneSend']
|
|
}
|
|
const rejection = classifyDispatchRejection({ reason })
|
|
if (rejection.kind === 'writeFailed') {
|
|
return ['unreachable', ...agentSessionWriteNotDoneParts(write)]
|
|
}
|
|
// A legacy marker is an internal cause with no user-facing meaning; any other reason is a
|
|
// sentence written to be read — the provider's, or the host's own.
|
|
return rejection.kind ? agentSessionWriteNotDoneParts(write) : [{ text: reason }]
|
|
}
|
|
|
|
/** The line under a message the host recorded and then rejected. `startFailures`: what the loaded
|
|
* rows already state (`agentSessionVisibleFailureFacts`), so the reason is said once. */
|
|
export function structuredAgentSessionRecordedRejectionParts(
|
|
submission: AgentJournalSubmission,
|
|
context: AgentSessionFailureWordsContext,
|
|
startFailures: readonly AgentSessionFailureFact[]
|
|
): AgentSessionWriteNoticePart[] {
|
|
if (agentSessionFailureStatedByRow(submission.rejection, startFailures)) {
|
|
return agentSessionWriteNotDoneParts('send')
|
|
}
|
|
return structuredAgentSessionRejectionParts(
|
|
submission.reason,
|
|
'send',
|
|
readWholeAgentSessionFailureFact(submission.rejection),
|
|
context
|
|
)
|
|
}
|
|
|
|
function rejectionFactParts(
|
|
write: 'send' | 'composer-send',
|
|
fact: AgentSessionFailureFact,
|
|
context: AgentSessionFailureWordsContext
|
|
): AgentSessionWriteNoticePart[] {
|
|
const { kind } = classifyDispatchRejection({ reason: null, rejection: fact })
|
|
if (kind === 'writeFailed') {
|
|
return ['unreachable', ...agentSessionWriteNotDoneParts(write)]
|
|
}
|
|
// A fact this build cannot place proves only that the message did not happen.
|
|
return kind
|
|
? [{ failure: { ...fact, kind }, surface: 'rejection', context }]
|
|
: agentSessionWriteNotDoneParts(write)
|
|
}
|
|
|
|
/** What a returned queue card says about why the host rejected its message. */
|
|
export function structuredAgentSessionAttemptFailureParts(
|
|
failure: { kind: 'rejected'; reason: string | null },
|
|
context: AgentSessionFailureWordsContext = {},
|
|
/** The journal's whole fact for the rejection, when the host sent one this build can read. */
|
|
recorded?: AgentSessionFailureFact
|
|
): AgentSessionWriteNoticePart[] {
|
|
return structuredAgentSessionRejectionParts(failure.reason, 'send', recorded, context)
|
|
}
|