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/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 STATUS_LABELS = Object.freeze({
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 ACTIVITY_LABELS = Object.freeze({
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 TOOL_LABELS = Object.freeze({
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 AGENT_ROLE_LABELS = Object.freeze({
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 consumeAccessToken() {
57
- const fragment = new URLSearchParams(window.location.hash.slice(1));
58
- const fragmentToken = fragment.get("token")?.trim() || "";
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 (fragmentToken) {
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 token;
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
- const accessToken = consumeAccessToken();
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
- if (HIDDEN_AGENT_ROLES.has(key)) {
143
- return "";
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 TOOL_LABELS[key] || `도구 · ${sanitizedFallbackLabel(toolName, "이름 미상")}`;
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 `작업 에이전트 ${activity.agentOrdinal} 시작`;
649
+ return t("agentStarted", { ordinal: activity.agentOrdinal });
156
650
  }
157
651
  if (activity.eventName === "subagent_stopped" && activity.agentOrdinal !== null) {
158
- return `작업 에이전트 ${activity.agentOrdinal} 완료`;
652
+ return t("agentStopped", { ordinal: activity.agentOrdinal });
159
653
  }
160
654
  if (activity.eventName === "permission_requested" && activity.toolName) {
161
- return `${formatToolLabel(activity.toolName)} 승인 요청`;
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 ACTIVITY_LABELS[activity.eventName] ||
170
- `활동 · ${sanitizedFallbackLabel(activity.eventName, "알 수 없음")}`;
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("ko-KR", {
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("ko", { numeric: "auto" });
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 `${seconds}초`;
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 `${minutes}분`;
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 ? `${hours}시간 ${remainingMinutes}분` : `${hours}시간`;
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 = STATUS_LABELS[status];
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 createTechnicalDetails(rows) {
455
- const details = document.createElement("details");
456
- details.className = "technical-details";
954
+ function createTechnicalInfo(rows) {
955
+ const info = document.createElement("div");
956
+ info.className = "technical-info";
457
957
 
458
- const summary = document.createElement("summary");
459
- summary.textContent = "기술 정보";
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
- details.append(summary, list);
476
- return details;
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 = `작업 에이전트 ${agent.ordinal}`;
992
+ name.textContent = t("subagentName", { ordinal: agent.ordinal });
492
993
  identity.append(name);
493
994
 
494
995
  const roleLabel = formatAgentRole(agent.agentType);
495
- if (roleLabel) {
496
- const role = document.createElement("span");
497
- role.className = "agent-role";
498
- role.textContent = roleLabel;
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 = [["에이전트 ID", agent.agentId]];
511
- if (roleLabel) {
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, createTechnicalDetails(technicalRows));
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 = [["원본 이벤트", activity.eventName]];
1036
+ const technicalRows = [[t("rawEvent"), activity.eventName]];
540
1037
  if (activity.toolName) {
541
- technicalRows.push(["원본 도구", activity.toolName]);
1038
+ technicalRows.push([t("rawTool"), activity.toolName]);
542
1039
  }
543
1040
 
544
- content.append(title, metadata, createTechnicalDetails(technicalRows));
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
- identity.append(
573
- eyebrow,
574
- title,
575
- createTechnicalDetails([["세션 ID", session.sessionId]]),
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 = `작업 에이전트 · ${session.agents.length}`;
594
- agentPanel.append(agentTitle);
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 [...viewState.sessions]
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 agents = viewState.sessions.flatMap((session) => session.agents);
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(viewState.sessions.length);
1185
+ elements.metricSessions.textContent = String(sessions.length);
677
1186
  elements.metricRunning.textContent = String(countStatus("running"));
678
- const waitingCount = countStatus("waiting") + viewState.sessions.filter(
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, includeRetry = false) {
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 (includeRetry) {
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
- retry.addEventListener("click", refreshState);
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 = `로컬 모니터가 활동 정보 ${viewState.diagnostics.length}건을 받았지만 표시 가능한 부모 작업으로 적용하지 못했습니다.`;
1270
+ heading.textContent = t("emptyWithDiagnosticsTitle");
1271
+ copy.textContent = t("emptyWithDiagnosticsCopy", { count: viewState.diagnostics.length });
718
1272
  } else {
719
- heading.textContent = "이 관찰 화면에서 수신된 작업 활동이 없습니다.";
720
- copy.textContent = "로컬 모니터 연결은 정상입니다. 이 결과만으로 Codex에 실행 중인 부모 작업이나 작업 에이전트가 없다고 판단할 수 없습니다.";
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 = "작업 ID를 입력하거나 작업별로 등록할 필요가 없습니다. 신뢰한 hook의 작업 활동이 이 목록에 자동으로 추가됩니다.";
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 = "관찰 화면은 첫 번째로 신뢰한 hook을 받은 시점부터 시작합니다. 그 전에 이미 지나간 활동과 로컬 상태 수집이 중단된 동안의 활동은 재생되지 않으며, 수집이 다시 시작되면 새 관찰 화면이 열립니다.";
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("details");
752
- diagnostics.className = "diagnostic-details";
753
- const summary = document.createElement("summary");
754
- summary.textContent = `검증 정보 ${viewState.diagnostics.length}건`;
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(summary, codes);
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 = viewState.sessions.length === 0;
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
- ? `전체 ${viewState.sessions.length}개 ${visibleSessions.length}개 표시`
788
- : `${viewState.sessions.length}개 부모 작업`;
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
- ? viewState.sessions.length
800
- ? "2초마다 자동으로 다시 연결합니다. 마지막 정상 상태를 계속 표시합니다."
801
- : "2초마다 자동으로 다시 연결합니다. Codex 앱에서 이 화면을 그대로 두어도 됩니다."
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 (!viewState.sessions.length) {
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.errorMessage = "이 탭에는 접근 토큰이 없습니다. Codex 앱에서 Codex Agent View의 실시간 화면 열기를 다시 요청하세요.";
860
- setConnectionStatus("error", "실시간 화면 인증 필요");
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.errorMessage = "이 실시간 화면의 인증이 더 이상 유효하지 않습니다. Codex 앱에서 Codex Agent View의 실시간 화면 열기를 다시 요청하세요.";
886
- setConnectionStatus("error", "실시간 화면 인증 필요");
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(`상태 요청 실패 (${response.status})`);
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) {