@scotthuang/agent-knock-knock 0.13.2 → 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 (54) hide show
  1. package/CHANGELOG.md +22 -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-terminal-agent-adapter.js +4 -0
  8. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  9. package/dist/src/herdr-terminal-control-provider.js +6 -1
  10. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  11. package/dist/src/host-adapter.d.ts +1 -1
  12. package/dist/src/host-adapter.js +1 -1
  13. package/dist/src/host-bridge-tools.js +2 -2
  14. package/dist/src/managed-session.js +2 -18
  15. package/dist/src/managed-session.js.map +1 -1
  16. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
  17. package/dist/src/native-thread-lifecycle-cli-adapter.js +578 -2
  18. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  19. package/dist/src/openclaw-plugin-command-adapter.js +325 -35
  20. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  21. package/dist/src/openclaw-plugin-helpers.d.ts +27 -0
  22. package/dist/src/openclaw-plugin-helpers.js +682 -2
  23. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  24. package/dist/src/openclaw-plugin-schemas.d.ts +53 -0
  25. package/dist/src/openclaw-plugin-schemas.js +53 -0
  26. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  27. package/dist/src/terminal-action-projection.d.ts +83 -1
  28. package/dist/src/terminal-action-projection.js +174 -0
  29. package/dist/src/terminal-action-projection.js.map +1 -1
  30. package/dist/src/terminal-agent-adapter.d.ts +7 -0
  31. package/dist/src/terminal-agent-adapter.js +8 -0
  32. package/dist/src/terminal-agent-adapter.js.map +1 -1
  33. package/dist/src/terminal-agent-bridge.d.ts +29 -0
  34. package/dist/src/terminal-agent-bridge.js +580 -14
  35. package/dist/src/terminal-agent-bridge.js.map +1 -1
  36. package/dist/src/terminal-control-ref.d.ts +23 -0
  37. package/dist/src/terminal-control-ref.js +32 -0
  38. package/dist/src/terminal-control-ref.js.map +1 -1
  39. package/dist/src/terminal-list-cli-adapter.js +324 -16
  40. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  41. package/dist/src/terminal-list-renderer.d.ts +1 -0
  42. package/dist/src/terminal-list-renderer.js +87 -3
  43. package/dist/src/terminal-list-renderer.js.map +1 -1
  44. package/dist/src/terminal-model-control.d.ts +312 -0
  45. package/dist/src/terminal-model-control.js +1595 -0
  46. package/dist/src/terminal-model-control.js.map +1 -0
  47. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
  48. package/dist/src/terminal-runtime-cli-adapter.js +59 -17
  49. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  50. package/docs/quickstart-herdr.md +1 -1
  51. package/docs/quickstart-tmux.md +27 -2
  52. package/openclaw.plugin.json +13 -1
  53. package/package.json +1 -1
  54. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +54 -11
@@ -5,6 +5,8 @@ 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
+ import { inspectNativeQuestionnaire } from "./terminal-questionnaire-adapter.js";
9
+ import { discoverTerminalModelOptions, inspectTerminalModelControlResidual, observeTerminalModelControl, planTerminalModelControl, probeTerminalModelControl, repairTerminalModelControlResidual, switchTerminalModel } from "./terminal-model-control.js";
8
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
@@ -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.
@@ -647,6 +654,367 @@ export class TerminalAgentBridge {
647
654
  };
648
655
  }
649
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
+ }
650
1018
  async send(agent, terminalControl, text, options = {}) {
651
1019
  const adapter = this.registry.require(agent);
652
1020
  const multiline = /[\r\n]/u.test(text.trimEnd());
@@ -2755,9 +3123,80 @@ function exactClaudeComposerFrame(screen) {
2755
3123
  .filter((line) => line.trim().length > 0)
2756
3124
  };
2757
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
+ }
2758
3194
  function claudeNativeInspectionTrailingIsFooter(lines) {
2759
- return lines.length <= 2 && lines.every((line) => /^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
2760
- .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);
2761
3200
  }
2762
3201
  function nativeInspectionScreenFingerprint(screen) {
2763
3202
  return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
@@ -3143,7 +3582,7 @@ export function inspectCodexAsyncQuestionInputMode(styledScreen) {
3143
3582
  * Classify only the bottom live Codex composer region. A matching transcript
3144
3583
  * prompt elsewhere in scrollback is deliberately ignored.
3145
3584
  */
3146
- function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false) {
3585
+ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false, exactSlashPopupRows) {
3147
3586
  const screen = stripTerminalEscapeSequences(styledScreen);
3148
3587
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3149
3588
  while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
@@ -3185,10 +3624,29 @@ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLarg
3185
3624
  if (positivelyEmpty) {
3186
3625
  return { state: "exact_empty", digest };
3187
3626
  }
3188
- 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) {
3189
3648
  return undefined;
3190
3649
  }
3191
- const expectedComparable = composerComparableText(expectedText);
3192
3650
  const expectedCharacterCount = Array.from(expectedText).length;
3193
3651
  const comparable = composerComparableText(bodyRows.join("\n"));
3194
3652
  const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
@@ -3197,17 +3655,67 @@ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLarg
3197
3655
  ? { state: "different_draft", digest }
3198
3656
  : undefined;
3199
3657
  }
3200
- 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);
3201
3665
  const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
3202
3666
  expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
3203
3667
  comparable === composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
3204
- if (exactVisibleDraft || exactLargePastePlaceholder) {
3205
- 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
+ };
3206
3680
  }
3207
3681
  return comparable.length > 0
3208
3682
  ? { state: "different_draft", digest }
3209
3683
  : undefined;
3210
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
+ }
3211
3719
  function exactCodexComposerCapture(screen, expectedText) {
3212
3720
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3213
3721
  const expectedComparable = composerComparableText(expectedText);
@@ -3624,6 +4132,9 @@ export function captureTerminalInteractionRuntimeOffer(input) {
3624
4132
  input.approvalBlocked === true) {
3625
4133
  return undefined;
3626
4134
  }
4135
+ if (exactCurrentModelControlSurface(input.agent, agentVersion, input.screen)) {
4136
+ return undefined;
4137
+ }
3627
4138
  const coreOffer = captureTerminalInteraction({
3628
4139
  subject,
3629
4140
  agent: input.agent,
@@ -3651,6 +4162,21 @@ export function captureTerminalInteractionRuntimeOffer(input) {
3651
4162
  nativeInspection: coreOffer.nativeInspection
3652
4163
  };
3653
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
+ }
3654
4180
  function terminalInteractionTrustedWatchIdentity(terminalControl, runtime, evidence) {
3655
4181
  if (runtime.interactionSubject?.kind !== "terminal_watch" ||
3656
4182
  !evidence ||
@@ -3934,7 +4460,18 @@ async function dispatchTerminalInteractionAnswer(input) {
3934
4460
  throw new TerminalInteractionInputNotStartedError("terminal interaction answer does not have a closed adapter action");
3935
4461
  }
3936
4462
  function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
3937
- 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);
3938
4475
  const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, {
3939
4476
  ...inspection,
3940
4477
  approval
@@ -3952,7 +4489,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3952
4489
  }] : [];
3953
4490
  })
3954
4491
  : [];
3955
- const interaction = options.screen === undefined
4492
+ const interaction = options.screen === undefined || exactModelSurface
3956
4493
  ? undefined
3957
4494
  : captureTerminalInteractionRuntimeOffer({
3958
4495
  agent: adapter.agent,
@@ -3968,10 +4505,10 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3968
4505
  agent: adapter.agent,
3969
4506
  reachable: true,
3970
4507
  capabilities: adapter.capabilities,
3971
- activity_state: inspection.activity.state,
3972
- activity_reason: inspection.activity.reason,
3973
- screen_state: inspection.activity.state,
3974
- 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,
3975
4512
  approval_state: {
3976
4513
  scanned: true,
3977
4514
  blocked: approval.blocked,
@@ -4116,4 +4653,33 @@ function unsupportedScreenStatus(adapter, terminalControl) {
4116
4653
  capability_limitation: reason
4117
4654
  };
4118
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
+ }
4119
4685
  //# sourceMappingURL=terminal-agent-bridge.js.map