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.
- package/dist/access-control.js +2 -2
- package/dist/agent-priming.js +826 -92
- package/dist/cli/read.js +406 -12
- package/dist/dialog.js +4 -0
- package/dist/docs/design.md +1 -0
- package/dist/docs/design.zh.md +1 -0
- package/dist/docs/dialog-system.md +12 -7
- package/dist/docs/dialog-system.zh.md +7 -3
- package/dist/docs/dominds-agent-priming.md +10 -1
- package/dist/docs/dominds-agent-priming.zh.md +9 -1
- package/dist/docs/dominds-terminology.md +8 -8
- package/dist/docs/fbr-implementation.md +77 -0
- package/dist/docs/fbr-implementation.zh.md +77 -0
- package/dist/docs/fbr.md +142 -141
- package/dist/docs/fbr.zh.md +129 -123
- package/dist/docs/keep-going.zh.md +162 -0
- package/dist/docs/showing-by-doing.md +208 -0
- package/dist/docs/showing-by-doing.zh.md +177 -0
- package/dist/docs/tellask-collab.md +250 -0
- package/dist/docs/tellask-collab.zh.md +254 -0
- package/dist/docs/txt-editing-tools.md +2 -2
- package/dist/docs/txt-editing-tools.zh.md +2 -2
- package/dist/llm/defaults.yaml +82 -4
- package/dist/llm/driver.js +280 -104
- package/dist/llm/gen/codex.js +49 -2
- package/dist/log.js +385 -30
- package/dist/mcp/supervisor.js +113 -40
- package/dist/minds/builtin/pangu/persona.zh.md +2 -2
- package/dist/minds/load.js +49 -284
- package/dist/minds/minds-i18n.js +2 -2
- package/dist/minds/promptdocs.js +263 -0
- package/dist/minds/system-prompt-parts.js +231 -0
- package/dist/minds/system-prompt.js +190 -223
- package/dist/persistence.js +66 -1
- package/dist/server/websocket-handler.js +14 -0
- package/dist/shared/diligence.js +40 -6
- package/dist/shared/utils/inter-dialog-format.js +3 -5
- package/dist/showing-by-doing.js +34 -31
- package/dist/snippets/README.en.md +3 -0
- package/dist/static/assets/{_baseUniq-C9vbtHF9.js → _baseUniq-C7IpU2Uk.js} +2 -2
- package/dist/static/assets/{_baseUniq-C9vbtHF9.js.map → _baseUniq-C7IpU2Uk.js.map} +1 -1
- package/dist/static/assets/{arc-hulXG01i.js → arc-1bhQqjON.js} +2 -2
- package/dist/static/assets/{arc-hulXG01i.js.map → arc-1bhQqjON.js.map} +1 -1
- package/dist/static/assets/{architectureDiagram-VXUJARFQ-DdLIAMT5.js → architectureDiagram-VXUJARFQ-CkEi1QpB.js} +6 -6
- package/dist/static/assets/{architectureDiagram-VXUJARFQ-DdLIAMT5.js.map → architectureDiagram-VXUJARFQ-CkEi1QpB.js.map} +1 -1
- package/dist/static/assets/{blockDiagram-VD42YOAC-DACsx66C.js → blockDiagram-VD42YOAC-DaBQ5-pY.js} +7 -7
- package/dist/static/assets/{blockDiagram-VD42YOAC-DACsx66C.js.map → blockDiagram-VD42YOAC-DaBQ5-pY.js.map} +1 -1
- package/dist/static/assets/{c4Diagram-YG6GDRKO-Cd5xZlLy.js → c4Diagram-YG6GDRKO-ChUgpgkP.js} +3 -3
- package/dist/static/assets/{c4Diagram-YG6GDRKO-Cd5xZlLy.js.map → c4Diagram-YG6GDRKO-ChUgpgkP.js.map} +1 -1
- package/dist/static/assets/{channel-NQehis0Z.js → channel-CxvmwllM.js} +2 -2
- package/dist/static/assets/{channel-NQehis0Z.js.map → channel-CxvmwllM.js.map} +1 -1
- package/dist/static/assets/{chunk-4BX2VUAB-DZDPl76b.js → chunk-4BX2VUAB-CKsrU2yk.js} +2 -2
- package/dist/static/assets/{chunk-4BX2VUAB-DZDPl76b.js.map → chunk-4BX2VUAB-CKsrU2yk.js.map} +1 -1
- package/dist/static/assets/{chunk-55IACEB6-CFSRDUbl.js → chunk-55IACEB6-BAau9SFt.js} +2 -2
- package/dist/static/assets/{chunk-55IACEB6-CFSRDUbl.js.map → chunk-55IACEB6-BAau9SFt.js.map} +1 -1
- package/dist/static/assets/{chunk-B4BG7PRW-BqQQ9M_z.js → chunk-B4BG7PRW--IiJ7W1m.js} +5 -5
- package/dist/static/assets/{chunk-B4BG7PRW-BqQQ9M_z.js.map → chunk-B4BG7PRW--IiJ7W1m.js.map} +1 -1
- package/dist/static/assets/{chunk-DI55MBZ5-FiFzz1Gh.js → chunk-DI55MBZ5-B83KrPQj.js} +4 -4
- package/dist/static/assets/{chunk-DI55MBZ5-FiFzz1Gh.js.map → chunk-DI55MBZ5-B83KrPQj.js.map} +1 -1
- package/dist/static/assets/{chunk-FMBD7UC4-DqqtCyWK.js → chunk-FMBD7UC4-BlDXzeza.js} +2 -2
- package/dist/static/assets/{chunk-FMBD7UC4-DqqtCyWK.js.map → chunk-FMBD7UC4-BlDXzeza.js.map} +1 -1
- package/dist/static/assets/{chunk-QN33PNHL-F0laQQ-J.js → chunk-QN33PNHL-B596W_v7.js} +2 -2
- package/dist/static/assets/{chunk-QN33PNHL-F0laQQ-J.js.map → chunk-QN33PNHL-B596W_v7.js.map} +1 -1
- package/dist/static/assets/{chunk-QZHKN3VN-CWhEZPaV.js → chunk-QZHKN3VN-UBBCxgBb.js} +2 -2
- package/dist/static/assets/{chunk-QZHKN3VN-CWhEZPaV.js.map → chunk-QZHKN3VN-UBBCxgBb.js.map} +1 -1
- package/dist/static/assets/{chunk-TZMSLE5B-Dx9cnwUy.js → chunk-TZMSLE5B-D-wCX2wJ.js} +2 -2
- package/dist/static/assets/{chunk-TZMSLE5B-Dx9cnwUy.js.map → chunk-TZMSLE5B-D-wCX2wJ.js.map} +1 -1
- package/dist/static/assets/{classDiagram-2ON5EDUG-Dp-dyEGy.js → classDiagram-2ON5EDUG-DvtmzPcu.js} +6 -6
- package/dist/static/assets/{classDiagram-2ON5EDUG-Dp-dyEGy.js.map → classDiagram-2ON5EDUG-DvtmzPcu.js.map} +1 -1
- package/dist/static/assets/{classDiagram-v2-WZHVMYZB-Dp-dyEGy.js → classDiagram-v2-WZHVMYZB-DvtmzPcu.js} +6 -6
- package/dist/static/assets/{classDiagram-v2-WZHVMYZB-Dp-dyEGy.js.map → classDiagram-v2-WZHVMYZB-DvtmzPcu.js.map} +1 -1
- package/dist/static/assets/{clone-C6mKvxs5.js → clone-DgJ0ZR-k.js} +2 -2
- package/dist/static/assets/{clone-C6mKvxs5.js.map → clone-DgJ0ZR-k.js.map} +1 -1
- package/dist/static/assets/{cose-bilkent-S5V4N54A-Dbwh3GoX.js → cose-bilkent-S5V4N54A-DXMyFQvy.js} +2 -2
- package/dist/static/assets/{cose-bilkent-S5V4N54A-Dbwh3GoX.js.map → cose-bilkent-S5V4N54A-DXMyFQvy.js.map} +1 -1
- package/dist/static/assets/{dagre-6UL2VRFP-BD_6e0Uk.js → dagre-6UL2VRFP-BdaUG-j_.js} +7 -7
- package/dist/static/assets/{dagre-6UL2VRFP-BD_6e0Uk.js.map → dagre-6UL2VRFP-BdaUG-j_.js.map} +1 -1
- package/dist/static/assets/{diagram-PSM6KHXK-BWt7Q59-.js → diagram-PSM6KHXK-NLiqKBzn.js} +7 -7
- package/dist/static/assets/{diagram-PSM6KHXK-BWt7Q59-.js.map → diagram-PSM6KHXK-NLiqKBzn.js.map} +1 -1
- package/dist/static/assets/{diagram-QEK2KX5R-D0BvBR_a.js → diagram-QEK2KX5R-D-0fyvY_.js} +6 -6
- package/dist/static/assets/{diagram-QEK2KX5R-D0BvBR_a.js.map → diagram-QEK2KX5R-D-0fyvY_.js.map} +1 -1
- package/dist/static/assets/{diagram-S2PKOQOG-D8uRdKXp.js → diagram-S2PKOQOG-BQ_FU59m.js} +6 -6
- package/dist/static/assets/{diagram-S2PKOQOG-D8uRdKXp.js.map → diagram-S2PKOQOG-BQ_FU59m.js.map} +1 -1
- package/dist/static/assets/{erDiagram-Q2GNP2WA-CQoifjFq.js → erDiagram-Q2GNP2WA-DyftKeuC.js} +5 -5
- package/dist/static/assets/{erDiagram-Q2GNP2WA-CQoifjFq.js.map → erDiagram-Q2GNP2WA-DyftKeuC.js.map} +1 -1
- package/dist/static/assets/{flowDiagram-NV44I4VS-CGhdeaG8.js → flowDiagram-NV44I4VS-9SGefONA.js} +6 -6
- package/dist/static/assets/{flowDiagram-NV44I4VS-CGhdeaG8.js.map → flowDiagram-NV44I4VS-9SGefONA.js.map} +1 -1
- package/dist/static/assets/{ganttDiagram-JELNMOA3-D8W0wb9H.js → ganttDiagram-JELNMOA3-k_WLhf-r.js} +3 -3
- package/dist/static/assets/{ganttDiagram-JELNMOA3-D8W0wb9H.js.map → ganttDiagram-JELNMOA3-k_WLhf-r.js.map} +1 -1
- package/dist/static/assets/{gitGraphDiagram-NY62KEGX-ChHni_jP.js → gitGraphDiagram-NY62KEGX-3eoLlCOY.js} +7 -7
- package/dist/static/assets/{gitGraphDiagram-NY62KEGX-ChHni_jP.js.map → gitGraphDiagram-NY62KEGX-3eoLlCOY.js.map} +1 -1
- package/dist/static/assets/{graph-BWoi_FgC.js → graph-vUevIs4s.js} +3 -3
- package/dist/static/assets/{graph-BWoi_FgC.js.map → graph-vUevIs4s.js.map} +1 -1
- package/dist/static/assets/{index-th_praGg.js → index-BNBG2CE1.js} +399 -68
- package/dist/static/assets/index-BNBG2CE1.js.map +1 -0
- package/dist/static/assets/{infoDiagram-WHAUD3N6-B_XKKZTV.js → infoDiagram-WHAUD3N6-CwEhVxkU.js} +5 -5
- package/dist/static/assets/{infoDiagram-WHAUD3N6-B_XKKZTV.js.map → infoDiagram-WHAUD3N6-CwEhVxkU.js.map} +1 -1
- package/dist/static/assets/{journeyDiagram-XKPGCS4Q-ChGuQ6T9.js → journeyDiagram-XKPGCS4Q-Dtdq4G4Q.js} +5 -5
- package/dist/static/assets/{journeyDiagram-XKPGCS4Q-ChGuQ6T9.js.map → journeyDiagram-XKPGCS4Q-Dtdq4G4Q.js.map} +1 -1
- package/dist/static/assets/{kanban-definition-3W4ZIXB7-BjWe623u.js → kanban-definition-3W4ZIXB7-Bli-AycJ.js} +3 -3
- package/dist/static/assets/{kanban-definition-3W4ZIXB7-BjWe623u.js.map → kanban-definition-3W4ZIXB7-Bli-AycJ.js.map} +1 -1
- package/dist/static/assets/{layout-BPyT310w.js → layout-CGlA8c09.js} +5 -5
- package/dist/static/assets/{layout-BPyT310w.js.map → layout-CGlA8c09.js.map} +1 -1
- package/dist/static/assets/{linear-xUsVjXWq.js → linear-Da2jDWL3.js} +2 -2
- package/dist/static/assets/{linear-xUsVjXWq.js.map → linear-Da2jDWL3.js.map} +1 -1
- package/dist/static/assets/{min-xFt7zeOd.js → min-Co741hTV.js} +3 -3
- package/dist/static/assets/{min-xFt7zeOd.js.map → min-Co741hTV.js.map} +1 -1
- package/dist/static/assets/{mindmap-definition-VGOIOE7T-DT_dvf2c.js → mindmap-definition-VGOIOE7T-DvkIjoq8.js} +4 -4
- package/dist/static/assets/{mindmap-definition-VGOIOE7T-DT_dvf2c.js.map → mindmap-definition-VGOIOE7T-DvkIjoq8.js.map} +1 -1
- package/dist/static/assets/{pieDiagram-ADFJNKIX-B1DQ-OaG.js → pieDiagram-ADFJNKIX-BGuGhTu8.js} +7 -7
- package/dist/static/assets/{pieDiagram-ADFJNKIX-B1DQ-OaG.js.map → pieDiagram-ADFJNKIX-BGuGhTu8.js.map} +1 -1
- package/dist/static/assets/{quadrantDiagram-AYHSOK5B-IHqyr3iT.js → quadrantDiagram-AYHSOK5B-DAZcrJMg.js} +3 -3
- package/dist/static/assets/{quadrantDiagram-AYHSOK5B-IHqyr3iT.js.map → quadrantDiagram-AYHSOK5B-DAZcrJMg.js.map} +1 -1
- package/dist/static/assets/{requirementDiagram-UZGBJVZJ-CKBpht7B.js → requirementDiagram-UZGBJVZJ-CXN0DxZs.js} +4 -4
- package/dist/static/assets/{requirementDiagram-UZGBJVZJ-CKBpht7B.js.map → requirementDiagram-UZGBJVZJ-CXN0DxZs.js.map} +1 -1
- package/dist/static/assets/{sankeyDiagram-TZEHDZUN-D2uGjv3i.js → sankeyDiagram-TZEHDZUN-B7-yAePZ.js} +2 -2
- package/dist/static/assets/{sankeyDiagram-TZEHDZUN-D2uGjv3i.js.map → sankeyDiagram-TZEHDZUN-B7-yAePZ.js.map} +1 -1
- package/dist/static/assets/{sequenceDiagram-WL72ISMW-wLFRhAKd.js → sequenceDiagram-WL72ISMW-DfBNY6h_.js} +4 -4
- package/dist/static/assets/{sequenceDiagram-WL72ISMW-wLFRhAKd.js.map → sequenceDiagram-WL72ISMW-DfBNY6h_.js.map} +1 -1
- package/dist/static/assets/{stateDiagram-FKZM4ZOC-BFGQTbx5.js → stateDiagram-FKZM4ZOC-BLo1xRVY.js} +9 -9
- package/dist/static/assets/{stateDiagram-FKZM4ZOC-BFGQTbx5.js.map → stateDiagram-FKZM4ZOC-BLo1xRVY.js.map} +1 -1
- package/dist/static/assets/{stateDiagram-v2-4FDKWEC3-DF7AjJuk.js → stateDiagram-v2-4FDKWEC3-Dq7MAD0I.js} +5 -5
- package/dist/static/assets/{stateDiagram-v2-4FDKWEC3-DF7AjJuk.js.map → stateDiagram-v2-4FDKWEC3-Dq7MAD0I.js.map} +1 -1
- package/dist/static/assets/{timeline-definition-IT6M3QCI-ChHFOb0o.js → timeline-definition-IT6M3QCI-ySWyBF3b.js} +3 -3
- package/dist/static/assets/{timeline-definition-IT6M3QCI-ChHFOb0o.js.map → timeline-definition-IT6M3QCI-ySWyBF3b.js.map} +1 -1
- package/dist/static/assets/{treemap-KMMF4GRG-BxaNvQU4.js → treemap-KMMF4GRG-DOp4sqOh.js} +4 -4
- package/dist/static/assets/{treemap-KMMF4GRG-BxaNvQU4.js.map → treemap-KMMF4GRG-DOp4sqOh.js.map} +1 -1
- package/dist/static/assets/{xychartDiagram-PRI3JC2R-CrNKeY_-.js → xychartDiagram-PRI3JC2R-vkmh67qb.js} +3 -3
- package/dist/static/assets/{xychartDiagram-PRI3JC2R-CrNKeY_-.js.map → xychartDiagram-PRI3JC2R-vkmh67qb.js.map} +1 -1
- package/dist/static/index.html +1 -1
- package/dist/team.js +29 -6
- package/dist/tool.js +56 -0
- package/dist/tools/builtins.js +4 -2
- package/dist/tools/context-health.js +7 -7
- package/dist/tools/os.js +267 -30
- package/dist/tools/pending-tellask-reminder.js +185 -0
- package/dist/tools/plan.js +1 -0
- package/dist/tools/ripgrep.js +145 -4
- package/dist/tools/shell-tools.js +21 -0
- package/dist/tools/team-mgmt.js +4 -4
- package/dist/tools/toolset-manual.js +74 -0
- package/dist/utils/task-doc.js +16 -16
- package/package.json +1 -1
- package/dist/minds/builtin/cmdr/persona.md +0 -3
- package/dist/minds/builtin/dijiang/knowledge.md +0 -287
- package/dist/minds/builtin/dijiang/persona.md +0 -7
- package/dist/static/assets/index-th_praGg.js.map +0 -1
- package/dist/static/testing/dom-observation-utils.js +0 -425
- 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();
|