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.
- package/README.md +29 -8
- package/package.json +22 -3
- package/src/__tests__/version.test.ts +24 -0
- package/src/auth.ts +8 -10
- package/src/cli-plain.ts +3 -3
- package/src/cli.ts +8 -0
- package/src/commands/index.ts +18 -0
- package/src/config.ts +29 -18
- package/src/cost.ts +2 -0
- package/src/paths.ts +25 -0
- package/src/session/__tests__/session.test.ts +9 -0
- package/src/session/store.ts +4 -5
- package/src/skills.ts +9 -9
- package/src/tools/__tests__/mutating.test.ts +1 -1
- package/src/tools/bash.ts +25 -6
- package/src/tools/grep.ts +27 -21
- package/src/tui/App.tsx +73 -30
- package/src/tui/ConfirmDialog.tsx +13 -4
- package/src/tui/DiffView.tsx +17 -17
- package/src/tui/MarkdownText.tsx +25 -3
- package/src/tui/ModelPicker.tsx +8 -7
- package/src/tui/Panel.tsx +8 -4
- package/src/tui/SelectRow.tsx +40 -0
- package/src/tui/SessionList.tsx +6 -7
- package/src/tui/Spinner.tsx +43 -0
- package/src/tui/ThinkingPicker.tsx +8 -8
- package/src/tui/ToolStep.tsx +130 -53
- package/src/tui/Welcome.tsx +20 -5
- package/src/tui/__tests__/reasoning.test.ts +24 -0
- package/src/tui/__tests__/syntax.test.ts +33 -0
- package/src/tui/__tests__/tui-render.test.tsx +30 -1
- package/src/tui/placeholders.ts +22 -0
- package/src/tui/reasoning.ts +23 -0
- package/src/tui/syntax.ts +58 -0
- package/src/tui/theme.ts +13 -1
- package/src/version.ts +79 -0
- package/assets/screenshot.png +0 -0
- package/bun.lock +0 -159
- package/tsconfig.json +0 -23
package/src/tui/ToolStep.tsx
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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" : "
|
|
141
|
+
const iconColor = tool.isError ? "red" : "white";
|
|
98
142
|
return (
|
|
99
143
|
<Box flexDirection="column" paddingLeft={3}>
|
|
100
144
|
<Box>
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
{
|
|
104
|
-
</
|
|
105
|
-
{pending
|
|
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
|
-
<
|
|
109
|
-
{"
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
}
|
package/src/tui/Welcome.tsx
CHANGED
|
@@ -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="/
|
|
72
|
-
<Tip cmd="/
|
|
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("
|
|
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
|
-
|
|
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
|
+
}
|
package/assets/screenshot.png
DELETED
|
Binary file
|