@bahulam/code 0.1.6 → 0.1.7

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +231 -53
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. package/pulse/types/claude.ts +0 -294
@@ -0,0 +1,2644 @@
1
+ /**
2
+ * Bahulam local service.
3
+ *
4
+ * Localhost-only HTTP surface for browser workspaces. This module intentionally
5
+ * does not depend on the terminal REPL daemon.
6
+ */
7
+
8
+ import * as crypto from 'node:crypto';
9
+ import * as fs from 'node:fs';
10
+ import * as http from 'node:http';
11
+ import * as path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { TarangAuth } from '../auth/tarang-auth.mjs';
14
+ import { resolveWebUrl } from '../core/backend-url.mjs';
15
+ import { LocalAgentRelay } from './agent-relay.mjs';
16
+ import {
17
+ DEFAULT_MAX_RAW_BYTES,
18
+ contentTypeForPath,
19
+ listWorkspacePath,
20
+ readWorkspaceFile,
21
+ resolveWorkspacePath,
22
+ } from './file-access.mjs';
23
+ import {
24
+ createOfficePdfPreview,
25
+ createSpreadsheetPreview,
26
+ resolvePreviewCacheFile,
27
+ } from './preview-converters.mjs';
28
+ import {
29
+ assertLoopbackRequest,
30
+ getLocalMachineIdentity,
31
+ loopbackHostForUrl,
32
+ normalizeLoopbackHost,
33
+ } from './machine.mjs';
34
+ import {
35
+ loadLocalWorkspaceSession,
36
+ publicSession,
37
+ touchLocalWorkspaceSession,
38
+ verifyLocalAccessToken,
39
+ } from './session-store.mjs';
40
+
41
+ const LOCAL_UPLOAD_DIR = 'bahulam-uploads';
42
+ const DEFAULT_MAX_UPLOAD_FILES = 20;
43
+ const DEFAULT_MAX_UPLOAD_FILE_BYTES = DEFAULT_MAX_RAW_BYTES;
44
+ const DEFAULT_MAX_UPLOAD_BODY_BYTES = 140 * 1024 * 1024;
45
+ const DEFAULT_MAX_SAVE_BYTES = 2 * 1024 * 1024;
46
+
47
+ export async function startLocalWorkspaceService({
48
+ session,
49
+ token,
50
+ port = 0,
51
+ host = '127.0.0.1',
52
+ } = {}) {
53
+ if (!session?.id) throw new Error('session is required');
54
+ if (!token) throw new Error('token is required');
55
+
56
+ const loopbackHost = normalizeLoopbackHost(host);
57
+ const machine = getLocalMachineIdentity();
58
+ const fullSession = loadLocalWorkspaceSession(session.id);
59
+ if (!fullSession) throw new Error(`Local workspace session not found: ${session.id}`);
60
+ if (fullSession.machine_id && fullSession.machine_id !== machine.id) {
61
+ const err = new Error('Local workspace session belongs to a different machine');
62
+ err.code = 'LOCAL_ONLY';
63
+ throw err;
64
+ }
65
+
66
+ const events = [];
67
+ const sseClients = new Set();
68
+ let agentRelay = null;
69
+
70
+ function emit(type, data = {}) {
71
+ const event = {
72
+ id: `evt_${Date.now().toString(36)}_${crypto.randomBytes(3).toString('hex')}`,
73
+ ts: new Date().toISOString(),
74
+ type,
75
+ session_id: session.id,
76
+ data,
77
+ };
78
+ events.push(event);
79
+ if (events.length > 500) events.shift();
80
+ const frame = `event: ${type}\ndata: ${JSON.stringify(event)}\n\n`;
81
+ for (const res of sseClients) {
82
+ try {
83
+ res.write(frame);
84
+ } catch {
85
+ sseClients.delete(res);
86
+ }
87
+ }
88
+ return event;
89
+ }
90
+
91
+ const server = http.createServer(async (req, res) => {
92
+ try {
93
+ assertLoopbackRequest(req);
94
+ await routeRequest({
95
+ req,
96
+ res,
97
+ sessionId: session.id,
98
+ token,
99
+ events,
100
+ sseClients,
101
+ emit,
102
+ getAgentRelay(localSession) {
103
+ if (!agentRelay) agentRelay = new LocalAgentRelay({ session: localSession, emit });
104
+ return agentRelay;
105
+ },
106
+ });
107
+ } catch (err) {
108
+ sendJson(res, errorStatus(err), {
109
+ ok: false,
110
+ error: err.code || 'local_service_error',
111
+ message: err.message,
112
+ });
113
+ }
114
+ });
115
+
116
+ await new Promise((resolve, reject) => {
117
+ server.listen(Number(port) || 0, loopbackHost, () => resolve());
118
+ server.once('error', reject);
119
+ });
120
+
121
+ const address = server.address();
122
+ const actualPort = typeof address === 'object' && address ? address.port : port;
123
+ const url = `http://${loopbackHostForUrl(loopbackHost)}:${actualPort}/workspace/${encodeURIComponent(session.id)}?token=${encodeURIComponent(token)}`;
124
+ touchLocalWorkspaceSession(session.id, {
125
+ status: 'active',
126
+ local_url: url,
127
+ port: actualPort,
128
+ host: loopbackHost,
129
+ pid: process.pid,
130
+ });
131
+ emit('session_started', { url, root_path: session.root_path, focus_path: session.focus_path });
132
+
133
+ return {
134
+ url,
135
+ port: actualPort,
136
+ host: loopbackHost,
137
+ close: () => new Promise((resolve) => {
138
+ emit('session_stopped', {});
139
+ const relay = agentRelay;
140
+ for (const client of sseClients) {
141
+ try {
142
+ client.end();
143
+ } catch {}
144
+ }
145
+ sseClients.clear();
146
+ server.close(async () => {
147
+ await relay?.close?.();
148
+ resolve();
149
+ });
150
+ }),
151
+ emit,
152
+ };
153
+ }
154
+
155
+ async function routeRequest({ req, res, sessionId, token, events, sseClients, emit, getAgentRelay }) {
156
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
157
+
158
+ if (req.method === 'GET' && url.pathname === '/health') {
159
+ sendJson(res, 200, {
160
+ ok: true,
161
+ product: 'bahulam-local-service',
162
+ session_id: sessionId,
163
+ });
164
+ return;
165
+ }
166
+
167
+ if (req.method === 'GET' && url.pathname.startsWith('/vendor/monaco/')) {
168
+ sendPackageAsset({
169
+ res,
170
+ root: fileURLToPath(new URL('../../node_modules/monaco-editor/min/', import.meta.url)),
171
+ pathname: url.pathname,
172
+ prefix: '/vendor/monaco/',
173
+ });
174
+ return;
175
+ }
176
+
177
+ if (req.method === 'GET' && url.pathname.startsWith('/vendor/mermaid/')) {
178
+ sendPackageAsset({
179
+ res,
180
+ root: fileURLToPath(new URL('../../node_modules/mermaid/dist/', import.meta.url)),
181
+ pathname: url.pathname,
182
+ prefix: '/vendor/mermaid/',
183
+ });
184
+ return;
185
+ }
186
+
187
+ if (req.method === 'GET' && url.pathname.startsWith('/vendor/katex/')) {
188
+ sendPackageAsset({
189
+ res,
190
+ root: fileURLToPath(new URL('../../node_modules/katex/dist/', import.meta.url)),
191
+ pathname: url.pathname,
192
+ prefix: '/vendor/katex/',
193
+ });
194
+ return;
195
+ }
196
+
197
+ if (req.method === 'GET' && url.pathname === '/assets/bahulam-mark.png') {
198
+ sendBrandMark(res);
199
+ return;
200
+ }
201
+
202
+ const session = requireAuthorizedSession(sessionId, token, req, url);
203
+
204
+ if (req.method === 'GET' && (url.pathname === `/workspace/${sessionId}` || url.pathname === '/')) {
205
+ sendHtml(res, workspaceHtml({ session, token }));
206
+ return;
207
+ }
208
+
209
+ if (req.method === 'GET' && url.pathname === '/api/session') {
210
+ sendJson(res, 200, { ok: true, session });
211
+ return;
212
+ }
213
+
214
+ if (req.method === 'GET' && url.pathname === '/api/chat/history') {
215
+ const relay = getAgentRelay(session);
216
+ const history = relay.currentHistory();
217
+ sendJson(res, 200, history);
218
+ return;
219
+ }
220
+
221
+ if (req.method === 'GET' && url.pathname === '/api/chat/sessions') {
222
+ const relay = getAgentRelay(session);
223
+ const historySessions = await relay.listHistorySessions();
224
+ sendJson(res, 200, historySessions);
225
+ return;
226
+ }
227
+
228
+ if (req.method === 'POST' && url.pathname === '/api/chat/resume') {
229
+ const body = await readJsonBody(req);
230
+ const relay = getAgentRelay(session);
231
+ const history = await relay.resumeHistory(body.session_id || body.sessionId);
232
+ sendJson(res, 200, history);
233
+ return;
234
+ }
235
+
236
+ if (req.method === 'POST' && url.pathname === '/api/chat/new') {
237
+ const relay = getAgentRelay(session);
238
+ const history = await relay.startNewHistory();
239
+ sendJson(res, 200, history);
240
+ return;
241
+ }
242
+
243
+ if (req.method === 'GET' && url.pathname === '/api/approvals/mode') {
244
+ const relay = getAgentRelay(session);
245
+ sendJson(res, 200, relay.approvalMode());
246
+ return;
247
+ }
248
+
249
+ if (req.method === 'POST' && url.pathname === '/api/approvals/mode') {
250
+ const body = await readJsonBody(req);
251
+ const relay = getAgentRelay(session);
252
+ const result = relay.setApprovalAutoMode(Boolean(body.auto ?? body.enabled));
253
+ sendJson(res, 200, result);
254
+ return;
255
+ }
256
+
257
+ if (req.method === 'POST' && url.pathname.startsWith('/api/approvals/')) {
258
+ const approvalId = decodeURIComponent(url.pathname.slice('/api/approvals/'.length));
259
+ const body = await readJsonBody(req);
260
+ const relay = getAgentRelay(session);
261
+ const result = relay.decideApproval(approvalId, body);
262
+ sendJson(res, 200, result);
263
+ return;
264
+ }
265
+
266
+ if (req.method === 'GET' && url.pathname === '/api/files') {
267
+ const requestedPath = url.searchParams.get('path') || session.focus_path || '.';
268
+ const listing = listWorkspacePath(session, requestedPath);
269
+ emit('file_browsed', { path: listing.path, type: listing.type });
270
+ sendJson(res, 200, listing);
271
+ return;
272
+ }
273
+
274
+ if (req.method === 'POST' && url.pathname === '/api/files/upload') {
275
+ const body = await readJsonBody(req, envInt('BAHULAM_LOCAL_UPLOAD_MAX_BODY_BYTES', DEFAULT_MAX_UPLOAD_BODY_BYTES));
276
+ const result = saveUploadedFiles(session, body);
277
+ emit('file_uploaded', {
278
+ count: result.files.length,
279
+ directory: result.directory,
280
+ files: result.files.map((file) => ({
281
+ name: file.name,
282
+ path: file.path,
283
+ size: file.size,
284
+ viewer: file.viewer,
285
+ kind: file.kind,
286
+ })),
287
+ });
288
+ sendJson(res, 200, result);
289
+ return;
290
+ }
291
+
292
+ if (req.method === 'POST' && url.pathname === '/api/file/save') {
293
+ const body = await readJsonBody(req, envInt('BAHULAM_LOCAL_SAVE_MAX_BODY_BYTES', DEFAULT_MAX_SAVE_BYTES + 64 * 1024));
294
+ const result = saveWorkspaceTextFile(session, body);
295
+ emit('file_saved', {
296
+ path: result.path,
297
+ size: result.file?.size ?? null,
298
+ viewer: result.file?.viewer ?? null,
299
+ kind: result.file?.kind ?? null,
300
+ });
301
+ sendJson(res, 200, result);
302
+ return;
303
+ }
304
+
305
+ if (req.method === 'GET' && url.pathname === '/api/file/raw') {
306
+ const requestedPath = url.searchParams.get('path') || session.focus_path;
307
+ if (!requestedPath) {
308
+ const err = new Error('path is required');
309
+ err.code = 'BAD_REQUEST';
310
+ throw err;
311
+ }
312
+ const bytes = readWorkspaceFile(session, requestedPath, { maxBytes: DEFAULT_MAX_RAW_BYTES });
313
+ emit('file_read', { path: requestedPath, bytes: bytes.length });
314
+ res.writeHead(200, {
315
+ 'Content-Type': contentTypeForPath(requestedPath),
316
+ 'Cache-Control': 'no-store',
317
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:",
318
+ 'X-Content-Type-Options': 'nosniff',
319
+ });
320
+ res.end(bytes);
321
+ return;
322
+ }
323
+
324
+ if (req.method === 'GET' && url.pathname === '/api/file/spreadsheet-preview') {
325
+ const requestedPath = url.searchParams.get('path') || session.focus_path;
326
+ if (!requestedPath) {
327
+ const err = new Error('path is required');
328
+ err.code = 'BAD_REQUEST';
329
+ throw err;
330
+ }
331
+ const preview = createSpreadsheetPreview(session, requestedPath);
332
+ emit('file_previewed', { path: requestedPath, viewer: 'spreadsheet', sheets: preview.sheet_count });
333
+ sendJson(res, 200, preview);
334
+ return;
335
+ }
336
+
337
+ if (req.method === 'GET' && url.pathname === '/api/file/office-preview') {
338
+ const requestedPath = url.searchParams.get('path') || session.focus_path;
339
+ if (!requestedPath) {
340
+ const err = new Error('path is required');
341
+ err.code = 'BAD_REQUEST';
342
+ throw err;
343
+ }
344
+ const preview = createOfficePdfPreview(session, requestedPath);
345
+ if (preview.ok) {
346
+ preview.preview_url = `/api/preview-cache/${encodeURIComponent(preview.cache_id)}/${encodeURIComponent(preview.file_name)}?token=${encodeURIComponent(token)}`;
347
+ }
348
+ emit('file_previewed', { path: requestedPath, viewer: 'office_pdf', ok: preview.ok, code: preview.code || null });
349
+ sendJson(res, 200, preview);
350
+ return;
351
+ }
352
+
353
+ if (req.method === 'GET' && url.pathname.startsWith('/api/preview-cache/')) {
354
+ const cachePath = decodeURIComponent(url.pathname.slice('/api/preview-cache/'.length));
355
+ const asset = resolvePreviewCacheFile(session, cachePath);
356
+ res.writeHead(200, {
357
+ 'Content-Type': contentTypeForPath(asset.path),
358
+ 'Content-Length': asset.stat.size,
359
+ 'Cache-Control': 'private, max-age=3600',
360
+ 'X-Content-Type-Options': 'nosniff',
361
+ });
362
+ fs.createReadStream(asset.path).pipe(res);
363
+ return;
364
+ }
365
+
366
+ if (req.method === 'GET' && url.pathname === '/api/events') {
367
+ res.writeHead(200, {
368
+ 'Content-Type': 'text/event-stream; charset=utf-8',
369
+ 'Cache-Control': 'no-store',
370
+ Connection: 'keep-alive',
371
+ });
372
+ sseClients.add(res);
373
+ for (const event of events.slice(-100)) {
374
+ res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
375
+ }
376
+ req.on('close', () => sseClients.delete(res));
377
+ return;
378
+ }
379
+
380
+ if (req.method === 'POST' && url.pathname === '/api/agent/turn') {
381
+ const body = await readJsonBody(req);
382
+ const prompt = String(body.prompt || '').slice(0, 200);
383
+ emit('agent_turn_requested', { prompt });
384
+ try {
385
+ const relay = getAgentRelay(session);
386
+ const result = await relay.runTurn({
387
+ prompt: body.prompt,
388
+ path: body.path || session.focus_path || '.',
389
+ attachments: body.attachments || [],
390
+ });
391
+ sendJson(res, 200, result);
392
+ } catch (err) {
393
+ emit('agent_error', {
394
+ code: err.code || 'agent_turn_failed',
395
+ message: err.message || String(err),
396
+ fatal: true,
397
+ });
398
+ throw err;
399
+ }
400
+ return;
401
+ }
402
+
403
+ if (req.method === 'POST' && url.pathname === '/api/agent/followup') {
404
+ const body = await readJsonBody(req);
405
+ const instruction = String(body.prompt || body.instruction || '').slice(0, 500);
406
+ emit('agent_followup_requested', { instruction });
407
+ const relay = getAgentRelay(session);
408
+ const result = await relay.sendFollowup({ instruction: body.prompt || body.instruction });
409
+ sendJson(res, 200, result);
410
+ return;
411
+ }
412
+
413
+ if (req.method === 'POST' && url.pathname === '/api/agent/cancel') {
414
+ const body = await readJsonBody(req);
415
+ const relay = getAgentRelay(session);
416
+ const result = await relay.cancelTurn(body.reason || 'Cancelled by user');
417
+ sendJson(res, 200, result);
418
+ return;
419
+ }
420
+
421
+ if (req.method === 'POST' && url.pathname === '/api/tools/execute') {
422
+ const body = await readJsonBody(req);
423
+ emit('tool_execution_requested', { name: body.name || null });
424
+ sendJson(res, 501, {
425
+ ok: false,
426
+ error: 'local_tool_bridge_not_wired',
427
+ message: 'Tool execution will be routed through the local service permission layer in the next slice.',
428
+ });
429
+ return;
430
+ }
431
+
432
+ sendJson(res, 404, { ok: false, error: 'not_found' });
433
+ }
434
+
435
+ function requireAuthorizedSession(sessionId, token, req, url) {
436
+ const session = loadLocalWorkspaceSession(sessionId);
437
+ if (!session) {
438
+ const err = new Error(`Unknown local workspace session: ${sessionId}`);
439
+ err.code = 'NOT_FOUND';
440
+ throw err;
441
+ }
442
+
443
+ const supplied = url.searchParams.get('token') || req.headers['x-bahulam-local-token'];
444
+ if (!verifyLocalAccessToken(session, supplied)) {
445
+ const err = new Error('Invalid or missing local workspace token');
446
+ err.code = 'UNAUTHORIZED';
447
+ throw err;
448
+ }
449
+ return publicSession(session);
450
+ }
451
+
452
+ async function readJsonBody(req, maxBytes = 1024 * 1024) {
453
+ const chunks = [];
454
+ let total = 0;
455
+ for await (const chunk of req) {
456
+ total += chunk.length;
457
+ if (total > maxBytes) {
458
+ const err = new Error('Request body is too large');
459
+ err.code = 'PAYLOAD_TOO_LARGE';
460
+ throw err;
461
+ }
462
+ chunks.push(chunk);
463
+ }
464
+ if (!chunks.length) return {};
465
+ try {
466
+ return JSON.parse(Buffer.concat(chunks).toString('utf-8'));
467
+ } catch {
468
+ const err = new Error('Request body must be valid JSON');
469
+ err.code = 'BAD_REQUEST';
470
+ throw err;
471
+ }
472
+ }
473
+
474
+ function envInt(name, fallback) {
475
+ const value = Number.parseInt(process.env[name] || '', 10);
476
+ return Number.isFinite(value) && value > 0 ? value : fallback;
477
+ }
478
+
479
+ function saveWorkspaceTextFile(session, body = {}) {
480
+ const requestedPath = String(body.path || '').trim();
481
+ const content = body.content;
482
+ if (!requestedPath) {
483
+ const err = new Error('path is required');
484
+ err.code = 'BAD_REQUEST';
485
+ throw err;
486
+ }
487
+ if (typeof content !== 'string') {
488
+ const err = new Error('content must be a string');
489
+ err.code = 'BAD_REQUEST';
490
+ throw err;
491
+ }
492
+
493
+ const before = listWorkspacePath(session, requestedPath);
494
+ if (before.type !== 'file') {
495
+ const err = new Error('Requested path is not a file');
496
+ err.code = 'NOT_FILE';
497
+ throw err;
498
+ }
499
+ if (!before.file?.text_like || !before.preview) {
500
+ const err = new Error('Only text-backed files can be edited in the browser preview');
501
+ err.code = 'BAD_REQUEST';
502
+ throw err;
503
+ }
504
+ if (before.preview.truncated) {
505
+ const err = new Error('Large truncated previews are read-only in the browser');
506
+ err.code = 'BAD_REQUEST';
507
+ throw err;
508
+ }
509
+
510
+ const bytes = Buffer.byteLength(content, 'utf8');
511
+ if (bytes > envInt('BAHULAM_LOCAL_SAVE_MAX_BYTES', DEFAULT_MAX_SAVE_BYTES)) {
512
+ const err = new Error(`Saved content exceeds ${envInt('BAHULAM_LOCAL_SAVE_MAX_BYTES', DEFAULT_MAX_SAVE_BYTES)} bytes`);
513
+ err.code = 'PAYLOAD_TOO_LARGE';
514
+ throw err;
515
+ }
516
+
517
+ const target = resolveWorkspacePath(session, requestedPath);
518
+ fs.writeFileSync(target, content, 'utf8');
519
+ return listWorkspacePath(session, requestedPath);
520
+ }
521
+
522
+ function saveUploadedFiles(session, body = {}) {
523
+ const files = Array.isArray(body.files) ? body.files : [];
524
+ if (!files.length) {
525
+ const err = new Error('files[] is required');
526
+ err.code = 'BAD_REQUEST';
527
+ throw err;
528
+ }
529
+ if (files.length > DEFAULT_MAX_UPLOAD_FILES) {
530
+ const err = new Error(`Too many files; max is ${DEFAULT_MAX_UPLOAD_FILES}`);
531
+ err.code = 'BAD_REQUEST';
532
+ throw err;
533
+ }
534
+
535
+ const root = fs.realpathSync(session.root_path);
536
+ const group = `${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}-${crypto.randomBytes(3).toString('hex')}`;
537
+ const directory = path.posix.join(LOCAL_UPLOAD_DIR, safePathSegment(session.id), group);
538
+ const uploadDir = path.join(root, ...directory.split('/'));
539
+ fs.mkdirSync(uploadDir, { recursive: true });
540
+
541
+ const uploaded = files.map((file, index) => {
542
+ const name = safeFileName(file?.name || `upload-${index + 1}.bin`);
543
+ const encoded = normalizeBase64Payload(file?.data_base64 || file?.base64 || '');
544
+ if (!encoded) {
545
+ const err = new Error(`Uploaded file ${name} is empty or missing data_base64`);
546
+ err.code = 'BAD_REQUEST';
547
+ throw err;
548
+ }
549
+ let bytes;
550
+ try {
551
+ bytes = Buffer.from(encoded, 'base64');
552
+ } catch {
553
+ const err = new Error(`Uploaded file ${name} is not valid base64`);
554
+ err.code = 'BAD_REQUEST';
555
+ throw err;
556
+ }
557
+ if (!bytes.length) {
558
+ const err = new Error(`Uploaded file ${name} is empty`);
559
+ err.code = 'BAD_REQUEST';
560
+ throw err;
561
+ }
562
+ if (bytes.length > DEFAULT_MAX_UPLOAD_FILE_BYTES) {
563
+ const err = new Error(`Uploaded file ${name} exceeds ${DEFAULT_MAX_UPLOAD_FILE_BYTES} bytes`);
564
+ err.code = 'PAYLOAD_TOO_LARGE';
565
+ throw err;
566
+ }
567
+
568
+ const target = uniqueUploadTarget(uploadDir, name);
569
+ fs.writeFileSync(target, bytes);
570
+ const rel = path.relative(root, target).split(path.sep).join('/');
571
+ const listing = listWorkspacePath(session, rel);
572
+ return {
573
+ ...listing.file,
574
+ mime_type: file?.mime_type || file?.type || contentTypeForPath(target),
575
+ upload_path: listing.path,
576
+ uploaded: true,
577
+ };
578
+ });
579
+
580
+ return {
581
+ ok: true,
582
+ directory,
583
+ files: uploaded,
584
+ };
585
+ }
586
+
587
+ function normalizeBase64Payload(value) {
588
+ const raw = String(value || '').trim();
589
+ if (!raw) return '';
590
+ const comma = raw.indexOf(',');
591
+ return raw.startsWith('data:') && comma >= 0 ? raw.slice(comma + 1) : raw;
592
+ }
593
+
594
+ function safePathSegment(value) {
595
+ return String(value || 'session').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'session';
596
+ }
597
+
598
+ function safeFileName(value) {
599
+ const base = path.basename(String(value || 'upload.bin')).replace(/[\u0000-\u001f\u007f]/g, '');
600
+ const safe = base.replace(/[\\/]+/g, '-').replace(/[^A-Za-z0-9._() -]+/g, '_').trim();
601
+ if (!safe || safe === '.' || safe === '..') return 'upload.bin';
602
+ return safe.length > 180 ? `${safe.slice(0, 120)}${path.extname(safe).slice(0, 40)}` : safe;
603
+ }
604
+
605
+ function uniqueUploadTarget(uploadDir, fileName) {
606
+ const parsed = path.parse(fileName);
607
+ for (let i = 0; i < 1000; i += 1) {
608
+ const suffix = i === 0 ? '' : `-${i + 1}`;
609
+ const candidate = path.join(uploadDir, `${parsed.name}${suffix}${parsed.ext}`);
610
+ if (!fs.existsSync(candidate)) return candidate;
611
+ }
612
+ return path.join(uploadDir, `${parsed.name}-${crypto.randomBytes(4).toString('hex')}${parsed.ext}`);
613
+ }
614
+
615
+ function sendJson(res, status, body) {
616
+ res.writeHead(status, {
617
+ 'Content-Type': 'application/json; charset=utf-8',
618
+ 'Cache-Control': 'no-store',
619
+ 'X-Content-Type-Options': 'nosniff',
620
+ });
621
+ res.end(JSON.stringify(body));
622
+ }
623
+
624
+ function sendHtml(res, html) {
625
+ res.writeHead(200, {
626
+ 'Content-Type': 'text/html; charset=utf-8',
627
+ 'Cache-Control': 'no-store',
628
+ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: blob:; frame-src 'self' blob:; media-src 'self' blob:; font-src 'self'; worker-src 'self' blob:",
629
+ 'X-Content-Type-Options': 'nosniff',
630
+ });
631
+ res.end(html);
632
+ }
633
+
634
+ function sendBrandMark(res) {
635
+ const candidates = [
636
+ '../../../appstak-platform/apps/bahulam/public/brand/bahulam-mark.png',
637
+ '../../../appstak-platform/apps/appstak/public/brand/bahulam-mark.png',
638
+ ].map((relative) => fileURLToPath(new URL(relative, import.meta.url)));
639
+
640
+ for (const file of candidates) {
641
+ try {
642
+ if (fs.existsSync(file)) {
643
+ res.writeHead(200, {
644
+ 'Content-Type': 'image/png',
645
+ 'Cache-Control': 'public, max-age=86400',
646
+ 'X-Content-Type-Options': 'nosniff',
647
+ });
648
+ res.end(fs.readFileSync(file));
649
+ return;
650
+ }
651
+ } catch {}
652
+ }
653
+
654
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" rx="10" fill="#1B1B1B"/><text x="20" y="25" text-anchor="middle" font-family="ui-monospace, Menlo, monospace" font-size="18" font-weight="800" fill="#FFFDF7">B</text></svg>';
655
+ res.writeHead(200, {
656
+ 'Content-Type': 'image/svg+xml; charset=utf-8',
657
+ 'Cache-Control': 'public, max-age=3600',
658
+ 'X-Content-Type-Options': 'nosniff',
659
+ });
660
+ res.end(svg);
661
+ }
662
+
663
+ function sendPackageAsset({ res, root, pathname, prefix }) {
664
+ let rel;
665
+ try {
666
+ rel = decodeURIComponent(pathname.slice(prefix.length));
667
+ } catch {
668
+ sendJson(res, 400, { ok: false, error: 'bad_asset_path' });
669
+ return;
670
+ }
671
+
672
+ if (!rel || rel.includes('\0')) {
673
+ sendJson(res, 404, { ok: false, error: 'asset_not_found' });
674
+ return;
675
+ }
676
+
677
+ const fullPath = path.resolve(root, rel);
678
+ const rootPath = path.resolve(root);
679
+ const relative = path.relative(rootPath, fullPath);
680
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
681
+ sendJson(res, 403, { ok: false, error: 'asset_outside_root' });
682
+ return;
683
+ }
684
+
685
+ try {
686
+ const stat = fs.statSync(fullPath);
687
+ if (!stat.isFile()) {
688
+ sendJson(res, 404, { ok: false, error: 'asset_not_found' });
689
+ return;
690
+ }
691
+ res.writeHead(200, {
692
+ 'Content-Type': staticContentType(fullPath),
693
+ 'Cache-Control': 'public, max-age=31536000, immutable',
694
+ 'X-Content-Type-Options': 'nosniff',
695
+ });
696
+ fs.createReadStream(fullPath).pipe(res);
697
+ } catch {
698
+ sendJson(res, 404, { ok: false, error: 'asset_not_found' });
699
+ }
700
+ }
701
+
702
+ function staticContentType(filePath) {
703
+ const ext = path.extname(filePath).toLowerCase();
704
+ switch (ext) {
705
+ case '.js':
706
+ case '.mjs': return 'text/javascript; charset=utf-8';
707
+ case '.css': return 'text/css; charset=utf-8';
708
+ case '.json':
709
+ case '.map': return 'application/json; charset=utf-8';
710
+ case '.svg': return 'image/svg+xml';
711
+ case '.png': return 'image/png';
712
+ case '.jpg':
713
+ case '.jpeg': return 'image/jpeg';
714
+ case '.gif': return 'image/gif';
715
+ case '.webp': return 'image/webp';
716
+ case '.ttf': return 'font/ttf';
717
+ case '.woff': return 'font/woff';
718
+ case '.woff2': return 'font/woff2';
719
+ default: return 'application/octet-stream';
720
+ }
721
+ }
722
+
723
+ function localAuthInfo() {
724
+ try {
725
+ const credentials = new TarangAuth().loadCredentials();
726
+ return {
727
+ authenticated: Boolean(credentials.token),
728
+ backendUrl: credentials.backendUrl || '',
729
+ modelMode: credentials.modelMode || '',
730
+ routePreference: credentials.routePreference || '',
731
+ };
732
+ } catch {
733
+ return {
734
+ authenticated: false,
735
+ backendUrl: '',
736
+ modelMode: '',
737
+ routePreference: '',
738
+ };
739
+ }
740
+ }
741
+
742
+ function joinUrl(base, pathSuffix) {
743
+ const root = String(base || 'https://bahulam.ai').replace(/\/$/, '');
744
+ const suffix = String(pathSuffix || '/');
745
+ return `${root}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
746
+ }
747
+
748
+ function errorStatus(err) {
749
+ switch (err.code) {
750
+ case 'UNAUTHORIZED': return 401;
751
+ case 'NOT_FOUND': return 404;
752
+ case 'OUTSIDE_WORKSPACE': return 403;
753
+ case 'LOCAL_ONLY': return 403;
754
+ case 'NOT_FILE': return 400;
755
+ case 'BAD_REQUEST': return 400;
756
+ case 'CONFLICT': return 409;
757
+ case 'auth_required': return 401;
758
+ case 'PAYLOAD_TOO_LARGE': return 413;
759
+ case 'FILE_TOO_LARGE': return 413;
760
+ default: return 500;
761
+ }
762
+ }
763
+
764
+ function workspaceHtml({ session, token }) {
765
+ const webUrl = resolveWebUrl();
766
+ const publicUrl = process.env.BAHULAM_PUBLIC_URL || webUrl;
767
+ const auth = localAuthInfo();
768
+ const dashboardUrl = joinUrl(webUrl, '/dashboard');
769
+ const loginUrl = joinUrl(webUrl, '/login?redirect=/dashboard');
770
+ const billingUrl = joinUrl(webUrl, '/dashboard/billing');
771
+ const pricingUrl = joinUrl(webUrl, '/pricing');
772
+ const cloudIdeUrl = joinUrl(webUrl, '/cloudide');
773
+ const authLabel = auth.authenticated ? 'CLI signed in' : 'CLI login needed';
774
+ const accountUrl = auth.authenticated ? billingUrl : loginUrl;
775
+ const accountLabel = auth.authenticated ? 'Account' : 'Login';
776
+ const boot = JSON.stringify({
777
+ session,
778
+ token,
779
+ initialPath: session.focus_path || '.',
780
+ }).replace(/</g, '\\u003c');
781
+ const title = escapeHtml(session.title || 'Local Workspace');
782
+ const rootName = escapeHtml(path.basename(session.root_path) || session.root_path);
783
+
784
+ return `<!doctype html>
785
+ <html lang="en">
786
+ <head>
787
+ <meta charset="utf-8">
788
+ <meta name="viewport" content="width=device-width,initial-scale=1">
789
+ <title>${title} - Bahulam Local</title>
790
+ <style>
791
+ :root{color-scheme:light;--ws-foreground:#1B1B1B;--ws-muted:rgba(27,27,27,.52);--ws-faint:rgba(27,27,27,.3);--ws-border:rgba(27,27,27,.10);--ws-border-subtle:rgba(27,27,27,.06);--ws-bg:#FFFDF7;--ws-surface:#F7F5EF;--ws-panel:#fff;--ws-tab:#FAF9F5;--ws-primary:#0891B2;--ws-hover:rgba(27,27,27,.04);--ws-active:rgba(27,27,27,.08);--ws-success:#047857;--ws-success-bg:#ECFDF5;--ws-error:#B91C1C;--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;--sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
792
+ *{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--ws-bg);color:var(--ws-foreground);font:13px/1.45 var(--sans);overflow:hidden}
793
+ button,textarea,input{font:inherit}button{color:inherit}.shell{display:flex;flex-direction:column;height:100vh;min-height:560px;background:var(--ws-bg)}
794
+ .global-bar{height:40px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(27,27,27,.08);background:#FFFDF7;padding:0 16px;flex:0 0 auto;min-width:0}
795
+ .dashboard-link{height:28px;display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;border-radius:6px;padding:0;color:rgba(27,27,27,.40);font-size:12px;text-decoration:none;cursor:pointer;transition:color .15s ease;white-space:nowrap}.dashboard-link:hover{color:rgba(27,27,27,.70)}.dashboard-link svg{width:14px;height:14px;flex:0 0 auto}.brand-divider{width:1px;height:16px;background:rgba(27,27,27,.10);flex:0 0 auto}
796
+ .ide-brand{height:28px;display:flex;align-items:center;gap:6px;text-decoration:none;color:var(--ws-foreground);white-space:nowrap}.brand-mark{width:20px;height:20px;border-radius:5px;object-fit:contain;display:block}.brand-title{font:800 12px/1 var(--mono);letter-spacing:-.01em;color:#1B1B1B}
797
+ .global-spacer{flex:1;min-width:10px}.global-links{display:flex;align-items:center;gap:10px;min-width:0;overflow:hidden}.global-link{color:rgba(27,27,27,.46);font-size:12px;text-decoration:none;white-space:nowrap}.global-link:hover{color:rgba(27,27,27,.78)}.auth-chip{height:22px;display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(27,27,27,.10);border-radius:999px;background:#fff;padding:0 8px;color:rgba(27,27,27,.46);font-size:11px;white-space:nowrap}.auth-chip:before{content:"";width:6px;height:6px;border-radius:999px;background:#A8A29E}.auth-chip.signed-in{border-color:#BBF7D0;background:#ECFDF5;color:#047857}.auth-chip.signed-in:before{background:#10B981}
798
+ .topbar{height:38px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(27,27,27,.06);background:rgba(255,255,255,.80);backdrop-filter:blur(10px);padding:0 16px;flex:0 0 auto}.terminal-icon{width:16px;height:16px;color:rgba(27,27,27,.30);flex:0 0 auto}.workspace-name{font-size:14px;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.workspace-kind{font-size:12px;color:rgba(27,27,27,.30)}.top-actions{margin-left:auto;display:flex;align-items:center;gap:6px;min-width:0}
799
+ .badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;border:1px solid var(--ws-border);background:#fff;padding:2px 8px;font-size:10px;font-weight:500;color:rgba(27,27,27,.58);white-space:nowrap}.badge.local{border-color:#BBF7D0;background:var(--ws-success-bg);color:var(--ws-success)}.badge.local:before{content:"";width:6px;height:6px;border-radius:999px;background:#10B981}
800
+ .icon-button{height:26px;min-width:26px;border:1px solid rgba(27,27,27,.10);border-radius:6px;background:#fff;color:rgba(27,27,27,.64);display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 8px;cursor:pointer;font-size:11px;font-weight:500;transition:background .15s ease,color .15s ease,border-color .15s ease}.icon-button:hover{background:#F7F5EF;color:var(--ws-foreground)}.icon-button[aria-pressed="true"]{background:rgba(27,27,27,.08);border-color:rgba(27,27,27,.10);color:#1B1B1B}.icon-button svg{width:14px;height:14px}
801
+ .auto-toggle{height:26px;display:inline-flex;align-items:center;gap:7px;border:1px solid rgba(27,27,27,.10);border-radius:999px;background:#fff;padding:0 8px 0 6px;color:rgba(27,27,27,.55);font-size:11px;font-weight:650;cursor:pointer;white-space:nowrap}.auto-toggle:hover{background:#F7F5EF;color:rgba(27,27,27,.76)}.auto-toggle input{position:absolute;opacity:0;pointer-events:none}.toggle-track{width:28px;height:16px;border-radius:999px;background:#D6D3CB;position:relative;transition:background .15s ease}.toggle-track:before{content:"";position:absolute;width:12px;height:12px;border-radius:999px;left:2px;top:2px;background:#fff;box-shadow:0 1px 2px rgba(27,27,27,.25);transition:transform .15s ease}.auto-toggle input:checked+.toggle-track{background:#0891B2}.auto-toggle input:checked+.toggle-track:before{transform:translateX(12px)}.auto-toggle input:focus-visible+.toggle-track{outline:2px solid rgba(8,145,178,.35);outline-offset:2px}
802
+ .menubar{height:28px;display:flex;align-items:center;gap:2px;border-bottom:1px solid rgba(27,27,27,.06);background:#F7F5EF;padding:0 12px;flex:0 0 auto;user-select:none;position:relative;z-index:20}.menu-item{height:22px;border:0;background:transparent;border-radius:5px;padding:0 8px;color:rgba(27,27,27,.58);font-size:12px;cursor:pointer}.menu-item:hover{background:var(--ws-hover);color:rgba(27,27,27,.86)}.menubar-spacer{flex:1}.session-menu-wrap{display:flex;align-items:center}.session-menu-button{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-modal-backdrop,.approval-modal-backdrop{position:fixed;inset:0;z-index:80;display:flex;align-items:flex-start;justify-content:center;background:rgba(27,27,27,.28);padding:92px 16px 24px}.approval-modal-backdrop{z-index:90;background:rgba(27,27,27,.36)}.session-modal-backdrop[hidden],.approval-modal-backdrop[hidden]{display:none}.session-dialog,.approval-dialog{width:min(620px,100%);max-height:min(620px,calc(100vh - 128px));display:flex;flex-direction:column;overflow:hidden;border:1px solid rgba(27,27,27,.12);border-radius:8px;background:#FFFDF7;box-shadow:0 24px 70px rgba(27,27,27,.22)}.approval-dialog{width:min(680px,100%)}.session-dialog-head,.approval-dialog-head{height:48px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--ws-border-subtle);padding:0 12px}.session-dialog-title,.approval-dialog-title{font-size:13px;font-weight:750;color:rgba(27,27,27,.80)}.session-dialog-subtitle,.approval-dialog-subtitle{margin-top:1px;font:10px var(--mono);color:rgba(27,27,27,.38);max-width:560px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-menu{display:block;overflow:auto;padding:6px}.session-menu-empty{padding:10px;color:rgba(27,27,27,.42);font-size:12px}.approval-body{padding:12px;overflow:auto}.approval-subject{border:1px solid var(--ws-border);border-radius:7px;background:#fff;padding:10px;font:12px/1.45 var(--mono);color:rgba(27,27,27,.72);white-space:pre-wrap;overflow-wrap:anywhere}.approval-reason{margin-top:10px;color:rgba(27,27,27,.58);font-size:12px;line-height:1.5}.approval-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;border-top:1px solid var(--ws-border-subtle);padding:10px 12px;background:#F7F5EF}.approval-actions .danger{border-color:rgba(185,28,28,.24);color:#B91C1C}.approval-actions .primary-action{background:#1B1B1B;color:#FFFDF7;border-color:#1B1B1B}.approval-inline{width:100%;max-width:960px;border:1px solid rgba(217,119,6,.22);border-radius:10px;background:#FFFBEB;overflow:hidden;box-shadow:0 8px 24px rgba(146,64,14,.08)}.approval-inline.done{border-color:rgba(27,27,27,.08);background:#F7F5EF;box-shadow:none}.approval-inline-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 11px;border-bottom:1px solid rgba(217,119,6,.14)}.approval-inline-title{font-size:12px;font-weight:800;color:#92400E}.approval-inline-meta{font:10px var(--mono);color:rgba(146,64,14,.62);white-space:nowrap}.approval-inline-subject{margin:10px 11px 0;border:1px solid rgba(217,119,6,.18);border-radius:7px;background:#fff;padding:9px;font:11px/1.45 var(--mono);color:rgba(27,27,27,.74);white-space:pre-wrap;overflow-wrap:anywhere;max-height:180px;overflow:auto}.approval-inline-reason{padding:8px 11px 10px;color:rgba(27,27,27,.58);font-size:12px;line-height:1.45}.approval-inline-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;padding:9px 11px;border-top:1px solid rgba(217,119,6,.12);background:rgba(255,255,255,.58)}
803
+ main{--left-w:256px;--right-w:480px;display:grid;grid-template-columns:var(--left-w) 6px minmax(360px,1fr) 6px var(--right-w);flex:1;min-height:0}.panel{min-width:0;background:var(--ws-panel);overflow:hidden}.files{background:rgba(255,255,255,.52)}.work{display:flex;flex-direction:column;background:#fff}.agent{background:rgba(255,255,255,.58);display:flex;flex-direction:column}.resizer{background:transparent;border-left:1px solid var(--ws-border-subtle);border-right:1px solid transparent;cursor:col-resize;position:relative}.resizer:hover,.resizer.dragging{background:rgba(8,145,178,.08);border-left-color:rgba(8,145,178,.25)}
804
+ main.hide-files{grid-template-columns:0 0 minmax(360px,1fr) 6px var(--right-w)}main.hide-files .files,main.hide-files .left-resizer{display:none}main.hide-agent{grid-template-columns:var(--left-w) 6px minmax(360px,1fr) 0 0}main.hide-agent .agent,main.hide-agent .right-resizer{display:none}
805
+ .section-head{height:36px;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 12px;color:var(--ws-faint);font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;background:rgba(255,255,255,.45)}.section-actions{display:flex;align-items:center;gap:4px}.section-button{height:20px;border:0;background:transparent;border-radius:5px;color:rgba(27,27,27,.35);cursor:pointer;padding:0 4px}.section-button:hover{background:var(--ws-hover);color:rgba(27,27,27,.70)}
806
+ .file-list{padding:6px;overflow:auto;height:calc(100% - 36px)}.row{display:flex;align-items:center;gap:5px;width:100%;height:27px;border:0;background:transparent;text-align:left;border-radius:6px;padding:0 8px;color:rgba(27,27,27,.62);cursor:pointer;font-size:12px;line-height:1}.row:hover{background:var(--ws-hover);color:rgba(27,27,27,.84)}.row.selected{background:var(--ws-active);color:var(--ws-foreground)}.row:focus-visible{outline:2px solid rgba(8,145,178,.35);outline-offset:1px}.chev{width:12px;text-align:center;color:rgba(27,27,27,.32);font:11px var(--mono)}.icon{width:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--ws-faint);font-size:11px;font-family:var(--mono)}.name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sub{color:rgba(27,27,27,.28);font:10px var(--mono);margin-left:auto}.loading-row{height:24px;padding-left:32px;color:rgba(27,27,27,.30);font:11px var(--mono)}
807
+ .tabbar{height:36px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:var(--ws-tab);flex:0 0 auto;overflow-x:auto}.tab{height:100%;display:flex;align-items:center;gap:7px;border:0;border-right:1px solid var(--ws-border-subtle);padding:0 9px;background:transparent;color:rgba(27,27,27,.45);font-size:11px;cursor:pointer;max-width:220px;min-width:90px}.tab.active{background:#fff;color:var(--ws-foreground);box-shadow:inset 0 -1.5px 0 var(--ws-foreground)}.tab:hover{background:rgba(27,27,27,.03);color:rgba(27,27,27,.75)}.tab-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close{border:0;background:transparent;border-radius:4px;color:rgba(27,27,27,.28);cursor:pointer;padding:0 2px}.tab-close:hover{background:rgba(27,27,27,.07);color:rgba(27,27,27,.70)}.tab-muted{flex:1;height:100%;border-left:1px solid var(--ws-border-subtle);min-width:24px}
808
+ .viewer{flex:1;min-height:0;overflow:hidden;background:#fff;display:flex;flex-direction:column}.file-header{min-height:34px;flex:0 0 auto;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:5px 12px;background:rgba(27,27,27,.015);font:12px var(--mono);color:rgba(27,27,27,.50)}.path{min-width:120px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-chip{display:inline-flex;align-items:center;gap:5px;border:1px solid rgba(27,27,27,.08);border-radius:999px;background:#fff;padding:1px 7px;font:10px var(--sans);color:rgba(27,27,27,.48);white-space:nowrap}.file-chip.warn{border-color:#FDE68A;background:#FFFBEB;color:#92400E}.file-actions{margin-left:auto;display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap}.file-action{font:11px var(--sans);color:rgba(27,27,27,.50);text-decoration:none;border:1px solid var(--ws-border);border-radius:6px;padding:2px 7px;background:#fff;cursor:pointer}.file-action:hover{color:var(--ws-foreground);background:var(--ws-surface)}.file-icon-action{width:24px;height:24px;min-width:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.file-icon-action svg{width:13px;height:13px;display:block}select.file-action{height:23px;max-width:190px;padding:1px 24px 1px 7px}.empty{color:var(--ws-faint);padding:24px;font-size:12px}.error{color:var(--ws-error)}
809
+ .monaco-host{flex:1;min-height:0}.code-fallback{flex:1;min-height:0;overflow:auto}.code-wrap{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start;min-height:100%;font:12px/1.55 var(--mono)}.line-nums{user-select:none;text-align:right;padding:14px 10px 14px 14px;color:rgba(27,27,27,.25);background:#FAF9F5;border-right:1px solid var(--ws-border-subtle);white-space:pre}.code-pre{margin:0;padding:14px;white-space:pre;overflow:auto;color:#1F2937;background:#fff;min-height:100%}.markdown-preview{flex:1;min-height:0;overflow:auto;max-width:920px;padding:24px 28px;color:rgba(27,27,27,.86);font-size:14px;line-height:1.65}.markdown-preview h1,.markdown-preview h2,.markdown-preview h3{line-height:1.2;margin:18px 0 8px}.markdown-preview p{margin:0 0 12px}.markdown-preview code,.message-content code{font-family:var(--mono);font-size:.92em;background:rgba(27,27,27,.06);border-radius:4px;padding:1px 4px}.markdown-preview pre,.message-content pre{overflow:auto;background:#0D1117;color:#E5E7EB;border-radius:7px;padding:12px}.image-stage{flex:1;min-height:0;display:flex;flex-direction:column;background:#F8F7F2}.image-toolbar{height:34px;flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:6px;padding:0 10px;border-bottom:1px solid var(--ws-border-subtle);background:#fff}.image-preview{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:18px;overflow:auto}.image-preview img{max-width:100%;max-height:100%;object-fit:contain;border:1px solid var(--ws-border);background:#fff;box-shadow:0 10px 30px rgba(27,27,27,.10);transform-origin:center center}.frame-preview{flex:1;min-height:0;background:#F8F7F2}.frame-preview iframe{width:100%;height:100%;border:0;background:#fff}.table-preview{flex:1;min-height:0;padding:18px;overflow:auto}.table-preview .table-meta{margin:0 0 10px;color:rgba(27,27,27,.42);font:11px var(--mono)}.table-preview table{border-collapse:collapse;font-size:12px;background:#fff}.table-preview th,.table-preview td{border:1px solid var(--ws-border);padding:5px 7px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.table-preview th{background:#F7F5EF;text-align:left;color:rgba(27,27,27,.65);position:sticky;top:0}.table-preview td.formula-cell{background:#F0F9FF;color:#075985}.diagram-preview{flex:1;min-height:0;padding:18px;display:grid;gap:14px;max-width:none;overflow:auto}.mermaid-card{border:1px solid var(--ws-border);border-radius:8px;background:#fff;overflow:hidden;display:flex;flex-direction:column}.mermaid-output{padding:18px;overflow:auto;min-height:220px;display:flex;align-items:center;justify-content:center}.mermaid-output svg{width:100%;max-width:100%;height:auto;display:block}.mermaid-source{margin:0;border-top:1px solid var(--ws-border-subtle);border-radius:0;background:#FAF9F5;color:rgba(27,27,27,.68);font:11px/1.45 var(--mono);max-height:220px;overflow:auto;padding:10px 12px;white-space:pre}.viewer-note{flex:1;min-height:0;overflow:auto;display:flex;align-items:center;justify-content:center;padding:24px;background:#FAF9F5}.viewer-note-card{width:min(520px,100%);max-height:100%;overflow:auto;border:1px solid var(--ws-border);border-radius:8px;background:#fff;padding:18px;box-shadow:0 10px 34px rgba(27,27,27,.05)}.viewer-note-title{font-size:14px;font-weight:750;color:rgba(27,27,27,.84)}.viewer-note-body{margin-top:8px;color:rgba(27,27,27,.56);font-size:12px;line-height:1.55}.viewer-note-actions{margin-top:14px;display:flex;gap:8px;flex-wrap:wrap}.binary-preview{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:24px;color:rgba(27,27,27,.50);text-align:center}
810
+ .chat-head{height:36px;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;gap:8px;padding:0 12px;background:rgba(255,255,255,.55)}.chat-title{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:rgba(27,27,27,.35)}.chat-subtitle{min-width:0;flex:1;font-size:10px;color:rgba(27,27,27,.28);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-tabs{height:32px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:#F7F5EF;padding:0 8px;gap:3px}.agent-tab{height:24px;border:0;border-radius:6px;background:transparent;color:rgba(27,27,27,.45);font-size:11px;font-weight:650;padding:0 9px;cursor:pointer}.agent-tab:hover{background:var(--ws-hover);color:rgba(27,27,27,.76)}.agent-tab.active{background:#fff;color:var(--ws-foreground);box-shadow:0 0 0 1px rgba(27,27,27,.06)}.chat-body{display:flex;flex-direction:column;flex:1;min-height:0}.chat-pane{display:none;flex-direction:column;flex:1;min-height:0}.chat-pane.active{display:flex}.thread{flex:1;min-height:0;overflow:auto;padding:16px 14px 10px;background:rgba(255,255,255,.40)}.thread-inner{display:flex;flex-direction:column;gap:12px}.empty-chat{display:flex;height:100%;align-items:center;justify-content:center;text-align:center;color:rgba(27,27,27,.35);font-size:12px}.session-list{display:flex;flex-direction:column;gap:2px}.session-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;border-radius:6px;padding:8px}.session-row:hover{background:rgba(27,27,27,.035)}.session-main{min-width:0}.session-prompt{font-size:12px;color:rgba(27,27,27,.82);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-meta{margin-top:3px;font:10px/1.4 var(--mono);color:rgba(27,27,27,.38);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-tools{margin-top:5px;display:flex;gap:4px;flex-wrap:wrap}.session-chip{border-radius:999px;border:1px solid rgba(27,27,27,.07);background:#F7F5EF;padding:1px 6px;font:10px var(--mono);color:rgba(27,27,27,.48)}.msg{display:flex}.msg.user{justify-content:flex-end}.msg.assistant{justify-content:flex-start}.bubble{max-width:86%;border-radius:16px;padding:10px 12px;font-size:13px;line-height:1.55;word-break:break-word}.user .bubble{background:#1B1B1B;color:#FFFDF7}.message-attachments{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.message-attachment{max-width:100%;display:inline-flex;align-items:center;gap:5px;border:1px solid rgba(255,255,255,.20);border-radius:999px;background:rgba(255,255,255,.10);color:#FFFDF7;padding:2px 7px;cursor:pointer}.message-attachment span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.message-attachment small{font:10px var(--mono);opacity:.68}.message-attachment:hover{background:rgba(255,255,255,.17)}.assistant-stack{width:100%;max-width:960px;display:flex;flex-direction:column;gap:8px}.assistant-bubble{display:none;max-width:94%;border-radius:16px;background:#F7F5EF;padding:11px 13px;color:rgba(27,27,27,.86);font-size:13px;line-height:1.6}.assistant-bubble:not(:empty){display:block}.message-content p{margin:0 0 10px}.message-content p:last-child{margin-bottom:0}.message-content ul{margin:0 0 10px 18px;padding:0}.message-content li{margin:2px 0}
811
+ .activity-card{display:none;overflow:hidden;border:1px solid var(--ws-border);border-radius:8px;background:#FFFDF7;font-size:12px}.activity-card.active{display:block}.activity-head{height:28px;width:100%;border:0;background:transparent;display:flex;align-items:center;gap:8px;padding:0 9px;cursor:pointer;color:rgba(27,27,27,.68)}.activity-head:hover{background:#F7F5EF}.activity-label{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;font-size:11px;font-weight:650}.activity-rollup{font-size:10px;color:rgba(27,27,27,.38)}.activity-rows{border-top:1px solid rgba(27,27,27,.05);padding:7px 9px;max-height:96px;overflow:auto;display:flex;flex-direction:column;gap:5px}.activity-card.expanded .activity-rows{max-height:360px}.activity-row{display:flex;align-items:flex-start;gap:7px;color:rgba(27,27,27,.65);font-size:11px;line-height:1.35}.activity-row .status-dot{width:8px;height:8px;border-radius:999px;background:rgba(27,27,27,.18);margin-top:4px;flex:0 0 auto}.activity-row.running .status-dot{background:#0891B2}.activity-row.done .status-dot{background:#059669}.activity-row.error .status-dot{background:#DC2626}.activity-row.approval .status-dot{background:#D97706}.activity-row.thinking{font-style:italic;color:rgba(27,27,27,.48)}.activity-main{display:block;min-width:0;flex:1}.activity-message{display:block}.activity-row pre{display:none;margin:4px 0 0;max-height:120px;overflow:auto;border-radius:6px;background:rgba(27,27,27,.04);padding:6px;font:10px/1.4 var(--mono);color:rgba(27,27,27,.62);white-space:pre-wrap}.activity-card.expanded .activity-row pre{display:block}.activity-approval{margin-top:7px;border:1px solid rgba(217,119,6,.20);border-radius:7px;background:#FFFBEB;overflow:hidden;transition:opacity .85s ease,transform .85s ease,max-height .85s ease,margin .85s ease}.activity-approval.fading{opacity:0;transform:translateY(-4px);max-height:0;margin:0;pointer-events:none}.activity-approval-head{display:flex;align-items:center;justify-content:space-between;gap:8px;border-bottom:1px solid rgba(217,119,6,.12);padding:6px 8px}.activity-approval-title{font-size:11px;font-weight:800;color:#92400E}.activity-approval-meta{font:10px var(--mono);color:rgba(146,64,14,.58);white-space:nowrap}.activity-approval-subject{margin:7px 8px 0;border:1px solid rgba(217,119,6,.14);border-radius:5px;background:#fff;padding:7px;font:10px/1.45 var(--mono);color:rgba(27,27,27,.72);white-space:pre-wrap;overflow-wrap:anywhere;max-height:130px;overflow:auto}.activity-approval-reason{padding:6px 8px 8px;color:rgba(27,27,27,.56);font-size:11px;line-height:1.45}.activity-approval-actions{display:flex;justify-content:flex-end;gap:6px;flex-wrap:wrap;border-top:1px solid rgba(217,119,6,.12);background:rgba(255,255,255,.58);padding:7px 8px}.activity-result{margin-top:6px}.trace{display:block;flex:1;min-height:0;overflow:auto;background:#fff;padding:8px}.trace-row{font:11px/1.45 var(--mono);border-bottom:1px solid rgba(27,27,27,.05);padding:7px 4px;color:rgba(27,27,27,.52);white-space:pre-wrap;overflow-wrap:anywhere}.trace-row b{color:var(--ws-primary)}
812
+ .notebook-preview{flex:1;min-height:0;overflow:auto;background:#F7F5EF;padding:14px 14px 40px;display:flex;flex-direction:column;gap:10px}.notebook-meta{font:11px var(--mono);color:rgba(27,27,27,.42);padding:0 6px}.notebook-cell{border:1px solid var(--ws-border-subtle);border-radius:8px;background:#fff;overflow:hidden;box-shadow:0 1px 0 rgba(27,27,27,.02)}.notebook-cell.code{display:grid;grid-template-columns:64px minmax(0,1fr);padding:0;align-items:stretch}.notebook-cell.markdown{padding:16px 20px}.notebook-cell.raw{background:#F7F5EF;padding:12px 16px}.notebook-cell.raw .notebook-cell-body{padding:0}.notebook-cell.raw pre{margin:0;white-space:pre-wrap;font:12px/1.5 var(--mono);color:rgba(27,27,27,.72)}.notebook-cell-body{padding:0}.notebook-markdown{font-size:14px;line-height:1.65;color:rgba(27,27,27,.86)}.notebook-markdown h1,.notebook-markdown h2,.notebook-markdown h3{line-height:1.2;margin:14px 0 8px;color:rgba(27,27,27,.92)}.notebook-markdown h1{font-size:22px;border-bottom:1px solid var(--ws-border-subtle);padding-bottom:4px}.notebook-markdown p{margin:0 0 10px}.notebook-markdown code{font-family:var(--mono);font-size:.92em;background:rgba(27,27,27,.06);border-radius:4px;padding:1px 4px}.notebook-markdown pre{overflow:auto;background:#0D1117;color:#E5E7EB;border-radius:6px;padding:10px}.notebook-prompt{padding:12px 10px 12px 10px;font:11px/1.4 var(--mono);text-align:right;user-select:none;color:#4B6BFB;background:transparent;border-right:1px solid var(--ws-border-subtle);white-space:nowrap;display:flex;align-items:flex-start;justify-content:flex-end}.notebook-prompt.out{color:#A21818}.notebook-prompt.empty{color:transparent;pointer-events:none}.notebook-prompt.in::before{content:"In "}.notebook-prompt.out::before{content:"Out "}.notebook-cell-monaco{background:#FAFBFC;min-height:52px;position:relative;overflow:hidden;padding:6px 0}.notebook-cell-monaco .monaco-editor,.notebook-cell-monaco .monaco-editor .overflow-guard,.notebook-cell-monaco .monaco-editor-background,.notebook-cell-monaco .margin{background:#FAFBFC!important}.notebook-code-fallback{margin:0;padding:8px 12px;background:transparent;color:#1F2937;font:12px/1.5 var(--mono);white-space:pre;overflow:auto}.notebook-cell-outputs{grid-column:1 / -1;display:grid;grid-template-columns:64px minmax(0,1fr);background:#fff;border-top:1px solid var(--ws-border-subtle)}.notebook-output-item{display:contents}.notebook-output-item+.notebook-output-item>.notebook-prompt,.notebook-output-item+.notebook-output-item>.notebook-output-body{border-top:1px solid var(--ws-border-subtle)}.notebook-output-body{padding:8px 12px;overflow:auto;min-width:0;background:#fff}.notebook-stream{margin:0;font:12px/1.5 var(--mono);color:#1F2937;white-space:pre-wrap;word-break:break-word;background:transparent;padding:0}.notebook-stream.stderr{color:#7F1D1D;background:#FEF2F2;padding:6px 8px;border-radius:5px}.notebook-error{margin:0;font:12px/1.5 var(--mono);color:#7F1D1D;background:#FEF2F2;padding:8px 10px;border-radius:5px;white-space:pre-wrap;word-break:break-word;overflow:auto}.notebook-plain{margin:0;font:12px/1.5 var(--mono);color:#1F2937;white-space:pre-wrap;word-break:break-word;background:transparent;padding:0}.notebook-json{margin:0;font:12px/1.5 var(--mono);color:#0F172A;background:#F8FAFC;padding:8px 10px;border-radius:5px;white-space:pre;overflow:auto}.notebook-output-body img{max-width:100%;height:auto;display:block;background:#fff;border-radius:4px}.notebook-output-body iframe{width:100%;min-height:220px;border:1px solid var(--ws-border-subtle);border-radius:5px;background:#fff}.notebook-latex{font-size:14px;color:rgba(27,27,27,.90);padding:6px 0;overflow:auto}.katex-display{margin:6px 0!important}.viewer.maximized{position:fixed;inset:12px;z-index:70;border:1px solid rgba(27,27,27,.12);border-radius:8px;box-shadow:0 24px 80px rgba(27,27,27,.28);background:#fff}.viewer.maximized .diagram-preview,.viewer.maximized .markdown-preview{max-width:none}.viewer.maximized .mermaid-output{min-height:calc(100vh - 220px)}.composer{border-top:1px solid var(--ws-border-subtle);background:#FFFDF7;padding:10px 12px}.composer-box{border:1px solid var(--ws-border);border-radius:10px;background:#fff;overflow:hidden}.upload-tray{display:flex;gap:6px;flex-wrap:wrap;padding:8px 9px 0}.upload-tray[hidden]{display:none}.upload-chip{height:24px;display:inline-flex;align-items:center;gap:6px;max-width:100%;border:1px solid rgba(8,145,178,.18);border-radius:999px;background:#F0F9FF;color:#075985;padding:0 4px 0 8px;font-size:11px}.upload-chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:210px}.upload-chip-meta{font:10px var(--mono);color:rgba(7,89,133,.58)}.upload-chip button{width:18px;height:18px;border:0;border-radius:999px;background:transparent;color:rgba(7,89,133,.52);cursor:pointer;padding:0}.upload-chip button:hover{background:rgba(8,145,178,.12);color:#075985}textarea{display:block;width:100%;min-height:84px;max-height:240px;resize:vertical;border:0;padding:10px 11px;background:#fff;color:var(--ws-foreground);outline:none}.composer-actions{height:34px;border-top:1px solid var(--ws-border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 8px}.composer-left,.composer-right{display:flex;align-items:center;gap:6px;min-width:0}.composer-right{margin-left:auto}.status{font-size:11px;color:var(--ws-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px}.stop-button{height:24px;border:1px solid rgba(185,28,28,.20);border-radius:6px;background:#FEF2F2;color:#B91C1C;font-size:11px;font-weight:750;padding:0 8px;display:inline-flex;align-items:center;gap:5px;cursor:pointer}.stop-button svg{width:12px;height:12px}.stop-button:hover{background:#FEE2E2;border-color:rgba(185,28,28,.30)}.stop-button:disabled{opacity:.55;cursor:not-allowed}.stop-button[hidden]{display:none}button.primary{height:24px;border:0;border-radius:6px;background:var(--ws-foreground);color:#fff;font-size:11px;font-weight:750;padding:0 10px;cursor:pointer}button.primary:hover{background:#2B2B2B}button.primary:disabled{opacity:.45;cursor:not-allowed}
813
+ .approval-inline{transition:opacity .85s ease,transform .85s ease,max-height .85s ease,margin .85s ease}.approval-inline.fading{opacity:0;transform:translateY(-4px);max-height:0;margin:0;pointer-events:none}.approval-inline-actions .approval-approve{background:#ECFDF5;border-color:#A7F3D0;color:#047857}.approval-inline-actions .approval-approve:hover{background:#D1FAE5;color:#065F46}.approval-inline-actions .approval-reject{background:#FEF2F2;border-color:#FECACA;color:#B91C1C}.approval-inline-actions .approval-reject:hover{background:#FEE2E2;color:#991B1B}.approval-inline-actions .approval-secondary{background:#EFF6FF;border-color:#BFDBFE;color:#1D4ED8}.approval-inline-actions .approval-secondary:hover{background:#DBEAFE;color:#1E40AF}.approval-result-line{display:inline-flex;max-width:100%;align-items:center;gap:7px;border:1px solid rgba(27,27,27,.08);border-radius:999px;background:#F7F5EF;color:rgba(27,27,27,.58);padding:5px 9px;font:11px/1.2 var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.approval-result-line.approved{background:#ECFDF5;border-color:#A7F3D0;color:#047857}.approval-result-line.denied{background:#FEF2F2;border-color:#FECACA;color:#B91C1C}.approval-result-tool{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:420px}
814
+ .file-action:disabled{opacity:.38;cursor:not-allowed}.file-action.unsaved{border-color:rgba(8,145,178,.28);background:#ECFEFF;color:#0E7490}.tab-dirty{width:6px;height:6px;border-radius:999px;background:#0891B2;flex:0 0 auto}.notebook-preview{overflow-y:auto;overflow-x:hidden}.notebook-cell,.notebook-output-body{min-width:0}.notebook-code-fallback,.notebook-json{white-space:pre-wrap;overflow:hidden;overflow-wrap:anywhere}.notebook-output-body{overflow-x:hidden;overflow-y:visible}.notebook-error,.notebook-latex{overflow:hidden;overflow-wrap:anywhere}.notebook-markdown pre,.markdown-preview pre,.message-content pre{white-space:pre-wrap;overflow-wrap:anywhere}
815
+ @media(max-width:980px){body{overflow:auto}.shell{height:auto;min-height:100vh}.global-links .global-link:not(:last-of-type){display:none}.auth-chip{display:none}main,main.hide-files,main.hide-agent{display:flex;flex-direction:column}.resizer{display:none}.panel{min-height:320px}.files,.agent{border:0;border-bottom:1px solid var(--ws-border-subtle)}.top-actions .badge:not(.local){display:none}.thread{min-height:420px}.bubble{max-width:94%}}
816
+ </style>
817
+ </head>
818
+ <body>
819
+ <div class="shell">
820
+ <header class="global-bar">
821
+ <a class="dashboard-link" href="${escapeHtml(dashboardUrl)}" target="_blank" rel="noreferrer" title="Open dashboard">
822
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>
823
+ <span>Dashboard</span>
824
+ </a>
825
+ <div class="brand-divider"></div>
826
+ <a class="ide-brand" href="${escapeHtml(cloudIdeUrl)}" target="_blank" rel="noreferrer" aria-label="Cloud IDE">
827
+ <img class="brand-mark" src="/assets/bahulam-mark.png" alt="">
828
+ <span class="brand-title">Cloud IDE</span>
829
+ </a>
830
+ <div class="global-spacer"></div>
831
+ <nav class="global-links" aria-label="Bahulam links">
832
+ <a class="global-link" href="${escapeHtml(publicUrl)}" target="_blank" rel="noreferrer">Bahulam</a>
833
+ <a class="global-link" href="${escapeHtml(pricingUrl)}" target="_blank" rel="noreferrer">Pricing</a>
834
+ <a class="global-link" href="${escapeHtml(billingUrl)}" target="_blank" rel="noreferrer">Subscriptions</a>
835
+ <a class="global-link" href="${escapeHtml(accountUrl)}" target="_blank" rel="noreferrer">${escapeHtml(accountLabel)}</a>
836
+ <span class="auth-chip ${auth.authenticated ? 'signed-in' : ''}" title="${escapeHtml(auth.backendUrl || 'No backend URL resolved')}">${escapeHtml(authLabel)}</span>
837
+ </nav>
838
+ </header>
839
+ <div class="topbar">
840
+ <svg class="terminal-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
841
+ <div class="workspace-name">${rootName}</div>
842
+ <div class="workspace-kind">${escapeHtml(session.kind)}</div>
843
+ <div class="top-actions">
844
+ <label class="auto-toggle" id="approvalAutoWrap" title="Auto-approve routine tool approvals for this browser session">
845
+ <input id="approvalAuto" type="checkbox">
846
+ <span class="toggle-track"></span>
847
+ <span id="approvalAutoLabel">Auto off</span>
848
+ </label>
849
+ <span class="badge local">local</span>
850
+ <span class="badge">${escapeHtml(session.id)}</span>
851
+ <button class="icon-button" id="toggleExplorer" type="button" title="Hide file explorer" aria-label="Toggle file explorer" aria-pressed="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M9 3v18"></path></svg></button>
852
+ <button class="icon-button" id="toggleAgent" type="button" title="Hide agent chat" aria-label="Toggle agent chat" aria-pressed="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M15 3v18"></path></svg></button>
853
+ </div>
854
+ </div>
855
+ <div class="menubar">
856
+ <button class="menu-item" type="button">File</button>
857
+ <button class="menu-item" type="button">Edit</button>
858
+ <button class="menu-item" type="button">Run</button>
859
+ <button class="menu-item" type="button">Tools</button>
860
+ <div class="menubar-spacer"></div>
861
+ <div class="session-menu-wrap">
862
+ <button class="menu-item session-menu-button" id="sessionMenuButton" type="button" aria-haspopup="dialog" aria-expanded="false">Chat: New</button>
863
+ </div>
864
+ </div>
865
+ <main id="layout">
866
+ <section class="panel files" id="filesPanel">
867
+ <div class="section-head"><span>Files</span><span class="section-actions"><button class="section-button" id="refreshFiles" type="button" title="Refresh">↻</button><span id="fileCount"></span></span></div>
868
+ <div class="file-list" id="fileList"></div>
869
+ </section>
870
+ <div class="resizer left-resizer" data-resize="left"></div>
871
+ <section class="panel work">
872
+ <div class="tabbar" id="tabbar"><div class="tab-muted"></div></div>
873
+ <div class="viewer" id="viewer"><div class="empty">Loading...</div></div>
874
+ </section>
875
+ <div class="resizer right-resizer" data-resize="right"></div>
876
+ <section class="panel agent" id="agentPanel">
877
+ <div class="chat-head"><span class="chat-title">Agent</span><span class="chat-subtitle" id="bridgeState">bridge pending</span></div>
878
+ <div class="agent-tabs" role="tablist" aria-label="Agent panel">
879
+ <button class="agent-tab active" id="chatTab" type="button" role="tab" aria-selected="true" data-agent-tab="chat">Chat</button>
880
+ <button class="agent-tab" id="traceTab" type="button" role="tab" aria-selected="false" data-agent-tab="trace">Trace</button>
881
+ </div>
882
+ <div class="chat-body">
883
+ <div class="chat-pane active" id="chatPane" role="tabpanel" aria-labelledby="chatTab">
884
+ <div class="thread" id="thread"><div class="thread-inner" id="threadInner"><div class="empty-chat" id="emptyChat">Ask about this local workspace.</div></div></div>
885
+ <form class="composer" id="composer">
886
+ <div class="composer-box">
887
+ <div class="upload-tray" id="uploadTray" hidden></div>
888
+ <textarea id="prompt" placeholder="Ask about this local workspace"></textarea>
889
+ <div class="composer-actions">
890
+ <div class="composer-left">
891
+ <button class="icon-button" id="attachFiles" type="button" title="Attach files" aria-label="Attach files"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg></button>
892
+ <input id="fileUpload" type="file" multiple hidden>
893
+ <div class="status" id="sendStatus"></div>
894
+ </div>
895
+ <div class="composer-right">
896
+ <button class="stop-button" id="stopAgent" type="button" title="Stop current agent run" aria-label="Stop current agent run" hidden><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"></rect></svg><span>Stop</span></button>
897
+ <button class="primary" id="send" type="submit">Send</button>
898
+ </div>
899
+ </div>
900
+ </div>
901
+ </form>
902
+ </div>
903
+ <div class="chat-pane" id="tracePane" role="tabpanel" aria-labelledby="traceTab">
904
+ <div class="trace" id="events"></div>
905
+ </div>
906
+ </div>
907
+ </section>
908
+ </main>
909
+ <div class="session-modal-backdrop" id="sessionModal" hidden>
910
+ <section class="session-dialog" role="dialog" aria-modal="true" aria-labelledby="sessionDialogTitle">
911
+ <div class="session-dialog-head">
912
+ <div>
913
+ <div class="session-dialog-title" id="sessionDialogTitle">Chat sessions</div>
914
+ <div class="session-dialog-subtitle">${escapeHtml(session.root_path)}</div>
915
+ </div>
916
+ <button class="icon-button" id="sessionModalClose" type="button" aria-label="Close chat sessions">×</button>
917
+ </div>
918
+ <div class="session-menu" id="sessionMenu" role="menu"></div>
919
+ </section>
920
+ </div>
921
+ <div class="approval-modal-backdrop" id="approvalModal" hidden>
922
+ <section class="approval-dialog" role="dialog" aria-modal="true" aria-labelledby="approvalTitle">
923
+ <div class="approval-dialog-head">
924
+ <div>
925
+ <div class="approval-dialog-title" id="approvalTitle">Approval required</div>
926
+ <div class="approval-dialog-subtitle" id="approvalSubtitle"></div>
927
+ </div>
928
+ <button class="icon-button danger" id="approvalDenyTop" type="button">Deny</button>
929
+ </div>
930
+ <div class="approval-body">
931
+ <div class="approval-subject" id="approvalSubject"></div>
932
+ <div class="approval-reason" id="approvalReason"></div>
933
+ </div>
934
+ <div class="approval-actions" id="approvalActions"></div>
935
+ </section>
936
+ </div>
937
+ </div>
938
+ <script src="/vendor/monaco/vs/loader.js"></script>
939
+ <script>
940
+ const BOOT = ${boot};
941
+ const token = BOOT.token;
942
+ const layoutKey = 'bahulam_local_ide_layout_v2';
943
+ let currentPath = BOOT.initialPath || '.';
944
+ const headers = { 'X-Bahulam-Local-Token': token };
945
+ const dirCache = new Map();
946
+ const expandedDirs = new Set(['.']);
947
+ const loadingDirs = new Set();
948
+ const openTabs = [];
949
+ const fileCache = new Map();
950
+ let activePath = null;
951
+ let activeTurn = null;
952
+ let turnSeq = 0;
953
+ let turnRunning = false;
954
+ let turnCancelling = false;
955
+ const queuedFollowups = [];
956
+ const queuedFollowupIds = new Set();
957
+ let historySelectionReady = false;
958
+ let historySessions = [];
959
+ let sessionMenuOpen = false;
960
+ let traceCount = 0;
961
+ let pendingApproval = null;
962
+ let pendingApprovalEl = null;
963
+ let approvalAutoMode = false;
964
+ let monacoReady = null;
965
+ let activeEditor = null;
966
+ let activeEditorPath = null;
967
+ let activeEditorOriginal = '';
968
+ let activeNotebookState = null;
969
+ let activeImageScale = 1;
970
+ let mermaidReady = null;
971
+ let mermaidSeq = 0;
972
+ let pendingAttachments = [];
973
+ let previewMaximized = false;
974
+ const unsavedFiles = new Map();
975
+ const dirtyPaths = new Set();
976
+
977
+ const LANGUAGE_OPTIONS = [
978
+ ['plaintext','Plain text'],['javascript','JavaScript'],['typescript','TypeScript'],['json','JSON'],
979
+ ['html','HTML'],['css','CSS'],['scss','SCSS'],['markdown','Markdown'],['yaml','YAML'],['toml','TOML'],
980
+ ['xml','XML'],['python','Python'],['ruby','Ruby'],['go','Go'],['rust','Rust'],['java','Java'],
981
+ ['c','C'],['cpp','C++'],['csharp','C#'],['php','PHP'],['kotlin','Kotlin'],['swift','Swift'],
982
+ ['dart','Dart'],['scala','Scala'],['r','R'],['lua','Lua'],['perl','Perl'],['elixir','Elixir'],
983
+ ['clojure','Clojure'],['fsharp','F#'],['shell','Shell'],['powershell','PowerShell'],['bat','Batch'],
984
+ ['sql','SQL'],['graphql','GraphQL'],['dockerfile','Dockerfile'],['hcl','HCL/Terraform'],
985
+ ['powerquery','Power Query'],['msdax','DAX'],['ini','INI']
986
+ ];
987
+ const NOTEBOOK_LANGUAGE = ['notebook','Jupyter Notebook (.ipynb)'];
988
+ const LANGUAGES = LANGUAGE_OPTIONS.slice().sort((a,b)=>a[1].localeCompare(b[1],undefined,{sensitivity:'base'}));
989
+
990
+ function esc(value){return String(value ?? '').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));}
991
+ async function api(path, opts={}){
992
+ const res = await fetch(path, { ...opts, headers: { ...headers, ...(opts.headers || {}) } });
993
+ const text = await res.text();
994
+ let body; try { body = text ? JSON.parse(text) : {}; } catch { body = { ok:false, message:text }; }
995
+ if (!res.ok) {
996
+ const err = new Error(body.message || body.error || 'request failed');
997
+ err.status = res.status;
998
+ err.body = body;
999
+ throw err;
1000
+ }
1001
+ return body;
1002
+ }
1003
+ function rawUrl(path){return '/api/file/raw?token=' + encodeURIComponent(token) + '&path=' + encodeURIComponent(path);}
1004
+ function basename(p){const parts=String(p||'').split('/').filter(Boolean);return parts.length?parts[parts.length-1]:'Workspace';}
1005
+ function extname(p){const name=basename(p).toLowerCase();const i=name.lastIndexOf('.');return i>=0?name.slice(i+1):'';}
1006
+ function formatBytes(size){const n=Number(size)||0;if(n<1024)return String(n);if(n<1024*1024)return Math.round(n/1024)+'K';return (n/1024/1024).toFixed(1)+'M';}
1007
+ function attachmentHint(file){
1008
+ const kind=String(file?.kind||file?.viewer||'').toLowerCase();
1009
+ const mime=String(file?.mime_type||'').toLowerCase();
1010
+ const ext=extname(file?.path||file?.name||'');
1011
+ if(kind==='image'||mime.startsWith('image/'))return 'vision';
1012
+ if(kind==='spreadsheet'||kind==='table'||['csv','tsv','xlsx','xls','ods'].includes(ext))return 'table';
1013
+ if(['pdf','markdown','text','code','config','notebook'].includes(kind)||['txt','md','mdx','pdf','json','yaml','yml','toml','html','xml','ipynb','log','rst','sql','sh'].includes(ext))return 'read';
1014
+ return 'file';
1015
+ }
1016
+ function fileToDataBase64(file){
1017
+ return new Promise((resolve,reject)=>{
1018
+ const reader=new FileReader();
1019
+ reader.onload=()=>resolve(String(reader.result||'').split(',').pop()||'');
1020
+ reader.onerror=()=>reject(reader.error||new Error('File read failed'));
1021
+ reader.readAsDataURL(file);
1022
+ });
1023
+ }
1024
+ function renderUploadTray(){
1025
+ const tray=document.getElementById('uploadTray');
1026
+ if(!tray)return;
1027
+ if(!pendingAttachments.length){tray.hidden=true;tray.innerHTML='';return;}
1028
+ tray.hidden=false;
1029
+ tray.innerHTML=pendingAttachments.map((file,index)=>'<span class="upload-chip" title="'+esc(file.path)+'"><span class="upload-chip-name">'+esc(file.name||basename(file.path))+'</span><span class="upload-chip-meta">'+esc(attachmentHint(file))+' · '+esc(formatBytes(file.size))+'</span><button type="button" data-remove-upload="'+index+'" aria-label="Remove attachment">×</button></span>').join('');
1030
+ tray.querySelectorAll('[data-remove-upload]').forEach(btn=>btn.addEventListener('click',()=>{
1031
+ pendingAttachments.splice(Number(btn.dataset.removeUpload),1);
1032
+ renderUploadTray();
1033
+ }));
1034
+ }
1035
+ async function uploadFiles(fileList){
1036
+ const files=[...fileList];
1037
+ if(!files.length)return;
1038
+ document.getElementById('sendStatus').textContent='Uploading...';
1039
+ const payload=[];
1040
+ for(const file of files){
1041
+ payload.push({name:file.name||'upload.bin',mime_type:file.type||'',size:file.size||0,data_base64:await fileToDataBase64(file)});
1042
+ }
1043
+ const result=await api('/api/files/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files:payload})});
1044
+ pendingAttachments.push(...(result.files||[]));
1045
+ renderUploadTray();
1046
+ document.getElementById('sendStatus').textContent='Attached '+pendingAttachments.length+' file'+(pendingAttachments.length===1?'':'s');
1047
+ dirCache.clear();
1048
+ await loadDir('.');
1049
+ if(result.files?.[0]?.path)openFile(result.files[0].path);
1050
+ }
1051
+ function parentPath(p){const parts=String(p||'.').split('/').filter(Boolean);parts.pop();return parts.length?parts.join('/'):'.';}
1052
+ function compactId(id){const value=String(id||'');return value.length>16?value.slice(0,8)+'...'+value.slice(-5):value;}
1053
+ function formatSessionTime(value){if(!value)return 'unknown';try{return new Date(value).toLocaleString([], {month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch{return String(value);}}
1054
+ let notebookCellEditors=[];
1055
+ let katexReady=null;
1056
+ function disposeActiveEditor(){try{activeEditor?.dispose?.();}catch{}activeEditor=null;activeEditorPath=null;activeEditorOriginal='';activeNotebookState=null;disposeNotebookCellEditors();}
1057
+ function disposeNotebookCellEditors(){for(const item of notebookCellEditors){try{(item.editor||item)?.dispose?.();}catch{}}notebookCellEditors=[];}
1058
+ function loadMonaco(){
1059
+ if(window.monaco)return Promise.resolve(window.monaco);
1060
+ if(monacoReady)return monacoReady;
1061
+ monacoReady=new Promise((resolve,reject)=>{
1062
+ if(!window.require){reject(new Error('Monaco loader unavailable'));return;}
1063
+ window.require.config({paths:{vs:'/vendor/monaco/vs'}});
1064
+ window.require(['vs/editor/editor.main'],()=>resolve(window.monaco),reject);
1065
+ });
1066
+ return monacoReady;
1067
+ }
1068
+ async function importWithoutAmdDefine(url){
1069
+ const globalObj=window;
1070
+ const hadDefine=Object.prototype.hasOwnProperty.call(globalObj,'define');
1071
+ const previousDefine=globalObj.define;
1072
+ try{
1073
+ try{Object.defineProperty(globalObj,'define',{value:undefined,writable:true,configurable:true});}
1074
+ catch{try{globalObj.define=undefined;}catch{}}
1075
+ return await import(url);
1076
+ }finally{
1077
+ try{
1078
+ if(hadDefine)Object.defineProperty(globalObj,'define',{value:previousDefine,writable:true,configurable:true});
1079
+ else delete globalObj.define;
1080
+ }catch{
1081
+ try{globalObj.define=previousDefine;}catch{}
1082
+ }
1083
+ }
1084
+ }
1085
+ async function loadMermaid(){
1086
+ if(mermaidReady)return mermaidReady;
1087
+ mermaidReady=importWithoutAmdDefine('/vendor/mermaid/mermaid.esm.min.mjs').then(mod=>{
1088
+ const mermaid=mod.default||mod;
1089
+ mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:'base',themeVariables:{primaryColor:'#F7F5EF',primaryTextColor:'#1B1B1B',primaryBorderColor:'#D7D3C8',lineColor:'#64748B',fontFamily:'ui-sans-serif, system-ui, sans-serif'}});
1090
+ return mermaid;
1091
+ }).catch(err=>{mermaidReady=null;throw err;});
1092
+ return mermaidReady;
1093
+ }
1094
+ function fileKind(data){
1095
+ const file=data.file||{};
1096
+ if(file.viewer)return file.viewer;
1097
+ const ext=extname(data.path);
1098
+ if(['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext))return 'image';
1099
+ if(ext==='pdf')return 'pdf';
1100
+ if(['md','mdx'].includes(ext))return 'markdown';
1101
+ if(['mmd','mermaid'].includes(ext))return 'mermaid';
1102
+ if(['drawio','dio'].includes(ext))return 'drawio';
1103
+ if(['csv','tsv'].includes(ext))return 'table';
1104
+ if(ext==='ipynb')return 'notebook';
1105
+ if(['xlsx','xls','ods'].includes(ext))return 'spreadsheet';
1106
+ if(['docx','doc','odt'].includes(ext))return 'document';
1107
+ if(['pptx','ppt','odp'].includes(ext))return 'presentation';
1108
+ if(data.preview&&data.preview.content!=null)return 'code';
1109
+ return 'unsupported';
1110
+ }
1111
+ function fileLanguage(data){
1112
+ const file=data.file||{};
1113
+ if(file.language)return file.language;
1114
+ const ext=extname(data.path);
1115
+ const byExt={js:'javascript',mjs:'javascript',cjs:'javascript',jsx:'javascript',ts:'typescript',tsx:'typescript',json:'json',jsonl:'json',ipynb:'json',css:'css',scss:'scss',html:'html',htm:'html',md:'markdown',mdx:'markdown',yaml:'yaml',yml:'yaml',toml:'toml',xml:'xml',py:'python',rb:'ruby',go:'go',rs:'rust',java:'java',c:'c',h:'c',cpp:'cpp',cc:'cpp',cxx:'cpp',hpp:'cpp',sh:'shell',bash:'shell',zsh:'shell',sql:'sql',graphql:'graphql',gql:'graphql',dockerfile:'dockerfile',tf:'hcl',hcl:'hcl',dax:'msdax',pq:'powerquery',m:'powerquery',ini:'ini'};
1116
+ const name=basename(data.path).toLowerCase();
1117
+ if(name==='dockerfile')return 'dockerfile';
1118
+ if(name.startsWith('.env'))return 'plaintext';
1119
+ return byExt[ext]||'plaintext';
1120
+ }
1121
+ function languageLabel(id){
1122
+ const match=(id===NOTEBOOK_LANGUAGE[0]?NOTEBOOK_LANGUAGE:null)||LANGUAGES.find(([value])=>value===id);
1123
+ return match?match[1]:(id||'Plain text');
1124
+ }
1125
+ function languageSelect(current,path,auto=false,{includeNotebook=false}={}){
1126
+ const autoOpt='<option value="__auto" '+(auto?'selected':'')+'>Auto detect ('+esc(languageLabel(current))+')</option>';
1127
+ const options=includeNotebook?[NOTEBOOK_LANGUAGE,...LANGUAGES]:LANGUAGES;
1128
+ const opts=options.map(([id,label])=>'<option value="'+esc(id)+'" '+(!auto&&id===current?'selected':'')+'>'+esc(label)+'</option>').join('');
1129
+ return '<select class="file-action" data-language-select="'+esc(path)+'" title="Preview language">'+autoOpt+opts+'</select>';
1130
+ }
1131
+ function detectNotebookLanguage(notebook){
1132
+ const metadata=notebook&&notebook.metadata?notebook.metadata:{};
1133
+ const info=metadata.language_info||{};
1134
+ const kernel=metadata.kernelspec||{};
1135
+ return String(info.name||kernel.language||kernel.display_name||metadata.language||'').toLowerCase();
1136
+ }
1137
+ function monacoLanguageForNotebook(value){
1138
+ const lang=String(value||'').toLowerCase();
1139
+ if(lang.includes('python')||['py','ipython'].includes(lang))return 'python';
1140
+ if(lang.includes('javascript')||['js','node'].includes(lang))return 'javascript';
1141
+ if(lang.includes('typescript')||lang==='ts')return 'typescript';
1142
+ if(lang==='r'||lang.includes('r-language'))return 'r';
1143
+ if(lang.includes('sql'))return 'sql';
1144
+ if(lang.includes('julia'))return 'julia';
1145
+ if(['bash','shell','sh','zsh'].includes(lang)||lang.includes('bash'))return 'shell';
1146
+ if(lang==='notebook')return 'python';
1147
+ return lang||'plaintext';
1148
+ }
1149
+ function iconSvg(name){
1150
+ const attrs='viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
1151
+ const icons={
1152
+ maximize:'<svg '+attrs+'><path d="M15 3h6v6"></path><path d="m21 3-7 7"></path><path d="M9 21H3v-6"></path><path d="m3 21 7-7"></path></svg>',
1153
+ minimize:'<svg '+attrs+'><path d="M14 10h6V4"></path><path d="m20 10-7-7"></path><path d="M4 20h6v-6"></path><path d="m4 20 7-7"></path></svg>',
1154
+ external:'<svg '+attrs+'><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg>',
1155
+ raw:'<svg '+attrs+'><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><path d="M14 2v6h6"></path><path d="M10 13h4"></path><path d="M10 17h4"></path></svg>',
1156
+ ask:'<svg '+attrs+'><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"></path><path d="M9 9h6"></path><path d="M9 13h4"></path></svg>',
1157
+ save:'<svg '+attrs+'><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><path d="M17 21v-8H7v8"></path><path d="M7 3v5h8"></path></svg>',
1158
+ edit:'<svg '+attrs+'><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>',
1159
+ plus:'<svg '+attrs+'><path d="M5 12h14"></path><path d="M12 5v14"></path></svg>',
1160
+ minus:'<svg '+attrs+'><path d="M5 12h14"></path></svg>',
1161
+ reset:'<svg '+attrs+'><path d="M3 12a9 9 0 1 0 3-6.7"></path><path d="M3 3v6h6"></path></svg>'
1162
+ };
1163
+ return icons[name]||icons.raw;
1164
+ }
1165
+ function fileIconButton(attr,name,label){
1166
+ return '<button class="file-action file-icon-action" type="button" '+attr+' title="'+esc(label)+'" aria-label="'+esc(label)+'">'+iconSvg(name)+'</button>';
1167
+ }
1168
+ function fileIconLink(href,name,label){
1169
+ return '<a class="file-action file-icon-action" href="'+esc(href)+'" target="_blank" rel="noreferrer" title="'+esc(label)+'" aria-label="'+esc(label)+'">'+iconSvg(name)+'</a>';
1170
+ }
1171
+ function canEditFile(data){
1172
+ return Boolean(data?.file?.text_like && data?.preview && data.preview.content != null && !data.preview.truncated);
1173
+ }
1174
+ function currentSavedText(data){
1175
+ return String(data?.preview?.content ?? '');
1176
+ }
1177
+ function sourceTextForRender(data){
1178
+ return unsavedFiles.has(data.path) ? unsavedFiles.get(data.path) : currentSavedText(data);
1179
+ }
1180
+ function renderEditableSource(data,language=null,{auto=true}={}){
1181
+ renderCodeEditor(data,sourceTextForRender(data),language||fileLanguage(data),{auto,originalText:currentSavedText(data)});
1182
+ }
1183
+ function setPreviewMaximized(open){
1184
+ previewMaximized=!!open;
1185
+ const viewer=document.getElementById('viewer');
1186
+ if(viewer)viewer.classList.toggle('maximized',previewMaximized);
1187
+ document.querySelectorAll('[data-preview-maximize]').forEach(btn=>{
1188
+ const label=previewMaximized?'Exit full screen preview':'Maximize preview';
1189
+ btn.innerHTML=iconSvg(previewMaximized?'minimize':'maximize');
1190
+ btn.title=label;
1191
+ btn.setAttribute('aria-label',label);
1192
+ });
1193
+ setTimeout(()=>activeEditor?.layout?.(),0);
1194
+ }
1195
+ function openCurrentPreviewTab(path){
1196
+ const content=document.querySelector('#viewer [data-preview-content]');
1197
+ if(!content||content.classList.contains('monaco-host')){
1198
+ window.open(rawUrl(path),'_blank','noopener,noreferrer');
1199
+ return;
1200
+ }
1201
+ const popup=window.open('','_blank');
1202
+ if(!popup){
1203
+ window.open(rawUrl(path),'_blank','noopener,noreferrer');
1204
+ return;
1205
+ }
1206
+ const styles=[...document.querySelectorAll('style')].map(style=>style.textContent||'').join('\\n');
1207
+ popup.document.open();
1208
+ popup.document.write('<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="'+esc(location.origin)+'/"><title>'+esc(basename(path))+'</title><style>'+styles+'body{margin:0;background:#fff}.standalone-preview{height:100vh;overflow:hidden}.standalone-preview>[data-preview-content]{height:100%;max-width:none}</style></head><body><div class="viewer standalone-preview">'+content.outerHTML+'</div></body></html>');
1209
+ popup.document.close();
1210
+ }
1211
+ function bindHeaderActions(path){
1212
+ document.querySelectorAll('[data-ask-file]').forEach(btn=>btn.addEventListener('click',()=>askAgentForFile(btn.dataset.askFile,btn.dataset.askKind)));
1213
+ document.querySelectorAll('[data-save-file]').forEach(btn=>btn.addEventListener('click',()=>saveCurrentFile(btn.dataset.saveFile)));
1214
+ document.querySelectorAll('[data-edit-source]').forEach(btn=>btn.addEventListener('click',()=>{
1215
+ const data=fileCache.get(btn.dataset.editSource);
1216
+ if(data)renderEditableSource(data,fileKind(data)==='notebook'?'json':fileLanguage(data),{auto:false});
1217
+ }));
1218
+ document.querySelectorAll('[data-preview-maximize]').forEach(btn=>btn.addEventListener('click',()=>setPreviewMaximized(!previewMaximized)));
1219
+ document.querySelectorAll('[data-preview-new-tab]').forEach(btn=>btn.addEventListener('click',()=>openCurrentPreviewTab(btn.dataset.previewNewTab)));
1220
+ document.querySelectorAll('[data-language-select]').forEach(sel=>sel.addEventListener('change',()=>{
1221
+ const data=fileCache.get(sel.dataset.languageSelect);
1222
+ if(!data)return;
1223
+ const auto=sel.value==='__auto';
1224
+ if(fileKind(data)==='notebook'&&(!auto&&sel.value==='json'))renderEditableSource(data,'json',{auto:false});
1225
+ else if(fileKind(data)==='notebook')renderNotebookFile(data,sourceTextForRender(data),auto?null:sel.value,{auto});
1226
+ else renderEditableSource(data,auto?fileLanguage(data):sel.value,{auto});
1227
+ }));
1228
+ updateSaveButtons(path);
1229
+ }
1230
+ function askAgentForFile(path,kind){
1231
+ const prompt=document.getElementById('prompt');
1232
+ const label=kind==='image'?'Analyze this image':kind==='pdf'?'Summarize this PDF':kind==='spreadsheet'?'Inspect this spreadsheet':kind==='document'?'Inspect this document':kind==='presentation'?'Inspect this presentation':'Inspect this file';
1233
+ prompt.value=label+' at '+path+'. Explain what it contains and call the right local tools if needed.';
1234
+ prompt.focus();
1235
+ }
1236
+ function fileHeader(data,{language=null,languageAuto=false,truncated=false,extraActions='',editSource=false,save=false}={}){
1237
+ const file=data.file||{};
1238
+ const kind=fileKind(data);
1239
+ const label=file.label||kind;
1240
+ const chips=[
1241
+ '<span class="file-chip">'+esc(label)+'</span>',
1242
+ file.size!=null?'<span class="file-chip">'+formatBytes(file.size)+'</span>':'',
1243
+ truncated?'<span class="file-chip warn">truncated preview</span>':''
1244
+ ].filter(Boolean).join('');
1245
+ const lang=language?languageSelect(language,data.path,languageAuto,{includeNotebook:kind==='notebook'}):'';
1246
+ const saveAction=save?fileIconButton('data-save-file="'+esc(data.path)+'" disabled','save','Save file'):'';
1247
+ const editAction=editSource?fileIconButton('data-edit-source="'+esc(data.path)+'"','edit','Edit source'):'';
1248
+ const maximize=fileIconButton('data-preview-maximize="'+esc(data.path)+'"',previewMaximized?'minimize':'maximize',previewMaximized?'Exit full screen preview':'Maximize preview');
1249
+ const newTab=fileIconButton('data-preview-new-tab="'+esc(data.path)+'"','external','Open preview in new tab');
1250
+ const ask=fileIconButton('data-ask-file="'+esc(data.path)+'" data-ask-kind="'+esc(kind)+'"','ask','Ask agent');
1251
+ const raw=fileIconLink(rawUrl(data.path),'raw','Open raw file');
1252
+ return '<div class="file-header"><span>◇</span><span class="path">'+esc(data.path)+'</span>'+chips+'<span class="file-actions">'+lang+extraActions+saveAction+editAction+maximize+newTab+ask+raw+'</span></div>';
1253
+ }
1254
+ function renderViewer(html){
1255
+ disposeActiveEditor();
1256
+ const viewer=document.getElementById('viewer');
1257
+ viewer.innerHTML=html;
1258
+ viewer.classList.toggle('maximized',previewMaximized);
1259
+ }
1260
+ function setFileDirty(path,dirty,content=null){
1261
+ if(!path)return;
1262
+ if(dirty){
1263
+ dirtyPaths.add(path);
1264
+ if(content!=null)unsavedFiles.set(path,content);
1265
+ }else{
1266
+ dirtyPaths.delete(path);
1267
+ unsavedFiles.delete(path);
1268
+ }
1269
+ renderTabs();
1270
+ updateSaveButtons(path);
1271
+ }
1272
+ function updateSaveButtons(path=null){
1273
+ document.querySelectorAll('[data-save-file]').forEach(btn=>{
1274
+ if(path&&btn.dataset.saveFile!==path)return;
1275
+ const dirty=dirtyPaths.has(btn.dataset.saveFile);
1276
+ btn.disabled=!dirty;
1277
+ btn.classList.toggle('unsaved',dirty);
1278
+ });
1279
+ }
1280
+ function serializeNotebook(notebook){
1281
+ return JSON.stringify(notebook||{},null,2)+'\\n';
1282
+ }
1283
+ function syncNotebookEditorSources(){
1284
+ if(!activeNotebookState)return;
1285
+ for(const item of notebookCellEditors){
1286
+ if(!item?.cell||!item?.editor)continue;
1287
+ setNotebookCellSource(item.cell,item.editor.getValue());
1288
+ }
1289
+ }
1290
+ function activeEditableContent(path){
1291
+ if(activeEditor&&activeEditorPath===path)return activeEditor.getValue();
1292
+ if(activeNotebookState&&activeNotebookState.path===path){
1293
+ syncNotebookEditorSources();
1294
+ return serializeNotebook(activeNotebookState.notebook);
1295
+ }
1296
+ if(unsavedFiles.has(path))return unsavedFiles.get(path);
1297
+ return null;
1298
+ }
1299
+ async function saveCurrentFile(path){
1300
+ const data=fileCache.get(path);
1301
+ if(!data||!canEditFile(data)){
1302
+ document.getElementById('sendStatus').textContent='This file cannot be saved from the browser preview.';
1303
+ return;
1304
+ }
1305
+ const content=activeEditableContent(path);
1306
+ if(content==null)return;
1307
+ const buttons=[...document.querySelectorAll('[data-save-file]')].filter(btn=>btn.dataset.saveFile===path);
1308
+ buttons.forEach(btn=>{btn.disabled=true;});
1309
+ document.getElementById('sendStatus').textContent='Saving '+basename(path)+'...';
1310
+ try{
1311
+ const saved=await api('/api/file/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path,content})});
1312
+ fileCache.set(saved.path,saved);
1313
+ if(saved.path!==path)fileCache.delete(path);
1314
+ setFileDirty(saved.path,false);
1315
+ if(activeEditorPath===path)activeEditorOriginal=content;
1316
+ if(activeNotebookState?.path===path)activeNotebookState.originalText=content;
1317
+ document.getElementById('sendStatus').textContent='Saved '+basename(saved.path);
1318
+ const parent=parentPath(saved.path);
1319
+ dirCache.delete(parent);
1320
+ if(parent==='.')dirCache.delete('.');
1321
+ await loadDir(parent);
1322
+ }catch(err){
1323
+ document.getElementById('sendStatus').textContent=err.message;
1324
+ addTrace('file_save_failed',{path,message:err.message});
1325
+ updateSaveButtons(path);
1326
+ }
1327
+ }
1328
+ function applyLayout(){
1329
+ let prefs={leftWidth:256,rightWidth:480,explorerVisible:true,agentVisible:true};
1330
+ try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}
1331
+ const layout=document.getElementById('layout');
1332
+ layout.style.setProperty('--left-w', Math.max(180, Math.min(520, prefs.leftWidth)) + 'px');
1333
+ layout.style.setProperty('--right-w', Math.max(360, Math.min(760, prefs.rightWidth)) + 'px');
1334
+ layout.classList.toggle('hide-files', !prefs.explorerVisible);
1335
+ layout.classList.toggle('hide-agent', !prefs.agentVisible);
1336
+ document.getElementById('toggleExplorer').setAttribute('aria-pressed', String(prefs.explorerVisible));
1337
+ document.getElementById('toggleAgent').setAttribute('aria-pressed', String(prefs.agentVisible));
1338
+ document.getElementById('toggleExplorer').title = prefs.explorerVisible ? 'Hide file explorer' : 'Show file explorer';
1339
+ document.getElementById('toggleAgent').title = prefs.agentVisible ? 'Hide agent chat' : 'Show agent chat';
1340
+ }
1341
+ function saveLayoutPatch(patch){
1342
+ let prefs={leftWidth:256,rightWidth:480,explorerVisible:true,agentVisible:true};
1343
+ try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}
1344
+ prefs={...prefs,...patch};
1345
+ localStorage.setItem(layoutKey, JSON.stringify(prefs));
1346
+ applyLayout();
1347
+ }
1348
+ function setupResizers(){
1349
+ for(const handle of document.querySelectorAll('.resizer')){
1350
+ handle.addEventListener('mousedown', e=>{
1351
+ e.preventDefault();
1352
+ const type=handle.dataset.resize;
1353
+ handle.classList.add('dragging');
1354
+ const startX=e.clientX;
1355
+ let prefs={leftWidth:256,rightWidth:480,explorerVisible:true,agentVisible:true};
1356
+ try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}
1357
+ const startLeft=prefs.leftWidth;
1358
+ const startRight=prefs.rightWidth;
1359
+ function move(ev){
1360
+ if(type==='left') saveLayoutPatch({leftWidth:Math.max(180, Math.min(520, startLeft + ev.clientX - startX))});
1361
+ if(type==='right') saveLayoutPatch({rightWidth:Math.max(360, Math.min(760, startRight - ev.clientX + startX))});
1362
+ }
1363
+ function up(){handle.classList.remove('dragging');window.removeEventListener('mousemove',move);window.removeEventListener('mouseup',up);}
1364
+ window.addEventListener('mousemove',move);
1365
+ window.addEventListener('mouseup',up);
1366
+ });
1367
+ }
1368
+ }
1369
+ async function loadDir(path){
1370
+ const key=path||'.';
1371
+ if(loadingDirs.has(key))return;
1372
+ loadingDirs.add(key);
1373
+ renderExplorer();
1374
+ try {
1375
+ const data=await api('/api/files?path='+encodeURIComponent(key));
1376
+ if(data.type==='directory'){
1377
+ dirCache.set(data.path, data);
1378
+ document.getElementById('fileCount').textContent=String(data.entries.length);
1379
+ } else {
1380
+ openFile(data.path, data);
1381
+ }
1382
+ } catch (err) {
1383
+ addTrace('file_error',{path:key,message:err.message});
1384
+ } finally {
1385
+ loadingDirs.delete(key);
1386
+ renderExplorer();
1387
+ }
1388
+ }
1389
+ async function openFile(path, knownData){
1390
+ currentPath=path;
1391
+ if(!openTabs.some(t=>t.path===path))openTabs.push({path,name:basename(path)});
1392
+ activePath=path;
1393
+ renderTabs();
1394
+ renderExplorer();
1395
+ let data=knownData||fileCache.get(path);
1396
+ if(!data){
1397
+ renderViewer('<div class="file-header"><span class="path">'+esc(path)+'</span></div><div class="empty">Loading...</div>');
1398
+ data=await api('/api/files?path='+encodeURIComponent(path));
1399
+ fileCache.set(path,data);
1400
+ }
1401
+ renderFile(data);
1402
+ }
1403
+ function closeTab(path){
1404
+ const i=openTabs.findIndex(t=>t.path===path);
1405
+ if(i>=0)openTabs.splice(i,1);
1406
+ if(activePath===path){
1407
+ const next=openTabs[Math.max(0,i-1)]||openTabs[0]||null;
1408
+ activePath=next?next.path:null;
1409
+ if(next) openFile(next.path);
1410
+ else renderEmptyViewer();
1411
+ }
1412
+ renderTabs();
1413
+ }
1414
+ function renderTabs(){
1415
+ const bar=document.getElementById('tabbar');
1416
+ const tabs=openTabs.map(tab=>'<button class="tab '+(tab.path===activePath?'active':'')+'" data-tab="'+esc(tab.path)+'" title="'+esc(tab.path)+'">'+(dirtyPaths.has(tab.path)?'<span class="tab-dirty" title="Unsaved changes"></span>':'')+'<span>◇</span><span class="tab-name">'+esc(tab.name)+'</span><span class="tab-close" data-close="'+esc(tab.path)+'">×</span></button>').join('');
1417
+ bar.innerHTML=tabs+'<div class="tab-muted"></div>';
1418
+ bar.querySelectorAll('[data-tab]').forEach(btn=>btn.addEventListener('click',e=>{if(e.target.dataset.close)return;openFile(btn.dataset.tab);}));
1419
+ bar.querySelectorAll('[data-close]').forEach(btn=>btn.addEventListener('click',e=>{e.stopPropagation();closeTab(btn.dataset.close);}));
1420
+ }
1421
+ function renderExplorer(){
1422
+ const root=dirCache.get('.');
1423
+ const list=document.getElementById('fileList');
1424
+ if(!root){list.innerHTML='<div class="loading-row">Loading...</div>';return;}
1425
+ list.innerHTML=renderEntries(root.entries||[],0);
1426
+ list.querySelectorAll('[data-dir]').forEach(btn=>btn.addEventListener('click',async()=>{const p=btn.dataset.dir;if(expandedDirs.has(p)&&p!=='.')expandedDirs.delete(p);else{expandedDirs.add(p);if(!dirCache.has(p))await loadDir(p);}renderExplorer();}));
1427
+ list.querySelectorAll('[data-file]').forEach(btn=>btn.addEventListener('click',()=>openFile(btn.dataset.file)));
1428
+ }
1429
+ function renderEntries(entries,depth){
1430
+ return entries.map(entry=>{
1431
+ const pad=8+depth*13;
1432
+ if(entry.type==='directory'){
1433
+ const open=expandedDirs.has(entry.path);
1434
+ const child=dirCache.get(entry.path);
1435
+ const rows=open?(child?renderEntries(child.entries||[],depth+1):'<div class="loading-row" style="padding-left:'+(pad+24)+'px">Loading...</div>'):'';
1436
+ return '<button class="row" style="padding-left:'+pad+'px" data-dir="'+esc(entry.path)+'"><span class="chev">'+(open?'⌄':'›')+'</span><span class="icon">▦</span><span class="name">'+esc(entry.name)+'</span></button>'+rows;
1437
+ }
1438
+ return '<button class="row '+(entry.path===activePath?'selected':'')+'" style="padding-left:'+pad+'px" data-file="'+esc(entry.path)+'"><span class="chev"></span><span class="icon">◇</span><span class="name">'+esc(entry.name)+'</span><span class="sub">'+(entry.size!=null?formatBytes(entry.size):'')+'</span></button>';
1439
+ }).join('');
1440
+ }
1441
+ function renderEmptyViewer(){
1442
+ renderViewer('<div class="empty">Select a file.</div>');
1443
+ }
1444
+ function renderFile(data){
1445
+ currentPath=data.path;
1446
+ const text=data.preview&&data.preview.content!=null?sourceTextForRender(data):null;
1447
+ const kind=fileKind(data);
1448
+ if(kind==='image'){renderImage(data);return;}
1449
+ if(kind==='pdf'){renderPdf(data);return;}
1450
+ if(kind==='markdown'&&text!=null){renderMarkdownFile(data,text);return;}
1451
+ if(kind==='mermaid'&&text!=null){renderMermaidFile(data,text);return;}
1452
+ if(kind==='drawio'){renderDrawioFile(data,text);return;}
1453
+ if(kind==='table'&&text!=null){renderTableFile(data,text);return;}
1454
+ if(kind==='notebook'&&text!=null){renderNotebookFile(data,text);return;}
1455
+ if(kind==='spreadsheet'){renderSpreadsheetFile(data);return;}
1456
+ if(kind==='document'||kind==='presentation'){renderOfficeFile(data,kind);return;}
1457
+ if(text!=null){
1458
+ renderCodeEditor(data,text,fileLanguage(data),{originalText:currentSavedText(data)});
1459
+ return;
1460
+ }
1461
+ renderUnsupportedFile(data,kind);
1462
+ }
1463
+ function renderCodeEditor(data,text,language,{auto=true,originalText=null}={}){
1464
+ const editable=canEditFile(data);
1465
+ const savedText=String(originalText ?? currentSavedText(data));
1466
+ const value=unsavedFiles.has(data.path)?unsavedFiles.get(data.path):String(text||'');
1467
+ const header=fileHeader(data,{language,languageAuto:auto,truncated:Boolean(data.preview&&data.preview.truncated),save:editable});
1468
+ const hostId='monaco_'+Date.now().toString(36)+'_'+Math.random().toString(36).slice(2);
1469
+ renderViewer(header+'<div class="monaco-host" data-preview-content id="'+hostId+'"></div>');
1470
+ bindHeaderActions(data.path);
1471
+ loadMonaco().then(monaco=>{
1472
+ const host=document.getElementById(hostId);
1473
+ if(!host)return;
1474
+ activeEditorPath=data.path;
1475
+ activeEditorOriginal=savedText;
1476
+ activeEditor=monaco.editor.create(host,{
1477
+ value,
1478
+ language:language||'plaintext',
1479
+ theme:'vs',
1480
+ readOnly:!editable,
1481
+ minimap:{enabled:false},
1482
+ fontSize:13,
1483
+ fontFamily:'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
1484
+ lineNumbers:'on',
1485
+ scrollBeyondLastLine:false,
1486
+ wordWrap:'on',
1487
+ padding:{top:8},
1488
+ tabSize:2,
1489
+ renderWhitespace:'selection',
1490
+ bracketPairColorization:{enabled:true},
1491
+ automaticLayout:true,
1492
+ smoothScrolling:true
1493
+ });
1494
+ activeEditor.onDidChangeModelContent(()=>{
1495
+ const next=activeEditor.getValue();
1496
+ setFileDirty(data.path,next!==activeEditorOriginal,next);
1497
+ });
1498
+ updateSaveButtons(data.path);
1499
+ }).catch(err=>{
1500
+ addTrace('preview_editor_fallback',{path:data.path,message:err.message});
1501
+ const viewer=document.getElementById('viewer');
1502
+ if(viewer) viewer.innerHTML=header+'<div class="code-fallback" data-preview-content>'+renderCode(value)+'</div>';
1503
+ bindHeaderActions(data.path);
1504
+ });
1505
+ }
1506
+ function renderCode(text){
1507
+ const lines=String(text||'').split('\\n');
1508
+ const nums=lines.map((_,i)=>i+1).join('\\n');
1509
+ return '<div class="code-wrap"><pre class="line-nums">'+nums+'</pre><pre class="code-pre">'+esc(text)+'</pre></div>';
1510
+ }
1511
+ function notebookSource(value){
1512
+ if(Array.isArray(value))return value.map(notebookSource).join('');
1513
+ if(value&&typeof value==='object')return JSON.stringify(value,null,2);
1514
+ return String(value ?? '');
1515
+ }
1516
+ function splitNotebookSource(value){
1517
+ const text=String(value ?? '');
1518
+ if(!text)return [];
1519
+ const lines=text.split('\\n');
1520
+ return lines.map((line,index)=>index<lines.length-1?line+'\\n':line).filter((line,index)=>line||index<lines.length-1);
1521
+ }
1522
+ function setNotebookCellSource(cell,value){
1523
+ if(!cell||typeof cell!=='object')return;
1524
+ const key=cell.source!=null?'source':cell.input!=null?'input':cell.text!=null?'text':'source';
1525
+ const previous=cell[key];
1526
+ cell[key]=Array.isArray(previous)?splitNotebookSource(value):String(value ?? '');
1527
+ }
1528
+ function renderNotebookFile(data,text,overrideLanguage=null,{auto=true}={}){
1529
+ let notebook;
1530
+ const notebookText=sourceTextForRender(data);
1531
+ try{notebook=JSON.parse(notebookText||'{}');}catch{
1532
+ renderCodeEditor(data,notebookText,fileLanguage(data),{originalText:currentSavedText(data)});
1533
+ return;
1534
+ }
1535
+ const cells=notebookCells(notebook);
1536
+ const notebookMode=overrideLanguage==='notebook';
1537
+ const detected=monacoLanguageForNotebook((notebookMode?null:overrideLanguage)||detectNotebookLanguage(notebook)||'python');
1538
+ const language=detected||'python';
1539
+ const selectedLanguage=notebookMode?'notebook':language;
1540
+ const header=fileHeader(data,{language:selectedLanguage,languageAuto:auto,truncated:Boolean(data.preview&&data.preview.truncated),extraActions:'<span class="file-chip">'+cells.length+' cells</span>',save:canEditFile(data),editSource:canEditFile(data)});
1541
+ const meta=[
1542
+ notebookVersionLabel(notebook),
1543
+ languageLabel(language),
1544
+ cells.length+' cells'
1545
+ ].join(' · ');
1546
+ const visible=cells.slice(0,200);
1547
+ const cellDescriptors=[];
1548
+ const body=visible.length?visible.map((cell,index)=>renderNotebookCell(cell,index,language,cellDescriptors,canEditFile(data))).join(''):'<div class="empty">No cells.</div>';
1549
+ const truncated=cells.length>200?'<div class="viewer-note-card"><div class="viewer-note-title">Notebook truncated</div><div class="viewer-note-body">Showing first 200 cells in the browser preview.</div></div>':'';
1550
+ renderViewer(header+'<div class="notebook-preview" data-preview-content><div class="notebook-meta">'+meta+'</div>'+body+truncated+'</div>');
1551
+ bindHeaderActions(data.path);
1552
+ activeNotebookState={path:data.path,notebook,cells,originalText:currentSavedText(data)};
1553
+ renderMermaidBlocks();
1554
+ void mountNotebookCells(cellDescriptors);
1555
+ void renderLatexIn(document.getElementById('viewer'));
1556
+ }
1557
+ function notebookCells(notebook){
1558
+ if(Array.isArray(notebook?.cells))return notebook.cells;
1559
+ if(Array.isArray(notebook?.worksheets)){
1560
+ return notebook.worksheets.flatMap(sheet=>Array.isArray(sheet?.cells)?sheet.cells:[]);
1561
+ }
1562
+ return [];
1563
+ }
1564
+ function notebookVersionLabel(notebook){
1565
+ if(notebook?.nbformat)return 'nbformat '+esc(notebook.nbformat)+'.'+esc(notebook.nbformat_minor||0);
1566
+ return 'notebook';
1567
+ }
1568
+ function renderNotebookCell(cell,index,language,cellDescriptors,editable=false){
1569
+ const type=String(cell?.cell_type||'raw').toLowerCase();
1570
+ const source=notebookSource(cell?.source ?? cell?.input ?? cell?.text);
1571
+ const execCount=cell?.execution_count!=null?String(cell.execution_count):cell?.prompt_number!=null?String(cell.prompt_number):'';
1572
+ if(type==='markdown'||type==='heading'){
1573
+ const markdownSource=type==='heading'?'#'.repeat(Math.max(1,Math.min(6,Number(cell?.level)||1)))+' '+source:source;
1574
+ return '<section class="notebook-cell markdown" data-cell-type="markdown"><div class="notebook-cell-body notebook-markdown" data-katex-scan>'+markdown(markdownSource)+'</div></section>';
1575
+ }
1576
+ if(type==='raw'){
1577
+ return '<section class="notebook-cell raw" data-cell-type="raw"><div class="notebook-cell-body notebook-raw"><pre>'+esc(source)+'</pre></div></section>';
1578
+ }
1579
+ const hostId='nbc_'+Date.now().toString(36)+'_'+index+'_'+Math.random().toString(36).slice(2,6);
1580
+ cellDescriptors.push({hostId,source,language,cell,editable});
1581
+ const promptText=execCount?'['+esc(execCount)+']':'[ ]';
1582
+ const inPrompt='<div class="notebook-prompt in" title="Execution '+esc(execCount||'n/a')+'">'+promptText+':</div>';
1583
+ const codeBlock='<div class="notebook-cell-monaco" id="'+hostId+'"><pre class="notebook-code-fallback">'+esc(source)+'</pre></div>';
1584
+ const outputs=renderCellOutputs(cell?.outputs||[],execCount);
1585
+ return '<section class="notebook-cell code" data-cell-type="code">'+inPrompt+codeBlock+outputs+'</section>';
1586
+ }
1587
+ function renderCellOutputs(outputs,execCount){
1588
+ if(!Array.isArray(outputs)||!outputs.length)return '';
1589
+ const items=outputs.slice(0,12).map(o=>renderNotebookOutput(o,execCount)).join('');
1590
+ return '<div class="notebook-cell-outputs">'+items+'</div>';
1591
+ }
1592
+ function renderNotebookOutput(output,execCount){
1593
+ const type=String(output?.output_type||'output');
1594
+ const isResult=type==='execute_result'||type==='pyout';
1595
+ const prompt=isResult&&execCount
1596
+ ?'<div class="notebook-prompt out">['+esc(execCount)+']:</div>'
1597
+ :'<div class="notebook-prompt empty">&nbsp;</div>';
1598
+ const body=renderNotebookOutputBody(output,type);
1599
+ return '<div class="notebook-output-item">'+prompt+'<div class="notebook-output-body">'+body+'</div></div>';
1600
+ }
1601
+ function renderNotebookOutputBody(output,type){
1602
+ if(type==='stream'){
1603
+ const name=String(output.name||'stdout').toLowerCase();
1604
+ const cls=name==='stderr'?'notebook-stream stderr':'notebook-stream';
1605
+ return '<pre class="'+cls+'">'+ansiToHtml(notebookSource(output.text))+'</pre>';
1606
+ }
1607
+ if(type==='error'||type==='pyerr'){
1608
+ const traceback=Array.isArray(output.traceback)?output.traceback.join('\\n'):notebookSource(output.traceback);
1609
+ const fallback=(output.ename||'Error')+': '+(output.evalue||'');
1610
+ return '<pre class="notebook-error">'+ansiToHtml(traceback||fallback)+'</pre>';
1611
+ }
1612
+ const data=notebookOutputData(output);
1613
+ const png=data['image/png'];
1614
+ const jpeg=data['image/jpeg'];
1615
+ const svg=data['image/svg+xml'];
1616
+ const html=data['text/html'];
1617
+ const latex=data['text/latex'];
1618
+ const md=data['text/markdown'];
1619
+ const plain=data['text/plain'];
1620
+ const json=data['application/json'];
1621
+ if(png)return '<img alt="Notebook output" src="data:image/png;base64,'+esc(notebookSource(png))+'">';
1622
+ if(jpeg)return '<img alt="Notebook output" src="data:image/jpeg;base64,'+esc(notebookSource(jpeg))+'">';
1623
+ if(svg)return '<iframe sandbox="" srcdoc="'+esc(notebookSource(svg))+'" title="svg output"></iframe>';
1624
+ if(html)return '<iframe sandbox="allow-same-origin" srcdoc="'+esc(notebookSource(html))+'" title="html output"></iframe>';
1625
+ if(latex)return '<div class="notebook-latex">'+esc(notebookSource(latex))+'</div>';
1626
+ if(md)return '<div class="notebook-markdown" data-katex-scan>'+markdown(notebookSource(md))+'</div>';
1627
+ if(json)return '<pre class="notebook-json">'+esc(JSON.stringify(json,null,2))+'</pre>';
1628
+ if(plain)return '<pre class="notebook-plain">'+ansiToHtml(notebookSource(plain))+'</pre>';
1629
+ return '<pre class="notebook-plain">'+esc(JSON.stringify(output||{},null,2))+'</pre>';
1630
+ }
1631
+ function notebookOutputData(output){
1632
+ const data=output?.data&&typeof output.data==='object'?{...output.data}:{};
1633
+ if(output?.png!=null)data['image/png']=output.png;
1634
+ if(output?.jpeg!=null)data['image/jpeg']=output.jpeg;
1635
+ if(output?.svg!=null)data['image/svg+xml']=output.svg;
1636
+ if(output?.html!=null)data['text/html']=output.html;
1637
+ if(output?.markdown!=null)data['text/markdown']=output.markdown;
1638
+ if(output?.text!=null)data['text/plain']=output.text;
1639
+ return data;
1640
+ }
1641
+ function renderImage(data){
1642
+ activeImageScale=1;
1643
+ const zoomActions=fileIconButton('data-image-zoom="out"','minus','Zoom out')+fileIconButton('data-image-zoom="reset"','reset','Reset zoom')+fileIconButton('data-image-zoom="in"','plus','Zoom in');
1644
+ const header=fileHeader(data,{extraActions:zoomActions});
1645
+ renderViewer(header+'<div class="image-stage" data-preview-content><div class="image-toolbar"><span class="file-chip" id="imageScale">100%</span></div><div class="image-preview"><img id="imagePreview" src="'+rawUrl(data.path)+'" alt="'+esc(data.path)+'"></div></div>');
1646
+ bindHeaderActions(data.path);
1647
+ document.querySelectorAll('[data-image-zoom]').forEach(btn=>btn.addEventListener('click',()=>{
1648
+ const action=btn.dataset.imageZoom;
1649
+ if(action==='in')activeImageScale=Math.min(5,activeImageScale+.25);
1650
+ else if(action==='out')activeImageScale=Math.max(.25,activeImageScale-.25);
1651
+ else activeImageScale=1;
1652
+ const img=document.getElementById('imagePreview');
1653
+ const scale=document.getElementById('imageScale');
1654
+ if(img)img.style.transform='scale('+activeImageScale+')';
1655
+ if(scale)scale.textContent=Math.round(activeImageScale*100)+'%';
1656
+ }));
1657
+ }
1658
+ function renderPdf(data){
1659
+ const header=fileHeader(data);
1660
+ renderViewer(header+'<div class="frame-preview" data-preview-content><iframe src="'+rawUrl(data.path)+'" title="'+esc(data.path)+'"></iframe></div>');
1661
+ bindHeaderActions(data.path);
1662
+ }
1663
+ function renderMarkdownFile(data,text){
1664
+ const header=fileHeader(data,{language:'markdown',truncated:Boolean(data.preview&&data.preview.truncated),save:canEditFile(data),editSource:canEditFile(data)});
1665
+ renderViewer(header+'<div class="markdown-preview" data-preview-content>'+markdown(text)+'</div>');
1666
+ bindHeaderActions(data.path);
1667
+ renderMermaidBlocks();
1668
+ }
1669
+ function renderMermaidFile(data,text){
1670
+ const header=fileHeader(data,{language:'markdown',truncated:Boolean(data.preview&&data.preview.truncated),save:canEditFile(data),editSource:canEditFile(data)});
1671
+ renderViewer(header+'<div class="diagram-preview" data-preview-content>'+mermaidCard(text)+'</div>');
1672
+ bindHeaderActions(data.path);
1673
+ renderMermaidBlocks();
1674
+ }
1675
+ function renderDrawioFile(data,text){
1676
+ const pages=countDrawioPages(text);
1677
+ const title=pages>0?'Draw.io diagram with '+pages+' page'+(pages===1?'':'s'):'Draw.io diagram';
1678
+ const body=text?renderCode(text):'<div class="viewer-note" data-preview-content><div class="viewer-note-card"><div class="viewer-note-title">'+title+'</div><div class="viewer-note-body">This diagram file is available to the local agent, but the browser renderer is not bundled yet. Use Ask agent to inspect or convert it.</div></div></div>';
1679
+ const header=fileHeader(data,{language:text?'xml':null,truncated:Boolean(data.preview&&data.preview.truncated),save:canEditFile(data),editSource:canEditFile(data)});
1680
+ renderViewer(header+(text?'<div class="diagram-preview" data-preview-content><div class="viewer-note-card"><div class="viewer-note-title">'+title+'</div><div class="viewer-note-body">Source preview shown for now. A bundled Draw.io renderer can be added without exposing raw binary content.</div></div>'+body+'</div>':body));
1681
+ bindHeaderActions(data.path);
1682
+ }
1683
+ function countDrawioPages(text){
1684
+ const value=String(text||'');
1685
+ const matches=value.match(/<diagram\\b/g);
1686
+ return matches?matches.length:0;
1687
+ }
1688
+ function renderTableFile(data,text){
1689
+ const ext=extname(data.path);
1690
+ const header=fileHeader(data,{language:ext==='csv'?'plaintext':'plaintext',truncated:Boolean(data.preview&&data.preview.truncated),save:canEditFile(data),editSource:canEditFile(data)});
1691
+ renderViewer(header+renderTable(text,ext==='tsv'?'\\t':','));
1692
+ bindHeaderActions(data.path);
1693
+ }
1694
+ async function renderSpreadsheetFile(data){
1695
+ const header=fileHeader(data);
1696
+ renderViewer(header+'<div class="table-preview" data-preview-content><div class="table-meta">Loading workbook preview...</div></div>');
1697
+ bindHeaderActions(data.path);
1698
+ try{
1699
+ const preview=await api('/api/file/spreadsheet-preview?path='+encodeURIComponent(data.path));
1700
+ renderSpreadsheetSheet(data,preview,0);
1701
+ }catch(err){
1702
+ renderPreviewError(data,'Spreadsheet preview failed',err.message||String(err),'spreadsheet');
1703
+ }
1704
+ }
1705
+ function renderSpreadsheetSheet(data,preview,index){
1706
+ const sheets=Array.isArray(preview.sheets)?preview.sheets:[];
1707
+ const sheet=sheets[index]||sheets[0];
1708
+ if(!sheet){
1709
+ renderPreviewError(data,'Spreadsheet preview','No sheets were found in this workbook.','spreadsheet');
1710
+ return;
1711
+ }
1712
+ const select=sheets.length>1?'<select class="file-action" id="sheetSelect" title="Sheet">'+sheets.map((s,i)=>'<option value="'+i+'" '+(i===index?'selected':'')+'>'+esc(s.name)+'</option>').join('')+'</select>':'';
1713
+ const header=fileHeader(data,{extraActions:select});
1714
+ const meta=[
1715
+ esc(sheet.name),
1716
+ esc(sheet.row_count)+' rows',
1717
+ esc(sheet.column_count)+' columns',
1718
+ sheet.truncated_rows?'rows truncated':'',
1719
+ sheet.truncated_columns?'columns truncated':'',
1720
+ preview.truncated_sheets?'sheets truncated':''
1721
+ ].filter(Boolean).join(' · ');
1722
+ renderViewer(header+'<div class="table-preview" data-preview-content><div class="table-meta">'+meta+'</div>'+renderWorkbookTable(sheet)+'</div>');
1723
+ bindHeaderActions(data.path);
1724
+ document.getElementById('sheetSelect')?.addEventListener('change',e=>renderSpreadsheetSheet(data,preview,Number(e.target.value)||0));
1725
+ }
1726
+ function renderWorkbookTable(sheet){
1727
+ const columns=Array.isArray(sheet.columns)?sheet.columns:[];
1728
+ const rows=Array.isArray(sheet.rows)?sheet.rows:[];
1729
+ if(!columns.length||!rows.length)return '<div class="empty">No visible cells.</div>';
1730
+ return '<table><thead><tr><th></th>'+columns.map(col=>'<th>'+esc(col)+'</th>').join('')+'</tr></thead><tbody>'+rows.map(row=>{
1731
+ const cells=Array.isArray(row.cells)?row.cells:[];
1732
+ return '<tr><th>'+esc(row.number)+'</th>'+columns.map((_,i)=>{
1733
+ const cell=cells[i]||{};
1734
+ const value=cell.value||'';
1735
+ const formula=cell.formula;
1736
+ const cls=formula?' class="formula-cell"':'';
1737
+ const title=formula?' title="'+esc(formula)+'"':'';
1738
+ return '<td'+cls+title+'>'+esc(value)+'</td>';
1739
+ }).join('')+'</tr>';
1740
+ }).join('')+'</tbody></table>';
1741
+ }
1742
+ async function renderOfficeFile(data,kind){
1743
+ const label=kind==='presentation'?'Presentation preview':'Document preview';
1744
+ const header=fileHeader(data);
1745
+ renderViewer(header+'<div class="viewer-note" data-preview-content><div class="viewer-note-card"><div class="viewer-note-title">'+label+'</div><div class="viewer-note-body">Converting locally for preview...</div></div></div>');
1746
+ bindHeaderActions(data.path);
1747
+ try{
1748
+ const preview=await api('/api/file/office-preview?path='+encodeURIComponent(data.path));
1749
+ if(preview.ok&&preview.preview_url){
1750
+ renderOfficePdf(data,preview);
1751
+ return;
1752
+ }
1753
+ renderPreviewError(data,label,preview.message||'Conversion preview is unavailable.',kind,preview.install_hint||'');
1754
+ }catch(err){
1755
+ renderPreviewError(data,label,err.message||String(err),kind);
1756
+ }
1757
+ }
1758
+ function renderOfficePdf(data,preview){
1759
+ const header=fileHeader(data,{extraActions:'<span class="file-chip">converted PDF</span>'});
1760
+ renderViewer(header+'<div class="frame-preview" data-preview-content><iframe src="'+esc(preview.preview_url)+'" title="'+esc(data.path)+' preview"></iframe></div>');
1761
+ bindHeaderActions(data.path);
1762
+ }
1763
+ function renderPreviewError(data,title,message,kind,hint=''){
1764
+ const header=fileHeader(data);
1765
+ renderViewer(header+'<div class="viewer-note" data-preview-content><div class="viewer-note-card"><div class="viewer-note-title">'+esc(title)+'</div><div class="viewer-note-body">'+esc(message)+(hint?'<br><br>'+esc(hint):'')+'</div><div class="viewer-note-actions"><button class="file-action" type="button" data-ask-file="'+esc(data.path)+'" data-ask-kind="'+esc(kind)+'">Ask agent to inspect</button></div></div></div>');
1766
+ bindHeaderActions(data.path);
1767
+ }
1768
+ function renderUnsupportedFile(data,kind){
1769
+ const file=data.file||{};
1770
+ const title={
1771
+ spreadsheet:'Spreadsheet preview',
1772
+ document:'Document preview',
1773
+ presentation:'Presentation preview',
1774
+ unsupported:'Preview unavailable'
1775
+ }[kind]||'Preview unavailable';
1776
+ const body={
1777
+ spreadsheet:'The local agent can inspect sheets, formulas, and tables. A formatted grid renderer is the next viewer to add.',
1778
+ document:'The local agent can read or convert this document locally. A formatted DOCX/PDF conversion preview is the next viewer to add.',
1779
+ presentation:'The local agent can inspect slides and embedded media. A formatted slide preview is the next viewer to add.',
1780
+ unsupported:'This file type is not rendered in the browser yet. It is still available to the local agent and CLI tools.'
1781
+ }[kind]||'This file type is not rendered in the browser yet.';
1782
+ const header=fileHeader(data);
1783
+ renderViewer(header+'<div class="viewer-note" data-preview-content><div class="viewer-note-card"><div class="viewer-note-title">'+esc(title)+'</div><div class="viewer-note-body">'+esc(body)+'<br><br>'+esc(file.label||kind)+' · '+(file.size!=null?esc(formatBytes(file.size)):'unknown size')+'</div><div class="viewer-note-actions"><button class="file-action" type="button" data-ask-file="'+esc(data.path)+'" data-ask-kind="'+esc(kind)+'">Ask agent to inspect</button></div></div></div>');
1784
+ bindHeaderActions(data.path);
1785
+ }
1786
+ function parseDelimited(text,delim){
1787
+ const rows=[];let row=[];let cell='';let quoted=false;
1788
+ for(let i=0;i<text.length;i++){const ch=text[i];const next=text[i+1];if(ch==='"'&&quoted&&next==='"'){cell+='"';i++;continue;}if(ch==='"'){quoted=!quoted;continue;}if(ch===delim&&!quoted){row.push(cell);cell='';continue;}if((ch==='\\n'||ch==='\\r')&&!quoted){if(ch==='\\r'&&next==='\\n')i++;row.push(cell);rows.push(row);row=[];cell='';if(rows.length>=200)break;continue;}cell+=ch;}
1789
+ if(cell||row.length){row.push(cell);rows.push(row);}
1790
+ return rows;
1791
+ }
1792
+ function renderTable(text,delim){
1793
+ const rows=parseDelimited(text,delim);
1794
+ if(!rows.length)return '<div class="empty">No rows.</div>';
1795
+ const head=rows[0]||[];
1796
+ const body=rows.slice(1);
1797
+ return '<div class="table-preview" data-preview-content><div class="table-meta">'+esc(rows.length-1)+' rows shown, '+esc(head.length)+' columns</div><table><thead><tr>'+head.map(h=>'<th>'+esc(h)+'</th>').join('')+'</tr></thead><tbody>'+body.map(r=>'<tr>'+head.map((_,i)=>'<td>'+esc(r[i]||'')+'</td>').join('')+'</tr>').join('')+'</tbody></table></div>';
1798
+ }
1799
+ function markdown(text){
1800
+ const lines=String(text||'').split('\\n');
1801
+ let out='';let inUl=false;let inPre=false;let preLang='';let preLines=[];
1802
+ const fence=String.fromCharCode(96,96,96);
1803
+ for(const raw of lines){const line=String(raw).trimEnd();
1804
+ if(line.startsWith(fence)){
1805
+ if(inUl){out+='</ul>';inUl=false;}
1806
+ if(inPre){
1807
+ const code=preLines.join('\\n');
1808
+ out+=preLang==='mermaid'?mermaidCard(code):'<pre><code>'+esc(code)+'</code></pre>';
1809
+ inPre=false;preLang='';preLines=[];
1810
+ }else{
1811
+ preLang=line.slice(fence.length).trim().toLowerCase();
1812
+ inPre=true;preLines=[];
1813
+ }
1814
+ continue;
1815
+ }
1816
+ if(inPre){preLines.push(line);continue;}
1817
+ if(!line.trim()){if(inUl){out+='</ul>';inUl=false;}continue;}
1818
+ const h=line.match(/^(#{1,3})\\s+(.*)$/);if(h){if(inUl){out+='</ul>';inUl=false;}out+='<h'+h[1].length+'>'+inlineMd(h[2])+'</h'+h[1].length+'>';continue;}
1819
+ const li=line.match(/^[-*]\\s+(.*)$/);if(li){if(!inUl){out+='<ul>';inUl=true;}out+='<li>'+inlineMd(li[1])+'</li>';continue;}
1820
+ if(inUl){out+='</ul>';inUl=false;}out+='<p>'+inlineMd(line)+'</p>';
1821
+ }
1822
+ if(inUl)out+='</ul>';if(inPre)out+='<pre><code>'+esc(preLines.join('\\n'))+'</code></pre>';return out;
1823
+ }
1824
+ function inlineMd(s){const tick=String.fromCharCode(96);return s.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>');}
1825
+ function mermaidCard(source){
1826
+ return '<div class="mermaid-card" data-mermaid-card><div class="mermaid-output" data-mermaid-output>Rendering diagram...</div><pre class="mermaid-source">'+esc(source)+'</pre></div>';
1827
+ }
1828
+ async function renderMermaidBlocks(){
1829
+ const cards=[...document.querySelectorAll('[data-mermaid-card]')];
1830
+ if(!cards.length)return;
1831
+ try{
1832
+ const mermaid=await loadMermaid();
1833
+ for(const card of cards){
1834
+ const source=card.querySelector('.mermaid-source')?.textContent||'';
1835
+ const output=card.querySelector('[data-mermaid-output]');
1836
+ if(!output||!source.trim())continue;
1837
+ mermaidSeq+=1;
1838
+ const result=await mermaid.render('bahulam_mermaid_'+mermaidSeq,source);
1839
+ output.innerHTML=result.svg;
1840
+ result.bindFunctions?.(output);
1841
+ }
1842
+ }catch(err){
1843
+ for(const card of cards){
1844
+ const output=card.querySelector('[data-mermaid-output]');
1845
+ if(output)output.innerHTML='<span class="error">Mermaid preview failed: '+esc(err.message||err)+'</span>';
1846
+ }
1847
+ }
1848
+ }
1849
+ async function loadKatex(){
1850
+ if(katexReady)return katexReady;
1851
+ if(!document.querySelector('link[data-katex]')){
1852
+ const link=document.createElement('link');
1853
+ link.rel='stylesheet';
1854
+ link.href='/vendor/katex/katex.min.css';
1855
+ link.setAttribute('data-katex','1');
1856
+ document.head.appendChild(link);
1857
+ }
1858
+ katexReady=importWithoutAmdDefine('/vendor/katex/contrib/auto-render.mjs')
1859
+ .then(mod=>mod.default||mod.renderMathInElement||mod)
1860
+ .catch(err=>{katexReady=null;throw err;});
1861
+ return katexReady;
1862
+ }
1863
+ async function renderLatexIn(root){
1864
+ if(!root)return;
1865
+ const inline=[...root.querySelectorAll('[data-katex-scan]')];
1866
+ const blocks=[...root.querySelectorAll('.notebook-latex')];
1867
+ if(!inline.length&&!blocks.length)return;
1868
+ try{
1869
+ const renderMathInElement=await loadKatex();
1870
+ for(const el of inline){
1871
+ renderMathInElement(el,{delimiters:[{left:'$$',right:'$$',display:true},{left:'\\\\[',right:'\\\\]',display:true},{left:'$',right:'$',display:false},{left:'\\\\(',right:'\\\\)',display:false}],throwOnError:false});
1872
+ }
1873
+ for(const el of blocks){
1874
+ const raw=(el.textContent||'').trim();
1875
+ const wrapped=/^\\$\\$[\\s\\S]*\\$\\$$|^\\$[\\s\\S]*\\$$/.test(raw)?raw:'$$'+raw+'$$';
1876
+ el.textContent=wrapped;
1877
+ renderMathInElement(el,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false});
1878
+ }
1879
+ }catch(err){
1880
+ addTrace('katex_render_failed',{message:err.message||String(err)});
1881
+ }
1882
+ }
1883
+ const ANSI_COLORS={30:'#374151',31:'#DC2626',32:'#059669',33:'#B45309',34:'#2563EB',35:'#7C3AED',36:'#0891B2',37:'#4B5563',90:'#6B7280',91:'#EF4444',92:'#10B981',93:'#F59E0B',94:'#3B82F6',95:'#8B5CF6',96:'#06B6D4',97:'#111827'};
1884
+ function ansiToHtml(text){
1885
+ const s=String(text??'');
1886
+ if(!s)return '';
1887
+ const re=/\\u001b\\[([0-9;]*)m/g;
1888
+ let html='',last=0,openTags=0,m;
1889
+ const closeAll=()=>{while(openTags>0){html+='</span>';openTags--;}};
1890
+ while((m=re.exec(s))!==null){
1891
+ if(m.index>last)html+=esc(s.slice(last,m.index));
1892
+ const codes=m[1].split(';').filter(x=>x!=='').map(Number);
1893
+ if(!codes.length||codes.includes(0)){closeAll();}
1894
+ else{
1895
+ let color=null,bg=null,bold=false,italic=false,underline=false;
1896
+ for(const n of codes){
1897
+ if(n===1)bold=true;
1898
+ else if(n===3)italic=true;
1899
+ else if(n===4)underline=true;
1900
+ else if(ANSI_COLORS[n])color=ANSI_COLORS[n];
1901
+ else if(n>=40&&n<=47)bg=ANSI_COLORS[n-10];
1902
+ else if(n>=100&&n<=107)bg=ANSI_COLORS[n-10];
1903
+ }
1904
+ const styles=[];
1905
+ if(color)styles.push('color:'+color);
1906
+ if(bg)styles.push('background:'+bg);
1907
+ if(bold)styles.push('font-weight:600');
1908
+ if(italic)styles.push('font-style:italic');
1909
+ if(underline)styles.push('text-decoration:underline');
1910
+ if(styles.length){html+='<span style="'+styles.join(';')+'">';openTags++;}
1911
+ }
1912
+ last=re.lastIndex;
1913
+ }
1914
+ if(last<s.length)html+=esc(s.slice(last));
1915
+ closeAll();
1916
+ return html;
1917
+ }
1918
+ async function mountNotebookCells(descriptors){
1919
+ if(!descriptors||!descriptors.length)return;
1920
+ let monaco;
1921
+ try{monaco=await loadMonaco();}catch{return;}
1922
+ for(const {hostId,source,language,cell,editable} of descriptors){
1923
+ const host=document.getElementById(hostId);
1924
+ if(!host)continue;
1925
+ host.querySelector('.notebook-code-fallback')?.remove();
1926
+ // Rough initial height so the page doesn't jump when Monaco settles.
1927
+ // Real height comes from onDidContentSizeChange once tokenization + wrap
1928
+ // are computed. Line count without wrap is a good lower bound.
1929
+ const lineCount=Math.max(1,String(source||'').split('\\n').length);
1930
+ const preHeight=Math.min(1200,Math.max(80,lineCount*19+22));
1931
+ host.style.height=preHeight+'px';
1932
+ let editor;
1933
+ try{
1934
+ editor=monaco.editor.create(host,{
1935
+ value:String(source||''),
1936
+ language:language||'plaintext',
1937
+ theme:'vs',
1938
+ readOnly:!editable,
1939
+ minimap:{enabled:false},
1940
+ fontSize:13,
1941
+ fontFamily:'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
1942
+ lineNumbers:'off',
1943
+ scrollBeyondLastLine:false,
1944
+ wordWrap:'on',
1945
+ wrappingStrategy:'advanced',
1946
+ padding:{top:8,bottom:8},
1947
+ tabSize:4,
1948
+ renderWhitespace:'none',
1949
+ bracketPairColorization:{enabled:true},
1950
+ automaticLayout:true,
1951
+ scrollbar:{alwaysConsumeMouseWheel:false,verticalScrollbarSize:0,horizontalScrollbarSize:0,handleMouseWheel:false},
1952
+ guides:{indentation:false},
1953
+ renderLineHighlight:'none',
1954
+ overviewRulerLanes:0,
1955
+ hideCursorInOverviewRuler:true,
1956
+ occurrencesHighlight:false,
1957
+ contextmenu:false,
1958
+ stickyScroll:{enabled:false},
1959
+ });
1960
+ }catch(err){
1961
+ const pre=document.createElement('pre');
1962
+ pre.className='notebook-code-fallback';
1963
+ pre.textContent=String(source||'');
1964
+ host.replaceChildren(pre);
1965
+ continue;
1966
+ }
1967
+ notebookCellEditors.push({editor,cell});
1968
+ editor.onDidChangeModelContent(()=>{
1969
+ if(!activeNotebookState)return;
1970
+ setNotebookCellSource(cell,editor.getValue());
1971
+ const next=serializeNotebook(activeNotebookState.notebook);
1972
+ setFileDirty(activeNotebookState.path,next!==activeNotebookState.originalText,next);
1973
+ });
1974
+ // Auto-size cell to its FULL content — no in-cell vertical scrollbar,
1975
+ // no clip. The outer .notebook-preview handles page-level scrolling.
1976
+ let lastAppliedHeight=-1;
1977
+ const syncHeight=()=>{
1978
+ const raw=editor.getContentHeight();
1979
+ if(raw<=0)return;
1980
+ const target=Math.max(60,raw);
1981
+ if(Math.abs(target-lastAppliedHeight)<2)return;
1982
+ lastAppliedHeight=target;
1983
+ host.style.height=target+'px';
1984
+ try{editor.layout();}catch{}
1985
+ };
1986
+ editor.onDidContentSizeChange(syncHeight);
1987
+ // Kick off after first paint so tokenization + wrap have run.
1988
+ setTimeout(syncHeight,0);
1989
+ }
1990
+ }
1991
+ function addTrace(type, detail){
1992
+ const el = document.createElement('div');
1993
+ el.className = 'trace-row';
1994
+ const text = typeof detail === 'string' ? detail : JSON.stringify(detail || {});
1995
+ el.innerHTML = '<b>' + esc(type) + '</b> ' + esc(text || '');
1996
+ const events = document.getElementById('events');
1997
+ events.appendChild(el);
1998
+ events.scrollTop = events.scrollHeight;
1999
+ traceCount += 1;
2000
+ const traceTab=document.getElementById('traceTab');
2001
+ if(traceTab) traceTab.textContent='Trace ' + traceCount;
2002
+ }
2003
+ function resetThread(emptyText='Ask about this local workspace.'){
2004
+ activeTurn=null;
2005
+ document.getElementById('threadInner').innerHTML='<div class="empty-chat" id="emptyChat">'+esc(emptyText)+'</div>';
2006
+ }
2007
+ function attachmentHtml(attachments){
2008
+ const files=Array.isArray(attachments)?attachments:[];
2009
+ if(!files.length)return '';
2010
+ return '<div class="message-attachments">'+files.map(file=>'<button type="button" class="message-attachment" data-open-upload="'+esc(file.path)+'"><span>'+esc(file.name||basename(file.path))+'</span><small>'+esc(attachmentHint(file))+' · '+esc(formatBytes(file.size))+'</small></button>').join('')+'</div>';
2011
+ }
2012
+ function addUserMessage(text, attachments=[]){
2013
+ document.getElementById('emptyChat')?.remove();
2014
+ const el=document.createElement('div');
2015
+ el.className='msg user';
2016
+ el.innerHTML='<div class="bubble">'+esc(text)+attachmentHtml(attachments)+'</div>';
2017
+ document.getElementById('threadInner').appendChild(el);
2018
+ el.querySelectorAll('[data-open-upload]').forEach(btn=>btn.addEventListener('click',()=>openFile(btn.dataset.openUpload)));
2019
+ scrollThread();
2020
+ }
2021
+ function addAssistantMessage(text){
2022
+ document.getElementById('emptyChat')?.remove();
2023
+ const el=document.createElement('div');
2024
+ el.className='msg assistant';
2025
+ el.innerHTML='<div class="assistant-stack"><div class="assistant-bubble message-content">'+markdown(text)+'</div></div>';
2026
+ document.getElementById('threadInner').appendChild(el);
2027
+ scrollThread();
2028
+ }
2029
+ function renderHistorySnapshot(history){
2030
+ resetThread(history.backend_session_id ? 'No chat messages in this transcript yet.' : 'Ask about this local workspace.');
2031
+ const messages=Array.isArray(history.messages)?history.messages:[];
2032
+ const trace=Array.isArray(history.trace)?history.trace:[];
2033
+ if(history.backend_session_id){
2034
+ document.getElementById('bridgeState').textContent='resumed';
2035
+ document.getElementById('sendStatus').textContent='Resumed '+compactId(history.backend_session_id);
2036
+ setSessionButtonLabel('Chat: '+compactId(history.backend_session_id));
2037
+ } else {
2038
+ document.getElementById('bridgeState').textContent='new session';
2039
+ document.getElementById('sendStatus').textContent='';
2040
+ setSessionButtonLabel('Chat: New');
2041
+ }
2042
+ for(const item of trace){
2043
+ addTrace(item.type || 'history_tool', {tool:item.tool, kind:item.kind, content:item.content});
2044
+ }
2045
+ for(const msg of messages){
2046
+ if(msg.role==='user')addUserMessage(msg.content||'');
2047
+ else if(msg.role==='assistant')addAssistantMessage(msg.content||'');
2048
+ }
2049
+ scrollThread();
2050
+ setAgentTab('chat');
2051
+ }
2052
+ function setSessionButtonLabel(label){
2053
+ const button=document.getElementById('sessionMenuButton');
2054
+ if(button) button.textContent=label;
2055
+ }
2056
+ function setSessionMenuOpen(open){
2057
+ sessionMenuOpen=!!open;
2058
+ const modal=document.getElementById('sessionModal');
2059
+ const button=document.getElementById('sessionMenuButton');
2060
+ if(modal) modal.hidden=!sessionMenuOpen;
2061
+ if(button) button.setAttribute('aria-expanded', String(sessionMenuOpen));
2062
+ if(sessionMenuOpen){
2063
+ const first=document.querySelector('#sessionMenu button');
2064
+ if(first) first.focus();
2065
+ } else if(document.activeElement && modal && modal.contains(document.activeElement)) {
2066
+ button?.focus();
2067
+ }
2068
+ }
2069
+ function setApprovalModalOpen(open){
2070
+ const modal=document.getElementById('approvalModal');
2071
+ if(modal) modal.hidden=true;
2072
+ }
2073
+ function showApprovalModal(data){
2074
+ pendingApproval=data||null;
2075
+ if(!pendingApproval)return;
2076
+ setApprovalModalOpen(false);
2077
+ pendingApprovalEl=attachApprovalToActivity(pendingApproval);
2078
+ setAgentTab('chat');
2079
+ scrollThread();
2080
+ const first=document.querySelector('[data-approval-decision]');
2081
+ if(first) first.focus();
2082
+ }
2083
+ function approvalActivityKey(approval){
2084
+ return approval?.call_id || approval?.tool_id || approval?.request_id || approval?.approval_id || approval?.tool || 'approval';
2085
+ }
2086
+ function attachApprovalToActivity(approval){
2087
+ const turn=ensureAssistantTurn();
2088
+ const key=approvalActivityKey(approval);
2089
+ let row=turn.byKey.get(key);
2090
+ const prompt=approval?.prompt||{};
2091
+ if(!row){
2092
+ row={key,level:'approval',message:approval?.tool || prompt.subject || 'Tool approval',ts:Date.now(),kind:'tool',detail:approval?.args?JSON.stringify(approval.args,null,2):''};
2093
+ turn.activity.push(row);
2094
+ turn.byKey.set(key,row);
2095
+ }
2096
+ row.level='approval';
2097
+ row.kind='tool';
2098
+ row.message=row.message || approval?.tool || prompt.subject || 'Tool approval';
2099
+ row.approval=approval;
2100
+ row.approvalResult=null;
2101
+ row.approvalFading=false;
2102
+ renderActivity(turn);
2103
+ return {turn,key};
2104
+ }
2105
+ function approvalButtonClass(value){
2106
+ const decision=String(value||'');
2107
+ if(decision==='reject')return ' approval-reject';
2108
+ if(decision==='approve')return ' approval-approve';
2109
+ return ' approval-secondary';
2110
+ }
2111
+ function approvalOptions(approval){
2112
+ return Array.isArray(approval?.options)&&approval.options.length
2113
+ ? approval.options.filter(option=>['approve','reject'].includes(String(option.value||'')))
2114
+ : [{label:'approve once',value:'approve',hint:'run this call'},{label:'cancel',value:'reject',hint:'do not run'}];
2115
+ }
2116
+ function activityApprovalHtml(approval,fading=false){
2117
+ if(!approval)return '';
2118
+ const prompt=approval.prompt||{};
2119
+ const options=approvalOptions(approval);
2120
+ const actions=options.map(option=>{
2121
+ const intent=approvalButtonClass(option.value);
2122
+ const hint=option.hint?' title="'+esc(option.hint)+'"':'';
2123
+ return '<button class="icon-button'+intent+'" type="button" data-approval-id="'+esc(approval.approval_id||'')+'" data-approval-decision="'+esc(option.value)+'"'+hint+'>'+esc(option.label)+'</button>';
2124
+ }).join('');
2125
+ return '<div class="activity-approval '+(fading?'fading':'')+'" data-inline-approval="'+esc(approval.approval_id||'')+'"><div class="activity-approval-head"><div class="activity-approval-title">'+esc(prompt.title||('Approval · '+(approval.tool||'tool')))+'</div><div class="activity-approval-meta">'+esc((approval.tier||approval.risk||'approval') + ' · ' + compactId(approval.approval_id||''))+'</div></div><div class="activity-approval-subject">'+esc(prompt.subject || approval.tool || 'Tool request')+'</div><div class="activity-approval-reason">'+esc(prompt.reason || approval.reason || '')+'</div><div class="activity-approval-actions">'+actions+'</div></div>';
2126
+ }
2127
+ function approvalLineHtml(approval,decision,message=''){
2128
+ const approved=decision!=='reject';
2129
+ const prompt=approval?.prompt||{};
2130
+ const tool=approval?.tool||prompt.subject||'tool';
2131
+ const scope=approval?.scope?(' · '+approval.scope):'';
2132
+ const label=approved?'approved':'denied';
2133
+ const reason=message?(' · '+message):'';
2134
+ return '<div class="approval-result-line '+(approved?'approved':'denied')+'" title="'+esc(prompt.subject||tool)+'"><span>'+label+'</span><span class="approval-result-tool">'+esc(tool+scope+reason)+'</span></div>';
2135
+ }
2136
+ function findApprovalRow(approval){
2137
+ const key=approvalActivityKey(approval);
2138
+ if(activeTurn?.byKey?.has(key))return {turn:activeTurn,row:activeTurn.byKey.get(key)};
2139
+ return null;
2140
+ }
2141
+ function resolveInlineApproval(decision,message,approval=pendingApproval){
2142
+ const found=findApprovalRow(approval);
2143
+ if(!found)return;
2144
+ pendingApprovalEl=null;
2145
+ found.row.approvalFading=true;
2146
+ renderActivity(found.turn);
2147
+ setTimeout(()=>{
2148
+ found.row.approval=null;
2149
+ found.row.approvalFading=false;
2150
+ found.row.approvalResult={approval,decision,message};
2151
+ found.row.level=decision==='reject'?'error':'running';
2152
+ renderActivity(found.turn);
2153
+ scrollThread();
2154
+ },850);
2155
+ }
2156
+ async function submitApproval(decision){
2157
+ if(!pendingApproval?.approval_id)return;
2158
+ const approval=pendingApproval;
2159
+ document.querySelectorAll('[data-approval-decision]').forEach(btn=>{btn.disabled=true;});
2160
+ try {
2161
+ await api('/api/approvals/'+encodeURIComponent(approval.approval_id),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision})});
2162
+ addTrace('approval_decision', {approval_id:approval.approval_id, decision});
2163
+ resolveInlineApproval(decision,'',approval);
2164
+ } catch (err) {
2165
+ addTrace('approval_error', {approval_id:approval.approval_id, message:err.message});
2166
+ resolveInlineApproval('reject', err.message || 'Approval decision failed.', approval);
2167
+ } finally {
2168
+ pendingApproval=null;
2169
+ setApprovalModalOpen(false);
2170
+ }
2171
+ }
2172
+ function renderSessionMenu(sessions){
2173
+ historySessions=Array.isArray(sessions)?sessions:[];
2174
+ const menu=document.getElementById('sessionMenu');
2175
+ if(!menu)return;
2176
+ const rows=historySessions.slice(0,8).map(s=>{
2177
+ const tools=(Array.isArray(s.tools)?s.tools:[]).map(t=>'<span class="session-chip">'+esc(t)+'</span>').join('');
2178
+ const prompt=s.first_prompt || 'Untitled session';
2179
+ const meta=[formatSessionTime(s.last_activity_at||s.started_at), (s.user_messages||0)+' user', (s.assistant_messages||0)+' assistant', s.status||'unknown', compactId(s.session_id)].filter(Boolean).join(' · ');
2180
+ return '<div class="session-row"><div class="session-main"><div class="session-prompt">'+esc(prompt)+'</div><div class="session-meta">'+esc(meta)+'</div>'+(tools?'<div class="session-tools">'+tools+'</div>':'')+'</div><button class="icon-button" type="button" data-resume-session="'+esc(s.session_id)+'">Resume</button></div>';
2181
+ }).join('');
2182
+ const empty=historySessions.length?'':'<div class="session-menu-empty">No previous sessions for this folder.</div>';
2183
+ menu.innerHTML='<div class="session-list"><div class="session-row"><div class="session-main"><div class="session-prompt">New chat</div><div class="session-meta">Start without loading prior folder history</div></div><button class="icon-button" type="button" id="newHistorySession">Start</button></div>'+empty+rows+'</div>';
2184
+ menu.querySelector('#newHistorySession').addEventListener('click',startNewChat);
2185
+ menu.querySelectorAll('[data-resume-session]').forEach(btn=>btn.addEventListener('click',()=>resumeChat(btn.dataset.resumeSession)));
2186
+ if(historySessions.length){
2187
+ document.getElementById('bridgeState').textContent='choose session';
2188
+ setSessionButtonLabel('Chat: Choose '+historySessions.length);
2189
+ } else {
2190
+ document.getElementById('bridgeState').textContent='new session';
2191
+ setSessionButtonLabel('Chat: New');
2192
+ }
2193
+ historySelectionReady=true;
2194
+ setSessionMenuOpen(true);
2195
+ }
2196
+ async function loadSessionChoices(){
2197
+ try {
2198
+ const result=await api('/api/chat/sessions');
2199
+ renderSessionMenu(result.sessions || []);
2200
+ } catch (err) {
2201
+ addTrace('history_error', {message:err.message});
2202
+ historySelectionReady=true;
2203
+ }
2204
+ }
2205
+ async function resumeChat(sessionId){
2206
+ if(!sessionId)return;
2207
+ document.getElementById('sendStatus').textContent='Resuming...';
2208
+ try {
2209
+ const history=await api('/api/chat/resume',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:sessionId})});
2210
+ setSessionMenuOpen(false);
2211
+ renderHistorySnapshot(history);
2212
+ } catch (err) {
2213
+ document.getElementById('sendStatus').textContent=err.message;
2214
+ addTrace('history_error', {message:err.message, session_id:sessionId});
2215
+ }
2216
+ }
2217
+ async function startNewChat(){
2218
+ document.getElementById('sendStatus').textContent='Starting new chat...';
2219
+ try {
2220
+ const history=await api('/api/chat/new',{method:'POST'});
2221
+ setSessionMenuOpen(false);
2222
+ renderHistorySnapshot(history);
2223
+ } catch (err) {
2224
+ document.getElementById('sendStatus').textContent=err.message;
2225
+ addTrace('history_error', {message:err.message});
2226
+ }
2227
+ }
2228
+ function ensureAssistantTurn(){
2229
+ if(activeTurn)return activeTurn;
2230
+ turnSeq+=1;
2231
+ const el=document.createElement('div');
2232
+ el.className='msg assistant';
2233
+ el.innerHTML='<div class="assistant-stack"><div class="activity-card" data-activity><button class="activity-head" type="button"><span class="activity-label">Starting...</span><span class="activity-rollup"></span><span>›</span></button><div class="activity-rows"></div></div><div class="assistant-bubble message-content" data-content></div></div>';
2234
+ document.getElementById('threadInner').appendChild(el);
2235
+ const activity=el.querySelector('[data-activity]');
2236
+ activity.querySelector('.activity-head').addEventListener('click',()=>{activity.classList.toggle('expanded');});
2237
+ activeTurn={id:'local_'+turnSeq,el,text:'',activity:[],byKey:new Map(),active:true};
2238
+ return activeTurn;
2239
+ }
2240
+ function addActivity(level,message,opts={}){
2241
+ const turn=ensureAssistantTurn();
2242
+ const key=opts.key||null;
2243
+ let row=key?turn.byKey.get(key):null;
2244
+ if(row){Object.assign(row,{level,message,...opts});}
2245
+ else{row={key,level,message,ts:Date.now(),...opts};turn.activity.push(row);if(key)turn.byKey.set(key,row);}
2246
+ renderActivity(turn);
2247
+ }
2248
+ function renderActivity(turn){
2249
+ const card=turn.el.querySelector('[data-activity]');
2250
+ const rows=turn.activity.filter(a=>a.message&&String(a.message).trim());
2251
+ if(!rows.length&&!turn.active){card.classList.remove('active');return;}
2252
+ card.classList.add('active');
2253
+ const latest=rows[rows.length-1];
2254
+ card.querySelector('.activity-label').textContent=latest?latest.message:'Starting...';
2255
+ const tools=rows.filter(r=>r.kind==='tool').length;
2256
+ const errors=rows.filter(r=>r.level==='error').length;
2257
+ card.querySelector('.activity-rollup').textContent=[tools?tools+' tools':'',errors?errors+' errors':''].filter(Boolean).join(' · ');
2258
+ card.querySelector('.activity-rows').innerHTML=rows.slice(card.classList.contains('expanded')?0:-4).map(r=>{
2259
+ const approval=r.approval?activityApprovalHtml(r.approval,r.approvalFading):'';
2260
+ const result=r.approvalResult?'<div class="activity-result">'+approvalLineHtml(r.approvalResult.approval,r.approvalResult.decision,r.approvalResult.message)+'</div>':'';
2261
+ const detail=r.detail?'<pre>'+esc(r.detail)+'</pre>':'';
2262
+ return '<div class="activity-row '+esc(r.level||'')+'" data-activity-key="'+esc(r.key||'')+'"><span class="status-dot"></span><span class="activity-main"><span class="activity-message">'+esc(r.message)+'</span>'+approval+result+detail+'</span></div>';
2263
+ }).join('');
2264
+ card.querySelectorAll('[data-approval-decision]').forEach(btn=>btn.addEventListener('click',()=>submitApproval(btn.dataset.approvalDecision)));
2265
+ }
2266
+ function appendAssistantText(text){
2267
+ const turn=ensureAssistantTurn();
2268
+ turn.text+=text||'';
2269
+ turn.el.querySelector('[data-content]').innerHTML=markdown(turn.text);
2270
+ scrollThread();
2271
+ }
2272
+ function scrollThread(){const t=document.getElementById('thread');t.scrollTop=t.scrollHeight;}
2273
+ function setAgentTab(tab){
2274
+ const next=tab==='trace'?'trace':'chat';
2275
+ document.getElementById('chatPane')?.classList.toggle('active', next==='chat');
2276
+ document.getElementById('tracePane')?.classList.toggle('active', next==='trace');
2277
+ document.querySelectorAll('[data-agent-tab]').forEach(btn=>{
2278
+ const active=btn.dataset.agentTab===next;
2279
+ btn.classList.toggle('active', active);
2280
+ btn.setAttribute('aria-selected', String(active));
2281
+ });
2282
+ }
2283
+ function setTurnRunning(running){
2284
+ turnRunning=Boolean(running);
2285
+ if(!turnRunning)turnCancelling=false;
2286
+ const send=document.getElementById('send');
2287
+ const stop=document.getElementById('stopAgent');
2288
+ const prompt=document.getElementById('prompt');
2289
+ if(send){send.textContent=turnRunning?'Follow up':'Send';send.disabled=turnCancelling;}
2290
+ if(stop){stop.hidden=!turnRunning;stop.disabled=turnCancelling;}
2291
+ if(prompt)prompt.placeholder=turnRunning?'Send a follow-up to the running agent':'Ask about this local workspace';
2292
+ }
2293
+ function setTurnCancelling(cancelling){
2294
+ turnCancelling=Boolean(cancelling);
2295
+ const send=document.getElementById('send');
2296
+ const stop=document.getElementById('stopAgent');
2297
+ if(send)send.disabled=turnCancelling;
2298
+ if(stop)stop.disabled=turnCancelling;
2299
+ }
2300
+ function finishCancelledTurn(data={}){
2301
+ addActivity('error', data.reason || 'Cancelled by user', {key:'turn-cancelled', kind:'status'});
2302
+ if(activeTurn){activeTurn.active=false;renderActivity(activeTurn);activeTurn=null;}
2303
+ setTurnRunning(false);
2304
+ setTurnCancelling(false);
2305
+ document.getElementById('bridgeState').textContent='cancelled';
2306
+ document.getElementById('sendStatus').textContent='Cancelled';
2307
+ }
2308
+ async function stopAgentTurn(){
2309
+ if(!turnRunning||turnCancelling)return;
2310
+ setTurnCancelling(true);
2311
+ document.getElementById('sendStatus').textContent='Cancelling...';
2312
+ addActivity('thinking','Cancellation requested', {key:'turn-cancel-request', kind:'status'});
2313
+ try{
2314
+ const result=await api('/api/agent/cancel',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({reason:'Cancelled by user'})});
2315
+ if(result.status==='idle'){
2316
+ setTurnRunning(false);
2317
+ setTurnCancelling(false);
2318
+ document.getElementById('sendStatus').textContent='No running task';
2319
+ }else{
2320
+ addActivity('thinking','Cancellation sent', {key:'turn-cancel-request', kind:'status'});
2321
+ }
2322
+ }catch(err){
2323
+ setTurnCancelling(false);
2324
+ document.getElementById('sendStatus').textContent=err.message;
2325
+ addActivity('error','Cancellation failed',{key:'turn-cancel-request',kind:'status',detail:err.message});
2326
+ }
2327
+ }
2328
+ function setApprovalAutoUi(enabled){
2329
+ approvalAutoMode=Boolean(enabled);
2330
+ const input=document.getElementById('approvalAuto');
2331
+ const label=document.getElementById('approvalAutoLabel');
2332
+ if(input)input.checked=approvalAutoMode;
2333
+ if(label)label.textContent=approvalAutoMode?'Auto on':'Auto off';
2334
+ }
2335
+ async function loadApprovalMode(){
2336
+ try{
2337
+ const result=await api('/api/approvals/mode');
2338
+ setApprovalAutoUi(Boolean(result.auto));
2339
+ }catch(err){
2340
+ addTrace('approval_mode_error',{message:err.message});
2341
+ }
2342
+ }
2343
+ async function setApprovalAutoMode(enabled){
2344
+ setApprovalAutoUi(enabled);
2345
+ try{
2346
+ const result=await api('/api/approvals/mode',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({auto:enabled})});
2347
+ setApprovalAutoUi(Boolean(result.auto));
2348
+ addTrace('approval_mode',{mode:result.mode});
2349
+ }catch(err){
2350
+ setApprovalAutoUi(!enabled);
2351
+ addTrace('approval_mode_error',{message:err.message});
2352
+ }
2353
+ }
2354
+ async function handleLocalSlashCommand(rawPrompt){
2355
+ const text=String(rawPrompt||'').trim();
2356
+ if(!text.startsWith('/'))return false;
2357
+ const parts=text.split(/\\s+/).filter(Boolean);
2358
+ const command=String(parts[0]||'').toLowerCase();
2359
+ if(command!=='/auto')return false;
2360
+ const mode=String(parts[1]||'on').toLowerCase();
2361
+ if(['on','enable','enabled','true','1'].includes(mode)){
2362
+ await setApprovalAutoMode(true);
2363
+ document.getElementById('sendStatus').textContent='Auto mode on';
2364
+ addTrace('command',{command:'/auto on',mode:'auto'});
2365
+ return true;
2366
+ }
2367
+ if(['off','disable','disabled','false','0'].includes(mode)){
2368
+ await setApprovalAutoMode(false);
2369
+ document.getElementById('sendStatus').textContent='Auto mode off';
2370
+ addTrace('command',{command:'/auto off',mode:'ask'});
2371
+ return true;
2372
+ }
2373
+ if(mode==='status'){
2374
+ const modeLabel=approvalAutoMode?'auto':'ask';
2375
+ document.getElementById('sendStatus').textContent='Approval mode: '+modeLabel;
2376
+ addTrace('command',{command:'/auto status',mode:modeLabel});
2377
+ return true;
2378
+ }
2379
+ if(['full','dangerous','yolo'].includes(mode)){
2380
+ document.getElementById('sendStatus').textContent='/auto full is available only in the terminal CLI';
2381
+ addTrace('command_ignored',{command:text,reason:'full auto is not exposed in the browser workspace'});
2382
+ return true;
2383
+ }
2384
+ document.getElementById('sendStatus').textContent='Usage: /auto [on|off|status]';
2385
+ addTrace('command_error',{command:text,message:'Usage: /auto [on|off|status]'});
2386
+ return true;
2387
+ }
2388
+ function followupMessage(data){
2389
+ const status=String(data?.status||'');
2390
+ if(status==='accepted')return 'Follow-up sent to running agent';
2391
+ if(status==='delivered')return 'Follow-up delivered to agent';
2392
+ if(status==='queued')return 'Follow-up queued';
2393
+ if(status==='waiting_for_task')return 'Follow-up waiting for agent task';
2394
+ if(status==='duplicate')return 'Follow-up already sent';
2395
+ if(status==='queued_next_turn')return 'Follow-up queued for next turn';
2396
+ if(status==='error')return 'Follow-up failed';
2397
+ return 'Follow-up status: '+(status||'unknown');
2398
+ }
2399
+ function shouldQueueFollowup(result){
2400
+ const status=String(result?.status||'');
2401
+ return status==='queued_next_turn'||status==='no_task';
2402
+ }
2403
+ function queueFollowupNextTurn(prompt,attachments=[],path=currentPath,reason='',id=''){
2404
+ prompt=String(prompt||'').trim();
2405
+ if(!prompt)return;
2406
+ const key=id||prompt;
2407
+ if(queuedFollowupIds.has(key))return;
2408
+ queuedFollowupIds.add(key);
2409
+ queuedFollowups.push({prompt,attachments,path,id:key});
2410
+ document.getElementById('sendStatus').textContent=reason||'Queued for next turn';
2411
+ }
2412
+ async function runAgentTurn(prompt,attachments=[],path=currentPath,{displayed=false}={}){
2413
+ if(!displayed)addUserMessage(prompt,attachments);
2414
+ activeTurn=null;
2415
+ setTurnRunning(true);
2416
+ document.getElementById('sendStatus').textContent='Sending...';
2417
+ try{
2418
+ await api('/api/agent/turn',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,path,attachments})});
2419
+ }catch(err){
2420
+ document.getElementById('sendStatus').textContent=err.message;
2421
+ document.getElementById('bridgeState').textContent=err.status===401?'auth required':'error';
2422
+ setTurnRunning(false);
2423
+ addActivity('error',(err.status?err.status+' ':'')+err.message);
2424
+ pendingAttachments=attachments.concat(pendingAttachments);
2425
+ renderUploadTray();
2426
+ }
2427
+ }
2428
+ function runNextQueuedFollowup(){
2429
+ if(turnRunning||!queuedFollowups.length)return;
2430
+ const next=queuedFollowups.shift();
2431
+ queuedFollowupIds.delete(next.id);
2432
+ document.getElementById('sendStatus').textContent='Running queued follow-up...';
2433
+ runAgentTurn(next.prompt,next.attachments,next.path,{displayed:true});
2434
+ }
2435
+ function handleRelayEvent(type, event){
2436
+ const data = event.data || {};
2437
+ addTrace(type, JSON.stringify(data));
2438
+ if (type === 'agent_content') {
2439
+ appendAssistantText(data.text || '');
2440
+ return;
2441
+ }
2442
+ if (type === 'agent_relay_ready') {
2443
+ document.getElementById('bridgeState').textContent = 'ready';
2444
+ if(data.approval_mode)setApprovalAutoUi(data.approval_mode==='auto');
2445
+ return;
2446
+ }
2447
+ if (type === 'agent_approval_mode') {
2448
+ setApprovalAutoUi(Boolean(data.auto));
2449
+ return;
2450
+ }
2451
+ if (type === 'agent_history_loaded') {
2452
+ document.getElementById('bridgeState').textContent = 'resumed';
2453
+ if(data.backend_session_id) setSessionButtonLabel('Chat: '+compactId(data.backend_session_id));
2454
+ return;
2455
+ }
2456
+ if (type === 'agent_history_new') {
2457
+ document.getElementById('bridgeState').textContent = 'new session';
2458
+ setSessionButtonLabel('Chat: New');
2459
+ return;
2460
+ }
2461
+ if (type === 'agent_approval_required') {
2462
+ document.getElementById('bridgeState').textContent = 'approval required';
2463
+ showApprovalModal(data);
2464
+ return;
2465
+ }
2466
+ if (type === 'agent_approval_event') {
2467
+ if(data.state==='required'){
2468
+ addActivity('approval', data.tool || 'Tool approval', {key:data.call_id||data.approval_id||data.tool, kind:'tool', detail:data.args?JSON.stringify(data.args,null,2):''});
2469
+ } else if(data.state==='granted'||data.state==='denied') {
2470
+ addActivity(data.state==='granted'?'running':'error', (data.tool||'tool')+' '+data.state, {key:data.call_id||data.approval_id||data.tool, kind:'tool'});
2471
+ }
2472
+ return;
2473
+ }
2474
+ if (type === 'agent_approval_resolved') {
2475
+ if(pendingApproval?.approval_id===data.approval_id){
2476
+ resolveInlineApproval(data.approved?'approve':'reject', data.reason || '', {...pendingApproval,...data});
2477
+ pendingApproval=null;
2478
+ setApprovalModalOpen(false);
2479
+ }
2480
+ document.getElementById('bridgeState').textContent = data.approved ? 'running' : 'approval denied';
2481
+ return;
2482
+ }
2483
+ if (type === 'agent_session') {
2484
+ if(data.session_id) setSessionButtonLabel('Chat: '+compactId(data.session_id));
2485
+ return;
2486
+ }
2487
+ if (type === 'agent_turn_started') {
2488
+ activeTurn=null;
2489
+ ensureAssistantTurn();
2490
+ setTurnRunning(true);
2491
+ document.getElementById('bridgeState').textContent = 'running';
2492
+ return;
2493
+ }
2494
+ if (type === 'agent_cancel_requested') {
2495
+ setTurnCancelling(true);
2496
+ document.getElementById('bridgeState').textContent = 'cancelling';
2497
+ document.getElementById('sendStatus').textContent = 'Cancelling...';
2498
+ addActivity('thinking','Cancellation requested', {key:'turn-cancel-request', kind:'status'});
2499
+ return;
2500
+ }
2501
+ if (type === 'agent_turn_cancelled' || type === 'cancelled') {
2502
+ finishCancelledTurn(data);
2503
+ return;
2504
+ }
2505
+ if (type === 'agent_turn_complete') {
2506
+ if(activeTurn){activeTurn.active=false;renderActivity(activeTurn);activeTurn=null;}
2507
+ setTurnRunning(false);
2508
+ document.getElementById('bridgeState').textContent = 'ready';
2509
+ document.getElementById('sendStatus').textContent = 'Done';
2510
+ runNextQueuedFollowup();
2511
+ return;
2512
+ }
2513
+ if (type === 'agent_error') {
2514
+ setTurnRunning(false);
2515
+ document.getElementById('bridgeState').textContent = 'error';
2516
+ document.getElementById('sendStatus').textContent = data.message || data.error || 'Agent error';
2517
+ addActivity('error', data.message || data.error || 'Agent error', {detail: data.code || ''});
2518
+ return;
2519
+ }
2520
+ if (type === 'agent_followup_status') {
2521
+ addActivity(data.ok===false?'error':'thinking', followupMessage(data), {key:data.intervention_id||'followup', kind:'followup', detail:data.error||data.instruction||''});
2522
+ if(shouldQueueFollowup(data))queueFollowupNextTurn(data.instruction||'',[],currentPath,followupMessage(data),data.intervention_id||'');
2523
+ document.getElementById('sendStatus').textContent=followupMessage(data);
2524
+ return;
2525
+ }
2526
+ if (type === 'agent_followup_event') {
2527
+ const inner=data.data||{};
2528
+ addActivity('done', followupMessage({status:String(data.type||'').replace('user_intervention_',''), intervention_id:inner.intervention_id}), {key:inner.intervention_id||data.type, kind:'followup', detail:inner.delivered_at_tool||''});
2529
+ return;
2530
+ }
2531
+ if (type === 'agent_tool_call') {
2532
+ const detail=data.args?JSON.stringify(data.args,null,2):'';
2533
+ addActivity('running', (data.tool || 'tool'), {key:data.call_id||data.tool, kind:'tool', detail});
2534
+ return;
2535
+ }
2536
+ if (type === 'agent_tool_result') {
2537
+ addActivity(data.success===false?'error':'done', (data.tool || 'tool') + (data.success===false?' failed':' completed'), {key:data.call_id||data.tool, kind:'tool', detail:data.output||''});
2538
+ return;
2539
+ }
2540
+ if (type === 'agent_status') {
2541
+ addActivity('thinking', data.message || data.type || 'Working');
2542
+ return;
2543
+ }
2544
+ if (type === 'agent_reasoning') {
2545
+ addActivity('thinking', data.text || 'Thinking');
2546
+ return;
2547
+ }
2548
+ }
2549
+ const es = new EventSource('/api/events?token=' + encodeURIComponent(token));
2550
+ es.onmessage = evt => { try { const e = JSON.parse(evt.data); addTrace(e.type, JSON.stringify(e.data || {})); } catch {} };
2551
+ [
2552
+ 'session_started','file_browsed','file_read','file_uploaded','file_saved','tool_execution_requested','session_stopped',
2553
+ 'agent_turn_requested','agent_relay_ready','agent_turn_started','agent_cancel_requested','agent_turn_cancelled','agent_turn_complete','cancelled',
2554
+ 'agent_history_loaded','agent_history_new','agent_approval_mode','agent_approval_event','agent_approval_required','agent_approval_resolved','agent_followup_requested','agent_followup_status','agent_followup_event','agent_session','agent_status','agent_reasoning','agent_content','agent_tool_call',
2555
+ 'agent_tool_result','agent_activity','agent_complete','agent_error','agent_event'
2556
+ ].forEach(type => {
2557
+ es.addEventListener(type, evt => { try { handleRelayEvent(type, JSON.parse(evt.data)); } catch {} });
2558
+ });
2559
+ document.getElementById('composer').addEventListener('submit', async (e) => {
2560
+ e.preventDefault();
2561
+ const rawPrompt = document.getElementById('prompt').value.trim();
2562
+ if(await handleLocalSlashCommand(rawPrompt)){
2563
+ document.getElementById('prompt').value='';
2564
+ return;
2565
+ }
2566
+ const attachments = pendingAttachments.slice();
2567
+ const prompt = rawPrompt || (attachments.length ? 'Review the attached files.' : '');
2568
+ if (!prompt && !attachments.length) return;
2569
+ if(turnCancelling){document.getElementById('sendStatus').textContent='Cancelling...';return;}
2570
+ setSessionMenuOpen(false);
2571
+ if(!historySelectionReady) historySelectionReady=true;
2572
+ addUserMessage(prompt, attachments);
2573
+ document.getElementById('prompt').value='';
2574
+ pendingAttachments=[];
2575
+ renderUploadTray();
2576
+ const followup=turnRunning;
2577
+ if(followup){
2578
+ const attachmentLines=attachments.length?('\\n\\nAttached files for this follow-up:\\n'+attachments.map(file=>'- '+(file.name||basename(file.path))+' path='+file.path).join('\\n')):'';
2579
+ document.getElementById('sendStatus').textContent = 'Sending follow-up...';
2580
+ try {
2581
+ const result=await api('/api/agent/followup', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ prompt: prompt+attachmentLines }) });
2582
+ document.getElementById('sendStatus').textContent = followupMessage(result);
2583
+ if(shouldQueueFollowup(result))queueFollowupNextTurn(prompt+attachmentLines,attachments,currentPath,followupMessage(result),result.intervention_id||'');
2584
+ if(result.ok===false)addActivity('error', followupMessage(result), {key:result.intervention_id||'followup', kind:'followup', detail:result.error||''});
2585
+ } catch (err) {
2586
+ document.getElementById('sendStatus').textContent = err.message;
2587
+ addActivity('error', (err.status ? err.status + ' ' : '') + err.message);
2588
+ pendingAttachments=attachments.concat(pendingAttachments);
2589
+ renderUploadTray();
2590
+ }
2591
+ return;
2592
+ }
2593
+ activeTurn=null;
2594
+ setTurnRunning(true);
2595
+ document.getElementById('sendStatus').textContent = 'Sending...';
2596
+ try {
2597
+ await api('/api/agent/turn', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ prompt, path: currentPath, attachments }) });
2598
+ } catch (err) {
2599
+ document.getElementById('sendStatus').textContent = err.message;
2600
+ document.getElementById('bridgeState').textContent = err.status === 401 ? 'auth required' : 'error';
2601
+ setTurnRunning(false);
2602
+ addActivity('error', (err.status ? err.status + ' ' : '') + err.message);
2603
+ pendingAttachments=attachments.concat(pendingAttachments);
2604
+ renderUploadTray();
2605
+ }
2606
+ });
2607
+ document.getElementById('prompt').addEventListener('keydown', e=>{if(e.key==='Enter'&&(e.metaKey||e.ctrlKey)){e.preventDefault();document.getElementById('composer').requestSubmit();}});
2608
+ document.getElementById('attachFiles').addEventListener('click',()=>document.getElementById('fileUpload').click());
2609
+ document.getElementById('stopAgent').addEventListener('click',stopAgentTurn);
2610
+ document.getElementById('fileUpload').addEventListener('change',async(e)=>{
2611
+ try{await uploadFiles(e.target.files||[]);}catch(err){document.getElementById('sendStatus').textContent=err.message;addActivity('error','Upload failed',{detail:err.message});}
2612
+ finally{e.target.value='';}
2613
+ });
2614
+ document.querySelectorAll('[data-agent-tab]').forEach(btn=>btn.addEventListener('click',()=>setAgentTab(btn.dataset.agentTab)));
2615
+ document.getElementById('sessionMenuButton').addEventListener('click',(e)=>{e.stopPropagation();setSessionMenuOpen(!sessionMenuOpen);});
2616
+ document.getElementById('sessionModalClose').addEventListener('click',()=>setSessionMenuOpen(false));
2617
+ document.getElementById('sessionModal').addEventListener('click',(e)=>{if(e.target.id==='sessionModal')setSessionMenuOpen(false);});
2618
+ document.getElementById('approvalDenyTop').addEventListener('click',()=>submitApproval('reject'));
2619
+ document.getElementById('approvalAuto').addEventListener('change',e=>setApprovalAutoMode(e.target.checked));
2620
+ document.addEventListener('keydown',(e)=>{if(e.key==='Escape'&&pendingApproval){submitApproval('reject');return;}if(e.key==='Escape'&&sessionMenuOpen){setSessionMenuOpen(false);return;}if(e.key==='Escape'&&previewMaximized)setPreviewMaximized(false);});
2621
+ document.getElementById('refreshFiles').addEventListener('click',()=>{dirCache.clear();loadDir('.');});
2622
+ document.getElementById('toggleExplorer').addEventListener('click',()=>{let prefs={explorerVisible:true};try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}saveLayoutPatch({explorerVisible:!prefs.explorerVisible});});
2623
+ document.getElementById('toggleAgent').addEventListener('click',()=>{let prefs={agentVisible:true};try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}saveLayoutPatch({agentVisible:!prefs.agentVisible});});
2624
+ applyLayout();
2625
+ setupResizers();
2626
+ renderTabs();
2627
+ renderEmptyViewer();
2628
+ loadApprovalMode();
2629
+ loadSessionChoices();
2630
+ loadDir('.').then(()=>{if(currentPath&&currentPath!=='.')openFile(currentPath).catch(()=>{});});
2631
+ </script>
2632
+ </body>
2633
+ </html>`;
2634
+ }
2635
+
2636
+ function escapeHtml(value) {
2637
+ return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
2638
+ '&': '&amp;',
2639
+ '<': '&lt;',
2640
+ '>': '&gt;',
2641
+ '"': '&quot;',
2642
+ "'": '&#39;',
2643
+ }[ch]));
2644
+ }