@idl3/claude-control 1.0.1 → 1.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.
@@ -55,31 +55,44 @@ export async function readPaneRegistry(dir = PANES_DIR) {
55
55
  }
56
56
 
57
57
  /**
58
- * Remove registry files for panes that no longer exist (best-effort GC, e.g.
59
- * when SessionEnd didn't fire on a crash). `livePaneIds` is the set of tmux %N
60
- * currently present.
58
+ * Remove registry files that have gone stale. A pin is stale iff its transcript
59
+ * file no longer exists the SAME rule readPaneRegistry uses to drop an entry
60
+ * in memory. This is the only safe deletion trigger.
61
61
  *
62
- * @param {Set<string>} livePaneIds
62
+ * It deliberately does NOT use the live tmux pane set. That scan flickers
63
+ * (transient `list-panes` hiccups, a session momentarily not enumerated on a
64
+ * busy socket), and a flaky "pane absent" reading looks identical to a genuine
65
+ * pane close — so keying deletion off it wrongly nukes pins for panes that are
66
+ * very much alive (the long-lived window-1 binding kept vanishing this way).
67
+ *
68
+ * A pin for a closed pane whose transcript still lingers is harmless: there is
69
+ * no live pane to bind it to, and if the pane id is later reused the hook
70
+ * overwrites the file. It self-expires here once its transcript is removed.
71
+ *
72
+ * @param {string} [dir] Override registry dir (tests).
63
73
  * @returns {Promise<void>}
64
74
  */
65
- export async function gcPaneRegistry(livePaneIds) {
75
+ export async function gcPaneRegistry(dir = PANES_DIR) {
66
76
  let entries;
67
77
  try {
68
- entries = await fsp.readdir(PANES_DIR);
78
+ entries = await fsp.readdir(dir);
69
79
  } catch {
70
80
  return;
71
81
  }
82
+
72
83
  await Promise.all(
73
84
  entries
74
85
  .filter((f) => f.endsWith('.json'))
75
86
  .map(async (f) => {
76
87
  try {
77
- const rec = JSON.parse(await fsp.readFile(path.join(PANES_DIR, f), 'utf8'));
78
- if (rec && typeof rec.paneId === 'string' && !livePaneIds.has(rec.paneId)) {
79
- await fsp.rm(path.join(PANES_DIR, f), { force: true });
88
+ const filePath = path.join(dir, f);
89
+ const rec = JSON.parse(await fsp.readFile(filePath, 'utf8'));
90
+ if (!rec || typeof rec.transcriptPath !== 'string') return;
91
+ if (!fs.existsSync(rec.transcriptPath)) {
92
+ await fsp.rm(filePath, { force: true }); // transcript gone → stale
80
93
  }
81
94
  } catch {
82
- // ignore
95
+ // ignore unreadable/partial files
83
96
  }
84
97
  }),
85
98
  );
package/lib/sessions.js CHANGED
@@ -18,11 +18,18 @@ import { parsePanePrompt } from './prompt.js';
18
18
  import { assignTranscripts, parseEtime, fingerprintScore, shouldRebind } from './match.js';
19
19
  import { pinKey } from './pins.js';
20
20
  import { readPaneRegistry, gcPaneRegistry } from './pane-registry.js';
21
+ import {
22
+ matchesProcess as codexMatchesProcess,
23
+ buildTranscriptIndex as buildCodexIndex,
24
+ parseTuiStatus as parseCodexTuiStatus,
25
+ } from './codex.js';
21
26
 
22
27
  const execFile = promisify(_execFile);
23
28
 
24
29
  // Matches Claude Code's executable basename (e.g. /Users/x/.local/bin/claude).
25
30
  const CLAUDE_COMM_RE = /(^|\/)claude$/;
31
+ // Matches Codex CLI executable basename.
32
+ const CODEX_COMM_RE = /(^|\/)codex$/;
26
33
 
27
34
  // A pane is a Claude Code session when its process title is the Claude version
28
35
  // (e.g. "2.1.162") — shells report zsh/bash/etc. A linked transcript also counts.
@@ -337,11 +344,12 @@ export async function listRecentTranscripts({ projectsRoot, limit = 60 }) {
337
344
 
338
345
  export class SessionRegistry extends EventEmitter {
339
346
  /**
340
- * @param {{ projectsRoot: string, tmux: object, debounceMs?: number }} opts
347
+ * @param {{ projectsRoot: string, codexSessionsRoot?: string, tmux: object, debounceMs?: number }} opts
341
348
  */
342
- constructor({ projectsRoot, tmux, debounceMs = 1000 } = {}) {
349
+ constructor({ projectsRoot, codexSessionsRoot, tmux, debounceMs = 1000 } = {}) {
343
350
  super();
344
351
  this._projectsRoot = projectsRoot;
352
+ this._codexSessionsRoot = codexSessionsRoot;
345
353
  this._tmux = tmux;
346
354
  this._debounceMs = debounceMs;
347
355
 
@@ -451,21 +459,28 @@ export class SessionRegistry extends EventEmitter {
451
459
  return true;
452
460
  });
453
461
 
454
- // Classify every pane by its process subtree (a `claude` descendant) and get
455
- // its claude start time in one ps snapshot. Falls back to the cmd heuristic
462
+ // Classify every pane by its process subtree (a `claude` or `codex` descendant)
463
+ // and get its start time in one ps snapshot. Falls back to the cmd heuristic
456
464
  // only when ps is unavailable.
457
465
  const paneProc = await this._buildPaneProc(panes);
458
466
  const isClaudePane = (p) => {
459
467
  const info = paneProc.get(p.target);
460
468
  return info ? info.isClaude : isClaudeCmd(p.cmd);
461
469
  };
470
+ const paneKind = (p) => {
471
+ const info = paneProc.get(p.target);
472
+ if (info?.kind) return info.kind;
473
+ if (isClaudeCmd(p.cmd)) return 'claude';
474
+ if (codexMatchesProcess(p.cmd)) return 'codex';
475
+ return 'terminal';
476
+ };
462
477
  const claudePanes = panes.filter(isClaudePane);
463
478
 
464
479
  // The exact pane→transcript map authored by the SessionStart hook. This is
465
480
  // the deterministic binding; everything below is fallback for panes with no
466
481
  // hook record (sessions started before the hook was installed).
467
482
  const paneReg = await readPaneRegistry();
468
- gcPaneRegistry(new Set(panes.map((p) => p.paneId).filter(Boolean))).catch(() => {});
483
+ gcPaneRegistry().catch(() => {}); // prunes only pins whose transcript is gone
469
484
 
470
485
  // Manual pins win first: a pinned pane is force-bound to its transcript and
471
486
  // that transcript is removed from the auto-matcher pool. Pins are keyed by
@@ -564,9 +579,56 @@ export class SessionRegistry extends EventEmitter {
564
579
  }
565
580
  // ── End self-heal ─────────────────────────────────────────────────────────
566
581
 
582
+ // ── Codex pane → transcript matching ────────────────────────────────────
583
+ // Discover Codex session transcripts and match them to Codex panes.
584
+ // The Claude assignment above is computed first and left untouched;
585
+ // codex results are merged in after.
586
+ const codexPanes = panes.filter((p) => paneKind(p) === 'codex');
587
+ if (codexPanes.length > 0) {
588
+ const codexIndex = await buildCodexIndex({ codexSessionsRoot: this._codexSessionsRoot });
589
+ const codexCandidates = [];
590
+ for (const rec of codexIndex.byCwd.values()) {
591
+ codexCandidates.push({
592
+ transcriptPath: rec.transcriptPath,
593
+ cwd: rec.cwd,
594
+ projectDir: null, // triggers isCwdConsistent scope fallback in match.js
595
+ birthtimeMs: rec.mtime,
596
+ mtimeMs: rec.mtime,
597
+ lastActivityMs: rec.lastActivityMs ?? rec.mtime,
598
+ customTitle: rec.customTitle,
599
+ aiTitle: rec.aiTitle,
600
+ recentText: null,
601
+ // Pass through for later session assembly
602
+ sessionId: rec.sessionId,
603
+ lastActivity: rec.lastActivity,
604
+ model: rec.model,
605
+ transcriptPending: rec.transcriptPending,
606
+ pendingToolUseId: rec.pendingToolUseId,
607
+ pendingQuestion: rec.pendingQuestion,
608
+ agentType: rec.agentType,
609
+ usagePct: rec.usagePct ?? null,
610
+ usageWindowMin: rec.usageWindowMin ?? null,
611
+ mtime: rec.mtime,
612
+ });
613
+ }
614
+ const codexPaneInputs = codexPanes.map((p) => ({
615
+ target: p.target,
616
+ windowName: p.windowName,
617
+ cwd: p.cwd,
618
+ projectDir: null,
619
+ procStartMs: paneProc.get(p.target)?.startMs ?? null,
620
+ capturedText: this._paneTextCache.get(p.target) ?? null,
621
+ }));
622
+ const codexAssignment = assignTranscripts(codexPaneInputs, codexCandidates);
623
+ for (const [t, rec] of codexAssignment) assignment.set(t, rec);
624
+ }
625
+ // ── End Codex matching ───────────────────────────────────────────────────
626
+
567
627
  const sessions = panes.map((win) => {
568
628
  const isClaude = isClaudePane(win);
569
- const transcript = isClaude ? assignment.get(win.target) ?? null : null;
629
+ const kind = paneKind(win);
630
+ const hasTranscript = kind === 'claude' || kind === 'codex';
631
+ const transcript = hasTranscript ? assignment.get(win.target) ?? null : null;
570
632
  const isPinned = pinnedByTarget.has(win.target);
571
633
  const id = win.target;
572
634
  // Pending = subscribed-tailer pending (live modal) OR transcript-derived
@@ -578,7 +640,11 @@ export class SessionRegistry extends EventEmitter {
578
640
  !!transcript?.transcriptPending ||
579
641
  !!panePrompt?.pending;
580
642
  const title = transcript?.customTitle || transcript?.aiTitle || null;
581
- const ctx = isClaude ? this._ctxMap.get(win.target) || {} : {};
643
+ // Read the polled TUI status (model/ctx) for Claude AND Codex. Codex's
644
+ // _pollCtx populates _ctxMap with its model (ctxPct stays null — Codex's
645
+ // TUI has no context %). Without this, the assembly would discard the
646
+ // polled codex model and the rail would show no model for codex rows.
647
+ const ctx = isClaude || kind === 'codex' ? this._ctxMap.get(win.target) || {} : {};
582
648
 
583
649
  return {
584
650
  id,
@@ -603,12 +669,14 @@ export class SessionRegistry extends EventEmitter {
603
669
  pendingQuestion: transcript?.pendingQuestion ?? panePrompt?.question ?? null,
604
670
  cmd: win.cmd,
605
671
  isClaude,
606
- kind: isClaude ? 'claude' : 'terminal',
672
+ kind,
607
673
  ccShell: !!win.ccShell, // a composer >_ sister shell pane
608
674
 
609
675
  model: ctx.model || prettyModel(transcript?.model) || null,
610
676
  ctxPct: ctx.ctxPct ?? null,
611
- thinking: isClaude ? this._thinkingMap.get(win.target) ?? false : false,
677
+ thinking: (isClaude || kind === 'codex') ? this._thinkingMap.get(win.target) ?? false : false,
678
+ usagePct: transcript?.usagePct ?? null,
679
+ usageWindowMin: transcript?.usageWindowMin ?? null,
612
680
  };
613
681
  });
614
682
 
@@ -634,7 +702,11 @@ export class SessionRegistry extends EventEmitter {
634
702
  if (!this._tmux.isValidTarget(s.target)) return;
635
703
  try {
636
704
  const cap = await this._tmux.capturePane(s.target, 8);
637
- const { ctxPct, model } = parseTuiStatus(cap);
705
+ // Codex panes use the codex header/footer parser (the Claude tui.js
706
+ // parser doesn't match codex's "model:"/footer formats). Codex has no
707
+ // ctx% in its TUI, so ctxPct stays null for codex (no faked value).
708
+ const { ctxPct, model } =
709
+ s.kind === 'codex' ? parseCodexTuiStatus(cap) : parseTuiStatus(cap);
638
710
  this._ctxMap.set(s.target, { ctxPct, model });
639
711
  // Merge into the live session object without a full rebuild.
640
712
  if (ctxPct !== null) s.ctxPct = ctxPct;
@@ -854,7 +926,7 @@ export class SessionRegistry extends EventEmitter {
854
926
  }
855
927
 
856
928
  const now = Date.now();
857
- // BFS from the pane shell pid for a `claude` descendant; return its start.
929
+ // BFS from the pane shell pid for a `claude` or `codex` descendant; return its start.
858
930
  const findClaude = (rootPid) => {
859
931
  const queue = [rootPid];
860
932
  const seen = new Set();
@@ -865,15 +937,19 @@ export class SessionRegistry extends EventEmitter {
865
937
  const meta = info.get(pid);
866
938
  if (meta && CLAUDE_COMM_RE.test(meta.comm)) {
867
939
  const sec = parseEtime(meta.etime);
868
- return { isClaude: true, startMs: sec == null ? null : now - sec * 1000 };
940
+ return { isClaude: true, isCodex: false, kind: 'claude', startMs: sec == null ? null : now - sec * 1000 };
941
+ }
942
+ if (meta && CODEX_COMM_RE.test(meta.comm)) {
943
+ const sec = parseEtime(meta.etime);
944
+ return { isClaude: false, isCodex: true, kind: 'codex', startMs: sec == null ? null : now - sec * 1000 };
869
945
  }
870
946
  for (const c of children.get(pid) ?? []) queue.push(c);
871
947
  }
872
- return { isClaude: false, startMs: null };
948
+ return { isClaude: false, isCodex: false, kind: null, startMs: null };
873
949
  };
874
950
 
875
951
  for (const p of allPanes) {
876
- out.set(p.target, p.panePid ? findClaude(p.panePid) : { isClaude: false, startMs: null });
952
+ out.set(p.target, p.panePid ? findClaude(p.panePid) : { isClaude: false, isCodex: false, kind: null, startMs: null });
877
953
  }
878
954
  return out;
879
955
  }
package/lib/transcript.js CHANGED
@@ -194,13 +194,14 @@ export function parseRecord(line) {
194
194
  export class TranscriptTailer extends EventEmitter {
195
195
  /**
196
196
  * @param {string} filePath
197
- * @param {{ maxBuffer?: number, debounceMs?: number }} options
197
+ * @param {{ maxBuffer?: number, debounceMs?: number, parser?: Function }} options
198
198
  */
199
- constructor(filePath, { maxBuffer = DEFAULT_MAX_BUFFER, debounceMs = 150 } = {}) {
199
+ constructor(filePath, { maxBuffer = DEFAULT_MAX_BUFFER, debounceMs = 150, parser = parseRecord } = {}) {
200
200
  super();
201
201
  this._filePath = filePath;
202
202
  this._maxBuffer = maxBuffer;
203
203
  this._debounceMs = debounceMs;
204
+ this._parse = parser;
204
205
 
205
206
  /** @type {import('./transcript.js').NormalizedMessage[]} */
206
207
  this._messages = [];
@@ -340,7 +341,7 @@ export class TranscriptTailer extends EventEmitter {
340
341
 
341
342
  const parsed = [];
342
343
  for (const line of lines) {
343
- const msg = parseRecord(line);
344
+ const msg = this._parse(line);
344
345
  if (msg) {
345
346
  parsed.push(msg);
346
347
  this._trackPending(msg);
@@ -413,7 +414,7 @@ export class TranscriptTailer extends EventEmitter {
413
414
 
414
415
  const newMsgs = [];
415
416
  for (const line of complete) {
416
- const msg = parseRecord(line);
417
+ const msg = this._parse(line);
417
418
  if (msg) {
418
419
  newMsgs.push(msg);
419
420
  this._trackPending(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idl3/claude-control",
3
- "version": "1.0.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Local web UI to watch and drive your Claude Code sessions running in tmux — live transcripts, reply, answer AskUserQuestion, attach files, from a browser or phone.",
6
6
  "keywords": [
package/server.js CHANGED
@@ -10,7 +10,11 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import os from 'node:os';
13
- import { spawn } from 'node:child_process';
13
+ import { spawn, execFile as _execFileRaw } from 'node:child_process';
14
+ import { promisify } from 'node:util';
15
+ import fsp from 'node:fs/promises';
16
+
17
+ const _execFile = promisify(_execFileRaw);
14
18
  import { WebSocketServer } from 'ws';
15
19
 
16
20
  import * as tmux from './lib/tmux.js';
@@ -27,6 +31,7 @@ import { sweepUploads, resolveUploadPath } from './lib/uploads.js';
27
31
  import { getVersionInfo, currentVersion } from './lib/version.js';
28
32
  import * as push from './lib/push.js';
29
33
  import { readConfig, writeConfig } from './lib/config.js';
34
+ import { parseCodexRecord, parseCodexPrompt, buildSpawnCommand } from './lib/codex.js';
30
35
  import { optimizePrompt, rulesOptimize } from './lib/optimize.js';
31
36
  import * as mlx from './lib/mlx.js';
32
37
  import {
@@ -76,6 +81,8 @@ const CONFIG = {
76
81
  host: env('HOST') || '127.0.0.1',
77
82
  projectsRoot:
78
83
  env('PROJECTS') || path.join(os.homedir(), '.claude', 'projects'),
84
+ codexSessionsRoot:
85
+ env('CODEX_SESSIONS') || path.join(os.homedir(), '.codex', 'sessions'),
79
86
  // 768MB: a long-running Node server (WS + transcript tailing + the bundled
80
87
  // web app) baselines ~300-450MB of V8 heap + RSS, so the old 350MB budget
81
88
  // tripped "over limit" permanently. Override with CLAUDE_CONTROL_RSS_LIMIT_MB.
@@ -123,7 +130,7 @@ const IMAGE_MIME = {
123
130
  };
124
131
 
125
132
  // --- shared state -----------------------------------------------------------
126
- const registry = new SessionRegistry({ projectsRoot: CONFIG.projectsRoot, tmux });
133
+ const registry = new SessionRegistry({ projectsRoot: CONFIG.projectsRoot, codexSessionsRoot: CONFIG.codexSessionsRoot, tmux });
127
134
  const resources = new ResourceMonitor({ rssLimitMB: CONFIG.rssLimitMB });
128
135
 
129
136
  // Manual transcript pins (windowId.paneIndex -> transcript path). Loaded at boot,
@@ -347,6 +354,33 @@ const _handler = (req, res) => {
347
354
  if (!checkToken(req)) return endJson(res, 401, { error: 'unauthorized' });
348
355
  return handleTranscribe(req, res, u);
349
356
  }
357
+ // GET /api/spawn-agents — agent-type availability (claude vs codex).
358
+ // Returns which agent binaries are resolvable on this machine so the UI can
359
+ // disable an unavailable agent picker option and show a reason.
360
+ // Token-gated + localhost, same as other GET endpoints.
361
+ if (u.pathname === '/api/spawn-agents') {
362
+ if (!checkToken(req)) return endJson(res, 401, { error: 'unauthorized' });
363
+ const cfg = readConfig();
364
+ return Promise.all([
365
+ resolveBin(cfg.claudeBin || cfg.launchCommand),
366
+ resolveBin(cfg.codexBin || cfg.codexLaunchCommand),
367
+ ]).then(([claudeResult, codexResult]) => {
368
+ return endJson(res, 200, {
369
+ agents: [
370
+ {
371
+ id: 'claude',
372
+ available: claudeResult.available,
373
+ ...(claudeResult.available ? {} : { reason: claudeResult.reason }),
374
+ },
375
+ {
376
+ id: 'codex',
377
+ available: codexResult.available,
378
+ ...(codexResult.available ? {} : { reason: codexResult.reason }),
379
+ },
380
+ ],
381
+ });
382
+ }).catch((err) => endJson(res, 500, { error: String(err?.message || err) }));
383
+ }
350
384
  if (u.pathname === '/api/session/new') {
351
385
  if (req.method !== 'POST') return endJson(res, 405, { error: 'method not allowed' });
352
386
  if (!checkToken(req)) return endJson(res, 401, { error: 'unauthorized' });
@@ -672,6 +706,40 @@ function handleTranscribe(req, res, u) {
672
706
  });
673
707
  }
674
708
 
709
+ // ---------------------------------------------------------------------------
710
+ // resolveBin — async PATH lookup for a binary name or absolute path.
711
+ //
712
+ // If `bin` is an absolute path, checks it is executable directly.
713
+ // Otherwise runs `which <bin>` on PATH.
714
+ //
715
+ // Returns { available: true, path } on success, { available: false, reason }
716
+ // on failure. Never throws.
717
+ // ---------------------------------------------------------------------------
718
+ async function resolveBin(bin) {
719
+ if (!bin || typeof bin !== 'string' || !bin.trim()) {
720
+ return { available: false, reason: 'no binary configured' };
721
+ }
722
+ const b = bin.trim();
723
+ // Absolute path: check existence + execute permission directly.
724
+ if (b.startsWith('/')) {
725
+ try {
726
+ await fsp.access(b, fsp.constants?.X_OK ?? 1);
727
+ return { available: true, path: b };
728
+ } catch {
729
+ return { available: false, reason: `binary not found or not executable: ${b}` };
730
+ }
731
+ }
732
+ // Relative / bare name: resolve via `which`.
733
+ try {
734
+ const { stdout } = await _execFile('which', [b], { timeout: 5000 });
735
+ const resolved = stdout.trim();
736
+ if (resolved) return { available: true, path: resolved };
737
+ return { available: false, reason: `${b} not found on PATH` };
738
+ } catch {
739
+ return { available: false, reason: `${b} not found on PATH` };
740
+ }
741
+ }
742
+
675
743
  // POST /api/session/new — create a new tmux window in the configured (or
676
744
  // body-overridden) cwd, then type the launch command into it via send-keys so
677
745
  // the interactive shell resolves aliases. Security: the command is operator
@@ -687,23 +755,69 @@ async function handleSessionNew(req, res) {
687
755
  const config = readConfig();
688
756
  const cwd =
689
757
  typeof body.cwd === 'string' && body.cwd.trim() ? body.cwd : config.defaultCwd;
758
+
759
+ // agent ∈ {'claude','codex'}, default 'claude'.
760
+ const agent = body.agent === 'codex' ? 'codex' : 'claude';
761
+
690
762
  // Name is required-with-default: sanitize the requested name, falling back to
691
763
  // `session-<short-ts>` so a session is ALWAYS named (the rail reads the tmux
692
764
  // window name until a transcript title exists).
693
765
  const name = tmux.sanitizeName(body.name) || tmux.defaultSessionName();
766
+
767
+ // --- Pre-validation: binary resolution + cwd check BEFORE creating any window ---
768
+
769
+ // (i) Resolve the agent binary and return 400 if unavailable.
770
+ const agentBin = agent === 'codex'
771
+ ? (config.codexBin || config.codexLaunchCommand)
772
+ : (config.claudeBin || config.launchCommand);
773
+ const binCheck = await resolveBin(agentBin);
774
+ if (!binCheck.available) {
775
+ return endJson(res, 400, { error: `agent binary unavailable: ${binCheck.reason}` });
776
+ }
777
+
778
+ // (ii) For codex: pre-validate cwd exists and is a directory BEFORE createWindow,
779
+ // so a bad request creates NO window (400 not 500, window-leak prevention).
780
+ if (agent === 'codex') {
781
+ try {
782
+ const st = await fsp.stat(cwd);
783
+ if (!st.isDirectory()) {
784
+ return endJson(res, 400, { error: `cwd is not a directory: ${cwd}` });
785
+ }
786
+ } catch {
787
+ return endJson(res, 400, { error: `cwd does not exist: ${cwd}` });
788
+ }
789
+ }
790
+
694
791
  try {
695
792
  // (1) Reliable named path: the tmux window name. createWindow sets it via
696
- // `new-window -n`, so the rail shows the name immediately.
793
+ // `new-window -n` and the `-c cwd` flag cwd flows through tmux's own
794
+ // working-directory flag, never a shell `cd`.
697
795
  const target = await tmux.createWindow({ cwd, name });
698
- // (2) Claude's own session title: `claude --help` exposes `-n/--name`
699
- // (display name in the prompt box, /resume picker, terminal title), so
700
- // we append it to the launch command rather than relying on a delayed
701
- // `/rename`. The name is shell-quoted (sanitizeName already stripped
702
- // control chars/newlines) since the command is typed into an interactive
703
- // shell so aliases like `yolo` resolve. sendText appends Enter runs it.
704
- const launch = `${config.launchCommand} --name ${tmux.shellQuoteName(name)}`;
796
+
797
+ let launch;
798
+ if (agent === 'codex') {
799
+ // Codex path: uses -C <cwd> (its own cwd flag). No --name flag — Codex
800
+ // has none. The tmux window is still named (above) so the rail shows it.
801
+ // buildSpawnCommand is the single source of truth for Codex's launch
802
+ // shape; the cwd arg is shell-quoted since the command is typed into an
803
+ // interactive shell via sendText. The executed command is
804
+ // config.codexLaunchCommand (may be a shell alias), validated above via
805
+ // codexBin||codexLaunchCommand — same pattern as the Claude branch.
806
+ const { bin, args } = buildSpawnCommand({ cwd, bin: config.codexLaunchCommand });
807
+ launch = `${bin} ${args.map((a) => (a === cwd ? tmux.shellQuoteName(cwd) : a)).join(' ')}`;
808
+ } else {
809
+ // Claude path: BYTE-IDENTICAL to the pre-Phase-D implementation.
810
+ // (2) Claude's own session title: `claude --help` exposes `-n/--name`
811
+ // (display name in the prompt box, /resume picker, terminal title), so
812
+ // we append it to the launch command rather than relying on a delayed
813
+ // `/rename`. The name is shell-quoted (sanitizeName already stripped
814
+ // control chars/newlines) since the command is typed into an interactive
815
+ // shell so aliases like `yolo` resolve. sendText appends Enter → runs it.
816
+ launch = `${config.launchCommand} --name ${tmux.shellQuoteName(name)}`;
817
+ }
818
+
705
819
  await tmux.sendText(target, launch);
706
- return endJson(res, 200, { ok: true, target, name });
820
+ return endJson(res, 200, { ok: true, target, name, agent });
707
821
  } catch (err) {
708
822
  return endJson(res, 500, { error: String(err?.message || err) });
709
823
  }
@@ -1135,7 +1249,7 @@ function ensureSubscription(id) {
1135
1249
  return sub;
1136
1250
  }
1137
1251
 
1138
- const tailer = new TranscriptTailer(session.transcriptPath, { maxBuffer: CONFIG.maxBuffer });
1252
+ const tailer = new TranscriptTailer(session.transcriptPath, { maxBuffer: CONFIG.maxBuffer, parser: session.kind === 'codex' ? parseCodexRecord : undefined });
1139
1253
  // Watch this session's sub-agent transcripts (Task/Agent). Discovery is polled
1140
1254
  // when the parent transcript grows (when sub-agents spawn) + once at subscribe.
1141
1255
  const subagents = new SubAgentsWatcher(session.transcriptPath);
@@ -1201,7 +1315,7 @@ function startPromptPoller(id, sub) {
1201
1315
  let prompt = null;
1202
1316
  try {
1203
1317
  const cap = await tmux.capturePane(session.target, 40);
1204
- prompt = parsePanePrompt(cap);
1318
+ prompt = session.kind === 'codex' ? parseCodexPrompt(cap) : parsePanePrompt(cap);
1205
1319
  } catch {
1206
1320
  return;
1207
1321
  }
@@ -1,3 +1,3 @@
1
- import{g as Ve}from"./index-DFru8Gzx.js";function xe(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(t=>{const i=e[t],u=typeof i;(u==="object"||u==="function")&&!Object.isFrozen(i)&&xe(i)}),e}class he{constructor(t){t.data===void 0&&(t.data={}),this.data=t.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function we(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")}function B(e,...t){const i=Object.create(null);for(const u in e)i[u]=e[u];return t.forEach(function(u){for(const b in u)i[b]=u[b]}),i}const qe="</span>",pe=e=>!!e.scope,Qe=(e,{prefix:t})=>{if(e.startsWith("language:"))return e.replace("language:","language-");if(e.includes(".")){const i=e.split(".");return[`${t}${i.shift()}`,...i.map((u,b)=>`${u}${"_".repeat(b+1)}`)].join(" ")}return`${t}${e}`};class me{constructor(t,i){this.buffer="",this.classPrefix=i.classPrefix,t.walk(this)}addText(t){this.buffer+=we(t)}openNode(t){if(!pe(t))return;const i=Qe(t.scope,{prefix:this.classPrefix});this.span(i)}closeNode(t){pe(t)&&(this.buffer+=qe)}value(){return this.buffer}span(t){this.buffer+=`<span class="${t}">`}}const de=(e={})=>{const t={children:[]};return Object.assign(t,e),t};class te{constructor(){this.rootNode=de(),this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(t){this.top.children.push(t)}openNode(t){const i=de({scope:t});this.add(i),this.stack.push(i)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(t){return this.constructor._walk(t,this.rootNode)}static _walk(t,i){return typeof i=="string"?t.addText(i):i.children&&(t.openNode(i),i.children.forEach(u=>this._walk(t,u)),t.closeNode(i)),t}static _collapse(t){typeof t!="string"&&t.children&&(t.children.every(i=>typeof i=="string")?t.children=[t.children.join("")]:t.children.forEach(i=>{te._collapse(i)}))}}class et extends te{constructor(t){super(),this.options=t}addText(t){t!==""&&this.add(t)}startScope(t){this.openNode(t)}endScope(){this.closeNode()}__addSublanguage(t,i){const u=t.root;i&&(u.scope=`language:${i}`),this.add(u)}toHTML(){return new me(this,this.options).value()}finalize(){return this.closeAllNodes(),!0}}function P(e){return e?typeof e=="string"?e:e.source:null}function Oe(e){return C("(?=",e,")")}function tt(e){return C("(?:",e,")*")}function nt(e){return C("(?:",e,")?")}function C(...e){return e.map(i=>P(i)).join("")}function it(e){const t=e[e.length-1];return typeof t=="object"&&t.constructor===Object?(e.splice(e.length-1,1),t):{}}function ne(...e){return"("+(it(e).capture?"":"?:")+e.map(u=>P(u)).join("|")+")"}function Re(e){return new RegExp(e.toString()+"|").exec("").length-1}function st(e,t){const i=e&&e.exec(t);return i&&i.index===0}const rt=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;function ie(e,{joinWith:t}){let i=0;return e.map(u=>{i+=1;const b=i;let _=P(u),c="";for(;_.length>0;){const r=rt.exec(_);if(!r){c+=_;break}c+=_.substring(0,r.index),_=_.substring(r.index+r[0].length),r[0][0]==="\\"&&r[1]?c+="\\"+String(Number(r[1])+b):(c+=r[0],r[0]==="("&&i++)}return c}).map(u=>`(${u})`).join(t)}const ct=/\b\B/,ye="[a-zA-Z]\\w*",se="[a-zA-Z_]\\w*",Se="\\b\\d+(\\.\\d+)?",Ne="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",Ae="\\b(0b[01]+)",ot="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",at=(e={})=>{const t=/^#![ ]*\//;return e.binary&&(e.begin=C(t,/.*\b/,e.binary,/\b.*/)),B({scope:"meta",begin:t,end:/$/,relevance:0,"on:begin":(i,u)=>{i.index!==0&&u.ignoreMatch()}},e)},U={begin:"\\\\[\\s\\S]",relevance:0},lt={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[U]},ut={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[U]},ft={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},Y=function(e,t,i={}){const u=B({scope:"comment",begin:e,end:t,contains:[]},i);u.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});const b=ne("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return u.contains.push({begin:C(/[ ]+/,"(",b,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),u},gt=Y("//","$"),ht=Y("/\\*","\\*/"),pt=Y("#","$"),dt={scope:"number",begin:Se,relevance:0},Et={scope:"number",begin:Ne,relevance:0},bt={scope:"number",begin:Ae,relevance:0},_t={scope:"regexp",begin:/\/(?=[^/\n]*\/)/,end:/\/[gimuy]*/,contains:[U,{begin:/\[/,end:/\]/,relevance:0,contains:[U]}]},Mt={scope:"title",begin:ye,relevance:0},xt={scope:"title",begin:se,relevance:0},wt={begin:"\\.\\s*"+se,relevance:0},Ot=function(e){return Object.assign(e,{"on:begin":(t,i)=>{i.data._beginMatch=t[1]},"on:end":(t,i)=>{i.data._beginMatch!==t[1]&&i.ignoreMatch()}})};var z=Object.freeze({__proto__:null,APOS_STRING_MODE:lt,BACKSLASH_ESCAPE:U,BINARY_NUMBER_MODE:bt,BINARY_NUMBER_RE:Ae,COMMENT:Y,C_BLOCK_COMMENT_MODE:ht,C_LINE_COMMENT_MODE:gt,C_NUMBER_MODE:Et,C_NUMBER_RE:Ne,END_SAME_AS_BEGIN:Ot,HASH_COMMENT_MODE:pt,IDENT_RE:ye,MATCH_NOTHING_RE:ct,METHOD_GUARD:wt,NUMBER_MODE:dt,NUMBER_RE:Se,PHRASAL_WORDS_MODE:ft,QUOTE_STRING_MODE:ut,REGEXP_MODE:_t,RE_STARTERS_RE:ot,SHEBANG:at,TITLE_MODE:Mt,UNDERSCORE_IDENT_RE:se,UNDERSCORE_TITLE_MODE:xt});function Rt(e,t){e.input[e.index-1]==="."&&t.ignoreMatch()}function yt(e,t){e.className!==void 0&&(e.scope=e.className,delete e.className)}function St(e,t){t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=Rt,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,e.relevance===void 0&&(e.relevance=0))}function Nt(e,t){Array.isArray(e.illegal)&&(e.illegal=ne(...e.illegal))}function At(e,t){if(e.match){if(e.begin||e.end)throw new Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function kt(e,t){e.relevance===void 0&&(e.relevance=1)}const Tt=(e,t)=>{if(!e.beforeMatch)return;if(e.starts)throw new Error("beforeMatch cannot be used with starts");const i=Object.assign({},e);Object.keys(e).forEach(u=>{delete e[u]}),e.keywords=i.keywords,e.begin=C(i.beforeMatch,Oe(i.begin)),e.starts={relevance:0,contains:[Object.assign(i,{endsParent:!0})]},e.relevance=0,delete i.beforeMatch},It=["of","and","for","in","not","or","if","then","parent","list","value"],Bt="keyword";function ke(e,t,i=Bt){const u=Object.create(null);return typeof e=="string"?b(i,e.split(" ")):Array.isArray(e)?b(i,e):Object.keys(e).forEach(function(_){Object.assign(u,ke(e[_],t,_))}),u;function b(_,c){t&&(c=c.map(r=>r.toLowerCase())),c.forEach(function(r){const l=r.split("|");u[l[0]]=[_,Dt(l[0],l[1])]})}}function Dt(e,t){return t?Number(t):vt(e)?0:1}function vt(e){return It.includes(e.toLowerCase())}const Ee={},v=e=>{console.error(e)},be=(e,...t)=>{console.log(`WARN: ${e}`,...t)},L=(e,t)=>{Ee[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),Ee[`${e}/${t}`]=!0)},X=new Error;function Te(e,t,{key:i}){let u=0;const b=e[i],_={},c={};for(let r=1;r<=t.length;r++)c[r+u]=b[r],_[r+u]=!0,u+=Re(t[r-1]);e[i]=c,e[i]._emit=_,e[i]._multi=!0}function Ct(e){if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw v("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),X;if(typeof e.beginScope!="object"||e.beginScope===null)throw v("beginScope must be object"),X;Te(e,e.begin,{key:"beginScope"}),e.begin=ie(e.begin,{joinWith:""})}}function Lt(e){if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw v("skip, excludeEnd, returnEnd not compatible with endScope: {}"),X;if(typeof e.endScope!="object"||e.endScope===null)throw v("endScope must be object"),X;Te(e,e.end,{key:"endScope"}),e.end=ie(e.end,{joinWith:""})}}function Ht(e){e.scope&&typeof e.scope=="object"&&e.scope!==null&&(e.beginScope=e.scope,delete e.scope)}function jt(e){Ht(e),typeof e.beginScope=="string"&&(e.beginScope={_wrap:e.beginScope}),typeof e.endScope=="string"&&(e.endScope={_wrap:e.endScope}),Ct(e),Lt(e)}function Pt(e){function t(c,r){return new RegExp(P(c),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(r?"g":""))}class i{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(r,l){l.position=this.position++,this.matchIndexes[this.matchAt]=l,this.regexes.push([l,r]),this.matchAt+=Re(r)+1}compile(){this.regexes.length===0&&(this.exec=()=>null);const r=this.regexes.map(l=>l[1]);this.matcherRe=t(ie(r,{joinWith:"|"}),!0),this.lastIndex=0}exec(r){this.matcherRe.lastIndex=this.lastIndex;const l=this.matcherRe.exec(r);if(!l)return null;const w=l.findIndex((j,Z)=>Z>0&&j!==void 0),M=this.matchIndexes[w];return l.splice(0,w),Object.assign(l,M)}}class u{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(r){if(this.multiRegexes[r])return this.multiRegexes[r];const l=new i;return this.rules.slice(r).forEach(([w,M])=>l.addRule(w,M)),l.compile(),this.multiRegexes[r]=l,l}resumingScanAtSamePosition(){return this.regexIndex!==0}considerAll(){this.regexIndex=0}addRule(r,l){this.rules.push([r,l]),l.type==="begin"&&this.count++}exec(r){const l=this.getMatcher(this.regexIndex);l.lastIndex=this.lastIndex;let w=l.exec(r);if(this.resumingScanAtSamePosition()&&!(w&&w.index===this.lastIndex)){const M=this.getMatcher(0);M.lastIndex=this.lastIndex+1,w=M.exec(r)}return w&&(this.regexIndex+=w.position+1,this.regexIndex===this.count&&this.considerAll()),w}}function b(c){const r=new u;return c.contains.forEach(l=>r.addRule(l.begin,{rule:l,type:"begin"})),c.terminatorEnd&&r.addRule(c.terminatorEnd,{type:"end"}),c.illegal&&r.addRule(c.illegal,{type:"illegal"}),r}function _(c,r){const l=c;if(c.isCompiled)return l;[yt,At,jt,Tt].forEach(M=>M(c,r)),e.compilerExtensions.forEach(M=>M(c,r)),c.__beforeBegin=null,[St,Nt,kt].forEach(M=>M(c,r)),c.isCompiled=!0;let w=null;return typeof c.keywords=="object"&&c.keywords.$pattern&&(c.keywords=Object.assign({},c.keywords),w=c.keywords.$pattern,delete c.keywords.$pattern),w=w||/\w+/,c.keywords&&(c.keywords=ke(c.keywords,e.case_insensitive)),l.keywordPatternRe=t(w,!0),r&&(c.begin||(c.begin=/\B|\b/),l.beginRe=t(l.begin),!c.end&&!c.endsWithParent&&(c.end=/\B|\b/),c.end&&(l.endRe=t(l.end)),l.terminatorEnd=P(l.end)||"",c.endsWithParent&&r.terminatorEnd&&(l.terminatorEnd+=(c.end?"|":"")+r.terminatorEnd)),c.illegal&&(l.illegalRe=t(c.illegal)),c.contains||(c.contains=[]),c.contains=[].concat(...c.contains.map(function(M){return Ut(M==="self"?c:M)})),c.contains.forEach(function(M){_(M,l)}),c.starts&&_(c.starts,r),l.matcher=b(l),l}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=B(e.classNameAliases||{}),_(e)}function Ie(e){return e?e.endsWithParent||Ie(e.starts):!1}function Ut(e){return e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map(function(t){return B(e,{variants:null},t)})),e.cachedVariants?e.cachedVariants:Ie(e)?B(e,{starts:e.starts?B(e.starts):null}):Object.isFrozen(e)?B(e):e}var $t="11.11.1";class Gt extends Error{constructor(t,i){super(t),this.name="HTMLInjectionError",this.html=i}}const ee=we,_e=B,Me=Symbol("nomatch"),Wt=7,Be=function(e){const t=Object.create(null),i=Object.create(null),u=[];let b=!0;const _="Could not find the language '{}', did you forget to load/include a language module?",c={disableAutodetect:!0,name:"Plain text",contains:[]};let r={ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:et};function l(n){return r.noHighlightRe.test(n)}function w(n){let a=n.className+" ";a+=n.parentNode?n.parentNode.className:"";const h=r.languageDetectRe.exec(a);if(h){const d=T(h[1]);return d||(be(_.replace("{}",h[1])),be("Falling back to no-highlight mode for this block.",n)),d?h[1]:"no-highlight"}return a.split(/\s+/).find(d=>l(d)||T(d))}function M(n,a,h){let d="",x="";typeof a=="object"?(d=n,h=a.ignoreIllegals,x=a.language):(L("10.7.0","highlight(lang, code, ...args) has been deprecated."),L("10.7.0",`Please use highlight(code, options) instead.
1
+ import{g as Ve}from"./index-DzIDTXLS.js";function xe(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(t=>{const i=e[t],u=typeof i;(u==="object"||u==="function")&&!Object.isFrozen(i)&&xe(i)}),e}class he{constructor(t){t.data===void 0&&(t.data={}),this.data=t.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function we(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")}function B(e,...t){const i=Object.create(null);for(const u in e)i[u]=e[u];return t.forEach(function(u){for(const b in u)i[b]=u[b]}),i}const qe="</span>",pe=e=>!!e.scope,Qe=(e,{prefix:t})=>{if(e.startsWith("language:"))return e.replace("language:","language-");if(e.includes(".")){const i=e.split(".");return[`${t}${i.shift()}`,...i.map((u,b)=>`${u}${"_".repeat(b+1)}`)].join(" ")}return`${t}${e}`};class me{constructor(t,i){this.buffer="",this.classPrefix=i.classPrefix,t.walk(this)}addText(t){this.buffer+=we(t)}openNode(t){if(!pe(t))return;const i=Qe(t.scope,{prefix:this.classPrefix});this.span(i)}closeNode(t){pe(t)&&(this.buffer+=qe)}value(){return this.buffer}span(t){this.buffer+=`<span class="${t}">`}}const de=(e={})=>{const t={children:[]};return Object.assign(t,e),t};class te{constructor(){this.rootNode=de(),this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(t){this.top.children.push(t)}openNode(t){const i=de({scope:t});this.add(i),this.stack.push(i)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(t){return this.constructor._walk(t,this.rootNode)}static _walk(t,i){return typeof i=="string"?t.addText(i):i.children&&(t.openNode(i),i.children.forEach(u=>this._walk(t,u)),t.closeNode(i)),t}static _collapse(t){typeof t!="string"&&t.children&&(t.children.every(i=>typeof i=="string")?t.children=[t.children.join("")]:t.children.forEach(i=>{te._collapse(i)}))}}class et extends te{constructor(t){super(),this.options=t}addText(t){t!==""&&this.add(t)}startScope(t){this.openNode(t)}endScope(){this.closeNode()}__addSublanguage(t,i){const u=t.root;i&&(u.scope=`language:${i}`),this.add(u)}toHTML(){return new me(this,this.options).value()}finalize(){return this.closeAllNodes(),!0}}function P(e){return e?typeof e=="string"?e:e.source:null}function Oe(e){return C("(?=",e,")")}function tt(e){return C("(?:",e,")*")}function nt(e){return C("(?:",e,")?")}function C(...e){return e.map(i=>P(i)).join("")}function it(e){const t=e[e.length-1];return typeof t=="object"&&t.constructor===Object?(e.splice(e.length-1,1),t):{}}function ne(...e){return"("+(it(e).capture?"":"?:")+e.map(u=>P(u)).join("|")+")"}function Re(e){return new RegExp(e.toString()+"|").exec("").length-1}function st(e,t){const i=e&&e.exec(t);return i&&i.index===0}const rt=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;function ie(e,{joinWith:t}){let i=0;return e.map(u=>{i+=1;const b=i;let _=P(u),c="";for(;_.length>0;){const r=rt.exec(_);if(!r){c+=_;break}c+=_.substring(0,r.index),_=_.substring(r.index+r[0].length),r[0][0]==="\\"&&r[1]?c+="\\"+String(Number(r[1])+b):(c+=r[0],r[0]==="("&&i++)}return c}).map(u=>`(${u})`).join(t)}const ct=/\b\B/,ye="[a-zA-Z]\\w*",se="[a-zA-Z_]\\w*",Se="\\b\\d+(\\.\\d+)?",Ne="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",Ae="\\b(0b[01]+)",ot="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",at=(e={})=>{const t=/^#![ ]*\//;return e.binary&&(e.begin=C(t,/.*\b/,e.binary,/\b.*/)),B({scope:"meta",begin:t,end:/$/,relevance:0,"on:begin":(i,u)=>{i.index!==0&&u.ignoreMatch()}},e)},U={begin:"\\\\[\\s\\S]",relevance:0},lt={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[U]},ut={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[U]},ft={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},Y=function(e,t,i={}){const u=B({scope:"comment",begin:e,end:t,contains:[]},i);u.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});const b=ne("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return u.contains.push({begin:C(/[ ]+/,"(",b,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),u},gt=Y("//","$"),ht=Y("/\\*","\\*/"),pt=Y("#","$"),dt={scope:"number",begin:Se,relevance:0},Et={scope:"number",begin:Ne,relevance:0},bt={scope:"number",begin:Ae,relevance:0},_t={scope:"regexp",begin:/\/(?=[^/\n]*\/)/,end:/\/[gimuy]*/,contains:[U,{begin:/\[/,end:/\]/,relevance:0,contains:[U]}]},Mt={scope:"title",begin:ye,relevance:0},xt={scope:"title",begin:se,relevance:0},wt={begin:"\\.\\s*"+se,relevance:0},Ot=function(e){return Object.assign(e,{"on:begin":(t,i)=>{i.data._beginMatch=t[1]},"on:end":(t,i)=>{i.data._beginMatch!==t[1]&&i.ignoreMatch()}})};var z=Object.freeze({__proto__:null,APOS_STRING_MODE:lt,BACKSLASH_ESCAPE:U,BINARY_NUMBER_MODE:bt,BINARY_NUMBER_RE:Ae,COMMENT:Y,C_BLOCK_COMMENT_MODE:ht,C_LINE_COMMENT_MODE:gt,C_NUMBER_MODE:Et,C_NUMBER_RE:Ne,END_SAME_AS_BEGIN:Ot,HASH_COMMENT_MODE:pt,IDENT_RE:ye,MATCH_NOTHING_RE:ct,METHOD_GUARD:wt,NUMBER_MODE:dt,NUMBER_RE:Se,PHRASAL_WORDS_MODE:ft,QUOTE_STRING_MODE:ut,REGEXP_MODE:_t,RE_STARTERS_RE:ot,SHEBANG:at,TITLE_MODE:Mt,UNDERSCORE_IDENT_RE:se,UNDERSCORE_TITLE_MODE:xt});function Rt(e,t){e.input[e.index-1]==="."&&t.ignoreMatch()}function yt(e,t){e.className!==void 0&&(e.scope=e.className,delete e.className)}function St(e,t){t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=Rt,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,e.relevance===void 0&&(e.relevance=0))}function Nt(e,t){Array.isArray(e.illegal)&&(e.illegal=ne(...e.illegal))}function At(e,t){if(e.match){if(e.begin||e.end)throw new Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function kt(e,t){e.relevance===void 0&&(e.relevance=1)}const Tt=(e,t)=>{if(!e.beforeMatch)return;if(e.starts)throw new Error("beforeMatch cannot be used with starts");const i=Object.assign({},e);Object.keys(e).forEach(u=>{delete e[u]}),e.keywords=i.keywords,e.begin=C(i.beforeMatch,Oe(i.begin)),e.starts={relevance:0,contains:[Object.assign(i,{endsParent:!0})]},e.relevance=0,delete i.beforeMatch},It=["of","and","for","in","not","or","if","then","parent","list","value"],Bt="keyword";function ke(e,t,i=Bt){const u=Object.create(null);return typeof e=="string"?b(i,e.split(" ")):Array.isArray(e)?b(i,e):Object.keys(e).forEach(function(_){Object.assign(u,ke(e[_],t,_))}),u;function b(_,c){t&&(c=c.map(r=>r.toLowerCase())),c.forEach(function(r){const l=r.split("|");u[l[0]]=[_,Dt(l[0],l[1])]})}}function Dt(e,t){return t?Number(t):vt(e)?0:1}function vt(e){return It.includes(e.toLowerCase())}const Ee={},v=e=>{console.error(e)},be=(e,...t)=>{console.log(`WARN: ${e}`,...t)},L=(e,t)=>{Ee[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),Ee[`${e}/${t}`]=!0)},X=new Error;function Te(e,t,{key:i}){let u=0;const b=e[i],_={},c={};for(let r=1;r<=t.length;r++)c[r+u]=b[r],_[r+u]=!0,u+=Re(t[r-1]);e[i]=c,e[i]._emit=_,e[i]._multi=!0}function Ct(e){if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw v("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),X;if(typeof e.beginScope!="object"||e.beginScope===null)throw v("beginScope must be object"),X;Te(e,e.begin,{key:"beginScope"}),e.begin=ie(e.begin,{joinWith:""})}}function Lt(e){if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw v("skip, excludeEnd, returnEnd not compatible with endScope: {}"),X;if(typeof e.endScope!="object"||e.endScope===null)throw v("endScope must be object"),X;Te(e,e.end,{key:"endScope"}),e.end=ie(e.end,{joinWith:""})}}function Ht(e){e.scope&&typeof e.scope=="object"&&e.scope!==null&&(e.beginScope=e.scope,delete e.scope)}function jt(e){Ht(e),typeof e.beginScope=="string"&&(e.beginScope={_wrap:e.beginScope}),typeof e.endScope=="string"&&(e.endScope={_wrap:e.endScope}),Ct(e),Lt(e)}function Pt(e){function t(c,r){return new RegExp(P(c),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(r?"g":""))}class i{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(r,l){l.position=this.position++,this.matchIndexes[this.matchAt]=l,this.regexes.push([l,r]),this.matchAt+=Re(r)+1}compile(){this.regexes.length===0&&(this.exec=()=>null);const r=this.regexes.map(l=>l[1]);this.matcherRe=t(ie(r,{joinWith:"|"}),!0),this.lastIndex=0}exec(r){this.matcherRe.lastIndex=this.lastIndex;const l=this.matcherRe.exec(r);if(!l)return null;const w=l.findIndex((j,Z)=>Z>0&&j!==void 0),M=this.matchIndexes[w];return l.splice(0,w),Object.assign(l,M)}}class u{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(r){if(this.multiRegexes[r])return this.multiRegexes[r];const l=new i;return this.rules.slice(r).forEach(([w,M])=>l.addRule(w,M)),l.compile(),this.multiRegexes[r]=l,l}resumingScanAtSamePosition(){return this.regexIndex!==0}considerAll(){this.regexIndex=0}addRule(r,l){this.rules.push([r,l]),l.type==="begin"&&this.count++}exec(r){const l=this.getMatcher(this.regexIndex);l.lastIndex=this.lastIndex;let w=l.exec(r);if(this.resumingScanAtSamePosition()&&!(w&&w.index===this.lastIndex)){const M=this.getMatcher(0);M.lastIndex=this.lastIndex+1,w=M.exec(r)}return w&&(this.regexIndex+=w.position+1,this.regexIndex===this.count&&this.considerAll()),w}}function b(c){const r=new u;return c.contains.forEach(l=>r.addRule(l.begin,{rule:l,type:"begin"})),c.terminatorEnd&&r.addRule(c.terminatorEnd,{type:"end"}),c.illegal&&r.addRule(c.illegal,{type:"illegal"}),r}function _(c,r){const l=c;if(c.isCompiled)return l;[yt,At,jt,Tt].forEach(M=>M(c,r)),e.compilerExtensions.forEach(M=>M(c,r)),c.__beforeBegin=null,[St,Nt,kt].forEach(M=>M(c,r)),c.isCompiled=!0;let w=null;return typeof c.keywords=="object"&&c.keywords.$pattern&&(c.keywords=Object.assign({},c.keywords),w=c.keywords.$pattern,delete c.keywords.$pattern),w=w||/\w+/,c.keywords&&(c.keywords=ke(c.keywords,e.case_insensitive)),l.keywordPatternRe=t(w,!0),r&&(c.begin||(c.begin=/\B|\b/),l.beginRe=t(l.begin),!c.end&&!c.endsWithParent&&(c.end=/\B|\b/),c.end&&(l.endRe=t(l.end)),l.terminatorEnd=P(l.end)||"",c.endsWithParent&&r.terminatorEnd&&(l.terminatorEnd+=(c.end?"|":"")+r.terminatorEnd)),c.illegal&&(l.illegalRe=t(c.illegal)),c.contains||(c.contains=[]),c.contains=[].concat(...c.contains.map(function(M){return Ut(M==="self"?c:M)})),c.contains.forEach(function(M){_(M,l)}),c.starts&&_(c.starts,r),l.matcher=b(l),l}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=B(e.classNameAliases||{}),_(e)}function Ie(e){return e?e.endsWithParent||Ie(e.starts):!1}function Ut(e){return e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map(function(t){return B(e,{variants:null},t)})),e.cachedVariants?e.cachedVariants:Ie(e)?B(e,{starts:e.starts?B(e.starts):null}):Object.isFrozen(e)?B(e):e}var $t="11.11.1";class Gt extends Error{constructor(t,i){super(t),this.name="HTMLInjectionError",this.html=i}}const ee=we,_e=B,Me=Symbol("nomatch"),Wt=7,Be=function(e){const t=Object.create(null),i=Object.create(null),u=[];let b=!0;const _="Could not find the language '{}', did you forget to load/include a language module?",c={disableAutodetect:!0,name:"Plain text",contains:[]};let r={ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:et};function l(n){return r.noHighlightRe.test(n)}function w(n){let a=n.className+" ";a+=n.parentNode?n.parentNode.className:"";const h=r.languageDetectRe.exec(a);if(h){const d=T(h[1]);return d||(be(_.replace("{}",h[1])),be("Falling back to no-highlight mode for this block.",n)),d?h[1]:"no-highlight"}return a.split(/\s+/).find(d=>l(d)||T(d))}function M(n,a,h){let d="",x="";typeof a=="object"?(d=n,h=a.ignoreIllegals,x=a.language):(L("10.7.0","highlight(lang, code, ...args) has been deprecated."),L("10.7.0",`Please use highlight(code, options) instead.
2
2
  https://github.com/highlightjs/highlight.js/issues/2277`),x=n,d=a),h===void 0&&(h=!0);const S={code:d,language:x};G("before:highlight",S);const I=S.result?S.result:j(S.language,S.code,h);return I.code=S.code,G("after:highlight",I),I}function j(n,a,h,d){const x=Object.create(null);function S(s,o){return s.keywords[o]}function I(){if(!f.keywords){O.addText(E);return}let s=0;f.keywordPatternRe.lastIndex=0;let o=f.keywordPatternRe.exec(E),g="";for(;o;){g+=E.substring(s,o.index);const p=A.case_insensitive?o[0].toLowerCase():o[0],R=S(f,p);if(R){const[k,Ze]=R;if(O.addText(g),g="",x[p]=(x[p]||0)+1,x[p]<=Wt&&(F+=Ze),k.startsWith("_"))g+=o[0];else{const Je=A.classNameAliases[k]||k;N(o[0],Je)}}else g+=o[0];s=f.keywordPatternRe.lastIndex,o=f.keywordPatternRe.exec(E)}g+=E.substring(s),O.addText(g)}function W(){if(E==="")return;let s=null;if(typeof f.subLanguage=="string"){if(!t[f.subLanguage]){O.addText(E);return}s=j(f.subLanguage,E,!0,ge[f.subLanguage]),ge[f.subLanguage]=s._top}else s=J(E,f.subLanguage.length?f.subLanguage:null);f.relevance>0&&(F+=s.relevance),O.__addSublanguage(s._emitter,s.language)}function y(){f.subLanguage!=null?W():I(),E=""}function N(s,o){s!==""&&(O.startScope(o),O.addText(s),O.endScope())}function ae(s,o){let g=1;const p=o.length-1;for(;g<=p;){if(!s._emit[g]){g++;continue}const R=A.classNameAliases[s[g]]||s[g],k=o[g];R?N(k,R):(E=k,I(),E=""),g++}}function le(s,o){return s.scope&&typeof s.scope=="string"&&O.openNode(A.classNameAliases[s.scope]||s.scope),s.beginScope&&(s.beginScope._wrap?(N(E,A.classNameAliases[s.beginScope._wrap]||s.beginScope._wrap),E=""):s.beginScope._multi&&(ae(s.beginScope,o),E="")),f=Object.create(s,{parent:{value:f}}),f}function ue(s,o,g){let p=st(s.endRe,g);if(p){if(s["on:end"]){const R=new he(s);s["on:end"](o,R),R.isMatchIgnored&&(p=!1)}if(p){for(;s.endsParent&&s.parent;)s=s.parent;return s}}if(s.endsWithParent)return ue(s.parent,o,g)}function Ke(s){return f.matcher.regexIndex===0?(E+=s[0],1):(m=!0,0)}function Fe(s){const o=s[0],g=s.rule,p=new he(g),R=[g.__beforeBegin,g["on:begin"]];for(const k of R)if(k&&(k(s,p),p.isMatchIgnored))return Ke(o);return g.skip?E+=o:(g.excludeBegin&&(E+=o),y(),!g.returnBegin&&!g.excludeBegin&&(E=o)),le(g,s),g.returnBegin?0:o.length}function ze(s){const o=s[0],g=a.substring(s.index),p=ue(f,s,g);if(!p)return Me;const R=f;f.endScope&&f.endScope._wrap?(y(),N(o,f.endScope._wrap)):f.endScope&&f.endScope._multi?(y(),ae(f.endScope,s)):R.skip?E+=o:(R.returnEnd||R.excludeEnd||(E+=o),y(),R.excludeEnd&&(E=o));do f.scope&&O.closeNode(),!f.skip&&!f.subLanguage&&(F+=f.relevance),f=f.parent;while(f!==p.parent);return p.starts&&le(p.starts,s),R.returnEnd?0:o.length}function Xe(){const s=[];for(let o=f;o!==A;o=o.parent)o.scope&&s.unshift(o.scope);s.forEach(o=>O.openNode(o))}let K={};function fe(s,o){const g=o&&o[0];if(E+=s,g==null)return y(),0;if(K.type==="begin"&&o.type==="end"&&K.index===o.index&&g===""){if(E+=a.slice(o.index,o.index+1),!b){const p=new Error(`0 width match regex (${n})`);throw p.languageName=n,p.badRule=K.rule,p}return 1}if(K=o,o.type==="begin")return Fe(o);if(o.type==="illegal"&&!h){const p=new Error('Illegal lexeme "'+g+'" for mode "'+(f.scope||"<unnamed>")+'"');throw p.mode=f,p}else if(o.type==="end"){const p=ze(o);if(p!==Me)return p}if(o.type==="illegal"&&g==="")return E+=`
3
3
  `,1;if(Q>1e5&&Q>o.index*3)throw new Error("potential infinite loop, way more iterations than matches");return E+=g,g.length}const A=T(n);if(!A)throw v(_.replace("{}",n)),new Error('Unknown language: "'+n+'"');const Ye=Pt(A);let q="",f=d||Ye;const ge={},O=new r.__emitter(r);Xe();let E="",F=0,D=0,Q=0,m=!1;try{if(A.__emitTokens)A.__emitTokens(a,O);else{for(f.matcher.considerAll();;){Q++,m?m=!1:f.matcher.considerAll(),f.matcher.lastIndex=D;const s=f.matcher.exec(a);if(!s)break;const o=a.substring(D,s.index),g=fe(o,s);D=s.index+g}fe(a.substring(D))}return O.finalize(),q=O.toHTML(),{language:n,value:q,relevance:F,illegal:!1,_emitter:O,_top:f}}catch(s){if(s.message&&s.message.includes("Illegal"))return{language:n,value:ee(a),illegal:!0,relevance:0,_illegalBy:{message:s.message,index:D,context:a.slice(D-100,D+100),mode:s.mode,resultSoFar:q},_emitter:O};if(b)return{language:n,value:ee(a),illegal:!1,relevance:0,errorRaised:s,_emitter:O,_top:f};throw s}}function Z(n){const a={value:ee(n),illegal:!1,relevance:0,_top:c,_emitter:new r.__emitter(r)};return a._emitter.addText(n),a}function J(n,a){a=a||r.languages||Object.keys(t);const h=Z(n),d=a.filter(T).filter(oe).map(y=>j(y,n,!1));d.unshift(h);const x=d.sort((y,N)=>{if(y.relevance!==N.relevance)return N.relevance-y.relevance;if(y.language&&N.language){if(T(y.language).supersetOf===N.language)return 1;if(T(N.language).supersetOf===y.language)return-1}return 0}),[S,I]=x,W=S;return W.secondBest=I,W}function De(n,a,h){const d=a&&i[a]||h;n.classList.add("hljs"),n.classList.add(`language-${d}`)}function V(n){let a=null;const h=w(n);if(l(h))return;if(G("before:highlightElement",{el:n,language:h}),n.dataset.highlighted){console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",n);return}if(n.children.length>0&&(r.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(n)),r.throwUnescapedHTML))throw new Gt("One of your code blocks includes unescaped HTML.",n.innerHTML);a=n;const d=a.textContent,x=h?M(d,{language:h,ignoreIllegals:!0}):J(d);n.innerHTML=x.value,n.dataset.highlighted="yes",De(n,h,x.language),n.result={language:x.language,re:x.relevance,relevance:x.relevance},x.secondBest&&(n.secondBest={language:x.secondBest.language,relevance:x.secondBest.relevance}),G("after:highlightElement",{el:n,result:x,text:d})}function ve(n){r=_e(r,n)}const Ce=()=>{$(),L("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")};function Le(){$(),L("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")}let re=!1;function $(){function n(){$()}if(document.readyState==="loading"){re||window.addEventListener("DOMContentLoaded",n,!1),re=!0;return}document.querySelectorAll(r.cssSelector).forEach(V)}function He(n,a){let h=null;try{h=a(e)}catch(d){if(v("Language definition for '{}' could not be registered.".replace("{}",n)),b)v(d);else throw d;h=c}h.name||(h.name=n),t[n]=h,h.rawDefinition=a.bind(null,e),h.aliases&&ce(h.aliases,{languageName:n})}function je(n){delete t[n];for(const a of Object.keys(i))i[a]===n&&delete i[a]}function Pe(){return Object.keys(t)}function T(n){return n=(n||"").toLowerCase(),t[n]||t[i[n]]}function ce(n,{languageName:a}){typeof n=="string"&&(n=[n]),n.forEach(h=>{i[h.toLowerCase()]=a})}function oe(n){const a=T(n);return a&&!a.disableAutodetect}function Ue(n){n["before:highlightBlock"]&&!n["before:highlightElement"]&&(n["before:highlightElement"]=a=>{n["before:highlightBlock"](Object.assign({block:a.el},a))}),n["after:highlightBlock"]&&!n["after:highlightElement"]&&(n["after:highlightElement"]=a=>{n["after:highlightBlock"](Object.assign({block:a.el},a))})}function $e(n){Ue(n),u.push(n)}function Ge(n){const a=u.indexOf(n);a!==-1&&u.splice(a,1)}function G(n,a){const h=n;u.forEach(function(d){d[h]&&d[h](a)})}function We(n){return L("10.7.0","highlightBlock will be removed entirely in v12.0"),L("10.7.0","Please use highlightElement now."),V(n)}Object.assign(e,{highlight:M,highlightAuto:J,highlightAll:$,highlightElement:V,highlightBlock:We,configure:ve,initHighlighting:Ce,initHighlightingOnLoad:Le,registerLanguage:He,unregisterLanguage:je,listLanguages:Pe,getLanguage:T,registerAliases:ce,autoDetection:oe,inherit:_e,addPlugin:$e,removePlugin:Ge}),e.debugMode=function(){b=!1},e.safeMode=function(){b=!0},e.versionString=$t,e.regex={concat:C,lookahead:Oe,either:ne,optional:nt,anyNumberOfTimes:tt};for(const n in z)typeof z[n]=="object"&&xe(z[n]);return Object.assign(e,z),e},H=Be({});H.newInstance=()=>Be({});var Kt=H;H.HighlightJS=H;H.default=H;const zt=Ve(Kt);export{zt as HighlightJS,zt as default};