loadtoagent 1.0.0 → 1.3.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.
Files changed (86) hide show
  1. package/README.ko.md +7 -0
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +23 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +183 -37
  9. package/package.json +15 -4
  10. package/preload.js +11 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -9
  13. package/renderer/app-dashboard.js +197 -58
  14. package/renderer/app-drawer-content.js +103 -86
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +79 -55
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -9
  19. package/renderer/app-events-navigation.js +78 -31
  20. package/renderer/app-events-sessions.js +171 -4
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +32 -4
  23. package/renderer/app-graph-orchestration.js +16 -13
  24. package/renderer/app-graph-view.js +228 -112
  25. package/renderer/app-management.js +211 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +103 -33
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +119 -38
  32. package/renderer/app.js +155 -76
  33. package/renderer/i18n-messages.js +833 -15
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +162 -75
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +50 -1
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-collaboration.css +18 -88
  40. package/renderer/styles-components.css +126 -14
  41. package/renderer/styles-management.css +355 -0
  42. package/renderer/styles-overlays.css +13 -2
  43. package/renderer/styles-product.css +24 -16
  44. package/renderer/styles-quality.css +312 -0
  45. package/renderer/styles-readability.css +862 -0
  46. package/renderer/styles-responsive-product.css +84 -12
  47. package/renderer/styles-responsive-runtime.css +31 -14
  48. package/renderer/styles-responsive-shell.css +44 -48
  49. package/renderer/styles-responsive-workflows.css +37 -17
  50. package/renderer/styles-run-composer.css +5 -0
  51. package/renderer/styles-runtime-overview.css +285 -0
  52. package/renderer/styles-settings.css +160 -0
  53. package/renderer/styles-terminal.css +339 -2
  54. package/renderer/styles-tmux.css +154 -0
  55. package/renderer/styles-workflow-map.css +362 -15
  56. package/renderer/styles-workflows.css +69 -2
  57. package/renderer/styles.css +27 -12
  58. package/renderer/terminal-agent.js +17 -13
  59. package/renderer/terminal-events.js +233 -40
  60. package/renderer/terminal-workbench.js +176 -78
  61. package/renderer/terminal.js +350 -37
  62. package/src/agentMonitor/claudeParser.js +96 -7
  63. package/src/agentMonitor/codexParser.js +59 -4
  64. package/src/agentMonitor/executionActivity.js +282 -0
  65. package/src/agentMonitor/genericParser.js +56 -13
  66. package/src/agentMonitor/hierarchy.js +17 -0
  67. package/src/agentMonitor/responseIntent.js +51 -0
  68. package/src/agentMonitor.js +21 -3
  69. package/src/agentRunner.js +66 -1
  70. package/src/attentionNotifier.js +71 -0
  71. package/src/automationMonitor.js +258 -0
  72. package/src/contracts.js +10 -1
  73. package/src/ipc/registerAgentIpc.js +9 -3
  74. package/src/ipc/registerAppIpc.js +4 -1
  75. package/src/ipc/registerTerminalIpc.js +12 -6
  76. package/src/macUpdateHelper.js +210 -0
  77. package/src/monitorWorker.js +65 -6
  78. package/src/platformPath.js +80 -0
  79. package/src/processMonitor.js +81 -23
  80. package/src/providerVisibilityStore.js +45 -0
  81. package/src/sessionIntelligence.js +247 -0
  82. package/src/terminalHost.js +381 -0
  83. package/src/terminalHostDaemon.js +60 -0
  84. package/src/terminalManager.js +158 -3
  85. package/src/tmuxMonitor.js +2 -2
  86. package/src/updateInstaller.js +175 -0
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
+ const { createExecutionTracker } = require('./executionActivity');
4
5
 
5
6
  function createClaudeParser(dependencies) {
6
7
  const {
@@ -18,6 +19,8 @@ function createClaudeParser(dependencies) {
18
19
  sumUsage,
19
20
  timestamp,
20
21
  trimSession,
22
+ assistantRequestsUserResponse,
23
+ isUserInputTool,
21
24
  } = dependencies;
22
25
 
23
26
  function normalizeUsage(raw = {}) {
@@ -30,18 +33,27 @@ function createClaudeParser(dependencies) {
30
33
  });
31
34
  }
32
35
 
36
+ function utilityKind(value) {
37
+ const raw = compactText(value, 12000);
38
+ if (/^Extract durable memory candidates from this Claude Code transcript tail/i.test(raw)
39
+ || /^You are a memory extraction/i.test(raw)) return 'memory-extraction';
40
+ if (/^Reply with exactly OK\. Do not use tools\.?$/i.test(raw)) return 'authentication-check';
41
+ if (/^Approved command prefix saved:/i.test(raw)) return 'command-approval';
42
+ return '';
43
+ }
44
+
33
45
  function visibleUserText(value) {
34
46
  const raw = compactText(value, 12000);
35
47
  if (!raw) return '';
36
48
  const objective = raw.match(/<objective>\s*([\s\S]*?)\s*<\/objective>/i);
37
49
  if (objective) return compactText(objective[1], 6000);
38
50
  if (/^<(?:local-command-[^>]+|command-name|command-message|system-reminder|task-notification)>/i.test(raw)) return '';
39
- if (/^Extract durable memory candidates from this Claude Code transcript tail/i.test(raw)) return '';
51
+ if (utilityKind(raw)) return '';
40
52
  if (/^(?:Updated task #\d+|Your questions have been answered:)/i.test(raw)) return '';
41
53
  return raw;
42
54
  }
43
55
 
44
- function recordContent(session, row, item, index) {
56
+ function recordContent(session, state, row, item, index) {
45
57
  const kind = item && item.type;
46
58
  const id = `${row.uuid || row.requestId || session.externalId}:${index}`;
47
59
  if (kind === 'text' && item.text) {
@@ -49,6 +61,10 @@ function createClaudeParser(dependencies) {
49
61
  if (text) addMessage(session, { id, role: row.message.role, text, timestamp: row.timestamp });
50
62
  } else if (kind === 'tool_use') {
51
63
  const name = item.name || 'tool';
64
+ const callId = item.id || id;
65
+ const args = item.input && typeof item.input === 'object' ? item.input : {};
66
+ state.toolCalls.set(String(callId), { name, args });
67
+ state.executionTracker.recordCall({ name, callId, args, rawInput: item.input, at: row.timestamp });
52
68
  addMessage(session, {
53
69
  id,
54
70
  role: 'tool',
@@ -68,6 +84,15 @@ function createClaudeParser(dependencies) {
68
84
  timestamp: row.timestamp,
69
85
  });
70
86
  } else if (kind === 'tool_result') {
87
+ const call = state.toolCalls.get(String(item.tool_use_id || ''));
88
+ state.executionTracker.recordOutput({
89
+ name: call && call.name,
90
+ callId: item.tool_use_id,
91
+ args: call && call.args || {},
92
+ output: item.content || item,
93
+ at: row.timestamp,
94
+ isError: item.is_error === true,
95
+ });
71
96
  settleLifecycle(session, item.tool_use_id, item.is_error ? 'failed' : 'done', row.timestamp);
72
97
  addLifecycle(session, {
73
98
  id: `result:${item.tool_use_id || id}`,
@@ -106,19 +131,47 @@ function createClaudeParser(dependencies) {
106
131
 
107
132
  function processMessageRow(session, state, row) {
108
133
  const role = row.message.role === 'assistant' ? 'assistant' : 'user';
134
+ const internalUserRow = role === 'user' && Boolean(row.isMeta || row.sourceToolUseID);
109
135
  state.lastRole = role;
110
136
  if (row.message.model) session.model = row.message.model;
111
137
  const content = Array.isArray(row.message.content)
112
138
  ? row.message.content
113
139
  : [{ type: 'text', text: row.message.content }];
114
- content.forEach((item, index) => recordContent(session, row, item, index));
140
+ content.forEach((item, index) => {
141
+ if (internalUserRow && (!item.type || item.type === 'text')) return;
142
+ recordContent(session, state, row, item, index);
143
+ });
115
144
  if (role === 'user') {
116
- const visibleUser = visibleUserText(content
145
+ content.filter(item => item && item.type === 'tool_result')
146
+ .forEach(item => state.pendingUserInputCalls.delete(String(item.tool_use_id || '')));
147
+ const rawUser = content
117
148
  .filter(item => !item.type || item.type === 'text')
118
149
  .map(item => typeof item === 'string' ? item : item.text)
119
150
  .filter(Boolean)
120
- .join('\n'));
121
- if (visibleUser) state.latestUser = visibleUser;
151
+ .join('\n');
152
+ if (!internalUserRow) {
153
+ const detectedUtility = utilityKind(rawUser);
154
+ if (detectedUtility) session.utilityKind = detectedUtility;
155
+ const visibleUser = visibleUserText(rawUser);
156
+ if (visibleUser) {
157
+ session.utilityKind = '';
158
+ state.latestUser = visibleUser;
159
+ state.lastConversationRole = 'user';
160
+ }
161
+ }
162
+ }
163
+ if (role === 'assistant') {
164
+ const assistantText = compactText(content
165
+ .filter(item => item && item.type === 'text')
166
+ .map(item => item.text)
167
+ .filter(Boolean)
168
+ .join('\n'), 6000);
169
+ if (assistantText) {
170
+ state.lastAssistantText = assistantText;
171
+ state.lastConversationRole = 'assistant';
172
+ }
173
+ content.filter(item => item && item.type === 'tool_use' && isUserInputTool(item.name))
174
+ .forEach(item => state.pendingUserInputCalls.add(String(item.id || item.name)));
122
175
  }
123
176
  if (role === 'assistant' && row.message.usage) {
124
177
  const key = row.requestId || row.message.id || row.uuid;
@@ -134,14 +187,38 @@ function createClaudeParser(dependencies) {
134
187
  requestUsage: new Map(),
135
188
  latestUser: '',
136
189
  lastRole: '',
190
+ lastConversationRole: '',
191
+ lastAssistantText: '',
192
+ pendingUserInputCalls: new Set(),
193
+ toolCalls: new Map(),
194
+ executionTracker: createExecutionTracker({ compactText, timestamp }),
137
195
  latestTs: session.updatedAt,
138
196
  lastTurnFinished: false,
139
197
  };
140
198
  for (const row of rows) {
141
199
  state.latestTs = timestamp(row.timestamp, state.latestTs);
200
+ if (row.cwd && !session.originCwd) session.originCwd = row.cwd;
142
201
  if (row.cwd && !session.cwd) session.cwd = row.cwd;
143
202
  if (row.gitBranch) session.branch = row.gitBranch;
144
203
  if (row.agentId && session.depth) session.agentName = row.agentId;
204
+ if (row.type === 'queue-operation' && row.operation === 'enqueue' && row.content) {
205
+ const detectedUtility = utilityKind(row.content);
206
+ if (detectedUtility) session.utilityKind = detectedUtility;
207
+ const visibleUser = visibleUserText(row.content);
208
+ const queueTitle = /^\//.test(visibleUser)
209
+ ? compactText(visibleUser.split(/\r?\n/)[0], 6000)
210
+ : visibleUser;
211
+ if (queueTitle) {
212
+ session.utilityKind = '';
213
+ state.latestUser = queueTitle;
214
+ state.lastRole = 'user';
215
+ state.lastConversationRole = 'user';
216
+ }
217
+ }
218
+ if (row.type === 'last-prompt' && !state.latestUser && row.lastPrompt) {
219
+ const visibleUser = visibleUserText(row.lastPrompt);
220
+ if (visibleUser) state.latestUser = visibleUser;
221
+ }
145
222
  if (row.type === 'system' && row.subtype === 'init') {
146
223
  session.model = row.model || session.model;
147
224
  addLifecycle(session, {
@@ -164,13 +241,25 @@ function createClaudeParser(dependencies) {
164
241
  session.updatedAt = state.latestTs;
165
242
  session.startedAt = timestamp(parsed.rows[0].timestamp, session.updatedAt);
166
243
  session.usage = sumUsage([...state.requestUsage.values()]);
244
+ session.executions = state.executionTracker.finalize();
245
+ const utilityTitle = session.utilityKind === 'memory-extraction'
246
+ ? 'Claude 백그라운드 메모리 추출'
247
+ : (session.utilityKind === 'authentication-check' ? 'Claude 인증 점검' : '');
167
248
  session.title = compactText(state.latestUser, 180)
249
+ || utilityTitle
168
250
  || (session.depth ? `Claude ${session.agentName}` : 'Claude 세션');
169
251
  const currentInput = session.turnUsage.input + session.turnUsage.cachedInput
170
252
  + session.turnUsage.cacheWrite + session.turnUsage.output + session.turnUsage.reasoning;
171
253
  session.context = contextInfo(currentInput, modelContextWindow('claude', session.model, 0));
172
254
  const age = Date.now() - fileInfo.mtimeMs;
173
- if (age < ACTIVE_THRESHOLD_MS && !state.lastTurnFinished) {
255
+ const pendingUserInput = state.pendingUserInputCalls.size > 0;
256
+ const conversationalInput = state.lastConversationRole === 'assistant'
257
+ && assistantRequestsUserResponse(state.lastAssistantText)
258
+ && (state.lastTurnFinished || age >= ACTIVE_THRESHOLD_MS);
259
+ if (!session.depth && (pendingUserInput || conversationalInput)) {
260
+ session.status = 'waiting';
261
+ session.statusDetail = pendingUserInput ? '선택 또는 입력 대기' : '답변 또는 선택 대기';
262
+ } else if (age < ACTIVE_THRESHOLD_MS && !state.lastTurnFinished) {
174
263
  session.status = 'running';
175
264
  session.statusDetail = state.lastRole === 'user' ? '응답 생성 중' : '도구 실행 또는 스트리밍 중';
176
265
  } else if (state.lastRole === 'user' && age < STALE_TURN_THRESHOLD_MS) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const path = require('path');
4
4
  const { createCodexCollaboration } = require('./codexCollaboration');
5
+ const { createExecutionTracker } = require('./executionActivity');
5
6
 
6
7
  const COLLABORATION_TOOLS = new Set([
7
8
  'spawn_agent',
@@ -26,10 +27,12 @@ function createCodexParser(dependencies) {
26
27
  },
27
28
  textOps: {
28
29
  agentEnvelope,
30
+ assistantRequestsUserResponse,
29
31
  codexContentText,
30
32
  codexVisibleUserText,
31
33
  compactText,
32
34
  encryptedCollaborationText,
35
+ isUserInputTool,
33
36
  jsonObject,
34
37
  },
35
38
  collaborationOps: {
@@ -52,6 +55,7 @@ function createCodexParser(dependencies) {
52
55
  const session = baseSession('codex', externalId, fileInfo.file, fileInfo);
53
56
  session.truncated = parsed.truncated;
54
57
  session.cwd = meta.cwd || '';
58
+ session.originCwd = meta.cwd || '';
55
59
  session.model = meta.model || '';
56
60
  session.branch = meta.git && meta.git.branch || '';
57
61
  session.startedAt = timestamp(meta.timestamp || (metaRow && metaRow.timestamp), session.updatedAt);
@@ -82,14 +86,19 @@ function createCodexParser(dependencies) {
82
86
  return {
83
87
  latestUser: '',
84
88
  latestInternalGoal: '',
89
+ goalContexts: new Set(),
85
90
  latestDelegationNarration: '',
86
91
  activeTurn: false,
87
92
  lastTurnCompleted: false,
88
93
  lastFinalAnswer: '',
94
+ lastAssistantText: '',
95
+ lastConversationRole: '',
96
+ pendingUserInputCalls: new Set(),
89
97
  latestTs: session.updatedAt,
90
98
  observedWindow: Number(meta.context_window || 0),
91
99
  messageObservations: new Map(),
92
100
  collaboration: createCodexCollaboration(session, { compactText, timestamp }),
101
+ executionTracker: createExecutionTracker({ compactText, timestamp }),
93
102
  sessionStartedMs: Date.parse(session.startedAt || '') || 0,
94
103
  };
95
104
  }
@@ -153,7 +162,11 @@ function createCodexParser(dependencies) {
153
162
  settleRunningLifecycle(session, payload.completed_at || row.timestamp);
154
163
  session.completedAt = timestamp(payload.completed_at || row.timestamp, session.updatedAt);
155
164
  session.completionObserved = true;
156
- if (payload.last_agent_message) state.lastFinalAnswer = compactText(payload.last_agent_message, 6000);
165
+ if (payload.last_agent_message) {
166
+ state.lastFinalAnswer = compactText(payload.last_agent_message, 6000);
167
+ state.lastAssistantText = state.lastFinalAnswer;
168
+ state.lastConversationRole = 'assistant';
169
+ }
157
170
  addLifecycle(session, {
158
171
  id: payload.turn_id,
159
172
  type: 'turn-complete',
@@ -170,15 +183,21 @@ function createCodexParser(dependencies) {
170
183
  if (!text) return;
171
184
  if (/<codex_internal_context(?:\s|>)/i.test(rawUser)) {
172
185
  state.latestInternalGoal = text;
186
+ if (/<codex_internal_context[^>]*\bsource=["']goal["']/i.test(rawUser)) state.goalContexts.add(`${row.timestamp || ''}:${text}`);
173
187
  return;
174
188
  }
175
189
  state.latestUser = text;
190
+ state.lastConversationRole = 'user';
176
191
  const key = `u:${payload.client_id || row.timestamp}:${text}`;
177
192
  addCodexMessage(session, state.messageObservations, { id: key, role: 'user', text, timestamp: row.timestamp }, 'event');
178
193
  } else if (payload.type === 'agent_message') {
179
194
  const text = compactText(payload.message);
180
195
  if (text) state.latestDelegationNarration = text;
181
196
  if (payload.phase === 'final_answer') state.lastFinalAnswer = text;
197
+ if (text) {
198
+ state.lastAssistantText = text;
199
+ state.lastConversationRole = 'assistant';
200
+ }
182
201
  const key = `a:${row.timestamp}:${text}`;
183
202
  addCodexMessage(session, state.messageObservations, { id: key, role: 'assistant', text, timestamp: row.timestamp }, 'event');
184
203
  } else if (payload.type === 'agent_reasoning') {
@@ -264,6 +283,14 @@ function createCodexParser(dependencies) {
264
283
  const collaborationTool = payload.namespace === 'collaboration' || COLLABORATION_TOOLS.has(name);
265
284
  if (collaborationTool && !timing.ownCollaborationRow) return;
266
285
  state.collaboration.calls.set(String(callId || ''), { name, args, timestamp: row.timestamp });
286
+ state.executionTracker.recordCall({
287
+ name,
288
+ callId,
289
+ args,
290
+ rawInput: payload.arguments || payload.input,
291
+ at: row.timestamp,
292
+ });
293
+ if (isUserInputTool(name)) state.pendingUserInputCalls.add(String(callId || name));
267
294
 
268
295
  let collaborationMessageProtected = false;
269
296
  if (collaborationTool) {
@@ -299,6 +326,15 @@ function createCodexParser(dependencies) {
299
326
  function processToolOutput(session, state, row, payload) {
300
327
  settleLifecycle(session, payload.call_id, 'done', row.timestamp);
301
328
  const call = state.collaboration.calls.get(String(payload.call_id || ''));
329
+ state.executionTracker.recordOutput({
330
+ name: call && call.name,
331
+ callId: payload.call_id,
332
+ args: call && call.args || {},
333
+ output: payload.output,
334
+ at: row.timestamp,
335
+ isError: payload.is_error === true || payload.status === 'failed',
336
+ });
337
+ state.pendingUserInputCalls.delete(String(payload.call_id || ''));
302
338
  const output = jsonObject(payload.output);
303
339
  if (call && call.name === 'spawn_agent') {
304
340
  const record = state.collaboration.ensureSpawn(payload.call_id);
@@ -356,10 +392,18 @@ function createCodexParser(dependencies) {
356
392
  if (!text) return;
357
393
  if (role === 'user' && /<codex_internal_context(?:\s|>)/i.test(rawText)) {
358
394
  state.latestInternalGoal = text;
395
+ if (/<codex_internal_context[^>]*\bsource=["']goal["']/i.test(rawText)) state.goalContexts.add(`${row.timestamp || ''}:${text}`);
359
396
  return;
360
397
  }
361
- if (role === 'user') state.latestUser = text;
362
- if (role === 'assistant') state.latestDelegationNarration = text;
398
+ if (role === 'user') {
399
+ state.latestUser = text;
400
+ state.lastConversationRole = 'user';
401
+ }
402
+ if (role === 'assistant') {
403
+ state.latestDelegationNarration = text;
404
+ state.lastAssistantText = text;
405
+ state.lastConversationRole = 'assistant';
406
+ }
363
407
  const key = payload.id || `message:${role}:${row.timestamp}:${text.slice(0, 80)}`;
364
408
  addCodexMessage(session, state.messageObservations, { id: key, role, text, timestamp: row.timestamp }, 'response');
365
409
  }
@@ -386,6 +430,7 @@ function createCodexParser(dependencies) {
386
430
  };
387
431
  if (row.type === 'turn_context') {
388
432
  session.model = payload.model || session.model;
433
+ if (payload.cwd && !session.originCwd) session.originCwd = payload.cwd;
389
434
  session.cwd = payload.cwd || session.cwd;
390
435
  } else if (row.type === 'world_state') {
391
436
  const retainedText = payload.state && payload.state.environments && payload.state.environments.subagents;
@@ -408,15 +453,25 @@ function createCodexParser(dependencies) {
408
453
  ? (session.taskName || `${session.agentName} 서브에이전트`)
409
454
  : (compactText(state.latestUser || state.latestInternalGoal, 180) || 'GPT 작업 세션');
410
455
  session.result = state.lastFinalAnswer;
456
+ if (!session.depth && state.goalContexts.size) session.loop = { kind: 'goal', iteration: state.goalContexts.size };
411
457
  const collaboration = state.collaboration.finalize(session.collaboration.retainedAgents);
412
458
  session.collaboration.spawns = collaboration.spawns;
413
459
  session.collaboration.communications = collaboration.communications;
460
+ session.executions = state.executionTracker.finalize();
414
461
  const windowInfo = modelContextWindow('codex', session.model, state.observedWindow);
415
462
  session.context = contextInfo(session.turnUsage.total || session.turnUsage.input, windowInfo);
416
463
 
417
464
  if (session.status !== 'failed') {
418
465
  const turnAge = Date.now() - fileInfo.mtimeMs;
419
- if (state.activeTurn && turnAge < STALE_TURN_THRESHOLD_MS) {
466
+ const pendingUserInput = state.pendingUserInputCalls.size > 0;
467
+ const conversationalInput = state.lastConversationRole === 'assistant'
468
+ && assistantRequestsUserResponse(state.lastAssistantText || state.lastFinalAnswer)
469
+ && (state.lastTurnCompleted || turnAge >= ACTIVE_THRESHOLD_MS);
470
+ if (!session.depth && (pendingUserInput || conversationalInput)) {
471
+ session.status = 'waiting';
472
+ session.statusDetail = pendingUserInput ? '선택 또는 입력 대기' : '답변 또는 선택 대기';
473
+ session.statusObserved = true;
474
+ } else if (state.activeTurn && turnAge < STALE_TURN_THRESHOLD_MS) {
420
475
  session.status = 'running';
421
476
  session.statusDetail = '턴 실행 중';
422
477
  session.statusObserved = true;
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+
3
+ const SHELL_TOOLS = new Set([
4
+ 'bash',
5
+ 'cmd',
6
+ 'exec_command',
7
+ 'powershell',
8
+ 'pwsh',
9
+ 'shell',
10
+ 'shell_command',
11
+ 'terminal',
12
+ ]);
13
+
14
+ const CONTINUATION_TOOLS = new Set([
15
+ 'bashoutput',
16
+ 'taskoutput',
17
+ 'wait',
18
+ 'write_stdin',
19
+ ]);
20
+
21
+ function toolName(value) {
22
+ return String(value || '').trim().toLowerCase().split(/[.:/]/).filter(Boolean).pop() || '';
23
+ }
24
+
25
+ function outputText(value, seen = new Set()) {
26
+ if (typeof value === 'string') return value;
27
+ if (value == null || typeof value === 'number' || typeof value === 'boolean') return value == null ? '' : String(value);
28
+ if (seen.has(value)) return '';
29
+ if (typeof value !== 'object') return '';
30
+ seen.add(value);
31
+ if (Array.isArray(value)) return value.map(item => outputText(item, seen)).filter(Boolean).join('\n');
32
+ return [
33
+ value.text,
34
+ value.input_text,
35
+ value.output_text,
36
+ value.message,
37
+ value.content,
38
+ value.output,
39
+ value.error,
40
+ ].map(item => outputText(item, seen)).filter(Boolean).join('\n');
41
+ }
42
+
43
+ function quotedProperty(source, key) {
44
+ const pattern = new RegExp(`\\b${key}\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`, 'm');
45
+ const match = String(source || '').match(pattern);
46
+ if (!match) return '';
47
+ const literal = match[1];
48
+ if (literal.startsWith('"')) {
49
+ try {
50
+ return JSON.parse(literal);
51
+ } catch (_invalidStringLiteral) {
52
+ return literal.slice(1, -1);
53
+ }
54
+ }
55
+ return literal.slice(1, -1)
56
+ .replace(/\\([\\'`])/g, '$1')
57
+ .replace(/\\n/g, '\n')
58
+ .replace(/\\r/g, '\r')
59
+ .replace(/\\t/g, '\t');
60
+ }
61
+
62
+ function booleanProperty(source, key) {
63
+ const match = String(source || '').match(new RegExp(`\\b${key}\\s*:\\s*(true|false)`, 'i'));
64
+ return match ? match[1].toLowerCase() === 'true' : null;
65
+ }
66
+
67
+ function scalarProperty(source, key) {
68
+ const quoted = quotedProperty(source, key);
69
+ if (quoted) return quoted;
70
+ const match = String(source || '').match(new RegExp(`\\b${key}\\s*:\\s*([A-Za-z0-9._:-]+)`, 'i'));
71
+ return match ? match[1] : '';
72
+ }
73
+
74
+ function nestedToolName(name, rawInput) {
75
+ const normalizedName = toolName(name);
76
+ if (normalizedName !== 'exec') return normalizedName;
77
+ const match = String(rawInput || '').match(/tools\.(exec_command|write_stdin|wait)\s*\(/);
78
+ return match ? match[1] : normalizedName;
79
+ }
80
+
81
+ function executionInput(name, args = {}, rawInput = '') {
82
+ const normalizedName = toolName(name);
83
+ const source = String(rawInput || '');
84
+ const nestedExec = normalizedName === 'exec' && /tools\.exec_command\s*\(/.test(source);
85
+ const shell = SHELL_TOOLS.has(normalizedName) || nestedExec;
86
+ const runInBackground = args.run_in_background === true
87
+ || args.background === true
88
+ || args.detach === true
89
+ || booleanProperty(source, 'run_in_background') === true
90
+ || booleanProperty(source, 'background') === true;
91
+ if (!shell && !runInBackground) return null;
92
+ if (/^(?:task|agent|spawn_agent)$/.test(normalizedName)) return null;
93
+ const command = args.command || args.cmd || args.script
94
+ || quotedProperty(source, 'cmd') || quotedProperty(source, 'command');
95
+ const cwd = args.workdir || args.cwd
96
+ || quotedProperty(source, 'workdir') || quotedProperty(source, 'cwd');
97
+ const description = args.description || quotedProperty(source, 'description');
98
+ return {
99
+ kind: shell ? 'shell' : 'background',
100
+ mode: runInBackground ? 'background' : 'foreground',
101
+ command: String(command || ''),
102
+ cwd: String(cwd || ''),
103
+ description: String(description || ''),
104
+ tool: nestedExec ? 'exec_command' : normalizedName,
105
+ };
106
+ }
107
+
108
+ function runtimeLabel(activity) {
109
+ const value = `${activity.tool || ''} ${activity.command || ''}`.toLowerCase();
110
+ if (/\bpowershell\b|\bpwsh\b/.test(value)) return 'PowerShell';
111
+ if (/\bcmd(?:\.exe)?\b/.test(value)) return 'Command Prompt';
112
+ if (/\bbash\b/.test(value)) return 'Bash';
113
+ return activity.kind === 'shell' ? 'Shell' : 'Background';
114
+ }
115
+
116
+ function firstCommandLine(value, fallback = '') {
117
+ return String(value || '').split(/\r?\n/).map(line => line.trim()).find(Boolean) || fallback;
118
+ }
119
+
120
+ function runtimeHandle(value) {
121
+ const text = outputText(value);
122
+ const patterns = [
123
+ { type: 'cell', pattern: /(?:cell(?:\s+id)?|cell_id)[\s:=`"']+([A-Za-z0-9._:-]+)/i },
124
+ { type: 'session', pattern: /(?:session(?:\s+id)?|session_id)[\s:=`"']+([A-Za-z0-9._:-]+)/i },
125
+ { type: 'task', pattern: /(?:background(?:\s+(?:task|command))?(?:\s+with)?(?:\s+id)?|task_id)[\s:=`"']+([A-Za-z0-9._:-]+)/i },
126
+ ];
127
+ for (const candidate of patterns) {
128
+ const match = text.match(candidate.pattern);
129
+ if (match) return { type: candidate.type, id: match[1].replace(/[.,;]+$/, '') };
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function handleFromArgs(args = {}) {
135
+ const values = [
136
+ ['cell', args.cell_id || args.cellId],
137
+ ['session', args.session_id || args.sessionId],
138
+ ['task', args.task_id || args.taskId || args.bash_id || args.shell_id],
139
+ ];
140
+ const found = values.find(([, value]) => value != null && String(value));
141
+ return found ? { type: found[0], id: String(found[1]) } : null;
142
+ }
143
+
144
+ function exitCode(value) {
145
+ const text = outputText(value);
146
+ const match = text.match(/^\s*(?:exit\s+code|exit_code)\s*[:=]\s*(-?\d+)\b/im)
147
+ || text.match(/\b(?:process|command)\s+exited\s+with\s+(?:exit\s+)?code\s+(-?\d+)\b/i);
148
+ return match ? Number(match[1]) : null;
149
+ }
150
+
151
+ function outputStatus(value, explicitError = false) {
152
+ const text = outputText(value);
153
+ const firstLine = text.split(/\r?\n/).map(line => line.trim().toLowerCase()).find(Boolean) || '';
154
+ if (firstLine.startsWith('script failed')) return 'failed';
155
+ if (firstLine.startsWith('script completed')) return 'completed';
156
+ if (firstLine.startsWith('script running')) return 'running';
157
+ const code = exitCode(text);
158
+ if (explicitError || (code != null && code !== 0) || /(?:script|command|process)\s+failed|isError["':\s]+true|fatal error/i.test(text)) return 'failed';
159
+ if (/(?:script|command|process)\s+(?:is\s+)?(?:still\s+)?running|running\s+in\s+(?:the\s+)?background|background\s+(?:task|command)|yielded/i.test(text)) return 'running';
160
+ return 'completed';
161
+ }
162
+
163
+ function createExecutionTracker(options = {}) {
164
+ const compactText = options.compactText || (value => String(value || ''));
165
+ const timestamp = options.timestamp || (value => value || null);
166
+ const activities = [];
167
+ const byCallId = new Map();
168
+ const byHandle = new Map();
169
+ const continuations = new Map();
170
+
171
+ function registerHandle(activity, handle) {
172
+ if (!activity || !handle || !handle.id) return;
173
+ activity.backgroundId = handle.id;
174
+ activity.backgroundIdType = handle.type;
175
+ activity.mode = 'background';
176
+ byHandle.set(`${handle.type}:${handle.id}`, activity);
177
+ byHandle.set(handle.id, activity);
178
+ }
179
+
180
+ function recordCall({ name, callId, args = {}, rawInput = '', at }) {
181
+ const normalizedName = nestedToolName(name, rawInput);
182
+ const effectiveArgs = args && typeof args === 'object' && !Array.isArray(args) ? { ...args } : {};
183
+ if (normalizedName === 'wait' && !effectiveArgs.cell_id) effectiveArgs.cell_id = scalarProperty(rawInput, 'cell_id');
184
+ if (normalizedName === 'write_stdin' && !effectiveArgs.session_id) effectiveArgs.session_id = scalarProperty(rawInput, 'session_id');
185
+ if (CONTINUATION_TOOLS.has(normalizedName)) {
186
+ continuations.set(String(callId || ''), { name: normalizedName, args: effectiveArgs });
187
+ return null;
188
+ }
189
+ const input = executionInput(normalizedName, effectiveArgs, rawInput);
190
+ if (!input) return null;
191
+ const startedAt = timestamp(at, null);
192
+ const activity = {
193
+ id: String(callId || `execution:${activities.length}`),
194
+ callId: String(callId || ''),
195
+ kind: input.kind,
196
+ mode: input.mode,
197
+ tool: input.tool,
198
+ runtime: '',
199
+ label: compactText(input.description || firstCommandLine(input.command, input.kind === 'shell' ? '셸 명령' : '백그라운드 작업'), 180),
200
+ command: compactText(input.command, 1200),
201
+ cwd: compactText(input.cwd, 360),
202
+ status: 'running',
203
+ statusDetail: input.mode === 'background' ? '백그라운드 실행 시작' : '포그라운드 실행 시작',
204
+ output: '',
205
+ backgroundId: '',
206
+ backgroundIdType: '',
207
+ exitCode: null,
208
+ startedAt,
209
+ updatedAt: startedAt,
210
+ completedAt: null,
211
+ source: 'tool-call',
212
+ };
213
+ activity.runtime = runtimeLabel(activity);
214
+ activities.push(activity);
215
+ if (activity.callId) byCallId.set(activity.callId, activity);
216
+ const requestedHandle = handleFromArgs(effectiveArgs);
217
+ if (requestedHandle) registerHandle(activity, requestedHandle);
218
+ return activity;
219
+ }
220
+
221
+ function findContinuation(args = {}) {
222
+ const handle = handleFromArgs(args);
223
+ if (handle) return byHandle.get(`${handle.type}:${handle.id}`) || byHandle.get(handle.id) || null;
224
+ return [...activities].reverse().find(activity => activity.mode === 'background' && activity.status === 'running') || null;
225
+ }
226
+
227
+ function recordOutput({ name, callId, args = {}, output, at, isError = false }) {
228
+ const pendingContinuation = continuations.get(String(callId || ''));
229
+ const normalizedName = pendingContinuation && pendingContinuation.name || toolName(name);
230
+ const effectiveArgs = pendingContinuation && pendingContinuation.args || args;
231
+ const continuation = CONTINUATION_TOOLS.has(normalizedName);
232
+ const activity = continuation ? findContinuation(effectiveArgs) : byCallId.get(String(callId || ''));
233
+ if (!activity) {
234
+ continuations.delete(String(callId || ''));
235
+ return null;
236
+ }
237
+ const status = outputStatus(output, isError);
238
+ const observedOutput = compactText(outputText(output), 2400);
239
+ const argumentHandle = handleFromArgs(effectiveArgs);
240
+ const handle = argumentHandle || (status === 'running' ? runtimeHandle(output) : null);
241
+ if (handle && (status === 'running' || continuation || activity.mode === 'background')) registerHandle(activity, handle);
242
+ const code = exitCode(output);
243
+ activity.status = status;
244
+ if (observedOutput) {
245
+ activity.output = continuation && activity.output
246
+ ? compactText(`${activity.output}\n${observedOutput}`, 2400)
247
+ : observedOutput;
248
+ }
249
+ activity.exitCode = code;
250
+ activity.updatedAt = timestamp(at, activity.updatedAt);
251
+ if (status === 'running') {
252
+ activity.mode = 'background';
253
+ activity.statusDetail = handle && handle.id
254
+ ? `백그라운드 ${handle.type} ${handle.id}`
255
+ : '백그라운드에서 계속 실행 중';
256
+ } else {
257
+ activity.completedAt = activity.updatedAt;
258
+ activity.statusDetail = status === 'failed'
259
+ ? (code == null ? '실행 실패' : `종료 코드 ${code}`)
260
+ : (code == null ? '실행 완료' : `종료 코드 ${code}`);
261
+ }
262
+ continuations.delete(String(callId || ''));
263
+ return activity;
264
+ }
265
+
266
+ function finalize(limit = 120) {
267
+ return activities.slice(-limit).map(activity => ({ ...activity }));
268
+ }
269
+
270
+ return { activities, recordCall, recordOutput, finalize };
271
+ }
272
+
273
+ module.exports = {
274
+ CONTINUATION_TOOLS,
275
+ SHELL_TOOLS,
276
+ createExecutionTracker,
277
+ executionInput,
278
+ outputStatus,
279
+ outputText,
280
+ runtimeHandle,
281
+ toolName,
282
+ };