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.
- package/README.ko.md +175 -0
- package/README.md +175 -0
- package/README.zh-CN.md +175 -0
- package/bin/loadtoagent.js +171 -0
- package/docs/ARCHITECTURE.md +30 -0
- package/docs/PROVIDER-CONTRACTS.md +58 -0
- package/docs/assets/loadtoagent-dashboard.png +0 -0
- package/docs/assets/loadtoagent-demo.gif +0 -0
- package/main.js +493 -0
- package/package.json +133 -0
- package/preload.js +75 -0
- package/renderer/app-agent-actions.js +285 -0
- package/renderer/app-bootstrap.js +95 -0
- package/renderer/app-dashboard.js +361 -0
- package/renderer/app-drawer-content.js +203 -0
- package/renderer/app-drawer-data.js +41 -0
- package/renderer/app-drawer.js +183 -0
- package/renderer/app-events-dialogs.js +169 -0
- package/renderer/app-events-filters.js +74 -0
- package/renderer/app-events-navigation.js +96 -0
- package/renderer/app-events-sessions.js +195 -0
- package/renderer/app-events.js +16 -0
- package/renderer/app-graph-layout.js +71 -0
- package/renderer/app-graph-model.js +107 -0
- package/renderer/app-graph-orchestration.js +76 -0
- package/renderer/app-graph-view.js +544 -0
- package/renderer/app-run-modal.js +221 -0
- package/renderer/app-session-render.js +243 -0
- package/renderer/app-tmux-render.js +247 -0
- package/renderer/app.js +608 -0
- package/renderer/fonts/geist-ext.woff2 +0 -0
- package/renderer/fonts/geist.woff2 +0 -0
- package/renderer/i18n-messages.js +355 -0
- package/renderer/i18n.js +122 -0
- package/renderer/index.html +386 -0
- package/renderer/shared.js +26 -0
- package/renderer/styles-agent-map.css +401 -0
- package/renderer/styles-cards.css +600 -0
- package/renderer/styles-collaboration.css +534 -0
- package/renderer/styles-components.css +1293 -0
- package/renderer/styles-onboarding.css +344 -0
- package/renderer/styles-overlays.css +284 -0
- package/renderer/styles-product.css +686 -0
- package/renderer/styles-responsive-product.css +689 -0
- package/renderer/styles-responsive-runtime.css +718 -0
- package/renderer/styles-responsive-shell.css +533 -0
- package/renderer/styles-responsive-workflows.css +778 -0
- package/renderer/styles-run-composer.css +695 -0
- package/renderer/styles-settings.css +606 -0
- package/renderer/styles-terminal.css +1241 -0
- package/renderer/styles-tmux.css +1162 -0
- package/renderer/styles-workflow-map.css +513 -0
- package/renderer/styles-workflows.css +1217 -0
- package/renderer/styles.css +486 -0
- package/renderer/terminal-agent.js +152 -0
- package/renderer/terminal-events.js +226 -0
- package/renderer/terminal-workbench.js +464 -0
- package/renderer/terminal.js +497 -0
- package/src/agentMonitor/claudeParser.js +197 -0
- package/src/agentMonitor/codexCollaboration.js +95 -0
- package/src/agentMonitor/codexParser.js +447 -0
- package/src/agentMonitor/genericParser.js +244 -0
- package/src/agentMonitor/hierarchy.js +248 -0
- package/src/agentMonitor/sessionFiles.js +86 -0
- package/src/agentMonitor.js +591 -0
- package/src/agentRunner.js +465 -0
- package/src/bridgeServer.js +246 -0
- package/src/contracts.js +71 -0
- package/src/diagnostics.js +23 -0
- package/src/ipc/registerAgentIpc.js +12 -0
- package/src/ipc/registerAppIpc.js +20 -0
- package/src/ipc/registerTerminalIpc.js +50 -0
- package/src/ipc/registerTmuxIpc.js +30 -0
- package/src/ipc/registerWorkspaceIpc.js +14 -0
- package/src/monitorWorker.js +273 -0
- package/src/processMonitor.js +394 -0
- package/src/providerRegistry.js +120 -0
- package/src/terminalManager.js +438 -0
- package/src/tmuxController.js +183 -0
- package/src/tmuxMonitor.js +432 -0
- package/src/updateManager.js +339 -0
- package/src/workspaceStore.js +77 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger,
|
|
8
|
+
providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionWorkspaceLabel,
|
|
9
|
+
agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
|
|
10
|
+
chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentConversationHtml,
|
|
11
|
+
} = context;
|
|
12
|
+
|
|
13
|
+
function openDrawer(id) {
|
|
14
|
+
rememberDialogTrigger();
|
|
15
|
+
markGuideStep("detail");
|
|
16
|
+
state.selectedId = id;
|
|
17
|
+
state.drawerMode = "session";
|
|
18
|
+
state.drawerTab = "chat";
|
|
19
|
+
state.drawerForceLatest = true;
|
|
20
|
+
clearTimeout(motionState.drawerTimer);
|
|
21
|
+
$("#drawerBackdrop").classList.remove("hidden");
|
|
22
|
+
$("#drawerBackdrop").classList.remove("closing");
|
|
23
|
+
$("#detailDrawer").classList.add("open");
|
|
24
|
+
$("#detailDrawer").removeAttribute("inert");
|
|
25
|
+
$("#detailDrawer").setAttribute("aria-hidden", "false");
|
|
26
|
+
renderDrawer();
|
|
27
|
+
loadSessionDetail(id, true);
|
|
28
|
+
setTimeout(() => $("#closeDrawerBtn").focus(), 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function openSubagentConversation(id) {
|
|
32
|
+
const child = snapshotSession(id) || state.details.get(id);
|
|
33
|
+
if (!child || !child.parentId) return openDrawer(id);
|
|
34
|
+
rememberDialogTrigger();
|
|
35
|
+
markGuideStep("detail");
|
|
36
|
+
state.selectedId = id;
|
|
37
|
+
state.drawerMode = "subagent";
|
|
38
|
+
state.drawerTab = "chat";
|
|
39
|
+
state.drawerForceLatest = true;
|
|
40
|
+
clearTimeout(motionState.drawerTimer);
|
|
41
|
+
$("#drawerBackdrop").classList.remove("hidden");
|
|
42
|
+
$("#drawerBackdrop").classList.remove("closing");
|
|
43
|
+
$("#detailDrawer").classList.add("open");
|
|
44
|
+
$("#detailDrawer").removeAttribute("inert");
|
|
45
|
+
$("#detailDrawer").setAttribute("aria-hidden", "false");
|
|
46
|
+
renderDrawer();
|
|
47
|
+
loadSubagentParentDetail(child);
|
|
48
|
+
setTimeout(() => $("#closeDrawerBtn").focus(), 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function closeDrawer(restoreFocus = true) {
|
|
52
|
+
if (!$("#detailDrawer").classList.contains("open")) return;
|
|
53
|
+
$("#detailDrawer").classList.remove("open");
|
|
54
|
+
$("#detailDrawer").setAttribute("aria-hidden", "true");
|
|
55
|
+
$("#detailDrawer").setAttribute("inert", "");
|
|
56
|
+
$("#drawerBackdrop").classList.add("closing");
|
|
57
|
+
clearTimeout(motionState.drawerTimer);
|
|
58
|
+
motionState.drawerTimer = setTimeout(
|
|
59
|
+
() => {
|
|
60
|
+
$("#drawerBackdrop").classList.add("hidden");
|
|
61
|
+
$("#drawerBackdrop").classList.remove("closing");
|
|
62
|
+
if (restoreFocus) restoreDialogTrigger();
|
|
63
|
+
else motionState.activeDialogTrigger = null;
|
|
64
|
+
},
|
|
65
|
+
motionPreference.matches ? 0 : 260,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderDrawer() {
|
|
70
|
+
const session = selectedSession();
|
|
71
|
+
if (!session) return closeDrawer();
|
|
72
|
+
const provider = providerInfo(session.provider);
|
|
73
|
+
const subagentMode = state.drawerMode === "subagent" && Boolean(session.parentId);
|
|
74
|
+
const detailLoading = !subagentMode && state.detailLoadingIds.has(state.selectedId);
|
|
75
|
+
$("#detailDrawer").style.setProperty("--drawer-provider", provider.accent);
|
|
76
|
+
$("#drawerProviderMark").style.setProperty("--provider", provider.accent);
|
|
77
|
+
$("#drawerProviderMark").textContent = provider.mark;
|
|
78
|
+
$("#drawerProvider").textContent = subagentMode
|
|
79
|
+
? `${session.agentName || provider.label} · 메인 AI와의 소통`
|
|
80
|
+
: `${provider.company} · ${STATUS[session.status] || session.status}`;
|
|
81
|
+
$("#drawerTitle").textContent = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
|
|
82
|
+
const stopping = session.runId && state.stopRequests.has(session.runId);
|
|
83
|
+
const stop =
|
|
84
|
+
session.runId && (session.status === "running" || session.status === "starting")
|
|
85
|
+
? `<button class="meta-chip stop-run" data-stop-run="${esc(session.runId)}"
|
|
86
|
+
${stopping ? 'disabled aria-busy="true"' : ""}>
|
|
87
|
+
${stopping ? "중지 요청 중…" : "■ 실행 중지"}</button>`
|
|
88
|
+
: "";
|
|
89
|
+
const runtime = session.runtimePresence || [];
|
|
90
|
+
const resume =
|
|
91
|
+
!isLiveSession(session) && agentResumeSupport(session).supported
|
|
92
|
+
? `<button class="meta-chip resume-agent" data-resume-agent="${esc(session.id)}">▶
|
|
93
|
+
<b>${originAppInfo(session) ? "백그라운드 터미널로 이어가기" : "터미널로 다시 일 시키기"}</b>
|
|
94
|
+
</button>`
|
|
95
|
+
: "";
|
|
96
|
+
const communicationCount = subagentMode ? subagentCommunicationEvents(session).length : 0;
|
|
97
|
+
$("#drawerMeta").innerHTML = subagentMode
|
|
98
|
+
? `<span class="meta-chip work-state ${subagentWorkState(session)}">
|
|
99
|
+
<b>${esc(subagentWorkLabel(session))}</b>
|
|
100
|
+
</span>
|
|
101
|
+
<span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
|
|
102
|
+
</span>
|
|
103
|
+
<span class="meta-chip">메인과 소통 <b>${communicationCount}건</b>
|
|
104
|
+
</span>${resume}`
|
|
105
|
+
: `<span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
|
|
106
|
+
</span>
|
|
107
|
+
<span class="meta-chip">작업 폴더
|
|
108
|
+
<b title="${esc(isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.session_not_linked_to_a_specific_project") : session.cwd)}">
|
|
109
|
+
${esc(sessionWorkspaceLabel(session))}
|
|
110
|
+
</b>
|
|
111
|
+
</span>
|
|
112
|
+
<span class="meta-chip">작업 번호 <b>${esc(String(session.externalId || "").slice(0, 12) || "정보 없음")}</b>
|
|
113
|
+
</span>${
|
|
114
|
+
session.parentId
|
|
115
|
+
? `<span class="meta-chip">⑂ <b>도움을 맡은 AI</b>
|
|
116
|
+
</span>`
|
|
117
|
+
: ""
|
|
118
|
+
}${
|
|
119
|
+
runtime.length
|
|
120
|
+
? `<span class="meta-chip runtime-meta">● <b>실행 중인 프로그램 ${runtime.length}개</b>
|
|
121
|
+
</span>`
|
|
122
|
+
: ""
|
|
123
|
+
}${resume}${stop}`;
|
|
124
|
+
$$(".drawer-tab").forEach((tab) => {
|
|
125
|
+
const hidden = subagentMode && tab.dataset.tab !== "chat";
|
|
126
|
+
tab.classList.toggle("hidden", hidden);
|
|
127
|
+
if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? "메인과의 대화" : window.LoadToAgentI18n.t("ui.conversation");
|
|
128
|
+
const active = tab.dataset.tab === state.drawerTab;
|
|
129
|
+
tab.classList.toggle("active", active);
|
|
130
|
+
tab.setAttribute("aria-selected", active ? "true" : "false");
|
|
131
|
+
tab.tabIndex = active ? 0 : -1;
|
|
132
|
+
});
|
|
133
|
+
const activeTab = $(`.drawer-tab[data-tab="${state.drawerTab}"]`);
|
|
134
|
+
if (activeTab) $("#drawerContent").setAttribute("aria-labelledby", activeTab.id);
|
|
135
|
+
const content = $("#drawerContent");
|
|
136
|
+
const previousTop = content.scrollTop;
|
|
137
|
+
const wasNearBottom = content.scrollHeight - content.scrollTop - content.clientHeight < 90;
|
|
138
|
+
const renderKey = `${state.drawerMode}:${state.selectedId}:${state.drawerTab}:${detailLoading ? "loading" : "ready"}`;
|
|
139
|
+
const previousRenderKey = motionState.drawerRenderKey;
|
|
140
|
+
const shouldAnimateContent = previousRenderKey !== renderKey;
|
|
141
|
+
const tabChanged = Boolean(motionState.drawerTab) && motionState.drawerTab !== state.drawerTab;
|
|
142
|
+
motionState.drawerRenderKey = renderKey;
|
|
143
|
+
motionState.drawerTab = state.drawerTab;
|
|
144
|
+
const detailError = state.detailErrors.get(state.selectedId);
|
|
145
|
+
content.innerHTML = detailLoading
|
|
146
|
+
? '<div class="drawer-loading"><span></span><b>전체 작업 기록을 불러오는 중</b><small>잠시만 기다리면 대화와 진행 과정을 볼 수 있어요.</small></div>'
|
|
147
|
+
: detailError && !subagentMode
|
|
148
|
+
? `<div class="drawer-error">
|
|
149
|
+
<b>작업 기록을 불러오지 못했습니다</b>
|
|
150
|
+
<span>${esc(detailError)}</span>
|
|
151
|
+
<button type="button" data-retry-detail="${esc(state.selectedId)}">다시 시도</button>
|
|
152
|
+
</div>`
|
|
153
|
+
: subagentMode
|
|
154
|
+
? subagentConversationHtml(session)
|
|
155
|
+
: state.drawerTab === "chat"
|
|
156
|
+
? chatHtml(session)
|
|
157
|
+
: state.drawerTab === "lifecycle"
|
|
158
|
+
? lifecycleHtml(session)
|
|
159
|
+
: tokensHtml(session);
|
|
160
|
+
content.classList.toggle("motion-content-in", shouldAnimateContent && !motionPreference.matches);
|
|
161
|
+
clearTimeout(motionState.drawerContentTimer);
|
|
162
|
+
if (shouldAnimateContent)
|
|
163
|
+
motionState.drawerContentTimer = setTimeout(() => content.classList.remove("motion-content-in"), motionPreference.matches ? 0 : 520);
|
|
164
|
+
if (!detailLoading)
|
|
165
|
+
requestAnimationFrame(() => {
|
|
166
|
+
const forceLatest = state.drawerForceLatest;
|
|
167
|
+
if (state.drawerTab === "chat" && forceLatest) {
|
|
168
|
+
const rows = [...content.querySelectorAll(".chat-row")];
|
|
169
|
+
const latest = rows[rows.length - 1];
|
|
170
|
+
if (latest && latest.offsetHeight > content.clientHeight - 90) {
|
|
171
|
+
const contentTop = content.getBoundingClientRect().top;
|
|
172
|
+
const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
|
|
173
|
+
content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
|
|
174
|
+
} else content.scrollTop = content.scrollHeight;
|
|
175
|
+
} else if (tabChanged) content.scrollTop = 0;
|
|
176
|
+
else if (state.drawerTab === "chat" && wasNearBottom) content.scrollTop = content.scrollHeight;
|
|
177
|
+
else content.scrollTop = Math.min(previousTop, Math.max(0, content.scrollHeight - content.clientHeight));
|
|
178
|
+
state.drawerForceLatest = false;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { openDrawer, openSubagentConversation, closeDrawer, renderDrawer };
|
|
183
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createDialogEventBindings = function createDialogEventBindings(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$, $$, state, providerInfo, renderProviderRail, scheduleAgentWorkflowConnections, resumeAgentTerminal, loadSessionDetail,
|
|
8
|
+
closeDrawer, renderDrawer, providerPickerHtml, syncRunComposer, openRunModal, closeRunModal, toast, performUiAction,
|
|
9
|
+
handleRun, trapDialogFocus,
|
|
10
|
+
} = context;
|
|
11
|
+
|
|
12
|
+
function bindRunComposerEvents() {
|
|
13
|
+
$("#newRunBtn").addEventListener("click", openRunModal);
|
|
14
|
+
$$("[data-open-run]").forEach((button) => button.addEventListener("click", openRunModal));
|
|
15
|
+
$("#closeRunModalBtn").addEventListener("click", closeRunModal);
|
|
16
|
+
$("#cancelRunBtn").addEventListener("click", closeRunModal);
|
|
17
|
+
$("#runModal").addEventListener("click", (event) => {
|
|
18
|
+
if (event.target === $("#runModal")) closeRunModal();
|
|
19
|
+
});
|
|
20
|
+
$("#runProviderPicker").addEventListener("click", (event) => {
|
|
21
|
+
const button = event.target.closest("[data-run-provider]");
|
|
22
|
+
if (!button || button.disabled) return;
|
|
23
|
+
state.runProvider = button.dataset.runProvider;
|
|
24
|
+
$("#runProviderPicker").innerHTML = providerPickerHtml();
|
|
25
|
+
syncRunComposer();
|
|
26
|
+
});
|
|
27
|
+
$("#runProviderHelp").addEventListener("click", async (event) => {
|
|
28
|
+
const docs = event.target.closest("[data-provider-docs]");
|
|
29
|
+
if (docs) {
|
|
30
|
+
const provider = providerInfo(docs.dataset.providerDocs);
|
|
31
|
+
if (provider.docs)
|
|
32
|
+
await performUiAction(async () => {
|
|
33
|
+
const result = await window.loadtoagent.openExternal(provider.docs);
|
|
34
|
+
if (!result || result.ok === false) throw new Error("공식 설치 문서를 열지 못했습니다.");
|
|
35
|
+
}, "공식 설치 문서를 열지 못했습니다.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (event.target.closest("[data-provider-recheck]")) {
|
|
39
|
+
const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), "AI CLI 연결 상태를 확인하지 못했습니다.");
|
|
40
|
+
if (!nextAvailability) return;
|
|
41
|
+
state.availability = nextAvailability;
|
|
42
|
+
const installed = state.providers.find((provider) => state.availability[provider.id]);
|
|
43
|
+
if (installed) state.runProvider = installed.id;
|
|
44
|
+
$("#runProviderPicker").innerHTML = providerPickerHtml();
|
|
45
|
+
renderProviderRail();
|
|
46
|
+
syncRunComposer();
|
|
47
|
+
toast(installed ? `${installed.label} CLI를 찾았습니다. 이제 작업을 시작할 수 있어요.` : "아직 설치된 AI CLI를 찾지 못했습니다. 설치와 로그인을 확인해 주세요.");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
$("#runPrompt").addEventListener("input", syncRunComposer);
|
|
51
|
+
$(".run-prompt-examples").addEventListener("click", (event) => {
|
|
52
|
+
const example = event.target.closest("[data-run-prompt-example]");
|
|
53
|
+
if (!example) return;
|
|
54
|
+
const input = $("#runPrompt");
|
|
55
|
+
const text = example.dataset.runPromptExample;
|
|
56
|
+
if (!input.value.trim()) input.value = text;
|
|
57
|
+
else input.setRangeText(`${input.selectionStart ? "\n\n" : ""}${text}`, input.selectionStart, input.selectionEnd, "end");
|
|
58
|
+
syncRunComposer();
|
|
59
|
+
input.focus();
|
|
60
|
+
});
|
|
61
|
+
$("#runWorkspaceSuggestions").addEventListener("click", (event) => {
|
|
62
|
+
const workspace = event.target.closest("[data-run-workspace]");
|
|
63
|
+
if (!workspace) return;
|
|
64
|
+
$("#runCwd").value = workspace.dataset.runWorkspace;
|
|
65
|
+
syncRunComposer();
|
|
66
|
+
});
|
|
67
|
+
$("#runCwd").addEventListener("input", syncRunComposer);
|
|
68
|
+
$("#allowWrites").addEventListener("change", syncRunComposer);
|
|
69
|
+
$("#pickRunCwdBtn").addEventListener("click", async () => {
|
|
70
|
+
const folder = await performUiAction(() => window.loadtoagent.pickWorkspace(), "작업 폴더를 선택하지 못했습니다.");
|
|
71
|
+
if (folder) {
|
|
72
|
+
$("#runCwd").value = folder;
|
|
73
|
+
syncRunComposer();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
$("#runForm").addEventListener("keydown", (event) => {
|
|
77
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
$("#runForm").requestSubmit();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
$("#runForm").addEventListener("submit", handleRun);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function bindDrawerAndGlobalEvents() {
|
|
86
|
+
$("#closeDrawerBtn").addEventListener("click", closeDrawer);
|
|
87
|
+
$("#drawerBackdrop").addEventListener("click", closeDrawer);
|
|
88
|
+
$(".drawer-tabs").addEventListener("click", (event) => {
|
|
89
|
+
const tab = event.target.closest("[data-tab]");
|
|
90
|
+
if (tab) {
|
|
91
|
+
state.drawerTab = tab.dataset.tab;
|
|
92
|
+
state.drawerForceLatest = tab.dataset.tab === "chat";
|
|
93
|
+
renderDrawer();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
$(".drawer-tabs").addEventListener("keydown", (event) => {
|
|
97
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
|
98
|
+
const tabs = $$(".drawer-tab:not(.hidden)");
|
|
99
|
+
const current = Math.max(0, tabs.indexOf(event.target.closest(".drawer-tab")));
|
|
100
|
+
const next =
|
|
101
|
+
event.key === "Home"
|
|
102
|
+
? 0
|
|
103
|
+
: event.key === "End"
|
|
104
|
+
? tabs.length - 1
|
|
105
|
+
: (current + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
state.drawerTab = tabs[next].dataset.tab;
|
|
108
|
+
state.drawerForceLatest = state.drawerTab === "chat";
|
|
109
|
+
renderDrawer();
|
|
110
|
+
$(`.drawer-tab[data-tab="${state.drawerTab}"]`)?.focus();
|
|
111
|
+
});
|
|
112
|
+
$("#detailDrawer").addEventListener("click", async (event) => {
|
|
113
|
+
const resume = event.target.closest("[data-resume-agent]");
|
|
114
|
+
if (resume) {
|
|
115
|
+
await resumeAgentTerminal(resume.dataset.resumeAgent);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const retry = event.target.closest("[data-retry-detail]");
|
|
119
|
+
if (retry) {
|
|
120
|
+
loadSessionDetail(retry.dataset.retryDetail, true);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const latest = event.target.closest("[data-scroll-latest]");
|
|
124
|
+
if (latest) {
|
|
125
|
+
const content = $("#drawerContent");
|
|
126
|
+
content.scrollTo({ top: content.scrollHeight, behavior: "smooth" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const stop = event.target.closest("[data-stop-run]");
|
|
130
|
+
if (!stop) return;
|
|
131
|
+
const runId = stop.dataset.stopRun;
|
|
132
|
+
if (state.stopRequests.has(runId)) return;
|
|
133
|
+
state.stopRequests.add(runId);
|
|
134
|
+
renderDrawer();
|
|
135
|
+
try {
|
|
136
|
+
const result = await window.loadtoagent.stopAgent(runId);
|
|
137
|
+
toast(result.ok ? window.LoadToAgentI18n.t("ui.stop_request_sent") : result.error);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_send_the_stop_request"));
|
|
140
|
+
} finally {
|
|
141
|
+
state.stopRequests.delete(runId);
|
|
142
|
+
if (state.selectedId) renderDrawer();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
document.addEventListener("keydown", (event) => {
|
|
146
|
+
trapDialogFocus(event);
|
|
147
|
+
if (event.key.toLowerCase() === "n" && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey) {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
if ($("#runModal").classList.contains("hidden")) openRunModal();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (event.key !== "Escape") return;
|
|
153
|
+
if (!$("#mobileToolsMenu").classList.contains("hidden")) {
|
|
154
|
+
$("#mobileToolsMenu").classList.add("hidden");
|
|
155
|
+
$("#mobileMoreBtn").setAttribute("aria-expanded", "false");
|
|
156
|
+
$("#mobileMoreBtn").focus();
|
|
157
|
+
} else if (!$("#runModal").classList.contains("hidden")) closeRunModal();
|
|
158
|
+
else closeDrawer();
|
|
159
|
+
});
|
|
160
|
+
window.addEventListener("resize", scheduleAgentWorkflowConnections);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function bindDialogAndGlobalEvents() {
|
|
164
|
+
bindRunComposerEvents();
|
|
165
|
+
bindDrawerAndGlobalEvents();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { bindDialogAndGlobalEvents };
|
|
169
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createFilterEventBindings = function createFilterEventBindings(context = {}) {
|
|
6
|
+
const { $, state, renderSessions, render, renderWorkspaces, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, performUiAction, toast } = context;
|
|
7
|
+
|
|
8
|
+
function bindFilterAndWorkspaceEvents() {
|
|
9
|
+
$("#loadMoreBtn").addEventListener("click", () => {
|
|
10
|
+
state.visibleLimit += 30;
|
|
11
|
+
renderSessions("load-more");
|
|
12
|
+
});
|
|
13
|
+
$("#workspaceList").addEventListener("click", async (event) => {
|
|
14
|
+
const remove = event.target.closest("[data-remove-workspace]");
|
|
15
|
+
if (remove) {
|
|
16
|
+
event.stopPropagation();
|
|
17
|
+
const workspaces = await performUiAction(() => window.loadtoagent.removeWorkspace(remove.dataset.removeWorkspace), "작업 폴더를 제거하지 못했습니다.");
|
|
18
|
+
if (!workspaces) return;
|
|
19
|
+
state.workspaces = workspaces;
|
|
20
|
+
if (state.workspace === remove.dataset.removeWorkspace) state.workspace = "all";
|
|
21
|
+
render();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const item = event.target.closest("[data-workspace]");
|
|
25
|
+
if (item) {
|
|
26
|
+
state.workspace = item.dataset.workspace;
|
|
27
|
+
state.visibleLimit = 30;
|
|
28
|
+
renderWorkspaces();
|
|
29
|
+
renderSessions("filter");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
let searchTimer = null;
|
|
33
|
+
$("#searchInput").addEventListener("input", (event) => {
|
|
34
|
+
clearTimeout(searchTimer);
|
|
35
|
+
const value = event.target.value;
|
|
36
|
+
searchTimer = setTimeout(() => {
|
|
37
|
+
state.search = value;
|
|
38
|
+
state.visibleLimit = 30;
|
|
39
|
+
renderSessions("filter");
|
|
40
|
+
}, 120);
|
|
41
|
+
});
|
|
42
|
+
$("#providerFilter").addEventListener("click", (event) => {
|
|
43
|
+
const chip = event.target.closest("[data-provider-filter]");
|
|
44
|
+
if (!chip) return;
|
|
45
|
+
toggleProviderFilter(chip.dataset.providerFilter);
|
|
46
|
+
state.visibleLimit = 30;
|
|
47
|
+
renderProviderFilter();
|
|
48
|
+
renderProviderOverview();
|
|
49
|
+
renderSessions("filter");
|
|
50
|
+
announceProviderFilter();
|
|
51
|
+
requestAnimationFrame(() => $("#providerFilter").querySelector(`[data-provider-filter="${CSS.escape(chip.dataset.providerFilter)}"]`)?.focus());
|
|
52
|
+
});
|
|
53
|
+
$("#sortSelect").addEventListener("change", (event) => {
|
|
54
|
+
state.sort = event.target.value;
|
|
55
|
+
state.visibleLimit = 30;
|
|
56
|
+
renderSessions("filter");
|
|
57
|
+
});
|
|
58
|
+
$("#addWorkspaceBtn").addEventListener("click", async () => {
|
|
59
|
+
const workspaces = await performUiAction(() => window.loadtoagent.addWorkspaces(), "작업 폴더를 추가하지 못했습니다.");
|
|
60
|
+
if (!workspaces) return;
|
|
61
|
+
state.workspaces = workspaces;
|
|
62
|
+
renderWorkspaces();
|
|
63
|
+
});
|
|
64
|
+
$("#probeBtn").addEventListener("click", async () => {
|
|
65
|
+
const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), "AI CLI 연결 상태를 확인하지 못했습니다.");
|
|
66
|
+
if (!nextAvailability) return;
|
|
67
|
+
state.availability = nextAvailability;
|
|
68
|
+
render();
|
|
69
|
+
toast(window.LoadToAgentI18n.t("ui.ai_cli_connections_were_checked_again"));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { bindFilterAndWorkspaceEvents };
|
|
74
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createNavigationEventBindings = function createNavigationEventBindings(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$, state, motionPreference, saveGuideState, selectView, renderUpdateSettings,
|
|
8
|
+
filteredSessions, renderSessions, openRunModal, openDrawer, toast,
|
|
9
|
+
} = context;
|
|
10
|
+
|
|
11
|
+
function bindNavigationAndUpdateEvents() {
|
|
12
|
+
$(".view-nav").addEventListener("click", (event) => {
|
|
13
|
+
const button = event.target.closest(".nav-item");
|
|
14
|
+
if (!button || !button.dataset.view) return;
|
|
15
|
+
selectView(button.dataset.view);
|
|
16
|
+
});
|
|
17
|
+
$("#updateNoticeBtn").addEventListener("click", () => {
|
|
18
|
+
selectView("settings");
|
|
19
|
+
});
|
|
20
|
+
$("#guideBtn").addEventListener("click", () => {
|
|
21
|
+
state.guideExpanded = !state.guideExpanded || state.view !== "all";
|
|
22
|
+
saveGuideState();
|
|
23
|
+
if (state.view !== "all") selectView("all");
|
|
24
|
+
else renderSessions("guide");
|
|
25
|
+
if (state.guideExpanded) {
|
|
26
|
+
setTimeout(
|
|
27
|
+
() => $("#beginnerGuide").scrollIntoView({ behavior: motionPreference.matches ? "auto" : "smooth", block: "start" }),
|
|
28
|
+
0,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
$("#dismissGuideBtn").addEventListener("click", () => {
|
|
33
|
+
state.guideExpanded = false;
|
|
34
|
+
saveGuideState();
|
|
35
|
+
renderSessions("guide");
|
|
36
|
+
$("#guideBtn").focus();
|
|
37
|
+
});
|
|
38
|
+
$("#beginnerGuide").addEventListener("click", (event) => {
|
|
39
|
+
const action = event.target.closest("[data-guide-action]")?.dataset.guideAction;
|
|
40
|
+
if (!action) return;
|
|
41
|
+
if (action === "create") return openRunModal();
|
|
42
|
+
if (action === "active" || action === "waiting") return selectView(action, { focusMain: true });
|
|
43
|
+
if (action === "detail") {
|
|
44
|
+
const first = filteredSessions()[0] || ((state.snapshot && state.snapshot.sessions) || [])[0];
|
|
45
|
+
if (first) openDrawer(first.id);
|
|
46
|
+
else {
|
|
47
|
+
toast("아직 열어볼 작업이 없어요. 먼저 AI에게 첫 일을 맡겨보세요.");
|
|
48
|
+
openRunModal();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
$("#mobileMoreBtn").addEventListener("click", () => {
|
|
53
|
+
const menu = $("#mobileToolsMenu");
|
|
54
|
+
const opening = menu.classList.contains("hidden");
|
|
55
|
+
menu.classList.toggle("hidden", !opening);
|
|
56
|
+
$("#mobileMoreBtn").setAttribute("aria-expanded", opening ? "true" : "false");
|
|
57
|
+
if (opening) setTimeout(() => menu.querySelector("button")?.focus(), 0);
|
|
58
|
+
});
|
|
59
|
+
$("#mobileToolsMenu").addEventListener("click", (event) => {
|
|
60
|
+
const button = event.target.closest("[data-mobile-view]");
|
|
61
|
+
if (button) selectView(button.dataset.mobileView, { focusMain: true });
|
|
62
|
+
});
|
|
63
|
+
$("#checkUpdateBtn").addEventListener("click", async () => {
|
|
64
|
+
try {
|
|
65
|
+
state.update = { ...(state.update || {}), status: "checking", error: "" };
|
|
66
|
+
renderUpdateSettings();
|
|
67
|
+
state.update = await window.loadtoagent.checkForUpdate();
|
|
68
|
+
renderUpdateSettings();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_check_for_updates"));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
$("#installUpdateBtn").addEventListener("click", async () => {
|
|
74
|
+
try {
|
|
75
|
+
if (state.update && state.update.status === "downloaded") {
|
|
76
|
+
await window.loadtoagent.openDownloadedUpdate();
|
|
77
|
+
toast("설치 파일을 열었습니다. 화면의 안내에 따라 업데이트를 마무리하세요.");
|
|
78
|
+
} else {
|
|
79
|
+
state.update = await window.loadtoagent.downloadUpdate();
|
|
80
|
+
renderUpdateSettings();
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_prepare_the_update_file"));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
$("#openReleaseBtn").addEventListener("click", async () => {
|
|
87
|
+
try {
|
|
88
|
+
await window.loadtoagent.openUpdateRelease();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_open_the_github_release_page"));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { bindNavigationAndUpdateEvents };
|
|
96
|
+
};
|