deckide 3.5.24 → 3.5.25

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.
@@ -5,7 +5,7 @@ import { TERMINAL_BUFFER_LIMIT } from '../config.js';
5
5
  import { createHttpError, handleError, readJson } from '../utils/error.js';
6
6
  import { getDefaultShell } from '../utils/shell.js';
7
7
  import { saveTerminal, deleteTerminal as deleteTerminalFromDb } from '../utils/database.js';
8
- import { alignToUtf8Start } from '../utils/utf8.js';
8
+ import { alignToUtf8Start, skipPartialEscapeSequence } from '../utils/utf8.js';
9
9
  const DEFAULT_TERMINAL_TITLE = 'ターミナル';
10
10
  const MAX_SOCKET_BUFFERED_AMOUNT = 1024 * 1024;
11
11
  export function createTerminalRouter(db, decks, terminals) {
@@ -21,15 +21,22 @@ export function createTerminalRouter(db, decks, terminals) {
21
21
  if (chunk.length >= TERMINAL_BUFFER_LIMIT) {
22
22
  let cutPos = chunk.length - TERMINAL_BUFFER_LIMIT;
23
23
  cutPos = alignToUtf8Start(chunk, cutPos);
24
- const retainedChunk = Buffer.from(chunk.subarray(cutPos));
25
- session.bufferBase += session.bufferLength + cutPos;
26
- session.bufferChunks = [retainedChunk];
27
- session.bufferLength = retainedChunk.length;
24
+ let retained = Buffer.from(chunk.subarray(cutPos));
25
+ // Skip partial escape sequence at the new start
26
+ const escSkip = skipPartialEscapeSequence(retained, 0);
27
+ if (escSkip > 0) {
28
+ retained = Buffer.from(retained.subarray(escSkip));
29
+ }
30
+ session.bufferBase += session.bufferLength + cutPos + escSkip;
31
+ session.bufferChunks = [retained];
32
+ session.bufferLength = retained.length;
28
33
  return;
29
34
  }
30
35
  session.bufferChunks.push(Buffer.from(chunk));
31
36
  session.bufferLength += chunk.length;
37
+ let trimmed = false;
32
38
  while (session.bufferLength > TERMINAL_BUFFER_LIMIT && session.bufferChunks.length > 0) {
39
+ trimmed = true;
33
40
  const overflow = session.bufferLength - TERMINAL_BUFFER_LIMIT;
34
41
  const firstChunk = session.bufferChunks[0];
35
42
  if (firstChunk.length <= overflow) {
@@ -43,9 +50,9 @@ export function createTerminalRouter(db, decks, terminals) {
43
50
  session.bufferBase += cutPos;
44
51
  session.bufferLength -= cutPos;
45
52
  }
46
- // After removing whole chunks, the new first chunk may start with
47
- // orphaned UTF-8 continuation bytes from a character that spanned chunks.
48
- if (session.bufferChunks.length > 0) {
53
+ if (trimmed && session.bufferChunks.length > 0) {
54
+ // After removing whole chunks, the new first chunk may start with
55
+ // orphaned UTF-8 continuation bytes from a character that spanned chunks.
49
56
  const first = session.bufferChunks[0];
50
57
  const skip = alignToUtf8Start(first, 0);
51
58
  if (skip > 0) {
@@ -53,6 +60,16 @@ export function createTerminalRouter(db, decks, terminals) {
53
60
  session.bufferBase += skip;
54
61
  session.bufferLength -= skip;
55
62
  }
63
+ // Also skip past any partial ANSI CSI escape sequence at the start.
64
+ // Without this, orphaned parameter bytes like "100;50m" are rendered
65
+ // as literal text and shift all cursor positions.
66
+ const cur = session.bufferChunks[0];
67
+ const escSkip = skipPartialEscapeSequence(cur, 0);
68
+ if (escSkip > 0) {
69
+ session.bufferChunks[0] = Buffer.from(cur.subarray(escSkip));
70
+ session.bufferBase += escSkip;
71
+ session.bufferLength -= escSkip;
72
+ }
56
73
  }
57
74
  }
58
75
  function getUniqueTerminalTitle(deckId, requestedTitle) {
@@ -53,3 +53,74 @@ export function alignToUtf8End(buf, offset) {
53
53
  // The character is incomplete — exclude it
54
54
  return pos;
55
55
  }
56
+ /**
57
+ * Skip past a partial ANSI CSI escape sequence at the start of a buffer.
58
+ *
59
+ * When a terminal buffer is trimmed from the front, the cut point may land
60
+ * inside a CSI sequence (e.g. "\x1b[38;2;100;50m"). The ESC and '[' are
61
+ * discarded, leaving orphaned parameter bytes like "100;50m" that xterm.js
62
+ * would render as literal text, shifting all subsequent cursor positions.
63
+ *
64
+ * This function detects two patterns:
65
+ * 1. Buffer starts with '[' followed by CSI parameter / intermediate /
66
+ * final bytes → partial CSI whose ESC was trimmed.
67
+ * 2. Buffer starts with CSI parameter bytes (digits, ';') containing at
68
+ * least one ';', ending with a final byte → partial CSI whose
69
+ * "ESC [" was trimmed.
70
+ *
71
+ * Returns the number of bytes to skip (0 if no partial sequence detected).
72
+ */
73
+ export function skipPartialEscapeSequence(buf, offset) {
74
+ if (offset >= buf.length)
75
+ return 0;
76
+ const limit = Math.min(buf.length, offset + 128);
77
+ let pos = offset;
78
+ // Pattern 1: starts with '[' (CSI intro without preceding ESC)
79
+ if (buf[pos] === 0x5B /* '[' */ && pos + 1 < limit) {
80
+ const next = buf[pos + 1];
81
+ // Only treat as CSI if next byte is a parameter (0x30-0x3F) or
82
+ // intermediate (0x20-0x2F) byte — avoids false positives like "[user@host"
83
+ if ((next >= 0x30 && next <= 0x3F) || (next >= 0x20 && next <= 0x2F)) {
84
+ pos++; // skip '['
85
+ while (pos < limit) {
86
+ const c = buf[pos];
87
+ if ((c >= 0x30 && c <= 0x3F) || (c >= 0x20 && c <= 0x2F)) {
88
+ pos++;
89
+ continue;
90
+ }
91
+ if (c >= 0x40 && c <= 0x7E) {
92
+ // CSI final byte — skip it and we're done
93
+ return (pos + 1) - offset;
94
+ }
95
+ break;
96
+ }
97
+ return 0;
98
+ }
99
+ return 0;
100
+ }
101
+ // Pattern 2: starts with CSI parameter bytes (digits, ';', etc.)
102
+ const b = buf[pos];
103
+ if (b < 0x30 || b > 0x3F)
104
+ return 0;
105
+ let hasSemicolon = false;
106
+ while (pos < limit) {
107
+ const c = buf[pos];
108
+ if (c >= 0x30 && c <= 0x3F) {
109
+ if (c === 0x3B)
110
+ hasSemicolon = true;
111
+ pos++;
112
+ }
113
+ else if (c >= 0x20 && c <= 0x2F) {
114
+ pos++;
115
+ }
116
+ else if (c >= 0x40 && c <= 0x7E) {
117
+ // CSI final byte — only skip if we saw at least one semicolon
118
+ // (avoids false positives like "5m" at the start of normal text)
119
+ return hasSemicolon ? (pos + 1) - offset : 0;
120
+ }
121
+ else {
122
+ break;
123
+ }
124
+ }
125
+ return 0;
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.5.24",
3
+ "version": "3.5.25",
4
4
  "description": "Deck IDE - Browser-based IDE with terminal, file explorer, and git integration",
5
5
  "type": "module",
6
6
  "bin": {