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.
Files changed (43) hide show
  1. package/dist/agent/agent-client.d.ts +3 -0
  2. package/dist/agent/agent-client.d.ts.map +1 -1
  3. package/dist/agent/agent-client.js +63 -9
  4. package/dist/agent/agent-client.js.map +1 -1
  5. package/dist/agent/command-parser.d.ts.map +1 -1
  6. package/dist/agent/command-parser.js +28 -5
  7. package/dist/agent/command-parser.js.map +1 -1
  8. package/dist/agent/providers-manager.d.ts +6 -0
  9. package/dist/agent/providers-manager.d.ts.map +1 -1
  10. package/dist/agent/providers-manager.js +20 -0
  11. package/dist/agent/providers-manager.js.map +1 -1
  12. package/dist/agent/single-agent.d.ts +12 -0
  13. package/dist/agent/single-agent.d.ts.map +1 -1
  14. package/dist/agent/single-agent.js +75 -10
  15. package/dist/agent/single-agent.js.map +1 -1
  16. package/dist/agent/tool-executor.d.ts +5 -0
  17. package/dist/agent/tool-executor.d.ts.map +1 -1
  18. package/dist/agent/tool-executor.js +78 -31
  19. package/dist/agent/tool-executor.js.map +1 -1
  20. package/dist/agent/worker-agent.d.ts +8 -0
  21. package/dist/agent/worker-agent.d.ts.map +1 -1
  22. package/dist/agent/worker-agent.js +20 -1
  23. package/dist/agent/worker-agent.js.map +1 -1
  24. package/dist/config.d.ts +18 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +10 -0
  27. package/dist/config.js.map +1 -1
  28. package/dist/project-memory.d.ts +1 -2
  29. package/dist/project-memory.d.ts.map +1 -1
  30. package/dist/project-memory.js +13 -1
  31. package/dist/project-memory.js.map +1 -1
  32. package/dist/setup-wizard.d.ts +1 -0
  33. package/dist/setup-wizard.d.ts.map +1 -1
  34. package/dist/setup-wizard.js +42 -1
  35. package/dist/setup-wizard.js.map +1 -1
  36. package/dist/ui/prompt.d.ts.map +1 -1
  37. package/dist/ui/prompt.js +93 -5
  38. package/dist/ui/prompt.js.map +1 -1
  39. package/dist/ui/render-primitives.d.ts +39 -0
  40. package/dist/ui/render-primitives.d.ts.map +1 -1
  41. package/dist/ui/render-primitives.js +94 -0
  42. package/dist/ui/render-primitives.js.map +1 -1
  43. 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
- renderToolCall({ kind: 'read', name: 'Read', detail: `${shortenPath(args.path, cwd)} (deferred — predictive gate)` });
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
- renderToolCall({ kind: 'bash', name: 'Run', detail: truncate(args.command, 60) });
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
- renderToolCall({ kind: 'search', name: 'Search', detail: `"${truncate(args.query, 40)}" in ${args.path ?? '.'}` });
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
- renderToolCall({ kind: 'read', name: 'List', detail: args.path ?? '.' });
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
- renderToolCall({ kind: 'write', name: 'Delete', detail: shortenPath(args.path, cwd) });
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
- renderToolCall({ kind: 'write', name: 'Patch', detail: 'unified diff' });
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
- renderToolCall({ kind: 'write', name: 'Replace', detail: shortenPath(args.path, cwd) });
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
- renderToolCall({ kind: 'write', name: 'Insert', detail: shortenPath(args.path, cwd) });
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
- renderToolCall({ kind: 'write', name: 'Rename', detail: `${args.from} -> ${args.to}` });
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
- renderToolCall({ kind: 'write', name: 'Branch', detail: args.branchName });
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
- renderToolCall({ kind: 'write', name: 'Commit', detail: truncate(args.message, 60) });
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
- renderToolCall({ kind: 'write', name: 'Push', detail: args.remote || 'origin' });
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
- renderToolCall({ kind: 'write', name: 'PR', detail: `base: ${args.baseBranch || 'main'}` });
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
- renderToolCall({ kind: 'read', name: 'Definition', detail: `${fileBasename}:${line}:${char}` });
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
- renderToolCall({ kind: 'read', name: 'References', detail: `${fileBasename}:${line}:${char}` });
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
- renderToolCall({ kind: 'read', name: 'Hover', detail: `${fileBasename}:${line}:${char}` });
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
- renderToolCall({ kind: 'search', name: 'Fetch', detail: args.url });
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
- renderToolCall({ kind: 'search', name: 'Search', detail: truncate(args.query, 40) });
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
- renderToolCall({ kind: 'write', name: 'Surgical', detail: shortenPath(args.path, cwd) });
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
- renderToolCall({ kind: 'search', name: 'Glob', detail: truncate(args.pattern, 60) });
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
- renderToolCall({ kind: 'search', name: 'Todo', detail: 'read' });
1221
+ setSpinner({ kind: 'search', name: 'Todo', detail: 'read' });
1198
1222
  event.result = executeTodoRead(cwd);
1199
1223
  break;
1200
1224
  case 'todo_write':
1201
- renderToolCall({ kind: 'write', name: 'Todo', detail: String(args.op ?? 'add') });
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
- renderToolCall({ kind: 'bash', name: 'Async', detail: truncate(args.cmd, 30) });
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
- renderToolCall({ kind: 'bash', name: 'Poll', detail: String(args.jobId ?? '?') });
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
- renderToolCall({ kind: 'bash', name: 'Kill', detail: String(args.jobId ?? '?') });
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
- renderToolCall({ kind: 'error', name, detail: truncate(msg, 80) });
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) ────