diff --git a/.gitignore b/.gitignore index 5e47088..8e5686b 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ MULTI_USER_PLAN.md COPILOT_PLAN.md ARCHITECTURAL_DEBT_REFACTOR.md COPILOT_UI_FEATURES.md +MULTI_USER_IMPLEMENTATION_STEPS.md #themes nord.yml diff --git a/connpy/cli/terminal_ui.py b/connpy/cli/terminal_ui.py index f76f485..6ce12d1 100644 --- a/connpy/cli/terminal_ui.py +++ b/connpy/cli/terminal_ui.py @@ -131,9 +131,8 @@ class CopilotInterface: if state['context_mode'] == self.mode_lines: return '\n'.join(buffer.split('\n')[-state['context_lines']:]) idx = max(0, state['total_cmds'] - state['context_cmd']) - start, preview = blocks[idx] - if state['context_mode'] == self.mode_single and idx + 1 < state['total_cmds']: - end = blocks[idx + 1][0] + start, end, preview = blocks[idx] + if state['context_mode'] == self.mode_single: active_raw = raw_bytes[start:end] else: active_raw = raw_bytes[start:] @@ -175,7 +174,7 @@ class CopilotInterface: base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]' else: idx = max(0, state['total_cmds'] - state['context_cmd']) - desc = blocks[idx][1] + desc = blocks[idx][2] base_str = f'\u25b6 {desc} [Tab: {m_label}]' # Wrap base_str in a style to maintain consistency and avoid glitches @@ -302,10 +301,11 @@ class CopilotInterface: # 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" active_buffer = get_active_buffer() live_text = "Thinking..." - panel = Panel(live_text, title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color) + panel = Panel(live_text, title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color) def on_chunk(text): nonlocal live_text @@ -314,7 +314,7 @@ class CopilotInterface: with Live(panel, console=self.console, refresh_per_second=10) as live: def update_live(t): - live.update(Panel(Markdown(t), title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color)) + live.update(Panel(Markdown(t), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color)) wrapped_chunk = lambda t: (on_chunk(t), update_live(live_text)) @@ -334,7 +334,7 @@ class CopilotInterface: # 4. Handle result if live_text == "Thinking..." and result.get("guide"): - self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color)) + self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color)) commands = result.get("commands", []) if not commands: diff --git a/connpy/services/ai_service.py b/connpy/services/ai_service.py index f30e8a1..a6af2cf 100644 --- a/connpy/services/ai_service.py +++ b/connpy/services/ai_service.py @@ -20,6 +20,7 @@ class AIService(BaseService): except Exception: prompt_re = re.compile(re.sub(r'(?= 1: for i in range(1, len(cmd_byte_positions)): pos, known_cmd = cmd_byte_positions[i] @@ -31,7 +32,7 @@ class AIService(BaseService): prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()] prompt_text = prev_lines[-1].strip() if prev_lines else "" preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd - blocks.append((pos, preview[:80])) + parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]}) else: chunk = raw_bytes[prev_pos:pos] cleaned = log_cleaner(chunk.decode(errors='replace')) @@ -43,19 +44,38 @@ class AIService(BaseService): if match: cmd_text = preview[match.end():].strip() if cmd_text: - blocks.append((pos, preview[:80])) + parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]}) + else: + parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""}) + else: + parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""}) + else: + parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""}) - # Always ensure there is a final block representing the current prompt - # Find the start of the last line in the raw buffer to avoid selecting everything - # when no commands have been executed yet. last_newline = raw_bytes.rfind(b'\n') current_prompt_pos = last_newline + 1 if last_newline != -1 else 0 - + current_end = len(raw_bytes) + + for i, item in enumerate(parsed_positions): + if item["type"] == "VALID_CMD": + start_pos = item["pos"] + preview = item["preview"] + + # Find the end position: next VALID_CMD or EMPTY_PROMPT + end_pos = current_prompt_pos + for j in range(i + 1, len(parsed_positions)): + next_item = parsed_positions[j] + if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT"): + end_pos = next_item["pos"] + break + + blocks.append((start_pos, end_pos, preview)) + + # Always ensure there is a final block representing the current prompt if not blocks: - blocks.append((current_prompt_pos, last_line[:80] if last_line else "CURRENT CONTEXT")) + blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT")) elif blocks[-1][0] < current_prompt_pos: - # If the last command block ends before the current prompt, add the prompt block - blocks.append((current_prompt_pos, last_line[:80] if last_line else "CURRENT CONTEXT")) + blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT")) return blocks