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.
- package/README.md +29 -8
- package/package.json +22 -3
- package/src/__tests__/version.test.ts +24 -0
- 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/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 +33 -23
- 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 +7 -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 +78 -0
- package/assets/screenshot.png +0 -0
- package/bun.lock +0 -159
- package/tsconfig.json +0 -23
package/src/tui/DiffView.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
30
|
+
add(nw);
|
|
25
31
|
ni++;
|
|
26
32
|
continue;
|
|
27
33
|
}
|
|
28
34
|
if (oldEnd.ni === ni) {
|
|
29
|
-
|
|
35
|
+
rem(old);
|
|
30
36
|
oi++;
|
|
31
37
|
continue;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
while (oi < oldEnd.oi) {
|
|
35
|
-
|
|
41
|
+
rem(oldLines[oi]!);
|
|
36
42
|
oi++;
|
|
37
43
|
}
|
|
38
44
|
while (ni < oldEnd.ni) {
|
|
39
|
-
|
|
45
|
+
add(newLines[ni]!);
|
|
40
46
|
ni++;
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
while (oi < oldLines.length) {
|
|
45
|
-
|
|
51
|
+
rem(oldLines[oi]!);
|
|
46
52
|
oi++;
|
|
47
53
|
}
|
|
48
54
|
while (ni < newLines.length) {
|
|
49
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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");
|
package/src/tui/MarkdownText.tsx
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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);
|
package/src/tui/ModelPicker.tsx
CHANGED
|
@@ -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
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{m.id === current
|
|
39
|
-
|
|
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 {
|
|
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={
|
|
30
|
+
<Text key={i} backgroundColor={bg} color="white" bold={bold}>
|
|
27
31
|
{bar && i === 0 ? (
|
|
28
|
-
<Text backgroundColor={
|
|
32
|
+
<Text backgroundColor={bg} color={barColor} bold>
|
|
29
33
|
{prefix}
|
|
30
34
|
</Text>
|
|
31
35
|
) : (
|
|
32
|
-
<Text backgroundColor={
|
|
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
|
+
}
|
package/src/tui/SessionList.tsx
CHANGED
|
@@ -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
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{l === current
|
|
49
|
-
|
|
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>
|
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
|
+
});
|