jules-ruby 0.0.67
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 +7 -0
- data/.jules/bolt.md +4 -0
- data/.rubocop.yml +51 -0
- data/AGENTS.md +250 -0
- data/CHANGELOG.md +20 -0
- data/CONTRIBUTING.md +82 -0
- data/LICENSE +21 -0
- data/README.md +330 -0
- data/Rakefile +70 -0
- data/SECURITY.md +41 -0
- data/assets/banner.png +0 -0
- data/bin/jules-ruby +7 -0
- data/jules-ruby.gemspec +43 -0
- data/lib/jules-ruby/cli/activities.rb +142 -0
- data/lib/jules-ruby/cli/banner.rb +113 -0
- data/lib/jules-ruby/cli/base.rb +38 -0
- data/lib/jules-ruby/cli/interactive/activity_renderer.rb +81 -0
- data/lib/jules-ruby/cli/interactive/session_creator.rb +112 -0
- data/lib/jules-ruby/cli/interactive/session_manager.rb +285 -0
- data/lib/jules-ruby/cli/interactive/source_manager.rb +65 -0
- data/lib/jules-ruby/cli/interactive.rb +48 -0
- data/lib/jules-ruby/cli/prompts.rb +184 -0
- data/lib/jules-ruby/cli/sessions.rb +185 -0
- data/lib/jules-ruby/cli/sources.rb +72 -0
- data/lib/jules-ruby/cli.rb +127 -0
- data/lib/jules-ruby/client.rb +130 -0
- data/lib/jules-ruby/configuration.rb +20 -0
- data/lib/jules-ruby/errors.rb +35 -0
- data/lib/jules-ruby/models/activity.rb +137 -0
- data/lib/jules-ruby/models/artifact.rb +78 -0
- data/lib/jules-ruby/models/github_branch.rb +17 -0
- data/lib/jules-ruby/models/github_repo.rb +31 -0
- data/lib/jules-ruby/models/plan.rb +23 -0
- data/lib/jules-ruby/models/plan_step.rb +25 -0
- data/lib/jules-ruby/models/pull_request.rb +23 -0
- data/lib/jules-ruby/models/session.rb +111 -0
- data/lib/jules-ruby/models/source.rb +23 -0
- data/lib/jules-ruby/models/source_context.rb +35 -0
- data/lib/jules-ruby/resources/activities.rb +76 -0
- data/lib/jules-ruby/resources/base.rb +27 -0
- data/lib/jules-ruby/resources/sessions.rb +125 -0
- data/lib/jules-ruby/resources/sources.rb +61 -0
- data/lib/jules-ruby/version.rb +5 -0
- data/lib/jules-ruby.rb +43 -0
- data/mise.toml +2 -0
- metadata +232 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JulesRuby
|
|
4
|
+
# ASCII art banner for the CLI
|
|
5
|
+
module Banner
|
|
6
|
+
# Octopus ASCII art - matching Jules logo
|
|
7
|
+
OCTOPUS = [
|
|
8
|
+
' ████████████████ ',
|
|
9
|
+
' ████████████████████████ ',
|
|
10
|
+
' ████████████████████████████ ',
|
|
11
|
+
' ██████████████████████████████ ',
|
|
12
|
+
' ████████████████████████████████ ',
|
|
13
|
+
' ████████████████████████████████ ',
|
|
14
|
+
' ████████████████████████████████ ',
|
|
15
|
+
' ████████ ████████████ ████████ ',
|
|
16
|
+
' ████████ ████████████ ████████ ',
|
|
17
|
+
' ████████████████████████████████ ',
|
|
18
|
+
' ██████████████████████████████ ',
|
|
19
|
+
' ████████████████████████████ ',
|
|
20
|
+
' ██ ██████████████ ██ ',
|
|
21
|
+
' ██ ████ ████ ██ ',
|
|
22
|
+
' ██ ████ ████ ██ ',
|
|
23
|
+
' ██ ████ ████ ██ ',
|
|
24
|
+
' ██ ██ ████ ████ ██ ██ ',
|
|
25
|
+
' ██ ██ ██ ██ ██ ██ ',
|
|
26
|
+
' ██████ ██████ '
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
# Jules text banner
|
|
30
|
+
JULES_TEXT = [
|
|
31
|
+
' ',
|
|
32
|
+
' ',
|
|
33
|
+
' ██╗██╗ ██╗██╗ ███████╗███████╗ ',
|
|
34
|
+
' ██║██║ ██║██║ ██╔════╝██╔════╝ ',
|
|
35
|
+
' ██║██║ ██║██║ █████╗ ███████╗ ',
|
|
36
|
+
' ██ ██║██║ ██║██║ ██╔══╝ ╚════██║ ',
|
|
37
|
+
' ╚█████╔╝╚██████╔╝███████╗███████╗███████║ ',
|
|
38
|
+
' ╚════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ',
|
|
39
|
+
' ',
|
|
40
|
+
' ██████╗ ██╗ ██╗██████╗ ██╗ ██ ',
|
|
41
|
+
' ██╔══██╗██║ ██║██╔══██╗╚██╗ ██╔╝ ',
|
|
42
|
+
' ██████╔╝██║ ██║██████╔╝ ╚████╔╝ ',
|
|
43
|
+
' ██╔══██╗██║ ██║██╔══██╗ ╚██╔╝ ',
|
|
44
|
+
' ██║ ██║╚██████╔╝██████╔╝ ██║ ',
|
|
45
|
+
' ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ',
|
|
46
|
+
' ',
|
|
47
|
+
' '
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
def hsl_to_rgb(h, s, l)
|
|
52
|
+
s /= 100.0
|
|
53
|
+
l /= 100.0
|
|
54
|
+
|
|
55
|
+
c = (1 - ((2 * l) - 1).abs) * s
|
|
56
|
+
x = c * (1 - (((h / 60.0) % 2) - 1).abs)
|
|
57
|
+
m = l - (c / 2)
|
|
58
|
+
|
|
59
|
+
r, g, b = case h
|
|
60
|
+
when 0...60 then [c, x, 0]
|
|
61
|
+
when 60...120 then [x, c, 0]
|
|
62
|
+
when 120...180 then [0, c, x]
|
|
63
|
+
when 180...240 then [0, x, c]
|
|
64
|
+
when 240...300 then [x, 0, c]
|
|
65
|
+
else [c, 0, x]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
[((r + m) * 255).round, ((g + m) * 255).round, ((b + m) * 255).round]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def print_banner
|
|
72
|
+
num_lines = [OCTOPUS.length, JULES_TEXT.length].max
|
|
73
|
+
octopus_width = OCTOPUS.map(&:length).max
|
|
74
|
+
jules_width = JULES_TEXT.first&.length || 0
|
|
75
|
+
|
|
76
|
+
num_lines.times do |row|
|
|
77
|
+
octopus_line = OCTOPUS[row] || ''
|
|
78
|
+
jules_line = JULES_TEXT[row] || ''
|
|
79
|
+
|
|
80
|
+
# Pad octopus line to consistent width
|
|
81
|
+
octopus_line = octopus_line.ljust(octopus_width)
|
|
82
|
+
|
|
83
|
+
# Print octopus with purple gradient
|
|
84
|
+
octopus_progress = row.to_f / num_lines
|
|
85
|
+
oct_h = 255 + (10 * octopus_progress)
|
|
86
|
+
oct_s = 80 - (5 * octopus_progress)
|
|
87
|
+
oct_l = 65 - (15 * octopus_progress)
|
|
88
|
+
oct_r, oct_g, oct_b = hsl_to_rgb(oct_h, oct_s, oct_l)
|
|
89
|
+
|
|
90
|
+
octopus_line.each_char do |char|
|
|
91
|
+
print "\e[38;2;#{oct_r};#{oct_g};#{oct_b}m#{char}\e[0m"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Print jules text with red-to-purple horizontal gradient
|
|
95
|
+
jules_line.each_char.with_index do |char, col|
|
|
96
|
+
progress = col.to_f / jules_width
|
|
97
|
+
|
|
98
|
+
# Red (hsl 348) to Purple (hsl 280)
|
|
99
|
+
h = 348 + ((280 - 348) * progress)
|
|
100
|
+
s = 83 + ((70 - 83) * progress)
|
|
101
|
+
l = 47 + ((45 - 47) * progress)
|
|
102
|
+
|
|
103
|
+
r, g, b = hsl_to_rgb(h, s, l)
|
|
104
|
+
print "\e[38;2;#{r};#{g};#{b}m#{char}\e[0m"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
puts
|
|
108
|
+
end
|
|
109
|
+
puts
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module JulesRuby
|
|
8
|
+
module Commands
|
|
9
|
+
# Base class for CLI subcommands with shared helper methods
|
|
10
|
+
class Base < Thor
|
|
11
|
+
# Helper to define the common format option
|
|
12
|
+
def self.format_option
|
|
13
|
+
method_option :format, type: :string, default: 'table', enum: %w[table json], desc: 'Output format'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
no_commands do
|
|
17
|
+
def client
|
|
18
|
+
@client ||= JulesRuby::Client.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def error_exit(error)
|
|
22
|
+
if options[:format] == 'json'
|
|
23
|
+
puts JSON.generate({ error: error.message })
|
|
24
|
+
else
|
|
25
|
+
warn "Error: #{error.message}"
|
|
26
|
+
end
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def truncate(str, length)
|
|
31
|
+
return '' unless str
|
|
32
|
+
|
|
33
|
+
str.length > length ? "#{str[0...(length - 3)]}..." : str
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../prompts'
|
|
4
|
+
|
|
5
|
+
module JulesRuby
|
|
6
|
+
class Interactive
|
|
7
|
+
# Renders activity items in the interactive CLI
|
|
8
|
+
class ActivityRenderer
|
|
9
|
+
def render(activity)
|
|
10
|
+
time = Prompts.time_ago_in_words(activity.create_time)
|
|
11
|
+
type_str = activity.type.to_s.gsub('_', ' ').capitalize
|
|
12
|
+
|
|
13
|
+
puts
|
|
14
|
+
puts Prompts.rgb_color(" ┌─ #{type_str} (#{time})", :muted)
|
|
15
|
+
|
|
16
|
+
display_activity_box_content(activity)
|
|
17
|
+
|
|
18
|
+
puts Prompts.rgb_color(' └─', :muted)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def display_activity_box_content(activity)
|
|
24
|
+
case activity.type
|
|
25
|
+
when :agent_messaged, :user_messaged
|
|
26
|
+
display_message_box(activity)
|
|
27
|
+
when :plan_generated
|
|
28
|
+
display_plan_box(activity)
|
|
29
|
+
when :progress_updated
|
|
30
|
+
display_progress_box(activity)
|
|
31
|
+
when :session_failed
|
|
32
|
+
display_failure_box(activity)
|
|
33
|
+
when :session_completed
|
|
34
|
+
display_completion_box
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def display_message_box(activity)
|
|
39
|
+
return unless activity.message
|
|
40
|
+
|
|
41
|
+
puts Prompts.rgb_color(' │', :muted)
|
|
42
|
+
wrap_text(activity.message, 72).each_line do |line|
|
|
43
|
+
puts "#{Prompts.rgb_color(' │', :muted)} #{Prompts.rgb_color(line.chomp, :purple)}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def display_plan_box(activity)
|
|
48
|
+
return unless activity.plan&.steps
|
|
49
|
+
|
|
50
|
+
puts Prompts.rgb_color(' │', :muted)
|
|
51
|
+
activity.plan.steps.each_with_index do |step, i|
|
|
52
|
+
puts "#{Prompts.rgb_color(' │', :muted)} #{Prompts.rgb_color("#{i + 1}. #{step.title}", :purple)}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def display_progress_box(activity)
|
|
57
|
+
puts "#{Prompts.rgb_color(' │', :muted)} #{Prompts.rgb_color(activity.progress_title, :purple)}"
|
|
58
|
+
return unless activity.progress_description
|
|
59
|
+
|
|
60
|
+
puts "#{Prompts.rgb_color(' │', :muted)} #{Prompts.rgb_color(activity.progress_description, :purple)}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def display_failure_box(activity)
|
|
64
|
+
puts Prompts.rgb_color(' │', :muted)
|
|
65
|
+
wrap_text(activity.failure_reason, 72).each_line do |line|
|
|
66
|
+
puts "#{Prompts.rgb_color(' │', :muted)} #{Prompts.rgb_color(line.chomp, :purple)}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def display_completion_box
|
|
71
|
+
puts "#{Prompts.rgb_color(' │', :muted)} #{Prompts.rgb_color('Session completed successfully', :purple)}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def wrap_text(text, width = 76)
|
|
75
|
+
return '' unless text
|
|
76
|
+
|
|
77
|
+
text.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../prompts'
|
|
4
|
+
|
|
5
|
+
module JulesRuby
|
|
6
|
+
class Interactive
|
|
7
|
+
# Wizard for creating new sessions
|
|
8
|
+
class SessionCreator
|
|
9
|
+
def initialize(client, prompt)
|
|
10
|
+
@client = client
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
Prompts.clear_screen
|
|
16
|
+
Prompts.print_banner
|
|
17
|
+
|
|
18
|
+
source = select_source
|
|
19
|
+
return unless source
|
|
20
|
+
|
|
21
|
+
branch = @prompt.ask(Prompts.rgb_color('Starting branch:', :lavender), default: 'main')
|
|
22
|
+
task_prompt = ask_for_prompt
|
|
23
|
+
title = @prompt.ask(Prompts.rgb_color('Session title (optional):', :lavender))
|
|
24
|
+
auto_pr = @prompt.yes?(Prompts.rgb_color('Auto-create PR when done?', :lavender), default: true)
|
|
25
|
+
|
|
26
|
+
display_session_summary(source, branch, task_prompt, title, auto_pr)
|
|
27
|
+
|
|
28
|
+
return unless @prompt.yes?(Prompts.rgb_color('Create this session?', :lavender), default: true)
|
|
29
|
+
|
|
30
|
+
create_and_display_session(source, branch, task_prompt, title, auto_pr)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def select_source
|
|
36
|
+
sources = Prompts.with_spinner('Loading sources...') do
|
|
37
|
+
@client.sources.all
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if sources.empty?
|
|
41
|
+
@prompt.error(Prompts.rgb_color('No sources found. Please connect a repository first.', :purple))
|
|
42
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
43
|
+
return nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
source_choices = sources.map { |s| Prompts.format_source_choice(s) }
|
|
47
|
+
@prompt.select(
|
|
48
|
+
Prompts.rgb_color('Select a repository:', :lavender),
|
|
49
|
+
source_choices,
|
|
50
|
+
filter: true,
|
|
51
|
+
per_page: 15
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ask_for_prompt
|
|
56
|
+
puts
|
|
57
|
+
@prompt.ask(Prompts.rgb_color('What would you like Jules to do?', :lavender)) do |q|
|
|
58
|
+
q.required true
|
|
59
|
+
q.validate(/\S/, 'Prompt cannot be empty')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def display_session_summary(source, branch, task_prompt, title, auto_pr)
|
|
64
|
+
puts
|
|
65
|
+
puts " 📋 #{Prompts.highlight('Session Summary')}"
|
|
66
|
+
puts " #{Prompts.divider}"
|
|
67
|
+
display_summary_field('📦 Repository:', source.github_repo&.full_name)
|
|
68
|
+
display_summary_field('🌿 Branch:', branch)
|
|
69
|
+
display_summary_field('📝 Prompt:', truncate(task_prompt, 50))
|
|
70
|
+
display_summary_field('🏷️ Title:', title || '(auto-generated)')
|
|
71
|
+
display_summary_field('🔄 Auto PR:', auto_pr ? 'Yes' : 'No')
|
|
72
|
+
puts
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def display_summary_field(label, value)
|
|
76
|
+
puts " #{Prompts.rgb_color(label, :lavender)} #{Prompts.rgb_color(value, :purple)}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_and_display_session(source, branch, task_prompt, title, auto_pr)
|
|
80
|
+
session = Prompts.with_spinner('Creating session...') do
|
|
81
|
+
source_context = {
|
|
82
|
+
'source' => source.name,
|
|
83
|
+
'githubRepoContext' => { 'startingBranch' => branch }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
params = {
|
|
87
|
+
prompt: task_prompt,
|
|
88
|
+
source_context: source_context
|
|
89
|
+
}
|
|
90
|
+
params[:title] = title if title && !title.empty?
|
|
91
|
+
params[:automation_mode] = 'AUTO_CREATE_PR' if auto_pr
|
|
92
|
+
|
|
93
|
+
@client.sessions.create(**params)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts
|
|
97
|
+
puts Prompts.rgb_color(" ✅ Session created: #{session.name}", :purple)
|
|
98
|
+
puts Prompts.rgb_color(" 🔗 URL: #{session.url}", :purple)
|
|
99
|
+
puts Prompts.rgb_color(" 📊 State: #{session.state}", :purple)
|
|
100
|
+
puts
|
|
101
|
+
|
|
102
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def truncate(str, length)
|
|
106
|
+
return '' unless str
|
|
107
|
+
|
|
108
|
+
str.length > length ? "#{str[0...(length - 3)]}..." : str
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../prompts'
|
|
4
|
+
require_relative 'activity_renderer'
|
|
5
|
+
require_relative 'session_creator'
|
|
6
|
+
|
|
7
|
+
module JulesRuby
|
|
8
|
+
class Interactive
|
|
9
|
+
# Manages viewing and interacting with sessions
|
|
10
|
+
class SessionManager
|
|
11
|
+
# States that should trigger auto-refresh
|
|
12
|
+
AUTO_REFRESH_STATES = %w[PLANNING IN_PROGRESS QUEUED].freeze
|
|
13
|
+
AUTO_REFRESH_INTERVAL = 60 # seconds
|
|
14
|
+
|
|
15
|
+
def initialize(client, prompt)
|
|
16
|
+
@client = client
|
|
17
|
+
@prompt = prompt
|
|
18
|
+
@activity_renderer = ActivityRenderer.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
loop do
|
|
23
|
+
Prompts.clear_screen
|
|
24
|
+
Prompts.print_banner
|
|
25
|
+
|
|
26
|
+
sessions = fetch_sessions
|
|
27
|
+
return if sessions.empty?
|
|
28
|
+
|
|
29
|
+
session = select_session(sessions)
|
|
30
|
+
break if session == :back
|
|
31
|
+
|
|
32
|
+
if session == :create
|
|
33
|
+
SessionCreator.new(@client, @prompt).run
|
|
34
|
+
else
|
|
35
|
+
session_detail(session)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def fetch_sessions
|
|
43
|
+
sessions = Prompts.with_spinner('Loading sessions...') { @client.sessions.all }
|
|
44
|
+
if sessions.empty?
|
|
45
|
+
@prompt.warn(Prompts.rgb_color('No sessions found.', :purple))
|
|
46
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
47
|
+
end
|
|
48
|
+
sessions
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def select_session(sessions)
|
|
52
|
+
choices = sessions.map { |s| Prompts.format_session_choice(s) }
|
|
53
|
+
choices.unshift({ name: "➕ #{Prompts.rgb_color('Create new session', :purple)}", value: :create })
|
|
54
|
+
choices << { name: "← #{Prompts.rgb_color('Back to main menu', :purple)}", value: :back }
|
|
55
|
+
|
|
56
|
+
@prompt.select(
|
|
57
|
+
Prompts.rgb_color('Select a session to view:', :lavender),
|
|
58
|
+
choices,
|
|
59
|
+
per_page: 15,
|
|
60
|
+
cycle: true
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def session_detail(session)
|
|
65
|
+
needs_activity_fetch = true
|
|
66
|
+
|
|
67
|
+
loop do
|
|
68
|
+
Prompts.clear_screen
|
|
69
|
+
Prompts.print_banner
|
|
70
|
+
display_session_header(session)
|
|
71
|
+
|
|
72
|
+
if needs_activity_fetch
|
|
73
|
+
latest_activity = fetch_latest_activity(session)
|
|
74
|
+
needs_activity_fetch = false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
display_latest_activity(latest_activity)
|
|
78
|
+
puts
|
|
79
|
+
|
|
80
|
+
choices = get_session_choices(session)
|
|
81
|
+
action = select_with_auto_refresh(choices, session)
|
|
82
|
+
result = handle_session_action(action, session)
|
|
83
|
+
|
|
84
|
+
break if result == :back
|
|
85
|
+
break if result == :deleted
|
|
86
|
+
|
|
87
|
+
session, needs_activity_fetch = update_session_state(session, result, needs_activity_fetch)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def display_session_header(session)
|
|
92
|
+
display_title_line(session)
|
|
93
|
+
puts " #{Prompts.divider}"
|
|
94
|
+
puts
|
|
95
|
+
display_header_field('ID:', session.id)
|
|
96
|
+
display_header_field('State:', Prompts.state_label(session.state))
|
|
97
|
+
display_header_field('Prompt:', truncate(session.prompt, 60))
|
|
98
|
+
display_header_field('URL:', session.url) if session.url
|
|
99
|
+
display_header_field('Created:', Prompts.format_datetime(session.create_time))
|
|
100
|
+
display_header_field('Updated:', Prompts.format_datetime(session.update_time))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def display_title_line(session)
|
|
104
|
+
puts " 📋 #{Prompts.highlight(session.title || 'Session Details')} #{Prompts.state_emoji(session.state)}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def display_header_field(label, value)
|
|
108
|
+
puts " #{Prompts.rgb_color(label, :lavender)} #{Prompts.rgb_color(value, :purple)}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fetch_latest_activity(session)
|
|
112
|
+
activities = Prompts.with_spinner('Loading latest activity...') do
|
|
113
|
+
@client.activities.all(session.name)
|
|
114
|
+
end
|
|
115
|
+
activities&.last
|
|
116
|
+
rescue StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def display_latest_activity(latest_activity)
|
|
121
|
+
return unless latest_activity
|
|
122
|
+
|
|
123
|
+
puts
|
|
124
|
+
puts " 📍 #{Prompts.highlight('Latest Activity')}"
|
|
125
|
+
puts " #{Prompts.divider}"
|
|
126
|
+
time = Prompts.time_ago_in_words(latest_activity.create_time)
|
|
127
|
+
type_str = latest_activity.type.to_s.gsub('_', ' ').capitalize
|
|
128
|
+
puts Prompts.rgb_color(" #{type_str} (#{time})", :purple)
|
|
129
|
+
puts
|
|
130
|
+
display_activity_content(latest_activity)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def display_activity_content(activity)
|
|
134
|
+
# Reuse ActivityRenderer logic? Or just minimal display for header?
|
|
135
|
+
# The original code had simplified display.
|
|
136
|
+
# "display_plan_simple" etc.
|
|
137
|
+
# I'll adapt the original simple display logic here or use ActivityRenderer.
|
|
138
|
+
# The original code had specific `display_activity_content` separate from `display_activity_box_content`.
|
|
139
|
+
|
|
140
|
+
case activity.type
|
|
141
|
+
when :agent_messaged, :user_messaged
|
|
142
|
+
wrap_text(activity.message, 76).each_line { |line| puts Prompts.rgb_color(" #{line}", :purple) }
|
|
143
|
+
when :plan_generated
|
|
144
|
+
display_plan_simple(activity)
|
|
145
|
+
when :progress_updated
|
|
146
|
+
puts Prompts.rgb_color(" #{activity.progress_title}", :purple)
|
|
147
|
+
puts Prompts.rgb_color(" #{activity.progress_description}", :purple) if activity.progress_description
|
|
148
|
+
when :session_failed
|
|
149
|
+
wrap_text(activity.failure_reason, 76).each_line { |line| puts Prompts.rgb_color(" #{line}", :purple) }
|
|
150
|
+
when :session_completed
|
|
151
|
+
puts Prompts.rgb_color(' Session completed successfully', :purple)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def display_plan_simple(activity)
|
|
156
|
+
return unless activity.plan&.steps
|
|
157
|
+
|
|
158
|
+
activity.plan.steps.each_with_index do |step, i|
|
|
159
|
+
puts Prompts.rgb_color(" #{i + 1}. #{step.title}", :purple)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def get_session_choices(session)
|
|
164
|
+
choices = []
|
|
165
|
+
if session.awaiting_plan_approval?
|
|
166
|
+
choices << { name: "✅ #{Prompts.rgb_color('Approve Plan', :purple)}", value: :approve }
|
|
167
|
+
end
|
|
168
|
+
choices << { name: "💬 #{Prompts.rgb_color('Send Message', :purple)}", value: :message }
|
|
169
|
+
choices << { name: "📜 #{Prompts.rgb_color('View Activities', :purple)}", value: :activities }
|
|
170
|
+
choices << { name: "🌐 #{Prompts.rgb_color('Open in Browser', :purple)}", value: :open_url } if session.url
|
|
171
|
+
choices << { name: "🗑️ #{Prompts.rgb_color('Delete Session', :purple)}", value: :delete }
|
|
172
|
+
choices << { name: "🔄 #{Prompts.rgb_color('Refresh', :purple)}", value: :refresh }
|
|
173
|
+
choices << { name: "← #{Prompts.rgb_color('Back', :purple)}", value: :back }
|
|
174
|
+
choices
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def select_with_auto_refresh(choices, session)
|
|
178
|
+
if AUTO_REFRESH_STATES.include?(session.state)
|
|
179
|
+
puts Prompts.rgb_color(" ⏱️ Auto-refresh in #{AUTO_REFRESH_INTERVAL}s (press any key for menu)", :purple)
|
|
180
|
+
puts
|
|
181
|
+
key = @prompt.keypress(timeout: AUTO_REFRESH_INTERVAL)
|
|
182
|
+
return :refresh unless key
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
@prompt.select(Prompts.rgb_color('Action:', :lavender), choices, cycle: true)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def handle_session_action(action, session)
|
|
189
|
+
case action
|
|
190
|
+
when :approve then approve_plan(session)
|
|
191
|
+
when :message then send_message(session)
|
|
192
|
+
when :activities
|
|
193
|
+
view_activities(session)
|
|
194
|
+
:refresh
|
|
195
|
+
when :open_url
|
|
196
|
+
system('open', session.url) if session.url
|
|
197
|
+
nil
|
|
198
|
+
when :delete then delete_session?(session) ? :deleted : nil
|
|
199
|
+
when :refresh then refresh_session(session)
|
|
200
|
+
when :back then :back
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def approve_plan(session)
|
|
205
|
+
Prompts.with_spinner('Approving plan...') do
|
|
206
|
+
@client.sessions.approve_plan(session.name)
|
|
207
|
+
end
|
|
208
|
+
puts Prompts.rgb_color("\n ✅ Plan approved!", :purple)
|
|
209
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
210
|
+
@client.sessions.find(session.name)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def send_message(session)
|
|
214
|
+
msg = @prompt.ask(Prompts.rgb_color('Message to send:', :lavender))
|
|
215
|
+
return unless msg && !msg.empty?
|
|
216
|
+
|
|
217
|
+
Prompts.with_spinner('Sending message...') do
|
|
218
|
+
@client.sessions.send_message(session.name, prompt: msg)
|
|
219
|
+
end
|
|
220
|
+
puts Prompts.rgb_color("\n ✅ Message sent!", :purple)
|
|
221
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
222
|
+
@client.sessions.find(session.name)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def view_activities(session)
|
|
226
|
+
Prompts.clear_screen
|
|
227
|
+
Prompts.print_banner
|
|
228
|
+
|
|
229
|
+
activities = Prompts.with_spinner('Loading activities...') { @client.activities.all(session.name) }
|
|
230
|
+
|
|
231
|
+
if activities.empty?
|
|
232
|
+
@prompt.warn(Prompts.rgb_color('No activities found.', :purple))
|
|
233
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
234
|
+
return
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
activities.each { |activity| @activity_renderer.render(activity) }
|
|
238
|
+
puts
|
|
239
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def delete_session?(session)
|
|
243
|
+
return false unless @prompt.yes?(
|
|
244
|
+
Prompts.rgb_color("Are you sure you want to delete session #{session.id}?", :lavender), default: false
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
Prompts.with_spinner('Deleting session...') do
|
|
248
|
+
@client.sessions.destroy(session.name)
|
|
249
|
+
end
|
|
250
|
+
puts Prompts.rgb_color("\n ✅ Session deleted!", :purple)
|
|
251
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def refresh_session(session)
|
|
256
|
+
Prompts.with_spinner('Refreshing...') do
|
|
257
|
+
@client.sessions.find(session.name)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def update_session_state(session, result, needs_activity_fetch)
|
|
262
|
+
if result.is_a?(JulesRuby::Models::Session)
|
|
263
|
+
[result, true]
|
|
264
|
+
elsif result == :refresh
|
|
265
|
+
[session, true]
|
|
266
|
+
else
|
|
267
|
+
[session, needs_activity_fetch]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def wrap_text(text, width = 76)
|
|
272
|
+
return '' unless text
|
|
273
|
+
|
|
274
|
+
text.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def truncate(text, length)
|
|
278
|
+
return '' unless text
|
|
279
|
+
return text if text.length <= length
|
|
280
|
+
|
|
281
|
+
"#{text.slice(0, length)}..."
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../prompts'
|
|
4
|
+
|
|
5
|
+
module JulesRuby
|
|
6
|
+
class Interactive
|
|
7
|
+
# Manages viewing sources
|
|
8
|
+
class SourceManager
|
|
9
|
+
def initialize(client, prompt)
|
|
10
|
+
@client = client
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
Prompts.clear_screen
|
|
16
|
+
Prompts.print_banner
|
|
17
|
+
|
|
18
|
+
sources = fetch_sources
|
|
19
|
+
return if sources.empty?
|
|
20
|
+
|
|
21
|
+
source = select_source_to_view(sources)
|
|
22
|
+
return if source == :back
|
|
23
|
+
|
|
24
|
+
display_source_details(source)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def fetch_sources
|
|
30
|
+
sources = Prompts.with_spinner('Loading sources...') { @client.sources.all }
|
|
31
|
+
if sources.empty?
|
|
32
|
+
@prompt.warn(Prompts.rgb_color('No sources found.', :purple))
|
|
33
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
34
|
+
end
|
|
35
|
+
sources
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def select_source_to_view(sources)
|
|
39
|
+
choices = sources.map { |s| Prompts.format_source_choice(s) }
|
|
40
|
+
choices << { name: "← #{Prompts.rgb_color('Back to main menu', :purple)}", value: :back }
|
|
41
|
+
|
|
42
|
+
@prompt.select(
|
|
43
|
+
Prompts.rgb_color('Select a source to view:', :lavender),
|
|
44
|
+
choices,
|
|
45
|
+
filter: true,
|
|
46
|
+
per_page: 15,
|
|
47
|
+
cycle: true
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def display_source_details(source)
|
|
52
|
+
puts
|
|
53
|
+
puts " #{Prompts.rgb_color('Name:', :lavender)} #{Prompts.rgb_color(source.name, :purple)}"
|
|
54
|
+
puts " #{Prompts.rgb_color('ID:', :lavender)} #{Prompts.rgb_color(source.id, :purple)}"
|
|
55
|
+
|
|
56
|
+
repo_label = Prompts.rgb_color('Repository:', :lavender)
|
|
57
|
+
repo_val = Prompts.rgb_color(source.github_repo&.full_name, :purple)
|
|
58
|
+
puts " #{repo_label} #{repo_val}"
|
|
59
|
+
puts
|
|
60
|
+
|
|
61
|
+
@prompt.keypress(Prompts.rgb_color('Press any key to continue...', :dim))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|