codex-agent-view 0.3.2 → 0.4.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.
@@ -442,16 +442,90 @@ async function inspectRuntime() {
442
442
  }
443
443
  }
444
444
 
445
- async function runtimeResponds(runtime) {
445
+ async function runtimeEndpointState(runtime) {
446
446
  try {
447
- await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
447
+ const response = await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
448
448
  headers: { authorization: `Bearer ${runtime.token}` },
449
449
  signal: AbortSignal.timeout(1_500),
450
450
  });
451
- return true;
451
+ if (!response.ok) {
452
+ await response.body?.cancel();
453
+ return "unrelated";
454
+ }
455
+ const snapshot = await response.json().catch(() => null);
456
+ return snapshot?.schema_version === 1 && snapshot?.source_of_truth === "hook"
457
+ ? "owned"
458
+ : "unrelated";
452
459
  } catch {
460
+ return "absent";
461
+ }
462
+ }
463
+
464
+ async function runtimeResponds(runtime) {
465
+ return (await runtimeEndpointState(runtime)) === "owned";
466
+ }
467
+
468
+ async function stopRunningRuntime(preflight) {
469
+ if (preflight.kind !== "valid") {
470
+ return false;
471
+ }
472
+ const endpointState = await runtimeEndpointState(preflight.info);
473
+ if (endpointState === "absent") {
453
474
  return false;
454
475
  }
476
+ if (endpointState === "unrelated") {
477
+ throw new Error(
478
+ "the runtime endpoint was not identified as an owned monitor; plugin and runtime files were preserved",
479
+ );
480
+ }
481
+
482
+ let response;
483
+ try {
484
+ response = await fetch(
485
+ `http://${preflight.info.host}:${preflight.info.port}/api/internal/shutdown`,
486
+ {
487
+ headers: { authorization: `Bearer ${preflight.info.token}` },
488
+ method: "POST",
489
+ signal: AbortSignal.timeout(1_500),
490
+ },
491
+ );
492
+ } catch {
493
+ throw new Error("the owned monitor could not be stopped; plugin files were preserved");
494
+ }
495
+ if (!response.ok) {
496
+ throw new Error(
497
+ `the owned monitor refused shutdown with HTTP ${response.status}; plugin files were preserved`,
498
+ );
499
+ }
500
+ const result = await response.json().catch(() => null);
501
+ if (result?.status !== "shutting_down") {
502
+ throw new Error(
503
+ "the owned monitor returned an invalid shutdown response; plugin files were preserved",
504
+ );
505
+ }
506
+
507
+ const deadline = Date.now() + 3_000;
508
+ while (Date.now() < deadline) {
509
+ const current = await inspectRuntime();
510
+ if (current.kind === "absent") {
511
+ return true;
512
+ }
513
+ if (current.kind === "unknown") {
514
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
515
+ continue;
516
+ }
517
+ if (current.info.token !== preflight.info.token) {
518
+ throw new Error(
519
+ "runtime ownership changed during uninstall; new or unrecognized runtime data was preserved",
520
+ );
521
+ }
522
+ if (!(await runtimeResponds(current.info))) {
523
+ await removeRuntimeInfo(current.info.token);
524
+ return true;
525
+ }
526
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
527
+ }
528
+ throw new Error("the owned monitor did not stop in time; plugin files were preserved");
455
529
  }
456
530
 
457
531
  async function inspectPluginBundle(destination) {
@@ -593,12 +667,19 @@ async function purgeStaleRuntime(preflight) {
593
667
  }
594
668
 
595
669
  const current = await inspectRuntime();
670
+ if (current.kind === "absent") {
671
+ return false;
672
+ }
596
673
  if (current.kind !== "valid" || current.info.token !== preflight.info.token) {
597
674
  return true;
598
675
  }
599
- if (await runtimeResponds(current.info)) {
676
+ const endpointState = await runtimeEndpointState(current.info);
677
+ if (endpointState === "owned") {
600
678
  throw new Error("the Codex Agent View monitor started during uninstall; runtime data was preserved");
601
679
  }
680
+ if (endpointState === "unrelated") {
681
+ return true;
682
+ }
602
683
  await removeRuntimeInfo(current.info.token);
603
684
  return false;
604
685
  }
@@ -624,7 +705,28 @@ async function removeRuntimeRootIfEmpty(root) {
624
705
  }
625
706
  }
626
707
 
708
+ function parseUninstallArgs(args) {
709
+ let purge = false;
710
+
711
+ for (const argument of args) {
712
+ if (argument === "--purge") {
713
+ if (purge) {
714
+ throw new Error("--purge may only be specified once");
715
+ }
716
+ purge = true;
717
+ continue;
718
+ }
719
+ if (argument.startsWith("-")) {
720
+ throw new Error(`unknown uninstall option: ${argument}`);
721
+ }
722
+ throw new Error(`unexpected uninstall argument: ${argument}`);
723
+ }
724
+
725
+ return { purge };
726
+ }
727
+
627
728
  async function uninstall(args) {
729
+ const { purge } = parseUninstallArgs(args);
628
730
  const root = runtimeDirectory();
629
731
  const bundle = join(root, "marketplace");
630
732
  if (dirname(bundle) !== root || bundle === root) {
@@ -636,16 +738,8 @@ async function uninstall(args) {
636
738
  throw unmanagedBundleError(bundle);
637
739
  }
638
740
 
639
- const purge = args.includes("--purge");
640
- const runtimePreflight = purge ? await inspectRuntime() : { kind: "absent" };
641
- if (
642
- runtimePreflight.kind === "valid" &&
643
- (await runtimeResponds(runtimePreflight.info))
644
- ) {
645
- throw new Error(
646
- "the Codex Agent View monitor is running; stop it before uninstalling with --purge",
647
- );
648
- }
741
+ const runtimePreflight = await inspectRuntime();
742
+ const stoppedMonitor = await stopRunningRuntime(runtimePreflight);
649
743
 
650
744
  await run("codex", ["plugin", "remove", PLUGIN_ID, "--json"], { allowFailure: true });
651
745
  await run("codex", ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"], {
@@ -668,7 +762,11 @@ async function uninstall(args) {
668
762
  );
669
763
  }
670
764
  } else {
671
- process.stdout.write("Removed plugin and marketplace bundle. Runtime data was preserved.\n");
765
+ process.stdout.write(
766
+ stoppedMonitor
767
+ ? "Stopped the owned monitor and removed the plugin and marketplace bundle.\n"
768
+ : "Removed plugin and marketplace bundle. Runtime data was preserved.\n",
769
+ );
672
770
  }
673
771
  }
674
772
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codex-agent-view",
3
- "version": "0.3.2",
4
- "description": "Read-only Codex app task view with an optional local live monitor.",
3
+ "version": "0.4.0",
4
+ "description": "Read-only Codex app task view with a trusted-hook auto-prepared local live backend.",
5
5
  "type": "module",
6
6
  "main": "./src/core/index.mjs",
7
7
  "exports": "./src/core/index.mjs",
@@ -15,11 +15,13 @@
15
15
  "bin/",
16
16
  "hooks/",
17
17
  "public/",
18
+ "scripts/auto-start-monitor.mjs",
18
19
  "scripts/capture-hook.mjs",
19
20
  "scripts/send-hook.mjs",
20
21
  "skills/",
21
22
  "src/",
22
23
  "README.md",
24
+ "README.ko.md",
23
25
  "LICENSE",
24
26
  "NOTICE"
25
27
  ],
package/public/app.js CHANGED
@@ -69,6 +69,7 @@ const viewState = {
69
69
  hasLoaded: false,
70
70
  errorMessage: "",
71
71
  canRetry: true,
72
+ authenticationFailed: false,
72
73
  requestInFlight: false,
73
74
  };
74
75
 
@@ -531,9 +532,9 @@ function setEmptyObservationMessage() {
531
532
 
532
533
  const steps = document.createElement("ol");
533
534
  for (const step of [
534
- "Codex Agent View를 실행한 Codex task를 시작합니다.",
535
- "설치된 plugin의 현재 hook command를 검토하고 직접 trust했는지 확인합니다.",
536
- "plugin 설치 또는 hook trust 변경 공식 Codex 앱을 완전히 재시작하고 task에서 subagent를 실행합니다.",
535
+ "Plugin을 설치한공식 Codex 앱을 완전히 재시작했는지 확인합니다.",
536
+ " task에서 표시되는 Codex Agent View hook command를 검토하고 직접 trust합니다.",
537
+ "Trust 이후 task를 시작해 subagent 작업을 실행합니다. Hook event가 목록에 자동으로 추가됩니다.",
537
538
  ]) {
538
539
  const item = document.createElement("li");
539
540
  item.textContent = step;
@@ -542,7 +543,7 @@ function setEmptyObservationMessage() {
542
543
 
543
544
  const boundary = document.createElement("p");
544
545
  boundary.className = "observation-boundary";
545
- boundary.textContent = "Plugin 설치·trust 전에 이미 지나간 event와 monitor가 꺼져 있던 동안의 event는 재생되지 않습니다.";
546
+ boundary.textContent = "관찰 window는 첫 trusted hook에서 시작합니다. 그 전에 이미 지나간 event와 로컬 상태 수집이 중단된 동안의 event는 재생되지 않으며, 수집이 다시 시작되면 새 관찰 window가 열립니다.";
546
547
 
547
548
  guidance.append(guidanceTitle, automaticTracking, steps, boundary);
548
549
 
@@ -594,12 +595,16 @@ function renderSessions() {
594
595
 
595
596
  if (viewState.errorMessage) {
596
597
  elements.sessionList.hidden = !visibleSessions.length;
597
- const description = viewState.sessions.length
598
- ? "마지막 정상 상태를 계속 표시합니다."
598
+ const description = viewState.canRetry
599
+ ? viewState.sessions.length
600
+ ? "2초마다 자동으로 다시 연결합니다. 마지막 정상 상태를 계속 표시합니다."
601
+ : "2초마다 자동으로 다시 연결합니다. Codex 앱에서 이 화면을 그대로 두어도 됩니다."
599
602
  : viewState.errorMessage;
600
603
  setStateMessage(
601
604
  "error",
602
- "로컬 monitor에 연결할 수 없습니다.",
605
+ viewState.canRetry
606
+ ? "로컬 상태 연결이 끊겨 다시 시도 중입니다."
607
+ : "이 live view를 인증할 수 없습니다.",
603
608
  description,
604
609
  viewState.canRetry,
605
610
  );
@@ -632,26 +637,27 @@ function render() {
632
637
  renderSessions();
633
638
  }
634
639
 
635
- function setConnectionStatus(status) {
640
+ function setConnectionStatus(status, labelOverride = "") {
636
641
  elements.connectionStatus.dataset.status = status;
637
642
  const labels = {
638
643
  connecting: "로컬 상태 연결 중",
639
644
  connected: "로컬 monitor 연결됨",
640
- error: "연결 끊김",
645
+ error: "연결 끊김 · 재시도 중",
641
646
  };
642
- elements.connectionLabel.textContent = labels[status];
647
+ elements.connectionLabel.textContent = labelOverride || labels[status];
643
648
  }
644
649
 
645
650
  async function refreshState() {
646
- if (viewState.requestInFlight) {
651
+ if (viewState.requestInFlight || viewState.authenticationFailed) {
647
652
  return;
648
653
  }
649
654
 
650
655
  if (!accessToken) {
651
656
  viewState.hasLoaded = true;
652
657
  viewState.canRetry = false;
653
- viewState.errorMessage = "접근 token이 없습니다. monitor를 다시 실행해 새 주소를 여세요.";
654
- setConnectionStatus("error");
658
+ viewState.authenticationFailed = true;
659
+ viewState.errorMessage = "이 탭에는 접근 token이 없습니다. Codex 앱에서 Codex Agent View에 live view 열기를 다시 요청하세요.";
660
+ setConnectionStatus("error", "live view 인증 필요");
655
661
  render();
656
662
  return;
657
663
  }
@@ -671,6 +677,16 @@ async function refreshState() {
671
677
  },
672
678
  });
673
679
 
680
+ if (response.status === 401 || response.status === 403) {
681
+ await response.body?.cancel();
682
+ viewState.hasLoaded = true;
683
+ viewState.canRetry = false;
684
+ viewState.authenticationFailed = true;
685
+ viewState.errorMessage = "이 live view의 인증이 더 이상 유효하지 않습니다. Codex 앱에서 Codex Agent View에 live view 열기를 다시 요청하세요.";
686
+ setConnectionStatus("error", "live view 인증 필요");
687
+ return;
688
+ }
689
+
674
690
  if (!response.ok) {
675
691
  throw new Error(`상태 요청 실패 (${response.status})`);
676
692
  }
@@ -685,6 +701,7 @@ async function refreshState() {
685
701
  setConnectionStatus("connected");
686
702
  } catch (error) {
687
703
  viewState.hasLoaded = true;
704
+ viewState.canRetry = true;
688
705
  viewState.errorMessage = error instanceof Error
689
706
  ? error.message
690
707
  : "알 수 없는 연결 오류가 발생했습니다.";
@@ -702,6 +719,10 @@ elements.toolbar.addEventListener("submit", (event) => {
702
719
  });
703
720
  window.addEventListener("online", refreshState);
704
721
  window.addEventListener("offline", () => {
722
+ if (viewState.authenticationFailed) {
723
+ return;
724
+ }
725
+ viewState.canRetry = true;
705
726
  viewState.errorMessage = "이 기기가 오프라인입니다.";
706
727
  setConnectionStatus("error");
707
728
  render();
package/public/index.html CHANGED
@@ -126,7 +126,7 @@
126
126
 
127
127
  <div id="state-message" class="state-message" role="status" aria-live="polite">
128
128
  <strong>상태를 불러오는 중입니다.</strong>
129
- <span>로컬 monitor에 연결하고 있습니다.</span>
129
+ <span>Codex 앱의 로컬 상태에 연결하고 있습니다.</span>
130
130
  </div>
131
131
 
132
132
  <ul id="session-list" class="session-list" aria-label="Codex 부모 task 목록" hidden></ul>
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { startMonitorServer } from "../src/runtime/server.mjs";
7
+ import { autoStartPort, readRuntimeInfo } from "../src/runtime/config.mjs";
8
+
9
+ const HEALTH_TIMEOUT_MS = 350;
10
+ const OWNER_CHECK_INTERVAL_MS = 1_000;
11
+ const OWNER_FILE = fileURLToPath(
12
+ new URL("../.codex-plugin/plugin.json", import.meta.url),
13
+ );
14
+
15
+ async function currentMonitorIsHealthy() {
16
+ try {
17
+ const runtime = await readRuntimeInfo();
18
+ const response = await fetch(
19
+ `http://${runtime.host}:${runtime.port}/api/state`,
20
+ {
21
+ headers: { authorization: `Bearer ${runtime.token}` },
22
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
23
+ },
24
+ );
25
+ return response.ok;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async function main() {
32
+ let monitor;
33
+ let ownerCheck;
34
+ let stopRequested = false;
35
+ let stopping = false;
36
+ const stop = async () => {
37
+ stopRequested = true;
38
+ if (!monitor || stopping) {
39
+ return;
40
+ }
41
+ stopping = true;
42
+ clearInterval(ownerCheck);
43
+ try {
44
+ await monitor.close();
45
+ } finally {
46
+ process.exit(0);
47
+ }
48
+ };
49
+
50
+ process.once("SIGINT", stop);
51
+ process.once("SIGTERM", stop);
52
+
53
+ if (await currentMonitorIsHealthy()) {
54
+ process.off("SIGINT", stop);
55
+ process.off("SIGTERM", stop);
56
+ return;
57
+ }
58
+
59
+ try {
60
+ monitor = await startMonitorServer({ port: autoStartPort() });
61
+ } catch (error) {
62
+ process.off("SIGINT", stop);
63
+ process.off("SIGTERM", stop);
64
+ throw error;
65
+ }
66
+ if (stopRequested) {
67
+ await stop();
68
+ return;
69
+ }
70
+
71
+ // An npm/plugin uninstall removes this owned bundle. The detached monitor
72
+ // then shuts itself down and removes only the runtime file it still owns.
73
+ ownerCheck = setInterval(async () => {
74
+ try {
75
+ await access(OWNER_FILE);
76
+ } catch (error) {
77
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
78
+ await stop();
79
+ }
80
+ }
81
+ }, OWNER_CHECK_INTERVAL_MS);
82
+ ownerCheck.unref();
83
+ }
84
+
85
+ // Auto-start is an internal hook implementation detail. It must never write a
86
+ // tokenized URL, a local path, or hook-derived data to stdout/stderr.
87
+ main().catch(() => {
88
+ process.exitCode = 0;
89
+ });
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { spawn } from "node:child_process";
3
4
  import { basename } from "node:path";
5
+ import { fileURLToPath } from "node:url";
4
6
 
5
7
  import { minimizePayload } from "./capture-hook.mjs";
6
8
  import { readRuntimeInfo } from "../src/runtime/config.mjs";
7
9
 
8
10
  const MAX_STDIN_BYTES = 2 * 1024 * 1024;
9
- const SEND_TIMEOUT_MS = 750;
11
+ const SEND_TIMEOUT_MS = 500;
12
+ const AUTO_START_WAIT_MS = 1_600;
13
+ const AUTO_START_POLL_MS = 40;
10
14
  const MAX_WORKSPACE_LABEL_LENGTH = 120;
11
15
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g;
12
16
 
@@ -55,8 +59,7 @@ function monitorEnvelope(payload) {
55
59
  : minimized;
56
60
  }
57
61
 
58
- async function send(payload) {
59
- const runtime = await readRuntimeInfo();
62
+ async function sendToRuntime(runtime, envelope) {
60
63
  const response = await fetch(
61
64
  `http://${runtime.host}:${runtime.port}/api/events`,
62
65
  {
@@ -65,7 +68,7 @@ async function send(payload) {
65
68
  authorization: `Bearer ${runtime.token}`,
66
69
  "content-type": "application/json",
67
70
  },
68
- body: JSON.stringify(monitorEnvelope(payload)),
71
+ body: JSON.stringify(envelope),
69
72
  signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
70
73
  },
71
74
  );
@@ -74,6 +77,66 @@ async function send(payload) {
74
77
  }
75
78
  }
76
79
 
80
+ function childEnvironment() {
81
+ const env = {};
82
+ for (const key of [
83
+ "CODEX_AGENT_VIEW_AUTO_START_PORT",
84
+ "CODEX_AGENT_VIEW_RUNTIME_DIR",
85
+ "SystemRoot",
86
+ ]) {
87
+ if (typeof process.env[key] === "string") {
88
+ env[key] = process.env[key];
89
+ }
90
+ }
91
+ return env;
92
+ }
93
+
94
+ function startMonitorDetached() {
95
+ const child = spawn(
96
+ process.execPath,
97
+ [fileURLToPath(new URL("./auto-start-monitor.mjs", import.meta.url))],
98
+ {
99
+ detached: true,
100
+ env: childEnvironment(),
101
+ shell: false,
102
+ stdio: "ignore",
103
+ },
104
+ );
105
+ child.on("error", () => {});
106
+ child.unref();
107
+ }
108
+
109
+ async function delay(milliseconds) {
110
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
111
+ }
112
+
113
+ async function tryDelivery(envelope) {
114
+ try {
115
+ await sendToRuntime(await readRuntimeInfo(), envelope);
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ async function send(payload) {
123
+ const envelope = monitorEnvelope(payload);
124
+ if (await tryDelivery(envelope)) {
125
+ return;
126
+ }
127
+
128
+ startMonitorDetached();
129
+ const deadline = Date.now() + AUTO_START_WAIT_MS;
130
+ do {
131
+ await delay(AUTO_START_POLL_MS);
132
+ if (await tryDelivery(envelope)) {
133
+ return;
134
+ }
135
+ } while (Date.now() < deadline);
136
+
137
+ throw new Error("monitor auto-start delivery timed out");
138
+ }
139
+
77
140
  async function main() {
78
141
  try {
79
142
  await send(await readStdin());
@@ -109,6 +109,13 @@ means that monitor process observed no hook events; it does not prove that the
109
109
  Codex app has no tasks. Restarting the in-memory monitor begins a new bounded
110
110
  observation window.
111
111
 
112
+ After explicit installation, hook review/trust, and a Codex app restart, the
113
+ first trusted hook normally prepares the local backend internally and retries
114
+ delivery of that same privacy-minimized event. The user never registers a task
115
+ ID or runs `start`, `status`, or `doctor` as part of ordinary use. A bounded
116
+ auto-start failure remains fail-open and does not create a persistent replay
117
+ queue.
118
+
112
119
  ## Open the live view only on request
113
120
 
114
121
  Only when the user explicitly asks to open, show, or start the live view:
@@ -117,7 +124,15 @@ The plugin agent performs the health check and any required start internally.
117
124
  The user's entire interaction after installation remains inside the official
118
125
  Codex app; do not turn the commands below into instructions for the user.
119
126
 
120
- 1. Check monitor health with the packaged CLI.
127
+ The public Codex plugin API cannot create a sidebar, panel, or Browser tab
128
+ without a prompt at app startup. The first live view therefore requires one
129
+ explicit request in a Codex app task. Do not claim that installation alone
130
+ opens a screen. An already-open in-app live tab refreshes and reconnects after
131
+ temporary disconnects while the same monitor observation window and its
132
+ session token remain valid.
133
+
134
+ 1. Check monitor health with the packaged CLI. A trusted hook may already have
135
+ prepared it automatically.
121
136
  2. If it is not running, start it with `codex-agent-view start --no-open` so the
122
137
  CLI never launches the operating system's external browser.
123
138
  3. Keep the returned tokenized localhost URL private. Never quote it, place it
@@ -133,6 +148,10 @@ Do not restart or replace a healthy monitor merely to recover its URL because
133
148
  that would discard its in-memory observation window. Reuse an existing in-app
134
149
  monitor tab when possible. Do not close user-owned browser tabs.
135
150
 
151
+ If an existing tab has lost its session token or the monitor restarted, do not
152
+ promise automatic recovery across observation windows. Reopen the live view
153
+ through the same explicit in-app workflow without exposing the private URL.
154
+
136
155
  ## Lifecycle and safety
137
156
 
138
157
  Run `codex-agent-view install` or `codex-agent-view uninstall` only when the
@@ -140,7 +159,15 @@ user explicitly requests that lifecycle action. Explain that install changes
140
159
  local Codex plugin registration and requires hook review/trust. Before
141
160
  uninstalling, distinguish the default command, which preserves runtime data,
142
161
  from `codex-agent-view uninstall --purge`, which removes the configured runtime
143
- directory.
162
+ directory only within its owned-file safety boundary. Do not ask the user to
163
+ stop an auto-started or foreground monitor first. The uninstall command uses
164
+ the validated runtime bearer token to authenticate and internally shut down a
165
+ healthy owned monitor before removing plugin files. The default command
166
+ preserves remaining runtime-directory data. `--purge` additionally removes
167
+ only an owned stale runtime file and an empty runtime directory; it preserves
168
+ unrecognized files, unrelated loopback services, and non-empty directories.
169
+ If an owned monitor cannot be stopped safely or the endpoint is unrelated,
170
+ report that removal stopped with plugin and runtime files preserved.
144
171
 
145
172
  Keep every workflow read-only with respect to Codex tasks. Never stop or
146
173
  restart a task or subagent, send a message to an agent, approve or deny a
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: show-agents
3
+ description: Open the Codex Agent View live task and subagent monitor in the official Codex app. Use when the user explicitly selects the bundled Show Agents skill from the app's @ menu.
4
+ ---
5
+
6
+ # Show Agents
7
+
8
+ Treat selection of the bundled **Show Agents** skill from the Codex app's `@`
9
+ menu as an explicit request to open the live monitor, not as a request for
10
+ terminal instructions or a text-only snapshot. Keep the whole ordinary-use
11
+ workflow inside the calling Codex app task.
12
+
13
+ ## Open the live view
14
+
15
+ 1. Run `codex-agent-view doctor --json` internally and inspect only its
16
+ structured diagnostics. Capture the result internally; do not quote the
17
+ command, raw output, runtime path, IDs, or private URL in commentary or the
18
+ final response.
19
+ 2. If diagnostics contain `plugin_version_mismatch`, stop the workflow before
20
+ running `codex-agent-view status --json`, starting a monitor, or opening a
21
+ panel. Briefly tell the user inside the current Codex app task that the
22
+ installed plugin and global CLI versions differ and that the exact intended
23
+ `codex-agent-view` version must be globally reinstalled before they select
24
+ **Show Agents** again. Do not perform the reinstall, change Codex settings,
25
+ expose paths, or quote the diagnostic payload.
26
+ 3. Otherwise, check the packaged monitor with
27
+ `codex-agent-view status --json`. Capture the result internally; do not
28
+ quote the command, raw output, runtime path, IDs, or private URL in
29
+ commentary or the final response.
30
+ 4. If the monitor is healthy, reuse it. Recover its authenticated URL from the
31
+ owned private runtime record without restarting it, because restarting would
32
+ discard the current in-memory observation window.
33
+ 5. If the monitor is not healthy, run `codex-agent-view start --no-open` as a
34
+ persistent internal process and capture the authenticated URL it returns.
35
+ Never use `--open` or launch an external browser.
36
+ 6. Accept the URL only when it uses `http`, host `127.0.0.1`, a valid local
37
+ port, and the expected non-empty fragment token. Treat every other target as
38
+ invalid and do not open it.
39
+ 7. Call `codex_app__open_in_codex` for the calling task with a browser target,
40
+ the validated private URL, and `placement: "right"`. Omit `threadId`; never
41
+ navigate to or open the monitor in another task.
42
+ 8. If a previous successful call in the current context supplied the same
43
+ monitor tab's `tabId`, prefer reopening that browser target by `tabId`.
44
+ Otherwise open the validated URL. Do not close or replace user-owned tabs.
45
+
46
+ The in-app Browser capability or site permission may be unavailable or may
47
+ require a user confirmation. Do not claim that the panel opened until
48
+ `codex_app__open_in_codex` reports success. Let Codex show its normal app
49
+ permission request when required; never replace it with terminal instructions.
50
+
51
+ Never place the tokenized localhost URL in Markdown, plain text, code, logs, or
52
+ user instructions. It may appear only as private agent-internal state and as
53
+ the browser target passed to `codex_app__open_in_codex`.
54
+
55
+ ## Failure behavior
56
+
57
+ If the official app cannot open a browser panel, Browser is unavailable, or
58
+ site permission is denied, do not expose the private URL or suggest a terminal
59
+ or external-browser workaround. Briefly report that the live panel could not
60
+ be opened and offer the existing app-native task snapshot from the bundled
61
+ `codex-agent-view` skill.
62
+
63
+ Keep the workflow read-only. Never stop or restart a Codex task or subagent,
64
+ send messages to them, or approve or deny permission requests.
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "Show Agents"
3
+ short_description: "Open the live agent monitor inside Codex"
4
+ policy:
5
+ allow_implicit_invocation: false
@@ -16,6 +16,21 @@ export const DEFAULT_PORT = 43127;
16
16
  export const MAX_EVENT_BODY_BYTES = 64 * 1024;
17
17
  export const RUNTIME_SCHEMA_VERSION = 1;
18
18
 
19
+ export function autoStartPort(env = process.env) {
20
+ const configured = env.CODEX_AGENT_VIEW_AUTO_START_PORT;
21
+ if (configured === undefined || configured === "") {
22
+ return DEFAULT_PORT;
23
+ }
24
+ if (!/^\d+$/.test(configured)) {
25
+ throw new Error("invalid Codex Agent View auto-start port");
26
+ }
27
+ const port = Number(configured);
28
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
29
+ throw new Error("invalid Codex Agent View auto-start port");
30
+ }
31
+ return port;
32
+ }
33
+
19
34
  export function runtimeDirectory(env = process.env) {
20
35
  return resolve(
21
36
  env.CODEX_AGENT_VIEW_RUNTIME_DIR || join(homedir(), ".codex-agent-view"),