loadtoagent 1.3.5 → 1.3.7

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.
@@ -16,6 +16,13 @@
16
16
  --panel-2: #121d28;
17
17
  --panel-3: #172532;
18
18
  --focus: #99ebff;
19
+ --space-1: 4px;
20
+ --space-2: 8px;
21
+ --space-3: 12px;
22
+ --space-4: 16px;
23
+ --space-5: 24px;
24
+ --control-gap: 12px;
25
+ --control-height: 44px;
19
26
  }
20
27
 
21
28
  html,
@@ -118,6 +125,10 @@ body[data-current-view="all"] #sessionSection { order: 8; }
118
125
  min-height: 48px;
119
126
  }
120
127
 
128
+ .top-actions {
129
+ gap: var(--space-4);
130
+ }
131
+
121
132
  .sync-state.connection-error > span {
122
133
  background: var(--red, #ff7373);
123
134
  box-shadow: 0 0 9px rgba(255, 115, 115, .55);
@@ -474,14 +485,7 @@ body :is(input, textarea)::placeholder {
474
485
  }
475
486
 
476
487
  .terminal-session-row {
477
- grid-template-columns: minmax(0, 1fr) 44px;
478
- gap: 7px;
479
- }
480
-
481
- .terminal-reorder-actions button {
482
- min-width: 44px;
483
- min-height: 44px;
484
- font-size: 16px;
488
+ grid-template-columns: minmax(0, 1fr);
485
489
  }
486
490
 
487
491
  .terminal-font-controls {
@@ -886,7 +890,7 @@ body :is(
886
890
 
887
891
  body button:not(.icon-button) {
888
892
  min-width: 44px;
889
- min-height: 44px !important;
893
+ min-height: var(--control-height) !important;
890
894
  font-size: 12px !important;
891
895
  line-height: 1.35;
892
896
  }
@@ -914,7 +918,6 @@ body :is(
914
918
  .terminal-workspace-actions,
915
919
  .terminal-key-actions,
916
920
  .terminal-tmux-tools,
917
- .terminal-reorder-actions,
918
921
  .tmux-section-actions,
919
922
  .tmux-pane-actions,
920
923
  .run-modal-actions,
@@ -924,7 +927,7 @@ body :is(
924
927
  .detail-meta-actions,
925
928
  .mobile-bottom-nav
926
929
  ) {
927
- gap: 8px !important;
930
+ gap: var(--control-gap) !important;
928
931
  }
929
932
 
930
933
  /* Keep adjacent controls visually and physically distinct at every breakpoint. */
@@ -938,18 +941,30 @@ body :is(
938
941
  .run-prompt-examples,
939
942
  .path-field
940
943
  ) {
941
- gap: 8px !important;
944
+ gap: var(--control-gap) !important;
942
945
  }
943
946
 
944
947
  .sidebar .section-title {
945
948
  height: auto;
946
949
  min-height: 44px;
947
- margin-bottom: 8px;
950
+ margin-bottom: var(--space-3);
948
951
  padding-block: 2px;
949
952
  }
950
953
 
951
954
  .sidebar .workspace-list {
952
- gap: 8px !important;
955
+ gap: 10px !important;
956
+ }
957
+
958
+ .session-tools,
959
+ .modal-actions,
960
+ .run-modal-actions,
961
+ .app-error-actions {
962
+ gap: var(--control-gap) !important;
963
+ }
964
+
965
+ .modal-actions,
966
+ .run-modal-actions {
967
+ margin-top: var(--space-5);
953
968
  }
954
969
 
955
970
  .run-modal.terminal-modal :is(.field-label, label, small),
@@ -1025,7 +1040,7 @@ body .primary-button:disabled {
1025
1040
  }
1026
1041
 
1027
1042
  .mobile-bottom-nav {
1028
- gap: 8px !important;
1043
+ gap: 10px !important;
1029
1044
  }
1030
1045
  }
1031
1046
 
@@ -7,13 +7,25 @@
7
7
 
8
8
  /* Compact screens ≤ 1280px: collapse content columns and simplify the side rail. */
9
9
  @media (max-width:1280px) {
10
+ body[data-current-view="all"] .topbar {
11
+ display: block;
12
+ }
13
+
14
+ body[data-current-view="all"] .top-actions {
15
+ width: 100%;
16
+ flex-wrap: wrap;
17
+ justify-content: flex-end;
18
+ margin-top: 16px;
19
+ }
20
+
21
+ body[data-current-view="all"] .top-actions > * {
22
+ flex: 0 0 auto;
23
+ }
24
+
10
25
  .session-grid {
11
26
  grid-template-columns: 1fr;
12
27
  }
13
- }
14
28
 
15
- /* Compact screens ≤ 1100px: collapse content columns and simplify the side rail. */
16
- @media (max-width:1100px) {
17
29
  .sessions-heading {
18
30
  align-items: flex-start;
19
31
  flex-direction: column;
@@ -34,11 +46,6 @@
34
46
  max-width: 100%;
35
47
  overflow-x: auto;
36
48
  }
37
-
38
- .top-actions {
39
- flex-wrap: wrap;
40
- justify-content: flex-end;
41
- }
42
49
  }
43
50
 
44
51
  /* Compact screens ≤ 980px: keep project filtering readable while reducing the side rail. */
@@ -399,10 +406,17 @@
399
406
  }
400
407
 
401
408
  .chat-progress-updates,
402
- .chat-turn-waiting {
409
+ .chat-turn-waiting,
410
+ .subagent-call-moment,
411
+ .subagent-call-event {
403
412
  margin-left: 0;
404
413
  }
405
414
 
415
+ .subagent-call-moment,
416
+ .subagent-call-event {
417
+ width: 100%;
418
+ }
419
+
406
420
  .json-object>div {
407
421
  grid-template-columns: 1fr;
408
422
  }
@@ -475,6 +489,20 @@
475
489
  border-radius: 7px;
476
490
  }
477
491
 
492
+ .subagent-call-event {
493
+ grid-template-columns: 32px minmax(0,1fr);
494
+ }
495
+
496
+ .subagent-call-anchor {
497
+ grid-template-columns: 26px minmax(0,1fr);
498
+ padding: 9px 10px;
499
+ }
500
+
501
+ .subagent-call-action {
502
+ grid-column: 2;
503
+ justify-items: start;
504
+ }
505
+
478
506
  .main-stage {
479
507
  padding-right: 11px;
480
508
  padding-left: 11px;
@@ -421,35 +421,8 @@
421
421
 
422
422
  .terminal-session-row {
423
423
  display: grid;
424
- grid-template-columns: minmax(0,1fr) 26px;
424
+ grid-template-columns: minmax(0,1fr);
425
425
  align-items: stretch;
426
- gap: 4px;
427
- }
428
-
429
- .terminal-reorder-actions {
430
- display: grid;
431
- gap: 4px;
432
- }
433
-
434
- .terminal-reorder-actions button {
435
- min-width: 26px;
436
- padding: 0;
437
- border: 1px solid #243946;
438
- border-radius: 7px;
439
- background: #101a24;
440
- color: #7f91a3;
441
- cursor: pointer;
442
- }
443
-
444
- .terminal-reorder-actions button:hover:not(:disabled),
445
- .terminal-reorder-actions button:focus-visible {
446
- border-color: #397766;
447
- color: #70e6bd;
448
- }
449
-
450
- .terminal-reorder-actions button:disabled {
451
- opacity: .3;
452
- cursor: default;
453
426
  }
454
427
 
455
428
  .terminal-session-item:hover {
@@ -1395,7 +1368,7 @@
1395
1368
  cursor: grab;
1396
1369
  position: relative;
1397
1370
  display: grid;
1398
- grid-template-columns: 38px minmax(0,1fr) 8px;
1371
+ grid-template-columns: 12px 38px minmax(0,1fr) auto;
1399
1372
  gap: 10px;
1400
1373
  min-height: 72px;
1401
1374
  padding: 10px 9px;
@@ -1406,6 +1379,23 @@
1406
1379
  transition: transform var(--motion-fast) var(--motion-ease),background-color var(--motion-fast) ease,border-color var(--motion-fast) ease,color var(--motion-fast) ease,box-shadow var(--motion-medium) ease,opacity var(--motion-fast) ease;
1407
1380
  }
1408
1381
 
1382
+ .terminal-session-drag-handle {
1383
+ width: 3px;
1384
+ height: 3px;
1385
+ justify-self: center;
1386
+ border-radius: 50%;
1387
+ background: #607281;
1388
+ box-shadow: 0 -5px #607281,0 5px #607281;
1389
+ transition: background-color var(--motion-fast) ease,box-shadow var(--motion-fast) ease;
1390
+ }
1391
+
1392
+ .terminal-session-item:hover .terminal-session-drag-handle,
1393
+ .terminal-session-item:focus-visible .terminal-session-drag-handle,
1394
+ .terminal-session-item.dragging .terminal-session-drag-handle {
1395
+ background: #68dcb6;
1396
+ box-shadow: 0 -5px #68dcb6,0 5px #68dcb6;
1397
+ }
1398
+
1409
1399
  .terminal-session-item.dragging {
1410
1400
  opacity: .45;
1411
1401
  cursor: grabbing;
@@ -1468,10 +1458,10 @@ body[data-current-view="terminal"] .topbar h1 {
1468
1458
  .terminal-resource-head>div span { margin-top: 5px; color: #93a6b2; font-size: 12px; }
1469
1459
  .terminal-resource-live { font-size: 12px; }
1470
1460
  .terminal-session-list { gap: 9px; padding: 12px; }
1471
- .terminal-session-row { grid-template-columns: minmax(0,1fr) 28px; gap: 5px; }
1461
+ .terminal-session-row { grid-template-columns: minmax(0,1fr); }
1472
1462
 
1473
1463
  .terminal-session-item {
1474
- grid-template-columns: 42px minmax(0,1fr) auto;
1464
+ grid-template-columns: 12px 42px minmax(0,1fr) auto;
1475
1465
  gap: 11px;
1476
1466
  min-height: 86px;
1477
1467
  padding: 11px 10px;
@@ -610,6 +610,10 @@
610
610
  border-color: rgba(230,104,112,.4);
611
611
  }
612
612
 
613
+ .execution-activity-card.unverified {
614
+ border-color: rgba(229,184,95,.34);
615
+ }
616
+
613
617
  .execution-activity-icon {
614
618
  width: 36px;
615
619
  height: 36px;
@@ -716,6 +720,10 @@
716
720
  background: #e26972;
717
721
  }
718
722
 
723
+ .execution-activity-card.unverified .execution-activity-state>span:first-child i {
724
+ background: #d6b36c;
725
+ }
726
+
719
727
  .execution-activity-state b {
720
728
  color: #c6d2d7;
721
729
  font-size: 12px;
@@ -93,19 +93,6 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
93
93
  });
94
94
  };
95
95
  sessionList.addEventListener('click', event => {
96
- const move = event.target.closest('[data-session-move][data-session-move-id]');
97
- if (move) {
98
- const changed = moveSessionByOffset(move.dataset.sessionMoveId, Number(move.dataset.sessionMove));
99
- if (!changed) return;
100
- renderAll();
101
- requestAnimationFrame(() => {
102
- const next = sessionList.querySelector(`[data-session-move-id="${CSS.escape(move.dataset.sessionMoveId)}"][data-session-move="${CSS.escape(move.dataset.sessionMove)}"]`);
103
- if (next && !next.disabled) next.focus();
104
- else sessionList.querySelector(`[data-terminal-id="${CSS.escape(move.dataset.sessionMoveId)}"]`)?.focus();
105
- });
106
- notice(window.LoadToAgentI18n.t('terminal.reordered'), 'success');
107
- return;
108
- }
109
96
  if (state.sessionDragJustEnded) return;
110
97
  const item = event.target.closest('[data-terminal-id]');
111
98
  if (item) selectSession(item.dataset.terminalId);
@@ -178,14 +178,11 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
178
178
  aria-grabbed="false"
179
179
  aria-describedby="terminalReorderHelp"
180
180
  title="${esc(session.cwd || window.LoadToAgentI18n.t('terminal.reorder_hint'))}">
181
+ <span class="terminal-session-drag-handle" aria-hidden="true"></span>
181
182
  <span class="terminal-session-icon">${esc(terminalTypeMark(session))}</span>
182
183
  <span><b>${esc(session.title)}</b><small>${esc(terminalTypeLabel(session))}${session.background ? ` · ${t('terminal.background_kept')}` : ''}${session.recoveredAfterHostRestart ? ` · ${t('terminal.recovered_after_host_restart')}` : ''}</small><em>${esc(session.cwd || session.distro || `PID ${session.pid || '--'}`)}</em><span class="sr-only">${index + 1}/${general.length}</span></span>
183
184
  <span class="terminal-session-status" data-status="${esc(presentation.tone)}"><i></i>${esc(presentation.label)}</span>
184
185
  </button>
185
- <span class="terminal-reorder-actions" aria-label="${esc(window.LoadToAgentI18n.t('terminal.reorder_group', { title: session.title }))}">
186
- <button type="button" data-session-move="-1" data-session-move-id="${esc(session.id)}" aria-label="${esc(window.LoadToAgentI18n.t('terminal.move_up', { title: session.title }))}" ${index === 0 ? 'disabled' : ''}>↑</button>
187
- <button type="button" data-session-move="1" data-session-move-id="${esc(session.id)}" aria-label="${esc(window.LoadToAgentI18n.t('terminal.move_down', { title: session.title }))}" ${index === general.length - 1 ? 'disabled' : ''}>↓</button>
188
- </span>
189
186
  </div>`;
190
187
  }).join('') : `<div class="terminal-resource-empty">${t('terminal.empty.general')}</div>`;
191
188
  }
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const { createExecutionTracker } = require('./executionActivity');
4
+ const { createExecutionTracker, reconcileExecutionActivities } = require('./executionActivity');
5
5
 
6
6
  function createClaudeParser(dependencies) {
7
7
  const {
@@ -23,6 +23,34 @@ function createClaudeParser(dependencies) {
23
23
  isUserInputTool,
24
24
  } = dependencies;
25
25
 
26
+ function taskNotification(value) {
27
+ const text = String(value || '');
28
+ if (!/<task-notification\b/i.test(text)) return null;
29
+ const field = name => {
30
+ const match = text.match(new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`, 'i'));
31
+ return match ? match[1].trim() : '';
32
+ };
33
+ const taskId = field('task-id');
34
+ const toolUseId = field('tool-use-id');
35
+ const status = field('status').toLowerCase();
36
+ if (!taskId && !toolUseId) return null;
37
+ return { taskId, toolUseId, status, text };
38
+ }
39
+
40
+ function recordTaskNotification(state, value, at) {
41
+ const notification = taskNotification(value);
42
+ if (!notification || !/^(?:completed|failed|error|cancelled)$/.test(notification.status)) return null;
43
+ state.executionTracker.recordOutput({
44
+ name: 'bash',
45
+ callId: notification.toolUseId,
46
+ args: { task_id: notification.taskId },
47
+ output: notification.text,
48
+ at,
49
+ isError: /^(?:failed|error)$/.test(notification.status),
50
+ });
51
+ return notification;
52
+ }
53
+
26
54
  function normalizeUsage(raw = {}) {
27
55
  return finalizeUsage({
28
56
  input: raw.input_tokens,
@@ -53,6 +81,136 @@ function createClaudeParser(dependencies) {
53
81
  return raw;
54
82
  }
55
83
 
84
+ function isClaudeAgentTool(name) {
85
+ return /^(?:Agent|Task)$/i.test(String(name || ''));
86
+ }
87
+
88
+ function addClaudeCommunication(session, event) {
89
+ const communications = session.collaboration.communications;
90
+ const row = {
91
+ id: String(event.id || `claude-communication:${communications.length}`),
92
+ kind: event.kind || 'message',
93
+ label: compactText(event.label || '메시지', 100),
94
+ from: compactText(event.from, 180),
95
+ to: compactText(event.to, 180),
96
+ taskName: compactText(event.taskName, 180),
97
+ childId: compactText(event.childId, 180),
98
+ text: compactText(event.text, 6000),
99
+ protected: false,
100
+ assignmentSource: compactText(event.assignmentSource, 80),
101
+ timestamp: timestamp(event.timestamp, session.updatedAt),
102
+ };
103
+ if (!communications.some(item => item.id === row.id)) communications.push(row);
104
+ }
105
+
106
+ function findClaudeSpawn(session, callId) {
107
+ return session.collaboration.spawns.find(record => record.callId === String(callId || ''));
108
+ }
109
+
110
+ function recordClaudeAgentCall(session, state, row, callId, args) {
111
+ const prompt = compactText(args.prompt || args.message, 6000);
112
+ const taskName = compactText(args.description || args.name || args.subagent_type, 180);
113
+ const record = {
114
+ callId: String(callId || `claude-spawn:${session.collaboration.spawns.length}`),
115
+ taskName,
116
+ agentPath: '',
117
+ childId: '',
118
+ assignment: prompt,
119
+ assignmentObserved: Boolean(prompt),
120
+ assignmentProtected: false,
121
+ assignmentSource: prompt ? 'claude-agent-prompt' : 'unavailable',
122
+ assignmentContext: '',
123
+ sharedGoal: compactText(state.latestUser, 6000),
124
+ status: 'requested',
125
+ startedAt: timestamp(row.timestamp, session.updatedAt),
126
+ completedAt: null,
127
+ result: '',
128
+ agentName: compactText(args.subagent_type, 120),
129
+ currentlyRetained: false,
130
+ };
131
+ session.collaboration.spawns.push(record);
132
+ addClaudeCommunication(session, {
133
+ id: `assign:${record.callId}`,
134
+ kind: 'assignment',
135
+ label: '새 작업 배정',
136
+ from: session.agentPath || session.id,
137
+ to: record.taskName,
138
+ taskName: record.taskName,
139
+ text: record.assignment,
140
+ assignmentSource: record.assignmentSource,
141
+ timestamp: row.timestamp,
142
+ });
143
+ }
144
+
145
+ function updateClaudeSpawn(session, callId, details = {}) {
146
+ const record = findClaudeSpawn(session, callId);
147
+ if (!record) return;
148
+ const childExternalId = compactText(details.childExternalId, 180);
149
+ if (childExternalId) {
150
+ record.childId = `claude:${childExternalId}`;
151
+ record.agentPath = record.childId;
152
+ }
153
+ if (details.status) record.status = details.status;
154
+ if (details.completedAt) record.completedAt = timestamp(details.completedAt, record.completedAt || session.updatedAt);
155
+ if (details.result) record.result = compactText(details.result, 6000);
156
+ for (const communication of session.collaboration.communications) {
157
+ if (communication.taskName !== record.taskName || communication.childId) continue;
158
+ communication.childId = record.childId;
159
+ communication.to = record.agentPath || communication.to;
160
+ }
161
+ if ((record.status === 'running' || record.childId) && !session.collaboration.communications.some(item => item.id === `started:${record.callId}`)) {
162
+ addClaudeCommunication(session, {
163
+ id: `started:${record.callId}`,
164
+ kind: 'started',
165
+ label: '서브에이전트 실행 시작',
166
+ from: 'Claude 런타임',
167
+ to: record.agentPath || record.taskName,
168
+ taskName: record.taskName,
169
+ childId: record.childId,
170
+ timestamp: record.startedAt,
171
+ });
172
+ }
173
+ if (record.completedAt && !session.collaboration.communications.some(item => item.id === `result:${record.callId}`)) {
174
+ addClaudeCommunication(session, {
175
+ id: `result:${record.callId}`,
176
+ kind: 'result',
177
+ label: '결과 반환 확인',
178
+ from: record.agentPath || record.taskName,
179
+ to: session.agentPath || session.id,
180
+ taskName: record.taskName,
181
+ childId: record.childId,
182
+ text: record.result,
183
+ timestamp: record.completedAt,
184
+ });
185
+ }
186
+ }
187
+
188
+ function recordClaudeAgentToolResult(session, state, item, at) {
189
+ const call = state.toolCalls.get(String(item.tool_use_id || ''));
190
+ if (!call || !isClaudeAgentTool(call.name)) return;
191
+ const output = compactText(item.content || item, 12000);
192
+ const agentId = output.match(/\bagentId:\s*([A-Za-z0-9_-]+)/i);
193
+ const launched = /agent launched successfully|working in the background/i.test(output);
194
+ const failed = item.is_error === true;
195
+ updateClaudeSpawn(session, item.tool_use_id, {
196
+ childExternalId: agentId && agentId[1],
197
+ status: failed ? 'failed' : (launched ? 'running' : 'completed'),
198
+ completedAt: launched ? null : at,
199
+ result: launched ? '' : output,
200
+ });
201
+ }
202
+
203
+ function recordClaudeTaskCompletion(session, notification, at) {
204
+ if (!notification) return;
205
+ const resultMatch = notification.text.match(/<result>([\s\S]*?)<\/result>/i);
206
+ updateClaudeSpawn(session, notification.toolUseId, {
207
+ childExternalId: notification.taskId,
208
+ status: notification.status === 'completed' ? 'completed' : notification.status,
209
+ completedAt: at,
210
+ result: resultMatch ? resultMatch[1].trim() : '',
211
+ });
212
+ }
213
+
56
214
  function recordContent(session, state, row, item, index) {
57
215
  const kind = item && item.type;
58
216
  const id = `${row.uuid || row.requestId || session.externalId}:${index}`;
@@ -64,6 +222,7 @@ function createClaudeParser(dependencies) {
64
222
  const callId = item.id || id;
65
223
  const args = item.input && typeof item.input === 'object' ? item.input : {};
66
224
  state.toolCalls.set(String(callId), { name, args });
225
+ if (isClaudeAgentTool(name)) recordClaudeAgentCall(session, state, row, callId, args);
67
226
  state.executionTracker.recordCall({ name, callId, args, rawInput: item.input, at: row.timestamp });
68
227
  addMessage(session, {
69
228
  id,
@@ -93,6 +252,7 @@ function createClaudeParser(dependencies) {
93
252
  at: row.timestamp,
94
253
  isError: item.is_error === true,
95
254
  });
255
+ recordClaudeAgentToolResult(session, state, item, row.timestamp);
96
256
  settleLifecycle(session, item.tool_use_id, item.is_error ? 'failed' : 'done', row.timestamp);
97
257
  addLifecycle(session, {
98
258
  id: `result:${item.tool_use_id || id}`,
@@ -136,11 +296,18 @@ function createClaudeParser(dependencies) {
136
296
  function processMessageRow(session, state, row) {
137
297
  const role = row.message.role === 'assistant' ? 'assistant' : 'user';
138
298
  const internalUserRow = role === 'user' && Boolean(row.isMeta || row.sourceToolUseID);
299
+ state.lastTurnFinished = false;
139
300
  state.lastRole = role;
140
301
  if (row.message.model) session.model = row.message.model;
141
302
  const content = Array.isArray(row.message.content)
142
303
  ? row.message.content
143
304
  : [{ type: 'text', text: row.message.content }];
305
+ content.filter(item => item && (!item.type || item.type === 'text'))
306
+ .forEach((item) => {
307
+ const notification = recordTaskNotification(state, typeof item === 'string' ? item : item.text, row.timestamp);
308
+ recordClaudeTaskCompletion(session, notification, row.timestamp);
309
+ });
310
+ if (session.depth && role === 'user') state.subagentCompletedAt = null;
144
311
  content.forEach((item, index) => {
145
312
  if (internalUserRow && (!item.type || item.type === 'text')) return;
146
313
  recordContent(session, state, row, item, index);
@@ -176,6 +343,10 @@ function createClaudeParser(dependencies) {
176
343
  }
177
344
  content.filter(item => item && item.type === 'tool_use' && isUserInputTool(item.name))
178
345
  .forEach(item => state.pendingUserInputCalls.add(String(item.id || item.name)));
346
+ if (String(row.message.stop_reason || '').toLowerCase() === 'end_turn') {
347
+ state.lastTurnFinished = true;
348
+ if (session.depth) state.subagentCompletedAt = timestamp(row.timestamp, state.latestTs);
349
+ }
179
350
  }
180
351
  if (role === 'assistant' && row.message.usage) {
181
352
  const key = row.requestId || row.message.id || row.uuid;
@@ -198,6 +369,7 @@ function createClaudeParser(dependencies) {
198
369
  executionTracker: createExecutionTracker({ compactText, timestamp }),
199
370
  latestTs: session.updatedAt,
200
371
  lastTurnFinished: false,
372
+ subagentCompletedAt: null,
201
373
  };
202
374
  for (const row of rows) {
203
375
  state.latestTs = timestamp(row.timestamp, state.latestTs);
@@ -206,6 +378,8 @@ function createClaudeParser(dependencies) {
206
378
  if (row.gitBranch) session.branch = row.gitBranch;
207
379
  if (row.agentId && session.depth) session.agentName = row.agentId;
208
380
  if (row.type === 'queue-operation' && row.operation === 'enqueue' && row.content) {
381
+ const notification = recordTaskNotification(state, row.content, row.timestamp);
382
+ recordClaudeTaskCompletion(session, notification, row.timestamp);
209
383
  const detectedUtility = utilityKind(row.content);
210
384
  if (detectedUtility) session.utilityKind = detectedUtility;
211
385
  const visibleUser = visibleUserText(row.content);
@@ -245,7 +419,6 @@ function createClaudeParser(dependencies) {
245
419
  session.updatedAt = state.latestTs;
246
420
  session.startedAt = timestamp(parsed.rows[0].timestamp, session.updatedAt);
247
421
  session.usage = sumUsage([...state.requestUsage.values()]);
248
- session.executions = state.executionTracker.finalize();
249
422
  const utilityTitle = session.utilityKind === 'memory-extraction'
250
423
  ? 'Claude 백그라운드 메모리 추출'
251
424
  : (session.utilityKind === 'authentication-check' ? 'Claude 인증 점검' : '');
@@ -260,10 +433,17 @@ function createClaudeParser(dependencies) {
260
433
  const conversationalInput = state.lastConversationRole === 'assistant'
261
434
  && assistantRequestsUserResponse(state.lastAssistantText)
262
435
  && (state.lastTurnFinished || age >= ACTIVE_THRESHOLD_MS);
263
- if (!session.depth && (pendingUserInput || conversationalInput)) {
436
+ if (session.depth && state.subagentCompletedAt) {
437
+ session.status = 'completed';
438
+ session.statusDetail = '작업 완료';
439
+ session.completedAt = state.subagentCompletedAt;
440
+ session.completionObserved = true;
441
+ session.statusObserved = false;
442
+ session.result = state.lastAssistantText || session.result;
443
+ } else if (!session.depth && (pendingUserInput || conversationalInput)) {
264
444
  session.status = 'waiting';
265
445
  session.statusDetail = pendingUserInput ? '선택 또는 입력 대기' : '답변 또는 선택 대기';
266
- } else if (age < ACTIVE_THRESHOLD_MS && !state.lastTurnFinished) {
446
+ } else if (age < STALE_TURN_THRESHOLD_MS && !state.lastTurnFinished) {
267
447
  session.status = 'running';
268
448
  session.statusDetail = state.lastRole === 'user' ? '응답 생성 중' : '도구 실행 또는 스트리밍 중';
269
449
  } else if (state.lastRole === 'user' && age < STALE_TURN_THRESHOLD_MS) {
@@ -273,6 +453,11 @@ function createClaudeParser(dependencies) {
273
453
  session.status = 'idle';
274
454
  session.statusDetail = state.lastRole === 'user' ? '마지막 응답 기록이 종료됨' : '다음 요청 대기';
275
455
  }
456
+ session.executions = reconcileExecutionActivities(state.executionTracker.finalize(), {
457
+ staleAfterMs: STALE_TURN_THRESHOLD_MS,
458
+ turnFinished: state.lastTurnFinished,
459
+ waitingForUser: session.status === 'waiting',
460
+ });
276
461
  session.statusObserved = age < ACTIVE_THRESHOLD_MS;
277
462
  trimSession(session);
278
463
  return session;
@@ -18,6 +18,7 @@ function createCodexCollaboration(session, dependencies) {
18
18
  assignmentObserved: false,
19
19
  assignmentProtected: false,
20
20
  assignmentSource: 'unavailable',
21
+ assignmentContext: '',
21
22
  sharedGoal: '',
22
23
  status: 'requested',
23
24
  startedAt: null,
@@ -232,20 +232,21 @@ function createCodexParser(dependencies) {
232
232
  : record.agentPath;
233
233
  record.assignmentProtected = messageProtected;
234
234
  const directAssignment = rawMessage && !messageProtected ? rawMessage : '';
235
- record.assignment = directAssignment || (messageProtected ? state.latestDelegationNarration : '');
236
- record.assignmentObserved = Boolean(record.assignment);
237
- record.assignmentSource = directAssignment ? 'spawn-message' : (record.assignment ? 'parent-narration' : 'unavailable');
235
+ record.assignment = directAssignment;
236
+ record.assignmentObserved = Boolean(directAssignment);
237
+ record.assignmentSource = directAssignment ? 'spawn-message' : (messageProtected ? 'protected' : 'unavailable');
238
+ record.assignmentContext = messageProtected ? compactText(state.latestDelegationNarration, 6000) : '';
238
239
  record.sharedGoal = compactText(state.latestUser, 6000);
239
240
  record.startedAt = timestamp(row.timestamp, record.startedAt || session.updatedAt);
240
241
  state.collaboration.addCommunication({
241
242
  id: `assign:${callId}`,
242
243
  kind: 'assignment',
243
- label: record.assignmentSource === 'parent-narration' ? '메인 AI 작업 지시' : '새 작업 배정',
244
+ label: '새 작업 배정',
244
245
  from: session.agentPath || '/root',
245
246
  to: record.agentPath,
246
247
  taskName: record.taskName,
247
248
  text: record.assignment,
248
- protected: record.assignmentProtected && !record.assignment,
249
+ protected: record.assignmentProtected,
249
250
  assignmentSource: record.assignmentSource,
250
251
  timestamp: row.timestamp,
251
252
  });