interference-agent 0.1.0 → 0.2.2

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,19 +1,25 @@
1
1
  export interface DiffLine {
2
2
  type: "same" | "add" | "remove";
3
3
  text: string;
4
+ oldNo?: number; // numero riga nel file vecchio (1-based) — same/remove
5
+ newNo?: number; // numero riga nel file nuovo (1-based) — same/add
4
6
  }
5
7
 
6
8
  export function computeDiff(oldLines: string[], newLines: string[]): DiffLine[] {
7
9
  const result: DiffLine[] = [];
8
10
  let oi = 0;
9
11
  let ni = 0;
12
+ // Numeri di riga 1-based: oi/ni sono indici 0-based → +1 al push.
13
+ const same = (t: string) => result.push({ type: "same", text: t, oldNo: oi + 1, newNo: ni + 1 });
14
+ const rem = (t: string) => result.push({ type: "remove", text: t, oldNo: oi + 1 });
15
+ const add = (t: string) => result.push({ type: "add", text: t, newNo: ni + 1 });
10
16
 
11
17
  while (oi < oldLines.length && ni < newLines.length) {
12
18
  const old = oldLines[oi]!;
13
19
  const nw = newLines[ni]!;
14
20
 
15
21
  if (old === nw) {
16
- result.push({ type: "same", text: old });
22
+ same(old);
17
23
  oi++;
18
24
  ni++;
19
25
  continue;
@@ -21,32 +27,32 @@ export function computeDiff(oldLines: string[], newLines: string[]): DiffLine[]
21
27
 
22
28
  const oldEnd = findEnd(oldLines, newLines, oi, ni);
23
29
  if (oldEnd.oi === oi) {
24
- result.push({ type: "add", text: nw });
30
+ add(nw);
25
31
  ni++;
26
32
  continue;
27
33
  }
28
34
  if (oldEnd.ni === ni) {
29
- result.push({ type: "remove", text: old });
35
+ rem(old);
30
36
  oi++;
31
37
  continue;
32
38
  }
33
39
 
34
40
  while (oi < oldEnd.oi) {
35
- result.push({ type: "remove", text: oldLines[oi]! });
41
+ rem(oldLines[oi]!);
36
42
  oi++;
37
43
  }
38
44
  while (ni < oldEnd.ni) {
39
- result.push({ type: "add", text: newLines[ni]! });
45
+ add(newLines[ni]!);
40
46
  ni++;
41
47
  }
42
48
  }
43
49
 
44
50
  while (oi < oldLines.length) {
45
- result.push({ type: "remove", text: oldLines[oi]! });
51
+ rem(oldLines[oi]!);
46
52
  oi++;
47
53
  }
48
54
  while (ni < newLines.length) {
49
- result.push({ type: "add", text: newLines[ni]! });
55
+ add(newLines[ni]!);
50
56
  ni++;
51
57
  }
52
58
 
@@ -71,17 +77,11 @@ function findEnd(
71
77
 
72
78
  export function formatDiff(diff: DiffLine[]): string {
73
79
  const lines: string[] = [];
80
+ const no = (n?: number) => String(n ?? "").padStart(4, " ");
74
81
  for (const d of diff.slice(0, 80)) {
75
- switch (d.type) {
76
- case "add":
77
- lines.push(`+ ${d.text}`);
78
- break;
79
- case "remove":
80
- lines.push(`- ${d.text}`);
81
- break;
82
- default:
83
- lines.push(` ${d.text}`);
84
- }
82
+ const num = d.type === "add" ? no(d.newNo) : no(d.oldNo);
83
+ const mark = d.type === "add" ? "+" : d.type === "remove" ? "-" : " ";
84
+ lines.push(`${num} ${mark} ${d.text}`);
85
85
  }
86
86
  if (diff.length > 80) lines.push(`… and ${diff.length - 80} more lines`);
87
87
  return lines.join("\n");
@@ -1,5 +1,6 @@
1
1
  import { Box, Text } from "ink";
2
2
  import type { ReactNode } from "react";
3
+ import { tokenizeLine, normalizeLang } from "./syntax.ts";
3
4
 
4
5
  // Rendering markdown minimale per il terminale (no dipendenze):
5
6
  // - fenced code ``` → blocco dim
@@ -32,14 +33,35 @@ export function MarkdownText({ content }: { content: string }) {
32
33
  const lines = content.split("\n");
33
34
  const blocks: ReactNode[] = [];
34
35
  let inFence = false;
36
+ let lang = "";
37
+ let fenceLines = 0;
35
38
 
36
39
  lines.forEach((line, i) => {
37
- if (line.trimStart().startsWith("```")) {
38
- inFence = !inFence;
40
+ const t = line.trimStart();
41
+ if (t.startsWith("```")) {
42
+ if (!inFence) {
43
+ inFence = true;
44
+ lang = normalizeLang(t.slice(3));
45
+ fenceLines = 0;
46
+ } else {
47
+ inFence = false;
48
+ lang = "";
49
+ }
39
50
  return; // nascondi i marker di fence
40
51
  }
41
52
  if (inFence) {
42
- blocks.push(<Text key={i} dimColor> {line}</Text>);
53
+ if (++fenceLines > 200) return; // cap di sicurezza
54
+ const toks = tokenizeLine(line, lang);
55
+ blocks.push(
56
+ <Text key={i}>
57
+ {" "}
58
+ {toks.map((tk, j) => (
59
+ <Text key={j} color={tk.color} dimColor={tk.dim}>
60
+ {tk.text}
61
+ </Text>
62
+ ))}
63
+ </Text>,
64
+ );
43
65
  return;
44
66
  }
45
67
  const heading = /^(#{1,6})\s+(.*)$/.exec(line);
@@ -1,6 +1,7 @@
1
1
  import { useState, type FC } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { currentModel, setModel, setProvider, PROVIDERS, type ProviderId } from "../config.ts";
4
+ import { SelectRow } from "./SelectRow.tsx";
4
5
 
5
6
  export const ModelPicker: FC<{ onCancel: () => void }> = ({ onCancel }) => {
6
7
  const current = currentModel();
@@ -30,13 +31,13 @@ export const ModelPicker: FC<{ onCancel: () => void }> = ({ onCancel }) => {
30
31
  <Box flexDirection="column" borderStyle="round" borderColor="blue" padding={1}>
31
32
  <Box marginBottom={1}><Text bold>Select model</Text></Box>
32
33
  {flat.map((m, i) => (
33
- <Box key={m.id}>
34
- <Text color={i === idx ? "cyan" : undefined} bold={i === idx}>
35
- {i === idx ? "▸ " : " "}{m.label}
36
- </Text>
37
- <Text dimColor> · {m.provider}</Text>
38
- {m.id === current && <Text color="cyan"> (active)</Text>}
39
- </Box>
34
+ <SelectRow
35
+ key={m.id}
36
+ label={m.label}
37
+ meta={`· ${m.provider}`}
38
+ selected={i === idx}
39
+ current={m.id === current}
40
+ />
40
41
  ))}
41
42
  <Box marginTop={1}><Text dimColor>↑↓ j/k navigate · Enter select · Esc cancel</Text></Box>
42
43
  </Box>
package/src/tui/Panel.tsx CHANGED
@@ -1,35 +1,39 @@
1
1
  import { Box, Text, useStdout } from "ink";
2
- import { PANEL, padRight, panelWidth } from "./theme.ts";
2
+ import { BG_PANEL, BG_ELEMENT, padRight, panelWidth } from "./theme.ts";
3
3
 
4
4
  // Pannello con sfondo REALE: ogni riga è un <Text backgroundColor> riempito di
5
5
  // spazi fino alla larghezza, così il colore si vede (il bg dei <Box> in Ink non
6
6
  // riempie il padding). Barra laterale opzionale come primo carattere della riga.
7
+ // level: "element" (#1e1e1e, messaggi/selezione) | "panel" (#141414, blocchi/dialog).
7
8
  export function Panel({
8
9
  content,
9
10
  bar,
10
11
  barColor,
11
12
  bold,
13
+ level = "element",
12
14
  }: {
13
15
  content: string;
14
16
  bar?: string;
15
17
  barColor?: string;
16
18
  bold?: boolean;
19
+ level?: "panel" | "element";
17
20
  }) {
18
21
  const { stdout } = useStdout();
19
22
  const w = panelWidth(stdout?.columns);
23
+ const bg = level === "panel" ? BG_PANEL : BG_ELEMENT;
20
24
  const prefix = bar ? `${bar} ` : "";
21
25
  const lines = content.split("\n");
22
26
 
23
27
  return (
24
28
  <Box flexDirection="column" marginBottom={1}>
25
29
  {lines.map((ln, i) => (
26
- <Text key={i} backgroundColor={PANEL} color="white" bold={bold}>
30
+ <Text key={i} backgroundColor={bg} color="white" bold={bold}>
27
31
  {bar && i === 0 ? (
28
- <Text backgroundColor={PANEL} color={barColor} bold>
32
+ <Text backgroundColor={bg} color={barColor} bold>
29
33
  {prefix}
30
34
  </Text>
31
35
  ) : (
32
- <Text backgroundColor={PANEL}>{bar ? " " : ""}</Text>
36
+ <Text backgroundColor={bg}>{bar ? " " : ""}</Text>
33
37
  )}
34
38
  {padRight(ln, w - prefix.length)}
35
39
  </Text>
@@ -0,0 +1,40 @@
1
+ import { Text, useStdout } from "ink";
2
+ import { BG_ELEMENT, padRight, panelWidth } from "./theme.ts";
3
+
4
+ // Riga di selezione stile opencode (it. 24): selezione = sfondo a riga piena,
5
+ // `●` per il valore corrente (vs ` `). Niente pointer `▸`.
6
+ export function SelectRow({
7
+ label,
8
+ meta,
9
+ selected,
10
+ current,
11
+ color = "white",
12
+ }: {
13
+ label: string;
14
+ meta?: string;
15
+ selected: boolean;
16
+ current?: boolean;
17
+ color?: string;
18
+ }) {
19
+ const { stdout } = useStdout();
20
+ const w = Math.min(panelWidth(stdout?.columns), 72);
21
+ const marker = current ? "● " : " ";
22
+
23
+ if (selected) {
24
+ const text = `${marker}${label}${meta ? " " + meta : ""}`;
25
+ return (
26
+ <Text backgroundColor={BG_ELEMENT} color="white" bold>
27
+ {padRight(text, w)}
28
+ </Text>
29
+ );
30
+ }
31
+ return (
32
+ <Text>
33
+ <Text color={current ? color : undefined} bold={current}>
34
+ {marker}
35
+ {label}
36
+ </Text>
37
+ {meta ? <Text dimColor>{" " + meta}</Text> : null}
38
+ </Text>
39
+ );
40
+ }
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useCallback, type FC } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { listSessions, loadSession, type SessionMeta } from "../session/store.ts";
4
+ import { SelectRow } from "./SelectRow.tsx";
4
5
 
5
6
  interface Props {
6
7
  onSelect: (sessionId: string) => void;
@@ -56,13 +57,12 @@ export const SessionList: FC<Props> = ({ onSelect, onCancel }) => {
56
57
  <Text bold>Sessions ({sessions.length})</Text>
57
58
  </Box>
58
59
  {sessions.slice(0, PAGE_SIZE).map((s, i) => (
59
- <Box key={s.id}>
60
- <Text color={i === idx ? "cyan" : undefined} bold={i === idx}>
61
- {i === idx ? "▸" : " "}
62
- </Text>
63
- <Text dimColor>{s.id.slice(0, 12)}</Text>
64
- <Text> {s.mode} · {s.turnCount}t · {s.updatedAt.slice(0, 10)}</Text>
65
- </Box>
60
+ <SelectRow
61
+ key={s.id}
62
+ label={s.id.slice(0, 12)}
63
+ meta={`${s.mode} · ${s.turnCount}t · ${s.updatedAt.slice(0, 10)}`}
64
+ selected={i === idx}
65
+ />
66
66
  ))}
67
67
  <Box marginTop={1}>
68
68
  <Text dimColor>↑↓ j/k navigate · Enter select · Esc/q cancel</Text>
@@ -0,0 +1,43 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Text } from "ink";
3
+
4
+ // Spinner d'interferenza (it. 26), eco del mark animato del brand
5
+ // (logo/interference-mark-animated.svg): due sorgenti `◉` i cui fronti d'onda `‹ ›`
6
+ // si espandono verso il centro e interferiscono in `✕`, poi svaniscono e ripartono.
7
+ // Tutti i frame larghi 7 → niente jitter.
8
+ const FRAMES = [
9
+ "◉ ◉",
10
+ "◉‹ ›◉",
11
+ "◉‹‹ ››◉",
12
+ "◉‹‹✕››◉",
13
+ "◉‹ ✕ ›◉",
14
+ "◉ ✕ ◉",
15
+ "◉ · ◉",
16
+ ];
17
+
18
+ // Variante compatta (3 col) per le righe inline dei tool.
19
+ const FRAMES_INLINE = ["‹✕›", "·✕·", " ✕ ", "‹ ›"];
20
+
21
+ function useFrame(frames: string[], ms: number): string {
22
+ const [i, setI] = useState(0);
23
+ useEffect(() => {
24
+ const t = setInterval(() => setI((x) => (x + 1) % frames.length), ms);
25
+ return () => clearInterval(t);
26
+ }, [frames, ms]);
27
+ return frames[i]!;
28
+ }
29
+
30
+ export function Spinner({ label }: { label?: string }) {
31
+ const frame = useFrame(FRAMES, 110);
32
+ return (
33
+ <Text>
34
+ <Text color="white">{frame}</Text>
35
+ {label ? <Text dimColor>{" " + label}</Text> : null}
36
+ </Text>
37
+ );
38
+ }
39
+
40
+ export function SpinnerInline() {
41
+ const frame = useFrame(FRAMES_INLINE, 130);
42
+ return <Text dimColor>{frame}</Text>;
43
+ }
@@ -1,6 +1,7 @@
1
1
  import { useState, type FC } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { currentProvider, currentThinking, type ThinkingLevel } from "../config.ts";
4
+ import { SelectRow } from "./SelectRow.tsx";
4
5
 
5
6
  interface Props {
6
7
  onSelect: (level: ThinkingLevel) => void;
@@ -40,14 +41,13 @@ export const ThinkingPicker: FC<Props> = ({ onSelect, onCancel }) => {
40
41
  <Text bold>Thinking level · {provider.label}</Text>
41
42
  </Box>
42
43
  {levels.map((l, i) => (
43
- <Box key={l}>
44
- <Text color={i === idx ? "magenta" : undefined} bold={i === idx}>
45
- {i === idx ? "▸ " : " "}
46
- {l}
47
- </Text>
48
- {l === current && <Text dimColor> (current)</Text>}
49
- {HINT[l] && <Text dimColor> — {HINT[l]}</Text>}
50
- </Box>
44
+ <SelectRow
45
+ key={l}
46
+ label={l}
47
+ meta={HINT[l]}
48
+ selected={i === idx}
49
+ current={l === current}
50
+ />
51
51
  ))}
52
52
  <Box marginTop={1}>
53
53
  <Text dimColor>↑↓ j/k navigate · Enter select · Esc cancel</Text>
@@ -1,6 +1,35 @@
1
- import { Box, Text } from "ink";
2
- import { Spinner } from "@inkjs/ui";
1
+ import type { ReactNode } from "react";
2
+ import { Box, Text, useStdout } from "ink";
3
+ import { SpinnerInline } from "./Spinner.tsx";
3
4
  import type { DiffLine } from "./DiffView.tsx";
5
+ import { BG_PANEL, DIFF_ADD_BG, DIFF_REM_BG, panelWidth } from "./theme.ts";
6
+
7
+ const fill = (w: number, used: number) => " ".repeat(Math.max(0, w - used));
8
+
9
+ // Riga di diff: numero riga + marker + testo, su sfondo verde/rosso attenuato (it. 20).
10
+ function DiffLineRow({ d, w }: { d: DiffLine; w: number }) {
11
+ const bg = d.type === "add" ? DIFF_ADD_BG : d.type === "remove" ? DIFF_REM_BG : BG_PANEL;
12
+ const fg = d.type === "add" ? "green" : d.type === "remove" ? "red" : undefined;
13
+ const num = String((d.type === "add" ? d.newNo : d.oldNo) ?? "").padStart(4, " ");
14
+ const mark = d.type === "add" ? "+ " : d.type === "remove" ? "- " : " ";
15
+ const text = d.text.slice(0, Math.max(0, w - 9));
16
+ const used = 2 + 4 + 1 + 2 + text.length; // barra + numero + spazio + marker + testo
17
+ return (
18
+ <Text backgroundColor={bg}>
19
+ <Text color="gray" bold backgroundColor={bg}>
20
+ {"▌ "}
21
+ </Text>
22
+ <Text dimColor backgroundColor={bg}>
23
+ {num}{" "}
24
+ </Text>
25
+ <Text color={fg} dimColor={d.type === "same"} backgroundColor={bg}>
26
+ {mark}
27
+ {text}
28
+ </Text>
29
+ <Text backgroundColor={bg}>{fill(w, used)}</Text>
30
+ </Text>
31
+ );
32
+ }
4
33
 
5
34
  // Vista di un tool step (call → result). Allineata a ToolEntry in App.tsx.
6
35
  export interface ToolView {
@@ -27,6 +56,21 @@ const ICON: Record<string, string> = {
27
56
  task: "│",
28
57
  };
29
58
 
59
+ // Testo descrittivo durante l'esecuzione (it. 21, stile opencode `~ <verbo>…`).
60
+ const PENDING: Record<string, string> = {
61
+ bash: "Running command…",
62
+ read: "Reading file…",
63
+ ls: "Listing…",
64
+ glob: "Finding files…",
65
+ grep: "Searching content…",
66
+ webfetch: "Fetching…",
67
+ websearch: "Searching web…",
68
+ todowrite: "Updating todos…",
69
+ question: "Asking…",
70
+ task: "Delegating…",
71
+ };
72
+ const pendingText = (name: string) => PENDING[name] ?? "Working…";
73
+
30
74
  // Tool con output complesso → blocco con bordo sinistro. Gli altri → riga inline.
31
75
  const BLOCK = new Set(["bash", "write", "edit"]);
32
76
 
@@ -94,26 +138,59 @@ function InlineTool({
94
138
  desc: string;
95
139
  pending: boolean;
96
140
  }) {
97
- const iconColor = tool.isError ? "red" : "cyan";
141
+ const iconColor = tool.isError ? "red" : "white";
98
142
  return (
99
143
  <Box flexDirection="column" paddingLeft={3}>
100
144
  <Box>
101
- <Text color={iconColor}>{icon} </Text>
102
- <Text color={tool.isError ? "red" : undefined} dimColor={!tool.isError && !pending}>
103
- {tool.toolName} {desc}
104
- </Text>
105
- {pending && <Spinner label="" />}
145
+ {/* icona a larghezza fissa (2 col) → allineamento tra tool diversi */}
146
+ <Box width={2}>
147
+ <Text color={iconColor}>{icon}</Text>
148
+ </Box>
149
+ {pending ? (
150
+ <>
151
+ <Text dimColor>~ {pendingText(tool.toolName)} </Text>
152
+ <SpinnerInline />
153
+ </>
154
+ ) : (
155
+ <Text color={tool.isError ? "red" : undefined} dimColor={!tool.isError}>
156
+ {tool.toolName} {desc}
157
+ </Text>
158
+ )}
106
159
  </Box>
107
160
  {!pending && tool.output && (
108
- <Text dimColor color={tool.isError ? "red" : undefined}>
109
- {" "}
110
- {oneLine(tool.output)}
111
- </Text>
161
+ <Box paddingLeft={2}>
162
+ <Text dimColor color={tool.isError ? "red" : undefined}>
163
+ {oneLine(tool.output)}
164
+ </Text>
165
+ </Box>
112
166
  )}
113
167
  </Box>
114
168
  );
115
169
  }
116
170
 
171
+ // Riga del pannello: barra "▌ " (2 col) + contenuto + riempimento di sfondo a larghezza.
172
+ function PanelLine({
173
+ w,
174
+ barColor,
175
+ used,
176
+ children,
177
+ }: {
178
+ w: number;
179
+ barColor: string;
180
+ used: number;
181
+ children: ReactNode;
182
+ }) {
183
+ return (
184
+ <Text backgroundColor={BG_PANEL}>
185
+ <Text color={barColor} bold>
186
+ {"▌ "}
187
+ </Text>
188
+ {children}
189
+ <Text backgroundColor={BG_PANEL}>{fill(w - 2, used)}</Text>
190
+ </Text>
191
+ );
192
+ }
193
+
117
194
  function BlockTool({
118
195
  tool,
119
196
  icon,
@@ -125,55 +202,55 @@ function BlockTool({
125
202
  desc: string;
126
203
  pending: boolean;
127
204
  }) {
205
+ const { stdout } = useStdout();
206
+ const w = panelWidth(stdout?.columns);
207
+ const barColor = tool.isError ? "red" : "gray";
128
208
  const title =
129
209
  tool.toolName === "bash" ? oneLine(desc, 160) : `${tool.toolName} ${desc}`;
210
+ const titleClip = title.slice(0, w - 6);
211
+
212
+ const diff = tool.diff && tool.diff.length > 0 ? tool.diff : null;
213
+ const outLines =
214
+ !diff && !pending && tool.output ? clip(tool.output).split("\n") : [];
215
+
130
216
  return (
131
- <Box
132
- flexDirection="column"
133
- borderStyle="round"
134
- borderColor={tool.isError ? "red" : "gray"}
135
- borderTop={false}
136
- borderRight={false}
137
- borderBottom={false}
138
- paddingLeft={1}
139
- marginTop={1}
140
- marginBottom={1}
141
- >
142
- <Box>
143
- <Text color={tool.isError ? "red" : "cyan"} bold>
217
+ <Box flexDirection="column" marginTop={1} marginBottom={1}>
218
+ <PanelLine w={w} barColor={barColor} used={2 + titleClip.length}>
219
+ <Text color={tool.isError ? "red" : "white"} bold backgroundColor={BG_PANEL}>
144
220
  {icon}{" "}
145
221
  </Text>
146
- <Text bold>{title}</Text>
147
- {pending && <Spinner label="" />}
148
- </Box>
149
- {tool.diff && tool.diff.length > 0 ? (
150
- <DiffBody diff={tool.diff} />
151
- ) : (
152
- !pending &&
153
- tool.output && (
154
- <Text dimColor color={tool.isError ? "red" : undefined}>
155
- {clip(tool.output)}
222
+ <Text bold backgroundColor={BG_PANEL}>
223
+ {titleClip}
224
+ </Text>
225
+ </PanelLine>
226
+
227
+ {pending && (
228
+ <PanelLine w={w} barColor={barColor} used={2 + pendingText(tool.toolName).length}>
229
+ <Text dimColor backgroundColor={BG_PANEL}>
230
+ ~ {pendingText(tool.toolName)}
156
231
  </Text>
157
- )
232
+ </PanelLine>
158
233
  )}
159
- </Box>
160
- );
161
- }
162
234
 
163
- function DiffBody({ diff }: { diff: DiffLine[] }) {
164
- return (
165
- <Box flexDirection="column">
166
- {diff.slice(0, 15).map((d, i) => (
167
- <Text
168
- key={i}
169
- color={d.type === "add" ? "green" : d.type === "remove" ? "red" : undefined}
170
- dimColor={d.type === "same"}
171
- >
172
- {d.type === "add" ? "+ " : d.type === "remove" ? "- " : " "}
173
- {d.text.slice(0, 100)}
174
- </Text>
175
- ))}
176
- {diff.length > 15 && <Text dimColor>… {diff.length - 15} more lines</Text>}
235
+ {diff && diff.slice(0, 15).map((d, i) => <DiffLineRow key={i} d={d} w={w} />)}
236
+ {diff && diff.length > 15 && (
237
+ <PanelLine w={w} barColor={barColor} used={2 + 14}>
238
+ <Text dimColor backgroundColor={BG_PANEL}>
239
+ … {diff.length - 15} more
240
+ </Text>
241
+ </PanelLine>
242
+ )}
243
+
244
+ {outLines.map((ln, i) => {
245
+ const t = ln.slice(0, w - 2);
246
+ return (
247
+ <PanelLine key={i} w={w} barColor={barColor} used={t.length}>
248
+ <Text dimColor color={tool.isError ? "red" : undefined} backgroundColor={BG_PANEL}>
249
+ {t}
250
+ </Text>
251
+ </PanelLine>
252
+ );
253
+ })}
177
254
  </Box>
178
255
  );
179
256
  }
@@ -2,6 +2,7 @@ import { Box, Text } from "ink";
2
2
  import { currentThinking } from "../config.ts";
3
3
  import { WORDMARK } from "./wordmark.ts";
4
4
  import { PANEL, padRight } from "./theme.ts";
5
+ import { CURRENT_VERSION } from "../version.ts";
5
6
 
6
7
  // Larghezza del pannello wordmark = riga più lunga + margini.
7
8
  const WM_W = Math.max(...WORDMARK.map((l) => l.length)) + 4;
@@ -19,6 +20,7 @@ interface Props {
19
20
  provider: string;
20
21
  model: string;
21
22
  sessionCount: number;
23
+ update?: string | null;
22
24
  }
23
25
 
24
26
  function Tip({ cmd, desc }: { cmd: string; desc: string }) {
@@ -34,9 +36,19 @@ function Tip({ cmd, desc }: { cmd: string; desc: string }) {
34
36
 
35
37
  // Branding-only: l'input è condiviso (gestito da App), così gli slash e
36
38
  // l'autocomplete funzionano anche dalla home.
37
- export function Welcome({ provider, model, sessionCount }: Props) {
39
+ export function Welcome({ provider, model, sessionCount, update }: Props) {
38
40
  return (
39
41
  <Box flexDirection="column" marginBottom={1}>
42
+ {/* Banner aggiornamento (it. 28): discreto, solo se c'è una versione più recente */}
43
+ {update && (
44
+ <Box justifyContent="center" marginBottom={1}>
45
+ <Text color={ACCENT}>
46
+ interference {CURRENT_VERSION} → {update}
47
+ </Text>
48
+ <Text color={MUTED}> · run </Text>
49
+ <Text color={ACCENT}>/update</Text>
50
+ </Box>
51
+ )}
40
52
  {/* Header centrato: wordmark su pannello (sfondo reale via Text) + tagline */}
41
53
  <Box flexDirection="column" alignItems="center" marginBottom={1}>
42
54
  <Box flexDirection="column">
@@ -65,13 +77,16 @@ export function Welcome({ provider, model, sessionCount }: Props) {
65
77
  </Box>
66
78
  )}
67
79
 
68
- {/* Tips (blocco, centrato) */}
80
+ {/* Tips (blocco, centrato) — i due comandi fondamentali in cima */}
69
81
  <Box flexDirection="column" alignItems="center" marginTop={1}>
82
+ <Box marginBottom={1}>
83
+ <Text color={MUTED}>Get started — connect a provider, then pick a model:</Text>
84
+ </Box>
70
85
  <Box flexDirection="column">
71
- <Tip cmd="/help" desc="show all commands" />
72
- <Tip cmd="/build" desc="switch to full-access mode" />
86
+ <Tip cmd="/provider" desc="connect a provider (add your API key)" />
87
+ <Tip cmd="/model" desc="choose the model to use" />
88
+ <Tip cmd="/help" desc="all commands" />
73
89
  <Tip cmd="/thinking" desc="set reasoning level" />
74
- <Tip cmd="/init" desc="generate AGENTS.md" />
75
90
  </Box>
76
91
  </Box>
77
92
 
@@ -0,0 +1,24 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { reasoningSummary } from "../reasoning.ts";
3
+
4
+ describe("reasoningSummary (iter 23)", () => {
5
+ test("prima frase breve", () => {
6
+ expect(reasoningSummary("Devo controllare i file. Poi rispondo.")).toBe("Devo controllare i file.");
7
+ });
8
+
9
+ test("strip markdown iniziale e cap a 60", () => {
10
+ const long = "## " + "x".repeat(80);
11
+ const out = reasoningSummary(long);
12
+ expect(out.startsWith("x")).toBe(true);
13
+ expect(out.length).toBeLessThanOrEqual(61); // 60 + …
14
+ expect(out.endsWith("…")).toBe(true);
15
+ });
16
+
17
+ test("salta righe vuote, prende la prima significativa", () => {
18
+ expect(reasoningSummary("\n\n- Analizzo il loop")).toBe("Analizzo il loop");
19
+ });
20
+
21
+ test("testo vuoto → stringa vuota", () => {
22
+ expect(reasoningSummary(" \n ")).toBe("");
23
+ });
24
+ });