@idl3/claude-control 1.3.0 → 1.4.3
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/bin/claude-print-bridge.mjs +247 -0
- package/lib/claude-print.js +352 -0
- package/lib/codex-rpc.js +719 -0
- package/lib/codex.js +553 -89
- package/lib/pane-registry.js +38 -3
- package/lib/prompt.js +60 -21
- package/lib/sessions.js +281 -60
- package/lib/subagents.js +113 -0
- package/lib/tmux.js +68 -11
- package/lib/transcribe.js +1 -1
- package/lib/tui.js +10 -3
- package/lib/version.js +44 -8
- package/package.json +1 -1
- package/server.js +561 -39
- package/web/dist/assets/{core-C29-1O9j.js → core-BPDebW1g.js} +1 -1
- package/web/dist/assets/index-B3rIEzoc.css +1 -0
- package/web/dist/assets/index-DIwGyVZ7.js +104 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CT-y6LU4.css +0 -1
- package/web/dist/assets/index-DzIDTXLS.js +0 -103
package/lib/pane-registry.js
CHANGED
|
@@ -12,6 +12,20 @@ import os from 'node:os';
|
|
|
12
12
|
|
|
13
13
|
const PANES_DIR = path.join(os.homedir(), '.claude-control', 'panes');
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Reset GC state. Exported FOR TESTS ONLY; current GC is transcript-existence
|
|
17
|
+
* based and has no mutable state, so this is intentionally a no-op.
|
|
18
|
+
*/
|
|
19
|
+
export function _resetGcStateForTest() {
|
|
20
|
+
// no-op
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** %5 → "5"; tolerate any tmux pane-id form, keep it filename-safe. */
|
|
24
|
+
function paneFile(tmuxPane, dir = PANES_DIR) {
|
|
25
|
+
const safe = String(tmuxPane || '').replace(/[^A-Za-z0-9_-]/g, '');
|
|
26
|
+
return safe ? path.join(dir, `${safe}.json`) : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
/**
|
|
16
30
|
* @typedef {Object} PaneRecord
|
|
17
31
|
* @property {string} paneId tmux %N (matches a pane's paneId)
|
|
@@ -55,9 +69,30 @@ export async function readPaneRegistry(dir = PANES_DIR) {
|
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
72
|
+
* Persist an exact pane→transcript binding for transports that discover the
|
|
73
|
+
* rollout path programmatically instead of via Claude's hook.
|
|
74
|
+
*
|
|
75
|
+
* @param {{paneId:string, sessionId?:string|null, transcriptPath:string, cwd?:string|null}} rec
|
|
76
|
+
* @param {string} [dir] Override the registry dir (tests).
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
export async function writePaneRegistryRecord(rec, dir = PANES_DIR) {
|
|
80
|
+
if (!rec || typeof rec.paneId !== 'string' || typeof rec.transcriptPath !== 'string') return;
|
|
81
|
+
const file = paneFile(rec.paneId, dir);
|
|
82
|
+
if (!file) return;
|
|
83
|
+
await fsp.mkdir(dir, { recursive: true }).catch(() => {});
|
|
84
|
+
const record = {
|
|
85
|
+
paneId: rec.paneId,
|
|
86
|
+
sessionId: rec.sessionId ?? null,
|
|
87
|
+
transcriptPath: rec.transcriptPath,
|
|
88
|
+
cwd: rec.cwd ?? null,
|
|
89
|
+
ts: Date.now(),
|
|
90
|
+
};
|
|
91
|
+
await fsp.writeFile(file, JSON.stringify(record), { mode: 0o600 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove registry files whose transcript files no longer exist.
|
|
61
96
|
*
|
|
62
97
|
* It deliberately does NOT use the live tmux pane set. That scan flickers
|
|
63
98
|
* (transient `list-panes` hiccups, a session momentarily not enumerated on a
|
package/lib/prompt.js
CHANGED
|
@@ -15,16 +15,24 @@ function stripAnsi(s) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// A numbered option line, optionally preceded by the TUI cursor (❯/›).
|
|
18
|
-
|
|
18
|
+
// `\d+` (not `\d`) so pickers with ≥10 rows parse their two-digit numbers.
|
|
19
|
+
const OPTION_RE = /^\s*([❯›]?)\s*(\d+)[.)]\s+(.+?)\s*$/;
|
|
19
20
|
// A checkbox marker at the START of an option label, e.g. "[ ] Label" or "[x] Label".
|
|
20
21
|
// Matches the bracket content: space = unchecked; x/✓/✗ = checked.
|
|
21
22
|
const CHECKBOX_RE = /^\[([✓x✗ ])\]\s*(.*)/;
|
|
22
|
-
// The footer real Claude Code
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
const
|
|
23
|
+
// The footer a real Claude Code SELECTION prompt renders under the options.
|
|
24
|
+
// Deterministically EXCLUDES "esc to interrupt" — that is the working-state
|
|
25
|
+
// footer (Claude is generating), not a prompt. A numbered list in assistant
|
|
26
|
+
// prose shown while Claude works would otherwise false-positive as a prompt.
|
|
27
|
+
// Real selection prompts say "esc to cancel / reject / keep".
|
|
28
|
+
const ESC_HINT_RE = /\besc\b[^\n]*(cancel|reject|keep)/i;
|
|
29
|
+
// How many lines from the bottom to consider. The active prompt always renders
|
|
30
|
+
// at the bottom of the pane; the cursor/Esc-footer guard (not this window) is
|
|
31
|
+
// what rejects assistant prose, so this can be generous. It must be large
|
|
32
|
+
// enough to contain a tall AskUserQuestion (long question + 5 options each with
|
|
33
|
+
// a multi-line description + footer) — otherwise the question + first options
|
|
34
|
+
// scroll out and the header heuristic grabs an option-description fragment.
|
|
35
|
+
const BOTTOM_REGION = 80;
|
|
28
36
|
const MAX_LABEL = 80;
|
|
29
37
|
|
|
30
38
|
/**
|
|
@@ -104,25 +112,56 @@ export function parsePanePrompt(capture) {
|
|
|
104
112
|
// Require a genuine interactive-prompt signal — not just numbered prose.
|
|
105
113
|
if (!hasCursor && !hasEsc) return null;
|
|
106
114
|
|
|
107
|
-
// Question = the contiguous
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
// Question = the contiguous block above the option run. Only trust it when the
|
|
116
|
+
// run starts at key 1 — i.e. the WHOLE picker is in view. If it starts higher
|
|
117
|
+
// (1/2 scrolled off despite the large window), the lines above the first
|
|
118
|
+
// visible option are a prior option's wrapped DESCRIPTION, not the question, so
|
|
119
|
+
// we emit no header rather than a misleading fragment.
|
|
120
|
+
let question = null;
|
|
121
|
+
if (Number(options[0].key) === 1) {
|
|
122
|
+
let i = firstLine - 1;
|
|
123
|
+
while (i >= 0 && !lines[i].trim()) i--; // skip the blank separator(s)
|
|
124
|
+
const qLines = [];
|
|
125
|
+
for (; i >= 0; i--) {
|
|
126
|
+
const t = lines[i].trim();
|
|
127
|
+
if (!t) break; // stop at the blank above the question block
|
|
128
|
+
if (OPTION_RE.test(lines[i])) break; // don't bleed into a prior option
|
|
129
|
+
qLines.unshift(t);
|
|
130
|
+
}
|
|
131
|
+
question = qLines.join(' ').slice(0, 400) || null;
|
|
114
132
|
}
|
|
115
|
-
|
|
133
|
+
|
|
134
|
+
// Each option may carry a wrapped DESCRIPTION — the indented sub-text the TUI
|
|
135
|
+
// renders under the label (e.g. "Tear down the proxy/gateway…"). It lives on
|
|
136
|
+
// the contiguous non-blank lines between this option's line and the next
|
|
137
|
+
// option's line (or the footer), so capture those so the cockpit shows the
|
|
138
|
+
// same context the TUI does instead of just the bare label.
|
|
139
|
+
const descFor = (idx) => {
|
|
140
|
+
const start = options[idx].line + 1;
|
|
141
|
+
const end = idx + 1 < options.length ? options[idx + 1].line : lines.length;
|
|
142
|
+
const out = [];
|
|
143
|
+
for (let i = start; i < end; i++) {
|
|
144
|
+
const t = lines[i].trim();
|
|
145
|
+
if (!t || OPTION_RE.test(lines[i]) || ESC_HINT_RE.test(lines[i])) break;
|
|
146
|
+
out.push(t);
|
|
147
|
+
}
|
|
148
|
+
const desc = out.join(' ').slice(0, 300);
|
|
149
|
+
return desc || undefined;
|
|
150
|
+
};
|
|
116
151
|
|
|
117
152
|
const hasCheckboxes = options.some((o) => o.checked !== undefined);
|
|
118
153
|
return {
|
|
119
154
|
question: question || 'Make a selection',
|
|
120
155
|
...(hasCheckboxes ? { multiSelect: true } : {}),
|
|
121
|
-
options: options.map((o) =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
options: options.map((o, idx) => {
|
|
157
|
+
const description = descFor(idx);
|
|
158
|
+
return {
|
|
159
|
+
key: o.key,
|
|
160
|
+
label: o.label,
|
|
161
|
+
selected: o.cursor,
|
|
162
|
+
...(description ? { description } : {}),
|
|
163
|
+
...(o.checked !== undefined ? { checked: o.checked } : {}),
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
127
166
|
};
|
|
128
167
|
}
|