@ian2018cs/agenthub 0.1.78 → 0.1.80

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-NaMmXkCt.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-ByqBXYb8.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-Bv0Nkan8.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-sVRjxPVQ.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian2018cs/agenthub",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "description": "A web-based UI for AI Agents",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -760,6 +760,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
760
760
 
761
761
  // Transform and send message to WebSocket
762
762
  const transformedMessage = transformMessage(message);
763
+
764
+ // 为内置工具拦截的 tool_result 打 _builtinHandled 标记,前端据此不显示错误样式
765
+ if (transformedMessage.message?.role === 'user' && Array.isArray(transformedMessage.message?.content)) {
766
+ for (const part of transformedMessage.message.content) {
767
+ if (part.type === 'tool_result' && part.is_error && part.tool_use_id) {
768
+ if (builtinTools.consumeBuiltinHandled(part.tool_use_id)) {
769
+ part._builtinHandled = true;
770
+ }
771
+ }
772
+ }
773
+ }
774
+
763
775
  mutableWriter.send({
764
776
  type: 'claude-response',
765
777
  data: transformedMessage
package/server/index.js CHANGED
@@ -924,11 +924,14 @@ function handleChatConnection(ws, userData) {
924
924
  }
925
925
  }
926
926
 
927
- // Query 结束后,尝试投递待处理的后台任务结果
927
+ // Query 结束后,延迟投递待处理的后台任务结果
928
+ // 必须用 setTimeout 与 claude-complete 消息错开,避免两条 WS 消息
929
+ // 在同一事件循环 tick 内发出,导致前端 React 批量合并渲染时
930
+ // claude-complete 被跳过、isLoading 永久为 true 的竞态问题。
928
931
  {
929
932
  const sid = data.options?.sessionId || writer.getSessionId();
930
933
  if (userUuid && sid) {
931
- tryDeliverBgResult(userUuid, sid);
934
+ setTimeout(() => tryDeliverBgResult(userUuid, sid), 100);
932
935
  }
933
936
  }
934
937
  } else if (data.type === 'abort-session') {
@@ -347,6 +347,47 @@ async function ensureSharedGitRepo(repoUrl, branch) {
347
347
  return targetPath;
348
348
  }
349
349
 
350
+ /**
351
+ * Try to move an existing local git clone directly into the shared location,
352
+ * avoiding a fresh network clone. Only succeeds when:
353
+ * - localPath is a real directory (not a symlink) with a .git folder
354
+ * - The shared target path does not yet exist
355
+ * Returns the shared path on success, or null if promotion is not possible.
356
+ */
357
+ async function tryPromoteLocalRepoToShared(localPath, repoUrl, branch) {
358
+ const parsed = parseGitUrl(repoUrl);
359
+ if (!parsed) return null;
360
+
361
+ const publicPaths = getPublicPaths();
362
+ const targetPath = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);
363
+
364
+ // Shared repo already exists — nothing to promote
365
+ try {
366
+ await fs.access(targetPath);
367
+ return null;
368
+ } catch {}
369
+
370
+ // Source must be a real git directory (not a symlink, must have .git)
371
+ try {
372
+ const stat = await fs.lstat(localPath);
373
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
374
+ await fs.access(path.join(localPath, '.git'));
375
+ } catch {
376
+ return null;
377
+ }
378
+
379
+ try {
380
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
381
+ await fs.rename(localPath, targetPath);
382
+ console.log(`[GitRepo] Promoted local repo to shared: ${localPath} → ${targetPath}`);
383
+ return targetPath;
384
+ } catch (e) {
385
+ // rename may fail on cross-device links — fall back to normal clone
386
+ console.warn(`[GitRepo] Could not promote local repo (will clone instead): ${e.message}`);
387
+ return null;
388
+ }
389
+ }
390
+
350
391
  /**
351
392
  * Create a symlink from a project subdirectory to a shared git repo.
352
393
  * Removes existing directory or symlink at the target location.
@@ -513,6 +554,7 @@ async function scanClaudeMdRefs(projectPath) {
513
554
  if (!ref) continue;
514
555
  if (/^https?:\/\/|^mailto:|^\/\//.test(ref)) continue; // skip absolute URLs
515
556
  if (path.isAbsolute(ref)) continue; // skip absolute paths
557
+ if (ref.endsWith('/')) continue; // skip directory references like [src/utils/]
516
558
  if (seen.has(ref)) continue;
517
559
  seen.add(ref);
518
560
 
@@ -1530,8 +1572,22 @@ router.post('/submissions/:id/approve', async (req, res) => {
1530
1572
  for (const gitRepo of yamlGitRepos) {
1531
1573
  if (!gitRepo.name || !gitRepo.repo) continue;
1532
1574
  try {
1533
- // Ensure shared git repo exists
1534
- const sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
1575
+ // Try to promote the submitter's existing local clone to shared location first
1576
+ // (avoids a full network re-clone if the user already has the repo cloned)
1577
+ let sharedPath = null;
1578
+ if (submitterUuid) {
1579
+ const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
1580
+ const localRepoPath = path.join(submitterProjectDir, gitRepo.name);
1581
+ sharedPath = await tryPromoteLocalRepoToShared(localRepoPath, gitRepo.repo, gitRepo.branch || 'main');
1582
+ if (sharedPath) {
1583
+ console.log(`[AgentApprove] Promoted local repo "${gitRepo.name}" to shared (no re-clone needed)`);
1584
+ }
1585
+ }
1586
+
1587
+ // Fall back to normal clone/update if promotion was not possible
1588
+ if (!sharedPath) {
1589
+ sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
1590
+ }
1535
1591
  console.log(`[AgentApprove] Ensured shared git repo "${gitRepo.name}" at ${sharedPath}`);
1536
1592
 
1537
1593
  // Replace submitter's original subdirectory with symlink to shared repo
@@ -124,6 +124,21 @@ __bg_status__ bg_xxxxxxxxxxxx
124
124
  return { decision: 'deny', reason: `后台任务提交失败: ${err.message}` };
125
125
  }
126
126
 
127
+ // sessionId 可能在新会话的第一个 turn 内尚未建立(getSessionId() 返回 null)。
128
+ // 轮询直到 session-created 事件触发后 mutableWriter 持有真实 sessionId,
129
+ // 避免任务完成时以 null sessionId 投递导致前端无法匹配。
130
+ if (!task.sessionId) {
131
+ const pollTimer = setInterval(() => {
132
+ const sid = mutableWriter.getSessionId();
133
+ if (sid) {
134
+ task.sessionId = sid;
135
+ clearInterval(pollTimer);
136
+ }
137
+ }, 200);
138
+ // 最多等 60 秒,之后放弃(任务完成时 sessionId 仍为 null 则依赖前端兜底匹配)
139
+ setTimeout(() => clearInterval(pollTimer), 60_000);
140
+ }
141
+
127
142
  const timeoutSec = Math.round(task.timeout / 1000);
128
143
  return {
129
144
  decision: 'deny',
@@ -31,6 +31,8 @@ class BuiltinToolRegistry {
31
31
  #tools = [];
32
32
  #responseTypes = new Set();
33
33
  #pendingRequests = new Map();
34
+ /** tool_use_id → true,记录被内置工具拦截处理的工具调用(deny 即成功)*/
35
+ #builtinHandledIds = new Set();
34
36
 
35
37
  /**
36
38
  * 注册一个内置工具
@@ -67,6 +69,10 @@ class BuiltinToolRegistry {
67
69
 
68
70
  try {
69
71
  const result = await tool.execute(hookInput, toolContext);
72
+ // deny 作为正常拦截响应时,记录 tool_use_id 供后续标记
73
+ if (result.decision === 'deny' && hookInput.tool_use_id) {
74
+ this.#builtinHandledIds.add(hookInput.tool_use_id);
75
+ }
70
76
  return {
71
77
  hookSpecificOutput: {
72
78
  hookEventName: 'PreToolUse',
@@ -89,6 +95,18 @@ class BuiltinToolRegistry {
89
95
  return null;
90
96
  }
91
97
 
98
+ /**
99
+ * 检查某个 tool_use_id 是否由内置工具拦截处理(deny = 成功)
100
+ * 调用后自动从集合中移除(一次性消费)
101
+ */
102
+ consumeBuiltinHandled(toolUseId) {
103
+ if (this.#builtinHandledIds.has(toolUseId)) {
104
+ this.#builtinHandledIds.delete(toolUseId);
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+
92
110
  /**
93
111
  * 检查 WS 消息类型是否属于内置工具的响应
94
112
  */