omk-tui 0.90.6 → 0.90.7
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.
- package/dist/autocomplete.d.ts +2 -0
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +130 -37
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/editor.d.ts +9 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +118 -25
- package/dist/components/editor.js.map +1 -1
- package/package.json +1 -1
|
@@ -123,20 +123,26 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
|
123
123
|
wrapOppIndex = -1;
|
|
124
124
|
}
|
|
125
125
|
if (gWidth > maxWidth) {
|
|
126
|
-
//
|
|
127
|
-
// in a narrow terminal)
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
126
|
+
// Multi-grapheme atomic segment wider than maxWidth (e.g. paste marker
|
|
127
|
+
// in a narrow terminal): re-wrap it at grapheme granularity. The segment
|
|
128
|
+
// remains logically atomic for cursor movement / editing — the split is
|
|
129
|
+
// purely visual for word-wrap layout.
|
|
130
|
+
const subSegments = [...graphemeSegmenter.segment(grapheme)];
|
|
131
|
+
if (subSegments.length > 1) {
|
|
132
|
+
const subChunks = wordWrapLine(grapheme, maxWidth, subSegments);
|
|
133
|
+
for (let j = 0; j < subChunks.length - 1; j++) {
|
|
134
|
+
const sc = subChunks[j];
|
|
135
|
+
chunks.push({ text: sc.text, startIndex: charIndex + sc.startIndex, endIndex: charIndex + sc.endIndex });
|
|
136
|
+
}
|
|
137
|
+
const last = subChunks[subChunks.length - 1];
|
|
138
|
+
chunkStart = charIndex + last.startIndex;
|
|
139
|
+
currentWidth = visibleWidth(last.text);
|
|
140
|
+
wrapOppIndex = -1;
|
|
141
|
+
continue;
|
|
134
142
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
wrapOppIndex = -1;
|
|
139
|
-
continue;
|
|
143
|
+
// An indivisible grapheme wider than maxWidth (e.g. a wide CJK cell at
|
|
144
|
+
// width 1) cannot be split further: keep it whole and let the next
|
|
145
|
+
// overflow check force-break around it instead of recursing forever.
|
|
140
146
|
}
|
|
141
147
|
// Advance.
|
|
142
148
|
currentWidth += gWidth;
|
|
@@ -889,8 +895,9 @@ export class Editor {
|
|
|
889
895
|
}
|
|
890
896
|
// Check if we should trigger or update autocomplete
|
|
891
897
|
if (!this.autocompleteState) {
|
|
892
|
-
// Auto-trigger for "/" at
|
|
893
|
-
|
|
898
|
+
// Auto-trigger for "/" at start of message (slash commands) or after whitespace
|
|
899
|
+
// mid-message (skill list). Absolute paths still fall through to path AC.
|
|
900
|
+
if (char === "/" && this.isSlashTokenTrigger()) {
|
|
894
901
|
this.tryTriggerAutocomplete();
|
|
895
902
|
}
|
|
896
903
|
else if (char === "@" || char === "#") {
|
|
@@ -910,7 +917,8 @@ export class Editor {
|
|
|
910
917
|
this.tryTriggerAutocomplete();
|
|
911
918
|
}
|
|
912
919
|
}
|
|
913
|
-
// Also auto-trigger when typing letters in a slash command
|
|
920
|
+
// Also auto-trigger when typing letters in a slash command, mid-message
|
|
921
|
+
// `/skill` token, bang launcher, bare first-token skill name, or @/# context
|
|
914
922
|
else if (/[a-zA-Z0-9.\-_]/.test(char)) {
|
|
915
923
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
916
924
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -918,12 +926,18 @@ export class Editor {
|
|
|
918
926
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
919
927
|
this.tryTriggerAutocomplete();
|
|
920
928
|
}
|
|
929
|
+
else if (this.isMidMessageSlashSkillContext(textBeforeCursor)) {
|
|
930
|
+
this.tryTriggerAutocomplete();
|
|
931
|
+
}
|
|
921
932
|
else if (textBeforeCursor.match(/(?:^|[\s])[@#][^\s]*$/)) {
|
|
922
933
|
this.tryTriggerAutocomplete();
|
|
923
934
|
}
|
|
924
935
|
else if (this.isBangLauncherContext(textBeforeCursor)) {
|
|
925
936
|
this.tryTriggerAutocomplete();
|
|
926
937
|
}
|
|
938
|
+
else if (this.isPlainSkillLauncherContext(textBeforeCursor)) {
|
|
939
|
+
this.tryTriggerAutocomplete();
|
|
940
|
+
}
|
|
927
941
|
}
|
|
928
942
|
}
|
|
929
943
|
else {
|
|
@@ -1075,12 +1089,18 @@ export class Editor {
|
|
|
1075
1089
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1076
1090
|
this.tryTriggerAutocomplete();
|
|
1077
1091
|
}
|
|
1092
|
+
else if (this.isMidMessageSlashSkillContext(textBeforeCursor)) {
|
|
1093
|
+
this.tryTriggerAutocomplete();
|
|
1094
|
+
}
|
|
1078
1095
|
else if (textBeforeCursor.match(/(?:^|[\s])[@#][^\s]*$/)) {
|
|
1079
1096
|
this.tryTriggerAutocomplete();
|
|
1080
1097
|
}
|
|
1081
1098
|
else if (this.isBangLauncherContext(textBeforeCursor)) {
|
|
1082
1099
|
this.tryTriggerAutocomplete();
|
|
1083
1100
|
}
|
|
1101
|
+
else if (this.isPlainSkillLauncherContext(textBeforeCursor)) {
|
|
1102
|
+
this.tryTriggerAutocomplete();
|
|
1103
|
+
}
|
|
1084
1104
|
}
|
|
1085
1105
|
}
|
|
1086
1106
|
/**
|
|
@@ -1092,6 +1112,45 @@ export class Editor {
|
|
|
1092
1112
|
this.preferredVisualCol = null;
|
|
1093
1113
|
this.snappedFromCursorCol = null;
|
|
1094
1114
|
}
|
|
1115
|
+
/** Display-cell column of char offset `col` within the visual line starting at `startCol`. */
|
|
1116
|
+
displayColAt(lineText, startCol, col) {
|
|
1117
|
+
return visibleWidth(lineText.slice(startCol, col));
|
|
1118
|
+
}
|
|
1119
|
+
/** Char offset of the boundary at or before `displayCol` cells into `chunkText`. */
|
|
1120
|
+
offsetAtDisplayCol(chunkText, displayCol) {
|
|
1121
|
+
let cells = 0;
|
|
1122
|
+
let offset = 0;
|
|
1123
|
+
for (const seg of this.segment(chunkText, "grapheme")) {
|
|
1124
|
+
const segWidth = visibleWidth(seg.segment);
|
|
1125
|
+
if (cells + segWidth > displayCol) {
|
|
1126
|
+
// Landing inside this segment. Multi-char atomic segments (e.g.
|
|
1127
|
+
// paste markers) keep the char-level remainder so the snap logic
|
|
1128
|
+
// can record the pre-snap column; indivisible wide graphemes
|
|
1129
|
+
// floor to their start cell.
|
|
1130
|
+
if (seg.segment.length > 1) {
|
|
1131
|
+
offset += Math.min(seg.segment.length, Math.max(0, displayCol - cells));
|
|
1132
|
+
}
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
cells += segWidth;
|
|
1136
|
+
offset += seg.segment.length;
|
|
1137
|
+
}
|
|
1138
|
+
return offset;
|
|
1139
|
+
}
|
|
1140
|
+
/** Max reachable display-cell column on a visual line segment. */
|
|
1141
|
+
maxDisplayColOf(chunkText, isLastSegment) {
|
|
1142
|
+
if (isLastSegment)
|
|
1143
|
+
return visibleWidth(chunkText);
|
|
1144
|
+
// Non-final wrapped segments keep the cursor before the segment end:
|
|
1145
|
+
// clamp to the start cell of the last grapheme.
|
|
1146
|
+
let lastStart = 0;
|
|
1147
|
+
let cells = 0;
|
|
1148
|
+
for (const seg of this.segment(chunkText, "grapheme")) {
|
|
1149
|
+
lastStart = cells;
|
|
1150
|
+
cells += visibleWidth(seg.segment);
|
|
1151
|
+
}
|
|
1152
|
+
return Math.max(0, lastStart);
|
|
1153
|
+
}
|
|
1095
1154
|
/**
|
|
1096
1155
|
* Move cursor to a target visual line, applying sticky column logic.
|
|
1097
1156
|
* Shared by moveCursor() and pageScroll().
|
|
@@ -1103,27 +1162,30 @@ export class Editor {
|
|
|
1103
1162
|
return;
|
|
1104
1163
|
// When the cursor was snapped to a segment start, resolve the pre-snap
|
|
1105
1164
|
// position against the VL it belongs to. This gives the correct visual
|
|
1106
|
-
// column even after a resize reshuffles VLs.
|
|
1165
|
+
// column even after a resize reshuffles VLs. Visual columns are display
|
|
1166
|
+
// cells, so wide (e.g. CJK) graphemes keep the cursor on the same column.
|
|
1167
|
+
const currentLineText = this.state.lines[currentVL.logicalLine] || "";
|
|
1107
1168
|
let currentVisualCol;
|
|
1108
1169
|
if (this.snappedFromCursorCol !== null) {
|
|
1109
1170
|
const vlIndex = this.findVisualLineAt(visualLines, currentVL.logicalLine, this.snappedFromCursorCol);
|
|
1110
|
-
currentVisualCol = this.
|
|
1171
|
+
currentVisualCol = this.displayColAt(currentLineText, visualLines[vlIndex].startCol, this.snappedFromCursorCol);
|
|
1111
1172
|
}
|
|
1112
1173
|
else {
|
|
1113
|
-
currentVisualCol = this.
|
|
1174
|
+
currentVisualCol = this.displayColAt(currentLineText, currentVL.startCol, this.state.cursorCol);
|
|
1114
1175
|
}
|
|
1115
|
-
// For non-last segments, clamp to
|
|
1176
|
+
// For non-last segments, clamp to the last grapheme's start cell to stay within the segment
|
|
1116
1177
|
const isLastSourceSegment = currentVisualLine === visualLines.length - 1 ||
|
|
1117
1178
|
visualLines[currentVisualLine + 1]?.logicalLine !== currentVL.logicalLine;
|
|
1118
|
-
const sourceMaxVisualCol =
|
|
1179
|
+
const sourceMaxVisualCol = this.maxDisplayColOf(currentLineText.slice(currentVL.startCol, currentVL.startCol + currentVL.length), isLastSourceSegment);
|
|
1119
1180
|
const isLastTargetSegment = targetVisualLine === visualLines.length - 1 ||
|
|
1120
1181
|
visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine;
|
|
1121
|
-
const
|
|
1182
|
+
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
1183
|
+
const targetChunkText = logicalLine.slice(targetVL.startCol, targetVL.startCol + targetVL.length);
|
|
1184
|
+
const targetMaxVisualCol = this.maxDisplayColOf(targetChunkText, isLastTargetSegment);
|
|
1122
1185
|
const moveToVisualCol = this.computeVerticalMoveColumn(currentVisualCol, sourceMaxVisualCol, targetMaxVisualCol);
|
|
1123
1186
|
// Set cursor position
|
|
1124
1187
|
this.state.cursorLine = targetVL.logicalLine;
|
|
1125
|
-
const targetCol = targetVL.startCol + moveToVisualCol;
|
|
1126
|
-
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
1188
|
+
const targetCol = targetVL.startCol + this.offsetAtDisplayCol(targetChunkText, moveToVisualCol);
|
|
1127
1189
|
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
1128
1190
|
// Snap cursor to atomic segment boundary (e.g. paste markers)
|
|
1129
1191
|
// so the cursor never lands in the middle of a multi-grapheme unit.
|
|
@@ -1376,12 +1438,18 @@ export class Editor {
|
|
|
1376
1438
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1377
1439
|
this.tryTriggerAutocomplete();
|
|
1378
1440
|
}
|
|
1441
|
+
else if (this.isMidMessageSlashSkillContext(textBeforeCursor)) {
|
|
1442
|
+
this.tryTriggerAutocomplete();
|
|
1443
|
+
}
|
|
1379
1444
|
else if (textBeforeCursor.match(/(?:^|[\s])[@#][^\s]*$/)) {
|
|
1380
1445
|
this.tryTriggerAutocomplete();
|
|
1381
1446
|
}
|
|
1382
1447
|
else if (this.isBangLauncherContext(textBeforeCursor)) {
|
|
1383
1448
|
this.tryTriggerAutocomplete();
|
|
1384
1449
|
}
|
|
1450
|
+
else if (this.isPlainSkillLauncherContext(textBeforeCursor)) {
|
|
1451
|
+
this.tryTriggerAutocomplete();
|
|
1452
|
+
}
|
|
1385
1453
|
}
|
|
1386
1454
|
}
|
|
1387
1455
|
/**
|
|
@@ -1470,7 +1538,7 @@ export class Editor {
|
|
|
1470
1538
|
// At end of last line - can't move, but set preferredVisualCol for up/down navigation
|
|
1471
1539
|
const currentVL = visualLines[currentVisualLine];
|
|
1472
1540
|
if (currentVL) {
|
|
1473
|
-
this.preferredVisualCol = this.
|
|
1541
|
+
this.preferredVisualCol = this.displayColAt(currentLine, currentVL.startCol, this.state.cursorCol);
|
|
1474
1542
|
}
|
|
1475
1543
|
}
|
|
1476
1544
|
}
|
|
@@ -1693,9 +1761,29 @@ export class Editor {
|
|
|
1693
1761
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1694
1762
|
return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
|
|
1695
1763
|
}
|
|
1764
|
+
// "/" typed at start of message OR as a new token after whitespace (skill list mid-message).
|
|
1765
|
+
isSlashTokenTrigger() {
|
|
1766
|
+
if (!this.isSlashMenuAllowed())
|
|
1767
|
+
return false;
|
|
1768
|
+
if (this.isAtStartOfMessage())
|
|
1769
|
+
return true;
|
|
1770
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1771
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1772
|
+
// just typed "/" after whitespace
|
|
1773
|
+
return /(?:^|[\s])\/$/.test(beforeCursor);
|
|
1774
|
+
}
|
|
1696
1775
|
isInSlashCommandContext(textBeforeCursor) {
|
|
1697
1776
|
return this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/");
|
|
1698
1777
|
}
|
|
1778
|
+
// Mid-message "/skill…" token (not an absolute path with extra slashes).
|
|
1779
|
+
// Provider rewrites selection to leading !skill: so submit can expand the skill.
|
|
1780
|
+
isMidMessageSlashSkillContext(textBeforeCursor) {
|
|
1781
|
+
if (!this.isSlashMenuAllowed())
|
|
1782
|
+
return false;
|
|
1783
|
+
if (textBeforeCursor.trimStart().startsWith("/"))
|
|
1784
|
+
return false;
|
|
1785
|
+
return /(?:^|[\s])\/[a-zA-Z0-9][a-zA-Z0-9:._-]*$/.test(textBeforeCursor);
|
|
1786
|
+
}
|
|
1699
1787
|
// A "!" skill launcher is only actionable when it begins the whole message:
|
|
1700
1788
|
// parseBangInvocation (coding-agent) only treats a message that starts with
|
|
1701
1789
|
// "!" as a bang, so a mid-message "!skill:name" would be submitted as plain
|
|
@@ -1705,6 +1793,11 @@ export class Editor {
|
|
|
1705
1793
|
isBangLauncherContext(textBeforeCursor) {
|
|
1706
1794
|
return this.isSlashMenuAllowed() && /^!(?!!)[^\s]*$/.test(textBeforeCursor);
|
|
1707
1795
|
}
|
|
1796
|
+
// Bare skill-name autocomplete at the start of the first line (no leading ! or /).
|
|
1797
|
+
// Selection inserts !skill:name so submit still goes through parseBangInvocation.
|
|
1798
|
+
isPlainSkillLauncherContext(textBeforeCursor) {
|
|
1799
|
+
return this.isSlashMenuAllowed() && /^[a-zA-Z0-9][a-zA-Z0-9.\-_]*$/.test(textBeforeCursor);
|
|
1800
|
+
}
|
|
1708
1801
|
// Autocomplete methods
|
|
1709
1802
|
/**
|
|
1710
1803
|
* Find the best autocomplete item index for the given prefix.
|