@ridit/lens 0.1.6 → 0.1.8

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.
@@ -1,29 +1,415 @@
1
+ // StaticMessage.tsx
1
2
  import React from "react";
2
3
  import { Box, Text } from "ink";
3
- import { ACCENT } from "../../colors";
4
+ import { tokenize } from "sugar-high";
5
+ import {
6
+ ACCENT,
7
+ TOKEN_KEYWORD,
8
+ TOKEN_STRING,
9
+ TOKEN_NUMBER,
10
+ TOKEN_PROPERTY,
11
+ TOKEN_ENTITY,
12
+ TOKEN_TEXT,
13
+ TOKEN_MUTED,
14
+ TOKEN_COMMENT,
15
+ } from "../../colors";
4
16
  import type { Message } from "../../types/chat";
5
17
 
18
+ const T_IDENTIFIER = 0;
19
+ const T_KEYWORD = 1;
20
+ const T_STRING = 2;
21
+ const T_CLS_NUMBER = 3;
22
+ const T_PROPERTY = 4;
23
+ const T_ENTITY = 5;
24
+ const T_JSX_LITERAL = 6;
25
+ const T_SIGN = 7;
26
+ const T_COMMENT = 8;
27
+ const T_BREAK = 9;
28
+ const T_SPACE = 10;
29
+
30
+ const JS_LANGS = new Set([
31
+ "js",
32
+ "javascript",
33
+ "jsx",
34
+ "ts",
35
+ "typescript",
36
+ "tsx",
37
+ "mjs",
38
+ "cjs",
39
+ ]);
40
+
41
+ function tokenColor(type: number): string {
42
+ switch (type) {
43
+ case T_KEYWORD:
44
+ return TOKEN_KEYWORD;
45
+ case T_STRING:
46
+ return TOKEN_STRING;
47
+ case T_CLS_NUMBER:
48
+ return TOKEN_NUMBER;
49
+ case T_PROPERTY:
50
+ return TOKEN_PROPERTY;
51
+ case T_ENTITY:
52
+ return TOKEN_ENTITY;
53
+ case T_JSX_LITERAL:
54
+ return TOKEN_TEXT;
55
+ case T_SIGN:
56
+ return TOKEN_MUTED;
57
+ case T_COMMENT:
58
+ return TOKEN_COMMENT;
59
+ case T_IDENTIFIER:
60
+ return TOKEN_TEXT;
61
+ default:
62
+ return TOKEN_TEXT;
63
+ }
64
+ }
65
+
66
+ type SimpleToken = { color: string; text: string };
67
+
68
+ const PYTHON_KW = new Set([
69
+ "def",
70
+ "class",
71
+ "import",
72
+ "from",
73
+ "return",
74
+ "if",
75
+ "elif",
76
+ "else",
77
+ "for",
78
+ "while",
79
+ "in",
80
+ "not",
81
+ "and",
82
+ "or",
83
+ "is",
84
+ "None",
85
+ "True",
86
+ "False",
87
+ "try",
88
+ "except",
89
+ "finally",
90
+ "with",
91
+ "as",
92
+ "pass",
93
+ "break",
94
+ "continue",
95
+ "raise",
96
+ "yield",
97
+ "lambda",
98
+ "async",
99
+ "await",
100
+ "del",
101
+ "global",
102
+ "nonlocal",
103
+ "assert",
104
+ ]);
105
+ const RUST_KW = new Set([
106
+ "fn",
107
+ "let",
108
+ "mut",
109
+ "const",
110
+ "struct",
111
+ "enum",
112
+ "impl",
113
+ "trait",
114
+ "pub",
115
+ "use",
116
+ "mod",
117
+ "match",
118
+ "if",
119
+ "else",
120
+ "loop",
121
+ "while",
122
+ "for",
123
+ "in",
124
+ "return",
125
+ "self",
126
+ "Self",
127
+ "super",
128
+ "where",
129
+ "type",
130
+ "as",
131
+ "ref",
132
+ "move",
133
+ "unsafe",
134
+ "extern",
135
+ "dyn",
136
+ "async",
137
+ "await",
138
+ "true",
139
+ "false",
140
+ "Some",
141
+ "None",
142
+ "Ok",
143
+ "Err",
144
+ ]);
145
+ const GO_KW = new Set([
146
+ "func",
147
+ "var",
148
+ "const",
149
+ "type",
150
+ "struct",
151
+ "interface",
152
+ "package",
153
+ "import",
154
+ "return",
155
+ "if",
156
+ "else",
157
+ "for",
158
+ "range",
159
+ "switch",
160
+ "case",
161
+ "default",
162
+ "break",
163
+ "continue",
164
+ "goto",
165
+ "defer",
166
+ "go",
167
+ "chan",
168
+ "map",
169
+ "make",
170
+ "new",
171
+ "nil",
172
+ "true",
173
+ "false",
174
+ "error",
175
+ ]);
176
+ const SHELL_KW = new Set([
177
+ "if",
178
+ "then",
179
+ "else",
180
+ "elif",
181
+ "fi",
182
+ "for",
183
+ "do",
184
+ "done",
185
+ "while",
186
+ "case",
187
+ "esac",
188
+ "in",
189
+ "function",
190
+ "return",
191
+ "echo",
192
+ "export",
193
+ "local",
194
+ "source",
195
+ "exit",
196
+ ]);
197
+ const CSS_AT = /^@[\w-]+/;
198
+ const CSS_PROP = /^[\w-]+(?=\s*:)/;
199
+
200
+ function tokenizeGeneric(code: string, lang: string): SimpleToken[][] {
201
+ const keywords =
202
+ lang === "python" || lang === "py"
203
+ ? PYTHON_KW
204
+ : lang === "rust" || lang === "rs"
205
+ ? RUST_KW
206
+ : lang === "go"
207
+ ? GO_KW
208
+ : lang === "bash" ||
209
+ lang === "sh" ||
210
+ lang === "shell" ||
211
+ lang === "zsh"
212
+ ? SHELL_KW
213
+ : new Set<string>();
214
+
215
+ const lines = code.split("\n");
216
+ return lines.map((line) => {
217
+ const tokens: SimpleToken[] = [];
218
+ let i = 0;
219
+
220
+ const push = (color: string, text: string) => {
221
+ if (text) tokens.push({ color, text });
222
+ };
223
+
224
+ while (i < line.length) {
225
+ const rest = line.slice(i);
226
+
227
+ const commentPrefixes =
228
+ lang === "python" || lang === "py"
229
+ ? ["#"]
230
+ : lang === "bash" ||
231
+ lang === "sh" ||
232
+ lang === "shell" ||
233
+ lang === "zsh"
234
+ ? ["#"]
235
+ : lang === "css" || lang === "scss"
236
+ ? ["//", "/*"]
237
+ : lang === "html" || lang === "xml"
238
+ ? ["<!--"]
239
+ : lang === "sql"
240
+ ? ["--", "#"]
241
+ : ["//", "#"];
242
+
243
+ let matchedComment = false;
244
+ for (const prefix of commentPrefixes) {
245
+ if (rest.startsWith(prefix)) {
246
+ push(TOKEN_COMMENT, line.slice(i));
247
+ i = line.length;
248
+ matchedComment = true;
249
+ break;
250
+ }
251
+ }
252
+ if (matchedComment) continue;
253
+
254
+ if (line[i] === '"' || line[i] === "'" || line[i] === "`") {
255
+ const quote = line[i]!;
256
+ let j = i + 1;
257
+ while (j < line.length) {
258
+ if (line[j] === "\\") {
259
+ j += 2;
260
+ continue;
261
+ }
262
+ if (line[j] === quote) {
263
+ j++;
264
+ break;
265
+ }
266
+ j++;
267
+ }
268
+ push(TOKEN_STRING, line.slice(i, j));
269
+ i = j;
270
+ continue;
271
+ }
272
+
273
+ const numMatch = rest.match(/^-?\d+\.?\d*/);
274
+ if (numMatch && (i === 0 || !/\w/.test(line[i - 1] ?? ""))) {
275
+ push(TOKEN_NUMBER, numMatch[0]);
276
+ i += numMatch[0].length;
277
+ continue;
278
+ }
279
+
280
+ if (lang === "css" || lang === "scss") {
281
+ const atMatch = rest.match(CSS_AT);
282
+ if (atMatch) {
283
+ push(TOKEN_KEYWORD, atMatch[0]);
284
+ i += atMatch[0].length;
285
+ continue;
286
+ }
287
+ const propMatch = rest.match(CSS_PROP);
288
+ if (propMatch) {
289
+ push(TOKEN_PROPERTY, propMatch[0]);
290
+ i += propMatch[0].length;
291
+ continue;
292
+ }
293
+ }
294
+
295
+ if ((lang === "html" || lang === "xml") && line[i] === "<") {
296
+ const tagMatch = rest.match(/^<\/?[\w:-]+/);
297
+ if (tagMatch) {
298
+ push(TOKEN_ENTITY, tagMatch[0]);
299
+ i += tagMatch[0].length;
300
+ continue;
301
+ }
302
+ }
303
+
304
+ const wordMatch = rest.match(/^[a-zA-Z_$][\w$]*/);
305
+ if (wordMatch) {
306
+ const word = wordMatch[0];
307
+ push(keywords.has(word) ? TOKEN_KEYWORD : TOKEN_TEXT, word);
308
+ i += word.length;
309
+ continue;
310
+ }
311
+
312
+ const opMatch = rest.match(/^[^\w\s"'`]+/);
313
+ if (opMatch) {
314
+ push(TOKEN_MUTED, opMatch[0]);
315
+ i += opMatch[0].length;
316
+ continue;
317
+ }
318
+
319
+ push(TOKEN_TEXT, line[i]!);
320
+ i++;
321
+ }
322
+
323
+ return tokens;
324
+ });
325
+ }
326
+
327
+ function HighlightedLine({ tokens }: { tokens: SimpleToken[] }) {
328
+ return (
329
+ <Text>
330
+ {" "}
331
+ {tokens.map((t, i) => (
332
+ <Text key={i} color={t.color as any}>
333
+ {t.text}
334
+ </Text>
335
+ ))}
336
+ </Text>
337
+ );
338
+ }
339
+
340
+ function CodeBlock({ lang, code }: { lang: string; code: string }) {
341
+ const normalizedLang = lang.toLowerCase().trim();
342
+
343
+ let lines: SimpleToken[][];
344
+
345
+ if (JS_LANGS.has(normalizedLang)) {
346
+ const tokens = tokenize(code);
347
+ const lineAccum: SimpleToken[][] = [[]];
348
+
349
+ for (const [type, value] of tokens) {
350
+ if (type === T_BREAK) {
351
+ lineAccum.push([]);
352
+ } else if (type !== T_SPACE) {
353
+ lineAccum[lineAccum.length - 1]!.push({
354
+ color: tokenColor(type),
355
+ text: value,
356
+ });
357
+ } else {
358
+ lineAccum[lineAccum.length - 1]!.push({
359
+ color: TOKEN_TEXT,
360
+ text: value,
361
+ });
362
+ }
363
+ }
364
+ lines = lineAccum;
365
+ } else if (normalizedLang) {
366
+ lines = tokenizeGeneric(code, normalizedLang);
367
+ } else {
368
+ lines = code.split("\n").map((l) => [{ color: TOKEN_TEXT, text: l }]);
369
+ }
370
+
371
+ return (
372
+ <Box flexDirection="column" marginY={1} marginLeft={2}>
373
+ {normalizedLang ? (
374
+ <Text color={TOKEN_MUTED} dimColor>
375
+ {normalizedLang}
376
+ </Text>
377
+ ) : null}
378
+ {lines.map((lineTokens, i) => (
379
+ <HighlightedLine key={i} tokens={lineTokens} />
380
+ ))}
381
+ </Box>
382
+ );
383
+ }
384
+
6
385
  function InlineText({ text }: { text: string }) {
7
- const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
386
+ const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g);
8
387
  return (
9
388
  <>
10
389
  {parts.map((part, i) => {
11
- if (part.startsWith("`") && part.endsWith("`")) {
390
+ if (part.startsWith("**") && part.endsWith("**")) {
12
391
  return (
13
- <Text key={i} color={ACCENT}>
392
+ <Text key={i} bold color={TOKEN_TEXT}>
393
+ {part.slice(2, -2)}
394
+ </Text>
395
+ );
396
+ }
397
+ if (part.startsWith("*") && part.endsWith("*") && part.length > 2) {
398
+ return (
399
+ <Text key={i} italic color={TOKEN_TEXT}>
14
400
  {part.slice(1, -1)}
15
401
  </Text>
16
402
  );
17
403
  }
18
- if (part.startsWith("**") && part.endsWith("**")) {
404
+ if (part.startsWith("`") && part.endsWith("`")) {
19
405
  return (
20
- <Text key={i} bold color="white">
21
- {part.slice(2, -2)}
406
+ <Text key={i} color={ACCENT}>
407
+ {part.slice(1, -1)}
22
408
  </Text>
23
409
  );
24
410
  }
25
411
  return (
26
- <Text key={i} color="white">
412
+ <Text key={i} color={TOKEN_TEXT}>
27
413
  {part}
28
414
  </Text>
29
415
  );
@@ -32,68 +418,177 @@ function InlineText({ text }: { text: string }) {
32
418
  );
33
419
  }
34
420
 
35
- function CodeBlock({ lang, code }: { lang: string; code: string }) {
36
- return (
37
- <Box flexDirection="column" marginY={1} marginLeft={2}>
38
- {lang && <Text color="gray">{lang}</Text>}
39
- {code.split("\n").map((line, i) => (
40
- <Text key={i} color={ACCENT}>
41
- {" "}
42
- {line}
421
+ function Heading({ level, text }: { level: 1 | 2 | 3; text: string }) {
422
+ if (level === 1) {
423
+ return (
424
+ <Box marginTop={1}>
425
+ <Text color={ACCENT} bold underline>
426
+ {text}
43
427
  </Text>
44
- ))}
428
+ </Box>
429
+ );
430
+ }
431
+ if (level === 2) {
432
+ return (
433
+ <Box marginTop={1}>
434
+ <Text color={ACCENT} bold>
435
+ {text}
436
+ </Text>
437
+ </Box>
438
+ );
439
+ }
440
+ return (
441
+ <Box marginTop={1}>
442
+ <Text color={TOKEN_TEXT} bold>
443
+ {text}
444
+ </Text>
45
445
  </Box>
46
446
  );
47
447
  }
48
448
 
49
- function MessageBody({ content }: { content: string }) {
449
+ function BulletItem({ text }: { text: string }) {
450
+ return (
451
+ <Box gap={1}>
452
+ <Text color={ACCENT}>{"*"}</Text>
453
+ <Box flexShrink={1}>
454
+ <InlineText text={text} />
455
+ </Box>
456
+ </Box>
457
+ );
458
+ }
459
+
460
+ function NumberedItem({ num, text }: { num: string; text: string }) {
461
+ return (
462
+ <Box gap={1}>
463
+ <Text color={TOKEN_MUTED}>{num}.</Text>
464
+ <Box flexShrink={1}>
465
+ <InlineText text={text} />
466
+ </Box>
467
+ </Box>
468
+ );
469
+ }
470
+
471
+ function BlockQuote({ text }: { text: string }) {
472
+ return (
473
+ <Box gap={1} marginLeft={1}>
474
+ <Text color={TOKEN_MUTED}>{"│"}</Text>
475
+ <Text color={TOKEN_MUTED} dimColor>
476
+ {text}
477
+ </Text>
478
+ </Box>
479
+ );
480
+ }
481
+
482
+ type Block =
483
+ | { type: "heading"; level: 1 | 2 | 3; text: string }
484
+ | { type: "code"; lang: string; code: string }
485
+ | { type: "bullet"; text: string }
486
+ | { type: "numbered"; num: string; text: string }
487
+ | { type: "blockquote"; text: string }
488
+ | { type: "hr" }
489
+ | { type: "paragraph"; text: string };
490
+
491
+ function parseBlocks(content: string): Block[] {
492
+ const blocks: Block[] = [];
50
493
  const segments = content.split(/(```[\s\S]*?```)/g);
51
494
 
495
+ for (const seg of segments) {
496
+ if (seg.startsWith("```")) {
497
+ const lines = seg.slice(3).split("\n");
498
+ const lang = lines[0]?.trim() ?? "";
499
+ const code = lines
500
+ .slice(1)
501
+ .join("\n")
502
+ .replace(/```\s*$/, "")
503
+ .trimEnd();
504
+ blocks.push({ type: "code", lang, code });
505
+ continue;
506
+ }
507
+
508
+ for (const line of seg.split("\n")) {
509
+ const trimmed = line.trim();
510
+ if (!trimmed) continue;
511
+
512
+ const h3 = trimmed.match(/^### (.+)$/);
513
+ const h2 = trimmed.match(/^## (.+)$/);
514
+ const h1 = trimmed.match(/^# (.+)$/);
515
+ if (h3) {
516
+ blocks.push({ type: "heading", level: 3, text: h3[1]! });
517
+ continue;
518
+ }
519
+ if (h2) {
520
+ blocks.push({ type: "heading", level: 2, text: h2[1]! });
521
+ continue;
522
+ }
523
+ if (h1) {
524
+ blocks.push({ type: "heading", level: 1, text: h1[1]! });
525
+ continue;
526
+ }
527
+
528
+ if (/^[-*_]{3,}$/.test(trimmed)) {
529
+ blocks.push({ type: "hr" });
530
+ continue;
531
+ }
532
+
533
+ if (trimmed.startsWith("> ")) {
534
+ blocks.push({ type: "blockquote", text: trimmed.slice(2).trim() });
535
+ continue;
536
+ }
537
+
538
+ if (/^[-*•]\s/.test(trimmed)) {
539
+ blocks.push({ type: "bullet", text: trimmed.slice(2).trim() });
540
+ continue;
541
+ }
542
+
543
+ const numMatch = trimmed.match(/^(\d+)\.\s(.+)/);
544
+ if (numMatch) {
545
+ blocks.push({
546
+ type: "numbered",
547
+ num: numMatch[1]!,
548
+ text: numMatch[2]!,
549
+ });
550
+ continue;
551
+ }
552
+
553
+ blocks.push({ type: "paragraph", text: trimmed });
554
+ }
555
+ }
556
+
557
+ return blocks;
558
+ }
559
+
560
+ function MessageBody({ content }: { content: string }) {
561
+ const blocks = parseBlocks(content);
562
+
52
563
  return (
53
564
  <Box flexDirection="column">
54
- {segments.map((seg, si) => {
55
- if (seg.startsWith("```")) {
56
- const lines = seg.slice(3).split("\n");
57
- const lang = lines[0]?.trim() ?? "";
58
- const code = lines
59
- .slice(1)
60
- .join("\n")
61
- .replace(/```\s*$/, "")
62
- .trimEnd();
63
- return <CodeBlock key={si} lang={lang} code={code} />;
565
+ {blocks.map((block, i) => {
566
+ switch (block.type) {
567
+ case "heading":
568
+ return <Heading key={i} level={block.level} text={block.text} />;
569
+ case "code":
570
+ return <CodeBlock key={i} lang={block.lang} code={block.code} />;
571
+ case "bullet":
572
+ return <BulletItem key={i} text={block.text} />;
573
+ case "numbered":
574
+ return <NumberedItem key={i} num={block.num} text={block.text} />;
575
+ case "blockquote":
576
+ return <BlockQuote key={i} text={block.text} />;
577
+ case "hr":
578
+ return (
579
+ <Box key={i} marginY={1}>
580
+ <Text color={TOKEN_MUTED} dimColor>
581
+ {"─".repeat(40)}
582
+ </Text>
583
+ </Box>
584
+ );
585
+ case "paragraph":
586
+ return (
587
+ <Box key={i}>
588
+ <InlineText text={block.text} />
589
+ </Box>
590
+ );
64
591
  }
65
-
66
- const lines = seg.split("\n").filter((l) => l.trim() !== "");
67
- return (
68
- <Box key={si} flexDirection="column">
69
- {lines.map((line, li) => {
70
- if (line.match(/^[-*•]\s/)) {
71
- return (
72
- <Box key={li} gap={1}>
73
- <Text color={ACCENT}>*</Text>
74
- <InlineText text={line.slice(2).trim()} />
75
- </Box>
76
- );
77
- }
78
-
79
- if (line.match(/^\d+\.\s/)) {
80
- const num = line.match(/^(\d+)\.\s/)![1];
81
- return (
82
- <Box key={li} gap={1}>
83
- <Text color="gray">{num}.</Text>
84
- <InlineText text={line.replace(/^\d+\.\s/, "").trim()} />
85
- </Box>
86
- );
87
- }
88
-
89
- return (
90
- <Box key={li}>
91
- <InlineText text={line} />
92
- </Box>
93
- );
94
- })}
95
- </Box>
96
- );
97
592
  })}
98
593
  </Box>
99
594
  );
@@ -103,8 +598,8 @@ export function StaticMessage({ msg }: { msg: Message }) {
103
598
  if (msg.role === "user") {
104
599
  return (
105
600
  <Box marginBottom={1} gap={1}>
106
- <Text color="gray">{">"}</Text>
107
- <Text color="white" bold>
601
+ <Text color={TOKEN_MUTED}>{">"}</Text>
602
+ <Text color={TOKEN_TEXT} bold>
108
603
  {msg.content}
109
604
  </Text>
110
605
  </Box>
@@ -116,6 +611,12 @@ export function StaticMessage({ msg }: { msg: Message }) {
116
611
  shell: "$",
117
612
  fetch: "~>",
118
613
  "read-file": "r",
614
+ "read-folder": "d",
615
+ grep: "/",
616
+ "delete-file": "x",
617
+ "delete-folder": "X",
618
+ "open-url": "↗",
619
+ "generate-pdf": "P",
119
620
  "write-file": "w",
120
621
  search: "?",
121
622
  };
@@ -131,14 +632,17 @@ export function StaticMessage({ msg }: { msg: Message }) {
131
632
  <Box flexDirection="column" marginBottom={1}>
132
633
  <Box gap={1}>
133
634
  <Text color={msg.approved ? ACCENT : "red"}>{icon}</Text>
134
- <Text color={msg.approved ? "gray" : "red"} dimColor={!msg.approved}>
635
+ <Text
636
+ color={msg.approved ? TOKEN_MUTED : "red"}
637
+ dimColor={!msg.approved}
638
+ >
135
639
  {label}
136
640
  </Text>
137
641
  {!msg.approved && <Text color="red">denied</Text>}
138
642
  </Box>
139
643
  {msg.approved && msg.result && (
140
644
  <Box marginLeft={2}>
141
- <Text color="gray">
645
+ <Text color={TOKEN_MUTED}>
142
646
  {msg.result.split("\n")[0]?.slice(0, 120)}
143
647
  {(msg.result.split("\n")[0]?.length ?? 0) > 120 ? "…" : ""}
144
648
  </Text>
@@ -156,10 +660,13 @@ export function StaticMessage({ msg }: { msg: Message }) {
156
660
  <MessageBody content={msg.content} />
157
661
  </Box>
158
662
  <Box marginLeft={2} gap={1}>
159
- <Text color={msg.applied ? "green" : "gray"}>
663
+ <Text color={msg.applied ? "green" : TOKEN_MUTED}>
160
664
  {msg.applied ? "✓" : "·"}
161
665
  </Text>
162
- <Text color={msg.applied ? "green" : "gray"} dimColor={!msg.applied}>
666
+ <Text
667
+ color={msg.applied ? "green" : TOKEN_MUTED}
668
+ dimColor={!msg.applied}
669
+ >
163
670
  {msg.applied ? "changes applied" : "changes skipped"}
164
671
  </Text>
165
672
  </Box>