loadtoagent 1.3.10 → 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
+ }
@@ -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; }
@@ -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
  });
@@ -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 };
@@ -10,12 +10,15 @@ const { spawn } = require('child_process');
10
10
  const { endpointFor, safeWriteJson } = require('./bridgeServer');
11
11
  const { runBestEffort } = require('./diagnostics');
12
12
 
13
- const TERMINAL_HOST_PROTOCOL = 3;
13
+ const TERMINAL_HOST_PROTOCOL = 4;
14
14
  const TERMINAL_HOST_RUNTIME = `node-pty-${require('node-pty/package.json').version}`;
15
15
  const MAX_FRAME_CHARS = 4 * 1024 * 1024;
16
16
  const AUTH_TIMEOUT_MS = 5_000;
17
17
  const REQUEST_TIMEOUT_MS = 15_000;
18
- const HOST_OPERATIONS = new Set(['list', 'get', 'create', 'write', 'command', 'resize', 'signal', 'restart', 'close']);
18
+ const HOST_OPERATIONS = new Set([
19
+ 'list', 'get', 'create', 'write', 'command', 'resize', 'signal',
20
+ 'restart', 'reconnect', 'detach', 'stop', 'close',
21
+ ]);
19
22
 
20
23
  function sendFrame(socket, payload) {
21
24
  if (!socket || socket.destroyed) return;
@@ -524,6 +527,9 @@ class TerminalHostClient extends EventEmitter {
524
527
  resize(id, cols, rows) { return this.request('resize', id, cols, rows); }
525
528
  signal(id, signal) { return this.request('signal', id, signal); }
526
529
  restart(id) { return this.request('restart', id); }
530
+ reconnect(id) { return this.request('reconnect', id); }
531
+ detach(id) { return this.request('detach', id); }
532
+ stop(id) { return this.request('stop', id); }
527
533
  close(id) { return this.request('close', id); }
528
534
 
529
535
  dispose({ shutdownIfIdle = false } = {}) {