janito 3.12.1__py3-none-any.whl → 3.12.2__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.
@@ -1,505 +1,505 @@
1
- """
2
- Session management for Janito Chat CLI.
3
- Defines ChatSession and ChatShellState classes.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import types
9
- from rich.console import Console
10
- from rich.rule import Rule
11
- from prompt_toolkit.history import InMemoryHistory
12
- from janito.cli.chat_mode.shell.input_history import UserInputHistory
13
- from prompt_toolkit.formatted_text import HTML
14
- from prompt_toolkit import PromptSession
15
- from janito.cli.chat_mode.toolbar import get_toolbar_func
16
- from prompt_toolkit.enums import EditingMode
17
- from janito.cli.chat_mode.prompt_style import chat_shell_style
18
- from janito.cli.chat_mode.bindings import KeyBindingsFactory
19
- from janito.cli.chat_mode.shell.commands import handle_command
20
- from janito.cli.chat_mode.shell.autocomplete import ShellCommandCompleter
21
- import time
22
-
23
- # Shared prompt/agent factory
24
- from janito.cli.prompt_setup import setup_agent_and_prompt_handler
25
-
26
- import time
27
-
28
-
29
- class ChatShellState:
30
- def __init__(self, mem_history, conversation_history):
31
- self.mem_history = mem_history
32
- self.conversation_history = conversation_history
33
- self.paste_mode = False
34
- self.interactive_mode = True # Default to interactive mode
35
-
36
- self._pid = None
37
- self._stdout_path = None
38
- self._stderr_path = None
39
-
40
- self._status = (
41
- "starting" # Tracks the current status (updated by background thread/UI)
42
- )
43
-
44
- self.last_usage_info = {}
45
- self.last_elapsed = None
46
- self.main_agent = {}
47
- self.mode = None
48
- self.agent = None
49
- self.main_agent = None
50
- self.main_enabled = False
51
- self.no_tools_mode = False
52
-
53
-
54
- class ChatSession:
55
- def __init__(
56
- self,
57
- console,
58
- provider_instance=None,
59
- llm_driver_config=None,
60
- role=None,
61
- args=None,
62
- verbose_tools=False,
63
- verbose_agent=False,
64
- allowed_permissions=None,
65
- ):
66
- self.console = console
67
- self.session_start_time = time.time()
68
- self.user_input_history = UserInputHistory()
69
- self.input_dicts = self.user_input_history.load()
70
- self.mem_history = InMemoryHistory()
71
- for item in self.input_dicts:
72
- if isinstance(item, dict) and "input" in item:
73
- self.mem_history.append_string(item["input"])
74
- self.provider_instance = provider_instance
75
- self.llm_driver_config = llm_driver_config
76
-
77
- profile, role, profile_system_prompt, no_tools_mode = (
78
- self._select_profile_and_role(args, role)
79
- )
80
- # Propagate no_tools_mode flag to downstream components via args
81
- if args is not None and not hasattr(args, "no_tools_mode"):
82
- try:
83
- setattr(args, "no_tools_mode", no_tools_mode)
84
- except Exception:
85
- pass
86
- conversation_history = self._create_conversation_history()
87
- self.agent, self._prompt_handler = self._setup_agent_and_prompt_handler(
88
- args,
89
- provider_instance,
90
- llm_driver_config,
91
- role,
92
- verbose_tools,
93
- verbose_agent,
94
- allowed_permissions,
95
- profile,
96
- profile_system_prompt,
97
- conversation_history,
98
- )
99
- self.profile = profile # Store profile name for welcome message
100
- self.shell_state = ChatShellState(self.mem_history, conversation_history)
101
- self.shell_state.agent = self.agent
102
- # Set no_tools_mode if present
103
- self.shell_state.no_tools_mode = bool(no_tools_mode)
104
- self._filter_execution_tools()
105
- from janito.perf_singleton import performance_collector
106
-
107
- self.performance_collector = performance_collector
108
- self.key_bindings = KeyBindingsFactory.create()
109
- self._prompt_handler.agent = self.agent
110
- self._prompt_handler.conversation_history = (
111
- self.shell_state.conversation_history
112
- )
113
- self._support = False
114
-
115
- # Check if multi-line mode should be enabled by default
116
- self.multi_line_mode = getattr(args, "multi", False) if args else False
117
-
118
- def _select_profile_and_role(self, args, role):
119
- profile, role_arg, python_profile, market_profile = self._extract_args(args)
120
- profile_system_prompt = None
121
- no_tools_mode = False
122
-
123
- profile = self._determine_profile(profile, python_profile, market_profile)
124
-
125
- if (
126
- profile is None
127
- and role_arg is None
128
- and not python_profile
129
- and not market_profile
130
- ):
131
- skip_profile_selection = self._should_skip_profile_selection(args)
132
- else:
133
- skip_profile_selection = False
134
-
135
- if skip_profile_selection:
136
- profile = "Developer with Python Tools" # Default for non-interactive commands
137
- else:
138
- profile = "Developer with Python Tools"
139
-
140
- return profile, role, profile_system_prompt, no_tools_mode
141
-
142
- def _create_conversation_history(self):
143
- from janito.conversation_history import LLMConversationHistory
144
-
145
- return LLMConversationHistory()
146
-
147
- def _setup_agent_and_prompt_handler(
148
- self,
149
- args,
150
- provider_instance,
151
- llm_driver_config,
152
- role,
153
- verbose_tools,
154
- verbose_agent,
155
- allowed_permissions,
156
- profile,
157
- profile_system_prompt,
158
- conversation_history,
159
- ):
160
- return setup_agent_and_prompt_handler(
161
- args=args,
162
- provider_instance=provider_instance,
163
- llm_driver_config=llm_driver_config,
164
- role=role,
165
- verbose_tools=verbose_tools,
166
- verbose_agent=verbose_agent,
167
- allowed_permissions=allowed_permissions,
168
- profile=profile,
169
- profile_system_prompt=profile_system_prompt,
170
- conversation_history=conversation_history,
171
- )
172
-
173
- def _filter_execution_tools(self):
174
- try:
175
- getattr(
176
- __import__("janito.tools", fromlist=["get_local_tools_adapter"]),
177
- "get_local_tools_adapter",
178
- )()
179
- except Exception as e:
180
- self.console.print(
181
- f"[yellow]Warning: Could not filter execution tools at startup: {e}[/yellow]"
182
- )
183
-
184
- _thread = _start_and_watch(self.shell_state, self._lock, get__port())
185
- self._thread = _thread
186
- else:
187
- self.shell_state._support = False
188
- self.shell_state._status = "offline"
189
-
190
- def run(self):
191
- self.console.clear()
192
- from janito import __version__
193
-
194
- self.console.print(f"[bold green]Janito Chat Mode v{__version__}[/bold green]")
195
- self.console.print(f"[dim]Profile: {self.profile}[/dim]")
196
-
197
- import os
198
-
199
- cwd = os.getcwd()
200
- home = os.path.expanduser("~")
201
- if cwd.startswith(home):
202
- cwd_display = "~" + cwd[len(home) :]
203
- else:
204
- cwd_display = cwd
205
- from janito.cli.chat_mode.shell.commands._priv_status import (
206
- get_privilege_status_message,
207
- )
208
-
209
- priv_status = get_privilege_status_message()
210
- self.console.print(
211
- f"[green]Working Dir:[/green] [cyan]{cwd_display}[/cyan] | {priv_status}"
212
- )
213
-
214
- if self.multi_line_mode:
215
- self.console.print(
216
- "[blue]Multi-line input mode enabled (Esc+Enter or Ctrl+D to submit)[/blue]"
217
- )
218
-
219
- from janito.cli.chat_mode.shell.commands._priv_check import (
220
- user_has_any_privileges,
221
- )
222
-
223
- perms = __import__(
224
- "janito.tools.permissions", fromlist=["get_global_allowed_permissions"]
225
- ).get_global_allowed_permissions()
226
- if perms.execute:
227
- self.console.print(
228
- "[bold red]Commands/Code execution is enabled - Be cautious[/bold red]"
229
- )
230
- if not (perms.read or perms.write or perms.execute):
231
- self.console.print(
232
- "[yellow]Note: You currently have no privileges enabled. If you need to interact with files or the system, enable permissions using /read on, /write on, or /execute on.[/yellow]"
233
- )
234
-
235
- session = self._create_prompt_session()
236
- self._chat_loop(session)
237
-
238
- def _chat_loop(self, session):
239
- self.msg_count = 0
240
- timer_started = False
241
- while True:
242
- if not timer_started:
243
- timer_started = True
244
- cmd_input = self._handle_input(session)
245
- if cmd_input is None:
246
- break
247
- if not cmd_input:
248
- continue
249
- if self._handle_exit_conditions(cmd_input):
250
- break
251
- if self._handle_command_input(cmd_input):
252
- continue
253
- self.user_input_history.append(cmd_input)
254
- self._process_prompt(cmd_input)
255
-
256
- def _handle_command_input(self, cmd_input):
257
- if cmd_input.startswith("/"):
258
- handle_command(cmd_input, shell_state=self.shell_state)
259
- return True
260
- if cmd_input.startswith("!"):
261
- handle_command(f"! {cmd_input[1:]}", shell_state=self.shell_state)
262
- return True
263
- return False
264
-
265
- def _process_prompt(self, cmd_input):
266
- try:
267
- # Clear screen before processing new prompt
268
- self.console.clear()
269
- import time
270
-
271
- final_event = (
272
- self._prompt_handler.agent.last_event
273
- if hasattr(self._prompt_handler.agent, "last_event")
274
- else None
275
- )
276
- start_time = time.time()
277
-
278
- model_name, provider_name = self._get_model_info()
279
- backend_hostname = self._get_backend_hostname()
280
-
281
- self.console.print(
282
- Rule(
283
- f"[bold blue]Model: {model_name} ({provider_name}) | Backend: {backend_hostname}[/bold blue]"
284
- )
285
- )
286
-
287
- self._prompt_handler.run_prompt(cmd_input)
288
- end_time = time.time()
289
- elapsed = end_time - start_time
290
- self.msg_count += 1
291
- from janito.formatting_token import print_token_message_summary
292
-
293
- usage = self.performance_collector.get_last_request_usage()
294
- print_token_message_summary(
295
- self.console, self.msg_count, usage, elapsed=elapsed
296
- )
297
- if final_event and hasattr(final_event, "metadata"):
298
- exit_reason = (
299
- final_event.metadata.get("exit_reason")
300
- if hasattr(final_event, "metadata")
301
- else None
302
- )
303
- if exit_reason:
304
- self.console.print(
305
- f"[bold yellow]Exit reason: {exit_reason}[/bold yellow]"
306
- )
307
- except Exception as exc:
308
- self.console.print(f"[red]Exception in agent: {exc}[/red]")
309
- import traceback
310
-
311
- self.console.print(traceback.format_exc())
312
-
313
- def _extract_args(self, args):
314
- """Extract profile and role arguments from args."""
315
- profile = getattr(args, "profile", None) if args is not None else None
316
- role_arg = None
317
- python_profile = (
318
- getattr(args, "developer", False) if args is not None else False
319
- )
320
- market_profile = getattr(args, "market", False) if args is not None else False
321
- return profile, role_arg, python_profile, market_profile
322
-
323
- def _determine_profile(self, profile, python_profile, market_profile):
324
- """Determine the profile based on flags and arguments."""
325
- if python_profile and profile is None:
326
- return "Developer with Python Tools"
327
- if market_profile and profile is None:
328
- return "Market Analyst"
329
- return profile
330
-
331
- def _should_skip_profile_selection(self, args):
332
- """Check if profile selection should be skipped for getter commands."""
333
- from janito.cli.core.getters import GETTER_KEYS
334
-
335
- if args is None:
336
- return False
337
-
338
- for key in GETTER_KEYS:
339
- if getattr(args, key, False):
340
- return True
341
- return False
342
-
343
- def _get_model_info(self):
344
- """Get model and provider information."""
345
- model_name = (
346
- self.agent.get_model_name()
347
- if hasattr(self.agent, "get_model_name")
348
- else "Unknown"
349
- )
350
- provider_name = (
351
- self.agent.get_provider_name()
352
- if hasattr(self.agent, "get_provider_name")
353
- else "Unknown"
354
- )
355
- return model_name, provider_name
356
-
357
- def _get_backend_hostname(self):
358
- """Extract backend hostname from agent configuration."""
359
- candidates = self._collect_base_urls()
360
- return self._parse_hostname_from_urls(candidates)
361
-
362
- def _collect_base_urls(self):
363
- """Collect all possible base URLs from agent configuration."""
364
- candidates = []
365
-
366
- # Collect from driver
367
- drv = getattr(self.agent, "driver", None)
368
- if drv is not None:
369
- cfg = getattr(drv, "config", None)
370
- if cfg is not None:
371
- b = getattr(cfg, "base_url", None)
372
- if b:
373
- candidates.append(b)
374
- direct_base = getattr(drv, "base_url", None)
375
- if direct_base:
376
- candidates.append(direct_base)
377
-
378
- # Collect from agent config
379
- cfg2 = getattr(self.agent, "config", None)
380
- if cfg2 is not None:
381
- b2 = getattr(cfg2, "base_url", None)
382
- if b2:
383
- candidates.append(b2)
384
-
385
- # Collect from agent directly
386
- top_base = getattr(self.agent, "base_url", None)
387
- if top_base:
388
- candidates.append(top_base)
389
-
390
- return candidates
391
-
392
- def _parse_hostname_from_urls(self, candidates):
393
- """Parse hostname from a list of URL candidates."""
394
- from urllib.parse import urlparse
395
-
396
- for candidate in candidates:
397
- try:
398
- if not candidate:
399
- continue
400
- parsed = urlparse(str(candidate))
401
- host = parsed.netloc or parsed.path
402
- if host:
403
- return host
404
- except Exception:
405
- return str(candidate)
406
-
407
- return "Unknown"
408
-
409
- def _create_prompt_session(self):
410
- return PromptSession(
411
- style=chat_shell_style,
412
- completer=ShellCommandCompleter(),
413
- history=self.mem_history,
414
- editing_mode=EditingMode.EMACS,
415
- key_bindings=self.key_bindings,
416
- bottom_toolbar=lambda: get_toolbar_func(
417
- self.performance_collector, 0, self.shell_state
418
- )(),
419
- multiline=self.multi_line_mode,
420
- )
421
-
422
- def _handle_input(self, session):
423
- injected = getattr(self.shell_state, "injected_input", None)
424
- if injected is not None:
425
- cmd_input = injected
426
- self.shell_state.injected_input = None
427
- else:
428
- try:
429
- cmd_input = session.prompt(HTML("<inputline>💬 </inputline>"))
430
- except KeyboardInterrupt:
431
- # Ask for confirmation on Ctrl+C
432
- from prompt_toolkit import prompt
433
-
434
- try:
435
- confirm = prompt(
436
- "Are you sure you want to exit? (y/n): ",
437
- style=self._create_prompt_session().style,
438
- )
439
- if confirm.lower() == "y":
440
- self._handle_exit()
441
- return None
442
- else:
443
- return "" # Return empty string to continue
444
- except (KeyboardInterrupt, EOFError):
445
- # Handle second Ctrl+C or Ctrl+D as immediate exit
446
- self._handle_exit()
447
- return None
448
- except EOFError:
449
- self._handle_exit()
450
- return None
451
- sanitized = cmd_input.strip()
452
- try:
453
- sanitized.encode("utf-8")
454
- except UnicodeEncodeError:
455
- sanitized = sanitized.encode("utf-8", errors="replace").decode("utf-8")
456
- self.console.print(
457
- "[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
458
- )
459
- return sanitized
460
-
461
- def _handle_exit(self):
462
- session_duration = time.time() - self.session_start_time
463
-
464
- # Get total token usage from performance collector
465
- from janito.perf_singleton import performance_collector
466
-
467
- total_tokens = performance_collector.get_token_usage().get("total_tokens", 0)
468
-
469
- # Format session duration
470
- if session_duration < 60:
471
- duration_str = f"{session_duration:.1f}s"
472
- elif session_duration < 3600:
473
- duration_str = f"{session_duration/60:.1f}m"
474
- else:
475
- duration_str = f"{session_duration/3600:.1f}h"
476
-
477
- # Format tokens in k/m/t as appropriate
478
- if total_tokens >= 1_000_000_000:
479
- token_str = f"{total_tokens/1_000_000_000:.1f}t"
480
- elif total_tokens >= 1_000_000:
481
- token_str = f"{total_tokens/1_000_000:.1f}m"
482
- elif total_tokens >= 1_000:
483
- token_str = f"{total_tokens/1_000:.1f}k"
484
- else:
485
- token_str = f"{total_tokens}"
486
-
487
- self.console.print(f"[bold yellow]Session completed![/bold yellow]")
488
- self.console.print(
489
- f"[dim]Session time: {duration_str} | Total tokens: {token_str}[/dim]"
490
- )
491
- self.console.print("[bold yellow]Goodbye![/bold yellow]")
492
-
493
- if hasattr(self, "agent") and hasattr(self.agent, "join_driver"):
494
- if (
495
- hasattr(self.agent, "input_queue")
496
- and self.agent.input_queue is not None
497
- ):
498
- self.agent.input_queue.put(None)
499
- self.agent.join_driver()
500
-
501
- def _handle_exit_conditions(self, cmd_input):
502
- if cmd_input.lower() in ("/exit", ":q", ":quit"):
503
- self._handle_exit()
504
- return True
505
- return False
1
+ """
2
+ Session management for Janito Chat CLI.
3
+ Defines ChatSession and ChatShellState classes.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import types
9
+ from rich.console import Console
10
+ from rich.rule import Rule
11
+ from prompt_toolkit.history import InMemoryHistory
12
+ from janito.cli.chat_mode.shell.input_history import UserInputHistory
13
+ from prompt_toolkit.formatted_text import HTML
14
+ from prompt_toolkit import PromptSession
15
+ from janito.cli.chat_mode.toolbar import get_toolbar_func
16
+ from prompt_toolkit.enums import EditingMode
17
+ from janito.cli.chat_mode.prompt_style import chat_shell_style
18
+ from janito.cli.chat_mode.bindings import KeyBindingsFactory
19
+ from janito.cli.chat_mode.shell.commands import handle_command
20
+ from janito.cli.chat_mode.shell.autocomplete import ShellCommandCompleter
21
+ import time
22
+
23
+ # Shared prompt/agent factory
24
+ from janito.cli.prompt_setup import setup_agent_and_prompt_handler
25
+
26
+ import time
27
+
28
+
29
+ class ChatShellState:
30
+ def __init__(self, mem_history, conversation_history):
31
+ self.mem_history = mem_history
32
+ self.conversation_history = conversation_history
33
+ self.paste_mode = False
34
+ self.interactive_mode = True # Default to interactive mode
35
+
36
+ self._pid = None
37
+ self._stdout_path = None
38
+ self._stderr_path = None
39
+
40
+ self._status = (
41
+ "starting" # Tracks the current status (updated by background thread/UI)
42
+ )
43
+
44
+ self.last_usage_info = {}
45
+ self.last_elapsed = None
46
+ self.main_agent = {}
47
+ self.mode = None
48
+ self.agent = None
49
+ self.main_agent = None
50
+ self.main_enabled = False
51
+ self.no_tools_mode = False
52
+
53
+
54
+ class ChatSession:
55
+ def __init__(
56
+ self,
57
+ console,
58
+ provider_instance=None,
59
+ llm_driver_config=None,
60
+ role=None,
61
+ args=None,
62
+ verbose_tools=False,
63
+ verbose_agent=False,
64
+ allowed_permissions=None,
65
+ ):
66
+ self.console = console
67
+ self.session_start_time = time.time()
68
+ self.user_input_history = UserInputHistory()
69
+ self.input_dicts = self.user_input_history.load()
70
+ self.mem_history = InMemoryHistory()
71
+ for item in self.input_dicts:
72
+ if isinstance(item, dict) and "input" in item:
73
+ self.mem_history.append_string(item["input"])
74
+ self.provider_instance = provider_instance
75
+ self.llm_driver_config = llm_driver_config
76
+
77
+ profile, role, profile_system_prompt, no_tools_mode = (
78
+ self._select_profile_and_role(args, role)
79
+ )
80
+ # Propagate no_tools_mode flag to downstream components via args
81
+ if args is not None and not hasattr(args, "no_tools_mode"):
82
+ try:
83
+ setattr(args, "no_tools_mode", no_tools_mode)
84
+ except Exception:
85
+ pass
86
+ conversation_history = self._create_conversation_history()
87
+ self.agent, self._prompt_handler = self._setup_agent_and_prompt_handler(
88
+ args,
89
+ provider_instance,
90
+ llm_driver_config,
91
+ role,
92
+ verbose_tools,
93
+ verbose_agent,
94
+ allowed_permissions,
95
+ profile,
96
+ profile_system_prompt,
97
+ conversation_history,
98
+ )
99
+ self.profile = profile # Store profile name for welcome message
100
+ self.shell_state = ChatShellState(self.mem_history, conversation_history)
101
+ self.shell_state.agent = self.agent
102
+ # Set no_tools_mode if present
103
+ self.shell_state.no_tools_mode = bool(no_tools_mode)
104
+ self._filter_execution_tools()
105
+ from janito.perf_singleton import performance_collector
106
+
107
+ self.performance_collector = performance_collector
108
+ self.key_bindings = KeyBindingsFactory.create()
109
+ self._prompt_handler.agent = self.agent
110
+ self._prompt_handler.conversation_history = (
111
+ self.shell_state.conversation_history
112
+ )
113
+ self._support = False
114
+
115
+ # Check if multi-line mode should be enabled by default
116
+ self.multi_line_mode = getattr(args, "multi", False) if args else False
117
+
118
+ def _select_profile_and_role(self, args, role):
119
+ profile, role_arg, python_profile, market_profile = self._extract_args(args)
120
+ profile_system_prompt = None
121
+ no_tools_mode = False
122
+
123
+ profile = self._determine_profile(profile, python_profile, market_profile)
124
+
125
+ if (
126
+ profile is None
127
+ and role_arg is None
128
+ and not python_profile
129
+ and not market_profile
130
+ ):
131
+ skip_profile_selection = self._should_skip_profile_selection(args)
132
+ else:
133
+ skip_profile_selection = False
134
+
135
+ if skip_profile_selection:
136
+ profile = "Developer" # Default for non-interactive commands
137
+ else:
138
+ profile = "Developer"
139
+
140
+ return profile, role, profile_system_prompt, no_tools_mode
141
+
142
+ def _create_conversation_history(self):
143
+ from janito.conversation_history import LLMConversationHistory
144
+
145
+ return LLMConversationHistory()
146
+
147
+ def _setup_agent_and_prompt_handler(
148
+ self,
149
+ args,
150
+ provider_instance,
151
+ llm_driver_config,
152
+ role,
153
+ verbose_tools,
154
+ verbose_agent,
155
+ allowed_permissions,
156
+ profile,
157
+ profile_system_prompt,
158
+ conversation_history,
159
+ ):
160
+ return setup_agent_and_prompt_handler(
161
+ args=args,
162
+ provider_instance=provider_instance,
163
+ llm_driver_config=llm_driver_config,
164
+ role=role,
165
+ verbose_tools=verbose_tools,
166
+ verbose_agent=verbose_agent,
167
+ allowed_permissions=allowed_permissions,
168
+ profile=profile,
169
+ profile_system_prompt=profile_system_prompt,
170
+ conversation_history=conversation_history,
171
+ )
172
+
173
+ def _filter_execution_tools(self):
174
+ try:
175
+ getattr(
176
+ __import__("janito.tools", fromlist=["get_local_tools_adapter"]),
177
+ "get_local_tools_adapter",
178
+ )()
179
+ except Exception as e:
180
+ self.console.print(
181
+ f"[yellow]Warning: Could not filter execution tools at startup: {e}[/yellow]"
182
+ )
183
+
184
+ _thread = _start_and_watch(self.shell_state, self._lock, get__port())
185
+ self._thread = _thread
186
+ else:
187
+ self.shell_state._support = False
188
+ self.shell_state._status = "offline"
189
+
190
+ def run(self):
191
+ self.console.clear()
192
+ from janito import __version__
193
+
194
+ self.console.print(f"[bold green]Janito Chat Mode v{__version__}[/bold green]")
195
+ self.console.print(f"[dim]Profile: {self.profile}[/dim]")
196
+
197
+ import os
198
+
199
+ cwd = os.getcwd()
200
+ home = os.path.expanduser("~")
201
+ if cwd.startswith(home):
202
+ cwd_display = "~" + cwd[len(home) :]
203
+ else:
204
+ cwd_display = cwd
205
+ from janito.cli.chat_mode.shell.commands._priv_status import (
206
+ get_privilege_status_message,
207
+ )
208
+
209
+ priv_status = get_privilege_status_message()
210
+ self.console.print(
211
+ f"[green]Working Dir:[/green] [cyan]{cwd_display}[/cyan] | {priv_status}"
212
+ )
213
+
214
+ if self.multi_line_mode:
215
+ self.console.print(
216
+ "[blue]Multi-line input mode enabled (Esc+Enter or Ctrl+D to submit)[/blue]"
217
+ )
218
+
219
+ from janito.cli.chat_mode.shell.commands._priv_check import (
220
+ user_has_any_privileges,
221
+ )
222
+
223
+ perms = __import__(
224
+ "janito.tools.permissions", fromlist=["get_global_allowed_permissions"]
225
+ ).get_global_allowed_permissions()
226
+ if perms.execute:
227
+ self.console.print(
228
+ "[bold red]Commands/Code execution is enabled - Be cautious[/bold red]"
229
+ )
230
+ if not (perms.read or perms.write or perms.execute):
231
+ self.console.print(
232
+ "[yellow]Note: You currently have no privileges enabled. If you need to interact with files or the system, enable permissions using /read on, /write on, or /execute on.[/yellow]"
233
+ )
234
+
235
+ session = self._create_prompt_session()
236
+ self._chat_loop(session)
237
+
238
+ def _chat_loop(self, session):
239
+ self.msg_count = 0
240
+ timer_started = False
241
+ while True:
242
+ if not timer_started:
243
+ timer_started = True
244
+ cmd_input = self._handle_input(session)
245
+ if cmd_input is None:
246
+ break
247
+ if not cmd_input:
248
+ continue
249
+ if self._handle_exit_conditions(cmd_input):
250
+ break
251
+ if self._handle_command_input(cmd_input):
252
+ continue
253
+ self.user_input_history.append(cmd_input)
254
+ self._process_prompt(cmd_input)
255
+
256
+ def _handle_command_input(self, cmd_input):
257
+ if cmd_input.startswith("/"):
258
+ handle_command(cmd_input, shell_state=self.shell_state)
259
+ return True
260
+ if cmd_input.startswith("!"):
261
+ handle_command(f"! {cmd_input[1:]}", shell_state=self.shell_state)
262
+ return True
263
+ return False
264
+
265
+ def _process_prompt(self, cmd_input):
266
+ try:
267
+ # Clear screen before processing new prompt
268
+ self.console.clear()
269
+ import time
270
+
271
+ final_event = (
272
+ self._prompt_handler.agent.last_event
273
+ if hasattr(self._prompt_handler.agent, "last_event")
274
+ else None
275
+ )
276
+ start_time = time.time()
277
+
278
+ model_name, provider_name = self._get_model_info()
279
+ backend_hostname = self._get_backend_hostname()
280
+
281
+ self.console.print(
282
+ Rule(
283
+ f"[bold blue]Model: {model_name} ({provider_name}) | Backend: {backend_hostname}[/bold blue]"
284
+ )
285
+ )
286
+
287
+ self._prompt_handler.run_prompt(cmd_input)
288
+ end_time = time.time()
289
+ elapsed = end_time - start_time
290
+ self.msg_count += 1
291
+ from janito.formatting_token import print_token_message_summary
292
+
293
+ usage = self.performance_collector.get_last_request_usage()
294
+ print_token_message_summary(
295
+ self.console, self.msg_count, usage, elapsed=elapsed
296
+ )
297
+ if final_event and hasattr(final_event, "metadata"):
298
+ exit_reason = (
299
+ final_event.metadata.get("exit_reason")
300
+ if hasattr(final_event, "metadata")
301
+ else None
302
+ )
303
+ if exit_reason:
304
+ self.console.print(
305
+ f"[bold yellow]Exit reason: {exit_reason}[/bold yellow]"
306
+ )
307
+ except Exception as exc:
308
+ self.console.print(f"[red]Exception in agent: {exc}[/red]")
309
+ import traceback
310
+
311
+ self.console.print(traceback.format_exc())
312
+
313
+ def _extract_args(self, args):
314
+ """Extract profile and role arguments from args."""
315
+ profile = getattr(args, "profile", None) if args is not None else None
316
+ role_arg = None
317
+ python_profile = (
318
+ getattr(args, "developer", False) if args is not None else False
319
+ )
320
+ market_profile = getattr(args, "market", False) if args is not None else False
321
+ return profile, role_arg, python_profile, market_profile
322
+
323
+ def _determine_profile(self, profile, python_profile, market_profile):
324
+ """Determine the profile based on flags and arguments."""
325
+ if python_profile and profile is None:
326
+ return "Developer"
327
+ if market_profile and profile is None:
328
+ return "Market Analyst"
329
+ return profile
330
+
331
+ def _should_skip_profile_selection(self, args):
332
+ """Check if profile selection should be skipped for getter commands."""
333
+ from janito.cli.core.getters import GETTER_KEYS
334
+
335
+ if args is None:
336
+ return False
337
+
338
+ for key in GETTER_KEYS:
339
+ if getattr(args, key, False):
340
+ return True
341
+ return False
342
+
343
+ def _get_model_info(self):
344
+ """Get model and provider information."""
345
+ model_name = (
346
+ self.agent.get_model_name()
347
+ if hasattr(self.agent, "get_model_name")
348
+ else "Unknown"
349
+ )
350
+ provider_name = (
351
+ self.agent.get_provider_name()
352
+ if hasattr(self.agent, "get_provider_name")
353
+ else "Unknown"
354
+ )
355
+ return model_name, provider_name
356
+
357
+ def _get_backend_hostname(self):
358
+ """Extract backend hostname from agent configuration."""
359
+ candidates = self._collect_base_urls()
360
+ return self._parse_hostname_from_urls(candidates)
361
+
362
+ def _collect_base_urls(self):
363
+ """Collect all possible base URLs from agent configuration."""
364
+ candidates = []
365
+
366
+ # Collect from driver
367
+ drv = getattr(self.agent, "driver", None)
368
+ if drv is not None:
369
+ cfg = getattr(drv, "config", None)
370
+ if cfg is not None:
371
+ b = getattr(cfg, "base_url", None)
372
+ if b:
373
+ candidates.append(b)
374
+ direct_base = getattr(drv, "base_url", None)
375
+ if direct_base:
376
+ candidates.append(direct_base)
377
+
378
+ # Collect from agent config
379
+ cfg2 = getattr(self.agent, "config", None)
380
+ if cfg2 is not None:
381
+ b2 = getattr(cfg2, "base_url", None)
382
+ if b2:
383
+ candidates.append(b2)
384
+
385
+ # Collect from agent directly
386
+ top_base = getattr(self.agent, "base_url", None)
387
+ if top_base:
388
+ candidates.append(top_base)
389
+
390
+ return candidates
391
+
392
+ def _parse_hostname_from_urls(self, candidates):
393
+ """Parse hostname from a list of URL candidates."""
394
+ from urllib.parse import urlparse
395
+
396
+ for candidate in candidates:
397
+ try:
398
+ if not candidate:
399
+ continue
400
+ parsed = urlparse(str(candidate))
401
+ host = parsed.netloc or parsed.path
402
+ if host:
403
+ return host
404
+ except Exception:
405
+ return str(candidate)
406
+
407
+ return "Unknown"
408
+
409
+ def _create_prompt_session(self):
410
+ return PromptSession(
411
+ style=chat_shell_style,
412
+ completer=ShellCommandCompleter(),
413
+ history=self.mem_history,
414
+ editing_mode=EditingMode.EMACS,
415
+ key_bindings=self.key_bindings,
416
+ bottom_toolbar=lambda: get_toolbar_func(
417
+ self.performance_collector, 0, self.shell_state
418
+ )(),
419
+ multiline=self.multi_line_mode,
420
+ )
421
+
422
+ def _handle_input(self, session):
423
+ injected = getattr(self.shell_state, "injected_input", None)
424
+ if injected is not None:
425
+ cmd_input = injected
426
+ self.shell_state.injected_input = None
427
+ else:
428
+ try:
429
+ cmd_input = session.prompt(HTML("<inputline>💬 </inputline>"))
430
+ except KeyboardInterrupt:
431
+ # Ask for confirmation on Ctrl+C
432
+ from prompt_toolkit import prompt
433
+
434
+ try:
435
+ confirm = prompt(
436
+ "Are you sure you want to exit? (y/n): ",
437
+ style=self._create_prompt_session().style,
438
+ )
439
+ if confirm.lower() == "y":
440
+ self._handle_exit()
441
+ return None
442
+ else:
443
+ return "" # Return empty string to continue
444
+ except (KeyboardInterrupt, EOFError):
445
+ # Handle second Ctrl+C or Ctrl+D as immediate exit
446
+ self._handle_exit()
447
+ return None
448
+ except EOFError:
449
+ self._handle_exit()
450
+ return None
451
+ sanitized = cmd_input.strip()
452
+ try:
453
+ sanitized.encode("utf-8")
454
+ except UnicodeEncodeError:
455
+ sanitized = sanitized.encode("utf-8", errors="replace").decode("utf-8")
456
+ self.console.print(
457
+ "[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
458
+ )
459
+ return sanitized
460
+
461
+ def _handle_exit(self):
462
+ session_duration = time.time() - self.session_start_time
463
+
464
+ # Get total token usage from performance collector
465
+ from janito.perf_singleton import performance_collector
466
+
467
+ total_tokens = performance_collector.get_token_usage().get("total_tokens", 0)
468
+
469
+ # Format session duration
470
+ if session_duration < 60:
471
+ duration_str = f"{session_duration:.1f}s"
472
+ elif session_duration < 3600:
473
+ duration_str = f"{session_duration/60:.1f}m"
474
+ else:
475
+ duration_str = f"{session_duration/3600:.1f}h"
476
+
477
+ # Format tokens in k/m/t as appropriate
478
+ if total_tokens >= 1_000_000_000:
479
+ token_str = f"{total_tokens/1_000_000_000:.1f}t"
480
+ elif total_tokens >= 1_000_000:
481
+ token_str = f"{total_tokens/1_000_000:.1f}m"
482
+ elif total_tokens >= 1_000:
483
+ token_str = f"{total_tokens/1_000:.1f}k"
484
+ else:
485
+ token_str = f"{total_tokens}"
486
+
487
+ self.console.print(f"[bold yellow]Session completed![/bold yellow]")
488
+ self.console.print(
489
+ f"[dim]Session time: {duration_str} | Total tokens: {token_str}[/dim]"
490
+ )
491
+ self.console.print("[bold yellow]Goodbye![/bold yellow]")
492
+
493
+ if hasattr(self, "agent") and hasattr(self.agent, "join_driver"):
494
+ if (
495
+ hasattr(self.agent, "input_queue")
496
+ and self.agent.input_queue is not None
497
+ ):
498
+ self.agent.input_queue.put(None)
499
+ self.agent.join_driver()
500
+
501
+ def _handle_exit_conditions(self, cmd_input):
502
+ if cmd_input.lower() in ("/exit", ":q", ":quit"):
503
+ self._handle_exit()
504
+ return True
505
+ return False