tmux-weblink 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/addon-fit-D5N335P2.js +16 -0
  3. package/dist/assets/terminal-client.js +4 -0
  4. package/dist/assets/xterm-NI46XXQY.js +51 -0
  5. package/dist/assets/xterm.css +285 -0
  6. package/dist/browser/terminal-client.js +4 -0
  7. package/dist/frontend.js +1 -0
  8. package/dist/index.js +12 -0
  9. package/dist/lib/agent-detect.js +3 -0
  10. package/dist/lib/agents-watch.js +2 -0
  11. package/dist/lib/atomicWrite.js +1 -0
  12. package/dist/lib/auditLog.js +1 -0
  13. package/dist/lib/auth.js +2 -0
  14. package/dist/lib/cli.js +32 -0
  15. package/dist/lib/commandbar.js +555 -0
  16. package/dist/lib/db.js +1 -0
  17. package/dist/lib/drawer-resize.js +166 -0
  18. package/dist/lib/drawer-script.js +8 -0
  19. package/dist/lib/ext-loader.js +1 -0
  20. package/dist/lib/html.js +1 -0
  21. package/dist/lib/icons.js +1 -0
  22. package/dist/lib/image-upload.js +1 -0
  23. package/dist/lib/load-env.js +6 -0
  24. package/dist/lib/mobile-toolbar.js +313 -0
  25. package/dist/lib/notes-db.js +18 -0
  26. package/dist/lib/notes-drawer.js +210 -0
  27. package/dist/lib/notes-utils.js +15 -0
  28. package/dist/lib/pages/agents-index.js +105 -0
  29. package/dist/lib/pages/history-index.js +85 -0
  30. package/dist/lib/pages/landing.js +78 -0
  31. package/dist/lib/pages/login.js +191 -0
  32. package/dist/lib/pages/notes-index.js +65 -0
  33. package/dist/lib/pages/notes-page.js +158 -0
  34. package/dist/lib/pages/quick-commands.js +361 -0
  35. package/dist/lib/pages/schedule-index.js +392 -0
  36. package/dist/lib/pages/settings.js +206 -0
  37. package/dist/lib/pages/terminal.js +364 -0
  38. package/dist/lib/pinned-views.js +1 -0
  39. package/dist/lib/plugins.js +3 -0
  40. package/dist/lib/quick-commands.js +1 -0
  41. package/dist/lib/rateLimiter.js +2 -0
  42. package/dist/lib/schedule-delay.js +9 -0
  43. package/dist/lib/scheduler-drawer.js +379 -0
  44. package/dist/lib/scheduler.js +1 -0
  45. package/dist/lib/security-config.js +2 -0
  46. package/dist/lib/session-access.js +1 -0
  47. package/dist/lib/session-windows.js +1 -0
  48. package/dist/lib/sessions-drawer.js +634 -0
  49. package/dist/lib/sessions-sidebar.js +1 -0
  50. package/dist/lib/settings.js +2 -0
  51. package/dist/lib/setup-features.js +1 -0
  52. package/dist/lib/setup-prompts.js +3 -0
  53. package/dist/lib/shared-layout.js +412 -0
  54. package/dist/lib/state-paths.js +1 -0
  55. package/dist/lib/terminal-config.js +1 -0
  56. package/dist/lib/theme-store.js +2 -0
  57. package/dist/lib/theme.js +28 -0
  58. package/dist/lib/themes/dark-cove.js +1 -0
  59. package/dist/lib/themes/ghostty.js +1 -0
  60. package/dist/lib/themes/index.js +1 -0
  61. package/dist/lib/themes/types.js +0 -0
  62. package/dist/lib/themes/vscode.js +1 -0
  63. package/dist/lib/themes/warm-clay.js +1 -0
  64. package/dist/lib/tmux-capture.js +5 -0
  65. package/dist/lib/tmux-control.js +2 -0
  66. package/dist/lib/tmux-panes.js +2 -0
  67. package/dist/lib/tmux-windows.js +4 -0
  68. package/dist/lib/watched-panes.js +1 -0
  69. package/dist/lib/window-history.js +1 -0
  70. package/dist/lib/window-labels.js +1 -0
  71. package/dist/lib/windows-drawer.js +364 -0
  72. package/dist/lib/ws-message.js +1 -0
  73. package/dist/sessions.js +2 -0
  74. package/package.json +62 -0
  75. package/scripts/fix-pty-perms.mjs +16 -0
@@ -0,0 +1,412 @@
1
+ import{commandbarButtonHTML as y}from"./commandbar.js";import{escapeHtml as l}from"./html.js";import{icon as a,iconPath as o}from"./icons.js";const d=[{id:"vscode",name:"VS Code",dot:"#007acc"},{id:"ghostty",name:"Ghostty",dot:"#ff5f00"},{id:"warm-clay",name:"Warm Clay",dot:"#b86b52"},{id:"dark-cove",name:"Dark Cove",dot:"#7aa2f7"}];function i(e="var(--panel-accent)"){return`box-shadow: 0 0 0 2px ${e}; outline: none;`}function k(e=""){return`
2
+ @media (prefers-reduced-motion: reduce) {
3
+ *, *::before, *::after {
4
+ animation-duration: 0.01ms !important;
5
+ animation-iteration-count: 1 !important;
6
+ transition-duration: 0.01ms !important;
7
+ scroll-behavior: auto !important;
8
+ }
9
+ ${e}
10
+ }`}function I(e="vscode"){const r=d.find(t=>t.id===e)??d[0],s=d.map(t=>{const n=t.id===e;return`<button type="button" class="theme-option${n?" active":""}" data-theme="${t.id}" role="menuitem">
11
+ <span class="theme-dot" style="background:${t.dot}"></span>
12
+ ${l(t.name)}
13
+ ${n?a("check",'class="theme-check" aria-hidden="true"'):""}
14
+ </button>`}).join(`
15
+ `);return`<div class="theme-switcher" id="theme-switcher">
16
+ <button type="button" class="theme-switcher-btn" aria-label="Theme" aria-haspopup="true" aria-expanded="false">
17
+ <span class="theme-dot" style="background:${r.dot}"></span>
18
+ <span>${l(r.name)}</span>
19
+ </button>
20
+ <div class="theme-switcher-popover" role="menu">
21
+ ${s}
22
+ </div>
23
+ </div>`}function E(){return`(function() {
24
+ const switcher = document.getElementById('theme-switcher');
25
+ if (!switcher) return;
26
+ const btn = switcher.querySelector('.theme-switcher-btn');
27
+ const popover = switcher.querySelector('.theme-switcher-popover');
28
+ function close() { switcher.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); }
29
+ function open() { switcher.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); }
30
+ btn.addEventListener('click', (e) => {
31
+ e.stopPropagation();
32
+ switcher.classList.contains('open') ? close() : open();
33
+ });
34
+ popover.addEventListener('click', async (e) => {
35
+ const opt = e.target.closest('.theme-option');
36
+ if (!opt) return;
37
+ const theme = opt.dataset.theme;
38
+ try {
39
+ await fetch('/api/theme', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ template: theme }),
43
+ });
44
+ location.reload();
45
+ } catch {}
46
+ });
47
+ document.addEventListener('click', (e) => {
48
+ if (switcher.classList.contains('open') && !switcher.contains(e.target)) close();
49
+ });
50
+ })();`}function T(e=""){return`
51
+ *, *::before, *::after { box-sizing: border-box; }
52
+ html, body { background: var(--page-bg); color: var(--page-fg); min-height: 100%; font-family: var(--font-sans); margin: 0; padding: 0; }
53
+
54
+ /* \u2500\u2500 Fixed header \u2500\u2500 */
55
+ .fixed-header {
56
+ position: fixed; top: 0; left: 0; right: 0; z-index: 200;
57
+ background: color-mix(in srgb, var(--panel-bg) 92%, transparent);
58
+ backdrop-filter: blur(12px);
59
+ border-bottom: 1px solid var(--panel-border);
60
+ display: flex; justify-content: space-between; align-items: center;
61
+ padding: 0 16px; height: 56px;
62
+ }
63
+ .fixed-header .brand {
64
+ font-size: var(--text-base); font-weight: 600; letter-spacing: -0.01em; color: var(--page-fg);
65
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
66
+ }
67
+ .fixed-header .brand span { color: var(--panel-accent); font-weight: 500; }
68
+ .fixed-header .brand a { color: inherit; text-decoration: none; }
69
+ .header-actions { display: flex; align-items: center; gap: 4px; }
70
+ .header-btn, .icon-btn {
71
+ display: flex; align-items: center; justify-content: center; gap: 6px;
72
+ background: none; border: none; color: var(--panel-muted); cursor: pointer;
73
+ min-width: 44px; min-height: 44px; padding: 8px 12px; border-radius: 10px;
74
+ transition: color 0.15s, background 0.15s;
75
+ font-size: var(--text-sm); text-decoration: none;
76
+ }
77
+ .header-btn:hover, .icon-btn:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
78
+ .header-btn:focus-visible, .icon-btn:focus-visible { ${i()} }
79
+ .header-btn svg, .icon-btn svg { width: 18px; height: 18px; fill: currentColor; flex-shrink: 0; }
80
+
81
+ /* \u2500\u2500 Theme switcher popover \u2500\u2500 */
82
+ .theme-switcher { position: relative; }
83
+ .theme-switcher-btn {
84
+ display: flex; align-items: center; justify-content: center; gap: 6px;
85
+ background: none; border: none; color: var(--panel-muted); cursor: pointer;
86
+ min-width: 44px; min-height: 44px; padding: 8px 12px; border-radius: 10px;
87
+ transition: color 0.15s, background 0.15s;
88
+ font-size: var(--text-sm); text-decoration: none; font-family: inherit;
89
+ }
90
+ .theme-switcher-btn:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
91
+ .theme-switcher-btn:focus-visible { ${i()} }
92
+ .theme-switcher-btn svg { width: 18px; height: 18px; fill: currentColor; flex-shrink: 0; }
93
+ .theme-switcher-popover {
94
+ position: absolute; top: calc(100% + 6px); right: 0;
95
+ min-width: 180px; max-width: calc(100vw - 24px);
96
+ background: var(--panel-bg); border: 1px solid var(--panel-border);
97
+ border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.12); padding: 6px;
98
+ display: none; z-index: 300;
99
+ }
100
+ .theme-switcher.open .theme-switcher-popover { display: block; }
101
+ .theme-option {
102
+ display: flex; align-items: center; gap: 10px; width: 100%;
103
+ min-height: 44px; padding: 10px 12px; border-radius: 8px; border: none; background: none;
104
+ color: var(--page-fg); font-size: var(--text-sm); cursor: pointer; text-align: left; font-family: inherit;
105
+ }
106
+ .theme-option:hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); color: var(--panel-accent); }
107
+ .theme-option:focus-visible { ${i()} }
108
+ .theme-option.active { color: var(--panel-accent); font-weight: 500; }
109
+ .theme-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
110
+ .theme-check { width: 16px; height: 16px; margin-left: auto; color: var(--panel-accent); }
111
+
112
+ /* \u2500\u2500 Page layout \u2500\u2500 */
113
+ .page-wrap { padding-top: 56px; }
114
+ .page-layout {
115
+ display: flex; gap: 24px; max-width: 1120px; margin: 0 auto; padding: 24px 16px;
116
+ align-items: flex-start;
117
+ }
118
+
119
+ /* \u2500\u2500 Main content panel \u2500\u2500 */
120
+ .main-panel { flex: 1; min-width: 0; }
121
+
122
+ /* \u2500\u2500 Action sidebar \u2500\u2500 */
123
+ .action-sidebar {
124
+ flex: 0 0 220px; max-width: 220px; min-width: 180px;
125
+ margin-right: 0; position: sticky; top: 80px;
126
+ }
127
+ .sidebar-label {
128
+ font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase;
129
+ color: var(--panel-muted); margin: 0 0 12px 4px; font-weight: 500;
130
+ }
131
+ .sidebar-btn {
132
+ display: flex; align-items: center; gap: 12px;
133
+ width: 100%; min-height: 44px;
134
+ padding: 10px 14px; border: 1px solid transparent; border-radius: 12px;
135
+ background: transparent; color: var(--page-fg); cursor: pointer;
136
+ font-size: var(--text-sm); font-family: inherit; text-decoration: none;
137
+ transition: background 0.15s, color 0.15s; margin-bottom: 4px;
138
+ text-align: left;
139
+ }
140
+ .sidebar-btn:hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); color: var(--panel-accent); }
141
+ .sidebar-btn:focus-visible { ${i()} }
142
+ .sidebar-btn.primary {
143
+ background: var(--panel-accent); border-color: var(--panel-accent); color: var(--panel-accent-on);
144
+ font-weight: 500; margin-bottom: 16px; justify-content: center;
145
+ }
146
+ .sidebar-btn.primary:hover { opacity: 0.9; }
147
+ .sidebar-btn.current {
148
+ background: color-mix(in srgb, var(--panel-accent) 8%, transparent); color: var(--panel-accent);
149
+ font-weight: 500; cursor: default; pointer-events: none;
150
+ }
151
+ .sidebar-btn svg { width: 18px; height: 18px; fill: currentColor; flex-shrink: 0; }
152
+ .sidebar-divider { border: none; border-top: 1px solid var(--panel-border); margin: 14px 0; }
153
+
154
+ /* \u2500\u2500 New session modal \u2500\u2500 */
155
+ .modal-backdrop {
156
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.3);
157
+ z-index: 500; align-items: center; justify-content: center;
158
+ padding: 16px;
159
+ }
160
+ .modal-backdrop.open { display: flex; }
161
+ .modal-panel {
162
+ background: var(--panel-bg); border: 1px solid var(--panel-border);
163
+ border-radius: 20px; padding: 24px; width: 100%; max-width: 440px;
164
+ }
165
+ .modal-panel h2 { font-size: var(--text-lg); font-weight: 600; margin: 0 0 20px; color: var(--page-fg); }
166
+ .modal-field { margin-bottom: 18px; position: relative; }
167
+ .modal-field label { display: block; font-size: var(--text-sm); font-weight: 500; color: var(--page-fg); margin-bottom: 8px; }
168
+ .modal-field input {
169
+ width: 100%; padding: 13px 15px; background: var(--page-bg);
170
+ border: 1px solid var(--panel-border); border-radius: 14px;
171
+ color: var(--page-fg); font-size: var(--text-base); font-family: inherit;
172
+ outline: none; transition: border-color 0.15s, box-shadow 0.15s;
173
+ }
174
+ .modal-field input:focus { border-color: var(--panel-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--panel-accent) 8%, transparent); }
175
+ /* Custom directory autocomplete dropdown (replaces native <datalist>) */
176
+ .modal-dropdown {
177
+ display: none; position: absolute; left: 0; right: 0; top: 100%;
178
+ margin-top: 4px; max-height: 220px; overflow-y: auto; z-index: 10;
179
+ background: var(--panel-bg); border: 1px solid var(--panel-border);
180
+ border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.12);
181
+ }
182
+ .modal-dropdown.open { display: block; }
183
+ .modal-dropdown-item {
184
+ display: flex; align-items: center;
185
+ min-height: 44px; padding: 10px 14px; font-size: var(--text-sm); color: var(--page-fg); cursor: pointer;
186
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
187
+ }
188
+ .modal-dropdown-item:hover, .modal-dropdown-item.active, .modal-dropdown-item:focus-visible {
189
+ background: color-mix(in srgb, var(--panel-accent) 8%, transparent); color: var(--panel-accent);
190
+ }
191
+ .modal-dropdown-item:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--panel-accent); }
192
+ .modal-error { font-size: var(--text-sm); color: #b91c1c; margin-bottom: 12px; display: none; }
193
+ .modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 8px; }
194
+ .modal-btn {
195
+ min-height: 44px; padding: 10px 20px; border-radius: 12px; font-size: var(--text-sm); font-family: inherit;
196
+ cursor: pointer; border: 1px solid var(--panel-border); background: var(--panel-bg);
197
+ color: var(--page-fg); transition: opacity 0.15s;
198
+ }
199
+ .modal-btn:hover { opacity: 0.85; }
200
+ .modal-btn:focus-visible { ${i()} }
201
+ .modal-btn.confirm {
202
+ background: var(--panel-accent); border-color: var(--panel-accent);
203
+ color: var(--panel-accent-on); font-weight: 500;
204
+ }
205
+ .modal-btn.confirm:hover { opacity: 0.9; }
206
+
207
+ /* \u2500\u2500 Mobile \u2500\u2500 */
208
+ @media (max-width: 560px) {
209
+ .page-layout { flex-direction: column; padding: 16px; gap: 0; }
210
+ .action-sidebar { max-width: 100%; min-width: 0; width: 100%; margin-right: 0; position: static; order: -1; }
211
+ .sidebar-label { display: none; }
212
+ .sidebar-btn { justify-content: center; }
213
+ .fixed-header { padding: 0 12px; }
214
+ .header-btn span,
215
+ .theme-switcher-btn span { display: none; }
216
+ .header-btn, .theme-switcher-btn { padding: 8px; }
217
+ }
218
+
219
+ ${k()}
220
+ ${e}`}function z(e){const{commandbarEnabled:r,title:s="TMUX Sessions",themeTemplate:t="vscode"}=e;return`<script>
221
+ (function() {
222
+ const token = localStorage.getItem('tmux-web-token');
223
+ if (token) {
224
+ const orig = window.fetch;
225
+ window.fetch = function(input, init) {
226
+ init = init || {};
227
+ const headers = init.headers || {};
228
+ if (typeof headers === 'object' && !Array.isArray(headers) && !headers['Authorization'] && !headers['authorization']) {
229
+ init.headers = { ...headers, Authorization: 'Bearer ' + token };
230
+ }
231
+ return orig(input, init);
232
+ };
233
+ }
234
+ })();
235
+ </script>
236
+ <header class="fixed-header">
237
+ <div class="brand"><a href="/" aria-label="Go to home">tmux<span>-weblink</span></a></div>
238
+ <div class="header-actions">
239
+ ${r?y("Search"):""}
240
+ <button class="header-btn" id="notes-toggle" title="Global notes" aria-label="Global notes">
241
+ ${a("notes")}
242
+ <span>Notes</span>
243
+ </button>
244
+ ${I(t)}
245
+ <a class="header-btn" href="/settings" title="Settings">
246
+ ${a("settings")}
247
+ <span>Settings</span>
248
+ </a>
249
+ </div>
250
+ </header>
251
+ <script>${E()}</script>`}function D(e){const{activePage:r,agentsEnabled:s,refreshHref:t}=e;function n(c,x,g,v,w=""){return`<a href="${x}" class="${c!==null&&c===r?"sidebar-btn current":"sidebar-btn"}"${w}>
252
+ <svg viewBox="0 0 24 24" fill="currentColor">${g}</svg>
253
+ ${v}
254
+ </a>`}const p=o("sessions"),m=o("notes"),u=o("schedule"),h=o("agents"),b=o("history"),f=o("quick-commands"),L=o("refresh");return`<aside class="action-sidebar">
255
+ <p class="sidebar-label">Actions</p>
256
+ <button class="sidebar-btn primary" id="new-session-btn">
257
+ ${a("add")}
258
+ New Session
259
+ </button>
260
+ <hr class="sidebar-divider">
261
+ ${n("home","/",p,"Sessions")}
262
+ ${n("notes","/notes",m,"All Notes")}
263
+ ${n("schedule","/schedule",u,"Scheduled")}
264
+ ${n("history","/history",b,"History")}
265
+ ${n("quickCommands","/quick-commands",f,"Quick Commands")}
266
+ ${s?n("agents","/agents",h,"All Agents"):""}
267
+ <hr class="sidebar-divider">
268
+ <a href="${t}" class="sidebar-btn">
269
+ ${a("refresh")}
270
+ Refresh
271
+ </a>
272
+ </aside>`}function M(){return`<div class="modal-backdrop" id="new-session-modal" role="dialog" aria-modal="true" aria-label="Create new tmux session">
273
+ <div class="modal-panel">
274
+ <h2>New Session</h2>
275
+ <div class="modal-field">
276
+ <label for="ns-name">Session name</label>
277
+ <input type="text" id="ns-name" placeholder="e.g. myproject" autocomplete="off" spellcheck="false" />
278
+ </div>
279
+ <div class="modal-field">
280
+ <label for="ns-dir">Start directory</label>
281
+ <input type="text" id="ns-dir" placeholder="~" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="ns-dir-list" />
282
+ <div class="modal-dropdown" id="ns-dir-list" role="listbox"></div>
283
+ </div>
284
+ <p class="modal-error" id="ns-error"></p>
285
+ <div class="modal-actions">
286
+ <button class="modal-btn" id="ns-cancel">Cancel</button>
287
+ <button class="modal-btn confirm" id="ns-submit">Create</button>
288
+ </div>
289
+ </div>
290
+ </div>`}function j(){return`(function() {
291
+ const modal = document.getElementById('new-session-modal');
292
+ const openBtn = document.getElementById('new-session-btn');
293
+ const cancelBtn = document.getElementById('ns-cancel');
294
+ const submitBtn = document.getElementById('ns-submit');
295
+ const nameInput = document.getElementById('ns-name');
296
+ const dirInput = document.getElementById('ns-dir');
297
+ const dirList = document.getElementById('ns-dir-list');
298
+ const errorEl = document.getElementById('ns-error');
299
+
300
+ function openModal() {
301
+ modal.classList.add('open');
302
+ nameInput.value = '';
303
+ dirInput.value = '';
304
+ errorEl.style.display = 'none';
305
+ errorEl.textContent = '';
306
+ closeDropdown();
307
+ setTimeout(() => nameInput.focus(), 50);
308
+ }
309
+
310
+ function closeModal() { modal.classList.remove('open'); }
311
+
312
+ openBtn.addEventListener('click', openModal);
313
+ cancelBtn.addEventListener('click', closeModal);
314
+ modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
315
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.classList.contains('open')) closeModal(); });
316
+
317
+ let debounceTimer = null;
318
+ let activeIdx = -1;
319
+
320
+ function closeDropdown() {
321
+ dirList.classList.remove('open');
322
+ dirList.innerHTML = '';
323
+ activeIdx = -1;
324
+ dirInput.setAttribute('aria-expanded', 'false');
325
+ }
326
+
327
+ function setActive(idx) {
328
+ const items = dirList.querySelectorAll('.modal-dropdown-item');
329
+ if (!items.length) return;
330
+ activeIdx = (idx + items.length) % items.length;
331
+ items.forEach((it, i) => it.classList.toggle('active', i === activeIdx));
332
+ items[activeIdx].scrollIntoView({ block: 'nearest' });
333
+ }
334
+
335
+ function renderDropdown(dirs) {
336
+ dirList.innerHTML = '';
337
+ activeIdx = -1;
338
+ if (!dirs.length) { closeDropdown(); return; }
339
+ for (const d of dirs) {
340
+ const item = document.createElement('div');
341
+ item.className = 'modal-dropdown-item';
342
+ item.setAttribute('role', 'option');
343
+ item.textContent = d;
344
+ // mousedown (not click) so the input doesn't blur before we read the value
345
+ item.addEventListener('mousedown', (e) => {
346
+ e.preventDefault();
347
+ dirInput.value = d;
348
+ closeDropdown();
349
+ dirInput.focus();
350
+ });
351
+ dirList.appendChild(item);
352
+ }
353
+ dirList.classList.add('open');
354
+ dirInput.setAttribute('aria-expanded', 'true');
355
+ }
356
+
357
+ dirInput.addEventListener('input', () => {
358
+ clearTimeout(debounceTimer);
359
+ const val = dirInput.value.trim();
360
+ if (!val) { closeDropdown(); return; }
361
+ debounceTimer = setTimeout(async () => {
362
+ try {
363
+ const res = await fetch('/api/fs/list?path=' + encodeURIComponent(val));
364
+ const data = await res.json();
365
+ renderDropdown(data.dirs || []);
366
+ } catch { closeDropdown(); }
367
+ }, 200);
368
+ });
369
+
370
+ dirInput.addEventListener('blur', () => { setTimeout(closeDropdown, 120); });
371
+
372
+ async function submit() {
373
+ const name = nameInput.value.trim();
374
+ const dir = dirInput.value.trim();
375
+ if (!name) { showError('Session name is required.'); nameInput.focus(); return; }
376
+ submitBtn.disabled = true;
377
+ submitBtn.textContent = 'Creating\u2026';
378
+ errorEl.style.display = 'none';
379
+ try {
380
+ const res = await fetch('/api/sessions/new', {
381
+ method: 'POST',
382
+ headers: { 'Content-Type': 'application/json' },
383
+ body: JSON.stringify({ name, dir: dir || undefined }),
384
+ });
385
+ const data = await res.json();
386
+ if (!res.ok) { showError(data.error || 'Failed to create session.'); return; }
387
+ window.location.href = '/s/' + encodeURIComponent(name);
388
+ } catch { showError('Network error. Please try again.'); }
389
+ finally { submitBtn.disabled = false; submitBtn.textContent = 'Create'; }
390
+ }
391
+
392
+ function showError(msg) { errorEl.textContent = msg; errorEl.style.display = 'block'; }
393
+
394
+ submitBtn.addEventListener('click', submit);
395
+ nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') dirInput.focus(); });
396
+ dirInput.addEventListener('keydown', (e) => {
397
+ const open = dirList.classList.contains('open');
398
+ const items = dirList.querySelectorAll('.modal-dropdown-item');
399
+ if (open && items.length) {
400
+ if (e.key === 'ArrowDown') { e.preventDefault(); setActive(activeIdx + 1); return; }
401
+ if (e.key === 'ArrowUp') { e.preventDefault(); setActive(activeIdx - 1); return; }
402
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeDropdown(); return; }
403
+ if (e.key === 'Enter' && activeIdx >= 0) {
404
+ e.preventDefault();
405
+ dirInput.value = items[activeIdx].textContent;
406
+ closeDropdown();
407
+ return;
408
+ }
409
+ }
410
+ if (e.key === 'Enter') submit();
411
+ });
412
+ })();`}export{M as newSessionModalHTML,j as newSessionModalScript,z as sharedHeader,T as sharedLayoutCSS,D as sharedSidebar,I as themeSwitcherButtonHTML,E as themeSwitcherScript};
@@ -0,0 +1 @@
1
+ import e from"node:path";import{homedir as n}from"node:os";function s(t){if(!t)return!1;const o=t.trim().toLowerCase();return o==="1"||o==="true"||o==="yes"}function i(){const t=process.env.TMUX_WEB_MODE?.trim().toLowerCase();return t==="development"||t==="dev"||s(process.env.TMUX_WEB_DEV)?!0:process.env.NODE_ENV==="development"||process.env.npm_lifecycle_event==="dev"}function r(){return i()?e.join(n(),".dev",".tmux-web"):e.join(n(),".tmux-web")}function u(){return i()?e.join(n(),".dev",".config"):e.join(n(),".config")}function f(){return e.join(u(),"tmux-web","settings.json")}function m(){return e.join(u(),"tmux-web","theme.json")}function d(){return e.join(r(),"node_modules")}function x(t){return e.join(r(),"extensions",t)}function a(){return e.join(r(),"uploads")}export{u as getConfigRoot,r as getDataRoot,x as getExtensionDataDir,d as getPluginDir,f as getSettingsPath,m as getThemePath,a as getUploadsRoot};
@@ -0,0 +1 @@
1
+ function n(s,r){if(!s)return r;const e=parseInt(s,10);return Number.isFinite(e)&&e>0?e:r}function i(){return{initialLines:n(process.env.TMUX_WEB_INITIAL_LINES,1e3),historyChunk:n(process.env.TMUX_WEB_HISTORY_CHUNK,500),syncIdleMs:n(process.env.TMUX_WEB_SYNC_IDLE_MS,200),syncMaxMs:n(process.env.TMUX_WEB_SYNC_MAX_MS,3e3)}}export{i as readTerminalBufferConfig};
@@ -0,0 +1,2 @@
1
+ import{readFile as o,writeFile as a,mkdir as m}from"node:fs/promises";import l from"node:path";import{getThemePath as c}from"./state-paths.js";import{isThemeTemplateId as s,resolveTheme as i}from"./themes/index.js";const r=c();function f(t){if(!t||typeof t!="object")return!1;const e=t;return s(e.template)&&typeof e.shell=="object"&&e.shell!==null&&typeof e.shell.pageBg=="string"&&typeof e.terminal=="object"&&e.terminal!==null&&typeof e.terminal.background=="string"}async function n(t){await m(l.dirname(r),{recursive:!0}),await a(r,JSON.stringify(t,null,2)+`
2
+ `)}async function y(){try{const e=JSON.parse(await o(r,"utf-8"));if(f(e))return e}catch{}const t=i("vscode");return await n(t),t}async function w(t){const e=i(t);return await n(e),e}export{y as readActiveTheme,w as setActiveThemeTemplate,n as writeActiveTheme};
@@ -0,0 +1,28 @@
1
+ import{vscodeTheme as n}from"./themes/vscode.js";function a(e=n.shell){return`
2
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap');
3
+ :root {
4
+ --page-bg: ${e.pageBg};
5
+ --page-fg: ${e.pageFg};
6
+ --panel-bg: ${e.panelBg};
7
+ --panel-border: ${e.panelBorder};
8
+ --panel-muted: ${e.panelMuted};
9
+ --panel-accent: ${e.panelAccent};
10
+ --panel-accent-on: ${e.panelAccentOn};
11
+ --panel-success: ${e.panelSuccess};
12
+ --terminal-bg: ${e.terminalBg};
13
+ --header-gradient: ${e.headerGradient??"none"};
14
+
15
+ /* Typography */
16
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
17
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace;
18
+
19
+ --text-xs: 0.75rem; /* 12px */
20
+ --text-sm: 0.875rem; /* 14px */
21
+ --text-base: 1rem; /* 16px */
22
+ --text-lg: 1.25rem; /* 20px */
23
+ --text-xl: 1.5rem; /* 24px */
24
+ --text-2xl: 1.75rem; /* 28px */
25
+ }
26
+ * { margin: 0; padding: 0; box-sizing: border-box; }
27
+ html { font-family: var(--font-sans); font-size: 16px; line-height: 1.5; }
28
+ body { font-family: var(--font-sans); }`}export{a as cssVarsStyle};
@@ -0,0 +1 @@
1
+ const e={template:"dark-cove",shell:{pageBg:"#1a1d23",pageFg:"#e8eaed",panelBg:"#22262d",panelBorder:"#2f353f",panelMuted:"#9097a3",panelAccent:"#7aa2f7",panelAccentOn:"#15181d",panelSuccess:"#73daca",terminalBg:"#15181d",headerGradient:"linear-gradient(180deg, rgba(34, 38, 45, 0.98), rgba(26, 29, 35, 0.98))"},terminal:{foreground:"#c0caf5",background:"#15181d",cursor:"#c0caf5",cursorAccent:"#15181d",selectionBackground:"rgba(122, 162, 247, 0.35)",selectionForeground:"#c0caf5",black:"#15161e",red:"#f7768e",green:"#9ece6a",yellow:"#e0af68",blue:"#7aa2f7",magenta:"#bb9af7",cyan:"#7dcfff",white:"#a9b1d6",brightBlack:"#414868",brightRed:"#f7768e",brightGreen:"#9ece6a",brightYellow:"#e0af68",brightBlue:"#7aa2f7",brightMagenta:"#bb9af7",brightCyan:"#7dcfff",brightWhite:"#c0caf5"}};export{e as darkCoveTheme};
@@ -0,0 +1 @@
1
+ const e={template:"ghostty",shell:{pageBg:"#1a1b26",pageFg:"#a9b1d6",panelBg:"#1f2335",panelBorder:"#292e42",panelMuted:"#565f89",panelAccent:"#7aa2f7",panelAccentOn:"#1a1b26",panelSuccess:"#9ece6a",terminalBg:"#1a1b26",headerGradient:"linear-gradient(180deg, rgba(31, 35, 53, 0.98), rgba(26, 27, 38, 0.98))"},terminal:{foreground:"#c0caf5",background:"#1a1b26",cursor:"#c0caf5",cursorAccent:"#1a1b26",selectionBackground:"#33467c",selectionForeground:"#c0caf5",black:"#15161e",red:"#f7768e",green:"#9ece6a",yellow:"#e0af68",blue:"#7aa2f7",magenta:"#bb9af7",cyan:"#7dcfff",white:"#a9b1d6",brightBlack:"#414868",brightRed:"#f7768e",brightGreen:"#9ece6a",brightYellow:"#e0af68",brightBlue:"#7aa2f7",brightMagenta:"#bb9af7",brightCyan:"#7dcfff",brightWhite:"#c0caf5"}};export{e as ghosttyTheme};
@@ -0,0 +1 @@
1
+ import{darkCoveTheme as t}from"./dark-cove.js";import{ghosttyTheme as m}from"./ghostty.js";import{vscodeTheme as h}from"./vscode.js";import{warmClayTheme as T}from"./warm-clay.js";import{darkCoveTheme as w}from"./dark-cove.js";import{ghosttyTheme as k}from"./ghostty.js";import{vscodeTheme as v}from"./vscode.js";import{warmClayTheme as l}from"./warm-clay.js";const o={vscode:h,ghostty:m,"warm-clay":T,"dark-cove":t},f=Object.keys(o);function a(){return o}function d(e){const r=o[e];if(!r)throw new Error(`unknown theme template: ${e}`);return structuredClone(r)}function i(e){return e==="vscode"||e==="ghostty"||e==="warm-clay"||e==="dark-cove"}export{f as THEME_TEMPLATE_IDS,w as darkCoveTheme,a as getThemeTemplates,k as ghosttyTheme,i as isThemeTemplateId,d as resolveTheme,v as vscodeTheme,l as warmClayTheme};
File without changes
@@ -0,0 +1 @@
1
+ const e={template:"vscode",shell:{pageBg:"#111111",pageFg:"#d0d0d0",panelBg:"#11161d",panelBorder:"#243241",panelMuted:"#8a97a6",panelAccent:"#7dd3fc",panelAccentOn:"#0d1117",panelSuccess:"#73c991",terminalBg:"#1e1e1e",headerGradient:"linear-gradient(180deg, rgba(19, 28, 36, 0.98), rgba(13, 18, 24, 0.98))"},terminal:{foreground:"#cccccc",background:"#1e1e1e",cursor:"#aeafad",cursorAccent:"#1e1e1e",selectionBackground:"#264f78",selectionForeground:"#ffffff",black:"#000000",red:"#cd3131",green:"#0dbc79",yellow:"#e5e510",blue:"#2472c8",magenta:"#bc3fbc",cyan:"#11a8cd",white:"#e5e5e5",brightBlack:"#666666",brightRed:"#f14c4c",brightGreen:"#23d18b",brightYellow:"#f5f543",brightBlue:"#3b8eea",brightMagenta:"#d670d6",brightCyan:"#29b8db",brightWhite:"#ffffff"}};export{e as vscodeTheme};
@@ -0,0 +1 @@
1
+ const e={template:"warm-clay",shell:{pageBg:"#faf8f5",pageFg:"#2d2926",panelBg:"#ffffff",panelBorder:"#ebe5df",panelMuted:"#7b726b",panelAccent:"#b86b52",panelAccentOn:"#ffffff",panelSuccess:"#6b9e7c",terminalBg:"#2b2623",headerGradient:"linear-gradient(180deg, rgba(255,255,255,0.98), rgba(250,248,245,0.98))"},terminal:{foreground:"#e8e2dc",background:"#2b2623",cursor:"#e8e2dc",cursorAccent:"#2b2623",selectionBackground:"rgba(184, 107, 82, 0.35)",selectionForeground:"#e8e2dc",black:"#2b2623",red:"#d88a8a",green:"#8fc49f",yellow:"#e0c28a",blue:"#8ab4d8",magenta:"#c9a5c9",cyan:"#8ad0d0",white:"#d8d0c8",brightBlack:"#4a433e",brightRed:"#e8a0a0",brightGreen:"#a5d6b2",brightYellow:"#efd69a",brightBlue:"#a5c8e8",brightMagenta:"#d9b8d9",brightCyan:"#a0dede",brightWhite:"#f5f0eb"}};export{e as warmClayTheme};
@@ -0,0 +1,5 @@
1
+ import{execFileSync as f}from"node:child_process";const l=3e3;function g(n){return n.replace(/\r?\n/g,`\r
2
+ `)}function o(n){return f("tmux",n,{encoding:"utf-8",timeout:l}).trimEnd()}function S(n){return o(["display-message","-p","-t",n,"#{session_name}:#{window_index}.#{pane_index}"])}function m(n){const t=o(["display-message","-p","-t",n,"#{history_size}"]),e=parseInt(t,10);return Number.isFinite(e)?e:0}function _(n){return o(["display-message","-p","-t",n,"#{alternate_on}"])==="1"}function c(n,t,e){const i=["capture-pane","-t",n,"-p","-S",String(t)];return e!==void 0&&i.push("-E",String(e)),o(i)}function h(n,t){return c(n,-t)}function y(n,t,e){const i=m(n);if(t>=i)return{data:"",lines:0};const p=-(t+e),a=-(t+1);let r=c(n,p,a);const u=r===""?0:r.split(`
3
+ `).length,s=Math.min(e,u,i-t);return s<u&&s>0&&(r=r.split(`
4
+ `).slice(-s).join(`
5
+ `)),{data:r,lines:s}}export{y as capturePaneHistoryChunk,c as capturePaneLines,h as capturePaneTail,m as getPaneHistorySize,S as getSessionPaneTarget,_ as isAlternateScreen,g as toCrlf};
@@ -0,0 +1,2 @@
1
+ import*as w from"node-pty";import{captureAndStoreWindows as p}from"./session-windows.js";import{recordWindowVisit as T}from"./window-history.js";const h=["%session-window-changed","%window-add","%window-close","%unlinked-window-close","%window-renamed","%session-renamed"];function x(e){return h.some(r=>e.startsWith(r))}const y=75,C=e=>w.spawn("tmux",["-CC","attach-session","-t",e,"-f","ignore-size,no-output"],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env});function v(e){const r=p(e);if(!r.length)return null;const o=r.find(i=>i.active);return o&&T(e,o.index,o.name),{activeIndex:o?o.index:r[0].index,windows:r.map(i=>({index:i.index,name:i.name,active:i.active}))}}const c=new Map;function M(e,r,o={}){const i=o.spawn??C,a=o.requery??v,f=o.debounceMs??y;let l=c.get(e);if(!l){let n;try{n=i(e)}catch{return()=>{}}const t={pty:n,refCount:0,subscribers:new Set,buf:"",debounceTimer:null,schedule:()=>{}},m=()=>{t.debounceTimer=null;let u;try{u=a(e)}catch{u=null}if(u)for(const d of t.subscribers)try{d(u)}catch{}};t.schedule=()=>{t.debounceTimer&&clearTimeout(t.debounceTimer),t.debounceTimer=setTimeout(m,f)},n.onData(u=>{t.buf+=u;let d;for(;(d=t.buf.indexOf(`
2
+ `))>=0;){const b=t.buf.slice(0,d).replace(/\r$/,"");t.buf=t.buf.slice(d+1),x(b)&&t.schedule()}}),n.onExit(()=>{t.debounceTimer&&clearTimeout(t.debounceTimer),c.delete(e)}),c.set(e,t),l=t}l.subscribers.add(r),l.refCount+=1,l.schedule();let s=!1;return()=>{if(s)return;s=!0;const n=c.get(e);if(n&&(n.subscribers.delete(r),n.refCount-=1,n.refCount<=0)){n.debounceTimer&&clearTimeout(n.debounceTimer);try{n.pty.kill()}catch{}c.delete(e)}}}function R(){for(const e of c.values()){e.debounceTimer&&clearTimeout(e.debounceTimer);try{e.pty.kill()}catch{}}c.clear()}function S(){return c.size}export{S as _controlClientCount,M as acquireControlClient,R as killAllControlClients,x as shouldRequery};
@@ -0,0 +1,2 @@
1
+ import{execFileSync as u}from"node:child_process";const w=3e3,l="#{pane_id} #{pane_index} #{window_index} #{pane_pid} #{pane_current_command} #{pane_active} #{window_active}";function m(t,e){const[n,a,c,r,d,p,s]=e.split(" ");if(!n)return null;const i=parseInt(c,10),o=parseInt(a,10);return{paneId:n,paneIndex:Number.isFinite(o)?o:0,windowIndex:Number.isFinite(i)?i:0,pid:parseInt(r,10)||0,command:d??"",active:p==="1",windowActive:s==="1",target:`${t}:${i}.${o}`}}function x(t){try{return u("tmux",["list-panes","-s","-t",t,"-F",l],{encoding:"utf-8",timeout:w}).trim().split(`
2
+ `).filter(Boolean).map(n=>m(t,n)).filter(n=>n!==null)}catch{return[]}}function _(t){return x(t).find(n=>n.active&&n.windowActive)??null}export{_ as getActivePaneInfo,x as listSessionPanes};
@@ -0,0 +1,4 @@
1
+ import{execFileSync as w}from"node:child_process";class r extends Error{status;constructor(n,e){super(n),this.status=e,this.name="TmuxWindowsError"}}function u(t){try{return w("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
2
+ `).filter(Boolean).map(e=>{const[o,i,s]=e.split(" ");return{index:parseInt(o,10),name:i,active:s==="1"}})}catch{return[]}}function d(t){try{return w("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active} #{pane_current_path}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
3
+ `).filter(Boolean).map(e=>{const[o,i,s,a]=e.split(" ");return{index:parseInt(o,10),name:i,active:s==="1",path:a??""}})}catch{return[]}}function f(t){if(!t)return!1;try{const n=w("git",["-C",t,"rev-parse","--git-dir","--git-common-dir"],{encoding:"utf-8",timeout:3e3,stdio:["ignore","pipe","ignore"]}),[e,o]=n.trim().split(`
4
+ `);return!e||!o?!1:e!==o}catch{return!1}}function c(t){try{return w("tmux",["has-session","-t",t],{timeout:3e3}),!0}catch{return!1}}function p(t){if(!c(t))throw new r("session not found",404);try{w("tmux",["new-window","-t",t],{timeout:3e3})}catch(n){const e=n instanceof Error?n.message:"new-window failed";throw new r(e,500)}}function h(t,n){const e=["new-session","-d","-s",t];n&&e.push("-c",n);try{w("tmux",e,{timeout:5e3})}catch(o){const i=o instanceof Error?o.message:"new-session failed";throw new r(i,500)}}function x(t,n){if(!c(t))throw new r("session not found",404);if(!u(t).some(o=>o.index===n))throw new r("window not found",404);try{w("tmux",["select-window","-t",`${t}:${n}`],{timeout:3e3})}catch(o){const i=o instanceof Error?o.message:"select-window failed";throw new r(i,500)}}function l(t,n,e){if(!c(t))throw new r("session not found",404);if(!u(t).some(s=>s.index===n))throw new r("window not found",404);const i=e.trim();if(!i)throw new r("name is required",500);try{w("tmux",["rename-window","-t",`${t}:${n}`,i],{timeout:3e3})}catch(s){const a=s instanceof Error?s.message:"rename-window failed";throw new r(a,500)}}export{r as TmuxWindowsError,d as captureSessionWindowsWithPath,f as isGitWorktree,u as listSessionWindows,p as newSessionWindow,h as newTmuxSession,l as renameSessionWindow,x as selectSessionWindow};
@@ -0,0 +1 @@
1
+ import{db as a}from"./db.js";const c=50;async function o(t){const e={...t,watchedAt:Date.now()},n=a.data.watchedPanes.filter(d=>d.paneId!==t.paneId);a.data.watchedPanes=[e,...n].slice(0,c),await a.write()}function r(){return a.data.watchedPanes}async function h(t){const e=a.data.watchedPanes.length;a.data.watchedPanes=a.data.watchedPanes.filter(n=>t.has(n.paneId)),a.data.watchedPanes.length!==e&&await a.write()}export{c as MAX_WATCHED_PANES,r as getWatchedPanes,h as pruneWatchedPanes,o as recordWatchedPane};
@@ -0,0 +1 @@
1
+ import{db as t}from"./db.js";const e=200;async function c(i,r,o){const s=t.data.windowHistory??=[],a={sessionName:i,windowIndex:r,windowName:o,visitedAt:Date.now()},w=s.filter(n=>!(n.sessionName===i&&n.windowName===o));t.data.windowHistory=[a,...w].slice(0,e),await t.write()}function y(){return t.data.windowHistory??[]}async function H(){(t.data.windowHistory??[]).length&&(t.data.windowHistory=[],await t.write())}export{e as MAX_WINDOW_HISTORY,H as clearWindowHistory,y as listWindowHistory,c as recordWindowVisit};
@@ -0,0 +1 @@
1
+ import{db as a}from"./db.js";function r(t){return(a.data.windowLabels??[]).filter(n=>n.sessionName===t)}async function b(t,n,w){const e=a.data.windowLabels??=[],d=w.trim(),i=e.findIndex(o=>o.sessionName===t&&o.windowIndex===n);if(!d)return i>=0&&(e.splice(i,1),await a.write()),r(t);const l=Date.now();return i>=0?(e[i].label=d,e[i].updatedAt=l):e.push({sessionName:t,windowIndex:n,label:d,updatedAt:l}),await a.write(),r(t)}export{r as listWindowLabels,b as setWindowLabel};