@scotthuang/agent-knock-knock 0.13.1 → 0.13.3

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +17 -4
  3. package/dist/src/claude-terminal-agent-adapter.js +4 -0
  4. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  5. package/dist/src/cli-core.js +9 -1
  6. package/dist/src/cli-core.js.map +1 -1
  7. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  8. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  9. package/dist/src/codex-terminal-agent-adapter.js +4 -0
  10. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  11. package/dist/src/herdr-terminal-control-provider.js +6 -1
  12. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  13. package/dist/src/host-adapter.d.ts +1 -1
  14. package/dist/src/host-adapter.js +1 -1
  15. package/dist/src/host-bridge-tools.js +2 -2
  16. package/dist/src/managed-session.js +2 -18
  17. package/dist/src/managed-session.js.map +1 -1
  18. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
  19. package/dist/src/native-thread-lifecycle-cli-adapter.js +578 -2
  20. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  21. package/dist/src/native-thread-transition-verification-adapter.js +1 -0
  22. package/dist/src/native-thread-transition-verification-adapter.js.map +1 -1
  23. package/dist/src/openclaw-plugin-command-adapter.js +326 -36
  24. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  25. package/dist/src/openclaw-plugin-helpers.d.ts +27 -0
  26. package/dist/src/openclaw-plugin-helpers.js +682 -2
  27. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  28. package/dist/src/openclaw-plugin-schemas.d.ts +53 -0
  29. package/dist/src/openclaw-plugin-schemas.js +54 -1
  30. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  31. package/dist/src/terminal-action-projection.d.ts +83 -1
  32. package/dist/src/terminal-action-projection.js +174 -0
  33. package/dist/src/terminal-action-projection.js.map +1 -1
  34. package/dist/src/terminal-agent-adapter.d.ts +7 -0
  35. package/dist/src/terminal-agent-adapter.js +8 -0
  36. package/dist/src/terminal-agent-adapter.js.map +1 -1
  37. package/dist/src/terminal-agent-bridge.d.ts +40 -0
  38. package/dist/src/terminal-agent-bridge.js +808 -16
  39. package/dist/src/terminal-agent-bridge.js.map +1 -1
  40. package/dist/src/terminal-control-ref.d.ts +23 -0
  41. package/dist/src/terminal-control-ref.js +32 -0
  42. package/dist/src/terminal-control-ref.js.map +1 -1
  43. package/dist/src/terminal-list-cli-adapter.js +324 -16
  44. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  45. package/dist/src/terminal-list-renderer.d.ts +1 -0
  46. package/dist/src/terminal-list-renderer.js +87 -3
  47. package/dist/src/terminal-list-renderer.js.map +1 -1
  48. package/dist/src/terminal-model-control.d.ts +312 -0
  49. package/dist/src/terminal-model-control.js +1595 -0
  50. package/dist/src/terminal-model-control.js.map +1 -0
  51. package/dist/src/terminal-questionnaire-adapter.d.ts +3 -1
  52. package/dist/src/terminal-questionnaire-adapter.js +27 -19
  53. package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
  54. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
  55. package/dist/src/terminal-runtime-cli-adapter.js +59 -17
  56. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  57. package/docs/quickstart-herdr.md +1 -1
  58. package/docs/quickstart-tmux.md +28 -3
  59. package/openclaw.plugin.json +13 -1
  60. package/package.json +1 -1
  61. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +56 -13
@@ -5,7 +5,9 @@ import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalC
5
5
  import { CLAUDE_INJECTED_PASTE_FRAME_PROFILE, claudeInjectedPasteProofEligible, revalidateClaudeComposerProof, settleClaudeInjectedPasteComposer } from "./claude-injected-paste-proof.js";
6
6
  import { TERMINAL_INTERACTION_SCHEMA, TERMINAL_INTERACTION_VERSION, validateAnyTerminalInteractionResponse, validateTerminalInteractionProjection } from "./terminal-interaction-protocol.js";
7
7
  import { captureTerminalInteraction } from "./terminal-interaction-core.js";
8
- // Verified Codex profiles through 0.153.4 keep Enter in paste/newline mode for
8
+ import { inspectNativeQuestionnaire } from "./terminal-questionnaire-adapter.js";
9
+ import { discoverTerminalModelOptions, inspectTerminalModelControlResidual, observeTerminalModelControl, planTerminalModelControl, probeTerminalModelControl, repairTerminalModelControlResidual, switchTerminalModel } from "./terminal-model-control.js";
10
+ // Verified Codex profiles through 0.154.0 keep Enter in paste/newline mode for
9
11
  // 120ms after burst input. Cross that boundary rather than landing on it, and
10
12
  // also require observable composer stability instead of treating this delay
11
13
  // alone as acceptance.
@@ -22,6 +24,11 @@ const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
22
24
  const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
23
25
  const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
24
26
  const CODEX_COMPLETE_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s+\S+)?|[-\w.]+ default)\s+·\s+\S.*$/u;
27
+ const CODEX_MODEL_CONTROL_SLASH_POPUP_BY_PROFILE = {
28
+ "codex-model-control-0.154.0": [
29
+ " /model choose what model and reasoning effort to use"
30
+ ]
31
+ };
25
32
  // Keep every exact slash-completion shape closed per behavior profile so a
26
33
  // version adding another matching command cannot silently become an authorized
27
34
  // native command surface.
@@ -60,6 +67,10 @@ const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
60
67
  " /status show current session configuration and token usage",
61
68
  " /statusline configure which items appear in the status line"
62
69
  ],
70
+ "codex-tui-0.154.0": [
71
+ " /status show current session configuration and token usage",
72
+ " /statusline configure which items appear in the status line"
73
+ ],
63
74
  "codex-tui-generic-v1": [
64
75
  " /status show current session configuration and token usage",
65
76
  " /statusline configure which items appear in the status line"
@@ -80,6 +91,7 @@ const CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE = {
80
91
  "codex-tui-0.151.0": 80,
81
92
  "codex-tui-0.153.0": 80,
82
93
  "codex-tui-0.153.4": 80,
94
+ "codex-tui-0.154.0": 80,
83
95
  "codex-tui-generic-v1": 80
84
96
  };
85
97
  const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
@@ -642,6 +654,367 @@ export class TerminalAgentBridge {
642
654
  };
643
655
  }
644
656
  }
657
+ async modelOptions(agent, terminalControl, agentVersion, plan, options = {}) {
658
+ const adapter = this.registry.require(agent);
659
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
660
+ assertTerminalMutationCapabilities({
661
+ provider: this.terminalProvider,
662
+ terminal: this.terminalProvider.endpoint(terminalControl),
663
+ semantic: ["send_keys", "screen_status"],
664
+ transport: [
665
+ "stable_resource_resolution",
666
+ "screen_capture",
667
+ "ansi_capture",
668
+ "text_delivery",
669
+ "key_delivery"
670
+ ]
671
+ });
672
+ const result = await discoverTerminalModelOptions({
673
+ agent,
674
+ agentVersion,
675
+ plan,
676
+ terminalControl,
677
+ ports: this.modelControlPorts(adapter, plan, options),
678
+ initialResidual: options.initialResidual
679
+ });
680
+ return {
681
+ terminalControl: result.terminalControl,
682
+ catalog: result.catalog
683
+ };
684
+ }
685
+ async setModel(agent, terminalControl, agentVersion, plan, expectedCatalogFingerprint, request, options = {}) {
686
+ const adapter = this.registry.require(agent);
687
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
688
+ assertTerminalMutationCapabilities({
689
+ provider: this.terminalProvider,
690
+ terminal: this.terminalProvider.endpoint(terminalControl),
691
+ semantic: ["send_keys", "screen_status"],
692
+ transport: [
693
+ "stable_resource_resolution",
694
+ "screen_capture",
695
+ "ansi_capture",
696
+ "text_delivery",
697
+ "key_delivery"
698
+ ]
699
+ });
700
+ const result = await switchTerminalModel({
701
+ agent,
702
+ agentVersion,
703
+ plan,
704
+ terminalControl,
705
+ expectedCatalogFingerprint,
706
+ request,
707
+ ports: this.modelControlPorts(adapter, plan, options)
708
+ });
709
+ return {
710
+ ...result,
711
+ terminalControl: result.terminalControl
712
+ };
713
+ }
714
+ async inspectModelControlResidual(agent, terminalControl, agentVersion, plan, options = {}) {
715
+ const adapter = this.registry.require(agent);
716
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
717
+ assertTerminalMutationCapabilities({
718
+ provider: this.terminalProvider,
719
+ terminal: this.terminalProvider.endpoint(terminalControl),
720
+ semantic: ["screen_status"],
721
+ transport: ["stable_resource_resolution", "screen_capture", "ansi_capture"]
722
+ });
723
+ return inspectTerminalModelControlResidual({
724
+ plan,
725
+ terminalControl,
726
+ ports: this.modelControlPorts(adapter, plan, options)
727
+ });
728
+ }
729
+ async repairModelControlResidual(agent, terminalControl, agentVersion, plan, expectedResidualFingerprint, options = {}) {
730
+ const adapter = this.registry.require(agent);
731
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
732
+ assertTerminalMutationCapabilities({
733
+ provider: this.terminalProvider,
734
+ terminal: this.terminalProvider.endpoint(terminalControl),
735
+ semantic: ["send_keys", "screen_status"],
736
+ transport: [
737
+ "stable_resource_resolution",
738
+ "screen_capture",
739
+ "ansi_capture",
740
+ "key_delivery"
741
+ ]
742
+ });
743
+ const result = await repairTerminalModelControlResidual({
744
+ plan,
745
+ terminalControl,
746
+ expectedResidualFingerprint,
747
+ ports: this.modelControlPorts(adapter, plan, options)
748
+ });
749
+ return {
750
+ ...result,
751
+ terminalControl: result.terminalControl
752
+ };
753
+ }
754
+ modelControlPorts(adapter, plan, options) {
755
+ if (!options.beforeInput) {
756
+ throw new Error("terminal model control requires current-snapshot authority before every input");
757
+ }
758
+ // `claude agents` deliberately reports the same interactive process as
759
+ // `waiting` / `dialog open` while Claude's native model picker is open.
760
+ // Keep that lifecycle relaxation private to this model-control operation:
761
+ // a capture may cross the state boundary read-only, but a terminal key is
762
+ // authorized only by the immediately preceding, exact profiled picker
763
+ // frame. The permit is one-shot so no later transport can inherit it.
764
+ let claudeModelDialogInputPermit;
765
+ let codexModelInputPermit;
766
+ const consumeCodexModelInputPermit = () => {
767
+ const permit = codexModelInputPermit;
768
+ codexModelInputPermit = undefined;
769
+ return permit;
770
+ };
771
+ const capture = async (input) => {
772
+ claudeModelDialogInputPermit = undefined;
773
+ codexModelInputPermit = undefined;
774
+ const original = input.terminalControl;
775
+ const verified = await this.verifyModelControlTerminalIdentity(adapter, plan, original, options.runtime, { claudeModelDialog: "allow" });
776
+ const styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verified), { scrollbackLines: 160, preserveEscapes: true });
777
+ const screen = stripTerminalEscapeSequences(styledScreen);
778
+ const inspection = adapter.inspectScreen({
779
+ screen,
780
+ runtime: options.runtime
781
+ });
782
+ const modelObservation = adapter.observeModelControl?.(plan, screen);
783
+ const exactCodexModelSurface = adapter.agent === "codex" &&
784
+ modelObservation !== undefined &&
785
+ modelObservation.state !== "none" &&
786
+ modelObservation.state !== "ambiguous";
787
+ const exactClaudeModelDialog = adapter.agent === "claude" &&
788
+ modelObservation?.state === "claude_model_picker";
789
+ const reverified = await this.verifyModelControlTerminalIdentity(adapter, plan, verified, options.runtime, {
790
+ claudeModelDialog: exactClaudeModelDialog
791
+ ? "require"
792
+ : "forbid"
793
+ });
794
+ if (!sameTerminalControlIdentity(verified, reverified)) {
795
+ throw new Error("terminal control identity changed across model-control capture");
796
+ }
797
+ if (exactClaudeModelDialog) {
798
+ claudeModelDialogInputPermit = {
799
+ terminalControl: reverified,
800
+ fingerprint: modelObservation.fingerprint
801
+ };
802
+ }
803
+ const codexComposer = adapter.agent === "codex"
804
+ ? currentCodexComposerCapture(styledScreen, input.expectedComposer ?? "", false, false, input.expectedComposer === plan.command
805
+ ? CODEX_MODEL_CONTROL_SLASH_POPUP_BY_PROFILE[plan.behaviorProfile]
806
+ : undefined)
807
+ : undefined;
808
+ let codexInputBlocked = false;
809
+ if (adapter.agent === "codex") {
810
+ try {
811
+ const questionnaire = inspectNativeQuestionnaire({
812
+ agent: "codex",
813
+ version: "0.154.0",
814
+ screen: styledScreen
815
+ });
816
+ const asyncQuestionMode = inspectCodexAsyncQuestionInputMode(styledScreen);
817
+ codexInputBlocked = !exactCodexModelSurface && (questionnaire.status !== "none" ||
818
+ codexActiveWriterViewerVisible(styledScreen) ||
819
+ asyncQuestionMode === "expanded" ||
820
+ asyncQuestionMode === "ambiguous");
821
+ }
822
+ catch {
823
+ codexInputBlocked = !exactCodexModelSurface;
824
+ }
825
+ }
826
+ const exactEmptyComposer = adapter.agent === "codex"
827
+ ? codexComposer?.state === "exact_empty"
828
+ : isExactClaudeIdleComposer(screen);
829
+ const claudeProfiledCommand = adapter.agent === "claude" &&
830
+ input.expectedComposer === plan.command &&
831
+ exactClaudeModelControlComposerCapture(screen, plan, input.expectedComposer) !== undefined;
832
+ const claudeBareCommand = adapter.agent === "claude" &&
833
+ input.expectedComposer !== undefined &&
834
+ exactTerminalComposerCapture("claude", screen, input.expectedComposer) !== undefined;
835
+ const exactCommandComposer = input.expectedComposer !== undefined && (adapter.agent === "codex"
836
+ ? codexComposer?.state === "exact_draft"
837
+ : claudeProfiledCommand || claudeBareCommand);
838
+ const exactCommandReady = adapter.agent === "codex"
839
+ ? codexComposer?.state === "exact_draft" &&
840
+ codexComposer.profiledSlashPopup === true
841
+ : claudeProfiledCommand;
842
+ const exactBareCommand = adapter.agent === "codex"
843
+ ? codexComposer?.state === "exact_draft" &&
844
+ codexComposer.bareCommand === true
845
+ : claudeBareCommand && !claudeProfiledCommand;
846
+ if (adapter.agent === "codex") {
847
+ const exactModelSurface = modelObservation &&
848
+ modelObservation.state !== "none" &&
849
+ modelObservation.state !== "ambiguous"
850
+ ? {
851
+ kind: "model_surface",
852
+ fingerprint: modelObservation.fingerprint
853
+ }
854
+ : undefined;
855
+ const exactComposerSurface = codexComposer?.state === "exact_empty"
856
+ ? {
857
+ kind: "composer_empty",
858
+ fingerprint: codexComposer.digest
859
+ }
860
+ : codexComposer?.state === "exact_draft"
861
+ ? {
862
+ kind: "composer_command",
863
+ fingerprint: codexComposer.digest
864
+ }
865
+ : undefined;
866
+ const proof = exactModelSurface ?? exactComposerSurface;
867
+ if (proof) {
868
+ codexModelInputPermit = {
869
+ terminalControl: reverified,
870
+ expectedComposer: input.expectedComposer,
871
+ ...proof
872
+ };
873
+ }
874
+ }
875
+ return {
876
+ terminalControl: reverified,
877
+ screen,
878
+ // The exact profiled picker is the current input owner. Generic
879
+ // lifecycle/questionnaire detectors may still see stale transcript
880
+ // markers behind that overlay, so keep their result from vetoing the
881
+ // model-control state machine. Unknown or partial pickers receive no
882
+ // such override and remain fail-closed.
883
+ activityState: exactCodexModelSurface
884
+ ? "unknown"
885
+ : inspection.activity.state,
886
+ approvalBlocked: exactCodexModelSurface
887
+ ? false
888
+ : inspection.approval.blocked,
889
+ exactEmptyComposer,
890
+ exactCommandReady,
891
+ exactCommandComposer,
892
+ exactBareCommand,
893
+ ...(codexComposer?.state === "exact_draft"
894
+ ? { exactCommandFingerprint: codexComposer.digest }
895
+ : {}),
896
+ inputBlocked: codexInputBlocked
897
+ };
898
+ };
899
+ return {
900
+ beforeInput: options.beforeInput,
901
+ loadCodexCatalog: options.loadCodexCatalog,
902
+ capture,
903
+ sendText: async (control, text) => {
904
+ claudeModelDialogInputPermit = undefined;
905
+ if (adapter.agent === "codex") {
906
+ const permit = consumeCodexModelInputPermit();
907
+ if (!permit || permit.kind !== "composer_empty") {
908
+ throw new Error("Codex /model text requires one fresh exact empty-composer permit");
909
+ }
910
+ const fresh = await capture({
911
+ terminalControl: control,
912
+ expectedComposer: permit.expectedComposer
913
+ });
914
+ const freshPermit = consumeCodexModelInputPermit();
915
+ if (!freshPermit ||
916
+ freshPermit.kind !== permit.kind ||
917
+ freshPermit.fingerprint !== permit.fingerprint ||
918
+ !sameTerminalControlIdentity(permit.terminalControl, freshPermit.terminalControl) ||
919
+ fresh.inputBlocked === true ||
920
+ fresh.approvalBlocked ||
921
+ fresh.activityState === "working" ||
922
+ fresh.activityState === "awaiting_approval" ||
923
+ !fresh.exactEmptyComposer) {
924
+ throw new Error("the exact Codex empty composer changed before /model text delivery");
925
+ }
926
+ await this.terminalProvider.sendText(this.terminalProvider.endpoint(freshPermit.terminalControl), text);
927
+ return;
928
+ }
929
+ const verified = await this.verifyTerminalIdentity(adapter.agent, control, options.runtime);
930
+ await this.terminalProvider.sendText(this.terminalProvider.endpoint(verified), text);
931
+ },
932
+ sendKeys: async (control, keys) => {
933
+ const requestedControl = control;
934
+ if (adapter.agent === "codex") {
935
+ const permit = consumeCodexModelInputPermit();
936
+ claudeModelDialogInputPermit = undefined;
937
+ if (!permit || !sameTerminalControlIdentity(permit.terminalControl, requestedControl)) {
938
+ throw new Error("Codex model-control key requires one fresh exact input-surface permit");
939
+ }
940
+ const fresh = await capture({
941
+ terminalControl: requestedControl,
942
+ expectedComposer: permit.expectedComposer
943
+ });
944
+ const freshPermit = consumeCodexModelInputPermit();
945
+ if (!freshPermit ||
946
+ freshPermit.kind !== permit.kind ||
947
+ freshPermit.fingerprint !== permit.fingerprint ||
948
+ !sameTerminalControlIdentity(permit.terminalControl, freshPermit.terminalControl) ||
949
+ fresh.inputBlocked === true ||
950
+ fresh.approvalBlocked ||
951
+ fresh.activityState === "working" ||
952
+ fresh.activityState === "awaiting_approval") {
953
+ throw new Error("the exact Codex model-control input surface changed before key dispatch");
954
+ }
955
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(freshPermit.terminalControl), keys);
956
+ return;
957
+ }
958
+ const permit = claudeModelDialogInputPermit;
959
+ claudeModelDialogInputPermit = undefined;
960
+ const requireClaudeModelDialog = permit !== undefined &&
961
+ sameTerminalControlIdentity(permit.terminalControl, requestedControl);
962
+ const verified = await this.verifyModelControlTerminalIdentity(adapter, plan, requestedControl, options.runtime, {
963
+ claudeModelDialog: requireClaudeModelDialog
964
+ ? "require"
965
+ : "forbid"
966
+ });
967
+ if (requireClaudeModelDialog) {
968
+ const styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verified), { scrollbackLines: 160, preserveEscapes: true });
969
+ const screen = stripTerminalEscapeSequences(styledScreen);
970
+ const inspection = adapter.inspectScreen({
971
+ screen,
972
+ runtime: options.runtime
973
+ });
974
+ const observation = adapter.observeModelControl?.(plan, screen);
975
+ if (inspection.approval.blocked ||
976
+ inspection.activity.state === "working" ||
977
+ observation?.state !== "claude_model_picker" ||
978
+ observation.fingerprint !== permit?.fingerprint) {
979
+ throw new Error("the exact Claude model picker changed before key dispatch");
980
+ }
981
+ }
982
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), keys);
983
+ },
984
+ sleep: this.sleep
985
+ };
986
+ }
987
+ async verifyModelControlTerminalIdentity(adapter, plan, terminalControl, runtime, options) {
988
+ const canUseClaudeModelDialog = adapter.agent === "claude" &&
989
+ plan.behaviorProfile === "claude-model-control-2.1.266" &&
990
+ runtime?.requireExactClaudeAgentRow === true &&
991
+ runtime.exactClaudeAgentState === "idle";
992
+ if (options.claudeModelDialog === "require") {
993
+ if (!canUseClaudeModelDialog) {
994
+ throw new Error("Claude model-dialog identity cannot be required outside its exact profile");
995
+ }
996
+ return this.verifyTerminalIdentity(adapter.agent, terminalControl, { ...runtime, exactClaudeAgentState: "status_dialog" });
997
+ }
998
+ try {
999
+ return await this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime);
1000
+ }
1001
+ catch (idleError) {
1002
+ if (options.claudeModelDialog !== "allow" ||
1003
+ !canUseClaudeModelDialog) {
1004
+ throw idleError;
1005
+ }
1006
+ try {
1007
+ return await this.verifyTerminalIdentity(adapter.agent, terminalControl, { ...runtime, exactClaudeAgentState: "status_dialog" });
1008
+ }
1009
+ catch {
1010
+ // Preserve the original current-snapshot failure. Both attempts use
1011
+ // the same PID, process incarnation, native Session, cwd, and terminal
1012
+ // control fence; the second changes only the exact allowed agents row
1013
+ // state from idle to `waiting` / `dialog open`.
1014
+ throw idleError;
1015
+ }
1016
+ }
1017
+ }
645
1018
  async send(agent, terminalControl, text, options = {}) {
646
1019
  const adapter = this.registry.require(agent);
647
1020
  const multiline = /[\r\n]/u.test(text.trimEnd());
@@ -955,6 +1328,13 @@ export class TerminalAgentBridge {
955
1328
  // not let historical Esc instructions in scrollback veto Send.
956
1329
  scrollbackLines: 0
957
1330
  });
1331
+ const asyncQuestionInputMode = inspectCodexAsyncQuestionInputMode(captured.screen);
1332
+ if (asyncQuestionInputMode === "expanded" ||
1333
+ asyncQuestionInputMode === "ambiguous") {
1334
+ throw new Error(asyncQuestionInputMode === "expanded"
1335
+ ? "the Codex async question editor currently owns terminal input; answer, skip, or return to the main prompt before retrying the explicit user Send"
1336
+ : "Codex shows an async-question surface but AKK cannot prove that the main prompt owns terminal input; return to a complete main prompt before retrying the explicit user Send");
1337
+ }
958
1338
  if (captured.inspection.approval.blocked ||
959
1339
  captured.inspection.activity.state === "awaiting_approval" ||
960
1340
  codexBlockingModalVisible(captured.screen)) {
@@ -2743,9 +3123,80 @@ function exactClaudeComposerFrame(screen) {
2743
3123
  .filter((line) => line.trim().length > 0)
2744
3124
  };
2745
3125
  }
3126
+ function exactClaudeModelControlComposerCapture(screen, plan, expectedText) {
3127
+ if (plan.behaviorProfile !== "claude-model-control-2.1.266" ||
3128
+ expectedText !== plan.command ||
3129
+ expectedText !== "/model")
3130
+ return undefined;
3131
+ const frame = exactClaudeComposerFrame(screen);
3132
+ if (!frame ||
3133
+ frame.composerRows.length !== 1 ||
3134
+ frame.composerRows[0].replace(/^\s*❯\s?/u, "").trimEnd() !== expectedText ||
3135
+ frame.trailing.length > 2 ||
3136
+ !claudeNativeInspectionTrailingIsFooter(frame.trailing))
3137
+ return undefined;
3138
+ const beforeComposer = frame.lines.slice(0, frame.openIndex);
3139
+ while (beforeComposer.length > 0 &&
3140
+ beforeComposer.at(-1)?.trim() === "") {
3141
+ beforeComposer.pop();
3142
+ }
3143
+ let popupStart = beforeComposer.length;
3144
+ while (popupStart > 0) {
3145
+ const line = beforeComposer[popupStart - 1];
3146
+ if (/^\s*(?:(?:❯|›)\s*)?\/[a-z][a-z0-9-]*(?:\s{2,}|\s*$)/iu.test(line) ||
3147
+ /^\s{2,}\S/u.test(line)) {
3148
+ popupStart -= 1;
3149
+ continue;
3150
+ }
3151
+ break;
3152
+ }
3153
+ const popupRows = beforeComposer.slice(popupStart);
3154
+ if (popupRows.length === 0 || popupRows.length > 32)
3155
+ return undefined;
3156
+ const suggestions = [];
3157
+ for (const [lineIndex, line] of popupRows.entries()) {
3158
+ const match = /^\s*(?:(❯|›)\s*)?(\/[a-z][a-z0-9-]*)(?:\s{2,}|\s*$)(.*)$/iu
3159
+ .exec(line);
3160
+ if (match) {
3161
+ suggestions.push({
3162
+ lineIndex,
3163
+ selected: Boolean(match[1]),
3164
+ command: match[2],
3165
+ normalized: `${match[2]} ${match[3]}`.trim().replace(/\s+/gu, " ")
3166
+ });
3167
+ continue;
3168
+ }
3169
+ if (suggestions.length === 0 || !/^\s{2,}\S/u.test(line)) {
3170
+ return undefined;
3171
+ }
3172
+ const previous = suggestions.at(-1);
3173
+ previous.normalized = `${previous.normalized} ${line.trim()}`
3174
+ .replace(/\s+/gu, " ");
3175
+ }
3176
+ if (suggestions.length === 0 || suggestions[0].lineIndex !== 0) {
3177
+ return undefined;
3178
+ }
3179
+ const selected = suggestions.filter((row) => row.selected);
3180
+ if (selected.length > 1 ||
3181
+ selected.length === 1 && selected[0].lineIndex !== 0 ||
3182
+ suggestions.slice(1).some((row) => row.command === "/model"))
3183
+ return undefined;
3184
+ const first = suggestions[0].normalized;
3185
+ if (!/^\/model Set the AI model for Claude Code \(currently [A-Za-z0-9][A-Za-z0-9._:/+\[\]\- ]{0,127}\)$/u.test(first)) {
3186
+ return undefined;
3187
+ }
3188
+ return {
3189
+ digest: createHash("sha256")
3190
+ .update(frame.lines.slice(frame.openIndex).join("\n"))
3191
+ .digest("hex")
3192
+ };
3193
+ }
2746
3194
  function claudeNativeInspectionTrailingIsFooter(lines) {
2747
- return lines.length <= 2 && lines.every((line) => /^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
2748
- .test(line));
3195
+ return lines.length <= 2 && lines.every(isClaudeNativeComposerFooterLine);
3196
+ }
3197
+ function isClaudeNativeComposerFooterLine(line) {
3198
+ return /^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
3199
+ .test(line);
2749
3200
  }
2750
3201
  function nativeInspectionScreenFingerprint(screen) {
2751
3202
  return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
@@ -2911,13 +3362,227 @@ function codexBlockingModalVisible(screen) {
2911
3362
  .join("\n");
2912
3363
  return /\b(?:press|use)\s+(?:esc|escape)\s+to\s+(?:cancel|close|dismiss)\b/iu
2913
3364
  .test(tail) ||
2914
- /\besc\s+to\s+cancel\b/iu.test(tail);
3365
+ /\besc\s+to\s+cancel\b/iu.test(tail) ||
3366
+ codexActiveWriterViewerVisible(tail) ||
3367
+ ["expanded", "ambiguous"].includes(inspectCodexAsyncQuestionInputMode(tail));
3368
+ }
3369
+ function codexActiveWriterViewerVisible(styledScreen) {
3370
+ const lines = stripTerminalEscapeSequences(styledScreen)
3371
+ .replace(/\r\n?/gu, "\n")
3372
+ .split("\n")
3373
+ .slice(-80);
3374
+ let titleIndex = -1;
3375
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
3376
+ if (/^ {2}🔒(?:\s|$)/u.test(lines[index])) {
3377
+ titleIndex = index;
3378
+ break;
3379
+ }
3380
+ }
3381
+ if (titleIndex < 0) {
3382
+ return false;
3383
+ }
3384
+ const laterMainComposer = lines.findIndex((line, index) => index > titleIndex && CODEX_COMPOSER_MARKER.test(line));
3385
+ if (laterMainComposer >= 0) {
3386
+ return false;
3387
+ }
3388
+ const frame = lines.slice(titleIndex, titleIndex + 12)
3389
+ .map((line) => line.trim().replace(/\s+/gu, " "))
3390
+ .filter((line) => line.length > 0)
3391
+ .join(" ");
3392
+ return /🔒\s+This conversation is open in another app\s+\S+ to Retry/iu
3393
+ .test(frame) &&
3394
+ /Close it there and press\s+\S+\s+to continue here\./iu.test(frame) &&
3395
+ /\S+ retry\s+\S+ exit(?:\s+\S+ transcript)?/iu.test(frame);
3396
+ }
3397
+ const CODEX_ASYNC_QUESTION_FOOTER_LABEL = /\s(submit|skip|main prompt|prev question|next question|queued messages)(?=\s{2,}|\s*$)/giu;
3398
+ function codexStructuredAsyncFooterEvidence(lines, evidenceFloor) {
3399
+ let strongest = { index: -1, labelCount: 0 };
3400
+ for (let start = evidenceFloor + 1; start < lines.length; start += 1) {
3401
+ const labels = new Set();
3402
+ for (let end = start; end < Math.min(lines.length, start + 5); end += 1) {
3403
+ const line = lines[end];
3404
+ if (!/^ {2}(?![ ↳?])\S/u.test(line)) {
3405
+ break;
3406
+ }
3407
+ for (const match of line.matchAll(CODEX_ASYNC_QUESTION_FOOTER_LABEL)) {
3408
+ labels.add(match[1].toLowerCase());
3409
+ }
3410
+ if (labels.size > strongest.labelCount) {
3411
+ strongest = { index: end, labelCount: labels.size };
3412
+ }
3413
+ if (labels.size >= 2) {
3414
+ return strongest;
3415
+ }
3416
+ }
3417
+ }
3418
+ return strongest;
3419
+ }
3420
+ function codexMainComposerFooterIndex(lines, composerIndex) {
3421
+ for (let index = lines.length - 1; index > composerIndex; index -= 1) {
3422
+ // A real statusline starts at column zero. Draft continuations are inset,
3423
+ // even when their text happens to look exactly like a model statusline.
3424
+ if (CODEX_COMPLETE_COMPOSER_FOOTER.test(lines[index].trimEnd())) {
3425
+ return index;
3426
+ }
3427
+ }
3428
+ return -1;
3429
+ }
3430
+ /**
3431
+ * Classify Codex 0.154's non-blocking inline-question surface by current input
3432
+ * ownership. The shared `Queued follow-up inputs` heading is not sufficient:
3433
+ * ordinary queued messages use it too. An exact collapsed summary, after any
3434
+ * ordinary queue preview is removed, proves Send-safe ownership even when the
3435
+ * Composer is outside the viewport. Positive editor evidence without a
3436
+ * complete shape remains fail-closed because paste plus Enter would otherwise
3437
+ * submit the user's task as an answer.
3438
+ */
3439
+ export function inspectCodexAsyncQuestionInputMode(styledScreen) {
3440
+ const lines = stripTerminalEscapeSequences(styledScreen)
3441
+ .replace(/\r\n?/gu, "\n")
3442
+ .replace(/\u00a0/gu, " ")
3443
+ .split("\n")
3444
+ .slice(-80);
3445
+ while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
3446
+ lines.pop();
3447
+ }
3448
+ const normalized = lines.map((line) => line.trim().replace(/\s+/gu, " "));
3449
+ let mainComposerIndex = -1;
3450
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
3451
+ // The main Composer marker is column zero. Async option and inline-text
3452
+ // markers are inset by the menu surface, even after ANSI is removed.
3453
+ if (CODEX_COMPOSER_MARKER.test(lines[index])) {
3454
+ mainComposerIndex = index;
3455
+ break;
3456
+ }
3457
+ }
3458
+ let headerIndex = -1;
3459
+ let wrappedHeaderRows = 1;
3460
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
3461
+ if (/^•\s+Queued follow-up inputs\s*$/u.test(lines[index]) ||
3462
+ /^•\s+Queued follow-up inpu…\s*$/u.test(lines[index]) ||
3463
+ (/^•\s+Queued follow-up\s*$/u.test(lines[index]) &&
3464
+ /^ {2}inputs\s*$/u.test(lines[index + 1] ?? ""))) {
3465
+ headerIndex = index;
3466
+ wrappedHeaderRows = /^•\s+Queued follow-up\s*$/u.test(lines[index])
3467
+ ? 2
3468
+ : 1;
3469
+ break;
3470
+ }
3471
+ }
3472
+ const headerBodyIndex = headerIndex < 0
3473
+ ? -1
3474
+ : headerIndex + wrappedHeaderRows;
3475
+ let questionEvidenceStart = headerBodyIndex;
3476
+ if (headerIndex >= 0) {
3477
+ let cursor = headerBodyIndex;
3478
+ while (cursor < lines.length && lines[cursor].trim().length === 0) {
3479
+ cursor += 1;
3480
+ }
3481
+ let sawQueuedPreview = false;
3482
+ while (cursor < lines.length) {
3483
+ if (/^ {2}↳\s/u.test(lines[cursor])) {
3484
+ sawQueuedPreview = true;
3485
+ cursor += 1;
3486
+ while (cursor < lines.length) {
3487
+ if (lines[cursor].trim().length === 0 ||
3488
+ /^ {4,}\S/u.test(lines[cursor])) {
3489
+ cursor += 1;
3490
+ continue;
3491
+ }
3492
+ break;
3493
+ }
3494
+ continue;
3495
+ }
3496
+ break;
3497
+ }
3498
+ if (sawQueuedPreview) {
3499
+ questionEvidenceStart = cursor;
3500
+ }
3501
+ }
3502
+ const mainComposerFooterIndex = mainComposerIndex < 0
3503
+ ? -1
3504
+ : codexMainComposerFooterIndex(lines, mainComposerIndex);
3505
+ const summaryBeforeMainComposer = headerIndex < 0 || mainComposerIndex < 0
3506
+ ? -1
3507
+ : normalized.findIndex((line, index) => index >= questionEvidenceStart && index < mainComposerIndex &&
3508
+ /^\?\s+\d+\s+questions?(?:\s+·\s+\d+s)?$/iu.test(line));
3509
+ if (mainComposerIndex >= 0 && mainComposerFooterIndex < 0) {
3510
+ // A statusline-disabled Composer is the strongest available ownership
3511
+ // anchor. Treat all following wrapped rows as its arbitrary draft instead
3512
+ // of letting question-like user text veto a human-priority Send.
3513
+ return summaryBeforeMainComposer >= 0 ? "collapsed" : "absent";
3514
+ }
3515
+ const evidenceFloor = Math.max(mainComposerFooterIndex, questionEvidenceStart - 1);
3516
+ const structuredFooter = codexStructuredAsyncFooterEvidence(lines, evidenceFloor);
3517
+ if (structuredFooter.labelCount >= 2) {
3518
+ return "expanded";
3519
+ }
3520
+ const clippedFooterIndex = normalized.findIndex((line, index) => index > evidenceFloor && /^ente(?:r)?…$/iu.test(line) &&
3521
+ /^ctrl…$/iu.test(normalized[index + 1] ?? ""));
3522
+ const clippedChoiceIndex = normalized.findIndex((line, index) => index > evidenceFloor &&
3523
+ line === "Expand terminal to read the entire option");
3524
+ if (clippedFooterIndex >= 0 || clippedChoiceIndex >= 0) {
3525
+ return "ambiguous";
3526
+ }
3527
+ if (headerIndex < 0) {
3528
+ const evidence = new Set();
3529
+ normalized.forEach((line, index) => {
3530
+ if (index <= evidenceFloor) {
3531
+ return;
3532
+ }
3533
+ if (/^\d+\s+of(?:\s+\d+)?$/iu.test(line)) {
3534
+ evidence.add("progress");
3535
+ }
3536
+ if (/^›\s+\S/u.test(line)) {
3537
+ evidence.add("selector");
3538
+ }
3539
+ if (/^Type your answer(?:\s*\(optional\))?$/iu.test(line)) {
3540
+ evidence.add("free_text");
3541
+ }
3542
+ });
3543
+ return evidence.size >= 2 ||
3544
+ (evidence.size >= 1 && structuredFooter.labelCount >= 1)
3545
+ ? "ambiguous"
3546
+ : "absent";
3547
+ }
3548
+ const weakQuestionIndex = normalized.findIndex((line, index) => index > evidenceFloor && index >= questionEvidenceStart && (/^\d+\s+of(?:\s+\d+)?$/iu.test(line) ||
3549
+ /^›\s+\S/u.test(line) ||
3550
+ /^Type your answer(?:\s*\(optional\))?$/iu.test(line)));
3551
+ if (mainComposerIndex > headerIndex && weakQuestionIndex > evidenceFloor) {
3552
+ return "ambiguous";
3553
+ }
3554
+ const summaryIndex = normalized.findIndex((line, index) => index >= questionEvidenceStart &&
3555
+ (mainComposerIndex < 0 || index < mainComposerIndex) &&
3556
+ /^\?\s+\d+\s+questions?(?:\s+·\s+\d+s)?$/iu.test(line));
3557
+ if (summaryIndex >= 0) {
3558
+ return weakQuestionIndex >= 0 ? "ambiguous" : "collapsed";
3559
+ }
3560
+ if (mainComposerIndex > headerIndex) {
3561
+ return "absent";
3562
+ }
3563
+ const afterHeader = normalized.slice(questionEvidenceStart)
3564
+ .filter((line) => line.length > 0);
3565
+ const positiveAfterHeader = afterHeader.some((line) => /^\d+\s+of(?:\s+\d+)?$/iu.test(line) ||
3566
+ /^›\s+\d+\.(?:\s|$)/u.test(line) ||
3567
+ /^Type your answer(?:\s*\(optional\))?$/iu.test(line));
3568
+ if (positiveAfterHeader) {
3569
+ return "ambiguous";
3570
+ }
3571
+ // A bare heading (optionally followed by ↳ queued-message previews) is not
3572
+ // question authority. Preserve human-explicit Send in that ordinary state.
3573
+ if (afterHeader.length === 0) {
3574
+ return "absent";
3575
+ }
3576
+ // An expanded editor can temporarily replace its footer with a flash. If
3577
+ // there is question-like content under the shared heading and no proven main
3578
+ // Composer, do not guess which input surface will receive the next paste.
3579
+ return "ambiguous";
2915
3580
  }
2916
3581
  /**
2917
3582
  * Classify only the bottom live Codex composer region. A matching transcript
2918
3583
  * prompt elsewhere in scrollback is deliberately ignored.
2919
3584
  */
2920
- function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false) {
3585
+ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false, exactSlashPopupRows) {
2921
3586
  const screen = stripTerminalEscapeSequences(styledScreen);
2922
3587
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2923
3588
  while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
@@ -2959,10 +3624,29 @@ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLarg
2959
3624
  if (positivelyEmpty) {
2960
3625
  return { state: "exact_empty", digest };
2961
3626
  }
2962
- if (footerIndex < 0) {
3627
+ const expectedComparable = composerComparableText(expectedText);
3628
+ const exactStyledProfiledCommand = exactSlashPopupRows !== undefined &&
3629
+ exactCodexStyledCommandComposerCapture(styledScreen, expectedText) !== undefined;
3630
+ const exactFooterlessProfiledCommand = footerIndex < 0 &&
3631
+ exactStyledProfiledCommand;
3632
+ // Codex 0.154 replaces the ordinary model/cwd footer with its slash
3633
+ // completion surface. Real terminal renderers may retain one or more blank
3634
+ // layout rows between the Composer and that surface. The exact styled
3635
+ // command is sufficient only for reversible cleanup; Enter additionally
3636
+ // requires the unique, ordered, version-profiled completion rows below.
3637
+ const popupRows = region.slice(1).map((row) => row.trimEnd());
3638
+ const exactPopupLayout = exactSlashPopupRows !== undefined && (JSON.stringify(popupRows) === JSON.stringify(exactSlashPopupRows) ||
3639
+ popupRows.length === exactSlashPopupRows.length + 1 &&
3640
+ popupRows[0] === "" &&
3641
+ JSON.stringify(popupRows.slice(1)) ===
3642
+ JSON.stringify(exactSlashPopupRows));
3643
+ const exactProfiledSlashPopup = exactSlashPopupRows !== undefined &&
3644
+ composerComparableText(bodyRows[0] ?? "").trimEnd() ===
3645
+ expectedComparable &&
3646
+ exactPopupLayout;
3647
+ if (footerIndex < 0 && !exactFooterlessProfiledCommand) {
2963
3648
  return undefined;
2964
3649
  }
2965
- const expectedComparable = composerComparableText(expectedText);
2966
3650
  const expectedCharacterCount = Array.from(expectedText).length;
2967
3651
  const comparable = composerComparableText(bodyRows.join("\n"));
2968
3652
  const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
@@ -2971,17 +3655,67 @@ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLarg
2971
3655
  ? { state: "different_draft", digest }
2972
3656
  : undefined;
2973
3657
  }
2974
- const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
3658
+ // Herdr's ANSI visible buffer preserves Codex's fixed-width Composer paint:
3659
+ // the typed command is followed by layout padding through the viewport edge.
3660
+ // Accept that padding only when the closed 0.154 profile, full-row
3661
+ // background paint, exact command text, and complete model/cwd footer all
3662
+ // agree. Plain padded text remains untrusted.
3663
+ const exactVisibleDraft = footerIndex >= 0 && (terminalComposerRowsMatchExpected(bodyRows, expectedComparable) ||
3664
+ exactStyledProfiledCommand);
2975
3665
  const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
2976
3666
  expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
2977
3667
  comparable === composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
2978
- if (exactVisibleDraft || exactLargePastePlaceholder) {
2979
- return { state: "exact_draft", digest };
3668
+ if (exactVisibleDraft ||
3669
+ exactLargePastePlaceholder ||
3670
+ exactProfiledSlashPopup ||
3671
+ exactFooterlessProfiledCommand) {
3672
+ return {
3673
+ state: "exact_draft",
3674
+ digest,
3675
+ ...(exactProfiledSlashPopup ? { profiledSlashPopup: true } : {}),
3676
+ ...(exactVisibleDraft && footerIndex >= 0
3677
+ ? { bareCommand: true }
3678
+ : {})
3679
+ };
2980
3680
  }
2981
3681
  return comparable.length > 0
2982
3682
  ? { state: "different_draft", digest }
2983
3683
  : undefined;
2984
3684
  }
3685
+ /**
3686
+ * Prove the live footerless Codex slash Composer without trusting transcript
3687
+ * text alone. The 0.154 TUI paints this row across the current viewport while
3688
+ * its completion popup replaces the ordinary model/cwd footer.
3689
+ */
3690
+ function exactCodexStyledCommandComposerCapture(styledScreen, expectedText) {
3691
+ const rows = styledScreen.replace(/\r\n?/gu, "\n").split("\n");
3692
+ const viewportColumns = inferCodexVisibleViewportColumns(styledScreen);
3693
+ if (!viewportColumns)
3694
+ return undefined;
3695
+ let composerLine;
3696
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
3697
+ const plain = stripTerminalEscapeSequences(rows[index]);
3698
+ if (CODEX_COMPOSER_MARKER.test(plain)) {
3699
+ composerLine = rows[index];
3700
+ break;
3701
+ }
3702
+ }
3703
+ if (!composerLine)
3704
+ return undefined;
3705
+ const plain = stripTerminalEscapeSequences(composerLine);
3706
+ const exactCommand = plain.trimEnd().replace(/^[›»]\s?/u, "") ===
3707
+ expectedText;
3708
+ const spansViewport = Array.from(plain).length === viewportColumns &&
3709
+ plain.endsWith(" ");
3710
+ const hasBackground = [...composerLine.matchAll(/\x1b\[([0-9;]*)m/gu)]
3711
+ .some((match) => match[1].split(";").map(Number).includes(48));
3712
+ if (!exactCommand || !spansViewport || !hasBackground) {
3713
+ return undefined;
3714
+ }
3715
+ return {
3716
+ digest: createHash("sha256").update(composerLine).digest("hex")
3717
+ };
3718
+ }
2985
3719
  function exactCodexComposerCapture(screen, expectedText) {
2986
3720
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2987
3721
  const expectedComparable = composerComparableText(expectedText);
@@ -3398,6 +4132,9 @@ export function captureTerminalInteractionRuntimeOffer(input) {
3398
4132
  input.approvalBlocked === true) {
3399
4133
  return undefined;
3400
4134
  }
4135
+ if (exactCurrentModelControlSurface(input.agent, agentVersion, input.screen)) {
4136
+ return undefined;
4137
+ }
3401
4138
  const coreOffer = captureTerminalInteraction({
3402
4139
  subject,
3403
4140
  agent: input.agent,
@@ -3425,6 +4162,21 @@ export function captureTerminalInteractionRuntimeOffer(input) {
3425
4162
  nativeInspection: coreOffer.nativeInspection
3426
4163
  };
3427
4164
  }
4165
+ function exactCurrentModelControlSurface(agent, agentVersion, screen) {
4166
+ if (!agentVersion)
4167
+ return false;
4168
+ try {
4169
+ const capabilities = probeTerminalModelControl(agent, agentVersion);
4170
+ if (capabilities.status !== "supported")
4171
+ return false;
4172
+ const observation = observeTerminalModelControl(planTerminalModelControl(capabilities), screen);
4173
+ return observation.state !== "none" &&
4174
+ observation.state !== "ambiguous";
4175
+ }
4176
+ catch {
4177
+ return false;
4178
+ }
4179
+ }
3428
4180
  function terminalInteractionTrustedWatchIdentity(terminalControl, runtime, evidence) {
3429
4181
  if (runtime.interactionSubject?.kind !== "terminal_watch" ||
3430
4182
  !evidence ||
@@ -3708,7 +4460,18 @@ async function dispatchTerminalInteractionAnswer(input) {
3708
4460
  throw new TerminalInteractionInputNotStartedError("terminal interaction answer does not have a closed adapter action");
3709
4461
  }
3710
4462
  function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
3711
- const approval = dispatchableApprovalInspection(adapter, inspection.approval);
4463
+ const exactModelSurface = options.screen !== undefined &&
4464
+ exactCurrentModelControlSurface(adapter.agent, options.runtime?.agentVersion, options.screen);
4465
+ const modelSurfaceReason = exactModelSurface
4466
+ ? `exact ${adapter.displayName} native model-control surface is open`
4467
+ : undefined;
4468
+ const approval = exactModelSurface
4469
+ ? {
4470
+ blocked: false,
4471
+ approvable: false,
4472
+ reason: modelSurfaceReason
4473
+ }
4474
+ : dispatchableApprovalInspection(adapter, inspection.approval);
3712
4475
  const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, {
3713
4476
  ...inspection,
3714
4477
  approval
@@ -3726,7 +4489,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3726
4489
  }] : [];
3727
4490
  })
3728
4491
  : [];
3729
- const interaction = options.screen === undefined
4492
+ const interaction = options.screen === undefined || exactModelSurface
3730
4493
  ? undefined
3731
4494
  : captureTerminalInteractionRuntimeOffer({
3732
4495
  agent: adapter.agent,
@@ -3742,10 +4505,10 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3742
4505
  agent: adapter.agent,
3743
4506
  reachable: true,
3744
4507
  capabilities: adapter.capabilities,
3745
- activity_state: inspection.activity.state,
3746
- activity_reason: inspection.activity.reason,
3747
- screen_state: inspection.activity.state,
3748
- screen_reason: inspection.activity.reason,
4508
+ activity_state: exactModelSurface ? "unknown" : inspection.activity.state,
4509
+ activity_reason: modelSurfaceReason ?? inspection.activity.reason,
4510
+ screen_state: exactModelSurface ? "unknown" : inspection.activity.state,
4511
+ screen_reason: modelSurfaceReason ?? inspection.activity.reason,
3749
4512
  approval_state: {
3750
4513
  scanned: true,
3751
4514
  blocked: approval.blocked,
@@ -3890,4 +4653,33 @@ function unsupportedScreenStatus(adapter, terminalControl) {
3890
4653
  capability_limitation: reason
3891
4654
  };
3892
4655
  }
4656
+ function assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan) {
4657
+ if (!terminalControl.capabilities.includes("send_keys") ||
4658
+ !terminalControl.capabilities.includes("screen_status")) {
4659
+ throw new Error("terminal model control requires exact screen-status and key-delivery capabilities");
4660
+ }
4661
+ const capabilities = adapter.probeModelControl?.(agentVersion);
4662
+ if (capabilities?.status !== "supported" ||
4663
+ capabilities.modelSelection !== true ||
4664
+ capabilities.reasoningEffortSelection !== true ||
4665
+ capabilities.agentVersion !== agentVersion) {
4666
+ throw new Error(capabilities?.reason ??
4667
+ `${adapter.displayName} has no verified model-control profile`);
4668
+ }
4669
+ const expected = adapter.planModelControl?.(capabilities);
4670
+ if (!expected ||
4671
+ expected.command !== "/model" ||
4672
+ expected.requiresIdle !== true ||
4673
+ expected.requiresExactEmptyComposer !== true ||
4674
+ JSON.stringify(expected) !== JSON.stringify(plan)) {
4675
+ throw new Error("the terminal adapter did not produce the exact current model-control plan");
4676
+ }
4677
+ if (adapter.agent === "codex"
4678
+ ? plan.behaviorProfile !== "codex-model-control-0.154.0" ||
4679
+ plan.scope !== "current_and_new_sessions"
4680
+ : plan.behaviorProfile !== "claude-model-control-2.1.266" ||
4681
+ plan.scope !== "current_session") {
4682
+ throw new Error("refusing an unprofiled terminal model-control plan");
4683
+ }
4684
+ }
3893
4685
  //# sourceMappingURL=terminal-agent-bridge.js.map