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
package/preload.js ADDED
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const { contextBridge, ipcRenderer } = require('electron');
4
+
5
+ contextBridge.exposeInMainWorld('loadtoagent', {
6
+ bootstrap: () => ipcRenderer.invoke('app:bootstrap'),
7
+ backgroundState: () => ipcRenderer.invoke('app:background-state'),
8
+ showApp: () => ipcRenderer.invoke('app:show'),
9
+ setLocale: locale => ipcRenderer.invoke('app:set-locale', locale),
10
+ checkForUpdate: () => ipcRenderer.invoke('app:update-check'),
11
+ downloadUpdate: () => ipcRenderer.invoke('app:update-download'),
12
+ openDownloadedUpdate: () => ipcRenderer.invoke('app:update-open'),
13
+ openUpdateRelease: () => ipcRenderer.invoke('app:update-open-release'),
14
+ snapshot: () => ipcRenderer.invoke('agents:snapshot'),
15
+ sessionDetail: sessionId => ipcRenderer.invoke('agents:detail', sessionId),
16
+ runAgent: options => ipcRenderer.invoke('agents:run', options),
17
+ stopAgent: runId => ipcRenderer.invoke('agents:stop', runId),
18
+ activeRuns: () => ipcRenderer.invoke('agents:active-runs'),
19
+ probeProviders: () => ipcRenderer.invoke('providers:probe'),
20
+ listWorkspaces: () => ipcRenderer.invoke('workspaces:list'),
21
+ addWorkspaces: () => ipcRenderer.invoke('workspaces:add'),
22
+ removeWorkspace: folder => ipcRenderer.invoke('workspaces:remove', folder),
23
+ pickWorkspace: () => ipcRenderer.invoke('workspaces:pick'),
24
+ openExternal: url => ipcRenderer.invoke('external:open', url),
25
+ openSessionOrigin: session => ipcRenderer.invoke('agents:open-origin', session),
26
+ writeClipboard: value => ipcRenderer.invoke('clipboard:write', value),
27
+ bridgeCommand: provider => ipcRenderer.invoke('bridge:command', provider),
28
+ terminalList: () => ipcRenderer.invoke('terminals:list'),
29
+ wslDistros: () => ipcRenderer.invoke('wsl:list-distros'),
30
+ terminalGet: id => ipcRenderer.invoke('terminals:get', id),
31
+ terminalCreate: options => ipcRenderer.invoke('terminals:create', options),
32
+ terminalWrite: (id, data) => ipcRenderer.send('terminals:write', id, data),
33
+ terminalCommand: (id, command) => ipcRenderer.invoke('terminals:command', id, command),
34
+ terminalResize: (id, cols, rows) => ipcRenderer.send('terminals:resize', id, cols, rows),
35
+ terminalSignal: (id, signal) => ipcRenderer.invoke('terminals:signal', id, signal),
36
+ terminalRestart: id => ipcRenderer.invoke('terminals:restart', id),
37
+ terminalClose: id => ipcRenderer.invoke('terminals:close', id),
38
+ tmuxSendText: options => ipcRenderer.invoke('tmux:send-text', options),
39
+ tmuxSendKey: options => ipcRenderer.invoke('tmux:send-key', options),
40
+ tmuxCapture: options => ipcRenderer.invoke('tmux:capture', options),
41
+ tmuxNewSession: options => ipcRenderer.invoke('tmux:new-session', options),
42
+ tmuxNewWindow: options => ipcRenderer.invoke('tmux:new-window', options),
43
+ tmuxSplitPane: options => ipcRenderer.invoke('tmux:split-pane', options),
44
+ tmuxRenameSession: options => ipcRenderer.invoke('tmux:rename-session', options),
45
+ tmuxRenameWindow: options => ipcRenderer.invoke('tmux:rename-window', options),
46
+ tmuxSelectLayout: options => ipcRenderer.invoke('tmux:select-layout', options),
47
+ tmuxKillPane: options => ipcRenderer.invoke('tmux:kill-pane', options),
48
+ tmuxKillWindow: options => ipcRenderer.invoke('tmux:kill-window', options),
49
+ tmuxKillSession: options => ipcRenderer.invoke('tmux:kill-session', options),
50
+ onTerminalData: callback => {
51
+ const handler = (_event, payload) => callback(payload);
52
+ ipcRenderer.on('terminals:data', handler);
53
+ return () => ipcRenderer.removeListener('terminals:data', handler);
54
+ },
55
+ onTerminalState: callback => {
56
+ const handler = (_event, payload) => callback(payload);
57
+ ipcRenderer.on('terminals:state', handler);
58
+ return () => ipcRenderer.removeListener('terminals:state', handler);
59
+ },
60
+ onTerminalError: callback => {
61
+ const handler = (_event, payload) => callback(payload);
62
+ ipcRenderer.on('terminals:error', handler);
63
+ return () => ipcRenderer.removeListener('terminals:error', handler);
64
+ },
65
+ onSnapshot: callback => {
66
+ const handler = (_event, snapshot) => callback(snapshot);
67
+ ipcRenderer.on('agents:snapshot', handler);
68
+ return () => ipcRenderer.removeListener('agents:snapshot', handler);
69
+ },
70
+ onUpdateState: callback => {
71
+ const handler = (_event, update) => callback(update);
72
+ ipcRenderer.on('app:update-state', handler);
73
+ return () => ipcRenderer.removeListener('app:update-state', handler);
74
+ },
75
+ });
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(context = {}) {
6
+ const {
7
+ $,
8
+ esc,
9
+ state,
10
+ selectView,
11
+ providerInfo,
12
+ isLiveSession,
13
+ } = context;
14
+
15
+ function agentCommandTargets(session) {
16
+ try {
17
+ return window.LoadToAgentTerminal && typeof window.LoadToAgentTerminal.agentTargets === "function"
18
+ ? window.LoadToAgentTerminal.agentTargets(session)
19
+ : [];
20
+ } catch (error) {
21
+ window.LoadToAgentRendererUtils.reportRecoverableError("agent-command-targets", error);
22
+ return [];
23
+ }
24
+ }
25
+
26
+ function agentResumeSupport(session) {
27
+ try {
28
+ return window.LoadToAgentTerminal && typeof window.LoadToAgentTerminal.resumeSupport === "function"
29
+ ? window.LoadToAgentTerminal.resumeSupport(session)
30
+ : { supported: false, reason: "터미널 재개 기능을 준비하는 중입니다." };
31
+ } catch (error) {
32
+ return { supported: false, reason: (error && error.message) || "세션 재개 가능 여부를 확인하지 못했습니다." };
33
+ }
34
+ }
35
+
36
+ function originAppInfo(session) {
37
+ if (session && session.provider === "codex" && session.clientKind === "codex-desktop") {
38
+ return { provider: "Codex", label: "Codex 데스크톱 앱" };
39
+ }
40
+ if (session && session.provider === "claude" && session.clientKind === "claude-desktop") {
41
+ return { provider: "Claude", label: "Claude 데스크톱 앱" };
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function agentControlMode(session, targets) {
47
+ if (targets.length) return "direct";
48
+ if (originAppInfo(session)) return !isLiveSession(session) && agentResumeSupport(session).supported ? "origin-resume" : "origin";
49
+ if (agentResumeSupport(session).supported) return isLiveSession(session) ? "handoff" : "resume";
50
+ if (isLiveSession(session)) return "connect";
51
+ return "ended";
52
+ }
53
+
54
+ function agentCommandComposer(session) {
55
+ const targets = agentCommandTargets(session);
56
+ const mode = agentControlMode(session, targets);
57
+ const savedTarget = state.agentCommandTargets.get(session.id) || "";
58
+ const targetId = targets.some((target) => target.id === savedTarget) ? savedTarget : targets.length === 1 ? targets[0].id : "";
59
+ if (targetId) state.agentCommandTargets.set(session.id, targetId);
60
+ const target = targets.find((item) => item.id === targetId) || null;
61
+ const draft = state.agentCommandDrafts.get(session.id) || "";
62
+ const sending = state.agentCommandSending.has(session.id);
63
+ const canSend = ((mode === "direct" && Boolean(target)) || ["resume", "handoff", "origin-resume"].includes(mode)) && !sending;
64
+ const origin = originAppInfo(session);
65
+ const status =
66
+ mode === "direct"
67
+ ? `직접 입력 가능 · ${targets.length === 1 ? target.label : `${targets.length}개 터미널 중 선택`}`
68
+ : mode === "handoff"
69
+ ? "외부 터미널에서 실행 중 · 같은 대화로 이어받기 가능"
70
+ : mode === "resume"
71
+ ? "원래 터미널이 종료됨 · 같은 세션으로 복구 가능"
72
+ : mode === "origin-resume"
73
+ ? "쉬는 데스크톱 작업 · 백그라운드 터미널로 이어가기 가능"
74
+ : mode === "connect"
75
+ ? "연결 후 입력 가능 · 현재 세션은 보기 전용"
76
+ : mode === "origin"
77
+ ? "보기 전용 · 원래 앱에서 계속"
78
+ : window.LoadToAgentI18n.t("ui.ended_session");
79
+ const help =
80
+ mode === "direct"
81
+ ? "기존 터미널에 바로 보냅니다. 앱 창을 닫아도 백그라운드에서 세션을 계속 유지합니다."
82
+ : mode === "handoff"
83
+ ? "같은 세션 ID와 대화 내역을 LoadToAgent 관리 터미널로 이어받고 백그라운드에서 유지합니다."
84
+ : mode === "resume"
85
+ ? "기존 세션 ID와 대화 맥락을 복구하고, 앱 창을 닫아도 백그라운드에서 유지합니다."
86
+ : mode === "origin-resume"
87
+ ? `원래 ${(origin && origin.provider) || "데스크톱"} 앱으로 돌아가거나, 같은 세션을 백그라운드 터미널로 이어서 작업할 수 있습니다.`
88
+ : mode === "connect"
89
+ ? `새 터미널에서 loadtoagent run ${session.provider}로 시작하면 창을 닫아도 안전하게 유지됩니다.`
90
+ : mode === "origin"
91
+ ? `이 대화는 ${(origin && origin.label) || "데스크톱 앱"}에서 현재 실행 중이므로 원래 작업에서 계속합니다.`
92
+ : agentResumeSupport(session).reason || "이 제공사의 세션 재개 방식을 확인할 수 없습니다.";
93
+ const picker =
94
+ targets.length > 1
95
+ ? `<label class="agent-command-target">
96
+ <span>보낼 터미널</span>
97
+ <select data-agent-command-target="${esc(session.id)}">
98
+ <option value="">터미널을 선택하세요</option>
99
+ ${targets.map((item) => `<option value="${esc(item.id)}" ${item.id === targetId ? "selected" : ""}>${esc(item.label)}</option>`).join("")}
100
+ </select>
101
+ </label>`
102
+ : "";
103
+ const originAction = `<button type="button" data-agent-open-origin="${esc(session.id)}">원래 ${esc((origin && origin.provider) || "데스크톱")} 앱에서 계속하기</button>`;
104
+ const actions =
105
+ mode === "direct"
106
+ ? `<button type="button" data-agent-terminal-open="${esc(session.id)}" ${canSend ? "" : "disabled"}>터미널에서 열기</button>
107
+ <button type="submit" ${canSend ? "" : "disabled"}>${sending ? "보내는 중…" : "바로 보내기 ↵"}</button>`
108
+ : mode === "resume"
109
+ ? `<button type="submit" ${canSend ? "" : "disabled"}>${sending ? "복구하는 중…" : "같은 세션으로 복구해 보내기 ↵"}</button>`
110
+ : mode === "handoff"
111
+ ? `<button type="submit" ${canSend ? "" : "disabled"}>${sending ? "이어받는 중…" : "관리 터미널로 이어받아 보내기 ↵"}</button>`
112
+ : mode === "origin-resume"
113
+ ? `${originAction}<button type="submit" ${canSend ? "" : "disabled"}>${sending ? "연결하는 중…" : "백그라운드 터미널로 이어서 보내기 ↵"}</button>`
114
+ : mode === "connect"
115
+ ? `<button type="button" data-agent-bridge-copy="${esc(session.provider)}">연결 명령 복사</button>`
116
+ : mode === "origin"
117
+ ? originAction
118
+ : "";
119
+ const editable = ["direct", "resume", "handoff", "origin-resume"].includes(mode);
120
+ const placeholder = editable ? "예: 이전 작업에 이어서 테스트를 실행하고 결과를 알려줘" : status;
121
+ const availabilityClass = mode === "direct" ? "connected" : ["resume", "handoff", "origin-resume"].includes(mode) ? "resume-ready" : "unavailable";
122
+ return `<form class="agent-command-panel ${availabilityClass} control-${mode}"
123
+ data-agent-command-form="${esc(session.id)}">
124
+ <header>
125
+ <span class="agent-command-icon" aria-hidden="true">›_</span>
126
+ <span><b>이 AI에게 바로 지시하기</b><small>${esc(status)}</small></span>
127
+ <i class="${mode === "direct" ? "connected" : ""}" aria-hidden="true"></i>
128
+ </header>
129
+ ${picker}
130
+ <label class="agent-command-input">
131
+ <span class="sr-only">AI에게 보낼 터미널 지시</span>
132
+ <textarea data-agent-command-draft="${esc(session.id)}" maxlength="8000" rows="3"
133
+ placeholder="${esc(placeholder)}" ${editable ? "" : "disabled"}>${editable ? esc(draft) : ""}</textarea>
134
+ </label>
135
+ <div class="agent-command-actions"><small aria-live="polite">${esc(help)}</small>${actions}</div>
136
+ </form>`;
137
+ }
138
+
139
+ function selectedSession() {
140
+ return (
141
+ state.details.get(state.selectedId) || ((state.snapshot && state.snapshot.sessions) || []).find((session) => session.id === state.selectedId) || null
142
+ );
143
+ }
144
+
145
+ function snapshotSession(id) {
146
+ return ((state.snapshot && state.snapshot.sessions) || []).find((session) => session.id === id) || null;
147
+ }
148
+
149
+ function chosenAgentCommandTarget(session) {
150
+ const targets = agentCommandTargets(session);
151
+ const saved = state.agentCommandTargets.get(session.id) || "";
152
+ if (saved) return targets.find((target) => target.id === saved) || null;
153
+ return targets.length === 1 ? targets[0] : null;
154
+ }
155
+
156
+ async function resumeAgentTerminal(sessionId, sendDraft = false) {
157
+ if (state.agentCommandSending.has(sessionId)) return;
158
+ const session = snapshotSession(sessionId) || state.details.get(sessionId);
159
+ if (!session || !window.LoadToAgentTerminal) return context.toast("다시 연결할 AI 세션 정보를 찾지 못했습니다.");
160
+ const support = agentResumeSupport(session);
161
+ if (!support.supported) return context.toast(support.reason || "이 AI 세션은 터미널에서 다시 연결할 수 없습니다.");
162
+ state.agentCommandSending.add(sessionId);
163
+ try {
164
+ if ($("#detailDrawer").classList.contains("open")) context.closeDrawer(false);
165
+ selectView("terminal");
166
+ const draft = state.agentCommandDrafts.get(sessionId) || "";
167
+ await window.LoadToAgentTerminal.resumeForAgent(session, draft, sendDraft);
168
+ if (sendDraft && draft.trim()) state.agentCommandDrafts.delete(sessionId);
169
+ document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
170
+ context.toast(`${providerInfo(session.provider).label}의 기존 세션을 터미널에 다시 연결했습니다.`);
171
+ } catch (error) {
172
+ context.toast((error && error.message) || "AI 세션을 다시 연결하지 못했습니다.");
173
+ } finally {
174
+ state.agentCommandSending.delete(sessionId);
175
+ }
176
+ }
177
+
178
+ async function dispatchAgentCommand(sessionId, form) {
179
+ if (state.agentCommandSending.has(sessionId)) return;
180
+ const session = snapshotSession(sessionId);
181
+ if (!session || !window.LoadToAgentTerminal) return context.toast("선택한 AI의 최신 정보를 찾지 못했습니다.");
182
+ const mode = agentControlMode(session, agentCommandTargets(session));
183
+ if (mode === "resume" || mode === "handoff" || mode === "origin-resume") {
184
+ const input = form.querySelector("[data-agent-command-draft]");
185
+ if (input) state.agentCommandDrafts.set(sessionId, input.value);
186
+ if (!String((input && input.value) || "").trim()) return context.toast("AI에게 보낼 지시를 입력하세요.");
187
+ return resumeAgentTerminal(sessionId, true);
188
+ }
189
+ const target = chosenAgentCommandTarget(session);
190
+ const input = form.querySelector("[data-agent-command-draft]");
191
+ const command = String((input && input.value) || "").trim();
192
+ if (!target)
193
+ return context.toast(agentCommandTargets(session).length ? "지시를 보낼 터미널을 먼저 선택하세요." : "이 AI에 연결된 입력 가능한 터미널이 없습니다.");
194
+ if (!command) return context.toast("AI에게 보낼 지시를 입력하세요.");
195
+ state.agentCommandSending.add(sessionId);
196
+ const submit = form.querySelector('[type="submit"]');
197
+ if (submit) {
198
+ submit.disabled = true;
199
+ submit.textContent = "보내는 중…";
200
+ }
201
+ try {
202
+ await window.LoadToAgentTerminal.dispatchAgentCommand(session, command, target.id);
203
+ state.agentCommandDrafts.delete(sessionId);
204
+ if (input) input.value = "";
205
+ context.toast(`${target.label}에 지시를 보냈습니다.`);
206
+ } catch (error) {
207
+ const latest = snapshotSession(sessionId) || session;
208
+ const support = agentResumeSupport(latest);
209
+ if (!agentCommandTargets(latest).length && support.supported) {
210
+ try {
211
+ state.agentCommandDrafts.set(sessionId, command);
212
+ await window.LoadToAgentTerminal.resumeForAgent(latest, command, true);
213
+ state.agentCommandDrafts.delete(sessionId);
214
+ if (input) input.value = "";
215
+ context.toast("원래 터미널 연결이 종료되어 같은 AI 세션으로 복구한 뒤 지시를 보냈습니다.");
216
+ return;
217
+ } catch (resumeError) {
218
+ context.toast((resumeError && resumeError.message) || "터미널 연결이 끊어졌고 세션 복구에도 실패했습니다.");
219
+ }
220
+ } else context.toast((error && error.message) || "터미널에 지시를 보내지 못했습니다.");
221
+ } finally {
222
+ state.agentCommandSending.delete(sessionId);
223
+ if (submit && submit.isConnected) {
224
+ submit.disabled = false;
225
+ submit.textContent = "바로 보내기 ↵";
226
+ }
227
+ }
228
+ }
229
+
230
+ async function openAgentTerminal(sessionId) {
231
+ const session = snapshotSession(sessionId);
232
+ if (!session || !window.LoadToAgentTerminal) return context.toast("선택한 AI의 터미널 정보를 찾지 못했습니다.");
233
+ const target = chosenAgentCommandTarget(session);
234
+ if (!target)
235
+ return context.toast(agentCommandTargets(session).length ? "열어볼 터미널을 먼저 선택하세요." : "이 AI에 연결된 입력 가능한 터미널이 없습니다.");
236
+ selectView(target.kind === "tmux" ? "tmux" : "terminal");
237
+ try {
238
+ await window.LoadToAgentTerminal.openForAgent(session, target.id, state.agentCommandDrafts.get(sessionId) || "");
239
+ document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
240
+ } catch (error) {
241
+ context.toast((error && error.message) || "AI의 터미널을 열지 못했습니다.");
242
+ }
243
+ }
244
+
245
+ async function copyBridgeCommand(provider) {
246
+ try {
247
+ const result = await window.loadtoagent.bridgeCommand(provider);
248
+ if (!result || !result.ok) throw new Error("연결 명령을 만들지 못했습니다.");
249
+ const command = result.command;
250
+ await window.loadtoagent.writeClipboard(command);
251
+ context.toast(`${command} 명령을 복사했습니다.`);
252
+ } catch (error) {
253
+ context.toast((error && error.message) || "연결 명령을 복사하지 못했습니다.");
254
+ }
255
+ }
256
+
257
+ async function openSessionOrigin(sessionId) {
258
+ const session = snapshotSession(sessionId);
259
+ const origin = originAppInfo(session);
260
+ if (!session || !origin) return context.toast("원래 데스크톱 작업 정보를 찾지 못했습니다.");
261
+ try {
262
+ const result = await window.loadtoagent.openSessionOrigin(session);
263
+ if (!result || !result.ok) return context.toast(`이 작업은 ${origin.label}에서 직접 열 수 없습니다.`);
264
+ context.toast(`원래 ${origin.provider} 작업을 열었습니다.`);
265
+ } catch (error) {
266
+ context.toast((error && error.message) || `${origin.label}을 열지 못했습니다.`);
267
+ }
268
+ }
269
+
270
+ return {
271
+ agentCommandTargets,
272
+ agentResumeSupport,
273
+ originAppInfo,
274
+ agentControlMode,
275
+ agentCommandComposer,
276
+ selectedSession,
277
+ snapshotSession,
278
+ chosenAgentCommandTarget,
279
+ resumeAgentTerminal,
280
+ dispatchAgentCommand,
281
+ openAgentTerminal,
282
+ copyBridgeCommand,
283
+ openSessionOrigin,
284
+ };
285
+ };
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ (() => {
4
+ const factories = window.LoadToAgentAppFactories || {};
5
+ const app = {};
6
+ const install = (name) => {
7
+ if (typeof factories[name] !== "function") throw new Error(`앱 모듈을 찾지 못했습니다: ${name}`);
8
+ Object.assign(app, factories[name](app));
9
+ };
10
+
11
+ [
12
+ "createCore",
13
+ "createDashboard",
14
+ "createGraphModel",
15
+ "createGraphView",
16
+ "createGraphLayout",
17
+ "createGraphOrchestration",
18
+ "createTmuxRenderer",
19
+ "createSessionRenderer",
20
+ "createAgentActions",
21
+ "createDrawerData",
22
+ "createDrawerContent",
23
+ "createDrawer",
24
+ "createRunModal",
25
+ "createNavigationEventBindings",
26
+ "createSessionEventBindings",
27
+ "createFilterEventBindings",
28
+ "createDialogEventBindings",
29
+ "createEventBindings",
30
+ ].forEach(install);
31
+ window.LoadToAgentApp = app;
32
+
33
+ const { $, esc, state, loadGuideState, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, toast } = app;
34
+
35
+ async function init() {
36
+ loadGuideState();
37
+ if (!window.loadtoagent) {
38
+ $("#emptyState").classList.remove("hidden");
39
+ $("#emptyState p").textContent = "LoadToAgent 프로그램에서 열면 이 컴퓨터의 AI 작업 기록을 불러옵니다.";
40
+ return;
41
+ }
42
+ const bootstrap = await window.loadtoagent.bootstrap();
43
+ if (window.loadtoagent.setLocale) await window.loadtoagent.setLocale(window.LoadToAgentI18n?.getLocale() || "ko");
44
+ state.providers = bootstrap.providers || [];
45
+ state.providerMap = new Map(state.providers.map((provider) => [provider.id, provider]));
46
+ state.availability = bootstrap.availability || {};
47
+ state.workspaces = bootstrap.workspaces || [];
48
+ state.snapshot = bootstrap.snapshot;
49
+ state.activeRuns = bootstrap.activeRuns || [];
50
+ state.platform = bootstrap.platform || state.platform;
51
+ state.versions = bootstrap.versions || {};
52
+ state.update = bootstrap.update || { status: "idle", currentVersion: state.versions.app || "" };
53
+ bindEvents();
54
+ render();
55
+ $("#lastSync").textContent = timeOnly(state.snapshot && state.snapshot.generatedAt);
56
+ window.loadtoagent.onSnapshot((snapshot) => {
57
+ state.snapshot = snapshot;
58
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(snapshot, state.workspaces);
59
+ $("#lastSync").textContent = timeOnly(snapshot.generatedAt);
60
+ render();
61
+ if (state.selectedId && $("#detailDrawer").classList.contains("open") && !state.detailLoadingIds.has(state.selectedId)) {
62
+ const card = (snapshot.sessions || []).find((session) => session.id === state.selectedId);
63
+ const detail = state.details.get(state.selectedId);
64
+ if (card && detail && card.updatedAt !== detail.updatedAt) loadSessionDetail(state.selectedId, true);
65
+ }
66
+ });
67
+ if (window.loadtoagent.onUpdateState)
68
+ window.loadtoagent.onUpdateState((update) => {
69
+ state.update = update;
70
+ renderUpdateSettings();
71
+ });
72
+ }
73
+
74
+ app.init = init;
75
+
76
+ window.addEventListener("loadtoagent:locale-changed", (event) => {
77
+ if (window.loadtoagent?.setLocale) {
78
+ Promise.resolve(window.loadtoagent.setLocale(event.detail.locale)).catch((error) => {
79
+ window.LoadToAgentRendererUtils.reportRecoverableError("locale-persistence", error);
80
+ });
81
+ }
82
+ if (!state.snapshot) {
83
+ syncViewChrome();
84
+ return;
85
+ }
86
+ render("locale");
87
+ $("#lastSync").textContent = timeOnly(state.snapshot.generatedAt);
88
+ });
89
+
90
+ init().catch((error) => {
91
+ console.error(error);
92
+ $("#lastSync").textContent = window.LoadToAgentI18n.t("ui.connection_failed");
93
+ toast(`초기화 실패: ${error.message}`);
94
+ });
95
+ })();