@ridit/lens 0.1.8 → 0.2.0

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,415 +1,29 @@
1
- // StaticMessage.tsx
2
1
  import React from "react";
3
2
  import { Box, Text } from "ink";
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";
3
+ import { ACCENT } from "../../colors";
16
4
  import type { Message } from "../../types/chat";
17
5
 
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
-
385
6
  function InlineText({ text }: { text: string }) {
386
- const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g);
7
+ const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
387
8
  return (
388
9
  <>
389
10
  {parts.map((part, i) => {
390
- if (part.startsWith("**") && part.endsWith("**")) {
391
- return (
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) {
11
+ if (part.startsWith("`") && part.endsWith("`")) {
398
12
  return (
399
- <Text key={i} italic color={TOKEN_TEXT}>
13
+ <Text key={i} color={ACCENT}>
400
14
  {part.slice(1, -1)}
401
15
  </Text>
402
16
  );
403
17
  }
404
- if (part.startsWith("`") && part.endsWith("`")) {
18
+ if (part.startsWith("**") && part.endsWith("**")) {
405
19
  return (
406
- <Text key={i} color={ACCENT}>
407
- {part.slice(1, -1)}
20
+ <Text key={i} bold color="white">
21
+ {part.slice(2, -2)}
408
22
  </Text>
409
23
  );
410
24
  }
411
25
  return (
412
- <Text key={i} color={TOKEN_TEXT}>
26
+ <Text key={i} color="white">
413
27
  {part}
414
28
  </Text>
415
29
  );
@@ -418,177 +32,67 @@ function InlineText({ text }: { text: string }) {
418
32
  );
419
33
  }
420
34
 
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}
427
- </Text>
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>
445
- </Box>
446
- );
447
- }
448
-
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 }) {
35
+ function CodeBlock({ lang, code }: { lang: string; code: string }) {
472
36
  return (
473
- <Box gap={1} marginLeft={1}>
474
- <Text color={TOKEN_MUTED}>{""}</Text>
475
- <Text color={TOKEN_MUTED} dimColor>
476
- {text}
477
- </Text>
37
+ <Box flexDirection="column">
38
+ {code.split("\n").map((line, i) => (
39
+ <Text key={i} color={ACCENT}>
40
+ {" "}
41
+ {line}
42
+ </Text>
43
+ ))}
478
44
  </Box>
479
45
  );
480
46
  }
481
47
 
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[] = [];
493
- const segments = content.split(/(```[\s\S]*?```)/g);
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
48
  function MessageBody({ content }: { content: string }) {
561
- const blocks = parseBlocks(content);
49
+ const segments = content.split(/(```[\s\S]*?```)/g);
562
50
 
563
51
  return (
564
52
  <Box flexDirection="column">
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
- );
53
+ {segments.map((seg, si) => {
54
+ if (seg.startsWith("```")) {
55
+ const lines = seg.slice(3).split("\n");
56
+ const lang = lines[0]?.trim() ?? "";
57
+ const code = lines
58
+ .slice(1)
59
+ .join("\n")
60
+ .replace(/```\s*$/, "")
61
+ .trimEnd();
62
+ return <CodeBlock key={si} lang={lang} code={code} />;
591
63
  }
64
+
65
+ const lines = seg.split("\n").filter((l) => l.trim() !== "");
66
+ return (
67
+ <Box key={si} flexDirection="column">
68
+ {lines.map((line, li) => {
69
+ if (line.match(/^[-*•]\s/)) {
70
+ return (
71
+ <Box key={li} gap={1}>
72
+ <Text color={ACCENT}>*</Text>
73
+ <InlineText text={line.slice(2).trim()} />
74
+ </Box>
75
+ );
76
+ }
77
+
78
+ if (line.match(/^\d+\.\s/)) {
79
+ const num = line.match(/^(\d+)\.\s/)![1];
80
+ return (
81
+ <Box key={li} gap={1}>
82
+ <Text color="gray">{num}.</Text>
83
+ <InlineText text={line.replace(/^\d+\.\s/, "").trim()} />
84
+ </Box>
85
+ );
86
+ }
87
+
88
+ return (
89
+ <Box key={li}>
90
+ <InlineText text={line} />
91
+ </Box>
92
+ );
93
+ })}
94
+ </Box>
95
+ );
592
96
  })}
593
97
  </Box>
594
98
  );
@@ -597,9 +101,15 @@ function MessageBody({ content }: { content: string }) {
597
101
  export function StaticMessage({ msg }: { msg: Message }) {
598
102
  if (msg.role === "user") {
599
103
  return (
600
- <Box marginBottom={1} gap={1}>
601
- <Text color={TOKEN_MUTED}>{">"}</Text>
602
- <Text color={TOKEN_TEXT} bold>
104
+ <Box
105
+ marginBottom={1}
106
+ gap={1}
107
+ backgroundColor={"#1a1a1a"}
108
+ paddingLeft={1}
109
+ paddingRight={2}
110
+ >
111
+ <Text color="gray">{">"}</Text>
112
+ <Text color="white" bold>
603
113
  {msg.content}
604
114
  </Text>
605
115
  </Box>
@@ -611,12 +121,6 @@ export function StaticMessage({ msg }: { msg: Message }) {
611
121
  shell: "$",
612
122
  fetch: "~>",
613
123
  "read-file": "r",
614
- "read-folder": "d",
615
- grep: "/",
616
- "delete-file": "x",
617
- "delete-folder": "X",
618
- "open-url": "↗",
619
- "generate-pdf": "P",
620
124
  "write-file": "w",
621
125
  search: "?",
622
126
  };
@@ -632,17 +136,14 @@ export function StaticMessage({ msg }: { msg: Message }) {
632
136
  <Box flexDirection="column" marginBottom={1}>
633
137
  <Box gap={1}>
634
138
  <Text color={msg.approved ? ACCENT : "red"}>{icon}</Text>
635
- <Text
636
- color={msg.approved ? TOKEN_MUTED : "red"}
637
- dimColor={!msg.approved}
638
- >
139
+ <Text color={msg.approved ? "gray" : "red"} dimColor={!msg.approved}>
639
140
  {label}
640
141
  </Text>
641
142
  {!msg.approved && <Text color="red">denied</Text>}
642
143
  </Box>
643
144
  {msg.approved && msg.result && (
644
145
  <Box marginLeft={2}>
645
- <Text color={TOKEN_MUTED}>
146
+ <Text color="gray">
646
147
  {msg.result.split("\n")[0]?.slice(0, 120)}
647
148
  {(msg.result.split("\n")[0]?.length ?? 0) > 120 ? "…" : ""}
648
149
  </Text>
@@ -660,13 +161,10 @@ export function StaticMessage({ msg }: { msg: Message }) {
660
161
  <MessageBody content={msg.content} />
661
162
  </Box>
662
163
  <Box marginLeft={2} gap={1}>
663
- <Text color={msg.applied ? "green" : TOKEN_MUTED}>
164
+ <Text color={msg.applied ? "green" : "gray"}>
664
165
  {msg.applied ? "✓" : "·"}
665
166
  </Text>
666
- <Text
667
- color={msg.applied ? "green" : TOKEN_MUTED}
668
- dimColor={!msg.applied}
669
- >
167
+ <Text color={msg.applied ? "green" : "gray"} dimColor={!msg.applied}>
670
168
  {msg.applied ? "changes applied" : "changes skipped"}
671
169
  </Text>
672
170
  </Box>