@zds-ai/cli 0.1.4 → 0.1.6

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 (78) hide show
  1. package/dist/agent/grok-agent.d.ts +13 -1
  2. package/dist/agent/grok-agent.js +374 -64
  3. package/dist/agent/grok-agent.js.map +1 -1
  4. package/dist/agent/llm-agent.d.ts +276 -0
  5. package/dist/agent/llm-agent.js +2839 -0
  6. package/dist/agent/llm-agent.js.map +1 -0
  7. package/dist/agent/prompt-variables.d.ts +127 -0
  8. package/dist/agent/prompt-variables.js +325 -0
  9. package/dist/agent/prompt-variables.js.map +1 -0
  10. package/dist/bin/fastcaption.sh +56 -0
  11. package/dist/bin/generate_image_sd.sh +2 -2
  12. package/dist/grok/client.d.ts +10 -9
  13. package/dist/grok/client.js +26 -10
  14. package/dist/grok/client.js.map +1 -1
  15. package/dist/grok/tools.d.ts +5 -5
  16. package/dist/grok/tools.js +16 -15
  17. package/dist/grok/tools.js.map +1 -1
  18. package/dist/hooks/use-input-handler.d.ts +3 -3
  19. package/dist/hooks/use-input-handler.js +47 -6
  20. package/dist/hooks/use-input-handler.js.map +1 -1
  21. package/dist/index.js +17 -10
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp/client.js +10 -2
  24. package/dist/mcp/client.js.map +1 -1
  25. package/dist/tools/character-tool.js +1 -1
  26. package/dist/tools/character-tool.js.map +1 -1
  27. package/dist/tools/clear-cache-tool.js +1 -1
  28. package/dist/tools/clear-cache-tool.js.map +1 -1
  29. package/dist/tools/file-conversion-tool.js +1 -1
  30. package/dist/tools/file-conversion-tool.js.map +1 -1
  31. package/dist/tools/image-tool.d.ts +4 -4
  32. package/dist/tools/image-tool.js +11 -19
  33. package/dist/tools/image-tool.js.map +1 -1
  34. package/dist/tools/internet-tool.js +1 -1
  35. package/dist/tools/internet-tool.js.map +1 -1
  36. package/dist/tools/introspect-tool.js +7 -12
  37. package/dist/tools/introspect-tool.js.map +1 -1
  38. package/dist/tools/task-tool.js +1 -1
  39. package/dist/tools/task-tool.js.map +1 -1
  40. package/dist/tools/text-editor.js +1 -1
  41. package/dist/tools/text-editor.js.map +1 -1
  42. package/dist/tools/zsh.js +1 -1
  43. package/dist/tools/zsh.js.map +1 -1
  44. package/dist/ui/components/active-task-status.d.ts +2 -2
  45. package/dist/ui/components/api-key-input.d.ts +2 -2
  46. package/dist/ui/components/api-key-input.js +2 -2
  47. package/dist/ui/components/api-key-input.js.map +1 -1
  48. package/dist/ui/components/backend-status.d.ts +2 -2
  49. package/dist/ui/components/chat-history.d.ts +1 -1
  50. package/dist/ui/components/chat-history.js +1 -1
  51. package/dist/ui/components/chat-history.js.map +1 -1
  52. package/dist/ui/components/chat-interface.d.ts +2 -2
  53. package/dist/ui/components/chat-interface.js +4 -0
  54. package/dist/ui/components/chat-interface.js.map +1 -1
  55. package/dist/ui/components/context-status.d.ts +2 -2
  56. package/dist/ui/components/model-selection.js +1 -1
  57. package/dist/ui/components/model-selection.js.map +1 -1
  58. package/dist/ui/components/mood-status.d.ts +2 -2
  59. package/dist/ui/components/persona-status.d.ts +2 -2
  60. package/dist/utils/chat-history-manager.d.ts +2 -1
  61. package/dist/utils/chat-history-manager.js.map +1 -1
  62. package/dist/utils/hook-executor.d.ts +8 -2
  63. package/dist/utils/hook-executor.js +138 -13
  64. package/dist/utils/hook-executor.js.map +1 -1
  65. package/dist/utils/image-encoder.js +4 -3
  66. package/dist/utils/image-encoder.js.map +1 -1
  67. package/dist/utils/rephrase-handler.d.ts +2 -2
  68. package/dist/utils/rephrase-handler.js.map +1 -1
  69. package/dist/utils/settings-manager.d.ts +5 -0
  70. package/dist/utils/settings-manager.js +6 -0
  71. package/dist/utils/settings-manager.js.map +1 -1
  72. package/dist/utils/slash-commands.d.ts +7 -3
  73. package/dist/utils/slash-commands.js +42 -11
  74. package/dist/utils/slash-commands.js.map +1 -1
  75. package/dist/utils/startup-hook.d.ts +3 -3
  76. package/dist/utils/startup-hook.js +30 -16
  77. package/dist/utils/startup-hook.js.map +1 -1
  78. package/package.json +2 -1
@@ -5,6 +5,7 @@ import { ChatHistoryManager } from "../utils/chat-history-manager.js";
5
5
  import { logApiError } from "../utils/error-logger.js";
6
6
  import { parseImagesFromMessage, hasImageReferences } from "../utils/image-encoder.js";
7
7
  import { getTextContent } from "../utils/content-utils.js";
8
+ import { Variable } from "./prompt-variables.js";
8
9
  import fs from "fs";
9
10
  import { TextEditorTool, MorphEditorTool, ZshTool, ConfirmationTool, SearchTool, EnvTool, IntrospectTool, ClearCacheTool, CharacterTool, TaskTool, InternetTool, ImageTool, FileConversionTool, RestartTool } from "../tools/index.js";
10
11
  import { EventEmitter } from "events";
@@ -109,6 +110,7 @@ export class GrokAgent extends EventEmitter {
109
110
  apiKeyEnvVar = "GROK_API_KEY";
110
111
  pendingContextEditSession = null;
111
112
  rephraseState = null;
113
+ hookPrefillText = null;
112
114
  constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
113
115
  super();
114
116
  const manager = getSettingsManager();
@@ -160,6 +162,7 @@ export class GrokAgent extends EventEmitter {
160
162
  }
161
163
  startupHookOutput;
162
164
  systemPrompt = "Initializing..."; // THE system prompt (always at messages[0])
165
+ hasRunInstanceHook = false;
163
166
  /**
164
167
  * Initialize the agent with dynamic system prompt
165
168
  * Must be called after construction
@@ -167,35 +170,19 @@ export class GrokAgent extends EventEmitter {
167
170
  async initialize() {
168
171
  // Build system message
169
172
  await this.buildSystemMessage();
170
- // Execute instance hook on every startup (fresh or not)
171
- const settings = getSettingsManager();
172
- const instanceHookPath = settings.getInstanceHook();
173
- if (instanceHookPath) {
174
- const hookResult = await executeOperationHook(instanceHookPath, "instance", {}, 30000, false, // Instance hook is not mandatory
175
- this.getCurrentTokenCount(), this.getMaxContextSize());
176
- if (hookResult.approved && hookResult.commands && hookResult.commands.length > 0) {
177
- // Apply hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM)
178
- await this.processHookResult(hookResult);
179
- }
180
- }
181
173
  }
182
174
  /**
183
175
  * Build/rebuild the system message with current tool availability
184
176
  * Updates this.systemPrompt which is always used for messages[0]
185
177
  */
186
178
  async buildSystemMessage() {
187
- // Add startup hook output if provided
188
- const startupHookSection = this.startupHookOutput
189
- ? `${this.startupHookOutput}\n`
190
- : "";
191
179
  // Generate dynamic tool list using introspect tool
192
180
  const toolsResult = await this.introspect.introspect("tools");
193
181
  const toolsSection = toolsResult.success ? toolsResult.output : "Tools: Unknown";
182
+ // Set APP:TOOLS variable
183
+ Variable.set("APP:TOOLS", toolsSection);
194
184
  // Build THE system prompt
195
- this.systemPrompt = `${startupHookSection}
196
- ${toolsSection}
197
-
198
- Current working directory: ${process.cwd()}`;
185
+ this.systemPrompt = Variable.renderFull('SYSTEM');
199
186
  // Update messages[0] with the system prompt
200
187
  this.messages[0] = {
201
188
  role: "system",
@@ -355,16 +342,27 @@ Current working directory: ${process.cwd()}`;
355
342
  let isSystemRephrase = false;
356
343
  let messageToSend = message;
357
344
  let messageType = "user";
345
+ let prefillText;
358
346
  if (message.startsWith("/system rephrase")) {
359
347
  isRephraseCommand = true;
360
348
  isSystemRephrase = true;
361
349
  messageToSend = message.substring(8).trim(); // Strip "/system " (8 chars including space)
362
350
  messageType = "system";
351
+ // Extract prefill text after "/system rephrase "
352
+ const prefillMatch = message.match(/^\/system rephrase\s+(.+)$/);
353
+ if (prefillMatch) {
354
+ prefillText = prefillMatch[1];
355
+ }
363
356
  }
364
357
  else if (message.startsWith("/rephrase")) {
365
358
  isRephraseCommand = true;
366
359
  messageToSend = message; // Keep full text including "/rephrase"
367
360
  messageType = "user";
361
+ // Extract prefill text after "/rephrase "
362
+ const prefillMatch = message.match(/^\/rephrase\s+(.+)$/);
363
+ if (prefillMatch) {
364
+ prefillText = prefillMatch[1];
365
+ }
368
366
  }
369
367
  // If this is a rephrase command, find the last assistant message
370
368
  if (isRephraseCommand) {
@@ -381,7 +379,7 @@ Current working directory: ${process.cwd()}`;
381
379
  }
382
380
  // Store rephrase state (will be updated with newResponseIndex after response)
383
381
  // For now, just mark that we're in rephrase mode
384
- this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType);
382
+ this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType, prefillText);
385
383
  }
386
384
  // Before adding the new user message, check if there are incomplete tool calls
387
385
  // from a previous interrupted turn. This prevents malformed message sequences
@@ -413,29 +411,83 @@ Current working directory: ${process.cwd()}`;
413
411
  }
414
412
  }
415
413
  }
414
+ // Clear one-shot variables
415
+ Variable.clearOneShot();
416
+ // Execute instance hook once per session (after first clearOneShot)
417
+ if (!this.hasRunInstanceHook) {
418
+ this.hasRunInstanceHook = true;
419
+ const settings = getSettingsManager();
420
+ const instanceHookPath = settings.getInstanceHook();
421
+ if (instanceHookPath) {
422
+ const hookResult = await executeOperationHook(instanceHookPath, "instance", {}, 30000, false, // Instance hook is not mandatory
423
+ this.getCurrentTokenCount(), this.getMaxContextSize());
424
+ if (hookResult.approved && hookResult.commands && hookResult.commands.length > 0) {
425
+ // Apply hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM, SET*)
426
+ const results = applyHookCommands(hookResult.commands);
427
+ // Apply prompt variables from SET* commands
428
+ for (const [varName, value] of results.promptVars.entries()) {
429
+ Variable.set(varName, value);
430
+ }
431
+ // Process other hook commands (MODEL, BACKEND, ENV)
432
+ await this.processHookCommands(results);
433
+ // Add SYSTEM message to messages array if present
434
+ if (results.system) {
435
+ this.messages.push({
436
+ role: 'system',
437
+ content: results.system
438
+ });
439
+ }
440
+ // Store prefill text from hook if present
441
+ if (results.prefill) {
442
+ this.hookPrefillText = results.prefill;
443
+ }
444
+ }
445
+ }
446
+ }
447
+ // Parse images once if present (for both text extraction and later assembly)
448
+ const parsed = hasImageReferences(messageToSend)
449
+ ? parseImagesFromMessage(messageToSend)
450
+ : { text: messageToSend, images: [] };
451
+ // Set USER:PROMPT variable (text only, images stripped)
452
+ Variable.set("USER:PROMPT", parsed.text);
453
+ // Execute prePrompt hook if configured
454
+ const hookPath = getSettingsManager().getPrePromptHook();
455
+ if (hookPath) {
456
+ const hookResult = await executeOperationHook(hookPath, "prePrompt", { USER_MESSAGE: parsed.text }, 30000, false, // prePrompt hook is never mandatory
457
+ this.getCurrentTokenCount(), this.getMaxContextSize());
458
+ if (hookResult.approved && hookResult.commands) {
459
+ const results = applyHookCommands(hookResult.commands);
460
+ // Set prompt variables from hook output (SET, SET_FILE, SET_TEMP_FILE)
461
+ for (const [varName, value] of results.promptVars.entries()) {
462
+ Variable.set(varName, value);
463
+ }
464
+ // Process other hook commands (MODEL, BACKEND, SYSTEM, etc.)
465
+ await this.processHookCommands(results);
466
+ // Store prefill text from hook if present
467
+ if (results.prefill) {
468
+ this.hookPrefillText = results.prefill;
469
+ }
470
+ }
471
+ }
472
+ // Assemble final message from variables
473
+ const assembledMessage = Variable.renderFull("USER");
416
474
  // Add user/system message to conversation
417
- // Check for image references and parse if present
418
475
  // Note: System messages can only have string content, so images are only supported for user messages
419
476
  const supportsVision = this.grokClient.getSupportsVision();
420
- let messageContent = messageToSend;
421
- if (messageType === "user" && hasImageReferences(messageToSend) && supportsVision) {
422
- // Parse images from message (only for user messages)
423
- const parsed = parseImagesFromMessage(messageToSend);
424
- if (parsed.images.length > 0) {
425
- // Construct content array with text and images
426
- messageContent = [
427
- { type: "text", text: parsed.text },
428
- ...parsed.images
429
- ];
430
- }
431
- else {
432
- // No valid images found (errors will be in parsed.text)
433
- messageContent = parsed.text;
434
- }
477
+ let messageContent = assembledMessage;
478
+ if (messageType === "user" && parsed.images.length > 0 && supportsVision) {
479
+ // Construct content array with assembled text and images
480
+ messageContent = [
481
+ { type: "text", text: assembledMessage },
482
+ ...parsed.images
483
+ ];
435
484
  }
436
485
  const userEntry = {
437
486
  type: messageType,
438
487
  content: messageContent,
488
+ originalContent: messageType === "user" ? (parsed.images.length > 0 && supportsVision
489
+ ? [{ type: "text", text: parsed.text }, ...parsed.images]
490
+ : parsed.text) : undefined,
439
491
  timestamp: new Date(),
440
492
  };
441
493
  this.chatHistory.push(userEntry);
@@ -453,6 +505,20 @@ Current working directory: ${process.cwd()}`;
453
505
  let toolRounds = 0;
454
506
  let consecutiveNonToolResponses = 0;
455
507
  try {
508
+ // If this is a rephrase with prefill text, add the assistant message now
509
+ if (this.rephraseState?.prefillText) {
510
+ this.messages.push({
511
+ role: "assistant",
512
+ content: this.rephraseState.prefillText
513
+ });
514
+ }
515
+ // If a hook returned prefill text, add the assistant message now
516
+ if (this.hookPrefillText) {
517
+ this.messages.push({
518
+ role: "assistant",
519
+ content: this.hookPrefillText
520
+ });
521
+ }
456
522
  // Always fetch tools fresh - getAllGrokTools() handles lazy refresh internally
457
523
  const supportsTools = this.grokClient.getSupportsTools();
458
524
  let currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
@@ -626,7 +692,16 @@ Current working directory: ${process.cwd()}`;
626
692
  }
627
693
  else {
628
694
  // No tool calls in this response - only add it if there's actual content
629
- const trimmedContent = assistantMessage.content?.trim();
695
+ let trimmedContent = assistantMessage.content?.trim();
696
+ // If this was a rephrase with prefill, prepend the prefill text to the response
697
+ if (trimmedContent && this.rephraseState?.prefillText) {
698
+ trimmedContent = this.rephraseState.prefillText + trimmedContent;
699
+ }
700
+ // If a hook provided prefill, prepend it to the response
701
+ if (trimmedContent && this.hookPrefillText) {
702
+ trimmedContent = this.hookPrefillText + trimmedContent;
703
+ this.hookPrefillText = null; // Clear after use
704
+ }
630
705
  if (trimmedContent) {
631
706
  const responseEntry = {
632
707
  type: "assistant",
@@ -642,7 +717,7 @@ Current working directory: ${process.cwd()}`;
642
717
  // Update rephrase state with the new response index
643
718
  if (this.rephraseState && this.rephraseState.newResponseIndex === -1) {
644
719
  const newResponseIndex = this.chatHistory.length - 1;
645
- this.setRephraseState(this.rephraseState.originalAssistantMessageIndex, this.rephraseState.rephraseRequestIndex, newResponseIndex, this.rephraseState.messageType);
720
+ this.setRephraseState(this.rephraseState.originalAssistantMessageIndex, this.rephraseState.rephraseRequestIndex, newResponseIndex, this.rephraseState.messageType, this.rephraseState.prefillText);
646
721
  }
647
722
  }
648
723
  // TODO: HACK - This is a temporary fix to prevent duplicate responses.
@@ -691,6 +766,22 @@ Current working directory: ${process.cwd()}`;
691
766
  return newEntries;
692
767
  }
693
768
  catch (error) {
769
+ // Check if context is too large (413 error when vision already disabled)
770
+ if (error.message && error.message.startsWith('CONTEXT_TOO_LARGE:')) {
771
+ const beforeCount = this.chatHistory.length;
772
+ this.compactContext(20);
773
+ const afterCount = this.chatHistory.length;
774
+ const removedCount = beforeCount - afterCount;
775
+ const compactEntry = {
776
+ type: "system",
777
+ content: `Context was too large for backend. Automatically compacted: removed ${removedCount} older messages, keeping last 20 messages. Please retry your request.`,
778
+ timestamp: new Date(),
779
+ };
780
+ this.chatHistory.push(compactEntry);
781
+ // Mark first message as processed
782
+ this.firstMessageProcessed = true;
783
+ return [userEntry, compactEntry];
784
+ }
694
785
  const errorEntry = {
695
786
  type: "assistant",
696
787
  content: `Sorry, I encountered an error: ${error.message}`,
@@ -804,16 +895,27 @@ Current working directory: ${process.cwd()}`;
804
895
  let isSystemRephrase = false;
805
896
  let messageToSend = message;
806
897
  let messageType = "user";
898
+ let prefillText;
807
899
  if (message.startsWith("/system rephrase")) {
808
900
  isRephraseCommand = true;
809
901
  isSystemRephrase = true;
810
902
  messageToSend = message.substring(8).trim(); // Strip "/system " (8 chars including space)
811
903
  messageType = "system";
904
+ // Extract prefill text after "/system rephrase "
905
+ const prefillMatch = message.match(/^\/system rephrase\s+(.+)$/);
906
+ if (prefillMatch) {
907
+ prefillText = prefillMatch[1];
908
+ }
812
909
  }
813
910
  else if (message.startsWith("/rephrase")) {
814
911
  isRephraseCommand = true;
815
912
  messageToSend = message; // Keep full text including "/rephrase"
816
913
  messageType = "user";
914
+ // Extract prefill text after "/rephrase "
915
+ const prefillMatch = message.match(/^\/rephrase\s+(.+)$/);
916
+ if (prefillMatch) {
917
+ prefillText = prefillMatch[1];
918
+ }
817
919
  }
818
920
  // If this is a rephrase command, find the last assistant message
819
921
  if (isRephraseCommand) {
@@ -830,7 +932,7 @@ Current working directory: ${process.cwd()}`;
830
932
  }
831
933
  // Store rephrase state (will be updated with newResponseIndex after response)
832
934
  // For now, just mark that we're in rephrase mode
833
- this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType);
935
+ this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType, prefillText);
834
936
  }
835
937
  // Before adding the new user message, check if there are incomplete tool calls
836
938
  // from a previous interrupted turn. This prevents malformed message sequences
@@ -862,29 +964,83 @@ Current working directory: ${process.cwd()}`;
862
964
  }
863
965
  }
864
966
  }
967
+ // Clear one-shot variables
968
+ Variable.clearOneShot();
969
+ // Execute instance hook once per session (after first clearOneShot)
970
+ if (!this.hasRunInstanceHook) {
971
+ this.hasRunInstanceHook = true;
972
+ const settings = getSettingsManager();
973
+ const instanceHookPath = settings.getInstanceHook();
974
+ if (instanceHookPath) {
975
+ const hookResult = await executeOperationHook(instanceHookPath, "instance", {}, 30000, false, // Instance hook is not mandatory
976
+ this.getCurrentTokenCount(), this.getMaxContextSize());
977
+ if (hookResult.approved && hookResult.commands && hookResult.commands.length > 0) {
978
+ // Apply hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM, SET*)
979
+ const results = applyHookCommands(hookResult.commands);
980
+ // Apply prompt variables from SET* commands
981
+ for (const [varName, value] of results.promptVars.entries()) {
982
+ Variable.set(varName, value);
983
+ }
984
+ // Process other hook commands (MODEL, BACKEND, ENV)
985
+ await this.processHookCommands(results);
986
+ // Add SYSTEM message to messages array if present
987
+ if (results.system) {
988
+ this.messages.push({
989
+ role: 'system',
990
+ content: results.system
991
+ });
992
+ }
993
+ // Store prefill text from hook if present
994
+ if (results.prefill) {
995
+ this.hookPrefillText = results.prefill;
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ // Parse images once if present (for both text extraction and later assembly)
1001
+ const parsed = hasImageReferences(messageToSend)
1002
+ ? parseImagesFromMessage(messageToSend)
1003
+ : { text: messageToSend, images: [] };
1004
+ // Set USER:PROMPT variable (text only, images stripped)
1005
+ Variable.set("USER:PROMPT", parsed.text);
1006
+ // Execute prePrompt hook if configured
1007
+ const hookPath = getSettingsManager().getPrePromptHook();
1008
+ if (hookPath) {
1009
+ const hookResult = await executeOperationHook(hookPath, "prePrompt", { USER_MESSAGE: parsed.text }, 30000, false, // prePrompt hook is never mandatory
1010
+ this.getCurrentTokenCount(), this.getMaxContextSize());
1011
+ if (hookResult.approved && hookResult.commands) {
1012
+ const results = applyHookCommands(hookResult.commands);
1013
+ // Set prompt variables from hook output (SET, SET_FILE, SET_TEMP_FILE)
1014
+ for (const [varName, value] of results.promptVars.entries()) {
1015
+ Variable.set(varName, value);
1016
+ }
1017
+ // Process other hook commands (MODEL, BACKEND, SYSTEM, etc.)
1018
+ await this.processHookCommands(results);
1019
+ // Store prefill text from hook if present
1020
+ if (results.prefill) {
1021
+ this.hookPrefillText = results.prefill;
1022
+ }
1023
+ }
1024
+ }
1025
+ // Assemble final message from variables
1026
+ const assembledMessage = Variable.renderFull("USER");
865
1027
  // Add user/system message to both API conversation and chat history
866
- // Check for image references and parse if present
867
1028
  // Note: System messages can only have string content, so images are only supported for user messages
868
1029
  const supportsVision = this.grokClient.getSupportsVision();
869
- let messageContent = messageToSend;
870
- if (messageType === "user" && hasImageReferences(messageToSend) && supportsVision) {
871
- // Parse images from message (only for user messages)
872
- const parsed = parseImagesFromMessage(messageToSend);
873
- if (parsed.images.length > 0) {
874
- // Construct content array with text and images
875
- messageContent = [
876
- { type: "text", text: parsed.text },
877
- ...parsed.images
878
- ];
879
- }
880
- else {
881
- // No valid images found (errors will be in parsed.text)
882
- messageContent = parsed.text;
883
- }
1030
+ let messageContent = assembledMessage;
1031
+ if (messageType === "user" && parsed.images.length > 0 && supportsVision) {
1032
+ // Construct content array with assembled text and images
1033
+ messageContent = [
1034
+ { type: "text", text: assembledMessage },
1035
+ ...parsed.images
1036
+ ];
884
1037
  }
885
1038
  const userEntry = {
886
1039
  type: messageType,
887
1040
  content: messageContent,
1041
+ originalContent: messageType === "user" ? (parsed.images.length > 0 && supportsVision
1042
+ ? [{ type: "text", text: parsed.text }, ...parsed.images]
1043
+ : parsed.text) : undefined,
888
1044
  timestamp: new Date(),
889
1045
  };
890
1046
  this.chatHistory.push(userEntry);
@@ -894,7 +1050,7 @@ Current working directory: ${process.cwd()}`;
894
1050
  }
895
1051
  else {
896
1052
  // System messages must have string content only
897
- this.messages.push({ role: "system", content: typeof messageContent === "string" ? messageContent : messageToSend });
1053
+ this.messages.push({ role: "system", content: typeof messageContent === "string" ? messageContent : assembledMessage });
898
1054
  }
899
1055
  await this.emitContextChange();
900
1056
  // Yield user message so UI can display it immediately
@@ -902,6 +1058,20 @@ Current working directory: ${process.cwd()}`;
902
1058
  type: "user_message",
903
1059
  userEntry: userEntry,
904
1060
  };
1061
+ // If this is a rephrase with prefill text, add the assistant message now
1062
+ if (this.rephraseState?.prefillText) {
1063
+ this.messages.push({
1064
+ role: "assistant",
1065
+ content: this.rephraseState.prefillText
1066
+ });
1067
+ }
1068
+ // If a hook returned prefill text, add the assistant message now
1069
+ if (this.hookPrefillText) {
1070
+ this.messages.push({
1071
+ role: "assistant",
1072
+ content: this.hookPrefillText
1073
+ });
1074
+ }
905
1075
  // Calculate input tokens
906
1076
  let inputTokens = this.tokenCounter.countMessageTokens(this.messages);
907
1077
  yield {
@@ -940,6 +1110,23 @@ Current working directory: ${process.cwd()}`;
940
1110
  let tool_calls_yielded = false;
941
1111
  let streamFinished = false;
942
1112
  let insideThinkTag = false;
1113
+ // If this is a rephrase with prefill, yield the prefill text first and add to accumulated content
1114
+ if (this.rephraseState?.prefillText) {
1115
+ yield {
1116
+ type: "content",
1117
+ content: this.rephraseState.prefillText,
1118
+ };
1119
+ accumulatedContent = this.rephraseState.prefillText;
1120
+ }
1121
+ // If a hook provided prefill, yield it first and add to accumulated content
1122
+ if (this.hookPrefillText) {
1123
+ yield {
1124
+ type: "content",
1125
+ content: this.hookPrefillText,
1126
+ };
1127
+ accumulatedContent = this.hookPrefillText;
1128
+ this.hookPrefillText = null; // Clear after use
1129
+ }
943
1130
  try {
944
1131
  for await (const chunk of stream) {
945
1132
  // Check for cancellation in the streaming loop
@@ -1237,6 +1424,25 @@ Current working directory: ${process.cwd()}`;
1237
1424
  yield { type: "done" };
1238
1425
  return;
1239
1426
  }
1427
+ // Check if context is too large (413 error when vision already disabled)
1428
+ if (error.message && error.message.startsWith('CONTEXT_TOO_LARGE:')) {
1429
+ const beforeCount = this.chatHistory.length;
1430
+ this.compactContext(20);
1431
+ const afterCount = this.chatHistory.length;
1432
+ const removedCount = beforeCount - afterCount;
1433
+ const compactEntry = {
1434
+ type: "system",
1435
+ content: `Context was too large for backend. Automatically compacted: removed ${removedCount} older messages, keeping last 20 messages. Please retry your request.`,
1436
+ timestamp: new Date(),
1437
+ };
1438
+ this.chatHistory.push(compactEntry);
1439
+ yield {
1440
+ type: "content",
1441
+ content: getTextContent(compactEntry.content),
1442
+ };
1443
+ yield { type: "done" };
1444
+ return;
1445
+ }
1240
1446
  const errorEntry = {
1241
1447
  type: "assistant",
1242
1448
  content: `Sorry, I encountered an error: ${error.message}`,
@@ -1496,7 +1702,7 @@ Current working directory: ${process.cwd()}`;
1496
1702
  case "generateImage":
1497
1703
  return await this.imageTool.generateImage(args.prompt, args.negativePrompt, args.width, args.height, args.model, args.sampler, args.configScale, args.numSteps, args.nsfw, args.name, args.move, args.seed);
1498
1704
  case "captionImage":
1499
- return await this.imageTool.captionImage(args.filename, args.prompt);
1705
+ return await this.imageTool.captionImage(args.filename, args.backend);
1500
1706
  case "pngInfo":
1501
1707
  return await this.imageTool.pngInfo(args.filename);
1502
1708
  case "listImageModels":
@@ -1683,8 +1889,8 @@ Current working directory: ${process.cwd()}`;
1683
1889
  clearPendingContextEditSession() {
1684
1890
  this.pendingContextEditSession = null;
1685
1891
  }
1686
- setRephraseState(originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType) {
1687
- this.rephraseState = { originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType };
1892
+ setRephraseState(originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType, prefillText) {
1893
+ this.rephraseState = { originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType, prefillText };
1688
1894
  }
1689
1895
  getRephraseState() {
1690
1896
  return this.rephraseState;
@@ -2038,6 +2244,8 @@ Current working directory: ${process.cwd()}`;
2038
2244
  }
2039
2245
  setModel(model) {
2040
2246
  this.grokClient.setModel(model);
2247
+ // Reset supportsVision flag for new model
2248
+ this.grokClient.setSupportsVision(true);
2041
2249
  // Update token counter for new model
2042
2250
  this.tokenCounter.dispose();
2043
2251
  this.tokenCounter = createTokenCounter(model);
@@ -2412,6 +2620,7 @@ Current working directory: ${process.cwd()}`;
2412
2620
  baseUrl: this.grokClient.getBaseURL(),
2413
2621
  apiKeyEnvVar: this.apiKeyEnvVar,
2414
2622
  model: this.getCurrentModel(),
2623
+ supportsVision: this.grokClient.getSupportsVision(),
2415
2624
  };
2416
2625
  }
2417
2626
  /**
@@ -2425,10 +2634,15 @@ Current working directory: ${process.cwd()}`;
2425
2634
  // Restore cwd early (hooks may need correct working directory)
2426
2635
  if (state.cwd) {
2427
2636
  try {
2428
- process.chdir(state.cwd);
2637
+ const fs = await import('fs');
2638
+ // Only attempt to change directory if it exists
2639
+ if (fs.existsSync(state.cwd)) {
2640
+ process.chdir(state.cwd);
2641
+ }
2642
+ // Silently skip if directory doesn't exist (common in containerized environments)
2429
2643
  }
2430
2644
  catch (error) {
2431
- console.warn(`Failed to restore working directory to ${state.cwd}:`, error);
2645
+ // Silently skip on any error - working directory restoration is non-critical
2432
2646
  }
2433
2647
  }
2434
2648
  // Restore backend/baseUrl/apiKeyEnvVar/model if present (creates initial client)
@@ -2441,6 +2655,10 @@ Current working directory: ${process.cwd()}`;
2441
2655
  const model = state.model || this.getCurrentModel();
2442
2656
  this.grokClient = new GrokClient(apiKey, model, state.baseUrl, state.backend);
2443
2657
  this.apiKeyEnvVar = state.apiKeyEnvVar;
2658
+ // Restore supportsVision flag if present
2659
+ if (state.supportsVision !== undefined) {
2660
+ this.grokClient.setSupportsVision(state.supportsVision);
2661
+ }
2444
2662
  // Reinitialize MCP servers when restoring session
2445
2663
  try {
2446
2664
  const config = loadMCPConfig();
@@ -2469,30 +2687,122 @@ Current working directory: ${process.cwd()}`;
2469
2687
  // Restore persona (hook may change backend/model and sets env vars)
2470
2688
  if (state.persona) {
2471
2689
  try {
2472
- await this.setPersona(state.persona, state.personaColor);
2690
+ const result = await this.setPersona(state.persona, state.personaColor);
2691
+ if (!result.success) {
2692
+ // If persona hook failed (e.g., backend test failed), still set the persona values
2693
+ // but don't change backend/model. This prevents losing persona state on transitory errors.
2694
+ console.warn(`Persona hook failed, setting persona without backend change: ${result.error}`);
2695
+ this.persona = state.persona;
2696
+ this.personaColor = state.personaColor;
2697
+ process.env.ZDS_AI_AGENT_PERSONA = state.persona;
2698
+ }
2473
2699
  }
2474
2700
  catch (error) {
2475
2701
  console.warn(`Failed to restore persona "${state.persona}":`, error);
2702
+ // Still set persona values even if hook crashed
2703
+ this.persona = state.persona;
2704
+ this.personaColor = state.personaColor;
2705
+ process.env.ZDS_AI_AGENT_PERSONA = state.persona;
2476
2706
  }
2477
2707
  }
2478
2708
  // Restore mood (hook sets env vars)
2479
2709
  if (state.mood) {
2480
2710
  try {
2481
- await this.setMood(state.mood, state.moodColor);
2711
+ const result = await this.setMood(state.mood, state.moodColor);
2712
+ if (!result.success) {
2713
+ // If mood hook failed (e.g., backend test failed), still set the mood values
2714
+ // but don't change backend/model. This prevents losing mood state on transitory errors.
2715
+ console.warn(`Mood hook failed, setting mood without backend change: ${result.error}`);
2716
+ this.mood = state.mood;
2717
+ this.moodColor = state.moodColor;
2718
+ process.env.ZDS_AI_AGENT_MOOD = state.mood;
2719
+ }
2482
2720
  }
2483
2721
  catch (error) {
2484
2722
  console.warn(`Failed to restore mood "${state.mood}":`, error);
2723
+ // Still set mood values even if hook crashed
2724
+ this.mood = state.mood;
2725
+ this.moodColor = state.moodColor;
2726
+ process.env.ZDS_AI_AGENT_MOOD = state.mood;
2485
2727
  }
2486
2728
  }
2487
2729
  // Restore active task (hook sets env vars)
2488
2730
  if (state.activeTask) {
2489
2731
  try {
2490
- await this.startActiveTask(state.activeTask, state.activeTaskAction, state.activeTaskColor);
2732
+ const result = await this.startActiveTask(state.activeTask, state.activeTaskAction, state.activeTaskColor);
2733
+ if (!result.success) {
2734
+ // If task hook failed (e.g., backend test failed), still set the task values
2735
+ // but don't change backend/model. This prevents losing task state on transitory errors.
2736
+ console.warn(`Task hook failed, setting active task without backend change: ${result.error}`);
2737
+ this.activeTask = state.activeTask;
2738
+ this.activeTaskAction = state.activeTaskAction;
2739
+ this.activeTaskColor = state.activeTaskColor;
2740
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK = state.activeTask;
2741
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION = state.activeTaskAction;
2742
+ }
2491
2743
  }
2492
2744
  catch (error) {
2493
2745
  console.warn(`Failed to restore active task "${state.activeTask}":`, error);
2746
+ // Still set task values even if hook crashed
2747
+ this.activeTask = state.activeTask;
2748
+ this.activeTaskAction = state.activeTaskAction;
2749
+ this.activeTaskColor = state.activeTaskColor;
2750
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK = state.activeTask;
2751
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION = state.activeTaskAction;
2752
+ }
2753
+ }
2754
+ }
2755
+ /**
2756
+ * Compact conversation context by keeping system prompt and last N messages
2757
+ * Reduces context size when it grows too large for backend to handle
2758
+ * @returns Number of messages removed
2759
+ */
2760
+ compactContext(keepLastMessages = 20) {
2761
+ if (this.chatHistory.length <= keepLastMessages) {
2762
+ // Nothing to compact
2763
+ return 0;
2764
+ }
2765
+ const removedCount = this.chatHistory.length - keepLastMessages;
2766
+ const keptMessages = this.chatHistory.slice(-keepLastMessages);
2767
+ // Clear both arrays
2768
+ this.chatHistory = keptMessages;
2769
+ this.messages = [];
2770
+ // Add system message noting the compaction
2771
+ const compactionNote = {
2772
+ type: 'system',
2773
+ content: `Context compacted: removed ${removedCount} older messages, keeping last ${keepLastMessages} messages.`,
2774
+ timestamp: new Date()
2775
+ };
2776
+ this.chatHistory.push(compactionNote);
2777
+ // Rebuild this.messages from compacted chatHistory
2778
+ for (const entry of this.chatHistory) {
2779
+ if (entry.type === 'system') {
2780
+ this.messages.push({
2781
+ role: 'system',
2782
+ content: entry.content
2783
+ });
2784
+ }
2785
+ else if (entry.type === 'user') {
2786
+ this.messages.push({
2787
+ role: 'user',
2788
+ content: entry.content
2789
+ });
2790
+ }
2791
+ else if (entry.type === 'assistant') {
2792
+ this.messages.push({
2793
+ role: 'assistant',
2794
+ content: entry.content
2795
+ });
2796
+ }
2797
+ else if (entry.type === 'tool_result') {
2798
+ this.messages.push({
2799
+ role: 'tool',
2800
+ tool_call_id: entry.toolResult.output || '',
2801
+ content: JSON.stringify(entry.toolResult)
2802
+ });
2494
2803
  }
2495
2804
  }
2805
+ return removedCount;
2496
2806
  }
2497
2807
  /**
2498
2808
  * Get all tool instances and their class names for display purposes