KiWA001 commited on
Commit
80d9d9d
·
1 Parent(s): dc381e9

Use tmux to capture OpenCode terminal output

Browse files
Files changed (1) hide show
  1. opencode_microservice.py +75 -95
opencode_microservice.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- OpenCode Microservice - Proper Terminal Capture
3
- ===============================================
4
- Uses pexpect to properly interact with OpenCode TUI
5
  """
6
 
7
  import asyncio
@@ -10,13 +10,13 @@ import os
10
  import json
11
  import random
12
  import string
13
- import pexpect
14
- import time
15
  from fastapi import FastAPI, HTTPException
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from pydantic import BaseModel
18
  from typing import Optional
19
  import shutil
 
20
 
21
  logging.basicConfig(level=logging.INFO)
22
  logger = logging.getLogger("opencode_microservice")
@@ -33,12 +33,11 @@ app.add_middleware(
33
 
34
  class OpenCodeSession:
35
  def __init__(self):
36
- self.process: Optional[pexpect.spawn] = None
37
  self.message_count = 0
38
  self.max_messages = 20
39
  self.session_id = None
40
  self.is_running = False
41
- self.output_buffer = []
42
 
43
  def generate_identity(self):
44
  return {
@@ -46,8 +45,22 @@ class OpenCodeSession:
46
  "session_id": ''.join(random.choices(string.hexdigits.lower(), k=16)),
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  async def start(self):
50
- """Start fresh OpenCode session with proper terminal"""
51
  if self.is_running:
52
  await self.stop()
53
 
@@ -63,6 +76,9 @@ class OpenCodeSession:
63
  if os.path.exists(path):
64
  shutil.rmtree(path, ignore_errors=True)
65
 
 
 
 
66
  # Create config
67
  session_dir = f"/tmp/opencode_micro_{self.session_id}"
68
  os.makedirs(session_dir, exist_ok=True)
@@ -92,49 +108,44 @@ class OpenCodeSession:
92
  json.dump(config, f, indent=2)
93
 
94
  try:
95
- # Start OpenCode with pexpect (proper PTY)
96
- env = os.environ.copy()
97
- env['OPENCODE_CONFIG'] = f"{session_dir}/config.json"
98
- env['OPENCODE_NO_AUTH'] = '1'
99
- env['TERM'] = 'xterm-256color'
100
 
101
- self.process = pexpect.spawn(
102
- 'npx -y opencode-ai',
103
- env=env,
104
- timeout=30,
105
- maxread=50000,
106
- encoding='utf-8',
107
- codec_errors='ignore'
108
- )
109
 
110
- # Wait for startup
111
- await asyncio.sleep(4)
112
 
113
- # Wait for TUI to load
114
- try:
115
- self.process.expect(['OpenCode', 'Ask anything', 'What can I help'], timeout=10)
116
- except:
117
- pass # Continue anyway
118
 
119
  # Select model
120
- self.process.sendcontrol('x')
121
  await asyncio.sleep(0.5)
122
- self.process.send('m')
123
  await asyncio.sleep(1)
124
- self.process.send('\n')
125
  await asyncio.sleep(2)
126
 
127
- self.is_running = True
128
- self.message_count = 0
129
- self.output_buffer = []
130
-
131
- logger.info(f"✅ OpenCode started - Session: {self.session_id}")
132
  return True
133
 
134
  except Exception as e:
135
  logger.error(f"Failed to start OpenCode: {e}")
136
  return False
137
 
 
 
 
 
 
 
 
138
  async def chat(self, message: str) -> str:
139
  """Send message and capture response"""
140
  if not self.is_running:
@@ -143,45 +154,30 @@ class OpenCodeSession:
143
  return "Failed to start OpenCode"
144
 
145
  try:
146
- # Clear any pending output first
147
- try:
148
- while self.process.readline_nonblocking(size=1000, timeout=0.1):
149
- pass
150
- except:
151
- pass
152
 
153
  # Send message
154
- self.process.sendline(message)
155
-
156
- # Wait for response generation (AI needs time)
157
- await asyncio.sleep(12) # Increased wait time
158
-
159
- # Read all output with multiple attempts
160
- output_parts = []
161
- for _ in range(5): # Try multiple times
162
- try:
163
- # Read available output
164
- available = self.process.read_nonblocking(size=10000, timeout=2)
165
- if available:
166
- output_parts.append(available)
167
- except pexpect.TIMEOUT:
168
- break
169
- except:
170
- break
171
- await asyncio.sleep(1)
172
-
173
- output = "".join(output_parts)
174
-
175
- # Clean up output
176
- import re
177
- # Remove ANSI escape codes
178
- clean_output = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', output)
179
- # Remove other control characters
180
- clean_output = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]', '', clean_output)
181
- # Normalize whitespace
182
- clean_output = re.sub(r'\r\n', '\n', clean_output)
183
- clean_output = re.sub(r'\n+', '\n', clean_output)
184
- clean_output = clean_output.strip()
185
 
186
  self.message_count += 1
187
 
@@ -191,36 +187,20 @@ class OpenCodeSession:
191
  await self.stop()
192
  await self.start()
193
 
194
- # Return last few lines (the response)
195
- lines = clean_output.strip().split('\n')
196
- # Filter out empty lines and prompts
197
- response_lines = [l for l in lines if l.strip() and not l.startswith(('>', '$', '┌', '│', '└'))]
198
-
199
  if response_lines:
200
- return '\n'.join(response_lines[-10:]) # Return last 10 lines
 
201
  else:
202
- return "Response received (check terminal for full output)"
 
203
 
204
  except Exception as e:
205
  logger.error(f"Chat error: {e}")
206
  return f"Error: {str(e)}"
207
 
208
  async def stop(self):
209
- if self.process:
210
- try:
211
- self.process.sendline('/exit')
212
- await asyncio.sleep(1)
213
- except:
214
- pass
215
-
216
- if self.process.isalive():
217
- self.process.terminate()
218
- await asyncio.sleep(2)
219
- if self.process.isalive():
220
- self.process.kill()
221
-
222
- self.process = None
223
-
224
  self.is_running = False
225
  self.message_count = 0
226
  logger.info("🛑 OpenCode stopped")
 
1
  """
2
+ OpenCode Microservice - Tmux Screen Capture Approach
3
+ ====================================================
4
+ Uses tmux to capture terminal output
5
  """
6
 
7
  import asyncio
 
10
  import json
11
  import random
12
  import string
13
+ import subprocess
 
14
  from fastapi import FastAPI, HTTPException
15
  from fastapi.middleware.cors import CORSMiddleware
16
  from pydantic import BaseModel
17
  from typing import Optional
18
  import shutil
19
+ import time
20
 
21
  logging.basicConfig(level=logging.INFO)
22
  logger = logging.getLogger("opencode_microservice")
 
33
 
34
  class OpenCodeSession:
35
  def __init__(self):
36
+ self.tmux_session = "opencode"
37
  self.message_count = 0
38
  self.max_messages = 20
39
  self.session_id = None
40
  self.is_running = False
 
41
 
42
  def generate_identity(self):
43
  return {
 
45
  "session_id": ''.join(random.choices(string.hexdigits.lower(), k=16)),
46
  }
47
 
48
+ def run_tmux_cmd(self, cmd):
49
+ """Run tmux command"""
50
+ try:
51
+ result = subprocess.run(
52
+ ['tmux', '-L', self.tmux_session] + cmd,
53
+ capture_output=True,
54
+ text=True,
55
+ timeout=5
56
+ )
57
+ return result.stdout, result.stderr, result.returncode
58
+ except Exception as e:
59
+ logger.error(f"Tmux error: {e}")
60
+ return "", str(e), 1
61
+
62
  async def start(self):
63
+ """Start OpenCode in tmux session"""
64
  if self.is_running:
65
  await self.stop()
66
 
 
76
  if os.path.exists(path):
77
  shutil.rmtree(path, ignore_errors=True)
78
 
79
+ # Kill existing tmux session
80
+ subprocess.run(['tmux', 'kill-session', '-t', self.tmux_session], capture_output=True)
81
+
82
  # Create config
83
  session_dir = f"/tmp/opencode_micro_{self.session_id}"
84
  os.makedirs(session_dir, exist_ok=True)
 
108
  json.dump(config, f, indent=2)
109
 
110
  try:
111
+ # Create tmux session with OpenCode
112
+ env_vars = f"export OPENCODE_CONFIG={session_dir}/config.json; export OPENCODE_NO_AUTH=1;"
113
+ cmd = f'{env_vars} npx -y opencode-ai'
 
 
114
 
115
+ subprocess.Popen([
116
+ 'tmux', 'new-session', '-d', '-s', self.tmux_session,
117
+ '-c', session_dir,
118
+ 'bash', '-c', cmd
119
+ ])
 
 
 
120
 
121
+ self.is_running = True
122
+ self.message_count = 0
123
 
124
+ # Wait for startup
125
+ await asyncio.sleep(5)
 
 
 
126
 
127
  # Select model
128
+ self.run_tmux_cmd(['send-keys', 'C-x'])
129
  await asyncio.sleep(0.5)
130
+ self.run_tmux_cmd(['send-keys', 'm'])
131
  await asyncio.sleep(1)
132
+ self.run_tmux_cmd(['send-keys', 'Enter'])
133
  await asyncio.sleep(2)
134
 
135
+ logger.info(f"✅ OpenCode started in tmux - Session: {self.session_id}")
 
 
 
 
136
  return True
137
 
138
  except Exception as e:
139
  logger.error(f"Failed to start OpenCode: {e}")
140
  return False
141
 
142
+ def capture_screen(self):
143
+ """Capture tmux pane content"""
144
+ stdout, _, code = self.run_tmux_cmd(['capture-pane', '-p', '-S', '-100'])
145
+ if code == 0:
146
+ return stdout
147
+ return ""
148
+
149
  async def chat(self, message: str) -> str:
150
  """Send message and capture response"""
151
  if not self.is_running:
 
154
  return "Failed to start OpenCode"
155
 
156
  try:
157
+ # Get current screen before message
158
+ before_screen = self.capture_screen()
159
+ before_lines = set(before_screen.split('\n'))
 
 
 
160
 
161
  # Send message
162
+ self.run_tmux_cmd(['send-keys', message])
163
+ self.run_tmux_cmd(['send-keys', 'Enter'])
164
+
165
+ # Wait for AI response
166
+ await asyncio.sleep(15)
167
+
168
+ # Get screen after response
169
+ after_screen = self.capture_screen()
170
+ after_lines = after_screen.split('\n')
171
+
172
+ # Find new lines (the response)
173
+ response_lines = []
174
+ for line in after_lines:
175
+ line = line.strip()
176
+ # Skip empty lines, prompts, and old lines
177
+ if line and line not in before_lines and not line.startswith(('>', '$', '┌', '│', '└', '╭', '╰')):
178
+ # Skip the user's question
179
+ if message not in line:
180
+ response_lines.append(line)
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
  self.message_count += 1
183
 
 
187
  await self.stop()
188
  await self.start()
189
 
 
 
 
 
 
190
  if response_lines:
191
+ # Return last few lines (most recent response)
192
+ return '\n'.join(response_lines[-15:])
193
  else:
194
+ # Return full screen if we can't parse
195
+ return after_screen[-2000:] # Last 2000 chars
196
 
197
  except Exception as e:
198
  logger.error(f"Chat error: {e}")
199
  return f"Error: {str(e)}"
200
 
201
  async def stop(self):
202
+ """Stop tmux session"""
203
+ subprocess.run(['tmux', 'kill-session', '-t', self.tmux_session], capture_output=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  self.is_running = False
205
  self.message_count = 0
206
  logger.info("🛑 OpenCode stopped")