Skip to content

fix(desktop): ship the runtime footer as metadata, not message text - #357

Merged
Kyzcreig merged 2 commits into
mainfrom
fix/footer-as-metadata
Jul 16, 2026
Merged

Kyzcreig merged 2 commits into
mainfrom
fix/footer-as-metadata

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

Live incident (Ace, 2026-07-15): with /footer on, every desktop message renders TWICE — once without the footer (streamed bubble), once with it (completed text).

Root cause — a two-PR interaction: #333 appended the footer to the message.complete text; #352 made the client's streamed-vs-final reconciliation text-sensitive. Streamed bubble text ≠ completed text (footer delta) → dedupe misses → the completed text lands as a duplicate bubble. Same text-baked design also made the footer vanish on session-sync re-hydration (DB rows carry no footer).

Fix — footer is metadata, never text:

  • Server: payload["footer"] on message.complete; text stays pristine
  • Client: completeAssistantMessage(sessionId, text, footer) → ChatMessage.footer → toRuntimeMessage metadata.custom.footer → new RuntimeMetadataFooter renders it as a muted monospace line under the bubble
  • Survives re-hydration and can never desync the dedupe again by construction

Tests: new server contract test (footer never concatenated into text); desktop 1305/1305, TSC clean, tui_gateway 91/91.

The /footer runtime footer was appended to the message.complete TEXT. That
broke the desktop two ways once #352's stamping landed:
1. DUPLICATED every message: the streamed bubble has no footer, the completed
   text does -> the streamed-vs-final dedupe in completeAssistantMessage no
   longer matches -> the completed text appends as a second bubble.
2. The footer vanished on session-sync re-hydration (DB rows carry no footer).

Fix: the server ships the footer as payload.footer on message.complete (text
stays pristine); the client carries it through completeAssistantMessage ->
ChatMessage.footer -> toRuntimeMessage metadata.custom.footer and renders it
as a muted monospace line under the bubble (RuntimeMetadataFooter), proper
chrome instead of fake message text.

Contract test pins 'footer never concatenated into text'. Desktop 1305 green,
TSC clean, tui_gateway suites 91 green.
@greptile-apps

greptile-apps Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR fixes a live incident where every desktop message rendered twice when /footer on was active. The root cause was that the footer text was appended to message.complete's text payload, causing a mismatch between the streamed bubble text and the completed text, which broke the dedupe logic and re-inserted the completed message as a duplicate bubble.

  • Server (tui_gateway/server.py): Footer is now placed in payload["footer"] (never concatenated into raw), and the payload construction is reordered to place it before the conditional footer block.
  • Client pipeline: GatewayEventPayload.footer → completeAssistantMessage(sessionId, text, footer) → ChatMessage.footer → toRuntimeMessage puts it in metadata.custom.footer → new RuntimeMetadataFooter component reads it via useAuiState and renders it as a muted monospace line beneath the bubble.
  • Test: A new source-scanning test in test_desktop_runtime_footer.py verifies the server keeps footer out of text by checking for the payload["footer"] = footer assignment pattern and asserting the old concatenation pattern is absent.

Confidence Score: 5/5

Safe to merge — the fix correctly moves footer delivery out of message text across the full server→client pipeline, and the deduplication logic is untouched.

The change is a targeted, structurally sound fix: the server now puts the footer in a dedicated payload key rather than mutating the raw text, and each client layer (event handler, stream hook, runtime adapter, component) handles it correctly with appropriate guards. The metadata path is independent of the dedupe and re-hydration logic, so neither can be broken by footer presence or absence. No existing behavior outside of footer rendering is affected.

All files look correct. The one observation is in the test's negative regex, which has limited coverage of alternate footer-concatenation patterns, but the positive assertion (payload["footer"] = footer must be present) is the stronger of the two guards.

Important Files Changed

Filename Overview
tui_gateway/server.py Footer is correctly split out of raw text into payload["footer"]; payload construction reordering is safe and logic is correct.
apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts Correctly extracts payload.footer with a guard for empty/whitespace strings before forwarding to completeAssistantMessage.
apps/desktop/src/app/session/hooks/use-message-stream/index.ts Footer is correctly spread into both the completeMessage (existing message path) and newAssistantFromCompletion (new message path), and is properly excluded on error completions.
apps/desktop/src/lib/chat-runtime.ts Footer is surfaced as metadata.custom.footer in the runtime message, cleanly separating it from message text and making it accessible to the new RuntimeMetadataFooter component.
apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx New RuntimeMetadataFooter reads from metadata.custom.footer via useAuiState with appropriate type guard; placed correctly inside MessagePrimitive.Root context and before the action bar.
apps/desktop/src/lib/chat-messages.ts Adds footer?: string to both ChatMessage and GatewayEventPayload types; minimal, correct type additions.
tests/tui_gateway/test_desktop_runtime_footer.py Uses server.__file__ for correct path resolution; positive assertion is a reliable guard, but the negative regex doesn't cover all concatenation forms (e.g., raw += ... or single-quote f-strings).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant S as tui_gateway/server.py
    participant GE as gateway-event.ts
    participant MS as useMessageStream
    participant RT as chat-runtime.ts
    participant UI as RuntimeMetadataFooter

    S->>S: "build payload = {text: raw, usage, status}"
    S->>S: "if raw && status=="complete": payload["footer"] = footer"
    S-->>GE: "emit message.complete {text, footer?, ...}"

    GE->>GE: extract footer (trim, guard empty)
    GE->>MS: completeAssistantMessage(sessionId, text, footer?)

    MS->>MS: completeMessage: spread footer onto ChatMessage
    MS->>RT: toRuntimeMessage(chatMessage)
    RT->>RT: "metadata.custom = footer ? {footer} : {}"
    RT-->>UI: ThreadMessage with metadata.custom.footer

    UI->>UI: useAuiState → read metadata.custom.footer
    UI-->>UI: render muted monospace line (or null)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant S as tui_gateway/server.py
    participant GE as gateway-event.ts
    participant MS as useMessageStream
    participant RT as chat-runtime.ts
    participant UI as RuntimeMetadataFooter

    S->>S: "build payload = {text: raw, usage, status}"
    S->>S: "if raw && status=="complete": payload["footer"] = footer"
    S-->>GE: "emit message.complete {text, footer?, ...}"

    GE->>GE: extract footer (trim, guard empty)
    GE->>MS: completeAssistantMessage(sessionId, text, footer?)

    MS->>MS: completeMessage: spread footer onto ChatMessage
    MS->>RT: toRuntimeMessage(chatMessage)
    RT->>RT: "metadata.custom = footer ? {footer} : {}"
    RT-->>UI: ThreadMessage with metadata.custom.footer

    UI->>UI: useAuiState → read metadata.custom.footer
    UI-->>UI: render muted monospace line (or null)
Loading

Reviews (2): Last reviewed commit: "test: CWD-independent path + explicit fi..." | Re-trigger Greptile

Comment thread tests/tui_gateway/test_desktop_runtime_footer.py Outdated
Comment thread tests/tui_gateway/test_desktop_runtime_footer.py Outdated
@Kyzcreig
Kyzcreig merged commit 4567c99 into main Jul 16, 2026
40 checks passed
@Kyzcreig
Kyzcreig deleted the fix/footer-as-metadata branch July 16, 2026 01:55
Kyzcreig added a commit that referenced this pull request Jul 16, 2026
…pts a committed twin (#362)

The #361 zombie sweep drops an un-stamped optimistic row in favor of its
polled committed twin — but the runtime footer travels ONLY on the
message.complete frame and lives on that optimistic row (DB rows never carry
it). So the sweep silently traded a footer-bearing zombie for a footer-less
committed row: footer visible live, gone one poll later (regression of the
#357 behavior, observed live 2026-07-16).

dropZombieOptimisticRows now harvests the swept zombie's footer (keyed by the
same media-normalized content key) and adoptZombieFooters grafts it onto the
adopting committed row. Guardrails: assistant rows only, never overwrites an
existing footer, one graft per harvested key.

Tests: transplant case (RED without the graft: footer undefined after adopt)
+ no-overwrite case. Suite 1323/1323, tsc clean.

Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
@Kyzcreig
Kyzcreig restored the fix/footer-as-metadata branch September 21, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant