loadtoagent 1.0.0

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 (82) hide show
  1. package/README.ko.md +175 -0
  2. package/README.md +175 -0
  3. package/README.zh-CN.md +175 -0
  4. package/bin/loadtoagent.js +171 -0
  5. package/docs/ARCHITECTURE.md +30 -0
  6. package/docs/PROVIDER-CONTRACTS.md +58 -0
  7. package/docs/assets/loadtoagent-dashboard.png +0 -0
  8. package/docs/assets/loadtoagent-demo.gif +0 -0
  9. package/main.js +493 -0
  10. package/package.json +133 -0
  11. package/preload.js +75 -0
  12. package/renderer/app-agent-actions.js +285 -0
  13. package/renderer/app-bootstrap.js +95 -0
  14. package/renderer/app-dashboard.js +361 -0
  15. package/renderer/app-drawer-content.js +203 -0
  16. package/renderer/app-drawer-data.js +41 -0
  17. package/renderer/app-drawer.js +183 -0
  18. package/renderer/app-events-dialogs.js +169 -0
  19. package/renderer/app-events-filters.js +74 -0
  20. package/renderer/app-events-navigation.js +96 -0
  21. package/renderer/app-events-sessions.js +195 -0
  22. package/renderer/app-events.js +16 -0
  23. package/renderer/app-graph-layout.js +71 -0
  24. package/renderer/app-graph-model.js +107 -0
  25. package/renderer/app-graph-orchestration.js +76 -0
  26. package/renderer/app-graph-view.js +544 -0
  27. package/renderer/app-run-modal.js +221 -0
  28. package/renderer/app-session-render.js +243 -0
  29. package/renderer/app-tmux-render.js +247 -0
  30. package/renderer/app.js +608 -0
  31. package/renderer/fonts/geist-ext.woff2 +0 -0
  32. package/renderer/fonts/geist.woff2 +0 -0
  33. package/renderer/i18n-messages.js +355 -0
  34. package/renderer/i18n.js +122 -0
  35. package/renderer/index.html +386 -0
  36. package/renderer/shared.js +26 -0
  37. package/renderer/styles-agent-map.css +401 -0
  38. package/renderer/styles-cards.css +600 -0
  39. package/renderer/styles-collaboration.css +534 -0
  40. package/renderer/styles-components.css +1293 -0
  41. package/renderer/styles-onboarding.css +344 -0
  42. package/renderer/styles-overlays.css +284 -0
  43. package/renderer/styles-product.css +686 -0
  44. package/renderer/styles-responsive-product.css +689 -0
  45. package/renderer/styles-responsive-runtime.css +718 -0
  46. package/renderer/styles-responsive-shell.css +533 -0
  47. package/renderer/styles-responsive-workflows.css +778 -0
  48. package/renderer/styles-run-composer.css +695 -0
  49. package/renderer/styles-settings.css +606 -0
  50. package/renderer/styles-terminal.css +1241 -0
  51. package/renderer/styles-tmux.css +1162 -0
  52. package/renderer/styles-workflow-map.css +513 -0
  53. package/renderer/styles-workflows.css +1217 -0
  54. package/renderer/styles.css +486 -0
  55. package/renderer/terminal-agent.js +152 -0
  56. package/renderer/terminal-events.js +226 -0
  57. package/renderer/terminal-workbench.js +464 -0
  58. package/renderer/terminal.js +497 -0
  59. package/src/agentMonitor/claudeParser.js +197 -0
  60. package/src/agentMonitor/codexCollaboration.js +95 -0
  61. package/src/agentMonitor/codexParser.js +447 -0
  62. package/src/agentMonitor/genericParser.js +244 -0
  63. package/src/agentMonitor/hierarchy.js +248 -0
  64. package/src/agentMonitor/sessionFiles.js +86 -0
  65. package/src/agentMonitor.js +591 -0
  66. package/src/agentRunner.js +465 -0
  67. package/src/bridgeServer.js +246 -0
  68. package/src/contracts.js +71 -0
  69. package/src/diagnostics.js +23 -0
  70. package/src/ipc/registerAgentIpc.js +12 -0
  71. package/src/ipc/registerAppIpc.js +20 -0
  72. package/src/ipc/registerTerminalIpc.js +50 -0
  73. package/src/ipc/registerTmuxIpc.js +30 -0
  74. package/src/ipc/registerWorkspaceIpc.js +14 -0
  75. package/src/monitorWorker.js +273 -0
  76. package/src/processMonitor.js +394 -0
  77. package/src/providerRegistry.js +120 -0
  78. package/src/terminalManager.js +438 -0
  79. package/src/tmuxController.js +183 -0
  80. package/src/tmuxMonitor.js +432 -0
  81. package/src/updateManager.js +339 -0
  82. package/src/workspaceStore.js +77 -0
@@ -0,0 +1,486 @@
1
+ /*
2
+ * Foundation
3
+ * -----------
4
+ * Design tokens, document defaults, application shell, and primary navigation.
5
+ */
6
+
7
+ * {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ .hidden {
12
+ display: none!important;
13
+ }
14
+
15
+ .sidebar>.brand {
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ .sidebar>.view-nav {
20
+ flex-shrink: 0;
21
+ }
22
+
23
+ .sidebar>.sidebar-section {
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ .sidebar>.sidebar-footer {
28
+ flex-shrink: 0;
29
+ }
30
+
31
+ .sidebar>.workspace-section {
32
+ flex: none;
33
+ overflow: visible;
34
+ }
35
+
36
+ .sidebar>.workspace-section .workspace-list {
37
+ max-height: 180px;
38
+ }
39
+
40
+ .brand {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 12px;
44
+ padding: 0 8px 24px;
45
+ }
46
+
47
+ .brand-orbit {
48
+ position: relative;
49
+ width: 34px;
50
+ height: 34px;
51
+ border: 1px solid #365169;
52
+ border-radius: 50%;
53
+ box-shadow: inset 0 0 14px rgba(77,190,232,.09);
54
+ }
55
+
56
+ .brand-orbit:before {
57
+ content: "";
58
+ position: absolute;
59
+ inset: 6px;
60
+ border: 1px solid #263849;
61
+ border-radius: 50%;
62
+ }
63
+
64
+ .brand-orbit span {
65
+ position: absolute;
66
+ width: 7px;
67
+ height: 7px;
68
+ border-radius: 50%;
69
+ background: var(--cyan);
70
+ box-shadow: 0 0 14px var(--cyan);
71
+ top: 4px;
72
+ left: 4px;
73
+ }
74
+
75
+ .view-nav {
76
+ display: grid;
77
+ gap: 4px;
78
+ }
79
+
80
+ .nav-item:hover {
81
+ background: #121824;
82
+ color: #dbe5f2;
83
+ }
84
+
85
+ .nav-item.active {
86
+ background: linear-gradient(90deg,rgba(77,183,226,.15),rgba(77,183,226,.05));
87
+ color: #eefaff;
88
+ box-shadow: inset 2px 0 var(--cyan);
89
+ }
90
+
91
+ .nav-item.active b {
92
+ background: #173b4a;
93
+ color: #85e5ff;
94
+ }
95
+
96
+ .nav-icon {
97
+ font-size: 14px;
98
+ text-align: center;
99
+ }
100
+
101
+ .sidebar-section {
102
+ margin-top: 24px;
103
+ min-height: 0;
104
+ }
105
+
106
+ .sidebar-footer {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 9px;
110
+ padding: 15px 8px 0;
111
+ border-top: 1px solid var(--line-soft);
112
+ }
113
+
114
+ .topbar {
115
+ display: flex;
116
+ justify-content: space-between;
117
+ gap: 40px;
118
+ align-items: flex-start;
119
+ max-width: 1640px;
120
+ margin: auto;
121
+ }
122
+
123
+ .top-actions {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 14px;
127
+ padding-top: 5px;
128
+ }
129
+
130
+ .sidebar {
131
+ height: 100vh;
132
+ display: flex;
133
+ flex-direction: column;
134
+ background: linear-gradient(180deg,rgba(14,18,28,.98),rgba(9,12,19,.98));
135
+ border-right: 1px solid var(--line);
136
+ overflow-x: hidden;
137
+ overflow-y: auto;
138
+ padding: 24px 17px 18px;
139
+ }
140
+
141
+ .brand strong {
142
+ display: block;
143
+ letter-spacing: -.02em;
144
+ font-size: 20px;
145
+ }
146
+
147
+ .brand small {
148
+ display: block;
149
+ margin-top: 3px;
150
+ color: #657187;
151
+ font-weight: 700;
152
+ letter-spacing: .18em;
153
+ font-size: 9px;
154
+ }
155
+
156
+ .nav-group-label {
157
+ margin: 18px 10px 6px;
158
+ color: #718096;
159
+ font-size: 11px;
160
+ font-weight: 700;
161
+ letter-spacing: .08em;
162
+ }
163
+
164
+ html[data-motion="full"] {
165
+ scroll-behavior: smooth;
166
+ }
167
+
168
+ body {
169
+ margin: 0;
170
+ min-height: 100%;
171
+ background: var(--bg);
172
+ color: var(--text);
173
+ font-family: Geist,"Segoe UI",sans-serif;
174
+ overflow: hidden;
175
+ max-width: 100%;
176
+ }
177
+
178
+ .sidebar-app-version {
179
+ margin-left: auto;
180
+ padding: 4px 7px;
181
+ border: 1px solid #2b3b44;
182
+ border-radius: 7px;
183
+ background: #101923;
184
+ color: #7f929d;
185
+ font-family: "Geist Mono","Cascadia Code",Consolas,monospace;
186
+ font-size: 8px;
187
+ font-weight: 700;
188
+ letter-spacing: .02em;
189
+ white-space: nowrap;
190
+ }
191
+
192
+ html {
193
+ margin: 0;
194
+ min-height: 100%;
195
+ background: var(--bg);
196
+ color: var(--text);
197
+ font-family: Geist,"Segoe UI",sans-serif;
198
+ max-width: 100%;
199
+ -webkit-font-smoothing: antialiased;
200
+ -moz-osx-font-smoothing: grayscale;
201
+ }
202
+
203
+ :root {
204
+ color-scheme: dark;
205
+ --bg: #070910;
206
+ --panel: #0d111a;
207
+ --panel-2: #111722;
208
+ --panel-3: #151c28;
209
+ --line: #202838;
210
+ --line-soft: #171e2b;
211
+ --text: #f4f7fb;
212
+ --cyan: #59d8ff;
213
+ --green: #4ce39a;
214
+ --amber: #f6bd55;
215
+ --red: #ff6b7f;
216
+ --radius: 17px;
217
+ --sidebar: 272px;
218
+ --motion-fast: 160ms;
219
+ --motion-medium: 280ms;
220
+ --motion-slow: 440ms;
221
+ --motion-ease: cubic-bezier(.22,1,.36,1);
222
+ --motion-standard: cubic-bezier(.2,.8,.2,1);
223
+ --muted: #aab6c5;
224
+ --dim: #8794a6;
225
+ --focus: #8ae8ff;
226
+ }
227
+
228
+ .eyebrow {
229
+ margin: 0 0 8px;
230
+ font-weight: 760;
231
+ letter-spacing: .17em;
232
+ color: #8d9bad;
233
+ font-size: 11px;
234
+ }
235
+
236
+ .nav-item b {
237
+ min-width: 22px;
238
+ text-align: center;
239
+ padding: 2px 6px;
240
+ border-radius: 10px;
241
+ background: #1c2431;
242
+ color: #aeb9c7;
243
+ font-size: 11px;
244
+ }
245
+
246
+ .sidebar-footer strong {
247
+ display: block;
248
+ color: #aeb9c7;
249
+ font-size: 11px;
250
+ }
251
+
252
+ .sidebar-footer small {
253
+ display: block;
254
+ margin-top: 3px;
255
+ color: #8795a6;
256
+ font-size: 10px;
257
+ }
258
+
259
+ .app-shell {
260
+ height: 100vh;
261
+ display: grid;
262
+ grid-template-columns: var(--sidebar) minmax(0,1fr);
263
+ background: radial-gradient(circle at 76% -12%,rgba(58,115,160,.16),transparent 35%),var(--bg);
264
+ max-width: 100%;
265
+ min-width: 0;
266
+ }
267
+
268
+ .main-stage {
269
+ height: 100vh;
270
+ overflow: auto;
271
+ scrollbar-color: #283244 transparent;
272
+ padding: 28px clamp(24px,2.5vw,44px) 64px;
273
+ max-width: 100%;
274
+ min-width: 0;
275
+ }
276
+
277
+ .topbar>div {
278
+ min-width: 0;
279
+ }
280
+
281
+ .nav-item {
282
+ width: 100%;
283
+ padding: 0 10px;
284
+ border: 0;
285
+ border-radius: 10px;
286
+ background: transparent;
287
+ display: grid;
288
+ grid-template-columns: 22px 1fr auto;
289
+ align-items: center;
290
+ gap: 7px;
291
+ text-align: left;
292
+ cursor: pointer;
293
+ height: 46px;
294
+ min-height: 44px;
295
+ color: #aab5c4;
296
+ font-size: 13px;
297
+ transition: transform var(--motion-fast) var(--motion-ease),background-color var(--motion-fast) ease,border-color var(--motion-fast) ease,color var(--motion-fast) ease,box-shadow var(--motion-medium) ease,opacity var(--motion-fast) ease;
298
+ }
299
+
300
+ .nav-item.active .nav-icon {
301
+ animation: motion-select-pop var(--motion-medium) var(--motion-ease);
302
+ }
303
+
304
+ .subtitle {
305
+ max-width: 750px;
306
+ margin: 10px 0 0;
307
+ color: #a0adbd;
308
+ font-size: 14px;
309
+ line-height: 1.65;
310
+ word-break: keep-all;
311
+ overflow-wrap: break-word;
312
+ }
313
+
314
+ .topbar h1 {
315
+ margin: 0;
316
+ font-weight: 680;
317
+ letter-spacing: -.045em;
318
+ font-size: clamp(28px,2vw,36px);
319
+ word-break: keep-all;
320
+ overflow-wrap: break-word;
321
+ text-wrap: balance;
322
+ }
323
+
324
+ @font-face {
325
+ font-family: Geist;
326
+ src: url("fonts/geist.woff2") format("woff2");
327
+ font-weight: 100 900;
328
+ font-style: normal;
329
+ font-display: swap;
330
+ }
331
+
332
+ @font-face {
333
+ font-family: Geist;
334
+ src: url("fonts/geist-ext.woff2") format("woff2");
335
+ font-weight: 100 900;
336
+ font-style: normal;
337
+ font-display: swap;
338
+ unicode-range: U+1100-11FF,U+3130-318F,U+AC00-D7AF;
339
+ }
340
+
341
+ @keyframes wave {
342
+ 50% {
343
+ transform: scaleY(.45);
344
+ opacity: .32;
345
+ }
346
+ }
347
+
348
+ @keyframes meter-alert {
349
+ 50% {
350
+ filter: brightness(1.25);
351
+ }
352
+ }
353
+
354
+ @keyframes spin {
355
+ to {
356
+ transform: rotate(360deg);
357
+ }
358
+ }
359
+
360
+ @keyframes motion-select-pop {
361
+ 0% {
362
+ transform: scale(.86);
363
+ opacity: .65;
364
+ }
365
+
366
+ 70% {
367
+ transform: scale(1.08);
368
+ }
369
+
370
+ 100% {
371
+ transform: scale(1);
372
+ opacity: 1;
373
+ }
374
+ }
375
+
376
+ @keyframes motion-section-in {
377
+ from {
378
+ opacity: 0;
379
+ transform: translateY(14px);
380
+ }
381
+
382
+ to {
383
+ opacity: 1;
384
+ transform: translateY(0);
385
+ }
386
+ }
387
+
388
+ @keyframes motion-edge-draw {
389
+ to {
390
+ stroke-dashoffset: 0;
391
+ }
392
+ }
393
+
394
+ @keyframes motion-content-in {
395
+ from {
396
+ opacity: .55;
397
+ transform: translateY(6px);
398
+ }
399
+
400
+ to {
401
+ opacity: 1;
402
+ transform: translateY(0);
403
+ }
404
+ }
405
+
406
+ @keyframes motion-item-in {
407
+ from {
408
+ opacity: 0;
409
+ transform: translateY(8px);
410
+ }
411
+
412
+ to {
413
+ opacity: 1;
414
+ transform: translateY(0);
415
+ }
416
+ }
417
+
418
+ @keyframes motion-backdrop-in {
419
+ from {
420
+ opacity: 0;
421
+ backdrop-filter: blur(0);
422
+ }
423
+
424
+ to {
425
+ opacity: 1;
426
+ }
427
+ }
428
+
429
+ @keyframes motion-backdrop-out {
430
+ from {
431
+ opacity: 1;
432
+ }
433
+
434
+ to {
435
+ opacity: 0;
436
+ backdrop-filter: blur(0);
437
+ }
438
+ }
439
+
440
+ @keyframes motion-modal-in {
441
+ from {
442
+ opacity: 0;
443
+ transform: translateY(18px) scale(.975);
444
+ }
445
+
446
+ to {
447
+ opacity: 1;
448
+ transform: translateY(0) scale(1);
449
+ }
450
+ }
451
+
452
+ @keyframes motion-modal-out {
453
+ from {
454
+ opacity: 1;
455
+ transform: translateY(0) scale(1);
456
+ }
457
+
458
+ to {
459
+ opacity: 0;
460
+ transform: translateY(10px) scale(.985);
461
+ }
462
+ }
463
+
464
+ @keyframes motion-toast-in {
465
+ from {
466
+ opacity: 0;
467
+ transform: translate(-50%,12px) scale(.96);
468
+ }
469
+
470
+ to {
471
+ opacity: 1;
472
+ transform: translate(-50%,0) scale(1);
473
+ }
474
+ }
475
+
476
+ @keyframes motion-toast-out {
477
+ from {
478
+ opacity: 1;
479
+ transform: translate(-50%,0);
480
+ }
481
+
482
+ to {
483
+ opacity: 0;
484
+ transform: translate(-50%,8px);
485
+ }
486
+ }
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ /** Connect dashboard agent sessions to live or resumed terminal targets. */
4
+ window.LoadToAgentTerminalAgentActions = function createModule(context) {
5
+ const {
6
+ $, state, init, notice, moveWorkbench, selectTmux, selectSession, bindAgent, queueHistoryRefresh,
7
+ renderTarget, fitEntry, refreshSessions, resumeSupport, resumeLaunchArgs, preferredWorkspace, providerLabel, esc,
8
+ } = context;
9
+
10
+ function tmuxRows(snapshot = state.snapshot) {
11
+ const rows = [];
12
+ for (const distro of snapshot && snapshot.tmux && snapshot.tmux.distros || []) {
13
+ for (const session of distro.sessions || []) {
14
+ for (const windowItem of session.windows || []) {
15
+ for (const pane of windowItem.panes || []) rows.push({ distro, session, window: windowItem, pane });
16
+ }
17
+ }
18
+ }
19
+ return rows;
20
+ }
21
+
22
+ function agentTargets(agentSession) {
23
+ if (!agentSession || !agentSession.id) return [];
24
+ const targets = [];
25
+ const presence = Array.isArray(agentSession.runtimePresence) ? agentSession.runtimePresence : [];
26
+ const tmuxPresence = presence.filter(item => item.kind === 'tmux');
27
+ for (const row of tmuxRows()) {
28
+ const pane = row.pane || {};
29
+ const linked = pane.agent && pane.agent.linkedSessionId === agentSession.id;
30
+ const observed = tmuxPresence.some(item => item.paneId === pane.id
31
+ || item.paneNativeId === pane.nativeId
32
+ || item.id === `tmux:${row.distro.name}:${pane.nativeId}`);
33
+ if (!linked && !observed) continue;
34
+ targets.push({
35
+ id: `tmux:${row.distro.name}:${pane.nativeId}`,
36
+ kind: 'tmux',
37
+ label: `${row.distro.name} · ${row.session.name} · ${pane.nativeId}`,
38
+ detail: `${row.window.index}:${row.window.name} · ${pane.command || 'AI 명령창'}`,
39
+ distro: row.distro.name,
40
+ paneId: pane.id,
41
+ paneNativeId: pane.nativeId,
42
+ });
43
+ }
44
+ for (const terminal of state.sessions) {
45
+ if (terminal.status !== 'running') continue;
46
+ const matched = terminal.bridgeId === agentSession.id || presence.some(item => item.terminalId === terminal.id
47
+ || Number(item.pid || 0) === Number(terminal.pid || -1)
48
+ || Number(item.parentPid || 0) === Number(terminal.pid || -1));
49
+ if (!matched) continue;
50
+ targets.push({
51
+ id: terminal.id,
52
+ kind: 'terminal',
53
+ label: terminal.title,
54
+ detail: `${String(terminal.type || '').toUpperCase()} · PID ${terminal.pid || '--'}`,
55
+ terminalId: terminal.id,
56
+ });
57
+ }
58
+ return [...new Map(targets.map(target => [target.id, target])).values()];
59
+ }
60
+
61
+ function requiredAgentTarget(agentSession, targetId = '') {
62
+ const targets = agentTargets(agentSession);
63
+ if (!targets.length) throw new Error('이 AI가 실행 중인 입력 가능한 터미널을 찾지 못했습니다. 외부 터미널에서 시작한 AI는 LoadToAgent가 직접 입력할 수 없습니다.');
64
+ if (targetId) {
65
+ const selected = targets.find(target => target.id === targetId);
66
+ if (!selected) throw new Error('선택한 터미널 연결이 더 이상 유효하지 않습니다.');
67
+ return selected;
68
+ }
69
+ if (targets.length > 1) throw new Error('지시를 보낼 터미널을 먼저 선택하세요.');
70
+ return targets[0];
71
+ }
72
+
73
+ async function dispatchAgentCommand(agentSession, command, targetId = '') {
74
+ await init();
75
+ const text = String(command || '').trim();
76
+ if (!text) throw new Error('AI에게 보낼 지시를 입력하세요.');
77
+ const target = requiredAgentTarget(agentSession, targetId);
78
+ const result = target.kind === 'tmux'
79
+ ? await window.loadtoagent.tmuxSendText({ distro: target.distro, target: target.paneNativeId, text, enter: true })
80
+ : await window.loadtoagent.terminalCommand(target.terminalId, text);
81
+ if (!result || result.ok === false) throw new Error(result && result.error || '터미널에 지시를 보내지 못했습니다.');
82
+ notice(`${target.label}에 AI 지시를 보냈습니다.`, 'success');
83
+ return { ok: true, target };
84
+ }
85
+
86
+ async function openForAgent(agentSession, targetId = '', draft = '') {
87
+ await init();
88
+ const target = requiredAgentTarget(agentSession, targetId);
89
+ state.mode = target.kind === 'tmux' ? 'tmux' : 'general';
90
+ moveWorkbench(state.mode);
91
+ if (target.kind === 'tmux') await selectTmux(target.distro, target.paneNativeId);
92
+ else await selectSession(target.terminalId);
93
+ bindAgent(agentSession, target);
94
+ queueHistoryRefresh(agentSession);
95
+ renderTarget();
96
+ const entry = target.kind === 'tmux' ? state.remoteTerminal : state.terminals.get(target.terminalId);
97
+ fitEntry(entry, target.kind === 'tmux' ? '' : target.terminalId);
98
+ const input = $('#terminalCommandInput');
99
+ input.value = String(draft || '');
100
+ state.commandDrafts.set(target.id, input.value);
101
+ input.focus({ preventScroll: true });
102
+ notice(`${target.label}의 기존 세션을 유지하고 있습니다. 이전 대화를 보면서 이어서 입력하세요.`, 'success');
103
+ return target;
104
+ }
105
+
106
+ async function resumeForAgent(agentSession, draft = '', sendDraft = false) {
107
+ await init();
108
+ const support = resumeSupport(agentSession);
109
+ if (!support.supported) throw new Error(support.reason);
110
+ const cwd = String(agentSession.cwd || preferredWorkspace() || '').trim();
111
+ if (!cwd) throw new Error('이 AI가 작업하던 폴더를 찾지 못해 세션을 안전하게 재개할 수 없습니다.');
112
+ const prompt = String(draft || '').trim();
113
+ const title = `${providerLabel(agentSession.provider)} · ${agentSession.taskName || agentSession.agentName || '세션'} 이어서 작업`;
114
+ const created = await window.loadtoagent.terminalCreate({
115
+ type: 'agent',
116
+ provider: support.provider,
117
+ args: resumeLaunchArgs(support, sendDraft ? prompt : ''),
118
+ cwd,
119
+ bridgeId: agentSession.id,
120
+ title,
121
+ cols: 120,
122
+ rows: 32,
123
+ });
124
+ if (!created || !created.id) throw new Error('AI 세션을 재개할 터미널을 만들지 못했습니다.');
125
+ state.mode = 'general';
126
+ moveWorkbench('general');
127
+ await refreshSessions();
128
+ await selectSession(created.id);
129
+ const target = {
130
+ id: created.id,
131
+ kind: 'terminal',
132
+ label: created.title || title,
133
+ detail: `${String(created.type || 'agent').toUpperCase()} · PID ${created.pid || '--'}`,
134
+ terminalId: created.id,
135
+ };
136
+ bindAgent(agentSession, target);
137
+ queueHistoryRefresh(agentSession);
138
+ renderTarget();
139
+ const input = $('#terminalCommandInput');
140
+ if (input) {
141
+ input.value = sendDraft ? '' : String(draft || '');
142
+ state.commandDrafts.set(target.id, input.value);
143
+ input.focus({ preventScroll: true });
144
+ }
145
+ notice(sendDraft && prompt
146
+ ? `${providerLabel(agentSession.provider)} 세션 ${support.sessionId.slice(0, 12)}을 이어받아 지시를 보냈습니다.`
147
+ : `${providerLabel(agentSession.provider)} 세션 ${support.sessionId.slice(0, 12)}을 다시 연결했습니다. 이어서 지시할 수 있습니다.`, 'success');
148
+ return { ...target, promptSent: Boolean(sendDraft && prompt) };
149
+ }
150
+
151
+ return { tmuxRows, agentTargets, requiredAgentTarget, dispatchAgentCommand, openForAgent, resumeForAgent };
152
+ };