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,634 @@
1
+ import{closeOtherDrawersExcept as n,wrapDrawerScript as s}from"./drawer-script.js";import{drawerResizeCSS as o,drawerResizeHandleHTML as t,drawerResizeScriptLeft as i}from"./drawer-resize.js";import{icon as r}from"./icons.js";function l(){return`
2
+ #sessions-backdrop {
3
+ position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 999;
4
+ opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
5
+ }
6
+ #sessions-backdrop.open { opacity: 1; pointer-events: auto; }
7
+ #sessions-drawer {
8
+ position: fixed; left: 0; top: 0; height: 100%; width: 360px; z-index: 1000;
9
+ background: var(--panel-bg); border-right: 1px solid var(--panel-border);
10
+ display: flex; flex-direction: column;
11
+ transform: translateX(-100%); transition: transform 0.25s ease;
12
+ }
13
+ #sessions-drawer.open { transform: translateX(0); }
14
+ #sessions-drawer .drawer-resize-handle {
15
+ left: auto; right: -5px;
16
+ }
17
+ ${o()}
18
+ #sessions-drawer .drawer-header {
19
+ display: flex; justify-content: space-between; align-items: center;
20
+ padding: 10px 16px; border-bottom: 1px solid var(--panel-border);
21
+ font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
22
+ color: var(--panel-accent); font-family: var(--font-mono); flex-shrink: 0;
23
+ }
24
+ #sessions-drawer .drawer-header button {
25
+ background: none; border: none; color: var(--panel-muted); cursor: pointer;
26
+ font-size: var(--text-lg); line-height: 1; min-width: 44px; min-height: 44px; padding: 8px; border-radius: 8px; transition: color 0.15s, background 0.15s;
27
+ }
28
+ #sessions-drawer .drawer-header button:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
29
+ #sessions-drawer .drawer-header button:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
30
+ #sessions-toolbar {
31
+ display: flex; align-items: center; justify-content: flex-end;
32
+ padding: 8px 16px; border-bottom: 1px solid var(--panel-border); flex-shrink: 0;
33
+ }
34
+ #sessions-pin-window {
35
+ font-size: var(--text-xs); color: var(--panel-muted); background: none;
36
+ border: 1px solid var(--panel-border); padding: 8px 14px; border-radius: 6px;
37
+ cursor: pointer; font-family: var(--font-mono); transition: all 0.15s;
38
+ min-height: 44px;
39
+ }
40
+ #sessions-pin-window:hover:not(:disabled) {
41
+ border-color: var(--panel-accent); color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent);
42
+ }
43
+ #sessions-pin-window:disabled { opacity: 0.4; cursor: not-allowed; }
44
+ #sessions-pin-window:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
45
+ #sessions-error {
46
+ padding: 8px 16px; font-size: var(--text-xs); color: #cc6666;
47
+ font-family: var(--font-mono); flex-shrink: 0;
48
+ border-bottom: 1px solid var(--panel-border);
49
+ display: none;
50
+ }
51
+ #sessions-error.show { display: block; }
52
+ #sessions-list {
53
+ flex: 1; overflow-y: auto; padding: 8px 0;
54
+ scrollbar-width: thin;
55
+ scrollbar-color: var(--panel-border) transparent;
56
+ }
57
+ #sessions-list::-webkit-scrollbar { width: 4px; }
58
+ #sessions-list::-webkit-scrollbar-track { background: transparent; }
59
+ #sessions-list::-webkit-scrollbar-thumb {
60
+ background: var(--panel-border); border-radius: 2px;
61
+ }
62
+ #sessions-list::-webkit-scrollbar-thumb:hover { background: var(--panel-muted); }
63
+ .sessions-section-label {
64
+ padding: 8px 16px 4px; font-size: var(--text-xs); letter-spacing: 0.08em;
65
+ text-transform: uppercase; color: var(--panel-muted);
66
+ font-family: var(--font-mono);
67
+ }
68
+ .sessions-row {
69
+ display: flex; align-items: center; gap: 8px;
70
+ width: 100%; min-height: 44px; padding: 10px 12px 10px 16px;
71
+ background: none; border: none; border-bottom: 1px solid var(--panel-border);
72
+ color: var(--page-fg); cursor: pointer; text-align: left;
73
+ font-family: var(--font-mono); transition: background 0.15s;
74
+ }
75
+ .sessions-row:last-child { border-bottom: none; }
76
+ .sessions-row:hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
77
+ .sessions-row.current { background: color-mix(in srgb, var(--panel-accent) 10%, transparent); }
78
+ .sessions-row.missing { opacity: 0.65; }
79
+ .sessions-row-main {
80
+ flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px;
81
+ }
82
+ .sessions-row-name {
83
+ font-size: var(--text-sm); color: var(--panel-accent); overflow: hidden;
84
+ text-overflow: ellipsis; white-space: nowrap;
85
+ }
86
+ .sessions-row-meta {
87
+ font-size: var(--text-xs); color: var(--panel-muted); overflow: hidden;
88
+ text-overflow: ellipsis; white-space: nowrap;
89
+ }
90
+ .sessions-pin-btn {
91
+ flex-shrink: 0; display: flex; align-items: center; justify-content: center;
92
+ background: none; border: none; cursor: pointer; min-width: 44px; min-height: 44px;
93
+ color: var(--panel-muted); font-size: var(--text-base); line-height: 1; padding: 4px;
94
+ border-radius: 6px; transition: color 0.15s, background 0.15s;
95
+ }
96
+ .sessions-pin-btn:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
97
+ .sessions-pin-btn:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
98
+ .sessions-pin-btn.pinned { color: #fbbf24; }
99
+ .sessions-row-icon {
100
+ flex-shrink: 0; width: 44px; height: 44px; color: var(--panel-muted);
101
+ border: none; background: none; padding: 0; border-radius: 6px;
102
+ display: flex; align-items: center; justify-content: center;
103
+ }
104
+ .sessions-row-icon svg { width: 16px; height: 16px; fill: currentColor; display: block; }
105
+ button.sessions-row-icon { cursor: pointer; transition: color 0.15s, background 0.15s; }
106
+ button.sessions-row-icon:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
107
+ button.sessions-row-icon:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
108
+ button.sessions-row-icon.expanded { color: var(--panel-accent); }
109
+ .sessions-item { display: flex; flex-direction: column; }
110
+ .sessions-windows {
111
+ display: none; flex-direction: column;
112
+ background: color-mix(in srgb, var(--page-bg) 60%, transparent);
113
+ border-bottom: 1px solid var(--panel-border);
114
+ }
115
+ .sessions-windows.open { display: flex; }
116
+ .sessions-window-row {
117
+ display: flex; align-items: center; gap: 8px;
118
+ padding: 8px 12px 8px 40px; cursor: pointer; color: var(--page-fg);
119
+ font-family: var(--font-mono);
120
+ border-bottom: 1px solid var(--panel-border);
121
+ transition: background 0.15s;
122
+ }
123
+ .sessions-window-row:last-child { border-bottom: none; }
124
+ .sessions-window-row:hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
125
+ .sessions-window-index {
126
+ font-size: var(--text-xs); color: var(--panel-muted); flex-shrink: 0; min-width: 18px;
127
+ }
128
+ .sessions-window-name {
129
+ flex: 1; min-width: 0; font-size: var(--text-xs);
130
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
131
+ }
132
+ .sessions-window-wt {
133
+ flex-shrink: 0; width: 13px; height: 13px; fill: var(--panel-success, #7ee787);
134
+ display: block;
135
+ }
136
+ .sessions-window-input {
137
+ flex: 1; min-width: 0; font-size: var(--text-xs);
138
+ font-family: var(--font-mono);
139
+ background: var(--page-bg); color: var(--page-fg);
140
+ border: 1px solid var(--panel-accent); border-radius: 4px; padding: 2px 6px;
141
+ }
142
+ .sessions-window-edit {
143
+ flex-shrink: 0; display: flex; align-items: center; justify-content: center;
144
+ background: none; border: none; cursor: pointer; min-width: 44px; min-height: 44px;
145
+ color: var(--panel-muted); padding: 4px; border-radius: 6px;
146
+ transition: color 0.15s, background 0.15s;
147
+ }
148
+ .sessions-window-edit:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
149
+ .sessions-window-edit:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
150
+ .sessions-window-edit svg { width: 13px; height: 13px; fill: currentColor; display: block; }
151
+ .sessions-windows-empty {
152
+ padding: 8px 12px 8px 40px; font-size: var(--text-xs); color: var(--panel-muted);
153
+ font-family: var(--font-mono);
154
+ }
155
+ .sessions-empty {
156
+ padding: 16px; text-align: center; color: var(--panel-muted);
157
+ font-size: var(--text-xs); font-family: var(--font-mono);
158
+ }
159
+ @media (max-width: 560px) {
160
+ #sessions-drawer { width: min(100vw - 16px, 400px); }
161
+ .sessions-row { min-height: 48px; padding: 12px 12px 12px 16px; }
162
+ .sessions-row-name { font-size: var(--text-sm); }
163
+ }`}function w(){return`<button class="sessions-btn" id="sessions-toggle" title="Recent sessions" aria-label="Recent sessions">
164
+ ${r("menu")}
165
+ </button>`}function p(){return`
166
+ <div id="sessions-backdrop"></div>
167
+ <div id="sessions-drawer" class="resizable-drawer">
168
+ ${t()}
169
+ <div class="drawer-header">
170
+ <span>Sessions</span>
171
+ <button id="sessions-close">&times;</button>
172
+ </div>
173
+ <div id="sessions-toolbar">
174
+ <button type="button" id="sessions-pin-window">Pin current window</button>
175
+ </div>
176
+ <div id="sessions-error"></div>
177
+ <div id="sessions-list"></div>
178
+ </div>`}function m(e){return s("sessions",`
179
+ ${i("sessions-drawer","tmux-web:drawer-width:sessions",360)}
180
+ const SIDEBAR_SESSION = ${JSON.stringify(e)};
181
+ const sessionsDrawer = document.getElementById('sessions-drawer');
182
+ const sessionsBackdrop = document.getElementById('sessions-backdrop');
183
+ const sessionsList = document.getElementById('sessions-list');
184
+ const sessionsError = document.getElementById('sessions-error');
185
+ const pinWindowBtn = document.getElementById('sessions-pin-window');
186
+ let sidebarData = { pinned: [], recent: [], currentSession: SIDEBAR_SESSION };
187
+ let activeWindowIndex = null;
188
+
189
+ const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
190
+
191
+ function relativeTime(ts) {
192
+ if (!ts) return '';
193
+ const diffMs = Date.now() - ts;
194
+ if (diffMs < 45000) return 'just now';
195
+ const minutes = Math.floor(diffMs / 60000);
196
+ const hours = Math.floor(minutes / 60);
197
+ const days = Math.floor(hours / 24);
198
+ const weeks = Math.floor(days / 7);
199
+ const months = Math.floor(days / 30);
200
+ if (minutes < 60) return formatter.format(-minutes, 'minute');
201
+ if (hours < 24) return formatter.format(-hours, 'hour');
202
+ if (days < 7) return formatter.format(-days, 'day');
203
+ if (weeks < 5) return formatter.format(-weeks, 'week');
204
+ if (months < 12) return formatter.format(-months, 'month');
205
+ return formatter.format(-Math.floor(days / 365), 'year');
206
+ }
207
+
208
+ function showSessionsError(msg) {
209
+ sessionsError.textContent = msg;
210
+ sessionsError.classList.add('show');
211
+ }
212
+
213
+ function clearSessionsError() {
214
+ sessionsError.textContent = '';
215
+ sessionsError.classList.remove('show');
216
+ }
217
+
218
+ function clearSessionsList(el) {
219
+ while (el.firstChild) el.removeChild(el.firstChild);
220
+ }
221
+
222
+ function sessionMeta(session) {
223
+ const parts = [
224
+ session.windows + ' window' + (session.windows === 1 ? '' : 's'),
225
+ ];
226
+ if (session.attached) parts.push('attached');
227
+ const recent = relativeTime(session.lastAccessedAt);
228
+ if (recent) parts.push(recent);
229
+ return parts.join(' \xB7 ');
230
+ }
231
+
232
+ function pinnedLabel(view) {
233
+ if (view.windowIndex !== undefined) {
234
+ const suffix = view.windowName ? ' \xB7 ' + view.windowName : '';
235
+ return view.sessionName + ' \xB7 win ' + view.windowIndex + suffix;
236
+ }
237
+ return view.sessionName;
238
+ }
239
+
240
+ function pinnedMeta(view) {
241
+ if (view.missing) return 'missing';
242
+ const parts = [];
243
+ if (view.windows !== undefined) {
244
+ parts.push(view.windows + ' window' + (view.windows === 1 ? '' : 's'));
245
+ }
246
+ if (view.attached) parts.push('attached');
247
+ return parts.join(' \xB7 ') || 'pinned';
248
+ }
249
+
250
+ function viewHref(view) {
251
+ if (view.windowIndex !== undefined) {
252
+ return '/s/' + encodeURIComponent(view.sessionName) + '?window=' + view.windowIndex;
253
+ }
254
+ return '/s/' + encodeURIComponent(view.sessionName);
255
+ }
256
+
257
+ function isCurrentView(view) {
258
+ if (view.sessionName !== SIDEBAR_SESSION) return false;
259
+ if (view.windowIndex === undefined) return true;
260
+ return activeWindowIndex === view.windowIndex;
261
+ }
262
+
263
+ function isCurrentSession(session) {
264
+ return session.name === SIDEBAR_SESSION;
265
+ }
266
+
267
+ function createPinButton(pinned, sessionName, windowIndex) {
268
+ const btn = document.createElement('button');
269
+ btn.type = 'button';
270
+ btn.className = 'sessions-pin-btn' + (pinned ? ' pinned' : '');
271
+ btn.title = pinned ? 'Unpin' : 'Pin';
272
+ btn.setAttribute('aria-label', pinned ? 'Unpin' : 'Pin');
273
+ btn.textContent = pinned ? '\u2605' : '\u2606';
274
+ btn.addEventListener('click', (event) => {
275
+ event.stopPropagation();
276
+ void togglePin(sessionName, windowIndex, pinned);
277
+ });
278
+ return btn;
279
+ }
280
+
281
+ function makeSvgIcon(pathData, className) {
282
+ const el = document.createElement('button');
283
+ el.type = 'button';
284
+ el.className = className;
285
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
286
+ svg.setAttribute('viewBox', '0 0 24 24');
287
+ svg.setAttribute('aria-hidden', 'true');
288
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
289
+ path.setAttribute('d', pathData);
290
+ svg.appendChild(path);
291
+ el.appendChild(svg);
292
+ return el;
293
+ }
294
+
295
+ const FOLDER_PATH = 'M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z';
296
+ const PENCIL_PATH = 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z';
297
+ const WORKTREE_PATH = 'M6 3a3 3 0 0 0-1 5.83v6.34a3 3 0 1 0 2 0V8.83A3 3 0 0 0 6 3zm0 2a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM18 3a3 3 0 0 0-1 5.83V9a4 4 0 0 1-4 4h-2a3 3 0 1 0 0 2h2a6 6 0 0 0 6-6v-.17A3 3 0 0 0 18 3zm0 2a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM9 13a1 1 0 1 1 0 2 1 1 0 0 1 0-2z';
298
+
299
+ function startWindowEdit(sessionName, win, nameEl) {
300
+ if (nameEl.parentNode && nameEl.parentNode.querySelector('.sessions-window-input')) return;
301
+ const input = document.createElement('input');
302
+ input.type = 'text';
303
+ input.className = 'sessions-window-input';
304
+ input.value = win.label || win.name || '';
305
+ input.placeholder = win.name || '';
306
+ input.addEventListener('click', (event) => event.stopPropagation());
307
+
308
+ let done = false;
309
+ const finish = async (save) => {
310
+ if (done) return;
311
+ done = true;
312
+ if (save) {
313
+ try {
314
+ const res = await fetch(
315
+ '/api/session/' + encodeURIComponent(sessionName) + '/window-label',
316
+ {
317
+ method: 'POST',
318
+ headers: { 'Content-Type': 'application/json' },
319
+ body: JSON.stringify({ windowIndex: win.index, label: input.value }),
320
+ },
321
+ );
322
+ if (res.ok) win.label = input.value.trim() || null;
323
+ } catch {}
324
+ }
325
+ nameEl.textContent = win.label || win.name;
326
+ input.replaceWith(nameEl);
327
+ };
328
+
329
+ input.addEventListener('keydown', (event) => {
330
+ if (event.key === 'Enter') { event.preventDefault(); void finish(true); }
331
+ else if (event.key === 'Escape') { event.preventDefault(); void finish(false); }
332
+ });
333
+ input.addEventListener('blur', () => { void finish(true); });
334
+
335
+ nameEl.replaceWith(input);
336
+ input.focus();
337
+ input.select();
338
+ }
339
+
340
+ function createWindowRow(sessionName, win) {
341
+ const row = document.createElement('div');
342
+ row.className = 'sessions-window-row';
343
+
344
+ const idx = document.createElement('span');
345
+ idx.className = 'sessions-window-index';
346
+ idx.textContent = String(win.index);
347
+
348
+ const name = document.createElement('span');
349
+ name.className = 'sessions-window-name';
350
+ name.textContent = win.label || win.name;
351
+
352
+ row.appendChild(idx);
353
+ if (win.worktree) {
354
+ const wt = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
355
+ wt.setAttribute('viewBox', '0 0 24 24');
356
+ wt.setAttribute('class', 'sessions-window-wt');
357
+ const wtTitle = document.createElementNS('http://www.w3.org/2000/svg', 'title');
358
+ wtTitle.textContent = 'git worktree';
359
+ const wtPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
360
+ wtPath.setAttribute('d', WORKTREE_PATH);
361
+ wt.appendChild(wtTitle);
362
+ wt.appendChild(wtPath);
363
+ row.appendChild(wt);
364
+ }
365
+
366
+ const editBtn = makeSvgIcon(PENCIL_PATH, 'sessions-window-edit');
367
+ editBtn.title = 'Rename label';
368
+ editBtn.setAttribute('aria-label', 'Rename label');
369
+ editBtn.addEventListener('click', (event) => {
370
+ event.stopPropagation();
371
+ startWindowEdit(sessionName, win, name);
372
+ });
373
+
374
+ row.appendChild(name);
375
+ row.appendChild(editBtn);
376
+
377
+ row.addEventListener('click', () => {
378
+ window.location.href = '/s/' + encodeURIComponent(sessionName) + '?window=' + win.index;
379
+ });
380
+
381
+ return row;
382
+ }
383
+
384
+ async function loadWindows(sessionName, container) {
385
+ clearSessionsList(container);
386
+ let windows = [];
387
+ try {
388
+ const res = await fetch('/api/sidebar/session-windows/' + encodeURIComponent(sessionName));
389
+ if (res.ok) windows = await res.json();
390
+ } catch {}
391
+ clearSessionsList(container);
392
+ if (!windows.length) {
393
+ container.appendChild(Object.assign(document.createElement('div'), {
394
+ className: 'sessions-windows-empty',
395
+ textContent: 'Open this session to load its windows',
396
+ }));
397
+ } else {
398
+ for (const win of windows) container.appendChild(createWindowRow(sessionName, win));
399
+ }
400
+ container.dataset.loaded = '1';
401
+ }
402
+
403
+ async function toggleWindows(sessionName, iconEl, container) {
404
+ if (container.classList.contains('open')) {
405
+ container.classList.remove('open');
406
+ iconEl.classList.remove('expanded');
407
+ return;
408
+ }
409
+ container.classList.add('open');
410
+ iconEl.classList.add('expanded');
411
+ if (!container.dataset.loaded) await loadWindows(sessionName, container);
412
+ }
413
+
414
+ function createRow({ label, meta, href, current, missing, pinned, sessionName, windowIndex, expandable }) {
415
+ const row = document.createElement('button');
416
+ row.type = 'button';
417
+ row.className = 'sessions-row'
418
+ + (current ? ' current' : '')
419
+ + (missing ? ' missing' : '');
420
+
421
+ let folderIcon = null;
422
+ if (expandable) {
423
+ folderIcon = makeSvgIcon(FOLDER_PATH, 'sessions-row-icon');
424
+ folderIcon.title = 'Show windows';
425
+ folderIcon.setAttribute('aria-label', 'Show windows');
426
+ row.appendChild(folderIcon);
427
+ }
428
+
429
+ const main = document.createElement('span');
430
+ main.className = 'sessions-row-main';
431
+
432
+ const name = document.createElement('span');
433
+ name.className = 'sessions-row-name';
434
+ name.textContent = label;
435
+
436
+ const metaEl = document.createElement('span');
437
+ metaEl.className = 'sessions-row-meta';
438
+ metaEl.textContent = meta;
439
+
440
+ main.appendChild(name);
441
+ main.appendChild(metaEl);
442
+ row.appendChild(main);
443
+ row.appendChild(createPinButton(pinned, sessionName, windowIndex));
444
+
445
+ if (!missing) {
446
+ row.addEventListener('click', () => {
447
+ window.location.href = href;
448
+ });
449
+ }
450
+
451
+ if (!expandable) return row;
452
+
453
+ const wrap = document.createElement('div');
454
+ wrap.className = 'sessions-item';
455
+ const windowsContainer = document.createElement('div');
456
+ windowsContainer.className = 'sessions-windows';
457
+ folderIcon.addEventListener('click', (event) => {
458
+ event.stopPropagation();
459
+ void toggleWindows(sessionName, folderIcon, windowsContainer);
460
+ });
461
+ wrap.appendChild(row);
462
+ wrap.appendChild(windowsContainer);
463
+ return wrap;
464
+ }
465
+
466
+ function renderSectionLabel(text) {
467
+ const label = document.createElement('div');
468
+ label.className = 'sessions-section-label';
469
+ label.textContent = text;
470
+ return label;
471
+ }
472
+
473
+ function renderSidebar() {
474
+ clearSessionsList(sessionsList);
475
+
476
+ const pinned = sidebarData.pinned || [];
477
+ const recent = sidebarData.recent || [];
478
+
479
+ if (!pinned.length && !recent.length) {
480
+ sessionsList.appendChild(Object.assign(document.createElement('div'), {
481
+ className: 'sessions-empty',
482
+ textContent: 'No tmux sessions found',
483
+ }));
484
+ return;
485
+ }
486
+
487
+ if (pinned.length) {
488
+ sessionsList.appendChild(renderSectionLabel('Pinned'));
489
+ for (const view of pinned) {
490
+ sessionsList.appendChild(createRow({
491
+ label: pinnedLabel(view),
492
+ meta: pinnedMeta(view),
493
+ href: viewHref(view),
494
+ current: isCurrentView(view),
495
+ missing: !!view.missing,
496
+ pinned: true,
497
+ sessionName: view.sessionName,
498
+ windowIndex: view.windowIndex,
499
+ expandable: view.windowIndex === undefined && !view.missing,
500
+ }));
501
+ }
502
+ }
503
+
504
+ if (recent.length) {
505
+ sessionsList.appendChild(renderSectionLabel('Recent'));
506
+ for (const session of recent) {
507
+ sessionsList.appendChild(createRow({
508
+ label: session.name,
509
+ meta: sessionMeta(session),
510
+ href: '/s/' + encodeURIComponent(session.name),
511
+ current: isCurrentSession(session),
512
+ missing: false,
513
+ pinned: false,
514
+ sessionName: session.name,
515
+ windowIndex: undefined,
516
+ expandable: true,
517
+ }));
518
+ }
519
+ }
520
+ }
521
+
522
+ async function fetchSidebarData() {
523
+ const url = '/api/sidebar/sessions?currentSession=' + encodeURIComponent(SIDEBAR_SESSION);
524
+ const res = await fetch(url);
525
+ if (!res.ok) throw new Error('Failed to load sessions');
526
+ return await res.json();
527
+ }
528
+
529
+ async function fetchActiveWindow() {
530
+ try {
531
+ const res = await fetch('/api/session/' + encodeURIComponent(SIDEBAR_SESSION) + '/windows');
532
+ if (!res.ok) return null;
533
+ const windows = await res.json();
534
+ const active = windows.find((window) => window.active);
535
+ return active ? active.index : null;
536
+ } catch {
537
+ return null;
538
+ }
539
+ }
540
+
541
+ function updatePinWindowButton() {
542
+ pinWindowBtn.disabled = activeWindowIndex === null;
543
+ }
544
+
545
+ async function refreshSidebar() {
546
+ sidebarData = await fetchSidebarData();
547
+ activeWindowIndex = await fetchActiveWindow();
548
+ updatePinWindowButton();
549
+ renderSidebar();
550
+ }
551
+
552
+ async function togglePin(sessionName, windowIndex, pinned) {
553
+ clearSessionsError();
554
+ try {
555
+ const res = await fetch('/api/pinned-views', {
556
+ method: pinned ? 'DELETE' : 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify(
559
+ windowIndex === undefined
560
+ ? { sessionName }
561
+ : { sessionName, windowIndex },
562
+ ),
563
+ });
564
+ if (!res.ok) {
565
+ const data = await res.json().catch(() => ({}));
566
+ showSessionsError(data.error || 'Failed to update pin');
567
+ return;
568
+ }
569
+ sidebarData = await res.json();
570
+ renderSidebar();
571
+ } catch {
572
+ showSessionsError('Failed to update pin');
573
+ }
574
+ }
575
+
576
+ async function openSessionsDrawer() {
577
+ ${n("sessions")}
578
+ clearSessionsError();
579
+ sessionsDrawer.classList.add('open');
580
+ sessionsBackdrop.classList.add('open');
581
+ await refreshSidebar();
582
+ const url = new URL(location.href);
583
+ if (url.searchParams.get('tab') !== 'sessions') {
584
+ url.searchParams.set('tab', 'sessions');
585
+ history.pushState({ sessionsOpen: true }, '', url);
586
+ }
587
+ }
588
+
589
+ function closeSessionsDrawer() {
590
+ sessionsDrawer.classList.remove('open');
591
+ sessionsBackdrop.classList.remove('open');
592
+ clearSessionsError();
593
+ const url = new URL(location.href);
594
+ if (url.searchParams.get('tab') === 'sessions') {
595
+ url.searchParams.delete('tab');
596
+ history.pushState({}, '', url);
597
+ }
598
+ }
599
+
600
+ pinWindowBtn.addEventListener('click', async () => {
601
+ if (activeWindowIndex === null) return;
602
+ clearSessionsError();
603
+ try {
604
+ const res = await fetch('/api/pinned-views', {
605
+ method: 'POST',
606
+ headers: { 'Content-Type': 'application/json' },
607
+ body: JSON.stringify({ sessionName: SIDEBAR_SESSION, windowIndex: activeWindowIndex }),
608
+ });
609
+ if (!res.ok) {
610
+ const data = await res.json().catch(() => ({}));
611
+ showSessionsError(data.error || 'Failed to pin current window');
612
+ return;
613
+ }
614
+ sidebarData = await res.json();
615
+ renderSidebar();
616
+ } catch {
617
+ showSessionsError('Failed to pin current window');
618
+ }
619
+ });
620
+
621
+ document.getElementById('sessions-toggle').addEventListener('click', () => {
622
+ if (sessionsDrawer.classList.contains('open')) closeSessionsDrawer();
623
+ else void openSessionsDrawer();
624
+ });
625
+ document.getElementById('sessions-close').addEventListener('click', closeSessionsDrawer);
626
+ sessionsBackdrop.addEventListener('click', closeSessionsDrawer);
627
+
628
+ window.addEventListener('popstate', () => {
629
+ const tab = new URLSearchParams(location.search).get('tab');
630
+ if (tab === 'sessions') void openSessionsDrawer();
631
+ else closeSessionsDrawer();
632
+ });
633
+
634
+ if (new URLSearchParams(location.search).get('tab') === 'sessions') void openSessionsDrawer();`,"closeSessionsDrawer")}export{w as sessionsDrawerButtonHTML,l as sessionsDrawerCSS,p as sessionsDrawerHTML,m as sessionsDrawerScript};
@@ -0,0 +1 @@
1
+ import{viewKey as r}from"./pinned-views.js";import{listSessionWindows as f}from"./tmux-windows.js";function A(t,o){return t.has(r(o))}function N(t,o,d){const m=new Map(t.map(n=>[n.name,n])),c=new Set(d.map(n=>r(n.sessionName,n.windowIndex))),w=[...d].sort((n,s)=>s.pinnedAt-n.pinnedAt).map(n=>{const s=m.get(n.sessionName),e={sessionName:n.sessionName,windowIndex:n.windowIndex,pinnedAt:n.pinnedAt};if(!s)return e.missing=!0,e;if(e.windows=s.windows,e.attached=s.attached,n.windowIndex!==void 0){const a=f(n.sessionName).find(u=>u.index===n.windowIndex);a?e.windowName=a.name:e.missing=!0}return e}),p=t.map(n=>({...n,lastAccessedAt:o.get(n.name)})).filter(n=>!A(c,n.name)).sort((n,s)=>{const e=n.lastAccessedAt??0,i=s.lastAccessedAt??0;return e!==i?i-e:n.name.localeCompare(s.name)});return{pinned:w,recent:p}}export{N as buildSidebarSessions};
@@ -0,0 +1,2 @@
1
+ import{readFile as i,writeFile as e,mkdir as n}from"node:fs/promises";import a from"node:path";import{getSettingsPath as o}from"./state-paths.js";const t=o();async function u(){try{return JSON.parse(await i(t,"utf-8"))}catch{return{}}}async function f(r){await n(a.dirname(t),{recursive:!0}),await e(t,JSON.stringify(r,null,2)+`
2
+ `)}export{u as readSettings,f as writeSettings};
@@ -0,0 +1 @@
1
+ import{readSettings as e,writeSettings as n}from"./settings.js";const t=[{id:"commandbar",label:"Command bar",description:"\u2318K session search + quick actions",kind:"builtin",isEnabled:a=>a.commandbar===!0,async enable(){const a=await e();await n({...a,commandbar:!0}),console.log("\u2713 command bar enabled")},async disable(){const a=await e();await n({...a,commandbar:!1}),console.log("\u2713 command bar disabled")}},{id:"agents",label:"Agents page",description:"watch AI agents (Claude, Codex, OpenCode, Cursor) in recently-viewed panes",kind:"builtin",isEnabled:a=>a.agents===!0,async enable(){const a=await e();await n({...a,agents:!0}),console.log("\u2713 agents page enabled")},async disable(){const a=await e();await n({...a,agents:!1}),console.log("\u2713 agents page disabled")}}];export{t as SETUP_FEATURES};
@@ -0,0 +1,3 @@
1
+ import*as c from"node:readline/promises";import{stdin as i,stdout as a}from"node:process";function l(){i.isTTY||(console.error(`tmux-web setup requires an interactive terminal.
2
+ Use flags instead, e.g. tmux-web setup --yes`),process.exit(1))}async function m(n,t){const o=t?"Y/n":"y/N",s=c.createInterface({input:i,output:a});try{const e=(await s.question(`${n} [${o}]: `)).trim().toLowerCase();return e?e==="y"||e==="yes":t}finally{s.close()}}async function y(n,t,o){const s=t.map(r=>r===o?r.toUpperCase():r).join("/"),e=c.createInterface({input:i,output:a});try{for(;;){const r=(await e.question(`${n} [${s}]: `)).trim().toLowerCase();if(!r)return o;const p=t.find(u=>u.toLowerCase()===r);if(p)return p;a.write(`Please choose one of: ${t.join(", ")}
3
+ `)}}finally{e.close()}}async function w(n){const t=c.createInterface({input:i,output:a});try{return(await t.question(`${n}: `)).trim()}finally{t.close()}}export{y as promptChoice,w as promptSecret,m as promptYesNo,l as requireTty};