Class: RailsConsoleAi::Channel::Slack

Inherits:
Base
  • Object
show all
Defined in:
lib/rails_console_ai/channel/slack.rb

Constant Summary collapse

ANSI_REGEX =
/\e\[[0-9;]*m/

Instance Method Summary collapse

Methods inherited from Base

#edit_code

Constructor Details

#initialize(slack_bot:, channel_id:, thread_ts:, user_name: nil) ⇒ Slack

Returns a new instance of Slack.



8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/rails_console_ai/channel/slack.rb', line 8

def initialize(slack_bot:, channel_id:, thread_ts:, user_name: nil)
  @slack_bot = slack_bot
  @channel_id = channel_id
  @thread_ts = thread_ts
  @user_name = user_name
  @reply_queue = Queue.new
  @guidance_main = []
  @guidance_sub = []
  @guidance_mutex = Mutex.new
  @cancelled = false
  @log_prefix = "[#{@channel_id}/#{@thread_ts}] @#{@user_name}"
  @output_log = StringIO.new
end

Instance Method Details

#add_guidance(text) ⇒ Object

Guidance is broadcast to both the main-engine queue and the sub-agent queue so a steering message arriving during a sub-agent run is seen by both layers (sub-agent reacts immediately; main engine reacts after delegate_task returns).



33
34
35
36
37
38
# File 'lib/rails_console_ai/channel/slack.rb', line 33

def add_guidance(text)
  @guidance_mutex.synchronize do
    @guidance_main << text
    @guidance_sub << text
  end
end

#cancel!Object



22
23
24
25
26
27
28
# File 'lib/rails_console_ai/channel/slack.rb', line 22

def cancel!
  @cancelled = true
  @guidance_mutex.synchronize do
    @guidance_main.clear
    @guidance_sub.clear
  end
end

#cancelled?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/rails_console_ai/channel/slack.rb', line 56

def cancelled?
  @cancelled
end

#confirm(_text) ⇒ Object



120
121
122
# File 'lib/rails_console_ai/channel/slack.rb', line 120

def confirm(_text)
  'y'
end

#console_capture_stringObject



188
189
190
# File 'lib/rails_console_ai/channel/slack.rb', line 188

def console_capture_string
  @output_log.string
end

#display(text) ⇒ Object



60
61
62
# File 'lib/rails_console_ai/channel/slack.rb', line 60

def display(text)
  post(strip_ansi(text))
end

#display_code(code) ⇒ Object



95
96
97
98
99
100
101
# File 'lib/rails_console_ai/channel/slack.rb', line 95

def display_code(code)
  # Don't post raw code/plan steps to Slack — non-technical users don't need to see Ruby
  # But do log to STDOUT so server logs show what was generated/executed
  @output_log.write("# Generated code:\n#{code}\n")
  STDOUT.puts "#{@log_prefix} (code)"
  code.each_line { |line| STDOUT.puts "#{@log_prefix} (code) #{line.rstrip}" }
end

#display_error(text) ⇒ Object



86
87
88
# File 'lib/rails_console_ai/channel/slack.rb', line 86

def display_error(text)
  post(":x: #{strip_ansi(text)}")
end

#display_result(_result) ⇒ Object



110
111
112
113
# File 'lib/rails_console_ai/channel/slack.rb', line 110

def display_result(_result)
  # Don't post raw return values to Slack — the LLM formats output via puts
  nil
end

#display_result_output(output) ⇒ Object



103
104
105
106
107
108
# File 'lib/rails_console_ai/channel/slack.rb', line 103

def display_result_output(output)
  text = strip_ansi(output).strip
  return if text.empty?
  text = text[0, 3000] + "\n... (truncated)" if text.length > 3000
  post("```#{text}```")
end

#display_status(text) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/rails_console_ai/channel/slack.rb', line 70

def display_status(text)
  stripped = strip_ansi(text).strip
  return if stripped.empty?

  if stripped =~ /\AThinking\.\.\.|\AAttempting to fix|\ACancelled|\A_session:/
    post(stripped)
  else
    @output_log.write("#{stripped}\n")
    log_prefixed("(status)", stripped)
  end
end

#display_thinking(text) ⇒ Object



64
65
66
67
68
# File 'lib/rails_console_ai/channel/slack.rb', line 64

def display_thinking(text)
  stripped = strip_ansi(text).strip
  return if stripped.empty?
  post(stripped)
end

#display_tool_call(text) ⇒ Object



90
91
92
93
# File 'lib/rails_console_ai/channel/slack.rb', line 90

def display_tool_call(text)
  @output_log.write("-> #{text}\n")
  log_prefixed("->", text)
end

#display_warning(text) ⇒ Object



82
83
84
# File 'lib/rails_console_ai/channel/slack.rb', line 82

def display_warning(text)
  post(":warning: #{strip_ansi(text)}")
end

#drain_guidance(scope: :main) ⇒ Object



40
41
42
43
44
45
46
47
# File 'lib/rails_console_ai/channel/slack.rb', line 40

def drain_guidance(scope: :main)
  @guidance_mutex.synchronize do
    arr = scope == :sub ? @guidance_sub : @guidance_main
    pending = arr.dup
    arr.clear
    pending
  end
end

#log_input(text) ⇒ Object



178
179
180
# File 'lib/rails_console_ai/channel/slack.rb', line 178

def log_input(text)
  @output_log.write("@#{@user_name}: #{text}\n")
end

#modeObject



128
129
130
# File 'lib/rails_console_ai/channel/slack.rb', line 128

def mode
  'slack'
end

#pending_guidance?(scope: :main) ⇒ Boolean

Returns:

  • (Boolean)


49
50
51
52
53
54
# File 'lib/rails_console_ai/channel/slack.rb', line 49

def pending_guidance?(scope: :main)
  @guidance_mutex.synchronize do
    arr = scope == :sub ? @guidance_sub : @guidance_main
    !arr.empty?
  end
end

#prompt(text) ⇒ Object



115
116
117
118
# File 'lib/rails_console_ai/channel/slack.rb', line 115

def prompt(text)
  post(strip_ansi(text))
  @reply_queue.pop
end

#receive_reply(text) ⇒ Object

Called by SlackBot when a thread reply arrives



183
184
185
186
# File 'lib/rails_console_ai/channel/slack.rb', line 183

def receive_reply(text)
  @output_log.write("@#{@user_name}: #{text}\n")
  @reply_queue.push(text)
end

#supports_danger?Boolean

Returns:

  • (Boolean)


132
133
134
# File 'lib/rails_console_ai/channel/slack.rb', line 132

def supports_danger?
  false
end

#supports_editing?Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/rails_console_ai/channel/slack.rb', line 136

def supports_editing?
  false
end

#system_instructionsObject



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/rails_console_ai/channel/slack.rb', line 144

def system_instructions
  <<~INSTRUCTIONS.strip
    ## Slack Channel

    You are responding in a Slack thread.

    ## Code Execution
    - ALWAYS use the `execute_code` tool to run Ruby code. Do NOT put code in markdown
      code fences expecting it to be executed — code fences are display-only in Slack.
    - Use `execute_code` for simple queries, and `execute_plan` for multi-step operations.
    - If the user asks you to provide code they can run later, put it in a code fence
      in your text response (it will be displayed but not executed).

    ## Formatting
    - Slack does NOT support markdown tables. For tabular data, use `puts` to print
      a plain-text table inside a code block. Use fixed-width columns with padding so
      columns align. Example format:
      ```
      ID   Name              Email
      123  John Smith        john@example.com
      456  Jane Doe          jane@example.com
      ```
    - Use `puts` with formatted output instead of returning arrays or hashes.
    - Never return raw Ruby objects — always present data in a human-readable way.
    - The output of `puts` in your code is automatically shown to the user. Do NOT
      repeat or re-display data that your code already printed via `puts`.
      Just add a brief summary after (e.g. "10 events found" or "Let me know if you need more detail").
    - Do not offer to make changes or take actions on behalf of the user. Only report findings.
    - This is a live production database — other processes, users, and background jobs are
      constantly changing data. Never assume results will be the same as a previous query.
      Always re-run queries when asked, even if you just ran the same one.
  INSTRUCTIONS
end

#user_identityObject



124
125
126
# File 'lib/rails_console_ai/channel/slack.rb', line 124

def user_identity
  @user_name
end

#wrap_llm_call(&block) ⇒ Object



140
141
142
# File 'lib/rails_console_ai/channel/slack.rb', line 140

def wrap_llm_call(&block)
  yield
end