centaurus-cli 2.9.2 → 2.9.4
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/cli-adapter.d.ts +78 -11
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +851 -215
- package/dist/cli-adapter.js.map +1 -1
- package/dist/commands/CommandParser.d.ts +1 -1
- package/dist/commands/CommandParser.d.ts.map +1 -1
- package/dist/commands/CommandParser.js +113 -0
- package/dist/commands/CommandParser.js.map +1 -1
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +2 -0
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.d.ts +5 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +63 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/context/context-manager.d.ts +1 -1
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +3 -1
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/handlers/docker-handler.d.ts +9 -0
- package/dist/context/handlers/docker-handler.d.ts.map +1 -1
- package/dist/context/handlers/docker-handler.js +99 -10
- package/dist/context/handlers/docker-handler.js.map +1 -1
- package/dist/context/handlers/ssh-handler.d.ts +20 -0
- package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
- package/dist/context/handlers/ssh-handler.js +129 -1
- package/dist/context/handlers/ssh-handler.js.map +1 -1
- package/dist/context/subshell-handler.d.ts +15 -0
- package/dist/context/subshell-handler.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/services/ai-autocomplete-agent.d.ts +39 -0
- package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
- package/dist/services/ai-autocomplete-agent.js +189 -0
- package/dist/services/ai-autocomplete-agent.js.map +1 -0
- package/dist/services/ai-service-client.d.ts +25 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +195 -12
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.js +1 -1
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/local-chat-storage.d.ts +21 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +138 -43
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/ollama-service.d.ts +197 -0
- package/dist/services/ollama-service.d.ts.map +1 -0
- package/dist/services/ollama-service.js +324 -0
- package/dist/services/ollama-service.js.map +1 -0
- package/dist/services/warpify-detector.d.ts +43 -0
- package/dist/services/warpify-detector.d.ts.map +1 -0
- package/dist/services/warpify-detector.js +203 -0
- package/dist/services/warpify-detector.js.map +1 -0
- package/dist/services/workflow-storage.d.ts +72 -0
- package/dist/services/workflow-storage.d.ts.map +1 -0
- package/dist/services/workflow-storage.js +239 -0
- package/dist/services/workflow-storage.js.map +1 -0
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +14 -0
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/enter-remote-session.d.ts +13 -0
- package/dist/tools/enter-remote-session.d.ts.map +1 -0
- package/dist/tools/enter-remote-session.js +226 -0
- package/dist/tools/enter-remote-session.js.map +1 -0
- package/dist/tools/find-files.d.ts.map +1 -1
- package/dist/tools/find-files.js +9 -2
- package/dist/tools/find-files.js.map +1 -1
- package/dist/tools/grep-search.d.ts +104 -31
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +699 -430
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/workflow-tool.d.ts +11 -0
- package/dist/tools/workflow-tool.d.ts.map +1 -0
- package/dist/tools/workflow-tool.js +87 -0
- package/dist/tools/workflow-tool.js.map +1 -0
- package/dist/types/workflow.d.ts +110 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +8 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/ui/components/App.d.ts +12 -3
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +162 -6
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/Breadcrumbs.d.ts +4 -3
- package/dist/ui/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/ui/components/Breadcrumbs.js +60 -54
- package/dist/ui/components/Breadcrumbs.js.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +2 -2
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +3 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +488 -20
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts +2 -0
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +13 -3
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MultiLineInput.d.ts.map +1 -1
- package/dist/ui/components/MultiLineInput.js +68 -2
- package/dist/ui/components/MultiLineInput.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +169 -26
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/WorkflowCreatorScreen.d.ts +25 -0
- package/dist/ui/components/WorkflowCreatorScreen.d.ts.map +1 -0
- package/dist/ui/components/WorkflowCreatorScreen.js +164 -0
- package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -0
- package/dist/utils/command-history.d.ts +12 -2
- package/dist/utils/command-history.d.ts.map +1 -1
- package/dist/utils/command-history.js +57 -13
- package/dist/utils/command-history.js.map +1 -1
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +3 -2
- package/dist/utils/input-classifier.js.map +1 -1
- package/package.json +1 -1
package/dist/cli-adapter.js
CHANGED
|
@@ -20,6 +20,8 @@ import { readBinaryFileTool } from './tools/read-binary-file.js';
|
|
|
20
20
|
import { createImageTool } from './tools/create-image.js';
|
|
21
21
|
import { backgroundCommandTool } from './tools/background-command.js';
|
|
22
22
|
import { subAgentTool } from './tools/sub-agent.js';
|
|
23
|
+
import { enterRemoteSessionTool } from './tools/enter-remote-session.js';
|
|
24
|
+
import { workflowTool } from './tools/workflow-tool.js';
|
|
23
25
|
import { SubAgentManager } from './services/sub-agent-manager.js';
|
|
24
26
|
import { ShellInputAgent } from './services/shell-input-agent.js';
|
|
25
27
|
import { apiClient } from './services/api-client.js';
|
|
@@ -42,6 +44,8 @@ import { localChatStorage } from './services/local-chat-storage.js';
|
|
|
42
44
|
import { logWarning } from './utils/logger.js';
|
|
43
45
|
import { BackgroundTaskManager } from './services/background-task-manager.js';
|
|
44
46
|
import { sessionQuotaManager } from './services/session-quota-manager.js';
|
|
47
|
+
import { ollamaService, OllamaService } from './services/ollama-service.js';
|
|
48
|
+
import { workflowStorage } from './services/workflow-storage.js';
|
|
45
49
|
export class CentaurusCLI {
|
|
46
50
|
configManager;
|
|
47
51
|
toolRegistry;
|
|
@@ -89,7 +93,7 @@ export class CentaurusCLI {
|
|
|
89
93
|
onShowChatRenamePickerCallback;
|
|
90
94
|
onRestoreMessagesCallback;
|
|
91
95
|
uiMessageHistory = []; // Mirror of App.tsx's messageHistory for saving
|
|
92
|
-
|
|
96
|
+
cwdStack = []; // Stack of CWDs for nested sessions (pushed when entering, popped when exiting)
|
|
93
97
|
lastConnectionCommand = null; // Track the command used to connect to remote
|
|
94
98
|
onBackgroundModeChange;
|
|
95
99
|
onBackgroundTaskCountChange;
|
|
@@ -110,6 +114,11 @@ export class CentaurusCLI {
|
|
|
110
114
|
onShowMCPListScreen;
|
|
111
115
|
onSubAgentCountChange; // Callback for sub-agent count changes
|
|
112
116
|
onPromptAnswered; // Callback when AI answers a shell prompt
|
|
117
|
+
onShowWorkflowCreatorCallback; // Callback to show workflow creator screen with optional initial steps
|
|
118
|
+
onWorkflowSaveCallback; // Callback when workflow is saved
|
|
119
|
+
// Workflow learning mode state
|
|
120
|
+
workflowLearningActive = false;
|
|
121
|
+
learnedWorkflowSteps = [];
|
|
113
122
|
constructor() {
|
|
114
123
|
this.configManager = new ConfigManager();
|
|
115
124
|
this.toolRegistry = new ToolRegistry();
|
|
@@ -119,13 +128,14 @@ export class CentaurusCLI {
|
|
|
119
128
|
this.commandDetector = new CommandDetector();
|
|
120
129
|
this.aiContextInjector = new AIContextInjector();
|
|
121
130
|
// Register context change callback to update cwd
|
|
122
|
-
|
|
131
|
+
// Register context change callback to update cwd
|
|
132
|
+
this.contextManager.onContextChange((context, stack) => {
|
|
123
133
|
this.cwd = context.metadata.workingDirectory;
|
|
124
134
|
if (this.onCwdChange) {
|
|
125
135
|
this.onCwdChange(this.cwd);
|
|
126
136
|
}
|
|
127
137
|
if (this.onSubshellContextChange) {
|
|
128
|
-
this.onSubshellContextChange(context);
|
|
138
|
+
this.onSubshellContextChange(context, stack);
|
|
129
139
|
}
|
|
130
140
|
});
|
|
131
141
|
// Initialize MCP
|
|
@@ -217,6 +227,355 @@ export class CentaurusCLI {
|
|
|
217
227
|
// Wire this callback to ShellInputAgent
|
|
218
228
|
ShellInputAgent.setOnPromptAnswered(callback);
|
|
219
229
|
}
|
|
230
|
+
setOnShowWorkflowCreator(callback) {
|
|
231
|
+
this.onShowWorkflowCreatorCallback = callback;
|
|
232
|
+
}
|
|
233
|
+
setOnWorkflowSave(callback) {
|
|
234
|
+
this.onWorkflowSaveCallback = callback;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Save a workflow from the workflow creator UI
|
|
238
|
+
*/
|
|
239
|
+
saveWorkflow(name, steps, description) {
|
|
240
|
+
const workflow = workflowStorage.createWorkflow(name, steps, description);
|
|
241
|
+
workflowStorage.save(workflow);
|
|
242
|
+
if (this.onWorkflowSaveCallback) {
|
|
243
|
+
this.onWorkflowSaveCallback(workflow);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Toggle workflow learning mode.
|
|
248
|
+
* First call: Start learning and return a message to display.
|
|
249
|
+
* Second call: Stop learning and show workflow creator with learned steps.
|
|
250
|
+
* @returns Message to display to user, or null if showing workflow creator
|
|
251
|
+
*/
|
|
252
|
+
toggleWorkflowLearning() {
|
|
253
|
+
if (this.workflowLearningActive) {
|
|
254
|
+
// Stop learning mode
|
|
255
|
+
this.workflowLearningActive = false;
|
|
256
|
+
const steps = [...this.learnedWorkflowSteps];
|
|
257
|
+
this.learnedWorkflowSteps = []; // Clear for next learning session
|
|
258
|
+
if (steps.length === 0) {
|
|
259
|
+
return '⚠️ No steps were recorded. Learning mode cancelled.';
|
|
260
|
+
}
|
|
261
|
+
// Show workflow creator with the learned steps
|
|
262
|
+
if (this.onShowWorkflowCreatorCallback) {
|
|
263
|
+
this.onShowWorkflowCreatorCallback(steps);
|
|
264
|
+
return null; // UI will handle the screen change
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
return '❌ Workflow creator not available. Please update the CLI.';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Start learning mode
|
|
272
|
+
this.workflowLearningActive = true;
|
|
273
|
+
this.learnedWorkflowSteps = [];
|
|
274
|
+
return `📚 **Learning mode started!**
|
|
275
|
+
|
|
276
|
+
Commands and prompts from here will be recorded to create your workflow.
|
|
277
|
+
|
|
278
|
+
**Instructions:**
|
|
279
|
+
• Run commands normally - they'll be saved as command steps
|
|
280
|
+
• Type AI prompts - they'll be saved as instruction steps
|
|
281
|
+
• Run \`/workflow new learn-workflow\` again when you're done
|
|
282
|
+
|
|
283
|
+
Recording will begin with your next input.`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Check if workflow learning is currently active
|
|
288
|
+
*/
|
|
289
|
+
isWorkflowLearningActive() {
|
|
290
|
+
return this.workflowLearningActive;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Record a step during workflow learning mode
|
|
294
|
+
* @param type Type of step (command or instruction)
|
|
295
|
+
* @param content The command or instruction content
|
|
296
|
+
*/
|
|
297
|
+
recordWorkflowStep(type, content) {
|
|
298
|
+
if (this.workflowLearningActive && content.trim()) {
|
|
299
|
+
this.learnedWorkflowSteps.push({ type, content: content.trim() });
|
|
300
|
+
quickLog(`[${new Date().toISOString()}] [WorkflowLearning] Recorded ${type}: ${content.trim()}\n`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Cancel workflow learning mode with a message
|
|
305
|
+
* Used when conflicting commands are run during learning (e.g., /exit, /clear, /workflow run)
|
|
306
|
+
* @param reason The reason for cancellation
|
|
307
|
+
* @returns Message to display to user, or empty string if not in learning mode
|
|
308
|
+
*/
|
|
309
|
+
cancelWorkflowLearning(reason) {
|
|
310
|
+
if (!this.workflowLearningActive) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
this.workflowLearningActive = false;
|
|
314
|
+
const stepsCount = this.learnedWorkflowSteps.length;
|
|
315
|
+
this.learnedWorkflowSteps = [];
|
|
316
|
+
quickLog(`[${new Date().toISOString()}] [WorkflowLearning] Cancelled: ${reason} (${stepsCount} steps discarded)\n`);
|
|
317
|
+
return `⚠️ **Learning mode cancelled**: ${reason}\n\n` +
|
|
318
|
+
`${stepsCount > 0 ? `${stepsCount} recorded step(s) were discarded.` : 'No steps were recorded.'}\n` +
|
|
319
|
+
`Run \`/workflow new learn-workflow\` to start again.`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Build a prompt for the AI to execute a workflow
|
|
323
|
+
* This formats the workflow steps into a clear instruction set
|
|
324
|
+
*/
|
|
325
|
+
buildWorkflowPrompt(workflow) {
|
|
326
|
+
const stepsText = workflow.steps.map((step, index) => {
|
|
327
|
+
const stepNum = index + 1;
|
|
328
|
+
if (step.type === 'command') {
|
|
329
|
+
return `Step ${stepNum}: [RUN COMMAND] ${step.content}`;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
return `Step ${stepNum}: [INSTRUCTION] ${step.content}`;
|
|
333
|
+
}
|
|
334
|
+
}).join('\n');
|
|
335
|
+
return `Execute the following workflow "${workflow.name}" step by step:
|
|
336
|
+
|
|
337
|
+
${stepsText}
|
|
338
|
+
|
|
339
|
+
IMPORTANT:
|
|
340
|
+
- For [RUN COMMAND] steps, execute the exact command shown using the execute_command tool.
|
|
341
|
+
- For [INSTRUCTION] steps, follow the instruction using appropriate tools.
|
|
342
|
+
- Execute each step in order, waiting for completion before moving to the next.
|
|
343
|
+
- If any step fails, stop and report the error.
|
|
344
|
+
- After all steps complete successfully, summarize what was done.
|
|
345
|
+
|
|
346
|
+
Begin executing now, starting with Step 1.`;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Warpify a detected SSH/WSL/Docker session.
|
|
350
|
+
* This establishes a proper ssh2/handler connection (will prompt for password)
|
|
351
|
+
* so commands can be executed via the handler, not just PTY passthrough.
|
|
352
|
+
*
|
|
353
|
+
* @param command - The original command (e.g., "ssh rohan@localhost")
|
|
354
|
+
* @param type - The detected session type
|
|
355
|
+
* @param connectionString - The connection string (e.g., "rohan@localhost")
|
|
356
|
+
* @returns Promise<boolean> - true if warpify succeeded
|
|
357
|
+
*/
|
|
358
|
+
async warpifySession(command, type, connectionString) {
|
|
359
|
+
try {
|
|
360
|
+
// Show connecting status
|
|
361
|
+
if (this.onConnectionStatusUpdate) {
|
|
362
|
+
this.onConnectionStatusUpdate({
|
|
363
|
+
type: type,
|
|
364
|
+
status: 'connecting',
|
|
365
|
+
connectionString: connectionString
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Detect and connect using the command
|
|
369
|
+
const detection = this.commandDetector.detect(command);
|
|
370
|
+
if (!detection) {
|
|
371
|
+
throw new Error(`Could not detect handler for command: ${command}`);
|
|
372
|
+
}
|
|
373
|
+
// Check if we are already in a remote session
|
|
374
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
375
|
+
let context;
|
|
376
|
+
// Save current CWD to stack BEFORE entering any new session (local or nested)
|
|
377
|
+
// This enables proper restoration when exiting, regardless of nesting level
|
|
378
|
+
this.cwdStack.push(this.cwd);
|
|
379
|
+
if (currentContext.type !== 'local') {
|
|
380
|
+
// Nested session: connect from remote
|
|
381
|
+
if (detection.handler.connectFromRemote) {
|
|
382
|
+
quickLog(`[${new Date().toISOString()}] [Warpify] nesting connection: ${currentContext.type} -> ${type}\n`);
|
|
383
|
+
context = await detection.handler.connectFromRemote(command, this.cwd, currentContext);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// If nested connection fails, pop the CWD we just pushed
|
|
387
|
+
this.cwdStack.pop();
|
|
388
|
+
throw new Error(`Nested connections are not supported by the ${type} handler yet.`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Local session: connect from local
|
|
393
|
+
// For SSH, we shouldn't pass the local (potentially Windows) CWD as it won't exist remotely.
|
|
394
|
+
// For local->WSL or local->Docker, the handler might translate it, but strictly speaking
|
|
395
|
+
// starting 'fresh' in the remote user's home/default dir is safer for 'warpify' semantics regardless of type.
|
|
396
|
+
// However, to avoid regression for WSL/Docker usage where inheritance is expected, we restricts this fix to SSH.
|
|
397
|
+
const initialCwd = type === 'ssh' ? undefined : this.cwd;
|
|
398
|
+
context = await detection.handler.connect(command, initialCwd);
|
|
399
|
+
}
|
|
400
|
+
this.lastConnectionCommand = command;
|
|
401
|
+
this.contextManager.pushContext(context);
|
|
402
|
+
// Explicitly sync this.cwd with the new context's CWD to ensure consistency immediately
|
|
403
|
+
if (context.metadata.workingDirectory) {
|
|
404
|
+
this.cwd = context.metadata.workingDirectory;
|
|
405
|
+
}
|
|
406
|
+
// Set up disconnect callback for remote connections
|
|
407
|
+
if (context.handler && context.handler.setDisconnectCallback) {
|
|
408
|
+
context.handler.setDisconnectCallback((error) => {
|
|
409
|
+
this.handleRemoteDisconnect(connectionString || '', type, error);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
// Show success
|
|
413
|
+
if (this.onConnectionStatusUpdate) {
|
|
414
|
+
this.onConnectionStatusUpdate({
|
|
415
|
+
type: type,
|
|
416
|
+
status: 'connected',
|
|
417
|
+
connectionString: connectionString
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// Update CWD to remote working directory
|
|
421
|
+
if (this.onCwdChange && context.metadata.workingDirectory) {
|
|
422
|
+
this.onCwdChange(context.metadata.workingDirectory);
|
|
423
|
+
}
|
|
424
|
+
quickLog(`[${new Date().toISOString()}] [Warpify] Successfully established ${type} connection via ssh2/handler\n`);
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
// Connection failed
|
|
429
|
+
if (this.onConnectionStatusUpdate) {
|
|
430
|
+
this.onConnectionStatusUpdate({
|
|
431
|
+
type: type,
|
|
432
|
+
status: 'error',
|
|
433
|
+
connectionString: connectionString,
|
|
434
|
+
error: error.message
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
quickLog(`[${new Date().toISOString()}] [Warpify] Failed to establish connection: ${error.message}\n`);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Exit the current remote session and return to parent environment.
|
|
443
|
+
* Called by the AI agent's enter_remote_session tool with action="exit".
|
|
444
|
+
* @returns Promise<boolean> - true if there was a session to exit
|
|
445
|
+
*/
|
|
446
|
+
async exitRemoteSession() {
|
|
447
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
448
|
+
// Check if we're in a remote session
|
|
449
|
+
if (currentContext.type === 'local') {
|
|
450
|
+
return false; // No remote session to exit
|
|
451
|
+
}
|
|
452
|
+
// Get connection info for logging
|
|
453
|
+
const sessionType = currentContext.type;
|
|
454
|
+
const metadata = currentContext.metadata;
|
|
455
|
+
const connectionString = metadata.connectionString ||
|
|
456
|
+
metadata.host ||
|
|
457
|
+
metadata.containerId ||
|
|
458
|
+
metadata.distro ||
|
|
459
|
+
metadata.workingDirectory ||
|
|
460
|
+
'unknown';
|
|
461
|
+
quickLog(`[${new Date().toISOString()}] [ExitRemoteSession] Exiting ${sessionType} session: ${connectionString}\n`);
|
|
462
|
+
// Close the handler if it has a disconnect method
|
|
463
|
+
if (currentContext.handler && typeof currentContext.handler.disconnect === 'function') {
|
|
464
|
+
try {
|
|
465
|
+
await currentContext.handler.disconnect();
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
quickLog(`[${new Date().toISOString()}] [ExitRemoteSession] Error during handler disconnect: ${error.message}\n`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Pop the context
|
|
472
|
+
this.contextManager.popContext();
|
|
473
|
+
// Restore CWD from stack
|
|
474
|
+
const previousCwd = this.cwdStack.pop();
|
|
475
|
+
const newContext = this.contextManager.getCurrentContext();
|
|
476
|
+
if (previousCwd) {
|
|
477
|
+
this.cwd = previousCwd;
|
|
478
|
+
}
|
|
479
|
+
else if (newContext.type !== 'local') {
|
|
480
|
+
this.cwd = newContext.metadata.workingDirectory;
|
|
481
|
+
}
|
|
482
|
+
this.contextManager.updateWorkingDirectory(this.cwd);
|
|
483
|
+
if (newContext.type === 'local') {
|
|
484
|
+
this.lastConnectionCommand = null;
|
|
485
|
+
}
|
|
486
|
+
// Notify CWD change
|
|
487
|
+
if (this.onCwdChange) {
|
|
488
|
+
this.onCwdChange(this.cwd);
|
|
489
|
+
}
|
|
490
|
+
// Save chat state
|
|
491
|
+
this.saveCurrentChat();
|
|
492
|
+
// Update UI connection status
|
|
493
|
+
if (this.onConnectionStatusUpdate) {
|
|
494
|
+
if (newContext.type === 'local') {
|
|
495
|
+
this.onConnectionStatusUpdate({
|
|
496
|
+
type: sessionType,
|
|
497
|
+
status: 'disconnected',
|
|
498
|
+
connectionString: connectionString
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
// Still in a nested session
|
|
503
|
+
const newMetadata = newContext.metadata;
|
|
504
|
+
this.onConnectionStatusUpdate({
|
|
505
|
+
type: newContext.type,
|
|
506
|
+
status: 'connected',
|
|
507
|
+
connectionString: newMetadata.connectionString || newMetadata.host || newMetadata.workingDirectory
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Handle disconnection from a remote session
|
|
515
|
+
*/
|
|
516
|
+
handleRemoteDisconnect(connectionString, type, error) {
|
|
517
|
+
quickLog(`[${new Date().toISOString()}] [Warpify] Disconnected from ${type} session: ${connectionString} (${error || 'clean exit'})\n`);
|
|
518
|
+
// Pop the context (this was the session that just ended)
|
|
519
|
+
const endedContext = this.contextManager.popContext();
|
|
520
|
+
// Now look at the NEW current context (the parent)
|
|
521
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
522
|
+
// Restore CWD from stack - this handles any nesting level
|
|
523
|
+
const previousCwd = this.cwdStack.pop();
|
|
524
|
+
if (previousCwd) {
|
|
525
|
+
this.cwd = previousCwd;
|
|
526
|
+
}
|
|
527
|
+
else if (currentContext.type !== 'local') {
|
|
528
|
+
// Fallback: use parent context's CWD if no stack entry
|
|
529
|
+
this.cwd = currentContext.metadata.workingDirectory;
|
|
530
|
+
}
|
|
531
|
+
this.contextManager.updateWorkingDirectory(this.cwd);
|
|
532
|
+
if (currentContext.type === 'local') {
|
|
533
|
+
this.lastConnectionCommand = null;
|
|
534
|
+
}
|
|
535
|
+
// Notify CWD change
|
|
536
|
+
if (this.onCwdChange) {
|
|
537
|
+
this.onCwdChange(this.cwd);
|
|
538
|
+
}
|
|
539
|
+
// Save chat state
|
|
540
|
+
this.saveCurrentChat();
|
|
541
|
+
// Update UI connection status
|
|
542
|
+
if (this.onConnectionStatusUpdate) {
|
|
543
|
+
if (currentContext.type === 'local') {
|
|
544
|
+
this.onConnectionStatusUpdate({
|
|
545
|
+
type: type,
|
|
546
|
+
status: 'disconnected',
|
|
547
|
+
connectionString
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
// Update status to reflect the parent session
|
|
552
|
+
let parentConnString = '';
|
|
553
|
+
if (currentContext.type === 'ssh') {
|
|
554
|
+
parentConnString = `${currentContext.metadata.username}@${currentContext.metadata.hostname}`;
|
|
555
|
+
}
|
|
556
|
+
else if (currentContext.type === 'wsl') {
|
|
557
|
+
parentConnString = currentContext.metadata.distroName || '';
|
|
558
|
+
}
|
|
559
|
+
else if (currentContext.type === 'docker') {
|
|
560
|
+
parentConnString = currentContext.metadata.containerId || '';
|
|
561
|
+
}
|
|
562
|
+
this.onConnectionStatusUpdate({
|
|
563
|
+
type: currentContext.type,
|
|
564
|
+
status: 'connected',
|
|
565
|
+
connectionString: parentConnString
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// If there's an active tool execution (shell running), mark it as error
|
|
570
|
+
if (this.onToolExecutionUpdate && this.currentInteractiveProcess) {
|
|
571
|
+
this.onToolExecutionUpdate({
|
|
572
|
+
toolName: 'execute_command',
|
|
573
|
+
status: 'error',
|
|
574
|
+
error: `Disconnected from ${type}: ${error || 'Connection lost'}`
|
|
575
|
+
});
|
|
576
|
+
this.currentInteractiveProcess = undefined;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
220
579
|
/**
|
|
221
580
|
* Calculate and update token count based on current conversation history
|
|
222
581
|
* This ensures UI is always in sync with the actual AI context
|
|
@@ -441,25 +800,48 @@ export class CentaurusCLI {
|
|
|
441
800
|
}
|
|
442
801
|
async handlePickerSelection(selection, pickerType) {
|
|
443
802
|
try {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
803
|
+
if (pickerType === 'local-model') {
|
|
804
|
+
// Local Ollama model selection
|
|
805
|
+
// Selection is the model name (e.g., "llama3:latest")
|
|
806
|
+
const modelName = selection;
|
|
807
|
+
// Store the local model configuration
|
|
808
|
+
this.configManager.set('model', modelName);
|
|
809
|
+
this.configManager.set('modelName', modelName);
|
|
810
|
+
this.configManager.set('isLocalModel', true);
|
|
811
|
+
// Notify UI of model name change
|
|
812
|
+
// Local models don't have a fixed context window, use a reasonable default
|
|
813
|
+
if (this.onModelChange) {
|
|
814
|
+
this.onModelChange(modelName, 128000); // Most local models have 128k context
|
|
815
|
+
}
|
|
816
|
+
const responseMessage = `✅ Switched to local Ollama model: ${modelName}`;
|
|
817
|
+
// Send response back to UI
|
|
818
|
+
if (this.onResponseCallback) {
|
|
819
|
+
this.onResponseCallback(responseMessage);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
// Cloud model selection (existing behavior)
|
|
824
|
+
// Selection is the index of the model in models array from backend
|
|
825
|
+
const modelsConfig = await fetchModelsConfig();
|
|
826
|
+
const modelIndex = parseInt(selection, 10);
|
|
827
|
+
if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= modelsConfig.models.length) {
|
|
828
|
+
throw new Error('Invalid model selection');
|
|
829
|
+
}
|
|
830
|
+
const selectedModel = modelsConfig.models[modelIndex];
|
|
831
|
+
// Store only the model ID and name (not the full config with thinkingConfig)
|
|
832
|
+
// This prevents caching issues when we update model configs
|
|
833
|
+
this.configManager.set('model', selectedModel.id);
|
|
834
|
+
this.configManager.set('modelName', selectedModel.name);
|
|
835
|
+
this.configManager.set('isLocalModel', false);
|
|
836
|
+
// Notify UI of model name change and contextWindow
|
|
837
|
+
if (this.onModelChange) {
|
|
838
|
+
this.onModelChange(selectedModel.name, selectedModel.contextWindow);
|
|
839
|
+
}
|
|
840
|
+
const responseMessage = `✅ Switched to cloud model: ${selectedModel.name}`;
|
|
841
|
+
// Send response back to UI
|
|
842
|
+
if (this.onResponseCallback) {
|
|
843
|
+
this.onResponseCallback(responseMessage);
|
|
844
|
+
}
|
|
463
845
|
}
|
|
464
846
|
}
|
|
465
847
|
catch (error) {
|
|
@@ -474,6 +856,11 @@ export class CentaurusCLI {
|
|
|
474
856
|
* Notify UI about tool execution status
|
|
475
857
|
*/
|
|
476
858
|
notifyToolStatus(toolName, status, args, result, error) {
|
|
859
|
+
// Skip UI status updates for enter_remote_session - the password prompt
|
|
860
|
+
// and "Established Wormhole" message already provide sufficient feedback
|
|
861
|
+
if (toolName === 'enter_remote_session') {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
477
864
|
if (this.onToolExecutionUpdate) {
|
|
478
865
|
// Get current context for remote prefix
|
|
479
866
|
const currentContext = this.contextManager.getCurrentContext();
|
|
@@ -550,6 +937,8 @@ export class CentaurusCLI {
|
|
|
550
937
|
this.toolRegistry.register(createImageTool);
|
|
551
938
|
this.toolRegistry.register(backgroundCommandTool);
|
|
552
939
|
this.toolRegistry.register(subAgentTool);
|
|
940
|
+
this.toolRegistry.register(enterRemoteSessionTool);
|
|
941
|
+
this.toolRegistry.register(workflowTool);
|
|
553
942
|
// Initialize SubAgentManager with tool registry
|
|
554
943
|
SubAgentManager.initialize(this.toolRegistry);
|
|
555
944
|
SubAgentManager.setOnSubAgentCountChange((count) => {
|
|
@@ -659,41 +1048,69 @@ Press Enter to continue...
|
|
|
659
1048
|
}
|
|
660
1049
|
/**
|
|
661
1050
|
* Clean up orphaned tool_calls from conversation history.
|
|
662
|
-
* This
|
|
663
|
-
*
|
|
1051
|
+
* This validates the ENTIRE history and removes any assistant messages
|
|
1052
|
+
* where tool_calls don't have matching tool result messages.
|
|
1053
|
+
*
|
|
1054
|
+
* Vertex AI / Claude APIs require that every assistant message with tool_calls
|
|
1055
|
+
* has matching tool result messages immediately following it.
|
|
664
1056
|
*/
|
|
665
1057
|
cleanupOrphanedToolCalls() {
|
|
666
1058
|
if (this.conversationHistory.length === 0)
|
|
667
1059
|
return;
|
|
668
|
-
|
|
669
|
-
let
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1060
|
+
let cleanedAny = false;
|
|
1061
|
+
let iterations = 0;
|
|
1062
|
+
const maxIterations = 20; // Safety limit to prevent infinite loops
|
|
1063
|
+
// Keep cleaning until no more orphans are found
|
|
1064
|
+
// (removing one orphan may expose another)
|
|
1065
|
+
while (iterations < maxIterations) {
|
|
1066
|
+
iterations++;
|
|
1067
|
+
let foundOrphan = false;
|
|
1068
|
+
// Scan through history to find ALL assistant messages with tool_calls
|
|
1069
|
+
for (let i = 0; i < this.conversationHistory.length; i++) {
|
|
1070
|
+
const msg = this.conversationHistory[i];
|
|
1071
|
+
if (msg.role !== 'assistant' || !msg.tool_calls || msg.tool_calls.length === 0) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
// Collect all tool_call IDs from this assistant message
|
|
1075
|
+
const expectedToolCallIds = new Set(msg.tool_calls.map((tc) => tc.id));
|
|
1076
|
+
// Check if ALL tool_calls have matching tool result messages after this message
|
|
1077
|
+
// Tool results must come AFTER the assistant message, before the next user/assistant message
|
|
1078
|
+
let j = i + 1;
|
|
1079
|
+
while (j < this.conversationHistory.length) {
|
|
1080
|
+
const nextMsg = this.conversationHistory[j];
|
|
1081
|
+
// If we hit a user or assistant message, stop looking for tool results
|
|
1082
|
+
if (nextMsg.role === 'user' || nextMsg.role === 'assistant') {
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
// If it's a tool result, check if it matches one of our expected IDs
|
|
1086
|
+
if (nextMsg.role === 'tool' && nextMsg.tool_call_id) {
|
|
1087
|
+
expectedToolCallIds.delete(nextMsg.tool_call_id);
|
|
1088
|
+
}
|
|
1089
|
+
j++;
|
|
1090
|
+
}
|
|
1091
|
+
// If there are still unmatched tool_calls, this is an orphan
|
|
1092
|
+
if (expectedToolCallIds.size > 0) {
|
|
1093
|
+
try {
|
|
1094
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Found orphaned tool_calls at index ${i}: ${Array.from(expectedToolCallIds).join(', ')}\n`);
|
|
1095
|
+
}
|
|
1096
|
+
catch (e) { }
|
|
1097
|
+
// Remove this assistant message and all tool results up to (but not including) the next user/assistant message
|
|
1098
|
+
const removeCount = j - i;
|
|
1099
|
+
this.conversationHistory.splice(i, removeCount);
|
|
1100
|
+
foundOrphan = true;
|
|
1101
|
+
cleanedAny = true;
|
|
1102
|
+
break; // Restart scan from beginning since indices changed
|
|
1103
|
+
}
|
|
675
1104
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return; // No assistant messages with tool_calls
|
|
679
|
-
const assistantMsg = this.conversationHistory[lastAssistantWithToolCallsIndex];
|
|
680
|
-
const toolCallIds = new Set(assistantMsg.tool_calls.map((tc) => tc.id));
|
|
681
|
-
// Check if all tool_calls have matching tool result messages after this assistant message
|
|
682
|
-
for (let i = lastAssistantWithToolCallsIndex + 1; i < this.conversationHistory.length; i++) {
|
|
683
|
-
const msg = this.conversationHistory[i];
|
|
684
|
-
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
685
|
-
toolCallIds.delete(msg.tool_call_id);
|
|
1105
|
+
if (!foundOrphan) {
|
|
1106
|
+
break; // No more orphans found, we're done
|
|
686
1107
|
}
|
|
687
1108
|
}
|
|
688
|
-
|
|
689
|
-
if (toolCallIds.size > 0) {
|
|
690
|
-
// Log the cleanup for debugging
|
|
1109
|
+
if (cleanedAny) {
|
|
691
1110
|
try {
|
|
692
|
-
quickLog(`[${new Date().toISOString()}] [CLI]
|
|
1111
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Completed history cleanup after ${iterations} iteration(s), ${this.conversationHistory.length} messages remaining\n`);
|
|
693
1112
|
}
|
|
694
1113
|
catch (e) { }
|
|
695
|
-
// Remove the orphaned assistant message and any partial tool results after it
|
|
696
|
-
this.conversationHistory.splice(lastAssistantWithToolCallsIndex);
|
|
697
1114
|
}
|
|
698
1115
|
}
|
|
699
1116
|
/**
|
|
@@ -717,14 +1134,22 @@ Press Enter to continue...
|
|
|
717
1134
|
}
|
|
718
1135
|
}
|
|
719
1136
|
}
|
|
720
|
-
async handleMessage(message) {
|
|
1137
|
+
async handleMessage(message, options = {}) {
|
|
721
1138
|
// Handle command mode - execute commands directly
|
|
722
1139
|
if (this.commandMode) {
|
|
1140
|
+
// Record command step if workflow learning mode is active
|
|
1141
|
+
if (this.workflowLearningActive) {
|
|
1142
|
+
this.recordWorkflowStep('command', message);
|
|
1143
|
+
}
|
|
723
1144
|
await this.handleCommandModeExecution(message);
|
|
724
1145
|
return;
|
|
725
1146
|
}
|
|
726
1147
|
// Handle background mode - execute commands in background
|
|
727
1148
|
if (this.backgroundMode) {
|
|
1149
|
+
// Record command step if workflow learning mode is active
|
|
1150
|
+
if (this.workflowLearningActive) {
|
|
1151
|
+
this.recordWorkflowStep('command', message);
|
|
1152
|
+
}
|
|
728
1153
|
this.handleBackgroundModeExecution(message);
|
|
729
1154
|
return;
|
|
730
1155
|
}
|
|
@@ -733,6 +1158,11 @@ Press Enter to continue...
|
|
|
733
1158
|
await this.handleSlashCommand(message);
|
|
734
1159
|
return;
|
|
735
1160
|
}
|
|
1161
|
+
// Record step if workflow learning mode is active
|
|
1162
|
+
// AI prompts are recorded as instruction steps
|
|
1163
|
+
if (this.workflowLearningActive) {
|
|
1164
|
+
this.recordWorkflowStep('instruction', message);
|
|
1165
|
+
}
|
|
736
1166
|
// Check authentication
|
|
737
1167
|
if (!apiClient.isAuthenticated()) {
|
|
738
1168
|
throw new Error('Authentication required. Please sign in to use AI features.');
|
|
@@ -780,8 +1210,27 @@ Press Enter to continue...
|
|
|
780
1210
|
// Cancel any active request when a new message comes in
|
|
781
1211
|
// This enables "interrupt and replace" - new message takes priority
|
|
782
1212
|
if (this.currentAbortController) {
|
|
783
|
-
|
|
784
|
-
this.
|
|
1213
|
+
// Mark as intentionally aborted so error handling knows not to throw or show message
|
|
1214
|
+
this.requestIntentionallyAborted = true;
|
|
1215
|
+
const oldController = this.currentAbortController;
|
|
1216
|
+
// Create new controller BEFORE aborting old one to avoid race condition
|
|
1217
|
+
// where new request tries to access undefined controller
|
|
1218
|
+
this.currentAbortController = new AbortController();
|
|
1219
|
+
oldController.abort();
|
|
1220
|
+
// Clean up orphaned tool calls from the interrupted turn
|
|
1221
|
+
this.cleanupOrphanedToolCalls();
|
|
1222
|
+
// Remove the last user message from history (it's being replaced by the new message)
|
|
1223
|
+
// Walk backwards and remove messages until we find and remove a user message
|
|
1224
|
+
while (this.conversationHistory.length > 0) {
|
|
1225
|
+
const lastMsg = this.conversationHistory[this.conversationHistory.length - 1];
|
|
1226
|
+
this.conversationHistory.pop();
|
|
1227
|
+
if (lastMsg.role === 'user') {
|
|
1228
|
+
// Found and removed the interrupted user message, stop here
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
// Continue removing assistant/tool messages that were part of the interrupted turn
|
|
1232
|
+
}
|
|
1233
|
+
quickLog(`[${new Date().toISOString()}] [handleMessage] Interrupted active request - cleaned up history for replacement\n`);
|
|
785
1234
|
}
|
|
786
1235
|
// Store original request if in planning mode (for execution phase after approval)
|
|
787
1236
|
if (this.planMode && !this.pendingPlanRequest) {
|
|
@@ -816,6 +1265,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
816
1265
|
role: 'user',
|
|
817
1266
|
content: userMessageContent,
|
|
818
1267
|
});
|
|
1268
|
+
// Calculate start index for AI context (0 for normal, current index for isolated workflow)
|
|
1269
|
+
const contextStartIndex = options.isolatedWorkflow ? this.conversationHistory.length - 1 : 0;
|
|
1270
|
+
// Helper to get messages for AI context respecting isolation
|
|
1271
|
+
const getMessagesForContext = () => {
|
|
1272
|
+
if (options.isolatedWorkflow) {
|
|
1273
|
+
return this.conversationHistory.slice(contextStartIndex);
|
|
1274
|
+
}
|
|
1275
|
+
return [...this.conversationHistory];
|
|
1276
|
+
};
|
|
819
1277
|
// Messages are stored locally only - no backend persistence needed
|
|
820
1278
|
// Local storage is handled by saveCurrentChat() which saves to ~/.centaurus/chats/
|
|
821
1279
|
// Start logging session and log user message
|
|
@@ -857,7 +1315,10 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
857
1315
|
// Build messages array WITHOUT system prompt - backend will inject it
|
|
858
1316
|
// The backend uses cli-system-prompt.md for CLI clients
|
|
859
1317
|
// We pass environmentContext and mode separately so backend can inject them
|
|
860
|
-
|
|
1318
|
+
// SAFETY: Clean up any orphaned tool calls before making AI request
|
|
1319
|
+
// This prevents "improperly formed request" errors from corrupted history
|
|
1320
|
+
this.cleanupOrphanedToolCalls();
|
|
1321
|
+
let messages = getMessagesForContext();
|
|
861
1322
|
// Inject subshell context if in a subshell environment
|
|
862
1323
|
const currentContext = this.contextManager.getCurrentContext();
|
|
863
1324
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -879,9 +1340,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
879
1340
|
// ANTI-LOOP: Track ALL duplicate tool calls (not just file ops)
|
|
880
1341
|
const toolCallTracker = new Map(); // Hash -> count
|
|
881
1342
|
const MAX_IDENTICAL_TOOL_CALLS = 3; // Max times exact same tool call allowed
|
|
882
|
-
// Create AbortController for this request
|
|
883
|
-
this.currentAbortController
|
|
884
|
-
|
|
1343
|
+
// Create AbortController for this request (if not already created during interruption handling)
|
|
1344
|
+
if (!this.currentAbortController) {
|
|
1345
|
+
this.currentAbortController = new AbortController();
|
|
1346
|
+
}
|
|
1347
|
+
// Note: Don't reset requestIntentionallyAborted here - let the error handler reset it
|
|
1348
|
+
// to avoid race condition where old request's error handler sees false
|
|
885
1349
|
// Clean up any orphaned tool_calls from a previous aborted request
|
|
886
1350
|
// This prevents 400 Bad Request errors when sending to the backend
|
|
887
1351
|
this.cleanupOrphanedToolCalls();
|
|
@@ -960,7 +1424,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
960
1424
|
});
|
|
961
1425
|
// Stream AI response from backend
|
|
962
1426
|
// Backend will inject system prompt automatically with environment context
|
|
963
|
-
for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController
|
|
1427
|
+
for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController?.signal)) {
|
|
964
1428
|
// Handle error chunks
|
|
965
1429
|
if (chunk.type === 'error') {
|
|
966
1430
|
// Check if this is an abort situation (user cancelled or sent new message)
|
|
@@ -1303,7 +1767,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1303
1767
|
// Clear pending plan request
|
|
1304
1768
|
this.pendingPlanRequest = null;
|
|
1305
1769
|
// Update messages array for this turn
|
|
1306
|
-
messages =
|
|
1770
|
+
messages = getMessagesForContext();
|
|
1307
1771
|
// Continue the loop - AI will now execute with plan context
|
|
1308
1772
|
continue;
|
|
1309
1773
|
}
|
|
@@ -1368,7 +1832,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1368
1832
|
});
|
|
1369
1833
|
// Mark this tool call as handled so it's not duplicated
|
|
1370
1834
|
handledToolCallIds.add(toolCall.id);
|
|
1371
|
-
messages =
|
|
1835
|
+
messages = getMessagesForContext();
|
|
1372
1836
|
}
|
|
1373
1837
|
}
|
|
1374
1838
|
else {
|
|
@@ -1392,7 +1856,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1392
1856
|
});
|
|
1393
1857
|
// Mark this tool call as handled so it's not duplicated
|
|
1394
1858
|
handledToolCallIds.add(toolCall.id);
|
|
1395
|
-
messages =
|
|
1859
|
+
messages = getMessagesForContext();
|
|
1396
1860
|
}
|
|
1397
1861
|
continue;
|
|
1398
1862
|
}
|
|
@@ -1459,7 +1923,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1459
1923
|
// Mark as handled
|
|
1460
1924
|
handledToolCallIds.add(toolCall.id);
|
|
1461
1925
|
// Update messages and continue
|
|
1462
|
-
messages =
|
|
1926
|
+
messages = getMessagesForContext();
|
|
1463
1927
|
continue;
|
|
1464
1928
|
}
|
|
1465
1929
|
}
|
|
@@ -1801,7 +2265,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1801
2265
|
// Rebuild messages array with updated history
|
|
1802
2266
|
// During agent loop: keep ALL thinking for current task
|
|
1803
2267
|
// (Thinking from previous tasks was already stripped at request start)
|
|
1804
|
-
messages =
|
|
2268
|
+
messages = getMessagesForContext();
|
|
1805
2269
|
// No need to reset currentTurnThinking - keep accumulating for the task
|
|
1806
2270
|
// Re-inject subshell context
|
|
1807
2271
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -1844,7 +2308,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1844
2308
|
}
|
|
1845
2309
|
// Rebuild messages array with updated history
|
|
1846
2310
|
// Backend will inject system prompt
|
|
1847
|
-
messages =
|
|
2311
|
+
messages = getMessagesForContext();
|
|
1848
2312
|
// Re-inject subshell context
|
|
1849
2313
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1850
2314
|
// Continue loop to get AI's response (removed 500ms delay for faster response)
|
|
@@ -1865,7 +2329,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1865
2329
|
});
|
|
1866
2330
|
// Rebuild messages array with updated history
|
|
1867
2331
|
// Backend will inject system prompt
|
|
1868
|
-
messages =
|
|
2332
|
+
messages = getMessagesForContext();
|
|
1869
2333
|
// Re-inject subshell context
|
|
1870
2334
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1871
2335
|
// Add delay before prompting
|
|
@@ -1910,8 +2374,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1910
2374
|
catch (error) {
|
|
1911
2375
|
// Log the error
|
|
1912
2376
|
conversationLogger.logError('handleMessage', error);
|
|
1913
|
-
// Check if this was an abort/cancellation
|
|
1914
|
-
if (error.name === 'AbortError' || error.message?.includes('aborted')) {
|
|
2377
|
+
// Check if this was an abort/cancellation (including timeout errors from aborted requests)
|
|
2378
|
+
if (error.name === 'AbortError' || error.message?.includes('aborted') || error.message?.includes('timed out') || this.requestIntentionallyAborted) {
|
|
2379
|
+
// If intentionally aborted for replacement by new message, return silently
|
|
2380
|
+
// The new message will take over - no need to show cancellation message
|
|
2381
|
+
if (this.requestIntentionallyAborted) {
|
|
2382
|
+
this.requestIntentionallyAborted = false;
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
1915
2385
|
conversationLogger.logError('handleMessage', new Error('Request cancelled by user'));
|
|
1916
2386
|
if (this.onResponseCallback) {
|
|
1917
2387
|
this.onResponseCallback('⚠️ Request cancelled by user.');
|
|
@@ -2193,6 +2663,11 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2193
2663
|
break;
|
|
2194
2664
|
case 'logout':
|
|
2195
2665
|
try {
|
|
2666
|
+
// Cancel workflow learning if active before logout
|
|
2667
|
+
const logoutCancelMsg = this.cancelWorkflowLearning('Logging out');
|
|
2668
|
+
if (logoutCancelMsg && this.onDirectMessageCallback) {
|
|
2669
|
+
this.onDirectMessageCallback(logoutCancelMsg);
|
|
2670
|
+
}
|
|
2196
2671
|
await apiClient.logout();
|
|
2197
2672
|
responseMessage = '✅ Logged out successfully.\n\n' +
|
|
2198
2673
|
'Your session has been cleared.\n' +
|
|
@@ -2271,6 +2746,11 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2271
2746
|
'• Manual completion detection';
|
|
2272
2747
|
break;
|
|
2273
2748
|
case 'clear':
|
|
2749
|
+
// Cancel workflow learning if active before clearing
|
|
2750
|
+
const clearCancelMsg = this.cancelWorkflowLearning('Clearing chat session');
|
|
2751
|
+
if (clearCancelMsg && this.onDirectMessageCallback) {
|
|
2752
|
+
this.onDirectMessageCallback(clearCancelMsg);
|
|
2753
|
+
}
|
|
2274
2754
|
// Start a new chat session (clears history and generates new chat ID)
|
|
2275
2755
|
this.startNewChat();
|
|
2276
2756
|
// Don't send any response message - the UI will handle clearing
|
|
@@ -2323,43 +2803,126 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2323
2803
|
`Model: ${config.model || 'gemini-2.5-flash (default)'}\n` +
|
|
2324
2804
|
`Enhanced Quality: ${config.enhancedQuality !== false ? '✅ Enabled' : '❌ Disabled'}\n` +
|
|
2325
2805
|
`Autonomous Mode: ${config.autonomousMode === true ? '✅ Enabled' : '❌ Disabled'}\n` +
|
|
2806
|
+
`AI Auto-Suggest: ${config.aiAutoSuggest === true ? '✅ Enabled' : '❌ Disabled'}\n` +
|
|
2326
2807
|
`Authentication: ${apiClient.isAuthenticated() ? '✅ Signed in' : '❌ Not signed in'}`;
|
|
2327
2808
|
}
|
|
2328
2809
|
break;
|
|
2810
|
+
case 'settings':
|
|
2811
|
+
if (args.length >= 2 && args[0].toLowerCase() === 'auto-suggest') {
|
|
2812
|
+
// Handle /settings auto-suggest <on/off>
|
|
2813
|
+
const value = args[1].toLowerCase();
|
|
2814
|
+
if (value === 'on') {
|
|
2815
|
+
this.configManager.set('aiAutoSuggest', true);
|
|
2816
|
+
responseMessage = '✅ **AI Auto-Suggestions Enabled**\n\n' +
|
|
2817
|
+
'From now on, I will suggest commands after 5 seconds of inactivity.\n' +
|
|
2818
|
+
'Suggestions will appear in grey text. Use the **Right Arrow** key to accept them.';
|
|
2819
|
+
}
|
|
2820
|
+
else if (value === 'off') {
|
|
2821
|
+
this.configManager.set('aiAutoSuggest', false);
|
|
2822
|
+
responseMessage = '✅ **AI Auto-Suggestions Disabled**\n\n' +
|
|
2823
|
+
'I will no longer provide AI-powered command suggestions.';
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
responseMessage = '❌ Invalid option. Usage: `/settings auto-suggest on` or `/settings auto-suggest off`';
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
responseMessage = '❌ Invalid command format.\n\nUsage:\n- `/settings auto-suggest on`\n- `/settings auto-suggest off`';
|
|
2831
|
+
}
|
|
2832
|
+
break;
|
|
2329
2833
|
case 'model':
|
|
2330
|
-
|
|
2331
|
-
|
|
2834
|
+
case 'models':
|
|
2835
|
+
// Handle subcommands: local, cloud
|
|
2836
|
+
const modelSubCommand = args[0]?.toLowerCase();
|
|
2837
|
+
if (modelSubCommand === 'local') {
|
|
2838
|
+
// Local Ollama models
|
|
2839
|
+
try {
|
|
2840
|
+
// Check if Ollama is running
|
|
2841
|
+
const status = await ollamaService.isOllamaRunning();
|
|
2842
|
+
if (!status.available) {
|
|
2843
|
+
responseMessage = `❌ Cannot connect to Ollama
|
|
2844
|
+
|
|
2845
|
+
${status.error || 'Ollama is not running.'}
|
|
2846
|
+
|
|
2847
|
+
To use local models:
|
|
2848
|
+
1. Install Ollama from: https://ollama.ai
|
|
2849
|
+
2. Start Ollama by running: ollama serve
|
|
2850
|
+
3. Pull a model: ollama pull llama3
|
|
2851
|
+
|
|
2852
|
+
Then try /models local again.`;
|
|
2853
|
+
break;
|
|
2854
|
+
}
|
|
2855
|
+
// Get available local models
|
|
2856
|
+
const localModels = await ollamaService.getLocalModels();
|
|
2857
|
+
if (localModels.length === 0) {
|
|
2858
|
+
responseMessage = `📭 No local models found
|
|
2859
|
+
|
|
2860
|
+
Ollama is running (v${status.version}) but no models are downloaded.
|
|
2861
|
+
|
|
2862
|
+
To download models, run:
|
|
2863
|
+
ollama pull llama3
|
|
2864
|
+
ollama pull codellama
|
|
2865
|
+
ollama pull mistral
|
|
2866
|
+
|
|
2867
|
+
Then try /models local again.`;
|
|
2868
|
+
break;
|
|
2869
|
+
}
|
|
2870
|
+
// Show picker for local model selection
|
|
2871
|
+
if (this.onShowPickerCallback) {
|
|
2872
|
+
const config = this.configManager.load();
|
|
2873
|
+
const currentModelName = config.modelName || '';
|
|
2874
|
+
const isCurrentLocal = config.isLocalModel === true;
|
|
2875
|
+
this.onShowPickerCallback({
|
|
2876
|
+
message: 'Select Local Model (Ollama)',
|
|
2877
|
+
type: 'local-model', // Cast to bypass type check, will be handled in handlePickerSelection
|
|
2878
|
+
choices: localModels.map((model) => {
|
|
2879
|
+
const size = OllamaService.formatModelSize(model.size);
|
|
2880
|
+
const isCurrent = isCurrentLocal && currentModelName === model.name;
|
|
2881
|
+
const supportsTools = OllamaService.modelSupportsTools(model.name);
|
|
2882
|
+
const toolsBadge = supportsTools ? ' [Tools]' : '';
|
|
2883
|
+
return {
|
|
2884
|
+
label: `${model.name} (${size})${toolsBadge}${isCurrent ? ' [CURRENT]' : ''}`,
|
|
2885
|
+
value: model.name
|
|
2886
|
+
};
|
|
2887
|
+
})
|
|
2888
|
+
});
|
|
2889
|
+
return; // Don't send a text response, picker will handle it
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
catch (error) {
|
|
2893
|
+
responseMessage = OllamaService.getHelpfulErrorMessage(error);
|
|
2894
|
+
}
|
|
2895
|
+
break;
|
|
2896
|
+
}
|
|
2897
|
+
if (modelSubCommand === 'cloud' || args.length === 0) {
|
|
2898
|
+
// Cloud models (default behavior when no subcommand or 'cloud' specified)
|
|
2332
2899
|
if (this.onShowPickerCallback) {
|
|
2333
2900
|
const config = this.configManager.load();
|
|
2334
2901
|
const currentModelName = config.modelName || '';
|
|
2902
|
+
const isCurrentCloud = config.isLocalModel !== true;
|
|
2335
2903
|
// Fetch models from backend
|
|
2336
2904
|
const modelsConfig = await fetchModelsConfig();
|
|
2337
2905
|
this.onShowPickerCallback({
|
|
2338
|
-
message: 'Select Model',
|
|
2906
|
+
message: '☁️ Select Cloud Model',
|
|
2339
2907
|
type: 'model',
|
|
2340
|
-
choices: modelsConfig.models.map((modelConfig, index) =>
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2908
|
+
choices: modelsConfig.models.map((modelConfig, index) => {
|
|
2909
|
+
const isCurrent = isCurrentCloud && currentModelName === modelConfig.name;
|
|
2910
|
+
return {
|
|
2911
|
+
label: `${modelConfig.name} - ${modelConfig.description}${isCurrent ? ' [CURRENT]' : ''}`,
|
|
2912
|
+
value: `${index}` // Use index as unique identifier
|
|
2913
|
+
};
|
|
2914
|
+
})
|
|
2344
2915
|
});
|
|
2345
2916
|
return; // Don't send a text response, picker will handle it
|
|
2346
2917
|
}
|
|
2347
2918
|
}
|
|
2348
2919
|
else {
|
|
2349
|
-
//
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
}
|
|
2356
|
-
try {
|
|
2357
|
-
this.configManager.set('model', newModel);
|
|
2358
|
-
responseMessage = `✅ Model changed to: ${newModel}`;
|
|
2359
|
-
}
|
|
2360
|
-
catch (error) {
|
|
2361
|
-
responseMessage = `❌ Failed to set model: ${error.message}`;
|
|
2362
|
-
}
|
|
2920
|
+
// Unrecognized subcommand - show help
|
|
2921
|
+
responseMessage = `Usage: /models [local|cloud]
|
|
2922
|
+
|
|
2923
|
+
/models local - Select from locally installed Ollama models
|
|
2924
|
+
/models cloud - Select from cloud models (Centaurus backend)
|
|
2925
|
+
/models - Default: show cloud models`;
|
|
2363
2926
|
}
|
|
2364
2927
|
break;
|
|
2365
2928
|
case 'mcp':
|
|
@@ -2372,7 +2935,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2372
2935
|
break;
|
|
2373
2936
|
case 'docs':
|
|
2374
2937
|
// Open documentation URL in default browser
|
|
2375
|
-
const docsUrl = 'https://
|
|
2938
|
+
const docsUrl = 'https://centauruslabs.in/docs';
|
|
2376
2939
|
const { exec } = await import('child_process');
|
|
2377
2940
|
const platform = process.platform;
|
|
2378
2941
|
if (platform === 'win32') {
|
|
@@ -2626,6 +3189,163 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2626
3189
|
' /background-task cancel - Cancel a running background task';
|
|
2627
3190
|
}
|
|
2628
3191
|
break;
|
|
3192
|
+
case 'workflow':
|
|
3193
|
+
case 'wf':
|
|
3194
|
+
// Workflow management commands
|
|
3195
|
+
const wfSubCommand = args[0]?.toLowerCase();
|
|
3196
|
+
if (!wfSubCommand) {
|
|
3197
|
+
// Show workflow help
|
|
3198
|
+
responseMessage = `📋 **Workflow Commands:**
|
|
3199
|
+
|
|
3200
|
+
**/workflow new** - Create a new workflow
|
|
3201
|
+
**/workflow list** - List saved workflows
|
|
3202
|
+
**/workflow run <name>** - Run a workflow
|
|
3203
|
+
**/workflow view <name>** - View workflow steps
|
|
3204
|
+
**/workflow delete <name>** - Delete a workflow
|
|
3205
|
+
|
|
3206
|
+
**What are workflows?**
|
|
3207
|
+
Workflows let you save and replay sequences of commands and instructions.
|
|
3208
|
+
Create once, run many times across different machines.`;
|
|
3209
|
+
}
|
|
3210
|
+
else if (wfSubCommand === 'new' || wfSubCommand === 'create') {
|
|
3211
|
+
// Check for nested subcommand (manual or learn-workflow)
|
|
3212
|
+
const newSubCommand = args[1]?.toLowerCase();
|
|
3213
|
+
if (newSubCommand === 'manual') {
|
|
3214
|
+
// If learning mode is active, cancel it instead of starting manual mode
|
|
3215
|
+
if (this.workflowLearningActive) {
|
|
3216
|
+
responseMessage = this.cancelWorkflowLearning('Switching to manual mode');
|
|
3217
|
+
break; // Don't start manual mode, just show cancellation message
|
|
3218
|
+
}
|
|
3219
|
+
// Show workflow creator screen (manual mode)
|
|
3220
|
+
if (this.onShowWorkflowCreatorCallback) {
|
|
3221
|
+
this.onShowWorkflowCreatorCallback();
|
|
3222
|
+
return; // Don't send text response, UI will handle it
|
|
3223
|
+
}
|
|
3224
|
+
else {
|
|
3225
|
+
responseMessage = '❌ Workflow creator not available. Please update the CLI.';
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
else if (newSubCommand === 'learn-workflow') {
|
|
3229
|
+
// Toggle workflow learning mode
|
|
3230
|
+
const result = this.toggleWorkflowLearning();
|
|
3231
|
+
if (result === null) {
|
|
3232
|
+
return; // UI will handle the screen change
|
|
3233
|
+
}
|
|
3234
|
+
responseMessage = result;
|
|
3235
|
+
}
|
|
3236
|
+
else if (newSubCommand) {
|
|
3237
|
+
// Unknown subcommand
|
|
3238
|
+
responseMessage = `❌ Unknown workflow new subcommand: ${newSubCommand}. Use 'manual' or 'learn-workflow'.`;
|
|
3239
|
+
}
|
|
3240
|
+
else {
|
|
3241
|
+
// No subcommand - show help for new options
|
|
3242
|
+
responseMessage = `📋 **Create a New Workflow:**
|
|
3243
|
+
|
|
3244
|
+
**/workflow new manual** - Manually create by typing steps
|
|
3245
|
+
**/workflow new learn-workflow** - Learn from your commands and prompts
|
|
3246
|
+
|
|
3247
|
+
**Manual Mode:** Type each step one at a time, toggle between command/instruction mode.
|
|
3248
|
+
|
|
3249
|
+
**Learn Mode:** Start learning, then run commands and prompts naturally. Run the command again to save.`;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
else if (wfSubCommand === 'list' || wfSubCommand === 'ls') {
|
|
3253
|
+
// List saved workflows
|
|
3254
|
+
const workflows = workflowStorage.list();
|
|
3255
|
+
if (workflows.length === 0) {
|
|
3256
|
+
responseMessage = '📭 No workflows saved yet.\n\nCreate one with: /workflow new';
|
|
3257
|
+
}
|
|
3258
|
+
else {
|
|
3259
|
+
responseMessage = `📋 **Saved Workflows (${workflows.length}):**\n\n`;
|
|
3260
|
+
for (const wf of workflows) {
|
|
3261
|
+
const date = new Date(wf.updatedAt).toLocaleDateString();
|
|
3262
|
+
responseMessage += `• **${wf.name}** (${wf.stepCount} steps)\n`;
|
|
3263
|
+
if (wf.description) {
|
|
3264
|
+
responseMessage += ` ${wf.description}\n`;
|
|
3265
|
+
}
|
|
3266
|
+
responseMessage += ` Last updated: ${date}\n\n`;
|
|
3267
|
+
}
|
|
3268
|
+
responseMessage += 'Use `/workflow run <name>` to execute a workflow.';
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
else if (wfSubCommand === 'run' || wfSubCommand === 'execute') {
|
|
3272
|
+
// Run a workflow
|
|
3273
|
+
const wfName = args.slice(1).join(' ').trim();
|
|
3274
|
+
if (!wfName) {
|
|
3275
|
+
responseMessage = 'Usage: /workflow run <name>\n\nUse /workflow list to see available workflows.';
|
|
3276
|
+
}
|
|
3277
|
+
else {
|
|
3278
|
+
// Cancel workflow learning if active before running a different workflow
|
|
3279
|
+
const runCancelMsg = this.cancelWorkflowLearning('Running another workflow');
|
|
3280
|
+
if (runCancelMsg && this.onDirectMessageCallback) {
|
|
3281
|
+
this.onDirectMessageCallback(runCancelMsg);
|
|
3282
|
+
}
|
|
3283
|
+
const workflow = workflowStorage.load(wfName);
|
|
3284
|
+
if (!workflow) {
|
|
3285
|
+
responseMessage = `❌ Workflow '${wfName}' not found.\n\nUse /workflow list to see available workflows.`;
|
|
3286
|
+
}
|
|
3287
|
+
else {
|
|
3288
|
+
// Start workflow execution
|
|
3289
|
+
responseMessage = `🚀 Starting workflow: **${workflow.name}**\n\n` +
|
|
3290
|
+
`${workflow.steps.length} steps to execute...\n\n` +
|
|
3291
|
+
workflowStorage.formatWorkflowForDisplay(workflow);
|
|
3292
|
+
// Send initial response
|
|
3293
|
+
if (this.onDirectMessageCallback) {
|
|
3294
|
+
this.onDirectMessageCallback(responseMessage);
|
|
3295
|
+
}
|
|
3296
|
+
// Trigger workflow execution by sending the workflow to the AI
|
|
3297
|
+
// Format the workflow as a prompt for the AI to execute
|
|
3298
|
+
const workflowPrompt = this.buildWorkflowPrompt(workflow);
|
|
3299
|
+
// Use setTimeout to allow the UI to update before starting execution
|
|
3300
|
+
setTimeout(() => {
|
|
3301
|
+
this.handleMessage(workflowPrompt, { isolatedWorkflow: true });
|
|
3302
|
+
}, 100);
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
else if (wfSubCommand === 'view' || wfSubCommand === 'show') {
|
|
3308
|
+
// View a workflow's steps
|
|
3309
|
+
const wfName = args.slice(1).join(' ').trim();
|
|
3310
|
+
if (!wfName) {
|
|
3311
|
+
responseMessage = 'Usage: /workflow view <name>';
|
|
3312
|
+
}
|
|
3313
|
+
else {
|
|
3314
|
+
const workflow = workflowStorage.load(wfName);
|
|
3315
|
+
if (!workflow) {
|
|
3316
|
+
responseMessage = `❌ Workflow '${wfName}' not found.\n\nUse /workflow list to see available workflows.`;
|
|
3317
|
+
}
|
|
3318
|
+
else {
|
|
3319
|
+
responseMessage = workflowStorage.formatWorkflowForDisplay(workflow);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
else if (wfSubCommand === 'delete' || wfSubCommand === 'rm' || wfSubCommand === 'remove') {
|
|
3324
|
+
// Delete a workflow
|
|
3325
|
+
const wfName = args.slice(1).join(' ').trim();
|
|
3326
|
+
if (!wfName) {
|
|
3327
|
+
responseMessage = 'Usage: /workflow delete <name>';
|
|
3328
|
+
}
|
|
3329
|
+
else {
|
|
3330
|
+
const result = workflowStorage.delete(wfName);
|
|
3331
|
+
if (result.success) {
|
|
3332
|
+
responseMessage = `✅ Workflow '${wfName}' deleted successfully.`;
|
|
3333
|
+
}
|
|
3334
|
+
else {
|
|
3335
|
+
responseMessage = `❌ ${result.error}`;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
else {
|
|
3340
|
+
responseMessage = `Unknown /workflow subcommand: ${wfSubCommand}\n\n` +
|
|
3341
|
+
'Usage:\n' +
|
|
3342
|
+
' /workflow new - Create a new workflow\n' +
|
|
3343
|
+
' /workflow list - List saved workflows\n' +
|
|
3344
|
+
' /workflow run <name> - Run a workflow\n' +
|
|
3345
|
+
' /workflow view <name> - View workflow steps\n' +
|
|
3346
|
+
' /workflow delete <name> - Delete a workflow';
|
|
3347
|
+
}
|
|
3348
|
+
break;
|
|
2629
3349
|
case 'sync':
|
|
2630
3350
|
// Sync local data to/from cloud
|
|
2631
3351
|
if (!apiClient.isAuthenticated()) {
|
|
@@ -3146,7 +3866,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3146
3866
|
type: currentContext.type,
|
|
3147
3867
|
connectionCommand: this.lastConnectionCommand,
|
|
3148
3868
|
remoteCwd: currentContext.metadata.workingDirectory,
|
|
3149
|
-
localCwdBeforeRemote: this.
|
|
3869
|
+
localCwdBeforeRemote: this.cwdStack.length > 0 ? this.cwdStack[0] : process.cwd(),
|
|
3150
3870
|
metadata: {
|
|
3151
3871
|
hostname: currentContext.metadata.hostname,
|
|
3152
3872
|
username: currentContext.metadata.username,
|
|
@@ -3156,9 +3876,9 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3156
3876
|
}
|
|
3157
3877
|
};
|
|
3158
3878
|
}
|
|
3159
|
-
// Determine the local CWD to save (use
|
|
3160
|
-
const cwdToSave = currentContext.type !== 'local' && this.
|
|
3161
|
-
? this.
|
|
3879
|
+
// Determine the local CWD to save (use base of cwdStack if in remote, otherwise current cwd)
|
|
3880
|
+
const cwdToSave = currentContext.type !== 'local' && this.cwdStack.length > 0
|
|
3881
|
+
? this.cwdStack[0]
|
|
3162
3882
|
: this.cwd;
|
|
3163
3883
|
try {
|
|
3164
3884
|
localChatStorage.saveChat(this.currentChatId, storedMessages, storedUIMessages, cwdToSave, remoteContext);
|
|
@@ -3216,6 +3936,10 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3216
3936
|
this.updateTokenCount().catch(err => {
|
|
3217
3937
|
quickLog(`[${new Date().toISOString()}] [loadChatFromPicker] Failed to update token count: ${err}\n`);
|
|
3218
3938
|
});
|
|
3939
|
+
// Clean up any orphaned tool calls from previous interrupted sessions
|
|
3940
|
+
// This prevents "improperly formed request" errors when continuing conversations
|
|
3941
|
+
this.cleanupOrphanedToolCalls();
|
|
3942
|
+
quickLog(`[${new Date().toISOString()}] [loadChat] Cleaned up conversation history after load\n`);
|
|
3219
3943
|
return true;
|
|
3220
3944
|
}
|
|
3221
3945
|
/**
|
|
@@ -3245,7 +3969,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3245
3969
|
// Pop context to return to local
|
|
3246
3970
|
this.contextManager.popContext();
|
|
3247
3971
|
// Clear remote context tracking
|
|
3248
|
-
this.
|
|
3972
|
+
this.cwdStack = [];
|
|
3249
3973
|
this.lastConnectionCommand = null;
|
|
3250
3974
|
}
|
|
3251
3975
|
// Load AI context
|
|
@@ -3273,8 +3997,8 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3273
3997
|
// Attempt to restore remote context if chat was saved while in remote environment
|
|
3274
3998
|
if (chat.remoteContext) {
|
|
3275
3999
|
const { type, connectionCommand, remoteCwd, localCwdBeforeRemote } = chat.remoteContext;
|
|
3276
|
-
// Store local CWD for when user exits remote
|
|
3277
|
-
this.
|
|
4000
|
+
// Store local CWD for when user exits remote (as base of stack)
|
|
4001
|
+
this.cwdStack = [localCwdBeforeRemote];
|
|
3278
4002
|
this.lastConnectionCommand = connectionCommand;
|
|
3279
4003
|
// Show reconnection notification
|
|
3280
4004
|
if (this.onDirectMessageCallback) {
|
|
@@ -3319,7 +4043,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3319
4043
|
}
|
|
3320
4044
|
catch (error) {
|
|
3321
4045
|
// Connection failed - fall back to local mode
|
|
3322
|
-
this.
|
|
4046
|
+
this.cwdStack = [];
|
|
3323
4047
|
this.lastConnectionCommand = null;
|
|
3324
4048
|
if (this.onConnectionStatusUpdate) {
|
|
3325
4049
|
this.onConnectionStatusUpdate({
|
|
@@ -3382,7 +4106,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
3382
4106
|
this.currentChatId = null;
|
|
3383
4107
|
this.conversationStarted = false;
|
|
3384
4108
|
this.uiMessageHistory = [];
|
|
3385
|
-
this.
|
|
4109
|
+
this.cwdStack = [];
|
|
3386
4110
|
this.lastConnectionCommand = null;
|
|
3387
4111
|
// Reset context limit state
|
|
3388
4112
|
if (this.contextLimitReached) {
|
|
@@ -3519,45 +4243,6 @@ Once the user approves the plan:
|
|
|
3519
4243
|
}
|
|
3520
4244
|
}
|
|
3521
4245
|
}
|
|
3522
|
-
/**
|
|
3523
|
-
* Handle unexpected remote session disconnect
|
|
3524
|
-
* Called when SSH/WSL/Docker connection is lost unexpectedly
|
|
3525
|
-
*/
|
|
3526
|
-
handleRemoteDisconnect(connectionString, type, error) {
|
|
3527
|
-
// Pop the remote context
|
|
3528
|
-
this.contextManager.popContext();
|
|
3529
|
-
// Restore local CWD
|
|
3530
|
-
if (this.localCwdBeforeRemote) {
|
|
3531
|
-
this.cwd = this.localCwdBeforeRemote;
|
|
3532
|
-
this.contextManager.updateWorkingDirectory(this.localCwdBeforeRemote);
|
|
3533
|
-
if (this.onCwdChange) {
|
|
3534
|
-
this.onCwdChange(this.localCwdBeforeRemote);
|
|
3535
|
-
}
|
|
3536
|
-
}
|
|
3537
|
-
// Clear remote context tracking
|
|
3538
|
-
this.localCwdBeforeRemote = null;
|
|
3539
|
-
this.lastConnectionCommand = null;
|
|
3540
|
-
// Save chat with no remote context
|
|
3541
|
-
this.saveCurrentChat();
|
|
3542
|
-
// Notify UI of disconnection via connection status update
|
|
3543
|
-
if (this.onConnectionStatusUpdate) {
|
|
3544
|
-
this.onConnectionStatusUpdate({
|
|
3545
|
-
type: type,
|
|
3546
|
-
status: 'disconnected',
|
|
3547
|
-
connectionString,
|
|
3548
|
-
error: error
|
|
3549
|
-
});
|
|
3550
|
-
}
|
|
3551
|
-
// If there's an active tool execution (shell running), mark it as error
|
|
3552
|
-
if (this.onToolExecutionUpdate && this.currentInteractiveProcess) {
|
|
3553
|
-
this.onToolExecutionUpdate({
|
|
3554
|
-
toolName: 'execute_command',
|
|
3555
|
-
status: 'error',
|
|
3556
|
-
error: `Disconnected from ${type}: ${error || 'Connection lost'}`
|
|
3557
|
-
});
|
|
3558
|
-
this.currentInteractiveProcess = undefined;
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
4246
|
/**
|
|
3562
4247
|
* Set the current interactive process
|
|
3563
4248
|
* This is called by the execute_command tool when starting an interactive command
|
|
@@ -3599,20 +4284,32 @@ Once the user approves the plan:
|
|
|
3599
4284
|
if (currentContext.handler) {
|
|
3600
4285
|
await currentContext.handler.disconnect();
|
|
3601
4286
|
}
|
|
3602
|
-
// Pop context
|
|
4287
|
+
// Pop context - this returns us to the parent context
|
|
3603
4288
|
this.contextManager.popContext();
|
|
3604
|
-
//
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
4289
|
+
// Check the NEW current context after popping
|
|
4290
|
+
const newContext = this.contextManager.getCurrentContext();
|
|
4291
|
+
// Pop the previous CWD from stack - this handles any nesting level
|
|
4292
|
+
const previousCwd = this.cwdStack.pop();
|
|
4293
|
+
if (previousCwd) {
|
|
4294
|
+
this.cwd = previousCwd;
|
|
4295
|
+
this.contextManager.updateWorkingDirectory(previousCwd);
|
|
4296
|
+
if (this.onCwdChange) {
|
|
4297
|
+
this.onCwdChange(previousCwd);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
else if (newContext.type !== 'local') {
|
|
4301
|
+
// Fallback: use parent context's CWD if no stack entry
|
|
4302
|
+
const parentCwd = newContext.metadata?.workingDirectory || '~';
|
|
4303
|
+
this.cwd = parentCwd;
|
|
3608
4304
|
if (this.onCwdChange) {
|
|
3609
|
-
this.onCwdChange(
|
|
4305
|
+
this.onCwdChange(parentCwd);
|
|
3610
4306
|
}
|
|
3611
4307
|
}
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
4308
|
+
if (newContext.type === 'local') {
|
|
4309
|
+
// Clear tracking when back to local
|
|
4310
|
+
this.lastConnectionCommand = null;
|
|
4311
|
+
}
|
|
4312
|
+
// Save chat - include remote context info if still in remote
|
|
3616
4313
|
this.saveCurrentChat();
|
|
3617
4314
|
if (this.onResponseCallback) {
|
|
3618
4315
|
this.onResponseCallback('✅ Exited subshell');
|
|
@@ -3620,80 +4317,19 @@ Once the user approves the plan:
|
|
|
3620
4317
|
return;
|
|
3621
4318
|
}
|
|
3622
4319
|
}
|
|
3623
|
-
//
|
|
4320
|
+
// WARPIFY MODE: SSH/WSL/Docker commands now run as NORMAL PTY commands
|
|
4321
|
+
// The old flow intercepted these commands and used the ssh2/etc libraries directly.
|
|
4322
|
+
// The new flow lets them run in focus mode, user enters password in terminal,
|
|
4323
|
+
// then presses Alt+E to "warpify" the session for AI context awareness.
|
|
4324
|
+
// The detection code is preserved below (commented) for reference.
|
|
4325
|
+
/*
|
|
4326
|
+
// Detect subshell commands (OLD FLOW - DISABLED FOR WARPIFY)
|
|
3624
4327
|
const detection = this.commandDetector.detect(command);
|
|
3625
4328
|
if (detection) {
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
if (detection.handler.type === 'ssh') {
|
|
3629
|
-
// Parse SSH command to get user@host
|
|
3630
|
-
const sshMatch = command.match(/ssh\s+(?:(?:-\w+\s+)+)?(?:(\S+)@)?(\S+)/);
|
|
3631
|
-
if (sshMatch) {
|
|
3632
|
-
const user = sshMatch[1] || 'user';
|
|
3633
|
-
const host = sshMatch[2] || 'remote';
|
|
3634
|
-
connectionString = `${user}@${host}`;
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
else if (detection.handler.type === 'wsl') {
|
|
3638
|
-
// Parse WSL command to get distribution name
|
|
3639
|
-
const wslMatch = command.match(/wsl(?:\s+(?:-d|--distribution)\s+(\S+))?/);
|
|
3640
|
-
connectionString = wslMatch?.[1] || 'Ubuntu';
|
|
3641
|
-
}
|
|
3642
|
-
else if (detection.handler.type === 'docker') {
|
|
3643
|
-
// Parse Docker command to get container
|
|
3644
|
-
const dockerMatch = command.match(/docker\s+exec\s+(?:(?:-\w+\s+)+)?(\S+)/);
|
|
3645
|
-
connectionString = dockerMatch?.[1]?.substring(0, 12) || 'container';
|
|
3646
|
-
}
|
|
3647
|
-
// Show connecting message with spinner (dynamic)
|
|
3648
|
-
if (this.onConnectionStatusUpdate) {
|
|
3649
|
-
this.onConnectionStatusUpdate({
|
|
3650
|
-
type: detection.handler.type,
|
|
3651
|
-
status: 'connecting',
|
|
3652
|
-
connectionString
|
|
3653
|
-
});
|
|
3654
|
-
}
|
|
3655
|
-
// Update connection state
|
|
3656
|
-
this.contextManager.updateConnectionState('connecting');
|
|
3657
|
-
try {
|
|
3658
|
-
// Save local CWD before entering remote session (for restoration when resuming chat)
|
|
3659
|
-
if (this.contextManager.getCurrentContext().type === 'local') {
|
|
3660
|
-
this.localCwdBeforeRemote = this.cwd;
|
|
3661
|
-
}
|
|
3662
|
-
this.lastConnectionCommand = command;
|
|
3663
|
-
// Connect to subshell
|
|
3664
|
-
const context = await detection.handler.connect(command, this.cwd);
|
|
3665
|
-
this.contextManager.pushContext(context);
|
|
3666
|
-
// Set up disconnect callback for SSH connections
|
|
3667
|
-
if (detection.handler.type === 'ssh' && context.handler?.setDisconnectCallback) {
|
|
3668
|
-
context.handler.setDisconnectCallback((error) => {
|
|
3669
|
-
// Handle unexpected disconnect
|
|
3670
|
-
this.handleRemoteDisconnect(connectionString, detection.handler.type, error);
|
|
3671
|
-
});
|
|
3672
|
-
}
|
|
3673
|
-
// Show success message (replaces the spinner with static message)
|
|
3674
|
-
if (this.onConnectionStatusUpdate) {
|
|
3675
|
-
this.onConnectionStatusUpdate({
|
|
3676
|
-
type: detection.handler.type,
|
|
3677
|
-
status: 'connected',
|
|
3678
|
-
connectionString
|
|
3679
|
-
});
|
|
3680
|
-
}
|
|
3681
|
-
return;
|
|
3682
|
-
}
|
|
3683
|
-
catch (error) {
|
|
3684
|
-
// Connection failed
|
|
3685
|
-
this.contextManager.updateConnectionState('error');
|
|
3686
|
-
if (this.onConnectionStatusUpdate) {
|
|
3687
|
-
this.onConnectionStatusUpdate({
|
|
3688
|
-
type: detection.handler.type,
|
|
3689
|
-
status: 'error',
|
|
3690
|
-
connectionString,
|
|
3691
|
-
error: error.message
|
|
3692
|
-
});
|
|
3693
|
-
}
|
|
3694
|
-
return;
|
|
3695
|
-
}
|
|
4329
|
+
// ... old interception logic ...
|
|
4330
|
+
// This triggered password prompt BEFORE running the command
|
|
3696
4331
|
}
|
|
4332
|
+
*/
|
|
3697
4333
|
// Special handling for cd command - change the actual working directory
|
|
3698
4334
|
const cdMatch = command.match(/^cd\s+(.+)$/);
|
|
3699
4335
|
if (cdMatch) {
|