agen-vektor 0.3.6 → 0.3.8
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 +17 -23
- package/dist/agent/agent.js +19 -9
- package/dist/agent/loop.js +11 -5
- package/dist/cli/index.js +87 -11
- package/dist/providers/openai-compat.js +11 -0
- package/dist/tui/app.js +188 -46
- package/dist/tui/chat.js +3 -2
- package/dist/tui/input.js +31 -29
- package/dist/tui/statusbar.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,30 +12,25 @@
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
│
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
│ $ npm test │
|
|
29
|
-
│ │
|
|
30
|
-
│ ✓ Tests passed │
|
|
31
|
-
│ │
|
|
32
|
-
├──────────────────────────────────────────────────────────────┤
|
|
33
|
-
│ > _ │
|
|
34
|
-
├──────────────────────────────────────────────────────────────┤
|
|
35
|
-
│ ENTER Send │ TAB Commands │ CTRL+C Stop │ ? Help │
|
|
36
|
-
╰──────────────────────────────────────────────────────────────╯
|
|
15
|
+
VectorHead ● Ready custom · deepseek-ai/DeepSeek-V4-Flash
|
|
16
|
+
Project: ~/my-project Mode: ask
|
|
17
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
│ Perbaiki error authentication pada project ini
|
|
19
|
+
VectorHead akan memeriksa file auth, memperbaiki, lalu menjalankan test.
|
|
20
|
+
|
|
21
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
⚙ shell · $ npm test
|
|
23
|
+
✓ Tests passed
|
|
24
|
+
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
25
|
+
│ ▍ Enter a coding task or / for commands │
|
|
26
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
27
|
+
● Ready ● ask
|
|
37
28
|
```
|
|
38
29
|
|
|
30
|
+
A Freebuff-style terminal UI: chat history with a bold-white assistant line,
|
|
31
|
+
compact agent-activity strip (⚙ tool calls), and a rounded input box with a
|
|
32
|
+
block cursor and placeholder — all rendered incrementally with no flicker.
|
|
33
|
+
|
|
39
34
|
## Features
|
|
40
35
|
|
|
41
36
|
- **Interactive TUI** — chat, agent status, tool execution, diff viewer, input. Keyboard driven, mouse-aware, works on 80×24 terminals and survives resize.
|
|
@@ -213,7 +208,6 @@ npm run build
|
|
|
213
208
|
- Web search tool (provider-backed) behind the `web` abstraction
|
|
214
209
|
- Interactive diff approval before edits in safe mode
|
|
215
210
|
- Persistent permission rules per project
|
|
216
|
-
- Publish under a dedicated npm package name for the VectorHead brand
|
|
217
211
|
- Windows support (WSL-focused)
|
|
218
212
|
|
|
219
213
|
## License
|
package/dist/agent/agent.js
CHANGED
|
@@ -35,31 +35,41 @@ class Agent {
|
|
|
35
35
|
]);
|
|
36
36
|
}
|
|
37
37
|
async run(request, signal) {
|
|
38
|
-
return
|
|
38
|
+
return this.runLoop(request, [], signal);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run continuing from a prior conversation (multi-turn continuity).
|
|
42
|
+
* The previous turn's messages are passed as history so the model
|
|
43
|
+
* remembers earlier exchanges instead of starting fresh every turn.
|
|
44
|
+
*/
|
|
45
|
+
async runWithHistory(request, history, signal) {
|
|
46
|
+
return this.runLoop(request, history, signal);
|
|
47
|
+
}
|
|
48
|
+
async runLoop(request, history, signal) {
|
|
49
|
+
const result = await (0, loop_1.runAgentLoop)(request, {
|
|
39
50
|
provider: this.provider,
|
|
40
51
|
tools: this.tools,
|
|
41
52
|
permissions: this.permissions,
|
|
42
53
|
config: this.config,
|
|
43
54
|
cwd: this.cwd,
|
|
44
55
|
signal,
|
|
56
|
+
history,
|
|
45
57
|
}, this.callbacks);
|
|
58
|
+
this.lastMessages = result.messages;
|
|
59
|
+
return result;
|
|
46
60
|
}
|
|
47
61
|
/**
|
|
48
62
|
* Run with optional prior session messages (non-interactive continue).
|
|
49
|
-
* If sessionMessages is provided, the
|
|
63
|
+
* If sessionMessages is provided, the loop continues from that history.
|
|
50
64
|
*/
|
|
51
65
|
async runWithContext(request, session, cb = {}) {
|
|
52
66
|
this.setCallbacks(cb);
|
|
53
67
|
if (!session)
|
|
54
68
|
return this.run(request);
|
|
55
|
-
|
|
56
|
-
const context = session.messages
|
|
57
|
-
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
58
|
-
.slice(-30)
|
|
59
|
-
.map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content.slice(0, 2000)}`)
|
|
60
|
-
.join('\n\n');
|
|
61
|
-
return this.run(`[Previous conversation summary]\n${context}\n\n---\n\nTASK:\n${request}`);
|
|
69
|
+
return this.runWithHistory(request, session.messages.filter((m) => m.role !== 'system').slice(-40));
|
|
62
70
|
}
|
|
71
|
+
/** Raw messages of the most recent run (for cross-turn continuity). */
|
|
72
|
+
lastMessages = [];
|
|
63
73
|
/** Rebuild the provider (e.g. after changing provider id). */
|
|
64
74
|
rebuildProvider() {
|
|
65
75
|
this.provider = (0, factory_1.createProvider)(this.config);
|
package/dist/agent/loop.js
CHANGED
|
@@ -18,13 +18,19 @@ const prompts_1 = require("./prompts");
|
|
|
18
18
|
async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
19
19
|
const { provider, tools, permissions, config, cwd, signal } = opts;
|
|
20
20
|
const maxIterations = config.maxIterations;
|
|
21
|
-
// Seed conversation with system + project context + task
|
|
21
|
+
// Seed conversation with system + project context + task.
|
|
22
|
+
// On a continuation (history present) the project context is not
|
|
23
|
+
// re-injected: the model already saw it in the first turn, and repeating
|
|
24
|
+
// the folder listing on every turn makes the agent re-explain/echo it.
|
|
25
|
+
const history = (opts.history ?? []).filter((m) => m.role !== 'system');
|
|
26
|
+
const isContinuation = history.length > 0;
|
|
27
|
+
const context = isContinuation
|
|
28
|
+
? ''
|
|
29
|
+
: `${(0, context_1.buildProjectContext)(cwd)}\n\n${(0, context_1.listRootEntries)(cwd)}\n\n---\n\n`;
|
|
22
30
|
const messages = [
|
|
23
31
|
{ role: 'system', content: prompts_1.SYSTEM_PROMPT },
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
content: `${(0, context_1.buildProjectContext)(cwd)}\n\n${(0, context_1.listRootEntries)(cwd)}\n\n---\n\nTASK:\n${userRequest}`,
|
|
27
|
-
},
|
|
32
|
+
...history,
|
|
33
|
+
{ role: 'user', content: `${context}TASK:\n${userRequest}` },
|
|
28
34
|
];
|
|
29
35
|
let iterations = 0;
|
|
30
36
|
let toolCalls = 0;
|
package/dist/cli/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const logger_1 = require("../utils/logger");
|
|
|
23
23
|
const keyboard_1 = require("./keyboard");
|
|
24
24
|
const terminal_1 = require("../utils/terminal");
|
|
25
25
|
const session_1 = require("../agent/session");
|
|
26
|
+
const paths_2 = require("../utils/paths");
|
|
26
27
|
// Read version from package.json so --version always matches the published release.
|
|
27
28
|
function getVersion() {
|
|
28
29
|
try {
|
|
@@ -164,12 +165,29 @@ async function runNonInteractive(opts) {
|
|
|
164
165
|
}
|
|
165
166
|
console.log(`${terminal_1.ANSI.brightYellow}⚙ ${t}${terminal_1.ANSI.reset} ${terminal_1.ANSI.dim}${preview}${terminal_1.ANSI.reset}`);
|
|
166
167
|
},
|
|
167
|
-
onDelta: (d) => process.stdout.write(d),
|
|
168
|
-
onFinal: () => process.stdout.write('\n'),
|
|
169
168
|
});
|
|
169
|
+
// Print the final reply. The loop uses the non-streaming path, so the
|
|
170
|
+
// answer only arrives in result.content (never via onDelta).
|
|
171
|
+
const reply = result.content.trim();
|
|
172
|
+
if (reply) {
|
|
173
|
+
process.stdout.write(reply + '\n');
|
|
174
|
+
}
|
|
175
|
+
else if (result.stopped || result.aborted) {
|
|
176
|
+
console.log(`${terminal_1.ANSI.yellow}⏹ Stopped — no reply was produced.${terminal_1.ANSI.reset}`);
|
|
177
|
+
}
|
|
170
178
|
console.log(`\n${terminal_1.ANSI.dim}— ${result.iterations} iterations, ${result.toolCalls} tool calls${terminal_1.ANSI.reset}`);
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
// Persist the conversation (per-project session by default) so the
|
|
180
|
+
// session is not lost after the run finishes.
|
|
181
|
+
try {
|
|
182
|
+
const name = opts.sessionName || defaultSessionName(agent.cwd);
|
|
183
|
+
(0, session_1.saveSession)(name, result.messages, {
|
|
184
|
+
cwd: agent.cwd,
|
|
185
|
+
model: agent.config.model,
|
|
186
|
+
provider: agent.config.provider,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* ignore */
|
|
173
191
|
}
|
|
174
192
|
process.exit(0);
|
|
175
193
|
}
|
|
@@ -193,6 +211,13 @@ function latestSessionName() {
|
|
|
193
211
|
const sessions = (0, session_1.listSessions)();
|
|
194
212
|
return sessions.length > 0 ? sessions[0].name : undefined;
|
|
195
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Default session name: one persistent session per project directory,
|
|
216
|
+
* so conversations are never silently lost when no --session is given.
|
|
217
|
+
*/
|
|
218
|
+
function defaultSessionName(cwd) {
|
|
219
|
+
return (0, paths_2.sanitizeName)(cwd) || 'default';
|
|
220
|
+
}
|
|
196
221
|
async function runTui(opts) {
|
|
197
222
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
198
223
|
await runNonInteractive(opts);
|
|
@@ -207,11 +232,15 @@ async function runTui(opts) {
|
|
|
207
232
|
console.log(`${terminal_1.ANSI.yellow}⚠${terminal_1.ANSI.reset} No API key configured for provider "${config.provider}".\n` +
|
|
208
233
|
`Set ${providerEnv(config.provider)} in your environment, or run /provider inside VectorHead to add one.\n`);
|
|
209
234
|
}
|
|
210
|
-
// Resolve session name
|
|
235
|
+
// Resolve session name — default to a persistent per-project session so
|
|
236
|
+
// conversations are saved even when no --session flag is given.
|
|
211
237
|
let sessionName = opts.sessionName;
|
|
212
238
|
if (opts.continueSession && !sessionName) {
|
|
213
239
|
sessionName = latestSessionName();
|
|
214
240
|
}
|
|
241
|
+
if (!sessionName) {
|
|
242
|
+
sessionName = defaultSessionName(process.cwd());
|
|
243
|
+
}
|
|
215
244
|
let exit = false;
|
|
216
245
|
// Permission callback: drive a modal through the app
|
|
217
246
|
let app = null;
|
|
@@ -243,11 +272,46 @@ async function runTui(opts) {
|
|
|
243
272
|
});
|
|
244
273
|
// Terminal setup
|
|
245
274
|
const cleanup = (0, keyboard_1.enableRawMode)();
|
|
275
|
+
// Incremental renderer: keep the previous frame and only rewrite the rows
|
|
276
|
+
// that actually changed. Full-screen redraws every tick caused constant
|
|
277
|
+
// flicker, especially while the agent is running (spinner + timer animate
|
|
278
|
+
// every frame). Modal overlays are absolutely positioned, so they force a
|
|
279
|
+
// full repaint.
|
|
280
|
+
let lastFrameRows = [];
|
|
281
|
+
let lastCursorPos = '';
|
|
246
282
|
const doRender = () => {
|
|
247
|
-
|
|
248
|
-
|
|
283
|
+
if (app.modalActive()) {
|
|
284
|
+
// A modal (e.g. a permission prompt) is static while open, so only
|
|
285
|
+
// repaint when something actually changed (open/close/keystroke).
|
|
286
|
+
// Full-screen redraws every tick while waiting for permission caused
|
|
287
|
+
// the chat to flicker during multi-tool turns.
|
|
288
|
+
if (app.isDirty()) {
|
|
289
|
+
lastFrameRows = [];
|
|
290
|
+
process.stdout.write(terminal_1.ANSI.hideCursor + app.render());
|
|
291
|
+
app.consumeRender();
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const snap = app.frameSnapshot();
|
|
296
|
+
const rows = snap.rows;
|
|
297
|
+
const out = [];
|
|
298
|
+
for (let i = 0; i < rows.length; i++) {
|
|
299
|
+
if (lastFrameRows[i] !== rows[i]) {
|
|
300
|
+
out.push((0, terminal_1.cursorTo)(i + 1, 1) + terminal_1.ANSI.clearLineEnd + rows[i]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
lastFrameRows = rows;
|
|
304
|
+
const cursor = (0, terminal_1.cursorTo)(snap.inputRow, snap.cursorCol);
|
|
305
|
+
if (cursor !== lastCursorPos)
|
|
306
|
+
out.push(cursor);
|
|
307
|
+
lastCursorPos = cursor;
|
|
308
|
+
out.push(terminal_1.ANSI.showCursor);
|
|
309
|
+
process.stdout.write(terminal_1.ANSI.hideCursor + out.join(''));
|
|
310
|
+
};
|
|
311
|
+
const onResize = () => {
|
|
312
|
+
app.markDirty();
|
|
313
|
+
doRender();
|
|
249
314
|
};
|
|
250
|
-
const onResize = () => doRender();
|
|
251
315
|
process.stdout.on('resize', onResize);
|
|
252
316
|
process.on('SIGINT', () => {
|
|
253
317
|
// In raw mode Ctrl+C arrives as a key event (0x03), but some terminals
|
|
@@ -258,6 +322,12 @@ async function runTui(opts) {
|
|
|
258
322
|
else
|
|
259
323
|
process.exit(0);
|
|
260
324
|
});
|
|
325
|
+
process.on('SIGTERM', () => {
|
|
326
|
+
if (app)
|
|
327
|
+
app.requestExit();
|
|
328
|
+
else
|
|
329
|
+
process.exit(0);
|
|
330
|
+
});
|
|
261
331
|
process.on('SIGWINCH', onResize);
|
|
262
332
|
const keyHandler = (ev) => {
|
|
263
333
|
void app.handleKey(ev);
|
|
@@ -269,11 +339,17 @@ async function runTui(opts) {
|
|
|
269
339
|
(0, keyboard_1.keyStream)(keyHandler);
|
|
270
340
|
app.start();
|
|
271
341
|
doRender();
|
|
272
|
-
|
|
342
|
+
app.consumeRender();
|
|
343
|
+
// Main tick loop — repaint only when something changed. Repainting the
|
|
344
|
+
// whole frame every tick caused constant flicker and made keystrokes
|
|
345
|
+
// (Enter, Ctrl+C) feel laggy on slow/mobile connections.
|
|
273
346
|
while (!exit) {
|
|
274
347
|
await app.tick();
|
|
275
|
-
|
|
276
|
-
|
|
348
|
+
if (app.needsRender()) {
|
|
349
|
+
doRender();
|
|
350
|
+
app.consumeRender();
|
|
351
|
+
}
|
|
352
|
+
await sleep(80);
|
|
277
353
|
}
|
|
278
354
|
}
|
|
279
355
|
finally {
|
|
@@ -82,6 +82,17 @@ class OpenAICompatProvider {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
endpoint(path) {
|
|
85
|
+
if (!this.baseUrl) {
|
|
86
|
+
throw new provider_1.ProviderError('Custom API base URL is not set. Run /connect (or /setup) to enter it.', 0, false);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const u = new URL(this.baseUrl);
|
|
90
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
91
|
+
throw new Error('not http');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new provider_1.ProviderError(`Invalid API base URL "${this.baseUrl}". Run /connect to fix it (e.g. https://host/v1).`, 0, false);
|
|
95
|
+
}
|
|
85
96
|
return `${this.baseUrl}${path}`;
|
|
86
97
|
}
|
|
87
98
|
headers() {
|
package/dist/tui/app.js
CHANGED
|
@@ -79,6 +79,21 @@ class App {
|
|
|
79
79
|
abortController = null;
|
|
80
80
|
spinner = new theme_1.Spinner();
|
|
81
81
|
runStart = null;
|
|
82
|
+
/** set on every mutation so the render loop only repaints when needed */
|
|
83
|
+
dirty = true;
|
|
84
|
+
/**
|
|
85
|
+
* When true the chat view follows the newest messages. Disabled while the
|
|
86
|
+
* user scrolls up (PgUp); re-enabled when they scroll back to the bottom.
|
|
87
|
+
* Without this, new turns render below the visible area and replies to
|
|
88
|
+
* turns 2+ appear to "not respond".
|
|
89
|
+
*/
|
|
90
|
+
followBottom = true;
|
|
91
|
+
/**
|
|
92
|
+
* Compact Freebuff-style activity strip: the current/last tool action
|
|
93
|
+
* (e.g. searching folders, running a command) shown above the input.
|
|
94
|
+
* Kept out of the main chat transcript so the conversation stays clean.
|
|
95
|
+
*/
|
|
96
|
+
activity = null;
|
|
82
97
|
constructor(agent, opts) {
|
|
83
98
|
this.agent = agent;
|
|
84
99
|
this.opts = opts;
|
|
@@ -88,7 +103,20 @@ class App {
|
|
|
88
103
|
}
|
|
89
104
|
}
|
|
90
105
|
// ─── Public API ───────────────────────────────────────────────
|
|
106
|
+
/** Mark the screen as needing a repaint. */
|
|
107
|
+
markDirty() {
|
|
108
|
+
this.dirty = true;
|
|
109
|
+
}
|
|
110
|
+
/** True when the screen must be repainted (dirty, or streaming). */
|
|
111
|
+
needsRender() {
|
|
112
|
+
return this.dirty || this.running;
|
|
113
|
+
}
|
|
114
|
+
/** Clear the dirty flag after a repaint. */
|
|
115
|
+
consumeRender() {
|
|
116
|
+
this.dirty = false;
|
|
117
|
+
}
|
|
91
118
|
start() {
|
|
119
|
+
this.markDirty();
|
|
92
120
|
this.connected = true;
|
|
93
121
|
this.status = 'Ready';
|
|
94
122
|
if (this.opts.continueSession && this.sessionName) {
|
|
@@ -106,6 +134,7 @@ class App {
|
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
136
|
async handleKey(ev) {
|
|
137
|
+
this.markDirty();
|
|
109
138
|
if (this.modal.type !== 'none') {
|
|
110
139
|
await this.handleModalKey(ev);
|
|
111
140
|
return;
|
|
@@ -119,6 +148,10 @@ class App {
|
|
|
119
148
|
this.running = false;
|
|
120
149
|
this.addSystem('⏹ Stopped. Press Ctrl+C again to quit.');
|
|
121
150
|
}
|
|
151
|
+
else if (ev.name === 'ctrl_d') {
|
|
152
|
+
// Ctrl+D stops and quits in one go (handy on mobile)
|
|
153
|
+
this.requestExit();
|
|
154
|
+
}
|
|
122
155
|
return;
|
|
123
156
|
}
|
|
124
157
|
switch (ev.name) {
|
|
@@ -164,10 +197,14 @@ class App {
|
|
|
164
197
|
break;
|
|
165
198
|
case 'pageup':
|
|
166
199
|
this.scroll = (0, chat_1.clampScroll)(this.scroll - 10, this.messages, this.cols(), this.chatHeight());
|
|
200
|
+
this.followBottom = false;
|
|
167
201
|
break;
|
|
168
|
-
case 'pagedown':
|
|
202
|
+
case 'pagedown': {
|
|
169
203
|
this.scroll = (0, chat_1.clampScroll)(this.scroll + 10, this.messages, this.cols(), this.chatHeight());
|
|
204
|
+
const all = this.messages.length === 0 ? [] : (0, chat_1.renderMessages)(this.messages, this.cols());
|
|
205
|
+
this.followBottom = this.scroll >= Math.max(0, all.length - this.chatHeight());
|
|
170
206
|
break;
|
|
207
|
+
}
|
|
171
208
|
case 'ctrl_u':
|
|
172
209
|
this.input.deleteToStart();
|
|
173
210
|
break;
|
|
@@ -183,6 +220,8 @@ class App {
|
|
|
183
220
|
case 'ctrl_l':
|
|
184
221
|
this.messages = [];
|
|
185
222
|
this.scroll = 0;
|
|
223
|
+
this.followBottom = true;
|
|
224
|
+
this.activity = null;
|
|
186
225
|
this.addSystem('Conversation cleared.');
|
|
187
226
|
break;
|
|
188
227
|
case 'ctrl_d':
|
|
@@ -204,11 +243,13 @@ class App {
|
|
|
204
243
|
}
|
|
205
244
|
async tick() {
|
|
206
245
|
if (this.pendingPrompt) {
|
|
246
|
+
this.markDirty();
|
|
207
247
|
const p = this.pendingPrompt;
|
|
208
248
|
this.pendingPrompt = '';
|
|
209
249
|
await this.submit(p);
|
|
210
250
|
}
|
|
211
251
|
if (this.planToExecute) {
|
|
252
|
+
this.markDirty();
|
|
212
253
|
const plan = this.planToExecute;
|
|
213
254
|
this.planToExecute = null;
|
|
214
255
|
await this.executeWithPlan(plan);
|
|
@@ -222,6 +263,7 @@ class App {
|
|
|
222
263
|
* Used by Ctrl+C and SIGINT so the TUI never needs to be killed.
|
|
223
264
|
*/
|
|
224
265
|
requestExit() {
|
|
266
|
+
this.markDirty();
|
|
225
267
|
if (this.running) {
|
|
226
268
|
this.abortController?.abort();
|
|
227
269
|
this.running = false;
|
|
@@ -229,13 +271,23 @@ class App {
|
|
|
229
271
|
this.exitRequested = true;
|
|
230
272
|
}
|
|
231
273
|
// ─── Submission ───────────────────────────────────────────────
|
|
274
|
+
/** Snap the chat view to the newest messages (unless the user scrolled up). */
|
|
275
|
+
scrollToLatest() {
|
|
276
|
+
if (!this.followBottom)
|
|
277
|
+
return;
|
|
278
|
+
const { cols } = (0, terminal_1.getTerminalSize)();
|
|
279
|
+
const all = this.messages.length === 0 ? [] : (0, chat_1.renderMessages)(this.messages, cols);
|
|
280
|
+
this.scroll = Math.max(0, all.length - this.chatHeight());
|
|
281
|
+
}
|
|
232
282
|
async submit(text) {
|
|
283
|
+
this.markDirty();
|
|
233
284
|
if (text.startsWith('/')) {
|
|
234
285
|
await this.handleCommand(text);
|
|
235
286
|
return;
|
|
236
287
|
}
|
|
237
288
|
this.messages.push({ kind: 'user', content: text });
|
|
238
|
-
this.
|
|
289
|
+
this.activity = null;
|
|
290
|
+
this.scrollToLatest();
|
|
239
291
|
this.running = true;
|
|
240
292
|
this.status = 'Thinking';
|
|
241
293
|
this.statusColor = (0, theme_1.statusColor)('Thinking');
|
|
@@ -284,15 +336,19 @@ class App {
|
|
|
284
336
|
}) - 1;
|
|
285
337
|
this.agent.setCallbacks({
|
|
286
338
|
onDelta: (delta) => {
|
|
339
|
+
this.markDirty();
|
|
287
340
|
const m = this.messages[msgIndex];
|
|
288
341
|
if (m)
|
|
289
342
|
m.content += delta;
|
|
343
|
+
this.scrollToLatest();
|
|
290
344
|
},
|
|
291
345
|
onStatus: (status) => {
|
|
346
|
+
this.markDirty();
|
|
292
347
|
this.status = status;
|
|
293
348
|
this.statusColor = (0, theme_1.statusColor)(status);
|
|
294
349
|
},
|
|
295
350
|
onToolCall: (tool, args) => {
|
|
351
|
+
this.markDirty();
|
|
296
352
|
let preview = '';
|
|
297
353
|
try {
|
|
298
354
|
const parsed = JSON.parse(args);
|
|
@@ -303,17 +359,17 @@ class App {
|
|
|
303
359
|
catch {
|
|
304
360
|
preview = args.slice(0, 80);
|
|
305
361
|
}
|
|
306
|
-
|
|
362
|
+
// Show tool activity in the compact strip, not the chat transcript.
|
|
363
|
+
this.activity = { tool, detail: preview };
|
|
307
364
|
},
|
|
308
365
|
onToolResult: (tool, result) => {
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
if (result.split('\n').length > 12)
|
|
313
|
-
last.content += '\n…';
|
|
366
|
+
this.markDirty();
|
|
367
|
+
if (this.activity && this.activity.tool === tool) {
|
|
368
|
+
this.activity = { ...this.activity, result: result.split('\n').slice(0, 8).join('\n') };
|
|
314
369
|
}
|
|
315
370
|
},
|
|
316
371
|
onError: (err) => {
|
|
372
|
+
this.markDirty();
|
|
317
373
|
const m = this.messages[msgIndex];
|
|
318
374
|
if (m) {
|
|
319
375
|
m.kind = 'error';
|
|
@@ -322,18 +378,23 @@ class App {
|
|
|
322
378
|
}
|
|
323
379
|
},
|
|
324
380
|
onFinal: (result) => {
|
|
381
|
+
this.markDirty();
|
|
325
382
|
const m = this.messages[msgIndex];
|
|
326
383
|
if (m) {
|
|
327
384
|
m.content = result.content || m.content;
|
|
328
385
|
m.streaming = false;
|
|
329
386
|
}
|
|
387
|
+
this.scrollToLatest();
|
|
330
388
|
},
|
|
331
389
|
});
|
|
390
|
+
let stoppedRun = false;
|
|
332
391
|
try {
|
|
333
|
-
await this.agent.
|
|
392
|
+
const res = await this.agent.runWithHistory(task, this.agent.lastMessages.filter((m) => m.role !== 'system').slice(-40), this.abortController?.signal);
|
|
393
|
+
stoppedRun = res.stopped || res.aborted;
|
|
334
394
|
}
|
|
335
395
|
catch (err) {
|
|
336
396
|
if (err.message === 'aborted') {
|
|
397
|
+
stoppedRun = true;
|
|
337
398
|
const m = this.messages[msgIndex];
|
|
338
399
|
if (m) {
|
|
339
400
|
m.content += '\n\n⏹ (stopped by user)';
|
|
@@ -353,8 +414,10 @@ class App {
|
|
|
353
414
|
const m = this.messages[msgIndex];
|
|
354
415
|
if (m) {
|
|
355
416
|
m.streaming = false;
|
|
356
|
-
|
|
357
|
-
|
|
417
|
+
// Keep any partial reply; only fall back when there is nothing.
|
|
418
|
+
if (!m.content.trim()) {
|
|
419
|
+
m.content = stoppedRun ? '⏹ (stopped — no reply)' : '(no response)';
|
|
420
|
+
}
|
|
358
421
|
}
|
|
359
422
|
this.running = false;
|
|
360
423
|
this.runStart = null;
|
|
@@ -386,6 +449,7 @@ class App {
|
|
|
386
449
|
}));
|
|
387
450
|
}
|
|
388
451
|
async executeWithPlan(plan) {
|
|
452
|
+
this.markDirty();
|
|
389
453
|
this.running = true;
|
|
390
454
|
this.status = 'Executing plan';
|
|
391
455
|
this.statusColor = terminal_1.ANSI.yellow;
|
|
@@ -429,6 +493,8 @@ class App {
|
|
|
429
493
|
case '/clear':
|
|
430
494
|
this.messages = [];
|
|
431
495
|
this.scroll = 0;
|
|
496
|
+
this.followBottom = true;
|
|
497
|
+
this.activity = null;
|
|
432
498
|
this.addSystem('Conversation cleared.');
|
|
433
499
|
break;
|
|
434
500
|
case '/continue':
|
|
@@ -478,6 +544,21 @@ class App {
|
|
|
478
544
|
this.addSystem(`Current session: ${this.sessionName || '(none)'}`);
|
|
479
545
|
}
|
|
480
546
|
break;
|
|
547
|
+
case '/connect':
|
|
548
|
+
// Wizard for a custom OpenAI-compatible API: base URL + token
|
|
549
|
+
this.agent.config.provider = 'custom';
|
|
550
|
+
this.agent.rebuildProvider();
|
|
551
|
+
(0, config_1.saveConfig)(this.agent.config);
|
|
552
|
+
this.addSystem('Custom provider — masukkan base URL API (mis. https://…/v1), lalu token.');
|
|
553
|
+
this.modal = { type: 'custom-url', value: this.agent.config.apiUrl || '' };
|
|
554
|
+
break;
|
|
555
|
+
case '/agent':
|
|
556
|
+
this.agent.setMode(this.agent.config.permissionMode === 'yolo' ? 'ask' : 'yolo');
|
|
557
|
+
(0, config_1.saveConfig)(this.agent.config);
|
|
558
|
+
this.addSystem(this.agent.config.permissionMode === 'yolo'
|
|
559
|
+
? 'Agent mode: YOLO — prompt permission untuk aksi ask-level dilewati.'
|
|
560
|
+
: 'Agent mode: ask — prompt permission aktif.');
|
|
561
|
+
break;
|
|
481
562
|
default:
|
|
482
563
|
this.addSystem(`Unknown command: ${name} (type /help for commands)`);
|
|
483
564
|
break;
|
|
@@ -512,9 +593,12 @@ class App {
|
|
|
512
593
|
}
|
|
513
594
|
else if (ev.name === 'pageup') {
|
|
514
595
|
this.scroll = (0, chat_1.clampScroll)(this.scroll - 10, this.messages, this.cols(), this.chatHeight());
|
|
596
|
+
this.followBottom = false;
|
|
515
597
|
}
|
|
516
598
|
else if (ev.name === 'pagedown') {
|
|
517
599
|
this.scroll = (0, chat_1.clampScroll)(this.scroll + 10, this.messages, this.cols(), this.chatHeight());
|
|
600
|
+
const all = this.messages.length === 0 ? [] : (0, chat_1.renderMessages)(this.messages, this.cols());
|
|
601
|
+
this.followBottom = this.scroll >= Math.max(0, all.length - this.chatHeight());
|
|
518
602
|
}
|
|
519
603
|
break;
|
|
520
604
|
case 'provider':
|
|
@@ -625,14 +709,17 @@ class App {
|
|
|
625
709
|
case 'apikey':
|
|
626
710
|
this.handleTextInput(ev, (value) => {
|
|
627
711
|
(0, credentials_1.setApiKey)(modal.provider, value);
|
|
712
|
+
this.agent.rebuildProvider();
|
|
628
713
|
this.addSystem(`API key stored for ${modal.provider} (not printed).`);
|
|
629
714
|
this.modal = { type: 'none' };
|
|
630
715
|
});
|
|
631
716
|
break;
|
|
632
717
|
case 'custom-url':
|
|
633
718
|
this.handleTextInput(ev, (value) => {
|
|
634
|
-
this.agent.config.apiUrl = value;
|
|
719
|
+
this.agent.config.apiUrl = value.trim();
|
|
635
720
|
(0, config_1.saveConfig)(this.agent.config);
|
|
721
|
+
// Rebuild so the live provider picks up the new base URL
|
|
722
|
+
this.agent.rebuildProvider();
|
|
636
723
|
this.addSystem('Custom API URL set.');
|
|
637
724
|
if (!(0, credentials_1.hasApiKey)('custom')) {
|
|
638
725
|
this.modal = { type: 'custom-key', value: '' };
|
|
@@ -644,7 +731,9 @@ class App {
|
|
|
644
731
|
break;
|
|
645
732
|
case 'custom-key':
|
|
646
733
|
this.handleTextInput(ev, (value) => {
|
|
647
|
-
(0, credentials_1.setApiKey)('custom', value);
|
|
734
|
+
(0, credentials_1.setApiKey)('custom', value.trim());
|
|
735
|
+
// Rebuild so the live provider picks up the new API key
|
|
736
|
+
this.agent.rebuildProvider();
|
|
648
737
|
this.addSystem('Custom API key stored (not printed).');
|
|
649
738
|
this.modal = { type: 'none' };
|
|
650
739
|
});
|
|
@@ -689,10 +778,13 @@ class App {
|
|
|
689
778
|
}
|
|
690
779
|
// ─── Message helpers ──────────────────────────────────────────
|
|
691
780
|
addSystem(text) {
|
|
781
|
+
this.markDirty();
|
|
692
782
|
this.messages.push({ kind: 'system', content: text });
|
|
783
|
+
this.scrollToLatest();
|
|
693
784
|
}
|
|
694
785
|
/** Show a permission modal (called by the permission callback). */
|
|
695
786
|
setPermissionModal(req) {
|
|
787
|
+
this.markDirty();
|
|
696
788
|
this.modal = {
|
|
697
789
|
type: 'permission',
|
|
698
790
|
req,
|
|
@@ -706,16 +798,35 @@ class App {
|
|
|
706
798
|
this.permissionResolver = fn;
|
|
707
799
|
}
|
|
708
800
|
addTool(tool, content) {
|
|
801
|
+
this.markDirty();
|
|
709
802
|
this.messages.push({ kind: 'tool', tool, content });
|
|
803
|
+
this.scrollToLatest();
|
|
710
804
|
}
|
|
711
805
|
// ─── Rendering ────────────────────────────────────────────────
|
|
712
806
|
cols() {
|
|
713
807
|
return (0, terminal_1.getTerminalSize)().cols;
|
|
714
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Rows reserved for the activity strip above the input.
|
|
811
|
+
* FIXED at 3 while working (or while the last tool action is shown) so the
|
|
812
|
+
* chat area never resizes between tool calls — a dynamic height made the
|
|
813
|
+
* separator/chat reflow on every tool result, which looked like the screen
|
|
814
|
+
* blinking during multi-tool turns (e.g. "apakah vectorhead sehat").
|
|
815
|
+
*/
|
|
816
|
+
activityHeight() {
|
|
817
|
+
return this.running || this.activity ? 3 : 0;
|
|
818
|
+
}
|
|
715
819
|
chatHeight() {
|
|
716
|
-
|
|
820
|
+
// Fixed rows: header(1) + project(1) + sep(1) + chatSep(1) + input box(3)
|
|
821
|
+
// + status(1) = 8, plus the activity strip when working.
|
|
822
|
+
return Math.max(1, (0, terminal_1.getTerminalSize)().rows - 8 - this.activityHeight());
|
|
717
823
|
}
|
|
718
|
-
|
|
824
|
+
/**
|
|
825
|
+
* Compute the full frame as plain row strings (index 0 = terminal row 1)
|
|
826
|
+
* plus the input cursor position. Modal overlays are excluded (they are
|
|
827
|
+
* absolutely positioned and handled separately by render()).
|
|
828
|
+
*/
|
|
829
|
+
computeFrame() {
|
|
719
830
|
const { rows, cols } = (0, terminal_1.getTerminalSize)();
|
|
720
831
|
const info = {
|
|
721
832
|
status: this.status,
|
|
@@ -729,48 +840,77 @@ class App {
|
|
|
729
840
|
running: this.running,
|
|
730
841
|
elapsed: this.running && this.runStart !== null ? Math.floor((Date.now() - this.runStart) / 1000) : undefined,
|
|
731
842
|
};
|
|
732
|
-
const parts = [];
|
|
733
|
-
// Row 1-2: header + project line
|
|
734
|
-
parts.push((0, terminal_1.cursorTo)(1, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderHeader)(cols, info));
|
|
735
|
-
parts.push((0, terminal_1.cursorTo)(2, 1) + terminal_1.ANSI.clearLineEnd + (0, statusbar_1.renderProjectBar)(cols, info));
|
|
736
|
-
// Row 3: separator
|
|
737
|
-
parts.push((0, terminal_1.cursorTo)(3, 1) + terminal_1.ANSI.clearLineEnd + theme_1.THEME.border + terminal_1.BOX.h.repeat(Math.min(cols, 120)) + theme_1.THEME.reset);
|
|
738
|
-
// Chat area (rows 4..) — Freebuff-style message blocks (welcome screen when empty)
|
|
739
843
|
const chatH = this.chatHeight();
|
|
740
844
|
const all = this.messages.length === 0 ? (0, chat_1.renderWelcome)(cols, chatH, this.agent.cwd) : (0, chat_1.renderMessages)(this.messages, cols);
|
|
741
845
|
const maxScroll = Math.max(0, all.length - chatH);
|
|
742
846
|
this.scroll = Math.min(this.scroll, maxScroll);
|
|
743
847
|
const visible = all.slice(this.scroll, this.scroll + chatH);
|
|
744
848
|
const chatTop = 4;
|
|
745
|
-
for (let i = 0; i < chatH; i++) {
|
|
746
|
-
const content = visible[i] !== undefined ? visible[i] : '';
|
|
747
|
-
parts.push((0, terminal_1.cursorTo)(chatTop + i, 1) + terminal_1.ANSI.clearLineEnd + content);
|
|
748
|
-
}
|
|
749
|
-
// Separator between chat and input
|
|
750
849
|
const sepRow = chatTop + chatH;
|
|
751
|
-
|
|
752
|
-
//
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
|
|
850
|
+
const activityH = this.activityHeight();
|
|
851
|
+
// Freebuff-style rounded input box (3 rows: top border, content, bottom).
|
|
852
|
+
const boxTop = sepRow + activityH + 1;
|
|
853
|
+
const boxMid = boxTop + 1;
|
|
854
|
+
const boxBot = boxTop + 2;
|
|
855
|
+
const statusRow = boxBot + 1;
|
|
856
|
+
const rendered = this.input.render(cols, input_1.DEFAULT_PLACEHOLDER);
|
|
756
857
|
const cursorCol = rendered.cursorCol + 1;
|
|
757
|
-
//
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
858
|
+
// Inner width is cols - 2 (between the borders); pad to fill it exactly.
|
|
859
|
+
const padInner = Math.max(0, cols - 2 - (0, terminal_1.visibleWidth)(rendered.line));
|
|
860
|
+
const frame = new Array(rows).fill('');
|
|
861
|
+
frame[0] = (0, statusbar_1.renderHeader)(cols, info);
|
|
862
|
+
frame[1] = (0, statusbar_1.renderProjectBar)(cols, info);
|
|
863
|
+
frame[2] = theme_1.THEME.border + terminal_1.BOX.h.repeat(Math.min(cols, 120)) + theme_1.THEME.reset;
|
|
864
|
+
for (let i = 0; i < chatH; i++) {
|
|
865
|
+
frame[chatTop - 1 + i] = visible[i] !== undefined ? visible[i] : '';
|
|
866
|
+
}
|
|
867
|
+
frame[sepRow - 1] = theme_1.THEME.border + terminal_1.BOX.h.repeat(Math.min(cols, 120)) + theme_1.THEME.reset;
|
|
868
|
+
if (activityH > 0 && this.activity) {
|
|
869
|
+
const actRows = (0, statusbar_1.renderActivity)(this.activity, cols, activityH);
|
|
870
|
+
for (let i = 0; i < actRows.length; i++)
|
|
871
|
+
frame[sepRow + i] = actRows[i];
|
|
872
|
+
}
|
|
873
|
+
// Freebuff renders the input box border in the foreground color (white).
|
|
874
|
+
const boxBorder = `${theme_1.THEME.bold}${theme_1.THEME.textBright}`;
|
|
875
|
+
frame[boxTop - 1] = `${boxBorder}╭${terminal_1.BOX.h.repeat(Math.max(0, cols - 2))}╮${theme_1.THEME.reset}`;
|
|
876
|
+
frame[boxMid - 1] = `${boxBorder}│${theme_1.THEME.reset}${rendered.line}${' '.repeat(padInner)}${boxBorder}│${theme_1.THEME.reset}`;
|
|
877
|
+
frame[boxBot - 1] = `${boxBorder}╰${terminal_1.BOX.h.repeat(Math.max(0, cols - 2))}╯${theme_1.THEME.reset}`;
|
|
878
|
+
frame[statusRow - 1] = (0, statusbar_1.renderStatusBar)(cols, info);
|
|
879
|
+
return { rows: frame, inputRow: boxMid, cursorCol };
|
|
880
|
+
}
|
|
881
|
+
/** True while a modal overlay is shown (its rows are absolutely positioned). */
|
|
882
|
+
modalActive() {
|
|
883
|
+
return this.modal.type !== 'none';
|
|
884
|
+
}
|
|
885
|
+
/** True when a repaint is needed because something actually changed. */
|
|
886
|
+
isDirty() {
|
|
887
|
+
return this.dirty;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Frame snapshot for the CLI's incremental renderer: plain rows + cursor.
|
|
891
|
+
* The caller compares rows with the previous frame and only rewrites the
|
|
892
|
+
* changed lines — this is what eliminates the full-screen flicker.
|
|
893
|
+
*/
|
|
894
|
+
frameSnapshot() {
|
|
895
|
+
return this.computeFrame();
|
|
896
|
+
}
|
|
897
|
+
render() {
|
|
898
|
+
const { rows, cols } = (0, terminal_1.getTerminalSize)();
|
|
899
|
+
const { rows: frame, inputRow, cursorCol } = this.computeFrame();
|
|
900
|
+
const parts = [];
|
|
901
|
+
for (let r = 0; r < rows; r++) {
|
|
902
|
+
parts.push((0, terminal_1.cursorTo)(r + 1, 1) + terminal_1.ANSI.clearLineEnd + frame[r]);
|
|
903
|
+
}
|
|
761
904
|
if (this.modal.type !== 'none') {
|
|
762
905
|
const overlay = this.renderModal(rows, cols);
|
|
763
906
|
for (const l of overlay.lines)
|
|
764
907
|
parts.push(l);
|
|
908
|
+
parts.push(terminal_1.ANSI.hideCursor);
|
|
765
909
|
}
|
|
766
|
-
|
|
767
|
-
if (this.modal.type === 'none') {
|
|
910
|
+
else {
|
|
768
911
|
parts.push((0, terminal_1.cursorTo)(inputRow, cursorCol));
|
|
769
912
|
parts.push(terminal_1.ANSI.showCursor);
|
|
770
913
|
}
|
|
771
|
-
else {
|
|
772
|
-
parts.push(terminal_1.ANSI.hideCursor);
|
|
773
|
-
}
|
|
774
914
|
return parts.join('');
|
|
775
915
|
}
|
|
776
916
|
renderModal(rows, cols) {
|
|
@@ -799,11 +939,12 @@ class App {
|
|
|
799
939
|
' PgUp/Dn scroll chat',
|
|
800
940
|
' ? this help',
|
|
801
941
|
' Ctrl+C stop agent / quit',
|
|
942
|
+
' Ctrl+D stop + quit (mobile)',
|
|
802
943
|
' Ctrl+L clear conversation',
|
|
803
944
|
'',
|
|
804
|
-
'Commands: /setup /model /provider
|
|
805
|
-
'/settings /status /clear /continue
|
|
806
|
-
'/git /compact /
|
|
945
|
+
'Commands: /connect /setup /model /provider',
|
|
946
|
+
'/session /settings /status /clear /continue',
|
|
947
|
+
'/diff /git /compact /agent /exit',
|
|
807
948
|
],
|
|
808
949
|
}, rows, cols);
|
|
809
950
|
case 'commands':
|
|
@@ -811,7 +952,8 @@ class App {
|
|
|
811
952
|
title: 'Commands',
|
|
812
953
|
body: [
|
|
813
954
|
'/help show help',
|
|
814
|
-
'/
|
|
955
|
+
'/connect set custom API base URL + token',
|
|
956
|
+
'/setup setup provider',
|
|
815
957
|
'/model select model',
|
|
816
958
|
'/provider select provider',
|
|
817
959
|
'/session manage sessions',
|
|
@@ -822,7 +964,7 @@ class App {
|
|
|
822
964
|
'/diff show diff',
|
|
823
965
|
'/git git status',
|
|
824
966
|
'/compact compact context',
|
|
825
|
-
'/
|
|
967
|
+
'/agent toggle permission mode (ask/yolo)',
|
|
826
968
|
'/exit quit',
|
|
827
969
|
],
|
|
828
970
|
}, rows, cols);
|
package/dist/tui/chat.js
CHANGED
|
@@ -83,13 +83,14 @@ function renderMessage(m, width) {
|
|
|
83
83
|
out.push('');
|
|
84
84
|
return out;
|
|
85
85
|
default: {
|
|
86
|
-
// Assistant:
|
|
86
|
+
// Assistant: bold white foreground so the conversation is clearly visible.
|
|
87
87
|
const marker = m.streaming ? `${theme_1.THEME.info}◐ ${theme_1.THEME.reset}` : '';
|
|
88
88
|
const lines = (0, terminal_1.wrapText)(body, width);
|
|
89
89
|
if (lines.length === 0)
|
|
90
90
|
lines.push('(no content)');
|
|
91
|
+
const style = `${theme_1.THEME.bold}${theme_1.THEME.textBright}`;
|
|
91
92
|
for (let i = 0; i < lines.length; i++) {
|
|
92
|
-
out.push(i === 0 ? `${marker}${lines[i]}` : lines[i]);
|
|
93
|
+
out.push(i === 0 ? `${marker}${style}${lines[i]}${theme_1.THEME.reset}` : `${style}${lines[i]}${theme_1.THEME.reset}`);
|
|
93
94
|
}
|
|
94
95
|
out.push('');
|
|
95
96
|
return out;
|
package/dist/tui/input.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InputBox = void 0;
|
|
3
|
+
exports.InputBox = exports.DEFAULT_PLACEHOLDER = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* Input box — single-line text input with
|
|
5
|
+
* Input box — single-line text input with a Freebuff-style block cursor (▍)
|
|
6
|
+
* and placeholder, rendered inside a rounded border box.
|
|
6
7
|
*/
|
|
7
|
-
const terminal_1 = require("../utils/terminal");
|
|
8
8
|
const theme_1 = require("./theme");
|
|
9
|
+
/** Freebuff-style placeholder shown while the input is empty. */
|
|
10
|
+
exports.DEFAULT_PLACEHOLDER = 'Enter a coding task or / for commands';
|
|
9
11
|
class InputBox {
|
|
10
12
|
text = '';
|
|
11
13
|
cursor = 0;
|
|
12
14
|
history = [];
|
|
13
15
|
historyIndex = -1;
|
|
14
|
-
hint = '';
|
|
15
|
-
setHint(hint) {
|
|
16
|
-
this.hint = hint;
|
|
17
|
-
}
|
|
18
16
|
get value() {
|
|
19
17
|
return this.text;
|
|
20
18
|
}
|
|
@@ -96,30 +94,34 @@ class InputBox {
|
|
|
96
94
|
}
|
|
97
95
|
this.cursor = this.text.length;
|
|
98
96
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
cursorCol =
|
|
97
|
+
/**
|
|
98
|
+
* Render the input content line (between the box borders) with the ▍ block
|
|
99
|
+
* cursor. `width` is the full terminal row width; the returned `line` starts
|
|
100
|
+
* with 2 leading spaces (Freebuff style) and `cursorCol` is the 0-based
|
|
101
|
+
* column where the block cursor sits.
|
|
102
|
+
*/
|
|
103
|
+
render(width, placeholder = exports.DEFAULT_PLACEHOLDER) {
|
|
104
|
+
const inner = Math.max(4, width - 2); // between the │ borders
|
|
105
|
+
const lead = 2; // spaces after the left border
|
|
106
|
+
const textW = Math.max(1, inner - lead - 1); // room for the text + right padding
|
|
107
|
+
let display;
|
|
108
|
+
let cursorCol;
|
|
109
|
+
if (!this.text) {
|
|
110
|
+
display = `${theme_1.THEME.accent}▍${theme_1.THEME.reset} ${theme_1.THEME.faint}${placeholder}${theme_1.THEME.reset}`;
|
|
111
|
+
cursorCol = lead; // the ▍ sits at column `lead`
|
|
114
112
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
else {
|
|
114
|
+
// Window the text around the cursor when it overflows the box.
|
|
115
|
+
let start = 0;
|
|
116
|
+
if (this.text.length > textW) {
|
|
117
|
+
start = Math.max(0, Math.min(this.cursor - Math.floor(textW / 2), this.text.length - textW));
|
|
118
|
+
}
|
|
119
|
+
const visible = this.text.slice(start, start + textW);
|
|
120
|
+
const cur = Math.min(Math.max(0, this.cursor - start), visible.length);
|
|
121
|
+
display = `${visible.slice(0, cur)}${theme_1.THEME.accent}▍${theme_1.THEME.reset}${visible.slice(cur)}`;
|
|
122
|
+
cursorCol = lead + cur;
|
|
118
123
|
}
|
|
119
|
-
return {
|
|
120
|
-
line: `${prefix}${display}${suffix}`,
|
|
121
|
-
cursorCol,
|
|
122
|
-
};
|
|
124
|
+
return { line: `${' '.repeat(lead)}${display}`, cursorCol };
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
exports.InputBox = InputBox;
|
package/dist/tui/statusbar.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderHeader = renderHeader;
|
|
4
4
|
exports.renderProjectBar = renderProjectBar;
|
|
5
|
+
exports.renderActivity = renderActivity;
|
|
5
6
|
exports.renderStatusBar = renderStatusBar;
|
|
6
7
|
/**
|
|
7
8
|
* Status bar — top header + project line + bottom status bar.
|
|
@@ -44,6 +45,28 @@ function renderProjectBar(width, info) {
|
|
|
44
45
|
const mid = Math.max(1, width - (0, terminal_1.stripAnsi)(fit.left).length - (0, terminal_1.stripAnsi)(fit.right).length - 1);
|
|
45
46
|
return theme_1.THEME.bgBar + (0, terminal_1.pad)(`${fit.left}${' '.repeat(mid)}${fit.right}`, width) + theme_1.THEME.reset;
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Render the dedicated agent-activity strip (Freebuff-style): a compact
|
|
50
|
+
* block above the input showing the current/last tool action instead of
|
|
51
|
+
* flooding the main chat transcript with every tool call.
|
|
52
|
+
*/
|
|
53
|
+
function renderActivity(act, width, maxRows) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const head = `${theme_1.THEME.warn}⚙${theme_1.THEME.reset} ${theme_1.THEME.bold}${act.tool}${theme_1.THEME.reset} ${theme_1.THEME.faint}${act.detail}${theme_1.THEME.reset}`;
|
|
56
|
+
for (const line of (0, terminal_1.wrapText)(head, Math.max(10, width - 2))) {
|
|
57
|
+
out.push(line);
|
|
58
|
+
if (out.length >= maxRows)
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
if (act.result) {
|
|
62
|
+
const lines = act.result.split('\n').filter((l) => l.trim());
|
|
63
|
+
const budget = Math.max(0, maxRows - out.length);
|
|
64
|
+
for (const rl of lines.slice(0, budget)) {
|
|
65
|
+
out.push(`${theme_1.THEME.faint}${rl.slice(0, Math.max(20, width - 4))}${theme_1.THEME.reset}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
47
70
|
/** Render the bottom status bar: live status + spinner + elapsed timer. */
|
|
48
71
|
function renderStatusBar(width, info) {
|
|
49
72
|
const statusColorCode = (0, theme_1.statusColor)(info.status);
|
package/package.json
CHANGED