@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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# nui Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
nui is an alternative terminal UI for llxprt-code, built on opentui instead of Ink. It provides a cleaner separation between UI rendering and backend logic.
|
|
6
|
+
|
|
7
|
+
## Design Principles
|
|
8
|
+
|
|
9
|
+
1. **UI is Dumb**: The UI layer only renders what it's told. No business logic.
|
|
10
|
+
2. **Adapter Owns Logic**: The adapter layer interprets events and manages state.
|
|
11
|
+
3. **Delegate to Core**: Reuse llxprt-code-core for history, streaming, tools.
|
|
12
|
+
4. **Stream Everything**: Real-time rendering as data arrives.
|
|
13
|
+
|
|
14
|
+
## Layer Responsibilities
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────────────┐
|
|
18
|
+
│ UI Layer │
|
|
19
|
+
│ - Renders text, markdown, tool status │
|
|
20
|
+
│ - Handles user input │
|
|
21
|
+
│ - Shows approval prompts │
|
|
22
|
+
│ - NO business logic │
|
|
23
|
+
├─────────────────────────────────────────────────────────┤
|
|
24
|
+
│ Adapter Layer │
|
|
25
|
+
│ - Transforms GeminiClient events → UI events │
|
|
26
|
+
│ - Manages session lifecycle │
|
|
27
|
+
│ - Bridges approval flow │
|
|
28
|
+
│ - Owns Config stub │
|
|
29
|
+
├─────────────────────────────────────────────────────────┤
|
|
30
|
+
│ llxprt-code-core │
|
|
31
|
+
│ - GeminiClient (streaming, history, tools) │
|
|
32
|
+
│ - Providers (OpenAI, Anthropic, Gemini) │
|
|
33
|
+
│ - HistoryService, SettingsService │
|
|
34
|
+
│ - All the battle-tested logic │
|
|
35
|
+
└─────────────────────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Event Flow
|
|
39
|
+
|
|
40
|
+
### User Message Flow
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
User Input
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
┌─────────────────┐
|
|
47
|
+
│ UI Layer │ captures text, sends to adapter
|
|
48
|
+
└────────┬────────┘
|
|
49
|
+
│
|
|
50
|
+
▼
|
|
51
|
+
┌─────────────────┐
|
|
52
|
+
│ Adapter Layer │ calls GeminiClient.sendMessageStream()
|
|
53
|
+
└────────┬────────┘
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
┌─────────────────┐
|
|
57
|
+
│ GeminiClient │ manages history, calls provider
|
|
58
|
+
└────────┬────────┘
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
┌─────────────────┐
|
|
62
|
+
│ Provider │ streams from API
|
|
63
|
+
└────────┬────────┘
|
|
64
|
+
│
|
|
65
|
+
Stream Events
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
┌─────────────────┐
|
|
69
|
+
│ Adapter Layer │ transforms events
|
|
70
|
+
└────────┬────────┘
|
|
71
|
+
│
|
|
72
|
+
AdapterEvents
|
|
73
|
+
│
|
|
74
|
+
▼
|
|
75
|
+
┌─────────────────┐
|
|
76
|
+
│ UI Layer │ renders incrementally
|
|
77
|
+
└─────────────────┘
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Event Types
|
|
81
|
+
|
|
82
|
+
**From GeminiClient (ServerGeminiStreamEvent)**:
|
|
83
|
+
|
|
84
|
+
- `Content` - text chunk from model
|
|
85
|
+
- `Thought` - thinking/reasoning content
|
|
86
|
+
- `ToolCallRequest` - model wants to call a tool
|
|
87
|
+
- `ToolCallConfirmation` - tool needs approval
|
|
88
|
+
- `ToolCallResponse` - tool execution result
|
|
89
|
+
- `Finished` - stream complete
|
|
90
|
+
- `Error` - something went wrong
|
|
91
|
+
|
|
92
|
+
**To UI (AdapterEvent)**:
|
|
93
|
+
|
|
94
|
+
- `text_delta` - append text to current message
|
|
95
|
+
- `thinking_delta` - append to thinking section
|
|
96
|
+
- `tool_pending` - show tool as waiting
|
|
97
|
+
- `tool_approval_needed` - prompt user for approval
|
|
98
|
+
- `tool_executing` - show spinner
|
|
99
|
+
- `tool_complete` - show result
|
|
100
|
+
- `complete` - message done
|
|
101
|
+
- `error` - show error
|
|
102
|
+
|
|
103
|
+
## Config Stub Strategy
|
|
104
|
+
|
|
105
|
+
GeminiClient requires a Config object. Rather than importing the full 1700-line Config class, we create a minimal stub that satisfies GeminiClient's actual needs:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const configStub = {
|
|
109
|
+
getSessionId: () => sessionId,
|
|
110
|
+
getModel: () => model,
|
|
111
|
+
getProvider: () => provider,
|
|
112
|
+
getSettingsService: () => settingsService,
|
|
113
|
+
getContentGeneratorConfig: () => ({ ... }),
|
|
114
|
+
getToolRegistry: () => undefined, // No tools initially
|
|
115
|
+
getEmbeddingModel: () => undefined,
|
|
116
|
+
getComplexityAnalyzerSettings: () => ({ ... }),
|
|
117
|
+
// ... other required methods
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use a Proxy wrapper to log any missing methods during development.
|
|
122
|
+
|
|
123
|
+
## Tool Execution (Future)
|
|
124
|
+
|
|
125
|
+
When tool execution is implemented:
|
|
126
|
+
|
|
127
|
+
1. GeminiClient yields `ToolCallRequest` event
|
|
128
|
+
2. Adapter checks approval mode:
|
|
129
|
+
- Auto-approve: Execute immediately
|
|
130
|
+
- Require approval: Yield `tool_approval_needed` to UI
|
|
131
|
+
3. UI shows approval prompt
|
|
132
|
+
4. User approves/rejects via callback
|
|
133
|
+
5. Adapter tells GeminiClient to proceed or abort
|
|
134
|
+
6. GeminiClient handles tool execution internally
|
|
135
|
+
7. Results flow back as events
|
|
136
|
+
|
|
137
|
+
## Session Lifecycle
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
┌──────────────┐
|
|
141
|
+
│ Create │ createSession(config)
|
|
142
|
+
└──────┬───────┘
|
|
143
|
+
│
|
|
144
|
+
▼
|
|
145
|
+
┌──────────────┐
|
|
146
|
+
│ Initialize │ session.initialize()
|
|
147
|
+
└──────┬───────┘
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
┌──────────────┐
|
|
151
|
+
│ Active │ session.sendMessage() (repeatable)
|
|
152
|
+
└──────┬───────┘
|
|
153
|
+
│
|
|
154
|
+
▼
|
|
155
|
+
┌──────────────┐
|
|
156
|
+
│ Dispose │ session.dispose()
|
|
157
|
+
└──────────────┘
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Streaming Markdown
|
|
161
|
+
|
|
162
|
+
The UI must render markdown incrementally as chunks arrive:
|
|
163
|
+
|
|
164
|
+
1. **Accumulate**: Buffer all text received so far
|
|
165
|
+
2. **Parse**: Re-parse the full buffer on each chunk
|
|
166
|
+
3. **Render**: Show parsed result up to current position
|
|
167
|
+
4. **Grow**: Incomplete constructs (code blocks) render as if complete
|
|
168
|
+
|
|
169
|
+
This ensures correct formatting even when chunks split markdown syntax.
|
|
170
|
+
|
|
171
|
+
## Future: Integration with llxprt-code
|
|
172
|
+
|
|
173
|
+
nui is designed to eventually move into `llxprt-code/packages/nui`:
|
|
174
|
+
|
|
175
|
+
- Entry point: `llxprt --ui nui` or separate binary
|
|
176
|
+
- Full access to real Config, arg parsing, profiles
|
|
177
|
+
- No more Config stub needed
|
|
178
|
+
- Tool execution fully integrated
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Code Organization for nui
|
|
2
|
+
|
|
3
|
+
## Directory Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
nui/
|
|
7
|
+
├── dev-docs/ # Documentation for LLMs and developers
|
|
8
|
+
│ ├── ARCHITECTURE.md # System design and principles
|
|
9
|
+
│ ├── CODE_ORGANIZATION.md # This file
|
|
10
|
+
│ └── STANDARDS.md # Coding standards and TDD rules
|
|
11
|
+
│
|
|
12
|
+
├── src/
|
|
13
|
+
│ ├── features/ # Feature modules (domain-organized)
|
|
14
|
+
│ │ ├── chat/ # Chat UI components
|
|
15
|
+
│ │ │ ├── ChatLayout.ts
|
|
16
|
+
│ │ │ ├── ChatLayout.test.ts
|
|
17
|
+
│ │ │ └── index.ts
|
|
18
|
+
│ │ │
|
|
19
|
+
│ │ ├── config/ # Configuration and adapter
|
|
20
|
+
│ │ │ ├── llxprtAdapter.ts
|
|
21
|
+
│ │ │ ├── llxprtAdapter.test.ts
|
|
22
|
+
│ │ │ └── index.ts
|
|
23
|
+
│ │ │
|
|
24
|
+
│ │ ├── markdown/ # Markdown rendering
|
|
25
|
+
│ │ │ ├── StreamingMarkdown.ts
|
|
26
|
+
│ │ │ ├── StreamingMarkdown.test.ts
|
|
27
|
+
│ │ │ └── index.ts
|
|
28
|
+
│ │ │
|
|
29
|
+
│ │ └── tools/ # Tool display and approval
|
|
30
|
+
│ │ ├── ToolDisplay.ts
|
|
31
|
+
│ │ ├── ToolApproval.ts
|
|
32
|
+
│ │ └── index.ts
|
|
33
|
+
│ │
|
|
34
|
+
│ ├── lib/ # Shared utilities
|
|
35
|
+
│ │ ├── logger.ts # Logging (use this, not console)
|
|
36
|
+
│ │ ├── logger.test.ts
|
|
37
|
+
│ │ └── index.ts
|
|
38
|
+
│ │
|
|
39
|
+
│ ├── types/ # Shared type definitions
|
|
40
|
+
│ │ ├── events.ts # AdapterEvent types
|
|
41
|
+
│ │ └── index.ts
|
|
42
|
+
│ │
|
|
43
|
+
│ ├── renderer.ts # opentui setup
|
|
44
|
+
│ └── index.ts # Main entry point
|
|
45
|
+
│
|
|
46
|
+
├── themes/ # UI themes
|
|
47
|
+
│ └── default.json
|
|
48
|
+
│
|
|
49
|
+
├── package.json
|
|
50
|
+
├── tsconfig.json
|
|
51
|
+
├── vitest.config.ts
|
|
52
|
+
└── eslint.config.cjs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Feature Module Pattern
|
|
56
|
+
|
|
57
|
+
Each feature is a self-contained module:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
features/
|
|
61
|
+
feature-name/
|
|
62
|
+
├── index.ts # Public exports only
|
|
63
|
+
├── FeatureName.ts # Main implementation
|
|
64
|
+
├── FeatureName.test.ts # Tests (colocated)
|
|
65
|
+
├── types.ts # Feature-specific types (if needed)
|
|
66
|
+
└── helpers.ts # Internal helpers (if needed)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Export Rules
|
|
70
|
+
|
|
71
|
+
- `index.ts` exports ONLY the public API
|
|
72
|
+
- Internal helpers are NOT exported
|
|
73
|
+
- Tests import from the implementation file, not index
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// index.ts - public API only
|
|
77
|
+
export { ChatLayout } from './ChatLayout';
|
|
78
|
+
export type { ChatLayoutProps } from './ChatLayout';
|
|
79
|
+
|
|
80
|
+
// ChatLayout.test.ts - imports implementation directly
|
|
81
|
+
import { ChatLayout, internalHelper } from './ChatLayout';
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Naming Conventions
|
|
85
|
+
|
|
86
|
+
### Files
|
|
87
|
+
|
|
88
|
+
| Type | Convention | Example |
|
|
89
|
+
| ----------------- | ------------------------ | -------------------- |
|
|
90
|
+
| Feature component | PascalCase | `ChatLayout.ts` |
|
|
91
|
+
| Utility | camelCase | `logger.ts` |
|
|
92
|
+
| Types | camelCase | `events.ts` |
|
|
93
|
+
| Tests | Same as source + `.test` | `ChatLayout.test.ts` |
|
|
94
|
+
| Config | camelCase | `vitest.config.ts` |
|
|
95
|
+
|
|
96
|
+
### Code
|
|
97
|
+
|
|
98
|
+
| Type | Convention | Example |
|
|
99
|
+
| ---------- | ------------------------ | --------------------------- |
|
|
100
|
+
| Classes | PascalCase | `class StreamingMarkdown` |
|
|
101
|
+
| Interfaces | PascalCase (no I prefix) | `interface AdapterEvent` |
|
|
102
|
+
| Types | PascalCase | `type EventHandler` |
|
|
103
|
+
| Functions | camelCase | `function transformEvent()` |
|
|
104
|
+
| Variables | camelCase | `const eventBuffer` |
|
|
105
|
+
| Constants | UPPER_SNAKE_CASE | `const MAX_BUFFER_SIZE` |
|
|
106
|
+
|
|
107
|
+
## Import Order
|
|
108
|
+
|
|
109
|
+
Imports must be ordered:
|
|
110
|
+
|
|
111
|
+
1. Node.js built-ins
|
|
112
|
+
2. External packages
|
|
113
|
+
3. Internal absolute imports (if using aliases)
|
|
114
|
+
4. Relative imports
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// 1. Node built-ins
|
|
118
|
+
import { readFileSync } from 'node:fs';
|
|
119
|
+
import { join } from 'node:path';
|
|
120
|
+
|
|
121
|
+
// 2. External packages
|
|
122
|
+
import { GeminiClient } from '@vybestack/llxprt-code-core';
|
|
123
|
+
|
|
124
|
+
// 3. Internal (relative)
|
|
125
|
+
import { getLogger } from '../../lib/logger';
|
|
126
|
+
import { transformEvent } from './helpers';
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Test Organization
|
|
130
|
+
|
|
131
|
+
### Colocation
|
|
132
|
+
|
|
133
|
+
Tests live next to the code they test:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
ChatLayout.ts
|
|
137
|
+
ChatLayout.test.ts # Right next to it
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Test File Structure
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
144
|
+
import { functionUnderTest } from './module';
|
|
145
|
+
|
|
146
|
+
describe('functionUnderTest', () => {
|
|
147
|
+
describe('when given valid input', () => {
|
|
148
|
+
it('should return expected result', () => {
|
|
149
|
+
// Arrange
|
|
150
|
+
const input = createTestInput();
|
|
151
|
+
|
|
152
|
+
// Act
|
|
153
|
+
const result = functionUnderTest(input);
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
156
|
+
expect(result).toEqual(expectedOutput);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('when given invalid input', () => {
|
|
161
|
+
it('should return error result', () => {
|
|
162
|
+
// ...
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Test Naming
|
|
169
|
+
|
|
170
|
+
- Describe blocks: function/class name
|
|
171
|
+
- Nested describes: conditions ("when...", "with...")
|
|
172
|
+
- It blocks: behavior in plain English ("should...")
|
|
173
|
+
|
|
174
|
+
## Types Location
|
|
175
|
+
|
|
176
|
+
### Shared Types
|
|
177
|
+
|
|
178
|
+
Types used across multiple features go in `src/types/`:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// src/types/events.ts
|
|
182
|
+
export type AdapterEvent =
|
|
183
|
+
| { type: 'text_delta'; text: string }
|
|
184
|
+
| { type: 'thinking_delta'; text: string }
|
|
185
|
+
| { type: 'tool_pending'; id: string; name: string }
|
|
186
|
+
| { type: 'complete' };
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Feature-Specific Types
|
|
190
|
+
|
|
191
|
+
Types used only within a feature stay in that feature:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// src/features/chat/types.ts
|
|
195
|
+
export interface ChatLayoutProps {
|
|
196
|
+
onSubmit: (text: string) => void;
|
|
197
|
+
events: AdapterEvent[];
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Adding a New Feature
|
|
202
|
+
|
|
203
|
+
1. Create directory: `src/features/feature-name/`
|
|
204
|
+
2. Write failing test: `FeatureName.test.ts`
|
|
205
|
+
3. Create implementation: `FeatureName.ts`
|
|
206
|
+
4. Create index: `index.ts` with public exports
|
|
207
|
+
5. Add to parent index if needed
|
|
208
|
+
|
|
209
|
+
## Dependencies
|
|
210
|
+
|
|
211
|
+
### External Dependencies
|
|
212
|
+
|
|
213
|
+
- `@vybestack/llxprt-code-core` - Backend integration
|
|
214
|
+
- `opentui` - Terminal UI framework
|
|
215
|
+
- `vitest` - Testing
|
|
216
|
+
|
|
217
|
+
### Internal Dependencies
|
|
218
|
+
|
|
219
|
+
Features should minimize cross-dependencies:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
lib/ # No dependencies on features
|
|
223
|
+
types/ # No dependencies on features or lib
|
|
224
|
+
features/ # Can depend on lib and types
|
|
225
|
+
# Features should NOT depend on each other
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
If features need to communicate, use:
|
|
229
|
+
|
|
230
|
+
- Events/callbacks passed through props
|
|
231
|
+
- Shared types in `src/types/`
|
|
232
|
+
- Coordination at the app level (`src/index.ts`)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Development Standards for nui
|
|
2
|
+
|
|
3
|
+
## Core Principle: Test-Driven Development is Mandatory
|
|
4
|
+
|
|
5
|
+
**Every line of production code must be written in response to a failing test. No exceptions.**
|
|
6
|
+
|
|
7
|
+
## Quick Reference
|
|
8
|
+
|
|
9
|
+
### Must Do:
|
|
10
|
+
|
|
11
|
+
- Write test first (RED) → Minimal code to pass (GREEN) → Refactor if valuable
|
|
12
|
+
- Test behavior, not implementation
|
|
13
|
+
- Use TypeScript strict mode (no `any`, no type assertions)
|
|
14
|
+
- Use nui's Logger (`src/lib/logger.ts`) for all logging
|
|
15
|
+
- Explicit return types on all functions
|
|
16
|
+
- Run `npm run lint` and `npm run typecheck` before commits
|
|
17
|
+
|
|
18
|
+
### Never Do:
|
|
19
|
+
|
|
20
|
+
- Write production code without a failing test
|
|
21
|
+
- Use `console.log`, `console.warn`, `console.error` (use Logger)
|
|
22
|
+
- Use `any` type (use `unknown` with type guards)
|
|
23
|
+
- Write "mock theater" tests that verify mock calls instead of behavior
|
|
24
|
+
- Add comments (code must be self-documenting)
|
|
25
|
+
- Create speculative abstractions
|
|
26
|
+
|
|
27
|
+
## Technology Stack
|
|
28
|
+
|
|
29
|
+
- **Language**: TypeScript (strict mode required)
|
|
30
|
+
- **Testing**: Vitest
|
|
31
|
+
- **UI Framework**: opentui (terminal UI)
|
|
32
|
+
- **Backend Integration**: @vybestack/llxprt-code-core
|
|
33
|
+
|
|
34
|
+
## TDD Process
|
|
35
|
+
|
|
36
|
+
### Red-Green-Refactor (Follow Strictly)
|
|
37
|
+
|
|
38
|
+
1. **RED**: Write a failing test for the next small behavior
|
|
39
|
+
2. **GREEN**: Write ONLY enough code to make the test pass
|
|
40
|
+
3. **REFACTOR**: Assess if refactoring adds value. If yes, improve. If no, move on.
|
|
41
|
+
4. **COMMIT**: Feature + tests together, refactoring separately
|
|
42
|
+
|
|
43
|
+
### Example TDD Flow
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// 1. RED - Test first
|
|
47
|
+
describe('transformEvent', () => {
|
|
48
|
+
it('should convert Content event to text_delta', () => {
|
|
49
|
+
const input = { type: GeminiEventType.Content, value: 'hello' };
|
|
50
|
+
const result = transformEvent(input);
|
|
51
|
+
expect(result).toEqual({ type: 'text_delta', text: 'hello' });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 2. GREEN - Minimal implementation
|
|
56
|
+
function transformEvent(event: ServerGeminiStreamEvent): AdapterEvent {
|
|
57
|
+
if (event.type === GeminiEventType.Content) {
|
|
58
|
+
return { type: 'text_delta', text: event.value };
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Unknown event type: ${event.type}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. REFACTOR - Only if it improves clarity
|
|
64
|
+
// 4. COMMIT - "feat: add event transformation for Content type"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## TypeScript Rules
|
|
68
|
+
|
|
69
|
+
### Required Practices
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Explicit return types on all functions
|
|
73
|
+
function calculateTotal(items: Item[]): number {
|
|
74
|
+
return items.reduce((sum, item) => sum + item.price, 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use unknown with type guards, not any
|
|
78
|
+
function handleError(error: unknown): string {
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
return error.message;
|
|
81
|
+
}
|
|
82
|
+
return String(error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Use type predicates instead of assertions
|
|
86
|
+
function isTextEvent(event: AdapterEvent): event is TextDeltaEvent {
|
|
87
|
+
return event.type === 'text_delta';
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Forbidden Patterns
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// BAD: Using any
|
|
95
|
+
function process(data: any) { ... }
|
|
96
|
+
|
|
97
|
+
// BAD: Type assertions
|
|
98
|
+
const user = data as User;
|
|
99
|
+
|
|
100
|
+
// BAD: Non-null assertions
|
|
101
|
+
const name = user!.name;
|
|
102
|
+
|
|
103
|
+
// BAD: Console logging
|
|
104
|
+
console.log("debug info");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Testing Guidelines
|
|
108
|
+
|
|
109
|
+
### What to Test
|
|
110
|
+
|
|
111
|
+
- Public API behavior (input → output)
|
|
112
|
+
- Edge cases and error conditions
|
|
113
|
+
- Integration between units
|
|
114
|
+
- Event transformations
|
|
115
|
+
|
|
116
|
+
### What NOT to Test
|
|
117
|
+
|
|
118
|
+
- Implementation details
|
|
119
|
+
- Private methods
|
|
120
|
+
- That mocks were called (mock theater)
|
|
121
|
+
- Third-party library internals
|
|
122
|
+
|
|
123
|
+
### Mock Theater (FORBIDDEN)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// BAD: Testing that a mock was called
|
|
127
|
+
it('should call database.find', () => {
|
|
128
|
+
const mockDb = { find: vi.fn() };
|
|
129
|
+
service.getUser('123');
|
|
130
|
+
expect(mockDb.find).toHaveBeenCalledWith('123');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// GOOD: Testing actual behavior
|
|
134
|
+
it('should return user data for valid ID', () => {
|
|
135
|
+
const user = service.getUser('123');
|
|
136
|
+
expect(user).toEqual({ id: '123', name: 'Alice' });
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Reverse Tests (FORBIDDEN)
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// BAD: Test that passes when code is wrong
|
|
144
|
+
it('should not throw', () => {
|
|
145
|
+
expect(() => brokenFunction()).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// GOOD: Test that verifies correct behavior
|
|
149
|
+
it('should return calculated result', () => {
|
|
150
|
+
expect(calculate(2, 3)).toBe(5);
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Stub Implementations (FORBIDDEN)
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// BAD: Stub that doesn't do anything
|
|
158
|
+
function processMessage(msg: Message): void {
|
|
159
|
+
// TODO: implement
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// GOOD: Real implementation or throw
|
|
163
|
+
function processMessage(msg: Message): ProcessedMessage {
|
|
164
|
+
return { id: msg.id, processed: true, timestamp: Date.now() };
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Logging
|
|
169
|
+
|
|
170
|
+
Use nui's Logger for all logging:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { getLogger } from '../lib/logger';
|
|
174
|
+
|
|
175
|
+
const logger = getLogger('nui:my-module');
|
|
176
|
+
|
|
177
|
+
logger.debug('Processing started', { itemCount: items.length });
|
|
178
|
+
logger.warn('Unexpected state', { state });
|
|
179
|
+
logger.error('Operation failed', { error: err.message });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Logs go to `~/.llxprt/nuilog/nui.log`.
|
|
183
|
+
|
|
184
|
+
## Error Handling
|
|
185
|
+
|
|
186
|
+
### Use Explicit Error States
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// BAD: Throwing for control flow
|
|
190
|
+
try {
|
|
191
|
+
const user = getUser(id);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// Handle missing user
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// GOOD: Explicit result types
|
|
197
|
+
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
198
|
+
|
|
199
|
+
function getUser(id: string): Result<User> {
|
|
200
|
+
const user = users.get(id);
|
|
201
|
+
if (!user) {
|
|
202
|
+
return { ok: false, error: `User ${id} not found` };
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, value: user };
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Immutability
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// BAD: Mutation
|
|
212
|
+
function addItem(cart: Cart, item: Item): Cart {
|
|
213
|
+
cart.items.push(item);
|
|
214
|
+
return cart;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// GOOD: Immutable
|
|
218
|
+
function addItem(cart: Cart, item: Item): Cart {
|
|
219
|
+
return { ...cart, items: [...cart.items, item] };
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Review Checklist
|
|
224
|
+
|
|
225
|
+
Before submitting code, verify:
|
|
226
|
+
|
|
227
|
+
- [ ] All tests pass (`npm test`)
|
|
228
|
+
- [ ] No TypeScript errors (`npm run typecheck`)
|
|
229
|
+
- [ ] No linting warnings (`npm run lint`)
|
|
230
|
+
- [ ] No console.log or debug code
|
|
231
|
+
- [ ] No stub implementations
|
|
232
|
+
- [ ] No mock theater tests
|
|
233
|
+
- [ ] Code is self-documenting
|
|
234
|
+
- [ ] Follows immutability patterns
|
|
235
|
+
- [ ] Error cases handled explicitly
|