@vybestack/llxprt-ui 0.7.0-nightly.251211.5750c518a
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.
- package/PLAN-messages.md +681 -0
- package/PLAN.md +47 -0
- package/README.md +25 -0
- package/bun.lock +1024 -0
- package/dev-docs/ARCHITECTURE.md +178 -0
- package/dev-docs/CODE_ORGANIZATION.md +232 -0
- package/dev-docs/STANDARDS.md +235 -0
- package/dev-docs/UI_DESIGN.md +425 -0
- package/eslint.config.cjs +194 -0
- package/images/nui.png +0 -0
- package/llxprt.png +0 -0
- package/llxprt.svg +128 -0
- package/package.json +66 -0
- package/scripts/check-limits.ts +177 -0
- package/scripts/start.js +71 -0
- package/src/app.tsx +599 -0
- package/src/bootstrap.tsx +23 -0
- package/src/commands/AuthCommand.tsx +80 -0
- package/src/commands/ModelCommand.tsx +102 -0
- package/src/commands/ProviderCommand.tsx +103 -0
- package/src/commands/ThemeCommand.tsx +71 -0
- package/src/features/chat/history.ts +178 -0
- package/src/features/chat/index.ts +3 -0
- package/src/features/chat/persistentHistory.ts +102 -0
- package/src/features/chat/responder.ts +217 -0
- package/src/features/completion/completions.ts +161 -0
- package/src/features/completion/index.ts +3 -0
- package/src/features/completion/slash.test.ts +82 -0
- package/src/features/completion/slash.ts +248 -0
- package/src/features/completion/suggestions.test.ts +51 -0
- package/src/features/completion/suggestions.ts +112 -0
- package/src/features/config/configSession.test.ts +189 -0
- package/src/features/config/configSession.ts +179 -0
- package/src/features/config/index.ts +4 -0
- package/src/features/config/llxprtAdapter.integration.test.ts +202 -0
- package/src/features/config/llxprtAdapter.test.ts +139 -0
- package/src/features/config/llxprtAdapter.ts +257 -0
- package/src/features/config/llxprtCommands.test.ts +40 -0
- package/src/features/config/llxprtCommands.ts +35 -0
- package/src/features/config/llxprtConfig.test.ts +261 -0
- package/src/features/config/llxprtConfig.ts +418 -0
- package/src/features/theme/index.ts +2 -0
- package/src/features/theme/theme.test.ts +51 -0
- package/src/features/theme/theme.ts +105 -0
- package/src/features/theme/themeManager.ts +84 -0
- package/src/hooks/useAppCommands.ts +129 -0
- package/src/hooks/useApprovalKeyboard.ts +156 -0
- package/src/hooks/useChatStore.test.ts +112 -0
- package/src/hooks/useChatStore.ts +252 -0
- package/src/hooks/useInputManager.ts +99 -0
- package/src/hooks/useKeyboardHandlers.ts +130 -0
- package/src/hooks/useListNavigation.test.ts +166 -0
- package/src/hooks/useListNavigation.ts +62 -0
- package/src/hooks/usePersistentHistory.ts +94 -0
- package/src/hooks/useScrollManagement.ts +107 -0
- package/src/hooks/useSelectionClipboard.ts +48 -0
- package/src/hooks/useSessionManager.test.ts +85 -0
- package/src/hooks/useSessionManager.ts +101 -0
- package/src/hooks/useStreamingLifecycle.ts +71 -0
- package/src/hooks/useStreamingResponder.ts +401 -0
- package/src/hooks/useSuggestionSetup.ts +23 -0
- package/src/hooks/useToolApproval.test.ts +140 -0
- package/src/hooks/useToolApproval.ts +264 -0
- package/src/hooks/useToolScheduler.ts +432 -0
- package/src/index.ts +3 -0
- package/src/jsx.d.ts +11 -0
- package/src/lib/clipboard.ts +18 -0
- package/src/lib/logger.ts +107 -0
- package/src/lib/random.ts +5 -0
- package/src/main.tsx +13 -0
- package/src/test/mockTheme.ts +51 -0
- package/src/types/events.ts +87 -0
- package/src/types.ts +13 -0
- package/src/ui/components/ChatLayout.tsx +694 -0
- package/src/ui/components/CommandComponents.tsx +74 -0
- package/src/ui/components/DiffViewer.tsx +306 -0
- package/src/ui/components/FilterInput.test.ts +69 -0
- package/src/ui/components/FilterInput.tsx +62 -0
- package/src/ui/components/HeaderBar.tsx +137 -0
- package/src/ui/components/RadioSelect.test.ts +140 -0
- package/src/ui/components/RadioSelect.tsx +88 -0
- package/src/ui/components/SelectableList.test.ts +83 -0
- package/src/ui/components/SelectableList.tsx +35 -0
- package/src/ui/components/StatusBar.tsx +45 -0
- package/src/ui/components/SuggestionPanel.tsx +102 -0
- package/src/ui/components/messages/ModelMessage.tsx +14 -0
- package/src/ui/components/messages/SystemMessage.tsx +29 -0
- package/src/ui/components/messages/ThinkingMessage.tsx +14 -0
- package/src/ui/components/messages/UserMessage.tsx +26 -0
- package/src/ui/components/messages/index.ts +15 -0
- package/src/ui/components/messages/renderMessage.test.ts +49 -0
- package/src/ui/components/messages/renderMessage.tsx +43 -0
- package/src/ui/components/messages/types.test.ts +24 -0
- package/src/ui/components/messages/types.ts +36 -0
- package/src/ui/modals/AuthModal.tsx +106 -0
- package/src/ui/modals/ModalShell.tsx +60 -0
- package/src/ui/modals/SearchSelectModal.tsx +236 -0
- package/src/ui/modals/ThemeModal.tsx +204 -0
- package/src/ui/modals/ToolApprovalModal.test.ts +206 -0
- package/src/ui/modals/ToolApprovalModal.tsx +282 -0
- package/src/ui/modals/index.ts +20 -0
- package/src/ui/modals/modals.test.ts +26 -0
- package/src/ui/modals/types.ts +19 -0
- package/src/uicontext/Command.tsx +102 -0
- package/src/uicontext/Dialog.tsx +65 -0
- package/src/uicontext/index.ts +2 -0
- package/themes/ansi-light.json +59 -0
- package/themes/ansi.json +59 -0
- package/themes/atom-one-dark.json +59 -0
- package/themes/ayu-light.json +59 -0
- package/themes/ayu.json +59 -0
- package/themes/default-light.json +59 -0
- package/themes/default.json +59 -0
- package/themes/dracula.json +59 -0
- package/themes/github-dark.json +59 -0
- package/themes/github-light.json +59 -0
- package/themes/googlecode.json +59 -0
- package/themes/green-screen.json +59 -0
- package/themes/no-color.json +59 -0
- package/themes/shades-of-purple.json +59 -0
- package/themes/xcode.json +59 -0
- package/tsconfig.json +28 -0
- package/vitest.config.ts +10 -0
package/PLAN-messages.md
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
# Message Rendering System Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This plan defines a clean, modular message component architecture for the nui TUI application. The goal is to replace the current `[user]`/`[responder]` text prefixes with proper visual decorations using opentui's box border system.
|
|
6
|
+
|
|
7
|
+
## Current State Analysis
|
|
8
|
+
|
|
9
|
+
### Existing Code Structure
|
|
10
|
+
|
|
11
|
+
**`src/ui/components/ChatLayout.tsx`**:
|
|
12
|
+
|
|
13
|
+
- `renderChatLine(line: ChatLine, theme: ThemeDefinition)` - renders messages with `[role]` prefix
|
|
14
|
+
- `renderToolBlock(block: ToolBlock, theme: ThemeDefinition)` - renders tool output in bordered boxes
|
|
15
|
+
- `roleColor(role: Role, theme: ThemeDefinition)` - maps roles to theme colors
|
|
16
|
+
|
|
17
|
+
**`src/hooks/useChatStore.ts`**:
|
|
18
|
+
|
|
19
|
+
- Defines `Role = "user" | "responder" | "thinking"`
|
|
20
|
+
- Defines `ChatLine` and `ToolBlock` interfaces
|
|
21
|
+
- Exports types used throughout the app
|
|
22
|
+
|
|
23
|
+
**`src/features/theme/theme.ts`**:
|
|
24
|
+
|
|
25
|
+
- `ThemeColors` interface defines all theme color properties
|
|
26
|
+
- Currently has `text.user`, `text.responder`, `text.thinking` for message colors
|
|
27
|
+
|
|
28
|
+
### Identified Issues
|
|
29
|
+
|
|
30
|
+
1. Messages use text prefixes (`[user]`, `[responder]`) that get copied during text selection
|
|
31
|
+
2. No visual distinction between message types beyond color
|
|
32
|
+
3. No "system" message type for notifications like "Loaded profile: chutes"
|
|
33
|
+
4. No spacing/grouping between messages from different roles
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Target Architecture
|
|
38
|
+
|
|
39
|
+
### Component Hierarchy
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/ui/components/messages/
|
|
43
|
+
index.ts # Public exports
|
|
44
|
+
types.ts # Message types and interfaces
|
|
45
|
+
MessageContainer.tsx # Groups consecutive messages from same role
|
|
46
|
+
BaseMessage.tsx # Shared message rendering logic (internal)
|
|
47
|
+
UserMessage.tsx # User input with left border
|
|
48
|
+
SystemMessage.tsx # System notifications with distinct border
|
|
49
|
+
ModelMessage.tsx # AI responses without decoration
|
|
50
|
+
ThinkingMessage.tsx # Thinking/reasoning with subtle styling
|
|
51
|
+
renderMessage.ts # Factory function for rendering any message type
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Data Model Changes
|
|
55
|
+
|
|
56
|
+
**Current Role type:**
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
type Role = 'user' | 'responder' | 'thinking';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**New MessageRole type:**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
type MessageRole = 'user' | 'model' | 'system' | 'thinking';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Note: `responder` is renamed to `model` for semantic clarity. A migration function will handle backward compatibility.
|
|
69
|
+
|
|
70
|
+
**New ChatLine interface:**
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface ChatLine {
|
|
74
|
+
id: string;
|
|
75
|
+
kind: 'line';
|
|
76
|
+
role: MessageRole;
|
|
77
|
+
text: string;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Theme Schema Additions
|
|
82
|
+
|
|
83
|
+
Add to `ThemeColors` interface in `src/features/theme/theme.ts`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface ThemeColors {
|
|
87
|
+
// ... existing properties ...
|
|
88
|
+
|
|
89
|
+
readonly message: {
|
|
90
|
+
readonly userBorder: string; // Left border for user messages
|
|
91
|
+
readonly systemBorder: string; // Left border for system messages
|
|
92
|
+
readonly systemText: string; // Text color for system messages
|
|
93
|
+
readonly groupSpacing: number; // Vertical gap between message groups (optional, default 1)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Component Interfaces
|
|
99
|
+
|
|
100
|
+
**MessageProps (base interface):**
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface MessageProps {
|
|
104
|
+
readonly id: string;
|
|
105
|
+
readonly text: string;
|
|
106
|
+
readonly theme: ThemeDefinition;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**UserMessageProps:**
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
interface UserMessageProps extends MessageProps {
|
|
114
|
+
// No additional props needed
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**SystemMessageProps:**
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface SystemMessageProps extends MessageProps {
|
|
122
|
+
// No additional props needed
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**ModelMessageProps:**
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface ModelMessageProps extends MessageProps {
|
|
130
|
+
// No additional props needed
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**ThinkingMessageProps:**
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
interface ThinkingMessageProps extends MessageProps {
|
|
138
|
+
// No additional props needed
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Visual Design
|
|
143
|
+
|
|
144
|
+
**UserMessage:**
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<box
|
|
148
|
+
border={['left']}
|
|
149
|
+
borderColor={theme.colors.message.userBorder}
|
|
150
|
+
customBorderChars={{
|
|
151
|
+
...EmptyBorder,
|
|
152
|
+
vertical: '┃',
|
|
153
|
+
bottomLeft: '╹',
|
|
154
|
+
topLeft: '╻',
|
|
155
|
+
}}
|
|
156
|
+
style={{ paddingLeft: 1 }}
|
|
157
|
+
>
|
|
158
|
+
<text fg={theme.colors.text.user}>{text}</text>
|
|
159
|
+
</box>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**SystemMessage:**
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<box
|
|
166
|
+
border={['left']}
|
|
167
|
+
borderColor={theme.colors.message.systemBorder}
|
|
168
|
+
customBorderChars={{
|
|
169
|
+
...EmptyBorder,
|
|
170
|
+
vertical: '│',
|
|
171
|
+
bottomLeft: '╵',
|
|
172
|
+
topLeft: '╷',
|
|
173
|
+
}}
|
|
174
|
+
style={{ paddingLeft: 1 }}
|
|
175
|
+
>
|
|
176
|
+
<text fg={theme.colors.message.systemText}>{text}</text>
|
|
177
|
+
</box>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**ModelMessage:**
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<text fg={theme.colors.text.responder}>{text}</text>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**ThinkingMessage:**
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<text fg={theme.colors.text.thinking} style={{ fontStyle: 'italic' }}>
|
|
190
|
+
{text}
|
|
191
|
+
</text>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Empty Border Constant
|
|
195
|
+
|
|
196
|
+
Define a constant for custom border characters:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
export const EmptyBorder = {
|
|
200
|
+
top: ' ',
|
|
201
|
+
bottom: ' ',
|
|
202
|
+
left: ' ',
|
|
203
|
+
right: ' ',
|
|
204
|
+
topLeft: ' ',
|
|
205
|
+
topRight: ' ',
|
|
206
|
+
bottomLeft: ' ',
|
|
207
|
+
bottomRight: ' ',
|
|
208
|
+
horizontal: ' ',
|
|
209
|
+
vertical: ' ',
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Test Specification (Test-First Approach)
|
|
216
|
+
|
|
217
|
+
### 1. Theme Tests (`src/features/theme/theme.test.ts`)
|
|
218
|
+
|
|
219
|
+
**Create new file** with the following tests:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
describe('ThemeColors message properties', () => {
|
|
223
|
+
it('should require message.userBorder property', () => {
|
|
224
|
+
// Verify type requires the property
|
|
225
|
+
// Verify existing themes fail validation without it
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should require message.systemBorder property', () => {
|
|
229
|
+
// Verify type requires the property
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should require message.systemText property', () => {
|
|
233
|
+
// Verify type requires the property
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should accept optional message.groupSpacing property', () => {
|
|
237
|
+
// Verify property is optional
|
|
238
|
+
// Verify defaults to expected value when not present
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('loadThemes', () => {
|
|
243
|
+
it('should load themes with message color properties', () => {
|
|
244
|
+
// Verify loaded themes have message properties
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 2. Message Type Tests (`src/ui/components/messages/types.test.ts`)
|
|
250
|
+
|
|
251
|
+
**Create new file:**
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
describe('MessageRole type', () => {
|
|
255
|
+
it('should include user role', () => {
|
|
256
|
+
const role: MessageRole = 'user';
|
|
257
|
+
expect(role).toBe('user');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should include model role', () => {
|
|
261
|
+
const role: MessageRole = 'model';
|
|
262
|
+
expect(role).toBe('model');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should include system role', () => {
|
|
266
|
+
const role: MessageRole = 'system';
|
|
267
|
+
expect(role).toBe('system');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should include thinking role', () => {
|
|
271
|
+
const role: MessageRole = 'thinking';
|
|
272
|
+
expect(role).toBe('thinking');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('migrateRole', () => {
|
|
277
|
+
it('should convert responder to model', () => {
|
|
278
|
+
expect(migrateRole('responder')).toBe('model');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should pass through user unchanged', () => {
|
|
282
|
+
expect(migrateRole('user')).toBe('user');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should pass through thinking unchanged', () => {
|
|
286
|
+
expect(migrateRole('thinking')).toBe('thinking');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should pass through system unchanged', () => {
|
|
290
|
+
expect(migrateRole('system')).toBe('system');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should pass through model unchanged', () => {
|
|
294
|
+
expect(migrateRole('model')).toBe('model');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 3. Message Rendering Tests (`src/ui/components/messages/renderMessage.test.ts`)
|
|
300
|
+
|
|
301
|
+
**Create new file:**
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { describe, it, expect } from 'vitest';
|
|
305
|
+
import { getMessageRenderer, MessageRole } from './types';
|
|
306
|
+
|
|
307
|
+
describe('getMessageRenderer', () => {
|
|
308
|
+
const mockTheme = createMockTheme(); // Helper function
|
|
309
|
+
|
|
310
|
+
it('should return UserMessage renderer for user role', () => {
|
|
311
|
+
const renderer = getMessageRenderer('user');
|
|
312
|
+
expect(renderer.displayName || renderer.name).toBe('UserMessage');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should return ModelMessage renderer for model role', () => {
|
|
316
|
+
const renderer = getMessageRenderer('model');
|
|
317
|
+
expect(renderer.displayName || renderer.name).toBe('ModelMessage');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should return SystemMessage renderer for system role', () => {
|
|
321
|
+
const renderer = getMessageRenderer('system');
|
|
322
|
+
expect(renderer.displayName || renderer.name).toBe('SystemMessage');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should return ThinkingMessage renderer for thinking role', () => {
|
|
326
|
+
const renderer = getMessageRenderer('thinking');
|
|
327
|
+
expect(renderer.displayName || renderer.name).toBe('ThinkingMessage');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('roleColor', () => {
|
|
332
|
+
const mockTheme = createMockTheme();
|
|
333
|
+
|
|
334
|
+
it('should return user text color for user role', () => {
|
|
335
|
+
expect(roleColor('user', mockTheme)).toBe(mockTheme.colors.text.user);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should return responder text color for model role', () => {
|
|
339
|
+
expect(roleColor('model', mockTheme)).toBe(mockTheme.colors.text.responder);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should return systemText color for system role', () => {
|
|
343
|
+
expect(roleColor('system', mockTheme)).toBe(
|
|
344
|
+
mockTheme.colors.message.systemText,
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return thinking text color for thinking role', () => {
|
|
349
|
+
expect(roleColor('thinking', mockTheme)).toBe(
|
|
350
|
+
mockTheme.colors.text.thinking,
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### 4. Integration Tests (`src/ui/components/messages/integration.test.ts`)
|
|
357
|
+
|
|
358
|
+
**Create new file:**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
describe('Message rendering integration', () => {
|
|
362
|
+
it('should render user message with border decoration', () => {
|
|
363
|
+
// Test that UserMessage produces a box with left border
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should render system message with distinct border style', () => {
|
|
367
|
+
// Test that SystemMessage produces a box with left border
|
|
368
|
+
// Verify border style differs from user message
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should render model message without border decoration', () => {
|
|
372
|
+
// Test that ModelMessage produces plain text without box wrapper
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should use theme colors for all message types', () => {
|
|
376
|
+
// Verify each message type uses appropriate theme color
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 5. ChatStore Migration Tests (`src/hooks/useChatStore.test.ts`)
|
|
382
|
+
|
|
383
|
+
**Create new file:**
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
describe('useChatStore role migration', () => {
|
|
387
|
+
it('should accept system role in appendLines', () => {
|
|
388
|
+
// Verify appendLines works with system role
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should accept model role in appendLines', () => {
|
|
392
|
+
// Verify appendLines works with model role
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should store lines with correct role', () => {
|
|
396
|
+
// Verify stored lines have expected role
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Test Helper: Mock Theme
|
|
402
|
+
|
|
403
|
+
Create a shared test utility:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// src/test/mockTheme.ts
|
|
407
|
+
export function createMockTheme(): ThemeDefinition {
|
|
408
|
+
return {
|
|
409
|
+
slug: 'test',
|
|
410
|
+
name: 'Test Theme',
|
|
411
|
+
kind: 'dark',
|
|
412
|
+
colors: {
|
|
413
|
+
background: '#000000',
|
|
414
|
+
panel: {
|
|
415
|
+
bg: '#111111',
|
|
416
|
+
border: '#333333',
|
|
417
|
+
},
|
|
418
|
+
text: {
|
|
419
|
+
primary: '#ffffff',
|
|
420
|
+
muted: '#888888',
|
|
421
|
+
user: '#00ff00',
|
|
422
|
+
responder: '#0088ff',
|
|
423
|
+
thinking: '#ff8800',
|
|
424
|
+
tool: '#ff00ff',
|
|
425
|
+
},
|
|
426
|
+
input: {
|
|
427
|
+
fg: '#ffffff',
|
|
428
|
+
bg: '#000000',
|
|
429
|
+
border: '#333333',
|
|
430
|
+
placeholder: '#666666',
|
|
431
|
+
},
|
|
432
|
+
status: {
|
|
433
|
+
fg: '#ffffff',
|
|
434
|
+
},
|
|
435
|
+
accent: {
|
|
436
|
+
primary: '#00ffff',
|
|
437
|
+
},
|
|
438
|
+
diff: {
|
|
439
|
+
addedBg: '#003300',
|
|
440
|
+
addedFg: '#00ff00',
|
|
441
|
+
removedBg: '#330000',
|
|
442
|
+
removedFg: '#ff0000',
|
|
443
|
+
},
|
|
444
|
+
selection: {
|
|
445
|
+
fg: '#000000',
|
|
446
|
+
bg: '#ffffff',
|
|
447
|
+
},
|
|
448
|
+
message: {
|
|
449
|
+
userBorder: '#00ff00',
|
|
450
|
+
systemBorder: '#ffff00',
|
|
451
|
+
systemText: '#ffff00',
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Implementation Order
|
|
461
|
+
|
|
462
|
+
### Phase 1: Foundation (No Breaking Changes)
|
|
463
|
+
|
|
464
|
+
1. **Add test helper file**: `src/test/mockTheme.ts`
|
|
465
|
+
2. **Write theme tests**: `src/features/theme/theme.test.ts`
|
|
466
|
+
3. **Update ThemeColors interface**: Add `message` property group
|
|
467
|
+
4. **Update all theme JSON files**: Add `message` colors to each theme
|
|
468
|
+
|
|
469
|
+
### Phase 2: Message Types Module
|
|
470
|
+
|
|
471
|
+
5. **Write types tests**: `src/ui/components/messages/types.test.ts`
|
|
472
|
+
6. **Create types module**: `src/ui/components/messages/types.ts`
|
|
473
|
+
- Define `MessageRole` type
|
|
474
|
+
- Define `migrateRole()` function
|
|
475
|
+
- Define component interfaces
|
|
476
|
+
|
|
477
|
+
### Phase 3: Message Components
|
|
478
|
+
|
|
479
|
+
7. **Write renderMessage tests**: `src/ui/components/messages/renderMessage.test.ts`
|
|
480
|
+
8. **Create EmptyBorder constant**: In types.ts or separate constants file
|
|
481
|
+
9. **Create BaseMessage**: Internal shared logic
|
|
482
|
+
10. **Create UserMessage**: User input with left border
|
|
483
|
+
11. **Create SystemMessage**: System notifications with distinct border
|
|
484
|
+
12. **Create ModelMessage**: Plain text for AI responses
|
|
485
|
+
13. **Create ThinkingMessage**: Subtle styling for reasoning
|
|
486
|
+
14. **Create renderMessage factory**: Function to select appropriate component
|
|
487
|
+
15. **Create index.ts**: Public exports
|
|
488
|
+
|
|
489
|
+
### Phase 4: Integration
|
|
490
|
+
|
|
491
|
+
16. **Write useChatStore tests**: `src/hooks/useChatStore.test.ts`
|
|
492
|
+
17. **Update useChatStore**: Accept new roles, use `migrateRole()` for backward compatibility
|
|
493
|
+
18. **Update ChatLayout**: Replace `renderChatLine` with new message components
|
|
494
|
+
19. **Remove old code**: Delete `[role]` prefix rendering
|
|
495
|
+
20. **Write integration tests**: `src/ui/components/messages/integration.test.ts`
|
|
496
|
+
|
|
497
|
+
### Phase 5: Polish
|
|
498
|
+
|
|
499
|
+
21. **Add message grouping**: Optional spacing between role changes
|
|
500
|
+
22. **Verify all themes**: Ensure all theme files have valid message colors
|
|
501
|
+
23. **Manual testing**: Visual verification in terminal
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## File Changes Summary
|
|
506
|
+
|
|
507
|
+
### New Files
|
|
508
|
+
|
|
509
|
+
| File | Purpose |
|
|
510
|
+
| -------------------------------------------------- | ------------------------------- |
|
|
511
|
+
| `src/test/mockTheme.ts` | Shared mock theme for tests |
|
|
512
|
+
| `src/features/theme/theme.test.ts` | Theme validation tests |
|
|
513
|
+
| `src/ui/components/messages/index.ts` | Public exports |
|
|
514
|
+
| `src/ui/components/messages/types.ts` | Types and interfaces |
|
|
515
|
+
| `src/ui/components/messages/types.test.ts` | Type tests |
|
|
516
|
+
| `src/ui/components/messages/constants.ts` | EmptyBorder and other constants |
|
|
517
|
+
| `src/ui/components/messages/BaseMessage.tsx` | Shared message logic |
|
|
518
|
+
| `src/ui/components/messages/UserMessage.tsx` | User message component |
|
|
519
|
+
| `src/ui/components/messages/SystemMessage.tsx` | System message component |
|
|
520
|
+
| `src/ui/components/messages/ModelMessage.tsx` | Model message component |
|
|
521
|
+
| `src/ui/components/messages/ThinkingMessage.tsx` | Thinking message component |
|
|
522
|
+
| `src/ui/components/messages/renderMessage.ts` | Factory function |
|
|
523
|
+
| `src/ui/components/messages/renderMessage.test.ts` | Factory tests |
|
|
524
|
+
| `src/ui/components/messages/integration.test.ts` | Integration tests |
|
|
525
|
+
| `src/hooks/useChatStore.test.ts` | Chat store tests |
|
|
526
|
+
|
|
527
|
+
### Modified Files
|
|
528
|
+
|
|
529
|
+
| File | Changes |
|
|
530
|
+
| ---------------------------------- | ----------------------------------------- |
|
|
531
|
+
| `src/features/theme/theme.ts` | Add `message` to ThemeColors interface |
|
|
532
|
+
| `themes/*.json` (15 files) | Add `message` color properties |
|
|
533
|
+
| `src/hooks/useChatStore.ts` | Update Role type, add system role support |
|
|
534
|
+
| `src/ui/components/ChatLayout.tsx` | Replace renderChatLine, remove prefix |
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Theme Color Recommendations
|
|
539
|
+
|
|
540
|
+
For each theme, add `message` properties. Example for dracula theme:
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"message": {
|
|
545
|
+
"userBorder": "#8be9fd",
|
|
546
|
+
"systemBorder": "#f1fa8c",
|
|
547
|
+
"systemText": "#f1fa8c"
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**Color Guidelines:**
|
|
553
|
+
|
|
554
|
+
- `userBorder`: Match or complement `text.user` color
|
|
555
|
+
- `systemBorder`: Use `accent.warning` or a distinct notification color
|
|
556
|
+
- `systemText`: Same as `systemBorder` for consistency
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Risks and Edge Cases
|
|
561
|
+
|
|
562
|
+
### Risk 1: OpenTUI Border Support
|
|
563
|
+
|
|
564
|
+
**Risk:** The `border={["left"]}` syntax and `customBorderChars` may behave differently than expected in opentui/react vs opentui/solid.
|
|
565
|
+
|
|
566
|
+
**Mitigation:**
|
|
567
|
+
|
|
568
|
+
- Test border rendering early in Phase 3
|
|
569
|
+
- Have fallback to simpler border style if custom chars don't work
|
|
570
|
+
- Consider using standard `borderStyle: "single"` with only left border if needed
|
|
571
|
+
|
|
572
|
+
### Risk 2: Backward Compatibility
|
|
573
|
+
|
|
574
|
+
**Risk:** Existing code uses `"responder"` role which needs migration.
|
|
575
|
+
|
|
576
|
+
**Mitigation:**
|
|
577
|
+
|
|
578
|
+
- `migrateRole()` function handles conversion transparently
|
|
579
|
+
- Keep `"responder"` as valid input that gets converted to `"model"`
|
|
580
|
+
- Run migration at data boundary (useChatStore)
|
|
581
|
+
|
|
582
|
+
### Risk 3: Theme File Updates
|
|
583
|
+
|
|
584
|
+
**Risk:** Missing `message` properties in theme files could cause runtime errors.
|
|
585
|
+
|
|
586
|
+
**Mitigation:**
|
|
587
|
+
|
|
588
|
+
- Add optional defaults in theme loading code
|
|
589
|
+
- Validate all theme files in CI
|
|
590
|
+
- Provide clear error messages for missing properties
|
|
591
|
+
|
|
592
|
+
### Risk 4: Text Selection
|
|
593
|
+
|
|
594
|
+
**Risk:** Box borders might still interfere with text selection in some terminals.
|
|
595
|
+
|
|
596
|
+
**Mitigation:**
|
|
597
|
+
|
|
598
|
+
- Test in multiple terminals (iTerm2, Terminal.app, Windows Terminal)
|
|
599
|
+
- Document known limitations
|
|
600
|
+
- Border characters are separate from content, should not be selected
|
|
601
|
+
|
|
602
|
+
### Edge Case: Empty Messages
|
|
603
|
+
|
|
604
|
+
Handle messages with empty or whitespace-only text:
|
|
605
|
+
|
|
606
|
+
- Render minimal height (1 line)
|
|
607
|
+
- Still show border decoration for user/system
|
|
608
|
+
- Don't collapse message entirely
|
|
609
|
+
|
|
610
|
+
### Edge Case: Multi-line Messages
|
|
611
|
+
|
|
612
|
+
Messages can span multiple lines. Ensure:
|
|
613
|
+
|
|
614
|
+
- Border extends full height of message
|
|
615
|
+
- Text wraps properly within border
|
|
616
|
+
- No visual artifacts at line breaks
|
|
617
|
+
|
|
618
|
+
### Edge Case: Very Long Messages
|
|
619
|
+
|
|
620
|
+
For messages exceeding viewport:
|
|
621
|
+
|
|
622
|
+
- Let scrollbox handle scrolling (already implemented)
|
|
623
|
+
- Border should extend through all lines
|
|
624
|
+
- Consider lazy rendering for performance (future optimization)
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Success Criteria
|
|
629
|
+
|
|
630
|
+
1. All tests pass (existing + new)
|
|
631
|
+
2. User messages have visible left border that doesn't appear in clipboard on select
|
|
632
|
+
3. System messages have distinct visual style from user messages
|
|
633
|
+
4. Model messages render as plain text without decoration
|
|
634
|
+
5. No `[user]` or `[responder]` text prefixes visible
|
|
635
|
+
6. All 15 theme files include valid message colors
|
|
636
|
+
7. Backward compatibility: existing `"responder"` role still works
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Example Usage After Implementation
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// In ChatLayout or similar
|
|
644
|
+
import { renderMessage, migrateRole } from "./messages";
|
|
645
|
+
|
|
646
|
+
function ScrollbackView(props: ScrollbackProps): JSX.Element {
|
|
647
|
+
return (
|
|
648
|
+
<scrollbox ...>
|
|
649
|
+
<box flexDirection="column" style={{ gap: 0, width: "100%" }}>
|
|
650
|
+
{props.lines.map((entry) =>
|
|
651
|
+
entry.kind === "line"
|
|
652
|
+
? renderMessage(migrateRole(entry.role), entry.id, entry.text, props.theme)
|
|
653
|
+
: renderToolBlock(entry, props.theme)
|
|
654
|
+
)}
|
|
655
|
+
</box>
|
|
656
|
+
</scrollbox>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Appendix: Theme File Locations
|
|
664
|
+
|
|
665
|
+
All theme files to update:
|
|
666
|
+
|
|
667
|
+
1. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/ansi-light.json`
|
|
668
|
+
2. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/ansi.json`
|
|
669
|
+
3. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/atom-one-dark.json`
|
|
670
|
+
4. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/ayu-light.json`
|
|
671
|
+
5. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/ayu.json`
|
|
672
|
+
6. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/default-light.json`
|
|
673
|
+
7. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/default.json`
|
|
674
|
+
8. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/dracula.json`
|
|
675
|
+
9. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/github-dark.json`
|
|
676
|
+
10. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/github-light.json`
|
|
677
|
+
11. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/googlecode.json`
|
|
678
|
+
12. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/green-screen.json`
|
|
679
|
+
13. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/no-color.json`
|
|
680
|
+
14. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/shades-of-purple.json`
|
|
681
|
+
15. `/Users/acoliver/projects/llxprt-code-branches/newui/nui/themes/xcode.json`
|