fix(tui,host): project the human transcript from append-origin events
The terminal and history pagination both treated the model-visible surface as the human transcript. A landed compaction replacement therefore erased the conversation it summarized — messages the reader had already seen — and a model-only replacement copy consumed a page's `maxMessages` quota, which could also split a compaction's provenance from the replacement citing it. `dsh-session` now exports the marker split `isAppendSurfaceEvent` / `isReplacementSurfaceEvent`. The terminal replays append-origin surface events, keeps a shadowed step's tool cards paired through its append-origin assistant message, and renders one dim marker where a compaction landed; the checkpoint is recognized through the compaction seam's `isCompactCheckpointSource` contract, not the shape of the replacement. `session.history` counts only append-origin human messages. Everything model-facing keeps reading `session.surface`.
This commit is contained in:
@@ -35,6 +35,9 @@ import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-llm-retry'
|
||||
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import {
|
||||
isAppendSurfaceEvent,
|
||||
isReplacementSurfaceEvent,
|
||||
isSurfaceEligibleType,
|
||||
SessionId,
|
||||
type SessionEvent,
|
||||
type UserMessage,
|
||||
@@ -118,14 +121,14 @@ import {
|
||||
} from './chat/skill-invocation.ts'
|
||||
import { ReferenceAutocompleteProvider } from './chat/autocomplete.ts'
|
||||
import {
|
||||
activeSurfaceSeqs,
|
||||
activeToolCallIds,
|
||||
BANNER_REVEAL_INTERVAL_MS,
|
||||
BANNER_REVEAL_STEPS,
|
||||
formatCwd,
|
||||
gitBranch,
|
||||
HintEditor,
|
||||
isCompactCheckpoint,
|
||||
sessionReferenceCard,
|
||||
transcriptToolCallIds,
|
||||
} from './chat/helpers.ts'
|
||||
import {
|
||||
createModelController,
|
||||
@@ -268,6 +271,13 @@ export const inject = ['agents', 'sessions', 'commands', 'userInteraction', 'too
|
||||
/** Model guidance for path-only file references selected through the TUI. */
|
||||
export const FILE_REFERENCE_PROMPT = 'Paths prefixed with @ are files explicitly referenced by the user. Use the read tool when their contents are needed; do not claim to have inspected a file before reading it.'
|
||||
|
||||
/**
|
||||
* Transcript row standing in for one compacted range. The conversation the
|
||||
* compaction replaced stays rendered above it: the marker reports where the
|
||||
* model stopped seeing that history, not that the history is gone.
|
||||
*/
|
||||
const COMPACTION_MARKER = '… earlier context was compacted …'
|
||||
|
||||
interface RunningStatus {
|
||||
turn: number | undefined
|
||||
timer: ReturnType<typeof setInterval>
|
||||
@@ -816,6 +826,17 @@ export function createTuiChat(
|
||||
}
|
||||
}
|
||||
|
||||
const renderCompactionMarker = (): void => {
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(new Text(palette.dim(COMPACTION_MARKER), 0, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay the human transcript from the append-only log. The model-visible
|
||||
* surface shadows compacted ranges, so it is not the source here: every
|
||||
* append-origin message stays rendered, and a replacement contributes at most
|
||||
* the compaction marker at its own log position.
|
||||
*/
|
||||
const rebuildTranscript = (populateHistory: boolean): void => {
|
||||
chat.clear()
|
||||
toolCards.clear()
|
||||
@@ -823,15 +844,13 @@ export function createTuiChat(
|
||||
contextCards.clear()
|
||||
streaming = undefined
|
||||
todo.update([])
|
||||
const active = activeSurfaceSeqs(agent.session)
|
||||
const activeCalls = activeToolCallIds(agent.session, active)
|
||||
const transcriptCalls = transcriptToolCallIds(agent.session)
|
||||
for (const event of agent.session.events) {
|
||||
const isSurface = event.type === 'user/message'
|
||||
|| event.type === 'assistant/message'
|
||||
|| event.type === 'tool/result'
|
||||
|| event.type === 'steering/message'
|
||||
if (isSurface && !active.has(event.seq)) continue
|
||||
if (event.type === 'tool/call' && !activeCalls.has(event.data.callId)) continue
|
||||
if (isSurfaceEligibleType(event.type) && !isAppendSurfaceEvent(event)) {
|
||||
if (isCompactCheckpoint(event)) renderCompactionMarker()
|
||||
continue
|
||||
}
|
||||
if (event.type === 'tool/call' && !transcriptCalls.has(event.data.callId)) continue
|
||||
renderEvent(event, { addHistory: populateHistory, renderChunks: false })
|
||||
}
|
||||
requestRender()
|
||||
@@ -1438,8 +1457,11 @@ export function createTuiChat(
|
||||
recordEventUsage(tokens, event)
|
||||
if (event.type === 'turn/start' && runningStatus !== undefined) runningStatus.turn = event.data.turn
|
||||
if (event.type === 'assistant/message' && streaming?.isSettled()) streaming = undefined
|
||||
if ('surfaceOp' in event && typeof event.surfaceOp === 'object') {
|
||||
rebuildTranscript(false)
|
||||
// A replacement mutates only the model surface, so the rendered transcript
|
||||
// keeps what it already showed; a landed summary checkpoint adds its marker.
|
||||
if (isReplacementSurfaceEvent(event)) {
|
||||
if (isCompactCheckpoint(event)) renderCompactionMarker()
|
||||
requestRender()
|
||||
return
|
||||
}
|
||||
renderEvent(event, { addHistory: false, renderChunks: true })
|
||||
|
||||
Reference in New Issue
Block a user