@code-yeongyu/senpi 2026.5.23 → 2026.5.29

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 (200) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/cli/args.d.ts +0 -6
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +15 -2
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +4 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +116 -80
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +18 -24
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +4 -2
  18. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js.map +1 -1
  19. package/dist/core/extensions/builtin/history-search/filter.d.ts +3 -0
  20. package/dist/core/extensions/builtin/history-search/filter.d.ts.map +1 -0
  21. package/dist/core/extensions/builtin/history-search/filter.js +22 -0
  22. package/dist/core/extensions/builtin/history-search/filter.js.map +1 -0
  23. package/dist/core/extensions/builtin/history-search/index.d.ts +7 -0
  24. package/dist/core/extensions/builtin/history-search/index.d.ts.map +1 -0
  25. package/dist/core/extensions/builtin/history-search/index.js +45 -0
  26. package/dist/core/extensions/builtin/history-search/index.js.map +1 -0
  27. package/dist/core/extensions/builtin/history-search/indexer.d.ts +3 -0
  28. package/dist/core/extensions/builtin/history-search/indexer.d.ts.map +1 -0
  29. package/dist/core/extensions/builtin/history-search/indexer.js +161 -0
  30. package/dist/core/extensions/builtin/history-search/indexer.js.map +1 -0
  31. package/dist/core/extensions/builtin/history-search/overlay.d.ts +30 -0
  32. package/dist/core/extensions/builtin/history-search/overlay.d.ts.map +1 -0
  33. package/dist/core/extensions/builtin/history-search/overlay.js +115 -0
  34. package/dist/core/extensions/builtin/history-search/overlay.js.map +1 -0
  35. package/dist/core/extensions/builtin/history-search/types.d.ts +8 -0
  36. package/dist/core/extensions/builtin/history-search/types.d.ts.map +1 -0
  37. package/dist/core/extensions/builtin/history-search/types.js +2 -0
  38. package/dist/core/extensions/builtin/history-search/types.js.map +1 -0
  39. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  40. package/dist/core/extensions/builtin/index.js +4 -0
  41. package/dist/core/extensions/builtin/index.js.map +1 -1
  42. package/dist/core/extensions/builtin/session-observer/index.d.ts +5 -0
  43. package/dist/core/extensions/builtin/session-observer/index.d.ts.map +1 -0
  44. package/dist/core/extensions/builtin/session-observer/index.js +36 -0
  45. package/dist/core/extensions/builtin/session-observer/index.js.map +1 -0
  46. package/dist/core/extensions/builtin/session-observer/loader.d.ts +3 -0
  47. package/dist/core/extensions/builtin/session-observer/loader.d.ts.map +1 -0
  48. package/dist/core/extensions/builtin/session-observer/loader.js +20 -0
  49. package/dist/core/extensions/builtin/session-observer/loader.js.map +1 -0
  50. package/dist/core/extensions/builtin/session-observer/overlay-format.d.ts +7 -0
  51. package/dist/core/extensions/builtin/session-observer/overlay-format.d.ts.map +1 -0
  52. package/dist/core/extensions/builtin/session-observer/overlay-format.js +30 -0
  53. package/dist/core/extensions/builtin/session-observer/overlay-format.js.map +1 -0
  54. package/dist/core/extensions/builtin/session-observer/overlay.d.ts +51 -0
  55. package/dist/core/extensions/builtin/session-observer/overlay.d.ts.map +1 -0
  56. package/dist/core/extensions/builtin/session-observer/overlay.js +234 -0
  57. package/dist/core/extensions/builtin/session-observer/overlay.js.map +1 -0
  58. package/dist/core/extensions/builtin/session-observer/scanner.d.ts +10 -0
  59. package/dist/core/extensions/builtin/session-observer/scanner.d.ts.map +1 -0
  60. package/dist/core/extensions/builtin/session-observer/scanner.js +142 -0
  61. package/dist/core/extensions/builtin/session-observer/scanner.js.map +1 -0
  62. package/dist/core/extensions/builtin/session-observer/text.d.ts +7 -0
  63. package/dist/core/extensions/builtin/session-observer/text.d.ts.map +1 -0
  64. package/dist/core/extensions/builtin/session-observer/text.js +37 -0
  65. package/dist/core/extensions/builtin/session-observer/text.js.map +1 -0
  66. package/dist/core/extensions/builtin/session-observer/transcript-entries.d.ts +7 -0
  67. package/dist/core/extensions/builtin/session-observer/transcript-entries.d.ts.map +1 -0
  68. package/dist/core/extensions/builtin/session-observer/transcript-entries.js +71 -0
  69. package/dist/core/extensions/builtin/session-observer/transcript-entries.js.map +1 -0
  70. package/dist/core/extensions/builtin/session-observer/transcript-format.d.ts +11 -0
  71. package/dist/core/extensions/builtin/session-observer/transcript-format.d.ts.map +1 -0
  72. package/dist/core/extensions/builtin/session-observer/transcript-format.js +65 -0
  73. package/dist/core/extensions/builtin/session-observer/transcript-format.js.map +1 -0
  74. package/dist/core/extensions/builtin/session-observer/transcript.d.ts +4 -0
  75. package/dist/core/extensions/builtin/session-observer/transcript.d.ts.map +1 -0
  76. package/dist/core/extensions/builtin/session-observer/transcript.js +81 -0
  77. package/dist/core/extensions/builtin/session-observer/transcript.js.map +1 -0
  78. package/dist/core/extensions/builtin/session-observer/types.d.ts +33 -0
  79. package/dist/core/extensions/builtin/session-observer/types.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/session-observer/types.js +2 -0
  81. package/dist/core/extensions/builtin/session-observer/types.js.map +1 -0
  82. package/dist/core/extensions/loader.d.ts.map +1 -1
  83. package/dist/core/extensions/loader.js +21 -9
  84. package/dist/core/extensions/loader.js.map +1 -1
  85. package/dist/core/extensions/runner.d.ts.map +1 -1
  86. package/dist/core/extensions/runner.js +1 -0
  87. package/dist/core/extensions/runner.js.map +1 -1
  88. package/dist/core/keybindings.d.ts +10 -0
  89. package/dist/core/keybindings.d.ts.map +1 -1
  90. package/dist/core/keybindings.js +3 -0
  91. package/dist/core/keybindings.js.map +1 -1
  92. package/dist/core/output-guard.d.ts +1 -0
  93. package/dist/core/output-guard.d.ts.map +1 -1
  94. package/dist/core/output-guard.js +52 -22
  95. package/dist/core/output-guard.js.map +1 -1
  96. package/dist/core/package-manager.d.ts.map +1 -1
  97. package/dist/core/package-manager.js +16 -4
  98. package/dist/core/package-manager.js.map +1 -1
  99. package/dist/core/session-work-barrier.d.ts +9 -0
  100. package/dist/core/session-work-barrier.d.ts.map +1 -0
  101. package/dist/core/session-work-barrier.js +50 -0
  102. package/dist/core/session-work-barrier.js.map +1 -0
  103. package/dist/main.d.ts.map +1 -1
  104. package/dist/main.js +0 -15
  105. package/dist/main.js.map +1 -1
  106. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/footer.js +74 -63
  108. package/dist/modes/interactive/components/footer.js.map +1 -1
  109. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/user-message.js +1 -1
  111. package/dist/modes/interactive/components/user-message.js.map +1 -1
  112. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.js +24 -0
  114. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  115. package/dist/modes/rpc/rpc-client.d.ts +3 -0
  116. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  117. package/dist/modes/rpc/rpc-client.js +64 -7
  118. package/dist/modes/rpc/rpc-client.js.map +1 -1
  119. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  120. package/dist/modes/rpc/rpc-mode.js +15 -3
  121. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  122. package/dist/utils/paths.d.ts +1 -0
  123. package/dist/utils/paths.d.ts.map +1 -1
  124. package/dist/utils/paths.js +8 -0
  125. package/dist/utils/paths.js.map +1 -1
  126. package/docs/settings.md +3 -1
  127. package/docs/terminal-setup.md +6 -0
  128. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.d.ts.map +1 -1
  129. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +18 -24
  130. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js.map +1 -1
  131. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  132. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  133. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +2 -2
  134. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
  135. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  136. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +54 -12
  137. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js.map +1 -1
  138. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  139. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +1 -1
  140. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  141. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.d.ts.map +1 -1
  142. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js +1 -1
  143. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js.map +1 -1
  144. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  145. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js +46 -29
  146. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  147. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  148. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +1 -1
  149. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js.map +1 -1
  150. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  151. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +1 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js.map +1 -1
  153. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +2 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +5 -2
  156. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  157. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  158. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts.map +1 -1
  159. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +2 -17
  160. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js.map +1 -1
  161. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  162. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +40 -55
  163. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  164. package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts.map +1 -1
  165. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +2 -2
  166. package/node_modules/@earendil-works/pi-tui/dist/components/input.js.map +1 -1
  167. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts +7 -1
  168. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts.map +1 -1
  169. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +12 -2
  170. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js.map +1 -1
  171. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +1 -1
  172. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
  173. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
  174. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts +3 -0
  175. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts.map +1 -0
  176. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +53 -0
  177. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js.map +1 -0
  178. package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.d.ts +3 -0
  179. package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.d.ts.map +1 -0
  180. package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.js +38 -0
  181. package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.js.map +1 -0
  182. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +1 -1
  183. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +4 -1
  184. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +1 -1
  185. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +2 -0
  186. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  187. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +13 -1
  188. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  189. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +5 -1
  190. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +1 -1
  191. package/node_modules/@earendil-works/pi-tui/dist/utils.js +66 -14
  192. package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +1 -1
  193. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  194. package/npm-shrinkwrap.json +13 -13
  195. package/package.json +6 -7
  196. package/dist/modes/neo-mode.d.ts +0 -43
  197. package/dist/modes/neo-mode.d.ts.map +0 -1
  198. package/dist/modes/neo-mode.js +0 -142
  199. package/dist/modes/neo-mode.js.map +0 -1
  200. package/dist/neo-tui-bin/senpi-neo-tui-linux-x64 +0 -0
@@ -3,9 +3,10 @@ import { decodePrintableKey, matchesKey } from "../keys.js";
3
3
  import { KillRing } from "../kill-ring.js";
4
4
  import { CURSOR_MARKER } from "../tui.js";
5
5
  import { UndoStack } from "../undo-stack.js";
6
- import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.js";
6
+ import { getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.js";
7
7
  import { SelectList } from "./select-list.js";
8
- const baseSegmenter = getSegmenter();
8
+ const graphemeSegmenter = getGraphemeSegmenter();
9
+ const wordSegmenter = getWordSegmenter();
9
10
  /** Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. */
10
11
  const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
11
12
  /** Non-global version for single-segment testing. */
@@ -21,7 +22,7 @@ function isPasteMarker(segment) {
21
22
  *
22
23
  * Only markers whose numeric ID exists in `validIds` are merged.
23
24
  */
24
- function segmentWithMarkers(text, validIds) {
25
+ function segmentWithMarkers(text, baseSegmenter, validIds) {
25
26
  // Fast path: no paste markers in the text or no valid IDs.
26
27
  if (validIds.size === 0 || !text.includes("[paste #")) {
27
28
  return baseSegmenter.segment(text);
@@ -86,7 +87,7 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
86
87
  return [{ text: line, startIndex: 0, endIndex: line.length }];
87
88
  }
88
89
  const chunks = [];
89
- const segments = preSegmented ?? [...baseSegmenter.segment(line)];
90
+ const segments = preSegmented ?? [...graphemeSegmenter.segment(line)];
90
91
  let currentWidth = 0;
91
92
  let chunkStart = 0;
92
93
  // Wrap opportunity: the position after the last whitespace before a non-whitespace
@@ -225,8 +226,8 @@ export class Editor {
225
226
  return new Set(this.pastes.keys());
226
227
  }
227
228
  /** Segment text with paste-marker awareness, only merging markers with valid IDs. */
228
- segment(text) {
229
- return segmentWithMarkers(text, this.validPasteIds());
229
+ segment(text, mode) {
230
+ return segmentWithMarkers(text, mode === "word" ? wordSegmenter : graphemeSegmenter, this.validPasteIds());
230
231
  }
231
232
  getPaddingX() {
232
233
  return this.paddingX;
@@ -381,7 +382,7 @@ export class Editor {
381
382
  if (after.length > 0) {
382
383
  // Cursor is on a character (grapheme) - replace it with highlighted version
383
384
  // Get the first grapheme from 'after'
384
- const afterGraphemes = [...this.segment(after)];
385
+ const afterGraphemes = [...this.segment(after, "grapheme")];
385
386
  const firstGrapheme = afterGraphemes[0]?.segment || "";
386
387
  const restAfter = after.slice(firstGrapheme.length);
387
388
  const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
@@ -717,7 +718,7 @@ export class Editor {
717
718
  }
718
719
  else {
719
720
  // Line needs wrapping - use word-aware wrapping
720
- const chunks = wordWrapLine(line, contentWidth, [...this.segment(line)]);
721
+ const chunks = wordWrapLine(line, contentWidth, [...this.segment(line, "grapheme")]);
721
722
  for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
722
723
  const chunk = chunks[chunkIndex];
723
724
  if (!chunk)
@@ -1030,7 +1031,7 @@ export class Editor {
1030
1031
  const line = this.state.lines[this.state.cursorLine] || "";
1031
1032
  const beforeCursor = line.slice(0, this.state.cursorCol);
1032
1033
  // Find the last grapheme in the text before cursor
1033
- const graphemes = [...this.segment(beforeCursor)];
1034
+ const graphemes = [...this.segment(beforeCursor, "grapheme")];
1034
1035
  const lastGrapheme = graphemes[graphemes.length - 1];
1035
1036
  const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
1036
1037
  const before = line.slice(0, this.state.cursorCol - graphemeLength);
@@ -1114,7 +1115,7 @@ export class Editor {
1114
1115
  // Snap cursor to atomic segment boundary (e.g. paste markers)
1115
1116
  // so the cursor never lands in the middle of a multi-grapheme unit.
1116
1117
  // Single-grapheme segments don't need snapping.
1117
- const segments = [...this.segment(logicalLine)];
1118
+ const segments = [...this.segment(logicalLine, "grapheme")];
1118
1119
  for (const seg of segments) {
1119
1120
  if (seg.index > this.state.cursorCol)
1120
1121
  break;
@@ -1334,7 +1335,7 @@ export class Editor {
1334
1335
  // Delete grapheme at cursor position (handles emojis, combining characters, etc.)
1335
1336
  const afterCursor = currentLine.slice(this.state.cursorCol);
1336
1337
  // Find the first grapheme at cursor
1337
- const graphemes = [...this.segment(afterCursor)];
1338
+ const graphemes = [...this.segment(afterCursor, "grapheme")];
1338
1339
  const firstGrapheme = graphemes[0];
1339
1340
  const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
1340
1341
  const before = currentLine.slice(0, this.state.cursorCol);
@@ -1389,7 +1390,7 @@ export class Editor {
1389
1390
  }
1390
1391
  else {
1391
1392
  // Line needs wrapping - use word-aware wrapping
1392
- const chunks = wordWrapLine(line, width, [...this.segment(line)]);
1393
+ const chunks = wordWrapLine(line, width, [...this.segment(line, "grapheme")]);
1393
1394
  for (const chunk of chunks) {
1394
1395
  visualLines.push({
1395
1396
  logicalLine: i,
@@ -1441,7 +1442,7 @@ export class Editor {
1441
1442
  // Moving right - move by one grapheme (handles emojis, combining characters, etc.)
1442
1443
  if (this.state.cursorCol < currentLine.length) {
1443
1444
  const afterCursor = currentLine.slice(this.state.cursorCol);
1444
- const graphemes = [...this.segment(afterCursor)];
1445
+ const graphemes = [...this.segment(afterCursor, "grapheme")];
1445
1446
  const firstGrapheme = graphemes[0];
1446
1447
  this.setCursorCol(this.state.cursorCol + (firstGrapheme ? firstGrapheme.segment.length : 1));
1447
1448
  }
@@ -1462,7 +1463,7 @@ export class Editor {
1462
1463
  // Moving left - move by one grapheme (handles emojis, combining characters, etc.)
1463
1464
  if (this.state.cursorCol > 0) {
1464
1465
  const beforeCursor = currentLine.slice(0, this.state.cursorCol);
1465
- const graphemes = [...this.segment(beforeCursor)];
1466
+ const graphemes = [...this.segment(beforeCursor, "grapheme")];
1466
1467
  const lastGrapheme = graphemes[graphemes.length - 1];
1467
1468
  this.setCursorCol(this.state.cursorCol - (lastGrapheme ? lastGrapheme.segment.length : 1));
1468
1469
  }
@@ -1501,35 +1502,27 @@ export class Editor {
1501
1502
  return;
1502
1503
  }
1503
1504
  const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
1504
- const graphemes = [...this.segment(textBeforeCursor)];
1505
+ const segments = [...this.segment(textBeforeCursor, "word")];
1505
1506
  let newCol = this.state.cursorCol;
1506
1507
  // Skip trailing whitespace
1507
- while (graphemes.length > 0 &&
1508
- !isPasteMarker(graphemes[graphemes.length - 1]?.segment || "") &&
1509
- isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
1510
- newCol -= graphemes.pop()?.segment.length || 0;
1511
- }
1512
- if (graphemes.length > 0) {
1513
- const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
1514
- if (isPasteMarker(lastGrapheme)) {
1515
- // Paste marker is a single atomic word
1516
- newCol -= graphemes.pop()?.segment.length || 0;
1517
- }
1518
- else if (isPunctuationChar(lastGrapheme)) {
1519
- // Skip punctuation run
1520
- while (graphemes.length > 0 &&
1521
- isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
1522
- !isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
1523
- newCol -= graphemes.pop()?.segment.length || 0;
1524
- }
1508
+ while (segments.length > 0 &&
1509
+ !isPasteMarker(segments[segments.length - 1]?.segment || "") &&
1510
+ isWhitespaceChar(segments[segments.length - 1]?.segment || "")) {
1511
+ newCol -= segments.pop()?.segment.length || 0;
1512
+ }
1513
+ if (segments.length > 0) {
1514
+ const last = segments[segments.length - 1];
1515
+ if (isPasteMarker(last.segment) || last.isWordLike) {
1516
+ // Skip one word-like segment (or paste marker)
1517
+ newCol -= segments.pop()?.segment.length || 0;
1525
1518
  }
1526
1519
  else {
1527
- // Skip word run
1528
- while (graphemes.length > 0 &&
1529
- !isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
1530
- !isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
1531
- !isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
1532
- newCol -= graphemes.pop()?.segment.length || 0;
1520
+ // Skip non-word non-whitespace run (punctuation)
1521
+ while (segments.length > 0 &&
1522
+ !isPasteMarker(segments[segments.length - 1]?.segment || "") &&
1523
+ !segments[segments.length - 1]?.isWordLike &&
1524
+ !isWhitespaceChar(segments[segments.length - 1]?.segment || "")) {
1525
+ newCol -= segments.pop()?.segment.length || 0;
1533
1526
  }
1534
1527
  }
1535
1528
  }
@@ -1691,7 +1684,7 @@ export class Editor {
1691
1684
  return;
1692
1685
  }
1693
1686
  const textAfterCursor = currentLine.slice(this.state.cursorCol);
1694
- const segments = this.segment(textAfterCursor);
1687
+ const segments = this.segment(textAfterCursor, "word");
1695
1688
  const iterator = segments[Symbol.iterator]();
1696
1689
  let next = iterator.next();
1697
1690
  let newCol = this.state.cursorCol;
@@ -1701,24 +1694,16 @@ export class Editor {
1701
1694
  next = iterator.next();
1702
1695
  }
1703
1696
  if (!next.done) {
1704
- const firstGrapheme = next.value.segment;
1705
- if (isPasteMarker(firstGrapheme)) {
1706
- // Paste marker is a single atomic word
1707
- newCol += firstGrapheme.length;
1708
- }
1709
- else if (isPunctuationChar(firstGrapheme)) {
1710
- // Skip punctuation run
1711
- while (!next.done && isPunctuationChar(next.value.segment) && !isPasteMarker(next.value.segment)) {
1712
- newCol += next.value.segment.length;
1713
- next = iterator.next();
1714
- }
1697
+ if (isPasteMarker(next.value.segment) || next.value.isWordLike) {
1698
+ // Skip one word-like segment (or paste marker)
1699
+ newCol += next.value.segment.length;
1715
1700
  }
1716
1701
  else {
1717
- // Skip word run
1702
+ // Skip non-word non-whitespace run (punctuation)
1718
1703
  while (!next.done &&
1719
- !isWhitespaceChar(next.value.segment) &&
1720
- !isPunctuationChar(next.value.segment) &&
1721
- !isPasteMarker(next.value.segment)) {
1704
+ !isPasteMarker(next.value.segment) &&
1705
+ !next.value.isWordLike &&
1706
+ !isWhitespaceChar(next.value.segment)) {
1722
1707
  newCol += next.value.segment.length;
1723
1708
  next = iterator.next();
1724
1709
  }