janito 1.8.1__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 (142) 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 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,26 @@
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)
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)
@@ -0,0 +1,28 @@
1
+ def handle_help(console, **kwargs):
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}")
9
+
10
+
11
+ def handle_clear(console, **kwargs):
12
+ import os
13
+
14
+ os.system("cls" if os.name == "nt" else "clear")
15
+
16
+
17
+ handle_clear.help_text = "Clear the terminal screen"
18
+
19
+
20
+ def handle_multi(console, shell_state=None, **kwargs):
21
+ console.print(
22
+ "[bold yellow]Multiline mode activated. Provide or write your text and press Esc + Enter to submit.[/bold yellow]"
23
+ )
24
+ if shell_state:
25
+ shell_state.paste_mode = True
26
+
27
+
28
+ handle_multi.help_text = "Provide multiline input as next message"
@@ -1,11 +1,7 @@
1
1
  from janito.agent.runtime_config import runtime_config
2
2
 
3
3
 
4
- def handle_verbose(console, state, **kwargs):
5
- """
6
- /verbose [on|off]
7
- Shows or sets verbose mode for the current shell session.
8
- """
4
+ def handle_verbose(console, shell_state=None, **kwargs):
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, state, **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,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}")
@@ -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 ADDED
@@ -0,0 +1,257 @@
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.api_exceptions import ApiError
12
+ from janito.agent.llm_conversation_history import LLMConversationHistory
13
+ import janito.i18n as i18n
14
+ from janito.agent.runtime_config import runtime_config
15
+ from rich.console import Console
16
+ import os
17
+ from janito.shell.session.manager import get_session_id
18
+ from prompt_toolkit.formatted_text import HTML
19
+ import time
20
+ from janito.shell.input_history import UserInputHistory
21
+
22
+
23
+ @dataclass
24
+ class ShellState:
25
+ mem_history: Any = field(default_factory=InMemoryHistory)
26
+ conversation_history: Any = field(default_factory=lambda: LLMConversationHistory())
27
+ last_usage_info: Dict[str, int] = field(
28
+ default_factory=lambda: {
29
+ "prompt_tokens": 0,
30
+ "completion_tokens": 0,
31
+ "total_tokens": 0,
32
+ }
33
+ )
34
+ last_elapsed: Optional[float] = None
35
+ termweb_stdout_path: Optional[str] = None
36
+ termweb_stderr_path: Optional[str] = None
37
+ livereload_stdout_path: Optional[str] = None
38
+ livereload_stderr_path: Optional[str] = None
39
+ paste_mode: bool = False
40
+ profile_manager: Optional[Any] = None
41
+ user_input_history: Optional[Any] = None
42
+
43
+
44
+ # Track the active prompt session for cleanup
45
+ active_prompt_session = None
46
+
47
+
48
+ def load_session(shell_state, continue_session, session_id, profile_manager):
49
+ from janito.shell.session.manager import load_conversation_by_session_id
50
+
51
+ if continue_session and session_id:
52
+ try:
53
+ messages, prompts, usage = load_conversation_by_session_id(session_id)
54
+ except FileNotFoundError as e:
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)
60
+ conversation_history = shell_state.conversation_history
61
+ found = False
62
+ for msg in conversation_history.get_messages():
63
+ if msg.get("role") == "system":
64
+ msg["content"] = profile_manager.system_prompt_template
65
+ found = True
66
+ break
67
+ if not found:
68
+ conversation_history.set_system_message(
69
+ profile_manager.system_prompt_template
70
+ )
71
+ shell_state.last_usage_info = usage or {}
72
+ else:
73
+ conversation_history = shell_state.conversation_history
74
+ if (
75
+ profile_manager.system_prompt_template
76
+ and (
77
+ not runtime_config.get("vanilla_mode", False)
78
+ or runtime_config.get("system_prompt_template")
79
+ )
80
+ and not any(
81
+ m.get("role") == "system" for m in conversation_history.get_messages()
82
+ )
83
+ ):
84
+ conversation_history.set_system_message(
85
+ profile_manager.system_prompt_template
86
+ )
87
+ return True
88
+
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
97
+ while True:
98
+ try:
99
+ if shell_state.paste_mode:
100
+ user_input = session.prompt("Multiline> ", multiline=True)
101
+ was_paste_mode = True
102
+ shell_state.paste_mode = False
103
+ else:
104
+ user_input = session.prompt(
105
+ HTML("<inputline>💬 </inputline>"), multiline=False
106
+ )
107
+ was_paste_mode = False
108
+ except EOFError:
109
+ console.print("\n[bold red]Exiting...[/bold red]")
110
+ break
111
+ except KeyboardInterrupt:
112
+ console.print()
113
+ try:
114
+ confirm = (
115
+ session.prompt(
116
+ HTML(
117
+ "<inputline>Do you really want to exit? (y/n): </inputline>"
118
+ )
119
+ )
120
+ .strip()
121
+ .lower()
122
+ )
123
+ except KeyboardInterrupt:
124
+ message_handler.handle_message(
125
+ {"type": "error", "message": "Exiting..."}
126
+ )
127
+ break
128
+ if confirm == "y":
129
+ message_handler.handle_message(
130
+ {"type": "error", "message": "Exiting..."}
131
+ )
132
+ conversation_history.add_message(
133
+ {"role": "system", "content": "[Session ended by user]"}
134
+ )
135
+ break
136
+ else:
137
+ continue
138
+ cmd_input = user_input.strip().lower()
139
+ if not was_paste_mode and (cmd_input.startswith("/") or cmd_input == "exit"):
140
+ result = handle_command(
141
+ user_input.strip(),
142
+ console,
143
+ shell_state=shell_state,
144
+ )
145
+ if result == "exit":
146
+ conversation_history.add_message(
147
+ {"role": "system", "content": "[Session ended by user]"}
148
+ )
149
+ break
150
+ continue
151
+ if not user_input.strip():
152
+ continue
153
+ shell_state.mem_history.append_string(user_input)
154
+ shell_state.user_input_history.append(user_input)
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)
159
+
160
+
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
204
+
205
+ history_dir = os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
206
+ os.makedirs(history_dir, exist_ok=True)
207
+ session_id_to_save = session_id if session_id else get_session_id()
208
+ history_path = os.path.join(history_dir, f"{session_id_to_save}.json")
209
+ conversation_history.to_json_file(history_path)
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
+ )
@@ -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)
@@ -0,0 +1,101 @@
1
+ import os
2
+ import json
3
+ from datetime import datetime
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
+
34
+
35
+ def load_last_summary(path=".janito/last_conversation.json"):
36
+ if not os.path.exists(path):
37
+ return None
38
+ with open(path, "r", encoding="utf-8") as f:
39
+ data = json.load(f)
40
+ return data
41
+
42
+
43
+ def load_last_conversation(path=".janito/last_conversation.json"):
44
+ if not os.path.exists(path):
45
+ return [], [], None
46
+ with open(path, "r", encoding="utf-8") as f:
47
+ data = json.load(f)
48
+ messages = data.get("messages", [])
49
+ prompts = data.get("prompts", [])
50
+ usage = data.get("last_usage_info")
51
+ return messages, prompts, usage
52
+
53
+
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")
78
+ os.makedirs(os.path.dirname(path), exist_ok=True)
79
+ data = {"messages": messages, "prompts": prompts, "last_usage_info": usage_info}
80
+
81
+ def usage_serializer(obj):
82
+ if hasattr(obj, "to_dict"):
83
+ return obj.to_dict()
84
+ if hasattr(obj, "__dict__"):
85
+ return obj.__dict__
86
+ return str(obj)
87
+
88
+ with open(path, "w", encoding="utf-8") as f:
89
+ json.dump(data, f, indent=2, default=usage_serializer)
90
+
91
+
92
+ def last_conversation_exists(path=".janito/last_conversation.json"):
93
+ if not os.path.exists(path):
94
+ return False
95
+ try:
96
+ with open(path, "r", encoding="utf-8") as f:
97
+ data = json.load(f)
98
+ messages = data.get("messages", [])
99
+ return bool(messages)
100
+ except Exception:
101
+ return False