@siteboon/claude-code-ui 1.21.0 → 1.23.0

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.
@@ -56,4 +56,4 @@ Error generating stack: `+u.message+`
56
56
  * LICENSE.md file in the root directory of this source tree.
57
57
  *
58
58
  * @license MIT
59
- */const up="6";try{window.__reactRouterVersion=up}catch{}const ip="startTransition",Ha=od[ip];function dp(o){let{basename:f,children:a,future:y,window:g}=o,C=j.useRef();C.current==null&&(C.current=pd({window:g,v5Compat:!0}));let _=C.current,[T,P]=j.useState({action:_.action,location:_.location}),{v7_startTransition:U}=y||{},$=j.useCallback(N=>{U&&Ha?Ha(()=>P(N)):P(N)},[P,U]);return j.useLayoutEffect(()=>_.listen($),[_,$]),j.useEffect(()=>np(y),[y]),j.createElement(lp,{basename:f,children:a,location:T.location,navigationType:T.action,navigator:_,future:y})}var Qa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(Qa||(Qa={}));var Ka;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(Ka||(Ka={}));export{dp as B,id as R,j as a,fd as b,sp as c,op as d,cp as e,fp as f,Ya as g,rp as h,Xa as r,ap as u};
59
+ */const up="6";try{window.__reactRouterVersion=up}catch{}const ip="startTransition",Ha=od[ip];function dp(o){let{basename:f,children:a,future:y,window:g}=o,C=j.useRef();C.current==null&&(C.current=pd({window:g,v5Compat:!0}));let _=C.current,[T,P]=j.useState({action:_.action,location:_.location}),{v7_startTransition:U}=y||{},$=j.useCallback(N=>{U&&Ha?Ha(()=>P(N)):P(N)},[P,U]);return j.useLayoutEffect(()=>_.listen($),[_,$]),j.useEffect(()=>np(y),[y]),j.createElement(lp,{basename:f,children:a,location:T.location,navigationType:T.action,navigator:_,future:y})}var Qa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(Qa||(Qa={}));var Ka;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(Ka||(Ka={}));export{dp as B,sp as R,j as a,fd as b,id as c,op as d,cp as e,fp as f,Ya as g,rp as h,Xa as r,ap as u};
package/dist/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  <title>CloudCLI UI</title>
9
9
 
10
10
  <!-- PWA Manifest -->
11
- <link rel="manifest" href="/manifest.json" />
11
+ <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
12
12
 
13
13
  <!-- iOS Safari PWA Meta Tags -->
14
14
  <meta name="mobile-web-app-capable" content="yes" />
@@ -25,11 +25,11 @@
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-DN2ZJcRJ.js"></script>
29
- <link rel="modulepreload" crossorigin href="/assets/vendor-react-DIN4KjD2.js">
30
- <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-BMLq5tLB.js">
28
+ <script type="module" crossorigin src="/assets/index-7_J3n3lH.js"></script>
29
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-CdSTmIF1.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-C8f1vU1x.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
32
- <link rel="stylesheet" crossorigin href="/assets/index-Cxnz_sny.css">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-BFyod1Qa.css">
33
33
  </head>
34
34
  <body>
35
35
  <div id="root"></div>
@@ -49,4 +49,4 @@
49
49
  }
50
50
  </script>
51
51
  </body>
52
- </html>
52
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteboon/claude-code-ui",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -30,10 +30,13 @@
30
30
  "build": "vite build",
31
31
  "preview": "vite preview",
32
32
  "typecheck": "tsc --noEmit -p tsconfig.json",
33
+ "lint": "eslint src/",
34
+ "lint:fix": "eslint src/ --fix",
33
35
  "start": "npm run build && npm run server",
34
36
  "release": "./release.sh",
35
37
  "prepublishOnly": "npm run build",
36
- "postinstall": "node scripts/fix-node-pty.js"
38
+ "postinstall": "node scripts/fix-node-pty.js",
39
+ "prepare": "husky"
37
40
  },
38
41
  "keywords": [
39
42
  "claude code",
@@ -78,6 +81,7 @@
78
81
  "i18next": "^25.7.4",
79
82
  "i18next-browser-languagedetector": "^8.2.0",
80
83
  "jsonwebtoken": "^9.0.2",
84
+ "jszip": "^3.10.1",
81
85
  "katex": "^0.16.25",
82
86
  "lucide-react": "^0.515.0",
83
87
  "mime-types": "^3.0.1",
@@ -102,6 +106,9 @@
102
106
  "ws": "^8.14.2"
103
107
  },
104
108
  "devDependencies": {
109
+ "@commitlint/cli": "^20.4.3",
110
+ "@commitlint/config-conventional": "^20.4.3",
111
+ "@eslint/js": "^9.39.3",
105
112
  "@release-it/conventional-changelog": "^10.0.5",
106
113
  "@types/node": "^22.19.7",
107
114
  "@types/react": "^18.2.43",
@@ -110,12 +117,26 @@
110
117
  "auto-changelog": "^2.5.0",
111
118
  "autoprefixer": "^10.4.16",
112
119
  "concurrently": "^8.2.2",
120
+ "eslint": "^9.39.3",
121
+ "eslint-plugin-import-x": "^4.16.1",
122
+ "eslint-plugin-react": "^7.37.5",
123
+ "eslint-plugin-react-hooks": "^7.0.1",
124
+ "eslint-plugin-react-refresh": "^0.5.2",
125
+ "eslint-plugin-tailwindcss": "^3.18.2",
126
+ "eslint-plugin-unused-imports": "^4.4.1",
127
+ "globals": "^17.4.0",
128
+ "husky": "^9.1.7",
129
+ "lint-staged": "^16.3.2",
113
130
  "node-gyp": "^10.0.0",
114
131
  "postcss": "^8.4.32",
115
132
  "release-it": "^19.0.5",
116
133
  "sharp": "^0.34.2",
117
134
  "tailwindcss": "^3.4.0",
118
135
  "typescript": "^5.9.3",
136
+ "typescript-eslint": "^8.56.1",
119
137
  "vite": "^7.0.4"
138
+ },
139
+ "lint-staged": {
140
+ "src/**/*.{ts,tsx,js,jsx}": "eslint"
120
141
  }
121
142
  }
@@ -34,7 +34,7 @@ function createRequestId() {
34
34
  }
35
35
 
36
36
  function waitForToolApproval(requestId, options = {}) {
37
- const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options;
37
+ const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel, metadata } = options;
38
38
 
39
39
  return new Promise(resolve => {
40
40
  let settled = false;
@@ -78,9 +78,14 @@ function waitForToolApproval(requestId, options = {}) {
78
78
  signal.addEventListener('abort', abortHandler, { once: true });
79
79
  }
80
80
 
81
- pendingToolApprovals.set(requestId, (decision) => {
81
+ const resolver = (decision) => {
82
82
  finalize(decision);
83
- });
83
+ };
84
+ // Attach metadata for getPendingApprovalsForSession lookup
85
+ if (metadata) {
86
+ Object.assign(resolver, metadata);
87
+ }
88
+ pendingToolApprovals.set(requestId, resolver);
84
89
  });
85
90
  }
86
91
 
@@ -209,13 +214,14 @@ function mapCliOptionsToSDK(options = {}) {
209
214
  * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
210
215
  * @param {string} tempDir - Temp directory for cleanup
211
216
  */
212
- function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
217
+ function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, writer = null) {
213
218
  activeSessions.set(sessionId, {
214
219
  instance: queryInstance,
215
220
  startTime: Date.now(),
216
221
  status: 'active',
217
222
  tempImagePaths,
218
- tempDir
223
+ tempDir,
224
+ writer
219
225
  });
220
226
  }
221
227
 
@@ -512,6 +518,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
512
518
  const decision = await waitForToolApproval(requestId, {
513
519
  timeoutMs: requiresInteraction ? 0 : undefined,
514
520
  signal: context?.signal,
521
+ metadata: {
522
+ _sessionId: capturedSessionId || sessionId || null,
523
+ _toolName: toolName,
524
+ _input: input,
525
+ _receivedAt: new Date(),
526
+ },
515
527
  onCancel: (reason) => {
516
528
  ws.send({
517
529
  type: 'claude-permission-cancelled',
@@ -562,7 +574,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
562
574
 
563
575
  // Track the query instance for abort capability
564
576
  if (capturedSessionId) {
565
- addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
577
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
566
578
  }
567
579
 
568
580
  // Process streaming messages
@@ -572,7 +584,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
572
584
  if (message.session_id && !capturedSessionId) {
573
585
 
574
586
  capturedSessionId = message.session_id;
575
- addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
587
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
576
588
 
577
589
  // Set session ID on writer
578
590
  if (ws.setSessionId && typeof ws.setSessionId === 'function') {
@@ -593,9 +605,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
593
605
  console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
594
606
  }
595
607
 
596
- // logs which model was used in the message
597
- console.log("---> Model was sent using:", Object.keys(message.modelUsage || {}));
598
-
599
608
  // Transform and send message to WebSocket
600
609
  const transformedMessage = transformMessage(message);
601
610
  ws.send({
@@ -606,6 +615,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
606
615
 
607
616
  // Extract and send token budget updates from result messages
608
617
  if (message.type === 'result') {
618
+ const models = Object.keys(message.modelUsage || {});
619
+ if (models.length > 0) {
620
+ console.log("---> Model was sent using:", models);
621
+ }
609
622
  const tokenBudget = extractTokenBudget(message);
610
623
  if (tokenBudget) {
611
624
  console.log('Token budget from modelUsage:', tokenBudget);
@@ -711,11 +724,50 @@ function getActiveClaudeSDKSessions() {
711
724
  return getAllSessions();
712
725
  }
713
726
 
727
+ /**
728
+ * Get pending tool approvals for a specific session.
729
+ * @param {string} sessionId - The session ID
730
+ * @returns {Array} Array of pending permission request objects
731
+ */
732
+ function getPendingApprovalsForSession(sessionId) {
733
+ const pending = [];
734
+ for (const [requestId, resolver] of pendingToolApprovals.entries()) {
735
+ if (resolver._sessionId === sessionId) {
736
+ pending.push({
737
+ requestId,
738
+ toolName: resolver._toolName || 'UnknownTool',
739
+ input: resolver._input,
740
+ context: resolver._context,
741
+ sessionId,
742
+ receivedAt: resolver._receivedAt || new Date(),
743
+ });
744
+ }
745
+ }
746
+ return pending;
747
+ }
748
+
749
+ /**
750
+ * Reconnect a session's WebSocketWriter to a new raw WebSocket.
751
+ * Called when client reconnects (e.g. page refresh) while SDK is still running.
752
+ * @param {string} sessionId - The session ID
753
+ * @param {Object} newRawWs - The new raw WebSocket connection
754
+ * @returns {boolean} True if writer was successfully reconnected
755
+ */
756
+ function reconnectSessionWriter(sessionId, newRawWs) {
757
+ const session = getSession(sessionId);
758
+ if (!session?.writer?.updateWebSocket) return false;
759
+ session.writer.updateWebSocket(newRawWs);
760
+ console.log(`[RECONNECT] Writer swapped for session ${sessionId}`);
761
+ return true;
762
+ }
763
+
714
764
  // Export public API
715
765
  export {
716
766
  queryClaudeSDK,
717
767
  abortClaudeSDKSession,
718
768
  isClaudeSDKSessionActive,
719
769
  getActiveClaudeSDKSessions,
720
- resolveToolApproval
770
+ resolveToolApproval,
771
+ getPendingApprovalsForSession,
772
+ reconnectSessionWriter
721
773
  };
@@ -91,6 +91,18 @@ const runMigrations = () => {
91
91
  db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
92
92
  }
93
93
 
94
+ // Create session_names table if it doesn't exist (for existing installations)
95
+ db.exec(`CREATE TABLE IF NOT EXISTS session_names (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ session_id TEXT NOT NULL,
98
+ provider TEXT NOT NULL DEFAULT 'claude',
99
+ custom_name TEXT NOT NULL,
100
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
101
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
102
+ UNIQUE(session_id, provider)
103
+ )`);
104
+ db.exec('CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider)');
105
+
94
106
  console.log('Database migrations completed successfully');
95
107
  } catch (error) {
96
108
  console.error('Error running migrations:', error.message);
@@ -348,6 +360,60 @@ const credentialsDb = {
348
360
  }
349
361
  };
350
362
 
363
+ // Session custom names database operations
364
+ const sessionNamesDb = {
365
+ // Set (insert or update) a custom session name
366
+ setName: (sessionId, provider, customName) => {
367
+ db.prepare(`
368
+ INSERT INTO session_names (session_id, provider, custom_name)
369
+ VALUES (?, ?, ?)
370
+ ON CONFLICT(session_id, provider)
371
+ DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP
372
+ `).run(sessionId, provider, customName);
373
+ },
374
+
375
+ // Get a single custom session name
376
+ getName: (sessionId, provider) => {
377
+ const row = db.prepare(
378
+ 'SELECT custom_name FROM session_names WHERE session_id = ? AND provider = ?'
379
+ ).get(sessionId, provider);
380
+ return row?.custom_name || null;
381
+ },
382
+
383
+ // Batch lookup — returns Map<sessionId, customName>
384
+ getNames: (sessionIds, provider) => {
385
+ if (!sessionIds.length) return new Map();
386
+ const placeholders = sessionIds.map(() => '?').join(',');
387
+ const rows = db.prepare(
388
+ `SELECT session_id, custom_name FROM session_names
389
+ WHERE session_id IN (${placeholders}) AND provider = ?`
390
+ ).all(...sessionIds, provider);
391
+ return new Map(rows.map(r => [r.session_id, r.custom_name]));
392
+ },
393
+
394
+ // Delete a custom session name
395
+ deleteName: (sessionId, provider) => {
396
+ return db.prepare(
397
+ 'DELETE FROM session_names WHERE session_id = ? AND provider = ?'
398
+ ).run(sessionId, provider).changes > 0;
399
+ },
400
+ };
401
+
402
+ // Apply custom session names from the database (overrides CLI-generated summaries)
403
+ function applyCustomSessionNames(sessions, provider) {
404
+ if (!sessions?.length) return;
405
+ try {
406
+ const ids = sessions.map(s => s.id);
407
+ const customNames = sessionNamesDb.getNames(ids, provider);
408
+ for (const session of sessions) {
409
+ const custom = customNames.get(session.id);
410
+ if (custom) session.summary = custom;
411
+ }
412
+ } catch (error) {
413
+ console.warn(`[DB] Failed to apply custom session names for ${provider}:`, error.message);
414
+ }
415
+ }
416
+
351
417
  // Backward compatibility - keep old names pointing to new system
352
418
  const githubTokensDb = {
353
419
  createGithubToken: (userId, tokenName, githubToken, description = null) => {
@@ -373,5 +439,7 @@ export {
373
439
  userDb,
374
440
  apiKeysDb,
375
441
  credentialsDb,
442
+ sessionNamesDb,
443
+ applyCustomSessionNames,
376
444
  githubTokensDb // Backward compatibility
377
445
  };
@@ -49,4 +49,17 @@ CREATE TABLE IF NOT EXISTS user_credentials (
49
49
 
50
50
  CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
51
51
  CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
52
- CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
52
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
53
+
54
+ -- Session custom names (provider-agnostic display name overrides)
55
+ CREATE TABLE IF NOT EXISTS session_names (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ session_id TEXT NOT NULL,
58
+ provider TEXT NOT NULL DEFAULT 'claude',
59
+ custom_name TEXT NOT NULL,
60
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
61
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
62
+ UNIQUE(session_id, provider)
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider);