Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/renderer/src/features/feed/ui/semantic/StreamingTurn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,43 @@ export const SemanticStreamingTurn = memo(function SemanticStreamingTurn({
)
const hasBlocks = blocks.length > 0

// Compaction-synthesis placeholder.
//
// When Claude Code triggers `/compact` (manual or auto), the proxy
// sees a normal `/v1/messages` POST whose last user message is the
// fixed compact prompt. The mitm addon sniffs that signature and
// the proxy adapter tags the resulting semantic turn with
// `isCompactionSynthesis: true` at turn_started. The model's
// response body is `<analysis>…</analysis><summary>…</summary>` XML
// — not user-visible text — and rendering it raw is the visible
// bug we're fixing.
//
// Returning a placeholder here is the simplest correct UI: the
// user-facing artefacts of compaction (the `compact_boundary`
// system entry and the `isCompactSummary: true` user entry) land
// via JSONL later and already have proper renderers
// (`CompactBoundaryRow` and `CompactSummaryRow` in feed/ui/rows/).
// The placeholder just covers the seconds-to-minutes streaming
// window where the synthesis turn would otherwise leak XML into
// the feed. Mirrors `CompactBoundaryRow` styling so the live and
// committed states share visual language.
//
// Placed AFTER the hooks (useContext/useMemo above) on purpose:
// moving it earlier would break Rules of Hooks because the hook
// count would differ between compaction and non-compaction renders
// of the same component instance. The wasted hook work is cheap —
// `committedAssistantTextKeys` memoizes on `committedEntries` which
// doesn't grow during the compaction window.
if (turn.isCompactionSynthesis) {
return (
<MarkerRow marker="·" tone="muted">
<div className="text-[11px] uppercase tracking-wider text-muted">
Compacting conversation…
</div>
</MarkerRow>
)
}

if (!hasBlocks) {
// WHY collapse to null instead of rendering an empty MarkerRow:
//
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/src/workspace/semantic/foldEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export function foldSemanticEvent(
//
// Same-turnId refresh (re-entry / source promotion) is
// identical for both providers.
// Compaction-synthesis flag rides on `turn_started` only — it's
// a turn-scope attribute set by ClaudeProxyAdapter from the
// request-body sniff. Read once here; subsequent delta events
// don't carry it. Defaults to false on every shape that isn't
// an explicit boolean true (other source events, older proxy
// adapters, codex/screen sources) so the placeholder UI fails
// closed.
const isCompactionSynthesis = ev.isCompactionSynthesis === true
if (!currentTurn) {
currentTurn = {
turnId,
Expand All @@ -146,11 +154,17 @@ export function foldSemanticEvent(
lookups: emptySemanticLookupSnapshot(),
startedAt: now,
endedAt: null,
...(isCompactionSynthesis ? { isCompactionSynthesis: true } : {}),
}
} else if (currentTurn.turnId === turnId) {
// Same-turn refresh (source promotion / re-entry). Preserve
// the existing flag rather than letting a flagless later
// event clear it. The proxy is the only source that sets the
// flag and it sets it on the very first turn_started.
currentTurn = {
...currentTurn,
source: typeof ev.source === 'string' ? ev.source : currentTurn.source,
...(isCompactionSynthesis ? { isCompactionSynthesis: true } : {}),
}
} else if (sessionKind === 'claude') {
history = [...history, semanticHistoryRow(currentTurn)].slice(-SEMANTIC_HISTORY_CAP)
Expand All @@ -166,6 +180,7 @@ export function foldSemanticEvent(
lookups: emptySemanticLookupSnapshot(),
startedAt: now,
endedAt: null,
...(isCompactionSynthesis ? { isCompactionSynthesis: true } : {}),
}
}
// Codex: mismatched turnId falls through — drop the event.
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/src/workspace/workspaceState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ export type SemanticLiveTurn = {
lookups: SemanticLookupSnapshot
startedAt: number
endedAt: number | null
/** True when the proxy adapter tagged this assistant turn as
* Claude Code's compaction synthesis call (the request body's
* last user message matched the fixed compact-prompt signature).
* The body that streams back is `<analysis>…</analysis>
* <summary>…</summary>` XML, NOT user-visible text — the real
* user-facing artefacts are the `compact_boundary` system entry
* and the `isCompactSummary: true` user entry that land later
* via JSONL. The streaming renderer uses this flag to swap the
* raw block stream for a "Compacting conversation…" placeholder.
*
* Defaults missing/false because non-Claude turns (codex, screen
* fallback) and pre-2026-05-11 proxy adapters never set it. The
* flag is set ONCE at `turn_started`; subsequent events for the
* same turn don't carry it (it's a turn-scope attribute, not an
* event attribute). */
isCompactionSynthesis?: boolean
}

export type SemanticFlow = {
Expand Down