@scotthuang/agent-knock-knock 0.11.4 → 0.11.5

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.
@@ -24,6 +24,14 @@ export declare class NativeInspectionSubmissionError extends Error {
24
24
  cause?: unknown;
25
25
  });
26
26
  }
27
+ /** A verified modal could not be dismissed across one exact key attempt. */
28
+ export declare class NativeInspectionDismissalError extends Error {
29
+ readonly code = "AKK_NATIVE_INSPECTION_DISMISSAL_FAILED";
30
+ readonly doNotRetry = true;
31
+ constructor(message: string, options?: {
32
+ cause?: unknown;
33
+ });
34
+ }
27
35
  export interface TerminalBridgeStatus {
28
36
  provider: "tmux";
29
37
  target: string;
@@ -129,7 +137,7 @@ export interface TerminalNativeInspectionMaterializationEvidence {
129
137
  stableCaptures: number;
130
138
  }
131
139
  export interface TerminalNativeInspectionBeforeEnterContext {
132
- agent: "codex";
140
+ agent: ExecutorKind;
133
141
  terminalControl: TerminalControlRef;
134
142
  plan: TerminalNativeInspectionPlan;
135
143
  preEnterScreenDigest: string;
@@ -146,15 +154,33 @@ export interface TerminalNativeInspectionOptions {
146
154
  }
147
155
  export interface TerminalNativeInspectionResult {
148
156
  stage: "enter_dispatched";
149
- agent: "codex";
157
+ agent: ExecutorKind;
150
158
  terminalControl: TerminalControlRef;
151
- command: "/status";
159
+ command: string;
152
160
  behaviorProfile: string;
153
161
  preEnterScreenDigest: string;
154
162
  preEnterEvidenceInventory: readonly TerminalNativeInspectionEvidenceInventoryEntry[];
155
163
  materialization: TerminalNativeInspectionMaterializationEvidence;
156
164
  enterCount: 1;
157
165
  }
166
+ export interface TerminalNativeInspectionBeforeDismissContext {
167
+ agent: ExecutorKind;
168
+ terminalControl: TerminalControlRef;
169
+ plan: TerminalNativeInspectionPlan;
170
+ evidenceFingerprint: string;
171
+ }
172
+ export interface TerminalNativeInspectionDismissalOptions {
173
+ runtime?: TerminalRuntimeIdentity;
174
+ scrollbackLines?: number;
175
+ beforeDismiss?: (context: TerminalNativeInspectionBeforeDismissContext) => void | Promise<void>;
176
+ }
177
+ export interface TerminalNativeInspectionDismissalResult {
178
+ stage: "dismiss_dispatched";
179
+ agent: ExecutorKind;
180
+ terminalControl: TerminalControlRef;
181
+ keys: readonly string[];
182
+ dismissCount: 1;
183
+ }
158
184
  export interface TerminalNativeInspectionObservationResult {
159
185
  terminalControl: TerminalControlRef;
160
186
  status: TerminalBridgeStatus;
@@ -221,8 +247,14 @@ export declare class TerminalAgentBridge {
221
247
  runtime?: TerminalRuntimeIdentity;
222
248
  scrollbackLines?: number;
223
249
  }): Promise<TerminalNativeInspectionObservationResult>;
224
- private settleCodexNativeInspectionComposer;
225
- private revalidateCodexNativeInspectionComposer;
250
+ /**
251
+ * Dismiss one exact adapter-owned modal result after re-observing the same
252
+ * evidence under the terminal identity fence. There is exactly one key
253
+ * attempt and no automated retry across an uncertain dismissal boundary.
254
+ */
255
+ dismissNativeInspection(agent: ExecutorKind, terminalControl: TerminalControlRef, plan: TerminalNativeInspectionPlan, request: TerminalNativeInspectionObservationRequest, expectedEvidenceFingerprint: string, options?: TerminalNativeInspectionDismissalOptions): Promise<TerminalNativeInspectionDismissalResult>;
256
+ private settleNativeInspectionComposer;
257
+ private revalidateNativeInspectionComposer;
226
258
  private settleCodexMultilineComposer;
227
259
  /**
228
260
  * Prove that the exact managed draft is still present after Enter dispatch.
@@ -281,6 +313,12 @@ export declare class TerminalAgentBridge {
281
313
  private captureInspection;
282
314
  private verifyTerminalIdentity;
283
315
  }
316
+ /**
317
+ * Prove the exact idle input frame from the verified Claude Code 2.1.218
318
+ * profile. A loose or historical `❯` prompt is not enough to authorize even
319
+ * the initial literal `/status` injection.
320
+ */
321
+ export declare function isExactClaudeNativeInspectionIdleComposer(screen: string): boolean;
284
322
  export declare function terminalApprovalFingerprint(agent: ExecutorKind, terminalControl: TerminalControlRef, inspection: TerminalScreenInspection, options?: {
285
323
  screen?: string;
286
324
  runtime?: TerminalRuntimeIdentity;
@@ -18,6 +18,14 @@ const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
18
18
  "codex-tui-0.146.0": " /status show current session configuration and token usage",
19
19
  "codex-tui-0.146.1": " /status show current session configuration and token usage"
20
20
  };
21
+ const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
22
+ "claude-code-2.1.218-native-status": [
23
+ "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
24
+ "/statusline Set up Claude Code's status line UI",
25
+ "/ide Manage IDE integrations and show status",
26
+ "/usage Show session cost, plan usage, and activity stats"
27
+ ]
28
+ };
21
29
  const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
22
30
  /** A terminal send failed at a boundary that proves input never started. */
23
31
  export class TerminalInputNotStartedError extends Error {
@@ -45,6 +53,15 @@ export class NativeInspectionSubmissionError extends Error {
45
53
  this.doNotRetry = stage !== "not_started";
46
54
  }
47
55
  }
56
+ /** A verified modal could not be dismissed across one exact key attempt. */
57
+ export class NativeInspectionDismissalError extends Error {
58
+ code = "AKK_NATIVE_INSPECTION_DISMISSAL_FAILED";
59
+ doNotRetry = true;
60
+ constructor(message, options = {}) {
61
+ super(message, options);
62
+ this.name = "NativeInspectionDismissalError";
63
+ }
64
+ }
48
65
  export class TerminalAgentBridge {
49
66
  registry;
50
67
  terminalProvider;
@@ -254,7 +271,7 @@ export class TerminalAgentBridge {
254
271
  async submitNativeInspection(agent, terminalControl, plan, options = {}) {
255
272
  const adapter = this.registry.require(agent);
256
273
  try {
257
- assertCodexStatusInspectionPlan(adapter, terminalControl, plan);
274
+ assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
258
275
  }
259
276
  catch (error) {
260
277
  throw nativeInspectionSubmissionError("not_started", error);
@@ -279,15 +296,15 @@ export class TerminalAgentBridge {
279
296
  }
280
297
  let settled;
281
298
  try {
282
- settled = await this.settleCodexNativeInspectionComposer(adapter, terminalControl, plan, options.runtime);
299
+ settled = await this.settleNativeInspectionComposer(adapter, terminalControl, plan, options.runtime);
283
300
  await options.beforeEnter?.({
284
- agent: "codex",
301
+ agent: adapter.agent,
285
302
  terminalControl: settled.terminalControl,
286
303
  plan,
287
304
  preEnterScreenDigest: settled.screenDigest,
288
305
  materialization: settled.materialization
289
306
  });
290
- settled = await this.revalidateCodexNativeInspectionComposer(adapter, settled.terminalControl, plan, settled.materialization, options.runtime);
307
+ settled = await this.revalidateNativeInspectionComposer(adapter, settled.terminalControl, plan, settled.materialization, options.runtime);
291
308
  }
292
309
  catch (error) {
293
310
  throw nativeInspectionSubmissionError("text_injected", error);
@@ -302,9 +319,9 @@ export class TerminalAgentBridge {
302
319
  }
303
320
  return {
304
321
  stage: "enter_dispatched",
305
- agent: "codex",
322
+ agent: adapter.agent,
306
323
  terminalControl: settled.terminalControl,
307
- command: "/status",
324
+ command: plan.command,
308
325
  behaviorProfile: plan.behaviorProfile,
309
326
  preEnterScreenDigest: settled.screenDigest,
310
327
  preEnterEvidenceInventory: settled.evidenceInventory,
@@ -322,14 +339,24 @@ export class TerminalAgentBridge {
322
339
  scrollbackLines: options.scrollbackLines
323
340
  });
324
341
  const screenDigest = nativeInspectionScreenFingerprint(captured.screen);
325
- const observation = adapter.observeNativeInspection({
342
+ const adapterObservation = adapter.observeNativeInspection({
326
343
  operation: request.operation,
327
344
  previousScreenFingerprint: request.previousScreenFingerprint,
328
345
  preEnterEvidenceInventory: request.preEnterEvidenceInventory,
329
346
  expectedNativeThreadId: request.expectedNativeThreadId,
330
347
  expectedAgentVersion: request.expectedAgentVersion,
348
+ expectedCwd: request.expectedCwd,
331
349
  screen: captured.screen
332
350
  });
351
+ const observation = adapter.agent === "claude" &&
352
+ options.runtime?.requireExactClaudeAgentRow === true &&
353
+ adapterObservation.status === "observed" &&
354
+ adapterObservation.evidence === "claude_status_panel"
355
+ ? {
356
+ ...adapterObservation,
357
+ evidence: "claude_status_panel+claude_agents_exact_pid"
358
+ }
359
+ : adapterObservation;
333
360
  return {
334
361
  terminalControl: captured.terminalControl,
335
362
  status: statusFromInspection(adapter, captured.terminalControl, captured.inspection, { screen: captured.screen, runtime: options.runtime }),
@@ -337,7 +364,78 @@ export class TerminalAgentBridge {
337
364
  observation
338
365
  };
339
366
  }
340
- async settleCodexNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
367
+ /**
368
+ * Dismiss one exact adapter-owned modal result after re-observing the same
369
+ * evidence under the terminal identity fence. There is exactly one key
370
+ * attempt and no automated retry across an uncertain dismissal boundary.
371
+ */
372
+ async dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options = {}) {
373
+ const adapter = this.registry.require(agent);
374
+ try {
375
+ assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
376
+ assertClosedNativeInspectionDismissal(plan);
377
+ }
378
+ catch (error) {
379
+ throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
380
+ }
381
+ const observeExactPanel = async (control) => {
382
+ const captured = await this.captureInspection(adapter, control, {
383
+ runtime: options.runtime,
384
+ scrollbackLines: options.scrollbackLines
385
+ });
386
+ const observation = adapter.observeNativeInspection?.({
387
+ operation: request.operation,
388
+ previousScreenFingerprint: request.previousScreenFingerprint,
389
+ preEnterEvidenceInventory: request.preEnterEvidenceInventory,
390
+ expectedNativeThreadId: request.expectedNativeThreadId,
391
+ expectedAgentVersion: request.expectedAgentVersion,
392
+ expectedCwd: request.expectedCwd,
393
+ screen: captured.screen
394
+ });
395
+ if (!observation ||
396
+ observation.status !== "observed" ||
397
+ observation.result?.kind !== "native_status" ||
398
+ observation.evidenceFingerprint !== expectedEvidenceFingerprint) {
399
+ throw new Error(observation?.reason ??
400
+ "the exact fresh native status panel changed before dismissal");
401
+ }
402
+ const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
403
+ if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
404
+ throw new Error("terminal control identity changed before native status dismissal");
405
+ }
406
+ return verified;
407
+ };
408
+ try {
409
+ let verified = await observeExactPanel(terminalControl);
410
+ await options.beforeDismiss?.({
411
+ agent: adapter.agent,
412
+ terminalControl: verified,
413
+ plan,
414
+ evidenceFingerprint: expectedEvidenceFingerprint
415
+ });
416
+ verified = await observeExactPanel(verified);
417
+ try {
418
+ await this.terminalProvider.sendKeys(verified.target, plan.expectedResult.dismissal.keys, { socketPath: verified.socketPath });
419
+ }
420
+ catch (error) {
421
+ throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
422
+ }
423
+ return {
424
+ stage: "dismiss_dispatched",
425
+ agent: adapter.agent,
426
+ terminalControl: verified,
427
+ keys: plan.expectedResult.dismissal.keys,
428
+ dismissCount: 1
429
+ };
430
+ }
431
+ catch (error) {
432
+ if (error instanceof NativeInspectionDismissalError) {
433
+ throw error;
434
+ }
435
+ throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
436
+ }
437
+ }
438
+ async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
341
439
  const startedAt = performance.now();
342
440
  let stableDigest;
343
441
  let stableKind;
@@ -349,7 +447,7 @@ export class TerminalAgentBridge {
349
447
  scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
350
448
  });
351
449
  assertNativeInspectionComposerSafe(captured.inspection);
352
- const materialized = exactCodexNativeInspectionComposerCapture(captured.screen, plan);
450
+ const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
353
451
  const now = performance.now();
354
452
  if (materialized) {
355
453
  if (materialized.digest === stableDigest &&
@@ -371,7 +469,7 @@ export class TerminalAgentBridge {
371
469
  stableForMs,
372
470
  stableCaptures
373
471
  };
374
- return this.revalidateCodexNativeInspectionComposer(adapter, captured.terminalControl, plan, evidence, runtime);
472
+ return this.revalidateNativeInspectionComposer(adapter, captured.terminalControl, plan, evidence, runtime);
375
473
  }
376
474
  }
377
475
  else {
@@ -387,19 +485,19 @@ export class TerminalAgentBridge {
387
485
  }
388
486
  await terminalSettleDelay(Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remaining));
389
487
  }
390
- throw new Error("Codex /status composer did not become exact, idle, and stable before the bounded submit deadline");
488
+ throw new Error(`${adapter.displayName} /status composer did not become exact, idle, and stable before the bounded submit deadline`);
391
489
  }
392
- async revalidateCodexNativeInspectionComposer(adapter, terminalControl, plan, expected, runtime) {
490
+ async revalidateNativeInspectionComposer(adapter, terminalControl, plan, expected, runtime) {
393
491
  const captured = await this.captureInspection(adapter, terminalControl, {
394
492
  runtime,
395
493
  scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
396
494
  });
397
495
  assertNativeInspectionComposerSafe(captured.inspection);
398
- const materialized = exactCodexNativeInspectionComposerCapture(captured.screen, plan);
496
+ const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
399
497
  if (!materialized ||
400
498
  materialized.digest !== expected.digest ||
401
499
  materialized.kind !== expected.kind) {
402
- throw new Error("Codex /status composer changed after its stable pre-submit capture");
500
+ throw new Error(`${adapter.displayName} /status composer changed after its stable pre-submit capture`);
403
501
  }
404
502
  const baseline = adapter.observeNativeInspection?.({
405
503
  operation: plan.operation,
@@ -409,11 +507,11 @@ export class TerminalAgentBridge {
409
507
  baseline.status === "ambiguous" ||
410
508
  !Array.isArray(baseline.evidenceInventory)) {
411
509
  throw new Error(baseline?.reason ??
412
- "Codex /status pre-Enter evidence inventory was not proven");
510
+ `${adapter.displayName} /status pre-Enter evidence inventory was not proven`);
413
511
  }
414
512
  const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
415
513
  if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
416
- throw new Error("terminal control identity changed after the final Codex /status composer capture");
514
+ throw new Error(`terminal control identity changed after the final ${adapter.displayName} /status composer capture`);
417
515
  }
418
516
  return {
419
517
  terminalControl: verifiedImmediatelyBeforeEnter,
@@ -958,36 +1056,55 @@ function nativeInspectionSubmissionError(stage, error) {
958
1056
  }
959
1057
  return new NativeInspectionSubmissionError(stage, error instanceof Error ? error.message : String(error), { cause: error });
960
1058
  }
961
- function assertCodexStatusInspectionPlan(adapter, terminalControl, plan) {
962
- if (adapter.agent !== "codex") {
963
- throw new Error("native /status inspection is currently supported only for Codex");
964
- }
1059
+ function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
965
1060
  if (!terminalControl.capabilities.includes("send_keys")) {
966
- throw new Error("Codex terminal input is not supported");
1061
+ throw new Error(`${adapter.displayName} terminal input is not supported`);
967
1062
  }
968
1063
  if (!terminalControl.capabilities.includes("screen_status")) {
969
- throw new Error("Codex terminal screen inspection is not supported");
1064
+ throw new Error(`${adapter.displayName} terminal screen inspection is not supported`);
970
1065
  }
1066
+ const codexProfile = adapter.agent === "codex" && [
1067
+ "codex-tui-0.146.0",
1068
+ "codex-tui-0.146.1"
1069
+ ].includes(plan.behaviorProfile);
1070
+ const claudeProfile = adapter.agent === "claude" &&
1071
+ plan.behaviorProfile === "claude-code-2.1.218-native-status";
1072
+ const exactPresentation = codexProfile
1073
+ ? plan.expectedResult.presentation === "inline" &&
1074
+ plan.expectedResult.dismissal === undefined &&
1075
+ plan.composer.minimumStableMs >= CODEX_PASTE_ENTER_SETTLE_MS
1076
+ : claudeProfile
1077
+ ? plan.expectedResult.presentation === "modal" &&
1078
+ plan.composer.minimumStableMs >= 80 &&
1079
+ plan.expectedResult.dismissal?.expected === "idle_empty_composer" &&
1080
+ JSON.stringify(plan.expectedResult.dismissal.keys) ===
1081
+ JSON.stringify(["Escape"])
1082
+ : false;
971
1083
  if (plan.operation.kind !== "status" ||
972
1084
  plan.command !== "/status" ||
973
1085
  plan.effect !== "read_only" ||
974
1086
  plan.requiresIdle !== true ||
975
1087
  plan.composer.kind !== "exact" ||
976
1088
  !Number.isFinite(plan.composer.minimumStableMs) ||
977
- plan.composer.minimumStableMs < CODEX_PASTE_ENTER_SETTLE_MS ||
1089
+ plan.composer.minimumStableMs < 0 ||
978
1090
  plan.expectedResult.kind !== "native_status" ||
979
- ![
980
- "codex-tui-0.146.0",
981
- "codex-tui-0.146.1"
982
- ].includes(plan.behaviorProfile)) {
1091
+ !exactPresentation) {
983
1092
  throw new Error("refusing a non-closed or unverified native inspection plan");
984
1093
  }
985
1094
  }
986
- function assertNativeInspectionComposerSafe(inspection) {
1095
+ function assertClosedNativeInspectionDismissal(plan) {
1096
+ if (plan.expectedResult.presentation !== "modal" ||
1097
+ plan.expectedResult.dismissal?.expected !== "idle_empty_composer" ||
1098
+ JSON.stringify(plan.expectedResult.dismissal.keys) !==
1099
+ JSON.stringify(["Escape"])) {
1100
+ throw new Error("native inspection has no closed verified modal dismissal plan");
1101
+ }
1102
+ }
1103
+ function assertNativeInspectionComposerSafe(inspection, displayName = "terminal agent") {
987
1104
  if (inspection.approval.blocked ||
988
1105
  inspection.activity.state === "awaiting_approval" ||
989
1106
  inspection.activity.state === "working") {
990
- throw new Error("Codex became busy or blocked while its /status composer was settling");
1107
+ throw new Error(`${displayName} became busy or blocked while its /status composer was settling`);
991
1108
  }
992
1109
  // Codex's generic activity parser deliberately reports a non-empty slash
993
1110
  // composer as unknown. At this stage the caller has already proved an idle,
@@ -995,6 +1112,11 @@ function assertNativeInspectionComposerSafe(inspection) {
995
1112
  // capture below is the stronger continuation proof after AKK injected only
996
1113
  // the adapter-owned /status command.
997
1114
  }
1115
+ function exactNativeInspectionComposerCapture(agent, screen, plan) {
1116
+ return agent === "codex"
1117
+ ? exactCodexNativeInspectionComposerCapture(screen, plan)
1118
+ : exactClaudeNativeInspectionComposerCapture(screen, plan);
1119
+ }
998
1120
  function exactCodexNativeInspectionComposerCapture(screen, plan) {
999
1121
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
1000
1122
  let currentComposerIndex = -1;
@@ -1037,6 +1159,88 @@ function exactCodexNativeInspectionComposerCapture(screen, plan) {
1037
1159
  digest: createHash("sha256").update(region.join("\n")).digest("hex")
1038
1160
  };
1039
1161
  }
1162
+ function exactClaudeNativeInspectionComposerCapture(screen, plan) {
1163
+ const frame = exactClaudeNativeInspectionComposerFrame(screen);
1164
+ if (!frame) {
1165
+ return undefined;
1166
+ }
1167
+ const { lines, openIndex, closeIndex, composerRows, trailing } = frame;
1168
+ if (composerRows.length !== 1 ||
1169
+ !/^\s*❯(?:\s|$)/u.test(composerRows[0]) ||
1170
+ composerRows[0].replace(/^\s*❯\s?/u, "").trimEnd() !== plan.command) {
1171
+ return undefined;
1172
+ }
1173
+ let kind;
1174
+ if (trailing.length === 0 || claudeNativeInspectionTrailingIsFooter(trailing)) {
1175
+ kind = "exact_slash_composer";
1176
+ }
1177
+ else {
1178
+ const suggestions = [];
1179
+ for (const line of trailing) {
1180
+ const trimmed = line.trim();
1181
+ if (trimmed.startsWith("/")) {
1182
+ suggestions.push(trimmed.replace(/\s+/gu, " "));
1183
+ }
1184
+ else if (suggestions.length > 0) {
1185
+ suggestions[suggestions.length - 1] +=
1186
+ ` ${trimmed.replace(/\s+/gu, " ")}`;
1187
+ }
1188
+ else {
1189
+ return undefined;
1190
+ }
1191
+ }
1192
+ if (JSON.stringify(suggestions) !== JSON.stringify(CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
1193
+ return undefined;
1194
+ }
1195
+ kind = "exact_slash_popup";
1196
+ }
1197
+ return {
1198
+ kind,
1199
+ digest: createHash("sha256")
1200
+ .update(lines.slice(openIndex, closeIndex + 1).concat(trailing).join("\n"))
1201
+ .digest("hex")
1202
+ };
1203
+ }
1204
+ /**
1205
+ * Prove the exact idle input frame from the verified Claude Code 2.1.218
1206
+ * profile. A loose or historical `❯` prompt is not enough to authorize even
1207
+ * the initial literal `/status` injection.
1208
+ */
1209
+ export function isExactClaudeNativeInspectionIdleComposer(screen) {
1210
+ const frame = exactClaudeNativeInspectionComposerFrame(screen);
1211
+ if (!frame) {
1212
+ return false;
1213
+ }
1214
+ return (frame.composerRows.length === 1 &&
1215
+ /^\s*❯\s*$/u.test(frame.composerRows[0]) &&
1216
+ (frame.trailing.length === 0 ||
1217
+ claudeNativeInspectionTrailingIsFooter(frame.trailing)));
1218
+ }
1219
+ function exactClaudeNativeInspectionComposerFrame(screen) {
1220
+ const lines = screen.replace(/\r\n?/gu, "\n").replace(/\u00a0/gu, " ")
1221
+ .split("\n");
1222
+ const dividerIndexes = lines
1223
+ .map((line, index) => /^\s*[─━]{8,}\s*$/u.test(line) ? index : -1)
1224
+ .filter((index) => index >= 0);
1225
+ if (dividerIndexes.length < 2) {
1226
+ return undefined;
1227
+ }
1228
+ const closeIndex = dividerIndexes.at(-1);
1229
+ const openIndex = dividerIndexes.at(-2);
1230
+ return {
1231
+ lines,
1232
+ openIndex,
1233
+ closeIndex,
1234
+ composerRows: lines.slice(openIndex + 1, closeIndex)
1235
+ .filter((line) => line.trim().length > 0),
1236
+ trailing: lines.slice(closeIndex + 1)
1237
+ .filter((line) => line.trim().length > 0)
1238
+ };
1239
+ }
1240
+ function claudeNativeInspectionTrailingIsFooter(lines) {
1241
+ return lines.length <= 2 && lines.every((line) => /^\s*(?:[⏵⏴]{1,2}|\?)\s*.*(?:shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
1242
+ .test(line));
1243
+ }
1040
1244
  function nativeInspectionScreenFingerprint(screen) {
1041
1245
  return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
1042
1246
  }