loadtoagent 1.3.9 → 1.3.11

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.
@@ -2258,3 +2258,402 @@ body details.control-room-project-group > summary.control-project-header {
2258
2258
  animation: none!important;
2259
2259
  }
2260
2260
  }
2261
+
2262
+ /*
2263
+ * Conversation workspace
2264
+ * ----------------------
2265
+ * Live-flow entries open beside the graph and keep terminal controls secondary
2266
+ * until the user explicitly asks for direct terminal input.
2267
+ */
2268
+
2269
+ :root {
2270
+ --conversation-panel-width: clamp(600px,40vw,680px);
2271
+ }
2272
+
2273
+ .drawer-flow-button {
2274
+ min-height: 36px;
2275
+ display: inline-flex;
2276
+ align-items: center;
2277
+ justify-content: center;
2278
+ gap: 7px;
2279
+ padding: 0 10px;
2280
+ border: 1px solid #304050;
2281
+ border-radius: 9px;
2282
+ background: rgba(17,25,36,.92);
2283
+ color: #b9c7d2;
2284
+ font: inherit;
2285
+ font-size: 12px;
2286
+ white-space: nowrap;
2287
+ flex: 0 0 auto;
2288
+ cursor: pointer;
2289
+ transition: border-color var(--motion-fast) ease,background-color var(--motion-fast) ease,color var(--motion-fast) ease,transform var(--motion-fast) ease;
2290
+ }
2291
+
2292
+ .drawer-flow-button:hover {
2293
+ border-color: color-mix(in srgb,var(--drawer-provider,#79d9f8) 50%,#304050);
2294
+ background: color-mix(in srgb,var(--drawer-provider,#79d9f8) 8%,#111924);
2295
+ color: #f3f7fa;
2296
+ }
2297
+
2298
+ .drawer-flow-button:active {
2299
+ transform: translateY(1px);
2300
+ }
2301
+
2302
+ .drawer-flow-button > span {
2303
+ color: var(--drawer-provider,#79d9f8);
2304
+ font-style: normal;
2305
+ }
2306
+
2307
+ .drawer-flow-button b {
2308
+ font-weight: 720;
2309
+ }
2310
+
2311
+ .drawer-resize-handle {
2312
+ position: absolute;
2313
+ z-index: 4;
2314
+ inset: 0 auto 0 -7px;
2315
+ width: 14px;
2316
+ cursor: col-resize;
2317
+ touch-action: none;
2318
+ }
2319
+
2320
+ .drawer-resize-handle::before {
2321
+ position: absolute;
2322
+ top: 50%;
2323
+ left: 5px;
2324
+ width: 3px;
2325
+ height: 58px;
2326
+ border-radius: 99px;
2327
+ background: #31485b;
2328
+ box-shadow: 0 0 0 1px rgba(8,14,21,.8);
2329
+ content: "";
2330
+ opacity: .62;
2331
+ transform: translateY(-50%);
2332
+ transition: height var(--motion-fast) ease,background-color var(--motion-fast) ease,opacity var(--motion-fast) ease;
2333
+ }
2334
+
2335
+ .drawer-resize-handle:hover::before,
2336
+ .drawer-resize-handle:focus-visible::before,
2337
+ body.conversation-panel-resizing .drawer-resize-handle::before {
2338
+ height: 88px;
2339
+ background: var(--drawer-provider,#79d9f8);
2340
+ opacity: 1;
2341
+ }
2342
+
2343
+ body.conversation-panel-resizing {
2344
+ cursor: col-resize;
2345
+ user-select: none;
2346
+ }
2347
+
2348
+ .conversation-composer-shell {
2349
+ display: grid;
2350
+ gap: 8px;
2351
+ }
2352
+
2353
+ .agent-command-actions .conversation-terminal-toggle {
2354
+ min-width: 0;
2355
+ border-color: #2c3c49;
2356
+ background: transparent;
2357
+ color: #82939f;
2358
+ }
2359
+
2360
+ .agent-command-actions .conversation-terminal-toggle > span {
2361
+ margin-right: 6px;
2362
+ color: #65b9cc;
2363
+ font-family: "Cascadia Code",Consolas,monospace;
2364
+ font-size: 11px;
2365
+ font-weight: 800;
2366
+ }
2367
+
2368
+ .conversation-terminal-expanded > span {
2369
+ min-width: 0;
2370
+ }
2371
+
2372
+ .conversation-terminal-expanded > span b,
2373
+ .conversation-terminal-expanded > span small {
2374
+ display: block;
2375
+ overflow: hidden;
2376
+ text-overflow: ellipsis;
2377
+ white-space: nowrap;
2378
+ }
2379
+
2380
+ .conversation-terminal-expanded > span b {
2381
+ color: #c8d2db;
2382
+ font-size: 12px;
2383
+ }
2384
+
2385
+ .conversation-terminal-expanded > span small {
2386
+ margin-top: 2px;
2387
+ color: #7f8f9d!important;
2388
+ }
2389
+
2390
+ .conversation-terminal-expanded > button,
2391
+ .conversation-terminal-expanded > [data-agent-terminal-open],
2392
+ .conversation-terminal-expanded > [data-resume-agent] {
2393
+ min-height: 32px;
2394
+ padding: 0 10px;
2395
+ border: 1px solid #34495a;
2396
+ border-radius: 8px;
2397
+ background: #121d28;
2398
+ color: #adbecb;
2399
+ font: inherit;
2400
+ font-size: 12px;
2401
+ font-weight: 680;
2402
+ cursor: pointer;
2403
+ }
2404
+
2405
+ .conversation-terminal-expanded > button:hover:not(:disabled) {
2406
+ border-color: #4d7188;
2407
+ color: #eef6fa;
2408
+ }
2409
+
2410
+ .conversation-terminal-expanded button:disabled {
2411
+ opacity: .4;
2412
+ cursor: not-allowed;
2413
+ }
2414
+
2415
+ .conversation-terminal-expanded {
2416
+ min-width: 0;
2417
+ display: grid;
2418
+ grid-template-columns: auto minmax(0,1fr) auto;
2419
+ align-items: center;
2420
+ gap: 10px;
2421
+ padding: 6px;
2422
+ border: 1px solid rgba(91,169,255,.22);
2423
+ border-radius: 10px;
2424
+ background: rgba(91,169,255,.045);
2425
+ }
2426
+
2427
+ .conversation-terminal-expanded > button:first-child {
2428
+ border-color: transparent;
2429
+ background: transparent;
2430
+ }
2431
+
2432
+ .conversation-terminal-expanded > button:first-child > span {
2433
+ margin-right: 6px;
2434
+ color: #79d9f8;
2435
+ }
2436
+
2437
+ @media (min-width:1280px) {
2438
+ body.conversation-context-open #appShell {
2439
+ width: calc(100vw - var(--conversation-panel-width));
2440
+ max-width: calc(100vw - var(--conversation-panel-width));
2441
+ transition: width var(--motion-medium) var(--motion-ease),max-width var(--motion-medium) var(--motion-ease);
2442
+ }
2443
+
2444
+ body.conversation-context-open {
2445
+ --sidebar: clamp(220px,16vw,272px);
2446
+ }
2447
+
2448
+ body.conversation-context-open .main-stage {
2449
+ padding-inline: clamp(18px,1.6vw,28px);
2450
+ }
2451
+
2452
+ body.conversation-context-open[data-current-view="all"] .topbar {
2453
+ display: block;
2454
+ }
2455
+
2456
+ body.conversation-context-open[data-current-view="all"] .topbar .eyebrow,
2457
+ body.conversation-context-open[data-current-view="all"] .topbar .subtitle {
2458
+ display: none;
2459
+ }
2460
+
2461
+ body.conversation-context-open[data-current-view="all"] .topbar h1 {
2462
+ max-width: 640px;
2463
+ font-size: clamp(25px,2.2vw,32px);
2464
+ line-height: 1.18;
2465
+ }
2466
+
2467
+ body.conversation-context-open[data-current-view="all"] .top-actions {
2468
+ width: 100%;
2469
+ flex-wrap: nowrap;
2470
+ justify-content: flex-start;
2471
+ gap: 8px;
2472
+ margin-top: 14px;
2473
+ padding-top: 0;
2474
+ }
2475
+
2476
+ body.conversation-context-open[data-current-view="all"] #guideBtn {
2477
+ width: 44px;
2478
+ min-width: 44px;
2479
+ padding: 0;
2480
+ }
2481
+
2482
+ body.conversation-context-open[data-current-view="all"] #guideBtn > span:nth-child(2),
2483
+ body.conversation-context-open[data-current-view="all"] .new-run-cta-copy {
2484
+ display: none;
2485
+ }
2486
+
2487
+ body.conversation-context-open[data-current-view="all"] .sync-state {
2488
+ max-width: 168px;
2489
+ }
2490
+
2491
+ body.conversation-context-open[data-current-view="all"] .new-run-cta {
2492
+ width: 48px;
2493
+ min-width: 48px;
2494
+ padding-inline: 0;
2495
+ }
2496
+
2497
+ body.conversation-context-open .detail-drawer[data-presentation="context"] {
2498
+ width: var(--conversation-panel-width);
2499
+ border-left-color: #304253;
2500
+ box-shadow: -12px 0 38px rgba(0,0,0,.28);
2501
+ }
2502
+
2503
+ body.conversation-context-open .detail-drawer .drawer-head {
2504
+ min-height: 58px;
2505
+ display: grid;
2506
+ grid-template-columns: auto minmax(0,1fr);
2507
+ padding: 11px 18px 8px;
2508
+ gap: 12px;
2509
+ }
2510
+
2511
+ body.conversation-context-open .detail-drawer .provider-mark,
2512
+ body.conversation-context-open .detail-drawer .close-button {
2513
+ display: none;
2514
+ }
2515
+
2516
+ body.conversation-context-open .detail-drawer .drawer-title h2 {
2517
+ margin-top: 0;
2518
+ font-size: 16px;
2519
+ }
2520
+
2521
+ body.conversation-context-open .detail-drawer .drawer-title > span {
2522
+ display: none;
2523
+ }
2524
+
2525
+ body.conversation-context-open .detail-drawer .drawer-meta {
2526
+ padding: 0 18px 10px;
2527
+ }
2528
+
2529
+ body.conversation-context-open .detail-drawer .drawer-tabs {
2530
+ padding-inline: 18px;
2531
+ }
2532
+
2533
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .drawer-tabs,
2534
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-work-source {
2535
+ display: none;
2536
+ }
2537
+
2538
+ body.conversation-context-open .detail-drawer .drawer-content {
2539
+ padding: 18px 20px 24px;
2540
+ background: #0d141d;
2541
+ }
2542
+
2543
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .drawer-content {
2544
+ padding-top: 14px;
2545
+ }
2546
+
2547
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-assignment-card {
2548
+ grid-template-columns: minmax(0,1fr);
2549
+ gap: 0;
2550
+ margin-bottom: 14px;
2551
+ padding: 0 0 14px;
2552
+ border: 0;
2553
+ border-bottom: 1px solid #22313e;
2554
+ border-radius: 0;
2555
+ background: transparent;
2556
+ }
2557
+
2558
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-assignment-card > span,
2559
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-assignment-card small {
2560
+ display: none;
2561
+ }
2562
+
2563
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-assignment-card b {
2564
+ color: #77c8d6;
2565
+ font-size: 11px;
2566
+ }
2567
+
2568
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-assignment-card p {
2569
+ margin-top: 5px;
2570
+ color: #b8c6d1;
2571
+ font-size: 12px;
2572
+ line-height: 1.5;
2573
+ }
2574
+
2575
+ body.conversation-context-open .detail-drawer[data-mode="subagent"] .subagent-work-source + .chat-history-head {
2576
+ top: -14px;
2577
+ margin: -14px 0 14px;
2578
+ padding-top: 10px;
2579
+ }
2580
+
2581
+ body.conversation-context-open .detail-drawer .chat-list {
2582
+ gap: 22px;
2583
+ }
2584
+
2585
+ body.conversation-context-open .detail-drawer .chat-turn {
2586
+ gap: 16px;
2587
+ padding-bottom: 22px;
2588
+ }
2589
+
2590
+ body.conversation-context-open .detail-drawer .chat-content.markdown {
2591
+ padding: 2px 0;
2592
+ border: 0;
2593
+ border-radius: 0;
2594
+ background: transparent;
2595
+ color: #d1dae2;
2596
+ font-size: 14px;
2597
+ line-height: 1.72;
2598
+ }
2599
+
2600
+ body.conversation-context-open .detail-drawer .chat-row.user .chat-content.markdown {
2601
+ padding: 10px 12px;
2602
+ border-left: 2px solid color-mix(in srgb,var(--drawer-provider,#79d9f8) 65%,#526271);
2603
+ border-radius: 0 8px 8px 0;
2604
+ background: rgba(255,255,255,.025);
2605
+ }
2606
+
2607
+ body.conversation-context-open .detail-drawer .chat-row {
2608
+ grid-template-columns: 31px minmax(0,1fr);
2609
+ gap: 12px;
2610
+ }
2611
+
2612
+ body.conversation-context-open .detail-drawer .chat-avatar {
2613
+ width: 29px;
2614
+ height: 29px;
2615
+ }
2616
+
2617
+ body.conversation-context-open .detail-drawer .chat-history-head {
2618
+ top: -18px;
2619
+ background: linear-gradient(180deg,#0d141d 78%,rgba(13,20,29,.94));
2620
+ }
2621
+
2622
+ body.conversation-context-open .drawer-composer {
2623
+ padding: 8px 14px 12px;
2624
+ border-top-color: #2b3a48;
2625
+ background: #0a1119;
2626
+ box-shadow: 0 -10px 28px rgba(0,0,0,.2);
2627
+ }
2628
+
2629
+ body.conversation-context-open .conversation-composer-shell {
2630
+ gap: 6px;
2631
+ }
2632
+
2633
+ body.conversation-context-open .drawer-composer .agent-command-panel {
2634
+ gap: 7px;
2635
+ padding: 0;
2636
+ border: 0;
2637
+ border-radius: 0;
2638
+ background: transparent;
2639
+ box-shadow: none;
2640
+ }
2641
+
2642
+ body.conversation-context-open .drawer-composer .conversation-composer > header {
2643
+ display: none;
2644
+ }
2645
+
2646
+ body.conversation-context-open .drawer-composer textarea {
2647
+ min-height: 52px;
2648
+ font-size: 13px;
2649
+ }
2650
+
2651
+ body.conversation-context-open .conversation-composer-shell.mode-terminal .agent-command-panel {
2652
+ padding-top: 2px;
2653
+ }
2654
+
2655
+ body.conversation-panel-resizing #appShell,
2656
+ body.conversation-panel-resizing .detail-drawer {
2657
+ transition: none!important;
2658
+ }
2659
+ }
@@ -151,7 +151,8 @@
151
151
  animation: motion-backdrop-in var(--motion-medium) ease both;
152
152
  }
153
153
 
154
- .drawer-backdrop.closing {
154
+ .drawer-backdrop.closing,
155
+ .drawer-backdrop.hidden {
155
156
  animation: motion-backdrop-out 260ms ease both;
156
157
  pointer-events: none;
157
158
  }
@@ -1472,6 +1472,8 @@ body[data-current-view="terminal"] .topbar h1 {
1472
1472
  }
1473
1473
 
1474
1474
  .terminal-session-item[data-status="running"] { border-left-color: #51dbae; }
1475
+ .terminal-session-item[data-status="detached"] { border-left-color: #7ba9d8; }
1476
+ .terminal-session-item[data-status="stopped"] { border-left-color: #667783; opacity: .88; }
1475
1477
  .terminal-session-item[data-status="attention"] { border-color: #60482f; border-left-color: #f0ad62; background: linear-gradient(100deg,rgba(133,83,31,.18),#0d171e 58%); }
1476
1478
  .terminal-session-item[data-status="failed"] { border-color: #58323c; border-left-color: #ef7d8e; }
1477
1479
  .terminal-session-item[data-status="completed"] { border-left-color: #6cbdd1; }
@@ -1500,6 +1502,10 @@ body[data-current-view="terminal"] .topbar h1 {
1500
1502
  .terminal-session-status i { width: 6px; height: 6px; border-radius: 50%; background: #657581; }
1501
1503
  .terminal-session-status[data-status="running"] { border-color: #2e6958; color: #86e8c5; background: #102921; }
1502
1504
  .terminal-session-status[data-status="running"] i { background: #55e2ad; box-shadow: 0 0 8px rgba(85,226,173,.65); }
1505
+ .terminal-session-status[data-status="detached"] { border-color: #365875; color: #a8d1f2; background: #112333; }
1506
+ .terminal-session-status[data-status="detached"] i { background: #78afe0; }
1507
+ .terminal-session-status[data-status="stopped"] { border-color: #3b4a54; color: #a8b4bc; background: #141d23; }
1508
+ .terminal-session-status[data-status="stopped"] i { background: #71818c; }
1503
1509
  .terminal-session-status[data-status="attention"] { border-color: #725432; color: #f3bb77; background: #2a2014; }
1504
1510
  .terminal-session-status[data-status="attention"] i { background: #ffbd66; box-shadow: 0 0 8px rgba(255,189,102,.5); }
1505
1511
  .terminal-session-status[data-status="failed"] { border-color: #6f3b47; color: #f3a0ad; background: #29161d; }
@@ -153,12 +153,15 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
153
153
  const created = await window.loadtoagent.terminalCreate({
154
154
  type: 'agent',
155
155
  provider: support.provider,
156
- args: resumeLaunchArgs(support, sendDraft ? prompt : '', { background: options.focus === false }),
156
+ args: resumeLaunchArgs(support, sendDraft ? prompt : ''),
157
157
  cwd,
158
158
  distro,
159
159
  bridgeId: agentSession.id,
160
160
  title,
161
- transient: options.focus === false,
161
+ // Conversation sends must keep the resumed PTY alive. A transient
162
+ // one-shot process can exit after spawn (for example when the session is
163
+ // busy) while the composer incorrectly reports that the prompt was sent.
164
+ transient: false,
162
165
  cols: 120,
163
166
  rows: 32,
164
167
  });
@@ -262,7 +262,14 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
262
262
  const session = currentSession();
263
263
  if (!session) return;
264
264
  await runBusy(event.currentTarget, async () => {
265
- const restarted = await guarded(() => window.loadtoagent.terminalRestart(session.id), t('terminal.session.restarted'), `terminal-restart:${session.id}`);
265
+ const managedSession = session.backend === 'managed-tmux';
266
+ const restarted = await guarded(
267
+ () => managedSession
268
+ ? window.loadtoagent.terminalReconnect(session.id)
269
+ : window.loadtoagent.terminalRestart(session.id),
270
+ managedSession ? t('terminal.session.reconnected') : t('terminal.session.restarted'),
271
+ `${managedSession ? 'terminal-reconnect' : 'terminal-restart'}:${session.id}`,
272
+ );
266
273
  if (restarted) {
267
274
  const entry = state.terminals.get(session.id);
268
275
  if (entry) entry.terminal.reset();
@@ -273,11 +280,21 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
273
280
  });
274
281
  const endTerminalSession = async (button, session) => {
275
282
  if (!session) return;
283
+ const managedSession = session.backend === 'managed-tmux';
284
+ const stopManagedSession = managedSession && session.status !== 'stopped';
276
285
  const confirmation = isAiTerminalSession(session) ? 'terminal.session.confirm_end_ai' : 'terminal.session.confirm_end';
277
- if (session.type !== 'tmux' && session.status === 'running' && !window.confirm(t(confirmation, { title: session.title }))) return;
286
+ if (session.type !== 'tmux' && ['running', 'detached'].includes(session.status) && !window.confirm(t(confirmation, { title: session.title }))) return;
278
287
  await runBusy(button, async () => {
279
- const message = session.type === 'tmux' ? t('terminal.tmux.detached_input') : t('terminal.session.ended');
280
- const closed = await guarded(() => window.loadtoagent.terminalClose(session.id), message, `terminal-close:${session.id}`);
288
+ const message = stopManagedSession
289
+ ? t('terminal.session.stopped')
290
+ : session.type === 'tmux' ? t('terminal.tmux.detached_input') : t('terminal.session.ended');
291
+ const closed = await guarded(
292
+ () => stopManagedSession
293
+ ? window.loadtoagent.terminalStop(session.id)
294
+ : window.loadtoagent.terminalClose(session.id),
295
+ message,
296
+ `${stopManagedSession ? 'terminal-stop' : 'terminal-close'}:${session.id}`,
297
+ );
281
298
  if (!closed) return;
282
299
  const entry = state.terminals.get(session.id);
283
300
  if (entry) {
@@ -304,6 +321,15 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
304
321
  return;
305
322
  }
306
323
  if (isAiTerminalSession(session)) {
324
+ if (session.backend === 'managed-tmux' && session.status === 'running') {
325
+ const detached = await runBusy(event.currentTarget, () => guarded(
326
+ () => window.loadtoagent.terminalDetach(session.id),
327
+ t('terminal.tmux.detached_input'),
328
+ `terminal-detach:${session.id}`,
329
+ ));
330
+ if (!detached) return;
331
+ await refreshSessions();
332
+ }
307
333
  state.captureGeneration += 1;
308
334
  state.selectedId = null;
309
335
  state.boundAgent = null;
@@ -212,6 +212,8 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
212
212
  const remote = currentTmux();
213
213
  const bound = visibleBoundAgent();
214
214
  const aiTerminal = isAiTerminalSession(session);
215
+ const managedSession = Boolean(session && session.backend === 'managed-tmux');
216
+ const reconnectable = managedSession && session.status === 'detached';
215
217
  const hasTarget = Boolean(session || remote);
216
218
  const canInput = Boolean((remote && !remote.pane.dead) || (session && session.status === 'running'));
217
219
  const closeButton = $('#terminalCloseBtn');
@@ -225,8 +227,14 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
225
227
  const endSessionButton = $('#terminalEndSessionBtn');
226
228
  endSessionButton.classList.toggle('hidden', !aiTerminal);
227
229
  endSessionButton.disabled = !aiTerminal;
228
- $('#terminalRestartBtn').classList.toggle('hidden', !session || session.status === 'running' || session.type === 'agent');
229
- $('#terminalRestartBtn').disabled = !session || session.status === 'running';
230
+ endSessionButton.textContent = managedSession && session.status === 'stopped'
231
+ ? t('terminal.remove_session_record')
232
+ : t('terminal.end_ai_session');
233
+ const restartButton = $('#terminalRestartBtn');
234
+ const showRestart = Boolean(session && (reconnectable || (session.type !== 'agent' && session.status !== 'running')));
235
+ restartButton.classList.toggle('hidden', !showRestart);
236
+ restartButton.disabled = !showRestart;
237
+ restartButton.textContent = reconnectable ? t('terminal.reconnect') : t('ui.restart');
230
238
  $('#terminalCommandInput').disabled = !canInput;
231
239
  const commandForm = $('#terminalCommandForm');
232
240
  const commandButton = commandForm.querySelector('button[type="submit"]');
@@ -244,7 +252,11 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
244
252
  $('#terminalTargetIcon').textContent = terminalTypeMark(session);
245
253
  $('#terminalTargetMeta').innerHTML = `<b>${esc(session.title)}</b><span>${bound ? `● ${t('terminal.bound_ai_session')} · ` : ''}${session.recoveredAfterHostRestart ? `${t('terminal.recovered_after_host_restart')} · ` : ''}${esc(session.type.toUpperCase())} · PID ${session.pid || '--'} · ${esc(session.cwd || session.distro || '')}</span>`;
246
254
  $('#terminalConsoleCaption').textContent = `${terminalTypeLabel(session)} · PID ${session.pid || '--'}`;
247
- $('#terminalConsoleState').textContent = presentation.tone === 'attention' || presentation.tone === 'completed'
255
+ $('#terminalConsoleState').textContent = session.status === 'detached'
256
+ ? t('terminal.detached_work_continues')
257
+ : session.status === 'stopped'
258
+ ? t('terminal.stopped_record_kept')
259
+ : presentation.tone === 'attention' || presentation.tone === 'completed'
248
260
  ? presentation.label
249
261
  : canInput ? window.LoadToAgentI18n.t("ui.direct_input_available") : window.LoadToAgentI18n.t("ui.ended_session");
250
262
  $('#terminalConsoleState').dataset.status = presentation.tone;
@@ -83,6 +83,8 @@
83
83
  Object.assign(STATUS_LABELS, {
84
84
  starting: t('ui.preparing'),
85
85
  running: t('terminal.status.running'),
86
+ detached: t('terminal.status.detached'),
87
+ stopped: t('terminal.status.stopped'),
86
88
  exited: t('terminal.status.exited'),
87
89
  failed: t('terminal.status.failed'),
88
90
  });
@@ -179,11 +181,9 @@
179
181
  return { supported: true, provider, sessionId, args };
180
182
  }
181
183
 
182
- function resumeLaunchArgs(support, prompt = '', options = {}) {
184
+ function resumeLaunchArgs(support, prompt = '') {
183
185
  const args = [...support.args];
184
186
  const text = String(prompt || '').trim();
185
- if (options.background && text && support.provider === 'codex') return ['exec', 'resume', support.sessionId, text];
186
- if (options.background && text && support.provider === 'claude') return ['--resume', support.sessionId, '--print', text];
187
187
  if (text) args.push(text);
188
188
  return args;
189
189
  }
@@ -698,13 +698,26 @@
698
698
  state.active = false;
699
699
  return;
700
700
  }
701
+ // Snapshot renders call activate repeatedly while this view is already
702
+ // open. Refreshing and re-showing the same xterm screen would temporarily
703
+ // hide its helper textarea and steal direct keyboard focus.
704
+ if (!enteringMode) return;
701
705
  await refreshSessions();
702
706
  if (!state.active || state.mode !== nextMode) return;
707
+ let selectionChanged = false;
703
708
  if (enteringMode && !state.selectedId && !state.selectedTmux) {
704
709
  const visible = modeSessions();
705
- if (visible.length) state.selectedId = visible[0].id;
706
- else if (state.mode === 'tmux') state.selectedTmux = tmuxRows()[0] || null;
710
+ if (visible.length) {
711
+ state.selectedId = visible[0].id;
712
+ selectionChanged = true;
713
+ } else if (state.mode === 'tmux') {
714
+ state.selectedTmux = tmuxRows()[0] || null;
715
+ selectionChanged = Boolean(state.selectedTmux);
716
+ }
707
717
  }
718
+ // refreshSessions already rendered and showed the existing selection. Avoid
719
+ // hiding the same xterm host a second time after users can start typing.
720
+ if (!selectionChanged) return;
708
721
  renderAll();
709
722
  await showSelection();
710
723
  }
@@ -209,7 +209,7 @@ class BridgeServer {
209
209
  for (const client of this.clients.values()) {
210
210
  if (client.terminalId !== session.id) continue;
211
211
  sendFrame(client.socket, { type: 'state', status: session.status, exitCode: session.exitCode, signal: session.signal });
212
- if (session.status === 'exited' || session.status === 'failed') client.socket.end();
212
+ if (['detached', 'stopped', 'exited', 'failed'].includes(session.status)) client.socket.end();
213
213
  }
214
214
  }
215
215
 
@@ -31,7 +31,7 @@ function registerTerminalIpc({ ipcMain, requireTrustedSender, trustedSender, man
31
31
  requireTrustedSender(event);
32
32
  return requireManager(manager).resize(id, cols, rows);
33
33
  });
34
- for (const operation of ['signal', 'restart', 'close']) {
34
+ for (const operation of ['signal', 'restart', 'reconnect', 'detach', 'stop', 'close']) {
35
35
  ipcMain.handle(`terminals:${operation}`, (event, ...args) => {
36
36
  requireTrustedSender(event);
37
37
  return requireManager(manager)[operation](...args);
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+
5
+ class ManagedTmuxRuntime {
6
+ constructor(options = {}) {
7
+ this.platform = options.platform || process.platform;
8
+ this.execFileSync = options.execFileSync || execFileSync;
9
+ }
10
+
11
+ command(options, args) {
12
+ if (this.platform === 'win32') {
13
+ return {
14
+ file: 'wsl.exe',
15
+ args: ['-d', options.distro, '--', 'tmux', '-L', options.tmuxSocket, ...args],
16
+ };
17
+ }
18
+ return {
19
+ file: 'tmux',
20
+ args: ['-L', options.tmuxSocket, ...args],
21
+ };
22
+ }
23
+
24
+ execute(options, args) {
25
+ const command = this.command(options, args);
26
+ return this.execFileSync(command.file, command.args, {
27
+ encoding: 'utf8',
28
+ timeout: this.platform === 'win32' ? 15_000 : 5_000,
29
+ windowsHide: true,
30
+ stdio: ['ignore', 'pipe', 'pipe'],
31
+ });
32
+ }
33
+
34
+ exists(options) {
35
+ try {
36
+ this.execute(options, ['has-session', '-t', `=${options.managedTmuxSession}`]);
37
+ return true;
38
+ } catch (_missingSession) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ stop(options) {
44
+ try {
45
+ this.execute(options, ['kill-session', '-t', `=${options.managedTmuxSession}`]);
46
+ } catch (error) {
47
+ if (error?.status !== 1) throw error;
48
+ }
49
+ return { ok: true };
50
+ }
51
+ }
52
+
53
+ module.exports = { ManagedTmuxRuntime };