@siteboon/claude-code-ui 1.18.1 → 1.19.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.
package/dist/index.html CHANGED
@@ -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-CIy8of-s.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-E8FwAGHq.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DIN4KjD2.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-l-lAmaJ1.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DfaPXD3y.js">
32
- <link rel="stylesheet" crossorigin href="/assets/index-L7z5zikg.css">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-BPHfv_yU.css">
33
33
  </head>
34
34
  <body>
35
35
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteboon/claude-code-ui",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -12,6 +12,7 @@
12
12
  "server/",
13
13
  "shared/",
14
14
  "dist/",
15
+ "scripts/",
15
16
  "README.md"
16
17
  ],
17
18
  "homepage": "https://cloudcli.ai",
@@ -30,7 +31,8 @@
30
31
  "preview": "vite preview",
31
32
  "typecheck": "tsc --noEmit -p tsconfig.json",
32
33
  "start": "npm run build && npm run server",
33
- "release": "./release.sh"
34
+ "release": "./release.sh",
35
+ "postinstall": "node scripts/fix-node-pty.js"
34
36
  },
35
37
  "keywords": [
36
38
  "claude code",
@@ -98,6 +100,7 @@
98
100
  "ws": "^8.14.2"
99
101
  },
100
102
  "devDependencies": {
103
+ "@release-it/conventional-changelog": "^10.0.5",
101
104
  "@types/node": "^22.19.7",
102
105
  "@types/react": "^18.2.43",
103
106
  "@types/react-dom": "^18.2.17",
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fix node-pty spawn-helper permissions on macOS
4
+ *
5
+ * This script fixes a known issue with node-pty where the spawn-helper
6
+ * binary is shipped without execute permissions, causing "posix_spawnp failed" errors.
7
+ *
8
+ * @see https://github.com/microsoft/node-pty/issues/850
9
+ * @module scripts/fix-node-pty
10
+ */
11
+
12
+ import { promises as fs } from 'fs';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ /**
20
+ * Fixes the spawn-helper binary permissions for node-pty on macOS.
21
+ *
22
+ * The node-pty package ships the spawn-helper binary without execute permissions
23
+ * (644 instead of 755), which causes "posix_spawnp failed" errors when trying
24
+ * to spawn terminal processes.
25
+ *
26
+ * This function:
27
+ * 1. Checks if running on macOS (darwin)
28
+ * 2. Locates spawn-helper binaries for both arm64 and x64 architectures
29
+ * 3. Sets execute permissions (755) on each binary found
30
+ *
31
+ * @async
32
+ * @function fixSpawnHelper
33
+ * @returns {Promise<void>} Resolves when permissions are fixed or skipped
34
+ * @example
35
+ * // Run as postinstall script
36
+ * await fixSpawnHelper();
37
+ */
38
+ async function fixSpawnHelper() {
39
+ const nodeModulesPath = path.join(__dirname, '..', 'node_modules', 'node-pty', 'prebuilds');
40
+
41
+ // Only run on macOS
42
+ if (process.platform !== 'darwin') {
43
+ return;
44
+ }
45
+
46
+ const darwinDirs = ['darwin-arm64', 'darwin-x64'];
47
+
48
+ for (const dir of darwinDirs) {
49
+ const spawnHelperPath = path.join(nodeModulesPath, dir, 'spawn-helper');
50
+
51
+ try {
52
+ // Check if file exists
53
+ await fs.access(spawnHelperPath);
54
+
55
+ // Make it executable (755)
56
+ await fs.chmod(spawnHelperPath, 0o755);
57
+ console.log(`[postinstall] Fixed permissions for ${spawnHelperPath}`);
58
+ } catch (err) {
59
+ // File doesn't exist or other error - ignore
60
+ if (err.code !== 'ENOENT') {
61
+ console.warn(`[postinstall] Warning: Could not fix ${spawnHelperPath}: ${err.message}`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ fixSpawnHelper().catch(console.error);
@@ -250,7 +250,13 @@ function getAllSessions() {
250
250
  * @returns {Object} Transformed message ready for WebSocket
251
251
  */
252
252
  function transformMessage(sdkMessage) {
253
- // Pass-through; SDK messages match frontend format.
253
+ // Extract parent_tool_use_id for subagent tool grouping
254
+ if (sdkMessage.parent_tool_use_id) {
255
+ return {
256
+ ...sdkMessage,
257
+ parentToolUseId: sdkMessage.parent_tool_use_id
258
+ };
259
+ }
254
260
  return sdkMessage;
255
261
  }
256
262
 
Binary file
package/server/index.js CHANGED
@@ -1886,6 +1886,9 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
1886
1886
  }
1887
1887
 
1888
1888
  const PORT = process.env.PORT || 3001;
1889
+ const HOST = process.env.HOST || '0.0.0.0';
1890
+ // Show localhost in URL when binding to all interfaces (0.0.0.0 isn't a connectable address)
1891
+ const DISPLAY_HOST = HOST === '0.0.0.0' ? 'localhost' : HOST;
1889
1892
 
1890
1893
  // Initialize database and start server
1891
1894
  async function startServer() {
@@ -1905,7 +1908,7 @@ async function startServer() {
1905
1908
  console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
1906
1909
  }
1907
1910
 
1908
- server.listen(PORT, '0.0.0.0', async () => {
1911
+ server.listen(PORT, HOST, async () => {
1909
1912
  const appInstallPath = path.join(__dirname, '..');
1910
1913
 
1911
1914
  console.log('');
@@ -1913,7 +1916,7 @@ async function startServer() {
1913
1916
  console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
1914
1917
  console.log(c.dim('═'.repeat(63)));
1915
1918
  console.log('');
1916
- console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://0.0.0.0:' + PORT)}`);
1919
+ console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + PORT)}`);
1917
1920
  console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
1918
1921
  console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
1919
1922
  console.log('');
@@ -889,22 +889,81 @@ async function parseJsonlSessions(filePath) {
889
889
  }
890
890
  }
891
891
 
892
+ // Parse an agent JSONL file and extract tool uses
893
+ async function parseAgentTools(filePath) {
894
+ const tools = [];
895
+
896
+ try {
897
+ const fileStream = fsSync.createReadStream(filePath);
898
+ const rl = readline.createInterface({
899
+ input: fileStream,
900
+ crlfDelay: Infinity
901
+ });
902
+
903
+ for await (const line of rl) {
904
+ if (line.trim()) {
905
+ try {
906
+ const entry = JSON.parse(line);
907
+ // Look for assistant messages with tool_use
908
+ if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
909
+ for (const part of entry.message.content) {
910
+ if (part.type === 'tool_use') {
911
+ tools.push({
912
+ toolId: part.id,
913
+ toolName: part.name,
914
+ toolInput: part.input,
915
+ timestamp: entry.timestamp
916
+ });
917
+ }
918
+ }
919
+ }
920
+ // Look for tool results
921
+ if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
922
+ for (const part of entry.message.content) {
923
+ if (part.type === 'tool_result') {
924
+ // Find the matching tool and add result
925
+ const tool = tools.find(t => t.toolId === part.tool_use_id);
926
+ if (tool) {
927
+ tool.toolResult = {
928
+ content: typeof part.content === 'string' ? part.content :
929
+ Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
930
+ JSON.stringify(part.content),
931
+ isError: Boolean(part.is_error)
932
+ };
933
+ }
934
+ }
935
+ }
936
+ }
937
+ } catch (parseError) {
938
+ // Skip malformed lines
939
+ }
940
+ }
941
+ }
942
+ } catch (error) {
943
+ console.warn(`Error parsing agent file ${filePath}:`, error.message);
944
+ }
945
+
946
+ return tools;
947
+ }
948
+
892
949
  // Get messages for a specific session with pagination support
893
950
  async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
894
951
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
895
952
 
896
953
  try {
897
954
  const files = await fs.readdir(projectDir);
898
- // agent-*.jsonl files contain session start data at this point. This needs to be revisited
899
- // periodically to make sure only accurate data is there and no new functionality is added there
955
+ // agent-*.jsonl files contain subagent tool history - we'll process them separately
900
956
  const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
901
-
957
+ const agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
958
+
902
959
  if (jsonlFiles.length === 0) {
903
960
  return { messages: [], total: 0, hasMore: false };
904
961
  }
905
-
962
+
906
963
  const messages = [];
907
-
964
+ // Map of agentId -> tools for subagent tool grouping
965
+ const agentToolsCache = new Map();
966
+
908
967
  // Process all JSONL files to find messages for this session
909
968
  for (const file of jsonlFiles) {
910
969
  const jsonlFile = path.join(projectDir, file);
@@ -913,7 +972,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
913
972
  input: fileStream,
914
973
  crlfDelay: Infinity
915
974
  });
916
-
975
+
917
976
  for await (const line of rl) {
918
977
  if (line.trim()) {
919
978
  try {
@@ -927,26 +986,55 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
927
986
  }
928
987
  }
929
988
  }
930
-
989
+
990
+ // Collect agentIds from Task tool results
991
+ const agentIds = new Set();
992
+ for (const message of messages) {
993
+ if (message.toolUseResult?.agentId) {
994
+ agentIds.add(message.toolUseResult.agentId);
995
+ }
996
+ }
997
+
998
+ // Load agent tools for each agentId found
999
+ for (const agentId of agentIds) {
1000
+ const agentFileName = `agent-${agentId}.jsonl`;
1001
+ if (agentFiles.includes(agentFileName)) {
1002
+ const agentFilePath = path.join(projectDir, agentFileName);
1003
+ const tools = await parseAgentTools(agentFilePath);
1004
+ agentToolsCache.set(agentId, tools);
1005
+ }
1006
+ }
1007
+
1008
+ // Attach agent tools to their parent Task messages
1009
+ for (const message of messages) {
1010
+ if (message.toolUseResult?.agentId) {
1011
+ const agentId = message.toolUseResult.agentId;
1012
+ const agentTools = agentToolsCache.get(agentId);
1013
+ if (agentTools && agentTools.length > 0) {
1014
+ message.subagentTools = agentTools;
1015
+ }
1016
+ }
1017
+ }
1018
+
931
1019
  // Sort messages by timestamp
932
- const sortedMessages = messages.sort((a, b) =>
1020
+ const sortedMessages = messages.sort((a, b) =>
933
1021
  new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
934
1022
  );
935
-
1023
+
936
1024
  const total = sortedMessages.length;
937
-
1025
+
938
1026
  // If no limit is specified, return all messages (backward compatibility)
939
1027
  if (limit === null) {
940
1028
  return sortedMessages;
941
1029
  }
942
-
1030
+
943
1031
  // Apply pagination - for recent messages, we need to slice from the end
944
1032
  // offset 0 should give us the most recent messages
945
1033
  const startIndex = Math.max(0, total - offset - limit);
946
1034
  const endIndex = total - offset;
947
1035
  const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
948
1036
  const hasMore = startIndex > 0;
949
-
1037
+
950
1038
  return {
951
1039
  messages: paginatedMessages,
952
1040
  total,