actoviq-agent-sdk 0.4.0 → 0.4.1
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 +10 -1
- package/README.md +15 -6
- 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 +15 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -1
- 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 +14 -0
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +84 -8
- 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 +29 -0
- package/dist/src/team/modelTeam.d.ts.map +1 -0
- package/dist/src/team/modelTeam.js +879 -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 +162 -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 +533 -34
- 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 +309 -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, 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,17 @@ 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;
|
|
136
194
|
let abortCtrl = null;
|
|
137
195
|
let dialog = null;
|
|
138
196
|
let selectionDialog = null;
|
|
139
197
|
let textInputDialog = null;
|
|
140
198
|
let menuSelected = 0;
|
|
199
|
+
// @-mention file completion: highlighted candidate + lazily-cached file list.
|
|
200
|
+
let atSelected = 0;
|
|
201
|
+
let workspaceFiles = null;
|
|
141
202
|
let spinnerFrame = 0;
|
|
142
203
|
let spinnerTimer = null;
|
|
143
204
|
let dynamicRenderTimer = null;
|
|
@@ -302,8 +363,9 @@ export async function runActoviqTui(options = {}) {
|
|
|
302
363
|
const lines = [];
|
|
303
364
|
lines.push(promptDivider());
|
|
304
365
|
if (editor.isEmpty()) {
|
|
305
|
-
|
|
306
|
-
|
|
366
|
+
// Always-visible block caret on the empty input so the box reads as active.
|
|
367
|
+
const placeholder = truncateToWidth('Try "write a test for <filepath>"', editorWidth - 4);
|
|
368
|
+
lines.push(`${A.magenta}${PROMPT_GLYPH}${A.reset} ${A.inverse} ${A.reset} ${A.dim}${placeholder}${A.reset}`);
|
|
307
369
|
}
|
|
308
370
|
else {
|
|
309
371
|
const visual = editor.visualLines(editorWidth - 1);
|
|
@@ -316,21 +378,152 @@ export async function runActoviqTui(options = {}) {
|
|
|
316
378
|
lines.push(promptDivider());
|
|
317
379
|
return lines;
|
|
318
380
|
}
|
|
381
|
+
// ── @-mention file completion ──────────────────────────────────────
|
|
382
|
+
// Prefer git's view (tracked + untracked, honoring .gitignore) so the list
|
|
383
|
+
// matches what the agent actually sees; fall back to a bounded fs walk for
|
|
384
|
+
// non-git workspaces. Cached for the session and invalidated after each run.
|
|
385
|
+
function loadWorkspaceFiles() {
|
|
386
|
+
if (workspaceFiles)
|
|
387
|
+
return workspaceFiles;
|
|
388
|
+
try {
|
|
389
|
+
const out = execSync('git ls-files --cached --others --exclude-standard', {
|
|
390
|
+
cwd: workDir,
|
|
391
|
+
encoding: 'utf8',
|
|
392
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
393
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
394
|
+
});
|
|
395
|
+
workspaceFiles = out.split('\n').map((line) => line.trim()).filter(Boolean).slice(0, 8000);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
workspaceFiles = walkWorkspaceFiles(workDir, 8000);
|
|
399
|
+
}
|
|
400
|
+
return workspaceFiles;
|
|
401
|
+
}
|
|
402
|
+
function walkWorkspaceFiles(root, limit) {
|
|
403
|
+
const skip = new Set(['node_modules', '.git', 'dist', '.codegraph', '.next', 'coverage']);
|
|
404
|
+
const out = [];
|
|
405
|
+
const stack = [root];
|
|
406
|
+
while (stack.length > 0 && out.length < limit) {
|
|
407
|
+
const dir = stack.pop();
|
|
408
|
+
let entries;
|
|
409
|
+
try {
|
|
410
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
if (entry.isDirectory()) {
|
|
417
|
+
if (!skip.has(entry.name) && !entry.name.startsWith('.'))
|
|
418
|
+
stack.push(path.join(dir, entry.name));
|
|
419
|
+
}
|
|
420
|
+
else if (entry.isFile()) {
|
|
421
|
+
out.push(path.relative(root, path.join(dir, entry.name)).split(path.sep).join('/'));
|
|
422
|
+
if (out.length >= limit)
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
function atCompletions(token) {
|
|
430
|
+
const files = loadWorkspaceFiles();
|
|
431
|
+
const query = token.toLowerCase();
|
|
432
|
+
if (!query)
|
|
433
|
+
return files.slice(0, 200);
|
|
434
|
+
const matches = files.filter((file) => file.toLowerCase().includes(query));
|
|
435
|
+
matches.sort((a, b) => {
|
|
436
|
+
const aBase = a.slice(a.lastIndexOf('/') + 1).toLowerCase();
|
|
437
|
+
const bBase = b.slice(b.lastIndexOf('/') + 1).toLowerCase();
|
|
438
|
+
const aScore = aBase.startsWith(query) ? 0 : aBase.includes(query) ? 1 : 2;
|
|
439
|
+
const bScore = bBase.startsWith(query) ? 0 : bBase.includes(query) ? 1 : 2;
|
|
440
|
+
if (aScore !== bScore)
|
|
441
|
+
return aScore - bScore;
|
|
442
|
+
return a.length - b.length;
|
|
443
|
+
});
|
|
444
|
+
return matches.slice(0, 200);
|
|
445
|
+
}
|
|
446
|
+
function buildAtMenu() {
|
|
447
|
+
const active = activeAtToken(editor.text, editor.cursor);
|
|
448
|
+
if (!active)
|
|
449
|
+
return [];
|
|
450
|
+
const matches = atCompletions(active.token);
|
|
451
|
+
if (matches.length === 0) {
|
|
452
|
+
return [`${A.dim} @${active.token} — no matching files${A.reset}`];
|
|
453
|
+
}
|
|
454
|
+
if (atSelected >= matches.length)
|
|
455
|
+
atSelected = matches.length - 1;
|
|
456
|
+
if (atSelected < 0)
|
|
457
|
+
atSelected = 0;
|
|
458
|
+
const windowStart = Math.max(0, Math.min(atSelected - MENU_MAX_ROWS + 1, matches.length - MENU_MAX_ROWS));
|
|
459
|
+
const visible = matches.slice(windowStart, windowStart + MENU_MAX_ROWS);
|
|
460
|
+
const lines = visible.map((file, i) => {
|
|
461
|
+
const index = windowStart + i;
|
|
462
|
+
const display = truncateToWidth(file, Math.max(screen.width - 6, 12));
|
|
463
|
+
return index === atSelected ? `${A.inverse} @${display} ${A.reset}` : ` ${A.cyan}@${display}${A.reset}`;
|
|
464
|
+
});
|
|
465
|
+
const hiddenAbove = windowStart;
|
|
466
|
+
const hiddenBelow = matches.length - (windowStart + visible.length);
|
|
467
|
+
if (hiddenAbove > 0 || hiddenBelow > 0) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
if (hiddenAbove > 0)
|
|
470
|
+
parts.push(`↑${hiddenAbove}`);
|
|
471
|
+
if (hiddenBelow > 0)
|
|
472
|
+
parts.push(`↓${hiddenBelow}`);
|
|
473
|
+
lines.push(`${A.dim} ${parts.join(' ')} more · ${atSelected + 1}/${matches.length} (↑/↓ · Tab/Enter to insert)${A.reset}`);
|
|
474
|
+
}
|
|
475
|
+
return lines;
|
|
476
|
+
}
|
|
477
|
+
/** Replace the active @-token with the highlighted file path. */
|
|
478
|
+
function applyAtCompletion() {
|
|
479
|
+
const active = activeAtToken(editor.text, editor.cursor);
|
|
480
|
+
if (!active)
|
|
481
|
+
return false;
|
|
482
|
+
const matches = atCompletions(active.token);
|
|
483
|
+
if (matches.length === 0)
|
|
484
|
+
return false;
|
|
485
|
+
const file = matches[Math.min(atSelected, matches.length - 1)];
|
|
486
|
+
const before = editor.text.slice(0, active.start);
|
|
487
|
+
const after = editor.text.slice(editor.cursor);
|
|
488
|
+
const mention = `@${file} `;
|
|
489
|
+
editor.setTextWithCursor(`${before}${mention}${after}`, before.length + mention.length);
|
|
490
|
+
atSelected = 0;
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
319
493
|
function buildMenu() {
|
|
320
494
|
const matches = filterSlashCommands(editor.text);
|
|
321
495
|
if (matches.length === 0)
|
|
322
496
|
return [];
|
|
323
497
|
if (menuSelected >= matches.length)
|
|
324
498
|
menuSelected = matches.length - 1;
|
|
499
|
+
if (menuSelected < 0)
|
|
500
|
+
menuSelected = 0;
|
|
325
501
|
const commandWidth = Math.min(28, Math.max(14, Math.floor(screen.width * 0.28)));
|
|
326
502
|
const descriptionWidth = Math.max(screen.width - commandWidth - 4, 12);
|
|
327
|
-
|
|
503
|
+
// Scroll a window of MENU_MAX_ROWS so the highlighted item stays visible and
|
|
504
|
+
// commands past the cap (e.g. /workflows, /worktree, /team) are reachable
|
|
505
|
+
// with the arrow keys instead of being clipped off the bottom.
|
|
506
|
+
const windowStart = Math.max(0, Math.min(menuSelected - MENU_MAX_ROWS + 1, matches.length - MENU_MAX_ROWS));
|
|
507
|
+
const visible = matches.slice(windowStart, windowStart + MENU_MAX_ROWS);
|
|
508
|
+
const lines = visible.map((name, i) => {
|
|
509
|
+
const index = windowStart + i;
|
|
328
510
|
const selected = index === menuSelected;
|
|
329
511
|
const command = `/${name}`.padEnd(commandWidth);
|
|
330
512
|
const label = selected ? `${A.inverse}${command}${A.reset}` : command;
|
|
331
513
|
const description = truncateToWidth(TUI_SLASH_COMMANDS[name] ?? '', descriptionWidth);
|
|
332
514
|
return `${label} ${A.dim}${description}${A.reset}`;
|
|
333
515
|
});
|
|
516
|
+
const hiddenAbove = windowStart;
|
|
517
|
+
const hiddenBelow = matches.length - (windowStart + visible.length);
|
|
518
|
+
if (hiddenAbove > 0 || hiddenBelow > 0) {
|
|
519
|
+
const parts = [];
|
|
520
|
+
if (hiddenAbove > 0)
|
|
521
|
+
parts.push(`↑${hiddenAbove}`);
|
|
522
|
+
if (hiddenBelow > 0)
|
|
523
|
+
parts.push(`↓${hiddenBelow}`);
|
|
524
|
+
lines.push(`${A.dim} ${parts.join(' ')} more · ${menuSelected + 1}/${matches.length} (↑/↓ to scroll)${A.reset}`);
|
|
525
|
+
}
|
|
526
|
+
return lines;
|
|
334
527
|
}
|
|
335
528
|
function buildDialog() {
|
|
336
529
|
if (!dialog)
|
|
@@ -405,25 +598,45 @@ export async function runActoviqTui(options = {}) {
|
|
|
405
598
|
lines.push(`${A.dim} enter confirm · esc cancel${textInputDialog.secret ? ' · value hidden' : ''}${A.reset}`);
|
|
406
599
|
return lines;
|
|
407
600
|
}
|
|
601
|
+
/** Friendly permission label matching the /permissions presets. */
|
|
602
|
+
function permissionLabel() {
|
|
603
|
+
const m = currentPermissionMode();
|
|
604
|
+
if (m === 'bypassPermissions')
|
|
605
|
+
return 'full-access';
|
|
606
|
+
if (m === 'acceptEdits')
|
|
607
|
+
return 'workspace';
|
|
608
|
+
if (m === 'default' && session.permissionContext.permissions.some((p) => p.behavior === 'deny'))
|
|
609
|
+
return 'read-only';
|
|
610
|
+
return m;
|
|
611
|
+
}
|
|
612
|
+
/** Always-visible mode + live context-usage line (usage shown as % of the window). */
|
|
613
|
+
function buildModeLine() {
|
|
614
|
+
const used = lastTokenEstimate ?? 0;
|
|
615
|
+
const window = sdk.config.compact?.contextWindowTokens ?? 200_000;
|
|
616
|
+
const pct = window > 0 ? Math.min(100, Math.round((used / window) * 100)) : 0;
|
|
617
|
+
const usedK = used >= 1000 ? `${(used / 1000).toFixed(used >= 100_000 ? 0 : 1)}k` : `${used}`;
|
|
618
|
+
const ctxColor = pct >= 90 ? A.red : pct >= 70 ? A.yellow : A.dim;
|
|
619
|
+
const left = `${session.model} · ${permissionLabel()} · effort:${currentEffort() ?? 'auto'} · team:${activeTeamName ?? 'none'} · `;
|
|
620
|
+
return `${A.dim} ${left}${A.reset}${ctxColor}ctx ${pct}% (${usedK})${A.reset}`;
|
|
621
|
+
}
|
|
408
622
|
function buildStatusLine() {
|
|
623
|
+
const modeLine = buildModeLine();
|
|
409
624
|
if (!running)
|
|
410
|
-
return [];
|
|
625
|
+
return [modeLine];
|
|
411
626
|
const elapsed = Math.max(Math.round((Date.now() - runStartedAt) / 1000), 0);
|
|
412
627
|
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
413
|
-
const context = lastTokenEstimate && lastTokenEstimate >= 1000
|
|
414
|
-
? ` · ~${Math.round(lastTokenEstimate / 1000)}k ctx`
|
|
415
|
-
: '';
|
|
416
628
|
const note = statusNote ? ` · ${statusNote}` : '';
|
|
417
629
|
const queued = queuedInputs.length > 0 ? ` · ${queuedInputs.length} queued` : '';
|
|
418
630
|
return [
|
|
419
|
-
`${A.cyan}${frame}${A.reset} ${A.bold}Working…${A.reset}${A.dim} (${elapsed}s · ${runToolCount} tool${runToolCount === 1 ? '' : 's'}${
|
|
631
|
+
`${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}`,
|
|
632
|
+
modeLine,
|
|
420
633
|
];
|
|
421
634
|
}
|
|
422
635
|
function buildHintLine() {
|
|
423
636
|
if (running) {
|
|
424
637
|
return [`${A.dim} enter to queue a steering message · esc interrupt · ctrl+c twice to exit${A.reset}`];
|
|
425
638
|
}
|
|
426
|
-
return [`${A.dim} ?
|
|
639
|
+
return [`${A.dim} ? shortcuts · / commands · @ files · \\↵ newline · ↑↓ history · ctrl+c clear/exit${A.reset}`];
|
|
427
640
|
}
|
|
428
641
|
function renderDynamic() {
|
|
429
642
|
const lines = [];
|
|
@@ -443,8 +656,14 @@ export async function runActoviqTui(options = {}) {
|
|
|
443
656
|
}
|
|
444
657
|
else {
|
|
445
658
|
lines.push(...buildPromptBar());
|
|
446
|
-
const
|
|
447
|
-
|
|
659
|
+
const atMenu = buildAtMenu();
|
|
660
|
+
if (atMenu.length > 0) {
|
|
661
|
+
lines.push(...atMenu);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
const menu = buildMenu();
|
|
665
|
+
lines.push(...(menu.length > 0 ? menu : buildHintLine()));
|
|
666
|
+
}
|
|
448
667
|
}
|
|
449
668
|
screen.setDynamic(lines);
|
|
450
669
|
}
|
|
@@ -487,6 +706,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
487
706
|
permissionMode: currentPermissionMode(),
|
|
488
707
|
effort: currentEffort(),
|
|
489
708
|
approver,
|
|
709
|
+
...(activeTeamTool ? { tools: [...tools, activeTeamTool] } : {}),
|
|
490
710
|
...(canUseTool ? { canUseTool } : {}),
|
|
491
711
|
drainQueuedInputs: () => {
|
|
492
712
|
const drained = queuedInputs.splice(0);
|
|
@@ -523,6 +743,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
523
743
|
finally {
|
|
524
744
|
running = false;
|
|
525
745
|
abortCtrl = null;
|
|
746
|
+
workspaceFiles = null; // the agent may have created/removed files — refresh @-completion
|
|
526
747
|
if (spinnerTimer) {
|
|
527
748
|
clearInterval(spinnerTimer);
|
|
528
749
|
spinnerTimer = null;
|
|
@@ -630,6 +851,9 @@ export async function runActoviqTui(options = {}) {
|
|
|
630
851
|
mcp: '/mcp',
|
|
631
852
|
plugins: '/plugins',
|
|
632
853
|
dream: '/dream [run|status]',
|
|
854
|
+
workflows: '/workflows [run <name>|list]',
|
|
855
|
+
worktree: '/worktree [enter <name>|exit|list]',
|
|
856
|
+
team: '/team [ask <name> <prompt>|list]',
|
|
633
857
|
exit: '/exit',
|
|
634
858
|
};
|
|
635
859
|
return usage[name] ?? `/${name}`;
|
|
@@ -884,15 +1108,17 @@ export async function runActoviqTui(options = {}) {
|
|
|
884
1108
|
title: 'Skills',
|
|
885
1109
|
items: skills.map(skill => ({
|
|
886
1110
|
id: skill.name,
|
|
887
|
-
label: skill.name,
|
|
888
|
-
description: `${skill.source} · ${skill.context}`,
|
|
1111
|
+
label: skill.displayName ? `${skill.displayName} (${skill.name})` : skill.name,
|
|
1112
|
+
description: `${skill.source} · ${skill.context}${skill.version ? ` · v${skill.version}` : ''}`,
|
|
889
1113
|
detail: `${skill.description} ${skill.whenToUse ?? ''}`,
|
|
890
1114
|
})),
|
|
891
1115
|
});
|
|
892
1116
|
const skill = skills.find(item => item.name === selected);
|
|
893
1117
|
if (skill) {
|
|
1118
|
+
const heading = skill.displayName ? `${skill.displayName} (/${skill.name})` : `/${skill.name}`;
|
|
1119
|
+
const ver = skill.version ? ` v${skill.version}` : '';
|
|
894
1120
|
appendStatic([
|
|
895
|
-
`${A.cyan}
|
|
1121
|
+
`${A.cyan}${heading}${A.reset}${A.dim}${ver}${A.reset} ${skill.description}`,
|
|
896
1122
|
`${A.dim}${skill.whenToUse ?? `source: ${skill.source} · context: ${skill.context}`}${A.reset}`,
|
|
897
1123
|
'',
|
|
898
1124
|
]);
|
|
@@ -1070,25 +1296,45 @@ export async function runActoviqTui(options = {}) {
|
|
|
1070
1296
|
await setEffort(args.toLowerCase());
|
|
1071
1297
|
return;
|
|
1072
1298
|
case 'permissions': {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1299
|
+
// Three presets, selectable or named directly:
|
|
1300
|
+
// read-only → deny mutating tools (read / search / web only)
|
|
1301
|
+
// workspace → acceptEdits (auto-accept edits in the workspace)
|
|
1302
|
+
// full → bypassPermissions (no prompts)
|
|
1303
|
+
const READONLY_DENY = ['Bash', 'Write', 'Edit', 'NotebookEdit', 'PowerShell'];
|
|
1304
|
+
const presets = {
|
|
1305
|
+
'read-only': {
|
|
1306
|
+
mode: 'default',
|
|
1307
|
+
rules: READONLY_DENY.map((t) => ({ toolName: t, behavior: 'deny', source: 'permissions-preset' })),
|
|
1308
|
+
label: 'Read-only',
|
|
1309
|
+
},
|
|
1310
|
+
workspace: { mode: 'acceptEdits', rules: [], label: 'Workspace access' },
|
|
1311
|
+
full: { mode: 'bypassPermissions', rules: [], label: 'Full access' },
|
|
1312
|
+
};
|
|
1313
|
+
let key = args.trim().toLowerCase().replace(/[ _]/g, '-');
|
|
1314
|
+
if (!key) {
|
|
1315
|
+
const choice = await selectItem({
|
|
1316
|
+
title: 'Permission mode',
|
|
1317
|
+
subtitle: `current: ${session.permissionContext.mode ?? permissionMode}`,
|
|
1318
|
+
items: [
|
|
1319
|
+
{ id: 'read-only', label: 'Read-only', description: 'Read, search, and web only — deny Write/Edit/Bash/NotebookEdit/PowerShell' },
|
|
1320
|
+
{ id: 'workspace', label: 'Workspace access', description: 'Auto-accept edits in the workspace (acceptEdits)' },
|
|
1321
|
+
{ id: 'full', label: 'Full access', description: 'No prompts — run any tool (bypassPermissions)' },
|
|
1322
|
+
],
|
|
1323
|
+
});
|
|
1324
|
+
if (!choice)
|
|
1325
|
+
return;
|
|
1326
|
+
key = choice;
|
|
1081
1327
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1328
|
+
const preset = presets[key];
|
|
1329
|
+
if (!preset) {
|
|
1330
|
+
appendStatic([...formatErrorLine(`unknown permission preset: ${key} (read-only | workspace | full)`), '']);
|
|
1084
1331
|
return;
|
|
1085
1332
|
}
|
|
1086
|
-
await session.setPermissionContext({
|
|
1087
|
-
|
|
1088
|
-
permissions:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
appendStatic([...formatInfoLine(`permissions set to: ${requested}`), '']);
|
|
1333
|
+
await session.setPermissionContext({ mode: preset.mode, permissions: preset.rules, approver });
|
|
1334
|
+
appendStatic([
|
|
1335
|
+
...formatInfoLine(`permissions: ${preset.label} — ${preset.mode}${preset.rules.length ? ` · ${preset.rules.length} deny rules` : ''}`),
|
|
1336
|
+
'',
|
|
1337
|
+
]);
|
|
1092
1338
|
return;
|
|
1093
1339
|
}
|
|
1094
1340
|
case 'sessions': {
|
|
@@ -1169,6 +1415,229 @@ export async function runActoviqTui(options = {}) {
|
|
|
1169
1415
|
case 'plugins':
|
|
1170
1416
|
await showPlugins();
|
|
1171
1417
|
return;
|
|
1418
|
+
// ── v0.5.0: Dynamic Workflows ────────────────────────────
|
|
1419
|
+
case 'workflows': {
|
|
1420
|
+
const runSavedWorkflow = async (wfName, wfTask) => {
|
|
1421
|
+
const wf = loadWorkflow(wfName, sdk.config.workDir);
|
|
1422
|
+
if (!wf) {
|
|
1423
|
+
appendStatic([...formatErrorLine(`workflow not found: ${wfName}`), '']);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
appendStatic([
|
|
1427
|
+
...formatInfoLine(`running workflow: ${wfName}`),
|
|
1428
|
+
...formatInfoLine(`phases: ${wf.meta?.phases?.map((p) => p.title).join(', ') ?? 'none'}`),
|
|
1429
|
+
'',
|
|
1430
|
+
]);
|
|
1431
|
+
try {
|
|
1432
|
+
const { WorkflowScriptRuntime } = await import('../workflow/workflowScriptRuntime.js');
|
|
1433
|
+
const runtime = new WorkflowScriptRuntime({
|
|
1434
|
+
sdk: sdk,
|
|
1435
|
+
args: wfTask,
|
|
1436
|
+
onEvent: (e) => {
|
|
1437
|
+
if (e.type === 'workflow.phase.start') {
|
|
1438
|
+
appendStatic([`${A.bold}${A.magenta}▶ ${e.title}${A.reset}`]);
|
|
1439
|
+
}
|
|
1440
|
+
else if (e.type === 'workflow.agent.start') {
|
|
1441
|
+
appendStatic([`${A.dim} ⚡ ${e.label ?? e.agentId}${e.cached ? ' (cached)' : ''}${A.reset}`]);
|
|
1442
|
+
}
|
|
1443
|
+
else if (e.type === 'workflow.agent.done') {
|
|
1444
|
+
const secs = e.durationMs ? ` · ${Math.round(e.durationMs / 1000)}s` : '';
|
|
1445
|
+
appendStatic([`${A.dim} ✓ ${e.label ?? e.agentId}${secs}${A.reset}`]);
|
|
1446
|
+
}
|
|
1447
|
+
else if (e.type === 'workflow.log') {
|
|
1448
|
+
appendStatic([`${A.dim} │ ${e.message}${A.reset}`]);
|
|
1449
|
+
}
|
|
1450
|
+
else if (e.type === 'workflow.script.done') {
|
|
1451
|
+
const secs = e.durationMs ? ` · ${Math.round(e.durationMs / 1000)}s` : '';
|
|
1452
|
+
appendStatic([
|
|
1453
|
+
`${A.green}✓ workflow done${A.reset}${A.dim} · ${e.agentCount} agents · ${e.totalTokens} tokens${secs}${A.reset}`,
|
|
1454
|
+
'',
|
|
1455
|
+
]);
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
});
|
|
1459
|
+
const output = await runtime.execute(wf.script);
|
|
1460
|
+
if (typeof output.result === 'string' && output.result.trim()) {
|
|
1461
|
+
appendStatic([...formatInfoLine('workflow result:'), ...renderRichText(output.result, screen.width), '']);
|
|
1462
|
+
}
|
|
1463
|
+
if (output.state.errors.length > 0) {
|
|
1464
|
+
appendStatic([...formatErrorLine(`${output.state.errors.length} errors during workflow execution`), '']);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
appendStatic([...formatErrorLine(`workflow error: ${error.message}`), '']);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
if (args.startsWith('run ')) {
|
|
1472
|
+
const runRest = args.slice(4).trim();
|
|
1473
|
+
const runSpace = runRest.indexOf(' ');
|
|
1474
|
+
await runSavedWorkflow(runSpace === -1 ? runRest : runRest.slice(0, runSpace), runSpace === -1 ? undefined : runRest.slice(runSpace + 1).trim());
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
// No sub-command → selection picker.
|
|
1478
|
+
const saved = listWorkflows(sdk.config.workDir);
|
|
1479
|
+
const items = [
|
|
1480
|
+
...saved.map((w) => ({
|
|
1481
|
+
id: `run:${w.name}`,
|
|
1482
|
+
label: w.name,
|
|
1483
|
+
description: `${w.source} · ${w.description}`.slice(0, 80),
|
|
1484
|
+
})),
|
|
1485
|
+
{
|
|
1486
|
+
id: '__orchestrate__',
|
|
1487
|
+
label: '+ ask the agent to orchestrate a new workflow',
|
|
1488
|
+
description: 'describe a task in the prompt box; the agent designs & runs a workflow, then you can save it',
|
|
1489
|
+
},
|
|
1490
|
+
];
|
|
1491
|
+
const choice = await selectItem({
|
|
1492
|
+
title: 'Workflows',
|
|
1493
|
+
subtitle: 'run a saved workflow, or have the agent build a new one',
|
|
1494
|
+
items,
|
|
1495
|
+
});
|
|
1496
|
+
if (!choice)
|
|
1497
|
+
return;
|
|
1498
|
+
if (choice.startsWith('run:')) {
|
|
1499
|
+
const name = choice.slice('run:'.length);
|
|
1500
|
+
const task = await promptText({ title: `Run /${name}`, label: 'Task / input (optional — Enter to skip)' });
|
|
1501
|
+
await runSavedWorkflow(name, task && task.trim() ? task.trim() : undefined);
|
|
1502
|
+
}
|
|
1503
|
+
else if (choice === '__orchestrate__') {
|
|
1504
|
+
appendStatic([
|
|
1505
|
+
...formatInfoLine('Type your task in the prompt box and ask: "orchestrate a workflow to <task>".'),
|
|
1506
|
+
`${A.dim}After it runs and works, ask me to save it as a reusable workflow.${A.reset}`,
|
|
1507
|
+
'',
|
|
1508
|
+
]);
|
|
1509
|
+
}
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
// ── v0.5.0: Worktrees ────────────────────────────────────
|
|
1513
|
+
case 'worktree': {
|
|
1514
|
+
const ws = new WorktreeService(sdk.config.workDir);
|
|
1515
|
+
if (args === 'list') {
|
|
1516
|
+
await ws.init();
|
|
1517
|
+
const trees = await ws.listWorktrees();
|
|
1518
|
+
if (trees.length === 0) {
|
|
1519
|
+
appendStatic([...formatInfoLine('no worktrees'), '']);
|
|
1520
|
+
}
|
|
1521
|
+
else {
|
|
1522
|
+
appendStatic([
|
|
1523
|
+
...trees.map((t) => `${A.dim}${t.path}${A.reset} · ${t.isDirty ? `${A.yellow}dirty${A.reset}` : `${A.green}clean${A.reset}`}`),
|
|
1524
|
+
'',
|
|
1525
|
+
]);
|
|
1526
|
+
}
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (args === 'exit') {
|
|
1530
|
+
try {
|
|
1531
|
+
ws.exitWorktree();
|
|
1532
|
+
appendStatic([...formatInfoLine(`exited worktree, cwd: ${ws.currentWorkDir}`), '']);
|
|
1533
|
+
}
|
|
1534
|
+
catch (error) {
|
|
1535
|
+
appendStatic([...formatErrorLine(error.message), '']);
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (args.startsWith('enter ')) {
|
|
1540
|
+
const wfName = args.slice(6).trim();
|
|
1541
|
+
try {
|
|
1542
|
+
await ws.init();
|
|
1543
|
+
await ws.createAndEnterWorktree({ name: wfName });
|
|
1544
|
+
appendStatic([...formatInfoLine(`entered worktree: ${wfName} (${ws.currentWorkDir})`), '']);
|
|
1545
|
+
}
|
|
1546
|
+
catch (error) {
|
|
1547
|
+
appendStatic([...formatErrorLine(error.message), '']);
|
|
1548
|
+
}
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
appendStatic([...formatInfoLine('usage: /worktree [enter <name>|exit|list]'), '']);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
// ── v0.5.0: Model Team ───────────────────────────────────
|
|
1555
|
+
case 'team': {
|
|
1556
|
+
if (args.startsWith('ask ')) {
|
|
1557
|
+
const rest = args.slice(4).trim();
|
|
1558
|
+
const spaceIdx = rest.indexOf(' ');
|
|
1559
|
+
if (spaceIdx === -1) {
|
|
1560
|
+
appendStatic([...formatErrorLine('usage: /team ask <name> <prompt>'), '']);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const teamName = rest.slice(0, spaceIdx);
|
|
1564
|
+
const prompt = rest.slice(spaceIdx + 1).trim();
|
|
1565
|
+
const loaded = loadTeamDefinition(teamName, sdk.config.workDir);
|
|
1566
|
+
if (!loaded) {
|
|
1567
|
+
appendStatic([...formatErrorLine(`team not found: ${teamName}`), '']);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
appendStatic([
|
|
1571
|
+
...formatInfoLine(`asking team "${teamName}" (${loaded.definition.mode} mode)`),
|
|
1572
|
+
`${A.dim}convening: ${loaded.definition.members?.map((m) => m.model).join(', ') ?? 'configured members'}${A.reset}`,
|
|
1573
|
+
'',
|
|
1574
|
+
]);
|
|
1575
|
+
try {
|
|
1576
|
+
const team = createModelTeam(loaded.definition);
|
|
1577
|
+
const result = await team.ask(prompt);
|
|
1578
|
+
appendStatic([
|
|
1579
|
+
`${A.green}✓ team response${A.reset}${A.dim} · ${result.mode} · ${Math.round(result.durationMs / 1000)}s${A.reset}`,
|
|
1580
|
+
`${A.dim}cost: ${result.cost.estimatedCost !== null ? `$${result.cost.estimatedCost.toFixed(4)}` : 'N/A'} · ${result.cost.totalInputTokens + result.cost.totalOutputTokens} tokens${A.reset}`,
|
|
1581
|
+
'',
|
|
1582
|
+
...renderRichText(result.answer, screen.width),
|
|
1583
|
+
'',
|
|
1584
|
+
]);
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
appendStatic([...formatErrorLine(`team error: ${error.message}`), '']);
|
|
1588
|
+
}
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
// No sub-command → picker that toggles which team the agent may call.
|
|
1592
|
+
const member = (sp) => ({ model: session.model, systemPrompt: sp });
|
|
1593
|
+
const buildDefault = (mode) => {
|
|
1594
|
+
switch (mode) {
|
|
1595
|
+
case 'analysis':
|
|
1596
|
+
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 };
|
|
1597
|
+
case 'panel':
|
|
1598
|
+
return { name: 'panel', mode: 'panel', members: [member('Thorough analyst.'), member('Creative problem-solver.')], primary: member('Synthesizer. Reconcile views into the best answer.'), timeoutMs: 300000 };
|
|
1599
|
+
case 'discussion':
|
|
1600
|
+
return { name: 'discussion', mode: 'discussion', members: [member('Systems thinker.'), member('Pragmatist who weighs trade-offs.')], primary: member('Convener and final decision-maker.'), timeoutMs: 300000 };
|
|
1601
|
+
case 'executor-reviewer':
|
|
1602
|
+
return { name: 'executor-reviewer', mode: 'executor-reviewer', members: [], executor: member('Executor. Own the output; you decide what to accept.'), reviewer: member('Reviewer. Advise; never command.'), timeoutMs: 300000 };
|
|
1603
|
+
default:
|
|
1604
|
+
return undefined;
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const saved = listTeamDefinitions(sdk.config.workDir);
|
|
1608
|
+
const items = [
|
|
1609
|
+
{ id: '__none__', label: activeTeamTool ? `No team — remove "${activeTeamName}"` : 'No team (individual) — current', description: 'the agent works solo, no team tool attached' },
|
|
1610
|
+
...saved.map((t) => ({ id: `saved:${t.name}`, label: t.name, description: `saved · ${t.definition.mode} · ${t.definition.members?.length ?? 0} members` })),
|
|
1611
|
+
...['analysis', 'panel', 'discussion', 'executor-reviewer'].map((m) => ({ id: `mode:${m}`, label: `+ new ${m} team`, description: `built-in ${m} mode · default ${session.model} members` })),
|
|
1612
|
+
];
|
|
1613
|
+
const choice = await selectItem({ title: 'Team', subtitle: 'attach a team the agent can call as a tool, or remove it', items });
|
|
1614
|
+
if (!choice)
|
|
1615
|
+
return;
|
|
1616
|
+
if (choice === '__none__') {
|
|
1617
|
+
activeTeamTool = null;
|
|
1618
|
+
activeTeamName = null;
|
|
1619
|
+
appendStatic([...formatInfoLine('team: none — the agent works individually'), '']);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
let def;
|
|
1623
|
+
if (choice.startsWith('saved:'))
|
|
1624
|
+
def = loadTeamDefinition(choice.slice('saved:'.length), sdk.config.workDir)?.definition;
|
|
1625
|
+
else if (choice.startsWith('mode:'))
|
|
1626
|
+
def = buildDefault(choice.slice('mode:'.length));
|
|
1627
|
+
if (!def) {
|
|
1628
|
+
appendStatic([...formatErrorLine('could not load team definition'), '']);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
activeTeamTool = createTeamTool(def);
|
|
1633
|
+
activeTeamName = def.name;
|
|
1634
|
+
appendStatic([...formatInfoLine(`team active: ${def.name} (${def.mode}) — the agent can now call "${def.name}" as a tool when it helps`), '']);
|
|
1635
|
+
}
|
|
1636
|
+
catch (error) {
|
|
1637
|
+
appendStatic([...formatErrorLine(`team error: ${error.message}`), '']);
|
|
1638
|
+
}
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1172
1641
|
default:
|
|
1173
1642
|
appendStatic([...formatErrorLine(`unknown command: /${name} — type /help`), '']);
|
|
1174
1643
|
return;
|
|
@@ -1181,6 +1650,10 @@ export async function runActoviqTui(options = {}) {
|
|
|
1181
1650
|
}
|
|
1182
1651
|
// ── Submit / key handling ──────────────────────────────────────────
|
|
1183
1652
|
async function submit() {
|
|
1653
|
+
if (!running && applyAtCompletion()) {
|
|
1654
|
+
renderDynamic();
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1184
1657
|
const matches = filterSlashCommands(editor.text);
|
|
1185
1658
|
if (matches.length > 0 && !running) {
|
|
1186
1659
|
const selected = matches[Math.min(menuSelected, matches.length - 1)];
|
|
@@ -1469,6 +1942,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
1469
1942
|
case 'backspace':
|
|
1470
1943
|
editor.backspace();
|
|
1471
1944
|
menuSelected = 0;
|
|
1945
|
+
atSelected = 0;
|
|
1472
1946
|
break;
|
|
1473
1947
|
case 'delete':
|
|
1474
1948
|
editor.deleteForward();
|
|
@@ -1492,8 +1966,13 @@ export async function runActoviqTui(options = {}) {
|
|
|
1492
1966
|
editor.moveEnd();
|
|
1493
1967
|
break;
|
|
1494
1968
|
case 'up': {
|
|
1969
|
+
const atToken = activeAtToken(editor.text, editor.cursor);
|
|
1970
|
+
const atCount = atToken ? atCompletions(atToken.token).length : 0;
|
|
1495
1971
|
const menu = filterSlashCommands(editor.text);
|
|
1496
|
-
if (
|
|
1972
|
+
if (atCount > 0) {
|
|
1973
|
+
atSelected = (atSelected + atCount - 1) % atCount;
|
|
1974
|
+
}
|
|
1975
|
+
else if (menu.length > 0) {
|
|
1497
1976
|
menuSelected = (menuSelected + menu.length - 1) % menu.length;
|
|
1498
1977
|
}
|
|
1499
1978
|
else if (!editor.onFirstLine()) {
|
|
@@ -1505,8 +1984,13 @@ export async function runActoviqTui(options = {}) {
|
|
|
1505
1984
|
break;
|
|
1506
1985
|
}
|
|
1507
1986
|
case 'down': {
|
|
1987
|
+
const atToken = activeAtToken(editor.text, editor.cursor);
|
|
1988
|
+
const atCount = atToken ? atCompletions(atToken.token).length : 0;
|
|
1508
1989
|
const menu = filterSlashCommands(editor.text);
|
|
1509
|
-
if (
|
|
1990
|
+
if (atCount > 0) {
|
|
1991
|
+
atSelected = (atSelected + 1) % atCount;
|
|
1992
|
+
}
|
|
1993
|
+
else if (menu.length > 0) {
|
|
1510
1994
|
menuSelected = (menuSelected + 1) % menu.length;
|
|
1511
1995
|
}
|
|
1512
1996
|
else if (!editor.onLastLine()) {
|
|
@@ -1518,6 +2002,8 @@ export async function runActoviqTui(options = {}) {
|
|
|
1518
2002
|
break;
|
|
1519
2003
|
}
|
|
1520
2004
|
case 'tab': {
|
|
2005
|
+
if (applyAtCompletion())
|
|
2006
|
+
break;
|
|
1521
2007
|
const menu = filterSlashCommands(editor.text);
|
|
1522
2008
|
if (menu.length > 0) {
|
|
1523
2009
|
const selected = menu[Math.min(menuSelected, menu.length - 1)];
|
|
@@ -1535,6 +2021,7 @@ export async function runActoviqTui(options = {}) {
|
|
|
1535
2021
|
if (cleaned) {
|
|
1536
2022
|
editor.insert(cleaned);
|
|
1537
2023
|
menuSelected = 0;
|
|
2024
|
+
atSelected = 0;
|
|
1538
2025
|
}
|
|
1539
2026
|
}
|
|
1540
2027
|
break;
|
|
@@ -1591,4 +2078,16 @@ export async function runActoviqTui(options = {}) {
|
|
|
1591
2078
|
// Keep the process alive until shutdown() exits it.
|
|
1592
2079
|
await new Promise(() => { });
|
|
1593
2080
|
}
|
|
2081
|
+
// Allow running this module directly (`npx tsx src/tui/actoviqTui.ts`), not only
|
|
2082
|
+
// via the cli/ wrapper. Requires an interactive terminal.
|
|
2083
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2084
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2085
|
+
process.stderr.write('actoviq TUI requires an interactive terminal (TTY). Run it directly in your terminal — not piped or through another tool.\n');
|
|
2086
|
+
process.exit(1);
|
|
2087
|
+
}
|
|
2088
|
+
runActoviqTui().catch((error) => {
|
|
2089
|
+
process.stderr.write(`Fatal: ${error.stack ?? error.message}\n`);
|
|
2090
|
+
process.exit(1);
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
1594
2093
|
//# sourceMappingURL=actoviqTui.js.map
|