janito 1.9.0__py3-none-any.whl → 1.10.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. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -26
  5. janito/agent/conversation.py +163 -122
  6. janito/agent/conversation_api.py +149 -159
  7. janito/agent/{conversation_history.py → llm_conversation_history.py} +18 -1
  8. janito/agent/openai_client.py +38 -23
  9. janito/agent/openai_schema_generator.py +162 -129
  10. janito/agent/platform_discovery.py +134 -77
  11. janito/agent/profile_manager.py +5 -5
  12. janito/agent/rich_message_handler.py +80 -31
  13. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +5 -4
  14. janito/agent/test_openai_schema_generator.py +93 -0
  15. janito/agent/tool_base.py +7 -2
  16. janito/agent/tool_executor.py +54 -49
  17. janito/agent/tool_registry.py +5 -2
  18. janito/agent/tool_use_tracker.py +26 -5
  19. janito/agent/tools/__init__.py +6 -3
  20. janito/agent/tools/create_directory.py +3 -1
  21. janito/agent/tools/create_file.py +7 -1
  22. janito/agent/tools/fetch_url.py +40 -3
  23. janito/agent/tools/find_files.py +3 -1
  24. janito/agent/tools/get_file_outline/core.py +6 -7
  25. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  26. janito/agent/tools/get_lines.py +7 -2
  27. janito/agent/tools/move_file.py +3 -1
  28. janito/agent/tools/present_choices.py +3 -1
  29. janito/agent/tools/python_command_runner.py +150 -0
  30. janito/agent/tools/python_file_runner.py +148 -0
  31. janito/agent/tools/python_stdin_runner.py +154 -0
  32. janito/agent/tools/remove_directory.py +3 -1
  33. janito/agent/tools/remove_file.py +5 -1
  34. janito/agent/tools/replace_file.py +12 -2
  35. janito/agent/tools/replace_text_in_file.py +4 -2
  36. janito/agent/tools/run_bash_command.py +30 -69
  37. janito/agent/tools/run_powershell_command.py +134 -105
  38. janito/agent/tools/search_text.py +172 -122
  39. janito/agent/tools/validate_file_syntax/core.py +3 -1
  40. janito/agent/tools_utils/action_type.py +7 -0
  41. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  42. janito/agent/tools_utils/formatting.py +47 -21
  43. janito/agent/tools_utils/gitignore_utils.py +66 -40
  44. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  45. janito/cli/_print_config.py +63 -61
  46. janito/cli/arg_parser.py +13 -12
  47. janito/cli/cli_main.py +137 -147
  48. janito/cli/main.py +152 -174
  49. janito/cli/one_shot.py +40 -26
  50. janito/i18n/__init__.py +1 -1
  51. janito/rich_utils.py +46 -8
  52. janito/shell/commands/__init__.py +2 -4
  53. janito/shell/commands/conversation_restart.py +3 -1
  54. janito/shell/commands/edit.py +3 -0
  55. janito/shell/commands/history_view.py +3 -3
  56. janito/shell/commands/lang.py +3 -0
  57. janito/shell/commands/livelogs.py +5 -3
  58. janito/shell/commands/prompt.py +6 -0
  59. janito/shell/commands/session.py +3 -0
  60. janito/shell/commands/session_control.py +3 -0
  61. janito/shell/commands/termweb_log.py +8 -0
  62. janito/shell/commands/tools.py +3 -0
  63. janito/shell/commands/track.py +36 -0
  64. janito/shell/commands/utility.py +13 -18
  65. janito/shell/commands/verbose.py +3 -4
  66. janito/shell/input_history.py +62 -0
  67. janito/shell/main.py +117 -181
  68. janito/shell/session/manager.py +0 -21
  69. janito/shell/ui/interactive.py +0 -2
  70. janito/termweb/static/editor.css +0 -4
  71. janito/tests/test_rich_utils.py +44 -0
  72. janito/web/app.py +0 -75
  73. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/METADATA +61 -42
  74. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/RECORD +78 -71
  75. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  76. janito/agent/providers.py +0 -77
  77. janito/agent/tools/run_python_command.py +0 -161
  78. janito/shell/commands/sum.py +0 -49
  79. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  80. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  81. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -21,3 +21,6 @@ def handle_tools(console, args=None, shell_state=None):
21
21
  console.print(f"[red]Error loading tools: {e}[/red]")
22
22
  return
23
23
  console.print(table)
24
+
25
+
26
+ handle_tools.help_text = "List available tools"
@@ -0,0 +1,36 @@
1
+ from janito.agent.tool_use_tracker import ToolUseTracker
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+
6
+ def handle_track(console: Console, args=None, shell_state=None):
7
+ tracker = ToolUseTracker.instance()
8
+ history = tracker.get_history()
9
+ if not history:
10
+ console.print("[bold yellow]No tool usage history found.[/bold yellow]")
11
+ return
12
+ table = Table(show_header=True, header_style="bold magenta")
13
+ table.add_column("#", style="dim", width=4)
14
+ table.add_column("Tool")
15
+ table.add_column("Params/Result")
16
+ for idx, entry in enumerate(history, 1):
17
+ tool = entry["tool"]
18
+ params = entry["params"].copy()
19
+ result = entry.get("result", "")
20
+ # For create/replace file, trim content in params only
21
+ if tool in ("create_file", "replace_file") and "content" in params:
22
+ content = params["content"]
23
+ if isinstance(content, str):
24
+ lines = content.splitlines()
25
+ if len(lines) > 3:
26
+ params["content"] = (
27
+ "\n".join(lines[:2])
28
+ + f"\n... (trimmed, {len(lines)} lines total)"
29
+ )
30
+ elif len(content) > 120:
31
+ params["content"] = content[:120] + "... (trimmed)"
32
+ else:
33
+ params["content"] = content
34
+ param_result = f"{params}\n--- Result ---\n{result}" if result else str(params)
35
+ table.add_row(str(idx), tool, param_result)
36
+ console.print(table)
@@ -1,22 +1,11 @@
1
1
  def handle_help(console, **kwargs):
2
- console.print(
3
- """
4
- [bold green]Available commands:[/bold green]
5
- /exit, exit - Exit chat mode
6
- /restart - Start a new conversation
7
- /help - Show this help message
8
- /continue - Restore last saved conversation
9
- /prompt - Show the system prompt
10
- /role - Change the system role
11
- /clear - Clear the terminal screen
12
- /multi - Provide multiline input as next message
13
- /config - Show or set configuration (see: /config show, /config set local|global key=value)
14
- /termweb-logs - Show the last lines of the latest termweb logs
15
- /livelogs - Show live updates from the server log file (default: server.log)
16
- /termweb-status - Show status information about the running termweb server
17
- /verbose [on|off] - Show or set verbose mode for this session
18
- """
19
- )
2
+ from janito.shell.commands import COMMAND_HANDLERS
3
+
4
+ console.print("[bold green]Available commands:[/bold green]")
5
+ for cmd, handler in COMMAND_HANDLERS.items():
6
+ help_text = getattr(handler, "help_text", None)
7
+ if help_text:
8
+ console.print(f" {cmd} - {help_text}")
20
9
 
21
10
 
22
11
  def handle_clear(console, **kwargs):
@@ -25,9 +14,15 @@ def handle_clear(console, **kwargs):
25
14
  os.system("cls" if os.name == "nt" else "clear")
26
15
 
27
16
 
17
+ handle_clear.help_text = "Clear the terminal screen"
18
+
19
+
28
20
  def handle_multi(console, shell_state=None, **kwargs):
29
21
  console.print(
30
22
  "[bold yellow]Multiline mode activated. Provide or write your text and press Esc + Enter to submit.[/bold yellow]"
31
23
  )
32
24
  if shell_state:
33
25
  shell_state.paste_mode = True
26
+
27
+
28
+ handle_multi.help_text = "Provide multiline input as next message"
@@ -2,10 +2,6 @@ from janito.agent.runtime_config import runtime_config
2
2
 
3
3
 
4
4
  def handle_verbose(console, shell_state=None, **kwargs):
5
- """
6
- /verbose [on|off]
7
- Shows or sets verbose mode for the current shell session.
8
- """
9
5
  args = kwargs.get("args", [])
10
6
  verbose = runtime_config.get("verbose", False)
11
7
  if not args:
@@ -27,3 +23,6 @@ def handle_verbose(console, shell_state=None, **kwargs):
27
23
  )
28
24
  else:
29
25
  console.print("[bold red]Usage:[/bold red] /verbose [on|off]")
26
+
27
+
28
+ handle_verbose.help_text = "Show or set verbose mode for this session"
@@ -0,0 +1,62 @@
1
+ import os
2
+ import json
3
+ from datetime import datetime
4
+ from typing import List, Any, Dict
5
+
6
+
7
+ class UserInputHistory:
8
+ """
9
+ Handles loading, saving, and appending of user input history for the shell.
10
+ Each day's history is stored in a line-delimited JSON file (.json.log) under .janito/input_history/.
11
+ Each line is a JSON dict, e.g., {"input": ..., "ts": ...}
12
+ """
13
+
14
+ def __init__(self, history_dir=None):
15
+ self.history_dir = history_dir or os.path.join(".janito", "input_history")
16
+ os.makedirs(self.history_dir, exist_ok=True)
17
+
18
+ def _get_today_file(self):
19
+ today_str = datetime.now().strftime("%y%m%d")
20
+ return os.path.join(self.history_dir, f"{today_str}.json.log")
21
+
22
+ def load(self) -> List[Dict[str, Any]]:
23
+ """Load today's input history as a list of dicts."""
24
+ history_file = self._get_today_file()
25
+ history = []
26
+ try:
27
+ with open(history_file, "r", encoding="utf-8") as f:
28
+ for line in f:
29
+ line = line.strip()
30
+ if not line:
31
+ continue
32
+ try:
33
+ history.append(json.loads(line))
34
+ except json.JSONDecodeError:
35
+ continue
36
+ except FileNotFoundError:
37
+ pass
38
+ return history
39
+
40
+ def sanitize_surrogates(self, s):
41
+ if isinstance(s, str):
42
+ return s.encode("utf-8", errors="replace").decode("utf-8")
43
+ return s
44
+
45
+ def append(self, input_str: str):
46
+ """Append a new input as a JSON dict to today's history file."""
47
+ history_file = self._get_today_file()
48
+ input_str = self.sanitize_surrogates(input_str)
49
+ entry = {"input": input_str, "ts": datetime.now().isoformat()}
50
+ with open(history_file, "a", encoding="utf-8") as f:
51
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
52
+
53
+ def save(self, history_list: List[Any]):
54
+ """Overwrite today's history file with the given list (for compatibility)."""
55
+ history_file = self._get_today_file()
56
+ with open(history_file, "w", encoding="utf-8") as f:
57
+ for item in history_list:
58
+ if isinstance(item, dict):
59
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
60
+ else:
61
+ entry = {"input": str(item), "ts": datetime.now().isoformat()}
62
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
janito/shell/main.py CHANGED
@@ -8,84 +8,22 @@ from janito.shell.prompt.session_setup import (
8
8
  )
9
9
  from janito.shell.commands import handle_command
10
10
  from janito.agent.conversation_exceptions import EmptyResponseError, ProviderError
11
- from janito.agent.conversation_history import ConversationHistory
12
- from janito.agent.tool_use_tracker import ToolUseTracker
11
+ from janito.agent.api_exceptions import ApiError
12
+ from janito.agent.llm_conversation_history import LLMConversationHistory
13
13
  import janito.i18n as i18n
14
14
  from janito.agent.runtime_config import runtime_config
15
15
  from rich.console import Console
16
- from collections import Counter
17
16
  import os
18
17
  from janito.shell.session.manager import get_session_id
19
18
  from prompt_toolkit.formatted_text import HTML
20
19
  import time
21
-
22
-
23
- def chat_start_summary(conversation_history, console, last_usage_info):
24
- def format_tokens(n):
25
- if n is None:
26
- return "-"
27
- if n >= 1_000_000:
28
- return f"{n/1_000_000:.2f}m"
29
- elif n >= 1_000:
30
- return f"{n/1_000:.2f}k"
31
- return str(n)
32
-
33
- num_messages = len(conversation_history)
34
- roles = [m.get("role") for m in conversation_history.get_messages()]
35
- role_counts = {role: roles.count(role) for role in set(roles)}
36
- roles_str = ", ".join(
37
- f"[bold]{role}[/]: {count}" for role, count in role_counts.items()
38
- )
39
- stats_lines = [
40
- f"[cyan]Messages:[/] [bold]{num_messages}[/]",
41
- f"[cyan]Roles:[/] {roles_str}",
42
- ]
43
- # Use last_usage_info for tokens
44
- if last_usage_info:
45
- prompt_tokens = last_usage_info.get("prompt_tokens")
46
- completion_tokens = last_usage_info.get("completion_tokens")
47
- total_tokens = last_usage_info.get("total_tokens")
48
- tokens_parts = []
49
- if prompt_tokens is not None:
50
- tokens_parts.append(f"Prompt: [bold]{format_tokens(prompt_tokens)}[/]")
51
- if completion_tokens is not None:
52
- tokens_parts.append(
53
- f"Completion: [bold]{format_tokens(completion_tokens)}[/]"
54
- )
55
- if total_tokens is not None:
56
- tokens_parts.append(f"Total: [bold]{format_tokens(total_tokens)}[/]")
57
- if tokens_parts:
58
- stats_lines.append(f"[cyan]Tokens:[/] {', '.join(tokens_parts)}")
59
-
60
- # Add global tool usage stats
61
- try:
62
- tool_history = ToolUseTracker().get_history()
63
- if tool_history:
64
- tool_counts = Counter(
65
- entry["tool"] for entry in tool_history if "tool" in entry
66
- )
67
- tools_str = ", ".join(
68
- f"[bold]{tool}[/]: {count}" for tool, count in tool_counts.items()
69
- )
70
- stats_lines.append(f"[cyan]Tools used:[/] {tools_str}")
71
- except Exception:
72
- pass # Fail silently if tracker is unavailable
73
-
74
- # Print all stats in a single line, no panel
75
- # Print stats in a single line, but tokens info on a separate line if present
76
- if len(stats_lines) > 2:
77
- console.print(" | ".join(stats_lines[:2]))
78
- console.print(stats_lines[2])
79
- if len(stats_lines) > 3:
80
- console.print(" | ".join(stats_lines[3:]))
81
- else:
82
- console.print(" | ".join(stats_lines))
20
+ from janito.shell.input_history import UserInputHistory
83
21
 
84
22
 
85
23
  @dataclass
86
24
  class ShellState:
87
25
  mem_history: Any = field(default_factory=InMemoryHistory)
88
- conversation_history: Any = field(default_factory=lambda: ConversationHistory())
26
+ conversation_history: Any = field(default_factory=lambda: LLMConversationHistory())
89
27
  last_usage_info: Dict[str, int] = field(
90
28
  default_factory=lambda: {
91
29
  "prompt_tokens": 0,
@@ -100,44 +38,26 @@ class ShellState:
100
38
  livereload_stderr_path: Optional[str] = None
101
39
  paste_mode: bool = False
102
40
  profile_manager: Optional[Any] = None
41
+ user_input_history: Optional[Any] = None
103
42
 
104
43
 
105
44
  # Track the active prompt session for cleanup
106
45
  active_prompt_session = None
107
46
 
108
47
 
109
- def start_chat_shell(
110
- profile_manager,
111
- continue_session=False,
112
- session_id=None,
113
- max_rounds=100,
114
- termweb_stdout_path=None,
115
- termweb_stderr_path=None,
116
- livereload_stdout_path=None,
117
- livereload_stderr_path=None,
118
- ):
119
-
120
- i18n.set_locale(runtime_config.get("lang", "en"))
121
- global active_prompt_session
122
- agent = profile_manager.agent
123
- message_handler = RichMessageHandler()
124
- console = message_handler.console
125
-
126
- # Print session id at start
48
+ def load_session(shell_state, continue_session, session_id, profile_manager):
127
49
  from janito.shell.session.manager import load_conversation_by_session_id
128
50
 
129
- shell_state = ShellState()
130
- shell_state.profile_manager = profile_manager
131
51
  if continue_session and session_id:
132
52
  try:
133
53
  messages, prompts, usage = load_conversation_by_session_id(session_id)
134
54
  except FileNotFoundError as e:
135
- console.print(f"[bold red]{str(e)}[/bold red]")
136
- return
137
- # Initialize ConversationHistory with loaded messages
138
- shell_state.conversation_history = ConversationHistory(messages)
55
+ shell_state.profile_manager.agent.message_handler.console.print(
56
+ f"[bold red]{str(e)}[/bold red]"
57
+ )
58
+ return False
59
+ shell_state.conversation_history = LLMConversationHistory(messages)
139
60
  conversation_history = shell_state.conversation_history
140
- # Always refresh the system prompt in the loaded history
141
61
  found = False
142
62
  for msg in conversation_history.get_messages():
143
63
  if msg.get("role") == "system":
@@ -148,12 +68,9 @@ def start_chat_shell(
148
68
  conversation_history.set_system_message(
149
69
  profile_manager.system_prompt_template
150
70
  )
151
- # Optionally set prompts/usage if needed
152
71
  shell_state.last_usage_info = usage or {}
153
72
  else:
154
73
  conversation_history = shell_state.conversation_history
155
- # Add system prompt if needed (skip in vanilla mode)
156
-
157
74
  if (
158
75
  profile_manager.system_prompt_template
159
76
  and (
@@ -167,29 +84,18 @@ def start_chat_shell(
167
84
  conversation_history.set_system_message(
168
85
  profile_manager.system_prompt_template
169
86
  )
170
- mem_history = shell_state.mem_history
171
-
172
- def last_usage_info_ref():
173
- return shell_state.last_usage_info
87
+ return True
174
88
 
175
- last_elapsed = shell_state.last_elapsed
176
-
177
- print_welcome_message(console, continue_id=session_id if continue_session else None)
178
-
179
- session = setup_prompt_session(
180
- lambda: conversation_history.get_messages(),
181
- last_usage_info_ref,
182
- last_elapsed,
183
- mem_history,
184
- profile_manager,
185
- agent,
186
- lambda: conversation_history,
187
- )
188
- active_prompt_session = session
189
89
 
90
+ def handle_prompt_loop(
91
+ shell_state, session, profile_manager, agent, max_rounds, session_id
92
+ ):
93
+ global active_prompt_session
94
+ conversation_history = shell_state.conversation_history
95
+ message_handler = RichMessageHandler()
96
+ console = message_handler.console
190
97
  while True:
191
98
  try:
192
-
193
99
  if shell_state.paste_mode:
194
100
  user_input = session.prompt("Multiline> ", multiline=True)
195
101
  was_paste_mode = True
@@ -203,11 +109,10 @@ def start_chat_shell(
203
109
  console.print("\n[bold red]Exiting...[/bold red]")
204
110
  break
205
111
  except KeyboardInterrupt:
206
- console.print() # Move to next line
112
+ console.print()
207
113
  try:
208
114
  confirm = (
209
115
  session.prompt(
210
- # Use <inputline> for full-line blue background, <prompt> for icon only
211
116
  HTML(
212
117
  "<inputline>Do you really want to exit? (y/n): </inputline>"
213
118
  )
@@ -230,10 +135,8 @@ def start_chat_shell(
230
135
  break
231
136
  else:
232
137
  continue
233
-
234
138
  cmd_input = user_input.strip().lower()
235
139
  if not was_paste_mode and (cmd_input.startswith("/") or cmd_input == "exit"):
236
- # Treat both '/exit' and 'exit' as commands
237
140
  result = handle_command(
238
141
  user_input.strip(),
239
142
  console,
@@ -245,77 +148,110 @@ def start_chat_shell(
245
148
  )
246
149
  break
247
150
  continue
248
-
249
151
  if not user_input.strip():
250
152
  continue
251
-
252
- mem_history.append_string(user_input)
153
+ shell_state.mem_history.append_string(user_input)
154
+ shell_state.user_input_history.append(user_input)
253
155
  conversation_history.add_message({"role": "user", "content": user_input})
156
+ handle_chat(shell_state, profile_manager, agent, max_rounds, session_id)
157
+ # Save conversation history after exiting
158
+ save_conversation_history(conversation_history, session_id)
254
159
 
255
- start_time = time.time()
256
-
257
- # No need to propagate verbose; ToolExecutor and others fetch from runtime_config
258
160
 
259
- # Clear the screen before starting LLM conversation
260
- console = Console()
261
- console.clear()
262
-
263
- # Print a summary of the current conversation history
264
-
265
- chat_start_summary(conversation_history, console, shell_state.last_usage_info)
266
-
267
- try:
268
- response = profile_manager.agent.chat(
269
- conversation_history,
270
- max_rounds=max_rounds,
271
- message_handler=message_handler,
272
- spinner=True,
273
- )
274
- except KeyboardInterrupt:
275
- message_handler.handle_message(
276
- {"type": "info", "message": "Request interrupted. Returning to prompt."}
277
- )
278
- continue
279
- except ProviderError as e:
280
- message_handler.handle_message(
281
- {"type": "error", "message": f"Provider error: {e}"}
282
- )
283
- continue
284
- except EmptyResponseError as e:
285
- message_handler.handle_message({"type": "error", "message": f"Error: {e}"})
286
- continue
287
- last_elapsed = time.time() - start_time
288
-
289
- usage = response.get("usage")
290
- if usage:
291
- for k in ("prompt_tokens", "completion_tokens", "total_tokens"):
292
- shell_state.last_usage_info[k] = usage.get(k, 0)
293
-
294
- # --- Ensure assistant and tool messages are added to ConversationHistory ---
295
- # If the last message is not an assistant/tool, add the response content
296
- content = response.get("content")
297
- if content and (
298
- len(conversation_history) == 0
299
- or conversation_history.get_messages()[-1].get("role") != "assistant"
300
- ):
301
- conversation_history.add_message({"role": "assistant", "content": content})
302
- # Optionally, add tool messages if present in response (extend here if needed)
303
- # ---------------------------------------------------------------------------
304
-
305
- # --- Save conversation history after each assistant reply ---
306
- session_id_to_save = session_id if session_id else get_session_id()
307
- history_dir = os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
308
- os.makedirs(history_dir, exist_ok=True)
309
- history_path = os.path.join(history_dir, f"{session_id_to_save}.json")
310
- conversation_history.to_json_file(history_path)
311
- # -----------------------------------------------------------
312
-
313
- # After exiting the main loop, print restart info if conversation has >1 message
161
+ def handle_chat(shell_state, profile_manager, agent, max_rounds, session_id):
162
+ conversation_history = shell_state.conversation_history
163
+ message_handler = RichMessageHandler()
164
+ console = message_handler.console
165
+ start_time = time.time()
166
+ try:
167
+ response = profile_manager.agent.chat(
168
+ conversation_history,
169
+ max_rounds=max_rounds,
170
+ message_handler=message_handler,
171
+ spinner=True,
172
+ )
173
+ except KeyboardInterrupt:
174
+ message_handler.handle_message(
175
+ {"type": "info", "message": "Request interrupted. Returning to prompt."}
176
+ )
177
+ return
178
+ except ProviderError as e:
179
+ message_handler.handle_message(
180
+ {"type": "error", "message": f"Provider error: {e}"}
181
+ )
182
+ return
183
+ except EmptyResponseError as e:
184
+ message_handler.handle_message({"type": "error", "message": f"Error: {e}"})
185
+ return
186
+ except ApiError as e:
187
+ message_handler.handle_message({"type": "error", "message": str(e)})
188
+ return
189
+ shell_state.last_elapsed = time.time() - start_time
190
+ usage = response.get("usage")
191
+ if usage:
192
+ for k in ("prompt_tokens", "completion_tokens", "total_tokens"):
193
+ shell_state.last_usage_info[k] = usage.get(k, 0)
194
+ content = response.get("content")
195
+ if content and (
196
+ len(conversation_history) == 0
197
+ or conversation_history.get_messages()[-1].get("role") != "assistant"
198
+ ):
199
+ conversation_history.add_message({"role": "assistant", "content": content})
200
+
201
+
202
+ def save_conversation_history(conversation_history, session_id):
203
+ from janito.shell.session.manager import get_session_id
314
204
 
315
- # --- Save conversation history to .janito/chat_history/(session_id).json ---
316
- session_id_to_save = session_id if session_id else get_session_id()
317
205
  history_dir = os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
318
206
  os.makedirs(history_dir, exist_ok=True)
207
+ session_id_to_save = session_id if session_id else get_session_id()
319
208
  history_path = os.path.join(history_dir, f"{session_id_to_save}.json")
320
209
  conversation_history.to_json_file(history_path)
321
- # -------------------------------------------------------------------------
210
+
211
+
212
+ def start_chat_shell(
213
+ profile_manager,
214
+ continue_session=False,
215
+ session_id=None,
216
+ max_rounds=100,
217
+ termweb_stdout_path=None,
218
+ termweb_stderr_path=None,
219
+ livereload_stdout_path=None,
220
+ livereload_stderr_path=None,
221
+ ):
222
+ i18n.set_locale(runtime_config.get("lang", "en"))
223
+ global active_prompt_session
224
+ agent = profile_manager.agent
225
+ message_handler = RichMessageHandler()
226
+ console = message_handler.console
227
+ console.clear()
228
+ shell_state = ShellState()
229
+ shell_state.profile_manager = profile_manager
230
+ user_input_history = UserInputHistory()
231
+ user_input_dicts = user_input_history.load()
232
+ mem_history = shell_state.mem_history
233
+ for item in user_input_dicts:
234
+ if isinstance(item, dict) and "input" in item:
235
+ mem_history.append_string(item["input"])
236
+ shell_state.user_input_history = user_input_history
237
+ if not load_session(shell_state, continue_session, session_id, profile_manager):
238
+ return
239
+
240
+ def last_usage_info_ref():
241
+ return shell_state.last_usage_info
242
+
243
+ last_elapsed = shell_state.last_elapsed
244
+ print_welcome_message(console, continue_id=session_id if continue_session else None)
245
+ session = setup_prompt_session(
246
+ lambda: shell_state.conversation_history.get_messages(),
247
+ last_usage_info_ref,
248
+ last_elapsed,
249
+ mem_history,
250
+ profile_manager,
251
+ agent,
252
+ lambda: shell_state.conversation_history,
253
+ )
254
+ active_prompt_session = session
255
+ handle_prompt_loop(
256
+ shell_state, session, profile_manager, agent, max_rounds, session_id
257
+ )
@@ -89,27 +89,6 @@ def save_conversation(messages, prompts, usage_info=None, path=None):
89
89
  json.dump(data, f, indent=2, default=usage_serializer)
90
90
 
91
91
 
92
- def load_input_history():
93
- history_dir = os.path.join(".janito", "input_history")
94
- os.makedirs(history_dir, exist_ok=True)
95
- today_str = datetime.now().strftime("%y%m%d")
96
- history_file = os.path.join(history_dir, f"{today_str}.json")
97
- try:
98
- with open(history_file, "r", encoding="utf-8") as f:
99
- return json.load(f)
100
- except (FileNotFoundError, json.JSONDecodeError):
101
- return []
102
-
103
-
104
- def save_input_history(history_list):
105
- history_dir = os.path.join(".janito", "input_history")
106
- os.makedirs(history_dir, exist_ok=True)
107
- today_str = datetime.now().strftime("%y%m%d")
108
- history_file = os.path.join(history_dir, f"{today_str}.json")
109
- with open(history_file, "w", encoding="utf-8") as f:
110
- json.dump(history_list, f, indent=2)
111
-
112
-
113
92
  def last_conversation_exists(path=".janito/last_conversation.json"):
114
93
  if not os.path.exists(path):
115
94
  return False
@@ -17,12 +17,10 @@ def print_welcome(console, version=None, continue_id=None):
17
17
  if runtime_config.get("vanilla_mode", False):
18
18
  console.print(
19
19
  f"[bold magenta]{tr('Welcome to Janito{version_str} in [white on magenta]VANILLA MODE[/white on magenta]! Tools, system prompt, and temperature are disabled unless overridden.', version_str=version_str)}[/bold magenta]\n"
20
- f"[cyan]{tr('F12 = Quick Action (follows the recommended action)')}[/cyan]"
21
20
  )
22
21
  else:
23
22
  console.print(
24
23
  f"[bold green]{tr('Welcome to Janito{version_str}! Entering chat mode. Type /exit to exit.', version_str=version_str)}[/bold green]\n"
25
- f"[cyan]{tr('F12 = Quick Action (follows the recommended action)')}[/cyan]"
26
24
  )
27
25
 
28
26
 
@@ -90,10 +90,6 @@ body.light-theme .CodeMirror {
90
90
  display: flex;
91
91
  align-items: center;
92
92
  justify-content: flex-start;
93
- position: fixed;
94
- bottom: 0;
95
- left: 0;
96
- z-index: 100;
97
93
  border-top: 1px solid #333;
98
94
  min-height: 48px;
99
95
  }
@@ -0,0 +1,44 @@
1
+ import io
2
+ from rich.console import Console
3
+ from janito.rich_utils import RichPrinter
4
+
5
+
6
+ def test_print_info(capsys=None):
7
+ buf = io.StringIO()
8
+ printer = RichPrinter(
9
+ console=Console(file=buf, force_terminal=True, color_system=None)
10
+ )
11
+ printer.print_info("info message")
12
+ output = buf.getvalue()
13
+ assert "info message" in output
14
+ assert "cyan" in output or output # Style is present if rich renders ANSI
15
+
16
+
17
+ def test_print_error():
18
+ buf = io.StringIO()
19
+ printer = RichPrinter(
20
+ console=Console(file=buf, force_terminal=True, color_system=None)
21
+ )
22
+ printer.print_error("error message")
23
+ output = buf.getvalue()
24
+ assert "error message" in output
25
+
26
+
27
+ def test_print_warning():
28
+ buf = io.StringIO()
29
+ printer = RichPrinter(
30
+ console=Console(file=buf, force_terminal=True, color_system=None)
31
+ )
32
+ printer.print_warning("warning message")
33
+ output = buf.getvalue()
34
+ assert "warning message" in output
35
+
36
+
37
+ def test_print_magenta():
38
+ buf = io.StringIO()
39
+ printer = RichPrinter(
40
+ console=Console(file=buf, force_terminal=True, color_system=None)
41
+ )
42
+ printer.print_magenta("magenta message")
43
+ output = buf.getvalue()
44
+ assert "magenta message" in output