@ridit/lens 0.3.6 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ridit/lens",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Know Your Codebase.",
5
5
  "author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
6
6
  "license": "MIT",
@@ -1,23 +1,17 @@
1
1
  import React from "react";
2
- import { Box, Text } from "ink";
3
- import figures from "figures";
4
- import { existsSync } from "fs";
5
- import path from "path";
2
+ import { Box } from "ink";
6
3
  import { ChatRunner } from "../components/chat/ChatRunner";
7
- import { ACCENT } from "../colors";
8
4
 
9
- export const ChatCommand = ({ path: inputPath }: { path: string }) => {
10
- const resolvedPath = path.resolve(inputPath);
11
-
12
- if (!existsSync(resolvedPath)) {
13
- return (
14
- <Box marginTop={1}>
15
- <Text color="red">
16
- {figures.cross} Path not found: {resolvedPath}
17
- </Text>
18
- </Box>
19
- );
20
- }
21
-
22
- return <ChatRunner repoPath={resolvedPath} />;
23
- };
5
+ export function ChatCommand({
6
+ path,
7
+ autoForce = false,
8
+ }: {
9
+ path: string;
10
+ autoForce?: boolean;
11
+ }) {
12
+ return (
13
+ <Box flexDirection="column">
14
+ <ChatRunner repoPath={path} autoForce={autoForce} />
15
+ </Box>
16
+ );
17
+ }
@@ -1,7 +1,9 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
+ import figures from "figures";
3
4
  import { ACCENT, GREEN, RED } from "../../colors";
4
5
  import type { Message } from "../../types/chat";
6
+ import type { DiffLine } from "../repo/DiffViewer";
5
7
 
6
8
  function InlineText({ text }: { text: string }) {
7
9
  const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
@@ -170,10 +172,12 @@ export function StaticMessage({ msg }: { msg: Message }) {
170
172
  if (msg.type === "plan") {
171
173
  return (
172
174
  <Box flexDirection="column" marginBottom={1}>
173
- <Box gap={1}>
174
- <Text color={ACCENT}>*</Text>
175
- <MessageBody content={msg.content} />
176
- </Box>
175
+ {msg.content ? (
176
+ <Box gap={1}>
177
+ <Text color={ACCENT}>*</Text>
178
+ <MessageBody content={msg.content} />
179
+ </Box>
180
+ ) : null}
177
181
  <Box marginLeft={2} gap={1}>
178
182
  <Text color={msg.applied ? GREEN : "gray"}>
179
183
  {msg.applied ? "✓" : "·"}
@@ -182,6 +186,44 @@ export function StaticMessage({ msg }: { msg: Message }) {
182
186
  {msg.applied ? "changes applied" : "changes skipped"}
183
187
  </Text>
184
188
  </Box>
189
+ {msg.applied && msg.diffLines && msg.diffLines.length > 0 && (
190
+ <Box flexDirection="column" marginLeft={2} marginTop={0}>
191
+ {msg.patches.map((patch, fi) => (
192
+ <Box key={patch.path} flexDirection="column">
193
+ <Text bold color={fi % 2 === 0 ? "cyan" : "magenta"}>
194
+ {figures.bullet} {patch.path}
195
+ {patch.isNew ? " (new)" : ""}
196
+ </Text>
197
+ {(msg.diffLines![fi] ?? []).map((line: DiffLine, li: number) => {
198
+ const prefix =
199
+ line.type === "added"
200
+ ? "+"
201
+ : line.type === "removed"
202
+ ? "-"
203
+ : " ";
204
+ const color =
205
+ line.type === "added"
206
+ ? "green"
207
+ : line.type === "removed"
208
+ ? "red"
209
+ : "gray";
210
+ const lineNumStr =
211
+ line.lineNum === -1
212
+ ? " "
213
+ : String(line.lineNum).padStart(3, " ");
214
+ return (
215
+ <Box key={li}>
216
+ <Text color="gray">{lineNumStr} </Text>
217
+ <Text color={color}>
218
+ {prefix} {line.content}
219
+ </Text>
220
+ </Box>
221
+ );
222
+ })}
223
+ </Box>
224
+ ))}
225
+ </Box>
226
+ )}
185
227
  </Box>
186
228
  );
187
229
  }
@@ -1,7 +1,7 @@
1
- import React from "react";
2
- import { Box, Static, Text } from "ink";
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Static, Text, useStdout } from "ink";
3
3
  import Spinner from "ink-spinner";
4
- import TextInput from "ink-text-input";
4
+ import { TextArea } from "./TextArea";
5
5
  import { ACCENT, GREEN, RED } from "../../colors";
6
6
  import { DiffViewer } from "../repo/DiffViewer";
7
7
  import { StaticMessage } from "./ChatMessage";
@@ -24,11 +24,6 @@ function Hint({ text }: { text: string }) {
24
24
  );
25
25
  }
26
26
 
27
- // ── PermissionPrompt ──────────────────────────────────────────────────────────
28
- //
29
- // Works with both the old explicit ToolCall union and the new generic
30
- // { type, _label, _display } shape produced by the plugin system.
31
-
32
27
  export function PermissionPrompt({
33
28
  tool,
34
29
  onDecide,
@@ -40,7 +35,6 @@ export function PermissionPrompt({
40
35
  let label: string;
41
36
  let value: string;
42
37
 
43
- // Generic plugin tool shape
44
38
  if ("_label" in tool) {
45
39
  const iconMap: Record<string, string> = {
46
40
  run: "$",
@@ -61,7 +55,6 @@ export function PermissionPrompt({
61
55
  label = tool._label;
62
56
  value = tool._display;
63
57
  } else {
64
- // Legacy explicit ToolCall union
65
58
  if (tool.type === "shell") {
66
59
  icon = "$";
67
60
  label = "run";
@@ -134,25 +127,37 @@ export function InputBox({
134
127
  onSubmit: (v: string) => void;
135
128
  inputKey?: number;
136
129
  }) {
130
+ const { stdout } = useStdout();
131
+ const [cols, setCols] = useState(stdout?.columns ?? 80);
132
+
133
+ useEffect(() => {
134
+ const handler = () => setCols(stdout?.columns ?? 80);
135
+ stdout?.on("resize", handler);
136
+ return () => {
137
+ stdout?.off("resize", handler);
138
+ };
139
+ }, [stdout]);
140
+
141
+ const rule = "─".repeat(Math.max(1, cols));
142
+
137
143
  return (
138
- <Box
139
- marginTop={1}
140
- borderBottom
141
- borderTop
142
- borderRight={false}
143
- borderLeft={false}
144
- borderColor={"gray"}
145
- borderStyle="single"
146
- >
144
+ <Box flexDirection="column" marginTop={1}>
145
+ <Text color="gray" dimColor>
146
+ {rule}
147
+ </Text>
147
148
  <Box gap={1}>
148
149
  <Text color={ACCENT}>{">"}</Text>
149
- <TextInput
150
+ <TextArea
150
151
  key={inputKey}
151
152
  value={value}
152
153
  onChange={onChange}
153
154
  onSubmit={onSubmit}
155
+ placeholder="ask anything..."
154
156
  />
155
157
  </Box>
158
+ <Text color="gray" dimColor>
159
+ {rule}
160
+ </Text>
156
161
  </Box>
157
162
  );
158
163
  }
@@ -196,7 +201,7 @@ export function ShortcutBar({
196
201
  return (
197
202
  <Box gap={3} marginTop={0}>
198
203
  <Text color="gray" dimColor>
199
- enter send · ^v paste · ^c exit
204
+ enter send · ctrl+enter newline · ctrl+del del word · ^f force · ^c exit
200
205
  </Text>
201
206
  {forceApprove ? (
202
207
  <Text color={RED}>⚡⚡ force-all</Text>
@@ -244,7 +249,7 @@ export function CloningView({
244
249
  <History committed={committed} />
245
250
  <Box gap={1} marginTop={1}>
246
251
  <Text color={ACCENT}>
247
- <Spinner />
252
+ <Spinner type="line" />
248
253
  </Text>
249
254
  <Text color="gray">cloning </Text>
250
255
  <Text color={ACCENT}>{stage.repoUrl}</Text>
@@ -4,11 +4,11 @@ import Spinner from "ink-spinner";
4
4
  import { useState } from "react";
5
5
  import path from "path";
6
6
  import os from "os";
7
- import TextInput from "ink-text-input";
7
+ import { TextArea } from "./TextArea";
8
8
  import { ACCENT } from "../../colors";
9
9
  import { ProviderPicker } from "../provider/ProviderPicker";
10
10
  import { startCloneRepo } from "../../utils/repo";
11
- import { useThinkingPhrase } from "../../utils/thinking";
11
+ import { useThinkingPhrase, useThinkingTip, useThinkingTimer } from "../../utils/thinking";
12
12
  import { walkDir, applyPatches, toCloneUrl } from "../../utils/chat";
13
13
  import { appendMemory } from "../../utils/memory";
14
14
  import { getChatNameSuggestions, saveChat } from "../../utils/chatHistory";
@@ -136,7 +136,7 @@ function ForceAllWarning({
136
136
  esc
137
137
  </Text>
138
138
  <Text color="gray"> to cancel: </Text>
139
- <TextInput
139
+ <TextArea
140
140
  value={input}
141
141
  onChange={setInput}
142
142
  onSubmit={(v) => onConfirm(v.trim().toLowerCase() === "yes")}
@@ -147,9 +147,18 @@ function ForceAllWarning({
147
147
  );
148
148
  }
149
149
 
150
- export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
151
- const chat = useChat(repoPath);
152
- const thinkingPhrase = useThinkingPhrase(chat.stage.type === "thinking");
150
+ export const ChatRunner = ({
151
+ repoPath,
152
+ autoForce = false,
153
+ }: {
154
+ repoPath: string;
155
+ autoForce?: boolean;
156
+ }) => {
157
+ const chat = useChat(repoPath, autoForce);
158
+ const isThinking = chat.stage.type === "thinking";
159
+ const thinkingPhrase = useThinkingPhrase(isThinking);
160
+ const thinkingTip = useThinkingTip(isThinking);
161
+ const thinkingTimer = useThinkingTimer(isThinking);
153
162
 
154
163
  const handleStageKey = (input: string, key: any) => {
155
164
  const { stage } = chat;
@@ -321,10 +330,14 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
321
330
  if (msg?.type === "plan") {
322
331
  chat.applyPatchesAndContinue(msg.patches);
323
332
  const applied: Message = { ...msg, applied: true };
324
- chat.setAllMessages((prev) =>
325
- prev.map((m, i) => (i === chat.pendingMsgIndex ? applied : m)),
333
+ const updatedAll = chat.allMessages.map((m, i) =>
334
+ i === chat.pendingMsgIndex ? applied : m,
326
335
  );
336
+ chat.setAllMessages(updatedAll);
327
337
  chat.setCommitted((prev) => [...prev, applied]);
338
+ chat.setPendingMsgIndex(null);
339
+ chat.continueAfterChanges(updatedAll, msg.content || "code changes");
340
+ return;
328
341
  }
329
342
  }
330
343
  chat.setPendingMsgIndex(null);
@@ -352,6 +365,26 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
352
365
  }
353
366
  };
354
367
 
368
+ useInput(
369
+ (input, key) => {
370
+ if (!(key.ctrl && input === "f")) return;
371
+ if (chat.forceApprove) {
372
+ chat.setForceApprove(false);
373
+ chat.setAutoApprove(false);
374
+ const msg: Message = {
375
+ role: "assistant",
376
+ content: "Force-all mode OFF — tools will ask for permission again.",
377
+ type: "text",
378
+ };
379
+ chat.setCommitted((prev) => [...prev, msg]);
380
+ chat.setAllMessages((prev: Message[]) => [...prev, msg]);
381
+ } else {
382
+ chat.setShowForceWarning(true);
383
+ }
384
+ },
385
+ { isActive: chat.stage.type === "idle" },
386
+ );
387
+
355
388
  const chatInput = useChatInput(
356
389
  chat.stage,
357
390
  chat.showTimeline,
@@ -408,7 +441,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
408
441
  <Box gap={1} marginTop={1}>
409
442
  <Text color={ACCENT}>*</Text>
410
443
  <Text color={ACCENT}>
411
- <Spinner />
444
+ <Spinner type="arc" />
412
445
  </Text>
413
446
  <Text color="gray" dimColor>
414
447
  indexing codebase…
@@ -476,12 +509,19 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
476
509
  )}
477
510
 
478
511
  {!chat.showForceWarning && stage.type === "thinking" && (
479
- <Box gap={1}>
480
- <Text color={ACCENT}>●</Text>
481
- <TypewriterText text={thinkingPhrase} />
482
- <Text color="gray" dimColor>
483
- · esc cancel
484
- </Text>
512
+ <Box flexDirection="column">
513
+ <Box gap={1}>
514
+ <Text color={ACCENT}>●</Text>
515
+ <TypewriterText text={thinkingPhrase} />
516
+ <Text color="gray" dimColor>
517
+ {thinkingTimer ? `· ${thinkingTimer} ` : ""}· esc cancel
518
+ </Text>
519
+ </Box>
520
+ <Box marginLeft={2}>
521
+ <Text color="gray" dimColor>
522
+ tip: {thinkingTip}
523
+ </Text>
524
+ </Box>
485
525
  </Box>
486
526
  )}
487
527
 
@@ -0,0 +1,177 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Text, useInput } from "ink";
3
+ import chalk from "chalk";
4
+
5
+ function isWordChar(ch: string): boolean {
6
+ return /[\w]/.test(ch);
7
+ }
8
+
9
+ function wordBoundaryLeft(text: string, pos: number): number {
10
+ if (pos <= 0) return 0;
11
+ let i = pos - 1;
12
+
13
+ while (i > 0 && !isWordChar(text[i]!)) i--;
14
+
15
+ while (i > 0 && isWordChar(text[i - 1]!)) i--;
16
+ return i;
17
+ }
18
+
19
+ function wordBoundaryRight(text: string, pos: number): number {
20
+ const len = text.length;
21
+ if (pos >= len) return len;
22
+ let i = pos;
23
+
24
+ while (i < len && isWordChar(text[i]!)) i++;
25
+
26
+ while (i < len && !isWordChar(text[i]!)) i++;
27
+ return i;
28
+ }
29
+
30
+ export interface TextAreaProps {
31
+ value: string;
32
+ onChange: (value: string) => void;
33
+ onSubmit: (value: string) => void;
34
+ focus?: boolean;
35
+ placeholder?: string;
36
+ }
37
+
38
+ export function TextArea({
39
+ value,
40
+ onChange,
41
+ onSubmit,
42
+ focus = true,
43
+ placeholder = "",
44
+ }: TextAreaProps) {
45
+ const [cursor, setCursor] = useState(value.length);
46
+ const [prevValue, setPrevValue] = useState(value);
47
+
48
+ if (value !== prevValue) {
49
+ setPrevValue(value);
50
+ const lenDiff = Math.abs(value.length - prevValue.length);
51
+ if (cursor > value.length || lenDiff > 1) {
52
+ setCursor(value.length);
53
+ }
54
+ }
55
+
56
+ useInput(
57
+ (input, key) => {
58
+ if (key.upArrow || key.downArrow) return;
59
+ if (key.tab || (key.shift && key.tab)) return;
60
+ if (key.ctrl && input === "c") return;
61
+
62
+ const isShiftEnter =
63
+ (key.return && key.shift) ||
64
+ input === "\x1b[27;2;13~" ||
65
+ input === "\x1b[13;2u";
66
+
67
+ if (key.return && !key.meta && !key.shift && !isShiftEnter) {
68
+ onSubmit(value);
69
+ return;
70
+ }
71
+
72
+ if (isShiftEnter) {
73
+ const next = value.slice(0, cursor) + "\n" + value.slice(cursor);
74
+ onChange(next);
75
+ setCursor((c) => c + 1);
76
+ return;
77
+ }
78
+
79
+ if (key.leftArrow && key.ctrl) {
80
+ setCursor(wordBoundaryLeft(value, cursor));
81
+ return;
82
+ }
83
+
84
+ if (key.rightArrow && key.ctrl) {
85
+ setCursor(wordBoundaryRight(value, cursor));
86
+ return;
87
+ }
88
+
89
+ if (key.leftArrow) {
90
+ setCursor((c) => Math.max(0, c - 1));
91
+ return;
92
+ }
93
+
94
+ if (key.rightArrow) {
95
+ setCursor((c) => Math.min(value.length, c + 1));
96
+ return;
97
+ }
98
+
99
+ if (key.ctrl && input === "a") {
100
+ const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
101
+ setCursor(lineStart);
102
+ return;
103
+ }
104
+
105
+ if (key.ctrl && input === "e") {
106
+ const lineEnd = value.indexOf("\n", cursor);
107
+ setCursor(lineEnd === -1 ? value.length : lineEnd);
108
+ return;
109
+ }
110
+
111
+ if (key.ctrl && input === "u") {
112
+ const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
113
+ onChange(value.slice(0, lineStart) + value.slice(cursor));
114
+ setCursor(lineStart);
115
+ return;
116
+ }
117
+
118
+ if (key.ctrl && input === "k") {
119
+ const lineEnd = value.indexOf("\n", cursor);
120
+ onChange(
121
+ value.slice(0, cursor) + (lineEnd === -1 ? "" : value.slice(lineEnd)),
122
+ );
123
+ return;
124
+ }
125
+
126
+ if (key.ctrl && input === "f") return;
127
+
128
+ if ((key.ctrl && key.delete) || input === "\x1b[3;5~") {
129
+ const to = wordBoundaryLeft(value, cursor);
130
+ onChange(value.slice(0, to) + value.slice(cursor));
131
+ setCursor(to);
132
+ return;
133
+ }
134
+
135
+ if (key.backspace || key.delete) {
136
+ if (cursor > 0) {
137
+ onChange(value.slice(0, cursor - 1) + value.slice(cursor));
138
+ setCursor((c) => c - 1);
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (key.escape) return;
144
+
145
+ if (input) {
146
+ const next = value.slice(0, cursor) + input + value.slice(cursor);
147
+ onChange(next);
148
+ setCursor((c) => c + input.length);
149
+ }
150
+ },
151
+ { isActive: focus },
152
+ );
153
+
154
+ if (value.length === 0 && placeholder) {
155
+ return (
156
+ <Text>
157
+ {chalk.inverse(placeholder[0] ?? " ")}
158
+ {placeholder.length > 1 ? chalk.gray(placeholder.slice(1)) : ""}
159
+ </Text>
160
+ );
161
+ }
162
+
163
+ let rendered = "";
164
+ for (let i = 0; i < value.length; i++) {
165
+ const ch = value[i]!;
166
+ if (i === cursor) {
167
+ rendered += ch === "\n" ? chalk.inverse(" ") + "\n" : chalk.inverse(ch);
168
+ } else {
169
+ rendered += ch;
170
+ }
171
+ }
172
+ if (cursor === value.length) {
173
+ rendered += chalk.inverse(" ");
174
+ }
175
+
176
+ return <Text>{rendered}</Text>;
177
+ }