@phenx-inc/ctlsurf 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/out/headless/index.mjs +1 -1
  2. package/out/headless/index.mjs.map +1 -1
  3. package/out/renderer/assets/{cssMode-Xx1-SozN.js → cssMode-DwHD6JSn.js} +3 -3
  4. package/out/renderer/assets/{freemarker2-DM49yPw1.js → freemarker2-Na9-bXYw.js} +1 -1
  5. package/out/renderer/assets/{handlebars-DJJBblpV.js → handlebars-D7gzKLS8.js} +1 -1
  6. package/out/renderer/assets/{html-DnMnJOxw.js → html-DDK7xN27.js} +1 -1
  7. package/out/renderer/assets/{htmlMode-BG5u2CSH.js → htmlMode-BfbLv-NB.js} +3 -3
  8. package/out/renderer/assets/{index-D65QIRev.js → index-HoDaBul3.js} +62 -34
  9. package/out/renderer/assets/{javascript-Dci_phmN.js → javascript-DJjWOxID.js} +2 -2
  10. package/out/renderer/assets/{jsonMode-Dmp3JJSm.js → jsonMode-BileWbig.js} +3 -3
  11. package/out/renderer/assets/{liquid-C9KVysk1.js → liquid-DvNU_dI9.js} +1 -1
  12. package/out/renderer/assets/{lspLanguageFeatures-IDD0-KWa.js → lspLanguageFeatures-Bb27EY8c.js} +1 -1
  13. package/out/renderer/assets/{mdx-ascmBFXX.js → mdx-CLkNPVgD.js} +1 -1
  14. package/out/renderer/assets/{python-DcbM4Hy5.js → python-DOJdL4kC.js} +1 -1
  15. package/out/renderer/assets/{razor-XJROxpYK.js → razor-CSr8UlJc.js} +1 -1
  16. package/out/renderer/assets/{tsMode-EXbzul3n.js → tsMode-sbsAvk21.js} +1 -1
  17. package/out/renderer/assets/{typescript-DhTtFBLt.js → typescript-r5QB4yRW.js} +1 -1
  18. package/out/renderer/assets/{xml-BLfEkVl1.js → xml-CYhKh3Pi.js} +1 -1
  19. package/out/renderer/assets/{yaml-DpzsqvWa.js → yaml-B8DsoaYW.js} +1 -1
  20. package/out/renderer/index.html +1 -1
  21. package/package.json +1 -1
  22. package/scripts/test-terminal-scroll.cjs +64 -0
  23. package/src/renderer/components/TerminalPanel.tsx +54 -20
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-D65QIRev.js";
2
- import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-IDD0-KWa.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-IDD0-KWa.js";
1
+ import { c as createWebWorker, l as languages } from "./index-HoDaBul3.js";
2
+ import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-Bb27EY8c.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-Bb27EY8c.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-D65QIRev.js";
2
- import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-IDD0-KWa.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-IDD0-KWa.js";
1
+ import { c as createWebWorker, l as languages } from "./index-HoDaBul3.js";
2
+ import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-Bb27EY8c.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-Bb27EY8c.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {
@@ -1,4 +1,4 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./cssMode-Xx1-SozN.js","./lspLanguageFeatures-IDD0-KWa.js","./htmlMode-BG5u2CSH.js","./jsonMode-Dmp3JJSm.js","./javascript-Dci_phmN.js","./typescript-DhTtFBLt.js"])))=>i.map(i=>d[i]);
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./cssMode-DwHD6JSn.js","./lspLanguageFeatures-Bb27EY8c.js","./htmlMode-BfbLv-NB.js","./jsonMode-BileWbig.js","./javascript-DJjWOxID.js","./typescript-r5QB4yRW.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
  }
@@ -19525,6 +19525,20 @@ function markGesture(tabId) {
19525
19525
  const s = _terminals.get(tabId);
19526
19526
  if (s) s.userScrollPending = true;
19527
19527
  }
19528
+ function followLatest(state) {
19529
+ state.userScrollPending = false;
19530
+ setPinned(state, true);
19531
+ reassert(state);
19532
+ }
19533
+ function rememberScrollPosition(state) {
19534
+ state.userScrollPending = false;
19535
+ if (isAtBottom(state.terminal)) {
19536
+ setPinned(state, true);
19537
+ } else {
19538
+ state.anchorLine = state.terminal.buffer.active.viewportY;
19539
+ setPinned(state, false);
19540
+ }
19541
+ }
19528
19542
  function getOrCreateTerminal(tabId, onExit) {
19529
19543
  const existing = _terminals.get(tabId);
19530
19544
  if (existing) {
@@ -19550,7 +19564,9 @@ function getOrCreateTerminal(tabId, onExit) {
19550
19564
  fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', monospace",
19551
19565
  theme: currentTheme().terminal,
19552
19566
  scrollback: 1e4,
19553
- scrollOnUserInput: true,
19567
+ // Follow explicit keyboard/paste input below. xterm also counts mouse
19568
+ // reports as user input, which must not pull someone out of scrollback.
19569
+ scrollOnUserInput: false,
19554
19570
  allowTransparency: false
19555
19571
  });
19556
19572
  const fitAddon = new addonFitExports.FitAddon();
@@ -19612,17 +19628,10 @@ function getOrCreateTerminal(tabId, onExit) {
19612
19628
  reassert(state);
19613
19629
  return;
19614
19630
  }
19615
- state.userScrollPending = false;
19616
- if (isAtBottom(terminal)) {
19617
- setPinned(state, true);
19618
- } else {
19619
- state.anchorLine = terminal.buffer.active.viewportY;
19620
- setPinned(state, false);
19621
- }
19631
+ rememberScrollPosition(state);
19622
19632
  });
19633
+ terminal.onKey(() => followLatest(state));
19623
19634
  terminal.onData((data) => {
19624
- setPinned(state, true);
19625
- terminal.scrollToBottom();
19626
19635
  window.worker.writePty(tabId, data);
19627
19636
  });
19628
19637
  state.unsubData = window.worker.onPtyData((eventTabId, data) => {
@@ -19680,8 +19689,7 @@ function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, isActive
19680
19689
  const jumpToLatest = () => {
19681
19690
  const s = _terminals.get(tabId);
19682
19691
  if (!s) return;
19683
- setPinned(s, true);
19684
- s.terminal.scrollToBottom();
19692
+ followLatest(s);
19685
19693
  s.terminal.focus();
19686
19694
  };
19687
19695
  const handleContextMenu = (e) => {
@@ -19761,6 +19769,22 @@ function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, isActive
19761
19769
  if (attached) reassert(attached);
19762
19770
  const onWheel = () => markGesture(tabId);
19763
19771
  terminal.element?.addEventListener("wheel", onWheel, { capture: true, passive: true });
19772
+ const viewport = terminal.element?.querySelector(".xterm-viewport");
19773
+ const onViewportScroll = () => {
19774
+ const state = _terminals.get(tabId);
19775
+ if (state && !state.holding && state.userScrollPending) rememberScrollPosition(state);
19776
+ };
19777
+ viewport?.addEventListener("scroll", onViewportScroll);
19778
+ const onTextInput = () => {
19779
+ const state = _terminals.get(tabId);
19780
+ if (state) followLatest(state);
19781
+ };
19782
+ const onPaste = (event) => {
19783
+ if (event.clipboardData?.getData("text/plain")) onTextInput();
19784
+ };
19785
+ terminal.textarea?.addEventListener("paste", onPaste, true);
19786
+ terminal.textarea?.addEventListener("input", onTextInput, true);
19787
+ terminal.textarea?.addEventListener("compositionend", onTextInput, true);
19764
19788
  let resizeTimeout;
19765
19789
  const handleResize = () => {
19766
19790
  clearTimeout(resizeTimeout);
@@ -19784,6 +19808,10 @@ function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, isActive
19784
19808
  window.removeEventListener("resize", handleResize);
19785
19809
  observer.disconnect();
19786
19810
  terminal.element?.removeEventListener("wheel", onWheel, true);
19811
+ viewport?.removeEventListener("scroll", onViewportScroll);
19812
+ terminal.textarea?.removeEventListener("paste", onPaste, true);
19813
+ terminal.textarea?.removeEventListener("input", onTextInput, true);
19814
+ terminal.textarea?.removeEventListener("compositionend", onTextInput, true);
19787
19815
  };
19788
19816
  }, [tabId, onExit]);
19789
19817
  reactExports.useEffect(() => {
@@ -208443,7 +208471,7 @@ const lessDefaults = new LanguageServiceDefaultsImpl$3(
208443
208471
  modeConfigurationDefault$2
208444
208472
  );
208445
208473
  function getMode$3() {
208446
- return __vitePreload(() => import("./cssMode-Xx1-SozN.js"), true ? __vite__mapDeps([0,1]) : void 0, import.meta.url);
208474
+ return __vitePreload(() => import("./cssMode-DwHD6JSn.js"), true ? __vite__mapDeps([0,1]) : void 0, import.meta.url);
208447
208475
  }
208448
208476
  languages.onLanguage("less", () => {
208449
208477
  getMode$3().then((mode2) => mode2.setupMode(lessDefaults));
@@ -208548,7 +208576,7 @@ const razorLanguageService = registerHTMLLanguageService(
208548
208576
  );
208549
208577
  const razorDefaults = razorLanguageService.defaults;
208550
208578
  function getMode$2() {
208551
- return __vitePreload(() => import("./htmlMode-BG5u2CSH.js"), true ? __vite__mapDeps([2,1]) : void 0, import.meta.url);
208579
+ return __vitePreload(() => import("./htmlMode-BfbLv-NB.js"), true ? __vite__mapDeps([2,1]) : void 0, import.meta.url);
208552
208580
  }
208553
208581
  function registerHTMLLanguageService(languageId, options = optionsDefault, modeConfiguration = getConfigurationDefault(languageId)) {
208554
208582
  const defaults = new LanguageServiceDefaultsImpl$2(languageId, options, modeConfiguration);
@@ -208632,7 +208660,7 @@ const jsonDefaults = new LanguageServiceDefaultsImpl$1(
208632
208660
  );
208633
208661
  const getWorker$1 = () => getMode$1().then((mode2) => mode2.getWorker());
208634
208662
  function getMode$1() {
208635
- return __vitePreload(() => import("./jsonMode-Dmp3JJSm.js"), true ? __vite__mapDeps([3,1]) : void 0, import.meta.url);
208663
+ return __vitePreload(() => import("./jsonMode-BileWbig.js"), true ? __vite__mapDeps([3,1]) : void 0, import.meta.url);
208636
208664
  }
208637
208665
  languages.register({
208638
208666
  id: "json",
@@ -208878,7 +208906,7 @@ const getJavaScriptWorker = () => {
208878
208906
  return getMode().then((mode) => mode.getJavaScriptWorker());
208879
208907
  };
208880
208908
  function getMode() {
208881
- return __vitePreload(() => import("./tsMode-EXbzul3n.js"), true ? [] : void 0, import.meta.url);
208909
+ return __vitePreload(() => import("./tsMode-sbsAvk21.js"), true ? [] : void 0, import.meta.url);
208882
208910
  }
208883
208911
  languages.onLanguage("typescript", () => {
208884
208912
  return getMode().then((mode) => mode.setupTypeScript(typescriptDefaults));
@@ -209073,49 +209101,49 @@ registerLanguage({
209073
209101
  extensions: [".ftl", ".ftlh", ".ftlx"],
209074
209102
  aliases: ["FreeMarker2", "Apache FreeMarker2"],
209075
209103
  loader: () => {
209076
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
209104
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
209077
209105
  }
209078
209106
  });
209079
209107
  registerLanguage({
209080
209108
  id: "freemarker2.tag-angle.interpolation-dollar",
209081
209109
  aliases: ["FreeMarker2 (Angle/Dollar)", "Apache FreeMarker2 (Angle/Dollar)"],
209082
209110
  loader: () => {
209083
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationDollar);
209111
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationDollar);
209084
209112
  }
209085
209113
  });
209086
209114
  registerLanguage({
209087
209115
  id: "freemarker2.tag-bracket.interpolation-dollar",
209088
209116
  aliases: ["FreeMarker2 (Bracket/Dollar)", "Apache FreeMarker2 (Bracket/Dollar)"],
209089
209117
  loader: () => {
209090
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationDollar);
209118
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationDollar);
209091
209119
  }
209092
209120
  });
209093
209121
  registerLanguage({
209094
209122
  id: "freemarker2.tag-angle.interpolation-bracket",
209095
209123
  aliases: ["FreeMarker2 (Angle/Bracket)", "Apache FreeMarker2 (Angle/Bracket)"],
209096
209124
  loader: () => {
209097
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationBracket);
209125
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationBracket);
209098
209126
  }
209099
209127
  });
209100
209128
  registerLanguage({
209101
209129
  id: "freemarker2.tag-bracket.interpolation-bracket",
209102
209130
  aliases: ["FreeMarker2 (Bracket/Bracket)", "Apache FreeMarker2 (Bracket/Bracket)"],
209103
209131
  loader: () => {
209104
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationBracket);
209132
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationBracket);
209105
209133
  }
209106
209134
  });
209107
209135
  registerLanguage({
209108
209136
  id: "freemarker2.tag-auto.interpolation-dollar",
209109
209137
  aliases: ["FreeMarker2 (Auto/Dollar)", "Apache FreeMarker2 (Auto/Dollar)"],
209110
209138
  loader: () => {
209111
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
209139
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
209112
209140
  }
209113
209141
  });
209114
209142
  registerLanguage({
209115
209143
  id: "freemarker2.tag-auto.interpolation-bracket",
209116
209144
  aliases: ["FreeMarker2 (Auto/Bracket)", "Apache FreeMarker2 (Auto/Bracket)"],
209117
209145
  loader: () => {
209118
- return __vitePreload(() => import("./freemarker2-DM49yPw1.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationBracket);
209146
+ return __vitePreload(() => import("./freemarker2-Na9-bXYw.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationBracket);
209119
209147
  }
209120
209148
  });
209121
209149
  registerLanguage({
@@ -209136,7 +209164,7 @@ registerLanguage({
209136
209164
  extensions: [".handlebars", ".hbs"],
209137
209165
  aliases: ["Handlebars", "handlebars", "hbs"],
209138
209166
  mimetypes: ["text/x-handlebars-template"],
209139
- loader: () => __vitePreload(() => import("./handlebars-DJJBblpV.js"), true ? [] : void 0, import.meta.url)
209167
+ loader: () => __vitePreload(() => import("./handlebars-D7gzKLS8.js"), true ? [] : void 0, import.meta.url)
209140
209168
  });
209141
209169
  registerLanguage({
209142
209170
  id: "hcl",
@@ -209149,7 +209177,7 @@ registerLanguage({
209149
209177
  extensions: [".html", ".htm", ".shtml", ".xhtml", ".mdoc", ".jsp", ".asp", ".aspx", ".jshtm"],
209150
209178
  aliases: ["HTML", "htm", "html", "xhtml"],
209151
209179
  mimetypes: ["text/html", "text/x-jshtm", "text/template", "text/ng-template"],
209152
- loader: () => __vitePreload(() => import("./html-DnMnJOxw.js"), true ? [] : void 0, import.meta.url)
209180
+ loader: () => __vitePreload(() => import("./html-DDK7xN27.js"), true ? [] : void 0, import.meta.url)
209153
209181
  });
209154
209182
  registerLanguage({
209155
209183
  id: "ini",
@@ -209172,7 +209200,7 @@ registerLanguage({
209172
209200
  filenames: ["jakefile"],
209173
209201
  aliases: ["JavaScript", "javascript", "js"],
209174
209202
  mimetypes: ["text/javascript"],
209175
- loader: () => __vitePreload(() => import("./javascript-Dci_phmN.js"), true ? __vite__mapDeps([4,5]) : void 0, import.meta.url)
209203
+ loader: () => __vitePreload(() => import("./javascript-DJjWOxID.js"), true ? __vite__mapDeps([4,5]) : void 0, import.meta.url)
209176
209204
  });
209177
209205
  registerLanguage({
209178
209206
  id: "julia",
@@ -209211,7 +209239,7 @@ registerLanguage({
209211
209239
  extensions: [".liquid", ".html.liquid"],
209212
209240
  aliases: ["Liquid", "liquid"],
209213
209241
  mimetypes: ["application/liquid"],
209214
- loader: () => __vitePreload(() => import("./liquid-C9KVysk1.js"), true ? [] : void 0, import.meta.url)
209242
+ loader: () => __vitePreload(() => import("./liquid-DvNU_dI9.js"), true ? [] : void 0, import.meta.url)
209215
209243
  });
209216
209244
  registerLanguage({
209217
209245
  id: "m3",
@@ -209229,7 +209257,7 @@ registerLanguage({
209229
209257
  id: "mdx",
209230
209258
  extensions: [".mdx"],
209231
209259
  aliases: ["MDX", "mdx"],
209232
- loader: () => __vitePreload(() => import("./mdx-ascmBFXX.js"), true ? [] : void 0, import.meta.url)
209260
+ loader: () => __vitePreload(() => import("./mdx-CLkNPVgD.js"), true ? [] : void 0, import.meta.url)
209233
209261
  });
209234
209262
  registerLanguage({
209235
209263
  id: "mips",
@@ -209328,7 +209356,7 @@ registerLanguage({
209328
209356
  extensions: [".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi"],
209329
209357
  aliases: ["Python", "py"],
209330
209358
  firstLine: "^#!/.*\\bpython[0-9.-]*\\b",
209331
- loader: () => __vitePreload(() => import("./python-DcbM4Hy5.js"), true ? [] : void 0, import.meta.url)
209359
+ loader: () => __vitePreload(() => import("./python-DOJdL4kC.js"), true ? [] : void 0, import.meta.url)
209332
209360
  });
209333
209361
  registerLanguage({
209334
209362
  id: "qsharp",
@@ -209347,7 +209375,7 @@ registerLanguage({
209347
209375
  extensions: [".cshtml"],
209348
209376
  aliases: ["Razor", "razor"],
209349
209377
  mimetypes: ["text/x-cshtml"],
209350
- loader: () => __vitePreload(() => import("./razor-XJROxpYK.js"), true ? [] : void 0, import.meta.url)
209378
+ loader: () => __vitePreload(() => import("./razor-CSr8UlJc.js"), true ? [] : void 0, import.meta.url)
209351
209379
  });
209352
209380
  registerLanguage({
209353
209381
  id: "redis",
@@ -209480,7 +209508,7 @@ registerLanguage({
209480
209508
  aliases: ["TypeScript", "ts", "typescript"],
209481
209509
  mimetypes: ["text/typescript"],
209482
209510
  loader: () => {
209483
- return __vitePreload(() => import("./typescript-DhTtFBLt.js"), true ? [] : void 0, import.meta.url);
209511
+ return __vitePreload(() => import("./typescript-r5QB4yRW.js"), true ? [] : void 0, import.meta.url);
209484
209512
  }
209485
209513
  });
209486
209514
  registerLanguage({
@@ -209525,14 +209553,14 @@ registerLanguage({
209525
209553
  firstLine: "(\\<\\?xml.*)|(\\<svg)|(\\<\\!doctype\\s+svg)",
209526
209554
  aliases: ["XML", "xml"],
209527
209555
  mimetypes: ["text/xml", "application/xml", "application/xaml+xml", "application/xml-dtd"],
209528
- loader: () => __vitePreload(() => import("./xml-BLfEkVl1.js"), true ? [] : void 0, import.meta.url)
209556
+ loader: () => __vitePreload(() => import("./xml-CYhKh3Pi.js"), true ? [] : void 0, import.meta.url)
209529
209557
  });
209530
209558
  registerLanguage({
209531
209559
  id: "yaml",
209532
209560
  extensions: [".yaml", ".yml"],
209533
209561
  aliases: ["YAML", "yaml", "YML", "yml"],
209534
209562
  mimetypes: ["application/x-yaml", "text/x-yaml"],
209535
- loader: () => __vitePreload(() => import("./yaml-DpzsqvWa.js"), true ? [] : void 0, import.meta.url)
209563
+ loader: () => __vitePreload(() => import("./yaml-B8DsoaYW.js"), true ? [] : void 0, import.meta.url)
209536
209564
  });
209537
209565
  var __defProp = Object.defineProperty;
209538
209566
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -1,5 +1,5 @@
1
- import { conf as conf$1, language as language$1 } from "./typescript-DhTtFBLt.js";
2
- import "./index-D65QIRev.js";
1
+ import { conf as conf$1, language as language$1 } from "./typescript-r5QB4yRW.js";
2
+ import "./index-HoDaBul3.js";
3
3
  const conf = conf$1;
4
4
  const language = {
5
5
  // Set defaultToken to invalid to see what you do not tokenize yet
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages, e as editor } from "./index-D65QIRev.js";
2
- import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-IDD0-KWa.js";
3
- import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-IDD0-KWa.js";
1
+ import { c as createWebWorker, l as languages, e as editor } from "./index-HoDaBul3.js";
2
+ import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-Bb27EY8c.js";
3
+ import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-Bb27EY8c.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-D65QIRev.js";
1
+ import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-HoDaBul3.js";
2
2
  var DocumentUri;
3
3
  (function(DocumentUri2) {
4
4
  function is(value) {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  blockComment: ["{/*", "*/}"]
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  lineComment: "#",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-D65QIRev.js";
1
+ import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-HoDaBul3.js";
2
2
  class WorkerManager {
3
3
  constructor(_modeId, _defaults) {
4
4
  this._modeId = _modeId;
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const conf = {
3
3
  wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
4
4
  comments: {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  blockComment: ["<!--", "-->"]
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-HoDaBul3.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  lineComment: "#"
@@ -106,7 +106,7 @@
106
106
  :root[data-theme='catppuccin-latte'] .boot-splash-title { color: #2f3145; }
107
107
  :root[data-theme='catppuccin-latte'] .boot-splash-label { color: #7c7f93; }
108
108
  </style>
109
- <script type="module" crossorigin src="./assets/index-D65QIRev.js"></script>
109
+ <script type="module" crossorigin src="./assets/index-HoDaBul3.js"></script>
110
110
  <link rel="stylesheet" crossorigin href="./assets/index-DID1LlMU.css">
111
111
  </head>
112
112
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
5
  "main": "out/main/index.js",
6
6
  "bin": {
@@ -0,0 +1,64 @@
1
+ // Exercise the actual React terminal panel and xterm parser in Electron.
2
+ // Run: node_modules/.bin/electron scripts/test-terminal-scroll.cjs
3
+ const { app, BrowserWindow } = require('electron')
4
+ const { build } = require('esbuild')
5
+ const fs = require('node:fs/promises')
6
+ const os = require('node:os')
7
+ const path = require('node:path')
8
+ app.on('window-all-closed', () => {})
9
+ app.on('child-process-gone', (_event, details) => console.error('Child process exited:', details))
10
+
11
+ async function main() {
12
+ console.log('Starting terminal scroll regression checks...')
13
+ const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'ctlsurf-scroll-'))
14
+ app.setPath('userData', path.join(temp, 'profile'))
15
+ let win
16
+ try {
17
+ await build({
18
+ entryPoints: [path.join(__dirname, '../repro-terminal-scroll.tsx')],
19
+ outfile: path.join(temp, 'test.js'),
20
+ bundle: true,
21
+ platform: 'browser',
22
+ jsx: 'automatic',
23
+ external: ['@huggingface/transformers'],
24
+ })
25
+ await fs.writeFile(path.join(temp, 'index.html'), `<!doctype html><meta charset="UTF-8">
26
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'">
27
+ <link rel="stylesheet" href="test.css">
28
+ <style>
29
+ html, body, #root { margin: 0; width: 100%; height: 100%; }
30
+ .terminal-wrap, .terminal-container, .xterm { width: 100%; height: 100%; }
31
+ .terminal-wrap { position: relative; }
32
+ .term-jump-latest { position: absolute; bottom: 0; right: 0; }
33
+ </style>
34
+ <div id="root"></div><script src="test.js"></script>`)
35
+ await app.whenReady()
36
+ win = new BrowserWindow({
37
+ show: false, width: 900, height: 500,
38
+ webPreferences: { backgroundThrottling: false },
39
+ })
40
+ win.webContents.on('console-message', (event) => {
41
+ if (event.level === 'error') console.error(event.message)
42
+ })
43
+ win.webContents.on('render-process-gone', (_event, details) => console.error('Renderer exited:', details))
44
+ await win.loadFile(path.join(temp, 'index.html'))
45
+ let timeout
46
+ const result = await Promise.race([
47
+ win.webContents.executeJavaScript('window.scrollTest'),
48
+ new Promise((_, reject) => {
49
+ timeout = setTimeout(() => reject(new Error('Scroll test timed out')), 15_000)
50
+ }),
51
+ ]).finally(() => clearTimeout(timeout))
52
+ for (const line of result.checks) console.log(line)
53
+ if (result.error) throw new Error(result.error)
54
+ console.log('Terminal scroll regression checks passed.')
55
+ } finally {
56
+ win?.destroy()
57
+ await fs.rm(temp, { recursive: true, force: true, maxRetries: 5 })
58
+ }
59
+ }
60
+
61
+ main().then(() => app.exit(0), (err) => {
62
+ console.error(err)
63
+ app.exit(1)
64
+ })
@@ -127,7 +127,7 @@ type TermState = {
127
127
  // (neither down to the bottom, nor up to the top — the old bug).
128
128
  anchorLine: number | null
129
129
  // One-shot flag set by a real user scroll gesture (wheel / scroll keys) and
130
- // consumed by the very next onScroll, so a redraw firing onScroll right after
130
+ // consumed by the next terminal/viewport scroll, so a redraw right after
131
131
  // a gesture isn't mistaken for one. Spinner redraws (~10x/s while "thinking")
132
132
  // would otherwise be misread as user intent and re-pin to the bottom.
133
133
  userScrollPending: boolean
@@ -190,6 +190,22 @@ function markGesture(tabId: string): void {
190
190
  if (s) s.userScrollPending = true
191
191
  }
192
192
 
193
+ function followLatest(state: TermState): void {
194
+ state.userScrollPending = false
195
+ setPinned(state, true)
196
+ reassert(state)
197
+ }
198
+
199
+ function rememberScrollPosition(state: TermState): void {
200
+ state.userScrollPending = false
201
+ if (isAtBottom(state.terminal)) {
202
+ setPinned(state, true)
203
+ } else {
204
+ state.anchorLine = state.terminal.buffer.active.viewportY
205
+ setPinned(state, false)
206
+ }
207
+ }
208
+
193
209
  function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): { terminal: Terminal; fitAddon: FitAddon } {
194
210
  const existing = _terminals.get(tabId)
195
211
  if (existing) {
@@ -216,7 +232,9 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
216
232
  fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', monospace",
217
233
  theme: currentTheme().terminal,
218
234
  scrollback: 10000,
219
- scrollOnUserInput: true,
235
+ // Follow explicit keyboard/paste input below. xterm also counts mouse
236
+ // reports as user input, which must not pull someone out of scrollback.
237
+ scrollOnUserInput: false,
220
238
  allowTransparency: false
221
239
  })
222
240
 
@@ -302,19 +320,14 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
302
320
  reassert(state)
303
321
  return
304
322
  }
305
- state.userScrollPending = false
306
- if (isAtBottom(terminal)) {
307
- setPinned(state, true)
308
- } else {
309
- state.anchorLine = terminal.buffer.active.viewportY
310
- setPinned(state, false)
311
- }
323
+ rememberScrollPosition(state)
312
324
  })
313
325
 
314
- // Send keystrokes to pty — typing always returns to (and follows) the bottom.
326
+ // onData includes automatic cursor/device/color replies and focus/mouse
327
+ // reports, not just keystrokes. Codex's terminal queries used to re-enable
328
+ // follow mode here, snapping the viewport down while reading old output.
329
+ terminal.onKey(() => followLatest(state))
315
330
  terminal.onData((data) => {
316
- setPinned(state, true)
317
- terminal.scrollToBottom()
318
331
  window.worker.writePty(tabId, data)
319
332
  })
320
333
 
@@ -394,8 +407,7 @@ export function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, i
394
407
  const jumpToLatest = () => {
395
408
  const s = _terminals.get(tabId)
396
409
  if (!s) return
397
- setPinned(s, true)
398
- s.terminal.scrollToBottom()
410
+ followLatest(s)
399
411
  s.terminal.focus()
400
412
  }
401
413
 
@@ -496,15 +508,33 @@ export function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, i
496
508
  const attached = _terminals.get(tabId)
497
509
  if (attached) reassert(attached)
498
510
 
499
- // Wheel over the terminal is an explicit user scroll — stamp it so onScroll
500
- // treats the resulting move as intentional (not an involuntary redraw jump).
501
- // Capture is important: xterm handles wheel events on a descendant and
502
- // emits onScroll synchronously. A bubbling listener runs too late, causing
503
- // streamed Codex output to mistake the wheel scroll for a redraw and snap
504
- // the viewport straight back to the bottom.
511
+ // Wheel handling starts inside xterm, so record intent in capture phase.
505
512
  const onWheel = () => markGesture(tabId)
506
513
  terminal.element?.addEventListener('wheel', onWheel, { capture: true, passive: true })
507
514
 
515
+ // xterm 5.5 suppresses its public onScroll event for native viewport
516
+ // scrolling. Observe the DOM event after xterm updates buffer.viewportY,
517
+ // otherwise a wheel gesture never pauses follow mode before the next write.
518
+ const viewport = terminal.element?.querySelector('.xterm-viewport')
519
+ const onViewportScroll = () => {
520
+ const state = _terminals.get(tabId)
521
+ if (state && !state.holding && state.userScrollPending) rememberScrollPosition(state)
522
+ }
523
+ viewport?.addEventListener('scroll', onViewportScroll)
524
+
525
+ // Paste and IME/emoji input can bypass onKey. Capture them before xterm
526
+ // handles the event (paste stops propagation inside xterm).
527
+ const onTextInput = () => {
528
+ const state = _terminals.get(tabId)
529
+ if (state) followLatest(state)
530
+ }
531
+ const onPaste = (event: ClipboardEvent) => {
532
+ if (event.clipboardData?.getData('text/plain')) onTextInput()
533
+ }
534
+ terminal.textarea?.addEventListener('paste', onPaste, true)
535
+ terminal.textarea?.addEventListener('input', onTextInput, true)
536
+ terminal.textarea?.addEventListener('compositionend', onTextInput, true)
537
+
508
538
  // Resize handling — skip when the container is hidden (display:none → 0x0),
509
539
  // otherwise fit() reports a tiny size and the PTY gets stuck at ~4 cols
510
540
  // until the user switches back.
@@ -533,6 +563,10 @@ export function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, i
533
563
  window.removeEventListener('resize', handleResize)
534
564
  observer.disconnect()
535
565
  terminal.element?.removeEventListener('wheel', onWheel, true)
566
+ viewport?.removeEventListener('scroll', onViewportScroll)
567
+ terminal.textarea?.removeEventListener('paste', onPaste, true)
568
+ terminal.textarea?.removeEventListener('input', onTextInput, true)
569
+ terminal.textarea?.removeEventListener('compositionend', onTextInput, true)
536
570
  // Do NOT dispose terminal or unsub PTY listeners — they persist across remounts
537
571
  }
538
572
  }, [tabId, onExit])