agents_control 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b71d165050803893091b3bce0857cb4a9cac7875e28289e0aaebc6bf4a1aa820
4
- data.tar.gz: a301e0e972625e0eb57a79e80799e21edf2232d24ef8bbbfb74840484d1feb7e
3
+ metadata.gz: aadeb50868f7e15b7da9346e051f268ffad1c652c1e7297d54cb328380b030f6
4
+ data.tar.gz: cf62c6519dfd54bb7b9ee169c5e1b58f5bcf92b54993e5a61e954547180b6ec2
5
5
  SHA512:
6
- metadata.gz: b6a5dd4eb521026c1084ade954fbe7d7a8f26426e3d68d78077a3d69e71addc0c40165181eaaff963a225434a71167e0ed02f68382fe8ed4605a46f8e781bc54
7
- data.tar.gz: 7077e2aa298e86630f4c619e67a6bde0be85a2316f86c43b46dbd354353bc9404f91a0db8aa86ab9f8fcf1f0c3d6e8cee101c1b55cbf53080810f52d2ba37df1
6
+ metadata.gz: dce0f05882260d26dddc1a65f392ef0cf8ad7351b1ccbd07e29d2378c2871077434669e4f3c7248b682fd10aa72aedbfbbfae5739703222040bd3741e9a2be4a
7
+ data.tar.gz: f3e61a373a6252b920e6086b9e9b01591855fecc8708ffd059e0f87d412fce749d97cc445a6553aa42d69e404c60973ab1eb6551c2db60ae3362cce2756964f7
data/README.md CHANGED
@@ -104,20 +104,35 @@ Bot commands:
104
104
 
105
105
  The number `N` comes from the last list shown.
106
106
 
107
- `/run` treats the command itself as the reference point: the shell
108
- echoes back whatever's typed, so it looks for that exact text on
109
- screen and shows from there — sharper than diffing screenshots, and
110
- it still works even if something else wrote to the same tab in
111
- between, since it doesn't need the screen from right before typing to
112
- relate to the screen after at all. A short reply ("y", "n" mid `git
113
- add -p`) isn't a safe anchor on its own — too likely to match
114
- something unrelated — so those fall back to a before/after diff
115
- instead, and when even that can't cleanly tell what's new, to the
116
- current screen outright: seeing the result, possibly with a little
117
- stale context around it, beats not seeing it at all. How much gets
118
- captured either way is a `/settings` option (`terminal.run_result_lines`,
119
- 200 by default) — a command whose output runs longer just gets its
120
- last N lines, same as the default gets cut by a screen that's too tall.
107
+ Against a plain tab, `/run` treats the command itself as the reference
108
+ point: the shell echoes back whatever's typed, so it looks for that
109
+ exact text on screen and shows from there — sharper than diffing
110
+ screenshots, and it still works even if something else wrote to the
111
+ same tab in between, since it doesn't need the screen from right
112
+ before typing to relate to the screen after at all. A short reply
113
+ ("y", "n" mid `git add -p`) isn't a safe anchor on its own — too
114
+ likely to match something unrelated — so those fall back to a
115
+ before/after diff instead, and when even that can't cleanly tell
116
+ what's new, to the current screen outright: seeing the result,
117
+ possibly with a little stale context around it, beats not seeing it
118
+ at all. How much gets captured either way is a `/settings` option
119
+ (`terminal.run_result_lines`, 200 by default) — a command whose output
120
+ runs longer just gets its last N lines, same as the default gets cut
121
+ by a screen that's too tall.
122
+
123
+ Against an agent, `/run` just confirms the send — an agent isn't a
124
+ shell command that finishes in a couple of seconds, so there's no
125
+ "result" to capture yet by the time it would look. Hooks already own
126
+ telling Telegram when it's actually done or needs something, the same
127
+ as replying to one of its own questions.
128
+
129
+ A tab stuck inside `less`, `vim`, a REPL, or anything else that reads
130
+ keystrokes as its own input rather than a line to submit gets a
131
+ confirmation first instead of a blind send — the text would go to
132
+ whatever's actually running there, not run as a command. A pager not
133
+ on that recognized list still gets caught: a bare `:` as the entire
134
+ last line is the one thing practically every pager agrees on for
135
+ "waiting on you," and a real shell prompt never looks like that.
121
136
 
122
137
  The result stays a live target: replying to it — "y", "n", anything —
123
138
  types straight into that same pane and shows what came back, so
@@ -14,6 +14,18 @@ module AgentsControl
14
14
  # they get a separate confirmation.
15
15
  REMOTE_COMMANDS = %w[ssh mosh].freeze
16
16
 
17
+ # Programs that read keystrokes as their own input, not as a
18
+ # line to submit to a shell. Sending a command into one of
19
+ # these wouldn't run it — it'd feed the letters to whatever's
20
+ # already there instead: pager navigation, an editor's insert
21
+ # mode, a REPL evaluating each character-by-character. Confirmed
22
+ # against a real incident: `git log`'s pager was still open,
23
+ # "git status" went in as keystrokes, and nothing about it ran
24
+ # as a command at all.
25
+ INTERACTIVE_COMMANDS = %w[less more most vim vi nvim emacs nano pico
26
+ man top htop irb pry python python3 node
27
+ mysql psql sqlite3].freeze
28
+
17
29
  # Shared list for the Telegram menu and for /help: a menu that's
18
30
  # drifted from reality is worse than no menu. The third element
19
31
  # is the argument syntax, needed only in /help; setMyCommands
@@ -196,11 +208,29 @@ module AgentsControl
196
208
 
197
209
  with_session(chat_id, number) do |session|
198
210
  next say(chat_id, "This session has no terminal — nothing to run there.") if session.terminalless?
199
-
200
- if remote?(session)
201
- confirm_remote(chat_id, session, command)
211
+ next confirm_remote(chat_id, session, command) if remote?(session)
212
+
213
+ # An agent isn't a shell command that finishes in a couple
214
+ # of seconds — it's a real task, and hooks already own
215
+ # telling Telegram when it's actually done or needs
216
+ # something. Capturing a "result" a moment after typing
217
+ # would just catch it mid-thought and fall back to dumping
218
+ # its whole transcript, which isn't a result at all — same
219
+ # reasoning as replying to an agent's own question
220
+ # (type_into_session).
221
+ next execute(chat_id, session, command, show_result: false) if session.agent?
222
+ next confirm_interactive(chat_id, session, command) if interactive?(session)
223
+
224
+ # One capture serves two purposes: it's checked for a
225
+ # pager's bare `:` prompt right here, and reused as
226
+ # execute()'s own "before" snapshot if it turns out clean —
227
+ # capturing twice would desync anything that reads the
228
+ # screen expecting to see it change between calls.
229
+ before = capture_screen(session, lines: run_result_lines)
230
+ if paused_for_input?(before)
231
+ confirm_interactive(chat_id, session, command)
202
232
  else
203
- execute(chat_id, session, command, show_result: true)
233
+ execute(chat_id, session, command, show_result: true, before: before)
204
234
  end
205
235
  end
206
236
  end
@@ -211,6 +241,21 @@ module AgentsControl
211
241
  REMOTE_COMMANDS.include?(session.foreground_command.to_s)
212
242
  end
213
243
 
244
+ def interactive?(session)
245
+ INTERACTIVE_COMMANDS.include?(session.foreground_command.to_s)
246
+ end
247
+
248
+ # Backstop for interactive? missing a pager by name — a custom
249
+ # $PAGER, or anything else not on that list. A bare `:` as the
250
+ # entire last line is the one thing practically every pager in
251
+ # the less/more lineage agrees on for "waiting on you"; a real
252
+ # shell prompt always has more on that line than a single
253
+ # colon. Takes the screen rather than the session so the
254
+ # caller's own capture can be reused instead of taking another.
255
+ def paused_for_input?(screen)
256
+ screen.to_s.rstrip.lines.last.to_s.strip == ":"
257
+ end
258
+
214
259
  def confirm_remote(chat_id, session, command)
215
260
  key = @store.put({ "action" => "run", "session_id" => session.id, "text" => command },
216
261
  ttl: 300)
@@ -225,6 +270,21 @@ module AgentsControl
225
270
  markup: markup)
226
271
  end
227
272
 
273
+ def confirm_interactive(chat_id, session, command)
274
+ key = @store.put({ "action" => "run", "session_id" => session.id, "text" => command },
275
+ ttl: 300)
276
+
277
+ markup = { inline_keyboard: [[
278
+ { text: "⚠️ Send anyway", callback_data: key },
279
+ { text: "cancel", callback_data: @store.put({ "action" => "cancel" }, ttl: 300) }
280
+ ]] }
281
+
282
+ say(chat_id, "#{session.label} is inside #{session.foreground_command} right now, " \
283
+ "not at a shell prompt — the text would go to that, not run as a command:\n\n" \
284
+ "`#{command}`",
285
+ markup: markup)
286
+ end
287
+
228
288
  # Enter is sent as a separate call, not tacked onto the same
229
289
  # input: a merged call can fail to send multi-line text at all.
230
290
  TYPING_PAUSE = 0.4
@@ -241,9 +301,9 @@ module AgentsControl
241
301
 
242
302
  def run_result_lines = @config.get("terminal.run_result_lines", 200)
243
303
 
244
- def execute(chat_id, session, command, show_result: false)
304
+ def execute(chat_id, session, command, show_result: false, before: nil)
245
305
  backend = @registry.backend_for(session)
246
- before = show_result ? capture_screen(session, lines: run_result_lines) : nil
306
+ before ||= capture_screen(session, lines: run_result_lines) if show_result
247
307
 
248
308
  ok = backend.send_text(session.id, command, newline: false) &&
249
309
  sleep(TYPING_PAUSE).then { backend.send_text(session.id, "", newline: true) }
@@ -514,7 +574,7 @@ module AgentsControl
514
574
  case payload["action"]
515
575
  when "focus" then focus_session(chat_id, session)
516
576
  when "screen" then show_screen(chat_id, session)
517
- when "run" then execute(chat_id, session, payload["text"], show_result: true)
577
+ when "run" then execute(chat_id, session, payload["text"], show_result: !session.agent?)
518
578
  when "close_confirm" then say(chat_id, "Close #{session.label}?",
519
579
  markup: @keyboards.confirm("close", session))
520
580
  when "close" then close_session(chat_id, session)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentsControl
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agents_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sapar Kurmanov