pari-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. pari/__init__.py +3 -0
  2. pari/agent/__init__.py +14 -0
  3. pari/agent/loop.py +35 -0
  4. pari/agent/reflection.py +39 -0
  5. pari/agent/runtime.py +91 -0
  6. pari/agent/subagent.py +34 -0
  7. pari/cli.py +132 -0
  8. pari/config.py +23 -0
  9. pari/context/__init__.py +15 -0
  10. pari/context/cache.py +15 -0
  11. pari/context/chunks.py +40 -0
  12. pari/context/compaction.py +21 -0
  13. pari/context/epoch.py +32 -0
  14. pari/context/warmer.py +32 -0
  15. pari/edit/__init__.py +20 -0
  16. pari/edit/cascade.py +22 -0
  17. pari/edit/diff.py +8 -0
  18. pari/edit/dmp.py +15 -0
  19. pari/edit/exact.py +37 -0
  20. pari/edit/git_osr.py +14 -0
  21. pari/edit/indenter.py +12 -0
  22. pari/eval/__init__.py +14 -0
  23. pari/eval/judge.py +21 -0
  24. pari/eval/logging.py +19 -0
  25. pari/eval/metrics.py +22 -0
  26. pari/eval/runner.py +34 -0
  27. pari/fs/__init__.py +16 -0
  28. pari/fs/atomic.py +53 -0
  29. pari/fs/path.py +28 -0
  30. pari/fs/queue.py +22 -0
  31. pari/intelligence/__init__.py +15 -0
  32. pari/intelligence/ast.py +25 -0
  33. pari/intelligence/cache.py +21 -0
  34. pari/intelligence/ppr.py +21 -0
  35. pari/intelligence/repomap.py +21 -0
  36. pari/intelligence/search.py +33 -0
  37. pari/output/__init__.py +14 -0
  38. pari/output/parser.py +14 -0
  39. pari/output/spooler.py +33 -0
  40. pari/output/truncation.py +20 -0
  41. pari/providers/__init__.py +24 -0
  42. pari/providers/anthropic.py +23 -0
  43. pari/providers/base.py +56 -0
  44. pari/providers/deepseek.py +24 -0
  45. pari/providers/mock.py +46 -0
  46. pari/providers/ollama.py +25 -0
  47. pari/providers/openai.py +23 -0
  48. pari/routing/__init__.py +16 -0
  49. pari/routing/anti_verbosity.py +13 -0
  50. pari/routing/classifier.py +24 -0
  51. pari/routing/router.py +23 -0
  52. pari/sandbox/__init__.py +16 -0
  53. pari/sandbox/base.py +74 -0
  54. pari/sandbox/bwrap.py +39 -0
  55. pari/sandbox/dispatcher.py +31 -0
  56. pari/sandbox/e2b.py +32 -0
  57. pari/sandbox/process.py +38 -0
  58. pari/sandbox/seatbelt.py +32 -0
  59. pari/session/__init__.py +15 -0
  60. pari/session/checkpoint.py +24 -0
  61. pari/session/dag.py +34 -0
  62. pari/session/events.py +50 -0
  63. pari/session/logger.py +41 -0
  64. pari/session/undo.py +99 -0
  65. pari/tools/__init__.py +14 -0
  66. pari/tools/base.py +35 -0
  67. pari/tools/bash.py +24 -0
  68. pari/tools/fs.py +53 -0
  69. pari/tools/registry.py +36 -0
  70. pari/tools/search.py +38 -0
  71. pari/tui/__init__.py +28 -0
  72. pari/tui/app.py +355 -0
  73. pari/tui/commands.py +45 -0
  74. pari/tui/diff_view.py +17 -0
  75. pari/tui/status.py +65 -0
  76. pari_cli-0.1.0.dist-info/METADATA +167 -0
  77. pari_cli-0.1.0.dist-info/RECORD +81 -0
  78. pari_cli-0.1.0.dist-info/WHEEL +5 -0
  79. pari_cli-0.1.0.dist-info/entry_points.txt +2 -0
  80. pari_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  81. pari_cli-0.1.0.dist-info/top_level.txt +1 -0
pari/tools/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """PARI Standard Tool System.
2
+
3
+ Bridges model tool definitions to sandbox-contained, concurrency-safe execution layers.
4
+
5
+ Available Tools:
6
+ - bash: Execute shell commands inside the Layer 0 sandbox with output spooling
7
+ - read_file: Safe workspace-relative file reading
8
+ - edit_file: Atomic, Stop-on-Ambiguity search and replace
9
+ - write_file: Atomic file creation / overwrite
10
+ - find_files: Workspace file globbing respecting ignore boundaries
11
+ - grep_search: Substring and regex text search across repository files
12
+ """
13
+
14
+ # Placeholder imports will be exported once implementations are added
pari/tools/base.py ADDED
@@ -0,0 +1,35 @@
1
+ """PARI Tools: Base Interfaces and JSON Schema Definitions.
2
+
3
+ Converts Python tool implementations into standard OpenAI/Anthropic compatible
4
+ tool call schemas.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass
9
+ from typing import Any, Dict
10
+
11
+
12
+ @dataclass
13
+ class ToolDefinition:
14
+ name: str
15
+ description: str
16
+ parameters: Dict[str, Any]
17
+
18
+ def to_schema(self) -> Dict[str, Any]:
19
+ """Format definition as standard tool calling schema."""
20
+ raise NotImplementedError
21
+
22
+
23
+ class Tool(ABC):
24
+ """Abstract base class for all agent-callable tools."""
25
+
26
+ @property
27
+ @abstractmethod
28
+ def definition(self) -> ToolDefinition:
29
+ """Tool metadata and argument JSON schema."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ async def execute(self, **kwargs: Any) -> Any:
34
+ """Execute tool action safely."""
35
+ pass
pari/tools/bash.py ADDED
@@ -0,0 +1,24 @@
1
+ """PARI Tools: Sandboxed Bash Execution.
2
+
3
+ Routes command execution through Layer 0 sandbox (Bubblewrap / Seatbelt / Process Group)
4
+ and spools large outputs through Layer 7 ManagedOutputSpooler.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Dict
9
+ from pari.tools.base import Tool, ToolDefinition
10
+
11
+
12
+ class BashTool(Tool):
13
+ """Executes a shell command inside the kernel sandbox."""
14
+
15
+ def __init__(self, workspace: Path):
16
+ self.workspace = workspace
17
+
18
+ @property
19
+ def definition(self) -> ToolDefinition:
20
+ raise NotImplementedError
21
+
22
+ async def execute(self, command: str, timeout: float = 30.0) -> Dict[str, Any]:
23
+ """Execute command inside sandbox with output spooling."""
24
+ raise NotImplementedError
pari/tools/fs.py ADDED
@@ -0,0 +1,53 @@
1
+ """PARI Tools: Filesystem and Editing Tools.
2
+
3
+ Integrates Layer 6 safe path resolution and atomic writes with Layer 5 edit cascade.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Any, Dict
8
+ from pari.tools.base import Tool, ToolDefinition
9
+
10
+
11
+ class ReadFileTool(Tool):
12
+ """Safely reads file contents within workspace boundary."""
13
+
14
+ def __init__(self, workspace: Path):
15
+ self.workspace = workspace
16
+
17
+ @property
18
+ def definition(self) -> ToolDefinition:
19
+ raise NotImplementedError
20
+
21
+ async def execute(self, path: str) -> str:
22
+ """Read target file."""
23
+ raise NotImplementedError
24
+
25
+
26
+ class WriteFileTool(Tool):
27
+ """Safely and atomically writes or creates a file."""
28
+
29
+ def __init__(self, workspace: Path):
30
+ self.workspace = workspace
31
+
32
+ @property
33
+ def definition(self) -> ToolDefinition:
34
+ raise NotImplementedError
35
+
36
+ async def execute(self, path: str, content: str) -> str:
37
+ """Atomically write file."""
38
+ raise NotImplementedError
39
+
40
+
41
+ class EditFileTool(Tool):
42
+ """Safely edits an existing file using multi-tier fuzzy search/replace cascade."""
43
+
44
+ def __init__(self, workspace: Path):
45
+ self.workspace = workspace
46
+
47
+ @property
48
+ def definition(self) -> ToolDefinition:
49
+ raise NotImplementedError
50
+
51
+ async def execute(self, path: str, search: str, replace: str) -> Dict[str, Any]:
52
+ """Apply edit using cascade."""
53
+ raise NotImplementedError
pari/tools/registry.py ADDED
@@ -0,0 +1,36 @@
1
+ """PARI Tools: Central Registry and Dispatcher.
2
+
3
+ Manages tool registration, schema exportation for model prompts, and dispatch execution.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+ from pari.tools.base import Tool
9
+
10
+
11
+ class ToolRegistry:
12
+ """Registry maintaining active agent tools."""
13
+
14
+ def __init__(self):
15
+ self._tools: Dict[str, Tool] = {}
16
+
17
+ def register(self, tool: Tool):
18
+ """Register a new tool instance."""
19
+ raise NotImplementedError
20
+
21
+ def get(self, name: str) -> Optional[Tool]:
22
+ """Look up tool by name."""
23
+ raise NotImplementedError
24
+
25
+ def get_schemas(self) -> List[Dict[str, Any]]:
26
+ """Return list of JSON schemas for all registered tools."""
27
+ raise NotImplementedError
28
+
29
+ async def execute(self, name: str, arguments: Dict[str, Any]) -> Any:
30
+ """Dispatch tool execution by name with given arguments."""
31
+ raise NotImplementedError
32
+
33
+
34
+ def get_default_registry(workspace: Path) -> ToolRegistry:
35
+ """Instantiate and register the standard PARI tool suite for a given workspace."""
36
+ raise NotImplementedError
pari/tools/search.py ADDED
@@ -0,0 +1,38 @@
1
+ """PARI Tools: Codebase Search and Discovery Tools.
2
+
3
+ Routes globbing and text grep queries through Layer 3 intelligence search.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List
8
+ from pari.tools.base import Tool, ToolDefinition
9
+
10
+
11
+ class FindFilesTool(Tool):
12
+ """Finds files matching glob patterns while respecting ignore rules."""
13
+
14
+ def __init__(self, workspace: Path):
15
+ self.workspace = workspace
16
+
17
+ @property
18
+ def definition(self) -> ToolDefinition:
19
+ raise NotImplementedError
20
+
21
+ async def execute(self, pattern: str = "*", max_results: int = 50) -> List[str]:
22
+ """Find files matching pattern."""
23
+ raise NotImplementedError
24
+
25
+
26
+ class GrepTool(Tool):
27
+ """Searches workspace files for regex or substring pattern matches."""
28
+
29
+ def __init__(self, workspace: Path):
30
+ self.workspace = workspace
31
+
32
+ @property
33
+ def definition(self) -> ToolDefinition:
34
+ raise NotImplementedError
35
+
36
+ async def execute(self, query: str, max_matches: int = 50) -> List[Dict[str, Any]]:
37
+ """Search file contents for pattern."""
38
+ raise NotImplementedError
pari/tui/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """PARI Layer 9: Terminal Interface & Differential TUI.
2
+
3
+ Architectural Purpose:
4
+ Harvested from Pi's differential TUI rendering and Aider's prompt_toolkit interface.
5
+ Closes Cursor F4 (background CPU pegging vector indexer).
6
+ Provides responsive streaming, syntax-highlighted diff review, token cost meter,
7
+ and inline slash commands without heavy Electron overhead.
8
+
9
+ Subsystems:
10
+ - app: Interactive terminal session loop
11
+ - diff_view: Syntax-highlighted unified diff preview & approval prompt
12
+ - status: Token tracker & USD cost meter
13
+ - commands: Interactive slash commands (/undo, /diff, /model, /help)
14
+ """
15
+
16
+ from pari.tui.app import TerminalApp
17
+ from pari.tui.commands import SlashCommandHandler
18
+ from pari.tui.diff_view import display_diff
19
+ from pari.tui.status import CostMeter, TokenUsage
20
+
21
+ __all__ = [
22
+ "TerminalApp",
23
+ "SlashCommandHandler",
24
+ "display_diff",
25
+ "CostMeter",
26
+ "TokenUsage",
27
+ ]
28
+
pari/tui/app.py ADDED
@@ -0,0 +1,355 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ import shutil
4
+ from typing import Optional
5
+
6
+ from collections.abc import Iterable
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.application.current import get_app
9
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
10
+ from prompt_toolkit.document import Document
11
+ from prompt_toolkit.filters import has_completions
12
+ from prompt_toolkit.formatted_text import HTML
13
+ from prompt_toolkit.history import InMemoryHistory
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.shortcuts import CompleteStyle
16
+ from prompt_toolkit.styles import Style
17
+ from rich.console import Console
18
+ from rich.markdown import Markdown
19
+ from rich.panel import Panel
20
+
21
+ from pari.tui.commands import SlashCommandHandler
22
+ from pari.tui.diff_view import display_diff
23
+
24
+ # Slash command metadata definitions
25
+ COMMAND_METADATA = [
26
+ ("/help", "Show available slash commands and help"),
27
+ ("/diff", "Preview pending or latest unified diffs"),
28
+ ("/undo", "Revert the most recent file change"),
29
+ ("/clear", "Clear terminal screen"),
30
+ ("/model", "Inspect or switch active LLM"),
31
+ ("/add", "Pin reference file or glob into context"),
32
+ ("/exit", "End interactive session"),
33
+ ("/quit", "End interactive session"),
34
+ ]
35
+
36
+
37
+ class SlashCommandCompleter(Completer):
38
+ """Autocompleter for slash commands with formatted descriptions and clean prefix matching."""
39
+
40
+ def __init__(self, commands: list[tuple[str, str]] | None = None):
41
+ self.commands = commands or COMMAND_METADATA
42
+
43
+ def get_completions(
44
+ self, document: Document, complete_event: CompleteEvent
45
+ ) -> Iterable[Completion]:
46
+ text = document.text_before_cursor.lstrip()
47
+
48
+ # If user has entered arguments (e.g. "/model gpt-4o "), don't complete slash commands
49
+ if " " in text:
50
+ return
51
+
52
+ # Trigger on '/' or when explicit completion is requested (e.g. Tab on empty line)
53
+ if not text.startswith("/") and not (complete_event.completion_requested and not text):
54
+ return
55
+
56
+ query = text.lower()
57
+ for cmd, desc in self.commands:
58
+ if cmd.lower().startswith(query):
59
+ yield Completion(
60
+ text=cmd,
61
+ start_position=-len(text),
62
+ display=cmd,
63
+ display_meta=desc,
64
+ )
65
+
66
+
67
+ # Modern CLI theme matching the completion dropdown menu & toolbar styling
68
+ CLI_STYLE = Style.from_dict({
69
+ # Prompt & input
70
+ "prompt": "fg:#38bdf8 bold",
71
+ "": "fg:#f3f4f6",
72
+ "placeholder": "fg:#6b7280 italic",
73
+
74
+ # Completion Menu dropdown
75
+ "completion-menu": "bg:#18181b fg:#f4f4f5",
76
+ "completion-menu.completion": "bg:#18181b fg:#38bdf8",
77
+ "completion-menu.completion.current": "bg:#27272a fg:#38bdf8 bold",
78
+ "completion-menu.meta.completion": "bg:#18181b fg:#717882",
79
+ "completion-menu.meta.completion.current": "bg:#27272a fg:#e4e4e7",
80
+ "completion-menu.multi-column-meta": "bg:#18181b fg:#717882",
81
+
82
+ # Scrollbar
83
+ "scrollbar.background": "bg:#18181b",
84
+ "scrollbar.button": "bg:#3f3f46",
85
+ "scrollbar.arrow": "bg:#18181b fg:#717882",
86
+
87
+ # Bottom toolbar & navigation hint keys
88
+ "bottom-toolbar": "bg:default fg:#717882",
89
+ "bottom-toolbar.text": "bg:default fg:#717882",
90
+ "toolbar-key": "fg:#38bdf8 bold",
91
+ "toolbar-key-esc": "fg:#717882",
92
+ "toolbar-action": "fg:#717882",
93
+ "toolbar-dot": "fg:#4b5563",
94
+ })
95
+
96
+
97
+ class TerminalApp:
98
+ """Manages developer interactions in the terminal REPL."""
99
+
100
+ def __init__(self, workspace: Path, model_name: str = "pari-core"):
101
+ self.workspace = workspace.resolve()
102
+ self.model_name = model_name
103
+ self.console = Console()
104
+ # TODO:
105
+ # self.runtime = runtime or AgentRuntime(self.workspace)
106
+ # self.cost_meter = CostMeter(model_family=self.model_name) # later todo
107
+ self.commands = SlashCommandHandler()
108
+ self._register_commands()
109
+
110
+ self.completer = SlashCommandCompleter()
111
+ self.key_bindings = self._create_key_bindings()
112
+ self.session: PromptSession = PromptSession(
113
+ history=InMemoryHistory(),
114
+ completer=self.completer,
115
+ style=CLI_STYLE,
116
+ complete_while_typing=True,
117
+ complete_style=CompleteStyle.COLUMN,
118
+ reserve_space_for_menu=10,
119
+ key_bindings=self.key_bindings,
120
+ )
121
+
122
+ def _create_key_bindings(self) -> KeyBindings:
123
+ """Keybindings for autocomplete navigation: enter select, tab complete, esc cancel."""
124
+ kb = KeyBindings()
125
+
126
+ @kb.add("enter", filter=has_completions)
127
+ def _handle_enter(event):
128
+ buff = event.current_buffer
129
+ if buff.complete_state and buff.complete_state.current_completion:
130
+ buff.apply_completion(buff.complete_state.current_completion)
131
+ else:
132
+ buff.validate_and_handle()
133
+
134
+ @kb.add("tab", filter=has_completions)
135
+ def _handle_tab(event):
136
+ buff = event.current_buffer
137
+ if buff.complete_state:
138
+ buff.complete_next()
139
+
140
+ @kb.add("s-tab", filter=has_completions)
141
+ def _handle_s_tab(event):
142
+ buff = event.current_buffer
143
+ if buff.complete_state:
144
+ buff.complete_previous()
145
+
146
+ @kb.add("down", filter=has_completions)
147
+ def _handle_down(event):
148
+ event.current_buffer.complete_next()
149
+
150
+ @kb.add("up", filter=has_completions)
151
+ def _handle_up(event):
152
+ event.current_buffer.complete_previous()
153
+
154
+ @kb.add("escape", filter=has_completions)
155
+ def _handle_escape(event):
156
+ buff = event.current_buffer
157
+ buff.cancel_completion()
158
+
159
+ return kb
160
+
161
+ def _register_commands(self) -> None:
162
+ """Register built-in slash commands."""
163
+ self.commands.register("/help", self._cmd_help)
164
+ self.commands.register("/diff", self._cmd_diff)
165
+ self.commands.register("/undo", self._cmd_undo)
166
+ self.commands.register("/model", self._cmd_model)
167
+ self.commands.register("/add", self._cmd_add)
168
+
169
+ def _cmd_help(self, args: str) -> str:
170
+ return (
171
+ "**Available Slash Commands:**\n\n"
172
+ "- `/diff` - Preview pending or latest unified diffs\n"
173
+ "- `/undo` - Revert the most recent file change\n"
174
+ "- `/model [name]` - Inspect or switch active LLM\n"
175
+ "- `/add [path]` - Pin reference file or glob into context\n"
176
+ "- `/clear` - Clear terminal screen\n"
177
+ "- `/help` - Show this commands overview\n"
178
+ "- `/exit` or `/quit` - End interactive session"
179
+ )
180
+
181
+ def _cmd_diff(self, args: str) -> str:
182
+ # Display sample/active unified diff preview
183
+ sample_diff = (
184
+ "--- a/src/example.py\n"
185
+ "+++ b/src/example.py\n"
186
+ "@@ -1,3 +1,3 @@\n"
187
+ "-def hello():\n"
188
+ "- return 'world'\n"
189
+ "+def hello() -> str:\n"
190
+ "+ return 'hello world from PARI!'"
191
+ )
192
+ display_diff(sample_diff, filename="src/example.py", console=self.console)
193
+ return "Inspecting diff preview for `src/example.py`."
194
+
195
+ def _cmd_undo(self, args: str) -> str:
196
+ # TODO: res = self.runtime.undo_manager.undo()
197
+ if hasattr(self, "runtime") and hasattr(self.runtime, "undo_manager"):
198
+ res = self.runtime.undo_manager.undo()
199
+ if res.reverted_files:
200
+ return f"Reverted files: {', '.join(res.reverted_files)}"
201
+ return "No previous file modifications in session history to undo."
202
+
203
+ def _cmd_model(self, args: str) -> str:
204
+ if args:
205
+ self.model_name = args.strip()
206
+ # TODO: self.cost_meter.model_family = self.model_name.lower()
207
+ if hasattr(self, "cost_meter"):
208
+ self.cost_meter.model_family = self.model_name.lower()
209
+ return f"Switched active model to: `{self.model_name}`"
210
+ return f"Active model: `{self.model_name}` (Tiers: claude-3-7-sonnet, claude-3-5-sonnet, gpt-4o, pari-core)"
211
+
212
+ def _cmd_add(self, args: str) -> str:
213
+ if not args:
214
+ return "Usage: `/add <file_or_glob>` (e.g. `/add pyproject.toml`)"
215
+ # TODO: self.runtime.add_pinned_file(args.strip())
216
+ if hasattr(self, "runtime"):
217
+ added = self.runtime.add_pinned_file(args.strip())
218
+ if added:
219
+ return f"Added {args.strip()} to active context."
220
+ target = (self.workspace / args.strip()).resolve()
221
+ if not target.exists():
222
+ return f"File not found: `{args}`"
223
+ try:
224
+ rel = target.relative_to(self.workspace)
225
+ except ValueError:
226
+ rel = target
227
+ return f"Added {rel} to active context."
228
+
229
+
230
+
231
+ def print_welcome(self):
232
+ """Display startup banner."""
233
+ welcome_text = (
234
+ f"[bold cyan]PARI v0.1.0[/bold cyan] — Coding Agent\n"
235
+ f"[dim]Workspace :[/dim] [yellow]{self.workspace}[/yellow]\n"
236
+ f"[dim]Model :[/dim] [green]{self.model_name}[/green]\n\n"
237
+ f"[dim]Type your message, [/dim][bold cyan]/help[/bold cyan][dim] for commands, [/dim][bold cyan]/exit[/bold cyan][dim] to quit.[/dim]"
238
+ )
239
+ self.console.print(
240
+ Panel(
241
+ welcome_text,
242
+ title="[bold green]PARI[/bold green]",
243
+ border_style="dim cyan",
244
+ padding=(1, 2),
245
+ )
246
+ )
247
+
248
+ def get_prompt(self):
249
+ """Clean prompt indicator matching modern CLI design."""
250
+ return [
251
+ ("class:prompt", "> "),
252
+ ]
253
+
254
+ def get_placeholder(self):
255
+ """Placeholder text when input line is empty."""
256
+ return [("class:placeholder", "Ask PARI to do anything...")]
257
+
258
+ def get_bottom_toolbar(self):
259
+ """Dynamic toolbar showing key navigation hints during completion or session status."""
260
+ try:
261
+ app = get_app()
262
+ buff = app.current_buffer
263
+ is_completing = bool(buff.complete_state) or (
264
+ buff.text.strip().startswith("/") and " " not in buff.text.strip()
265
+ )
266
+ except Exception:
267
+ is_completing = False
268
+
269
+ if is_completing:
270
+ return [
271
+ ("class:toolbar-key", "↑/↓"),
272
+ ("class:toolbar-action", " Navigate "),
273
+ ("class:toolbar-dot", "· "),
274
+ ("class:toolbar-key", "enter"),
275
+ ("class:toolbar-action", " Select "),
276
+ ("class:toolbar-dot", "· "),
277
+ ("class:toolbar-key", "tab"),
278
+ ("class:toolbar-action", " Complete\n"),
279
+ ("class:toolbar-key-esc", "esc"),
280
+ ("class:toolbar-action", " to cancel"),
281
+ ]
282
+
283
+ home = Path.home()
284
+ try:
285
+ rel_path = f"~/{self.workspace.relative_to(home)}"
286
+ except ValueError:
287
+ rel_path = str(self.workspace)
288
+
289
+ cost_str = f" · {self.cost_meter.format_status()}" if hasattr(self, "cost_meter") else ""
290
+ status_meta = f" {self.model_name} default · {rel_path}{cost_str}"
291
+ return [
292
+ ("class:bottom-toolbar.text", status_meta),
293
+ ]
294
+
295
+ def process_message(self, user_msg: str) -> str:
296
+ """Process user message and generate a response."""
297
+ # 1. Check slash commands first
298
+ cmd_result = self.commands.handle(user_msg)
299
+ if cmd_result is not None:
300
+ _, output = cmd_result
301
+ return output
302
+
303
+ normalized = user_msg.strip().lower()
304
+
305
+ # 2. Handle greetings like "hi how are you", "hi howr you", etc.
306
+ if "how" in normalized and ("are you" in normalized or "r you" in normalized or "howr" in normalized):
307
+ return "Good! How can I help you today?"
308
+ elif normalized in ("hi", "hello", "hey"):
309
+ return "Hello! How can I assist you with your project?"
310
+
311
+ # 3. Normal turn execution (TODO: self.cost_meter.add_usage)
312
+ if hasattr(self, "cost_meter"):
313
+ self.cost_meter.add_usage(in_tokens=len(user_msg.split()) * 4 + 150, out_tokens=60)
314
+ return f"Received: {user_msg}\n\n*(Agent core is ready for prompt orchestration)*"
315
+
316
+
317
+ async def run_repl(self):
318
+ """Main interactive terminal read-eval-print loop."""
319
+ self.print_welcome()
320
+
321
+ while True:
322
+ try:
323
+ user_input = await self.session.prompt_async(
324
+ message=self.get_prompt,
325
+ placeholder=self.get_placeholder,
326
+ bottom_toolbar=self.get_bottom_toolbar,
327
+ )
328
+
329
+ text = user_input.strip()
330
+ if not text:
331
+ continue
332
+
333
+ # 2. Built-in session commands
334
+ if text in ("/exit", "/quit", "exit", "quit"):
335
+ self.console.print("[dim]Exiting PARI session. Goodbye![/dim]")
336
+ break
337
+
338
+ if text == "/clear":
339
+ self.console.clear()
340
+ self.print_welcome()
341
+ continue
342
+
343
+ # 3. Simulate thinking with Rich status spinner
344
+ with self.console.status("[bold cyan]PARI is thinking...", spinner="dots"):
345
+ await asyncio.sleep(0.4)
346
+ response = self.process_message(text)
347
+
348
+ # 4. Render response via Rich
349
+ self.console.print()
350
+ self.console.print(Panel(Markdown(response), title="[bold cyan]PARI Agent[/bold cyan]", border_style="dim"))
351
+ self.console.print()
352
+
353
+ except (KeyboardInterrupt, EOFError):
354
+ self.console.print("\n[dim]Session interrupted. Goodbye![/dim]")
355
+ break
pari/tui/commands.py ADDED
@@ -0,0 +1,45 @@
1
+ """PARI Layer 9: Interactive Slash Command Dispatcher.
2
+
3
+ Handles in-session slash commands:
4
+ /undo - Revert the most recent turn's file modifications
5
+ /diff - Inspect pending or latest diffs
6
+ /add - Add a specific file into active context
7
+ /model - Inspect or switch active LLM model
8
+ /help - Display available commands
9
+ """
10
+
11
+ from typing import Callable, Dict, Optional, Tuple
12
+
13
+
14
+ class SlashCommandHandler:
15
+ """Dispatches slash commands entered by the developer."""
16
+
17
+ def __init__(self):
18
+ self._handlers: Dict[str, Callable[[str], str]] = {}
19
+
20
+ def register(self, command: str, handler: Callable[[str], str]) -> None:
21
+ """Register custom slash command handler."""
22
+ cmd_name = command.lower().strip()
23
+ if not cmd_name.startswith("/"):
24
+ cmd_name = f"/{cmd_name}"
25
+ self._handlers[cmd_name] = handler
26
+
27
+ def handle(self, input_text: str) -> Optional[Tuple[str, str]]:
28
+ """If input_text starts with '/', parse and execute command.
29
+
30
+ Returns (command_name, result_message) if handled, or None if not a slash command.
31
+ """
32
+ text = input_text.strip()
33
+ if not text.startswith("/"):
34
+ return None
35
+
36
+ parts = text.split(maxsplit=1)
37
+ cmd = parts[0].lower()
38
+ args = parts[1].strip() if len(parts) > 1 else ""
39
+
40
+ if cmd in self._handlers:
41
+ output = self._handlers[cmd](args)
42
+ return (cmd, output)
43
+
44
+ return (cmd, f"Unknown command: '{cmd}'. Type `/help` for a list of commands.")
45
+
pari/tui/diff_view.py ADDED
@@ -0,0 +1,17 @@
1
+ from typing import Optional
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+ from rich.syntax import Syntax
5
+
6
+
7
+ def display_diff(diff_text: str, filename: str = "", console: Optional[Console] = None) -> None:
8
+ """Renders a unified diff in the terminal with colored highlighting."""
9
+ c = console or Console()
10
+ if not diff_text.strip():
11
+ c.print("[dim]No diff changes to display.[/dim]")
12
+ return
13
+
14
+ syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=True, word_wrap=True)
15
+ title = f"[bold cyan]Diff: {filename}[/bold cyan]" if filename else "[bold cyan]Unified Diff Preview[/bold cyan]"
16
+ c.print(Panel(syntax, title=title, border_style="cyan", padding=(0, 1)))
17
+