janito 1.8.0__py3-none-any.whl → 1.9.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 (119) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config_defaults.py +23 -0
  3. janito/agent/config_utils.py +0 -9
  4. janito/agent/conversation.py +31 -9
  5. janito/agent/conversation_api.py +32 -2
  6. janito/agent/conversation_history.py +53 -0
  7. janito/agent/conversation_tool_calls.py +11 -8
  8. janito/agent/openai_client.py +11 -3
  9. janito/agent/openai_schema_generator.py +9 -6
  10. janito/agent/providers.py +77 -0
  11. janito/agent/rich_message_handler.py +1 -1
  12. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
  13. janito/agent/tool_executor.py +18 -10
  14. janito/agent/tool_use_tracker.py +16 -0
  15. janito/agent/tools/__init__.py +7 -9
  16. janito/agent/tools/create_directory.py +7 -6
  17. janito/agent/tools/create_file.py +29 -54
  18. janito/agent/tools/delete_text_in_file.py +97 -0
  19. janito/agent/tools/fetch_url.py +11 -3
  20. janito/agent/tools/find_files.py +37 -25
  21. janito/agent/tools/get_file_outline/__init__.py +1 -0
  22. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
  23. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  24. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
  25. janito/agent/tools/get_lines.py +15 -11
  26. janito/agent/tools/move_file.py +10 -11
  27. janito/agent/tools/remove_directory.py +2 -2
  28. janito/agent/tools/remove_file.py +11 -13
  29. janito/agent/tools/replace_file.py +62 -0
  30. janito/agent/tools/replace_text_in_file.py +3 -3
  31. janito/agent/tools/run_bash_command.py +3 -7
  32. janito/agent/tools/run_powershell_command.py +39 -28
  33. janito/agent/tools/run_python_command.py +3 -5
  34. janito/agent/tools/search_text.py +10 -14
  35. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  36. janito/agent/tools/validate_file_syntax/core.py +92 -0
  37. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  38. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  39. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  40. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  41. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  42. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  43. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  44. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  45. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  46. janito/agent/tools_utils/__init__.py +1 -0
  47. janito/agent/tools_utils/dir_walk_utils.py +23 -0
  48. janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
  49. janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
  50. janito/agent/tools_utils/utils.py +30 -0
  51. janito/cli/_livereload_log_utils.py +13 -0
  52. janito/cli/arg_parser.py +45 -3
  53. janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
  54. janito/cli/livereload_starter.py +60 -0
  55. janito/cli/main.py +110 -21
  56. janito/cli/one_shot.py +66 -0
  57. janito/cli/termweb_starter.py +2 -2
  58. janito/livereload/app.py +25 -0
  59. janito/rich_utils.py +0 -22
  60. janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
  61. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  62. janito/shell/commands/conversation_restart.py +72 -0
  63. janito/shell/commands/edit.py +21 -0
  64. janito/shell/commands/history_view.py +18 -0
  65. janito/shell/commands/livelogs.py +40 -0
  66. janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
  67. janito/shell/commands/session.py +32 -0
  68. janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
  69. janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
  70. janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
  71. janito/shell/commands/tools.py +23 -0
  72. janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
  73. janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
  74. janito/shell/commands.py +40 -0
  75. janito/shell/main.py +321 -0
  76. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  77. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  78. janito/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
  79. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
  80. janito/termweb/app.py +3 -3
  81. janito/termweb/static/editor.css +146 -0
  82. janito/termweb/static/editor.css.bak +27 -0
  83. janito/termweb/static/editor.html +15 -213
  84. janito/termweb/static/editor.html.bak +16 -215
  85. janito/termweb/static/editor.js +209 -0
  86. janito/termweb/static/editor.js.bak +227 -0
  87. janito/termweb/static/index.html +2 -3
  88. janito/termweb/static/index.html.bak +2 -3
  89. janito/termweb/static/termweb.css.bak +33 -84
  90. janito/termweb/static/termweb.js +15 -34
  91. janito/termweb/static/termweb.js.bak +18 -36
  92. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
  93. janito-1.9.0.dist-info/RECORD +151 -0
  94. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
  95. janito/agent/tools/dir_walk_utils.py +0 -16
  96. janito/agent/tools/memory.py +0 -48
  97. janito/agent/tools/outline_file/python_outline.py +0 -71
  98. janito/agent/tools/present_choices_test.py +0 -18
  99. janito/agent/tools/rich_live.py +0 -44
  100. janito/agent/tools/tools_utils.py +0 -56
  101. janito/agent/tools/utils.py +0 -33
  102. janito/agent/tools/validate_file_syntax.py +0 -163
  103. janito/cli_chat_shell/chat_loop.py +0 -163
  104. janito/cli_chat_shell/chat_state.py +0 -38
  105. janito/cli_chat_shell/commands/history_start.py +0 -37
  106. janito/cli_chat_shell/commands/session.py +0 -48
  107. janito-1.8.0.dist-info/RECORD +0 -127
  108. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  109. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  110. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  111. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  112. /janito/{cli/runner → shell}/__init__.py +0 -0
  113. /janito/{cli_chat_shell → shell}/commands/lang.py +0 -0
  114. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  115. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  116. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  117. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
  118. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
  119. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- def handle_sum(console, state, **kwargs):
1
+ def handle_sum(console, shell_state=None, **kwargs):
2
2
  """
3
3
  Summarize the current chat history and replace it with a summary message.
4
4
  """
@@ -7,15 +7,15 @@ def handle_sum(console, state, **kwargs):
7
7
  console.print("[bold red]Agent not provided to /sum command.[/bold red]")
8
8
  return
9
9
 
10
- messages = state.get("messages", [])
11
- if not messages or len(messages) < 2:
10
+ history = shell_state.conversation_history.get_messages()
11
+ if not history or len(history) < 2:
12
12
  console.print(
13
13
  "[bold yellow]Not enough conversation to summarize.[/bold yellow]"
14
14
  )
15
15
  return
16
16
 
17
17
  # Find the system message if present
18
- system_msg = next((m for m in messages if m.get("role") == "system"), None)
18
+ system_msg = next((m for m in history if m.get("role") == "system"), None)
19
19
 
20
20
  # Prepare summary prompt
21
21
  summary_prompt = {
@@ -23,7 +23,7 @@ def handle_sum(console, state, **kwargs):
23
23
  "content": "Summarize the following conversation in a concise paragraph for context. Only output the summary, do not include any tool calls or formatting.",
24
24
  }
25
25
  # Exclude system messages for the summary context
26
- convo_for_summary = [m for m in messages if m.get("role") != "system"]
26
+ convo_for_summary = [m for m in history if m.get("role") != "system"]
27
27
  summary_messages = [summary_prompt] + convo_for_summary
28
28
 
29
29
  try:
@@ -42,7 +42,7 @@ def handle_sum(console, state, **kwargs):
42
42
  if system_msg:
43
43
  new_history.append(system_msg)
44
44
  new_history.append({"role": "assistant", "content": summary_text})
45
- state["messages"] = new_history
45
+ shell_state.conversation_history.set_messages(new_history)
46
46
 
47
47
  console.print(
48
48
  "[bold green]Conversation summarized and history replaced with summary.[/bold green]"
@@ -14,12 +14,12 @@ def is_termweb_running(port):
14
14
  return False
15
15
 
16
16
 
17
- def handle_termweb_log_tail(console: Console, *args, state=None, **kwargs):
17
+ def handle_termweb_log_tail(console: Console, *args, shell_state=None, **kwargs):
18
18
  lines = 20
19
19
  if args and args[0].isdigit():
20
20
  lines = int(args[0])
21
- stdout_path = state.get("termweb_stdout_path") if state else None
22
- stderr_path = state.get("termweb_stderr_path") if state else None
21
+ stdout_path = shell_state.termweb_stdout_path if shell_state else None
22
+ stderr_path = shell_state.termweb_stderr_path if shell_state else None
23
23
  if not stdout_path and not stderr_path:
24
24
  console.print(
25
25
  "[yellow][termweb] No termweb log files found for this session.[/yellow]"
@@ -51,20 +51,20 @@ def handle_termweb_log_tail(console: Console, *args, state=None, **kwargs):
51
51
  console.print("[termweb] No output or errors captured in logs.")
52
52
 
53
53
 
54
- def handle_termweb_status(console: Console, *args, state=None, **kwargs):
55
- if state is None:
54
+ def handle_termweb_status(console: Console, *args, shell_state=None, **kwargs):
55
+ if shell_state is None:
56
56
  console.print(
57
57
  "[red]No shell state available. Cannot determine termweb status.[/red]"
58
58
  )
59
59
  return
60
- port = state.get("termweb_port")
61
- port_source = "state"
60
+ port = getattr(shell_state, "termweb_port", None)
61
+ port_source = "shell_state"
62
62
  if not port:
63
63
  port = runtime_config.get("termweb_port")
64
64
  port_source = "runtime_config"
65
- pid = state.get("termweb_pid")
66
- stdout_path = state.get("termweb_stdout_path")
67
- stderr_path = state.get("termweb_stderr_path")
65
+ pid = getattr(shell_state, "termweb_pid", None)
66
+ stdout_path = getattr(shell_state, "termweb_stdout_path", None)
67
+ stderr_path = getattr(shell_state, "termweb_stderr_path", None)
68
68
  running = False
69
69
  if port:
70
70
  running = is_termweb_running(port)
@@ -0,0 +1,23 @@
1
+ from janito.agent.tool_registry import get_tool_schemas
2
+ from rich.table import Table
3
+
4
+
5
+ def handle_tools(console, args=None, shell_state=None):
6
+ table = Table(title="Available Tools", show_lines=True, style="bold magenta")
7
+ table.add_column("Name", style="cyan", no_wrap=True)
8
+ table.add_column("Description", style="green")
9
+ table.add_column("Parameters", style="yellow")
10
+ try:
11
+ for schema in get_tool_schemas():
12
+ fn = schema["function"]
13
+ params = "\n".join(
14
+ [
15
+ f"[bold]{k}[/]: {v['type']}"
16
+ for k, v in fn["parameters"].get("properties", {}).items()
17
+ ]
18
+ )
19
+ table.add_row(f"[b]{fn['name']}[/b]", fn["description"], params or "-")
20
+ except Exception as e:
21
+ console.print(f"[red]Error loading tools: {e}[/red]")
22
+ return
23
+ console.print(table)
@@ -3,16 +3,16 @@ def handle_help(console, **kwargs):
3
3
  """
4
4
  [bold green]Available commands:[/bold green]
5
5
  /exit, exit - Exit chat mode
6
- /restart - Restart the CLI
6
+ /restart - Start a new conversation
7
7
  /help - Show this help message
8
8
  /continue - Restore last saved conversation
9
- /start - Reset conversation history
10
9
  /prompt - Show the system prompt
11
10
  /role - Change the system role
12
11
  /clear - Clear the terminal screen
13
12
  /multi - Provide multiline input as next message
14
13
  /config - Show or set configuration (see: /config show, /config set local|global key=value)
15
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
16
  /termweb-status - Show status information about the running termweb server
17
17
  /verbose [on|off] - Show or set verbose mode for this session
18
18
  """
@@ -25,8 +25,9 @@ def handle_clear(console, **kwargs):
25
25
  os.system("cls" if os.name == "nt" else "clear")
26
26
 
27
27
 
28
- def handle_multi(console, state, **kwargs):
28
+ def handle_multi(console, shell_state=None, **kwargs):
29
29
  console.print(
30
30
  "[bold yellow]Multiline mode activated. Provide or write your text and press Esc + Enter to submit.[/bold yellow]"
31
31
  )
32
- state["paste_mode"] = True
32
+ if shell_state:
33
+ shell_state.paste_mode = True
@@ -1,7 +1,7 @@
1
1
  from janito.agent.runtime_config import runtime_config
2
2
 
3
3
 
4
- def handle_verbose(console, state, **kwargs):
4
+ def handle_verbose(console, shell_state=None, **kwargs):
5
5
  """
6
6
  /verbose [on|off]
7
7
  Shows or sets verbose mode for the current shell session.
@@ -0,0 +1,40 @@
1
+ from janito.agent.tool_registry import get_tool_schemas
2
+ from rich.console import Console
3
+
4
+
5
+ def handle_command(cmd, console: Console, shell_state=None):
6
+ cmd = cmd.strip().lower()
7
+ if cmd in ("/exit", "exit"):
8
+ return "exit"
9
+ if cmd in ("/help", "help"):
10
+ console.print("[bold cyan]/help[/]: Show this help message")
11
+ console.print("[bold cyan]/exit[/]: Exit the shell")
12
+ console.print("[bold cyan]/tools[/]: List available tools")
13
+ return
14
+ if cmd in ("/tools", "tools"):
15
+ table = None
16
+ try:
17
+ from rich.table import Table
18
+
19
+ table = Table(
20
+ title="Available Tools", show_lines=True, style="bold magenta"
21
+ )
22
+ table.add_column("Name", style="cyan", no_wrap=True)
23
+ table.add_column("Description", style="green")
24
+ table.add_column("Parameters", style="yellow")
25
+ for schema in get_tool_schemas():
26
+ fn = schema["function"]
27
+ params = "\n".join(
28
+ [
29
+ f"[bold]{k}[/]: {v['type']}"
30
+ for k, v in fn["parameters"].get("properties", {}).items()
31
+ ]
32
+ )
33
+ table.add_row(f"[b]{fn['name']}[/b]", fn["description"], params or "-")
34
+ except Exception as e:
35
+ console.print(f"[red]Error loading tools: {e}[/red]")
36
+ if table:
37
+ console.print(table)
38
+ return
39
+ # Unknown command
40
+ console.print(f"[yellow]Unknown command:[/] {cmd}")
janito/shell/main.py ADDED
@@ -0,0 +1,321 @@
1
+ from janito.agent.rich_message_handler import RichMessageHandler
2
+ from prompt_toolkit.history import InMemoryHistory
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional, Any, Dict
5
+ from janito.shell.prompt.session_setup import (
6
+ setup_prompt_session,
7
+ print_welcome_message,
8
+ )
9
+ from janito.shell.commands import handle_command
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
13
+ import janito.i18n as i18n
14
+ from janito.agent.runtime_config import runtime_config
15
+ from rich.console import Console
16
+ from collections import Counter
17
+ import os
18
+ from janito.shell.session.manager import get_session_id
19
+ from prompt_toolkit.formatted_text import HTML
20
+ 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))
83
+
84
+
85
+ @dataclass
86
+ class ShellState:
87
+ mem_history: Any = field(default_factory=InMemoryHistory)
88
+ conversation_history: Any = field(default_factory=lambda: ConversationHistory())
89
+ last_usage_info: Dict[str, int] = field(
90
+ default_factory=lambda: {
91
+ "prompt_tokens": 0,
92
+ "completion_tokens": 0,
93
+ "total_tokens": 0,
94
+ }
95
+ )
96
+ last_elapsed: Optional[float] = None
97
+ termweb_stdout_path: Optional[str] = None
98
+ termweb_stderr_path: Optional[str] = None
99
+ livereload_stdout_path: Optional[str] = None
100
+ livereload_stderr_path: Optional[str] = None
101
+ paste_mode: bool = False
102
+ profile_manager: Optional[Any] = None
103
+
104
+
105
+ # Track the active prompt session for cleanup
106
+ active_prompt_session = None
107
+
108
+
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
127
+ from janito.shell.session.manager import load_conversation_by_session_id
128
+
129
+ shell_state = ShellState()
130
+ shell_state.profile_manager = profile_manager
131
+ if continue_session and session_id:
132
+ try:
133
+ messages, prompts, usage = load_conversation_by_session_id(session_id)
134
+ 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)
139
+ conversation_history = shell_state.conversation_history
140
+ # Always refresh the system prompt in the loaded history
141
+ found = False
142
+ for msg in conversation_history.get_messages():
143
+ if msg.get("role") == "system":
144
+ msg["content"] = profile_manager.system_prompt_template
145
+ found = True
146
+ break
147
+ if not found:
148
+ conversation_history.set_system_message(
149
+ profile_manager.system_prompt_template
150
+ )
151
+ # Optionally set prompts/usage if needed
152
+ shell_state.last_usage_info = usage or {}
153
+ else:
154
+ conversation_history = shell_state.conversation_history
155
+ # Add system prompt if needed (skip in vanilla mode)
156
+
157
+ if (
158
+ profile_manager.system_prompt_template
159
+ and (
160
+ not runtime_config.get("vanilla_mode", False)
161
+ or runtime_config.get("system_prompt_template")
162
+ )
163
+ and not any(
164
+ m.get("role") == "system" for m in conversation_history.get_messages()
165
+ )
166
+ ):
167
+ conversation_history.set_system_message(
168
+ profile_manager.system_prompt_template
169
+ )
170
+ mem_history = shell_state.mem_history
171
+
172
+ def last_usage_info_ref():
173
+ return shell_state.last_usage_info
174
+
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
+
190
+ while True:
191
+ try:
192
+
193
+ if shell_state.paste_mode:
194
+ user_input = session.prompt("Multiline> ", multiline=True)
195
+ was_paste_mode = True
196
+ shell_state.paste_mode = False
197
+ else:
198
+ user_input = session.prompt(
199
+ HTML("<inputline>💬 </inputline>"), multiline=False
200
+ )
201
+ was_paste_mode = False
202
+ except EOFError:
203
+ console.print("\n[bold red]Exiting...[/bold red]")
204
+ break
205
+ except KeyboardInterrupt:
206
+ console.print() # Move to next line
207
+ try:
208
+ confirm = (
209
+ session.prompt(
210
+ # Use <inputline> for full-line blue background, <prompt> for icon only
211
+ HTML(
212
+ "<inputline>Do you really want to exit? (y/n): </inputline>"
213
+ )
214
+ )
215
+ .strip()
216
+ .lower()
217
+ )
218
+ except KeyboardInterrupt:
219
+ message_handler.handle_message(
220
+ {"type": "error", "message": "Exiting..."}
221
+ )
222
+ break
223
+ if confirm == "y":
224
+ message_handler.handle_message(
225
+ {"type": "error", "message": "Exiting..."}
226
+ )
227
+ conversation_history.add_message(
228
+ {"role": "system", "content": "[Session ended by user]"}
229
+ )
230
+ break
231
+ else:
232
+ continue
233
+
234
+ cmd_input = user_input.strip().lower()
235
+ if not was_paste_mode and (cmd_input.startswith("/") or cmd_input == "exit"):
236
+ # Treat both '/exit' and 'exit' as commands
237
+ result = handle_command(
238
+ user_input.strip(),
239
+ console,
240
+ shell_state=shell_state,
241
+ )
242
+ if result == "exit":
243
+ conversation_history.add_message(
244
+ {"role": "system", "content": "[Session ended by user]"}
245
+ )
246
+ break
247
+ continue
248
+
249
+ if not user_input.strip():
250
+ continue
251
+
252
+ mem_history.append_string(user_input)
253
+ conversation_history.add_message({"role": "user", "content": user_input})
254
+
255
+ start_time = time.time()
256
+
257
+ # No need to propagate verbose; ToolExecutor and others fetch from runtime_config
258
+
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
314
+
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
+ history_dir = os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
318
+ os.makedirs(history_dir, exist_ok=True)
319
+ history_path = os.path.join(history_dir, f"{session_id_to_save}.json")
320
+ conversation_history.to_json_file(history_path)
321
+ # -------------------------------------------------------------------------
@@ -4,7 +4,7 @@ from prompt_toolkit.completion import Completer, Completion
4
4
  class ShellCommandCompleter(Completer):
5
5
  def __init__(self):
6
6
  # Import here to avoid circular import at module level
7
- from janito.cli_chat_shell.commands import COMMAND_HANDLERS
7
+ from janito.shell.commands import COMMAND_HANDLERS
8
8
 
9
9
  # Only commands starting with '/'
10
10
  self.commands = sorted(
@@ -1,19 +1,31 @@
1
- from .ui import print_welcome, get_toolbar_func, get_prompt_session
1
+ from janito.shell.ui.interactive import (
2
+ print_welcome,
3
+ get_toolbar_func,
4
+ get_prompt_session,
5
+ )
2
6
  from janito import __version__
3
7
  from janito.agent.config import effective_config
4
8
  from janito.agent.runtime_config import runtime_config
9
+ from janito.shell.session.manager import get_session_id
5
10
 
6
11
 
7
12
  def setup_prompt_session(
8
- messages, last_usage_info_ref, last_elapsed, mem_history, profile_manager, agent
13
+ messages,
14
+ last_usage_info_ref,
15
+ last_elapsed,
16
+ mem_history,
17
+ profile_manager,
18
+ agent,
19
+ history_ref,
9
20
  ):
10
21
  model_name = getattr(agent, "model", None)
22
+ session_id = get_session_id()
11
23
 
12
24
  def get_messages():
13
25
  return messages
14
26
 
15
27
  def get_usage():
16
- return last_usage_info_ref["value"]
28
+ return last_usage_info_ref()
17
29
 
18
30
  def get_elapsed():
19
31
  return last_elapsed
@@ -32,11 +44,13 @@ def setup_prompt_session(
32
44
  )
33
45
  else (runtime_config.get("role") or effective_config.get("role"))
34
46
  ),
47
+ session_id=session_id,
48
+ history_ref=history_ref,
35
49
  ),
36
50
  mem_history,
37
51
  )
38
52
  return session
39
53
 
40
54
 
41
- def print_welcome_message(console, continued):
42
- print_welcome(console, version=__version__, continued=continued)
55
+ def print_welcome_message(console, continue_id=None):
56
+ print_welcome(console, version=__version__, continue_id=continue_id)
@@ -2,6 +2,35 @@ import os
2
2
  import json
3
3
  from datetime import datetime
4
4
 
5
+ # --- Session ID generation ---
6
+ _current_session_id = None
7
+
8
+
9
+ def generate_session_id():
10
+ # Use seconds since start of year, encode as base36 for shortness
11
+ now = datetime.now()
12
+ start_of_year = datetime(now.year, 1, 1)
13
+ seconds = int((now - start_of_year).total_seconds())
14
+ chars = "0123456789abcdefghijklmnopqrstuvwxyz"
15
+ out = ""
16
+ n = seconds
17
+ while n:
18
+ n, r = divmod(n, 36)
19
+ out = chars[r] + out
20
+ return out or "0"
21
+
22
+
23
+ def reset_session_id():
24
+ global _current_session_id
25
+ _current_session_id = None
26
+
27
+
28
+ def get_session_id():
29
+ global _current_session_id
30
+ if _current_session_id is None:
31
+ _current_session_id = generate_session_id()
32
+ return _current_session_id
33
+
5
34
 
6
35
  def load_last_summary(path=".janito/last_conversation.json"):
7
36
  if not os.path.exists(path):
@@ -22,9 +51,30 @@ def load_last_conversation(path=".janito/last_conversation.json"):
22
51
  return messages, prompts, usage
23
52
 
24
53
 
25
- def save_conversation(
26
- messages, prompts, usage_info=None, path=".janito/last_conversation.json"
27
- ):
54
+ def load_conversation_by_session_id(session_id):
55
+ path = os.path.join(".janito", "chat_history", f"{session_id}.json")
56
+ if not os.path.exists(path):
57
+ raise FileNotFoundError(f"Session file not found: {path}")
58
+ with open(path, "r", encoding="utf-8") as f:
59
+ data = json.load(f)
60
+ messages = data.get("messages", [])
61
+ prompts = data.get("prompts", [])
62
+ usage = data.get("last_usage_info")
63
+ return messages, prompts, usage
64
+
65
+
66
+ def save_conversation(messages, prompts, usage_info=None, path=None):
67
+ # Do not save if only one message and it is a system message (noop session)
68
+ if (
69
+ isinstance(messages, list)
70
+ and len(messages) == 1
71
+ and messages[0].get("role") == "system"
72
+ ):
73
+ return
74
+
75
+ if path is None:
76
+ session_id = get_session_id()
77
+ path = os.path.join(".janito", "chat_history", f"{session_id}.json")
28
78
  os.makedirs(os.path.dirname(path), exist_ok=True)
29
79
  data = {"messages": messages, "prompts": prompts, "last_usage_info": usage_info}
30
80