@ridit/lens 0.2.4 → 0.2.6

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.
Files changed (41) hide show
  1. package/LENS.md +32 -68
  2. package/README.md +91 -0
  3. package/addons/README.md +3 -0
  4. package/addons/run-tests.js +127 -0
  5. package/dist/index.mjs +226459 -2638
  6. package/package.json +13 -4
  7. package/src/colors.ts +5 -0
  8. package/src/commands/commit.tsx +686 -0
  9. package/src/commands/provider.tsx +36 -22
  10. package/src/components/__tests__/Header.test.tsx +9 -0
  11. package/src/components/chat/ChatMessage.tsx +6 -6
  12. package/src/components/chat/ChatOverlays.tsx +20 -10
  13. package/src/components/chat/ChatRunner.tsx +197 -31
  14. package/src/components/provider/ApiKeyStep.tsx +77 -121
  15. package/src/components/provider/ModelStep.tsx +35 -20
  16. package/src/components/{repo → provider}/ProviderPicker.tsx +1 -1
  17. package/src/components/provider/ProviderTypeStep.tsx +12 -5
  18. package/src/components/provider/RemoveProviderStep.tsx +7 -8
  19. package/src/components/repo/RepoAnalysis.tsx +1 -1
  20. package/src/components/task/TaskRunner.tsx +1 -1
  21. package/src/components/timeline/CommitDetail.tsx +2 -4
  22. package/src/components/timeline/CommitList.tsx +2 -14
  23. package/src/components/timeline/TimelineChat.tsx +1 -2
  24. package/src/components/timeline/TimelineRunner.tsx +506 -423
  25. package/src/index.tsx +38 -0
  26. package/src/prompts/fewshot.ts +144 -47
  27. package/src/prompts/system.ts +25 -21
  28. package/src/tools/chart.ts +210 -0
  29. package/src/tools/convert-image.ts +312 -0
  30. package/src/tools/files.ts +1 -9
  31. package/src/tools/git.ts +577 -0
  32. package/src/tools/index.ts +17 -13
  33. package/src/tools/pdf.ts +136 -78
  34. package/src/tools/view-image.ts +335 -0
  35. package/src/tools/web.ts +0 -4
  36. package/src/utils/addons/loadAddons.ts +6 -3
  37. package/src/utils/chat.ts +38 -23
  38. package/src/utils/thinking.tsx +275 -162
  39. package/src/utils/tools/builtins.ts +39 -32
  40. package/src/utils/tools/registry.ts +0 -14
  41. package/tsconfig.json +2 -2
@@ -1,13 +1,15 @@
1
- import { Box, Text, useInput, type Key } from "ink";
1
+ import { Box, Text, useInput } from "ink";
2
+ import TextInput from "ink-text-input";
2
3
  import { useState } from "react";
3
4
  import { execSync } from "child_process";
4
5
  import type { ProviderType } from "../../types/config";
6
+ import { ACCENT, TEXT } from "../../colors";
5
7
 
6
8
  const LABELS: Record<ProviderType, string> = {
7
9
  anthropic: "Anthropic API key",
8
10
  gemini: "Gemini API key",
9
11
  openai: "OpenAI API key",
10
- ollama: "Ollama base URL (default: http://localhost:11434)",
12
+ ollama: "Ollama base URL",
11
13
  custom: "API key",
12
14
  };
13
15
 
@@ -38,184 +40,139 @@ function readClipboard(): string | null {
38
40
  type CustomResult = { apiKey: string; baseUrl?: string };
39
41
  type Field = "apiKey" | "baseUrl";
40
42
 
41
- const useFieldInput = (initial: string, onPasteError: (v: boolean) => void) => {
42
- const [value, setValue] = useState(initial);
43
-
44
- const handle = (input: string, key: Key) => {
45
- if (key.backspace || key.delete) {
46
- setValue((v) => v.slice(0, -1));
47
- onPasteError(false);
48
- return;
49
- }
50
- if (key.ctrl && input === "v") {
51
- const clip = readClipboard();
52
- if (clip) {
53
- setValue((v) => v + clip);
54
- onPasteError(false);
55
- } else onPasteError(true);
56
- return;
57
- }
58
- if (key.ctrl && input === "a") {
59
- setValue("");
60
- return;
61
- }
62
- if (!key.ctrl && !key.meta && input) {
63
- setValue((v) => v + input);
64
- onPasteError(false);
65
- }
66
- };
67
-
68
- return { value, setValue, handle };
69
- };
70
-
71
43
  const SimpleInput = ({
72
44
  providerType,
73
45
  onSubmit,
74
- onSkip,
46
+ onBack,
75
47
  }: {
76
48
  providerType: Exclude<ProviderType, "custom">;
77
49
  onSubmit: (value: string) => void;
78
- onSkip?: () => void;
50
+ onBack?: () => void;
79
51
  }) => {
80
- const [pasteError, setPasteError] = useState(false);
81
52
  const isPassword = providerType !== "ollama";
82
- const { value, handle } = useFieldInput(
53
+ const [value, setValue] = useState(
83
54
  providerType === "ollama" ? "http://localhost:11434" : "",
84
- setPasteError,
85
55
  );
86
56
 
87
57
  useInput((input, key) => {
88
- if (key.return) {
89
- if (value.trim()) onSubmit(value.trim());
58
+ if (key.escape) {
59
+ onBack?.();
90
60
  return;
91
61
  }
92
- if (key.escape && onSkip) {
93
- onSkip();
94
- return;
62
+ if (key.ctrl && input === "v") {
63
+ const clip = readClipboard();
64
+ if (clip) setValue((v) => v + clip);
95
65
  }
96
- handle(input, key);
97
66
  });
98
67
 
99
- const display = isPassword ? "•".repeat(value.length) : value;
100
-
101
68
  return (
102
69
  <Box flexDirection="column" gap={1}>
103
- <Text bold color="cyan">
104
- {LABELS[providerType]}
105
- </Text>
70
+ <Text color={TEXT}>{LABELS[providerType]}</Text>
106
71
  <Box borderStyle="round" borderColor="gray" paddingX={1}>
107
- <Text>{display || " "}</Text>
72
+ <TextInput
73
+ value={value}
74
+ onChange={setValue}
75
+ onSubmit={(v) => {
76
+ if (v.trim()) onSubmit(v.trim());
77
+ }}
78
+ mask={isPassword ? "*" : undefined}
79
+ placeholder={
80
+ providerType === "ollama" ? "http://localhost:11434" : ""
81
+ }
82
+ />
108
83
  </Box>
109
- {pasteError ? (
110
- <Text color="red">⚠ Could not read clipboard</Text>
111
- ) : (
112
- <Text color="gray">
113
- enter to confirm · ctrl+v to paste · ctrl+a to clear
114
- {onSkip ? " · esc to skip" : ""}
115
- </Text>
116
- )}
84
+ <Text color="gray">
85
+ enter to confirm · ctrl+v to paste{onBack ? " · esc back" : ""}
86
+ </Text>
117
87
  </Box>
118
88
  );
119
89
  };
120
90
 
121
91
  const CustomInput = ({
122
92
  onSubmit,
123
- onSkip,
93
+ onBack,
124
94
  }: {
125
95
  onSubmit: (result: CustomResult) => void;
126
- onSkip?: () => void;
96
+ onBack?: () => void;
127
97
  }) => {
128
98
  const [activeField, setActiveField] = useState<Field>("apiKey");
129
- const [pasteError, setPasteError] = useState(false);
130
-
131
- const apiKeyField = useFieldInput("", setPasteError);
132
- const baseUrlField = useFieldInput("", setPasteError);
133
-
134
- const active = activeField === "apiKey" ? apiKeyField : baseUrlField;
99
+ const [apiKey, setApiKey] = useState("");
100
+ const [baseUrl, setBaseUrl] = useState("");
135
101
 
136
102
  useInput((input, key) => {
137
- if (key.escape && onSkip) {
138
- onSkip();
103
+ if (key.escape) {
104
+ onBack?.();
139
105
  return;
140
106
  }
141
-
142
107
  if (key.tab) {
143
108
  setActiveField((f) => (f === "apiKey" ? "baseUrl" : "apiKey"));
144
- setPasteError(false);
145
109
  return;
146
110
  }
147
-
148
- if (key.return) {
149
- if (activeField === "apiKey" && apiKeyField.value.trim()) {
150
- setActiveField("baseUrl");
151
- return;
152
- }
153
- if (activeField === "baseUrl" && apiKeyField.value.trim()) {
154
- onSubmit({
155
- apiKey: apiKeyField.value.trim(),
156
- baseUrl: baseUrlField.value.trim() || undefined,
157
- });
158
- return;
111
+ if (key.ctrl && input === "v") {
112
+ const clip = readClipboard();
113
+ if (clip) {
114
+ if (activeField === "apiKey") setApiKey((v) => v + clip);
115
+ else setBaseUrl((v) => v + clip);
159
116
  }
160
117
  }
161
-
162
- active.handle(input, key);
163
118
  });
164
119
 
165
- const fields: {
166
- id: Field;
167
- label: string;
168
- password: boolean;
169
- placeholder: string;
170
- }[] = [
171
- { id: "apiKey", label: "API key", password: true, placeholder: "sk-..." },
120
+ const fields: { id: Field; label: string; placeholder: string }[] = [
121
+ { id: "apiKey", label: "API key", placeholder: "sk-..." },
172
122
  {
173
123
  id: "baseUrl",
174
- label: "Base URL",
175
- password: false,
124
+ label: "Base URL (optional)",
176
125
  placeholder: "https://api.example.com/v1",
177
126
  },
178
127
  ];
179
128
 
180
129
  return (
181
130
  <Box flexDirection="column" gap={1}>
182
- <Text bold color="cyan">
183
- Custom provider
184
- </Text>
185
-
186
- {fields.map(({ id, label, password, placeholder }) => {
131
+ {fields.map(({ id, label, placeholder }) => {
187
132
  const isActive = activeField === id;
188
- const val = id === "apiKey" ? apiKeyField.value : baseUrlField.value;
189
- const display = password ? "•".repeat(val.length) : val;
190
-
133
+ const val = id === "apiKey" ? apiKey : baseUrl;
191
134
  return (
192
135
  <Box key={id} flexDirection="column" gap={0}>
193
- <Text color={isActive ? "cyan" : "gray"}>
136
+ <Text color={isActive ? ACCENT : "gray"}>
194
137
  {isActive ? "›" : " "} {label}
195
- {id === "baseUrl" ? " (optional)" : ""}
196
138
  </Text>
197
139
  <Box
198
140
  borderStyle="round"
199
- borderColor={isActive ? "cyan" : "gray"}
141
+ borderColor={isActive ? ACCENT : "gray"}
200
142
  paddingX={1}
201
143
  >
202
- <Text color={val ? "white" : "gray"}>
203
- {display || placeholder}
204
- </Text>
144
+ {isActive ? (
145
+ <TextInput
146
+ value={val}
147
+ onChange={id === "apiKey" ? setApiKey : setBaseUrl}
148
+ onSubmit={() => {
149
+ if (id === "apiKey" && apiKey.trim()) {
150
+ setActiveField("baseUrl");
151
+ } else if (id === "baseUrl" && apiKey.trim()) {
152
+ onSubmit({
153
+ apiKey: apiKey.trim(),
154
+ baseUrl: baseUrl.trim() || undefined,
155
+ });
156
+ }
157
+ }}
158
+ mask={id === "apiKey" ? "*" : undefined}
159
+ placeholder={placeholder}
160
+ />
161
+ ) : (
162
+ <Text color={val ? "white" : "gray"}>
163
+ {id === "apiKey" && val
164
+ ? "*".repeat(val.length)
165
+ : val || placeholder}
166
+ </Text>
167
+ )}
205
168
  </Box>
206
169
  </Box>
207
170
  );
208
171
  })}
209
-
210
- {pasteError ? (
211
- <Text color="red">⚠ Could not read clipboard</Text>
212
- ) : (
213
- <Text color="gray">
214
- enter to next field · tab to switch · ctrl+v to paste · ctrl+a to
215
- clear
216
- {onSkip ? " · esc to skip" : ""}
217
- </Text>
218
- )}
172
+ <Text color="gray">
173
+ enter to next · tab to switch · ctrl+v to paste
174
+ {onBack ? " · esc back" : ""}
175
+ </Text>
219
176
  </Box>
220
177
  );
221
178
  };
@@ -223,21 +180,20 @@ const CustomInput = ({
223
180
  export const ApiKeyStep = ({
224
181
  providerType,
225
182
  onSubmit,
226
- onSkip,
183
+ onBack,
227
184
  }: {
228
185
  providerType: ProviderType;
229
186
  onSubmit: (value: string | CustomResult) => void;
230
- onSkip?: () => void;
187
+ onBack?: () => void;
231
188
  }) => {
232
189
  if (providerType === "custom") {
233
- return <CustomInput onSubmit={onSubmit} onSkip={onSkip} />;
190
+ return <CustomInput onSubmit={onSubmit} onBack={onBack} />;
234
191
  }
235
-
236
192
  return (
237
193
  <SimpleInput
238
194
  providerType={providerType}
239
195
  onSubmit={onSubmit}
240
- onSkip={onSkip}
196
+ onBack={onBack}
241
197
  />
242
198
  );
243
199
  };
@@ -1,34 +1,35 @@
1
1
  import { Box, Text, useInput } from "ink";
2
+ import TextInput from "ink-text-input";
2
3
  import figures from "figures";
3
4
  import { useState } from "react";
4
5
  import { DEFAULT_MODELS } from "../../utils/config";
5
6
  import type { ProviderType } from "../../types/config";
7
+ import { TEXT } from "../../colors";
6
8
 
7
9
  export const ModelStep = ({
8
10
  providerType,
9
11
  onSelect,
12
+ onBack,
10
13
  }: {
11
14
  providerType: ProviderType;
12
15
  onSelect: (model: string) => void;
16
+ onBack?: () => void;
13
17
  }) => {
14
18
  const models = DEFAULT_MODELS[providerType] ?? [];
15
19
  const [index, setIndex] = useState(0);
16
20
  const [custom, setCustom] = useState("");
17
21
  const [typing, setTyping] = useState(models.length === 0);
18
22
 
19
- useInput((input, key) => {
20
- if (typing) {
21
- if (key.return && custom.trim()) {
22
- onSelect(custom.trim());
23
+ useInput((_, key) => {
24
+ if (key.escape) {
25
+ if (typing && models.length > 0) {
26
+ setTyping(false);
23
27
  return;
24
28
  }
25
- if (key.backspace || key.delete) {
26
- setCustom((v) => v.slice(0, -1));
27
- return;
28
- }
29
- if (!key.ctrl && !key.meta && input) setCustom((v) => v + input);
29
+ onBack?.();
30
30
  return;
31
31
  }
32
+ if (typing) return;
32
33
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
33
34
  if (key.downArrow) setIndex((i) => Math.min(models.length, i + 1));
34
35
  if (key.return) {
@@ -55,19 +56,33 @@ export const ModelStep = ({
55
56
  );
56
57
  })}
57
58
  <Box marginLeft={1}>
58
- <Text color={index === models.length && !typing ? "cyan" : "gray"}>
59
- {index === models.length && !typing ? figures.arrowRight : " "}
60
- {" "}
61
- {typing ? (
62
- <Text>
63
- Custom: <Text color="white">{custom || " "}</Text>
59
+ {typing ? (
60
+ <Box gap={1}>
61
+ <Text color={TEXT}>
62
+ {figures.arrowRight}
63
+ {" "}Custom:{" "}
64
64
  </Text>
65
- ) : (
66
- "Enter custom model name"
67
- )}
68
- </Text>
65
+ <TextInput
66
+ value={custom}
67
+ onChange={setCustom}
68
+ onSubmit={(v) => {
69
+ if (v.trim()) onSelect(v.trim());
70
+ }}
71
+ placeholder="enter model name"
72
+ />
73
+ </Box>
74
+ ) : (
75
+ <Text color={index === models.length ? "cyan" : "gray"}>
76
+ {index === models.length ? figures.arrowRight : " "}
77
+ {" "}Enter custom model name
78
+ </Text>
79
+ )}
69
80
  </Box>
70
- <Text color="gray">↑↓ navigate · enter to select</Text>
81
+ <Text color="gray">
82
+ {typing
83
+ ? "enter to confirm · esc back"
84
+ : `↑↓ navigate · enter to select${onBack ? " · esc back" : ""}`}
85
+ </Text>
71
86
  </Box>
72
87
  );
73
88
  };
@@ -34,7 +34,7 @@ export const ProviderPicker = ({
34
34
  <Box marginTop={1}>
35
35
  <Text color="red">
36
36
  {figures.cross} No providers configured. Run{" "}
37
- <Text color="cyan">lens init</Text> first.
37
+ <Text color="cyan">lens provider</Text> first.
38
38
  </Text>
39
39
  </Box>
40
40
  );
@@ -2,6 +2,7 @@ import { Box, Text, useInput } from "ink";
2
2
  import figures from "figures";
3
3
  import { useState } from "react";
4
4
  import type { ProviderType } from "../../types/config";
5
+ import { ACCENT, TEXT } from "../../colors";
5
6
 
6
7
  const OPTIONS: { type: ProviderType; label: string; description: string }[] = [
7
8
  { type: "anthropic", label: "Anthropic", description: "Claude models" },
@@ -16,12 +17,18 @@ const OPTIONS: { type: ProviderType; label: string; description: string }[] = [
16
17
 
17
18
  export const ProviderTypeStep = ({
18
19
  onSelect,
20
+ onBack,
19
21
  }: {
20
22
  onSelect: (type: ProviderType) => void;
23
+ onBack?: () => void;
21
24
  }) => {
22
25
  const [index, setIndex] = useState(0);
23
26
 
24
27
  useInput((_, key) => {
28
+ if (key.escape) {
29
+ onBack?.();
30
+ return;
31
+ }
25
32
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
26
33
  if (key.downArrow) setIndex((i) => Math.min(OPTIONS.length - 1, i + 1));
27
34
  if (key.return) onSelect(OPTIONS[index]!.type);
@@ -29,14 +36,12 @@ export const ProviderTypeStep = ({
29
36
 
30
37
  return (
31
38
  <Box flexDirection="column" gap={1}>
32
- <Text bold color="cyan">
33
- Select a provider
34
- </Text>
39
+ <Text color={TEXT}>Select a provider</Text>
35
40
  {OPTIONS.map((opt, i) => {
36
41
  const selected = i === index;
37
42
  return (
38
43
  <Box key={opt.type} marginLeft={1}>
39
- <Text color={selected ? "cyan" : "white"}>
44
+ <Text color={selected ? ACCENT : "white"}>
40
45
  {selected ? figures.arrowRight : " "}
41
46
  {" "}
42
47
  <Text bold={selected}>{opt.label}</Text>
@@ -48,7 +53,9 @@ export const ProviderTypeStep = ({
48
53
  </Box>
49
54
  );
50
55
  })}
51
- <Text color="gray">↑↓ navigate · enter to select</Text>
56
+ <Text color="gray">
57
+ ↑↓ navigate · enter to select{onBack ? " · esc back" : ""}
58
+ </Text>
52
59
  </Box>
53
60
  );
54
61
  };
@@ -3,6 +3,7 @@ import figures from "figures";
3
3
  import { useState } from "react";
4
4
  import { loadConfig, saveConfig } from "../../utils/config";
5
5
  import type { Provider } from "../../types/config";
6
+ import { RED, TEXT } from "../../colors";
6
7
 
7
8
  export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
8
9
  const config = loadConfig();
@@ -38,7 +39,7 @@ export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
38
39
  if (providers.length === 0) {
39
40
  return (
40
41
  <Box marginTop={1}>
41
- <Text color="gray">{figures.info} No providers configured.</Text>
42
+ <Text color="gray">No providers configured.</Text>
42
43
  </Box>
43
44
  );
44
45
  }
@@ -48,8 +49,8 @@ export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
48
49
  if (confirming && selected) {
49
50
  return (
50
51
  <Box flexDirection="column" gap={1} marginTop={1}>
51
- <Text color="red">
52
- {figures.warning} Remove <Text bold>{selected.name}</Text>? (y/n)
52
+ <Text color={RED}>
53
+ {figures.warning} Remove <Text>{selected.name}</Text>? (y/n)
53
54
  </Text>
54
55
  </Box>
55
56
  );
@@ -57,17 +58,15 @@ export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
57
58
 
58
59
  return (
59
60
  <Box flexDirection="column" gap={1} marginTop={1}>
60
- <Text bold color="cyan">
61
- Remove a provider
62
- </Text>
61
+ <Text color={TEXT}>Remove a provider</Text>
63
62
  {providers.map((p, i) => {
64
63
  const isSelected = i === index;
65
64
  return (
66
65
  <Box key={p.id} marginLeft={1}>
67
- <Text color={isSelected ? "red" : "white"}>
66
+ <Text color={isSelected ? RED : "white"}>
68
67
  {isSelected ? figures.arrowRight : " "}
69
68
  {" "}
70
- <Text bold={isSelected}>{p.name}</Text>
69
+ <Text>{p.name}</Text>
71
70
  <Text color="gray">
72
71
  {" "}
73
72
  {p.type} · {p.model}
@@ -7,7 +7,7 @@ import { writeFileSync } from "fs";
7
7
  import path from "path";
8
8
  import { ACCENT } from "../../colors";
9
9
  import { requestFileList, analyzeRepo } from "../../utils/ai";
10
- import { ProviderPicker } from "./ProviderPicker";
10
+ import { ProviderPicker } from "../provider/ProviderPicker";
11
11
  import { PreviewRunner } from "./PreviewRunner";
12
12
  import { IssueFixer } from "./IssueFixer";
13
13
  import { writeLensFile } from "../../utils/lensfile";
@@ -8,7 +8,7 @@ import path from "path";
8
8
  import { ACCENT } from "../../colors";
9
9
  import { callModelRaw } from "../../utils/ai";
10
10
  import { DiffViewer, buildDiffs } from "../repo/DiffViewer";
11
- import { ProviderPicker } from "../repo/ProviderPicker";
11
+ import { ProviderPicker } from "../provider/ProviderPicker";
12
12
  import type { DiffLine, FilePatch } from "../repo/DiffViewer";
13
13
  import type { Provider } from "../../types/config";
14
14
  import type { ImportantFile } from "../../types/repo";
@@ -1,8 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import type { Commit, DiffFile } from "../../utils/git";
4
-
5
- const ACCENT = "#FF8C00";
4
+ import { ACCENT } from "../../colors";
6
5
 
7
6
  type Props = {
8
7
  commit: Commit | null;
@@ -73,7 +72,6 @@ export function CommitDetail({
73
72
 
74
73
  const divider = "─".repeat(Math.max(0, width - 2));
75
74
 
76
- // Build all diff lines for scrolling
77
75
  const allDiffLines: Array<{
78
76
  type: string;
79
77
  content: string;
@@ -177,7 +175,7 @@ export function CommitDetail({
177
175
  {/* stats bar */}
178
176
  <Box paddingX={1} marginTop={1} gap={3}>
179
177
  <Text color="green">+{commit.insertions} insertions</Text>
180
- <Text color="red">-{commit.deletions} deletions</Text>
178
+ <Text color="red">-{commit.deletions}</Text>
181
179
  <Text color="gray" dimColor>
182
180
  {commit.filesChanged} file{commit.filesChanged !== 1 ? "s" : ""}{" "}
183
181
  changed
@@ -1,8 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import type { Commit } from "../../utils/git";
4
-
5
- const ACCENT = "#FF8C00";
4
+ import { ACCENT } from "../../colors";
6
5
 
7
6
  type Props = {
8
7
  commits: Commit[];
@@ -29,7 +28,6 @@ function formatRefs(refs: string): string {
29
28
  }
30
29
 
31
30
  function shortDate(dateStr: string): string {
32
- // "2026-03-12 14:22:01 +0530" → "Mar 12"
33
31
  try {
34
32
  const d = new Date(dateStr);
35
33
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
@@ -82,8 +80,7 @@ export function CommitList({
82
80
  const refs = formatRefs(commit.refs);
83
81
  const date = shortDate(commit.date);
84
82
 
85
- // truncate message to fit width
86
- const prefixLen = 14; // symbol + hash + date
83
+ const prefixLen = 14;
87
84
  const maxMsg = Math.max(10, width - prefixLen - 3);
88
85
  const msg =
89
86
  commit.message.length > maxMsg
@@ -92,22 +89,18 @@ export function CommitList({
92
89
 
93
90
  return (
94
91
  <Box key={commit.hash} paddingX={1} flexDirection="column">
95
- {/* graph line above (not first) */}
96
92
  {i > 0 && (
97
93
  <Text color="gray" dimColor>
98
94
  {"│"}
99
95
  </Text>
100
96
  )}
101
97
  <Box gap={1}>
102
- {/* selection indicator */}
103
98
  <Text color={isSelected ? ACCENT : "gray"}>
104
99
  {isSelected ? "▶" : " "}
105
100
  </Text>
106
101
 
107
- {/* graph node */}
108
102
  <Text color={isSelected ? ACCENT : color}>{symbol}</Text>
109
103
 
110
- {/* short hash */}
111
104
  <Text
112
105
  color={isSelected ? "white" : "gray"}
113
106
  dimColor={!isSelected}
@@ -115,12 +108,10 @@ export function CommitList({
115
108
  {commit.shortHash}
116
109
  </Text>
117
110
 
118
- {/* date */}
119
111
  <Text color="cyan" dimColor={!isSelected}>
120
112
  {date}
121
113
  </Text>
122
114
 
123
- {/* message */}
124
115
  <Text
125
116
  color={isSelected ? "white" : "gray"}
126
117
  bold={isSelected}
@@ -130,14 +121,12 @@ export function CommitList({
130
121
  </Text>
131
122
  </Box>
132
123
 
133
- {/* refs on selected */}
134
124
  {isSelected && refs && (
135
125
  <Box paddingLeft={4}>
136
126
  <Text color="yellow">{refs}</Text>
137
127
  </Box>
138
128
  )}
139
129
 
140
- {/* stat summary on selected */}
141
130
  {isSelected && (
142
131
  <Box paddingLeft={4} gap={2}>
143
132
  <Text color="gray" dimColor>
@@ -159,7 +148,6 @@ export function CommitList({
159
148
  );
160
149
  })}
161
150
 
162
- {/* scroll hint */}
163
151
  <Box paddingX={1} marginTop={1}>
164
152
  <Text color="gray" dimColor>
165
153
  {scrollOffset > 0 ? "↑ more above" : ""}
@@ -5,8 +5,7 @@ import type { Commit } from "../../utils/git";
5
5
  import { summarizeTimeline } from "../../utils/git";
6
6
  import type { Provider } from "../../types/config";
7
7
  import { callChat } from "../../utils/chat";
8
-
9
- const ACCENT = "#FF8C00";
8
+ import { ACCENT } from "../../colors";
10
9
 
11
10
  type TLMessage = { role: "user" | "assistant"; content: string; type: "text" };
12
11