@cluesmith/codev 2.0.2 → 2.0.3

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 (209) hide show
  1. package/dashboard/dist/assets/{index-b38SaXk5.js → index-UsH9ixz1.js} +20 -20
  2. package/dashboard/dist/assets/index-UsH9ixz1.js.map +1 -0
  3. package/dashboard/dist/index.html +1 -1
  4. package/dist/agent-farm/cli.js +1 -1
  5. package/dist/agent-farm/cli.js.map +1 -1
  6. package/dist/agent-farm/commands/architect.d.ts +1 -1
  7. package/dist/agent-farm/commands/architect.js +3 -3
  8. package/dist/agent-farm/commands/architect.js.map +1 -1
  9. package/dist/agent-farm/commands/attach.js +1 -1
  10. package/dist/agent-farm/commands/attach.js.map +1 -1
  11. package/dist/agent-farm/commands/cleanup.js +6 -6
  12. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  13. package/dist/agent-farm/commands/open.js +5 -5
  14. package/dist/agent-farm/commands/open.js.map +1 -1
  15. package/dist/agent-farm/commands/send.js +5 -5
  16. package/dist/agent-farm/commands/send.js.map +1 -1
  17. package/dist/agent-farm/commands/shell.js +5 -5
  18. package/dist/agent-farm/commands/shell.js.map +1 -1
  19. package/dist/agent-farm/commands/spawn-worktree.d.ts +1 -1
  20. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/spawn-worktree.js +8 -8
  22. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  23. package/dist/agent-farm/commands/spawn.js +3 -3
  24. package/dist/agent-farm/commands/spawn.js.map +1 -1
  25. package/dist/agent-farm/commands/start.d.ts +4 -4
  26. package/dist/agent-farm/commands/start.js +16 -16
  27. package/dist/agent-farm/commands/start.js.map +1 -1
  28. package/dist/agent-farm/commands/status.d.ts +1 -1
  29. package/dist/agent-farm/commands/status.js +16 -16
  30. package/dist/agent-farm/commands/status.js.map +1 -1
  31. package/dist/agent-farm/commands/stop.d.ts +4 -4
  32. package/dist/agent-farm/commands/stop.js +9 -9
  33. package/dist/agent-farm/commands/stop.js.map +1 -1
  34. package/dist/agent-farm/db/index.d.ts.map +1 -1
  35. package/dist/agent-farm/db/index.js +82 -7
  36. package/dist/agent-farm/db/index.js.map +1 -1
  37. package/dist/agent-farm/db/schema.d.ts +2 -2
  38. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  39. package/dist/agent-farm/db/schema.js +21 -4
  40. package/dist/agent-farm/db/schema.js.map +1 -1
  41. package/dist/agent-farm/lib/tower-client.d.ts +20 -20
  42. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  43. package/dist/agent-farm/lib/tower-client.js +25 -25
  44. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  45. package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
  46. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  47. package/dist/agent-farm/lib/tunnel-client.js +59 -1
  48. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  49. package/dist/agent-farm/servers/tower-instances.d.ts +18 -18
  50. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  51. package/dist/agent-farm/servers/tower-instances.js +89 -89
  52. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  53. package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
  54. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  55. package/dist/agent-farm/servers/tower-routes.js +184 -162
  56. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  57. package/dist/agent-farm/servers/tower-server.js +23 -19
  58. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  59. package/dist/agent-farm/servers/tower-terminals.d.ts +27 -29
  60. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  61. package/dist/agent-farm/servers/tower-terminals.js +95 -116
  62. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  63. package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
  64. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  65. package/dist/agent-farm/servers/tower-tunnel.js +12 -12
  66. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  67. package/dist/agent-farm/servers/tower-types.d.ts +8 -10
  68. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  69. package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
  70. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/tower-utils.js +18 -18
  72. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  73. package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
  74. package/dist/agent-farm/servers/tower-websocket.js +14 -14
  75. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  76. package/dist/agent-farm/types.d.ts +2 -2
  77. package/dist/agent-farm/types.d.ts.map +1 -1
  78. package/dist/agent-farm/utils/config.d.ts +1 -1
  79. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  80. package/dist/agent-farm/utils/config.js +16 -16
  81. package/dist/agent-farm/utils/config.js.map +1 -1
  82. package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
  83. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
  84. package/dist/agent-farm/utils/file-tabs.js +9 -9
  85. package/dist/agent-farm/utils/file-tabs.js.map +1 -1
  86. package/dist/agent-farm/utils/gate-status.d.ts +2 -2
  87. package/dist/agent-farm/utils/gate-status.d.ts.map +1 -1
  88. package/dist/agent-farm/utils/gate-status.js +3 -3
  89. package/dist/agent-farm/utils/gate-status.js.map +1 -1
  90. package/dist/agent-farm/utils/index.d.ts +0 -1
  91. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  92. package/dist/agent-farm/utils/index.js +0 -1
  93. package/dist/agent-farm/utils/index.js.map +1 -1
  94. package/dist/agent-farm/utils/notifications.d.ts +4 -4
  95. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  96. package/dist/agent-farm/utils/notifications.js +18 -18
  97. package/dist/agent-farm/utils/notifications.js.map +1 -1
  98. package/dist/commands/adopt.d.ts +2 -2
  99. package/dist/commands/adopt.d.ts.map +1 -1
  100. package/dist/commands/adopt.js +13 -3
  101. package/dist/commands/adopt.js.map +1 -1
  102. package/dist/commands/consult/index.d.ts +1 -1
  103. package/dist/commands/consult/index.d.ts.map +1 -1
  104. package/dist/commands/consult/index.js +52 -51
  105. package/dist/commands/consult/index.js.map +1 -1
  106. package/dist/commands/doctor.js +6 -6
  107. package/dist/commands/doctor.js.map +1 -1
  108. package/dist/commands/import.js +4 -4
  109. package/dist/commands/import.js.map +1 -1
  110. package/dist/commands/init.d.ts +2 -2
  111. package/dist/commands/init.d.ts.map +1 -1
  112. package/dist/commands/init.js +13 -3
  113. package/dist/commands/init.js.map +1 -1
  114. package/dist/commands/porch/index.d.ts +6 -6
  115. package/dist/commands/porch/index.d.ts.map +1 -1
  116. package/dist/commands/porch/index.js +37 -37
  117. package/dist/commands/porch/index.js.map +1 -1
  118. package/dist/commands/porch/next.d.ts +1 -1
  119. package/dist/commands/porch/next.d.ts.map +1 -1
  120. package/dist/commands/porch/next.js +43 -40
  121. package/dist/commands/porch/next.js.map +1 -1
  122. package/dist/commands/porch/notify.d.ts +11 -0
  123. package/dist/commands/porch/notify.d.ts.map +1 -0
  124. package/dist/commands/porch/notify.js +30 -0
  125. package/dist/commands/porch/notify.js.map +1 -0
  126. package/dist/commands/porch/plan.d.ts +1 -1
  127. package/dist/commands/porch/plan.d.ts.map +1 -1
  128. package/dist/commands/porch/plan.js +3 -3
  129. package/dist/commands/porch/plan.js.map +1 -1
  130. package/dist/commands/porch/prompts.d.ts +1 -1
  131. package/dist/commands/porch/prompts.d.ts.map +1 -1
  132. package/dist/commands/porch/prompts.js +13 -13
  133. package/dist/commands/porch/prompts.js.map +1 -1
  134. package/dist/commands/porch/protocol.d.ts +1 -1
  135. package/dist/commands/porch/protocol.d.ts.map +1 -1
  136. package/dist/commands/porch/protocol.js +6 -6
  137. package/dist/commands/porch/protocol.js.map +1 -1
  138. package/dist/commands/porch/state.d.ts +6 -6
  139. package/dist/commands/porch/state.d.ts.map +1 -1
  140. package/dist/commands/porch/state.js +11 -11
  141. package/dist/commands/porch/state.js.map +1 -1
  142. package/dist/commands/update.d.ts.map +1 -1
  143. package/dist/commands/update.js +10 -1
  144. package/dist/commands/update.js.map +1 -1
  145. package/dist/lib/scaffold.d.ts +13 -0
  146. package/dist/lib/scaffold.d.ts.map +1 -1
  147. package/dist/lib/scaffold.js +34 -0
  148. package/dist/lib/scaffold.js.map +1 -1
  149. package/dist/lib/skeleton.d.ts +7 -7
  150. package/dist/lib/skeleton.d.ts.map +1 -1
  151. package/dist/lib/skeleton.js +10 -10
  152. package/dist/lib/skeleton.js.map +1 -1
  153. package/dist/terminal/pty-manager.d.ts +1 -1
  154. package/dist/terminal/pty-manager.d.ts.map +1 -1
  155. package/dist/terminal/pty-manager.js +3 -3
  156. package/dist/terminal/pty-manager.js.map +1 -1
  157. package/package.json +1 -1
  158. package/templates/open.html +13 -13
  159. package/templates/tower.html +54 -54
  160. package/templates/vendor/marked.min.js +6 -0
  161. package/templates/vendor/prism-bash.min.js +1 -0
  162. package/templates/vendor/prism-css.min.js +1 -0
  163. package/templates/vendor/prism-javascript.min.js +1 -0
  164. package/templates/vendor/prism-json.min.js +1 -0
  165. package/templates/vendor/prism-markdown.min.js +1 -0
  166. package/templates/vendor/prism-markup.min.js +1 -0
  167. package/templates/vendor/prism-python.min.js +1 -0
  168. package/templates/vendor/prism-tomorrow.min.css +1 -0
  169. package/templates/vendor/prism-typescript.min.js +1 -0
  170. package/templates/vendor/prism-yaml.min.js +1 -0
  171. package/templates/vendor/prism.min.js +1 -0
  172. package/templates/vendor/purify.min.js +3 -0
  173. package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
  174. package/dist/agent-farm/hq-connector.d.ts +0 -19
  175. package/dist/agent-farm/hq-connector.d.ts.map +0 -1
  176. package/dist/agent-farm/hq-connector.js +0 -351
  177. package/dist/agent-farm/hq-connector.js.map +0 -1
  178. package/dist/agent-farm/utils/deps.d.ts +0 -51
  179. package/dist/agent-farm/utils/deps.d.ts.map +0 -1
  180. package/dist/agent-farm/utils/deps.js +0 -162
  181. package/dist/agent-farm/utils/deps.js.map +0 -1
  182. package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
  183. package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
  184. package/dist/agent-farm/utils/gate-watcher.js +0 -122
  185. package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
  186. package/dist/agent-farm/utils/session.d.ts +0 -32
  187. package/dist/agent-farm/utils/session.d.ts.map +0 -1
  188. package/dist/agent-farm/utils/session.js +0 -57
  189. package/dist/agent-farm/utils/session.js.map +0 -1
  190. package/dist/lib/projectlist-parser.d.ts +0 -70
  191. package/dist/lib/projectlist-parser.d.ts.map +0 -1
  192. package/dist/lib/projectlist-parser.js +0 -200
  193. package/dist/lib/projectlist-parser.js.map +0 -1
  194. package/templates/dashboard/css/dialogs.css +0 -149
  195. package/templates/dashboard/css/files.css +0 -558
  196. package/templates/dashboard/css/layout.css +0 -133
  197. package/templates/dashboard/css/projects.css +0 -501
  198. package/templates/dashboard/css/statusbar.css +0 -23
  199. package/templates/dashboard/css/tabs.css +0 -314
  200. package/templates/dashboard/css/utilities.css +0 -50
  201. package/templates/dashboard/css/variables.css +0 -45
  202. package/templates/dashboard/index.html +0 -149
  203. package/templates/dashboard/js/dialogs.js +0 -368
  204. package/templates/dashboard/js/files.js +0 -448
  205. package/templates/dashboard/js/main.js +0 -476
  206. package/templates/dashboard/js/projects.js +0 -544
  207. package/templates/dashboard/js/state.js +0 -91
  208. package/templates/dashboard/js/tabs.js +0 -518
  209. package/templates/dashboard/js/utils.js +0 -191
@@ -1,518 +0,0 @@
1
- // Tab Management - Rendering, Selection, Overflow
2
-
3
- // Get the base URL for server connections (uses current hostname for remote access)
4
- // DEPRECATED: Use getTerminalUrl() for terminal tabs (Spec 0062)
5
- function getBaseUrl(port) {
6
- return `http://${window.location.hostname}:${port}`;
7
- }
8
-
9
- /**
10
- * Get the terminal URL for a tab (Spec 0062 - Secure Remote Access)
11
- * Uses the reverse proxy instead of direct port access, enabling SSH tunnel support.
12
- *
13
- * @param {Object} tab - The tab object
14
- * @returns {string} The URL to load in the iframe
15
- */
16
- function getTerminalUrl(tab) {
17
- // Architect terminal
18
- if (tab.type === 'architect') {
19
- return '/terminal/architect';
20
- }
21
-
22
- // Builder terminal - use the builder's ID (e.g., builder-0055)
23
- if (tab.type === 'builder') {
24
- return `/terminal/builder-${tab.projectId}`;
25
- }
26
-
27
- // Shell/utility terminal - use the utility's ID (e.g., util-U12345)
28
- if (tab.type === 'shell') {
29
- return `/terminal/util-${tab.utilId}`;
30
- }
31
-
32
- // File/annotation tabs - use the annotation ID for proxy routing
33
- if (tab.type === 'file' && tab.annotationId) {
34
- return `/annotation/${tab.annotationId}/`;
35
- }
36
-
37
- // Fallback for backward compatibility
38
- if (tab.port) {
39
- return getBaseUrl(tab.port);
40
- }
41
-
42
- return null;
43
- }
44
-
45
- // Build tabs from initial state
46
- function buildTabsFromState() {
47
- const previousTabIds = new Set(tabs.map(t => t.id));
48
- // Preserve client-side-only tabs (like activity)
49
- const clientSideTabs = tabs.filter(t => t.type === 'activity');
50
- tabs = [];
51
-
52
- // Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
53
- tabs.push({
54
- id: 'dashboard',
55
- type: 'dashboard',
56
- name: 'Dashboard',
57
- closeable: false
58
- });
59
-
60
- // Add file tabs from annotations
61
- for (const annotation of state.annotations || []) {
62
- tabs.push({
63
- id: `file-${annotation.id}`,
64
- type: 'file',
65
- name: getFileName(annotation.file),
66
- path: annotation.file,
67
- port: annotation.port,
68
- annotationId: annotation.id
69
- });
70
- }
71
-
72
- // Add builder tabs
73
- for (const builder of state.builders || []) {
74
- tabs.push({
75
- id: `builder-${builder.id}`,
76
- type: 'builder',
77
- name: builder.name || `Builder ${builder.id}`,
78
- projectId: builder.id,
79
- port: builder.port,
80
- status: builder.status
81
- });
82
- }
83
-
84
- // Add shell tabs
85
- for (const util of state.utils || []) {
86
- tabs.push({
87
- id: `shell-${util.id}`,
88
- type: 'shell',
89
- name: util.name,
90
- port: util.port,
91
- utilId: util.id
92
- });
93
- }
94
-
95
- // Re-add preserved client-side tabs
96
- for (const tab of clientSideTabs) {
97
- tabs.push(tab);
98
- }
99
-
100
- // Detect new tabs and auto-switch to them (skip projects tab)
101
- for (const tab of tabs) {
102
- if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
103
- // This is a new tab - switch to it
104
- activeTabId = tab.id;
105
- break;
106
- }
107
- }
108
-
109
- // Update known tab IDs
110
- knownTabIds = new Set(tabs.map(t => t.id));
111
-
112
- // Set active tab to Dashboard on first load if none selected
113
- if (!activeTabId) {
114
- activeTabId = 'dashboard';
115
- }
116
- }
117
-
118
- // Render architect pane
119
- function renderArchitect() {
120
- const content = document.getElementById('architect-content');
121
- const statusDot = document.getElementById('architect-status');
122
-
123
- if (state.architect && state.architect.port) {
124
- statusDot.classList.remove('inactive');
125
- // Only update iframe if port changed (avoid flashing on poll)
126
- if (currentArchitectPort !== state.architect.port) {
127
- currentArchitectPort = state.architect.port;
128
- // Use proxied URL for remote access support (Spec 0062)
129
- content.innerHTML = `<iframe src="/terminal/architect" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
130
- }
131
- } else {
132
- if (currentArchitectPort !== null) {
133
- currentArchitectPort = null;
134
- content.innerHTML = `
135
- <div class="architect-placeholder">
136
- <p>Architect not running</p>
137
- <p>Run <code>agent-farm start</code> to begin</p>
138
- </div>
139
- `;
140
- }
141
- statusDot.classList.add('inactive');
142
- }
143
- }
144
-
145
- // Render tabs
146
- function renderTabs() {
147
- const container = document.getElementById('tabs-container');
148
-
149
- if (tabs.length === 0) {
150
- container.innerHTML = '';
151
- checkTabOverflow(); // Update overflow state when tabs cleared
152
- return;
153
- }
154
-
155
- container.innerHTML = tabs.map(tab => {
156
- const isActive = tab.id === activeTabId;
157
- const icon = getTabIcon(tab.type);
158
- const statusDot = tab.type === 'builder' ? getStatusDot(tab.status) : '';
159
- const tooltip = getTabTooltip(tab);
160
- const isUncloseable = tab.closeable === false;
161
-
162
- return `
163
- <div class="tab ${isActive ? 'active' : ''} ${isUncloseable ? 'tab-uncloseable' : ''}"
164
- onclick="selectTab('${tab.id}')"
165
- oncontextmenu="showContextMenu(event, '${tab.id}')"
166
- data-tab-id="${tab.id}"
167
- title="${tooltip}">
168
- <span class="icon">${icon}</span>
169
- <span class="name">${tab.name}</span>
170
- ${statusDot}
171
- ${!isUncloseable ? `<span class="close"
172
- onclick="event.stopPropagation(); closeTab('${tab.id}', event)"
173
- role="button"
174
- tabindex="0"
175
- aria-label="Close ${tab.name}"
176
- onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();closeTab('${tab.id}',event)}">&times;</span>` : ''}
177
- </div>
178
- `;
179
- }).join('');
180
-
181
- // Check overflow after tabs are rendered
182
- checkTabOverflow();
183
- }
184
-
185
- // Get tab icon
186
- function getTabIcon(type) {
187
- switch (type) {
188
- case 'dashboard': return '🏠';
189
- case 'files': return '📁';
190
- case 'file': return '📄';
191
- case 'builder': return '🔨';
192
- case 'shell': return '>_';
193
- default: return '?';
194
- }
195
- }
196
-
197
- // Status configuration - hoisted for performance (per Codex review)
198
- const STATUS_CONFIG = {
199
- 'spawning': { color: 'var(--status-active)', label: 'Spawning', shape: 'circle', animation: 'pulse' },
200
- 'implementing': { color: 'var(--status-active)', label: 'Implementing', shape: 'circle', animation: 'pulse' },
201
- 'blocked': { color: 'var(--status-error)', label: 'Blocked', shape: 'diamond', animation: 'blink-fast' },
202
- 'pr-ready': { color: 'var(--status-waiting)', label: 'PR Ready', shape: 'ring', animation: 'blink-slow' },
203
- 'complete': { color: 'var(--status-complete)', label: 'Complete', shape: 'circle', animation: null }
204
- };
205
- const DEFAULT_STATUS_CONFIG = { color: 'var(--text-muted)', label: 'Unknown', shape: 'circle', animation: null };
206
-
207
- // Get status dot HTML with accessibility support
208
- function getStatusDot(status) {
209
- const config = STATUS_CONFIG[status] || { ...DEFAULT_STATUS_CONFIG, label: status || 'Unknown' };
210
- const classes = ['status-dot'];
211
- if (config.shape === 'diamond') classes.push('status-dot--diamond');
212
- if (config.shape === 'ring') classes.push('status-dot--ring');
213
- if (config.animation === 'pulse') classes.push('status-dot--pulse');
214
- if (config.animation === 'blink-slow') classes.push('status-dot--blink-slow');
215
- if (config.animation === 'blink-fast') classes.push('status-dot--blink-fast');
216
- return `<span class="${classes.join(' ')}" style="background: ${config.color}" title="${config.label}" role="img" aria-label="${config.label}"></span>`;
217
- }
218
-
219
- // Generate tooltip text for tab hover
220
- function getTabTooltip(tab) {
221
- const lines = [tab.name];
222
-
223
- if (tab.type === 'builder') {
224
- if (tab.port) lines.push(`Port: ${tab.port}`);
225
- lines.push(`Status: ${tab.status || 'unknown'}`);
226
- const projectId = tab.id.replace('builder-', '');
227
- lines.push(`Worktree: .builders/${projectId}`);
228
- } else if (tab.type === 'file') {
229
- lines.push(`Path: ${tab.path}`);
230
- if (tab.port) lines.push(`Port: ${tab.port}`);
231
- } else if (tab.type === 'shell') {
232
- if (tab.port) lines.push(`Port: ${tab.port}`);
233
- }
234
-
235
- return escapeHtml(lines.join('\n'));
236
- }
237
-
238
- // Render tab content
239
- function renderTabContent() {
240
- const content = document.getElementById('tab-content');
241
-
242
- if (!activeTabId || tabs.length === 0) {
243
- if (currentTabPort !== null || currentTabType !== null) {
244
- currentTabPort = null;
245
- currentTabType = null;
246
- content.innerHTML = `
247
- <div class="empty-state">
248
- <p>No tabs open</p>
249
- <p class="hint">Click the + buttons above or ask the architect to open files/builders</p>
250
- </div>
251
- `;
252
- }
253
- return;
254
- }
255
-
256
- const tab = tabs.find(t => t.id === activeTabId);
257
- if (!tab) {
258
- if (currentTabPort !== null || currentTabType !== null) {
259
- currentTabPort = null;
260
- currentTabType = null;
261
- content.innerHTML = '<div class="empty-state"><p>Tab not found</p></div>';
262
- }
263
- return;
264
- }
265
-
266
- // Handle dashboard tab specially (no iframe, inline content)
267
- if (tab.type === 'dashboard') {
268
- if (currentTabType !== 'dashboard') {
269
- currentTabType = 'dashboard';
270
- currentTabPort = null;
271
- renderDashboardTab();
272
- }
273
- return;
274
- }
275
-
276
- // Handle activity tab specially (no iframe, inline content)
277
- if (tab.type === 'activity') {
278
- if (currentTabType !== 'activity') {
279
- currentTabType = 'activity';
280
- currentTabPort = null;
281
- renderActivityTab();
282
- }
283
- return;
284
- }
285
-
286
- // For other tabs, only update iframe if port changed (avoid flashing on poll)
287
- if (currentTabPort !== tab.port || currentTabType !== tab.type) {
288
- currentTabPort = tab.port;
289
- currentTabType = tab.type;
290
- // Use proxied URL for terminal tabs (Spec 0062 - Secure Remote Access)
291
- const url = getTerminalUrl(tab);
292
- if (url) {
293
- content.innerHTML = `<iframe src="${url}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
294
- } else {
295
- content.innerHTML = `<div class="empty-state"><p>Terminal unavailable</p></div>`;
296
- }
297
- }
298
- }
299
-
300
- // Force refresh the iframe for a file tab (reloads content from server)
301
- function refreshFileTab(tabId) {
302
- const tab = tabs.find(t => t.id === tabId);
303
- if (!tab || tab.type !== 'file') return;
304
-
305
- if (activeTabId === tabId) {
306
- const content = document.getElementById('tab-content');
307
- const iframe = content.querySelector('iframe');
308
- if (iframe) {
309
- const baseUrl = getTerminalUrl(tab);
310
- iframe.src = baseUrl ? `${baseUrl}?t=${Date.now()}` : iframe.src;
311
- }
312
- }
313
- }
314
-
315
- // Update status bar
316
- function updateStatusBar() {
317
- const archStatus = document.getElementById('status-architect');
318
- if (state.architect) {
319
- archStatus.innerHTML = `
320
- <span class="dot" style="background: var(--status-active)"></span>
321
- <span>Architect: running</span>
322
- `;
323
- } else {
324
- archStatus.innerHTML = `
325
- <span class="dot" style="background: var(--text-muted)"></span>
326
- <span>Architect: stopped</span>
327
- `;
328
- }
329
-
330
- const builderCount = (state.builders || []).length;
331
- const shellCount = (state.utils || []).length;
332
- const fileCount = (state.annotations || []).length;
333
-
334
- document.getElementById('status-builders').innerHTML = `<span>${builderCount} builder${builderCount !== 1 ? 's' : ''}</span>`;
335
- document.getElementById('status-shells').innerHTML = `<span>${shellCount} shell${shellCount !== 1 ? 's' : ''}</span>`;
336
- document.getElementById('status-files').innerHTML = `<span>${fileCount} file${fileCount !== 1 ? 's' : ''}</span>`;
337
- }
338
-
339
- // Select tab
340
- function selectTab(tabId) {
341
- activeTabId = tabId;
342
- renderTabs();
343
- renderTabContent();
344
- scrollActiveTabIntoView();
345
- }
346
-
347
- // Scroll the active tab into view
348
- function scrollActiveTabIntoView() {
349
- const container = document.getElementById('tabs-container');
350
- const activeTab = container.querySelector('.tab.active');
351
- if (activeTab) {
352
- activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
353
- }
354
- }
355
-
356
- // Set up overflow detection for the tab bar
357
- function setupOverflowDetection() {
358
- const container = document.getElementById('tabs-container');
359
-
360
- checkTabOverflow();
361
-
362
- let resizeTimeout;
363
- window.addEventListener('resize', () => {
364
- clearTimeout(resizeTimeout);
365
- resizeTimeout = setTimeout(checkTabOverflow, 100);
366
- });
367
-
368
- if (container) {
369
- let scrollTimeout;
370
- container.addEventListener('scroll', () => {
371
- clearTimeout(scrollTimeout);
372
- scrollTimeout = setTimeout(checkTabOverflow, 50);
373
- });
374
- }
375
-
376
- if (typeof ResizeObserver !== 'undefined') {
377
- if (container) {
378
- const observer = new ResizeObserver(() => {
379
- checkTabOverflow();
380
- });
381
- observer.observe(container);
382
- }
383
- }
384
- }
385
-
386
- // Check if tabs are overflowing and update the overflow button
387
- function checkTabOverflow() {
388
- const container = document.getElementById('tabs-container');
389
- const overflowBtn = document.getElementById('overflow-btn');
390
- const overflowCount = document.getElementById('overflow-count');
391
-
392
- if (!container || !overflowBtn) return;
393
-
394
- const isOverflowing = container.scrollWidth > container.clientWidth;
395
- overflowBtn.style.display = isOverflowing ? 'flex' : 'none';
396
-
397
- if (isOverflowing) {
398
- const tabElements = container.querySelectorAll('.tab');
399
- const containerRect = container.getBoundingClientRect();
400
- let hiddenCount = 0;
401
-
402
- tabElements.forEach(tab => {
403
- const rect = tab.getBoundingClientRect();
404
- if (rect.right > containerRect.right + 1) {
405
- hiddenCount++;
406
- } else if (rect.left < containerRect.left - 1) {
407
- hiddenCount++;
408
- }
409
- });
410
-
411
- overflowCount.textContent = `+${hiddenCount}`;
412
- }
413
- }
414
-
415
- // Toggle the overflow menu
416
- function toggleOverflowMenu() {
417
- const menu = document.getElementById('overflow-menu');
418
- const isHidden = menu.classList.contains('hidden');
419
-
420
- if (isHidden) {
421
- showOverflowMenu();
422
- } else {
423
- hideOverflowMenu();
424
- }
425
- }
426
-
427
- // Show the overflow menu
428
- function showOverflowMenu() {
429
- const menu = document.getElementById('overflow-menu');
430
- const btn = document.getElementById('overflow-btn');
431
-
432
- menu.innerHTML = tabs.map((tab, index) => {
433
- const icon = getTabIcon(tab.type);
434
- const isActive = tab.id === activeTabId;
435
- return `
436
- <div class="overflow-menu-item ${isActive ? 'active' : ''}"
437
- role="menuitem"
438
- tabindex="${index === 0 ? 0 : -1}"
439
- data-tab-id="${tab.id}"
440
- onclick="selectTabFromMenu('${tab.id}')"
441
- onkeydown="handleOverflowMenuKeydown(event, '${tab.id}')">
442
- <span class="icon">${icon}</span>
443
- <span class="name">${tab.name}</span>
444
- <span class="open-external"
445
- onclick="event.stopPropagation(); openInNewTabFromMenu('${tab.id}')"
446
- onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();openInNewTabFromMenu('${tab.id}')}"
447
- title="Open in new tab"
448
- role="button"
449
- tabindex="0"
450
- aria-label="Open ${tab.name} in new tab">↗</span>
451
- </div>
452
- `;
453
- }).join('');
454
-
455
- menu.classList.remove('hidden');
456
- btn.setAttribute('aria-expanded', 'true');
457
-
458
- const firstItem = menu.querySelector('.overflow-menu-item');
459
- if (firstItem) firstItem.focus();
460
-
461
- setTimeout(() => {
462
- document.addEventListener('click', handleOverflowClickOutside);
463
- }, 0);
464
- }
465
-
466
- // Hide the overflow menu
467
- function hideOverflowMenu() {
468
- const menu = document.getElementById('overflow-menu');
469
- const btn = document.getElementById('overflow-btn');
470
- menu.classList.add('hidden');
471
- btn.setAttribute('aria-expanded', 'false');
472
- document.removeEventListener('click', handleOverflowClickOutside);
473
- }
474
-
475
- // Handle click outside overflow menu
476
- function handleOverflowClickOutside(event) {
477
- const menu = document.getElementById('overflow-menu');
478
- const btn = document.getElementById('overflow-btn');
479
- if (!menu.contains(event.target) && !btn.contains(event.target)) {
480
- hideOverflowMenu();
481
- }
482
- }
483
-
484
- // Select tab from overflow menu
485
- function selectTabFromMenu(tabId) {
486
- hideOverflowMenu();
487
- selectTab(tabId);
488
- }
489
-
490
- // Open tab in new window from overflow menu
491
- function openInNewTabFromMenu(tabId) {
492
- hideOverflowMenu();
493
- openInNewTab(tabId);
494
- }
495
-
496
- // Handle keyboard navigation in overflow menu
497
- // Uses shared handleMenuKeydown from utils.js (Maintenance Run 0004)
498
- function handleOverflowMenuKeydown(event, tabId) {
499
- handleMenuKeydown(event, 'overflow-menu', 'overflow-menu-item', hideOverflowMenu, {
500
- onEnter: () => selectTabFromMenu(tabId),
501
- focusOnEscape: 'overflow-btn'
502
- });
503
- }
504
-
505
- // Open tab content in a new browser tab
506
- function openInNewTab(tabId) {
507
- const tab = tabs.find(t => t.id === tabId);
508
- if (!tab) return;
509
-
510
- // Use proxied URL for terminal tabs (Spec 0062 - Secure Remote Access)
511
- const url = getTerminalUrl(tab);
512
- if (!url) {
513
- showToast('Tab not ready', 'error');
514
- return;
515
- }
516
-
517
- window.open(url, '_blank', 'noopener,noreferrer');
518
- }
@@ -1,191 +0,0 @@
1
- // Dashboard Utility Functions
2
-
3
- // Get the base path for API calls (handles tower proxy context)
4
- // When accessed via tower proxy at /project/<encoded>/, API calls need to go through the proxy
5
- // When accessed directly at /, API calls go to root
6
- function getApiBase() {
7
- const path = window.location.pathname;
8
- // Check if we're in tower proxy context: /project/<encoded>/
9
- const match = path.match(/^(\/project\/[^/]+\/)/);
10
- if (match) {
11
- return match[1]; // Returns /project/<encoded>/
12
- }
13
- return '/'; // Direct access
14
- }
15
-
16
- // Build API URL that works both directly and through tower proxy
17
- function apiUrl(endpoint) {
18
- const base = getApiBase();
19
- // Remove leading slash from endpoint if present
20
- const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
21
- return base + cleanEndpoint;
22
- }
23
-
24
- // Escape HTML special characters to prevent XSS
25
- function escapeHtml(text) {
26
- return String(text)
27
- .replace(/&/g, '&amp;')
28
- .replace(/</g, '&lt;')
29
- .replace(/>/g, '&gt;')
30
- .replace(/"/g, '&quot;')
31
- .replace(/'/g, '&#39;');
32
- }
33
-
34
- // XSS-safe HTML escaping (used by projects module)
35
- function escapeProjectHtml(text) {
36
- if (!text) return '';
37
- const div = document.createElement('div');
38
- div.textContent = String(text);
39
- return div.innerHTML;
40
- }
41
-
42
- // Escape a string for use inside a JavaScript string literal in onclick handlers
43
- function escapeJsString(str) {
44
- return str
45
- .replace(/\\/g, '\\\\')
46
- .replace(/'/g, "\\'")
47
- .replace(/"/g, '\\"')
48
- .replace(/\n/g, '\\n')
49
- .replace(/\r/g, '\\r');
50
- }
51
-
52
- // Get filename from path
53
- function getFileName(path) {
54
- const parts = path.split('/');
55
- return parts[parts.length - 1];
56
- }
57
-
58
- // Simple DJB2 hash for change detection
59
- function hashString(str) {
60
- let hash = 5381;
61
- for (let i = 0; i < str.length; i++) {
62
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
63
- }
64
- return hash >>> 0;
65
- }
66
-
67
- // Toast notifications
68
- function showToast(message, type = 'info') {
69
- const container = document.getElementById('toast-container');
70
- const toast = document.createElement('div');
71
- toast.className = `toast ${type}`;
72
- toast.textContent = message;
73
- container.appendChild(toast);
74
-
75
- setTimeout(() => {
76
- toast.remove();
77
- }, 3000);
78
- }
79
-
80
- // ========================================
81
- // Shared Utilities (Maintenance Run 0004)
82
- // ========================================
83
-
84
- /**
85
- * Open a file in a new tab (or switch to existing)
86
- * Consolidates duplicate code from main.js, files.js, dialogs.js
87
- *
88
- * @param {string} filePath - Path to the file to open
89
- * @param {Object} options - Optional settings
90
- * @param {number} options.lineNumber - Line number to show in toast
91
- * @param {boolean} options.showSwitchToast - Show toast when switching to existing tab
92
- * @param {Function} options.onSuccess - Callback after successful open
93
- */
94
- async function openFileTab(filePath, options = {}) {
95
- const { lineNumber, showSwitchToast = true, onSuccess } = options;
96
-
97
- try {
98
- // Check for existing tab
99
- const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
100
- if (existingTab) {
101
- selectTab(existingTab.id);
102
- refreshFileTab(existingTab.id);
103
- if (showSwitchToast) {
104
- showToast(`Switched to ${getFileName(filePath)}`, 'success');
105
- }
106
- if (onSuccess) onSuccess();
107
- return;
108
- }
109
-
110
- // Create new tab
111
- const response = await fetch(apiUrl('api/tabs/file'), {
112
- method: 'POST',
113
- headers: { 'Content-Type': 'application/json' },
114
- body: JSON.stringify({ path: filePath })
115
- });
116
-
117
- if (!response.ok) {
118
- throw new Error(await response.text());
119
- }
120
-
121
- const result = await response.json();
122
- await refresh();
123
-
124
- // Select the new tab
125
- const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
126
- if (newTab) {
127
- selectTab(newTab.id);
128
- }
129
-
130
- const lineInfo = lineNumber ? `:${lineNumber}` : '';
131
- showToast(`Opened ${getFileName(filePath)}${lineInfo}`, 'success');
132
- if (onSuccess) onSuccess();
133
- } catch (err) {
134
- showToast('Failed to open file: ' + err.message, 'error');
135
- }
136
- }
137
-
138
- /**
139
- * Handle keyboard navigation in dropdown menus
140
- * Consolidates duplicate code from dialogs.js and tabs.js
141
- *
142
- * @param {KeyboardEvent} event - The keyboard event
143
- * @param {string} menuId - ID of the menu element
144
- * @param {string} itemClass - CSS class of menu items
145
- * @param {Function} hideFunction - Function to hide the menu
146
- * @param {Object} options - Optional settings
147
- * @param {Function} options.onEnter - Custom handler for Enter/Space (default: call data-action)
148
- * @param {string} options.focusOnEscape - Element ID to focus after Escape
149
- */
150
- function handleMenuKeydown(event, menuId, itemClass, hideFunction, options = {}) {
151
- const { onEnter, focusOnEscape } = options;
152
- const menu = document.getElementById(menuId);
153
- const items = Array.from(menu.querySelectorAll(`.${itemClass}`));
154
- const currentIndex = items.findIndex(item => item === document.activeElement);
155
-
156
- switch (event.key) {
157
- case 'ArrowDown':
158
- event.preventDefault();
159
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
160
- items[nextIndex].focus();
161
- break;
162
- case 'ArrowUp':
163
- event.preventDefault();
164
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
165
- items[prevIndex].focus();
166
- break;
167
- case 'Enter':
168
- case ' ':
169
- event.preventDefault();
170
- if (onEnter) {
171
- onEnter(event);
172
- } else {
173
- const actionName = event.target.dataset.action;
174
- if (actionName && typeof window[actionName] === 'function') {
175
- window[actionName]();
176
- }
177
- }
178
- break;
179
- case 'Escape':
180
- event.preventDefault();
181
- hideFunction();
182
- if (focusOnEscape) {
183
- document.getElementById(focusOnEscape).focus();
184
- }
185
- break;
186
- case 'Tab':
187
- hideFunction();
188
- break;
189
- }
190
- }
191
-