Last active
December 14, 2025 20:15
-
-
Save pedramamini/c505cb7498b652a4d980101ccc78e0d0 to your computer and use it in GitHub Desktop.
Revisions
-
pedramamini revised this gist
Dec 14, 2025 . 1 changed file with 132 additions and 220 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,96 +1,81 @@ # Feature: Show Thinking Toggle A simple toggle button in the AI terminal (bottom right of chat, alongside History and Read-Only) that lets users see Claude's thinking process as it streams, instead of waiting for the final result. ## Overview **Current Behavior**: Maestro waits for the complete `type: 'result'` message from Claude Code's stream-json output. The `type: 'assistant'` streaming chunks are explicitly skipped (see `process-manager.ts:319-327`). Users see only the final polished response. **New Behavior**: When "Show Thinking" is enabled, users see the agent's response as it streams in real-time, giving visibility into reasoning before the final result arrives. ## Key Files | File | Role | |------|------| | `src/main/process-manager.ts` | Emits streaming data (currently skips `assistant` messages at line 327) | | `src/renderer/components/InputArea.tsx` | Toggle buttons location (History, Read-Only are at lines 661-700) | | `src/renderer/components/TerminalOutput.tsx` | Log rendering (handles `LogEntry.source` types) | | `src/renderer/types/index.ts` | `AITab` interface (line 272), `LogEntry` interface (line 28) | | `src/renderer/App.tsx` | State management and IPC handler registration | | `src/main/preload.ts` | IPC bridge for new events | ## Data Flow ``` User enables "Show Thinking" toggle → Tab state: aiTab.showThinking = true → Main process checks flag before spawning → ProcessManager emits 'assistant-chunk' events (new event type) → App.tsx handler appends to tab logs with source: 'thinking' → TerminalOutput renders thinking with distinct styling → Final 'result' replaces/consolidates thinking display ``` --- ## Phase 1: Type Definitions & State Add the new field to AITab and the new log source type. ### Tasks - [ ] **1.1** In `src/renderer/types/index.ts`, add `showThinking?: boolean` to the `AITab` interface (around line 289, after `scrollTop`) - [ ] **1.2** In `src/renderer/types/index.ts`, update `LogEntry.source` type (line 31) to include `'thinking'`: ```typescript source: 'stdout' | 'stderr' | 'system' | 'user' | 'ai' | 'thinking'; ``` - [ ] **1.3** In `src/renderer/App.tsx`, update the `handleToggleTabShowThinking` handler pattern (similar to existing `handleToggleTabReadOnlyMode`): ```typescript const handleToggleTabShowThinking = useCallback((sessionId: string, tabId: string) => { setSessions(prev => prev.map(s => { if (s.id !== sessionId) return s; return { ...s, aiTabs: s.aiTabs.map(tab => tab.id === tabId ? { ...tab, showThinking: !tab.showThinking } : tab ) }; })); }, []); ``` --- ## Phase 2: UI Toggle Button Add the "Show Thinking" toggle to InputArea, matching the existing History and Read-Only button patterns. ### Tasks - [ ] **2.1** In `src/renderer/components/InputArea.tsx`, import `Brain` icon from lucide-react (line ~2) - [ ] **2.2** Add props to `InputAreaProps` interface (around line 74): ```typescript tabShowThinking?: boolean; onToggleTabShowThinking?: () => void; ``` - [ ] **2.3** Add toggle button after the Read-Only button (around line 700), using `theme.colors.info` (blue) for the active state to differentiate from History (accent) and Read-Only (warning): ```tsx {/* Show Thinking toggle - AI mode only */} {session.inputMode === 'ai' && onToggleTabShowThinking && ( @@ -104,194 +89,149 @@ Add the "Show Thinking" toggle to InputArea, matching existing patterns. color: tabShowThinking ? theme.colors.info : theme.colors.textDim, border: tabShowThinking ? `1px solid ${theme.colors.info}50` : '1px solid transparent' }} title="Show Thinking - Stream AI reasoning in real-time" > <Brain className="w-3 h-3" /> <span>Thinking</span> </button> )} ``` - [ ] **2.4** In `App.tsx`, pass `tabShowThinking` and `onToggleTabShowThinking` props to InputArea (find the InputArea component usage and add these props) --- ## Phase 3: Process Manager Streaming Modify the main process to conditionally emit assistant message chunks. ### Tasks - [ ] **3.1** In `src/main/process-manager.ts`, add `streamAssistant?: boolean` to the `ManagedProcess` interface (around line 20) - [ ] **3.2** In `src/main/process-manager.ts`, modify the stream-json handling (lines 317-327) to conditionally emit assistant messages: ```typescript // Current code skips assistant messages (line 327): // Skip 'assistant' type - we prefer the complete 'result' over streaming chunks // New: Emit assistant messages if streaming is enabled if (msg.type === 'assistant' && msg.message?.content && managedProcess.streamAssistant) { const textContent = msg.message.content .filter((c: any) => c.type === 'text') .map((c: any) => c.text) .join(''); if (textContent) { this.emit('assistant-chunk', sessionId, textContent); } } ``` - [ ] **3.3** Add the `'assistant-chunk'` event type to the ProcessManager EventEmitter (check existing event patterns) - [ ] **3.4** In `src/main/preload.ts`, expose a new `onAssistantChunk` callback in the process API (similar to `onData`) - [ ] **3.5** In `src/main/index.ts`, forward the `streamAssistant` flag from spawn options to ProcessManager --- ## Phase 4: Thinking Display in UI Render thinking chunks in TerminalOutput with distinct styling. ### Tasks - [ ] **4.1** In `src/renderer/App.tsx`, add an `onAssistantChunk` listener in the useEffect that registers IPC listeners (similar to the existing `onData` pattern): ```typescript window.maestro.process.onAssistantChunk?.((sessionId: string, content: string) => { setSessions(prev => prev.map(s => { if (s.id !== sessionId) return s; const activeTab = s.aiTabs.find(t => t.id === s.activeTabId); if (!activeTab?.showThinking) return s; // Append to existing thinking entry or create new one const lastLog = activeTab.logs[activeTab.logs.length - 1]; if (lastLog?.source === 'thinking') { return { ...s, aiTabs: s.aiTabs.map(tab => tab.id === s.activeTabId ? { ...tab, logs: [...tab.logs.slice(0, -1), { ...lastLog, text: lastLog.text + content }] } : tab ) }; } else { const newLog: LogEntry = { id: `thinking-${Date.now()}`, timestamp: Date.now(), source: 'thinking', text: content }; return { ...s, aiTabs: s.aiTabs.map(tab => tab.id === s.activeTabId ? { ...tab, logs: [...tab.logs, newLog] } : tab ) }; } })); }); ``` - [ ] **4.2** In `src/renderer/components/TerminalOutput.tsx`, add handling for `log.source === 'thinking'` in the LogItem rendering (around line 280). Style with left border and dim italic text: ```tsx {log.source === 'thinking' && ( <div className="px-4 py-2 text-sm font-mono italic border-l-2" style={{ color: theme.colors.textDim, borderColor: theme.colors.info, backgroundColor: `${theme.colors.info}08`, opacity: 0.8 }} > <span className="text-xs mr-2 not-italic" style={{ color: theme.colors.info }}> thinking... </span> {log.text} </div> )} ``` - [ ] **4.3** When `result` message arrives, remove the thinking log entries and show final result (keeps UI clean). Alternatively, keep thinking visible but collapse it - decide based on user testing. --- ## Phase 5: Polish & Edge Cases Handle edge cases and improve the experience. ### Tasks - [ ] **5.1** Clear thinking block if process is interrupted (Ctrl+C / stop button) - [ ] **5.2** Add keyboard shortcut `Cmd+Shift+T` for toggling thinking (add to `src/renderer/constants/shortcuts.ts`) - [ ] **5.3** Add "Toggle Show Thinking" to command palette (Cmd+K menu) - [ ] **5.4** Debounce UI updates if chunks arrive very rapidly (e.g., 16ms throttle) - [ ] **5.5** Test with long thinking streams to ensure memory/performance is acceptable - [ ] **5.6** Ensure thinking content is excluded from synopsis generation (don't pollute History) - [ ] **5.7** Persist `showThinking` state when saving/loading sessions (add to session serialization) --- ## Test Cases | Scenario | Expected | |----------|----------| | Toggle off (default) | No thinking shown, only final result | | Toggle on, send message | Thinking streams in real-time with italic/dim styling | | Toggle on, multiple chunks | Chunks concatenate into single thinking block | | Toggle off mid-stream | Stops appending new chunks | | Final result arrives | Thinking replaced with final response | | Interrupt with stop button | Thinking block cleared | | Switch tabs | Toggle state preserved per-tab | | Restart app | Toggle state persisted (if Phase 5.7 done) | --- ## Implementation Notes ### Stream-JSON Message Format From Claude Code's `--output-format stream-json`, assistant messages look like: ```json { @@ -301,10 +241,7 @@ From Claude Code's output, `assistant` messages look like: "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Let me think about this..." } ], "model": "claude-sonnet-4-20250514", "stop_reason": null @@ -315,47 +252,22 @@ From Claude Code's output, `assistant` messages look like: The `content` array may contain multiple items; extract and concatenate all `type: 'text'` items. ### Tool Use in Thinking `assistant` messages may also contain tool use blocks. For simplicity, start by showing only text content. Tool visibility can be a future enhancement. ### Performance - Thinking streams can be verbose (10KB+ per response) - String concatenation for log entries is O(n) - acceptable for typical use - Virtual scrolling already exists in TerminalOutput for long output --- ## Success Criteria 1. Toggle appears next to History and Read-Only buttons with blue highlight 2. Enabling shows real-time streaming with distinct visual styling 3. Performance remains smooth with rapid chunk updates 4. Toggle state persists per-tab 5. Clean transition from thinking to final result -
pedramamini created this gist
Dec 14, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,361 @@ # Feature: Show Thinking Toggle Stream Claude's thinking process in real-time instead of waiting for the final synopsis. A per-tab toggle button in the AI input area (alongside History and Read-Only) that enables live streaming of assistant messages. ## Overview **Current Behavior**: When sending messages to AI agents, Maestro waits for the complete `type: 'result'` message and skips `type: 'assistant'` streaming chunks. Users see only the final response. **New Behavior**: With "Show Thinking" enabled, users see the agent's response as it streams in real-time, providing visibility into the reasoning process. ## Architecture Changes ### Key Files to Modify | File | Changes | |------|---------| | `src/main/process-manager.ts` | Emit `assistant` messages when streaming enabled | | `src/main/preload.ts` | Expose streaming preference per-session | | `src/renderer/components/InputArea.tsx` | Add toggle button UI | | `src/renderer/App.tsx` | Per-tab state management, pass to process | | `src/renderer/types/index.ts` | Add `showThinking` to AITab interface | | `src/main/index.ts` | Pass streaming flag through spawn options | ### Data Flow ``` User enables "Show Thinking" toggle → Tab state updated: aiTab.showThinking = true → Next message spawn includes streaming flag → ProcessManager emits 'assistant' chunks as they arrive → App.tsx onData handler appends to tab logs with 'thinking' type → UI renders thinking content with distinct styling → Final 'result' message replaces/consolidates thinking chunks ``` --- ## Phase 1: Core Infrastructure Add the streaming capability to the process layer without UI yet. ### Tasks - [ ] **1.1** Add `showThinking?: boolean` field to `AITab` interface in `src/renderer/types/index.ts` - [ ] **1.2** Add `streamAssistant?: boolean` to spawn options interface in `src/main/process-manager.ts` - [ ] **1.3** Modify `process-manager.ts` lines 317-326 to conditionally emit `assistant` messages: ```typescript // Existing: Skip assistant messages // if (msg.type === 'assistant') { /* skipped */ } // New: Emit assistant messages if streaming enabled if (msg.type === 'assistant' && msg.message?.content && managedProcess.streamAssistant) { // Extract text content from assistant message const textContent = msg.message.content .filter((c: any) => c.type === 'text') .map((c: any) => c.text) .join(''); if (textContent) { this.emit('thinking', sessionId, textContent); } } ``` - [ ] **1.4** Add new event type `'thinking'` to ProcessManager event emitter - [ ] **1.5** Update `preload.ts` to expose `onThinking` callback alongside `onData` - [ ] **1.6** Pass `streamAssistant` flag from spawn call in `App.tsx` to `process.spawn()` - [ ] **1.7** Update IPC handler in `index.ts` to forward `streamAssistant` option ### Test Cases - Phase 1 | Test | Expected Result | |------|-----------------| | Spawn process with `streamAssistant: false` | No `thinking` events emitted | | Spawn process with `streamAssistant: true` | `thinking` events emitted for each `assistant` chunk | | `thinking` event content | Contains only text, no JSON structure | | `result` event still fires | Final complete response still emitted | | Multiple `assistant` chunks | Each chunk emits separate `thinking` event | --- ## Phase 2: UI Toggle Button Add the "Show Thinking" toggle to InputArea, matching existing patterns. ### Tasks - [ ] **2.1** Import `Brain` icon from lucide-react in `InputArea.tsx` (or use `Sparkles`/`MessageSquareText`) - [ ] **2.2** Add props to `InputAreaProps` interface: ```typescript // Show Thinking toggle (per-tab) tabShowThinking?: boolean; onToggleTabShowThinking?: () => void; ``` - [ ] **2.3** Add toggle button between History and Read-Only buttons (line ~679): ```tsx {/* Show Thinking toggle - AI mode only */} {session.inputMode === 'ai' && onToggleTabShowThinking && ( <button onClick={onToggleTabShowThinking} className={`flex items-center gap-1.5 text-[10px] px-2 py-1 rounded-full cursor-pointer transition-all ${ tabShowThinking ? '' : 'opacity-40 hover:opacity-70' }`} style={{ backgroundColor: tabShowThinking ? `${theme.colors.info}25` : 'transparent', color: tabShowThinking ? theme.colors.info : theme.colors.textDim, border: tabShowThinking ? `1px solid ${theme.colors.info}50` : '1px solid transparent' }} title="Show Thinking - Stream AI responses in real-time" > <Brain className="w-3 h-3" /> <span>Thinking</span> </button> )} ``` - [ ] **2.4** Use `theme.colors.info` (blue) for active state to differentiate from History (accent) and Read-Only (warning/yellow) - [ ] **2.5** Update `App.tsx` to manage per-tab `showThinking` state - [ ] **2.6** Add `onToggleTabShowThinking` handler in `App.tsx`: ```typescript const handleToggleTabShowThinking = useCallback((sessionId: string, tabId: string) => { setSessions(prev => prev.map(s => { if (s.id !== sessionId) return s; return { ...s, aiTabs: s.aiTabs.map(tab => tab.id === tabId ? { ...tab, showThinking: !tab.showThinking } : tab ) }; })); }, []); ``` - [ ] **2.7** Pass `tabShowThinking` and `onToggleTabShowThinking` props to InputArea ### Test Cases - Phase 2 | Test | Expected Result | |------|-----------------| | Toggle visible in AI mode | Button appears between History and Read-Only | | Toggle hidden in terminal mode | Button not rendered | | Click toggle when off | Turns on with blue highlight | | Click toggle when on | Turns off, becomes dim | | Hover on inactive toggle | Opacity increases | | Switch tabs | Toggle state preserved per-tab | | Tooltip on hover | Shows "Show Thinking - Stream AI responses in real-time" | --- ## Phase 3: Thinking Display Render thinking chunks in the AI terminal with distinct styling. ### Tasks - [ ] **3.1** Add `'thinking'` to LogEntry type enum in `src/renderer/types/index.ts`: ```typescript type: 'user' | 'assistant' | 'system' | 'error' | 'thinking'; ``` - [ ] **3.2** Update `onThinking` handler in `App.tsx` to append thinking entries to tab logs: ```typescript const handleThinkingData = useCallback((sessionId: string, content: string) => { setSessions(prev => prev.map(s => { if (s.id !== sessionId) return s; const activeTab = s.aiTabs.find(t => t.id === s.activeTabId); if (!activeTab?.showThinking) return s; // Respect toggle state // Append or update the last thinking entry const lastLog = activeTab.logs[activeTab.logs.length - 1]; if (lastLog?.type === 'thinking') { // Append to existing thinking block return { ...s, aiTabs: s.aiTabs.map(tab => tab.id === s.activeTabId ? { ...tab, logs: [...tab.logs.slice(0, -1), { ...lastLog, content: lastLog.content + content }] } : tab ) }; } else { // Start new thinking block return { ...s, aiTabs: s.aiTabs.map(tab => tab.id === s.activeTabId ? { ...tab, logs: [...tab.logs, { type: 'thinking', content, timestamp: Date.now() }] } : tab ) }; } })); }, []); ``` - [ ] **3.3** Register `onThinking` listener in App.tsx useEffect alongside `onData` - [ ] **3.4** Update log rendering in `AITerminal.tsx` (or wherever logs are rendered) to handle `thinking` type: ```tsx {log.type === 'thinking' && ( <div className="px-4 py-2 text-sm font-mono opacity-70 italic border-l-2" style={{ color: theme.colors.textDim, borderColor: theme.colors.info, backgroundColor: `${theme.colors.info}08` }} > <span className="text-xs mr-2" style={{ color: theme.colors.info }}>thinking...</span> {log.content} </div> )} ``` - [ ] **3.5** Add subtle animation/pulsing indicator while thinking is active - [ ] **3.6** When `result` message arrives, optionally collapse/replace thinking entries: - Option A: Keep thinking visible above final result (audit trail) - Option B: Replace thinking with final result (cleaner) - **Recommendation**: Option A with collapse toggle ### Test Cases - Phase 3 | Test | Expected Result | |------|-----------------| | Thinking chunks appear | Streamed text visible with italic/dim styling | | Thinking has left border | Blue left border distinguishes from regular output | | Chunks concatenate | Multiple chunks merge into single thinking block | | Final result appears | Complete response shown after thinking | | Thinking toggle off mid-stream | No new thinking chunks appended | | Long thinking content | Scrolls properly, no layout breaking | | Rapid chunk updates | UI remains responsive, no flickering | --- ## Phase 4: State Persistence Persist the toggle state so it survives session restarts. ### Tasks - [ ] **4.1** Add `showThinking` to tab serialization in session save logic - [ ] **4.2** Load `showThinking` from persisted tab data on session restore - [ ] **4.3** Add global default setting `defaultShowThinking` in `useSettings.ts`: ```typescript const [defaultShowThinking, setDefaultShowThinkingState] = useState(false); const setDefaultShowThinking = (value: boolean) => { setDefaultShowThinkingState(value); window.maestro.settings.set('defaultShowThinking', value); }; ``` - [ ] **4.4** Apply global default when creating new tabs - [ ] **4.5** Add Settings modal toggle for global default (optional, can defer) ### Test Cases - Phase 4 | Test | Expected Result | |------|-----------------| | Toggle on, restart app | Toggle state preserved | | New tab with default off | New tabs start with thinking off | | Change global default | New tabs use new default | | Existing tabs unchanged | Changing default doesn't affect existing tabs | --- ## Phase 5: Polish & Edge Cases Handle edge cases and improve UX. ### Tasks - [ ] **5.1** Handle agent disconnection during thinking stream - [ ] **5.2** Clear thinking block if process is interrupted (Ctrl+C) - [ ] **5.3** Add keyboard shortcut for toggle (e.g., `Cmd+Shift+T` for Thinking) - [ ] **5.4** Update shortcuts documentation in `src/renderer/constants/shortcuts.ts` - [ ] **5.5** Add thinking toggle to command palette (Cmd+K) - [ ] **5.6** Consider rate limiting UI updates if chunks arrive very rapidly (debounce) - [ ] **5.7** Test with non-Claude agents (ensure graceful handling if no assistant messages) - [ ] **5.8** Add "Thinking" label to status indicator while streaming - [ ] **5.9** Ensure thinking content is excluded from synopsis generation - [ ] **5.10** Test memory usage with long thinking streams ### Test Cases - Phase 5 | Test | Expected Result | |------|-----------------| | Interrupt during thinking | Thinking block ends cleanly, no orphaned state | | Very rapid chunks | UI updates smoothly, no lag | | Non-Claude agent | Toggle visible but no effect (no assistant messages) | | Keyboard shortcut | `Cmd+Shift+T` toggles thinking | | Command palette | "Toggle Show Thinking" appears in Cmd+K | | Memory with 10MB of thinking | App remains responsive | --- ## Implementation Notes ### Stream-JSON Message Format From Claude Code's output, `assistant` messages look like: ```json { "type": "assistant", "message": { "id": "msg_xxx", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Let me think about this..." } ], "model": "claude-sonnet-4-20250514", "stop_reason": null }, "session_id": "xxx" } ``` The `content` array may contain multiple items; extract and concatenate all `type: 'text'` items. ### Thinking vs Tool Use `assistant` messages may also contain tool use blocks: ```json { "type": "tool_use", "id": "toolu_xxx", "name": "Read", "input": { "file_path": "/some/path" } } ``` **Decision**: Should tool use be shown in thinking stream? - **Option A**: Show only text content (simpler, less noise) - **Option B**: Show tool names too (more transparency) - **Recommendation**: Start with Option A, add tool names as enhancement ### Performance Considerations - Thinking streams can be verbose (10KB+ per message) - Concatenating to log entries is O(n) - consider ring buffer for very long streams - Virtual scrolling may be needed for extremely long thinking output - Consider collapsing old thinking blocks to reduce DOM nodes --- ## Success Criteria 1. **Functional**: Toggle enables/disables real-time streaming per tab 2. **Performant**: No noticeable lag with rapid streaming updates 3. **Persistent**: Toggle state survives app restart 4. **Consistent**: UI matches existing History/Read-Only toggle patterns 5. **Graceful**: Works with all agents, degrades gracefully for non-streaming agents --- ## Future Enhancements (Out of Scope) - [ ] Collapsible thinking sections - [ ] Syntax highlighting in thinking output - [ ] "Thinking time" metrics display - [ ] Export thinking transcript - [ ] Filter thinking by category (text vs tool use) - [ ] Global "Show Thinking for all tabs" quick toggle