codex-agent-view 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +6 -6
- package/README.md +126 -45
- package/bin/codex-agent-view.mjs +231 -20
- package/hooks/hooks.json +44 -0
- package/package.json +2 -2
- package/public/app.js +92 -10
- package/public/index.html +9 -4
- package/public/styles.css +72 -0
- package/scripts/send-hook.mjs +27 -1
- package/skills/codex-agent-view/SKILL.md +130 -10
- package/src/core/monitor-store.mjs +110 -2
- package/src/core/normalize-hook-payload.mjs +32 -3
package/bin/codex-agent-view.mjs
CHANGED
|
@@ -52,7 +52,7 @@ function printHelp() {
|
|
|
52
52
|
process.stdout.write(`Codex Agent View
|
|
53
53
|
|
|
54
54
|
Usage:
|
|
55
|
-
codex-agent-view start [--port <port>] [--
|
|
55
|
+
codex-agent-view start [--port <port>] [--open]
|
|
56
56
|
codex-agent-view status [--json]
|
|
57
57
|
codex-agent-view doctor [--json]
|
|
58
58
|
codex-agent-view install
|
|
@@ -60,12 +60,53 @@ Usage:
|
|
|
60
60
|
codex-agent-view --version
|
|
61
61
|
|
|
62
62
|
The monitor is read-only and binds only to 127.0.0.1.
|
|
63
|
+
Start prints the local URL without opening an external browser unless --open is set.
|
|
63
64
|
`);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
function parseStartArgs(args) {
|
|
68
|
+
let open = false;
|
|
69
|
+
let legacyNoOpen = false;
|
|
70
|
+
let port = DEFAULT_PORT;
|
|
71
|
+
let portSeen = false;
|
|
72
|
+
|
|
73
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
74
|
+
const argument = args[index];
|
|
75
|
+
if (argument === "--open") {
|
|
76
|
+
open = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (argument === "--no-open") {
|
|
80
|
+
legacyNoOpen = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (argument === "--port") {
|
|
84
|
+
if (portSeen) {
|
|
85
|
+
throw new Error("--port may only be specified once");
|
|
86
|
+
}
|
|
87
|
+
const value = args[index + 1];
|
|
88
|
+
if (value === undefined || value.startsWith("--")) {
|
|
89
|
+
throw new Error("--port requires a value");
|
|
90
|
+
}
|
|
91
|
+
port = Number(value);
|
|
92
|
+
portSeen = true;
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (argument.startsWith("-")) {
|
|
97
|
+
throw new Error(`unknown start option: ${argument}`);
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`unexpected start argument: ${argument}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (open && legacyNoOpen) {
|
|
103
|
+
throw new Error("--open and --no-open cannot be used together");
|
|
104
|
+
}
|
|
105
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
106
|
+
throw new Error("--port must be an integer from 0 to 65535");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { open, port };
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
function run(command, args, { allowFailure = false } = {}) {
|
|
@@ -114,11 +155,7 @@ function openBrowser(url) {
|
|
|
114
155
|
}
|
|
115
156
|
|
|
116
157
|
async function start(args) {
|
|
117
|
-
const
|
|
118
|
-
const port = requestedPort === undefined ? DEFAULT_PORT : Number(requestedPort);
|
|
119
|
-
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
120
|
-
throw new Error("--port must be an integer from 0 to 65535");
|
|
121
|
-
}
|
|
158
|
+
const options = parseStartArgs(args);
|
|
122
159
|
|
|
123
160
|
const runtime = await inspectRuntime();
|
|
124
161
|
if (runtime.kind === "unknown") {
|
|
@@ -130,10 +167,10 @@ async function start(args) {
|
|
|
130
167
|
throw new Error("a Codex Agent View monitor is already running; stop it before starting another");
|
|
131
168
|
}
|
|
132
169
|
|
|
133
|
-
const monitor = await startMonitorServer({ port });
|
|
170
|
+
const monitor = await startMonitorServer({ port: options.port });
|
|
134
171
|
process.stdout.write(`Codex Agent View is running at ${monitor.url}\n`);
|
|
135
172
|
process.stdout.write("Press Ctrl+C to stop the in-memory monitor.\n");
|
|
136
|
-
if (
|
|
173
|
+
if (options.open) {
|
|
137
174
|
openBrowser(monitor.url);
|
|
138
175
|
}
|
|
139
176
|
|
|
@@ -172,32 +209,172 @@ async function status(args) {
|
|
|
172
209
|
0,
|
|
173
210
|
);
|
|
174
211
|
process.stdout.write(`${sessions.length} task(s), ${agents} subagent(s) observed.\n`);
|
|
212
|
+
if (!Number.isFinite(snapshot.updated_at_ms) || snapshot.updated_at_ms <= 0) {
|
|
213
|
+
process.stdout.write(
|
|
214
|
+
"No hook event has reached this monitor yet. The monitor can be healthy while Codex skips an untrusted or not-yet-loaded hook. Run `codex-agent-view doctor`, review `/hooks` in Codex CLI, then use a new task.\n",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
175
217
|
for (const session of sessions) {
|
|
176
218
|
process.stdout.write(`- ${session.session_id}: ${session.status} (${session.agents.length} agents)\n`);
|
|
177
219
|
}
|
|
178
220
|
}
|
|
179
221
|
|
|
222
|
+
function parsePluginList(result) {
|
|
223
|
+
if (result.code !== 0) return null;
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(result.stdout);
|
|
226
|
+
return Array.isArray(parsed.installed) ? parsed.installed : [];
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function inspectInstalledHookBundle(pluginEntry) {
|
|
233
|
+
const sourcePath = pluginEntry?.source?.path;
|
|
234
|
+
if (typeof sourcePath !== "string" || sourcePath.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
hooks_file: false,
|
|
237
|
+
sender_file: false,
|
|
238
|
+
source_path: null,
|
|
239
|
+
wiring_ok: false,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const hooksPath = join(sourcePath, "hooks", "hooks.json");
|
|
243
|
+
const senderPath = join(sourcePath, "scripts", "send-hook.mjs");
|
|
244
|
+
const hooks = await readJsonRegularFile(hooksPath);
|
|
245
|
+
const sender = await pathExists(senderPath);
|
|
246
|
+
const hookGroups = hooks?.hooks;
|
|
247
|
+
const expectedEvents = [
|
|
248
|
+
"SessionStart",
|
|
249
|
+
"SessionEnd",
|
|
250
|
+
"UserPromptSubmit",
|
|
251
|
+
"Stop",
|
|
252
|
+
"SubagentStart",
|
|
253
|
+
"SubagentStop",
|
|
254
|
+
"PreToolUse",
|
|
255
|
+
"PostToolUse",
|
|
256
|
+
"PermissionRequest",
|
|
257
|
+
];
|
|
258
|
+
const hooksFile = hookGroups && typeof hookGroups === "object";
|
|
259
|
+
const declaredEvents = hooksFile
|
|
260
|
+
? expectedEvents.filter((event) => Array.isArray(hookGroups[event]))
|
|
261
|
+
: [];
|
|
262
|
+
return {
|
|
263
|
+
declared_events: declaredEvents,
|
|
264
|
+
hooks_file: Boolean(hooksFile),
|
|
265
|
+
sender_file: Boolean(sender?.isFile() && !sender.isSymbolicLink()),
|
|
266
|
+
source_path: sourcePath,
|
|
267
|
+
wiring_ok:
|
|
268
|
+
Boolean(hooksFile) &&
|
|
269
|
+
declaredEvents.length === expectedEvents.length &&
|
|
270
|
+
Boolean(sender?.isFile() && !sender.isSymbolicLink()),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
180
274
|
async function doctor(args) {
|
|
181
275
|
const codex = await run("codex", ["--version"], { allowFailure: true });
|
|
182
276
|
let monitor = { ok: false, message: "not running" };
|
|
183
277
|
try {
|
|
184
278
|
const snapshot = await fetchState();
|
|
185
|
-
monitor = {
|
|
279
|
+
monitor = {
|
|
280
|
+
events_received:
|
|
281
|
+
Number.isFinite(snapshot.updated_at_ms) && snapshot.updated_at_ms > 0,
|
|
282
|
+
ok: true,
|
|
283
|
+
sessions: snapshot.sessions?.length || 0,
|
|
284
|
+
updated_at_ms:
|
|
285
|
+
Number.isFinite(snapshot.updated_at_ms) && snapshot.updated_at_ms > 0
|
|
286
|
+
? snapshot.updated_at_ms
|
|
287
|
+
: null,
|
|
288
|
+
};
|
|
186
289
|
} catch (error) {
|
|
187
290
|
monitor = { ok: false, message: error.message };
|
|
188
291
|
}
|
|
189
292
|
const plugins = await run("codex", ["plugin", "list", "--json"], {
|
|
190
293
|
allowFailure: true,
|
|
191
294
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
295
|
+
const pluginList = parsePluginList(plugins);
|
|
296
|
+
const pluginEntry = pluginList?.find((entry) => entry.pluginId === PLUGIN_ID) || null;
|
|
297
|
+
const hookBundle = await inspectInstalledHookBundle(pluginEntry);
|
|
298
|
+
const diagnostics = [];
|
|
299
|
+
if (codex.code !== 0) {
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
action: "Install or repair Codex CLI so `codex --version` succeeds.",
|
|
302
|
+
code: "codex_cli_unavailable",
|
|
303
|
+
severity: "error",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (pluginList === null) {
|
|
307
|
+
diagnostics.push({
|
|
308
|
+
action: "Check `codex plugin list --json` and the installed Codex CLI version.",
|
|
309
|
+
code: "plugin_list_unavailable",
|
|
310
|
+
severity: "error",
|
|
311
|
+
});
|
|
312
|
+
} else if (!pluginEntry) {
|
|
313
|
+
diagnostics.push({
|
|
314
|
+
action: "Run `codex-agent-view install`.",
|
|
315
|
+
code: "plugin_not_installed",
|
|
316
|
+
severity: "error",
|
|
317
|
+
});
|
|
318
|
+
} else if (pluginEntry.enabled !== true) {
|
|
319
|
+
diagnostics.push({
|
|
320
|
+
action: "Enable Codex Agent View in the Codex plugin browser, then start a new task.",
|
|
321
|
+
code: "plugin_disabled",
|
|
322
|
+
severity: "error",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (pluginEntry && !hookBundle.wiring_ok) {
|
|
326
|
+
diagnostics.push({
|
|
327
|
+
action: "Run `codex-agent-view install` again to restore the owned plugin bundle.",
|
|
328
|
+
code: "hook_bundle_invalid",
|
|
329
|
+
severity: "error",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (pluginEntry && pluginEntry.version !== (await packageVersion())) {
|
|
333
|
+
diagnostics.push({
|
|
334
|
+
action: "Run `codex-agent-view install` to align the installed plugin with this CLI package.",
|
|
335
|
+
code: "plugin_version_mismatch",
|
|
336
|
+
severity: "warning",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (!monitor.ok) {
|
|
340
|
+
diagnostics.push({
|
|
341
|
+
action: "Run `codex-agent-view start` before expecting live task events.",
|
|
342
|
+
code: "monitor_not_running",
|
|
343
|
+
severity: "warning",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (pluginEntry) {
|
|
347
|
+
diagnostics.push({
|
|
348
|
+
action: "Review the current plugin hook definition in interactive Codex CLI `/hooks`.",
|
|
349
|
+
code: "hook_trust_unverified",
|
|
350
|
+
severity: "info",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (monitor.ok && monitor.events_received === false) {
|
|
354
|
+
diagnostics.push({
|
|
355
|
+
action:
|
|
356
|
+
"Open Codex CLI, review and trust the current definition in `/hooks`, then start a new task. Restart a Codex app process that was already open during installation.",
|
|
357
|
+
code: "no_hook_events_observed",
|
|
358
|
+
severity: "warning",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
197
361
|
const report = {
|
|
198
362
|
codex: { ok: codex.code === 0, version: codex.stdout.trim() || null },
|
|
363
|
+
diagnostics,
|
|
364
|
+
hook: {
|
|
365
|
+
...hookBundle,
|
|
366
|
+
trust: pluginEntry ? "unknown" : "not_applicable",
|
|
367
|
+
trust_note: pluginEntry
|
|
368
|
+
? "Codex CLI does not expose persisted hook trust through `plugin list --json`; review `/hooks` interactively."
|
|
369
|
+
: null,
|
|
370
|
+
},
|
|
199
371
|
monitor,
|
|
200
|
-
plugin: {
|
|
372
|
+
plugin: {
|
|
373
|
+
enabled: pluginEntry?.enabled === true,
|
|
374
|
+
installed: Boolean(pluginEntry),
|
|
375
|
+
source_path: pluginEntry?.source?.path || null,
|
|
376
|
+
version: pluginEntry?.version || null,
|
|
377
|
+
},
|
|
201
378
|
runtime_directory: runtimeDirectory(),
|
|
202
379
|
};
|
|
203
380
|
if (args.includes("--json")) {
|
|
@@ -205,8 +382,20 @@ async function doctor(args) {
|
|
|
205
382
|
return;
|
|
206
383
|
}
|
|
207
384
|
process.stdout.write(`Codex CLI: ${report.codex.ok ? report.codex.version : "not available"}\n`);
|
|
208
|
-
process.stdout.write(
|
|
385
|
+
process.stdout.write(
|
|
386
|
+
`Plugin: ${report.plugin.installed ? `installed (${report.plugin.enabled ? "enabled" : "disabled"})` : "not installed"}\n`,
|
|
387
|
+
);
|
|
388
|
+
if (report.plugin.installed) {
|
|
389
|
+
process.stdout.write(`Hook bundle: ${report.hook.wiring_ok ? "valid" : "invalid"}\n`);
|
|
390
|
+
process.stdout.write("Hook trust: unknown (Codex exposes review through interactive `/hooks`)\n");
|
|
391
|
+
}
|
|
209
392
|
process.stdout.write(`Monitor: ${monitor.ok ? "running" : monitor.message}\n`);
|
|
393
|
+
if (monitor.ok) {
|
|
394
|
+
process.stdout.write(`Hook events: ${monitor.events_received ? "observed" : "none observed"}\n`);
|
|
395
|
+
}
|
|
396
|
+
for (const diagnostic of diagnostics) {
|
|
397
|
+
process.stdout.write(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.action}\n`);
|
|
398
|
+
}
|
|
210
399
|
}
|
|
211
400
|
|
|
212
401
|
async function pathExists(path) {
|
|
@@ -362,8 +551,30 @@ async function install() {
|
|
|
362
551
|
await run("codex", ["plugin", "marketplace", "add", destination, "--json"]);
|
|
363
552
|
}
|
|
364
553
|
await run("codex", ["plugin", "add", PLUGIN_ID, "--json"]);
|
|
554
|
+
const verification = await run("codex", ["plugin", "list", "--json"], {
|
|
555
|
+
allowFailure: true,
|
|
556
|
+
});
|
|
557
|
+
const installed = parsePluginList(verification)?.find(
|
|
558
|
+
(entry) => entry.pluginId === PLUGIN_ID,
|
|
559
|
+
);
|
|
560
|
+
if (!installed) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
"Codex did not report the plugin as installed after registration; run `codex-agent-view doctor` for details",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (installed.enabled !== true) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
"the plugin was registered but is disabled; enable it in the Codex plugin browser, then run `codex-agent-view doctor`",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
365
570
|
process.stdout.write(`Installed ${PLUGIN_ID} from ${destination}.\n`);
|
|
366
|
-
process.stdout.write("
|
|
571
|
+
process.stdout.write("Registration verified: installed and enabled.\n");
|
|
572
|
+
process.stdout.write(
|
|
573
|
+
"Hook trust cannot be granted or inspected non-interactively. Review and trust this plugin's current hook definition in Codex CLI `/hooks`.\n",
|
|
574
|
+
);
|
|
575
|
+
process.stdout.write(
|
|
576
|
+
"If Codex was already open during installation, restart it completely, then create a new task.\n",
|
|
577
|
+
);
|
|
367
578
|
}
|
|
368
579
|
|
|
369
580
|
function isBroadRuntimeRoot(root) {
|
package/hooks/hooks.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "Capture privacy-minimized Codex lifecycle payloads for the local companion monitor.",
|
|
3
3
|
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SessionEnd": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
21
|
+
"timeout": 3
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"UserPromptSubmit": [
|
|
27
|
+
{
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
32
|
+
"timeout": 5
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"Stop": [
|
|
38
|
+
{
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
43
|
+
"timeout": 5
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
4
48
|
"SubagentStart": [
|
|
5
49
|
{
|
|
6
50
|
"hooks": [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-agent-view",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Read-only Codex app task view with an optional local live monitor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/core/index.mjs",
|
|
7
7
|
"exports": "./src/core/index.mjs",
|
package/public/app.js
CHANGED
|
@@ -65,6 +65,7 @@ const elements = Object.freeze({
|
|
|
65
65
|
const viewState = {
|
|
66
66
|
updatedAtMs: null,
|
|
67
67
|
sessions: [],
|
|
68
|
+
diagnostics: [],
|
|
68
69
|
hasLoaded: false,
|
|
69
70
|
errorMessage: "",
|
|
70
71
|
canRetry: true,
|
|
@@ -130,10 +131,22 @@ function normalizeActivity(value) {
|
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function normalizeDiagnostic(value) {
|
|
135
|
+
const diagnostic = isRecord(value) ? value : {};
|
|
136
|
+
return {
|
|
137
|
+
code: safeString(diagnostic.code, "unknown_diagnostic"),
|
|
138
|
+
diagnosedAtMs: safeTimestamp(diagnostic.diagnosed_at_ms),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
function deriveSessionStatus(session, agents, recentActivities) {
|
|
134
143
|
if (session.permission?.status === "waiting_for_user") {
|
|
135
144
|
return "waiting";
|
|
136
145
|
}
|
|
146
|
+
const reportedStatus = normalizeCoreStatus(session.status);
|
|
147
|
+
if (reportedStatus === "running" || reportedStatus === "completed") {
|
|
148
|
+
return reportedStatus;
|
|
149
|
+
}
|
|
137
150
|
if (
|
|
138
151
|
agents.some((agent) => agent.status === "running") ||
|
|
139
152
|
recentActivities[0]?.status === "running"
|
|
@@ -154,6 +167,7 @@ function normalizeSession(value, index) {
|
|
|
154
167
|
|
|
155
168
|
return {
|
|
156
169
|
sessionId: safeString(session.session_id, `unknown-session-${index + 1}`),
|
|
170
|
+
workspaceLabel: safeString(session.workspace_label, ""),
|
|
157
171
|
status: deriveSessionStatus(session, agents, recentActivities),
|
|
158
172
|
lastActivityAtMs: safeTimestamp(session.last_seen_at_ms),
|
|
159
173
|
agents,
|
|
@@ -166,7 +180,8 @@ function normalizeState(value) {
|
|
|
166
180
|
!isRecord(value) ||
|
|
167
181
|
value.schema_version !== 1 ||
|
|
168
182
|
value.source_of_truth !== "hook" ||
|
|
169
|
-
!Array.isArray(value.sessions)
|
|
183
|
+
!Array.isArray(value.sessions) ||
|
|
184
|
+
!Array.isArray(value.diagnostics)
|
|
170
185
|
) {
|
|
171
186
|
throw new TypeError("상태 응답 형식이 올바르지 않습니다.");
|
|
172
187
|
}
|
|
@@ -174,6 +189,7 @@ function normalizeState(value) {
|
|
|
174
189
|
return {
|
|
175
190
|
updatedAtMs: safeTimestamp(value.updated_at_ms),
|
|
176
191
|
sessions: value.sessions.map(normalizeSession),
|
|
192
|
+
diagnostics: value.diagnostics.map(normalizeDiagnostic),
|
|
177
193
|
};
|
|
178
194
|
}
|
|
179
195
|
|
|
@@ -354,11 +370,12 @@ function createSessionCard(session) {
|
|
|
354
370
|
identity.className = "session-identity";
|
|
355
371
|
const eyebrow = document.createElement("span");
|
|
356
372
|
eyebrow.className = "session-kind";
|
|
357
|
-
eyebrow.
|
|
358
|
-
const title = document.createElement("h3");
|
|
373
|
+
eyebrow.append("PARENT TASK · ");
|
|
359
374
|
const id = document.createElement("code");
|
|
360
375
|
id.textContent = session.sessionId;
|
|
361
|
-
|
|
376
|
+
eyebrow.append(id);
|
|
377
|
+
const title = document.createElement("h3");
|
|
378
|
+
title.textContent = session.workspaceLabel || "프로젝트 정보 없음";
|
|
362
379
|
identity.append(eyebrow, title);
|
|
363
380
|
|
|
364
381
|
const sessionState = document.createElement("div");
|
|
@@ -420,6 +437,7 @@ function sessionMatchesQuery(session, query) {
|
|
|
420
437
|
|
|
421
438
|
const searchableValues = [
|
|
422
439
|
session.sessionId,
|
|
440
|
+
session.workspaceLabel,
|
|
423
441
|
session.status,
|
|
424
442
|
...session.agents.flatMap((agent) => [agent.agentId, agent.agentType, agent.status]),
|
|
425
443
|
...session.recentActivities.flatMap((activity) => [
|
|
@@ -462,7 +480,7 @@ function renderMetrics() {
|
|
|
462
480
|
elements.metricWaiting.textContent = String(waitingCount);
|
|
463
481
|
elements.metricCompleted.textContent = String(countStatus("completed"));
|
|
464
482
|
elements.lastUpdated.textContent = !viewState.updatedAtMs
|
|
465
|
-
? "
|
|
483
|
+
? "수신된 hook 없음"
|
|
466
484
|
: formatDateTime(viewState.updatedAtMs);
|
|
467
485
|
}
|
|
468
486
|
|
|
@@ -486,10 +504,77 @@ function setStateMessage(kind, title, description, includeRetry = false) {
|
|
|
486
504
|
}
|
|
487
505
|
}
|
|
488
506
|
|
|
507
|
+
function setEmptyObservationMessage() {
|
|
508
|
+
elements.stateMessage.className = "state-message state-empty state-empty-observation";
|
|
509
|
+
elements.stateMessage.replaceChildren();
|
|
510
|
+
|
|
511
|
+
const heading = document.createElement("strong");
|
|
512
|
+
const copy = document.createElement("span");
|
|
513
|
+
|
|
514
|
+
if (viewState.diagnostics.length) {
|
|
515
|
+
heading.textContent = "표시 가능한 task가 없습니다.";
|
|
516
|
+
copy.textContent = `Monitor가 hook 입력 ${viewState.diagnostics.length}건을 받았지만 유효한 session으로 적용하지 않았습니다.`;
|
|
517
|
+
} else {
|
|
518
|
+
heading.textContent = "이 관찰 창에서 수신된 hook event가 0건입니다.";
|
|
519
|
+
copy.textContent = "로컬 monitor 연결은 정상입니다. 이 결과만으로 Codex에 실행 중인 task나 agent가 없다고 판단할 수 없습니다.";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const guidance = document.createElement("div");
|
|
523
|
+
guidance.className = "empty-guidance";
|
|
524
|
+
|
|
525
|
+
const guidanceTitle = document.createElement("h3");
|
|
526
|
+
guidanceTitle.textContent = "표시되지 않을 때 확인 순서";
|
|
527
|
+
|
|
528
|
+
const automaticTracking = document.createElement("p");
|
|
529
|
+
automaticTracking.className = "automatic-tracking";
|
|
530
|
+
automaticTracking.textContent = "task ID를 입력하거나 task별로 등록할 필요가 없습니다. Trusted hook event가 자동으로 이 목록에 추가됩니다.";
|
|
531
|
+
|
|
532
|
+
const steps = document.createElement("ol");
|
|
533
|
+
for (const step of [
|
|
534
|
+
"Codex Agent View를 실행한 뒤 새 Codex task를 시작합니다.",
|
|
535
|
+
"설치된 plugin의 현재 hook command를 검토하고 직접 trust했는지 확인합니다.",
|
|
536
|
+
"plugin 설치 또는 hook trust 변경 후 공식 Codex 앱을 완전히 재시작하고 새 task에서 subagent를 실행합니다.",
|
|
537
|
+
]) {
|
|
538
|
+
const item = document.createElement("li");
|
|
539
|
+
item.textContent = step;
|
|
540
|
+
steps.append(item);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const boundary = document.createElement("p");
|
|
544
|
+
boundary.className = "observation-boundary";
|
|
545
|
+
boundary.textContent = "Plugin 설치·trust 전에 이미 지나간 event와 monitor가 꺼져 있던 동안의 event는 재생되지 않습니다.";
|
|
546
|
+
|
|
547
|
+
guidance.append(guidanceTitle, automaticTracking, steps, boundary);
|
|
548
|
+
|
|
549
|
+
if (viewState.diagnostics.length) {
|
|
550
|
+
const diagnostics = document.createElement("details");
|
|
551
|
+
diagnostics.className = "diagnostic-details";
|
|
552
|
+
const summary = document.createElement("summary");
|
|
553
|
+
summary.textContent = `검증 diagnostic ${viewState.diagnostics.length}건`;
|
|
554
|
+
const codes = document.createElement("ul");
|
|
555
|
+
const diagnosticCounts = new Map();
|
|
556
|
+
for (const { code } of viewState.diagnostics) {
|
|
557
|
+
diagnosticCounts.set(code, (diagnosticCounts.get(code) ?? 0) + 1);
|
|
558
|
+
}
|
|
559
|
+
for (const [diagnosticCode, count] of diagnosticCounts) {
|
|
560
|
+
const item = document.createElement("li");
|
|
561
|
+
const code = document.createElement("code");
|
|
562
|
+
code.textContent = diagnosticCode;
|
|
563
|
+
item.append(code, ` · ${count}건`);
|
|
564
|
+
codes.append(item);
|
|
565
|
+
}
|
|
566
|
+
diagnostics.append(summary, codes);
|
|
567
|
+
guidance.append(diagnostics);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
elements.stateMessage.append(heading, copy, guidance);
|
|
571
|
+
}
|
|
572
|
+
|
|
489
573
|
function renderSessions() {
|
|
490
574
|
const visibleSessions = filteredSessions();
|
|
491
575
|
const hasFilters = elements.search.value.trim() || elements.statusFilter.value !== "all";
|
|
492
576
|
|
|
577
|
+
elements.toolbar.hidden = viewState.sessions.length === 0;
|
|
493
578
|
elements.stateMessage.hidden = false;
|
|
494
579
|
|
|
495
580
|
elements.sessionList.replaceChildren();
|
|
@@ -523,11 +608,7 @@ function renderSessions() {
|
|
|
523
608
|
|
|
524
609
|
if (!viewState.sessions.length) {
|
|
525
610
|
elements.sessionList.hidden = true;
|
|
526
|
-
|
|
527
|
-
"empty",
|
|
528
|
-
"아직 관찰된 task가 없습니다.",
|
|
529
|
-
"Codex에서 task나 subagent가 시작되면 이곳에 나타납니다.",
|
|
530
|
-
);
|
|
611
|
+
setEmptyObservationMessage();
|
|
531
612
|
return;
|
|
532
613
|
}
|
|
533
614
|
|
|
@@ -597,6 +678,7 @@ async function refreshState() {
|
|
|
597
678
|
const nextState = normalizeState(await response.json());
|
|
598
679
|
viewState.updatedAtMs = nextState.updatedAtMs;
|
|
599
680
|
viewState.sessions = nextState.sessions;
|
|
681
|
+
viewState.diagnostics = nextState.diagnostics;
|
|
600
682
|
viewState.hasLoaded = true;
|
|
601
683
|
viewState.errorMessage = "";
|
|
602
684
|
viewState.canRetry = true;
|
package/public/index.html
CHANGED
|
@@ -95,20 +95,25 @@
|
|
|
95
95
|
</p>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
|
-
<form
|
|
98
|
+
<form
|
|
99
|
+
class="toolbar"
|
|
100
|
+
role="search"
|
|
101
|
+
aria-label="자동 수신된 task와 subagent 목록 필터"
|
|
102
|
+
hidden
|
|
103
|
+
>
|
|
99
104
|
<div class="field field-search">
|
|
100
|
-
<label for="session-search"
|
|
105
|
+
<label for="session-search">목록 필터 (선택)</label>
|
|
101
106
|
<input
|
|
102
107
|
id="session-search"
|
|
103
108
|
name="query"
|
|
104
109
|
type="search"
|
|
105
|
-
placeholder="
|
|
110
|
+
placeholder="자동 수신된 task·agent 목록에서 찾기"
|
|
106
111
|
autocomplete="off"
|
|
107
112
|
spellcheck="false"
|
|
108
113
|
>
|
|
109
114
|
</div>
|
|
110
115
|
<div class="field">
|
|
111
|
-
<label for="status-filter"
|
|
116
|
+
<label for="status-filter">상태 필터 (선택)</label>
|
|
112
117
|
<select id="status-filter" name="status">
|
|
113
118
|
<option value="all">모든 상태</option>
|
|
114
119
|
<option value="running">실행 중</option>
|
package/public/styles.css
CHANGED
|
@@ -362,6 +362,10 @@ h1 {
|
|
|
362
362
|
gap: var(--space-3);
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
.toolbar[hidden] {
|
|
366
|
+
display: none;
|
|
367
|
+
}
|
|
368
|
+
|
|
365
369
|
.field {
|
|
366
370
|
display: grid;
|
|
367
371
|
gap: var(--space-2);
|
|
@@ -425,6 +429,74 @@ h1 {
|
|
|
425
429
|
color: var(--text-secondary);
|
|
426
430
|
}
|
|
427
431
|
|
|
432
|
+
.state-empty-observation {
|
|
433
|
+
justify-items: stretch;
|
|
434
|
+
text-align: left;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.state-empty-observation > strong,
|
|
438
|
+
.state-empty-observation > span {
|
|
439
|
+
justify-self: center;
|
|
440
|
+
text-align: center;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.empty-guidance {
|
|
444
|
+
width: min(42rem, 100%);
|
|
445
|
+
margin-top: var(--space-5);
|
|
446
|
+
padding: var(--space-4);
|
|
447
|
+
background: var(--surface-raised);
|
|
448
|
+
border: 1px solid var(--border-default);
|
|
449
|
+
border-radius: var(--radius-sm);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.empty-guidance h3 {
|
|
453
|
+
margin-bottom: var(--space-3);
|
|
454
|
+
font-size: var(--text-sm);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.automatic-tracking {
|
|
458
|
+
margin-bottom: var(--space-3);
|
|
459
|
+
color: var(--text-secondary);
|
|
460
|
+
font-size: var(--text-sm);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.empty-guidance ol {
|
|
464
|
+
margin: 0;
|
|
465
|
+
padding-left: 1.25rem;
|
|
466
|
+
color: var(--text-secondary);
|
|
467
|
+
font-size: var(--text-sm);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.empty-guidance li + li {
|
|
471
|
+
margin-top: var(--space-2);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.observation-boundary {
|
|
475
|
+
margin: var(--space-4) 0 0;
|
|
476
|
+
padding-top: var(--space-3);
|
|
477
|
+
color: var(--text-muted);
|
|
478
|
+
border-top: 1px solid var(--border-default);
|
|
479
|
+
font-size: var(--text-xs);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.diagnostic-details {
|
|
483
|
+
margin-top: var(--space-3);
|
|
484
|
+
color: var(--text-secondary);
|
|
485
|
+
font-size: var(--text-sm);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.diagnostic-details summary {
|
|
489
|
+
min-height: 2.75rem;
|
|
490
|
+
padding: var(--space-2) 0;
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
font-weight: 700;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.diagnostic-details ul {
|
|
496
|
+
margin: 0;
|
|
497
|
+
padding-left: 1.25rem;
|
|
498
|
+
}
|
|
499
|
+
|
|
428
500
|
.state-message.state-error {
|
|
429
501
|
background: var(--danger-soft);
|
|
430
502
|
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|