loadtoagent 1.1.0 → 1.3.1
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 +7 -0
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/docs/PROVIDER-CONTRACTS.md +23 -1
- package/docs/UI-AUDIT-100.md +118 -0
- package/docs/UI-AUDIT-101-200.md +120 -0
- package/docs/UI-AUDIT-201-300.md +120 -0
- package/main.js +135 -38
- package/package.json +13 -4
- package/preload.js +6 -0
- package/renderer/app-agent-actions.js +125 -53
- package/renderer/app-bootstrap.js +54 -19
- package/renderer/app-dashboard.js +190 -55
- package/renderer/app-drawer-content.js +48 -44
- package/renderer/app-drawer-data.js +26 -13
- package/renderer/app-drawer.js +72 -54
- package/renderer/app-events-dialogs.js +146 -37
- package/renderer/app-events-filters.js +172 -13
- package/renderer/app-events-navigation.js +77 -30
- package/renderer/app-events-sessions.js +156 -9
- package/renderer/app-events.js +2 -1
- package/renderer/app-graph-model.js +20 -38
- package/renderer/app-graph-orchestration.js +15 -13
- package/renderer/app-graph-view.js +228 -113
- package/renderer/app-management.js +211 -0
- package/renderer/app-provider-visibility.js +124 -0
- package/renderer/app-quality.js +568 -0
- package/renderer/app-run-modal.js +103 -33
- package/renderer/app-runtime-overview.js +312 -0
- package/renderer/app-session-render.js +94 -37
- package/renderer/app-tmux-render.js +52 -44
- package/renderer/app.js +153 -75
- package/renderer/i18n-messages.js +833 -16
- package/renderer/i18n.js +104 -0
- package/renderer/index.html +145 -59
- package/renderer/shared.js +17 -0
- package/renderer/styles-agent-map.css +6 -0
- package/renderer/styles-cards.css +26 -0
- package/renderer/styles-components.css +86 -9
- package/renderer/styles-management.css +355 -0
- package/renderer/styles-overlays.css +8 -2
- package/renderer/styles-product.css +24 -16
- package/renderer/styles-quality.css +312 -0
- package/renderer/styles-readability.css +862 -0
- package/renderer/styles-responsive-product.css +84 -12
- package/renderer/styles-responsive-runtime.css +22 -14
- package/renderer/styles-responsive-shell.css +44 -48
- package/renderer/styles-responsive-workflows.css +37 -3
- package/renderer/styles-run-composer.css +5 -0
- package/renderer/styles-runtime-overview.css +285 -0
- package/renderer/styles-settings.css +160 -0
- package/renderer/styles-terminal.css +339 -2
- package/renderer/styles-workflow-map.css +362 -15
- package/renderer/styles-workflows.css +69 -2
- package/renderer/styles.css +27 -12
- package/renderer/terminal-agent.js +17 -13
- package/renderer/terminal-events.js +233 -40
- package/renderer/terminal-workbench.js +165 -77
- package/renderer/terminal.js +341 -34
- package/src/agentMonitor/claudeParser.js +96 -7
- package/src/agentMonitor/codexParser.js +59 -4
- package/src/agentMonitor/executionActivity.js +282 -0
- package/src/agentMonitor/genericParser.js +56 -13
- package/src/agentMonitor/hierarchy.js +17 -0
- package/src/agentMonitor/responseIntent.js +51 -0
- package/src/agentMonitor.js +21 -3
- package/src/agentRunner.js +66 -1
- package/src/attentionNotifier.js +14 -10
- package/src/automationMonitor.js +258 -0
- package/src/contracts.js +10 -1
- package/src/ipc/registerAgentIpc.js +9 -3
- package/src/ipc/registerAppIpc.js +4 -1
- package/src/ipc/registerTerminalIpc.js +12 -6
- package/src/macUpdateHelper.js +210 -0
- package/src/monitorWorker.js +65 -6
- package/src/platformPath.js +80 -0
- package/src/processMonitor.js +80 -22
- package/src/providerVisibilityStore.js +45 -0
- package/src/sessionIntelligence.js +247 -0
- package/src/terminalHost.js +381 -0
- package/src/terminalHostDaemon.js +60 -0
- package/src/terminalManager.js +158 -3
- package/src/updateInstaller.js +175 -0
|
@@ -3,34 +3,43 @@
|
|
|
3
3
|
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
4
|
|
|
5
5
|
window.LoadToAgentAppFactories.createRunModal = function createRunModal(context = {}) {
|
|
6
|
+
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
6
7
|
const {
|
|
7
8
|
$,
|
|
8
9
|
esc,
|
|
9
10
|
uiLocale,
|
|
10
11
|
state,
|
|
12
|
+
PROJECTLESS_WORKSPACE,
|
|
11
13
|
motionPreference,
|
|
12
14
|
motionState,
|
|
13
15
|
markGuideStep,
|
|
14
16
|
rememberDialogTrigger,
|
|
15
17
|
restoreDialogTrigger,
|
|
18
|
+
setDialogOpenState,
|
|
19
|
+
announce,
|
|
16
20
|
providerInfo,
|
|
17
21
|
providerStyle,
|
|
22
|
+
visibleProviders = () => state.providers,
|
|
23
|
+
isProviderVisible = () => true,
|
|
24
|
+
restoreRunDraft = () => {},
|
|
25
|
+
clearRunDraft = () => {},
|
|
18
26
|
} = context;
|
|
19
27
|
|
|
20
28
|
function providerPickerHtml() {
|
|
21
|
-
return
|
|
29
|
+
return visibleProviders()
|
|
22
30
|
.map((provider) => {
|
|
23
31
|
const installed = !!state.availability[provider.id];
|
|
24
32
|
const selected = state.runProvider === provider.id;
|
|
25
33
|
return `<button type="button" class="run-provider-option ${selected ? "selected" : ""}"
|
|
26
34
|
data-run-provider="${esc(provider.id)}"
|
|
27
35
|
style="${providerStyle(provider.id)}"
|
|
28
|
-
aria-
|
|
36
|
+
role="radio" aria-checked="${selected ? "true" : "false"}"
|
|
37
|
+
tabindex="${selected ? "0" : "-1"}"
|
|
29
38
|
${installed ? "" : "disabled"}>
|
|
30
39
|
<span class="provider-mini-mark">${esc(provider.mark)}</span>
|
|
31
40
|
<span class="run-provider-copy">
|
|
32
41
|
<b>${esc(provider.label)}</b>
|
|
33
|
-
<small>${esc(installed ?
|
|
42
|
+
<small>${esc(installed ? t("run.cli_found", { company: provider.company }) : t("ui.setup_required"))}</small>
|
|
34
43
|
</span>
|
|
35
44
|
<span class="run-provider-check" aria-hidden="true">✓</span>
|
|
36
45
|
</button>`;
|
|
@@ -39,26 +48,30 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function runProviderHelpHtml() {
|
|
42
|
-
|
|
51
|
+
if (!visibleProviders().length) return `<div class="run-provider-help-copy">
|
|
52
|
+
<b>${t("settings.providers.all_hidden_title")}</b>
|
|
53
|
+
<p>${t("settings.providers.all_hidden_description")}</p>
|
|
54
|
+
</div>`;
|
|
55
|
+
const available = visibleProviders().filter((provider) => state.availability[provider.id]);
|
|
43
56
|
if (available.length) return "";
|
|
44
|
-
const docs =
|
|
57
|
+
const docs = visibleProviders()
|
|
45
58
|
.map(
|
|
46
59
|
(provider) => `<button type="button" data-provider-docs="${esc(provider.id)}">
|
|
47
60
|
<span class="provider-mini-mark" style="${providerStyle(provider.id)}">${esc(provider.mark)}</span>
|
|
48
61
|
<span>
|
|
49
|
-
<b>${esc(
|
|
50
|
-
<small
|
|
62
|
+
<b>${esc(t("provider.install_guide", { provider: provider.label }))}</b>
|
|
63
|
+
<small>${esc(t("run.check_official_docs"))}</small>
|
|
51
64
|
</span>
|
|
52
65
|
<i aria-hidden="true">↗</i>
|
|
53
66
|
</button>`,
|
|
54
67
|
)
|
|
55
68
|
.join("");
|
|
56
69
|
return `<div class="run-provider-help-copy">
|
|
57
|
-
<b
|
|
58
|
-
<p
|
|
70
|
+
<b>${esc(t("run.prepare_cli"))}</b>
|
|
71
|
+
<p>${esc(t("run.prepare_cli_steps"))}</p>
|
|
59
72
|
</div>
|
|
60
73
|
<div class="run-provider-docs">${docs}</div>
|
|
61
|
-
<button type="button" class="provider-recheck" data-provider-recheck>↻
|
|
74
|
+
<button type="button" class="provider-recheck" data-provider-recheck>↻ ${esc(t("run.recheck_installation"))}</button>`;
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
function runWorkspaceSuggestionsHtml() {
|
|
@@ -68,7 +81,7 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
68
81
|
.map((workspace) => {
|
|
69
82
|
const path = workspace.path || workspace.name || "";
|
|
70
83
|
const active = path === selected;
|
|
71
|
-
return `<button type="button" data-run-workspace="${esc(path)}" class="${active ? "selected" : ""}" title="${esc(path)}">
|
|
84
|
+
return `<button type="button" data-run-workspace="${esc(path)}" class="${active ? "selected" : ""}" title="${esc(path)}" aria-pressed="${active ? "true" : "false"}">
|
|
72
85
|
<span aria-hidden="true">⌘</span>
|
|
73
86
|
${esc(workspace.name || path.split(/[\\/]/).filter(Boolean).pop() || window.LoadToAgentI18n.t("ui.workspaces"))}
|
|
74
87
|
</button>`;
|
|
@@ -79,23 +92,30 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
79
92
|
function syncRunComposer() {
|
|
80
93
|
const prompt = $("#runPrompt");
|
|
81
94
|
const count = $("#runPromptCount");
|
|
82
|
-
if (prompt && count)
|
|
95
|
+
if (prompt && count) {
|
|
96
|
+
const wasWarning = count.dataset.warning === "true";
|
|
97
|
+
const warning = prompt.value.length >= 7_200;
|
|
98
|
+
count.textContent = `${prompt.value.length.toLocaleString(uiLocale())} / 8,000`;
|
|
99
|
+
count.classList.toggle("warning", warning);
|
|
100
|
+
count.dataset.warning = warning ? "true" : "false";
|
|
101
|
+
if (warning && !wasWarning) announce(t("run.prompt_near_limit", { count: Math.max(0, 8_000 - prompt.value.length) }));
|
|
102
|
+
}
|
|
83
103
|
const submitLabel = $("#runSubmitLabel");
|
|
84
104
|
const submit = $('#runForm button[type="submit"]');
|
|
85
|
-
const hasProvider = Boolean(state.availability[state.runProvider]);
|
|
105
|
+
const hasProvider = isProviderVisible(state.runProvider) && Boolean(state.availability[state.runProvider]);
|
|
86
106
|
const providerHelp = $("#runProviderHelp");
|
|
87
107
|
if (providerHelp) {
|
|
88
108
|
providerHelp.innerHTML = runProviderHelpHtml();
|
|
89
109
|
providerHelp.classList.toggle(
|
|
90
110
|
"hidden",
|
|
91
|
-
|
|
111
|
+
visibleProviders().some((provider) => state.availability[provider.id]),
|
|
92
112
|
);
|
|
93
113
|
}
|
|
94
114
|
if (submit) submit.disabled = submit.dataset.submitting === "true" || !hasProvider;
|
|
95
115
|
if (submitLabel && submit.dataset.submitting !== "true")
|
|
96
116
|
submitLabel.textContent = hasProvider
|
|
97
|
-
?
|
|
98
|
-
: "
|
|
117
|
+
? t("provider.assign", { provider: providerInfo(state.runProvider).label })
|
|
118
|
+
: visibleProviders().length ? t("run.ai_installation_required") : t("settings.providers.enable_to_run");
|
|
99
119
|
const writeIntent = /(고치|수정|추가|구현|변경|삭제|작성|리팩터|fix|implement|update|edit|refactor)/i.test((prompt && prompt.value) || "");
|
|
100
120
|
const permissionHint = $("#runPermissionHint");
|
|
101
121
|
const permissionNeeded = writeIntent && !$("#allowWrites").checked;
|
|
@@ -110,40 +130,57 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
110
130
|
const submit = $('#runForm button[type="submit"]');
|
|
111
131
|
if (!submit) return;
|
|
112
132
|
submit.dataset.submitting = submitting ? "true" : "false";
|
|
113
|
-
submit.disabled = submitting || !state.availability[state.runProvider];
|
|
133
|
+
submit.disabled = submitting || !isProviderVisible(state.runProvider) || !state.availability[state.runProvider];
|
|
114
134
|
submit.setAttribute("aria-busy", submitting ? "true" : "false");
|
|
135
|
+
$("#closeRunModalBtn").disabled = submitting;
|
|
136
|
+
$("#cancelRunBtn").disabled = submitting;
|
|
115
137
|
const label = $("#runSubmitLabel");
|
|
116
138
|
if (label) label.textContent = submitting
|
|
117
|
-
? "
|
|
118
|
-
:
|
|
139
|
+
? t("run.preparing")
|
|
140
|
+
: t("provider.assign", { provider: providerInfo(state.runProvider).label });
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
function openRunModal() {
|
|
122
144
|
rememberDialogTrigger();
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
restoreRunDraft();
|
|
146
|
+
const installed = visibleProviders().find((provider) => state.availability[provider.id]);
|
|
147
|
+
if ((!isProviderVisible(state.runProvider) || !state.availability[state.runProvider]) && installed) state.runProvider = installed.id;
|
|
148
|
+
if (!isProviderVisible(state.runProvider)) state.runProvider = visibleProviders()[0]?.id || "";
|
|
125
149
|
$("#runProviderPicker").innerHTML = providerPickerHtml();
|
|
126
|
-
if (!$("#runCwd").value) $("#runCwd").value = state.workspace !== "all"
|
|
150
|
+
if (!$("#runCwd").value) $("#runCwd").value = state.workspace !== "all" && state.workspace !== PROJECTLESS_WORKSPACE
|
|
151
|
+
? state.workspace
|
|
152
|
+
: state.workspace === PROJECTLESS_WORKSPACE ? "" : (state.workspaces[0] && state.workspaces[0].path) || "";
|
|
127
153
|
$("#runCwd").placeholder = state.platform.id === "win32" ? "D:\\project" : "/Users/me/project";
|
|
128
|
-
const advanced = $("#runForm .run-advanced");
|
|
129
|
-
if (advanced) advanced.open = Boolean($("#runModel").value.trim());
|
|
130
154
|
$("#runError").classList.add("hidden");
|
|
131
155
|
syncRunComposer();
|
|
132
156
|
clearTimeout(motionState.modalTimer);
|
|
157
|
+
clearTimeout(motionState.modalFocusTimer);
|
|
158
|
+
setDialogOpenState($("#runModal"), true);
|
|
133
159
|
$("#runModal").classList.remove("hidden", "closing");
|
|
134
|
-
|
|
160
|
+
const focusPromptIfOutside = () => {
|
|
161
|
+
const modal = $("#runModal");
|
|
162
|
+
if (!modal.classList.contains("hidden") && !modal.classList.contains("closing") && !modal.contains(document.activeElement)) {
|
|
163
|
+
$("#runPrompt").focus();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
setTimeout(focusPromptIfOutside, 0);
|
|
167
|
+
motionState.modalFocusTimer = setTimeout(focusPromptIfOutside, motionPreference.matches ? 0 : 300);
|
|
135
168
|
}
|
|
136
169
|
|
|
137
|
-
function closeRunModal() {
|
|
170
|
+
function closeRunModal(force = false) {
|
|
138
171
|
const modal = $("#runModal");
|
|
139
172
|
if (modal.classList.contains("hidden") || modal.classList.contains("closing")) return;
|
|
173
|
+
if (force !== true && $('#runForm button[type="submit"]').dataset.submitting === "true") return;
|
|
174
|
+
const modalGeneration = motionState.dialogGeneration;
|
|
175
|
+
clearTimeout(motionState.modalFocusTimer);
|
|
140
176
|
modal.classList.add("closing");
|
|
141
177
|
clearTimeout(motionState.modalTimer);
|
|
142
178
|
motionState.modalTimer = setTimeout(
|
|
143
179
|
() => {
|
|
144
180
|
modal.classList.add("hidden");
|
|
145
181
|
modal.classList.remove("closing");
|
|
146
|
-
|
|
182
|
+
setDialogOpenState(modal, false);
|
|
183
|
+
restoreDialogTrigger(modalGeneration);
|
|
147
184
|
},
|
|
148
185
|
motionPreference.matches ? 0 : 220,
|
|
149
186
|
);
|
|
@@ -166,20 +203,52 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
166
203
|
}, 3200);
|
|
167
204
|
}
|
|
168
205
|
|
|
169
|
-
async function performUiAction(action, failureMessage) {
|
|
206
|
+
async function performUiAction(action, failureMessage, control = null) {
|
|
207
|
+
if (control?.dataset.busy === "true") return null;
|
|
208
|
+
const wasDisabled = Boolean(control?.disabled);
|
|
209
|
+
if (control) {
|
|
210
|
+
control.dataset.busy = "true";
|
|
211
|
+
control.disabled = true;
|
|
212
|
+
control.setAttribute("aria-busy", "true");
|
|
213
|
+
}
|
|
170
214
|
try {
|
|
171
215
|
return await action();
|
|
172
216
|
} catch (error) {
|
|
173
|
-
toast((error
|
|
217
|
+
toast(window.LoadToAgentI18n.errorText(error, failureMessage));
|
|
174
218
|
return null;
|
|
219
|
+
} finally {
|
|
220
|
+
if (control?.isConnected) {
|
|
221
|
+
delete control.dataset.busy;
|
|
222
|
+
control.disabled = wasDisabled;
|
|
223
|
+
control.removeAttribute("aria-busy");
|
|
224
|
+
}
|
|
175
225
|
}
|
|
176
226
|
}
|
|
177
227
|
|
|
178
228
|
async function handleRun(event) {
|
|
179
229
|
event.preventDefault();
|
|
180
|
-
|
|
230
|
+
const prompt = $("#runPrompt");
|
|
231
|
+
const cwd = $("#runCwd");
|
|
232
|
+
const invalid = [];
|
|
233
|
+
if (!prompt.value.trim()) {
|
|
234
|
+
prompt.setAttribute("aria-invalid", "true");
|
|
235
|
+
invalid.push({ element: prompt, message: t("quality.run_prompt_required") });
|
|
236
|
+
}
|
|
237
|
+
if (!cwd.value.trim()) {
|
|
238
|
+
cwd.setAttribute("aria-invalid", "true");
|
|
239
|
+
invalid.push({ element: cwd, message: t("quality.run_cwd_required") });
|
|
240
|
+
}
|
|
241
|
+
if (invalid.length) {
|
|
242
|
+
$("#runError").textContent = invalid[0].message;
|
|
243
|
+
$("#runError").classList.remove("hidden");
|
|
244
|
+
invalid[0].element.focus({ preventScroll: true });
|
|
245
|
+
announce(invalid[0].message);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!isProviderVisible(state.runProvider) || !state.availability[state.runProvider]) {
|
|
181
249
|
$("#runError").textContent = window.LoadToAgentI18n.t("ui.no_ai_cli_is_ready_follow_the_official_setup_guide");
|
|
182
250
|
$("#runError").classList.remove("hidden");
|
|
251
|
+
$("#runError").focus({ preventScroll: true });
|
|
183
252
|
return;
|
|
184
253
|
}
|
|
185
254
|
setRunSubmitting(true);
|
|
@@ -194,13 +263,14 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
|
|
|
194
263
|
});
|
|
195
264
|
if (!result.ok) throw new Error(result.error || window.LoadToAgentI18n.t("ui.could_not_start_the_task"));
|
|
196
265
|
markGuideStep("create");
|
|
197
|
-
closeRunModal();
|
|
198
|
-
|
|
266
|
+
closeRunModal(true);
|
|
267
|
+
clearRunDraft({ silent: true, focus: false });
|
|
199
268
|
syncRunComposer();
|
|
200
269
|
toast(window.LoadToAgentI18n.t("provider.started", { provider: providerInfo(state.runProvider).label }));
|
|
201
270
|
} catch (error) {
|
|
202
|
-
$("#runError").textContent = error.
|
|
271
|
+
$("#runError").textContent = window.LoadToAgentI18n.errorText(error, "ui.could_not_start_the_task");
|
|
203
272
|
$("#runError").classList.remove("hidden");
|
|
273
|
+
$("#runError").focus({ preventScroll: true });
|
|
204
274
|
} finally {
|
|
205
275
|
setRunSubmitting(false);
|
|
206
276
|
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createRuntimeOverview = function createRuntimeOverview(context = {}) {
|
|
6
|
+
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
7
|
+
const {
|
|
8
|
+
$,
|
|
9
|
+
esc,
|
|
10
|
+
uiLocale,
|
|
11
|
+
state,
|
|
12
|
+
providerInfo,
|
|
13
|
+
providerStyle,
|
|
14
|
+
currentActivity,
|
|
15
|
+
visibleSessions = () => ((state.snapshot && state.snapshot.sessions) || []),
|
|
16
|
+
isProviderVisible = () => true,
|
|
17
|
+
isRuntimeLoopSession = () => false,
|
|
18
|
+
} = context;
|
|
19
|
+
|
|
20
|
+
let runtimeTicker = 0;
|
|
21
|
+
let runtimeRenderVersion = 0;
|
|
22
|
+
let pendingRuntimeFocus = null;
|
|
23
|
+
|
|
24
|
+
function activeRootLoops() {
|
|
25
|
+
return visibleSessions()
|
|
26
|
+
.filter(isRuntimeLoopSession)
|
|
27
|
+
.sort((a, b) => Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function visibleAutomations() {
|
|
31
|
+
return ((state.snapshot && state.snapshot.automations) || [])
|
|
32
|
+
.filter((item) => isProviderVisible(item.provider || "codex"))
|
|
33
|
+
.sort((a, b) => Date.parse(a.nextRunAt || 0) - Date.parse(b.nextRunAt || 0));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function automationSession(item) {
|
|
37
|
+
if (!item || !item.targetThreadId) return null;
|
|
38
|
+
return visibleSessions().find((session) => session.externalId === item.targetThreadId || session.id === item.targetThreadId) || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scheduleTime(value) {
|
|
42
|
+
const date = new Date(value);
|
|
43
|
+
if (Number.isNaN(date.getTime())) return t("runtime.not_scheduled");
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
46
|
+
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
47
|
+
const day = Math.round((target.getTime() - start.getTime()) / 86_400_000);
|
|
48
|
+
const time = date.toLocaleTimeString(uiLocale(), { hour: "2-digit", minute: "2-digit" });
|
|
49
|
+
if (day === 0) return t("runtime.today_at", { time });
|
|
50
|
+
if (day === 1) return t("runtime.tomorrow_at", { time });
|
|
51
|
+
return date.toLocaleString(uiLocale(), { month: "short", day: "numeric", weekday: "short", hour: "2-digit", minute: "2-digit" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scheduleRule(value) {
|
|
55
|
+
const rule = Object.fromEntries(String(value || "").split(";").map((pair) => pair.split("=", 2)).filter((pair) => pair.length === 2));
|
|
56
|
+
const interval = Math.max(1, Number.parseInt(rule.INTERVAL || "1", 10) || 1);
|
|
57
|
+
const frequencyKeys = {
|
|
58
|
+
DAILY: "runtime.every_day",
|
|
59
|
+
WEEKLY: "runtime.every_week",
|
|
60
|
+
HOURLY: "runtime.every_hour",
|
|
61
|
+
MINUTELY: "runtime.every_minute",
|
|
62
|
+
};
|
|
63
|
+
const intervalKeys = {
|
|
64
|
+
DAILY: "runtime.every_n_days",
|
|
65
|
+
WEEKLY: "runtime.every_n_weeks",
|
|
66
|
+
HOURLY: "runtime.every_n_hours",
|
|
67
|
+
MINUTELY: "runtime.every_n_minutes",
|
|
68
|
+
};
|
|
69
|
+
const base = interval > 1 && intervalKeys[rule.FREQ]
|
|
70
|
+
? t(intervalKeys[rule.FREQ], { count: interval })
|
|
71
|
+
: frequencyKeys[rule.FREQ] ? t(frequencyKeys[rule.FREQ]) : t("runtime.recurring");
|
|
72
|
+
const details = [];
|
|
73
|
+
if (rule.BYDAY) {
|
|
74
|
+
const weekdayDates = { SU: 4, MO: 5, TU: 6, WE: 7, TH: 8, FR: 9, SA: 10 };
|
|
75
|
+
const days = rule.BYDAY.split(",")
|
|
76
|
+
.map((day) => weekdayDates[day])
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map((day) => new Intl.DateTimeFormat(uiLocale(), { weekday: "short" }).format(new Date(2026, 0, day)));
|
|
79
|
+
if (days.length) details.push(days.join("·"));
|
|
80
|
+
}
|
|
81
|
+
if (rule.BYHOUR) {
|
|
82
|
+
const hour = String(rule.BYHOUR).padStart(2, "0");
|
|
83
|
+
const minute = String(rule.BYMINUTE || "0").padStart(2, "0");
|
|
84
|
+
details.push(`${hour}:${minute}`);
|
|
85
|
+
} else if (rule.FREQ === "HOURLY" && rule.BYMINUTE) {
|
|
86
|
+
details.push(t("runtime.at_minute", { minute: Number.parseInt(rule.BYMINUTE, 10) || 0 }));
|
|
87
|
+
}
|
|
88
|
+
return [base, ...details].join(" · ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loopPhase(session) {
|
|
92
|
+
const explicitPhase = String(session.loop && session.loop.phase || "").toLowerCase();
|
|
93
|
+
const explicitIndex = { input: 0, decide: 1, decision: 1, act: 2, action: 2, observe: 3, observation: 3 }[explicitPhase];
|
|
94
|
+
if (Number.isInteger(explicitIndex)) return explicitIndex;
|
|
95
|
+
const event = [...(session.lifecycle || [])].reverse().find((item) => item.status === "running")
|
|
96
|
+
|| (session.lifecycle || [])[session.lifecycle.length - 1]
|
|
97
|
+
|| {};
|
|
98
|
+
const signal = `${event.type || ""} ${event.label || ""}`.toLowerCase();
|
|
99
|
+
if (/result|output|wait|complete|observe|검수|확인/.test(signal)) return 3;
|
|
100
|
+
if (/tool|collaboration|command|exec|실행|도구/.test(signal)) return 2;
|
|
101
|
+
if (/reason|think|decid|추론|판단/.test(signal)) return 1;
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loopPhases(session) {
|
|
106
|
+
const active = loopPhase(session);
|
|
107
|
+
const definitions = [
|
|
108
|
+
["input", "runtime.phase_input", "runtime.phase_input_detail"],
|
|
109
|
+
["decide", "runtime.phase_decide", "runtime.phase_decide_detail"],
|
|
110
|
+
["act", "runtime.phase_act", "runtime.phase_act_detail"],
|
|
111
|
+
["observe", "runtime.phase_observe", "runtime.phase_observe_detail"],
|
|
112
|
+
];
|
|
113
|
+
return definitions.map(([key, label, detail], index) => ({
|
|
114
|
+
key,
|
|
115
|
+
label: t(label),
|
|
116
|
+
detail: t(detail),
|
|
117
|
+
state: index < active ? "done" : index === active ? "active" : "queued",
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function phaseStatusLabel(stateValue) {
|
|
122
|
+
return t({ done: "runtime.phase_done", active: "runtime.phase_active", queued: "runtime.phase_queued" }[stateValue] || "runtime.phase_queued");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function elapsedSince(value) {
|
|
126
|
+
if (!value) return t("runtime.just_started");
|
|
127
|
+
const elapsed = Date.now() - Date.parse(value || 0);
|
|
128
|
+
if (!Number.isFinite(elapsed) || elapsed < 60_000) return t("runtime.just_started");
|
|
129
|
+
const minutes = Math.floor(elapsed / 60_000);
|
|
130
|
+
if (minutes < 60) return t("runtime.elapsed_minutes", { count: minutes });
|
|
131
|
+
return t("runtime.elapsed_hours", { count: Math.floor(minutes / 60) });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scheduleCard(item) {
|
|
135
|
+
const session = automationSession(item);
|
|
136
|
+
const cwd = (item.cwds || [])[0] || "";
|
|
137
|
+
const workspace = String(cwd).replace(/\\/g, "/").split("/").filter(Boolean).pop();
|
|
138
|
+
const location = workspace || (item.environment?.kind === "wsl" && item.sourceLabel) || t("runtime.workspace_unspecified");
|
|
139
|
+
const body = `<span class="runtime-schedule-time" ${item.enabled ? `data-runtime-next-run-at="${esc(item.nextRunAt || "")}"` : ""}>${esc(item.enabled ? scheduleTime(item.nextRunAt) : t("runtime.paused"))}</span>
|
|
140
|
+
<strong>${esc(item.name)}</strong>
|
|
141
|
+
<small>${esc(scheduleRule(item.rrule))} · ${esc(location)}</small>`;
|
|
142
|
+
const card = session
|
|
143
|
+
? `<button type="button" class="runtime-schedule-card ${item.enabled ? "" : "paused"}" data-automation-id="${esc(item.id)}" data-automation-enabled="${item.enabled ? "true" : "false"}" data-automation-session="${esc(session.id)}">${body}<i aria-hidden="true">↗</i></button>`
|
|
144
|
+
: `<article class="runtime-schedule-card ${item.enabled ? "" : "paused"}" data-automation-id="${esc(item.id)}" data-automation-enabled="${item.enabled ? "true" : "false"}">${body}<i aria-hidden="true">${item.enabled ? "●" : "Ⅱ"}</i></article>`;
|
|
145
|
+
return `<div class="runtime-schedule-item" role="listitem">${card}</div>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function emptySchedules() {
|
|
149
|
+
return `<div class="runtime-schedule-empty"><span aria-hidden="true">+</span><div><b>${esc(t("runtime.no_schedules"))}</b><small>${esc(t("runtime.no_schedules_detail"))}</small></div></div>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function loopSelector(loop, selected) {
|
|
153
|
+
const provider = providerInfo(loop.provider);
|
|
154
|
+
const activePhase = loopPhases(loop).find((phase) => phase.state === "active") || loopPhases(loop)[0];
|
|
155
|
+
return `<button type="button" class="runtime-loop-tab ${selected ? "selected" : ""}" data-loop-select="${esc(loop.id)}"
|
|
156
|
+
id="runtime-loop-tab-${esc(loop.id)}" role="tab" aria-controls="runtime-loop-panel-${esc(loop.id)}"
|
|
157
|
+
style="${providerStyle(loop.provider)}" aria-selected="${selected ? "true" : "false"}" aria-pressed="${selected ? "true" : "false"}" tabindex="${selected ? "0" : "-1"}">
|
|
158
|
+
<span class="runtime-loop-tab-mark">${esc(provider.mark)}</span>
|
|
159
|
+
<span><b>${esc(loop.title)}</b><small>${esc(provider.label)} · ${esc(activePhase.label)} · <span data-runtime-started-at="${esc(loop.startedAt || "")}">${esc(elapsedSince(loop.startedAt))}</span></small></span>
|
|
160
|
+
<i aria-hidden="true"></i>
|
|
161
|
+
</button>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function loopDiagram(session) {
|
|
165
|
+
const phases = loopPhases(session);
|
|
166
|
+
const activeIndex = Math.max(0, phases.findIndex((phase) => phase.state === "active"));
|
|
167
|
+
const activePhase = phases[activeIndex];
|
|
168
|
+
return `<div class="runtime-loop-cycle" role="img" aria-label="${esc(t("runtime.loop_flow_state", { phase: activePhase.label }))}" style="--loop-progress:${activeIndex / Math.max(1, phases.length - 1) * 100}%">
|
|
169
|
+
<div class="runtime-loop-spine" aria-hidden="true"><span></span></div>
|
|
170
|
+
${phases.map((phase, index) => `<div class="runtime-loop-phase ${phase.state}" data-loop-phase="${phase.key}">
|
|
171
|
+
<span class="runtime-loop-phase-index">0${index + 1}<em>${esc(phaseStatusLabel(phase.state))}</em></span>
|
|
172
|
+
<i aria-hidden="true">${phase.state === "done" ? "✓" : phase.state === "active" ? "●" : "·"}</i>
|
|
173
|
+
<b>${esc(phase.label)}</b>
|
|
174
|
+
<small>${esc(phase.detail)}</small>
|
|
175
|
+
</div>`).join("")}
|
|
176
|
+
<div class="runtime-loop-return" aria-hidden="true"><span>↺</span><b>${esc(t("runtime.phase_repeat"))}</b></div>
|
|
177
|
+
</div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function loopDetail(session) {
|
|
181
|
+
const provider = providerInfo(session.provider);
|
|
182
|
+
const activity = currentActivity(session);
|
|
183
|
+
const children = (session.childIds || []).map((id) => visibleSessions().find((item) => item.id === id)).filter(Boolean);
|
|
184
|
+
const runningChildren = children.filter((item) => ["running", "starting"].includes(item.status)).length;
|
|
185
|
+
const iteration = Number(session.loop && session.loop.iteration || 0);
|
|
186
|
+
const iterationLabel = iteration > 0
|
|
187
|
+
? t("runtime.iteration_value", { count: iteration })
|
|
188
|
+
: session.loop ? t("runtime.iteration_observed") : t("runtime.iteration_scheduled");
|
|
189
|
+
const activePhase = loopPhases(session).find((phase) => phase.state === "active") || loopPhases(session)[0];
|
|
190
|
+
const activityTitle = window.LoadToAgentI18n.observedText(activity.title);
|
|
191
|
+
const activityDetail = window.LoadToAgentI18n.observedText(activity.detail || session.statusDetail || "");
|
|
192
|
+
return `<article id="runtime-loop-panel-${esc(session.id)}" class="runtime-loop-detail" role="tabpanel" aria-labelledby="runtime-loop-tab-${esc(session.id)}" style="${providerStyle(session.provider)}" data-motion-key="runtime-loop:${esc(session.id)}" data-motion-value="${esc(session.updatedAt || "")}">
|
|
193
|
+
<header>
|
|
194
|
+
<div><span class="runtime-loop-kicker"><i></i>${esc(t("runtime.active_loop"))}</span><h3>${esc(session.title)}</h3><p>${esc(provider.label)} · ${esc(session.model || t("session.model_unknown"))}</p></div>
|
|
195
|
+
<div class="runtime-loop-header-actions"><span class="runtime-active-phase"><small>${esc(t("runtime.current_phase"))}</small><b>${esc(activePhase.label)}</b></span><button type="button" class="runtime-open-task" data-loop-open="${esc(session.id)}">${esc(t("runtime.open_task"))}<span aria-hidden="true">↗</span></button></div>
|
|
196
|
+
</header>
|
|
197
|
+
<section class="runtime-now-strip" aria-label="${esc(t("runtime.now_working"))}">
|
|
198
|
+
<span class="runtime-now-mark" aria-hidden="true">NOW</span>
|
|
199
|
+
<div><small>${esc(t("runtime.now_working"))}</small><b title="${esc(activityTitle)}">${esc(activityTitle)}</b><p title="${esc(activityDetail)}">${esc(activityDetail)}</p></div>
|
|
200
|
+
<time data-runtime-updated-at="${esc(session.updatedAt || session.startedAt || "")}">${esc(t("runtime.last_signal_time", { time: elapsedSince(session.updatedAt || session.startedAt) }))}</time>
|
|
201
|
+
</section>
|
|
202
|
+
${loopDiagram(session)}
|
|
203
|
+
<footer class="runtime-loop-footer">
|
|
204
|
+
<dl>
|
|
205
|
+
<div><dt>${esc(t("runtime.running_time"))}</dt><dd data-runtime-started-at="${esc(session.startedAt || "")}">${esc(elapsedSince(session.startedAt))}</dd></div>
|
|
206
|
+
<div><dt>${esc(t("runtime.iteration"))}</dt><dd>${esc(iterationLabel)}</dd></div>
|
|
207
|
+
<div><dt>${esc(t("runtime.subagents"))}</dt><dd>${runningChildren ? esc(t("runtime.subagents_running", { running: runningChildren, total: children.length })) : esc(t("runtime.subagents_total", { count: children.length }))}</dd></div>
|
|
208
|
+
</dl>
|
|
209
|
+
</footer>
|
|
210
|
+
</article>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function noActiveLoop() {
|
|
214
|
+
return `<div class="runtime-loop-empty"><span class="runtime-loop-empty-orbit" aria-hidden="true"><i></i></span><div><b>${esc(t("runtime.no_active_loop"))}</b><p>${esc(t("runtime.no_active_loop_detail"))}</p></div></div>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function refreshRuntimeTimes(section = $("#automationOverview")) {
|
|
218
|
+
if (!section || section.classList.contains("hidden")) return;
|
|
219
|
+
section.querySelectorAll("[data-runtime-next-run-at]").forEach((element) => {
|
|
220
|
+
element.textContent = scheduleTime(element.dataset.runtimeNextRunAt);
|
|
221
|
+
});
|
|
222
|
+
section.querySelectorAll("[data-runtime-started-at]").forEach((element) => {
|
|
223
|
+
element.textContent = elapsedSince(element.dataset.runtimeStartedAt);
|
|
224
|
+
});
|
|
225
|
+
section.querySelectorAll("[data-runtime-updated-at]").forEach((element) => {
|
|
226
|
+
element.textContent = t("runtime.last_signal_time", { time: elapsedSince(element.dataset.runtimeUpdatedAt) });
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function ensureRuntimeTicker() {
|
|
231
|
+
if (runtimeTicker) return;
|
|
232
|
+
runtimeTicker = window.setInterval(() => refreshRuntimeTimes(), 30_000);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderRuntimeOverview() {
|
|
236
|
+
const renderVersion = ++runtimeRenderVersion;
|
|
237
|
+
const section = $("#automationOverview");
|
|
238
|
+
const previousScheduleList = section.querySelector(".runtime-schedule-list");
|
|
239
|
+
const previousLoopTabs = section.querySelector(".runtime-loop-tabs");
|
|
240
|
+
const previousSelectedId = section.querySelector(".runtime-loop-tab.selected")?.dataset.loopSelect || "";
|
|
241
|
+
const scheduleScrollTop = previousScheduleList?.scrollTop || 0;
|
|
242
|
+
const loopScrollLeft = previousLoopTabs?.scrollLeft || 0;
|
|
243
|
+
const restoreScheduleFocus = document.activeElement === previousScheduleList;
|
|
244
|
+
const focusedAutomationId = document.activeElement?.closest?.("[data-automation-id]")?.dataset.automationId || "";
|
|
245
|
+
const focusedLoopId = document.activeElement?.closest?.("[data-loop-select]")?.dataset.loopSelect || "";
|
|
246
|
+
const detectedFocus = restoreScheduleFocus
|
|
247
|
+
? { type: "schedule-list", id: "" }
|
|
248
|
+
: focusedAutomationId ? { type: "automation", id: focusedAutomationId }
|
|
249
|
+
: focusedLoopId ? { type: "loop", id: focusedLoopId } : null;
|
|
250
|
+
if (detectedFocus) pendingRuntimeFocus = detectedFocus;
|
|
251
|
+
const focusIntent = detectedFocus || pendingRuntimeFocus;
|
|
252
|
+
const automations = visibleAutomations();
|
|
253
|
+
const interactiveScheduleCount = automations.filter((item) => automationSession(item)).length;
|
|
254
|
+
const scheduleListFocusable = automations.length > 0 && interactiveScheduleCount === 0;
|
|
255
|
+
const scheduleListLabel = scheduleListFocusable
|
|
256
|
+
? t("runtime.schedule_list_scroll_label")
|
|
257
|
+
: interactiveScheduleCount > 0 ? t("runtime.schedule_list_action_label") : t("runtime.schedule_list_label");
|
|
258
|
+
const loops = activeRootLoops();
|
|
259
|
+
const enabled = automations.filter((item) => item.enabled);
|
|
260
|
+
const paused = automations.filter((item) => !item.enabled);
|
|
261
|
+
if (!loops.some((loop) => loop.id === state.selectedRuntimeLoopId)) state.selectedRuntimeLoopId = loops[0] && loops[0].id || null;
|
|
262
|
+
const selected = loops.find((loop) => loop.id === state.selectedRuntimeLoopId) || loops[0] || null;
|
|
263
|
+
const selectedId = selected?.id || "";
|
|
264
|
+
section.innerHTML = `<header class="runtime-overview-head">
|
|
265
|
+
<div class="runtime-overview-title"><span class="runtime-overview-emblem" aria-hidden="true"><i></i><b>↻</b></span><div><p>${esc(t("runtime.eyebrow"))}</p><h2>${esc(t("runtime.status_summary"))}</h2></div></div>
|
|
266
|
+
<div class="runtime-overview-counts"><span><i></i>${esc(t("runtime.schedules_count", { count: enabled.length }))}</span>${paused.length ? `<span class="paused"><i></i>${esc(t("runtime.paused_count", { count: paused.length }))}</span>` : ""}<span><i></i>${esc(t("runtime.loops_count", { count: loops.length }))}</span><b><i></i>${esc(t("runtime.live_status"))}</b></div>
|
|
267
|
+
</header>
|
|
268
|
+
<div class="runtime-overview-grid">
|
|
269
|
+
<aside class="runtime-schedule-lane">
|
|
270
|
+
<header><div><span>${esc(t("runtime.schedule_lane"))}</span><b>${esc(t("runtime.schedule_list"))}</b></div><em>${String(automations.length).padStart(2, "0")}</em></header>
|
|
271
|
+
<div class="runtime-schedule-list" role="list" tabindex="${scheduleListFocusable ? "0" : "-1"}" aria-label="${esc(scheduleListLabel)}">${automations.length ? automations.map(scheduleCard).join("") : emptySchedules()}</div>
|
|
272
|
+
</aside>
|
|
273
|
+
<section class="runtime-loop-lane" aria-label="${esc(t("runtime.loop_lane"))}">
|
|
274
|
+
<header class="runtime-loop-lane-head"><div><span>${esc(t("runtime.loop_lane"))}</span><b>${esc(t("runtime.loop_system"))}</b><small>${esc(t("runtime.inferred_phase"))}</small></div>${loops.length ? `<div class="runtime-loop-tabs" role="tablist" aria-orientation="horizontal" aria-label="${esc(t("runtime.choose_loop"))}">${loops.map((loop) => loopSelector(loop, loop.id === selectedId)).join("")}</div>` : ""}</header>
|
|
275
|
+
${selected ? loopDetail(selected) : noActiveLoop()}
|
|
276
|
+
</section>
|
|
277
|
+
</div>`;
|
|
278
|
+
requestAnimationFrame(() => {
|
|
279
|
+
if (renderVersion !== runtimeRenderVersion) return;
|
|
280
|
+
const scheduleList = section.querySelector(".runtime-schedule-list");
|
|
281
|
+
const selectedTab = section.querySelector(".runtime-loop-tab.selected");
|
|
282
|
+
const tabList = selectedTab && selectedTab.closest(".runtime-loop-tabs");
|
|
283
|
+
if (scheduleList) scheduleList.scrollTop = scheduleScrollTop;
|
|
284
|
+
if (tabList) tabList.scrollLeft = loopScrollLeft;
|
|
285
|
+
if (selectedTab && tabList && (!previousLoopTabs || previousSelectedId !== selectedId)) {
|
|
286
|
+
const item = selectedTab.getBoundingClientRect();
|
|
287
|
+
const list = tabList.getBoundingClientRect();
|
|
288
|
+
if (item.left < list.left || item.right > list.right) selectedTab.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
289
|
+
}
|
|
290
|
+
const focusTarget = focusIntent?.type === "schedule-list"
|
|
291
|
+
? scheduleList
|
|
292
|
+
: focusIntent?.type === "automation" ? section.querySelector(`[data-automation-id="${CSS.escape(focusIntent.id)}"]`)
|
|
293
|
+
: focusIntent?.type === "loop" ? section.querySelector(`[data-loop-select="${CSS.escape(focusIntent.id)}"]`) : null;
|
|
294
|
+
focusTarget?.focus({ preventScroll: true });
|
|
295
|
+
if (focusTarget && document.activeElement === focusTarget) pendingRuntimeFocus = null;
|
|
296
|
+
if (scheduleList) scheduleList.scrollTop = scheduleScrollTop;
|
|
297
|
+
if (tabList && previousLoopTabs && previousSelectedId === selectedId) tabList.scrollLeft = loopScrollLeft;
|
|
298
|
+
});
|
|
299
|
+
ensureRuntimeTicker();
|
|
300
|
+
return automations.length + loops.length;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
activeRootLoops,
|
|
305
|
+
visibleAutomations,
|
|
306
|
+
scheduleTime,
|
|
307
|
+
scheduleRule,
|
|
308
|
+
loopPhases,
|
|
309
|
+
refreshRuntimeTimes,
|
|
310
|
+
renderRuntimeOverview,
|
|
311
|
+
};
|
|
312
|
+
};
|