@bahulam/code 0.1.12 → 0.1.14

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.
@@ -19,8 +19,8 @@ import {
19
19
  DEFAULT_MAX_RAW_BYTES,
20
20
  contentTypeForPath,
21
21
  listWorkspacePath,
22
- readWorkspaceFile,
23
22
  resolveWorkspacePath,
23
+ searchWorkspaceFiles,
24
24
  } from './file-access.mjs';
25
25
  import {
26
26
  createOfficePdfPreview,
@@ -45,6 +45,7 @@ const DEFAULT_MAX_UPLOAD_FILES = 20;
45
45
  const DEFAULT_MAX_UPLOAD_FILE_BYTES = DEFAULT_MAX_RAW_BYTES;
46
46
  const DEFAULT_MAX_UPLOAD_BODY_BYTES = 140 * 1024 * 1024;
47
47
  const DEFAULT_MAX_SAVE_BYTES = 2 * 1024 * 1024;
48
+ const DEFAULT_MAX_MEDIA_BYTES = 1024 * 1024 * 1024;
48
49
 
49
50
  export async function startLocalWorkspaceService({
50
51
  session,
@@ -364,6 +365,19 @@ async function routeRequest({ req, res, sessionId, token, events, sseClients, em
364
365
  return;
365
366
  }
366
367
 
368
+ if (req.method === 'GET' && url.pathname === '/api/workspace/search') {
369
+ const query = url.searchParams.get('q') || '';
370
+ const kinds = (url.searchParams.get('kind') || url.searchParams.get('kinds') || '')
371
+ .split(',')
372
+ .map((item) => item.trim())
373
+ .filter(Boolean);
374
+ const maxResults = envInt('BAHULAM_LOCAL_SEARCH_MAX_RESULTS', 200);
375
+ const result = searchWorkspaceFiles(session, { query, kinds, maxResults });
376
+ emit('workspace_searched', { query: result.query, kinds: result.kinds, count: result.results.length });
377
+ sendJson(res, 200, result);
378
+ return;
379
+ }
380
+
367
381
  if (req.method === 'POST' && url.pathname === '/api/files/upload') {
368
382
  const body = await readJsonBody(req, envInt('BAHULAM_LOCAL_UPLOAD_MAX_BODY_BYTES', DEFAULT_MAX_UPLOAD_BODY_BYTES));
369
383
  const result = saveUploadedFiles(session, body);
@@ -402,15 +416,8 @@ async function routeRequest({ req, res, sessionId, token, events, sseClients, em
402
416
  err.code = 'BAD_REQUEST';
403
417
  throw err;
404
418
  }
405
- const bytes = readWorkspaceFile(session, requestedPath, { maxBytes: DEFAULT_MAX_RAW_BYTES });
406
- emit('file_read', { path: requestedPath, bytes: bytes.length });
407
- res.writeHead(200, {
408
- 'Content-Type': contentTypeForPath(requestedPath),
409
- 'Cache-Control': 'no-store',
410
- 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:",
411
- 'X-Content-Type-Options': 'nosniff',
412
- });
413
- res.end(bytes);
419
+ sendWorkspaceRawFile({ req, res, session, requestedPath });
420
+ emit('file_read', { path: requestedPath, streamed: true });
414
421
  return;
415
422
  }
416
423
 
@@ -691,7 +698,9 @@ const PLUGIN_VIEW_MIME = {
691
698
  js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
692
699
  json: 'application/json', map: 'application/json',
693
700
  png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
694
- webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon',
701
+ webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon', avif: 'image/avif',
702
+ mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', m4v: 'video/x-m4v', ogv: 'video/ogg',
703
+ mp3: 'audio/mpeg', m4a: 'audio/mp4', wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac', aac: 'audio/aac',
695
704
  woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
696
705
  wasm: 'application/wasm',
697
706
  };
@@ -801,8 +810,7 @@ function saveUploadedFiles(session, body = {}) {
801
810
  }
802
811
 
803
812
  const root = fs.realpathSync(session.root_path);
804
- const group = `${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}-${crypto.randomBytes(3).toString('hex')}`;
805
- const directory = path.posix.join(LOCAL_UPLOAD_DIR, safePathSegment(session.id), group);
813
+ const directory = LOCAL_UPLOAD_DIR;
806
814
  const uploadDir = path.join(root, ...directory.split('/'));
807
815
  fs.mkdirSync(uploadDir, { recursive: true });
808
816
 
@@ -880,6 +888,72 @@ function uniqueUploadTarget(uploadDir, fileName) {
880
888
  return path.join(uploadDir, `${parsed.name}-${crypto.randomBytes(4).toString('hex')}${parsed.ext}`);
881
889
  }
882
890
 
891
+ function sendWorkspaceRawFile({ req, res, session, requestedPath }) {
892
+ const target = resolveWorkspacePath(session, requestedPath);
893
+ const stat = fs.statSync(target);
894
+ if (!stat.isFile()) {
895
+ const err = new Error('Requested path is not a file');
896
+ err.code = 'NOT_FILE';
897
+ throw err;
898
+ }
899
+
900
+ const contentType = contentTypeForPath(target);
901
+ const mediaLike = /^(audio|video)\//.test(contentType);
902
+ const maxBytes = mediaLike
903
+ ? envInt('BAHULAM_LOCAL_MEDIA_MAX_BYTES', DEFAULT_MAX_MEDIA_BYTES)
904
+ : DEFAULT_MAX_RAW_BYTES;
905
+ if (stat.size > maxBytes) {
906
+ const err = new Error(`File is larger than ${maxBytes} bytes`);
907
+ err.code = 'FILE_TOO_LARGE';
908
+ throw err;
909
+ }
910
+
911
+ const commonHeaders = {
912
+ 'Content-Type': contentType,
913
+ 'Cache-Control': 'no-store',
914
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; media-src 'self' blob:",
915
+ 'X-Content-Type-Options': 'nosniff',
916
+ };
917
+ const range = parseRangeHeader(req.headers.range, stat.size);
918
+ if (range) {
919
+ res.writeHead(206, {
920
+ ...commonHeaders,
921
+ 'Accept-Ranges': 'bytes',
922
+ 'Content-Range': `bytes ${range.start}-${range.end}/${stat.size}`,
923
+ 'Content-Length': range.end - range.start + 1,
924
+ });
925
+ fs.createReadStream(target, { start: range.start, end: range.end }).pipe(res);
926
+ return;
927
+ }
928
+
929
+ res.writeHead(200, {
930
+ ...commonHeaders,
931
+ 'Accept-Ranges': mediaLike ? 'bytes' : 'none',
932
+ 'Content-Length': stat.size,
933
+ });
934
+ fs.createReadStream(target).pipe(res);
935
+ }
936
+
937
+ function parseRangeHeader(header, size) {
938
+ const value = String(header || '').trim();
939
+ if (!value || !value.startsWith('bytes=')) return null;
940
+ const first = value.slice(6).split(',')[0]?.trim() || '';
941
+ const match = first.match(/^(\d*)-(\d*)$/);
942
+ if (!match) return null;
943
+ let start = match[1] ? Number(match[1]) : null;
944
+ let end = match[2] ? Number(match[2]) : null;
945
+ if (start == null && end == null) return null;
946
+ if (start == null) {
947
+ const suffix = Math.max(0, Math.min(size, end || 0));
948
+ start = Math.max(0, size - suffix);
949
+ end = size - 1;
950
+ } else {
951
+ end = end == null ? size - 1 : Math.min(end, size - 1);
952
+ }
953
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || start >= size) return null;
954
+ return { start, end };
955
+ }
956
+
883
957
  function sendJson(res, status, body) {
884
958
  res.writeHead(status, {
885
959
  'Content-Type': 'application/json; charset=utf-8',
@@ -981,6 +1055,18 @@ function staticContentType(filePath) {
981
1055
  case '.jpeg': return 'image/jpeg';
982
1056
  case '.gif': return 'image/gif';
983
1057
  case '.webp': return 'image/webp';
1058
+ case '.avif': return 'image/avif';
1059
+ case '.mp4': return 'video/mp4';
1060
+ case '.webm': return 'video/webm';
1061
+ case '.mov': return 'video/quicktime';
1062
+ case '.m4v': return 'video/x-m4v';
1063
+ case '.ogv': return 'video/ogg';
1064
+ case '.mp3': return 'audio/mpeg';
1065
+ case '.m4a': return 'audio/mp4';
1066
+ case '.wav': return 'audio/wav';
1067
+ case '.ogg': return 'audio/ogg';
1068
+ case '.flac': return 'audio/flac';
1069
+ case '.aac': return 'audio/aac';
984
1070
  case '.ttf': return 'font/ttf';
985
1071
  case '.woff': return 'font/woff';
986
1072
  case '.woff2': return 'font/woff2';
@@ -1070,11 +1156,11 @@ button,textarea,input{font:inherit}button{color:inherit}.shell{display:flex;flex
1070
1156
  .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:var(--ws-radius-sm);padding:0 8px;color:rgba(27,27,27,.58);font-size:12px;font-weight:500;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:var(--ws-radius-md);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:600;color:rgba(27,27,27,.84)}.session-dialog-subtitle,.approval-dialog-subtitle{margin-top:1px;font:12px/1.45 var(--sans);color:rgba(27,27,27,.46);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:var(--ws-radius-md);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:var(--ws-radius-md);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:13px;font-weight:600;color:#92400E}.approval-inline-meta{font:11px var(--sans);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:var(--ws-radius-md);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)}
1071
1157
  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{grid-column:1;background:rgba(255,255,255,.52)}.left-resizer{grid-column:2}.work{grid-column:3;display:flex;flex-direction:column;background:#fff}.right-resizer{grid-column:4}.agent{grid-column:5;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)}
1072
1158
  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}
1073
- .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:11px;font-weight:600;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:var(--ws-radius-sm);color:rgba(27,27,27,.35);cursor:pointer;padding:0 4px}.section-button:hover{background:var(--ws-hover);color:rgba(27,27,27,.70)}
1074
- .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:var(--ws-radius-sm);padding:0 8px;color:rgba(27,27,27,.62);cursor:pointer;font-size:13px;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,.34);font:11px var(--sans);margin-left:auto}.loading-row{height:24px;padding-left:32px;color:rgba(27,27,27,.38);font:12px var(--sans)}
1159
+ .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:11px;font-weight:600;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:var(--ws-radius-sm);color:rgba(27,27,27,.35);cursor:pointer;padding:0 4px}.section-button:hover{background:var(--ws-hover);color:rgba(27,27,27,.70)}.workspace-tools{height:68px;border-bottom:1px solid var(--ws-border-subtle);padding:7px 8px;background:#FFFDF7;display:flex;flex-direction:column;gap:7px}.file-search{width:100%;height:26px;border:1px solid rgba(27,27,27,.10);border-radius:6px;background:#fff;color:var(--ws-foreground);outline:none;padding:0 8px;font-size:12px}.file-search:focus{border-color:rgba(8,145,178,.35);box-shadow:0 0 0 2px rgba(8,145,178,.10)}.file-filters{display:flex;gap:5px;overflow-x:auto;scrollbar-width:none}.file-filters::-webkit-scrollbar{display:none}.filter-chip{height:22px;border:1px solid rgba(27,27,27,.08);border-radius:999px;background:#fff;color:rgba(27,27,27,.52);padding:0 8px;font-size:11px;cursor:pointer;white-space:nowrap}.filter-chip:hover{background:#F7F5EF;color:rgba(27,27,27,.78)}.filter-chip.active{background:#ECFEFF;border-color:rgba(8,145,178,.22);color:#0E7490}.search-results{padding:4px 8px;color:rgba(27,27,27,.38);font:11px var(--sans)}
1160
+ .file-list{padding:6px;overflow:auto;height:calc(100% - 104px)}.row{display:flex;align-items:center;gap:5px;width:100%;height:27px;border:0;background:transparent;text-align:left;border-radius:var(--ws-radius-sm);padding:0 8px;color:rgba(27,27,27,.62);cursor:pointer;font-size:13px;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,.34);font:11px var(--sans);margin-left:auto}.loading-row{height:24px;padding-left:32px;color:rgba(27,27,27,.38);font:12px var(--sans)}
1075
1161
  .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,.48);font-size:13px;font-weight:500;cursor:pointer;max-width:220px;min-width:90px}.tab.active{background:#fff;color:var(--ws-foreground);font-weight:600;box-shadow:inset 0 -1.5px 0 var(--ws-primary)}.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:var(--ws-radius-sm);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}
1076
1162
  .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)}
1077
- .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}
1163
+ .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,.media-stage{flex:1;min-height:0;display:flex;flex-direction:column;background:#F8F7F2}.image-toolbar,.media-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,.media-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}.media-preview video{width:min(100%,1120px);max-height:100%;background:#0B0B0B;border-radius:8px;box-shadow:0 14px 42px rgba(27,27,27,.16)}.media-preview audio{width:min(100%,720px)}.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}
1078
1164
  .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:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:rgba(27,27,27,.42)}.chat-subtitle{min-width:0;flex:1;font-size:12px;color:rgba(27,27,27,.42);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-tabs{height:34px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:#F7F5EF;padding:0 8px;gap:3px}.agent-tab{height:26px;border:0;border-radius:var(--ws-radius-md);background:transparent;color:rgba(27,27,27,.48);font-size:13px;font-weight:500;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);font-weight:600;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}.plugins-menu-wrap{position:relative}.plugins-menu-wrap[hidden]{display:none}.plugins-dropdown{position:absolute;top:26px;left:0;z-index:60;min-width:210px;background:#fff;border:1px solid var(--ws-border);border-radius:var(--ws-radius-md);box-shadow:0 12px 34px rgba(27,27,27,.14);padding:4px;display:flex;flex-direction:column}.plugins-dropdown[hidden]{display:none}.plugins-dropdown button{border:0;background:transparent;text-align:left;font:12px var(--sans);color:rgba(27,27,27,.78);padding:6px 9px;border-radius:var(--ws-radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:10px}.plugins-dropdown button:hover{background:var(--ws-hover)}.plugins-dropdown .plugin-origin{color:var(--ws-faint);font:11px var(--sans)}.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,.42);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;width:100%;border:0;background:transparent;text-align:left;border-radius:var(--ws-radius-md);padding:8px;color:inherit;cursor:pointer}.session-row:hover{background:rgba(27,27,27,.035)}.session-row.selected{background:var(--ws-active);box-shadow:inset 2px 0 0 var(--ws-primary)}.session-row:focus-visible{outline:2px solid rgba(8,145,178,.35);outline-offset:1px}.session-main{min-width:0}.session-prompt{display:block;font-size:13px;font-weight:500;color:rgba(27,27,27,.84);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-meta{display:block;margin-top:3px;font:12px/1.4 var(--sans);color:rgba(27,27,27,.46);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:11px var(--sans);color:rgba(27,27,27,.52)}.session-action{height:24px;display:inline-flex;align-items:center;justify-content:center;border:1px solid rgba(27,27,27,.10);border-radius:var(--ws-radius-md);background:#fff;color:rgba(27,27,27,.62);padding:0 8px;font-size:12px;font-weight:500}.session-row:hover .session-action{background:#F7F5EF;color:var(--ws-foreground)}.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:11px var(--sans);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}
1079
1165
  .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)}
1080
1166
  .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}
@@ -1137,6 +1223,17 @@ main.hide-files{grid-template-columns:0 0 minmax(360px,1fr) 6px var(--right-w)}m
1137
1223
  <main id="layout">
1138
1224
  <section class="panel files" id="filesPanel">
1139
1225
  <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>
1226
+ <div class="workspace-tools">
1227
+ <input class="file-search" id="fileSearch" type="search" placeholder="Search workspace" autocomplete="off">
1228
+ <div class="file-filters" id="fileFilters" aria-label="File type filters">
1229
+ <button class="filter-chip active" type="button" data-file-filter="all">All</button>
1230
+ <button class="filter-chip" type="button" data-file-filter="code">Code</button>
1231
+ <button class="filter-chip" type="button" data-file-filter="docs">Docs</button>
1232
+ <button class="filter-chip" type="button" data-file-filter="data">Data</button>
1233
+ <button class="filter-chip" type="button" data-file-filter="media">Media</button>
1234
+ <button class="filter-chip" type="button" data-file-filter="web">Web</button>
1235
+ </div>
1236
+ </div>
1140
1237
  <div class="file-list" id="fileList"></div>
1141
1238
  </section>
1142
1239
  <div class="resizer left-resizer" data-resize="left"></div>
@@ -1217,6 +1314,10 @@ const headers = { 'X-Bahulam-Local-Token': token };
1217
1314
  const dirCache = new Map();
1218
1315
  const expandedDirs = new Set(['.']);
1219
1316
  const loadingDirs = new Set();
1317
+ let workspaceQuery = '';
1318
+ let workspaceFilter = 'all';
1319
+ let workspaceSearchTimer = null;
1320
+ let workspaceSearchSeq = 0;
1220
1321
  const openTabs = [];
1221
1322
  const fileCache = new Map();
1222
1323
  let activePath = null;
@@ -1282,7 +1383,10 @@ function attachmentHint(file){
1282
1383
  const mime=String(file?.mime_type||'').toLowerCase();
1283
1384
  const ext=extname(file?.path||file?.name||'');
1284
1385
  if(kind==='image'||mime.startsWith('image/'))return 'vision';
1386
+ if(kind==='video'||mime.startsWith('video/'))return 'video';
1387
+ if(kind==='audio'||mime.startsWith('audio/'))return 'audio';
1285
1388
  if(kind==='spreadsheet'||kind==='table'||['csv','tsv','xlsx','xls','ods'].includes(ext))return 'table';
1389
+ if(kind==='web')return 'web';
1286
1390
  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';
1287
1391
  return 'file';
1288
1392
  }
@@ -1368,8 +1472,11 @@ function fileKind(data){
1368
1472
  const file=data.file||{};
1369
1473
  if(file.viewer)return file.viewer;
1370
1474
  const ext=extname(data.path);
1371
- if(['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext))return 'image';
1475
+ if(['png','jpg','jpeg','gif','webp','svg','bmp','ico','avif'].includes(ext))return 'image';
1476
+ if(['mp4','webm','mov','m4v','ogv'].includes(ext))return 'video';
1477
+ if(['mp3','m4a','wav','ogg','flac','aac'].includes(ext))return 'audio';
1372
1478
  if(ext==='pdf')return 'pdf';
1479
+ if(['html','htm'].includes(ext))return 'web';
1373
1480
  if(['md','mdx'].includes(ext))return 'markdown';
1374
1481
  if(['mmd','mermaid'].includes(ext))return 'mermaid';
1375
1482
  if(['drawio','dio'].includes(ext))return 'drawio';
@@ -1502,7 +1609,7 @@ function bindHeaderActions(path){
1502
1609
  }
1503
1610
  function askAgentForFile(path,kind){
1504
1611
  const prompt=document.getElementById('prompt');
1505
- 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';
1612
+ const label=kind==='image'?'Analyze this image':kind==='video'?'Inspect this video':kind==='audio'?'Inspect this audio':kind==='web'?'Review this web page':kind==='pdf'?'Summarize this PDF':kind==='spreadsheet'?'Inspect this spreadsheet':kind==='document'?'Inspect this document':kind==='presentation'?'Inspect this presentation':'Inspect this file';
1506
1613
  prompt.value=label+' at '+path+'. Explain what it contains and call the right local tools if needed.';
1507
1614
  prompt.focus();
1508
1615
  }
@@ -1695,11 +1802,75 @@ function renderTabs(){
1695
1802
  function renderExplorer(){
1696
1803
  const root=dirCache.get('.');
1697
1804
  const list=document.getElementById('fileList');
1805
+ if(workspaceSearchActive()){scheduleWorkspaceSearch(0);return;}
1698
1806
  if(!root){list.innerHTML='<div class="loading-row">Loading...</div>';return;}
1699
1807
  list.innerHTML=renderEntries(root.entries||[],0);
1700
1808
  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();}));
1701
1809
  list.querySelectorAll('[data-file]').forEach(btn=>btn.addEventListener('click',()=>openFile(btn.dataset.file)));
1702
1810
  }
1811
+ function workspaceSearchActive(){
1812
+ return Boolean(workspaceQuery.trim() || workspaceFilter !== 'all');
1813
+ }
1814
+ function filterKinds(filter){
1815
+ switch(filter){
1816
+ case 'code': return ['code','config'];
1817
+ case 'docs': return ['markdown','pdf','document','presentation','notebook'];
1818
+ case 'data': return ['table','spreadsheet'];
1819
+ case 'media': return ['image','video','audio'];
1820
+ case 'web': return ['web','mermaid','drawio'];
1821
+ default: return [];
1822
+ }
1823
+ }
1824
+ function setWorkspaceFilter(filter){
1825
+ workspaceFilter=filter||'all';
1826
+ document.querySelectorAll('[data-file-filter]').forEach(btn=>btn.classList.toggle('active',btn.dataset.fileFilter===workspaceFilter));
1827
+ renderExplorer();
1828
+ }
1829
+ function scheduleWorkspaceSearch(delay=160){
1830
+ if(workspaceSearchTimer)clearTimeout(workspaceSearchTimer);
1831
+ workspaceSearchTimer=setTimeout(loadWorkspaceSearch,delay);
1832
+ }
1833
+ async function loadWorkspaceSearch(){
1834
+ if(!workspaceSearchActive())return;
1835
+ const list=document.getElementById('fileList');
1836
+ const seq=++workspaceSearchSeq;
1837
+ const kinds=filterKinds(workspaceFilter);
1838
+ list.innerHTML='<div class="loading-row">Searching...</div>';
1839
+ try{
1840
+ const url='/api/workspace/search?q='+encodeURIComponent(workspaceQuery.trim())+(kinds.length?'&kind='+encodeURIComponent(kinds.join(',')):'');
1841
+ const result=await api(url);
1842
+ if(seq!==workspaceSearchSeq)return;
1843
+ document.getElementById('fileCount').textContent=String((result.results||[]).length);
1844
+ renderSearchResults(result);
1845
+ }catch(err){
1846
+ if(seq!==workspaceSearchSeq)return;
1847
+ list.innerHTML='<div class="search-results error">'+esc(err.message||'Search failed')+'</div>';
1848
+ }
1849
+ }
1850
+ function renderSearchResults(result){
1851
+ const list=document.getElementById('fileList');
1852
+ const rows=Array.isArray(result.results)?result.results:[];
1853
+ const note='<div class="search-results">'+esc(rows.length)+' result'+(rows.length===1?'':'s')+(result.truncated?' · truncated':'')+'</div>';
1854
+ if(!rows.length){list.innerHTML=note+'<div class="empty">No matching files.</div>';return;}
1855
+ list.innerHTML=note+rows.map(entry=>{
1856
+ const attrs=entry.type==='directory'?'data-dir="'+esc(entry.path)+'"':'data-file="'+esc(entry.path)+'"';
1857
+ return '<button class="row '+(entry.path===activePath?'selected':'')+'" '+attrs+' title="'+esc(entry.path)+'"><span class="chev"></span><span class="icon">'+entryIcon(entry)+'</span><span class="name">'+esc(entry.path)+'</span><span class="sub">'+esc(entry.label||entry.kind||'')+'</span></button>';
1858
+ }).join('');
1859
+ list.querySelectorAll('[data-dir]').forEach(btn=>btn.addEventListener('click',async()=>{const dir=btn.dataset.dir;expandedDirs.add(dir);workspaceQuery='';document.getElementById('fileSearch').value='';setWorkspaceFilter('all');if(!dirCache.has(dir))await loadDir(dir);else renderExplorer();}));
1860
+ list.querySelectorAll('[data-file]').forEach(btn=>btn.addEventListener('click',()=>openFile(btn.dataset.file)));
1861
+ }
1862
+ function entryIcon(entry){
1863
+ const kind=String(entry.kind||entry.viewer||'').toLowerCase();
1864
+ if(entry.type==='directory'||kind==='directory')return '▦';
1865
+ if(kind==='image')return '◉';
1866
+ if(kind==='video')return '▶';
1867
+ if(kind==='audio')return '♪';
1868
+ if(kind==='web')return '⌘';
1869
+ if(['spreadsheet','table'].includes(kind))return '▤';
1870
+ if(['markdown','pdf','document','presentation','notebook'].includes(kind))return '▣';
1871
+ if(['code','config','mermaid','drawio'].includes(kind))return '◇';
1872
+ return '·';
1873
+ }
1703
1874
  function renderEntries(entries,depth){
1704
1875
  return entries.map(entry=>{
1705
1876
  const pad=8+depth*13;
@@ -1709,7 +1880,7 @@ function renderEntries(entries,depth){
1709
1880
  const rows=open?(child?renderEntries(child.entries||[],depth+1):'<div class="loading-row" style="padding-left:'+(pad+24)+'px">Loading...</div>'):'';
1710
1881
  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;
1711
1882
  }
1712
- 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>';
1883
+ 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">'+entryIcon(entry)+'</span><span class="name">'+esc(entry.name)+'</span><span class="sub">'+(entry.size!=null?formatBytes(entry.size):'')+'</span></button>';
1713
1884
  }).join('');
1714
1885
  }
1715
1886
  function renderEmptyViewer(){
@@ -1720,6 +1891,8 @@ function renderFile(data){
1720
1891
  const text=data.preview&&data.preview.content!=null?sourceTextForRender(data):null;
1721
1892
  const kind=fileKind(data);
1722
1893
  if(kind==='image'){renderImage(data);return;}
1894
+ if(kind==='video'||kind==='audio'){renderMedia(data,kind);return;}
1895
+ if(kind==='web'&&text!=null){renderWebFile(data,text);return;}
1723
1896
  if(kind==='pdf'){renderPdf(data);return;}
1724
1897
  if(kind==='markdown'&&text!=null){renderMarkdownFile(data,text);return;}
1725
1898
  if(kind==='mermaid'&&text!=null){renderMermaidFile(data,text);return;}
@@ -1929,11 +2102,25 @@ function renderImage(data){
1929
2102
  if(scale)scale.textContent=Math.round(activeImageScale*100)+'%';
1930
2103
  }));
1931
2104
  }
2105
+ function renderMedia(data,kind){
2106
+ const header=fileHeader(data,{extraActions:'<span class="file-chip">'+esc(kind)+'</span>'});
2107
+ const source=rawUrl(data.path);
2108
+ const body=kind==='video'
2109
+ ? '<video controls playsinline preload="metadata" src="'+source+'"></video>'
2110
+ : '<audio controls preload="metadata" src="'+source+'"></audio>';
2111
+ renderViewer(header+'<div class="media-stage" data-preview-content><div class="media-toolbar"><span class="file-chip">'+esc(formatBytes(data.file?.size))+'</span></div><div class="media-preview">'+body+'</div></div>');
2112
+ bindHeaderActions(data.path);
2113
+ }
1932
2114
  function renderPdf(data){
1933
2115
  const header=fileHeader(data);
1934
2116
  renderViewer(header+'<div class="frame-preview" data-preview-content><iframe src="'+rawUrl(data.path)+'" title="'+esc(data.path)+'"></iframe></div>');
1935
2117
  bindHeaderActions(data.path);
1936
2118
  }
2119
+ function renderWebFile(data,text){
2120
+ const header=fileHeader(data,{language:'html',truncated:Boolean(data.preview&&data.preview.truncated),save:canEditFile(data),editSource:canEditFile(data),extraActions:'<span class="file-chip">web preview</span>'});
2121
+ renderViewer(header+'<div class="frame-preview" data-preview-content><iframe sandbox="allow-scripts allow-forms allow-modals" srcdoc="'+esc(text)+'" title="'+esc(data.path)+'"></iframe></div>');
2122
+ bindHeaderActions(data.path);
2123
+ }
1937
2124
  function renderMarkdownFile(data,text){
1938
2125
  const header=fileHeader(data,{language:'markdown',truncated:Boolean(data.preview&&data.preview.truncated),save:canEditFile(data),editSource:canEditFile(data)});
1939
2126
  renderViewer(header+'<div class="markdown-preview" data-preview-content>'+markdown(text)+'</div>');
@@ -2942,7 +3129,9 @@ document.getElementById('sessionModal').addEventListener('click',(e)=>{if(e.targ
2942
3129
  document.getElementById('approvalDenyTop').addEventListener('click',()=>submitApproval('reject'));
2943
3130
  document.getElementById('approvalAuto').addEventListener('change',e=>setApprovalAutoMode(e.target.checked));
2944
3131
  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);});
2945
- document.getElementById('refreshFiles').addEventListener('click',()=>{dirCache.clear();loadDir('.');});
3132
+ document.getElementById('fileSearch').addEventListener('input',e=>{workspaceQuery=e.target.value||'';renderExplorer();});
3133
+ document.querySelectorAll('[data-file-filter]').forEach(btn=>btn.addEventListener('click',()=>setWorkspaceFilter(btn.dataset.fileFilter)));
3134
+ document.getElementById('refreshFiles').addEventListener('click',()=>{dirCache.clear();if(workspaceSearchActive())scheduleWorkspaceSearch(0);else loadDir('.');});
2946
3135
  document.getElementById('toggleExplorer').addEventListener('click',()=>{let prefs={explorerVisible:true};try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}saveLayoutPatch({explorerVisible:!prefs.explorerVisible});});
2947
3136
  document.getElementById('toggleAgent').addEventListener('click',()=>{let prefs={agentVisible:true};try{prefs={...prefs,...JSON.parse(localStorage.getItem(layoutKey)||'{}')};}catch{}saveLayoutPatch({agentVisible:!prefs.agentVisible});});
2948
3137
  applyLayout();
@@ -7,6 +7,7 @@
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { load as yamlLoad } from 'js-yaml';
10
+ import { normalizeComposes } from './pi-compose.mjs';
10
11
 
11
12
  /**
12
13
  * Parse a YAML text string into an object using js-yaml.
@@ -185,6 +186,7 @@ export function normalizeManifest(raw, source = '') {
185
186
  // Inline wins on name collision so authors can override a portable
186
187
  // config for the local plugin without editing mcp.json.
187
188
  const mcpServers = _readMcpServers(spec.mcpServers, source);
189
+ const composes = normalizeComposes(spec.composes);
188
190
 
189
191
  return {
190
192
  apiVersion,
@@ -201,6 +203,7 @@ export function normalizeManifest(raw, source = '') {
201
203
  agents,
202
204
  workspace,
203
205
  mcpServers,
206
+ composes,
204
207
  },
205
208
  source,
206
209
  _dir: source ? path.dirname(source) : '',
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Shared npm-install helper for pack and ingredient installs.
3
+ *
4
+ * Two responsibilities:
5
+ * 1. Materialize node_modules/ from package.json::dependencies
6
+ * (co-located with the package/pack, no shared/global install).
7
+ * 2. Gate execution of postinstall scripts on a verified-package
8
+ * allowlist. Unverified packages run `npm install --ignore-scripts`
9
+ * so a malicious postinstall can't execute on `bahulam pull`.
10
+ *
11
+ * Pi peer-dep quirk: pi packages often declare peerDependencies with `*`
12
+ * ranges, which crashes npm arborist. We migrate peers → dependencies
13
+ * before install. Native packs (hand-authored) don't need that step;
14
+ * their package.json is already ordinary.
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import * as os from 'node:os';
20
+ import { spawn } from 'node:child_process';
21
+
22
+ const DIM = '\x1b[2m';
23
+ const YELLOW = '\x1b[33m';
24
+ const RESET = '\x1b[0m';
25
+
26
+ // Verified list: packages we've reviewed and trust to run postinstall.
27
+ // v1 ships an inline dev-time list that mirrors what we're actively
28
+ // using. Long term this migrates to `~/.bahulam/verified-pi-packages.json`
29
+ // synced from awesome-bahulam-plugins (§13.6.1c of PRD-102).
30
+ //
31
+ // The dev-time list intentionally covers only pi packages we've actually
32
+ // pulled and confirmed. Every other package installs with --ignore-scripts
33
+ // until reviewed.
34
+ const DEV_VERIFIED_PI_PACKAGES = new Set([
35
+ 'pi-web-access',
36
+ 'pi-redmine',
37
+ 'pi-goal-x',
38
+ '@speclip/pi-media',
39
+ ]);
40
+
41
+ /**
42
+ * Determine whether a package is verified (postinstall scripts allowed).
43
+ * Reads ~/.bahulam/verified-pi-packages.json if present; falls back to
44
+ * the built-in dev list.
45
+ */
46
+ export function isPackageVerified(packageName, { bahulamHomeDir = null } = {}) {
47
+ if (!packageName) return false;
48
+ if (DEV_VERIFIED_PI_PACKAGES.has(packageName)) return true;
49
+ const home = bahulamHomeDir || process.env.BAHULAM_HOME || path.join(os.homedir(), '.bahulam');
50
+ const registryPath = path.join(home, 'verified-pi-packages.json');
51
+ if (fs.existsSync(registryPath)) {
52
+ try {
53
+ const data = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
54
+ if (data && typeof data === 'object' && data[packageName]) return true;
55
+ } catch { /* fall through */ }
56
+ }
57
+ return false;
58
+ }
59
+
60
+ function runNpm(args, opts) {
61
+ return new Promise((resolve, reject) => {
62
+ const child = spawn('npm', args, { stdio: ['ignore', 'pipe', 'pipe'], ...opts });
63
+ let stderr = '';
64
+ child.stderr.on('data', d => stderr += d);
65
+ child.on('close', code => {
66
+ if (code === 0) resolve();
67
+ else reject(new Error(`npm ${args.join(' ')} exited ${code}: ${stderr.slice(0, 400)}`));
68
+ });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Migrate peerDependencies → dependencies (pi packages only) to sidestep
74
+ * the npm arborist crash on `*` ranges. Idempotent.
75
+ */
76
+ function migratePeersForPi(pkgPath) {
77
+ let pkg = {};
78
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); } catch { return {}; }
79
+ const peers = pkg.peerDependencies || {};
80
+ if (!Object.keys(peers).length) return pkg;
81
+ const merged = { ...(pkg.dependencies || {}) };
82
+ for (const [name, range] of Object.entries(peers)) {
83
+ if (!merged[name]) merged[name] = range === '*' ? 'latest' : range;
84
+ }
85
+ const rewritten = { ...pkg, dependencies: merged };
86
+ delete rewritten.peerDependencies;
87
+ fs.writeFileSync(pkgPath, JSON.stringify(rewritten, null, 2));
88
+ return rewritten;
89
+ }
90
+
91
+ /**
92
+ * Install the package's dependencies into a co-located node_modules.
93
+ * If the package.json has no deps, no-op (returns { installed: 0 }).
94
+ *
95
+ * @param {Object} opts
96
+ * @param {string} opts.dir Package/pack root directory
97
+ * @param {string} [opts.packageName] For verified-list lookup + logs
98
+ * @param {'pi'|'pack'} [opts.kind] Which peer-migration behavior to use
99
+ * @param {boolean} [opts.forceScripts] Force scripts on (ignores verified list)
100
+ * @param {Function} [opts.log] Log fn (line-oriented). Defaults to stderr.
101
+ * @returns {Promise<{installed: number, ranScripts: boolean, verified: boolean}>}
102
+ */
103
+ export async function installPackageDependencies({
104
+ dir,
105
+ packageName = null,
106
+ kind = 'pack',
107
+ forceScripts = false,
108
+ log = null,
109
+ } = {}) {
110
+ const pkgPath = path.join(dir, 'package.json');
111
+ if (!fs.existsSync(pkgPath)) return { installed: 0, ranScripts: false, verified: false };
112
+
113
+ const write = log || ((msg) => process.stderr.write(` ${msg}\n`));
114
+
115
+ // Pi ingredients: migrate peerDependencies → dependencies before install.
116
+ const pkg = kind === 'pi' ? migratePeersForPi(pkgPath) : (() => {
117
+ try { return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); } catch { return {}; }
118
+ })();
119
+
120
+ const depCount = Object.keys(pkg.dependencies || {}).length;
121
+ if (depCount === 0) return { installed: 0, ranScripts: false, verified: false };
122
+
123
+ const verified = forceScripts || isPackageVerified(packageName);
124
+ const scriptsFlag = verified ? [] : ['--ignore-scripts'];
125
+ const label = kind === 'pi' ? 'pi runtime deps' : 'pack npm deps';
126
+
127
+ write(`${DIM}installing ${depCount} ${label}${verified ? '' : ' (scripts skipped — unverified)'}…${RESET}`);
128
+ await runNpm(
129
+ ['install', '--no-audit', '--no-fund', '--legacy-peer-deps', '--silent', ...scriptsFlag],
130
+ { cwd: dir },
131
+ );
132
+
133
+ if (!verified && packageName) {
134
+ write(`${YELLOW}!${RESET} ${DIM}${packageName} is not in the verified list — postinstall scripts were skipped. Some packages need scripts to build native modules; if the pack fails at first use, re-install with ${RESET}--force-scripts${DIM} once you've reviewed the package.${RESET}`);
135
+ }
136
+
137
+ return { installed: depCount, ranScripts: verified, verified };
138
+ }