fixo-cli 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-client.d.ts +3 -0
- package/dist/agent/agent-client.d.ts.map +1 -1
- package/dist/agent/agent-client.js +63 -9
- package/dist/agent/agent-client.js.map +1 -1
- package/dist/agent/command-parser.d.ts.map +1 -1
- package/dist/agent/command-parser.js +28 -5
- package/dist/agent/command-parser.js.map +1 -1
- package/dist/agent/providers-manager.d.ts +6 -0
- package/dist/agent/providers-manager.d.ts.map +1 -1
- package/dist/agent/providers-manager.js +20 -0
- package/dist/agent/providers-manager.js.map +1 -1
- package/dist/agent/single-agent.d.ts +12 -0
- package/dist/agent/single-agent.d.ts.map +1 -1
- package/dist/agent/single-agent.js +75 -10
- package/dist/agent/single-agent.js.map +1 -1
- package/dist/agent/tool-executor.d.ts +5 -0
- package/dist/agent/tool-executor.d.ts.map +1 -1
- package/dist/agent/tool-executor.js +78 -31
- package/dist/agent/tool-executor.js.map +1 -1
- package/dist/agent/worker-agent.d.ts +8 -0
- package/dist/agent/worker-agent.d.ts.map +1 -1
- package/dist/agent/worker-agent.js +20 -1
- package/dist/agent/worker-agent.js.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/project-memory.d.ts +1 -2
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +13 -1
- package/dist/project-memory.js.map +1 -1
- package/dist/setup-wizard.d.ts +1 -0
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +42 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/ui/prompt.d.ts.map +1 -1
- package/dist/ui/prompt.js +93 -5
- package/dist/ui/prompt.js.map +1 -1
- package/dist/ui/render-primitives.d.ts +39 -0
- package/dist/ui/render-primitives.d.ts.map +1 -1
- package/dist/ui/render-primitives.js +94 -0
- package/dist/ui/render-primitives.js.map +1 -1
- package/package.json +4 -4
|
@@ -6,7 +6,7 @@ import fs from 'fs';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
8
|
import { colors } from '../ui/colors.js';
|
|
9
|
-
import { renderToolCall } from '../ui/render-primitives.js';
|
|
9
|
+
import { renderToolCall, startInlineToolSpinner, } from '../ui/render-primitives.js';
|
|
10
10
|
import { WorkspaceGuard } from '../workspace-guard.js';
|
|
11
11
|
import { classifyCommand } from '../runtime/policy.js';
|
|
12
12
|
import { checkPermission } from './permissions.js';
|
|
@@ -884,6 +884,22 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
884
884
|
result: '',
|
|
885
885
|
isWrite: false,
|
|
886
886
|
};
|
|
887
|
+
// Check for user cancellation before starting any tool work
|
|
888
|
+
if (options.signal?.aborted) {
|
|
889
|
+
event.result = 'Error: Task cancelled by user.';
|
|
890
|
+
return event;
|
|
891
|
+
}
|
|
892
|
+
// Single inline spinner shared across the whole tool invocation —
|
|
893
|
+
// each case under the switch below calls `setSpinner` exactly once,
|
|
894
|
+
// and the outer try/finally converts the spinner into a ✔ or ✗
|
|
895
|
+
// summary line when the tool completes (or throws).
|
|
896
|
+
let inlineSpinner = null;
|
|
897
|
+
const toolStartedAt = Date.now();
|
|
898
|
+
const setSpinner = (tool) => {
|
|
899
|
+
if (inlineSpinner)
|
|
900
|
+
inlineSpinner.clear();
|
|
901
|
+
inlineSpinner = startInlineToolSpinner(tool);
|
|
902
|
+
};
|
|
887
903
|
try {
|
|
888
904
|
const policy = options.policy ?? options.session?.policy ?? 'shell-confirm';
|
|
889
905
|
// ──── PreToolUse hooks (§3.4) ────
|
|
@@ -920,6 +936,10 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
920
936
|
}
|
|
921
937
|
const plugin = loadedPlugins.find(p => p.tools.some(t => t.function.name === name));
|
|
922
938
|
if (plugin) {
|
|
939
|
+
if (options.signal?.aborted) {
|
|
940
|
+
event.result = 'Error: Task cancelled by user.';
|
|
941
|
+
return event;
|
|
942
|
+
}
|
|
923
943
|
const action = name.includes('read') || name.includes('get') || name.includes('list') || name.includes('view') ? 'read' : 'write';
|
|
924
944
|
const decision = evaluateToolGate(name, args, cwd, policy, action, name);
|
|
925
945
|
if (!decision.allowed) {
|
|
@@ -1003,8 +1023,13 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1003
1023
|
options.session?.record('tool_started', { tool: name, args, risk: decision.risk, matchedRule: decision.matchedRule, source: decision.source });
|
|
1004
1024
|
switch (name) {
|
|
1005
1025
|
case 'read_file': {
|
|
1026
|
+
if (options.signal?.aborted) {
|
|
1027
|
+
event.result = 'Error: Task cancelled by user.';
|
|
1028
|
+
return event;
|
|
1029
|
+
}
|
|
1006
1030
|
const guard = new WorkspaceGuard(cwd);
|
|
1007
1031
|
const resolved = guard.resolve(args.path, 'file');
|
|
1032
|
+
setSpinner({ kind: 'read', name: 'Read', detail: shortenPath(args.path, cwd) });
|
|
1008
1033
|
const budgetPct = options.safety?.predictiveBudgetPct ?? DEFAULT_PREDICTIVE_BUDGET_PCT;
|
|
1009
1034
|
// Skip the predictive gate when it is explicitly disabled
|
|
1010
1035
|
// (>=1.0) or when no model is configured (we cannot map
|
|
@@ -1016,7 +1041,7 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1016
1041
|
if (deferDecision.defer) {
|
|
1017
1042
|
event.result = formatPredictiveGateDirective(args.path, estimate, deferDecision);
|
|
1018
1043
|
event.affectedPath = resolved;
|
|
1019
|
-
|
|
1044
|
+
setSpinner({ kind: 'read', name: 'Read', detail: `${shortenPath(args.path, cwd)} (deferred — predictive gate)` });
|
|
1020
1045
|
options.session?.record('predictive_gate_fired', {
|
|
1021
1046
|
path: args.path,
|
|
1022
1047
|
projectedTokens: estimate.projectedTokens,
|
|
@@ -1028,27 +1053,26 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1028
1053
|
}
|
|
1029
1054
|
event.result = executeReadFile(args.path, cwd, options.session, options.safety?.largeFileGateBytes, options.safety?.largeFileGateLines);
|
|
1030
1055
|
event.affectedPath = resolved;
|
|
1031
|
-
renderToolCall({ kind: 'read', name: 'Read', detail: shortenPath(args.path, cwd) });
|
|
1032
1056
|
break;
|
|
1033
1057
|
}
|
|
1034
1058
|
case 'extract_symbols':
|
|
1059
|
+
setSpinner({ kind: 'read', name: 'Symbols', detail: shortenPath(args.path, cwd) });
|
|
1035
1060
|
event.result = await executeExtractSymbols(args.path, cwd, options.session);
|
|
1036
1061
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1037
|
-
renderToolCall({ kind: 'read', name: 'Symbols', detail: shortenPath(args.path, cwd) });
|
|
1038
1062
|
break;
|
|
1039
1063
|
case 'extract_imports':
|
|
1064
|
+
setSpinner({ kind: 'read', name: 'Imports', detail: shortenPath(args.path, cwd) });
|
|
1040
1065
|
event.result = await executeExtractImports(args.path, cwd, options.session);
|
|
1041
1066
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1042
|
-
renderToolCall({ kind: 'read', name: 'Imports', detail: shortenPath(args.path, cwd) });
|
|
1043
1067
|
break;
|
|
1044
1068
|
case 'write_file':
|
|
1069
|
+
setSpinner({ kind: 'write', name: 'Write', detail: shortenPath(args.path, cwd) });
|
|
1045
1070
|
event.result = await executeWriteFile(args.path, args.content, cwd, options);
|
|
1046
1071
|
event.isWrite = true;
|
|
1047
1072
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1048
|
-
renderToolCall({ kind: 'write', name: 'Write', detail: shortenPath(args.path, cwd) });
|
|
1049
1073
|
break;
|
|
1050
1074
|
case 'run_command':
|
|
1051
|
-
|
|
1075
|
+
setSpinner({ kind: 'bash', name: 'Run', detail: truncate(args.command, 60) });
|
|
1052
1076
|
let safetyResult = { safe: true, reason: '' };
|
|
1053
1077
|
try {
|
|
1054
1078
|
const { isCommandSafe } = await import('./command-parser.js');
|
|
@@ -1083,59 +1107,59 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1083
1107
|
event.result = executeRunCommand(args.command, args.cwd || cwd, cwd, options.session);
|
|
1084
1108
|
break;
|
|
1085
1109
|
case 'search_code':
|
|
1086
|
-
|
|
1110
|
+
setSpinner({ kind: 'search', name: 'Search', detail: `"${truncate(args.query, 40)}" in ${args.path ?? '.'}` });
|
|
1087
1111
|
event.result = executeSearchCode(args.query, args.path, args.file_pattern, cwd);
|
|
1088
1112
|
break;
|
|
1089
1113
|
case 'list_dir':
|
|
1090
|
-
|
|
1114
|
+
setSpinner({ kind: 'read', name: 'List', detail: args.path ?? '.' });
|
|
1091
1115
|
event.result = executeListDir(args.path, cwd);
|
|
1092
1116
|
break;
|
|
1093
1117
|
case 'delete_file':
|
|
1094
|
-
|
|
1118
|
+
setSpinner({ kind: 'write', name: 'Delete', detail: shortenPath(args.path, cwd) });
|
|
1095
1119
|
event.result = executeDeleteFile(args.path, cwd, options.session);
|
|
1096
1120
|
event.isWrite = true;
|
|
1097
1121
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1098
1122
|
break;
|
|
1099
1123
|
case 'apply_patch':
|
|
1100
|
-
|
|
1124
|
+
setSpinner({ kind: 'write', name: 'Patch', detail: 'unified diff' });
|
|
1101
1125
|
event.result = await executeApplyPatch(args.patch, cwd, options);
|
|
1102
1126
|
event.isWrite = true;
|
|
1103
1127
|
break;
|
|
1104
1128
|
case 'replace_range':
|
|
1105
|
-
|
|
1129
|
+
setSpinner({ kind: 'write', name: 'Replace', detail: shortenPath(args.path, cwd) });
|
|
1106
1130
|
event.result = await executeReplaceRange(args.path, Number(args.startLine), Number(args.endLine), args.content, cwd, options);
|
|
1107
1131
|
event.isWrite = true;
|
|
1108
1132
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1109
1133
|
break;
|
|
1110
1134
|
case 'insert_after':
|
|
1111
|
-
|
|
1135
|
+
setSpinner({ kind: 'write', name: 'Insert', detail: shortenPath(args.path, cwd) });
|
|
1112
1136
|
event.result = await executeInsertAfter(args.path, args.anchor, args.content, cwd, options);
|
|
1113
1137
|
event.isWrite = true;
|
|
1114
1138
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1115
1139
|
break;
|
|
1116
1140
|
case 'rename_file':
|
|
1117
|
-
|
|
1141
|
+
setSpinner({ kind: 'write', name: 'Rename', detail: `${args.from} -> ${args.to}` });
|
|
1118
1142
|
event.result = await executeRenameFile(args.from, args.to, cwd, options);
|
|
1119
1143
|
event.isWrite = true;
|
|
1120
1144
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.to, 'file');
|
|
1121
1145
|
break;
|
|
1122
1146
|
case 'create_branch':
|
|
1123
|
-
|
|
1147
|
+
setSpinner({ kind: 'write', name: 'Branch', detail: args.branchName });
|
|
1124
1148
|
event.result = createBranch(cwd, args.branchName);
|
|
1125
1149
|
event.isWrite = true;
|
|
1126
1150
|
break;
|
|
1127
1151
|
case 'commit_changes':
|
|
1128
|
-
|
|
1152
|
+
setSpinner({ kind: 'write', name: 'Commit', detail: truncate(args.message, 60) });
|
|
1129
1153
|
event.result = commitChanges(cwd, args.message);
|
|
1130
1154
|
event.isWrite = true;
|
|
1131
1155
|
break;
|
|
1132
1156
|
case 'push_branch':
|
|
1133
|
-
|
|
1157
|
+
setSpinner({ kind: 'write', name: 'Push', detail: args.remote || 'origin' });
|
|
1134
1158
|
event.result = pushBranch(cwd, args.remote || 'origin');
|
|
1135
1159
|
event.isWrite = true;
|
|
1136
1160
|
break;
|
|
1137
1161
|
case 'create_pull_request':
|
|
1138
|
-
|
|
1162
|
+
setSpinner({ kind: 'write', name: 'PR', detail: `base: ${args.baseBranch || 'main'}` });
|
|
1139
1163
|
if (!options.client) {
|
|
1140
1164
|
throw new Error('Agent client is required to generate pull request description');
|
|
1141
1165
|
}
|
|
@@ -1146,7 +1170,7 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1146
1170
|
const line = Number(args.line);
|
|
1147
1171
|
const char = Number(args.character);
|
|
1148
1172
|
const fileBasename = path.basename(args.path);
|
|
1149
|
-
|
|
1173
|
+
setSpinner({ kind: 'read', name: 'Definition', detail: `${fileBasename}:${line}:${char}` });
|
|
1150
1174
|
const manager = getLspManager(cwd);
|
|
1151
1175
|
const resolvedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1152
1176
|
const def = await manager.gotoDefinition(resolvedPath, line, char);
|
|
@@ -1157,7 +1181,7 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1157
1181
|
const line = Number(args.line);
|
|
1158
1182
|
const char = Number(args.character);
|
|
1159
1183
|
const fileBasename = path.basename(args.path);
|
|
1160
|
-
|
|
1184
|
+
setSpinner({ kind: 'read', name: 'References', detail: `${fileBasename}:${line}:${char}` });
|
|
1161
1185
|
const manager = getLspManager(cwd);
|
|
1162
1186
|
const resolvedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1163
1187
|
const refs = await manager.findReferences(resolvedPath, line, char);
|
|
@@ -1168,7 +1192,7 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1168
1192
|
const line = Number(args.line);
|
|
1169
1193
|
const char = Number(args.character);
|
|
1170
1194
|
const fileBasename = path.basename(args.path);
|
|
1171
|
-
|
|
1195
|
+
setSpinner({ kind: 'read', name: 'Hover', detail: `${fileBasename}:${line}:${char}` });
|
|
1172
1196
|
const manager = getLspManager(cwd);
|
|
1173
1197
|
const resolvedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1174
1198
|
const hoverRes = await manager.hover(resolvedPath, line, char);
|
|
@@ -1176,42 +1200,42 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1176
1200
|
break;
|
|
1177
1201
|
}
|
|
1178
1202
|
case 'web_fetch':
|
|
1179
|
-
|
|
1203
|
+
setSpinner({ kind: 'search', name: 'Fetch', detail: args.url });
|
|
1180
1204
|
event.result = await webFetch(args.url);
|
|
1181
1205
|
break;
|
|
1182
1206
|
case 'web_search':
|
|
1183
|
-
|
|
1207
|
+
setSpinner({ kind: 'search', name: 'Search', detail: truncate(args.query, 40) });
|
|
1184
1208
|
event.result = await webSearch(args.query);
|
|
1185
1209
|
break;
|
|
1186
1210
|
case 'str_replace':
|
|
1187
|
-
|
|
1211
|
+
setSpinner({ kind: 'write', name: 'Surgical', detail: shortenPath(args.path, cwd) });
|
|
1188
1212
|
event.result = await executeStrReplace(args, cwd, options);
|
|
1189
1213
|
event.isWrite = true;
|
|
1190
1214
|
event.affectedPath = new WorkspaceGuard(cwd).resolve(args.path, 'file');
|
|
1191
1215
|
break;
|
|
1192
1216
|
case 'glob_files':
|
|
1193
|
-
|
|
1217
|
+
setSpinner({ kind: 'search', name: 'Glob', detail: truncate(args.pattern, 60) });
|
|
1194
1218
|
event.result = await executeGlobFiles(args, cwd, options);
|
|
1195
1219
|
break;
|
|
1196
1220
|
case 'todo_read':
|
|
1197
|
-
|
|
1221
|
+
setSpinner({ kind: 'search', name: 'Todo', detail: 'read' });
|
|
1198
1222
|
event.result = executeTodoRead(cwd);
|
|
1199
1223
|
break;
|
|
1200
1224
|
case 'todo_write':
|
|
1201
|
-
|
|
1225
|
+
setSpinner({ kind: 'write', name: 'Todo', detail: String(args.op ?? 'add') });
|
|
1202
1226
|
event.result = await executeTodoWrite(args, cwd, options);
|
|
1203
1227
|
event.isWrite = true;
|
|
1204
1228
|
break;
|
|
1205
1229
|
case 'run_command_async':
|
|
1206
|
-
|
|
1230
|
+
setSpinner({ kind: 'bash', name: 'Async', detail: truncate(args.cmd, 30) });
|
|
1207
1231
|
event.result = await executeRunCommandAsync(args, cwd, options);
|
|
1208
1232
|
break;
|
|
1209
1233
|
case 'poll_command_status':
|
|
1210
|
-
|
|
1234
|
+
setSpinner({ kind: 'bash', name: 'Poll', detail: String(args.jobId ?? '?') });
|
|
1211
1235
|
event.result = executePollCommandStatus(args, cwd);
|
|
1212
1236
|
break;
|
|
1213
1237
|
case 'kill_command':
|
|
1214
|
-
|
|
1238
|
+
setSpinner({ kind: 'bash', name: 'Kill', detail: String(args.jobId ?? '?') });
|
|
1215
1239
|
event.result = executeKillCommand(args, cwd);
|
|
1216
1240
|
break;
|
|
1217
1241
|
default:
|
|
@@ -1221,7 +1245,30 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1221
1245
|
catch (error) {
|
|
1222
1246
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1223
1247
|
event.result = `Error: ${msg}`;
|
|
1224
|
-
|
|
1248
|
+
if (inlineSpinner) {
|
|
1249
|
+
inlineSpinner.fail(truncate(msg, 80));
|
|
1250
|
+
inlineSpinner = null;
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
renderToolCall({ kind: 'error', name, detail: truncate(msg, 80) });
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
// Finalise the inline spinner — convert it to a ✔ or ✗ summary line
|
|
1257
|
+
// based on whether the tool result starts with `Error:`. The brief
|
|
1258
|
+
// summary is the original detail plus an elapsed-time suffix so the
|
|
1259
|
+
// user sees roughly how long the call took.
|
|
1260
|
+
if (inlineSpinner) {
|
|
1261
|
+
const handle = inlineSpinner;
|
|
1262
|
+
const elapsedMs = Date.now() - toolStartedAt;
|
|
1263
|
+
const elapsedStr = elapsedMs < 1000 ? `${elapsedMs}ms` : `${(elapsedMs / 1000).toFixed(1)}s`;
|
|
1264
|
+
const failed = typeof event.result === 'string' && event.result.startsWith('Error:');
|
|
1265
|
+
if (failed) {
|
|
1266
|
+
handle.fail(`${truncate(event.result.replace(/^Error:\s*/, ''), 60)} (${elapsedStr})`);
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
handle.succeed(`done (${elapsedStr})`);
|
|
1270
|
+
}
|
|
1271
|
+
inlineSpinner = null;
|
|
1225
1272
|
}
|
|
1226
1273
|
options.session?.record('tool_finished', { tool: name, result: truncate(event.result, 2000), isWrite: event.isWrite });
|
|
1227
1274
|
// ──── PostToolUse hooks (§3.4) ────
|