janito 1.6.0__py3-none-any.whl → 1.8.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 (117) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config.py +3 -3
  3. janito/agent/config_defaults.py +3 -2
  4. janito/agent/conversation.py +73 -27
  5. janito/agent/conversation_api.py +104 -4
  6. janito/agent/conversation_exceptions.py +6 -0
  7. janito/agent/conversation_tool_calls.py +17 -3
  8. janito/agent/event.py +24 -0
  9. janito/agent/event_dispatcher.py +24 -0
  10. janito/agent/event_handler_protocol.py +5 -0
  11. janito/agent/event_system.py +15 -0
  12. janito/agent/message_handler.py +4 -1
  13. janito/agent/message_handler_protocol.py +5 -0
  14. janito/agent/openai_client.py +5 -6
  15. janito/agent/openai_schema_generator.py +23 -4
  16. janito/agent/platform_discovery.py +90 -0
  17. janito/agent/profile_manager.py +34 -110
  18. janito/agent/queued_message_handler.py +22 -3
  19. janito/agent/rich_message_handler.py +3 -1
  20. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +14 -0
  21. janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +13 -0
  22. janito/agent/test_handler_protocols.py +47 -0
  23. janito/agent/tests/__init__.py +1 -0
  24. janito/agent/tool_base.py +1 -1
  25. janito/agent/tool_executor.py +109 -0
  26. janito/agent/tool_registry.py +3 -75
  27. janito/agent/tool_use_tracker.py +46 -0
  28. janito/agent/tools/__init__.py +11 -8
  29. janito/agent/tools/ask_user.py +26 -12
  30. janito/agent/tools/create_directory.py +50 -18
  31. janito/agent/tools/create_file.py +60 -29
  32. janito/agent/tools/dir_walk_utils.py +16 -0
  33. janito/agent/tools/fetch_url.py +10 -11
  34. janito/agent/tools/find_files.py +49 -40
  35. janito/agent/tools/get_lines.py +60 -25
  36. janito/agent/tools/memory.py +48 -0
  37. janito/agent/tools/move_file.py +72 -23
  38. janito/agent/tools/outline_file/__init__.py +85 -0
  39. janito/agent/tools/outline_file/formatting.py +20 -0
  40. janito/agent/tools/outline_file/markdown_outline.py +14 -0
  41. janito/agent/tools/outline_file/python_outline.py +71 -0
  42. janito/agent/tools/present_choices.py +62 -0
  43. janito/agent/tools/present_choices_test.py +18 -0
  44. janito/agent/tools/remove_directory.py +31 -26
  45. janito/agent/tools/remove_file.py +31 -13
  46. janito/agent/tools/replace_text_in_file.py +135 -36
  47. janito/agent/tools/run_bash_command.py +113 -97
  48. janito/agent/tools/run_powershell_command.py +169 -0
  49. janito/agent/tools/run_python_command.py +53 -29
  50. janito/agent/tools/search_outline.py +17 -0
  51. janito/agent/tools/search_text.py +208 -0
  52. janito/agent/tools/tools_utils.py +47 -4
  53. janito/agent/tools/utils.py +14 -15
  54. janito/agent/tools/validate_file_syntax.py +163 -0
  55. janito/cli/_print_config.py +1 -1
  56. janito/cli/arg_parser.py +36 -4
  57. janito/cli/config_commands.py +1 -1
  58. janito/cli/logging_setup.py +7 -2
  59. janito/cli/main.py +97 -3
  60. janito/cli/runner/__init__.py +0 -2
  61. janito/cli/runner/_termweb_log_utils.py +17 -0
  62. janito/cli/runner/cli_main.py +121 -89
  63. janito/cli/runner/config.py +6 -4
  64. janito/cli/termweb_starter.py +73 -0
  65. janito/cli_chat_shell/chat_loop.py +52 -13
  66. janito/cli_chat_shell/chat_state.py +1 -1
  67. janito/cli_chat_shell/chat_ui.py +2 -3
  68. janito/cli_chat_shell/commands/__init__.py +17 -6
  69. janito/cli_chat_shell/commands/{history_reset.py → history_start.py} +13 -5
  70. janito/cli_chat_shell/commands/lang.py +16 -0
  71. janito/cli_chat_shell/commands/prompt.py +42 -0
  72. janito/cli_chat_shell/commands/session_control.py +36 -1
  73. janito/cli_chat_shell/commands/sum.py +49 -0
  74. janito/cli_chat_shell/commands/termweb_log.py +86 -0
  75. janito/cli_chat_shell/commands/utility.py +5 -2
  76. janito/cli_chat_shell/commands/verbose.py +29 -0
  77. janito/cli_chat_shell/load_prompt.py +47 -8
  78. janito/cli_chat_shell/session_manager.py +9 -1
  79. janito/cli_chat_shell/shell_command_completer.py +20 -0
  80. janito/cli_chat_shell/ui.py +110 -93
  81. janito/i18n/__init__.py +35 -0
  82. janito/i18n/messages.py +23 -0
  83. janito/i18n/pt.py +46 -0
  84. janito/rich_utils.py +43 -43
  85. janito/termweb/app.py +95 -0
  86. janito/termweb/static/editor.html +238 -0
  87. janito/termweb/static/editor.html.bak +238 -0
  88. janito/termweb/static/explorer.html.bak +59 -0
  89. janito/termweb/static/favicon.ico +0 -0
  90. janito/termweb/static/favicon.ico.bak +0 -0
  91. janito/termweb/static/index.html +55 -0
  92. janito/termweb/static/index.html.bak +55 -0
  93. janito/termweb/static/index.html.bak.bak +175 -0
  94. janito/termweb/static/landing.html.bak +36 -0
  95. janito/termweb/static/termicon.svg +1 -0
  96. janito/termweb/static/termweb.css +235 -0
  97. janito/termweb/static/termweb.css.bak +286 -0
  98. janito/termweb/static/termweb.js +187 -0
  99. janito/termweb/static/termweb.js.bak +187 -0
  100. janito/termweb/static/termweb.js.bak.bak +157 -0
  101. janito/termweb/static/termweb_quickopen.js +135 -0
  102. janito/termweb/static/termweb_quickopen.js.bak +125 -0
  103. janito/web/app.py +10 -13
  104. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/METADATA +73 -32
  105. janito-1.8.0.dist-info/RECORD +127 -0
  106. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/WHEEL +1 -1
  107. janito/agent/tool_registry_core.py +0 -2
  108. janito/agent/tools/get_file_outline.py +0 -117
  109. janito/agent/tools/py_compile_file.py +0 -40
  110. janito/agent/tools/replace_file.py +0 -51
  111. janito/agent/tools/search_files.py +0 -71
  112. janito/cli/runner/scan.py +0 -44
  113. janito/cli_chat_shell/commands/system.py +0 -73
  114. janito-1.6.0.dist-info/RECORD +0 -81
  115. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/entry_points.txt +0 -0
  116. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/licenses/LICENSE +0 -0
  117. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,286 @@
1
+ /* --- Layout and Theme --- */
2
+ html, body {
3
+ height: 100%;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+ body {
8
+ display: flex;
9
+ flex-direction: column;
10
+ min-height: 100vh;
11
+ background: #181a1b;
12
+ color: #f1f1f1;
13
+ transition: background 0.2s, color 0.2s;
14
+ }
15
+ body.light-theme {
16
+ background: #f5f5f5;
17
+ color: #181a1b;
18
+ }
19
+
20
+ .header {
21
+ background: #23272b;
22
+ color: #f1f1f1;
23
+ padding: 1em 2em;
24
+ font-size: 1.5em;
25
+ font-weight: bold;
26
+ letter-spacing: 0.04em;
27
+ border-bottom: 2px solid #1a73e8;
28
+ position: relative;
29
+ transition: background 0.2s, color 0.2s;
30
+ }
31
+ body.dark-theme .header {
32
+ background: #181a1b;
33
+ color: #f1f1f1;
34
+ border-bottom: 2px solid #1976d2;
35
+ }
36
+ body.light-theme .header {
37
+ background: #eaeaea;
38
+ color: #181a1b;
39
+ border-bottom: 2px solid #1565c0;
40
+ }
41
+
42
+ /* --- Enhanced Header --- */
43
+ .header-logo {
44
+ width: 2.4em;
45
+ height: 2.4em;
46
+ margin-right: 0.5em;
47
+ vertical-align: middle;
48
+ }
49
+ .header-title {
50
+ font-size: 1.7em;
51
+ font-weight: bold;
52
+ letter-spacing: 0.04em;
53
+ color: #8be9fd;
54
+ text-shadow: 0 2px 8px #222a, 0 1px 0 #222a;
55
+ }
56
+ .header-subtitle {
57
+ font-size: 1.1em;
58
+ font-weight: 400;
59
+ opacity: 0.7;
60
+ margin-left: 0.5em;
61
+ color: #90caf9;
62
+ }
63
+
64
+ /* --- Explorer Entry Flex Alignment --- */
65
+ .explorer-entry {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.7em;
69
+ }
70
+ .explorer-icon {
71
+ font-size: 1.2em;
72
+ min-width: 1.6em;
73
+ text-align: center;
74
+ margin-right: 0.2em;
75
+ }
76
+ .explorer-name {
77
+ font-size: 1em;
78
+ word-break: break-all;
79
+ }
80
+
81
+ .main {
82
+ flex: 1 1 auto;
83
+ display: flex;
84
+ flex-direction: column;
85
+ justify-content: stretch;
86
+ align-items: stretch;
87
+ min-height: 0;
88
+ padding: 0;
89
+ background: transparent;
90
+ }
91
+
92
+ .editor-pane {
93
+ flex: 1 1 auto;
94
+ display: flex;
95
+ flex-direction: column;
96
+ height: 100%;
97
+ min-height: 0;
98
+ }
99
+
100
+ .CodeMirror {
101
+ flex: 1 1 auto;
102
+ height: 100% !important;
103
+ min-height: 0;
104
+ font-size: 1.1em;
105
+ background: #23272b;
106
+ color: #f8f8f2;
107
+ border-radius: 0;
108
+ border: none;
109
+ transition: background 0.2s, color 0.2s;
110
+ }
111
+ body.light-theme .CodeMirror {
112
+ background: #fff;
113
+ color: #181a1b;
114
+ }
115
+
116
+ .footer {
117
+ flex-shrink: 0;
118
+ background: #23272b;
119
+ color: #f1f1f1;
120
+ padding: 1.2em 2em 1em 2em;
121
+ font-size: 1em;
122
+ border-top: 2px solid #1a73e8;
123
+ text-align: center;
124
+ display: flex;
125
+ flex-direction: column;
126
+ align-items: center;
127
+ justify-content: center;
128
+ position: fixed;
129
+ left: 0;
130
+ bottom: 0;
131
+ width: 100%;
132
+ z-index: 100;
133
+ transition: background 0.2s, color 0.2s;
134
+ }
135
+ body.dark-theme .footer {
136
+ background: #181a1b;
137
+ color: #f1f1f1;
138
+ border-top: 2px solid #1976d2;
139
+ }
140
+ body.light-theme .footer {
141
+ background: #eaeaea;
142
+ color: #181a1b;
143
+ border-top: 2px solid #1565c0;
144
+ }
145
+ .footer a {
146
+ color: #90caf9;
147
+ text-decoration: underline;
148
+ }
149
+ body.light-theme .footer a {
150
+ color: #1976d2;
151
+ }
152
+ .footer ul {
153
+ list-style: none;
154
+ padding: 0;
155
+ margin: 0.5em 0 0 0;
156
+ display: flex;
157
+ gap: 1.5em;
158
+ justify-content: center;
159
+ align-items: center;
160
+ }
161
+ .footer li {
162
+ display: inline;
163
+ }
164
+
165
+ .theme-switcher {
166
+ position: absolute;
167
+ right: 20px;
168
+ top: 10px;
169
+ background: #444;
170
+ color: #f1f1f1;
171
+ border: none;
172
+ border-radius: 4px;
173
+ padding: 6px 14px;
174
+ cursor: pointer;
175
+ font-size: 1em;
176
+ transition: background 0.2s, color 0.2s;
177
+ }
178
+ body.light-theme .theme-switcher {
179
+ background: #ddd;
180
+ color: #181a1b;
181
+ }
182
+ body.dark-theme .theme-switcher {
183
+ background: #23272b;
184
+ color: #f1f1f1;
185
+ }
186
+
187
+ /* Explorer file/dir links */
188
+ .explorer-link, .explorer-link:visited {
189
+ color: #90caf9;
190
+ text-decoration: none;
191
+ font-weight: 500;
192
+ }
193
+ body.light-theme .explorer-link, body.light-theme .explorer-link:visited {
194
+ color: #1976d2;
195
+ }
196
+ .explorer-link:hover {
197
+ text-decoration: underline;
198
+ color: #42a5f5;
199
+ }
200
+ body.light-theme .explorer-link:hover {
201
+ color: #0d47a1;
202
+ }
203
+
204
+ /* Error and status messages */
205
+ .error {
206
+ color: #ff5252;
207
+ background: #2d2d2d;
208
+ padding: 0.5em 1em;
209
+ border-radius: 4px;
210
+ margin: 1em 0;
211
+ }
212
+ body.light-theme .error {
213
+ color: #b71c1c;
214
+ background: #fff3f3;
215
+ }
216
+
217
+ /* Spinner for loading */
218
+ .spinner {
219
+ border: 4px solid #f3f3f3;
220
+ border-top: 4px solid #1976d2;
221
+ border-radius: 50%;
222
+ width: 24px;
223
+ height: 24px;
224
+ animation: spin 1s linear infinite;
225
+ display: inline-block;
226
+ }
227
+ @keyframes spin {
228
+ 0% { transform: rotate(0deg); }
229
+ 100% { transform: rotate(360deg); }
230
+ }
231
+
232
+ /* --- Enhanced Toolbar & Panes --- */
233
+ .toolbar {
234
+ display: flex;
235
+ justify-content: flex-end;
236
+ align-items: center;
237
+ gap: 0.7em;
238
+ background: #23272b;
239
+ border-bottom: 1px solid #1a73e8;
240
+ padding: 0.7em 2em;
241
+ margin-bottom: 0.5em;
242
+ box-shadow: 0 2px 8px #0002;
243
+ }
244
+ .view-toggle {
245
+ background: #23272b;
246
+ color: #90caf9;
247
+ border: 1px solid #1a73e8;
248
+ border-radius: 0.5em;
249
+ padding: 0.5em 1.2em;
250
+ font-size: 1.1em;
251
+ cursor: pointer;
252
+ transition: background 0.15s, color 0.15s, box-shadow 0.15s;
253
+ margin-left: 0.2em;
254
+ }
255
+ .view-toggle.active, .view-toggle:hover {
256
+ background: #1a73e8;
257
+ color: #fff;
258
+ box-shadow: 0 2px 8px #1976d255;
259
+ }
260
+
261
+ #explorer-main, #explorer-preview {
262
+ background: #22262b;
263
+ border-radius: 1.1em;
264
+ box-shadow: 0 4px 24px #0004;
265
+ border: 1px solid #1a73e8;
266
+ padding: 1.5em 1.2em;
267
+ min-height: 40vh;
268
+ overflow: auto;
269
+ }
270
+ #explorer-main {
271
+ margin-right: 0.5em;
272
+ }
273
+ #explorer-preview {
274
+ margin-left: 0.5em;
275
+ }
276
+ @media (max-width: 900px) {
277
+ .main {
278
+ flex-direction: column;
279
+ gap: 1em;
280
+ padding: 1em;
281
+ }
282
+ #explorer-main, #explorer-preview {
283
+ min-height: 20vh;
284
+ padding: 1em 0.7em;
285
+ }
286
+ }
@@ -0,0 +1,187 @@
1
+ // Directory/File browser logic for explorer with right-side preview and URL sync (using /path)
2
+ let explorerView = localStorage.getItem('explorerView') || 'list';
3
+ let currentExplorerPath = '.';
4
+
5
+ function getPaiPath(path) {
6
+ // Normalize slashes
7
+ path = (path || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
8
+ if (!path || path === '.' || path === '') return '.';
9
+ const parts = path.split('/');
10
+ if (parts.length <= 1) return '.';
11
+ parts.pop();
12
+ const parent = parts.join('/');
13
+ return parent === '' ? '.' : parent;
14
+ }
15
+
16
+ function setExplorerView(view) {
17
+ explorerView = view;
18
+ localStorage.setItem('explorerView', view);
19
+ document.getElementById('view-list').classList.toggle('active', view === 'list');
20
+ document.getElementById('view-icons').classList.toggle('active', view === 'icons');
21
+ }
22
+
23
+ function normalizeExplorerPath(path) {
24
+ if (!path || path === '/' || path === '' || path === '.') return '.';
25
+ return path.replace(/^\/+|\/+$/g, '');
26
+ }
27
+
28
+ function updateExplorerUrl(path, push=true) {
29
+ let url = '/';
30
+ if (path && path !== '.' && path !== '/') {
31
+ url = '/' + path.replace(/^\/+|\/+$/g, '');
32
+ }
33
+ if (push) {
34
+ window.history.pushState({ explorerPath: path }, '', url);
35
+ }
36
+ }
37
+
38
+ function renderExplorer(path, pushUrl=true) {
39
+ currentExplorerPath = normalizeExplorerPath(path);
40
+ fetch(`/api/explorer/${encodeURIComponent(currentExplorerPath)}`)
41
+ .then(resp => resp.json())
42
+ .then(data => {
43
+ const main = document.getElementById('explorer-main');
44
+ if (!main) return;
45
+ if (data.error) {
46
+ main.innerHTML = `<div class='error'>${data.error}</div>`;
47
+ return;
48
+ }
49
+ if (data.type === 'dir') {
50
+ let html = `<h3>Diretório: ${data.path}</h3>`;
51
+ if (explorerView === 'list') {
52
+ html += `<ul class='explorer-list'>`;
53
+ if (data.path !== '.') {
54
+ const parent = getPaiPath(data.path);
55
+ html += `<li><a href='#' data-path='${parent}' class='explorer-link'><span class='explorer-entry'><span class='explorer-icon'>⬆️</span><span class='explorer-name'>(.. parent)</span></span></a></li>`;
56
+ }
57
+ for (const entry of data.entries) {
58
+ const entryPath = data.path === '.' ? entry.name : data.path + '/' + entry.name;
59
+ if (entry.is_dir) {
60
+ html += `<li><a href='#' data-path='${entryPath}' class='explorer-link'><span class='explorer-entry'><span class='explorer-icon'>📁</span><span class='explorer-name'>${entry.name}</span></span></a></li>`;
61
+ } else {
62
+ html += `<li><a href='#' data-path='${entryPath}' class='explorer-link file-link'><span class='explorer-entry'><span class='explorer-icon'>📄</span><span class='explorer-name'>${entry.name}</span></span></a></li>`;
63
+ }
64
+ }
65
+ html += '</ul>';
66
+ } else {
67
+ html += `<div class='explorer-icons'>`;
68
+ if (data.path !== '.') {
69
+ const parent = getPaiPath(data.path);
70
+ html += `<div class='explorer-icon'><a href='#' data-path='${parent}' class='explorer-link' title='Pai'>(..)</a></div>`;
71
+ }
72
+ for (const entry of data.entries) {
73
+ const entryPath = data.path === '.' ? entry.name : data.path + '/' + entry.name;
74
+ if (entry.is_dir) {
75
+ html += `<div class='explorer-icon'><a href='#' data-path='${entryPath}' class='explorer-link' title='${entry.name}'>📁<br>${entry.name}</a></div>`;
76
+ } else {
77
+ html += `<div class='explorer-icon'><a href='#' data-path='${entryPath}' class='explorer-link file-link' title='${entry.name}'>📄<br>${entry.name}</a></div>`;
78
+ }
79
+ }
80
+ html += '</div>';
81
+ }
82
+ main.innerHTML = html;
83
+ // Clear preview panel when changing directories
84
+ const preview = document.getElementById('explorer-preview');
85
+ if (preview) preview.innerHTML = '';
86
+ if (pushUrl) updateExplorerUrl(currentExplorerPath, true);
87
+ }
88
+ // Attach click handlers
89
+ document.querySelectorAll('.explorer-link').forEach(link => {
90
+ link.onclick = function(e) {
91
+ e.preventDefault();
92
+ const p = this.getAttribute('data-path');
93
+ // If file, show preview; if dir, update explorer
94
+ if (this.classList.contains('file-link')) {
95
+ const preview = document.getElementById('explorer-preview');
96
+ if (preview) {
97
+ preview.innerHTML = `<div class='spinner' style='display:inline-block;vertical-align:middle;'></div> <span style='vertical-align:middle;'>Carregando arquivo...</span>`;
98
+ }
99
+ fetch(`/api/explorer/${encodeURIComponent(p)}`)
100
+ .then(resp => resp.json())
101
+ .then(fileData => {
102
+ if (preview && fileData.type === 'file') {
103
+ if (window.renderCodePreview) {
104
+ preview.innerHTML = `<h3>Arquivo: ${fileData.path}</h3><div id='explorer-codemirror-preview'></div>`;
105
+ window.renderCodePreview(document.getElementById('explorer-codemirror-preview'), fileData.content, 'python');
106
+ } else {
107
+ preview.innerHTML = `<h3>Arquivo: ${fileData.path}</h3><pre class='explorer-file'>${escapeHtml(fileData.content)}</pre>`;
108
+ }
109
+ }
110
+ });
111
+ } else {
112
+ renderExplorer(p, true);
113
+ }
114
+ };
115
+ });
116
+ });
117
+ }
118
+
119
+ function escapeHtml(text) {
120
+ if (!text) return '';
121
+ return text.replace(/[&<>"']/g, function (c) {
122
+ return {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'}[c];
123
+ });
124
+ }
125
+
126
+ // Patch: Use CodeMirror for explorer preview in read-only mode
127
+ window.renderCodePreview = function(container, content, mode) {
128
+ if (!container) return;
129
+ container.innerHTML = '';
130
+ try {
131
+ var textarea = document.createElement('textarea');
132
+ textarea.value = (typeof content === 'string') ? content : '';
133
+ container.appendChild(textarea);
134
+ if (window.CodeMirror && container.offsetPai !== null) {
135
+ var editor = CodeMirror.fromTextArea(textarea, {
136
+ lineNumbers: true,
137
+ mode: mode || 'python',
138
+ theme: (document.body.classList.contains('light-theme') ? 'default' : 'dracula'),
139
+ readOnly: true,
140
+ indentUnit: 4,
141
+ tabSize: 4,
142
+ });
143
+ editor.setSize('100%', 'calc(60vh)');
144
+ return editor;
145
+ } else {
146
+ container.innerHTML = '<pre>' + (content ? String(content) : '') + '</pre>';
147
+ }
148
+ } catch (e) {
149
+ container.innerHTML = '<pre>' + (content ? String(content) : '') + '</pre>';
150
+ }
151
+ }
152
+
153
+ // Theme switcher logic
154
+ function setTheme(dark) {
155
+ if (dark) {
156
+ document.body.classList.add('dark-theme');
157
+ document.body.classList.remove('light-theme');
158
+ localStorage.setItem('theme', 'dark');
159
+ document.getElementById('theme-switcher').textContent = 'Switch to Light Theme';
160
+ } else {
161
+ document.body.classList.remove('dark-theme');
162
+ document.body.classList.add('light-theme');
163
+ localStorage.setItem('theme', 'light');
164
+ document.getElementById('theme-switcher').textContent = 'Switch to Dark Theme';
165
+ }
166
+ }
167
+ document.addEventListener('DOMContentLoaded', function() {
168
+ // Initial theme
169
+ var theme = localStorage.getItem('theme') || 'dark';
170
+ setTheme(theme === 'dark');
171
+ document.getElementById('theme-switcher').onclick = function() {
172
+ setTheme(document.body.classList.contains('light-theme'));
173
+ };
174
+ renderExplorer('.')
175
+ setExplorerView(localStorage.getItem('explorerView') || 'list');
176
+ document.getElementById('view-list').onclick = function() {
177
+ setExplorerView('list');
178
+ renderExplorer('.')
179
+ };
180
+ document.getElementById('view-icons').onclick = function() {
181
+ setExplorerView('icons');
182
+ renderExplorer('.')
183
+ };
184
+ });
185
+
186
+ window.renderExplorer = renderExplorer;
187
+ window.setExplorerView = setExplorerView;
@@ -0,0 +1,187 @@
1
+ // Directory/File browser logic for explorer with right-side preview and URL sync (using /path)
2
+ let explorerView = localStorage.getItem('explorerView') || 'list';
3
+ let currentExplorerPath = '.';
4
+
5
+ function getPaiPath(path) {
6
+ // Normalize slashes
7
+ path = (path || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
8
+ if (!path || path === '.' || path === '') return '.';
9
+ const parts = path.split('/');
10
+ if (parts.length <= 1) return '.';
11
+ parts.pop();
12
+ const parent = parts.join('/');
13
+ return parent === '' ? '.' : parent;
14
+ }
15
+
16
+ function setExplorerView(view) {
17
+ explorerView = view;
18
+ localStorage.setItem('explorerView', view);
19
+ document.getElementById('view-list').classList.toggle('active', view === 'list');
20
+ document.getElementById('view-icons').classList.toggle('active', view === 'icons');
21
+ }
22
+
23
+ function normalizeExplorerPath(path) {
24
+ if (!path || path === '/' || path === '' || path === '.') return '.';
25
+ return path.replace(/^\/+|\/+$/g, '');
26
+ }
27
+
28
+ function updateExplorerUrl(path, push=true) {
29
+ let url = '/';
30
+ if (path && path !== '.' && path !== '/') {
31
+ url = '/' + path.replace(/^\/+|\/+$/g, '');
32
+ }
33
+ if (push) {
34
+ window.history.pushState({ explorerPath: path }, '', url);
35
+ }
36
+ }
37
+
38
+ function renderExplorer(path, pushUrl=true) {
39
+ currentExplorerPath = normalizeExplorerPath(path);
40
+ fetch(`/api/explorer/${encodeURIComponent(currentExplorerPath)}`)
41
+ .then(resp => resp.json())
42
+ .then(data => {
43
+ const main = document.getElementById('explorer-main');
44
+ if (!main) return;
45
+ if (data.error) {
46
+ main.innerHTML = `<div class='error'>${data.error}</div>`;
47
+ return;
48
+ }
49
+ if (data.type === 'dir') {
50
+ let html = `<h3>Diretório: ${data.path}</h3>`;
51
+ if (explorerView === 'list') {
52
+ html += `<ul class='explorer-list'>`;
53
+ if (data.path !== '.') {
54
+ const parent = getPaiPath(data.path);
55
+ html += `<li><a href='#' data-path='${parent}' class='explorer-link'><span class='explorer-entry'><span class='explorer-icon'>\u2b06\ufe0f</span><span class='explorer-name'>(.. parent)</span></span></a></li>`;
56
+ }
57
+ for (const entry of data.entries) {
58
+ const entryPath = data.path === '.' ? entry.name : data.path + '/' + entry.name;
59
+ if (entry.is_dir) {
60
+ html += `<li><a href='#' data-path='${entryPath}' class='explorer-link'><span class='explorer-entry'><span class='explorer-icon'>\ud83d\udcc1</span><span class='explorer-name'>${entry.name}</span></span></a></li>`;
61
+ } else {
62
+ html += `<li><a href='#' data-path='${entryPath}' class='explorer-link file-link'><span class='explorer-entry'><span class='explorer-icon'>\ud83d\udcc4</span><span class='explorer-name'>${entry.name}</span></span></a></li>`;
63
+ }
64
+ }
65
+ html += '</ul>';
66
+ } else {
67
+ html += `<div class='explorer-icons'>`;
68
+ if (data.path !== '.') {
69
+ const parent = getPaiPath(data.path);
70
+ html += `<div class='explorer-icon'><a href='#' data-path='${parent}' class='explorer-link' title='Pai'>(..)</a></div>`;
71
+ }
72
+ for (const entry of data.entries) {
73
+ const entryPath = data.path === '.' ? entry.name : data.path + '/' + entry.name;
74
+ if (entry.is_dir) {
75
+ html += `<div class='explorer-icon'><a href='#' data-path='${entryPath}' class='explorer-link' title='${entry.name}'>\ud83d\udcc1<br>${entry.name}</a></div>`;
76
+ } else {
77
+ html += `<div class='explorer-icon'><a href='#' data-path='${entryPath}' class='explorer-link file-link' title='${entry.name}'>\ud83d\udcc4<br>${entry.name}</a></div>`;
78
+ }
79
+ }
80
+ html += '</div>';
81
+ }
82
+ main.innerHTML = html;
83
+ // Clear preview panel when changing directories
84
+ const preview = document.getElementById('explorer-preview');
85
+ if (preview) preview.innerHTML = '';
86
+ if (pushUrl) updateExplorerUrl(currentExplorerPath, true);
87
+ }
88
+ // Attach click handlers
89
+ document.querySelectorAll('.explorer-link').forEach(link => {
90
+ link.onclick = function(e) {
91
+ e.preventDefault();
92
+ const p = this.getAttribute('data-path');
93
+ // If file, show preview; if dir, update explorer
94
+ if (this.classList.contains('file-link')) {
95
+ const preview = document.getElementById('explorer-preview');
96
+ if (preview) {
97
+ preview.innerHTML = `<div class='spinner' style='display:inline-block;vertical-align:middle;'></div> <span style='vertical-align:middle;'>Carregando arquivo...</span>`;
98
+ }
99
+ fetch(`/api/explorer/${encodeURIComponent(p)}`)
100
+ .then(resp => resp.json())
101
+ .then(fileData => {
102
+ if (preview && fileData.type === 'file') {
103
+ if (window.renderCodePreview) {
104
+ preview.innerHTML = `<h3>Arquivo: ${fileData.path}</h3><div id='explorer-codemirror-preview'></div>`;
105
+ window.renderCodePreview(document.getElementById('explorer-codemirror-preview'), fileData.content, 'python');
106
+ } else {
107
+ preview.innerHTML = `<h3>Arquivo: ${fileData.path}</h3><pre class='explorer-file'>${escapeHtml(fileData.content)}</pre>`;
108
+ }
109
+ }
110
+ });
111
+ } else {
112
+ renderExplorer(p, true);
113
+ }
114
+ };
115
+ });
116
+ });
117
+ }
118
+
119
+ function escapeHtml(text) {
120
+ if (!text) return '';
121
+ return text.replace(/[&<>"']/g, function (c) {
122
+ return {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'}[c];
123
+ });
124
+ }
125
+
126
+ // Patch: Use CodeMirror for explorer preview in read-only mode
127
+ window.renderCodePreview = function(container, content, mode) {
128
+ if (!container) return;
129
+ container.innerHTML = '';
130
+ try {
131
+ var textarea = document.createElement('textarea');
132
+ textarea.value = (typeof content === 'string') ? content : '';
133
+ container.appendChild(textarea);
134
+ if (window.CodeMirror && container.offsetPai !== null) {
135
+ var editor = CodeMirror.fromTextArea(textarea, {
136
+ lineNumbers: true,
137
+ mode: mode || 'python',
138
+ theme: (document.body.classList.contains('light-theme') ? 'default' : 'dracula'),
139
+ readOnly: true,
140
+ indentUnit: 4,
141
+ tabSize: 4,
142
+ });
143
+ editor.setSize('100%', 'calc(60vh)');
144
+ return editor;
145
+ } else {
146
+ container.innerHTML = '<pre>' + (content ? String(content) : '') + '</pre>';
147
+ }
148
+ } catch (e) {
149
+ container.innerHTML = '<pre>' + (content ? String(content) : '') + '</pre>';
150
+ }
151
+ }
152
+
153
+ // Theme switcher logic
154
+ function setTheme(dark) {
155
+ if (dark) {
156
+ document.body.classList.add('dark-theme');
157
+ document.body.classList.remove('light-theme');
158
+ localStorage.setItem('theme', 'dark');
159
+ document.getElementById('theme-switcher').textContent = 'Switch to Light Theme';
160
+ } else {
161
+ document.body.classList.remove('dark-theme');
162
+ document.body.classList.add('light-theme');
163
+ localStorage.setItem('theme', 'light');
164
+ document.getElementById('theme-switcher').textContent = 'Switch to Dark Theme';
165
+ }
166
+ }
167
+ document.addEventListener('DOMContentLoaded', function() {
168
+ // Initial theme
169
+ var theme = localStorage.getItem('theme') || 'dark';
170
+ setTheme(theme === 'dark');
171
+ document.getElementById('theme-switcher').onclick = function() {
172
+ setTheme(document.body.classList.contains('light-theme'));
173
+ };
174
+ renderExplorer('.')
175
+ setExplorerView(localStorage.getItem('explorerView') || 'list');
176
+ document.getElementById('view-list').onclick = function() {
177
+ setExplorerView('list');
178
+ renderExplorer('.')
179
+ };
180
+ document.getElementById('view-icons').onclick = function() {
181
+ setExplorerView('icons');
182
+ renderExplorer('.')
183
+ };
184
+ });
185
+
186
+ window.renderExplorer = renderExplorer;
187
+ window.setExplorerView = setExplorerView;