thincoder 0.7.5 → 0.7.7
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 +195 -172
- package/package.json +1 -1
- package/src/SYSTEM_PROMPT.md +0 -30
- package/src/agent.mjs +32 -15
- package/src/config.mjs +9 -1
- package/src/discipline-rules.md +30 -0
- package/src/explore-overlay.md +1 -0
- package/src/memory.mjs +105 -0
- package/src/plan-overlay.md +1 -0
- package/src/provider.mjs +3 -2
- package/src/tui.mjs +130 -48
package/README.md
CHANGED
|
@@ -2,133 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
**Sharp Code, Zero Bloat.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**A "thin" AI coding agent: pure `.mjs`, no build step, zero npm dependencies, native Node.js.**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
The "Thin" in ThinCoder doesn't mean "feature-poor" — it means **sharp thinking, straight to the point** — like a blade.
|
|
8
|
+
While every AI agent races to be "all-powerful", ThinCoder plays the opposite card: **restraint, precision, no filler**.
|
|
9
|
+
Its persona is a geek engineer of few words who cuts to the bone: give it a complex requirement, get back a clean implementation.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Design philosophy (the entire meaning of the name): if the Node standard library can do it, no dependency is allowed. The project's `node_modules` is empty.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Features
|
|
14
14
|
|
|
15
|
-
- **Agent
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- **Plan Mode
|
|
24
|
-
- **AUTO
|
|
25
|
-
-
|
|
26
|
-
- **Goal/Verify/Skills
|
|
27
|
-
-
|
|
15
|
+
- **Agent loop**: LLM ↔ tool-call loop, 100-turn cap against runaway, completion guard blocks unverified changes
|
|
16
|
+
- **Codebase understanding** ⭐0.5.0: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (source FTS5 + vectors + JSDoc extraction), `doc_search` (docs chunked by ## headings) — background indexing, auto-incremental updates on file writes, three tools guided by "structure → intent → details"
|
|
17
|
+
- **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from five leading Chinese vendors — DeepSeek / Kimi / GLM / Qwen / MiniMax. No legacy model compatibility, no local model support. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type / reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all five deeply adapted.
|
|
18
|
+
- **Toolset**: `read` / `write` / `edit` / `bash` / `glob` (supports `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image` (image/video paste) + three retrieval tools + MCP — all zero-dependency, file tools confined to the working directory
|
|
19
|
+
- **Memory system**: three layers (personal/project/team), FTS5 + vector RRF hybrid retrieval, git-friendly markdown format
|
|
20
|
+
- **Two-phase tool scheduling**: permission prompts serialized, read-only tools parallelized, side-effect tools serialized
|
|
21
|
+
- **Session persistence** ⭐0.5.0: up to 5 archive slots, `/session` to switch anytime, tool results visible after restore
|
|
22
|
+
- **Concurrent subagents**: three roles — `explore`/`plan`/`coder` — dispatched in parallel, streaming output visible, reports land in the conversation
|
|
23
|
+
- **Plan Mode**: read-only exploration + design, implement after user approval
|
|
24
|
+
- **AUTO mode**: `/auto` full authorization, no confirmations on long tasks
|
|
25
|
+
- **Task tracking**: `task` tool breaks down multi-step work, status bar ✓n/m live progress, auto-filters completed items
|
|
26
|
+
- **Goal/Verify/Skills**: long-goal tracking, completion verification, reusable skills
|
|
27
|
+
- **Streaming TUI**: bare ANSI, permission preview right above the input box, write/edit auto-shows diffs, paste shortcut hint in the input box corner for multimodal models (Win: Alt+V / Mac/Linux: Ctrl+V)
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Memory: What One Learns, the Whole Team Knows
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Three layers, all "query if present, skip if absent", unified hybrid retrieval:
|
|
32
32
|
|
|
33
|
-
|
|
|
34
|
-
| ---------------- |
|
|
35
|
-
| **Personal** | `~/.thincoder/memory.db
|
|
36
|
-
| **Project** |
|
|
37
|
-
| **Team
|
|
33
|
+
| Layer | Location | Sync method |
|
|
34
|
+
| ---------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
35
|
+
| **Personal** | `~/.thincoder/memory.db` (sqlite) | Not synced, private |
|
|
36
|
+
| **Project** | `.thincoder/memory/*.md` in the project repo | With the project git (ThinCoder **only writes files, never commits for you**) |
|
|
37
|
+
| **Team** (opt.) | Dedicated memory repo, cloned to `~/.thincoder/teams/<name>/` | `thincoder sync` (pull --rebase); auto commit + push on write (dedicated facility, opt-in) |
|
|
38
38
|
|
|
39
|
-
-
|
|
40
|
-
- **
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
39
|
+
- **Hybrid retrieval**: FTS5 (BM25, per-character CJK indexing, bigrams matchable) + embedding vectors (brute-force cosine) + RRF(k=60) fusion ranking
|
|
40
|
+
- **Embeddings**: OpenAI-compatible `/v1/embeddings`, defaults to SiliconFlow `BAAI/bge-m3` (free tier, good CJK support); Ollama works as an offline option. Vectors generated lazily — not computed on write, backfilled and persisted on first search
|
|
41
|
+
- **Entry format**: Markdown + frontmatter (type/title/tags/author/created), readable and reviewable directly on GitHub; one file per entry, naturally avoiding merge conflicts; real conflicts produce honest errors, never auto-merged
|
|
42
|
+
- **Dual-track accumulation**: conventions written manually (`memory_put`), experience extracted from sessions via `/distill` — **the LLM proposes candidates, a human confirms each y/n** before anything is stored; never fully automatic
|
|
43
|
+
- **Retrieval isolation**: the Project layer is isolated by project path — project A's memories never leak into project B
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## Requirements
|
|
46
46
|
|
|
47
47
|
- Node.js >= 24
|
|
48
|
-
-
|
|
49
|
-
-
|
|
48
|
+
- An API key for any OpenAI-compatible endpoint
|
|
49
|
+
- Optional: an embedding service key (without it, retrieval degrades to pure FTS)
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## Quick Start
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
#
|
|
54
|
+
# Install
|
|
55
55
|
npm install -g thincoder
|
|
56
56
|
|
|
57
|
-
#
|
|
57
|
+
# Launch the TUI (default command)
|
|
58
58
|
thincoder
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
First launch walks you through a setup wizard: arrow keys to pick a provider (built-in presets or a custom endpoint) → enter API key → optionally enter an embedding key (SiliconFlow, enables vector memory search, skippable) → arrow keys to pick a model — no hand-editing config files. Adjust anytime with `/provider`, `/model`, `/config embedkey`. `chat`/`distill` also offer in-place interactive setup when no key is configured in a terminal (in pipes/CI they exit with an error and instructions).
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
You can also hand-write `~/.thincoder/config.json` (see "Configuration" below), then:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
#
|
|
67
|
-
thincoder chat "
|
|
66
|
+
# One-shot Q&A (pipe-friendly)
|
|
67
|
+
thincoder chat "read package.json and summarize it"
|
|
68
68
|
|
|
69
|
-
#
|
|
70
|
-
thincoder memory put --type=rule --title="
|
|
71
|
-
thincoder memory search "
|
|
69
|
+
# Memory management
|
|
70
|
+
thincoder memory put --type=rule --title="code style" --content="no semicolons"
|
|
71
|
+
thincoder memory search "code style"
|
|
72
72
|
thincoder memory list
|
|
73
73
|
thincoder memory remove 1
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
thincoder sync #
|
|
75
|
+
# Team memory (optional, available after configuring memory.team)
|
|
76
|
+
thincoder sync # pull the team repo and rebuild the index
|
|
77
77
|
|
|
78
|
-
#
|
|
78
|
+
# Extract knowledge from a session transcript (stored after per-item confirmation)
|
|
79
79
|
thincoder distill session.txt
|
|
80
80
|
|
|
81
|
-
#
|
|
81
|
+
# Upgrade
|
|
82
82
|
thincoder upgrade
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Running from source: replace `thincoder` above with `node bin/thincoder.mjs`.
|
|
86
86
|
|
|
87
|
-
TUI
|
|
87
|
+
Slash commands in the TUI: `/help`, `/model` (arrow-key picker across all models of all providers; `/model <name>` switches directly), `/provider` (add/remove providers, set keys, custom endpoints), `/think` (thinking mode toggle and reasoning effort), `/config` (view config, `/config embedkey` for the embedding key, `/config set` for parameters), `/session` (list/switch archived sessions), `/reindex` (rebuild the index), `/distill` (extract knowledge from the current session), `/clear`, `/exit`. Typing `/` shows live matching hints in the status bar.
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
Environment variables: `THINCODER_API_KEY` (or `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`), `THINCODER_BASE_URL`, `THINCODER_MODEL`, `SILICONFLOW_API_KEY`.
|
|
90
90
|
|
|
91
|
-
##
|
|
91
|
+
## Configuration
|
|
92
92
|
|
|
93
|
-
`~/.thincoder/config.json
|
|
93
|
+
`~/.thincoder/config.json`:
|
|
94
94
|
|
|
95
95
|
```jsonc
|
|
96
96
|
{
|
|
97
97
|
"providers": [
|
|
98
|
-
//
|
|
98
|
+
// multiple allowed; switch with /model <name>
|
|
99
99
|
{
|
|
100
100
|
"name": "deepseek",
|
|
101
|
-
"baseURL": "https://api.deepseek.com/v1", //
|
|
102
|
-
"apiKey": "sk-...", //
|
|
101
|
+
"baseURL": "https://api.deepseek.com/v1", // any OpenAI-compatible endpoint
|
|
102
|
+
"apiKey": "sk-...", // or leave empty to use env vars
|
|
103
103
|
"model": "deepseek-chat",
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// "
|
|
104
|
+
// optional: proactive throttling budget (match your account's rate-limit tier;
|
|
105
|
+
// without it the gate is off, 429 backoff still applies).
|
|
106
|
+
// Rate limits are per-account counters (RPM/TPM over a 60s window) — check each vendor's console
|
|
107
|
+
// "tpm": 200000, // tokens/minute (input + output total)
|
|
108
|
+
// "rpm": 50, // requests/minute
|
|
108
109
|
},
|
|
109
110
|
],
|
|
110
|
-
"activeProvider": "deepseek", //
|
|
111
|
+
"activeProvider": "deepseek", // currently active provider name
|
|
111
112
|
"embedding": {
|
|
112
|
-
//
|
|
113
|
+
// optional: without it, retrieval is pure FTS
|
|
113
114
|
"baseURL": "https://api.siliconflow.cn/v1",
|
|
114
|
-
"apiKey": "sk-...", //
|
|
115
|
+
"apiKey": "sk-...", // or SILICONFLOW_API_KEY
|
|
115
116
|
"model": "BAAI/bge-m3",
|
|
116
117
|
},
|
|
117
118
|
"agent": {
|
|
118
|
-
"maxTurns": 100, //
|
|
119
|
-
"compactThreshold": 100000, //
|
|
119
|
+
"maxTurns": 100, // tool-loop cap
|
|
120
|
+
"compactThreshold": 100000, // context compaction threshold (approx. tokens)
|
|
120
121
|
},
|
|
121
122
|
"memory": {
|
|
122
|
-
"dbPath": "~/.thincoder/memory.db", // sqlite
|
|
123
|
-
"projectDir": ".thincoder/memory", // Project
|
|
123
|
+
"dbPath": "~/.thincoder/memory.db", // sqlite index path
|
|
124
|
+
"projectDir": ".thincoder/memory", // Project layer directory (relative to project root)
|
|
124
125
|
"team": {
|
|
125
|
-
//
|
|
126
|
+
// optional: Team layer disabled without it
|
|
126
127
|
"name": "myteam",
|
|
127
128
|
"repo": "git@github.com:org/team-memory.git",
|
|
128
129
|
},
|
|
129
130
|
},
|
|
130
131
|
"mcp": {
|
|
131
|
-
//
|
|
132
|
+
// optional: MCP server list
|
|
132
133
|
"servers": [
|
|
133
134
|
{
|
|
134
135
|
"name": "filesystem",
|
|
@@ -140,128 +141,150 @@ TUI 内斜杠命令:`/help`、`/model`(方向键选择全部 provider 的全
|
|
|
140
141
|
}
|
|
141
142
|
```
|
|
142
143
|
|
|
143
|
-
##
|
|
144
|
+
## Architecture
|
|
144
145
|
|
|
145
146
|
```
|
|
146
|
-
bin/thincoder.mjs
|
|
147
|
+
bin/thincoder.mjs command entry (tui / chat / memory / sync / distill)
|
|
147
148
|
src/
|
|
148
|
-
provider.mjs LLM
|
|
149
|
-
embedding.mjs
|
|
150
|
-
tools.mjs 16
|
|
151
|
-
mcp.mjs MCP
|
|
152
|
-
agent.mjs
|
|
153
|
-
+
|
|
154
|
-
repomap.mjs
|
|
155
|
-
context.mjs token
|
|
156
|
-
memory.mjs
|
|
157
|
-
+ FTS5 +
|
|
158
|
-
session.mjs
|
|
159
|
-
skills.mjs
|
|
160
|
-
markdown.mjs
|
|
161
|
-
gitmem.mjs Team
|
|
162
|
-
distill.mjs
|
|
163
|
-
checkpoint.mjs git patch
|
|
164
|
-
config.mjs
|
|
165
|
-
tui.mjs
|
|
166
|
-
test/ node:test
|
|
167
|
-
scripts/
|
|
149
|
+
provider.mjs LLM calls (fetch, SSE streaming, retries)
|
|
150
|
+
embedding.mjs vector embeddings (OpenAI-compatible /v1/embeddings)
|
|
151
|
+
tools.mjs 16 builtin tools + MCP wrapping + readonly scheduling flags
|
|
152
|
+
mcp.mjs MCP client (JSON-RPC + stdio transport, zero-dependency)
|
|
153
|
+
agent.mjs main loop + two-phase tool execution + plan/task/goal/skill/subagent/verify tools
|
|
154
|
+
+ incremental indexing (auto reindexFile after write/edit/delete)
|
|
155
|
+
repomap.mjs repo dependency outline (import/export regex parsing, on-demand via tool)
|
|
156
|
+
context.mjs rough token estimation + history compaction + task re-injection
|
|
157
|
+
memory.mjs memory core: three-layer merged retrieval + code/doc indexing (code_chunks/doc_chunks)
|
|
158
|
+
+ FTS5 + vector RRF + JSDoc extraction + single-file incremental indexing
|
|
159
|
+
session.mjs session persistence (up to 5 archive slots, isolated by project cwd)
|
|
160
|
+
skills.mjs skill discovery/loading (.thincoder/skills/*.md)
|
|
161
|
+
markdown.mjs entry format (frontmatter parse/serialize)
|
|
162
|
+
gitmem.mjs Team layer git sync (clone/pull --rebase/push, system git)
|
|
163
|
+
distill.mjs session knowledge extraction (candidates + human confirmation)
|
|
164
|
+
checkpoint.mjs git patch snapshots / rewind
|
|
165
|
+
config.mjs config loading
|
|
166
|
+
tui.mjs bare-ANSI terminal UI (wide-char wrapping, scrolling, permission prompts, slash commands)
|
|
167
|
+
test/ node:test offline unit tests (npm test)
|
|
168
|
+
scripts/ real-environment verification scripts (compaction, team sync)
|
|
168
169
|
```
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
Key design decisions:
|
|
171
172
|
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
- **git
|
|
177
|
-
-
|
|
173
|
+
- **Two-phase tool execution**: phase one serializes permission prompts (each side-effect tool asks the user); phase two runs read-only tools in parallel (`Promise.all`) and side-effect tools serially. Results are fed back paired by `toolCallId`
|
|
174
|
+
- **Permissions live in the UI layer**: tools only execute; "ask the user or not" is the TUI/CLI's business, so headless scenarios need no tool changes
|
|
175
|
+
- **The index is disposable**: sqlite is just a local index of code/docs/memories — `reindex` rebuilds it anytime
|
|
176
|
+
- **Separate code/doc indexes**: source and markdown docs are indexed in separate tables and searched through different tools — keeps the model from mistaking old code patterns for design conventions
|
|
177
|
+
- **git boundaries**: the Project layer only writes files and never touches your repo's commits; the Team layer is a ThinCoder-managed repo where auto commit+push is allowed
|
|
178
|
+
- **CJK retrieval**: FTS5 unicode61 + per-character CJK spacing on both write and query sides; semantic matching goes through the vector channel
|
|
178
179
|
|
|
179
|
-
##
|
|
180
|
+
## Development
|
|
180
181
|
|
|
181
182
|
```bash
|
|
182
|
-
npm test #
|
|
183
|
-
node scripts/verify-compress.mjs #
|
|
184
|
-
node scripts/verify-team.mjs #
|
|
183
|
+
npm test # offline unit tests (node:test, with local mock servers)
|
|
184
|
+
node scripts/verify-compress.mjs # real-API verification of context compaction (needs valid config)
|
|
185
|
+
node scripts/verify-team.mjs # team memory A->git->B full-chain verification (local git, offline)
|
|
185
186
|
```
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
##
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
- **
|
|
207
|
-
- **
|
|
208
|
-
-
|
|
209
|
-
- **
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
- **
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
### 0.7.
|
|
224
|
-
-
|
|
225
|
-
- **
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
- **
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
188
|
+
Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (including devDependencies).
|
|
189
|
+
|
|
190
|
+
## Roadmap
|
|
191
|
+
|
|
192
|
+
- More builtin skills
|
|
193
|
+
|
|
194
|
+
## Changelog
|
|
195
|
+
|
|
196
|
+
### 0.7.7 (2026-07)
|
|
197
|
+
- **Code review fixes (4 critical bugs)**:
|
|
198
|
+
- `gitSync` anchor never set after full `codeSync` fallback → fast path was dead in production; now `codeSync`/`docSync` write the anchor on success
|
|
199
|
+
- `gitSync` skipped deleted files → stale chunks remained in index forever; `--diff-filter` now includes `D`, and `ENOENT` is distinguished from other errors (failed files don't advance the anchor)
|
|
200
|
+
- Completion guard was a one-shot latch → after firing once, further mutations could finish unverified; now re-armed with a pushback counter (max 2 pushes, 3rd passes through)
|
|
201
|
+
- Verify-failure exhaustion returned raw model text without honesty framing → now injects a system reminder forcing the model to state what's still failing, what was tried, and that the work is unfinished
|
|
202
|
+
- **Input queue during processing**: messages typed while the agent is processing are queued and auto-executed when processing ends. Queue preview shown as a single line above the input box (no collision with subagent panel). `Ctrl+D` deletes the last queued item. `/cancel` and `/exit` bypass the queue
|
|
203
|
+
|
|
204
|
+
### 0.7.6 (2026-07)
|
|
205
|
+
- **SYSTEM_PROMPT split into core + discipline**: core rules (shared by all agents) separated from coding/testing/debugging discipline (main agent + coder), so explore/plan subagents no longer burn attention on irrelevant coding clauses — single source of truth, one rule changed in one file
|
|
206
|
+
- **git-driven incremental indexing**: new `gitSync` uses `git diff` at startup to find files changed since the last index and rebuilds only their FTS5 chunks. Non-git repos / first run / large changesets (>200 files) automatically fall back to full scans. `codeSync` + `docSync` startup parallelized
|
|
207
|
+
- **Embeddings backfilled right after reindexFile**: incremental indexing after each agent write/edit no longer leaves vector NULLs — `ensureEmbeddings` runs immediately, so freshly changed files are semantically searchable at once
|
|
208
|
+
- **Project-instruction injection hardening**: AGENTS.md content wrapped with `escapeXml` + `<untrusted_project_instructions>`, closing the prompt-injection hole from malicious project instructions
|
|
209
|
+
- **Compaction threshold cap**: for 1M-window models the compaction threshold drops from 800K to 300K tokens (`COMPACT_CAP_TOKENS`), preventing history from blowing the TPM budget and the compaction request itself from hitting 429
|
|
210
|
+
- **readSSE tool_calls name dedup**: some APIs (GLM occasionally) resend the full name instead of deltas in the stream, and `+=` produced `readread`. Now only the first non-empty value is taken
|
|
211
|
+
- **Edge-case thinking across all prompt layers**: plan/explore/coder/main overlays each gained an edge-case recognition rule (open-ended, no scenario enumeration)
|
|
212
|
+
- **Testing discipline refined**: full-test trigger changed from "touched core infrastructure files" to "changed core infrastructure behavior" — adding a helper to memory.mjs no longer triggers the full suite
|
|
213
|
+
|
|
214
|
+
### 0.7.5 (2026-07)
|
|
215
|
+
- **Compound prompt instructions split**: 8 compound sentences across SYSTEM_PROMPT / main-overlay / coder-overlay split into independent bullets (one attention node per instruction), improving instruction-following on DeepSeek/GLM/Qwen — fallback clauses like "add tests after changing code" no longer get skipped
|
|
216
|
+
- **Testing discipline strengthened**: SYSTEM_PROMPT Testing discipline gained an independent hard rule (changing behavior/adding code requires tests); main-overlay self-review checklist gained "do existing tests cover the change"; coder-overlay final checklist gained a test item
|
|
217
|
+
- **Plan mode workflow**: main-overlay's plan mode instruction split from one compound sentence into a 3-step numbered flow
|
|
218
|
+
|
|
219
|
+
### 0.7.4 (2026-07)
|
|
220
|
+
- **verify tiered self-check**: default quick mode (syntax-check changed files + git diff + self-review checklist, milliseconds); `full=true` also runs the full npm test suite — no more waiting ten-plus seconds per line changed; quick satisfies the completion guard, use full when wrapping up or touching core infrastructure
|
|
221
|
+
- **Prompt discipline strengthened**: SYSTEM_PROMPT gained Testing discipline (when to run which tier) and Debugging strategy (diagnose before treating, one change at a time); coder/plan/main overlays gained self-review checklists (simplest solution, match project patterns, don't touch unrelated files)
|
|
222
|
+
- **Fix**: quick mode marked verification passed even when syntax checks failed, gutting the completion guard
|
|
223
|
+
|
|
224
|
+
### 0.7.3 (2026-07)
|
|
225
|
+
- **Image paste**: new `read_image` tool — paste images/videos from the clipboard, multimodal models directly understand screenshots, UI mockups, architecture diagrams (Win: `Alt+V` / Mac/Linux: `Ctrl+V`)
|
|
226
|
+
- **TUI paste hint**: with a multimodal model, the input box corner shows the OS-appropriate paste shortcut; hidden for text-only models
|
|
227
|
+
|
|
228
|
+
### 0.7.2 (2026-07)
|
|
229
|
+
- **TPM/RPM proactive rate gate**: with `tpm`/`rpm` budgets configured on a provider, requests are booked against a local sliding window (60s, input+output) before sending — over budget means sleeping until the window frees up instead of gambling on 429s; covers the main loop / compaction summaries / subagents / truncation resume. Status bar shows `TPM throttle wait ~Ns`; the gate is off for unconfigured providers
|
|
230
|
+
- **429-specific backoff**: respects the `Retry-After` header, otherwise backs off 15s/30s/60s (60s window — sub-second backoff is pointless); quota/balance errors (`exceeded_current_quota_error`) are distinguished from rate limits and no longer retried uselessly
|
|
231
|
+
- **Dependency injection as compact summary**: `buildSummary` (directory-level dependencies + hub files + entry points, naturally ~1-2k chars) replaces the full-outline injection; detailed import/export available on demand via `repo_outline`
|
|
232
|
+
- **TUI menus**: `/model` `/config` `/provider` `/think` `/mcp` `/goal` `/session` `/rewind` unified into picker menus
|
|
233
|
+
- **Session robustness**: corrupted files or disk errors during archive/switch no longer crash — silently abandoned
|
|
234
|
+
|
|
235
|
+
### 0.7.1 (2026-07)
|
|
236
|
+
- **Context explosion fix (urgent)**: the startup dependency-outline injection is no longer unbounded — a multi-repo parent directory (thousands of indexed files) produced a 1.4M-char ≈ 350K-token outline, re-injected every turn, blowing up context within a few turns and tripping TPM limits. Now truncated to 6000 chars (with a pointer to `repo_outline` for focused queries) and injected only once per session
|
|
237
|
+
- **Compaction escape hatch**: when history was too short (≤13 messages) to slice a middle section, compaction never happened — one giant message (huge paste/oversized injection) could deadlock. Now a deterministic slimming path: oversized user/tool bodies are truncated to stubs, reasoning_content and tool_calls pairing untouched
|
|
238
|
+
- **docSync ReferenceError fix**: undeclared `failed`/`errors` made every doc index sync throw (two tests red)
|
|
239
|
+
- **apply_patch tool**: unified-diff multi-file atomic patching (any failed hunk → nothing written), permission preview shows the diff directly
|
|
240
|
+
- **checkpoint tool**: `list`/`create`/`rewind` snapshot abilities exposed to the model (previously only wired to TUI auto-snapshots + /rewind, so the model couldn't save itself); the bash destructive-git guard upgraded to per-segment detection (chained forms like `&&`/`;`/`|`/command substitution no longer slip through)
|
|
241
|
+
- **bash process-tree kill**: timeout/interrupt kills the whole tree (POSIX process groups / Windows taskkill /T) — no orphaned grandchildren
|
|
242
|
+
- **Subagent display contract**: only content/thinking tokens relay to the TUI scrolling area; internal tool calls no longer flood the screen
|
|
243
|
+
- **Path safety**: `resolveInCwd` prevents symlink escapes (realpath double-check); edit rejects empty old_string; single-file incremental indexing skips hidden directories and node_modules
|
|
244
|
+
- **Misc**: SQLite WAL + busy_timeout, single-transaction schema migrations, semantic version comparison for upgrades, MCP cmd.exe quote-doubling escape, gitmem skips commits when nothing changed
|
|
245
|
+
|
|
246
|
+
### 0.7.0 (2026-07)
|
|
247
|
+
- **Deep model protocol adaptation**: reasoning_content echo differentiated per model (`reasoningEcho` spec field) — DeepSeek/Kimi must echo, GLM must not; reasoning_effort enum validation (`reasoningEffortEnum`); temperature range clamping (`tempRange`)
|
|
248
|
+
- **Qwen/MiniMax spec completion**: reasoning_effort enums (Qwen 3.8-max-preview), temperature ranges (Qwen [0,2), MiniMax [0,2]), MiniMax M3 thinking mode
|
|
249
|
+
- **grep context lines**: `before`/`after` params (grep -B/-A equivalents), matches marked with `:`, context with `-`, adjacent ranges in the same file merged and deduped
|
|
250
|
+
- **System prompt boundary rule**: never modify files outside the working directory; never use bash to bypass the read/write/edit directory confinement
|
|
251
|
+
- **question tool input box title**: fixed to ` Question `, question text goes to the conversation area (no longer crammed into the box title)
|
|
252
|
+
|
|
253
|
+
### 0.5.0 (2026-07)
|
|
254
|
+
- **Codebase understanding**: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (FTS5 + vectors + JSDoc), `doc_search` (chunked by ## headings), auto-incremental indexing on file writes
|
|
255
|
+
- **Model adaptation**: 5 built-in presets (DeepSeek/Kimi/GLM/Qwen/MiniMax), maxTokens maxed out, truncation resume, thinking-mode APIs auto-matched
|
|
256
|
+
- Session 5-slot archiving, `/session` switching, tool results shown after restore
|
|
257
|
+
- Subagent streaming output visible, final reports in the conversation area
|
|
258
|
+
- File tools confined to the working directory, permission preview above the input box
|
|
259
|
+
- write/edit auto-attach git diffs, edit error messages with better hints
|
|
260
|
+
- task auto-filters completed items, proactive reminder when all done
|
|
261
|
+
- Prompt guidance: "check official docs → save discrepancies to project memory"
|
|
239
262
|
|
|
240
263
|
### 0.4.0
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
264
|
+
- Permission approval shows content previews (write content, edit diff, bash command)
|
|
265
|
+
- Todo panel progress visualization, status bar token usage and context utilization
|
|
266
|
+
- Two-layer project instructions merge (global + project AGENTS.md)
|
|
244
267
|
|
|
245
268
|
### 0.3.0
|
|
246
|
-
- MCP
|
|
247
|
-
- Skills
|
|
248
|
-
- Plan/Goal/Question
|
|
249
|
-
-
|
|
250
|
-
- task
|
|
251
|
-
- DeepSeek thinking
|
|
252
|
-
- checkpoint
|
|
269
|
+
- MCP client (JSON-RPC + stdio, zero-dependency)
|
|
270
|
+
- Skills system (`.thincoder/skills/*.md`)
|
|
271
|
+
- Plan/Goal/Question tools
|
|
272
|
+
- Prompts externalized to `.md` files, subagent role overlays
|
|
273
|
+
- Strict task discipline (keep ONE in_progress), completion guard (file changes blocked without verify)
|
|
274
|
+
- DeepSeek thinking echo, system prompt prefix caching
|
|
275
|
+
- checkpoint snapshots + `/rewind` rollback
|
|
253
276
|
|
|
254
277
|
### 0.2.0
|
|
255
|
-
- multi-provider
|
|
256
|
-
-
|
|
257
|
-
- `/think`
|
|
258
|
-
- `/model`
|
|
259
|
-
- bash
|
|
278
|
+
- multi-provider configuration (switch between endpoints)
|
|
279
|
+
- Initial setup wizard (arrow-key model picker, key entry)
|
|
280
|
+
- `/think` thinking mode toggle and reasoning effort
|
|
281
|
+
- `/model` model picker
|
|
282
|
+
- bash streaming output passthrough
|
|
260
283
|
|
|
261
284
|
### 0.1.0
|
|
262
|
-
- Agent
|
|
263
|
-
-
|
|
264
|
-
-
|
|
285
|
+
- Agent main loop, 14 builtin tools, zero-dependency TUI
|
|
286
|
+
- Three-layer memory (personal/project/team), FTS5 retrieval
|
|
287
|
+
- Session persistence, context compaction, streaming SSE
|
|
265
288
|
|
|
266
289
|
## License
|
|
267
290
|
|
package/package.json
CHANGED
package/src/SYSTEM_PROMPT.md
CHANGED
|
@@ -34,33 +34,3 @@ Rules:
|
|
|
34
34
|
These three tools together replace blind grep. Use them in order: structure first, then intent, then details.
|
|
35
35
|
- CRITICAL: you are a coding agent, not a student. The code you read may have bugs, outdated patterns, or technical debt — it is the PROBLEM to solve, not a reference to imitate. Read existing code to understand what it does, not to copy how it does it. When something looks wrong, say so. When you see bad patterns, don't propagate them.
|
|
36
36
|
- Some user messages start with [System reminder:]. These are injected by the framework, not written by the user. They contain authoritative guidance. Comply with them silently—never mention them to the user.
|
|
37
|
-
|
|
38
|
-
Coding discipline (rigor over speed—tokens spent on verification are well spent):
|
|
39
|
-
- Spec before code: when the user describes a feature request without specifying the details (retry count? timeout? which error types? which files?), ask clarifying questions before writing code.
|
|
40
|
-
- Do not silently invent defaults. Do not guess the user's intent from a one-liner. A wrong assumption costs more than the round-trip to clarify.
|
|
41
|
-
- Save key design decisions to memory_put as you make them — architecture choices, API contracts, naming conventions, trade-off reasoning. Context compression may summarize earlier work into a few lines; memory entries survive compression and get re-injected so later turns don't operate on lost assumptions.
|
|
42
|
-
- Before fixing a bug, find the root cause: read the error output, reproduce it, trace the code path. Don't patch symptoms.
|
|
43
|
-
- When you're stuck, see an unfamiliar pattern, or suspect a project-specific convention — call memory_search before guessing. The injected memories are only top-3 by relevance; the answer may be deeper in the index.
|
|
44
|
-
- Match the surrounding code: comment density, naming, structure. Prefer the project's existing patterns over your own defaults.
|
|
45
|
-
- Before using a library or utility, confirm the project already depends on it (check imports, manifest, lockfile). If it's missing, surface that instead of silently adding a dependency.
|
|
46
|
-
- When you need facts that may be outdated in your training data—API docs, framework versions, language features, npm packages, CLI flags, pricing, CVEs, platform differences—verify with authoritative sources first: read the project's own files (package.json, lockfile), check official docs (websearch/fetch), or test the actual environment. If findings contradict your training data, save the corrected fact to project memory so future sessions benefit.
|
|
47
|
-
- Refactoring: update every caller when an interface changes; never change existing test logic just to make tests pass.
|
|
48
|
-
- Deliver complete changes: no placeholder stubs, no "// rest unchanged", no TODO gaps left for the user to fill in.
|
|
49
|
-
- After changing behavior, sweep comments and docstrings that now describe the old behavior and bring them in line with the code.
|
|
50
|
-
- Before your final reply, re-read the user's latest request and confirm you are answering that one—not an earlier ask left over from a steer or compaction.
|
|
51
|
-
|
|
52
|
-
Testing discipline (right check at the right time — don't run the full suite for every line change):
|
|
53
|
-
- After every write/edit of .mjs/.js files: call syntax_check immediately — it catches parse errors in milliseconds
|
|
54
|
-
- Before declaring a coding task complete: call verify — it checks syntax on all changed files, shows git diff, and displays a self-review checklist. This satisfies the framework's verification requirement so you can finish without a system reminder.
|
|
55
|
-
- Run the full test suite (verify with full=true, or npm test directly) only when:
|
|
56
|
-
a) You're about to mark the last task done and declare completion
|
|
57
|
-
b) You changed core infrastructure files (agent loop, provider, config, tools, or memory system)
|
|
58
|
-
c) The user explicitly asks you to run tests
|
|
59
|
-
- If verify reports syntax errors or test failures, fix them before claiming completion — never mark work done with known failures
|
|
60
|
-
- When you change behavior or add code, add at least one test that covers the change. If the project has no test suite yet, note that in your report. Never skip this step — untested code is incomplete code.
|
|
61
|
-
|
|
62
|
-
Debugging strategy (when something goes wrong, diagnose before treating):
|
|
63
|
-
- Read the FULL error output — the root cause is often at the end, not the first line
|
|
64
|
-
- Don't change multiple things at once hoping one works — that destroys the signal
|
|
65
|
-
- Narrow down systematically: reproduce the failure in isolation, read the file you just wrote to confirm it matches your intent, trace the control flow with grep or code_search, then fix ONE thing and re-run
|
|
66
|
-
- If the error message is unclear, search the web for it before guessing at a fix
|
package/src/agent.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import { fileURLToPath } from "node:url"
|
|
|
18
18
|
import { execSync } from "node:child_process"
|
|
19
19
|
|
|
20
20
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
21
|
-
const SYSTEM_PROMPT = readFileSync(join(__dirname, "SYSTEM_PROMPT.md"), "utf8") //
|
|
21
|
+
const SYSTEM_PROMPT = readFileSync(join(__dirname, "SYSTEM_PROMPT.md"), "utf8") // 核心规则(所有 agent 通用)
|
|
22
|
+
const DISCIPLINE_RULES = readFileSync(join(__dirname, "discipline-rules.md"), "utf8") // 编码/测试/调试纪律(主 agent + coder 子 agent)
|
|
22
23
|
const MAIN_OVERLAY = readFileSync(join(__dirname, "main-overlay.md"), "utf8") // 主 agent 专属条款(子 agent 没有这些工具)
|
|
23
24
|
const EXPLORE_OVERLAY = readFileSync(join(__dirname, "explore-overlay.md"), "utf8")
|
|
24
25
|
const CODER_OVERLAY = readFileSync(join(__dirname, "coder-overlay.md"), "utf8")
|
|
@@ -309,6 +310,7 @@ export const subagentTool = {
|
|
|
309
310
|
cwd: parent.cwd,
|
|
310
311
|
memory: parent.memory,
|
|
311
312
|
overlay,
|
|
313
|
+
role,
|
|
312
314
|
})
|
|
313
315
|
|
|
314
316
|
// explore/plan:注入 git 上下文(分支/最近提交/工作区状态)——探索与规划都和仓库现状有关(借鉴 kimi-code 的 promptPrefix)
|
|
@@ -761,7 +763,7 @@ export async function loadProjectInstructions(cwd) {
|
|
|
761
763
|
* { provider, tools, config, cwd, memory?, overlay? }
|
|
762
764
|
* overlay — 子 agent 角色覆盖文本,拼接在 system prompt 末尾
|
|
763
765
|
*/
|
|
764
|
-
export function createAgent({ provider, tools, config, cwd, memory = null, overlay = "" }) {
|
|
766
|
+
export function createAgent({ provider, tools, config, cwd, memory = null, overlay = "", role = "" }) {
|
|
765
767
|
return {
|
|
766
768
|
provider,
|
|
767
769
|
tools,
|
|
@@ -769,6 +771,7 @@ export function createAgent({ provider, tools, config, cwd, memory = null, overl
|
|
|
769
771
|
cwd,
|
|
770
772
|
memory,
|
|
771
773
|
overlay,
|
|
774
|
+
_role: role,
|
|
772
775
|
history: [], // OpenAI 格式的对话历史(不含 system)
|
|
773
776
|
tasks: [], // task 工具维护的任务列表
|
|
774
777
|
planMode: false, // plan 工具切换的规划模式
|
|
@@ -866,14 +869,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
866
869
|
agent._onTaskUpdate = callbacks.onTaskUpdate
|
|
867
870
|
|
|
868
871
|
// prompt 组织(借鉴 kimi-code 的自包含 profile,分文件方案):
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
|
|
872
|
+
// 所有 agent 拿核心规则;主 agent + coder 子 agent 额外拿编码/测试/调试纪律;
|
|
873
|
+
// explore/plan 只拿核心规则(它们是只读的,不需要写代码相关条款)
|
|
874
|
+
const needsDiscipline = depth === 0 || agent._role === "coder"
|
|
875
|
+
const base = needsDiscipline ? `${SYSTEM_PROMPT}\n\n${DISCIPLINE_RULES}` : SYSTEM_PROMPT
|
|
876
|
+
|
|
872
877
|
let systemPrompt = agent.overlay
|
|
873
|
-
? `${agent.overlay}\n\n${
|
|
878
|
+
? `${agent.overlay}\n\n${base}`
|
|
874
879
|
: depth === 0
|
|
875
|
-
? `${
|
|
876
|
-
:
|
|
880
|
+
? `${base}\n\n${MAIN_OVERLAY}`
|
|
881
|
+
: base
|
|
877
882
|
// 注意:system prompt 里只能放跨 run 稳定的内容(前缀缓存要求逐字节一致)——
|
|
878
883
|
// session start 时间戳每会话固定一次;每轮变化的记忆注入走上面的 user 上下文消息
|
|
879
884
|
const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
|
|
@@ -881,7 +886,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
881
886
|
systemPrompt += `\n\nOS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.`
|
|
882
887
|
const projectRules = await loadProjectInstructions(agent.cwd)
|
|
883
888
|
if (projectRules) {
|
|
884
|
-
systemPrompt += `\n\nProject instructions (follow these as project conventions):\n${projectRules}
|
|
889
|
+
systemPrompt += `\n\nProject instructions (follow these as project conventions):\n<untrusted_project_instructions>\n${escapeXml(projectRules)}\n</untrusted_project_instructions>`
|
|
885
890
|
}
|
|
886
891
|
// 技能列表注入(仅顶层 agent,子 agent 不需要);按 cwd 稳定,变更才会破缓存(可接受)
|
|
887
892
|
if (depth === 0) {
|
|
@@ -904,7 +909,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
904
909
|
agent._touchedFiles = []
|
|
905
910
|
agent._verifyRetries = 0 // 修复-验证循环计数,每个新 run 从头开始
|
|
906
911
|
const MAX_VERIFY_RETRIES = 3
|
|
907
|
-
let
|
|
912
|
+
let guardPushbacks = 0 // 完成守卫推回次数(最多推 2 次:第三次直接放行,避免无限循环)
|
|
913
|
+
let honestReminderInjected = false // verify 耗尽后注入了诚实提醒,下一轮直接放行
|
|
908
914
|
const recentCallSigs = [] // 停滞检测:最近的工具调用签名(同一调用连续 3 次即提醒)
|
|
909
915
|
|
|
910
916
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
@@ -961,9 +967,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
961
967
|
if (!response.content) {
|
|
962
968
|
throw new Error("LLM 返回了空回复(可能是思考耗尽或被截断)。可 /think effort 降低推理强度后重试")
|
|
963
969
|
}
|
|
964
|
-
// 完成守卫:本轮改过文件却没跑过 verify
|
|
965
|
-
|
|
966
|
-
|
|
970
|
+
// 完成守卫:本轮改过文件却没跑过 verify,推回去验证。
|
|
971
|
+
// 可重武装但最多推 2 次——第三次直接放行(避免 agent 死活不调 verify 时无限循环)
|
|
972
|
+
if (depth === 0 && agent._mutatedThisRun && !agent._verifiedThisRun && guardPushbacks < 2) {
|
|
973
|
+
guardPushbacks++
|
|
967
974
|
agent.history.push({ role: "assistant", content: response.content })
|
|
968
975
|
agent.history.push({
|
|
969
976
|
role: "user",
|
|
@@ -982,10 +989,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
982
989
|
})
|
|
983
990
|
continue
|
|
984
991
|
}
|
|
985
|
-
//
|
|
992
|
+
// 重试用尽:测试仍然失败,注入诚实提醒后给模型最后一轮总结
|
|
986
993
|
if (depth === 0 && agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
|
|
994
|
+
if (honestReminderInjected) {
|
|
995
|
+
// 已经注入过诚实提醒且模型又给了最终回答 → 放行返回
|
|
996
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
997
|
+
return response.content
|
|
998
|
+
}
|
|
999
|
+
honestReminderInjected = true
|
|
987
1000
|
agent.history.push({ role: "assistant", content: response.content })
|
|
988
|
-
|
|
1001
|
+
agent.history.push({
|
|
1002
|
+
role: "user",
|
|
1003
|
+
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
|
|
1004
|
+
})
|
|
1005
|
+
continue
|
|
989
1006
|
}
|
|
990
1007
|
agent.history.push({ role: "assistant", content: response.content })
|
|
991
1008
|
return response.content
|
package/src/config.mjs
CHANGED
|
@@ -93,7 +93,11 @@ const MODEL_SPECS = [
|
|
|
93
93
|
const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
|
|
94
94
|
// 窗口利用率上限:0.8(DeepSeek 内部即全窗口;压缩本身要花一次 LLM 调用,过早压缩是纯浪费。
|
|
95
95
|
// 留 20% 余量给压缩后的尾部增长与输出 token)
|
|
96
|
+
// 但 1M 窗口模型按 0.8 算 = 80 万 token,历史涨到那么大才压缩会打爆 TPM 预算、
|
|
97
|
+
// 压缩请求本身也可能 429。加 cap:不超过 maxOutput 的 8 倍(128K×8≈100万→实际仍偏大但合理),
|
|
98
|
+
// 不超过 30 万(大窗口模型的合理工作上限,再大缓存命中率下降)
|
|
96
99
|
const COMPACT_RATIO = 0.8
|
|
100
|
+
const COMPACT_CAP_TOKENS = 300_000
|
|
97
101
|
|
|
98
102
|
/** 按模型名前缀查规格(大小写不敏感),未知模型给保守默认 */
|
|
99
103
|
export function specForModel(model) {
|
|
@@ -111,7 +115,11 @@ export function contextWindowForModel(model) {
|
|
|
111
115
|
/** 推导压缩阈值;explicit 为配置文件中显式设置的值(优先),否则按模型自动算 */
|
|
112
116
|
export function resolveCompactThreshold(explicit, model) {
|
|
113
117
|
if (explicit != null) return { value: explicit, auto: false }
|
|
114
|
-
|
|
118
|
+
const spec = specForModel(model)
|
|
119
|
+
const ratioBased = Math.floor(spec.context * COMPACT_RATIO)
|
|
120
|
+
// 大窗口模型(1M)按比例算出来太大,用 cap 限制——宁可早压缩也别让历史涨到打爆 TPM
|
|
121
|
+
const value = Math.min(ratioBased, COMPACT_CAP_TOKENS)
|
|
122
|
+
return { value, auto: true }
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Coding discipline (rigor over speed—tokens spent on verification are well spent):
|
|
2
|
+
- Spec before code: when the user describes a feature request without specifying the details (retry count? timeout? which error types? which files?), ask clarifying questions before writing code.
|
|
3
|
+
- Do not silently invent defaults. Do not guess the user's intent from a one-liner. A wrong assumption costs more than the round-trip to clarify.
|
|
4
|
+
- Save key design decisions to memory_put as you make them — architecture choices, API contracts, naming conventions, trade-off reasoning. Context compression may summarize earlier work into a few lines; memory entries survive compression and get re-injected so later turns don't operate on lost assumptions.
|
|
5
|
+
- Before fixing a bug, find the root cause: read the error output, reproduce it, trace the code path. Don't patch symptoms.
|
|
6
|
+
- When you're stuck, see an unfamiliar pattern, or suspect a project-specific convention — call memory_search before guessing. The injected memories are only top-3 by relevance; the answer may be deeper in the index.
|
|
7
|
+
- Match the surrounding code: comment density, naming, structure. Prefer the project's existing patterns over your own defaults.
|
|
8
|
+
- Before using a library or utility, confirm the project already depends on it (check imports, manifest, lockfile). If it's missing, surface that instead of silently adding a dependency.
|
|
9
|
+
- When you need facts that may be outdated in your training data—API docs, framework versions, language features, npm packages, CLI flags, pricing, CVEs, platform differences—verify with authoritative sources first: read the project's own files (package.json, lockfile), check official docs (websearch/fetch), or test the actual environment. If findings contradict your training data, save the corrected fact to project memory so future sessions benefit.
|
|
10
|
+
- Refactoring: update every caller when an interface changes; never change existing test logic just to make tests pass.
|
|
11
|
+
- Deliver complete changes: no placeholder stubs, no "// rest unchanged", no TODO gaps left for the user to fill in.
|
|
12
|
+
- Before finalizing any implementation, pause and think through edge cases: what could go wrong? what happens on failure? what boundary conditions exist? Reason about the failure modes — then handle or document the fallback. "It works on my machine" is not completion.
|
|
13
|
+
- After changing behavior, sweep comments and docstrings that now describe the old behavior and bring them in line with the code.
|
|
14
|
+
- Before your final reply, re-read the user's latest request and confirm you are answering that one—not an earlier ask left over from a steer or compaction.
|
|
15
|
+
|
|
16
|
+
Testing discipline (right check at the right time — don't run the full suite for every line change):
|
|
17
|
+
- After every write/edit of .mjs/.js files: call syntax_check immediately — it catches parse errors in milliseconds
|
|
18
|
+
- Before declaring a coding task complete: call verify — it checks syntax on all changed files, shows git diff, and displays a self-review checklist. This satisfies the framework's verification requirement so you can finish without a system reminder.
|
|
19
|
+
- Run the full test suite (verify with full=true, or npm test directly) only when:
|
|
20
|
+
a) You're about to commit or publish — final gate before code ships
|
|
21
|
+
b) You changed core infrastructure behavior (agent loop, provider protocol, config schema, tool execution, memory schema) — not just touched the file
|
|
22
|
+
c) The user explicitly asks you to run tests
|
|
23
|
+
- If verify reports syntax errors or test failures, fix them before claiming completion — never mark work done with known failures
|
|
24
|
+
- When you change behavior or add code, add at least one test that covers the change. If the project has no test suite yet, note that in your report. Never skip this step — untested code is incomplete code.
|
|
25
|
+
|
|
26
|
+
Debugging strategy (when something goes wrong, diagnose before treating):
|
|
27
|
+
- Read the FULL error output — the root cause is often at the end, not the first line
|
|
28
|
+
- Don't change multiple things at once hoping one works — that destroys the signal
|
|
29
|
+
- Narrow down systematically: reproduce the failure in isolation, read the file you just wrote to confirm it matches your intent, trace the control flow with grep or code_search, then fix ONE thing and re-run
|
|
30
|
+
- If the error message is unclear, search the web for it before guessing at a fix
|
package/src/explore-overlay.md
CHANGED
|
@@ -11,4 +11,5 @@ Guidelines:
|
|
|
11
11
|
- Use WebSearch or Fetch when external context is needed (docs, error messages)
|
|
12
12
|
- Issue parallel tool calls whenever possible — read multiple files at once
|
|
13
13
|
- Complete the search efficiently and report findings in a structured format
|
|
14
|
+
- If the expected pattern doesn't exist, report that explicitly: what you searched for, which tools you used, and that nothing matched. "Probably there" is not a finding — only report what you actually saw.
|
|
14
15
|
- If something is ambiguous, note it in your report; do not ask the user
|
package/src/memory.mjs
CHANGED
|
@@ -819,6 +819,94 @@ function _upsertDocFile(memory, origin, rel, lines, mtimeMs) {
|
|
|
819
819
|
}
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
/**
|
|
823
|
+
* git 驱动增量索引:用 git diff 找出上次索引以来的变更文件,
|
|
824
|
+
* 只重建这些文件的 FTS5 块(不碰向量)。比全量 mtime 扫描快一个数量级。
|
|
825
|
+
* 返回 { updated, removed, skipped } 或 null(git 不可用时,调用方退到 codeSync)。
|
|
826
|
+
*/
|
|
827
|
+
export async function gitSync(memory, dir, { onProgress } = {}) {
|
|
828
|
+
const { execSync } = await import("node:child_process")
|
|
829
|
+
const opts = { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 }
|
|
830
|
+
|
|
831
|
+
let head
|
|
832
|
+
try { head = execSync("git rev-parse HEAD", opts).trim() } catch { return null }
|
|
833
|
+
|
|
834
|
+
const stored = memory.db.prepare(`SELECT value FROM meta WHERE key = 'last_indexed_commit'`).get()?.value
|
|
835
|
+
if (!stored) return null // 首次运行,走全量 codeSync
|
|
836
|
+
|
|
837
|
+
// 取两个 diff 的并集:已提交的变更(pull/merge)+ 工作区脏文件(用户在外部编辑器改的)
|
|
838
|
+
let diffOut
|
|
839
|
+
try {
|
|
840
|
+
// 包含 D(删除):外部删文件也要清理索引块
|
|
841
|
+
const committed = execSync(`git diff --name-only --diff-filter=ACMRTD ${stored} HEAD`, opts).trim()
|
|
842
|
+
const dirty = execSync(`git diff --name-only --diff-filter=ACMRTD`, opts).trim()
|
|
843
|
+
const lines = [...new Set([...committed.split("\n").filter(Boolean), ...dirty.split("\n").filter(Boolean)])]
|
|
844
|
+
diffOut = lines
|
|
845
|
+
} catch {
|
|
846
|
+
// rebase / shallow clone 导致旧 commit 不可达 → 退到全量
|
|
847
|
+
return null
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (diffOut.length > 200) {
|
|
851
|
+
// 大范围变更(分支切换等)→ 退到 codeSync,它有更好的进度反馈
|
|
852
|
+
return null
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
let updated = 0, removed = 0, skipped = 0, failed = 0
|
|
856
|
+
const errors = []
|
|
857
|
+
for (let i = 0; i < diffOut.length; i++) {
|
|
858
|
+
const rel = diffOut[i].replaceAll("\\", "/")
|
|
859
|
+
const abs = join(dir, rel)
|
|
860
|
+
const ext = rel.slice(rel.lastIndexOf(".")).toLowerCase()
|
|
861
|
+
|
|
862
|
+
// 跳过隐藏目录和 SKIP_DIRS 里的文件
|
|
863
|
+
const pathDirs = rel.split("/")
|
|
864
|
+
if (pathDirs.some((d) => SKIP_DIRS.has(d) || d.startsWith("."))) continue
|
|
865
|
+
|
|
866
|
+
if (!CODE_EXTS.has(ext) && !DOC_EXTS.has(ext)) { skipped++; continue }
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
const text = await readFile(abs, "utf8")
|
|
870
|
+
const lines = text.split("\n")
|
|
871
|
+
if (CODE_EXTS.has(ext)) {
|
|
872
|
+
const lang = detectLanguage(abs)
|
|
873
|
+
let mtimeMs = 0
|
|
874
|
+
try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* 新文件 */ }
|
|
875
|
+
_upsertCodeFile(memory, dir, rel, lines, lang, mtimeMs)
|
|
876
|
+
} else {
|
|
877
|
+
let mtimeMs = 0
|
|
878
|
+
try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { /* 新文件 */ }
|
|
879
|
+
_upsertDocFile(memory, dir, rel, lines, mtimeMs)
|
|
880
|
+
}
|
|
881
|
+
updated++
|
|
882
|
+
} catch (e) {
|
|
883
|
+
// readFile 失败 = 文件已删 → 清理索引块;
|
|
884
|
+
// 其他错误(权限/SQLite)不推进锚点——下次 gitSync 会重试
|
|
885
|
+
const isDeleted = e.code === "ENOENT"
|
|
886
|
+
if (isDeleted) {
|
|
887
|
+
if (CODE_EXTS.has(ext)) memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
|
|
888
|
+
else memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
|
|
889
|
+
removed++
|
|
890
|
+
} else {
|
|
891
|
+
failed++
|
|
892
|
+
if (errors.length < 5) errors.push(`${rel}: ${e.message}`)
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (onProgress && i % 5 === 0) {
|
|
896
|
+
onProgress({ phase: "index", current: i + 1, total: diffOut.length, updated, removed, skipped })
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 更新锚点(有 failed 时不推进——下次 gitSync 会重试这些文件)
|
|
901
|
+
if (failed === 0) {
|
|
902
|
+
memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
|
|
903
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
onProgress?.({ phase: "done", total: diffOut.length, updated, removed, skipped, failed })
|
|
907
|
+
return { updated, removed, skipped, failed, errors }
|
|
908
|
+
}
|
|
909
|
+
|
|
822
910
|
/**
|
|
823
911
|
* 同步代码索引:扫描 dir 下所有源文件 → 分块 → upsert 到 code_chunks。
|
|
824
912
|
* 按 mtime 增量——只重建变更过的文件块。
|
|
@@ -891,9 +979,21 @@ export async function codeSync(memory, dir, { onProgress } = {}) {
|
|
|
891
979
|
}
|
|
892
980
|
|
|
893
981
|
onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed })
|
|
982
|
+
// 全量同步成功后记录 git 锚点(下次启动 gitSync 用它做增量 diff 基准)
|
|
983
|
+
markIndexedCommit(memory, dir)
|
|
894
984
|
return { updated, removed, skipped, failed, errors, total: files.length }
|
|
895
985
|
}
|
|
896
986
|
|
|
987
|
+
/** 记录当前 HEAD 作为索引锚点(gitSync 增量 diff 基准);非 git 仓库静默跳过 */
|
|
988
|
+
async function markIndexedCommit(memory, dir) {
|
|
989
|
+
try {
|
|
990
|
+
const { execSync } = await import("node:child_process")
|
|
991
|
+
const head = execSync("git rev-parse HEAD", { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim()
|
|
992
|
+
memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
|
|
993
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
|
|
994
|
+
} catch { /* 非 git 仓库或 git 不可用,跳过 */ }
|
|
995
|
+
}
|
|
996
|
+
|
|
897
997
|
/**
|
|
898
998
|
* 代码检索:FTS5(BM25) + 可选向量余弦,RRF 合并。
|
|
899
999
|
* 无 embedder 时退化为纯 FTS;ftsQuery 为空(纯标点查询)且有 embedder 时退化为纯向量。
|
|
@@ -1023,6 +1123,10 @@ export async function reindexFile(memory, cwd, absPath) {
|
|
|
1023
1123
|
try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* 新文件 */ }
|
|
1024
1124
|
_upsertDocFile(memory, cwd, rel, lines, mtimeMs)
|
|
1025
1125
|
}
|
|
1126
|
+
// 立即补算向量,不等惰性检索(刚改的文件应该有语义搜索能力)
|
|
1127
|
+
if (memory.embedder) {
|
|
1128
|
+
try { await ensureEmbeddings(memory) } catch { /* embedding 失败不阻塞 */ }
|
|
1129
|
+
}
|
|
1026
1130
|
}
|
|
1027
1131
|
|
|
1028
1132
|
// ========== 文档索引 ==========
|
|
@@ -1121,6 +1225,7 @@ export async function docSync(memory, dir, { onProgress } = {}) {
|
|
|
1121
1225
|
}
|
|
1122
1226
|
|
|
1123
1227
|
onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed })
|
|
1228
|
+
markIndexedCommit(memory, dir)
|
|
1124
1229
|
return { updated, removed, skipped, failed, errors, total: files.length }
|
|
1125
1230
|
}
|
|
1126
1231
|
|
package/src/plan-overlay.md
CHANGED
|
@@ -8,6 +8,7 @@ Guidelines:
|
|
|
8
8
|
3. Your plan—preliminary if questions remain, final if context is sufficient
|
|
9
9
|
- Ground the plan in reality: cite real file paths and line numbers, name actual functions and modules. No invented architecture.
|
|
10
10
|
- Make steps concrete and verifiable: each step small enough to check, ordered so dependencies come first.
|
|
11
|
+
- Identify edge cases and failure modes in the plan. What boundary conditions does the implementation need to handle? Each step that encounters a risk must specify its fallback — not "handle error", but the concrete recovery path.
|
|
11
12
|
- Where a real design choice exists, call out the trade-offs and recommend ONE option with reasoning—don't list possibilities without taking a stance.
|
|
12
13
|
- Keep scope minimal: the plan should solve the task, not redesign the codebase. Prefer modifying existing files over creating new ones—new files should only appear when the task genuinely demands a new module. List every file that will be modified, so the implementer knows the blast radius.
|
|
13
14
|
- If something is ambiguous, note it in the plan; do not ask the user.
|
package/src/provider.mjs
CHANGED
|
@@ -353,11 +353,12 @@ async function readSSE(response, { onToken, onReasoning }) {
|
|
|
353
353
|
result.content += delta.content
|
|
354
354
|
onToken?.(delta.content)
|
|
355
355
|
}
|
|
356
|
-
// tool_calls 按 index 分槽累积,
|
|
356
|
+
// tool_calls 按 index 分槽累积,arguments 是分片到达的需拼接;
|
|
357
|
+
// name 个别 API(GLM 偶尔)会重发完整 name 而非增量,用 += 会拼成 "readread"——只取第一次非空值
|
|
357
358
|
for (const tc of delta.tool_calls ?? []) {
|
|
358
359
|
const slot = (result.toolCalls[tc.index] ??= { id: "", name: "", arguments: "" })
|
|
359
360
|
if (tc.id) slot.id = tc.id
|
|
360
|
-
if (tc.function?.name) slot.name
|
|
361
|
+
if (tc.function?.name && !slot.name) slot.name = tc.function.name
|
|
361
362
|
if (tc.function?.arguments) slot.arguments += tc.function.arguments
|
|
362
363
|
}
|
|
363
364
|
}
|
package/src/tui.mjs
CHANGED
|
@@ -279,6 +279,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
279
279
|
currentTool: null, // 正在执行的工具名 (状态栏显示)
|
|
280
280
|
processingStarted: 0, // 本轮处理开始时间 (状态栏计时)
|
|
281
281
|
status: "Ready",
|
|
282
|
+
queue: [], // 处理中排队的待执行消息:[{ text }],处理完自动取下一条
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
// 恢复的会话如果所有任务completed,自动收起 todo 面板 (对齐运行时行为)
|
|
@@ -553,6 +554,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
553
554
|
}
|
|
554
555
|
}
|
|
555
556
|
|
|
557
|
+
// 队列预览 (暗色,紧挨输入框上方):与子 agent 面板/权限预览共享输入框上方空间
|
|
558
|
+
// 只在 processing 时显示(非 processing 时队列应为空),且最多 1 行预览避免挤压对话区
|
|
559
|
+
if (state.queue.length > 0 && state.processing) {
|
|
560
|
+
const preview = sliceByWidth(state.queue[0].text, W - 20)
|
|
561
|
+
out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D del${ansi.reset}${ansi.clearLine}`)
|
|
562
|
+
}
|
|
563
|
+
|
|
556
564
|
// 输入框 (全边框,宽 W)
|
|
557
565
|
let borderColor = C.tool
|
|
558
566
|
let title
|
|
@@ -663,7 +671,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
663
671
|
? ` │ ${ansi.reset}${C.warn}ctx ${ctxPct}%${ansi.reset}${ansi.dim}`
|
|
664
672
|
: ` │ ctx ${ctxPct}%`
|
|
665
673
|
: ""
|
|
666
|
-
|
|
674
|
+
const queueHint = state.queue.length > 0 ? ` │ queue: ${state.queue.length}` : ""
|
|
675
|
+
statusLine = ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
|
|
667
676
|
}
|
|
668
677
|
const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
|
|
669
678
|
const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
|
|
@@ -679,8 +688,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
679
688
|
process.stdout.write(frame)
|
|
680
689
|
}
|
|
681
690
|
|
|
682
|
-
// 光标:输入态定位到输入框内 (IME
|
|
683
|
-
if (state.
|
|
691
|
+
// 光标:输入态定位到输入框内 (IME 候选框跟随真实光标);权限确认/菜单态时隐藏
|
|
692
|
+
if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
|
|
684
693
|
process.stdout.write(ansi.hideCursor)
|
|
685
694
|
} else {
|
|
686
695
|
const cursorRow = 1 + convH + pickerH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
|
|
@@ -695,20 +704,43 @@ export async function startTUI(agent, opts = {}) {
|
|
|
695
704
|
|
|
696
705
|
async function submit() {
|
|
697
706
|
const text = state.input.join("").trim()
|
|
698
|
-
if (!text
|
|
707
|
+
if (!text) return
|
|
699
708
|
state.input = []
|
|
700
709
|
state.cursor = 0
|
|
701
710
|
state.history.push(text)
|
|
702
711
|
state.historyIndex = -1
|
|
703
712
|
state.scroll = 0
|
|
704
713
|
|
|
705
|
-
// 斜杠Commands:本地处理,不进入 agent
|
|
714
|
+
// 斜杠Commands:本地处理,不进入 agent(处理中也允许执行部分命令如 /cancel)
|
|
706
715
|
if (text.startsWith("/")) {
|
|
716
|
+
if (state.processing) {
|
|
717
|
+
// 处理中只允许取消当前任务,其他命令排队
|
|
718
|
+
if (text === "/cancel" || text === "/exit") {
|
|
719
|
+
await handleSlash(text)
|
|
720
|
+
} else {
|
|
721
|
+
state.queue.push({ text })
|
|
722
|
+
render()
|
|
723
|
+
}
|
|
724
|
+
return
|
|
725
|
+
}
|
|
707
726
|
await handleSlash(text)
|
|
708
727
|
return
|
|
709
728
|
}
|
|
710
729
|
|
|
711
|
-
|
|
730
|
+
// 处理中:入队等待,不立即执行
|
|
731
|
+
if (state.processing) {
|
|
732
|
+
state.queue.push({ text })
|
|
733
|
+
pushLabel(`❯ You: (queued #${state.queue.length})`, ansi.bold + C.user)
|
|
734
|
+
pushLine(text, C.dim)
|
|
735
|
+
render()
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
await runAgentTurn(text)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** 执行一轮 agent 对话(从 submit 或队列取出调用) */
|
|
743
|
+
async function runAgentTurn(text) {
|
|
712
744
|
pushLine(text, C.text)
|
|
713
745
|
|
|
714
746
|
// 任务开始前自动打存档点 (git 仓库内;失败静默,不挡任务)
|
|
@@ -894,6 +926,24 @@ export async function startTUI(agent, opts = {}) {
|
|
|
894
926
|
// 存失败不打断使用
|
|
895
927
|
}
|
|
896
928
|
render()
|
|
929
|
+
|
|
930
|
+
// 队列里有待执行消息:自动取下一条执行
|
|
931
|
+
if (state.queue.length > 0) {
|
|
932
|
+
const next = state.queue.shift()
|
|
933
|
+
// 队列里的斜杠命令直接执行
|
|
934
|
+
if (next.text.startsWith("/")) {
|
|
935
|
+
await handleSlash(next.text)
|
|
936
|
+
render()
|
|
937
|
+
// 斜杠命令执行完也继续检查队列
|
|
938
|
+
if (state.queue.length > 0 && !state.processing) {
|
|
939
|
+
const next2 = state.queue.shift()
|
|
940
|
+
await runAgentTurn(next2.text)
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
944
|
+
await runAgentTurn(next.text)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
897
947
|
}
|
|
898
948
|
|
|
899
949
|
function flushStream() {
|
|
@@ -1117,25 +1167,25 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1117
1167
|
total += s.added
|
|
1118
1168
|
pushLine(` team: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
|
|
1119
1169
|
}
|
|
1120
|
-
//
|
|
1121
|
-
pushLine(` [code] Rebuilding
|
|
1122
|
-
const cr = await
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1170
|
+
// 重建代码索引和文档索引并行(读写不同表,WAL 支持)
|
|
1171
|
+
pushLine(` [code+doc] Rebuilding indexes...`, C.tool)
|
|
1172
|
+
const [cr, dr] = await Promise.all([
|
|
1173
|
+
codeSync(agent.memory, agent.cwd, {
|
|
1174
|
+
onProgress: (p) => {
|
|
1175
|
+
if (p.phase === "index" && p.current % 20 === 0) {
|
|
1176
|
+
pushLine(` code: ${p.current}/${p.total}`, C.dim)
|
|
1177
|
+
}
|
|
1126
1178
|
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
onProgress: (p) => {
|
|
1134
|
-
if (p.phase === "index" && p.current % 5 === 0) {
|
|
1135
|
-
pushLine(` Indexing... ${p.current}/${p.total}`, C.dim)
|
|
1179
|
+
}),
|
|
1180
|
+
docSync(agent.memory, agent.cwd, {
|
|
1181
|
+
onProgress: (p) => {
|
|
1182
|
+
if (p.phase === "index" && p.current % 5 === 0) {
|
|
1183
|
+
pushLine(` doc: ${p.current}/${p.total}`, C.dim)
|
|
1184
|
+
}
|
|
1136
1185
|
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1186
|
+
}),
|
|
1187
|
+
])
|
|
1188
|
+
pushLine(` code: ${cr.total} files, +${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
|
|
1139
1189
|
pushLine(` doc: ${dr.total} files, +${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
|
|
1140
1190
|
pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
|
|
1141
1191
|
return
|
|
@@ -2286,7 +2336,19 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2286
2336
|
return
|
|
2287
2337
|
}
|
|
2288
2338
|
|
|
2289
|
-
if (state.processing)
|
|
2339
|
+
if (state.processing) {
|
|
2340
|
+
// 处理中允许输入(排队),但屏蔽方向键历史和 Tab 补全
|
|
2341
|
+
if (key.name === "tab" || key.name === "up" || key.name === "down") return
|
|
2342
|
+
// Ctrl+D:删除队列中最后一条
|
|
2343
|
+
if (key.ctrl && key.name === "d") {
|
|
2344
|
+
if (state.queue.length > 0) {
|
|
2345
|
+
state.queue.pop()
|
|
2346
|
+
render()
|
|
2347
|
+
}
|
|
2348
|
+
return
|
|
2349
|
+
}
|
|
2350
|
+
// 其余可打印字符正常进入输入框
|
|
2351
|
+
}
|
|
2290
2352
|
|
|
2291
2353
|
// Tab:斜杠Commands补全 (循环候选);其余输入忽略 (\t 会顶破输入框,永不直接插入)
|
|
2292
2354
|
if (key.name === "tab") {
|
|
@@ -2423,36 +2485,56 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2423
2485
|
render()
|
|
2424
2486
|
|
|
2425
2487
|
// 后台索引 (进界面后再跑,不阻塞启动);进度走底部状态栏,不往对话区塞行
|
|
2488
|
+
// 优先用 git diff 增量(快),git 不可用或首次运行时退到全量扫描
|
|
2426
2489
|
;(async () => {
|
|
2427
|
-
const { codeSync, docSync } = await import("./memory.mjs")
|
|
2490
|
+
const { codeSync, docSync, gitSync } = await import("./memory.mjs")
|
|
2428
2491
|
const cwd = agent.cwd
|
|
2429
2492
|
let codeFiles = 0, docFiles = 0
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2493
|
+
|
|
2494
|
+
state.status = "Indexing..."
|
|
2495
|
+
render()
|
|
2496
|
+
|
|
2497
|
+
const gitRes = await gitSync(agent.memory, cwd, {
|
|
2498
|
+
onProgress: (p) => {
|
|
2499
|
+
if (p.phase === "index" && p.current % 5 === 0) {
|
|
2500
|
+
state.status = `Indexing... ${p.current}/${p.total}`
|
|
2501
|
+
render()
|
|
2439
2502
|
}
|
|
2440
|
-
}
|
|
2503
|
+
}
|
|
2504
|
+
})
|
|
2505
|
+
|
|
2506
|
+
if (gitRes !== null) {
|
|
2507
|
+
// git 增量成功,直接统计
|
|
2441
2508
|
codeFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM code_chunks`).get()?.n ?? 0
|
|
2442
|
-
} catch { /* 不阻塞 */ }
|
|
2443
|
-
try {
|
|
2444
|
-
state.status = "Indexing docs..."
|
|
2445
|
-
render()
|
|
2446
|
-
await docSync(agent.memory, cwd, {
|
|
2447
|
-
onProgress: (p) => {
|
|
2448
|
-
if (p.phase === "index" && p.current % 10 === 0) {
|
|
2449
|
-
state.status = `Indexing docs... ${p.current}/${p.total}`
|
|
2450
|
-
render()
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
})
|
|
2454
2509
|
docFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM doc_chunks`).get()?.n ?? 0
|
|
2455
|
-
}
|
|
2510
|
+
} else {
|
|
2511
|
+
// 退到全量扫描(codeSync 和 docSync 并行——读写不同表,SQLite WAL 天然支持)
|
|
2512
|
+
const [codeRes, docRes] = await Promise.allSettled([
|
|
2513
|
+
codeSync(agent.memory, cwd, {
|
|
2514
|
+
onProgress: (p) => {
|
|
2515
|
+
if (p.phase === "index" && p.current % 30 === 0) {
|
|
2516
|
+
state.status = `Indexing code... ${p.current}/${p.total}`
|
|
2517
|
+
render()
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}),
|
|
2521
|
+
docSync(agent.memory, cwd, {
|
|
2522
|
+
onProgress: (p) => {
|
|
2523
|
+
if (p.phase === "index" && p.current % 10 === 0) {
|
|
2524
|
+
state.status = `Indexing docs... ${p.current}/${p.total}`
|
|
2525
|
+
render()
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}),
|
|
2529
|
+
])
|
|
2530
|
+
if (codeRes.status === "fulfilled") {
|
|
2531
|
+
codeFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM code_chunks`).get()?.n ?? 0
|
|
2532
|
+
}
|
|
2533
|
+
if (docRes.status === "fulfilled") {
|
|
2534
|
+
docFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM doc_chunks`).get()?.n ?? 0
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2456
2538
|
state.status = codeFiles || docFiles
|
|
2457
2539
|
? `Ready — idx code ${codeFiles} doc ${docFiles}`
|
|
2458
2540
|
: "Ready"
|