@siteboon/claude-code-ui 1.13.0 → 1.13.1

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/dist/index.html CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  <!-- Prevent zoom on iOS -->
27
27
  <meta name="format-detection" content="telephone=no" />
28
- <script type="module" crossorigin src="/assets/index-Zq2roSUR.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-BL1HpeHJ.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DVSKlM5e.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-CnTQH7Pk.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DfaPXD3y.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteboon/claude-code-ui",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -16,6 +16,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
16
16
  import { promises as fs } from 'fs';
17
17
  import path from 'path';
18
18
  import os from 'os';
19
+ import { CLAUDE_MODELS } from '../shared/modelConstants.js';
19
20
 
20
21
  // Session tracking: Map of session IDs to active query instances
21
22
  const activeSessions = new Map();
@@ -77,7 +78,7 @@ function mapCliOptionsToSDK(options = {}) {
77
78
 
78
79
  // Map model (default to sonnet)
79
80
  // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
80
- sdkOptions.model = options.model || 'sonnet';
81
+ sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
81
82
  console.log(`Using model: ${sdkOptions.model}`);
82
83
 
83
84
  // Map system prompt configuration
@@ -397,10 +398,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
397
398
  // Send session-created event only once for new sessions
398
399
  if (!sessionId && !sessionCreatedSent) {
399
400
  sessionCreatedSent = true;
400
- ws.send(JSON.stringify({
401
+ ws.send({
401
402
  type: 'session-created',
402
403
  sessionId: capturedSessionId
403
- }));
404
+ });
404
405
  } else {
405
406
  console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
406
407
  }
@@ -410,20 +411,20 @@ async function queryClaudeSDK(command, options = {}, ws) {
410
411
 
411
412
  // Transform and send message to WebSocket
412
413
  const transformedMessage = transformMessage(message);
413
- ws.send(JSON.stringify({
414
+ ws.send({
414
415
  type: 'claude-response',
415
416
  data: transformedMessage
416
- }));
417
+ });
417
418
 
418
419
  // Extract and send token budget updates from result messages
419
420
  if (message.type === 'result') {
420
421
  const tokenBudget = extractTokenBudget(message);
421
422
  if (tokenBudget) {
422
423
  console.log('Token budget from modelUsage:', tokenBudget);
423
- ws.send(JSON.stringify({
424
+ ws.send({
424
425
  type: 'token-budget',
425
426
  data: tokenBudget
426
- }));
427
+ });
427
428
  }
428
429
  }
429
430
  }
@@ -438,12 +439,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
438
439
 
439
440
  // Send completion event
440
441
  console.log('Streaming complete, sending claude-complete event');
441
- ws.send(JSON.stringify({
442
+ ws.send({
442
443
  type: 'claude-complete',
443
444
  sessionId: capturedSessionId,
444
445
  exitCode: 0,
445
446
  isNewSession: !sessionId && !!command
446
- }));
447
+ });
447
448
  console.log('claude-complete event sent');
448
449
 
449
450
  } catch (error) {
@@ -458,10 +459,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
458
459
  await cleanupTempFiles(tempImagePaths, tempDir);
459
460
 
460
461
  // Send error to WebSocket
461
- ws.send(JSON.stringify({
462
+ ws.send({
462
463
  type: 'claude-error',
463
464
  error: error.message
464
- }));
465
+ });
465
466
 
466
467
  throw error;
467
468
  }
@@ -102,29 +102,29 @@ async function spawnCursor(command, options = {}, ws) {
102
102
  // Send session-created event only once for new sessions
103
103
  if (!sessionId && !sessionCreatedSent) {
104
104
  sessionCreatedSent = true;
105
- ws.send(JSON.stringify({
105
+ ws.send({
106
106
  type: 'session-created',
107
107
  sessionId: capturedSessionId,
108
108
  model: response.model,
109
109
  cwd: response.cwd
110
- }));
110
+ });
111
111
  }
112
112
  }
113
113
 
114
114
  // Send system info to frontend
115
- ws.send(JSON.stringify({
115
+ ws.send({
116
116
  type: 'cursor-system',
117
117
  data: response
118
- }));
118
+ });
119
119
  }
120
120
  break;
121
121
 
122
122
  case 'user':
123
123
  // Forward user message
124
- ws.send(JSON.stringify({
124
+ ws.send({
125
125
  type: 'cursor-user',
126
126
  data: response
127
- }));
127
+ });
128
128
  break;
129
129
 
130
130
  case 'assistant':
@@ -134,7 +134,7 @@ async function spawnCursor(command, options = {}, ws) {
134
134
  messageBuffer += textContent;
135
135
 
136
136
  // Send as Claude-compatible format for frontend
137
- ws.send(JSON.stringify({
137
+ ws.send({
138
138
  type: 'claude-response',
139
139
  data: {
140
140
  type: 'content_block_delta',
@@ -143,7 +143,7 @@ async function spawnCursor(command, options = {}, ws) {
143
143
  text: textContent
144
144
  }
145
145
  }
146
- }));
146
+ });
147
147
  }
148
148
  break;
149
149
 
@@ -153,37 +153,37 @@ async function spawnCursor(command, options = {}, ws) {
153
153
 
154
154
  // Send final message if we have buffered content
155
155
  if (messageBuffer) {
156
- ws.send(JSON.stringify({
156
+ ws.send({
157
157
  type: 'claude-response',
158
158
  data: {
159
159
  type: 'content_block_stop'
160
160
  }
161
- }));
161
+ });
162
162
  }
163
163
 
164
164
  // Send completion event
165
- ws.send(JSON.stringify({
165
+ ws.send({
166
166
  type: 'cursor-result',
167
167
  sessionId: capturedSessionId || sessionId,
168
168
  data: response,
169
169
  success: response.subtype === 'success'
170
- }));
170
+ });
171
171
  break;
172
172
 
173
173
  default:
174
174
  // Forward any other message types
175
- ws.send(JSON.stringify({
175
+ ws.send({
176
176
  type: 'cursor-response',
177
177
  data: response
178
- }));
178
+ });
179
179
  }
180
180
  } catch (parseError) {
181
181
  console.log('📄 Non-JSON response:', line);
182
182
  // If not JSON, send as raw text
183
- ws.send(JSON.stringify({
183
+ ws.send({
184
184
  type: 'cursor-output',
185
185
  data: line
186
- }));
186
+ });
187
187
  }
188
188
  }
189
189
  });
@@ -191,10 +191,10 @@ async function spawnCursor(command, options = {}, ws) {
191
191
  // Handle stderr
192
192
  cursorProcess.stderr.on('data', (data) => {
193
193
  console.error('Cursor CLI stderr:', data.toString());
194
- ws.send(JSON.stringify({
194
+ ws.send({
195
195
  type: 'cursor-error',
196
196
  error: data.toString()
197
- }));
197
+ });
198
198
  });
199
199
 
200
200
  // Handle process completion
@@ -205,12 +205,12 @@ async function spawnCursor(command, options = {}, ws) {
205
205
  const finalSessionId = capturedSessionId || sessionId || processKey;
206
206
  activeCursorProcesses.delete(finalSessionId);
207
207
 
208
- ws.send(JSON.stringify({
208
+ ws.send({
209
209
  type: 'claude-complete',
210
210
  sessionId: finalSessionId,
211
211
  exitCode: code,
212
212
  isNewSession: !sessionId && !!command // Flag to indicate this was a new session
213
- }));
213
+ });
214
214
 
215
215
  if (code === 0) {
216
216
  resolve();
@@ -226,12 +226,12 @@ async function spawnCursor(command, options = {}, ws) {
226
226
  // Clean up process reference on error
227
227
  const finalSessionId = capturedSessionId || sessionId || processKey;
228
228
  activeCursorProcesses.delete(finalSessionId);
229
-
230
- ws.send(JSON.stringify({
229
+
230
+ ws.send({
231
231
  type: 'cursor-error',
232
232
  error: error.message
233
- }));
234
-
233
+ });
234
+
235
235
  reject(error);
236
236
  });
237
237
 
package/server/index.js CHANGED
@@ -717,6 +717,32 @@ wss.on('connection', (ws, request) => {
717
717
  }
718
718
  });
719
719
 
720
+ /**
721
+ * WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
722
+ */
723
+ class WebSocketWriter {
724
+ constructor(ws) {
725
+ this.ws = ws;
726
+ this.sessionId = null;
727
+ this.isWebSocketWriter = true; // Marker for transport detection
728
+ }
729
+
730
+ send(data) {
731
+ if (this.ws.readyState === 1) { // WebSocket.OPEN
732
+ // Providers send raw objects, we stringify for WebSocket
733
+ this.ws.send(JSON.stringify(data));
734
+ }
735
+ }
736
+
737
+ setSessionId(sessionId) {
738
+ this.sessionId = sessionId;
739
+ }
740
+
741
+ getSessionId() {
742
+ return this.sessionId;
743
+ }
744
+ }
745
+
720
746
  // Handle chat WebSocket connections
721
747
  function handleChatConnection(ws) {
722
748
  console.log('[INFO] Chat WebSocket connected');
@@ -724,6 +750,9 @@ function handleChatConnection(ws) {
724
750
  // Add to connected clients for project updates
725
751
  connectedClients.add(ws);
726
752
 
753
+ // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
754
+ const writer = new WebSocketWriter(ws);
755
+
727
756
  ws.on('message', async (message) => {
728
757
  try {
729
758
  const data = JSON.parse(message);
@@ -734,19 +763,19 @@ function handleChatConnection(ws) {
734
763
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
735
764
 
736
765
  // Use Claude Agents SDK
737
- await queryClaudeSDK(data.command, data.options, ws);
766
+ await queryClaudeSDK(data.command, data.options, writer);
738
767
  } else if (data.type === 'cursor-command') {
739
768
  console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
740
769
  console.log('📁 Project:', data.options?.cwd || 'Unknown');
741
770
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
742
771
  console.log('🤖 Model:', data.options?.model || 'default');
743
- await spawnCursor(data.command, data.options, ws);
772
+ await spawnCursor(data.command, data.options, writer);
744
773
  } else if (data.type === 'codex-command') {
745
774
  console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
746
775
  console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
747
776
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
748
777
  console.log('🤖 Model:', data.options?.model || 'default');
749
- await queryCodex(data.command, data.options, ws);
778
+ await queryCodex(data.command, data.options, writer);
750
779
  } else if (data.type === 'cursor-resume') {
751
780
  // Backward compatibility: treat as cursor-command with resume and no prompt
752
781
  console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -754,7 +783,7 @@ function handleChatConnection(ws) {
754
783
  sessionId: data.sessionId,
755
784
  resume: true,
756
785
  cwd: data.options?.cwd
757
- }, ws);
786
+ }, writer);
758
787
  } else if (data.type === 'abort-session') {
759
788
  console.log('[DEBUG] Abort session request:', data.sessionId);
760
789
  const provider = data.provider || 'claude';
@@ -769,21 +798,21 @@ function handleChatConnection(ws) {
769
798
  success = await abortClaudeSDKSession(data.sessionId);
770
799
  }
771
800
 
772
- ws.send(JSON.stringify({
801
+ writer.send({
773
802
  type: 'session-aborted',
774
803
  sessionId: data.sessionId,
775
804
  provider,
776
805
  success
777
- }));
806
+ });
778
807
  } else if (data.type === 'cursor-abort') {
779
808
  console.log('[DEBUG] Abort Cursor session:', data.sessionId);
780
809
  const success = abortCursorSession(data.sessionId);
781
- ws.send(JSON.stringify({
810
+ writer.send({
782
811
  type: 'session-aborted',
783
812
  sessionId: data.sessionId,
784
813
  provider: 'cursor',
785
814
  success
786
- }));
815
+ });
787
816
  } else if (data.type === 'check-session-status') {
788
817
  // Check if a specific session is currently processing
789
818
  const provider = data.provider || 'claude';
@@ -799,12 +828,12 @@ function handleChatConnection(ws) {
799
828
  isActive = isClaudeSDKSessionActive(sessionId);
800
829
  }
801
830
 
802
- ws.send(JSON.stringify({
831
+ writer.send({
803
832
  type: 'session-status',
804
833
  sessionId,
805
834
  provider,
806
835
  isProcessing: isActive
807
- }));
836
+ });
808
837
  } else if (data.type === 'get-active-sessions') {
809
838
  // Get all currently active sessions
810
839
  const activeSessions = {
@@ -812,17 +841,17 @@ function handleChatConnection(ws) {
812
841
  cursor: getActiveCursorSessions(),
813
842
  codex: getActiveCodexSessions()
814
843
  };
815
- ws.send(JSON.stringify({
844
+ writer.send({
816
845
  type: 'active-sessions',
817
846
  sessions: activeSessions
818
- }));
847
+ });
819
848
  }
820
849
  } catch (error) {
821
850
  console.error('[ERROR] Chat WebSocket error:', error.message);
822
- ws.send(JSON.stringify({
851
+ writer.send({
823
852
  type: 'error',
824
853
  error: error.message
825
- }));
854
+ });
826
855
  }
827
856
  });
828
857
 
@@ -213,7 +213,8 @@ export async function queryCodex(command, options = {}, ws) {
213
213
  workingDirectory,
214
214
  skipGitRepoCheck: true,
215
215
  sandboxMode,
216
- approvalPolicy
216
+ approvalPolicy,
217
+ model
217
218
  };
218
219
 
219
220
  // Start or resume thread
@@ -359,12 +360,12 @@ export function getActiveCodexSessions() {
359
360
  */
360
361
  function sendMessage(ws, data) {
361
362
  try {
362
- if (typeof ws.send === 'function') {
363
- // WebSocket
363
+ if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
364
+ // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
365
+ ws.send(data);
366
+ } else if (typeof ws.send === 'function') {
367
+ // Raw WebSocket - stringify here
364
368
  ws.send(JSON.stringify(data));
365
- } else if (typeof ws.write === 'function') {
366
- // SSE writer (for agent API)
367
- ws.write(`data: ${JSON.stringify(data)}\n\n`);
368
369
  }
369
370
  } catch (error) {
370
371
  console.error('[Codex] Error sending message:', error);
@@ -10,6 +10,7 @@ import { queryClaudeSDK } from '../claude-sdk.js';
10
10
  import { spawnCursor } from '../cursor-cli.js';
11
11
  import { queryCodex } from '../openai-codex.js';
12
12
  import { Octokit } from '@octokit/rest';
13
+ import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
13
14
 
14
15
  const router = express.Router();
15
16
 
@@ -450,6 +451,7 @@ class SSEStreamWriter {
450
451
  constructor(res) {
451
452
  this.res = res;
452
453
  this.sessionId = null;
454
+ this.isSSEStreamWriter = true; // Marker for transport detection
453
455
  }
454
456
 
455
457
  send(data) {
@@ -457,7 +459,7 @@ class SSEStreamWriter {
457
459
  return;
458
460
  }
459
461
 
460
- // Format as SSE
462
+ // Format as SSE - providers send raw objects, we stringify
461
463
  this.res.write(`data: ${JSON.stringify(data)}\n\n`);
462
464
  }
463
465
 
@@ -634,9 +636,14 @@ class ResponseCollector {
634
636
  * - true: Returns text/event-stream with incremental updates
635
637
  * - false: Returns complete JSON response after completion
636
638
  *
637
- * @param {string} model - (Optional) Model identifier for Cursor provider.
638
- * Only applicable when provider='cursor'.
639
- * Examples: 'gpt-4', 'claude-3-opus', etc.
639
+ * @param {string} model - (Optional) Model identifier for providers.
640
+ *
641
+ * Claude models: 'sonnet' (default), 'opus', 'haiku', 'opusplan', 'sonnet[1m]'
642
+ * Cursor models: 'gpt-5' (default), 'gpt-5.2', 'gpt-5.2-high', 'sonnet-4.5', 'opus-4.5',
643
+ * 'gemini-3-pro', 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high',
644
+ * 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max',
645
+ * 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants
646
+ * Codex models: 'gpt-5.2' (default), 'gpt-5.1-codex-max', 'o3', 'o4-mini'
640
647
  *
641
648
  * @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
642
649
  * Default: true
@@ -939,6 +946,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
939
946
  projectPath: finalProjectPath,
940
947
  cwd: finalProjectPath,
941
948
  sessionId: null, // New session
949
+ model: model,
942
950
  permissionMode: 'bypassPermissions' // Bypass all permissions for API calls
943
951
  }, writer);
944
952
 
@@ -959,7 +967,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
959
967
  projectPath: finalProjectPath,
960
968
  cwd: finalProjectPath,
961
969
  sessionId: null,
962
- model: model || 'gpt-5.2',
970
+ model: model || CODEX_MODELS.DEFAULT,
963
971
  permissionMode: 'bypassPermissions'
964
972
  }, writer);
965
973
  }
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import os from 'os';
6
6
  import matter from 'gray-matter';
7
+ import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
@@ -182,23 +183,15 @@ Custom commands can be created in:
182
183
  },
183
184
 
184
185
  '/model': async (args, context) => {
185
- // Read available models from config or defaults
186
+ // Read available models from centralized constants
186
187
  const availableModels = {
187
- claude: [
188
- 'claude-sonnet-4.5',
189
- 'claude-sonnet-4',
190
- 'claude-opus-4',
191
- 'claude-sonnet-3.5'
192
- ],
193
- cursor: [
194
- 'gpt-5',
195
- 'sonnet-4',
196
- 'opus-4.1'
197
- ]
188
+ claude: CLAUDE_MODELS.OPTIONS.map(o => o.value),
189
+ cursor: CURSOR_MODELS.OPTIONS.map(o => o.value),
190
+ codex: CODEX_MODELS.OPTIONS.map(o => o.value)
198
191
  };
199
192
 
200
193
  const currentProvider = context?.provider || 'claude';
201
- const currentModel = context?.model || 'claude-sonnet-4.5';
194
+ const currentModel = context?.model || CLAUDE_MODELS.DEFAULT;
202
195
 
203
196
  return {
204
197
  type: 'builtin',
@@ -216,50 +209,6 @@ Custom commands can be created in:
216
209
  };
217
210
  },
218
211
 
219
- '/cost': async (args, context) => {
220
- // Calculate token usage and cost
221
- const sessionId = context?.sessionId;
222
- const tokenUsage = context?.tokenUsage || { used: 0, total: 200000 };
223
-
224
- const costPerMillion = {
225
- 'claude-sonnet-4.5': { input: 3, output: 15 },
226
- 'claude-sonnet-4': { input: 3, output: 15 },
227
- 'claude-opus-4': { input: 15, output: 75 },
228
- 'gpt-5': { input: 5, output: 15 }
229
- };
230
-
231
- const model = context?.model || 'claude-sonnet-4.5';
232
- const rates = costPerMillion[model] || costPerMillion['claude-sonnet-4.5'];
233
-
234
- // Estimate 70% input, 30% output
235
- const estimatedInputTokens = Math.floor(tokenUsage.used * 0.7);
236
- const estimatedOutputTokens = Math.floor(tokenUsage.used * 0.3);
237
-
238
- const inputCost = (estimatedInputTokens / 1000000) * rates.input;
239
- const outputCost = (estimatedOutputTokens / 1000000) * rates.output;
240
- const totalCost = inputCost + outputCost;
241
-
242
- return {
243
- type: 'builtin',
244
- action: 'cost',
245
- data: {
246
- tokenUsage: {
247
- used: tokenUsage.used,
248
- total: tokenUsage.total,
249
- percentage: ((tokenUsage.used / tokenUsage.total) * 100).toFixed(1)
250
- },
251
- cost: {
252
- input: inputCost.toFixed(4),
253
- output: outputCost.toFixed(4),
254
- total: totalCost.toFixed(4),
255
- currency: 'USD'
256
- },
257
- model,
258
- rates
259
- }
260
- };
261
- },
262
-
263
212
  '/status': async (args, context) => {
264
213
  // Read version from package.json
265
214
  const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
@@ -6,6 +6,7 @@ import { spawn } from 'child_process';
6
6
  import sqlite3 from 'sqlite3';
7
7
  import { open } from 'sqlite';
8
8
  import crypto from 'crypto';
9
+ import { CURSOR_MODELS } from '../../shared/modelConstants.js';
9
10
 
10
11
  const router = express.Router();
11
12
 
@@ -33,7 +34,7 @@ router.get('/config', async (req, res) => {
33
34
  config: {
34
35
  version: 1,
35
36
  model: {
36
- modelId: "gpt-5",
37
+ modelId: CURSOR_MODELS.DEFAULT,
37
38
  displayName: "GPT-5"
38
39
  },
39
40
  permissions: {
Binary file