@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,248 @@
1
+ interface SlashNode {
2
+ name: string;
3
+ description: string;
4
+ children?: SlashNode[];
5
+ }
6
+
7
+ let themeNodes: SlashNode[] = [];
8
+ let profileNodes: SlashNode[] = [];
9
+
10
+ export function setThemeSuggestions(
11
+ themes: { slug: string; name: string }[],
12
+ ): void {
13
+ themeNodes = themes.map((theme) => ({
14
+ name: theme.slug,
15
+ description: theme.name,
16
+ }));
17
+ }
18
+
19
+ export function setProfileSuggestions(profileNames: string[]): void {
20
+ profileNodes = profileNames.map((name) => ({
21
+ name,
22
+ description: `Profile: ${name}`,
23
+ }));
24
+ }
25
+
26
+ const SLASH_COMMANDS: SlashNode[] = [
27
+ { name: 'about', description: 'show version info' },
28
+ {
29
+ name: 'auth',
30
+ description:
31
+ 'Open auth dialog or toggle OAuth enablement for providers (gemini, qwen, anthropic)',
32
+ },
33
+ { name: 'bug', description: 'submit a bug report' },
34
+ { name: 'chat', description: 'Manage conversation checkpoints' },
35
+ { name: 'clear', description: 'clear the screen and conversation history' },
36
+ {
37
+ name: 'compress',
38
+ description: 'Compresses the context by replacing it with a summary.',
39
+ },
40
+ {
41
+ name: 'copy',
42
+ description: 'Copy the last result or code snippet to clipboard',
43
+ },
44
+ {
45
+ name: 'docs',
46
+ description: 'open full LLxprt Code documentation in your browser',
47
+ },
48
+ { name: 'directory', description: 'Manage workspace directories' },
49
+ { name: 'editor', description: 'set external editor preference' },
50
+ { name: 'extensions', description: 'Manage extensions' },
51
+ { name: 'help', description: 'for help on LLxprt Code' },
52
+ { name: 'ide', description: 'manage IDE integration' },
53
+ {
54
+ name: 'init',
55
+ description: 'Analyzes the project and creates a tailored LLXPRT.md file.',
56
+ },
57
+ { name: 'model', description: 'Set the model id for the current provider' },
58
+ {
59
+ name: 'mcp',
60
+ description:
61
+ 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers',
62
+ },
63
+ { name: 'memory', description: 'Commands for interacting with memory.' },
64
+ {
65
+ name: 'privacy',
66
+ description: 'view Gemini API privacy disclosure and terms',
67
+ },
68
+ { name: 'logging', description: 'manage conversation logging settings' },
69
+ {
70
+ name: 'provider',
71
+ description: 'Set the provider (openai | gemini | anthropic)',
72
+ },
73
+ { name: 'baseurl', description: 'Set the provider base URL' },
74
+ { name: 'key', description: 'Set the API key' },
75
+ { name: 'keyfile', description: 'Set the API keyfile path' },
76
+ {
77
+ name: 'profile',
78
+ description: 'Load a profile',
79
+ children: [
80
+ {
81
+ name: 'load',
82
+ description: 'Load a profile by name',
83
+ children: profileNodes,
84
+ },
85
+ ],
86
+ },
87
+ { name: 'quit', description: 'exit the cli' },
88
+ {
89
+ name: 'stats',
90
+ description: 'check session stats. Usage: /stats [model|tools|cache]',
91
+ children: [
92
+ { name: 'model', description: 'Show model-specific usage statistics.' },
93
+ { name: 'tools', description: 'Show tool-specific usage statistics.' },
94
+ {
95
+ name: 'cache',
96
+ description: 'Show cache usage statistics (Anthropic only).',
97
+ },
98
+ ],
99
+ },
100
+ { name: 'theme', description: 'change the theme' },
101
+ { name: 'tools', description: 'List, enable, or disable Gemini CLI tools' },
102
+ { name: 'settings', description: 'View and edit LLxprt Code settings' },
103
+ { name: 'vim', description: 'toggle vim mode on/off' },
104
+ {
105
+ name: 'set',
106
+ description: 'Set an option',
107
+ children: [
108
+ { name: 'unset', description: 'Unset option' },
109
+ { name: 'modelparam', description: 'Model parameter option' },
110
+ {
111
+ name: 'emojifilter',
112
+ description: 'Emoji filter option',
113
+ children: [
114
+ { name: 'allowed', description: 'Allow all emojis' },
115
+ {
116
+ name: 'auto',
117
+ description: 'Automatically filter inappropriate emojis',
118
+ },
119
+ { name: 'warn', description: 'Warn about filtered emojis' },
120
+ { name: 'error', description: 'Error on filtered emojis' },
121
+ ],
122
+ },
123
+ { name: 'context-limit', description: 'Context Limit option' },
124
+ {
125
+ name: 'compression-threshold',
126
+ description: 'Compression Threshold option',
127
+ },
128
+ { name: 'base-url', description: 'Base Url option' },
129
+ { name: 'api-version', description: 'Api Version option' },
130
+ { name: 'streaming', description: 'Streaming option' },
131
+ ],
132
+ },
133
+ ];
134
+
135
+ export interface SlashSuggestion {
136
+ value: string;
137
+ description: string;
138
+ fullPath: string;
139
+ hasChildren: boolean;
140
+ }
141
+
142
+ export function getSlashSuggestions(
143
+ parts: string[],
144
+ limit: number,
145
+ ): SlashSuggestion[] {
146
+ const prefixParts = parts.slice(0, Math.max(parts.length - 1, 0));
147
+ const query = parts[parts.length - 1] ?? '';
148
+ const nodeList = resolvePath(prefixParts);
149
+
150
+ if (nodeList.length === 0) {
151
+ return [];
152
+ }
153
+
154
+ const normalized = query.toLowerCase();
155
+
156
+ const suggestions = nodeList
157
+ .filter((node) => node.name.toLowerCase().startsWith(normalized))
158
+ .sort((a, b) => {
159
+ const aStarts = a.name.toLowerCase().startsWith(normalized);
160
+ const bStarts = b.name.toLowerCase().startsWith(normalized);
161
+ if (aStarts !== bStarts) {
162
+ return aStarts ? -1 : 1;
163
+ }
164
+ return a.name.localeCompare(b.name);
165
+ })
166
+ .slice(0, limit)
167
+ .map((node) => ({
168
+ value: node.name,
169
+ description: node.description,
170
+ fullPath: buildFullPath(parts, node.name),
171
+ hasChildren: Boolean(node.children?.length),
172
+ }));
173
+
174
+ return suggestions;
175
+ }
176
+
177
+ export function extractSlashContext(
178
+ input: string,
179
+ cursorOffset: number,
180
+ ): { parts: string[]; start: number; end: number } | null {
181
+ const safeOffset = Math.min(Math.max(cursorOffset, 0), input.length);
182
+ const upToCursor = input.slice(0, safeOffset);
183
+ const slashIndex = upToCursor.lastIndexOf('/');
184
+
185
+ if (slashIndex === -1) {
186
+ return null;
187
+ }
188
+
189
+ if (slashIndex > 0 && /\S/.test(upToCursor[slashIndex - 1] ?? '')) {
190
+ return null;
191
+ }
192
+
193
+ let end = safeOffset;
194
+ while (end < input.length) {
195
+ const char = input[end] ?? '';
196
+ if (char === '\n') {
197
+ break;
198
+ }
199
+ if (char.trim() === '') {
200
+ break;
201
+ }
202
+ end += 1;
203
+ }
204
+
205
+ const token = input.slice(slashIndex + 1, end);
206
+ const parts = token.length === 0 ? [] : token.split(/\s+/);
207
+
208
+ return { parts, start: slashIndex, end };
209
+ }
210
+
211
+ function resolvePath(parts: string[]): SlashNode[] {
212
+ if (parts.length === 0) {
213
+ return SLASH_COMMANDS;
214
+ }
215
+ if (parts.length >= 1 && parts[0] === 'theme') {
216
+ return parts.length === 1 ? themeNodes : [];
217
+ }
218
+ if (parts.length === 1 && parts[0] === 'profile') {
219
+ return [
220
+ {
221
+ name: 'load',
222
+ description: 'Load a profile by name',
223
+ children: profileNodes,
224
+ },
225
+ ];
226
+ }
227
+ if (parts.length === 2 && parts[0] === 'profile' && parts[1] === 'load') {
228
+ return profileNodes;
229
+ }
230
+ let current: SlashNode[] = SLASH_COMMANDS;
231
+ for (const part of parts) {
232
+ const node = current.find((n) => n.name === part);
233
+ if (!node?.children) {
234
+ return [];
235
+ }
236
+ current = node.children;
237
+ }
238
+ return current;
239
+ }
240
+
241
+ function buildFullPath(parts: string[], next: string): string {
242
+ const existing = [...parts];
243
+ if (existing.length === 0) {
244
+ return `/${next}`;
245
+ }
246
+ const pathParts = [...existing.slice(0, -1), next];
247
+ return `/${pathParts.join(' ')}`;
248
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ extractMentionQuery,
4
+ findMentionRange,
5
+ getSuggestions,
6
+ } from './suggestions';
7
+
8
+ describe('getSuggestions', () => {
9
+ it('returns matches starting with query first', () => {
10
+ const results = getSuggestions('A');
11
+ expect(results[0]).toBe('Abacadbras.tsx');
12
+ });
13
+
14
+ it('prioritizes files over directories for shared prefix', () => {
15
+ const results = getSuggestions('p');
16
+ expect(results[0]).toBe('package.json');
17
+ expect(results[1]).toBe('packages/');
18
+ });
19
+
20
+ it('returns exact subpath matches', () => {
21
+ const results = getSuggestions('src');
22
+ expect(results[0]).toBe('packages/src');
23
+ });
24
+
25
+ it('returns empty when no matches', () => {
26
+ expect(getSuggestions('b')).toHaveLength(0);
27
+ });
28
+ });
29
+
30
+ describe('extractMentionQuery', () => {
31
+ it('extracts query after @ before cursor', () => {
32
+ expect(extractMentionQuery('hello @pac', 10)).toBe('pac');
33
+ });
34
+
35
+ it('returns null when no @', () => {
36
+ expect(extractMentionQuery('hello world', 5)).toBeNull();
37
+ });
38
+
39
+ it('ignores @ inside words', () => {
40
+ expect(extractMentionQuery('hello@pac', 9)).toBeNull();
41
+ });
42
+
43
+ it('finds mention range for replacement', () => {
44
+ const range = findMentionRange('test @pa th', 8);
45
+ expect(range).toStrictEqual({ start: 5, end: 8 });
46
+ });
47
+
48
+ it('returns null range when none', () => {
49
+ expect(findMentionRange('test', 2)).toBeNull();
50
+ });
51
+ });
@@ -0,0 +1,112 @@
1
+ const FILE_ENTRIES = [
2
+ 'Abacadbras.tsx',
3
+ 'README.md',
4
+ 'Zapper.ts',
5
+ 'packages/',
6
+ 'packages/src',
7
+ 'packages/src/Main.ts',
8
+ 'packages/src/Other.ts',
9
+ 'package.json',
10
+ ] as const;
11
+
12
+ const MAX_SUGGESTIONS = 5;
13
+
14
+ export function getSuggestions(
15
+ query: string,
16
+ limit: number = MAX_SUGGESTIONS,
17
+ ): string[] {
18
+ const normalized = query.trim().toLowerCase();
19
+
20
+ const matches = FILE_ENTRIES.filter((entry) => {
21
+ const segments = entry.split('/').filter(Boolean);
22
+ if (normalized.length === 0) {
23
+ return true;
24
+ }
25
+ return segments.some((segment) =>
26
+ segment.toLowerCase().startsWith(normalized),
27
+ );
28
+ });
29
+
30
+ const sorted = [...matches].sort((a, b) => {
31
+ const aSegments = a.split('/').filter(Boolean);
32
+ const bSegments = b.split('/').filter(Boolean);
33
+
34
+ const aIsFile = isFile(a);
35
+ const bIsFile = isFile(b);
36
+
37
+ if (aSegments.length !== bSegments.length) {
38
+ return aSegments.length - bSegments.length;
39
+ }
40
+
41
+ if (aIsFile !== bIsFile) {
42
+ return aIsFile ? -1 : 1;
43
+ }
44
+
45
+ if (a.length !== b.length) {
46
+ return a.length - b.length;
47
+ }
48
+
49
+ return a.toLowerCase().localeCompare(b.toLowerCase());
50
+ });
51
+
52
+ return sorted.slice(0, limit);
53
+ }
54
+
55
+ export function extractMentionQuery(
56
+ input: string,
57
+ cursorOffset: number,
58
+ ): string | null {
59
+ const safeOffset = Math.min(Math.max(cursorOffset, 0), input.length);
60
+ const upToCursor = input.slice(0, safeOffset);
61
+ const atIndex = upToCursor.lastIndexOf('@');
62
+
63
+ if (atIndex === -1) {
64
+ return null;
65
+ }
66
+
67
+ if (atIndex > 0 && /\S/.test(upToCursor[atIndex - 1] ?? '')) {
68
+ return null;
69
+ }
70
+
71
+ const tail = upToCursor.slice(atIndex + 1);
72
+ const query = tail.split(/\s|\n/)[0] ?? '';
73
+ return query;
74
+ }
75
+
76
+ export function findMentionRange(
77
+ input: string,
78
+ cursorOffset: number,
79
+ ): { start: number; end: number } | null {
80
+ const safeOffset = Math.min(Math.max(cursorOffset, 0), input.length);
81
+ const upToCursor = input.slice(0, safeOffset);
82
+ const atIndex = upToCursor.lastIndexOf('@');
83
+
84
+ if (atIndex === -1) {
85
+ return null;
86
+ }
87
+
88
+ if (atIndex > 0 && /\S/.test(upToCursor[atIndex - 1] ?? '')) {
89
+ return null;
90
+ }
91
+
92
+ let end = safeOffset;
93
+ while (end < input.length) {
94
+ const char = input[end] ?? '';
95
+ if (char === '\n' || char.trim() === '') {
96
+ break;
97
+ }
98
+ end += 1;
99
+ }
100
+
101
+ return { start: atIndex, end };
102
+ }
103
+
104
+ export const SUGGESTION_SOURCE = FILE_ENTRIES;
105
+ export const MAX_SUGGESTION_COUNT = MAX_SUGGESTIONS;
106
+
107
+ function isFile(entry: string): boolean {
108
+ if (entry.endsWith('/')) {
109
+ return false;
110
+ }
111
+ return entry.includes('.');
112
+ }
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { createConfigSession } from './configSession';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import * as os from 'node:os';
6
+
7
+ describe('ConfigSession', () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(() => {
11
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nui-test-'));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe('createConfigSession', () => {
19
+ it('should create Config with minimal required parameters', () => {
20
+ const session = createConfigSession({
21
+ model: 'gemini-2.5-flash',
22
+ workingDir: tempDir,
23
+ });
24
+
25
+ expect(session.config).toBeDefined();
26
+ expect(session.config.getModel()).toBe('gemini-2.5-flash');
27
+ });
28
+
29
+ it('should apply provider setting', () => {
30
+ const session = createConfigSession({
31
+ model: 'gpt-4',
32
+ provider: 'openai',
33
+ workingDir: tempDir,
34
+ });
35
+
36
+ expect(session.config.getProvider()).toBe('openai');
37
+ });
38
+
39
+ it('should disable telemetry by default', () => {
40
+ const session = createConfigSession({
41
+ model: 'gemini-2.5-flash',
42
+ workingDir: tempDir,
43
+ });
44
+
45
+ expect(session.config.getTelemetryEnabled()).toBe(false);
46
+ });
47
+ });
48
+
49
+ describe('initialize', () => {
50
+ it('should make GeminiClient available after initialization', async () => {
51
+ const session = createConfigSession({
52
+ model: 'gemini-2.5-flash',
53
+ workingDir: tempDir,
54
+ });
55
+
56
+ await session.initialize();
57
+
58
+ expect(session.getClient()).toBeDefined();
59
+ });
60
+
61
+ it('should register tools in ToolRegistry', async () => {
62
+ const session = createConfigSession({
63
+ model: 'gemini-2.5-flash',
64
+ workingDir: tempDir,
65
+ });
66
+
67
+ await session.initialize();
68
+
69
+ const registry = session.config.getToolRegistry();
70
+ const tools = registry.getFunctionDeclarations();
71
+
72
+ expect(tools.length).toBeGreaterThan(0);
73
+ });
74
+
75
+ it('should be idempotent', async () => {
76
+ const session = createConfigSession({
77
+ model: 'gemini-2.5-flash',
78
+ workingDir: tempDir,
79
+ });
80
+
81
+ await session.initialize();
82
+ await session.initialize();
83
+
84
+ expect(session.getClient()).toBeDefined();
85
+ });
86
+ });
87
+
88
+ describe('getClient', () => {
89
+ it('should throw if called before initialize', () => {
90
+ const session = createConfigSession({
91
+ model: 'gemini-2.5-flash',
92
+ workingDir: tempDir,
93
+ });
94
+
95
+ expect(() => session.getClient()).toThrow(
96
+ 'ConfigSession not initialized. Call initialize() first.',
97
+ );
98
+ });
99
+ });
100
+
101
+ describe('provider initialization', () => {
102
+ // Provider initialization is slow on Windows CI due to ProviderManager setup
103
+ it(
104
+ 'should initialize with OpenAI provider without error',
105
+ { timeout: 15000 },
106
+ async () => {
107
+ const session = createConfigSession({
108
+ model: 'gpt-4',
109
+ provider: 'openai',
110
+ baseUrl: 'https://api.openai.com/v1',
111
+ apiKey: 'test-key',
112
+ workingDir: tempDir,
113
+ });
114
+
115
+ // This should NOT throw - the test ensures the auth flow works
116
+ await session.initialize();
117
+
118
+ expect(session.getClient()).toBeDefined();
119
+ },
120
+ );
121
+
122
+ it(
123
+ 'should initialize with Anthropic provider without error',
124
+ { timeout: 15000 },
125
+ async () => {
126
+ const session = createConfigSession({
127
+ model: 'claude-3-sonnet',
128
+ provider: 'anthropic',
129
+ baseUrl: 'https://api.anthropic.com',
130
+ apiKey: 'test-key',
131
+ workingDir: tempDir,
132
+ });
133
+
134
+ // This should NOT throw - the test ensures the auth flow works
135
+ await session.initialize();
136
+
137
+ expect(session.getClient()).toBeDefined();
138
+ },
139
+ );
140
+
141
+ it('should initialize with Gemini provider without error', async () => {
142
+ const session = createConfigSession({
143
+ model: 'gemini-2.5-flash',
144
+ provider: 'gemini',
145
+ apiKey: 'test-key',
146
+ workingDir: tempDir,
147
+ });
148
+
149
+ await session.initialize();
150
+
151
+ expect(session.getClient()).toBeDefined();
152
+ });
153
+
154
+ it(
155
+ 'should set up ProviderManager for OpenAI provider',
156
+ { timeout: 15000 },
157
+ async () => {
158
+ const session = createConfigSession({
159
+ model: 'gpt-4',
160
+ provider: 'openai',
161
+ baseUrl: 'https://api.openai.com/v1',
162
+ apiKey: 'test-key',
163
+ workingDir: tempDir,
164
+ });
165
+
166
+ await session.initialize();
167
+
168
+ // ProviderManager should be set for non-gemini providers
169
+ const providerManager = session.config.getProviderManager();
170
+ expect(providerManager).toBeDefined();
171
+ },
172
+ );
173
+
174
+ it('should NOT set up ProviderManager for Gemini provider', async () => {
175
+ const session = createConfigSession({
176
+ model: 'gemini-2.5-flash',
177
+ provider: 'gemini',
178
+ apiKey: 'test-key',
179
+ workingDir: tempDir,
180
+ });
181
+
182
+ await session.initialize();
183
+
184
+ // ProviderManager should NOT be set for Gemini
185
+ const providerManager = session.config.getProviderManager();
186
+ expect(providerManager).toBeUndefined();
187
+ });
188
+ });
189
+ });