loadtoagent 1.3.4 → 1.3.6

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 (35) hide show
  1. package/docs/PROVIDER-CONTRACTS.md +3 -1
  2. package/package.json +4 -2
  3. package/renderer/app-dashboard.js +75 -13
  4. package/renderer/app-drawer-content.js +189 -8
  5. package/renderer/app-drawer.js +4 -3
  6. package/renderer/app-events-dialogs.js +6 -1
  7. package/renderer/app-events-filters.js +80 -2
  8. package/renderer/app-events-sessions.js +110 -17
  9. package/renderer/app-graph-model.js +8 -5
  10. package/renderer/app-graph-orchestration.js +43 -11
  11. package/renderer/app-graph-view.js +75 -14
  12. package/renderer/app-quality.js +2 -0
  13. package/renderer/app-session-render.js +7 -7
  14. package/renderer/app.js +104 -0
  15. package/renderer/i18n-messages.js +46 -9
  16. package/renderer/index.html +33 -7
  17. package/renderer/styles-components.css +218 -1
  18. package/renderer/styles-control-room.css +820 -29
  19. package/renderer/styles-readability.css +1 -9
  20. package/renderer/styles-responsive-shell.css +22 -1
  21. package/renderer/styles-terminal.css +21 -31
  22. package/renderer/styles-workflow-map.css +8 -0
  23. package/renderer/terminal-events.js +0 -13
  24. package/renderer/terminal-workbench.js +1 -4
  25. package/src/agentMonitor/claudeParser.js +189 -4
  26. package/src/agentMonitor/codexCollaboration.js +1 -0
  27. package/src/agentMonitor/codexParser.js +6 -5
  28. package/src/agentMonitor/executionActivity.js +26 -1
  29. package/src/agentMonitor/hierarchy.js +6 -3
  30. package/src/agentMonitor.js +10 -3
  31. package/src/contracts.js +1 -1
  32. package/src/monitorWorker.js +2 -0
  33. package/src/nodePtyRuntime.js +58 -0
  34. package/src/terminalHost.js +104 -5
  35. package/src/terminalManager.js +5 -1
@@ -20,6 +20,9 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
20
20
  statusClass,
21
21
  currentActivity,
22
22
  isLiveSession,
23
+ isControlRoomSession = isLiveSession,
24
+ controlRoomStatus = session => session?.status,
25
+ sessionRetentionMinutes = () => 0,
23
26
  subagentWorkState,
24
27
  subagentWorkLabel,
25
28
  latestWorkCopy,
@@ -30,6 +33,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
30
33
  executionModeBadge,
31
34
  graphDescendantCount,
32
35
  sessionWorkspaceLabel,
36
+ controlRoomProject = session => ({ key: String(session?.workspace || session?.id || "unknown"), label: sessionWorkspaceLabel(session) }),
33
37
  } = context;
34
38
  const t = (key, params) => window.LoadToAgentI18n.t(key, params);
35
39
  const statusLabel = (status) => ({
@@ -44,6 +48,8 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
44
48
  const usage = session.usage || {};
45
49
  const percent = Math.max(0, Math.min(100, Number(context.percent || 0)));
46
50
  const running = isLiveSession(session);
51
+ const presentationStatus = controlRoomStatus(session);
52
+ const retained = isControlRoomSession(session) && !running;
47
53
  const childCount = (session.childIds || []).length;
48
54
  const childMetrics = session.collaboration && session.collaboration.metrics;
49
55
  const cumulativeChildren = childMetrics ? childMetrics.cumulativeCreated : childCount;
@@ -72,10 +78,12 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
72
78
  <span class="provider-mark">${esc(provider.mark)}</span>
73
79
  <span class="agent-identity"><b>${esc(role)}</b><small>${esc(provider.label)} · ${esc(session.model || t("graph.model_unknown"))}</small></span>
74
80
  ${executionModeBadge(session, true)}
75
- <span class="status-pill ${statusClass(session.status)}">${esc(statusLabel(session.status))}</span>
81
+ <span class="status-pill ${statusClass(presentationStatus)}">${esc(statusLabel(presentationStatus))}</span>
76
82
  </span>
77
83
  <span class="agent-task-label">
78
- ${session.parentId ? t("graph.assigned_task", { source: delegation.assignmentSource === "parent-narration" ? t("graph.main_ai_explanation_suffix") : "" }) : t("graph.current_goal")}
84
+ ${session.parentId ? t("graph.assigned_task", { source: delegation.assignmentSource === "protected"
85
+ ? t("graph.assignment_protected_suffix")
86
+ : (delegation.assignmentSource === "parent-narration" ? t("graph.main_ai_explanation_suffix") : "") }) : t("graph.current_goal")}
79
87
  </span>
80
88
  <strong class="agent-task" title="${esc(goalPreview.full)}">${esc(goalPreview.text)}</strong>
81
89
  ${goalPreview.truncated ? `<span class="agent-goal-note">${esc(t("graph.summary_shown"))}</span>` : ""}
@@ -103,6 +111,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
103
111
  <span>${session.parentId
104
112
  ? (cumulativeChildren ? t("graph.subagents_created", { count: cumulativeChildren }) : t("graph.helper_ai"))
105
113
  : t("project.origin_named", { name: sessionWorkspaceLabel(session) })}</span>
114
+ ${retained ? `<button type="button" data-session-archive="${esc(session.id)}">${esc(t("control.move_to_history"))}</button>` : ""}
106
115
  <button type="button" data-open-session="${esc(session.id)}">${esc(t("graph.view_conversation"))} <b>↗</b>
107
116
  </button>
108
117
  </footer>
@@ -113,6 +122,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
113
122
  const provider = providerInfo(session.provider);
114
123
  const usage = session.usage || {};
115
124
  const directChildren = graphChildren(session, model).length;
125
+ const presentationStatus = controlRoomStatus(session);
116
126
  const identity = session.parentId
117
127
  ? t("graph.helper_ai_named", { name: session.agentName || agentRoleLabel(session.agentRole) })
118
128
  : t("project.origin_named", { name: sessionWorkspaceLabel(session) });
@@ -124,8 +134,9 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
124
134
  const outcomeText = outcome || latestWorkCopy(session);
125
135
  const assignedWorkPreview = readablePreview(assignedWork, session.parentId ? 110 : 104);
126
136
  const taskLabel = session.parentId ? `${label || agentRoleLabel(session.agentRole)}${taskName ? t("graph.assigned_name_suffix", { name: taskName }) : ""}` : label;
127
- const assignmentSourceNote =
128
- session.parentId && delegation.assignmentSource === "parent-narration"
137
+ const assignmentSourceNote = session.parentId && delegation.assignmentSource === "protected"
138
+ ? `<span class="agent-flow-assignment-source">${esc(t("graph.assignment_source_protected"))}</span>`
139
+ : session.parentId && delegation.assignmentSource === "parent-narration"
129
140
  ? `<span class="agent-flow-assignment-source">${esc(t("graph.main_ai_prestart_explanation"))}</span>`
130
141
  : "";
131
142
  const sharedGoalCopy =
@@ -173,7 +184,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
173
184
  </span>
174
185
  </button>`;
175
186
  }
176
- return `<button type="button" class="agent-flow-row ${isLiveSession(session) ? "running" : ""} ${statusClass(session.status)}"
187
+ return `<button type="button" class="agent-flow-row ${isLiveSession(session) ? "running" : ""} ${statusClass(presentationStatus)}"
177
188
  data-graph-focus="${esc(session.id)}"
178
189
  data-motion-key="agent:${esc(session.id)}"
179
190
  data-motion-value="${esc(session.updatedAt || "")}:${usage.total || 0}:${esc(session.status || "")}"
@@ -185,7 +196,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
185
196
  <em>${esc(identity)} · ${directChildren ? `${t("graph.helper_ai_count", { count: directChildren })} · ` : ""}${esc(timeAgo(session.updatedAt))}</em>
186
197
  ${assignmentSourceNote}${sharedGoalCopy}${outcomeCopy}
187
198
  </span>
188
- <span class="agent-flow-provider"><i>${esc(provider.mark)}</i><small>${esc(statusLabel(session.status))}</small></span>
199
+ <span class="agent-flow-provider"><i>${esc(provider.mark)}</i><small>${esc(statusLabel(presentationStatus))}</small></span>
189
200
  </button>`;
190
201
  }
191
202
 
@@ -418,7 +429,8 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
418
429
  const executionLabel = t("control.runtime_work", { runtime, kind: executionKind });
419
430
  const ownerGoal = controlRoomAgentGoal(owner, 52);
420
431
  const running = activity.status === "running";
421
- return `<button type="button" class="control-room-node execution-node ${running ? "is-running" : "is-complete"}"
432
+ const stateClass = running ? "is-running" : (activity.status === "unverified" ? "is-unverified" : "is-complete");
433
+ return `<button type="button" class="control-room-node execution-node ${stateClass}"
422
434
  data-open-execution-owner="${esc(owner.id)}"
423
435
  data-open-execution-id="${esc(activity.id)}"
424
436
  data-control-summary="${esc(summary.text)}"
@@ -433,10 +445,13 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
433
445
 
434
446
  function controlRoomSession(root, model) {
435
447
  const provider = providerInfo(root.provider);
448
+ const presentationStatus = controlRoomStatus(root);
449
+ const waiting = presentationStatus === "waiting";
450
+ const retained = isControlRoomSession(root) && !isLiveSession(root);
436
451
  const descendants = controlRoomDescendants(root, model);
437
452
  const actors = [root, ...descendants];
438
453
  const executionItems = actors.flatMap(owner => (owner.executions || []).map(activity => ({ activity, owner })));
439
- const activeChildren = descendants.filter(child => !["completed", "cancelled", "failed"].includes(child.status) && !child.completionObserved);
454
+ const activeChildren = descendants.filter(child => ["starting", "running", "paused", "waiting"].includes(child.status) && !child.completionObserved);
440
455
  const completedChildren = descendants.filter(child => ["completed", "cancelled", "failed"].includes(child.status) || child.completionObserved);
441
456
  const activeExecutions = executionItems.filter(item => item.activity.status === "running");
442
457
  const completedExecutions = executionItems
@@ -454,7 +469,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
454
469
  data-control-summary="${esc(title.text)}"
455
470
  data-motion-key="control-main:${esc(root.id)}" data-motion-value="${esc(root.updatedAt || "")}:${esc(root.status || "")}"
456
471
  style="${providerStyle(root.provider)}">
457
- <span class="control-main-top"><span class="provider-mark">${esc(provider.mark)}</span><span><small>${esc(t("control.main_agent"))}</small><b>${esc(provider.label)} · ${esc(root.model || t("session.model_unknown"))}</b></span><em><i aria-hidden="true"></i>${esc(statusLabel(root.status))}</em></span>
472
+ <span class="control-main-top"><span class="provider-mark">${esc(provider.mark)}</span><span><small>${esc(t("control.main_agent"))}</small><b>${esc(provider.label)} · ${esc(root.model || t("session.model_unknown"))}</b></span><em><i aria-hidden="true"></i>${esc(statusLabel(presentationStatus))}</em></span>
458
473
  <strong title="${esc(title.full)}">${esc(title.text)}</strong>
459
474
  <span class="control-main-now"><small>${esc(t("graph.current_work"))}</small><b title="${esc(current.full)}">${esc(current.text)}</b></span>
460
475
  <span class="control-main-meta"><small>${esc(t("control.unit_counts", { helpers: activeChildren.length, executions: activeExecutions.length }))}</small><b>${esc(t("graph.view_conversation"))} →</b></span>
@@ -467,8 +482,16 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
467
482
  const completed = completedUnits.length
468
483
  ? completedUnits.map(unit => unit.kind === "child" ? controlRoomChildNode(unit.child) : controlRoomExecutionNode(unit.item)).join("")
469
484
  : `<div class="control-room-complete-empty"><span>✓</span><small>${esc(t("control.completed_empty"))}</small></div>`;
470
- return `<article class="control-room-session" data-control-session="${esc(root.id)}" style="${providerStyle(root.provider)}">
471
- <header><div><span class="control-session-live"><i></i>${esc(t("control.live_session"))}</span><b>${esc(title.text)}</b></div><span class="session-order-actions control-session-order" role="group" aria-label="${esc(t("session.change_position"))}"><button type="button" data-session-order-move="${esc(root.id)}" data-session-order-offset="-1" title="${esc(t("session.move_up"))}" aria-label="${esc(t("session.move_up"))}">↑</button><button type="button" data-session-order-move="${esc(root.id)}" data-session-order-offset="1" title="${esc(t("session.move_down"))}" aria-label="${esc(t("session.move_down"))}">↓</button></span><button type="button" data-graph-focus="${esc(root.id)}">${esc(t("control.open_full_flow"))} ↗</button></header>
485
+ const waitingWithBackground = waiting && activeExecutions.some(item => item.activity.mode === "background" || item.activity.kind === "background");
486
+ const sessionStateKey = waitingWithBackground
487
+ ? "control.waiting_background_session"
488
+ : (waiting ? "control.waiting_session" : "control.live_session");
489
+ const retention = retained ? `<small class="control-session-retention">${esc(t("control.auto_history_in_minutes", { minutes: sessionRetentionMinutes(root) }))}</small>` : "";
490
+ const archive = retained ? `<button type="button" class="control-session-archive" data-session-archive="${esc(root.id)}">${esc(t("control.move_to_history"))}</button>` : "";
491
+ return `<article class="control-room-session ${waiting ? "is-waiting" : ""} ${waitingWithBackground ? "has-background-work" : ""}" data-control-session="${esc(root.id)}" data-session-sortable="${esc(root.id)}"
492
+ style="${providerStyle(root.provider)}" role="group" tabindex="0" draggable="true" aria-grabbed="false"
493
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown" aria-label="${esc(t("session.drag_label", { title: title.text }))}" aria-describedby="sessionReorderHelp">
494
+ <header><div><span class="control-session-live"><i></i>${esc(t(sessionStateKey))}</span><b>${esc(title.text)}</b>${retention}</div><span class="session-drag-handle" aria-hidden="true" title="${esc(t("session.reorder_hint"))}"></span>${archive}<button type="button" class="control-session-flow" data-graph-focus="${esc(root.id)}">${esc(t("control.open_full_flow"))} ↗</button></header>
472
495
  <div class="control-room-flow">
473
496
  <section class="control-room-column main-column"><span class="control-column-label">${esc(t("control.main_work_column"))}</span>${main}</section>
474
497
  <span class="control-flow-link live" aria-hidden="true"><i></i></span>
@@ -479,9 +502,44 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
479
502
  </article>`;
480
503
  }
481
504
 
482
- function runtimeSeparatedOverview(roots, model) {
505
+ function runtimeSeparatedOverview(roots, model, allRoots = roots) {
506
+ const projectDescriptor = (root) => {
507
+ const project = controlRoomProject(root);
508
+ return { key: project.key, name: project.label };
509
+ };
510
+ const allGroups = new Map();
511
+ allRoots.forEach((root) => {
512
+ const { key, name } = projectDescriptor(root);
513
+ if (!allGroups.has(key)) allGroups.set(key, { name, roots: [] });
514
+ allGroups.get(key).roots.push(root);
515
+ });
516
+ const groups = new Map();
517
+ roots.forEach((root) => {
518
+ const { key, name } = projectDescriptor(root);
519
+ if (!groups.has(key)) groups.set(key, { name, roots: [] });
520
+ groups.get(key).roots.push(root);
521
+ });
522
+ const projectGroups = [...groups.entries()].map(([key, { name, roots: projectRoots }], index) => {
523
+ const projectTotals = allGroups.get(key)?.roots || projectRoots;
524
+ const activeCount = projectTotals.filter((root) => isLiveSession(root)).length;
525
+ const attentionCount = projectTotals.filter((root) => !isLiveSession(root) && isControlRoomSession(root)).length;
526
+ const summary = attentionCount
527
+ ? t("control.project_live_attention_summary", { active: activeCount, attention: attentionCount })
528
+ : t("control.project_live_summary", { active: activeCount });
529
+ const disclosureKey = `control-project:${key}`;
530
+ const presentation = index === 0 ? "is-primary" : index === 1 ? "is-preview" : "is-collapsed";
531
+ const projectFocusId = projectRoots[0]?.id || "";
532
+ return `<details class="control-room-project-group ${presentation}" data-control-project="${esc(name)}" data-disclosure-key="${esc(disclosureKey)}" ${index < 2 ? "open" : ""}>
533
+ <summary class="control-project-header" data-project-toggle="${esc(name)}">
534
+ <span class="control-project-heading"><i aria-hidden="true">□</i><b>${esc(name)}</b><small>${esc(summary)}</small><em>${projectTotals.length}</em></span>
535
+ <span class="control-project-handle" role="img" aria-label="프로젝트 그룹" title="프로젝트 그룹 · 세션은 최근 활동순으로 표시됩니다"></span>
536
+ </summary>
537
+ <button type="button" class="control-project-flow-link" data-graph-focus="${esc(projectFocusId)}"><span>${esc(t("control.open_full_flow"))} ↗</span></button>
538
+ <div class="control-project-body" ${index === 1 ? 'inert aria-hidden="true"' : ""}>${projectRoots.map(root => controlRoomSession(root, model)).join("")}</div>
539
+ </details>`;
540
+ }).join("");
483
541
  return `<div class="control-room-overview" data-control-room-overview="true">
484
- ${sortGraphNodes(roots).map(root => controlRoomSession(root, model)).join("")}
542
+ ${projectGroups}
485
543
  </div>`;
486
544
  }
487
545
 
@@ -609,7 +667,9 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
609
667
  : event.kind === "started"
610
668
  ? t("graph.runtime_start_confirmed")
611
669
  : t("graph.status_only_recorded"));
612
- const sourceLabel = event.assignmentSource === "parent-narration" ? ` · ${t("graph.main_ai_prestart_short")}` : "";
670
+ const sourceLabel = event.assignmentSource === "protected"
671
+ ? ` · ${t("graph.assignment_source_protected_short")}`
672
+ : (event.assignmentSource === "parent-narration" ? ` · ${t("graph.main_ai_prestart_short")}` : "");
613
673
  return `<article class="agent-communication-event ${esc(event.kind)}" data-communication-kind="${esc(event.kind)}">
614
674
  <span class="communication-route">
615
675
  <b>${esc(communicationEndpoint(event.from, owner, model))}</b><i>→</i>
@@ -650,6 +710,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
650
710
  running: t("graph.execution_running"),
651
711
  completed: t("graph.execution_completed"),
652
712
  failed: t("graph.execution_failed"),
713
+ unverified: t("graph.execution_unverified"),
653
714
  cancelled: t("ui.stopped"),
654
715
  })[activity.status] || activity.status;
655
716
  }
@@ -94,6 +94,7 @@ window.LoadToAgentAppFactories.createQualityEnhancements = function createQualit
94
94
  ? dashboard.workspace
95
95
  : "all";
96
96
  state.sort = ["recent", "tokens", "context"].includes(dashboard.sort) ? dashboard.sort : "recent";
97
+ state.controlRoomSort = ["recent", "tokens", "context"].includes(dashboard.controlRoomSort) ? dashboard.controlRoomSort : "recent";
97
98
  state.sessionOrder = Array.isArray(dashboard.sessionOrder)
98
99
  ? dashboard.sessionOrder.filter(id => typeof id === "string" && id.length <= 500).slice(0, 1_000)
99
100
  : [];
@@ -130,6 +131,7 @@ window.LoadToAgentAppFactories.createQualityEnhancements = function createQualit
130
131
  providers: [...state.providerFilters],
131
132
  workspace: String(state.workspace || "all").slice(0, 2_000),
132
133
  sort: ["recent", "tokens", "context"].includes(state.sort) ? state.sort : "recent",
134
+ controlRoomSort: ["recent", "tokens", "context"].includes(state.controlRoomSort) ? state.controlRoomSort : "recent",
133
135
  sessionOrder: (state.sessionOrder || []).filter(id => typeof id === "string" && id.length <= 500).slice(0, 1_000),
134
136
  };
135
137
  try {
@@ -24,6 +24,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
24
24
  statusClass,
25
25
  currentActivity,
26
26
  isLiveSession,
27
+ isControlRoomSession = isLiveSession,
27
28
  latestWorkCopy,
28
29
  statusIcon,
29
30
  renderProviderRail,
@@ -75,11 +76,13 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
75
76
  const originLabel = sessionWorkspaceLabel(session);
76
77
  return `<article class="session-card session-record ${opts.live ? "live-card" : ""} ${statusClass(session.status)} ${session.parentId ? "subagent" : ""}"
77
78
  data-session-id="${esc(session.id)}"
79
+ data-session-sortable="${esc(session.id)}"
78
80
  data-motion-key="session:${esc(session.id)}"
79
81
  data-motion-value="${esc(session.updatedAt || "")}:${esc(session.status || "")}"
80
82
  style="${providerStyle(session.provider)}"
81
- role="button" tabindex="0"
82
- aria-labelledby="${accessibleId}-title" aria-describedby="${accessibleId}-summary">
83
+ role="button" tabindex="0" draggable="true" aria-grabbed="false"
84
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
85
+ aria-labelledby="${accessibleId}-title" aria-describedby="${accessibleId}-summary sessionReorderHelp">
83
86
  <div class="card-head">
84
87
  <span class="provider-mark">${esc(provider.mark)}</span>
85
88
  <div class="card-head-main"><div class="card-provider-line"><b>${esc(provider.label)}</b><span>${esc(session.model || t("session.model_unknown"))}</span></div></div>
@@ -95,10 +98,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
95
98
  </div>
96
99
  <footer class="card-footer">
97
100
  <span>${esc(timeAgo(session.updatedAt))}</span>
98
- <span class="session-order-actions" role="group" aria-label="${esc(t("session.change_position"))}">
99
- <button type="button" data-session-order-move="${esc(session.id)}" data-session-order-offset="-1" title="${esc(t("session.move_up"))}" aria-label="${esc(t("session.move_up"))}">↑</button>
100
- <button type="button" data-session-order-move="${esc(session.id)}" data-session-order-offset="1" title="${esc(t("session.move_down"))}" aria-label="${esc(t("session.move_down"))}">↓</button>
101
- </span>
101
+ <span class="session-drag-handle" aria-hidden="true" title="${esc(t("session.reorder_hint"))}"></span>
102
102
  <strong>${esc(t("graph.view_conversation"))}<i aria-hidden="true">→</i></strong>
103
103
  </footer>
104
104
  </article>`;
@@ -166,7 +166,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
166
166
  const attentionCount = attentionView ? renderAttentionInbox() : 0;
167
167
  const showMap = ["all", "active"].includes(state.view);
168
168
  const graphLiveCount = showMap ? renderAgentMap(graphFilteredSessions(), motionKind) : 0;
169
- const regular = state.view === "all" ? sessions.filter((session) => !isLiveSession(session)) : state.view === "active" ? [] : sessions;
169
+ const regular = state.view === "all" ? sessions.filter((session) => !isControlRoomSession(session)) : state.view === "active" ? [] : sessions;
170
170
  const visible = regular.slice(0, state.visibleLimit);
171
171
  const resultCount = attentionView ? attentionCount : graphLiveCount + regular.length;
172
172
  $("#sessionResultSummary").textContent = window.LoadToAgentI18n.t("quality.results_summary", { count: resultCount });
package/renderer/app.js CHANGED
@@ -5,6 +5,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
5
5
  const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
6
  const observedText = (value) => window.LoadToAgentI18n.observedText(value);
7
7
  const PROJECTLESS_WORKSPACE = "__projectless__";
8
+ const SESSION_RETENTION_MS = 30 * 60 * 1000;
9
+ const SESSION_ARCHIVE_STORAGE_KEY = "loadtoagent:session-archives:v1";
8
10
  const state = {
9
11
  providers: [],
10
12
  providerMap: new Map(),
@@ -22,6 +24,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
22
24
  search: "",
23
25
  sort: "recent",
24
26
  sessionOrder: [],
27
+ sessionArchives: new Map(),
28
+ controlRoomObservedIds: new Set(),
25
29
  selectedId: null,
26
30
  drawerTab: "chat",
27
31
  drawerMode: "session",
@@ -32,6 +36,9 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
32
36
  drawerForceLatest: false,
33
37
  visibleLimit: 30,
34
38
  graphFocusId: null,
39
+ controlRoomPage: 0,
40
+ controlRoomPageSize: 4,
41
+ controlRoomSort: "recent",
35
42
  supervisionFocusId: null,
36
43
  graphExpandedProviders: new Set(),
37
44
  expandedExecutionSessions: new Set(),
@@ -566,6 +573,91 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
566
573
  function isLiveSession(session) {
567
574
  return session && (session.status === "running" || session.status === "starting");
568
575
  }
576
+ function hasRunningExecution(session) {
577
+ return Boolean((session?.executions || []).some((execution) => execution?.status === "running"));
578
+ }
579
+ function sessionResponseTimestamp(session) {
580
+ const assistantAt = Math.max(0, ...(session?.messages || [])
581
+ .filter((message) => message?.role === "assistant")
582
+ .map((message) => Date.parse(message.timestamp || 0))
583
+ .filter(Number.isFinite));
584
+ if (assistantAt) return assistantAt;
585
+ const completedAt = Date.parse(session?.completedAt || session?.endedAt || 0);
586
+ return Number.isFinite(completedAt) ? completedAt : 0;
587
+ }
588
+ function loadSessionArchives() {
589
+ try {
590
+ const saved = JSON.parse(localStorage.getItem(SESSION_ARCHIVE_STORAGE_KEY) || "{}");
591
+ state.sessionArchives = new Map(Object.entries(saved)
592
+ .filter(([id, value]) => id && Number.isFinite(Number(value?.responseAt)))
593
+ .map(([id, value]) => [id, { responseAt: Number(value.responseAt), archivedAt: Number(value.archivedAt || 0) }]));
594
+ } catch (error) {
595
+ reportRecoverableError("session-archives-load", error);
596
+ state.sessionArchives = new Map();
597
+ }
598
+ }
599
+ function saveSessionArchives() {
600
+ try {
601
+ const recent = [...state.sessionArchives.entries()]
602
+ .sort((left, right) => Number(right[1]?.archivedAt || 0) - Number(left[1]?.archivedAt || 0))
603
+ .slice(0, 500);
604
+ state.sessionArchives = new Map(recent);
605
+ localStorage.setItem(SESSION_ARCHIVE_STORAGE_KEY, JSON.stringify(Object.fromEntries(recent)));
606
+ } catch (error) {
607
+ reportRecoverableError("session-archives-save", error);
608
+ }
609
+ }
610
+ function isSessionManuallyArchived(session) {
611
+ const archived = session && state.sessionArchives.get(String(session.id || ""));
612
+ if (!archived) return false;
613
+ return sessionResponseTimestamp(session) <= Number(archived.responseAt || 0);
614
+ }
615
+ function isControlRoomSession(session, now = Date.now()) {
616
+ if (!session) return false;
617
+ if (isLiveSession(session) || hasRunningExecution(session)) {
618
+ state.controlRoomObservedIds.add(String(session.id || ""));
619
+ return true;
620
+ }
621
+ if (!state.controlRoomObservedIds.has(String(session.id || ""))) return false;
622
+ const responseAt = sessionResponseTimestamp(session);
623
+ const retained = Boolean(responseAt
624
+ && !isSessionManuallyArchived(session)
625
+ && Math.max(0, Number(now) - responseAt) < SESSION_RETENTION_MS);
626
+ if (!retained && responseAt && Math.max(0, Number(now) - responseAt) >= SESSION_RETENTION_MS) {
627
+ state.controlRoomObservedIds.delete(String(session.id || ""));
628
+ }
629
+ return retained;
630
+ }
631
+ function controlRoomStatus(session, now = Date.now()) {
632
+ return isLiveSession(session) ? session.status : (isControlRoomSession(session, now) ? "waiting" : session?.status);
633
+ }
634
+ function sessionRetentionMinutes(session, now = Date.now()) {
635
+ const remaining = SESSION_RETENTION_MS - Math.max(0, Number(now) - sessionResponseTimestamp(session));
636
+ return Math.max(0, Math.ceil(remaining / 60_000));
637
+ }
638
+ function archiveSession(sessionOrId) {
639
+ const session = typeof sessionOrId === "object"
640
+ ? sessionOrId
641
+ : (state.snapshot?.sessions || []).find((item) => item.id === String(sessionOrId || ""));
642
+ if (!session || isLiveSession(session)) return false;
643
+ const responseAt = sessionResponseTimestamp(session);
644
+ if (!responseAt) return false;
645
+ const archivedAt = Date.now();
646
+ const sessions = state.snapshot?.sessions || [];
647
+ const byId = new Map(sessions.map((item) => [item.id, item]));
648
+ const queue = [session, ...(session.childIds || []).map((id) => byId.get(id)).filter(Boolean)];
649
+ const seen = new Set();
650
+ while (queue.length) {
651
+ const item = queue.shift();
652
+ if (!item?.id || seen.has(item.id)) continue;
653
+ seen.add(item.id);
654
+ const itemResponseAt = sessionResponseTimestamp(item);
655
+ if (itemResponseAt) state.sessionArchives.set(String(item.id), { responseAt: itemResponseAt, archivedAt });
656
+ queue.push(...(item.childIds || []).map((id) => byId.get(id)).filter(Boolean));
657
+ }
658
+ saveSessionArchives();
659
+ return true;
660
+ }
569
661
  function isRuntimeLoopSession(session) {
570
662
  if (!session || session.parentId || !isLiveSession(session)) return false;
571
663
  if (session.loop === true || (session.loop && typeof session.loop === "object")) return true;
@@ -627,6 +719,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
627
719
  if (/end|complete/.test(type)) return "✓";
628
720
  return "·";
629
721
  }
722
+ loadSessionArchives();
630
723
  return {
631
724
  $,
632
725
  $$,
@@ -636,6 +729,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
636
729
  reportRecoverableError,
637
730
  observedText,
638
731
  PROJECTLESS_WORKSPACE,
732
+ SESSION_RETENTION_MS,
733
+ SESSION_ARCHIVE_STORAGE_KEY,
639
734
  state,
640
735
  motionPreference,
641
736
  motionState,
@@ -681,6 +776,15 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
681
776
  statusClass,
682
777
  currentActivity,
683
778
  isLiveSession,
779
+ hasRunningExecution,
780
+ sessionResponseTimestamp,
781
+ loadSessionArchives,
782
+ saveSessionArchives,
783
+ isSessionManuallyArchived,
784
+ isControlRoomSession,
785
+ controlRoomStatus,
786
+ sessionRetentionMinutes,
787
+ archiveSession,
684
788
  isRuntimeLoopSession,
685
789
  subagentWorkState,
686
790
  subagentWorkLabel,
@@ -351,6 +351,18 @@
351
351
  "drawer.progress_updates": {"ko":"진행 업데이트 {count}개","en":"{count} progress updates","zh-CN":"{count} 条进度更新"},
352
352
  "drawer.progress_updates_help": {"ko":"중간 작업 설명과 원문 전체 보기","en":"View intermediate notes and full text","zh-CN":"查看中间说明和完整原文"},
353
353
  "drawer.progress_update_item": {"ko":"진행 {count}","en":"Update {count}","zh-CN":"进度 {count}"},
354
+ "drawer.subagent_called": {"ko":"서브에이전트 호출","en":"Helper agent called","zh-CN":"已调用协助智能体"},
355
+ "drawer.subagent_call_point": {"ko":"서브에이전트 호출 #{count}","en":"Helper-agent call #{count}","zh-CN":"协助智能体调用 #{count}"},
356
+ "drawer.called_after_user_request": {"ko":"사용자 요청 {elapsed} 후 호출","en":"Called {elapsed} after the user request","zh-CN":"用户请求 {elapsed} 后调用"},
357
+ "drawer.duration_less_than_second": {"ko":"1초 미만","en":"less than 1s","zh-CN":"不到 1 秒"},
358
+ "drawer.duration_seconds": {"ko":"{seconds}초","en":"{seconds}s","zh-CN":"{seconds} 秒"},
359
+ "drawer.duration_minutes_seconds": {"ko":"{minutes}분 {seconds}초","en":"{minutes}m {seconds}s","zh-CN":"{minutes} 分 {seconds} 秒"},
360
+ "drawer.duration_hours_minutes_seconds": {"ko":"{hours}시간 {minutes}분 {seconds}초","en":"{hours}h {minutes}m {seconds}s","zh-CN":"{hours} 小时 {minutes} 分 {seconds} 秒"},
361
+ "drawer.called_after_main_message": {"ko":"메인 에이전트가 이 말 다음에 호출","en":"The lead agent called a helper after this message","zh-CN":"主智能体在这段话之后调用了协助智能体"},
362
+ "drawer.main_called_here": {"ko":"메인 에이전트가 이 지점에서 서브에이전트를 호출","en":"The lead agent called a helper at this point","zh-CN":"主智能体在此处调用了协助智能体"},
363
+ "drawer.main_progress_before_call": {"ko":"메인 에이전트 중간 응답","en":"Lead-agent progress update","zh-CN":"主智能体中间回复"},
364
+ "drawer.subagent_name": {"ko":"호출된 서브에이전트 · {name}","en":"Called helper · {name}","zh-CN":"调用的协助智能体 · {name}"},
365
+ "drawer.open_subagent_work": {"ko":"작업 내용 보기","en":"View work","zh-CN":"查看工作内容"},
354
366
  "drawer.activities": {"ko":"활동 {count}건","en":"{count} activities","zh-CN":"{count} 条活动"},
355
367
  "drawer.latest_conversation": {"ko":"가장 최근 대화","en":"Latest conversation","zh-CN":"最新对话"},
356
368
  "drawer.no_lifecycle": {"ko":"아직 기록된 진행 과정이 없습니다","en":"No progress has been recorded yet","zh-CN":"尚未记录进度"},
@@ -381,6 +393,12 @@
381
393
  "drawer.subagent_history_actual": {"ko":"이 도움 AI 세션에 실제로 저장된 작업 지시와 답변을 시간순으로 표시합니다.","en":"Shows the instructions and responses actually saved in this helper-AI session, in chronological order.","zh-CN":"按时间顺序显示此协助 AI 会话中实际保存的指令和回答。"},
382
394
  "drawer.subagent_work_history": {"ko":"도움 AI 실제 작업 기록","en":"Actual helper-AI work history","zh-CN":"协助 AI 实际工作记录"},
383
395
  "drawer.assignment": {"ko":"작업 지시","en":"Assignment","zh-CN":"任务指令"},
396
+ "drawer.assignment_context": {"ko":"호출 직전 메인 AI 설명 (실제 전달 원문 아님)","en":"Lead-AI explanation before the call (not the transmitted prompt)","zh-CN":"调用前主 AI 的说明(不是实际传递的原文)"},
397
+ "drawer.assignment_protected": {"ko":"실제 작업 지시 원문은 제공사 로그에서 암호화되어 있어 확인할 수 없습니다.","en":"The exact assignment is encrypted in the provider log and cannot be verified.","zh-CN":"实际任务指令原文在提供商日志中已加密,无法核实。"},
398
+ "drawer.assignment_protected_short": {"ko":"실제 지시 원문 보호됨","en":"Exact prompt protected","zh-CN":"实际指令原文受保护"},
399
+ "drawer.assignment_source_claude": {"ko":"Claude Agent/Task 호출 prompt 원문","en":"Exact Claude Agent/Task call prompt","zh-CN":"Claude Agent/Task 调用的 prompt 原文"},
400
+ "drawer.assignment_source_codex": {"ko":"Codex spawn_agent 호출 message 원문","en":"Exact Codex spawn_agent call message","zh-CN":"Codex spawn_agent 调用的 message 原文"},
401
+ "drawer.assignment_source_protected": {"ko":"제공사 로그에서 원문 보호됨","en":"Exact text protected in provider log","zh-CN":"原文在提供商日志中受保护"},
384
402
  "drawer.sub_ai": {"ko":"도움 AI","en":"Helper AI","zh-CN":"协助 AI"},
385
403
  "drawer.work_history": {"ko":"작업 기록","en":"Work history","zh-CN":"工作记录"},
386
404
  "drawer.execution_unit": {"ko":"실행 작업","en":"Execution unit","zh-CN":"执行任务"},
@@ -416,9 +434,9 @@
416
434
  "session.total_tokens": {"ko":"전체 사용","en":"Total usage","zh-CN":"总用量"},
417
435
  "session.subagents_created": {"ko":"도움 AI {count}개 누적 생성","en":"{count} helper AI created","zh-CN":"累计创建 {count} 个协助 AI"},
418
436
  "session.local_history": {"ko":"내 PC의 작업 기록","en":"Work history on this PC","zh-CN":"此电脑上的工作记录"},
419
- "session.change_position": {"ko":"세션 위치 변경","en":"Change session position","zh-CN":"调整会话位置"},
420
- "session.move_up": {"ko":"위로 이동","en":"Move up","zh-CN":"上移"},
421
- "session.move_down": {"ko":"아래로 이동","en":"Move down","zh-CN":"下移"},
437
+ "session.drag_label": {"ko":"{title} 위치 변경","en":"Reorder {title}","zh-CN":"调整 {title} 的位置"},
438
+ "session.reorder_hint": {"ko":"끌어서 위치 변경 · Alt+↑/↓ 키도 사용 가능","en":"Drag to reorder · Alt+↑/↓ also works","zh-CN":"拖动调整位置 · 也可使用 Alt+↑/↓"},
439
+ "session.reorder_help": {"ko":"세션 카드를 끌어 놓아 위치를 바꿀 수 있습니다. 키보드에서는 카드에 초점을 둔 뒤 Alt와 위아래 화살표 키를 사용하세요.","en":"Drag and drop a session card to change its position. With a keyboard, focus the card and use Alt with the up or down arrow key.","zh-CN":"拖放会话卡片可调整位置。使用键盘时,请聚焦卡片并按 Alt 加上或下方向键。"},
422
440
  "session.position_changed": {"ko":"세션 위치를 변경했습니다.","en":"Session position updated.","zh-CN":"会话位置已更新。"},
423
441
  "bootstrap.open_in_app": {"ko":"LoadToAgent 프로그램에서 열면 이 컴퓨터의 AI 작업 기록을 불러옵니다.","en":"Open this page in LoadToAgent to load AI work history from this computer.","zh-CN":"请在 LoadToAgent 应用中打开此页面,以加载此电脑上的 AI 工作记录。"},
424
442
  "bootstrap.opened_attention_list": {"ko":"확인이 필요한 세션 목록을 열었습니다.","en":"Opened the list of sessions that need your review.","zh-CN":"已打开需要您确认的会话列表。"},
@@ -541,6 +559,7 @@
541
559
  "graph.execution_recent_hidden": {"ko":"이전 실행 기록 {count}개는 상세 기록에 보존됩니다.","en":"{count} older execution records remain available in session history.","zh-CN":"会话历史中仍保留 {count} 条较早的执行记录。"},
542
560
  "graph.execution_show_older": {"ko":"이전 실행 {count}개 더 보기","en":"Show {count} older executions","zh-CN":"再显示 {count} 条较早运行"},
543
561
  "graph.execution_running": {"ko":"실행 중","en":"Running","zh-CN":"运行中"},
562
+ "graph.execution_unverified": {"ko":"상태 미확인","en":"Status unverified","zh-CN":"状态未确认"},
544
563
  "graph.execution_runtime": {"ko":"실행 환경","en":"Runtime","zh-CN":"运行环境"},
545
564
  "graph.execution_started": {"ko":"시작","en":"Started","zh-CN":"开始"},
546
565
  "graph.execution_status_label": {"ko":"상태","en":"Status","zh-CN":"状态"},
@@ -563,6 +582,9 @@
563
582
  "graph.main_ai_in_charge": {"ko":"전체 일을 맡은 메인 AI","en":"Main AI in charge of the whole task","zh-CN":"负责整体工作的主 AI"},
564
583
  "graph.main_ai_prestart_explanation": {"ko":"메인 AI가 작업 시작 직전에 설명한 내용","en":"Description provided by the main AI just before the task started","zh-CN":"主 AI 在任务开始前说明的内容"},
565
584
  "graph.main_ai_prestart_short": {"ko":"작업 시작 직전 메인 AI 설명","en":"Main AI description before start","zh-CN":"任务开始前的主 AI 说明"},
585
+ "graph.assignment_protected_suffix": {"ko":" · 실제 지시 원문 보호됨","en":" · Exact prompt protected","zh-CN":" · 实际指令原文受保护"},
586
+ "graph.assignment_source_protected": {"ko":"실제 지시 원문은 암호화되어 확인할 수 없음","en":"The exact prompt is encrypted and unavailable","zh-CN":"实际指令原文已加密,无法查看"},
587
+ "graph.assignment_source_protected_short": {"ko":"실제 지시 원문 보호됨","en":"Exact prompt protected","zh-CN":"实际指令原文受保护"},
566
588
  "graph.major_tasks_and_agents": {"ko":"큰 일 {tasks}개 · 작업 중 도움 AI {active}명 · 도움 AI 기록 {records}명","en":"{tasks} major tasks · {active} active helper AI · {records} helper records","zh-CN":"{tasks} 个主要任务 · {active} 个工作中的协助 AI · {records} 条协助 AI 记录"},
567
589
  "graph.memory_usage": {"ko":"기억 공간 사용","en":"Memory usage","zh-CN":"记忆空间使用"},
568
590
  "graph.model_unknown": {"ko":"모델 정보 없음","en":"Model information unavailable","zh-CN":"无模型信息"},
@@ -666,11 +688,8 @@
666
688
  "time.minutes_ago": {"ko":"{count}분 전","en":"{count} min ago","zh-CN":"{count} 分钟前"},
667
689
  "time.seconds_ago": {"ko":"{count}초 전","en":"{count} sec ago","zh-CN":"{count} 秒前"},
668
690
  "terminal.activity_details": {"ko":"활동 {count}건은 상세에서 확인","en":"See {count} activities in details","zh-CN":"在详情中查看 {count} 条活动"},
669
- "terminal.move_down": {"ko":"{title} 아래로 이동","en":"Move {title} down","zh-CN":"下移 {title}"},
670
- "terminal.move_up": {"ko":"{title} 위로 이동","en":"Move {title} up","zh-CN":"上移 {title}"},
671
- "terminal.reorder_group": {"ko":"{title} 순서 변경","en":"Reorder {title}","zh-CN":"调整 {title} 的顺序"},
672
691
  "terminal.reorder_hint": {"ko":"끌어서 순서 변경 · Alt+↑/↓ 키도 사용 가능","en":"Drag to reorder · Alt+↑/↓ also works","zh-CN":"拖动排序 · 也可使用 Alt+↑/↓"},
673
- "terminal.reorder_help": {"ko":"세션 옆의 위아래 버튼을 누르거나, 세션에 초점을 둔 뒤 Alt와 위아래 화살표 키를 눌러 순서를 바꿀 수 있습니다.","en":"Use the up and down buttons beside a session, or focus it and press Alt with the arrow keys to reorder.","zh-CN":"可使用会话旁的上下按钮,或聚焦会话后按 Alt 加方向键调整顺序。"},
692
+ "terminal.reorder_help": {"ko":"세션을 끌어 놓아 순서를 바꿀 수 있습니다. 키보드에서는 세션에 초점을 둔 뒤 Alt와 위아래 화살표 키를 사용하세요.","en":"Drag and drop a session to reorder it. With a keyboard, focus the session and use Alt with the up or down arrow key.","zh-CN":"拖放会话可调整顺序。使用键盘时,请聚焦会话并按 Alt 加上或下方向键。"},
674
693
  "terminal.reordered": {"ko":"세션 순서를 변경했습니다.","en":"Session order updated.","zh-CN":"会话顺序已更新。"},
675
694
  "terminal.agent.ai_terminal": {"ko":"AI 명령창","en":"AI terminal","zh-CN":"AI 终端"},
676
695
  "terminal.agent.command_required": {"ko":"AI에게 보낼 지시를 입력하세요.","en":"Enter instructions to send to the AI.","zh-CN":"请输入要发送给 AI 的指令。"},
@@ -1324,8 +1343,10 @@
1324
1343
  "control.attention_eyebrow": {"ko":"확인 필요","en":"Action needed","zh-CN":"需要处理"},
1325
1344
  "control.attention_more": {"ko":"나머지 {count}개 모두 보기","en":"View {count} more","zh-CN":"查看其余 {count} 项"},
1326
1345
  "control.attention_title": {"ko":"지금 개입할 작업 {count}","en":"{count} tasks need action","zh-CN":"{count} 个任务需要处理"},
1327
- "control.completed_empty": {"ko":"아직 완료된 실행 단위가 없습니다","en":"No execution unit has completed yet","zh-CN":"尚无已完成的执行单元"},
1328
- "control.completed_work_column": {"ko":"최근 끝난 작업","en":"Recently finished work","zh-CN":"最近完成的工作"},
1346
+ "control.add_project": {"ko":"프로젝트 추가","en":"Add project","zh-CN":"添加项目"},
1347
+ "control.all_projects": {"ko":"모든 프로젝트","en":"All projects","zh-CN":"所有项目"},
1348
+ "control.completed_empty": {"ko":"아직 최근 실행 기록이 없습니다","en":"No recent execution record yet","zh-CN":"尚无最近的执行记录"},
1349
+ "control.completed_work_column": {"ko":"최근 끝난 작업","en":"Recent work","zh-CN":"最近的工作"},
1329
1350
  "control.background_execution": {"ko":"{owner} 백그라운드 작업","en":"{owner} background work","zh-CN":"{owner}后台工作"},
1330
1351
  "control.background_work_kind": {"ko":"백그라운드 작업","en":"Background work","zh-CN":"后台任务"},
1331
1352
  "control.command_execution": {"ko":"{owner} 실행 작업","en":"{owner} command work","zh-CN":"{owner}命令工作"},
@@ -1338,6 +1359,11 @@
1338
1359
  "control.legend_spawn": {"ko":"생성 중","en":"Spawning","zh-CN":"创建中"},
1339
1360
  "control.live_activity": {"ko":"실시간 활동","en":"Live activity","zh-CN":"实时活动"},
1340
1361
  "control.live_session": {"ko":"실행 중인 세션","en":"Live session","zh-CN":"运行中的会话"},
1362
+ "control.waiting_session": {"ko":"대기 중인 세션","en":"Waiting session","zh-CN":"等待中的会话"},
1363
+ "control.waiting_background_session": {"ko":"응답 대기 · 백그라운드 실행 중","en":"Waiting for response · background work running","zh-CN":"等待回复 · 后台任务运行中"},
1364
+ "control.auto_history_in_minutes": {"ko":"{minutes}분 뒤 지난 기록으로 이동","en":"Moves to history in {minutes} min","zh-CN":"{minutes} 分钟后移至历史记录"},
1365
+ "control.move_to_history": {"ko":"지난 기록으로 이동","en":"Move to history","zh-CN":"移至历史记录"},
1366
+ "control.moved_to_history": {"ko":"세션을 지난 기록으로 옮겼습니다.","en":"Session moved to history.","zh-CN":"会话已移至历史记录。"},
1341
1367
  "control.live_summary": {"ko":"메인 {sessions} · 서브 {helpers} · 실행 {executions}","en":"{sessions} lead · {helpers} helper · {executions} commands","zh-CN":"主 {sessions} · 协助 {helpers} · 执行 {executions}"},
1342
1368
  "control.main_assignment": {"ko":"메인 에이전트가 시킨 일","en":"Assigned by the lead agent","zh-CN":"主智能体分配的任务"},
1343
1369
  "control.main_agent": {"ko":"메인 에이전트","en":"Lead agent","zh-CN":"主智能体"},
@@ -1351,7 +1377,18 @@
1351
1377
  "control.open_full_flow": {"ko":"전체 구조 보기","en":"Open full structure","zh-CN":"查看完整结构"},
1352
1378
  "control.open_remaining_units": {"ko":"나머지 {count}개 보기","en":"View {count} more","zh-CN":"查看其余 {count} 个"},
1353
1379
  "control.open_subagent": {"ko":"서브에이전트 작업 ‘{task}’ 대화 열기","en":"Open conversation for helper task ‘{task}’","zh-CN":"打开协助任务“{task}”的对话"},
1380
+ "control.open_search": {"ko":"실행 세션 검색 열기","en":"Open live-session search","zh-CN":"打开运行会话搜索"},
1381
+ "control.other_projects": {"ko":"기타","en":"Other","zh-CN":"其他"},
1354
1382
  "control.other_sessions": {"ko":"다른 세션","en":"Other sessions","zh-CN":"其他会话"},
1383
+ "control.page_summary": {"ko":"{start}–{end} / {total}","en":"{start}–{end} / {total}","zh-CN":"{start}–{end} / {total}"},
1384
+ "control.previous_page": {"ko":"이전 실행 세션","en":"Previous live sessions","zh-CN":"上一页运行会话"},
1385
+ "control.next_page": {"ko":"다음 실행 세션","en":"Next live sessions","zh-CN":"下一页运行会话"},
1386
+ "control.project_filter": {"ko":"프로젝트별 실행 세션 필터","en":"Filter live sessions by project","zh-CN":"按项目筛选运行会话"},
1387
+ "control.project_live_attention_summary": {"ko":"실행 중 {active} · 확인 필요 {attention}","en":"{active} running · {attention} need attention","zh-CN":"运行中 {active} · 需确认 {attention}"},
1388
+ "control.project_live_summary": {"ko":"실행 중 {active}","en":"{active} running","zh-CN":"运行中 {active}"},
1389
+ "control.recent_activity": {"ko":"최근 활동순","en":"Recent activity","zh-CN":"最近活动"},
1390
+ "control.search_sessions": {"ko":"실행 세션 검색","en":"Search live sessions","zh-CN":"搜索运行会话"},
1391
+ "control.sort_sessions": {"ko":"실행 세션 정렬","en":"Sort live sessions","zh-CN":"排序运行会话"},
1355
1392
  "control.recently_completed": {"ko":"방금 완료","en":"Just completed","zh-CN":"刚刚完成"},
1356
1393
  "control.subagent": {"ko":"서브에이전트","en":"Helper agent","zh-CN":"协助智能体"},
1357
1394
  "control.subagent_short": {"ko":"서브","en":"Helper","zh-CN":"协助"},