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.
- package/README.md +70 -0
- package/dist/assets/addon-fit-D5N335P2.js +16 -0
- package/dist/assets/terminal-client.js +4 -0
- package/dist/assets/xterm-NI46XXQY.js +51 -0
- package/dist/assets/xterm.css +285 -0
- package/dist/browser/terminal-client.js +4 -0
- package/dist/frontend.js +1 -0
- package/dist/index.js +12 -0
- package/dist/lib/agent-detect.js +3 -0
- package/dist/lib/agents-watch.js +2 -0
- package/dist/lib/atomicWrite.js +1 -0
- package/dist/lib/auditLog.js +1 -0
- package/dist/lib/auth.js +2 -0
- package/dist/lib/cli.js +32 -0
- package/dist/lib/commandbar.js +555 -0
- package/dist/lib/db.js +1 -0
- package/dist/lib/drawer-resize.js +166 -0
- package/dist/lib/drawer-script.js +8 -0
- package/dist/lib/ext-loader.js +1 -0
- package/dist/lib/html.js +1 -0
- package/dist/lib/icons.js +1 -0
- package/dist/lib/image-upload.js +1 -0
- package/dist/lib/load-env.js +6 -0
- package/dist/lib/mobile-toolbar.js +313 -0
- package/dist/lib/notes-db.js +18 -0
- package/dist/lib/notes-drawer.js +210 -0
- package/dist/lib/notes-utils.js +15 -0
- package/dist/lib/pages/agents-index.js +105 -0
- package/dist/lib/pages/history-index.js +85 -0
- package/dist/lib/pages/landing.js +78 -0
- package/dist/lib/pages/login.js +191 -0
- package/dist/lib/pages/notes-index.js +65 -0
- package/dist/lib/pages/notes-page.js +158 -0
- package/dist/lib/pages/quick-commands.js +361 -0
- package/dist/lib/pages/schedule-index.js +392 -0
- package/dist/lib/pages/settings.js +206 -0
- package/dist/lib/pages/terminal.js +364 -0
- package/dist/lib/pinned-views.js +1 -0
- package/dist/lib/plugins.js +3 -0
- package/dist/lib/quick-commands.js +1 -0
- package/dist/lib/rateLimiter.js +2 -0
- package/dist/lib/schedule-delay.js +9 -0
- package/dist/lib/scheduler-drawer.js +379 -0
- package/dist/lib/scheduler.js +1 -0
- package/dist/lib/security-config.js +2 -0
- package/dist/lib/session-access.js +1 -0
- package/dist/lib/session-windows.js +1 -0
- package/dist/lib/sessions-drawer.js +634 -0
- package/dist/lib/sessions-sidebar.js +1 -0
- package/dist/lib/settings.js +2 -0
- package/dist/lib/setup-features.js +1 -0
- package/dist/lib/setup-prompts.js +3 -0
- package/dist/lib/shared-layout.js +412 -0
- package/dist/lib/state-paths.js +1 -0
- package/dist/lib/terminal-config.js +1 -0
- package/dist/lib/theme-store.js +2 -0
- package/dist/lib/theme.js +28 -0
- package/dist/lib/themes/dark-cove.js +1 -0
- package/dist/lib/themes/ghostty.js +1 -0
- package/dist/lib/themes/index.js +1 -0
- package/dist/lib/themes/types.js +0 -0
- package/dist/lib/themes/vscode.js +1 -0
- package/dist/lib/themes/warm-clay.js +1 -0
- package/dist/lib/tmux-capture.js +5 -0
- package/dist/lib/tmux-control.js +2 -0
- package/dist/lib/tmux-panes.js +2 -0
- package/dist/lib/tmux-windows.js +4 -0
- package/dist/lib/watched-panes.js +1 -0
- package/dist/lib/window-history.js +1 -0
- package/dist/lib/window-labels.js +1 -0
- package/dist/lib/windows-drawer.js +364 -0
- package/dist/lib/ws-message.js +1 -0
- package/dist/sessions.js +2 -0
- package/package.json +62 -0
- package/scripts/fix-pty-perms.mjs +16 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
function d(){return`
|
|
2
|
+
.resizable-drawer {
|
|
3
|
+
min-width: min(280px, calc(100vw - 48px));
|
|
4
|
+
max-width: calc(100vw - 48px);
|
|
5
|
+
}
|
|
6
|
+
.drawer-resize-handle {
|
|
7
|
+
position: absolute; top: 0; left: -5px; width: 10px; height: 100%;
|
|
8
|
+
cursor: col-resize; z-index: 2; touch-action: none;
|
|
9
|
+
}
|
|
10
|
+
.drawer-resize-handle::after {
|
|
11
|
+
content: ""; position: absolute; top: 0; bottom: 0; left: 4px; width: 1px;
|
|
12
|
+
background: transparent; transition: background 0.15s, box-shadow 0.15s;
|
|
13
|
+
}
|
|
14
|
+
.drawer-resize-handle:hover::after,
|
|
15
|
+
.drawer-resize-handle.active::after {
|
|
16
|
+
background: var(--panel-accent);
|
|
17
|
+
box-shadow: 0 0 10px rgba(125, 211, 252, 0.45);
|
|
18
|
+
}
|
|
19
|
+
body.drawer-resizing { cursor: col-resize; user-select: none; }
|
|
20
|
+
body.drawer-resizing iframe { pointer-events: none; }`}function a(){return'<div class="drawer-resize-handle" title="Resize drawer"></div>'}function o(e,t,n){const i=JSON.stringify(e),r=JSON.stringify(t);return`
|
|
21
|
+
(function() {
|
|
22
|
+
const drawer = document.getElementById(${i});
|
|
23
|
+
if (!drawer) return;
|
|
24
|
+
const handle = drawer.querySelector('.drawer-resize-handle');
|
|
25
|
+
if (!handle) return;
|
|
26
|
+
|
|
27
|
+
const storageKey = ${r};
|
|
28
|
+
const defaultWidth = ${n};
|
|
29
|
+
const minWidth = 280;
|
|
30
|
+
|
|
31
|
+
function maxWidth() {
|
|
32
|
+
return Math.max(minWidth, window.innerWidth - 48);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clampWidth(width) {
|
|
36
|
+
return Math.min(Math.max(width, minWidth), maxWidth());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function applyWidth(width) {
|
|
40
|
+
drawer.style.width = clampWidth(width) + 'px';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readSavedWidth() {
|
|
44
|
+
try {
|
|
45
|
+
return Number(localStorage.getItem(storageKey));
|
|
46
|
+
} catch {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveWidth(width) {
|
|
52
|
+
try {
|
|
53
|
+
localStorage.setItem(storageKey, String(Math.round(width)));
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const saved = readSavedWidth();
|
|
58
|
+
applyWidth(Number.isFinite(saved) && saved > 0 ? saved : defaultWidth);
|
|
59
|
+
|
|
60
|
+
handle.addEventListener('pointerdown', (event) => {
|
|
61
|
+
if (event.button !== 0) return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
handle.classList.add('active');
|
|
64
|
+
document.body.classList.add('drawer-resizing');
|
|
65
|
+
drawer.style.transition = 'none';
|
|
66
|
+
handle.setPointerCapture?.(event.pointerId);
|
|
67
|
+
|
|
68
|
+
function resize(moveEvent) {
|
|
69
|
+
const nextWidth = clampWidth(window.innerWidth - moveEvent.clientX);
|
|
70
|
+
drawer.style.width = nextWidth + 'px';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function finish(upEvent) {
|
|
74
|
+
handle.classList.remove('active');
|
|
75
|
+
document.body.classList.remove('drawer-resizing');
|
|
76
|
+
drawer.style.transition = '';
|
|
77
|
+
handle.releasePointerCapture?.(upEvent.pointerId);
|
|
78
|
+
saveWidth(drawer.getBoundingClientRect().width);
|
|
79
|
+
handle.removeEventListener('pointermove', resize);
|
|
80
|
+
handle.removeEventListener('pointerup', finish);
|
|
81
|
+
handle.removeEventListener('pointercancel', finish);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
handle.addEventListener('pointermove', resize);
|
|
85
|
+
handle.addEventListener('pointerup', finish);
|
|
86
|
+
handle.addEventListener('pointercancel', finish);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
window.addEventListener('resize', () => {
|
|
90
|
+
const width = clampWidth(drawer.getBoundingClientRect().width || defaultWidth);
|
|
91
|
+
drawer.style.width = width + 'px';
|
|
92
|
+
});
|
|
93
|
+
}());`}function s(e,t,n){const i=JSON.stringify(e),r=JSON.stringify(t);return`
|
|
94
|
+
(function() {
|
|
95
|
+
const drawer = document.getElementById(${i});
|
|
96
|
+
if (!drawer) return;
|
|
97
|
+
const handle = drawer.querySelector('.drawer-resize-handle');
|
|
98
|
+
if (!handle) return;
|
|
99
|
+
|
|
100
|
+
const storageKey = ${r};
|
|
101
|
+
const defaultWidth = ${n};
|
|
102
|
+
const minWidth = 280;
|
|
103
|
+
|
|
104
|
+
function maxWidth() {
|
|
105
|
+
return Math.max(minWidth, window.innerWidth - 48);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clampWidth(width) {
|
|
109
|
+
return Math.min(Math.max(width, minWidth), maxWidth());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function applyWidth(width) {
|
|
113
|
+
drawer.style.width = clampWidth(width) + 'px';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readSavedWidth() {
|
|
117
|
+
try {
|
|
118
|
+
return Number(localStorage.getItem(storageKey));
|
|
119
|
+
} catch {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveWidth(width) {
|
|
125
|
+
try {
|
|
126
|
+
localStorage.setItem(storageKey, String(Math.round(width)));
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const saved = readSavedWidth();
|
|
131
|
+
applyWidth(Number.isFinite(saved) && saved > 0 ? saved : defaultWidth);
|
|
132
|
+
|
|
133
|
+
handle.addEventListener('pointerdown', (event) => {
|
|
134
|
+
if (event.button !== 0) return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
handle.classList.add('active');
|
|
137
|
+
document.body.classList.add('drawer-resizing');
|
|
138
|
+
drawer.style.transition = 'none';
|
|
139
|
+
handle.setPointerCapture?.(event.pointerId);
|
|
140
|
+
|
|
141
|
+
function resize(moveEvent) {
|
|
142
|
+
const nextWidth = clampWidth(moveEvent.clientX);
|
|
143
|
+
drawer.style.width = nextWidth + 'px';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function finish(upEvent) {
|
|
147
|
+
handle.classList.remove('active');
|
|
148
|
+
document.body.classList.remove('drawer-resizing');
|
|
149
|
+
drawer.style.transition = '';
|
|
150
|
+
handle.releasePointerCapture?.(upEvent.pointerId);
|
|
151
|
+
saveWidth(drawer.getBoundingClientRect().width);
|
|
152
|
+
handle.removeEventListener('pointermove', resize);
|
|
153
|
+
handle.removeEventListener('pointerup', finish);
|
|
154
|
+
handle.removeEventListener('pointercancel', finish);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
handle.addEventListener('pointermove', resize);
|
|
158
|
+
handle.addEventListener('pointerup', finish);
|
|
159
|
+
handle.addEventListener('pointercancel', finish);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
window.addEventListener('resize', () => {
|
|
163
|
+
const width = clampWidth(drawer.getBoundingClientRect().width || defaultWidth);
|
|
164
|
+
drawer.style.width = width + 'px';
|
|
165
|
+
});
|
|
166
|
+
}());`}export{d as drawerResizeCSS,a as drawerResizeHandleHTML,o as drawerResizeScript,s as drawerResizeScriptLeft};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
function w(r,e,t){return`(function() {
|
|
2
|
+
${e}
|
|
3
|
+
window.tmuxWebDrawers = window.tmuxWebDrawers || {};
|
|
4
|
+
window.tmuxWebDrawers[${JSON.stringify(r)}] = { close: ${t} };
|
|
5
|
+
})();`}function i(r){return`
|
|
6
|
+
for (const [id, drawer] of Object.entries(window.tmuxWebDrawers || {})) {
|
|
7
|
+
if (id !== ${JSON.stringify(r)} && drawer?.close) drawer.close();
|
|
8
|
+
}`}export{i as closeOtherDrawersExcept,w as wrapDrawerScript};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readdir as x,readFile as y}from"node:fs/promises";import{existsSync as p,mkdirSync as g}from"node:fs";import{spawn as b}from"node:child_process";import j from"node:http";import k from"node:os";import d from"node:path";import{getDataRoot as E,getExtensionDataDir as $,getPluginDir as T}from"./state-paths.js";import{readSettings as P}from"./settings.js";async function q(n){const o=d.join(n,"package.json");if(p(o))try{const t=JSON.parse(await y(o,"utf-8"));if(typeof t.name=="string"&&t.name)return t.name.startsWith("@")?t.name.split("/")[1]:t.name}catch{}return d.basename(n)}async function w(n){const o=d.join(n,"tmux-extension.json");if(!p(o))return null;try{const t=JSON.parse(await y(o,"utf-8"));return t.dir=n,t.id=await q(n),t}catch(t){return console.warn(`[extensions] Failed to load ${o}: ${t.message}`),null}}function _(n){const o=[T(),d.join(process.cwd(),"node_modules")];for(const t of o){const s=d.join(t,n);if(p(s))return s}return null}async function N(n){const o=new Set,t=[];function s(r){o.has(r.id)||(o.add(r.id),t.push(r))}if(p(n)){const r=await x(n,{withFileTypes:!0});for(const c of r){if(!c.isDirectory())continue;const e=await w(d.join(n,c.name));e&&s(e)}}const u=await P();for(const r of u.plugins??[]){const c=_(r);if(!c){console.warn(`[extensions] Plugin "${r}" not installed \u2014 run: tmux-web add ${r}`);continue}const e=await w(c);e&&s(e)}return t}function U(n,o){const t=d.join(k.tmpdir(),`tmux-web-ext-${o.id}.sock`),s=$(o.id),u=E();g(s,{recursive:!0}),o._socket=t;const[r,...c]=o.start.split(" "),e=b(r,c,{cwd:n,env:{...process.env,TMUX_WEB_DATA_ROOT:u,EXT_SOCKET:t,EXT_DATA_DIR:s},stdio:"pipe"}),a=`[ext:${o.id}]`;return e.stdout?.on("data",i=>process.stdout.write(`${a} ${i}`)),e.stderr?.on("data",i=>process.stderr.write(`${a} ${i}`)),e.on("exit",i=>{i!==0&&console.warn(`${a} exited with code ${i}`)}),e}function v(n,o,t,s,u){return new Promise((r,c)=>{const e=j.request({socketPath:n,path:t,method:o,headers:s},a=>{const i=[];a.on("data",l=>i.push(l)),a.on("end",()=>r({status:a.statusCode??200,contentType:a.headers["content-type"]??"application/json",body:Buffer.concat(i)}))});e.on("error",c),u?.length&&e.write(u),e.end()})}function D(n,o){const t=d.resolve(n,o),s=d.resolve(n);return t.startsWith(s+d.sep)?t:null}function W(n,o,t){for(const s of t){const u=s.id,r=d.join(s.dir,"dist","ui"),c=s._socket;n.get(`/ext/${u}/ui/:file{.+}`,async e=>{const a=e.req.param("file"),i=D(r,a);if(!i||!p(i))return e.notFound();const{readFile:l}=await import("node:fs/promises"),m=await l(i),f=d.extname(a),h={".html":"text/html",".js":"application/javascript",".css":"text/css",".json":"application/json"};return e.body(m,200,{"Content-Type":h[f]??"application/octet-stream"})}),c&&n.all(`/ext/${u}/api/*`,async e=>{const a=e.req.path.slice(`/ext/${u}/api`.length)+new URL(e.req.url).search,l=!["GET","HEAD","DELETE"].includes(e.req.method)?Buffer.from(await e.req.arrayBuffer()):void 0,m=Object.fromEntries([...e.req.raw.headers.entries()].filter(([h])=>h!=="host")),f=await v(c,e.req.method,a,m,l);return e.body(f.body.buffer.slice(f.body.byteOffset,f.body.byteOffset+f.body.byteLength),f.status,{"Content-Type":f.contentType})})}}export{N as loadExtensions,W as registerExtensionRoutes,D as resolveExtensionUiFile,U as spawnExtensionBackend};
|
package/dist/lib/html.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function t(e){return r(e)}export{t as escapeAttr,r as escapeHtml};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a={menu:"M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z",close:"M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7a1 1 0 1 0-1.41 1.41L10.59 12l-4.89 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 1 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z",search:"M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",refresh:"M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 8 8h-2a6 6 0 1 1-6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z",add:"M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",edit:"M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM5.92 19H5v-.92l9.06-9.06.92.92L5.92 19zm13.66-12.83a1 1 0 0 0 0-1.41l-1.34-1.34a1 1 0 0 0-1.41 0l-1.05 1.05 2.75 2.75 1.05-1.05z",delete:"M15 4V3H9v1H4v2h1.5l.81 13.05A2 2 0 0 0 8.3 21h7.4a2 2 0 0 0 1.99-1.95L18.5 6H20V4h-5zm-5.5 2h5l-.75 12h-3.5L9.5 6z",check:"M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z",sessions:"M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z",notes:"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 2 5 5h-5V4zM6 20V4h5v7h7v9H6z",schedule:"M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 16H5V10h14v10zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2zm-8 4H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z",agents:"M12 2a5 5 0 1 0 0 10 5 5 0 0 0 0-10zm0 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm-7 13v-2a5 5 0 0 1 5-5h4a5 5 0 0 1 5 5v2h2v-2a7 7 0 0 0-7-7H10a7 7 0 0 0-7 7v2h2z",history:"M13 3a9 9 0 0 0-9 9H1l4 4 4-4H6a7 7 0 1 1 7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.96 8.96 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.25 2.55.75-1.23-3.5-2.08V8H12z","quick-commands":"M13 3 4 14h7l-1 7 9-11h-7l1-7z",windows:"M4 6h16v2H4V6zm0 5h10v2H4v-2zm0 5h16v2H4v-2z",terminal:"M4 17.6 6.4 20l8-8-8-8L4 6.4 11.6 12 4 17.6zM20 18h-8v2h8v-2z",microphone:"M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z",keyboard:"M20 5H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z",settings:"M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6a3.6 3.6 0 1 1 0-7.2 3.6 3.6 0 0 1 0 7.2z",pin:"M16 12V4h1V2H7v2h1v8l-4 4v2h5.2v6h1.6v-6H20v-2l-4-4z","pin-outline":"M16 12V4h1V2H7v2h1v8l-4 4v2h5.2v6h1.6v-6H20v-2l-4-4zm-4.8 7.2V11l2-2V4h1.6v5l2 2v5.2H11.2z",folder:"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",worktree:"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",copy:"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z",export:"M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",chevronRight:"M9.29 6.71a1 1 0 0 0 0 1.41L13.17 12l-3.88 3.88a1 1 0 1 0 1.41 1.41l4.59-4.59a1 1 0 0 0 0-1.41L10.7 6.7a1 1 0 0 0-1.41.01z",lightning:"M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.33.51-.89.73-1.27L13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15c-.22.46-.58 1.18-.78 1.58L11 21z","git-branch":"M13.5 7a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0zm0 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0zM11 9.5v5.1a4.03 4.03 0 0 0-3-.1V9.4a4.03 4.03 0 0 0 3 .1z",external:"M14 3h7v7h-2V6l-9.29 9.29-1.41-1.41L17.59 4.5H14V3zm4 16H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2v7z",play:"M8 5v14l11-7L8 5z",stop:"M6 6h12v12H6V6z",warning:"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z",info:"M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z",spinner:"M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8z",dots:"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"};function c(h){return a[h]??a.menu}function l(h,v=""){return`<svg viewBox="0 0 24 24" fill="currentColor"${v?" "+v:""}><path d="${c(h)}"/></svg>`}function n(h){return`<path d="${c(h)}"/>`}const m={};function H(h,v){const z=m[h];return z?l(z):v?.trim().startsWith("<svg")?v:`<span style="font-size:18px;line-height:1">${v??""}</span>`}export{H as extIcon,l as icon,c as iconPath,n as iconPathString};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as c}from"node:crypto";import{mkdir as h,writeFile as w}from"node:fs/promises";import r from"node:path";import{getUploadsRoot as x}from"./state-paths.js";const d={"image/png":".png","image/jpeg":".jpg","image/webp":".webp","image/gif":".gif"};class m extends Error{status;constructor(t,n){super(t),this.status=n,this.name="ImageUploadError"}}function I(){const e=process.env.TMUX_WEB_MAX_IMAGE_UPLOAD_BYTES;if(!e)return 10*1024*1024;const t=parseInt(e,10);return Number.isFinite(t)&&t>0?t:10*1024*1024}const f=I();function _(e){return e.length>=8&&e[0]===137&&e[1]===80&&e[2]===78&&e[3]===71?"image/png":e.length>=3&&e[0]===255&&e[1]===216&&e[2]===255?"image/jpeg":e.length>=6&&e.toString("ascii",0,6)==="GIF87a"||e.length>=6&&e.toString("ascii",0,6)==="GIF89a"?"image/gif":e.length>=12&&e.toString("ascii",0,4)==="RIFF"&&e.toString("ascii",8,12)==="WEBP"?"image/webp":null}function u(e){return r.basename(e).replace(/[^a-zA-Z0-9._-]/g,"_")||"upload"}function E(){const e=new Date,t=e.getFullYear(),n=String(e.getMonth()+1).padStart(2,"0"),g=String(e.getDate()).padStart(2,"0");return`${t}-${n}-${g}`}async function A(e,t,n){if(e.length===0)throw new m("empty file",400);if(e.length>f)throw new m(`file exceeds ${f} bytes`,413);const g=E(),l=r.join(x(),g);await h(l,{recursive:!0,mode:448});let o;const a=t?.split(";")[0]?.trim().toLowerCase();if(a&&d[a]){const i=_(e);if(i&&i!==a)throw new m("file content does not match declared type",400);const s=d[a];o=`${c()}${s}`}else if(n){const i=u(n),s=r.extname(i);o=`${r.basename(i,s)}-${c()}${s}`}else o=c();const p=r.join(l,o);return await w(p,e,{mode:384}),{path:p}}export{m as ImageUploadError,A as saveUploadedImage};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{readFileSync as p,existsSync as u}from"node:fs";import{writeFile as d,chmod as $,mkdir as v}from"node:fs/promises";import l from"node:path";import{getDataRoot as g}from"./state-paths.js";function h(){return l.join(g(),".env")}function y(){const n=h();if(!u(n))return;let o;try{o=p(n,"utf-8")}catch{return}for(const e of o.split(`
|
|
2
|
+
`)){const s=e.trim();if(!s||s.startsWith("#"))continue;const r=s.indexOf("=");if(r<=0)continue;const c=s.slice(0,r).trim();if(!c||process.env[c]!==void 0)continue;let t=s.slice(r+1).trim();(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1)),process.env[c]=t}}async function F(n,o){const e=h(),s=`${n}=${o}
|
|
3
|
+
`;let r="";u(e)&&(r=p(e,"utf-8"));const c=r.length>0?r.split(`
|
|
4
|
+
`):[],t=new RegExp(`^\\s*${n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\s*=`);let f=!1;const i=[];for(const a of c){if(t.test(a)){f||(i.push(`${n}=${o}`),f=!0);continue}i.push(a)}f||(i.length>0&&i[i.length-1]!==""&&i.push(""),i.push(`${n}=${o}`));const m=i.join(`
|
|
5
|
+
`).replace(/\n*$/,`
|
|
6
|
+
`);await v(l.dirname(e),{recursive:!0}),await d(e,m,{mode:384});try{await $(e,384)}catch{}return process.env[n]=o,e}export{h as getEnvFilePath,y as loadDotEnv,F as upsertEnvVar};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import{icon as e}from"./icons.js";function n(){return`
|
|
2
|
+
#mobile-toolbar { display: none; }
|
|
3
|
+
@media (max-width: 560px) {
|
|
4
|
+
#mobile-toolbar {
|
|
5
|
+
display: flex; justify-content: space-around; align-items: center;
|
|
6
|
+
flex-shrink: 0; gap: 8px;
|
|
7
|
+
padding: 6px 12px;
|
|
8
|
+
padding-bottom: calc(6px + env(safe-area-inset-bottom));
|
|
9
|
+
background: var(--panel-bg);
|
|
10
|
+
border-top: 1px solid var(--panel-border);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
#mobile-toolbar button {
|
|
14
|
+
display: flex; align-items: center; justify-content: center;
|
|
15
|
+
flex: 1; min-height: 44px;
|
|
16
|
+
background: none; border: none; color: var(--panel-muted);
|
|
17
|
+
cursor: pointer; border-radius: 8px; transition: color 0.15s, background 0.15s;
|
|
18
|
+
}
|
|
19
|
+
#mobile-toolbar button:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
|
|
20
|
+
#mobile-toolbar button:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
|
|
21
|
+
#mobile-toolbar button svg { width: 22px; height: 22px; fill: currentColor; }
|
|
22
|
+
#mobile-toolbar button.listening {
|
|
23
|
+
color: var(--panel-success);
|
|
24
|
+
animation: tmux-mic-pulse 1.2s ease-in-out infinite;
|
|
25
|
+
}
|
|
26
|
+
@keyframes tmux-mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
|
|
27
|
+
|
|
28
|
+
#type-backdrop {
|
|
29
|
+
position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 1001;
|
|
30
|
+
opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
|
|
31
|
+
}
|
|
32
|
+
#type-backdrop.open { opacity: 1; pointer-events: auto; }
|
|
33
|
+
/* Anchored near the top so the on-screen keyboard never covers the textarea. */
|
|
34
|
+
#type-modal {
|
|
35
|
+
position: fixed; left: 50%; top: 12px; transform: translate(-50%, -130%);
|
|
36
|
+
width: min(100vw - 24px, 560px); z-index: 1002;
|
|
37
|
+
background: var(--panel-bg); border: 1px solid var(--panel-border);
|
|
38
|
+
border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
39
|
+
display: flex; flex-direction: column; gap: 10px; padding: 14px;
|
|
40
|
+
transition: transform 0.25s ease;
|
|
41
|
+
}
|
|
42
|
+
#type-modal.open { transform: translate(-50%, 0); }
|
|
43
|
+
#type-modal .type-modal-header {
|
|
44
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
45
|
+
font-family: var(--font-mono); font-size: var(--text-xs); color: var(--panel-muted);
|
|
46
|
+
}
|
|
47
|
+
#type-modal #type-close {
|
|
48
|
+
display: flex; align-items: center; justify-content: center;
|
|
49
|
+
background: none; border: none; color: var(--panel-muted);
|
|
50
|
+
font-size: var(--text-xl); line-height: 1; cursor: pointer;
|
|
51
|
+
min-width: 44px; min-height: 44px; padding: 8px; border-radius: 8px;
|
|
52
|
+
transition: color 0.15s, background 0.15s;
|
|
53
|
+
}
|
|
54
|
+
#type-modal #type-close:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
|
|
55
|
+
#type-modal #type-close:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
|
|
56
|
+
#type-input {
|
|
57
|
+
width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
|
|
58
|
+
background: var(--terminal-bg, rgba(0,0,0,0.28)); color: var(--page-fg);
|
|
59
|
+
border: 1px solid var(--panel-border); border-radius: 6px; padding: 10px;
|
|
60
|
+
font-family: var(--font-mono); font-size: var(--text-base); line-height: 1.4;
|
|
61
|
+
}
|
|
62
|
+
#type-input:focus { outline: none; border-color: var(--panel-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--panel-accent) 8%, transparent); }
|
|
63
|
+
#type-status {
|
|
64
|
+
min-height: 14px; font-size: var(--text-xs); color: var(--panel-muted);
|
|
65
|
+
font-family: var(--font-mono);
|
|
66
|
+
}
|
|
67
|
+
#type-status.error { color: #cc6666; }
|
|
68
|
+
#type-modal .type-modal-footer { display: flex; gap: 8px; }
|
|
69
|
+
#type-modal .type-modal-footer button {
|
|
70
|
+
flex: 1; min-height: 44px; cursor: pointer; border-radius: 6px;
|
|
71
|
+
font-family: var(--font-mono); font-size: var(--text-sm);
|
|
72
|
+
border: 1px solid var(--panel-border); background: none; color: var(--page-fg);
|
|
73
|
+
transition: background 0.15s, color 0.15s;
|
|
74
|
+
}
|
|
75
|
+
#type-modal .type-modal-footer button:hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
|
|
76
|
+
#type-modal .type-modal-footer button:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
|
|
77
|
+
.type-modal-modifiers {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
80
|
+
gap: 4px;
|
|
81
|
+
padding: 3px;
|
|
82
|
+
background: var(--terminal-bg, rgba(0,0,0,0.28));
|
|
83
|
+
border: 1px solid var(--panel-border);
|
|
84
|
+
border-radius: 7px;
|
|
85
|
+
}
|
|
86
|
+
.type-modal-modifiers label {
|
|
87
|
+
position: relative;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
.type-modal-modifiers input {
|
|
92
|
+
position: absolute;
|
|
93
|
+
opacity: 0;
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
}
|
|
96
|
+
.type-modal-modifier-label {
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
min-height: 44px;
|
|
101
|
+
border: 1px solid transparent;
|
|
102
|
+
border-radius: 5px;
|
|
103
|
+
color: var(--panel-muted);
|
|
104
|
+
font-family: var(--font-mono);
|
|
105
|
+
font-size: var(--text-xs);
|
|
106
|
+
line-height: 1;
|
|
107
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
108
|
+
}
|
|
109
|
+
.type-modal-modifiers label:hover .type-modal-modifier-label {
|
|
110
|
+
color: var(--panel-accent);
|
|
111
|
+
background: color-mix(in srgb, var(--panel-accent) 8%, transparent);
|
|
112
|
+
}
|
|
113
|
+
.type-modal-modifiers input:focus-visible + .type-modal-modifier-label {
|
|
114
|
+
outline: 1px solid var(--panel-accent);
|
|
115
|
+
outline-offset: 1px;
|
|
116
|
+
}
|
|
117
|
+
.type-modal-modifiers label:has(input:checked) .type-modal-modifier-label {
|
|
118
|
+
border-color: var(--panel-accent);
|
|
119
|
+
background: color-mix(in srgb, var(--panel-accent) 10%, transparent);
|
|
120
|
+
color: var(--panel-accent);
|
|
121
|
+
}
|
|
122
|
+
.type-modal-modifiers label:has(input[value="Ctrl"]:checked) .type-modal-modifier-label {
|
|
123
|
+
border-color: var(--panel-success);
|
|
124
|
+
background: color-mix(in srgb, var(--panel-success) 10%, transparent);
|
|
125
|
+
color: var(--panel-success);
|
|
126
|
+
}
|
|
127
|
+
#type-modal .type-modal-footer #type-send-enter {
|
|
128
|
+
border-color: var(--panel-success); color: var(--panel-success);
|
|
129
|
+
}`}function a(){return`
|
|
130
|
+
<div id="mobile-toolbar">
|
|
131
|
+
<button id="mic-toggle" type="button" title="Voice input" aria-label="Voice input">
|
|
132
|
+
${e("microphone")}
|
|
133
|
+
</button>
|
|
134
|
+
<button id="type-toggle" type="button" title="Type to send" aria-label="Type to send">
|
|
135
|
+
${e("keyboard")}
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
<div id="type-backdrop"></div>
|
|
139
|
+
<div id="type-modal">
|
|
140
|
+
<div class="type-modal-header">
|
|
141
|
+
<span>Send to pane</span>
|
|
142
|
+
<button id="type-close" type="button" aria-label="Close">×</button>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="type-modal-modifiers" role="group" aria-label="Special key">
|
|
145
|
+
<label><input type="radio" name="type-modifier" value="None" checked /><span class="type-modal-modifier-label">None</span></label>
|
|
146
|
+
<label><input type="radio" name="type-modifier" value="Esc" /><span class="type-modal-modifier-label">Esc</span></label>
|
|
147
|
+
<label><input type="radio" name="type-modifier" value="Tab" /><span class="type-modal-modifier-label">Tab</span></label>
|
|
148
|
+
<label><input type="radio" name="type-modifier" value="Ctrl" /><span class="type-modal-modifier-label">Ctrl</span></label>
|
|
149
|
+
</div>
|
|
150
|
+
<textarea id="type-input" placeholder="Type a command or tap the mic\u2026" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"></textarea>
|
|
151
|
+
<div id="type-status"></div>
|
|
152
|
+
<div class="type-modal-footer">
|
|
153
|
+
<button id="type-send" type="button">Send</button>
|
|
154
|
+
<button id="type-send-enter" type="button">Send ⏎</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>`}function i(t){return`(function() {
|
|
157
|
+
const micBtn = document.getElementById('mic-toggle');
|
|
158
|
+
const typeBtn = document.getElementById('type-toggle');
|
|
159
|
+
const modal = document.getElementById('type-modal');
|
|
160
|
+
const backdrop = document.getElementById('type-backdrop');
|
|
161
|
+
const input = document.getElementById('type-input');
|
|
162
|
+
const status = document.getElementById('type-status');
|
|
163
|
+
const closeBtn = document.getElementById('type-close');
|
|
164
|
+
const sendBtn = document.getElementById('type-send');
|
|
165
|
+
const sendEnterBtn = document.getElementById('type-send-enter');
|
|
166
|
+
const keyInputs = Array.from(document.querySelectorAll('input[name="type-modifier"]'));
|
|
167
|
+
|
|
168
|
+
function setStatus(msg, isError) {
|
|
169
|
+
status.textContent = msg || '';
|
|
170
|
+
status.classList.toggle('error', !!isError);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function openModal() {
|
|
174
|
+
// Close any open header drawers so panels don't stack.
|
|
175
|
+
for (const drawer of Object.values(window.tmuxWebDrawers || {})) {
|
|
176
|
+
if (drawer && drawer.close) drawer.close();
|
|
177
|
+
}
|
|
178
|
+
modal.classList.add('open');
|
|
179
|
+
backdrop.classList.add('open');
|
|
180
|
+
setTimeout(() => input.focus(), 50);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function closeModal() {
|
|
184
|
+
modal.classList.remove('open');
|
|
185
|
+
backdrop.classList.remove('open');
|
|
186
|
+
stopRecognition();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getActiveModifier() {
|
|
190
|
+
const active = keyInputs.find((candidate) => candidate.checked);
|
|
191
|
+
return active ? active.value : 'None';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function toControlChar(value) {
|
|
195
|
+
const code = value.charCodeAt(0);
|
|
196
|
+
return String.fromCharCode(code & 0x1f);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildPayload(text, withEnter) {
|
|
200
|
+
const modifier = getActiveModifier();
|
|
201
|
+
let payload = text;
|
|
202
|
+
|
|
203
|
+
if (modifier === 'Esc') {
|
|
204
|
+
payload = '\\u001b' + text;
|
|
205
|
+
} else if (modifier === 'Tab') {
|
|
206
|
+
payload = '\\t' + text;
|
|
207
|
+
} else if (modifier === 'Ctrl') {
|
|
208
|
+
if (!text) {
|
|
209
|
+
setStatus('Ctrl needs a key', true);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
payload = toControlChar(text[0]) + text.slice(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return withEnter ? payload + '\\r' : payload;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function send(withEnter) {
|
|
219
|
+
const text = input.value;
|
|
220
|
+
if (window.tmuxWeb && window.tmuxWeb.sendInput) {
|
|
221
|
+
const payload = buildPayload(text, withEnter);
|
|
222
|
+
if (payload === null) return;
|
|
223
|
+
const hasText = !!text;
|
|
224
|
+
const modifier = getActiveModifier();
|
|
225
|
+
if (hasText || modifier !== 'None') {
|
|
226
|
+
window.tmuxWeb.sendInput(payload);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
input.value = '';
|
|
230
|
+
setStatus('');
|
|
231
|
+
closeModal();
|
|
232
|
+
if (window.tmuxWeb && window.tmuxWeb.focusTerminal) window.tmuxWeb.focusTerminal();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
typeBtn.addEventListener('click', () => {
|
|
236
|
+
if (modal.classList.contains('open')) closeModal();
|
|
237
|
+
else { setStatus(''); openModal(); }
|
|
238
|
+
});
|
|
239
|
+
closeBtn.addEventListener('click', closeModal);
|
|
240
|
+
backdrop.addEventListener('click', closeModal);
|
|
241
|
+
sendBtn.addEventListener('click', () => send(false));
|
|
242
|
+
sendEnterBtn.addEventListener('click', () => send(true));
|
|
243
|
+
|
|
244
|
+
// \u2500\u2500 Speech-to-text (browser Web Speech API) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
245
|
+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
246
|
+
let recognition = null;
|
|
247
|
+
let listening = false;
|
|
248
|
+
|
|
249
|
+
if (!SR) {
|
|
250
|
+
// Unsupported browser \u2014 hide the mic entirely.
|
|
251
|
+
micBtn.style.display = 'none';
|
|
252
|
+
} else {
|
|
253
|
+
micBtn.addEventListener('click', () => {
|
|
254
|
+
if (listening) { stopRecognition(); return; }
|
|
255
|
+
startRecognition();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function startRecognition() {
|
|
260
|
+
if (!SR) return;
|
|
261
|
+
recognition = new SR();
|
|
262
|
+
recognition.lang = navigator.language || 'en-US';
|
|
263
|
+
recognition.interimResults = true;
|
|
264
|
+
recognition.continuous = false;
|
|
265
|
+
const baseText = input.value ? input.value + ' ' : '';
|
|
266
|
+
|
|
267
|
+
recognition.onstart = () => {
|
|
268
|
+
listening = true;
|
|
269
|
+
micBtn.classList.add('listening');
|
|
270
|
+
openModal();
|
|
271
|
+
setStatus('Listening\u2026');
|
|
272
|
+
};
|
|
273
|
+
recognition.onresult = (event) => {
|
|
274
|
+
let transcript = '';
|
|
275
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
276
|
+
transcript += event.results[i][0].transcript;
|
|
277
|
+
}
|
|
278
|
+
input.value = baseText + transcript;
|
|
279
|
+
};
|
|
280
|
+
recognition.onerror = (event) => {
|
|
281
|
+
const code = event && event.error;
|
|
282
|
+
if (code === 'not-allowed' || code === 'service-not-allowed') {
|
|
283
|
+
setStatus('Microphone permission denied', true);
|
|
284
|
+
} else if (code === 'no-speech') {
|
|
285
|
+
setStatus('No speech detected', true);
|
|
286
|
+
} else {
|
|
287
|
+
setStatus('Voice input error', true);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
recognition.onend = () => {
|
|
291
|
+
listening = false;
|
|
292
|
+
micBtn.classList.remove('listening');
|
|
293
|
+
if (!status.textContent || status.textContent === 'Listening\u2026') {
|
|
294
|
+
setStatus(input.value ? 'Review and send' : '');
|
|
295
|
+
}
|
|
296
|
+
recognition = null;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
recognition.start();
|
|
301
|
+
} catch (e) {
|
|
302
|
+
setStatus('Voice input unavailable', true);
|
|
303
|
+
listening = false;
|
|
304
|
+
micBtn.classList.remove('listening');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function stopRecognition() {
|
|
309
|
+
if (recognition) {
|
|
310
|
+
try { recognition.stop(); } catch (e) { /* ignore */ }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
})();`}export{n as mobileToolbarCSS,a as mobileToolbarHTML,i as mobileToolbarScript};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function n(){return`
|
|
2
|
+
async function loadNote(scope) {
|
|
3
|
+
try {
|
|
4
|
+
const res = await fetch('/api/notes/' + encodeURIComponent(scope));
|
|
5
|
+
if (!res.ok) return null;
|
|
6
|
+
return res.json();
|
|
7
|
+
} catch { return null; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function saveNote(scope, content) {
|
|
11
|
+
try {
|
|
12
|
+
await fetch('/api/notes/' + encodeURIComponent(scope), {
|
|
13
|
+
method: 'PUT',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ content }),
|
|
16
|
+
});
|
|
17
|
+
} catch {}
|
|
18
|
+
}`}export{n as notesDbScript};
|