agen-vektor 0.3.15 → 0.3.16
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 +133 -81
- package/dist/agent/loop.js +40 -0
- package/dist/agent/skills.js +13 -1
- package/dist/providers/anthropic.js +13 -0
- package/dist/providers/gemini.js +64 -10
- package/dist/providers/ollama.js +62 -10
- package/dist/providers/provider.js +33 -3
- package/dist/tui/app.js +56 -10
- package/install.sh +82 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,90 +1,84 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
█ V E C T O R A G E N T █
|
|
5
|
-
█ █
|
|
6
|
-
████████████████████████████████████████████████
|
|
7
|
-
```
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/banner.svg" alt="VectorHead — will run commands on your behalf to help you build." width="720"/>
|
|
3
|
+
</div>
|
|
8
4
|
|
|
9
5
|
<div align="center">
|
|
10
6
|
|
|
11
7
|
**AI Coding Agent for Linux & Termux** — a terminal-native coding agent that inspects your project, plans, edits code, runs commands and tests, and iterates until the task is done.
|
|
12
8
|
|
|
13
|
-

|
|
14
|
-

|
|
15
|
-
](https://www.npmjs.com/package/agen-vektor)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](https://www.npmjs.com/package/agen-vektor)
|
|
12
|
+
[](LICENSE)
|
|
16
13
|
[](https://github.com/clickmamaheti-prog/vector-agent/actions/workflows/ci.yml)
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
---
|
|
15
|
+
*Zero runtime dependencies · Free tier included · No telemetry*
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
VectorHead ● Ready deepseek-ai/DeepSeek-V4-Flash
|
|
24
|
-
[09:41]
|
|
25
|
-
Perbaiki error authentication pada project ini
|
|
26
|
-
|
|
27
|
-
# Cek Auth ← kuning (heading H1–H6, Freebuff palette)
|
|
28
|
-
VectorHead akan memeriksa file auth, memperbaiki, lalu menjalankan test.
|
|
29
|
-
[09:42]
|
|
30
|
-
$ npm test
|
|
31
|
-
... (2 more lines above)
|
|
32
|
-
3 tests passed
|
|
33
|
-
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
34
|
-
│ │
|
|
35
|
-
│ ▍ Enter a coding task or / for commands │
|
|
36
|
-
│ │
|
|
37
|
-
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
38
|
-
● Ready ● ask
|
|
39
|
-
```
|
|
17
|
+
</div>
|
|
40
18
|
|
|
41
|
-
A
|
|
42
|
-
|
|
43
|
-
BOLD orange `#FF8534` on a `#374151` chip, links blue `#3B82F6`, fenced
|
|
44
|
-
code `// lang` header `#5b647a` on a `#374151` slab, plain prose — quoted
|
|
45
|
-
from [CodebuffAI/freebuff](https://github.com/CodebuffAI/freebuff)
|
|
46
|
-
theme-system.ts + markdown-renderer.tsx), a compact agent-activity strip
|
|
47
|
-
(⚙ tool calls), and a rounded input box with a block cursor — all rendered
|
|
48
|
-
incrementally with no flicker.
|
|
19
|
+
*A terminal UI that stays out of your way — chat, tool activity, and code
|
|
20
|
+
render cleanly right where you work.*
|
|
49
21
|
|
|
50
22
|
## Contents
|
|
51
23
|
|
|
52
|
-
- [Features](#features) · [Installation](#installation) · [Quick start](#quick-start) · [Command line](#command-line) · [TUI commands](#tui-commands)
|
|
53
|
-
- [Provider configuration](#provider-configuration) · [
|
|
54
|
-
- [Testing](#testing) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [License](#license)
|
|
24
|
+
- [Features](#features) · [Free tier](#free-tier-zero-config) · [Installation](#installation) · [Quick start](#quick-start) · [Command line](#command-line) · [TUI commands](#tui-commands)
|
|
25
|
+
- [Provider configuration](#provider-configuration) · [Skills](#skills) · [Agent files](#agent-files) · [Themes](#themes) · [Security model](#security-model) · [Privacy](#privacy)
|
|
26
|
+
- [Architecture](#architecture) · [Development](#development) · [Testing](#testing) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [License](#license)
|
|
55
27
|
|
|
56
28
|
## Features
|
|
57
29
|
|
|
58
30
|
- **Interactive TUI** — chat, agent status, tool execution, diff viewer, input. Keyboard driven, mouse-aware, works on 80×24 terminals and survives resize. Scroll the chat with PgUp/PgDn, the mouse wheel, or ↑/↓ while the input line is empty.
|
|
59
31
|
- **Agent loop** — plan → inspect → tool call → result → review → fix → retry until done (with an iteration cap).
|
|
60
32
|
- **Tools** — read/write/edit files, list directories, search files, run shell commands, git, fetch web pages, and **live web search out of the box** (no key needed — searches relay through the VectorHead gateway; bring your own Exa/Serper/Brave key to use your own quota).
|
|
33
|
+
- **Skills** — 14 bundled, agentskills.io-format skills with progressive disclosure (`list_skills` / `read_skill` tools, `/skills` in the TUI); drop your own into `~/.vector/skills/` or `.agents/skills/`.
|
|
34
|
+
- **Agent files** — SOUL.md / MEMORY.md / USER.md / AGENTS.md auto-seeded in `~/.vector/` and injected into the system prompt (identity, notes, preferences, project rules).
|
|
61
35
|
- **Security first** — command policy classifies every shell command (SAFE / ASK / DANGEROUS / BLOCKED), permission prompts before risky actions, `--yolo` mode with warnings, and API keys are never printed or persisted to sessions.
|
|
62
|
-
- **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint.
|
|
36
|
+
- **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint — unlimited side-by-side providers with per-provider keys, models and headers.
|
|
63
37
|
- **Sessions & memory** — conversations persist to `~/.vector/sessions/`; a relaunch in the same project resumes the conversation automatically (agent keeps the last replies in context), `/continue` resumes any session, and `Ctrl+L`/`/clear` starts fresh.
|
|
64
38
|
- **Context management** — only relevant context is sent to the model, with compaction (`/compact`).
|
|
65
39
|
|
|
40
|
+
## Free tier (zero config)
|
|
41
|
+
|
|
42
|
+
Install and chat — no account, no API key:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g agen-vektor
|
|
46
|
+
vector
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The default provider is **`vectorhead-free`** (model `glm-5.3-flash`, 40 free
|
|
50
|
+
models via `/model`). The shared provider key stays server-side; web search
|
|
51
|
+
works through the same gateway out of the box. Bring your own provider key at
|
|
52
|
+
any time via `/connect`.
|
|
53
|
+
|
|
66
54
|
## Installation
|
|
67
55
|
|
|
68
56
|
Requirements: **Node.js 20+** and **npm** (Termux: `pkg install nodejs`).
|
|
69
57
|
|
|
70
|
-
###
|
|
58
|
+
### One-shot installer (recommended)
|
|
71
59
|
|
|
72
|
-
|
|
60
|
+
No Node.js/npm yet? The installer sets up Node 20+ & npm (apt on Debian/Ubuntu,
|
|
61
|
+
`pkg` on Termux) and then installs VectorHead for you:
|
|
73
62
|
|
|
74
63
|
```bash
|
|
75
|
-
|
|
76
|
-
npm run build
|
|
77
|
-
npm link # makes `vector` available on your PATH
|
|
64
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/clickmamaheti-prog/vector-agent/main/install.sh)
|
|
78
65
|
```
|
|
79
66
|
|
|
67
|
+
### From npm
|
|
68
|
+
|
|
80
69
|
```bash
|
|
70
|
+
npm install -g agen-vektor
|
|
81
71
|
vector
|
|
82
72
|
```
|
|
83
73
|
|
|
84
|
-
### From
|
|
74
|
+
### From source
|
|
85
75
|
|
|
86
76
|
```bash
|
|
87
|
-
|
|
77
|
+
git clone https://github.com/clickmamaheti-prog/vector-agent
|
|
78
|
+
cd vector-agent
|
|
79
|
+
npm install
|
|
80
|
+
npm run build
|
|
81
|
+
npm link # makes `vector` available on your PATH
|
|
88
82
|
vector
|
|
89
83
|
```
|
|
90
84
|
|
|
@@ -127,22 +121,29 @@ vector
|
|
|
127
121
|
| `vector --yolo` | Skip permission prompts for ask-level actions (with warning) |
|
|
128
122
|
| `vector --version` / `--help` | Version / help |
|
|
129
123
|
|
|
124
|
+
Generic environment variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_THEME`, `VECTOR_HOME`, `VECTOR_AUTO_APPROVE` (=1 auto-approves ASK-level permission prompts, for automated runs/tests; dangerous actions still prompt), `VECTOR_ASCII` (=1 forces ASCII safe mode — glyphs map to 1-column ASCII so CJK/Termux fonts never tear the layout; auto-on for zh/ja/ko locales and Termux, `=0` opts out on desktop).
|
|
125
|
+
|
|
130
126
|
## TUI commands
|
|
131
127
|
|
|
132
|
-
Type `/` inside VectorHead:
|
|
128
|
+
Type `/` inside VectorHead — suggestions filter as you type:
|
|
133
129
|
|
|
134
130
|
| Command | Description |
|
|
135
131
|
|---|---|
|
|
136
|
-
| `/
|
|
137
|
-
| `/
|
|
138
|
-
| `/provider` | Select a provider |
|
|
132
|
+
| `/help` | Show help — `/help all` lists every command |
|
|
133
|
+
| `/connect` | Configure provider: pick built-in / add custom API (`/provider`, `/setup` are aliases) |
|
|
139
134
|
| `/provider-remove` | Remove the stored provider key (and custom URL/name) so you can re-enter fresh values |
|
|
135
|
+
| `/model` | Select a model — auto-fetches the live model list from the provider |
|
|
136
|
+
| `/theme` | Select a theme |
|
|
140
137
|
| `/session` | Browse saved sessions |
|
|
138
|
+
| `/session-name` | Set session name (arg optional) |
|
|
141
139
|
| `/settings` | Show configuration |
|
|
142
140
|
| `/status` | Show agent status |
|
|
143
141
|
| `/clear` | Clear the conversation (keeps the session) |
|
|
144
142
|
| `/new` | Start a fresh conversation — rotates to a brand-new session (the old one is kept on disk) |
|
|
143
|
+
| `/reset` | Fresh conversation — old session file deleted from disk |
|
|
145
144
|
| `/continue` | Continue the current/last session |
|
|
145
|
+
| `/soul` `/memory` `/user` `/agents` | Edit agent files (SOUL/MEMORY/USER/AGENTS.md) |
|
|
146
|
+
| `/skills` | List installed skills |
|
|
146
147
|
| `/diff` | Show diff viewer |
|
|
147
148
|
| `/git` | Run `git status` |
|
|
148
149
|
| `/compact` | Compact context (truncate old messages) |
|
|
@@ -160,9 +161,7 @@ export GEMINI_API_KEY=...
|
|
|
160
161
|
export OPENROUTER_API_KEY=...
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
Or inside VectorHead: `/
|
|
164
|
-
|
|
165
|
-
Generic variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_THEME`, `VECTOR_HOME`, `VECTOR_AUTO_APPROVE` (=1 auto-approves ASK-level permission prompts, for automated runs/tests; dangerous actions still prompt).
|
|
164
|
+
Or inside VectorHead: `/connect` → pick provider → paste key (stored masked in `~/.vector/credentials.json`, permissions `0600`).
|
|
166
165
|
|
|
167
166
|
### Custom API (OpenAI-compatible)
|
|
168
167
|
|
|
@@ -174,12 +173,13 @@ vector --provider custom --model deepseek-v3
|
|
|
174
173
|
|
|
175
174
|
The provider speaks `/v1/chat/completions` (and `/models`). Anthropic and Gemini protocols for custom endpoints are documented as adapters; OpenAI-compatible endpoints are fully supported.
|
|
176
175
|
|
|
177
|
-
|
|
176
|
+
<details>
|
|
177
|
+
<summary><b>Multiple custom providers</b></summary>
|
|
178
|
+
|
|
179
|
+
<br>
|
|
178
180
|
|
|
179
181
|
Any number of OpenAI-compatible providers can be defined side by side in
|
|
180
|
-
`~/.vector/config.json` under `providers
|
|
181
|
-
its `provider` section (minus the `npm` field, since every custom endpoint
|
|
182
|
-
here is OpenAI-compatible):
|
|
182
|
+
`~/.vector/config.json` under `providers`:
|
|
183
183
|
|
|
184
184
|
```jsonc
|
|
185
185
|
{
|
|
@@ -190,7 +190,7 @@ here is OpenAI-compatible):
|
|
|
190
190
|
"name": "BitDeer",
|
|
191
191
|
"options": {
|
|
192
192
|
"baseURL": "https://api-inference.bitdeer.ai/v1",
|
|
193
|
-
"apiKey": "{env:BITDEER_API_KEY}" // or a plain key, or omit and store via /
|
|
193
|
+
"apiKey": "{env:BITDEER_API_KEY}" // or a plain key, or omit and store via /connect
|
|
194
194
|
},
|
|
195
195
|
"models": {
|
|
196
196
|
"deepseek-ai/DeepSeek-V4-Flash": { "name": "DeepSeek V4 Flash" }
|
|
@@ -219,33 +219,70 @@ here is OpenAI-compatible):
|
|
|
219
219
|
|
|
220
220
|
- `provider` is the active id — a built-in, the legacy `custom` slot, or any
|
|
221
221
|
key of `providers`. Keys are **per provider id**: store each under its own
|
|
222
|
-
id in `~/.vector/credentials.json` (0600) via `/
|
|
222
|
+
id in `~/.vector/credentials.json` (0600) via `/connect`, inline them in
|
|
223
223
|
`options.apiKey`, or reference an env var with `"{env:NAME}"`.
|
|
224
224
|
- `options.headers` adds extra HTTP headers to every request of that provider
|
|
225
225
|
(e.g. a `User-Agent` required by a client-filtering gateway like
|
|
226
226
|
AgentRouter). Values may be plain strings or `"{env:NAME}"` templates;
|
|
227
227
|
headers referencing a missing env var are dropped.
|
|
228
228
|
- `model` is the **default model** for that provider: when the provider
|
|
229
|
-
becomes active (`/
|
|
229
|
+
becomes active (`/connect`, `--provider`, or `VECTOR_PROVIDER`), the active
|
|
230
230
|
model automatically follows it unless an explicit `--model` / `VECTOR_MODEL`
|
|
231
231
|
was given. Handy when each provider speaks its own model ids (e.g.
|
|
232
232
|
AgentRouter uses `deepseek-v4-flash` while BitDeer uses
|
|
233
233
|
`deepseek-ai/DeepSeek-V4-Flash`).
|
|
234
|
-
- `/
|
|
234
|
+
- `/connect` lists built-ins + every configured custom and offers
|
|
235
235
|
**Add custom provider** (id → name → base URL → key). `/model` then
|
|
236
236
|
auto-discovers models from the endpoint's `/models` route and falls back
|
|
237
237
|
to the `models` map above.
|
|
238
238
|
- The old single-custom setup (`provider: "custom"` + `apiUrl`/`customName`
|
|
239
239
|
+ key under `custom`) keeps working unchanged.
|
|
240
240
|
|
|
241
|
-
Switch at runtime with `/
|
|
241
|
+
Switch at runtime with `/connect`, or per-run with
|
|
242
242
|
`VECTOR_PROVIDER=bitdeer vector …` / `--provider bitdeer`.
|
|
243
243
|
|
|
244
|
+
</details>
|
|
245
|
+
|
|
246
|
+
## Skills
|
|
247
|
+
|
|
248
|
+
VectorHead ships a built-in skill system (agentskills.io format —
|
|
249
|
+
YAML frontmatter + markdown body) with **progressive disclosure**: the agent
|
|
250
|
+
sees a one-line catalog first and loads a skill's full body only when needed
|
|
251
|
+
via the `list_skills` / `read_skill` tools.
|
|
252
|
+
|
|
253
|
+
**14 bundled skills:** `verify-node-project`, `safe-git-workflow`,
|
|
254
|
+
`debug-loop`, `fetch-docs`, `secure-secrets`, `systematic-debugging`,
|
|
255
|
+
`test-driven-development`, `codebase-inspection`, `requesting-code-review`,
|
|
256
|
+
`spike`, `simplify-code`, `grounded-citations`, `blocked-page-recovery`,
|
|
257
|
+
`humanize-writing`.
|
|
258
|
+
|
|
259
|
+
Install your own (multi-root resolution):
|
|
260
|
+
|
|
261
|
+
| Root | Scope |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `~/.vector/skills/<name>/SKILL.md` | personal — always active |
|
|
264
|
+
| `<project>/.agents/skills/<name>/SKILL.md` | project — active when running from that project |
|
|
265
|
+
| `~/.agents/skills/<name>/SKILL.md` | personal (standard `npx skills` layout) |
|
|
266
|
+
|
|
267
|
+
Browse in the TUI with `/skills`.
|
|
268
|
+
|
|
269
|
+
## Agent files
|
|
270
|
+
|
|
271
|
+
Markdown files auto-seeded in `~/.vector/` (never overwritten by upgrades):
|
|
272
|
+
|
|
273
|
+
| File | Purpose |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `SOUL.md` | Agent identity — slot #1 of the system prompt |
|
|
276
|
+
| `MEMORY.md` | Persistent notes the agent keeps across sessions |
|
|
277
|
+
| `USER.md` | What the agent knows about you |
|
|
278
|
+
| `AGENTS.md` | Project rules (also read from the project root, like this repo's own) |
|
|
279
|
+
|
|
280
|
+
Edit via the TUI (`/soul`, `/memory`, `/user`, `/agents`) or any editor. Size is capped per file to keep prompts lean.
|
|
281
|
+
|
|
244
282
|
## Themes
|
|
245
283
|
|
|
246
|
-
VectorHead
|
|
247
|
-
|
|
248
|
-
`~/.vector/themes/` (same files OpenCode uses work verbatim).
|
|
284
|
+
VectorHead has its own theme system: pick a built-in theme, or drop your
|
|
285
|
+
own JSON files into `~/.vector/themes/` — no restart, applies on the fly.
|
|
249
286
|
|
|
250
287
|
```bash
|
|
251
288
|
vector --theme tokyonight # one-off
|
|
@@ -255,10 +292,13 @@ echo '{ "theme": "nord" }' >> ~/.vector/config.json # (merge manually)
|
|
|
255
292
|
# or env: export VECTOR_THEME=matrix
|
|
256
293
|
```
|
|
257
294
|
|
|
258
|
-
Built-ins: `vectorhead` (default
|
|
259
|
-
`nord`, `tokyonight`, `gruvbox`, `catppuccin`, `one-dark`, `matrix`.
|
|
295
|
+
Built-ins: `vectorhead` (default — the signature orange & gold VectorHead
|
|
296
|
+
palette), `nord`, `tokyonight`, `gruvbox`, `catppuccin`, `one-dark`, `matrix`.
|
|
297
|
+
|
|
298
|
+
<details>
|
|
299
|
+
<summary><b>Custom themes</b></summary>
|
|
260
300
|
|
|
261
|
-
|
|
301
|
+
<br>
|
|
262
302
|
|
|
263
303
|
A theme is a JSON file with an optional `defs` block of reusable colors
|
|
264
304
|
plus a semantic `theme` block. Color values may be hex (`#88C0D0`), ANSI
|
|
@@ -284,14 +324,16 @@ dark/light variants (`{ "dark": "#000", "light": "#fff" }`), or
|
|
|
284
324
|
```
|
|
285
325
|
|
|
286
326
|
Save it as `~/.vector/themes/my-theme.json`, then `/theme` (or
|
|
287
|
-
`vector --theme my-theme`) to use it. Semantic keys
|
|
288
|
-
|
|
327
|
+
`vector --theme my-theme`) to use it. Semantic keys and their effect:
|
|
328
|
+
`primary` (brand accent), `accent`/`markdownHeading`
|
|
289
329
|
(headings, prompt), `text`, `textMuted`, `textFaint`, `markdownCode`,
|
|
290
330
|
`border`/`borderActive`, `error`/`warning`/`success`/`info`, `diffAdded`/
|
|
291
331
|
`diffRemoved`/`diffContext`, `backgroundPanel`/`backgroundElement`.
|
|
292
332
|
Unknown keys and unresolvable colors are ignored, so partial themes keep
|
|
293
333
|
the default palette for anything you don't override.
|
|
294
334
|
|
|
335
|
+
</details>
|
|
336
|
+
|
|
295
337
|
> Hex colors are emitted as truecolor (24-bit) escapes — terminals need
|
|
296
338
|
> `COLORTERM=truecolor` for full fidelity (most modern terminals). ANSI
|
|
297
339
|
> palette indexes and `vectorhead` work everywhere.
|
|
@@ -331,11 +373,11 @@ Every shell command is classified:
|
|
|
331
373
|
src/
|
|
332
374
|
├── cli/ argument parsing, keyboard, bootstrap
|
|
333
375
|
├── tui/ terminal UI: chat, input, statusbar, diff, components
|
|
334
|
-
├── agent/ agent loop, planner, context, memory, prompts, session
|
|
376
|
+
├── agent/ agent loop, planner, context, memory, prompts, session, skills
|
|
335
377
|
├── providers/ AIProvider interface + OpenAI/Anthropic/Gemini/OpenRouter/Ollama/Custom
|
|
336
|
-
├── tools/ tool registry + filesystem/shell/search/git/web
|
|
378
|
+
├── tools/ tool registry + filesystem/shell/search/git/web/skills
|
|
337
379
|
├── security/ command policy + permission manager
|
|
338
|
-
├── config/ config, credentials, model catalog
|
|
380
|
+
├── config/ config, credentials, model catalog, free tier
|
|
339
381
|
└── utils/ terminal (ANSI), logger, paths
|
|
340
382
|
```
|
|
341
383
|
|
|
@@ -361,19 +403,29 @@ npm run build
|
|
|
361
403
|
|
|
362
404
|
## Troubleshooting
|
|
363
405
|
|
|
364
|
-
- **"No API key configured"** — set the relevant `*_API_KEY` env var or run `/
|
|
406
|
+
- **"No API key configured"** — set the relevant `*_API_KEY` env var or run `/connect` in the TUI. On the free tier this never appears — a default free provider is pre-configured.
|
|
365
407
|
- **"401" / auth failed** — check the key, or for OpenRouter use `OPENROUTER_API_KEY`.
|
|
366
|
-
- **
|
|
408
|
+
- **"model not found" on the free tier** — the free catalog rotates; run `/model` to pick from the live list.
|
|
409
|
+
- **"Rate limited (429) / cooling down"** — VectorHead retries automatically with backoff, and the free gateway cools down briefly instead of burning the shared key. Switch to another free model via `/model` and retry.
|
|
410
|
+
- **Nothing happens on `vector` in Termux** — make sure your terminal is a TTY and TERM is set (e.g. `export TERM=xterm-256color`). If glyphs look broken (█ everywhere, torn logo), ASCII safe mode is already default-on; desktop users can opt back in with `VECTOR_ASCII=0`.
|
|
367
411
|
- **Runs out of iterations** — raise `maxIterations` in `~/.vector/config.json` or refine the request.
|
|
368
|
-
- **Rate limited (429)** — VectorHead retries automatically with backoff; wait and retry.
|
|
369
412
|
|
|
370
413
|
## Roadmap
|
|
371
414
|
|
|
372
|
-
- Web search tool (provider-backed) behind the `web` abstraction
|
|
373
415
|
- Interactive diff approval before edits in safe mode
|
|
374
416
|
- Persistent permission rules per project
|
|
375
417
|
- Windows support (WSL-focused)
|
|
376
418
|
|
|
419
|
+
## Changelog
|
|
420
|
+
|
|
421
|
+
### 0.3.15
|
|
422
|
+
- **Thinking cards are segmented per completed agent step.** Reasoning for each step (think → tool → think → tool) now renders as its own `• Thinking` card instead of stacking every step's reasoning into one ever-growing card — stale reasoning no longer appears to "leak" below the live thinking preview during multi-step tasks like web research.
|
|
423
|
+
- Streaming answers keep the guarded `mergeStreamDelta` accumulation across all providers (no more doubled final text on gateways that resend the full text).
|
|
424
|
+
|
|
425
|
+
### 0.3.14
|
|
426
|
+
- Removed the anonymous `device-id` / `X-Vector-Device` tracking pair entirely — no device identity is generated or sent.
|
|
427
|
+
- Emoji-aware character widths: `/connect` picker borders align on CJK/emoji terminals.
|
|
428
|
+
|
|
377
429
|
## License
|
|
378
430
|
|
|
379
431
|
[MIT](LICENSE)
|
package/dist/agent/loop.js
CHANGED
|
@@ -288,6 +288,46 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
288
288
|
// request history stay well-formed even if a provider regresses.
|
|
289
289
|
result.toolCalls = result.toolCalls.filter((tc) => !!tc && typeof tc.name === 'string' && tc.name !== '');
|
|
290
290
|
const hasToolCalls = result.toolCalls.length > 0;
|
|
291
|
+
// TOOL PENUTUP (aturan prompt Freebuff-style): suggest_followups,
|
|
292
|
+
// task_completed, render_ui — dipanggil BERSAMA teks jawaban final dalam
|
|
293
|
+
// respons yang sama. Eksekusinya di-sideload DI SINI — tanpa round-trip
|
|
294
|
+
// LLM lagi: dulu tool ini diperlakukan seperti tool biasa → loop memanggil
|
|
295
|
+
// model SEKALI LAGI sekadar menutup giliran (1 panggilan LLM terbuang per
|
|
296
|
+
// giliran) dan TUI menghidupkan kembali status "Thinking" + baris
|
|
297
|
+
// "Working…" DI BAWAH jawaban final yang sudah tampil (laporan user
|
|
298
|
+
// 2026-09-09: "sudah selesai tugas masih working"). Callback
|
|
299
|
+
// onToolCall/onToolResult TETAP ditembak agar kartu followups / ✓ Task
|
|
300
|
+
// Complete / tombol render_ui tetap ter-render; hanya status yang hilang.
|
|
301
|
+
const CLOSING_TOOLS = new Set(['suggest_followups', 'task_completed', 'render_ui']);
|
|
302
|
+
const closerCalls = hasToolCalls
|
|
303
|
+
? result.toolCalls.filter((tc) => CLOSING_TOOLS.has(tc.name))
|
|
304
|
+
: [];
|
|
305
|
+
if (result.content.trim() && closerCalls.length > 0 && closerCalls.length === result.toolCalls.length) {
|
|
306
|
+
// History tetap well-formed: balasan + toolCalls + hasil tool-nya.
|
|
307
|
+
messages.push({ role: 'assistant', content: result.content, toolCalls: result.toolCalls });
|
|
308
|
+
for (const tc of closerCalls) {
|
|
309
|
+
callbacks.onToolCall?.(tc.name, tc.arguments, tc.id);
|
|
310
|
+
let toolResult;
|
|
311
|
+
try {
|
|
312
|
+
toolResult = await tools.execute(tc.name, tc.arguments, {
|
|
313
|
+
cwd,
|
|
314
|
+
permissions,
|
|
315
|
+
onActivity: callbacks.onActivity,
|
|
316
|
+
maxOutput: 30_000,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
toolResult = { output: `ERROR: ${err.message}`, summary: 'tool error' };
|
|
321
|
+
}
|
|
322
|
+
callbacks.onToolResult?.(tc.name, toolResult.output, toolResult.data, tc.id);
|
|
323
|
+
messages.push({ role: 'tool', toolCallId: tc.id, name: tc.name, content: toolResult.output.slice(0, 40_000) });
|
|
324
|
+
}
|
|
325
|
+
finalContent = result.content;
|
|
326
|
+
callbacks.onStatus?.('Completed');
|
|
327
|
+
callbacks.onFinal?.({ ...result, reasoning: lastReasoning || result.reasoning });
|
|
328
|
+
lastReasoning = '';
|
|
329
|
+
return { content: finalContent, iterations, toolCalls: toolCalls + closerCalls.length, stopped, aborted, messages };
|
|
330
|
+
}
|
|
291
331
|
// Streaming-free path: collect the assistant message and execute tools.
|
|
292
332
|
messages.push({
|
|
293
333
|
role: 'assistant',
|
package/dist/agent/skills.js
CHANGED
|
@@ -80,8 +80,20 @@ function parseFrontmatter(raw) {
|
|
|
80
80
|
const yaml = m[1];
|
|
81
81
|
const body = m[2].trim();
|
|
82
82
|
const meta = { name: '', description: '' };
|
|
83
|
+
// Multi-line YAML values (folded `>` / literal `|`, e.g. community skills
|
|
84
|
+
// from `npx skills add` use `description: >` with indented continuation
|
|
85
|
+
// lines). Re-join each block into one logical `key: value` line so the
|
|
86
|
+
// simple line-based parser below keeps working unchanged.
|
|
87
|
+
const yamlFlat = yaml.replace(/^([a-zA-Z_-]+):[ \t]*([>|][+-]?\d*)[ \t]*\n((?:[ \t]+.*(?:\n|$))*)/gm, (_all, key, _ind, cont) => {
|
|
88
|
+
const joined = cont
|
|
89
|
+
.split('\n')
|
|
90
|
+
.map((l) => l.trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(' ');
|
|
93
|
+
return `${key}: ${joined}\n`;
|
|
94
|
+
});
|
|
83
95
|
const simple = /^([a-zA-Z_-]+):\s*(.+?)\s*$/gm;
|
|
84
|
-
for (const match of
|
|
96
|
+
for (const match of yamlFlat.matchAll(simple)) {
|
|
85
97
|
const key = match[1];
|
|
86
98
|
const value = match[2].replace(/^['"]|['"]$/g, '');
|
|
87
99
|
if (key === 'name')
|
|
@@ -288,6 +288,19 @@ class AnthropicProvider {
|
|
|
288
288
|
},
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
// Distinguish a USER interrupt (outer signal aborted → rethrow as-is;
|
|
293
|
+
// the loop marks the run 'Stopped') from OUR 180s deadline. The latter
|
|
294
|
+
// surfaced as a bare AbortError that withRetry refuses to retry — one
|
|
295
|
+
// slow response killed the whole run mid-task ("macet"). Same fix as
|
|
296
|
+
// openai-compat.ts: our deadline = retryable transport failure.
|
|
297
|
+
if (params.signal?.aborted)
|
|
298
|
+
throw err;
|
|
299
|
+
if (err.name === 'AbortError') {
|
|
300
|
+
throw new provider_1.ProviderError('Anthropic stream aborted by timeout (180s).', 0, true);
|
|
301
|
+
}
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
291
304
|
finally {
|
|
292
305
|
clearTimeout(timer);
|
|
293
306
|
if (params.signal)
|
package/dist/providers/gemini.js
CHANGED
|
@@ -130,26 +130,45 @@ class GeminiProvider {
|
|
|
130
130
|
const alt = stream ? '&alt=sse' : '';
|
|
131
131
|
return `${BASE}/models/${params.model}:${action}?key=${encodeURIComponent(key)}${alt}`;
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
post(params, stream, signal) {
|
|
134
134
|
const controller = new AbortController();
|
|
135
135
|
const timer = setTimeout(() => controller.abort(), 180_000);
|
|
136
136
|
const onOuterAbort = () => controller.abort();
|
|
137
|
+
if (signal) {
|
|
138
|
+
if (signal.aborted)
|
|
139
|
+
controller.abort();
|
|
140
|
+
else
|
|
141
|
+
signal.addEventListener('abort', onOuterAbort);
|
|
142
|
+
}
|
|
137
143
|
if (params.signal) {
|
|
138
144
|
if (params.signal.aborted)
|
|
139
145
|
controller.abort();
|
|
140
146
|
else
|
|
141
147
|
params.signal.addEventListener('abort', onOuterAbort);
|
|
142
148
|
}
|
|
143
|
-
const
|
|
149
|
+
const finish = (res) => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
if (signal)
|
|
152
|
+
signal.removeEventListener('abort', onOuterAbort);
|
|
153
|
+
if (params.signal)
|
|
154
|
+
params.signal.removeEventListener('abort', onOuterAbort);
|
|
155
|
+
return { res };
|
|
156
|
+
};
|
|
157
|
+
return fetch(this.url(params, stream), {
|
|
144
158
|
method: 'POST',
|
|
145
159
|
headers: { 'Content-Type': 'application/json' },
|
|
146
160
|
body: JSON.stringify(this.buildBody(params, stream)),
|
|
147
161
|
signal: controller.signal,
|
|
162
|
+
})
|
|
163
|
+
.then(finish)
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
if (signal)
|
|
167
|
+
signal.removeEventListener('abort', onOuterAbort);
|
|
168
|
+
if (params.signal)
|
|
169
|
+
params.signal.removeEventListener('abort', onOuterAbort);
|
|
170
|
+
throw err;
|
|
148
171
|
});
|
|
149
|
-
clearTimeout(timer);
|
|
150
|
-
if (params.signal)
|
|
151
|
-
params.signal.removeEventListener('abort', onOuterAbort);
|
|
152
|
-
return { res };
|
|
153
172
|
}
|
|
154
173
|
async chat(params) {
|
|
155
174
|
const { res } = await this.post(params, false);
|
|
@@ -168,11 +187,46 @@ class GeminiProvider {
|
|
|
168
187
|
};
|
|
169
188
|
}
|
|
170
189
|
async *stream(params) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
// Own abort wiring for the BODY phase: post() clears its 180s timer as
|
|
191
|
+
// soon as headers arrive, so a stream that connects and then STALLS
|
|
192
|
+
// forever had no deadline at all (spinner frozen). Body-phase aborts
|
|
193
|
+
// surface as a bare AbortError that withRetry refuses to retry — one
|
|
194
|
+
// stall killed the run mid-task ("macet"). Our deadline = retryable
|
|
195
|
+
// transport failure; a USER interrupt rethrows untouched.
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const timer = setTimeout(() => controller.abort(), 180_000);
|
|
198
|
+
const onOuterAbort = () => controller.abort();
|
|
199
|
+
if (params.signal) {
|
|
200
|
+
if (params.signal.aborted)
|
|
201
|
+
controller.abort();
|
|
202
|
+
else
|
|
203
|
+
params.signal.addEventListener('abort', onOuterAbort);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const posted = await this.post(params, true, controller.signal);
|
|
207
|
+
const res = posted.res;
|
|
208
|
+
if (!res.ok || !res.body) {
|
|
209
|
+
const text = await res.text().catch(() => '');
|
|
210
|
+
throw new provider_1.ProviderError(`Gemini HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
|
|
211
|
+
}
|
|
212
|
+
yield* this.consumeStream(res, params);
|
|
175
213
|
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (params.signal?.aborted)
|
|
216
|
+
throw err;
|
|
217
|
+
if (err.name === 'AbortError') {
|
|
218
|
+
throw new provider_1.ProviderError('Gemini stream stalled or aborted by timeout.', 0, true);
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
if (params.signal)
|
|
225
|
+
params.signal.removeEventListener('abort', onOuterAbort);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Consume the SSE body of an established streaming response. */
|
|
229
|
+
async *consumeStream(res, params) {
|
|
176
230
|
const reader = res.body.getReader();
|
|
177
231
|
const decoder = new TextDecoder();
|
|
178
232
|
let buffer = '';
|
package/dist/providers/ollama.js
CHANGED
|
@@ -50,26 +50,45 @@ class OllamaProvider {
|
|
|
50
50
|
capabilities() {
|
|
51
51
|
return { streaming: true, tools: true, contextWindow: 128000 };
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
post(body, params, signal) {
|
|
54
54
|
const controller = new AbortController();
|
|
55
55
|
const timer = setTimeout(() => controller.abort(), 180_000);
|
|
56
56
|
const onOuterAbort = () => controller.abort();
|
|
57
|
+
if (signal) {
|
|
58
|
+
if (signal.aborted)
|
|
59
|
+
controller.abort();
|
|
60
|
+
else
|
|
61
|
+
signal.addEventListener('abort', onOuterAbort);
|
|
62
|
+
}
|
|
57
63
|
if (params.signal) {
|
|
58
64
|
if (params.signal.aborted)
|
|
59
65
|
controller.abort();
|
|
60
66
|
else
|
|
61
67
|
params.signal.addEventListener('abort', onOuterAbort);
|
|
62
68
|
}
|
|
63
|
-
const
|
|
69
|
+
const finish = (res) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
if (signal)
|
|
72
|
+
signal.removeEventListener('abort', onOuterAbort);
|
|
73
|
+
if (params.signal)
|
|
74
|
+
params.signal.removeEventListener('abort', onOuterAbort);
|
|
75
|
+
return { res };
|
|
76
|
+
};
|
|
77
|
+
return fetch(`${baseUrl()}/api/chat`, {
|
|
64
78
|
method: 'POST',
|
|
65
79
|
headers: { 'Content-Type': 'application/json' },
|
|
66
80
|
body: JSON.stringify(body),
|
|
67
81
|
signal: controller.signal,
|
|
82
|
+
})
|
|
83
|
+
.then(finish)
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
if (signal)
|
|
87
|
+
signal.removeEventListener('abort', onOuterAbort);
|
|
88
|
+
if (params.signal)
|
|
89
|
+
params.signal.removeEventListener('abort', onOuterAbort);
|
|
90
|
+
throw err;
|
|
68
91
|
});
|
|
69
|
-
clearTimeout(timer);
|
|
70
|
-
if (params.signal)
|
|
71
|
-
params.signal.removeEventListener('abort', onOuterAbort);
|
|
72
|
-
return { res };
|
|
73
92
|
}
|
|
74
93
|
async chat(params) {
|
|
75
94
|
const body = {
|
|
@@ -113,11 +132,44 @@ class OllamaProvider {
|
|
|
113
132
|
};
|
|
114
133
|
if (params.tools && params.tools.length > 0)
|
|
115
134
|
body.tools = toToolDefs(params.tools);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
// Own abort wiring for the BODY phase: post() clears its 180s timer when
|
|
136
|
+
// headers arrive, so a stream that connects then STALLS forever had no
|
|
137
|
+
// deadline. Same fix as openai-compat: our deadline = retryable
|
|
138
|
+
// transport failure; a USER interrupt rethrows untouched.
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), 180_000);
|
|
141
|
+
const onOuterAbort = () => controller.abort();
|
|
142
|
+
if (params.signal) {
|
|
143
|
+
if (params.signal.aborted)
|
|
144
|
+
controller.abort();
|
|
145
|
+
else
|
|
146
|
+
params.signal.addEventListener('abort', onOuterAbort);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const posted = await this.post(body, params, controller.signal);
|
|
150
|
+
const res = posted.res;
|
|
151
|
+
if (!res.ok || !res.body) {
|
|
152
|
+
const text = await res.text().catch(() => '');
|
|
153
|
+
throw new provider_1.ProviderError(`Ollama HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
|
|
154
|
+
}
|
|
155
|
+
yield* this.consumeStream(res, params);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (params.signal?.aborted)
|
|
159
|
+
throw err;
|
|
160
|
+
if (err.name === 'AbortError') {
|
|
161
|
+
throw new provider_1.ProviderError('Ollama stream stalled or aborted by timeout.', 0, true);
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
120
164
|
}
|
|
165
|
+
finally {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
if (params.signal)
|
|
168
|
+
params.signal.removeEventListener('abort', onOuterAbort);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Consume the NDJSON body of an established streaming response. */
|
|
172
|
+
async *consumeStream(res, params) {
|
|
121
173
|
const reader = res.body.getReader();
|
|
122
174
|
const decoder = new TextDecoder();
|
|
123
175
|
let buffer = '';
|
|
@@ -20,8 +20,19 @@ exports.withRetry = withRetry;
|
|
|
20
20
|
function mergeStreamDelta(existing, delta) {
|
|
21
21
|
if (!delta)
|
|
22
22
|
return existing;
|
|
23
|
-
if (existing.endsWith(delta))
|
|
24
|
-
|
|
23
|
+
if (existing.endsWith(delta)) {
|
|
24
|
+
// Repeat of the trailing chunk → treat as a resend and skip. KNOWN
|
|
25
|
+
// LIMITATION (deliberate, 2026-09-09 audit): a GENUINE next chunk that
|
|
26
|
+
// merely equals the tail (a model repeating a word across a chunk edge)
|
|
27
|
+
// is swallowed too — one repeated token lost. Distinguishing the two
|
|
28
|
+
// without per-connection framing is impossible: a tail resend of a full
|
|
29
|
+
// word ("Hello world" + "world") and a genuine repeat are
|
|
30
|
+
// byte-identical, and any seam heuristic that appends the latter also
|
|
31
|
+
// DOUBLES the former (the exact "dobel" bug this guard exists to
|
|
32
|
+
// prevent, 2026-09-08). Losing one token degrades gracefully; a doubled
|
|
33
|
+
// answer does not. Do not "fix" this without real stream framing.
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
25
36
|
if (existing.startsWith(delta))
|
|
26
37
|
return existing; // earlier chunk re-emitted
|
|
27
38
|
if (delta.startsWith(existing))
|
|
@@ -75,8 +86,19 @@ function friendlyApiError(err) {
|
|
|
75
86
|
503: 'provider unavailable',
|
|
76
87
|
504: 'timeout',
|
|
77
88
|
};
|
|
78
|
-
if (status !== undefined && labelByStatus[status])
|
|
89
|
+
if (status !== undefined && labelByStatus[status]) {
|
|
90
|
+
// 400 is the ONE status whose body is usually the most actionable
|
|
91
|
+
// detail (strict-gateway protocol errors: "tool_calls without a
|
|
92
|
+
// response", "unknown tool_call_id", context too long). A generic
|
|
93
|
+
// "bad request" label hides exactly what the user needs to fix
|
|
94
|
+
// (agent dying mid-task, "macet", diagnostics). ProviderError already
|
|
95
|
+
// flattened newlines to one line — show the (capped) body instead.
|
|
96
|
+
if (status === 400 && raw && !labelByStatus[status].includes(raw.slice(0, 20))) {
|
|
97
|
+
const text = raw.length > 64 ? `${raw.slice(0, 63)}…` : raw;
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
79
100
|
return labelByStatus[status];
|
|
101
|
+
}
|
|
80
102
|
// Cut at the first `{` — everything from there on is a body dump.
|
|
81
103
|
const noJson = raw.split('{')[0].trim();
|
|
82
104
|
// Drop redundant `HTTP <code>:` / `<Name> HTTP <code>:` prefixes — the
|
|
@@ -97,6 +119,14 @@ onRetry, } = {}) {
|
|
|
97
119
|
return await fn();
|
|
98
120
|
}
|
|
99
121
|
catch (err) {
|
|
122
|
+
// A user interrupt must never be retried — regardless of the error
|
|
123
|
+
// SHAPE it arrives in. Providers signal abort either as a DOM
|
|
124
|
+
// AbortError (fetch) or as a plain Error('aborted') (the loop's
|
|
125
|
+
// checkAbort convention); both were previously retryable here via the
|
|
126
|
+
// default-true branch, so Esc during a stall waited 1s/2s/4s before
|
|
127
|
+
// the abort was noticed ("Stopped" felt dead).
|
|
128
|
+
if (signal?.aborted)
|
|
129
|
+
throw err;
|
|
100
130
|
attempt++;
|
|
101
131
|
const retryable = err instanceof ProviderError
|
|
102
132
|
? err.retryable
|
package/dist/tui/app.js
CHANGED
|
@@ -166,6 +166,17 @@ class App {
|
|
|
166
166
|
* turn's card (dulu: reanchor menarik LAGI batch lama ke dasar transkrip
|
|
167
167
|
* setiap giliran, jadi kartu followup lama "menempel" di bawah selamanya). */
|
|
168
168
|
runFollowupMsgs = [];
|
|
169
|
+
/** Tool messages created during the CURRENT run — the boxed live panel
|
|
170
|
+
* above the input renders ONLY these. The old panel scanned the WHOLE
|
|
171
|
+
* transcript backwards with no run boundary, so the moment a new run
|
|
172
|
+
* started (running=true, model still THINKING, no new tool yet) the
|
|
173
|
+
* PREVIOUS run's shell/web-search cards re-appeared above the input —
|
|
174
|
+
* "web-search/shell muncul di area thinking yang bukan tempatnya"
|
|
175
|
+
* (laporan user 2026-09-08, live di VPS: panel penuh `$ Shell find ~ …`
|
|
176
|
+
* dari run lama selagi run baru baru saja berpikir). Cleared per run
|
|
177
|
+
* seperti runFollowupMsgs; kartu tetap tinggal di transkrip sebagai
|
|
178
|
+
* riwayat. */
|
|
179
|
+
runToolMsgs = [];
|
|
169
180
|
/** Message OBJECT of the last tool line (indexes shift when showWorking
|
|
170
181
|
* splices the previous working line out — see onToolCall). */
|
|
171
182
|
lastToolMsg = null;
|
|
@@ -701,6 +712,7 @@ class App {
|
|
|
701
712
|
this.replyBubble = null;
|
|
702
713
|
this.wasAbortedByUser = false;
|
|
703
714
|
this.thinkingMsg = null;
|
|
715
|
+
this.runToolMsgs = [];
|
|
704
716
|
this.todoMsg = null;
|
|
705
717
|
this.todoSteps = [];
|
|
706
718
|
this.todoDone = 0;
|
|
@@ -1311,6 +1323,7 @@ class App {
|
|
|
1311
1323
|
this.replyHandled = false;
|
|
1312
1324
|
this.lastToolMsg = null;
|
|
1313
1325
|
this.thinkingMsg = null;
|
|
1326
|
+
this.runToolMsgs = [];
|
|
1314
1327
|
// Model-driven todos: the plan's steps become a ☐/☑ checklist right
|
|
1315
1328
|
// under the user's message, ticking off as tools complete.
|
|
1316
1329
|
if (planSteps && planSteps.length > 0)
|
|
@@ -1404,6 +1417,13 @@ class App {
|
|
|
1404
1417
|
this.markDirty();
|
|
1405
1418
|
this.status = status;
|
|
1406
1419
|
this.statusColor = (0, theme_1.statusColor)(status);
|
|
1420
|
+
// Tool PENUTUP (suggest_followups/task_completed/render_ui): setelah
|
|
1421
|
+
// jawaban final tampil, status "Running …" TIDAK boleh menghidupkan
|
|
1422
|
+
// kembali baris "Working…" di bawahnya (bug "sudah selesai masih
|
|
1423
|
+
// working", laporan 2026-09-09). Loop kini men-sideload tool-tool ini
|
|
1424
|
+
// tanpa status, tapi pagar tetap terpasang utk provider lama/edge case.
|
|
1425
|
+
if (/^Running (suggest_followups|task_completed|render_ui)/.test(status))
|
|
1426
|
+
return;
|
|
1407
1427
|
// Just a clean "Working" line — the exact activity detail is shown
|
|
1408
1428
|
// by the per-tool lines (tool rows) and the bottom status bar, so the
|
|
1409
1429
|
// working line never reads "Working — Thinking…".
|
|
@@ -1468,6 +1488,7 @@ class App {
|
|
|
1468
1488
|
shell: { command, isRunning: true },
|
|
1469
1489
|
};
|
|
1470
1490
|
this.messages.push(msg);
|
|
1491
|
+
this.runToolMsgs.push(msg);
|
|
1471
1492
|
this.lastToolMsg = msg;
|
|
1472
1493
|
this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)}`);
|
|
1473
1494
|
this.scrollToLatest();
|
|
@@ -1507,12 +1528,19 @@ class App {
|
|
|
1507
1528
|
},
|
|
1508
1529
|
};
|
|
1509
1530
|
this.messages.push(msg);
|
|
1531
|
+
this.runToolMsgs.push(msg);
|
|
1510
1532
|
this.lastToolMsg = msg;
|
|
1511
1533
|
this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)}`);
|
|
1512
1534
|
this.scrollToLatest();
|
|
1513
1535
|
return;
|
|
1514
1536
|
}
|
|
1515
1537
|
}
|
|
1538
|
+
// Tool PENUTUP (datang bersama jawaban final): TIDAK boleh membuat
|
|
1539
|
+
// kartu tool + baris "Working…" baru di bawah jawaban — kartu
|
|
1540
|
+
// followups / ✓ Task Complete / tombol UI-nya sendiri dirender oleh
|
|
1541
|
+
// onToolResult (bug "selesai tapi masih working", laporan 2026-09-09).
|
|
1542
|
+
if (tool === 'suggest_followups' || tool === 'task_completed' || tool === 'render_ui')
|
|
1543
|
+
return;
|
|
1516
1544
|
let preview = '';
|
|
1517
1545
|
try {
|
|
1518
1546
|
const parsed = JSON.parse(args);
|
|
@@ -1535,6 +1563,7 @@ class App {
|
|
|
1535
1563
|
if (id)
|
|
1536
1564
|
msg.toolCallId = id;
|
|
1537
1565
|
this.messages.push(msg);
|
|
1566
|
+
this.runToolMsgs.push(msg);
|
|
1538
1567
|
this.lastToolMsg = msg;
|
|
1539
1568
|
this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)}`);
|
|
1540
1569
|
this.scrollToLatest();
|
|
@@ -1607,7 +1636,17 @@ class App {
|
|
|
1607
1636
|
this.markDirty();
|
|
1608
1637
|
return;
|
|
1609
1638
|
}
|
|
1610
|
-
|
|
1639
|
+
// Tool PENUTUP (suggest_followups/task_completed/render_ui): output
|
|
1640
|
+
// teksnya TIDAK boleh menempel ke kartu tool LAMA — tool penutup
|
|
1641
|
+
// tidak pernah membuat kartu sendiri (onToolCall early-return)
|
|
1642
|
+
// sehingga `owner` masih menunjuk kartu tool terakhir run biasa;
|
|
1643
|
+
// dulu `[task_completed] …` / `rendered button …` menempel DI BAWAH
|
|
1644
|
+
// OUTPUT KARTU LAMA itu (regresi fix 2026-09-09, kelas bug sama dgn
|
|
1645
|
+
// "web-shell muncul di tempat yang bukan tempatnya", fix eea7151).
|
|
1646
|
+
// Blok render kartu penutup sendiri (group followups / tombol UI)
|
|
1647
|
+
// ada DI BAWAH dan TETAP jalan — hanya preview yang di-skip.
|
|
1648
|
+
const isClosingTool = tool === 'suggest_followups' || tool === 'task_completed' || tool === 'render_ui';
|
|
1649
|
+
if (this.lastToolMsg?.kind === 'tool' && !isClosingTool) {
|
|
1611
1650
|
const lines = result
|
|
1612
1651
|
.split('\n')
|
|
1613
1652
|
.filter((l) => l.trim())
|
|
@@ -1677,6 +1716,11 @@ class App {
|
|
|
1677
1716
|
this.activeUiButton = w;
|
|
1678
1717
|
this.messages.push({ kind: 'ui', content: w.text, ui: w, ts: Date.now() });
|
|
1679
1718
|
}
|
|
1719
|
+
// Tool penutup: kartunya (group followups / tombol UI) sudah
|
|
1720
|
+
// dirender di atas — jangan hidupkan kembali baris "Working… ✓"
|
|
1721
|
+
// setelah jawaban final, dan jangan tick checklist todo plan.
|
|
1722
|
+
if (isClosingTool)
|
|
1723
|
+
return;
|
|
1680
1724
|
// A finished tool = one more plan step done: tick the next todo.
|
|
1681
1725
|
this.bumpTodo();
|
|
1682
1726
|
this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)} ✓`);
|
|
@@ -1825,6 +1869,7 @@ class App {
|
|
|
1825
1869
|
this.wasAbortedByUser = false;
|
|
1826
1870
|
this.running = false;
|
|
1827
1871
|
this.runStart = null;
|
|
1872
|
+
this.runToolMsgs = [];
|
|
1828
1873
|
// Re-snap to the REAL bottom: the transient Working bubble made the
|
|
1829
1874
|
// max scroll several lines larger than the final transcript, so the
|
|
1830
1875
|
// stored scroll could sit above the true max after the bubble is
|
|
@@ -2711,14 +2756,13 @@ class App {
|
|
|
2711
2756
|
const n = this.recentToolCount();
|
|
2712
2757
|
return n > 0 ? n + 2 : 0;
|
|
2713
2758
|
}
|
|
2714
|
-
/** Count of the most recent tool calls (≤ TOOL_PANEL_ROWS).
|
|
2759
|
+
/** Count of the most recent tool calls (≤ TOOL_PANEL_ROWS). Only tool
|
|
2760
|
+
* lines from the CURRENT run count — the panel must never resurrect the
|
|
2761
|
+
* previous run's shell/web-search cards (bug 2026-09-08: the old whole-
|
|
2762
|
+
* transcript scan re-showed them above the input during the new run's
|
|
2763
|
+
* THINKING phase, before any new tool existed). */
|
|
2715
2764
|
recentToolCount() {
|
|
2716
|
-
|
|
2717
|
-
for (let i = this.messages.length - 1; i >= 0 && n < TOOL_PANEL_ROWS; i--) {
|
|
2718
|
-
if (this.messages[i].kind === 'tool')
|
|
2719
|
-
n++;
|
|
2720
|
-
}
|
|
2721
|
-
return n;
|
|
2765
|
+
return Math.min(this.runToolMsgs.length, TOOL_PANEL_ROWS);
|
|
2722
2766
|
}
|
|
2723
2767
|
/**
|
|
2724
2768
|
* The recent-tool-calls panel content: one `· Tool args` line per call
|
|
@@ -2731,8 +2775,10 @@ class App {
|
|
|
2731
2775
|
return [];
|
|
2732
2776
|
const out = [];
|
|
2733
2777
|
const inner = Math.max(8, cols - 4);
|
|
2734
|
-
|
|
2735
|
-
|
|
2778
|
+
// ONLY this run's tool lines (runToolMsgs), newest last — the panel is
|
|
2779
|
+
// a LIVE indicator of the current work, not a history re-run.
|
|
2780
|
+
for (let i = this.runToolMsgs.length - 1; i >= 0 && out.length < TOOL_PANEL_ROWS; i--) {
|
|
2781
|
+
const m = this.runToolMsgs[i];
|
|
2736
2782
|
if (m.kind !== 'tool')
|
|
2737
2783
|
continue;
|
|
2738
2784
|
const args = (m.content.split('\n')[0] || '').trim();
|
package/install.sh
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# VectorHead (agen-vektor) — one-shot installer for Debian/Ubuntu & Termux.
|
|
4
|
+
#
|
|
5
|
+
# What it does:
|
|
6
|
+
# 1. Installs Node.js 20+ and npm when missing/too old
|
|
7
|
+
# (Termux: pkg; Debian/Ubuntu: apt, with NodeSource 20.x fallback when
|
|
8
|
+
# the distro ships an older node — e.g. Ubuntu 24.04 ships Node 18).
|
|
9
|
+
# 2. Installs the agent globally: npm i -g agen-vektor
|
|
10
|
+
# 3. Verifies with `vector --version`.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# bash install.sh
|
|
14
|
+
#
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
MIN_MAJOR=20
|
|
18
|
+
|
|
19
|
+
node_major() {
|
|
20
|
+
node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
need_node() {
|
|
24
|
+
command -v node >/dev/null 2>&1 || return 0
|
|
25
|
+
[ "$(node_major)" -lt "$MIN_MAJOR" ]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
need_npm() {
|
|
29
|
+
! command -v npm >/dev/null 2>&1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SUDO=""
|
|
33
|
+
if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
|
34
|
+
SUDO="sudo"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
as_root() {
|
|
38
|
+
if [ -n "$SUDO" ]; then
|
|
39
|
+
$SUDO "$@"
|
|
40
|
+
else
|
|
41
|
+
"$@"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
say() { printf '\033[1;38;5;208m[vector]\033[0m %s\n' "$*"; }
|
|
46
|
+
|
|
47
|
+
if ! need_node && ! need_npm; then
|
|
48
|
+
say "Node.js $(node -v) & npm sudah terpasang — skip instalasi sistem."
|
|
49
|
+
else
|
|
50
|
+
if [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *com.termux* ]]; then
|
|
51
|
+
say "Termux terdeteksi — pkg update + install nodejs…"
|
|
52
|
+
pkg update -y
|
|
53
|
+
pkg install -y nodejs
|
|
54
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
55
|
+
say "apt: update + install nodejs npm…"
|
|
56
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
57
|
+
as_root apt-get update -y
|
|
58
|
+
as_root apt-get install -y curl ca-certificates gnupg nodejs npm || true
|
|
59
|
+
if need_node || need_npm; then
|
|
60
|
+
say "Node distro < ${MIN_MAJOR} atau npm hilang — pakai NodeSource ${MIN_MAJOR}.x…"
|
|
61
|
+
if [ -n "$SUDO" ]; then
|
|
62
|
+
curl -fsSL "https://deb.nodesource.com/setup_${MIN_MAJOR}.x" | $SUDO -E bash -
|
|
63
|
+
else
|
|
64
|
+
curl -fsSL "https://deb.nodesource.com/setup_${MIN_MAJOR}.x" | bash -
|
|
65
|
+
fi
|
|
66
|
+
as_root apt-get install -y nodejs
|
|
67
|
+
fi
|
|
68
|
+
else
|
|
69
|
+
say "Tidak ada apt-get — install Node.js ${MIN_MAJOR}+ manual dari https://nodejs.org lalu jalankan script ini lagi."
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if need_node || need_npm; then
|
|
75
|
+
say "Node/npm masih belum siap (node=$(command -v node >/dev/null && node -v || echo none), npm=$(command -v npm >/dev/null && echo yes || echo no))."
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
say "Install agen-vektor (global)…"
|
|
80
|
+
npm install -g agen-vektor
|
|
81
|
+
|
|
82
|
+
say "Selesai: $(vector --version 2>/dev/null || echo 'vector') — jalankan dengan perintah: vector"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agen-vektor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "VectorHead (agen-vektor) — AI Coding Agent CLI/TUI for Linux & Termux. Multi-provider, tool calling, session, permission system.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"main": "dist/cli/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"install.sh",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|