@ridit/lens 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ridit/lens",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Know Your Codebase.",
5
5
  "author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
6
6
  "license": "MIT",
@@ -98,6 +98,20 @@ function MessageBody({ content }: { content: string }) {
98
98
  );
99
99
  }
100
100
 
101
+ function summarizeToolContent(toolName: string, content: string): string {
102
+ // For write-file, extract just the path
103
+ if (toolName === "write-file" || toolName === "read-file") {
104
+ const pathMatch = content.match(/"path"\s*:\s*"([^"]+)"/);
105
+ if (pathMatch) return pathMatch[1]!;
106
+ }
107
+ // For changes blocks, just say what changed
108
+ if (content.includes('"summary"')) {
109
+ const summaryMatch = content.match(/"summary"\s*:\s*"([^"]+)"/);
110
+ if (summaryMatch) return summaryMatch[1]!;
111
+ }
112
+ return content.length > 120 ? content.slice(0, 120) + "…" : content;
113
+ }
114
+
101
115
  export function StaticMessage({ msg }: { msg: Message }) {
102
116
  if (msg.role === "user") {
103
117
  return (
@@ -130,7 +144,7 @@ export function StaticMessage({ msg }: { msg: Message }) {
130
144
  ? msg.content
131
145
  : msg.toolName === "search"
132
146
  ? `"${msg.content}"`
133
- : msg.content;
147
+ : summarizeToolContent(msg.toolName, msg.content);
134
148
 
135
149
  return (
136
150
  <Box flexDirection="column" marginBottom={1}>
@@ -100,10 +100,12 @@ export function InputBox({
100
100
  value,
101
101
  onChange,
102
102
  onSubmit,
103
+ inputKey,
103
104
  }: {
104
105
  value: string;
105
106
  onChange: (v: string) => void;
106
107
  onSubmit: (v: string) => void;
108
+ inputKey?: number;
107
109
  }) {
108
110
  return (
109
111
  <Box
@@ -117,7 +119,12 @@ export function InputBox({
117
119
  >
118
120
  <Box gap={1}>
119
121
  <Text color={ACCENT}>{">"}</Text>
120
- <TextInput value={value} onChange={onChange} onSubmit={onSubmit} />
122
+ <TextInput
123
+ key={inputKey}
124
+ value={value}
125
+ onChange={onChange}
126
+ onSubmit={onSubmit}
127
+ />
121
128
  </Box>
122
129
  </Box>
123
130
  );
@@ -32,6 +32,13 @@ import {
32
32
  callChat,
33
33
  searchWeb,
34
34
  } from "../../utils/chat";
35
+ import {
36
+ saveChat,
37
+ loadChat,
38
+ listChats,
39
+ deleteChat,
40
+ getChatNameSuggestions,
41
+ } from "../../utils/chatHistory";
35
42
  import { StaticMessage } from "./ChatMessage";
36
43
  import {
37
44
  PermissionPrompt,
@@ -50,30 +57,60 @@ import { TimelineRunner } from "../timeline/TimelineRunner";
50
57
  import type { Provider } from "../../types/config";
51
58
  import type { Message, ChatStage } from "../../types/chat";
52
59
  import {
53
- appendHistory,
54
- buildHistorySummary,
55
- clearRepoHistory,
56
- } from "../../utils/history";
60
+ appendMemory,
61
+ buildMemorySummary,
62
+ clearRepoMemory,
63
+ addMemory,
64
+ deleteMemory,
65
+ listMemories,
66
+ } from "../../utils/memory";
57
67
  import { readLensFile } from "../../utils/lensfile";
58
68
  import { ReviewCommand } from "../../commands/review";
59
69
 
60
70
  const COMMANDS = [
61
71
  { cmd: "/timeline", desc: "browse commit history" },
62
72
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
63
- { cmd: "/review", desc: "review current codebsae" },
73
+ { cmd: "/review", desc: "review current codebase" },
64
74
  { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
75
+ { cmd: "/chat", desc: "chat history commands" },
76
+ { cmd: "/chat list", desc: "list saved chats for this repo" },
77
+ { cmd: "/chat load", desc: "load a saved chat by name" },
78
+ { cmd: "/chat rename", desc: "rename the current chat" },
79
+ { cmd: "/chat delete", desc: "delete a saved chat by name" },
80
+ { cmd: "/memory", desc: "memory commands" },
81
+ { cmd: "/memory list", desc: "list all memories for this repo" },
82
+ { cmd: "/memory add", desc: "add a memory" },
83
+ { cmd: "/memory delete", desc: "delete a memory by id" },
84
+ { cmd: "/memory clear", desc: "clear all memories for this repo" },
65
85
  ];
66
86
 
67
87
  function CommandPalette({
68
88
  query,
69
89
  onSelect,
90
+ recentChats,
70
91
  }: {
71
92
  query: string;
72
93
  onSelect: (cmd: string) => void;
94
+ recentChats: string[];
73
95
  }) {
74
96
  const q = query.toLowerCase();
97
+
98
+ // If typing "/chat load <something>", stay visible and filter chats
99
+ const isChatLoad = q.startsWith("/chat load") || q.startsWith("/chat delete");
100
+ const chatFilter = isChatLoad
101
+ ? q.startsWith("/chat load")
102
+ ? q.slice("/chat load".length).trim()
103
+ : q.slice("/chat delete".length).trim()
104
+ : "";
105
+ const filteredChats = chatFilter
106
+ ? recentChats.filter((n) => n.toLowerCase().includes(chatFilter))
107
+ : recentChats;
108
+
75
109
  const matches = COMMANDS.filter((c) => c.cmd.startsWith(q));
76
- if (!matches.length) return null;
110
+
111
+ // Keep palette open if we're in /chat load mode even after space
112
+ if (!matches.length && !isChatLoad) return null;
113
+ if (!matches.length && isChatLoad && filteredChats.length === 0) return null;
77
114
 
78
115
  return (
79
116
  <Box flexDirection="column" marginBottom={1} marginLeft={2}>
@@ -90,6 +127,19 @@ function CommandPalette({
90
127
  </Box>
91
128
  );
92
129
  })}
130
+ {isChatLoad && filteredChats.length > 0 && (
131
+ <Box flexDirection="column" marginTop={matches.length ? 1 : 0}>
132
+ <Text color="gray" dimColor>
133
+ {chatFilter ? `matching "${chatFilter}":` : "recent chats:"}
134
+ </Text>
135
+ {filteredChats.map((name, i) => (
136
+ <Box key={i} gap={1} marginLeft={2}>
137
+ <Text color={ACCENT}>·</Text>
138
+ <Text color="white">{name}</Text>
139
+ </Box>
140
+ ))}
141
+ </Box>
142
+ )}
93
143
  </Box>
94
144
  );
95
145
  }
@@ -106,19 +156,37 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
106
156
  const [showTimeline, setShowTimeline] = useState(false);
107
157
  const [showReview, setShowReview] = useState(false);
108
158
  const [autoApprove, setAutoApprove] = useState(false);
159
+ const [chatName, setChatName] = useState<string | null>(null);
160
+ const chatNameRef = useRef<string | null>(null);
161
+ const [recentChats, setRecentChats] = useState<string[]>([]);
162
+ const inputHistoryRef = useRef<string[]>([]);
163
+ const historyIndexRef = useRef<number>(-1);
164
+ const [inputKey, setInputKey] = useState(0);
165
+
166
+ const updateChatName = (name: string) => {
167
+ chatNameRef.current = name;
168
+ setChatName(name);
169
+ };
109
170
 
110
- // Abort controller for the currently in-flight API call.
111
- // Pressing ESC while thinking aborts the request and drops the response.
112
171
  const abortControllerRef = useRef<AbortController | null>(null);
113
-
114
- // Cache of tool results within a single conversation turn to prevent
115
- // the model from re-calling tools it already ran with the same args
116
172
  const toolResultCache = useRef<Map<string, string>>(new Map());
117
-
118
173
  const inputBuffer = useRef("");
119
174
  const flushTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
120
175
  const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
121
176
 
177
+ // Load recent chats on mount
178
+ React.useEffect(() => {
179
+ const chats = listChats(repoPath);
180
+ setRecentChats(chats.slice(0, 10).map((c) => c.name));
181
+ }, [repoPath]);
182
+
183
+ // Auto-save whenever messages change
184
+ React.useEffect(() => {
185
+ if (chatNameRef.current && allMessages.length > 1) {
186
+ saveChat(chatNameRef.current, repoPath, allMessages);
187
+ }
188
+ }, [allMessages]);
189
+
122
190
  const flushBuffer = () => {
123
191
  const buf = inputBuffer.current;
124
192
  if (!buf) return;
@@ -135,7 +203,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
135
203
  };
136
204
 
137
205
  const handleError = (currentAll: Message[]) => (err: unknown) => {
138
- // Silently drop aborted requests — user pressed ESC intentionally
139
206
  if (err instanceof Error && err.name === "AbortError") {
140
207
  setStage({ type: "idle" });
141
208
  return;
@@ -155,13 +222,33 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
155
222
  currentAll: Message[],
156
223
  signal: AbortSignal,
157
224
  ) => {
158
- // If ESC was pressed before we got here, silently drop the response
159
225
  if (signal.aborted) {
160
226
  setStage({ type: "idle" });
161
227
  return;
162
228
  }
163
229
 
164
- const parsed = parseResponse(raw);
230
+ // Handle inline memory operations the model may emit
231
+ const memAddMatches = [
232
+ ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
233
+ ];
234
+ const memDelMatches = [
235
+ ...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g),
236
+ ];
237
+ for (const match of memAddMatches) {
238
+ const content = match[1]!.trim();
239
+ if (content) addMemory(content, repoPath);
240
+ }
241
+ for (const match of memDelMatches) {
242
+ const id = match[1]!.trim();
243
+ if (id) deleteMemory(id, repoPath);
244
+ }
245
+ // Strip memory tags from raw before parsing
246
+ const cleanRaw = raw
247
+ .replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
248
+ .replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
249
+ .trim();
250
+
251
+ const parsed = parseResponse(cleanRaw);
165
252
 
166
253
  if (parsed.kind === "changes") {
167
254
  if (parsed.patches.length === 0) {
@@ -252,7 +339,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
252
339
  setCommitted((prev) => [...prev, preambleMsg]);
253
340
  }
254
341
 
255
- // Safe tools that can be auto-approved (no side effects)
256
342
  const isSafeTool =
257
343
  parsed.kind === "read-file" ||
258
344
  parsed.kind === "read-folder" ||
@@ -334,7 +420,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
334
420
  "write-file": "file-written",
335
421
  search: "url-fetched",
336
422
  } as const;
337
- appendHistory({
423
+ appendMemory({
338
424
  kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
339
425
  detail:
340
426
  parsed.kind === "shell"
@@ -418,7 +504,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
418
504
  setAllMessages(withTool);
419
505
  setCommitted((prev) => [...prev, toolMsg]);
420
506
 
421
- // Create a fresh abort controller for the follow-up call
422
507
  const nextAbort = new AbortController();
423
508
  abortControllerRef.current = nextAbort;
424
509
 
@@ -514,7 +599,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
514
599
  }
515
600
 
516
601
  if (text.trim().toLowerCase() === "/clear history") {
517
- clearRepoHistory(repoPath);
602
+ clearRepoMemory(repoPath);
518
603
  const clearedMsg: Message = {
519
604
  role: "assistant",
520
605
  content: "History cleared for this repo.",
@@ -525,13 +610,258 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
525
610
  return;
526
611
  }
527
612
 
613
+ // bare /chat — show usage
614
+ if (text.trim().toLowerCase() === "/chat") {
615
+ const msg: Message = {
616
+ role: "assistant",
617
+ content:
618
+ "Chat commands: `/chat list` · `/chat load <n>` · `/chat rename <n>` · `/chat delete <n>`",
619
+ type: "text",
620
+ };
621
+ setCommitted((prev) => [...prev, msg]);
622
+ setAllMessages((prev) => [...prev, msg]);
623
+ return;
624
+ }
625
+
626
+ // /chat rename <newname>
627
+ if (text.trim().toLowerCase().startsWith("/chat rename")) {
628
+ const parts = text.trim().split(/\s+/);
629
+ const newName = parts.slice(2).join("-");
630
+ if (!newName) {
631
+ const msg: Message = {
632
+ role: "assistant",
633
+ content: "Usage: `/chat rename <new-name>`",
634
+ type: "text",
635
+ };
636
+ setCommitted((prev) => [...prev, msg]);
637
+ setAllMessages((prev) => [...prev, msg]);
638
+ return;
639
+ }
640
+ const oldName = chatNameRef.current;
641
+ if (oldName) deleteChat(oldName);
642
+ updateChatName(newName);
643
+ saveChat(newName, repoPath, allMessages);
644
+ setRecentChats((prev) =>
645
+ [newName, ...prev.filter((n) => n !== newName && n !== oldName)].slice(
646
+ 0,
647
+ 10,
648
+ ),
649
+ );
650
+ const msg: Message = {
651
+ role: "assistant",
652
+ content: `Chat renamed to **${newName}**.`,
653
+ type: "text",
654
+ };
655
+ setCommitted((prev) => [...prev, msg]);
656
+ setAllMessages((prev) => [...prev, msg]);
657
+ return;
658
+ }
659
+
660
+ // /chat delete <name>
661
+ if (text.trim().toLowerCase().startsWith("/chat delete")) {
662
+ const parts = text.trim().split(/\s+/);
663
+ const name = parts.slice(2).join("-");
664
+ if (!name) {
665
+ const msg: Message = {
666
+ role: "assistant",
667
+ content: "Usage: `/chat delete <name>`",
668
+ type: "text",
669
+ };
670
+ setCommitted((prev) => [...prev, msg]);
671
+ setAllMessages((prev) => [...prev, msg]);
672
+ return;
673
+ }
674
+ const deleted = deleteChat(name);
675
+ if (!deleted) {
676
+ const msg: Message = {
677
+ role: "assistant",
678
+ content: `Chat **${name}** not found.`,
679
+ type: "text",
680
+ };
681
+ setCommitted((prev) => [...prev, msg]);
682
+ setAllMessages((prev) => [...prev, msg]);
683
+ return;
684
+ }
685
+ // If deleting the current chat, clear the name so it gets re-named on next message
686
+ if (chatNameRef.current === name) {
687
+ chatNameRef.current = null;
688
+ setChatName(null);
689
+ }
690
+ setRecentChats((prev) => prev.filter((n) => n !== name));
691
+ const msg: Message = {
692
+ role: "assistant",
693
+ content: `Chat **${name}** deleted.`,
694
+ type: "text",
695
+ };
696
+ setCommitted((prev) => [...prev, msg]);
697
+ setAllMessages((prev) => [...prev, msg]);
698
+ return;
699
+ }
700
+
701
+ // /chat list
702
+ if (text.trim().toLowerCase() === "/chat list") {
703
+ const chats = listChats(repoPath);
704
+ const content =
705
+ chats.length === 0
706
+ ? "No saved chats for this repo yet."
707
+ : `Saved chats:\n\n${chats
708
+ .map(
709
+ (c) =>
710
+ `- **${c.name}** · ${c.userMessageCount} messages · ${new Date(c.savedAt).toLocaleString()}`,
711
+ )
712
+ .join("\n")}`;
713
+ const msg: Message = { role: "assistant", content, type: "text" };
714
+ setCommitted((prev) => [...prev, msg]);
715
+ setAllMessages((prev) => [...prev, msg]);
716
+ return;
717
+ }
718
+
719
+ // /chat load <n>
720
+ if (text.trim().toLowerCase().startsWith("/chat load")) {
721
+ const parts = text.trim().split(/\s+/);
722
+ const name = parts.slice(2).join("-");
723
+ if (!name) {
724
+ const chats = listChats(repoPath);
725
+ const content =
726
+ chats.length === 0
727
+ ? "No saved chats found."
728
+ : `Specify a chat name. Recent chats:\n\n${chats
729
+ .slice(0, 10)
730
+ .map((c) => `- **${c.name}**`)
731
+ .join("\n")}`;
732
+ const msg: Message = { role: "assistant", content, type: "text" };
733
+ setCommitted((prev) => [...prev, msg]);
734
+ setAllMessages((prev) => [...prev, msg]);
735
+ return;
736
+ }
737
+ const saved = loadChat(name);
738
+ if (!saved) {
739
+ const msg: Message = {
740
+ role: "assistant",
741
+ content: `Chat **${name}** not found. Use \`/chat list\` to see saved chats.`,
742
+ type: "text",
743
+ };
744
+ setCommitted((prev) => [...prev, msg]);
745
+ setAllMessages((prev) => [...prev, msg]);
746
+ return;
747
+ }
748
+ updateChatName(name);
749
+ setAllMessages(saved.messages);
750
+ setCommitted(saved.messages);
751
+ const notice: Message = {
752
+ role: "assistant",
753
+ content: `Loaded chat **${name}** · ${saved.userMessageCount} messages · saved ${new Date(saved.savedAt).toLocaleString()}`,
754
+ type: "text",
755
+ };
756
+ setCommitted((prev) => [...prev, notice]);
757
+ setAllMessages((prev) => [...prev, notice]);
758
+ return;
759
+ }
760
+
761
+ // /memory list
762
+ if (
763
+ text.trim().toLowerCase() === "/memory list" ||
764
+ text.trim().toLowerCase() === "/memory"
765
+ ) {
766
+ const mems = listMemories(repoPath);
767
+ const content =
768
+ mems.length === 0
769
+ ? "No memories stored for this repo yet."
770
+ : `Memories for this repo:\n\n${mems.map((m) => `- [${m.id}] ${m.content}`).join("\n")}`;
771
+ const msg: Message = { role: "assistant", content, type: "text" };
772
+ setCommitted((prev) => [...prev, msg]);
773
+ setAllMessages((prev) => [...prev, msg]);
774
+ return;
775
+ }
776
+
777
+ // /memory add <content>
778
+ if (text.trim().toLowerCase().startsWith("/memory add")) {
779
+ const content = text.trim().slice("/memory add".length).trim();
780
+ if (!content) {
781
+ const msg: Message = {
782
+ role: "assistant",
783
+ content: "Usage: `/memory add <content>`",
784
+ type: "text",
785
+ };
786
+ setCommitted((prev) => [...prev, msg]);
787
+ setAllMessages((prev) => [...prev, msg]);
788
+ return;
789
+ }
790
+ const mem = addMemory(content, repoPath);
791
+ const msg: Message = {
792
+ role: "assistant",
793
+ content: `Memory saved **[${mem.id}]**: ${mem.content}`,
794
+ type: "text",
795
+ };
796
+ setCommitted((prev) => [...prev, msg]);
797
+ setAllMessages((prev) => [...prev, msg]);
798
+ return;
799
+ }
800
+
801
+ // /memory delete <id>
802
+ if (text.trim().toLowerCase().startsWith("/memory delete")) {
803
+ const id = text.trim().split(/\s+/)[2];
804
+ if (!id) {
805
+ const msg: Message = {
806
+ role: "assistant",
807
+ content: "Usage: `/memory delete <id>`",
808
+ type: "text",
809
+ };
810
+ setCommitted((prev) => [...prev, msg]);
811
+ setAllMessages((prev) => [...prev, msg]);
812
+ return;
813
+ }
814
+ const deleted = deleteMemory(id, repoPath);
815
+ const msg: Message = {
816
+ role: "assistant",
817
+ content: deleted
818
+ ? `Memory **[${id}]** deleted.`
819
+ : `Memory **[${id}]** not found.`,
820
+ type: "text",
821
+ };
822
+ setCommitted((prev) => [...prev, msg]);
823
+ setAllMessages((prev) => [...prev, msg]);
824
+ return;
825
+ }
826
+
827
+ // /memory clear
828
+ if (text.trim().toLowerCase() === "/memory clear") {
829
+ clearRepoMemory(repoPath);
830
+ const msg: Message = {
831
+ role: "assistant",
832
+ content: "All memories cleared for this repo.",
833
+ type: "text",
834
+ };
835
+ setCommitted((prev) => [...prev, msg]);
836
+ setAllMessages((prev) => [...prev, msg]);
837
+ return;
838
+ }
839
+
528
840
  const userMsg: Message = { role: "user", content: text, type: "text" };
529
841
  const nextAll = [...allMessages, userMsg];
530
842
  setCommitted((prev) => [...prev, userMsg]);
531
843
  setAllMessages(nextAll);
532
844
  toolResultCache.current.clear();
533
845
 
534
- // Create a fresh abort controller for this request
846
+ // Track input history for up/down navigation
847
+ inputHistoryRef.current = [
848
+ text,
849
+ ...inputHistoryRef.current.filter((m) => m !== text),
850
+ ].slice(0, 50);
851
+ historyIndexRef.current = -1;
852
+
853
+ // Auto-name chat on first user message
854
+ if (!chatName) {
855
+ const name =
856
+ getChatNameSuggestions(nextAll)[0] ??
857
+ `chat-${new Date().toISOString().slice(0, 10)}`;
858
+ updateChatName(name);
859
+ setRecentChats((prev) =>
860
+ [name, ...prev.filter((n) => n !== name)].slice(0, 10),
861
+ );
862
+ saveChat(name, repoPath, nextAll);
863
+ }
864
+
535
865
  const abort = new AbortController();
536
866
  abortControllerRef.current = abort;
537
867
 
@@ -544,7 +874,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
544
874
  useInput((input, key) => {
545
875
  if (showTimeline) return;
546
876
 
547
- // ESC while thinking → abort the in-flight request and go idle
548
877
  if (stage.type === "thinking" && key.escape) {
549
878
  abortControllerRef.current?.abort();
550
879
  abortControllerRef.current = null;
@@ -558,6 +887,26 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
558
887
  return;
559
888
  }
560
889
 
890
+ if (key.upArrow && inputHistoryRef.current.length > 0) {
891
+ const next = Math.min(
892
+ historyIndexRef.current + 1,
893
+ inputHistoryRef.current.length - 1,
894
+ );
895
+ historyIndexRef.current = next;
896
+ setInputValue(inputHistoryRef.current[next]!);
897
+ setInputKey((k) => k + 1);
898
+ return;
899
+ }
900
+
901
+ if (key.downArrow) {
902
+ const next = historyIndexRef.current - 1;
903
+ historyIndexRef.current = next;
904
+ const val = next < 0 ? "" : inputHistoryRef.current[next]!;
905
+ setInputValue(val);
906
+ setInputKey((k) => k + 1);
907
+ return;
908
+ }
909
+
561
910
  if (key.tab && inputValue.startsWith("/")) {
562
911
  const q = inputValue.toLowerCase();
563
912
  const match = COMMANDS.find((c) => c.cmd.startsWith(q));
@@ -582,7 +931,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
582
931
  ?.replace(/\.git$/, "") ?? "repo";
583
932
  const destPath = path.join(os.tmpdir(), repoName);
584
933
  const fileCount = walkDir(destPath).length;
585
- appendHistory({
934
+ appendMemory({
586
935
  kind: "url-fetched",
587
936
  detail: repoUrl,
588
937
  summary: `Cloned ${repoName} — ${fileCount} files`,
@@ -663,13 +1012,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
663
1012
  if (key.return || key.escape) {
664
1013
  if (stage.type === "clone-done") {
665
1014
  const repoName = stage.repoUrl.split("/").pop() ?? "repo";
666
-
667
1015
  const summaryMsg: Message = {
668
1016
  role: "assistant",
669
1017
  type: "text",
670
1018
  content: `Cloned **${repoName}** (${stage.fileCount} files) to \`${stage.destPath}\`.\n\nAsk me anything about it — I can read files, explain how it works, or suggest improvements.`,
671
1019
  };
672
-
673
1020
  const contextMsg: Message = {
674
1021
  role: "assistant",
675
1022
  type: "tool",
@@ -720,7 +1067,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
720
1067
  const msg = allMessages[pendingMsgIndex];
721
1068
  if (msg?.type === "plan") {
722
1069
  setCommitted((prev) => [...prev, { ...msg, applied: false }]);
723
- appendHistory({
1070
+ appendMemory({
724
1071
  kind: "code-skipped",
725
1072
  detail: msg.patches
726
1073
  .map((p: { path: string }) => p.path)
@@ -737,7 +1084,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
737
1084
  if (key.return || input === "a" || input === "A") {
738
1085
  try {
739
1086
  applyPatches(repoPath, stage.patches);
740
- appendHistory({
1087
+ appendMemory({
741
1088
  kind: "code-applied",
742
1089
  detail: stage.patches.map((p) => p.path).join(", "),
743
1090
  summary: `Applied changes to ${stage.patches.length} file(s)`,
@@ -788,7 +1135,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
788
1135
  .catch(() => walkDir(repoPath))
789
1136
  .then((fileTree) => {
790
1137
  const importantFiles = readImportantFiles(repoPath, fileTree);
791
- const historySummary = buildHistorySummary(repoPath);
1138
+ const historySummary = buildMemorySummary(repoPath);
792
1139
  const lensFile = readLensFile(repoPath);
793
1140
  const lensContext = lensFile
794
1141
  ? `
@@ -892,18 +1239,21 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
892
1239
  {inputValue.startsWith("/") && (
893
1240
  <CommandPalette
894
1241
  query={inputValue}
895
- onSelect={(cmd) => {
896
- setInputValue(cmd);
897
- }}
1242
+ onSelect={(cmd) => setInputValue(cmd)}
1243
+ recentChats={recentChats}
898
1244
  />
899
1245
  )}
900
1246
  <InputBox
901
1247
  value={inputValue}
902
- onChange={setInputValue}
1248
+ onChange={(v) => {
1249
+ historyIndexRef.current = -1;
1250
+ setInputValue(v);
1251
+ }}
903
1252
  onSubmit={(val) => {
904
1253
  if (val.trim()) sendMessage(val.trim());
905
1254
  setInputValue("");
906
1255
  }}
1256
+ inputKey={inputKey}
907
1257
  />
908
1258
  <ShortcutBar autoApprove={autoApprove} />
909
1259
  </Box>