@heliohq/helio 0.1.2

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 (114) hide show
  1. package/bin/helio +3 -0
  2. package/dist/commands/assistant/chat.js +6435 -0
  3. package/dist/commands/assistant/chat.js.map +1 -0
  4. package/dist/commands/assistant/connect/lark.js +6257 -0
  5. package/dist/commands/assistant/connect/lark.js.map +1 -0
  6. package/dist/commands/assistant/connect/slack.js +6322 -0
  7. package/dist/commands/assistant/connect/slack.js.map +1 -0
  8. package/dist/commands/assistant/connect/wechat.js +6258 -0
  9. package/dist/commands/assistant/connect/wechat.js.map +1 -0
  10. package/dist/commands/assistant/create.js +6190 -0
  11. package/dist/commands/assistant/create.js.map +1 -0
  12. package/dist/commands/assistant/delete.js +6176 -0
  13. package/dist/commands/assistant/delete.js.map +1 -0
  14. package/dist/commands/assistant/disconnect.js +6302 -0
  15. package/dist/commands/assistant/disconnect.js.map +1 -0
  16. package/dist/commands/assistant/list.js +6192 -0
  17. package/dist/commands/assistant/list.js.map +1 -0
  18. package/dist/commands/assistant/messages.js +6488 -0
  19. package/dist/commands/assistant/messages.js.map +1 -0
  20. package/dist/commands/assistant/send.js +6468 -0
  21. package/dist/commands/assistant/send.js.map +1 -0
  22. package/dist/commands/assistant/skills/add.js +6296 -0
  23. package/dist/commands/assistant/skills/add.js.map +1 -0
  24. package/dist/commands/assistant/skills/list.js +6258 -0
  25. package/dist/commands/assistant/skills/list.js.map +1 -0
  26. package/dist/commands/assistant/skills/remove.js +6288 -0
  27. package/dist/commands/assistant/skills/remove.js.map +1 -0
  28. package/dist/commands/auth/login.js +6327 -0
  29. package/dist/commands/auth/login.js.map +1 -0
  30. package/dist/commands/auth/logout.js +6174 -0
  31. package/dist/commands/auth/logout.js.map +1 -0
  32. package/dist/commands/auth/status.js +6205 -0
  33. package/dist/commands/auth/status.js.map +1 -0
  34. package/dist/commands/channel/chat.js +6341 -0
  35. package/dist/commands/channel/chat.js.map +1 -0
  36. package/dist/commands/channel/create.js +6199 -0
  37. package/dist/commands/channel/create.js.map +1 -0
  38. package/dist/commands/channel/delete.js +6178 -0
  39. package/dist/commands/channel/delete.js.map +1 -0
  40. package/dist/commands/channel/list.js +6207 -0
  41. package/dist/commands/channel/list.js.map +1 -0
  42. package/dist/commands/channel/members/add.js +6235 -0
  43. package/dist/commands/channel/members/add.js.map +1 -0
  44. package/dist/commands/channel/members/index.js +6198 -0
  45. package/dist/commands/channel/members/index.js.map +1 -0
  46. package/dist/commands/channel/members/remove.js +6237 -0
  47. package/dist/commands/channel/members/remove.js.map +1 -0
  48. package/dist/commands/channel/messages.js +6261 -0
  49. package/dist/commands/channel/messages.js.map +1 -0
  50. package/dist/commands/channel/send.js +6171 -0
  51. package/dist/commands/channel/send.js.map +1 -0
  52. package/dist/commands/claude.js +11928 -0
  53. package/dist/commands/claude.js.map +1 -0
  54. package/dist/commands/codex.js +11928 -0
  55. package/dist/commands/codex.js.map +1 -0
  56. package/dist/commands/config/init.js +6186 -0
  57. package/dist/commands/config/init.js.map +1 -0
  58. package/dist/commands/config/profile/create.js +6207 -0
  59. package/dist/commands/config/profile/create.js.map +1 -0
  60. package/dist/commands/config/profile/delete.js +6183 -0
  61. package/dist/commands/config/profile/delete.js.map +1 -0
  62. package/dist/commands/config/profile/list.js +6204 -0
  63. package/dist/commands/config/profile/list.js.map +1 -0
  64. package/dist/commands/config/show.js +6187 -0
  65. package/dist/commands/config/show.js.map +1 -0
  66. package/dist/commands/marketplace/info.js +6325 -0
  67. package/dist/commands/marketplace/info.js.map +1 -0
  68. package/dist/commands/marketplace/list.js +6288 -0
  69. package/dist/commands/marketplace/list.js.map +1 -0
  70. package/dist/commands/task/assign.js +6254 -0
  71. package/dist/commands/task/assign.js.map +1 -0
  72. package/dist/commands/task/claim.js +6244 -0
  73. package/dist/commands/task/claim.js.map +1 -0
  74. package/dist/commands/task/create.js +6249 -0
  75. package/dist/commands/task/create.js.map +1 -0
  76. package/dist/commands/task/done.js +6244 -0
  77. package/dist/commands/task/done.js.map +1 -0
  78. package/dist/commands/task/list.js +6281 -0
  79. package/dist/commands/task/list.js.map +1 -0
  80. package/dist/commands/task/reject.js +6244 -0
  81. package/dist/commands/task/reject.js.map +1 -0
  82. package/dist/commands/task/review.js +6244 -0
  83. package/dist/commands/task/review.js.map +1 -0
  84. package/dist/commands/task/show.js +6250 -0
  85. package/dist/commands/task/show.js.map +1 -0
  86. package/dist/commands/task/unclaim.js +6244 -0
  87. package/dist/commands/task/unclaim.js.map +1 -0
  88. package/dist/commands/update.js +6229 -0
  89. package/dist/commands/update.js.map +1 -0
  90. package/dist/help.js +6168 -0
  91. package/dist/help.js.map +1 -0
  92. package/dist/index.js +2 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/lib/channel-helper.js +11893 -0
  95. package/dist/lib/channel-helper.js.map +1 -0
  96. package/dist/pty/broker-transport.js +10271 -0
  97. package/dist/pty/broker-transport.js.map +1 -0
  98. package/dist/pty/client.js +7312 -0
  99. package/dist/pty/client.js.map +1 -0
  100. package/dist/pty/clipboard.js +39 -0
  101. package/dist/pty/clipboard.js.map +1 -0
  102. package/dist/pty/errors.js +57 -0
  103. package/dist/pty/errors.js.map +1 -0
  104. package/dist/pty/protocol.js +18 -0
  105. package/dist/pty/protocol.js.map +1 -0
  106. package/dist/pty/screen-renderer.js +492 -0
  107. package/dist/pty/screen-renderer.js.map +1 -0
  108. package/dist/pty/terminal.js +24 -0
  109. package/dist/pty/terminal.js.map +1 -0
  110. package/dist/pty/transport.js +2 -0
  111. package/dist/pty/transport.js.map +1 -0
  112. package/dist/types/qrcode-terminal.d.js +2 -0
  113. package/dist/types/qrcode-terminal.d.js.map +1 -0
  114. package/package.json +55 -0
@@ -0,0 +1,39 @@
1
+ import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);
2
+
3
+ // src/pty/clipboard.ts
4
+ import { execSync } from "child_process";
5
+ import * as fs from "fs";
6
+ import * as os from "os";
7
+ function readClipboardImage() {
8
+ if (os.platform() !== "darwin") {
9
+ return "";
10
+ }
11
+ try {
12
+ return readClipboardImageDarwin();
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+ function readClipboardImageDarwin() {
18
+ const tmpFile = "/tmp/.ship-clipboard-local.png";
19
+ try {
20
+ execSync("osascript -e 'the clipboard as \xABclass PNGf\xBB'", {
21
+ stdio: "pipe"
22
+ });
23
+ execSync(
24
+ `osascript -e 'set png_data to (the clipboard as \xABclass PNGf\xBB)' -e 'set fp to open for access POSIX file "${tmpFile}" with write permission' -e 'write png_data to fp' -e 'close access fp'`,
25
+ { stdio: "pipe" }
26
+ );
27
+ const data = fs.readFileSync(tmpFile);
28
+ return data.toString("base64");
29
+ } finally {
30
+ try {
31
+ fs.unlinkSync(tmpFile);
32
+ } catch {
33
+ }
34
+ }
35
+ }
36
+ export {
37
+ readClipboardImage
38
+ };
39
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pty/clipboard.ts"],"sourcesContent":["import { execSync } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\n\n/**\n * Read an image from the system clipboard.\n * Returns base64-encoded PNG data, or empty string if no image is available.\n * Only supported on macOS.\n */\nexport function readClipboardImage(): string {\n if (os.platform() !== 'darwin') {\n return '';\n }\n\n try {\n return readClipboardImageDarwin();\n } catch {\n return '';\n }\n}\n\nfunction readClipboardImageDarwin(): string {\n const tmpFile = '/tmp/.ship-clipboard-local.png';\n\n try {\n // Check if clipboard has an image\n execSync(\"osascript -e 'the clipboard as «class PNGf»'\", {\n stdio: 'pipe',\n });\n\n // Save clipboard image to temp file\n execSync(\n `osascript -e 'set png_data to (the clipboard as «class PNGf»)' -e 'set fp to open for access POSIX file \"${tmpFile}\" with write permission' -e 'write png_data to fp' -e 'close access fp'`,\n { stdio: 'pipe' },\n );\n\n const data = fs.readFileSync(tmpFile);\n return data.toString('base64');\n } finally {\n try {\n fs.unlinkSync(tmpFile);\n } catch {\n // ignore\n }\n }\n}\n"],"mappings":";;;AAAA,SAAS,gBAAgB;AACzB,YAAY,QAAQ;AACpB,YAAY,QAAQ;AAOb,SAAS,qBAA6B;AAC3C,MAAO,YAAS,MAAM,UAAU;AAC9B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,yBAAyB;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,2BAAmC;AAC1C,QAAM,UAAU;AAEhB,MAAI;AAEF,aAAS,sDAAgD;AAAA,MACvD,OAAO;AAAA,IACT,CAAC;AAGD;AAAA,MACE,kHAA4G,OAAO;AAAA,MACnH,EAAE,OAAO,OAAO;AAAA,IAClB;AAEA,UAAM,OAAU,gBAAa,OAAO;AACpC,WAAO,KAAK,SAAS,QAAQ;AAAA,EAC/B,UAAE;AACA,QAAI;AACF,MAAG,cAAW,OAAO;AAAA,IACvB,QAAQ;AAAA,IAER;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,57 @@
1
+ import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);
2
+
3
+ // src/pty/errors.ts
4
+ var PTYAPIError = class extends Error {
5
+ statusCode;
6
+ operation;
7
+ body;
8
+ hint;
9
+ constructor(statusCode, operation, body, hint) {
10
+ const msg = body ? `${operation} (${statusCode}) ${body}` : `${operation} (${statusCode})`;
11
+ super(msg);
12
+ this.name = "PTYAPIError";
13
+ this.statusCode = statusCode;
14
+ this.operation = operation;
15
+ this.body = body;
16
+ this.hint = hint ?? "";
17
+ }
18
+ };
19
+ function hintForStatus(code) {
20
+ switch (code) {
21
+ case 401:
22
+ return "run `helio auth login` to re-authenticate";
23
+ case 403:
24
+ return "you may not have permission for this operation";
25
+ case 404:
26
+ return "the resource was not found";
27
+ case 429:
28
+ return "rate limited \u2014 try again shortly";
29
+ case 500:
30
+ case 502:
31
+ case 503:
32
+ return "the backend may be temporarily unavailable, try again shortly";
33
+ default:
34
+ return "";
35
+ }
36
+ }
37
+ function newPTYAPIError(statusCode, operation, body) {
38
+ const hint = hintForStatus(statusCode);
39
+ return new PTYAPIError(statusCode, operation, body, hint);
40
+ }
41
+ function formatError(err) {
42
+ if (err instanceof PTYAPIError) {
43
+ let msg = `helio: ${err.message}`;
44
+ if (err.hint) {
45
+ msg += `
46
+ Tip: ${err.hint}`;
47
+ }
48
+ return msg;
49
+ }
50
+ return `helio: ${err.message}`;
51
+ }
52
+ export {
53
+ PTYAPIError,
54
+ formatError,
55
+ newPTYAPIError
56
+ };
57
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pty/errors.ts"],"sourcesContent":["/**\n * APIError wraps an HTTP API error with a user-facing hint.\n */\nexport class PTYAPIError extends Error {\n statusCode: number;\n operation: string;\n body: string;\n hint: string;\n\n constructor(statusCode: number, operation: string, body: string, hint?: string) {\n const msg = body ? `${operation} (${statusCode}) ${body}` : `${operation} (${statusCode})`;\n super(msg);\n this.name = 'PTYAPIError';\n this.statusCode = statusCode;\n this.operation = operation;\n this.body = body;\n this.hint = hint ?? '';\n }\n}\n\nfunction hintForStatus(code: number): string {\n switch (code) {\n case 401:\n return 'run `helio auth login` to re-authenticate';\n case 403:\n return 'you may not have permission for this operation';\n case 404:\n return 'the resource was not found';\n case 429:\n return 'rate limited — try again shortly';\n case 500:\n case 502:\n case 503:\n return 'the backend may be temporarily unavailable, try again shortly';\n default:\n return '';\n }\n}\n\n/**\n * Create a PTYAPIError with an appropriate hint based on status code.\n */\nexport function newPTYAPIError(\n statusCode: number,\n operation: string,\n body: string,\n): PTYAPIError {\n const hint = hintForStatus(statusCode);\n return new PTYAPIError(statusCode, operation, body, hint);\n}\n\n/**\n * Format an error for display on stderr, including any hint.\n */\nexport function formatError(err: Error): string {\n if (err instanceof PTYAPIError) {\n let msg = `helio: ${err.message}`;\n if (err.hint) {\n msg += `\\n Tip: ${err.hint}`;\n }\n return msg;\n }\n return `helio: ${err.message}`;\n}\n"],"mappings":";;;AAGO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,YAAoB,WAAmB,MAAc,MAAe;AAC9E,UAAM,MAAM,OAAO,GAAG,SAAS,KAAK,UAAU,KAAK,IAAI,KAAK,GAAG,SAAS,KAAK,UAAU;AACvF,UAAM,GAAG;AACT,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,YAAY;AACjB,SAAK,OAAO;AACZ,SAAK,OAAO,QAAQ;AAAA,EACtB;AACF;AAEA,SAAS,cAAc,MAAsB;AAC3C,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAKO,SAAS,eACd,YACA,WACA,MACa;AACb,QAAM,OAAO,cAAc,UAAU;AACrC,SAAO,IAAI,YAAY,YAAY,WAAW,MAAM,IAAI;AAC1D;AAKO,SAAS,YAAY,KAAoB;AAC9C,MAAI,eAAe,aAAa;AAC9B,QAAI,MAAM,UAAU,IAAI,OAAO;AAC/B,QAAI,IAAI,MAAM;AACZ,aAAO;AAAA,SAAY,IAAI,IAAI;AAAA,IAC7B;AACA,WAAO;AAAA,EACT;AACA,SAAO,UAAU,IAAI,OAAO;AAC9B;","names":[]}
@@ -0,0 +1,18 @@
1
+ import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);
2
+
3
+ // src/pty/protocol.ts
4
+ var CONTROL_EXIT = "exit";
5
+ var CONTROL_CLIPBOARD = "clipboard";
6
+ var CONTROL_ROLE = "role";
7
+ var CONTROL_PRIMARY_DISCONNECTED = "primary_disconnected";
8
+ var CONTROL_TAKEOVER = "takeover";
9
+ var CONTROL_TAKEOVER_DENIED = "takeover_denied";
10
+ export {
11
+ CONTROL_CLIPBOARD,
12
+ CONTROL_EXIT,
13
+ CONTROL_PRIMARY_DISCONNECTED,
14
+ CONTROL_ROLE,
15
+ CONTROL_TAKEOVER,
16
+ CONTROL_TAKEOVER_DENIED
17
+ };
18
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pty/protocol.ts"],"sourcesContent":["/**\n * Control message type constants used by PTYClient to handle\n * inbound control messages from the PTY bridge.\n */\nexport const CONTROL_EXIT = 'exit';\nexport const CONTROL_CLIPBOARD = 'clipboard';\nexport const CONTROL_ROLE = 'role';\nexport const CONTROL_PRIMARY_DISCONNECTED = 'primary_disconnected';\nexport const CONTROL_TAKEOVER = 'takeover';\nexport const CONTROL_TAKEOVER_DENIED = 'takeover_denied';\n"],"mappings":";;;AAIO,IAAM,eAAe;AACrB,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AACrB,IAAM,+BAA+B;AACrC,IAAM,mBAAmB;AACzB,IAAM,0BAA0B;","names":[]}
@@ -0,0 +1,492 @@
1
+ import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);
2
+
3
+ // src/pty/screen-renderer.ts
4
+ import xtermHeadless from "@xterm/headless";
5
+ var Terminal = xtermHeadless.Terminal;
6
+ var ScreenRenderer = class _ScreenRenderer {
7
+ term;
8
+ writeBuffer;
9
+ // xterm internal API — depends on @xterm/headless ^5.5.0 internals
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ core;
12
+ overlayTimer = null;
13
+ pendingOverlay = "";
14
+ incompleteEsc = false;
15
+ vterm;
16
+ lastFrame = [];
17
+ cols;
18
+ rows;
19
+ renderTimer = null;
20
+ /** Render batch delay in ms — matches Ink's 16ms (60fps) throttle to prevent
21
+ * partial-frame rendering when TUI frameworks send rapid incremental updates. */
22
+ static RENDER_BATCH_MS = 16;
23
+ /** Last seen baseY — when it changes the viewport shifted and lastFrame must be invalidated. */
24
+ lastBaseY = 0;
25
+ /** Last observed content height (last non-blank row + 1). Used to detect
26
+ * dialog dismiss events where content shrinks significantly. */
27
+ lastContentHeight = 0;
28
+ /** Callback fired when content height drops significantly (e.g., dialog dismissed).
29
+ * The PTYClient uses this to send a resize trick to force Ink to relayout. */
30
+ onContentShrink = null;
31
+ scrollOffset = 0;
32
+ // 0 = bottom (live), >0 = scrolled up
33
+ scrollCursorY = 0;
34
+ // cursor row within viewport (0-indexed)
35
+ scrollCursorX = 0;
36
+ // actual display col (clamped to content)
37
+ scrollDesiredX = 0;
38
+ // remembered col for up/down navigation
39
+ constructor(cols, rows) {
40
+ this.cols = cols;
41
+ this.rows = rows;
42
+ this.vterm = !process.env["HELIO_PASSTHROUGH"];
43
+ this.term = new Terminal({ cols, rows, scrollback: 1e4, allowProposedApi: true });
44
+ this.core = this.term["_core"];
45
+ this.writeBuffer = this.core._writeBuffer;
46
+ this.term.parser.registerCsiHandler({ prefix: "?", final: "h" }, (params) => {
47
+ if (params[0] === 1049) this.lastFrame = [];
48
+ return false;
49
+ });
50
+ this.term.parser.registerCsiHandler({ prefix: "?", final: "l" }, (params) => {
51
+ if (params[0] === 1049) this.lastFrame = [];
52
+ return false;
53
+ });
54
+ }
55
+ /**
56
+ * Relay terminal query responses from xterm to the agent PTY.
57
+ * Also intercepts queries xterm can't answer (OSC 10 foreground color)
58
+ * and provides default responses to prevent agent startup delays.
59
+ */
60
+ setupResponseRelay(sendToAgent) {
61
+ this.term.onData((data) => {
62
+ sendToAgent(data);
63
+ });
64
+ this.term.parser.registerOscHandler(10, (data) => {
65
+ if (data === "?") {
66
+ sendToAgent("\x1B]10;rgb:ffff/ffff/ffff\x1B\\");
67
+ }
68
+ return false;
69
+ });
70
+ }
71
+ write(data, statusBarEsc) {
72
+ const str = typeof data === "string" ? data : new TextDecoder().decode(data);
73
+ this.writeBuffer.writeSync(str, false);
74
+ if (this.scrollOffset > 0) {
75
+ this.scrollOffset = 0;
76
+ this.lastFrame = [];
77
+ }
78
+ if (this.vterm) {
79
+ this.pendingOverlay = statusBarEsc;
80
+ if (!this.renderTimer) {
81
+ this.renderTimer = setTimeout(() => {
82
+ this.renderTimer = null;
83
+ this.vtermRender();
84
+ }, _ScreenRenderer.RENDER_BATCH_MS);
85
+ }
86
+ } else {
87
+ process.stdout.write(data);
88
+ this.incompleteEsc = this.endsWithIncompleteEsc(str);
89
+ if (statusBarEsc) {
90
+ this.pendingOverlay = statusBarEsc;
91
+ if (!this.overlayTimer) {
92
+ this.overlayTimer = setImmediate(() => {
93
+ this.overlayTimer = null;
94
+ if (!this.incompleteEsc) {
95
+ this.flushOverlay();
96
+ }
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ /** Vterm render (legacy, no scroll). */
103
+ vtermRender() {
104
+ const buf = this.term.buffer.active;
105
+ let output = "";
106
+ if (buf.baseY !== this.lastBaseY) {
107
+ this.lastFrame = [];
108
+ this.lastBaseY = buf.baseY;
109
+ }
110
+ for (let y = 0; y < this.rows; y++) {
111
+ const line = buf.getLine(buf.baseY + y);
112
+ if (!line) {
113
+ if (this.lastFrame[y] !== void 0) {
114
+ output += `\x1B[${y + 1};1H\x1B[2K`;
115
+ delete this.lastFrame[y];
116
+ }
117
+ continue;
118
+ }
119
+ const rendered = this.renderLine(line);
120
+ if (this.lastFrame[y] === rendered) continue;
121
+ this.lastFrame[y] = rendered;
122
+ output += `\x1B[${y + 1};1H\x1B[2K${rendered}`;
123
+ }
124
+ let contentHeight = 0;
125
+ for (let y = this.rows - 1; y >= 0; y--) {
126
+ const line = buf.getLine(buf.baseY + y);
127
+ if (line && line.translateToString(true).length > 0) {
128
+ contentHeight = y + 1;
129
+ break;
130
+ }
131
+ }
132
+ const prevHeight = this.lastContentHeight;
133
+ this.lastContentHeight = contentHeight;
134
+ const shrinkThreshold = this.rows * 0.6;
135
+ const shrunkTo = this.rows * 0.4;
136
+ if (prevHeight > shrinkThreshold && contentHeight < shrunkTo && this.onContentShrink) {
137
+ setTimeout(() => this.onContentShrink?.(), 50);
138
+ }
139
+ const cx = buf.cursorX;
140
+ const cy = buf.cursorY;
141
+ const cursorHidden = this.core?.coreService?.isCursorHidden ?? false;
142
+ const cursorSuffix = cursorHidden ? "\x1B[?25l" : "\x1B[?25h";
143
+ output += this.pendingOverlay;
144
+ output += `\x1B[${cy + 1};${cx + 1}H${cursorSuffix}`;
145
+ if (output) {
146
+ process.stdout.write(output);
147
+ }
148
+ }
149
+ renderLine(line) {
150
+ let result = "";
151
+ let prevSgr = "";
152
+ const cell = line.getCell(0);
153
+ if (!cell) return line.translateToString(true);
154
+ for (let x = 0; x < this.cols; x++) {
155
+ line.getCell(x, cell);
156
+ if (!cell) break;
157
+ if (cell.getWidth() === 0) continue;
158
+ const sgr = this.cellSGR(cell);
159
+ if (sgr !== prevSgr) {
160
+ result += "\x1B[0m" + sgr;
161
+ prevSgr = sgr;
162
+ }
163
+ result += cell.getChars() || " ";
164
+ }
165
+ result += "\x1B[0m";
166
+ return result;
167
+ }
168
+ cellSGR(cell) {
169
+ let sgr = "";
170
+ if (cell.isBold()) sgr += "\x1B[1m";
171
+ if (cell.isDim()) sgr += "\x1B[2m";
172
+ if (cell.isItalic()) sgr += "\x1B[3m";
173
+ if (cell.isUnderline()) sgr += "\x1B[4m";
174
+ if (cell.isInverse()) sgr += "\x1B[7m";
175
+ if (cell.isStrikethrough()) sgr += "\x1B[9m";
176
+ if (cell.isFgPalette()) {
177
+ const fg = cell.getFgColor();
178
+ if (fg < 8) sgr += `\x1B[${30 + fg}m`;
179
+ else if (fg < 16) sgr += `\x1B[${90 + fg - 8}m`;
180
+ else sgr += `\x1B[38;5;${fg}m`;
181
+ } else if (cell.isFgRGB()) {
182
+ const fg = cell.getFgColor();
183
+ sgr += `\x1B[38;2;${fg >> 16 & 255};${fg >> 8 & 255};${fg & 255}m`;
184
+ }
185
+ if (cell.isBgPalette()) {
186
+ const bg = cell.getBgColor();
187
+ if (bg < 8) sgr += `\x1B[${40 + bg}m`;
188
+ else if (bg < 16) sgr += `\x1B[${100 + bg - 8}m`;
189
+ else sgr += `\x1B[48;5;${bg}m`;
190
+ } else if (cell.isBgRGB()) {
191
+ const bg = cell.getBgColor();
192
+ sgr += `\x1B[48;2;${bg >> 16 & 255};${bg >> 8 & 255};${bg & 255}m`;
193
+ }
194
+ return sgr;
195
+ }
196
+ flushOverlay() {
197
+ if (!this.pendingOverlay) return;
198
+ const buf = this.term.buffer.active;
199
+ const cx = buf.cursorX;
200
+ const cy = buf.cursorY;
201
+ const cursorHidden = this.core?.coreService?.isCursorHidden ?? false;
202
+ const cursorSuffix = cursorHidden ? "\x1B[?25l" : "\x1B[?25h";
203
+ process.stdout.write(
204
+ `\x1B[?25l${this.pendingOverlay}\x1B[${cy + 1};${cx + 1}H${cursorSuffix}`
205
+ );
206
+ }
207
+ writeSilent(data) {
208
+ const str = typeof data === "string" ? data : new TextDecoder().decode(data);
209
+ this.writeBuffer.writeSync(str, false);
210
+ }
211
+ renderStatusBar(statusBarEsc) {
212
+ const buf = this.term.buffer.active;
213
+ const cx = buf.cursorX;
214
+ const cy = buf.cursorY;
215
+ const cursorHidden = this.core?.coreService?.isCursorHidden ?? false;
216
+ const cursorSuffix = cursorHidden ? "\x1B[?25l" : "\x1B[?25h";
217
+ process.stdout.write(
218
+ `\x1B[?25l${statusBarEsc}\x1B[${cy + 1};${cx + 1}H${cursorSuffix}`
219
+ );
220
+ }
221
+ endsWithIncompleteEsc(str) {
222
+ const lastEsc = str.lastIndexOf("\x1B");
223
+ if (lastEsc === -1) return false;
224
+ const tail = str.substring(lastEsc);
225
+ if (tail.length === 1) return true;
226
+ if (tail[1] === "[") {
227
+ for (let i = 2; i < tail.length; i++) {
228
+ const c = tail.charCodeAt(i);
229
+ if (c >= 64 && c <= 126) return false;
230
+ }
231
+ return true;
232
+ }
233
+ if (tail[1] === "]") {
234
+ if (tail.includes("\x1B\\") || tail.includes("\x07")) return false;
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+ /** Enter scroll mode: freeze viewport at current position, show cursor. */
240
+ enterScrollMode() {
241
+ const buf = this.term.buffer.active;
242
+ this.scrollOffset = 0;
243
+ this.scrollCursorY = buf.cursorY;
244
+ this.scrollCursorX = buf.cursorX;
245
+ this.scrollDesiredX = buf.cursorX;
246
+ const totalLines = buf.baseY + this.rows;
247
+ const currentLine = buf.baseY + this.scrollCursorY + 1;
248
+ const pos = `${currentLine}/${totalLines}`;
249
+ const sep = "\u2500".repeat(this.cols);
250
+ let output = "";
251
+ const pad = " ".repeat(this.cols);
252
+ output += `\x1B[${this.rows + 1};1H${pad}\x1B[${this.rows + 1};1H\x1B[90m${sep}\x1B[0m`;
253
+ output += `\x1B[${this.rows + 2};1H${pad}\x1B[${this.rows + 2};1H\x1B[7m [\u2191\u2193/hjkl] move [w/b] word [0/$] line [g/G] top/end [q/Esc] exit \x1B[0m`;
254
+ output += `\x1B[${this.rows + 2};${this.cols - pos.length - 1}H\x1B[43;30m ${pos} \x1B[0m`;
255
+ output += `\x1B[${this.scrollCursorY + 1};${this.scrollCursorX + 1}H\x1B[?25h`;
256
+ process.stdout.write(output);
257
+ }
258
+ /** Scroll up by n lines. Returns true if scrolled. */
259
+ scrollUp(n) {
260
+ const buf = this.term.buffer.active;
261
+ const maxScroll = buf.baseY;
262
+ const newOffset = Math.min(this.scrollOffset + n, maxScroll);
263
+ if (newOffset === this.scrollOffset) return false;
264
+ this.scrollOffset = newOffset;
265
+ this.lastFrame = [];
266
+ this.renderScrolled();
267
+ return true;
268
+ }
269
+ /** Scroll down by n lines. Returns true if scrolled. */
270
+ scrollDown(n) {
271
+ const newOffset = Math.max(this.scrollOffset - n, 0);
272
+ if (newOffset === this.scrollOffset) return false;
273
+ this.scrollOffset = newOffset;
274
+ this.lastFrame = [];
275
+ if (this.scrollOffset === 0) {
276
+ return true;
277
+ }
278
+ this.renderScrolled();
279
+ return true;
280
+ }
281
+ /** Move cursor up in scroll mode. Scrolls viewport if needed. */
282
+ moveCursorUp(n) {
283
+ this.scrollCursorY -= n;
284
+ if (this.scrollCursorY < 0) {
285
+ const overflow = -this.scrollCursorY;
286
+ this.scrollUp(overflow);
287
+ this.scrollCursorY = 0;
288
+ }
289
+ const lineLen = this.getScrollLineLength(this.scrollCursorY);
290
+ this.scrollCursorX = Math.min(this.scrollDesiredX, Math.max(lineLen - 1, 0));
291
+ this.renderScrolled();
292
+ }
293
+ /** Move cursor left. Wraps to end of previous line at col 0. */
294
+ moveCursorLeft(n) {
295
+ for (let i = 0; i < n; i++) {
296
+ if (this.scrollCursorX > 0) {
297
+ this.scrollCursorX--;
298
+ } else if (this.scrollCursorY > 0 || this.scrollOffset > 0) {
299
+ this.scrollCursorY--;
300
+ if (this.scrollCursorY < 0) {
301
+ this.scrollOffset = Math.min(this.scrollOffset + 1, this.term.buffer.active.baseY);
302
+ this.scrollCursorY = 0;
303
+ this.lastFrame = [];
304
+ }
305
+ const lineLen = this.getScrollLineLength(this.scrollCursorY);
306
+ this.scrollCursorX = Math.max(lineLen - 1, 0);
307
+ }
308
+ }
309
+ this.scrollDesiredX = this.scrollCursorX;
310
+ this.renderScrolled();
311
+ }
312
+ /** Move cursor right. Wraps to start of next line at end of content. */
313
+ moveCursorRight(n) {
314
+ for (let i = 0; i < n; i++) {
315
+ const lineLen = this.getScrollLineLength(this.scrollCursorY);
316
+ if (this.scrollCursorX < lineLen - 1) {
317
+ this.scrollCursorX++;
318
+ } else {
319
+ this.scrollCursorY++;
320
+ if (this.scrollCursorY >= this.rows) {
321
+ const overflow = this.scrollCursorY - this.rows + 1;
322
+ this.scrollOffset = Math.max(this.scrollOffset - overflow, 0);
323
+ this.scrollCursorY = this.rows - 1;
324
+ this.lastFrame = [];
325
+ }
326
+ this.scrollCursorX = 0;
327
+ }
328
+ }
329
+ this.scrollDesiredX = this.scrollCursorX;
330
+ this.renderScrolled();
331
+ }
332
+ /** Move cursor down in scroll mode. Scrolls viewport if needed. */
333
+ moveCursorDown(n) {
334
+ this.scrollCursorY += n;
335
+ if (this.scrollCursorY >= this.rows) {
336
+ const overflow = this.scrollCursorY - this.rows + 1;
337
+ this.scrollDown(overflow);
338
+ this.scrollCursorY = this.rows - 1;
339
+ }
340
+ const lineLen = this.getScrollLineLength(this.scrollCursorY);
341
+ this.scrollCursorX = Math.min(this.scrollDesiredX, Math.max(lineLen - 1, 0));
342
+ this.renderScrolled();
343
+ }
344
+ /** Get content length of a viewport row in scroll mode. */
345
+ getScrollLineLength(viewportY) {
346
+ const buf = this.term.buffer.active;
347
+ const viewStart = buf.baseY - this.scrollOffset;
348
+ const line = buf.getLine(viewStart + viewportY);
349
+ if (!line) return 0;
350
+ return line.translateToString(true).length;
351
+ }
352
+ /** Get text content of a viewport row in scroll mode. */
353
+ getScrollLineText(viewportY) {
354
+ const buf = this.term.buffer.active;
355
+ const viewStart = buf.baseY - this.scrollOffset;
356
+ const line = buf.getLine(viewStart + viewportY);
357
+ if (!line) return "";
358
+ return line.translateToString(true);
359
+ }
360
+ static WORD_SEPARATORS = new Set(
361
+ " `~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?".split("")
362
+ );
363
+ /** Move cursor to the start of the previous word (vim b / Alt+Left). */
364
+ moveCursorPrevWord() {
365
+ const seps = _ScreenRenderer.WORD_SEPARATORS;
366
+ let x = this.scrollCursorX;
367
+ let y = this.scrollCursorY;
368
+ let text = this.getScrollLineText(y);
369
+ x--;
370
+ if (x < 0) {
371
+ if (y > 0 || this.scrollOffset > 0) {
372
+ y--;
373
+ if (y < 0) {
374
+ this.scrollUp(1);
375
+ y = 0;
376
+ }
377
+ text = this.getScrollLineText(y);
378
+ x = Math.max(text.length - 1, 0);
379
+ } else {
380
+ x = 0;
381
+ }
382
+ }
383
+ while (x >= 0 && seps.has(text[x] ?? " ")) {
384
+ x--;
385
+ if (x < 0) {
386
+ if (y > 0 || this.scrollOffset > 0) {
387
+ y--;
388
+ if (y < 0) {
389
+ this.scrollUp(1);
390
+ y = 0;
391
+ }
392
+ text = this.getScrollLineText(y);
393
+ x = Math.max(text.length - 1, 0);
394
+ } else {
395
+ x = 0;
396
+ break;
397
+ }
398
+ }
399
+ }
400
+ while (x > 0 && !seps.has(text[x - 1] ?? " ")) {
401
+ x--;
402
+ }
403
+ this.scrollCursorX = Math.max(x, 0);
404
+ this.scrollCursorY = y;
405
+ this.scrollDesiredX = this.scrollCursorX;
406
+ this.renderScrolled();
407
+ }
408
+ /** Move cursor to the start of the next word (vim w / Alt+Right). */
409
+ moveCursorNextWord() {
410
+ const seps = _ScreenRenderer.WORD_SEPARATORS;
411
+ let x = this.scrollCursorX;
412
+ let y = this.scrollCursorY;
413
+ let text = this.getScrollLineText(y);
414
+ const maxY = this.rows - 1;
415
+ while (x < text.length && !seps.has(text[x] ?? " ")) {
416
+ x++;
417
+ }
418
+ while (true) {
419
+ if (x < text.length && !seps.has(text[x] ?? " ")) break;
420
+ if (x >= text.length) {
421
+ y++;
422
+ if (y > maxY) {
423
+ this.scrollDown(1);
424
+ y = maxY;
425
+ }
426
+ text = this.getScrollLineText(y);
427
+ x = 0;
428
+ if (text.length === 0) continue;
429
+ if (!seps.has(text[0] ?? " ")) break;
430
+ }
431
+ x++;
432
+ }
433
+ this.scrollCursorX = Math.min(x, Math.max(text.length - 1, 0));
434
+ this.scrollCursorY = y;
435
+ this.scrollDesiredX = this.scrollCursorX;
436
+ this.renderScrolled();
437
+ }
438
+ /** Whether there's content in scrollback to view. */
439
+ hasScrollback() {
440
+ return this.term.buffer.active.baseY > 0;
441
+ }
442
+ /** Whether we're scrolled back (not showing live content). */
443
+ get isScrolledBack() {
444
+ return this.scrollOffset > 0;
445
+ }
446
+ /** Render from scrollback buffer at current offset. */
447
+ renderScrolled() {
448
+ const buf = this.term.buffer.active;
449
+ const viewStart = buf.baseY - this.scrollOffset;
450
+ const totalLines = buf.baseY + this.rows;
451
+ const currentLine = viewStart + this.scrollCursorY + 1;
452
+ let output = "";
453
+ const pad = " ".repeat(this.cols);
454
+ for (let y = 0; y < this.rows; y++) {
455
+ const line = buf.getLine(viewStart + y);
456
+ const rendered = line ? this.renderLine(line) : "";
457
+ output += `\x1B[${y + 1};1H${pad}\x1B[${y + 1};1H${rendered}`;
458
+ }
459
+ const pos = `${currentLine}/${totalLines}`;
460
+ const sep = "\u2500".repeat(this.cols);
461
+ output += `\x1B[${this.rows + 1};1H${pad}\x1B[${this.rows + 1};1H\x1B[90m${sep}\x1B[0m`;
462
+ output += `\x1B[${this.rows + 2};1H${pad}\x1B[${this.rows + 2};1H\x1B[7m [\u2191\u2193/hjkl] move [w/b] word [0/$] line [g/G] top/end [q/Esc] exit \x1B[0m`;
463
+ output += `\x1B[${this.rows + 2};${this.cols - pos.length - 1}H\x1B[43;30m ${pos} \x1B[0m`;
464
+ output += `\x1B[${this.scrollCursorY + 1};${this.scrollCursorX + 1}H\x1B[?25h`;
465
+ process.stdout.write(output);
466
+ }
467
+ /** Force full redraw on next render (e.g. after sidebar toggle). */
468
+ invalidate() {
469
+ this.lastFrame = [];
470
+ let clear = "";
471
+ for (let y = 0; y < this.rows; y++) {
472
+ clear += `\x1B[${y + 1};1H\x1B[2K`;
473
+ }
474
+ process.stdout.write(clear);
475
+ }
476
+ resize(cols, rows) {
477
+ this.cols = cols;
478
+ this.rows = rows;
479
+ this.term.resize(cols, rows);
480
+ this.lastFrame = [];
481
+ }
482
+ dispose() {
483
+ if (this.overlayTimer) clearImmediate(this.overlayTimer);
484
+ if (this.renderTimer) clearTimeout(this.renderTimer);
485
+ process.stdout.write("\x1B[?25h");
486
+ this.term.dispose();
487
+ }
488
+ };
489
+ export {
490
+ ScreenRenderer
491
+ };
492
+ //# sourceMappingURL=screen-renderer.js.map