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
package/renderer/app.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
3
|
+
window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
|
|
4
|
+
const { $, $$, esc, uiLocale, providerLabel, reportRecoverableError } = window.LoadToAgentRendererUtils;
|
|
5
|
+
const PROJECTLESS_WORKSPACE = "__projectless__";
|
|
6
|
+
const state = {
|
|
7
|
+
providers: [],
|
|
8
|
+
providerMap: new Map(),
|
|
9
|
+
availability: {},
|
|
10
|
+
workspaces: [],
|
|
11
|
+
snapshot: null,
|
|
12
|
+
activeRuns: [],
|
|
13
|
+
versions: {},
|
|
14
|
+
update: null,
|
|
15
|
+
view: "all",
|
|
16
|
+
providerFilters: new Set(),
|
|
17
|
+
workspace: "all",
|
|
18
|
+
search: "",
|
|
19
|
+
sort: "recent",
|
|
20
|
+
selectedId: null,
|
|
21
|
+
drawerTab: "chat",
|
|
22
|
+
drawerMode: "session",
|
|
23
|
+
runProvider: "claude",
|
|
24
|
+
details: new Map(),
|
|
25
|
+
detailLoadingIds: new Set(),
|
|
26
|
+
drawerForceLatest: false,
|
|
27
|
+
visibleLimit: 30,
|
|
28
|
+
graphFocusId: null,
|
|
29
|
+
graphExpandedProviders: new Set(),
|
|
30
|
+
expandedCompletedSubagents: new Set(),
|
|
31
|
+
tmuxFocus: null,
|
|
32
|
+
agentCommandDrafts: new Map(),
|
|
33
|
+
agentCommandTargets: new Map(),
|
|
34
|
+
agentCommandSending: new Set(),
|
|
35
|
+
stopRequests: new Set(),
|
|
36
|
+
detailErrors: new Map(),
|
|
37
|
+
guideCompleted: new Set(),
|
|
38
|
+
guideExpanded: true,
|
|
39
|
+
platform: { id: "win32", label: "Windows", localShell: "powershell", localShellLabel: "Windows 명령창", nativeTmux: false },
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(state, "provider", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get() {
|
|
44
|
+
const selected = [...state.providerFilters];
|
|
45
|
+
return selected.length === 0 ? "all" : selected.length === 1 ? selected[0] : "multiple";
|
|
46
|
+
},
|
|
47
|
+
set(value) {
|
|
48
|
+
state.providerFilters.clear();
|
|
49
|
+
if (value && value !== "all" && value !== "multiple") state.providerFilters.add(String(value));
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
53
|
+
const motionState = {
|
|
54
|
+
ready: false, modalTimer: 0, toastTimer: 0, drawerTimer: 0, drawerContentTimer: 0,
|
|
55
|
+
drawerRenderKey: "", drawerTab: "", activeDialogTrigger: null,
|
|
56
|
+
};
|
|
57
|
+
document.documentElement.dataset.motion = motionPreference.matches ? "reduced" : "full";
|
|
58
|
+
motionPreference.addEventListener("change", (event) => {
|
|
59
|
+
document.documentElement.dataset.motion = event.matches ? "reduced" : "full";
|
|
60
|
+
});
|
|
61
|
+
function captureMotionLayout() {
|
|
62
|
+
const items = new Map();
|
|
63
|
+
$$("[data-motion-key]").forEach((element) => {
|
|
64
|
+
const key = element.dataset.motionKey;
|
|
65
|
+
if (!key || items.has(key)) return;
|
|
66
|
+
const rect = element.getBoundingClientRect();
|
|
67
|
+
items.set(key, { rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, value: element.dataset.motionValue || "" });
|
|
68
|
+
});
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
function motionEnterOffset(element, kind) {
|
|
72
|
+
if (kind === "focus" || kind === "focus-back") {
|
|
73
|
+
if (element.closest(".upstream-column")) return { x: -18, y: 0 };
|
|
74
|
+
if (element.closest(".downstream-column")) return { x: 18, y: 0 };
|
|
75
|
+
}
|
|
76
|
+
if (kind === "view") return { x: 0, y: 14 };
|
|
77
|
+
return { x: 0, y: 9 };
|
|
78
|
+
}
|
|
79
|
+
function playMotionLayout(previous, kind = "refresh") {
|
|
80
|
+
const elements = $$("[data-motion-key]");
|
|
81
|
+
document.documentElement.dataset.lastMotion = kind;
|
|
82
|
+
if (!motionState.ready) {
|
|
83
|
+
motionState.ready = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (motionPreference.matches) return;
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
let entered = 0;
|
|
89
|
+
elements.forEach((element) => {
|
|
90
|
+
const key = element.dataset.motionKey;
|
|
91
|
+
const before = previous.get(key);
|
|
92
|
+
const after = element.getBoundingClientRect();
|
|
93
|
+
if (before) {
|
|
94
|
+
const dx = before.rect.left - after.left;
|
|
95
|
+
const dy = before.rect.top - after.top;
|
|
96
|
+
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
97
|
+
element.animate(
|
|
98
|
+
[
|
|
99
|
+
{ transform: `translate(${dx}px, ${dy}px)`, opacity: 0.82 },
|
|
100
|
+
{ transform: "translate(0, 0)", opacity: 1 },
|
|
101
|
+
],
|
|
102
|
+
{ duration: 440, easing: "cubic-bezier(.22, 1, .36, 1)" },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (before.value && before.value !== (element.dataset.motionValue || "")) {
|
|
106
|
+
element.classList.add("motion-updated");
|
|
107
|
+
element.addEventListener("animationend", () => element.classList.remove("motion-updated"), { once: true });
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const offset = motionEnterOffset(element, kind);
|
|
112
|
+
element.animate(
|
|
113
|
+
[
|
|
114
|
+
{ transform: `translate(${offset.x}px, ${offset.y}px) scale(.985)`, opacity: 0 },
|
|
115
|
+
{ transform: "translate(0, 0) scale(1)", opacity: 1 },
|
|
116
|
+
],
|
|
117
|
+
{ duration: 360, delay: Math.min(entered++, 8) * 28, easing: "cubic-bezier(.22, 1, .36, 1)", fill: "backwards" },
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function animateVisibleSections() {
|
|
123
|
+
if (motionPreference.matches) return;
|
|
124
|
+
$$(".main-stage > section:not(.hidden)").forEach((section, index) => {
|
|
125
|
+
section.classList.remove("motion-section-in");
|
|
126
|
+
section.style.setProperty("--motion-section-delay", `${Math.min(index, 4) * 42}ms`);
|
|
127
|
+
requestAnimationFrame(() => section.classList.add("motion-section-in"));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const STATUS = {
|
|
131
|
+
starting: window.LoadToAgentI18n.t("ui.preparing"), running: window.LoadToAgentI18n.t("ui.working"),
|
|
132
|
+
waiting: window.LoadToAgentI18n.t("app.nav.needs_review"), idle: window.LoadToAgentI18n.t("ui.idle"),
|
|
133
|
+
completed: window.LoadToAgentI18n.t("ui.completed"), failed: window.LoadToAgentI18n.t("ui.problem"),
|
|
134
|
+
cancelled: window.LoadToAgentI18n.t("ui.stopped"),
|
|
135
|
+
};
|
|
136
|
+
const VIEW_TITLES = {
|
|
137
|
+
all: window.LoadToAgentI18n.t("ui.recent_conversations_and_tasks"), active: window.LoadToAgentI18n.t("ui.active_tasks"),
|
|
138
|
+
waiting: window.LoadToAgentI18n.t("ui.tasks_needing_review"), terminal: window.LoadToAgentI18n.t("app.nav.session_terminal"),
|
|
139
|
+
tmux: window.LoadToAgentI18n.t("app.nav.tmux"), settings: window.LoadToAgentI18n.t("settings.title"),
|
|
140
|
+
};
|
|
141
|
+
const VIEW_META = {
|
|
142
|
+
all: {
|
|
143
|
+
eyebrow: window.LoadToAgentI18n.t("ui.ai_work_overview"), title: window.LoadToAgentI18n.t("ui.see_all_ai_work_at_a_glance"),
|
|
144
|
+
subtitle: window.LoadToAgentI18n.t("ui.active_work_and_items_needing_your_review_appear_first_find"),
|
|
145
|
+
},
|
|
146
|
+
active: {
|
|
147
|
+
eyebrow: window.LoadToAgentI18n.t("ui.active_now"), title: window.LoadToAgentI18n.t("ui.see_which_ai_is_working_now"),
|
|
148
|
+
subtitle: window.LoadToAgentI18n.t("ui.see_what_is_being_handled_then_open_a_task_for"),
|
|
149
|
+
},
|
|
150
|
+
waiting: {
|
|
151
|
+
eyebrow: window.LoadToAgentI18n.t("ui.your_turn"), title: window.LoadToAgentI18n.t("ui.handle_items_that_need_your_review_first"),
|
|
152
|
+
subtitle: window.LoadToAgentI18n.t("ui.only_tasks_waiting_for_your_response_or_choice_are_shown"),
|
|
153
|
+
},
|
|
154
|
+
terminal: {
|
|
155
|
+
eyebrow: window.LoadToAgentI18n.t("ui.continue_an_existing_conversation"), title: window.LoadToAgentI18n.t("ui.continue_ai_sessions_in_the_terminal"),
|
|
156
|
+
subtitle: window.LoadToAgentI18n.t("ui.continue_the_same_task_with_its_previous_conversation_beside_the"),
|
|
157
|
+
},
|
|
158
|
+
tmux: {
|
|
159
|
+
eyebrow: window.LoadToAgentI18n.t("ui.advanced_work_tools"), title: window.LoadToAgentI18n.t("ui.manage_multi_terminal_work_in_one_place"),
|
|
160
|
+
subtitle: window.LoadToAgentI18n.t("ui.this_view_is_only_for_existing_tmux_workflows_home_and"),
|
|
161
|
+
},
|
|
162
|
+
settings: {
|
|
163
|
+
eyebrow: window.LoadToAgentI18n.t("ui.application_management"), title: window.LoadToAgentI18n.t("ui.check_versions_and_updates"),
|
|
164
|
+
subtitle: window.LoadToAgentI18n.t("ui.compare_the_installed_and_latest_stable_versions_then_download_a"),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const GUIDE_STORAGE_KEY = "loadtoagent:start-guide:v1";
|
|
168
|
+
const GUIDE_STEPS = ["create", "active", "waiting", "detail"];
|
|
169
|
+
function loadGuideState() {
|
|
170
|
+
try {
|
|
171
|
+
const saved = JSON.parse(localStorage.getItem(GUIDE_STORAGE_KEY) || "{}");
|
|
172
|
+
state.guideCompleted = new Set((saved.completed || []).filter((step) => GUIDE_STEPS.includes(step)));
|
|
173
|
+
state.guideExpanded = saved.expanded !== false;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
reportRecoverableError("guide-state-load", error);
|
|
176
|
+
state.guideCompleted = new Set();
|
|
177
|
+
state.guideExpanded = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function saveGuideState() {
|
|
181
|
+
try {
|
|
182
|
+
localStorage.setItem(GUIDE_STORAGE_KEY, JSON.stringify({ completed: [...state.guideCompleted], expanded: state.guideExpanded }));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
reportRecoverableError("guide-state-save", error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function renderGuide() {
|
|
188
|
+
const completed = state.guideCompleted.size;
|
|
189
|
+
GUIDE_STEPS.forEach((step) => {
|
|
190
|
+
const item = document.querySelector(`[data-guide-step="${step}"]`);
|
|
191
|
+
const done = state.guideCompleted.has(step);
|
|
192
|
+
item?.classList.toggle("completed", done);
|
|
193
|
+
item?.querySelector("button")?.setAttribute("aria-pressed", done ? "true" : "false");
|
|
194
|
+
});
|
|
195
|
+
const percent = (completed / GUIDE_STEPS.length) * 100;
|
|
196
|
+
$("#guideProgressBar").style.width = `${percent}%`;
|
|
197
|
+
const progress = $(".guide-progress");
|
|
198
|
+
progress.setAttribute("aria-valuenow", String(completed));
|
|
199
|
+
$("#guideButtonProgress").textContent = completed === GUIDE_STEPS.length
|
|
200
|
+
? window.LoadToAgentI18n.t("ui.basics_completed")
|
|
201
|
+
: window.LoadToAgentI18n.t("common.progress", { current: completed, total: GUIDE_STEPS.length });
|
|
202
|
+
$("#guideProgressText").textContent =
|
|
203
|
+
completed === GUIDE_STEPS.length
|
|
204
|
+
? window.LoadToAgentI18n.t("ui.you_completed_the_basics_you_can_reopen_this_guide_anytime")
|
|
205
|
+
: window.LoadToAgentI18n.t("guide.steps_remaining", { count: GUIDE_STEPS.length - completed });
|
|
206
|
+
$("#guideBtn").setAttribute("aria-expanded", state.guideExpanded ? "true" : "false");
|
|
207
|
+
}
|
|
208
|
+
function markGuideStep(step) {
|
|
209
|
+
if (!GUIDE_STEPS.includes(step) || state.guideCompleted.has(step)) return;
|
|
210
|
+
state.guideCompleted.add(step);
|
|
211
|
+
saveGuideState();
|
|
212
|
+
renderGuide();
|
|
213
|
+
}
|
|
214
|
+
function syncViewChrome() {
|
|
215
|
+
const meta = VIEW_META[state.view] || VIEW_META.all;
|
|
216
|
+
document.body.dataset.currentView = state.view;
|
|
217
|
+
$("#pageEyebrow").textContent = meta.eyebrow;
|
|
218
|
+
$("#pageTitle").textContent = meta.title;
|
|
219
|
+
$("#pageSubtitle").textContent = meta.subtitle;
|
|
220
|
+
document.title = `${VIEW_TITLES[state.view] || "LoadToAgent"} · LoadToAgent`;
|
|
221
|
+
$$(".nav-item[data-view]").forEach((item) => {
|
|
222
|
+
const active = item.dataset.view === state.view;
|
|
223
|
+
item.classList.toggle("active", active);
|
|
224
|
+
if (active) item.setAttribute("aria-current", "page");
|
|
225
|
+
else item.removeAttribute("aria-current");
|
|
226
|
+
});
|
|
227
|
+
const advancedView = ["terminal", "tmux", "settings"].includes(state.view);
|
|
228
|
+
$("#mobileMoreBtn")?.classList.toggle("active", advancedView);
|
|
229
|
+
if (advancedView) $("#mobileMoreBtn")?.setAttribute("aria-current", "page");
|
|
230
|
+
else $("#mobileMoreBtn")?.removeAttribute("aria-current");
|
|
231
|
+
}
|
|
232
|
+
function selectView(view, options = {}) {
|
|
233
|
+
state.view = view;
|
|
234
|
+
state.visibleLimit = 30;
|
|
235
|
+
if (view === "active" || view === "waiting") markGuideStep(view);
|
|
236
|
+
syncViewChrome();
|
|
237
|
+
context.renderSessions(options.motionKind || "view");
|
|
238
|
+
$("#mobileToolsMenu")?.classList.add("hidden");
|
|
239
|
+
$("#mobileMoreBtn")?.setAttribute("aria-expanded", "false");
|
|
240
|
+
document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
|
|
241
|
+
if (options.focusMain) $("#mainContent")?.focus({ preventScroll: true });
|
|
242
|
+
}
|
|
243
|
+
function currentDialog() {
|
|
244
|
+
if (!$("#runModal").classList.contains("hidden")) return $("#runModal");
|
|
245
|
+
if (!$("#tmuxCreateModal").classList.contains("hidden")) return $("#tmuxCreateModal");
|
|
246
|
+
if ($("#detailDrawer").classList.contains("open")) return $("#detailDrawer");
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
function dialogFocusable(dialog) {
|
|
250
|
+
const selector = [
|
|
251
|
+
"button:not([disabled])", "[href]", "input:not([disabled])", "select:not([disabled])",
|
|
252
|
+
"textarea:not([disabled])", '[tabindex]:not([tabindex="-1"])',
|
|
253
|
+
].join(", ");
|
|
254
|
+
return [...dialog.querySelectorAll(selector)].filter(
|
|
255
|
+
(element) => !element.closest(".hidden") && !element.hidden && element.getClientRects().length,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function trapDialogFocus(event) {
|
|
259
|
+
if (event.key !== "Tab") return;
|
|
260
|
+
const dialog = currentDialog();
|
|
261
|
+
if (!dialog) return;
|
|
262
|
+
const focusable = dialogFocusable(dialog);
|
|
263
|
+
if (!focusable.length) return;
|
|
264
|
+
const first = focusable[0];
|
|
265
|
+
const last = focusable[focusable.length - 1];
|
|
266
|
+
if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
last.focus();
|
|
269
|
+
} else if (!event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement))) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
first.focus();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function rememberDialogTrigger() {
|
|
275
|
+
motionState.activeDialogTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
276
|
+
}
|
|
277
|
+
function restoreDialogTrigger() {
|
|
278
|
+
const trigger = motionState.activeDialogTrigger;
|
|
279
|
+
motionState.activeDialogTrigger = null;
|
|
280
|
+
if (trigger && trigger.isConnected) trigger.focus();
|
|
281
|
+
}
|
|
282
|
+
window.LoadToAgentA11y = { rememberDialogTrigger, restoreDialogTrigger };
|
|
283
|
+
function readablePreview(value, maxCharacters = 120) {
|
|
284
|
+
const full = String(value == null ? "" : value)
|
|
285
|
+
.replace(/\s+/g, " ")
|
|
286
|
+
.trim();
|
|
287
|
+
if (full.length <= maxCharacters) return { full, text: full, truncated: false };
|
|
288
|
+
const sample = full.slice(0, maxCharacters + 1);
|
|
289
|
+
const wordBoundary = sample.lastIndexOf(" ");
|
|
290
|
+
const cut = wordBoundary >= Math.floor(maxCharacters * 0.72) ? wordBoundary : maxCharacters;
|
|
291
|
+
return { full, text: `${sample.slice(0, cut).trimEnd()}…`, truncated: true };
|
|
292
|
+
}
|
|
293
|
+
function memoryCategoryLabel(value) {
|
|
294
|
+
const labels = { insight: "인사이트", convention: "작업 규칙", failure: "실패 기록", decision: "결정", pattern: "반복 패턴" };
|
|
295
|
+
return labels[String(value || "").toLowerCase()] || String(value || "기록");
|
|
296
|
+
}
|
|
297
|
+
function jsonValueHtml(value, depth = 0) {
|
|
298
|
+
if (value == null) return '<span class="json-empty">없음</span>';
|
|
299
|
+
if (typeof value === "boolean") return `<span class="json-primitive">${value ? "예" : "아니요"}</span>`;
|
|
300
|
+
if (typeof value === "number") return `<span class="json-primitive">${esc(value.toLocaleString(uiLocale()))}</span>`;
|
|
301
|
+
if (typeof value === "string") return `<span class="json-string">${esc(value)}</span>`;
|
|
302
|
+
if (depth >= 4) return `<span class="json-string">${esc(JSON.stringify(value))}</span>`;
|
|
303
|
+
if (Array.isArray(value)) {
|
|
304
|
+
const shown = value.slice(0, 40);
|
|
305
|
+
const visibleItems = shown.map((item) => `<li>${jsonValueHtml(item, depth + 1)}</li>`).join("");
|
|
306
|
+
const moreItems = value.length > shown.length ? `<li class="json-more">외 ${value.length - shown.length}개</li>` : "";
|
|
307
|
+
return `<ol class="json-array">${visibleItems}${moreItems}</ol>`;
|
|
308
|
+
}
|
|
309
|
+
const entries = Object.entries(value).slice(0, 40);
|
|
310
|
+
return `<dl class="json-object">${entries.map(([key, item]) => `<div><dt>${esc(key)}</dt><dd>${jsonValueHtml(item, depth + 1)}</dd></div>`).join("")}</dl>`;
|
|
311
|
+
}
|
|
312
|
+
function memoryCandidatesHtml(items) {
|
|
313
|
+
return `<div class="memory-candidates">
|
|
314
|
+
<div class="structured-heading">
|
|
315
|
+
<b>저장할 작업 기억</b>
|
|
316
|
+
<span>${items.length}개 항목</span>
|
|
317
|
+
</div>${items
|
|
318
|
+
.map(
|
|
319
|
+
(item, index) => `<article class="memory-candidate">
|
|
320
|
+
<header>
|
|
321
|
+
<b>${index + 1}</b>
|
|
322
|
+
<span class="memory-target">${esc(item.target || "MEMORY")}</span>
|
|
323
|
+
<span class="memory-category">${esc(memoryCategoryLabel(item.category))}</span>
|
|
324
|
+
</header>
|
|
325
|
+
<p>${esc(item.content || "내용 없음")}</p>
|
|
326
|
+
</article>`,
|
|
327
|
+
)
|
|
328
|
+
.join("")}</div>`;
|
|
329
|
+
}
|
|
330
|
+
function inlineMarkdown(value) {
|
|
331
|
+
return esc(value)
|
|
332
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
333
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
334
|
+
}
|
|
335
|
+
function markdownHtml(value) {
|
|
336
|
+
const output = [];
|
|
337
|
+
let fence = false;
|
|
338
|
+
let code = [];
|
|
339
|
+
let list = "";
|
|
340
|
+
const closeList = () => {
|
|
341
|
+
if (list) {
|
|
342
|
+
output.push(`</${list}>`);
|
|
343
|
+
list = "";
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
for (const line of String(value || "")
|
|
347
|
+
.replace(/\r\n/g, "\n")
|
|
348
|
+
.split("\n")) {
|
|
349
|
+
if (/^```/.test(line)) {
|
|
350
|
+
closeList();
|
|
351
|
+
if (fence) {
|
|
352
|
+
output.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
|
|
353
|
+
code = [];
|
|
354
|
+
fence = false;
|
|
355
|
+
} else fence = true;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (fence) {
|
|
359
|
+
code.push(line);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
|
|
363
|
+
const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
|
364
|
+
if (bullet || ordered) {
|
|
365
|
+
const next = bullet ? "ul" : "ol";
|
|
366
|
+
if (list !== next) {
|
|
367
|
+
closeList();
|
|
368
|
+
output.push(`<${next}>`);
|
|
369
|
+
list = next;
|
|
370
|
+
}
|
|
371
|
+
output.push(`<li>${inlineMarkdown((bullet || ordered)[1])}</li>`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
closeList();
|
|
375
|
+
if (!line.trim()) {
|
|
376
|
+
output.push("<br>");
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const heading = line.match(/^(#{1,3})\s+(.+)$/);
|
|
380
|
+
if (heading) output.push(`<h${heading[1].length + 2}>${inlineMarkdown(heading[2])}</h${heading[1].length + 2}>`);
|
|
381
|
+
else output.push(`<p>${inlineMarkdown(line)}</p>`);
|
|
382
|
+
}
|
|
383
|
+
closeList();
|
|
384
|
+
if (fence) output.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
|
|
385
|
+
return `<div class="chat-content markdown">${output.join("")}</div>`;
|
|
386
|
+
}
|
|
387
|
+
function roadmapHtml(value) {
|
|
388
|
+
const text = String(value || "").trim();
|
|
389
|
+
const lines = text
|
|
390
|
+
.replace(/\r\n/g, "\n")
|
|
391
|
+
.split("\n")
|
|
392
|
+
.map((line) => line.trim())
|
|
393
|
+
.filter(Boolean);
|
|
394
|
+
const hasRoadmapSignal = /(?:로드맵|작업\s*계획|roadmap|implementation\s+plan|milestone|phase\s*\d+)/i.test(text);
|
|
395
|
+
const steps = lines.filter((line) => /^(?:[-*]\s+|\d+[.)]\s+|#{2,3}\s+(?!로드맵|roadmap))/i.test(line));
|
|
396
|
+
if (!hasRoadmapSignal || (text.length < 420 && steps.length < 6)) return "";
|
|
397
|
+
const heading = lines.find((line) => /^#{1,3}\s+/.test(line));
|
|
398
|
+
const title = readablePreview((heading || "작업 로드맵").replace(/^#{1,3}\s+/, ""), 72).text;
|
|
399
|
+
const previewSteps = (steps.length ? steps : lines.filter((line) => !/^#{1,3}\s+/.test(line)))
|
|
400
|
+
.slice(0, 3)
|
|
401
|
+
.map((line) => readablePreview(line.replace(/^(?:[-*]\s+|\d+[.)]\s+|#{2,3}\s+)/, "").replace(/\*\*/g, ""), 92).text);
|
|
402
|
+
const countLabel = steps.length ? `${steps.length}개 단계` : "긴 계획";
|
|
403
|
+
return `<details class="chat-roadmap" data-roadmap-collapsed="true">
|
|
404
|
+
<summary>
|
|
405
|
+
<span class="chat-roadmap-mark" aria-hidden="true">MAP</span>
|
|
406
|
+
<span>
|
|
407
|
+
<b>${esc(title)}</b>
|
|
408
|
+
<small>${esc(countLabel)} · 접어서 표시</small>
|
|
409
|
+
</span>
|
|
410
|
+
<i aria-hidden="true">↓</i>
|
|
411
|
+
</summary>
|
|
412
|
+
<ol class="chat-roadmap-preview">${previewSteps.map((step) => `<li>${esc(step)}</li>`).join("")}</ol>
|
|
413
|
+
<div class="chat-roadmap-full">${markdownHtml(text)}</div>
|
|
414
|
+
</details>`;
|
|
415
|
+
}
|
|
416
|
+
function messageContentHtml(message) {
|
|
417
|
+
const text = String((message && message.text) || "").trim();
|
|
418
|
+
if (!text) return '<div class="chat-content empty">표시할 내용이 없습니다.</div>';
|
|
419
|
+
if (/^[\[{]/.test(text) && /[\]}]$/.test(text)) {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(text);
|
|
422
|
+
const isMemoryCandidate = (item) =>
|
|
423
|
+
item && typeof item === "object" && "content" in item && ("target" in item || "category" in item);
|
|
424
|
+
if (Array.isArray(parsed) && parsed.length && parsed.every(isMemoryCandidate)) {
|
|
425
|
+
return memoryCandidatesHtml(parsed);
|
|
426
|
+
}
|
|
427
|
+
return `<div class="structured-json">
|
|
428
|
+
<div class="structured-heading">
|
|
429
|
+
<b>구조화된 데이터</b>
|
|
430
|
+
<span>${Array.isArray(parsed) ? `${parsed.length}개 항목` : "JSON"}</span>
|
|
431
|
+
</div>${jsonValueHtml(parsed)}</div>`;
|
|
432
|
+
} catch (_plainChatMessage) {
|
|
433
|
+
// A normal chat message may begin with JSON punctuation without being JSON.
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const roadmap = message && message.role === "assistant" ? roadmapHtml(text) : "";
|
|
437
|
+
if (roadmap) return roadmap;
|
|
438
|
+
return markdownHtml(text);
|
|
439
|
+
}
|
|
440
|
+
function compact(value) {
|
|
441
|
+
const n = Number(value || 0);
|
|
442
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(n >= 10_000_000_000 ? 0 : 1)}B`;
|
|
443
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`;
|
|
444
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(n >= 100_000 ? 0 : 1)}K`;
|
|
445
|
+
return n.toLocaleString(uiLocale());
|
|
446
|
+
}
|
|
447
|
+
function fullNumber(value) {
|
|
448
|
+
return Number(value || 0).toLocaleString(uiLocale());
|
|
449
|
+
}
|
|
450
|
+
function timeAgo(value) {
|
|
451
|
+
const ms = Date.now() - Date.parse(value || 0);
|
|
452
|
+
if (!Number.isFinite(ms)) return "-";
|
|
453
|
+
if (ms < 8_000) return window.LoadToAgentI18n.t("time.just_now");
|
|
454
|
+
const sec = Math.floor(ms / 1000);
|
|
455
|
+
if (sec < 60) return window.LoadToAgentI18n.t("time.seconds_ago", { count: sec });
|
|
456
|
+
const min = Math.floor(sec / 60);
|
|
457
|
+
if (min < 60) return window.LoadToAgentI18n.t("time.minutes_ago", { count: min });
|
|
458
|
+
const hour = Math.floor(min / 60);
|
|
459
|
+
if (hour < 24) return window.LoadToAgentI18n.t("time.hours_ago", { count: hour });
|
|
460
|
+
const day = Math.floor(hour / 24);
|
|
461
|
+
return day < 30 ? window.LoadToAgentI18n.t("time.days_ago", { count: day }) : new Date(value).toLocaleDateString(uiLocale());
|
|
462
|
+
}
|
|
463
|
+
function timeOnly(value) {
|
|
464
|
+
const date = new Date(value);
|
|
465
|
+
return Number.isNaN(date.getTime()) ? "-" : date.toLocaleTimeString(uiLocale(), { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
466
|
+
}
|
|
467
|
+
function providerInfo(id) {
|
|
468
|
+
return state.providerMap.get(id) || { id, label: providerLabel(id), company: "", accent: "#8fa2b7", mark: "AI", docs: "" };
|
|
469
|
+
}
|
|
470
|
+
function providerStyle(id) {
|
|
471
|
+
return `--provider:${providerInfo(id).accent}`;
|
|
472
|
+
}
|
|
473
|
+
function agentRoleLabel(value) {
|
|
474
|
+
const labels = {
|
|
475
|
+
explorer: window.LoadToAgentI18n.t("ui.research"), reviewer: window.LoadToAgentI18n.t("ui.review"),
|
|
476
|
+
worker: window.LoadToAgentI18n.t("ui.execution"), general: window.LoadToAgentI18n.t("ui.assistance"),
|
|
477
|
+
planner: window.LoadToAgentI18n.t("ui.planning"), tester: window.LoadToAgentI18n.t("ui.testing"),
|
|
478
|
+
};
|
|
479
|
+
return labels[String(value || "").toLowerCase()] || String(value || window.LoadToAgentI18n.t("ui.assistance"));
|
|
480
|
+
}
|
|
481
|
+
function statusClass(status) {
|
|
482
|
+
return ["running", "waiting", "completed", "failed", "cancelled"].includes(status) ? status : "";
|
|
483
|
+
}
|
|
484
|
+
function currentActivity(session) {
|
|
485
|
+
const items = session.lifecycle || [];
|
|
486
|
+
const running = isLiveSession(session) ? [...items].reverse().find((item) => item.status === "running") : null;
|
|
487
|
+
const last = running || items[items.length - 1];
|
|
488
|
+
if (last) {
|
|
489
|
+
return {
|
|
490
|
+
title: last.label || window.LoadToAgentI18n.t("ui.activity"), detail: last.detail || session.statusDetail || "", type: last.type || "activity",
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const message = (session.messages || [])[session.messages.length - 1];
|
|
494
|
+
return { title: session.statusDetail || window.LoadToAgentI18n.t("ui.temporarily_idle"), detail: (message && message.text) || "", type: "activity" };
|
|
495
|
+
}
|
|
496
|
+
function isLiveSession(session) {
|
|
497
|
+
return session && (session.status === "running" || session.status === "starting");
|
|
498
|
+
}
|
|
499
|
+
function subagentWorkState(session) {
|
|
500
|
+
if (isLiveSession(session)) return "working";
|
|
501
|
+
if (session && session.status === "failed") return "attention";
|
|
502
|
+
return "resting";
|
|
503
|
+
}
|
|
504
|
+
function subagentWorkLabel(session) {
|
|
505
|
+
const labels = {
|
|
506
|
+
working: window.LoadToAgentI18n.t("ui.working"), resting: window.LoadToAgentI18n.t("ui.idle"),
|
|
507
|
+
attention: window.LoadToAgentI18n.t("ui.needs_attention"),
|
|
508
|
+
};
|
|
509
|
+
return labels[subagentWorkState(session)];
|
|
510
|
+
}
|
|
511
|
+
function readableActivityDetail(value) {
|
|
512
|
+
const text = String(value || "").trim();
|
|
513
|
+
if (!text || !/^[\[{]/.test(text)) return text;
|
|
514
|
+
try {
|
|
515
|
+
const parsed = JSON.parse(text);
|
|
516
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return text;
|
|
517
|
+
if (parsed.cell_id) return `실행 중인 작업 결과를 기다리는 중${parsed.yield_time_ms ? ` · 최대 ${Math.round(Number(parsed.yield_time_ms) / 1000)}초` : ""}`;
|
|
518
|
+
if (parsed.command) return `명령 실행 · ${String(parsed.command).replace(/\s+/g, " ").slice(0, 180)}`;
|
|
519
|
+
if (parsed.path || parsed.file_path) return `파일 확인 · ${parsed.path || parsed.file_path}`;
|
|
520
|
+
if (parsed.prompt) return `AI에게 맡긴 일 · ${String(parsed.prompt).replace(/\s+/g, " ").slice(0, 180)}`;
|
|
521
|
+
const summary = Object.entries(parsed)
|
|
522
|
+
.slice(0, 3)
|
|
523
|
+
.map(([key, item]) => `${key}: ${typeof item === "object" ? "구조화 데이터" : item}`)
|
|
524
|
+
.join(" · ");
|
|
525
|
+
return summary || text;
|
|
526
|
+
} catch (_malformedCommandPreview) {
|
|
527
|
+
// Command previews may be truncated while an agent is still writing them.
|
|
528
|
+
return text;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function latestWorkCopy(session) {
|
|
532
|
+
const delegation = session.delegation || {};
|
|
533
|
+
const completedResult = delegation.result || session.result;
|
|
534
|
+
if (session.status === "completed" && completedResult) return `완료 결과 · ${completedResult}`;
|
|
535
|
+
if (session.status === "completed") return "담당 작업을 완료하고 메인 AI에 결과를 반환했습니다.";
|
|
536
|
+
const activity = currentActivity(session);
|
|
537
|
+
if (activity.detail) return readableActivityDetail(activity.detail);
|
|
538
|
+
const messages = session.messages || [];
|
|
539
|
+
const assistant = [...messages].reverse().find((item) => item.role === "assistant" && item.text);
|
|
540
|
+
if (assistant) return assistant.text;
|
|
541
|
+
const tool = [...messages].reverse().find((item) => item.role === "tool");
|
|
542
|
+
if (tool) return `${tool.title || "도구"} 실행 · ${tool.text || "결과를 기다리는 중"}`;
|
|
543
|
+
return activity.title || session.statusDetail || "다음 할 일을 기다리는 중";
|
|
544
|
+
}
|
|
545
|
+
function statusIcon(type) {
|
|
546
|
+
if (/tool/.test(type)) return "⌘";
|
|
547
|
+
if (/reason/.test(type)) return "◌";
|
|
548
|
+
if (/error|fail/.test(type)) return "!";
|
|
549
|
+
if (/start|turn/.test(type)) return "↗";
|
|
550
|
+
if (/end|complete/.test(type)) return "✓";
|
|
551
|
+
return "·";
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
$,
|
|
555
|
+
$$,
|
|
556
|
+
esc,
|
|
557
|
+
uiLocale,
|
|
558
|
+
providerLabel,
|
|
559
|
+
reportRecoverableError,
|
|
560
|
+
PROJECTLESS_WORKSPACE,
|
|
561
|
+
state,
|
|
562
|
+
motionPreference,
|
|
563
|
+
motionState,
|
|
564
|
+
STATUS,
|
|
565
|
+
VIEW_TITLES,
|
|
566
|
+
VIEW_META,
|
|
567
|
+
GUIDE_STORAGE_KEY,
|
|
568
|
+
GUIDE_STEPS,
|
|
569
|
+
captureMotionLayout,
|
|
570
|
+
motionEnterOffset,
|
|
571
|
+
playMotionLayout,
|
|
572
|
+
animateVisibleSections,
|
|
573
|
+
loadGuideState,
|
|
574
|
+
saveGuideState,
|
|
575
|
+
renderGuide,
|
|
576
|
+
markGuideStep,
|
|
577
|
+
syncViewChrome,
|
|
578
|
+
selectView,
|
|
579
|
+
currentDialog,
|
|
580
|
+
dialogFocusable,
|
|
581
|
+
trapDialogFocus,
|
|
582
|
+
rememberDialogTrigger,
|
|
583
|
+
restoreDialogTrigger,
|
|
584
|
+
readablePreview,
|
|
585
|
+
memoryCategoryLabel,
|
|
586
|
+
jsonValueHtml,
|
|
587
|
+
memoryCandidatesHtml,
|
|
588
|
+
inlineMarkdown,
|
|
589
|
+
markdownHtml,
|
|
590
|
+
roadmapHtml,
|
|
591
|
+
messageContentHtml,
|
|
592
|
+
compact,
|
|
593
|
+
fullNumber,
|
|
594
|
+
timeAgo,
|
|
595
|
+
timeOnly,
|
|
596
|
+
providerInfo,
|
|
597
|
+
providerStyle,
|
|
598
|
+
agentRoleLabel,
|
|
599
|
+
statusClass,
|
|
600
|
+
currentActivity,
|
|
601
|
+
isLiveSession,
|
|
602
|
+
subagentWorkState,
|
|
603
|
+
subagentWorkLabel,
|
|
604
|
+
readableActivityDetail,
|
|
605
|
+
latestWorkCopy,
|
|
606
|
+
statusIcon,
|
|
607
|
+
};
|
|
608
|
+
};
|
|
Binary file
|
|
Binary file
|