Class: Harnex::Adapters::Opencode

Inherits:
Base
  • Object
show all
Defined in:
lib/harnex/adapters/opencode.rb

Constant Summary collapse

SUBMIT_DELAY_MS =
75
SUBMIT_DELAY_PER_KB_MS =
50
EXIT_SIGNAL_DELAY_MS =
100

Constants inherited from Base

Base::AGENT_VERSION_TIMEOUT_SECONDS, Base::PROMPT_PREFIXES

Instance Attribute Summary

Attributes inherited from Base

#key

Instance Method Summary collapse

Methods inherited from Base

#agent_version, #build_command, #describe, #send_wait_seconds, #transport, #wait_for_sendable, #wait_for_sendable_state?

Constructor Details

#initialize(extra_args = []) ⇒ Opencode

Returns a new instance of Opencode.



8
9
10
11
# File 'lib/harnex/adapters/opencode.rb', line 8

def initialize(extra_args = [])
  super("opencode", extra_args)
  @screen_seen = false
end

Instance Method Details

#base_commandObject



17
18
19
# File 'lib/harnex/adapters/opencode.rb', line 17

def base_command
  ["opencode"]
end

#build_send_payload(text:, submit:, enter_only:, screen_text:, force: false) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/harnex/adapters/opencode.rb', line 81

def build_send_payload(text:, submit:, enter_only:, screen_text:, force: false)
  state = input_state(screen_text)
  if !force && blocked_state?(state, enter_only: enter_only)
    raise ArgumentError, blocked_message(state, enter_only: enter_only)
  end

  steps = []
  unless enter_only
    body = text.to_s
    steps << { text: body, newline: false } unless body.empty?
  end

  if submit || enter_only
    step = { text: submit_bytes, newline: false }
    step[:delay_ms] = submit_delay_ms(text) if steps.any?
    steps << step
  end

  {
    steps: steps,
    input_state: state,
    force: force
  }
end

#infer_repo_path(argv) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/harnex/adapters/opencode.rb', line 21

def infer_repo_path(argv)
  index = 0
  while index < argv.length
    arg = argv[index].to_s
    case arg
    when "--dir"
      next_value = argv[index + 1]
      return next_value if next_value && !next_value.to_s.strip.empty?
      break
    when /\A--dir=(.+)\z/
      return Regexp.last_match(1)
    end
    index += 1
  end

  positional = argv.find { |value| value && !value.start_with?("-") }
  return positional if positional

  Dir.pwd
end

#inject_exit(writer) ⇒ Object



106
107
108
109
110
111
112
113
114
115
# File 'lib/harnex/adapters/opencode.rb', line 106

def inject_exit(writer)
  # Ctrl+C is OpenCode's native terminal stop path. A second signal
  # shortly after the first handles "interrupt-first, quit-second"
  # cases when work is in-flight.
  writer.write("\u0003")
  writer.flush
  sleep(EXIT_SIGNAL_DELAY_MS / 1000.0)
  writer.write("\u0003")
  writer.flush
end

#input_state(screen_text) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/harnex/adapters/opencode.rb', line 42

def input_state(screen_text)
  @screen_seen ||= !screen_text.to_s.empty?
  lines = recent_lines(screen_text, limit: 80)
  return prompt_state if @screen_seen && lines.empty?
  return prompt_state if lines.any? { |line| prompt_line?(line) }

  compact = lines.join(" ").gsub(/\s+/, " ")

  # OpenCode's TUI keeps rendering on an alternate screen and does
  # not expose a stable prompt token in plain text snapshots. Treat
  # observed screen content as send-ready so inbox messages don't
  # stall indefinitely in :unknown.
  return prompt_state if compact.match?(/\bOpenCode\b/i)
  return prompt_state if compact.match?(/\bSession\b.*\bContinue\b.*\bopencode\s+-s\b/i)
  return prompt_state if compact.match?(/[■⬝╹]/)

  return prompt_state if @screen_seen

  super
end

#parse_session_summary(transcript_tail) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/harnex/adapters/opencode.rb', line 63

def parse_session_summary(transcript_tail)
  summary = {
    input_tokens: nil,
    output_tokens: nil,
    reasoning_tokens: nil,
    cached_tokens: nil,
    total_tokens: nil,
    agent_session_id: nil
  }

  text = normalized_screen_text(transcript_tail)
  if (match = text.match(/\bContinue\s+opencode\s+-s\s+([A-Za-z0-9._:-]+)/))
    summary[:agent_session_id] = match[1]
  end

  summary
end

#providerObject



13
14
15
# File 'lib/harnex/adapters/opencode.rb', line 13

def provider
  "opencode"
end