codex-agent-view 0.4.3 → 0.4.5
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/.codex-plugin/plugin.json +5 -8
- package/README.ko.md +31 -26
- package/README.md +31 -26
- package/package.json +2 -2
- package/public/app.js +706 -147
- package/public/index.html +62 -52
- package/public/styles.css +92 -17
- package/scripts/send-hook.mjs +10 -3
- package/skills/codex-agent-view/SKILL.md +63 -43
- package/skills/show-agents/SKILL.md +56 -20
- package/src/core/index.mjs +0 -1
- package/src/core/monitor-store.mjs +20 -0
- package/src/core/normalize-hook-payload.mjs +78 -0
package/public/app.js
CHANGED
|
@@ -1,13 +1,420 @@
|
|
|
1
1
|
const API_STATE_URL = "/api/state";
|
|
2
2
|
const POLL_INTERVAL_MS = 2_000;
|
|
3
3
|
const SESSION_TOKEN_KEY = "codex-agent-view-access-token";
|
|
4
|
+
const EXCLUDED_SESSION_KEY = "codex-agent-view-excluded-session";
|
|
5
|
+
const LANGUAGE_KEY = "codex-agent-view-language";
|
|
6
|
+
const SUPPORTED_LANGUAGES = new Set(["en", "ko", "es"]);
|
|
4
7
|
const KNOWN_STATUSES = new Set(["running", "waiting", "completed", "unknown"]);
|
|
8
|
+
const VIEWER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
const CANONICAL_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
10
|
+
|
|
11
|
+
const MESSAGES = Object.freeze({
|
|
12
|
+
en: Object.freeze({
|
|
13
|
+
metaDescription: "A clear, live view of Codex work and the agents moving it forward.",
|
|
14
|
+
skipToContent: "Skip to content",
|
|
15
|
+
brandHome: "Codex Agent View home",
|
|
16
|
+
brandSubtitle: "Live work overview",
|
|
17
|
+
languageLabel: "Language",
|
|
18
|
+
connectionConnecting: "Connecting to local status",
|
|
19
|
+
connectionConnected: "Local monitor connected",
|
|
20
|
+
connectionRetrying: "Disconnected · retrying",
|
|
21
|
+
authenticationRequired: "Live view authentication required",
|
|
22
|
+
heroEyebrow: "LIVE CODEX WORK",
|
|
23
|
+
heroTitle: "See active work at a glance",
|
|
24
|
+
heroCopy: "See what Codex is working on, which agents are involved, and how each part is progressing—all in one place.",
|
|
25
|
+
freshnessAria: "Refresh information",
|
|
26
|
+
lastUpdatedLabel: "Last updated",
|
|
27
|
+
notYet: "Not yet",
|
|
28
|
+
refreshIntervalLabel: "Refresh interval",
|
|
29
|
+
twoSeconds: "2 seconds",
|
|
30
|
+
metricsAria: "Task status summary",
|
|
31
|
+
parentTasks: "Work items",
|
|
32
|
+
currentlyObserved: "Currently observed",
|
|
33
|
+
runningAgents: "Active agents",
|
|
34
|
+
workingNow: "Working now",
|
|
35
|
+
waitingStatus: "Waiting",
|
|
36
|
+
waitingExplanation: "Waiting for input or the next step",
|
|
37
|
+
completedAgents: "Completed agents",
|
|
38
|
+
currentView: "In the current view",
|
|
39
|
+
liveWork: "LIVE WORK",
|
|
40
|
+
sessionsHeading: "Work and participating agents",
|
|
41
|
+
loadingState: "Loading status.",
|
|
42
|
+
toolbarAria: "Filter automatically observed work and participating agents",
|
|
43
|
+
searchLabel: "Filter list (optional)",
|
|
44
|
+
searchPlaceholder: "Find work or an agent",
|
|
45
|
+
statusFilterLabel: "Status filter (optional)",
|
|
46
|
+
statusAll: "All statuses",
|
|
47
|
+
statusRunning: "Running",
|
|
48
|
+
statusWaiting: "Waiting",
|
|
49
|
+
statusCompleted: "Completed",
|
|
50
|
+
statusUnknown: "Unknown",
|
|
51
|
+
connectingCopy: "Connecting to the Codex app's local status.",
|
|
52
|
+
sessionListAria: "Codex work list",
|
|
53
|
+
privacyPrompt: "This view shows only a shortened request summary; it never displays the full request or tool inputs.",
|
|
54
|
+
privacyLocal: "Data is read only from the local monitor on this device.",
|
|
55
|
+
timeUnknown: "Time unavailable",
|
|
56
|
+
startedUnknown: "Start time unavailable",
|
|
57
|
+
noReceivedActivity: "No activity received",
|
|
58
|
+
parentTask: "WORK",
|
|
59
|
+
projectUnknown: "Project unavailable",
|
|
60
|
+
taskSummary: "Request",
|
|
61
|
+
taskSummaryUnavailable: "A short request summary is not available yet.",
|
|
62
|
+
recentActivity: "Recent activity",
|
|
63
|
+
recent: "Recent",
|
|
64
|
+
subagentsCount: "Participating agents · {count}",
|
|
65
|
+
subagentName: "Agent {ordinal}",
|
|
66
|
+
agentProfile: "Role/profile · {profile}",
|
|
67
|
+
agentProfileNote: "Codex currently provides each agent's role, but not its full assignment description.",
|
|
68
|
+
noSubagents: "No participating agents have been observed for this work item.",
|
|
69
|
+
noRecentActivity: "No recent activity to display.",
|
|
70
|
+
technicalInfo: "Technical information",
|
|
71
|
+
agentId: "Agent ID",
|
|
72
|
+
rawProfile: "Raw role/profile",
|
|
73
|
+
rawEvent: "Raw event",
|
|
74
|
+
rawTool: "Raw tool",
|
|
75
|
+
retry: "Retry connection",
|
|
76
|
+
retryAuthentication: "Try this tab again",
|
|
77
|
+
checkAuthentication: "Check authentication again",
|
|
78
|
+
recoveryTitle: "Open a newly authenticated view",
|
|
79
|
+
recoveryStep: "In the Codex app, select @codex-agent-view in the composer, then choose the actual $show-agents skill from the skill picker.",
|
|
80
|
+
recoveryNote: "The skill opens a new live view with fresh authentication. No terminal command or external browser is needed.",
|
|
81
|
+
resultsFiltered: "Showing {visible} of {total}",
|
|
82
|
+
resultsTotal: "{count} work items",
|
|
83
|
+
searchEmptyTitle: "No matching results.",
|
|
84
|
+
searchEmptyCopy: "Try changing the search term or status filter.",
|
|
85
|
+
emptyWithDiagnosticsTitle: "No work can be displayed.",
|
|
86
|
+
emptyWithDiagnosticsCopy: "The local monitor received {count} activity records but could not apply them to displayable work items.",
|
|
87
|
+
emptyTitle: "No task activity has been received in this observation window.",
|
|
88
|
+
emptyCopy: "The local monitor is connected. This result alone does not mean that Codex has no active work or participating agents.",
|
|
89
|
+
emptyGuidanceTitle: "If work does not appear",
|
|
90
|
+
automaticTracking: "You do not need to enter or register task IDs. Activity from trusted hooks is added automatically.",
|
|
91
|
+
emptyStep1: "After installing the plugin, fully restart the official Codex app.",
|
|
92
|
+
emptyStep2: "In a new task, review and explicitly trust the Codex Agent View hook command.",
|
|
93
|
+
emptyStep3: "After trusting it, start new work and run agents. New activity is added automatically.",
|
|
94
|
+
observationBoundary: "Observation starts with the first trusted hook event. Earlier activity and activity missed while local collection was stopped cannot be replayed; restarting collection opens a new observation window.",
|
|
95
|
+
diagnosticsCount: "Validation information · {count}",
|
|
96
|
+
diagnosticOccurrences: "{count} occurrences",
|
|
97
|
+
disconnectedTitle: "Local status disconnected; retrying.",
|
|
98
|
+
authTitle: "This live view cannot be authenticated.",
|
|
99
|
+
retryWithState: "Reconnecting automatically every 2 seconds while keeping the last good state visible.",
|
|
100
|
+
retryWithoutState: "Reconnecting automatically every 2 seconds. You can leave this view open in the Codex app.",
|
|
101
|
+
missingToken: "This tab does not have the authentication needed to display live work.",
|
|
102
|
+
expiredToken: "This tab's live-view authentication was rejected or is no longer valid.",
|
|
103
|
+
requestFailed: "Status request failed ({status})",
|
|
104
|
+
unknownConnectionError: "An unknown connection error occurred.",
|
|
105
|
+
offline: "This device is offline.",
|
|
106
|
+
invalidState: "The status response format is invalid.",
|
|
107
|
+
durationSeconds: "{count}s",
|
|
108
|
+
durationMinutes: "{count}m",
|
|
109
|
+
durationHours: "{count}h",
|
|
110
|
+
durationHoursMinutes: "{hours}h {minutes}m",
|
|
111
|
+
toolFallback: "Tool · {name}",
|
|
112
|
+
activityFallback: "Activity · {name}",
|
|
113
|
+
nameUnknown: "Unknown name",
|
|
114
|
+
activityUnknown: "Unknown",
|
|
115
|
+
approvalRequest: "{tool} approval requested",
|
|
116
|
+
agentStarted: "Agent {ordinal} started",
|
|
117
|
+
agentStopped: "Agent {ordinal} completed",
|
|
118
|
+
activitySessionStarted: "Observation started",
|
|
119
|
+
activitySessionEnded: "Observation ended",
|
|
120
|
+
activityTurnStarted: "Work started",
|
|
121
|
+
activityTurnStopped: "Work response completed",
|
|
122
|
+
activitySubagentStarted: "Agent started",
|
|
123
|
+
activitySubagentStopped: "Agent completed",
|
|
124
|
+
activityToolStarted: "Tool activity",
|
|
125
|
+
activityToolCompleted: "Tool activity",
|
|
126
|
+
activityPermissionRequested: "User approval requested",
|
|
127
|
+
toolApplyPatch: "File edit",
|
|
128
|
+
toolBash: "Terminal activity",
|
|
129
|
+
toolFollowup: "Agent follow-up requested",
|
|
130
|
+
toolInterrupt: "Agent interruption requested",
|
|
131
|
+
toolList: "Agent status checked",
|
|
132
|
+
toolMessage: "Message sent to agent",
|
|
133
|
+
toolSpawn: "Agent started",
|
|
134
|
+
toolWaitAgent: "Waiting for agent",
|
|
135
|
+
toolExec: "Terminal activity",
|
|
136
|
+
toolUserInput: "User input requested",
|
|
137
|
+
toolWait: "Waiting for completion",
|
|
138
|
+
toolStdin: "Terminal input",
|
|
139
|
+
roleExplorer: "Research",
|
|
140
|
+
roleReviewer: "Review",
|
|
141
|
+
roleWorker: "Implementation",
|
|
142
|
+
roleDefault: "General agent",
|
|
143
|
+
roleUnknown: "Not reported",
|
|
144
|
+
}),
|
|
145
|
+
ko: Object.freeze({
|
|
146
|
+
metaDescription: "Codex가 수행 중인 작업과 참여 에이전트의 진행 상황을 한눈에 확인합니다.",
|
|
147
|
+
skipToContent: "본문으로 건너뛰기",
|
|
148
|
+
brandHome: "Codex Agent View 홈",
|
|
149
|
+
brandSubtitle: "실시간 작업 현황",
|
|
150
|
+
languageLabel: "언어",
|
|
151
|
+
connectionConnecting: "로컬 상태 연결 중",
|
|
152
|
+
connectionConnected: "로컬 모니터 연결됨",
|
|
153
|
+
connectionRetrying: "연결 끊김 · 재시도 중",
|
|
154
|
+
authenticationRequired: "실시간 화면 인증 필요",
|
|
155
|
+
heroEyebrow: "CODEX 작업 현황",
|
|
156
|
+
heroTitle: "작업 흐름을 한눈에",
|
|
157
|
+
heroCopy: "Codex가 어떤 요청을 처리하고 있는지, 어떤 에이전트가 참여하는지, 각 단계가 어디까지 왔는지 한 화면에서 확인하세요.",
|
|
158
|
+
freshnessAria: "상태 갱신 정보",
|
|
159
|
+
lastUpdatedLabel: "마지막 갱신",
|
|
160
|
+
notYet: "아직 없음",
|
|
161
|
+
refreshIntervalLabel: "갱신 주기",
|
|
162
|
+
twoSeconds: "2초",
|
|
163
|
+
metricsAria: "작업 상태 요약",
|
|
164
|
+
parentTasks: "작업",
|
|
165
|
+
currentlyObserved: "현재 관찰 중",
|
|
166
|
+
runningAgents: "실행 중 에이전트",
|
|
167
|
+
workingNow: "작업 수행 중",
|
|
168
|
+
waitingStatus: "대기 상태",
|
|
169
|
+
waitingExplanation: "사용자 응답 또는 다음 작업 대기",
|
|
170
|
+
completedAgents: "완료 에이전트",
|
|
171
|
+
currentView: "현재 화면 기준",
|
|
172
|
+
liveWork: "실시간 작업",
|
|
173
|
+
sessionsHeading: "작업과 참여 에이전트",
|
|
174
|
+
loadingState: "상태를 불러오는 중입니다.",
|
|
175
|
+
toolbarAria: "자동 수신된 작업과 참여 에이전트 목록 필터",
|
|
176
|
+
searchLabel: "목록 필터 (선택)",
|
|
177
|
+
searchPlaceholder: "작업 또는 에이전트 찾기",
|
|
178
|
+
statusFilterLabel: "상태 필터 (선택)",
|
|
179
|
+
statusAll: "모든 상태",
|
|
180
|
+
statusRunning: "실행 중",
|
|
181
|
+
statusWaiting: "대기",
|
|
182
|
+
statusCompleted: "완료",
|
|
183
|
+
statusUnknown: "알 수 없음",
|
|
184
|
+
connectingCopy: "Codex 앱의 로컬 상태에 연결하고 있습니다.",
|
|
185
|
+
sessionListAria: "Codex 작업 목록",
|
|
186
|
+
privacyPrompt: "이 화면은 짧게 줄인 요청 요약만 표시하며, 전체 요청이나 도구 입력은 표시하지 않습니다.",
|
|
187
|
+
privacyLocal: "데이터는 이 기기의 로컬 모니터에서만 읽습니다.",
|
|
188
|
+
timeUnknown: "시간 정보 없음",
|
|
189
|
+
startedUnknown: "시작 시간 없음",
|
|
190
|
+
noReceivedActivity: "수신된 활동 없음",
|
|
191
|
+
parentTask: "작업",
|
|
192
|
+
projectUnknown: "프로젝트 정보 없음",
|
|
193
|
+
taskSummary: "요청 내용",
|
|
194
|
+
taskSummaryUnavailable: "요청 내용을 요약할 수 있는 정보가 아직 없습니다.",
|
|
195
|
+
recentActivity: "최근 활동",
|
|
196
|
+
recent: "최근",
|
|
197
|
+
subagentsCount: "참여 에이전트 · {count}",
|
|
198
|
+
subagentName: "에이전트 {ordinal}",
|
|
199
|
+
agentProfile: "역할/프로필 · {profile}",
|
|
200
|
+
agentProfileNote: "Codex는 현재 각 에이전트의 역할을 제공하지만, 구체적인 할당 내용 전체는 제공하지 않습니다.",
|
|
201
|
+
noSubagents: "이 작업에 참여한 에이전트가 아직 관찰되지 않았습니다.",
|
|
202
|
+
noRecentActivity: "표시할 최근 활동이 없습니다.",
|
|
203
|
+
technicalInfo: "기술 정보",
|
|
204
|
+
agentId: "에이전트 ID",
|
|
205
|
+
rawProfile: "원본 역할/프로필",
|
|
206
|
+
rawEvent: "원본 이벤트",
|
|
207
|
+
rawTool: "원본 도구",
|
|
208
|
+
retry: "연결 다시 시도",
|
|
209
|
+
retryAuthentication: "이 탭에서 다시 시도",
|
|
210
|
+
checkAuthentication: "인증 정보 다시 확인",
|
|
211
|
+
recoveryTitle: "새 인증 화면 열기",
|
|
212
|
+
recoveryStep: "Codex 앱 입력창에서 @codex-agent-view를 선택한 다음, 스킬 선택기에서 실제 $show-agents 스킬을 선택하세요.",
|
|
213
|
+
recoveryNote: "새 인증이 적용된 실시간 화면이 열립니다. 터미널 명령이나 외부 브라우저는 필요하지 않습니다.",
|
|
214
|
+
resultsFiltered: "전체 {total}개 중 {visible}개 표시",
|
|
215
|
+
resultsTotal: "작업 {count}개",
|
|
216
|
+
searchEmptyTitle: "검색 결과가 없습니다.",
|
|
217
|
+
searchEmptyCopy: "검색어나 상태 필터를 바꿔 보세요.",
|
|
218
|
+
emptyWithDiagnosticsTitle: "표시 가능한 작업이 없습니다.",
|
|
219
|
+
emptyWithDiagnosticsCopy: "로컬 모니터가 활동 정보 {count}건을 받았지만 표시 가능한 작업으로 적용하지 못했습니다.",
|
|
220
|
+
emptyTitle: "이 관찰 화면에서 수신된 작업 활동이 없습니다.",
|
|
221
|
+
emptyCopy: "로컬 모니터 연결은 정상입니다. 이 결과만으로 Codex에 진행 중인 작업이나 참여 에이전트가 없다고 판단할 수 없습니다.",
|
|
222
|
+
emptyGuidanceTitle: "표시되지 않을 때 확인 순서",
|
|
223
|
+
automaticTracking: "작업 ID를 입력하거나 작업별로 등록할 필요가 없습니다. 신뢰한 hook의 작업 활동이 이 목록에 자동으로 추가됩니다.",
|
|
224
|
+
emptyStep1: "플러그인을 설치한 뒤 공식 Codex 앱을 완전히 재시작했는지 확인합니다.",
|
|
225
|
+
emptyStep2: "새 작업에서 표시되는 Codex Agent View hook 명령을 검토하고 직접 신뢰합니다.",
|
|
226
|
+
emptyStep3: "신뢰 설정 후 새 작업을 시작해 에이전트를 실행합니다. 새 활동은 이 목록에 자동으로 추가됩니다.",
|
|
227
|
+
observationBoundary: "관찰 화면은 첫 번째로 신뢰한 hook을 받은 시점부터 시작합니다. 그 전에 이미 지나간 활동과 로컬 상태 수집이 중단된 동안의 활동은 재생되지 않으며, 수집이 다시 시작되면 새 관찰 화면이 열립니다.",
|
|
228
|
+
diagnosticsCount: "검증 정보 · {count}건",
|
|
229
|
+
diagnosticOccurrences: "{count}건",
|
|
230
|
+
disconnectedTitle: "로컬 상태 연결이 끊겨 다시 시도 중입니다.",
|
|
231
|
+
authTitle: "이 실시간 화면을 인증할 수 없습니다.",
|
|
232
|
+
retryWithState: "2초마다 자동으로 다시 연결합니다. 마지막 정상 상태를 계속 표시합니다.",
|
|
233
|
+
retryWithoutState: "2초마다 자동으로 다시 연결합니다. Codex 앱에서 이 화면을 그대로 두어도 됩니다.",
|
|
234
|
+
missingToken: "이 탭에는 실시간 작업을 표시하는 데 필요한 인증 정보가 없습니다.",
|
|
235
|
+
expiredToken: "이 탭의 실시간 화면 인증이 거부되었거나 더 이상 유효하지 않습니다.",
|
|
236
|
+
requestFailed: "상태 요청 실패 ({status})",
|
|
237
|
+
unknownConnectionError: "알 수 없는 연결 오류가 발생했습니다.",
|
|
238
|
+
offline: "이 기기가 오프라인입니다.",
|
|
239
|
+
invalidState: "상태 응답 형식이 올바르지 않습니다.",
|
|
240
|
+
durationSeconds: "{count}초",
|
|
241
|
+
durationMinutes: "{count}분",
|
|
242
|
+
durationHours: "{count}시간",
|
|
243
|
+
durationHoursMinutes: "{hours}시간 {minutes}분",
|
|
244
|
+
toolFallback: "도구 · {name}",
|
|
245
|
+
activityFallback: "활동 · {name}",
|
|
246
|
+
nameUnknown: "이름 미상",
|
|
247
|
+
activityUnknown: "알 수 없음",
|
|
248
|
+
approvalRequest: "{tool} 승인 요청",
|
|
249
|
+
agentStarted: "에이전트 {ordinal} 시작",
|
|
250
|
+
agentStopped: "에이전트 {ordinal} 완료",
|
|
251
|
+
activitySessionStarted: "관찰 시작",
|
|
252
|
+
activitySessionEnded: "관찰 종료",
|
|
253
|
+
activityTurnStarted: "작업 시작",
|
|
254
|
+
activityTurnStopped: "작업 응답 완료",
|
|
255
|
+
activitySubagentStarted: "에이전트 시작",
|
|
256
|
+
activitySubagentStopped: "에이전트 완료",
|
|
257
|
+
activityToolStarted: "도구 작업",
|
|
258
|
+
activityToolCompleted: "도구 작업",
|
|
259
|
+
activityPermissionRequested: "사용자 승인 요청",
|
|
260
|
+
toolApplyPatch: "파일 수정",
|
|
261
|
+
toolBash: "터미널 작업",
|
|
262
|
+
toolFollowup: "에이전트 후속 작업 요청",
|
|
263
|
+
toolInterrupt: "에이전트 작업 중단 요청",
|
|
264
|
+
toolList: "에이전트 상태 확인",
|
|
265
|
+
toolMessage: "에이전트에게 메시지 전달",
|
|
266
|
+
toolSpawn: "에이전트 시작",
|
|
267
|
+
toolWaitAgent: "에이전트 응답 대기",
|
|
268
|
+
toolExec: "터미널 작업",
|
|
269
|
+
toolUserInput: "사용자 입력 요청",
|
|
270
|
+
toolWait: "작업 완료 대기",
|
|
271
|
+
toolStdin: "터미널 입력",
|
|
272
|
+
roleExplorer: "조사",
|
|
273
|
+
roleReviewer: "검토",
|
|
274
|
+
roleWorker: "구현",
|
|
275
|
+
roleDefault: "일반 에이전트",
|
|
276
|
+
roleUnknown: "보고되지 않음",
|
|
277
|
+
}),
|
|
278
|
+
es: Object.freeze({
|
|
279
|
+
metaDescription: "Una vista clara del trabajo de Codex y de los agentes que lo hacen avanzar.",
|
|
280
|
+
skipToContent: "Saltar al contenido",
|
|
281
|
+
brandHome: "Inicio de Codex Agent View",
|
|
282
|
+
brandSubtitle: "Resumen del trabajo en vivo",
|
|
283
|
+
languageLabel: "Idioma",
|
|
284
|
+
connectionConnecting: "Conectando al estado local",
|
|
285
|
+
connectionConnected: "Monitor local conectado",
|
|
286
|
+
connectionRetrying: "Desconectado · reintentando",
|
|
287
|
+
authenticationRequired: "Se requiere autenticar la vista",
|
|
288
|
+
heroEyebrow: "TRABAJO ACTIVO EN CODEX",
|
|
289
|
+
heroTitle: "Observa el trabajo activo de un vistazo",
|
|
290
|
+
heroCopy: "Consulta qué está haciendo Codex, qué agentes participan y cómo avanza cada parte, todo en un solo lugar.",
|
|
291
|
+
freshnessAria: "Información de actualización",
|
|
292
|
+
lastUpdatedLabel: "Última actualización",
|
|
293
|
+
notYet: "Aún no",
|
|
294
|
+
refreshIntervalLabel: "Intervalo de actualización",
|
|
295
|
+
twoSeconds: "2 segundos",
|
|
296
|
+
metricsAria: "Resumen del estado de las tareas",
|
|
297
|
+
parentTasks: "Trabajos",
|
|
298
|
+
currentlyObserved: "En observación",
|
|
299
|
+
runningAgents: "Agentes activos",
|
|
300
|
+
workingNow: "Trabajando ahora",
|
|
301
|
+
waitingStatus: "En espera",
|
|
302
|
+
waitingExplanation: "Esperando una respuesta o el siguiente paso",
|
|
303
|
+
completedAgents: "Agentes que terminaron",
|
|
304
|
+
currentView: "En la vista actual",
|
|
305
|
+
liveWork: "TRABAJO EN VIVO",
|
|
306
|
+
sessionsHeading: "Trabajos y agentes participantes",
|
|
307
|
+
loadingState: "Cargando el estado.",
|
|
308
|
+
toolbarAria: "Filtrar trabajos y agentes participantes observados automáticamente",
|
|
309
|
+
searchLabel: "Filtrar lista (opcional)",
|
|
310
|
+
searchPlaceholder: "Buscar un trabajo o agente",
|
|
311
|
+
statusFilterLabel: "Filtrar por estado (opcional)",
|
|
312
|
+
statusAll: "Todos los estados",
|
|
313
|
+
statusRunning: "En ejecución",
|
|
314
|
+
statusWaiting: "En espera",
|
|
315
|
+
statusCompleted: "Completado",
|
|
316
|
+
statusUnknown: "Desconocido",
|
|
317
|
+
connectingCopy: "Conectando al estado local de la aplicación Codex.",
|
|
318
|
+
sessionListAria: "Lista de trabajos de Codex",
|
|
319
|
+
privacyPrompt: "Esta vista solo muestra un resumen abreviado de la solicitud; nunca muestra la solicitud completa ni las entradas de herramientas.",
|
|
320
|
+
privacyLocal: "Los datos se leen únicamente del monitor local de este dispositivo.",
|
|
321
|
+
timeUnknown: "Hora no disponible",
|
|
322
|
+
startedUnknown: "Hora de inicio no disponible",
|
|
323
|
+
noReceivedActivity: "No se recibió actividad",
|
|
324
|
+
parentTask: "TRABAJO",
|
|
325
|
+
projectUnknown: "Proyecto no disponible",
|
|
326
|
+
taskSummary: "Solicitud",
|
|
327
|
+
taskSummaryUnavailable: "Todavía no hay información para resumir esta solicitud.",
|
|
328
|
+
recentActivity: "Actividad reciente",
|
|
329
|
+
recent: "Reciente",
|
|
330
|
+
subagentsCount: "Agentes participantes · {count}",
|
|
331
|
+
subagentName: "Agente {ordinal}",
|
|
332
|
+
agentProfile: "Rol/perfil · {profile}",
|
|
333
|
+
agentProfileNote: "Codex proporciona el rol de cada agente, pero no la descripción completa de su asignación.",
|
|
334
|
+
noSubagents: "Todavía no se observaron agentes participantes en este trabajo.",
|
|
335
|
+
noRecentActivity: "No hay actividad reciente que mostrar.",
|
|
336
|
+
technicalInfo: "Información técnica",
|
|
337
|
+
agentId: "ID del agente",
|
|
338
|
+
rawProfile: "Rol/perfil original",
|
|
339
|
+
rawEvent: "Evento original",
|
|
340
|
+
rawTool: "Herramienta original",
|
|
341
|
+
retry: "Reintentar conexión",
|
|
342
|
+
retryAuthentication: "Reintentar en esta pestaña",
|
|
343
|
+
checkAuthentication: "Volver a comprobar la autenticación",
|
|
344
|
+
recoveryTitle: "Abrir una vista con autenticación nueva",
|
|
345
|
+
recoveryStep: "En el cuadro de texto de Codex, selecciona @codex-agent-view y luego elige la skill real $show-agents en el selector de skills.",
|
|
346
|
+
recoveryNote: "La skill abre una vista en vivo nueva con autenticación actualizada. No necesitas la terminal ni un navegador externo.",
|
|
347
|
+
resultsFiltered: "Mostrando {visible} de {total}",
|
|
348
|
+
resultsTotal: "{count} trabajos",
|
|
349
|
+
searchEmptyTitle: "No hay resultados.",
|
|
350
|
+
searchEmptyCopy: "Prueba otra búsqueda o filtro de estado.",
|
|
351
|
+
emptyWithDiagnosticsTitle: "No hay trabajos que mostrar.",
|
|
352
|
+
emptyWithDiagnosticsCopy: "El monitor local recibió {count} registros de actividad, pero no pudo aplicarlos a trabajos visibles.",
|
|
353
|
+
emptyTitle: "No se recibió actividad en esta ventana de observación.",
|
|
354
|
+
emptyCopy: "El monitor local está conectado. Este resultado no implica por sí solo que Codex no tenga trabajos o agentes activos.",
|
|
355
|
+
emptyGuidanceTitle: "Si el trabajo no aparece",
|
|
356
|
+
automaticTracking: "No es necesario introducir ni registrar IDs de tareas. La actividad de hooks confiables se añade automáticamente.",
|
|
357
|
+
emptyStep1: "Tras instalar el plugin, reinicia por completo la aplicación oficial de Codex.",
|
|
358
|
+
emptyStep2: "En una tarea nueva, revisa y autoriza explícitamente el comando hook de Codex Agent View.",
|
|
359
|
+
emptyStep3: "Después, inicia un trabajo nuevo y ejecuta agentes. La actividad se añadirá automáticamente.",
|
|
360
|
+
observationBoundary: "La observación comienza con el primer evento de un hook autorizado. La actividad anterior o perdida mientras la recopilación local estuvo detenida no se puede reproducir; al reiniciarla se abre una ventana nueva.",
|
|
361
|
+
diagnosticsCount: "Información de validación · {count}",
|
|
362
|
+
diagnosticOccurrences: "{count} apariciones",
|
|
363
|
+
disconnectedTitle: "Se perdió la conexión local; reintentando.",
|
|
364
|
+
authTitle: "No se puede autenticar esta vista en vivo.",
|
|
365
|
+
retryWithState: "Se reconecta automáticamente cada 2 segundos y mantiene visible el último estado válido.",
|
|
366
|
+
retryWithoutState: "Se reconecta automáticamente cada 2 segundos. Puedes dejar esta vista abierta en Codex.",
|
|
367
|
+
missingToken: "Esta pestaña no tiene la autenticación necesaria para mostrar el trabajo en vivo.",
|
|
368
|
+
expiredToken: "La autenticación de esta pestaña fue rechazada o ya no es válida.",
|
|
369
|
+
requestFailed: "Falló la solicitud de estado ({status})",
|
|
370
|
+
unknownConnectionError: "Se produjo un error de conexión desconocido.",
|
|
371
|
+
offline: "Este dispositivo está sin conexión.",
|
|
372
|
+
invalidState: "El formato de la respuesta de estado no es válido.",
|
|
373
|
+
durationSeconds: "{count} s",
|
|
374
|
+
durationMinutes: "{count} min",
|
|
375
|
+
durationHours: "{count} h",
|
|
376
|
+
durationHoursMinutes: "{hours} h {minutes} min",
|
|
377
|
+
toolFallback: "Herramienta · {name}",
|
|
378
|
+
activityFallback: "Actividad · {name}",
|
|
379
|
+
nameUnknown: "Nombre desconocido",
|
|
380
|
+
activityUnknown: "Desconocida",
|
|
381
|
+
approvalRequest: "Se solicitó aprobación para {tool}",
|
|
382
|
+
agentStarted: "Agente {ordinal} iniciado",
|
|
383
|
+
agentStopped: "Agente {ordinal} completado",
|
|
384
|
+
activitySessionStarted: "Observación iniciada",
|
|
385
|
+
activitySessionEnded: "Observación finalizada",
|
|
386
|
+
activityTurnStarted: "Trabajo iniciado",
|
|
387
|
+
activityTurnStopped: "Respuesta del trabajo completada",
|
|
388
|
+
activitySubagentStarted: "Agente iniciado",
|
|
389
|
+
activitySubagentStopped: "Agente completado",
|
|
390
|
+
activityToolStarted: "Actividad de herramienta",
|
|
391
|
+
activityToolCompleted: "Actividad de herramienta",
|
|
392
|
+
activityPermissionRequested: "Se solicitó aprobación",
|
|
393
|
+
toolApplyPatch: "Edición de archivo",
|
|
394
|
+
toolBash: "Actividad de terminal",
|
|
395
|
+
toolFollowup: "Seguimiento del agente solicitado",
|
|
396
|
+
toolInterrupt: "Interrupción del agente solicitada",
|
|
397
|
+
toolList: "Estado de agentes consultado",
|
|
398
|
+
toolMessage: "Mensaje enviado al agente",
|
|
399
|
+
toolSpawn: "Agente iniciado",
|
|
400
|
+
toolWaitAgent: "Esperando al agente",
|
|
401
|
+
toolExec: "Actividad de terminal",
|
|
402
|
+
toolUserInput: "Se solicitó entrada del usuario",
|
|
403
|
+
toolWait: "Esperando finalización",
|
|
404
|
+
toolStdin: "Entrada de terminal",
|
|
405
|
+
roleExplorer: "Investigación",
|
|
406
|
+
roleReviewer: "Revisión",
|
|
407
|
+
roleWorker: "Implementación",
|
|
408
|
+
roleDefault: "Agente general",
|
|
409
|
+
roleUnknown: "No informado",
|
|
410
|
+
}),
|
|
411
|
+
});
|
|
5
412
|
|
|
6
|
-
const
|
|
7
|
-
running: "
|
|
8
|
-
waiting: "
|
|
9
|
-
completed: "
|
|
10
|
-
unknown: "
|
|
413
|
+
const STATUS_KEYS = Object.freeze({
|
|
414
|
+
running: "statusRunning",
|
|
415
|
+
waiting: "statusWaiting",
|
|
416
|
+
completed: "statusCompleted",
|
|
417
|
+
unknown: "statusUnknown",
|
|
11
418
|
});
|
|
12
419
|
|
|
13
420
|
const STATUS_ORDER = Object.freeze({
|
|
@@ -17,45 +424,86 @@ const STATUS_ORDER = Object.freeze({
|
|
|
17
424
|
completed: 3,
|
|
18
425
|
});
|
|
19
426
|
|
|
20
|
-
const
|
|
21
|
-
session_started: "
|
|
22
|
-
session_ended: "
|
|
23
|
-
turn_started: "
|
|
24
|
-
turn_stopped: "
|
|
25
|
-
subagent_started: "
|
|
26
|
-
subagent_stopped: "
|
|
27
|
-
tool_started: "
|
|
28
|
-
tool_completed: "
|
|
29
|
-
permission_requested: "
|
|
427
|
+
const ACTIVITY_KEYS = Object.freeze({
|
|
428
|
+
session_started: "activitySessionStarted",
|
|
429
|
+
session_ended: "activitySessionEnded",
|
|
430
|
+
turn_started: "activityTurnStarted",
|
|
431
|
+
turn_stopped: "activityTurnStopped",
|
|
432
|
+
subagent_started: "activitySubagentStarted",
|
|
433
|
+
subagent_stopped: "activitySubagentStopped",
|
|
434
|
+
tool_started: "activityToolStarted",
|
|
435
|
+
tool_completed: "activityToolCompleted",
|
|
436
|
+
permission_requested: "activityPermissionRequested",
|
|
30
437
|
});
|
|
31
438
|
|
|
32
|
-
const
|
|
33
|
-
apply_patch: "
|
|
34
|
-
bash: "
|
|
35
|
-
collaborationfollowup_task: "
|
|
36
|
-
collaborationinterrupt_agent: "
|
|
37
|
-
collaborationlist_agents: "
|
|
38
|
-
collaborationsend_message: "
|
|
39
|
-
collaborationspawn_agent: "
|
|
40
|
-
collaborationwait_agent: "
|
|
41
|
-
exec_command: "
|
|
42
|
-
request_user_input: "
|
|
43
|
-
wait: "
|
|
44
|
-
write_stdin: "
|
|
439
|
+
const TOOL_KEYS = Object.freeze({
|
|
440
|
+
apply_patch: "toolApplyPatch",
|
|
441
|
+
bash: "toolBash",
|
|
442
|
+
collaborationfollowup_task: "toolFollowup",
|
|
443
|
+
collaborationinterrupt_agent: "toolInterrupt",
|
|
444
|
+
collaborationlist_agents: "toolList",
|
|
445
|
+
collaborationsend_message: "toolMessage",
|
|
446
|
+
collaborationspawn_agent: "toolSpawn",
|
|
447
|
+
collaborationwait_agent: "toolWaitAgent",
|
|
448
|
+
exec_command: "toolExec",
|
|
449
|
+
request_user_input: "toolUserInput",
|
|
450
|
+
wait: "toolWait",
|
|
451
|
+
write_stdin: "toolStdin",
|
|
45
452
|
});
|
|
46
453
|
|
|
47
|
-
const
|
|
48
|
-
explorer: "
|
|
49
|
-
reviewer: "
|
|
50
|
-
worker: "
|
|
454
|
+
const AGENT_ROLE_KEYS = Object.freeze({
|
|
455
|
+
explorer: "roleExplorer",
|
|
456
|
+
reviewer: "roleReviewer",
|
|
457
|
+
worker: "roleWorker",
|
|
458
|
+
default: "roleDefault",
|
|
459
|
+
unknown: "roleUnknown",
|
|
51
460
|
});
|
|
52
461
|
|
|
53
|
-
const HIDDEN_AGENT_ROLES = new Set(["", "default", "unknown"]);
|
|
54
462
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
55
463
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
464
|
+
function readStoredLanguage() {
|
|
465
|
+
try {
|
|
466
|
+
const stored = window.localStorage.getItem(LANGUAGE_KEY);
|
|
467
|
+
return SUPPORTED_LANGUAGES.has(stored) ? stored : "en";
|
|
468
|
+
} catch {
|
|
469
|
+
return "en";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let currentLanguage = readStoredLanguage();
|
|
474
|
+
|
|
475
|
+
function t(key, replacements = {}) {
|
|
476
|
+
const template = MESSAGES[currentLanguage][key] ?? MESSAGES.en[key] ?? key;
|
|
477
|
+
return Object.entries(replacements).reduce(
|
|
478
|
+
(result, [name, value]) => result.replaceAll(`{${name}}`, String(value)),
|
|
479
|
+
template,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function isExactLiveFragment(entries) {
|
|
484
|
+
const tokenEntries = entries.filter(([key]) => key === "token");
|
|
485
|
+
const excludeEntries = entries.filter(([key]) => key === "exclude");
|
|
486
|
+
return (
|
|
487
|
+
entries.length === tokenEntries.length + excludeEntries.length &&
|
|
488
|
+
tokenEntries.length === 1 &&
|
|
489
|
+
excludeEntries.length <= 1 &&
|
|
490
|
+
VIEWER_TOKEN_PATTERN.test(tokenEntries[0][1]) &&
|
|
491
|
+
(excludeEntries.length === 0 || CANONICAL_SESSION_ID_PATTERN.test(excludeEntries[0][1]))
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function consumeLiveContext() {
|
|
496
|
+
const hasFragment = window.location.hash.length > 1;
|
|
497
|
+
const entries = hasFragment
|
|
498
|
+
? [...new URLSearchParams(window.location.hash.slice(1)).entries()]
|
|
499
|
+
: [];
|
|
500
|
+
const validFragment = hasFragment && isExactLiveFragment(entries);
|
|
501
|
+
const fragmentToken = validFragment
|
|
502
|
+
? entries.find(([key]) => key === "token")[1]
|
|
503
|
+
: "";
|
|
504
|
+
const fragmentExclude = validFragment
|
|
505
|
+
? entries.find(([key]) => key === "exclude")?.[1] || ""
|
|
506
|
+
: "";
|
|
59
507
|
|
|
60
508
|
if (window.location.hash) {
|
|
61
509
|
window.history.replaceState(
|
|
@@ -66,25 +514,39 @@ function consumeAccessToken() {
|
|
|
66
514
|
}
|
|
67
515
|
|
|
68
516
|
let token = fragmentToken;
|
|
517
|
+
let excludedSessionId = fragmentExclude;
|
|
69
518
|
try {
|
|
70
|
-
if (
|
|
519
|
+
if (validFragment) {
|
|
71
520
|
window.sessionStorage.setItem(SESSION_TOKEN_KEY, fragmentToken);
|
|
521
|
+
if (fragmentExclude) {
|
|
522
|
+
window.sessionStorage.setItem(EXCLUDED_SESSION_KEY, fragmentExclude);
|
|
523
|
+
} else {
|
|
524
|
+
window.sessionStorage.removeItem(EXCLUDED_SESSION_KEY);
|
|
525
|
+
}
|
|
72
526
|
} else {
|
|
73
527
|
token = window.sessionStorage.getItem(SESSION_TOKEN_KEY)?.trim() || "";
|
|
528
|
+
excludedSessionId = window.sessionStorage.getItem(EXCLUDED_SESSION_KEY)?.trim() || "";
|
|
74
529
|
}
|
|
75
530
|
} catch {
|
|
76
531
|
// Storage can be unavailable in hardened browser contexts. The fragment
|
|
77
532
|
// token remains usable for this page load and is never copied elsewhere.
|
|
78
533
|
}
|
|
79
534
|
|
|
80
|
-
return
|
|
535
|
+
return {
|
|
536
|
+
accessToken: VIEWER_TOKEN_PATTERN.test(token) ? token : "",
|
|
537
|
+
excludedSessionId: CANONICAL_SESSION_ID_PATTERN.test(excludedSessionId)
|
|
538
|
+
? excludedSessionId
|
|
539
|
+
: "",
|
|
540
|
+
};
|
|
81
541
|
}
|
|
82
542
|
|
|
83
|
-
|
|
543
|
+
let { accessToken, excludedSessionId } = consumeLiveContext();
|
|
84
544
|
|
|
85
545
|
const elements = Object.freeze({
|
|
86
546
|
connectionStatus: document.querySelector("#connection-status"),
|
|
87
547
|
connectionLabel: document.querySelector("#connection-label"),
|
|
548
|
+
language: document.querySelector("#language-select"),
|
|
549
|
+
metaDescription: document.querySelector("#meta-description"),
|
|
88
550
|
lastUpdated: document.querySelector("#last-updated"),
|
|
89
551
|
metricSessions: document.querySelector("#metric-sessions"),
|
|
90
552
|
metricRunning: document.querySelector("#metric-running"),
|
|
@@ -98,12 +560,43 @@ const elements = Object.freeze({
|
|
|
98
560
|
sessionList: document.querySelector("#session-list"),
|
|
99
561
|
});
|
|
100
562
|
|
|
563
|
+
function applyStaticTranslations() {
|
|
564
|
+
document.documentElement.lang = currentLanguage;
|
|
565
|
+
elements.language.value = currentLanguage;
|
|
566
|
+
elements.metaDescription.content = t("metaDescription");
|
|
567
|
+
for (const element of document.querySelectorAll("[data-i18n]")) {
|
|
568
|
+
element.textContent = t(element.dataset.i18n);
|
|
569
|
+
}
|
|
570
|
+
for (const element of document.querySelectorAll("[data-i18n-aria-label]")) {
|
|
571
|
+
element.setAttribute("aria-label", t(element.dataset.i18nAriaLabel));
|
|
572
|
+
}
|
|
573
|
+
for (const element of document.querySelectorAll("[data-i18n-placeholder]")) {
|
|
574
|
+
element.setAttribute("placeholder", t(element.dataset.i18nPlaceholder));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function setLanguage(value) {
|
|
579
|
+
currentLanguage = SUPPORTED_LANGUAGES.has(value) ? value : "en";
|
|
580
|
+
try {
|
|
581
|
+
window.localStorage.setItem(LANGUAGE_KEY, currentLanguage);
|
|
582
|
+
} catch {
|
|
583
|
+
// The selected language still applies for this page load.
|
|
584
|
+
}
|
|
585
|
+
applyStaticTranslations();
|
|
586
|
+
setConnectionStatus(
|
|
587
|
+
elements.connectionStatus.dataset.status,
|
|
588
|
+
viewState.authenticationFailed ? t("authenticationRequired") : "",
|
|
589
|
+
);
|
|
590
|
+
render();
|
|
591
|
+
}
|
|
592
|
+
|
|
101
593
|
const viewState = {
|
|
102
594
|
updatedAtMs: null,
|
|
103
595
|
sessions: [],
|
|
104
596
|
diagnostics: [],
|
|
105
597
|
hasLoaded: false,
|
|
106
598
|
errorMessage: "",
|
|
599
|
+
errorKey: "",
|
|
107
600
|
canRetry: true,
|
|
108
601
|
authenticationFailed: false,
|
|
109
602
|
requestInFlight: false,
|
|
@@ -139,26 +632,27 @@ function normalizedLookupKey(value) {
|
|
|
139
632
|
|
|
140
633
|
function formatAgentRole(agentType) {
|
|
141
634
|
const key = normalizedLookupKey(agentType);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return AGENT_ROLE_LABELS[key] || sanitizedFallbackLabel(agentType, "");
|
|
635
|
+
return AGENT_ROLE_KEYS[key]
|
|
636
|
+
? t(AGENT_ROLE_KEYS[key])
|
|
637
|
+
: sanitizedFallbackLabel(agentType, t("roleUnknown"));
|
|
146
638
|
}
|
|
147
639
|
|
|
148
640
|
function formatToolLabel(toolName) {
|
|
149
641
|
const key = normalizedLookupKey(toolName);
|
|
150
|
-
return
|
|
642
|
+
return TOOL_KEYS[key]
|
|
643
|
+
? t(TOOL_KEYS[key])
|
|
644
|
+
: t("toolFallback", { name: sanitizedFallbackLabel(toolName, t("nameUnknown")) });
|
|
151
645
|
}
|
|
152
646
|
|
|
153
647
|
function formatActivityLabel(activity) {
|
|
154
648
|
if (activity.eventName === "subagent_started" && activity.agentOrdinal !== null) {
|
|
155
|
-
return
|
|
649
|
+
return t("agentStarted", { ordinal: activity.agentOrdinal });
|
|
156
650
|
}
|
|
157
651
|
if (activity.eventName === "subagent_stopped" && activity.agentOrdinal !== null) {
|
|
158
|
-
return
|
|
652
|
+
return t("agentStopped", { ordinal: activity.agentOrdinal });
|
|
159
653
|
}
|
|
160
654
|
if (activity.eventName === "permission_requested" && activity.toolName) {
|
|
161
|
-
return
|
|
655
|
+
return t("approvalRequest", { tool: formatToolLabel(activity.toolName) });
|
|
162
656
|
}
|
|
163
657
|
if (
|
|
164
658
|
(activity.eventName === "tool_started" || activity.eventName === "tool_completed") &&
|
|
@@ -166,8 +660,11 @@ function formatActivityLabel(activity) {
|
|
|
166
660
|
) {
|
|
167
661
|
return formatToolLabel(activity.toolName);
|
|
168
662
|
}
|
|
169
|
-
return
|
|
170
|
-
|
|
663
|
+
return ACTIVITY_KEYS[activity.eventName]
|
|
664
|
+
? t(ACTIVITY_KEYS[activity.eventName])
|
|
665
|
+
: t("activityFallback", {
|
|
666
|
+
name: sanitizedFallbackLabel(activity.eventName, t("activityUnknown")),
|
|
667
|
+
});
|
|
171
668
|
}
|
|
172
669
|
|
|
173
670
|
function safeTimestamp(value) {
|
|
@@ -322,6 +819,7 @@ function normalizeSession(value, index) {
|
|
|
322
819
|
return {
|
|
323
820
|
sessionId: safeString(session.session_id, `unknown-session-${index + 1}`),
|
|
324
821
|
workspaceLabel: safeString(session.workspace_label, ""),
|
|
822
|
+
taskSummary: safeString(session.task_summary, ""),
|
|
325
823
|
status: deriveSessionStatus(session, agents, recentActivities),
|
|
326
824
|
lastActivityAtMs: safeTimestamp(session.last_seen_at_ms),
|
|
327
825
|
agents,
|
|
@@ -337,7 +835,7 @@ function normalizeState(value) {
|
|
|
337
835
|
!Array.isArray(value.sessions) ||
|
|
338
836
|
!Array.isArray(value.diagnostics)
|
|
339
837
|
) {
|
|
340
|
-
throw new TypeError("
|
|
838
|
+
throw new TypeError(t("invalidState"));
|
|
341
839
|
}
|
|
342
840
|
|
|
343
841
|
return {
|
|
@@ -363,10 +861,10 @@ function compareSessions(left, right) {
|
|
|
363
861
|
|
|
364
862
|
function formatDateTime(timestampMs) {
|
|
365
863
|
if (timestampMs === null) {
|
|
366
|
-
return "
|
|
864
|
+
return t("timeUnknown");
|
|
367
865
|
}
|
|
368
866
|
|
|
369
|
-
return new Intl.DateTimeFormat(
|
|
867
|
+
return new Intl.DateTimeFormat(currentLanguage, {
|
|
370
868
|
dateStyle: "medium",
|
|
371
869
|
timeStyle: "medium",
|
|
372
870
|
}).format(new Date(timestampMs));
|
|
@@ -374,12 +872,12 @@ function formatDateTime(timestampMs) {
|
|
|
374
872
|
|
|
375
873
|
function formatRelativeTime(timestampMs) {
|
|
376
874
|
if (timestampMs === null) {
|
|
377
|
-
return "
|
|
875
|
+
return t("timeUnknown");
|
|
378
876
|
}
|
|
379
877
|
|
|
380
878
|
const differenceSeconds = Math.round((timestampMs - Date.now()) / 1_000);
|
|
381
879
|
const absoluteSeconds = Math.abs(differenceSeconds);
|
|
382
|
-
const formatter = new Intl.RelativeTimeFormat(
|
|
880
|
+
const formatter = new Intl.RelativeTimeFormat(currentLanguage, { numeric: "auto" });
|
|
383
881
|
|
|
384
882
|
if (absoluteSeconds < 60) {
|
|
385
883
|
return formatter.format(differenceSeconds, "second");
|
|
@@ -395,23 +893,25 @@ function formatRelativeTime(timestampMs) {
|
|
|
395
893
|
|
|
396
894
|
function formatDuration(startedAtMs, stoppedAtMs) {
|
|
397
895
|
if (startedAtMs === null) {
|
|
398
|
-
return "
|
|
896
|
+
return t("startedUnknown");
|
|
399
897
|
}
|
|
400
898
|
|
|
401
899
|
const endAtMs = stoppedAtMs ?? Date.now();
|
|
402
900
|
const seconds = Math.max(0, Math.floor((endAtMs - startedAtMs) / 1_000));
|
|
403
901
|
if (seconds < 60) {
|
|
404
|
-
return
|
|
902
|
+
return t("durationSeconds", { count: seconds });
|
|
405
903
|
}
|
|
406
904
|
|
|
407
905
|
const minutes = Math.floor(seconds / 60);
|
|
408
906
|
if (minutes < 60) {
|
|
409
|
-
return
|
|
907
|
+
return t("durationMinutes", { count: minutes });
|
|
410
908
|
}
|
|
411
909
|
|
|
412
910
|
const hours = Math.floor(minutes / 60);
|
|
413
911
|
const remainingMinutes = minutes % 60;
|
|
414
|
-
return remainingMinutes
|
|
912
|
+
return remainingMinutes
|
|
913
|
+
? t("durationHoursMinutes", { hours, minutes: remainingMinutes })
|
|
914
|
+
: t("durationHours", { count: hours });
|
|
415
915
|
}
|
|
416
916
|
|
|
417
917
|
function createStatusBadge(status) {
|
|
@@ -424,7 +924,7 @@ function createStatusBadge(status) {
|
|
|
424
924
|
dot.setAttribute("aria-hidden", "true");
|
|
425
925
|
|
|
426
926
|
const label = document.createElement("span");
|
|
427
|
-
label.textContent =
|
|
927
|
+
label.textContent = t(STATUS_KEYS[status]);
|
|
428
928
|
|
|
429
929
|
badge.append(dot, label);
|
|
430
930
|
return badge;
|
|
@@ -439,7 +939,7 @@ function createTime(timestampMs, prefix) {
|
|
|
439
939
|
}
|
|
440
940
|
|
|
441
941
|
if (timestampMs === null) {
|
|
442
|
-
wrapper.append("
|
|
942
|
+
wrapper.append(t("timeUnknown"));
|
|
443
943
|
return wrapper;
|
|
444
944
|
}
|
|
445
945
|
|
|
@@ -451,12 +951,13 @@ function createTime(timestampMs, prefix) {
|
|
|
451
951
|
return wrapper;
|
|
452
952
|
}
|
|
453
953
|
|
|
454
|
-
function
|
|
455
|
-
const
|
|
456
|
-
|
|
954
|
+
function createTechnicalInfo(rows) {
|
|
955
|
+
const info = document.createElement("div");
|
|
956
|
+
info.className = "technical-info";
|
|
457
957
|
|
|
458
|
-
const
|
|
459
|
-
|
|
958
|
+
const heading = document.createElement("p");
|
|
959
|
+
heading.className = "technical-info-title";
|
|
960
|
+
heading.textContent = t("technicalInfo");
|
|
460
961
|
const list = document.createElement("dl");
|
|
461
962
|
|
|
462
963
|
for (const [label, value] of rows) {
|
|
@@ -472,8 +973,8 @@ function createTechnicalDetails(rows) {
|
|
|
472
973
|
list.append(term, description);
|
|
473
974
|
}
|
|
474
975
|
|
|
475
|
-
|
|
476
|
-
return
|
|
976
|
+
info.append(heading, list);
|
|
977
|
+
return info;
|
|
477
978
|
}
|
|
478
979
|
|
|
479
980
|
function createAgentItem(agent) {
|
|
@@ -488,31 +989,27 @@ function createAgentItem(agent) {
|
|
|
488
989
|
identity.className = "agent-identity";
|
|
489
990
|
const name = document.createElement("strong");
|
|
490
991
|
name.className = "agent-name";
|
|
491
|
-
name.textContent =
|
|
992
|
+
name.textContent = t("subagentName", { ordinal: agent.ordinal });
|
|
492
993
|
identity.append(name);
|
|
493
994
|
|
|
494
995
|
const roleLabel = formatAgentRole(agent.agentType);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
identity.append(role);
|
|
500
|
-
}
|
|
996
|
+
const role = document.createElement("span");
|
|
997
|
+
role.className = "agent-role";
|
|
998
|
+
role.textContent = t("agentProfile", { profile: roleLabel });
|
|
999
|
+
identity.append(role);
|
|
501
1000
|
heading.append(identity, createStatusBadge(agent.status));
|
|
502
1001
|
|
|
503
1002
|
const metadata = document.createElement("div");
|
|
504
1003
|
metadata.className = "agent-metadata";
|
|
505
1004
|
metadata.append(
|
|
506
|
-
createTime(agent.lastActivityAtMs, "
|
|
1005
|
+
createTime(agent.lastActivityAtMs, t("recent")),
|
|
507
1006
|
document.createTextNode(` · ${formatDuration(agent.startedAtMs, agent.stoppedAtMs)}`),
|
|
508
1007
|
);
|
|
509
1008
|
|
|
510
|
-
const technicalRows = [["
|
|
511
|
-
|
|
512
|
-
technicalRows.push(["원본 역할", agent.agentType]);
|
|
513
|
-
}
|
|
1009
|
+
const technicalRows = [[t("agentId"), agent.agentId]];
|
|
1010
|
+
technicalRows.push([t("rawProfile"), agent.agentType]);
|
|
514
1011
|
|
|
515
|
-
item.append(heading, metadata,
|
|
1012
|
+
item.append(heading, metadata, createTechnicalInfo(technicalRows));
|
|
516
1013
|
return item;
|
|
517
1014
|
}
|
|
518
1015
|
|
|
@@ -536,12 +1033,12 @@ function createActivityItem(activity) {
|
|
|
536
1033
|
metadata.className = "activity-metadata";
|
|
537
1034
|
metadata.append(createStatusBadge(activity.status), createTime(activity.occurredAtMs, ""));
|
|
538
1035
|
|
|
539
|
-
const technicalRows = [["
|
|
1036
|
+
const technicalRows = [[t("rawEvent"), activity.eventName]];
|
|
540
1037
|
if (activity.toolName) {
|
|
541
|
-
technicalRows.push(["
|
|
1038
|
+
technicalRows.push([t("rawTool"), activity.toolName]);
|
|
542
1039
|
}
|
|
543
1040
|
|
|
544
|
-
content.append(title, metadata,
|
|
1041
|
+
content.append(title, metadata, createTechnicalInfo(technicalRows));
|
|
545
1042
|
item.append(marker, content);
|
|
546
1043
|
return item;
|
|
547
1044
|
}
|
|
@@ -566,20 +1063,23 @@ function createSessionCard(session) {
|
|
|
566
1063
|
identity.className = "session-identity";
|
|
567
1064
|
const eyebrow = document.createElement("span");
|
|
568
1065
|
eyebrow.className = "session-kind";
|
|
569
|
-
eyebrow.textContent = "
|
|
1066
|
+
eyebrow.textContent = t("parentTask");
|
|
570
1067
|
const title = document.createElement("h3");
|
|
571
|
-
title.textContent = session.workspaceLabel || "
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
);
|
|
1068
|
+
title.textContent = session.workspaceLabel || t("projectUnknown");
|
|
1069
|
+
const taskSummary = document.createElement("p");
|
|
1070
|
+
taskSummary.className = "task-summary";
|
|
1071
|
+
const taskSummaryLabel = document.createElement("strong");
|
|
1072
|
+
taskSummaryLabel.textContent = `${t("taskSummary")} · `;
|
|
1073
|
+
const taskSummaryCopy = document.createElement("span");
|
|
1074
|
+
taskSummaryCopy.textContent = session.taskSummary || t("taskSummaryUnavailable");
|
|
1075
|
+
taskSummary.append(taskSummaryLabel, taskSummaryCopy);
|
|
1076
|
+
identity.append(eyebrow, title, taskSummary);
|
|
577
1077
|
|
|
578
1078
|
const sessionState = document.createElement("div");
|
|
579
1079
|
sessionState.className = "session-state";
|
|
580
1080
|
sessionState.append(
|
|
581
1081
|
createStatusBadge(session.status),
|
|
582
|
-
createTime(session.lastActivityAtMs, "
|
|
1082
|
+
createTime(session.lastActivityAtMs, t("recentActivity")),
|
|
583
1083
|
);
|
|
584
1084
|
|
|
585
1085
|
cardHeader.append(identity, sessionState);
|
|
@@ -590,8 +1090,11 @@ function createSessionCard(session) {
|
|
|
590
1090
|
const agentPanel = document.createElement("section");
|
|
591
1091
|
agentPanel.className = "session-panel";
|
|
592
1092
|
const agentTitle = document.createElement("h4");
|
|
593
|
-
agentTitle.textContent =
|
|
594
|
-
|
|
1093
|
+
agentTitle.textContent = t("subagentsCount", { count: session.agents.length });
|
|
1094
|
+
const profileNote = document.createElement("p");
|
|
1095
|
+
profileNote.className = "agent-profile-note";
|
|
1096
|
+
profileNote.textContent = t("agentProfileNote");
|
|
1097
|
+
agentPanel.append(agentTitle, profileNote);
|
|
595
1098
|
|
|
596
1099
|
if (session.agents.length) {
|
|
597
1100
|
const list = document.createElement("ul");
|
|
@@ -601,13 +1104,13 @@ function createSessionCard(session) {
|
|
|
601
1104
|
});
|
|
602
1105
|
agentPanel.append(list);
|
|
603
1106
|
} else {
|
|
604
|
-
agentPanel.append(createEmptyPanel("
|
|
1107
|
+
agentPanel.append(createEmptyPanel(t("noSubagents")));
|
|
605
1108
|
}
|
|
606
1109
|
|
|
607
1110
|
const activityPanel = document.createElement("section");
|
|
608
1111
|
activityPanel.className = "session-panel";
|
|
609
1112
|
const activityTitle = document.createElement("h4");
|
|
610
|
-
activityTitle.textContent = "
|
|
1113
|
+
activityTitle.textContent = t("recentActivity");
|
|
611
1114
|
activityPanel.append(activityTitle);
|
|
612
1115
|
|
|
613
1116
|
if (session.recentActivities.length) {
|
|
@@ -618,7 +1121,7 @@ function createSessionCard(session) {
|
|
|
618
1121
|
});
|
|
619
1122
|
activityPanel.append(list);
|
|
620
1123
|
} else {
|
|
621
|
-
activityPanel.append(createEmptyPanel("
|
|
1124
|
+
activityPanel.append(createEmptyPanel(t("noRecentActivity")));
|
|
622
1125
|
}
|
|
623
1126
|
|
|
624
1127
|
panels.append(agentPanel, activityPanel);
|
|
@@ -635,6 +1138,7 @@ function sessionMatchesQuery(session, query) {
|
|
|
635
1138
|
const searchableValues = [
|
|
636
1139
|
session.sessionId,
|
|
637
1140
|
session.workspaceLabel,
|
|
1141
|
+
session.taskSummary,
|
|
638
1142
|
session.status,
|
|
639
1143
|
...session.agents.flatMap((agent) => [
|
|
640
1144
|
agent.agentId,
|
|
@@ -659,33 +1163,38 @@ function sessionMatchesStatus(session, status) {
|
|
|
659
1163
|
);
|
|
660
1164
|
}
|
|
661
1165
|
|
|
1166
|
+
function observableSessions() {
|
|
1167
|
+
return viewState.sessions.filter((session) => session.sessionId !== excludedSessionId);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
662
1170
|
function filteredSessions() {
|
|
663
1171
|
const query = elements.search.value.trim().toLocaleLowerCase();
|
|
664
1172
|
const status = elements.statusFilter.value;
|
|
665
1173
|
|
|
666
|
-
return
|
|
1174
|
+
return observableSessions()
|
|
667
1175
|
.filter((session) => sessionMatchesQuery(session, query))
|
|
668
1176
|
.filter((session) => sessionMatchesStatus(session, status))
|
|
669
1177
|
.sort(compareSessions);
|
|
670
1178
|
}
|
|
671
1179
|
|
|
672
1180
|
function renderMetrics() {
|
|
673
|
-
const
|
|
1181
|
+
const sessions = observableSessions();
|
|
1182
|
+
const agents = sessions.flatMap((session) => session.agents);
|
|
674
1183
|
const countStatus = (status) => agents.filter((agent) => agent.status === status).length;
|
|
675
1184
|
|
|
676
|
-
elements.metricSessions.textContent = String(
|
|
1185
|
+
elements.metricSessions.textContent = String(sessions.length);
|
|
677
1186
|
elements.metricRunning.textContent = String(countStatus("running"));
|
|
678
|
-
const waitingCount = countStatus("waiting") +
|
|
1187
|
+
const waitingCount = countStatus("waiting") + sessions.filter(
|
|
679
1188
|
(session) => session.status === "waiting",
|
|
680
1189
|
).length;
|
|
681
1190
|
elements.metricWaiting.textContent = String(waitingCount);
|
|
682
1191
|
elements.metricCompleted.textContent = String(countStatus("completed"));
|
|
683
1192
|
elements.lastUpdated.textContent = !viewState.updatedAtMs
|
|
684
|
-
? "
|
|
1193
|
+
? t("noReceivedActivity")
|
|
685
1194
|
: formatDateTime(viewState.updatedAtMs);
|
|
686
1195
|
}
|
|
687
1196
|
|
|
688
|
-
function setStateMessage(kind, title, description,
|
|
1197
|
+
function setStateMessage(kind, title, description, retryMode = "") {
|
|
689
1198
|
elements.stateMessage.className = `state-message state-${kind}`;
|
|
690
1199
|
elements.stateMessage.replaceChildren();
|
|
691
1200
|
|
|
@@ -695,16 +1204,61 @@ function setStateMessage(kind, title, description, includeRetry = false) {
|
|
|
695
1204
|
copy.textContent = description;
|
|
696
1205
|
elements.stateMessage.append(heading, copy);
|
|
697
1206
|
|
|
698
|
-
if (
|
|
1207
|
+
if (retryMode === "authentication") {
|
|
1208
|
+
const recovery = document.createElement("div");
|
|
1209
|
+
recovery.className = "recovery-guidance";
|
|
1210
|
+
|
|
1211
|
+
const recoveryTitle = document.createElement("strong");
|
|
1212
|
+
recoveryTitle.textContent = t("recoveryTitle");
|
|
1213
|
+
const recoveryStep = document.createElement("p");
|
|
1214
|
+
recoveryStep.textContent = t("recoveryStep");
|
|
1215
|
+
const recoveryNote = document.createElement("p");
|
|
1216
|
+
recoveryNote.className = "recovery-note";
|
|
1217
|
+
recoveryNote.textContent = t("recoveryNote");
|
|
1218
|
+
recovery.append(recoveryTitle, recoveryStep, recoveryNote);
|
|
1219
|
+
elements.stateMessage.append(recovery);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (retryMode) {
|
|
699
1223
|
const retry = document.createElement("button");
|
|
700
1224
|
retry.type = "button";
|
|
701
1225
|
retry.className = "retry-button";
|
|
702
|
-
retry.textContent =
|
|
703
|
-
|
|
1226
|
+
retry.textContent = retryMode === "authentication"
|
|
1227
|
+
? (accessToken ? t("retryAuthentication") : t("checkAuthentication"))
|
|
1228
|
+
: t("retry");
|
|
1229
|
+
retry.addEventListener(
|
|
1230
|
+
"click",
|
|
1231
|
+
retryMode === "authentication" ? retryAuthentication : refreshState,
|
|
1232
|
+
);
|
|
704
1233
|
elements.stateMessage.append(retry);
|
|
705
1234
|
}
|
|
706
1235
|
}
|
|
707
1236
|
|
|
1237
|
+
function retryAuthentication() {
|
|
1238
|
+
const refreshedContext = consumeLiveContext();
|
|
1239
|
+
accessToken = refreshedContext.accessToken;
|
|
1240
|
+
excludedSessionId = refreshedContext.excludedSessionId;
|
|
1241
|
+
|
|
1242
|
+
if (!accessToken) {
|
|
1243
|
+
viewState.hasLoaded = true;
|
|
1244
|
+
viewState.canRetry = false;
|
|
1245
|
+
viewState.authenticationFailed = true;
|
|
1246
|
+
viewState.errorKey = "missingToken";
|
|
1247
|
+
viewState.errorMessage = t(viewState.errorKey);
|
|
1248
|
+
setConnectionStatus("error", t("authenticationRequired"));
|
|
1249
|
+
render();
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
viewState.authenticationFailed = false;
|
|
1254
|
+
viewState.canRetry = true;
|
|
1255
|
+
viewState.errorKey = "";
|
|
1256
|
+
viewState.errorMessage = "";
|
|
1257
|
+
setConnectionStatus("connecting");
|
|
1258
|
+
render();
|
|
1259
|
+
void refreshState();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
708
1262
|
function setEmptyObservationMessage() {
|
|
709
1263
|
elements.stateMessage.className = "state-message state-empty state-empty-observation";
|
|
710
1264
|
elements.stateMessage.replaceChildren();
|
|
@@ -713,29 +1267,25 @@ function setEmptyObservationMessage() {
|
|
|
713
1267
|
const copy = document.createElement("span");
|
|
714
1268
|
|
|
715
1269
|
if (viewState.diagnostics.length) {
|
|
716
|
-
heading.textContent = "
|
|
717
|
-
copy.textContent =
|
|
1270
|
+
heading.textContent = t("emptyWithDiagnosticsTitle");
|
|
1271
|
+
copy.textContent = t("emptyWithDiagnosticsCopy", { count: viewState.diagnostics.length });
|
|
718
1272
|
} else {
|
|
719
|
-
heading.textContent = "
|
|
720
|
-
copy.textContent = "
|
|
1273
|
+
heading.textContent = t("emptyTitle");
|
|
1274
|
+
copy.textContent = t("emptyCopy");
|
|
721
1275
|
}
|
|
722
1276
|
|
|
723
1277
|
const guidance = document.createElement("div");
|
|
724
1278
|
guidance.className = "empty-guidance";
|
|
725
1279
|
|
|
726
1280
|
const guidanceTitle = document.createElement("h3");
|
|
727
|
-
guidanceTitle.textContent = "
|
|
1281
|
+
guidanceTitle.textContent = t("emptyGuidanceTitle");
|
|
728
1282
|
|
|
729
1283
|
const automaticTracking = document.createElement("p");
|
|
730
1284
|
automaticTracking.className = "automatic-tracking";
|
|
731
|
-
automaticTracking.textContent = "
|
|
1285
|
+
automaticTracking.textContent = t("automaticTracking");
|
|
732
1286
|
|
|
733
1287
|
const steps = document.createElement("ol");
|
|
734
|
-
for (const step of [
|
|
735
|
-
"플러그인을 설치한 뒤 공식 Codex 앱을 완전히 재시작했는지 확인합니다.",
|
|
736
|
-
"새 작업에서 표시되는 Codex Agent View hook 명령을 검토하고 직접 신뢰합니다.",
|
|
737
|
-
"신뢰 설정 후 새 작업을 시작해 작업 에이전트를 실행합니다. 새 활동은 이 목록에 자동으로 추가됩니다.",
|
|
738
|
-
]) {
|
|
1288
|
+
for (const step of [t("emptyStep1"), t("emptyStep2"), t("emptyStep3")]) {
|
|
739
1289
|
const item = document.createElement("li");
|
|
740
1290
|
item.textContent = step;
|
|
741
1291
|
steps.append(item);
|
|
@@ -743,15 +1293,16 @@ function setEmptyObservationMessage() {
|
|
|
743
1293
|
|
|
744
1294
|
const boundary = document.createElement("p");
|
|
745
1295
|
boundary.className = "observation-boundary";
|
|
746
|
-
boundary.textContent = "
|
|
1296
|
+
boundary.textContent = t("observationBoundary");
|
|
747
1297
|
|
|
748
1298
|
guidance.append(guidanceTitle, automaticTracking, steps, boundary);
|
|
749
1299
|
|
|
750
1300
|
if (viewState.diagnostics.length) {
|
|
751
|
-
const diagnostics = document.createElement("
|
|
752
|
-
diagnostics.className = "diagnostic-
|
|
753
|
-
const
|
|
754
|
-
|
|
1301
|
+
const diagnostics = document.createElement("div");
|
|
1302
|
+
diagnostics.className = "diagnostic-info";
|
|
1303
|
+
const diagnosticsTitle = document.createElement("p");
|
|
1304
|
+
diagnosticsTitle.className = "diagnostic-info-title";
|
|
1305
|
+
diagnosticsTitle.textContent = t("diagnosticsCount", { count: viewState.diagnostics.length });
|
|
755
1306
|
const codes = document.createElement("ul");
|
|
756
1307
|
const diagnosticCounts = new Map();
|
|
757
1308
|
for (const { code } of viewState.diagnostics) {
|
|
@@ -761,10 +1312,10 @@ function setEmptyObservationMessage() {
|
|
|
761
1312
|
const item = document.createElement("li");
|
|
762
1313
|
const code = document.createElement("code");
|
|
763
1314
|
code.textContent = diagnosticCode;
|
|
764
|
-
item.append(code, ` · ${count}
|
|
1315
|
+
item.append(code, ` · ${t("diagnosticOccurrences", { count })}`);
|
|
765
1316
|
codes.append(item);
|
|
766
1317
|
}
|
|
767
|
-
diagnostics.append(
|
|
1318
|
+
diagnostics.append(diagnosticsTitle, codes);
|
|
768
1319
|
guidance.append(diagnostics);
|
|
769
1320
|
}
|
|
770
1321
|
|
|
@@ -772,10 +1323,11 @@ function setEmptyObservationMessage() {
|
|
|
772
1323
|
}
|
|
773
1324
|
|
|
774
1325
|
function renderSessions() {
|
|
1326
|
+
const sessions = observableSessions();
|
|
775
1327
|
const visibleSessions = filteredSessions();
|
|
776
1328
|
const hasFilters = elements.search.value.trim() || elements.statusFilter.value !== "all";
|
|
777
1329
|
|
|
778
|
-
elements.toolbar.hidden =
|
|
1330
|
+
elements.toolbar.hidden = sessions.length === 0;
|
|
779
1331
|
elements.stateMessage.hidden = false;
|
|
780
1332
|
|
|
781
1333
|
elements.sessionList.replaceChildren();
|
|
@@ -784,34 +1336,34 @@ function renderSessions() {
|
|
|
784
1336
|
});
|
|
785
1337
|
|
|
786
1338
|
elements.resultsSummary.textContent = hasFilters
|
|
787
|
-
?
|
|
788
|
-
:
|
|
1339
|
+
? t("resultsFiltered", { total: sessions.length, visible: visibleSessions.length })
|
|
1340
|
+
: t("resultsTotal", { count: sessions.length });
|
|
789
1341
|
|
|
790
1342
|
if (!viewState.hasLoaded) {
|
|
791
1343
|
elements.sessionList.hidden = true;
|
|
792
|
-
setStateMessage("loading", "
|
|
1344
|
+
setStateMessage("loading", t("loadingState"), t("connectingCopy"));
|
|
793
1345
|
return;
|
|
794
1346
|
}
|
|
795
1347
|
|
|
796
1348
|
if (viewState.errorMessage) {
|
|
797
1349
|
elements.sessionList.hidden = !visibleSessions.length;
|
|
798
1350
|
const description = viewState.canRetry
|
|
799
|
-
?
|
|
800
|
-
? "
|
|
801
|
-
: "
|
|
802
|
-
: viewState.errorMessage;
|
|
1351
|
+
? sessions.length
|
|
1352
|
+
? t("retryWithState")
|
|
1353
|
+
: t("retryWithoutState")
|
|
1354
|
+
: viewState.errorKey ? t(viewState.errorKey) : viewState.errorMessage;
|
|
803
1355
|
setStateMessage(
|
|
804
1356
|
"error",
|
|
805
1357
|
viewState.canRetry
|
|
806
|
-
? "
|
|
807
|
-
: "
|
|
1358
|
+
? t("disconnectedTitle")
|
|
1359
|
+
: t("authTitle"),
|
|
808
1360
|
description,
|
|
809
|
-
viewState.canRetry,
|
|
1361
|
+
viewState.canRetry ? "connection" : "authentication",
|
|
810
1362
|
);
|
|
811
1363
|
return;
|
|
812
1364
|
}
|
|
813
1365
|
|
|
814
|
-
if (!
|
|
1366
|
+
if (!sessions.length) {
|
|
815
1367
|
elements.sessionList.hidden = true;
|
|
816
1368
|
setEmptyObservationMessage();
|
|
817
1369
|
return;
|
|
@@ -821,8 +1373,8 @@ function renderSessions() {
|
|
|
821
1373
|
elements.sessionList.hidden = true;
|
|
822
1374
|
setStateMessage(
|
|
823
1375
|
"empty",
|
|
824
|
-
"
|
|
825
|
-
"
|
|
1376
|
+
t("searchEmptyTitle"),
|
|
1377
|
+
t("searchEmptyCopy"),
|
|
826
1378
|
);
|
|
827
1379
|
return;
|
|
828
1380
|
}
|
|
@@ -840,9 +1392,9 @@ function render() {
|
|
|
840
1392
|
function setConnectionStatus(status, labelOverride = "") {
|
|
841
1393
|
elements.connectionStatus.dataset.status = status;
|
|
842
1394
|
const labels = {
|
|
843
|
-
connecting: "
|
|
844
|
-
connected: "
|
|
845
|
-
error: "
|
|
1395
|
+
connecting: t("connectionConnecting"),
|
|
1396
|
+
connected: t("connectionConnected"),
|
|
1397
|
+
error: t("connectionRetrying"),
|
|
846
1398
|
};
|
|
847
1399
|
elements.connectionLabel.textContent = labelOverride || labels[status];
|
|
848
1400
|
}
|
|
@@ -856,8 +1408,9 @@ async function refreshState() {
|
|
|
856
1408
|
viewState.hasLoaded = true;
|
|
857
1409
|
viewState.canRetry = false;
|
|
858
1410
|
viewState.authenticationFailed = true;
|
|
859
|
-
viewState.
|
|
860
|
-
|
|
1411
|
+
viewState.errorKey = "missingToken";
|
|
1412
|
+
viewState.errorMessage = t(viewState.errorKey);
|
|
1413
|
+
setConnectionStatus("error", t("authenticationRequired"));
|
|
861
1414
|
render();
|
|
862
1415
|
return;
|
|
863
1416
|
}
|
|
@@ -882,13 +1435,14 @@ async function refreshState() {
|
|
|
882
1435
|
viewState.hasLoaded = true;
|
|
883
1436
|
viewState.canRetry = false;
|
|
884
1437
|
viewState.authenticationFailed = true;
|
|
885
|
-
viewState.
|
|
886
|
-
|
|
1438
|
+
viewState.errorKey = "expiredToken";
|
|
1439
|
+
viewState.errorMessage = t(viewState.errorKey);
|
|
1440
|
+
setConnectionStatus("error", t("authenticationRequired"));
|
|
887
1441
|
return;
|
|
888
1442
|
}
|
|
889
1443
|
|
|
890
1444
|
if (!response.ok) {
|
|
891
|
-
throw new Error(
|
|
1445
|
+
throw new Error(t("requestFailed", { status: response.status }));
|
|
892
1446
|
}
|
|
893
1447
|
|
|
894
1448
|
const nextState = normalizeState(await response.json());
|
|
@@ -897,6 +1451,7 @@ async function refreshState() {
|
|
|
897
1451
|
viewState.diagnostics = nextState.diagnostics;
|
|
898
1452
|
viewState.hasLoaded = true;
|
|
899
1453
|
viewState.errorMessage = "";
|
|
1454
|
+
viewState.errorKey = "";
|
|
900
1455
|
viewState.canRetry = true;
|
|
901
1456
|
setConnectionStatus("connected");
|
|
902
1457
|
} catch (error) {
|
|
@@ -904,7 +1459,8 @@ async function refreshState() {
|
|
|
904
1459
|
viewState.canRetry = true;
|
|
905
1460
|
viewState.errorMessage = error instanceof Error
|
|
906
1461
|
? error.message
|
|
907
|
-
: "
|
|
1462
|
+
: t("unknownConnectionError");
|
|
1463
|
+
viewState.errorKey = "";
|
|
908
1464
|
setConnectionStatus("error");
|
|
909
1465
|
} finally {
|
|
910
1466
|
viewState.requestInFlight = false;
|
|
@@ -914,6 +1470,7 @@ async function refreshState() {
|
|
|
914
1470
|
|
|
915
1471
|
elements.search.addEventListener("input", renderSessions);
|
|
916
1472
|
elements.statusFilter.addEventListener("change", renderSessions);
|
|
1473
|
+
elements.language.addEventListener("change", (event) => setLanguage(event.target.value));
|
|
917
1474
|
elements.toolbar.addEventListener("submit", (event) => {
|
|
918
1475
|
event.preventDefault();
|
|
919
1476
|
});
|
|
@@ -923,7 +1480,8 @@ window.addEventListener("offline", () => {
|
|
|
923
1480
|
return;
|
|
924
1481
|
}
|
|
925
1482
|
viewState.canRetry = true;
|
|
926
|
-
viewState.errorMessage = "
|
|
1483
|
+
viewState.errorMessage = t("offline");
|
|
1484
|
+
viewState.errorKey = "offline";
|
|
927
1485
|
setConnectionStatus("error");
|
|
928
1486
|
render();
|
|
929
1487
|
});
|
|
@@ -933,6 +1491,7 @@ document.addEventListener("visibilitychange", () => {
|
|
|
933
1491
|
}
|
|
934
1492
|
});
|
|
935
1493
|
|
|
1494
|
+
applyStaticTranslations();
|
|
936
1495
|
render();
|
|
937
1496
|
refreshState();
|
|
938
1497
|
if (accessToken) {
|