updates al copilot
This commit is contained in:
@@ -164,6 +164,7 @@ connpy_roadmap.md
|
|||||||
MULTI_USER_PLAN.md
|
MULTI_USER_PLAN.md
|
||||||
COPILOT_PLAN.md
|
COPILOT_PLAN.md
|
||||||
ARCHITECTURAL_DEBT_REFACTOR.md
|
ARCHITECTURAL_DEBT_REFACTOR.md
|
||||||
|
COPILOT_UI_FEATURES.md
|
||||||
|
|
||||||
#themes
|
#themes
|
||||||
nord.yml
|
nord.yml
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ class ai:
|
|||||||
- COMPLETE MISSIONS: Execute ALL steps of a mission before reporting back.
|
- COMPLETE MISSIONS: Execute ALL steps of a mission before reporting back.
|
||||||
- DIAGRAM: Use ASCII art or Unicode box-drawing characters directly in your responses to visualize topologies or paths when helpful.
|
- DIAGRAM: Use ASCII art or Unicode box-drawing characters directly in your responses to visualize topologies or paths when helpful.
|
||||||
- EVIDENCE: Include 'Key Snippets' from tool outputs. Be token-efficient.
|
- EVIDENCE: Include 'Key Snippets' from tool outputs. Be token-efficient.
|
||||||
|
- LANGUAGE: You MUST respond in the same language used by the user in their question or instruction.
|
||||||
- NO WANDERING: Do not speculate. If stuck, report attempts.
|
- NO WANDERING: Do not speculate. If stuck, report attempts.
|
||||||
- SAFETY: When you use 'run_commands' with configuration commands, the system automatically prompts the user for confirmation. Just execute - don't ask permission first.
|
- SAFETY: When you use 'run_commands' with configuration commands, the system automatically prompts the user for confirmation. Just execute - don't ask permission first.
|
||||||
{architect_instructions}
|
{architect_instructions}
|
||||||
@@ -222,6 +223,7 @@ class ai:
|
|||||||
- ENGINEER CAPABILITIES: Your Engineer can:
|
- ENGINEER CAPABILITIES: Your Engineer can:
|
||||||
* Filter nodes (list_nodes), Run CLI commands (run_commands), Get metadata (get_node_info).
|
* Filter nodes (list_nodes), Run CLI commands (run_commands), Get metadata (get_node_info).
|
||||||
- ANALYSIS: Review technical findings to identify patterns or design failures.
|
- ANALYSIS: Review technical findings to identify patterns or design failures.
|
||||||
|
- LANGUAGE: You MUST respond in the same language used by the user in their question or instruction.
|
||||||
- MEMORY: Update long-term facts ONLY when the user explicitly requests it.
|
- MEMORY: Update long-term facts ONLY when the user explicitly requests it.
|
||||||
|
|
||||||
CRITICAL - EFFICIENT DELEGATION:
|
CRITICAL - EFFICIENT DELEGATION:
|
||||||
@@ -1334,6 +1336,7 @@ Your brief tactical guide in markdown.
|
|||||||
low
|
low
|
||||||
</risk>
|
</risk>
|
||||||
6. Risk level is usually "low" for read-only/no commands.
|
6. Risk level is usually "low" for read-only/no commands.
|
||||||
|
7. You MUST respond in the same language used by the user in their question.
|
||||||
|
|
||||||
Terminal Context:
|
Terminal Context:
|
||||||
{terminal_buffer}
|
{terminal_buffer}
|
||||||
@@ -1359,6 +1362,7 @@ command 2
|
|||||||
low, high, or destructive
|
low, high, or destructive
|
||||||
</risk>
|
</risk>
|
||||||
6. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
|
6. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
|
||||||
|
7. You MUST respond in the same language used by the user in their question.
|
||||||
|
|
||||||
Terminal Context:
|
Terminal Context:
|
||||||
{terminal_buffer}
|
{terminal_buffer}
|
||||||
|
|||||||
@@ -57,9 +57,10 @@ class CopilotInterface:
|
|||||||
|
|
||||||
async def run_session(self,
|
async def run_session(self,
|
||||||
raw_bytes: bytes,
|
raw_bytes: bytes,
|
||||||
cmd_byte_positions: List[tuple],
|
|
||||||
node_info: dict,
|
node_info: dict,
|
||||||
on_ai_call: Callable):
|
on_ai_call: Callable,
|
||||||
|
cmd_byte_positions: List[tuple] = None,
|
||||||
|
blocks: List[tuple] = None):
|
||||||
"""
|
"""
|
||||||
Runs the interactive Copilot session.
|
Runs the interactive Copilot session.
|
||||||
on_ai_call: async function(active_buffer, question) -> result_dict
|
on_ai_call: async function(active_buffer, question) -> result_dict
|
||||||
@@ -69,9 +70,11 @@ class CopilotInterface:
|
|||||||
try:
|
try:
|
||||||
# Prepare UI state
|
# Prepare UI state
|
||||||
buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||||
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
|
|
||||||
last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)"
|
# Use pre-calculated blocks if provided (remote mode), otherwise calculate locally (local mode)
|
||||||
blocks.append((len(raw_bytes), last_line[:80]))
|
if blocks is None:
|
||||||
|
last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)"
|
||||||
|
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info, last_line=last_line)
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
'context_cmd': 1,
|
'context_cmd': 1,
|
||||||
@@ -88,7 +91,7 @@ class CopilotInterface:
|
|||||||
self.console.print("") # Salto de línea real
|
self.console.print("") # Salto de línea real
|
||||||
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
||||||
self.console.print(Panel(
|
self.console.print(Panel(
|
||||||
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n"
|
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n"
|
||||||
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
|
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
|
||||||
border_style="cyan"
|
border_style="cyan"
|
||||||
))
|
))
|
||||||
|
|||||||
+47
-21
@@ -211,6 +211,7 @@ class node:
|
|||||||
self.output = ""
|
self.output = ""
|
||||||
self.status = 1
|
self.status = 1
|
||||||
self.result = {}
|
self.result = {}
|
||||||
|
self.cmd_byte_positions = [(0, None)]
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
def _passtx(self, passwords, *, keyfile=None):
|
def _passtx(self, passwords, *, keyfile=None):
|
||||||
@@ -385,9 +386,9 @@ class node:
|
|||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
child_reader_queue = asyncio.Queue()
|
child_reader_queue = asyncio.Queue()
|
||||||
|
|
||||||
# Track command byte positions for copilot context navigation
|
# Reset and track command byte positions for copilot context navigation
|
||||||
# Each entry is (byte_position, command_text_or_None)
|
# Each entry is (byte_position, command_text_or_None)
|
||||||
cmd_byte_positions = [(0, None)]
|
self.cmd_byte_positions = [(self.mylog.tell() if hasattr(self, 'mylog') else 0, None)]
|
||||||
|
|
||||||
def _child_read_ready():
|
def _child_read_ready():
|
||||||
try:
|
try:
|
||||||
@@ -428,7 +429,7 @@ class node:
|
|||||||
node_info["prompt"] = to_str(self.tags.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$'))
|
node_info["prompt"] = to_str(self.tags.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$'))
|
||||||
|
|
||||||
# Invoke copilot (async callback handles UI)
|
# Invoke copilot (async callback handles UI)
|
||||||
await copilot_handler(self.mylog.getvalue(), node_info, local_stream, child_fd, cmd_byte_positions)
|
await copilot_handler(self.mylog.getvalue(), node_info, local_stream, child_fd, self.cmd_byte_positions)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Remove any stray \x00 bytes and forward normally
|
# Remove any stray \x00 bytes and forward normally
|
||||||
@@ -436,10 +437,9 @@ class node:
|
|||||||
if clean_data:
|
if clean_data:
|
||||||
# Track command boundaries when user hits Enter
|
# Track command boundaries when user hits Enter
|
||||||
if hasattr(self, 'mylog') and (b'\r' in clean_data or b'\n' in clean_data):
|
if hasattr(self, 'mylog') and (b'\r' in clean_data or b'\n' in clean_data):
|
||||||
cmd_byte_positions.append((self.mylog.tell(), None))
|
self.cmd_byte_positions.append((self.mylog.tell(), None))
|
||||||
|
|
||||||
try:
|
try: os.write(child_fd, clean_data)
|
||||||
os.write(child_fd, clean_data)
|
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
self.lastinput = time()
|
self.lastinput = time()
|
||||||
@@ -561,6 +561,45 @@ class node:
|
|||||||
finally:
|
finally:
|
||||||
local_stream.teardown()
|
local_stream.teardown()
|
||||||
|
|
||||||
|
@MethodHook
|
||||||
|
async def inject_commands(self, commands, child_fd, on_inject=None):
|
||||||
|
"""
|
||||||
|
Inject a list of commands into the node's PTY.
|
||||||
|
Handles screen_length_command, history tracking and delays.
|
||||||
|
"""
|
||||||
|
if not commands:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 0. Clear line
|
||||||
|
os.write(child_fd, b'\x15')
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# 1. Prepare list (prepend screen_length if exists)
|
||||||
|
slc = self.tags.get("screen_length_command") if hasattr(self, 'tags') and isinstance(self.tags, dict) else None
|
||||||
|
|
||||||
|
to_send = list(commands)
|
||||||
|
if slc and slc not in to_send: # avoid duplicates if already there
|
||||||
|
to_send.insert(0, slc)
|
||||||
|
|
||||||
|
# 2. Inject one by one
|
||||||
|
for cmd in to_send:
|
||||||
|
# Register in node's official history (SKIP if it's the administrative screen length command)
|
||||||
|
if cmd != slc and hasattr(self, 'cmd_byte_positions') and self.cmd_byte_positions is not None:
|
||||||
|
log_pos = self.mylog.tell() if hasattr(self, 'mylog') else 0
|
||||||
|
self.cmd_byte_positions.append((log_pos, cmd))
|
||||||
|
|
||||||
|
# Write physically to PTY
|
||||||
|
os.write(child_fd, (cmd + "\n").encode())
|
||||||
|
|
||||||
|
# Notify (e.g., for gRPC or logs) - SKIP for administrative SLC
|
||||||
|
if on_inject and cmd != slc:
|
||||||
|
if asyncio.iscoroutinefunction(on_inject):
|
||||||
|
await on_inject(cmd)
|
||||||
|
else:
|
||||||
|
on_inject(cmd)
|
||||||
|
|
||||||
|
# Delay to avoid overwhelming the router
|
||||||
|
await asyncio.sleep(0.8)
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
def interact(self, debug=False, logger=None):
|
def interact(self, debug=False, logger=None):
|
||||||
@@ -642,7 +681,7 @@ class node:
|
|||||||
while True:
|
while True:
|
||||||
action, commands, custom_cmd = await interface.run_session(
|
action, commands, custom_cmd = await interface.run_session(
|
||||||
raw_bytes=raw_bytes,
|
raw_bytes=raw_bytes,
|
||||||
cmd_byte_positions=cmd_byte_positions,
|
cmd_byte_positions=self.cmd_byte_positions,
|
||||||
node_info=node_info,
|
node_info=node_info,
|
||||||
on_ai_call=on_ai_call
|
on_ai_call=on_ai_call
|
||||||
)
|
)
|
||||||
@@ -658,20 +697,7 @@ class node:
|
|||||||
|
|
||||||
if action in ("send_all", "custom"):
|
if action in ("send_all", "custom"):
|
||||||
cmds_to_send = commands if action == "send_all" else custom_cmd
|
cmds_to_send = commands if action == "send_all" else custom_cmd
|
||||||
|
await self.inject_commands(cmds_to_send, child_fd)
|
||||||
if cmds_to_send:
|
|
||||||
os.write(child_fd, b'\x15') # Ctrl+U
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# Prepend screen length command to avoid pagination
|
|
||||||
if "screen_length_command" in self.tags:
|
|
||||||
cmds_to_send.insert(0, self.tags["screen_length_command"])
|
|
||||||
|
|
||||||
for cmd in cmds_to_send:
|
|
||||||
if cmd_byte_positions is not None:
|
|
||||||
cmd_byte_positions.append((self.mylog.tell(), cmd))
|
|
||||||
os.write(child_fd, (cmd + "\n").encode())
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
else:
|
else:
|
||||||
os.write(child_fd, b'\x15\r')
|
os.write(child_fd, b'\x15\r')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+54
-37
@@ -207,15 +207,34 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
from ..services.ai_service import AIService
|
||||||
|
|
||||||
|
service = AIService(self.service.config)
|
||||||
|
|
||||||
if node_info is None:
|
if node_info is None:
|
||||||
node_info = {}
|
node_info = {}
|
||||||
|
|
||||||
|
# Calculate real command blocks from history using the central service
|
||||||
|
raw_bytes = n.mylog.getvalue() if hasattr(n, 'mylog') else buffer
|
||||||
|
if not isinstance(raw_bytes, bytes):
|
||||||
|
raw_bytes = str(raw_bytes).encode()
|
||||||
|
|
||||||
|
from connpy.utils import log_cleaner
|
||||||
|
last_line = log_cleaner(raw_bytes.decode(errors='replace')).split('\n')[-1].strip()
|
||||||
|
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
|
||||||
|
node_info["context_blocks"] = blocks
|
||||||
|
|
||||||
node_info_json = json.dumps(node_info)
|
node_info_json = json.dumps(node_info)
|
||||||
|
|
||||||
# Convert buffer to string if it's bytes for the preview
|
# Convert buffer to string if it's bytes for the preview
|
||||||
preview_str = buffer[-200:].decode(errors='replace') if isinstance(buffer, bytes) else str(buffer)[-200:]
|
preview_str = buffer[-200:].decode(errors='replace') if isinstance(buffer, bytes) else str(buffer)[-200:]
|
||||||
|
|
||||||
|
# Generate a unique session ID for this copilot interaction to prevent race conditions
|
||||||
|
import uuid
|
||||||
|
copilot_session_id = str(uuid.uuid4())
|
||||||
|
node_info["session_id"] = copilot_session_id
|
||||||
|
node_info_json = json.dumps(node_info)
|
||||||
|
|
||||||
# 1. Send prompt to client
|
# 1. Send prompt to client
|
||||||
response_queue.put(connpy_pb2.InteractResponse(
|
response_queue.put(connpy_pb2.InteractResponse(
|
||||||
copilot_prompt=True,
|
copilot_prompt=True,
|
||||||
@@ -224,6 +243,13 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
))
|
))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# 0. Drain the queue of any stale messages before starting a new interaction
|
||||||
|
while not remote_stream.copilot_queue.empty():
|
||||||
|
try:
|
||||||
|
remote_stream.copilot_queue.get_nowait()
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
# 2. Await the question from client via the copilot_queue
|
# 2. Await the question from client via the copilot_queue
|
||||||
import threading
|
import threading
|
||||||
def preload_ai_deps():
|
def preload_ai_deps():
|
||||||
@@ -236,8 +262,17 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
try:
|
try:
|
||||||
req_data = await asyncio.wait_for(remote_stream.copilot_queue.get(), timeout=120)
|
req_data = await asyncio.wait_for(remote_stream.copilot_queue.get(), timeout=120)
|
||||||
if not req_data: return
|
if not req_data: return
|
||||||
if "question" not in req_data or not req_data["question"] or req_data["question"] == "CANCEL" or req_data.get("action") == "cancel":
|
|
||||||
os.write(child_fd, b'\x15\r')
|
# Validate session ID if provided by client (skip validation if not provided for CLI compatibility)
|
||||||
|
req_session_id = req_data.get("session_id")
|
||||||
|
if req_session_id and req_session_id != copilot_session_id:
|
||||||
|
continue # Ignore stale request from a previous session
|
||||||
|
|
||||||
|
if "question" not in req_data or not req_data["question"] or req_data["question"] == "CANCEL" or req_data.get("action") in ("cancel", "web_cancel"):
|
||||||
|
if req_data.get("action") == "web_cancel":
|
||||||
|
os.write(child_fd, b'\x05')
|
||||||
|
else:
|
||||||
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
question = req_data["question"]
|
question = req_data["question"]
|
||||||
|
|
||||||
@@ -264,9 +299,6 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 3. Call AI Service with streaming
|
# 3. Call AI Service with streaming
|
||||||
from ..services.ai_service import AIService
|
|
||||||
service = AIService(self.service.config)
|
|
||||||
|
|
||||||
def chunk_callback(chunk_text):
|
def chunk_callback(chunk_text):
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
response_queue.put(connpy_pb2.InteractResponse(
|
response_queue.put(connpy_pb2.InteractResponse(
|
||||||
@@ -287,8 +319,11 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
if wait_action_task in done:
|
if wait_action_task in done:
|
||||||
req_data = wait_action_task.result()
|
req_data = wait_action_task.result()
|
||||||
ai_task.cancel()
|
ai_task.cancel()
|
||||||
if req_data.get("action") == "cancel" or req_data.get("question") == "CANCEL":
|
if req_data.get("action") in ("cancel", "web_cancel") or req_data.get("question") == "CANCEL":
|
||||||
os.write(child_fd, b'\x15\r')
|
if req_data.get("action") == "web_cancel":
|
||||||
|
os.write(child_fd, b'\x05')
|
||||||
|
else:
|
||||||
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
continue # Loop back instead of returning to keep session alive
|
continue # Loop back instead of returning to keep session alive
|
||||||
else:
|
else:
|
||||||
@@ -312,45 +347,27 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
if action == "continue":
|
if action == "continue":
|
||||||
continue # Loop back for next question
|
continue # Loop back for next question
|
||||||
|
|
||||||
if action == "cancel":
|
if action in ("cancel", "web_cancel"):
|
||||||
os.write(child_fd, b'\x15\r')
|
if action == "web_cancel":
|
||||||
|
os.write(child_fd, b'\x05')
|
||||||
|
else:
|
||||||
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
os.write(child_fd, b'\x15\r')
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def on_inject(cmd):
|
||||||
|
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=cmd))
|
||||||
|
|
||||||
if action == "send_all":
|
if action == "send_all":
|
||||||
commands = result.get("commands", [])
|
commands = result.get("commands", [])
|
||||||
os.write(child_fd, b'\x15') # Ctrl+U to clear line
|
await n.inject_commands(commands, child_fd, on_inject=on_inject)
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# Prepend screen length command to avoid pagination
|
|
||||||
if "screen_length_command" in n.tags:
|
|
||||||
os.write(child_fd, (n.tags["screen_length_command"] + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=n.tags["screen_length_command"]))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
os.write(child_fd, (cmd + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=cmd))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
return
|
return
|
||||||
elif action.startswith("custom:"):
|
elif action.startswith("custom:"):
|
||||||
custom_cmds = action[7:]
|
custom_cmds_raw = action[7:]
|
||||||
os.write(child_fd, b'\x15')
|
custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split('\n') if cmd.strip()]
|
||||||
await asyncio.sleep(0.1)
|
await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject)
|
||||||
|
|
||||||
# Prepend screen length command to avoid pagination
|
|
||||||
if "screen_length_command" in n.tags:
|
|
||||||
os.write(child_fd, (n.tags["screen_length_command"] + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=n.tags["screen_length_command"]))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
|
|
||||||
for cmd in custom_cmds.split('\n'):
|
|
||||||
if cmd.strip():
|
|
||||||
os.write(child_fd, (cmd.strip() + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=cmd.strip()))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
os.write(child_fd, b'\x15\r')
|
os.write(child_fd, b'\x15\r')
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class NodeStub:
|
|||||||
self.remote_host = remote_host
|
self.remote_host = remote_host
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, cmd_byte_positions, pause_generator, resume_generator, old_tty):
|
def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, pause_generator, resume_generator, old_tty):
|
||||||
import json, asyncio, termios, sys, tty, queue
|
import json, asyncio, termios, sys, tty, queue
|
||||||
from ..core import copilot_terminal_mode
|
from ..core import copilot_terminal_mode
|
||||||
from . import connpy_pb2
|
from . import connpy_pb2
|
||||||
@@ -51,6 +51,10 @@ class NodeStub:
|
|||||||
pause_generator()
|
pause_generator()
|
||||||
|
|
||||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
|
||||||
|
node_info = json.loads(res.copilot_node_info_json) if res.copilot_node_info_json else {}
|
||||||
|
blocks = node_info.get("context_blocks", [])
|
||||||
|
|
||||||
interface = CopilotInterface(
|
interface = CopilotInterface(
|
||||||
self.config,
|
self.config,
|
||||||
history=getattr(self, 'copilot_history', None),
|
history=getattr(self, 'copilot_history', None),
|
||||||
@@ -59,8 +63,6 @@ class NodeStub:
|
|||||||
self.copilot_history = interface.history
|
self.copilot_history = interface.history
|
||||||
self.copilot_state = interface.session_state
|
self.copilot_state = interface.session_state
|
||||||
|
|
||||||
node_info = json.loads(res.copilot_node_info_json) if res.copilot_node_info_json else {}
|
|
||||||
|
|
||||||
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
|
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
|
||||||
# Send request to server
|
# Send request to server
|
||||||
request_queue.put(connpy_pb2.InteractRequest(
|
request_queue.put(connpy_pb2.InteractRequest(
|
||||||
@@ -85,9 +87,9 @@ class NodeStub:
|
|||||||
while True:
|
while True:
|
||||||
action, commands, custom_cmd = await interface.run_session(
|
action, commands, custom_cmd = await interface.run_session(
|
||||||
raw_bytes=bytes(client_buffer_bytes),
|
raw_bytes=bytes(client_buffer_bytes),
|
||||||
cmd_byte_positions=cmd_byte_positions,
|
|
||||||
node_info=node_info,
|
node_info=node_info,
|
||||||
on_ai_call=on_ai_call_remote
|
on_ai_call=on_ai_call_remote,
|
||||||
|
blocks=blocks
|
||||||
)
|
)
|
||||||
|
|
||||||
if action == "continue":
|
if action == "continue":
|
||||||
@@ -124,7 +126,6 @@ class NodeStub:
|
|||||||
|
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
client_buffer_bytes = bytearray()
|
client_buffer_bytes = bytearray()
|
||||||
cmd_byte_positions = [(0, None)]
|
|
||||||
pause_stdin = [False]
|
pause_stdin = [False]
|
||||||
wake_r, wake_w = os.pipe()
|
wake_r, wake_w = os.pipe()
|
||||||
|
|
||||||
@@ -171,8 +172,6 @@ class NodeStub:
|
|||||||
data = os.read(sys.stdin.fileno(), 1024)
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
if b'\r' in data or b'\n' in data:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), None))
|
|
||||||
yield connpy_pb2.InteractRequest(stdin_data=data)
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
@@ -246,14 +245,11 @@ class NodeStub:
|
|||||||
if res.copilot_prompt:
|
if res.copilot_prompt:
|
||||||
self._handle_remote_copilot(
|
self._handle_remote_copilot(
|
||||||
res, request_queue, response_queue,
|
res, request_queue, response_queue,
|
||||||
client_buffer_bytes, cmd_byte_positions,
|
client_buffer_bytes,
|
||||||
pause_generator, resume_generator, old_tty
|
pause_generator, resume_generator, old_tty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if res.copilot_injected_command:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), res.copilot_injected_command))
|
|
||||||
|
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
client_buffer_bytes.extend(res.stdout_data)
|
client_buffer_bytes.extend(res.stdout_data)
|
||||||
@@ -275,7 +271,6 @@ class NodeStub:
|
|||||||
params_json = json.dumps(connection_params)
|
params_json = json.dumps(connection_params)
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
client_buffer_bytes = bytearray()
|
client_buffer_bytes = bytearray()
|
||||||
cmd_byte_positions = [(0, None)]
|
|
||||||
pause_stdin = [False]
|
pause_stdin = [False]
|
||||||
wake_r, wake_w = os.pipe()
|
wake_r, wake_w = os.pipe()
|
||||||
|
|
||||||
@@ -323,8 +318,6 @@ class NodeStub:
|
|||||||
data = os.read(sys.stdin.fileno(), 1024)
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
if b'\r' in data or b'\n' in data:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), None))
|
|
||||||
yield connpy_pb2.InteractRequest(stdin_data=data)
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
@@ -397,14 +390,11 @@ class NodeStub:
|
|||||||
if res.copilot_prompt:
|
if res.copilot_prompt:
|
||||||
self._handle_remote_copilot(
|
self._handle_remote_copilot(
|
||||||
res, request_queue, response_queue,
|
res, request_queue, response_queue,
|
||||||
client_buffer_bytes, cmd_byte_positions,
|
client_buffer_bytes,
|
||||||
pause_generator, resume_generator, old_tty
|
pause_generator, resume_generator, old_tty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if res.copilot_injected_command:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), res.copilot_injected_command))
|
|
||||||
|
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
client_buffer_bytes.extend(res.stdout_data)
|
client_buffer_bytes.extend(res.stdout_data)
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ from connpy.utils import log_cleaner
|
|||||||
class AIService(BaseService):
|
class AIService(BaseService):
|
||||||
"""Business logic for interacting with AI agents and LLM configurations."""
|
"""Business logic for interacting with AI agents and LLM configurations."""
|
||||||
|
|
||||||
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) -> list:
|
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = "") -> list:
|
||||||
"""Identifies command blocks in the terminal history."""
|
"""Identifies command blocks in the terminal history."""
|
||||||
blocks = []
|
blocks = []
|
||||||
if not (cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes):
|
if not raw_bytes:
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
||||||
@@ -20,29 +20,43 @@ class AIService(BaseService):
|
|||||||
except Exception:
|
except Exception:
|
||||||
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
||||||
|
|
||||||
for i in range(1, len(cmd_byte_positions)):
|
if cmd_byte_positions and len(cmd_byte_positions) >= 1:
|
||||||
pos, known_cmd = cmd_byte_positions[i]
|
for i in range(1, len(cmd_byte_positions)):
|
||||||
prev_pos = cmd_byte_positions[i-1][0]
|
pos, known_cmd = cmd_byte_positions[i]
|
||||||
|
prev_pos = cmd_byte_positions[i-1][0]
|
||||||
|
|
||||||
if known_cmd:
|
if known_cmd:
|
||||||
prev_chunk = raw_bytes[prev_pos:pos]
|
prev_chunk = raw_bytes[prev_pos:pos]
|
||||||
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
||||||
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
|
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
|
||||||
prompt_text = prev_lines[-1].strip() if prev_lines else ""
|
prompt_text = prev_lines[-1].strip() if prev_lines else ""
|
||||||
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
|
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
|
||||||
blocks.append((pos, preview[:80]))
|
blocks.append((pos, preview[:80]))
|
||||||
else:
|
else:
|
||||||
chunk = raw_bytes[prev_pos:pos]
|
chunk = raw_bytes[prev_pos:pos]
|
||||||
cleaned = log_cleaner(chunk.decode(errors='replace'))
|
cleaned = log_cleaner(chunk.decode(errors='replace'))
|
||||||
lines = [l for l in cleaned.split('\n') if l.strip()]
|
lines = [l for l in cleaned.split('\n') if l.strip()]
|
||||||
preview = lines[-1].strip() if lines else ""
|
preview = lines[-1].strip() if lines else ""
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
match = prompt_re.search(preview)
|
||||||
|
if match:
|
||||||
|
cmd_text = preview[match.end():].strip()
|
||||||
|
if cmd_text:
|
||||||
|
blocks.append((pos, preview[:80]))
|
||||||
|
|
||||||
|
# Always ensure there is a final block representing the current prompt
|
||||||
|
# Find the start of the last line in the raw buffer to avoid selecting everything
|
||||||
|
# when no commands have been executed yet.
|
||||||
|
last_newline = raw_bytes.rfind(b'\n')
|
||||||
|
current_prompt_pos = last_newline + 1 if last_newline != -1 else 0
|
||||||
|
|
||||||
|
if not blocks:
|
||||||
|
blocks.append((current_prompt_pos, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
elif blocks[-1][0] < current_prompt_pos:
|
||||||
|
# If the last command block ends before the current prompt, add the prompt block
|
||||||
|
blocks.append((current_prompt_pos, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
|
||||||
if preview:
|
|
||||||
match = prompt_re.search(preview)
|
|
||||||
if match:
|
|
||||||
cmd_text = preview[match.end():].strip()
|
|
||||||
if cmd_text:
|
|
||||||
blocks.append((pos, preview[:80]))
|
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
def process_copilot_input(self, input_text: str, session_state: dict) -> dict:
|
def process_copilot_input(self, input_text: str, session_state: dict) -> dict:
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ cd connpy
|
|||||||
docker compose build
|
docker compose build
|
||||||
|
|
||||||
# Run it like a native app (completely silent)
|
# Run it like a native app (completely silent)
|
||||||
docker compose --log-level ERROR run --rm --remove-orphans connpy-app [command]
|
docker compose run --rm --remove-orphans connpy-app [command]
|
||||||
|
|
||||||
# Pro Tip: Add this alias for a 100% native experience from any folder
|
# Pro Tip: Add this alias for a 100% native experience from any folder
|
||||||
alias conn='docker compose -f /path/to/connpy/docker-compose.yml --log-level ERROR run --rm --remove-orphans connpy-app'
|
alias conn='docker compose -f /path/to/connpy/docker-compose.yml run --rm --remove-orphans connpy-app'
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<hr>
|
<hr>
|
||||||
<h2 id="privacy-integration">🔒 Privacy & Integration</h2>
|
<h2 id="privacy-integration">🔒 Privacy & Integration</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user