@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.
Files changed (123) hide show
  1. package/PLAN-messages.md +681 -0
  2. package/PLAN.md +47 -0
  3. package/README.md +25 -0
  4. package/bun.lock +1024 -0
  5. package/dev-docs/ARCHITECTURE.md +178 -0
  6. package/dev-docs/CODE_ORGANIZATION.md +232 -0
  7. package/dev-docs/STANDARDS.md +235 -0
  8. package/dev-docs/UI_DESIGN.md +425 -0
  9. package/eslint.config.cjs +194 -0
  10. package/images/nui.png +0 -0
  11. package/llxprt.png +0 -0
  12. package/llxprt.svg +128 -0
  13. package/package.json +66 -0
  14. package/scripts/check-limits.ts +177 -0
  15. package/scripts/start.js +71 -0
  16. package/src/app.tsx +599 -0
  17. package/src/bootstrap.tsx +23 -0
  18. package/src/commands/AuthCommand.tsx +80 -0
  19. package/src/commands/ModelCommand.tsx +102 -0
  20. package/src/commands/ProviderCommand.tsx +103 -0
  21. package/src/commands/ThemeCommand.tsx +71 -0
  22. package/src/features/chat/history.ts +178 -0
  23. package/src/features/chat/index.ts +3 -0
  24. package/src/features/chat/persistentHistory.ts +102 -0
  25. package/src/features/chat/responder.ts +217 -0
  26. package/src/features/completion/completions.ts +161 -0
  27. package/src/features/completion/index.ts +3 -0
  28. package/src/features/completion/slash.test.ts +82 -0
  29. package/src/features/completion/slash.ts +248 -0
  30. package/src/features/completion/suggestions.test.ts +51 -0
  31. package/src/features/completion/suggestions.ts +112 -0
  32. package/src/features/config/configSession.test.ts +189 -0
  33. package/src/features/config/configSession.ts +179 -0
  34. package/src/features/config/index.ts +4 -0
  35. package/src/features/config/llxprtAdapter.integration.test.ts +202 -0
  36. package/src/features/config/llxprtAdapter.test.ts +139 -0
  37. package/src/features/config/llxprtAdapter.ts +257 -0
  38. package/src/features/config/llxprtCommands.test.ts +40 -0
  39. package/src/features/config/llxprtCommands.ts +35 -0
  40. package/src/features/config/llxprtConfig.test.ts +261 -0
  41. package/src/features/config/llxprtConfig.ts +418 -0
  42. package/src/features/theme/index.ts +2 -0
  43. package/src/features/theme/theme.test.ts +51 -0
  44. package/src/features/theme/theme.ts +105 -0
  45. package/src/features/theme/themeManager.ts +84 -0
  46. package/src/hooks/useAppCommands.ts +129 -0
  47. package/src/hooks/useApprovalKeyboard.ts +156 -0
  48. package/src/hooks/useChatStore.test.ts +112 -0
  49. package/src/hooks/useChatStore.ts +252 -0
  50. package/src/hooks/useInputManager.ts +99 -0
  51. package/src/hooks/useKeyboardHandlers.ts +130 -0
  52. package/src/hooks/useListNavigation.test.ts +166 -0
  53. package/src/hooks/useListNavigation.ts +62 -0
  54. package/src/hooks/usePersistentHistory.ts +94 -0
  55. package/src/hooks/useScrollManagement.ts +107 -0
  56. package/src/hooks/useSelectionClipboard.ts +48 -0
  57. package/src/hooks/useSessionManager.test.ts +85 -0
  58. package/src/hooks/useSessionManager.ts +101 -0
  59. package/src/hooks/useStreamingLifecycle.ts +71 -0
  60. package/src/hooks/useStreamingResponder.ts +401 -0
  61. package/src/hooks/useSuggestionSetup.ts +23 -0
  62. package/src/hooks/useToolApproval.test.ts +140 -0
  63. package/src/hooks/useToolApproval.ts +264 -0
  64. package/src/hooks/useToolScheduler.ts +432 -0
  65. package/src/index.ts +3 -0
  66. package/src/jsx.d.ts +11 -0
  67. package/src/lib/clipboard.ts +18 -0
  68. package/src/lib/logger.ts +107 -0
  69. package/src/lib/random.ts +5 -0
  70. package/src/main.tsx +13 -0
  71. package/src/test/mockTheme.ts +51 -0
  72. package/src/types/events.ts +87 -0
  73. package/src/types.ts +13 -0
  74. package/src/ui/components/ChatLayout.tsx +694 -0
  75. package/src/ui/components/CommandComponents.tsx +74 -0
  76. package/src/ui/components/DiffViewer.tsx +306 -0
  77. package/src/ui/components/FilterInput.test.ts +69 -0
  78. package/src/ui/components/FilterInput.tsx +62 -0
  79. package/src/ui/components/HeaderBar.tsx +137 -0
  80. package/src/ui/components/RadioSelect.test.ts +140 -0
  81. package/src/ui/components/RadioSelect.tsx +88 -0
  82. package/src/ui/components/SelectableList.test.ts +83 -0
  83. package/src/ui/components/SelectableList.tsx +35 -0
  84. package/src/ui/components/StatusBar.tsx +45 -0
  85. package/src/ui/components/SuggestionPanel.tsx +102 -0
  86. package/src/ui/components/messages/ModelMessage.tsx +14 -0
  87. package/src/ui/components/messages/SystemMessage.tsx +29 -0
  88. package/src/ui/components/messages/ThinkingMessage.tsx +14 -0
  89. package/src/ui/components/messages/UserMessage.tsx +26 -0
  90. package/src/ui/components/messages/index.ts +15 -0
  91. package/src/ui/components/messages/renderMessage.test.ts +49 -0
  92. package/src/ui/components/messages/renderMessage.tsx +43 -0
  93. package/src/ui/components/messages/types.test.ts +24 -0
  94. package/src/ui/components/messages/types.ts +36 -0
  95. package/src/ui/modals/AuthModal.tsx +106 -0
  96. package/src/ui/modals/ModalShell.tsx +60 -0
  97. package/src/ui/modals/SearchSelectModal.tsx +236 -0
  98. package/src/ui/modals/ThemeModal.tsx +204 -0
  99. package/src/ui/modals/ToolApprovalModal.test.ts +206 -0
  100. package/src/ui/modals/ToolApprovalModal.tsx +282 -0
  101. package/src/ui/modals/index.ts +20 -0
  102. package/src/ui/modals/modals.test.ts +26 -0
  103. package/src/ui/modals/types.ts +19 -0
  104. package/src/uicontext/Command.tsx +102 -0
  105. package/src/uicontext/Dialog.tsx +65 -0
  106. package/src/uicontext/index.ts +2 -0
  107. package/themes/ansi-light.json +59 -0
  108. package/themes/ansi.json +59 -0
  109. package/themes/atom-one-dark.json +59 -0
  110. package/themes/ayu-light.json +59 -0
  111. package/themes/ayu.json +59 -0
  112. package/themes/default-light.json +59 -0
  113. package/themes/default.json +59 -0
  114. package/themes/dracula.json +59 -0
  115. package/themes/github-dark.json +59 -0
  116. package/themes/github-light.json +59 -0
  117. package/themes/googlecode.json +59 -0
  118. package/themes/green-screen.json +59 -0
  119. package/themes/no-color.json +59 -0
  120. package/themes/shades-of-purple.json +59 -0
  121. package/themes/xcode.json +59 -0
  122. package/tsconfig.json +28 -0
  123. 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