@phenx-inc/ctlsurf 0.12.1 → 0.13.4

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 (41) hide show
  1. package/LICENSE +22 -0
  2. package/out/headless/index.mjs +33 -36
  3. package/out/headless/index.mjs.map +3 -3
  4. package/out/main/index.js +47 -40
  5. package/out/preload/index.js +1 -0
  6. package/out/renderer/assets/{cssMode-D7r66wvO.js → cssMode-CAEObGNt.js} +3 -3
  7. package/out/renderer/assets/{freemarker2-jSWJIjPG.js → freemarker2-D63hnnDq.js} +1 -1
  8. package/out/renderer/assets/{handlebars-BnFyz5RH.js → handlebars-UYo_IYpi.js} +1 -1
  9. package/out/renderer/assets/{html-Bl5UFavp.js → html-2dvYDi3g.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-D4VByBT6.js → htmlMode-B6fxDDPF.js} +3 -3
  11. package/out/renderer/assets/{index-Cc_TqOCL.css → index-BEekQe3-.css} +232 -8
  12. package/out/renderer/assets/{index-DxhW1emk.js → index-CMb4AqkZ.js} +786 -473
  13. package/out/renderer/assets/{javascript-CMN1OmwG.js → javascript-8RyFDDdk.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-CJ5AYSPV.js → jsonMode-CqspkWCb.js} +3 -3
  15. package/out/renderer/assets/{liquid-BGMIDTEm.js → liquid-DaEnAsYY.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-DGifVYB5.js → lspLanguageFeatures-Czkp5sZb.js} +1 -1
  17. package/out/renderer/assets/{mdx-B2Z7R10Q.js → mdx-Q-qSZsFg.js} +1 -1
  18. package/out/renderer/assets/{python-LMDMSSdn.js → python-Sw0S0BJV.js} +1 -1
  19. package/out/renderer/assets/{razor-BZSrziPu.js → razor-C7zQmabz.js} +1 -1
  20. package/out/renderer/assets/{tsMode-DF9m6_3n.js → tsMode-BWdOxk6v.js} +1 -1
  21. package/out/renderer/assets/{typescript-rZeCMf0a.js → typescript-BrdIiKZf.js} +1 -1
  22. package/out/renderer/assets/{xml-fNSZ-7so.js → xml-DbKLNPOq.js} +1 -1
  23. package/out/renderer/assets/{yaml-1wxIuvzh.js → yaml-ehupJBsZ.js} +1 -1
  24. package/out/renderer/index.html +2 -2
  25. package/package.json +2 -2
  26. package/src/main/bridge.ts +3 -2
  27. package/src/main/index.ts +15 -0
  28. package/src/main/orchestrator.ts +1 -1
  29. package/src/main/pty.ts +2 -5
  30. package/src/main/ticketStore.ts +28 -21
  31. package/src/main/timeTracker.ts +4 -1
  32. package/src/main/transcripts.ts +3 -2
  33. package/src/preload/index.ts +2 -0
  34. package/src/renderer/App.tsx +15 -2
  35. package/src/renderer/components/CtlsurfPanel.tsx +169 -35
  36. package/src/renderer/components/SettingsDialog.tsx +13 -0
  37. package/src/renderer/components/SpeakControls.tsx +5 -0
  38. package/src/renderer/components/TerminalPanel.tsx +155 -48
  39. package/src/renderer/components/TicketPanel.tsx +80 -24
  40. package/src/renderer/lib/speech.ts +2 -1
  41. package/src/renderer/styles.css +232 -8
@@ -1,4 +1,4 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./cssMode-D7r66wvO.js","./lspLanguageFeatures-DGifVYB5.js","./htmlMode-D4VByBT6.js","./jsonMode-CJ5AYSPV.js","./javascript-CMN1OmwG.js","./typescript-rZeCMf0a.js"])))=>i.map(i=>d[i]);
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./cssMode-CAEObGNt.js","./lspLanguageFeatures-Czkp5sZb.js","./htmlMode-B6fxDDPF.js","./jsonMode-CqspkWCb.js","./javascript-8RyFDDdk.js","./typescript-BrdIiKZf.js"])))=>i.map(i=>d[i]);
2
2
  function getDefaultExportFromCjs(x) {
3
3
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
4
4
  }
@@ -18818,35 +18818,416 @@ function convertToEmailTable(raw) {
18818
18818
  cols: rows[0].length
18819
18819
  };
18820
18820
  }
18821
- function copySelectionAsEmailTable(terminal) {
18822
- const result = convertToEmailTable(terminal.getSelection());
18823
- if (result) {
18824
- window.worker.copyEmailTable(result.html, result.text);
18821
+ const scriptRel = /* @__PURE__ */ (function detectScriptRel() {
18822
+ const relList = typeof document !== "undefined" && document.createElement("link").relList;
18823
+ return relList && relList.supports && relList.supports("modulepreload") ? "modulepreload" : "preload";
18824
+ })();
18825
+ const assetsURL = function(dep, importerUrl) {
18826
+ return new URL(dep, importerUrl).href;
18827
+ };
18828
+ const seen = {};
18829
+ const __vitePreload = function preload(baseModule, deps, importerUrl) {
18830
+ let promise = Promise.resolve();
18831
+ if (deps && deps.length > 0) {
18832
+ let allSettled2 = function(promises) {
18833
+ return Promise.all(
18834
+ promises.map(
18835
+ (p) => Promise.resolve(p).then(
18836
+ (value) => ({ status: "fulfilled", value }),
18837
+ (reason) => ({ status: "rejected", reason })
18838
+ )
18839
+ )
18840
+ );
18841
+ };
18842
+ const links = document.getElementsByTagName("link");
18843
+ const cspNonceMeta = document.querySelector(
18844
+ "meta[property=csp-nonce]"
18845
+ );
18846
+ const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
18847
+ promise = allSettled2(
18848
+ deps.map((dep) => {
18849
+ dep = assetsURL(dep, importerUrl);
18850
+ if (dep in seen) return;
18851
+ seen[dep] = true;
18852
+ const isCss = dep.endsWith(".css");
18853
+ const cssSelector = isCss ? '[rel="stylesheet"]' : "";
18854
+ const isBaseRelative = !!importerUrl;
18855
+ if (isBaseRelative) {
18856
+ for (let i2 = links.length - 1; i2 >= 0; i2--) {
18857
+ const link22 = links[i2];
18858
+ if (link22.href === dep && (!isCss || link22.rel === "stylesheet")) {
18859
+ return;
18860
+ }
18861
+ }
18862
+ } else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
18863
+ return;
18864
+ }
18865
+ const link2 = document.createElement("link");
18866
+ link2.rel = isCss ? "stylesheet" : scriptRel;
18867
+ if (!isCss) {
18868
+ link2.as = "script";
18869
+ }
18870
+ link2.crossOrigin = "";
18871
+ link2.href = dep;
18872
+ if (cspNonce) {
18873
+ link2.setAttribute("nonce", cspNonce);
18874
+ }
18875
+ document.head.appendChild(link2);
18876
+ if (isCss) {
18877
+ return new Promise((res, rej) => {
18878
+ link2.addEventListener("load", res);
18879
+ link2.addEventListener(
18880
+ "error",
18881
+ () => rej(new Error(`Unable to preload CSS for ${dep}`))
18882
+ );
18883
+ });
18884
+ }
18885
+ })
18886
+ );
18825
18887
  }
18826
- window.dispatchEvent(
18827
- new CustomEvent("email-table-result", {
18828
- detail: result ? { ok: true, message: `Copied ${result.rows}×${result.cols} table to clipboard` } : { ok: false, message: "No table found in selection" }
18829
- })
18830
- );
18888
+ function handlePreloadError(err) {
18889
+ const e = new Event("vite:preloadError", {
18890
+ cancelable: true
18891
+ });
18892
+ e.payload = err;
18893
+ window.dispatchEvent(e);
18894
+ if (!e.defaultPrevented) {
18895
+ throw err;
18896
+ }
18897
+ }
18898
+ return promise.then((res) => {
18899
+ for (const item of res || []) {
18900
+ if (item.status !== "rejected") continue;
18901
+ handlePreloadError(item.reason);
18902
+ }
18903
+ return baseModule().catch(handlePreloadError);
18904
+ });
18905
+ };
18906
+ const ENGINE_KEY = "ctlsurf.tts.engine";
18907
+ const VOICE_KEY = "ctlsurf.tts.voiceURI";
18908
+ const RATE_KEY = "ctlsurf.tts.rate";
18909
+ const MAX_SPEAK_CHARS = 1600;
18910
+ const MAX_CHUNK_CHARS = 280;
18911
+ function cleanForSpeech(input) {
18912
+ let t = input;
18913
+ t = t.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_m, body) => {
18914
+ const lines = body.replace(/\n+$/, "").split("\n").filter((l2) => l2.trim().length).length;
18915
+ return lines > 0 ? ` (code block, ${lines} ${lines === 1 ? "line" : "lines"}) ` : " (code) ";
18916
+ });
18917
+ t = t.replace(/```/g, " (code) ");
18918
+ t = t.replace(/`([^`]+)`/g, "$1");
18919
+ t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
18920
+ t = t.replace(/https?:\/\/\S+/g, "link");
18921
+ t = t.replace(/(?:[\w.-]*\/){2,}([\w.-]+)/g, "$1");
18922
+ t = t.replace(/[*_#>]+/g, " ");
18923
+ t = t.replace(/^\s*[-•]\s+/gm, ", ");
18924
+ t = t.replace(/<[^>]+>/g, " ");
18925
+ t = t.replace(/\s+/g, " ").trim();
18926
+ if (t.length > MAX_SPEAK_CHARS) {
18927
+ t = t.slice(0, MAX_SPEAK_CHARS).replace(/\s+\S*$/, "") + "…";
18928
+ }
18929
+ return t;
18930
+ }
18931
+ function splitIntoChunks(text2) {
18932
+ if (text2.length <= MAX_CHUNK_CHARS) return text2 ? [text2] : [];
18933
+ const sentences = text2.match(/[^.!?]+[.!?]+|\S[^.!?]*$/g) || [text2];
18934
+ const chunks = [];
18935
+ let buf = "";
18936
+ for (const s of sentences) {
18937
+ if ((buf + s).length > MAX_CHUNK_CHARS && buf) {
18938
+ chunks.push(buf.trim());
18939
+ buf = "";
18940
+ }
18941
+ if (s.length > MAX_CHUNK_CHARS) {
18942
+ for (let i2 = 0; i2 < s.length; i2 += MAX_CHUNK_CHARS) chunks.push(s.slice(i2, i2 + MAX_CHUNK_CHARS).trim());
18943
+ } else {
18944
+ buf += s;
18945
+ }
18946
+ }
18947
+ if (buf.trim()) chunks.push(buf.trim());
18948
+ return chunks;
18831
18949
  }
18832
- function openExternal(uri) {
18833
- const win = window.open();
18834
- if (win) {
18950
+ function getEngine() {
18951
+ try {
18952
+ return localStorage.getItem(ENGINE_KEY) === "web" ? "web" : "neural";
18953
+ } catch {
18954
+ return "neural";
18955
+ }
18956
+ }
18957
+ function setEngine(id) {
18958
+ try {
18959
+ localStorage.setItem(ENGINE_KEY, id);
18960
+ } catch {
18961
+ }
18962
+ }
18963
+ function getVoiceURI() {
18964
+ try {
18965
+ return localStorage.getItem(VOICE_KEY) || null;
18966
+ } catch {
18967
+ return null;
18968
+ }
18969
+ }
18970
+ function setVoiceURI(uri) {
18971
+ try {
18972
+ uri ? localStorage.setItem(VOICE_KEY, uri) : localStorage.removeItem(VOICE_KEY);
18973
+ } catch {
18974
+ }
18975
+ }
18976
+ function getRate() {
18977
+ try {
18978
+ const n2 = Number(localStorage.getItem(RATE_KEY));
18979
+ return Number.isFinite(n2) && n2 >= 0.5 && n2 <= 2 ? n2 : 1;
18980
+ } catch {
18981
+ return 1;
18982
+ }
18983
+ }
18984
+ function setRate(rate) {
18985
+ try {
18986
+ localStorage.setItem(RATE_KEY, String(rate));
18987
+ } catch {
18988
+ }
18989
+ }
18990
+ function listWebVoices() {
18991
+ if (typeof speechSynthesis === "undefined") return [];
18992
+ return speechSynthesis.getVoices();
18993
+ }
18994
+ const NEURAL_MODEL = "Xenova/mms-tts-eng";
18995
+ let synthPromise = null;
18996
+ function loadSynthesizer(onProgress) {
18997
+ if (!synthPromise) {
18998
+ synthPromise = (async () => {
18999
+ const { pipeline, env: env2 } = await __vitePreload(async () => {
19000
+ const { pipeline: pipeline2, env: env3 } = await import("./transformers.web-_PuZoIho.js");
19001
+ return { pipeline: pipeline2, env: env3 };
19002
+ }, true ? [] : void 0, import.meta.url);
19003
+ env2.allowLocalModels = false;
19004
+ const common = { progress_callback: onProgress };
19005
+ const hasWebGpu = typeof navigator !== "undefined" && "gpu" in navigator;
19006
+ if (hasWebGpu) {
19007
+ try {
19008
+ const s2 = await pipeline("text-to-speech", NEURAL_MODEL, { ...common, device: "webgpu" });
19009
+ console.info("[tts] neural backend: webgpu");
19010
+ return s2;
19011
+ } catch (err) {
19012
+ console.warn("[tts] WebGPU backend failed, falling back to WASM", err);
19013
+ }
19014
+ }
19015
+ const s = await pipeline("text-to-speech", NEURAL_MODEL, common);
19016
+ console.info("[tts] neural backend: wasm");
19017
+ return s;
19018
+ })();
19019
+ synthPromise.catch(() => {
19020
+ synthPromise = null;
19021
+ });
19022
+ }
19023
+ return synthPromise;
19024
+ }
19025
+ class SpeechController {
19026
+ queue = [];
19027
+ draining = false;
19028
+ audioCtx = null;
19029
+ currentSource = null;
19030
+ generation = 0;
19031
+ // bumped by stop() to abort in-flight work
19032
+ active = false;
19033
+ onModelProgress = null;
19034
+ // Fires true while a reply is being spoken/queued, false when idle — drives
19035
+ // the visible Stop button.
19036
+ onActivityChange = null;
19037
+ onError = null;
19038
+ // Multiple UI surfaces (titlebar SpeakControls + floating read button) need
19039
+ // the speaking state at once, so beyond the single onActivityChange slot we
19040
+ // also keep a listener set.
19041
+ activityListeners = /* @__PURE__ */ new Set();
19042
+ get isActive() {
19043
+ return this.active;
19044
+ }
19045
+ // Subscribe to speaking-state changes. Fires immediately with the current
19046
+ // state so a late subscriber syncs. Returns an unsubscribe function.
19047
+ onActivity(fn) {
19048
+ this.activityListeners.add(fn);
19049
+ fn(this.active);
19050
+ return () => {
19051
+ this.activityListeners.delete(fn);
19052
+ };
19053
+ }
19054
+ // Prime/resume the AudioContext from a user gesture so neural playback isn't
19055
+ // blocked by the browser autoplay policy (the System engine is unaffected).
19056
+ unlock() {
18835
19057
  try {
18836
- win.opener = null;
19058
+ const ctx = this.ensureCtx();
19059
+ if (ctx.state === "suspended") void ctx.resume();
18837
19060
  } catch {
18838
19061
  }
18839
- win.location.href = uri;
18840
19062
  }
19063
+ // Start loading the neural model in the background (e.g. when the user picks
19064
+ // the neural engine) so the first utterance doesn't pay download/compile time.
19065
+ warmup() {
19066
+ if (getEngine() !== "neural") return;
19067
+ void loadSynthesizer((p) => this.onModelProgress?.(p)).then(() => this.onModelProgress?.(null)).catch(() => {
19068
+ this.onModelProgress?.(null);
19069
+ });
19070
+ }
19071
+ enqueue(rawText) {
19072
+ const text2 = cleanForSpeech(rawText);
19073
+ if (!text2) return;
19074
+ this.queue.push(...splitIntoChunks(text2));
19075
+ void this.drain();
19076
+ }
19077
+ stop() {
19078
+ this.generation++;
19079
+ this.queue = [];
19080
+ this.draining = false;
19081
+ if (typeof speechSynthesis !== "undefined") {
19082
+ try {
19083
+ speechSynthesis.cancel();
19084
+ } catch {
19085
+ }
19086
+ }
19087
+ if (this.currentSource) {
19088
+ try {
19089
+ this.currentSource.stop();
19090
+ } catch {
19091
+ }
19092
+ this.currentSource = null;
19093
+ }
19094
+ this.onModelProgress?.(null);
19095
+ this.setActive(false);
19096
+ }
19097
+ ensureCtx() {
19098
+ if (!this.audioCtx) {
19099
+ const Ctx = window.AudioContext || window.webkitAudioContext;
19100
+ this.audioCtx = new Ctx();
19101
+ }
19102
+ return this.audioCtx;
19103
+ }
19104
+ setActive(a) {
19105
+ if (this.active === a) return;
19106
+ this.active = a;
19107
+ this.onActivityChange?.(a);
19108
+ this.activityListeners.forEach((fn) => fn(a));
19109
+ }
19110
+ async drain() {
19111
+ if (this.draining) return;
19112
+ this.draining = true;
19113
+ const gen = this.generation;
19114
+ this.setActive(true);
19115
+ while (this.queue.length && gen === this.generation) {
19116
+ const chunk = this.queue.shift();
19117
+ try {
19118
+ if (getEngine() === "neural") await this.speakNeural(chunk, gen);
19119
+ else await this.speakWeb(chunk, gen);
19120
+ } catch (err) {
19121
+ console.error("[tts] speak failed", err);
19122
+ this.onModelProgress?.(null);
19123
+ const detail = err instanceof Error ? err.message : String(err);
19124
+ this.onError?.(getEngine() === "neural" ? `Neural voice failed: ${detail}` : "Speech failed");
19125
+ }
19126
+ }
19127
+ if (gen === this.generation) {
19128
+ this.draining = false;
19129
+ this.setActive(false);
19130
+ }
19131
+ }
19132
+ speakWeb(text2, gen) {
19133
+ return new Promise((resolve2) => {
19134
+ if (typeof speechSynthesis === "undefined" || gen !== this.generation) return resolve2();
19135
+ const u = new SpeechSynthesisUtterance(text2);
19136
+ u.rate = getRate();
19137
+ const wantUri = getVoiceURI();
19138
+ if (wantUri) {
19139
+ const v2 = speechSynthesis.getVoices().find((vv) => vv.voiceURI === wantUri);
19140
+ if (v2) u.voice = v2;
19141
+ }
19142
+ u.onend = () => resolve2();
19143
+ u.onerror = () => resolve2();
19144
+ speechSynthesis.speak(u);
19145
+ });
19146
+ }
19147
+ async speakNeural(text2, gen) {
19148
+ console.info("[tts] neural: loading model…");
19149
+ this.onModelProgress?.({ status: "loading" });
19150
+ const synth = await loadSynthesizer((p) => this.onModelProgress?.(p));
19151
+ if (gen !== this.generation) return;
19152
+ console.info("[tts] neural: synthesizing", JSON.stringify(text2.slice(0, 60)));
19153
+ const out = await synth(text2);
19154
+ const raw = Array.isArray(out) ? out[0] : out;
19155
+ this.onModelProgress?.(null);
19156
+ if (gen !== this.generation) return;
19157
+ if (!raw?.audio?.length) throw new Error("neural synth returned no audio");
19158
+ console.info(`[tts] neural: playing ${raw.audio.length} samples @ ${raw.sampling_rate}Hz`);
19159
+ await this.playPcm(raw.audio, raw.sampling_rate, gen);
19160
+ }
19161
+ async playPcm(pcm, sampleRate, gen) {
19162
+ if (gen !== this.generation) return;
19163
+ const ctx = this.ensureCtx();
19164
+ if (ctx.state === "suspended") {
19165
+ try {
19166
+ await ctx.resume();
19167
+ } catch {
19168
+ }
19169
+ }
19170
+ if (gen !== this.generation) return;
19171
+ return new Promise((resolve2) => {
19172
+ const buffer = ctx.createBuffer(1, pcm.length, sampleRate);
19173
+ buffer.getChannelData(0).set(pcm);
19174
+ const source = ctx.createBufferSource();
19175
+ source.buffer = buffer;
19176
+ source.connect(ctx.destination);
19177
+ source.onended = () => {
19178
+ if (this.currentSource === source) this.currentSource = null;
19179
+ resolve2();
19180
+ };
19181
+ this.currentSource = source;
19182
+ source.start();
19183
+ });
19184
+ }
19185
+ }
19186
+ const speech = new SpeechController();
19187
+ function showTerminalToast(message) {
19188
+ window.dispatchEvent(new CustomEvent("terminal-toast", { detail: { message } }));
19189
+ }
19190
+ function copyTextValue(text2) {
19191
+ if (!text2) return;
19192
+ void window.worker.copyText(text2);
19193
+ showTerminalToast("Copied to clipboard");
19194
+ }
19195
+ function convertTextToTable(text2) {
19196
+ const result = convertToEmailTable(text2);
19197
+ if (result) {
19198
+ window.worker.copyEmailTable(result.html, result.text);
19199
+ showTerminalToast(`Copied ${result.rows}×${result.cols} table to clipboard`);
19200
+ } else {
19201
+ showTerminalToast("No table found in selection");
19202
+ }
19203
+ }
19204
+ function speakTextValue(text2) {
19205
+ if (!text2.trim()) return;
19206
+ speech.unlock();
19207
+ speech.enqueue(text2);
19208
+ }
19209
+ function copySelectionPlain(terminal) {
19210
+ copyTextValue(terminal.getSelection());
19211
+ }
19212
+ function copySelectionAsEmailTable(terminal) {
19213
+ convertTextToTable(terminal.getSelection());
19214
+ }
19215
+ function speakSelection(terminal) {
19216
+ speakTextValue(terminal.getSelection());
19217
+ }
19218
+ function captureVisibleText(terminal) {
19219
+ const buf = terminal.buffer.active;
19220
+ const lines = [];
19221
+ for (let i2 = 0; i2 < terminal.rows; i2++) {
19222
+ const line = buf.getLine(buf.viewportY + i2);
19223
+ lines.push(line ? line.translateToString(true) : "");
19224
+ }
19225
+ while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
19226
+ return lines.join("\n");
18841
19227
  }
18842
19228
  function handleTerminalLink(_event, uri) {
18843
- window.worker.getEmbedUrl(uri).then((embedUrl) => {
18844
- if (embedUrl) {
18845
- window.dispatchEvent(new CustomEvent("ctlsurf-open-url", { detail: { url: embedUrl } }));
18846
- } else {
18847
- openExternal(uri);
18848
- }
18849
- }).catch(() => openExternal(uri));
19229
+ const openInPanel = (url) => window.dispatchEvent(new CustomEvent("ctlsurf-open-url", { detail: { url } }));
19230
+ window.worker.getEmbedUrl(uri).then((embedUrl) => openInPanel(embedUrl || uri)).catch(() => openInPanel(uri));
18850
19231
  }
18851
19232
  const _terminals = /* @__PURE__ */ new Map();
18852
19233
  function isAtBottom(terminal) {
@@ -18925,11 +19306,23 @@ function getOrCreateTerminal(tabId, onExit) {
18925
19306
  if (e.key === "PageUp" || e.key === "PageDown" || e.key === "Home" || e.key === "End") {
18926
19307
  markGesture(tabId);
18927
19308
  }
18928
- const isCombo = (e.metaKey || e.ctrlKey) && e.shiftKey && e.code === "KeyE";
18929
- if (!isCombo) return true;
18930
- e.preventDefault();
18931
- copySelectionAsEmailTable(terminal);
18932
- return false;
19309
+ const mod = (e.metaKey || e.ctrlKey) && e.shiftKey;
19310
+ if (mod && e.code === "KeyE") {
19311
+ e.preventDefault();
19312
+ copySelectionAsEmailTable(terminal);
19313
+ return false;
19314
+ }
19315
+ if (mod && e.code === "KeyC" && terminal.hasSelection()) {
19316
+ e.preventDefault();
19317
+ copySelectionPlain(terminal);
19318
+ return false;
19319
+ }
19320
+ if (mod && e.code === "KeyS" && terminal.getSelection().trim()) {
19321
+ e.preventDefault();
19322
+ speakSelection(terminal);
19323
+ return false;
19324
+ }
19325
+ return true;
18933
19326
  });
18934
19327
  const state = {
18935
19328
  terminal,
@@ -18991,6 +19384,8 @@ function destroyTerminal(tabId) {
18991
19384
  function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }) {
18992
19385
  const containerRef = reactExports.useRef(null);
18993
19386
  const [menu, setMenu] = reactExports.useState(null);
19387
+ const [snapOpen, setSnapOpen] = reactExports.useState(false);
19388
+ const snapTextRef = reactExports.useRef("");
18994
19389
  const [toast, setToast] = reactExports.useState(null);
18995
19390
  const [followBottom, setFollowBottom] = reactExports.useState(true);
18996
19391
  const jumpToLatest = () => {
@@ -19002,22 +19397,44 @@ function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }) {
19002
19397
  };
19003
19398
  const handleContextMenu = (e) => {
19004
19399
  const sel = _terminals.get(tabId)?.terminal.getSelection() ?? "";
19005
- if (!convertToEmailTable(sel)) return;
19400
+ if (!sel.trim()) return;
19006
19401
  e.preventDefault();
19007
19402
  setMenu({ x: e.clientX, y: e.clientY });
19008
19403
  };
19404
+ const runCopy = () => {
19405
+ const term = _terminals.get(tabId)?.terminal;
19406
+ if (term) copySelectionPlain(term);
19407
+ setMenu(null);
19408
+ };
19009
19409
  const runConvert = () => {
19010
19410
  const term = _terminals.get(tabId)?.terminal;
19011
19411
  if (term) copySelectionAsEmailTable(term);
19012
19412
  setMenu(null);
19013
19413
  };
19414
+ const runSpeak = () => {
19415
+ const term = _terminals.get(tabId)?.terminal;
19416
+ if (term) speakSelection(term);
19417
+ setMenu(null);
19418
+ };
19419
+ const openSnapshot = () => {
19420
+ const term = _terminals.get(tabId)?.terminal;
19421
+ snapTextRef.current = term ? captureVisibleText(term) : "";
19422
+ setSnapOpen(true);
19423
+ };
19424
+ const snapAction = (kind) => {
19425
+ const domSel = window.getSelection()?.toString() ?? "";
19426
+ const text2 = domSel.trim() ? domSel : snapTextRef.current;
19427
+ if (kind === "copy") copyTextValue(text2);
19428
+ else if (kind === "table") convertTextToTable(text2);
19429
+ else speakTextValue(text2);
19430
+ };
19014
19431
  reactExports.useEffect(() => {
19015
- const onResult = (e) => {
19432
+ const onToast = (e) => {
19016
19433
  const detail = e.detail;
19017
- if (detail) setToast(detail.message);
19434
+ if (detail?.message) setToast(detail.message);
19018
19435
  };
19019
- window.addEventListener("email-table-result", onResult);
19020
- return () => window.removeEventListener("email-table-result", onResult);
19436
+ window.addEventListener("terminal-toast", onToast);
19437
+ return () => window.removeEventListener("terminal-toast", onToast);
19021
19438
  }, []);
19022
19439
  reactExports.useEffect(() => {
19023
19440
  if (!toast) return;
@@ -19141,100 +19558,75 @@ function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }) {
19141
19558
  title: "Jump to latest output",
19142
19559
  children: "↓ Latest"
19143
19560
  }
19144
- )
19561
+ ),
19562
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
19563
+ "button",
19564
+ {
19565
+ type: "button",
19566
+ className: "term-util-btn",
19567
+ onMouseDown: (e) => {
19568
+ e.preventDefault();
19569
+ openSnapshot();
19570
+ },
19571
+ title: "Snapshot screen → copy / convert to table / read aloud",
19572
+ children: "⋮"
19573
+ }
19574
+ ),
19575
+ snapOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "term-snapshot", children: [
19576
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "term-snapshot-head", children: [
19577
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Select text below (or select nothing to use everything), then pick an action." }),
19578
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
19579
+ "button",
19580
+ {
19581
+ type: "button",
19582
+ className: "term-snapshot-x",
19583
+ onMouseDown: (e) => {
19584
+ e.preventDefault();
19585
+ setSnapOpen(false);
19586
+ },
19587
+ title: "Close",
19588
+ children: "✕"
19589
+ }
19590
+ )
19591
+ ] }),
19592
+ /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "term-snapshot-text", children: snapTextRef.current }),
19593
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "term-snapshot-actions", children: [
19594
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19595
+ e.preventDefault();
19596
+ snapAction("copy");
19597
+ }, children: "⧉ Copy" }),
19598
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19599
+ e.preventDefault();
19600
+ snapAction("table");
19601
+ }, children: "▦ Convert to table" }),
19602
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19603
+ e.preventDefault();
19604
+ snapAction("speak");
19605
+ }, children: "🔊 Read aloud" }),
19606
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "term-snapshot-done", onMouseDown: (e) => {
19607
+ e.preventDefault();
19608
+ setSnapOpen(false);
19609
+ }, children: "Close" })
19610
+ ] })
19611
+ ] })
19612
+ ] }),
19613
+ menu && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "term-context-menu", style: { position: "fixed", left: menu.x, top: menu.y }, children: [
19614
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19615
+ e.preventDefault();
19616
+ runCopy();
19617
+ }, children: "Copy" }),
19618
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19619
+ e.preventDefault();
19620
+ runConvert();
19621
+ }, children: "Convert to table" }),
19622
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19623
+ e.preventDefault();
19624
+ runSpeak();
19625
+ }, children: "Read it" })
19145
19626
  ] }),
19146
- menu && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "term-context-menu", style: { position: "fixed", left: menu.x, top: menu.y }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onMouseDown: (e) => {
19147
- e.preventDefault();
19148
- runConvert();
19149
- }, children: "Copy as email table" }) }),
19150
19627
  toast && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "term-toast", children: toast })
19151
19628
  ] });
19152
19629
  }
19153
- const scriptRel = /* @__PURE__ */ (function detectScriptRel() {
19154
- const relList = typeof document !== "undefined" && document.createElement("link").relList;
19155
- return relList && relList.supports && relList.supports("modulepreload") ? "modulepreload" : "preload";
19156
- })();
19157
- const assetsURL = function(dep, importerUrl) {
19158
- return new URL(dep, importerUrl).href;
19159
- };
19160
- const seen = {};
19161
- const __vitePreload = function preload(baseModule, deps, importerUrl) {
19162
- let promise = Promise.resolve();
19163
- if (deps && deps.length > 0) {
19164
- let allSettled2 = function(promises) {
19165
- return Promise.all(
19166
- promises.map(
19167
- (p) => Promise.resolve(p).then(
19168
- (value) => ({ status: "fulfilled", value }),
19169
- (reason) => ({ status: "rejected", reason })
19170
- )
19171
- )
19172
- );
19173
- };
19174
- const links = document.getElementsByTagName("link");
19175
- const cspNonceMeta = document.querySelector(
19176
- "meta[property=csp-nonce]"
19177
- );
19178
- const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
19179
- promise = allSettled2(
19180
- deps.map((dep) => {
19181
- dep = assetsURL(dep, importerUrl);
19182
- if (dep in seen) return;
19183
- seen[dep] = true;
19184
- const isCss = dep.endsWith(".css");
19185
- const cssSelector = isCss ? '[rel="stylesheet"]' : "";
19186
- const isBaseRelative = !!importerUrl;
19187
- if (isBaseRelative) {
19188
- for (let i2 = links.length - 1; i2 >= 0; i2--) {
19189
- const link22 = links[i2];
19190
- if (link22.href === dep && (!isCss || link22.rel === "stylesheet")) {
19191
- return;
19192
- }
19193
- }
19194
- } else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
19195
- return;
19196
- }
19197
- const link2 = document.createElement("link");
19198
- link2.rel = isCss ? "stylesheet" : scriptRel;
19199
- if (!isCss) {
19200
- link2.as = "script";
19201
- }
19202
- link2.crossOrigin = "";
19203
- link2.href = dep;
19204
- if (cspNonce) {
19205
- link2.setAttribute("nonce", cspNonce);
19206
- }
19207
- document.head.appendChild(link2);
19208
- if (isCss) {
19209
- return new Promise((res, rej) => {
19210
- link2.addEventListener("load", res);
19211
- link2.addEventListener(
19212
- "error",
19213
- () => rej(new Error(`Unable to preload CSS for ${dep}`))
19214
- );
19215
- });
19216
- }
19217
- })
19218
- );
19219
- }
19220
- function handlePreloadError(err) {
19221
- const e = new Event("vite:preloadError", {
19222
- cancelable: true
19223
- });
19224
- e.payload = err;
19225
- window.dispatchEvent(e);
19226
- if (!e.defaultPrevented) {
19227
- throw err;
19228
- }
19229
- }
19230
- return promise.then((res) => {
19231
- for (const item of res || []) {
19232
- if (item.status !== "rejected") continue;
19233
- handlePreloadError(item.reason);
19234
- }
19235
- return baseModule().catch(handlePreloadError);
19236
- });
19237
- };
19238
19630
  const MODEL = "Xenova/whisper-base";
19239
19631
  const TARGET_SAMPLE_RATE = 16e3;
19240
19632
  const SILENCE_RMS = 8e-3;
@@ -19658,287 +20050,6 @@ function VoiceInput({ onTranscript, variant = "titlebar" }) {
19658
20050
  chip && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `voice-chip ${chip.kind} ${floating ? "voice-chip-floating" : ""}`, children: chip.text })
19659
20051
  ] });
19660
20052
  }
19661
- const ENGINE_KEY = "ctlsurf.tts.engine";
19662
- const VOICE_KEY = "ctlsurf.tts.voiceURI";
19663
- const RATE_KEY = "ctlsurf.tts.rate";
19664
- const MAX_SPEAK_CHARS = 1600;
19665
- const MAX_CHUNK_CHARS = 280;
19666
- function cleanForSpeech(input) {
19667
- let t = input;
19668
- t = t.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_m, body) => {
19669
- const lines = body.replace(/\n+$/, "").split("\n").filter((l2) => l2.trim().length).length;
19670
- return lines > 0 ? ` (code block, ${lines} ${lines === 1 ? "line" : "lines"}) ` : " (code) ";
19671
- });
19672
- t = t.replace(/```/g, " (code) ");
19673
- t = t.replace(/`([^`]+)`/g, "$1");
19674
- t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
19675
- t = t.replace(/https?:\/\/\S+/g, "link");
19676
- t = t.replace(/(?:[\w.-]*\/){2,}([\w.-]+)/g, "$1");
19677
- t = t.replace(/[*_#>]+/g, " ");
19678
- t = t.replace(/^\s*[-•]\s+/gm, ", ");
19679
- t = t.replace(/<[^>]+>/g, " ");
19680
- t = t.replace(/\s+/g, " ").trim();
19681
- if (t.length > MAX_SPEAK_CHARS) {
19682
- t = t.slice(0, MAX_SPEAK_CHARS).replace(/\s+\S*$/, "") + "…";
19683
- }
19684
- return t;
19685
- }
19686
- function splitIntoChunks(text2) {
19687
- if (text2.length <= MAX_CHUNK_CHARS) return text2 ? [text2] : [];
19688
- const sentences = text2.match(/[^.!?]+[.!?]+|\S[^.!?]*$/g) || [text2];
19689
- const chunks = [];
19690
- let buf = "";
19691
- for (const s of sentences) {
19692
- if ((buf + s).length > MAX_CHUNK_CHARS && buf) {
19693
- chunks.push(buf.trim());
19694
- buf = "";
19695
- }
19696
- if (s.length > MAX_CHUNK_CHARS) {
19697
- for (let i2 = 0; i2 < s.length; i2 += MAX_CHUNK_CHARS) chunks.push(s.slice(i2, i2 + MAX_CHUNK_CHARS).trim());
19698
- } else {
19699
- buf += s;
19700
- }
19701
- }
19702
- if (buf.trim()) chunks.push(buf.trim());
19703
- return chunks;
19704
- }
19705
- function getEngine() {
19706
- try {
19707
- return localStorage.getItem(ENGINE_KEY) === "neural" ? "neural" : "web";
19708
- } catch {
19709
- return "web";
19710
- }
19711
- }
19712
- function setEngine(id) {
19713
- try {
19714
- localStorage.setItem(ENGINE_KEY, id);
19715
- } catch {
19716
- }
19717
- }
19718
- function getVoiceURI() {
19719
- try {
19720
- return localStorage.getItem(VOICE_KEY) || null;
19721
- } catch {
19722
- return null;
19723
- }
19724
- }
19725
- function setVoiceURI(uri) {
19726
- try {
19727
- uri ? localStorage.setItem(VOICE_KEY, uri) : localStorage.removeItem(VOICE_KEY);
19728
- } catch {
19729
- }
19730
- }
19731
- function getRate() {
19732
- try {
19733
- const n2 = Number(localStorage.getItem(RATE_KEY));
19734
- return Number.isFinite(n2) && n2 >= 0.5 && n2 <= 2 ? n2 : 1;
19735
- } catch {
19736
- return 1;
19737
- }
19738
- }
19739
- function setRate(rate) {
19740
- try {
19741
- localStorage.setItem(RATE_KEY, String(rate));
19742
- } catch {
19743
- }
19744
- }
19745
- function listWebVoices() {
19746
- if (typeof speechSynthesis === "undefined") return [];
19747
- return speechSynthesis.getVoices();
19748
- }
19749
- const NEURAL_MODEL = "Xenova/mms-tts-eng";
19750
- let synthPromise = null;
19751
- function loadSynthesizer(onProgress) {
19752
- if (!synthPromise) {
19753
- synthPromise = (async () => {
19754
- const { pipeline, env: env2 } = await __vitePreload(async () => {
19755
- const { pipeline: pipeline2, env: env3 } = await import("./transformers.web-_PuZoIho.js");
19756
- return { pipeline: pipeline2, env: env3 };
19757
- }, true ? [] : void 0, import.meta.url);
19758
- env2.allowLocalModels = false;
19759
- const common = { progress_callback: onProgress };
19760
- const hasWebGpu = typeof navigator !== "undefined" && "gpu" in navigator;
19761
- if (hasWebGpu) {
19762
- try {
19763
- const s2 = await pipeline("text-to-speech", NEURAL_MODEL, { ...common, device: "webgpu" });
19764
- console.info("[tts] neural backend: webgpu");
19765
- return s2;
19766
- } catch (err) {
19767
- console.warn("[tts] WebGPU backend failed, falling back to WASM", err);
19768
- }
19769
- }
19770
- const s = await pipeline("text-to-speech", NEURAL_MODEL, common);
19771
- console.info("[tts] neural backend: wasm");
19772
- return s;
19773
- })();
19774
- synthPromise.catch(() => {
19775
- synthPromise = null;
19776
- });
19777
- }
19778
- return synthPromise;
19779
- }
19780
- class SpeechController {
19781
- queue = [];
19782
- draining = false;
19783
- audioCtx = null;
19784
- currentSource = null;
19785
- generation = 0;
19786
- // bumped by stop() to abort in-flight work
19787
- active = false;
19788
- onModelProgress = null;
19789
- // Fires true while a reply is being spoken/queued, false when idle — drives
19790
- // the visible Stop button.
19791
- onActivityChange = null;
19792
- onError = null;
19793
- // Multiple UI surfaces (titlebar SpeakControls + floating read button) need
19794
- // the speaking state at once, so beyond the single onActivityChange slot we
19795
- // also keep a listener set.
19796
- activityListeners = /* @__PURE__ */ new Set();
19797
- get isActive() {
19798
- return this.active;
19799
- }
19800
- // Subscribe to speaking-state changes. Fires immediately with the current
19801
- // state so a late subscriber syncs. Returns an unsubscribe function.
19802
- onActivity(fn) {
19803
- this.activityListeners.add(fn);
19804
- fn(this.active);
19805
- return () => {
19806
- this.activityListeners.delete(fn);
19807
- };
19808
- }
19809
- // Prime/resume the AudioContext from a user gesture so neural playback isn't
19810
- // blocked by the browser autoplay policy (the System engine is unaffected).
19811
- unlock() {
19812
- try {
19813
- const ctx = this.ensureCtx();
19814
- if (ctx.state === "suspended") void ctx.resume();
19815
- } catch {
19816
- }
19817
- }
19818
- // Start loading the neural model in the background (e.g. when the user picks
19819
- // the neural engine) so the first utterance doesn't pay download/compile time.
19820
- warmup() {
19821
- if (getEngine() !== "neural") return;
19822
- void loadSynthesizer((p) => this.onModelProgress?.(p)).then(() => this.onModelProgress?.(null)).catch(() => {
19823
- this.onModelProgress?.(null);
19824
- });
19825
- }
19826
- enqueue(rawText) {
19827
- const text2 = cleanForSpeech(rawText);
19828
- if (!text2) return;
19829
- this.queue.push(...splitIntoChunks(text2));
19830
- void this.drain();
19831
- }
19832
- stop() {
19833
- this.generation++;
19834
- this.queue = [];
19835
- this.draining = false;
19836
- if (typeof speechSynthesis !== "undefined") {
19837
- try {
19838
- speechSynthesis.cancel();
19839
- } catch {
19840
- }
19841
- }
19842
- if (this.currentSource) {
19843
- try {
19844
- this.currentSource.stop();
19845
- } catch {
19846
- }
19847
- this.currentSource = null;
19848
- }
19849
- this.onModelProgress?.(null);
19850
- this.setActive(false);
19851
- }
19852
- ensureCtx() {
19853
- if (!this.audioCtx) {
19854
- const Ctx = window.AudioContext || window.webkitAudioContext;
19855
- this.audioCtx = new Ctx();
19856
- }
19857
- return this.audioCtx;
19858
- }
19859
- setActive(a) {
19860
- if (this.active === a) return;
19861
- this.active = a;
19862
- this.onActivityChange?.(a);
19863
- this.activityListeners.forEach((fn) => fn(a));
19864
- }
19865
- async drain() {
19866
- if (this.draining) return;
19867
- this.draining = true;
19868
- const gen = this.generation;
19869
- this.setActive(true);
19870
- while (this.queue.length && gen === this.generation) {
19871
- const chunk = this.queue.shift();
19872
- try {
19873
- if (getEngine() === "neural") await this.speakNeural(chunk, gen);
19874
- else await this.speakWeb(chunk, gen);
19875
- } catch (err) {
19876
- console.error("[tts] speak failed", err);
19877
- this.onModelProgress?.(null);
19878
- const detail = err instanceof Error ? err.message : String(err);
19879
- this.onError?.(getEngine() === "neural" ? `Neural voice failed: ${detail}` : "Speech failed");
19880
- }
19881
- }
19882
- if (gen === this.generation) {
19883
- this.draining = false;
19884
- this.setActive(false);
19885
- }
19886
- }
19887
- speakWeb(text2, gen) {
19888
- return new Promise((resolve2) => {
19889
- if (typeof speechSynthesis === "undefined" || gen !== this.generation) return resolve2();
19890
- const u = new SpeechSynthesisUtterance(text2);
19891
- u.rate = getRate();
19892
- const wantUri = getVoiceURI();
19893
- if (wantUri) {
19894
- const v2 = speechSynthesis.getVoices().find((vv) => vv.voiceURI === wantUri);
19895
- if (v2) u.voice = v2;
19896
- }
19897
- u.onend = () => resolve2();
19898
- u.onerror = () => resolve2();
19899
- speechSynthesis.speak(u);
19900
- });
19901
- }
19902
- async speakNeural(text2, gen) {
19903
- console.info("[tts] neural: loading model…");
19904
- this.onModelProgress?.({ status: "loading" });
19905
- const synth = await loadSynthesizer((p) => this.onModelProgress?.(p));
19906
- if (gen !== this.generation) return;
19907
- console.info("[tts] neural: synthesizing", JSON.stringify(text2.slice(0, 60)));
19908
- const out = await synth(text2);
19909
- const raw = Array.isArray(out) ? out[0] : out;
19910
- this.onModelProgress?.(null);
19911
- if (gen !== this.generation) return;
19912
- if (!raw?.audio?.length) throw new Error("neural synth returned no audio");
19913
- console.info(`[tts] neural: playing ${raw.audio.length} samples @ ${raw.sampling_rate}Hz`);
19914
- await this.playPcm(raw.audio, raw.sampling_rate, gen);
19915
- }
19916
- async playPcm(pcm, sampleRate, gen) {
19917
- if (gen !== this.generation) return;
19918
- const ctx = this.ensureCtx();
19919
- if (ctx.state === "suspended") {
19920
- try {
19921
- await ctx.resume();
19922
- } catch {
19923
- }
19924
- }
19925
- if (gen !== this.generation) return;
19926
- return new Promise((resolve2) => {
19927
- const buffer = ctx.createBuffer(1, pcm.length, sampleRate);
19928
- buffer.getChannelData(0).set(pcm);
19929
- const source = ctx.createBufferSource();
19930
- source.buffer = buffer;
19931
- source.connect(ctx.destination);
19932
- source.onended = () => {
19933
- if (this.currentSource === source) this.currentSource = null;
19934
- resolve2();
19935
- };
19936
- this.currentSource = source;
19937
- source.start();
19938
- });
19939
- }
19940
- }
19941
- const speech = new SpeechController();
19942
20053
  const POS_KEY = "ctlsurf.floatingMicPos";
19943
20054
  const EDGE = 20;
19944
20055
  const TOP_MIN = 46;
@@ -20117,6 +20228,9 @@ function SpeakControls({ remote = false }) {
20117
20228
  alive = false;
20118
20229
  };
20119
20230
  }, []);
20231
+ reactExports.useEffect(() => {
20232
+ speech.warmup();
20233
+ }, []);
20120
20234
  reactExports.useEffect(() => {
20121
20235
  const off = window.worker.onAgentMessage((text2) => speech.enqueue(text2));
20122
20236
  return off;
@@ -20322,49 +20436,170 @@ function SpeakControls({ remote = false }) {
20322
20436
  error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "speak-error", children: error })
20323
20437
  ] });
20324
20438
  }
20439
+ function displayUrl(raw) {
20440
+ try {
20441
+ const u = new URL(raw);
20442
+ u.searchParams.delete("_code");
20443
+ u.searchParams.delete("embed");
20444
+ return u.toString();
20445
+ } catch {
20446
+ return raw;
20447
+ }
20448
+ }
20325
20449
  function CtlsurfPanel({ navigate }) {
20326
20450
  const webviewRef = reactExports.useRef(null);
20327
- const [url, setUrl] = reactExports.useState(null);
20451
+ const [src, setSrc] = reactExports.useState(null);
20452
+ const [address, setAddress] = reactExports.useState("");
20453
+ const [loading, setLoading] = reactExports.useState(false);
20454
+ const [nav, setNav] = reactExports.useState({ back: false, forward: false });
20328
20455
  const [key, setKey] = reactExports.useState(0);
20329
- const loadUrl = async () => {
20456
+ const hasSrcRef = reactExports.useRef(false);
20457
+ const readyRef = reactExports.useRef(false);
20458
+ const pendingRef = reactExports.useRef(null);
20459
+ const resolve2 = reactExports.useCallback(async (raw) => {
20460
+ try {
20461
+ return await window.worker.getEmbedUrl(raw) || raw;
20462
+ } catch {
20463
+ return raw;
20464
+ }
20465
+ }, []);
20466
+ const load = reactExports.useCallback((url) => {
20467
+ if (!url) return;
20468
+ setAddress(displayUrl(url));
20469
+ if (!hasSrcRef.current) {
20470
+ hasSrcRef.current = true;
20471
+ setSrc(url);
20472
+ return;
20473
+ }
20474
+ const wv = webviewRef.current;
20475
+ if (wv && readyRef.current) wv.loadURL(url);
20476
+ else pendingRef.current = url;
20477
+ }, []);
20478
+ const loadProject = reactExports.useCallback(async () => {
20330
20479
  try {
20331
20480
  const info = await window.worker.getWebviewInfo();
20332
- setUrl(info.pageUrl || info.frontendUrl);
20481
+ load(info.pageUrl || info.frontendUrl);
20333
20482
  } catch (err) {
20334
20483
  console.error("[ctlsurf] Failed to get webview info:", err);
20335
- setUrl("https://app.ctlsurf.com");
20484
+ load("https://app.ctlsurf.com");
20336
20485
  }
20337
- };
20486
+ }, [load]);
20338
20487
  reactExports.useEffect(() => {
20339
- if (!navigate?.url) loadUrl();
20488
+ if (!navigate?.url) loadProject();
20340
20489
  }, []);
20341
20490
  reactExports.useEffect(() => {
20342
- if (navigate?.url) {
20343
- setUrl(navigate.url);
20344
- setKey((k2) => k2 + 1);
20345
- }
20491
+ if (navigate?.url) resolve2(navigate.url).then(load);
20346
20492
  }, [navigate?.n]);
20347
20493
  reactExports.useEffect(() => {
20348
20494
  const unsub = window.worker.onCwdChanged(() => {
20349
- setUrl(null);
20495
+ hasSrcRef.current = false;
20496
+ readyRef.current = false;
20497
+ pendingRef.current = null;
20498
+ setSrc(null);
20350
20499
  setKey((k2) => k2 + 1);
20351
- loadUrl();
20500
+ loadProject();
20352
20501
  });
20353
20502
  return unsub;
20354
- }, []);
20355
- if (!url) {
20356
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ctlsurf-panel", style: { display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#565f89", fontSize: "13px" }, children: "Loading ctlsurf..." }) });
20357
- }
20358
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ctlsurf-panel", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
20359
- "webview",
20360
- {
20361
- ref: webviewRef,
20362
- src: url,
20363
- style: { width: "100%", height: "100%" },
20364
- allowpopups: "true"
20365
- },
20366
- key
20367
- ) });
20503
+ }, [loadProject]);
20504
+ reactExports.useEffect(() => {
20505
+ const wv = webviewRef.current;
20506
+ if (!wv) return;
20507
+ const syncNav = () => setNav({ back: wv.canGoBack?.() ?? false, forward: wv.canGoForward?.() ?? false });
20508
+ const onReady = () => {
20509
+ readyRef.current = true;
20510
+ if (pendingRef.current) {
20511
+ wv.loadURL(pendingRef.current);
20512
+ pendingRef.current = null;
20513
+ }
20514
+ syncNav();
20515
+ };
20516
+ const onNavigate = (e) => {
20517
+ const url = e.url;
20518
+ if (url) setAddress(displayUrl(url));
20519
+ syncNav();
20520
+ };
20521
+ const onStart = () => setLoading(true);
20522
+ const onStop = () => {
20523
+ setLoading(false);
20524
+ syncNav();
20525
+ };
20526
+ wv.addEventListener("dom-ready", onReady);
20527
+ wv.addEventListener("did-navigate", onNavigate);
20528
+ wv.addEventListener("did-navigate-in-page", onNavigate);
20529
+ wv.addEventListener("did-start-loading", onStart);
20530
+ wv.addEventListener("did-stop-loading", onStop);
20531
+ return () => {
20532
+ wv.removeEventListener("dom-ready", onReady);
20533
+ wv.removeEventListener("did-navigate", onNavigate);
20534
+ wv.removeEventListener("did-navigate-in-page", onNavigate);
20535
+ wv.removeEventListener("did-start-loading", onStart);
20536
+ wv.removeEventListener("did-stop-loading", onStop);
20537
+ };
20538
+ }, [src, key]);
20539
+ const onSubmit = (e) => {
20540
+ e.preventDefault();
20541
+ let target = address.trim();
20542
+ if (!target) return;
20543
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) target = "https://" + target;
20544
+ resolve2(target).then(load);
20545
+ };
20546
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ctlsurf-panel", children: [
20547
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ctlsurf-addressbar", children: [
20548
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
20549
+ "button",
20550
+ {
20551
+ type: "button",
20552
+ className: "ctlsurf-nav-btn",
20553
+ title: "Back",
20554
+ disabled: !nav.back,
20555
+ onClick: () => webviewRef.current?.goBack?.(),
20556
+ children: "‹"
20557
+ }
20558
+ ),
20559
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
20560
+ "button",
20561
+ {
20562
+ type: "button",
20563
+ className: "ctlsurf-nav-btn",
20564
+ title: "Forward",
20565
+ disabled: !nav.forward,
20566
+ onClick: () => webviewRef.current?.goForward?.(),
20567
+ children: "›"
20568
+ }
20569
+ ),
20570
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
20571
+ "button",
20572
+ {
20573
+ type: "button",
20574
+ className: "ctlsurf-nav-btn",
20575
+ title: loading ? "Stop" : "Reload",
20576
+ onClick: () => loading ? webviewRef.current?.stop?.() : webviewRef.current?.reload?.(),
20577
+ children: loading ? "×" : "⟳"
20578
+ }
20579
+ ),
20580
+ /* @__PURE__ */ jsxRuntimeExports.jsx("form", { className: "ctlsurf-address-form", onSubmit, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
20581
+ "input",
20582
+ {
20583
+ className: "ctlsurf-address-input",
20584
+ value: address,
20585
+ spellCheck: false,
20586
+ placeholder: "Enter a URL",
20587
+ onChange: (e) => setAddress(e.target.value),
20588
+ onFocus: (e) => e.currentTarget.select()
20589
+ }
20590
+ ) })
20591
+ ] }),
20592
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ctlsurf-webview-host", children: src ? /* @__PURE__ */ jsxRuntimeExports.jsx(
20593
+ "webview",
20594
+ {
20595
+ ref: webviewRef,
20596
+ src,
20597
+ style: { width: "100%", height: "100%" },
20598
+ allowpopups: "true"
20599
+ },
20600
+ key
20601
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ctlsurf-webview-loading", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#565f89", fontSize: "13px" }, children: "Loading ctlsurf..." }) }) })
20602
+ ] });
20368
20603
  }
20369
20604
  function _arrayLikeToArray(r, a) {
20370
20605
  (null == a || a > r.length) && (a = r.length);
@@ -207916,7 +208151,7 @@ const lessDefaults = new LanguageServiceDefaultsImpl$3(
207916
208151
  modeConfigurationDefault$2
207917
208152
  );
207918
208153
  function getMode$3() {
207919
- return __vitePreload(() => import("./cssMode-D7r66wvO.js"), true ? __vite__mapDeps([0,1]) : void 0, import.meta.url);
208154
+ return __vitePreload(() => import("./cssMode-CAEObGNt.js"), true ? __vite__mapDeps([0,1]) : void 0, import.meta.url);
207920
208155
  }
207921
208156
  languages.onLanguage("less", () => {
207922
208157
  getMode$3().then((mode2) => mode2.setupMode(lessDefaults));
@@ -208021,7 +208256,7 @@ const razorLanguageService = registerHTMLLanguageService(
208021
208256
  );
208022
208257
  const razorDefaults = razorLanguageService.defaults;
208023
208258
  function getMode$2() {
208024
- return __vitePreload(() => import("./htmlMode-D4VByBT6.js"), true ? __vite__mapDeps([2,1]) : void 0, import.meta.url);
208259
+ return __vitePreload(() => import("./htmlMode-B6fxDDPF.js"), true ? __vite__mapDeps([2,1]) : void 0, import.meta.url);
208025
208260
  }
208026
208261
  function registerHTMLLanguageService(languageId, options = optionsDefault, modeConfiguration = getConfigurationDefault(languageId)) {
208027
208262
  const defaults = new LanguageServiceDefaultsImpl$2(languageId, options, modeConfiguration);
@@ -208105,7 +208340,7 @@ const jsonDefaults = new LanguageServiceDefaultsImpl$1(
208105
208340
  );
208106
208341
  const getWorker$1 = () => getMode$1().then((mode2) => mode2.getWorker());
208107
208342
  function getMode$1() {
208108
- return __vitePreload(() => import("./jsonMode-CJ5AYSPV.js"), true ? __vite__mapDeps([3,1]) : void 0, import.meta.url);
208343
+ return __vitePreload(() => import("./jsonMode-CqspkWCb.js"), true ? __vite__mapDeps([3,1]) : void 0, import.meta.url);
208109
208344
  }
208110
208345
  languages.register({
208111
208346
  id: "json",
@@ -208351,7 +208586,7 @@ const getJavaScriptWorker = () => {
208351
208586
  return getMode().then((mode) => mode.getJavaScriptWorker());
208352
208587
  };
208353
208588
  function getMode() {
208354
- return __vitePreload(() => import("./tsMode-DF9m6_3n.js"), true ? [] : void 0, import.meta.url);
208589
+ return __vitePreload(() => import("./tsMode-BWdOxk6v.js"), true ? [] : void 0, import.meta.url);
208355
208590
  }
208356
208591
  languages.onLanguage("typescript", () => {
208357
208592
  return getMode().then((mode) => mode.setupTypeScript(typescriptDefaults));
@@ -208546,49 +208781,49 @@ registerLanguage({
208546
208781
  extensions: [".ftl", ".ftlh", ".ftlx"],
208547
208782
  aliases: ["FreeMarker2", "Apache FreeMarker2"],
208548
208783
  loader: () => {
208549
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
208784
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
208550
208785
  }
208551
208786
  });
208552
208787
  registerLanguage({
208553
208788
  id: "freemarker2.tag-angle.interpolation-dollar",
208554
208789
  aliases: ["FreeMarker2 (Angle/Dollar)", "Apache FreeMarker2 (Angle/Dollar)"],
208555
208790
  loader: () => {
208556
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationDollar);
208791
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationDollar);
208557
208792
  }
208558
208793
  });
208559
208794
  registerLanguage({
208560
208795
  id: "freemarker2.tag-bracket.interpolation-dollar",
208561
208796
  aliases: ["FreeMarker2 (Bracket/Dollar)", "Apache FreeMarker2 (Bracket/Dollar)"],
208562
208797
  loader: () => {
208563
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationDollar);
208798
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationDollar);
208564
208799
  }
208565
208800
  });
208566
208801
  registerLanguage({
208567
208802
  id: "freemarker2.tag-angle.interpolation-bracket",
208568
208803
  aliases: ["FreeMarker2 (Angle/Bracket)", "Apache FreeMarker2 (Angle/Bracket)"],
208569
208804
  loader: () => {
208570
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationBracket);
208805
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationBracket);
208571
208806
  }
208572
208807
  });
208573
208808
  registerLanguage({
208574
208809
  id: "freemarker2.tag-bracket.interpolation-bracket",
208575
208810
  aliases: ["FreeMarker2 (Bracket/Bracket)", "Apache FreeMarker2 (Bracket/Bracket)"],
208576
208811
  loader: () => {
208577
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationBracket);
208812
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationBracket);
208578
208813
  }
208579
208814
  });
208580
208815
  registerLanguage({
208581
208816
  id: "freemarker2.tag-auto.interpolation-dollar",
208582
208817
  aliases: ["FreeMarker2 (Auto/Dollar)", "Apache FreeMarker2 (Auto/Dollar)"],
208583
208818
  loader: () => {
208584
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
208819
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
208585
208820
  }
208586
208821
  });
208587
208822
  registerLanguage({
208588
208823
  id: "freemarker2.tag-auto.interpolation-bracket",
208589
208824
  aliases: ["FreeMarker2 (Auto/Bracket)", "Apache FreeMarker2 (Auto/Bracket)"],
208590
208825
  loader: () => {
208591
- return __vitePreload(() => import("./freemarker2-jSWJIjPG.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationBracket);
208826
+ return __vitePreload(() => import("./freemarker2-D63hnnDq.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationBracket);
208592
208827
  }
208593
208828
  });
208594
208829
  registerLanguage({
@@ -208609,7 +208844,7 @@ registerLanguage({
208609
208844
  extensions: [".handlebars", ".hbs"],
208610
208845
  aliases: ["Handlebars", "handlebars", "hbs"],
208611
208846
  mimetypes: ["text/x-handlebars-template"],
208612
- loader: () => __vitePreload(() => import("./handlebars-BnFyz5RH.js"), true ? [] : void 0, import.meta.url)
208847
+ loader: () => __vitePreload(() => import("./handlebars-UYo_IYpi.js"), true ? [] : void 0, import.meta.url)
208613
208848
  });
208614
208849
  registerLanguage({
208615
208850
  id: "hcl",
@@ -208622,7 +208857,7 @@ registerLanguage({
208622
208857
  extensions: [".html", ".htm", ".shtml", ".xhtml", ".mdoc", ".jsp", ".asp", ".aspx", ".jshtm"],
208623
208858
  aliases: ["HTML", "htm", "html", "xhtml"],
208624
208859
  mimetypes: ["text/html", "text/x-jshtm", "text/template", "text/ng-template"],
208625
- loader: () => __vitePreload(() => import("./html-Bl5UFavp.js"), true ? [] : void 0, import.meta.url)
208860
+ loader: () => __vitePreload(() => import("./html-2dvYDi3g.js"), true ? [] : void 0, import.meta.url)
208626
208861
  });
208627
208862
  registerLanguage({
208628
208863
  id: "ini",
@@ -208645,7 +208880,7 @@ registerLanguage({
208645
208880
  filenames: ["jakefile"],
208646
208881
  aliases: ["JavaScript", "javascript", "js"],
208647
208882
  mimetypes: ["text/javascript"],
208648
- loader: () => __vitePreload(() => import("./javascript-CMN1OmwG.js"), true ? __vite__mapDeps([4,5]) : void 0, import.meta.url)
208883
+ loader: () => __vitePreload(() => import("./javascript-8RyFDDdk.js"), true ? __vite__mapDeps([4,5]) : void 0, import.meta.url)
208649
208884
  });
208650
208885
  registerLanguage({
208651
208886
  id: "julia",
@@ -208684,7 +208919,7 @@ registerLanguage({
208684
208919
  extensions: [".liquid", ".html.liquid"],
208685
208920
  aliases: ["Liquid", "liquid"],
208686
208921
  mimetypes: ["application/liquid"],
208687
- loader: () => __vitePreload(() => import("./liquid-BGMIDTEm.js"), true ? [] : void 0, import.meta.url)
208922
+ loader: () => __vitePreload(() => import("./liquid-DaEnAsYY.js"), true ? [] : void 0, import.meta.url)
208688
208923
  });
208689
208924
  registerLanguage({
208690
208925
  id: "m3",
@@ -208702,7 +208937,7 @@ registerLanguage({
208702
208937
  id: "mdx",
208703
208938
  extensions: [".mdx"],
208704
208939
  aliases: ["MDX", "mdx"],
208705
- loader: () => __vitePreload(() => import("./mdx-B2Z7R10Q.js"), true ? [] : void 0, import.meta.url)
208940
+ loader: () => __vitePreload(() => import("./mdx-Q-qSZsFg.js"), true ? [] : void 0, import.meta.url)
208706
208941
  });
208707
208942
  registerLanguage({
208708
208943
  id: "mips",
@@ -208801,7 +209036,7 @@ registerLanguage({
208801
209036
  extensions: [".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi"],
208802
209037
  aliases: ["Python", "py"],
208803
209038
  firstLine: "^#!/.*\\bpython[0-9.-]*\\b",
208804
- loader: () => __vitePreload(() => import("./python-LMDMSSdn.js"), true ? [] : void 0, import.meta.url)
209039
+ loader: () => __vitePreload(() => import("./python-Sw0S0BJV.js"), true ? [] : void 0, import.meta.url)
208805
209040
  });
208806
209041
  registerLanguage({
208807
209042
  id: "qsharp",
@@ -208820,7 +209055,7 @@ registerLanguage({
208820
209055
  extensions: [".cshtml"],
208821
209056
  aliases: ["Razor", "razor"],
208822
209057
  mimetypes: ["text/x-cshtml"],
208823
- loader: () => __vitePreload(() => import("./razor-BZSrziPu.js"), true ? [] : void 0, import.meta.url)
209058
+ loader: () => __vitePreload(() => import("./razor-C7zQmabz.js"), true ? [] : void 0, import.meta.url)
208824
209059
  });
208825
209060
  registerLanguage({
208826
209061
  id: "redis",
@@ -208953,7 +209188,7 @@ registerLanguage({
208953
209188
  aliases: ["TypeScript", "ts", "typescript"],
208954
209189
  mimetypes: ["text/typescript"],
208955
209190
  loader: () => {
208956
- return __vitePreload(() => import("./typescript-rZeCMf0a.js"), true ? [] : void 0, import.meta.url);
209191
+ return __vitePreload(() => import("./typescript-BrdIiKZf.js"), true ? [] : void 0, import.meta.url);
208957
209192
  }
208958
209193
  });
208959
209194
  registerLanguage({
@@ -208998,14 +209233,14 @@ registerLanguage({
208998
209233
  firstLine: "(\\<\\?xml.*)|(\\<svg)|(\\<\\!doctype\\s+svg)",
208999
209234
  aliases: ["XML", "xml"],
209000
209235
  mimetypes: ["text/xml", "application/xml", "application/xaml+xml", "application/xml-dtd"],
209001
- loader: () => __vitePreload(() => import("./xml-fNSZ-7so.js"), true ? [] : void 0, import.meta.url)
209236
+ loader: () => __vitePreload(() => import("./xml-DbKLNPOq.js"), true ? [] : void 0, import.meta.url)
209002
209237
  });
209003
209238
  registerLanguage({
209004
209239
  id: "yaml",
209005
209240
  extensions: [".yaml", ".yml"],
209006
209241
  aliases: ["YAML", "yaml", "YML", "yml"],
209007
209242
  mimetypes: ["application/x-yaml", "text/x-yaml"],
209008
- loader: () => __vitePreload(() => import("./yaml-1wxIuvzh.js"), true ? [] : void 0, import.meta.url)
209243
+ loader: () => __vitePreload(() => import("./yaml-ehupJBsZ.js"), true ? [] : void 0, import.meta.url)
209009
209244
  });
209010
209245
  var __defProp = Object.defineProperty;
209011
209246
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -213297,6 +213532,7 @@ function SettingsDialog({ open, onClose }) {
213297
213532
  const [trackTime, setTrackTime] = reactExports.useState(false);
213298
213533
  const [idleTimeoutMin, setIdleTimeoutMin] = reactExports.useState(15);
213299
213534
  const [saved, setSaved] = reactExports.useState(false);
213535
+ const [version, setVersion] = reactExports.useState("");
213300
213536
  const loadProfiles = async () => {
213301
213537
  const data = await window.worker.listProfiles();
213302
213538
  setProfileList(data);
@@ -213307,6 +213543,8 @@ function SettingsDialog({ open, onClose }) {
213307
213543
  loadProfiles();
213308
213544
  setEditingId(null);
213309
213545
  setSaved(false);
213546
+ window.worker.getVersion().then(setVersion).catch(() => {
213547
+ });
213310
213548
  }
213311
213549
  }, [open]);
213312
213550
  const startEditing = (profile) => {
@@ -213477,7 +213715,16 @@ function SettingsDialog({ open, onClose }) {
213477
213715
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "btn-secondary", onClick: onClose, children: "Close" })
213478
213716
  ] })
213479
213717
  ] })
213480
- )
213718
+ ),
213719
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "settings-section settings-about", children: [
213720
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { children: "About" }),
213721
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "about-name", children: [
213722
+ "ctlsurf",
213723
+ version ? ` · v${version}` : ""
213724
+ ] }),
213725
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "about-copyright", children: "© 2026 Phenx, Inc. All rights reserved." }),
213726
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "settings-hint", children: "Proprietary and confidential. No license to use, copy, modify, or distribute is granted except under a separate written agreement with Phenx, Inc." })
213727
+ ] })
213481
213728
  ] }) });
213482
213729
  }
213483
213730
  const STATUS_OPTIONS = [
@@ -213518,6 +213765,47 @@ function TagIcon({ size: size2 = 16 }) {
213518
213765
  }
213519
213766
  );
213520
213767
  }
213768
+ function SendIcon({ size: size2 = 15 }) {
213769
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
213770
+ "svg",
213771
+ {
213772
+ viewBox: "0 0 24 24",
213773
+ width: size2,
213774
+ height: size2,
213775
+ fill: "none",
213776
+ stroke: "currentColor",
213777
+ strokeWidth: "2",
213778
+ strokeLinecap: "round",
213779
+ strokeLinejoin: "round",
213780
+ "aria-hidden": "true",
213781
+ children: [
213782
+ /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
213783
+ /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
213784
+ ]
213785
+ }
213786
+ );
213787
+ }
213788
+ function ticketPrompt(t, refs) {
213789
+ const lines = [
213790
+ "Please work on this ticket:",
213791
+ `Title: ${t.title || "Untitled"}`,
213792
+ `Priority: ${t.priority}`
213793
+ ];
213794
+ const desc = t.description?.trim();
213795
+ if (desc) lines.push("", desc);
213796
+ lines.push(
213797
+ "",
213798
+ "ctlsurf references (use the ctlsurf MCP tools to read details or update status):",
213799
+ `- Ticket row ID: ${t.id}`
213800
+ );
213801
+ if (refs) {
213802
+ lines.push(
213803
+ `- Tickets datastore block ID: ${refs.blockId}`,
213804
+ `- Agent Datastore page ID: ${refs.pageId}`
213805
+ );
213806
+ }
213807
+ return lines.join("\n");
213808
+ }
213521
213809
  function Segmented({ options, value, onChange }) {
213522
213810
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ticket-segmented", children: options.map((o) => {
213523
213811
  const active = o.value === value;
@@ -213537,33 +213825,47 @@ function Segmented({ options, value, onChange }) {
213537
213825
  );
213538
213826
  }) });
213539
213827
  }
213540
- function TicketCard({ ticket, onClick }) {
213541
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "ticket-card", onClick, children: [
213542
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-card-bar", style: { background: colorOf(STATUS_OPTIONS, ticket.status) } }),
213543
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ticket-card-main", children: [
213544
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-card-title", children: ticket.title || "Untitled" }),
213545
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ticket-card-meta", children: [
213546
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ticket-card-status", style: { color: colorOf(STATUS_OPTIONS, ticket.status) }, children: [
213547
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-seg-dot", style: { background: colorOf(STATUS_OPTIONS, ticket.status) } }),
213548
- ticket.status
213549
- ] }),
213550
- ticket.created && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-card-date", children: formatDate(ticket.created) })
213551
- ] })
213828
+ function TicketCard({ ticket, onClick, onSend }) {
213829
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ticket-card", children: [
213830
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "ticket-card-open", onClick, title: "Edit ticket", children: [
213831
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-card-bar", style: { background: colorOf(STATUS_OPTIONS, ticket.status) } }),
213832
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ticket-card-main", children: [
213833
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-card-title", children: ticket.title || "Untitled" }),
213834
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ticket-card-meta", children: [
213835
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ticket-card-status", style: { color: colorOf(STATUS_OPTIONS, ticket.status) }, children: [
213836
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-seg-dot", style: { background: colorOf(STATUS_OPTIONS, ticket.status) } }),
213837
+ ticket.status
213838
+ ] }),
213839
+ ticket.created && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-card-date", children: formatDate(ticket.created) })
213840
+ ] })
213841
+ ] }),
213842
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
213843
+ "span",
213844
+ {
213845
+ className: "ticket-card-pri",
213846
+ style: { color: colorOf(PRIORITY_OPTIONS, ticket.priority), borderColor: `${colorOf(PRIORITY_OPTIONS, ticket.priority)}66` },
213847
+ children: ticket.priority
213848
+ }
213849
+ )
213552
213850
  ] }),
213553
213851
  /* @__PURE__ */ jsxRuntimeExports.jsx(
213554
- "span",
213852
+ "button",
213555
213853
  {
213556
- className: "ticket-card-pri",
213557
- style: { color: colorOf(PRIORITY_OPTIONS, ticket.priority), borderColor: `${colorOf(PRIORITY_OPTIONS, ticket.priority)}66` },
213558
- children: ticket.priority
213854
+ type: "button",
213855
+ className: "ticket-card-send",
213856
+ onClick: onSend,
213857
+ title: "Send to coding terminal",
213858
+ "aria-label": "Send ticket to coding terminal",
213859
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(SendIcon, {})
213559
213860
  }
213560
213861
  )
213561
213862
  ] });
213562
213863
  }
213563
- function TicketPane() {
213864
+ function TicketPane({ onSendToTerminal }) {
213564
213865
  const [project, setProject] = reactExports.useState(null);
213565
213866
  const [view, setView] = reactExports.useState("list");
213566
213867
  const [tickets, setTickets] = reactExports.useState([]);
213868
+ const [refs, setRefs] = reactExports.useState(null);
213567
213869
  const [listLoading, setListLoading] = reactExports.useState(false);
213568
213870
  const [listError, setListError] = reactExports.useState(null);
213569
213871
  const [showDone, setShowDone] = reactExports.useState(false);
@@ -213578,13 +213880,17 @@ function TicketPane() {
213578
213880
  setListError(null);
213579
213881
  try {
213580
213882
  const r = await window.worker.listTickets();
213581
- if (r?.ok) setTickets(r.tickets || []);
213582
- else {
213883
+ if (r?.ok) {
213884
+ setTickets(r.tickets || []);
213885
+ setRefs(r.blockId && r.pageId ? { blockId: r.blockId, pageId: r.pageId } : null);
213886
+ } else {
213583
213887
  setTickets([]);
213888
+ setRefs(null);
213584
213889
  setListError(r?.error || "Failed to load tickets");
213585
213890
  }
213586
213891
  } catch (err) {
213587
213892
  setTickets([]);
213893
+ setRefs(null);
213588
213894
  setListError(err?.message || "Failed to load tickets");
213589
213895
  } finally {
213590
213896
  setListLoading(false);
@@ -213684,7 +213990,7 @@ function TicketPane() {
213684
213990
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-empty-title", children: "No tickets yet" }),
213685
213991
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-empty-hint", children: "Log the first one for this project." })
213686
213992
  ] }),
213687
- !listLoading && activeTickets.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(TicketCard, { ticket: t, onClick: () => openEditForm(t) }, t.id)),
213993
+ !listLoading && activeTickets.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(TicketCard, { ticket: t, onClick: () => openEditForm(t), onSend: () => onSendToTerminal(ticketPrompt(t, refs)) }, t.id)),
213688
213994
  !listLoading && activeTickets.length === 0 && doneTickets.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ticket-list-msg", children: "Nothing open — all caught up." }),
213689
213995
  !listLoading && doneTickets.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
213690
213996
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -213701,7 +214007,7 @@ function TicketPane() {
213701
214007
  ]
213702
214008
  }
213703
214009
  ),
213704
- showDone && doneTickets.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(TicketCard, { ticket: t, onClick: () => openEditForm(t) }, t.id))
214010
+ showDone && doneTickets.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(TicketCard, { ticket: t, onClick: () => openEditForm(t), onSend: () => onSendToTerminal(ticketPrompt(t, refs)) }, t.id))
213705
214011
  ] })
213706
214012
  ] }),
213707
214013
  tickets.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ticket-drawer-footer", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ticket-drawer-hint", children: doneTickets.length > 0 ? `${activeTickets.length} open · ${doneTickets.length} done` : `${tickets.length} ticket${tickets.length === 1 ? "" : "s"}` }) })
@@ -213897,6 +214203,13 @@ function App() {
213897
214203
  focusTerminal(activeTabId);
213898
214204
  setTimeout(() => window.worker.writePty(activeTabId, "\r"), 80);
213899
214205
  }, [activeTabId]);
214206
+ const handleSendToTerminal = reactExports.useCallback((text2) => {
214207
+ const trimmed = text2.trim();
214208
+ if (!trimmed) return;
214209
+ window.worker.writePty(activeTabId, trimmed);
214210
+ focusTerminal(activeTabId);
214211
+ setTimeout(() => window.worker.writePty(activeTabId, "\r"), 120);
214212
+ }, [activeTabId]);
213900
214213
  const cwdRef = reactExports.useRef(null);
213901
214214
  const handleSpawn = reactExports.useCallback(async (tabId, agent) => {
213902
214215
  setTabs((prev) => prev.map((t) => t.id === tabId ? { ...t, agentStatus: "active" } : t));
@@ -214109,7 +214422,7 @@ function App() {
214109
214422
  ] })
214110
214423
  },
214111
214424
  { id: "ctlsurf", label: "ctlsurf", content: /* @__PURE__ */ jsxRuntimeExports.jsx(CtlsurfPanel, { navigate: ctlsurfNav }) },
214112
- { id: "tickets", label: "Tickets", content: /* @__PURE__ */ jsxRuntimeExports.jsx(TicketPane, {}) }
214425
+ { id: "tickets", label: "Tickets", content: /* @__PURE__ */ jsxRuntimeExports.jsx(TicketPane, { onSendToTerminal: handleSendToTerminal }) }
214113
214426
  ];
214114
214427
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "app", children: [
214115
214428
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "titlebar", children: [