agen-vektor 0.3.26 → 0.3.28
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/dist/agent/checkpoints.js +264 -0
- package/dist/agent/loop.js +22 -0
- package/dist/config/config.js +5 -1
- package/dist/config/free-tier.js +6 -3
- package/dist/tui/app.js +69 -3
- package/dist/tui/commands.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.takeCheckpoint = takeCheckpoint;
|
|
37
|
+
exports.listCheckpoints = listCheckpoints;
|
|
38
|
+
exports.restoreCheckpoint = restoreCheckpoint;
|
|
39
|
+
exports.pruneCheckpoints = pruneCheckpoints;
|
|
40
|
+
exports.resetCheckpoints = resetCheckpoints;
|
|
41
|
+
/**
|
|
42
|
+
* Checkpoints — git-based safety net (Freebuff/Codebuff parity).
|
|
43
|
+
*
|
|
44
|
+
* Every run, before the FIRST mutating tool call (write_file / edit_file /
|
|
45
|
+
* delete_file / apply_patch / shell) touches the project, the agent takes a
|
|
46
|
+
* CHECKPOINT: a full snapshot of the working tree stored in a SHADOW git
|
|
47
|
+
* repository under ~/.vector/checkpoints/<project>/.
|
|
48
|
+
*
|
|
49
|
+
* Design rules:
|
|
50
|
+
* - The USER'S repo is never touched: no commits, no branches, no refs, no
|
|
51
|
+
* hooks. The shadow repo is a SEPARATE GIT_DIR (own object store, index,
|
|
52
|
+
* refs) using the project tree as its work-tree. `git status` in the user's
|
|
53
|
+
* project stays pristine — verified by tests.
|
|
54
|
+
* - Each checkpoint is an INDEPENDENT root commit (git commit-tree, no
|
|
55
|
+
* parent) pinned by its own ref `refs/vector-cp/<ref>`. Unchanged files
|
|
56
|
+
* dedupe by content in the object store, so snapshots stay small — and
|
|
57
|
+
* pruning is a plain ref deletion, no history rewrite.
|
|
58
|
+
* - Restore = index reset to the snapshot ref + `checkout <ref> -- .` +
|
|
59
|
+
* resurrection of files deleted after the snapshot. The user's git history
|
|
60
|
+
* is untouched (their unstaged changes are replaced — they can still diff
|
|
61
|
+
* via their own git BEFORE restoring; /undo warns in the TUI).
|
|
62
|
+
* - Bounded storage: keep the newest MAX_CHECKPOINTS refs, delete the rest.
|
|
63
|
+
* - Everything is best-effort: a missing git binary or a read-only home must
|
|
64
|
+
* NEVER break a run. All failures resolve to null/empty and the run
|
|
65
|
+
* proceeds without a safety net.
|
|
66
|
+
*/
|
|
67
|
+
const node_child_process_1 = require("node:child_process");
|
|
68
|
+
const fs = __importStar(require("node:fs"));
|
|
69
|
+
const path = __importStar(require("node:path"));
|
|
70
|
+
const paths_1 = require("../utils/paths");
|
|
71
|
+
const MAX_CHECKPOINTS = 30;
|
|
72
|
+
const REF_PREFIX = 'refs/vector-cp/';
|
|
73
|
+
const GIT = 'git';
|
|
74
|
+
// Monotonic per-process sequence: same-second snapshots must keep their
|
|
75
|
+
// creation order in listings (restore-by-index!), so the ref suffix is a
|
|
76
|
+
// zero-padded base36 counter instead of random chars — ref-name sort then
|
|
77
|
+
// equals reverse-creation order everywhere.
|
|
78
|
+
let cpSeq = 0;
|
|
79
|
+
function gitDirFor(cwd) {
|
|
80
|
+
// Stable per project path — two projects never share a shadow repo.
|
|
81
|
+
const slug = cwd.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(-80) || 'project';
|
|
82
|
+
return path.join((0, paths_1.getVectorDir)(), 'checkpoints', slug);
|
|
83
|
+
}
|
|
84
|
+
function git(gitDir, args, workTree) {
|
|
85
|
+
const r = (0, node_child_process_1.spawnSync)(GIT, [`--git-dir=${gitDir}`, ...(workTree ? [`--work-tree=${workTree}`] : []), ...args], { encoding: 'utf8', timeout: 20_000 });
|
|
86
|
+
return { ok: r.status === 0, out: (r.stdout || '') + (r.stderr || '') };
|
|
87
|
+
}
|
|
88
|
+
function ensureShadowRepo(gitDir) {
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync(path.join(gitDir, 'HEAD'))) {
|
|
91
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
92
|
+
// Fixed identity — nothing from the user's git config leaks in.
|
|
93
|
+
if (!git(gitDir, ['init', '--quiet']).ok)
|
|
94
|
+
return false;
|
|
95
|
+
git(gitDir, ['config', 'user.email', 'checkpoints@vectorhead.local']);
|
|
96
|
+
git(gitDir, ['config', 'user.name', 'VectorHead Checkpoints']);
|
|
97
|
+
git(gitDir, ['config', 'commit.gpgsign', 'false']);
|
|
98
|
+
git(gitDir, ['config', 'core.autocrlf', 'false']);
|
|
99
|
+
git(gitDir, ['config', 'gc.auto', '0']);
|
|
100
|
+
}
|
|
101
|
+
return fs.existsSync(path.join(gitDir, 'HEAD'));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Take a checkpoint. Returns null when snapshotting is impossible — callers
|
|
109
|
+
* must treat that as "no safety net", never as an error.
|
|
110
|
+
*/
|
|
111
|
+
function takeCheckpoint(cwd, reason, task = '') {
|
|
112
|
+
try {
|
|
113
|
+
if (!fs.existsSync(cwd))
|
|
114
|
+
return null;
|
|
115
|
+
const gitDir = gitDirFor(cwd);
|
|
116
|
+
if (!ensureShadowRepo(gitDir))
|
|
117
|
+
return null;
|
|
118
|
+
// -A stages the full tree incl. deletions and untracked files. A
|
|
119
|
+
// .gitignore in the user's project is respected (their intent), but
|
|
120
|
+
// that also means ignored files are NOT protected by checkpoints —
|
|
121
|
+
// documented trade-off, same as Claude Code.
|
|
122
|
+
if (!git(gitDir, ['add', '-A', '--'], cwd).ok)
|
|
123
|
+
return null;
|
|
124
|
+
const tree = git(gitDir, ['write-tree']);
|
|
125
|
+
if (!tree.ok)
|
|
126
|
+
return null;
|
|
127
|
+
const treeHash = tree.out.trim().split('\n')[0].trim();
|
|
128
|
+
if (!/^[0-9a-f]{40}$/.test(treeHash))
|
|
129
|
+
return null;
|
|
130
|
+
const stamp = new Date();
|
|
131
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
132
|
+
const t = `${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}` +
|
|
133
|
+
`-${pad(stamp.getHours())}${pad(stamp.getMinutes())}${pad(stamp.getSeconds())}`;
|
|
134
|
+
const suffix = (cpSeq++).toString(36).padStart(4, '0');
|
|
135
|
+
const ref = `cp-${t}-${suffix}`;
|
|
136
|
+
const msg = `checkpoint ${ref} (${reason}) ${task.slice(0, 80)}`.trim();
|
|
137
|
+
// Independent root commit — commit-tree gives full control over parents.
|
|
138
|
+
const commit = git(gitDir, ['commit-tree', treeHash, '-m', msg]);
|
|
139
|
+
if (!commit.ok)
|
|
140
|
+
return null;
|
|
141
|
+
const hash = commit.out.trim().split('\n')[0].trim();
|
|
142
|
+
if (!/^[0-9a-f]{40}$/.test(hash))
|
|
143
|
+
return null;
|
|
144
|
+
if (!git(gitDir, ['update-ref', `${REF_PREFIX}${ref}`, hash]).ok)
|
|
145
|
+
return null;
|
|
146
|
+
return { ref, time: stamp.toISOString(), reason, task: task.slice(0, 80), hash };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Newest-first list of surviving checkpoints. */
|
|
153
|
+
function listCheckpoints(cwd) {
|
|
154
|
+
try {
|
|
155
|
+
const gitDir = gitDirFor(cwd);
|
|
156
|
+
if (!fs.existsSync(path.join(gitDir, 'HEAD')))
|
|
157
|
+
return [];
|
|
158
|
+
const r = git(gitDir, [
|
|
159
|
+
'for-each-ref',
|
|
160
|
+
`--format=%(refname)%09%(contents:subject)%09%(committerdate:iso-strict)`,
|
|
161
|
+
REF_PREFIX,
|
|
162
|
+
]);
|
|
163
|
+
if (!r.ok)
|
|
164
|
+
return [];
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const line of r.out.split('\n')) {
|
|
167
|
+
if (line.trim() === '')
|
|
168
|
+
continue;
|
|
169
|
+
const [refname = '', subject = '', time = ''] = line.split('\t');
|
|
170
|
+
const ref = refname.replace(REF_PREFIX, '');
|
|
171
|
+
const m = subject.match(/^(cp-[0-9]{8}-[0-9]{6}-[a-z0-9]+) \(([^)]*)\) ?(.*)$/);
|
|
172
|
+
const hash = git(gitDir, ['rev-parse', `${REF_PREFIX}${ref}`]).out.trim();
|
|
173
|
+
if (!/^[0-9a-f]{40}$/.test(hash))
|
|
174
|
+
continue;
|
|
175
|
+
out.push({
|
|
176
|
+
ref: m ? m[1] : ref,
|
|
177
|
+
time: time.trim(),
|
|
178
|
+
reason: m ? m[2] : '',
|
|
179
|
+
task: m ? m[3] : subject,
|
|
180
|
+
hash,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Same-second snapshots tie on committerdate — the ref name embeds a
|
|
184
|
+
// fixed-width timestamp, so sort on THAT (newest first) in JS instead of
|
|
185
|
+
// relying on git tie-breaking.
|
|
186
|
+
out.sort((a, b) => (a.ref < b.ref ? 1 : a.ref > b.ref ? -1 : 0));
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Restore the working tree to a checkpoint (index into the newest-first
|
|
195
|
+
* list, an exact ref suffix, or a full hash). Files deleted AFTER the
|
|
196
|
+
* snapshot are resurrected; files the snapshot never tracked are left
|
|
197
|
+
* alone. Returns a user-facing message either way.
|
|
198
|
+
*/
|
|
199
|
+
function restoreCheckpoint(cwd, refOrIndex) {
|
|
200
|
+
const list = listCheckpoints(cwd);
|
|
201
|
+
if (list.length === 0)
|
|
202
|
+
return { ok: false, message: 'No checkpoints yet.' };
|
|
203
|
+
let cp;
|
|
204
|
+
if (typeof refOrIndex === 'number') {
|
|
205
|
+
if (refOrIndex < 0 || refOrIndex >= list.length)
|
|
206
|
+
return { ok: false, message: 'Checkpoint index out of range.' };
|
|
207
|
+
cp = list[refOrIndex];
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
cp = list.find((c) => c.ref === refOrIndex || c.hash === refOrIndex);
|
|
211
|
+
if (!cp)
|
|
212
|
+
return { ok: false, message: `Unknown checkpoint "${refOrIndex}".` };
|
|
213
|
+
}
|
|
214
|
+
const gitDir = gitDirFor(cwd);
|
|
215
|
+
const target = cp; // bounds-checked above
|
|
216
|
+
// index := snapshot tree, then materialize the whole tree over the
|
|
217
|
+
// work-tree (restores modified AND deleted files).
|
|
218
|
+
if (!git(gitDir, ['read-tree', '--reset', '-u', target.hash], cwd).ok) {
|
|
219
|
+
return { ok: false, message: `restore failed: read-tree ${target.ref}` };
|
|
220
|
+
}
|
|
221
|
+
const checkout = git(gitDir, ['checkout', target.hash, '--', '.'], cwd);
|
|
222
|
+
if (!checkout.ok) {
|
|
223
|
+
return { ok: false, message: `restore failed: ${checkout.out.trim().slice(0, 200)}` };
|
|
224
|
+
}
|
|
225
|
+
// read-tree -u handles deletions of tracked files already; walk the
|
|
226
|
+
// snapshot tree to be extra safe about paths git skipped.
|
|
227
|
+
const ls = git(gitDir, ['ls-tree', '-r', '--name-only', target.hash]);
|
|
228
|
+
if (ls.ok) {
|
|
229
|
+
for (const f of ls.out.split('\n')) {
|
|
230
|
+
const p = f.trim();
|
|
231
|
+
if (p && !fs.existsSync(path.join(cwd, p))) {
|
|
232
|
+
git(gitDir, ['checkout', target.hash, '--', p], cwd);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { ok: true, message: `Restored to checkpoint ${target.ref} (${target.time}).` };
|
|
237
|
+
}
|
|
238
|
+
/** Keep only the newest MAX_CHECKPOINTS refs (best-effort, storage bound). */
|
|
239
|
+
function pruneCheckpoints(cwd) {
|
|
240
|
+
try {
|
|
241
|
+
const gitDir = gitDirFor(cwd);
|
|
242
|
+
if (!fs.existsSync(path.join(gitDir, 'HEAD')))
|
|
243
|
+
return;
|
|
244
|
+
const list = listCheckpoints(cwd);
|
|
245
|
+
for (const cp of list.slice(MAX_CHECKPOINTS)) {
|
|
246
|
+
git(gitDir, ['update-ref', '-d', `${REF_PREFIX}${cp.ref}`]);
|
|
247
|
+
}
|
|
248
|
+
// Drop unreferenced objects occasionally — cheap, bounded.
|
|
249
|
+
if (list.length > MAX_CHECKPOINTS)
|
|
250
|
+
git(gitDir, ['gc', '--quiet', '--prune=now']);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* best-effort */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Test helper: wipe the shadow repo for a project. */
|
|
257
|
+
function resetCheckpoints(cwd) {
|
|
258
|
+
try {
|
|
259
|
+
fs.rmSync(gitDirFor(cwd), { recursive: true, force: true });
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.estimateContextSize = estimateContextSize;
|
|
|
15
15
|
*/
|
|
16
16
|
const provider_1 = require("../providers/provider");
|
|
17
17
|
const context_1 = require("./context");
|
|
18
|
+
const checkpoints_1 = require("./checkpoints");
|
|
18
19
|
const light_1 = require("./light");
|
|
19
20
|
const prompts_1 = require("./prompts");
|
|
20
21
|
const rules_1 = require("./rules");
|
|
@@ -139,6 +140,22 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
139
140
|
let stopped = false;
|
|
140
141
|
let finalContent = '';
|
|
141
142
|
let lastReasoning = '';
|
|
143
|
+
// ─── Checkpoints (git-based safety net, Freebuff/Codebuff parity) ───
|
|
144
|
+
// Armed lazily: the snapshot is taken right before the FIRST mutating
|
|
145
|
+
// tool call of the run (never for pure read/chat runs), once per run.
|
|
146
|
+
// Best-effort: null = no net, the run proceeds regardless.
|
|
147
|
+
const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'delete_file', 'apply_patch', 'shell', 'git', 'run_in_background']);
|
|
148
|
+
let checkpointTaken = false;
|
|
149
|
+
const maybeCheckpoint = (toolName) => {
|
|
150
|
+
if (checkpointTaken || !MUTATING_TOOLS.has(toolName))
|
|
151
|
+
return;
|
|
152
|
+
checkpointTaken = true;
|
|
153
|
+
const cp = (0, checkpoints_1.takeCheckpoint)(cwd, toolName, userRequest);
|
|
154
|
+
if (cp) {
|
|
155
|
+
callbacks.onStatus?.(`Checkpoint siap (${cp.ref})`);
|
|
156
|
+
callbacks.onActivity?.('checkpoint', `safety snapshot ${cp.ref}`);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
142
159
|
const checkAbort = () => {
|
|
143
160
|
if (signal?.aborted) {
|
|
144
161
|
aborted = true;
|
|
@@ -420,6 +437,11 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
420
437
|
for (const batch of batches) {
|
|
421
438
|
checkAbort();
|
|
422
439
|
const parallel = batch.length > 1 && batch.every((tc) => PARALLEL_TOOLS.has(tc.name));
|
|
440
|
+
// Safety net BEFORE the first mutating call of this run — armed
|
|
441
|
+
// once, lazily; parallel batches are read-only by construction and
|
|
442
|
+
// never trigger it.
|
|
443
|
+
if (!parallel)
|
|
444
|
+
maybeCheckpoint(batch[0].name);
|
|
423
445
|
toolCalls += batch.length;
|
|
424
446
|
if (!parallel) {
|
|
425
447
|
callbacks.onStatus?.(`Running ${batch[0].name}`);
|
package/dist/config/config.js
CHANGED
|
@@ -67,7 +67,11 @@ exports.DEFAULT_CONFIG = {
|
|
|
67
67
|
// so the guardrail floor stays intact. Opt back into prompting via
|
|
68
68
|
// VECTOR_ASK=1 or the /mode toggle in the TUI.
|
|
69
69
|
permissionMode: 'yolo',
|
|
70
|
-
|
|
70
|
+
// 50 (dulu 25): task panjang sering mati di tengah — "runs out of
|
|
71
|
+
// iterations" — padahal agen masih di jalur yang benar. Naik tanpa
|
|
72
|
+
// risiko: iteration = 1 langkah agen (LLM call + tools), bukan 1 API
|
|
73
|
+
// key query; kuota gateway dijaga rate-limit per-request di sisi gateway.
|
|
74
|
+
maxIterations: 50,
|
|
71
75
|
maxRetries: 3,
|
|
72
76
|
requestTimeoutMs: 120_000,
|
|
73
77
|
extraModels: {},
|
package/dist/config/free-tier.js
CHANGED
|
@@ -66,9 +66,11 @@ exports.FREE_PROVIDER_ID = 'vectorhead-free';
|
|
|
66
66
|
* (ketemu via smoke test 0.3.22). Router Kilo terverifikasi live end-to-end
|
|
67
67
|
* (chat 200, dijawab dots-3-note-preview:free). Riwayat trap yang sama:
|
|
68
68
|
* `deepseek-ai/DeepSeek-V4-Flash` → `claude-sonnet-4-6` → `glm-5.3-flash`
|
|
69
|
-
* (dua flip 2026-09-06/07) →
|
|
69
|
+
* (dua flip 2026-09-06/07) → `kilo-auto/free` (2026-09-13) → **z-ai/glm-5.3-free**
|
|
70
|
+
* (2026-09-14: katalog gateway tinggal 2 model — `kilo-auto/free` TIDAK lagi
|
|
71
|
+
* dilayani; default baru terverifikasi live: 1 request → 200 OK, jawaban "hai").
|
|
70
72
|
*/
|
|
71
|
-
exports.FREE_DEFAULT_MODEL = '
|
|
73
|
+
exports.FREE_DEFAULT_MODEL = 'z-ai/glm-5.3-free';
|
|
72
74
|
/** The providers-map entry for the free tier (idempotent — same shape always). */
|
|
73
75
|
function freeProviderDef() {
|
|
74
76
|
// When a catalog has been fetched in this process, the gateway URL and
|
|
@@ -94,7 +96,7 @@ function freeProviderDef() {
|
|
|
94
96
|
},
|
|
95
97
|
model: exports.FREE_DEFAULT_MODEL,
|
|
96
98
|
models: Object.keys(models).length > 0 ? models : {
|
|
97
|
-
[exports.FREE_DEFAULT_MODEL]: { name: '
|
|
99
|
+
[exports.FREE_DEFAULT_MODEL]: { name: 'GLM 5.3 (free)' },
|
|
98
100
|
},
|
|
99
101
|
};
|
|
100
102
|
}
|
|
@@ -108,6 +110,7 @@ exports.FREE_LEGACY_MODELS = [
|
|
|
108
110
|
'deepseek-ai/DeepSeek-V4-Flash',
|
|
109
111
|
'claude-sonnet-4-6',
|
|
110
112
|
'glm-5.3-flash',
|
|
113
|
+
'kilo-auto/free',
|
|
111
114
|
];
|
|
112
115
|
/**
|
|
113
116
|
* Ensure the free provider entry exists in the config's providers map
|
package/dist/tui/app.js
CHANGED
|
@@ -59,6 +59,7 @@ const commands_1 = require("./commands");
|
|
|
59
59
|
const suggest_1 = require("./suggest");
|
|
60
60
|
const mcp_1 = require("../tools/mcp");
|
|
61
61
|
const provider_1 = require("../providers/provider");
|
|
62
|
+
const checkpoints_1 = require("../agent/checkpoints");
|
|
62
63
|
const planner_1 = require("../agent/planner");
|
|
63
64
|
const extras_1 = require("../tools/extras");
|
|
64
65
|
const session_1 = require("../agent/session");
|
|
@@ -2163,6 +2164,17 @@ class App {
|
|
|
2163
2164
|
case '/session':
|
|
2164
2165
|
this.modal = { type: 'session', selected: 0, start: 0 };
|
|
2165
2166
|
break;
|
|
2167
|
+
case '/undo': {
|
|
2168
|
+
// Freebuff/Codebuff-style safety net: pick a checkpoint (auto taken
|
|
2169
|
+
// before the first mutating tool call of each run) and restore it.
|
|
2170
|
+
const checkpoints = (0, checkpoints_1.listCheckpoints)(this.agent.cwd);
|
|
2171
|
+
if (checkpoints.length === 0) {
|
|
2172
|
+
this.addSystem('Belum ada checkpoint — snapshot dibuat otomatis sebelum tool menulis pertama kali di sebuah run.');
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
this.modal = { type: 'undo', selected: 0, start: 0, checkpoints };
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2166
2178
|
case '/settings':
|
|
2167
2179
|
this.modal = { type: 'settings' };
|
|
2168
2180
|
break;
|
|
@@ -2414,8 +2426,11 @@ class App {
|
|
|
2414
2426
|
// Esc still closes. Changes persist via saveConfig immediately.
|
|
2415
2427
|
{
|
|
2416
2428
|
const cfg = this.agent.config;
|
|
2429
|
+
// Cap 999 (dulu 200): task besar (refactor menyeluruh, migrasi,
|
|
2430
|
+
// riset panjang) butuh budget lebih — 200 sering masih kurang.
|
|
2431
|
+
const cap = 999;
|
|
2417
2432
|
const step = (d) => {
|
|
2418
|
-
const next = Math.min(
|
|
2433
|
+
const next = Math.min(cap, Math.max(1, cfg.maxIterations + d));
|
|
2419
2434
|
if (next === cfg.maxIterations)
|
|
2420
2435
|
return;
|
|
2421
2436
|
cfg.maxIterations = next;
|
|
@@ -2451,7 +2466,7 @@ class App {
|
|
|
2451
2466
|
else if (ev.name === 'char' && ev.char && /[0-9]/.test(ev.char)) {
|
|
2452
2467
|
// Number keys type the exact value (not digit-append — no
|
|
2453
2468
|
// visible field, so append would be invisible state).
|
|
2454
|
-
const next = Math.min(
|
|
2469
|
+
const next = Math.min(cap, Math.max(1, Number(ev.char)));
|
|
2455
2470
|
if (next !== cfg.maxIterations) {
|
|
2456
2471
|
cfg.maxIterations = next;
|
|
2457
2472
|
try {
|
|
@@ -2527,6 +2542,41 @@ class App {
|
|
|
2527
2542
|
this.modal = { type: 'none' };
|
|
2528
2543
|
});
|
|
2529
2544
|
break;
|
|
2545
|
+
case 'undo': {
|
|
2546
|
+
const cps = modal.checkpoints;
|
|
2547
|
+
await this.handleSelector(modal, ev, cps.map((c) => ({
|
|
2548
|
+
label: `${c.time.replace('T', ' ').slice(0, 19)} — ${c.task || c.reason}`,
|
|
2549
|
+
value: c.ref,
|
|
2550
|
+
hint: c.reason,
|
|
2551
|
+
})), async (sel) => {
|
|
2552
|
+
this.modal = { type: 'none' };
|
|
2553
|
+
if (this.running) {
|
|
2554
|
+
this.addSystem('Tidak bisa restore saat agen sedang jalan — hentikan dulu (Esc).');
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
// Confirm: restore MENIMPA perubahan yang belum di-commit.
|
|
2558
|
+
this.modal = {
|
|
2559
|
+
type: 'confirm',
|
|
2560
|
+
title: 'Restore checkpoint',
|
|
2561
|
+
body: [
|
|
2562
|
+
`Kembalikan seluruh file project ke snapshot ${sel.value}?`,
|
|
2563
|
+
'',
|
|
2564
|
+
'Perubahan yang BELUM di-commit akan tertimpa (riwayat git kamu tetap utuh —',
|
|
2565
|
+
'restore hanya menyentuh working tree).',
|
|
2566
|
+
],
|
|
2567
|
+
focused: 'cancel',
|
|
2568
|
+
resolve: async (ok) => {
|
|
2569
|
+
this.modal = { type: 'none' };
|
|
2570
|
+
if (!ok)
|
|
2571
|
+
return;
|
|
2572
|
+
const r = (0, checkpoints_1.restoreCheckpoint)(this.agent.cwd, sel.value);
|
|
2573
|
+
this.addSystem(r.ok ? `⏪ ${r.message}` : `! ${r.message}`);
|
|
2574
|
+
this.markDirty();
|
|
2575
|
+
},
|
|
2576
|
+
};
|
|
2577
|
+
});
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2530
2580
|
case 'session': {
|
|
2531
2581
|
const sessions = (0, session_1.listSessions)();
|
|
2532
2582
|
await this.handleSelector(modal, ev, sessions.map((s) => ({ label: s.name, value: s.name, hint: `${s.messageCount} msgs` })), async (s) => {
|
|
@@ -3324,7 +3374,7 @@ class App {
|
|
|
3324
3374
|
label('Mode', this.agent.config.permissionMode),
|
|
3325
3375
|
// LIVE-EDITABLE: +/- / ↑↓ / PgUp-PgDn step the budget, digits
|
|
3326
3376
|
// type an exact value — persisted to ~/.vector/config.json.
|
|
3327
|
-
label('Max iterations', `${this.agent.config.maxIterations} ${theme_1.THEME.muted}(+/- / ↑↓ / PgUp-PgDn / angka)${theme_1.THEME.reset}`),
|
|
3377
|
+
label('Max iterations', `${this.agent.config.maxIterations} ${theme_1.THEME.muted}(+/- / ↑↓ / PgUp-PgDn / angka · cap 999)${theme_1.THEME.reset}`),
|
|
3328
3378
|
label('Max retries', String(this.agent.config.maxRetries)),
|
|
3329
3379
|
label('API URL', this.agent.config.apiUrl || '(default)'),
|
|
3330
3380
|
label('Key configured', (0, credentials_1.hasApiKey)(this.agent.config.provider) ? 'yes' : 'no'),
|
|
@@ -3414,6 +3464,22 @@ class App {
|
|
|
3414
3464
|
cols,
|
|
3415
3465
|
});
|
|
3416
3466
|
}
|
|
3467
|
+
case 'undo': {
|
|
3468
|
+
return (0, components_1.renderSelector)({
|
|
3469
|
+
title: 'Restore checkpoint',
|
|
3470
|
+
options: m.checkpoints.length > 0
|
|
3471
|
+
? m.checkpoints.map((c) => ({
|
|
3472
|
+
label: `${c.time.replace('T', ' ').slice(0, 19)} — ${c.task || c.reason}`,
|
|
3473
|
+
value: c.ref,
|
|
3474
|
+
hint: c.reason,
|
|
3475
|
+
}))
|
|
3476
|
+
: [{ label: '(no checkpoints yet)', value: '' }],
|
|
3477
|
+
selected: m.selected,
|
|
3478
|
+
start: m.start,
|
|
3479
|
+
rows,
|
|
3480
|
+
cols,
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3417
3483
|
case 'theme': {
|
|
3418
3484
|
const themes = (0, themes_1.listThemes)();
|
|
3419
3485
|
return (0, components_1.renderSelector)({
|
package/dist/tui/commands.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.COMMANDS = [
|
|
|
23
23
|
{ id: 'model', description: 'select model for the active provider' },
|
|
24
24
|
{ id: 'theme', description: 'select theme (OpenCode-style)' },
|
|
25
25
|
{ id: 'session', description: 'manage sessions (list / switch)' },
|
|
26
|
+
{ id: 'undo', description: 'restore project files to a checkpoint (auto-snapshot before edits)' },
|
|
26
27
|
{ id: 'session-name', description: 'set session name (arg optional)' },
|
|
27
28
|
{ id: 'settings', description: 'view settings (provider, model, limits, files)' },
|
|
28
29
|
{ id: 'mcp', description: 'MCP servers — probe config, list tools per server' },
|
package/package.json
CHANGED