@oh-my-pi/pi-coding-agent 15.11.8 → 15.12.0

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 (80) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/dist/cli.js +8083 -7692
  3. package/dist/types/collab/crypto.d.ts +1 -6
  4. package/dist/types/collab/guest.d.ts +2 -0
  5. package/dist/types/collab/host.d.ts +16 -0
  6. package/dist/types/collab/protocol.d.ts +14 -1
  7. package/dist/types/config/settings-schema.d.ts +40 -5
  8. package/dist/types/export/custom-share.d.ts +1 -2
  9. package/dist/types/export/html/index.d.ts +39 -1
  10. package/dist/types/export/share.d.ts +43 -0
  11. package/dist/types/main.d.ts +2 -0
  12. package/dist/types/modes/components/agent-hub.d.ts +19 -1
  13. package/dist/types/modes/components/status-line/component.d.ts +6 -1
  14. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  15. package/dist/types/modes/controllers/event-controller.d.ts +7 -0
  16. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
  18. package/dist/types/modes/interactive-mode.d.ts +9 -0
  19. package/dist/types/modes/session-observer-registry.d.ts +7 -0
  20. package/dist/types/modes/theme/theme.d.ts +2 -1
  21. package/dist/types/modes/types.d.ts +12 -0
  22. package/dist/types/session/agent-session.d.ts +2 -0
  23. package/dist/types/session/codex-auto-reset.d.ts +8 -4
  24. package/dist/types/task/executor.d.ts +7 -0
  25. package/dist/types/task/types.d.ts +9 -0
  26. package/package.json +13 -14
  27. package/scripts/build-binary.ts +4 -0
  28. package/scripts/bundle-dist.ts +4 -0
  29. package/scripts/generate-share-viewer.ts +34 -0
  30. package/src/collab/crypto.ts +10 -4
  31. package/src/collab/guest.ts +31 -2
  32. package/src/collab/host.ts +73 -11
  33. package/src/collab/protocol.ts +48 -7
  34. package/src/commands/join.ts +1 -1
  35. package/src/config/settings-schema.ts +40 -4
  36. package/src/config/settings.ts +12 -0
  37. package/src/export/custom-share.ts +1 -1
  38. package/src/export/html/index.ts +122 -17
  39. package/src/export/html/share-loader.js +102 -0
  40. package/src/export/html/template.css +745 -459
  41. package/src/export/html/template.html +6 -3
  42. package/src/export/html/template.js +240 -915
  43. package/src/export/html/tool-views.generated.js +38 -0
  44. package/src/export/share.ts +268 -0
  45. package/src/internal-urls/docs-index.generated.ts +73 -73
  46. package/src/main.ts +22 -9
  47. package/src/modes/components/agent-hub.ts +541 -410
  48. package/src/modes/components/status-line/component.ts +38 -5
  49. package/src/modes/components/status-line/segments.ts +5 -1
  50. package/src/modes/components/status-line/types.ts +2 -0
  51. package/src/modes/components/tips.txt +3 -1
  52. package/src/modes/controllers/command-controller.ts +55 -96
  53. package/src/modes/controllers/event-controller.ts +45 -16
  54. package/src/modes/controllers/input-controller.ts +104 -4
  55. package/src/modes/controllers/selector-controller.ts +11 -15
  56. package/src/modes/controllers/session-focus-controller.ts +112 -0
  57. package/src/modes/interactive-mode.ts +44 -2
  58. package/src/modes/session-observer-registry.ts +11 -0
  59. package/src/modes/theme/theme.ts +6 -0
  60. package/src/modes/types.ts +12 -0
  61. package/src/modes/utils/ui-helpers.ts +16 -13
  62. package/src/prompts/tools/job.md +1 -1
  63. package/src/session/agent-session.ts +65 -7
  64. package/src/session/codex-auto-reset.ts +23 -11
  65. package/src/slash-commands/builtin-registry.ts +62 -35
  66. package/src/task/executor.ts +14 -0
  67. package/src/task/index.ts +5 -1
  68. package/src/task/render.ts +76 -5
  69. package/src/task/types.ts +9 -0
  70. package/src/tiny/worker.ts +17 -95
  71. package/src/tools/job.ts +6 -9
  72. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  73. package/dist/types/export/html/template.generated.d.ts +0 -1
  74. package/dist/types/export/html/template.macro.d.ts +0 -5
  75. package/dist/types/tiny/compiled-runtime.d.ts +0 -35
  76. package/scripts/generate-template.ts +0 -33
  77. package/src/bun-imports.d.ts +0 -28
  78. package/src/export/html/template.generated.ts +0 -2
  79. package/src/export/html/template.macro.ts +0 -25
  80. package/src/tiny/compiled-runtime.ts +0 -179
@@ -2,17 +2,21 @@
2
2
  'use strict';
3
3
 
4
4
  // ============================================================
5
- // DATA LOADING
5
+ // BOOT
6
6
  // ============================================================
7
-
8
- const base64 = document.getElementById('session-data').textContent;
9
- const binary = atob(base64);
10
- const bytes = new Uint8Array(binary.length);
11
- for (let i = 0; i < binary.length; i++) {
12
- bytes[i] = binary.charCodeAt(i);
13
- }
14
- const data = JSON.parse(new TextDecoder('utf-8').decode(bytes));
15
- const { header, entries, leafId: defaultLeafId, systemPrompt, tools } = data;
7
+ //
8
+ // Two boot paths share this template:
9
+ // - Static export: session JSON rides base64-embedded in #session-data.
10
+ // - Share viewer: share-loader.js sets `window.__OMP_SESSION_DATA__` to
11
+ // a promise resolving to the session JSON (fetched + decrypted).
12
+ // The entire app lives in bootSession(); its body keeps the original
13
+ // one-level indentation to avoid a whole-file reindent.
14
+ function bootSession(data) {
15
+ const { header, entries, leafId: defaultLeafId, systemPrompt, tools, subSessions } = data;
16
+
17
+ // Session render context: scopes entry lookups and tool-view host
18
+ // wiring to one transcript (main session or an embedded subagent).
19
+ const mainSctx = { entries, prefix: '', idPrefix: 'entry-' };
16
20
 
17
21
  // ============================================================
18
22
  // URL PARAMETER HANDLING
@@ -630,29 +634,8 @@
630
634
  return text.replace(/\t/g, ' ');
631
635
  }
632
636
 
633
- /** Safely coerce value to string for display. Returns null if invalid type. */
634
- function str(value) {
635
- if (typeof value === 'string') return value;
636
- if (value == null) return '';
637
- return null;
638
- }
639
-
640
- function getLanguageFromPath(filePath) {
641
- const ext = filePath.split('.').pop()?.toLowerCase();
642
- const extToLang = {
643
- ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
644
- py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
645
- c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
646
- php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash',
647
- sql: 'sql', html: 'html', css: 'css', scss: 'scss',
648
- json: 'json', yaml: 'yaml', yml: 'yaml', xml: 'xml',
649
- md: 'markdown', dockerfile: 'dockerfile'
650
- };
651
- return extToLang[ext];
652
- }
653
-
654
- function findToolResult(toolCallId) {
655
- for (const entry of entries) {
637
+ function findToolResult(toolCallId, entryList) {
638
+ for (const entry of entryList) {
656
639
  if (entry.type === 'message' && entry.message.role === 'toolResult') {
657
640
  if (entry.message.toolCallId === toolCallId) {
658
641
  return entry.message;
@@ -721,911 +704,222 @@
721
704
  // ============================================================
722
705
  // TOOL CALL RENDERING
723
706
  // ============================================================
724
-
725
- // Shared helpers for per-tool renderers.
726
- function toolHead(label, pathHtml, badges) {
727
- let html = '<div class="tool-header"><span class="tool-name">' + escapeHtml(label) + '</span>';
728
- if (pathHtml) html += ' <span class="tool-path">' + pathHtml + '</span>';
729
- if (badges) {
730
- for (const badge of badges) {
731
- if (badge != null && badge !== '') {
732
- html += ' <span class="tool-badge">' + escapeHtml(String(badge)) + '</span>';
733
- }
734
- }
735
- }
736
- html += '</div>';
737
- return html;
738
- }
739
-
740
- function invalidArgHtml() {
741
- return '<span class="tool-error">[invalid arg]</span>';
742
- }
743
-
744
- function pathDisplay(filePath, offset, limit) {
745
- if (filePath == null) return invalidArgHtml();
746
- let html = escapeHtml(shortenPath(filePath || ''));
747
- if (offset !== undefined || limit !== undefined) {
748
- const start = offset == null ? 1 : offset;
749
- const end = limit !== undefined ? start + limit - 1 : '';
750
- html += '<span class="line-numbers">:' + start + (end ? '-' + end : '') + '</span>';
751
- }
752
- return html;
753
- }
754
-
755
- function codeBlock(code, lang) {
756
- if (code == null || code === '') return '';
757
- const text = String(code);
758
- let highlighted;
759
- try {
760
- highlighted = lang ? hljs.highlight(text, { language: lang }).value : escapeHtml(text);
761
- } catch {
762
- highlighted = escapeHtml(text);
763
- }
764
- return '<div class="tool-output"><pre><code class="hljs">' + highlighted + '</code></pre></div>';
765
- }
766
-
767
- // Per-tool renderers. Each accepts (name, args, result, ctx) and returns the inner HTML.
768
- function renderBash(name, args, result, ctx) {
769
- const command = str(args.command);
770
- const cwd = str(args.cwd);
771
- const env = args.env && typeof args.env === 'object' ? args.env : null;
772
- const cmdDisplay = command === null ? invalidArgHtml() : escapeHtml(command || '...');
773
- let prefix = '';
774
- if (env) {
775
- for (const [k, v] of Object.entries(env)) {
776
- prefix += escapeHtml(k) + '=' + escapeHtml(String(v)) + ' ';
777
- }
778
- }
779
- let html = '<div class="tool-command">$ ' + prefix + cmdDisplay + '</div>';
780
- const badges = [];
781
- if (cwd) badges.push('cwd=' + shortenPath(cwd));
782
- if (args.timeout) badges.push('timeout=' + args.timeout + 's');
783
- if (args.pty) badges.push('pty');
784
- if (args.head) badges.push('head=' + args.head);
785
- if (args.tail) badges.push('tail=' + args.tail);
786
- if (badges.length) {
787
- html += '<div class="tool-meta">' + badges.map(b => '<span class="tool-badge">' + escapeHtml(b) + '</span>').join(' ') + '</div>';
788
- }
789
- if (result) {
790
- html += ctx.renderResultImages();
791
- const output = ctx.getResultText().trim();
792
- if (output) html += formatExpandableOutput(output, 5);
793
- }
794
- return html;
795
- }
796
-
797
- function renderJsLike(name, args, result, ctx) {
798
- let html = toolHead(name, '');
799
- const cells = result && result.details && Array.isArray(result.details.cells) ? result.details.cells : null;
800
- if (cells) {
801
- for (const cell of cells) {
802
- html += '<div class="tool-cell">';
803
- if (cell && cell.title) html += '<div class="tool-cell-title">' + escapeHtml(String(cell.title)) + '</div>';
804
- const code = cell && typeof cell.code === 'string' ? cell.code : '';
805
- const lang = cell && cell.language === 'js' ? 'javascript' : 'python';
806
- html += codeBlock(code, lang);
807
- html += '</div>';
808
- }
809
- } else if (typeof args.input === 'string') {
810
- html += codeBlock(args.input, null);
811
- } else {
812
- html += '<div class="tool-error">[missing input]</div>';
813
- }
814
- if (result) {
815
- html += ctx.renderResultImages();
816
- const output = ctx.getResultText();
817
- if (output) html += formatExpandableOutput(output, 10);
818
- }
819
- return html;
820
- }
821
-
822
- function renderRead(name, args, result, ctx) {
823
- const filePath = str(args.file_path == null ? args.path : args.file_path);
824
- let pathHtml = pathDisplay(filePath, args.offset, args.limit);
825
- if (args.sel) pathHtml += '<span class="line-numbers">:' + escapeHtml(String(args.sel)) + '</span>';
826
- let html = toolHead('read', pathHtml);
827
- if (result) {
828
- html += ctx.renderResultImages();
829
- const output = ctx.getResultText();
830
- const lang = filePath ? getLanguageFromPath(filePath) : null;
831
- if (output) html += formatExpandableOutput(output, 10, lang);
832
- }
833
- return html;
834
- }
835
-
836
- function renderWrite(name, args, result, ctx) {
837
- const filePath = str(args.file_path == null ? args.path : args.file_path);
838
- const content = str(args.content);
839
- const pathHtml = filePath === null ? invalidArgHtml() : escapeHtml(shortenPath(filePath || ''));
840
- const lineCount = (content != null && content !== '') ? content.split('\n').length : 0;
841
- const badges = lineCount > 10 ? ['(' + lineCount + ' lines)'] : null;
842
- let html = toolHead('write', pathHtml, badges);
843
- if (content === null) {
844
- html += '<div class="tool-error">[invalid content arg - expected string]</div>';
845
- } else if (content) {
846
- const lang = filePath ? getLanguageFromPath(filePath) : null;
847
- html += formatExpandableOutput(content, 10, lang);
848
- }
849
- if (result) {
850
- const output = ctx.getResultText().trim();
851
- if (output) html += '<div class="tool-output"><div>' + escapeHtml(output) + '</div></div>';
852
- }
853
- return html;
854
- }
855
-
856
- function renderEdit(name, args, result, ctx) {
857
- const filePath = str(args.file_path == null ? args.path : args.file_path);
858
- const pathHtml = filePath ? escapeHtml(shortenPath(filePath)) : '';
859
- let html = toolHead('edit', pathHtml);
860
- if (typeof args.input === 'string' && args.input.length) {
861
- html += codeBlock(args.input, null);
862
- } else if (Array.isArray(args.edits)) {
863
- html += '<div class="tool-args">';
864
- for (const e of args.edits) {
865
- const op = e && typeof e.op === 'string' ? e.op : '?';
866
- const sel = e && typeof e.sel === 'string' ? e.sel : '?';
867
- html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(op) + '</span> <span class="tool-arg-val">' + escapeHtml(sel) + '</span></div>';
868
- }
869
- html += '</div>';
870
- }
871
- if (result?.details?.diff) {
872
- const diffLines = String(result.details.diff).split('\n');
873
- html += '<div class="tool-diff">';
874
- for (const line of diffLines) {
875
- const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context';
876
- // Blank gap rows mark non-contiguous regions; show a unicode ellipsis.
877
- const display = line.trim().length === 0 ? '\u2026' : replaceTabs(line);
878
- html += '<div class="' + cls + '">' + escapeHtml(display) + '</div>';
879
- }
880
- html += '</div>';
881
- } else if (result) {
882
- const output = ctx.getResultText().trim();
883
- if (output) html += '<div class="tool-output"><pre>' + escapeHtml(output) + '</pre></div>';
884
- }
885
- return html;
886
- }
887
-
888
- function renderAstEdit(name, args, result, ctx) {
889
- const lang = args.lang || null;
890
- const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (args.path ? shortenPath(String(args.path)) : '');
891
- const pathHtml = paths ? escapeHtml(paths) : '';
892
- const badges = [];
893
- if (lang) badges.push(lang);
894
- if (args.glob) badges.push('glob=' + args.glob);
895
- if (args.sel) badges.push('sel=' + args.sel);
896
- let html = toolHead('ast_edit', pathHtml, badges);
897
- if (Array.isArray(args.ops)) {
898
- for (const op of args.ops) {
899
- html += '<div class="tool-cell">';
900
- html += '<div class="tool-cell-title">pattern</div>';
901
- html += codeBlock(String(op?.pat == null ? '' : op.pat), lang);
902
- html += '<div class="tool-cell-title">replacement</div>';
903
- html += codeBlock(String(op?.out == null ? '' : op.out), lang);
904
- html += '</div>';
905
- }
906
- }
907
- if (result) {
908
- const output = ctx.getResultText();
909
- if (output) html += formatExpandableOutput(output, 10);
910
- }
911
- return html;
707
+ //
708
+ // Tool calls render through the bundled <omp-tool-view> web component
709
+ // (tool-views.generated.js the same React renderers collab-web uses).
710
+ // Payloads are handed over via a global store keyed by data-key, which
711
+ // survives innerHTML serialization and cloneNode round trips.
712
+
713
+ const TOOL_VIEW_DATA = new Map();
714
+ globalThis.__OMP_TOOL_VIEW_DATA = TOOL_VIEW_DATA;
715
+ let toolViewSeq = 0;
716
+
717
+ function renderToolCall(call, sctx) {
718
+ const result = findToolResult(call.id, sctx.entries);
719
+ const statusClass = result ? (result.isError ? 'error' : 'success') : 'pending';
720
+ const key = 'tv' + (++toolViewSeq);
721
+ TOOL_VIEW_DATA.set(key, {
722
+ name: call.name,
723
+ args: call.arguments || {},
724
+ result: result || undefined,
725
+ host: {
726
+ hasAgent: (id) => !!lookupSubSession(sctx.prefix, id),
727
+ openAgent: (id) => openSubSession(joinKey(sctx.prefix, id)),
728
+ },
729
+ });
730
+ return '<omp-tool-view class="tool-execution ' + statusClass + '" data-key="' + key + '" open></omp-tool-view>';
912
731
  }
913
732
 
914
- function renderAstGrep(name, args, result, ctx) {
915
- const lang = args.lang || null;
916
- const pathHtml = args.path ? escapeHtml(shortenPath(String(args.path))) : '';
917
- const badges = [];
918
- if (lang) badges.push(lang);
919
- if (args.glob) badges.push('glob=' + args.glob);
920
- if (args.sel) badges.push('sel=' + args.sel);
921
- let html = toolHead('ast_grep', pathHtml, badges);
922
- if (Array.isArray(args.pat)) {
923
- for (const pat of args.pat) {
924
- html += '<div class="tool-cell">' + codeBlock(String(pat == null ? '' : pat), lang) + '</div>';
925
- }
926
- }
927
- if (result) {
928
- const output = ctx.getResultText();
929
- if (output) html += formatExpandableOutput(output, 10);
930
- }
931
- return html;
932
- }
733
+ // ============================================================
734
+ // SUB-SESSION OVERLAY
735
+ // ============================================================
736
+ //
737
+ // Task tool cards expose agent chips (wired through the payload `host`
738
+ // above); clicking one opens that subagent's transcript in a stacked
739
+ // overlay. Keys are slash-joined agent ids relative to the main
740
+ // session: top-level agent 'ToolAsk', its child 'ToolAsk/Helper'.
933
741
 
934
- function renderGrep(name, args, result, ctx) {
935
- const pattern = str(args.pattern);
936
- const pathHtml = args.path ? escapeHtml(shortenPath(String(args.path))) : escapeHtml('.');
937
- const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
938
- let head = '<span class="tool-name">grep</span> <span class="tool-pattern">/' + patHtml + '/</span>';
939
- head += ' <span class="tool-arg-key">in</span> <span class="tool-path">' + pathHtml + '</span>';
940
- const badges = [];
941
- if (args.glob) badges.push('glob=' + args.glob);
942
- if (args.type) badges.push('type=' + args.type);
943
- if (args.i) badges.push('i');
944
- if (args.multiline) badges.push('multiline');
945
- for (const b of badges) head += ' <span class="tool-badge">' + escapeHtml(b) + '</span>';
946
- let html = '<div class="tool-header">' + head + '</div>';
947
- if (result) {
948
- const output = ctx.getResultText();
949
- if (output) html += formatExpandableOutput(output, 10);
950
- }
951
- return html;
742
+ function joinKey(prefix, id) {
743
+ return prefix ? prefix + '/' + id : id;
952
744
  }
953
745
 
954
- function renderFind(name, args, result, ctx) {
955
- const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (str(args.pattern) || '.');
956
- const patHtml = paths ? escapeHtml(paths) : invalidArgHtml();
957
- const badges = [];
958
- if (args.limit) badges.push('limit=' + args.limit);
959
- if (args.hidden === false) badges.push('no-hidden');
960
- let html = toolHead('find', '<span class="tool-pattern">' + patHtml + '</span>', badges.length ? badges : null);
961
- if (result) {
962
- const output = ctx.getResultText();
963
- if (output) html += formatExpandableOutput(output, 10);
964
- }
965
- return html;
746
+ function lookupSubSession(prefix, id) {
747
+ return subSessions ? subSessions[joinKey(prefix, id)] : undefined;
966
748
  }
967
749
 
968
- function renderLsp(name, args, result, ctx) {
969
- const action = str(args.action) || '?';
970
- let head = '<span class="tool-name">lsp</span> <span class="tool-badge">' + escapeHtml(action) + '</span>';
971
- if (args.file && args.file !== '*') {
972
- head += ' <span class="tool-path">' + escapeHtml(shortenPath(String(args.file))) + '</span>';
973
- } else if (args.file === '*') {
974
- head += ' <span class="tool-badge">workspace</span>';
975
- }
976
- if (args.line) head += '<span class="line-numbers">:' + args.line + '</span>';
977
- if (args.symbol) head += ' <span class="tool-arg-val">' + escapeHtml(String(args.symbol)) + '</span>';
978
- if (args.query) head += ' <span class="tool-arg-key">query=</span><span class="tool-arg-val">' + escapeHtml(String(args.query)) + '</span>';
979
- if (args.new_name) head += ' <span class="tool-arg-key">→</span> <span class="tool-arg-val">' + escapeHtml(String(args.new_name)) + '</span>';
980
- let html = '<div class="tool-header">' + head + '</div>';
981
- if (result) {
982
- const output = ctx.getResultText();
983
- if (output) html += formatExpandableOutput(output, 12);
750
+ // Render context per sub-session (entries scoped to that transcript).
751
+ const subSctxCache = new Map();
752
+ function getSubSctx(key) {
753
+ let sctx = subSctxCache.get(key);
754
+ if (!sctx) {
755
+ sctx = {
756
+ entries: subSessions[key].entries,
757
+ prefix: key,
758
+ idPrefix: 'sub-' + key.replace(/[^A-Za-z0-9_-]/g, '_') + '-entry-',
759
+ };
760
+ subSctxCache.set(key, sctx);
984
761
  }
985
- return html;
762
+ return sctx;
986
763
  }
987
764
 
988
- function todoRoman(n) {
989
- if (n <= 0) return '';
990
- var pairs = [[1000,'M'],[900,'CM'],[500,'D'],[400,'CD'],[100,'C'],[90,'XC'],[50,'L'],[40,'XL'],[10,'X'],[9,'IX'],[5,'V'],[4,'IV'],[1,'I']];
991
- var out = '', rem = n;
992
- for (var i = 0; i < pairs.length; i++) {
993
- while (rem >= pairs[i][0]) { out += pairs[i][1]; rem -= pairs[i][0]; }
765
+ /**
766
+ * Root-to-leaf path through an arbitrary entry list (subagent
767
+ * transcripts are linear chains; same parent-walk as getPath).
768
+ */
769
+ function getPathIn(entryList, targetId) {
770
+ const map = new Map();
771
+ for (const e of entryList) map.set(e.id, e);
772
+ let current = targetId ? map.get(targetId) : undefined;
773
+ if (!current && entryList.length > 0) current = entryList[entryList.length - 1];
774
+ const path = [];
775
+ while (current) {
776
+ path.unshift(current);
777
+ if (!current.parentId || current.parentId === current.id) break;
778
+ current = map.get(current.parentId);
994
779
  }
995
- return out;
780
+ return path;
996
781
  }
997
782
 
998
- function renderTodo(name, args, result, ctx) {
999
- let html = toolHead('todo');
1000
- const ops = Array.isArray(args.ops) ? args.ops : null;
1001
- if (ops) {
1002
- html += '<div class="tool-args">';
1003
- for (const op of ops) {
1004
- const t = op && op.op ? op.op : '?';
1005
- let line = '<span class="tool-arg-key">' + escapeHtml(t) + '</span>';
1006
- if (op?.id) line += ' <span class="tool-arg-val">' + escapeHtml(String(op.id)) + '</span>';
1007
- if (op?.status) line += ' <span class="tool-badge">' + escapeHtml(String(op.status)) + '</span>';
1008
- if (op?.content) line += ' ' + escapeHtml(truncate(String(op.content), 80));
1009
- if (op?.task && typeof op.task === 'object' && op.task.content) line += ' ' + escapeHtml(truncate(String(op.task.content), 80));
1010
- html += '<div class="tool-arg">' + line + '</div>';
1011
- }
1012
- html += '</div>';
1013
- }
1014
- const phases = result?.details?.phases;
1015
- if (Array.isArray(phases)) {
1016
- html += '<div class="todo-tree">';
1017
- for (var __i = 0; __i < phases.length; __i++) {
1018
- var phase = phases[__i];
1019
- var phaseLabel = todoRoman(__i + 1) + '. ' + String(phase.name || '');
1020
- html += '<div class="todo-phase">' + escapeHtml(phaseLabel) + '</div>';
1021
- if (Array.isArray(phase.tasks)) {
1022
- for (const task of phase.tasks) {
1023
- const status = task.status || 'pending';
1024
- const icon = status === 'completed' ? '✓' : status === 'in_progress' ? '→' : status === 'abandoned' ? '✕' : '○';
1025
- html += '<div class="todo-task todo-' + status + '"><span class="todo-icon">' + icon + '</span> ' + escapeHtml(String(task.content || '')) + '</div>';
1026
- }
1027
- }
783
+ const overlayStack = []; // slash-joined keys, root-first chain
784
+ const subSessionBodyCache = new Map(); // key -> rendered body element
785
+ let subOverlayEl = null;
786
+ let subOverlayLastFocus = null;
787
+
788
+ function ensureSubOverlay() {
789
+ if (subOverlayEl) return subOverlayEl;
790
+ subOverlayEl = document.createElement('div');
791
+ subOverlayEl.id = 'subsession-overlay';
792
+ subOverlayEl.innerHTML = `
793
+ <div class="subsession-backdrop"></div>
794
+ <div class="subsession-panel" role="dialog" aria-modal="true" aria-label="Subagent session" tabindex="-1">
795
+ <div class="subsession-header">
796
+ <nav class="subsession-breadcrumb" aria-label="Subagent breadcrumb"></nav>
797
+ <button type="button" class="subsession-close" title="Close (Esc)" aria-label="Close subagent view">&times;</button>
798
+ </div>
799
+ <div class="subsession-meta"></div>
800
+ <div class="subsession-body"></div>
801
+ </div>`;
802
+ subOverlayEl.querySelector('.subsession-backdrop').addEventListener('click', popSubSession);
803
+ subOverlayEl.querySelector('.subsession-close').addEventListener('click', closeAllSubSessions);
804
+ document.body.appendChild(subOverlayEl);
805
+ return subOverlayEl;
806
+ }
807
+
808
+ function buildSubSessionBody(key) {
809
+ let body = subSessionBodyCache.get(key);
810
+ if (body) return body;
811
+ const sub = subSessions[key];
812
+ const sctx = getSubSctx(key);
813
+ body = document.createElement('div');
814
+ body.className = 'subsession-messages';
815
+ for (const entry of getPathIn(sub.entries, sub.leafId)) {
816
+ const node = renderEntryToNode(entry, sctx);
817
+ if (node) body.appendChild(node);
818
+ }
819
+ if (!body.firstChild) {
820
+ const empty = document.createElement('div');
821
+ empty.className = 'subsession-empty';
822
+ empty.textContent = '(no renderable entries)';
823
+ body.appendChild(empty);
824
+ }
825
+ subSessionBodyCache.set(key, body);
826
+ return body;
827
+ }
828
+
829
+ function subSessionMetaText(key) {
830
+ const sub = subSessions[key];
831
+ const stats = computeStats(sub.entries);
832
+ const parts = [];
833
+ if (stats.models.length > 0) parts.push(stats.models.join(', '));
834
+ parts.push(sub.entries.length + (sub.entries.length === 1 ? ' entry' : ' entries'));
835
+ return parts.join(' · ');
836
+ }
837
+
838
+ function renderSubOverlay() {
839
+ const key = overlayStack[overlayStack.length - 1];
840
+ const el = ensureSubOverlay();
841
+
842
+ const crumbs = el.querySelector('.subsession-breadcrumb');
843
+ crumbs.innerHTML = '';
844
+ const segments = key.split('/');
845
+ for (let i = 0; i < segments.length; i++) {
846
+ if (i > 0) {
847
+ const sep = document.createElement('span');
848
+ sep.className = 'subsession-crumb-sep';
849
+ sep.textContent = '›';
850
+ crumbs.appendChild(sep);
1028
851
  }
1029
- html += '</div>';
1030
- } else if (result) {
1031
- const output = ctx.getResultText();
1032
- if (output) html += formatExpandableOutput(output, 8);
1033
- }
1034
- return html;
1035
- }
1036
-
1037
- function renderTask(name, args, result, ctx) {
1038
- const badges = [];
1039
- if (args.resume) badges.push('resume=' + str(args.resume));
1040
- else badges.push('agent=' + (str(args.agent) || '?'));
1041
- if (args.id) badges.push('id=' + str(args.id));
1042
- if (args.isolated) badges.push('isolated');
1043
- let html = toolHead('task', '', badges);
1044
- const description = str(args.description);
1045
- const assignment = str(args.assignment);
1046
- if (description || assignment) {
1047
- html += '<div class="tool-args">';
1048
- if (description) html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(description) + '</span></div>';
1049
- if (assignment) html += '<div class="tool-arg">' + escapeHtml(assignment) + '</div>';
1050
- html += '</div>';
1051
- }
1052
- if (result) {
1053
- const output = ctx.getResultText();
1054
- if (output) html += formatExpandableOutput(output, 12);
1055
- }
1056
- return html;
1057
- }
1058
-
1059
- function renderWebSearch(name, args, result, ctx) {
1060
- const query = str(args.query);
1061
- const queryHtml = query === null ? invalidArgHtml() : escapeHtml(query);
1062
- const badges = [];
1063
- if (args.recency) badges.push('recency=' + args.recency);
1064
- if (args.limit) badges.push('limit=' + args.limit);
1065
- let html = toolHead('web_search', '<span class="tool-pattern">' + queryHtml + '</span>', badges);
1066
- if (result) {
1067
- const output = ctx.getResultText();
1068
- if (output) html += formatExpandableOutput(output, 12, 'markdown');
1069
- }
1070
- return html;
1071
- }
1072
-
1073
- function renderFetch(name, args, result, ctx) {
1074
- const url = str(args.url) || '';
1075
- const badges = args.method ? [String(args.method)] : null;
1076
- let html = toolHead('fetch', '<span class="tool-path">' + escapeHtml(url) + '</span>', badges);
1077
- if (result) {
1078
- const output = ctx.getResultText();
1079
- if (output) html += formatExpandableOutput(output, 10);
1080
- }
1081
- return html;
1082
- }
1083
-
1084
- function renderDebug(name, args, result, ctx) {
1085
- const action = str(args.action) || '?';
1086
- const badges = [];
1087
- if (args.adapter) badges.push(args.adapter);
1088
- if (args.program) badges.push('program=' + shortenPath(String(args.program)));
1089
- if (args.file) badges.push('file=' + shortenPath(String(args.file)));
1090
- if (args.line) badges.push('line=' + args.line);
1091
- let head = '<span class="tool-name">debug</span> <span class="tool-badge">' + escapeHtml(action) + '</span>';
1092
- for (const b of badges) head += ' <span class="tool-badge">' + escapeHtml(String(b)) + '</span>';
1093
- let html = '<div class="tool-header">' + head + '</div>';
1094
- if (args.expression) html += codeBlock(String(args.expression));
1095
- if (result) {
1096
- const output = ctx.getResultText();
1097
- if (output) html += formatExpandableOutput(output, 10);
1098
- }
1099
- return html;
1100
- }
1101
-
1102
- function renderBrowser(name, args, result, ctx) {
1103
- const action = str(args.action) || '?';
1104
- const tabName = str(args.name);
1105
- const badges = [];
1106
- if (tabName) badges.push('name=' + tabName);
1107
- if (args.url) badges.push(String(args.url));
1108
- if (args.app && typeof args.app === 'object') {
1109
- if (args.app.path) badges.push('app=' + shortenPath(String(args.app.path)));
1110
- else if (args.app.cdp_url) badges.push('cdp=' + String(args.app.cdp_url));
1111
- }
1112
- if (args.all) badges.push('all');
1113
- if (args.kill) badges.push('kill');
1114
- let head = '<span class="tool-name">browser</span> <span class="tool-badge">' + escapeHtml(action) + '</span>';
1115
- for (const b of badges) head += ' <span class="tool-badge">' + escapeHtml(String(b)) + '</span>';
1116
- let html = '<div class="tool-header">' + head + '</div>';
1117
- if (action === 'run' && args.code) {
1118
- html += codeBlock(String(args.code), 'javascript');
1119
- }
1120
- if (result) {
1121
- html += ctx.renderResultImages();
1122
- const output = ctx.getResultText();
1123
- if (output) html += formatExpandableOutput(output, 10);
1124
- }
1125
- return html;
1126
- }
1127
-
1128
- function renderInspectImage(name, args, result, ctx) {
1129
- const p = str(args.path == null ? args.url : args.path) || '';
1130
- let html = toolHead('inspect_image', escapeHtml(shortenPath(p)));
1131
- if (result) {
1132
- html += ctx.renderResultImages();
1133
- const output = ctx.getResultText();
1134
- if (output) html += formatExpandableOutput(output, 8);
1135
- }
1136
- return html;
1137
- }
1138
-
1139
- function renderGenerateImage(name, args, result, ctx) {
1140
- const subject = str(args.subject) || '';
1141
- const badges = args.aspect_ratio ? [String(args.aspect_ratio)] : null;
1142
- let html = toolHead('generate_image', '', badges);
1143
- if (subject) html += '<div class="tool-output"><div>' + escapeHtml(subject) + '</div></div>';
1144
- if (result) {
1145
- html += ctx.renderResultImages();
1146
- }
1147
- return html;
1148
- }
1149
-
1150
- function renderAsk(name, args, result, ctx) {
1151
- let html = toolHead('ask');
1152
- const questions = Array.isArray(args.questions) ? args.questions : null;
1153
- if (questions) {
1154
- html += '<div class="tool-args">';
1155
- for (const q of questions) {
1156
- html += '<div class="tool-arg"><span class="tool-arg-key">Q:</span> ' + escapeHtml(String(q?.question || '')) + '</div>';
1157
- if (Array.isArray(q?.options)) {
1158
- for (const opt of q.options) {
1159
- html += '<div class="tool-arg"><span class="tool-arg-key"> -</span> ' + escapeHtml(String(opt?.label || '')) + '</div>';
1160
- }
1161
- }
852
+ if (i === segments.length - 1) {
853
+ const span = document.createElement('span');
854
+ span.className = 'subsession-crumb current';
855
+ span.textContent = segments[i];
856
+ crumbs.appendChild(span);
857
+ } else {
858
+ const btn = document.createElement('button');
859
+ btn.type = 'button';
860
+ btn.className = 'subsession-crumb';
861
+ btn.textContent = segments[i];
862
+ const ancestorKey = segments.slice(0, i + 1).join('/');
863
+ btn.addEventListener('click', () => popSubSessionTo(ancestorKey));
864
+ crumbs.appendChild(btn);
1162
865
  }
1163
- html += '</div>';
1164
- }
1165
- if (result) {
1166
- const output = ctx.getResultText();
1167
- if (output) html += formatExpandableOutput(output, 8);
1168
866
  }
1169
- return html;
1170
- }
1171
-
1172
- function renderResolve(name, args, result, ctx) {
1173
- const action = str(args.action) || '?';
1174
- let html = toolHead('resolve', '', [action]);
1175
- if (args.reason) html += '<div class="tool-output"><div>' + escapeHtml(String(args.reason)) + '</div></div>';
1176
- if (result) {
1177
- const output = ctx.getResultText();
1178
- if (output) html += formatExpandableOutput(output, 6);
1179
- }
1180
- return html;
1181
- }
1182
-
1183
- function renderGh(name, args, result, ctx) {
1184
- const op = str(args.op);
1185
- const badges = [];
1186
- if (op) badges.push(op);
1187
- if (args.repo) badges.push(String(args.repo));
1188
- if (args.issue) badges.push('#' + args.issue);
1189
- if (args.pr) badges.push(Array.isArray(args.pr) ? 'PRs ' + args.pr.join(',') : 'PR ' + args.pr);
1190
- if (args.branch) badges.push('branch=' + args.branch);
1191
- if (args.query) badges.push('query=' + truncate(String(args.query), 60));
1192
- if (args.run) badges.push('run=' + args.run);
1193
- if (args.title) badges.push('title=' + truncate(String(args.title), 40));
1194
- let html = toolHead(name, '', badges);
1195
- if (args.body) html += '<div class="tool-output"><div>' + escapeHtml(truncate(String(args.body), 400)) + '</div></div>';
1196
- if (result) {
1197
- const output = ctx.getResultText();
1198
- if (output) html += formatExpandableOutput(output, 12, 'markdown');
1199
- }
1200
- return html;
1201
- }
1202
-
1203
- function renderMermaid(name, args, result, ctx) {
1204
- let html = toolHead('render_mermaid');
1205
- const code = args.code || args.source;
1206
- if (code) html += codeBlock(String(code), 'mermaid');
1207
- if (result) {
1208
- html += ctx.renderResultImages();
1209
- const output = ctx.getResultText();
1210
- if (output) html += formatExpandableOutput(output, 6);
1211
- }
1212
- return html;
1213
- }
1214
-
1215
- function renderYield(name, args, result, ctx) {
1216
- let html = toolHead('yield');
1217
- if (args.data !== undefined) {
1218
- html += '<div class="tool-output"><pre>' + escapeHtml(JSON.stringify(args.data, null, 2)) + '</pre></div>';
1219
- }
1220
- if (result) {
1221
- const output = ctx.getResultText();
1222
- if (output) html += formatExpandableOutput(output, 6);
1223
- }
1224
- return html;
1225
- }
1226
-
1227
- function renderReportFinding(name, args, result, ctx) {
1228
- const badges = [];
1229
- if (args.priority) badges.push('priority=' + args.priority);
1230
- if (args.confidence != null) badges.push('confidence=' + args.confidence);
1231
- if (args.file_path) badges.push(shortenPath(String(args.file_path)));
1232
- let html = toolHead('report_finding', args.title ? escapeHtml(String(args.title)) : '', badges);
1233
- if (args.body) html += '<div class="tool-output"><div>' + escapeHtml(String(args.body)) + '</div></div>';
1234
- return html;
1235
- }
1236
-
1237
- function renderReportToolIssue(name, args, result, ctx) {
1238
- const pathHtml = args.tool ? '<span class="tool-badge">' + escapeHtml(String(args.tool)) + '</span>' : '';
1239
- let html = toolHead('report_tool_issue', pathHtml);
1240
- if (args.report) html += '<div class="tool-output"><div>' + escapeHtml(String(args.report)) + '</div></div>';
1241
- return html;
1242
- }
1243
-
1244
- function renderJob(name, args, result, ctx) {
1245
- const badges = [];
1246
- const pollIds = Array.isArray(args.poll) ? args.poll : Array.isArray(args.jobs) ? args.jobs : Array.isArray(args.jobIds) ? args.jobIds : [];
1247
- const cancelIds = Array.isArray(args.cancel) ? args.cancel : args.jobId ? [String(args.jobId)] : [];
1248
- if (cancelIds.length > 0) badges.push('cancel ' + cancelIds.length);
1249
- if (pollIds.length > 0) badges.push('poll ' + pollIds.length);
1250
- let html = toolHead('job', '', badges);
1251
- if (result) {
1252
- const output = ctx.getResultText();
1253
- if (output) html += formatExpandableOutput(output, 8);
1254
- }
1255
- return html;
1256
- }
1257
-
1258
- // Parse `*** Cell <attrs>` headers (canonical), plus legacy
1259
- // `*** Begin <LANG>` headers and `===== <info> =====` bars used in
1260
- // older transcripts. Cells emitted before each format cutover still
1261
- // need to render in HTML exports.
1262
- function parseEvalCells(input) {
1263
- const text = String(input);
1264
- if (/^[*]{2,}\s*Cell\b/im.test(text)) return parseEvalCellsCell(text);
1265
- if (/^[*]{2,}\s*Begin\b/im.test(text)) return parseEvalCellsBegin(text);
1266
- return parseEvalCellsLegacy(text);
1267
- }
1268
-
1269
- function evalLangAlias(token) {
1270
- const t = String(token || '').toUpperCase();
1271
- if (t === 'PY' || t === 'PYTHON' || t === 'IPY' || t === 'IPYTHON') return 'py';
1272
- if (t === 'JS' || t === 'JAVASCRIPT') return 'js';
1273
- if (t === 'TS' || t === 'TYPESCRIPT') return 'ts';
1274
- return null;
1275
- }
1276
867
 
1277
- // Tokenize a `*** Cell` header attribute list, preserving quoted
1278
- // segments. Mirrors `tokenizeCellAttrs` in src/eval/parse.ts.
1279
- function tokenizeCellAttrsHtml(input) {
1280
- const tokens = [];
1281
- let i = 0;
1282
- while (i < input.length) {
1283
- while (i < input.length && /\s/.test(input[i])) i++;
1284
- if (i >= input.length) break;
1285
- let tok = '';
1286
- while (i < input.length && !/\s/.test(input[i])) {
1287
- const ch = input[i];
1288
- if (ch === '"' || ch === "'") {
1289
- tok += ch; i++;
1290
- while (i < input.length && input[i] !== ch) { tok += input[i]; i++; }
1291
- if (i < input.length) { tok += input[i]; i++; }
1292
- } else { tok += ch; i++; }
1293
- }
1294
- tokens.push(tok);
1295
- }
1296
- return tokens;
1297
- }
868
+ el.querySelector('.subsession-meta').textContent = subSessionMetaText(key);
1298
869
 
1299
- function parseEvalCellsCell(text) {
1300
- const STARS = '\\*{2,}';
1301
- const CELL = new RegExp('^' + STARS + '\\s*Cell\\b\\s*(.*)$', 'i');
1302
- const END = new RegExp('^' + STARS + '\\s*End\\b.*$', 'i');
1303
- const ATTR = /^([a-zA-Z][\w-]*)(?::(?:"([^"]*)"|'([^']*)'|(.*)))?$/;
1304
- const DUR = /^\d+(?:ms|s|m)?$/;
1305
- const ID_KEYS = ['id', 'title', 'name', 'cell', 'file', 'label'];
1306
- const T_KEYS = ['t', 'timeout', 'duration', 'time'];
1307
- const RST_KEYS = ['rst', 'reset'];
1308
- const lines = text.split('\n');
1309
- if (lines.length && lines[lines.length - 1] === '') lines.pop();
1310
- const cells = [];
1311
- let i = 0;
1312
- while (i < lines.length && lines[i].trim() === '') i++;
1313
- while (i < lines.length) {
1314
- const m = CELL.exec(lines[i]);
1315
- if (!m) { i++; continue; }
1316
- const tokens = tokenizeCellAttrsHtml(m[1] || '');
1317
- let lang = null;
1318
- let title = '';
1319
- const attrs = [];
1320
- let bareReset = false;
1321
- const titleParts = [];
1322
- for (const tok of tokens) {
1323
- const lower = tok.toLowerCase();
1324
- if (RST_KEYS.indexOf(lower) >= 0) { bareReset = true; continue; }
1325
- const am = ATTR.exec(tok);
1326
- if (am && tok.indexOf(':') >= 0) {
1327
- const key = am[1].toLowerCase();
1328
- const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : (am[4] || '');
1329
- const lc = evalLangAlias(key);
1330
- if (lc) {
1331
- if (!lang) lang = lc;
1332
- if (!title && value) title = value;
1333
- continue;
1334
- }
1335
- if (ID_KEYS.indexOf(key) >= 0) { if (!title) title = value; continue; }
1336
- if (T_KEYS.indexOf(key) >= 0) { attrs.push('t=' + value); continue; }
1337
- if (RST_KEYS.indexOf(key) >= 0) { attrs.push('rst'); continue; }
1338
- continue;
1339
- }
1340
- const lc = evalLangAlias(tok);
1341
- if (lc && !lang) { lang = lc; continue; }
1342
- if (DUR.test(tok)) { attrs.push('t=' + tok); continue; }
1343
- titleParts.push(tok);
1344
- }
1345
- if (!title && titleParts.length) title = titleParts.join(' ');
1346
- if (bareReset) attrs.push('rst');
1347
- lang = lang || 'py';
1348
- i++;
1349
- const codeLines = [];
1350
- while (i < lines.length) {
1351
- if (END.test(lines[i])) { i++; break; }
1352
- if (CELL.test(lines[i])) break;
1353
- codeLines.push(lines[i]);
1354
- i++;
1355
- }
1356
- while (codeLines.length && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
1357
- cells.push({ lang, title, attrs, code: codeLines.join('\n') });
1358
- while (i < lines.length && lines[i].trim() === '') i++;
1359
- }
1360
- return cells;
1361
- }
870
+ const bodyHost = el.querySelector('.subsession-body');
871
+ bodyHost.innerHTML = '';
872
+ bodyHost.appendChild(buildSubSessionBody(key));
873
+ bodyHost.scrollTop = 0;
1362
874
 
1363
- function parseEvalCellsBegin(text) {
1364
- const STARS = '\\*{2,}';
1365
- const BEGIN = new RegExp('^' + STARS + '\\s*Begin\\b\\s*(\\S+)?\\s*$', 'i');
1366
- const END = new RegExp('^' + STARS + '\\s*End\\b.*$', 'i');
1367
- const TITLE = new RegExp('^' + STARS + '\\s*Title\\s*:\\s*(.+?)\\s*$', 'i');
1368
- const TIMEOUT = new RegExp('^' + STARS + '\\s*Timeout\\s*:\\s*(\\S+)\\s*$', 'i');
1369
- const RESET = new RegExp('^' + STARS + '\\s*Reset\\s*$', 'i');
1370
- const lines = text.split('\n');
1371
- if (lines.length && lines[lines.length - 1] === '') lines.pop();
1372
- const cells = [];
1373
- let i = 0;
1374
- while (i < lines.length && lines[i].trim() === '') i++;
1375
- while (i < lines.length) {
1376
- const beginMatch = BEGIN.exec(lines[i]);
1377
- if (!beginMatch) { i++; continue; }
1378
- const lang = evalLangAlias(beginMatch[1]) || 'py';
1379
- i++;
1380
- let title = '';
1381
- const attrs = [];
1382
- while (i < lines.length) {
1383
- const tm = TITLE.exec(lines[i]);
1384
- if (tm) { if (!title) title = tm[1]; i++; continue; }
1385
- const to = TIMEOUT.exec(lines[i]);
1386
- if (to) { attrs.push('t=' + to[1]); i++; continue; }
1387
- if (RESET.test(lines[i])) { attrs.push('rst'); i++; continue; }
1388
- break;
1389
- }
1390
- const codeLines = [];
1391
- while (i < lines.length) {
1392
- if (END.test(lines[i])) { i++; break; }
1393
- if (BEGIN.test(lines[i])) break;
1394
- codeLines.push(lines[i]);
1395
- i++;
1396
- }
1397
- while (codeLines.length && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
1398
- cells.push({ lang, title, attrs, code: codeLines.join('\n') });
1399
- while (i < lines.length && lines[i].trim() === '') i++;
1400
- }
1401
- return cells;
875
+ el.classList.add('open');
876
+ el.querySelector('.subsession-panel').focus();
1402
877
  }
1403
878
 
1404
- function parseEvalCellsLegacy(input) {
1405
- const HEADER = /^={5,}\s*(.*?)\s*={5,}\s*$/;
1406
- const lines = String(input).split('\n');
1407
- const cells = [];
1408
- let inheritedLang = 'py';
1409
- let current = null;
1410
- for (const line of lines) {
1411
- const m = line.match(HEADER);
1412
- if (m) {
1413
- if (current) cells.push(current);
1414
- const info = m[1] || '';
1415
- let lang = inheritedLang;
1416
- let title = '';
1417
- const langMatch = info.match(/^(py|js|ts)(?::"([^"]*)")?/);
1418
- if (langMatch) {
1419
- lang = langMatch[1];
1420
- if (langMatch[2]) title = langMatch[2];
1421
- }
1422
- if (!title) {
1423
- const idMatch = info.match(/id:"([^"]*)"/);
1424
- if (idMatch) title = idMatch[1];
1425
- }
1426
- inheritedLang = lang;
1427
- const attrs = [];
1428
- const tMatch = info.match(/(?:^|\s)t:(\S+)/);
1429
- if (tMatch) attrs.push('t=' + tMatch[1]);
1430
- if (/(?:^|\s)rst(?:\s|$)/.test(info)) attrs.push('rst');
1431
- current = { lang, title, attrs, code: '' };
1432
- } else {
1433
- if (!current) current = { lang: inheritedLang, title: '', attrs: [], code: '' };
1434
- current.code += (current.code ? '\n' : '') + line;
1435
- }
879
+ function openSubSession(key) {
880
+ if (!subSessions || !subSessions[key]) return;
881
+ if (overlayStack.length === 0) {
882
+ subOverlayLastFocus = document.activeElement;
1436
883
  }
1437
- if (current) cells.push(current);
1438
- return cells.map(c => ({ ...c, code: c.code.replace(/\s+$/, '') }));
884
+ overlayStack.push(key);
885
+ renderSubOverlay();
1439
886
  }
1440
887
 
1441
- function evalLangToHljs(lang) {
1442
- return lang === 'py' ? 'python' : lang === 'js' ? 'javascript' : lang === 'ts' ? 'typescript' : null;
1443
- }
1444
-
1445
- function renderEval(name, args, result, ctx) {
1446
- let html = toolHead('eval');
1447
- if (typeof args.input !== 'string') {
1448
- html += '<div class="tool-error">[missing input]</div>';
888
+ function popSubSession() {
889
+ if (overlayStack.length === 0) return;
890
+ overlayStack.pop();
891
+ if (overlayStack.length === 0) {
892
+ hideSubOverlay();
1449
893
  } else {
1450
- const cells = parseEvalCells(args.input);
1451
- if (cells.length === 0) {
1452
- html += codeBlock(args.input, 'python');
1453
- } else {
1454
- for (const cell of cells) {
1455
- html += '<div class="tool-cell">';
1456
- const titleParts = [];
1457
- if (cell.title) titleParts.push(cell.title);
1458
- titleParts.push(cell.lang);
1459
- if (cell.attrs && cell.attrs.length) titleParts.push(...cell.attrs);
1460
- html += '<div class="tool-cell-title">' + escapeHtml(titleParts.join(' · ')) + '</div>';
1461
- html += codeBlock(cell.code, evalLangToHljs(cell.lang));
1462
- html += '</div>';
1463
- }
1464
- }
1465
- }
1466
- if (result) {
1467
- html += ctx.renderResultImages();
1468
- const output = ctx.getResultText();
1469
- if (output) html += formatExpandableOutput(output, 12);
894
+ renderSubOverlay();
1470
895
  }
1471
- return html;
1472
896
  }
1473
897
 
1474
- function renderSearch(name, args, result, ctx) {
1475
- const pattern = str(args.pattern);
1476
- const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (args.path ? shortenPath(String(args.path)) : '.');
1477
- const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
1478
- let head = '<span class="tool-name">search</span> <span class="tool-pattern">/' + patHtml + '/</span>';
1479
- head += ' <span class="tool-arg-key">in</span> <span class="tool-path">' + escapeHtml(paths) + '</span>';
1480
- const badges = [];
1481
- if (args.i) badges.push('i');
1482
- if (args.skip) badges.push('skip=' + args.skip);
1483
- if (args.gitignore === false) badges.push('no-gitignore');
1484
- for (const b of badges) head += ' <span class="tool-badge">' + escapeHtml(b) + '</span>';
1485
- let html = '<div class="tool-header">' + head + '</div>';
1486
- if (result) {
1487
- const output = ctx.getResultText();
1488
- if (output) html += formatExpandableOutput(output, 12);
898
+ function popSubSessionTo(key) {
899
+ // Rebuild the chain root..key (the stack is always a prefix chain).
900
+ const segments = key.split('/');
901
+ overlayStack.length = 0;
902
+ for (let i = 1; i <= segments.length; i++) {
903
+ overlayStack.push(segments.slice(0, i).join('/'));
1489
904
  }
1490
- return html;
905
+ renderSubOverlay();
1491
906
  }
1492
907
 
1493
- function renderIrc(name, args, result, ctx) {
1494
- const details = result && result.details ? result.details : null;
1495
- const op = str(args.op) || (details && str(details.op)) || '?';
1496
- const badges = [op];
1497
- if (args.to) badges.push('to=' + str(args.to));
1498
- if (op === 'wait' && args.from) badges.push('from=' + str(args.from));
1499
- if (args.await) badges.push('await');
1500
- if (args.peek) badges.push('peek');
1501
- let html = toolHead('irc', '', badges);
1502
- if (args.message) html += '<div class="tool-output"><div>' + escapeHtml(String(args.message)) + '</div></div>';
1503
- let renderedDetails = false;
1504
- if (details && Array.isArray(details.receipts) && details.receipts.length) {
1505
- html += '<div class="tool-args">';
1506
- for (const receipt of details.receipts) {
1507
- const outcome = escapeHtml(String(receipt.outcome)) + (receipt.error ? ' — ' + escapeHtml(String(receipt.error)) : '');
1508
- html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(String(receipt.to)) + '</span> ' + outcome + '</div>';
1509
- }
1510
- html += '</div>';
1511
- renderedDetails = true;
1512
- }
1513
- if (details && details.waited) {
1514
- html += '<div class="tool-output"><div>' + escapeHtml(String(details.waited.from)) + ': ' + escapeHtml(String(details.waited.body)) + '</div></div>';
1515
- renderedDetails = true;
1516
- }
1517
- if (details && Array.isArray(details.inbox) && details.inbox.length) {
1518
- html += '<div class="tool-args">';
1519
- for (const msg of details.inbox) {
1520
- html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(String(msg.from)) + '</span> ' + escapeHtml(String(msg.body)) + '</div>';
1521
- }
1522
- html += '</div>';
1523
- renderedDetails = true;
1524
- }
1525
- if (!renderedDetails && result) {
1526
- const output = ctx.getResultText();
1527
- if (output) html += formatExpandableOutput(output, 8);
1528
- }
1529
- return html;
908
+ function closeAllSubSessions() {
909
+ if (overlayStack.length === 0) return;
910
+ overlayStack.length = 0;
911
+ hideSubOverlay();
1530
912
  }
1531
913
 
1532
-
1533
- function renderGenericTool(name, args, result, ctx) {
1534
- let html = toolHead(name);
1535
- const argText = JSON.stringify(args, null, 2);
1536
- if (argText && argText !== '{}') {
1537
- html += '<div class="tool-output"><pre>' + escapeHtml(argText) + '</pre></div>';
914
+ function hideSubOverlay() {
915
+ if (subOverlayEl) {
916
+ subOverlayEl.classList.remove('open');
917
+ subOverlayEl.querySelector('.subsession-body').innerHTML = '';
1538
918
  }
1539
- if (result) {
1540
- html += ctx.renderResultImages();
1541
- const output = ctx.getResultText();
1542
- if (output) html += formatExpandableOutput(output, 10);
919
+ if (subOverlayLastFocus && typeof subOverlayLastFocus.focus === 'function') {
920
+ subOverlayLastFocus.focus();
1543
921
  }
1544
- return html;
1545
- }
1546
-
1547
- const TOOL_RENDERERS = {
1548
- bash: renderBash,
1549
- eval: renderEval,
1550
- js: renderJsLike,
1551
- python: renderJsLike,
1552
- notebook: renderJsLike,
1553
- read: renderRead,
1554
- write: renderWrite,
1555
- edit: renderEdit,
1556
- ast_edit: renderAstEdit,
1557
- ast_grep: renderAstGrep,
1558
- grep: renderGrep,
1559
- search: renderSearch,
1560
- find: renderFind,
1561
- lsp: renderLsp,
1562
- todo: renderTodo,
1563
- task: renderTask,
1564
- web_search: renderWebSearch,
1565
- fetch: renderFetch,
1566
- debug: renderDebug,
1567
- puppeteer: renderBrowser,
1568
- browser: renderBrowser,
1569
- inspect_image: renderInspectImage,
1570
- generate_image: renderGenerateImage,
1571
- ask: renderAsk,
1572
- resolve: renderResolve,
1573
- github: renderGh,
1574
- render_mermaid: renderMermaid,
1575
- yield: renderYield,
1576
- report_finding: renderReportFinding,
1577
- report_tool_issue: renderReportToolIssue,
1578
- await: renderJob,
1579
- poll: renderJob,
1580
- cancel_job: renderJob,
1581
- job: renderJob,
1582
- irc: renderIrc,
1583
- };
1584
-
1585
- function renderToolCall(call) {
1586
- const result = findToolResult(call.id);
1587
- const isError = result?.isError || false;
1588
- const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
1589
- const rawArgs = call.arguments || {};
1590
- const intent = typeof rawArgs._i === 'string' ? rawArgs._i.trim() : '';
1591
- // Strip internal _i intent so renderers don't dump it as JSON.
1592
- const args = {};
1593
- for (const k of Object.keys(rawArgs)) {
1594
- if (k !== '_i') args[k] = rawArgs[k];
1595
- }
1596
- const name = call.name;
1597
-
1598
- const ctx = {
1599
- intent,
1600
- getResultText: () => {
1601
- if (!result) return '';
1602
- const textBlocks = result.content.filter(c => c.type === 'text');
1603
- return textBlocks.map(c => c.text).join('\n');
1604
- },
1605
- getResultImages: () => {
1606
- if (!result) return [];
1607
- return result.content.filter(c => c.type === 'image');
1608
- },
1609
- renderResultImages: () => {
1610
- if (!result) return '';
1611
- const images = result.content.filter(c => c.type === 'image');
1612
- if (images.length === 0) return '';
1613
- return '<div class="tool-images">' +
1614
- images.map(img => '<img src="data:' + img.mimeType + ';base64,' + img.data + '" class="tool-image" />').join('') +
1615
- '</div>';
1616
- },
1617
- };
1618
-
1619
- const renderer = TOOL_RENDERERS[name] || renderGenericTool;
1620
- let html = '<div class="tool-execution ' + statusClass + '">';
1621
- if (intent) html += '<div class="tool-intent">' + escapeHtml(intent) + '</div>';
1622
- try {
1623
- html += renderer(name, args, result, ctx);
1624
- } catch (err) {
1625
- html += renderGenericTool(name, args, result, ctx);
1626
- }
1627
- html += '</div>';
1628
- return html;
922
+ subOverlayLastFocus = null;
1629
923
  }
1630
924
 
1631
925
 
@@ -1710,11 +1004,12 @@
1710
1004
  </button>`;
1711
1005
  }
1712
1006
 
1713
- function renderEntry(entry) {
1007
+ function renderEntry(entry, sctx) {
1714
1008
  const ts = formatTimestamp(entry.timestamp);
1715
1009
  const tsHtml = ts ? `<div class="message-timestamp">${ts}</div>` : '';
1716
- const entryId = `entry-${entry.id}`;
1717
- const copyBtnHtml = renderCopyLinkButton(entry.id);
1010
+ const entryId = `${sctx.idPrefix}${entry.id}`;
1011
+ // Share links target main-session entries only; overlays skip them.
1012
+ const copyBtnHtml = sctx.prefix === '' ? renderCopyLinkButton(entry.id) : '';
1718
1013
 
1719
1014
  if (entry.type === 'message') {
1720
1015
  const msg = entry.message;
@@ -1771,7 +1066,7 @@
1771
1066
 
1772
1067
  for (const block of msg.content) {
1773
1068
  if (block.type === 'toolCall') {
1774
- html += renderToolCall(block);
1069
+ html += renderToolCall(block, sctx);
1775
1070
  }
1776
1071
  }
1777
1072
 
@@ -1959,14 +1254,16 @@
1959
1254
  // Cache for rendered entry DOM nodes
1960
1255
  const entryCache = new Map();
1961
1256
 
1962
- function renderEntryToNode(entry) {
1963
- // Check cache first
1964
- if (entryCache.has(entry.id)) {
1257
+ function renderEntryToNode(entry, sctx) {
1258
+ // Cache main-session nodes only; sub-session bodies are cached whole
1259
+ // per key in subSessionBodyCache, so each entry renders once anyway.
1260
+ const cacheable = sctx.prefix === '';
1261
+ if (cacheable && entryCache.has(entry.id)) {
1965
1262
  return entryCache.get(entry.id).cloneNode(true);
1966
1263
  }
1967
1264
 
1968
1265
  // Render to HTML string, then parse to node
1969
- const html = renderEntry(entry);
1266
+ const html = renderEntry(entry, sctx);
1970
1267
  if (!html) return null;
1971
1268
 
1972
1269
  const template = document.createElement('template');
@@ -1974,7 +1271,7 @@
1974
1271
  const node = template.content.firstElementChild;
1975
1272
 
1976
1273
  // Cache the node
1977
- if (node) {
1274
+ if (cacheable && node) {
1978
1275
  entryCache.set(entry.id, node.cloneNode(true));
1979
1276
  }
1980
1277
  return node;
@@ -1994,7 +1291,7 @@
1994
1291
  const fragment = document.createDocumentFragment();
1995
1292
 
1996
1293
  for (const entry of path) {
1997
- const node = renderEntryToNode(entry);
1294
+ const node = renderEntryToNode(entry, mainSctx);
1998
1295
  if (node) {
1999
1296
  fragment.appendChild(node);
2000
1297
  }
@@ -2228,13 +1525,13 @@
2228
1525
  document.getElementById('sidebar-close').addEventListener('click', closeSidebar);
2229
1526
 
2230
1527
  // Toggle states
2231
- let thinkingExpanded = true;
1528
+ let thinkingExpanded = false;
2232
1529
  let toolOutputsExpanded = false;
2233
1530
 
2234
1531
  const toggleThinking = () => {
2235
1532
  thinkingExpanded = !thinkingExpanded;
2236
1533
  document.querySelectorAll('.thinking-text').forEach(el => {
2237
- el.style.display = thinkingExpanded ? '' : 'none';
1534
+ el.style.display = thinkingExpanded ? 'block' : 'none';
2238
1535
  });
2239
1536
  document.querySelectorAll('.thinking-collapsed').forEach(el => {
2240
1537
  el.style.display = thinkingExpanded ? 'none' : 'block';
@@ -2254,6 +1551,11 @@
2254
1551
  // Keyboard shortcuts
2255
1552
  document.addEventListener('keydown', (e) => {
2256
1553
  if (e.key === 'Escape') {
1554
+ if (overlayStack.length > 0) {
1555
+ e.preventDefault();
1556
+ popSubSession();
1557
+ return;
1558
+ }
2257
1559
  searchInput.value = '';
2258
1560
  searchQuery = '';
2259
1561
  navigateTo(leafId, 'bottom');
@@ -2280,4 +1582,27 @@
2280
1582
  // Fallback: use last entry if no leafId
2281
1583
  navigateTo(entries[entries.length - 1].id, 'none');
2282
1584
  }
1585
+ } // end bootSession
1586
+
1587
+ function showLoadError(err) {
1588
+ const messages = document.getElementById('messages');
1589
+ if (!messages) return;
1590
+ const div = document.createElement('div');
1591
+ div.className = 'share-load-error';
1592
+ div.textContent = 'Failed to load session: ' + (err && err.message ? err.message : String(err));
1593
+ messages.appendChild(div);
1594
+ }
1595
+
1596
+ const pending = window.__OMP_SESSION_DATA__;
1597
+ if (pending && typeof pending.then === 'function') {
1598
+ pending.then(bootSession, showLoadError);
1599
+ } else {
1600
+ const base64 = document.getElementById('session-data').textContent;
1601
+ const binary = atob(base64);
1602
+ const bytes = new Uint8Array(binary.length);
1603
+ for (let i = 0; i < binary.length; i++) {
1604
+ bytes[i] = binary.charCodeAt(i);
1605
+ }
1606
+ bootSession(JSON.parse(new TextDecoder('utf-8').decode(bytes)));
1607
+ }
2283
1608
  })();