crewly 1.20.200 → 1.20.201
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/config/constants.ts +2 -0
- package/config/skills/agent/screenshot-compare/execute.sh +8 -2
- package/config/skills/agent/screenshot-compare/execute.test.sh +77 -0
- package/config/skills/agent/transcribe-audio/SKILL.md +1 -1
- package/config/skills/agent/transcribe-audio/execute.sh +13 -4
- package/config/skills/agent/transcribe-audio/execute.test.sh +39 -0
- package/config/skills/agent/transcribe-audio/instructions.md +1 -1
- package/config/skills/registry.json +3 -3
- package/dist/backend/backend/src/constants.d.ts +28 -4
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +27 -4
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts +32 -10
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +47 -16
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js +20 -7
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.d.ts +3 -1
- package/dist/backend/backend/src/controllers/session/session.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.js +7 -2
- package/dist/backend/backend/src/controllers/session/session.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.routes.js +15 -4
- package/dist/backend/backend/src/controllers/session/session.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/settings/settings.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/settings/settings.controller.js +91 -16
- package/dist/backend/backend/src/controllers/settings/settings.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +4 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +52 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +125 -11
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/antigravity-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/antigravity-runtime.service.js +3 -2
- package/dist/backend/backend/src/services/agent/antigravity-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.d.ts +17 -0
- package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.js +23 -0
- package/dist/backend/backend/src/services/agent/pty-activity-tracker.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts +14 -1
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js +23 -1
- package/dist/backend/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +10 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +22 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts +109 -15
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js +244 -41
- package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/backend/backend/src/services/session/tui-input-guard.d.ts +57 -0
- package/dist/backend/backend/src/services/session/tui-input-guard.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/tui-input-guard.js +104 -7
- package/dist/backend/backend/src/services/session/tui-input-guard.js.map +1 -1
- package/dist/backend/backend/src/types/settings.types.d.ts +24 -0
- package/dist/backend/backend/src/types/settings.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/settings.types.js +65 -3
- package/dist/backend/backend/src/types/settings.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +2 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +2 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +28 -4
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +27 -4
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +52 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +125 -11
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/antigravity-runtime.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/antigravity-runtime.service.js +3 -2
- package/dist/cli/backend/src/services/agent/antigravity-runtime.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts +17 -0
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js +23 -0
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts +14 -1
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js +23 -1
- package/dist/cli/backend/src/services/messaging/input-blocked-retry.service.js.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session.d.ts +10 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/pty/pty-session.js +22 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts +109 -15
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/session-command-helper.js +244 -41
- package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/cli/backend/src/services/session/tui-input-guard.d.ts +57 -0
- package/dist/cli/backend/src/services/session/tui-input-guard.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/tui-input-guard.js +104 -7
- package/dist/cli/backend/src/services/session/tui-input-guard.js.map +1 -1
- package/dist/cli/backend/src/types/settings.types.d.ts +24 -0
- package/dist/cli/backend/src/types/settings.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/settings.types.js +65 -3
- package/dist/cli/backend/src/types/settings.types.js.map +1 -1
- package/dist/cli/config/constants.d.ts +2 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +2 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -31,16 +31,36 @@ export declare class SessionCommandHelper {
|
|
|
31
31
|
private logger;
|
|
32
32
|
private backend;
|
|
33
33
|
/**
|
|
34
|
-
* Per session: the
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* ours
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
* Per session: the harness's own latest paste into a box it had proven
|
|
35
|
+
* empty. While the box keeps showing it (the collapsed marker "[Pasted
|
|
36
|
+
* text #4 +5 lines]" / "[Pasted Content 1234 chars]", or the text itself)
|
|
37
|
+
* it is ours — whatever message is sent next — so a lost or never-pressed
|
|
38
|
+
* Enter can be recovered. Written by every paste path; shared across
|
|
39
|
+
* helper instances.
|
|
40
|
+
*
|
|
41
|
+
* - `pending`: pasted, not yet seen in the box (a busy Claude Code renders
|
|
42
|
+
* a paste seconds late). The first readable box that is not empty must
|
|
43
|
+
* show the paste's shape, or the record ends; it also ends unseen after
|
|
44
|
+
* OWN_PASTE_PENDING_MAX_MS.
|
|
45
|
+
* - `seen`: the box showed `shown`. Every readable box since must show
|
|
46
|
+
* exactly that; the first that does not ends the record. Unreadable
|
|
47
|
+
* screens (a dialog) do not count as seen: unseen for
|
|
48
|
+
* OWN_PASTE_UNSEEN_MAX_MS ends it too. While busy the watcher re-reads
|
|
49
|
+
* the box, so a long turn keeps it alive.
|
|
50
|
+
*
|
|
51
|
+
* Also ended by our one Enter on it, and by session/runtime (re)start.
|
|
52
|
+
*/
|
|
53
|
+
private static readonly ownPastes;
|
|
54
|
+
/** Sessions a sendMessage is typing into right now (the watcher keeps off) */
|
|
55
|
+
private static readonly inFlight;
|
|
56
|
+
/** The watcher re-reading boxes that hold a paste of ours */
|
|
57
|
+
private static watchTimer;
|
|
58
|
+
/** Clock for the paste record (tests). */
|
|
41
59
|
static now: () => number;
|
|
60
|
+
/** How long sendMessage waits for a late paste to render (tests shorten it). */
|
|
61
|
+
static pasteRenderMaxWaitMs: number;
|
|
42
62
|
/**
|
|
43
|
-
* Forget the recorded paste
|
|
63
|
+
* Forget the recorded paste of a session: its runtime is being
|
|
44
64
|
* (re)started, a shell line is typed, or the session is created/killed —
|
|
45
65
|
* Claude Code's paste counter restarts at #1, and Codex markers carry only
|
|
46
66
|
* a character count, so an old record could match the owner's paste.
|
|
@@ -49,13 +69,58 @@ export declare class SessionCommandHelper {
|
|
|
49
69
|
*/
|
|
50
70
|
static forgetOwnPaste(sessionName: string): void;
|
|
51
71
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
72
|
+
* Whether a paste of ours is on record for a session (tests, diagnostics).
|
|
73
|
+
*
|
|
74
|
+
* @param sessionName - The session
|
|
75
|
+
* @returns True while the record stands
|
|
76
|
+
*/
|
|
77
|
+
static hasOwnPaste(sessionName: string): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Record our paste of `message` into a box proven empty.
|
|
80
|
+
*
|
|
81
|
+
* @param sessionName - The session
|
|
82
|
+
* @param message - What was pasted
|
|
83
|
+
*/
|
|
84
|
+
private recordOwnPaste;
|
|
85
|
+
/** Whether recording a paste starts the timed watcher (tests drive it by hand). */
|
|
86
|
+
static autoWatch: boolean;
|
|
87
|
+
/** Start the watcher (no-op when running). */
|
|
88
|
+
private static startWatching;
|
|
89
|
+
/** Stop the watcher once no paste is on record. */
|
|
90
|
+
private static stopWatchingIfIdle;
|
|
91
|
+
/**
|
|
92
|
+
* One watcher pass: re-read every box holding a paste of ours (keeping
|
|
93
|
+
* the "seen continuously" record alive through a long turn, ending it the
|
|
94
|
+
* moment the box shows anything else), and once the agent is idle with
|
|
95
|
+
* our paste still sitting there, press Enter on it once.
|
|
96
|
+
*
|
|
97
|
+
* @returns Resolves when every session was looked at
|
|
98
|
+
*/
|
|
99
|
+
static watchOwnPastes(): Promise<void>;
|
|
100
|
+
/** Pause between screen samples when checking that a busy screen repaints (tests shorten it). */
|
|
101
|
+
static repaintSampleMs: number;
|
|
102
|
+
/**
|
|
103
|
+
* Whether the agent in a session is mid-turn. All three must hold:
|
|
104
|
+
* - a turn signal where the runtime paints it (see readTurnSignals): the
|
|
105
|
+
* busy bar in the footer below the box, or the spinner line directly
|
|
106
|
+
* above it — never text in the transcript;
|
|
107
|
+
* - the screen is repainting: it changes within a few short samples (a
|
|
108
|
+
* live spinner animates its glyph and counter several times a second);
|
|
109
|
+
* - the PTY produced output within BUSY_FROZEN_MS: a screen that stopped
|
|
110
|
+
* repainting is frozen, not busy, and a held message must go out.
|
|
54
111
|
*
|
|
55
112
|
* @param sessionName - The session
|
|
56
|
-
* @returns
|
|
113
|
+
* @returns True when busy
|
|
57
114
|
*/
|
|
58
|
-
|
|
115
|
+
isAgentBusy(sessionName: string): Promise<boolean>;
|
|
116
|
+
/**
|
|
117
|
+
* Whether the agent in a session is resting: not busy (see
|
|
118
|
+
* {@link isAgentBusy}) and no meaningful output for a few seconds.
|
|
119
|
+
*
|
|
120
|
+
* @param sessionName - The session
|
|
121
|
+
* @returns True when idle
|
|
122
|
+
*/
|
|
123
|
+
isAgentIdle(sessionName: string): Promise<boolean>;
|
|
59
124
|
constructor(backend: ISessionBackend);
|
|
60
125
|
/**
|
|
61
126
|
* Get a session by name, throwing an error if it doesn't exist.
|
|
@@ -90,6 +155,11 @@ export declare class SessionCommandHelper {
|
|
|
90
155
|
* submitted and nothing is cleared: text we cannot prove is ours is
|
|
91
156
|
* never wiped.
|
|
92
157
|
*
|
|
158
|
+
* Every paste is recorded (see `ownPastes`): if its Enter is lost, or it
|
|
159
|
+
* renders only after we stopped looking (a busy Claude Code), it stays
|
|
160
|
+
* ours while the box keeps showing it, and is submitted once the agent
|
|
161
|
+
* is idle — by the next delivery or by the watcher.
|
|
162
|
+
*
|
|
93
163
|
* A refusal throws `TuiInputGuardError`; agent delivery keeps the message
|
|
94
164
|
* queued and retries (AgentRegistrationService). Shell command lines
|
|
95
165
|
* typed before a runtime starts use {@link sendShellLine} instead.
|
|
@@ -99,9 +169,14 @@ export declare class SessionCommandHelper {
|
|
|
99
169
|
* @throws Error if session does not exist
|
|
100
170
|
* @throws TuiInputGuardError when the box is unreadable or not ours
|
|
101
171
|
*/
|
|
102
|
-
sendMessage(sessionName: string, message: string
|
|
103
|
-
|
|
104
|
-
|
|
172
|
+
sendMessage(sessionName: string, message: string): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* The body of {@link sendMessage}, run while the watcher keeps off.
|
|
175
|
+
*
|
|
176
|
+
* @param sessionName - The session to send to
|
|
177
|
+
* @param message - The message to send
|
|
178
|
+
*/
|
|
179
|
+
private sendMessageGuarded;
|
|
105
180
|
/**
|
|
106
181
|
* After a delivery: if the box still holds the collapsed marker of our
|
|
107
182
|
* own paste, the Enter was lost — press Enter once and look again. Runs
|
|
@@ -143,6 +218,25 @@ export declare class SessionCommandHelper {
|
|
|
143
218
|
* @returns The reading
|
|
144
219
|
*/
|
|
145
220
|
readInputBox(sessionName: string, message: string, stage?: TuiInputStage): TuiInputReading;
|
|
221
|
+
/**
|
|
222
|
+
* What the box showed for our recorded paste, while the record stands.
|
|
223
|
+
* Ends the record when it has gone unseen too long.
|
|
224
|
+
*
|
|
225
|
+
* @param sessionName - The session
|
|
226
|
+
* @returns The text the box showed, or undefined (none seen / no record)
|
|
227
|
+
*/
|
|
228
|
+
private ownPasteShown;
|
|
229
|
+
/**
|
|
230
|
+
* Keep the paste record in step with a reading: the record stands only
|
|
231
|
+
* while every readable box shows our paste. A pending paste (not seen
|
|
232
|
+
* yet) is adopted when the box first shows its shape; an empty box keeps
|
|
233
|
+
* it pending; anything else ends it.
|
|
234
|
+
*
|
|
235
|
+
* @param sessionName - The session
|
|
236
|
+
* @param reading - The classification just made
|
|
237
|
+
* @returns The reading, marked `ownPasteMarker` when the box shows our paste
|
|
238
|
+
*/
|
|
239
|
+
private applyOwnPaste;
|
|
146
240
|
/**
|
|
147
241
|
* Empty the input box — only call it for text proven to be the harness's
|
|
148
242
|
* own. Sends Ctrl+U then Backspace (clear the line, join the empty line to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-command-helper.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/session/session-command-helper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAOhF,OAAO,
|
|
1
|
+
{"version":3,"file":"session-command-helper.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/session/session-command-helper.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ import { delay } from '../../utils/async.utils.js';
|
|
|
21
21
|
import { assertNotSecretEnvKey } from '../../utils/secret-env.js';
|
|
22
22
|
import { quietShellLine } from '../../utils/shell-history.js';
|
|
23
23
|
import { PtyActivityTrackerService } from '../agent/pty-activity-tracker.service.js';
|
|
24
|
-
import { classifyTuiInput,
|
|
24
|
+
import { classifyTuiInput, pasteShowsAs, screenShowsTurnInProgress, TuiInputGuardError } from './tui-input-guard.js';
|
|
25
25
|
import { noteHarnessWrite } from '../trace/turn-origin.js';
|
|
26
26
|
/**
|
|
27
27
|
* Key code mappings for special keys
|
|
@@ -69,16 +69,36 @@ export class SessionCommandHelper {
|
|
|
69
69
|
logger;
|
|
70
70
|
backend;
|
|
71
71
|
/**
|
|
72
|
-
* Per session: the
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* ours
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
* Per session: the harness's own latest paste into a box it had proven
|
|
73
|
+
* empty. While the box keeps showing it (the collapsed marker "[Pasted
|
|
74
|
+
* text #4 +5 lines]" / "[Pasted Content 1234 chars]", or the text itself)
|
|
75
|
+
* it is ours — whatever message is sent next — so a lost or never-pressed
|
|
76
|
+
* Enter can be recovered. Written by every paste path; shared across
|
|
77
|
+
* helper instances.
|
|
78
|
+
*
|
|
79
|
+
* - `pending`: pasted, not yet seen in the box (a busy Claude Code renders
|
|
80
|
+
* a paste seconds late). The first readable box that is not empty must
|
|
81
|
+
* show the paste's shape, or the record ends; it also ends unseen after
|
|
82
|
+
* OWN_PASTE_PENDING_MAX_MS.
|
|
83
|
+
* - `seen`: the box showed `shown`. Every readable box since must show
|
|
84
|
+
* exactly that; the first that does not ends the record. Unreadable
|
|
85
|
+
* screens (a dialog) do not count as seen: unseen for
|
|
86
|
+
* OWN_PASTE_UNSEEN_MAX_MS ends it too. While busy the watcher re-reads
|
|
87
|
+
* the box, so a long turn keeps it alive.
|
|
88
|
+
*
|
|
89
|
+
* Also ended by our one Enter on it, and by session/runtime (re)start.
|
|
90
|
+
*/
|
|
91
|
+
static ownPastes = new Map();
|
|
92
|
+
/** Sessions a sendMessage is typing into right now (the watcher keeps off) */
|
|
93
|
+
static inFlight = new Set();
|
|
94
|
+
/** The watcher re-reading boxes that hold a paste of ours */
|
|
95
|
+
static watchTimer = null;
|
|
96
|
+
/** Clock for the paste record (tests). */
|
|
79
97
|
static now = Date.now;
|
|
98
|
+
/** How long sendMessage waits for a late paste to render (tests shorten it). */
|
|
99
|
+
static pasteRenderMaxWaitMs = TUI_INPUT_GUARD.PASTE_RENDER_MAX_WAIT_MS;
|
|
80
100
|
/**
|
|
81
|
-
* Forget the recorded paste
|
|
101
|
+
* Forget the recorded paste of a session: its runtime is being
|
|
82
102
|
* (re)started, a shell line is typed, or the session is created/killed —
|
|
83
103
|
* Claude Code's paste counter restarts at #1, and Codex markers carry only
|
|
84
104
|
* a character count, so an old record could match the owner's paste.
|
|
@@ -86,24 +106,129 @@ export class SessionCommandHelper {
|
|
|
86
106
|
* @param sessionName - The session
|
|
87
107
|
*/
|
|
88
108
|
static forgetOwnPaste(sessionName) {
|
|
89
|
-
SessionCommandHelper.
|
|
109
|
+
SessionCommandHelper.ownPastes.delete(sessionName);
|
|
110
|
+
SessionCommandHelper.stopWatchingIfIdle();
|
|
90
111
|
}
|
|
91
112
|
/**
|
|
92
|
-
*
|
|
93
|
-
* OWN_MARKER_TTL_MS after our paste.
|
|
113
|
+
* Whether a paste of ours is on record for a session (tests, diagnostics).
|
|
94
114
|
*
|
|
95
115
|
* @param sessionName - The session
|
|
96
|
-
* @returns
|
|
116
|
+
* @returns True while the record stands
|
|
97
117
|
*/
|
|
98
|
-
static
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
static hasOwnPaste(sessionName) {
|
|
119
|
+
return SessionCommandHelper.ownPastes.has(sessionName);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Record our paste of `message` into a box proven empty.
|
|
123
|
+
*
|
|
124
|
+
* @param sessionName - The session
|
|
125
|
+
* @param message - What was pasted
|
|
126
|
+
*/
|
|
127
|
+
recordOwnPaste(sessionName, message) {
|
|
128
|
+
SessionCommandHelper.ownPastes.set(sessionName, { message, pastedAt: SessionCommandHelper.now(), helper: this });
|
|
129
|
+
SessionCommandHelper.startWatching();
|
|
130
|
+
}
|
|
131
|
+
/** Whether recording a paste starts the timed watcher (tests drive it by hand). */
|
|
132
|
+
static autoWatch = true;
|
|
133
|
+
/** Start the watcher (no-op when running). */
|
|
134
|
+
static startWatching() {
|
|
135
|
+
if (SessionCommandHelper.watchTimer || !SessionCommandHelper.autoWatch)
|
|
136
|
+
return;
|
|
137
|
+
SessionCommandHelper.watchTimer = setInterval(() => {
|
|
138
|
+
void SessionCommandHelper.watchOwnPastes();
|
|
139
|
+
}, TUI_INPUT_GUARD.OWN_PASTE_WATCH_MS);
|
|
140
|
+
SessionCommandHelper.watchTimer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
/** Stop the watcher once no paste is on record. */
|
|
143
|
+
static stopWatchingIfIdle() {
|
|
144
|
+
if (SessionCommandHelper.ownPastes.size > 0 || !SessionCommandHelper.watchTimer)
|
|
145
|
+
return;
|
|
146
|
+
clearInterval(SessionCommandHelper.watchTimer);
|
|
147
|
+
SessionCommandHelper.watchTimer = null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* One watcher pass: re-read every box holding a paste of ours (keeping
|
|
151
|
+
* the "seen continuously" record alive through a long turn, ending it the
|
|
152
|
+
* moment the box shows anything else), and once the agent is idle with
|
|
153
|
+
* our paste still sitting there, press Enter on it once.
|
|
154
|
+
*
|
|
155
|
+
* @returns Resolves when every session was looked at
|
|
156
|
+
*/
|
|
157
|
+
static async watchOwnPastes() {
|
|
158
|
+
for (const [sessionName, rec] of [...SessionCommandHelper.ownPastes]) {
|
|
159
|
+
if (SessionCommandHelper.inFlight.has(sessionName))
|
|
160
|
+
continue;
|
|
161
|
+
try {
|
|
162
|
+
if (!rec.helper.backend.sessionExists(sessionName)) {
|
|
163
|
+
SessionCommandHelper.ownPastes.delete(sessionName);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const reading = rec.helper.readInputBox(sessionName, '', 'recovery');
|
|
167
|
+
if (reading.ownPasteMarker && await rec.helper.isAgentIdle(sessionName)) {
|
|
168
|
+
const outcome = await rec.helper.ensureOwnPasteSubmitted(sessionName);
|
|
169
|
+
rec.helper.logger.warn('Our earlier paste was still in the input box of an idle agent — pressed Enter on it once', { sessionName, outcome });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// A session that vanished mid-pass: the next pass drops it.
|
|
174
|
+
}
|
|
105
175
|
}
|
|
106
|
-
|
|
176
|
+
SessionCommandHelper.stopWatchingIfIdle();
|
|
177
|
+
}
|
|
178
|
+
/** Pause between screen samples when checking that a busy screen repaints (tests shorten it). */
|
|
179
|
+
static repaintSampleMs = TUI_INPUT_GUARD.BUSY_REPAINT_SAMPLE_MS;
|
|
180
|
+
/**
|
|
181
|
+
* Whether the agent in a session is mid-turn. All three must hold:
|
|
182
|
+
* - a turn signal where the runtime paints it (see readTurnSignals): the
|
|
183
|
+
* busy bar in the footer below the box, or the spinner line directly
|
|
184
|
+
* above it — never text in the transcript;
|
|
185
|
+
* - the screen is repainting: it changes within a few short samples (a
|
|
186
|
+
* live spinner animates its glyph and counter several times a second);
|
|
187
|
+
* - the PTY produced output within BUSY_FROZEN_MS: a screen that stopped
|
|
188
|
+
* repainting is frozen, not busy, and a held message must go out.
|
|
189
|
+
*
|
|
190
|
+
* @param sessionName - The session
|
|
191
|
+
* @returns True when busy
|
|
192
|
+
*/
|
|
193
|
+
async isAgentBusy(sessionName) {
|
|
194
|
+
const read = () => {
|
|
195
|
+
try {
|
|
196
|
+
return this.backend.captureOutput(sessionName, TUI_INPUT_GUARD.BUSY_BAR_TAIL_LINES) ?? '';
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const first = read();
|
|
203
|
+
if (!screenShowsTurnInProgress(first))
|
|
204
|
+
return false;
|
|
205
|
+
const tracker = PtyActivityTrackerService.getInstance();
|
|
206
|
+
const quietMs = typeof tracker.getRawOutputIdleMs === 'function' ? tracker.getRawOutputIdleMs(sessionName) : null;
|
|
207
|
+
if (quietMs !== null && quietMs >= TUI_INPUT_GUARD.BUSY_FROZEN_MS) {
|
|
208
|
+
this.logger.warn('Screen shows a turn but the PTY has been silent — treating the agent as idle', { sessionName, quietMs });
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
for (let i = 0; i < TUI_INPUT_GUARD.BUSY_REPAINT_SAMPLES; i++) {
|
|
212
|
+
await delay(SessionCommandHelper.repaintSampleMs);
|
|
213
|
+
const next = read();
|
|
214
|
+
if (!screenShowsTurnInProgress(next))
|
|
215
|
+
return false;
|
|
216
|
+
if (next !== first)
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Whether the agent in a session is resting: not busy (see
|
|
223
|
+
* {@link isAgentBusy}) and no meaningful output for a few seconds.
|
|
224
|
+
*
|
|
225
|
+
* @param sessionName - The session
|
|
226
|
+
* @returns True when idle
|
|
227
|
+
*/
|
|
228
|
+
async isAgentIdle(sessionName) {
|
|
229
|
+
if (await this.isAgentBusy(sessionName))
|
|
230
|
+
return false;
|
|
231
|
+
return PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName) >= SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
|
|
107
232
|
}
|
|
108
233
|
constructor(backend) {
|
|
109
234
|
this.logger = LoggerService.getInstance().createComponentLogger('SessionCommandHelper');
|
|
@@ -152,6 +277,11 @@ export class SessionCommandHelper {
|
|
|
152
277
|
* submitted and nothing is cleared: text we cannot prove is ours is
|
|
153
278
|
* never wiped.
|
|
154
279
|
*
|
|
280
|
+
* Every paste is recorded (see `ownPastes`): if its Enter is lost, or it
|
|
281
|
+
* renders only after we stopped looking (a busy Claude Code), it stays
|
|
282
|
+
* ours while the box keeps showing it, and is submitted once the agent
|
|
283
|
+
* is idle — by the next delivery or by the watcher.
|
|
284
|
+
*
|
|
155
285
|
* A refusal throws `TuiInputGuardError`; agent delivery keeps the message
|
|
156
286
|
* queued and retries (AgentRegistrationService). Shell command lines
|
|
157
287
|
* typed before a runtime starts use {@link sendShellLine} instead.
|
|
@@ -161,7 +291,22 @@ export class SessionCommandHelper {
|
|
|
161
291
|
* @throws Error if session does not exist
|
|
162
292
|
* @throws TuiInputGuardError when the box is unreadable or not ours
|
|
163
293
|
*/
|
|
164
|
-
async sendMessage(sessionName, message
|
|
294
|
+
async sendMessage(sessionName, message) {
|
|
295
|
+
SessionCommandHelper.inFlight.add(sessionName);
|
|
296
|
+
try {
|
|
297
|
+
await this.sendMessageGuarded(sessionName, message);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
SessionCommandHelper.inFlight.delete(sessionName);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The body of {@link sendMessage}, run while the watcher keeps off.
|
|
305
|
+
*
|
|
306
|
+
* @param sessionName - The session to send to
|
|
307
|
+
* @param message - The message to send
|
|
308
|
+
*/
|
|
309
|
+
async sendMessageGuarded(sessionName, message) {
|
|
165
310
|
const session = this.getSessionOrThrow(sessionName);
|
|
166
311
|
this.logger.debug('Sending message to session', {
|
|
167
312
|
sessionName,
|
|
@@ -171,10 +316,14 @@ export class SessionCommandHelper {
|
|
|
171
316
|
// Step 1: the box must be readable and empty before we type.
|
|
172
317
|
let before = this.readInputBox(sessionName, message, 'before-write');
|
|
173
318
|
if (before.ownPasteMarker) {
|
|
174
|
-
// An earlier
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
319
|
+
// An earlier paste of ours is still in the box (its Enter was lost,
|
|
320
|
+
// or it rendered late): submit it first. When it is this very
|
|
321
|
+
// message, submitting it IS this delivery — never paste it twice.
|
|
322
|
+
const sameMessage = SessionCommandHelper.ownPastes.get(sessionName)?.message === message;
|
|
323
|
+
this.logger.warn('An earlier paste of ours is still in the input box — submitting it', { sessionName, sameMessage });
|
|
324
|
+
const outcome = await this.ensureOwnPasteSubmitted(sessionName);
|
|
325
|
+
if (sameMessage && outcome === 'submitted')
|
|
326
|
+
return;
|
|
178
327
|
before = this.readInputBox(sessionName, message, 'before-write');
|
|
179
328
|
}
|
|
180
329
|
if (before.state === 'ours' && !before.ownPasteMarker) {
|
|
@@ -196,6 +345,7 @@ export class SessionCommandHelper {
|
|
|
196
345
|
// Without this, special characters like (), $, `, and " can trigger
|
|
197
346
|
// shell mode or be read as key sequences in Gemini CLI (#292, #293).
|
|
198
347
|
session.write(`\x1b[200~${message}\x1b[201~`);
|
|
348
|
+
this.recordOwnPaste(sessionName, message);
|
|
199
349
|
// Scale delay based on message size: large prompts (e.g. 409-line registration
|
|
200
350
|
// prompts) need more time for Claude Code to process the bracketed paste.
|
|
201
351
|
// Base delay + 1ms per 10 characters, capped at 5 seconds.
|
|
@@ -209,11 +359,13 @@ export class SessionCommandHelper {
|
|
|
209
359
|
await delay(waitMs); // the paste may not have rendered yet
|
|
210
360
|
after = this.readInputBox(sessionName, message, 'after-paste');
|
|
211
361
|
}
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
362
|
+
// A busy agent renders a paste late (Claude Code: ~10 s mid-turn,
|
|
363
|
+
// 2026-10-03 Ella). Keep looking a while before giving up; if it shows
|
|
364
|
+
// up later anyway, the record makes it ours and it is submitted then.
|
|
365
|
+
const renderDeadline = SessionCommandHelper.now() + SessionCommandHelper.pasteRenderMaxWaitMs;
|
|
366
|
+
while (after.state === 'empty' && SessionCommandHelper.now() < renderDeadline) {
|
|
367
|
+
await delay(TUI_INPUT_GUARD.PASTE_RENDER_POLL_MS);
|
|
368
|
+
after = this.readInputBox(sessionName, message, 'after-paste');
|
|
217
369
|
}
|
|
218
370
|
if (after.state !== 'ours') {
|
|
219
371
|
this.logger.warn('Input box does not hold exactly our text after the paste — not pressing Enter, not clearing', {
|
|
@@ -260,7 +412,7 @@ export class SessionCommandHelper {
|
|
|
260
412
|
const after = this.readInputBox(sessionName, '', 'recovery');
|
|
261
413
|
// One Enter on it, whatever happened: never press Enter on that marker
|
|
262
414
|
// again from this record.
|
|
263
|
-
SessionCommandHelper.
|
|
415
|
+
SessionCommandHelper.forgetOwnPaste(sessionName);
|
|
264
416
|
if (after.ownPasteMarker || (after.state === 'foreign' && after.text.trim() === reading.text.trim())) {
|
|
265
417
|
this.logger.warn('Our pasted message is still in the input box after Enter — not delivered', { sessionName });
|
|
266
418
|
return 'stuck';
|
|
@@ -272,7 +424,9 @@ export class SessionCommandHelper {
|
|
|
272
424
|
* Forget recorded paste markers (tests).
|
|
273
425
|
*/
|
|
274
426
|
static resetOwnPasteMarkersForTesting() {
|
|
275
|
-
SessionCommandHelper.
|
|
427
|
+
SessionCommandHelper.ownPastes.clear();
|
|
428
|
+
SessionCommandHelper.inFlight.clear();
|
|
429
|
+
SessionCommandHelper.stopWatchingIfIdle();
|
|
276
430
|
}
|
|
277
431
|
/**
|
|
278
432
|
* Type a command line into a plain shell and run it (paste + Enter).
|
|
@@ -308,6 +462,9 @@ export class SessionCommandHelper {
|
|
|
308
462
|
* @returns The reading
|
|
309
463
|
*/
|
|
310
464
|
readInputBox(sessionName, message, stage = 'recovery') {
|
|
465
|
+
// First: a paste record that has gone unseen too long ends here, even
|
|
466
|
+
// when the box cannot be read now.
|
|
467
|
+
const shown = this.ownPasteShown(sessionName);
|
|
311
468
|
const capture = this.backend.captureInputView;
|
|
312
469
|
if (typeof capture !== 'function')
|
|
313
470
|
return { state: 'unknown', text: '', lineCount: 0 };
|
|
@@ -315,19 +472,65 @@ export class SessionCommandHelper {
|
|
|
315
472
|
const view = capture.call(this.backend, sessionName);
|
|
316
473
|
if (!view)
|
|
317
474
|
return { state: 'unknown', text: '', lineCount: 0 };
|
|
318
|
-
|
|
319
|
-
// and the record ends the moment the box shows anything else.
|
|
320
|
-
const own = SessionCommandHelper.ownMarkerFor(sessionName);
|
|
321
|
-
const reading = classifyTuiInput(view, message, stage, own);
|
|
322
|
-
if (own && reading.state !== 'unknown' && !reading.ownPasteMarker) {
|
|
323
|
-
SessionCommandHelper.ownPasteMarkers.delete(sessionName);
|
|
324
|
-
}
|
|
325
|
-
return reading;
|
|
475
|
+
return this.applyOwnPaste(sessionName, classifyTuiInput(view, message, stage, shown));
|
|
326
476
|
}
|
|
327
477
|
catch {
|
|
328
478
|
return { state: 'unknown', text: '', lineCount: 0 };
|
|
329
479
|
}
|
|
330
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* What the box showed for our recorded paste, while the record stands.
|
|
483
|
+
* Ends the record when it has gone unseen too long.
|
|
484
|
+
*
|
|
485
|
+
* @param sessionName - The session
|
|
486
|
+
* @returns The text the box showed, or undefined (none seen / no record)
|
|
487
|
+
*/
|
|
488
|
+
ownPasteShown(sessionName) {
|
|
489
|
+
const rec = SessionCommandHelper.ownPastes.get(sessionName);
|
|
490
|
+
if (!rec)
|
|
491
|
+
return undefined;
|
|
492
|
+
const now = SessionCommandHelper.now();
|
|
493
|
+
const unseenFor = now - (rec.lastSeenAt ?? rec.pastedAt);
|
|
494
|
+
const limit = rec.shown === undefined ? TUI_INPUT_GUARD.OWN_PASTE_PENDING_MAX_MS : TUI_INPUT_GUARD.OWN_PASTE_UNSEEN_MAX_MS;
|
|
495
|
+
if (unseenFor > limit) {
|
|
496
|
+
SessionCommandHelper.forgetOwnPaste(sessionName);
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
return rec.shown;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Keep the paste record in step with a reading: the record stands only
|
|
503
|
+
* while every readable box shows our paste. A pending paste (not seen
|
|
504
|
+
* yet) is adopted when the box first shows its shape; an empty box keeps
|
|
505
|
+
* it pending; anything else ends it.
|
|
506
|
+
*
|
|
507
|
+
* @param sessionName - The session
|
|
508
|
+
* @param reading - The classification just made
|
|
509
|
+
* @returns The reading, marked `ownPasteMarker` when the box shows our paste
|
|
510
|
+
*/
|
|
511
|
+
applyOwnPaste(sessionName, reading) {
|
|
512
|
+
const rec = SessionCommandHelper.ownPastes.get(sessionName);
|
|
513
|
+
if (!rec || reading.state === 'unknown')
|
|
514
|
+
return reading;
|
|
515
|
+
const now = SessionCommandHelper.now();
|
|
516
|
+
if (rec.shown !== undefined) {
|
|
517
|
+
if (reading.ownPasteMarker) {
|
|
518
|
+
rec.lastSeenAt = now;
|
|
519
|
+
return reading;
|
|
520
|
+
}
|
|
521
|
+
SessionCommandHelper.forgetOwnPaste(sessionName);
|
|
522
|
+
return reading;
|
|
523
|
+
}
|
|
524
|
+
if (reading.state === 'empty')
|
|
525
|
+
return reading; // not rendered yet
|
|
526
|
+
if (pasteShowsAs(reading.text, rec.message)) {
|
|
527
|
+
rec.shown = reading.text.trim();
|
|
528
|
+
rec.lastSeenAt = now;
|
|
529
|
+
return { ...reading, state: 'ours', ownPasteMarker: true };
|
|
530
|
+
}
|
|
531
|
+
SessionCommandHelper.forgetOwnPaste(sessionName);
|
|
532
|
+
return reading;
|
|
533
|
+
}
|
|
331
534
|
/**
|
|
332
535
|
* Empty the input box — only call it for text proven to be the harness's
|
|
333
536
|
* own. Sends Ctrl+U then Backspace (clear the line, join the empty line to
|