@indianic/dialout 1.13.1 → 1.13.2
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/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.js +20 -5
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.js +75 -0
- package/node_modules/@indianic/dialout-shared/src/pane-prompt.test.ts +82 -0
- package/node_modules/@indianic/dialout-shared/src/pane-prompt.ts +21 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pane-prompt.d.ts","sourceRoot":"","sources":["../src/pane-prompt.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,mEAAmE;IACnE,KAAK,EAAE,OAAO,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"pane-prompt.d.ts","sourceRoot":"","sources":["../src/pane-prompt.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,mEAAmE;IACnE,KAAK,EAAE,OAAO,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAgID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAuChE;AAED,yEAAyE;AACzE,eAAO,MAAM,WAAW,sGAAuG,CAAC;AAChI,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAG7D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAK7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAGjD"}
|
|
@@ -95,9 +95,18 @@ function numberedMenu(lines, bottom) {
|
|
|
95
95
|
for (let i = bottom; i >= 0; i--) {
|
|
96
96
|
const raw = lines[i];
|
|
97
97
|
if (!raw.trim()) {
|
|
98
|
-
if (opts.length
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
if (!opts.length && !hints.length)
|
|
99
|
+
continue;
|
|
100
|
+
// A blank line BETWEEN options is normal and must not end the scan.
|
|
101
|
+
// Claude Code separates its trailing rows ("Type something.", "Chat
|
|
102
|
+
// about this") from the numbered choices with one, and breaking here
|
|
103
|
+
// collected a single option, failed the two-option minimum, and returned
|
|
104
|
+
// null — so a question sitting on screen was reported as no prompt at
|
|
105
|
+
// all and the session read "working" while the agent waited. Measured
|
|
106
|
+
// against a real pane on 2026-10-30.
|
|
107
|
+
if (i > 0 && NUMBERED.test(lines[i - 1]))
|
|
108
|
+
continue;
|
|
109
|
+
break;
|
|
101
110
|
}
|
|
102
111
|
if (SEPARATOR.test(raw.trim())) {
|
|
103
112
|
if (++separators > 2)
|
|
@@ -115,8 +124,14 @@ function numberedMenu(lines, bottom) {
|
|
|
115
124
|
}
|
|
116
125
|
// Deeper than an option's label: that option's explanation (it sits
|
|
117
126
|
// below its option, so it is seen first when scanning upwards).
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
//
|
|
128
|
+
// Keep only the first few for display, but keep SCANNING past the rest. A
|
|
129
|
+
// long explanation is not the top of the menu, and stopping at the fourth
|
|
130
|
+
// line of one truncated the menu to whatever had been collected below it.
|
|
131
|
+
// Claude Code routinely writes five- and six-line explanations.
|
|
132
|
+
if (raw.search(/\S/) >= Math.max(4, labelCol)) {
|
|
133
|
+
if (hints.length < 3)
|
|
134
|
+
hints.unshift(raw.trim());
|
|
120
135
|
continue;
|
|
121
136
|
}
|
|
122
137
|
break;
|
|
@@ -182,3 +182,78 @@ README.md package.json src
|
|
|
182
182
|
(0, vitest_1.expect)((0, pane_prompt_1.freeTextRow)((0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH))).toBe(0);
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
|
+
/**
|
|
186
|
+
* A real Claude Code question, captured 2026-10-30 from a pane whose session
|
|
187
|
+
* showed "Claude is working…" in the app while the terminal sat waiting on it.
|
|
188
|
+
*
|
|
189
|
+
* Two guards rejected it, and the symptom of either is the same: detectPanePrompt
|
|
190
|
+
* returns null, statusWithPrompt leaves the status as whatever transcript timing
|
|
191
|
+
* said, and a blocked agent reads as a busy one. Nothing is logged, because "no
|
|
192
|
+
* prompt" is indistinguishable from "no prompt yet".
|
|
193
|
+
*
|
|
194
|
+
* - a blank line BETWEEN options ended the upward scan, so only the last row
|
|
195
|
+
* was collected and the two-option minimum failed;
|
|
196
|
+
* - an explanation longer than three lines ended it too, truncating the menu
|
|
197
|
+
* to whatever had been collected below.
|
|
198
|
+
*/
|
|
199
|
+
(0, vitest_1.describe)('a menu with blank lines between options and long explanations', () => {
|
|
200
|
+
const pane = [
|
|
201
|
+
" That's the right test design — you can't test \"the runner refuses to",
|
|
202
|
+
' continue when an applied migration has been edited" against your',
|
|
203
|
+
' real dev database. The question is how to give it a cluster.',
|
|
204
|
+
'',
|
|
205
|
+
' □ Test cluster',
|
|
206
|
+
' The new migration tests need a throwaway Postgres cluster, and',
|
|
207
|
+
" macOS's default kernel limits are too small for `initdb` to",
|
|
208
|
+
' start one. How should I unblock it?',
|
|
209
|
+
'',
|
|
210
|
+
'❯ 1. Use Docker instead (Recommended)',
|
|
211
|
+
' Redo database-schema so the ephemeral-cluster harness launches a',
|
|
212
|
+
' disposable postgres:17-alpine container instead of calling',
|
|
213
|
+
' initdb. Slower per test (~2-3s startup), but it needs no kernel',
|
|
214
|
+
' change, works on any machine, and matches how the project already',
|
|
215
|
+
' runs its dev database. Costs one more redo round.',
|
|
216
|
+
' 2. Raise the kernel limits',
|
|
217
|
+
' Run sudo sysctl to raise kern.sysv.shmmax/shmall/shmseg on your',
|
|
218
|
+
' Mac so initdb works. Fastest path — no code change, possibly',
|
|
219
|
+
' unblocks your project right now. But it is a machine-wide change,',
|
|
220
|
+
' needs your password, reverts on reboot unless written to',
|
|
221
|
+
' /etc/sysctl.conf, and the same failure will reappear on any other',
|
|
222
|
+
' machine or CI runner.',
|
|
223
|
+
' 3. Both',
|
|
224
|
+
' Raise the limits now to get P4B moving today, and separately',
|
|
225
|
+
' record a task to move the harness onto Docker before P9',
|
|
226
|
+
' launch-readiness, so CI is not left with a host-specific',
|
|
227
|
+
' dependency.',
|
|
228
|
+
' 4. Type something.',
|
|
229
|
+
'',
|
|
230
|
+
' 5. Chat about this',
|
|
231
|
+
'',
|
|
232
|
+
'Enter to select · ↑/↓ to navigate · Esc to cancel',
|
|
233
|
+
].join('\n');
|
|
234
|
+
(0, vitest_1.it)('is detected at all', () => {
|
|
235
|
+
(0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(pane)).not.toBeNull();
|
|
236
|
+
});
|
|
237
|
+
(0, vitest_1.it)('collects every option, not just the ones below the blank line', () => {
|
|
238
|
+
const p = (0, pane_prompt_1.detectPanePrompt)(pane);
|
|
239
|
+
(0, vitest_1.expect)(p.options.map((o) => o.n)).toEqual([1, 2, 3, 4, 5]);
|
|
240
|
+
(0, vitest_1.expect)(p.options[4].label).toBe('Chat about this');
|
|
241
|
+
});
|
|
242
|
+
(0, vitest_1.it)('keeps the cursor on the row the TUI has it on', () => {
|
|
243
|
+
const p = (0, pane_prompt_1.detectPanePrompt)(pane);
|
|
244
|
+
(0, vitest_1.expect)(p.options.filter((o) => o.selected).map((o) => o.n)).toEqual([1]);
|
|
245
|
+
});
|
|
246
|
+
(0, vitest_1.it)('reads it as a question to answer, not an approval to grant', () => {
|
|
247
|
+
(0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(pane).approval).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
// The row that takes typed text rather than being an answer itself: picking
|
|
250
|
+
// it with Enter alone would answer with nothing.
|
|
251
|
+
(0, vitest_1.it)('still finds the free-text row behind the long explanations', () => {
|
|
252
|
+
(0, vitest_1.expect)((0, pane_prompt_1.freeTextRow)((0, pane_prompt_1.detectPanePrompt)(pane))).toBe(4);
|
|
253
|
+
});
|
|
254
|
+
(0, vitest_1.it)('keeps an explanation with each option that has one', () => {
|
|
255
|
+
const p = (0, pane_prompt_1.detectPanePrompt)(pane);
|
|
256
|
+
(0, vitest_1.expect)(p.options[0].hint).toBeTruthy();
|
|
257
|
+
(0, vitest_1.expect)(p.options[3].hint).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -199,3 +199,85 @@ describe('free-text rows', () => {
|
|
|
199
199
|
expect(freeTextRow(detectPanePrompt(CLAUDE_BASH)!)).toBe(0);
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A real Claude Code question, captured 2026-10-30 from a pane whose session
|
|
205
|
+
* showed "Claude is working…" in the app while the terminal sat waiting on it.
|
|
206
|
+
*
|
|
207
|
+
* Two guards rejected it, and the symptom of either is the same: detectPanePrompt
|
|
208
|
+
* returns null, statusWithPrompt leaves the status as whatever transcript timing
|
|
209
|
+
* said, and a blocked agent reads as a busy one. Nothing is logged, because "no
|
|
210
|
+
* prompt" is indistinguishable from "no prompt yet".
|
|
211
|
+
*
|
|
212
|
+
* - a blank line BETWEEN options ended the upward scan, so only the last row
|
|
213
|
+
* was collected and the two-option minimum failed;
|
|
214
|
+
* - an explanation longer than three lines ended it too, truncating the menu
|
|
215
|
+
* to whatever had been collected below.
|
|
216
|
+
*/
|
|
217
|
+
describe('a menu with blank lines between options and long explanations', () => {
|
|
218
|
+
const pane = [
|
|
219
|
+
" That's the right test design — you can't test \"the runner refuses to",
|
|
220
|
+
' continue when an applied migration has been edited" against your',
|
|
221
|
+
' real dev database. The question is how to give it a cluster.',
|
|
222
|
+
'',
|
|
223
|
+
' □ Test cluster',
|
|
224
|
+
' The new migration tests need a throwaway Postgres cluster, and',
|
|
225
|
+
" macOS's default kernel limits are too small for `initdb` to",
|
|
226
|
+
' start one. How should I unblock it?',
|
|
227
|
+
'',
|
|
228
|
+
'❯ 1. Use Docker instead (Recommended)',
|
|
229
|
+
' Redo database-schema so the ephemeral-cluster harness launches a',
|
|
230
|
+
' disposable postgres:17-alpine container instead of calling',
|
|
231
|
+
' initdb. Slower per test (~2-3s startup), but it needs no kernel',
|
|
232
|
+
' change, works on any machine, and matches how the project already',
|
|
233
|
+
' runs its dev database. Costs one more redo round.',
|
|
234
|
+
' 2. Raise the kernel limits',
|
|
235
|
+
' Run sudo sysctl to raise kern.sysv.shmmax/shmall/shmseg on your',
|
|
236
|
+
' Mac so initdb works. Fastest path — no code change, possibly',
|
|
237
|
+
' unblocks your project right now. But it is a machine-wide change,',
|
|
238
|
+
' needs your password, reverts on reboot unless written to',
|
|
239
|
+
' /etc/sysctl.conf, and the same failure will reappear on any other',
|
|
240
|
+
' machine or CI runner.',
|
|
241
|
+
' 3. Both',
|
|
242
|
+
' Raise the limits now to get P4B moving today, and separately',
|
|
243
|
+
' record a task to move the harness onto Docker before P9',
|
|
244
|
+
' launch-readiness, so CI is not left with a host-specific',
|
|
245
|
+
' dependency.',
|
|
246
|
+
' 4. Type something.',
|
|
247
|
+
'',
|
|
248
|
+
' 5. Chat about this',
|
|
249
|
+
'',
|
|
250
|
+
'Enter to select · ↑/↓ to navigate · Esc to cancel',
|
|
251
|
+
].join('\n');
|
|
252
|
+
|
|
253
|
+
it('is detected at all', () => {
|
|
254
|
+
expect(detectPanePrompt(pane)).not.toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('collects every option, not just the ones below the blank line', () => {
|
|
258
|
+
const p = detectPanePrompt(pane)!;
|
|
259
|
+
expect(p.options.map((o) => o.n)).toEqual([1, 2, 3, 4, 5]);
|
|
260
|
+
expect(p.options[4].label).toBe('Chat about this');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('keeps the cursor on the row the TUI has it on', () => {
|
|
264
|
+
const p = detectPanePrompt(pane)!;
|
|
265
|
+
expect(p.options.filter((o) => o.selected).map((o) => o.n)).toEqual([1]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('reads it as a question to answer, not an approval to grant', () => {
|
|
269
|
+
expect(detectPanePrompt(pane)!.approval).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// The row that takes typed text rather than being an answer itself: picking
|
|
273
|
+
// it with Enter alone would answer with nothing.
|
|
274
|
+
it('still finds the free-text row behind the long explanations', () => {
|
|
275
|
+
expect(freeTextRow(detectPanePrompt(pane)!)).toBe(4);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('keeps an explanation with each option that has one', () => {
|
|
279
|
+
const p = detectPanePrompt(pane)!;
|
|
280
|
+
expect(p.options[0].hint).toBeTruthy();
|
|
281
|
+
expect(p.options[3].hint).toBeUndefined();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -101,7 +101,18 @@ function numberedMenu(lines: string[], bottom: number): Menu | null {
|
|
|
101
101
|
let separators = 0;
|
|
102
102
|
for (let i = bottom; i >= 0; i--) {
|
|
103
103
|
const raw = lines[i];
|
|
104
|
-
if (!raw.trim()) {
|
|
104
|
+
if (!raw.trim()) {
|
|
105
|
+
if (!opts.length && !hints.length) continue;
|
|
106
|
+
// A blank line BETWEEN options is normal and must not end the scan.
|
|
107
|
+
// Claude Code separates its trailing rows ("Type something.", "Chat
|
|
108
|
+
// about this") from the numbered choices with one, and breaking here
|
|
109
|
+
// collected a single option, failed the two-option minimum, and returned
|
|
110
|
+
// null — so a question sitting on screen was reported as no prompt at
|
|
111
|
+
// all and the session read "working" while the agent waited. Measured
|
|
112
|
+
// against a real pane on 2026-10-30.
|
|
113
|
+
if (i > 0 && NUMBERED.test(lines[i - 1])) continue;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
105
116
|
if (SEPARATOR.test(raw.trim())) { if (++separators > 2) break; continue; }
|
|
106
117
|
const m = raw.match(NUMBERED);
|
|
107
118
|
if (m) {
|
|
@@ -114,7 +125,15 @@ function numberedMenu(lines: string[], bottom: number): Menu | null {
|
|
|
114
125
|
}
|
|
115
126
|
// Deeper than an option's label: that option's explanation (it sits
|
|
116
127
|
// below its option, so it is seen first when scanning upwards).
|
|
117
|
-
|
|
128
|
+
//
|
|
129
|
+
// Keep only the first few for display, but keep SCANNING past the rest. A
|
|
130
|
+
// long explanation is not the top of the menu, and stopping at the fourth
|
|
131
|
+
// line of one truncated the menu to whatever had been collected below it.
|
|
132
|
+
// Claude Code routinely writes five- and six-line explanations.
|
|
133
|
+
if (raw.search(/\S/) >= Math.max(4, labelCol)) {
|
|
134
|
+
if (hints.length < 3) hints.unshift(raw.trim());
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
118
137
|
break;
|
|
119
138
|
}
|
|
120
139
|
if (opts.length < 2) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indianic/dialout",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.2",
|
|
4
4
|
"description": "Dialout agent — reach your office, home or build machine from any browser or phone. Remote tmux terminals, live port checks, filesystem browsing, process control and HTTP tunnels. The agent dials out: no inbound ports, no VPN, no port forwarding.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dialout": "./bin/devdash-agent.js"
|