@rind-ai/cli 0.4.1 → 0.6.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.
- package/bin/rind.js +5 -5
- package/lib/assistant-renderer.js +179 -265
- package/lib/choice-menu-state.js +46 -46
- package/lib/cli-input-actions.js +548 -0
- package/lib/cli-output-controller.js +460 -0
- package/lib/cli-runtime-controller.js +350 -0
- package/lib/cli-state-store.js +32 -0
- package/lib/cli-state.js +41 -0
- package/lib/command-controller.js +159 -126
- package/lib/compact-context-state.js +22 -22
- package/lib/components/assistant-message.js +169 -0
- package/lib/components/composer-area.js +25 -0
- package/lib/components/dynamic-block.js +20 -0
- package/lib/components/monitor-stack.js +35 -0
- package/lib/components/text-block.js +47 -0
- package/lib/components/tool-block.js +122 -0
- package/lib/composer-terminal.js +224 -203
- package/lib/event-controller.js +243 -242
- package/lib/frontend-cli-implementation.js +656 -1111
- package/lib/input-controller.js +75 -94
- package/lib/input-errors.js +3 -3
- package/lib/interrupt-state.js +9 -9
- package/lib/line-editor.js +541 -541
- package/lib/local-slash-commands.js +217 -0
- package/lib/markdown-lines.js +103 -0
- package/lib/model-menu-state.js +50 -50
- package/lib/one-shot-progress.js +145 -0
- package/lib/one-shot.js +228 -0
- package/lib/question-menu-state.js +61 -0
- package/lib/rendering.js +1295 -1037
- package/lib/runtime-client.js +241 -193
- package/lib/runtime-env.js +21 -21
- package/lib/runtime-protocol.js +122 -15
- package/lib/slash-command-mode.js +16 -27
- package/lib/slash-menu-state.js +59 -59
- package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
- package/lib/terminal-key.js +97 -97
- package/lib/text-width.js +335 -151
- package/lib/theme-menu-state.js +31 -0
- package/lib/theme.js +134 -0
- package/lib/tool-display.js +680 -0
- package/lib/tui/component.js +55 -0
- package/lib/tui/cursor.js +29 -0
- package/lib/tui/input-buffer.js +172 -0
- package/lib/tui/tui.js +591 -0
- package/lib/turn-controller.js +68 -78
- package/package.json +28 -28
- package/lib/assistant-stream-buffer.js +0 -25
- package/lib/terminal-ui.js +0 -581
package/bin/rind.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { runFrontendCli } from "../lib/frontend-cli.js";
|
|
4
|
-
|
|
5
|
-
await runFrontendCli(process.argv.slice(2));
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runFrontendCli } from "../lib/frontend-cli.js";
|
|
4
|
+
|
|
5
|
+
await runFrontendCli(process.argv.slice(2));
|
|
@@ -1,265 +1,179 @@
|
|
|
1
|
-
import { graphemes, textWidth } from "./text-width.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.pending
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (this.atLineStart) {
|
|
132
|
-
output += CONTENT_PREFIX;
|
|
133
|
-
this.atLineStart = false;
|
|
134
|
-
this.visibleColumn = prefixWidth;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const list = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
|
|
183
|
-
if (list) {
|
|
184
|
-
const marker = /^\d+\.$/.test(list[2]) ? list[2] : "•";
|
|
185
|
-
return `${list[1]}${dim(`${marker} `, color)}${renderInline(list[3], color)}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return renderInline(line, color);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function renderInline(text, color, baseStyle = "") {
|
|
192
|
-
const source = String(text || "");
|
|
193
|
-
let output = "";
|
|
194
|
-
let index = 0;
|
|
195
|
-
for (const match of source.matchAll(INLINE_TOKEN_RE)) {
|
|
196
|
-
output += styled(source.slice(index, match.index), color, baseStyle);
|
|
197
|
-
output += renderInlineToken(match[0], color, baseStyle);
|
|
198
|
-
index = match.index + match[0].length;
|
|
199
|
-
}
|
|
200
|
-
return output + styled(source.slice(index), color, baseStyle);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function renderInlineToken(token, color, baseStyle) {
|
|
204
|
-
const link = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
205
|
-
if (link) {
|
|
206
|
-
return `${renderInline(link[1], color, baseStyle)} ${dim(`(${link[2]})`, color)}`;
|
|
207
|
-
}
|
|
208
|
-
if (token.startsWith("`") && token.endsWith("`")) {
|
|
209
|
-
return styled(token.slice(1, -1), color, "inlineCode");
|
|
210
|
-
}
|
|
211
|
-
if (token.startsWith("**") && token.endsWith("**")) {
|
|
212
|
-
return styled(token.slice(2, -2), color, baseStyle || "emphasis");
|
|
213
|
-
}
|
|
214
|
-
return token;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function isPlainLine(line) {
|
|
218
|
-
if (!line) {
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
if (PLAIN_TEXT_RE.test(line)) {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
const stripped = line.trimStart();
|
|
225
|
-
return !stripped.match(/^([-*+]|\d+\.)\s+/);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function isTableLine(line, inCodeBlock) {
|
|
229
|
-
const stripped = line.trim();
|
|
230
|
-
return !inCodeBlock && stripped.includes("|") && stripped.split("|").length > 2;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function parseTableRow(line) {
|
|
234
|
-
let stripped = line.trim();
|
|
235
|
-
if (stripped.startsWith("|")) {
|
|
236
|
-
stripped = stripped.slice(1);
|
|
237
|
-
}
|
|
238
|
-
if (stripped.endsWith("|")) {
|
|
239
|
-
stripped = stripped.slice(0, -1);
|
|
240
|
-
}
|
|
241
|
-
return stripped.split("|").map((cell) => cell.trim());
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function codeOpenLabel(label) {
|
|
245
|
-
return label ? `┌ code ${label}` : "┌ code";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function styled(text, color, style) {
|
|
249
|
-
if (!text || !color || !style) {
|
|
250
|
-
return text;
|
|
251
|
-
}
|
|
252
|
-
const codes = {
|
|
253
|
-
codeBlock: "38;5;110",
|
|
254
|
-
emphasis: "1;38;5;221",
|
|
255
|
-
heading: "1;38;5;81",
|
|
256
|
-
inlineCode: "38;5;215",
|
|
257
|
-
tableHeader: "1;38;5;81",
|
|
258
|
-
};
|
|
259
|
-
const code = codes[style] || codes.emphasis;
|
|
260
|
-
return `\x1b[${code}m${text}\x1b[0m`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function dim(text, color) {
|
|
264
|
-
return color ? `\x1b[2m${text}\x1b[0m` : text;
|
|
265
|
-
}
|
|
1
|
+
import { graphemes, textWidth } from "./text-width.js";
|
|
2
|
+
import {
|
|
3
|
+
codeOpenLabel,
|
|
4
|
+
dim,
|
|
5
|
+
isPlainLine,
|
|
6
|
+
isTableLine,
|
|
7
|
+
parseTableRow,
|
|
8
|
+
renderInline,
|
|
9
|
+
renderMarkdownishLine,
|
|
10
|
+
styled,
|
|
11
|
+
} from "./markdown-lines.js";
|
|
12
|
+
|
|
13
|
+
const CONTENT_PREFIX = " ";
|
|
14
|
+
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
15
|
+
|
|
16
|
+
export { CONTENT_PREFIX };
|
|
17
|
+
|
|
18
|
+
export class AssistantRenderer {
|
|
19
|
+
constructor(write, options = {}) {
|
|
20
|
+
this.write = write;
|
|
21
|
+
this.color = options.color ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
|
|
22
|
+
this.pending = "";
|
|
23
|
+
this.inCodeBlock = false;
|
|
24
|
+
this.lineOpen = false;
|
|
25
|
+
this.atLineStart = true;
|
|
26
|
+
this.visibleColumn = 0;
|
|
27
|
+
this.columns = options.columns;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
append(text) {
|
|
31
|
+
this.pending += String(text || "");
|
|
32
|
+
while (true) {
|
|
33
|
+
const newlineIndex = this.pending.indexOf("\n");
|
|
34
|
+
if (newlineIndex === -1) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
const line = this.pending.slice(0, newlineIndex);
|
|
38
|
+
this.pending = this.pending.slice(newlineIndex + 1);
|
|
39
|
+
this.renderLine(line, true);
|
|
40
|
+
}
|
|
41
|
+
this.flushPlainPending();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
finish() {
|
|
45
|
+
if (this.pending) {
|
|
46
|
+
this.renderLine(this.pending, false);
|
|
47
|
+
this.pending = "";
|
|
48
|
+
}
|
|
49
|
+
if (this.lineOpen) {
|
|
50
|
+
this.writeText("\n");
|
|
51
|
+
this.lineOpen = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
flushPlainPending() {
|
|
56
|
+
if (!this.pending || this.inCodeBlock || !isPlainLine(this.pending)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.writePlain(this.pending, false);
|
|
60
|
+
this.pending = "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
renderLine(line, newline) {
|
|
64
|
+
if (isTableLine(line, this.inCodeBlock)) {
|
|
65
|
+
this.renderTableLine(line, newline);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (line.trim().startsWith("```")) {
|
|
69
|
+
this.renderCodeFence(line, newline);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (this.inCodeBlock) {
|
|
73
|
+
this.writeStyled(styled(line, this.color, "codeBlock"), newline);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (isPlainLine(line)) {
|
|
77
|
+
this.writePlain(line, newline);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.writeStyled(renderMarkdownishLine(line, this.color), newline);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
renderTableLine(line, newline) {
|
|
84
|
+
const cells = parseTableRow(line);
|
|
85
|
+
if (!cells.length || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const rendered = cells.map((cell, index) =>
|
|
89
|
+
renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
|
|
90
|
+
);
|
|
91
|
+
this.writeStyled(rendered.join(dim(" | ", this.color)), newline);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
renderCodeFence(line, newline) {
|
|
95
|
+
const opening = !this.inCodeBlock;
|
|
96
|
+
this.inCodeBlock = opening;
|
|
97
|
+
const label = opening ? line.trim().slice(3).trim().slice(0, 32) : "";
|
|
98
|
+
this.writeStyled(dim(opening ? codeOpenLabel(label) : "└ end", this.color), newline);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
writePlain(text, newline) {
|
|
102
|
+
this.writeText(text + (newline ? "\n" : ""));
|
|
103
|
+
this.lineOpen = Boolean(text) && !newline;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
writeStyled(text, newline) {
|
|
107
|
+
this.writeText(text + (newline ? "\n" : ""));
|
|
108
|
+
this.lineOpen = Boolean(text) && !newline;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
writeText(text) {
|
|
112
|
+
const parts = String(text || "").split(/(\r\n|\r|\n)/);
|
|
113
|
+
const maxWidth = Math.max(1, Math.floor(Number(this.columns ?? process.stdout.columns ?? 80) || 80));
|
|
114
|
+
const prefixWidth = textWidth(CONTENT_PREFIX);
|
|
115
|
+
let output = "";
|
|
116
|
+
for (const part of parts) {
|
|
117
|
+
if (!part) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (part === "\r\n" || part === "\r" || part === "\n") {
|
|
121
|
+
if (this.atLineStart) {
|
|
122
|
+
output += CONTENT_PREFIX;
|
|
123
|
+
}
|
|
124
|
+
output += part;
|
|
125
|
+
this.atLineStart = true;
|
|
126
|
+
this.visibleColumn = 0;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const segment of ansiSegments(part)) {
|
|
130
|
+
if (segment.ansi) {
|
|
131
|
+
if (this.atLineStart) {
|
|
132
|
+
output += CONTENT_PREFIX;
|
|
133
|
+
this.atLineStart = false;
|
|
134
|
+
this.visibleColumn = prefixWidth;
|
|
135
|
+
}
|
|
136
|
+
output += segment.text;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const grapheme of graphemes(segment.text)) {
|
|
140
|
+
const segmentWidth = textWidth(grapheme);
|
|
141
|
+
if (this.atLineStart) {
|
|
142
|
+
output += CONTENT_PREFIX;
|
|
143
|
+
this.atLineStart = false;
|
|
144
|
+
this.visibleColumn = prefixWidth;
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
segmentWidth > 0
|
|
148
|
+
&& this.visibleColumn > prefixWidth
|
|
149
|
+
&& this.visibleColumn + segmentWidth > maxWidth
|
|
150
|
+
) {
|
|
151
|
+
output += `\n${CONTENT_PREFIX}`;
|
|
152
|
+
this.visibleColumn = prefixWidth;
|
|
153
|
+
}
|
|
154
|
+
output += grapheme;
|
|
155
|
+
this.visibleColumn += segmentWidth;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (output) {
|
|
160
|
+
this.write(output);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ansiSegments(value) {
|
|
166
|
+
const segments = [];
|
|
167
|
+
let position = 0;
|
|
168
|
+
for (const match of value.matchAll(ANSI_SEQUENCE)) {
|
|
169
|
+
if (match.index > position) {
|
|
170
|
+
segments.push({ text: value.slice(position, match.index), ansi: false });
|
|
171
|
+
}
|
|
172
|
+
segments.push({ text: match[0], ansi: true });
|
|
173
|
+
position = match.index + match[0].length;
|
|
174
|
+
}
|
|
175
|
+
if (position < value.length) {
|
|
176
|
+
segments.push({ text: value.slice(position), ansi: false });
|
|
177
|
+
}
|
|
178
|
+
return segments;
|
|
179
|
+
}
|
package/lib/choice-menu-state.js
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
export function createChoiceMenuState(options,
|
|
2
|
-
const items = normalizeOptions(options);
|
|
3
|
-
let selected = items.indexOf(
|
|
4
|
-
if (selected < 0) {
|
|
5
|
-
selected = 0;
|
|
6
|
-
}
|
|
7
|
-
return {
|
|
8
|
-
options() {
|
|
9
|
-
return items;
|
|
10
|
-
},
|
|
11
|
-
selectedIndex() {
|
|
12
|
-
return selected;
|
|
13
|
-
},
|
|
14
|
-
selectedOption() {
|
|
15
|
-
return items[selected] || "";
|
|
16
|
-
},
|
|
17
|
-
handleKey(key = {}) {
|
|
18
|
-
if (!items.length) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
if (key.name === "up" || key.text === "k") {
|
|
22
|
-
selected = selected <= 0 ? items.length - 1 : selected - 1;
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
if (key.name === "down" || key.text === "j") {
|
|
26
|
-
selected = selected >= items.length - 1 ? 0 : selected + 1;
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalizeOptions(options) {
|
|
35
|
-
const seen = new Set();
|
|
36
|
-
const items = [];
|
|
37
|
-
for (const option of Array.isArray(options) ? options : []) {
|
|
38
|
-
const value = String(option || "").trim();
|
|
39
|
-
if (!value || seen.has(value)) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
seen.add(value);
|
|
43
|
-
items.push(value);
|
|
44
|
-
}
|
|
45
|
-
return items;
|
|
46
|
-
}
|
|
1
|
+
export function createChoiceMenuState(options, selectedValue = "") {
|
|
2
|
+
const items = normalizeOptions(options);
|
|
3
|
+
let selected = items.indexOf(selectedValue);
|
|
4
|
+
if (selected < 0) {
|
|
5
|
+
selected = 0;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
options() {
|
|
9
|
+
return items;
|
|
10
|
+
},
|
|
11
|
+
selectedIndex() {
|
|
12
|
+
return selected;
|
|
13
|
+
},
|
|
14
|
+
selectedOption() {
|
|
15
|
+
return items[selected] || "";
|
|
16
|
+
},
|
|
17
|
+
handleKey(key = {}) {
|
|
18
|
+
if (!items.length) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (key.name === "up" || key.text === "k") {
|
|
22
|
+
selected = selected <= 0 ? items.length - 1 : selected - 1;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (key.name === "down" || key.text === "j") {
|
|
26
|
+
selected = selected >= items.length - 1 ? 0 : selected + 1;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeOptions(options) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const items = [];
|
|
37
|
+
for (const option of Array.isArray(options) ? options : []) {
|
|
38
|
+
const value = String(option || "").trim();
|
|
39
|
+
if (!value || seen.has(value)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(value);
|
|
43
|
+
items.push(value);
|
|
44
|
+
}
|
|
45
|
+
return items;
|
|
46
|
+
}
|