vigthoria-cli 1.11.5 → 1.11.7

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.
@@ -63,6 +63,8 @@ export declare class ChatCommand {
63
63
  private getDefaultChatModel;
64
64
  private resolveInitialModel;
65
65
  private isLegacyAgentFallbackAllowed;
66
+ private shouldRescueFailedAnalysis;
67
+ private tryAnalysisLocalRescue;
66
68
  private resolveAgentExecutionPolicy;
67
69
  private getMessagesForModel;
68
70
  private getActivePersonaMode;
@@ -115,6 +117,7 @@ export declare class ChatCommand {
115
117
  private consumeV3StreamPayload;
116
118
  private writeV3StreamText;
117
119
  private sanitizeV3VisibleStreamText;
120
+ private writeAgentActivityLine;
118
121
  private updateV3AgentSpinner;
119
122
  private updateOperatorSpinner;
120
123
  constructor(config: Config, logger: Logger);
@@ -13,7 +13,7 @@ import { TaskDisplay } from '../utils/task-display.js';
13
13
  import { ProjectMemoryService } from '../utils/project-memory.js';
14
14
  import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
15
15
  import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
16
- import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
16
+ import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
17
17
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
18
18
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
19
19
  if (!rawValue) {
@@ -259,6 +259,28 @@ export class ChatCommand {
259
259
  // Operators can still opt in during emergency local debugging.
260
260
  return process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '1';
261
261
  }
262
+ shouldRescueFailedAnalysis() {
263
+ return process.env.VIGTHORIA_DISABLE_ANALYSIS_RESCUE !== '1';
264
+ }
265
+ async tryAnalysisLocalRescue(prompt, workspacePath) {
266
+ if (!this.shouldRescueFailedAnalysis() || !this.tools) {
267
+ return false;
268
+ }
269
+ const clientToolErrors = this.api.getClientToolErrors();
270
+ if (clientToolErrors.some((err) => /could not accept the result|client tool bridge failed/i.test(err))) {
271
+ return false;
272
+ }
273
+ console.log('');
274
+ console.log(chalk.yellow('V3 analysis did not produce a grounded answer — retrying with local workspace tools...'));
275
+ console.log(chalk.gray(`Workspace: ${workspacePath}`));
276
+ console.log('');
277
+ const last = this.messages[this.messages.length - 1];
278
+ if (last?.role === 'assistant') {
279
+ this.messages.pop();
280
+ }
281
+ await this.runLocalAgentLoop(prompt);
282
+ return true;
283
+ }
262
284
  resolveAgentExecutionPolicy(prompt) {
263
285
  const explicitModel = this.modelExplicitlySelected;
264
286
  const heavyTask = this.config.isComplexTask(prompt);
@@ -765,6 +787,11 @@ export class ChatCommand {
765
787
  .replace(/\n{3,}/g, '\n\n');
766
788
  return output;
767
789
  }
790
+ writeAgentActivityLine(text) {
791
+ if (this.jsonOutput)
792
+ return;
793
+ process.stdout.write(text);
794
+ }
768
795
  updateV3AgentSpinner(spinner, event) {
769
796
  if (this.isRawV3StreamPayload(event)) {
770
797
  this.consumeV3StreamPayload(spinner, event).catch((error) => {
@@ -785,20 +812,23 @@ export class ChatCommand {
785
812
  const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
786
813
  const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
787
814
  const shortTarget = sanitizedTarget ? ` → ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
788
- const stepLabel = chalk.cyan(` [${this.v3IterationCount}/${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
815
+ const stepLabel = chalk.cyan(` [${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
789
816
  if (spinner.isSpinning)
790
817
  spinner.stop();
791
- process.stderr.write(stepLabel + '\n');
818
+ this.writeAgentActivityLine(stepLabel + '\n');
792
819
  // Show extra detail for key tools
793
820
  const args = event.arguments || {};
794
821
  const toolName = event.name || event.tool || '';
795
822
  if ((toolName === 'write_file' || toolName === 'edit_file') && typeof args.content === 'string') {
796
823
  const len = args.content.length;
797
- process.stderr.write(chalk.gray(` ${len > 1000 ? Math.round(len / 1024) + ' KB' : len + ' bytes'} content\n`));
824
+ this.writeAgentActivityLine(chalk.gray(` ${len > 1000 ? Math.round(len / 1024) + ' KB' : len + ' bytes'} content\n`));
798
825
  }
799
826
  else if (toolName === 'bash' && typeof args.command === 'string') {
800
827
  const command = this.sanitizeServerPath(args.command);
801
- process.stderr.write(chalk.gray(` $ ${command.slice(0, 120)}${command.length > 120 ? '…' : ''}\n`));
828
+ this.writeAgentActivityLine(chalk.gray(` $ ${command.slice(0, 120)}${command.length > 120 ? '…' : ''}\n`));
829
+ }
830
+ else if (toolName === 'read_file') {
831
+ this.writeAgentActivityLine(chalk.gray(` reading ${sanitizedTarget || 'file'}\n`));
802
832
  }
803
833
  spinner.start();
804
834
  spinner.text = `Running ${toolDesc}...`;
@@ -810,18 +840,28 @@ export class ChatCommand {
810
840
  const indicator = success ? chalk.green(' ✓') : chalk.red(' ✗');
811
841
  if (spinner.isSpinning)
812
842
  spinner.stop();
813
- process.stderr.write(`${indicator} ${toolName}\n`);
843
+ this.writeAgentActivityLine(`${indicator} ${toolName}\n`);
814
844
  // Show output for failures, or brief summary for successes — sanitize server paths
815
845
  const rawOutput = typeof event.output === 'string' ? event.output.trim() : '';
816
846
  const output = this.sanitizeServerPath(rawOutput);
817
847
  if (!success && output) {
818
848
  const sanitizedError = this.sanitizeServerPath(typeof event.error === 'string' ? event.error : output);
819
849
  const lines = sanitizedError.split('\n').slice(0, 4);
820
- process.stderr.write(chalk.red(` ${lines.join('\n ')}\n`));
850
+ this.writeAgentActivityLine(chalk.red(` ${lines.join('\n ')}\n`));
821
851
  }
822
852
  else if (success && output && output.length > 0) {
823
- const brief = output.split('\n')[0].slice(0, 120);
824
- process.stderr.write(chalk.gray(` ${brief}${output.length > 120 ? '…' : ''}\n`));
853
+ if (toolName === 'read_file' || toolName === 'grep' || toolName === 'search_files' || toolName === 'list_directory') {
854
+ const lineCount = output.split('\n').length;
855
+ const preview = output.split('\n').slice(0, 3).join('\n');
856
+ this.writeAgentActivityLine(chalk.gray(` ${lineCount} line${lineCount === 1 ? '' : 's'} returned\n`));
857
+ if (preview && preview.length > 20 && !/^\(no matches\)$/i.test(preview.trim())) {
858
+ this.writeAgentActivityLine(chalk.gray(`${preview.split('\n').map((l) => ` ${l}`).join('\n')}\n`));
859
+ }
860
+ }
861
+ else {
862
+ const brief = output.split('\n')[0].slice(0, 120);
863
+ this.writeAgentActivityLine(chalk.gray(` ${brief}${output.length > 120 ? '…' : ''}\n`));
864
+ }
825
865
  }
826
866
  spinner.start();
827
867
  spinner.text = 'Next step...';
@@ -836,7 +876,10 @@ export class ChatCommand {
836
876
  const iterText = this.sanitizeServerPath(event.content || '');
837
877
  if (spinner.isSpinning)
838
878
  spinner.stop();
839
- process.stderr.write(chalk.cyan(`\n── ${iterText || `Iteration ${this.v3IterationCount}`} ──\n`));
879
+ const display = iterText && !/read-only analysis turn|run at least one discovery tool/i.test(iterText)
880
+ ? iterText
881
+ : `Step ${this.v3IterationCount}: inspecting workspace…`;
882
+ this.writeAgentActivityLine(chalk.cyan(`\n── ${display} ──\n`));
840
883
  spinner.start();
841
884
  spinner.text = 'Analyzing...';
842
885
  return;
@@ -2683,39 +2726,6 @@ export class ChatCommand {
2683
2726
  if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
2684
2727
  console.log(chalk.yellow(`Template Service preview gate did not fully validate this output, but generated workspace files were preserved${previewGate?.error ? `: ${previewGate.error}` : '.'}`));
2685
2728
  }
2686
- if (this.v3StreamingStarted) {
2687
- // Content was already streamed to stdout in real-time; skip duplicate print.
2688
- if (!this.jsonOutput) {
2689
- console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
2690
- }
2691
- }
2692
- else if (answerContent) {
2693
- if (!this.directPromptMode) {
2694
- console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
2695
- }
2696
- console.log('');
2697
- console.log(chalk.bold('Answer:'));
2698
- console.log(answerContent);
2699
- }
2700
- else if (response.content) {
2701
- if (!this.directPromptMode) {
2702
- console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
2703
- }
2704
- console.log(response.content);
2705
- }
2706
- else if (!requiresWorkspaceChanges) {
2707
- if (!this.directPromptMode) {
2708
- console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
2709
- }
2710
- console.log(chalk.yellow('The agent finished without producing an answer to your question.'));
2711
- console.log(chalk.gray('Try /retry or ask again with more detail (e.g. file name or feature).'));
2712
- }
2713
- else {
2714
- if (!this.directPromptMode) {
2715
- console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
2716
- }
2717
- console.log('V3 agent workflow completed.');
2718
- }
2719
2729
  if (!this.jsonOutput && previewGate?.required) {
2720
2730
  if (previewGate.passed) {
2721
2731
  console.log(chalk.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
@@ -2743,8 +2753,43 @@ export class ChatCommand {
2743
2753
  // executor errors and the spinner needs to clearly say "failed", not "✓".
2744
2754
  let selfHealStatus = null;
2745
2755
  let selfHealTool = null;
2756
+ if (isToolEvidenceStubAnswer(answerContent)) {
2757
+ liveOutcome.answerContent = '';
2758
+ }
2746
2759
  const runEvaluation = evaluateExecutorSuccess(liveOutcome);
2747
- const executorSucceeded = runEvaluation.executorSucceeded;
2760
+ let executorSucceeded = runEvaluation.executorSucceeded;
2761
+ let finalAnswer = answerContent;
2762
+ const needsAnalysisRescue = !requiresWorkspaceChanges && !this.jsonOutput
2763
+ && (!executorSucceeded || !isSubstantiveAgentAnswer(finalAnswer));
2764
+ if (needsAnalysisRescue) {
2765
+ const rescued = await this.tryAnalysisLocalRescue(prompt, workspacePath);
2766
+ if (rescued) {
2767
+ watcher?.stop();
2768
+ return true;
2769
+ }
2770
+ executorSucceeded = false;
2771
+ }
2772
+ if (!this.jsonOutput && !this.directPromptMode) {
2773
+ if (this.v3StreamingStarted && isSubstantiveAgentAnswer(this.v3StreamedAnswerBuffer)) {
2774
+ console.log('');
2775
+ console.log(chalk.bold('Answer:'));
2776
+ console.log(this.v3StreamedAnswerBuffer.trim());
2777
+ }
2778
+ else if (isSubstantiveAgentAnswer(finalAnswer)) {
2779
+ console.log('');
2780
+ console.log(chalk.bold('Answer:'));
2781
+ console.log(finalAnswer);
2782
+ }
2783
+ else if (!requiresWorkspaceChanges) {
2784
+ console.log('');
2785
+ console.log(chalk.yellow('No written analysis was produced for your question.'));
2786
+ console.log(chalk.gray('The agent inspected files but did not synthesize findings. Try /retry or rephrase (e.g. "Summarize architecture of index.html and game.js").'));
2787
+ }
2788
+ else if (finalAnswer) {
2789
+ console.log('');
2790
+ console.log(finalAnswer);
2791
+ }
2792
+ }
2748
2793
  if (!executorSucceeded && requiresWorkspaceChanges && changedFileCount === 0 && !liveOutcome.executorError) {
2749
2794
  const clientToolErrors = this.api.getClientToolErrors();
2750
2795
  const transportErrors = this.api.getLastChatTransportErrors();
@@ -2839,7 +2884,7 @@ export class ChatCommand {
2839
2884
  clientToolErrors: this.api.getClientToolErrors(),
2840
2885
  transportErrors: this.api.getLastChatTransportErrors(),
2841
2886
  workspacePath: workspacePath || null,
2842
- workspaceSyncIssue: (!executorSucceeded && workspaceHasOutput && changedFileCount === 0)
2887
+ workspaceSyncIssue: (!executorSucceeded && requiresWorkspaceChanges && workspaceHasOutput && changedFileCount === 0)
2843
2888
  ? 'Local workspace has files but the agent did not confirm tracked changes.'
2844
2889
  : null,
2845
2890
  finishedAt: Date.now(),
@@ -3352,7 +3397,7 @@ export class ChatCommand {
3352
3397
  : `${outcome.tasksSucceeded} task update${outcome.tasksSucceeded === 1 ? '' : 's'} reported`;
3353
3398
  console.log(' ' + chalk.white(taskLine + '.'));
3354
3399
  }
3355
- else if (outcome.answerContent) {
3400
+ else if (outcome.answerContent && isSubstantiveAgentAnswer(outcome.answerContent)) {
3356
3401
  const preview = outcome.answerContent.length > 160
3357
3402
  ? `${outcome.answerContent.slice(0, 160)}…`
3358
3403
  : outcome.answerContent;
@@ -31,6 +31,8 @@ export interface RunEvaluation {
31
31
  uiTheme: 'success' | 'warning' | 'error';
32
32
  }
33
33
  export declare function createLiveOutcome(): LiveOutcome;
34
+ /** Tool-trace stubs like `[read_file] "index.html"` — not a user-facing answer. */
35
+ export declare function isToolEvidenceStubAnswer(text: string): boolean;
34
36
  /** True when text looks like a real answer, not an executor/system placeholder. */
35
37
  export declare function isSubstantiveAgentAnswer(text: string): boolean;
36
38
  /** Pick the first substantive answer from response body and/or streamed text. */
@@ -27,15 +27,37 @@ const LIST_ONLY_DISCOVERY_TOOLS = new Set([
27
27
  'glob',
28
28
  'dir',
29
29
  ]);
30
+ const GENERIC_SUMMARY_RE = /^(completed the requested analysis|reviewed the workspace without writing changes|task completed|done|finished|analysis completed)([.\s]|$)/i;
31
+ /** Tool-trace stubs like `[read_file] "index.html"` — not a user-facing answer. */
32
+ export function isToolEvidenceStubAnswer(text) {
33
+ const trimmed = String(text || '').trim();
34
+ if (!trimmed)
35
+ return true;
36
+ if (GENERIC_SUMMARY_RE.test(trimmed.replace(/\.\s*\d+\s+tool steps.*$/i, '').trim()))
37
+ return true;
38
+ const lines = trimmed.split('\n').map((line) => line.trim()).filter(Boolean);
39
+ if (lines.length === 0)
40
+ return true;
41
+ const stubLine = /^\[(?:read_file|grep|list_directory|search_files|glob|tool_call)\]/i;
42
+ const filesOnly = /^Files inspected:/i;
43
+ const stubLines = lines.filter((line) => stubLine.test(line) || filesOnly.test(line)).length;
44
+ if (stubLines === lines.length)
45
+ return true;
46
+ if (stubLines > 0 && stubLines / lines.length >= 0.6 && trimmed.length < 400)
47
+ return true;
48
+ return false;
49
+ }
30
50
  /** True when text looks like a real answer, not an executor/system placeholder. */
31
51
  export function isSubstantiveAgentAnswer(text) {
32
52
  const trimmed = String(text || '').trim();
33
- if (trimmed.length < 30)
53
+ if (trimmed.length < 80)
34
54
  return false;
35
55
  if (EXECUTOR_PLACEHOLDER_RE.test(trimmed))
36
56
  return false;
37
57
  if (/^v3 agent workflow completed\.?$/i.test(trimmed))
38
58
  return false;
59
+ if (isToolEvidenceStubAnswer(trimmed))
60
+ return false;
39
61
  return true;
40
62
  }
41
63
  /** Pick the first substantive answer from response body and/or streamed text. */
package/dist/utils/api.js CHANGED
@@ -11,7 +11,7 @@ import path from 'path';
11
11
  import WebSocket from 'ws';
12
12
  import { buildSemanticContext } from './context-ranker.js';
13
13
  import { getChangedFiles } from './workspace-cache.js';
14
- import { isSubstantiveAgentAnswer } from './agentRunOutcome.js';
14
+ import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
15
15
  export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
16
16
  export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
17
17
  export class CLIError extends Error {
@@ -2745,9 +2745,10 @@ menu {
2745
2745
  .map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
2746
2746
  return { success: true, output: entries.join('\n') || '(empty)' };
2747
2747
  }
2748
- if (name === 'glob' || name === 'search_files') {
2748
+ if (name === 'glob' || name === 'search_files' || name === 'grep') {
2749
2749
  const pattern = String(args.pattern || args.query || '').toLowerCase();
2750
2750
  const files = [];
2751
+ const matches = [];
2751
2752
  const walk = (dir) => {
2752
2753
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2753
2754
  if (['node_modules', '.git'].includes(entry.name))
@@ -2758,18 +2759,30 @@ menu {
2758
2759
  continue;
2759
2760
  }
2760
2761
  const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
2761
- if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
2762
- files.push(relative);
2763
- }
2764
- else if (name === 'search_files') {
2765
- const content = fs.readFileSync(absolute, 'utf8');
2766
- if (content.toLowerCase().includes(pattern))
2762
+ if (name === 'glob' && pattern) {
2763
+ if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
2767
2764
  files.push(relative);
2765
+ }
2766
+ }
2767
+ else if (pattern) {
2768
+ try {
2769
+ const content = fs.readFileSync(absolute, 'utf8');
2770
+ if (relative.toLowerCase().includes(pattern.replace(/\*/g, '')) || content.toLowerCase().includes(pattern)) {
2771
+ matches.push(`${relative}: ${content.split('\n').find((line) => line.toLowerCase().includes(pattern))?.trim() || '(match)'}`);
2772
+ }
2773
+ }
2774
+ catch {
2775
+ // skip unreadable/binary files
2776
+ }
2768
2777
  }
2769
2778
  }
2770
2779
  };
2771
- walk(fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath);
2772
- return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
2780
+ const searchRoot = fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath;
2781
+ walk(searchRoot);
2782
+ if (name === 'glob') {
2783
+ return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
2784
+ }
2785
+ return { success: true, output: matches.slice(0, 100).join('\n') || '(no matches)' };
2773
2786
  }
2774
2787
  if (name === 'syntax_check') {
2775
2788
  const content = fs.readFileSync(target.absolutePath, 'utf8');
@@ -2805,8 +2818,27 @@ menu {
2805
2818
  if (!contextId || !callId)
2806
2819
  return;
2807
2820
  const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
2821
+ const emitStream = (streamEvent) => {
2822
+ if (typeof context.onStreamEvent === 'function') {
2823
+ try {
2824
+ context.onStreamEvent(streamEvent);
2825
+ }
2826
+ catch {
2827
+ // UI callbacks must never break the client tool bridge.
2828
+ }
2829
+ }
2830
+ };
2831
+ emitStream({ type: 'tool_call', name: toolName, tool: toolName, arguments: event.arguments || {} });
2808
2832
  try {
2809
2833
  const result = await this.executeV3ClientToolRequest(event, context);
2834
+ emitStream({
2835
+ type: 'tool_result',
2836
+ name: toolName,
2837
+ tool: toolName,
2838
+ success: result.success,
2839
+ output: result.output,
2840
+ error: result.error || '',
2841
+ });
2810
2842
  try {
2811
2843
  await this.submitClientToolResult(contextId, callId, result, backendUrl);
2812
2844
  }
@@ -3370,22 +3402,45 @@ document.addEventListener('DOMContentLoaded', () => {
3370
3402
  synthesizeAnswerFromV3Events(events) {
3371
3403
  const toolResults = [];
3372
3404
  const filesRead = [];
3405
+ const readExcerpts = [];
3373
3406
  const filesWritten = [];
3374
3407
  const assistantFragments = [];
3408
+ const pendingReadPaths = new Map();
3409
+ let readIndex = 0;
3375
3410
  for (const event of events) {
3376
3411
  if (!event)
3377
3412
  continue;
3413
+ if (event.type === 'tool_call') {
3414
+ const name = String(event.name || event.tool || '').trim();
3415
+ if (name === 'read_file') {
3416
+ const target = event.arguments?.path || event.arguments?.file_path || event.target;
3417
+ if (target)
3418
+ pendingReadPaths.set(readIndex++, String(target));
3419
+ }
3420
+ }
3378
3421
  if (event.type === 'tool_result' && event.success && typeof event.output === 'string') {
3379
- const name = event.name || 'unknown_tool';
3380
- if (name === 'read_file' && typeof event.target === 'string') {
3381
- filesRead.push(sanitizeUserFacingPathText(event.target));
3422
+ const name = String(event.name || event.tool || 'unknown_tool').trim();
3423
+ const output = event.output.trim();
3424
+ if (name === 'read_file') {
3425
+ const target = typeof event.target === 'string'
3426
+ ? event.target
3427
+ : (event.arguments?.path || event.arguments?.file_path || filesRead[filesRead.length] || 'file');
3428
+ if (target)
3429
+ filesRead.push(sanitizeUserFacingPathText(String(target)));
3430
+ if (output.length > 120 && !/^"[^"]+"$/.test(output)) {
3431
+ const header = sanitizeUserFacingPathText(String(target || 'file'));
3432
+ const body = sanitizeUserFacingPathText(output.slice(0, 2500));
3433
+ readExcerpts.push(`${header}\n${body}${output.length > 2500 ? '\n…' : ''}`);
3434
+ }
3435
+ else if (output.length > 0) {
3436
+ toolResults.push(`[${name}] ${sanitizeUserFacingPathText(output.slice(0, 300))}`);
3437
+ }
3382
3438
  }
3383
3439
  else if ((name === 'write_file' || name === 'create_file') && typeof event.target === 'string') {
3384
3440
  filesWritten.push(sanitizeUserFacingPathText(event.target));
3385
3441
  }
3386
3442
  else {
3387
- // Keep last ~300 chars of output for context
3388
- const excerpt = event.output.length > 300 ? event.output.slice(-300) : event.output;
3443
+ const excerpt = output.length > 300 ? output.slice(-300) : output;
3389
3444
  toolResults.push(`[${name}] ${sanitizeUserFacingPathText(excerpt)}`);
3390
3445
  }
3391
3446
  }
@@ -3410,10 +3465,13 @@ document.addEventListener('DOMContentLoaded', () => {
3410
3465
  // Concatenate ALL assistant text fragments in order — keeps full
3411
3466
  // multi-turn reasoning instead of only the last fragment.
3412
3467
  const fullAssistantText = assistantFragments.join('\n\n').trim();
3413
- if (fullAssistantText.length > 20) {
3468
+ if (isSubstantiveAgentAnswer(fullAssistantText)) {
3414
3469
  return fullAssistantText;
3415
3470
  }
3416
- // Otherwise build a summary from tool evidence
3471
+ if (readExcerpts.length > 0) {
3472
+ return '';
3473
+ }
3474
+ // Otherwise build a summary from tool evidence — but never treat stubs as answers.
3417
3475
  const sections = [];
3418
3476
  if (filesRead.length > 0) {
3419
3477
  sections.push(`Files inspected: ${filesRead.join(', ')}`);
@@ -3424,9 +3482,8 @@ document.addEventListener('DOMContentLoaded', () => {
3424
3482
  if (toolResults.length > 0) {
3425
3483
  sections.push(toolResults.slice(-5).join('\n'));
3426
3484
  }
3427
- return sections.length > 0
3428
- ? sections.join('\n\n')
3429
- : '';
3485
+ const synthesized = sections.join('\n\n');
3486
+ return isToolEvidenceStubAnswer(synthesized) ? '' : synthesized;
3430
3487
  }
3431
3488
  sanitizeV3AgentEventForUser(event) {
3432
3489
  const sanitizeValue = (value) => {
@@ -3646,6 +3703,7 @@ document.addEventListener('DOMContentLoaded', () => {
3646
3703
  // Exit stream early on complete — agent is done; server-side teardown
3647
3704
  // can hold the connection open for many seconds otherwise.
3648
3705
  if (event.type === 'complete') {
3706
+ await Promise.allSettled([...this.pendingV3ClientToolTasks]);
3649
3707
  reader.cancel().catch(() => { });
3650
3708
  return {
3651
3709
  task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.11.5",
3
+ "version": "1.11.7",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",