actoviq-agent-sdk 0.4.0 → 0.4.2
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-zh.md +15 -2
- package/README.md +20 -7
- package/dist/src/cli/actoviq-react.js +178 -3
- package/dist/src/cli/actoviq-react.js.map +1 -1
- package/dist/src/cli/actoviq-tui.js +2 -2
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/router/modelRouter.d.ts +46 -0
- package/dist/src/router/modelRouter.d.ts.map +1 -0
- package/dist/src/router/modelRouter.js +227 -0
- package/dist/src/router/modelRouter.js.map +1 -0
- package/dist/src/runtime/actoviqSkills.d.ts +6 -0
- package/dist/src/runtime/actoviqSkills.d.ts.map +1 -1
- package/dist/src/runtime/actoviqSkills.js +69 -1
- package/dist/src/runtime/actoviqSkills.js.map +1 -1
- package/dist/src/runtime/agentClient.d.ts +16 -0
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +89 -9
- package/dist/src/runtime/agentClient.js.map +1 -1
- package/dist/src/runtime/conversationEngine.d.ts +2 -0
- package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
- package/dist/src/runtime/conversationEngine.js +6 -5
- package/dist/src/runtime/conversationEngine.js.map +1 -1
- package/dist/src/storage/sessionStore.d.ts.map +1 -1
- package/dist/src/storage/sessionStore.js +25 -6
- package/dist/src/storage/sessionStore.js.map +1 -1
- package/dist/src/team/agentPool.d.ts +26 -0
- package/dist/src/team/agentPool.d.ts.map +1 -0
- package/dist/src/team/agentPool.js +97 -0
- package/dist/src/team/agentPool.js.map +1 -0
- package/dist/src/team/modelTeam.d.ts +56 -0
- package/dist/src/team/modelTeam.d.ts.map +1 -0
- package/dist/src/team/modelTeam.js +542 -0
- package/dist/src/team/modelTeam.js.map +1 -0
- package/dist/src/team/pricing.d.ts +15 -0
- package/dist/src/team/pricing.d.ts.map +1 -0
- package/dist/src/team/pricing.js +80 -0
- package/dist/src/team/pricing.js.map +1 -0
- package/dist/src/team/teamDefinitions.d.ts +28 -0
- package/dist/src/team/teamDefinitions.d.ts.map +1 -0
- package/dist/src/team/teamDefinitions.js +151 -0
- package/dist/src/team/teamDefinitions.js.map +1 -0
- package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqCoreTools.js +6 -1
- package/dist/src/tools/actoviqCoreTools.js.map +1 -1
- package/dist/src/tools/enterWorktree.d.ts +11 -0
- package/dist/src/tools/enterWorktree.d.ts.map +1 -0
- package/dist/src/tools/enterWorktree.js +79 -0
- package/dist/src/tools/enterWorktree.js.map +1 -0
- package/dist/src/tools/exitWorktree.d.ts +4 -0
- package/dist/src/tools/exitWorktree.d.ts.map +1 -0
- package/dist/src/tools/exitWorktree.js +48 -0
- package/dist/src/tools/exitWorktree.js.map +1 -0
- package/dist/src/tools/tavilySearch.d.ts +12 -0
- package/dist/src/tools/tavilySearch.d.ts.map +1 -0
- package/dist/src/tools/tavilySearch.js +139 -0
- package/dist/src/tools/tavilySearch.js.map +1 -0
- package/dist/src/tui/actoviqTui.d.ts +10 -0
- package/dist/src/tui/actoviqTui.d.ts.map +1 -1
- package/dist/src/tui/actoviqTui.js +606 -35
- package/dist/src/tui/actoviqTui.js.map +1 -1
- package/dist/src/tui/editor.d.ts +2 -0
- package/dist/src/tui/editor.d.ts.map +1 -1
- package/dist/src/tui/editor.js +5 -0
- package/dist/src/tui/editor.js.map +1 -1
- package/dist/src/types.d.ts +323 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/workflow/workflowPersistence.d.ts +34 -0
- package/dist/src/workflow/workflowPersistence.d.ts.map +1 -0
- package/dist/src/workflow/workflowPersistence.js +144 -0
- package/dist/src/workflow/workflowPersistence.js.map +1 -0
- package/dist/src/workflow/workflowScriptRuntime.d.ts +22 -0
- package/dist/src/workflow/workflowScriptRuntime.d.ts.map +1 -0
- package/dist/src/workflow/workflowScriptRuntime.js +655 -0
- package/dist/src/workflow/workflowScriptRuntime.js.map +1 -0
- package/dist/src/worktree/worktreeHooks.d.ts +38 -0
- package/dist/src/worktree/worktreeHooks.d.ts.map +1 -0
- package/dist/src/worktree/worktreeHooks.js +82 -0
- package/dist/src/worktree/worktreeHooks.js.map +1 -0
- package/dist/src/worktree/worktreeInclude.d.ts +11 -0
- package/dist/src/worktree/worktreeInclude.d.ts.map +1 -0
- package/dist/src/worktree/worktreeInclude.js +65 -0
- package/dist/src/worktree/worktreeInclude.js.map +1 -0
- package/dist/src/worktree/worktreeService.d.ts +43 -0
- package/dist/src/worktree/worktreeService.d.ts.map +1 -0
- package/dist/src/worktree/worktreeService.js +356 -0
- package/dist/src/worktree/worktreeService.js.map +1 -0
- package/package.json +2 -24
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* rendering (no React/Ink).
|
|
7
7
|
*/
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import * as readline from 'node:readline';
|
|
11
|
-
import { createActoviqCoreTools, createAgentSdk, loadDefaultActoviqSettings, loadJsonConfigFile, } from '../index.js';
|
|
12
|
+
import { createActoviqCoreTools, createAgentSdk, loadDefaultActoviqSettings, loadJsonConfigFile, listWorkflows, loadWorkflow, listTeamDefinitions, loadTeamDefinition, createModelTeam, createTeamTool, listRouterProfiles, loadRouterProfile, resolveRoutedRun, WorktreeService, } from '../index.js';
|
|
12
13
|
import { persistActoviqSettingsStore, resolveActoviqSettingsStore, } from '../config/actoviqSettingsStore.js';
|
|
13
14
|
import { isRecord } from '../runtime/helpers.js';
|
|
14
|
-
import {
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { A, stringWidth, truncateToWidth, wrapToWidth } from './ansi.js';
|
|
15
17
|
import { InputEditor } from './editor.js';
|
|
16
18
|
import { discoverActoviqPlugins } from './pluginCatalog.js';
|
|
17
19
|
import { TuiScreen } from './screen.js';
|
|
@@ -49,6 +51,9 @@ export const TUI_SLASH_COMMANDS = {
|
|
|
49
51
|
mcp: 'Inspect MCP servers and tools',
|
|
50
52
|
plugins: 'Browse discovered Clean plugins',
|
|
51
53
|
dream: 'Inspect or run memory consolidation',
|
|
54
|
+
workflows: 'Browse saved dynamic workflows',
|
|
55
|
+
worktree: 'Enter, exit, or list git worktrees',
|
|
56
|
+
team: 'List or run Model Team definitions',
|
|
52
57
|
exit: 'Quit',
|
|
53
58
|
};
|
|
54
59
|
export function filterSlashCommands(input) {
|
|
@@ -60,6 +65,56 @@ export function filterSlashCommands(input) {
|
|
|
60
65
|
const partial = head.toLowerCase();
|
|
61
66
|
return Object.keys(TUI_SLASH_COMMANDS).filter((name) => name.startsWith(partial));
|
|
62
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Detect an active "@file" mention at the cursor for path completion. Returns
|
|
70
|
+
* the partial token typed after the '@' plus the '@' offset, or null when the
|
|
71
|
+
* cursor is not inside a mention. The '@' only opens a mention at the start of
|
|
72
|
+
* input or after whitespace, and the token ends at the first whitespace.
|
|
73
|
+
*/
|
|
74
|
+
export function activeAtToken(text, cursor) {
|
|
75
|
+
for (let i = cursor - 1; i >= 0; i -= 1) {
|
|
76
|
+
const ch = text[i];
|
|
77
|
+
if (/\s/.test(ch))
|
|
78
|
+
return null;
|
|
79
|
+
if (ch === '@') {
|
|
80
|
+
if (i === 0 || /\s/.test(text[i - 1])) {
|
|
81
|
+
return { token: text.slice(i + 1, cursor), start: i };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Render free-form text for the scrollback: width-aware word wrapping with
|
|
90
|
+
* markdown-lite heading highlighting. Used for workflow/team results and the
|
|
91
|
+
* expert-panel member reports so long output reads cleanly instead of dumping
|
|
92
|
+
* raw lines. Optionally caps very long output with a "… (N more lines)" note.
|
|
93
|
+
*/
|
|
94
|
+
function renderRichText(text, width, opts = {}) {
|
|
95
|
+
const cols = Math.max(20, width - 2);
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const raw of text.replace(/\r/g, '').split('\n')) {
|
|
98
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(raw);
|
|
99
|
+
if (heading) {
|
|
100
|
+
out.push(`${A.bold}${A.cyan}${heading[2]}${A.reset}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (raw.trim() === '') {
|
|
104
|
+
out.push('');
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const line of wrapToWidth(raw, cols))
|
|
108
|
+
out.push(line);
|
|
109
|
+
}
|
|
110
|
+
const maxLines = opts.maxLines ?? 0;
|
|
111
|
+
if (maxLines > 0 && out.length > maxLines) {
|
|
112
|
+
const kept = out.slice(0, maxLines);
|
|
113
|
+
kept.push(`${A.dim}… (${out.length - maxLines} more lines)${A.reset}`);
|
|
114
|
+
return kept;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
63
118
|
function buildSystemPrompt(workDir) {
|
|
64
119
|
let isGit = false;
|
|
65
120
|
try {
|
|
@@ -133,11 +188,20 @@ export async function runActoviqTui(options = {}) {
|
|
|
133
188
|
let running = false;
|
|
134
189
|
let commandBusy = false;
|
|
135
190
|
let shuttingDown = false;
|
|
191
|
+
// Active team tool the main agent may call (toggled via /team). null = no team.
|
|
192
|
+
let activeTeamTool = null;
|
|
193
|
+
let activeTeamName = null;
|
|
194
|
+
// /model router: when set, each user turn is classified and routed to a model.
|
|
195
|
+
let activeRouter = null;
|
|
196
|
+
let routedModelLabel = null;
|
|
136
197
|
let abortCtrl = null;
|
|
137
198
|
let dialog = null;
|
|
138
199
|
let selectionDialog = null;
|
|
139
200
|
let textInputDialog = null;
|
|
140
201
|
let menuSelected = 0;
|
|
202
|
+
// @-mention file completion: highlighted candidate + lazily-cached file list.
|
|
203
|
+
let atSelected = 0;
|
|
204
|
+
let workspaceFiles = null;
|
|
141
205
|
let spinnerFrame = 0;
|
|
142
206
|
let spinnerTimer = null;
|
|
143
207
|
let dynamicRenderTimer = null;
|
|
@@ -302,8 +366,9 @@ export async function runActoviqTui(options = {}) {
|
|
|
302
366
|
const lines = [];
|
|
303
367
|
lines.push(promptDivider());
|
|
304
368
|
if (editor.isEmpty()) {
|
|
305
|
-
|
|
306
|
-
|
|
369
|
+
// Always-visible block caret on the empty input so the box reads as active.
|
|
370
|
+
const placeholder = truncateToWidth('Try "write a test for <filepath>"', editorWidth - 4);
|
|
371
|
+
lines.push(`${A.magenta}${PROMPT_GLYPH}${A.reset} ${A.inverse} ${A.reset} ${A.dim}${placeholder}${A.reset}`);
|
|
307
372
|
}
|
|
308
373
|
else {
|
|
309
374
|
const visual = editor.visualLines(editorWidth - 1);
|
|
@@ -316,21 +381,152 @@ export async function runActoviqTui(options = {}) {
|
|
|
316
381
|
lines.push(promptDivider());
|
|
317
382
|
return lines;
|
|
318
383
|
}
|
|
384
|
+
// ── @-mention file completion ──────────────────────────────────────
|
|
385
|
+
// Prefer git's view (tracked + untracked, honoring .gitignore) so the list
|
|
386
|
+
// matches what the agent actually sees; fall back to a bounded fs walk for
|
|
387
|
+
// non-git workspaces. Cached for the session and invalidated after each run.
|
|
388
|
+
function loadWorkspaceFiles() {
|
|
389
|
+
if (workspaceFiles)
|
|
390
|
+
return workspaceFiles;
|
|
391
|
+
try {
|
|
392
|
+
const out = execSync('git ls-files --cached --others --exclude-standard', {
|
|
393
|
+
cwd: workDir,
|
|
394
|
+
encoding: 'utf8',
|
|
395
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
396
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
397
|
+
});
|
|
398
|
+
workspaceFiles = out.split('\n').map((line) => line.trim()).filter(Boolean).slice(0, 8000);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
workspaceFiles = walkWorkspaceFiles(workDir, 8000);
|
|
402
|
+
}
|
|
403
|
+
return workspaceFiles;
|
|
404
|
+
}
|
|
405
|
+
function walkWorkspaceFiles(root, limit) {
|
|
406
|
+
const skip = new Set(['node_modules', '.git', 'dist', '.codegraph', '.next', 'coverage']);
|
|
407
|
+
const out = [];
|
|
408
|
+
const stack = [root];
|
|
409
|
+
while (stack.length > 0 && out.length < limit) {
|
|
410
|
+
const dir = stack.pop();
|
|
411
|
+
let entries;
|
|
412
|
+
try {
|
|
413
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
for (const entry of entries) {
|
|
419
|
+
if (entry.isDirectory()) {
|
|
420
|
+
if (!skip.has(entry.name) && !entry.name.startsWith('.'))
|
|
421
|
+
stack.push(path.join(dir, entry.name));
|
|
422
|
+
}
|
|
423
|
+
else if (entry.isFile()) {
|
|
424
|
+
out.push(path.relative(root, path.join(dir, entry.name)).split(path.sep).join('/'));
|
|
425
|
+
if (out.length >= limit)
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
function atCompletions(token) {
|
|
433
|
+
const files = loadWorkspaceFiles();
|
|
434
|
+
const query = token.toLowerCase();
|
|
435
|
+
if (!query)
|
|
436
|
+
return files.slice(0, 200);
|
|
437
|
+
const matches = files.filter((file) => file.toLowerCase().includes(query));
|
|
438
|
+
matches.sort((a, b) => {
|
|
439
|
+
const aBase = a.slice(a.lastIndexOf('/') + 1).toLowerCase();
|
|
440
|
+
const bBase = b.slice(b.lastIndexOf('/') + 1).toLowerCase();
|
|
441
|
+
const aScore = aBase.startsWith(query) ? 0 : aBase.includes(query) ? 1 : 2;
|
|
442
|
+
const bScore = bBase.startsWith(query) ? 0 : bBase.includes(query) ? 1 : 2;
|
|
443
|
+
if (aScore !== bScore)
|
|
444
|
+
return aScore - bScore;
|
|
445
|
+
return a.length - b.length;
|
|
446
|
+
});
|
|
447
|
+
return matches.slice(0, 200);
|
|
448
|
+
}
|
|
449
|
+
function buildAtMenu() {
|
|
450
|
+
const active = activeAtToken(editor.text, editor.cursor);
|
|
451
|
+
if (!active)
|
|
452
|
+
return [];
|
|
453
|
+
const matches = atCompletions(active.token);
|
|
454
|
+
if (matches.length === 0) {
|
|
455
|
+
return [`${A.dim} @${active.token} — no matching files${A.reset}`];
|
|
456
|
+
}
|
|
457
|
+
if (atSelected >= matches.length)
|
|
458
|
+
atSelected = matches.length - 1;
|
|
459
|
+
if (atSelected < 0)
|
|
460
|
+
atSelected = 0;
|
|
461
|
+
const windowStart = Math.max(0, Math.min(atSelected - MENU_MAX_ROWS + 1, matches.length - MENU_MAX_ROWS));
|
|
462
|
+
const visible = matches.slice(windowStart, windowStart + MENU_MAX_ROWS);
|
|
463
|
+
const lines = visible.map((file, i) => {
|
|
464
|
+
const index = windowStart + i;
|
|
465
|
+
const display = truncateToWidth(file, Math.max(screen.width - 6, 12));
|
|
466
|
+
return index === atSelected ? `${A.inverse} @${display} ${A.reset}` : ` ${A.cyan}@${display}${A.reset}`;
|
|
467
|
+
});
|
|
468
|
+
const hiddenAbove = windowStart;
|
|
469
|
+
const hiddenBelow = matches.length - (windowStart + visible.length);
|
|
470
|
+
if (hiddenAbove > 0 || hiddenBelow > 0) {
|
|
471
|
+
const parts = [];
|
|
472
|
+
if (hiddenAbove > 0)
|
|
473
|
+
parts.push(`↑${hiddenAbove}`);
|
|
474
|
+
if (hiddenBelow > 0)
|
|
475
|
+
parts.push(`↓${hiddenBelow}`);
|
|
476
|
+
lines.push(`${A.dim} ${parts.join(' ')} more · ${atSelected + 1}/${matches.length} (↑/↓ · Tab/Enter to insert)${A.reset}`);
|
|
477
|
+
}
|
|
478
|
+
return lines;
|
|
479
|
+
}
|
|
480
|
+
/** Replace the active @-token with the highlighted file path. */
|
|
481
|
+
function applyAtCompletion() {
|
|
482
|
+
const active = activeAtToken(editor.text, editor.cursor);
|
|
483
|
+
if (!active)
|
|
484
|
+
return false;
|
|
485
|
+
const matches = atCompletions(active.token);
|
|
486
|
+
if (matches.length === 0)
|
|
487
|
+
return false;
|
|
488
|
+
const file = matches[Math.min(atSelected, matches.length - 1)];
|
|
489
|
+
const before = editor.text.slice(0, active.start);
|
|
490
|
+
const after = editor.text.slice(editor.cursor);
|
|
491
|
+
const mention = `@${file} `;
|
|
492
|
+
editor.setTextWithCursor(`${before}${mention}${after}`, before.length + mention.length);
|
|
493
|
+
atSelected = 0;
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
319
496
|
function buildMenu() {
|
|
320
497
|
const matches = filterSlashCommands(editor.text);
|
|
321
498
|
if (matches.length === 0)
|
|
322
499
|
return [];
|
|
323
500
|
if (menuSelected >= matches.length)
|
|
324
501
|
menuSelected = matches.length - 1;
|
|
502
|
+
if (menuSelected < 0)
|
|
503
|
+
menuSelected = 0;
|
|
325
504
|
const commandWidth = Math.min(28, Math.max(14, Math.floor(screen.width * 0.28)));
|
|
326
505
|
const descriptionWidth = Math.max(screen.width - commandWidth - 4, 12);
|
|
327
|
-
|
|
506
|
+
// Scroll a window of MENU_MAX_ROWS so the highlighted item stays visible and
|
|
507
|
+
// commands past the cap (e.g. /workflows, /worktree, /team) are reachable
|
|
508
|
+
// with the arrow keys instead of being clipped off the bottom.
|
|
509
|
+
const windowStart = Math.max(0, Math.min(menuSelected - MENU_MAX_ROWS + 1, matches.length - MENU_MAX_ROWS));
|
|
510
|
+
const visible = matches.slice(windowStart, windowStart + MENU_MAX_ROWS);
|
|
511
|
+
const lines = visible.map((name, i) => {
|
|
512
|
+
const index = windowStart + i;
|
|
328
513
|
const selected = index === menuSelected;
|
|
329
514
|
const command = `/${name}`.padEnd(commandWidth);
|
|
330
515
|
const label = selected ? `${A.inverse}${command}${A.reset}` : command;
|
|
331
516
|
const description = truncateToWidth(TUI_SLASH_COMMANDS[name] ?? '', descriptionWidth);
|
|
332
517
|
return `${label} ${A.dim}${description}${A.reset}`;
|
|
333
518
|
});
|
|
519
|
+
const hiddenAbove = windowStart;
|
|
520
|
+
const hiddenBelow = matches.length - (windowStart + visible.length);
|
|
521
|
+
if (hiddenAbove > 0 || hiddenBelow > 0) {
|
|
522
|
+
const parts = [];
|
|
523
|
+
if (hiddenAbove > 0)
|
|
524
|
+
parts.push(`↑${hiddenAbove}`);
|
|
525
|
+
if (hiddenBelow > 0)
|
|
526
|
+
parts.push(`↓${hiddenBelow}`);
|
|
527
|
+
lines.push(`${A.dim} ${parts.join(' ')} more · ${menuSelected + 1}/${matches.length} (↑/↓ to scroll)${A.reset}`);
|
|
528
|
+
}
|
|
529
|
+
return lines;
|
|
334
530
|
}
|
|
335
531
|
function buildDialog() {
|
|
336
532
|
if (!dialog)
|
|
@@ -405,25 +601,48 @@ export async function runActoviqTui(options = {}) {
|
|
|
405
601
|
lines.push(`${A.dim} enter confirm · esc cancel${textInputDialog.secret ? ' · value hidden' : ''}${A.reset}`);
|
|
406
602
|
return lines;
|
|
407
603
|
}
|
|
604
|
+
/** Friendly permission label matching the /permissions presets. */
|
|
605
|
+
function permissionLabel() {
|
|
606
|
+
const m = currentPermissionMode();
|
|
607
|
+
if (m === 'bypassPermissions')
|
|
608
|
+
return 'full-access';
|
|
609
|
+
if (m === 'acceptEdits')
|
|
610
|
+
return 'workspace';
|
|
611
|
+
if (m === 'default' && session.permissionContext.permissions.some((p) => p.behavior === 'deny'))
|
|
612
|
+
return 'read-only';
|
|
613
|
+
return m;
|
|
614
|
+
}
|
|
615
|
+
/** Always-visible mode + live context-usage line (usage shown as % of the window). */
|
|
616
|
+
function buildModeLine() {
|
|
617
|
+
const used = lastTokenEstimate ?? 0;
|
|
618
|
+
const window = sdk.config.compact?.contextWindowTokens ?? 200_000;
|
|
619
|
+
const pct = window > 0 ? Math.min(100, Math.round((used / window) * 100)) : 0;
|
|
620
|
+
const usedK = used >= 1000 ? `${(used / 1000).toFixed(used >= 100_000 ? 0 : 1)}k` : `${used}`;
|
|
621
|
+
const ctxColor = pct >= 90 ? A.red : pct >= 70 ? A.yellow : A.dim;
|
|
622
|
+
const modelLabel = activeRouter
|
|
623
|
+
? `router:${activeRouter.name}${routedModelLabel ? ` → ${routedModelLabel}` : ''}`
|
|
624
|
+
: session.model;
|
|
625
|
+
const left = `${modelLabel} · ${permissionLabel()} · effort:${currentEffort() ?? 'auto'} · team:${activeTeamName ?? 'none'} · `;
|
|
626
|
+
return `${A.dim} ${left}${A.reset}${ctxColor}ctx ${pct}% (${usedK})${A.reset}`;
|
|
627
|
+
}
|
|
408
628
|
function buildStatusLine() {
|
|
629
|
+
const modeLine = buildModeLine();
|
|
409
630
|
if (!running)
|
|
410
|
-
return [];
|
|
631
|
+
return [modeLine];
|
|
411
632
|
const elapsed = Math.max(Math.round((Date.now() - runStartedAt) / 1000), 0);
|
|
412
633
|
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
413
|
-
const context = lastTokenEstimate && lastTokenEstimate >= 1000
|
|
414
|
-
? ` · ~${Math.round(lastTokenEstimate / 1000)}k ctx`
|
|
415
|
-
: '';
|
|
416
634
|
const note = statusNote ? ` · ${statusNote}` : '';
|
|
417
635
|
const queued = queuedInputs.length > 0 ? ` · ${queuedInputs.length} queued` : '';
|
|
418
636
|
return [
|
|
419
|
-
`${A.cyan}${frame}${A.reset} ${A.bold}Working…${A.reset}${A.dim} (${elapsed}s · ${runToolCount} tool${runToolCount === 1 ? '' : 's'}${
|
|
637
|
+
`${A.cyan}${frame}${A.reset} ${A.bold}Working…${A.reset}${A.dim} (${elapsed}s · ${runToolCount} tool${runToolCount === 1 ? '' : 's'}${note}${queued} · esc to interrupt)${A.reset}`,
|
|
638
|
+
modeLine,
|
|
420
639
|
];
|
|
421
640
|
}
|
|
422
641
|
function buildHintLine() {
|
|
423
642
|
if (running) {
|
|
424
643
|
return [`${A.dim} enter to queue a steering message · esc interrupt · ctrl+c twice to exit${A.reset}`];
|
|
425
644
|
}
|
|
426
|
-
return [`${A.dim} ?
|
|
645
|
+
return [`${A.dim} ? shortcuts · / commands · @ files · \\↵ newline · ↑↓ history · ctrl+c clear/exit${A.reset}`];
|
|
427
646
|
}
|
|
428
647
|
function renderDynamic() {
|
|
429
648
|
const lines = [];
|
|
@@ -443,8 +662,14 @@ export async function runActoviqTui(options = {}) {
|
|
|
443
662
|
}
|
|
444
663
|
else {
|
|
445
664
|
lines.push(...buildPromptBar());
|
|
446
|
-
const
|
|
447
|
-
|
|
665
|
+
const atMenu = buildAtMenu();
|
|
666
|
+
if (atMenu.length > 0) {
|
|
667
|
+
lines.push(...atMenu);
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
const menu = buildMenu();
|
|
671
|
+
lines.push(...(menu.length > 0 ? menu : buildHintLine()));
|
|
672
|
+
}
|
|
448
673
|
}
|
|
449
674
|
screen.setDynamic(lines);
|
|
450
675
|
}
|
|
@@ -466,6 +691,55 @@ export async function runActoviqTui(options = {}) {
|
|
|
466
691
|
screen.appendStatic(lines);
|
|
467
692
|
}
|
|
468
693
|
// ── Agent run ──────────────────────────────────────────────────────
|
|
694
|
+
// /model router: pick a saved router profile (or turn routing off). When
|
|
695
|
+
// active, each user turn is classified and routed to a model in startRun().
|
|
696
|
+
async function chooseRouter(arg) {
|
|
697
|
+
const turnOff = () => {
|
|
698
|
+
activeRouter = null;
|
|
699
|
+
routedModelLabel = null;
|
|
700
|
+
appendStatic([...formatInfoLine('router off — using the fixed model'), '']);
|
|
701
|
+
};
|
|
702
|
+
if (arg === 'off' || arg === 'none') {
|
|
703
|
+
turnOff();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (arg) {
|
|
707
|
+
const found = loadRouterProfile(arg, sdk.config.workDir);
|
|
708
|
+
if (!found) {
|
|
709
|
+
appendStatic([...formatErrorLine(`router profile not found: ${arg}`), '']);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
activeRouter = found.profile;
|
|
713
|
+
routedModelLabel = null;
|
|
714
|
+
appendStatic([...formatInfoLine(`router active: ${found.profile.name} — each turn is classified by ${found.profile.routerModel.model} and routed`), '']);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const profiles = listRouterProfiles(sdk.config.workDir);
|
|
718
|
+
if (profiles.length === 0) {
|
|
719
|
+
appendStatic([
|
|
720
|
+
...formatInfoLine('no router profiles found. Create one at ~/.actoviq/routers/<name>.json (routerModel + routes:[{ when, model, provider?, baseURL?, apiKey? }] + fallback).'),
|
|
721
|
+
'',
|
|
722
|
+
]);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const items = [
|
|
726
|
+
{ id: '__off__', label: activeRouter ? `Turn router off (active: ${activeRouter.name})` : 'Router off (current)', description: 'use the fixed model for every turn' },
|
|
727
|
+
...profiles.map((p) => ({ id: `profile:${p.name}`, label: p.name, description: `${p.profile.routes.length} routes · classifier ${p.profile.routerModel.model} · ${p.source}` })),
|
|
728
|
+
];
|
|
729
|
+
const choice = await selectItem({ title: 'Model router', subtitle: 'classify each turn and route to a model (may be cross-provider)', items });
|
|
730
|
+
if (!choice)
|
|
731
|
+
return;
|
|
732
|
+
if (choice === '__off__') {
|
|
733
|
+
turnOff();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const found = loadRouterProfile(choice.slice('profile:'.length), sdk.config.workDir);
|
|
737
|
+
if (found) {
|
|
738
|
+
activeRouter = found.profile;
|
|
739
|
+
routedModelLabel = null;
|
|
740
|
+
appendStatic([...formatInfoLine(`router active: ${found.profile.name} — turns routed by ${found.profile.routerModel.model}`), '']);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
469
743
|
async function startRun(text) {
|
|
470
744
|
running = true;
|
|
471
745
|
runStartedAt = Date.now();
|
|
@@ -480,6 +754,20 @@ export async function runActoviqTui(options = {}) {
|
|
|
480
754
|
}, 120);
|
|
481
755
|
appendStatic(formatUserPrompt(text));
|
|
482
756
|
renderDynamic();
|
|
757
|
+
// /model router: classify this turn and route it to the chosen model
|
|
758
|
+
// (possibly on a different provider). The turn then runs normally.
|
|
759
|
+
let routed;
|
|
760
|
+
if (activeRouter) {
|
|
761
|
+
try {
|
|
762
|
+
const decision = await resolveRoutedRun(activeRouter, text, abortCtrl.signal);
|
|
763
|
+
routed = { model: decision.model, modelApi: decision.modelApi };
|
|
764
|
+
routedModelLabel = `${decision.label} (${decision.model})`;
|
|
765
|
+
appendStatic(formatInfoLine(`router → ${routedModelLabel}`));
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
appendStatic(formatInfoLine(`router classification failed (${error.message}); using ${session.model}`));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
483
771
|
try {
|
|
484
772
|
const stream = session.stream(text, {
|
|
485
773
|
systemPrompt,
|
|
@@ -487,6 +775,8 @@ export async function runActoviqTui(options = {}) {
|
|
|
487
775
|
permissionMode: currentPermissionMode(),
|
|
488
776
|
effort: currentEffort(),
|
|
489
777
|
approver,
|
|
778
|
+
...(routed ? { model: routed.model, modelApi: routed.modelApi } : {}),
|
|
779
|
+
...(activeTeamTool ? { tools: [...tools, activeTeamTool] } : {}),
|
|
490
780
|
...(canUseTool ? { canUseTool } : {}),
|
|
491
781
|
drainQueuedInputs: () => {
|
|
492
782
|
const drained = queuedInputs.splice(0);
|
|
@@ -523,6 +813,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
523
813
|
finally {
|
|
524
814
|
running = false;
|
|
525
815
|
abortCtrl = null;
|
|
816
|
+
workspaceFiles = null; // the agent may have created/removed files — refresh @-completion
|
|
526
817
|
if (spinnerTimer) {
|
|
527
818
|
clearInterval(spinnerTimer);
|
|
528
819
|
spinnerTimer = null;
|
|
@@ -619,7 +910,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
619
910
|
clear: '/clear',
|
|
620
911
|
compact: '/compact [summary instructions]',
|
|
621
912
|
memory: '/memory',
|
|
622
|
-
model: '/model [model|min|medium|max|default|config]',
|
|
913
|
+
model: '/model [model|min|medium|max|default|config|router]',
|
|
623
914
|
effort: '/effort [low|medium|high|max|auto]',
|
|
624
915
|
permissions: '/permissions [default|acceptEdits|plan|bypassPermissions|auto]',
|
|
625
916
|
sessions: '/sessions',
|
|
@@ -630,6 +921,9 @@ export async function runActoviqTui(options = {}) {
|
|
|
630
921
|
mcp: '/mcp',
|
|
631
922
|
plugins: '/plugins',
|
|
632
923
|
dream: '/dream [run|status]',
|
|
924
|
+
workflows: '/workflows [run <name>|list]',
|
|
925
|
+
worktree: '/worktree [enter <name>|exit|list]',
|
|
926
|
+
team: '/team [ask <name> <prompt>|list]',
|
|
633
927
|
exit: '/exit',
|
|
634
928
|
};
|
|
635
929
|
return usage[name] ?? `/${name}`;
|
|
@@ -884,15 +1178,17 @@ export async function runActoviqTui(options = {}) {
|
|
|
884
1178
|
title: 'Skills',
|
|
885
1179
|
items: skills.map(skill => ({
|
|
886
1180
|
id: skill.name,
|
|
887
|
-
label: skill.name,
|
|
888
|
-
description: `${skill.source} · ${skill.context}`,
|
|
1181
|
+
label: skill.displayName ? `${skill.displayName} (${skill.name})` : skill.name,
|
|
1182
|
+
description: `${skill.source} · ${skill.context}${skill.version ? ` · v${skill.version}` : ''}`,
|
|
889
1183
|
detail: `${skill.description} ${skill.whenToUse ?? ''}`,
|
|
890
1184
|
})),
|
|
891
1185
|
});
|
|
892
1186
|
const skill = skills.find(item => item.name === selected);
|
|
893
1187
|
if (skill) {
|
|
1188
|
+
const heading = skill.displayName ? `${skill.displayName} (/${skill.name})` : `/${skill.name}`;
|
|
1189
|
+
const ver = skill.version ? ` v${skill.version}` : '';
|
|
894
1190
|
appendStatic([
|
|
895
|
-
`${A.cyan}
|
|
1191
|
+
`${A.cyan}${heading}${A.reset}${A.dim}${ver}${A.reset} ${skill.description}`,
|
|
896
1192
|
`${A.dim}${skill.whenToUse ?? `source: ${skill.source} · context: ${skill.context}`}${A.reset}`,
|
|
897
1193
|
'',
|
|
898
1194
|
]);
|
|
@@ -1059,6 +1355,10 @@ export async function runActoviqTui(options = {}) {
|
|
|
1059
1355
|
await configureModelSettings();
|
|
1060
1356
|
return;
|
|
1061
1357
|
}
|
|
1358
|
+
if (args === 'router' || args.startsWith('router ')) {
|
|
1359
|
+
await chooseRouter(args.slice('router'.length).trim());
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1062
1362
|
await session.setModel(args === 'default' ? sdk.config.model : args);
|
|
1063
1363
|
appendStatic([...formatInfoLine(`model set to: ${session.model}`), '']);
|
|
1064
1364
|
return;
|
|
@@ -1070,25 +1370,45 @@ export async function runActoviqTui(options = {}) {
|
|
|
1070
1370
|
await setEffort(args.toLowerCase());
|
|
1071
1371
|
return;
|
|
1072
1372
|
case 'permissions': {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1373
|
+
// Three presets, selectable or named directly:
|
|
1374
|
+
// read-only → deny mutating tools (read / search / web only)
|
|
1375
|
+
// workspace → acceptEdits (auto-accept edits in the workspace)
|
|
1376
|
+
// full → bypassPermissions (no prompts)
|
|
1377
|
+
const READONLY_DENY = ['Bash', 'Write', 'Edit', 'NotebookEdit', 'PowerShell'];
|
|
1378
|
+
const presets = {
|
|
1379
|
+
'read-only': {
|
|
1380
|
+
mode: 'default',
|
|
1381
|
+
rules: READONLY_DENY.map((t) => ({ toolName: t, behavior: 'deny', source: 'permissions-preset' })),
|
|
1382
|
+
label: 'Read-only',
|
|
1383
|
+
},
|
|
1384
|
+
workspace: { mode: 'acceptEdits', rules: [], label: 'Workspace access' },
|
|
1385
|
+
full: { mode: 'bypassPermissions', rules: [], label: 'Full access' },
|
|
1386
|
+
};
|
|
1387
|
+
let key = args.trim().toLowerCase().replace(/[ _]/g, '-');
|
|
1388
|
+
if (!key) {
|
|
1389
|
+
const choice = await selectItem({
|
|
1390
|
+
title: 'Permission mode',
|
|
1391
|
+
subtitle: `current: ${session.permissionContext.mode ?? permissionMode}`,
|
|
1392
|
+
items: [
|
|
1393
|
+
{ id: 'read-only', label: 'Read-only', description: 'Read, search, and web only — deny Write/Edit/Bash/NotebookEdit/PowerShell' },
|
|
1394
|
+
{ id: 'workspace', label: 'Workspace access', description: 'Auto-accept edits in the workspace (acceptEdits)' },
|
|
1395
|
+
{ id: 'full', label: 'Full access', description: 'No prompts — run any tool (bypassPermissions)' },
|
|
1396
|
+
],
|
|
1397
|
+
});
|
|
1398
|
+
if (!choice)
|
|
1399
|
+
return;
|
|
1400
|
+
key = choice;
|
|
1081
1401
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1402
|
+
const preset = presets[key];
|
|
1403
|
+
if (!preset) {
|
|
1404
|
+
appendStatic([...formatErrorLine(`unknown permission preset: ${key} (read-only | workspace | full)`), '']);
|
|
1084
1405
|
return;
|
|
1085
1406
|
}
|
|
1086
|
-
await session.setPermissionContext({
|
|
1087
|
-
|
|
1088
|
-
permissions:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
appendStatic([...formatInfoLine(`permissions set to: ${requested}`), '']);
|
|
1407
|
+
await session.setPermissionContext({ mode: preset.mode, permissions: preset.rules, approver });
|
|
1408
|
+
appendStatic([
|
|
1409
|
+
...formatInfoLine(`permissions: ${preset.label} — ${preset.mode}${preset.rules.length ? ` · ${preset.rules.length} deny rules` : ''}`),
|
|
1410
|
+
'',
|
|
1411
|
+
]);
|
|
1092
1412
|
return;
|
|
1093
1413
|
}
|
|
1094
1414
|
case 'sessions': {
|
|
@@ -1169,6 +1489,227 @@ export async function runActoviqTui(options = {}) {
|
|
|
1169
1489
|
case 'plugins':
|
|
1170
1490
|
await showPlugins();
|
|
1171
1491
|
return;
|
|
1492
|
+
// ── v0.5.0: Dynamic Workflows ────────────────────────────
|
|
1493
|
+
case 'workflows': {
|
|
1494
|
+
const runSavedWorkflow = async (wfName, wfTask) => {
|
|
1495
|
+
const wf = loadWorkflow(wfName, sdk.config.workDir);
|
|
1496
|
+
if (!wf) {
|
|
1497
|
+
appendStatic([...formatErrorLine(`workflow not found: ${wfName}`), '']);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
appendStatic([
|
|
1501
|
+
...formatInfoLine(`running workflow: ${wfName}`),
|
|
1502
|
+
...formatInfoLine(`phases: ${wf.meta?.phases?.map((p) => p.title).join(', ') ?? 'none'}`),
|
|
1503
|
+
'',
|
|
1504
|
+
]);
|
|
1505
|
+
try {
|
|
1506
|
+
const { WorkflowScriptRuntime } = await import('../workflow/workflowScriptRuntime.js');
|
|
1507
|
+
const runtime = new WorkflowScriptRuntime({
|
|
1508
|
+
sdk: sdk,
|
|
1509
|
+
args: wfTask,
|
|
1510
|
+
onEvent: (e) => {
|
|
1511
|
+
if (e.type === 'workflow.phase.start') {
|
|
1512
|
+
appendStatic([`${A.bold}${A.magenta}▶ ${e.title}${A.reset}`]);
|
|
1513
|
+
}
|
|
1514
|
+
else if (e.type === 'workflow.agent.start') {
|
|
1515
|
+
appendStatic([`${A.dim} ⚡ ${e.label ?? e.agentId}${e.cached ? ' (cached)' : ''}${A.reset}`]);
|
|
1516
|
+
}
|
|
1517
|
+
else if (e.type === 'workflow.agent.done') {
|
|
1518
|
+
const secs = e.durationMs ? ` · ${Math.round(e.durationMs / 1000)}s` : '';
|
|
1519
|
+
appendStatic([`${A.dim} ✓ ${e.label ?? e.agentId}${secs}${A.reset}`]);
|
|
1520
|
+
}
|
|
1521
|
+
else if (e.type === 'workflow.log') {
|
|
1522
|
+
appendStatic([`${A.dim} │ ${e.message}${A.reset}`]);
|
|
1523
|
+
}
|
|
1524
|
+
else if (e.type === 'workflow.script.done') {
|
|
1525
|
+
const secs = e.durationMs ? ` · ${Math.round(e.durationMs / 1000)}s` : '';
|
|
1526
|
+
appendStatic([
|
|
1527
|
+
`${A.green}✓ workflow done${A.reset}${A.dim} · ${e.agentCount} agents · ${e.totalTokens} tokens${secs}${A.reset}`,
|
|
1528
|
+
'',
|
|
1529
|
+
]);
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1533
|
+
const output = await runtime.execute(wf.script);
|
|
1534
|
+
if (typeof output.result === 'string' && output.result.trim()) {
|
|
1535
|
+
appendStatic([...formatInfoLine('workflow result:'), ...renderRichText(output.result, screen.width), '']);
|
|
1536
|
+
}
|
|
1537
|
+
if (output.state.errors.length > 0) {
|
|
1538
|
+
appendStatic([...formatErrorLine(`${output.state.errors.length} errors during workflow execution`), '']);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
appendStatic([...formatErrorLine(`workflow error: ${error.message}`), '']);
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
if (args.startsWith('run ')) {
|
|
1546
|
+
const runRest = args.slice(4).trim();
|
|
1547
|
+
const runSpace = runRest.indexOf(' ');
|
|
1548
|
+
await runSavedWorkflow(runSpace === -1 ? runRest : runRest.slice(0, runSpace), runSpace === -1 ? undefined : runRest.slice(runSpace + 1).trim());
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
// No sub-command → selection picker.
|
|
1552
|
+
const saved = listWorkflows(sdk.config.workDir);
|
|
1553
|
+
const items = [
|
|
1554
|
+
...saved.map((w) => ({
|
|
1555
|
+
id: `run:${w.name}`,
|
|
1556
|
+
label: w.name,
|
|
1557
|
+
description: `${w.source} · ${w.description}`.slice(0, 80),
|
|
1558
|
+
})),
|
|
1559
|
+
{
|
|
1560
|
+
id: '__orchestrate__',
|
|
1561
|
+
label: '+ ask the agent to orchestrate a new workflow',
|
|
1562
|
+
description: 'describe a task in the prompt box; the agent designs & runs a workflow, then you can save it',
|
|
1563
|
+
},
|
|
1564
|
+
];
|
|
1565
|
+
const choice = await selectItem({
|
|
1566
|
+
title: 'Workflows',
|
|
1567
|
+
subtitle: 'run a saved workflow, or have the agent build a new one',
|
|
1568
|
+
items,
|
|
1569
|
+
});
|
|
1570
|
+
if (!choice)
|
|
1571
|
+
return;
|
|
1572
|
+
if (choice.startsWith('run:')) {
|
|
1573
|
+
const name = choice.slice('run:'.length);
|
|
1574
|
+
const task = await promptText({ title: `Run /${name}`, label: 'Task / input (optional — Enter to skip)' });
|
|
1575
|
+
await runSavedWorkflow(name, task && task.trim() ? task.trim() : undefined);
|
|
1576
|
+
}
|
|
1577
|
+
else if (choice === '__orchestrate__') {
|
|
1578
|
+
appendStatic([
|
|
1579
|
+
...formatInfoLine('Type your task in the prompt box and ask: "orchestrate a workflow to <task>".'),
|
|
1580
|
+
`${A.dim}After it runs and works, ask me to save it as a reusable workflow.${A.reset}`,
|
|
1581
|
+
'',
|
|
1582
|
+
]);
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
// ── v0.5.0: Worktrees ────────────────────────────────────
|
|
1587
|
+
case 'worktree': {
|
|
1588
|
+
const ws = new WorktreeService(sdk.config.workDir);
|
|
1589
|
+
if (args === 'list') {
|
|
1590
|
+
await ws.init();
|
|
1591
|
+
const trees = await ws.listWorktrees();
|
|
1592
|
+
if (trees.length === 0) {
|
|
1593
|
+
appendStatic([...formatInfoLine('no worktrees'), '']);
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
appendStatic([
|
|
1597
|
+
...trees.map((t) => `${A.dim}${t.path}${A.reset} · ${t.isDirty ? `${A.yellow}dirty${A.reset}` : `${A.green}clean${A.reset}`}`),
|
|
1598
|
+
'',
|
|
1599
|
+
]);
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (args === 'exit') {
|
|
1604
|
+
try {
|
|
1605
|
+
ws.exitWorktree();
|
|
1606
|
+
appendStatic([...formatInfoLine(`exited worktree, cwd: ${ws.currentWorkDir}`), '']);
|
|
1607
|
+
}
|
|
1608
|
+
catch (error) {
|
|
1609
|
+
appendStatic([...formatErrorLine(error.message), '']);
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (args.startsWith('enter ')) {
|
|
1614
|
+
const wfName = args.slice(6).trim();
|
|
1615
|
+
try {
|
|
1616
|
+
await ws.init();
|
|
1617
|
+
await ws.createAndEnterWorktree({ name: wfName });
|
|
1618
|
+
appendStatic([...formatInfoLine(`entered worktree: ${wfName} (${ws.currentWorkDir})`), '']);
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
appendStatic([...formatErrorLine(error.message), '']);
|
|
1622
|
+
}
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
appendStatic([...formatInfoLine('usage: /worktree [enter <name>|exit|list]'), '']);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
// ── v0.5.0: Model Team ───────────────────────────────────
|
|
1629
|
+
case 'team': {
|
|
1630
|
+
if (args.startsWith('ask ')) {
|
|
1631
|
+
const rest = args.slice(4).trim();
|
|
1632
|
+
const spaceIdx = rest.indexOf(' ');
|
|
1633
|
+
if (spaceIdx === -1) {
|
|
1634
|
+
appendStatic([...formatErrorLine('usage: /team ask <name> <prompt>'), '']);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const teamName = rest.slice(0, spaceIdx);
|
|
1638
|
+
const prompt = rest.slice(spaceIdx + 1).trim();
|
|
1639
|
+
const loaded = loadTeamDefinition(teamName, sdk.config.workDir);
|
|
1640
|
+
if (!loaded) {
|
|
1641
|
+
appendStatic([...formatErrorLine(`team not found: ${teamName}`), '']);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
appendStatic([
|
|
1645
|
+
...formatInfoLine(`asking team "${teamName}" (${loaded.definition.mode} mode)`),
|
|
1646
|
+
`${A.dim}convening: ${loaded.definition.members?.map((m) => m.model).join(', ') ?? 'configured members'}${A.reset}`,
|
|
1647
|
+
'',
|
|
1648
|
+
]);
|
|
1649
|
+
try {
|
|
1650
|
+
const team = createModelTeam(loaded.definition);
|
|
1651
|
+
const result = await team.ask(prompt);
|
|
1652
|
+
appendStatic([
|
|
1653
|
+
`${A.green}✓ team response${A.reset}${A.dim} · ${result.mode} · ${Math.round(result.durationMs / 1000)}s${A.reset}`,
|
|
1654
|
+
`${A.dim}cost: ${result.cost.estimatedCost !== null ? `$${result.cost.estimatedCost.toFixed(4)}` : 'N/A'} · ${result.cost.totalInputTokens + result.cost.totalOutputTokens} tokens${A.reset}`,
|
|
1655
|
+
'',
|
|
1656
|
+
...renderRichText(result.answer, screen.width),
|
|
1657
|
+
'',
|
|
1658
|
+
]);
|
|
1659
|
+
}
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
appendStatic([...formatErrorLine(`team error: ${error.message}`), '']);
|
|
1662
|
+
}
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
// No sub-command → picker that toggles which team the agent may call.
|
|
1666
|
+
const member = (sp) => ({ model: session.model, systemPrompt: sp });
|
|
1667
|
+
const buildDefault = (mode) => {
|
|
1668
|
+
switch (mode) {
|
|
1669
|
+
case 'panel-analysis':
|
|
1670
|
+
return { name: 'panel-analysis', mode: 'panel-analysis', members: [member('Expert researcher. Investigate with read-only tools; cite sources.'), member('Rigorous skeptic. Verify with sources; challenge assumptions.')], primary: member('Synthesizer. Reconcile the panel findings into the best answer and decide when they suffice.'), timeoutMs: 300000, maxIterations: 12 };
|
|
1671
|
+
case 'analysis':
|
|
1672
|
+
return { name: 'analysis-panel', mode: 'analysis', members: [member('Expert researcher. Deep, source-grounded analysis.'), member('Rigorous skeptic. Verify with sources; challenge assumptions.')], timeoutMs: 300000, maxIterations: 12 };
|
|
1673
|
+
case 'reviewer':
|
|
1674
|
+
return { name: 'reviewer', mode: 'reviewer', members: [], reviewer: member('Meticulous reviewer. Surface only genuine, verifiable issues with file:line evidence; never speculate.'), timeoutMs: 300000, maxIterations: 16 };
|
|
1675
|
+
default:
|
|
1676
|
+
return undefined;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
const saved = listTeamDefinitions(sdk.config.workDir);
|
|
1680
|
+
const items = [
|
|
1681
|
+
{ id: '__none__', label: activeTeamTool ? `No team — remove "${activeTeamName}"` : 'No team (individual) — current', description: 'the agent works solo, no team tool attached' },
|
|
1682
|
+
...saved.map((t) => ({ id: `saved:${t.name}`, label: t.name, description: `saved · ${t.definition.mode} · ${t.definition.members?.length ?? 0} members` })),
|
|
1683
|
+
...['panel-analysis', 'analysis', 'reviewer'].map((m) => ({ id: `mode:${m}`, label: `+ new ${m} team`, description: `built-in ${m} mode · default ${session.model} members` })),
|
|
1684
|
+
];
|
|
1685
|
+
const choice = await selectItem({ title: 'Team', subtitle: 'attach a team the agent can call as a tool, or remove it', items });
|
|
1686
|
+
if (!choice)
|
|
1687
|
+
return;
|
|
1688
|
+
if (choice === '__none__') {
|
|
1689
|
+
activeTeamTool = null;
|
|
1690
|
+
activeTeamName = null;
|
|
1691
|
+
appendStatic([...formatInfoLine('team: none — the agent works individually'), '']);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
let def;
|
|
1695
|
+
if (choice.startsWith('saved:'))
|
|
1696
|
+
def = loadTeamDefinition(choice.slice('saved:'.length), sdk.config.workDir)?.definition;
|
|
1697
|
+
else if (choice.startsWith('mode:'))
|
|
1698
|
+
def = buildDefault(choice.slice('mode:'.length));
|
|
1699
|
+
if (!def) {
|
|
1700
|
+
appendStatic([...formatErrorLine('could not load team definition'), '']);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
try {
|
|
1704
|
+
activeTeamTool = createTeamTool(def);
|
|
1705
|
+
activeTeamName = def.name;
|
|
1706
|
+
appendStatic([...formatInfoLine(`team active: ${def.name} (${def.mode}) — the agent can now call "${def.name}" as a tool when it helps`), '']);
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
appendStatic([...formatErrorLine(`team error: ${error.message}`), '']);
|
|
1710
|
+
}
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1172
1713
|
default:
|
|
1173
1714
|
appendStatic([...formatErrorLine(`unknown command: /${name} — type /help`), '']);
|
|
1174
1715
|
return;
|
|
@@ -1181,6 +1722,10 @@ export async function runActoviqTui(options = {}) {
|
|
|
1181
1722
|
}
|
|
1182
1723
|
// ── Submit / key handling ──────────────────────────────────────────
|
|
1183
1724
|
async function submit() {
|
|
1725
|
+
if (!running && applyAtCompletion()) {
|
|
1726
|
+
renderDynamic();
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1184
1729
|
const matches = filterSlashCommands(editor.text);
|
|
1185
1730
|
if (matches.length > 0 && !running) {
|
|
1186
1731
|
const selected = matches[Math.min(menuSelected, matches.length - 1)];
|
|
@@ -1469,6 +2014,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
1469
2014
|
case 'backspace':
|
|
1470
2015
|
editor.backspace();
|
|
1471
2016
|
menuSelected = 0;
|
|
2017
|
+
atSelected = 0;
|
|
1472
2018
|
break;
|
|
1473
2019
|
case 'delete':
|
|
1474
2020
|
editor.deleteForward();
|
|
@@ -1492,8 +2038,13 @@ export async function runActoviqTui(options = {}) {
|
|
|
1492
2038
|
editor.moveEnd();
|
|
1493
2039
|
break;
|
|
1494
2040
|
case 'up': {
|
|
2041
|
+
const atToken = activeAtToken(editor.text, editor.cursor);
|
|
2042
|
+
const atCount = atToken ? atCompletions(atToken.token).length : 0;
|
|
1495
2043
|
const menu = filterSlashCommands(editor.text);
|
|
1496
|
-
if (
|
|
2044
|
+
if (atCount > 0) {
|
|
2045
|
+
atSelected = (atSelected + atCount - 1) % atCount;
|
|
2046
|
+
}
|
|
2047
|
+
else if (menu.length > 0) {
|
|
1497
2048
|
menuSelected = (menuSelected + menu.length - 1) % menu.length;
|
|
1498
2049
|
}
|
|
1499
2050
|
else if (!editor.onFirstLine()) {
|
|
@@ -1505,8 +2056,13 @@ export async function runActoviqTui(options = {}) {
|
|
|
1505
2056
|
break;
|
|
1506
2057
|
}
|
|
1507
2058
|
case 'down': {
|
|
2059
|
+
const atToken = activeAtToken(editor.text, editor.cursor);
|
|
2060
|
+
const atCount = atToken ? atCompletions(atToken.token).length : 0;
|
|
1508
2061
|
const menu = filterSlashCommands(editor.text);
|
|
1509
|
-
if (
|
|
2062
|
+
if (atCount > 0) {
|
|
2063
|
+
atSelected = (atSelected + 1) % atCount;
|
|
2064
|
+
}
|
|
2065
|
+
else if (menu.length > 0) {
|
|
1510
2066
|
menuSelected = (menuSelected + 1) % menu.length;
|
|
1511
2067
|
}
|
|
1512
2068
|
else if (!editor.onLastLine()) {
|
|
@@ -1518,6 +2074,8 @@ export async function runActoviqTui(options = {}) {
|
|
|
1518
2074
|
break;
|
|
1519
2075
|
}
|
|
1520
2076
|
case 'tab': {
|
|
2077
|
+
if (applyAtCompletion())
|
|
2078
|
+
break;
|
|
1521
2079
|
const menu = filterSlashCommands(editor.text);
|
|
1522
2080
|
if (menu.length > 0) {
|
|
1523
2081
|
const selected = menu[Math.min(menuSelected, menu.length - 1)];
|
|
@@ -1535,6 +2093,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
1535
2093
|
if (cleaned) {
|
|
1536
2094
|
editor.insert(cleaned);
|
|
1537
2095
|
menuSelected = 0;
|
|
2096
|
+
atSelected = 0;
|
|
1538
2097
|
}
|
|
1539
2098
|
}
|
|
1540
2099
|
break;
|
|
@@ -1591,4 +2150,16 @@ export async function runActoviqTui(options = {}) {
|
|
|
1591
2150
|
// Keep the process alive until shutdown() exits it.
|
|
1592
2151
|
await new Promise(() => { });
|
|
1593
2152
|
}
|
|
2153
|
+
// Allow running this module directly (`npx tsx src/tui/actoviqTui.ts`), not only
|
|
2154
|
+
// via the cli/ wrapper. Requires an interactive terminal.
|
|
2155
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2156
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2157
|
+
process.stderr.write('actoviq TUI requires an interactive terminal (TTY). Run it directly in your terminal — not piped or through another tool.\n');
|
|
2158
|
+
process.exit(1);
|
|
2159
|
+
}
|
|
2160
|
+
runActoviqTui().catch((error) => {
|
|
2161
|
+
process.stderr.write(`Fatal: ${error.stack ?? error.message}\n`);
|
|
2162
|
+
process.exit(1);
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
1594
2165
|
//# sourceMappingURL=actoviqTui.js.map
|