@myrialabs/clopen 0.0.8 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/backend/index.ts +13 -1
  2. package/backend/lib/chat/stream-manager.ts +130 -10
  3. package/backend/lib/database/queries/message-queries.ts +47 -0
  4. package/backend/lib/engine/adapters/claude/stream.ts +65 -1
  5. package/backend/lib/engine/adapters/opencode/message-converter.ts +35 -77
  6. package/backend/lib/engine/types.ts +6 -0
  7. package/backend/lib/files/file-operations.ts +2 -2
  8. package/backend/lib/files/file-reading.ts +2 -2
  9. package/backend/lib/files/path-browsing.ts +2 -2
  10. package/backend/lib/terminal/pty-session-manager.ts +1 -1
  11. package/backend/lib/terminal/shell-utils.ts +4 -4
  12. package/backend/ws/chat/background.ts +3 -0
  13. package/backend/ws/chat/stream.ts +43 -1
  14. package/bin/clopen.ts +10 -0
  15. package/bun.lock +259 -381
  16. package/frontend/lib/components/chat/ChatInterface.svelte +8 -1
  17. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +20 -0
  18. package/frontend/lib/components/chat/formatters/TextMessage.svelte +3 -15
  19. package/frontend/lib/components/chat/formatters/Tools.svelte +15 -8
  20. package/frontend/lib/components/chat/input/ChatInput.svelte +70 -21
  21. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +23 -11
  22. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +1 -1
  23. package/frontend/lib/components/chat/message/ChatMessage.svelte +13 -1
  24. package/frontend/lib/components/chat/message/ChatMessages.svelte +2 -2
  25. package/frontend/lib/components/chat/message/DateSeparator.svelte +1 -1
  26. package/frontend/lib/components/chat/message/MessageBubble.svelte +2 -2
  27. package/frontend/lib/components/chat/message/MessageHeader.svelte +14 -12
  28. package/frontend/lib/components/chat/tools/AgentTool.svelte +95 -0
  29. package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +396 -0
  30. package/frontend/lib/components/chat/tools/BashTool.svelte +9 -4
  31. package/frontend/lib/components/chat/tools/EnterPlanModeTool.svelte +24 -0
  32. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +4 -7
  33. package/frontend/lib/components/chat/tools/{KillShellTool.svelte → TaskStopTool.svelte} +6 -6
  34. package/frontend/lib/components/chat/tools/index.ts +5 -2
  35. package/frontend/lib/components/checkpoint/TimelineModal.svelte +7 -2
  36. package/frontend/lib/components/history/HistoryModal.svelte +13 -5
  37. package/frontend/lib/components/workspace/DesktopNavigator.svelte +2 -1
  38. package/frontend/lib/components/workspace/MobileNavigator.svelte +2 -1
  39. package/frontend/lib/services/chat/chat.service.ts +146 -12
  40. package/frontend/lib/stores/core/app.svelte.ts +77 -0
  41. package/frontend/lib/utils/chat/message-grouper.ts +94 -12
  42. package/frontend/lib/utils/chat/message-processor.ts +37 -4
  43. package/frontend/lib/utils/chat/tool-handler.ts +96 -5
  44. package/package.json +4 -4
  45. package/shared/constants/engines.ts +1 -1
  46. package/shared/types/database/schema.ts +1 -0
  47. package/shared/types/messaging/index.ts +15 -13
  48. package/shared/types/messaging/tool.ts +185 -361
  49. package/shared/utils/message-formatter.ts +1 -0
@@ -11,7 +11,8 @@
11
11
  * - Proper presence synchronization
12
12
  */
13
13
 
14
- import { appState } from '$frontend/lib/stores/core/app.svelte';
14
+ import { appState, updateSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
15
+ import type { SessionProcessState } from '$frontend/lib/stores/core/app.svelte';
15
16
  import { chatModelState } from '$frontend/lib/stores/ui/chat-model.svelte';
16
17
  import { projectState } from '$frontend/lib/stores/core/projects.svelte';
17
18
  import { sessionState, setCurrentSession, createSession, updateSession } from '$frontend/lib/stores/core/sessions.svelte';
@@ -23,6 +24,14 @@ import { buildMetadataFromTransport } from '$shared/utils/message-formatter';
23
24
  import { debug } from '$shared/utils/logger';
24
25
  import ws from '$frontend/lib/utils/ws';
25
26
 
27
+ /**
28
+ * Tools that block the SDK waiting for user interaction.
29
+ * When these tools appear in an assistant message without a result,
30
+ * and the stream is active, the chat status switches to "waiting for input".
31
+ * Extend this list for future interactive tools.
32
+ */
33
+ const INTERACTIVE_TOOLS = new Set(['AskUserQuestion']);
34
+
26
35
  class ChatService {
27
36
  private activeProcessId: string | null = null;
28
37
  private streamCompleted: boolean = false;
@@ -58,6 +67,30 @@ class ChatService {
58
67
  this.setupWebSocketHandlers();
59
68
  }
60
69
 
70
+ /**
71
+ * Update process state for a session.
72
+ * Writes to both the per-session map (for multi-session support)
73
+ * and global convenience flags (for backward-compatible single-session components).
74
+ *
75
+ * @param update - Partial state to merge
76
+ * @param sessionId - Override session ID (defaults to this.currentSessionId or current session)
77
+ */
78
+ private setProcessState(
79
+ update: Partial<SessionProcessState>,
80
+ sessionId?: string | null
81
+ ): void {
82
+ const resolvedId = sessionId ?? this.currentSessionId ?? sessionState.currentSession?.id;
83
+ if (resolvedId) {
84
+ updateSessionProcessState(resolvedId, update);
85
+ }
86
+ // Always sync to global convenience flags
87
+ if ('isLoading' in update) appState.isLoading = update.isLoading!;
88
+ if ('isWaitingInput' in update) appState.isWaitingInput = update.isWaitingInput!;
89
+ if ('isCancelling' in update) appState.isCancelling = update.isCancelling!;
90
+ if ('isRestoring' in update) appState.isRestoring = update.isRestoring!;
91
+ if ('error' in update && update.error !== undefined) appState.error = update.error;
92
+ }
93
+
61
94
  /**
62
95
  * Check if event should be skipped (sequence-based deduplication)
63
96
  */
@@ -137,8 +170,10 @@ class ChatService {
137
170
 
138
171
  this.streamCompleted = true;
139
172
  this.reconnected = false;
140
- appState.isLoading = false;
141
- appState.isCancelling = false;
173
+ this.setProcessState({ isLoading: false, isWaitingInput: false, isCancelling: false });
174
+
175
+ // Mark any tool_use blocks that never got a tool_result
176
+ this.markInterruptedTools();
142
177
 
143
178
  // Stream completed successfully — all old cancelled streams' events
144
179
  // have definitely been delivered by now, so clear the blacklist.
@@ -161,8 +196,10 @@ class ChatService {
161
196
  this.streamCompleted = true;
162
197
  this.reconnected = false;
163
198
  this.activeProcessId = null;
164
- appState.isLoading = false;
165
- appState.isCancelling = false;
199
+ this.setProcessState({ isLoading: false, isWaitingInput: false, isCancelling: false });
200
+
201
+ // Mark any tool_use blocks that never got a tool_result
202
+ this.markInterruptedTools();
166
203
 
167
204
  // Notifications handled by GlobalStreamMonitor via chat:stream-finished
168
205
  });
@@ -178,8 +215,10 @@ class ChatService {
178
215
  // (e.g. from multiple subscriptions or late-arriving events with different processId/seq)
179
216
  this.streamCompleted = true;
180
217
  this.reconnected = false;
181
- appState.isLoading = false;
182
- appState.isCancelling = false;
218
+ this.setProcessState({ isLoading: false, isWaitingInput: false, isCancelling: false });
219
+
220
+ // Mark any tool_use blocks that never got a tool_result
221
+ this.markInterruptedTools();
183
222
 
184
223
  // Don't show notification for cancel-triggered errors
185
224
  if (data.error === 'Stream cancelled') return;
@@ -277,11 +316,10 @@ class ChatService {
277
316
  }
278
317
 
279
318
  // Set loading state
280
- appState.isLoading = true;
281
- appState.isCancelling = false;
282
319
  this.streamCompleted = false;
283
320
  this.reconnected = false;
284
321
  this.currentSessionId = sessionState.currentSession.id;
322
+ this.setProcessState({ isLoading: true, isWaitingInput: false, isCancelling: false });
285
323
  // DON'T clear cancelledProcessIds — late events from previously cancelled
286
324
  // streams must still be blocked. The set is cleared on stream complete.
287
325
  // Clear sequence tracking for new stream
@@ -432,9 +470,9 @@ class ChatService {
432
470
  this.currentSessionId = null;
433
471
  this.streamCompleted = true;
434
472
  this.reconnected = false;
435
- appState.isLoading = false;
436
- // Prevent presence effect from re-enabling loading before server confirms cancel
437
- appState.isCancelling = true;
473
+ // Update per-session map with captured ID (before it was nulled above)
474
+ // and global flags cancel sets isCancelling=true to prevent presence re-enabling
475
+ this.setProcessState({ isLoading: false, isWaitingInput: false, isCancelling: true }, chatSessionId);
438
476
 
439
477
  // Clean up stale stream_events from the cancelled stream.
440
478
  // Without this, stale stream_events remain in the messages array and cause
@@ -465,6 +503,7 @@ class ChatService {
465
503
  this.reconnected = false;
466
504
  this.lastEventSeq.clear();
467
505
  appState.isLoading = false;
506
+ appState.isWaitingInput = false;
468
507
  appState.isCancelling = false;
469
508
  }
470
509
 
@@ -549,6 +588,27 @@ class ChatService {
549
588
  })
550
589
  };
551
590
 
591
+ // Detect interactive tool_use blocks (e.g., AskUserQuestion) and set waiting status.
592
+ // When the SDK is blocked on canUseTool, partial events stop and the user must interact.
593
+ if (sdkMessage.type === 'assistant' && sdkMessage.message?.content) {
594
+ const content = Array.isArray(sdkMessage.message.content) ? sdkMessage.message.content : [];
595
+ const hasInteractiveTool = content.some(
596
+ (item: any) => item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name)
597
+ );
598
+ if (hasInteractiveTool) {
599
+ this.setProcessState({ isWaitingInput: true });
600
+ }
601
+ }
602
+
603
+ // When a user message with tool_result arrives, the SDK is unblocked — clear waiting status
604
+ if (sdkMessage.type === 'user' && sdkMessage.message?.content) {
605
+ const content = Array.isArray(sdkMessage.message.content) ? sdkMessage.message.content : [];
606
+ const hasToolResult = content.some((item: any) => item.type === 'tool_result');
607
+ if (hasToolResult && appState.isWaitingInput) {
608
+ this.setProcessState({ isWaitingInput: false });
609
+ }
610
+ }
611
+
552
612
  // For reasoning messages that couldn't find a matching stream_event,
553
613
  // insert BEFORE trailing non-reasoning assistant messages (tools/text)
554
614
  // to preserve reasoning-before-tool ordering within the same turn.
@@ -671,6 +731,80 @@ class ChatService {
671
731
  }
672
732
  }
673
733
 
734
+ /**
735
+ * Detect whether any interactive tool (e.g. AskUserQuestion) is pending in the current messages.
736
+ * Used after browser refresh / catchup to restore the isWaitingInput state.
737
+ */
738
+ detectPendingInteractiveTools(): void {
739
+ if (!appState.isLoading) return;
740
+
741
+ // Collect all tool_use IDs that have a matching tool_result
742
+ const answeredToolIds = new Set<string>();
743
+ for (const msg of sessionState.messages) {
744
+ const msgAny = msg as any;
745
+ if (msgAny.type !== 'user' || !msgAny.message?.content) continue;
746
+ const content = Array.isArray(msgAny.message.content) ? msgAny.message.content : [];
747
+ for (const item of content) {
748
+ if (item.type === 'tool_result' && item.tool_use_id) {
749
+ answeredToolIds.add(item.tool_use_id);
750
+ }
751
+ }
752
+ }
753
+
754
+ // Check if any interactive tool is unanswered
755
+ for (const msg of sessionState.messages) {
756
+ const msgAny = msg as any;
757
+ if (msgAny.type !== 'assistant' || !msgAny.message?.content) continue;
758
+ const content = Array.isArray(msgAny.message.content) ? msgAny.message.content : [];
759
+ const hasPendingInteractive = content.some(
760
+ (item: any) => item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name) && item.id && !answeredToolIds.has(item.id)
761
+ );
762
+ if (hasPendingInteractive) {
763
+ this.setProcessState({ isWaitingInput: true });
764
+ return;
765
+ }
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Mark assistant messages with unanswered tool_use blocks as interrupted.
771
+ * Sets metadata.interrupted at the MESSAGE level (not tool_use content level).
772
+ * Called when stream ends (complete/error/cancel) for immediate in-memory update.
773
+ * The backend persists this to DB via stream:lifecycle for durability.
774
+ */
775
+ private markInterruptedTools(): void {
776
+ // Collect all tool_use IDs that have a matching tool_result
777
+ const answeredToolIds = new Set<string>();
778
+
779
+ for (const msg of sessionState.messages) {
780
+ const msgAny = msg as any;
781
+ if (msgAny.type !== 'user' || !msgAny.message?.content) continue;
782
+ const content = Array.isArray(msgAny.message.content) ? msgAny.message.content : [];
783
+
784
+ for (const item of content) {
785
+ if (item.type === 'tool_result' && item.tool_use_id) {
786
+ answeredToolIds.add(item.tool_use_id);
787
+ }
788
+ }
789
+ }
790
+
791
+ // Mark messages with unanswered tool_use blocks as interrupted (message-level metadata)
792
+ for (const msg of sessionState.messages) {
793
+ const msgAny = msg as any;
794
+ if (msgAny.type !== 'assistant' || !msgAny.message?.content) continue;
795
+ const content = Array.isArray(msgAny.message.content) ? msgAny.message.content : [];
796
+
797
+ const hasUnansweredTool = content.some(
798
+ (item: any) => item.type === 'tool_use' && item.id && !answeredToolIds.has(item.id)
799
+ );
800
+
801
+ if (hasUnansweredTool && !msgAny.metadata?.interrupted) {
802
+ if (!msgAny.metadata) msgAny.metadata = {};
803
+ msgAny.metadata.interrupted = true;
804
+ }
805
+ }
806
+ }
807
+
674
808
  /**
675
809
  * Handle general errors
676
810
  */
@@ -15,14 +15,41 @@ interface PageInfo {
15
15
  actions?: import('svelte').Snippet;
16
16
  }
17
17
 
18
+ /**
19
+ * Per-session process state.
20
+ * Tracks loading/waiting/cancelling state for each chat session independently,
21
+ * enabling correct multi-session and multi-project support.
22
+ */
23
+ export interface SessionProcessState {
24
+ isLoading: boolean;
25
+ isWaitingInput: boolean;
26
+ isRestoring: boolean;
27
+ isCancelling: boolean;
28
+ error: string | null;
29
+ }
30
+
31
+ const DEFAULT_SESSION_STATE: SessionProcessState = {
32
+ isLoading: false,
33
+ isWaitingInput: false,
34
+ isRestoring: false,
35
+ isCancelling: false,
36
+ error: null,
37
+ };
38
+
18
39
  interface AppState {
19
40
  // UI Navigation
20
41
  currentView: string;
42
+
43
+ // Current session process state (convenience — synced from sessionStates for the active session)
21
44
  isLoading: boolean;
45
+ isWaitingInput: boolean;
22
46
  isRestoring: boolean;
23
47
  isCancelling: boolean;
24
48
  error: string | null;
25
49
 
50
+ // Per-session process states (source of truth for multi-session support)
51
+ sessionStates: Record<string, SessionProcessState>;
52
+
26
53
  // Page Information
27
54
  pageInfo: PageInfo;
28
55
 
@@ -36,10 +63,14 @@ export const appState = $state<AppState>({
36
63
  // UI Navigation
37
64
  currentView: 'chat',
38
65
  isLoading: false,
66
+ isWaitingInput: false,
39
67
  isRestoring: false,
40
68
  isCancelling: false,
41
69
  error: null,
42
70
 
71
+ // Per-session process states
72
+ sessionStates: {},
73
+
43
74
  // Page Information
44
75
  pageInfo: {
45
76
  title: 'Claude Code',
@@ -52,6 +83,52 @@ export const appState = $state<AppState>({
52
83
  isAppInitialized: false
53
84
  });
54
85
 
86
+ // ========================================
87
+ // PER-SESSION PROCESS STATE MANAGEMENT
88
+ // ========================================
89
+
90
+ /**
91
+ * Get the process state for a specific session.
92
+ * Returns default (idle) state if the session has no entry.
93
+ */
94
+ export function getSessionProcessState(sessionId: string): SessionProcessState {
95
+ return appState.sessionStates[sessionId] ?? DEFAULT_SESSION_STATE;
96
+ }
97
+
98
+ /**
99
+ * Update process state for a specific session in the per-session map.
100
+ * Does NOT touch global convenience flags — caller is responsible for that.
101
+ */
102
+ export function updateSessionProcessState(
103
+ sessionId: string,
104
+ update: Partial<SessionProcessState>
105
+ ): void {
106
+ if (!appState.sessionStates[sessionId]) {
107
+ appState.sessionStates[sessionId] = { ...DEFAULT_SESSION_STATE };
108
+ }
109
+ Object.assign(appState.sessionStates[sessionId], update);
110
+ }
111
+
112
+ /**
113
+ * Sync global convenience flags from a session's per-session state.
114
+ * Call when switching sessions to derive global state from the new session.
115
+ */
116
+ export function syncGlobalStateFromSession(sessionId: string): void {
117
+ const state = appState.sessionStates[sessionId] ?? DEFAULT_SESSION_STATE;
118
+ appState.isLoading = state.isLoading;
119
+ appState.isWaitingInput = state.isWaitingInput;
120
+ appState.isRestoring = state.isRestoring;
121
+ appState.isCancelling = state.isCancelling;
122
+ appState.error = state.error;
123
+ }
124
+
125
+ /**
126
+ * Remove a session's process state entry (e.g. when deleting a session).
127
+ */
128
+ export function clearSessionProcessState(sessionId: string): void {
129
+ delete appState.sessionStates[sessionId];
130
+ }
131
+
55
132
  // ========================================
56
133
  // UI STATE MANAGEMENT
57
134
  // ========================================
@@ -2,7 +2,9 @@ import type { SDKMessageFormatter } from '$shared/types/database/schema';
2
2
  import {
3
3
  shouldFilterMessage,
4
4
  extractToolUses,
5
- extractToolResults
5
+ extractToolResults,
6
+ isCompactBoundaryMessage,
7
+ isSyntheticUserMessage
6
8
  } from './message-processor';
7
9
  import { processToolMessage } from './tool-handler';
8
10
 
@@ -21,13 +23,50 @@ export interface BackgroundBashData {
21
23
  // Processed message type
22
24
  export type ProcessedMessage = SDKMessageFormatter;
23
25
 
26
+ // Module-level map for compact summary lookup (rebuilt each groupMessages call)
27
+ let _compactSummaryMap = new WeakMap<SDKMessageFormatter, string>();
28
+
29
+ // Lookup compact summary for a given compact_boundary message
30
+ export function getCompactSummary(message: SDKMessageFormatter): string | undefined {
31
+ return _compactSummaryMap.get(message);
32
+ }
33
+
34
+ // Extract text content from a user message
35
+ function extractUserTextContent(message: SDKMessageFormatter): string {
36
+ if (!('message' in message) || !message.message?.content) return '';
37
+ const content = message.message.content;
38
+ if (typeof content === 'string') return content;
39
+ if (Array.isArray(content)) {
40
+ return content
41
+ .filter((item: any) => typeof item === 'object' && item?.type === 'text')
42
+ .map((item: any) => item.text)
43
+ .join('\n');
44
+ }
45
+ return '';
46
+ }
47
+
48
+ // Get parent_tool_use_id from any message type
49
+ function getParentToolUseId(message: SDKMessageFormatter): string | null {
50
+ if ('parent_tool_use_id' in message) {
51
+ return (message as any).parent_tool_use_id ?? null;
52
+ }
53
+ return null;
54
+ }
55
+
24
56
  // Group tool_use and tool_result messages together
25
57
  export function groupMessages(messages: SDKMessageFormatter[]): {
26
58
  groups: ProcessedMessage[],
27
- toolUseMap: Map<string, ToolGroup>
59
+ toolUseMap: Map<string, ToolGroup>,
60
+ subAgentMap: Map<string, SDKMessageFormatter[]>
28
61
  } {
29
62
  const groups: ProcessedMessage[] = [];
30
63
  const toolUseMap = new Map<string, ToolGroup>();
64
+ const agentToolUseIds = new Set<string>();
65
+ const subAgentMap = new Map<string, SDKMessageFormatter[]>();
66
+ let lastCompactBoundaryIdx = -1;
67
+
68
+ // Rebuild compact summary map each call
69
+ _compactSummaryMap = new WeakMap<SDKMessageFormatter, string>();
31
70
 
32
71
  messages.forEach(message => {
33
72
  // Skip messages that should be filtered
@@ -35,8 +74,30 @@ export function groupMessages(messages: SDKMessageFormatter[]): {
35
74
  return;
36
75
  }
37
76
 
77
+ // Intercept ALL sub-agent messages (any parent_tool_use_id !== null)
78
+ // before normal processing — these belong to Agent tool sub-conversations
79
+ const parentToolId = getParentToolUseId(message);
80
+ if (parentToolId) {
81
+ if (agentToolUseIds.has(parentToolId)) {
82
+ if (!subAgentMap.has(parentToolId)) {
83
+ subAgentMap.set(parentToolId, []);
84
+ }
85
+ subAgentMap.get(parentToolId)!.push(message);
86
+ }
87
+ // Don't add any sub-agent message to main groups
88
+ return;
89
+ }
90
+
91
+ // Handle compact boundary messages — track for synthetic user embedding
92
+ if (isCompactBoundaryMessage(message)) {
93
+ lastCompactBoundaryIdx = groups.length;
94
+ groups.push(message as ProcessedMessage);
95
+ return;
96
+ }
97
+
38
98
  // Handle assistant messages with tool_use
39
99
  if (message.type === 'assistant' && 'message' in message && message.message?.content) {
100
+ lastCompactBoundaryIdx = -1;
40
101
  const toolUses = extractToolUses(message.message.content);
41
102
 
42
103
  if (toolUses.length > 0) {
@@ -47,17 +108,35 @@ export function groupMessages(messages: SDKMessageFormatter[]): {
47
108
  toolUseMessage: message,
48
109
  toolResultMessage: null
49
110
  });
111
+ // Track Agent tool IDs for sub-agent message collection
112
+ if (toolUse.name === 'Agent') {
113
+ agentToolUseIds.add(toolUse.id);
114
+ }
50
115
  }
51
116
  });
52
117
  groups.push(message as ProcessedMessage);
53
118
  } else {
54
119
  groups.push(message as ProcessedMessage);
55
120
  }
121
+ return;
56
122
  }
57
- // Handle user messages with tool_result
58
- else if (message.type === 'user' && 'message' in message && message.message?.content) {
59
- const toolResults = extractToolResults(message.message.content);
60
123
 
124
+ // Handle user messages
125
+ if (message.type === 'user' && 'message' in message && message.message?.content) {
126
+ // Synthetic user messages (after compaction): store summary in WeakMap keyed by compact boundary message
127
+ if (isSyntheticUserMessage(message) && lastCompactBoundaryIdx >= 0) {
128
+ const compactMsg = groups[lastCompactBoundaryIdx];
129
+ const summaryText = extractUserTextContent(message);
130
+ if (summaryText) {
131
+ _compactSummaryMap.set(compactMsg, summaryText);
132
+ }
133
+ lastCompactBoundaryIdx = -1;
134
+ return;
135
+ }
136
+
137
+ lastCompactBoundaryIdx = -1;
138
+
139
+ const toolResults = extractToolResults(message.message.content);
61
140
  if (toolResults.length > 0) {
62
141
  // Group tool_result with corresponding tool_use
63
142
  toolResults.forEach((toolResult: any) => {
@@ -73,20 +152,22 @@ export function groupMessages(messages: SDKMessageFormatter[]): {
73
152
  // Regular user message
74
153
  groups.push(message as ProcessedMessage);
75
154
  }
155
+ return;
76
156
  }
157
+
77
158
  // Include stream_event and other messages
78
- else {
79
- groups.push(message as ProcessedMessage);
80
- }
159
+ lastCompactBoundaryIdx = -1;
160
+ groups.push(message as ProcessedMessage);
81
161
  });
82
162
 
83
- return { groups, toolUseMap };
163
+ return { groups, toolUseMap, subAgentMap };
84
164
  }
85
165
 
86
166
  // Add tool results to messages
87
167
  export function embedToolResults(
88
168
  groups: ProcessedMessage[],
89
- toolUseMap: Map<string, ToolGroup>
169
+ toolUseMap: Map<string, ToolGroup>,
170
+ subAgentMap: Map<string, SDKMessageFormatter[]>
90
171
  ): ProcessedMessage[] {
91
172
  // Track background bash sessions
92
173
  const backgroundBashMap = trackBackgroundBashSessions(groups, toolUseMap);
@@ -100,7 +181,8 @@ export function embedToolResults(
100
181
  const processedMessage = processToolMessage(
101
182
  message,
102
183
  toolUseMap,
103
- backgroundBashMap
184
+ backgroundBashMap,
185
+ subAgentMap
104
186
  );
105
187
  return processedMessage;
106
188
  }
@@ -216,4 +298,4 @@ function trackBashOutput(
216
298
  bashData.bashOutputs.push(toolResult);
217
299
  }
218
300
  }
219
- }
301
+ }
@@ -42,7 +42,7 @@ export const TOOLS_WITH_RESULTS: ToolInput['name'][] = [
42
42
  'ExitPlanMode',
43
43
  'Glob',
44
44
  'Grep',
45
- 'KillShell',
45
+ 'TaskStop',
46
46
  'ListMcpResources',
47
47
  'NotebookEdit',
48
48
  'ReadMcpResource',
@@ -51,7 +51,12 @@ export const TOOLS_WITH_RESULTS: ToolInput['name'][] = [
51
51
  'TodoWrite',
52
52
  'WebFetch',
53
53
  'WebSearch',
54
- 'Write'
54
+ 'Write',
55
+ 'AskUserQuestion',
56
+ 'Config',
57
+ 'EnterWorktree',
58
+ 'Agent',
59
+ 'EnterPlanMode'
55
60
  ];
56
61
 
57
62
  // Tools that should be hidden from display
@@ -60,10 +65,38 @@ export const HIDDEN_TOOLS: ToolInput['name'][] = [
60
65
  'TodoWrite'
61
66
  ];
62
67
 
68
+ // Message types that represent actual conversation content and should be rendered.
69
+ // All other types are transient/metadata and should be filtered out.
70
+ const RENDERABLE_MESSAGE_TYPES = new Set(['assistant', 'user', 'stream_event']);
71
+
72
+ // Check if this is a compact boundary message (system.compact_boundary)
73
+ export function isCompactBoundaryMessage(message: SDKMessageFormatter): boolean {
74
+ return message.type === 'system' && (message as any).subtype === 'compact_boundary';
75
+ }
76
+
77
+ // Check if this is a synthetic user message (generated by system after compaction)
78
+ export function isSyntheticUserMessage(message: SDKMessageFormatter): boolean {
79
+ return message.type === 'user' && (message as any).isSynthetic === true;
80
+ }
81
+
82
+ // Check if this is a sub-agent user message (prompt sent to a sub-agent)
83
+ export function isSubAgentUserMessage(message: SDKMessageFormatter): boolean {
84
+ return message.type === 'user' &&
85
+ 'parent_tool_use_id' in message &&
86
+ (message as any).parent_tool_use_id !== null;
87
+ }
88
+
63
89
  // Check if a message should be filtered out
64
90
  export function shouldFilterMessage(message: SDKMessageFormatter): boolean {
65
- // Skip system and result type messages
66
- if (message.type === 'system' || message.type === 'result') {
91
+ // Allow compact boundary messages (displayed as separator in chat)
92
+ if (isCompactBoundaryMessage(message)) {
93
+ return false;
94
+ }
95
+
96
+ // Whitelist approach: only render conversation content types.
97
+ // Filters out: system, result, rate_limit_event, tool_progress,
98
+ // auth_status, tool_use_summary, prompt_suggestion, and any future unknown types.
99
+ if (!RENDERABLE_MESSAGE_TYPES.has(message.type)) {
67
100
  return true;
68
101
  }
69
102