dominds 0.6.2 → 0.6.4

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 (149) hide show
  1. package/dist/access-control.js +2 -2
  2. package/dist/agent-priming.js +826 -92
  3. package/dist/cli/read.js +406 -12
  4. package/dist/dialog.js +4 -0
  5. package/dist/docs/design.md +1 -0
  6. package/dist/docs/design.zh.md +1 -0
  7. package/dist/docs/dialog-system.md +12 -7
  8. package/dist/docs/dialog-system.zh.md +7 -3
  9. package/dist/docs/dominds-agent-priming.md +10 -1
  10. package/dist/docs/dominds-agent-priming.zh.md +9 -1
  11. package/dist/docs/dominds-terminology.md +8 -8
  12. package/dist/docs/fbr-implementation.md +77 -0
  13. package/dist/docs/fbr-implementation.zh.md +77 -0
  14. package/dist/docs/fbr.md +142 -141
  15. package/dist/docs/fbr.zh.md +129 -123
  16. package/dist/docs/keep-going.zh.md +162 -0
  17. package/dist/docs/showing-by-doing.md +208 -0
  18. package/dist/docs/showing-by-doing.zh.md +177 -0
  19. package/dist/docs/tellask-collab.md +250 -0
  20. package/dist/docs/tellask-collab.zh.md +254 -0
  21. package/dist/docs/txt-editing-tools.md +2 -2
  22. package/dist/docs/txt-editing-tools.zh.md +2 -2
  23. package/dist/llm/defaults.yaml +82 -4
  24. package/dist/llm/driver.js +280 -104
  25. package/dist/llm/gen/codex.js +49 -2
  26. package/dist/log.js +385 -30
  27. package/dist/mcp/supervisor.js +113 -40
  28. package/dist/minds/builtin/pangu/persona.zh.md +2 -2
  29. package/dist/minds/load.js +49 -284
  30. package/dist/minds/minds-i18n.js +2 -2
  31. package/dist/minds/promptdocs.js +263 -0
  32. package/dist/minds/system-prompt-parts.js +231 -0
  33. package/dist/minds/system-prompt.js +190 -223
  34. package/dist/persistence.js +66 -1
  35. package/dist/server/websocket-handler.js +14 -0
  36. package/dist/shared/diligence.js +40 -6
  37. package/dist/shared/utils/inter-dialog-format.js +3 -5
  38. package/dist/showing-by-doing.js +34 -31
  39. package/dist/snippets/README.en.md +3 -0
  40. package/dist/static/assets/{_baseUniq-C9vbtHF9.js → _baseUniq-C7IpU2Uk.js} +2 -2
  41. package/dist/static/assets/{_baseUniq-C9vbtHF9.js.map → _baseUniq-C7IpU2Uk.js.map} +1 -1
  42. package/dist/static/assets/{arc-hulXG01i.js → arc-1bhQqjON.js} +2 -2
  43. package/dist/static/assets/{arc-hulXG01i.js.map → arc-1bhQqjON.js.map} +1 -1
  44. package/dist/static/assets/{architectureDiagram-VXUJARFQ-DdLIAMT5.js → architectureDiagram-VXUJARFQ-CkEi1QpB.js} +6 -6
  45. package/dist/static/assets/{architectureDiagram-VXUJARFQ-DdLIAMT5.js.map → architectureDiagram-VXUJARFQ-CkEi1QpB.js.map} +1 -1
  46. package/dist/static/assets/{blockDiagram-VD42YOAC-DACsx66C.js → blockDiagram-VD42YOAC-DaBQ5-pY.js} +7 -7
  47. package/dist/static/assets/{blockDiagram-VD42YOAC-DACsx66C.js.map → blockDiagram-VD42YOAC-DaBQ5-pY.js.map} +1 -1
  48. package/dist/static/assets/{c4Diagram-YG6GDRKO-Cd5xZlLy.js → c4Diagram-YG6GDRKO-ChUgpgkP.js} +3 -3
  49. package/dist/static/assets/{c4Diagram-YG6GDRKO-Cd5xZlLy.js.map → c4Diagram-YG6GDRKO-ChUgpgkP.js.map} +1 -1
  50. package/dist/static/assets/{channel-NQehis0Z.js → channel-CxvmwllM.js} +2 -2
  51. package/dist/static/assets/{channel-NQehis0Z.js.map → channel-CxvmwllM.js.map} +1 -1
  52. package/dist/static/assets/{chunk-4BX2VUAB-DZDPl76b.js → chunk-4BX2VUAB-CKsrU2yk.js} +2 -2
  53. package/dist/static/assets/{chunk-4BX2VUAB-DZDPl76b.js.map → chunk-4BX2VUAB-CKsrU2yk.js.map} +1 -1
  54. package/dist/static/assets/{chunk-55IACEB6-CFSRDUbl.js → chunk-55IACEB6-BAau9SFt.js} +2 -2
  55. package/dist/static/assets/{chunk-55IACEB6-CFSRDUbl.js.map → chunk-55IACEB6-BAau9SFt.js.map} +1 -1
  56. package/dist/static/assets/{chunk-B4BG7PRW-BqQQ9M_z.js → chunk-B4BG7PRW--IiJ7W1m.js} +5 -5
  57. package/dist/static/assets/{chunk-B4BG7PRW-BqQQ9M_z.js.map → chunk-B4BG7PRW--IiJ7W1m.js.map} +1 -1
  58. package/dist/static/assets/{chunk-DI55MBZ5-FiFzz1Gh.js → chunk-DI55MBZ5-B83KrPQj.js} +4 -4
  59. package/dist/static/assets/{chunk-DI55MBZ5-FiFzz1Gh.js.map → chunk-DI55MBZ5-B83KrPQj.js.map} +1 -1
  60. package/dist/static/assets/{chunk-FMBD7UC4-DqqtCyWK.js → chunk-FMBD7UC4-BlDXzeza.js} +2 -2
  61. package/dist/static/assets/{chunk-FMBD7UC4-DqqtCyWK.js.map → chunk-FMBD7UC4-BlDXzeza.js.map} +1 -1
  62. package/dist/static/assets/{chunk-QN33PNHL-F0laQQ-J.js → chunk-QN33PNHL-B596W_v7.js} +2 -2
  63. package/dist/static/assets/{chunk-QN33PNHL-F0laQQ-J.js.map → chunk-QN33PNHL-B596W_v7.js.map} +1 -1
  64. package/dist/static/assets/{chunk-QZHKN3VN-CWhEZPaV.js → chunk-QZHKN3VN-UBBCxgBb.js} +2 -2
  65. package/dist/static/assets/{chunk-QZHKN3VN-CWhEZPaV.js.map → chunk-QZHKN3VN-UBBCxgBb.js.map} +1 -1
  66. package/dist/static/assets/{chunk-TZMSLE5B-Dx9cnwUy.js → chunk-TZMSLE5B-D-wCX2wJ.js} +2 -2
  67. package/dist/static/assets/{chunk-TZMSLE5B-Dx9cnwUy.js.map → chunk-TZMSLE5B-D-wCX2wJ.js.map} +1 -1
  68. package/dist/static/assets/{classDiagram-2ON5EDUG-Dp-dyEGy.js → classDiagram-2ON5EDUG-DvtmzPcu.js} +6 -6
  69. package/dist/static/assets/{classDiagram-2ON5EDUG-Dp-dyEGy.js.map → classDiagram-2ON5EDUG-DvtmzPcu.js.map} +1 -1
  70. package/dist/static/assets/{classDiagram-v2-WZHVMYZB-Dp-dyEGy.js → classDiagram-v2-WZHVMYZB-DvtmzPcu.js} +6 -6
  71. package/dist/static/assets/{classDiagram-v2-WZHVMYZB-Dp-dyEGy.js.map → classDiagram-v2-WZHVMYZB-DvtmzPcu.js.map} +1 -1
  72. package/dist/static/assets/{clone-C6mKvxs5.js → clone-DgJ0ZR-k.js} +2 -2
  73. package/dist/static/assets/{clone-C6mKvxs5.js.map → clone-DgJ0ZR-k.js.map} +1 -1
  74. package/dist/static/assets/{cose-bilkent-S5V4N54A-Dbwh3GoX.js → cose-bilkent-S5V4N54A-DXMyFQvy.js} +2 -2
  75. package/dist/static/assets/{cose-bilkent-S5V4N54A-Dbwh3GoX.js.map → cose-bilkent-S5V4N54A-DXMyFQvy.js.map} +1 -1
  76. package/dist/static/assets/{dagre-6UL2VRFP-BD_6e0Uk.js → dagre-6UL2VRFP-BdaUG-j_.js} +7 -7
  77. package/dist/static/assets/{dagre-6UL2VRFP-BD_6e0Uk.js.map → dagre-6UL2VRFP-BdaUG-j_.js.map} +1 -1
  78. package/dist/static/assets/{diagram-PSM6KHXK-BWt7Q59-.js → diagram-PSM6KHXK-NLiqKBzn.js} +7 -7
  79. package/dist/static/assets/{diagram-PSM6KHXK-BWt7Q59-.js.map → diagram-PSM6KHXK-NLiqKBzn.js.map} +1 -1
  80. package/dist/static/assets/{diagram-QEK2KX5R-D0BvBR_a.js → diagram-QEK2KX5R-D-0fyvY_.js} +6 -6
  81. package/dist/static/assets/{diagram-QEK2KX5R-D0BvBR_a.js.map → diagram-QEK2KX5R-D-0fyvY_.js.map} +1 -1
  82. package/dist/static/assets/{diagram-S2PKOQOG-D8uRdKXp.js → diagram-S2PKOQOG-BQ_FU59m.js} +6 -6
  83. package/dist/static/assets/{diagram-S2PKOQOG-D8uRdKXp.js.map → diagram-S2PKOQOG-BQ_FU59m.js.map} +1 -1
  84. package/dist/static/assets/{erDiagram-Q2GNP2WA-CQoifjFq.js → erDiagram-Q2GNP2WA-DyftKeuC.js} +5 -5
  85. package/dist/static/assets/{erDiagram-Q2GNP2WA-CQoifjFq.js.map → erDiagram-Q2GNP2WA-DyftKeuC.js.map} +1 -1
  86. package/dist/static/assets/{flowDiagram-NV44I4VS-CGhdeaG8.js → flowDiagram-NV44I4VS-9SGefONA.js} +6 -6
  87. package/dist/static/assets/{flowDiagram-NV44I4VS-CGhdeaG8.js.map → flowDiagram-NV44I4VS-9SGefONA.js.map} +1 -1
  88. package/dist/static/assets/{ganttDiagram-JELNMOA3-D8W0wb9H.js → ganttDiagram-JELNMOA3-k_WLhf-r.js} +3 -3
  89. package/dist/static/assets/{ganttDiagram-JELNMOA3-D8W0wb9H.js.map → ganttDiagram-JELNMOA3-k_WLhf-r.js.map} +1 -1
  90. package/dist/static/assets/{gitGraphDiagram-NY62KEGX-ChHni_jP.js → gitGraphDiagram-NY62KEGX-3eoLlCOY.js} +7 -7
  91. package/dist/static/assets/{gitGraphDiagram-NY62KEGX-ChHni_jP.js.map → gitGraphDiagram-NY62KEGX-3eoLlCOY.js.map} +1 -1
  92. package/dist/static/assets/{graph-BWoi_FgC.js → graph-vUevIs4s.js} +3 -3
  93. package/dist/static/assets/{graph-BWoi_FgC.js.map → graph-vUevIs4s.js.map} +1 -1
  94. package/dist/static/assets/{index-th_praGg.js → index-BNBG2CE1.js} +399 -68
  95. package/dist/static/assets/index-BNBG2CE1.js.map +1 -0
  96. package/dist/static/assets/{infoDiagram-WHAUD3N6-B_XKKZTV.js → infoDiagram-WHAUD3N6-CwEhVxkU.js} +5 -5
  97. package/dist/static/assets/{infoDiagram-WHAUD3N6-B_XKKZTV.js.map → infoDiagram-WHAUD3N6-CwEhVxkU.js.map} +1 -1
  98. package/dist/static/assets/{journeyDiagram-XKPGCS4Q-ChGuQ6T9.js → journeyDiagram-XKPGCS4Q-Dtdq4G4Q.js} +5 -5
  99. package/dist/static/assets/{journeyDiagram-XKPGCS4Q-ChGuQ6T9.js.map → journeyDiagram-XKPGCS4Q-Dtdq4G4Q.js.map} +1 -1
  100. package/dist/static/assets/{kanban-definition-3W4ZIXB7-BjWe623u.js → kanban-definition-3W4ZIXB7-Bli-AycJ.js} +3 -3
  101. package/dist/static/assets/{kanban-definition-3W4ZIXB7-BjWe623u.js.map → kanban-definition-3W4ZIXB7-Bli-AycJ.js.map} +1 -1
  102. package/dist/static/assets/{layout-BPyT310w.js → layout-CGlA8c09.js} +5 -5
  103. package/dist/static/assets/{layout-BPyT310w.js.map → layout-CGlA8c09.js.map} +1 -1
  104. package/dist/static/assets/{linear-xUsVjXWq.js → linear-Da2jDWL3.js} +2 -2
  105. package/dist/static/assets/{linear-xUsVjXWq.js.map → linear-Da2jDWL3.js.map} +1 -1
  106. package/dist/static/assets/{min-xFt7zeOd.js → min-Co741hTV.js} +3 -3
  107. package/dist/static/assets/{min-xFt7zeOd.js.map → min-Co741hTV.js.map} +1 -1
  108. package/dist/static/assets/{mindmap-definition-VGOIOE7T-DT_dvf2c.js → mindmap-definition-VGOIOE7T-DvkIjoq8.js} +4 -4
  109. package/dist/static/assets/{mindmap-definition-VGOIOE7T-DT_dvf2c.js.map → mindmap-definition-VGOIOE7T-DvkIjoq8.js.map} +1 -1
  110. package/dist/static/assets/{pieDiagram-ADFJNKIX-B1DQ-OaG.js → pieDiagram-ADFJNKIX-BGuGhTu8.js} +7 -7
  111. package/dist/static/assets/{pieDiagram-ADFJNKIX-B1DQ-OaG.js.map → pieDiagram-ADFJNKIX-BGuGhTu8.js.map} +1 -1
  112. package/dist/static/assets/{quadrantDiagram-AYHSOK5B-IHqyr3iT.js → quadrantDiagram-AYHSOK5B-DAZcrJMg.js} +3 -3
  113. package/dist/static/assets/{quadrantDiagram-AYHSOK5B-IHqyr3iT.js.map → quadrantDiagram-AYHSOK5B-DAZcrJMg.js.map} +1 -1
  114. package/dist/static/assets/{requirementDiagram-UZGBJVZJ-CKBpht7B.js → requirementDiagram-UZGBJVZJ-CXN0DxZs.js} +4 -4
  115. package/dist/static/assets/{requirementDiagram-UZGBJVZJ-CKBpht7B.js.map → requirementDiagram-UZGBJVZJ-CXN0DxZs.js.map} +1 -1
  116. package/dist/static/assets/{sankeyDiagram-TZEHDZUN-D2uGjv3i.js → sankeyDiagram-TZEHDZUN-B7-yAePZ.js} +2 -2
  117. package/dist/static/assets/{sankeyDiagram-TZEHDZUN-D2uGjv3i.js.map → sankeyDiagram-TZEHDZUN-B7-yAePZ.js.map} +1 -1
  118. package/dist/static/assets/{sequenceDiagram-WL72ISMW-wLFRhAKd.js → sequenceDiagram-WL72ISMW-DfBNY6h_.js} +4 -4
  119. package/dist/static/assets/{sequenceDiagram-WL72ISMW-wLFRhAKd.js.map → sequenceDiagram-WL72ISMW-DfBNY6h_.js.map} +1 -1
  120. package/dist/static/assets/{stateDiagram-FKZM4ZOC-BFGQTbx5.js → stateDiagram-FKZM4ZOC-BLo1xRVY.js} +9 -9
  121. package/dist/static/assets/{stateDiagram-FKZM4ZOC-BFGQTbx5.js.map → stateDiagram-FKZM4ZOC-BLo1xRVY.js.map} +1 -1
  122. package/dist/static/assets/{stateDiagram-v2-4FDKWEC3-DF7AjJuk.js → stateDiagram-v2-4FDKWEC3-Dq7MAD0I.js} +5 -5
  123. package/dist/static/assets/{stateDiagram-v2-4FDKWEC3-DF7AjJuk.js.map → stateDiagram-v2-4FDKWEC3-Dq7MAD0I.js.map} +1 -1
  124. package/dist/static/assets/{timeline-definition-IT6M3QCI-ChHFOb0o.js → timeline-definition-IT6M3QCI-ySWyBF3b.js} +3 -3
  125. package/dist/static/assets/{timeline-definition-IT6M3QCI-ChHFOb0o.js.map → timeline-definition-IT6M3QCI-ySWyBF3b.js.map} +1 -1
  126. package/dist/static/assets/{treemap-KMMF4GRG-BxaNvQU4.js → treemap-KMMF4GRG-DOp4sqOh.js} +4 -4
  127. package/dist/static/assets/{treemap-KMMF4GRG-BxaNvQU4.js.map → treemap-KMMF4GRG-DOp4sqOh.js.map} +1 -1
  128. package/dist/static/assets/{xychartDiagram-PRI3JC2R-CrNKeY_-.js → xychartDiagram-PRI3JC2R-vkmh67qb.js} +3 -3
  129. package/dist/static/assets/{xychartDiagram-PRI3JC2R-CrNKeY_-.js.map → xychartDiagram-PRI3JC2R-vkmh67qb.js.map} +1 -1
  130. package/dist/static/index.html +1 -1
  131. package/dist/team.js +29 -6
  132. package/dist/tool.js +56 -0
  133. package/dist/tools/builtins.js +4 -2
  134. package/dist/tools/context-health.js +7 -7
  135. package/dist/tools/os.js +267 -30
  136. package/dist/tools/pending-tellask-reminder.js +185 -0
  137. package/dist/tools/plan.js +1 -0
  138. package/dist/tools/ripgrep.js +145 -4
  139. package/dist/tools/shell-tools.js +21 -0
  140. package/dist/tools/team-mgmt.js +4 -4
  141. package/dist/tools/toolset-manual.js +74 -0
  142. package/dist/utils/task-doc.js +16 -16
  143. package/package.json +1 -1
  144. package/dist/minds/builtin/cmdr/persona.md +0 -3
  145. package/dist/minds/builtin/dijiang/knowledge.md +0 -287
  146. package/dist/minds/builtin/dijiang/persona.md +0 -7
  147. package/dist/static/assets/index-th_praGg.js.map +0 -1
  148. package/dist/static/testing/dom-observation-utils.js +0 -425
  149. package/dist/static/testing/e2e-test-helper.js +0 -3119
@@ -1,3119 +0,0 @@
1
- /**
2
- * E2E Test Helper - DEFINITIVE IMPLEMENTATIONS
3
- * Source: dominds-app.tsx, running-dialog-list.ts, dominds-dialog-container.ts, dominds-q4h-input.ts
4
- *
5
- * PRINCIPLE: Use component public methods where available, otherwise use exact ID-based selectors.
6
- */
7
-
8
- // ============================================
9
- // SELECTORS - DEFINITIVE
10
- // ============================================
11
-
12
- const sel = {
13
- // App root
14
- app: 'dominds-app',
15
-
16
- // Q4H Input component (dominds-q4h-input.ts)
17
- q4hInputHost: 'dominds-q4h-input',
18
- textarea: '.message-input',
19
- sendBtn: '.send-button',
20
-
21
- // Dialog container (dominds-dialog-container.ts)
22
- dialogHost: '#dialog-container',
23
- userMsg: '.generation-bubble[data-user-msg-id]',
24
- genBubble: '.generation-bubble',
25
- genCompleted: '.generation-bubble.completed',
26
- genNotCompleted: '.generation-bubble:not(.completed)',
27
- teammateBubble: '.message.teammate',
28
- teammateContent: '.teammate-content',
29
- teammateLabel: '.teammate-label',
30
- teammateHeadline: '.teammate-headline',
31
- teammateDivider: '.teammate-response-divider',
32
- teammateIndicator: '.response-indicator',
33
- markdownSection: 'dominds-markdown-section',
34
- markdownContent: '.markdown-content',
35
- author: '.bubble-author, .author',
36
- thinkingCompleted: '.thinking-section.completed',
37
- markdownCompleted: '.markdown-section.completed',
38
- markdownContent: '.markdown-content',
39
- codeSection: '.codeblock-section',
40
- codeCompleted: '.codeblock-section.completed',
41
- codeTitle: '.codeblock-title',
42
- codeContent: '.codeblock-content',
43
-
44
- // Dialog creation (dominds-app.tsx)
45
- newDialogBtn: '#new-dialog-btn',
46
- teammateSelect: '#teammate-select',
47
- taskDocInput: '#task-doc-input',
48
- createBtn: '#create-dialog-btn',
49
- dialogModal: '.create-dialog-modal',
50
-
51
- // Dialog list (running-dialog-list.ts)
52
- sidebar: '.sidebar',
53
- dialogList: 'running-dialog-list',
54
- dialogListContainer: '#dialog-list',
55
- dialogItem: '.dialog-item',
56
-
57
- // Reminders widget (dominds-app.tsx)
58
- remindersToggle: '#toolbar-reminders-toggle',
59
- remindersWidget: '#reminders-widget',
60
- remindersContent: '#reminders-widget-content',
61
- remindersClose: '#reminders-widget-close',
62
-
63
- // Q4H panel (dominds-app.tsx)
64
- q4hPanel: '.q4h-panel-container',
65
- q4hToggleBar: '.q4h-toggle-bar',
66
- q4hResizeHandle: '.q4h-resize-handle',
67
- q4hContent: '.q4h-content',
68
- q4hBadge: '.q4h-badge',
69
- q4hPanelHost: 'dominds-q4h-panel',
70
- q4hGoToSiteBtn: '.q4h-goto-site-btn',
71
- };
72
-
73
- // ============================================
74
- // Load DOM Observation Utilities (REQUIRED)
75
- // ============================================
76
-
77
- let domObs = null;
78
- if (typeof window !== 'undefined' && typeof window.__domObservation__ === 'object') {
79
- domObs = window.__domObservation__;
80
- } else {
81
- throw new Error('dom-observation-utils.js must be loaded before e2e-test-helper.js');
82
- }
83
-
84
- // ============================================
85
- // Console Error Tracking
86
- // ============================================
87
-
88
- let __consoleErrors__ = [];
89
-
90
- // Known non-critical protocol error patterns to ignore
91
- const IGNORED_ERROR_PATTERNS = [
92
- // Teammate-call (tellask) events (renamed from call_* to teammate_call_*)
93
- 'teammate_call_headline_chunk_evt',
94
- 'teammate_call_body_start_evt',
95
- 'teammate_call_finish_evt',
96
- 'teammate_call_start_evt',
97
- 'teammate_call_finish_evt',
98
- ];
99
-
100
- // Console error interceptor
101
- (function () {
102
- const originalError = console.error.bind(console);
103
- console.error = function (...args) {
104
- __consoleErrors__.push({
105
- timestamp: Date.now(),
106
- message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
107
- });
108
- originalError.apply(console, args);
109
- };
110
- })();
111
-
112
- /**
113
- * Checks for console errors and optionally clears them.
114
- * @param {Object} options - Options object
115
- * @param {boolean} [options.clear=true] - Whether to clear errors after checking
116
- * @param {number} [options.threshold=0] - Maximum allowed errors before throwing
117
- * @returns {Array<{timestamp: number, message: string}>} The collected errors
118
- * @throws {Error} If error count exceeds threshold
119
- */
120
- function checkConsoleErrors(options = {}) {
121
- const { clear = true, threshold = 0 } = options;
122
-
123
- // Filter out known non-critical protocol errors
124
- const filteredErrors = __consoleErrors__.filter((error) => {
125
- const message = error.message;
126
- return !IGNORED_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
127
- });
128
-
129
- const errors = [...filteredErrors];
130
- if (clear) __consoleErrors__ = [];
131
- if (errors.length > threshold) {
132
- throw new Error(
133
- `Console errors detected (${errors.length}):\n` +
134
- errors.map((e) => `[${new Date(e.timestamp).toISOString()}] ${e.message}`).join('\n'),
135
- );
136
- }
137
- return errors;
138
- }
139
-
140
- // ============================================
141
- // Shadow DOM Accessors
142
- // ============================================
143
-
144
- function getAppShadow() {
145
- const app = document.querySelector(sel.app);
146
- return app && app.shadowRoot ? app.shadowRoot : null;
147
- }
148
-
149
- function getApp() {
150
- return document.querySelector(sel.app);
151
- }
152
-
153
- function getInputArea() {
154
- return document.querySelector('dominds-app')?.shadowRoot?.querySelector('dominds-q4h-input');
155
- }
156
-
157
- /**
158
- * Waits for a dialog to be selected and ready for input.
159
- * The input is NOT usable when no dialog is selected (common E2E state).
160
- * MUST be called before every fillAndSend() to prevent failures.
161
- * @param {number} [maxRetries=15] - Maximum retry attempts
162
- * @param {number} [delayMs=300] - Delay between retries in milliseconds
163
- * @returns {Promise<boolean>} True when input is ready
164
- * @throws {Error} If input is not ready after max retries
165
- */
166
- async function waitForInputEnabled(maxRetries = 15, delayMs = 300) {
167
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
168
- const app = getApp();
169
- if (!app) {
170
- await new Promise((resolve) => setTimeout(resolve, delayMs));
171
- continue;
172
- }
173
-
174
- const inputArea = getInputArea();
175
- if (!inputArea || !inputArea.shadowRoot) {
176
- await new Promise((resolve) => setTimeout(resolve, delayMs));
177
- continue;
178
- }
179
-
180
- const textarea = inputArea.shadowRoot.querySelector('.message-input');
181
- if (!textarea) {
182
- await new Promise((resolve) => setTimeout(resolve, delayMs));
183
- continue;
184
- }
185
-
186
- // Check if input is usable - textarea must be visible and interactive
187
- const isVisible = textarea.offsetParent !== null;
188
- const hasValue = textarea.value !== undefined;
189
- const isEditable = !textarea.disabled && !textarea.readOnly;
190
-
191
- // Also verify a dialog is selected (check app state)
192
- const hasCurrentDialog = app.getCurrentDialogInfo?.() !== null;
193
-
194
- if (isVisible && hasValue && isEditable && hasCurrentDialog) {
195
- return true;
196
- }
197
-
198
- if (attempt < maxRetries) {
199
- await new Promise((resolve) => setTimeout(resolve, delayMs));
200
- }
201
- }
202
- throw new Error(
203
- `Input not ready after ${maxRetries} attempts - no dialog selected or dialog not loaded`,
204
- );
205
- }
206
-
207
- function getDialogContainer() {
208
- return document.querySelector('dominds-app')?.shadowRoot?.querySelector('#dialog-container');
209
- }
210
-
211
- function getDialogList() {
212
- return document.querySelector('dominds-app')?.shadowRoot?.querySelector('running-dialog-list');
213
- }
214
-
215
- function getDialogListShadow() {
216
- const dialogList = getDialogList();
217
- return dialogList && dialogList.shadowRoot ? dialogList.shadowRoot : null;
218
- }
219
-
220
- function getConversationScrollArea() {
221
- const shadow = getAppShadow();
222
- if (!shadow) return null;
223
- return shadow.querySelector('.conversation-scroll-area');
224
- }
225
-
226
- function isScrollAtBottom(container, thresholdPx = 32) {
227
- if (!container) return false;
228
- const remaining = container.scrollHeight - container.scrollTop - container.clientHeight;
229
- return remaining <= thresholdPx;
230
- }
231
-
232
- function getMessageContainer() {
233
- const dialogContainer = getDialogContainer();
234
- if (!dialogContainer || !dialogContainer.shadowRoot) return null;
235
- return dialogContainer.shadowRoot.querySelector('.messages');
236
- }
237
-
238
- /**
239
- * Waits until a generating bubble exists and the conversation scroll area is at bottom.
240
- * Useful for verifying auto-scroll behavior around generation bubble insertion.
241
- */
242
- async function waitForGenBubbleAutoScroll(options = {}) {
243
- const { timeoutMs = 2000, thresholdPx = 32 } = options;
244
- const start = Date.now();
245
- while (Date.now() - start < timeoutMs) {
246
- const dialogContainer = getDialogContainer();
247
- const shadow = dialogContainer && dialogContainer.shadowRoot ? dialogContainer.shadowRoot : null;
248
- const hasGenBubble = shadow ? !!shadow.querySelector('.generation-bubble.generating') : false;
249
- const scrollArea = getConversationScrollArea();
250
- if (hasGenBubble && isScrollAtBottom(scrollArea, thresholdPx)) {
251
- return true;
252
- }
253
- await new Promise((resolve) => requestAnimationFrame(resolve));
254
- }
255
- throw new Error('Timed out waiting for generation bubble auto-scroll to bottom');
256
- }
257
-
258
- function getTeammateMessages() {
259
- const container = getMessageContainer();
260
- if (!container) return [];
261
- return Array.from(container.querySelectorAll('.message.teammate'));
262
- }
263
-
264
- function getTeammateMessageCount() {
265
- return getTeammateMessages().length;
266
- }
267
-
268
- function getTeammateResponseDetails() {
269
- return getTeammateMessages().map((node, index) => {
270
- const authorName =
271
- node.querySelector('.author-name')?.textContent?.trim() ||
272
- node.querySelector('.author')?.textContent?.trim() ||
273
- '';
274
- if (!authorName) {
275
- throw new Error('getTeammateResponseDetails: Missing teammate author name');
276
- }
277
- const responseIndicator = node.querySelector(sel.teammateIndicator)?.textContent?.trim() || '';
278
- if (!responseIndicator || !responseIndicator.includes('→')) {
279
- throw new Error(
280
- `getTeammateResponseDetails: Unexpected response indicator "${responseIndicator}"`,
281
- );
282
- }
283
- const requesterLabel = responseIndicator.split('→')[1]?.trim() || '';
284
- if (!requesterLabel) {
285
- throw new Error('getTeammateResponseDetails: Missing requester label in response indicator');
286
- }
287
- const bubbleHeadLine = node.querySelector(sel.teammateHeadline)?.textContent?.trim() || '';
288
- const callSiteId = parseCallSiteId(node.getAttribute('data-call-site-id'));
289
- const callId = node.getAttribute('data-call-id') || '';
290
- const calleeDialogId = node.getAttribute('data-callee-dialog-id') || '';
291
- const markdownSection = node.querySelector(sel.markdownSection);
292
- const markdownContent = markdownSection?.querySelector(sel.markdownContent);
293
- const rawMarkdown = markdownContent?.getAttribute('data-raw-md') || '';
294
- const renderedText = markdownContent?.innerText?.trim() || '';
295
- if (!rawMarkdown.trim()) {
296
- throw new Error('getTeammateResponseDetails: Missing response markdown content');
297
- }
298
- const rawLines = rawMarkdown.split('\n');
299
- let narrativeIndex = -1;
300
- for (let i = 0; i < rawLines.length; i += 1) {
301
- if (rawLines[i].trim() !== '') {
302
- narrativeIndex = i;
303
- break;
304
- }
305
- }
306
- if (narrativeIndex < 0) {
307
- throw new Error('getTeammateResponseDetails: Missing narrative line');
308
- }
309
- const narrativeLine = rawLines[narrativeIndex].trim();
310
- if (!narrativeLine.startsWith('Hi @') || !narrativeLine.includes('provided response')) {
311
- throw new Error(`getTeammateResponseDetails: Narrative line malformed "${narrativeLine}"`);
312
- }
313
- const originalCallMarker = 'to your original call:';
314
- const markerIndex = rawLines.findIndex((line) => line.trim() === originalCallMarker);
315
- if (markerIndex < 0) {
316
- throw new Error('getTeammateResponseDetails: Missing original call marker');
317
- }
318
- const stripQuotePrefix = (line) => (line.startsWith('> ') ? line.slice(2) : line);
319
- const responseLines = rawLines
320
- .slice(narrativeIndex + 1, markerIndex)
321
- .map((line) => line.trimEnd())
322
- .filter((line) => line.trim() !== '')
323
- .map((line) => stripQuotePrefix(line.trim()));
324
- if (responseLines.length === 0) {
325
- throw new Error('getTeammateResponseDetails: Missing response body section');
326
- }
327
- const responseBody = responseLines.join('\n');
328
- const callLines = rawLines
329
- .slice(markerIndex + 1)
330
- .map((line) => line.trimEnd())
331
- .filter((line) => line.trim() !== '')
332
- .map((line) => stripQuotePrefix(line.trim()));
333
- const callHeadLine = callLines.join('\n').trim() || bubbleHeadLine;
334
- return {
335
- index,
336
- authorName,
337
- responseIndicator,
338
- requesterLabel,
339
- bubbleHeadLine,
340
- callHeadLine,
341
- narrativeLine,
342
- responseBody,
343
- rawMarkdown,
344
- renderedText,
345
- callSiteId,
346
- callId,
347
- calleeDialogId,
348
- };
349
- });
350
- }
351
-
352
- function getLatestTeammateResponseDetails() {
353
- const all = getTeammateResponseDetails();
354
- return all.length > 0 ? all[all.length - 1] : null;
355
- }
356
-
357
- function getVisibleMessageNodes() {
358
- const container = getMessageContainer();
359
- if (!container) return [];
360
- return Array.from(container.children);
361
- }
362
-
363
- function getVisibleMessageTexts() {
364
- return getVisibleMessageNodes()
365
- .map((node) => (node.textContent || '').trim())
366
- .filter((text) => text.length > 0);
367
- }
368
-
369
- function findVisibleMessageContainingAll(needles, options = {}) {
370
- const list = Array.isArray(needles) ? needles : [needles];
371
- const caseInsensitive = options.caseInsensitive === true;
372
- const normalizedNeedles = list.map((n) =>
373
- caseInsensitive ? String(n).toLowerCase() : String(n),
374
- );
375
- const nodes = getVisibleMessageNodes();
376
- for (let i = 0; i < nodes.length; i++) {
377
- const text = (nodes[i].textContent || '').trim();
378
- if (!text) continue;
379
- const haystack = caseInsensitive ? text.toLowerCase() : text;
380
- const matchesAll = normalizedNeedles.every((needle) => haystack.includes(needle));
381
- if (matchesAll) {
382
- return { index: i, text };
383
- }
384
- }
385
- return null;
386
- }
387
-
388
- // ============================================
389
- // Utility
390
- // ============================================
391
-
392
- async function waitUntil(fn, timeoutMs = 15000, intervalMs = 100) {
393
- const start = Date.now();
394
- return new Promise((resolve, reject) => {
395
- const tick = () => {
396
- try {
397
- if (fn()) return resolve(true);
398
- } catch (err) {
399
- console.warn('Oops!', err);
400
- }
401
- if (Date.now() - start >= timeoutMs) return reject(new Error('timeout'));
402
- setTimeout(tick, intervalMs);
403
- };
404
- tick();
405
- });
406
- }
407
-
408
- function isElementVisible(el) {
409
- return !!(el && el.offsetParent !== null);
410
- }
411
-
412
- function escapeCssValue(value) {
413
- if (window.CSS && typeof window.CSS.escape === 'function') {
414
- return window.CSS.escape(String(value));
415
- }
416
- return String(value).replace(/["\\]/g, '\\$&');
417
- }
418
-
419
- // ============================================
420
- // Core Messaging Functions
421
- // ============================================
422
-
423
- /**
424
- * Sends a message via the input area component.
425
- * Source: dominds-q4h-input.ts
426
- * Component methods: setValue(), sendMessage()
427
- * @throws {Error} If input is disabled or no dialog is selected
428
- */
429
- async function fillAndSend(message) {
430
- const app = getApp();
431
- const inputArea = getInputArea();
432
-
433
- if (!inputArea || !inputArea.shadowRoot) {
434
- throw new Error('dominds-q4h-input not found');
435
- }
436
-
437
- const textarea = inputArea.shadowRoot.querySelector('.message-input');
438
- if (!textarea) {
439
- throw new Error('Input textarea not found');
440
- }
441
-
442
- // Check if input is usable
443
- const isDisabled = textarea.disabled || textarea.readOnly;
444
- const hasCurrentDialog = app?.getCurrentDialogInfo?.() !== null;
445
-
446
- if (isDisabled || !hasCurrentDialog) {
447
- throw new Error(
448
- `Input is disabled - no dialog selected or dialog not loaded. ` +
449
- `Use createDialog() first, or selectDialog() to load an existing dialog. ` +
450
- `Did you forget to call waitForInputEnabled() before fillAndSend()?`,
451
- );
452
- }
453
-
454
- if (typeof inputArea.setValue !== 'function') {
455
- throw new Error('Input area does not have setValue method');
456
- }
457
- if (typeof inputArea.sendMessage !== 'function') {
458
- throw new Error('Input area does not have sendMessage method');
459
- }
460
-
461
- inputArea.setValue(message);
462
- const result = await inputArea.sendMessage();
463
-
464
- if (!result.success) {
465
- throw new Error(result.error || 'sendMessage failed');
466
- }
467
-
468
- checkConsoleErrors({ threshold: 0 });
469
- return result.msgId;
470
- }
471
-
472
- /**
473
- * Wait for the generation bubble to complete.
474
- * Source: dominds-dialog-container.ts lines 414-461, 1380
475
- * Completion indicator: generation-bubble has .completed class
476
- */
477
- async function waitStreamingComplete(msgId, timeoutMs = 60000) {
478
- const dialogContainer = getDialogContainer();
479
- const shadow = dialogContainer?.shadowRoot;
480
- if (!shadow) return false;
481
-
482
- // IMPORTANT: Avoid false positives caused by races immediately after sendMessage().
483
- // We must observe the generation bubble for the specific msgId before declaring completion.
484
- let sawTargetBubble = false;
485
-
486
- const result = await waitUntil(() => {
487
- // First check user message bubble completion
488
- const userMsg = shadow.querySelector(`.user-message[data-user-msg-id="${msgId}"]`);
489
- const userBubble = shadow.querySelector(`.generation-bubble[data-user-msg-id="${msgId}"]`);
490
- const bubble = (userMsg ? userMsg.closest('.generation-bubble') : null) || userBubble;
491
-
492
- if (bubble) {
493
- sawTargetBubble = true;
494
- return bubble.classList.contains('completed');
495
- }
496
-
497
- // If we haven't even seen the bubble for this msgId yet, we are not done.
498
- // This prevents returning "complete" based on unrelated prior bubbles.
499
- if (!sawTargetBubble) return false;
500
-
501
- // Fallback: check for any completed bubble with no incomplete ones
502
- const completedBubble = shadow.querySelector(sel.genCompleted);
503
- if (completedBubble) {
504
- const incomplete = shadow.querySelectorAll(sel.genNotCompleted);
505
- if (incomplete.length === 0) {
506
- return true;
507
- }
508
- }
509
-
510
- return false;
511
- }, timeoutMs);
512
-
513
- checkConsoleErrors({ threshold: 0 });
514
- return result;
515
- }
516
-
517
- /**
518
- * Gets the latest user message text.
519
- * Source: dominds-dialog-container.ts line 1414
520
- */
521
- function latestUserText() {
522
- const dialogContainer = getDialogContainer();
523
- const shadow = dialogContainer?.shadowRoot;
524
- if (!shadow) return '';
525
-
526
- const nodes = Array.from(shadow.querySelectorAll(sel.userMsg));
527
- const n = nodes.length > 0 ? nodes[nodes.length - 1] : null;
528
- if (!n) {
529
- return '';
530
- }
531
- const raw = n.getAttribute ? n.getAttribute('data-raw-user-msg') : null;
532
- if (raw) {
533
- return raw.trim();
534
- }
535
- if (n.classList?.contains('generation-bubble')) {
536
- const body = n.querySelector('.bubble-body');
537
- if (!body) return '';
538
- const parts = [];
539
- for (const child of Array.from(body.children)) {
540
- if (child.classList.contains('user-response-divider')) {
541
- break;
542
- }
543
- const text = child.textContent?.trim() || '';
544
- if (text) {
545
- parts.push(text);
546
- }
547
- }
548
- return parts.join('\n').trim();
549
- }
550
- return (n.textContent || '').trim();
551
- }
552
-
553
- /**
554
- * Checks if all bubbles are complete.
555
- * Source: dominds-dialog-container.ts lines 451-452
556
- */
557
- function noLingering() {
558
- const dialogContainer = getDialogContainer();
559
- const shadow = dialogContainer?.shadowRoot;
560
- if (!shadow) return true;
561
- return shadow.querySelectorAll(sel.genNotCompleted).length === 0;
562
- }
563
-
564
- /**
565
- * Returns counts of messages and bubbles.
566
- * Source: dominds-dialog-container.ts
567
- */
568
- function counts() {
569
- const dialogContainer = getDialogContainer();
570
- const shadow = dialogContainer?.shadowRoot;
571
- if (!shadow) return { userCount: 0, bubbleCount: 0, incompleteCount: 0 };
572
-
573
- return {
574
- userCount: shadow.querySelectorAll(sel.userMsg).length,
575
- bubbleCount: shadow.querySelectorAll(sel.genBubble).length,
576
- incompleteCount: shadow.querySelectorAll(sel.genNotCompleted).length,
577
- };
578
- }
579
-
580
- /**
581
- * Gets the latest assistant bubble element.
582
- * Source: dominds-dialog-container.ts line 1348
583
- */
584
- function latestBubble() {
585
- const dialogContainer = getDialogContainer();
586
- const shadow = dialogContainer?.shadowRoot;
587
- if (!shadow) return null;
588
-
589
- const list = Array.from(shadow.querySelectorAll(sel.genBubble));
590
- return list.length > 0 ? list[list.length - 1] : null;
591
- }
592
-
593
- // ============================================
594
- // DomindsUI Class - UI State Snapshot
595
- // ============================================
596
-
597
- /**
598
- * DomindsUI represents a snapshot of the Dominds application state.
599
- * The tester agent observes these instances and compares them using reportDeltaTo().
600
- */
601
- class DomindsUI {
602
- constructor(data) {
603
- this.timestamp = data.timestamp;
604
- this.appExists = data.appExists;
605
- this.shadowExists = data.shadowExists;
606
-
607
- // 1. HEADER region
608
- this.header = data.header;
609
-
610
- // 2. SIDEBAR / DIALOG LIST
611
- this.sidebar = data.sidebar;
612
-
613
- // 3. CURRENT DIALOG INFO (toolbar area)
614
- this.currentDialog = data.currentDialog;
615
-
616
- // 4. CHAT AREA / MESSAGES
617
- this.chat = data.chat;
618
-
619
- // 5. INPUT AREA
620
- this.input = data.input;
621
-
622
- // 6. Q4H PANEL
623
- this.q4h = data.q4h;
624
-
625
- // 7. REMINDERS WIDGET
626
- this.reminders = data.reminders;
627
-
628
- // 8. MODALS
629
- this.modals = data.modals;
630
-
631
- // 9. CONNECTION STATUS
632
- this.connection = data.connection;
633
-
634
- // 10. TOASTS (if any)
635
- this.toasts = data.toasts;
636
- }
637
-
638
- /**
639
- * Report the delta between this snapshot and a previous one.
640
- * @param {DomindsUI} prev - Previous UI snapshot to compare against
641
- * @returns {string} Human-readable delta report
642
- */
643
- reportDeltaTo(prev) {
644
- if (!prev) {
645
- return formatFullState(this);
646
- }
647
-
648
- const delta = computeDeltaForClass(prev, this);
649
- if (delta.changes.length === 0) {
650
- return `=== UI STATE (NO CHANGES) ===
651
- ${formatFullState(this)}`;
652
- }
653
-
654
- const changeLines = delta.changes.map((c) => {
655
- if (c.path === 'currentDialog.title') {
656
- return ` • Dialog title: "${c.previous}" → "${c.current}"`;
657
- }
658
- if (c.path === 'header.runControls.emergencyStop.count') {
659
- return ` • Proceeding (header): ${c.previous} → ${c.current}`;
660
- }
661
- if (c.path === 'header.runControls.emergencyStop.disabled') {
662
- return ` • Emergency stop: ${c.current ? 'disabled' : 'enabled'}`;
663
- }
664
- if (c.path === 'header.runControls.resumeAll.count') {
665
- return ` • Resumable (header): ${c.previous} → ${c.current}`;
666
- }
667
- if (c.path === 'header.runControls.resumeAll.disabled') {
668
- return ` • Resume all: ${c.current ? 'disabled' : 'enabled'}`;
669
- }
670
- if (c.path === 'chat.messageCount') {
671
- return ` • Messages: ${c.previous} → ${c.current}`;
672
- }
673
- if (c.path === 'chat.visibleMessageCount') {
674
- return ` • Visible messages: ${c.previous} → ${c.current}`;
675
- }
676
- if (c.path === 'chat.resumePanel.visible') {
677
- return ` • Continue panel: ${c.previous ? 'VISIBLE' : 'hidden'} → ${c.current ? 'VISIBLE' : 'hidden'}`;
678
- }
679
- if (c.path === 'chat.resumePanel.reasonText') {
680
- return ` • Continue reason: "${c.previous || ''}" → "${c.current || ''}"`;
681
- }
682
- if (c.path === 'q4h.count') {
683
- return ` • Q4H questions: ${c.previous} → ${c.current}`;
684
- }
685
- if (c.path === 'reminders.count') {
686
- return ` • Reminders: ${c.previous} → ${c.current}`;
687
- }
688
- if (c.path === 'sidebar.dialogListLoaded') {
689
- return ` • Sidebar list: ${c.current ? 'loaded' : 'missing'}`;
690
- }
691
- if (c.path === 'sidebar.dialogCount') {
692
- return ` • Sidebar dialogs: ${c.previous} → ${c.current}`;
693
- }
694
- if (c.path === 'sidebar.taskGroupCount') {
695
- return ` • Sidebar tasks: ${c.previous} → ${c.current}`;
696
- }
697
- if (c.path === 'sidebar.visibleNodeTitles') {
698
- return ` • Sidebar visible: ${summarizeListDelta(c.previous, c.current)}`;
699
- }
700
- if (c.path === 'sidebar.selectedDialogTitle') {
701
- return ` • Sidebar selection: "${c.previous || ''}" → "${c.current || ''}"`;
702
- }
703
- if (c.path === 'modals.anyModalVisible') {
704
- return ` • Modal: ${c.current ? 'OPENED' : 'CLOSED'}`;
705
- }
706
- if (c.path === 'connection.connected') {
707
- return ` • Connection: ${c.current ? 'Connected' : 'Disconnected'}`;
708
- }
709
- return ` • ${c.path}: ${JSON.stringify(c.previous)} → ${JSON.stringify(c.current)}`;
710
- });
711
-
712
- return `=== UI STATE CHANGED (${delta.changes.length} change${delta.changes.length > 1 ? 's' : ''}) ===
713
- ${changeLines.join('\n')}
714
-
715
- === CURRENT STATE ===
716
- ${formatFullState(this)}`;
717
- }
718
- }
719
-
720
- /**
721
- * Takes a comprehensive snapshot of the Dominds UI state.
722
- * Returns a DomindsUI instance for observation and delta comparison.
723
- *
724
- * @returns {DomindsUI} UI state snapshot
725
- */
726
- function snapshotDomindsUI() {
727
- const app = getApp();
728
- const shadow = getAppShadow();
729
-
730
- // Capture all UI state
731
- const data = {
732
- timestamp: Date.now(),
733
- appExists: !!app,
734
- shadowExists: !!shadow,
735
-
736
- // 1. HEADER region
737
- header: captureHeaderState(shadow),
738
-
739
- // 2. SIDEBAR / DIALOG LIST
740
- sidebar: captureSidebarState(shadow),
741
-
742
- // 3. CURRENT DIALOG INFO (toolbar area)
743
- currentDialog: captureCurrentDialogState(shadow, app),
744
-
745
- // 4. CHAT AREA / MESSAGES
746
- chat: captureChatState(shadow),
747
-
748
- // 5. INPUT AREA
749
- input: captureInputState(shadow),
750
-
751
- // 6. Q4H PANEL
752
- q4h: captureQ4HState(shadow, app),
753
-
754
- // 7. REMINDERS WIDGET
755
- reminders: captureRemindersState(shadow),
756
-
757
- // 8. MODALS
758
- modals: captureModalsState(shadow),
759
-
760
- // 9. CONNECTION STATUS
761
- connection: captureConnectionState(app, shadow),
762
-
763
- // 10. TOASTS (if any)
764
- toasts: captureToastsState(shadow),
765
- };
766
-
767
- return new DomindsUI(data);
768
- }
769
-
770
- // ============================================
771
- // Capture functions for each UI region
772
- // ============================================
773
-
774
- function captureHeaderState(shadow) {
775
- if (!shadow) return { exists: false };
776
-
777
- const app = getApp();
778
- const header = shadow.querySelector('.header');
779
- const stopBtn = header?.querySelector('#toolbar-emergency-stop');
780
- const resumeAllBtn = header?.querySelector('#toolbar-resume-all');
781
-
782
- const parseBadgeCount = (btn) => {
783
- const raw = btn?.querySelector('span')?.textContent?.trim() || '0';
784
- const n = parseInt(raw, 10);
785
- return Number.isFinite(n) ? n : 0;
786
- };
787
-
788
- const emergencyStop =
789
- stopBtn instanceof HTMLButtonElement
790
- ? { exists: true, disabled: stopBtn.disabled, count: parseBadgeCount(stopBtn) }
791
- : { exists: false, disabled: true, count: 0 };
792
- const resumeAll =
793
- resumeAllBtn instanceof HTMLButtonElement
794
- ? { exists: true, disabled: resumeAllBtn.disabled, count: parseBadgeCount(resumeAllBtn) }
795
- : { exists: false, disabled: true, count: 0 };
796
-
797
- return {
798
- exists: !!header,
799
- rtws: header?.querySelector('.rtws-indicator')?.textContent?.trim() || null,
800
- uiLanguage: app?.uiLanguage || null,
801
- serverWorkLanguage: app?.serverWorkLanguage || null,
802
- themeToggle: header?.querySelector('#theme-toggle-btn')?.textContent?.trim() || null,
803
- runControls: {
804
- emergencyStop,
805
- resumeAll,
806
- },
807
- };
808
- }
809
-
810
- function captureSidebarState(shadow) {
811
- if (!shadow) return { exists: false };
812
-
813
- const sidebar = shadow.querySelector('.sidebar');
814
- const listShadow = getDialogListShadow();
815
-
816
- if (!listShadow) {
817
- return {
818
- exists: !!sidebar,
819
- dialogListLoaded: false,
820
- dialogCount: 0,
821
- taskGroupCount: 0,
822
- taskGroups: [],
823
- dialogs: [],
824
- visibleNodeTitles: [],
825
- selectedDialogTitle: null,
826
- newDialogBtnExists: !!shadow.querySelector('#new-dialog-btn'),
827
- };
828
- }
829
-
830
- // Capture dialog tree structure from the running dialog list shadow DOM
831
- const allDialogItems = Array.from(listShadow.querySelectorAll('.dialog-item') || []);
832
- const allTaskGroups = Array.from(listShadow.querySelectorAll('.task-group') || []);
833
- const dialogItems = allDialogItems.filter((item) => isElementVisible(item));
834
- const taskGroups = allTaskGroups.filter((group) => {
835
- const title = group.querySelector('.task-title');
836
- return title ? isElementVisible(title) : isElementVisible(group);
837
- });
838
-
839
- const dialogs = dialogItems.map((item) => {
840
- const toggle = item.querySelector('[data-action="toggle-root"]');
841
- const title = item.querySelector('.dialog-title');
842
- const status = item.querySelector('.dialog-status');
843
- const timestamp = item.querySelector('.dialog-time');
844
- const subdialogCount = item.querySelector('.dialog-count');
845
- const toggleText = toggle?.textContent?.trim() || '';
846
- const isSubdialog = item.classList.contains('sub-dialog');
847
- const level = isSubdialog ? '3' : '2';
848
- const countText = subdialogCount?.textContent?.trim() || '';
849
- const countValue = Number(countText);
850
-
851
- return {
852
- title: title?.textContent?.trim() || '',
853
- status: status?.textContent?.trim() || '',
854
- timestamp: timestamp?.textContent?.trim() || '',
855
- subdialogs: countText,
856
- expanded: !isSubdialog && toggleText === '▼',
857
- hasSubdialogs: !isSubdialog && Number.isFinite(countValue) ? countValue > 0 : false,
858
- level,
859
- rootId: item.getAttribute('data-root-id') || '',
860
- selfId: item.getAttribute('data-self-id') || '',
861
- };
862
- });
863
-
864
- const taskGroupsInfo = taskGroups.map((group) => {
865
- const title = group.querySelector('.task-title');
866
- const text = title?.querySelector('.task-title-left span');
867
- const count = title?.querySelector('.dialog-count');
868
- const toggle = title?.querySelector('[data-action="toggle-task"]');
869
-
870
- return {
871
- path: title?.getAttribute('data-task-path') || text?.textContent?.trim() || '',
872
- count: count?.textContent?.trim() || '',
873
- expanded: toggle?.textContent?.trim() === '▼',
874
- };
875
- });
876
-
877
- const orderedNodes = Array.from(listShadow.querySelectorAll('.task-title, .dialog-item') || []);
878
- const visibleNodeTitles = orderedNodes
879
- .filter((node) => isElementVisible(node))
880
- .map((node) => {
881
- if (node.classList.contains('task-title')) {
882
- const text = node.getAttribute('data-task-path') || node.textContent?.trim() || '';
883
- return text ? `Task: ${text}` : 'Task: (unnamed)';
884
- }
885
- const title = node.querySelector('.dialog-title')?.textContent?.trim() || '';
886
- const isSubdialog = node.classList.contains('sub-dialog');
887
- const prefix = isSubdialog ? 'Subdialog' : 'Dialog';
888
- return title ? `${prefix}: ${title}` : `${prefix}: (untitled)`;
889
- });
890
-
891
- // Find currently selected dialog in sidebar
892
- const selectedItem = listShadow.querySelector('.dialog-item.selected, .dialog-item.active');
893
-
894
- return {
895
- exists: !!sidebar,
896
- dialogListLoaded: true,
897
- dialogCount: dialogItems.length,
898
- taskGroupCount: taskGroups.length,
899
- taskGroups: taskGroupsInfo,
900
- dialogs,
901
- visibleNodeTitles,
902
- selectedDialogTitle: selectedItem?.querySelector('.dialog-title')?.textContent?.trim() || null,
903
- newDialogBtnExists: !!shadow.querySelector('#new-dialog-btn'),
904
- };
905
- }
906
-
907
- function captureCurrentDialogState(shadow, app) {
908
- if (!shadow) return { exists: false };
909
-
910
- // Use app method for reliable info
911
- const dialogInfo = app?.getCurrentDialogInfo?.() || null;
912
-
913
- // Fallback: check DOM for title
914
- const titleEl = shadow.querySelector('#current-dialog-title');
915
- const titleText = titleEl?.textContent?.trim() || '';
916
-
917
- // Course navigation state
918
- const prevBtn = shadow.querySelector('#toolbar-prev');
919
- const nextBtn = shadow.querySelector('#toolbar-next');
920
- const courseText = shadow.querySelector('#course-nav span');
921
-
922
- // Check if a dialog is actually selected (language-agnostic).
923
- // Title-based placeholder checks are not stable across UI languages.
924
- const hasRealDialog = dialogInfo !== null;
925
-
926
- return {
927
- exists: true,
928
- title: titleText,
929
- hasRealDialog,
930
- placeholder: dialogInfo === null,
931
- dialogInfo,
932
- course: courseText?.textContent?.trim() || '',
933
- prevEnabled: !prevBtn?.hasAttribute?.('disabled'),
934
- nextEnabled: !nextBtn?.hasAttribute?.('disabled'),
935
- };
936
- }
937
-
938
- function captureChatState(shadow) {
939
- if (!shadow) return { exists: false };
940
-
941
- const container = shadow.querySelector('dominds-dialog-container');
942
- const containerShadow = container?.shadowRoot;
943
-
944
- if (!containerShadow) {
945
- return {
946
- exists: !!container,
947
- messageCount: 0,
948
- messages: [],
949
- };
950
- }
951
-
952
- const resumePanel = containerShadow.querySelector('#resume-panel');
953
- const resumeBtn = containerShadow.querySelector('#resume-btn');
954
- const resumeReason = containerShadow.querySelector('#resume-reason');
955
-
956
- const resumePanelState = {
957
- exists: !!resumePanel,
958
- visible: resumePanel instanceof HTMLElement ? !resumePanel.classList.contains('hidden') : false,
959
- btnEnabled: resumeBtn instanceof HTMLButtonElement ? !resumeBtn.disabled : false,
960
- reasonText: resumeReason?.textContent?.trim() || '',
961
- };
962
-
963
- const bubbles = containerShadow.querySelectorAll('.generation-bubble') || [];
964
- const messageContainer = containerShadow.querySelector('.messages');
965
- const messageNodes = messageContainer ? Array.from(messageContainer.children) : [];
966
- const userMessages =
967
- containerShadow.querySelectorAll(
968
- '.user-message, .message.user, .generation-bubble[data-user-msg-id]',
969
- ) || [];
970
-
971
- const messages = Array.from(bubbles).map((bubble) => {
972
- const author = bubble.querySelector('.bubble-author')?.textContent?.trim() || '';
973
- const thinking = bubble.querySelector('.thinking-section')?.textContent?.trim() || '';
974
- const markdown = bubble.querySelector('.markdown-section')?.textContent?.trim() || '';
975
- const hasFuncCall = bubble.querySelector('.func-call-section');
976
- const funcTitle = bubble.querySelector('.func-call-title')?.textContent?.trim() || '';
977
- const funcNameMatch = funcTitle.match(/^Function:\\s*(.+)$/);
978
- const funcName = funcNameMatch ? funcNameMatch[1].trim() : funcTitle;
979
- const callingSection = bubble.querySelector('.calling-section.teammate-call');
980
- const callingHeadline =
981
- callingSection?.querySelector('.calling-headline')?.textContent?.trim() || '';
982
- const firstMention = callingSection?.getAttribute('data-first-mention') || '';
983
-
984
- // Check completion state
985
- const thinkingCompleted = bubble.querySelector('.thinking-section.completed');
986
- const markdownCompleted = bubble.querySelector('.markdown-section.completed');
987
-
988
- return {
989
- type: 'generation',
990
- author,
991
- hasThinking: !!thinking,
992
- thinkingPreview: thinking.slice(0, 100) + (thinking.length > 100 ? '...' : ''),
993
- hasMarkdown: !!markdown,
994
- markdownPreview: markdown.slice(0, 200) + (markdown.length > 200 ? '...' : ''),
995
- hasFuncCall: !!hasFuncCall,
996
- funcName: funcName || null,
997
- hasTeammate: !!callingSection,
998
- teammateLabel: callingHeadline || firstMention || '',
999
- thinkingCompleted: !!thinkingCompleted,
1000
- markdownCompleted: !!markdownCompleted,
1001
- };
1002
- });
1003
-
1004
- const visibleMessages = messageNodes.map((node) => {
1005
- if (node.classList.contains('generation-bubble')) {
1006
- const author = node.querySelector('.bubble-author')?.textContent?.trim() || '';
1007
- const markdownSections = Array.from(node.querySelectorAll('.markdown-section'))
1008
- .map((section) => section.textContent?.trim() || '')
1009
- .filter((text) => text.length > 0);
1010
- let content = '';
1011
- if (markdownSections.length === 1) {
1012
- content = markdownSections[0];
1013
- } else if (markdownSections.length > 1) {
1014
- const first = markdownSections[0];
1015
- const last = markdownSections[markdownSections.length - 1];
1016
- content = first === last ? first : `${first}\n${last}`;
1017
- }
1018
- return {
1019
- // Treat generation bubbles as assistant messages for scenario-level checks.
1020
- type: 'assistant',
1021
- author,
1022
- preview: content.slice(0, 120) + (content.length > 120 ? '...' : ''),
1023
- };
1024
- }
1025
- if (node.classList.contains('message')) {
1026
- const author =
1027
- node.querySelector('.author-name')?.textContent?.trim() ||
1028
- node.querySelector('.author')?.textContent?.trim() ||
1029
- '';
1030
- const type = node.classList.contains('teammate')
1031
- ? 'teammate'
1032
- : node.classList.contains('tool')
1033
- ? 'tool'
1034
- : node.classList.contains('assistant')
1035
- ? 'assistant'
1036
- : node.classList.contains('user')
1037
- ? 'user'
1038
- : node.classList.contains('system')
1039
- ? 'system'
1040
- : node.classList.contains('calling')
1041
- ? 'calling'
1042
- : node.classList.contains('subdialog')
1043
- ? 'subdialog'
1044
- : 'message';
1045
- const contentEl =
1046
- node.querySelector('.teammate-content') ||
1047
- node.querySelector('.content') ||
1048
- node.querySelector('.bubble-body') ||
1049
- node;
1050
- let contentText = contentEl?.innerText?.trim() || contentEl?.textContent?.trim() || '';
1051
- const nodeText = node.innerText?.trim() || node.textContent?.trim() || '';
1052
- if (nodeText.length > contentText.length + 20) {
1053
- contentText = nodeText;
1054
- }
1055
- if (contentText.length > 80) {
1056
- contentText = contentText.replace(/(\.(?:md|txt|rst))(?!\s|$)/g, '$1\n');
1057
- }
1058
- let previewText = contentText;
1059
- if (type === 'teammate') {
1060
- const lines = contentText
1061
- .split('\n')
1062
- .map((line) => line.trim())
1063
- .filter((line) => line.length > 0);
1064
- if (lines.length > 1) {
1065
- previewText = `${lines[0]}\n${lines[lines.length - 1]}`;
1066
- }
1067
- } else if (type === 'assistant') {
1068
- const lines = contentText
1069
- .split('\n')
1070
- .map((line) => line.trim())
1071
- .filter((line) => line.length > 0);
1072
- if (lines.length > 1) {
1073
- previewText = `${lines[0]}\n${lines[lines.length - 1]}`;
1074
- }
1075
- }
1076
- return {
1077
- type,
1078
- author,
1079
- preview: previewText.length > 200 ? previewText.slice(0, 200) + '...' : previewText,
1080
- };
1081
- }
1082
- const text = node.textContent?.trim() || '';
1083
- return {
1084
- type: 'other',
1085
- author: '',
1086
- preview: text.slice(0, 120) + (text.length > 120 ? '...' : ''),
1087
- };
1088
- });
1089
-
1090
- return {
1091
- exists: true,
1092
- messageCount: bubbles.length,
1093
- userMessageCount: userMessages.length,
1094
- messages,
1095
- latestMessage: messages[messages.length - 1] || null,
1096
- pendingTeammateCalls: getPendingTeammateCalls().length,
1097
- visibleMessageCount: messageNodes.length,
1098
- visibleMessages,
1099
- resumePanel: resumePanelState,
1100
- };
1101
- }
1102
-
1103
- function captureInputState(shadow) {
1104
- if (!shadow) return { exists: false };
1105
-
1106
- const inputArea = getInputArea();
1107
- if (!inputArea) return { exists: false };
1108
-
1109
- const inputShadow = inputArea.shadowRoot;
1110
- if (!inputShadow) return { exists: true, shadowMissing: true };
1111
-
1112
- const textarea = inputShadow.querySelector('.message-input');
1113
- const sendBtn = inputShadow.querySelector('.send-button');
1114
-
1115
- return {
1116
- exists: true,
1117
- textareaExists: !!textarea,
1118
- textareaVisible: textarea?.offsetParent !== null,
1119
- textareaEnabled: !textarea?.disabled && !textarea?.readOnly,
1120
- textareaPlaceholder: textarea?.placeholder || '',
1121
- sendBtnExists: !!sendBtn,
1122
- sendBtnEnabled: !sendBtn?.disabled,
1123
- };
1124
- }
1125
-
1126
- function captureQ4HState(shadow, app) {
1127
- if (!shadow) return { exists: false };
1128
-
1129
- const inputArea = getInputArea();
1130
- const inputShadow = inputArea?.shadowRoot;
1131
-
1132
- // Get count from app
1133
- const count = app?.q4hQuestions?.length || 0;
1134
-
1135
- // Check if Q4H section is expanded
1136
- const q4hSection = inputShadow?.querySelector('.question-list');
1137
- const isExpanded = q4hSection?.offsetParent !== null && q4hSection?.children?.length > 0;
1138
-
1139
- // Get question cards
1140
- const questionCards = inputShadow?.querySelectorAll('.q4h-question-card') || [];
1141
- const questions = Array.from(questionCards).map((card) => {
1142
- const title = card.querySelector('.q4h-question-title')?.textContent?.trim() || '';
1143
- const callHeadline = card.querySelector('.q4h-question-call-headline')?.textContent?.trim() || '';
1144
- const tellaskBody = card.querySelector('.q4h-question-call-body')?.textContent?.trim() || '';
1145
- const askedAt = card.getAttribute('data-asked-at') || '';
1146
- const isChecked = card.querySelector('.q4h-checkbox-check');
1147
-
1148
- return {
1149
- title: title.slice(0, 140) + (title.length > 140 ? '...' : ''),
1150
- callHeadline: callHeadline.slice(0, 120) + (callHeadline.length > 120 ? '...' : ''),
1151
- tellaskBodyPreview: tellaskBody.slice(0, 150) + (tellaskBody.length > 150 ? '...' : ''),
1152
- askedAt,
1153
- checked: !!isChecked,
1154
- };
1155
- });
1156
-
1157
- // Q4H panel in chat area (alternative view)
1158
- const q4hPanel = shadow.querySelector('dominds-q4h-panel');
1159
- const q4hPanelShadow = q4hPanel?.shadowRoot;
1160
-
1161
- return {
1162
- exists: true,
1163
- count,
1164
- isExpanded,
1165
- questionCount: questions.length,
1166
- questions,
1167
- panelExists: !!q4hPanel,
1168
- panelExpanded: !!q4hPanelShadow?.querySelector('.q4h-panel-container.expanded'),
1169
- };
1170
- }
1171
-
1172
- function captureRemindersState(shadow) {
1173
- if (!shadow) return { exists: false };
1174
-
1175
- const widget = shadow.querySelector('#reminders-widget');
1176
- const content = shadow.querySelector('#reminders-widget-content');
1177
- const toggle = shadow.querySelector('#toolbar-reminders-toggle');
1178
-
1179
- // Get count from toggle
1180
- const toggleBadge = toggle?.querySelector('span');
1181
- const countText = toggleBadge?.textContent?.trim() || '0';
1182
- const count = parseInt(countText, 10) || 0;
1183
-
1184
- const isVisible = widget?.offsetParent !== null;
1185
-
1186
- // Capture reminder items if visible
1187
- let reminders = [];
1188
- if (isVisible && content) {
1189
- const items = content.querySelectorAll('.reminder-item') || [];
1190
- reminders = Array.from(items).map((item) => {
1191
- const index = item.querySelector('.reminder-index')?.textContent?.trim() || '';
1192
- const text = item.querySelector('.reminder-content')?.textContent?.trim() || '';
1193
- return { index, text: text.slice(0, 100) + (text.length > 100 ? '...' : '') };
1194
- });
1195
- }
1196
-
1197
- return {
1198
- exists: true,
1199
- count,
1200
- isVisible,
1201
- hasReminders: reminders.length > 0,
1202
- reminderCount: reminders.length,
1203
- reminders,
1204
- closeBtnExists: !!shadow.querySelector('#reminders-widget-close'),
1205
- };
1206
- }
1207
-
1208
- function captureModalsState(shadow) {
1209
- if (!shadow) return { exists: false };
1210
-
1211
- const createDialogModal = shadow.querySelector('.create-dialog-modal');
1212
- const teamMembersModal = document.querySelector('.modal-overlay');
1213
-
1214
- const createDialogModalVisible = isElementVisible(createDialogModal);
1215
- const teamMembersModalVisible = isElementVisible(teamMembersModal);
1216
-
1217
- return {
1218
- exists: true,
1219
- createDialogModalVisible,
1220
- teamMembersModalVisible,
1221
- anyModalVisible: createDialogModalVisible || teamMembersModalVisible,
1222
- };
1223
- }
1224
-
1225
- function captureConnectionState(app, shadow) {
1226
- if (!app) return { exists: false };
1227
-
1228
- const statusEl = shadow?.querySelector('dominds-connection-status');
1229
- const appState = app.connectionState || null;
1230
- const appStatus = appState && typeof appState.status === 'string' ? appState.status : '';
1231
- const appError = appState && typeof appState.error === 'string' ? appState.error : '';
1232
- const statusAttr = statusEl?.getAttribute('status') || '';
1233
- const statusText = statusAttr || appStatus || statusEl?.textContent?.trim() || '';
1234
-
1235
- return {
1236
- exists: true,
1237
- statusText,
1238
- connected:
1239
- statusAttr === 'connected' || appStatus === 'connected' || statusText === 'connected',
1240
- error: statusEl?.getAttribute('error') || appError || null,
1241
- };
1242
- }
1243
-
1244
- function captureToastsState(shadow) {
1245
- if (!shadow) return { exists: false };
1246
-
1247
- const toasts = shadow.querySelectorAll('.toast') || [];
1248
- return {
1249
- exists: true,
1250
- count: toasts.length,
1251
- toasts: Array.from(toasts).map((t) => ({
1252
- text: t.textContent?.trim()?.slice(0, 100) || '',
1253
- type: t.classList.contains('error')
1254
- ? 'error'
1255
- : t.classList.contains('warning')
1256
- ? 'warning'
1257
- : 'info',
1258
- })),
1259
- };
1260
- }
1261
-
1262
- // ============================================
1263
- // Delta computation
1264
- // ============================================
1265
-
1266
- function computeDeltaForClass(previous, current) {
1267
- const delta = { changes: [] };
1268
-
1269
- // Helper to detect changes
1270
- const detectChange = (path, prevVal, currVal) => {
1271
- const prevStr = JSON.stringify(prevVal);
1272
- const currStr = JSON.stringify(currVal);
1273
- if (prevStr !== currStr) {
1274
- delta.changes.push({
1275
- path,
1276
- previous: prevVal,
1277
- current: currVal,
1278
- });
1279
- }
1280
- };
1281
-
1282
- // Compare key fields
1283
- detectChange(
1284
- 'currentDialog.hasRealDialog',
1285
- previous.currentDialog?.hasRealDialog,
1286
- current.currentDialog?.hasRealDialog,
1287
- );
1288
- detectChange('currentDialog.title', previous.currentDialog?.title, current.currentDialog?.title);
1289
- detectChange('currentDialog.course', previous.currentDialog?.course, current.currentDialog?.course);
1290
-
1291
- detectChange('chat.messageCount', previous.chat?.messageCount, current.chat?.messageCount);
1292
- detectChange(
1293
- 'chat.visibleMessageCount',
1294
- previous.chat?.visibleMessageCount,
1295
- current.chat?.visibleMessageCount,
1296
- );
1297
- detectChange(
1298
- 'chat.latestMessage.author',
1299
- previous.chat?.latestMessage?.author,
1300
- current.chat?.latestMessage?.author,
1301
- );
1302
- detectChange(
1303
- 'chat.pendingTeammateCalls',
1304
- previous.chat?.pendingTeammateCalls,
1305
- current.chat?.pendingTeammateCalls,
1306
- );
1307
-
1308
- detectChange(
1309
- 'input.textareaEnabled',
1310
- previous.input?.textareaEnabled,
1311
- current.input?.textareaEnabled,
1312
- );
1313
- detectChange(
1314
- 'input.textareaVisible',
1315
- previous.input?.textareaVisible,
1316
- current.input?.textareaVisible,
1317
- );
1318
-
1319
- detectChange('q4h.count', previous.q4h?.count, current.q4h?.count);
1320
- detectChange('q4h.isExpanded', previous.q4h?.isExpanded, current.q4h?.isExpanded);
1321
-
1322
- detectChange('reminders.count', previous.reminders?.count, current.reminders?.count);
1323
- detectChange('reminders.isVisible', previous.reminders?.isVisible, current.reminders?.isVisible);
1324
-
1325
- detectChange(
1326
- 'modals.anyModalVisible',
1327
- previous.modals?.anyModalVisible,
1328
- current.modals?.anyModalVisible,
1329
- );
1330
-
1331
- detectChange(
1332
- 'sidebar.selectedDialogTitle',
1333
- previous.sidebar?.selectedDialogTitle,
1334
- current.sidebar?.selectedDialogTitle,
1335
- );
1336
- detectChange(
1337
- 'sidebar.dialogListLoaded',
1338
- previous.sidebar?.dialogListLoaded,
1339
- current.sidebar?.dialogListLoaded,
1340
- );
1341
- detectChange('sidebar.dialogCount', previous.sidebar?.dialogCount, current.sidebar?.dialogCount);
1342
- detectChange(
1343
- 'sidebar.taskGroupCount',
1344
- previous.sidebar?.taskGroupCount,
1345
- current.sidebar?.taskGroupCount,
1346
- );
1347
- detectChange(
1348
- 'sidebar.visibleNodeTitles',
1349
- previous.sidebar?.visibleNodeTitles,
1350
- current.sidebar?.visibleNodeTitles,
1351
- );
1352
-
1353
- detectChange(
1354
- 'connection.connected',
1355
- previous.connection?.connected,
1356
- current.connection?.connected,
1357
- );
1358
-
1359
- detectChange('toasts.count', previous.toasts?.count, current.toasts?.count);
1360
-
1361
- return delta;
1362
- }
1363
-
1364
- // ============================================
1365
- // Human-readable state formatting
1366
- // ============================================
1367
-
1368
- function formatList(items, maxItems = 6) {
1369
- if (!Array.isArray(items) || items.length === 0) return '[]';
1370
- const slice = items.slice(0, maxItems);
1371
- const suffix = items.length > maxItems ? ` +${items.length - maxItems} more` : '';
1372
- return `[${slice.join(' | ')}]${suffix}`;
1373
- }
1374
-
1375
- function summarizeListDelta(previous, current) {
1376
- const prev = Array.isArray(previous) ? previous : [];
1377
- const curr = Array.isArray(current) ? current : [];
1378
- const prevSet = new Set(prev);
1379
- const currSet = new Set(curr);
1380
- const added = curr.filter((item) => !prevSet.has(item));
1381
- const removed = prev.filter((item) => !currSet.has(item));
1382
- const orderChanged =
1383
- added.length === 0 && removed.length === 0 && prev.join('|') !== curr.join('|');
1384
-
1385
- const parts = [];
1386
- if (added.length > 0) parts.push(`+${formatList(added, 4)}`);
1387
- if (removed.length > 0) parts.push(`-${formatList(removed, 4)}`);
1388
- if (orderChanged) parts.push('order changed');
1389
- if (parts.length === 0) return 'unchanged';
1390
- return parts.join(' ');
1391
- }
1392
-
1393
- function formatFullState(state) {
1394
- const lines = [];
1395
-
1396
- // Current dialog (most important)
1397
- if (state.currentDialog?.hasRealDialog) {
1398
- lines.push(` 📂 Dialog: "${state.currentDialog.title}"`);
1399
- if (state.currentDialog.course) {
1400
- lines.push(` Course: ${state.currentDialog.course}`);
1401
- }
1402
- } else {
1403
- lines.push(` 📂 No dialog selected`);
1404
- }
1405
-
1406
- // Chat messages
1407
- const chatState = state.chat || null;
1408
- const messageCount =
1409
- chatState && typeof chatState.messageCount === 'number' ? chatState.messageCount : 0;
1410
- const visibleCount =
1411
- chatState && typeof chatState.visibleMessageCount === 'number'
1412
- ? chatState.visibleMessageCount
1413
- : 0;
1414
- const visibleMessages =
1415
- chatState && Array.isArray(chatState.visibleMessages) ? chatState.visibleMessages : [];
1416
- const latestVisible =
1417
- visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1] : null;
1418
- const latestVisibleAuthor = latestVisible && latestVisible.author ? latestVisible.author : '';
1419
- const latestMessageAuthor =
1420
- chatState && chatState.latestMessage && chatState.latestMessage.author
1421
- ? chatState.latestMessage.author
1422
- : '?';
1423
-
1424
- if (messageCount > 0 || visibleCount > 0) {
1425
- const latestAuthor = latestVisibleAuthor || latestMessageAuthor || '?';
1426
- lines.push(
1427
- ` 💬 ${visibleCount} visible message(s) (bubbles: ${messageCount}), latest: @${latestAuthor}`,
1428
- );
1429
- } else {
1430
- lines.push(` 💬 No messages yet`);
1431
- }
1432
-
1433
- // Sidebar / dialog list
1434
- if (state.sidebar?.exists) {
1435
- if (state.sidebar.dialogListLoaded === false) {
1436
- lines.push(` 📚 Sidebar: dialog list not loaded`);
1437
- } else {
1438
- const dialogCount = state.sidebar.dialogCount || 0;
1439
- const taskCount = state.sidebar.taskGroupCount || 0;
1440
- lines.push(` 📚 Sidebar: ${dialogCount} dialog(s), ${taskCount} task group(s)`);
1441
- if (
1442
- Array.isArray(state.sidebar.visibleNodeTitles) &&
1443
- state.sidebar.visibleNodeTitles.length
1444
- ) {
1445
- lines.push(` Visible: ${formatList(state.sidebar.visibleNodeTitles, 6)}`);
1446
- }
1447
- }
1448
- }
1449
-
1450
- // Input state
1451
- const inputStatus = state.input?.textareaEnabled ? 'enabled' : 'disabled';
1452
- lines.push(` ✏️ Input: ${inputStatus}`);
1453
-
1454
- // Run controls (header)
1455
- const run = state.header?.runControls || null;
1456
- const stopCount =
1457
- run && run.emergencyStop && typeof run.emergencyStop.count === 'number'
1458
- ? run.emergencyStop.count
1459
- : 0;
1460
- const resumeCount =
1461
- run && run.resumeAll && typeof run.resumeAll.count === 'number' ? run.resumeAll.count : 0;
1462
- const stopDisabled =
1463
- !!(run && run.emergencyStop && typeof run.emergencyStop.disabled === 'boolean'
1464
- ? run.emergencyStop.disabled
1465
- : true);
1466
- const resumeDisabled =
1467
- !!(run && run.resumeAll && typeof run.resumeAll.disabled === 'boolean'
1468
- ? run.resumeAll.disabled
1469
- : true);
1470
- lines.push(
1471
- ` 🛑 Run controls: proceeding=${stopCount} (${stopDisabled ? 'stop disabled' : 'stop enabled'}), resumable=${resumeCount} (${resumeDisabled ? 'resume disabled' : 'resume enabled'})`,
1472
- );
1473
-
1474
- // Continue panel (per-dlg resume)
1475
- const resumePanel = state.chat?.resumePanel || null;
1476
- if (resumePanel && resumePanel.visible) {
1477
- lines.push(` ▶️ Continue: VISIBLE (${resumePanel.reasonText || 'no reason'})`);
1478
- } else {
1479
- lines.push(` ▶️ Continue: hidden`);
1480
- }
1481
-
1482
- // Q4H
1483
- if (state.q4h?.count > 0) {
1484
- lines.push(
1485
- ` ❓ Q4H: ${state.q4h.count} question(s) ${state.q4h.isExpanded ? '[expanded]' : '[collapsed]'}`,
1486
- );
1487
- } else {
1488
- lines.push(` ❓ Q4H: 0`);
1489
- }
1490
-
1491
- // Reminders
1492
- if (state.reminders?.isVisible) {
1493
- lines.push(` 🔔 Reminders: ${state.reminders.count} [VISIBLE]`);
1494
- } else {
1495
- lines.push(` 🔔 Reminders: ${state.reminders.count} [hidden]`);
1496
- }
1497
-
1498
- // Connection
1499
- lines.push(
1500
- ` ${state.connection?.connected ? '🟢' : '🔴'} Connection: ${state.connection?.statusText || 'unknown'}`,
1501
- );
1502
-
1503
- // Modals
1504
- if (state.modals?.anyModalVisible) {
1505
- lines.push(` ⚠️ Modal open`);
1506
- }
1507
-
1508
- return lines.join('\n');
1509
- }
1510
-
1511
- // ============================================
1512
- // Dialog Creation Functions
1513
- // ============================================
1514
-
1515
- /**
1516
- * Creates a new dialog using the UI modal flow.
1517
- * This simulates the full user interaction:
1518
- * 1. Click "New Dialog" button to open modal
1519
- * 2. Fill Taskdoc path in modal input
1520
- * 3. Select teammate from dropdown (optional - uses default if omitted)
1521
- * 4. Click "Create Dialog" button
1522
- *
1523
- * @param {string} taskDocPath - Path to the Taskdoc (e.g., 'cmds-test.md')
1524
- * @param {string} [callsign] - Optional teammate callsign (e.g., '@pangu', '@fuxi').
1525
- * If omitted, uses the rt team's default responder.
1526
- * @returns {Promise<{callsign: string, taskDocPath: string, dialogId: string, rootId: string, created: boolean}>}
1527
- *
1528
- * Source: dominds-app.tsx - showCreateDialogModal(), setupDialogModalEvents()
1529
- */
1530
- async function createDialog(taskDocPath, callsign) {
1531
- const app = getApp();
1532
- if (!app) {
1533
- throw new Error('dominds-app element not found');
1534
- }
1535
-
1536
- try {
1537
- await waitUntil(() => Array.isArray(app.teamMembers) && app.teamMembers.length > 0, 7000);
1538
- } catch {
1539
- throw new Error(
1540
- 'No team members available. Ensure an LLM provider API key env var is set so the default shadow team members can be created.',
1541
- );
1542
- }
1543
-
1544
- const shadow = getAppShadow();
1545
- if (!shadow) {
1546
- throw new Error('dominds-app shadowRoot not found');
1547
- }
1548
-
1549
- // Extract agentId from callsign if provided (e.g., '@pangu' -> 'pangu')
1550
- const agentId = callsign ? callsign.replace(/^@/, '') : null;
1551
-
1552
- // Capture original title
1553
- const originalTitle = getCurrentDialogTitle();
1554
-
1555
- // Step 1: Click "New Dialog" button to open modal
1556
- const newDialogBtn = shadow.querySelector(sel.newDialogBtn);
1557
- if (!newDialogBtn) {
1558
- throw new Error('New Dialog button (#new-dialog-btn) not found');
1559
- }
1560
- newDialogBtn.click();
1561
-
1562
- // Step 2: Wait for modal to appear
1563
- await waitUntil(() => {
1564
- const modal = shadow.querySelector(sel.dialogModal);
1565
- return modal !== null;
1566
- }, 3000);
1567
-
1568
- const modal = shadow.querySelector(sel.dialogModal);
1569
- if (!modal) {
1570
- throw new Error('Create Dialog modal (.create-dialog-modal) did not appear');
1571
- }
1572
-
1573
- // Step 3: Fill the Taskdoc path
1574
- const taskInput = shadow.querySelector(sel.taskDocInput);
1575
- if (!taskInput) {
1576
- throw new Error('Taskdoc input (#task-doc-input) not found');
1577
- }
1578
- taskInput.value = taskDocPath;
1579
- // Trigger input event for autocomplete to work properly
1580
- taskInput.dispatchEvent(new Event('input', { bubbles: true }));
1581
-
1582
- // Step 4: Select the teammate from dropdown (only if callsign provided)
1583
- if (agentId) {
1584
- const teammateSelect = shadow.querySelector(sel.teammateSelect);
1585
- if (!teammateSelect) {
1586
- throw new Error('Teammate select (#teammate-select) not found');
1587
- }
1588
- const hasDirectOption = teammateSelect.querySelector(`option[value="${agentId}"]`) !== null;
1589
- if (hasDirectOption) {
1590
- teammateSelect.value = agentId;
1591
- teammateSelect.dispatchEvent(new Event('change', { bubbles: true }));
1592
- } else {
1593
- const shadowOption = teammateSelect.querySelector(`option[value="__shadow__"]`);
1594
- if (!shadowOption) {
1595
- throw new Error(
1596
- `Agent '${agentId}' is not in the visible teammate list, and the shadow-members option is missing.`,
1597
- );
1598
- }
1599
- teammateSelect.value = '__shadow__';
1600
- teammateSelect.dispatchEvent(new Event('change', { bubbles: true }));
1601
-
1602
- const shadowSelect = shadow.querySelector('#shadow-teammate-select');
1603
- if (!(shadowSelect instanceof HTMLSelectElement)) {
1604
- throw new Error('Shadow teammate select (#shadow-teammate-select) not found');
1605
- }
1606
- if (shadowSelect.querySelector(`option[value="${agentId}"]`) === null) {
1607
- throw new Error(`Shadow teammate '${agentId}' not found in shadow select`);
1608
- }
1609
- shadowSelect.value = agentId;
1610
- shadowSelect.dispatchEvent(new Event('change', { bubbles: true }));
1611
- }
1612
- }
1613
-
1614
- // Step 5: Click "Create Dialog" button
1615
- const createBtn = shadow.querySelector(sel.createBtn);
1616
- if (!createBtn) {
1617
- throw new Error('Create Dialog button (#create-dialog-btn) not found');
1618
- }
1619
- createBtn.click();
1620
-
1621
- // Wait for modal to be removed and title to change
1622
- await waitUntil(() => {
1623
- const modalStillExists = shadow.querySelector(sel.dialogModal);
1624
- const newTitle = getCurrentDialogTitle();
1625
- return !modalStillExists && newTitle !== originalTitle;
1626
- }, 5000);
1627
-
1628
- // Get the final title and extract actual agent from it
1629
- const newTitle = getCurrentDialogTitle();
1630
-
1631
- // Extract agent callsign from title (format: "@agentId - taskName" or similar)
1632
- const actualAgentMatch = newTitle.match(/^@(\w+)/);
1633
- const actualAgentId = actualAgentMatch ? actualAgentMatch[1] : null;
1634
-
1635
- // Verify the agent if callsign was specified
1636
- if (agentId && actualAgentId !== agentId) {
1637
- throw new Error(`Expected @${agentId} in dialog title, got: "${newTitle}"`);
1638
- }
1639
-
1640
- // Get the created dialog info
1641
- const dialogInfo = getCurrentDialogInfo();
1642
-
1643
- return {
1644
- callsign: actualAgentId,
1645
- taskDocPath,
1646
- dialogId: dialogInfo?.selfId || dialogInfo?.rootId,
1647
- rootId: dialogInfo?.rootId,
1648
- created: true,
1649
- };
1650
- }
1651
-
1652
- // ============================================
1653
- // Dialog Selection Functions
1654
- // ============================================
1655
-
1656
- /**
1657
- * Selects a dialog from the sidebar by ID.
1658
- * Source: running-dialog-list.ts
1659
- * Component method: selectDialogById(rootId) returns boolean
1660
- */
1661
- function selectDialogById(rootId) {
1662
- const dialogList = getDialogList();
1663
- if (!dialogList) throw new Error('RunningDialogList component not found');
1664
-
1665
- if (typeof dialogList.selectDialogById !== 'function') {
1666
- throw new Error('selectDialogById method not available on RunningDialogList');
1667
- }
1668
-
1669
- return dialogList.selectDialogById(rootId);
1670
- }
1671
-
1672
- /**
1673
- * Selects a dialog from the sidebar using component methods.
1674
- * Source: running-dialog-list.ts, dominds-app.tsx
1675
- * Component methods: findDialogByRootId(), selectDialogById(), findSubdialog()
1676
- */
1677
- async function selectDialog(dialogText) {
1678
- const dialogList = getDialogList();
1679
- if (!dialogList) throw new Error('RunningDialogList component not found');
1680
-
1681
- if (typeof dialogList.selectDialogById !== 'function') {
1682
- throw new Error('selectDialogById method not available on RunningDialogList');
1683
- }
1684
-
1685
- // Try to find by root ID first
1686
- const dialog = dialogList.findDialogByRootId?.(dialogText);
1687
- if (dialog) {
1688
- const success = dialogList.selectDialogById(dialogText);
1689
- if (!success) throw new Error(`selectDialogById failed for "${dialogText}"`);
1690
- return true;
1691
- }
1692
-
1693
- // Try to find subdialog (format: "rootId:selfId")
1694
- if (dialogText.includes(':')) {
1695
- const [rootId, selfId] = dialogText.split(':');
1696
- await ensureSubdialogsLoaded(rootId);
1697
- const subdialog = dialogList.findSubdialog?.(rootId, selfId);
1698
- if (subdialog) {
1699
- const opened = await openSubdialog(rootId, selfId);
1700
- if (!opened) throw new Error(`openSubdialog failed for "${dialogText}"`);
1701
- return true;
1702
- }
1703
- }
1704
-
1705
- throw new Error(`Dialog with ID "${dialogText}" not found in sidebar`);
1706
- }
1707
-
1708
- /**
1709
- * Gets all dialogs from the sidebar.
1710
- * Source: running-dialog-list.ts
1711
- * Component method: getAllDialogs() returns ApiRootDialogResponse[]
1712
- */
1713
- function getAllDialogs() {
1714
- const dialogList = getDialogList();
1715
- if (!dialogList) return [];
1716
-
1717
- if (typeof dialogList.getAllDialogs === 'function') {
1718
- return dialogList.getAllDialogs();
1719
- }
1720
-
1721
- // Fallback to DOM traversal
1722
- const shadow = getDialogListShadow();
1723
- if (!shadow) return [];
1724
- return Array.from(shadow.querySelectorAll('.dialog-item'));
1725
- }
1726
-
1727
- // ============================================
1728
- // Subdialog Navigation Functions
1729
- // ============================================
1730
-
1731
- /**
1732
- * Ensure subdialogs for a root dialog are loaded (lazy loading aware).
1733
- * Attempts backend load via dominds-app if available; falls back to expanding the root dialog.
1734
- */
1735
- async function ensureSubdialogsLoaded(rootId, timeoutMs = 8000) {
1736
- const app = getApp();
1737
- if (!app) throw new Error('dominds-app not found');
1738
-
1739
- const dialogList = getDialogList();
1740
- if (typeof app.ensureSubdialogsLoaded === 'function') {
1741
- await app.ensureSubdialogsLoaded(rootId);
1742
- }
1743
-
1744
- // Ensure task group + root are expanded in the UI so subdialogs are visible.
1745
- const listShadow = getDialogListShadow();
1746
- if (listShadow) {
1747
- const rootDialogData = Array.isArray(app.dialogs)
1748
- ? app.dialogs.find((d) => d.rootId === rootId && !d.selfId)
1749
- : null;
1750
- const taskPath = rootDialogData?.taskDocPath;
1751
- if (taskPath) {
1752
- const taskTitle = listShadow.querySelector(
1753
- `.task-title[data-task-path="${escapeCssValue(taskPath)}"]`,
1754
- );
1755
- const taskToggle = taskTitle?.querySelector('[data-action="toggle-task"]');
1756
- if (taskToggle && taskToggle.textContent?.trim() === '▶') {
1757
- taskToggle.click();
1758
- }
1759
- }
1760
-
1761
- const rootItem = listShadow.querySelector(
1762
- `.dialog-item.root-dialog[data-root-id="${escapeCssValue(rootId)}"]`,
1763
- );
1764
- const rootToggle = rootItem?.querySelector('[data-action="toggle-root"]');
1765
- if (rootToggle && rootToggle.textContent?.trim() === '▶') {
1766
- rootToggle.click();
1767
- }
1768
- }
1769
-
1770
- try {
1771
- await waitUntil(() => {
1772
- const dialogs = getAllDialogs();
1773
- if (!Array.isArray(dialogs) || dialogs.length === 0) return false;
1774
- const rootDialog = dialogs.find(
1775
- (d) => d && typeof d.rootId === 'string' && d.rootId === rootId && !d.selfId,
1776
- );
1777
- const expectedCount =
1778
- rootDialog && typeof rootDialog.subdialogCount === 'number' ? rootDialog.subdialogCount : 0;
1779
- if (expectedCount === 0) return true;
1780
- return dialogs.some(
1781
- (d) => d && d.supdialogId === rootId && typeof d.selfId === 'string' && d.selfId !== '',
1782
- );
1783
- }, timeoutMs);
1784
- return true;
1785
- } catch {
1786
- return false;
1787
- }
1788
- }
1789
-
1790
- /**
1791
- * Opens a subdialog using the component's direct method.
1792
- * Source: dominds-app.tsx lines 1738-1756
1793
- * Component method: openSubdialog(rootId, subdialogId) returns Promise<boolean>
1794
- */
1795
- async function openSubdialog(rootId, subdialogId) {
1796
- const app = getApp();
1797
- if (!app) throw new Error('dominds-app not found');
1798
-
1799
- if (typeof app.openSubdialog !== 'function') {
1800
- throw new Error('openSubdialog method not available on dominds-app');
1801
- }
1802
-
1803
- let opened = await app.openSubdialog(rootId, subdialogId);
1804
- if (!opened) {
1805
- await ensureSubdialogsLoaded(rootId);
1806
- opened = await app.openSubdialog(rootId, subdialogId);
1807
- }
1808
- return opened;
1809
- }
1810
-
1811
- function dialogInfoMatches(info, expected) {
1812
- if (!expected) return true;
1813
- if (!info) return false;
1814
- if (expected.rootId && String(info.rootId || '') !== String(expected.rootId)) return false;
1815
- if (expected.selfId && String(info.selfId || '') !== String(expected.selfId)) return false;
1816
- if (expected.agentId && normalizeMention(info.agentId) !== normalizeMention(expected.agentId))
1817
- return false;
1818
- return true;
1819
- }
1820
-
1821
- async function waitForDialogSelected(expected, timeoutMs = 15000) {
1822
- await waitUntil(() => {
1823
- const info = getCurrentDialogInfo();
1824
- return dialogInfoMatches(info, expected);
1825
- }, timeoutMs);
1826
- return true;
1827
- }
1828
-
1829
- async function waitForCourseNavMatch(pattern, timeoutMs = 15000) {
1830
- const toRegex = (p) => {
1831
- if (p instanceof RegExp) return p;
1832
- return new RegExp(String(p));
1833
- };
1834
- const re = toRegex(pattern);
1835
- await waitUntil(() => {
1836
- const snap = snapshotDomindsUI();
1837
- const text = snap.currentDialog?.course || '';
1838
- return typeof text === 'string' && re.test(text);
1839
- }, timeoutMs);
1840
- return true;
1841
- }
1842
-
1843
- async function waitForInputEnabledState(enabled, timeoutMs = 15000) {
1844
- await waitUntil(() => {
1845
- const snap = snapshotDomindsUI();
1846
- return snap.input?.textareaEnabled === enabled;
1847
- }, timeoutMs);
1848
- return true;
1849
- }
1850
-
1851
- /**
1852
- * Wait until the UI is "idle" enough for deterministic scripted interaction.
1853
- *
1854
- * Defaults are chosen for ux-stories stability:
1855
- * - No incomplete generation bubbles
1856
- * - No pending teammate tellasks
1857
- *
1858
- * Options:
1859
- * - requireInputEnabled: boolean | null
1860
- * - requireNoLingering: boolean
1861
- * - requireNoPendingTeammateCalls: boolean
1862
- */
1863
- async function waitForDialogIdle(options = {}) {
1864
- const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 60000;
1865
- const requireInputEnabled =
1866
- typeof options.requireInputEnabled === 'boolean' ? options.requireInputEnabled : null;
1867
- const requireNoLingering =
1868
- typeof options.requireNoLingering === 'boolean' ? options.requireNoLingering : true;
1869
- const requireNoPendingTeammateCalls =
1870
- typeof options.requireNoPendingTeammateCalls === 'boolean'
1871
- ? options.requireNoPendingTeammateCalls
1872
- : true;
1873
-
1874
- await waitUntil(() => {
1875
- if (requireNoLingering && !noLingering()) return false;
1876
- const snap = snapshotDomindsUI();
1877
- if (requireNoPendingTeammateCalls && (snap.chat?.pendingTeammateCalls || 0) !== 0) return false;
1878
- if (requireInputEnabled !== null && snap.input?.textareaEnabled !== requireInputEnabled)
1879
- return false;
1880
- return true;
1881
- }, timeoutMs);
1882
- return true;
1883
- }
1884
-
1885
- /**
1886
- * Open a subdialog and wait until the UI has actually switched to it and become idle.
1887
- * This is a stricter variant of openSubdialog() intended for E2E scripting.
1888
- */
1889
- async function openSubdialogAndWait(rootId, subdialogId, options = {}) {
1890
- const opened = await openSubdialog(rootId, subdialogId);
1891
- if (!opened) return false;
1892
- await waitForDialogSelected({ rootId, selfId: subdialogId }, options.timeoutMs || 15000);
1893
- // Wait for toolbar/title to reflect the selected dialog.
1894
- // This avoids races where app state flips before DOM updates (common under fast timing).
1895
- await waitUntil(
1896
- () => getCurrentDialogTitle().includes(String(subdialogId)),
1897
- options.timeoutMs || 15000,
1898
- );
1899
- await waitForCourseNavMatch(/^C\s+\d+$/, options.timeoutMs || 15000);
1900
- if (typeof options.requireInputEnabled === 'boolean') {
1901
- await waitForInputEnabledState(options.requireInputEnabled, options.timeoutMs || 15000);
1902
- }
1903
- if (typeof options.minVisibleMessages === 'number') {
1904
- await waitForVisibleMessageCount(options.minVisibleMessages, options.timeoutMs || 60000);
1905
- }
1906
- await waitForDialogIdle(options);
1907
- return true;
1908
- }
1909
-
1910
- /**
1911
- * Navigate from a subdialog back to its parent and wait for the UI to settle.
1912
- */
1913
- async function navigateToParentAndWait(options = {}) {
1914
- const before = getSubdialogHierarchy();
1915
- const ok = await navigateToParent();
1916
- if (!ok) return false;
1917
- await waitUntil(() => {
1918
- const after = getSubdialogHierarchy();
1919
- return Array.isArray(after) && after.length < before.length;
1920
- }, options.timeoutMs || 15000);
1921
- const expectedParent = before.length >= 2 ? before[before.length - 2] : null;
1922
- if (expectedParent?.selfId) {
1923
- await waitUntil(
1924
- () => getCurrentDialogTitle().includes(String(expectedParent.selfId)),
1925
- options.timeoutMs || 15000,
1926
- );
1927
- }
1928
- await waitForCourseNavMatch(/^C\s+\d+$/, options.timeoutMs || 15000);
1929
- if (typeof options.requireInputEnabled === 'boolean') {
1930
- await waitForInputEnabledState(options.requireInputEnabled, options.timeoutMs || 15000);
1931
- }
1932
- if (typeof options.minVisibleMessages === 'number') {
1933
- await waitForVisibleMessageCount(options.minVisibleMessages, options.timeoutMs || 60000);
1934
- }
1935
- await waitForDialogIdle(options);
1936
- return true;
1937
- }
1938
-
1939
- /**
1940
- * Select a dialog (root or subdialog) and wait for the UI to settle.
1941
- *
1942
- * Supported formats:
1943
- * - rootId
1944
- * - rootId:selfId
1945
- */
1946
- async function selectDialogAndWait(dialogText, options = {}) {
1947
- if (typeof dialogText !== 'string' || dialogText.trim() === '') {
1948
- throw new Error('selectDialogAndWait: dialogText must be a non-empty string');
1949
- }
1950
- if (dialogText.includes(':')) {
1951
- const [rootId, selfId] = dialogText.split(':');
1952
- return await openSubdialogAndWait(rootId, selfId, options);
1953
- }
1954
- const ok = selectDialogById(dialogText);
1955
- if (!ok) throw new Error(`selectDialogAndWait: selectDialogById failed for "${dialogText}"`);
1956
- await waitForDialogSelected(
1957
- { rootId: dialogText, selfId: dialogText },
1958
- options.timeoutMs || 15000,
1959
- );
1960
- await waitUntil(
1961
- () => getCurrentDialogTitle().includes(String(dialogText)),
1962
- options.timeoutMs || 15000,
1963
- );
1964
- await waitForCourseNavMatch(/^C\s+\d+$/, options.timeoutMs || 15000);
1965
- if (typeof options.requireInputEnabled === 'boolean') {
1966
- await waitForInputEnabledState(options.requireInputEnabled, options.timeoutMs || 15000);
1967
- }
1968
- if (typeof options.minVisibleMessages === 'number') {
1969
- await waitForVisibleMessageCount(options.minVisibleMessages, options.timeoutMs || 60000);
1970
- }
1971
- await waitForDialogIdle(options);
1972
- return true;
1973
- }
1974
-
1975
- /**
1976
- * Gets the subdialog hierarchy from parent to current.
1977
- * Source: dominds-app.tsx lines 1666-1709, 1712-1736
1978
- * Uses: getCurrentDialogInfo(), app.dialogs[]
1979
- */
1980
- function getSubdialogHierarchy() {
1981
- const app = getApp();
1982
- if (!app) throw new Error('dominds-app not found');
1983
-
1984
- const hierarchy = [];
1985
- let current = app.getCurrentDialogInfo?.();
1986
-
1987
- while (current) {
1988
- hierarchy.unshift({
1989
- selfId: current.selfId || current.rootId,
1990
- rootId: current.rootId,
1991
- agentId: current.agentId || '',
1992
- });
1993
-
1994
- // Check if this is a subdialog (selfId !== rootId)
1995
- if (current.selfId !== current.rootId) {
1996
- // Try to find parent in app.dialogs using supdialogId
1997
- const currentDialogData = app.dialogs?.find(
1998
- (d) => d.rootId === current.rootId && d.selfId === current.selfId,
1999
- );
2000
-
2001
- if (currentDialogData?.supdialogId) {
2002
- const parentDialog = app.dialogs?.find((d) => d.rootId === currentDialogData.supdialogId);
2003
- if (parentDialog) {
2004
- current = {
2005
- selfId: parentDialog.selfId || parentDialog.rootId,
2006
- rootId: parentDialog.rootId,
2007
- agentId: parentDialog.agentId,
2008
- };
2009
- continue;
2010
- }
2011
- }
2012
- }
2013
-
2014
- // No more parent
2015
- break;
2016
- }
2017
-
2018
- return hierarchy;
2019
- }
2020
-
2021
- /**
2022
- * Navigates from a subdialog back to its supdialog.
2023
- * Source: dominds-app.tsx lines 1712-1736
2024
- * Component method: navigateToParent() returns Promise<boolean>
2025
- */
2026
- async function navigateToParent() {
2027
- const app = getApp();
2028
- if (!app) throw new Error('dominds-app not found');
2029
-
2030
- if (typeof app.navigateToParent !== 'function') {
2031
- throw new Error('navigateToParent method not available on dominds-app');
2032
- }
2033
-
2034
- const navigated = await app.navigateToParent();
2035
- if (!navigated) return false;
2036
-
2037
- // Allow time for dialog history to stream in after navigation.
2038
- try {
2039
- await waitUntil(() => {
2040
- const shadow = getAppShadow();
2041
- if (!shadow) return false;
2042
- const chat = captureChatState(shadow);
2043
- return typeof chat.visibleMessageCount === 'number' && chat.visibleMessageCount > 0;
2044
- }, 5000);
2045
- } catch (_err) {
2046
- // Non-fatal: some dialogs legitimately have no messages.
2047
- }
2048
-
2049
- return true;
2050
- }
2051
-
2052
- /**
2053
- * Gets the current dialog info from the component.
2054
- * Source: dominds-app.tsx lines 1666-1709
2055
- * Component method: getCurrentDialogInfo() returns DialogInfo | null
2056
- */
2057
- function getCurrentDialogInfo() {
2058
- const app = getApp();
2059
- if (!app) throw new Error('dominds-app not found');
2060
-
2061
- if (typeof app.getCurrentDialogInfo !== 'function') {
2062
- throw new Error('getCurrentDialogInfo method not available on dominds-app');
2063
- }
2064
-
2065
- return app.getCurrentDialogInfo();
2066
- }
2067
-
2068
- /**
2069
- * Gets the current dialog title text from #current-dialog-title element.
2070
- * Element is in app's Shadow DOM.
2071
- * @returns {string} The dialog title text (e.g., "@pangu - task-name")
2072
- */
2073
- function getCurrentDialogTitle() {
2074
- const shadow = getAppShadow();
2075
- if (!shadow) return '';
2076
- const titleEl = shadow.querySelector('#current-dialog-title');
2077
- return titleEl ? (titleEl.textContent || '').trim() : '';
2078
- }
2079
-
2080
- /**
2081
- * Gets the current Q4H count.
2082
- */
2083
- function getQ4HCount() {
2084
- const app = getApp();
2085
- if (!app) return 0;
2086
- return app.q4hQuestions?.length || 0;
2087
- }
2088
-
2089
- /**
2090
- * Opens the Q4H panel by clicking the toggle bar.
2091
- */
2092
- async function openQ4HPanel() {
2093
- const shadow = getAppShadow();
2094
- if (!shadow) throw new Error('App shadow not found');
2095
-
2096
- const panel = shadow.querySelector(sel.q4hPanel);
2097
- if (!panel) return;
2098
-
2099
- const isExpanded = panel.classList.contains('expanded');
2100
- if (isExpanded) return;
2101
-
2102
- const toggle = shadow.querySelector(sel.q4hToggleBar);
2103
- if (toggle) {
2104
- toggle.click();
2105
- await waitUntil(() => panel.classList.contains('expanded'));
2106
- }
2107
- }
2108
-
2109
- /**
2110
- * Gets the Q4H panel height.
2111
- */
2112
- function getQ4HPanelHeight() {
2113
- const shadow = getAppShadow();
2114
- if (!shadow) return 0;
2115
- const panel = shadow.querySelector(sel.q4hPanel);
2116
- return panel ? panel.offsetHeight : 0;
2117
- }
2118
-
2119
- /**
2120
- * Simulates dragging the Q4H resize handle.
2121
- * @param {number} deltaY - Pixels to drag (negative for up/larger)
2122
- */
2123
- async function dragQ4HResizeHandle(deltaY) {
2124
- const shadow = getAppShadow();
2125
- if (!shadow) throw new Error('App shadow not found');
2126
-
2127
- const handle = shadow.querySelector(sel.q4hResizeHandle);
2128
- if (!handle) throw new Error('Q4H resize handle not found');
2129
-
2130
- const rect = handle.getBoundingClientRect();
2131
- const startX = rect.left + rect.width / 2;
2132
- const startY = rect.top + rect.height / 2;
2133
-
2134
- // Dispatch mousedown
2135
- handle.dispatchEvent(
2136
- new MouseEvent('mousedown', {
2137
- bubbles: true,
2138
- clientX: startX,
2139
- clientY: startY,
2140
- }),
2141
- );
2142
-
2143
- // Dispatch mousemove
2144
- window.dispatchEvent(
2145
- new MouseEvent('mousemove', {
2146
- bubbles: true,
2147
- clientX: startX,
2148
- clientY: startY + deltaY,
2149
- }),
2150
- );
2151
-
2152
- // Dispatch mouseup
2153
- window.dispatchEvent(
2154
- new MouseEvent('mouseup', {
2155
- bubbles: true,
2156
- }),
2157
- );
2158
-
2159
- // Give some time for state update
2160
- await new Promise((resolve) => setTimeout(resolve, 50));
2161
- }
2162
-
2163
- /**
2164
- * Navigates to the call site of a specific Q4H question.
2165
- * @param {string} questionId - The ID of the question to navigate to
2166
- */
2167
- async function goToQ4HCallSite(questionId) {
2168
- const shadow = getAppShadow();
2169
- if (!shadow) throw new Error('App shadow not found');
2170
-
2171
- // Find the panel host
2172
- const panelHost = shadow.querySelector(sel.q4hPanelHost);
2173
- if (!panelHost || !panelHost.shadowRoot) throw new Error('Q4H panel host or shadow not found');
2174
-
2175
- const btn = panelHost.shadowRoot.querySelector(
2176
- `${sel.q4hGoToSiteBtn}[data-question-id="${questionId}"]`,
2177
- );
2178
- if (!btn) throw new Error(`Go to call site button for question ${questionId} not found`);
2179
-
2180
- btn.click();
2181
-
2182
- // Wait for potential dialog switch and scroll
2183
- await new Promise((resolve) => setTimeout(resolve, 300));
2184
- }
2185
-
2186
- /**
2187
- * Gets all Q4H questions across all dialogs.
2188
- */
2189
- function getQ4HList() {
2190
- const app = getApp();
2191
- if (!app) return [];
2192
- return app.q4hQuestions || [];
2193
- }
2194
-
2195
- /**
2196
- * Selects a Q4H question by ID.
2197
- */
2198
- function selectQ4HQuestion(questionId) {
2199
- // Preferred: use dominds-q4h-input public API so selection works even when the panel view is not rendered.
2200
- const inputArea = getInputArea();
2201
- if (inputArea && typeof inputArea.selectQuestion === 'function') {
2202
- try {
2203
- inputArea.selectQuestion(questionId);
2204
- if (typeof inputArea.getSelectedQuestionId === 'function') {
2205
- return inputArea.getSelectedQuestionId() === questionId;
2206
- }
2207
- return true;
2208
- } catch (err) {
2209
- console.warn('selectQ4HQuestion: failed to select via input component', err);
2210
- // Fall through to DOM-click fallback.
2211
- }
2212
- }
2213
-
2214
- const shadow = getAppShadow();
2215
- if (!shadow) return false;
2216
-
2217
- const panelHost = shadow.querySelector(sel.q4hPanelHost);
2218
- if (!panelHost || !panelHost.shadowRoot) return false;
2219
-
2220
- const card = panelHost.shadowRoot.querySelector(
2221
- `.q4h-question-card[data-question-id="${questionId}"]`,
2222
- );
2223
- if (!card) return false;
2224
-
2225
- const title = card.querySelector('.q4h-question-title');
2226
- if (title) {
2227
- title.click();
2228
- return true;
2229
- }
2230
- return false;
2231
- }
2232
-
2233
- // ============================================
2234
- // Reminders Widget Functions
2235
- // ============================================
2236
-
2237
- /**
2238
- * Opens the reminders widget.
2239
- * Source: dominds-app.tsx lines 1092, 1300, 2966-2992
2240
- * Toggle ID: #toolbar-reminders-toggle
2241
- */
2242
- function openReminders() {
2243
- const app = getApp();
2244
- if (!app || !app.shadowRoot) throw new Error('dominds-app or shadowRoot not found');
2245
-
2246
- const toggle = app.shadowRoot.querySelector('#toolbar-reminders-toggle');
2247
- if (!toggle) throw new Error('Reminders toggle button (#toolbar-reminders-toggle) not found');
2248
-
2249
- toggle.click();
2250
-
2251
- // Widget is dynamically created
2252
- return app.shadowRoot.querySelector('#reminders-widget');
2253
- }
2254
-
2255
- /**
2256
- * Closes the reminders widget.
2257
- * Source: dominds-app.tsx lines 1110, 2984, 172-179
2258
- * Close button ID: #reminders-widget-close
2259
- */
2260
- function closeReminders() {
2261
- const app = getApp();
2262
- if (!app || !app.shadowRoot) throw new Error('dominds-app or shadowRoot not found');
2263
-
2264
- // Try close button first
2265
- const closeBtn = app.shadowRoot.querySelector('#reminders-widget-close');
2266
- if (closeBtn) {
2267
- closeBtn.click();
2268
- return true;
2269
- }
2270
-
2271
- // Fallback: toggle again
2272
- const toggle = app.shadowRoot.querySelector('#toolbar-reminders-toggle');
2273
- if (toggle) {
2274
- toggle.click();
2275
- return true;
2276
- }
2277
-
2278
- throw new Error('Could not close reminders widget');
2279
- }
2280
-
2281
- /**
2282
- * Toggles the reminders widget open/close state.
2283
- * Source: dominds-app.tsx lines 1092, 2966-2992
2284
- */
2285
- function toggleReminders() {
2286
- const app = getApp();
2287
- if (!app || !app.shadowRoot) throw new Error('dominds-app or shadowRoot not found');
2288
-
2289
- const toggle = app.shadowRoot.querySelector('#toolbar-reminders-toggle');
2290
- if (!toggle) throw new Error('Reminders toggle button not found');
2291
-
2292
- const widget = app.shadowRoot.querySelector('#reminders-widget');
2293
- const isOpen = widget && widget.style.display !== 'none' && !widget.hasAttribute('hidden');
2294
-
2295
- toggle.click();
2296
- return !isOpen;
2297
- }
2298
-
2299
- /**
2300
- * Gets the current content of the reminders widget.
2301
- * Source: dominds-app.tsx lines 1114, 2988
2302
- * Content ID: #reminders-widget-content
2303
- * Note: Widget must be open and rendered before calling this function.
2304
- */
2305
- function getRemindersContent() {
2306
- const app = getApp();
2307
- if (!app || !app.shadowRoot) return '';
2308
-
2309
- // First ensure widget is open
2310
- const widget = app.shadowRoot.querySelector('#reminders-widget');
2311
- if (!widget || widget.hasAttribute('hidden') || widget.style.display === 'none') {
2312
- // Widget is not open, try to open it
2313
- const toggle = app.shadowRoot.querySelector('#toolbar-reminders-toggle');
2314
- if (toggle) {
2315
- toggle.click();
2316
- }
2317
- return '';
2318
- }
2319
-
2320
- const content = app.shadowRoot.querySelector('#reminders-widget-content');
2321
- if (!content) return '';
2322
-
2323
- return (content.textContent || '').trim();
2324
- }
2325
-
2326
- /**
2327
- * Gets the current reminder count from the app state.
2328
- * Accesses app.toolbarReminders directly for accurate count.
2329
- * @returns {number} Current reminder count (0 if none)
2330
- */
2331
- function getRemindersCount() {
2332
- const app = getApp();
2333
- if (!app) return 0;
2334
-
2335
- // Access the app's internal toolbarReminders array (private field but accessible in JS)
2336
- const reminders = app.toolbarReminders;
2337
- if (!reminders || !Array.isArray(reminders)) return 0;
2338
-
2339
- return reminders.length;
2340
- }
2341
-
2342
- /**
2343
- * Gets the reminders widget element for direct access.
2344
- * Source: dominds-app.tsx lines 2966-2992
2345
- * @returns {HTMLElement|null} The reminders widget element
2346
- */
2347
- function getRemindersWidget() {
2348
- const app = getApp();
2349
- if (!app || !app.shadowRoot) return null;
2350
- return app.shadowRoot.querySelector('#reminders-widget');
2351
- }
2352
-
2353
- /**
2354
- * Gets the reminders component for method access.
2355
- * @returns {HTMLElement|null} The dominds-reminders element if available
2356
- */
2357
- function getRemindersComponent() {
2358
- const widget = getRemindersWidget();
2359
- if (!widget) return null;
2360
- return widget.querySelector('dominds-reminders') || widget;
2361
- }
2362
-
2363
- /**
2364
- * Waits until the reminder count matches the expected count.
2365
- * Uses polling with configurable timeout.
2366
- * @param {number} expectedCount - The count to wait for
2367
- * @param {number} [timeoutMs=10000] - Maximum wait time in milliseconds
2368
- * @returns {Promise<boolean>} True if count reached, false if timeout
2369
- */
2370
- async function waitForRemindersCount(expectedCount, timeoutMs = 10000) {
2371
- const startTime = Date.now();
2372
-
2373
- return new Promise((resolve) => {
2374
- const check = () => {
2375
- try {
2376
- const currentCount = getRemindersCount();
2377
- if (currentCount === expectedCount) {
2378
- return resolve(true);
2379
- }
2380
- } catch (err) {
2381
- console.warn('Error checking reminder count:', err);
2382
- }
2383
-
2384
- if (Date.now() - startTime >= timeoutMs) {
2385
- console.log(
2386
- `waitForRemindersCount timeout: expected=${expectedCount}, got=${getRemindersCount()}`,
2387
- );
2388
- return resolve(false);
2389
- }
2390
-
2391
- setTimeout(check, 100);
2392
- };
2393
-
2394
- check();
2395
- });
2396
- }
2397
-
2398
- /**
2399
- * Waits until no reminder operations are pending.
2400
- * Checks for widget stability by monitoring app state.
2401
- * @param {number} [timeoutMs=5000] - Maximum wait time in milliseconds
2402
- * @param {number} [intervalMs=200] - Polling interval
2403
- * @returns {Promise<boolean>} True if stable, false if timeout
2404
- */
2405
- async function waitUntilReminderStable(timeoutMs = 5000, intervalMs = 200) {
2406
- const startTime = Date.now();
2407
-
2408
- return new Promise((resolve) => {
2409
- const check = () => {
2410
- try {
2411
- // Check if widget is open and content is stable
2412
- const widget = getRemindersWidget();
2413
- if (!widget || widget.hasAttribute('hidden') || widget.style.display === 'none') {
2414
- // Widget is closed, consider stable
2415
- return resolve(true);
2416
- }
2417
-
2418
- // Widget is open - check for any pending operations
2419
- // Use DOM observation utility if available
2420
- if (domObs && typeof domObs.isObserving === 'function' && domObs.isObserving()) {
2421
- // DOM is stable
2422
- return resolve(true);
2423
- }
2424
-
2425
- // Additional stability checks
2426
- const content = getRemindersContent();
2427
- if (content && content.length > 0) {
2428
- // Content appears loaded
2429
- return resolve(true);
2430
- }
2431
- } catch (err) {
2432
- console.warn('Error checking reminder stability:', err);
2433
- }
2434
-
2435
- if (Date.now() - startTime >= timeoutMs) {
2436
- console.log('waitUntilReminderStable timeout');
2437
- return resolve(false);
2438
- }
2439
-
2440
- setTimeout(check, intervalMs);
2441
- };
2442
-
2443
- check();
2444
- });
2445
- }
2446
-
2447
- /**
2448
- * Waits for widget animations to complete.
2449
- * @param {number} [timeoutMs=3000] - Maximum wait time in milliseconds
2450
- * @returns {Promise<boolean>} True if animations completed or no widget
2451
- */
2452
- async function waitForWidgetStable(timeoutMs = 3000) {
2453
- const startTime = Date.now();
2454
- const app = getApp();
2455
-
2456
- return new Promise((resolve) => {
2457
- const check = () => {
2458
- try {
2459
- const widget = getRemindersWidget();
2460
- if (!widget) return resolve(true);
2461
-
2462
- // Check if widget is fully visible (not mid-transition)
2463
- const style = window.getComputedStyle(widget);
2464
- if (style.display === 'none' || style.visibility === 'hidden') {
2465
- return resolve(true);
2466
- }
2467
-
2468
- // Widget is visible - check if toggle is responsive
2469
- const toggle = app?.shadowRoot?.querySelector('#toolbar-reminders-toggle');
2470
- if (toggle && toggle.offsetParent !== null) {
2471
- return resolve(true);
2472
- }
2473
- } catch (err) {
2474
- console.warn('Error checking widget stability:', err);
2475
- }
2476
-
2477
- if (Date.now() - startTime >= timeoutMs) {
2478
- console.log('waitForWidgetStable timeout');
2479
- return resolve(false);
2480
- }
2481
-
2482
- setTimeout(check, 100);
2483
- };
2484
-
2485
- check();
2486
- });
2487
- }
2488
-
2489
- /**
2490
- * Waits until no console errors appear.
2491
- * Useful for waiting for error handling to complete.
2492
- * @param {number} [timeoutMs=5000] - Maximum wait time in milliseconds
2493
- * @returns {Promise<boolean>} True if no errors, false if timeout
2494
- */
2495
- async function waitForNoConsoleErrors(timeoutMs = 5000) {
2496
- const startTime = Date.now();
2497
- const initialErrors = __consoleErrors__.length;
2498
-
2499
- return new Promise((resolve) => {
2500
- const check = () => {
2501
- const currentErrors = __consoleErrors__.length;
2502
-
2503
- // Check if errors have settled (no new errors for a bit)
2504
- if (currentErrors === initialErrors) {
2505
- return resolve(true);
2506
- }
2507
-
2508
- if (Date.now() - startTime >= timeoutMs) {
2509
- console.log('waitForNoConsoleErrors timeout');
2510
- return resolve(false);
2511
- }
2512
-
2513
- setTimeout(check, 200);
2514
- };
2515
-
2516
- check();
2517
- });
2518
- }
2519
-
2520
- // ============================================
2521
- // Q4H (Questions for Human) Helper Functions
2522
- // ============================================
2523
-
2524
- /**
2525
- * Gets the current Q4H badge count from the input area
2526
- * Source: dominds-q4h-input.ts - getQuestionCount() method
2527
- * @returns {number} Current Q4H count (0 if none)
2528
- */
2529
- function getQ4HCountFromInput() {
2530
- const inputArea = getInputArea();
2531
- if (!inputArea) return 0;
2532
-
2533
- if (typeof inputArea.getQuestionCount === 'function') {
2534
- return inputArea.getQuestionCount();
2535
- }
2536
-
2537
- // Fallback: count question cards
2538
- const shadow = inputArea.shadowRoot;
2539
- if (!shadow) return 0;
2540
-
2541
- const countEl = shadow.querySelector('.q4h-count-badge');
2542
- if (!countEl) return 0;
2543
-
2544
- const count = parseInt(countEl.textContent || '0', 10);
2545
- return isNaN(count) ? 0 : count;
2546
- }
2547
-
2548
- /**
2549
- * Gets all Q4H questions from the input area component
2550
- * Source: dominds-q4h-input.ts - getQuestions() method
2551
- * @returns {Array<{id: string, tellaskHead: string, bodyContent: string, askedAt: string}>} Array of Q4H questions
2552
- */
2553
- function getQ4HListFromInput() {
2554
- const inputArea = getInputArea();
2555
- if (!inputArea) return [];
2556
-
2557
- if (typeof inputArea.getQuestions === 'function') {
2558
- return inputArea.getQuestions();
2559
- }
2560
-
2561
- return [];
2562
- }
2563
-
2564
- /**
2565
- * Gets the active Q4H question IDs
2566
- * Useful for verifying which questions are pending
2567
- * @returns {string[]} Array of pending question IDs
2568
- */
2569
- function getPendingQ4HIds() {
2570
- const questions = getQ4HList();
2571
- return questions.map((q) => q.id);
2572
- }
2573
-
2574
- /**
2575
- * Selects a Q4H question in the component
2576
- * Source: dominds-q4h-input.ts - selectQuestion() method
2577
- * @param {string} questionId - The question ID to select
2578
- * @returns {boolean} True if selection succeeded
2579
- */
2580
- function selectQ4HQuestionFromInput(questionId) {
2581
- const inputArea = getInputArea();
2582
- if (!inputArea) throw new Error('dominds-q4h-input not found');
2583
-
2584
- if (typeof inputArea.selectQuestion !== 'function') {
2585
- throw new Error('selectQuestion method not available');
2586
- }
2587
-
2588
- inputArea.selectQuestion(questionId);
2589
- return true;
2590
- }
2591
-
2592
- /**
2593
- * Gets the currently selected Q4H question ID
2594
- * Source: dominds-q4h-input.ts - getSelectedQuestionId() method
2595
- * @returns {string|null} Selected question ID or null
2596
- */
2597
- function getSelectedQ4HQuestionId() {
2598
- const inputArea = getInputArea();
2599
- if (!inputArea) return null;
2600
-
2601
- if (typeof inputArea.getSelectedQuestionId === 'function') {
2602
- return inputArea.getSelectedQuestionId();
2603
- }
2604
-
2605
- return null;
2606
- }
2607
-
2608
- /**
2609
- * Answers a Q4H question inline
2610
- * Uses the component's setValue() and sendMessage() with active question
2611
- * @param {string} answer - The user's answer text
2612
- * @returns {Promise<string>} The message ID of the answer
2613
- */
2614
- async function answerQ4H(answer) {
2615
- const inputArea = getInputArea();
2616
- if (!inputArea) throw new Error('dominds-q4h-input not found');
2617
-
2618
- // Verify there's an active question
2619
- if (inputArea.getQuestionCount !== undefined && inputArea.getQuestionCount() === 0) {
2620
- throw new Error('No active Q4H to answer');
2621
- }
2622
-
2623
- if (typeof inputArea.setValue !== 'function') {
2624
- throw new Error('Input area does not have setValue method');
2625
- }
2626
-
2627
- inputArea.setValue(answer);
2628
- const result = await inputArea.sendMessage();
2629
-
2630
- if (!result.success) {
2631
- throw new Error(result.error || 'sendMessage failed for Q4H answer');
2632
- }
2633
-
2634
- checkConsoleErrors({ threshold: 0 });
2635
- return result.msgId;
2636
- }
2637
-
2638
- // ============================================
2639
- // Agent Function Call Detection & Nudging
2640
- // ============================================
2641
-
2642
- /**
2643
- * Detects if the last assistant message contains a function call.
2644
- * Looks for .func-call-section elements which contain the function name in .func-call-title
2645
- * @param {string} [toolName] - Optional tool name to check for (e.g., 'shell_cmd')
2646
- * @returns {Object} Result with hasFuncCall (boolean) and funcCallInfo (object)
2647
- */
2648
- function detectFuncCall(toolName) {
2649
- const dialogContainer = getDialogContainer();
2650
- const shadow = dialogContainer?.shadowRoot;
2651
- if (!shadow) {
2652
- return { hasFuncCall: false, toolName: null, index: -1 };
2653
- }
2654
-
2655
- // Look for func-call-section elements in the dialog
2656
- const funcCallSections = shadow.querySelectorAll('.func-call-section');
2657
-
2658
- if (funcCallSections.length === 0) {
2659
- return { hasFuncCall: false, toolName: null, index: -1 };
2660
- }
2661
-
2662
- // Get the last func-call-section
2663
- const lastIndex = funcCallSections.length - 1;
2664
- const lastSection = funcCallSections[lastIndex];
2665
-
2666
- // Extract function name from func-call-title element
2667
- const titleEl = lastSection.querySelector('.func-call-title');
2668
- const titleText = titleEl ? (titleEl.textContent || '').trim() : '';
2669
-
2670
- // Extract arguments from func-call-arguments element
2671
- const argsEl = lastSection.querySelector('.func-call-arguments');
2672
- const argsText = argsEl ? (argsEl.textContent || '').trim() : '';
2673
-
2674
- // Extract result from func-call-result element (if visible)
2675
- const resultEl = lastSection.querySelector('.func-call-result');
2676
- const resultText =
2677
- resultEl && resultEl.style.display !== 'none' ? (resultEl.textContent || '').trim() : '';
2678
-
2679
- // Extract the function name from "Function: name" format
2680
- const funcNameMatch = titleText.match(/^Function:\s*(.+)$/);
2681
- const funcName = funcNameMatch ? funcNameMatch[1].trim() : '';
2682
-
2683
- if (toolName) {
2684
- // Check if the last func call is for the specified tool
2685
- const hasTool = funcName === toolName || titleText.includes(toolName);
2686
- return {
2687
- hasFuncCall: hasTool,
2688
- toolName: hasTool ? funcName : null,
2689
- index: hasTool ? lastIndex : -1,
2690
- header: hasTool ? titleText : null,
2691
- content: hasTool ? argsText : null,
2692
- result: hasTool ? resultText : null,
2693
- funcName: hasTool ? funcName : null,
2694
- };
2695
- }
2696
-
2697
- return {
2698
- hasFuncCall: true,
2699
- toolName: funcName || null,
2700
- index: lastIndex,
2701
- header: titleText,
2702
- content: argsText,
2703
- result: resultText,
2704
- funcName,
2705
- };
2706
- }
2707
-
2708
- /**
2709
- * Gets all pending teammate tellasks (tellasks still waiting for response).
2710
- * @returns {Array<{element: HTMLElement, firstMention: string, isHuman: boolean, callSiteId: number | null}>}
2711
- */
2712
- function getPendingTeammateCalls() {
2713
- return getTeammateCallingSections().filter((item) => {
2714
- const el = item.element;
2715
- return !el.classList.contains('completed');
2716
- });
2717
- }
2718
-
2719
- function parseCallSiteId(value) {
2720
- if (value === null || value === undefined || value === '') return null;
2721
- const parsed = Number(value);
2722
- return Number.isFinite(parsed) ? parsed : null;
2723
- }
2724
-
2725
- function normalizeMention(value) {
2726
- return String(value || '')
2727
- .trim()
2728
- .replace(/^@/, '')
2729
- .trim();
2730
- }
2731
-
2732
- function getTeamMemberIds() {
2733
- const app = getApp();
2734
- return Array.isArray(app?.teamMembers) ? app.teamMembers.map((m) => m.id) : [];
2735
- }
2736
-
2737
- function isTeammateMention(firstMention) {
2738
- const normalized = normalizeMention(firstMention);
2739
- if (!normalized) return false;
2740
- return getTeamMemberIds().includes(normalized);
2741
- }
2742
-
2743
- function getNonTeammateCallingSections() {
2744
- const dialogContainer = getDialogContainer();
2745
- const shadow = dialogContainer?.shadowRoot;
2746
- if (!shadow) return [];
2747
- const sections = shadow.querySelectorAll('.calling-section');
2748
- return Array.from(sections).filter((el) => {
2749
- const firstMention = el.getAttribute('data-first-mention') || '';
2750
- const isTeammate = el.classList.contains('teammate-call') || isTeammateMention(firstMention);
2751
- return !isTeammate;
2752
- });
2753
- }
2754
-
2755
- /**
2756
- * Detects if the last calling section is a non-teammate tellask call (e.g. @clear_mind, @change_mind).
2757
- * This is NOT the same as a function call (.func-call-section).
2758
- *
2759
- * @param {string} [toolName] - Optional tool name to check for (e.g., 'clear_mind')
2760
- * @returns {Object} Result with hasCall (boolean) and call details (object)
2761
- */
2762
- function detectNonTeammateCall(toolName) {
2763
- const sections = getNonTeammateCallingSections();
2764
- if (sections.length === 0) {
2765
- return { hasCall: false, toolName: null, index: -1 };
2766
- }
2767
-
2768
- const lastIndex = sections.length - 1;
2769
- const lastSection = sections[lastIndex];
2770
- const firstMention = lastSection.getAttribute('data-first-mention') || '';
2771
- const tellaskHeadEl = lastSection.querySelector('.calling-headline');
2772
- const tellaskHeadText = tellaskHeadEl ? (tellaskHeadEl.textContent || '').trim() : '';
2773
- const bodyEl = lastSection.querySelector('.calling-body');
2774
- const bodyText = bodyEl ? (bodyEl.textContent || '').trim() : '';
2775
- const resultEl = lastSection.querySelector('.calling-result');
2776
- const resultText =
2777
- resultEl && resultEl.style.display !== 'none' ? (resultEl.textContent || '').trim() : '';
2778
-
2779
- if (toolName) {
2780
- const expected = normalizeMention(toolName);
2781
- const actual = normalizeMention(firstMention);
2782
- const hasTool = expected !== '' && actual === expected;
2783
- return {
2784
- hasCall: hasTool,
2785
- toolName: hasTool ? actual : null,
2786
- index: hasTool ? lastIndex : -1,
2787
- firstMention: hasTool ? actual : null,
2788
- tellaskHead: hasTool ? tellaskHeadText : null,
2789
- body: hasTool ? bodyText : null,
2790
- result: hasTool ? resultText : null,
2791
- };
2792
- }
2793
-
2794
- return {
2795
- hasCall: true,
2796
- toolName: normalizeMention(firstMention) || null,
2797
- index: lastIndex,
2798
- firstMention,
2799
- tellaskHead: tellaskHeadText,
2800
- body: bodyText,
2801
- result: resultText,
2802
- };
2803
- }
2804
-
2805
- function extractCallSiteIdFromSection(el) {
2806
- const callSiteId = parseCallSiteId(el.getAttribute('data-call-site-id'));
2807
- if (callSiteId !== null) return callSiteId;
2808
- return parseCallSiteId(el.getAttribute('data-genseq'));
2809
- }
2810
-
2811
- function getTeammateCallingSections() {
2812
- const dialogContainer = getDialogContainer();
2813
- const shadow = dialogContainer?.shadowRoot;
2814
- if (!shadow) return [];
2815
- const sections = shadow.querySelectorAll('.calling-section');
2816
- return Array.from(sections)
2817
- .map((el) => {
2818
- const firstMention = el.getAttribute('data-first-mention') || '';
2819
- const isTeammate = el.classList.contains('teammate-call') || isTeammateMention(firstMention);
2820
- if (!isTeammate) return null;
2821
- return {
2822
- element: el,
2823
- firstMention,
2824
- isHuman: el.getAttribute('data-is-human') === 'true',
2825
- callSiteId: extractCallSiteIdFromSection(el),
2826
- };
2827
- })
2828
- .filter((item) => item !== null);
2829
- }
2830
-
2831
- function getTeammateCallSites() {
2832
- return getTeammateCallingSections();
2833
- }
2834
-
2835
- function getLatestTeammateCallSiteId() {
2836
- const sites = getTeammateCallSites();
2837
- let latest = null;
2838
- for (const site of sites) {
2839
- if (typeof site.callSiteId !== 'number') continue;
2840
- if (latest === null || site.callSiteId > latest) {
2841
- latest = site.callSiteId;
2842
- }
2843
- }
2844
- return latest;
2845
- }
2846
-
2847
- /**
2848
- * Waits for a new teammate tellask site to appear after a known call-site ID.
2849
- * @param {Object} options - Options object
2850
- * @param {number} [options.timeoutMs=60000] - Maximum wait time
2851
- * @param {number} [options.after] - Only return call sites with ID > after
2852
- * @param {string} [options.firstMention] - Optional filter for @mention (e.g., "@pangu")
2853
- * @returns {Promise<number | null>} Call-site ID, or null on timeout
2854
- */
2855
- async function waitForTeammateCallSiteId(options = {}) {
2856
- const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 60000;
2857
- const after = typeof options.after === 'number' ? options.after : -Infinity;
2858
- const firstMention = typeof options.firstMention === 'string' ? options.firstMention : '';
2859
- const expectedMention = normalizeMention(firstMention);
2860
- const startTime = Date.now();
2861
-
2862
- return new Promise((resolve) => {
2863
- const check = () => {
2864
- const sites = getTeammateCallSites();
2865
- let latest = null;
2866
- for (const site of sites) {
2867
- if (typeof site.callSiteId !== 'number') continue;
2868
- if (site.callSiteId <= after) continue;
2869
- if (expectedMention && normalizeMention(site.firstMention) !== expectedMention) continue;
2870
- if (latest === null || site.callSiteId > latest) {
2871
- latest = site.callSiteId;
2872
- }
2873
- }
2874
-
2875
- if (latest !== null) return resolve(latest);
2876
- if (Date.now() - startTime >= timeoutMs) {
2877
- console.log(
2878
- `waitForTeammateCallSiteId timeout: after=${after}, mention=${firstMention || '*'}`,
2879
- );
2880
- return resolve(null);
2881
- }
2882
- setTimeout(check, 100);
2883
- };
2884
- check();
2885
- });
2886
- }
2887
-
2888
- /**
2889
- * Waits for all pending teammate tellasks to complete.
2890
- * @param {number} [timeoutMs=60000] - Maximum wait time
2891
- * @returns {Promise<boolean>} True if completed, false if timeout
2892
- */
2893
- async function waitForPendingTeammateCalls(timeoutMs = 60000) {
2894
- const startTime = Date.now();
2895
- return new Promise((resolve) => {
2896
- const check = () => {
2897
- const pendingCalls = getPendingTeammateCalls();
2898
- if (pendingCalls.length === 0) return resolve(true);
2899
- if (Date.now() - startTime >= timeoutMs) {
2900
- console.log(`waitForPendingTeammateCalls timeout: ${pendingCalls.length} pending`);
2901
- return resolve(false);
2902
- }
2903
- setTimeout(check, 100);
2904
- };
2905
- check();
2906
- });
2907
- }
2908
-
2909
- /**
2910
- * Waits for the visible message list to reach a minimum count.
2911
- * Useful when teammate responses render as .message.* entries.
2912
- * @param {number} minCount - Minimum visible messages in .messages container
2913
- * @param {number} [timeoutMs=60000] - Maximum wait time
2914
- * @returns {Promise<boolean>} True if count reached, false if timeout
2915
- */
2916
- async function waitForVisibleMessageCount(minCount, timeoutMs = 60000) {
2917
- const startTime = Date.now();
2918
- return new Promise((resolve) => {
2919
- const check = () => {
2920
- const container = getMessageContainer();
2921
- const count = container ? container.children.length : 0;
2922
- if (count >= minCount) return resolve(true);
2923
- if (Date.now() - startTime >= timeoutMs) {
2924
- console.log(`waitForVisibleMessageCount timeout: expected>=${minCount}, got ${count}`);
2925
- return resolve(false);
2926
- }
2927
- setTimeout(check, 100);
2928
- };
2929
- check();
2930
- });
2931
- }
2932
-
2933
- /**
2934
- * Waits for a teammate response bubble with non-trivial content.
2935
- * @param {Object} options - Options object
2936
- * @param {number} [options.timeoutMs=60000] - Maximum wait time
2937
- * @param {number} [options.minChars=12] - Minimum text length to consider complete
2938
- * @param {number} [options.initialCount] - Initial teammate message count (defaults to current)
2939
- * @param {number} [options.minNew=1] - Minimum number of new teammate messages to wait for
2940
- * @param {number} [options.callSiteId] - Require response bubble to match call-site ID
2941
- * @returns {Promise<boolean>} True if a new response appears, false if timeout
2942
- */
2943
- async function waitForTeammateResponse(options = {}) {
2944
- const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 60000;
2945
- const minChars = typeof options.minChars === 'number' ? options.minChars : 1;
2946
- const initialCount =
2947
- typeof options.initialCount === 'number' ? options.initialCount : getTeammateMessageCount();
2948
- const minNew = typeof options.minNew === 'number' ? options.minNew : 1;
2949
- const callSiteId = typeof options.callSiteId === 'number' ? options.callSiteId : null;
2950
- const startTime = Date.now();
2951
-
2952
- return new Promise((resolve) => {
2953
- const check = () => {
2954
- try {
2955
- const messages = getTeammateMessages();
2956
- if (messages.length >= initialCount + minNew) {
2957
- const newMessages = messages.slice(initialCount);
2958
- const contentMessages = [];
2959
- for (const message of newMessages) {
2960
- const contentEl = message.querySelector('.teammate-content');
2961
- if (!contentEl) continue;
2962
- const text = (contentEl.textContent || '').trim();
2963
- if (text.length < minChars) continue;
2964
- const messageCallSiteId = parseCallSiteId(message.getAttribute('data-call-site-id'));
2965
- contentMessages.push({ messageCallSiteId, text });
2966
- if (callSiteId !== null && messageCallSiteId === callSiteId) {
2967
- return resolve(true);
2968
- }
2969
- }
2970
- if (callSiteId === null) {
2971
- if (contentMessages.length > 0) {
2972
- return resolve(true);
2973
- }
2974
- return;
2975
- }
2976
- if (contentMessages.length === 1) {
2977
- console.log(
2978
- 'waitForTeammateResponse fallback: single content message, accepting despite call-site mismatch',
2979
- );
2980
- return resolve(true);
2981
- }
2982
- const ids = contentMessages
2983
- .map((item) => item.messageCallSiteId)
2984
- .filter((id) => id !== null);
2985
- if (ids.length === 0 && contentMessages.length > 0) {
2986
- console.log(
2987
- 'waitForTeammateResponse fallback: content has no call-site ids, accepting',
2988
- );
2989
- return resolve(true);
2990
- }
2991
- }
2992
- } catch (err) {
2993
- console.warn('Error checking teammate response:', err);
2994
- }
2995
-
2996
- if (Date.now() - startTime >= timeoutMs) {
2997
- const newCount = getTeammateMessageCount() - initialCount;
2998
- console.log(`waitForTeammateResponse timeout: expected+${minNew}, got +${newCount}`);
2999
- return resolve(false);
3000
- }
3001
-
3002
- setTimeout(check, 150);
3003
- };
3004
- check();
3005
- });
3006
- }
3007
-
3008
- // ============================================
3009
- // Export to window.__e2e__
3010
- // ============================================
3011
-
3012
- function setGlobal() {
3013
- const g = {
3014
- // Selectors
3015
- sel,
3016
- // Shadow DOM accessors
3017
- getAppShadow,
3018
- getApp,
3019
- getInputArea,
3020
- getDialogContainer,
3021
- getDialogList,
3022
- getDialogListShadow,
3023
- getMessageContainer,
3024
- getTeammateMessageCount,
3025
- getTeammateResponseDetails,
3026
- getLatestTeammateResponseDetails,
3027
- getVisibleMessageTexts,
3028
- findVisibleMessageContainingAll,
3029
- // Core messaging
3030
- fillAndSend,
3031
- waitStreamingComplete,
3032
- waitForInputEnabled,
3033
- // State inspection - NEW: snapshotDomindsUI for delta-based UI observation
3034
- snapshotDomindsUI,
3035
- DomindsUI, // Class for UI snapshots with reportDeltaTo() method
3036
- noLingering,
3037
- latestUserText,
3038
- waitUntil,
3039
- // Function call detection
3040
- detectFuncCall,
3041
- // Tellask call-block detection (non-teammate targets; e.g. @clear_mind)
3042
- getNonTeammateCallingSections,
3043
- detectNonTeammateCall,
3044
- // Dialog creation
3045
- createDialog,
3046
- // Dialog selection
3047
- selectDialog,
3048
- selectDialogById,
3049
- getAllDialogs,
3050
- // Subdialog navigation
3051
- ensureSubdialogsLoaded,
3052
- openSubdialog,
3053
- openSubdialogAndWait,
3054
- waitForDialogSelected,
3055
- waitForCourseNavMatch,
3056
- waitForInputEnabledState,
3057
- waitForDialogIdle,
3058
- getSubdialogHierarchy,
3059
- navigateToParent,
3060
- navigateToParentAndWait,
3061
- selectDialogAndWait,
3062
- getCurrentDialogInfo,
3063
- getCurrentDialogTitle,
3064
- // Reminders widget
3065
- openReminders,
3066
- closeReminders,
3067
- getRemindersContent,
3068
- toggleReminders,
3069
- getRemindersCount,
3070
- getRemindersWidget,
3071
- getRemindersComponent,
3072
- waitForRemindersCount,
3073
- waitUntilReminderStable,
3074
- waitForWidgetStable,
3075
- waitForNoConsoleErrors,
3076
- // Q4H helpers
3077
- getQ4HCount,
3078
- getQ4HList,
3079
- getPendingQ4HIds,
3080
- selectQ4HQuestion,
3081
- getSelectedQ4HQuestionId,
3082
- answerQ4H,
3083
- // Console error tracking
3084
- checkConsoleErrors,
3085
- // Error state accessor for MCP Playwright
3086
- get __consoleErrors__() {
3087
- return [...__consoleErrors__];
3088
- },
3089
- // DOM observation utilities
3090
- domObs,
3091
- // Teammate tellasks
3092
- getPendingTeammateCalls,
3093
- getLatestTeammateCallSiteId,
3094
- waitForPendingTeammateCalls,
3095
- waitForTeammateCallSiteId,
3096
- waitForVisibleMessageCount,
3097
- waitForTeammateResponse,
3098
- // Scroll helpers
3099
- getConversationScrollArea,
3100
- isScrollAtBottom,
3101
- waitForGenBubbleAutoScroll,
3102
- };
3103
- window.__e2e__ = g;
3104
-
3105
- // Expose commonly used helpers as globals so ux-stories snippets can call them
3106
- const names = Object.keys(g);
3107
- for (const name of names) {
3108
- if (name === '__consoleErrors__') continue; // keep as window.__e2e__.__consoleErrors__ accessor only
3109
- const value = g[name];
3110
- const shouldExpose = typeof value === 'function' || name === 'sel' || name === 'DomindsUI';
3111
- if (!shouldExpose) continue;
3112
- if (Object.prototype.hasOwnProperty.call(window, name)) continue;
3113
- window[name] = value;
3114
- }
3115
-
3116
- return g;
3117
- }
3118
-
3119
- const __e2e__ = setGlobal();