loadtoagent 1.3.3 → 1.3.4

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 (36) hide show
  1. package/main.js +53 -8
  2. package/package.json +2 -1
  3. package/renderer/app-agent-actions.js +172 -66
  4. package/renderer/app-bootstrap.js +3 -1
  5. package/renderer/app-dashboard.js +36 -7
  6. package/renderer/app-drawer-content.js +177 -59
  7. package/renderer/app-drawer-data.js +7 -3
  8. package/renderer/app-drawer.js +72 -33
  9. package/renderer/app-events-dialogs.js +8 -1
  10. package/renderer/app-events-navigation.js +7 -0
  11. package/renderer/app-events-sessions.js +136 -13
  12. package/renderer/app-graph-model.js +2 -5
  13. package/renderer/app-graph-orchestration.js +7 -18
  14. package/renderer/app-graph-view.js +194 -48
  15. package/renderer/app-management.js +330 -35
  16. package/renderer/app-quality.js +5 -0
  17. package/renderer/app-session-render.js +20 -83
  18. package/renderer/app.js +5 -1
  19. package/renderer/i18n-messages.js +232 -21
  20. package/renderer/index.html +9 -6
  21. package/renderer/styles-components.css +140 -0
  22. package/renderer/styles-control-room.css +1439 -0
  23. package/renderer/styles-management.css +390 -3
  24. package/renderer/styles-responsive-shell.css +5 -0
  25. package/renderer/styles-runtime-overview.css +20 -0
  26. package/renderer/terminal-agent.js +31 -7
  27. package/renderer/terminal.js +4 -1
  28. package/src/agentMonitor/claudeParser.js +8 -4
  29. package/src/agentMonitor/codexParser.js +5 -4
  30. package/src/agentMonitor/genericParser.js +7 -6
  31. package/src/agentMonitor.js +44 -4
  32. package/src/attentionNotifier.js +3 -0
  33. package/src/ipc/registerTerminalIpc.js +2 -2
  34. package/src/monitorWorker.js +1 -1
  35. package/src/processMonitor.js +68 -7
  36. package/src/terminalManager.js +16 -2
package/main.js CHANGED
@@ -32,6 +32,8 @@ process.title = PRODUCT_NAME;
32
32
  if (process.platform === 'win32') app.setAppUserModelId('com.wincube.loadtoagent');
33
33
 
34
34
  const demoCapture = process.env.LOADTOAGENT_DEMO_CAPTURE === '1';
35
+ // 사용자 메시지와 실제 개입 요청을 안정적으로 구분할 때까지 알림 발송을 중지한다.
36
+ const ATTENTION_NOTIFICATIONS_ENABLED = false;
35
37
  let mainWindow = null;
36
38
  let monitorWorker = null;
37
39
  let runner = null;
@@ -219,23 +221,35 @@ function createWindow() {
219
221
  mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
220
222
  const allowedUrl = pathToFileURL(path.join(__dirname, 'renderer', 'index.html')).href;
221
223
  mainWindow.webContents.on('will-navigate', (event, url) => { if (url !== allowedUrl) event.preventDefault(); });
222
- mainWindow.once('ready-to-show', () => mainWindow && mainWindow.show());
224
+ const showWindow = () => {
225
+ if (!mainWindow || mainWindow.isDestroyed()) return;
226
+ mainWindow.show();
227
+ mainWindow.focus();
228
+ };
229
+ const showFallback = setTimeout(showWindow, 2_000);
230
+ mainWindow.once('ready-to-show', () => {
231
+ clearTimeout(showFallback);
232
+ showWindow();
233
+ });
223
234
  mainWindow.on('close', event => {
224
235
  if (isQuitting || !backgroundTerminalSessions().length) return;
225
236
  event.preventDefault();
226
237
  mainWindow.hide();
227
238
  ensureBackgroundTray();
228
239
  });
229
- mainWindow.on('closed', () => { mainWindow = null; });
240
+ mainWindow.on('closed', () => {
241
+ clearTimeout(showFallback);
242
+ mainWindow = null;
243
+ });
230
244
  }
231
245
 
232
246
  function backgroundTerminalSessions() {
233
247
  if (!terminalManager) return [];
234
- return terminalManager.list().filter(session => session.status === 'running' || session.status === 'starting');
248
+ return terminalManager.list().filter(session => !session.transient && (session.status === 'running' || session.status === 'starting'));
235
249
  }
236
250
 
237
251
  function visibleTerminalSessions(sessions) {
238
- return (sessions || []).filter(session => session.type !== 'agent' || isProviderVisible(session.provider));
252
+ return (sessions || []).filter(session => !session.transient && (session.type !== 'agent' || isProviderVisible(session.provider)));
239
253
  }
240
254
 
241
255
  function showMainWindow() {
@@ -334,6 +348,7 @@ function markRendererReady() {
334
348
 
335
349
  function createAttentionNotifier() {
336
350
  return new AttentionNotifier({
351
+ enabled: ATTENTION_NOTIFICATIONS_ENABLED,
337
352
  Notification,
338
353
  isSupported: () => Notification.isSupported(),
339
354
  copy: session => {
@@ -364,6 +379,36 @@ function installationType() {
364
379
  return fs.existsSync(path.join(__dirname, '.git')) ? 'source' : 'npm';
365
380
  }
366
381
 
382
+ async function connectTerminalForStartup(timeoutMs = 4_000) {
383
+ const connection = terminalManager.connect();
384
+ let timedOut = false;
385
+ let timer = null;
386
+ connection.then(() => {
387
+ if (!timedOut) return;
388
+ const sessions = visibleTerminalSessions(terminalManager.list());
389
+ sendTerminal('terminals:state', { change: 'reconnected', session: null, sessions });
390
+ sendTerminal('terminals:connection', { state: 'connected', message: mainText('terminalHostReconnected') });
391
+ updateBackgroundTrayMenu();
392
+ }).catch(error => {
393
+ if (timedOut) reportRecoverableError('terminal-host-late-connect', error);
394
+ });
395
+ try {
396
+ await Promise.race([
397
+ connection,
398
+ new Promise((_, reject) => {
399
+ timer = setTimeout(() => {
400
+ timedOut = true;
401
+ reject(new Error('터미널 호스트 연결을 백그라운드에서 계속합니다.'));
402
+ }, timeoutMs);
403
+ }),
404
+ ]);
405
+ } catch (error) {
406
+ reportRecoverableError('terminal-host-startup-connect', error);
407
+ } finally {
408
+ if (timer) clearTimeout(timer);
409
+ }
410
+ }
411
+
367
412
  async function installDownloadedUpdate() {
368
413
  if (!updateManager) throw new Error('업데이트 관리자가 준비되지 않았습니다.');
369
414
  const downloaded = await updateManager.download();
@@ -428,7 +473,7 @@ async function setupRuntime() {
428
473
  }
429
474
  terminalManager.on('data', payload => sendTerminal('terminals:data', payload));
430
475
  terminalManager.on('state', payload => {
431
- if (!payload.session || payload.session.type !== 'agent' || isProviderVisible(payload.session.provider)) {
476
+ if (!payload.session || (!payload.session.transient && (payload.session.type !== 'agent' || isProviderVisible(payload.session.provider)))) {
432
477
  sendTerminal('terminals:state', { ...payload, sessions: visibleTerminalSessions(payload.sessions) });
433
478
  }
434
479
  updateBackgroundTrayMenu();
@@ -450,7 +495,7 @@ async function setupRuntime() {
450
495
  message: mainText('terminalHostReconnectFailed', { reason: error?.message || String(error) }),
451
496
  });
452
497
  });
453
- await terminalManager.connect();
498
+ await connectTerminalForStartup();
454
499
  availability = probeProviders();
455
500
  monitorWorker = new Worker(path.join(__dirname, 'src', 'monitorWorker.js'), {
456
501
  workerData: { runsDir, home: os.homedir(), intervalMs: 1200, availability },
@@ -480,7 +525,7 @@ function bridgePresence() {
480
525
  if (!terminalManager) return [];
481
526
  const environment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
482
527
  return terminalManager.list()
483
- .filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
528
+ .filter(session => !session.transient && session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
484
529
  .map(session => ({
485
530
  id: session.bridgeId || session.id,
486
531
  bridgeId: session.bridgeId || '',
@@ -528,7 +573,7 @@ function requestAgentDetail(sessionId) {
528
573
  if (!pendingDetails.has(requestId)) return;
529
574
  pendingDetails.delete(requestId);
530
575
  resolve(null);
531
- }, 4000);
576
+ }, 15000);
532
577
  pendingDetails.set(requestId, {
533
578
  resolve: value => {
534
579
  clearTimeout(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtoagent",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
5
5
  "main": "main.js",
6
6
  "author": "wincube AX",
@@ -53,6 +53,7 @@
53
53
  "test:scroll": "electron scripts/scroll-retention-check.js",
54
54
  "audit:ui": "electron scripts/browser-audit-host.js",
55
55
  "test:runtime-overview": "electron scripts/runtime-overview-visual.js",
56
+ "test:control-room": "electron scripts/control-room-visual.js",
56
57
  "test:readability": "electron scripts/readability-visual-check.js",
57
58
  "test:update:mac": "node scripts/mac-update-integration-test.js",
58
59
  "demo:readme": "electron scripts/readme-demo.js",
@@ -45,27 +45,94 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
45
45
  return null;
46
46
  }
47
47
 
48
+ function agentCommandRouteOptions(session) {
49
+ if (!session?.parentId) return [];
50
+ let parent = snapshotSession(session.parentId) || state.details.get(session.parentId) || null;
51
+ const visited = new Set([session.id]);
52
+ while (parent?.parentId && !visited.has(parent.id)) {
53
+ visited.add(parent.id);
54
+ parent = snapshotSession(parent.parentId) || state.details.get(parent.parentId) || parent;
55
+ }
56
+ const directTargets = isLiveSession(session) ? agentCommandTargets(session) : [];
57
+ const parentTargets = parent ? agentCommandTargets(parent) : [];
58
+ return [
59
+ { id: "direct", label: t("agent.route_direct"), available: directTargets.length > 0, targetSession: session, targets: directTargets },
60
+ { id: "parent", label: t("agent.route_parent"), available: Boolean(parent && (parentTargets.length || agentResumeSupport(parent).supported)), targetSession: parent, targets: parentTargets },
61
+ ];
62
+ }
63
+
64
+ function selectedAgentCommandRoute(session) {
65
+ if (!session?.parentId) return "direct";
66
+ const options = agentCommandRouteOptions(session);
67
+ const saved = state.agentCommandRoutes.get(session.id);
68
+ if (options.some(option => option.id === saved && option.available)) return saved;
69
+ const selected = options.find(option => option.id === "direct" && option.available)
70
+ || options.find(option => option.id === "parent" && option.available)
71
+ || options.find(option => option.id === "direct");
72
+ const route = selected?.id || "direct";
73
+ state.agentCommandRoutes.set(session.id, route);
74
+ return route;
75
+ }
76
+
77
+ function routedAgentCommandContext(session, requestedRoute = "") {
78
+ const route = requestedRoute || selectedAgentCommandRoute(session);
79
+ const options = agentCommandRouteOptions(session);
80
+ const selected = options.find(option => option.id === route) || null;
81
+ return {
82
+ route,
83
+ options,
84
+ targetSession: selected?.targetSession || session,
85
+ targets: selected?.targets || agentCommandTargets(session),
86
+ available: session?.parentId ? Boolean(selected?.available) : true,
87
+ };
88
+ }
89
+
90
+ function agentCommandTargetKey(session, route = "direct") {
91
+ return session?.parentId ? `${session.id}:${route}` : session.id;
92
+ }
93
+
48
94
  function agentControlMode(session, targets) {
49
- if (targets.length) return "direct";
50
- if (originAppInfo(session)) return !isLiveSession(session) && agentResumeSupport(session).supported ? "origin-resume" : "origin";
51
- if (agentResumeSupport(session).supported) return isLiveSession(session) ? "handoff" : "resume";
95
+ if (targets.length && isLiveSession(session)) return "direct";
96
+ const resume = agentResumeSupport(session);
97
+ if (resume.supported) {
98
+ if (originAppInfo(session)) return "origin-resume";
99
+ return isLiveSession(session) ? "handoff" : "resume";
100
+ }
52
101
  if (isLiveSession(session)) return "connect";
53
102
  return "ended";
54
103
  }
55
104
 
56
- function agentCommandComposer(session) {
57
- const targets = agentCommandTargets(session);
58
- const mode = agentControlMode(session, targets);
59
- const savedTarget = state.agentCommandTargets.get(session.id) || "";
60
- const targetId = targets.some((target) => target.id === savedTarget) ? savedTarget : targets.length === 1 ? targets[0].id : "";
61
- if (targetId) state.agentCommandTargets.set(session.id, targetId);
105
+ function agentCommandComposer(session, options = {}) {
106
+ const routingEnabled = Boolean(options.conversation && session.parentId);
107
+ const routeContext = routingEnabled
108
+ ? routedAgentCommandContext(session)
109
+ : {
110
+ route: "direct",
111
+ options: [],
112
+ targetSession: session,
113
+ targets: agentCommandTargets(session),
114
+ available: true,
115
+ };
116
+ const { route, targetSession, targets, available: routeAvailable } = routeContext;
117
+ const mode = routingEnabled && !routeAvailable ? "ended" : agentControlMode(targetSession, targets);
118
+ const relayed = routingEnabled && route === "parent" && routeAvailable;
119
+ const targetKey = agentCommandTargetKey(session, route);
120
+ const savedTarget = state.agentCommandTargets.get(targetKey) || "";
121
+ const relayTarget = relayed ? targets.find((target) => target.kind === "terminal") || targets[0] || null : null;
122
+ const targetId = targets.some((target) => target.id === savedTarget)
123
+ ? savedTarget
124
+ : targets.length === 1
125
+ ? targets[0].id
126
+ : relayTarget?.id || "";
127
+ if (targetId) state.agentCommandTargets.set(targetKey, targetId);
62
128
  const target = targets.find((item) => item.id === targetId) || null;
63
129
  const draft = state.agentCommandDrafts.get(session.id) || "";
64
130
  const sending = state.agentCommandSending.has(session.id);
65
131
  const canSend = ((mode === "direct" && Boolean(target)) || ["resume", "handoff", "origin-resume"].includes(mode)) && !sending;
66
- const origin = originAppInfo(session);
67
- const status =
68
- mode === "direct"
132
+ const origin = originAppInfo(targetSession);
133
+ const status = relayed
134
+ ? t("agent.route_parent_status")
135
+ : mode === "direct"
69
136
  ? t("agent.direct_status", { target: targets.length === 1 ? target.label : t("agent.choose_terminal_count", { count: targets.length }) })
70
137
  : mode === "handoff"
71
138
  ? t("agent.handoff_status")
@@ -75,11 +142,10 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
75
142
  ? t("agent.origin_resume_status")
76
143
  : mode === "connect"
77
144
  ? t("agent.connect_status")
78
- : mode === "origin"
79
- ? t("agent.origin_status")
80
- : window.LoadToAgentI18n.t("ui.ended_session");
81
- const help =
82
- mode === "direct"
145
+ : window.LoadToAgentI18n.t("ui.ended_session");
146
+ const help = relayed
147
+ ? t("agent.route_parent_inline_help")
148
+ : mode === "direct"
83
149
  ? t("agent.direct_help")
84
150
  : mode === "handoff"
85
151
  ? t("agent.handoff_help")
@@ -88,23 +154,31 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
88
154
  : mode === "origin-resume"
89
155
  ? t("agent.origin_resume_help", { provider: (origin && origin.provider) || t("agent.desktop") })
90
156
  : mode === "connect"
91
- ? t("agent.connect_help", { provider: session.provider })
92
- : mode === "origin"
93
- ? t("agent.origin_help", { app: (origin && origin.label) || t("agent.desktop_app", { provider: "" }).trim() })
94
- : agentResumeSupport(session).reason || t("agent.resume_method_unknown");
157
+ ? t("agent.connect_help", { provider: targetSession.provider })
158
+ : agentResumeSupport(targetSession).reason || t("agent.resume_method_unknown");
159
+ const routePicker = routingEnabled
160
+ ? `<div class="agent-command-route" role="group" aria-label="${esc(t("agent.route_label"))}">
161
+ <span>${esc(t("agent.route_label"))}</span>
162
+ <div>${routeContext.options.map(option => `<button type="button" data-agent-command-session="${esc(session.id)}" data-agent-command-route="${esc(option.id)}"
163
+ aria-pressed="${option.id === route ? "true" : "false"}" ${option.available ? "" : "disabled"}>${esc(option.label)}</button>`).join("")}</div>
164
+ <small class="${routeAvailable ? "available" : "unavailable"}">${esc(t(routeAvailable
165
+ ? route === "direct" ? "agent.route_direct_available" : "agent.route_parent_available"
166
+ : route === "direct" ? "agent.route_direct_unavailable" : "agent.route_parent_unavailable"))}</small>
167
+ </div>`
168
+ : "";
95
169
  const picker =
96
- targets.length > 1
170
+ targets.length > 1 && !relayed
97
171
  ? `<label class="agent-command-target">
98
172
  <span>${esc(t("agent.target_terminal"))}</span>
99
- <select data-agent-command-target="${esc(session.id)}">
173
+ <select data-agent-command-target="${esc(targetKey)}">
100
174
  <option value="">${esc(t("agent.choose_terminal"))}</option>
101
175
  ${targets.map((item) => `<option value="${esc(item.id)}" ${item.id === targetId ? "selected" : ""}>${esc(item.label)}</option>`).join("")}
102
176
  </select>
103
177
  </label>`
104
178
  : "";
105
- const originAction = `<button type="button" data-agent-open-origin="${esc(session.id)}">${esc(t("agent.continue_in_origin", { provider: (origin && origin.provider) || t("agent.desktop") }))}</button>`;
106
- const actions =
107
- mode === "direct"
179
+ const actions = relayed
180
+ ? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.sending" : "agent.send_via_parent"))}</button>`
181
+ : mode === "direct"
108
182
  ? `<button type="button" data-agent-terminal-open="${esc(session.id)}" ${canSend ? "" : "disabled"}>${esc(t("agent.open_terminal"))}</button>
109
183
  <button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.sending" : "agent.send_now"))}</button>`
110
184
  : mode === "resume"
@@ -112,26 +186,25 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
112
186
  : mode === "handoff"
113
187
  ? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.handing_off" : "agent.handoff_and_send"))}</button>`
114
188
  : mode === "origin-resume"
115
- ? `${originAction}<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.connecting" : "agent.background_and_send"))}</button>`
189
+ ? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.connecting" : "agent.background_and_send"))}</button>`
116
190
  : mode === "connect"
117
- ? `<button type="button" data-agent-bridge-copy="${esc(session.provider)}">${esc(t("agent.copy_bridge"))}</button>`
118
- : mode === "origin"
119
- ? originAction
120
- : "";
121
- const editable = ["direct", "resume", "handoff", "origin-resume"].includes(mode);
122
- const placeholder = editable ? t("agent.command_example") : status;
191
+ ? `<button type="button" data-agent-bridge-copy="${esc(targetSession.provider)}">${esc(t("agent.copy_bridge"))}</button>`
192
+ : "";
193
+ const editable = relayed || ["direct", "resume", "handoff", "origin-resume"].includes(mode);
194
+ const placeholder = editable ? t(options.conversation ? "agent.conversation_placeholder" : "agent.command_example") : status;
123
195
  const availabilityClass = mode === "direct" ? "connected" : ["resume", "handoff", "origin-resume"].includes(mode) ? "resume-ready" : "unavailable";
124
- return `<form class="agent-command-panel ${availabilityClass} control-${mode}"
125
- data-agent-command-form="${esc(session.id)}">
196
+ return `<form class="agent-command-panel ${availabilityClass} control-${mode} ${options.conversation ? "conversation-composer" : ""}"
197
+ data-agent-command-form="${esc(session.id)}" data-agent-command-route-selected="${esc(route)}" data-agent-command-routing="${routingEnabled ? "conversation" : "session"}">
126
198
  <header>
127
199
  <span class="agent-command-icon" aria-hidden="true">›_</span>
128
- <span><b>${esc(t("agent.command_title"))}</b><small>${esc(status)}</small></span>
200
+ <span><b>${esc(t(options.conversation ? "agent.conversation_title" : "agent.command_title"))}</b><small>${esc(status)}</small></span>
129
201
  <i class="${mode === "direct" ? "connected" : ""}" aria-hidden="true"></i>
130
202
  </header>
203
+ ${routePicker}
131
204
  ${picker}
132
205
  <label class="agent-command-input">
133
206
  <span class="sr-only">${esc(t("agent.command_sr"))}</span>
134
- <textarea data-agent-command-draft="${esc(session.id)}" maxlength="8000" rows="3"
207
+ <textarea data-agent-command-draft="${esc(session.id)}" maxlength="8000" rows="${options.conversation ? "2" : "3"}"
135
208
  placeholder="${esc(placeholder)}" ${editable ? "" : "disabled"}>${editable ? esc(draft) : ""}</textarea>
136
209
  </label>
137
210
  <div class="agent-command-actions"><small aria-live="polite">${esc(help)}</small>${actions}</div>
@@ -148,9 +221,10 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
148
221
  return ((state.snapshot && state.snapshot.sessions) || []).find((session) => session.id === id) || null;
149
222
  }
150
223
 
151
- function chosenAgentCommandTarget(session) {
152
- const targets = agentCommandTargets(session);
153
- const saved = state.agentCommandTargets.get(session.id) || "";
224
+ function chosenAgentCommandTarget(session, requestedRoute = "") {
225
+ const routeContext = routedAgentCommandContext(session, requestedRoute);
226
+ const targets = routeContext.targets;
227
+ const saved = state.agentCommandTargets.get(agentCommandTargetKey(session, routeContext.route)) || "";
154
228
  if (saved) return targets.find((target) => target.id === saved) || null;
155
229
  return targets.length === 1 ? targets[0] : null;
156
230
  }
@@ -181,16 +255,54 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
181
255
  if (state.agentCommandSending.has(sessionId)) return;
182
256
  const session = snapshotSession(sessionId);
183
257
  if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.latest_not_found"));
184
- const mode = agentControlMode(session, agentCommandTargets(session));
258
+ const routingEnabled = form?.dataset.agentCommandRouting === "conversation" && Boolean(session.parentId);
259
+ const requestedRoute = routingEnabled ? form?.dataset.agentCommandRouteSelected || selectedAgentCommandRoute(session) : "direct";
260
+ const routeContext = routingEnabled
261
+ ? routedAgentCommandContext(session, requestedRoute)
262
+ : { route: "direct", targetSession: session, targets: agentCommandTargets(session), available: true };
263
+ const targetSession = routeContext.targetSession;
264
+ const mode = routingEnabled && !routeContext.available ? "ended" : agentControlMode(targetSession, routeContext.targets);
265
+ if (routingEnabled && !["direct", "resume", "handoff", "origin-resume"].includes(mode)) return context.toast(t("agent.route_unavailable"));
266
+ const input = form.querySelector("[data-agent-command-draft]");
267
+ const command = String((input && input.value) || "").trim();
268
+ const routedCommand = routingEnabled && routeContext.route === "parent"
269
+ ? t("agent.route_via_parent_prompt", {
270
+ task: session.delegation?.taskName || session.taskName || session.agentName || session.title,
271
+ message: command,
272
+ })
273
+ : command;
185
274
  if (mode === "resume" || mode === "handoff" || mode === "origin-resume") {
186
- const input = form.querySelector("[data-agent-command-draft]");
187
275
  if (input) state.agentCommandDrafts.set(sessionId, input.value);
188
- if (!String((input && input.value) || "").trim()) return context.toast(t("agent.enter_command"));
276
+ if (!command) return context.toast(t("agent.enter_command"));
277
+ if (routingEnabled && routeContext.route === "parent") {
278
+ state.agentCommandSending.add(sessionId);
279
+ const submit = form.querySelector('[type="submit"]');
280
+ if (submit) {
281
+ submit.disabled = true;
282
+ submit.textContent = t("agent.sending");
283
+ }
284
+ try {
285
+ await window.LoadToAgentTerminal.resumeForAgent(targetSession, routedCommand, true, { focus: false });
286
+ state.agentCommandDrafts.delete(sessionId);
287
+ if (input) input.value = "";
288
+ context.toast(t("agent.command_routed_via_parent"));
289
+ } catch (error) {
290
+ context.toast(errorText(error, "agent.send_failed"));
291
+ } finally {
292
+ state.agentCommandSending.delete(sessionId);
293
+ if (submit && submit.isConnected) {
294
+ submit.disabled = false;
295
+ submit.textContent = t("agent.send_via_parent");
296
+ }
297
+ }
298
+ return;
299
+ }
189
300
  return resumeAgentTerminal(sessionId, true);
190
301
  }
191
- const target = chosenAgentCommandTarget(session);
192
- const input = form.querySelector("[data-agent-command-draft]");
193
- const command = String((input && input.value) || "").trim();
302
+ const savedTarget = state.agentCommandTargets.get(agentCommandTargetKey(session, routeContext.route)) || "";
303
+ const target = savedTarget
304
+ ? routeContext.targets.find((item) => item.id === savedTarget) || null
305
+ : routeContext.targets.length === 1 ? routeContext.targets[0] : null;
194
306
  if (!target)
195
307
  return context.toast(t(agentCommandTargets(session).length ? "agent.select_target_first" : "agent.no_writable_terminal"));
196
308
  if (!command) return context.toast(t("agent.enter_command"));
@@ -201,11 +313,15 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
201
313
  submit.textContent = t("agent.sending");
202
314
  }
203
315
  try {
204
- await window.LoadToAgentTerminal.dispatchAgentCommand(session, command, target.id);
316
+ await window.LoadToAgentTerminal.dispatchAgentCommand(targetSession, routedCommand, target.id);
205
317
  state.agentCommandDrafts.delete(sessionId);
206
318
  if (input) input.value = "";
207
- context.toast(t("agent.command_sent", { target: target.label }));
319
+ context.toast(t(routingEnabled && routeContext.route === "parent" ? "agent.command_routed_via_parent" : "agent.command_sent", { target: target.label }));
208
320
  } catch (error) {
321
+ if (session.parentId) {
322
+ context.toast(errorText(error, "agent.send_failed"));
323
+ return;
324
+ }
209
325
  const latest = snapshotSession(sessionId) || session;
210
326
  const support = agentResumeSupport(latest);
211
327
  if (!agentCommandTargets(latest).length && support.supported) {
@@ -224,7 +340,7 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
224
340
  state.agentCommandSending.delete(sessionId);
225
341
  if (submit && submit.isConnected) {
226
342
  submit.disabled = false;
227
- submit.textContent = t("agent.send_now");
343
+ submit.textContent = t(routingEnabled && routeContext.route === "parent" ? "agent.send_via_parent" : "agent.send_now");
228
344
  }
229
345
  }
230
346
  }
@@ -232,12 +348,13 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
232
348
  async function openAgentTerminal(sessionId) {
233
349
  const session = snapshotSession(sessionId);
234
350
  if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.terminal_info_not_found"));
235
- const target = chosenAgentCommandTarget(session);
351
+ const routeContext = routedAgentCommandContext(session);
352
+ const target = chosenAgentCommandTarget(session, routeContext.route);
236
353
  if (!target)
237
- return context.toast(t(agentCommandTargets(session).length ? "agent.select_open_target" : "agent.no_writable_terminal"));
354
+ return context.toast(t(routeContext.targets.length ? "agent.select_open_target" : "agent.no_writable_terminal"));
238
355
  selectView(target.kind === "tmux" ? "tmux" : "terminal");
239
356
  try {
240
- await window.LoadToAgentTerminal.openForAgent(session, target.id, state.agentCommandDrafts.get(sessionId) || "");
357
+ await window.LoadToAgentTerminal.openForAgent(routeContext.targetSession, target.id, state.agentCommandDrafts.get(sessionId) || "");
241
358
  document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
242
359
  } catch (error) {
243
360
  context.toast(errorText(error, "agent.open_terminal_failed"));
@@ -256,19 +373,6 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
256
373
  }
257
374
  }
258
375
 
259
- async function openSessionOrigin(sessionId) {
260
- const session = snapshotSession(sessionId);
261
- const origin = originAppInfo(session);
262
- if (!session || !origin) return context.toast(t("agent.origin_not_found"));
263
- try {
264
- const result = await window.loadtoagent.openSessionOrigin(session);
265
- if (!result || !result.ok) return context.toast(t("agent.origin_cannot_open", { app: origin.label }));
266
- context.toast(t("agent.origin_opened", { provider: origin.provider }));
267
- } catch (error) {
268
- context.toast(errorText(error, "agent.origin_open_failed", { app: origin.label }));
269
- }
270
- }
271
-
272
376
  async function controlManagedRun(sessionId, action) {
273
377
  const session = snapshotSession(sessionId) || state.details.get(sessionId);
274
378
  const runId = session && session.runId;
@@ -341,6 +445,9 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
341
445
  agentResumeSupport,
342
446
  originAppInfo,
343
447
  agentControlMode,
448
+ agentCommandRouteOptions,
449
+ selectedAgentCommandRoute,
450
+ routedAgentCommandContext,
344
451
  agentCommandComposer,
345
452
  selectedSession,
346
453
  snapshotSession,
@@ -349,7 +456,6 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
349
456
  dispatchAgentCommand,
350
457
  openAgentTerminal,
351
458
  copyBridgeCommand,
352
- openSessionOrigin,
353
459
  controlManagedRun,
354
460
  quickRespond,
355
461
  prepareReassignment,
@@ -35,7 +35,7 @@
35
35
  ].forEach(install);
36
36
  window.LoadToAgentApp = app;
37
37
 
38
- const { $, esc, state, loadGuideState, loadQualityState = () => {}, loadProviderVisibility, projectVisibleSnapshot, visibleSnapshot, isProviderVisible, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
38
+ const { $, esc, state, loadGuideState, loadQualityState = () => {}, saveDashboardPreferences = () => {}, loadProviderVisibility, projectVisibleSnapshot, visibleSnapshot, isProviderVisible, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
39
39
 
40
40
  let initializationError = "";
41
41
  const showInitializationError = (message) => {
@@ -91,6 +91,7 @@
91
91
  if (window.loadtoagent.onAttentionRequested) window.loadtoagent.onAttentionRequested(handleAttentionRequested);
92
92
  bindEvents();
93
93
  render();
94
+ saveDashboardPreferences();
94
95
  $("#appConnectionState")?.classList.remove("connection-error");
95
96
  $("#appErrorBanner").classList.add("hidden");
96
97
  app.initialized = true;
@@ -101,6 +102,7 @@
101
102
  if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(visibleSnapshot(), state.workspaces);
102
103
  $("#lastSync").textContent = timeOnly(snapshot.generatedAt);
103
104
  render();
105
+ saveDashboardPreferences();
104
106
  if (state.selectedId && $("#detailDrawer").classList.contains("open") && !state.detailLoadingIds.has(state.selectedId)) {
105
107
  const card = (snapshot.sessions || []).find((session) => session.id === state.selectedId);
106
108
  const detail = state.details.get(state.selectedId);
@@ -441,15 +441,42 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
441
441
  }
442
442
  if (state.sort === "tokens") sessions.sort((a, b) => Number((b.usage && b.usage.total) || 0) - Number((a.usage && a.usage.total) || 0));
443
443
  else if (state.sort === "context") sessions.sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0));
444
- else
445
- sessions.sort((a, b) => {
446
- const activeA = a.status === "running" || a.status === "starting" ? 1 : 0;
447
- const activeB = b.status === "running" || b.status === "starting" ? 1 : 0;
448
- return activeB - activeA || Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
449
- });
444
+ else sessions = stableSessionSort(sessions);
450
445
  return sessions;
451
446
  }
452
447
 
448
+ function ensureSessionOrder(sessions = []) {
449
+ if (!Array.isArray(state.sessionOrder)) state.sessionOrder = [];
450
+ const known = new Set(state.sessionOrder);
451
+ for (const session of sessions) {
452
+ const id = String(session?.id || "");
453
+ if (!id || known.has(id)) continue;
454
+ state.sessionOrder.push(id);
455
+ known.add(id);
456
+ }
457
+ return state.sessionOrder;
458
+ }
459
+
460
+ function stableSessionSort(sessions = []) {
461
+ const order = ensureSessionOrder(sessions);
462
+ const rank = new Map(order.map((id, index) => [id, index]));
463
+ return [...sessions].sort((a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER));
464
+ }
465
+
466
+ function moveSessionOrder(sourceId, targetId, placeAfter = false) {
467
+ const source = String(sourceId || "");
468
+ const target = String(targetId || "");
469
+ if (!source || !target || source === target) return false;
470
+ const order = ensureSessionOrder(displaySessions());
471
+ const sourceIndex = order.indexOf(source);
472
+ if (sourceIndex < 0 || !order.includes(target)) return false;
473
+ order.splice(sourceIndex, 1);
474
+ const targetIndex = order.indexOf(target);
475
+ order.splice(targetIndex + (placeAfter ? 1 : 0), 0, source);
476
+ state.sessionOrder = order;
477
+ return true;
478
+ }
479
+
453
480
  function graphFilteredSessions() {
454
481
  let sessions = displaySessions();
455
482
  if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
@@ -462,7 +489,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
462
489
  .toLowerCase()
463
490
  .includes(query),
464
491
  );
465
- return sessions;
492
+ return stableSessionSort(sessions);
466
493
  }
467
494
 
468
495
  function renderProviderVisibilitySettings() {
@@ -501,6 +528,8 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
501
528
  announceProviderFilter,
502
529
  filteredSessions,
503
530
  graphFilteredSessions,
531
+ stableSessionSort,
532
+ moveSessionOrder,
504
533
  renderProviderVisibilitySettings,
505
534
  };
506
535
  };