dmux 2.2.1 → 3.0.1

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 (201) hide show
  1. package/dist/DmuxApp.d.ts.map +1 -1
  2. package/dist/DmuxApp.js +433 -179
  3. package/dist/DmuxApp.js.map +1 -1
  4. package/dist/MergePane.d.ts.map +1 -1
  5. package/dist/MergePane.js +4 -6
  6. package/dist/MergePane.js.map +1 -1
  7. package/dist/PaneAnalyzer.d.ts +45 -0
  8. package/dist/PaneAnalyzer.d.ts.map +1 -0
  9. package/dist/PaneAnalyzer.js +278 -0
  10. package/dist/PaneAnalyzer.js.map +1 -0
  11. package/dist/_plugin-vue_export-helper.css +1 -0
  12. package/dist/actions/index.d.ts +19 -0
  13. package/dist/actions/index.d.ts.map +1 -0
  14. package/dist/actions/index.js +54 -0
  15. package/dist/actions/index.js.map +1 -0
  16. package/dist/actions/paneActions.d.ts +45 -0
  17. package/dist/actions/paneActions.d.ts.map +1 -0
  18. package/dist/actions/paneActions.js +932 -0
  19. package/dist/actions/paneActions.js.map +1 -0
  20. package/dist/actions/types.d.ts +101 -0
  21. package/dist/actions/types.d.ts.map +1 -0
  22. package/dist/actions/types.js +129 -0
  23. package/dist/actions/types.js.map +1 -0
  24. package/dist/adapters/apiActionHandler.d.ts +64 -0
  25. package/dist/adapters/apiActionHandler.d.ts.map +1 -0
  26. package/dist/adapters/apiActionHandler.js +170 -0
  27. package/dist/adapters/apiActionHandler.js.map +1 -0
  28. package/dist/adapters/tuiActionHandler.d.ts +57 -0
  29. package/dist/adapters/tuiActionHandler.d.ts.map +1 -0
  30. package/dist/adapters/tuiActionHandler.js +152 -0
  31. package/dist/adapters/tuiActionHandler.js.map +1 -0
  32. package/dist/chunks/_plugin-vue_export-helper-Cvoq67hi.js +28 -0
  33. package/dist/components/ActionChoiceDialog.d.ts +16 -0
  34. package/dist/components/ActionChoiceDialog.d.ts.map +1 -0
  35. package/dist/components/ActionChoiceDialog.js +29 -0
  36. package/dist/components/ActionChoiceDialog.js.map +1 -0
  37. package/dist/components/ActionConfirmDialog.d.ts +16 -0
  38. package/dist/components/ActionConfirmDialog.d.ts.map +1 -0
  39. package/dist/components/ActionConfirmDialog.js +31 -0
  40. package/dist/components/ActionConfirmDialog.js.map +1 -0
  41. package/dist/components/ActionInputDialog.d.ts +16 -0
  42. package/dist/components/ActionInputDialog.d.ts.map +1 -0
  43. package/dist/components/ActionInputDialog.js +49 -0
  44. package/dist/components/ActionInputDialog.js.map +1 -0
  45. package/dist/components/ActionProgressDialog.d.ts +13 -0
  46. package/dist/components/ActionProgressDialog.d.ts.map +1 -0
  47. package/dist/components/ActionProgressDialog.js +20 -0
  48. package/dist/components/ActionProgressDialog.js.map +1 -0
  49. package/dist/components/FooterHelp.d.ts +2 -0
  50. package/dist/components/FooterHelp.d.ts.map +1 -1
  51. package/dist/components/FooterHelp.js +9 -2
  52. package/dist/components/FooterHelp.js.map +1 -1
  53. package/dist/components/KebabMenu.d.ts +10 -0
  54. package/dist/components/KebabMenu.d.ts.map +1 -0
  55. package/dist/components/KebabMenu.js +18 -0
  56. package/dist/components/KebabMenu.js.map +1 -0
  57. package/dist/components/LoadingIndicator.d.ts.map +1 -1
  58. package/dist/components/LoadingIndicator.js +5 -5
  59. package/dist/components/LoadingIndicator.js.map +1 -1
  60. package/dist/components/PaneCard.d.ts +1 -0
  61. package/dist/components/PaneCard.d.ts.map +1 -1
  62. package/dist/components/PaneCard.js +21 -20
  63. package/dist/components/PaneCard.js.map +1 -1
  64. package/dist/components/PanesGrid.d.ts +1 -0
  65. package/dist/components/PanesGrid.d.ts.map +1 -1
  66. package/dist/components/PanesGrid.js +5 -4
  67. package/dist/components/PanesGrid.js.map +1 -1
  68. package/dist/components/QRCode.d.ts +7 -0
  69. package/dist/components/QRCode.d.ts.map +1 -0
  70. package/dist/components/QRCode.js +19 -0
  71. package/dist/components/QRCode.js.map +1 -0
  72. package/dist/components/Spinner.d.ts +10 -0
  73. package/dist/components/Spinner.d.ts.map +1 -0
  74. package/dist/components/Spinner.js +15 -0
  75. package/dist/components/Spinner.js.map +1 -0
  76. package/dist/dashboard.html +14 -0
  77. package/dist/dashboard.js +2 -0
  78. package/dist/hooks/useActionSystem.d.ts +31 -0
  79. package/dist/hooks/useActionSystem.d.ts.map +1 -0
  80. package/dist/hooks/useActionSystem.js +95 -0
  81. package/dist/hooks/useActionSystem.js.map +1 -0
  82. package/dist/hooks/useAgentStatus.d.ts +4 -3
  83. package/dist/hooks/useAgentStatus.d.ts.map +1 -1
  84. package/dist/hooks/useAgentStatus.js +45 -194
  85. package/dist/hooks/useAgentStatus.js.map +1 -1
  86. package/dist/hooks/usePaneCreation.d.ts +3 -1
  87. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  88. package/dist/hooks/usePaneCreation.js +49 -99
  89. package/dist/hooks/usePaneCreation.js.map +1 -1
  90. package/dist/hooks/usePanes.d.ts.map +1 -1
  91. package/dist/hooks/usePanes.js +6 -3
  92. package/dist/hooks/usePanes.js.map +1 -1
  93. package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
  94. package/dist/hooks/useTerminalWidth.js +18 -1
  95. package/dist/hooks/useTerminalWidth.js.map +1 -1
  96. package/dist/hooks/useWorktreeActions.d.ts +2 -1
  97. package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
  98. package/dist/hooks/useWorktreeActions.js +9 -1
  99. package/dist/hooks/useWorktreeActions.js.map +1 -1
  100. package/dist/index.js +48 -3
  101. package/dist/index.js.map +1 -1
  102. package/dist/server/actionsApi.d.ts +37 -0
  103. package/dist/server/actionsApi.d.ts.map +1 -0
  104. package/dist/server/actionsApi.js +256 -0
  105. package/dist/server/actionsApi.js.map +1 -0
  106. package/dist/server/embedded-assets.d.ts +13 -0
  107. package/dist/server/embedded-assets.d.ts.map +1 -0
  108. package/dist/server/embedded-assets.js +5038 -0
  109. package/dist/server/embedded-assets.js.map +1 -0
  110. package/dist/server/index.d.ts +21 -0
  111. package/dist/server/index.d.ts.map +1 -0
  112. package/dist/server/index.js +99 -0
  113. package/dist/server/index.js.map +1 -0
  114. package/dist/server/routes.d.ts +3 -0
  115. package/dist/server/routes.d.ts.map +1 -0
  116. package/dist/server/routes.js +672 -0
  117. package/dist/server/routes.js.map +1 -0
  118. package/dist/server/static.d.ts +6 -0
  119. package/dist/server/static.d.ts.map +1 -0
  120. package/dist/server/static.js +3040 -0
  121. package/dist/server/static.js.map +1 -0
  122. package/dist/services/ConfigWatcher.d.ts +20 -0
  123. package/dist/services/ConfigWatcher.d.ts.map +1 -0
  124. package/dist/services/ConfigWatcher.js +75 -0
  125. package/dist/services/ConfigWatcher.js.map +1 -0
  126. package/dist/services/PaneWorkerManager.d.ts +69 -0
  127. package/dist/services/PaneWorkerManager.d.ts.map +1 -0
  128. package/dist/services/PaneWorkerManager.js +272 -0
  129. package/dist/services/PaneWorkerManager.js.map +1 -0
  130. package/dist/services/StatusDetector.d.ts +87 -0
  131. package/dist/services/StatusDetector.d.ts.map +1 -0
  132. package/dist/services/StatusDetector.js +387 -0
  133. package/dist/services/StatusDetector.js.map +1 -0
  134. package/dist/services/TerminalDiffer.d.ts +85 -0
  135. package/dist/services/TerminalDiffer.d.ts.map +1 -0
  136. package/dist/services/TerminalDiffer.js +499 -0
  137. package/dist/services/TerminalDiffer.js.map +1 -0
  138. package/dist/services/TerminalStreamer.d.ts +80 -0
  139. package/dist/services/TerminalStreamer.d.ts.map +1 -0
  140. package/dist/services/TerminalStreamer.js +490 -0
  141. package/dist/services/TerminalStreamer.js.map +1 -0
  142. package/dist/services/TunnelService.d.ts +9 -0
  143. package/dist/services/TunnelService.d.ts.map +1 -0
  144. package/dist/services/TunnelService.js +34 -0
  145. package/dist/services/TunnelService.js.map +1 -0
  146. package/dist/services/WorkerMessageBus.d.ts +48 -0
  147. package/dist/services/WorkerMessageBus.d.ts.map +1 -0
  148. package/dist/services/WorkerMessageBus.js +120 -0
  149. package/dist/services/WorkerMessageBus.js.map +1 -0
  150. package/dist/shared/StateManager.d.ts +34 -0
  151. package/dist/shared/StateManager.d.ts.map +1 -0
  152. package/dist/shared/StateManager.js +108 -0
  153. package/dist/shared/StateManager.js.map +1 -0
  154. package/dist/shared/StreamProtocol.d.ts +75 -0
  155. package/dist/shared/StreamProtocol.d.ts.map +1 -0
  156. package/dist/shared/StreamProtocol.js +37 -0
  157. package/dist/shared/StreamProtocol.js.map +1 -0
  158. package/dist/terminal.html +17 -0
  159. package/dist/terminal.js +3 -0
  160. package/dist/types.d.ts +21 -1
  161. package/dist/types.d.ts.map +1 -1
  162. package/dist/utils/agentDetection.d.ts +18 -0
  163. package/dist/utils/agentDetection.d.ts.map +1 -0
  164. package/dist/utils/agentDetection.js +73 -0
  165. package/dist/utils/agentDetection.js.map +1 -0
  166. package/dist/utils/aiMerge.d.ts +35 -0
  167. package/dist/utils/aiMerge.d.ts.map +1 -0
  168. package/dist/utils/aiMerge.js +302 -0
  169. package/dist/utils/aiMerge.js.map +1 -0
  170. package/dist/utils/conflictResolutionPane.d.ts +19 -0
  171. package/dist/utils/conflictResolutionPane.d.ts.map +1 -0
  172. package/dist/utils/conflictResolutionPane.js +214 -0
  173. package/dist/utils/conflictResolutionPane.js.map +1 -0
  174. package/dist/utils/mergeExecution.d.ts +52 -0
  175. package/dist/utils/mergeExecution.d.ts.map +1 -0
  176. package/dist/utils/mergeExecution.js +192 -0
  177. package/dist/utils/mergeExecution.js.map +1 -0
  178. package/dist/utils/mergeValidation.d.ts +67 -0
  179. package/dist/utils/mergeValidation.d.ts.map +1 -0
  180. package/dist/utils/mergeValidation.js +213 -0
  181. package/dist/utils/mergeValidation.js.map +1 -0
  182. package/dist/utils/paneCreation.d.ts +17 -0
  183. package/dist/utils/paneCreation.d.ts.map +1 -0
  184. package/dist/utils/paneCreation.js +274 -0
  185. package/dist/utils/paneCreation.js.map +1 -0
  186. package/dist/utils/port.d.ts +5 -0
  187. package/dist/utils/port.d.ts.map +1 -0
  188. package/dist/utils/port.js +54 -0
  189. package/dist/utils/port.js.map +1 -0
  190. package/dist/utils/slug.d.ts.map +1 -1
  191. package/dist/utils/slug.js +32 -25
  192. package/dist/utils/slug.js.map +1 -1
  193. package/dist/workers/PaneWorker.d.ts +2 -0
  194. package/dist/workers/PaneWorker.d.ts.map +1 -0
  195. package/dist/workers/PaneWorker.js +362 -0
  196. package/dist/workers/PaneWorker.js.map +1 -0
  197. package/dist/workers/WorkerMessages.d.ts +36 -0
  198. package/dist/workers/WorkerMessages.d.ts.map +1 -0
  199. package/dist/workers/WorkerMessages.js +9 -0
  200. package/dist/workers/WorkerMessages.js.map +1 -0
  201. package/package.json +19 -5
@@ -0,0 +1,3040 @@
1
+ export function getTerminalViewerHtml() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
7
+ <title>Terminal Viewer - dmux</title>
8
+ <link rel="stylesheet" href="/styles.css">
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+
13
+ <script type="module" src="/terminal.js"></script>
14
+ </body>
15
+ </html>`;
16
+ }
17
+ export function getDashboardHtml() {
18
+ return `<!DOCTYPE html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
23
+ <title>dmux Dashboard</title>
24
+ <link rel="stylesheet" href="/styles.css">
25
+ </head>
26
+ <body>
27
+ <div id="app"></div>
28
+
29
+ <script type="module" src="/dashboard.js"></script>
30
+ </body>
31
+ </html>`;
32
+ }
33
+ export function getDashboardCss() {
34
+ return `* {
35
+ margin: 0;
36
+ padding: 0;
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ :root {
41
+ /* Dark theme (default) */
42
+ --bg-gradient-start: #0f0f23;
43
+ --bg-gradient-mid: #1a1a2e;
44
+ --bg-gradient-end: #16213e;
45
+ --text-primary: #e0e0e0;
46
+ --text-secondary: #a0a0a0;
47
+ --text-tertiary: #808080;
48
+ --text-dim: #606060;
49
+ --text-dimmer: #666;
50
+ --text-bright: #fff;
51
+ --border-color: rgba(255, 255, 255, 0.1);
52
+ --border-accent: rgba(255, 140, 0, 0.3);
53
+ --card-bg: rgba(255, 255, 255, 0.05);
54
+ --card-border: rgba(255, 255, 255, 0.1);
55
+ --header-bg: rgba(255, 255, 255, 0.05);
56
+ --input-bg: rgba(255, 255, 255, 0.05);
57
+ --input-border: rgba(255, 255, 255, 0.12);
58
+ --input-focus-border: rgba(255, 140, 0, 0.5);
59
+ --input-focus-bg: rgba(255, 255, 255, 0.08);
60
+ --input-focus-shadow: rgba(255, 140, 0, 0.1);
61
+ --button-bg: rgba(200, 210, 230, 0.15);
62
+ --button-border: rgba(255, 255, 255, 0.08);
63
+ --button-hover-bg: rgba(200, 210, 230, 0.25);
64
+ --button-hover-border: rgba(255, 255, 255, 0.15);
65
+ --tooltip-bg: rgba(20, 20, 30, 0.98);
66
+ --tooltip-border: rgba(255, 255, 255, 0.15);
67
+ --hint-bg: rgba(255, 255, 255, 0.05);
68
+ --agent-bg: rgba(255, 255, 255, 0.08);
69
+ --agent-border: rgba(255, 255, 255, 0.15);
70
+ --idle-badge-bg: rgba(255, 255, 255, 0.08);
71
+ --idle-badge-border: rgba(255, 255, 255, 0.1);
72
+ }
73
+
74
+ [data-theme="light"] {
75
+ /* Light theme */
76
+ --bg-gradient-start: #f0f4f8;
77
+ --bg-gradient-mid: #e6eef5;
78
+ --bg-gradient-end: #dce7f0;
79
+ --text-primary: #1a1a2e;
80
+ --text-secondary: #4a5568;
81
+ --text-tertiary: #718096;
82
+ --text-dim: #a0aec0;
83
+ --text-dimmer: #cbd5e0;
84
+ --text-bright: #000;
85
+ --border-color: rgba(0, 0, 0, 0.1);
86
+ --border-accent: rgba(255, 140, 0, 0.4);
87
+ --card-bg: rgba(255, 255, 255, 0.8);
88
+ --card-border: rgba(0, 0, 0, 0.08);
89
+ --header-bg: rgba(255, 255, 255, 0.9);
90
+ --input-bg: rgba(255, 255, 255, 0.6);
91
+ --input-border: rgba(0, 0, 0, 0.15);
92
+ --input-focus-border: rgba(255, 140, 0, 0.6);
93
+ --input-focus-bg: rgba(255, 255, 255, 0.9);
94
+ --input-focus-shadow: rgba(255, 140, 0, 0.15);
95
+ --button-bg: rgba(0, 0, 0, 0.05);
96
+ --button-border: rgba(0, 0, 0, 0.1);
97
+ --button-hover-bg: rgba(0, 0, 0, 0.1);
98
+ --button-hover-border: rgba(0, 0, 0, 0.2);
99
+ --tooltip-bg: rgba(255, 255, 255, 0.98);
100
+ --tooltip-border: rgba(0, 0, 0, 0.15);
101
+ --hint-bg: rgba(0, 0, 0, 0.03);
102
+ --agent-bg: rgba(0, 0, 0, 0.05);
103
+ --agent-border: rgba(0, 0, 0, 0.12);
104
+ --idle-badge-bg: rgba(0, 0, 0, 0.05);
105
+ --idle-badge-border: rgba(0, 0, 0, 0.1);
106
+ }
107
+
108
+ @keyframes fadeIn {
109
+ from { opacity: 0; transform: translateY(10px); }
110
+ to { opacity: 1; transform: translateY(0); }
111
+ }
112
+
113
+ @keyframes pulse {
114
+ 0%, 100% { opacity: 1; }
115
+ 50% { opacity: 0.5; }
116
+ }
117
+
118
+ @keyframes slideInFromTop {
119
+ from { opacity: 0; transform: translateY(-20px); }
120
+ to { opacity: 1; transform: translateY(0); }
121
+ }
122
+
123
+ body {
124
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
125
+ background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-mid) 50%, var(--bg-gradient-end) 100%);
126
+ background-attachment: fixed;
127
+ color: var(--text-primary);
128
+ min-height: 100vh;
129
+ display: flex;
130
+ flex-direction: column;
131
+ -webkit-font-smoothing: antialiased;
132
+ -moz-osx-font-smoothing: grayscale;
133
+ transition: background 0.3s ease, color 0.3s ease;
134
+ }
135
+
136
+ .container {
137
+ max-width: 1400px;
138
+ margin: 0 auto;
139
+ padding: 40px 20px;
140
+ width: 100%;
141
+ flex: 1;
142
+ display: flex;
143
+ flex-direction: column;
144
+ animation: fadeIn 0.5s ease-out;
145
+ }
146
+
147
+ header {
148
+ display: flex;
149
+ justify-content: space-between;
150
+ align-items: center;
151
+ padding: 16px 24px;
152
+ margin-bottom: 0;
153
+ background: var(--header-bg);
154
+ backdrop-filter: blur(10px);
155
+ -webkit-backdrop-filter: blur(10px);
156
+ border-bottom: 2px solid var(--border-accent);
157
+ animation: slideInFromTop 0.6s ease-out;
158
+ gap: 16px;
159
+ }
160
+
161
+ .logo {
162
+ height: 24px;
163
+ width: auto;
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ h1 {
168
+ font-size: 18px;
169
+ font-weight: 600;
170
+ letter-spacing: -0.5px;
171
+ color: var(--text-primary);
172
+ flex: 1;
173
+ text-align: center;
174
+ white-space: nowrap;
175
+ overflow: hidden;
176
+ text-overflow: ellipsis;
177
+ min-width: 0;
178
+ max-width: 500px;
179
+ margin: 0 auto;
180
+ }
181
+
182
+ .session-info {
183
+ display: flex;
184
+ gap: 12px;
185
+ align-items: center;
186
+ font-size: 13px;
187
+ color: var(--text-secondary);
188
+ flex-shrink: 0;
189
+ }
190
+
191
+ .theme-toggle {
192
+ background: transparent;
193
+ border: none;
194
+ padding: 4px;
195
+ cursor: pointer;
196
+ transition: all 0.2s ease;
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ color: var(--text-secondary);
201
+ flex-shrink: 0;
202
+ width: 24px;
203
+ height: 24px;
204
+ }
205
+
206
+ .theme-toggle:hover {
207
+ color: var(--text-primary);
208
+ transform: scale(1.1);
209
+ }
210
+
211
+ .theme-toggle svg {
212
+ width: 20px;
213
+ height: 20px;
214
+ fill: currentColor;
215
+ }
216
+
217
+ .session-info span {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 6px;
221
+ }
222
+
223
+ .status-indicator {
224
+ color: #4ade80;
225
+ font-size: 16px;
226
+ animation: pulse 2s ease-in-out infinite;
227
+ }
228
+
229
+ main {
230
+ flex: 1;
231
+ padding-top: 40px;
232
+ min-height: 0;
233
+ }
234
+
235
+ .panes-grid {
236
+ display: grid;
237
+ grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
238
+ gap: 24px;
239
+ margin-bottom: 40px;
240
+ }
241
+
242
+ .pane-card {
243
+ background: var(--card-bg);
244
+ backdrop-filter: blur(20px);
245
+ -webkit-backdrop-filter: blur(20px);
246
+ border: 1px solid var(--card-border);
247
+ border-radius: 12px;
248
+ padding: 12px;
249
+ position: relative;
250
+ overflow: hidden;
251
+ animation: fadeIn 0.5s ease-out backwards;
252
+ color: inherit;
253
+ display: block;
254
+ }
255
+
256
+ .pane-card:nth-child(1) { animation-delay: 0.1s; }
257
+ .pane-card:nth-child(2) { animation-delay: 0.15s; }
258
+ .pane-card:nth-child(3) { animation-delay: 0.2s; }
259
+ .pane-card:nth-child(4) { animation-delay: 0.25s; }
260
+ .pane-card:nth-child(5) { animation-delay: 0.3s; }
261
+ .pane-card:nth-child(6) { animation-delay: 0.35s; }
262
+
263
+ .pane-header {
264
+ margin-bottom: 16px;
265
+ display: flex;
266
+ align-items: flex-start;
267
+ justify-content: space-between;
268
+ gap: 12px;
269
+ position: relative;
270
+ }
271
+
272
+ .pane-header-content {
273
+ flex: 1;
274
+ display: flex;
275
+ flex-direction: column;
276
+ gap: 6px;
277
+ }
278
+
279
+ .action-menu-btn {
280
+ background: transparent;
281
+ border: none;
282
+ color: var(--text-tertiary);
283
+ font-size: 20px;
284
+ padding: 4px 8px;
285
+ cursor: pointer;
286
+ transition: all 0.2s ease;
287
+ line-height: 1;
288
+ flex-shrink: 0;
289
+ }
290
+
291
+ .action-menu-btn:hover {
292
+ color: var(--text-primary);
293
+ background: var(--button-hover-bg);
294
+ border-radius: 4px;
295
+ }
296
+
297
+ .action-menu-dropdown {
298
+ position: absolute;
299
+ top: 32px;
300
+ right: 0;
301
+ background: var(--tooltip-bg);
302
+ backdrop-filter: blur(20px);
303
+ -webkit-backdrop-filter: blur(20px);
304
+ border: 1px solid var(--tooltip-border);
305
+ border-radius: 8px;
306
+ padding: 4px;
307
+ z-index: 100;
308
+ min-width: 180px;
309
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
310
+ animation: fadeIn 0.2s ease-out;
311
+ }
312
+
313
+ .action-menu-item {
314
+ width: 100%;
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 8px;
318
+ padding: 8px 12px;
319
+ background: transparent;
320
+ border: none;
321
+ border-radius: 4px;
322
+ color: var(--text-primary);
323
+ font-size: 13px;
324
+ cursor: pointer;
325
+ transition: all 0.15s ease;
326
+ text-align: left;
327
+ }
328
+
329
+ .action-menu-item:hover:not(:disabled) {
330
+ background: var(--button-hover-bg);
331
+ }
332
+
333
+ .action-menu-item:disabled {
334
+ opacity: 0.5;
335
+ cursor: not-allowed;
336
+ }
337
+
338
+ .action-icon {
339
+ font-size: 14px;
340
+ width: 16px;
341
+ text-align: center;
342
+ }
343
+
344
+ .action-label {
345
+ flex: 1;
346
+ }
347
+
348
+ .pane-title-link {
349
+ display: inline-flex;
350
+ align-items: center;
351
+ gap: 8px;
352
+ text-decoration: none;
353
+ color: inherit;
354
+ width: fit-content;
355
+ }
356
+
357
+ .pane-title-link:hover .pane-title {
358
+ text-decoration: underline;
359
+ }
360
+
361
+ .pane-title {
362
+ font-size: 20px;
363
+ font-weight: 600;
364
+ color: var(--text-bright);
365
+ letter-spacing: -0.3px;
366
+ }
367
+
368
+ .pane-arrow {
369
+ font-size: 16px;
370
+ color: var(--text-secondary);
371
+ transition: all 0.2s ease;
372
+ opacity: 0.6;
373
+ }
374
+
375
+ .pane-title-link:hover .pane-arrow {
376
+ color: #ff8c00;
377
+ transform: translateX(2px);
378
+ opacity: 1;
379
+ }
380
+
381
+ .pane-meta {
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 12px;
385
+ }
386
+
387
+ .pane-agent {
388
+ padding: 2px 8px;
389
+ border-radius: 4px;
390
+ font-size: 10px;
391
+ font-weight: 600;
392
+ text-transform: uppercase;
393
+ letter-spacing: 0.3px;
394
+ background: var(--agent-bg);
395
+ border: 1px solid var(--agent-border);
396
+ color: var(--text-tertiary);
397
+ white-space: nowrap;
398
+ }
399
+
400
+ .pane-agent.claude {
401
+ background: rgba(217, 119, 87, 0.15);
402
+ border-color: rgba(217, 119, 87, 0.3);
403
+ color: #D97757;
404
+ }
405
+
406
+ .pane-agent.opencode {
407
+ background: rgba(102, 126, 234, 0.15);
408
+ border-color: rgba(102, 126, 234, 0.3);
409
+ color: #667eea;
410
+ }
411
+
412
+ .pane-prompt-section {
413
+ margin-bottom: 12px;
414
+ }
415
+
416
+ .pane-prompt-header {
417
+ display: flex;
418
+ justify-content: space-between;
419
+ align-items: center;
420
+ padding: 6px 8px;
421
+ background: var(--input-bg);
422
+ border: 1px solid var(--input-border);
423
+ border-radius: 6px;
424
+ cursor: pointer;
425
+ transition: all 0.2s ease;
426
+ }
427
+
428
+ .pane-prompt-header:hover {
429
+ background: var(--input-focus-bg);
430
+ border-color: var(--input-border);
431
+ }
432
+
433
+ .prompt-label {
434
+ font-size: 11px;
435
+ font-weight: 600;
436
+ text-transform: uppercase;
437
+ letter-spacing: 0.5px;
438
+ color: var(--text-tertiary);
439
+ }
440
+
441
+ .expand-icon {
442
+ font-size: 10px;
443
+ color: var(--text-tertiary);
444
+ transition: transform 0.2s ease;
445
+ }
446
+
447
+ .pane-prompt {
448
+ color: var(--text-secondary);
449
+ font-size: 13px;
450
+ margin-top: 8px;
451
+ padding: 8px 12px;
452
+ line-height: 1.6;
453
+ background: var(--input-bg);
454
+ border: 1px solid var(--input-border);
455
+ border-radius: 6px;
456
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
457
+ }
458
+
459
+ .agent-summary {
460
+ color: var(--text-secondary);
461
+ font-size: 13px;
462
+ margin-bottom: 12px;
463
+ padding: 10px 12px;
464
+ line-height: 1.5;
465
+ background: rgba(96, 165, 250, 0.08);
466
+ border: 1px solid rgba(96, 165, 250, 0.2);
467
+ border-radius: 6px;
468
+ font-style: italic;
469
+ }
470
+
471
+ .tooltip {
472
+ position: absolute;
473
+ background: var(--tooltip-bg);
474
+ backdrop-filter: blur(20px);
475
+ -webkit-backdrop-filter: blur(20px);
476
+ border: 1px solid var(--tooltip-border);
477
+ padding: 16px;
478
+ border-radius: 12px;
479
+ z-index: 1000;
480
+ white-space: pre-wrap;
481
+ max-width: 400px;
482
+ max-height: 200px;
483
+ overflow-y: auto;
484
+ box-shadow:
485
+ 0 20px 60px rgba(0, 0, 0, 0.3),
486
+ 0 0 0 1px var(--border-color);
487
+ font-size: 13px;
488
+ color: var(--text-primary);
489
+ pointer-events: none;
490
+ animation: fadeIn 0.2s ease-out;
491
+ }
492
+
493
+ .pane-status {
494
+ display: flex;
495
+ flex-direction: column;
496
+ gap: 10px;
497
+ }
498
+
499
+ .status-item {
500
+ display: flex;
501
+ justify-content: space-between;
502
+ align-items: center;
503
+ font-size: 13px;
504
+ padding: 8px 0;
505
+ }
506
+
507
+ .status-label {
508
+ color: var(--text-tertiary);
509
+ font-weight: 500;
510
+ }
511
+
512
+ .status-value {
513
+ display: flex;
514
+ align-items: center;
515
+ gap: 6px;
516
+ }
517
+
518
+ .status-badge {
519
+ padding: 4px 10px;
520
+ border-radius: 6px;
521
+ font-size: 11px;
522
+ font-weight: 600;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.3px;
525
+ transition: all 0.2s ease;
526
+ }
527
+
528
+ .status-badge.working {
529
+ background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
530
+ color: #000;
531
+ box-shadow: 0 2px 8px rgba(251, 191, 36, 0.4);
532
+ }
533
+
534
+ .status-badge.waiting {
535
+ background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
536
+ color: #000;
537
+ box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
538
+ }
539
+
540
+ .status-badge.idle {
541
+ background: var(--idle-badge-bg);
542
+ color: var(--text-tertiary);
543
+ border: 1px solid var(--idle-badge-border);
544
+ }
545
+
546
+ .status-badge.running {
547
+ background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
548
+ color: #000;
549
+ box-shadow: 0 2px 8px rgba(74, 222, 128, 0.4);
550
+ }
551
+
552
+ .status-badge.passed {
553
+ background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
554
+ color: #000;
555
+ box-shadow: 0 2px 8px rgba(74, 222, 128, 0.4);
556
+ }
557
+
558
+ .status-badge.failed {
559
+ background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
560
+ color: #000;
561
+ box-shadow: 0 2px 8px rgba(248, 113, 113, 0.4);
562
+ }
563
+
564
+ .status-badge.analyzing {
565
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
566
+ color: #000;
567
+ box-shadow: 0 2px 8px rgba(167, 139, 250, 0.4);
568
+ animation: pulse 2s ease-in-out infinite;
569
+ }
570
+
571
+ .pane-id {
572
+ font-family: 'SF Mono', Monaco, monospace;
573
+ font-size: 10px;
574
+ color: var(--text-dimmer);
575
+ font-weight: 500;
576
+ letter-spacing: 0.2px;
577
+ }
578
+
579
+ /* Interactive Area Styles */
580
+ .pane-interactive {
581
+ margin-top: 12px;
582
+ }
583
+
584
+ .options-dialog {
585
+ display: flex;
586
+ flex-direction: column;
587
+ gap: 12px;
588
+ }
589
+
590
+ .options-question {
591
+ font-size: 14px;
592
+ font-weight: 500;
593
+ color: var(--text-primary);
594
+ line-height: 1.4;
595
+ }
596
+
597
+ .options-warning {
598
+ padding: 8px 12px;
599
+ background: rgba(248, 113, 113, 0.1);
600
+ border: 1px solid rgba(248, 113, 113, 0.3);
601
+ border-radius: 6px;
602
+ color: #fca5a5;
603
+ font-size: 12px;
604
+ display: flex;
605
+ align-items: center;
606
+ gap: 6px;
607
+ }
608
+
609
+ .options-buttons {
610
+ display: flex;
611
+ flex-wrap: wrap;
612
+ gap: 8px;
613
+ }
614
+
615
+ .option-button {
616
+ padding: 8px 16px;
617
+ background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
618
+ color: #000;
619
+ border: none;
620
+ border-radius: 6px;
621
+ font-size: 13px;
622
+ font-weight: 600;
623
+ cursor: pointer;
624
+ transition: all 0.2s ease;
625
+ box-shadow: 0 2px 8px rgba(96, 165, 250, 0.3);
626
+ }
627
+
628
+ .option-button:hover {
629
+ transform: translateY(-2px);
630
+ box-shadow: 0 4px 12px rgba(96, 165, 250, 0.4);
631
+ }
632
+
633
+ .option-button:active {
634
+ transform: translateY(0);
635
+ }
636
+
637
+ .option-button-danger {
638
+ background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
639
+ box-shadow: 0 2px 8px rgba(248, 113, 113, 0.3);
640
+ }
641
+
642
+ .option-button-danger:hover {
643
+ box-shadow: 0 4px 12px rgba(248, 113, 113, 0.4);
644
+ }
645
+
646
+ .analyzing-state {
647
+ display: flex;
648
+ align-items: center;
649
+ gap: 12px;
650
+ padding: 8px 0;
651
+ color: #a78bfa;
652
+ font-size: 14px;
653
+ font-weight: 500;
654
+ }
655
+
656
+ .loader-spinner {
657
+ width: 20px;
658
+ height: 20px;
659
+ border: 3px solid rgba(167, 139, 250, 0.2);
660
+ border-top-color: #a78bfa;
661
+ border-radius: 50%;
662
+ animation: spin 1s linear infinite;
663
+ }
664
+
665
+ @keyframes spin {
666
+ to { transform: rotate(360deg); }
667
+ }
668
+
669
+ .prompt-input-wrapper {
670
+ display: flex;
671
+ align-items: flex-start;
672
+ gap: 8px;
673
+ padding: 8px;
674
+ background: var(--input-bg);
675
+ border: 1px solid var(--input-border);
676
+ border-radius: 8px;
677
+ transition: all 0.2s ease;
678
+ }
679
+
680
+ .prompt-input-wrapper:focus-within {
681
+ border-color: var(--input-focus-border);
682
+ background: var(--input-focus-bg);
683
+ box-shadow: 0 0 0 3px var(--input-focus-shadow);
684
+ }
685
+
686
+ .queued-message {
687
+ margin-top: 8px;
688
+ padding: 6px 10px;
689
+ background: rgba(74, 222, 128, 0.1);
690
+ border: 1px solid rgba(74, 222, 128, 0.3);
691
+ border-radius: 6px;
692
+ color: #4ade80;
693
+ font-size: 12px;
694
+ animation: fadeIn 0.3s ease-out;
695
+ }
696
+
697
+ .prompt-textarea {
698
+ flex: 1;
699
+ min-height: 20px;
700
+ max-height: 150px;
701
+ padding: 0;
702
+ background: transparent;
703
+ border: none;
704
+ color: var(--text-primary);
705
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
706
+ font-size: 13px;
707
+ line-height: 1.4;
708
+ resize: none;
709
+ overflow-y: auto;
710
+ }
711
+
712
+ .prompt-textarea:focus {
713
+ outline: none;
714
+ }
715
+
716
+ .prompt-textarea:disabled {
717
+ opacity: 0.5;
718
+ cursor: not-allowed;
719
+ }
720
+
721
+ .prompt-textarea::placeholder {
722
+ color: var(--text-dimmer);
723
+ }
724
+
725
+ .send-button {
726
+ flex-shrink: 0;
727
+ width: 28px;
728
+ height: 28px;
729
+ padding: 6px;
730
+ background: var(--button-bg);
731
+ color: var(--text-secondary);
732
+ border: 1px solid var(--button-border);
733
+ border-radius: 50%;
734
+ cursor: pointer;
735
+ transition: all 0.2s ease;
736
+ display: flex;
737
+ align-items: center;
738
+ justify-content: center;
739
+ }
740
+
741
+ .send-button:hover:not(:disabled) {
742
+ background: var(--button-hover-bg);
743
+ border-color: var(--button-hover-border);
744
+ }
745
+
746
+ .send-button:active:not(:disabled) {
747
+ transform: scale(0.92);
748
+ }
749
+
750
+ .send-button:disabled {
751
+ opacity: 0.3;
752
+ cursor: not-allowed;
753
+ }
754
+
755
+ .send-button svg {
756
+ width: 100%;
757
+ height: 100%;
758
+ fill: currentColor;
759
+ }
760
+
761
+ .button-loader {
762
+ width: 14px;
763
+ height: 14px;
764
+ border: 2px solid rgba(0, 0, 0, 0.2);
765
+ border-top-color: #000;
766
+ border-radius: 50%;
767
+ animation: spin 0.8s linear infinite;
768
+ }
769
+
770
+ .dev-server-status {
771
+ margin-top: 12px;
772
+ padding-top: 12px;
773
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
774
+ display: flex;
775
+ align-items: center;
776
+ gap: 8px;
777
+ font-size: 12px;
778
+ }
779
+
780
+ .dev-link {
781
+ color: #ff8c00;
782
+ text-decoration: none;
783
+ font-weight: 600;
784
+ transition: color 0.2s ease;
785
+ }
786
+
787
+ .dev-link:hover {
788
+ color: #ffa500;
789
+ }
790
+
791
+ .no-panes {
792
+ text-align: center;
793
+ padding: 100px 20px;
794
+ color: var(--text-tertiary);
795
+ animation: fadeIn 0.6s ease-out;
796
+ }
797
+
798
+ .no-panes p {
799
+ margin-bottom: 16px;
800
+ font-size: 18px;
801
+ font-weight: 500;
802
+ }
803
+
804
+ .hint {
805
+ font-size: 14px;
806
+ color: var(--text-dim);
807
+ background: var(--hint-bg);
808
+ padding: 12px 24px;
809
+ border-radius: 12px;
810
+ display: inline-block;
811
+ margin-top: 8px;
812
+ }
813
+
814
+ footer {
815
+ padding: 12px 0;
816
+ margin-top: auto;
817
+ animation: fadeIn 0.8s ease-out;
818
+ }
819
+
820
+ .footer-info {
821
+ display: flex;
822
+ justify-content: space-between;
823
+ font-size: 11px;
824
+ color: var(--text-dim);
825
+ padding: 0;
826
+ }
827
+
828
+ .footer-info span {
829
+ display: flex;
830
+ align-items: center;
831
+ gap: 8px;
832
+ }
833
+
834
+ @media (max-width: 768px) {
835
+ .container {
836
+ padding: 0 16px 24px;
837
+ }
838
+
839
+ header {
840
+ padding: 12px 18px;
841
+ gap: 8px;
842
+ }
843
+
844
+ .logo {
845
+ height: 20px;
846
+ }
847
+
848
+ h1 {
849
+ font-size: 14px;
850
+ max-width: none;
851
+ }
852
+
853
+ .session-info {
854
+ font-size: 11px;
855
+ gap: 8px;
856
+ }
857
+
858
+ .session-info span:not(.status-indicator) {
859
+ display: none;
860
+ }
861
+
862
+ main {
863
+ padding-top: 24px;
864
+ }
865
+
866
+ .panes-grid {
867
+ grid-template-columns: 1fr;
868
+ gap: 16px;
869
+ }
870
+
871
+ .footer-info {
872
+ flex-direction: column;
873
+ gap: 6px;
874
+ font-size: 10px;
875
+ }
876
+ }
877
+
878
+ /* Terminal text colors */
879
+ .term-fg-black { color: #000000; }
880
+ .term-fg-red { color: #cd3131; }
881
+ .term-fg-green { color: #0dbc79; }
882
+ .term-fg-yellow { color: #e5e510; }
883
+ .term-fg-blue { color: #2472c8; }
884
+ .term-fg-magenta { color: #bc3fbc; }
885
+ .term-fg-cyan { color: #11a8cd; }
886
+ .term-fg-white { color: #e5e5e5; }
887
+
888
+ .term-fg-bright-black { color: #666666; }
889
+ .term-fg-bright-red { color: #f14c4c; }
890
+ .term-fg-bright-green { color: #23d18b; }
891
+ .term-fg-bright-yellow { color: #f5f543; }
892
+ .term-fg-bright-blue { color: #3b8eea; }
893
+ .term-fg-bright-magenta { color: #d670d6; }
894
+ .term-fg-bright-cyan { color: #29b8db; }
895
+ .term-fg-bright-white { color: #ffffff; }
896
+
897
+ .term-bg-black { background-color: #000000; }
898
+ .term-bg-red { background-color: #cd3131; }
899
+ .term-bg-green { background-color: #0dbc79; }
900
+ .term-bg-yellow { background-color: #e5e510; }
901
+ .term-bg-blue { background-color: #2472c8; }
902
+ .term-bg-magenta { background-color: #bc3fbc; }
903
+ .term-bg-cyan { background-color: #11a8cd; }
904
+ .term-bg-white { background-color: #e5e5e5; }
905
+
906
+ .term-bold { font-weight: bold; }
907
+ .term-dim { opacity: 0.7; }
908
+ .term-italic { font-style: italic; }
909
+ .term-underline { text-decoration: underline; }
910
+ .term-strikethrough { text-decoration: line-through; }
911
+
912
+ /* Cursor styling - disabled by default for Ink apps */
913
+ .term-cursor {
914
+ /* Cursor hidden by default since Ink apps don't have meaningful cursor position */
915
+ /* background-color: rgba(255, 255, 255, 0.3); */
916
+ /* animation: cursor-blink 1s step-end infinite; */
917
+ }
918
+
919
+ /* Uncomment to enable cursor display */
920
+ /*
921
+ .term-cursor {
922
+ background-color: rgba(255, 255, 255, 0.3);
923
+ animation: cursor-blink 1s step-end infinite;
924
+ }
925
+
926
+ @keyframes cursor-blink {
927
+ 0%, 50% { background-color: rgba(255, 255, 255, 0.3); }
928
+ 51%, 100% { background-color: transparent; }
929
+ }
930
+ */
931
+
932
+ /* Action Dialog Styles */
933
+ .action-dialog-overlay {
934
+ position: fixed;
935
+ top: 0;
936
+ left: 0;
937
+ right: 0;
938
+ bottom: 0;
939
+ background: rgba(0, 0, 0, 0.7);
940
+ backdrop-filter: blur(4px);
941
+ -webkit-backdrop-filter: blur(4px);
942
+ display: flex;
943
+ align-items: center;
944
+ justify-content: center;
945
+ z-index: 1000;
946
+ animation: fadeIn 0.2s ease-out;
947
+ }
948
+
949
+ .action-dialog {
950
+ background: var(--card-bg);
951
+ backdrop-filter: blur(20px);
952
+ -webkit-backdrop-filter: blur(20px);
953
+ border: 1px solid var(--card-border);
954
+ border-radius: 16px;
955
+ padding: 24px;
956
+ max-width: 500px;
957
+ width: 90%;
958
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
959
+ animation: slideInFromTop 0.3s ease-out;
960
+ }
961
+
962
+ .action-dialog h3 {
963
+ margin: 0 0 12px 0;
964
+ font-size: 20px;
965
+ font-weight: 600;
966
+ color: var(--text-bright);
967
+ }
968
+
969
+ .action-dialog p {
970
+ margin: 0 0 20px 0;
971
+ color: var(--text-secondary);
972
+ font-size: 14px;
973
+ line-height: 1.5;
974
+ }
975
+
976
+ .dialog-buttons {
977
+ display: flex;
978
+ gap: 12px;
979
+ justify-content: flex-end;
980
+ }
981
+
982
+ .dialog-btn {
983
+ padding: 10px 20px;
984
+ border: 1px solid var(--button-border);
985
+ border-radius: 8px;
986
+ background: var(--button-bg);
987
+ color: var(--text-primary);
988
+ font-size: 14px;
989
+ font-weight: 500;
990
+ cursor: pointer;
991
+ transition: all 0.2s ease;
992
+ }
993
+
994
+ .dialog-btn:hover {
995
+ background: var(--button-hover-bg);
996
+ border-color: var(--button-hover-border);
997
+ }
998
+
999
+ .dialog-btn-primary {
1000
+ background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
1001
+ color: #000;
1002
+ border-color: transparent;
1003
+ box-shadow: 0 2px 8px rgba(96, 165, 250, 0.3);
1004
+ }
1005
+
1006
+ .dialog-btn-primary:hover {
1007
+ transform: translateY(-1px);
1008
+ box-shadow: 0 4px 12px rgba(96, 165, 250, 0.4);
1009
+ }
1010
+
1011
+ .choice-options {
1012
+ display: flex;
1013
+ flex-direction: column;
1014
+ gap: 8px;
1015
+ }
1016
+
1017
+ .choice-option-btn {
1018
+ width: 100%;
1019
+ padding: 12px 16px;
1020
+ background: var(--button-bg);
1021
+ border: 1px solid var(--button-border);
1022
+ border-radius: 8px;
1023
+ color: var(--text-primary);
1024
+ font-size: 14px;
1025
+ text-align: left;
1026
+ cursor: pointer;
1027
+ transition: all 0.2s ease;
1028
+ display: flex;
1029
+ flex-direction: column;
1030
+ gap: 4px;
1031
+ }
1032
+
1033
+ .choice-option-btn:hover {
1034
+ background: var(--button-hover-bg);
1035
+ border-color: var(--button-hover-border);
1036
+ transform: translateX(4px);
1037
+ }
1038
+
1039
+ .choice-option-btn.danger {
1040
+ border-color: rgba(248, 113, 113, 0.3);
1041
+ background: rgba(248, 113, 113, 0.1);
1042
+ }
1043
+
1044
+ .choice-option-btn.danger:hover {
1045
+ border-color: rgba(248, 113, 113, 0.5);
1046
+ background: rgba(248, 113, 113, 0.2);
1047
+ }
1048
+
1049
+ .option-description {
1050
+ font-size: 12px;
1051
+ color: var(--text-tertiary);
1052
+ font-weight: normal;
1053
+ }
1054
+
1055
+ /* Terminal Page Layout */
1056
+ .terminal-page {
1057
+ display: flex;
1058
+ flex-direction: column;
1059
+ height: 100vh;
1060
+ background: #000;
1061
+ }
1062
+
1063
+ .back-button {
1064
+ color: #e0e0e0;
1065
+ text-decoration: none;
1066
+ font-size: 14px;
1067
+ font-weight: 500;
1068
+ transition: color 0.2s;
1069
+ white-space: nowrap;
1070
+ flex-shrink: 0;
1071
+ }
1072
+
1073
+ .back-button:hover {
1074
+ color: #fff;
1075
+ }
1076
+
1077
+ .terminal-content {
1078
+ flex: 1;
1079
+ overflow: auto;
1080
+ padding: 10px;
1081
+ }
1082
+
1083
+ .terminal-page .terminal-output {
1084
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
1085
+ line-height: 1.0;
1086
+ color: #f0f0f0;
1087
+ margin: 0;
1088
+ min-height: 100%;
1089
+ }
1090
+
1091
+ .terminal-row {
1092
+ white-space: pre;
1093
+ margin: 0;
1094
+ padding: 0;
1095
+ line-height: 1.0;
1096
+ }
1097
+
1098
+ /* Mobile toolbar */
1099
+ .mobile-toolbar {
1100
+ display: flex;
1101
+ gap: 6px;
1102
+ padding: 8px;
1103
+ background: #1a1a1a;
1104
+ border-bottom: 1px solid #333;
1105
+ overflow-x: auto;
1106
+ flex-wrap: nowrap;
1107
+ }
1108
+
1109
+ .toolbar-key {
1110
+ background: #2d2d2d;
1111
+ border: 1px solid #444;
1112
+ border-radius: 4px;
1113
+ color: #e0e0e0;
1114
+ padding: 8px 12px;
1115
+ font-size: 13px;
1116
+ font-family: 'SF Mono', Monaco, monospace;
1117
+ cursor: pointer;
1118
+ flex-shrink: 0;
1119
+ min-width: 44px;
1120
+ transition: all 0.15s;
1121
+ user-select: none;
1122
+ -webkit-tap-highlight-color: transparent;
1123
+ }
1124
+
1125
+ .toolbar-key:active {
1126
+ background: #3d3d3d;
1127
+ transform: scale(0.95);
1128
+ }
1129
+
1130
+ .toolbar-key.active {
1131
+ background: #667eea;
1132
+ border-color: #667eea;
1133
+ color: #fff;
1134
+ }
1135
+
1136
+ /* Hidden mobile input */
1137
+ .mobile-input {
1138
+ position: absolute;
1139
+ left: -9999px;
1140
+ width: 1px;
1141
+ height: 1px;
1142
+ opacity: 0.01;
1143
+ pointer-events: none;
1144
+ }
1145
+
1146
+ /* Actions bar */
1147
+ .actions-bar {
1148
+ display: flex;
1149
+ justify-content: flex-end;
1150
+ margin-bottom: 20px;
1151
+ padding: 0 4px;
1152
+ }
1153
+
1154
+ .create-pane-button {
1155
+ display: flex;
1156
+ align-items: center;
1157
+ gap: 6px;
1158
+ padding: 6px 12px;
1159
+ background: var(--button-bg);
1160
+ color: var(--text-primary);
1161
+ border: 1px solid var(--button-border);
1162
+ border-radius: 6px;
1163
+ font-size: 13px;
1164
+ font-weight: 500;
1165
+ cursor: pointer;
1166
+ transition: all 0.15s ease;
1167
+ }
1168
+
1169
+ .create-pane-button:hover:not(:disabled) {
1170
+ background: var(--button-hover-bg);
1171
+ border-color: var(--button-hover-border);
1172
+ }
1173
+
1174
+ .create-pane-button:disabled {
1175
+ opacity: 0.5;
1176
+ cursor: not-allowed;
1177
+ }
1178
+
1179
+ .create-pane-button svg {
1180
+ width: 14px;
1181
+ height: 14px;
1182
+ fill: none;
1183
+ }
1184
+
1185
+ /* Modal styles */
1186
+ .modal-overlay {
1187
+ position: fixed;
1188
+ top: 0;
1189
+ left: 0;
1190
+ width: 100%;
1191
+ height: 100%;
1192
+ background: rgba(0, 0, 0, 0.7);
1193
+ backdrop-filter: blur(8px);
1194
+ -webkit-backdrop-filter: blur(8px);
1195
+ display: flex;
1196
+ align-items: center;
1197
+ justify-content: center;
1198
+ z-index: 2000;
1199
+ animation: fadeIn 0.2s ease-out;
1200
+ }
1201
+
1202
+ .modal-dialog {
1203
+ background: var(--card-bg);
1204
+ backdrop-filter: blur(20px);
1205
+ -webkit-backdrop-filter: blur(20px);
1206
+ border: 1px solid var(--card-border);
1207
+ border-radius: 16px;
1208
+ width: 90%;
1209
+ max-width: 600px;
1210
+ max-height: 90vh;
1211
+ overflow: hidden;
1212
+ display: flex;
1213
+ flex-direction: column;
1214
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
1215
+ animation: slideUp 0.3s ease-out;
1216
+ }
1217
+
1218
+ @keyframes slideUp {
1219
+ from {
1220
+ opacity: 0;
1221
+ transform: translateY(20px);
1222
+ }
1223
+ to {
1224
+ opacity: 1;
1225
+ transform: translateY(0);
1226
+ }
1227
+ }
1228
+
1229
+ .modal-header {
1230
+ display: flex;
1231
+ align-items: center;
1232
+ justify-content: space-between;
1233
+ padding: 20px 24px;
1234
+ border-bottom: 1px solid var(--border-color);
1235
+ }
1236
+
1237
+ .modal-header h2 {
1238
+ font-size: 20px;
1239
+ font-weight: 600;
1240
+ color: var(--text-primary);
1241
+ margin: 0;
1242
+ }
1243
+
1244
+ .modal-close {
1245
+ background: none;
1246
+ border: none;
1247
+ color: var(--text-secondary);
1248
+ font-size: 32px;
1249
+ line-height: 1;
1250
+ cursor: pointer;
1251
+ padding: 0;
1252
+ width: 32px;
1253
+ height: 32px;
1254
+ display: flex;
1255
+ align-items: center;
1256
+ justify-content: center;
1257
+ border-radius: 6px;
1258
+ transition: all 0.2s ease;
1259
+ }
1260
+
1261
+ .modal-close:hover {
1262
+ background: var(--button-hover-bg);
1263
+ color: var(--text-primary);
1264
+ }
1265
+
1266
+ .modal-body {
1267
+ padding: 24px;
1268
+ overflow-y: auto;
1269
+ flex: 1;
1270
+ }
1271
+
1272
+ .form-group {
1273
+ margin-bottom: 20px;
1274
+ }
1275
+
1276
+ .form-group:last-child {
1277
+ margin-bottom: 0;
1278
+ }
1279
+
1280
+ .form-group label {
1281
+ display: block;
1282
+ font-size: 14px;
1283
+ font-weight: 600;
1284
+ color: var(--text-primary);
1285
+ margin-bottom: 8px;
1286
+ }
1287
+
1288
+ .modal-textarea {
1289
+ width: 100%;
1290
+ padding: 12px;
1291
+ background: var(--input-bg);
1292
+ border: 1px solid var(--input-border);
1293
+ border-radius: 8px;
1294
+ color: var(--text-primary);
1295
+ font-size: 14px;
1296
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1297
+ resize: vertical;
1298
+ min-height: 100px;
1299
+ transition: all 0.2s ease;
1300
+ }
1301
+
1302
+ .modal-textarea:focus {
1303
+ outline: none;
1304
+ border-color: var(--input-focus-border);
1305
+ background: var(--input-focus-bg);
1306
+ box-shadow: 0 0 0 3px var(--input-focus-shadow);
1307
+ }
1308
+
1309
+ .modal-textarea:disabled {
1310
+ opacity: 0.5;
1311
+ cursor: not-allowed;
1312
+ }
1313
+
1314
+ .input-hint {
1315
+ font-size: 12px;
1316
+ color: var(--text-tertiary);
1317
+ margin-top: 6px;
1318
+ }
1319
+
1320
+ .agent-selector {
1321
+ display: flex;
1322
+ gap: 12px;
1323
+ }
1324
+
1325
+ .agent-option {
1326
+ flex: 1;
1327
+ display: flex;
1328
+ align-items: center;
1329
+ gap: 12px;
1330
+ padding: 12px 20px;
1331
+ background: var(--button-bg);
1332
+ border: 2px solid var(--button-border);
1333
+ border-radius: 8px;
1334
+ color: var(--text-primary);
1335
+ font-size: 14px;
1336
+ font-weight: 600;
1337
+ text-transform: capitalize;
1338
+ cursor: pointer;
1339
+ transition: all 0.2s ease;
1340
+ }
1341
+
1342
+ .agent-logo {
1343
+ width: 40px;
1344
+ height: 40px;
1345
+ flex-shrink: 0;
1346
+ }
1347
+
1348
+ .agent-option:hover:not(:disabled) {
1349
+ background: var(--button-hover-bg);
1350
+ border-color: var(--button-hover-border);
1351
+ }
1352
+
1353
+ .agent-option.selected {
1354
+ background: var(--input-focus-bg);
1355
+ border-color: var(--input-focus-border);
1356
+ color: var(--text-bright);
1357
+ }
1358
+
1359
+ .agent-option:disabled {
1360
+ opacity: 0.5;
1361
+ cursor: not-allowed;
1362
+ }
1363
+
1364
+ .modal-footer {
1365
+ display: flex;
1366
+ gap: 12px;
1367
+ padding: 20px 24px;
1368
+ border-top: 1px solid var(--border-color);
1369
+ justify-content: flex-end;
1370
+ }
1371
+
1372
+ .modal-button {
1373
+ padding: 10px 24px;
1374
+ border-radius: 8px;
1375
+ font-size: 14px;
1376
+ font-weight: 600;
1377
+ cursor: pointer;
1378
+ transition: all 0.2s ease;
1379
+ border: none;
1380
+ display: flex;
1381
+ align-items: center;
1382
+ gap: 8px;
1383
+ }
1384
+
1385
+ .modal-button-secondary {
1386
+ background: var(--button-bg);
1387
+ color: var(--text-primary);
1388
+ border: 1px solid var(--button-border);
1389
+ }
1390
+
1391
+ .modal-button-secondary:hover:not(:disabled) {
1392
+ background: var(--button-hover-bg);
1393
+ border-color: var(--button-hover-border);
1394
+ }
1395
+
1396
+ .modal-button-primary {
1397
+ background: var(--input-focus-bg);
1398
+ color: var(--text-bright);
1399
+ border: 1px solid var(--input-focus-border);
1400
+ }
1401
+
1402
+ .modal-button-primary:hover:not(:disabled) {
1403
+ background: var(--button-hover-bg);
1404
+ border-color: var(--button-hover-border);
1405
+ }
1406
+
1407
+ .modal-button:disabled {
1408
+ opacity: 0.5;
1409
+ cursor: not-allowed;
1410
+ }
1411
+
1412
+ .button-loader {
1413
+ width: 16px;
1414
+ height: 16px;
1415
+ border: 2px solid rgba(255, 255, 255, 0.3);
1416
+ border-top-color: #fff;
1417
+ border-radius: 50%;
1418
+ animation: spin 0.6s linear infinite;
1419
+ }
1420
+
1421
+ @keyframes spin {
1422
+ to { transform: rotate(360deg); }
1423
+ }`;
1424
+ }
1425
+ export function getDashboardJs() {
1426
+ return `// Dashboard with Vue.js
1427
+ import { createApp } from '/vue.esm-browser.js';
1428
+
1429
+ let refreshInterval = null;
1430
+
1431
+ const app = createApp({
1432
+ data() {
1433
+ return {
1434
+ projectName: 'Loading...',
1435
+ sessionName: '',
1436
+ connected: false,
1437
+ panes: [],
1438
+ lastUpdate: null,
1439
+ timeSinceUpdate: 'Never',
1440
+ promptInputs: {}, // Map of pane ID to prompt text
1441
+ sendingPrompts: new Set(), // Set of pane IDs currently sending
1442
+ queuedMessages: {}, // Map of pane ID to temporary "queued" message
1443
+ theme: localStorage.getItem('dmux-theme') || 'dark', // Theme state
1444
+ expandedPrompts: new Set(), // Set of pane IDs with expanded initial prompts
1445
+ loadingOptions: new Set(), // Set of pane IDs with loading option dialogs
1446
+ showCreateDialog: false,
1447
+ newPanePrompt: '',
1448
+ newPaneAgent: null,
1449
+ creatingPane: false,
1450
+ availableAgents: [],
1451
+ needsAgentChoice: false,
1452
+ createStep: 'prompt', // 'prompt' or 'agent'
1453
+ // Action system
1454
+ actions: [], // Available actions from API
1455
+ paneActions: {}, // Map of pane ID to available actions
1456
+ showActionMenu: null, // Pane ID with open action menu
1457
+ actionDialog: null, // Current action dialog { type: 'confirm'|'choice', ... }
1458
+ executingAction: false // Whether an action is currently executing
1459
+ };
1460
+ },
1461
+ template: \`
1462
+ <header>
1463
+ <img src="https://cdn.formk.it/dmux/dmux.png" alt="dmux" class="logo" />
1464
+ <h1>{{ projectName }}</h1>
1465
+ <div class="session-info">
1466
+ <button @click="toggleTheme" class="theme-toggle" :title="theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'">
1467
+ <svg v-if="theme === 'dark'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
1468
+ <path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"/>
1469
+ </svg>
1470
+ <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
1471
+ <path d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6.5a9 9 0 009 9 8.97 8.97 0 003.963-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"/>
1472
+ </svg>
1473
+ </button>
1474
+ <span v-if="sessionName">{{ sessionName }}</span>
1475
+ <span class="status-indicator" :style="{ color: connected ? '#4ade80' : '#f87171' }">●</span>
1476
+ </div>
1477
+ </header>
1478
+
1479
+ <div class="container">
1480
+ <main>
1481
+ <div class="actions-bar">
1482
+ <button @click="openCreateDialog" class="create-pane-button" :disabled="creatingPane">
1483
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
1484
+ <path d="M12 4.5v15m7.5-7.5h-15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1485
+ </svg>
1486
+ Create New Pane
1487
+ </button>
1488
+ </div>
1489
+
1490
+ <div v-if="panes.length === 0" class="no-panes">
1491
+ <p>No dmux panes active</p>
1492
+ <p class="hint">Click "Create New Pane" above or press 'n' in dmux</p>
1493
+ </div>
1494
+
1495
+ <div v-else class="panes-grid">
1496
+ <div
1497
+ v-for="pane in panes"
1498
+ :key="pane.id"
1499
+ class="pane-card"
1500
+ >
1501
+ <div class="pane-header">
1502
+ <div class="pane-header-content">
1503
+ <a :href="'/panes/' + pane.id" class="pane-title-link">
1504
+ <span class="pane-title">{{ pane.slug }}</span>
1505
+ <span class="pane-arrow">→</span>
1506
+ </a>
1507
+ <div class="pane-meta">
1508
+ <span class="pane-agent" :class="pane.agent || ''">{{ pane.agent || 'unknown' }}</span>
1509
+ <span class="pane-id">{{ pane.paneId }}</span>
1510
+ </div>
1511
+ </div>
1512
+ <button @click="toggleActionMenu(pane.id)" class="action-menu-btn" title="Actions">
1513
+ <span>⋮</span>
1514
+ </button>
1515
+ </div>
1516
+
1517
+ <!-- Action Menu Dropdown -->
1518
+ <div v-if="showActionMenu === pane.id && paneActions[pane.id]" class="action-menu-dropdown">
1519
+ <button
1520
+ v-for="action in paneActions[pane.id]"
1521
+ :key="action.id"
1522
+ @click="executeAction(pane, action)"
1523
+ class="action-menu-item"
1524
+ :disabled="executingAction"
1525
+ >
1526
+ <span class="action-icon">{{ action.icon || '•' }}</span>
1527
+ <span class="action-label">{{ action.label }}</span>
1528
+ </button>
1529
+ </div>
1530
+
1531
+ <div class="pane-prompt-section">
1532
+ <div
1533
+ class="pane-prompt-header"
1534
+ @click="togglePrompt(pane.id)"
1535
+ :class="{ 'expanded': expandedPrompts.has(pane.id) }"
1536
+ >
1537
+ <span class="prompt-label">Initial Prompt</span>
1538
+ <span class="expand-icon">{{ expandedPrompts.has(pane.id) ? '▼' : '▶' }}</span>
1539
+ </div>
1540
+ <div v-if="expandedPrompts.has(pane.id)" class="pane-prompt">
1541
+ {{ pane.prompt || 'No prompt' }}
1542
+ </div>
1543
+ </div>
1544
+
1545
+ <!-- Show agent summary when idle -->
1546
+ <div v-if="pane.agentStatus === 'idle' && pane.agentSummary" class="agent-summary">
1547
+ {{ pane.agentSummary }}
1548
+ </div>
1549
+
1550
+ <div class="pane-interactive" @click.prevent>
1551
+ <!-- Options Dialog (when waiting with options) -->
1552
+ <div v-if="pane.agentStatus === 'waiting' && pane.options && pane.options.length > 0" class="options-dialog">
1553
+ <div class="options-question">{{ pane.optionsQuestion || 'Choose an option:' }}</div>
1554
+ <div v-if="pane.potentialHarm && pane.potentialHarm.hasRisk" class="options-warning">
1555
+ ⚠️ {{ pane.potentialHarm.description }}
1556
+ </div>
1557
+ <div v-if="loadingOptions.has(pane.id)" class="analyzing-state">
1558
+ <div class="loader-spinner"></div>
1559
+ <span>Processing selection...</span>
1560
+ </div>
1561
+ <div v-else class="options-buttons">
1562
+ <button
1563
+ v-for="option in pane.options"
1564
+ :key="option.action"
1565
+ @click="selectOption(pane, option)"
1566
+ class="option-button"
1567
+ :class="{ 'option-button-danger': pane.potentialHarm && pane.potentialHarm.hasRisk }"
1568
+ :disabled="loadingOptions.has(pane.id)"
1569
+ >
1570
+ {{ option.action }}
1571
+ </button>
1572
+ </div>
1573
+ </div>
1574
+
1575
+ <!-- Analyzing (show loader) -->
1576
+ <div v-else-if="pane.agentStatus === 'analyzing'" class="analyzing-state">
1577
+ <div class="loader-spinner"></div>
1578
+ <span>Analyzing...</span>
1579
+ </div>
1580
+
1581
+ <!-- Working/Idle (show prompt input) -->
1582
+ <div v-else>
1583
+ <div class="prompt-input-wrapper">
1584
+ <textarea
1585
+ v-model="promptInputs[pane.id]"
1586
+ @input="autoExpand"
1587
+ :placeholder="pane.agentStatus === 'working' ? 'Queue a prompt...' : 'Send a prompt...'"
1588
+ :disabled="sendingPrompts.has(pane.id)"
1589
+ class="prompt-textarea"
1590
+ rows="1"
1591
+ ></textarea>
1592
+ <button
1593
+ @click="sendPrompt(pane)"
1594
+ :disabled="!promptInputs[pane.id] || sendingPrompts.has(pane.id)"
1595
+ class="send-button"
1596
+ :title="pane.agentStatus === 'working' ? 'Queue prompt' : 'Send prompt'"
1597
+ >
1598
+ <span v-if="sendingPrompts.has(pane.id)" class="button-loader"></span>
1599
+ <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 988.44 1200.05">
1600
+ <path d="M425.13,28.37L30.09,423.41C11.19,441.37.34,466.2,0,492.27c-.34,26.07,9.86,51.17,28.29,69.61,18.43,18.45,43.52,28.67,69.59,28.35,26.07-.31,50.91-11.14,68.88-30.02l233.16-233.52v776.64c0,34.56,18.43,66.48,48.36,83.76,29.93,17.28,66.8,17.28,96.72,0,29.93-17.28,48.36-49.21,48.36-83.76V328.85l231.72,231.36c24.63,23.41,59.74,32.18,92.48,23.09,32.74-9.08,58.32-34.68,67.38-67.43,9.05-32.75.25-67.85-23.18-92.46L566.73,28.37C548.63,10.16,524-.04,498.33.05c-.8-.06-1.6-.06-2.4,0-.8-.06-1.6-.06-2.4,0-25.65,0-50.25,10.19-68.4,28.32h0Z"/>
1601
+ </svg>
1602
+ </button>
1603
+ </div>
1604
+ <div v-if="queuedMessages[pane.id]" class="queued-message">
1605
+ ✓ {{ queuedMessages[pane.id] }}
1606
+ </div>
1607
+ </div>
1608
+ </div>
1609
+
1610
+ <div v-if="pane.devStatus && pane.devStatus !== 'stopped'" class="dev-server-status">
1611
+ <span class="status-label">Dev Server:</span>
1612
+ <span class="status-badge" :class="pane.devStatus">{{ pane.devStatus }}</span>
1613
+ <a v-if="pane.devUrl" :href="pane.devUrl" target="_blank" class="dev-link">↗</a>
1614
+ </div>
1615
+ </div>
1616
+ </div>
1617
+ </main>
1618
+
1619
+ <!-- Action Dialogs -->
1620
+ <div v-if="actionDialog" class="action-dialog-overlay" @click.self="closeActionDialog">
1621
+ <!-- Confirm Dialog -->
1622
+ <div v-if="actionDialog.type === 'confirm'" class="action-dialog">
1623
+ <h3>{{ actionDialog.title }}</h3>
1624
+ <p>{{ actionDialog.message }}</p>
1625
+ <div class="dialog-buttons">
1626
+ <button @click="handleConfirm(true)" class="dialog-btn dialog-btn-primary">
1627
+ {{ actionDialog.yesLabel || 'Yes' }}
1628
+ </button>
1629
+ <button @click="handleConfirm(false)" class="dialog-btn">
1630
+ {{ actionDialog.noLabel || 'No' }}
1631
+ </button>
1632
+ </div>
1633
+ </div>
1634
+
1635
+ <!-- Choice Dialog -->
1636
+ <div v-if="actionDialog.type === 'choice'" class="action-dialog">
1637
+ <h3>{{ actionDialog.title }}</h3>
1638
+ <p v-if="actionDialog.message">{{ actionDialog.message }}</p>
1639
+ <div class="choice-options">
1640
+ <button
1641
+ v-for="option in actionDialog.options"
1642
+ :key="option.id"
1643
+ @click="handleChoice(option.id)"
1644
+ class="choice-option-btn"
1645
+ :class="{ 'danger': option.danger }"
1646
+ >
1647
+ {{ option.label }}
1648
+ <span v-if="option.description" class="option-description">{{ option.description }}</span>
1649
+ </button>
1650
+ </div>
1651
+ </div>
1652
+ </div>
1653
+
1654
+ <footer>
1655
+ <div class="footer-info">
1656
+ <span>Auto-refresh: <span>ON</span></span>
1657
+ <span>Last update: <span>{{ timeSinceUpdate }}</span></span>
1658
+ </div>
1659
+ </footer>
1660
+ </div>
1661
+
1662
+ <!-- Create Pane Dialog -->
1663
+ <div v-if="showCreateDialog" class="modal-overlay" @click.self="closeCreateDialog">
1664
+ <div class="modal-dialog">
1665
+ <div class="modal-header">
1666
+ <h2>{{ createStep === 'prompt' ? 'Create New Pane' : 'Select Agent' }}</h2>
1667
+ <button @click="closeCreateDialog" class="modal-close">&times;</button>
1668
+ </div>
1669
+ <div class="modal-body">
1670
+ <!-- Step 1: Prompt -->
1671
+ <div v-if="createStep === 'prompt'" class="form-group">
1672
+ <label for="pane-prompt">Prompt</label>
1673
+ <textarea
1674
+ id="pane-prompt"
1675
+ v-model="newPanePrompt"
1676
+ placeholder="Enter task description (e.g., 'Add tests for the authentication module')"
1677
+ rows="4"
1678
+ class="modal-textarea"
1679
+ :disabled="creatingPane"
1680
+ @keydown.meta.enter="submitPrompt"
1681
+ @keydown.ctrl.enter="submitPrompt"
1682
+ ></textarea>
1683
+ <div class="input-hint">Cmd/Ctrl + Enter to continue</div>
1684
+ </div>
1685
+
1686
+ <!-- Step 2: Agent Selection -->
1687
+ <div v-if="createStep === 'agent'" class="form-group">
1688
+ <label>Select Agent</label>
1689
+ <div class="agent-selector">
1690
+ <button
1691
+ v-for="agent in availableAgents"
1692
+ :key="agent"
1693
+ @click="newPaneAgent = agent"
1694
+ class="agent-option"
1695
+ :class="{ 'selected': newPaneAgent === agent }"
1696
+ :disabled="creatingPane"
1697
+ >
1698
+ <img
1699
+ :src="'https://cdn.formk.it/dmux/' + agent + '.svg'"
1700
+ :alt="agent"
1701
+ class="agent-logo"
1702
+ />
1703
+ <span>{{ agent }}</span>
1704
+ </button>
1705
+ </div>
1706
+ </div>
1707
+ </div>
1708
+ <div class="modal-footer">
1709
+ <button
1710
+ v-if="createStep === 'agent'"
1711
+ @click="createStep = 'prompt'"
1712
+ class="modal-button modal-button-secondary"
1713
+ :disabled="creatingPane"
1714
+ >
1715
+ Back
1716
+ </button>
1717
+ <button @click="closeCreateDialog" class="modal-button modal-button-secondary" :disabled="creatingPane">
1718
+ Cancel
1719
+ </button>
1720
+ <button
1721
+ v-if="createStep === 'prompt'"
1722
+ @click="submitPrompt"
1723
+ class="modal-button modal-button-primary"
1724
+ :disabled="!newPanePrompt.trim() || creatingPane"
1725
+ >
1726
+ <span v-if="creatingPane" class="button-loader"></span>
1727
+ <span v-else>Continue</span>
1728
+ </button>
1729
+ <button
1730
+ v-if="createStep === 'agent'"
1731
+ @click="createPane"
1732
+ class="modal-button modal-button-primary"
1733
+ :disabled="!newPaneAgent || creatingPane"
1734
+ >
1735
+ <span v-if="creatingPane" class="button-loader"></span>
1736
+ <span v-else>Create Pane</span>
1737
+ </button>
1738
+ </div>
1739
+ </div>
1740
+ </div>
1741
+ \`,
1742
+ methods: {
1743
+ toggleTheme() {
1744
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
1745
+ localStorage.setItem('dmux-theme', this.theme);
1746
+ document.documentElement.setAttribute('data-theme', this.theme);
1747
+ },
1748
+ togglePrompt(paneId) {
1749
+ if (this.expandedPrompts.has(paneId)) {
1750
+ this.expandedPrompts.delete(paneId);
1751
+ } else {
1752
+ this.expandedPrompts.add(paneId);
1753
+ }
1754
+ // Force reactivity
1755
+ this.expandedPrompts = new Set(this.expandedPrompts);
1756
+ },
1757
+ openCreateDialog() {
1758
+ this.showCreateDialog = true;
1759
+ this.newPanePrompt = '';
1760
+ this.newPaneAgent = null;
1761
+ this.availableAgents = [];
1762
+ this.needsAgentChoice = false;
1763
+ this.createStep = 'prompt';
1764
+ // Focus the textarea after dialog opens
1765
+ setTimeout(() => {
1766
+ const textarea = document.getElementById('pane-prompt');
1767
+ if (textarea) textarea.focus();
1768
+ }, 100);
1769
+ },
1770
+ closeCreateDialog() {
1771
+ this.showCreateDialog = false;
1772
+ this.newPanePrompt = '';
1773
+ this.newPaneAgent = null;
1774
+ this.creatingPane = false;
1775
+ this.createStep = 'prompt';
1776
+ this.availableAgents = [];
1777
+ this.needsAgentChoice = false;
1778
+ },
1779
+ async submitPrompt() {
1780
+ if (!this.newPanePrompt.trim() || this.creatingPane) return;
1781
+
1782
+ this.creatingPane = true;
1783
+
1784
+ try {
1785
+ // First API call: Send prompt without agent
1786
+ const response = await fetch('/api/panes', {
1787
+ method: 'POST',
1788
+ headers: { 'Content-Type': 'application/json' },
1789
+ body: JSON.stringify({
1790
+ prompt: this.newPanePrompt.trim()
1791
+ })
1792
+ });
1793
+
1794
+ const data = await response.json();
1795
+
1796
+ if (data.success) {
1797
+ // Pane created without agent selection (only one agent available)
1798
+ this.closeCreateDialog();
1799
+ await this.fetchPanes();
1800
+ } else if (data.needsAgentChoice) {
1801
+ // Agent selection required
1802
+ this.availableAgents = data.availableAgents || [];
1803
+ this.needsAgentChoice = true;
1804
+ this.createStep = 'agent';
1805
+ this.creatingPane = false;
1806
+ // Auto-select first agent
1807
+ if (this.availableAgents.length > 0) {
1808
+ this.newPaneAgent = this.availableAgents[0];
1809
+ }
1810
+ } else {
1811
+ // Error occurred
1812
+ console.error('Failed to create pane:', data.error);
1813
+ alert('Failed to create pane: ' + (data.error || 'Unknown error'));
1814
+ this.creatingPane = false;
1815
+ }
1816
+ } catch (err) {
1817
+ console.error('Failed to create pane:', err);
1818
+ alert('Failed to create pane: ' + err.message);
1819
+ this.creatingPane = false;
1820
+ }
1821
+ },
1822
+ async createPane() {
1823
+ if (!this.newPaneAgent || this.creatingPane) return;
1824
+
1825
+ this.creatingPane = true;
1826
+
1827
+ try {
1828
+ // Second API call: Send prompt with selected agent
1829
+ const response = await fetch('/api/panes', {
1830
+ method: 'POST',
1831
+ headers: { 'Content-Type': 'application/json' },
1832
+ body: JSON.stringify({
1833
+ prompt: this.newPanePrompt.trim(),
1834
+ agent: this.newPaneAgent
1835
+ })
1836
+ });
1837
+
1838
+ const data = await response.json();
1839
+
1840
+ if (data.success) {
1841
+ // Pane created successfully
1842
+ this.closeCreateDialog();
1843
+ await this.fetchPanes();
1844
+ } else {
1845
+ // Error occurred
1846
+ console.error('Failed to create pane:', data.error);
1847
+ alert('Failed to create pane: ' + (data.error || 'Unknown error'));
1848
+ }
1849
+ } catch (err) {
1850
+ console.error('Failed to create pane:', err);
1851
+ alert('Failed to create pane: ' + err.message);
1852
+ } finally {
1853
+ this.creatingPane = false;
1854
+ }
1855
+ },
1856
+ async fetchPanes() {
1857
+ try {
1858
+ const response = await fetch('/api/panes');
1859
+ const data = await response.json();
1860
+ this.projectName = data.projectName || 'Unknown Project';
1861
+ this.sessionName = data.sessionName || '';
1862
+ this.panes = data.panes || [];
1863
+ this.lastUpdate = new Date();
1864
+ this.connected = true;
1865
+ this.updateTimeSinceUpdate();
1866
+
1867
+ // Clear loading state for panes that are no longer waiting
1868
+ this.loadingOptions.forEach(paneId => {
1869
+ const pane = this.panes.find(p => p.id === paneId);
1870
+ if (!pane || pane.agentStatus !== 'waiting') {
1871
+ this.loadingOptions.delete(paneId);
1872
+ }
1873
+ });
1874
+ // Force reactivity
1875
+ this.loadingOptions = new Set(this.loadingOptions);
1876
+ } catch (err) {
1877
+ this.connected = false;
1878
+ }
1879
+ },
1880
+ async sendKeys(paneId, keys) {
1881
+ try {
1882
+ const response = await fetch(\`/api/keys/\${paneId}\`, {
1883
+ method: 'POST',
1884
+ headers: { 'Content-Type': 'application/json' },
1885
+ body: JSON.stringify({ key: keys })
1886
+ });
1887
+ return response.ok;
1888
+ } catch (err) {
1889
+ console.error('Failed to send keys:', err);
1890
+ return false;
1891
+ }
1892
+ },
1893
+ async sendPrompt(pane) {
1894
+ const prompt = this.promptInputs[pane.id];
1895
+ if (!prompt || this.sendingPrompts.has(pane.id)) return;
1896
+
1897
+ this.sendingPrompts.add(pane.id);
1898
+
1899
+ try {
1900
+ // Send each character of the prompt
1901
+ for (const char of prompt) {
1902
+ await this.sendKeys(pane.id, char);
1903
+ await new Promise(resolve => setTimeout(resolve, 5)); // Small delay between chars
1904
+ }
1905
+
1906
+ // Send Enter to submit
1907
+ await this.sendKeys(pane.id, 'Enter');
1908
+
1909
+ // Show queued message for working status
1910
+ if (pane.agentStatus === 'working') {
1911
+ this.queuedMessages[pane.id] = 'Sent to queue';
1912
+ setTimeout(() => {
1913
+ delete this.queuedMessages[pane.id];
1914
+ }, 3000);
1915
+ }
1916
+
1917
+ // Clear input
1918
+ this.promptInputs[pane.id] = '';
1919
+ } catch (err) {
1920
+ console.error('Failed to send prompt:', err);
1921
+ } finally {
1922
+ this.sendingPrompts.delete(pane.id);
1923
+ }
1924
+ },
1925
+ async selectOption(pane, option) {
1926
+ if (!option.keys || option.keys.length === 0) return;
1927
+
1928
+ // Set loading state immediately
1929
+ this.loadingOptions.add(pane.id);
1930
+ // Force reactivity
1931
+ this.loadingOptions = new Set(this.loadingOptions);
1932
+
1933
+ try {
1934
+ // Send the first key in the array (usually the main option key)
1935
+ const key = option.keys[0];
1936
+ await this.sendKeys(pane.id, key);
1937
+
1938
+ // Clear loading state after a short delay to ensure the state has transitioned
1939
+ // The 2-second delay in the worker will prevent premature state detection
1940
+ setTimeout(() => {
1941
+ this.loadingOptions.delete(pane.id);
1942
+ this.loadingOptions = new Set(this.loadingOptions);
1943
+ }, 500);
1944
+ } catch (err) {
1945
+ console.error('Failed to select option:', err);
1946
+ // Clear loading state on error
1947
+ this.loadingOptions.delete(pane.id);
1948
+ this.loadingOptions = new Set(this.loadingOptions);
1949
+ }
1950
+ },
1951
+ autoExpand(event) {
1952
+ const textarea = event.target;
1953
+ textarea.style.height = 'auto';
1954
+ textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
1955
+ },
1956
+ async toggleActionMenu(paneId) {
1957
+ if (this.showActionMenu === paneId) {
1958
+ this.showActionMenu = null;
1959
+ } else {
1960
+ // Load actions for this pane if not already loaded
1961
+ if (!this.paneActions[paneId]) {
1962
+ try {
1963
+ const response = await fetch(\`/api/panes/\${paneId}/actions\`);
1964
+ const data = await response.json();
1965
+ this.paneActions[paneId] = data.actions || [];
1966
+ } catch (err) {
1967
+ console.error('Failed to load actions:', err);
1968
+ this.paneActions[paneId] = [];
1969
+ }
1970
+ }
1971
+ this.showActionMenu = paneId;
1972
+ }
1973
+ },
1974
+ async executeAction(pane, action) {
1975
+ this.executingAction = true;
1976
+ this.showActionMenu = null;
1977
+
1978
+ try {
1979
+ const response = await fetch(\`/api/panes/\${pane.id}/actions/\${action.id}\`, {
1980
+ method: 'POST',
1981
+ headers: { 'Content-Type': 'application/json' },
1982
+ body: JSON.stringify({})
1983
+ });
1984
+
1985
+ const result = await response.json();
1986
+
1987
+ // Handle different response types
1988
+ if (result.requiresInteraction) {
1989
+ if (result.interactionType === 'confirm') {
1990
+ this.actionDialog = {
1991
+ type: 'confirm',
1992
+ title: result.confirmData.title,
1993
+ message: result.confirmData.message,
1994
+ yesLabel: result.confirmData.yesLabel,
1995
+ noLabel: result.confirmData.noLabel,
1996
+ callbackId: result.confirmData.callbackId
1997
+ };
1998
+ } else if (result.interactionType === 'choice') {
1999
+ this.actionDialog = {
2000
+ type: 'choice',
2001
+ title: result.choiceData.title,
2002
+ message: result.choiceData.message,
2003
+ options: result.choiceData.options,
2004
+ callbackId: result.choiceData.callbackId
2005
+ };
2006
+ }
2007
+ }
2008
+ } catch (err) {
2009
+ console.error('Failed to execute action:', err);
2010
+ } finally {
2011
+ this.executingAction = false;
2012
+ }
2013
+ },
2014
+ async handleConfirm(confirmed) {
2015
+ if (!this.actionDialog || !this.actionDialog.callbackId) return;
2016
+
2017
+ try {
2018
+ const response = await fetch(\`/api/callbacks/confirm/\${this.actionDialog.callbackId}\`, {
2019
+ method: 'POST',
2020
+ headers: { 'Content-Type': 'application/json' },
2021
+ body: JSON.stringify({ confirmed })
2022
+ });
2023
+
2024
+ await response.json();
2025
+ } catch (err) {
2026
+ console.error('Failed to handle confirm:', err);
2027
+ } finally {
2028
+ this.actionDialog = null;
2029
+ }
2030
+ },
2031
+ async handleChoice(optionId) {
2032
+ if (!this.actionDialog || !this.actionDialog.callbackId) return;
2033
+
2034
+ try {
2035
+ const response = await fetch(\`/api/callbacks/choice/\${this.actionDialog.callbackId}\`, {
2036
+ method: 'POST',
2037
+ headers: { 'Content-Type': 'application/json' },
2038
+ body: JSON.stringify({ optionId })
2039
+ });
2040
+
2041
+ await response.json();
2042
+ } catch (err) {
2043
+ console.error('Failed to handle choice:', err);
2044
+ } finally {
2045
+ this.actionDialog = null;
2046
+ }
2047
+ },
2048
+ closeActionDialog() {
2049
+ this.actionDialog = null;
2050
+ },
2051
+ updateTimeSinceUpdate() {
2052
+ if (!this.lastUpdate) return;
2053
+
2054
+ const now = new Date();
2055
+ const diff = Math.floor((now - this.lastUpdate) / 1000);
2056
+
2057
+ if (diff < 60) {
2058
+ this.timeSinceUpdate = diff + 's ago';
2059
+ } else if (diff < 3600) {
2060
+ this.timeSinceUpdate = Math.floor(diff / 60) + 'm ago';
2061
+ } else {
2062
+ this.timeSinceUpdate = Math.floor(diff / 3600) + 'h ago';
2063
+ }
2064
+ },
2065
+ startAutoRefresh() {
2066
+ this.fetchPanes();
2067
+ refreshInterval = setInterval(() => {
2068
+ this.fetchPanes();
2069
+ }, 2000);
2070
+
2071
+ // Update time display every second
2072
+ setInterval(() => {
2073
+ this.updateTimeSinceUpdate();
2074
+ }, 1000);
2075
+ },
2076
+ stopAutoRefresh() {
2077
+ if (refreshInterval) {
2078
+ clearInterval(refreshInterval);
2079
+ refreshInterval = null;
2080
+ }
2081
+ }
2082
+ },
2083
+ mounted() {
2084
+ // Apply theme on mount
2085
+ document.documentElement.setAttribute('data-theme', this.theme);
2086
+
2087
+ this.startAutoRefresh();
2088
+
2089
+ // Handle page visibility to pause/resume updates
2090
+ document.addEventListener('visibilitychange', () => {
2091
+ if (document.hidden) {
2092
+ this.stopAutoRefresh();
2093
+ } else {
2094
+ this.startAutoRefresh();
2095
+ }
2096
+ });
2097
+ }
2098
+ });
2099
+
2100
+ app.mount('#app');`;
2101
+ }
2102
+ export function getTerminalJs() {
2103
+ return `// Terminal viewer with Vue.js and ANSI parsing
2104
+ import { createApp } from '/vue.esm-browser.js';
2105
+
2106
+ const paneId = window.location.pathname.split('/').pop();
2107
+
2108
+ // Helper to access Vue reactive data
2109
+ let vueApp = null;
2110
+
2111
+ // ANSI parsing state
2112
+ let currentAttrs = {};
2113
+
2114
+ // Convenience accessors for Vue data - these will be bound after Vue mounts
2115
+ const getTerminalBuffer = () => window.terminalBuffer || [];
2116
+ const setTerminalBuffer = (val) => { window.terminalBuffer = val; };
2117
+ const getTerminalDimensions = () => window.terminalDimensions || { width: 80, height: 24 };
2118
+ const setTerminalDimensions = (val) => { window.terminalDimensions = val; };
2119
+ const getCursorRow = () => window.cursorRow || 0;
2120
+ const setCursorRow = (val) => { window.cursorRow = val; };
2121
+ const getCursorCol = () => window.cursorCol || 0;
2122
+ const setCursorCol = (val) => { window.cursorCol = val; };
2123
+
2124
+ // Color palette for 256-color mode
2125
+ const colorPalette = [
2126
+ '#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0',
2127
+ '#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff',
2128
+ '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f',
2129
+ '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af',
2130
+ '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff',
2131
+ '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
2132
+ '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af',
2133
+ '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff',
2134
+ '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f',
2135
+ '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af',
2136
+ '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff',
2137
+ '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f',
2138
+ '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af',
2139
+ '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff',
2140
+ '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f',
2141
+ '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af',
2142
+ '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff',
2143
+ '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f',
2144
+ '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af',
2145
+ '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff',
2146
+ '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f',
2147
+ '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af',
2148
+ '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff',
2149
+ '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f',
2150
+ '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af',
2151
+ '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff',
2152
+ '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f',
2153
+ '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af',
2154
+ '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff',
2155
+ '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e',
2156
+ '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e',
2157
+ '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee'
2158
+ ];
2159
+
2160
+ // Initialize terminal buffer
2161
+ function initTerminal() {
2162
+ window.terminalBuffer = Array(window.terminalDimensions.height).fill(null).map(() =>
2163
+ Array(window.terminalDimensions.width).fill(null).map(() => ({
2164
+ char: ' ',
2165
+ fg: null,
2166
+ bg: null,
2167
+ bold: false,
2168
+ dim: false,
2169
+ italic: false,
2170
+ underline: false,
2171
+ strikethrough: false
2172
+ }))
2173
+ );
2174
+ }
2175
+
2176
+ // Parse ANSI codes and update buffer with target cursor constraint
2177
+ // Used for patches where we know the final cursor position and don't want to go past it
2178
+ function parseAnsiAndUpdateWithTarget(text, targetRow, targetCol) {
2179
+ let i = 0;
2180
+
2181
+ while (i < text.length) {
2182
+ const code = text.charCodeAt(i);
2183
+
2184
+ // Check for escape sequence (ESC = 27)
2185
+ if (code === 27) {
2186
+ const seqEnd = findEscapeSequenceEnd(text, i);
2187
+ if (seqEnd > i) {
2188
+ handleEscapeSequence(text.substring(i, seqEnd));
2189
+ i = seqEnd;
2190
+ continue;
2191
+ }
2192
+ }
2193
+
2194
+ // Handle backspace
2195
+ if (code === 8) {
2196
+ if (window.cursorCol > 0) {
2197
+ window.cursorCol--;
2198
+ }
2199
+ i++;
2200
+ continue;
2201
+ }
2202
+
2203
+ // Handle character - don't allow scrolling, clamp to target cursor
2204
+ handleCharacterWithTarget(text[i], targetRow);
2205
+ i++;
2206
+ }
2207
+ }
2208
+
2209
+ // Parse ANSI codes and update buffer
2210
+ // allowScrolling: if false, prevents buffer scrolling (for patches)
2211
+ function parseAnsiAndUpdate(text, debugPatch = false, allowScrolling = true) {
2212
+ let i = 0;
2213
+
2214
+ while (i < text.length) {
2215
+ const code = text.charCodeAt(i);
2216
+
2217
+ // Check for escape sequence (ESC = 27)
2218
+ if (code === 27) {
2219
+ // Escape sequence
2220
+ const seqEnd = findEscapeSequenceEnd(text, i);
2221
+ if (seqEnd > i) {
2222
+ const seq = text.substring(i, seqEnd);
2223
+ handleEscapeSequence(seq);
2224
+ i = seqEnd;
2225
+ continue;
2226
+ }
2227
+ }
2228
+
2229
+ // Handle backspace
2230
+ if (code === 8) {
2231
+ if (window.cursorCol > 0) {
2232
+ window.cursorCol--;
2233
+ }
2234
+ i++;
2235
+ continue;
2236
+ }
2237
+
2238
+ // Regular character
2239
+ handleCharacter(text[i], allowScrolling);
2240
+ i++;
2241
+ }
2242
+ }
2243
+
2244
+ function findEscapeSequenceEnd(text, start) {
2245
+ if (start + 1 >= text.length) return start + 1;
2246
+
2247
+ const next = text[start + 1];
2248
+
2249
+ // CSI sequence: ESC[
2250
+ if (next === '[') {
2251
+ for (let i = start + 2; i < text.length; i++) {
2252
+ const c = text[i];
2253
+ if ((c >= '@' && c <= '~')) {
2254
+ return i + 1;
2255
+ }
2256
+ }
2257
+ }
2258
+
2259
+ // OSC sequence: ESC]
2260
+ if (next === ']') {
2261
+ for (let i = start + 2; i < text.length; i++) {
2262
+ const code = text.charCodeAt(i);
2263
+ if (code === 7) { // BEL
2264
+ return i + 1;
2265
+ }
2266
+ if (code === 27 && i + 1 < text.length && text[i + 1] === '\\\\') { // ESC \\
2267
+ return i + 2;
2268
+ }
2269
+ }
2270
+ }
2271
+
2272
+ // Simple escape
2273
+ return start + 2;
2274
+ }
2275
+
2276
+ function handleEscapeSequence(seq) {
2277
+ if (seq.length < 2) return;
2278
+
2279
+ if (seq[1] === '[') {
2280
+ // CSI sequence
2281
+ const params = seq.substring(2, seq.length - 1);
2282
+ const command = seq[seq.length - 1];
2283
+ handleCSI(params, command);
2284
+ }
2285
+ }
2286
+
2287
+ function handleCSI(params, command) {
2288
+ const args = params.split(';').map(p => parseInt(p) || 0);
2289
+ const oldRow = window.cursorRow;
2290
+ const oldCol = window.cursorCol;
2291
+
2292
+ switch (command) {
2293
+ case 'H': // Cursor position
2294
+ case 'f':
2295
+ window.cursorRow = Math.min(Math.max((args[0] || 1) - 1, 0), window.terminalDimensions.height - 1);
2296
+ window.cursorCol = Math.min(Math.max((args[1] || 1) - 1, 0), window.terminalDimensions.width - 1);
2297
+ break;
2298
+
2299
+ case 'A': // Cursor up
2300
+ window.cursorRow = Math.max(window.cursorRow - (args[0] || 1), 0);
2301
+ break;
2302
+
2303
+ case 'B': // Cursor down
2304
+ window.cursorRow = Math.min(window.cursorRow + (args[0] || 1), window.terminalDimensions.height - 1);
2305
+ break;
2306
+
2307
+ case 'C': // Cursor forward
2308
+ window.cursorCol = Math.min(window.cursorCol + (args[0] || 1), window.terminalDimensions.width - 1);
2309
+ break;
2310
+
2311
+ case 'D': // Cursor back
2312
+ window.cursorCol = Math.max(window.cursorCol - (args[0] || 1), 0);
2313
+ break;
2314
+
2315
+ case 'G': // Cursor Horizontal Absolute
2316
+ window.cursorCol = Math.min(Math.max((args[0] || 1) - 1, 0), window.terminalDimensions.width - 1);
2317
+ break;
2318
+
2319
+ case 'J': // Erase display
2320
+ handleEraseDisplay(args[0] || 0);
2321
+ break;
2322
+
2323
+ case 'K': // Erase line
2324
+ handleEraseLine(args[0] || 0);
2325
+ break;
2326
+
2327
+ case 'm': // SGR (colors and attributes)
2328
+ handleSGR(args);
2329
+ break;
2330
+ }
2331
+ }
2332
+
2333
+ function handleSGR(args) {
2334
+ if (args.length === 0 || args[0] === 0) {
2335
+ currentAttrs = {};
2336
+ return;
2337
+ }
2338
+
2339
+ let i = 0;
2340
+ while (i < args.length) {
2341
+ const arg = args[i];
2342
+
2343
+ if (arg === 0) {
2344
+ currentAttrs = {};
2345
+ } else if (arg === 1) {
2346
+ currentAttrs.bold = true;
2347
+ } else if (arg === 2) {
2348
+ currentAttrs.dim = true;
2349
+ } else if (arg === 3) {
2350
+ currentAttrs.italic = true;
2351
+ } else if (arg === 4) {
2352
+ currentAttrs.underline = true;
2353
+ } else if (arg === 9) {
2354
+ currentAttrs.strikethrough = true;
2355
+ } else if (arg === 22) {
2356
+ currentAttrs.bold = false;
2357
+ currentAttrs.dim = false;
2358
+ } else if (arg === 23) {
2359
+ currentAttrs.italic = false;
2360
+ } else if (arg === 24) {
2361
+ currentAttrs.underline = false;
2362
+ } else if (arg === 29) {
2363
+ currentAttrs.strikethrough = false;
2364
+ } else if (arg >= 30 && arg <= 37) {
2365
+ // Standard foreground colors
2366
+ currentAttrs.fg = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'][arg - 30];
2367
+ } else if (arg === 38) {
2368
+ // Extended foreground color
2369
+ if (i + 1 < args.length) {
2370
+ if (args[i + 1] === 5 && i + 2 < args.length) {
2371
+ // 256 color: 38;5;n
2372
+ currentAttrs.fg = 'c' + args[i + 2];
2373
+ i += 2;
2374
+ } else if (args[i + 1] === 2 && i + 4 < args.length) {
2375
+ // RGB color: 38;2;r;g;b
2376
+ currentAttrs.fg = \`rgb(\${args[i + 2]},\${args[i + 3]},\${args[i + 4]})\`;
2377
+ i += 4;
2378
+ }
2379
+ }
2380
+ } else if (arg === 39) {
2381
+ currentAttrs.fg = null;
2382
+ } else if (arg >= 40 && arg <= 47) {
2383
+ // Standard background colors
2384
+ currentAttrs.bg = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'][arg - 40];
2385
+ } else if (arg === 48) {
2386
+ // Extended background color
2387
+ if (i + 1 < args.length) {
2388
+ if (args[i + 1] === 5 && i + 2 < args.length) {
2389
+ // 256 color: 48;5;n
2390
+ currentAttrs.bg = 'c' + args[i + 2];
2391
+ i += 2;
2392
+ } else if (args[i + 1] === 2 && i + 4 < args.length) {
2393
+ // RGB color: 48;2;r;g;b
2394
+ currentAttrs.bg = \`rgb(\${args[i + 2]},\${args[i + 3]},\${args[i + 4]})\`;
2395
+ i += 4;
2396
+ }
2397
+ }
2398
+ } else if (arg === 49) {
2399
+ currentAttrs.bg = null;
2400
+ } else if (arg >= 90 && arg <= 97) {
2401
+ // Bright foreground colors
2402
+ currentAttrs.fg = 'bright-' + ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'][arg - 90];
2403
+ } else if (arg >= 100 && arg <= 107) {
2404
+ // Bright background colors
2405
+ currentAttrs.bg = 'bright-' + ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'][arg - 100];
2406
+ }
2407
+
2408
+ i++;
2409
+ }
2410
+ }
2411
+
2412
+ // Handle character with target row constraint - don't go past target
2413
+ function handleCharacterWithTarget(char, targetRow) {
2414
+ if (char === '\\n') {
2415
+ window.cursorRow++;
2416
+ window.cursorCol = 0;
2417
+ // Clamp to target row - never go past where tmux says we should end up
2418
+ if (window.cursorRow > targetRow) {
2419
+ window.cursorRow = targetRow;
2420
+ }
2421
+ return;
2422
+ }
2423
+
2424
+ if (char === '\\r') {
2425
+ window.cursorCol = 0;
2426
+ return;
2427
+ }
2428
+
2429
+ if (char === '\\t') {
2430
+ window.cursorCol = Math.min(Math.floor((window.cursorCol + 8) / 8) * 8, window.terminalDimensions.width - 1);
2431
+ return;
2432
+ }
2433
+
2434
+ if (window.cursorCol >= window.terminalDimensions.width) {
2435
+ window.cursorCol = 0;
2436
+ window.cursorRow++;
2437
+ // Clamp to target row
2438
+ if (window.cursorRow > targetRow) {
2439
+ window.cursorRow = targetRow;
2440
+ }
2441
+ }
2442
+
2443
+ if (window.cursorRow < window.terminalDimensions.height && window.cursorCol < window.terminalDimensions.width) {
2444
+ window.terminalBuffer[window.cursorRow][window.cursorCol] = {
2445
+ char: char,
2446
+ ...currentAttrs
2447
+ };
2448
+ window.cursorCol++;
2449
+ }
2450
+ }
2451
+
2452
+ function handleCharacter(char, allowScrolling = true) {
2453
+ if (char === '\\n') {
2454
+ window.cursorRow++;
2455
+ window.cursorCol = 0;
2456
+ if (window.cursorRow >= window.terminalDimensions.height) {
2457
+ if (allowScrolling) {
2458
+ // Scroll up
2459
+ window.terminalBuffer.shift();
2460
+ window.terminalBuffer.push(Array(window.terminalDimensions.width).fill(null).map(() => ({
2461
+ char: ' ',
2462
+ fg: null,
2463
+ bg: null,
2464
+ bold: false,
2465
+ dim: false,
2466
+ italic: false,
2467
+ underline: false,
2468
+ strikethrough: false
2469
+ })));
2470
+ window.cursorRow = window.terminalDimensions.height - 1;
2471
+ } else {
2472
+ // Don't scroll during patches - just clamp cursor
2473
+ window.cursorRow = window.terminalDimensions.height - 1;
2474
+ }
2475
+ }
2476
+ return;
2477
+ }
2478
+
2479
+ if (char === '\\r') {
2480
+ window.cursorCol = 0;
2481
+ return;
2482
+ }
2483
+
2484
+ if (char === '\\t') {
2485
+ window.cursorCol = Math.min(Math.floor((window.cursorCol + 8) / 8) * 8, window.terminalDimensions.width - 1);
2486
+ return;
2487
+ }
2488
+
2489
+ if (window.cursorCol >= window.terminalDimensions.width) {
2490
+ window.cursorCol = 0;
2491
+ window.cursorRow++;
2492
+ if (window.cursorRow >= window.terminalDimensions.height) {
2493
+ if (allowScrolling) {
2494
+ window.terminalBuffer.shift();
2495
+ window.terminalBuffer.push(Array(window.terminalDimensions.width).fill(null).map(() => ({
2496
+ char: ' ',
2497
+ fg: null,
2498
+ bg: null,
2499
+ bold: false,
2500
+ dim: false,
2501
+ italic: false,
2502
+ underline: false,
2503
+ strikethrough: false
2504
+ })));
2505
+ window.cursorRow = window.terminalDimensions.height - 1;
2506
+ } else {
2507
+ // Don't scroll - just clamp
2508
+ window.cursorRow = window.terminalDimensions.height - 1;
2509
+ }
2510
+ }
2511
+ }
2512
+
2513
+ if (window.cursorRow < window.terminalDimensions.height && window.cursorCol < window.terminalDimensions.width) {
2514
+ window.terminalBuffer[window.cursorRow][window.cursorCol] = {
2515
+ char: char,
2516
+ ...currentAttrs
2517
+ };
2518
+ window.cursorCol++;
2519
+ }
2520
+ }
2521
+
2522
+ function handleEraseDisplay(mode) {
2523
+ // Implement erase display modes
2524
+ if (mode === 2) {
2525
+ initTerminal();
2526
+ }
2527
+ }
2528
+
2529
+ function handleEraseLine(mode) {
2530
+ if (mode === 0) {
2531
+ for (let col = window.cursorCol; col < window.terminalDimensions.width; col++) {
2532
+ window.terminalBuffer[window.cursorRow][col] = { char: ' ', fg: null, bg: null, bold: false, dim: false, italic: false, underline: false, strikethrough: false };
2533
+ }
2534
+ } else if (mode === 2) {
2535
+ for (let col = 0; col < window.terminalDimensions.width; col++) {
2536
+ window.terminalBuffer[window.cursorRow][col] = { char: ' ', fg: null, bg: null, bold: false, dim: false, italic: false, underline: false, strikethrough: false };
2537
+ }
2538
+ }
2539
+ }
2540
+
2541
+ // Removed duplicate colorPalette - using the one declared earlier
2542
+
2543
+ // HTML entity encoding
2544
+ function escapeHtml(text) {
2545
+ return text
2546
+ .replace(/&/g, '&amp;')
2547
+ .replace(/</g, '&lt;')
2548
+ .replace(/>/g, '&gt;')
2549
+ .replace(/"/g, '&quot;')
2550
+ .replace(/'/g, '&#39;');
2551
+ }
2552
+
2553
+ // Check if two cells have the same styling
2554
+ function hasSameStyle(cell1, cell2) {
2555
+ return cell1.fg === cell2.fg &&
2556
+ cell1.bg === cell2.bg &&
2557
+ cell1.bold === cell2.bold &&
2558
+ cell1.dim === cell2.dim &&
2559
+ cell1.italic === cell2.italic &&
2560
+ cell1.underline === cell2.underline &&
2561
+ cell1.strikethrough === cell2.strikethrough;
2562
+ }
2563
+
2564
+ // Build style attributes for a cell
2565
+ function buildStyleAttrs(cell) {
2566
+ const classes = [];
2567
+ const styles = [];
2568
+
2569
+ // Handle foreground color
2570
+ if (cell.fg) {
2571
+ if (cell.fg.startsWith('rgb(')) {
2572
+ styles.push(\`color: \${cell.fg}\`);
2573
+ } else if (cell.fg.startsWith('c')) {
2574
+ const colorIndex = parseInt(cell.fg.substring(1));
2575
+ if (colorIndex >= 0 && colorIndex < colorPalette.length) {
2576
+ styles.push(\`color: \${colorPalette[colorIndex]}\`);
2577
+ }
2578
+ } else {
2579
+ classes.push('term-fg-' + cell.fg);
2580
+ }
2581
+ }
2582
+
2583
+ // Handle background color
2584
+ if (cell.bg) {
2585
+ if (cell.bg.startsWith('rgb(')) {
2586
+ styles.push(\`background-color: \${cell.bg}\`);
2587
+ } else if (cell.bg.startsWith('c')) {
2588
+ const colorIndex = parseInt(cell.bg.substring(1));
2589
+ if (colorIndex >= 0 && colorIndex < colorPalette.length) {
2590
+ styles.push(\`background-color: \${colorPalette[colorIndex]}\`);
2591
+ }
2592
+ } else {
2593
+ classes.push('term-bg-' + cell.bg);
2594
+ }
2595
+ }
2596
+
2597
+ // Add attribute classes
2598
+ if (cell.bold) classes.push('term-bold');
2599
+ if (cell.dim) classes.push('term-dim');
2600
+ if (cell.italic) classes.push('term-italic');
2601
+ if (cell.underline) classes.push('term-underline');
2602
+ if (cell.strikethrough) classes.push('term-strikethrough');
2603
+
2604
+ return { classes, styles };
2605
+ }
2606
+
2607
+ // Render buffer to HTML with one div per row
2608
+ // Connect to stream
2609
+ function connectToStream() {
2610
+ const streamPaneId = window.actualPaneId || paneId;
2611
+ const url = \`/api/stream/\${streamPaneId}\`;
2612
+
2613
+ fetch(url)
2614
+ .then(response => {
2615
+ if (!response.ok) throw new Error('Failed to connect');
2616
+ if (!response.body) throw new Error('No response body');
2617
+
2618
+ const reader = response.body.getReader();
2619
+ const decoder = new TextDecoder();
2620
+ let buffer = '';
2621
+
2622
+ updateConnectionStatus(true);
2623
+
2624
+ const processStream = async () => {
2625
+ try {
2626
+ while (true) {
2627
+ const { done, value } = await reader.read();
2628
+ if (done) break;
2629
+
2630
+ buffer += decoder.decode(value, { stream: true });
2631
+
2632
+ let newlineIndex;
2633
+ while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {
2634
+ const message = buffer.substring(0, newlineIndex);
2635
+ buffer = buffer.substring(newlineIndex + 1);
2636
+
2637
+ if (message) {
2638
+ processMessage(message);
2639
+ }
2640
+ }
2641
+ }
2642
+ } catch (error) {
2643
+ updateConnectionStatus(false);
2644
+ }
2645
+ };
2646
+
2647
+ processStream();
2648
+ })
2649
+ .catch(error => {
2650
+ updateConnectionStatus(false);
2651
+ });
2652
+ }
2653
+
2654
+ function processMessage(message) {
2655
+ const colonIndex = message.indexOf(':');
2656
+ if (colonIndex === -1) return;
2657
+
2658
+ const type = message.substring(0, colonIndex);
2659
+ const jsonStr = message.substring(colonIndex + 1);
2660
+
2661
+ try {
2662
+ const data = JSON.parse(jsonStr);
2663
+
2664
+ switch (type) {
2665
+ case 'INIT':
2666
+ window.terminalDimensions = { width: data.width, height: data.height };
2667
+ initTerminal();
2668
+
2669
+ // Reset cursor to top-left before parsing INIT content
2670
+ window.cursorRow = 0;
2671
+ window.cursorCol = 0;
2672
+
2673
+ // Parse content - NO scrolling for INIT, just clamp cursor to buffer
2674
+ // This prevents losing the first line when content fills the entire buffer
2675
+ parseAnsiAndUpdate(data.content || '', false, false);
2676
+
2677
+ // Set cursor to actual tmux cursor position if provided
2678
+ if (data.cursorRow !== undefined && data.cursorCol !== undefined) {
2679
+ window.cursorRow = data.cursorRow;
2680
+ window.cursorCol = data.cursorCol;
2681
+ }
2682
+ renderToHtml();
2683
+ break;
2684
+
2685
+ case 'PATCH':
2686
+ // PATCH: The backend sends us the raw diff between terminal states
2687
+ // This diff contains ANSI escape sequences that position the cursor
2688
+ // and write text. We need to simply replay these sequences.
2689
+ // The key insight: scrolling already happened in tmux BEFORE we captured
2690
+ // the diff. We're not replaying terminal output - we're applying a diff.
2691
+ const targetCursorRow = data.cursorRow;
2692
+ const targetCursorCol = data.cursorCol;
2693
+
2694
+ // Apply changes - NO SCROLLING during patches
2695
+ // The diff tells us exactly what cells changed in the visible buffer
2696
+ data.changes.forEach(change => {
2697
+ parseAnsiAndUpdate(change.text, false, false);
2698
+ });
2699
+
2700
+ // Set cursor to final position from tmux
2701
+ if (targetCursorRow !== undefined && targetCursorCol !== undefined) {
2702
+ window.cursorRow = targetCursorRow;
2703
+ window.cursorCol = targetCursorCol;
2704
+ }
2705
+
2706
+ break;
2707
+
2708
+ case 'RESIZE':
2709
+ terminalDimensions = { width: data.width, height: data.height };
2710
+ initTerminal();
2711
+ parseAnsiAndUpdate(data.content || '');
2712
+ renderToHtml();
2713
+ break;
2714
+
2715
+ case 'HEARTBEAT':
2716
+ break;
2717
+ }
2718
+ } catch (error) {
2719
+ // Silently ignore parse errors
2720
+ }
2721
+ }
2722
+
2723
+ function updateConnectionStatus(connected) {
2724
+ if (vueApp) {
2725
+ vueApp.connected = connected;
2726
+ }
2727
+ }
2728
+
2729
+ // Initialize Vue app
2730
+ const app = createApp({
2731
+ data() {
2732
+ return {
2733
+ terminalBuffer: [],
2734
+ dimensions: { width: 80, height: 24 },
2735
+ connected: false,
2736
+ cursorRow: 0,
2737
+ cursorCol: 0,
2738
+ paneTitle: 'Loading...',
2739
+ isMobile: false,
2740
+ ctrlActive: false,
2741
+ altActive: false,
2742
+ shiftActive: false,
2743
+ mobileInputValue: ''
2744
+ };
2745
+ },
2746
+ template: \`
2747
+ <div class="terminal-page">
2748
+ <header>
2749
+ <a href="/" class="back-button">← dmux</a>
2750
+ <h1>{{ paneTitle }}</h1>
2751
+ <div class="session-info">
2752
+ <span>{{ dimensions.width }}×{{ dimensions.height }}</span>
2753
+ <span class="status-indicator" :style="{ color: connected ? '#4ade80' : '#f87171' }">●</span>
2754
+ </div>
2755
+ </header>
2756
+
2757
+ <!-- Mobile keyboard toolbar -->
2758
+ <div v-if="isMobile" class="mobile-toolbar">
2759
+ <button @click="toggleCtrl" :class="{ active: ctrlActive }" class="toolbar-key">Ctrl</button>
2760
+ <button @click="toggleAlt" :class="{ active: altActive }" class="toolbar-key">Alt</button>
2761
+ <button @click="toggleShift" :class="{ active: shiftActive }" class="toolbar-key">Shift</button>
2762
+ <button @click="sendKey('Escape')" class="toolbar-key">Esc</button>
2763
+ <button @click="sendKey('Tab')" class="toolbar-key">Tab</button>
2764
+ <button @click="sendKey('Enter')" class="toolbar-key">Enter</button>
2765
+ <button @click="sendKey('ArrowUp')" class="toolbar-key">↑</button>
2766
+ <button @click="sendKey('ArrowDown')" class="toolbar-key">↓</button>
2767
+ <button @click="sendKey('ArrowLeft')" class="toolbar-key">←</button>
2768
+ <button @click="sendKey('ArrowRight')" class="toolbar-key">→</button>
2769
+ </div>
2770
+
2771
+ <!-- Hidden input for mobile keyboard -->
2772
+ <input
2773
+ v-if="isMobile"
2774
+ ref="mobileInput"
2775
+ type="text"
2776
+ class="mobile-input"
2777
+ v-model="mobileInputValue"
2778
+ @input="handleMobileInput"
2779
+ @keydown="handleMobileKeydown"
2780
+ autocomplete="off"
2781
+ autocapitalize="off"
2782
+ autocorrect="off"
2783
+ />
2784
+
2785
+ <div class="terminal-content" @click="focusMobileInput">
2786
+ <div class="terminal-output" :style="terminalContainerStyle">
2787
+ <div
2788
+ v-for="(row, rowIndex) in terminalBuffer"
2789
+ :key="rowIndex"
2790
+ class="terminal-row"
2791
+ :data-row="rowIndex"
2792
+ v-html="renderRow(row, rowIndex)"
2793
+ ></div>
2794
+ </div>
2795
+ </div>
2796
+ </div>
2797
+ \`,
2798
+ computed: {
2799
+ terminalContainerStyle() {
2800
+ // Set width to fit exactly the terminal columns
2801
+ // Using ch units (character width in monospace fonts)
2802
+ return {
2803
+ width: \`\${this.dimensions.width}ch\`,
2804
+ maxWidth: '100vw',
2805
+ fontSize: \`calc(100vw / \${this.dimensions.width} / 0.6)\`
2806
+ };
2807
+ }
2808
+ },
2809
+ methods: {
2810
+ renderRow(row, rowIndex) {
2811
+ let html = '';
2812
+ let col = 0;
2813
+
2814
+ while (col < row.length) {
2815
+ const cell = row[col];
2816
+ const isCursor = (rowIndex === this.cursorRow && col === this.cursorCol);
2817
+ const hasStyle = cell.fg || cell.bg || cell.bold || cell.dim || cell.italic || cell.underline || cell.strikethrough || isCursor;
2818
+
2819
+ if (!hasStyle) {
2820
+ let text = '';
2821
+ while (col < row.length) {
2822
+ const c = row[col];
2823
+ const isCur = (rowIndex === this.cursorRow && col === this.cursorCol);
2824
+ if (c.fg || c.bg || c.bold || c.dim || c.italic || c.underline || c.strikethrough || isCur) break;
2825
+ text += c.char;
2826
+ col++;
2827
+ }
2828
+ html += escapeHtml(text);
2829
+ } else {
2830
+ const { classes, styles } = buildStyleAttrs(cell);
2831
+ if (isCursor) classes.push('term-cursor');
2832
+
2833
+ let text = cell.char;
2834
+ col++;
2835
+
2836
+ while (col < row.length) {
2837
+ const nextCell = row[col];
2838
+ const nextIsCursor = (rowIndex === this.cursorRow && col === this.cursorCol);
2839
+ if (nextIsCursor || !hasSameStyle(cell, nextCell)) break;
2840
+ text += nextCell.char;
2841
+ col++;
2842
+ }
2843
+
2844
+ const classAttr = classes.length ? ' class="' + classes.join(' ') + '"' : '';
2845
+ const styleAttr = styles.length ? ' style="' + styles.join('; ') + '"' : '';
2846
+ html += '<span' + classAttr + styleAttr + '>' + escapeHtml(text) + '</span>';
2847
+ }
2848
+ }
2849
+
2850
+ return html;
2851
+ },
2852
+ toggleCtrl() {
2853
+ this.ctrlActive = !this.ctrlActive;
2854
+ if (this.ctrlActive && this.altActive) {
2855
+ this.altActive = false;
2856
+ }
2857
+ },
2858
+ toggleAlt() {
2859
+ this.altActive = !this.altActive;
2860
+ if (this.altActive && this.ctrlActive) {
2861
+ this.ctrlActive = false;
2862
+ }
2863
+ },
2864
+ toggleShift() {
2865
+ this.shiftActive = !this.shiftActive;
2866
+ },
2867
+ async sendKey(key) {
2868
+ const keystrokeData = {
2869
+ key: key,
2870
+ ctrlKey: this.ctrlActive,
2871
+ altKey: this.altActive,
2872
+ shiftKey: this.shiftActive,
2873
+ metaKey: false
2874
+ };
2875
+
2876
+ // Reset modifiers after sending
2877
+ this.ctrlActive = false;
2878
+ this.altActive = false;
2879
+ this.shiftActive = false;
2880
+
2881
+ try {
2882
+ await fetch(\`/api/keys/\${window.actualPaneId}\`, {
2883
+ method: 'POST',
2884
+ headers: { 'Content-Type': 'application/json' },
2885
+ body: JSON.stringify(keystrokeData)
2886
+ });
2887
+ } catch (error) {
2888
+ // Silently ignore
2889
+ }
2890
+ },
2891
+ focusMobileInput() {
2892
+ if (this.isMobile && this.$refs.mobileInput) {
2893
+ this.$refs.mobileInput.focus();
2894
+ }
2895
+ },
2896
+ handleMobileInput(event) {
2897
+ // Get the new character(s) added
2898
+ const newValue = event.target.value;
2899
+ const oldValue = this.mobileInputValue;
2900
+
2901
+ if (newValue.length > oldValue.length) {
2902
+ // Characters were added
2903
+ const addedChars = newValue.substring(oldValue.length);
2904
+
2905
+ // Send each character
2906
+ for (const char of addedChars) {
2907
+ this.sendKey(char);
2908
+ }
2909
+ } else if (newValue.length < oldValue.length) {
2910
+ // Characters were deleted - send backspace
2911
+ const deletedCount = oldValue.length - newValue.length;
2912
+ for (let i = 0; i < deletedCount; i++) {
2913
+ this.sendKey('Backspace');
2914
+ }
2915
+ }
2916
+
2917
+ // Clear the input to allow continuous typing
2918
+ this.$nextTick(() => {
2919
+ this.mobileInputValue = '';
2920
+ });
2921
+ },
2922
+ handleMobileKeydown(event) {
2923
+ // Handle special keys
2924
+ if (event.key === 'Enter') {
2925
+ event.preventDefault();
2926
+ this.sendKey('Enter');
2927
+ } else if (event.key === 'Backspace' && this.mobileInputValue === '') {
2928
+ event.preventDefault();
2929
+ this.sendKey('Backspace');
2930
+ }
2931
+ }
2932
+ },
2933
+ mounted() {
2934
+ // Make Vue app instance globally accessible
2935
+ window.vueApp = this;
2936
+ vueApp = this;
2937
+
2938
+ // Detect mobile device
2939
+ this.isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.innerWidth < 768;
2940
+
2941
+ // Wire up global variables to Vue's reactive properties
2942
+ // When code reads/writes terminalBuffer, it actually reads/writes this.terminalBuffer
2943
+ Object.defineProperty(window, 'terminalBuffer', {
2944
+ get: () => this.terminalBuffer,
2945
+ set: (val) => { this.terminalBuffer = val; },
2946
+ configurable: true
2947
+ });
2948
+ Object.defineProperty(window, 'cursorRow', {
2949
+ get: () => this.cursorRow,
2950
+ set: (val) => { this.cursorRow = val; },
2951
+ configurable: true
2952
+ });
2953
+ Object.defineProperty(window, 'cursorCol', {
2954
+ get: () => this.cursorCol,
2955
+ set: (val) => { this.cursorCol = val; },
2956
+ configurable: true
2957
+ });
2958
+ Object.defineProperty(window, 'terminalDimensions', {
2959
+ get: () => this.dimensions,
2960
+ set: (val) => { this.dimensions = val; },
2961
+ configurable: true
2962
+ });
2963
+
2964
+ // Assign to module-level variables so code can use them
2965
+ terminalBuffer = this.terminalBuffer;
2966
+ terminalDimensions = this.dimensions;
2967
+ cursorRow = this.cursorRow;
2968
+ cursorCol = this.cursorCol;
2969
+
2970
+ // Load pane info and start streaming
2971
+ fetch('/api/panes')
2972
+ .then(r => r.json())
2973
+ .then(data => {
2974
+ // Try to find pane by ID first, then by slug (for backwards compat)
2975
+ let pane = data.panes.find(p => p.id === paneId);
2976
+ if (!pane) {
2977
+ pane = data.panes.find(p => p.slug === paneId);
2978
+ }
2979
+ if (pane) {
2980
+ this.paneTitle = pane.slug;
2981
+ // Use the actual pane ID for streaming
2982
+ window.actualPaneId = pane.id;
2983
+ }
2984
+ connectToStream();
2985
+ })
2986
+ .catch(err => {
2987
+ connectToStream();
2988
+ });
2989
+ }
2990
+ });
2991
+
2992
+ // Keyboard input handling - send keystrokes to backend
2993
+ document.addEventListener('keydown', async (event) => {
2994
+ // Don't capture keyboard if user is in browser UI (not focused on terminal)
2995
+ const activeElement = document.activeElement;
2996
+ if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
2997
+ return; // Let normal input handling work
2998
+ }
2999
+
3000
+ // Ignore modifier keys by themselves - they should only affect other keys
3001
+ const modifierKeys = ['Shift', 'Control', 'Alt', 'Meta'];
3002
+ if (modifierKeys.includes(event.key)) {
3003
+ return;
3004
+ }
3005
+
3006
+ // Prevent default for most keys to avoid browser shortcuts
3007
+ if (!event.metaKey && !event.ctrlKey || event.key === 'c' || event.key === 'd') {
3008
+ event.preventDefault();
3009
+ }
3010
+
3011
+ // Build the keystroke data
3012
+ const keystrokeData = {
3013
+ key: event.key,
3014
+ ctrlKey: event.ctrlKey,
3015
+ altKey: event.altKey,
3016
+ shiftKey: event.shiftKey,
3017
+ metaKey: event.metaKey
3018
+ };
3019
+
3020
+ try {
3021
+ const response = await fetch(\`/api/keys/\${window.actualPaneId}\`, {
3022
+ method: 'POST',
3023
+ headers: {
3024
+ 'Content-Type': 'application/json'
3025
+ },
3026
+ body: JSON.stringify(keystrokeData)
3027
+ });
3028
+ } catch (error) {
3029
+ // Silently ignore keystroke errors
3030
+ }
3031
+ });
3032
+
3033
+ app.mount('#app');
3034
+
3035
+ // Remove the old renderToHtml function - Vue handles rendering
3036
+ function renderToHtml() {
3037
+ // No-op: Vue reactively renders terminalBuffer changes
3038
+ }`;
3039
+ }
3040
+ //# sourceMappingURL=static.js.map