interference-agent 0.1.0 → 0.2.3

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,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
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { tokenizeLine, normalizeLang } from "../syntax.ts";
3
+
4
+ describe("syntax tokenizer (iter 22)", () => {
5
+ test("classifies keyword / string / number / comment", () => {
6
+ const toks = tokenizeLine('const x = "a" // c', "ts");
7
+ const byColor = Object.fromEntries(toks.filter((t) => t.color).map((t) => [t.text, t.color]));
8
+ expect(byColor["const"]).toBe("cyan");
9
+ expect(byColor['"a"']).toBe("green");
10
+ expect(byColor["// c"]).toBe("gray");
11
+ });
12
+
13
+ test("numbers are magenta", () => {
14
+ const toks = tokenizeLine("const n = 42", "ts");
15
+ expect(toks.find((t) => t.text === "42")?.color).toBe("magenta");
16
+ });
17
+
18
+ test("python uses # comments", () => {
19
+ const toks = tokenizeLine("x = 1 # note", "py");
20
+ expect(toks.find((t) => t.text === "# note")?.color).toBe("gray");
21
+ });
22
+
23
+ test("unknown lang → single plain token", () => {
24
+ expect(tokenizeLine("anything here", "")).toEqual([{ text: "anything here" }]);
25
+ });
26
+
27
+ test("normalizeLang maps aliases", () => {
28
+ expect(normalizeLang("typescript")).toBe("ts");
29
+ expect(normalizeLang("PYTHON")).toBe("py");
30
+ expect(normalizeLang("bash")).toBe("sh");
31
+ expect(normalizeLang("rust")).toBe("");
32
+ });
33
+ });
@@ -3,6 +3,7 @@ import React from "react";
3
3
  import { render } from "ink-testing-library";
4
4
  import { ToolStep } from "../ToolStep.tsx";
5
5
  import { MarkdownText } from "../MarkdownText.tsx";
6
+ import { computeDiff } from "../DiffView.tsx";
6
7
 
7
8
  function frame(el: React.ReactElement): string {
8
9
  const { lastFrame, unmount } = render(el);
@@ -19,10 +20,18 @@ describe("ToolStep rendering (iter 18)", () => {
19
20
  expect(out).not.toContain("{"); // niente JSON grezzo
20
21
  });
21
22
 
22
- test("bash block: command rendered with $ and output", () => {
23
+ test("pending tool shows descriptive text (iter 21)", () => {
24
+ const grep = frame(<ToolStep tool={{ toolName: "grep", input: { pattern: "foo" } }} />);
25
+ expect(grep).toContain("~ Searching content…");
26
+ const bash = frame(<ToolStep tool={{ toolName: "bash", input: { command: "ls" } }} />);
27
+ expect(bash).toContain("~ Running command…");
28
+ });
29
+
30
+ test("bash block: command rendered with $ and output, on a panel (▌ bar)", () => {
23
31
  const out = frame(<ToolStep tool={{ toolName: "bash", input: { command: "bun test" }, output: "77 pass" }} />);
24
32
  expect(out).toContain("$ bun test");
25
33
  expect(out).toContain("77 pass");
34
+ expect(out).toContain("▌"); // pannello (it. 19): barra laterale su sfondo
26
35
  });
27
36
 
28
37
  test("edit block: diff lines with +/-", () => {
@@ -45,6 +54,26 @@ describe("ToolStep rendering (iter 18)", () => {
45
54
  });
46
55
  });
47
56
 
57
+ describe("diff line numbers (iter 20)", () => {
58
+ test("computeDiff assigns old/new line numbers", () => {
59
+ const d = computeDiff(["a", "b", "c"], ["a", "B", "c"]);
60
+ expect(d).toEqual([
61
+ { type: "same", text: "a", oldNo: 1, newNo: 1 },
62
+ { type: "remove", text: "b", oldNo: 2 },
63
+ { type: "add", text: "B", newNo: 2 },
64
+ { type: "same", text: "c", oldNo: 3, newNo: 3 },
65
+ ]);
66
+ });
67
+
68
+ test("edit block renders line numbers in the diff", () => {
69
+ const d = computeDiff(["x"], ["y"]);
70
+ const out = frame(<ToolStep tool={{ toolName: "edit", input: { path: "f.ts" }, output: "ok", diff: d }} />);
71
+ expect(out).toContain("- x");
72
+ expect(out).toContain("+ y");
73
+ expect(out).toMatch(/\b1\b/); // numero di riga presente
74
+ });
75
+ });
76
+
48
77
  describe("MarkdownText rendering (iter 18)", () => {
49
78
  test("strips markdown markers, keeps content", () => {
50
79
  const out = frame(<MarkdownText content={"# Titolo\nUn **bold** e `code`.\n- item"} />);
@@ -0,0 +1,22 @@
1
+ // Esempi rotanti per il placeholder dell'input (it. 25), per modalità.
2
+ export const PLACEHOLDERS: Record<"plan" | "build", string[]> = {
3
+ build: [
4
+ "Fix a TODO in the codebase",
5
+ "Add a test for the parser",
6
+ "Refactor this module",
7
+ "Implement the next iteration",
8
+ ],
9
+ plan: [
10
+ "How does the agent loop work?",
11
+ "Where is resolveInWorkspace defined?",
12
+ "Map the tool system",
13
+ "What does this function return?",
14
+ ],
15
+ };
16
+
17
+ export function placeholderFor(mode: string, idx: number): string {
18
+ const list = mode === "plan" ? PLACEHOLDERS.plan : PLACEHOLDERS.build;
19
+ const verb = mode === "plan" ? "Explore" : "Ask anything";
20
+ const ex = list[((idx % list.length) + list.length) % list.length];
21
+ return `${verb}… "${ex}"`;
22
+ }
@@ -0,0 +1,23 @@
1
+ // Sommario del pensiero (it. 23): ricava un titolo breve dalla prima frase significativa
2
+ // del reasoning, per l'header `✻ Thinking/Thought: <titolo>` (stile opencode).
3
+
4
+ export function reasoningSummary(text: string): string {
5
+ const firstLine = text
6
+ .split("\n")
7
+ .map((l) => l.trim())
8
+ .find((l) => l.length > 0);
9
+ if (!firstLine) return "";
10
+
11
+ let s = firstLine
12
+ .replace(/^#{1,6}\s+/, "") // heading
13
+ .replace(/^[-*>]\s+/, "") // bullet/quote
14
+ .replace(/^\d+\.\s+/, "") // lista numerata
15
+ .replace(/[*_`]/g, "") // enfasi/code inline
16
+ .replace(/\s+/g, " ")
17
+ .trim();
18
+
19
+ // Taglia alla prima frase se breve, altrimenti cap a 60 char.
20
+ const sentence = s.match(/^(.*?[.!?])(\s|$)/);
21
+ if (sentence && sentence[1] && sentence[1].length <= 60) return sentence[1];
22
+ return s.length > 60 ? s.slice(0, 60).trimEnd() + "…" : s;
23
+ }
@@ -0,0 +1,58 @@
1
+ // Syntax highlighting MINIMALE per i code block (it. 22). Niente parser/dipendenze:
2
+ // un tokenizer a regex per pochi scope (commento/stringa/numero/keyword) — "good enough"
3
+ // come la subtle-syntax di opencode. Colori per il contenuto (i code block sono per natura
4
+ // colorati); la chrome resta B&W.
5
+
6
+ export interface Tok {
7
+ text: string;
8
+ color?: string;
9
+ dim?: boolean;
10
+ }
11
+
12
+ const KW: Record<string, string[]> = {
13
+ ts: ["import","export","from","const","let","var","function","return","if","else","for","while","class","extends","implements","interface","type","enum","new","async","await","try","catch","finally","throw","typeof","instanceof","as","in","of","public","private","protected","readonly","static","void","null","undefined","true","false"],
14
+ js: ["import","export","from","const","let","var","function","return","if","else","for","while","class","extends","new","async","await","try","catch","finally","throw","typeof","instanceof","in","of","null","undefined","true","false"],
15
+ py: ["def","class","import","from","return","if","elif","else","for","while","try","except","finally","with","as","in","not","and","or","is","lambda","yield","async","await","pass","raise","None","True","False","self"],
16
+ sh: ["if","then","else","elif","fi","for","in","do","done","while","case","esac","function","echo","export","local","return","cd","exit"],
17
+ json: [],
18
+ };
19
+
20
+ export function normalizeLang(info: string): string {
21
+ const l = info.toLowerCase().trim();
22
+ if (l === "ts" || l === "typescript" || l === "tsx") return "ts";
23
+ if (l === "js" || l === "javascript" || l === "jsx" || l === "mjs") return "js";
24
+ if (l === "py" || l === "python") return "py";
25
+ if (l === "sh" || l === "bash" || l === "shell" || l === "zsh") return "sh";
26
+ if (l === "json") return "json";
27
+ return "";
28
+ }
29
+
30
+ const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+
32
+ export function tokenizeLine(line: string, lang: string): Tok[] {
33
+ if (!lang) return [{ text: line }];
34
+ const kw = KW[lang] ?? [];
35
+ const commentPat = lang === "py" || lang === "sh" ? "(#[^\\n]*)" : "(\\/\\/[^\\n]*)";
36
+ const parts = [
37
+ commentPat,
38
+ "(\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|`(?:[^`\\\\]|\\\\.)*`)",
39
+ "(\\b\\d+(?:\\.\\d+)?\\b)",
40
+ ];
41
+ if (kw.length) parts.push("(\\b(?:" + kw.map(escape).join("|") + ")\\b)");
42
+ const re = new RegExp(parts.join("|"), "g");
43
+
44
+ const out: Tok[] = [];
45
+ let last = 0;
46
+ let m: RegExpExecArray | null;
47
+ while ((m = re.exec(line)) !== null) {
48
+ if (m.index > last) out.push({ text: line.slice(last, m.index) });
49
+ if (m[1]) out.push({ text: m[0], color: "gray", dim: true }); // commento
50
+ else if (m[2]) out.push({ text: m[0], color: "green" }); // stringa
51
+ else if (m[3]) out.push({ text: m[0], color: "magenta" }); // numero
52
+ else out.push({ text: m[0], color: "cyan" }); // keyword
53
+ last = m.index + m[0].length;
54
+ if (m[0].length === 0) re.lastIndex++; // guard anti-loop
55
+ }
56
+ if (last < line.length) out.push({ text: line.slice(last) });
57
+ return out.length ? out : [{ text: line }];
58
+ }
package/src/tui/theme.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  // Palette condivisa della TUI. Brand: bianco/nero (niente colori).
2
2
  // Il contrasto nasce dal PANEL (sfondo applicato sui <Text>, non sui <Box>:
3
3
  // in Ink il backgroundColor del Box non riempie il padding → si usa il Text).
4
- export const PANEL = "#262626"; // sfondo pannello (grigio scuro, stacca dal nero)
4
+ // Gerarchia sfondi a 3 livelli (B&W), stile opencode (base < panel < element).
5
+ export const BG_PANEL = "#141414"; // blocchi/dialog (un gradino sopra il nero)
6
+ export const BG_ELEMENT = "#1e1e1e"; // elementi/selezione/pannello utente (un gradino sopra ancora)
7
+ export const PANEL = BG_ELEMENT; // alias storico (messaggio utente, input, welcome)
5
8
  export const USER_BAR = "white"; // barra messaggi utente
6
9
  export const ASSISTANT_BAR = "gray"; // barra risposte assistant
7
10
 
11
+ // Sfondi diff (attenuati, leggibili su terminale scuro) — it. 20.
12
+ export const DIFF_ADD_BG = "#0f2a18"; // riga aggiunta (verde scuro)
13
+ export const DIFF_REM_BG = "#3a1414"; // riga rimossa (rosso scuro)
14
+
15
+ // Ambra: riservata ESCLUSIVAMENTE al pensiero (reasoning), come opencode.
16
+ // Esecuzione (tool) e risposte restano bianco/nero → le tre fasi si distinguono.
17
+ export const THINKING = "#d79921"; // header pensiero (ambra)
18
+ export const THINKING_BODY = "gray"; // corpo pensiero (attenuato)
19
+
8
20
  // Pad a destra fino a w caratteri (per estendere lo sfondo del Text).
9
21
  export function padRight(s: string, w: number): string {
10
22
  return s.length >= w ? s : s + " ".repeat(w - s.length);
package/src/version.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { interferenceDir } from "./paths.ts";
5
+
6
+ // Versione corrente letta dal package.json del pacchetto (sync, funziona anche
7
+ // installato globalmente: version.ts sta in <pkg>/src, package.json in <pkg>/).
8
+ function readVersion(): string {
9
+ try {
10
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
11
+ return (JSON.parse(raw).version as string) ?? "0.0.0";
12
+ } catch {
13
+ return "0.0.0";
14
+ }
15
+ }
16
+
17
+ export const CURRENT_VERSION: string = readVersion();
18
+
19
+ const PKG = "interference-agent";
20
+ const TTL = 24 * 60 * 60 * 1000; // 24h
21
+ const REGISTRY = `https://registry.npmjs.org/${PKG}/latest`;
22
+
23
+ function cachePath(): string {
24
+ // Reindirizzabile via INTERFERENCE_HOME (isolamento test) — vedi paths.ts.
25
+ return interferenceDir("update-check.json");
26
+ }
27
+
28
+ // Confronto semver "x.y.z" (con eventuale prefisso v). true se `latest` > `current`.
29
+ export function isNewer(latest: string, current: string): boolean {
30
+ const p = (v: string) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
31
+ const a = p(latest);
32
+ const b = p(current);
33
+ for (let i = 0; i < 3; i++) {
34
+ const x = a[i] ?? 0;
35
+ const y = b[i] ?? 0;
36
+ if (x !== y) return x > y;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ // Controlla npm per una versione più recente. Throttled (cache 24h), non bloccante,
42
+ // silenzioso offline. Ritorna la versione latest se più nuova, altrimenti null.
43
+ export async function checkForUpdate(): Promise<string | null> {
44
+ if (process.env.INTERFERENCE_NO_UPDATE_CHECK) return null;
45
+ const file = cachePath();
46
+ try {
47
+ let latest: string | null = null;
48
+
49
+ // 1) cache fresca?
50
+ try {
51
+ const c = JSON.parse(await readFile(file, "utf8"));
52
+ if (typeof c.ts === "number" && Date.now() - c.ts < TTL && typeof c.latest === "string") {
53
+ latest = c.latest;
54
+ }
55
+ } catch {
56
+ // niente cache valida
57
+ }
58
+
59
+ // 2) altrimenti interroga il registry (timeout breve)
60
+ if (!latest) {
61
+ const res = await fetch(REGISTRY, { signal: AbortSignal.timeout(2500) });
62
+ if (!res.ok) return null;
63
+ const j = (await res.json()) as { version?: string };
64
+ latest = j.version ?? null;
65
+ if (latest) {
66
+ try {
67
+ await mkdir(path.dirname(file), { recursive: true });
68
+ await writeFile(file, JSON.stringify({ ts: Date.now(), latest }));
69
+ } catch {
70
+ // cache best-effort
71
+ }
72
+ }
73
+ }
74
+
75
+ return latest && isNewer(latest, CURRENT_VERSION) ? latest : null;
76
+ } catch {
77
+ return null; // offline / errori → nessun avviso
78
+ }
79
+ }
Binary file