Skip to content

Commit ca4126d

Browse files
authored
feat: truncate thinking display to 2 lines with ctrl+o expand hint (#779)
* feat: truncate thinking display to 2 lines with ctrl+o expand hint
1 parent fe84e23 commit ca4126d

2 files changed

Lines changed: 114 additions & 1 deletion

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Thinking Component Truncation Design
2+
3+
## Problem
4+
5+
The `Thinking` component in `src/ui/Messages.tsx` displays all reasoning/thinking text from the LLM without any truncation. When the model produces long reasoning content, it takes up excessive screen space and distracts from the actual response.
6+
7+
## Solution
8+
9+
Add truncation logic to the `Thinking` component: default to showing at most 2 lines, with a hint to press `ctrl+o` (transcript mode) to see the full content.
10+
11+
## Design
12+
13+
### What Changes
14+
15+
**File**: `src/ui/Messages.tsx``Thinking` component only.
16+
17+
**Imports**: `useAppStore` is already imported in the file; `useMemo` is already imported from React.
18+
19+
### Current Implementation
20+
21+
```tsx
22+
function Thinking({ text }: { text: string }) {
23+
return (
24+
<Box flexDirection="column" marginTop={SPACING.MESSAGE_MARGIN_TOP}>
25+
<Text bold color="gray">thinking</Text>
26+
<Text color="gray" italic>{text}</Text>
27+
</Box>
28+
);
29+
}
30+
```
31+
32+
### New Implementation
33+
34+
1. Read `transcriptMode` from `useAppStore()`
35+
2. Use `useMemo` with dependencies `[text, transcriptMode]` to compute truncation:
36+
- Split `text` by `\n` into `lines`
37+
- If `transcriptMode || lines.length <= 2`, show all content
38+
- Otherwise, show `lines.slice(0, 2).join('\n')` and compute `hiddenCount = lines.length - 2`
39+
3. When truncated, render a separate `<Text>` hint element below the content
40+
4. Truncated text maintains existing `color="gray" italic` styling
41+
42+
### Truncation Hint Style
43+
44+
Rendered as a separate `<Text>` element (not counted as part of the 2 visible lines). Reuses the same style as `ExpandableOutput`:
45+
```tsx
46+
<Text color="gray" dimColor>
47+
... {hiddenCount} more line{hiddenCount === 1 ? '' : 's'} hidden (Press ctrl+o to expand) ...
48+
</Text>
49+
```
50+
51+
### Edge Cases
52+
53+
- **Empty or whitespace-only text**: Render nothing (return early or render empty)
54+
- **Trailing newlines**: `"line1\nline2\n".split('\n')` produces `["line1", "line2", ""]`. The empty trailing element is included in the count — this is acceptable since `ExpandableOutput` follows the same behavior
55+
- **Exactly 2 lines**: Show full content, no hint
56+
- **Single line**: Show full content, no hint
57+
58+
### Data Flow
59+
60+
```
61+
text (string)
62+
→ useMemo([text, transcriptMode])
63+
→ split('\n') → lines[]
64+
→ transcriptMode || lines.length <= 2 ? { displayText: text, shouldTruncate: false }
65+
: { displayText: lines.slice(0, 2).join('\n'), hiddenCount, shouldTruncate: true }
66+
→ render <Text color="gray" italic>{displayText}</Text>
67+
→ if shouldTruncate → render hint <Text> below
68+
```
69+
70+
### What Does NOT Change
71+
72+
- `ExpandableOutput` component — no modifications
73+
- `ctrl+o` / `transcriptMode` toggle logic in `App.tsx` — no modifications
74+
- `DiffViewer` component — no modifications
75+
- No new files created
76+
77+
### Constants
78+
79+
- Max lines value `2` is used inline within the `useMemo` computation (consistent with how `ExpandableOutput` uses its `maxLines` prop default)
80+
81+
## Testing
82+
83+
- Verify thinking content with 0 or 1 line renders fully without hint
84+
- Verify thinking content with exactly 2 lines renders fully without hint
85+
- Verify thinking content with 3+ lines shows only 2 lines + hint
86+
- Verify the hint text matches: `... N more lines hidden (Press ctrl+o to expand) ...`
87+
- Verify pressing `ctrl+o` (transcript mode) shows full thinking content
88+
- Verify pressing `ctrl+o` again (or Escape) returns to truncated view
89+
- Verify empty text renders nothing or an empty box

src/ui/Messages.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,14 +592,38 @@ function AssistantWithTools({
592592
}
593593

594594
function Thinking({ text }: { text: string }) {
595+
const { transcriptMode } = useAppStore();
596+
597+
const { displayText, hiddenCount, shouldTruncate } = useMemo(() => {
598+
const lines = text.split('\n');
599+
const maxLines = 2;
600+
const shouldTruncate = !transcriptMode && lines.length > maxLines;
601+
602+
if (!shouldTruncate) {
603+
return { displayText: text, hiddenCount: 0, shouldTruncate: false };
604+
}
605+
606+
return {
607+
displayText: lines.slice(0, maxLines).join('\n'),
608+
hiddenCount: lines.length - maxLines,
609+
shouldTruncate: true,
610+
};
611+
}, [text, transcriptMode]);
612+
595613
return (
596614
<Box flexDirection="column" marginTop={SPACING.MESSAGE_MARGIN_TOP}>
597615
<Text bold color="gray">
598616
thinking
599617
</Text>
600618
<Text color="gray" italic>
601-
{text}
619+
{displayText}
602620
</Text>
621+
{shouldTruncate && (
622+
<Text color="gray" dimColor>
623+
... {hiddenCount} more line{hiddenCount === 1 ? '' : 's'} hidden
624+
(Press ctrl+o to expand) ...
625+
</Text>
626+
)}
603627
</Box>
604628
);
605629
}

0 commit comments

Comments
 (0)