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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <p align="center"><strong>The open-source coding agent that lives in your terminal.</strong></p>
4
4
 
5
5
  <p align="center">
6
- TypeScript + Bun · Plan / Build modes · 9 tools · permissions · sessions with undo · 38 skills · subagents · TUI
6
+ TypeScript + Bun · Plan / Build modes · 11 tools · permissions · sessions with undo · extensible skills · subagents · TUI
7
7
  </p>
8
8
 
9
9
  ---
@@ -14,10 +14,10 @@ permissions and a read-only **Plan** mode so nothing happens without your say-so
14
14
 
15
15
  ## Features
16
16
 
17
- - **9 tools**: `read` · `ls` · `glob` · `grep` · `webfetch` · `write` · `edit` · `bash` · `task` (subagent)
17
+ - **11 tools**: `read` · `ls` · `glob` · `grep` · `webfetch` · `write` · `edit` · `bash` · `todowrite` · `question` · `task` (subagent)
18
18
  - **Plan & Build** modes — explore read-only, switch to full access when ready
19
19
  - **Permissioned by design** — allow / ask / deny enforced in code, not in the prompt; dangerous commands auto-blocked (`rm -rf`, `sudo`, `curl | sh`)
20
- - **38 skills** — auto-detected by keyword matching, or invoked explicitly via `/skill-name`; full Agent Skills format support
20
+ - **Extensible skills** — Agent Skills format (SKILL.md); auto-detected by keyword matching, or invoked via `/skill-name`; 3 skills bundled, user-extensible
21
21
  - **Subagents** — delegate complex tasks to isolated agents (`explore` for read-only, `general` for full access)
22
22
  - **Atomic edit** — unique-match string replacement with `replaceAll` support
23
23
  - **Safe bash** — timeout, output truncation, exit code, dangerous-command deny list
@@ -51,13 +51,34 @@ permissions and a read-only **Plan** mode so nothing happens without your say-so
51
51
  ## Quickstart
52
52
 
53
53
  ```bash
54
- bun install
55
- # Set API key in .env (Bun auto-loads it)
56
- echo 'DEEPSEEK_API_KEY=sk-...' > .env
57
- bun run interference
54
+ bun install -g interference-agent
55
+ interference
58
56
  ```
59
57
 
60
- > Requires **Bun 1.3+** (Node ≥22 / React ≥19.2 for the Ink TUI).
58
+ On first run, use `/provider` to add your API keys. They're saved in `~/.interference/auth.json`.
59
+
60
+ interference stores its state in `~/.interference/` — sessions, skills, snapshots, and auth — just like Claude Code uses `~/.claude/`.
61
+
62
+ > Requires **Bun 1.3+**.
63
+
64
+ ## Updating
65
+
66
+ ```bash
67
+ bun install -g interference-agent@latest # or: npm i -g interference-agent@latest
68
+ ```
69
+
70
+ interference checks npm for new versions and shows a discreet notice when one is available; run `/update` from inside the app to upgrade.
71
+
72
+ ## Releasing (maintainers)
73
+
74
+ Releases publish to npm automatically on tag push:
75
+
76
+ ```bash
77
+ npm version minor # runs typecheck+test, bumps, tags
78
+ git push --follow-tags # the `publish` GitHub Action publishes to npm
79
+ ```
80
+
81
+ Requires the `NPM_TOKEN` repo secret. See `CHANGELOG.md`.
61
82
 
62
83
  ## Screenshot
63
84
 
package/package.json CHANGED
@@ -1,23 +1,42 @@
1
1
  {
2
2
  "name": "interference-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "The open-source coding agent that lives in your terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "ricciviero",
8
8
  "repository": "github:ricciviero/interference",
9
9
  "homepage": "https://github.com/ricciviero/interference#readme",
10
- "keywords": ["ai", "coding-agent", "terminal", "cli", "bun", "typescript", "llm", "open-source"],
10
+ "keywords": [
11
+ "ai",
12
+ "coding-agent",
13
+ "terminal",
14
+ "cli",
15
+ "bun",
16
+ "typescript",
17
+ "llm",
18
+ "open-source"
19
+ ],
11
20
  "bin": {
12
21
  "interference": "./src/cli.ts"
13
22
  },
23
+ "files": [
24
+ "src",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "bun": ">=1.3.0"
30
+ },
14
31
  "scripts": {
15
32
  "interference": "bun run src/cli.ts",
16
33
  "start": "bun run src/cli.ts",
17
34
  "dev": "bun run --watch src/cli.ts",
18
35
  "typecheck": "bunx tsc --noEmit",
19
36
  "build": "bun build src/cli.ts --compile --minify --sourcemap --outfile dist/interference",
20
- "test": "bun test ./src"
37
+ "test": "bun test ./src",
38
+ "preversion": "bun run typecheck && bun run test",
39
+ "postversion": "git push && git push --tags"
21
40
  },
22
41
  "dependencies": {
23
42
  "@ai-sdk/anthropic": "^4.0.1",
@@ -0,0 +1,24 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { isNewer, CURRENT_VERSION } from "../version.ts";
3
+ import { dispatch } from "../commands/index.ts";
4
+
5
+ describe("version / update (iter 28)", () => {
6
+ test("isNewer compares semver correctly", () => {
7
+ expect(isNewer("0.2.0", "0.1.0")).toBe(true);
8
+ expect(isNewer("1.0.0", "0.9.9")).toBe(true);
9
+ expect(isNewer("0.1.1", "0.1.0")).toBe(true);
10
+ expect(isNewer("0.1.0", "0.1.0")).toBe(false);
11
+ expect(isNewer("0.1.0", "0.2.0")).toBe(false);
12
+ expect(isNewer("v0.3.0", "0.2.9")).toBe(true); // tollera prefisso v
13
+ });
14
+
15
+ test("CURRENT_VERSION matches package.json", async () => {
16
+ const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json();
17
+ expect(CURRENT_VERSION).toBe(pkg.version);
18
+ });
19
+
20
+ test("/version command returns the version", async () => {
21
+ const out = await dispatch("/version", {});
22
+ expect(out).toBe(`interference v${CURRENT_VERSION}`);
23
+ });
24
+ });
package/src/cli-plain.ts CHANGED
@@ -226,18 +226,18 @@ async function consumeTurn(chunks: AsyncGenerator<Chunk>): Promise<void> {
226
226
  for await (const chunk of chunks) {
227
227
  switch (chunk.type) {
228
228
  case "reasoning":
229
- if (!sawReasoning) { stdout.write(`${DIM} thinking${RESET}\n`); sawReasoning = true; }
229
+ if (!sawReasoning) { stdout.write(`${YELLOW} Thinking${RESET}\n`); sawReasoning = true; }
230
230
  stdout.write(`${DIM}${chunk.text}${RESET}`);
231
231
  break;
232
232
  case "text":
233
233
  if (activeTool) { stdout.write("\n"); activeTool = null; }
234
- if (sawReasoning && !inText) { stdout.write(`\n${DIM}┄${RESET}\n\n`); inText = true; }
234
+ if (sawReasoning && !inText) { stdout.write(`\n\n`); inText = true; }
235
235
  else if (!inText) { inText = true; }
236
236
  stdout.write(chunk.text);
237
237
  break;
238
238
  case "tool-call": {
239
239
  const args = typeof chunk.input === "string" ? chunk.input : JSON.stringify(chunk.input);
240
- if (sawReasoning && !inText) { stdout.write(`\n${DIM}┄${RESET}\n\n`); inText = true; }
240
+ if (sawReasoning && !inText) { stdout.write(`\n\n`); inText = true; }
241
241
  else if (activeTool || !inText) { stdout.write("\n"); }
242
242
  stdout.write(`${DIM}· ${chunk.toolName}${RESET}(${args})`);
243
243
  activeTool = { name: chunk.toolName, args, input: chunk.input };
package/src/cli.ts CHANGED
@@ -11,8 +11,16 @@ import { loadConfig, applyConfig } from "./config-file.ts";
11
11
  import { loadAuth, applyAuthToEnv } from "./auth.ts";
12
12
  import { PROVIDERS } from "./config.ts";
13
13
  import type { Session } from "./session/store.ts";
14
+ import { CURRENT_VERSION } from "./version.ts";
14
15
 
15
16
  async function main(): Promise<void> {
17
+ // --version / -v: stampa la versione ed esce (prima di tutto).
18
+ const cliArgs = Bun.argv.slice(2);
19
+ if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
20
+ stdout.write(`${CURRENT_VERSION}\n`);
21
+ return;
22
+ }
23
+
16
24
  // Titolo + nome-icona della tab del terminale (come Claude Code), solo in TTY
17
25
  // (in pipe/non-TTY le sequenze OSC sporcherebbero l'output).
18
26
  // OSC 1 = icon/tab name, OSC 2 = window title.
@@ -2,6 +2,7 @@ import type { AgentMode, ThinkingLevel } from "../config.ts";
2
2
  import { currentProvider, currentThinking, setThinking, currentModel, setModel } from "../config.ts";
3
3
  import { undo, redo } from "../session/snapshot.ts";
4
4
  import { loadSkillBody, getCachedRegistry, type SkillInfo } from "../skills.ts";
5
+ import { CURRENT_VERSION } from "../version.ts";
5
6
 
6
7
  export interface CommandInfo {
7
8
  name: string;
@@ -74,6 +75,23 @@ export function isSlashCommand(input: string): boolean {
74
75
  return /^\//.test(input);
75
76
  }
76
77
 
78
+ register("version", "Show the interference version", () => {
79
+ return `interference v${CURRENT_VERSION}`;
80
+ });
81
+
82
+ register("update", "Update interference to the latest version", async () => {
83
+ const proc = Bun.spawn(["npm", "i", "-g", "interference-agent@latest"], {
84
+ stdout: "pipe",
85
+ stderr: "pipe",
86
+ });
87
+ const code = await proc.exited;
88
+ if (code === 0) {
89
+ return "Updated to the latest version. Restart interference to use it.";
90
+ }
91
+ const err = (await new Response(proc.stderr).text()).trim().slice(0, 300);
92
+ return `Update failed (exit ${code}).${err ? `\n${err}` : ""}\nTry manually: npm i -g interference-agent@latest`;
93
+ });
94
+
77
95
  register("help", "Show available commands", () => {
78
96
  const lines = ["Available commands:"];
79
97
  for (const cmd of listCommands()) {
package/src/config.ts CHANGED
@@ -70,6 +70,7 @@ export const PROVIDERS: Record<ProviderId, ProviderDef> = {
70
70
  thinkingLevels: ["off", "low", "medium", "high", "max"],
71
71
  defaultThinking: "high",
72
72
  models: [
73
+ { id: "claude-sonnet-5", label: "Claude Sonnet 5 (1M ctx)" },
73
74
  { id: "claude-opus-4-8", label: "Claude Opus 4.8 (1M ctx)" },
74
75
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (1M ctx)" },
75
76
  ],
@@ -90,14 +91,16 @@ export const PROVIDERS: Record<ProviderId, ProviderDef> = {
90
91
  kimi: {
91
92
  label: "Moonshot Kimi",
92
93
  envKey: "KIMI_API_KEY",
93
- defaultModel: "kimi-k2.7",
94
+ defaultModel: "kimi-k2.7-code",
94
95
  kind: "openai-compatible",
95
96
  contextLimit: 1_000_000,
96
97
  baseURL: "https://api.moonshot.ai/v1",
97
98
  thinkingLevels: ["off", "max"],
98
99
  defaultThinking: "max",
99
100
  models: [
100
- { id: "kimi-k2.7", label: "Kimi K2.7 (1M ctx)" },
101
+ { id: "kimi-k2.7-code", label: "Kimi K2.7 Code (thinking always on)" },
102
+ { id: "kimi-k2.6", label: "Kimi K2.6 (thinking opzionale)" },
103
+ { id: "kimi-k2.5", label: "Kimi K2.5" },
101
104
  ],
102
105
  },
103
106
  };
@@ -171,19 +174,13 @@ export interface ReasoningConfig {
171
174
  maxOutputTokens?: number;
172
175
  }
173
176
 
174
- // Anthropic: livello → budget token del thinking (maxOutputTokens deve superarlo).
175
- const ANTHROPIC_BUDGET: Record<Exclude<ThinkingLevel, "off">, number> = {
176
- low: 8_000,
177
- medium: 16_000,
178
- high: 32_000,
179
- max: 60_000,
180
- };
181
-
182
177
  /** Traduce il livello di thinking corrente nelle opzioni del provider attivo. */
183
178
  export function reasoningConfig(): ReasoningConfig {
184
179
  const level = currentThinking();
185
180
 
186
- switch (config.provider) {
181
+ // Provider EFFETTIVO: rispetta l'override runtime (`/provider`, `/model`),
182
+ // non solo `config.provider` (statico da env all'avvio).
183
+ switch (_providerOverride ?? config.provider) {
187
184
  case "deepseek":
188
185
  if (level === "off") {
189
186
  return { providerOptions: { deepseek: { thinking: { type: "disabled" } } } };
@@ -193,11 +190,18 @@ export function reasoningConfig(): ReasoningConfig {
193
190
  };
194
191
 
195
192
  case "anthropic": {
196
- if (level === "off") return {};
197
- const budget = ANTHROPIC_BUDGET[level];
193
+ // Modelli Anthropic moderni (Sonnet 5, Opus 4.8/4.7, Fable 5) NON supportano
194
+ // `thinking.type: "enabled"` (→ HTTP 400): si usa adaptive thinking + effort.
195
+ // `off` → solo effort basso (niente adaptive): il modello resta veloce/economico.
196
+ // (Senza inviare effort, il default lato server è `high` → "off" non spegnerebbe nulla.)
197
+ if (level === "off") {
198
+ return { providerOptions: { anthropic: { effort: "low" } } };
199
+ }
200
+ // display:"summarized" rende visibile il ragionamento (di default è omesso).
198
201
  return {
199
- providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: budget } } },
200
- maxOutputTokens: budget + 8_000,
202
+ providerOptions: {
203
+ anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: level },
204
+ },
201
205
  };
202
206
  }
203
207
 
@@ -205,8 +209,15 @@ export function reasoningConfig(): ReasoningConfig {
205
209
  case "glm":
206
210
  return { extraBody: { thinking: { type: level === "off" ? "disabled" : "enabled" } } };
207
211
 
208
- case "kimi":
209
- if (level === "off") return { extraBody: { thinking: { type: "disabled" } } };
210
- return { extraBody: { thinking: { type: "enabled", keep: "all" } } };
212
+ case "kimi": {
213
+ // Reasoning Moonshot (campo `reasoning_content`, letto nativamente da @ai-sdk/openai-compatible).
214
+ // max_tokens 16000 richiesto: la somma reasoning+content non deve superare max_tokens.
215
+ // I modelli `*-code` hanno il thinking SEMPRE ON e NON accettano il param `thinking`.
216
+ const alwaysOn = currentModel().includes("k2.7-code");
217
+ if (alwaysOn) return { maxOutputTokens: 16_000 };
218
+ if (level === "off")
219
+ return { extraBody: { thinking: { type: "disabled" } }, maxOutputTokens: 16_000 };
220
+ return { extraBody: { thinking: { type: "enabled", keep: "all" } }, maxOutputTokens: 16_000 };
221
+ }
211
222
  }
212
223
  }
package/src/cost.ts CHANGED
@@ -6,6 +6,8 @@ interface Pricing {
6
6
  }
7
7
 
8
8
  const PRICING: Record<string, Pricing> = {
9
+ // Sonnet 5: standard $3/$15 (intro $2/$10 fino al 2026-08-31).
10
+ "claude-sonnet-5": { inputPer1M: 3.0, outputPer1M: 15.0 },
9
11
  "deepseek-v4-pro": { inputPer1M: 2.0, outputPer1M: 8.0 },
10
12
  "deepseek-v4-flash": { inputPer1M: 0.27, outputPer1M: 1.10 },
11
13
  "gpt-5.5": { inputPer1M: 5.0, outputPer1M: 30.0 },
@@ -311,7 +311,7 @@ describe("bash tool", () => {
311
311
  test("timeout kills process", async () => {
312
312
  setRules([{ tool: "bash", decision: "allow" }]);
313
313
  const out = await call<string>(bash, { command: "sleep 5", timeout: 500 });
314
- expect(out).toContain("exit code");
314
+ expect(out).toContain("timed out");
315
315
  });
316
316
 
317
317
  test("ask with confirmation", async () => {
package/src/tools/bash.ts CHANGED
@@ -39,16 +39,35 @@ export const bash = tool({
39
39
  cwd: process.cwd(),
40
40
  stdout: "pipe",
41
41
  stderr: "pipe",
42
- timeout: ms,
43
- killSignal: "SIGTERM",
44
42
  });
45
43
 
46
- const [stdout, stderr] = await Promise.all([
47
- readStream(proc.stdout, OUTPUT_CAP),
48
- readStream(proc.stderr, OUTPUT_CAP),
44
+ // Esecuzione (lettura stream + exit) in gara col timeout. Allo scadere
45
+ // uccidiamo il processo e ritorniamo SUBITO senza aspettare l'EOF: un figlio
46
+ // orfano (es. `sleep`) può tenere la pipe aperta dopo la morte della shell e
47
+ // bloccherebbe la lettura. (L'opzione `timeout` di Bun.spawn non è affidabile
48
+ // su tutti i runner.)
49
+ const finished = (async () => {
50
+ const [stdout, stderr] = await Promise.all([
51
+ readStream(proc.stdout, OUTPUT_CAP),
52
+ readStream(proc.stderr, OUTPUT_CAP),
53
+ ]);
54
+ const exitCode = await proc.exited;
55
+ return { stdout, stderr, exitCode };
56
+ })().catch(() => ({ stdout: "", stderr: "", exitCode: 1 }));
57
+
58
+ const result = await Promise.race([
59
+ finished,
60
+ new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), ms)),
49
61
  ]);
50
62
 
51
- const exitCode = await proc.exited;
63
+ if (result === "timeout") {
64
+ try {
65
+ proc.kill(9);
66
+ } catch {}
67
+ return `Command timed out after ${ms}ms and was killed.`;
68
+ }
69
+
70
+ const { stdout, stderr, exitCode } = result;
52
71
 
53
72
  let output = "";
54
73
  if (stdout.length > 0) output += stdout;
package/src/tools/grep.ts CHANGED
@@ -58,29 +58,35 @@ async function tryRg(
58
58
  args.push("--", pattern);
59
59
  args.push(cwd);
60
60
 
61
- const proc = Bun.spawn(["rg", ...args], {
62
- stdout: "pipe",
63
- stderr: "pipe",
64
- });
65
- const out = await proc.stdout.text();
66
- const err = await proc.stderr.text();
67
- await proc.exited;
68
-
69
- if (proc.exitCode === 0) {
70
- const cwdName = path.relative(process.cwd(), cwd) || ".";
71
- return formatMatches(out, pattern, cwdName);
72
- }
61
+ // ripgrep può non essere installato: `Bun.spawn(["rg"])` lancia ENOENT prima
62
+ // ancora di poter leggere stderr → wrappiamo tutto e torniamo null (→ fallback JS).
63
+ try {
64
+ const proc = Bun.spawn(["rg", ...args], {
65
+ stdout: "pipe",
66
+ stderr: "pipe",
67
+ });
68
+ const out = await proc.stdout.text();
69
+ const err = await proc.stderr.text();
70
+ await proc.exited;
71
+
72
+ if (proc.exitCode === 0) {
73
+ const cwdName = path.relative(process.cwd(), cwd) || ".";
74
+ return formatMatches(out, pattern, cwdName);
75
+ }
73
76
 
74
- if (proc.exitCode === 1) {
75
- const cwdName = path.relative(process.cwd(), cwd) || ".";
76
- return `No matches for '${pattern}' in ${cwdName}`;
77
- }
77
+ if (proc.exitCode === 1) {
78
+ const cwdName = path.relative(process.cwd(), cwd) || ".";
79
+ return `No matches for '${pattern}' in ${cwdName}`;
80
+ }
78
81
 
79
- if (err.includes("command not found") || err.includes("No such file")) {
80
- return null; // rg not available, fallback to JS
81
- }
82
+ if (err.includes("command not found") || err.includes("No such file")) {
83
+ return null; // rg non disponibile fallback JS
84
+ }
82
85
 
83
- return `grep error (exit ${proc.exitCode}): ${err || "unknown error"}`;
86
+ return `grep error (exit ${proc.exitCode}): ${err || "unknown error"}`;
87
+ } catch {
88
+ return null; // rg non installato (ENOENT) → fallback JS
89
+ }
84
90
  }
85
91
 
86
92
  async function jsGrep(
@@ -95,7 +101,7 @@ async function jsGrep(
95
101
  try {
96
102
  regex = new RegExp(pattern, flags);
97
103
  } catch {
98
- return `Invalid regex pattern: ${pattern}`;
104
+ return `grep error: invalid regex pattern '${pattern}'`;
99
105
  }
100
106
 
101
107
  const matches: string[] = [];
package/src/tui/App.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useState, useRef, useEffect, useCallback } from "react";
2
2
  import { Box, Text, Static, useInput, useApp } from "ink";
3
- import { Spinner } from "@inkjs/ui";
3
+ import { Spinner } from "./Spinner.tsx";
4
4
  import TextInput from "ink-text-input";
5
5
  import type { ModelMessage } from "ai";
6
6
  import { runTurn } from "../agent/loop.ts";
@@ -34,7 +34,10 @@ import { QuestionDialog } from "./QuestionDialog.tsx";
34
34
  import { setAnswerHandler, type QuestionSpec, type Answers } from "../tools/question.ts";
35
35
  import { ToolStep } from "./ToolStep.tsx";
36
36
  import { MarkdownText } from "./MarkdownText.tsx";
37
- import { USER_BAR, ASSISTANT_BAR } from "./theme.ts";
37
+ import { reasoningSummary } from "./reasoning.ts";
38
+ import { placeholderFor } from "./placeholders.ts";
39
+ import { checkForUpdate, CURRENT_VERSION } from "../version.ts";
40
+ import { USER_BAR, ASSISTANT_BAR, THINKING, THINKING_BODY } from "./theme.ts";
38
41
  import { Panel } from "./Panel.tsx";
39
42
 
40
43
  type HistoryItem = {
@@ -77,6 +80,8 @@ export default function App({ session }: { session: Session }) {
77
80
  const [gitBranch, setGitBranch] = useState("");
78
81
  const [todos, setTodosState] = useState<Todo[]>(session.todos ?? []);
79
82
  const [questions, setQuestions] = useState<QuestionSpec[] | null>(null);
83
+ const [phIdx, setPhIdx] = useState(0); // indice esempio placeholder (it. 25)
84
+ const [update, setUpdate] = useState<string | null>(null); // versione più recente (it. 28)
80
85
  const { toasts, addToast } = useToast();
81
86
  const confirmResolveRef = useRef<((v: boolean) => void) | null>(null);
82
87
  const answerResolveRef = useRef<((a: Answers) => void) | null>(null);
@@ -86,6 +91,11 @@ export default function App({ session }: { session: Session }) {
86
91
 
87
92
  useEffect(() => { sessionRef.current = session; }, [session]);
88
93
 
94
+ // Controllo aggiornamenti (it. 28): non bloccante, throttled, silenzioso offline.
95
+ useEffect(() => {
96
+ checkForUpdate().then(setUpdate).catch(() => {});
97
+ }, []);
98
+
89
99
  // Todos: ripristina dalla sessione e ri-renderizza ad ogni update del tool.
90
100
  useEffect(() => {
91
101
  setTodos(session.todos ?? []);
@@ -320,6 +330,7 @@ export default function App({ session }: { session: Session }) {
320
330
  setReasoning("");
321
331
  setToolSteps([]);
322
332
  setBusy(false);
333
+ setPhIdx((i) => i + 1); // ruota l'esempio del placeholder
323
334
  aborterRef.current = null;
324
335
 
325
336
  if (messageQueue.length > 0) {
@@ -537,6 +548,7 @@ ${args ? `Additional context: ${args}` : ""}`;
537
548
  provider={currentProvider().label}
538
549
  model={currentModel()}
539
550
  sessionCount={0}
551
+ update={update}
540
552
  />
541
553
  )}
542
554
 
@@ -601,7 +613,7 @@ ${args ? `Additional context: ${args}` : ""}`;
601
613
  if (val !== draft) setAcIdx(0);
602
614
  setDraft(val);
603
615
  }}
604
- placeholder={busy ? `working… (${messageQueue.length} queued)` : "Type a message (/ for commands)"}
616
+ placeholder={busy ? `working… (${messageQueue.length} queued)` : placeholderFor(sessionRef.current.meta.mode, phIdx)}
605
617
  onSubmit={onSubmit}
606
618
  />
607
619
  </Box>
@@ -657,7 +669,9 @@ function RoleBlock({
657
669
  );
658
670
  }
659
671
 
660
- function ReasoningBlock({ text, live }: { text: string; live?: boolean }) {
672
+ // Fase PENSIERO: header ambra "✻ Thinking/Thought · durata" + corpo attenuato.
673
+ // Distinta dall'esecuzione (icone tool, bianco) e dalla risposta (markdown bianco pieno).
674
+ function ReasoningBlock({ text, live, ms }: { text: string; live?: boolean; ms?: number }) {
661
675
  const [elapsed, setElapsed] = useState(0);
662
676
 
663
677
  useEffect(() => {
@@ -667,25 +681,23 @@ function ReasoningBlock({ text, live }: { text: string; live?: boolean }) {
667
681
  return () => clearInterval(timer);
668
682
  }, [live]);
669
683
 
670
- const shown = live
671
- ? text.length > 500 ? "…" + text.slice(-500) : text
672
- : text.length > 600 ? text.slice(0, 600) + " …" : text;
684
+ const title = reasoningSummary(text);
685
+ const titlePart = title ? `: ${title}` : live ? "…" : "";
686
+ const header = live
687
+ ? `✻ Thinking${titlePart}${elapsed > 0 ? ` · ${elapsed}s` : ""}`
688
+ : `✻ Thought${titlePart}${ms ? ` · ${(ms / 1000).toFixed(1)}s` : ""}`;
689
+
690
+ // Live: coda in streaming. History: testo dall'inizio, capato.
691
+ const body = live
692
+ ? text.length > 400 ? "…" + text.slice(-400) : text
693
+ : text.length > 700 ? text.slice(0, 700) + " …" : text;
673
694
 
674
695
  return (
675
- <Box
676
- flexDirection="column"
677
- borderStyle="round"
678
- borderColor="gray"
679
- borderTop={false}
680
- borderRight={false}
681
- borderBottom={false}
682
- paddingLeft={1}
683
- marginBottom={1}
684
- >
685
- <Text dimColor bold>
686
- ┄ thinking{live && elapsed > 0 ? ` ${elapsed}s` : ""}
696
+ <Box flexDirection="column" paddingLeft={3} marginBottom={1}>
697
+ <Text color={THINKING} bold>
698
+ {header}
687
699
  </Text>
688
- <Text dimColor>{shown}</Text>
700
+ <Text color={THINKING_BODY}>{body}</Text>
689
701
  </Box>
690
702
  );
691
703
  }
@@ -702,9 +714,7 @@ function MsgBlock({ item }: { item: HistoryItem }) {
702
714
  }
703
715
  return (
704
716
  <Box flexDirection="column">
705
- {item.reasoning && (
706
- <Text dimColor>┄ thought{item.reasoningMs ? ` · ${secs(item.reasoningMs)}` : ""}</Text>
707
- )}
717
+ {item.reasoning && <ReasoningBlock text={item.reasoning} ms={item.reasoningMs} />}
708
718
  <RoleBlock
709
719
  color={ASSISTANT_BAR}
710
720
  content={item.content}
@@ -1,5 +1,6 @@
1
1
  import { useState, type FC } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
+ import { BG_ELEMENT } from "./theme.ts";
3
4
 
4
5
  interface Props {
5
6
  tool: string;
@@ -33,11 +34,19 @@ export const ConfirmDialog: FC<Props> = ({ tool, preview, onResolve }) => {
33
34
  <Text dimColor>{preview.slice(0, 300)}</Text>
34
35
  </Box>
35
36
  <Box gap={2}>
36
- <Text color={selected === "allow" ? "green" : undefined} bold={selected === "allow"}>
37
- {selected === "allow" ? "▸ Allow" : " Allow"}
37
+ <Text
38
+ backgroundColor={selected === "allow" ? BG_ELEMENT : undefined}
39
+ color={selected === "allow" ? "green" : undefined}
40
+ bold={selected === "allow"}
41
+ >
42
+ {" Allow "}
38
43
  </Text>
39
- <Text color={selected === "deny" ? "red" : undefined} bold={selected === "deny"}>
40
- {selected === "deny" ? "▸ Deny" : " Deny"}
44
+ <Text
45
+ backgroundColor={selected === "deny" ? BG_ELEMENT : undefined}
46
+ color={selected === "deny" ? "red" : undefined}
47
+ bold={selected === "deny"}
48
+ >
49
+ {" Deny "}
41
50
  </Text>
42
51
  <Text dimColor>(←→ arrows, Enter, y/n)</Text>
43
52
  </Box>