taskchef 8.7.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +31 -158
  4. package/docs/mcp-app.md +21 -4
  5. package/docs/spec.md +38 -746
  6. package/docs/taskchef-next-storage.md +2 -2
  7. package/index.js +2 -99
  8. package/package.json +12 -49
  9. package/skills/delegate/SKILL.md +44 -0
  10. package/skills/delegate/agents/openai.yaml +4 -0
  11. package/src/codex-app.js +3 -152
  12. package/src/codex-session-scanner.js +2 -2
  13. package/src/file-lock.js +25 -0
  14. package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
  15. package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
  16. package/src/mcp-app/react/main.test.tsx +3 -3
  17. package/src/mcp-app/react/main.tsx +7 -7
  18. package/src/mcp-app.js +4 -4
  19. package/src/mcp.js +4 -542
  20. package/src/next-cache.js +2 -2
  21. package/src/next-github.js +3 -3
  22. package/src/next-notifications.js +2 -2
  23. package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
  24. package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
  25. package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
  26. package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
  27. package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
  28. package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
  29. package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
  30. package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
  31. package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
  32. package/src/{dashboard/react → ui}/presentation.ts +1 -1
  33. package/src/version.js +2 -1
  34. package/BACKLOG.md +0 -48
  35. package/assets/e2e-benchmark-example.json +0 -50
  36. package/assets/taskchef-dispatcher-instructions.md +0 -44
  37. package/bin/taskchef.js +0 -20
  38. package/docs/advanced-guide.md +0 -497
  39. package/docs/dashboard-lifecycle.md +0 -149
  40. package/docs/dashboard-ui.md +0 -40
  41. package/docs/firstmate-taskchef-comparison.md +0 -283
  42. package/docs/images/ccusage-token-consumption.png +0 -0
  43. package/docs/images/dashboard-identity.jpg +0 -0
  44. package/docs/images/dashboard-token-pending.jpg +0 -0
  45. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  46. package/docs/images/notification-event-snapshots.jpg +0 -0
  47. package/docs/images/readme-board.png +0 -0
  48. package/docs/images/readme-completed.png +0 -0
  49. package/docs/images/readme-list.png +0 -0
  50. package/docs/images/readme-needs-input.png +0 -0
  51. package/docs/images/readme-roles.png +0 -0
  52. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  53. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  54. package/docs/images/result-history-dashboard.jpg +0 -0
  55. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  56. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  57. package/docs/workflows.md +0 -476
  58. package/mcp/dashboard-session.js +0 -51
  59. package/scripts/benchmark-dispatch-prepare.js +0 -90
  60. package/scripts/e2e-benchmark.js +0 -206
  61. package/scripts/roles/resolve_roles.py +0 -293
  62. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  63. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  64. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  65. package/skills/taskchef-copilot/SKILL.md +0 -75
  66. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  67. package/skills/taskchef-dashboard/SKILL.md +0 -24
  68. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  69. package/skills/taskchef-delegate/SKILL.md +0 -102
  70. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  71. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  72. package/skills/taskchef-executor/SKILL.md +0 -154
  73. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  74. package/skills/taskchef-executor/references/compatibility.md +0 -22
  75. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  76. package/src/backups.js +0 -359
  77. package/src/cli.js +0 -733
  78. package/src/config-audit.js +0 -99
  79. package/src/config-mutations.js +0 -106
  80. package/src/dashboard/actions.js +0 -136
  81. package/src/dashboard/app.js +0 -1172
  82. package/src/dashboard/dist/app.js +0 -269
  83. package/src/dashboard/dist/app.js.map +0 -1
  84. package/src/dashboard/dist/index.html +0 -16
  85. package/src/dashboard/dist/styles.css +0 -1
  86. package/src/dashboard/index.html +0 -148
  87. package/src/dashboard/react/App.test.tsx +0 -346
  88. package/src/dashboard/react/App.tsx +0 -515
  89. package/src/dashboard/react/api.test.ts +0 -40
  90. package/src/dashboard/react/api.ts +0 -103
  91. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  92. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  93. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  94. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  95. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  96. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  97. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  98. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  99. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  100. package/src/dashboard/react/index.html +0 -15
  101. package/src/dashboard/react/main.tsx +0 -12
  102. package/src/dashboard/react/overlay-state.test.ts +0 -110
  103. package/src/dashboard/react/overlay-state.ts +0 -60
  104. package/src/dashboard/react/presentation.test.tsx +0 -571
  105. package/src/dashboard/styles.css +0 -261
  106. package/src/dashboard-control.js +0 -34
  107. package/src/dashboard-manager.js +0 -339
  108. package/src/dashboard-session-process.js +0 -88
  109. package/src/dashboard-session.js +0 -86
  110. package/src/dashboard.js +0 -1303
  111. package/src/delegation.js +0 -277
  112. package/src/github.js +0 -107
  113. package/src/model-roles.js +0 -70
  114. package/src/recorded-usage-tracker.js +0 -92
  115. package/src/usage-tracker.js +0 -650
  116. package/src/usage.js +0 -367
  117. package/src/workspace-path.js +0 -48
  118. package/src/workspace.js +0 -3629
  119. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  120. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  142. /package/src/{dashboard → ui}/github-links.js +0 -0
  143. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  145. /package/src/{dashboard → ui}/state.js +0 -0
  146. /package/src/{dashboard/react → ui}/styles.css +0 -0
  147. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  148. /package/src/{dashboard → ui}/time.js +0 -0
  149. /package/src/{dashboard/react → ui}/types.ts +0 -0
@@ -1,261 +0,0 @@
1
- :root {
2
- color-scheme: light;
3
- --background: #f5f4f0;
4
- --surface: #ffffff;
5
- --surface-muted: #eeece6;
6
- --text: #1d211f;
7
- --muted: #626964;
8
- --border: #d8d7d1;
9
- --accent: #215f4a;
10
- --accent-soft: #dcebe4;
11
- --danger: #8b3a35;
12
- --warning: #8a5c17;
13
- --shadow: 0 12px 30px rgb(24 33 29 / 10%);
14
- --control-height: 38px;
15
- --control-padding-inline: 11px;
16
- --control-radius: 7px;
17
- --control-font-size: 0.86rem;
18
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
19
- }
20
-
21
- * { box-sizing: border-box; }
22
-
23
- body {
24
- margin: 0;
25
- background: var(--background);
26
- color: var(--text);
27
- line-height: 1.45;
28
- }
29
-
30
- button, select { font: inherit; }
31
- button { color: inherit; }
32
-
33
- .site-header, main {
34
- width: min(1080px, calc(100% - 40px));
35
- margin-inline: auto;
36
- }
37
-
38
- .site-header {
39
- display: flex;
40
- align-items: flex-end;
41
- justify-content: space-between;
42
- gap: 20px;
43
- padding-block: 40px 24px;
44
- border-bottom: 1px solid var(--border);
45
- }
46
-
47
- h1, h2, h3, p { margin-top: 0; }
48
- h1 { margin-bottom: 5px; font-size: clamp(1.9rem, 4.5vw, 3.1rem); line-height: 1; letter-spacing: -0.04em; }
49
- h2 { line-height: 1.2; }
50
- h3 { margin-bottom: 8px; font-size: 0.84rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
51
-
52
- .title-row { display: flex; align-items: center; gap: clamp(9px, 1.8vw, 14px); margin-bottom: 5px; }
53
- .title-row h1 { margin-bottom: 0; }
54
- .dashboard-icon { display: block; flex: none; width: clamp(36px, 5.5vw, 44px); aspect-ratio: 1; }
55
- .dashboard-icon img { display: block; width: 100%; height: 100%; object-fit: contain; }
56
-
57
- .eyebrow { margin-bottom: 8px; color: var(--accent); font-size: 0.76rem; font-weight: 750; letter-spacing: 0.15em; text-transform: uppercase; }
58
- .dashboard-version { margin-inline-start: 0.45em; color: var(--muted); font-size: 0.72rem; letter-spacing: 0.04em; text-transform: none; }
59
- .subtitle, .task-project, time, .timestamp-missing { color: var(--muted); }
60
- .subtitle { margin-bottom: 0; font-size: 0.9rem; }
61
-
62
- .connection { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 0.82rem; }
63
- .connection-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--warning); }
64
- .connection-dot.connected { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
65
-
66
- main { padding-block: 24px 72px; }
67
-
68
- .toolbar {
69
- display: grid;
70
- gap: 10px;
71
- margin-bottom: 18px;
72
- }
73
-
74
- .toolbar-primary {
75
- display: grid;
76
- grid-template-columns: repeat(2, minmax(180px, 240px));
77
- align-items: end;
78
- gap: 14px;
79
- }
80
-
81
- label { display: grid; gap: 5px; color: var(--muted); font-size: 0.76rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
82
- select { min-width: 180px; min-height: 36px; padding: 7px 32px 7px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); color: var(--text); }
83
- .toolbar-primary select { width: 100%; }
84
- .status-filter-fieldset { width: 100%; min-width: 0; margin: 0; padding: 0; border: 0; }
85
- .status-filter-fieldset legend { margin-bottom: 5px; padding: 0; color: var(--muted); font-size: 0.76rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
86
- .status-filter-options { display: flex; flex-wrap: wrap; gap: 5px; }
87
- .status-filter-option { position: relative; display: block; color: var(--text); font-size: 0.78rem; letter-spacing: 0; text-transform: none; cursor: pointer; }
88
- .status-filter-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
89
- .status-filter-option span { display: flex; align-items: center; min-height: 34px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); white-space: nowrap; }
90
- .status-filter-option:hover span { border-color: var(--accent); }
91
- .status-filter-option:has(input:checked) span { border-color: var(--accent); background: var(--accent); color: white; box-shadow: 0 1px 2px rgb(24 33 29 / 16%); }
92
- .status-filter-option:has(input:focus-visible) span { outline: 3px solid var(--accent); outline-offset: 3px; }
93
- .task-count { justify-self: end; margin: 0; color: var(--muted); font-size: 0.82rem; }
94
-
95
- .task-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
96
- .task-card { padding: 16px 18px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); box-shadow: 0 1px 0 rgb(0 0 0 / 2%); }
97
- .task-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
98
- .task-title { padding: 0; border: 0; background: none; font-size: 1rem; font-weight: 720; line-height: 1.35; text-align: left; cursor: pointer; }
99
- .task-title:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
100
- .task-project { margin: 2px 0 10px; font-size: 0.8rem; }
101
- .task-title, .task-summary { overflow-wrap: anywhere; }
102
- .task-summary { display: grid; max-width: 82ch; margin-bottom: 12px; gap: 2px; font-size: 0.9rem; line-height: 1.45; }
103
- .task-summary strong { margin-top: 4px; color: var(--muted); font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; }
104
- .github-links { display: grid; gap: 7px; margin: 0 0 18px; }
105
- .github-links[hidden] { display: none; }
106
- .github-links-compact { margin: -2px 0 12px; gap: 5px; }
107
- .github-links-group { display: flex; min-width: 0; flex-wrap: wrap; gap: inherit; }
108
- .github-link { color: var(--accent); font-weight: 700; text-decoration-thickness: 1px; text-underline-offset: 3px; overflow-wrap: anywhere; }
109
- .github-link-compact { padding: 2px 7px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-muted); font-size: 0.72rem; line-height: 1.35; text-decoration: none; }
110
- .github-link:hover { text-decoration-thickness: 2px; }
111
- .github-link-compact:hover { border-color: var(--accent); background: var(--accent-soft); }
112
- .github-links-more { align-self: center; color: var(--muted); font-size: 0.75rem; font-weight: 700; }
113
- .github-reference-ambiguous { border-bottom: 2px dotted var(--warning); background: color-mix(in srgb, var(--warning) 12%, transparent); cursor: help; }
114
- time, .timestamp-missing { font-size: 0.75rem; }
115
- .timestamp-toggle { padding: 2px 0; border: 0; background: none; cursor: pointer; text-align: left; }
116
- .timestamp-toggle:hover time { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
117
- .task-footer { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
118
- .task-open { flex: none; }
119
- .primary-button.task-action, .secondary-button.task-action { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: var(--control-height); padding: 0 var(--control-padding-inline); font-size: var(--control-font-size); line-height: 1; white-space: nowrap; }
120
- .codex-icon { display: block; flex: none; width: 18px; height: 18px; }
121
- .codex-icon img { display: block; width: 100%; height: 100%; object-fit: contain; }
122
-
123
- .status { flex: none; padding: 3px 8px; border-radius: 999px; background: var(--surface-muted); color: var(--muted); font-size: 0.68rem; font-weight: 750; line-height: 1.4; text-transform: capitalize; }
124
- .status-completed { background: var(--accent-soft); color: var(--accent); }
125
- .status-failed { background: #f3dfdd; color: var(--danger); }
126
- .status-needs_input { background: #f4e9ce; color: var(--warning); }
127
- .status-interrupted { background: #e8e5df; color: #625d54; }
128
-
129
- .empty-state { padding: 60px 20px; border: 1px dashed var(--border); border-radius: 10px; text-align: center; color: var(--muted); }
130
- .empty-state h2 { margin-bottom: 6px; color: var(--text); }
131
-
132
- .dashboard-message { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; padding: 8px 8px 8px 14px; border: 1px solid #d8c58e; border-radius: 7px; background: #fff7de; color: #604613; }
133
- .dashboard-message[hidden] { display: none; }
134
- .dashboard-message span { flex: 1; }
135
- .dashboard-message .icon-button { flex: none; padding: 4px 7px; color: inherit; }
136
-
137
- .notifications { position: fixed; inset: auto 20px 20px auto; width: min(380px, calc(100vw - 40px)); margin: 0; padding: 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
138
- .notifications-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
139
- .toast-list { display: grid; gap: 7px; max-height: 50vh; overflow: auto; }
140
- .toast { display: flex; align-items: flex-start; gap: 8px; padding: 10px; border-radius: 7px; background: var(--surface-muted); }
141
- .toast-content { display: grid; flex: 1; gap: 2px; padding: 0; border: 0; background: none; text-align: left; cursor: pointer; }
142
- .toast-content span { color: var(--muted); font-size: 0.85rem; }
143
- .toast-content .toast-summary { color: var(--text); }
144
- .toast-content .toast-metadata { font-size: 0.75rem; }
145
- .toast-missing { border: 1px dashed var(--border); }
146
-
147
- .text-button, .icon-button { border: 0; background: none; cursor: pointer; }
148
- .text-button { color: var(--accent); font-size: 0.82rem; font-weight: 700; }
149
- .icon-button { padding: 0 4px; color: var(--muted); font-size: 1.4rem; line-height: 1; }
150
-
151
- dialog { width: min(760px, calc(100vw - 32px)); max-height: min(82vh, 900px); padding: 28px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
152
- dialog::backdrop { background: rgb(18 23 21 / 50%); backdrop-filter: blur(2px); }
153
- .dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
154
- .dialog-header h2 { margin-bottom: 12px; font-size: 1.6rem; }
155
- .dialog-actions { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; }
156
- .more-task-actions { width: var(--control-height); min-width: var(--control-height); padding-inline: 0; font-size: 1.2rem; line-height: 1; }
157
- .manual-transition-panel, .manual-transition-controls { display: contents; }
158
- .manual-transition-panel[hidden] { display: none; }
159
- .manual-transition-error, .manual-transition-pending { flex-basis: 100%; margin: 0; }
160
- .manual-transition-controls > button[hidden] { display: none; }
161
- .manual-transition-error { color: var(--danger); font-size: 0.86rem; }
162
- .manual-transition-pending { color: var(--muted); font-size: 0.86rem; }
163
- .primary-button, .secondary-button, .danger-button { display: inline-flex; align-items: center; justify-content: center; min-height: var(--control-height); padding: 0 var(--control-padding-inline); border-radius: var(--control-radius); font-size: var(--control-font-size); font-weight: 700; line-height: 1; cursor: pointer; }
164
- .primary-button { border: 1px solid var(--accent); background: var(--accent); color: white; }
165
- .secondary-button { border: 1px solid var(--border); background: var(--surface); }
166
- .danger-button { border: 1px solid var(--danger); background: var(--surface); color: var(--danger); }
167
- .primary-button:disabled, .secondary-button:disabled, .danger-button:disabled { opacity: 0.5; cursor: not-allowed; }
168
- dialog section + section { margin-top: 24px; }
169
- .preserve-lines { white-space: pre-wrap; }
170
- .result-history { display: grid; gap: 10px; }
171
- .result-history-item { padding: 13px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-muted); }
172
- .result-history-latest { border-color: var(--accent); background: var(--accent-soft); box-shadow: inset 3px 0 var(--accent); }
173
- .result-history-notified { outline: 3px solid color-mix(in srgb, var(--accent) 35%, transparent); outline-offset: 2px; }
174
- .result-history-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 9px; }
175
- .result-history-header .timestamp-toggle { margin-left: auto; }
176
- .result-history-source { color: var(--muted); font-size: 0.72rem; font-weight: 700; }
177
- .result-history-item p { margin-bottom: 7px; }
178
- .result-history-item p:last-child { margin-bottom: 0; }
179
- .result-history-item h4 { margin: 10px 0 3px; color: var(--muted); font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase; }
180
- .result-history-turn, .result-history-empty { color: var(--muted); font-size: 0.78rem; overflow-wrap: anywhere; }
181
- .result-history-empty { margin: 0; }
182
- .usage-panel { margin-top: 8px; }
183
- .usage-summary { display: grid; gap: 3px; margin: 10px 0 4px; color: var(--muted); font-size: 0.8rem; }
184
- .usage-summary strong { color: var(--text); font-size: 0.9rem; }
185
- .usage-pending, .usage-calculating { display: flex; align-items: center; }
186
- .text-shimmer {
187
- color: var(--muted);
188
- display: inline-block;
189
- width: fit-content;
190
- max-width: 100%;
191
- justify-self: start;
192
- vertical-align: top;
193
- background: linear-gradient(100deg, var(--muted) 48%, var(--text) 50%, var(--muted) 52%);
194
- background-size: 250% 100%;
195
- background-position: 100% 0;
196
- background-clip: text;
197
- -webkit-background-clip: text;
198
- -webkit-text-fill-color: transparent;
199
- animation: text-shimmer-sweep 6.4s linear infinite;
200
- }
201
- @keyframes text-shimmer-sweep {
202
- 0% { background-position: 100% 0; }
203
- 72%, 100% { background-position: -100% 0; }
204
- }
205
- @media (prefers-reduced-motion: reduce) {
206
- .text-shimmer { background: none; animation: none; -webkit-text-fill-color: currentcolor; }
207
- }
208
- pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius: 7px; background: var(--surface-muted); white-space: pre-wrap; overflow-wrap: anywhere; font: 0.86rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
209
- .metadata { display: grid; grid-template-columns: minmax(100px, 150px) 1fr; margin: 0; font-size: 0.88rem; }
210
- .metadata dt, .metadata dd { padding: 7px 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
211
- .metadata dt { color: var(--muted); }
212
- .metadata dd { margin: 0; }
213
-
214
- .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
215
- :focus-visible { outline: 3px solid rgb(33 95 74 / 35%); outline-offset: 3px; }
216
-
217
- @media (max-width: 650px) {
218
- :root { --control-height: 40px; }
219
- .site-header { align-items: flex-start; gap: 14px; padding-top: 28px; }
220
- .title-row { align-items: flex-start; }
221
- .toolbar-primary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
222
- .toolbar-primary label, .toolbar-primary select { width: 100%; min-width: 0; }
223
- .toolbar .status-filter-option { width: auto; }
224
- .task-count { justify-self: start; }
225
- .task-heading { align-items: flex-start; }
226
- .task-title, .timestamp-toggle { display: flex; align-items: center; min-height: 36px; }
227
- .task-footer { align-items: flex-start; flex-direction: column; }
228
- .github-links { max-width: 100%; }
229
- .github-link-compact { padding-block: 5px; }
230
- .task-open { align-self: flex-start; }
231
- select { min-height: 40px; }
232
- .status-filter-option span { min-height: 38px; }
233
- .dialog-actions { align-items: center; flex-flow: row wrap; }
234
- .metadata { grid-template-columns: 1fr; }
235
- .metadata dt { padding-bottom: 0; border-bottom: 0; }
236
- }
237
-
238
- @media (max-width: 480px) {
239
- .toolbar-primary { grid-template-columns: minmax(0, 1fr); }
240
- }
241
-
242
- @media (prefers-color-scheme: dark) {
243
- :root {
244
- color-scheme: dark;
245
- --background: #181b19;
246
- --surface: #222624;
247
- --surface-muted: #2d322f;
248
- --text: #edf1ee;
249
- --muted: #aeb7b1;
250
- --border: #3d4440;
251
- --accent: #7dc1a4;
252
- --accent-soft: #244436;
253
- --danger: #ef9991;
254
- --warning: #e4bd77;
255
- }
256
- .dashboard-message { border-color: #66592e; background: #38321d; color: #f1d98c; }
257
- .status-filter-option:has(input:checked) span { color: var(--background); }
258
- .status-failed { background: #4e2927; }
259
- .status-needs_input { background: #4c3b20; }
260
- .status-interrupted { background: #37332f; color: #d6d0c7; }
261
- }
@@ -1,34 +0,0 @@
1
- export const DASHBOARD_CONTROL_SHUTDOWN_PATH = "/api/control/shutdown";
2
- export const DASHBOARD_CONTROL_SESSION_PATH = "/api/control/session";
3
- export const DASHBOARD_CONTROL_HEADER = "x-taskchef-control";
4
-
5
- const DASHBOARD_IDENTITY_KEYS = Object.freeze([
6
- "launcher",
7
- "schemaVersion",
8
- "serverVersion",
9
- "service",
10
- "taskchefVersion",
11
- "workspace",
12
- ]);
13
-
14
- export function exactDashboardIdentity(value, expected) {
15
- if (!value || typeof value !== "object" || Array.isArray(value)
16
- || !expected || typeof expected !== "object" || Array.isArray(expected)) return false;
17
- const valueKeys = Object.keys(value).sort();
18
- const expectedKeys = Object.keys(expected).sort();
19
- return valueKeys.length === DASHBOARD_IDENTITY_KEYS.length
20
- && expectedKeys.length === DASHBOARD_IDENTITY_KEYS.length
21
- && DASHBOARD_IDENTITY_KEYS.every((key, index) => valueKeys[index] === key
22
- && expectedKeys[index] === key && value[key] === expected[key]);
23
- }
24
-
25
- export function validDashboardControlRequest(request, {
26
- authority,
27
- origin,
28
- action,
29
- } = {}) {
30
- return request.headers.host === authority
31
- && request.headers.origin === origin
32
- && request.headers[DASHBOARD_CONTROL_HEADER] === action
33
- && request.headers["content-type"] === "application/json";
34
- }
@@ -1,339 +0,0 @@
1
- import http from "node:http";
2
- import { spawn } from "node:child_process";
3
- import { realpath } from "node:fs/promises";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- import { DASHBOARD_HEALTH_MAX_BYTES, DASHBOARD_HEALTH_PATH, dashboardAuthority } from "./dashboard.js";
8
- import { DASHBOARD_CONTROL_HEADER, DASHBOARD_CONTROL_SESSION_PATH,
9
- DASHBOARD_CONTROL_SHUTDOWN_PATH, exactDashboardIdentity } from "./dashboard-control.js";
10
- import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
11
-
12
- const DEFAULT_HOST = "127.0.0.1";
13
- const DEFAULT_PORT = 3210;
14
- const HEALTH_TIMEOUT_MS = 750;
15
- const SESSION_START_ATTEMPTS = 80;
16
- const SESSION_START_INTERVAL_MS = 25;
17
- const SESSION_READY_TIMEOUT_MS = 15_000;
18
- const SHUTDOWN_TIMEOUT_MS = 2_000;
19
- const MAX_VERSION_REPLACEMENT_ATTEMPTS = 8;
20
- const SESSION_PROCESS_PATH = fileURLToPath(new URL("../mcp/dashboard-session.js", import.meta.url));
21
-
22
- export function launchDashboardSession({ workspace, host, port, sessionPid,
23
- processPath = process.execPath, spawnProcess = spawn,
24
- readyTimeoutMs = SESSION_READY_TIMEOUT_MS } = {}) {
25
- return new Promise((resolve, reject) => {
26
- const childEnvironment = {
27
- ...process.env,
28
- TASKCHEF_DASHBOARD_WORKSPACE: workspace,
29
- TASKCHEF_DASHBOARD_HOST: host,
30
- TASKCHEF_DASHBOARD_PORT: String(port),
31
- TASKCHEF_DASHBOARD_SESSION_PID: String(sessionPid),
32
- };
33
- delete childEnvironment.TASKCHEF_DASHBOARD_SECRET;
34
- delete childEnvironment.TASKCHEF_DASHBOARD_SESSION_PIDS;
35
- const child = spawnProcess(processPath, [SESSION_PROCESS_PATH], {
36
- detached: true,
37
- stdio: ["ignore", "ignore", "ignore", "ipc"],
38
- env: childEnvironment,
39
- });
40
- let settled = false;
41
- const finish = (callback, value, { disconnect = true } = {}) => {
42
- if (settled) return;
43
- settled = true;
44
- clearTimeout(timer);
45
- child.off("error", onError);
46
- child.off("exit", onExit);
47
- child.off("message", onMessage);
48
- child.on("error", () => {});
49
- if (disconnect && child.connected) child.disconnect();
50
- child.unref();
51
- callback(value);
52
- };
53
- const onError = (error) => finish(reject, error);
54
- const onExit = (code) => {
55
- const error = new Error("TaskChef dashboard session exited before becoming ready");
56
- error.code = code === 0 ? "TASKCHEF_DASHBOARD_START_EXIT" : "TASKCHEF_DASHBOARD_START_FAILED";
57
- finish(reject, error);
58
- };
59
- const onMessage = (message) => {
60
- if (message?.type === "ready" && message.port === port) {
61
- finish(resolve, { pid: child.pid, port: message.port });
62
- } else if (message?.type === "error") {
63
- const error = new Error("TaskChef dashboard session failed before becoming ready");
64
- error.code = typeof message.code === "string" ? message.code : "TASKCHEF_DASHBOARD_START_FAILED";
65
- finish(reject, error);
66
- }
67
- };
68
- const timer = setTimeout(() => {
69
- const error = new Error("TaskChef dashboard session readiness timed out");
70
- error.code = "TASKCHEF_DASHBOARD_START_TIMEOUT";
71
- finish(reject, error, { disconnect: false });
72
- if (child.connected) {
73
- try { child.send({ type: "cancel" }, () => { if (child.connected) child.disconnect(); }); }
74
- catch { if (child.connected) child.disconnect(); }
75
- }
76
- }, readyTimeoutMs);
77
- child.once("error", onError);
78
- child.once("exit", onExit);
79
- child.on("message", onMessage);
80
- });
81
- }
82
-
83
- function expectedIdentity(workspace, taskchefVersion, serverVersion, launcher) {
84
- return { schemaVersion: 1, service: "taskchef-dashboard", taskchefVersion,
85
- serverVersion, workspace, launcher };
86
- }
87
-
88
- function isRecognizedDashboardIdentity(value, workspace) {
89
- return value?.schemaVersion === 1 && value?.service === "taskchef-dashboard"
90
- && typeof value?.taskchefVersion === "string" && typeof value?.serverVersion === "string"
91
- && value?.workspace === workspace && new Set(["mcp", "session", "standalone"]).has(value?.launcher)
92
- && exactDashboardIdentity(value, expectedIdentity(workspace, value.taskchefVersion,
93
- value.serverVersion, value.launcher));
94
- }
95
-
96
- function listenerConflict(url, detail, { staleTaskchefVersion } = {}) {
97
- const error = new Error(`TaskChef dashboard port conflict at ${url} ${detail} `
98
- + "Stop that listener or choose another port for the foreground dashboard CLI; TaskChef will not terminate it.");
99
- error.code = "TASKCHEF_DASHBOARD_CONFLICT";
100
- if (staleTaskchefVersion) error.staleTaskchefVersion = staleTaskchefVersion;
101
- return error;
102
- }
103
-
104
- export function priorCompatibleVersion(candidate, current) {
105
- const parse = (value) => {
106
- const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
107
- if (!match) return null;
108
- const prerelease = match[4]?.split(".") ?? null;
109
- if (prerelease?.some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith("0"))) return null;
110
- return { core: match.slice(1, 4).map(BigInt), prerelease };
111
- };
112
- const left = parse(candidate); const right = parse(current);
113
- if (!left || !right) return false;
114
- for (let index = 0; index < left.core.length; index += 1) {
115
- if (left.core[index] !== right.core[index]) return left.core[index] < right.core[index];
116
- }
117
- if (left.prerelease === null || right.prerelease === null) return left.prerelease !== null && right.prerelease === null;
118
- for (let index = 0; index < Math.max(left.prerelease.length, right.prerelease.length); index += 1) {
119
- const l = left.prerelease[index]; const r = right.prerelease[index];
120
- if (l === undefined || r === undefined) return l === undefined;
121
- if (l === r) continue;
122
- const ln = /^\d+$/.test(l); const rn = /^\d+$/.test(r);
123
- if (ln && rn) return BigInt(l) < BigInt(r);
124
- if (ln !== rn) return ln;
125
- return l < r;
126
- }
127
- return false;
128
- }
129
-
130
- export function requestDashboardJson({ host, port, path: requestPath, action, body, timeoutMs }) {
131
- return new Promise((resolve, reject) => {
132
- let settled = false;
133
- let deadline;
134
- const finish = (error, value) => {
135
- if (settled) return;
136
- settled = true;
137
- clearTimeout(deadline);
138
- if (error) reject(error); else resolve(value);
139
- };
140
- const encoded = Buffer.from(`${JSON.stringify(body ?? {})}\n`);
141
- const authority = dashboardAuthority(host, port);
142
- const request = http.request({ host, port, path: requestPath, method: "POST", headers: {
143
- Accept: "application/json", Host: authority, Origin: `http://${authority}`,
144
- "Content-Type": "application/json", "Content-Length": encoded.length,
145
- [DASHBOARD_CONTROL_HEADER]: action,
146
- } }, (response) => {
147
- const chunks = []; let total = 0;
148
- response.on("data", (chunk) => {
149
- total += chunk.length;
150
- if (total > DASHBOARD_HEALTH_MAX_BYTES) {
151
- const error = new Error("dashboard control response exceeds the identity limit");
152
- finish(error); request.destroy(error);
153
- }
154
- else chunks.push(chunk);
155
- });
156
- response.on("end", () => {
157
- try { finish(null, { statusCode: response.statusCode,
158
- value: JSON.parse(Buffer.concat(chunks, total).toString("utf8")) }); }
159
- catch { finish(new Error("dashboard control returned invalid JSON")); }
160
- });
161
- const interrupted = () => finish(new Error("dashboard control response was interrupted"));
162
- response.on("aborted", interrupted);
163
- response.on("error", interrupted);
164
- response.on("close", () => { if (!response.complete) interrupted(); });
165
- });
166
- deadline = setTimeout(() => {
167
- const error = new Error("dashboard control request timed out");
168
- finish(error); request.destroy(error);
169
- }, timeoutMs);
170
- request.on("error", (error) => finish(error));
171
- request.end(encoded);
172
- });
173
- }
174
-
175
- export function readDashboardIdentity({ host = DEFAULT_HOST, port = DEFAULT_PORT,
176
- maximumBytes = DASHBOARD_HEALTH_MAX_BYTES, timeoutMs = HEALTH_TIMEOUT_MS,
177
- deadlineAt = Date.now() + timeoutMs } = {}) {
178
- return new Promise((resolve, reject) => {
179
- let settled = false;
180
- const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(deadline);
181
- if (error) reject(error); else resolve(value); };
182
- const request = http.get({ host, port, path: DASHBOARD_HEALTH_PATH,
183
- headers: { Accept: "application/json", Host: dashboardAuthority(host, port) } }, (response) => {
184
- const chunks = []; let total = 0;
185
- response.on("data", (chunk) => { total += chunk.length;
186
- if (total > maximumBytes) { const error = new Error("dashboard health response exceeds the identity limit");
187
- finish(error); request.destroy(error); } else chunks.push(chunk); });
188
- response.on("end", () => {
189
- if (response.statusCode === 503 && deadlineAt - Date.now() > 10) {
190
- settled = true; clearTimeout(deadline); const remaining = deadlineAt - Date.now();
191
- setTimeout(() => readDashboardIdentity({ host, port, maximumBytes,
192
- timeoutMs: Math.max(1, remaining), deadlineAt }).then(resolve, reject), 10); return;
193
- }
194
- if (response.statusCode !== 200) { finish(new Error(`dashboard health returned HTTP ${response.statusCode}`)); return; }
195
- try { finish(null, JSON.parse(Buffer.concat(chunks, total).toString("utf8"))); }
196
- catch { finish(new Error("dashboard health returned invalid JSON")); }
197
- });
198
- });
199
- const deadline = setTimeout(() => { const error = new Error("dashboard health request timed out");
200
- finish(error); request.destroy(error); }, timeoutMs);
201
- request.on("error", (error) => finish(error));
202
- });
203
- }
204
-
205
- function listenerAbsent(error) { return error?.code === "ECONNREFUSED" || error?.code === "EHOSTUNREACH"; }
206
-
207
- export function createDashboardManager({ workspace, host = DEFAULT_HOST, port = DEFAULT_PORT,
208
- taskchefVersion = TASKCHEF_VERSION, serverVersion = DASHBOARD_SERVER_VERSION,
209
- launcher = "session", sessionPid = process.ppid, launchSession = launchDashboardSession,
210
- readIdentity = readDashboardIdentity, requestJson = requestDashboardJson,
211
- shutdownTimeoutMs = SHUTDOWN_TIMEOUT_MS } = {}) {
212
- if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("dashboard manager port must be an integer from 1 to 65535");
213
- if (!Number.isFinite(shutdownTimeoutMs) || shutdownTimeoutMs <= 0) throw new Error("dashboard shutdown timeout must be positive");
214
- let canonicalWorkspace; let ensurePromise = null; let closePromise = null;
215
- const url = `http://${dashboardAuthority(host, port)}/`;
216
- const expected = () => expectedIdentity(canonicalWorkspace, taskchefVersion, serverVersion, launcher);
217
- const publicResult = (action, observedLauncher = launcher) => ({
218
- action,
219
- launcher: observedLauncher,
220
- url,
221
- workspace: canonicalWorkspace,
222
- taskchefVersion,
223
- serverVersion,
224
- });
225
-
226
- const registerAndVerify = async (identity) => {
227
- const registration = await requestJson({ host, port, path: DASHBOARD_CONTROL_SESSION_PATH,
228
- action: "session", body: { pid: sessionPid, expectedIdentity: identity },
229
- timeoutMs: HEALTH_TIMEOUT_MS }).catch(() => null);
230
- if (registration?.statusCode !== 200 || registration.value?.accepted !== true) return false;
231
- const verified = await readIdentity({ host, port, timeoutMs: HEALTH_TIMEOUT_MS }).catch(() => null);
232
- return exactDashboardIdentity(verified, identity);
233
- };
234
- const stopOlder = async (identity) => {
235
- const shutdown = await requestJson({ host, port, path: DASHBOARD_CONTROL_SHUTDOWN_PATH,
236
- action: "shutdown", body: { reason: "version-replacement", expectedIdentity: identity },
237
- timeoutMs: HEALTH_TIMEOUT_MS }).catch(() => null);
238
- if (shutdown?.statusCode !== 202 || shutdown.value?.accepted !== true) {
239
- const live = await readIdentity({ host, port, timeoutMs: HEALTH_TIMEOUT_MS }).catch((error) => {
240
- if (listenerAbsent(error) || error?.code === "ECONNRESET") return null;
241
- throw listenerConflict(url, "changed to an unexpected listener before graceful restart.");
242
- });
243
- if (live === null) return;
244
- if (!exactDashboardIdentity(live, identity)) {
245
- return probe();
246
- }
247
- throw listenerConflict(url, `is a recognized older TaskChef ${identity.taskchefVersion} listener that refused graceful shutdown.`,
248
- { staleTaskchefVersion: identity.taskchefVersion });
249
- }
250
- const deadline = Date.now() + shutdownTimeoutMs;
251
- do {
252
- try {
253
- const live = await readIdentity({ host, port, timeoutMs: Math.min(100, shutdownTimeoutMs) });
254
- if (!exactDashboardIdentity(live, identity)) {
255
- return probe();
256
- }
257
- } catch (error) {
258
- if (listenerAbsent(error) || error?.code === "ECONNRESET") return;
259
- if (error?.code === "TASKCHEF_DASHBOARD_CONFLICT") throw error;
260
- }
261
- await new Promise((resolve) => setTimeout(resolve, SESSION_START_INTERVAL_MS));
262
- } while (Date.now() < deadline);
263
- throw listenerConflict(url, `accepted graceful shutdown but did not release the port within ${shutdownTimeoutMs}ms.`,
264
- { staleTaskchefVersion: identity.taskchefVersion });
265
- };
266
- const probe = async () => {
267
- let identity;
268
- try { identity = await readIdentity({ host, port }); }
269
- catch (error) {
270
- if (listenerAbsent(error) || error?.code === "ECONNRESET") return false;
271
- throw listenerConflict(url, `is occupied but did not return a compatible identity (${error.message}).`);
272
- }
273
- const exactCurrent = isRecognizedDashboardIdentity(identity, canonicalWorkspace)
274
- && identity.taskchefVersion === taskchefVersion
275
- && identity.serverVersion === serverVersion;
276
- if (exactCurrent) {
277
- if (identity.launcher === "session") {
278
- if (await registerAndVerify(identity)) return identity;
279
- return null;
280
- }
281
- const verified = await readIdentity({ host, port, timeoutMs: HEALTH_TIMEOUT_MS }).catch(() => null);
282
- return exactDashboardIdentity(verified, identity) ? identity : null;
283
- }
284
- if (isRecognizedDashboardIdentity(identity, canonicalWorkspace)) {
285
- if (priorCompatibleVersion(taskchefVersion, identity.taskchefVersion))
286
- throw listenerConflict(url, `belongs to newer TaskChef ${identity.taskchefVersion}; refusing to downgrade it.`);
287
- if (priorCompatibleVersion(identity.taskchefVersion, taskchefVersion)) return stopOlder(identity);
288
- }
289
- throw listenerConflict(url, "belongs to an unknown, malformed, different-workspace, standalone, or incompatible service.");
290
- };
291
- const ensureOnce = async () => {
292
- canonicalWorkspace ??= await realpath(path.resolve(workspace));
293
- const existing = await probe();
294
- if (existing) return publicResult("reused", existing.launcher);
295
- for (let replacementAttempt = 0; replacementAttempt < MAX_VERSION_REPLACEMENT_ATTEMPTS; replacementAttempt += 1) {
296
- try { await launchSession({ workspace: canonicalWorkspace, host, port, sessionPid,
297
- taskchefVersion, serverVersion }); }
298
- catch (error) {
299
- if (error?.code !== "EADDRINUSE") throw error;
300
- const winner = await probe();
301
- if (winner) return publicResult("reused", winner.launcher);
302
- continue;
303
- }
304
- for (let attempt = 0; attempt < SESSION_START_ATTEMPTS; attempt += 1) {
305
- try {
306
- const identity = await readIdentity({ host, port, timeoutMs: 100 });
307
- if (exactDashboardIdentity(identity, expected())) {
308
- if (await registerAndVerify(identity)) return publicResult("started");
309
- } else if (isRecognizedDashboardIdentity(identity, canonicalWorkspace)
310
- && priorCompatibleVersion(identity.taskchefVersion, taskchefVersion)) {
311
- const replacement = await stopOlder(identity);
312
- if (replacement) return publicResult("reused", replacement.launcher);
313
- break;
314
- } else if (isRecognizedDashboardIdentity(identity, canonicalWorkspace)
315
- && priorCompatibleVersion(taskchefVersion, identity.taskchefVersion)) {
316
- throw listenerConflict(url, `belongs to newer TaskChef ${identity.taskchefVersion}; refusing to downgrade it.`);
317
- } else throw listenerConflict(url, "changed to an unexpected listener while the dashboard session was starting.");
318
- } catch (error) { if (!listenerAbsent(error) && error?.code !== "ECONNRESET") throw error; }
319
- await new Promise((resolve) => setTimeout(resolve, SESSION_START_INTERVAL_MS));
320
- }
321
- }
322
- const error = new Error(`TaskChef dashboard session did not become available at ${url}`);
323
- error.code = "TASKCHEF_DASHBOARD_START_TIMEOUT"; throw error;
324
- };
325
- return {
326
- async ensure() {
327
- if (closePromise) throw new Error("TaskChef dashboard manager is shutting down");
328
- if (ensurePromise) {
329
- await ensurePromise;
330
- const existing = await probe();
331
- if (existing) return publicResult("reused", existing.launcher);
332
- }
333
- ensurePromise = ensureOnce().finally(() => { ensurePromise = null; });
334
- return ensurePromise;
335
- },
336
- async close() { closePromise ??= (async () => { await ensurePromise?.catch(() => {}); })(); return closePromise; },
337
- get owned() { return false; },
338
- };
339
- }