@ai-setting/roy-plugin-task-show 0.6.12 → 0.8.5

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.
package/public/index.html CHANGED
@@ -5,20 +5,66 @@
5
5
  <title>roy-plugin-task-show</title>
6
6
  <link rel="stylesheet" href="/static/style.css" />
7
7
  </head>
8
- <body data-live-refresh>
8
+ <body data-live-refresh data-view="tasks-tree">
9
9
  <header class="topbar">
10
- <h1>📊 roy-plugin-task-show</h1>
10
+ <h1>🌳 roy-plugin-task-show · Tasks Tree</h1>
11
11
  <p class="meta">
12
- Standalone landing page served by the visualization service. The plugin
13
- replaces the index when sessions exist.
12
+ Hierarchical view of <code>roy-agent</code> tasks (driven by
13
+ <code>roy-agent tasks tree --json</code>).
14
14
  </p>
15
15
  <p class="meta connection-status" data-connection>
16
16
  <span class="conn-dot conn-disconnected"></span>
17
17
  <span class="conn-label">Connecting…</span>
18
18
  </p>
19
19
  </header>
20
- <section class="panel">
21
- <h2>No sessions yet</h2>
20
+
21
+ <section class="panel tree-controls">
22
+ <div class="tree-controls-row">
23
+ <label class="tree-search">
24
+ <span>🔎</span>
25
+ <input type="search" data-tree-search placeholder="Filter by id or title (case-insensitive)" />
26
+ </label>
27
+ <div class="tree-filter-group" role="group" aria-label="Status filter">
28
+ <button type="button" class="tree-filter-btn is-active" data-tree-status="">all</button>
29
+ <button type="button" class="tree-filter-btn" data-tree-status="active">active</button>
30
+ <button type="button" class="tree-filter-btn" data-tree-status="todo">todo</button>
31
+ <button type="button" class="tree-filter-btn" data-tree-status="completed">completed</button>
32
+ <button type="button" class="tree-filter-btn" data-tree-status="paused">paused</button>
33
+ <button type="button" class="tree-filter-btn" data-tree-status="cancelled">cancelled</button>
34
+ </div>
35
+ <div class="tree-filter-group" role="group" aria-label="Priority filter">
36
+ <button type="button" class="tree-filter-btn is-active" data-tree-priority="">all priority</button>
37
+ <button type="button" class="tree-filter-btn" data-tree-priority="high">high</button>
38
+ <button type="button" class="tree-filter-btn" data-tree-priority="medium">medium</button>
39
+ <button type="button" class="tree-filter-btn" data-tree-priority="low">low</button>
40
+ </div>
41
+ <div class="tree-filter-group" role="group" aria-label="Tree actions">
42
+ <button type="button" class="tree-filter-btn" data-tree-action="expand">expand all</button>
43
+ <button type="button" class="tree-filter-btn" data-tree-action="collapse">collapse all</button>
44
+ <button type="button" class="tree-filter-btn" data-tree-action="refresh">↻ refresh</button>
45
+ </div>
46
+ </div>
47
+ <p class="meta tree-summary" data-tree-summary>Loading…</p>
48
+ </section>
49
+
50
+ <section class="panel tree-panel" aria-label="Task hierarchy">
51
+ <div class="tasks-tree" data-tasks-tree role="tree" aria-busy="true">
52
+ <p class="empty">Loading task tree…</p>
53
+ </div>
54
+ </section>
55
+
56
+ <section class="panel tree-legend">
57
+ <h2>How to read this tree</h2>
58
+ <ul>
59
+ <li>Each <strong>chevron (▶ / ▼)</strong> expands or collapses a node's children. By default the first <strong>three levels</strong> are open so the hierarchy is obvious at a glance.</li>
60
+ <li>Use <strong>expand all</strong> / <strong>collapse all</strong> at the top to control the whole tree, or click a single chevron to drill into one branch.</li>
61
+ <li>Filter chips restrict by <strong>status</strong> or <strong>priority</strong>; the search box matches against id or title.</li>
62
+ <li>Click any task title or id (<code>#1234</code>) to open its per-task detail page (tool-call flow + operations timeline).</li>
63
+ </ul>
64
+ </section>
65
+
66
+ <section class="panel tree-legacy-hidden" id="legacy-sessions" hidden>
67
+ <h2>Live tool-call sessions (legacy view)</h2>
22
68
  <p class="empty">
23
69
  The <code>roy-agent</code> host process has not recorded any tool
24
70
  calls yet. Trigger a session — for example by sending a message in
@@ -32,21 +78,8 @@
32
78
  <tbody></tbody>
33
79
  </table>
34
80
  </section>
35
- <section class="panel">
36
- <h2>How to read this page</h2>
37
- <ul>
38
- <li>The page subscribes to <code>/api/events</code> (SSE) for real-time updates.</li>
39
- <li>If the SSE connection fails, a 3-second polling fallback keeps the page fresh.</li>
40
- <li>Useful endpoints:
41
- <ul>
42
- <li><a href="/api/sessions">/api/sessions</a> — JSON list of recorded sessions.</li>
43
- <li><a href="/api/events">/api/events</a> — SSE event stream.</li>
44
- <li><a href="/static/style.css">/static/style.css</a> — bundled CSS.</li>
45
- <li><a href="/static/app.js">/static/app.js</a> — bundled JS.</li>
46
- </ul>
47
- </li>
48
- </ul>
49
- </section>
81
+
50
82
  <script src="/static/app.js"></script>
83
+ <script src="/static/tasks-tree.js"></script>
51
84
  </body>
52
85
  </html>
package/public/style.css CHANGED
@@ -221,6 +221,28 @@ table.toolcalls .result {
221
221
  word-break: break-all;
222
222
  }
223
223
 
224
+ /* v0.8.0+ tool-row collapsed / expanded / highlight states. The row
225
+ starts in `.collapsed` (input/output <details> closed) and gets
226
+ toggled to `.expanded` on click. The `.highlight` class is added
227
+ for 2s by the Mermaid `__toolClick` callback so the user sees a
228
+ yellow flash pointing at the target row. */
229
+ table.toolcalls tr.tool-row {
230
+ transition: background 0.3s ease-out;
231
+ }
232
+ table.toolcalls tr.tool-row.collapsed .tool-detail summary {
233
+ color: var(--muted);
234
+ }
235
+ table.toolcalls tr.tool-row.expanded .tool-detail summary {
236
+ color: var(--link);
237
+ }
238
+ table.toolcalls tr.tool-row.highlight {
239
+ background: rgba(255, 200, 0, 0.30) !important;
240
+ transition: background 0.3s ease-out;
241
+ }
242
+ table.toolcalls tr.tool-row.highlight:hover {
243
+ background: rgba(255, 200, 0, 0.36) !important;
244
+ }
245
+
224
246
  .status {
225
247
  font-size: 11px;
226
248
  font-weight: 600;
@@ -339,6 +361,17 @@ details summary {
339
361
  /* keeps the existing panel rhythm */
340
362
  }
341
363
 
364
+ .panel-lifecycle {
365
+ /* The "Task lifecycle + tools" Mermaid section in the detail page. */
366
+ }
367
+
368
+ .lifecycle-hint {
369
+ margin-left: 8px;
370
+ font-size: 11.5px;
371
+ color: var(--muted);
372
+ font-weight: normal;
373
+ }
374
+
342
375
  .op-timeline {
343
376
  list-style: none;
344
377
  margin: 0;
@@ -452,6 +485,59 @@ details summary {
452
485
  margin-right: 4px;
453
486
  }
454
487
 
488
+ /* v0.8.0+ always-visible description / process blocks (no <details>
489
+ collapse unless the description is > 600 chars, in which case the
490
+ legacy details fallback kicks in). The two blocks share a panel
491
+ rhythm and use a left-border accent so the eye can separate them
492
+ at a glance. */
493
+ .op-desc-block,
494
+ .op-proc-block {
495
+ background: var(--panel-2);
496
+ border: 1px solid var(--border);
497
+ border-left: 3px solid var(--border);
498
+ border-radius: 6px;
499
+ padding: 6px 10px 8px;
500
+ margin-top: 6px;
501
+ font-size: 13px;
502
+ line-height: 1.5;
503
+ }
504
+
505
+ .op-desc-block {
506
+ border-left-color: var(--accent);
507
+ }
508
+
509
+ .op-proc-block {
510
+ border-left-color: var(--muted);
511
+ }
512
+
513
+ .op-block-label {
514
+ display: inline-block;
515
+ font-size: 11px;
516
+ font-weight: 600;
517
+ letter-spacing: 0.04em;
518
+ color: var(--muted);
519
+ text-transform: uppercase;
520
+ margin-bottom: 4px;
521
+ }
522
+
523
+ .op-desc-block .op-desc {
524
+ margin: 0;
525
+ }
526
+
527
+ .op-proc-block .op-proc-body {
528
+ font-size: 12.5px;
529
+ color: var(--text);
530
+ white-space: pre-wrap;
531
+ word-break: break-word;
532
+ }
533
+
534
+ .op-desc-empty,
535
+ .op-proc-empty {
536
+ color: var(--muted);
537
+ font-style: italic;
538
+ font-size: 12px;
539
+ }
540
+
455
541
  /* Status colors per canonical milestone type */
456
542
  .op-status-create .op-bullet { background: #1e40af; border-color: #3b82f6; }
457
543
  .op-status-create .op-badge { background: #1e3a8a; color: #dbeafe; border-color: #3b82f6; }
@@ -525,3 +611,419 @@ details summary {
525
611
  margin-left: 0;
526
612
  }
527
613
  }
614
+
615
+ /* ------------------------------------------------------------------------- */
616
+ /* Tasks tree (v0.8.0+) */
617
+ /* ------------------------------------------------------------------------- */
618
+
619
+ .tree-controls {
620
+ border-left: 3px solid var(--accent);
621
+ }
622
+ .tree-controls-row {
623
+ display: flex;
624
+ flex-wrap: wrap;
625
+ align-items: center;
626
+ gap: 12px;
627
+ margin-bottom: 8px;
628
+ }
629
+ .tree-search {
630
+ display: inline-flex;
631
+ align-items: center;
632
+ gap: 6px;
633
+ background: var(--code-bg);
634
+ border: 1px solid var(--border);
635
+ border-radius: 6px;
636
+ padding: 4px 10px;
637
+ flex: 1 1 240px;
638
+ min-width: 220px;
639
+ max-width: 380px;
640
+ }
641
+ .tree-search input {
642
+ background: transparent;
643
+ border: none;
644
+ color: var(--text);
645
+ font-size: 13px;
646
+ outline: none;
647
+ flex: 1;
648
+ min-width: 0;
649
+ }
650
+ .tree-search input::placeholder {
651
+ color: var(--muted);
652
+ opacity: 0.7;
653
+ }
654
+ .tree-filter-group {
655
+ display: inline-flex;
656
+ flex-wrap: wrap;
657
+ gap: 4px;
658
+ background: var(--code-bg);
659
+ border: 1px solid var(--border);
660
+ border-radius: 6px;
661
+ padding: 3px;
662
+ }
663
+ .tree-filter-btn {
664
+ background: transparent;
665
+ color: var(--muted);
666
+ border: 1px solid transparent;
667
+ border-radius: 4px;
668
+ padding: 4px 10px;
669
+ cursor: pointer;
670
+ font-size: 12px;
671
+ line-height: 1.4;
672
+ transition: all 120ms ease;
673
+ }
674
+ .tree-filter-btn:hover {
675
+ color: var(--text);
676
+ border-color: var(--border);
677
+ }
678
+ .tree-filter-btn.is-active {
679
+ background: var(--accent);
680
+ color: #0b1220;
681
+ font-weight: 600;
682
+ border-color: var(--accent);
683
+ }
684
+
685
+ .tree-summary {
686
+ margin-top: 8px;
687
+ font-size: 12.5px;
688
+ }
689
+ .tree-summary .stale-pill {
690
+ display: inline-block;
691
+ background: var(--warn);
692
+ color: #1a1300;
693
+ font-weight: 700;
694
+ padding: 1px 7px;
695
+ border-radius: 999px;
696
+ font-size: 10.5px;
697
+ margin-left: 4px;
698
+ letter-spacing: 0.02em;
699
+ text-transform: uppercase;
700
+ }
701
+
702
+ .tree-panel {
703
+ border-left: 3px solid var(--good);
704
+ }
705
+ .tasks-tree {
706
+ min-height: 80px;
707
+ font-size: 13px;
708
+ }
709
+ .tree-list,
710
+ .tree-children {
711
+ list-style: none;
712
+ margin: 0;
713
+ padding: 0;
714
+ }
715
+ .tree-children {
716
+ margin-left: 0;
717
+ }
718
+
719
+ .tree-item {
720
+ position: relative;
721
+ margin: 2px 0;
722
+ }
723
+ .tree-item.tree-item-hidden {
724
+ display: none;
725
+ }
726
+ .tree-row {
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 8px;
730
+ padding: 6px 8px 6px 8px;
731
+ border-radius: 6px;
732
+ background: rgba(255, 255, 255, 0.015);
733
+ border: 1px solid transparent;
734
+ transition: background 120ms ease, border-color 120ms ease;
735
+ }
736
+ .tree-row:hover {
737
+ background: rgba(96, 165, 250, 0.06);
738
+ border-color: rgba(96, 165, 250, 0.25);
739
+ }
740
+ .tree-row-meta {
741
+ margin-top: 4px;
742
+ font-size: 11.5px;
743
+ color: var(--muted);
744
+ font-style: italic;
745
+ }
746
+
747
+ .tree-toggler {
748
+ display: inline-flex;
749
+ align-items: center;
750
+ justify-content: center;
751
+ width: 22px;
752
+ height: 22px;
753
+ background: transparent;
754
+ color: var(--accent);
755
+ border: 1px solid var(--border);
756
+ border-radius: 4px;
757
+ cursor: pointer;
758
+ font-size: 11px;
759
+ padding: 0;
760
+ flex-shrink: 0;
761
+ transition: background 120ms ease;
762
+ }
763
+ .tree-toggler:hover {
764
+ background: rgba(96, 165, 250, 0.12);
765
+ }
766
+ .tree-toggler-leaf {
767
+ border-color: transparent;
768
+ color: var(--muted);
769
+ cursor: default;
770
+ }
771
+
772
+ .tree-id {
773
+ font-family: "JetBrains Mono", Menlo, Monaco, monospace;
774
+ font-size: 12.5px;
775
+ font-weight: 600;
776
+ color: var(--accent);
777
+ flex-shrink: 0;
778
+ min-width: 52px;
779
+ }
780
+ .tree-id:hover {
781
+ text-decoration: underline;
782
+ }
783
+
784
+ .tree-type-badge {
785
+ display: inline-block;
786
+ font-size: 10.5px;
787
+ font-family: "JetBrains Mono", Menlo, Monaco, monospace;
788
+ text-transform: uppercase;
789
+ color: var(--muted);
790
+ border: 1px solid var(--border);
791
+ border-radius: 4px;
792
+ padding: 1px 5px;
793
+ flex-shrink: 0;
794
+ letter-spacing: 0.04em;
795
+ }
796
+
797
+ .tree-title {
798
+ color: var(--text);
799
+ flex: 1 1 auto;
800
+ min-width: 0;
801
+ overflow: hidden;
802
+ text-overflow: ellipsis;
803
+ white-space: nowrap;
804
+ }
805
+ .tree-title:hover {
806
+ text-decoration: underline;
807
+ color: var(--accent);
808
+ }
809
+
810
+ .tree-tags {
811
+ display: inline-flex;
812
+ flex-wrap: wrap;
813
+ gap: 3px;
814
+ flex-shrink: 0;
815
+ }
816
+ .tree-tag {
817
+ display: inline-block;
818
+ font-size: 10.5px;
819
+ background: rgba(96, 165, 250, 0.12);
820
+ color: #bfdbfe;
821
+ border: 1px solid rgba(96, 165, 250, 0.3);
822
+ border-radius: 999px;
823
+ padding: 1px 7px;
824
+ white-space: nowrap;
825
+ }
826
+
827
+ .tree-progress {
828
+ display: inline-flex;
829
+ align-items: center;
830
+ gap: 6px;
831
+ flex-shrink: 0;
832
+ background: var(--code-bg);
833
+ border: 1px solid var(--border);
834
+ border-radius: 999px;
835
+ padding: 2px 8px;
836
+ font-size: 10.5px;
837
+ color: var(--muted);
838
+ min-width: 80px;
839
+ position: relative;
840
+ overflow: hidden;
841
+ }
842
+ .tree-progress-bar {
843
+ position: absolute;
844
+ left: 0;
845
+ top: 0;
846
+ bottom: 0;
847
+ background: rgba(34, 197, 94, 0.22);
848
+ border-right: 1px solid rgba(34, 197, 94, 0.45);
849
+ }
850
+ .tree-progress-label {
851
+ position: relative;
852
+ z-index: 1;
853
+ font-family: "JetBrains Mono", Menlo, Monaco, monospace;
854
+ font-weight: 600;
855
+ color: var(--text);
856
+ }
857
+
858
+ .tree-updated {
859
+ font-size: 10.5px;
860
+ color: var(--muted);
861
+ font-family: "JetBrains Mono", Menlo, Monaco, monospace;
862
+ flex-shrink: 0;
863
+ white-space: nowrap;
864
+ }
865
+
866
+ /* Branch lines — drawn as left borders on each child item so the tree
867
+ stays legible even with deep nesting. Each level offsets the lines by
868
+ the same 22px we use for indentation. v0.8.3: bumped the line weight
869
+ and contrast so the hierarchy is visible without squinting. */
870
+ .tree-children .tree-item {
871
+ border-left: 2px solid rgba(148, 163, 184, 0.45);
872
+ margin-left: 11px;
873
+ padding-left: 0;
874
+ }
875
+ .tree-children .tree-item .tree-row {
876
+ border-left: 2px solid transparent;
877
+ margin-left: -2px;
878
+ padding-left: 2px;
879
+ }
880
+
881
+ /* v0.8.3: render-time indent is computed as `depth * 22` in JS, but we
882
+ also bump the depth-0 row padding so it lines up with the toggler
883
+ size — without this the leaf dots look detached from their row. */
884
+ .tree-row {
885
+ display: flex;
886
+ align-items: center;
887
+ gap: 8px;
888
+ padding: 6px 8px 6px 8px;
889
+ border-radius: 6px;
890
+ background: rgba(255, 255, 255, 0.015);
891
+ border: 1px solid transparent;
892
+ transition: background 120ms ease, border-color 120ms ease;
893
+ }
894
+
895
+ /* v0.8.3: parent rows get a subtle accent so the user can immediately
896
+ tell which rows have children — even before they click the chevron. */
897
+ .tree-item-branch > .tree-row {
898
+ background: rgba(96, 165, 250, 0.04);
899
+ border-color: rgba(96, 165, 250, 0.18);
900
+ }
901
+
902
+ /* v0.8.3: depth-based indentation for visual scanning. We tag each
903
+ tree-row with its depth so the CSS can add a coloured left rail —
904
+ darker for roots, fading toward the leaves. */
905
+ .tree-row[data-depth="0"] {
906
+ box-shadow: inset 3px 0 0 rgba(96, 165, 250, 0.55);
907
+ }
908
+ .tree-row[data-depth="1"] {
909
+ box-shadow: inset 3px 0 0 rgba(96, 165, 250, 0.30);
910
+ }
911
+ .tree-row[data-depth="2"] {
912
+ box-shadow: inset 3px 0 0 rgba(96, 165, 250, 0.18);
913
+ }
914
+ .tree-row[data-depth="3"] {
915
+ box-shadow: inset 3px 0 0 rgba(148, 163, 184, 0.30);
916
+ }
917
+ .tree-row[data-depth="4"],
918
+ .tree-row[data-depth="5"],
919
+ .tree-row[data-depth="6"] {
920
+ box-shadow: inset 3px 0 0 rgba(148, 163, 184, 0.20);
921
+ }
922
+
923
+ /* v0.8.3: per-depth depth pill in the summary line. */
924
+ .tree-depth-breakdown {
925
+ display: inline-flex;
926
+ flex-wrap: wrap;
927
+ gap: 4px;
928
+ margin-left: 4px;
929
+ }
930
+ .tree-depth-pill {
931
+ display: inline-block;
932
+ padding: 1px 7px;
933
+ border-radius: 999px;
934
+ font-size: 11px;
935
+ border: 1px solid rgba(148, 163, 184, 0.35);
936
+ background: rgba(96, 165, 250, 0.08);
937
+ color: #cbd5e1;
938
+ }
939
+ .tree-depth-pill[data-depth="0"] {
940
+ background: rgba(96, 165, 250, 0.18);
941
+ color: #bfdbfe;
942
+ border-color: rgba(96, 165, 250, 0.55);
943
+ }
944
+ .tree-depth-pill[data-depth="1"] {
945
+ background: rgba(96, 165, 250, 0.10);
946
+ color: #cbd5e1;
947
+ border-color: rgba(96, 165, 250, 0.30);
948
+ }
949
+ .tree-depth-pill[data-depth="2"] {
950
+ background: rgba(96, 165, 250, 0.06);
951
+ color: #cbd5e1;
952
+ border-color: rgba(96, 165, 250, 0.22);
953
+ }
954
+
955
+ /* v0.8.3: legacy sections stay hidden. The id="legacy-sessions" hook
956
+ lets a future debug toggle restore them without code changes. */
957
+ .tree-legacy-hidden,
958
+ .tree-legacy-hidden[hidden] {
959
+ display: none !important;
960
+ }
961
+
962
+ /* v0.8.3: condensed legend so it doesn't dominate the page above the
963
+ fold. Keeps the same content, tighter padding. */
964
+ .tree-legend {
965
+ padding-top: 8px;
966
+ padding-bottom: 8px;
967
+ font-size: 12.5px;
968
+ }
969
+ .tree-legend h2 {
970
+ font-size: 13px;
971
+ margin-bottom: 4px;
972
+ color: var(--muted);
973
+ text-transform: uppercase;
974
+ letter-spacing: 0.04em;
975
+ }
976
+ .tree-legend ul {
977
+ margin: 0;
978
+ padding-left: 18px;
979
+ display: grid;
980
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
981
+ gap: 2px 16px;
982
+ }
983
+ .tree-legend li {
984
+ margin: 0;
985
+ }
986
+
987
+ /* Status badge color tweaks — keep the existing palette, just make the
988
+ ones we use most in the tree (active / completed / todo) more vivid. */
989
+ .badge.badge-active {
990
+ background: rgba(59, 130, 246, 0.18);
991
+ color: #93c5fd;
992
+ border: 1px solid rgba(59, 130, 246, 0.5);
993
+ }
994
+ .badge.badge-completed {
995
+ background: rgba(34, 197, 94, 0.18);
996
+ color: #86efac;
997
+ border: 1px solid rgba(34, 197, 94, 0.5);
998
+ }
999
+ .badge.badge-todo {
1000
+ background: rgba(148, 163, 184, 0.18);
1001
+ color: #cbd5e1;
1002
+ border: 1px solid rgba(148, 163, 184, 0.4);
1003
+ }
1004
+ .badge.badge-paused {
1005
+ background: rgba(245, 158, 11, 0.18);
1006
+ color: #fcd34d;
1007
+ border: 1px solid rgba(245, 158, 11, 0.45);
1008
+ }
1009
+ .badge.badge-cancelled {
1010
+ background: rgba(239, 68, 68, 0.18);
1011
+ color: #fca5a5;
1012
+ border: 1px solid rgba(239, 68, 68, 0.45);
1013
+ }
1014
+ .badge.badge-priority-high {
1015
+ background: rgba(239, 68, 68, 0.18);
1016
+ color: #fca5a5;
1017
+ border: 1px solid rgba(239, 68, 68, 0.45);
1018
+ }
1019
+ .badge.badge-priority-medium {
1020
+ background: rgba(245, 158, 11, 0.18);
1021
+ color: #fcd34d;
1022
+ border: 1px solid rgba(245, 158, 11, 0.45);
1023
+ }
1024
+ .badge.badge-priority-low {
1025
+ background: rgba(148, 163, 184, 0.18);
1026
+ color: #cbd5e1;
1027
+ border: 1px solid rgba(148, 163, 184, 0.4);
1028
+ }
1029
+
@@ -205,6 +205,16 @@
205
205
  var merged = mergeNodes(self.lastOps, r.ops);
206
206
  self.lastOps = merged;
207
207
  swapIn(renderPipelineHtml(merged, r.stale, self.taskId));
208
+ // v0.8.0+: cache the latest operations for the Mermaid
209
+ // re-renderer. Once operations are loaded, `app.js` will pick
210
+ // them up via `window.__lifecycleOps` and rebuild the Mermaid
211
+ // source with the per-operation subgraphs + click callbacks.
212
+ window.__lifecycleOps = {
213
+ taskId: self.taskId,
214
+ operations: merged,
215
+ fetchedAt: Date.now(),
216
+ };
217
+ notifyMermaidRebuild();
208
218
  if (r.terminal) {
209
219
  self.stopped = true;
210
220
  return;
@@ -225,6 +235,23 @@
225
235
  });
226
236
  };
227
237
 
238
+ /**
239
+ * Fire a CustomEvent the app.js Mermaid re-renderer listens to.
240
+ * We don't import a global helper to keep this file standalone.
241
+ */
242
+ function notifyMermaidRebuild() {
243
+ try {
244
+ var ev = new CustomEvent("task-show:lifecycle-ops-loaded", {
245
+ detail: { taskId: window.__lifecycleOps && window.__lifecycleOps.taskId },
246
+ });
247
+ window.dispatchEvent(ev);
248
+ } catch (_) {
249
+ // CustomEvent may not exist on ancient browsers — that's fine,
250
+ // app.js simply won't get the hint and the next SSE update will
251
+ // pick the ops up via window.__lifecycleOps instead.
252
+ }
253
+ }
254
+
228
255
  PipelineController.prototype.scheduleNext = function (failed) {
229
256
  var self = this;
230
257
  if (this.timer) clearTimeout(this.timer);