letsdo 0.1.0 → 0.2.0

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.
@@ -1,90 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'session/keys'
4
+ require_relative 'session/view'
5
+
3
6
  module Letsdo
4
7
  module Tui
5
- # The TUI controller: owns the terminal lifecycle, the background input
6
- # thread and the repaint loop around an orchestrator run.
7
- #
8
- # Threading model (the orchestrator loop stays on the calling thread,
9
- # exactly as in plain mode — Letsdo::PiRunner's signal design keeps
10
- # working unchanged):
11
- # main thread — the injected block (Letsdo::AgentLoop#run with its
12
- # own signal traps): provider queries, agent runs,
13
- # streamer writes into the log, metrics events;
14
- # input thread — the SOLE repainter: polls keys (tty-reader),
15
- # drains the log snapshot, tracks the metrics
16
- # snapshot and repaints on keys, data, 1s timer
17
- # ticks and SIGWINCH. Never touches the loop.
18
- #
19
- # Interactive actions (footer lists them):
20
- # ↑ / ↓ / PgUp / PgDn / Home / End — scroll; the view auto-follows
21
- # the newest line while at the bottom (tail -f semantics);
22
- # p — pause/resume: display freeze (PAUSED in the header, the log
23
- # keeps buffering, the view stays); keys still repaint;
24
- # r — immediate backlog re-query for the 'left' metric;
25
- # q / Ctrl-C — quit: exactly the signal-stop unwinding — Letsdo::Stopped
26
- # raised into the main thread terminates the pi child (PiRunner
27
- # rescue), stops the loop (AgentLoop rescue) and restores the
28
- # terminal from the ensure block; exit code 0.
29
- #
30
- # Signals: SIGWINCH sets a flag → the input thread re-queries the size
31
- # and repaints (no corruption). SIGINT/SIGTERM keep working through the
32
- # loop's own traps (main thread).
8
+ # The TUI controller: terminal lifecycle, background input thread, and
9
+ # the repaint loop around an orchestrator run.
33
10
  class Session
34
- # 1s session-timer repaint interval.
11
+ include SessionKeys
12
+ include SessionView
13
+
35
14
  REPAINT_INTERVAL = 1.0
36
- # Poll sleep when the display is frozen or nothing happened (avoids a
37
- # busy loop while still repainting at the timer interval).
38
15
  IDLE_SLEEP = 0.01
39
- # How long teardown waits for the input thread to finish its poll.
40
16
  INPUT_JOIN_TIMEOUT = 2.0
41
17
 
42
- # @param name [String] agent name (header identity)
43
- # @param handle [String] assignee handle (header identity)
44
- # @param log [LogBuffer] the combined log the streamer writes into
45
- # @param metrics [Metrics] the header metrics facade
46
- # @param terminal [Terminal] the terminal wrapper
47
- # @param input [Input] the key reader
48
- # @param refresh [Proc, nil] callable → open task count (Integer) or
49
- # nil (backlog unreadable); 'r' uses it for an immediate
50
- # re-query
51
- # @param wait_seconds [Numeric] retry interval for the waiting state
52
- # @param clock [Proc] monotonic clock for tick timing (injectable)
53
- def initialize(name:, handle:, log:, metrics:, terminal:, input:,
54
- refresh: nil, wait_seconds: 10.0, clock: nil)
55
- @name = name
56
- @handle = handle
57
- @log = log
58
- @metrics = metrics
59
- @terminal = terminal
60
- @input = input
61
- @refresh = refresh
62
- @wait_seconds = wait_seconds
63
- @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
64
- @offset = 0
65
- @follow = true
66
- @paused = false
67
- @stop = false
68
- @winch = false
69
- @body_height = 1
70
- @seen_version = 0
71
- @input_thread = nil
18
+ def initialize(**opts)
19
+ assign_identity(opts)
20
+ assign_io(opts)
21
+ assign_controls(opts)
22
+ reset_view_state
72
23
  end
73
24
 
74
- # Runs the TUI around the given orchestrator work.
75
- #
76
- # The block runs on the calling thread. On quit (q/Ctrl-C) or a stop
77
- # signal Letsdo::Stopped unwinds it; the terminal is restored from
78
- # the ensure block on every exit path.
79
- #
80
- # @yield the orchestrator run (e.g. Letsdo::AgentLoop#run)
81
- # @return [Integer] the block's result, or 0 when stopped
82
25
  def run(&work)
83
26
  install_winch_handler
84
27
  @terminal.enter
85
28
  start_input_thread
86
- result = work.call
87
- result
29
+ work.call
88
30
  rescue Letsdo::Stopped
89
31
  0
90
32
  ensure
@@ -95,122 +37,37 @@ module Letsdo
95
37
 
96
38
  private
97
39
 
98
- # The input thread body: the only place that writes to the terminal.
99
- def input_loop
100
- with_raw_input do
101
- repaint
102
- @last_repaint = @clock.call
103
- loop do
104
- break if @stop
105
-
106
- key = @input.next_key
107
- handled = key ? handle_key(key) : false
108
- winch = take_winch
109
- handled = true if winch
110
- now = @clock.call
111
- tick = now - @last_repaint >= REPAINT_INTERVAL
112
- new_data = @log.version != @seen_version
113
- if handled || (!@paused && (tick || new_data))
114
- repaint
115
- @last_repaint = @clock.call
116
- else
117
- sleep(IDLE_SLEEP)
118
- end
119
- end
120
- end
121
- rescue StandardError
122
- # The input thread must never die loudly into the middle of the
123
- # screen, and a quiet exit still leaves the loop running (teardown
124
- # joins it). Terminal state is restored by the ensure blocks.
125
- nil
126
- end
127
-
128
- def handle_key(key)
129
- case key
130
- when :up, :page_up
131
- @offset -= key == :page_up ? page_step : 1
132
- @follow = false
133
- true
134
- when :down, :page_down
135
- @offset += key == :page_down ? page_step : 1
136
- true # repaint decides re-sticking at the bottom
137
- when :home
138
- @offset = 0
139
- @follow = false
140
- true
141
- when :end
142
- @follow = true
143
- true
144
- when :p
145
- @paused = !@paused
146
- true
147
- when :r
148
- refresh
149
- true
150
- when :q, :ctrl_c
151
- quit
152
- false # leaving the session; no repaint needed
153
- else
154
- false
155
- end
156
- end
157
-
158
- # Immediate backlog re-query: the provider is re-run on this thread
159
- # (a subprocess call, thread-safe) and the count reaches the header
160
- # snapshot.
161
- def refresh
162
- return unless @refresh
163
-
164
- @metrics.provider_result(@refresh.call)
165
- end
166
-
167
- # Quit = the same unwinding as a stop signal: Letsdo::Stopped raised
168
- # into the main thread (inside the orchestrator work) terminates the
169
- # pi child and stops the loop; the ensure block restores the
170
- # terminal. No-op after teardown already started.
171
- def quit
172
- return if @stop
173
-
174
- Thread.main.raise(Letsdo::Stopped)
40
+ def assign_identity(opts)
41
+ @name = opts.fetch(:name)
42
+ @handle = opts.fetch(:handle)
43
+ @log = opts.fetch(:log)
44
+ @metrics = opts.fetch(:metrics)
175
45
  end
176
46
 
177
- # Repaints the whole frame from the current state.
178
- def repaint
179
- lines, version = @log.lines
180
- @seen_version = version
181
- height, width = @terminal.size
182
- @body_height = Renderer.body_height_for(height)
183
- max_offset = Renderer.max_offset(lines, @body_height)
184
- if @paused
185
- @offset = [[@offset, max_offset].min, 0].max
186
- elsif @follow
187
- @offset = max_offset
188
- else
189
- @offset = [[@offset, max_offset].min, 0].max
190
- @follow = true if @offset == max_offset
191
- end
192
- frame = Renderer.render(
193
- metrics: @metrics.snapshot, lines: lines, width: width, height: height,
194
- offset: @offset, follow: @follow, paused: @paused,
195
- wait_seconds: @wait_seconds
196
- )
197
- @terminal.render(frame)
47
+ def assign_io(opts)
48
+ @terminal = opts.fetch(:terminal)
49
+ @input = opts.fetch(:input)
50
+ @refresh = opts[:refresh]
51
+ @wait_seconds = opts.fetch(:wait_seconds, 10.0)
52
+ @clock = opts[:clock] || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
198
53
  end
199
54
 
200
- # Page height for PgUp/PgDn.
201
- def page_step
202
- @body_height.positive? ? @body_height : 1
55
+ def assign_controls(opts)
56
+ @pause_gate = opts[:pause_gate]
57
+ @runner = opts[:runner]
203
58
  end
204
59
 
205
- def take_winch
206
- flag = @winch
60
+ def reset_view_state
61
+ @offset = 0
62
+ @follow = true
63
+ @paused = false
64
+ @stop = false
207
65
  @winch = false
208
- flag
66
+ @body_height = 1
67
+ @seen_version = 0
68
+ @input_thread = nil
209
69
  end
210
70
 
211
- # Raw keyboard mode for the whole input session (the tty-reader
212
- # per-read raw wraps would leave the terminal echoing between polls);
213
- # no-op for non-tty inputs (tests use a pipe).
214
71
  def with_raw_input(&block)
215
72
  if @input.stdin.tty? && @input.stdin.respond_to?(:raw)
216
73
  @input.stdin.raw(&block)
@@ -220,13 +77,13 @@ module Letsdo
220
77
  end
221
78
 
222
79
  def install_winch_handler
223
- Signal.trap("SIGWINCH") { @winch = true }
80
+ Signal.trap('SIGWINCH') { @winch = true }
224
81
  rescue ArgumentError
225
- nil # no SIGWINCH on this platform — no resize repaints
82
+ nil
226
83
  end
227
84
 
228
85
  def restore_winch_handler
229
- Signal.trap("SIGWINCH", "DEFAULT")
86
+ Signal.trap('SIGWINCH', 'DEFAULT')
230
87
  rescue ArgumentError
231
88
  nil
232
89
  end
@@ -250,4 +107,4 @@ module Letsdo
250
107
  end
251
108
  end
252
109
  end
253
- end
110
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-cursor"
3
+ require 'tty-cursor'
4
4
 
5
5
  module Letsdo
6
6
  module Tui
@@ -24,11 +24,14 @@ module Letsdo
24
24
  # @param size_provider [Proc] callable → [height, width]; defaults to
25
25
  # TTY::Screen.size; injected in tests for deterministic size
26
26
  def initialize(stream:, size_provider: nil)
27
- require "tty-screen" unless defined?(TTY::Screen)
27
+ require 'tty-screen' unless defined?(TTY::Screen)
28
28
 
29
29
  @stream = stream
30
30
  @size_provider = size_provider ||
31
- -> { size = TTY::Screen.size; [size[0] || 24, size[1] || 80] }
31
+ lambda {
32
+ size = TTY::Screen.size
33
+ [size[0] || 24, size[1] || 80]
34
+ }
32
35
  end
33
36
 
34
37
  # Enters the alternate screen and hides the cursor.
@@ -64,4 +67,4 @@ module Letsdo
64
67
  end
65
68
  end
66
69
  end
67
- end
70
+ end
data/lib/letsdo/tui.rb CHANGED
@@ -16,9 +16,9 @@
16
16
  # Letsdo::Tui::Input - tty-reader key decoding (injectable)
17
17
  # Letsdo::Tui::Session - controller: terminal lifecycle + input thread
18
18
 
19
- require_relative "tui/log_buffer"
20
- require_relative "tui/metrics"
21
- require_relative "tui/renderer"
22
- require_relative "tui/terminal"
23
- require_relative "tui/input"
24
- require_relative "tui/session"
19
+ require_relative 'tui/log_buffer'
20
+ require_relative 'tui/metrics'
21
+ require_relative 'tui/renderer'
22
+ require_relative 'tui/terminal'
23
+ require_relative 'tui/input'
24
+ require_relative 'tui/session'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Letsdo
4
- VERSION = "0.1.0"
5
- end
4
+ VERSION = '0.2.0'
5
+ end
data/lib/letsdo.rb CHANGED
@@ -3,28 +3,34 @@
3
3
  # The Letsdo package — a local agent worker for Backlog.md/markdown tasks.
4
4
  #
5
5
  # OOP structure:
6
- # Letsdo::Errors - error hierarchy (UnknownAgentError and others)
6
+ # Letsdo::Errors - error hierarchy (Letsdo::Error and others)
7
7
  # Letsdo::PromptStore - access to agents/*.md prompts
8
+ # Letsdo::DefaultPrompt - the built-in default prompt (fallback run, --init template)
8
9
  # Letsdo::OutputStreamer - where and how agent text and service lines are printed
9
10
  # Letsdo::PiRunner - running pi --mode json and parsing the event stream
10
- # Letsdo::Agent - a single agent run: prompt from agents/ + pi
11
+ # Letsdo::Agent - a single agent run: prompt from agents/ or default + pi
12
+ # Letsdo::Capture - interruption-safe child stdout/stderr capture
11
13
  # Letsdo::BacklogTasks - open tasks from the backlog CLI (the task provider)
12
14
  # Letsdo::Loop - generic orchestrator: tasks → runs → waiting
13
15
  # Letsdo::AgentLoop - letsdo wiring: provider + agent + signals + messages
16
+ # Letsdo::Control - agent-control primitives (PauseGate)
14
17
  # Letsdo::Tui - the interactive TUI (LogBuffer, Metrics,
15
18
  # Renderer, Terminal, Input, Session)
16
19
  # Letsdo::CLI - command-line arguments, usage, exit code
17
20
  #
18
21
  # Entry point — bin/letsdo (a thin wrapper over Letsdo::CLI).
19
22
 
20
- require_relative "letsdo/version"
21
- require_relative "letsdo/errors"
22
- require_relative "letsdo/prompt_store"
23
- require_relative "letsdo/output_streamer"
24
- require_relative "letsdo/pi_runner"
25
- require_relative "letsdo/agent"
26
- require_relative "letsdo/backlog_tasks"
27
- require_relative "letsdo/loop"
28
- require_relative "letsdo/agent_loop"
29
- require_relative "letsdo/tui"
30
- require_relative "letsdo/cli"
23
+ require_relative 'letsdo/version'
24
+ require_relative 'letsdo/errors'
25
+ require_relative 'letsdo/prompt_store'
26
+ require_relative 'letsdo/default_prompt'
27
+ require_relative 'letsdo/output_streamer'
28
+ require_relative 'letsdo/pi_runner'
29
+ require_relative 'letsdo/agent'
30
+ require_relative 'letsdo/capture'
31
+ require_relative 'letsdo/backlog_tasks'
32
+ require_relative 'letsdo/loop'
33
+ require_relative 'letsdo/agent_loop'
34
+ require_relative 'letsdo/control'
35
+ require_relative 'letsdo/tui'
36
+ require_relative 'letsdo/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: letsdo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergei O. Udalov
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 1.77.0
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 1.77.0
68
82
  description: 'letsdo — a local agent worker for Backlog.md/markdown tasks. OOP structure:
69
83
  Letsdo::PromptStore (agents/), Letsdo::OutputStreamer and Letsdo::PiRunner (pi --mode
70
84
  json), Letsdo::Agent (one run), Letsdo::Loop (orchestrator loop), Letsdo::CLI. Minitest
@@ -82,12 +96,21 @@ files:
82
96
  - lib/letsdo.rb
83
97
  - lib/letsdo/agent.rb
84
98
  - lib/letsdo/agent_loop.rb
99
+ - lib/letsdo/agent_loop/tasks.rb
85
100
  - lib/letsdo/backlog_tasks.rb
101
+ - lib/letsdo/capture.rb
86
102
  - lib/letsdo/cli.rb
103
+ - lib/letsdo/cli/init.rb
104
+ - lib/letsdo/cli/launch.rb
105
+ - lib/letsdo/control.rb
106
+ - lib/letsdo/default_prompt.rb
87
107
  - lib/letsdo/errors.rb
88
108
  - lib/letsdo/loop.rb
89
109
  - lib/letsdo/output_streamer.rb
110
+ - lib/letsdo/output_streamer/arg_summary.rb
90
111
  - lib/letsdo/pi_runner.rb
112
+ - lib/letsdo/pi_runner/events.rb
113
+ - lib/letsdo/pi_runner/process.rb
91
114
  - lib/letsdo/prompt_store.rb
92
115
  - lib/letsdo/tui.rb
93
116
  - lib/letsdo/tui/input.rb
@@ -95,12 +118,19 @@ files:
95
118
  - lib/letsdo/tui/metrics.rb
96
119
  - lib/letsdo/tui/renderer.rb
97
120
  - lib/letsdo/tui/session.rb
121
+ - lib/letsdo/tui/session/keys.rb
122
+ - lib/letsdo/tui/session/view.rb
98
123
  - lib/letsdo/tui/terminal.rb
99
124
  - lib/letsdo/version.rb
125
+ homepage: https://github.com/sergio-fry/letsdo
100
126
  licenses:
101
127
  - MIT
102
128
  metadata:
129
+ homepage_uri: https://github.com/sergio-fry/letsdo
130
+ source_code_uri: https://github.com/sergio-fry/letsdo
131
+ changelog_uri: https://github.com/sergio-fry/letsdo/blob/main/CHANGELOG.md
103
132
  rubygems_mfa_required: 'true'
133
+ allowed_push_host: https://rubygems.org
104
134
  rdoc_options: []
105
135
  require_paths:
106
136
  - lib