@0xmaxma/claude-gateway 1.8.2 → 1.8.4
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/README.md +63 -2
- package/config.template.json +4 -0
- package/dist/agent/dreaming/config.d.ts +6 -0
- package/dist/agent/dreaming/config.d.ts.map +1 -1
- package/dist/agent/dreaming/config.js +17 -24
- package/dist/agent/dreaming/config.js.map +1 -1
- package/dist/agent/knowledge/config.d.ts.map +1 -1
- package/dist/agent/knowledge/config.js +7 -13
- package/dist/agent/knowledge/config.js.map +1 -1
- package/dist/agent/runner.d.ts +87 -13
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +342 -117
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/turn-stream.d.ts +202 -0
- package/dist/agent/turn-stream.d.ts.map +1 -0
- package/dist/agent/turn-stream.js +322 -0
- package/dist/agent/turn-stream.js.map +1 -0
- package/dist/api/apps-router.d.ts.map +1 -1
- package/dist/api/apps-router.js +14 -2
- package/dist/api/apps-router.js.map +1 -1
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +2 -3
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-webhook-router.d.ts +43 -1
- package/dist/api/line-webhook-router.d.ts.map +1 -1
- package/dist/api/line-webhook-router.js +286 -38
- package/dist/api/line-webhook-router.js.map +1 -1
- package/dist/api/router.d.ts +7 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +328 -114
- package/dist/api/router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +4 -1
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/apps/installer.d.ts +75 -2
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +198 -13
- package/dist/apps/installer.js.map +1 -1
- package/dist/cli/commands/debug-bundle.d.ts.map +1 -1
- package/dist/cli/commands/debug-bundle.js +9 -52
- package/dist/cli/commands/debug-bundle.js.map +1 -1
- package/dist/cli/commands/gateway.d.ts +6 -1
- package/dist/cli/commands/gateway.d.ts.map +1 -1
- package/dist/cli/commands/gateway.js +14 -3
- package/dist/cli/commands/gateway.js.map +1 -1
- package/dist/cli/commands/logs.d.ts +37 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +356 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +16 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs-dir.d.ts +69 -0
- package/dist/cli/logs-dir.d.ts.map +1 -0
- package/dist/cli/logs-dir.js +166 -0
- package/dist/cli/logs-dir.js.map +1 -0
- package/dist/config/agent-env.d.ts +41 -0
- package/dist/config/agent-env.d.ts.map +1 -0
- package/dist/config/agent-env.js +154 -0
- package/dist/config/agent-env.js.map +1 -0
- package/dist/config/loader.d.ts +33 -2
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +33 -9
- package/dist/config/loader.js.map +1 -1
- package/dist/config/watcher.d.ts +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +22 -1
- package/dist/config/watcher.js.map +1 -1
- package/dist/index.js +50 -51
- package/dist/index.js.map +1 -1
- package/dist/load-dotenv.d.ts +17 -0
- package/dist/load-dotenv.d.ts.map +1 -1
- package/dist/load-dotenv.js +32 -9
- package/dist/load-dotenv.js.map +1 -1
- package/dist/logger.d.ts +52 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +264 -2
- package/dist/logger.js.map +1 -1
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +12 -0
- package/dist/session/store.js.map +1 -1
- package/dist/shell/bypass-dialog.d.ts +234 -0
- package/dist/shell/bypass-dialog.d.ts.map +1 -0
- package/dist/shell/bypass-dialog.js +339 -0
- package/dist/shell/bypass-dialog.js.map +1 -0
- package/dist/shell/claude-pty-shell.js +67 -3
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/screen.d.ts +22 -11
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +50 -32
- package/dist/shell/screen.js.map +1 -1
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config-num.d.ts +22 -0
- package/dist/utils/config-num.d.ts.map +1 -0
- package/dist/utils/config-num.js +34 -0
- package/dist/utils/config-num.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision logic for accepting Claude Code's "Bypass Permissions mode" dialog.
|
|
3
|
+
*
|
|
4
|
+
* The wrapper always injects --dangerously-skip-permissions, so the resulting
|
|
5
|
+
* confirmation modal is accepted on the operator's behalf. How to accept it
|
|
6
|
+
* depends on the Claude Code build, because the dialog's rendering changed:
|
|
7
|
+
*
|
|
8
|
+
* Claude Code <= 2.1.247 Claude Code >= 2.1.248
|
|
9
|
+
* ❯ 1. No, exit ❯ No, exit
|
|
10
|
+
* 2. Yes, I accept Yes, I accept
|
|
11
|
+
*
|
|
12
|
+
* The old shape is a numbered select — typing the digit picks *and* confirms
|
|
13
|
+
* the row in one keystroke. The new shape has no indexes, so a digit selects
|
|
14
|
+
* nothing at all: the dialog stays up, gets re-detected every
|
|
15
|
+
* DIALOG_ACTION_COOLDOWN_MS, and the session wedges until the watchdog kills
|
|
16
|
+
* it (issue #431). Both shapes start with the caret on "No, exit".
|
|
17
|
+
*
|
|
18
|
+
* Both renderings are still in the wild, so this decides per screen rather
|
|
19
|
+
* than per version — there is no version string to read from inside the PTY
|
|
20
|
+
* anyway, and the screen is the thing that actually has to be driven.
|
|
21
|
+
*
|
|
22
|
+
* SAFETY: choosing "No, exit" terminates Claude Code, which is not
|
|
23
|
+
* recoverable, while leaving the dialog up merely means an operator sees it.
|
|
24
|
+
* So Enter is only ever sent once the caret is *observed* on the accept row,
|
|
25
|
+
* the digit is read off the accept row instead of hard-coded (a future
|
|
26
|
+
* reordering must not send us to "No, exit"), and anything unparseable or
|
|
27
|
+
* ambiguous produces no keystroke at all.
|
|
28
|
+
*
|
|
29
|
+
* This module is the pure detection + decision — kept free of node-pty / screen
|
|
30
|
+
* imports so it is cheap to unit-test in isolation, same pattern as
|
|
31
|
+
* menu-probe.ts's decideProbeAttempt and menu-cancel.ts's decideMenuCancel.
|
|
32
|
+
* ScreenModel.detectDialog() calls isBypassDialogOnScreen() below; see
|
|
33
|
+
* Driver.maybeHandleDialog() in claude-pty-shell.ts for where it is wired in.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The dialog's two option labels. BYPASS_ACCEPT_LABEL is the same string
|
|
37
|
+
* screen.ts lists in TUI_BYPASS_PERMS as a detection marker; it is repeated
|
|
38
|
+
* here rather than imported to keep this module dependency-free, and a unit
|
|
39
|
+
* test asserts the two never drift apart (screen.ts: "all matchers live here
|
|
40
|
+
* so a UI change requires touching exactly one file").
|
|
41
|
+
*/
|
|
42
|
+
export declare const BYPASS_ACCEPT_LABEL = "Yes, I accept";
|
|
43
|
+
export declare const BYPASS_DECLINE_LABEL = "No, exit";
|
|
44
|
+
/**
|
|
45
|
+
* The dialog's own confirm affordance, rendered a line or two below the option
|
|
46
|
+
* rows ("Enter to confirm · Esc to cancel"). Only the stable prefix is matched.
|
|
47
|
+
*
|
|
48
|
+
* Required by {@link isBypassDialogOnScreen} for the same reason
|
|
49
|
+
* TUI_REQUEST_TOO_LARGE_DISMISS is required by detectRequestTooLarge(): the
|
|
50
|
+
* labels alone appear in ordinary prose (this very dialog's warning text, a
|
|
51
|
+
* chat reply explaining it, re-injected history quoting it), whereas the
|
|
52
|
+
* footer is the affordance the live overlay renders — and it is exactly the
|
|
53
|
+
* affordance the accepting keystroke relies on, so gating on it keeps
|
|
54
|
+
* detection and the action consistent.
|
|
55
|
+
*/
|
|
56
|
+
export declare const BYPASS_CONFIRM_FOOTER = "Enter to confirm";
|
|
57
|
+
/**
|
|
58
|
+
* The dialog's heading — the same string screen.ts lists first in
|
|
59
|
+
* TUI_BYPASS_PERMS, repeated here for the dependency-free reason above, with a
|
|
60
|
+
* unit test asserting the two never drift apart.
|
|
61
|
+
*
|
|
62
|
+
* Required by {@link isBypassDialogOnScreen} so that predicate is the COMPLETE
|
|
63
|
+
* rule rather than half of one split across two files. See the note there.
|
|
64
|
+
*/
|
|
65
|
+
export declare const BYPASS_HEADING = "Bypass Permissions mode";
|
|
66
|
+
/**
|
|
67
|
+
* How far apart the dialog's own rows may sit before they stop being one block.
|
|
68
|
+
*
|
|
69
|
+
* The real capture has the two options on adjacent rows and the footer two rows
|
|
70
|
+
* below them (one blank row between). These bounds allow a little repaint slack
|
|
71
|
+
* while still requiring the elements to be visually together: without them the
|
|
72
|
+
* "structure" was only an ordering, so an option row, a second option row 15
|
|
73
|
+
* lines further down and any sentence containing "Enter to confirm" below that
|
|
74
|
+
* satisfied it — on a screen of ordinary conversation (review round 2, M3).
|
|
75
|
+
*/
|
|
76
|
+
export declare const BYPASS_MAX_ROW_GAP = 2;
|
|
77
|
+
export declare const BYPASS_MAX_FOOTER_GAP = 3;
|
|
78
|
+
/** Arrow keystrokes used to walk the caret onto the accept row. */
|
|
79
|
+
export declare const BYPASS_KEY_DOWN = "\u001B[B";
|
|
80
|
+
export declare const BYPASS_KEY_UP = "\u001B[A";
|
|
81
|
+
/** Confirms the highlighted row ("Enter to confirm" per the dialog's own footer). */
|
|
82
|
+
export declare const BYPASS_KEY_ENTER = "\r";
|
|
83
|
+
/**
|
|
84
|
+
* Ceiling on keystrokes sent per dialog — arrow moves plus the accepting key
|
|
85
|
+
* itself. A healthy dialog needs at most two (one move, one Enter); the
|
|
86
|
+
* ceiling exists so a dialog that renders but never responds cannot draw
|
|
87
|
+
* *unbounded* key traffic, because those keys are not discarded by an
|
|
88
|
+
* unresponsive TUI — they queue and land in the prompt once it opens.
|
|
89
|
+
*
|
|
90
|
+
* Sized to the startup window rather than to the happy path. maybeHandleDialog()
|
|
91
|
+
* only runs while the shell is not ready, so the caller can make at most
|
|
92
|
+
* STARTUP_TIMEOUT_MS / DIALOG_ACTION_COOLDOWN_MS = 120000/2000 = 60 attempts
|
|
93
|
+
* before the startup timeout ends the process anyway; a test pins that
|
|
94
|
+
* arithmetic against the driver's own constants. A tighter ceiling would go
|
|
95
|
+
* silent partway through that window, so a dialog that merely swallowed its
|
|
96
|
+
* first keystrokes (still rendering, key handler not yet attached) would never
|
|
97
|
+
* be retried and would die at the startup timeout into a respawn loop — the
|
|
98
|
+
* exact #431 symptom. Overspending is cheap by comparison: an unresponsive TUI
|
|
99
|
+
* only ever collects arrows and digits here (Enter is sent solely after the
|
|
100
|
+
* caret is *observed* to have moved, which an unresponsive TUI never does), so
|
|
101
|
+
* the worst case is junk characters in an input draft the driver already
|
|
102
|
+
* clears, never a submitted line.
|
|
103
|
+
*
|
|
104
|
+
* Exhausting the ceiling falls back to 'wait' — the fail-safe visible dialog.
|
|
105
|
+
*/
|
|
106
|
+
export declare const BYPASS_MAX_KEYS = 60;
|
|
107
|
+
/**
|
|
108
|
+
* Consecutive rounds with no dialog on screen before the keystroke ceiling is
|
|
109
|
+
* considered spent on a dialog that is gone.
|
|
110
|
+
*
|
|
111
|
+
* Not 1: detection is structural (see isBypassDialogOnScreen), so a mid-repaint
|
|
112
|
+
* frame — the option rows drawn but the footer not yet, or the caret momentarily
|
|
113
|
+
* on neither row — reads as "no dialog" for a single round while the dialog is
|
|
114
|
+
* still very much up. Resetting on one miss would zero the counter mid-dialog —
|
|
115
|
+
* precisely the case the ceiling exists for. Requiring sustained absence costs
|
|
116
|
+
* the next dialog one extra cooldown round at most.
|
|
117
|
+
*/
|
|
118
|
+
export declare const BYPASS_RESET_AFTER_MISSES = 2;
|
|
119
|
+
export type BypassDialogAction =
|
|
120
|
+
/** Numbered rendering: type this digit, which selects and confirms in one key. */
|
|
121
|
+
{
|
|
122
|
+
kind: 'digit';
|
|
123
|
+
key: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Un-numbered rendering: the caret is on the decline row, so step it onto
|
|
127
|
+
* the accept row. Exactly one step — see decideBypassDialogAction(); a caret
|
|
128
|
+
* anywhere other than the decline row is ambiguous and yields 'wait'.
|
|
129
|
+
*/
|
|
130
|
+
| {
|
|
131
|
+
kind: 'move';
|
|
132
|
+
key: string;
|
|
133
|
+
}
|
|
134
|
+
/** Caret is on the accept row — safe to confirm. */
|
|
135
|
+
| {
|
|
136
|
+
kind: 'confirm';
|
|
137
|
+
key: string;
|
|
138
|
+
}
|
|
139
|
+
/** Nothing safe to do this round; `reason` is for the log. */
|
|
140
|
+
| {
|
|
141
|
+
kind: 'wait';
|
|
142
|
+
reason: string;
|
|
143
|
+
};
|
|
144
|
+
/** Per-dialog bookkeeping. Reset once the dialog has left the screen. */
|
|
145
|
+
export interface BypassDialogState {
|
|
146
|
+
/** Keystrokes already sent for the current dialog. */
|
|
147
|
+
keys: number;
|
|
148
|
+
/** Consecutive rounds the dialog was not detected — see noteDialogAbsent(). */
|
|
149
|
+
misses: number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Record a round in which no dialog was detected, clearing the keystroke count
|
|
153
|
+
* once the dialog has been absent for BYPASS_RESET_AFTER_MISSES consecutive
|
|
154
|
+
* rounds so the next dialog starts with a full allowance. Tolerating a single
|
|
155
|
+
* miss keeps a one-round detection flicker (a repaint catching the dialog
|
|
156
|
+
* half-drawn) from silently refilling the allowance mid-dialog.
|
|
157
|
+
*/
|
|
158
|
+
export declare function noteDialogAbsent(state: BypassDialogState): void;
|
|
159
|
+
/** Record a round in which the dialog *was* detected, restarting the miss run. */
|
|
160
|
+
export declare function noteDialogPresent(state: BypassDialogState): void;
|
|
161
|
+
/** Exported only so a unit test can prove the escaping above. */
|
|
162
|
+
export declare function rowPattern(label: string): RegExp;
|
|
163
|
+
/**
|
|
164
|
+
* Is a live, drivable bypass-permissions dialog on this screen?
|
|
165
|
+
*
|
|
166
|
+
* This replaced a positional test — "both marker substrings inside the bottom
|
|
167
|
+
* 20 rows" — which assumed a modal is always anchored to the bottom. This
|
|
168
|
+
* dialog is not: it renders at boot, before there is any conversation to push
|
|
169
|
+
* it down, so on a clean start it sits at the TOP of the screen with the rest
|
|
170
|
+
* blank. Both markers then fall outside the window, detection returns null,
|
|
171
|
+
* nothing is pressed, and the session dies at the 120 s startup timeout and
|
|
172
|
+
* respawns into the same dialog (issue #436). Whether a given boot emitted
|
|
173
|
+
* enough output to push the dialog into the window is what made the wedge look
|
|
174
|
+
* random.
|
|
175
|
+
*
|
|
176
|
+
* The window was never really about position, though — it was a cheap proxy for
|
|
177
|
+
* "this is the live modal, not text that quotes it". Quoted text is a genuine
|
|
178
|
+
* hazard: an agent explaining this dialog, or re-injected conversation history,
|
|
179
|
+
* puts the same characters on the same screen, and a mis-aimed Enter can land on
|
|
180
|
+
* "No, exit" and kill Claude Code unrecoverably. So the proxy is replaced with
|
|
181
|
+
* the thing it was proxying for — structural properties, all position-
|
|
182
|
+
* independent, all read off a verbatim capture of the real dialog:
|
|
183
|
+
*
|
|
184
|
+
* 1. The dialog's heading is somewhere on screen. Cheap substring reject.
|
|
185
|
+
* 2. Both option labels occupy a WHOLE row (findRow's anchored patterns), so
|
|
186
|
+
* prose that mentions them mid-sentence never qualifies.
|
|
187
|
+
* 3. Exactly ONE of those two rows carries the ❯ caret. Zero means the frame
|
|
188
|
+
* is still rendering (or is not a live select); two is a shape we do not
|
|
189
|
+
* understand.
|
|
190
|
+
* 4. The rows and the footer form one BLOCK — the options within
|
|
191
|
+
* BYPASS_MAX_ROW_GAP rows of each other, the footer within
|
|
192
|
+
* BYPASS_MAX_FOOTER_GAP below them. Without this the three elements only
|
|
193
|
+
* had to appear in order, so an option row, an unrelated option row 15
|
|
194
|
+
* lines later and a sentence containing "Enter to confirm" further down
|
|
195
|
+
* still qualified (review round 2, finding M3).
|
|
196
|
+
* 5. The dialog's own confirm footer renders BELOW the option rows.
|
|
197
|
+
* 6. Nothing but blank rows renders below that footer.
|
|
198
|
+
*
|
|
199
|
+
* (6) is the load-bearing one, and it is what the old bottom-region test was
|
|
200
|
+
* actually encoding: a live modal owns the screen. Quoted text never can — the
|
|
201
|
+
* input box, its border and the status bar always render underneath it — so
|
|
202
|
+
* scrollback, a pasted dialog, and re-injected history all fail (6) no matter
|
|
203
|
+
* where on the screen they land.
|
|
204
|
+
*
|
|
205
|
+
* (6) is deliberately ZERO-tolerance: not even a box border may follow the
|
|
206
|
+
* footer. A future Claude Code that draws this dialog inside a box, or paints a
|
|
207
|
+
* status line beneath it, would therefore stop being detected. That is the
|
|
208
|
+
* intended direction of the trade — a miss is fail-safe (the operator sees the
|
|
209
|
+
* dialog, and maybeHandleDialog() now logs a warning naming this exact case),
|
|
210
|
+
* whereas relaxing (6) to tolerate border rows would admit a quoted dialog
|
|
211
|
+
* sitting above an EMPTY input box, whose rows are themselves nothing but
|
|
212
|
+
* border characters. Given "No, exit" is unrecoverable and a visible dialog is
|
|
213
|
+
* not, a miss beats a false accept (review round 2, finding M2 — accepted as
|
|
214
|
+
* accurate, resolved this way rather than by relaxing the rule).
|
|
215
|
+
*
|
|
216
|
+
* Fail-safe in the same direction as before: anything unrecognised reads as "no
|
|
217
|
+
* dialog", which means no keystroke and an operator who simply sees the dialog.
|
|
218
|
+
*/
|
|
219
|
+
export declare function isBypassDialogOnScreen(screenText: string): boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Decide the single keystroke to send at a bypass-permissions dialog, given
|
|
222
|
+
* the screen text the dialog was detected on.
|
|
223
|
+
*
|
|
224
|
+
* Callers send at most one key per call and re-read the screen before the
|
|
225
|
+
* next one (the DIALOG_ACTION_COOLDOWN_MS re-entry in maybeHandleDialog()),
|
|
226
|
+
* so multi-step acceptance is a screen-driven state machine rather than a
|
|
227
|
+
* blind key sequence: every keystroke is justified by what is on screen at
|
|
228
|
+
* the moment it is sent.
|
|
229
|
+
*
|
|
230
|
+
* `state.keys` is only read here; the caller advances it for every action that
|
|
231
|
+
* actually sends a key.
|
|
232
|
+
*/
|
|
233
|
+
export declare function decideBypassDialogAction(dialogText: string, state: BypassDialogState): BypassDialogAction;
|
|
234
|
+
//# sourceMappingURL=bypass-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bypass-dialog.d.ts","sourceRoot":"","sources":["../../src/shell/bypass-dialog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,oBAAoB,aAAa,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,mEAAmE;AACnE,eAAO,MAAM,eAAe,aAAW,CAAC;AACxC,eAAO,MAAM,aAAa,aAAW,CAAC;AACtC,qFAAqF;AACrF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,MAAM,kBAAkB;AAC5B,kFAAkF;AAChF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAChC;;;;GAIG;GACD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAC/B,oDAAoD;GAClD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAClC,8DAA8D;GAC5D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAG/D;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAEhE;AAmCD,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAuBlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,GACvB,kBAAkB,CA4DpB"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Decision logic for accepting Claude Code's "Bypass Permissions mode" dialog.
|
|
4
|
+
*
|
|
5
|
+
* The wrapper always injects --dangerously-skip-permissions, so the resulting
|
|
6
|
+
* confirmation modal is accepted on the operator's behalf. How to accept it
|
|
7
|
+
* depends on the Claude Code build, because the dialog's rendering changed:
|
|
8
|
+
*
|
|
9
|
+
* Claude Code <= 2.1.247 Claude Code >= 2.1.248
|
|
10
|
+
* ❯ 1. No, exit ❯ No, exit
|
|
11
|
+
* 2. Yes, I accept Yes, I accept
|
|
12
|
+
*
|
|
13
|
+
* The old shape is a numbered select — typing the digit picks *and* confirms
|
|
14
|
+
* the row in one keystroke. The new shape has no indexes, so a digit selects
|
|
15
|
+
* nothing at all: the dialog stays up, gets re-detected every
|
|
16
|
+
* DIALOG_ACTION_COOLDOWN_MS, and the session wedges until the watchdog kills
|
|
17
|
+
* it (issue #431). Both shapes start with the caret on "No, exit".
|
|
18
|
+
*
|
|
19
|
+
* Both renderings are still in the wild, so this decides per screen rather
|
|
20
|
+
* than per version — there is no version string to read from inside the PTY
|
|
21
|
+
* anyway, and the screen is the thing that actually has to be driven.
|
|
22
|
+
*
|
|
23
|
+
* SAFETY: choosing "No, exit" terminates Claude Code, which is not
|
|
24
|
+
* recoverable, while leaving the dialog up merely means an operator sees it.
|
|
25
|
+
* So Enter is only ever sent once the caret is *observed* on the accept row,
|
|
26
|
+
* the digit is read off the accept row instead of hard-coded (a future
|
|
27
|
+
* reordering must not send us to "No, exit"), and anything unparseable or
|
|
28
|
+
* ambiguous produces no keystroke at all.
|
|
29
|
+
*
|
|
30
|
+
* This module is the pure detection + decision — kept free of node-pty / screen
|
|
31
|
+
* imports so it is cheap to unit-test in isolation, same pattern as
|
|
32
|
+
* menu-probe.ts's decideProbeAttempt and menu-cancel.ts's decideMenuCancel.
|
|
33
|
+
* ScreenModel.detectDialog() calls isBypassDialogOnScreen() below; see
|
|
34
|
+
* Driver.maybeHandleDialog() in claude-pty-shell.ts for where it is wired in.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.BYPASS_RESET_AFTER_MISSES = exports.BYPASS_MAX_KEYS = exports.BYPASS_KEY_ENTER = exports.BYPASS_KEY_UP = exports.BYPASS_KEY_DOWN = exports.BYPASS_MAX_FOOTER_GAP = exports.BYPASS_MAX_ROW_GAP = exports.BYPASS_HEADING = exports.BYPASS_CONFIRM_FOOTER = exports.BYPASS_DECLINE_LABEL = exports.BYPASS_ACCEPT_LABEL = void 0;
|
|
38
|
+
exports.noteDialogAbsent = noteDialogAbsent;
|
|
39
|
+
exports.noteDialogPresent = noteDialogPresent;
|
|
40
|
+
exports.rowPattern = rowPattern;
|
|
41
|
+
exports.isBypassDialogOnScreen = isBypassDialogOnScreen;
|
|
42
|
+
exports.decideBypassDialogAction = decideBypassDialogAction;
|
|
43
|
+
/**
|
|
44
|
+
* The dialog's two option labels. BYPASS_ACCEPT_LABEL is the same string
|
|
45
|
+
* screen.ts lists in TUI_BYPASS_PERMS as a detection marker; it is repeated
|
|
46
|
+
* here rather than imported to keep this module dependency-free, and a unit
|
|
47
|
+
* test asserts the two never drift apart (screen.ts: "all matchers live here
|
|
48
|
+
* so a UI change requires touching exactly one file").
|
|
49
|
+
*/
|
|
50
|
+
exports.BYPASS_ACCEPT_LABEL = 'Yes, I accept';
|
|
51
|
+
exports.BYPASS_DECLINE_LABEL = 'No, exit';
|
|
52
|
+
/**
|
|
53
|
+
* The dialog's own confirm affordance, rendered a line or two below the option
|
|
54
|
+
* rows ("Enter to confirm · Esc to cancel"). Only the stable prefix is matched.
|
|
55
|
+
*
|
|
56
|
+
* Required by {@link isBypassDialogOnScreen} for the same reason
|
|
57
|
+
* TUI_REQUEST_TOO_LARGE_DISMISS is required by detectRequestTooLarge(): the
|
|
58
|
+
* labels alone appear in ordinary prose (this very dialog's warning text, a
|
|
59
|
+
* chat reply explaining it, re-injected history quoting it), whereas the
|
|
60
|
+
* footer is the affordance the live overlay renders — and it is exactly the
|
|
61
|
+
* affordance the accepting keystroke relies on, so gating on it keeps
|
|
62
|
+
* detection and the action consistent.
|
|
63
|
+
*/
|
|
64
|
+
exports.BYPASS_CONFIRM_FOOTER = 'Enter to confirm';
|
|
65
|
+
/**
|
|
66
|
+
* The dialog's heading — the same string screen.ts lists first in
|
|
67
|
+
* TUI_BYPASS_PERMS, repeated here for the dependency-free reason above, with a
|
|
68
|
+
* unit test asserting the two never drift apart.
|
|
69
|
+
*
|
|
70
|
+
* Required by {@link isBypassDialogOnScreen} so that predicate is the COMPLETE
|
|
71
|
+
* rule rather than half of one split across two files. See the note there.
|
|
72
|
+
*/
|
|
73
|
+
exports.BYPASS_HEADING = 'Bypass Permissions mode';
|
|
74
|
+
/**
|
|
75
|
+
* How far apart the dialog's own rows may sit before they stop being one block.
|
|
76
|
+
*
|
|
77
|
+
* The real capture has the two options on adjacent rows and the footer two rows
|
|
78
|
+
* below them (one blank row between). These bounds allow a little repaint slack
|
|
79
|
+
* while still requiring the elements to be visually together: without them the
|
|
80
|
+
* "structure" was only an ordering, so an option row, a second option row 15
|
|
81
|
+
* lines further down and any sentence containing "Enter to confirm" below that
|
|
82
|
+
* satisfied it — on a screen of ordinary conversation (review round 2, M3).
|
|
83
|
+
*/
|
|
84
|
+
exports.BYPASS_MAX_ROW_GAP = 2;
|
|
85
|
+
exports.BYPASS_MAX_FOOTER_GAP = 3;
|
|
86
|
+
/** Arrow keystrokes used to walk the caret onto the accept row. */
|
|
87
|
+
exports.BYPASS_KEY_DOWN = '\x1b[B';
|
|
88
|
+
exports.BYPASS_KEY_UP = '\x1b[A';
|
|
89
|
+
/** Confirms the highlighted row ("Enter to confirm" per the dialog's own footer). */
|
|
90
|
+
exports.BYPASS_KEY_ENTER = '\r';
|
|
91
|
+
/**
|
|
92
|
+
* Ceiling on keystrokes sent per dialog — arrow moves plus the accepting key
|
|
93
|
+
* itself. A healthy dialog needs at most two (one move, one Enter); the
|
|
94
|
+
* ceiling exists so a dialog that renders but never responds cannot draw
|
|
95
|
+
* *unbounded* key traffic, because those keys are not discarded by an
|
|
96
|
+
* unresponsive TUI — they queue and land in the prompt once it opens.
|
|
97
|
+
*
|
|
98
|
+
* Sized to the startup window rather than to the happy path. maybeHandleDialog()
|
|
99
|
+
* only runs while the shell is not ready, so the caller can make at most
|
|
100
|
+
* STARTUP_TIMEOUT_MS / DIALOG_ACTION_COOLDOWN_MS = 120000/2000 = 60 attempts
|
|
101
|
+
* before the startup timeout ends the process anyway; a test pins that
|
|
102
|
+
* arithmetic against the driver's own constants. A tighter ceiling would go
|
|
103
|
+
* silent partway through that window, so a dialog that merely swallowed its
|
|
104
|
+
* first keystrokes (still rendering, key handler not yet attached) would never
|
|
105
|
+
* be retried and would die at the startup timeout into a respawn loop — the
|
|
106
|
+
* exact #431 symptom. Overspending is cheap by comparison: an unresponsive TUI
|
|
107
|
+
* only ever collects arrows and digits here (Enter is sent solely after the
|
|
108
|
+
* caret is *observed* to have moved, which an unresponsive TUI never does), so
|
|
109
|
+
* the worst case is junk characters in an input draft the driver already
|
|
110
|
+
* clears, never a submitted line.
|
|
111
|
+
*
|
|
112
|
+
* Exhausting the ceiling falls back to 'wait' — the fail-safe visible dialog.
|
|
113
|
+
*/
|
|
114
|
+
exports.BYPASS_MAX_KEYS = 60;
|
|
115
|
+
/**
|
|
116
|
+
* Consecutive rounds with no dialog on screen before the keystroke ceiling is
|
|
117
|
+
* considered spent on a dialog that is gone.
|
|
118
|
+
*
|
|
119
|
+
* Not 1: detection is structural (see isBypassDialogOnScreen), so a mid-repaint
|
|
120
|
+
* frame — the option rows drawn but the footer not yet, or the caret momentarily
|
|
121
|
+
* on neither row — reads as "no dialog" for a single round while the dialog is
|
|
122
|
+
* still very much up. Resetting on one miss would zero the counter mid-dialog —
|
|
123
|
+
* precisely the case the ceiling exists for. Requiring sustained absence costs
|
|
124
|
+
* the next dialog one extra cooldown round at most.
|
|
125
|
+
*/
|
|
126
|
+
exports.BYPASS_RESET_AFTER_MISSES = 2;
|
|
127
|
+
/**
|
|
128
|
+
* Record a round in which no dialog was detected, clearing the keystroke count
|
|
129
|
+
* once the dialog has been absent for BYPASS_RESET_AFTER_MISSES consecutive
|
|
130
|
+
* rounds so the next dialog starts with a full allowance. Tolerating a single
|
|
131
|
+
* miss keeps a one-round detection flicker (a repaint catching the dialog
|
|
132
|
+
* half-drawn) from silently refilling the allowance mid-dialog.
|
|
133
|
+
*/
|
|
134
|
+
function noteDialogAbsent(state) {
|
|
135
|
+
state.misses++;
|
|
136
|
+
if (state.misses >= exports.BYPASS_RESET_AFTER_MISSES)
|
|
137
|
+
state.keys = 0;
|
|
138
|
+
}
|
|
139
|
+
/** Record a round in which the dialog *was* detected, restarting the miss run. */
|
|
140
|
+
function noteDialogPresent(state) {
|
|
141
|
+
state.misses = 0;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Match an option row: optional box border, optional ❯ caret, optional `N.`
|
|
145
|
+
* index, then the label and nothing else.
|
|
146
|
+
*
|
|
147
|
+
* Anchored at both ends on purpose. The dialog's own warning prose mentions
|
|
148
|
+
* accepting responsibility and Bypass Permissions mode, and a chat reply can
|
|
149
|
+
* quote the labels outright; requiring the label to be the *whole* row keeps
|
|
150
|
+
* those from being mistaken for a selectable option.
|
|
151
|
+
*
|
|
152
|
+
* The caret is U+276F only — never the ASCII '>' that TUI_MENU_OPTION_RE
|
|
153
|
+
* tolerates for row content — because the real TUI never renders '>' as a
|
|
154
|
+
* caret while prose (markdown blockquotes) renders it constantly.
|
|
155
|
+
*
|
|
156
|
+
* The label is escaped before interpolation. These patterns are built at module
|
|
157
|
+
* load, and the labels above are meant to be edited when the TUI changes: an
|
|
158
|
+
* unescaped label containing '(' would throw SyntaxError at import time and take
|
|
159
|
+
* every PTY session down with it, and a '.' would silently become a wildcard
|
|
160
|
+
* that widens what counts as a selectable row.
|
|
161
|
+
*/
|
|
162
|
+
function escapeForRegExp(literal) {
|
|
163
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
164
|
+
}
|
|
165
|
+
/** Exported only so a unit test can prove the escaping above. */
|
|
166
|
+
function rowPattern(label) {
|
|
167
|
+
return new RegExp(`^[\\s│]*(❯)?\\s*(?:(\\d+)\\.)?\\s*${escapeForRegExp(label)}[\\s│]*$`);
|
|
168
|
+
}
|
|
169
|
+
const ACCEPT_ROW_RE = rowPattern(exports.BYPASS_ACCEPT_LABEL);
|
|
170
|
+
const DECLINE_ROW_RE = rowPattern(exports.BYPASS_DECLINE_LABEL);
|
|
171
|
+
/**
|
|
172
|
+
* Find the option row nearest the bottom of the screen. A live modal is the
|
|
173
|
+
* bottom-most thing on screen, so scanning upward from the end means quoted
|
|
174
|
+
* scrollback above it can never be the row we act on.
|
|
175
|
+
*/
|
|
176
|
+
function findRow(lines, re) {
|
|
177
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
178
|
+
const m = re.exec(lines[i]);
|
|
179
|
+
if (m)
|
|
180
|
+
return { line: i, caret: m[1] === '❯', digit: m[2] ? Number(m[2]) : null };
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Is a live, drivable bypass-permissions dialog on this screen?
|
|
186
|
+
*
|
|
187
|
+
* This replaced a positional test — "both marker substrings inside the bottom
|
|
188
|
+
* 20 rows" — which assumed a modal is always anchored to the bottom. This
|
|
189
|
+
* dialog is not: it renders at boot, before there is any conversation to push
|
|
190
|
+
* it down, so on a clean start it sits at the TOP of the screen with the rest
|
|
191
|
+
* blank. Both markers then fall outside the window, detection returns null,
|
|
192
|
+
* nothing is pressed, and the session dies at the 120 s startup timeout and
|
|
193
|
+
* respawns into the same dialog (issue #436). Whether a given boot emitted
|
|
194
|
+
* enough output to push the dialog into the window is what made the wedge look
|
|
195
|
+
* random.
|
|
196
|
+
*
|
|
197
|
+
* The window was never really about position, though — it was a cheap proxy for
|
|
198
|
+
* "this is the live modal, not text that quotes it". Quoted text is a genuine
|
|
199
|
+
* hazard: an agent explaining this dialog, or re-injected conversation history,
|
|
200
|
+
* puts the same characters on the same screen, and a mis-aimed Enter can land on
|
|
201
|
+
* "No, exit" and kill Claude Code unrecoverably. So the proxy is replaced with
|
|
202
|
+
* the thing it was proxying for — structural properties, all position-
|
|
203
|
+
* independent, all read off a verbatim capture of the real dialog:
|
|
204
|
+
*
|
|
205
|
+
* 1. The dialog's heading is somewhere on screen. Cheap substring reject.
|
|
206
|
+
* 2. Both option labels occupy a WHOLE row (findRow's anchored patterns), so
|
|
207
|
+
* prose that mentions them mid-sentence never qualifies.
|
|
208
|
+
* 3. Exactly ONE of those two rows carries the ❯ caret. Zero means the frame
|
|
209
|
+
* is still rendering (or is not a live select); two is a shape we do not
|
|
210
|
+
* understand.
|
|
211
|
+
* 4. The rows and the footer form one BLOCK — the options within
|
|
212
|
+
* BYPASS_MAX_ROW_GAP rows of each other, the footer within
|
|
213
|
+
* BYPASS_MAX_FOOTER_GAP below them. Without this the three elements only
|
|
214
|
+
* had to appear in order, so an option row, an unrelated option row 15
|
|
215
|
+
* lines later and a sentence containing "Enter to confirm" further down
|
|
216
|
+
* still qualified (review round 2, finding M3).
|
|
217
|
+
* 5. The dialog's own confirm footer renders BELOW the option rows.
|
|
218
|
+
* 6. Nothing but blank rows renders below that footer.
|
|
219
|
+
*
|
|
220
|
+
* (6) is the load-bearing one, and it is what the old bottom-region test was
|
|
221
|
+
* actually encoding: a live modal owns the screen. Quoted text never can — the
|
|
222
|
+
* input box, its border and the status bar always render underneath it — so
|
|
223
|
+
* scrollback, a pasted dialog, and re-injected history all fail (6) no matter
|
|
224
|
+
* where on the screen they land.
|
|
225
|
+
*
|
|
226
|
+
* (6) is deliberately ZERO-tolerance: not even a box border may follow the
|
|
227
|
+
* footer. A future Claude Code that draws this dialog inside a box, or paints a
|
|
228
|
+
* status line beneath it, would therefore stop being detected. That is the
|
|
229
|
+
* intended direction of the trade — a miss is fail-safe (the operator sees the
|
|
230
|
+
* dialog, and maybeHandleDialog() now logs a warning naming this exact case),
|
|
231
|
+
* whereas relaxing (6) to tolerate border rows would admit a quoted dialog
|
|
232
|
+
* sitting above an EMPTY input box, whose rows are themselves nothing but
|
|
233
|
+
* border characters. Given "No, exit" is unrecoverable and a visible dialog is
|
|
234
|
+
* not, a miss beats a false accept (review round 2, finding M2 — accepted as
|
|
235
|
+
* accurate, resolved this way rather than by relaxing the rule).
|
|
236
|
+
*
|
|
237
|
+
* Fail-safe in the same direction as before: anything unrecognised reads as "no
|
|
238
|
+
* dialog", which means no keystroke and an operator who simply sees the dialog.
|
|
239
|
+
*/
|
|
240
|
+
function isBypassDialogOnScreen(screenText) {
|
|
241
|
+
// The heading gate lives HERE rather than in ScreenModel.detectDialog() so
|
|
242
|
+
// that this predicate is the whole rule. When detectDialog() held it, the
|
|
243
|
+
// action layer re-applied only the structural half, and a screen carrying a
|
|
244
|
+
// perfect dialog shape without the heading produced a live Enter if
|
|
245
|
+
// decideBypassDialogAction() was ever called from anywhere else — the exact
|
|
246
|
+
// drift the two-layer design claimed to prevent (review round 2, finding H1).
|
|
247
|
+
if (!screenText.includes(exports.BYPASS_HEADING))
|
|
248
|
+
return false;
|
|
249
|
+
const lines = screenText.split('\n');
|
|
250
|
+
const accept = findRow(lines, ACCEPT_ROW_RE);
|
|
251
|
+
const decline = findRow(lines, DECLINE_ROW_RE);
|
|
252
|
+
if (!accept || !decline)
|
|
253
|
+
return false;
|
|
254
|
+
// Exactly one caret: equal flags mean either none (still rendering) or both
|
|
255
|
+
// (unrecognised shape). Same rule decideBypassDialogAction() acts on.
|
|
256
|
+
if (accept.caret === decline.caret)
|
|
257
|
+
return false;
|
|
258
|
+
if (Math.abs(accept.line - decline.line) > exports.BYPASS_MAX_ROW_GAP)
|
|
259
|
+
return false;
|
|
260
|
+
const lastOptionRow = Math.max(accept.line, decline.line);
|
|
261
|
+
const footer = lines.findIndex((l, i) => i > lastOptionRow && l.includes(exports.BYPASS_CONFIRM_FOOTER));
|
|
262
|
+
if (footer === -1)
|
|
263
|
+
return false;
|
|
264
|
+
if (footer - lastOptionRow > exports.BYPASS_MAX_FOOTER_GAP)
|
|
265
|
+
return false;
|
|
266
|
+
return lines.slice(footer + 1).every((l) => l.trim() === '');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Decide the single keystroke to send at a bypass-permissions dialog, given
|
|
270
|
+
* the screen text the dialog was detected on.
|
|
271
|
+
*
|
|
272
|
+
* Callers send at most one key per call and re-read the screen before the
|
|
273
|
+
* next one (the DIALOG_ACTION_COOLDOWN_MS re-entry in maybeHandleDialog()),
|
|
274
|
+
* so multi-step acceptance is a screen-driven state machine rather than a
|
|
275
|
+
* blind key sequence: every keystroke is justified by what is on screen at
|
|
276
|
+
* the moment it is sent.
|
|
277
|
+
*
|
|
278
|
+
* `state.keys` is only read here; the caller advances it for every action that
|
|
279
|
+
* actually sends a key.
|
|
280
|
+
*/
|
|
281
|
+
function decideBypassDialogAction(dialogText, state) {
|
|
282
|
+
// Budget applies to every keystroke, not just the arrows: a dialog still on
|
|
283
|
+
// screen after this many keys is not responding to us, and the fix for #431
|
|
284
|
+
// is precisely that repeating a key at a dialog that ignores it is worse
|
|
285
|
+
// than doing nothing.
|
|
286
|
+
if (state.keys >= exports.BYPASS_MAX_KEYS) {
|
|
287
|
+
return { kind: 'wait', reason: `keystroke budget exhausted after ${state.keys} keys` };
|
|
288
|
+
}
|
|
289
|
+
// The caller only gets here after detectDialog() fired, and both now read the
|
|
290
|
+
// whole screen — so "act only on what was detected" can no longer be enforced
|
|
291
|
+
// by handing this function a narrow region. It is enforced by applying the
|
|
292
|
+
// same structural rule here instead, which also keeps the two layers from
|
|
293
|
+
// drifting apart if either is edited alone.
|
|
294
|
+
if (!isBypassDialogOnScreen(dialogText)) {
|
|
295
|
+
return { kind: 'wait', reason: 'no live bypass dialog on screen' };
|
|
296
|
+
}
|
|
297
|
+
// The checks below are reached only for a screen that already satisfies that
|
|
298
|
+
// rule, so several are belt-and-braces. They are kept deliberately: each one
|
|
299
|
+
// states an invariant this decision depends on, and a keystroke sent on a
|
|
300
|
+
// wrong assumption here can pick "No, exit" and end the session for good.
|
|
301
|
+
const lines = dialogText.split('\n');
|
|
302
|
+
const accept = findRow(lines, ACCEPT_ROW_RE);
|
|
303
|
+
if (!accept)
|
|
304
|
+
return { kind: 'wait', reason: 'accept row not parseable on screen' };
|
|
305
|
+
// Numbered rendering (<= 2.1.247): the digit selects and confirms in one
|
|
306
|
+
// keystroke, the same key the pre-#431 code sent — but read off the accept
|
|
307
|
+
// row rather than hard-coded, so a reordered dialog cannot make us type the
|
|
308
|
+
// digit that means "No, exit". A wider index is not one keystroke, and how
|
|
309
|
+
// such a dialog responds to arrows has never been observed, so it stops here
|
|
310
|
+
// rather than guessing its way toward Enter.
|
|
311
|
+
if (accept.digit !== null) {
|
|
312
|
+
if (accept.digit >= 1 && accept.digit <= 9) {
|
|
313
|
+
return { kind: 'digit', key: String(accept.digit) };
|
|
314
|
+
}
|
|
315
|
+
return { kind: 'wait', reason: `accept row index ${accept.digit} is not a single keystroke` };
|
|
316
|
+
}
|
|
317
|
+
// Un-numbered rendering (>= 2.1.248): navigate, then confirm.
|
|
318
|
+
const decline = findRow(lines, DECLINE_ROW_RE);
|
|
319
|
+
if (accept.caret && !decline?.caret)
|
|
320
|
+
return { kind: 'confirm', key: exports.BYPASS_KEY_ENTER };
|
|
321
|
+
// Not on the accept row. Only move when the caret is unambiguously on the
|
|
322
|
+
// decline row: a screen with no caret at all is still rendering (or is not
|
|
323
|
+
// the live dialog), and one with a caret on both rows is not a shape we
|
|
324
|
+
// understand. Guessing either way risks confirming "No, exit".
|
|
325
|
+
//
|
|
326
|
+
// So this is a single step between two known rows, not a search: on a dialog
|
|
327
|
+
// whose caret sits on some third row, every round yields 'wait' and the
|
|
328
|
+
// operator sees the dialog. Both observed renderings have exactly these two
|
|
329
|
+
// options, and stepping blindly toward a row we cannot see the caret on is
|
|
330
|
+
// how Enter ends up on "No, exit".
|
|
331
|
+
if (!decline?.caret || accept.caret) {
|
|
332
|
+
return { kind: 'wait', reason: 'no unambiguous caret on the dialog rows' };
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
kind: 'move',
|
|
336
|
+
key: accept.line > decline.line ? exports.BYPASS_KEY_DOWN : exports.BYPASS_KEY_UP,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=bypass-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bypass-dialog.js","sourceRoot":"","sources":["../../src/shell/bypass-dialog.ts"],"names":[],"mappings":"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"}
|