interference-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +74 -0
- package/assets/screenshot.png +0 -0
- package/bun.lock +159 -0
- package/package.json +39 -0
- package/src/agent/compaction.ts +114 -0
- package/src/agent/loop.ts +94 -0
- package/src/agent/prompt.ts +89 -0
- package/src/agent/subagent.ts +64 -0
- package/src/auth.ts +50 -0
- package/src/cli-plain.ts +274 -0
- package/src/cli.ts +87 -0
- package/src/commands/index.ts +184 -0
- package/src/config-file.ts +109 -0
- package/src/config.ts +212 -0
- package/src/context.ts +96 -0
- package/src/cost.ts +54 -0
- package/src/git.ts +22 -0
- package/src/permissions.ts +135 -0
- package/src/provider.ts +58 -0
- package/src/session/__tests__/session.test.ts +180 -0
- package/src/session/snapshot.ts +122 -0
- package/src/session/store.ts +120 -0
- package/src/skills.ts +177 -0
- package/src/tools/__tests__/mutating.test.ts +324 -0
- package/src/tools/__tests__/question.test.ts +53 -0
- package/src/tools/__tests__/todowrite.test.ts +57 -0
- package/src/tools/__tests__/tools.test.ts +217 -0
- package/src/tools/_fs.ts +12 -0
- package/src/tools/bash.ts +104 -0
- package/src/tools/edit.ts +98 -0
- package/src/tools/glob.ts +40 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/index.ts +21 -0
- package/src/tools/ls.ts +70 -0
- package/src/tools/question.ts +81 -0
- package/src/tools/read.ts +61 -0
- package/src/tools/registry.ts +36 -0
- package/src/tools/task.ts +71 -0
- package/src/tools/todowrite.ts +84 -0
- package/src/tools/webfetch.ts +111 -0
- package/src/tools/write.ts +51 -0
- package/src/tui/App.tsx +738 -0
- package/src/tui/ConfirmDialog.tsx +46 -0
- package/src/tui/DiffView.tsx +88 -0
- package/src/tui/MarkdownText.tsx +63 -0
- package/src/tui/Message.tsx +26 -0
- package/src/tui/ModelPicker.tsx +44 -0
- package/src/tui/Panel.tsx +39 -0
- package/src/tui/ProviderPicker.tsx +111 -0
- package/src/tui/QuestionDialog.tsx +64 -0
- package/src/tui/SessionList.tsx +72 -0
- package/src/tui/SlashAutocomplete.tsx +33 -0
- package/src/tui/StatusFooter.tsx +71 -0
- package/src/tui/ThinkingPicker.tsx +57 -0
- package/src/tui/Toast.tsx +64 -0
- package/src/tui/TodoList.tsx +49 -0
- package/src/tui/ToolStep.tsx +184 -0
- package/src/tui/Welcome.tsx +87 -0
- package/src/tui/__tests__/tui-render.test.tsx +59 -0
- package/src/tui/theme.ts +16 -0
- package/src/tui/wordmark.ts +7 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState, type FC } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { currentProvider, currentThinking, type ThinkingLevel } from "../config.ts";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
onSelect: (level: ThinkingLevel) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const HINT: Partial<Record<ThinkingLevel, string>> = {
|
|
11
|
+
off: "no reasoning (fastest)",
|
|
12
|
+
max: "deepest reasoning",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const ThinkingPicker: FC<Props> = ({ onSelect, onCancel }) => {
|
|
16
|
+
const provider = currentProvider();
|
|
17
|
+
const levels = provider.thinkingLevels;
|
|
18
|
+
const current = currentThinking();
|
|
19
|
+
const [idx, setIdx] = useState(Math.max(0, levels.indexOf(current)));
|
|
20
|
+
|
|
21
|
+
useInput(
|
|
22
|
+
(input, key) => {
|
|
23
|
+
if (key.upArrow || input === "k") {
|
|
24
|
+
setIdx((i) => (i > 0 ? i - 1 : levels.length - 1));
|
|
25
|
+
} else if (key.downArrow || input === "j") {
|
|
26
|
+
setIdx((i) => (i < levels.length - 1 ? i + 1 : 0));
|
|
27
|
+
} else if (key.return) {
|
|
28
|
+
const l = levels[idx];
|
|
29
|
+
if (l) onSelect(l);
|
|
30
|
+
} else if (key.escape || input === "q") {
|
|
31
|
+
onCancel();
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{ isActive: true },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
39
|
+
<Box marginBottom={1}>
|
|
40
|
+
<Text bold>Thinking level · {provider.label}</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
{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>
|
|
51
|
+
))}
|
|
52
|
+
<Box marginTop={1}>
|
|
53
|
+
<Text dimColor>↑↓ j/k navigate · Enter select · Esc cancel</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, type FC, type ReactNode } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
interface Toast {
|
|
5
|
+
id: number;
|
|
6
|
+
message: string;
|
|
7
|
+
type: "success" | "error" | "info" | "warning";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
toasts: Toast[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ToastContainer({ toasts }: Props) {
|
|
15
|
+
if (toasts.length === 0) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
{toasts.slice(0, 3).map((t) => (
|
|
20
|
+
<Box key={t.id} marginBottom={0}>
|
|
21
|
+
<Text
|
|
22
|
+
color={
|
|
23
|
+
t.type === "success"
|
|
24
|
+
? "green"
|
|
25
|
+
: t.type === "error"
|
|
26
|
+
? "red"
|
|
27
|
+
: t.type === "warning"
|
|
28
|
+
? "yellow"
|
|
29
|
+
: "blue"
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
{t.type === "success" ? "✓" : t.type === "error" ? "✗" : t.type === "warning" ? "△" : "ℹ"}{" "}
|
|
33
|
+
{t.message}
|
|
34
|
+
</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
))}
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let nextToastId = 1;
|
|
42
|
+
|
|
43
|
+
export function useToast() {
|
|
44
|
+
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
45
|
+
const timers = useRef(new Set<ReturnType<typeof setTimeout>>());
|
|
46
|
+
|
|
47
|
+
const addToast = useCallback((message: string, type: Toast["type"] = "info") => {
|
|
48
|
+
const id = nextToastId++;
|
|
49
|
+
setToasts((prev) => [...prev, { id, message, type }]);
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
52
|
+
timers.current.delete(timer);
|
|
53
|
+
}, 3000);
|
|
54
|
+
timers.current.add(timer);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
return () => {
|
|
59
|
+
for (const t of timers.current) clearTimeout(t);
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return { toasts, addToast };
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { Todo, TodoStatus } from "../tools/todowrite.ts";
|
|
3
|
+
|
|
4
|
+
const SYMBOL: Record<TodoStatus, string> = {
|
|
5
|
+
pending: "○",
|
|
6
|
+
in_progress: "◐",
|
|
7
|
+
completed: "✓",
|
|
8
|
+
cancelled: "✗",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const COLOR: Record<TodoStatus, string | undefined> = {
|
|
12
|
+
pending: undefined,
|
|
13
|
+
in_progress: "yellow",
|
|
14
|
+
completed: "green",
|
|
15
|
+
cancelled: "gray",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function TodoList({ todos }: { todos: Todo[] }) {
|
|
19
|
+
if (todos.length === 0) return null;
|
|
20
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Box
|
|
24
|
+
flexDirection="column"
|
|
25
|
+
borderStyle="round"
|
|
26
|
+
borderColor="gray"
|
|
27
|
+
borderTop={false}
|
|
28
|
+
borderRight={false}
|
|
29
|
+
borderBottom={false}
|
|
30
|
+
paddingLeft={1}
|
|
31
|
+
marginBottom={1}
|
|
32
|
+
>
|
|
33
|
+
<Text dimColor bold>
|
|
34
|
+
☰ todos {done}/{todos.length}
|
|
35
|
+
</Text>
|
|
36
|
+
{todos.map((t, i) => (
|
|
37
|
+
<Text key={i} color={COLOR[t.status]} dimColor={t.status === "cancelled"}>
|
|
38
|
+
{SYMBOL[t.status]}{" "}
|
|
39
|
+
<Text strikethrough={t.status === "completed" || t.status === "cancelled"}>
|
|
40
|
+
{t.content}
|
|
41
|
+
</Text>
|
|
42
|
+
{t.priority && t.priority !== "medium" ? (
|
|
43
|
+
<Text dimColor> ({t.priority})</Text>
|
|
44
|
+
) : null}
|
|
45
|
+
</Text>
|
|
46
|
+
))}
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { Spinner } from "@inkjs/ui";
|
|
3
|
+
import type { DiffLine } from "./DiffView.tsx";
|
|
4
|
+
|
|
5
|
+
// Vista di un tool step (call → result). Allineata a ToolEntry in App.tsx.
|
|
6
|
+
export interface ToolView {
|
|
7
|
+
toolName: string;
|
|
8
|
+
input: unknown;
|
|
9
|
+
output?: string;
|
|
10
|
+
isError?: boolean;
|
|
11
|
+
diff?: DiffLine[] | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Icone per tipo (convenzione opencode, adattata — fonte: opencode-dev/packages/tui).
|
|
15
|
+
const ICON: Record<string, string> = {
|
|
16
|
+
bash: "$",
|
|
17
|
+
read: "→",
|
|
18
|
+
ls: "→",
|
|
19
|
+
glob: "✱",
|
|
20
|
+
grep: "✱",
|
|
21
|
+
write: "←",
|
|
22
|
+
edit: "←",
|
|
23
|
+
webfetch: "%",
|
|
24
|
+
websearch: "◈",
|
|
25
|
+
todowrite: "⚙",
|
|
26
|
+
question: "?",
|
|
27
|
+
task: "│",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Tool con output complesso → blocco con bordo sinistro. Gli altri → riga inline.
|
|
31
|
+
const BLOCK = new Set(["bash", "write", "edit"]);
|
|
32
|
+
|
|
33
|
+
function rec(input: unknown): Record<string, unknown> {
|
|
34
|
+
return input && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Descrizione sintetica (path/comando/pattern) invece del JSON grezzo.
|
|
38
|
+
function describe(toolName: string, input: unknown): string {
|
|
39
|
+
const i = rec(input);
|
|
40
|
+
const s = (v: unknown) => (typeof v === "string" ? v : "");
|
|
41
|
+
switch (toolName) {
|
|
42
|
+
case "bash":
|
|
43
|
+
return s(i.command);
|
|
44
|
+
case "read":
|
|
45
|
+
case "write":
|
|
46
|
+
case "edit":
|
|
47
|
+
return s(i.path) || s(i.filePath);
|
|
48
|
+
case "ls":
|
|
49
|
+
return s(i.path) || ".";
|
|
50
|
+
case "glob":
|
|
51
|
+
case "grep":
|
|
52
|
+
return s(i.pattern);
|
|
53
|
+
case "webfetch":
|
|
54
|
+
return s(i.url);
|
|
55
|
+
case "websearch":
|
|
56
|
+
return s(i.query);
|
|
57
|
+
case "todowrite":
|
|
58
|
+
return "todos";
|
|
59
|
+
case "question":
|
|
60
|
+
return Array.isArray(i.questions) ? `${i.questions.length} question(s)` : "question";
|
|
61
|
+
case "task":
|
|
62
|
+
return s(i.description) || s(i.prompt).slice(0, 60);
|
|
63
|
+
default: {
|
|
64
|
+
const str = typeof input === "string" ? input : JSON.stringify(input);
|
|
65
|
+
return str.length > 80 ? str.slice(0, 80) + "…" : str;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function oneLine(s: string, max = 120): string {
|
|
71
|
+
const flat = s.replace(/\s*\n\s*/g, " ").trim();
|
|
72
|
+
return flat.length > max ? flat.slice(0, max) + "…" : flat;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ToolStep({ tool }: { tool: ToolView }) {
|
|
76
|
+
const icon = ICON[tool.toolName] ?? "·";
|
|
77
|
+
const desc = describe(tool.toolName, tool.input);
|
|
78
|
+
const pending = tool.output === undefined;
|
|
79
|
+
return BLOCK.has(tool.toolName) ? (
|
|
80
|
+
<BlockTool tool={tool} icon={icon} desc={desc} pending={pending} />
|
|
81
|
+
) : (
|
|
82
|
+
<InlineTool tool={tool} icon={icon} desc={desc} pending={pending} />
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function InlineTool({
|
|
87
|
+
tool,
|
|
88
|
+
icon,
|
|
89
|
+
desc,
|
|
90
|
+
pending,
|
|
91
|
+
}: {
|
|
92
|
+
tool: ToolView;
|
|
93
|
+
icon: string;
|
|
94
|
+
desc: string;
|
|
95
|
+
pending: boolean;
|
|
96
|
+
}) {
|
|
97
|
+
const iconColor = tool.isError ? "red" : "cyan";
|
|
98
|
+
return (
|
|
99
|
+
<Box flexDirection="column" paddingLeft={3}>
|
|
100
|
+
<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="" />}
|
|
106
|
+
</Box>
|
|
107
|
+
{!pending && tool.output && (
|
|
108
|
+
<Text dimColor color={tool.isError ? "red" : undefined}>
|
|
109
|
+
{" "}
|
|
110
|
+
{oneLine(tool.output)}
|
|
111
|
+
</Text>
|
|
112
|
+
)}
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function BlockTool({
|
|
118
|
+
tool,
|
|
119
|
+
icon,
|
|
120
|
+
desc,
|
|
121
|
+
pending,
|
|
122
|
+
}: {
|
|
123
|
+
tool: ToolView;
|
|
124
|
+
icon: string;
|
|
125
|
+
desc: string;
|
|
126
|
+
pending: boolean;
|
|
127
|
+
}) {
|
|
128
|
+
const title =
|
|
129
|
+
tool.toolName === "bash" ? oneLine(desc, 160) : `${tool.toolName} ${desc}`;
|
|
130
|
+
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>
|
|
144
|
+
{icon}{" "}
|
|
145
|
+
</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)}
|
|
156
|
+
</Text>
|
|
157
|
+
)
|
|
158
|
+
)}
|
|
159
|
+
</Box>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
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>}
|
|
177
|
+
</Box>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function clip(s: string, max = 500): string {
|
|
182
|
+
const lines = s.split("\n").slice(0, 12).join("\n");
|
|
183
|
+
return lines.length > max ? lines.slice(0, max) + "\n… [truncated]" : lines;
|
|
184
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { currentThinking } from "../config.ts";
|
|
3
|
+
import { WORDMARK } from "./wordmark.ts";
|
|
4
|
+
import { PANEL, padRight } from "./theme.ts";
|
|
5
|
+
|
|
6
|
+
// Larghezza del pannello wordmark = riga più lunga + margini.
|
|
7
|
+
const WM_W = Math.max(...WORDMARK.map((l) => l.length)) + 4;
|
|
8
|
+
// Righe del pannello: una vuota sopra/sotto + il wordmark, tutte riempite.
|
|
9
|
+
const WM_LINES = ["", ...WORDMARK, ""].map((l) => padRight(" " + l, WM_W));
|
|
10
|
+
|
|
11
|
+
// Colori per tema scuro (NON usare #0a0a0a sul testo → invisibile).
|
|
12
|
+
const FG = "white";
|
|
13
|
+
const ACCENT = "cyan";
|
|
14
|
+
const MUTED = "gray";
|
|
15
|
+
const GREEN = "#2e8b57";
|
|
16
|
+
const RED = "#cd5c5c";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
provider: string;
|
|
20
|
+
model: string;
|
|
21
|
+
sessionCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Tip({ cmd, desc }: { cmd: string; desc: string }) {
|
|
25
|
+
return (
|
|
26
|
+
<Box>
|
|
27
|
+
<Box width={11}>
|
|
28
|
+
<Text color={ACCENT}>{cmd}</Text>
|
|
29
|
+
</Box>
|
|
30
|
+
<Text color={MUTED}>{desc}</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Branding-only: l'input è condiviso (gestito da App), così gli slash e
|
|
36
|
+
// l'autocomplete funzionano anche dalla home.
|
|
37
|
+
export function Welcome({ provider, model, sessionCount }: Props) {
|
|
38
|
+
return (
|
|
39
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
40
|
+
{/* Header centrato: wordmark su pannello (sfondo reale via Text) + tagline */}
|
|
41
|
+
<Box flexDirection="column" alignItems="center" marginBottom={1}>
|
|
42
|
+
<Box flexDirection="column">
|
|
43
|
+
{WM_LINES.map((line, i) => (
|
|
44
|
+
<Text key={i} bold color={FG} backgroundColor={PANEL}>
|
|
45
|
+
{line}
|
|
46
|
+
</Text>
|
|
47
|
+
))}
|
|
48
|
+
</Box>
|
|
49
|
+
<Box marginTop={1}>
|
|
50
|
+
<Text color={MUTED}>The open-source coding agent that lives in your terminal.</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
</Box>
|
|
53
|
+
|
|
54
|
+
{/* Stato: provider · model · thinking */}
|
|
55
|
+
<Box justifyContent="center">
|
|
56
|
+
<Text color={MUTED}>
|
|
57
|
+
{provider} · {model} · <Text color="magenta">◇ {currentThinking()}</Text>
|
|
58
|
+
</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
{sessionCount > 0 && (
|
|
61
|
+
<Box justifyContent="center">
|
|
62
|
+
<Text color={MUTED}>
|
|
63
|
+
{sessionCount} previous session{sessionCount !== 1 ? "s" : ""} · <Text color={ACCENT}>--continue</Text> to resume
|
|
64
|
+
</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Tips (blocco, centrato) */}
|
|
69
|
+
<Box flexDirection="column" alignItems="center" marginTop={1}>
|
|
70
|
+
<Box flexDirection="column">
|
|
71
|
+
<Tip cmd="/help" desc="show all commands" />
|
|
72
|
+
<Tip cmd="/build" desc="switch to full-access mode" />
|
|
73
|
+
<Tip cmd="/thinking" desc="set reasoning level" />
|
|
74
|
+
<Tip cmd="/init" desc="generate AGENTS.md" />
|
|
75
|
+
</Box>
|
|
76
|
+
</Box>
|
|
77
|
+
|
|
78
|
+
{/* Made in Italy (discreto) */}
|
|
79
|
+
<Box marginTop={1} justifyContent="center">
|
|
80
|
+
<Text backgroundColor={GREEN}> </Text>
|
|
81
|
+
<Text backgroundColor="white"> </Text>
|
|
82
|
+
<Text backgroundColor={RED}> </Text>
|
|
83
|
+
<Text color={MUTED}> made in Italy</Text>
|
|
84
|
+
</Box>
|
|
85
|
+
</Box>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render } from "ink-testing-library";
|
|
4
|
+
import { ToolStep } from "../ToolStep.tsx";
|
|
5
|
+
import { MarkdownText } from "../MarkdownText.tsx";
|
|
6
|
+
|
|
7
|
+
function frame(el: React.ReactElement): string {
|
|
8
|
+
const { lastFrame, unmount } = render(el);
|
|
9
|
+
const out = lastFrame() ?? "";
|
|
10
|
+
unmount();
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("ToolStep rendering (iter 18)", () => {
|
|
15
|
+
test("inline tool: icon + name + synthetic label, no raw JSON", () => {
|
|
16
|
+
const out = frame(<ToolStep tool={{ toolName: "read", input: { path: "src/y.ts" }, output: "10 lines" }} />);
|
|
17
|
+
expect(out).toContain("→");
|
|
18
|
+
expect(out).toContain("read src/y.ts");
|
|
19
|
+
expect(out).not.toContain("{"); // niente JSON grezzo
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("bash block: command rendered with $ and output", () => {
|
|
23
|
+
const out = frame(<ToolStep tool={{ toolName: "bash", input: { command: "bun test" }, output: "77 pass" }} />);
|
|
24
|
+
expect(out).toContain("$ bun test");
|
|
25
|
+
expect(out).toContain("77 pass");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("edit block: diff lines with +/-", () => {
|
|
29
|
+
const out = frame(
|
|
30
|
+
<ToolStep
|
|
31
|
+
tool={{
|
|
32
|
+
toolName: "edit",
|
|
33
|
+
input: { path: "src/x.ts" },
|
|
34
|
+
output: "ok",
|
|
35
|
+
diff: [
|
|
36
|
+
{ type: "remove", text: "const a=1" },
|
|
37
|
+
{ type: "add", text: "const a=2" },
|
|
38
|
+
],
|
|
39
|
+
}}
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
expect(out).toContain("← edit src/x.ts");
|
|
43
|
+
expect(out).toContain("- const a=1");
|
|
44
|
+
expect(out).toContain("+ const a=2");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("MarkdownText rendering (iter 18)", () => {
|
|
49
|
+
test("strips markdown markers, keeps content", () => {
|
|
50
|
+
const out = frame(<MarkdownText content={"# Titolo\nUn **bold** e `code`.\n- item"} />);
|
|
51
|
+
expect(out).toContain("Titolo");
|
|
52
|
+
expect(out).toContain("bold");
|
|
53
|
+
expect(out).toContain("code");
|
|
54
|
+
expect(out).toContain("• item");
|
|
55
|
+
expect(out).not.toContain("**");
|
|
56
|
+
expect(out).not.toContain("`");
|
|
57
|
+
expect(out).not.toContain("#");
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/tui/theme.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Palette condivisa della TUI. Brand: bianco/nero (niente colori).
|
|
2
|
+
// Il contrasto nasce dal PANEL (sfondo applicato sui <Text>, non sui <Box>:
|
|
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)
|
|
5
|
+
export const USER_BAR = "white"; // barra messaggi utente
|
|
6
|
+
export const ASSISTANT_BAR = "gray"; // barra risposte assistant
|
|
7
|
+
|
|
8
|
+
// Pad a destra fino a w caratteri (per estendere lo sfondo del Text).
|
|
9
|
+
export function padRight(s: string, w: number): string {
|
|
10
|
+
return s.length >= w ? s : s + " ".repeat(w - s.length);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Larghezza utile del pannello dato lo stdout (clamp ragionevole).
|
|
14
|
+
export function panelWidth(columns: number | undefined, max = 96): number {
|
|
15
|
+
return Math.max(24, Math.min((columns ?? 80) - 4, max));
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Wordmark "interference" — generato da cfonts font "tiny" (blocchi pieni, ~48 col).
|
|
2
|
+
// Fedele al wordmark del brand (logo/interference-wordmark.svg). Niente mark: il simbolo
|
|
3
|
+
// reale (due sorgenti d onda) non e riproducibile fedelmente in ASCII.
|
|
4
|
+
export const WORDMARK: string[] = [
|
|
5
|
+
" █ █▄ █ ▀█▀ █▀▀ █▀█ █▀▀ █▀▀ █▀█ █▀▀ █▄ █ █▀▀ █▀▀",
|
|
6
|
+
" █ █ ▀█ █ ██▄ █▀▄ █▀ ██▄ █▀▄ ██▄ █ ▀█ █▄▄ ██▄",
|
|
7
|
+
];
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"types": ["bun"],
|
|
10
|
+
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noUncheckedIndexedAccess": true,
|
|
20
|
+
"noImplicitOverride": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"]
|
|
23
|
+
}
|