@adhdev/daemon-core 0.9.6 → 0.9.7

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.
@@ -94,6 +94,51 @@ export function hydrateCliParsedMessages(
94
94
  });
95
95
  }
96
96
 
97
+ function chooseMoreComparableCliMessage(left: CliChatMessage, right: CliChatMessage): CliChatMessage {
98
+ const leftComparable = normalizeComparableMessageContent(left.content || '');
99
+ const rightComparable = normalizeComparableMessageContent(right.content || '');
100
+
101
+ if (leftComparable && leftComparable === rightComparable) {
102
+ const leftNewlines = String(left.content || '').split(/\r\n|\n|\r/g).length - 1;
103
+ const rightNewlines = String(right.content || '').split(/\r\n|\n|\r/g).length - 1;
104
+ return rightNewlines < leftNewlines ? right : left;
105
+ }
106
+
107
+ return rightComparable.length > leftComparable.length ? right : left;
108
+ }
109
+
110
+ function dedupeConsecutiveComparableCliMessages(messages: CliChatMessage[]): CliChatMessage[] {
111
+ const deduped: CliChatMessage[] = [];
112
+
113
+ for (const message of messages) {
114
+ const current = {
115
+ ...message,
116
+ content: typeof message.content === 'string' ? message.content : String(message.content || ''),
117
+ } as CliChatMessage;
118
+ const previous = deduped[deduped.length - 1];
119
+ if (!previous) {
120
+ deduped.push(current);
121
+ continue;
122
+ }
123
+
124
+ const previousComparable = normalizeComparableMessageContent(previous.content || '');
125
+ const currentComparable = normalizeComparableMessageContent(current.content || '');
126
+ const sameRole = previous.role === current.role;
127
+ const sameKind = (previous.kind || 'standard') === (current.kind || 'standard');
128
+ const sameSender = (previous.senderName || '') === (current.senderName || '');
129
+ const comparableMatch = previousComparable && previousComparable === currentComparable;
130
+
131
+ if (sameRole && sameKind && sameSender && comparableMatch) {
132
+ deduped[deduped.length - 1] = chooseMoreComparableCliMessage(previous, current);
133
+ continue;
134
+ }
135
+
136
+ deduped.push(current);
137
+ }
138
+
139
+ return deduped;
140
+ }
141
+
97
142
  export function normalizeCliParsedMessages(
98
143
  parsedMessages: any[],
99
144
  options: {
@@ -103,7 +148,7 @@ export function normalizeCliParsedMessages(
103
148
  now?: number;
104
149
  },
105
150
  ): CliChatMessage[] {
106
- return hydrateCliParsedMessages(parsedMessages, options).map((message) => ({
151
+ return dedupeConsecutiveComparableCliMessages(hydrateCliParsedMessages(parsedMessages, options).map((message) => ({
107
152
  role: message.role,
108
153
  content: message.content,
109
154
  timestamp: message.timestamp,
@@ -113,7 +158,7 @@ export function normalizeCliParsedMessages(
113
158
  index: message.index,
114
159
  meta: message.meta,
115
160
  senderName: message.senderName,
116
- }));
161
+ })));
117
162
  }
118
163
 
119
164
  export function buildCliParseInput(options: {
@@ -373,8 +373,58 @@ export function normalizeScreenSnapshot(text: string): string {
373
373
  .trim();
374
374
  }
375
375
 
376
+ const COMMON_COMPARABLE_WRAP_WORDS = new Set([
377
+ 'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'from', 'in', 'into', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'their', 'then', 'this', 'to', 'was', 'with',
378
+ ]);
379
+
380
+ function shouldReflowComparableMessageLines(lines: string[]): boolean {
381
+ return Array.isArray(lines)
382
+ && lines.length > 1
383
+ && lines.slice(0, -1).every((line) => String(line || '').trim().length >= 48)
384
+ && !lines.some((line) => /^```/.test(line))
385
+ && !lines.some((line) => /^\|/.test(line))
386
+ && !lines.some((line) => /^\s*(?:[-*+] |\d+\.\s)/.test(line));
387
+ }
388
+
389
+ function joinComparableMessageLines(lines: string[]): string {
390
+ return lines.reduce((acc, line) => {
391
+ const next = String(line || '').trim();
392
+ if (!next) return acc;
393
+ if (!acc) return next;
394
+
395
+ if (/[,\d]$/.test(acc) && /^\d/.test(next)) {
396
+ return `${acc}${next}`;
397
+ }
398
+
399
+ if (/[A-Za-z]$/.test(acc) && /^\d/.test(next)) {
400
+ return `${acc}${next}`;
401
+ }
402
+
403
+ const fragmentMatch = acc.match(/([A-Za-z]{1,4})$/);
404
+ const fragment = fragmentMatch ? fragmentMatch[1].toLowerCase() : '';
405
+ if (/^[a-z]/.test(next) && fragment && !COMMON_COMPARABLE_WRAP_WORDS.has(fragment)) {
406
+ return `${acc}${next}`;
407
+ }
408
+
409
+ return `${acc} ${next}`;
410
+ }, '')
411
+ .replace(/\s+([,.;:!?])/g, '$1')
412
+ .replace(/(\d)\s+,/g, '$1,')
413
+ .replace(/\s+/g, ' ')
414
+ .trim();
415
+ }
416
+
376
417
  export function normalizeComparableMessageContent(text: string): string {
377
- return String(text || '')
418
+ const lines = String(text || '')
419
+ .split(/\r\n|\n|\r/g)
420
+ .map((line) => line.trim())
421
+ .filter(Boolean);
422
+
423
+ if (lines.length === 0) return '';
424
+ if (shouldReflowComparableMessageLines(lines)) {
425
+ return joinComparableMessageLines(lines);
426
+ }
427
+ return lines.join(' ')
378
428
  .replace(/\s+/g, ' ')
379
429
  .trim();
380
430
  }
@@ -414,10 +464,6 @@ export function getLastUserPromptText(messages: Array<{ role?: string; content?:
414
464
  return '';
415
465
  }
416
466
 
417
- export function looksLikeConfirmOnlyLabel(label: string): boolean {
418
- return /^(?:continue|confirm|ok|yes|trust|proceed|enter)$/i.test(String(label || '').trim());
419
- }
420
-
421
467
  function parsePatternEntry(x: unknown): RegExp | null {
422
468
  if (x instanceof RegExp) return x;
423
469
  if (x && typeof x === 'object' && typeof (x as { source?: string }).source === 'string') {
@@ -1255,26 +1255,24 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1255
1255
  ? 'waiting_approval'
1256
1256
  : status?.status;
1257
1257
  LOG.info('Command', `[resolveAction] CLI PTY gate target=${String(args?.targetSessionId || '')} rawStatus=${String(status?.status || '')} effectiveStatus=${String(effectiveStatus || '')} statusModal=${statusModal ? 'yes' : 'no'} surfacedModal=${surfacedModal ? 'yes' : 'no'} instance=${targetInstance ? 'yes' : 'no'}`);
1258
- if (effectiveStatus !== 'waiting_approval' && !effectiveModal) {
1258
+ if (!effectiveModal) {
1259
1259
  return { success: false, error: 'Not in approval state' };
1260
1260
  }
1261
- const buttons: string[] = effectiveModal?.buttons || ['Allow once', 'Always allow', 'Deny'];
1262
- // Resolve button index: explicit buttonIndex arg → button text match → action fallback
1261
+ const buttons: string[] = Array.isArray(effectiveModal.buttons) ? effectiveModal.buttons : [];
1262
+ // Resolve button index: explicit buttonIndex arg → exact text match → explicit action mapping
1263
1263
  let buttonIndex = typeof args?.buttonIndex === 'number' ? args.buttonIndex : -1;
1264
- if (buttonIndex < 0) {
1264
+ if (buttonIndex < 0 && button) {
1265
1265
  const btnLower = button.toLowerCase();
1266
1266
  buttonIndex = buttons.findIndex(b => b.toLowerCase().includes(btnLower));
1267
1267
  }
1268
+ if (buttonIndex < 0 && (action === 'reject' || action === 'deny')) {
1269
+ buttonIndex = buttons.findIndex(b => /deny|reject|no/i.test(b));
1270
+ }
1271
+ if (buttonIndex < 0 && (action === 'always' || /always/i.test(button))) {
1272
+ buttonIndex = buttons.findIndex(b => /always/i.test(b));
1273
+ }
1268
1274
  if (buttonIndex < 0) {
1269
- if (action === 'reject' || action === 'deny') {
1270
- buttonIndex = buttons.findIndex(b => /deny|reject|no/i.test(b));
1271
- if (buttonIndex < 0) buttonIndex = buttons.length - 1;
1272
- } else if (action === 'always' || /always/i.test(button)) {
1273
- buttonIndex = buttons.findIndex(b => /always/i.test(b));
1274
- if (buttonIndex < 0) buttonIndex = 1;
1275
- } else {
1276
- buttonIndex = 0; // approve → first option (default selected)
1277
- }
1275
+ return { success: false, error: 'Approval action did not match any visible button' };
1278
1276
  }
1279
1277
  if (typeof adapter.resolveModal === 'function') {
1280
1278
  adapter.resolveModal(buttonIndex);