@adhdev/daemon-core 0.9.82-rc.167 → 0.9.82-rc.169
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/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +261 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +257 -19
- package/dist/index.mjs.map +1 -1
- package/dist/providers/approval-utils.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/agent-stream/poller.ts +2 -3
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/index.ts +7 -0
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +8 -0
- package/src/providers/cli-provider-instance.ts +3 -3
- package/src/providers/ide-provider-instance.ts +2 -2
- package/src/providers/spec/evaluator.ts +39 -9
- package/src/providers/spec/schema.gen.ts +4 -0
- package/src/providers/spec/schema.json +2 -1
- package/src/providers/spec/types.ts +1 -0
|
@@ -4,6 +4,10 @@ export declare function pickApprovalButton(buttons: string[] | null | undefined,
|
|
|
4
4
|
index: number;
|
|
5
5
|
label: string;
|
|
6
6
|
};
|
|
7
|
+
export declare function pickAutoApprovalButton(buttons: string[] | null | undefined): {
|
|
8
|
+
index: number;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
7
11
|
export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string;
|
|
8
12
|
/**
|
|
9
13
|
* Returns true when the given text (e.g. last assistant message content, or
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import type { SessionRegistry } from '../sessions/registry.js';
|
|
|
19
19
|
import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
|
|
20
20
|
import { LOG } from '../logging/logger.js';
|
|
21
21
|
import type { AgentStreamState } from './types.js';
|
|
22
|
-
import { formatAutoApprovalMessage,
|
|
22
|
+
import { formatAutoApprovalMessage, pickAutoApprovalButton } from '../providers/approval-utils.js';
|
|
23
23
|
import type { ProviderModule } from '../providers/contracts.js';
|
|
24
24
|
import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normalization.js';
|
|
25
25
|
|
|
@@ -210,8 +210,7 @@ export class AgentStreamPoller {
|
|
|
210
210
|
if (stream?.status === 'waiting_approval') {
|
|
211
211
|
const autoApprove = providerLoader.getSettings(stream.agentType).autoApprove !== false;
|
|
212
212
|
if (autoApprove && resolvedActiveSessionId) {
|
|
213
|
-
const
|
|
214
|
-
const { label: buttonLabel } = pickApprovalButton(stream.activeModal?.buttons, provider);
|
|
213
|
+
const { label: buttonLabel } = pickAutoApprovalButton(stream.activeModal?.buttons);
|
|
215
214
|
const approved = await agentStreamManager.resolveSessionAction(cdp, resolvedActiveSessionId, 'approve', buttonLabel);
|
|
216
215
|
if (approved) {
|
|
217
216
|
const effectId = [
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
SessionHostClient,
|
|
4
|
+
type SessionHostEndpoint,
|
|
5
|
+
type SessionHostRequest,
|
|
6
|
+
type SessionHostResponse,
|
|
7
|
+
type SessionTerminalSnapshot,
|
|
8
|
+
type SessionTerminalState,
|
|
9
|
+
} from '@adhdev/session-host-core';
|
|
10
|
+
|
|
11
|
+
type LowercaseLetter =
|
|
12
|
+
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
|
|
13
|
+
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
|
|
14
|
+
|
|
15
|
+
type FunctionKey = 'f1' | 'f2' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'f10' | 'f11' | 'f12';
|
|
16
|
+
type BaseNamedKey =
|
|
17
|
+
| 'enter' | 'escape' | 'tab' | 'backspace'
|
|
18
|
+
| 'up' | 'down' | 'left' | 'right'
|
|
19
|
+
| 'home' | 'end' | 'pageup' | 'pagedown' | 'space'
|
|
20
|
+
| FunctionKey;
|
|
21
|
+
type ShiftNamedKey = BaseNamedKey | LowercaseLetter | `ctrl+${LowercaseLetter}` | `alt+${LowercaseLetter}`;
|
|
22
|
+
|
|
23
|
+
export type NamedKey =
|
|
24
|
+
| BaseNamedKey
|
|
25
|
+
| `ctrl+${LowercaseLetter}`
|
|
26
|
+
| `alt+${LowercaseLetter}`
|
|
27
|
+
| `shift+${ShiftNamedKey}`;
|
|
28
|
+
|
|
29
|
+
const BASE_KEY_SEQUENCES: Record<BaseNamedKey, string> = {
|
|
30
|
+
enter: '\r',
|
|
31
|
+
escape: '\x1b',
|
|
32
|
+
tab: '\t',
|
|
33
|
+
backspace: '\x7f',
|
|
34
|
+
up: '\x1b[A',
|
|
35
|
+
down: '\x1b[B',
|
|
36
|
+
right: '\x1b[C',
|
|
37
|
+
left: '\x1b[D',
|
|
38
|
+
home: '\x1b[H',
|
|
39
|
+
end: '\x1b[F',
|
|
40
|
+
pageup: '\x1b[5~',
|
|
41
|
+
pagedown: '\x1b[6~',
|
|
42
|
+
space: ' ',
|
|
43
|
+
f1: '\x1bOP',
|
|
44
|
+
f2: '\x1bOQ',
|
|
45
|
+
f3: '\x1bOR',
|
|
46
|
+
f4: '\x1bOS',
|
|
47
|
+
f5: '\x1b[15~',
|
|
48
|
+
f6: '\x1b[17~',
|
|
49
|
+
f7: '\x1b[18~',
|
|
50
|
+
f8: '\x1b[19~',
|
|
51
|
+
f9: '\x1b[20~',
|
|
52
|
+
f10: '\x1b[21~',
|
|
53
|
+
f11: '\x1b[23~',
|
|
54
|
+
f12: '\x1b[24~',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const SHIFTED_CSI_KEYS: Partial<Record<BaseNamedKey, string>> = {
|
|
58
|
+
up: '\x1b[1;2A',
|
|
59
|
+
down: '\x1b[1;2B',
|
|
60
|
+
right: '\x1b[1;2C',
|
|
61
|
+
left: '\x1b[1;2D',
|
|
62
|
+
home: '\x1b[1;2H',
|
|
63
|
+
end: '\x1b[1;2F',
|
|
64
|
+
pageup: '\x1b[5;2~',
|
|
65
|
+
pagedown: '\x1b[6;2~',
|
|
66
|
+
f1: '\x1b[1;2P',
|
|
67
|
+
f2: '\x1b[1;2Q',
|
|
68
|
+
f3: '\x1b[1;2R',
|
|
69
|
+
f4: '\x1b[1;2S',
|
|
70
|
+
f5: '\x1b[15;2~',
|
|
71
|
+
f6: '\x1b[17;2~',
|
|
72
|
+
f7: '\x1b[18;2~',
|
|
73
|
+
f8: '\x1b[19;2~',
|
|
74
|
+
f9: '\x1b[20;2~',
|
|
75
|
+
f10: '\x1b[21;2~',
|
|
76
|
+
f11: '\x1b[23;2~',
|
|
77
|
+
f12: '\x1b[24;2~',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function isLowercaseLetter(value: string): value is LowercaseLetter {
|
|
81
|
+
return /^[a-z]$/.test(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function encodeControlLetter(letter: LowercaseLetter): string {
|
|
85
|
+
return String.fromCharCode(letter.charCodeAt(0) - 96);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function encodeShiftedKey(key: string): string {
|
|
89
|
+
if (isLowercaseLetter(key)) return key.toUpperCase();
|
|
90
|
+
if (key.startsWith('ctrl+') && isLowercaseLetter(key.slice(5))) {
|
|
91
|
+
return encodeControlLetter(key.slice(5) as LowercaseLetter);
|
|
92
|
+
}
|
|
93
|
+
if (key.startsWith('alt+') && isLowercaseLetter(key.slice(4))) {
|
|
94
|
+
return `\x1b${key.slice(4).toUpperCase()}`;
|
|
95
|
+
}
|
|
96
|
+
if (key === 'tab') return '\x1b[Z';
|
|
97
|
+
if (key in SHIFTED_CSI_KEYS) return SHIFTED_CSI_KEYS[key as BaseNamedKey]!;
|
|
98
|
+
if (key in BASE_KEY_SEQUENCES) return BASE_KEY_SEQUENCES[key as BaseNamedKey];
|
|
99
|
+
throw new Error(`Unsupported named key: shift+${key}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function namedKeyToAnsi(key: NamedKey | string): string {
|
|
103
|
+
const normalized = String(key || '').trim().toLowerCase();
|
|
104
|
+
if (normalized in BASE_KEY_SEQUENCES) return BASE_KEY_SEQUENCES[normalized as BaseNamedKey];
|
|
105
|
+
if (normalized.startsWith('ctrl+') && isLowercaseLetter(normalized.slice(5))) {
|
|
106
|
+
return encodeControlLetter(normalized.slice(5) as LowercaseLetter);
|
|
107
|
+
}
|
|
108
|
+
if (normalized.startsWith('alt+') && isLowercaseLetter(normalized.slice(4))) {
|
|
109
|
+
return `\x1b${normalized.slice(4)}`;
|
|
110
|
+
}
|
|
111
|
+
if (normalized.startsWith('shift+')) return encodeShiftedKey(normalized.slice(6));
|
|
112
|
+
throw new Error(`Unsupported named key: ${key}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function namedKeysToAnsi(keys: readonly (NamedKey | string)[]): string {
|
|
116
|
+
if (!Array.isArray(keys)) throw new Error('keys must be an array');
|
|
117
|
+
return keys.map(namedKeyToAnsi).join('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface RawTerminalSessionHostClient {
|
|
121
|
+
connect(): Promise<void>;
|
|
122
|
+
request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>>;
|
|
123
|
+
close(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface RawTerminalAttachmentOptions {
|
|
127
|
+
endpoint?: SessionHostEndpoint;
|
|
128
|
+
sessionId: string;
|
|
129
|
+
mode?: 'read' | 'write';
|
|
130
|
+
clientId?: string;
|
|
131
|
+
client?: RawTerminalSessionHostClient;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class RawTerminalAttachment {
|
|
135
|
+
private closed = false;
|
|
136
|
+
|
|
137
|
+
private constructor(
|
|
138
|
+
readonly sessionId: string,
|
|
139
|
+
private readonly clientId: string,
|
|
140
|
+
private readonly mode: 'read' | 'write',
|
|
141
|
+
private readonly client: RawTerminalSessionHostClient,
|
|
142
|
+
) {}
|
|
143
|
+
|
|
144
|
+
static async attach(options: RawTerminalAttachmentOptions): Promise<RawTerminalAttachment> {
|
|
145
|
+
const sessionId = String(options.sessionId || '').trim();
|
|
146
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
147
|
+
const mode = options.mode || 'read';
|
|
148
|
+
const clientId = options.clientId || `raw-terminal-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
149
|
+
const client = options.client || new SessionHostClient({ endpoint: options.endpoint });
|
|
150
|
+
await client.connect();
|
|
151
|
+
|
|
152
|
+
const attachResponse = await client.request({
|
|
153
|
+
type: 'attach_session',
|
|
154
|
+
payload: {
|
|
155
|
+
sessionId,
|
|
156
|
+
clientId,
|
|
157
|
+
clientType: 'web',
|
|
158
|
+
readOnly: mode === 'read',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
if (!attachResponse.success) {
|
|
162
|
+
await client.close().catch(() => {});
|
|
163
|
+
throw new Error(attachResponse.error || `Failed to attach terminal session ${sessionId}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (mode === 'write') {
|
|
167
|
+
const ownerResponse = await client.request({
|
|
168
|
+
type: 'acquire_write',
|
|
169
|
+
payload: {
|
|
170
|
+
sessionId,
|
|
171
|
+
clientId,
|
|
172
|
+
ownerType: 'user',
|
|
173
|
+
force: true,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (!ownerResponse.success) {
|
|
177
|
+
await client.request({
|
|
178
|
+
type: 'detach_session',
|
|
179
|
+
payload: { sessionId, clientId },
|
|
180
|
+
}).catch(() => ({ success: false }));
|
|
181
|
+
await client.close().catch(() => {});
|
|
182
|
+
throw new Error(ownerResponse.error || `Failed to acquire terminal session ${sessionId}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return new RawTerminalAttachment(sessionId, clientId, mode, client);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async readSnapshot(): Promise<SessionTerminalSnapshot> {
|
|
190
|
+
const response = await this.client.request<SessionTerminalSnapshot>({
|
|
191
|
+
type: 'get_terminal_snapshot',
|
|
192
|
+
payload: { sessionId: this.sessionId },
|
|
193
|
+
});
|
|
194
|
+
if (!response.success || !response.result) {
|
|
195
|
+
throw new Error(response.error || `Terminal screen unavailable for ${this.sessionId}`);
|
|
196
|
+
}
|
|
197
|
+
return response.result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async readScreenText(): Promise<string> {
|
|
201
|
+
return (await this.readSnapshot()).text;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async readState(): Promise<SessionTerminalState> {
|
|
205
|
+
return (await this.readSnapshot()).state;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async writeInput(text: string): Promise<void> {
|
|
209
|
+
if (this.mode !== 'write') throw new Error('Raw terminal attachment is read-only');
|
|
210
|
+
const response = await this.client.request({
|
|
211
|
+
type: 'send_input',
|
|
212
|
+
payload: {
|
|
213
|
+
sessionId: this.sessionId,
|
|
214
|
+
clientId: this.clientId,
|
|
215
|
+
data: text,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
if (!response.success) throw new Error(response.error || `Failed to write terminal input to ${this.sessionId}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async writeKeys(keys: readonly (NamedKey | string)[]): Promise<void> {
|
|
222
|
+
await this.writeInput(namedKeysToAnsi(keys));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async close(): Promise<void> {
|
|
226
|
+
if (this.closed) return;
|
|
227
|
+
this.closed = true;
|
|
228
|
+
if (this.mode === 'write') {
|
|
229
|
+
await this.client.request({
|
|
230
|
+
type: 'release_write',
|
|
231
|
+
payload: { sessionId: this.sessionId, clientId: this.clientId },
|
|
232
|
+
}).catch(() => ({ success: false }));
|
|
233
|
+
}
|
|
234
|
+
await this.client.request({
|
|
235
|
+
type: 'detach_session',
|
|
236
|
+
payload: { sessionId: this.sessionId, clientId: this.clientId },
|
|
237
|
+
}).catch(() => ({ success: false }));
|
|
238
|
+
await this.client.close().catch(() => {});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function withRawTerminalAttachment<T>(
|
|
243
|
+
options: RawTerminalAttachmentOptions,
|
|
244
|
+
operation: (attachment: RawTerminalAttachment) => Promise<T>,
|
|
245
|
+
): Promise<T> {
|
|
246
|
+
const attachment = await RawTerminalAttachment.attach(options);
|
|
247
|
+
try {
|
|
248
|
+
return await operation(attachment);
|
|
249
|
+
} finally {
|
|
250
|
+
await attachment.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -428,6 +428,13 @@ export type { CliAdapter } from './cli-adapter-types.js';
|
|
|
428
428
|
export { NodePtyTransportFactory } from './cli-adapters/pty-transport.js';
|
|
429
429
|
export type { PtyRuntimeTransport, PtyTransportFactory, PtySpawnOptions } from './cli-adapters/pty-transport.js';
|
|
430
430
|
export { SessionHostPtyTransportFactory } from './cli-adapters/session-host-transport.js';
|
|
431
|
+
export {
|
|
432
|
+
RawTerminalAttachment,
|
|
433
|
+
namedKeyToAnsi,
|
|
434
|
+
namedKeysToAnsi,
|
|
435
|
+
withRawTerminalAttachment,
|
|
436
|
+
} from './cli-adapters/raw-terminal-io.js';
|
|
437
|
+
export type { NamedKey, RawTerminalAttachmentOptions, RawTerminalSessionHostClient } from './cli-adapters/raw-terminal-io.js';
|
|
431
438
|
export type { HostedCliRuntimeDescriptor, CliTransportFactoryParams } from './commands/cli-manager.js';
|
|
432
439
|
export {
|
|
433
440
|
DEFAULT_SESSION_HOST_APP_NAME,
|
|
@@ -4,4 +4,8 @@ export declare function pickApprovalButton(buttons: string[] | null | undefined,
|
|
|
4
4
|
index: number;
|
|
5
5
|
label: string;
|
|
6
6
|
};
|
|
7
|
+
export declare function pickAutoApprovalButton(buttons: string[] | null | undefined): {
|
|
8
|
+
index: number;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
7
11
|
export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string;
|
|
@@ -66,6 +66,14 @@ export function pickApprovalButton(
|
|
|
66
66
|
return { index: -1, label: '' };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function pickAutoApprovalButton(
|
|
70
|
+
buttons: string[] | null | undefined,
|
|
71
|
+
): { index: number; label: string } {
|
|
72
|
+
const labels = (buttons || []).map((button) => String(button || '').trim());
|
|
73
|
+
const index = labels.findIndex(Boolean);
|
|
74
|
+
return index >= 0 ? { index, label: labels[index] } : { index: -1, label: '' };
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {
|
|
70
78
|
const lines = [`Auto-approved${buttonLabel ? `: ${buttonLabel}` : ''}`];
|
|
71
79
|
const cleanMessage = String(modalMessage || '').trim();
|
|
@@ -22,7 +22,7 @@ import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderN
|
|
|
22
22
|
import { LOG } from '../logging/logger.js';
|
|
23
23
|
import type { ChatMessage } from '../types.js';
|
|
24
24
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
25
|
-
import { formatAutoApprovalMessage, pickApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
25
|
+
import { formatAutoApprovalMessage, pickApprovalButton, pickAutoApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
26
26
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
27
27
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
28
28
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
@@ -1092,9 +1092,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1092
1092
|
if (!modal || buttons.length === 0) {
|
|
1093
1093
|
return autoApproveActive;
|
|
1094
1094
|
}
|
|
1095
|
-
const { index: buttonIndex, label: buttonLabel } =
|
|
1095
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1096
1096
|
if (buttonIndex < 0) {
|
|
1097
|
-
// No
|
|
1097
|
+
// No concrete button matched — don't pick a random index, just
|
|
1098
1098
|
// surface the modal so the user can decide.
|
|
1099
1099
|
return autoApproveActive;
|
|
1100
1100
|
}
|
|
@@ -20,7 +20,7 @@ import { LOG } from '../logging/logger.js';
|
|
|
20
20
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
21
21
|
import { validateReadChatResultPayload } from './read-chat-contract.js';
|
|
22
22
|
import type { ChatMessage } from '../types.js';
|
|
23
|
-
import { formatAutoApprovalMessage,
|
|
23
|
+
import { formatAutoApprovalMessage, pickAutoApprovalButton } from './approval-utils.js';
|
|
24
24
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
25
25
|
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
26
26
|
import { getProviderSessionCapabilities, IDE_PROVIDER_SESSION_CAPABILITIES_BASE } from './open-panel-support.js';
|
|
@@ -708,7 +708,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
708
708
|
|
|
709
709
|
this.autoApproveBusy = true;
|
|
710
710
|
try {
|
|
711
|
-
const { label: targetButton } =
|
|
711
|
+
const { label: targetButton } = pickAutoApprovalButton(_chatData?.activeModal?.buttons);
|
|
712
712
|
|
|
713
713
|
const script = scriptFn({ action: 'approve', button: targetButton, buttonText: targetButton });
|
|
714
714
|
if (!script) return;
|
|
@@ -125,6 +125,11 @@ function compilePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
|
125
125
|
return new RegExp(ref.pattern, flags.includes('g') ? flags : flags + 'g');
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
129
|
+
const flags = (ref.flags ?? 'm').replace(/g/g, '');
|
|
130
|
+
return new RegExp(ref.pattern, flags);
|
|
131
|
+
}
|
|
132
|
+
|
|
128
133
|
// ────────────────────────────────────────────────────────────────────────────
|
|
129
134
|
// State matching
|
|
130
135
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -162,16 +167,41 @@ function extractModal(
|
|
|
162
167
|
): ModalSnapshot | null {
|
|
163
168
|
if (!state.modal_buttons) return null;
|
|
164
169
|
const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
|
|
165
|
-
const re = compilePattern(state.modal_buttons);
|
|
166
170
|
const buttons: { index: number; label: string; key: string }[] = [];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
if (state.modal_buttons.continuation_lines) {
|
|
172
|
+
const re = compileLinePattern(state.modal_buttons);
|
|
173
|
+
const lines = hay.split('\n');
|
|
174
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
175
|
+
const m = re.exec(lines[i]);
|
|
176
|
+
if (!m) continue;
|
|
177
|
+
const idx = Number(m[1]);
|
|
178
|
+
let label = String(m[2] ?? '').trim();
|
|
179
|
+
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
180
|
+
let j = i + 1;
|
|
181
|
+
while (j < lines.length) {
|
|
182
|
+
const next = lines[j];
|
|
183
|
+
if (!next.trim()) break;
|
|
184
|
+
if (re.test(next)) break;
|
|
185
|
+
if (!/^\s+/.test(next)) break;
|
|
186
|
+
label += ' ' + next.trim();
|
|
187
|
+
j += 1;
|
|
188
|
+
}
|
|
189
|
+
if (buttons.some(b => b.index === idx)) continue;
|
|
190
|
+
const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
|
|
191
|
+
buttons.push({ index: idx, label, key });
|
|
192
|
+
i = j - 1;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const re = compilePattern(state.modal_buttons);
|
|
196
|
+
let m: RegExpExecArray | null;
|
|
197
|
+
while ((m = re.exec(hay)) !== null) {
|
|
198
|
+
const idx = Number(m[1]);
|
|
199
|
+
const label = String(m[2] ?? '').trim();
|
|
200
|
+
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
201
|
+
if (buttons.some(b => b.index === idx)) continue;
|
|
202
|
+
const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
|
|
203
|
+
buttons.push({ index: idx, label, key });
|
|
204
|
+
}
|
|
175
205
|
}
|
|
176
206
|
buttons.sort((a, b) => a.index - b.index);
|
|
177
207
|
const minCount = state.modal_buttons.min_count ?? 2;
|
|
@@ -122,7 +122,8 @@
|
|
|
122
122
|
"pattern": { "type": "string", "minLength": 1 },
|
|
123
123
|
"flags": { "type": "string" },
|
|
124
124
|
"key_for_index": { "type": "string", "minLength": 1 },
|
|
125
|
-
"min_count": { "type": "integer", "minimum": 1, "default": 2 }
|
|
125
|
+
"min_count": { "type": "integer", "minimum": 1, "default": 2 },
|
|
126
|
+
"continuation_lines": { "type": "boolean", "default": false }
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
}
|