fix: orchestrator race conditions, diff preview, session snapshot tools, and test fixes

- Fix duplicate inference requests after approval resume by registering orchestrator
  jobs in activeSessionJobs (ServerModule.launchSessionRun), so the guard in the
  OrchestrationResumedEvent subscription prevents spurious duplicate resume.
- Replace blocking Files.readString in computeToolPreview with withContext(Dispatchers.IO)
  by making the function suspend — no blocking I/O on the coroutine dispatcher.
- Guard orderedIds.add() in rebuildTools against duplicate invocation IDs on replayed
  ToolInvocationRequestedEvent to prevent duplicate tool records in snapshots.
- Add unified-diff preview for file_write tools: computeToolPreview reads existing
  file and generates ---/+++ diff before emitting ApprovalRequestedEvent.
- Render diff preview with color coding in ApprovalSurface (@@ yellow, +/- green/red).
- Include current-stage tool records in SessionSnapshot via rebuildTools event replay.
- Fix RouterContextBuilderTest: recursive buildPack helper and 2 tests using class-level
  builder instead of their local builder instances (conversationKeepLast mismatch).
- Add suspend keyword to RouterFacadeTest mock, fix buildPack recursion.
This commit is contained in:
2026-05-28 15:37:16 +04:00
parent e05532e7b2
commit 57d2237ba0
11 changed files with 230 additions and 82 deletions
@@ -15,9 +15,14 @@ import dev.tamboui.widgets.block.Title
import dev.tamboui.widgets.paragraph.Paragraph
private const val PREVIEW_MAX_LINES = 20
private const val DIFF_LINE_MAX_LENGTH = 100
private val dimStyle = Style.create().dim().gray()
private val greenStyle = Style.create().green()
private val redStyle = Style.create().red()
private val yellowStyle = Style.create().yellow()
fun approvalSurfaceWidget(state: TuiState): Paragraph {
val dimStyle = Style.create().dim().gray()
val approval = state.selectedPendingApproval()
val pendingDecision = state.pendingDecision
@@ -32,13 +37,31 @@ fun approvalSurfaceWidget(state: TuiState): Paragraph {
val riskSpan = Span.styled(approval.riskSummary, dimStyle)
add(Line.from(toolSpan, Span.raw(" "), tierBadge, Span.raw(" "), riskSpan))
// Command preview (monospace, max 20 lines, truncation notice)
if (approval.preview != null) {
val previewLines = approval.preview.lines()
// Preview: unified diff for file_write/file_edit, raw text otherwise
val preview = approval.preview
val isDiff = preview?.startsWith("--- a/") == true
if (preview != null) {
val previewLines = preview.lines()
if (previewLines.isNotEmpty()) {
val visibleLines = previewLines.take(PREVIEW_MAX_LINES)
for (line in visibleLines) {
add(Line.from(Span.raw(" $line")))
if (isDiff) {
val truncated = if (line.length > DIFF_LINE_MAX_LENGTH) {
line.take(DIFF_LINE_MAX_LENGTH - 3) + "..."
} else {
line
}
val span = when {
truncated.startsWith("@@") -> Span.styled(" $truncated", yellowStyle)
truncated.startsWith("+++") || truncated.startsWith("---") -> Span.styled(" $truncated", dimStyle)
truncated.startsWith("+") -> Span.styled(" $truncated", greenStyle)
truncated.startsWith("-") -> Span.styled(" $truncated", redStyle)
else -> Span.raw(" $truncated")
}
add(Line.from(span))
} else {
add(Line.from(Span.raw(" $line")))
}
}
if (previewLines.size > PREVIEW_MAX_LINES) {
val remaining = previewLines.size - PREVIEW_MAX_LINES
@@ -274,6 +274,21 @@ object SessionsReducer {
detail = entry.detail,
)
}
val tools = msg.tools.map { tool ->
val displayStatus = when (tool.status) {
"COMPLETED" -> ToolDisplayStatus.COMPLETED
"FAILED" -> ToolDisplayStatus.FAILED
"REJECTED" -> ToolDisplayStatus.REJECTED
else -> ToolDisplayStatus.STARTED
}
TuiToolRecord(
name = tool.name,
tier = tool.tier,
status = displayStatus,
argsPreview = null,
diff = tool.diff,
)
}
val summary = SessionSummary(
id = msg.sessionId.value,
status = status,
@@ -284,6 +299,7 @@ object SessionsReducer {
currentStageId = msg.state.currentStageId,
pendingApproval = approvalInfo,
recentEvents = eventEntries,
tools = tools,
)
val selected = sessionState.selectedId ?: msg.sessionId.value
return sessionState.copy(