janito 1.9.0__py3-none-any.whl → 1.11.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 (106) 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 +246 -168
  7. janito/agent/conversation_ui.py +1 -1
  8. janito/agent/{conversation_history.py → llm_conversation_history.py} +30 -1
  9. janito/agent/openai_client.py +38 -23
  10. janito/agent/openai_schema_generator.py +162 -129
  11. janito/agent/platform_discovery.py +134 -77
  12. janito/agent/profile_manager.py +5 -5
  13. janito/agent/rich_message_handler.py +80 -31
  14. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +20 -4
  15. janito/agent/test_openai_schema_generator.py +93 -0
  16. janito/agent/tool_base.py +7 -2
  17. janito/agent/tool_executor.py +54 -49
  18. janito/agent/tool_registry.py +5 -2
  19. janito/agent/tool_use_tracker.py +26 -5
  20. janito/agent/tools/__init__.py +8 -3
  21. janito/agent/tools/create_directory.py +3 -1
  22. janito/agent/tools/create_file.py +7 -1
  23. janito/agent/tools/fetch_url.py +40 -3
  24. janito/agent/tools/find_files.py +29 -14
  25. janito/agent/tools/get_file_outline/core.py +7 -8
  26. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  27. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  28. janito/agent/tools/get_lines.py +98 -64
  29. janito/agent/tools/move_file.py +59 -31
  30. janito/agent/tools/open_url.py +31 -0
  31. janito/agent/tools/present_choices.py +3 -1
  32. janito/agent/tools/python_command_runner.py +149 -0
  33. janito/agent/tools/python_file_runner.py +147 -0
  34. janito/agent/tools/python_stdin_runner.py +153 -0
  35. janito/agent/tools/remove_directory.py +3 -1
  36. janito/agent/tools/remove_file.py +5 -1
  37. janito/agent/tools/replace_file.py +12 -2
  38. janito/agent/tools/replace_text_in_file.py +195 -149
  39. janito/agent/tools/run_bash_command.py +30 -69
  40. janito/agent/tools/run_powershell_command.py +138 -105
  41. janito/agent/tools/search_text/__init__.py +1 -0
  42. janito/agent/tools/search_text/core.py +176 -0
  43. janito/agent/tools/search_text/match_lines.py +58 -0
  44. janito/agent/tools/search_text/pattern_utils.py +65 -0
  45. janito/agent/tools/search_text/traverse_directory.py +127 -0
  46. janito/agent/tools/validate_file_syntax/core.py +43 -30
  47. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  48. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  49. janito/agent/tools_utils/action_type.py +7 -0
  50. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  51. janito/agent/tools_utils/formatting.py +47 -21
  52. janito/agent/tools_utils/gitignore_utils.py +89 -40
  53. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  54. janito/agent/tools_utils/utils.py +7 -1
  55. janito/cli/_print_config.py +63 -61
  56. janito/cli/arg_parser.py +13 -12
  57. janito/cli/cli_main.py +137 -147
  58. janito/cli/config_commands.py +112 -109
  59. janito/cli/main.py +152 -174
  60. janito/cli/one_shot.py +40 -26
  61. janito/i18n/__init__.py +1 -1
  62. janito/rich_utils.py +46 -8
  63. janito/shell/commands/__init__.py +2 -4
  64. janito/shell/commands/conversation_restart.py +3 -1
  65. janito/shell/commands/edit.py +3 -0
  66. janito/shell/commands/history_view.py +3 -3
  67. janito/shell/commands/lang.py +3 -0
  68. janito/shell/commands/livelogs.py +5 -3
  69. janito/shell/commands/prompt.py +6 -0
  70. janito/shell/commands/session.py +3 -0
  71. janito/shell/commands/session_control.py +3 -0
  72. janito/shell/commands/termweb_log.py +8 -0
  73. janito/shell/commands/tools.py +3 -0
  74. janito/shell/commands/track.py +36 -0
  75. janito/shell/commands/utility.py +13 -18
  76. janito/shell/commands/verbose.py +3 -4
  77. janito/shell/input_history.py +62 -0
  78. janito/shell/main.py +160 -181
  79. janito/shell/session/config.py +83 -75
  80. janito/shell/session/manager.py +0 -21
  81. janito/shell/ui/interactive.py +97 -75
  82. janito/termweb/static/editor.css +32 -33
  83. janito/termweb/static/editor.css.bak +140 -22
  84. janito/termweb/static/editor.html +12 -7
  85. janito/termweb/static/editor.html.bak +16 -11
  86. janito/termweb/static/editor.js +94 -40
  87. janito/termweb/static/editor.js.bak +97 -65
  88. janito/termweb/static/index.html +1 -2
  89. janito/termweb/static/index.html.bak +1 -1
  90. janito/termweb/static/termweb.css +1 -22
  91. janito/termweb/static/termweb.css.bak +6 -4
  92. janito/termweb/static/termweb.js +0 -6
  93. janito/termweb/static/termweb.js.bak +1 -2
  94. janito/tests/test_rich_utils.py +44 -0
  95. janito/web/app.py +0 -75
  96. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/METADATA +61 -42
  97. janito-1.11.0.dist-info/RECORD +163 -0
  98. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
  99. janito/agent/providers.py +0 -77
  100. janito/agent/tools/run_python_command.py +0 -161
  101. janito/agent/tools/search_text.py +0 -204
  102. janito/shell/commands/sum.py +0 -49
  103. janito-1.9.0.dist-info/RECORD +0 -151
  104. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
  105. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
  106. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/top_level.txt +0 -0
@@ -12,90 +12,98 @@ def handle_config_shell(console, *args, **kwargs):
12
12
  /config reset global
13
13
  """
14
14
  if not args or args[0] not in ("show", "set", "reset"):
15
- console.print(
16
- "[bold red]Usage:[/bold red] /config show | /config set local|global key=value | /config reset local|global"
17
- )
15
+ _print_usage(console)
18
16
  return
19
-
20
17
  if args[0] == "show":
21
- # Show config, unified with CLI
22
- from janito.cli._print_config import print_full_config
23
-
24
- print_full_config(
25
- local_config,
26
- global_config,
27
- unified_config,
28
- CONFIG_DEFAULTS,
29
- console=console,
30
- )
18
+ _show_config(console)
31
19
  return
32
-
33
20
  if args[0] == "reset":
34
- if len(args) < 2 or args[1] not in ("local", "global"):
35
- console.print("[bold red]Usage:[/bold red] /config reset local|global")
36
- return
37
- import os
38
- from pathlib import Path
21
+ _reset_config(console, args)
22
+ return
23
+ if args[0] == "set":
24
+ _set_config(console, args)
25
+ return
39
26
 
40
- scope = args[1]
41
- if scope == "local":
42
- local_path = Path(".janito/config.json")
43
- if local_path.exists():
44
- os.remove(local_path)
45
- console.print(f"[green]Removed local config file:[/green] {local_path}")
46
- else:
47
- console.print(
48
- f"[yellow]Local config file does not exist:[/yellow] {local_path}"
49
- )
50
- elif scope == "global":
51
- global_path = Path.home() / ".janito/config.json"
52
- if global_path.exists():
53
- os.remove(global_path)
54
- console.print(
55
- f"[green]Removed global config file:[/green] {global_path}"
56
- )
57
- else:
58
- console.print(
59
- f"[yellow]Global config file does not exist:[/yellow] {global_path}"
60
- )
61
- console.print(
62
- "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
63
- )
27
+
28
+ def _print_usage(console):
29
+ console.print(
30
+ "[bold red]Usage:[/bold red] /config show | /config set local|global key=value | /config reset local|global"
31
+ )
32
+
33
+
34
+ def _show_config(console):
35
+ from janito.cli._print_config import print_full_config
36
+
37
+ print_full_config(
38
+ local_config,
39
+ global_config,
40
+ unified_config,
41
+ CONFIG_DEFAULTS,
42
+ console=console,
43
+ )
44
+
45
+
46
+ def _reset_config(console, args):
47
+ if len(args) < 2 or args[1] not in ("local", "global"):
48
+ console.print("[bold red]Usage:[/bold red] /config reset local|global")
64
49
  return
50
+ import os
51
+ from pathlib import Path
65
52
 
66
- if args[0] == "set":
67
- if len(args) < 3 or args[1] not in ("local", "global"):
68
- console.print(
69
- "[bold red]Usage:[/bold red] /config set local|global key=value"
70
- )
71
- return
72
- scope = args[1]
73
- try:
74
- key, val = args[2].split("=", 1)
75
- except ValueError:
76
- console.print("[bold red]Invalid format, expected key=val[/bold red]")
77
- return
78
- key = key.strip()
79
- if key not in CONFIG_OPTIONS and not key.startswith("template."):
80
- console.print(
81
- f"[bold red]Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}"
82
- )
83
- return
84
- val = val.strip()
85
- if scope == "local":
86
- local_config.set(key, val)
87
- local_config.save()
88
- runtime_config.set(key, val)
89
- console.print(f"[green]Local config updated:[/green] {key} = {val}")
53
+ scope = args[1]
54
+ if scope == "local":
55
+ local_path = Path(".janito/config.json")
56
+ if local_path.exists():
57
+ os.remove(local_path)
58
+ console.print(f"[green]Removed local config file:[/green] {local_path}")
59
+ else:
90
60
  console.print(
91
- "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
61
+ f"[yellow]Local config file does not exist:[/yellow] {local_path}"
92
62
  )
93
- elif scope == "global":
94
- global_config.set(key, val)
95
- global_config.save()
96
- runtime_config.set(key, val)
97
- console.print(f"[green]Global config updated:[/green] {key} = {val}")
63
+ elif scope == "global":
64
+ global_path = Path.home() / ".janito/config.json"
65
+ if global_path.exists():
66
+ os.remove(global_path)
67
+ console.print(f"[green]Removed global config file:[/green] {global_path}")
68
+ else:
98
69
  console.print(
99
- "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
70
+ f"[yellow]Global config file does not exist:[/yellow] {global_path}"
100
71
  )
72
+ console.print(
73
+ "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
74
+ )
75
+
76
+
77
+ def _set_config(console, args):
78
+ if len(args) < 3 or args[1] not in ("local", "global"):
79
+ console.print("[bold red]Usage:[/bold red] /config set local|global key=value")
101
80
  return
81
+ scope = args[1]
82
+ try:
83
+ key, val = args[2].split("=", 1)
84
+ except ValueError:
85
+ console.print("[bold red]Invalid format, expected key=val[/bold red]")
86
+ return
87
+ key = key.strip()
88
+ if key not in CONFIG_OPTIONS and not key.startswith("template."):
89
+ console.print(
90
+ f"[bold red]Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}"
91
+ )
92
+ return
93
+ val = val.strip()
94
+ if scope == "local":
95
+ local_config.set(key, val)
96
+ local_config.save()
97
+ runtime_config.set(key, val)
98
+ console.print(f"[green]Local config updated:[/green] {key} = {val}")
99
+ console.print(
100
+ "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
101
+ )
102
+ elif scope == "global":
103
+ global_config.set(key, val)
104
+ global_config.save()
105
+ runtime_config.set(key, val)
106
+ console.print(f"[green]Global config updated:[/green] {key} = {val}")
107
+ console.print(
108
+ "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
109
+ )
@@ -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,15 +17,99 @@ 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
 
27
+ def format_tokens(n, tag=None):
28
+ if n is None:
29
+ return "?"
30
+ if n < 1000:
31
+ val = str(n)
32
+ elif n < 1000000:
33
+ val = f"{n/1000:.1f}k"
34
+ else:
35
+ val = f"{n/1000000:.1f}M"
36
+ return f"<{tag}>{val}</{tag}>" if tag else val
37
+
38
+
39
+ def assemble_first_line(model_name, role_ref, style_ref):
40
+ model_part = f" {tr('Model')}: <model>{model_name}</model>" if model_name else ""
41
+ role_part = ""
42
+ vanilla_mode = runtime_config.get("vanilla_mode", False)
43
+ if role_ref and not vanilla_mode:
44
+ role = role_ref()
45
+ if role:
46
+ role_part = f"{tr('Role')}: <role>{role}</role>"
47
+ style_part = ""
48
+ if style_ref:
49
+ style = style_ref()
50
+ if style:
51
+ style_part = f"{tr('Style')}: <b>{style}</b>"
52
+ first_line_parts = []
53
+ if model_part:
54
+ first_line_parts.append(model_part)
55
+ if role_part:
56
+ first_line_parts.append(role_part)
57
+ if style_part:
58
+ first_line_parts.append(style_part)
59
+ return " | ".join(first_line_parts)
60
+
61
+
62
+ def assemble_second_line(
63
+ width,
64
+ last_usage_info_ref,
65
+ history_ref,
66
+ messages_ref,
67
+ session_id,
68
+ model_name,
69
+ role_ref,
70
+ style_ref,
71
+ ):
72
+ usage = last_usage_info_ref()
73
+ prompt_tokens = usage.get("prompt_tokens") if usage else None
74
+ completion_tokens = usage.get("completion_tokens") if usage else None
75
+ total_tokens = usage.get("total_tokens") if usage else None
76
+ msg_count = len(history_ref()) if history_ref else len(messages_ref())
77
+ left = f" {tr('Messages')}: <msg_count>{msg_count}</msg_count>"
78
+ tokens_part = ""
79
+ if (
80
+ prompt_tokens is not None
81
+ or completion_tokens is not None
82
+ or total_tokens is not None
83
+ ):
84
+ tokens_part = (
85
+ f" | {tr('Tokens')} - {tr('Prompt')}: {format_tokens(prompt_tokens, 'tokens_in')}, "
86
+ f"{tr('Completion')}: {format_tokens(completion_tokens, 'tokens_out')}, "
87
+ f"{tr('Total')}: {format_tokens(total_tokens, 'tokens_total')}"
88
+ )
89
+ session_part = (
90
+ f" | Session ID: <session_id>{session_id}</session_id>" if session_id else ""
91
+ )
92
+ second_line = f"{left}{tokens_part}{session_part}"
93
+ total_len = len(left) + len(tokens_part) + len(session_part)
94
+ first_line = assemble_first_line(model_name, role_ref, style_ref)
95
+ if first_line:
96
+ total_len += len(first_line) + 3
97
+ if total_len < width:
98
+ padding = " " * (width - total_len)
99
+ second_line = f"{left}{tokens_part}{session_part}{padding}"
100
+ return second_line
101
+
102
+
103
+ def assemble_bindings_line():
104
+ return (
105
+ f"<b> F12</b>: {tr('Quick Action')} | "
106
+ f"<b>Ctrl-Y</b>: {tr('Yes')} | "
107
+ f"<b>Ctrl-N</b>: {tr('No')} | "
108
+ f"<b>/help</b>: {tr('Help')} | "
109
+ f"<b>/restart</b>: {tr('Reset Conversation')}"
110
+ )
111
+
112
+
29
113
  def get_toolbar_func(
30
114
  messages_ref,
31
115
  last_usage_info_ref,
@@ -39,82 +123,20 @@ def get_toolbar_func(
39
123
  ):
40
124
  from prompt_toolkit.application.current import get_app
41
125
 
42
- def format_tokens(n, tag=None):
43
- if n is None:
44
- return "?"
45
- if n < 1000:
46
- val = str(n)
47
- elif n < 1000000:
48
- val = f"{n/1000:.1f}k"
49
- else:
50
- val = f"{n/1000000:.1f}M"
51
- return f"<{tag}>{val}</{tag}>" if tag else val
52
-
53
126
  def get_toolbar():
54
127
  width = get_app().output.get_size().columns
55
- model_part = (
56
- f" {tr('Model')}: <model>{model_name}</model>" if model_name else ""
57
- )
58
- role_part = ""
59
- vanilla_mode = runtime_config.get("vanilla_mode", False)
60
- if role_ref and not vanilla_mode:
61
- role = role_ref()
62
- if role:
63
- role_part = f"{tr('Role')}: <role>{role}</role>"
64
-
65
- style_part = ""
66
- if style_ref:
67
- style = style_ref()
68
- if style:
69
- style_part = f"{tr('Style')}: <b>{style}</b>"
70
- usage = last_usage_info_ref()
71
- prompt_tokens = usage.get("prompt_tokens") if usage else None
72
- completion_tokens = usage.get("completion_tokens") if usage else None
73
- total_tokens = usage.get("total_tokens") if usage else None
74
- first_line_parts = []
75
- if model_part:
76
- first_line_parts.append(model_part)
77
-
78
- if role_part:
79
- first_line_parts.append(role_part)
80
- if style_part:
81
- first_line_parts.append(style_part)
82
- first_line = " | ".join(first_line_parts)
83
- msg_count = len(history_ref()) if history_ref else len(messages_ref())
84
- left = f" {tr('Messages')}: <msg_count>{msg_count}</msg_count>"
85
- tokens_part = ""
86
- if (
87
- prompt_tokens is not None
88
- or completion_tokens is not None
89
- or total_tokens is not None
90
- ):
91
- tokens_part = (
92
- f" | {tr('Tokens')} - {tr('Prompt')}: {format_tokens(prompt_tokens, 'tokens_in')}, "
93
- f"{tr('Completion')}: {format_tokens(completion_tokens, 'tokens_out')}, "
94
- f"{tr('Total')}: {format_tokens(total_tokens, 'tokens_total')}"
95
- )
96
- # Move /help and /restart tips to key bindings/info line
97
- # Compose second/status line (no help_part)
98
- session_part = (
99
- f" | Session ID: <session_id>{session_id}</session_id>"
100
- if session_id
101
- else ""
102
- )
103
- second_line = f"{left}{tokens_part}{session_part}"
104
- total_len = len(left) + len(tokens_part) + len(session_part)
105
- if first_line:
106
- total_len += len(first_line) + 3
107
- if total_len < width:
108
- padding = " " * (width - total_len)
109
- second_line = f"{left}{tokens_part}{session_part}{padding}"
110
- # Add key bindings info as an extra line, now including /help and /restart
111
- bindings_line = (
112
- f"<b> F12</b>: {tr('Quick Action')} | "
113
- f"<b>Ctrl-Y</b>: {tr('Yes')} | "
114
- f"<b>Ctrl-N</b>: {tr('No')} | "
115
- f"<b>/help</b>: {tr('Help')} | "
116
- f"<b>/restart</b>: {tr('Reset Conversation')}"
128
+ first_line = assemble_first_line(model_name, role_ref, style_ref)
129
+ second_line = assemble_second_line(
130
+ width,
131
+ last_usage_info_ref,
132
+ history_ref,
133
+ messages_ref,
134
+ session_id,
135
+ model_name,
136
+ role_ref,
137
+ style_ref,
117
138
  )
139
+ bindings_line = assemble_bindings_line()
118
140
  if first_line:
119
141
  toolbar_text = first_line + "\n" + second_line + "\n" + bindings_line
120
142
  else:
@@ -1,9 +1,9 @@
1
1
  /* Highlight active line in CodeMirror */
2
2
  .CodeMirror-activeline-background {
3
- background: #353b45 !important;
3
+ background: #353b45;
4
4
  }
5
5
  body.light-theme .CodeMirror-activeline-background {
6
- background: #e0eaff !important;
6
+ background: #e0eaff;
7
7
  }
8
8
  html, body {
9
9
  height: 100%;
@@ -39,36 +39,22 @@ body.light-theme {
39
39
  flex-direction: column;
40
40
  height: 100%;
41
41
  min-height: 0;
42
- padding: 0 !important;
43
- margin: 0 !important;
44
- }
45
- .CodeMirror {
46
- flex: 1 1 auto;
47
- height: 100% !important;
48
- min-height: 0;
49
- font-size: 1.1em;
50
- background: #282a36;
51
- color: #f8f8f2;
52
- border-radius: 0;
53
- border: none;
54
- transition: background 0.2s, color 0.2s;
55
- }
56
- body.light-theme .CodeMirror {
57
- background: #fff;
58
- color: #222;
42
+ padding: 0;
43
+ margin: 0;
59
44
  }
45
+ /* Removed custom .CodeMirror background/color to use CodeMirror's theme defaults */
60
46
  .header {
61
47
  background: #222;
62
48
  color: #fff;
63
- padding: 10px 24px;
64
- font-size: 1.3em;
49
+ padding: 4px 12px;
50
+ font-size: 0.95em;
65
51
  font-weight: bold;
66
52
  position: relative;
67
53
  transition: background 0.2s, color 0.2s;
68
54
  display: flex;
69
55
  align-items: center;
70
56
  justify-content: flex-end;
71
- height: 56px;
57
+ height: 36px;
72
58
  }
73
59
 
74
60
  .header-title {
@@ -78,24 +64,39 @@ body.light-theme .CodeMirror {
78
64
  transform: translate(-50%, -50%);
79
65
  pointer-events: none;
80
66
  font-weight: bold;
81
- font-size: 1.3em;
67
+ font-size: 1.05em;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.5em;
71
+ }
72
+
73
+ .filename-display {
74
+ font-weight: normal;
75
+ font-size: 0.98em;
76
+ color: #bbb;
77
+ margin-left: 0.5em;
78
+ pointer-events: auto;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ overflow: hidden;
82
+ max-width: 180px;
83
+ }
84
+ body.light-theme .filename-display {
85
+ color: #666;
82
86
  }
83
87
 
84
88
  .footer {
85
89
  width: 100%;
86
90
  background: #23272b;
87
91
  color: #fff;
88
- padding: 10px 24px;
92
+ padding: 4px 12px;
89
93
  box-sizing: border-box;
90
94
  display: flex;
91
95
  align-items: center;
92
96
  justify-content: flex-start;
93
- position: fixed;
94
- bottom: 0;
95
- left: 0;
96
- z-index: 100;
97
97
  border-top: 1px solid #333;
98
- min-height: 48px;
98
+ min-height: 28px;
99
+ font-size: 0.95em;
99
100
  }
100
101
 
101
102
  .save-btn {
@@ -106,7 +107,6 @@ body.light-theme .CodeMirror {
106
107
  margin-left: auto;
107
108
  }
108
109
 
109
-
110
110
  .save-btn {
111
111
  background: #4caf50;
112
112
  color: #fff;
@@ -121,8 +121,8 @@ body.light-theme .CodeMirror {
121
121
  display: flex;
122
122
  align-items: center;
123
123
  justify-content: center;
124
- width: 36px;
125
- height: 36px;
124
+ height: 100%;
125
+ aspect-ratio: 1/1;
126
126
  font-size: 1.3em;
127
127
  line-height: 1;
128
128
  padding: 0;
@@ -143,4 +143,3 @@ body.light-theme .header {
143
143
  background: #eaeaea;
144
144
  color: #222;
145
145
  }
146
-
@@ -1,27 +1,145 @@
1
+ /* Highlight active line in CodeMirror */
2
+ .CodeMirror-activeline-background {
3
+ background: #353b45;
4
+ }
5
+ body.light-theme .CodeMirror-activeline-background {
6
+ background: #e0eaff;
7
+ }
8
+ html, body {
9
+ height: 100%;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ body {
14
+ display: flex;
15
+ flex-direction: column;
16
+ min-height: 100vh;
17
+ background: #181a1b;
18
+ color: #eee;
19
+ transition: background 0.2s, color 0.2s;
20
+ }
21
+ body.light-theme {
22
+ background: #f5f5f5;
23
+ color: #222;
24
+ }
25
+ .main {
26
+ flex: 1 1 auto;
27
+ display: flex;
28
+ flex-direction: column;
29
+ justify-content: stretch;
30
+ align-items: stretch;
31
+ min-height: 0;
32
+ padding: 0;
33
+ margin: 0;
34
+ height: 100%;
35
+ }
36
+ .editor-pane {
37
+ flex: 1 1 auto;
38
+ display: flex;
39
+ flex-direction: column;
40
+ height: 100%;
41
+ min-height: 0;
42
+ padding: 0;
43
+ margin: 0;
44
+ }
45
+ /* Removed custom .CodeMirror background/color to use CodeMirror's theme defaults */
46
+ .header {
47
+ background: #222;
48
+ color: #fff;
49
+ padding: 4px 12px;
50
+ font-size: 0.95em;
51
+ font-weight: bold;
52
+ position: relative;
53
+ transition: background 0.2s, color 0.2s;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: flex-end;
57
+ height: 36px;
58
+ }
59
+
60
+ .header-title {
61
+ position: absolute;
62
+ left: 50%;
63
+ top: 50%;
64
+ transform: translate(-50%, -50%);
65
+ pointer-events: none;
66
+ font-weight: bold;
67
+ font-size: 1.05em;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.5em;
71
+ }
72
+
73
+ .filename-display {
74
+ font-weight: normal;
75
+ font-size: 0.98em;
76
+ color: #bbb;
77
+ margin-left: 0.5em;
78
+ pointer-events: auto;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ overflow: hidden;
82
+ max-width: 180px;
83
+ }
84
+ body.light-theme .filename-display {
85
+ color: #666;
86
+ }
87
+
88
+ .footer {
89
+ width: 100%;
90
+ background: #23272b;
91
+ color: #fff;
92
+ padding: 4px 12px;
93
+ box-sizing: border-box;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: flex-start;
97
+ border-top: 1px solid #333;
98
+ min-height: 28px;
99
+ font-size: 0.95em;
100
+ }
1
101
 
2
102
  .save-btn {
3
- background: linear-gradient(90deg, #00ff99 0%, #00cfff 100%);
4
- color: #181a1b;
103
+ /* No margin needed, align left in footer */
104
+ }
105
+
106
+ .theme-switcher {
107
+ margin-left: auto;
108
+ }
109
+
110
+ .save-btn {
111
+ background: #4caf50;
112
+ color: #fff;
113
+ border: none;
114
+ border-radius: 4px;
115
+ padding: 6px 14px;
116
+ cursor: pointer;
117
+ font-size: 1em;
118
+ }
119
+
120
+ .theme-switcher {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ width: 36px;
125
+ height: 36px;
126
+ font-size: 1.3em;
127
+ line-height: 1;
128
+ padding: 0;
129
+ background: #444;
130
+ color: #fff;
5
131
  border: none;
6
- border-radius: 8px;
7
- padding: 12px 38px;
132
+ border-radius: 4px;
8
133
  cursor: pointer;
9
- font-size: 1.18em;
10
- font-weight: 900;
11
- box-shadow: 0 0 16px 4px rgba(0,255,153,0.35), 0 2px 16px 0 rgba(0,207,255,0.18);
12
- outline: 2.5px solid #00cfff;
13
- outline-offset: 2px;
14
- transition: box-shadow 0.2s, outline 0.2s, background 0.2s, color 0.2s;
15
- letter-spacing: 0.7px;
16
- animation: save-btn-pop 0.7s cubic-bezier(.68,-0.55,.27,1.55) 1;
17
- min-width: 110px;
18
- min-height: 44px;
19
- z-index: 2;
20
- text-shadow: 0 2px 8px #fff8, 0 1px 0 #fff8;
21
- }
22
- .save-btn:hover, .save-btn:focus {
23
- background: linear-gradient(90deg, #00cfff 0%, #00ff99 100%);
24
- color: #111;
25
- box-shadow: 0 0 24px 6px rgba(0,255,153,0.45), 0 4px 24px 0 rgba(0,207,255,0.28);
26
- outline: 3px solid #00ff99;
134
+ transition: background 0.2s, color 0.2s;
135
+ }
136
+
137
+ body.light-theme .theme-switcher {
138
+ background: #ddd;
139
+ color: #222;
140
+ }
141
+
142
+ body.light-theme .header {
143
+ background: #eaeaea;
144
+ color: #222;
27
145
  }