thincoder 0.12.8 → 0.12.10
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 +13 -0
- package/package.json +1 -1
- package/src/advisor/history.mjs +2 -2
- package/src/advisor/messages.mjs +6 -6
- package/src/advisor/repos.mjs +2 -2
- package/src/advisor/run.mjs +18 -12
- package/src/advisor.mjs +8 -24
- package/src/agent-tools/advisor.mjs +2 -3
- package/src/agent-tools/plan.mjs +3 -3
- package/src/agent-tools/subagent.mjs +2 -3
- package/src/agent.mjs +1 -2
- package/src/context.mjs +1 -1
- package/src/prompts/advisor-round1.md +1 -1
- package/src/prompts/advisor-round2.md +5 -5
- package/src/prompts/advisor-round3.md +5 -5
- package/src/prompts/discipline.md +4 -1
- package/src/prompts/engineering.md +6 -8
- package/src/prompts/system.md +1 -2
- package/src/tui/agent-turn.mjs +18 -0
- package/src/tui/ansi.mjs +1 -1
- package/src/tui/clipboard.mjs +0 -20
- package/src/tui/index.mjs +3 -2
- package/src/tui/mouse.mjs +15 -47
- package/src/tui/render-conversation.mjs +53 -16
package/README.md
CHANGED
|
@@ -209,6 +209,19 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
|
|
|
209
209
|
|
|
210
210
|
## Changelog
|
|
211
211
|
|
|
212
|
+
### 0.12.10 (2026-08)
|
|
213
|
+
- **Code-quality pass (advisor subsystem):**
|
|
214
|
+
- **Drop 11 unused exports** — internal-use symbols no longer leak through the module API (advisor table headers/constants, plan reminders, token-UUID helper, shrinkOversized).
|
|
215
|
+
- **Advisor tool-timeout timer is now cleared** when the tool wins the race (no dangling timers); static import replaces a dynamic import on the hot path.
|
|
216
|
+
- **Advisor re-review no longer trusts git output** — the follow-up path previously injected a `git diff HEAD` snapshot: once fixes were committed the diff was empty and the model read "no changes" as "no fixes", misreporting fixed items as unfixed. The follow-up now injects **no git information at all** — verification is `read`-only, evidence must quote this round's read output (line numbers from the stale prior table are not evidence), and dead snapshot-dedup fields were removed.
|
|
217
|
+
|
|
218
|
+
### 0.12.9 (2026-08)
|
|
219
|
+
- **Prompt-system quality pass (both CLI and VS Code extension, byte-identical sync):**
|
|
220
|
+
- **Advisor-after-code rule moved from system.md to discipline.md** — engineering mode no longer receives the conflicting "call advisor after changing code" instruction (its review-timing rules say do not call unprompted). Standard mode behavior unchanged.
|
|
221
|
+
- **engineering.md delivery-review semantics unified** — the mandatory-flow step and the state table now both say: eng-coder self-reviews inside the subagent; the architect verifies against acceptance criteria and re-reviews only when asked or when the delivery looks wrong. (Previously the step forced a parent-side advisor code review that the hard rules contradicted.)
|
|
222
|
+
- **checkpoint description aligned with actual auto-snapshot triggers** (task-list deletion + context compaction; manual checkpoint for the rest).
|
|
223
|
+
- **advisor round budget wording fixed** — prompts advertise a 30-round budget; the mechanical hard cap is 100 rounds (loop guard). Both layers are now named explicitly.
|
|
224
|
+
|
|
212
225
|
### 0.12.8 (2026-08)
|
|
213
226
|
- **Fix: pending-task pushback fires at most once** — the completion guard that reminds the model to update pending tasks before finishing could loop forever when a pending item could not be resolved. Now each task-list state earns exactly one reminder; if the model insists on finishing anyway, it is allowed to (updating the list via the task tool resets the budget). VS Code extension synced.
|
|
214
227
|
|
package/package.json
CHANGED
package/src/advisor/history.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import { join } from "node:path"
|
|
|
6
6
|
|
|
7
7
|
export const ADVISOR_MD_PATH = ".thincoder/advisor.md"
|
|
8
8
|
export const ADVISOR_TABLE_HEADER = "| # | File | Severity | Issue | Suggestion |"
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const CONVERGENCE_TABLE_HEADER = "| # | Orig# | File | Severity | Status | Notes |"
|
|
10
|
+
const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
|
|
11
11
|
export const LEGACY_ADVISOR_HEADER = "| # | 文件 | 严重程度 | 问题描述 | 建议修复 |"
|
|
12
12
|
|
|
13
13
|
const DEFAULT_CRITERIA = `Review the code changes, focusing on:
|
package/src/advisor/messages.mjs
CHANGED
|
@@ -57,7 +57,6 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Pre-collected changes — the design doc diff.
|
|
60
|
-
// _advisorLastSnapshot is only consumed by code-review convergence — skip the write here.
|
|
61
60
|
const snapshots = collectRepoSnapshots(repos, agent.cwd)
|
|
62
61
|
if (snapshots.length > 0) {
|
|
63
62
|
parts.push("## Design Document (git diff)")
|
|
@@ -96,7 +95,7 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
96
95
|
return parts.join("\n")
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
// Convergence data (round 2+)
|
|
98
|
+
// Convergence data (round 2+). Design reviews returned above — only code reviews reach here.
|
|
100
99
|
if (prior && (agent._advisorRound || 0) > 0) {
|
|
101
100
|
const response = extractAgentResponseTable(agent.history, prior.sinceIdx)
|
|
102
101
|
|| "(Agent did not provide a response table — re-evaluate each issue)"
|
|
@@ -164,10 +163,11 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
164
163
|
parts.push("## Instructions")
|
|
165
164
|
parts.push("1. IMPORTANT: in the diff, `-` lines are REMOVED content (no longer in the file), `+` lines are ADDED. The prior issue table (if any) is HISTORY — always verify current file state with `read` before judging an item.")
|
|
166
165
|
if (isReReview) {
|
|
167
|
-
parts.push("2. STALE-CONTEXT WARNING: any diff embedded in earlier messages is a historical snapshot — treat it as expired. Only
|
|
168
|
-
parts.push("3.
|
|
169
|
-
parts.push("4. `read`
|
|
170
|
-
parts.push("5.
|
|
166
|
+
parts.push("2. STALE-CONTEXT WARNING: any diff or file content embedded in earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state. Never quote a `-` line from an earlier diff as if it were live code.")
|
|
167
|
+
parts.push("3. Verify the prior issue table against the CURRENT FILE STATE — use `read`, never `git diff` alone. Fixes may already be committed: an empty `git diff` does NOT mean nothing changed. `git log -3` shows recent commits.")
|
|
168
|
+
parts.push("4. `read` the files in the Review Scope in full — ALWAYS, regardless of what `git diff` shows. Batch reads/greps in a single reply.")
|
|
169
|
+
parts.push("5. Evidence rule: every 'Unfixed'/'New' finding MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may come from the stale prior table. Findings without a fresh quoted line are treated as unverified and will not be accepted.")
|
|
170
|
+
parts.push("6. Produce your verification table. Do not re-read content you already have.")
|
|
171
171
|
} else {
|
|
172
172
|
parts.push("2. Read `AGENTS.md` / design docs only if they exist (check once; do not re-probe with multiple patterns).")
|
|
173
173
|
parts.push("3. `read` changed files for full context beyond the diff. Batch independent reads/greps in a single reply instead of one call per round-trip.")
|
package/src/advisor/repos.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { execFileSync } from "node:child_process"
|
|
6
6
|
import { dirname, basename, resolve } from "node:path"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const GIT_TIMEOUT = 5_000
|
|
9
|
+
const MAX_EMBEDDED_DIFF = 50_000
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Find the git repository roots that contain the agent's touched files.
|
package/src/advisor/run.mjs
CHANGED
|
@@ -8,11 +8,14 @@ import { toOpenAISchema } from "../tools/index.mjs"
|
|
|
8
8
|
import { prepareAdvisorMessages } from "../advisor.mjs"
|
|
9
9
|
import { extractPriorIssueTable } from "../advisor/history.mjs"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const MAX_ADVISOR_TURNS = 100
|
|
12
12
|
// Mechanical convergence cap: the protocol assumes up to 5 rounds suffice
|
|
13
13
|
// (full review, verify+fix cycles, strict verification). A 6th call means the
|
|
14
14
|
// model is looping — refuse it instead of burning tokens on a review that cannot
|
|
15
15
|
// converge. Design reviews are exempt (each call resets the round).
|
|
16
|
+
// NOTE: prompts/advisor-round{1,2,3}.md advertise a 30-round BUDGET — the
|
|
17
|
+
// prompt-level efficiency target, distinct from this 100-round mechanical hard
|
|
18
|
+
// cap (loop guard). Keep both in sync when either changes.
|
|
16
19
|
export const MAX_ADVISOR_ROUNDS = 5
|
|
17
20
|
|
|
18
21
|
// Context window limits
|
|
@@ -169,18 +172,21 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
169
172
|
if (!tool) {
|
|
170
173
|
result = `Error: unknown tool "${tc.name}". Available: ${[...ADVISOR_TOOL_BY_NAME.keys()].join(", ")}`
|
|
171
174
|
} else {
|
|
172
|
-
// Execute with timeout
|
|
175
|
+
// Execute with timeout (clear the timer when the tool wins the race —
|
|
176
|
+
// otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
|
|
173
177
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
onOutput,
|
|
178
|
-
signal,
|
|
178
|
+
let timeoutId
|
|
179
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
180
|
+
timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
|
|
179
181
|
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
try {
|
|
183
|
+
result = await Promise.race([
|
|
184
|
+
tool.execute(args, { cwd, agent, onOutput, signal }),
|
|
185
|
+
timeoutPromise,
|
|
186
|
+
])
|
|
187
|
+
} finally {
|
|
188
|
+
clearTimeout(timeoutId)
|
|
189
|
+
}
|
|
184
190
|
} catch (e) {
|
|
185
191
|
const errorType = e.message.includes("timeout") ? "timeout"
|
|
186
192
|
: e.message.includes("ENOENT") ? "file_not_found"
|
|
@@ -221,7 +227,7 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
221
227
|
}
|
|
222
228
|
|
|
223
229
|
/** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
|
|
224
|
-
|
|
230
|
+
function resolveAdvisorProvider(agent) {
|
|
225
231
|
const cfg = agent.config?.advisor
|
|
226
232
|
if (cfg?.provider) {
|
|
227
233
|
try {
|
package/src/advisor.mjs
CHANGED
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
import { readFileSync } from "node:fs"
|
|
38
38
|
import { join, dirname } from "node:path"
|
|
39
39
|
import { fileURLToPath } from "node:url"
|
|
40
|
-
import {
|
|
41
|
-
import { findReviewRepos, collectRepoSnapshots } from "./advisor/repos.mjs"
|
|
40
|
+
import { findReviewRepos } from "./advisor/repos.mjs"
|
|
42
41
|
import { extractPriorIssueTable, extractAgentResponseTable } from "./advisor/history.mjs"
|
|
43
42
|
import { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
44
43
|
// Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
|
|
@@ -111,31 +110,18 @@ export function buildAdvisorFollowUp(agent, _prior) {
|
|
|
111
110
|
"## Instructions",
|
|
112
111
|
round === 2
|
|
113
112
|
? "Verify each item in the prior table. Flag any obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style). Produce a verification table."
|
|
114
|
-
: "Strictly verify ONLY the items in the prior table against the
|
|
113
|
+
: "Strictly verify ONLY the items in the prior table against the CURRENT FILE STATE (use `read` — an empty diff does not mean the fixes are absent). Do NOT look for new issues.",
|
|
115
114
|
"",
|
|
116
|
-
"IMPORTANT:
|
|
115
|
+
"IMPORTANT: the prior issue table is HISTORY — always verify current file state with `read` before judging an item as fixed or unfixed.",
|
|
117
116
|
// Round-aware evidence rule: "New" entries only exist in round 2 (round 3+ forbids them).
|
|
118
|
-
`STALE-CONTEXT WARNING:
|
|
117
|
+
`STALE-CONTEXT WARNING: only fresh \`read\` results describe the current state — never judge from earlier snapshots or from \`git diff\` (committed fixes never show in \`git diff HEAD\`). Read the files to verify. Any "Unfixed" entry${round === 2 ? ' (and any "New" entry)' : ""} MUST quote the exact line content from THIS round's \`read\` output (e.g. \`run.mjs:180: timeoutId = setTimeout(...)\`); line numbers alone are NOT evidence (they may come from the stale prior table). Uncited findings are unverified and will be ignored.`,
|
|
119
118
|
"",
|
|
120
|
-
"Do NOT re-read AGENTS.md / design docs
|
|
119
|
+
"Do NOT re-read AGENTS.md / design docs. Verify fix status with \`read\` only — do not rely on git output: a clean working tree does not mean fixes are absent (they may be committed).",
|
|
121
120
|
"",
|
|
122
121
|
]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Skip re-pushing an identical diff (e.g. advisor re-run without any file changes) —
|
|
127
|
-
// the previous snapshot is already in the conversation, duplicating it wastes tokens.
|
|
128
|
-
if (snapshots.length === 0) {
|
|
129
|
-
parts.push("## Current Changes", "(No git repository or no changes detected.)")
|
|
130
|
-
} else if (snapshotHash && snapshotHash === agent._advisorLastSnapshotHash) {
|
|
131
|
-
// Hash match → the snapshot from the previous round is still current.
|
|
132
|
-
// Using hash instead of full-text comparison avoids keeping the entire diff
|
|
133
|
-
// string in memory and handles edge cases (e.g. file changed then reverted).
|
|
134
|
-
parts.push("## Current Changes", "(No changes since your previous review — the diff snapshot is identical, so the one from your last round remains valid for this round.)")
|
|
135
|
-
} else {
|
|
136
|
-
parts.push("## Current Changes (git status + git diff HEAD, refreshed)", ...snapshots)
|
|
137
|
-
}
|
|
138
|
-
agent._advisorLastSnapshotHash = snapshotHash
|
|
122
|
+
// Deliberately NO git information injected here (no diff snapshot, no git context):
|
|
123
|
+
// git output misled re-reviews — committed fixes never show in `git diff HEAD`, so
|
|
124
|
+
// the model read "no changes" as "no fixes". Verification is `read`-only by design.
|
|
139
125
|
return parts.join("\n")
|
|
140
126
|
}
|
|
141
127
|
|
|
@@ -165,7 +151,6 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
165
151
|
// a follow-up "Verify Prior Table" would be meaningless; start a fresh full review
|
|
166
152
|
if (!prior) {
|
|
167
153
|
agent._advisorSession = null
|
|
168
|
-
agent._advisorLastSnapshotHash = null
|
|
169
154
|
session = null
|
|
170
155
|
// Only reset the round counter on a truly fresh start (no prior reviews at all).
|
|
171
156
|
// If _advisorRound > 0, there WAS a prior review — it just passed (all-clear).
|
|
@@ -188,7 +173,6 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
188
173
|
// Fresh session. Only reset round if this is truly the first review.
|
|
189
174
|
// If _advisorRound > 0, there was a prior review that passed (all-clear).
|
|
190
175
|
if (!agent._advisorRound) agent._advisorRound = 0
|
|
191
|
-
agent._advisorLastSnapshot = null
|
|
192
176
|
if (!prior) {
|
|
193
177
|
// Tell the advisor why no prior issue table is present
|
|
194
178
|
session[1] = {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID, createHmac } from "node:crypto"
|
|
7
7
|
import { runAdvisorReview } from "../advisor/run.mjs"
|
|
8
|
+
import { isDocFile } from "../advisor/repos.mjs"
|
|
8
9
|
|
|
9
10
|
const TOKEN_EXPIRY_MS = 3600000 // 1 hour
|
|
10
11
|
const TOKEN_SECRET = process.env.THINCODER_TOKEN_SECRET || "thincoder-default-secret"
|
|
@@ -50,7 +51,7 @@ export function validateDesignToken(token) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/** Extract UUID from signed token for regex matching */
|
|
53
|
-
|
|
54
|
+
function extractTokenUUID(token) {
|
|
54
55
|
const parts = token.split(":")
|
|
55
56
|
return parts.length >= 1 ? parts[0] : token
|
|
56
57
|
}
|
|
@@ -118,7 +119,6 @@ export const advisorTool = {
|
|
|
118
119
|
|
|
119
120
|
// Design review: validate that documents are in docs/ or are recognized doc files
|
|
120
121
|
if (reviewType === "design" && documents) {
|
|
121
|
-
const { isDocFile } = await import("../advisor/repos.mjs")
|
|
122
122
|
const invalidDocs = documents.filter((doc) => {
|
|
123
123
|
// Allow docs/ directory and recognized doc files (METHODOLOGY.md, README.md, etc.)
|
|
124
124
|
if (doc.startsWith("docs/") || doc.startsWith("docs\\")) return false
|
|
@@ -133,7 +133,6 @@ export const advisorTool = {
|
|
|
133
133
|
if (reviewType === "design") {
|
|
134
134
|
agent._advisorRound = 0
|
|
135
135
|
agent._advisorSession = null
|
|
136
|
-
agent._advisorLastSnapshotHash = null // stale diff dedup baseline must not leak into the next code review
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
// Generate the design token BEFORE the review and inject it into the advisor's prompt.
|
package/src/agent-tools/plan.mjs
CHANGED
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
* user sends a new message — so the constraint never fades from context.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const PLAN_FULL_REMINDER =
|
|
12
12
|
"[System reminder: plan mode is ON. Workflow: (1) explore/read codebase with read-only tools, " +
|
|
13
13
|
"(2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' " +
|
|
14
14
|
"so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands. " +
|
|
15
15
|
"Your turn must end with either a clarifying question to the user or a call to plan with action='exit'.]"
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
const PLAN_SPARSE_REMINDER =
|
|
18
18
|
"[System reminder: plan mode still active — read-only tools only (the current plan file exempt). " +
|
|
19
19
|
"Design the solution, then call plan with action='exit' for user approval.]"
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const PLAN_EXIT_REMINDER =
|
|
22
22
|
"[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. " +
|
|
23
23
|
"No need for a task list (plan already covered that) or further confirmation.]"
|
|
24
24
|
|
|
@@ -180,11 +180,10 @@ export function mergeChildMutations(parent, child) {
|
|
|
180
180
|
parent._verifiedThisRun = false
|
|
181
181
|
parent._verifyPassed = undefined
|
|
182
182
|
}
|
|
183
|
-
// Fresh code → fresh convergence budget + stale session
|
|
183
|
+
// Fresh code → fresh convergence budget + stale session cleanup.
|
|
184
184
|
// _advisorRound reset ensures new code gets a full round-1 review;
|
|
185
|
-
// _advisorSession
|
|
185
|
+
// _advisorSession prevents cross-contamination between reviews.
|
|
186
186
|
parent._advisorRound = 0
|
|
187
187
|
parent._advisorSession = null
|
|
188
|
-
parent._advisorLastSnapshotHash = null
|
|
189
188
|
return true
|
|
190
189
|
}
|
package/src/agent.mjs
CHANGED
|
@@ -100,7 +100,7 @@ export function createAgent({
|
|
|
100
100
|
_mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
|
|
101
101
|
_engDesignReviewed: false, // eng-coder: design review gate passed (hard gate in dispatch.mjs)
|
|
102
102
|
_engDesignToken: null, // issued by advisor(type="design"); required to spawn eng-coder
|
|
103
|
-
_touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
|
|
103
|
+
_touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
|
|
104
104
|
_lastEngState: false,
|
|
105
105
|
_pendingReminders: [],
|
|
106
106
|
_pendingTimers: [],
|
|
@@ -131,7 +131,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
131
131
|
agent._verifyRetries = 0
|
|
132
132
|
agent._advisorRound = 0
|
|
133
133
|
agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
|
|
134
|
-
agent._advisorLastSnapshotHash = null // dedup baseline is per-run too — stale snapshot could wrongly suppress a diff refresh
|
|
135
134
|
agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
|
|
136
135
|
agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
|
|
137
136
|
}
|
package/src/context.mjs
CHANGED
|
@@ -237,7 +237,7 @@ const OVERSIZE_CONTENT_LIMIT = 8_000
|
|
|
237
237
|
* does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400 risk.
|
|
238
238
|
* Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
|
|
239
239
|
*/
|
|
240
|
-
|
|
240
|
+
function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
|
|
241
241
|
let shrunk = false
|
|
242
242
|
for (const m of agent.history) {
|
|
243
243
|
if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") continue
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
You are a code review advisor.
|
|
2
2
|
Perform a full-scope review of the specified files.
|
|
3
3
|
You have read-only tools to explore the codebase.
|
|
4
|
-
You have a
|
|
4
|
+
You have a budget of 30 tool rounds (chat turns) — plan your exploration accordingly. Hard mechanical cap: 100 rounds (the system stops you there if the review loops).
|
|
5
5
|
|
|
6
6
|
Review workflow:
|
|
7
7
|
1. The files to review are listed in the review scope. Read them in full. The review scope defines exactly which files to inspect.
|
|
@@ -2,24 +2,24 @@ You are a code review advisor.
|
|
|
2
2
|
Verify the prior issue table (provided in the review context).
|
|
3
3
|
You may note obvious new issues introduced by the fixes.
|
|
4
4
|
You have read-only tools to explore the codebase.
|
|
5
|
-
You have a
|
|
5
|
+
You have a budget of 30 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
|
|
6
6
|
|
|
7
7
|
Review workflow:
|
|
8
|
-
1. The files
|
|
8
|
+
1. The affected files are named in the prior issue table — read them in full. The prior issue table is HISTORY from a previous review, not current state.
|
|
9
9
|
2. STALE-CONTEXT WARNING: any content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.
|
|
10
10
|
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a fix appears to contradict the task itself.
|
|
11
|
-
4.
|
|
11
|
+
4. **ALWAYS verify current file content with `read` before judging a prior-table item as fixed or unfixed — never decide based on the prior table alone.** An empty `git diff` does NOT mean nothing changed: fixes may already be committed (`git log -3` shows recent commits) — `read` the files named in the prior table regardless of the diff. Batch independent tool calls in one reply.
|
|
12
12
|
5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
13
13
|
6. Produce your review table.
|
|
14
14
|
|
|
15
|
-
Budget: read only the files
|
|
15
|
+
Budget: read only the files named in the prior-table items. If at 15 rounds you have not yet verified all items, wrap up.
|
|
16
16
|
|
|
17
17
|
Rules:
|
|
18
18
|
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
19
19
|
- Primarily check fix status of items in the prior issue table.
|
|
20
20
|
- For items marked "fixed": verify they were actually fixed.
|
|
21
21
|
- For items marked "not an issue": evaluate whether the reasoning is sound.
|
|
22
|
-
- Every "Unfixed" or "New" entry MUST
|
|
22
|
+
- Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may come from the stale prior table. Findings without a fresh quoted line are treated as unverified and will not be accepted.
|
|
23
23
|
- You may flag obvious new problems — but only if clearly visible in the reviewed files and would cause crashes, data loss, or logic errors.
|
|
24
24
|
- Do NOT nitpick style or naming.
|
|
25
25
|
- Output a Markdown table listing all remaining problems (old or new):
|
|
@@ -2,24 +2,24 @@ You are a code review advisor.
|
|
|
2
2
|
Strictly verify only the prior issue table (provided in the review context).
|
|
3
3
|
Do NOT look for new issues.
|
|
4
4
|
You have read-only tools to explore the codebase.
|
|
5
|
-
You have a
|
|
5
|
+
You have a budget of 30 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
|
|
6
6
|
|
|
7
7
|
Review workflow:
|
|
8
|
-
1. The files
|
|
8
|
+
1. The affected files are named in the prior issue table — read them in full. The prior issue table is HISTORY from a previous review, not current state.
|
|
9
9
|
2. STALE-CONTEXT WARNING: any content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.
|
|
10
10
|
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs.
|
|
11
|
-
4.
|
|
11
|
+
4. **ALWAYS verify current file content with `read` before judging a prior-table item as fixed or unfixed — never decide based on the prior table alone.** An empty `git diff` does NOT mean nothing changed: fixes may already be committed (`git log -3` shows recent commits) — `read` the files named in the prior table regardless of the diff. Batch independent tool calls in one reply.
|
|
12
12
|
5. Verify fix status of each item in the prior issue table.
|
|
13
13
|
6. Produce your review table.
|
|
14
14
|
|
|
15
|
-
Budget: read only the files
|
|
15
|
+
Budget: read only the files named in the prior-table items. If at 15 rounds you have not yet verified all items, wrap up.
|
|
16
16
|
|
|
17
17
|
Rules:
|
|
18
18
|
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
19
19
|
- Only check fix status of items in the prior issue table.
|
|
20
20
|
- For items marked "fixed": verify they were actually fixed.
|
|
21
21
|
- For items marked "not an issue": evaluate whether the reasoning is sound.
|
|
22
|
-
- Every "Unfixed" entry MUST
|
|
22
|
+
- Every "Unfixed" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may come from the stale prior table. Findings without a fresh quoted line are treated as unverified and will not be accepted.
|
|
23
23
|
- Output a Markdown table. Only list items that still have problems:
|
|
24
24
|
| # | Orig# | File | Severity | Status | Notes |
|
|
25
25
|
|---|-------|------|----------|--------|-------|
|
|
@@ -9,4 +9,7 @@ Debugging strategy:
|
|
|
9
9
|
- Verify against official docs before guessing.
|
|
10
10
|
- Binary search: cut the problem in half, test which half has the fault.
|
|
11
11
|
- Fix one thing at a time. Don't change multiple things at once.
|
|
12
|
-
- Don't get stuck reading code — write tests, add logs. Trust the runtime over your theories.
|
|
12
|
+
- Don't get stuck reading code — write tests, add logs. Trust the runtime over your theories.
|
|
13
|
+
|
|
14
|
+
Review discipline (standard mode only — engineering mode has its own review timing rules):
|
|
15
|
+
- **Advisor:** call after changing code. Must provide scope: `paths` (files/dirs to review) or `documents` (context). Response table: `| # | Action | Detail |`. Round 2 verifies prior table.
|
|
@@ -30,13 +30,11 @@ subagents only.
|
|
|
30
30
|
criteria — AND the designToken verbatim (the exact token string from the
|
|
31
31
|
advisor output). The token is required — eng-coder cannot modify files
|
|
32
32
|
without it.
|
|
33
|
-
5. **
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
directly if minor.
|
|
39
|
-
- If advisor approves: present results to the user.
|
|
33
|
+
5. **Delivery review.** After eng-coder returns, verify the delivery against
|
|
34
|
+
the acceptance criteria from the design. The eng-coder self-reviewed inside
|
|
35
|
+
the subagent — its advisor(code) call happens there. Re-review with the
|
|
36
|
+
`advisor` tool (`type="code"`, `documents=[...]` = the task's Docs involved
|
|
37
|
+
list) only when the user asks or the delivery looks wrong.
|
|
40
38
|
6. **Verify.** Run `verify` — it must pass before you claim the task complete.
|
|
41
39
|
|
|
42
40
|
## Work Loop (every user message)
|
|
@@ -51,7 +49,7 @@ passed?
|
|
|
51
49
|
| Design | Write or refine the DESIGN doc (approach + rationale, architecture/interface, affected files, key decisions), organized by business domain per METHODOLOGY, ask for confirmation |
|
|
52
50
|
| Awaiting approval | Present design summary, WAIT for explicit approval |
|
|
53
51
|
| Implementation | eng-coder is working — do not redesign in parallel |
|
|
54
|
-
| Delivery review |
|
|
52
|
+
| Delivery review | Verify the delivery against the acceptance criteria (the eng-coder self-reviewed inside the subagent); re-review with advisor (type="code", documents = Docs involved) only when the user asks or the delivery looks wrong; report |
|
|
55
53
|
| Wrapped up | Report, wait for next instruction |
|
|
56
54
|
|
|
57
55
|
Then handle the message:
|
package/src/prompts/system.md
CHANGED
|
@@ -33,7 +33,7 @@ Programming is collaborative labor between you and the human. The human decides
|
|
|
33
33
|
- No TTY — run shell commands non-interactively (git commit -m, --no-pager, -y/--yes).
|
|
34
34
|
- Never modify files outside the working directory. No bash redirects to bypass boundaries.
|
|
35
35
|
- **Reversibility tiers:** local edits — yours. Destructive (rm -rf, force-push) — confirm. Outward (commit/push/publish) — confirm each time.
|
|
36
|
-
- Checkpoint before risky bulk operations. Auto-
|
|
36
|
+
- Checkpoint before risky bulk operations. Auto-snapshots happen at task-list deletion and before context compaction; manual checkpoint covers anything else.
|
|
37
37
|
- When context is compacted mid-session: trust the summary's conclusions, but re-read AGENTS.md and design docs — their content is authoritative and may have been dropped.
|
|
38
38
|
- Long-term memory via memory_put/memory_search. Save bugs, conventions, preferences.
|
|
39
39
|
- Codebase exploration order: repo_outline → doc_search → code_search. Structure → intent → details.
|
|
@@ -52,5 +52,4 @@ Before finalizing: pause and think through edge cases. What could go wrong? Self
|
|
|
52
52
|
- After every write/edit: `lint`. Before done: `lint full=true`.
|
|
53
53
|
- Before declaring completion: `verify` (syntax, related tests, self-review checklist).
|
|
54
54
|
- Code changes need at least one test.
|
|
55
|
-
- **Advisor:** call after changing code. Must provide scope: `paths` (files/dirs to review) or `documents` (context). Response table: `| # | Action | Detail |`. Round 2 verifies prior table.
|
|
56
55
|
- **Done:** explain what you changed, why, what's simplified, what's not done.
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -29,6 +29,12 @@ export async function runAgentTurn(ctx, text) {
|
|
|
29
29
|
// 可注入覆盖(测试用);默认走真实实现
|
|
30
30
|
const runAgentImpl = ctx.runAgent ?? runAgent
|
|
31
31
|
const saveSessionImpl = ctx.saveSession ?? saveSession
|
|
32
|
+
// A new user message starts a new turn: auto-expanded completed replies from the
|
|
33
|
+
// previous turn (kept open so the user could read them) collapse now.
|
|
34
|
+
for (const idx of state._autoExpand ?? []) {
|
|
35
|
+
state.expandedBlocks?.delete(`long-${idx}`)
|
|
36
|
+
}
|
|
37
|
+
state._autoExpand = []
|
|
32
38
|
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
33
39
|
pushLine(text, C.text)
|
|
34
40
|
|
|
@@ -52,11 +58,23 @@ export async function runAgentTurn(ctx, text) {
|
|
|
52
58
|
|
|
53
59
|
const flushStream = () => {
|
|
54
60
|
if (state.reasoning) {
|
|
61
|
+
const idx = state.lines.length
|
|
55
62
|
pushLine(state.reasoning, C.reason)
|
|
63
|
+
// Completed reasoning stays expanded (user is reading it) until the next turn
|
|
64
|
+
state.expandedBlocks ??= new Set()
|
|
65
|
+
state.expandedBlocks.add(`long-${idx}`)
|
|
66
|
+
state._autoExpand ??= []
|
|
67
|
+
state._autoExpand.push(idx)
|
|
56
68
|
state.reasoning = ""
|
|
57
69
|
}
|
|
58
70
|
if (state.streaming) {
|
|
71
|
+
const idx = state.lines.length
|
|
59
72
|
pushLine(state.streaming, C.text)
|
|
73
|
+
// Completed main output stays expanded (user is reading it) until the next turn
|
|
74
|
+
state.expandedBlocks ??= new Set()
|
|
75
|
+
state.expandedBlocks.add(`long-${idx}`)
|
|
76
|
+
state._autoExpand ??= []
|
|
77
|
+
state._autoExpand.push(idx)
|
|
60
78
|
state.streaming = ""
|
|
61
79
|
}
|
|
62
80
|
state.advisorStreaming = ""
|
package/src/tui/ansi.mjs
CHANGED
|
@@ -42,5 +42,5 @@ export const C = {
|
|
|
42
42
|
dim: ansi.gray,
|
|
43
43
|
warn: ansi.fg(3),
|
|
44
44
|
advisor: `${ESC}[92m`, // bright green — visible on dark backgrounds
|
|
45
|
-
fold:
|
|
45
|
+
fold: ansi.bold + ansi.fg(6), // bold cyan — fold markers must stay visible on light AND dark themes (dim white vanished on light backgrounds)
|
|
46
46
|
}
|
package/src/tui/clipboard.mjs
CHANGED
|
@@ -18,26 +18,6 @@ export async function readClipboardText() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/** Write text to the system clipboard (Set-Clipboard / pbcopy / xclip). Throws on failure. */
|
|
22
|
-
export async function copyToClipboard(text) {
|
|
23
|
-
const { execFile } = await import("node:child_process")
|
|
24
|
-
const isWin = process.platform === "win32"
|
|
25
|
-
const isMac = process.platform === "darwin"
|
|
26
|
-
await new Promise((resolve, reject) => {
|
|
27
|
-
if (isWin) {
|
|
28
|
-
const child = execFile("powershell", ["-NoProfile", "-Command", "[Console]::In.ReadToEnd() | Set-Clipboard"], { timeout: 5000 }, (err) => err ? reject(err) : resolve())
|
|
29
|
-
child.stdin?.end(text)
|
|
30
|
-
} else if (isMac) {
|
|
31
|
-
const child = execFile("pbcopy", [], { timeout: 5000 }, (err) => err ? reject(err) : resolve())
|
|
32
|
-
child.stdin?.end(text)
|
|
33
|
-
} else {
|
|
34
|
-
const child = execFile("xclip", ["-selection", "clipboard"], { timeout: 5000 }, (err) => err ? reject(err) : resolve())
|
|
35
|
-
child.stdin?.end(text)
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
21
|
/** Insert pasted text into the active text target.
|
|
42
22
|
* Free-text question active → append to its answer (single-line field: newlines stripped).
|
|
43
23
|
* Options question active → ignore (no text field; must not leak into the input box).
|
package/src/tui/index.mjs
CHANGED
|
@@ -81,6 +81,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
81
81
|
ctxCache: { len: -1, tokens: 0 }, // context utilization estimate cache (estimateTokens is O(n), only recompute when history grows)
|
|
82
82
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
83
83
|
completion: null, // Tab completion state { candidates, index }
|
|
84
|
+
_autoExpand: [], // indices of completed replies kept expanded; cleared when the next user turn starts
|
|
84
85
|
subTasks: {}, // sub-agent panel: { roleName: { role, text, done } }, one line per role, marked done briefly after completion
|
|
85
86
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
86
87
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
@@ -386,8 +387,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
386
387
|
}
|
|
387
388
|
})
|
|
388
389
|
|
|
389
|
-
// Mouse clicks (SGR \x1b[<0;col;rowM) — picker selection +
|
|
390
|
-
const onMouseClick = (col, row) => handleMouseClick({ state, render,
|
|
390
|
+
// Mouse clicks (SGR \x1b[<0;col;rowM) — picker selection + fold expansion.
|
|
391
|
+
const onMouseClick = (col, row) => handleMouseClick({ state, render, popPicker }, col, row)
|
|
391
392
|
|
|
392
393
|
// ---------------------------------------------------------- Startup screen + background indexing
|
|
393
394
|
|
package/src/tui/mouse.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mouse.mjs — SGR mouse support: click parsing + hit-testing
|
|
2
|
+
* mouse.mjs — SGR mouse support: click parsing + hit-testing.
|
|
3
3
|
*
|
|
4
4
|
* Protocol (enabled at startup via \x1b[?1000h\x1b[?1006h):
|
|
5
5
|
* press: \x1b[<b;col;rowM (b=0 left, 64/65 wheel up/down — wheel handled upstream)
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Only left-click (button 0) is consumed. Everything else stays stripped
|
|
10
10
|
* upstream (sequence fragments must never leak into the input box).
|
|
11
|
+
*
|
|
12
|
+
* Click actions (deliberately minimal — a line-action menu was removed as
|
|
13
|
+
* over-engineering: terminals already copy via drag-select):
|
|
14
|
+
* - picker option click = select it
|
|
15
|
+
* - folded-block hint click = expand it
|
|
11
16
|
*/
|
|
12
17
|
import { computeLayout } from "./layout.mjs"
|
|
13
18
|
import { buildConvLines } from "./render-conversation.mjs"
|
|
14
|
-
import { sanitizeDisplay } from "./render.mjs"
|
|
15
19
|
|
|
16
20
|
/** Extract left-click presses from a chunk. Returns [{ col, row }] (1-based). */
|
|
17
21
|
export function parseMouseClicks(text) {
|
|
@@ -37,7 +41,7 @@ export function convGlobalIndex(convLen, convH, scroll) {
|
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* Handle a left-click at SGR (col, row) — 1-based terminal coordinates.
|
|
40
|
-
* ctx: { state, render,
|
|
44
|
+
* ctx: { state, render, popPicker }
|
|
41
45
|
* Returns true when the click was consumed.
|
|
42
46
|
*/
|
|
43
47
|
export function handleMouseClick(ctx, col, row) {
|
|
@@ -62,56 +66,20 @@ export function handleMouseClick(ctx, col, row) {
|
|
|
62
66
|
return true
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
// ── Conversation: fold
|
|
69
|
+
// ── Conversation: click a fold marker (expand hint or collapse marker) toggles it ──
|
|
66
70
|
if (r >= P.conversation.y && r < P.conversation.y + P.conversation.h) {
|
|
67
71
|
const convLines = buildConvLines(state, dims.cols)
|
|
68
72
|
const gIdx = convGlobalIndex(convLines.length, P.conversation.h, state.scroll ?? 0)(r - P.conversation.y)
|
|
69
73
|
if (gIdx === null) return false
|
|
70
74
|
const lineEl = convLines[gIdx]
|
|
71
|
-
if (!lineEl) return false
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
if (lineEl._foldToggle)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return true
|
|
79
|
-
}
|
|
80
|
-
// Click on a message line → action menu
|
|
81
|
-
if (lineEl._src !== undefined) {
|
|
82
|
-
const src = state.lines[lineEl._src]
|
|
83
|
-
if (src) {
|
|
84
|
-
openLineMenu(ctx, src)
|
|
85
|
-
return true
|
|
86
|
-
}
|
|
87
|
-
}
|
|
75
|
+
if (!lineEl?._foldToggle) return false
|
|
76
|
+
state.expandedBlocks ??= new Set()
|
|
77
|
+
// Bidirectional: click expands a folded block, collapses an expanded one
|
|
78
|
+
if (state.expandedBlocks.has(lineEl._foldToggle)) state.expandedBlocks.delete(lineEl._foldToggle)
|
|
79
|
+
else state.expandedBlocks.add(lineEl._foldToggle)
|
|
80
|
+
render()
|
|
81
|
+
return true
|
|
88
82
|
}
|
|
89
83
|
|
|
90
84
|
return false
|
|
91
85
|
}
|
|
92
|
-
|
|
93
|
-
/** Line action menu: copy / edit in input / (fold toggle if the source line folds). */
|
|
94
|
-
async function openLineMenu(ctx, srcLine) {
|
|
95
|
-
const { state, render, showPicker, pushLine } = ctx
|
|
96
|
-
const text = sanitizeDisplay(srcLine.text)
|
|
97
|
-
const entries = [
|
|
98
|
-
{ type: "item", text: `📋 Copy line (${text.length} chars)`, action: "copy" },
|
|
99
|
-
{ type: "item", text: "✏️ Edit in input box", action: "edit" },
|
|
100
|
-
]
|
|
101
|
-
const picked = await showPicker("Line actions", entries)
|
|
102
|
-
if (!picked) return
|
|
103
|
-
if (picked.action === "copy") {
|
|
104
|
-
try {
|
|
105
|
-
const { copyToClipboard } = await import("./clipboard.mjs")
|
|
106
|
-
await copyToClipboard(text)
|
|
107
|
-
pushLine(`[clipboard] copied ${text.length} chars`, (await import("./ansi.mjs")).C.dim)
|
|
108
|
-
} catch (e) {
|
|
109
|
-
pushLine(`[clipboard] copy failed: ${e.message}`, (await import("./ansi.mjs")).C.error)
|
|
110
|
-
}
|
|
111
|
-
render()
|
|
112
|
-
} else if (picked.action === "edit") {
|
|
113
|
-
state.input = [...text]
|
|
114
|
-
state.cursor = state.input.length
|
|
115
|
-
render()
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -15,8 +15,25 @@ export function convCacheKey(state) {
|
|
|
15
15
|
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${state.advisorStreaming?.length ?? 0}|${state._advisorThink?.length ?? 0}|${state.foldEnabled !== false ? "f" : "u"}|${exp}`
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Fold marker line: bold-cyan icon + "click to …" phrase underlined (clickable affordance).
|
|
19
|
+
* No indent — flush with the content below it; the caller adds a blank line BEFORE it
|
|
20
|
+
* so the control line stands apart from unrelated content (reported UX). */
|
|
21
|
+
function foldHintLine(text, foldKey, srcIdx) {
|
|
22
|
+
// Underline just the actionable phrase — link/button convention
|
|
23
|
+
const withUnderline = text.replace(/(click to (?:expand|collapse))/, "\x1b[4m$1\x1b[24m")
|
|
24
|
+
return { text: withUnderline, color: C.fold, _foldToggle: foldKey, _src: srcIdx }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Blank separator before a fold control line (uncolored — must not join consecutive-dim folding).
|
|
28
|
+
* Only the EXPANDED state uses it (▼ sits at the block head); the folded state's ▶
|
|
29
|
+
* control line sits mid-block where the ellipsis used to be, so no separator needed. */
|
|
30
|
+
function blankLine() {
|
|
31
|
+
return { text: "", color: "" }
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
function highlightSearchMatches(text, query, matchesInLine, globalCurrentIndex, allMatches, lineIndex) {
|
|
19
35
|
if (!matchesInLine || matchesInLine.length === 0 || !query) return text
|
|
36
|
+
|
|
20
37
|
let result = ""
|
|
21
38
|
let lastEnd = 0
|
|
22
39
|
for (const startIdx of matchesInLine) {
|
|
@@ -43,6 +60,10 @@ function buildConvLines(state, cols) {
|
|
|
43
60
|
if (_convCache.key === key && _convCache.cols === cols) return _convCache.lines
|
|
44
61
|
|
|
45
62
|
const convLines = []
|
|
63
|
+
// Folding constants (function scope — used by both the long-message fold below
|
|
64
|
+
// and the consecutive-dim fold at the bottom)
|
|
65
|
+
const LONG_FOLD_LINES = 12
|
|
66
|
+
const FOLD_KEEP = 5 // content lines kept in the folded state (first 4 + last 1)
|
|
46
67
|
for (let i = 0; i < state.lines.length; i++) {
|
|
47
68
|
const l = state.lines[i]
|
|
48
69
|
let text = l.text
|
|
@@ -52,15 +73,15 @@ function buildConvLines(state, cols) {
|
|
|
52
73
|
text = highlightSearchMatches(text, state.search.query, l._searchMatches, state.search.index, state.search.matches, i)
|
|
53
74
|
}
|
|
54
75
|
|
|
55
|
-
// Long-message folding:
|
|
56
|
-
//
|
|
57
|
-
// [
|
|
58
|
-
//
|
|
59
|
-
//
|
|
76
|
+
// Long-message folding: ANY single line (main output C.text, thinking C.reason,
|
|
77
|
+
// tool summaries C.dim — whatever wraps beyond LONG_FOLD_LINES display rows)
|
|
78
|
+
// collapses to [blank, ▶, first 4, last] — 5 content lines. Main output and
|
|
79
|
+
// thinking are the REAL long content; bidirectional folding (collapse markers
|
|
80
|
+
// + click toggle) keeps them readable — the 0.12.7 dim-only restriction was a
|
|
81
|
+
// temporary fix for the single-direction era and is now reverted. Keyed by the
|
|
60
82
|
// source-line index (`long-${i}`) so the toggle survives re-renders.
|
|
61
|
-
const LONG_FOLD_LINES = 12
|
|
62
83
|
const longKey = `long-${i}`
|
|
63
|
-
const folded = state.foldEnabled !== false && !state.expandedBlocks?.has(longKey)
|
|
84
|
+
const folded = state.foldEnabled !== false && !state.expandedBlocks?.has(longKey)
|
|
64
85
|
const block = []
|
|
65
86
|
for (const line of formatTables(sanitizeDisplay(text), cols - 1)) {
|
|
66
87
|
for (const wrapped of wrapText(line, cols - 1)) {
|
|
@@ -70,15 +91,24 @@ function buildConvLines(state, cols) {
|
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
if (folded && block.length > LONG_FOLD_LINES) {
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
// Folded state: first 4 content lines, then the ▶ control line where the
|
|
95
|
+
// ellipsis used to be (the marker itself reads "… N more lines" — ellipsis
|
|
96
|
+
// semantics built in), then the last line. No leading blank line needed:
|
|
97
|
+
// the block starts with real content now.
|
|
98
|
+
convLines.push(...block.slice(0, FOLD_KEEP - 1))
|
|
99
|
+
convLines.push(foldHintLine(`▶ … ${block.length - FOLD_KEEP} more lines — click to expand`, longKey, i))
|
|
75
100
|
convLines.push(block[block.length - 1])
|
|
76
|
-
} else {
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
101
|
+
} else if (block.length > LONG_FOLD_LINES) {
|
|
102
|
+
// EXPANDED long block: blank line + ▼ control line at the HEAD, directly
|
|
103
|
+
// before the content. DIM blocks must not re-trigger the consecutive-dim
|
|
104
|
+
// folding below (folding stacked on folding — reported regression).
|
|
105
|
+
if (l.color === C.dim) {
|
|
80
106
|
for (const line of block) line._skipDimFold = true
|
|
81
107
|
}
|
|
108
|
+
convLines.push(blankLine())
|
|
109
|
+
convLines.push(foldHintLine(`▼ … ${block.length} lines — click to collapse`, longKey, i))
|
|
110
|
+
convLines.push(...block)
|
|
111
|
+
} else {
|
|
82
112
|
convLines.push(...block)
|
|
83
113
|
}
|
|
84
114
|
}
|
|
@@ -125,12 +155,19 @@ function buildConvLines(state, cols) {
|
|
|
125
155
|
if (blockLen > FOLD_LINES && !hasExpandedLong) {
|
|
126
156
|
const foldKey = `fold-${foldCounter++}`
|
|
127
157
|
if (state.foldEnabled !== false && !state.expandedBlocks?.has(foldKey)) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
folded.push(
|
|
158
|
+
// First 4 lines, then the ▶ control line (ellipsis position), then the last line
|
|
159
|
+
folded.push(...convLines.slice(i, i + FOLD_KEEP - 1))
|
|
160
|
+
folded.push(foldHintLine(`▶ … ${blockLen - FOLD_KEEP} more lines — click to expand`, foldKey))
|
|
161
|
+
folded.push(convLines[j - 1])
|
|
131
162
|
i = j
|
|
132
163
|
continue
|
|
133
164
|
}
|
|
165
|
+
// EXPANDED consecutive-dim block: blank + ▼ at the HEAD, then every line
|
|
166
|
+
folded.push(blankLine())
|
|
167
|
+
folded.push(foldHintLine(`▼ … ${blockLen} lines — click to collapse`, foldKey))
|
|
168
|
+
for (let k = i; k < j; k++) folded.push(convLines[k])
|
|
169
|
+
i = j
|
|
170
|
+
continue
|
|
134
171
|
}
|
|
135
172
|
}
|
|
136
173
|
folded.push(line)
|