loadtoagent 1.3.5 → 1.3.6
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/docs/PROVIDER-CONTRACTS.md +3 -1
- package/package.json +1 -1
- package/renderer/app-dashboard.js +75 -13
- package/renderer/app-drawer-content.js +189 -8
- package/renderer/app-drawer.js +4 -3
- package/renderer/app-events-dialogs.js +6 -1
- package/renderer/app-events-filters.js +80 -2
- package/renderer/app-events-sessions.js +110 -17
- package/renderer/app-graph-model.js +8 -5
- package/renderer/app-graph-orchestration.js +43 -11
- package/renderer/app-graph-view.js +75 -14
- package/renderer/app-quality.js +2 -0
- package/renderer/app-session-render.js +7 -7
- package/renderer/app.js +104 -0
- package/renderer/i18n-messages.js +46 -9
- package/renderer/index.html +33 -7
- package/renderer/styles-components.css +218 -1
- package/renderer/styles-control-room.css +820 -29
- package/renderer/styles-readability.css +1 -9
- package/renderer/styles-responsive-shell.css +22 -1
- package/renderer/styles-terminal.css +21 -31
- package/renderer/styles-workflow-map.css +8 -0
- package/renderer/terminal-events.js +0 -13
- package/renderer/terminal-workbench.js +1 -4
- package/src/agentMonitor/claudeParser.js +189 -4
- package/src/agentMonitor/codexCollaboration.js +1 -0
- package/src/agentMonitor/codexParser.js +6 -5
- package/src/agentMonitor/executionActivity.js +26 -1
- package/src/agentMonitor/hierarchy.js +6 -3
- package/src/agentMonitor.js +10 -3
- package/src/contracts.js +1 -1
- package/src/monitorWorker.js +2 -0
|
@@ -19,7 +19,9 @@ LoadToAgent는 제공사별 이벤트를 아래 공통 단계로 정규화합니
|
|
|
19
19
|
|
|
20
20
|
관측 이벤트가 없는 값은 사실로 단정하지 않고 `inferred` 또는 `unverified`로 표시합니다. 시간 기반 건강 신호는 매분 다시 계산됩니다.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
서브에이전트 작업 지시는 제공사 호출 로그에서 확인한 원문만 `assignment`로 취급합니다. Claude의 `Agent`/`Task` 도구는 `input.prompt`, Codex의 `spawn_agent`는 평문 `message`가 근거입니다. Codex가 `message`를 암호문으로 저장한 경우 직전 메인 AI 설명을 실제 전달 원문으로 대체하지 않으며, 원문은 `protected`, 직전 설명은 별도 참고 문맥으로 표시합니다.
|
|
23
|
+
|
|
24
|
+
화면의 최근 세션 기준은 마지막 활동 후 24시간입니다. 실행 중인 세션은 마지막 활동 시각과 관계없이 계속 표시합니다. 앱이 실행 상태를 관측한 세션은 실행이 끝나도 마지막 AI 응답 후 30분 동안 관제 화면에서 `대기 중`으로 유지하며, 사용자가 직접 지난 기록으로 옮기면 즉시 제외합니다. 새 AI 응답이 생기면 이전의 수동 이동 기록은 무효화됩니다. `확인할 일`은 24시간 범위 안의 실제 응답 요청과 현재 실행 위험만 집계하며, 오래된 세션이나 단순한 낮은 확인 수준은 제외합니다.
|
|
23
25
|
|
|
24
26
|
## 명령·백그라운드 실행 계약
|
|
25
27
|
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
16
16
|
visibleSessions = () => ((state.snapshot && state.snapshot.sessions) || []),
|
|
17
17
|
isProviderVisible = () => true,
|
|
18
18
|
isRuntimeLoopSession = () => false,
|
|
19
|
+
isControlRoomSession = session => session?.status === "running" || session?.status === "starting",
|
|
19
20
|
} = context;
|
|
20
21
|
|
|
21
22
|
function displaySessions() {
|
|
@@ -64,11 +65,11 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
64
65
|
return normalized.split("/").filter(Boolean).pop() || t("workspace.unknown");
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function observedProjects() {
|
|
68
|
+
function observedProjects(sessions = displaySessions().filter((session) => !session.parentId)) {
|
|
68
69
|
const projects = new Map();
|
|
69
70
|
const saved = state.workspaces.map((item) => ({ ...item, key: normalizedProjectPath(item.path) }));
|
|
70
71
|
saved.forEach((item) => projects.set(item.key, { path: item.path, name: item.name || projectName(item.path), saved: true, count: 0 }));
|
|
71
|
-
|
|
72
|
+
sessions.filter((session) => !session.parentId && !isProjectlessSession(session)).forEach((session) => {
|
|
72
73
|
const originPath = sessionOriginPath(session);
|
|
73
74
|
if (!originPath) return;
|
|
74
75
|
const owner = saved
|
|
@@ -97,9 +98,26 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function sessionWorkspaceLabel(session) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
if (isProjectlessSession(session)) return t("ui.no_project");
|
|
102
|
+
const originPath = sessionOriginPath(session);
|
|
103
|
+
const owner = state.workspaces
|
|
104
|
+
.filter((item) => projectContainsPath(item.path, originPath))
|
|
105
|
+
.sort((a, b) => normalizedProjectPath(b.path).length - normalizedProjectPath(a.path).length)[0];
|
|
106
|
+
return owner?.name || (session && session.workspace) || projectName(originPath);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function controlRoomProject(session) {
|
|
110
|
+
if (isProjectlessSession(session)) return { key: PROJECTLESS_WORKSPACE, path: PROJECTLESS_WORKSPACE, label: t("control.other_projects") };
|
|
111
|
+
const originPath = sessionOriginPath(session);
|
|
112
|
+
const owner = state.workspaces
|
|
113
|
+
.filter((item) => projectContainsPath(item.path, originPath))
|
|
114
|
+
.sort((a, b) => normalizedProjectPath(b.path).length - normalizedProjectPath(a.path).length)[0];
|
|
115
|
+
const path = owner?.path || originPath;
|
|
116
|
+
return {
|
|
117
|
+
key: normalizedProjectPath(path) || String(session?.workspace || session?.id || "unknown").toLocaleLowerCase(),
|
|
118
|
+
path,
|
|
119
|
+
label: owner?.name || (session && session.workspace) || projectName(originPath) || t("control.other_projects"),
|
|
120
|
+
};
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
function matchesWorkspaceFilter(session) {
|
|
@@ -109,21 +127,29 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
function renderWorkspaces() {
|
|
112
|
-
const lists = [$("#workspaceList"), $("#mobileWorkspaceList")].filter(Boolean);
|
|
113
130
|
const rootSessions = displaySessions().filter((session) => !session.parentId);
|
|
114
|
-
const
|
|
131
|
+
const liveRootSessions = rootSessions.filter(isControlRoomSession);
|
|
132
|
+
const projects = observedProjects(rootSessions);
|
|
133
|
+
const savedProjectOrder = new Map(state.workspaces.map((workspace, index) => [normalizedProjectPath(workspace.path), index]));
|
|
134
|
+
const liveProjects = observedProjects(liveRootSessions).sort((a, b) => {
|
|
135
|
+
const aOrder = savedProjectOrder.get(normalizedProjectPath(a.path));
|
|
136
|
+
const bOrder = savedProjectOrder.get(normalizedProjectPath(b.path));
|
|
137
|
+
if (aOrder != null || bOrder != null) return (aOrder ?? Number.MAX_SAFE_INTEGER) - (bOrder ?? Number.MAX_SAFE_INTEGER);
|
|
138
|
+
return Number(b.count || 0) - Number(a.count || 0) || String(a.name).localeCompare(String(b.name), uiLocale());
|
|
139
|
+
});
|
|
115
140
|
const projectlessCount = rootSessions.filter(isProjectlessSession).length;
|
|
141
|
+
const liveProjectlessCount = liveRootSessions.filter(isProjectlessSession).length;
|
|
116
142
|
const savedWorkspaceExists = state.workspace === "all"
|
|
117
143
|
|| (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
|
|
118
144
|
|| projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
|
|
119
145
|
if (!savedWorkspaceExists) state.workspace = "all";
|
|
120
|
-
const projectButton = (item) => `<button type="button" class="workspace-item observed-project ${state.workspace === item.path ? "selected" : ""}"
|
|
146
|
+
const projectButton = (item, compactClass = "") => `<button type="button" class="workspace-item observed-project ${compactClass} ${state.workspace === item.path ? "selected" : ""}"
|
|
121
147
|
data-workspace="${esc(item.path)}" title="${esc(item.path)}"
|
|
122
148
|
aria-label="${esc(t("project.filter_named", { name: item.name, count: item.count }))}"
|
|
123
149
|
aria-pressed="${state.workspace === item.path ? "true" : "false"}">
|
|
124
150
|
<strong>${esc(item.name)}</strong><small>${Number(item.count || 0)}</small>
|
|
125
151
|
</button>`;
|
|
126
|
-
const
|
|
152
|
+
const mobileHtml =
|
|
127
153
|
`<button type="button" class="workspace-item ${state.workspace === "all" ? "selected" : ""}"
|
|
128
154
|
data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
|
|
129
155
|
<strong>${window.LoadToAgentI18n.t("project.all")}</strong><small>${rootSessions.length}</small>
|
|
@@ -144,7 +170,40 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
144
170
|
title="${esc(window.LoadToAgentI18n.t("ui.remove_from_list"))}">×</button>
|
|
145
171
|
</div>` : projectButton(item)).join("") +
|
|
146
172
|
(!projects.length && !projectlessCount ? `<div class="workspace-empty">${window.LoadToAgentI18n.t("project.empty")}</div>` : "");
|
|
147
|
-
|
|
173
|
+
const desktopHtml =
|
|
174
|
+
`<button type="button" class="workspace-item control-room-project-chip ${state.workspace === "all" ? "selected" : ""}"
|
|
175
|
+
data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
|
|
176
|
+
<strong>${esc(t("control.all_projects"))}</strong><small>${liveRootSessions.length}</small>
|
|
177
|
+
</button>` +
|
|
178
|
+
liveProjects.map((item) => projectButton(item, "control-room-project-chip")).join("") +
|
|
179
|
+
(liveProjectlessCount
|
|
180
|
+
? `<button type="button" class="workspace-item projectless control-room-project-chip ${state.workspace === PROJECTLESS_WORKSPACE ? "selected" : ""}"
|
|
181
|
+
data-workspace="${PROJECTLESS_WORKSPACE}" aria-pressed="${state.workspace === PROJECTLESS_WORKSPACE ? "true" : "false"}">
|
|
182
|
+
<strong>${esc(t("control.other_projects"))}</strong><small>${liveProjectlessCount}</small>
|
|
183
|
+
</button>`
|
|
184
|
+
: "") +
|
|
185
|
+
(!liveProjects.length && !liveProjectlessCount ? `<div class="workspace-empty">${window.LoadToAgentI18n.t("project.empty")}</div>` : "");
|
|
186
|
+
const desktopList = $("#workspaceList");
|
|
187
|
+
const mobileList = $("#mobileWorkspaceList");
|
|
188
|
+
if (desktopList) desktopList.innerHTML = desktopHtml;
|
|
189
|
+
if (mobileList) mobileList.innerHTML = mobileHtml;
|
|
190
|
+
const projectSelect = $("#controlRoomProjectSelect");
|
|
191
|
+
if (projectSelect) {
|
|
192
|
+
projectSelect.innerHTML = `<option value="all">${esc(t("control.all_projects"))}</option>`
|
|
193
|
+
+ liveProjects.map((item) => `<option value="${esc(item.path)}">${esc(item.name)}</option>`).join("")
|
|
194
|
+
+ (liveProjectlessCount ? `<option value="${PROJECTLESS_WORKSPACE}">${esc(t("control.other_projects"))}</option>` : "");
|
|
195
|
+
projectSelect.value = [...projectSelect.options].some((option) => option.value === state.workspace) ? state.workspace : "all";
|
|
196
|
+
}
|
|
197
|
+
const controlSort = $("#controlRoomSortSelect");
|
|
198
|
+
if (controlSort) controlSort.value = state.controlRoomSort || "recent";
|
|
199
|
+
const controlSearch = $("#controlRoomSearchInput");
|
|
200
|
+
if (controlSearch && controlSearch.value !== state.search) controlSearch.value = state.search;
|
|
201
|
+
$("#controlRoomSearch")?.classList.toggle("is-open", Boolean(state.search));
|
|
202
|
+
$("#controlRoomSearchBtn")?.setAttribute("aria-expanded", state.search ? "true" : "false");
|
|
203
|
+
if (controlSearch) {
|
|
204
|
+
controlSearch.tabIndex = state.search ? 0 : -1;
|
|
205
|
+
controlSearch.setAttribute("aria-hidden", state.search ? "false" : "true");
|
|
206
|
+
}
|
|
148
207
|
const selectedProject = projects.find((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
|
|
149
208
|
const mobileSummary = $("#mobileWorkspaceSummary");
|
|
150
209
|
if (mobileSummary) mobileSummary.textContent = state.workspace === "all"
|
|
@@ -181,7 +240,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
181
240
|
)
|
|
182
241
|
.join("");
|
|
183
242
|
$("#navAllCount").textContent = rootCount;
|
|
184
|
-
const activeRootCount = sessions.filter((session) => !session.parentId &&
|
|
243
|
+
const activeRootCount = sessions.filter((session) => !session.parentId && isControlRoomSession(session)).length;
|
|
185
244
|
$("#navActiveCount").textContent = activeRootCount;
|
|
186
245
|
const reviewCount = sessions.filter((session) => context.needsManagementReview?.(session)).length;
|
|
187
246
|
$("#navWaitingCount").textContent = reviewCount;
|
|
@@ -426,7 +485,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
426
485
|
function filteredSessions() {
|
|
427
486
|
const allSessions = displaySessions();
|
|
428
487
|
let sessions = state.view === "waiting" ? allSessions : allSessions.filter((session) => !session.parentId);
|
|
429
|
-
if (state.view === "active") sessions = sessions.filter(
|
|
488
|
+
if (state.view === "active") sessions = sessions.filter(isControlRoomSession);
|
|
430
489
|
if (state.view === "waiting") sessions = sessions.filter((session) => context.needsManagementReview?.(session));
|
|
431
490
|
if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
|
|
432
491
|
sessions = sessions.filter(matchesWorkspaceFilter);
|
|
@@ -489,7 +548,9 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
489
548
|
.toLowerCase()
|
|
490
549
|
.includes(query),
|
|
491
550
|
);
|
|
492
|
-
return
|
|
551
|
+
if (state.controlRoomSort === "tokens") return [...sessions].sort((a, b) => Number((b.usage && b.usage.total) || 0) - Number((a.usage && a.usage.total) || 0));
|
|
552
|
+
if (state.controlRoomSort === "context") return [...sessions].sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0));
|
|
553
|
+
return [...sessions].sort((a, b) => Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0));
|
|
493
554
|
}
|
|
494
555
|
|
|
495
556
|
function renderProviderVisibilitySettings() {
|
|
@@ -515,6 +576,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
515
576
|
sessionOriginPath,
|
|
516
577
|
observedProjects,
|
|
517
578
|
sessionWorkspaceLabel,
|
|
579
|
+
controlRoomProject,
|
|
518
580
|
matchesWorkspaceFilter,
|
|
519
581
|
renderWorkspaces,
|
|
520
582
|
renderGlobalStats,
|
|
@@ -84,9 +84,149 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
84
84
|
</details>`;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function subagentCallEvents(session) {
|
|
88
|
+
const spawns = session?.collaboration?.spawns || [];
|
|
89
|
+
const children = (session?.childIds || [])
|
|
90
|
+
.map(id => state.details.get(id) || snapshotSession(id))
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
const recordedChildren = new Set(spawns.map(spawn => spawn.childId).filter(Boolean));
|
|
93
|
+
const records = spawns.concat(children
|
|
94
|
+
.filter(child => !recordedChildren.has(child.id)
|
|
95
|
+
&& !spawns.some(spawn => (spawn.agentPath && child.agentPath === spawn.agentPath)
|
|
96
|
+
|| (spawn.taskName && child.taskName === spawn.taskName)))
|
|
97
|
+
.map(child => ({
|
|
98
|
+
callId: `inferred:${child.id}`,
|
|
99
|
+
childId: child.id,
|
|
100
|
+
agentPath: child.agentPath,
|
|
101
|
+
taskName: child.taskName || child.delegation?.taskName || child.agentName,
|
|
102
|
+
assignment: child.delegation?.assignmentObserved ? child.delegation.assignment : "",
|
|
103
|
+
assignmentProtected: Boolean(child.delegation?.assignmentProtected),
|
|
104
|
+
assignmentSource: child.delegation?.assignmentSource || "unavailable",
|
|
105
|
+
status: child.status,
|
|
106
|
+
startedAt: child.delegation?.startedAt || child.startedAt,
|
|
107
|
+
})));
|
|
108
|
+
const calls = records.map((spawn, index) => {
|
|
109
|
+
const child = (spawn.childId && (state.details.get(spawn.childId) || snapshotSession(spawn.childId)))
|
|
110
|
+
|| children.find(candidate => (spawn.agentPath && candidate.agentPath === spawn.agentPath)
|
|
111
|
+
|| (spawn.taskName && candidate.taskName === spawn.taskName));
|
|
112
|
+
const timestamp = spawn.startedAt || child?.startedAt || session.startedAt || session.updatedAt;
|
|
113
|
+
const assignmentProtected = Boolean(spawn.assignmentProtected || child?.delegation?.assignmentProtected);
|
|
114
|
+
const observedAssignment = spawn.assignmentObserved
|
|
115
|
+
? spawn.assignment
|
|
116
|
+
: (child?.delegation?.assignmentObserved ? child.delegation.assignment : "");
|
|
117
|
+
const childTitle = String(child?.title || "").trim();
|
|
118
|
+
return {
|
|
119
|
+
id: spawn.callId || child?.id || `subagent-call-${index}`,
|
|
120
|
+
childId: child?.id || spawn.childId || "",
|
|
121
|
+
taskName: spawn.taskName || child?.taskName || child?.agentName || t("control.subagent"),
|
|
122
|
+
assignment: observedAssignment,
|
|
123
|
+
workSummary: observedAssignment || (!assignmentProtected && childTitle && childTitle !== session.title ? childTitle : ""),
|
|
124
|
+
assignmentProtected,
|
|
125
|
+
assignmentSource: spawn.assignmentSource || child?.delegation?.assignmentSource || "unavailable",
|
|
126
|
+
status: child?.status || spawn.status || "idle",
|
|
127
|
+
timestamp,
|
|
128
|
+
};
|
|
129
|
+
}).sort((left, right) => Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0));
|
|
130
|
+
const messages = session.messages || [];
|
|
131
|
+
return calls.map((call, index) => {
|
|
132
|
+
const calledAt = Date.parse(call.timestamp || 0);
|
|
133
|
+
const latestUserAt = messages.reduce((latest, message) => {
|
|
134
|
+
const messageAt = Date.parse(message?.timestamp || 0);
|
|
135
|
+
return message?.role === "user" && messageAt <= calledAt ? Math.max(latest, messageAt) : latest;
|
|
136
|
+
}, Number.NEGATIVE_INFINITY);
|
|
137
|
+
const anchor = [...messages].reverse().find(message => {
|
|
138
|
+
const messageAt = Date.parse(message?.timestamp || 0);
|
|
139
|
+
return message?.role === "assistant" && String(message.text || "").trim()
|
|
140
|
+
&& messageAt <= calledAt && messageAt >= latestUserAt;
|
|
141
|
+
});
|
|
142
|
+
const anchorText = String(anchor?.text || "").replace(/\s+/g, " ").trim();
|
|
143
|
+
return {
|
|
144
|
+
...call,
|
|
145
|
+
sequence: index + 1,
|
|
146
|
+
anchorText: anchorText.length > 240 ? `${anchorText.slice(0, 240).trimEnd()}…` : anchorText,
|
|
147
|
+
anchorTimestamp: anchor?.timestamp || "",
|
|
148
|
+
requestTimestamp: Number.isFinite(latestUserAt) ? new Date(latestUserAt).toISOString() : "",
|
|
149
|
+
elapsedAfterRequestMs: Number.isFinite(latestUserAt) && Number.isFinite(calledAt)
|
|
150
|
+
? Math.max(0, calledAt - latestUserAt)
|
|
151
|
+
: null,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function subagentCallElapsed(milliseconds) {
|
|
157
|
+
if (!Number.isFinite(milliseconds)) return "";
|
|
158
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
159
|
+
if (totalSeconds < 1) return t("drawer.duration_less_than_second");
|
|
160
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
161
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
162
|
+
const seconds = totalSeconds % 60;
|
|
163
|
+
if (hours) return t("drawer.duration_hours_minutes_seconds", { hours, minutes, seconds });
|
|
164
|
+
if (minutes) return t("drawer.duration_minutes_seconds", { minutes, seconds });
|
|
165
|
+
return t("drawer.duration_seconds", { seconds });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function subagentCallStatus(status) {
|
|
169
|
+
if (status === "completed") return t("ui.completed");
|
|
170
|
+
if (status === "running" || status === "starting") return t("ui.working");
|
|
171
|
+
if (status === "waiting") return t("ui.waiting_for_review");
|
|
172
|
+
if (status === "failed") return t("ui.problem");
|
|
173
|
+
if (status === "cancelled") return t("ui.stopped");
|
|
174
|
+
return t("ui.idle");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function subagentCallHtml(call, options = {}) {
|
|
178
|
+
const fullTime = new Date(call.timestamp).toLocaleString(uiLocale());
|
|
179
|
+
const assignment = call.workSummary || call.assignment || (call.assignmentProtected ? t("drawer.assignment_protected_short") : call.taskName);
|
|
180
|
+
const elapsed = subagentCallElapsed(call.elapsedAfterRequestMs);
|
|
181
|
+
const timing = elapsed
|
|
182
|
+
? t("drawer.called_after_user_request", { elapsed })
|
|
183
|
+
: t("drawer.subagent_called");
|
|
184
|
+
const anchorTime = call.anchorTimestamp ? timeOnly(call.anchorTimestamp) : "";
|
|
185
|
+
const anchor = call.anchorText
|
|
186
|
+
? `<div class="subagent-call-anchor"><span aria-hidden="true">AI</span><div><small>${esc(t("drawer.called_after_main_message"))}${anchorTime ? ` · ${esc(anchorTime)}` : ""}</small><blockquote>${esc(call.anchorText)}</blockquote></div></div>`
|
|
187
|
+
: `<div class="subagent-call-anchor is-context-only"><span aria-hidden="true">AI</span><div><small>${esc(t("drawer.main_called_here"))}</small></div></div>`;
|
|
188
|
+
const showAnchor = options.showAnchor !== false || !call.anchorText;
|
|
189
|
+
const content = `<span class="subagent-call-icon" aria-hidden="true">⑂</span>
|
|
190
|
+
<span class="subagent-call-copy"><small class="subagent-call-timing">${esc(timing)}</small><span class="subagent-call-clock">${esc(t("drawer.subagent_call_point", { count: call.sequence }))} · <time title="${esc(fullTime)}">${esc(timeOnly(call.timestamp))}</time></span>
|
|
191
|
+
<b>${esc(assignment)}</b><span>${esc(t("drawer.subagent_name", { name: call.taskName }))}</span></span>
|
|
192
|
+
<span class="subagent-call-action"><em class="status-${esc(call.status)}">${esc(subagentCallStatus(call.status))}</em><strong>${esc(t("drawer.open_subagent_work"))} →</strong></span>`;
|
|
193
|
+
const event = !call.childId
|
|
194
|
+
? `<div class="subagent-call-event is-unavailable" data-subagent-call-event="${esc(call.id)}">${content}</div>`
|
|
195
|
+
: `<button type="button" class="subagent-call-event" data-subagent-call-event="${esc(call.id)}"
|
|
196
|
+
data-open-subagent-chat="${esc(call.childId)}" aria-label="${esc(t("control.open_subagent", { task: call.taskName }))}">${content}</button>`;
|
|
197
|
+
return `<div class="subagent-call-moment${showAnchor ? "" : " is-inline"}" data-subagent-call-sequence="${call.sequence}"${Number.isFinite(call.elapsedAfterRequestMs) ? ` data-subagent-call-elapsed-ms="${call.elapsedAfterRequestMs}"` : ""}>${showAnchor ? anchor : ""}<span class="subagent-call-connector" aria-hidden="true"><i></i><b>↓</b></span>${event}</div>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function turnWithSubagentCallsHtml(turn, session, calls, labels) {
|
|
201
|
+
const representativeId = turn.representative?.id || "";
|
|
202
|
+
const items = [
|
|
203
|
+
...turn.assistants.map((message, index) => ({ kind: "message", message, index, timestamp: message.timestamp })),
|
|
204
|
+
...calls.map((call, index) => ({ kind: "call", call, index, timestamp: call.timestamp })),
|
|
205
|
+
].sort((left, right) => {
|
|
206
|
+
const leftAt = Date.parse(left.timestamp || 0);
|
|
207
|
+
const rightAt = Date.parse(right.timestamp || 0);
|
|
208
|
+
if (leftAt !== rightAt) return leftAt - rightAt;
|
|
209
|
+
if (left.kind !== right.kind) return left.kind === "message" ? -1 : 1;
|
|
210
|
+
return left.index - right.index;
|
|
211
|
+
});
|
|
212
|
+
return items.map(item => {
|
|
213
|
+
if (item.kind === "call") return subagentCallHtml(item.call, { showAnchor: false });
|
|
214
|
+
const finalMessage = item.message.id === representativeId;
|
|
215
|
+
return conversationRowHtml(item.message, session, {
|
|
216
|
+
userLabel: labels.userLabel,
|
|
217
|
+
assistantLabel: labels.assistantLabel,
|
|
218
|
+
answerKind: finalMessage
|
|
219
|
+
? t(turn.live ? "drawer.current_progress" : turn.awaitingFinal ? "drawer.last_progress" : "drawer.final_answer")
|
|
220
|
+
: t("drawer.main_progress_before_call"),
|
|
221
|
+
live: turn.live && finalMessage,
|
|
222
|
+
});
|
|
223
|
+
}).join("");
|
|
224
|
+
}
|
|
225
|
+
|
|
87
226
|
function chatHtml(session, options = {}) {
|
|
88
227
|
const messages = session.messages || [];
|
|
89
|
-
|
|
228
|
+
const calls = options.showSubagentCalls === false ? [] : subagentCallEvents(session);
|
|
229
|
+
if (!messages.length && !calls.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_conversation"))}</h3></div>`;
|
|
90
230
|
const userLabel = options.userLabel || t("drawer.user");
|
|
91
231
|
const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
|
|
92
232
|
const conversationLabel = options.conversationLabel || t("drawer.conversation");
|
|
@@ -97,7 +237,7 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
97
237
|
omitted || session.truncated
|
|
98
238
|
? `<div class="chat-truncated">${esc(t("drawer.recent_history"))}${omitted ? ` · ${esc(t("drawer.messages_omitted", { count: omitted.toLocaleString(uiLocale()) }))}` : ""}</div>`
|
|
99
239
|
: "";
|
|
100
|
-
const rows = turns.map((turn) => {
|
|
240
|
+
const rows = turns.map((turn, turnIndex) => {
|
|
101
241
|
const user = conversationRowHtml(turn.user, session, { userLabel, assistantLabel });
|
|
102
242
|
const representative = conversationRowHtml(turn.representative, session, {
|
|
103
243
|
userLabel,
|
|
@@ -108,16 +248,35 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
108
248
|
const waiting = turn.live && !turn.representative
|
|
109
249
|
? `<div class="chat-turn-waiting"><span aria-hidden="true"></span><b>${esc(t("drawer.preparing_response"))}</b></div>`
|
|
110
250
|
: "";
|
|
251
|
+
const turnStartedAt = Date.parse(turn.user?.timestamp || turn.representative?.timestamp || 0);
|
|
252
|
+
const nextTurnTimestamp = turns[turnIndex + 1]?.user?.timestamp;
|
|
253
|
+
const nextTurnStartedAt = nextTurnTimestamp ? Date.parse(nextTurnTimestamp) : Number.NaN;
|
|
254
|
+
const turnCalls = calls.filter(call => {
|
|
255
|
+
const calledAt = Date.parse(call.timestamp || 0);
|
|
256
|
+
if (Number.isFinite(turnStartedAt) && calledAt < turnStartedAt) return false;
|
|
257
|
+
return !Number.isFinite(nextTurnStartedAt) || calledAt < nextTurnStartedAt;
|
|
258
|
+
});
|
|
259
|
+
const timeline = turnCalls.length
|
|
260
|
+
? turnWithSubagentCallsHtml(turn, session, turnCalls, { userLabel, assistantLabel })
|
|
261
|
+
: `${representative}${waiting}${progressUpdatesHtml(turn, session)}`;
|
|
111
262
|
return `<section class="chat-turn${turn.live ? " is-live" : ""}" data-conversation-turn="${esc(turn.id)}">
|
|
112
|
-
${user}${
|
|
263
|
+
${user}${timeline}${turnCalls.length ? waiting : ""}
|
|
113
264
|
</section>`;
|
|
114
265
|
}).join("");
|
|
115
|
-
const
|
|
266
|
+
const unmatchedCalls = calls.filter(call => !turns.some((turn, turnIndex) => {
|
|
267
|
+
const startedAt = Date.parse(turn.user?.timestamp || turn.representative?.timestamp || 0);
|
|
268
|
+
const nextTimestamp = turns[turnIndex + 1]?.user?.timestamp;
|
|
269
|
+
const nextStartedAt = nextTimestamp ? Date.parse(nextTimestamp) : Number.NaN;
|
|
270
|
+
const calledAt = Date.parse(call.timestamp || 0);
|
|
271
|
+
return (!Number.isFinite(startedAt) || calledAt >= startedAt) && (!Number.isFinite(nextStartedAt) || calledAt < nextStartedAt);
|
|
272
|
+
}));
|
|
273
|
+
const callOnlyRows = unmatchedCalls.map(subagentCallHtml).join("");
|
|
274
|
+
const emptyConversation = turns.length || calls.length ? "" : `<div class="empty-state compact"><h3>${esc(t("drawer.no_user_ai_conversation"))}</h3></div>`;
|
|
116
275
|
return `${notice}<div class="chat-history-head">
|
|
117
276
|
<span>${esc(t("drawer.turn_summary", { label: conversationLabel, count: turns.length, updates: progressCount ? ` · ${t("drawer.progress_updates", { count: progressCount })}` : "" }))}</span>
|
|
118
277
|
<button type="button" data-scroll-latest>${esc(t("drawer.latest_conversation"))} ↓</button>
|
|
119
278
|
</div>
|
|
120
|
-
<div class="chat-list">${rows}${emptyConversation}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
|
|
279
|
+
<div class="chat-list">${callOnlyRows}${rows}${emptyConversation}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
|
|
121
280
|
</div>
|
|
122
281
|
</div>`;
|
|
123
282
|
}
|
|
@@ -265,7 +424,23 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
265
424
|
const delegation = session.delegation || {};
|
|
266
425
|
const parent = session.parentId ? state.details.get(session.parentId) || snapshotSession(session.parentId) : null;
|
|
267
426
|
const assignmentEvent = subagentCoordinationEvents(session).find(event => event.kind === "assignment" && String(event.text || "").trim());
|
|
268
|
-
const
|
|
427
|
+
const delegatedAssignment = delegation.assignmentObserved && String(delegation.assignment || "").trim()
|
|
428
|
+
? delegation.assignment
|
|
429
|
+
: "";
|
|
430
|
+
const eventAssignment = assignmentEvent && !assignmentEvent.protected ? assignmentEvent.text : "";
|
|
431
|
+
const assignment = String(delegatedAssignment || eventAssignment || "").trim();
|
|
432
|
+
const assignmentProtected = Boolean(delegation.assignmentProtected || assignmentEvent?.protected);
|
|
433
|
+
const assignmentContext = String(delegation.assignmentContext || "").trim();
|
|
434
|
+
const assignmentBody = assignment || (assignmentProtected
|
|
435
|
+
? t("drawer.assignment_protected")
|
|
436
|
+
: t("management.signal_unavailable"));
|
|
437
|
+
const assignmentSource = delegation.assignmentSource === "claude-agent-prompt"
|
|
438
|
+
? t("drawer.assignment_source_claude")
|
|
439
|
+
: delegation.assignmentSource === "spawn-message"
|
|
440
|
+
? t("drawer.assignment_source_codex")
|
|
441
|
+
: assignmentProtected
|
|
442
|
+
? t("drawer.assignment_source_protected")
|
|
443
|
+
: "";
|
|
269
444
|
let assignmentRemoved = false;
|
|
270
445
|
const messages = subagentWorkMessages(session).filter(message => {
|
|
271
446
|
if (assignmentRemoved || message.role !== "user" || !assignment) return true;
|
|
@@ -279,7 +454,8 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
279
454
|
? t("drawer.subagent_history_reconstructed")
|
|
280
455
|
: t("drawer.subagent_history_actual");
|
|
281
456
|
return `<section class="subagent-assignment-card" data-subagent-assignment="true">
|
|
282
|
-
<span aria-hidden="true">⌁</span><div><b>${esc(t("control.main_assignment"))}</b>${parent ? `<small>${esc(t("control.created_from"))} · ${esc(parent.title)}</small>` : ""}
|
|
457
|
+
<span aria-hidden="true">⌁</span><div><b>${esc(t("control.main_assignment"))}</b>${parent ? `<small>${esc(t("control.created_from"))} · ${esc(parent.title)}</small>` : ""}${assignmentSource ? `<small>${esc(assignmentSource)}</small>` : ""}<p>${esc(assignmentBody)}</p>
|
|
458
|
+
${assignmentContext ? `<aside><b>${esc(t("drawer.assignment_context"))}</b><p>${esc(assignmentContext)}</p></aside>` : ""}</div>
|
|
283
459
|
</section><section class="subagent-work-source" data-subagent-work-messages="${conversationCount}" data-conversation-scope="subagent-only">
|
|
284
460
|
<b>${esc(t("control.subagent_conversation"))}</b><span>${esc(sourceCopy)}</span>
|
|
285
461
|
</section>${chatHtml(workSession, {
|
|
@@ -306,7 +482,12 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
306
482
|
: `${t("control.main_agent")} · ${session.agentName || providerInfo(session.provider).label}`;
|
|
307
483
|
const timeline = [
|
|
308
484
|
activity.startedAt ? { label: t("drawer.execution_started"), value: activity.startedAt } : null,
|
|
309
|
-
activity.updatedAt ? {
|
|
485
|
+
activity.updatedAt ? {
|
|
486
|
+
label: activity.status === "running" || activity.status === "unverified"
|
|
487
|
+
? t("drawer.execution_latest_activity")
|
|
488
|
+
: t("drawer.execution_finished"),
|
|
489
|
+
value: activity.updatedAt,
|
|
490
|
+
} : null,
|
|
310
491
|
].filter(Boolean);
|
|
311
492
|
return `<div class="execution-drawer" data-execution-detail="${esc(activity.id)}" data-conversation-scope="execution-only">
|
|
312
493
|
<section class="execution-purpose-card">
|
package/renderer/app-drawer.js
CHANGED
|
@@ -6,7 +6,7 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
6
6
|
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
7
7
|
const {
|
|
8
8
|
$, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState,
|
|
9
|
-
providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionOriginPath, sessionWorkspaceLabel,
|
|
9
|
+
providerInfo, isLiveSession, controlRoomStatus = session => session?.status, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionOriginPath, sessionWorkspaceLabel,
|
|
10
10
|
agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
|
|
11
11
|
chatHtml, lifecycleHtml, tokensHtml, outcomeHtml, subagentCoordinationEvents, subagentConversationHtml, executionActivityDetailHtml,
|
|
12
12
|
agentCommandComposer,
|
|
@@ -97,6 +97,7 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
97
97
|
const session = selectedSession();
|
|
98
98
|
if (!session) return closeDrawer();
|
|
99
99
|
const provider = providerInfo(session.provider);
|
|
100
|
+
const presentationStatus = controlRoomStatus(session);
|
|
100
101
|
const subagentMode = state.drawerMode === "subagent" && Boolean(session.parentId);
|
|
101
102
|
const executionMode = state.drawerMode === "execution" && Boolean(state.drawerExecutionId);
|
|
102
103
|
const snapshot = snapshotSession(session.id);
|
|
@@ -115,8 +116,8 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
115
116
|
$("#drawerProvider").textContent = executionMode
|
|
116
117
|
? `${activity?.runtime || activity?.tool || t("drawer.execution_unit")} · ${activity ? context.executionActivityStatus(activity) : t("drawer.unknown")}`
|
|
117
118
|
: subagentMode
|
|
118
|
-
? `${t("control.subagent")} · ${STATUS[
|
|
119
|
-
: `${provider.company} · ${STATUS[
|
|
119
|
+
? `${t("control.subagent")} · ${STATUS[presentationStatus] || presentationStatus}`
|
|
120
|
+
: `${provider.company} · ${STATUS[presentationStatus] || presentationStatus}`;
|
|
120
121
|
const drawerTitle = executionMode
|
|
121
122
|
? context.inferredExecutionSummary(activity || {}).text
|
|
122
123
|
: subagentMode ? session.title || session.taskName || (session.delegation && session.delegation.taskName) : session.title;
|
|
@@ -9,7 +9,7 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
|
|
|
9
9
|
closeDrawer, renderDrawer, providerPickerHtml, syncRunComposer, openRunModal, closeRunModal, toast, performUiAction,
|
|
10
10
|
handleRun, trapDialogFocus, currentDialog, selectView, saveRunDraft = () => {}, safeBackdrop = null,
|
|
11
11
|
copyText = async () => false,
|
|
12
|
-
dispatchAgentCommand, controlManagedRun, quickRespond, prepareReassignment,
|
|
12
|
+
dispatchAgentCommand, controlManagedRun, quickRespond, prepareReassignment, openSubagentConversation,
|
|
13
13
|
} = context;
|
|
14
14
|
|
|
15
15
|
function bindRunComposerEvents() {
|
|
@@ -161,6 +161,11 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
|
|
|
161
161
|
$(`.drawer-tab[data-tab="${state.drawerTab}"]`)?.focus();
|
|
162
162
|
});
|
|
163
163
|
$("#detailDrawer").addEventListener("click", async (event) => {
|
|
164
|
+
const subagent = event.target.closest("[data-open-subagent-chat]");
|
|
165
|
+
if (subagent) {
|
|
166
|
+
openSubagentConversation(subagent.dataset.openSubagentChat);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
164
169
|
const route = event.target.closest("[data-agent-command-route]");
|
|
165
170
|
if (route) {
|
|
166
171
|
state.agentCommandRoutes.set(route.dataset.agentCommandSession, route.dataset.agentCommandRoute);
|
|
@@ -9,7 +9,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
9
9
|
function bindFilterAndWorkspaceEvents() {
|
|
10
10
|
const syncFilterResetButton = () => {
|
|
11
11
|
const hasFilters = Boolean(
|
|
12
|
-
$("#searchInput").value || state.search || state.providerFilters.size || state.workspace !== "all" || state.sort !== "recent",
|
|
12
|
+
$("#searchInput").value || state.search || state.providerFilters.size || state.workspace !== "all" || state.sort !== "recent" || state.controlRoomSort !== "recent",
|
|
13
13
|
);
|
|
14
14
|
$("#resetFiltersBtn").classList.toggle("hidden", !hasFilters);
|
|
15
15
|
};
|
|
@@ -62,6 +62,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
62
62
|
if (item) {
|
|
63
63
|
const label = item.querySelector("strong")?.textContent.trim() || t("project.all");
|
|
64
64
|
state.workspace = item.dataset.workspace;
|
|
65
|
+
state.controlRoomPage = 0;
|
|
65
66
|
state.visibleLimit = 30;
|
|
66
67
|
renderWorkspaces();
|
|
67
68
|
renderSessions("filter");
|
|
@@ -85,9 +86,80 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
85
86
|
workspaceLists.forEach((list) => {
|
|
86
87
|
list.addEventListener("click", handleWorkspaceClick);
|
|
87
88
|
list.addEventListener("keydown", (event) => {
|
|
88
|
-
|
|
89
|
+
const horizontal = event.currentTarget.id === "workspaceList";
|
|
90
|
+
moveFocus(event, event.currentTarget, "[data-workspace]", horizontal ? ["ArrowLeft", "ArrowUp"] : ["ArrowUp"], horizontal ? ["ArrowRight", "ArrowDown"] : ["ArrowDown"]);
|
|
89
91
|
});
|
|
90
92
|
});
|
|
93
|
+
const controlProjectSelect = $("#controlRoomProjectSelect");
|
|
94
|
+
controlProjectSelect?.addEventListener("change", (event) => {
|
|
95
|
+
state.workspace = event.target.value;
|
|
96
|
+
state.controlRoomPage = 0;
|
|
97
|
+
state.visibleLimit = 30;
|
|
98
|
+
renderWorkspaces();
|
|
99
|
+
renderSessions("filter");
|
|
100
|
+
syncFilterResetButton();
|
|
101
|
+
saveDashboardPreferences();
|
|
102
|
+
announce(t("filter.workspace_results", { project: event.target.selectedOptions[0]?.textContent || t("control.all_projects"), count: filteredSessions().length }));
|
|
103
|
+
});
|
|
104
|
+
const controlSortSelect = $("#controlRoomSortSelect");
|
|
105
|
+
controlSortSelect?.addEventListener("change", (event) => {
|
|
106
|
+
state.controlRoomSort = event.target.value;
|
|
107
|
+
state.controlRoomPage = 0;
|
|
108
|
+
state.visibleLimit = 30;
|
|
109
|
+
renderSessions("filter");
|
|
110
|
+
syncFilterResetButton();
|
|
111
|
+
saveDashboardPreferences();
|
|
112
|
+
announce(t("filter.sort_changed", { sort: event.target.selectedOptions[0]?.textContent || event.target.value, count: filteredSessions().length }));
|
|
113
|
+
});
|
|
114
|
+
const controlSearch = $("#controlRoomSearch");
|
|
115
|
+
const controlSearchInput = $("#controlRoomSearchInput");
|
|
116
|
+
const controlSearchButton = $("#controlRoomSearchBtn");
|
|
117
|
+
let controlSearchTimer = null;
|
|
118
|
+
const setControlSearchOpen = (open) => {
|
|
119
|
+
controlSearch?.classList.toggle("is-open", open);
|
|
120
|
+
controlSearchButton?.setAttribute("aria-expanded", open ? "true" : "false");
|
|
121
|
+
if (controlSearchInput) {
|
|
122
|
+
controlSearchInput.tabIndex = open ? 0 : -1;
|
|
123
|
+
controlSearchInput.setAttribute("aria-hidden", open ? "false" : "true");
|
|
124
|
+
}
|
|
125
|
+
if (open) requestAnimationFrame(() => controlSearchInput?.focus());
|
|
126
|
+
};
|
|
127
|
+
controlSearchButton?.addEventListener("click", () => setControlSearchOpen(!controlSearch?.classList.contains("is-open")));
|
|
128
|
+
controlSearchInput?.addEventListener("input", (event) => {
|
|
129
|
+
clearTimeout(controlSearchTimer);
|
|
130
|
+
const value = event.target.value;
|
|
131
|
+
if ($("#searchInput")) $("#searchInput").value = value;
|
|
132
|
+
controlSearchTimer = setTimeout(() => {
|
|
133
|
+
state.search = normalizedSearch(value);
|
|
134
|
+
state.controlRoomPage = 0;
|
|
135
|
+
state.visibleLimit = 30;
|
|
136
|
+
renderSessions("filter");
|
|
137
|
+
syncFilterResetButton();
|
|
138
|
+
saveDashboardPreferences();
|
|
139
|
+
announce(t("filter.search_results", { count: filteredSessions().length }));
|
|
140
|
+
}, 120);
|
|
141
|
+
});
|
|
142
|
+
controlSearchInput?.addEventListener("keydown", (event) => {
|
|
143
|
+
if (event.key !== "Escape") return;
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
if (event.currentTarget.value) {
|
|
146
|
+
event.currentTarget.value = "";
|
|
147
|
+
event.currentTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
|
148
|
+
} else {
|
|
149
|
+
setControlSearchOpen(false);
|
|
150
|
+
controlSearchButton?.focus();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
$("#controlRoomPagePrev")?.addEventListener("click", (event) => {
|
|
154
|
+
state.controlRoomPage = Math.max(0, Number(state.controlRoomPage || 0) - 1);
|
|
155
|
+
renderSessions("filter");
|
|
156
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
157
|
+
});
|
|
158
|
+
$("#controlRoomPageNext")?.addEventListener("click", (event) => {
|
|
159
|
+
state.controlRoomPage = Math.max(0, Number(state.controlRoomPage || 0) + 1);
|
|
160
|
+
renderSessions("filter");
|
|
161
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
162
|
+
});
|
|
91
163
|
let searchTimer = null;
|
|
92
164
|
$("#searchInput").addEventListener("input", (event) => {
|
|
93
165
|
clearTimeout(searchTimer);
|
|
@@ -96,6 +168,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
96
168
|
syncFilterResetButton();
|
|
97
169
|
searchTimer = setTimeout(() => {
|
|
98
170
|
state.search = normalizedSearch(value);
|
|
171
|
+
state.controlRoomPage = 0;
|
|
99
172
|
state.visibleLimit = 30;
|
|
100
173
|
renderSessions("filter");
|
|
101
174
|
announce(window.LoadToAgentI18n.t("filter.search_results", { count: filteredSessions().length }));
|
|
@@ -108,6 +181,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
108
181
|
$("#searchInput").value = "";
|
|
109
182
|
$("#searchClearBtn").classList.add("hidden");
|
|
110
183
|
state.search = "";
|
|
184
|
+
state.controlRoomPage = 0;
|
|
111
185
|
state.visibleLimit = 30;
|
|
112
186
|
renderSessions("filter");
|
|
113
187
|
announce(window.LoadToAgentI18n.t("filter.search_cleared"));
|
|
@@ -138,6 +212,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
138
212
|
const chip = event.target.closest("[data-provider-filter]");
|
|
139
213
|
if (!chip) return;
|
|
140
214
|
toggleProviderFilter(chip.dataset.providerFilter);
|
|
215
|
+
state.controlRoomPage = 0;
|
|
141
216
|
state.visibleLimit = 30;
|
|
142
217
|
renderProviderFilter();
|
|
143
218
|
renderProviderOverview();
|
|
@@ -154,6 +229,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
154
229
|
});
|
|
155
230
|
$("#sortSelect").addEventListener("change", (event) => {
|
|
156
231
|
state.sort = event.target.value;
|
|
232
|
+
state.controlRoomPage = 0;
|
|
157
233
|
state.visibleLimit = 30;
|
|
158
234
|
renderSessions("filter");
|
|
159
235
|
const label = event.target.selectedOptions[0]?.textContent || event.target.value;
|
|
@@ -167,6 +243,8 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
167
243
|
state.providerFilters.clear();
|
|
168
244
|
state.workspace = "all";
|
|
169
245
|
state.sort = "recent";
|
|
246
|
+
state.controlRoomSort = "recent";
|
|
247
|
+
state.controlRoomPage = 0;
|
|
170
248
|
state.visibleLimit = 30;
|
|
171
249
|
$("#searchInput").value = "";
|
|
172
250
|
$("#searchClearBtn").classList.add("hidden");
|