@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.
- package/CHANGELOG.md +38 -0
- package/README.md +17 -4
- package/dist/src/claude-terminal-agent-adapter.js +4 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli-core.js +9 -1
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js +4 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/herdr-terminal-control-provider.js +6 -1
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/host-adapter.d.ts +1 -1
- package/dist/src/host-adapter.js +1 -1
- package/dist/src/host-bridge-tools.js +2 -2
- package/dist/src/managed-session.js +2 -18
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js +578 -2
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
- package/dist/src/native-thread-transition-verification-adapter.js +1 -0
- package/dist/src/native-thread-transition-verification-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-command-adapter.js +326 -36
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -0
- package/dist/src/openclaw-plugin-helpers.js +682 -2
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +53 -0
- package/dist/src/openclaw-plugin-schemas.js +54 -1
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +83 -1
- package/dist/src/terminal-action-projection.js +174 -0
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +7 -0
- package/dist/src/terminal-agent-adapter.js +8 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +40 -0
- package/dist/src/terminal-agent-bridge.js +808 -16
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-ref.d.ts +23 -0
- package/dist/src/terminal-control-ref.js +32 -0
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.js +324 -16
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +1 -0
- package/dist/src/terminal-list-renderer.js +87 -3
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-model-control.d.ts +312 -0
- package/dist/src/terminal-model-control.js +1595 -0
- package/dist/src/terminal-model-control.js.map +1 -0
- package/dist/src/terminal-questionnaire-adapter.d.ts +3 -1
- package/dist/src/terminal-questionnaire-adapter.js +27 -19
- package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
- package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-runtime-cli-adapter.js +59 -17
- package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
- package/docs/quickstart-herdr.md +1 -1
- package/docs/quickstart-tmux.md +28 -3
- package/openclaw.plugin.json +13 -1
- package/package.json +1 -1
- 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
|
-
|
|
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(
|
|
2748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
2979
|
-
|
|
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
|
|
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
|