Compare commits
2
Commits
3637d34f6f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01690c815f | ||
|
|
881d65b8a5 |
@@ -3,7 +3,7 @@
|
||||
</p>
|
||||
|
||||
|
||||
# Connpy (v6.2.0)
|
||||
# Connpy (v6.3.0)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
@@ -25,7 +25,9 @@ The v6 release introduces a comprehensive **AI Copilot** and **AI Playbook Engin
|
||||
### 1a. Terminal Copilot (Ctrl+Space)
|
||||
Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**.
|
||||
* **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**.
|
||||
* **Slash Commands (`/`)**: Control the AI persona and safety settings:
|
||||
* **Slash Commands (`/`)**: Control the AI persona, safety settings, and mission mode:
|
||||
* `/mission [objective]`: Start an autonomous multi-step investigation mission with human approval.
|
||||
* `/cancel`: Abort active mission.
|
||||
* `/architect` / `/engineer`: Swaps the agent between high-level strategist and technical executor.
|
||||
* `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands.
|
||||
* `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`).
|
||||
|
||||
+4
-2
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
|
||||
|
||||
# Connpy (v6.2.0)
|
||||
# Connpy (v6.3.0)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
@@ -27,7 +27,9 @@ The v6 release introduces a comprehensive **AI Copilot** and **AI Playbook Engin
|
||||
### 1a. Terminal Copilot (Ctrl+Space)
|
||||
Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**.
|
||||
* **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**.
|
||||
* **Slash Commands (`/`)**: Control the AI persona and safety settings:
|
||||
* **Slash Commands (`/`)**: Control the AI persona, safety settings, and mission mode:
|
||||
* `/mission [objective]`: Start an autonomous multi-step investigation mission with human approval.
|
||||
* `/cancel`: Abort active mission.
|
||||
* `/architect` / `/engineer`: Swaps the agent between high-level strategist and technical executor.
|
||||
* `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands.
|
||||
* `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`).
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "6.2.0"
|
||||
__version__ = "6.3.0"
|
||||
|
||||
+202
-90
@@ -104,7 +104,8 @@ class CopilotInterface:
|
||||
is_single = saved_mode in (self.mode_single, 1, 'SINGLE', 'single')
|
||||
|
||||
if is_range or is_single:
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1:
|
||||
is_mission = self.session_state.get('mission', {}).get('active', False)
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
@@ -137,15 +138,17 @@ class CopilotInterface:
|
||||
self.session_state['last_total_cmds'] = total_cmds
|
||||
self.session_state['last_total_lines'] = total_lines
|
||||
|
||||
# 1. Visual Separation
|
||||
# 1. Visual Separation (Only show help banner on initial entry)
|
||||
self.console.print("") # Real line break
|
||||
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
||||
self.console.print(Panel(
|
||||
"[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]",
|
||||
border_style="cyan"
|
||||
))
|
||||
self.console.print("\n") # Small space before the copilot prompt
|
||||
if not self.session_state.get('banner_shown', False):
|
||||
self.console.print(Panel(
|
||||
"[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]",
|
||||
border_style="cyan"
|
||||
))
|
||||
self.session_state['banner_shown'] = True
|
||||
self.console.print("") # Small space before the copilot prompt
|
||||
|
||||
bindings = KeyBindings()
|
||||
@bindings.add('c-up')
|
||||
@@ -227,7 +230,7 @@ class CopilotInterface:
|
||||
text = app.current_buffer.text
|
||||
# Only show command help if typing the first command and there are no spaces
|
||||
if text.startswith('/') and ' ' not in text:
|
||||
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear']
|
||||
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear', '/mission', '/cancel']
|
||||
matches = [c for c in commands if c.startswith(text.lower())]
|
||||
if matches:
|
||||
m_text = html.escape(f"Available: {' '.join(matches)}")
|
||||
@@ -235,7 +238,7 @@ class CopilotInterface:
|
||||
|
||||
m_label = {self.mode_range: "RANGE", self.mode_single: "SINGLE", self.mode_lines: "LINES"}[state['context_mode']]
|
||||
if state['context_mode'] == self.mode_lines:
|
||||
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
||||
base_str = f'\u25b6 [Tab: {m_label}] {state["context_lines"]}/{state["total_lines"]}L (Ctrl+\u2191/\u2193 adjusts lines)'
|
||||
else:
|
||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||
|
||||
@@ -258,7 +261,7 @@ class CopilotInterface:
|
||||
p = clean_preview(b[2])
|
||||
if p:
|
||||
# Truncar comandos individuales largos
|
||||
if len(p) > 25: p = p[:22] + "..."
|
||||
if len(p) > 25: p = p[:22].rstrip(' .,-_') + "..."
|
||||
previews.append(p)
|
||||
|
||||
if not previews:
|
||||
@@ -266,12 +269,12 @@ class CopilotInterface:
|
||||
elif len(previews) <= 3:
|
||||
desc = " + ".join(previews)
|
||||
else:
|
||||
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})"
|
||||
desc = f"{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})"
|
||||
else:
|
||||
# Modo SINGLE original
|
||||
desc = clean_preview(blocks[idx][2])
|
||||
|
||||
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
||||
base_str = f'\u25b6 [Tab: {m_label}] {desc}'
|
||||
|
||||
# Wrap base_str in a style to maintain consistency and avoid glitches
|
||||
# The fg color will be inherited from bottom-toolbar global style if not specified here
|
||||
@@ -305,7 +308,9 @@ class CopilotInterface:
|
||||
('/trust', 'Enable auto-execute'),
|
||||
('/untrust', 'Disable auto-execute'),
|
||||
('/memorize', 'Add fact to memory'),
|
||||
('/clear', 'Clear memory')
|
||||
('/clear', 'Clear memory'),
|
||||
('/mission', 'Start autonomous mission'),
|
||||
('/cancel', 'Cancel active mission')
|
||||
]
|
||||
for cmd, desc in commands:
|
||||
if cmd.startswith(cmd_part.lower()):
|
||||
@@ -313,68 +318,162 @@ class CopilotInterface:
|
||||
|
||||
copilot_completer = SlashCommandCompleter()
|
||||
|
||||
def _finalize_mission(reason="completed", final_guide=""):
|
||||
mission = self.session_state.get('mission', {})
|
||||
if not mission or not mission.get('active', False):
|
||||
return
|
||||
mission['active'] = False
|
||||
if reason == "completed":
|
||||
self.console.print("\n[bold green]🎉 Mission Completed[/bold green]")
|
||||
elif reason == "user_cancelled":
|
||||
self.console.print("\n[yellow]Mission cancelled by user.[/yellow]")
|
||||
elif reason == "limit_reached":
|
||||
self.console.print("\n[yellow]Mission step limit reached.[/yellow]")
|
||||
|
||||
final_notes = "\n".join(mission.get('scratchpad_notes', []))
|
||||
guide = final_guide or mission.get('last_guide', '')
|
||||
asst_msg = f"Notes: {final_notes}\nGuide: {guide}" if final_notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": f"/mission {mission.get('goal', '')}"})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
|
||||
while True:
|
||||
# 2. Ask question
|
||||
from prompt_toolkit.styles import Style
|
||||
c_contrast = self._get_theme_color("contrast", "gray")
|
||||
ui_style = Style.from_dict({
|
||||
'bottom-toolbar': f'fg:{c_contrast}',
|
||||
})
|
||||
overrides = {}
|
||||
# Check for active mission auto-looping
|
||||
mission = self.session_state.get('mission', {})
|
||||
is_mission = mission.get('active', False) and not mission.get('paused', False)
|
||||
|
||||
session = PromptSession(
|
||||
history=self.history,
|
||||
input=self.pt_input,
|
||||
output=self.pt_output,
|
||||
completer=copilot_completer,
|
||||
reserve_space_for_menu=0,
|
||||
style=ui_style
|
||||
)
|
||||
try:
|
||||
# We use an internal try/finally to ensure that if something fails in prompt_async,
|
||||
# we don't leave the terminal in a strange state.
|
||||
question = await session.prompt_async(
|
||||
get_prompt_text,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=get_toolbar,
|
||||
multiline=True
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
state['cancelled'] = True
|
||||
question = ""
|
||||
if is_mission:
|
||||
# Force mode_range for mission mode
|
||||
state['context_mode'] = self.mode_range
|
||||
self.session_state['context_mode'] = self.mode_range
|
||||
|
||||
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
|
||||
return "cancel", None, None
|
||||
if mission.get('start_block_idx') is None:
|
||||
mission['start_block_idx'] = state['total_cmds']
|
||||
|
||||
# 3. Process Input via AIService
|
||||
directive = self.ai_service.process_copilot_input(question, self.session_state)
|
||||
start_idx = mission.get('start_block_idx', state['total_cmds'])
|
||||
cmds_since_start = max(1, (state['total_cmds'] - start_idx) + 1)
|
||||
state['context_cmd'] = max(state.get('context_cmd', 1), cmds_since_start)
|
||||
self.session_state['context_cmd'] = state['context_cmd']
|
||||
|
||||
if directive["action"] == "state_update":
|
||||
msg = directive['message']
|
||||
state['toolbar_msg'] = msg
|
||||
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
|
||||
step = mission.get('step', 1)
|
||||
max_steps = mission.get('max_steps', 10)
|
||||
|
||||
async def delayed_refresh():
|
||||
await asyncio.sleep(3.1)
|
||||
# Only invalidate if the message hasn't been replaced by a newer one
|
||||
if state.get('toolbar_msg') == msg:
|
||||
state['toolbar_msg'] = '' # Explicitly clear
|
||||
try:
|
||||
from prompt_toolkit.application.current import get_app
|
||||
app = get_app()
|
||||
if app: app.invalidate()
|
||||
except: pass
|
||||
asyncio.create_task(delayed_refresh())
|
||||
if step > max_steps:
|
||||
ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
|
||||
c_warn = self._get_theme_color("warning", "yellow")
|
||||
import html
|
||||
p_warn = html.escape(f"[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: ")
|
||||
try:
|
||||
ext_ans = await ext_session.prompt_async(HTML(f'<style fg="{c_warn}" bold="true">{p_warn}</style>'))
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
ext_ans = 'n'
|
||||
|
||||
# Move the cursor up and clean the line so the new prompt replaces the previous one
|
||||
sys.stdout.write('\x1b[1A\x1b[2K')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
if (ext_ans or 'y').lower().strip() in ('y', 'yes'):
|
||||
mission['max_steps'] += 10
|
||||
else:
|
||||
goal = mission.get('goal', '')
|
||||
question = f"[MISSION SUMMARY]: Step limit reached ({max_steps} steps). Provide a concise summary of all findings and current status for: {goal}"
|
||||
clean_question = question
|
||||
mission['active'] = False
|
||||
is_mission = False
|
||||
self.console.print(f"\n[bold cyan]🤖 Generating Final Mission Summary ({max_steps} steps reached)...[/bold cyan]")
|
||||
|
||||
if is_mission:
|
||||
goal = mission.get('goal', '')
|
||||
step = mission.get('step', 1)
|
||||
question = f"[MISSION STEP {step}]: Continue analysis of: {goal}"
|
||||
|
||||
scratchpad = mission.get('scratchpad_notes', [])
|
||||
if scratchpad:
|
||||
notes_text = "\n".join(f"- {n}" for n in scratchpad)
|
||||
question += f"\n\nPast Mission Notes:\n{notes_text}"
|
||||
|
||||
clean_question = question
|
||||
self.console.print(f"\n[bold cyan]🤖 Executing Mission Step {step}: {goal}[/bold cyan]")
|
||||
elif not is_mission and 'clean_question' in locals() and clean_question.startswith("[MISSION SUMMARY]"):
|
||||
# Pass-through to AI execution for generating final summary
|
||||
pass
|
||||
else:
|
||||
# Clean the toolbar message when a real question is asked
|
||||
state['toolbar_msg'] = ''
|
||||
# 2. Ask question
|
||||
from prompt_toolkit.styles import Style
|
||||
c_contrast = self._get_theme_color("contrast", "gray")
|
||||
ui_style = Style.from_dict({
|
||||
'bottom-toolbar': f'fg:{c_contrast}',
|
||||
})
|
||||
|
||||
clean_question = directive.get("clean_prompt", question)
|
||||
overrides = directive.get("overrides", {})
|
||||
session = PromptSession(
|
||||
history=self.history,
|
||||
input=self.pt_input,
|
||||
output=self.pt_output,
|
||||
completer=copilot_completer,
|
||||
reserve_space_for_menu=0,
|
||||
style=ui_style
|
||||
)
|
||||
try:
|
||||
question = await session.prompt_async(
|
||||
get_prompt_text,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=get_toolbar,
|
||||
multiline=True
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
state['cancelled'] = True
|
||||
question = ""
|
||||
|
||||
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# 3. Process Input via AIService
|
||||
directive = self.ai_service.process_copilot_input(question, self.session_state)
|
||||
|
||||
if directive["action"] == "mission_start":
|
||||
mission = self.session_state.get('mission', {})
|
||||
mission['start_block_idx'] = state['total_cmds']
|
||||
state['context_mode'] = self.mode_range
|
||||
self.session_state['context_mode'] = self.mode_range
|
||||
state['context_cmd'] = 1
|
||||
self.session_state['context_cmd'] = 1
|
||||
clean_question = f"[MISSION STEP 1]: {directive['clean_prompt']}"
|
||||
overrides = directive.get("overrides", {})
|
||||
self.console.print(f"\n[bold cyan]🤖 Starting Mission: {directive['clean_prompt']}[/bold cyan]")
|
||||
elif directive["action"] == "mission_cancel":
|
||||
_finalize_mission("user_cancelled")
|
||||
state['toolbar_msg'] = 'Mission cancelled'
|
||||
continue
|
||||
elif directive["action"] == "state_update":
|
||||
msg = directive['message']
|
||||
state['toolbar_msg'] = msg
|
||||
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
|
||||
|
||||
async def delayed_refresh():
|
||||
await asyncio.sleep(3.1)
|
||||
if state.get('toolbar_msg') == msg:
|
||||
state['toolbar_msg'] = ''
|
||||
try:
|
||||
from prompt_toolkit.application.current import get_app
|
||||
app = get_app()
|
||||
if app: app.invalidate()
|
||||
except: pass
|
||||
asyncio.create_task(delayed_refresh())
|
||||
|
||||
sys.stdout.write('\x1b[1A\x1b[2K')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
else:
|
||||
state['toolbar_msg'] = ''
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = False
|
||||
step = mission.get('step', 1)
|
||||
goal = mission.get('goal', '')
|
||||
clean_question = f"[MISSION FEEDBACK (Step {step})]: User provided guidance: {question}. Continue the mission for goal: {goal}"
|
||||
self.console.print(f"\n[bold cyan]🤖 Continuing Mission with User Feedback: {question}[/bold cyan]")
|
||||
overrides = directive.get("overrides", {})
|
||||
else:
|
||||
clean_question = directive.get("clean_prompt", question)
|
||||
overrides = directive.get("overrides", {})
|
||||
|
||||
# Merge node_info with session_state and overrides
|
||||
merged_node_info = node_info.copy()
|
||||
@@ -389,7 +488,6 @@ class CopilotInterface:
|
||||
merged_node_info[k] = v
|
||||
|
||||
# 3. AI Execution
|
||||
# Use persona from overrides (one-shot) or from session state
|
||||
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
||||
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
||||
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
|
||||
@@ -416,7 +514,6 @@ class CopilotInterface:
|
||||
nonlocal live_text, first_chunk
|
||||
if first_chunk:
|
||||
status_spinner.stop()
|
||||
# Print header rule before first chunk arrives
|
||||
self.console.print(Rule(
|
||||
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
|
||||
style=persona_color
|
||||
@@ -425,7 +522,6 @@ class CopilotInterface:
|
||||
live_text += text
|
||||
md_parser.feed(text)
|
||||
|
||||
# Check for interruption during AI call
|
||||
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
|
||||
|
||||
try:
|
||||
@@ -434,13 +530,12 @@ class CopilotInterface:
|
||||
result = await ai_task
|
||||
except asyncio.CancelledError:
|
||||
status_spinner.stop()
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# Ensure spinner is stopped if no chunks arrived
|
||||
if first_chunk:
|
||||
status_spinner.stop()
|
||||
|
||||
# Close the streamed output with a Rule
|
||||
if not first_chunk:
|
||||
md_parser.flush()
|
||||
self.console.print(Rule(style=persona_color))
|
||||
@@ -448,26 +543,35 @@ class CopilotInterface:
|
||||
if not result or result.get("error"):
|
||||
if first_chunk and result and result.get("error"):
|
||||
self.console.print(f"[red]Error: {result['error']}[/red]")
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# If no chunks were streamed but we have a guide, print it as a panel
|
||||
if first_chunk and result and result.get("guide"):
|
||||
from rich.markdown import Markdown
|
||||
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
|
||||
|
||||
# Update copilot_chat_history with clean Q&A turn
|
||||
# Update copilot_chat_history or mission scratchpad
|
||||
if result and not result.get("error"):
|
||||
guide = result.get("guide", "")
|
||||
notes = result.get("notes", "")
|
||||
if guide:
|
||||
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": clean_question})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
|
||||
mission = self.session_state.get('mission', {})
|
||||
if mission.get('active', False):
|
||||
if notes:
|
||||
mission.setdefault('scratchpad_notes', []).append(notes)
|
||||
if guide:
|
||||
mission['last_guide'] = guide
|
||||
else:
|
||||
if guide or notes:
|
||||
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": clean_question})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
commands = result.get("commands", [])
|
||||
mission = self.session_state.get('mission', {})
|
||||
if not commands:
|
||||
if mission.get('active', False):
|
||||
_finalize_mission("completed", result.get("guide", ""))
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
@@ -476,11 +580,12 @@ class CopilotInterface:
|
||||
style_color = self._get_theme_color(risk_style, fallback="green")
|
||||
|
||||
cmd_text = "\n".join(f" {i+1}. {c}" for i, c in enumerate(commands))
|
||||
# Explicitly use 'bold style_color' for both TITLE and BORDER to ensure maximum consistency
|
||||
self.console.print(Panel(cmd_text, title=f"[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]", border_style=f"bold {style_color}"))
|
||||
|
||||
if merged_node_info.get('trust', False) and risk != "destructive":
|
||||
self.console.print(f"[dim]⚙️ Auto-executing (Trust Mode)[/dim]")
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", commands, None
|
||||
|
||||
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
|
||||
@@ -492,41 +597,39 @@ class CopilotInterface:
|
||||
import html
|
||||
try:
|
||||
p_text = html.escape(f"Send? (y/n/e/range) [n]: ")
|
||||
# Use the EXACT same style_color and force bold="true" for Prompt-Toolkit
|
||||
action = await confirm_session.prompt_async(HTML(f'<style fg="{style_color}" bold="true">{p_text}</style>'), key_bindings=c_bindings)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
_finalize_mission("user_cancelled", result.get("guide", ""))
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
def parse_indices(text, max_len):
|
||||
"""Helper to parse '1-3, 5, 7' into [0, 1, 2, 4, 6]."""
|
||||
indices = []
|
||||
# Replace commas with spaces and split
|
||||
parts = text.replace(',', ' ').split()
|
||||
for part in parts:
|
||||
if '-' in part:
|
||||
try:
|
||||
start, end = map(int, part.split('-'))
|
||||
# Ensure inclusive and 0-indexed
|
||||
indices.extend(range(start-1, end))
|
||||
except: continue
|
||||
elif part.isdigit():
|
||||
indices.append(int(part)-1)
|
||||
# Filter valid indices and remove duplicates
|
||||
return [i for i in sorted(set(indices)) if 0 <= i < max_len]
|
||||
|
||||
action_l = (action or "n").lower().strip()
|
||||
if action_l in ('y', 'yes', 'all'):
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", commands, None
|
||||
|
||||
# Check for numeric selection (e.g., "1, 2-4")
|
||||
if re.match(r'^[0-9,\-\s]+$', action_l):
|
||||
selected_idxs = parse_indices(action_l, len(commands))
|
||||
if selected_idxs:
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", [commands[i] for i in selected_idxs], None
|
||||
|
||||
elif action_l.startswith('e'):
|
||||
# Check if it's a selective edit like 'e1-2'
|
||||
selection_str = action_l[1:].strip()
|
||||
if selection_str:
|
||||
idxs = parse_indices(selection_str, len(commands))
|
||||
@@ -552,14 +655,23 @@ class CopilotInterface:
|
||||
default=target, multiline=True, key_bindings=e_bindings
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = True
|
||||
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
if edited and edited.strip():
|
||||
# Split by lines to ensure core.py applies delay between each command
|
||||
lines = [l.strip() for l in edited.split('\n') if l.strip()]
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "custom", None, lines
|
||||
|
||||
# User rejected/cancelled the commands
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = True
|
||||
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
|
||||
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
|
||||
+28
-5
@@ -791,6 +791,7 @@ class node:
|
||||
# Save history back to stream for persistence in current session
|
||||
stream.copilot_history = interface.history
|
||||
stream.copilot_state = interface.session_state
|
||||
interface.session_state['banner_shown'] = False
|
||||
|
||||
ai_service = AIService(config)
|
||||
|
||||
@@ -821,6 +822,32 @@ class node:
|
||||
node_info=node_info,
|
||||
on_ai_call=on_ai_call
|
||||
)
|
||||
if action in ("send_all", "custom"):
|
||||
cmds_to_send = commands if action == "send_all" else custom_cmd
|
||||
await self.inject_commands(cmds_to_send, child_fd)
|
||||
|
||||
# Dynamic Wait for Device Prompt Settle
|
||||
prompt_pattern = node_info.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')
|
||||
start_t = time()
|
||||
last_len = 0
|
||||
quiet_count = 0
|
||||
while time() - start_t < 60:
|
||||
await asyncio.sleep(0.15)
|
||||
cur_bytes = self.mylog.getvalue()
|
||||
if len(cur_bytes) == last_len:
|
||||
quiet_count += 1
|
||||
else:
|
||||
quiet_count = 0
|
||||
last_len = len(cur_bytes)
|
||||
|
||||
clean_txt = self._logclean(cur_bytes.decode(errors='replace'), True).strip()
|
||||
lines = [l.strip() for l in clean_txt.split('\n') if l.strip()]
|
||||
last_line = lines[-1] if lines else ""
|
||||
if quiet_count >= 2 and re.search(prompt_pattern, last_line):
|
||||
break
|
||||
|
||||
raw_bytes = self.mylog.getvalue()
|
||||
continue
|
||||
if action == "continue":
|
||||
continue
|
||||
break
|
||||
@@ -832,11 +859,7 @@ class node:
|
||||
elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'):
|
||||
stream._loop.add_reader(stream.stdin_fd, stream._read_ready)
|
||||
|
||||
if action in ("send_all", "custom"):
|
||||
cmds_to_send = commands if action == "send_all" else custom_cmd
|
||||
await self.inject_commands(cmds_to_send, child_fd)
|
||||
else:
|
||||
os.write(child_fd, b'\x15\r')
|
||||
os.write(child_fd, b'\x15\r')
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"\n[ERROR in Copilot Handler] {e}", flush=True)
|
||||
|
||||
@@ -277,7 +277,8 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
||||
is_single = saved_mode in (1, 'SINGLE', 'single')
|
||||
|
||||
if is_range or is_single:
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1:
|
||||
is_mission = session_state.get('mission', {}).get('active', False) or (isinstance(node_info, dict) and node_info.get('mission', {}).get('active', False))
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
@@ -321,6 +322,14 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
||||
))
|
||||
|
||||
while True:
|
||||
# Refresh latest buffer from terminal log
|
||||
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
|
||||
cleaned_buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||
buffer = cleaned_buffer
|
||||
|
||||
# 0. Drain the queue of any stale messages before starting a new interaction
|
||||
while not remote_stream.copilot_queue.empty():
|
||||
try:
|
||||
@@ -454,16 +463,74 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
||||
if action == "send_all":
|
||||
commands = result.get("commands", [])
|
||||
await n.inject_commands(commands, child_fd, on_inject=on_inject)
|
||||
return
|
||||
elif action.startswith("custom:"):
|
||||
custom_cmds_raw = action[7:]
|
||||
custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split('\n') if cmd.strip()]
|
||||
await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject)
|
||||
return
|
||||
else:
|
||||
os.write(child_fd, b'\x15\r')
|
||||
return
|
||||
|
||||
# Dynamic Wait for Device Prompt Settle
|
||||
prompt_pattern = node_info.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')
|
||||
import time
|
||||
import re
|
||||
start_t = time.time()
|
||||
last_len = 0
|
||||
quiet_count = 0
|
||||
while time.time() - start_t < 60:
|
||||
await asyncio.sleep(0.15)
|
||||
cur_bytes = n.mylog.getvalue()
|
||||
if len(cur_bytes) == last_len:
|
||||
quiet_count += 1
|
||||
else:
|
||||
quiet_count = 0
|
||||
last_len = len(cur_bytes)
|
||||
|
||||
clean_txt = n._logclean(cur_bytes.decode(errors='replace'), True).strip()
|
||||
lines = [l.strip() for l in clean_txt.split('\n') if l.strip()]
|
||||
last_line = lines[-1] if lines else ""
|
||||
if quiet_count >= 2 and re.search(prompt_pattern, last_line):
|
||||
break
|
||||
|
||||
# Recalculate context blocks with updated buffer
|
||||
raw_bytes = n.mylog.getvalue()
|
||||
from connpy.utils import log_cleaner
|
||||
cleaned_buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||
last_line = cleaned_buffer.split('\n')[-1].strip() if cleaned_buffer.strip() else "(prompt)"
|
||||
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
|
||||
node_info["context_blocks"] = blocks
|
||||
|
||||
total_cmds = len(blocks)
|
||||
total_lines = len(cleaned_buffer.split('\n'))
|
||||
|
||||
last_total_cmds = session_state.get('last_total_cmds', None)
|
||||
saved_cmd = session_state.get('context_cmd', 1)
|
||||
is_mission = session_state.get('mission', {}).get('active', False) or (isinstance(node_info, dict) and node_info.get('mission', {}).get('active', False))
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
initial_cmd = saved_cmd
|
||||
|
||||
session_state['context_cmd'] = max(1, initial_cmd)
|
||||
session_state['last_total_cmds'] = total_cmds
|
||||
session_state['last_total_lines'] = total_lines
|
||||
|
||||
node_info['context_cmd'] = min(session_state['context_cmd'], max(1, total_cmds))
|
||||
node_info['context_lines'] = min(session_state.get('context_lines', 50), max(1, total_lines))
|
||||
node_info_json = json.dumps(node_info)
|
||||
|
||||
preview_str = raw_bytes[-200:].decode(errors='replace') if isinstance(raw_bytes, bytes) else str(raw_bytes)[-200:]
|
||||
|
||||
# Signal prompt settle and send updated blocks to client
|
||||
response_queue.put(connpy_pb2.InteractResponse(
|
||||
copilot_prompt=True,
|
||||
copilot_buffer_preview=preview_str,
|
||||
copilot_node_info_json=node_info_json
|
||||
))
|
||||
continue
|
||||
|
||||
asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler))
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
@@ -44,7 +44,7 @@ class NodeStub:
|
||||
self.config = config
|
||||
|
||||
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, os
|
||||
from ..core import copilot_terminal_mode
|
||||
from . import connpy_pb2
|
||||
|
||||
@@ -62,6 +62,7 @@ class NodeStub:
|
||||
)
|
||||
self.copilot_history = interface.history
|
||||
self.copilot_state = interface.session_state
|
||||
interface.session_state['banner_shown'] = False
|
||||
|
||||
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
|
||||
# Send request to server
|
||||
@@ -84,6 +85,7 @@ class NodeStub:
|
||||
|
||||
# Wrap in async loop
|
||||
async def run_remote_copilot():
|
||||
nonlocal blocks, node_info
|
||||
while True:
|
||||
action, commands, custom_cmd = await interface.run_session(
|
||||
raw_bytes=bytes(client_buffer_bytes),
|
||||
@@ -97,6 +99,32 @@ class NodeStub:
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action="continue"))
|
||||
continue
|
||||
|
||||
if action in ("send_all", "custom"):
|
||||
action_sent = "cancel"
|
||||
if action == "send_all" and commands:
|
||||
action_sent = f"custom:{chr(10).join(commands)}"
|
||||
elif action == "custom" and custom_cmd:
|
||||
action_sent = f"custom:{chr(10).join(custom_cmd)}"
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
|
||||
|
||||
print("\r\n\033[2m [Remote] Ejecutando comandos y esperando al prompt...\033[0m\r\n", flush=True)
|
||||
while True:
|
||||
try:
|
||||
prompt_res = response_queue.get(timeout=0.1)
|
||||
if prompt_res is None:
|
||||
return "cancel", None, None
|
||||
if prompt_res.stdout_data:
|
||||
os.write(sys.stdout.fileno(), prompt_res.stdout_data)
|
||||
client_buffer_bytes.extend(prompt_res.stdout_data)
|
||||
if prompt_res.copilot_prompt:
|
||||
if prompt_res.copilot_node_info_json:
|
||||
node_info = json.loads(prompt_res.copilot_node_info_json)
|
||||
blocks = node_info.get("context_blocks", [])
|
||||
break
|
||||
except queue.Empty:
|
||||
await asyncio.sleep(0.05)
|
||||
continue
|
||||
|
||||
return action, commands, custom_cmd
|
||||
|
||||
with copilot_terminal_mode():
|
||||
@@ -104,14 +132,7 @@ class NodeStub:
|
||||
|
||||
print("\033[2m Returning to session...\033[0m", flush=True)
|
||||
# Prepare final action for server
|
||||
action_sent = "cancel"
|
||||
if action == "send_all" and commands:
|
||||
# In remote mode, send the selected commands as a custom block
|
||||
# so the server executes exactly what the user picked (e.g., selection '1')
|
||||
action_sent = f"custom:{chr(10).join(commands)}"
|
||||
elif action == "custom" and custom_cmd:
|
||||
action_sent = f"custom:{chr(10).join(custom_cmd)}"
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action="cancel"))
|
||||
resume_generator()
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
||||
|
||||
@@ -207,6 +207,26 @@ class AIService(BaseService):
|
||||
else:
|
||||
return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}}
|
||||
|
||||
elif cmd == "/mission":
|
||||
if args:
|
||||
session_state['mission'] = {
|
||||
'active': True,
|
||||
'goal': args,
|
||||
'step': 1,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': None,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
return {"action": "mission_start", "clean_prompt": args, "overrides": {}}
|
||||
else:
|
||||
return {"action": "state_update", "message": "Usage: /mission <objective_description>"}
|
||||
|
||||
elif cmd in ("/cancel", "/abort"):
|
||||
if session_state.get('mission', {}).get('active'):
|
||||
session_state['mission']['active'] = False
|
||||
return {"action": "mission_cancel", "message": "Mission cancelled"}
|
||||
return {"action": "state_update", "message": "No active mission to cancel"}
|
||||
|
||||
# Unknown command, execute normally
|
||||
return {"action": "execute", "clean_prompt": text, "overrides": {}}
|
||||
|
||||
|
||||
@@ -586,5 +586,125 @@ def test_aask_copilot_notes_parsing(mock_acompletion):
|
||||
assert msgs[3]["content"] == "Why is ping failing?"
|
||||
|
||||
|
||||
def test_process_copilot_input_mission_command():
|
||||
from connpy.services.ai_service import AIService
|
||||
|
||||
class MockConfig:
|
||||
def __init__(self):
|
||||
self.config = {"ai": {}}
|
||||
self.defaultdir = "/tmp"
|
||||
|
||||
svc = AIService(MockConfig())
|
||||
session_state = {}
|
||||
|
||||
# Test /mission command
|
||||
res = svc.process_copilot_input("/mission check bgp neighbors", session_state)
|
||||
assert res["action"] == "mission_start"
|
||||
assert res["clean_prompt"] == "check bgp neighbors"
|
||||
assert session_state["mission"]["active"] is True
|
||||
assert session_state["mission"]["goal"] == "check bgp neighbors"
|
||||
assert session_state["mission"]["step"] == 1
|
||||
|
||||
# Test /cancel command
|
||||
res_cancel = svc.process_copilot_input("/cancel", session_state)
|
||||
assert res_cancel["action"] == "mission_cancel"
|
||||
assert session_state["mission"]["active"] is False
|
||||
|
||||
|
||||
def test_copilot_mission_mode_lifecycle():
|
||||
from connpy.cli.terminal_ui import CopilotInterface
|
||||
|
||||
class MockConfig:
|
||||
def __init__(self):
|
||||
self.config = {"ai": {}}
|
||||
self.defaultdir = "/tmp"
|
||||
|
||||
raw_bytes = b"router# show ip bgp\r\nrouter# "
|
||||
blocks = [
|
||||
(0, 20, "router# show ip bgp"),
|
||||
(20, 30, "router#")
|
||||
]
|
||||
|
||||
async def mock_ai_call(active_buffer, question, on_chunk, node_info):
|
||||
return {"guide": "BGP is UP", "notes": "Neighbor 10.0.0.1 established", "commands": [], "risk_level": "low"}
|
||||
|
||||
session_state = {
|
||||
'persona': 'engineer',
|
||||
'trust_mode': False,
|
||||
'memories': [],
|
||||
'copilot_chat_history': [],
|
||||
'os': None,
|
||||
'prompt': None,
|
||||
'mission': {
|
||||
'active': True,
|
||||
'goal': 'verify bgp status',
|
||||
'step': 1,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': 0,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
}
|
||||
|
||||
interface = CopilotInterface(MockConfig(), session_state=session_state)
|
||||
|
||||
asyncio.run(interface.run_session(raw_bytes=raw_bytes, node_info={"name": "test"}, on_ai_call=mock_ai_call, blocks=blocks))
|
||||
|
||||
# Mission completed (no commands), active flag should be set to False
|
||||
assert interface.session_state['mission']['active'] is False
|
||||
# Check that a single consolidated turn was added to copilot_chat_history
|
||||
hist = interface.session_state.get('copilot_chat_history', [])
|
||||
assert len(hist) == 2
|
||||
assert hist[0]['role'] == 'user'
|
||||
assert hist[0]['content'] == '/mission verify bgp status'
|
||||
assert hist[1]['role'] == 'assistant'
|
||||
assert 'Notes: Neighbor 10.0.0.1 established' in hist[1]['content']
|
||||
assert 'Guide: BGP is UP' in hist[1]['content']
|
||||
|
||||
|
||||
def test_copilot_mission_pause_feedback_and_cancel():
|
||||
from connpy.cli.terminal_ui import CopilotInterface
|
||||
from unittest.mock import patch
|
||||
|
||||
class MockConfig:
|
||||
def __init__(self):
|
||||
self.config = {"ai": {}}
|
||||
self.defaultdir = "/tmp"
|
||||
|
||||
raw_bytes = b"router# show ip bgp\r\nrouter# "
|
||||
blocks = [
|
||||
(0, 20, "router# show ip bgp"),
|
||||
(20, 30, "router#")
|
||||
]
|
||||
|
||||
# AI returns commands to trigger rejection
|
||||
async def mock_ai_call(active_buffer, question, on_chunk, node_info):
|
||||
return {"guide": "Checking routes", "notes": "Need more info", "commands": ["clear ip bgp *"], "risk_level": "high"}
|
||||
|
||||
session_state = {
|
||||
'persona': 'engineer',
|
||||
'trust_mode': False,
|
||||
'memories': [],
|
||||
'copilot_chat_history': [],
|
||||
'os': None,
|
||||
'prompt': None,
|
||||
'mission': {
|
||||
'active': True,
|
||||
'goal': 'troubleshoot routing',
|
||||
'step': 2,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': 0,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
}
|
||||
|
||||
interface = CopilotInterface(MockConfig(), session_state=session_state)
|
||||
|
||||
# Simulate user rejecting the command ('n')
|
||||
with patch('prompt_toolkit.PromptSession.prompt_async', return_value='n'):
|
||||
action, commands, custom_cmd = asyncio.run(interface.run_session(
|
||||
raw_bytes=raw_bytes, node_info={"name": "test"}, on_ai_call=mock_ai_call, blocks=blocks
|
||||
))
|
||||
|
||||
# Mission should now be paused (active=True, paused=True)
|
||||
assert interface.session_state['mission']['active'] is True
|
||||
assert interface.session_state['mission']['paused'] is True
|
||||
|
||||
@@ -211,3 +211,80 @@ class TestGRPCIntegration:
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
@patch("sys.stdin.fileno", return_value=0)
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("tty.setraw")
|
||||
@patch("os.write")
|
||||
def test_handle_remote_copilot_command_execution_loop(self, mock_os_write, mock_setraw, mock_getattr, mock_setattr, mock_fileno):
|
||||
import queue
|
||||
from connpy.grpc_layer.stubs import NodeStub
|
||||
from connpy.cli.terminal_ui import CopilotInterface
|
||||
|
||||
mock_getattr.return_value = [0, 0, 0, 0, 0, 0, [0] * 32]
|
||||
mock_channel = MagicMock()
|
||||
stub = NodeStub(mock_channel, "localhost:8048")
|
||||
|
||||
initial_node_info = {
|
||||
"name": "r1",
|
||||
"context_blocks": [[0, 10, "router#"]]
|
||||
}
|
||||
res = connpy_pb2.InteractResponse(
|
||||
copilot_prompt=True,
|
||||
copilot_node_info_json=json.dumps(initial_node_info)
|
||||
)
|
||||
|
||||
request_queue = queue.Queue()
|
||||
response_queue = queue.Queue()
|
||||
client_buffer_bytes = bytearray(b"router# ")
|
||||
pause_gen = MagicMock()
|
||||
resume_gen = MagicMock()
|
||||
old_tty = [0] * 7
|
||||
|
||||
call_count = 0
|
||||
received_blocks_per_call = []
|
||||
|
||||
async def mock_run_session(raw_bytes, node_info, on_ai_call, blocks):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
received_blocks_per_call.append(list(blocks))
|
||||
if call_count == 1:
|
||||
# Step 1: User approves commands
|
||||
# Simulate server putting stdout data and next copilot prompt
|
||||
updated_node_info = {
|
||||
"name": "r1",
|
||||
"context_blocks": [[0, 10, "router#"], [10, 30, "router# show ip route"]]
|
||||
}
|
||||
response_queue.put(connpy_pb2.InteractResponse(stdout_data=b"show ip route\r\n10.0.0.0/24\r\nrouter# "))
|
||||
response_queue.put(connpy_pb2.InteractResponse(
|
||||
copilot_prompt=True,
|
||||
copilot_node_info_json=json.dumps(updated_node_info)
|
||||
))
|
||||
return "send_all", ["show ip route"], None
|
||||
else:
|
||||
# Step 2: Second turn, mission completes or user cancels
|
||||
return "cancel", None, None
|
||||
|
||||
with patch.object(CopilotInterface, "run_session", side_effect=mock_run_session):
|
||||
stub._handle_remote_copilot(
|
||||
res, request_queue, response_queue, client_buffer_bytes,
|
||||
pause_gen, resume_gen, old_tty
|
||||
)
|
||||
|
||||
# Assert that run_session was called twice
|
||||
assert call_count == 2
|
||||
# First call should have initial blocks
|
||||
assert len(received_blocks_per_call[0]) == 1
|
||||
# Second call should have updated blocks received from server
|
||||
assert len(received_blocks_per_call[1]) == 2
|
||||
assert received_blocks_per_call[1][1][2] == "router# show ip route"
|
||||
# Assert client_buffer_bytes was updated with stdout_data
|
||||
assert b"10.0.0.0/24" in bytes(client_buffer_bytes)
|
||||
# Assert action sent to server
|
||||
reqs = []
|
||||
while not request_queue.empty():
|
||||
reqs.append(request_queue.get_nowait())
|
||||
assert reqs[0].copilot_action == "custom:show ip route"
|
||||
assert reqs[1].copilot_action == "cancel"
|
||||
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.ai API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -2101,11 +2101,11 @@ Node: {node_name}"""
|
||||
<dl>
|
||||
<dt id="connpy.ai.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.ai.ai.deferred_class_hooks"><code class="name">var <span class="ident">deferred_class_hooks</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>Instance variables</h3>
|
||||
@@ -3234,7 +3234,7 @@ def confirm(self, user_input): return True</code></pre>
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.ai_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -670,7 +670,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.api_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -193,7 +193,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.config_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -600,7 +600,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.context_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -249,7 +249,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.forms API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -700,7 +700,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.help_text API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -303,7 +303,7 @@ tasks:
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.helpers API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -319,7 +319,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.import_export_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -304,7 +304,7 @@ def forms(self):
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -162,7 +162,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.login_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -611,7 +611,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.node_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -675,7 +675,7 @@ def forms(self):
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.plugin_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -397,7 +397,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.profile_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -346,7 +346,7 @@ def forms(self):
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.run_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -1163,7 +1163,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.shell_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -181,7 +181,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.sso_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -457,7 +457,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.sync_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -427,7 +427,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+406
-182
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.terminal_ui API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -138,7 +138,8 @@ el.replaceWith(d);
|
||||
is_single = saved_mode in (self.mode_single, 1, 'SINGLE', 'single')
|
||||
|
||||
if is_range or is_single:
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1:
|
||||
is_mission = self.session_state.get('mission', {}).get('active', False)
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
@@ -171,15 +172,17 @@ el.replaceWith(d);
|
||||
self.session_state['last_total_cmds'] = total_cmds
|
||||
self.session_state['last_total_lines'] = total_lines
|
||||
|
||||
# 1. Visual Separation
|
||||
# 1. Visual Separation (Only show help banner on initial entry)
|
||||
self.console.print("") # Real line break
|
||||
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
||||
self.console.print(Panel(
|
||||
"[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]",
|
||||
border_style="cyan"
|
||||
))
|
||||
self.console.print("\n") # Small space before the copilot prompt
|
||||
if not self.session_state.get('banner_shown', False):
|
||||
self.console.print(Panel(
|
||||
"[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]",
|
||||
border_style="cyan"
|
||||
))
|
||||
self.session_state['banner_shown'] = True
|
||||
self.console.print("") # Small space before the copilot prompt
|
||||
|
||||
bindings = KeyBindings()
|
||||
@bindings.add('c-up')
|
||||
@@ -261,7 +264,7 @@ el.replaceWith(d);
|
||||
text = app.current_buffer.text
|
||||
# Only show command help if typing the first command and there are no spaces
|
||||
if text.startswith('/') and ' ' not in text:
|
||||
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear']
|
||||
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear', '/mission', '/cancel']
|
||||
matches = [c for c in commands if c.startswith(text.lower())]
|
||||
if matches:
|
||||
m_text = html.escape(f"Available: {' '.join(matches)}")
|
||||
@@ -269,7 +272,7 @@ el.replaceWith(d);
|
||||
|
||||
m_label = {self.mode_range: "RANGE", self.mode_single: "SINGLE", self.mode_lines: "LINES"}[state['context_mode']]
|
||||
if state['context_mode'] == self.mode_lines:
|
||||
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
||||
base_str = f'\u25b6 [Tab: {m_label}] {state["context_lines"]}/{state["total_lines"]}L (Ctrl+\u2191/\u2193 adjusts lines)'
|
||||
else:
|
||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||
|
||||
@@ -292,7 +295,7 @@ el.replaceWith(d);
|
||||
p = clean_preview(b[2])
|
||||
if p:
|
||||
# Truncar comandos individuales largos
|
||||
if len(p) > 25: p = p[:22] + "..."
|
||||
if len(p) > 25: p = p[:22].rstrip(' .,-_') + "..."
|
||||
previews.append(p)
|
||||
|
||||
if not previews:
|
||||
@@ -300,12 +303,12 @@ el.replaceWith(d);
|
||||
elif len(previews) <= 3:
|
||||
desc = " + ".join(previews)
|
||||
else:
|
||||
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})"
|
||||
desc = f"{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})"
|
||||
else:
|
||||
# Modo SINGLE original
|
||||
desc = clean_preview(blocks[idx][2])
|
||||
|
||||
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
||||
base_str = f'\u25b6 [Tab: {m_label}] {desc}'
|
||||
|
||||
# Wrap base_str in a style to maintain consistency and avoid glitches
|
||||
# The fg color will be inherited from bottom-toolbar global style if not specified here
|
||||
@@ -339,7 +342,9 @@ el.replaceWith(d);
|
||||
('/trust', 'Enable auto-execute'),
|
||||
('/untrust', 'Disable auto-execute'),
|
||||
('/memorize', 'Add fact to memory'),
|
||||
('/clear', 'Clear memory')
|
||||
('/clear', 'Clear memory'),
|
||||
('/mission', 'Start autonomous mission'),
|
||||
('/cancel', 'Cancel active mission')
|
||||
]
|
||||
for cmd, desc in commands:
|
||||
if cmd.startswith(cmd_part.lower()):
|
||||
@@ -347,68 +352,162 @@ el.replaceWith(d);
|
||||
|
||||
copilot_completer = SlashCommandCompleter()
|
||||
|
||||
def _finalize_mission(reason="completed", final_guide=""):
|
||||
mission = self.session_state.get('mission', {})
|
||||
if not mission or not mission.get('active', False):
|
||||
return
|
||||
mission['active'] = False
|
||||
if reason == "completed":
|
||||
self.console.print("\n[bold green]🎉 Mission Completed[/bold green]")
|
||||
elif reason == "user_cancelled":
|
||||
self.console.print("\n[yellow]Mission cancelled by user.[/yellow]")
|
||||
elif reason == "limit_reached":
|
||||
self.console.print("\n[yellow]Mission step limit reached.[/yellow]")
|
||||
|
||||
final_notes = "\n".join(mission.get('scratchpad_notes', []))
|
||||
guide = final_guide or mission.get('last_guide', '')
|
||||
asst_msg = f"Notes: {final_notes}\nGuide: {guide}" if final_notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": f"/mission {mission.get('goal', '')}"})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
|
||||
while True:
|
||||
# 2. Ask question
|
||||
from prompt_toolkit.styles import Style
|
||||
c_contrast = self._get_theme_color("contrast", "gray")
|
||||
ui_style = Style.from_dict({
|
||||
'bottom-toolbar': f'fg:{c_contrast}',
|
||||
})
|
||||
overrides = {}
|
||||
# Check for active mission auto-looping
|
||||
mission = self.session_state.get('mission', {})
|
||||
is_mission = mission.get('active', False) and not mission.get('paused', False)
|
||||
|
||||
session = PromptSession(
|
||||
history=self.history,
|
||||
input=self.pt_input,
|
||||
output=self.pt_output,
|
||||
completer=copilot_completer,
|
||||
reserve_space_for_menu=0,
|
||||
style=ui_style
|
||||
)
|
||||
try:
|
||||
# We use an internal try/finally to ensure that if something fails in prompt_async,
|
||||
# we don't leave the terminal in a strange state.
|
||||
question = await session.prompt_async(
|
||||
get_prompt_text,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=get_toolbar,
|
||||
multiline=True
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
state['cancelled'] = True
|
||||
question = ""
|
||||
if is_mission:
|
||||
# Force mode_range for mission mode
|
||||
state['context_mode'] = self.mode_range
|
||||
self.session_state['context_mode'] = self.mode_range
|
||||
|
||||
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
|
||||
return "cancel", None, None
|
||||
if mission.get('start_block_idx') is None:
|
||||
mission['start_block_idx'] = state['total_cmds']
|
||||
|
||||
# 3. Process Input via AIService
|
||||
directive = self.ai_service.process_copilot_input(question, self.session_state)
|
||||
start_idx = mission.get('start_block_idx', state['total_cmds'])
|
||||
cmds_since_start = max(1, (state['total_cmds'] - start_idx) + 1)
|
||||
state['context_cmd'] = max(state.get('context_cmd', 1), cmds_since_start)
|
||||
self.session_state['context_cmd'] = state['context_cmd']
|
||||
|
||||
if directive["action"] == "state_update":
|
||||
msg = directive['message']
|
||||
state['toolbar_msg'] = msg
|
||||
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
|
||||
step = mission.get('step', 1)
|
||||
max_steps = mission.get('max_steps', 10)
|
||||
|
||||
async def delayed_refresh():
|
||||
await asyncio.sleep(3.1)
|
||||
# Only invalidate if the message hasn't been replaced by a newer one
|
||||
if state.get('toolbar_msg') == msg:
|
||||
state['toolbar_msg'] = '' # Explicitly clear
|
||||
try:
|
||||
from prompt_toolkit.application.current import get_app
|
||||
app = get_app()
|
||||
if app: app.invalidate()
|
||||
except: pass
|
||||
asyncio.create_task(delayed_refresh())
|
||||
if step > max_steps:
|
||||
ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
|
||||
c_warn = self._get_theme_color("warning", "yellow")
|
||||
import html
|
||||
p_warn = html.escape(f"[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: ")
|
||||
try:
|
||||
ext_ans = await ext_session.prompt_async(HTML(f'<style fg="{c_warn}" bold="true">{p_warn}</style>'))
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
ext_ans = 'n'
|
||||
|
||||
# Move the cursor up and clean the line so the new prompt replaces the previous one
|
||||
sys.stdout.write('\x1b[1A\x1b[2K')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
if (ext_ans or 'y').lower().strip() in ('y', 'yes'):
|
||||
mission['max_steps'] += 10
|
||||
else:
|
||||
goal = mission.get('goal', '')
|
||||
question = f"[MISSION SUMMARY]: Step limit reached ({max_steps} steps). Provide a concise summary of all findings and current status for: {goal}"
|
||||
clean_question = question
|
||||
mission['active'] = False
|
||||
is_mission = False
|
||||
self.console.print(f"\n[bold cyan]🤖 Generating Final Mission Summary ({max_steps} steps reached)...[/bold cyan]")
|
||||
|
||||
if is_mission:
|
||||
goal = mission.get('goal', '')
|
||||
step = mission.get('step', 1)
|
||||
question = f"[MISSION STEP {step}]: Continue analysis of: {goal}"
|
||||
|
||||
scratchpad = mission.get('scratchpad_notes', [])
|
||||
if scratchpad:
|
||||
notes_text = "\n".join(f"- {n}" for n in scratchpad)
|
||||
question += f"\n\nPast Mission Notes:\n{notes_text}"
|
||||
|
||||
clean_question = question
|
||||
self.console.print(f"\n[bold cyan]🤖 Executing Mission Step {step}: {goal}[/bold cyan]")
|
||||
elif not is_mission and 'clean_question' in locals() and clean_question.startswith("[MISSION SUMMARY]"):
|
||||
# Pass-through to AI execution for generating final summary
|
||||
pass
|
||||
else:
|
||||
# Clean the toolbar message when a real question is asked
|
||||
state['toolbar_msg'] = ''
|
||||
# 2. Ask question
|
||||
from prompt_toolkit.styles import Style
|
||||
c_contrast = self._get_theme_color("contrast", "gray")
|
||||
ui_style = Style.from_dict({
|
||||
'bottom-toolbar': f'fg:{c_contrast}',
|
||||
})
|
||||
|
||||
clean_question = directive.get("clean_prompt", question)
|
||||
overrides = directive.get("overrides", {})
|
||||
session = PromptSession(
|
||||
history=self.history,
|
||||
input=self.pt_input,
|
||||
output=self.pt_output,
|
||||
completer=copilot_completer,
|
||||
reserve_space_for_menu=0,
|
||||
style=ui_style
|
||||
)
|
||||
try:
|
||||
question = await session.prompt_async(
|
||||
get_prompt_text,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=get_toolbar,
|
||||
multiline=True
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
state['cancelled'] = True
|
||||
question = ""
|
||||
|
||||
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# 3. Process Input via AIService
|
||||
directive = self.ai_service.process_copilot_input(question, self.session_state)
|
||||
|
||||
if directive["action"] == "mission_start":
|
||||
mission = self.session_state.get('mission', {})
|
||||
mission['start_block_idx'] = state['total_cmds']
|
||||
state['context_mode'] = self.mode_range
|
||||
self.session_state['context_mode'] = self.mode_range
|
||||
state['context_cmd'] = 1
|
||||
self.session_state['context_cmd'] = 1
|
||||
clean_question = f"[MISSION STEP 1]: {directive['clean_prompt']}"
|
||||
overrides = directive.get("overrides", {})
|
||||
self.console.print(f"\n[bold cyan]🤖 Starting Mission: {directive['clean_prompt']}[/bold cyan]")
|
||||
elif directive["action"] == "mission_cancel":
|
||||
_finalize_mission("user_cancelled")
|
||||
state['toolbar_msg'] = 'Mission cancelled'
|
||||
continue
|
||||
elif directive["action"] == "state_update":
|
||||
msg = directive['message']
|
||||
state['toolbar_msg'] = msg
|
||||
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
|
||||
|
||||
async def delayed_refresh():
|
||||
await asyncio.sleep(3.1)
|
||||
if state.get('toolbar_msg') == msg:
|
||||
state['toolbar_msg'] = ''
|
||||
try:
|
||||
from prompt_toolkit.application.current import get_app
|
||||
app = get_app()
|
||||
if app: app.invalidate()
|
||||
except: pass
|
||||
asyncio.create_task(delayed_refresh())
|
||||
|
||||
sys.stdout.write('\x1b[1A\x1b[2K')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
else:
|
||||
state['toolbar_msg'] = ''
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = False
|
||||
step = mission.get('step', 1)
|
||||
goal = mission.get('goal', '')
|
||||
clean_question = f"[MISSION FEEDBACK (Step {step})]: User provided guidance: {question}. Continue the mission for goal: {goal}"
|
||||
self.console.print(f"\n[bold cyan]🤖 Continuing Mission with User Feedback: {question}[/bold cyan]")
|
||||
overrides = directive.get("overrides", {})
|
||||
else:
|
||||
clean_question = directive.get("clean_prompt", question)
|
||||
overrides = directive.get("overrides", {})
|
||||
|
||||
# Merge node_info with session_state and overrides
|
||||
merged_node_info = node_info.copy()
|
||||
@@ -423,7 +522,6 @@ el.replaceWith(d);
|
||||
merged_node_info[k] = v
|
||||
|
||||
# 3. AI Execution
|
||||
# Use persona from overrides (one-shot) or from session state
|
||||
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
||||
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
||||
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
|
||||
@@ -450,7 +548,6 @@ el.replaceWith(d);
|
||||
nonlocal live_text, first_chunk
|
||||
if first_chunk:
|
||||
status_spinner.stop()
|
||||
# Print header rule before first chunk arrives
|
||||
self.console.print(Rule(
|
||||
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
|
||||
style=persona_color
|
||||
@@ -459,7 +556,6 @@ el.replaceWith(d);
|
||||
live_text += text
|
||||
md_parser.feed(text)
|
||||
|
||||
# Check for interruption during AI call
|
||||
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
|
||||
|
||||
try:
|
||||
@@ -468,13 +564,12 @@ el.replaceWith(d);
|
||||
result = await ai_task
|
||||
except asyncio.CancelledError:
|
||||
status_spinner.stop()
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# Ensure spinner is stopped if no chunks arrived
|
||||
if first_chunk:
|
||||
status_spinner.stop()
|
||||
|
||||
# Close the streamed output with a Rule
|
||||
if not first_chunk:
|
||||
md_parser.flush()
|
||||
self.console.print(Rule(style=persona_color))
|
||||
@@ -482,26 +577,35 @@ el.replaceWith(d);
|
||||
if not result or result.get("error"):
|
||||
if first_chunk and result and result.get("error"):
|
||||
self.console.print(f"[red]Error: {result['error']}[/red]")
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# If no chunks were streamed but we have a guide, print it as a panel
|
||||
if first_chunk and result and result.get("guide"):
|
||||
from rich.markdown import Markdown
|
||||
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
|
||||
|
||||
# Update copilot_chat_history with clean Q&A turn
|
||||
# Update copilot_chat_history or mission scratchpad
|
||||
if result and not result.get("error"):
|
||||
guide = result.get("guide", "")
|
||||
notes = result.get("notes", "")
|
||||
if guide:
|
||||
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": clean_question})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
|
||||
mission = self.session_state.get('mission', {})
|
||||
if mission.get('active', False):
|
||||
if notes:
|
||||
mission.setdefault('scratchpad_notes', []).append(notes)
|
||||
if guide:
|
||||
mission['last_guide'] = guide
|
||||
else:
|
||||
if guide or notes:
|
||||
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": clean_question})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
commands = result.get("commands", [])
|
||||
mission = self.session_state.get('mission', {})
|
||||
if not commands:
|
||||
if mission.get('active', False):
|
||||
_finalize_mission("completed", result.get("guide", ""))
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
@@ -510,11 +614,12 @@ el.replaceWith(d);
|
||||
style_color = self._get_theme_color(risk_style, fallback="green")
|
||||
|
||||
cmd_text = "\n".join(f" {i+1}. {c}" for i, c in enumerate(commands))
|
||||
# Explicitly use 'bold style_color' for both TITLE and BORDER to ensure maximum consistency
|
||||
self.console.print(Panel(cmd_text, title=f"[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]", border_style=f"bold {style_color}"))
|
||||
|
||||
if merged_node_info.get('trust', False) and risk != "destructive":
|
||||
self.console.print(f"[dim]⚙️ Auto-executing (Trust Mode)[/dim]")
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", commands, None
|
||||
|
||||
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
|
||||
@@ -526,41 +631,39 @@ el.replaceWith(d);
|
||||
import html
|
||||
try:
|
||||
p_text = html.escape(f"Send? (y/n/e/range) [n]: ")
|
||||
# Use the EXACT same style_color and force bold="true" for Prompt-Toolkit
|
||||
action = await confirm_session.prompt_async(HTML(f'<style fg="{style_color}" bold="true">{p_text}</style>'), key_bindings=c_bindings)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
_finalize_mission("user_cancelled", result.get("guide", ""))
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
def parse_indices(text, max_len):
|
||||
"""Helper to parse '1-3, 5, 7' into [0, 1, 2, 4, 6]."""
|
||||
indices = []
|
||||
# Replace commas with spaces and split
|
||||
parts = text.replace(',', ' ').split()
|
||||
for part in parts:
|
||||
if '-' in part:
|
||||
try:
|
||||
start, end = map(int, part.split('-'))
|
||||
# Ensure inclusive and 0-indexed
|
||||
indices.extend(range(start-1, end))
|
||||
except: continue
|
||||
elif part.isdigit():
|
||||
indices.append(int(part)-1)
|
||||
# Filter valid indices and remove duplicates
|
||||
return [i for i in sorted(set(indices)) if 0 <= i < max_len]
|
||||
|
||||
action_l = (action or "n").lower().strip()
|
||||
if action_l in ('y', 'yes', 'all'):
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", commands, None
|
||||
|
||||
# Check for numeric selection (e.g., "1, 2-4")
|
||||
if re.match(r'^[0-9,\-\s]+$', action_l):
|
||||
selected_idxs = parse_indices(action_l, len(commands))
|
||||
if selected_idxs:
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", [commands[i] for i in selected_idxs], None
|
||||
|
||||
elif action_l.startswith('e'):
|
||||
# Check if it's a selective edit like 'e1-2'
|
||||
selection_str = action_l[1:].strip()
|
||||
if selection_str:
|
||||
idxs = parse_indices(selection_str, len(commands))
|
||||
@@ -586,14 +689,23 @@ el.replaceWith(d);
|
||||
default=target, multiline=True, key_bindings=e_bindings
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = True
|
||||
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
if edited and edited.strip():
|
||||
# Split by lines to ensure core.py applies delay between each command
|
||||
lines = [l.strip() for l in edited.split('\n') if l.strip()]
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "custom", None, lines
|
||||
|
||||
# User rejected/cancelled the commands
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = True
|
||||
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
|
||||
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
@@ -648,7 +760,8 @@ el.replaceWith(d);
|
||||
is_single = saved_mode in (self.mode_single, 1, 'SINGLE', 'single')
|
||||
|
||||
if is_range or is_single:
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1:
|
||||
is_mission = self.session_state.get('mission', {}).get('active', False)
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
@@ -681,15 +794,17 @@ el.replaceWith(d);
|
||||
self.session_state['last_total_cmds'] = total_cmds
|
||||
self.session_state['last_total_lines'] = total_lines
|
||||
|
||||
# 1. Visual Separation
|
||||
# 1. Visual Separation (Only show help banner on initial entry)
|
||||
self.console.print("") # Real line break
|
||||
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
||||
self.console.print(Panel(
|
||||
"[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]",
|
||||
border_style="cyan"
|
||||
))
|
||||
self.console.print("\n") # Small space before the copilot prompt
|
||||
if not self.session_state.get('banner_shown', False):
|
||||
self.console.print(Panel(
|
||||
"[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]",
|
||||
border_style="cyan"
|
||||
))
|
||||
self.session_state['banner_shown'] = True
|
||||
self.console.print("") # Small space before the copilot prompt
|
||||
|
||||
bindings = KeyBindings()
|
||||
@bindings.add('c-up')
|
||||
@@ -771,7 +886,7 @@ el.replaceWith(d);
|
||||
text = app.current_buffer.text
|
||||
# Only show command help if typing the first command and there are no spaces
|
||||
if text.startswith('/') and ' ' not in text:
|
||||
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear']
|
||||
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear', '/mission', '/cancel']
|
||||
matches = [c for c in commands if c.startswith(text.lower())]
|
||||
if matches:
|
||||
m_text = html.escape(f"Available: {' '.join(matches)}")
|
||||
@@ -779,7 +894,7 @@ el.replaceWith(d);
|
||||
|
||||
m_label = {self.mode_range: "RANGE", self.mode_single: "SINGLE", self.mode_lines: "LINES"}[state['context_mode']]
|
||||
if state['context_mode'] == self.mode_lines:
|
||||
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
||||
base_str = f'\u25b6 [Tab: {m_label}] {state["context_lines"]}/{state["total_lines"]}L (Ctrl+\u2191/\u2193 adjusts lines)'
|
||||
else:
|
||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||
|
||||
@@ -802,7 +917,7 @@ el.replaceWith(d);
|
||||
p = clean_preview(b[2])
|
||||
if p:
|
||||
# Truncar comandos individuales largos
|
||||
if len(p) > 25: p = p[:22] + "..."
|
||||
if len(p) > 25: p = p[:22].rstrip(' .,-_') + "..."
|
||||
previews.append(p)
|
||||
|
||||
if not previews:
|
||||
@@ -810,12 +925,12 @@ el.replaceWith(d);
|
||||
elif len(previews) <= 3:
|
||||
desc = " + ".join(previews)
|
||||
else:
|
||||
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})"
|
||||
desc = f"{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})"
|
||||
else:
|
||||
# Modo SINGLE original
|
||||
desc = clean_preview(blocks[idx][2])
|
||||
|
||||
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
||||
base_str = f'\u25b6 [Tab: {m_label}] {desc}'
|
||||
|
||||
# Wrap base_str in a style to maintain consistency and avoid glitches
|
||||
# The fg color will be inherited from bottom-toolbar global style if not specified here
|
||||
@@ -849,7 +964,9 @@ el.replaceWith(d);
|
||||
('/trust', 'Enable auto-execute'),
|
||||
('/untrust', 'Disable auto-execute'),
|
||||
('/memorize', 'Add fact to memory'),
|
||||
('/clear', 'Clear memory')
|
||||
('/clear', 'Clear memory'),
|
||||
('/mission', 'Start autonomous mission'),
|
||||
('/cancel', 'Cancel active mission')
|
||||
]
|
||||
for cmd, desc in commands:
|
||||
if cmd.startswith(cmd_part.lower()):
|
||||
@@ -857,68 +974,162 @@ el.replaceWith(d);
|
||||
|
||||
copilot_completer = SlashCommandCompleter()
|
||||
|
||||
def _finalize_mission(reason="completed", final_guide=""):
|
||||
mission = self.session_state.get('mission', {})
|
||||
if not mission or not mission.get('active', False):
|
||||
return
|
||||
mission['active'] = False
|
||||
if reason == "completed":
|
||||
self.console.print("\n[bold green]🎉 Mission Completed[/bold green]")
|
||||
elif reason == "user_cancelled":
|
||||
self.console.print("\n[yellow]Mission cancelled by user.[/yellow]")
|
||||
elif reason == "limit_reached":
|
||||
self.console.print("\n[yellow]Mission step limit reached.[/yellow]")
|
||||
|
||||
final_notes = "\n".join(mission.get('scratchpad_notes', []))
|
||||
guide = final_guide or mission.get('last_guide', '')
|
||||
asst_msg = f"Notes: {final_notes}\nGuide: {guide}" if final_notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": f"/mission {mission.get('goal', '')}"})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
|
||||
while True:
|
||||
# 2. Ask question
|
||||
from prompt_toolkit.styles import Style
|
||||
c_contrast = self._get_theme_color("contrast", "gray")
|
||||
ui_style = Style.from_dict({
|
||||
'bottom-toolbar': f'fg:{c_contrast}',
|
||||
})
|
||||
overrides = {}
|
||||
# Check for active mission auto-looping
|
||||
mission = self.session_state.get('mission', {})
|
||||
is_mission = mission.get('active', False) and not mission.get('paused', False)
|
||||
|
||||
session = PromptSession(
|
||||
history=self.history,
|
||||
input=self.pt_input,
|
||||
output=self.pt_output,
|
||||
completer=copilot_completer,
|
||||
reserve_space_for_menu=0,
|
||||
style=ui_style
|
||||
)
|
||||
try:
|
||||
# We use an internal try/finally to ensure that if something fails in prompt_async,
|
||||
# we don't leave the terminal in a strange state.
|
||||
question = await session.prompt_async(
|
||||
get_prompt_text,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=get_toolbar,
|
||||
multiline=True
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
state['cancelled'] = True
|
||||
question = ""
|
||||
if is_mission:
|
||||
# Force mode_range for mission mode
|
||||
state['context_mode'] = self.mode_range
|
||||
self.session_state['context_mode'] = self.mode_range
|
||||
|
||||
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
|
||||
return "cancel", None, None
|
||||
if mission.get('start_block_idx') is None:
|
||||
mission['start_block_idx'] = state['total_cmds']
|
||||
|
||||
# 3. Process Input via AIService
|
||||
directive = self.ai_service.process_copilot_input(question, self.session_state)
|
||||
start_idx = mission.get('start_block_idx', state['total_cmds'])
|
||||
cmds_since_start = max(1, (state['total_cmds'] - start_idx) + 1)
|
||||
state['context_cmd'] = max(state.get('context_cmd', 1), cmds_since_start)
|
||||
self.session_state['context_cmd'] = state['context_cmd']
|
||||
|
||||
if directive["action"] == "state_update":
|
||||
msg = directive['message']
|
||||
state['toolbar_msg'] = msg
|
||||
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
|
||||
step = mission.get('step', 1)
|
||||
max_steps = mission.get('max_steps', 10)
|
||||
|
||||
async def delayed_refresh():
|
||||
await asyncio.sleep(3.1)
|
||||
# Only invalidate if the message hasn't been replaced by a newer one
|
||||
if state.get('toolbar_msg') == msg:
|
||||
state['toolbar_msg'] = '' # Explicitly clear
|
||||
try:
|
||||
from prompt_toolkit.application.current import get_app
|
||||
app = get_app()
|
||||
if app: app.invalidate()
|
||||
except: pass
|
||||
asyncio.create_task(delayed_refresh())
|
||||
if step > max_steps:
|
||||
ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
|
||||
c_warn = self._get_theme_color("warning", "yellow")
|
||||
import html
|
||||
p_warn = html.escape(f"[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: ")
|
||||
try:
|
||||
ext_ans = await ext_session.prompt_async(HTML(f'<style fg="{c_warn}" bold="true">{p_warn}</style>'))
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
ext_ans = 'n'
|
||||
|
||||
# Move the cursor up and clean the line so the new prompt replaces the previous one
|
||||
sys.stdout.write('\x1b[1A\x1b[2K')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
if (ext_ans or 'y').lower().strip() in ('y', 'yes'):
|
||||
mission['max_steps'] += 10
|
||||
else:
|
||||
goal = mission.get('goal', '')
|
||||
question = f"[MISSION SUMMARY]: Step limit reached ({max_steps} steps). Provide a concise summary of all findings and current status for: {goal}"
|
||||
clean_question = question
|
||||
mission['active'] = False
|
||||
is_mission = False
|
||||
self.console.print(f"\n[bold cyan]🤖 Generating Final Mission Summary ({max_steps} steps reached)...[/bold cyan]")
|
||||
|
||||
if is_mission:
|
||||
goal = mission.get('goal', '')
|
||||
step = mission.get('step', 1)
|
||||
question = f"[MISSION STEP {step}]: Continue analysis of: {goal}"
|
||||
|
||||
scratchpad = mission.get('scratchpad_notes', [])
|
||||
if scratchpad:
|
||||
notes_text = "\n".join(f"- {n}" for n in scratchpad)
|
||||
question += f"\n\nPast Mission Notes:\n{notes_text}"
|
||||
|
||||
clean_question = question
|
||||
self.console.print(f"\n[bold cyan]🤖 Executing Mission Step {step}: {goal}[/bold cyan]")
|
||||
elif not is_mission and 'clean_question' in locals() and clean_question.startswith("[MISSION SUMMARY]"):
|
||||
# Pass-through to AI execution for generating final summary
|
||||
pass
|
||||
else:
|
||||
# Clean the toolbar message when a real question is asked
|
||||
state['toolbar_msg'] = ''
|
||||
# 2. Ask question
|
||||
from prompt_toolkit.styles import Style
|
||||
c_contrast = self._get_theme_color("contrast", "gray")
|
||||
ui_style = Style.from_dict({
|
||||
'bottom-toolbar': f'fg:{c_contrast}',
|
||||
})
|
||||
|
||||
clean_question = directive.get("clean_prompt", question)
|
||||
overrides = directive.get("overrides", {})
|
||||
session = PromptSession(
|
||||
history=self.history,
|
||||
input=self.pt_input,
|
||||
output=self.pt_output,
|
||||
completer=copilot_completer,
|
||||
reserve_space_for_menu=0,
|
||||
style=ui_style
|
||||
)
|
||||
try:
|
||||
question = await session.prompt_async(
|
||||
get_prompt_text,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=get_toolbar,
|
||||
multiline=True
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
state['cancelled'] = True
|
||||
question = ""
|
||||
|
||||
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# 3. Process Input via AIService
|
||||
directive = self.ai_service.process_copilot_input(question, self.session_state)
|
||||
|
||||
if directive["action"] == "mission_start":
|
||||
mission = self.session_state.get('mission', {})
|
||||
mission['start_block_idx'] = state['total_cmds']
|
||||
state['context_mode'] = self.mode_range
|
||||
self.session_state['context_mode'] = self.mode_range
|
||||
state['context_cmd'] = 1
|
||||
self.session_state['context_cmd'] = 1
|
||||
clean_question = f"[MISSION STEP 1]: {directive['clean_prompt']}"
|
||||
overrides = directive.get("overrides", {})
|
||||
self.console.print(f"\n[bold cyan]🤖 Starting Mission: {directive['clean_prompt']}[/bold cyan]")
|
||||
elif directive["action"] == "mission_cancel":
|
||||
_finalize_mission("user_cancelled")
|
||||
state['toolbar_msg'] = 'Mission cancelled'
|
||||
continue
|
||||
elif directive["action"] == "state_update":
|
||||
msg = directive['message']
|
||||
state['toolbar_msg'] = msg
|
||||
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
|
||||
|
||||
async def delayed_refresh():
|
||||
await asyncio.sleep(3.1)
|
||||
if state.get('toolbar_msg') == msg:
|
||||
state['toolbar_msg'] = ''
|
||||
try:
|
||||
from prompt_toolkit.application.current import get_app
|
||||
app = get_app()
|
||||
if app: app.invalidate()
|
||||
except: pass
|
||||
asyncio.create_task(delayed_refresh())
|
||||
|
||||
sys.stdout.write('\x1b[1A\x1b[2K')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
else:
|
||||
state['toolbar_msg'] = ''
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = False
|
||||
step = mission.get('step', 1)
|
||||
goal = mission.get('goal', '')
|
||||
clean_question = f"[MISSION FEEDBACK (Step {step})]: User provided guidance: {question}. Continue the mission for goal: {goal}"
|
||||
self.console.print(f"\n[bold cyan]🤖 Continuing Mission with User Feedback: {question}[/bold cyan]")
|
||||
overrides = directive.get("overrides", {})
|
||||
else:
|
||||
clean_question = directive.get("clean_prompt", question)
|
||||
overrides = directive.get("overrides", {})
|
||||
|
||||
# Merge node_info with session_state and overrides
|
||||
merged_node_info = node_info.copy()
|
||||
@@ -933,7 +1144,6 @@ el.replaceWith(d);
|
||||
merged_node_info[k] = v
|
||||
|
||||
# 3. AI Execution
|
||||
# Use persona from overrides (one-shot) or from session state
|
||||
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
||||
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
||||
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
|
||||
@@ -960,7 +1170,6 @@ el.replaceWith(d);
|
||||
nonlocal live_text, first_chunk
|
||||
if first_chunk:
|
||||
status_spinner.stop()
|
||||
# Print header rule before first chunk arrives
|
||||
self.console.print(Rule(
|
||||
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
|
||||
style=persona_color
|
||||
@@ -969,7 +1178,6 @@ el.replaceWith(d);
|
||||
live_text += text
|
||||
md_parser.feed(text)
|
||||
|
||||
# Check for interruption during AI call
|
||||
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
|
||||
|
||||
try:
|
||||
@@ -978,13 +1186,12 @@ el.replaceWith(d);
|
||||
result = await ai_task
|
||||
except asyncio.CancelledError:
|
||||
status_spinner.stop()
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# Ensure spinner is stopped if no chunks arrived
|
||||
if first_chunk:
|
||||
status_spinner.stop()
|
||||
|
||||
# Close the streamed output with a Rule
|
||||
if not first_chunk:
|
||||
md_parser.flush()
|
||||
self.console.print(Rule(style=persona_color))
|
||||
@@ -992,26 +1199,35 @@ el.replaceWith(d);
|
||||
if not result or result.get("error"):
|
||||
if first_chunk and result and result.get("error"):
|
||||
self.console.print(f"[red]Error: {result['error']}[/red]")
|
||||
_finalize_mission("user_cancelled")
|
||||
return "cancel", None, None
|
||||
|
||||
# If no chunks were streamed but we have a guide, print it as a panel
|
||||
if first_chunk and result and result.get("guide"):
|
||||
from rich.markdown import Markdown
|
||||
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
|
||||
|
||||
# Update copilot_chat_history with clean Q&A turn
|
||||
# Update copilot_chat_history or mission scratchpad
|
||||
if result and not result.get("error"):
|
||||
guide = result.get("guide", "")
|
||||
notes = result.get("notes", "")
|
||||
if guide:
|
||||
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": clean_question})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
|
||||
mission = self.session_state.get('mission', {})
|
||||
if mission.get('active', False):
|
||||
if notes:
|
||||
mission.setdefault('scratchpad_notes', []).append(notes)
|
||||
if guide:
|
||||
mission['last_guide'] = guide
|
||||
else:
|
||||
if guide or notes:
|
||||
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
|
||||
hist = self.session_state.setdefault("copilot_chat_history", [])
|
||||
hist.append({"role": "user", "content": clean_question})
|
||||
hist.append({"role": "assistant", "content": asst_msg})
|
||||
self.session_state["copilot_chat_history"] = hist[-10:]
|
||||
commands = result.get("commands", [])
|
||||
mission = self.session_state.get('mission', {})
|
||||
if not commands:
|
||||
if mission.get('active', False):
|
||||
_finalize_mission("completed", result.get("guide", ""))
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
@@ -1020,11 +1236,12 @@ el.replaceWith(d);
|
||||
style_color = self._get_theme_color(risk_style, fallback="green")
|
||||
|
||||
cmd_text = "\n".join(f" {i+1}. {c}" for i, c in enumerate(commands))
|
||||
# Explicitly use 'bold style_color' for both TITLE and BORDER to ensure maximum consistency
|
||||
self.console.print(Panel(cmd_text, title=f"[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]", border_style=f"bold {style_color}"))
|
||||
|
||||
if merged_node_info.get('trust', False) and risk != "destructive":
|
||||
self.console.print(f"[dim]⚙️ Auto-executing (Trust Mode)[/dim]")
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", commands, None
|
||||
|
||||
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
|
||||
@@ -1036,41 +1253,39 @@ el.replaceWith(d);
|
||||
import html
|
||||
try:
|
||||
p_text = html.escape(f"Send? (y/n/e/range) [n]: ")
|
||||
# Use the EXACT same style_color and force bold="true" for Prompt-Toolkit
|
||||
action = await confirm_session.prompt_async(HTML(f'<style fg="{style_color}" bold="true">{p_text}</style>'), key_bindings=c_bindings)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
_finalize_mission("user_cancelled", result.get("guide", ""))
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
def parse_indices(text, max_len):
|
||||
"""Helper to parse '1-3, 5, 7' into [0, 1, 2, 4, 6]."""
|
||||
indices = []
|
||||
# Replace commas with spaces and split
|
||||
parts = text.replace(',', ' ').split()
|
||||
for part in parts:
|
||||
if '-' in part:
|
||||
try:
|
||||
start, end = map(int, part.split('-'))
|
||||
# Ensure inclusive and 0-indexed
|
||||
indices.extend(range(start-1, end))
|
||||
except: continue
|
||||
elif part.isdigit():
|
||||
indices.append(int(part)-1)
|
||||
# Filter valid indices and remove duplicates
|
||||
return [i for i in sorted(set(indices)) if 0 <= i < max_len]
|
||||
|
||||
action_l = (action or "n").lower().strip()
|
||||
if action_l in ('y', 'yes', 'all'):
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", commands, None
|
||||
|
||||
# Check for numeric selection (e.g., "1, 2-4")
|
||||
if re.match(r'^[0-9,\-\s]+$', action_l):
|
||||
selected_idxs = parse_indices(action_l, len(commands))
|
||||
if selected_idxs:
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "send_all", [commands[i] for i in selected_idxs], None
|
||||
|
||||
elif action_l.startswith('e'):
|
||||
# Check if it's a selective edit like 'e1-2'
|
||||
selection_str = action_l[1:].strip()
|
||||
if selection_str:
|
||||
idxs = parse_indices(selection_str, len(commands))
|
||||
@@ -1096,14 +1311,23 @@ el.replaceWith(d);
|
||||
default=target, multiline=True, key_bindings=e_bindings
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = True
|
||||
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
if edited and edited.strip():
|
||||
# Split by lines to ensure core.py applies delay between each command
|
||||
lines = [l.strip() for l in edited.split('\n') if l.strip()]
|
||||
if mission.get('active', False):
|
||||
mission['step'] = mission.get('step', 1) + 1
|
||||
return "custom", None, lines
|
||||
|
||||
# User rejected/cancelled the commands
|
||||
if mission.get('active', False):
|
||||
mission['paused'] = True
|
||||
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
|
||||
|
||||
self.console.print("")
|
||||
return "continue", None, None
|
||||
|
||||
@@ -1144,7 +1368,7 @@ on_ai_call: async function(active_buffer, question) -> result_dict</p></div>
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.user_handler API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -516,7 +516,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.cli.validators API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -508,7 +508,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.connpy_pb2 API documentation</title>
|
||||
<meta name="description" content="Generated protocol buffer code.">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -61,7 +61,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.connpy_pb2_grpc API documentation</title>
|
||||
<meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -7006,7 +7006,7 @@ def stop_api(request,
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -107,7 +107,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title>
|
||||
<meta name="description" content="Generated protocol buffer code.">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -62,7 +62,7 @@ el.replaceWith(d);
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
@@ -81,7 +81,7 @@ el.replaceWith(d);
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
@@ -100,7 +100,7 @@ el.replaceWith(d);
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
@@ -119,7 +119,7 @@ el.replaceWith(d);
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
@@ -168,7 +168,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title>
|
||||
<meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -366,7 +366,7 @@ def invoke_plugin(request,
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.server API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -604,7 +604,7 @@ def service(self):
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.server.AuthInterceptor.OPEN_METHODS"><code class="name">var <span class="ident">OPEN_METHODS</span></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>Methods</h3>
|
||||
@@ -1583,7 +1583,8 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
||||
is_single = saved_mode in (1, 'SINGLE', 'single')
|
||||
|
||||
if is_range or is_single:
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1:
|
||||
is_mission = session_state.get('mission', {}).get('active', False) or (isinstance(node_info, dict) and node_info.get('mission', {}).get('active', False))
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
@@ -1627,6 +1628,14 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
||||
))
|
||||
|
||||
while True:
|
||||
# Refresh latest buffer from terminal log
|
||||
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
|
||||
cleaned_buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||
buffer = cleaned_buffer
|
||||
|
||||
# 0. Drain the queue of any stale messages before starting a new interaction
|
||||
while not remote_stream.copilot_queue.empty():
|
||||
try:
|
||||
@@ -1760,16 +1769,74 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
||||
if action == "send_all":
|
||||
commands = result.get("commands", [])
|
||||
await n.inject_commands(commands, child_fd, on_inject=on_inject)
|
||||
return
|
||||
elif action.startswith("custom:"):
|
||||
custom_cmds_raw = action[7:]
|
||||
custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split('\n') if cmd.strip()]
|
||||
await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject)
|
||||
return
|
||||
else:
|
||||
os.write(child_fd, b'\x15\r')
|
||||
return
|
||||
|
||||
# Dynamic Wait for Device Prompt Settle
|
||||
prompt_pattern = node_info.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')
|
||||
import time
|
||||
import re
|
||||
start_t = time.time()
|
||||
last_len = 0
|
||||
quiet_count = 0
|
||||
while time.time() - start_t < 60:
|
||||
await asyncio.sleep(0.15)
|
||||
cur_bytes = n.mylog.getvalue()
|
||||
if len(cur_bytes) == last_len:
|
||||
quiet_count += 1
|
||||
else:
|
||||
quiet_count = 0
|
||||
last_len = len(cur_bytes)
|
||||
|
||||
clean_txt = n._logclean(cur_bytes.decode(errors='replace'), True).strip()
|
||||
lines = [l.strip() for l in clean_txt.split('\n') if l.strip()]
|
||||
last_line = lines[-1] if lines else ""
|
||||
if quiet_count >= 2 and re.search(prompt_pattern, last_line):
|
||||
break
|
||||
|
||||
# Recalculate context blocks with updated buffer
|
||||
raw_bytes = n.mylog.getvalue()
|
||||
from connpy.utils import log_cleaner
|
||||
cleaned_buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||
last_line = cleaned_buffer.split('\n')[-1].strip() if cleaned_buffer.strip() else "(prompt)"
|
||||
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
|
||||
node_info["context_blocks"] = blocks
|
||||
|
||||
total_cmds = len(blocks)
|
||||
total_lines = len(cleaned_buffer.split('\n'))
|
||||
|
||||
last_total_cmds = session_state.get('last_total_cmds', None)
|
||||
saved_cmd = session_state.get('context_cmd', 1)
|
||||
is_mission = session_state.get('mission', {}).get('active', False) or (isinstance(node_info, dict) and node_info.get('mission', {}).get('active', False))
|
||||
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
|
||||
new_cmds = total_cmds - last_total_cmds
|
||||
initial_cmd = saved_cmd + new_cmds
|
||||
else:
|
||||
initial_cmd = saved_cmd
|
||||
|
||||
session_state['context_cmd'] = max(1, initial_cmd)
|
||||
session_state['last_total_cmds'] = total_cmds
|
||||
session_state['last_total_lines'] = total_lines
|
||||
|
||||
node_info['context_cmd'] = min(session_state['context_cmd'], max(1, total_cmds))
|
||||
node_info['context_lines'] = min(session_state.get('context_lines', 50), max(1, total_lines))
|
||||
node_info_json = json.dumps(node_info)
|
||||
|
||||
preview_str = raw_bytes[-200:].decode(errors='replace') if isinstance(raw_bytes, bytes) else str(raw_bytes)[-200:]
|
||||
|
||||
# Signal prompt settle and send updated blocks to client
|
||||
response_queue.put(connpy_pb2.InteractResponse(
|
||||
copilot_prompt=True,
|
||||
copilot_buffer_preview=preview_str,
|
||||
copilot_node_info_json=node_info_json
|
||||
))
|
||||
continue
|
||||
|
||||
asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler))
|
||||
except Exception as e:
|
||||
import traceback
|
||||
@@ -2516,7 +2583,7 @@ def service(self):
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.stubs API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -1373,7 +1373,7 @@ def set_reserved_names(self, names):
|
||||
self.config = config
|
||||
|
||||
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, os
|
||||
from ..core import copilot_terminal_mode
|
||||
from . import connpy_pb2
|
||||
|
||||
@@ -1391,6 +1391,7 @@ def set_reserved_names(self, names):
|
||||
)
|
||||
self.copilot_history = interface.history
|
||||
self.copilot_state = interface.session_state
|
||||
interface.session_state['banner_shown'] = False
|
||||
|
||||
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
|
||||
# Send request to server
|
||||
@@ -1413,6 +1414,7 @@ def set_reserved_names(self, names):
|
||||
|
||||
# Wrap in async loop
|
||||
async def run_remote_copilot():
|
||||
nonlocal blocks, node_info
|
||||
while True:
|
||||
action, commands, custom_cmd = await interface.run_session(
|
||||
raw_bytes=bytes(client_buffer_bytes),
|
||||
@@ -1426,6 +1428,32 @@ def set_reserved_names(self, names):
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action="continue"))
|
||||
continue
|
||||
|
||||
if action in ("send_all", "custom"):
|
||||
action_sent = "cancel"
|
||||
if action == "send_all" and commands:
|
||||
action_sent = f"custom:{chr(10).join(commands)}"
|
||||
elif action == "custom" and custom_cmd:
|
||||
action_sent = f"custom:{chr(10).join(custom_cmd)}"
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
|
||||
|
||||
print("\r\n\033[2m [Remote] Ejecutando comandos y esperando al prompt...\033[0m\r\n", flush=True)
|
||||
while True:
|
||||
try:
|
||||
prompt_res = response_queue.get(timeout=0.1)
|
||||
if prompt_res is None:
|
||||
return "cancel", None, None
|
||||
if prompt_res.stdout_data:
|
||||
os.write(sys.stdout.fileno(), prompt_res.stdout_data)
|
||||
client_buffer_bytes.extend(prompt_res.stdout_data)
|
||||
if prompt_res.copilot_prompt:
|
||||
if prompt_res.copilot_node_info_json:
|
||||
node_info = json.loads(prompt_res.copilot_node_info_json)
|
||||
blocks = node_info.get("context_blocks", [])
|
||||
break
|
||||
except queue.Empty:
|
||||
await asyncio.sleep(0.05)
|
||||
continue
|
||||
|
||||
return action, commands, custom_cmd
|
||||
|
||||
with copilot_terminal_mode():
|
||||
@@ -1433,14 +1461,7 @@ def set_reserved_names(self, names):
|
||||
|
||||
print("\033[2m Returning to session...\033[0m", flush=True)
|
||||
# Prepare final action for server
|
||||
action_sent = "cancel"
|
||||
if action == "send_all" and commands:
|
||||
# In remote mode, send the selected commands as a custom block
|
||||
# so the server executes exactly what the user picked (e.g., selection '1')
|
||||
action_sent = f"custom:{chr(10).join(commands)}"
|
||||
elif action == "custom" and custom_cmd:
|
||||
action_sent = f"custom:{chr(10).join(custom_cmd)}"
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
|
||||
request_queue.put(connpy_pb2.InteractRequest(copilot_action="cancel"))
|
||||
resume_generator()
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
||||
@@ -2967,7 +2988,7 @@ def stop_api(self):
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.user_registry API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -312,7 +312,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.grpc_layer.utils API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -138,7 +138,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+35
-10
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy API documentation</title>
|
||||
<meta name="description" content="<p align="center">
|
||||
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
|
||||
@@ -41,7 +41,7 @@ el.replaceWith(d);
|
||||
<p align="center">
|
||||
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
|
||||
</p>
|
||||
<h1 id="connpy-v620">Connpy (v6.2.0)</h1>
|
||||
<h1 id="connpy-v630">Connpy (v6.3.0)</h1>
|
||||
<p><a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/v/connpy.svg?style=flat-square"></a>
|
||||
<a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square"></a>
|
||||
<a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400"></a>
|
||||
@@ -57,7 +57,9 @@ el.replaceWith(d);
|
||||
<h3 id="1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</h3>
|
||||
<p>Invoke the context-aware AI Copilot directly inside any active terminal session by pressing <strong><code>Ctrl + Space</code></strong>.
|
||||
* <strong>Context Modes</strong>: Cycles through <code>LINES</code> (sends raw scroll buffer), <code>SINGLE</code> (captures exactly one command + output block), and <code>RANGE</code> (logical group of recent commands) using <strong><code>Ctrl+Up/Down</code></strong>.
|
||||
* <strong>Slash Commands (<code>/</code>)</strong>: Control the AI persona and safety settings:
|
||||
* <strong>Slash Commands (<code>/</code>)</strong>: Control the AI persona, safety settings, and mission mode:
|
||||
* <code>/mission [objective]</code>: Start an autonomous multi-step investigation mission with human approval.
|
||||
* <code>/cancel</code>: Abort active mission.
|
||||
* <code>/architect</code> / <code>/engineer</code>: Swaps the agent between high-level strategist and technical executor.
|
||||
* <code>/trust</code> / <code>/untrust</code>: Configures auto-run behavior for suggested non-destructive commands.
|
||||
* <code>/os [system]</code>: Manually overrides target OS parsing rules (e.g. <code>/os cisco_ios</code>).
|
||||
@@ -2421,6 +2423,7 @@ class node:
|
||||
# Save history back to stream for persistence in current session
|
||||
stream.copilot_history = interface.history
|
||||
stream.copilot_state = interface.session_state
|
||||
interface.session_state['banner_shown'] = False
|
||||
|
||||
ai_service = AIService(config)
|
||||
|
||||
@@ -2451,6 +2454,32 @@ class node:
|
||||
node_info=node_info,
|
||||
on_ai_call=on_ai_call
|
||||
)
|
||||
if action in ("send_all", "custom"):
|
||||
cmds_to_send = commands if action == "send_all" else custom_cmd
|
||||
await self.inject_commands(cmds_to_send, child_fd)
|
||||
|
||||
# Dynamic Wait for Device Prompt Settle
|
||||
prompt_pattern = node_info.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')
|
||||
start_t = time()
|
||||
last_len = 0
|
||||
quiet_count = 0
|
||||
while time() - start_t < 60:
|
||||
await asyncio.sleep(0.15)
|
||||
cur_bytes = self.mylog.getvalue()
|
||||
if len(cur_bytes) == last_len:
|
||||
quiet_count += 1
|
||||
else:
|
||||
quiet_count = 0
|
||||
last_len = len(cur_bytes)
|
||||
|
||||
clean_txt = self._logclean(cur_bytes.decode(errors='replace'), True).strip()
|
||||
lines = [l.strip() for l in clean_txt.split('\n') if l.strip()]
|
||||
last_line = lines[-1] if lines else ""
|
||||
if quiet_count >= 2 and re.search(prompt_pattern, last_line):
|
||||
break
|
||||
|
||||
raw_bytes = self.mylog.getvalue()
|
||||
continue
|
||||
if action == "continue":
|
||||
continue
|
||||
break
|
||||
@@ -2462,11 +2491,7 @@ class node:
|
||||
elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'):
|
||||
stream._loop.add_reader(stream.stdin_fd, stream._read_ready)
|
||||
|
||||
if action in ("send_all", "custom"):
|
||||
cmds_to_send = commands if action == "send_all" else custom_cmd
|
||||
await self.inject_commands(cmds_to_send, child_fd)
|
||||
else:
|
||||
os.write(child_fd, b'\x15\r')
|
||||
os.write(child_fd, b'\x15\r')
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"\n[ERROR in Copilot Handler] {e}", flush=True)
|
||||
@@ -4087,7 +4112,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
|
||||
<nav id="sidebar">
|
||||
<div class="toc">
|
||||
<ul>
|
||||
<li><a href="#connpy-v620">Connpy (v6.2.0)</a><ul>
|
||||
<li><a href="#connpy-v630">Connpy (v6.3.0)</a><ul>
|
||||
<li><a href="#1-ai-system">1. 🤖 AI System</a><ul>
|
||||
<li><a href="#1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</a></li>
|
||||
<li><a href="#1b-ai-chat-conn-ai">1b. AI Chat (conn ai)</a></li>
|
||||
@@ -4195,7 +4220,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.mcp_client API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -349,7 +349,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.proto API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -60,7 +60,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.ai_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -259,6 +259,26 @@ el.replaceWith(d);
|
||||
else:
|
||||
return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}}
|
||||
|
||||
elif cmd == "/mission":
|
||||
if args:
|
||||
session_state['mission'] = {
|
||||
'active': True,
|
||||
'goal': args,
|
||||
'step': 1,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': None,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
return {"action": "mission_start", "clean_prompt": args, "overrides": {}}
|
||||
else:
|
||||
return {"action": "state_update", "message": "Usage: /mission <objective_description>"}
|
||||
|
||||
elif cmd in ("/cancel", "/abort"):
|
||||
if session_state.get('mission', {}).get('active'):
|
||||
session_state['mission']['active'] = False
|
||||
return {"action": "mission_cancel", "message": "Mission cancelled"}
|
||||
return {"action": "state_update", "message": "No active mission to cancel"}
|
||||
|
||||
# Unknown command, execute normally
|
||||
return {"action": "execute", "clean_prompt": text, "overrides": {}}
|
||||
|
||||
@@ -877,6 +897,26 @@ el.replaceWith(d);
|
||||
else:
|
||||
return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}}
|
||||
|
||||
elif cmd == "/mission":
|
||||
if args:
|
||||
session_state['mission'] = {
|
||||
'active': True,
|
||||
'goal': args,
|
||||
'step': 1,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': None,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
return {"action": "mission_start", "clean_prompt": args, "overrides": {}}
|
||||
else:
|
||||
return {"action": "state_update", "message": "Usage: /mission <objective_description>"}
|
||||
|
||||
elif cmd in ("/cancel", "/abort"):
|
||||
if session_state.get('mission', {}).get('active'):
|
||||
session_state['mission']['active'] = False
|
||||
return {"action": "mission_cancel", "message": "Mission cancelled"}
|
||||
return {"action": "state_update", "message": "No active mission to cancel"}
|
||||
|
||||
# Unknown command, execute normally
|
||||
return {"action": "execute", "clean_prompt": text, "overrides": {}}</code></pre>
|
||||
</details>
|
||||
@@ -933,7 +973,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.base API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -152,7 +152,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.config_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -319,7 +319,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.context_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -370,7 +370,7 @@ def current_context(self) -> str:
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.exceptions API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -268,7 +268,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.execution_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -340,7 +340,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.import_export_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -361,7 +361,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -318,6 +318,26 @@ el.replaceWith(d);
|
||||
else:
|
||||
return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}}
|
||||
|
||||
elif cmd == "/mission":
|
||||
if args:
|
||||
session_state['mission'] = {
|
||||
'active': True,
|
||||
'goal': args,
|
||||
'step': 1,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': None,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
return {"action": "mission_start", "clean_prompt": args, "overrides": {}}
|
||||
else:
|
||||
return {"action": "state_update", "message": "Usage: /mission <objective_description>"}
|
||||
|
||||
elif cmd in ("/cancel", "/abort"):
|
||||
if session_state.get('mission', {}).get('active'):
|
||||
session_state['mission']['active'] = False
|
||||
return {"action": "mission_cancel", "message": "Mission cancelled"}
|
||||
return {"action": "state_update", "message": "No active mission to cancel"}
|
||||
|
||||
# Unknown command, execute normally
|
||||
return {"action": "execute", "clean_prompt": text, "overrides": {}}
|
||||
|
||||
@@ -936,6 +956,26 @@ el.replaceWith(d);
|
||||
else:
|
||||
return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}}
|
||||
|
||||
elif cmd == "/mission":
|
||||
if args:
|
||||
session_state['mission'] = {
|
||||
'active': True,
|
||||
'goal': args,
|
||||
'step': 1,
|
||||
'max_steps': 10,
|
||||
'start_block_idx': None,
|
||||
'scratchpad_notes': []
|
||||
}
|
||||
return {"action": "mission_start", "clean_prompt": args, "overrides": {}}
|
||||
else:
|
||||
return {"action": "state_update", "message": "Usage: /mission <objective_description>"}
|
||||
|
||||
elif cmd in ("/cancel", "/abort"):
|
||||
if session_state.get('mission', {}).get('active'):
|
||||
session_state['mission']['active'] = False
|
||||
return {"action": "mission_cancel", "message": "Mission cancelled"}
|
||||
return {"action": "state_update", "message": "No active mission to cancel"}
|
||||
|
||||
# Unknown command, execute normally
|
||||
return {"action": "execute", "clean_prompt": text, "overrides": {}}</code></pre>
|
||||
</details>
|
||||
@@ -5858,7 +5898,7 @@ Mode B: config_path set -> Reuses existing directory after validating its str
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.node_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -790,7 +790,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.plugin_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -838,7 +838,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.profile_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -429,7 +429,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.provider API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -351,7 +351,7 @@ def users(self):
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.sync_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -978,7 +978,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.system_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -325,7 +325,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.services.user_service API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -894,7 +894,7 @@ Mode B: config_path set -> Reuses existing directory after validating its str
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.tunnels API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -549,7 +549,7 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.</p></di
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="generator" content="pdoc3 0.11.5">
|
||||
<meta name="generator" content="pdoc3 0.11.6">
|
||||
<title>connpy.utils API documentation</title>
|
||||
<meta name="description" content="">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||
@@ -147,7 +147,7 @@ el.replaceWith(d);
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user