@ridit/lens 0.3.7 → 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.7",
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,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useState, useEffect } from "react";
2
2
  import { Box, Static, Text, useStdout } from "ink";
3
3
  import Spinner from "ink-spinner";
4
4
  import { TextArea } from "./TextArea";
@@ -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";
@@ -135,12 +128,23 @@ export function InputBox({
135
128
  inputKey?: number;
136
129
  }) {
137
130
  const { stdout } = useStdout();
138
- const cols = stdout?.columns ?? 80;
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
+
139
141
  const rule = "─".repeat(Math.max(1, cols));
140
142
 
141
143
  return (
142
144
  <Box flexDirection="column" marginTop={1}>
143
- <Text color="gray" dimColor>{rule}</Text>
145
+ <Text color="gray" dimColor>
146
+ {rule}
147
+ </Text>
144
148
  <Box gap={1}>
145
149
  <Text color={ACCENT}>{">"}</Text>
146
150
  <TextArea
@@ -151,7 +155,9 @@ export function InputBox({
151
155
  placeholder="ask anything..."
152
156
  />
153
157
  </Box>
154
- <Text color="gray" dimColor>{rule}</Text>
158
+ <Text color="gray" dimColor>
159
+ {rule}
160
+ </Text>
155
161
  </Box>
156
162
  );
157
163
  }
@@ -195,7 +201,7 @@ export function ShortcutBar({
195
201
  return (
196
202
  <Box gap={3} marginTop={0}>
197
203
  <Text color="gray" dimColor>
198
- enter send · alt+enter newline · ^w del word · ^c exit
204
+ enter send · ctrl+enter newline · ctrl+del del word · ^f force · ^c exit
199
205
  </Text>
200
206
  {forceApprove ? (
201
207
  <Text color={RED}>⚡⚡ force-all</Text>
@@ -243,7 +249,7 @@ export function CloningView({
243
249
  <History committed={committed} />
244
250
  <Box gap={1} marginTop={1}>
245
251
  <Text color={ACCENT}>
246
- <Spinner />
252
+ <Spinner type="line" />
247
253
  </Text>
248
254
  <Text color="gray">cloning </Text>
249
255
  <Text color={ACCENT}>{stage.repoUrl}</Text>
@@ -8,7 +8,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";
@@ -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
 
@@ -59,12 +59,17 @@ export function TextArea({
59
59
  if (key.tab || (key.shift && key.tab)) return;
60
60
  if (key.ctrl && input === "c") return;
61
61
 
62
- if (key.return && !key.meta) {
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) {
63
68
  onSubmit(value);
64
69
  return;
65
70
  }
66
71
 
67
- if ((key.return && key.meta) || (key.ctrl && input === "j")) {
72
+ if (isShiftEnter) {
68
73
  const next = value.slice(0, cursor) + "\n" + value.slice(cursor);
69
74
  onChange(next);
70
75
  setCursor((c) => c + 1);
@@ -118,19 +123,15 @@ export function TextArea({
118
123
  return;
119
124
  }
120
125
 
121
- if (key.ctrl && input === "w") {
126
+ if (key.ctrl && input === "f") return;
127
+
128
+ if ((key.ctrl && key.delete) || input === "\x1b[3;5~") {
122
129
  const to = wordBoundaryLeft(value, cursor);
123
130
  onChange(value.slice(0, to) + value.slice(cursor));
124
131
  setCursor(to);
125
132
  return;
126
133
  }
127
134
 
128
- if (key.ctrl && key.delete) {
129
- const to = wordBoundaryRight(value, cursor);
130
- onChange(value.slice(0, cursor) + value.slice(to));
131
- return;
132
- }
133
-
134
135
  if (key.backspace || key.delete) {
135
136
  if (cursor > 0) {
136
137
  onChange(value.slice(0, cursor - 1) + value.slice(cursor));