centaurus-cli 2.9.3 → 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 +72 -8
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +607 -141
- 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/slash-commands.d.ts +2 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +28 -0
- package/dist/config/slash-commands.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-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +33 -11
- 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/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 +10 -1
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +117 -4
- 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 +1 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +168 -2
- 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/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +164 -25
- 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/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +2 -1
- 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';
|
|
@@ -43,6 +45,7 @@ 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';
|
|
45
47
|
import { ollamaService, OllamaService } from './services/ollama-service.js';
|
|
48
|
+
import { workflowStorage } from './services/workflow-storage.js';
|
|
46
49
|
export class CentaurusCLI {
|
|
47
50
|
configManager;
|
|
48
51
|
toolRegistry;
|
|
@@ -90,7 +93,7 @@ export class CentaurusCLI {
|
|
|
90
93
|
onShowChatRenamePickerCallback;
|
|
91
94
|
onRestoreMessagesCallback;
|
|
92
95
|
uiMessageHistory = []; // Mirror of App.tsx's messageHistory for saving
|
|
93
|
-
|
|
96
|
+
cwdStack = []; // Stack of CWDs for nested sessions (pushed when entering, popped when exiting)
|
|
94
97
|
lastConnectionCommand = null; // Track the command used to connect to remote
|
|
95
98
|
onBackgroundModeChange;
|
|
96
99
|
onBackgroundTaskCountChange;
|
|
@@ -111,6 +114,11 @@ export class CentaurusCLI {
|
|
|
111
114
|
onShowMCPListScreen;
|
|
112
115
|
onSubAgentCountChange; // Callback for sub-agent count changes
|
|
113
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 = [];
|
|
114
122
|
constructor() {
|
|
115
123
|
this.configManager = new ConfigManager();
|
|
116
124
|
this.toolRegistry = new ToolRegistry();
|
|
@@ -120,13 +128,14 @@ export class CentaurusCLI {
|
|
|
120
128
|
this.commandDetector = new CommandDetector();
|
|
121
129
|
this.aiContextInjector = new AIContextInjector();
|
|
122
130
|
// Register context change callback to update cwd
|
|
123
|
-
|
|
131
|
+
// Register context change callback to update cwd
|
|
132
|
+
this.contextManager.onContextChange((context, stack) => {
|
|
124
133
|
this.cwd = context.metadata.workingDirectory;
|
|
125
134
|
if (this.onCwdChange) {
|
|
126
135
|
this.onCwdChange(this.cwd);
|
|
127
136
|
}
|
|
128
137
|
if (this.onSubshellContextChange) {
|
|
129
|
-
this.onSubshellContextChange(context);
|
|
138
|
+
this.onSubshellContextChange(context, stack);
|
|
130
139
|
}
|
|
131
140
|
});
|
|
132
141
|
// Initialize MCP
|
|
@@ -218,6 +227,355 @@ export class CentaurusCLI {
|
|
|
218
227
|
// Wire this callback to ShellInputAgent
|
|
219
228
|
ShellInputAgent.setOnPromptAnswered(callback);
|
|
220
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
|
+
}
|
|
221
579
|
/**
|
|
222
580
|
* Calculate and update token count based on current conversation history
|
|
223
581
|
* This ensures UI is always in sync with the actual AI context
|
|
@@ -498,6 +856,11 @@ export class CentaurusCLI {
|
|
|
498
856
|
* Notify UI about tool execution status
|
|
499
857
|
*/
|
|
500
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
|
+
}
|
|
501
864
|
if (this.onToolExecutionUpdate) {
|
|
502
865
|
// Get current context for remote prefix
|
|
503
866
|
const currentContext = this.contextManager.getCurrentContext();
|
|
@@ -574,6 +937,8 @@ export class CentaurusCLI {
|
|
|
574
937
|
this.toolRegistry.register(createImageTool);
|
|
575
938
|
this.toolRegistry.register(backgroundCommandTool);
|
|
576
939
|
this.toolRegistry.register(subAgentTool);
|
|
940
|
+
this.toolRegistry.register(enterRemoteSessionTool);
|
|
941
|
+
this.toolRegistry.register(workflowTool);
|
|
577
942
|
// Initialize SubAgentManager with tool registry
|
|
578
943
|
SubAgentManager.initialize(this.toolRegistry);
|
|
579
944
|
SubAgentManager.setOnSubAgentCountChange((count) => {
|
|
@@ -769,14 +1134,22 @@ Press Enter to continue...
|
|
|
769
1134
|
}
|
|
770
1135
|
}
|
|
771
1136
|
}
|
|
772
|
-
async handleMessage(message) {
|
|
1137
|
+
async handleMessage(message, options = {}) {
|
|
773
1138
|
// Handle command mode - execute commands directly
|
|
774
1139
|
if (this.commandMode) {
|
|
1140
|
+
// Record command step if workflow learning mode is active
|
|
1141
|
+
if (this.workflowLearningActive) {
|
|
1142
|
+
this.recordWorkflowStep('command', message);
|
|
1143
|
+
}
|
|
775
1144
|
await this.handleCommandModeExecution(message);
|
|
776
1145
|
return;
|
|
777
1146
|
}
|
|
778
1147
|
// Handle background mode - execute commands in background
|
|
779
1148
|
if (this.backgroundMode) {
|
|
1149
|
+
// Record command step if workflow learning mode is active
|
|
1150
|
+
if (this.workflowLearningActive) {
|
|
1151
|
+
this.recordWorkflowStep('command', message);
|
|
1152
|
+
}
|
|
780
1153
|
this.handleBackgroundModeExecution(message);
|
|
781
1154
|
return;
|
|
782
1155
|
}
|
|
@@ -785,6 +1158,11 @@ Press Enter to continue...
|
|
|
785
1158
|
await this.handleSlashCommand(message);
|
|
786
1159
|
return;
|
|
787
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
|
+
}
|
|
788
1166
|
// Check authentication
|
|
789
1167
|
if (!apiClient.isAuthenticated()) {
|
|
790
1168
|
throw new Error('Authentication required. Please sign in to use AI features.');
|
|
@@ -887,6 +1265,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
887
1265
|
role: 'user',
|
|
888
1266
|
content: userMessageContent,
|
|
889
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
|
+
};
|
|
890
1277
|
// Messages are stored locally only - no backend persistence needed
|
|
891
1278
|
// Local storage is handled by saveCurrentChat() which saves to ~/.centaurus/chats/
|
|
892
1279
|
// Start logging session and log user message
|
|
@@ -931,7 +1318,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
931
1318
|
// SAFETY: Clean up any orphaned tool calls before making AI request
|
|
932
1319
|
// This prevents "improperly formed request" errors from corrupted history
|
|
933
1320
|
this.cleanupOrphanedToolCalls();
|
|
934
|
-
let messages =
|
|
1321
|
+
let messages = getMessagesForContext();
|
|
935
1322
|
// Inject subshell context if in a subshell environment
|
|
936
1323
|
const currentContext = this.contextManager.getCurrentContext();
|
|
937
1324
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -1380,7 +1767,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1380
1767
|
// Clear pending plan request
|
|
1381
1768
|
this.pendingPlanRequest = null;
|
|
1382
1769
|
// Update messages array for this turn
|
|
1383
|
-
messages =
|
|
1770
|
+
messages = getMessagesForContext();
|
|
1384
1771
|
// Continue the loop - AI will now execute with plan context
|
|
1385
1772
|
continue;
|
|
1386
1773
|
}
|
|
@@ -1445,7 +1832,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1445
1832
|
});
|
|
1446
1833
|
// Mark this tool call as handled so it's not duplicated
|
|
1447
1834
|
handledToolCallIds.add(toolCall.id);
|
|
1448
|
-
messages =
|
|
1835
|
+
messages = getMessagesForContext();
|
|
1449
1836
|
}
|
|
1450
1837
|
}
|
|
1451
1838
|
else {
|
|
@@ -1469,7 +1856,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1469
1856
|
});
|
|
1470
1857
|
// Mark this tool call as handled so it's not duplicated
|
|
1471
1858
|
handledToolCallIds.add(toolCall.id);
|
|
1472
|
-
messages =
|
|
1859
|
+
messages = getMessagesForContext();
|
|
1473
1860
|
}
|
|
1474
1861
|
continue;
|
|
1475
1862
|
}
|
|
@@ -1536,7 +1923,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1536
1923
|
// Mark as handled
|
|
1537
1924
|
handledToolCallIds.add(toolCall.id);
|
|
1538
1925
|
// Update messages and continue
|
|
1539
|
-
messages =
|
|
1926
|
+
messages = getMessagesForContext();
|
|
1540
1927
|
continue;
|
|
1541
1928
|
}
|
|
1542
1929
|
}
|
|
@@ -1878,7 +2265,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1878
2265
|
// Rebuild messages array with updated history
|
|
1879
2266
|
// During agent loop: keep ALL thinking for current task
|
|
1880
2267
|
// (Thinking from previous tasks was already stripped at request start)
|
|
1881
|
-
messages =
|
|
2268
|
+
messages = getMessagesForContext();
|
|
1882
2269
|
// No need to reset currentTurnThinking - keep accumulating for the task
|
|
1883
2270
|
// Re-inject subshell context
|
|
1884
2271
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -1921,7 +2308,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1921
2308
|
}
|
|
1922
2309
|
// Rebuild messages array with updated history
|
|
1923
2310
|
// Backend will inject system prompt
|
|
1924
|
-
messages =
|
|
2311
|
+
messages = getMessagesForContext();
|
|
1925
2312
|
// Re-inject subshell context
|
|
1926
2313
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1927
2314
|
// Continue loop to get AI's response (removed 500ms delay for faster response)
|
|
@@ -1942,7 +2329,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1942
2329
|
});
|
|
1943
2330
|
// Rebuild messages array with updated history
|
|
1944
2331
|
// Backend will inject system prompt
|
|
1945
|
-
messages =
|
|
2332
|
+
messages = getMessagesForContext();
|
|
1946
2333
|
// Re-inject subshell context
|
|
1947
2334
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1948
2335
|
// Add delay before prompting
|
|
@@ -2276,6 +2663,11 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2276
2663
|
break;
|
|
2277
2664
|
case 'logout':
|
|
2278
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
|
+
}
|
|
2279
2671
|
await apiClient.logout();
|
|
2280
2672
|
responseMessage = '✅ Logged out successfully.\n\n' +
|
|
2281
2673
|
'Your session has been cleared.\n' +
|
|
@@ -2354,6 +2746,11 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2354
2746
|
'• Manual completion detection';
|
|
2355
2747
|
break;
|
|
2356
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
|
+
}
|
|
2357
2754
|
// Start a new chat session (clears history and generates new chat ID)
|
|
2358
2755
|
this.startNewChat();
|
|
2359
2756
|
// Don't send any response message - the UI will handle clearing
|
|
@@ -2792,6 +3189,163 @@ Then try /models local again.`;
|
|
|
2792
3189
|
' /background-task cancel - Cancel a running background task';
|
|
2793
3190
|
}
|
|
2794
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;
|
|
2795
3349
|
case 'sync':
|
|
2796
3350
|
// Sync local data to/from cloud
|
|
2797
3351
|
if (!apiClient.isAuthenticated()) {
|
|
@@ -3312,7 +3866,7 @@ Then try /models local again.`;
|
|
|
3312
3866
|
type: currentContext.type,
|
|
3313
3867
|
connectionCommand: this.lastConnectionCommand,
|
|
3314
3868
|
remoteCwd: currentContext.metadata.workingDirectory,
|
|
3315
|
-
localCwdBeforeRemote: this.
|
|
3869
|
+
localCwdBeforeRemote: this.cwdStack.length > 0 ? this.cwdStack[0] : process.cwd(),
|
|
3316
3870
|
metadata: {
|
|
3317
3871
|
hostname: currentContext.metadata.hostname,
|
|
3318
3872
|
username: currentContext.metadata.username,
|
|
@@ -3322,9 +3876,9 @@ Then try /models local again.`;
|
|
|
3322
3876
|
}
|
|
3323
3877
|
};
|
|
3324
3878
|
}
|
|
3325
|
-
// Determine the local CWD to save (use
|
|
3326
|
-
const cwdToSave = currentContext.type !== 'local' && this.
|
|
3327
|
-
? 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]
|
|
3328
3882
|
: this.cwd;
|
|
3329
3883
|
try {
|
|
3330
3884
|
localChatStorage.saveChat(this.currentChatId, storedMessages, storedUIMessages, cwdToSave, remoteContext);
|
|
@@ -3415,7 +3969,7 @@ Then try /models local again.`;
|
|
|
3415
3969
|
// Pop context to return to local
|
|
3416
3970
|
this.contextManager.popContext();
|
|
3417
3971
|
// Clear remote context tracking
|
|
3418
|
-
this.
|
|
3972
|
+
this.cwdStack = [];
|
|
3419
3973
|
this.lastConnectionCommand = null;
|
|
3420
3974
|
}
|
|
3421
3975
|
// Load AI context
|
|
@@ -3443,8 +3997,8 @@ Then try /models local again.`;
|
|
|
3443
3997
|
// Attempt to restore remote context if chat was saved while in remote environment
|
|
3444
3998
|
if (chat.remoteContext) {
|
|
3445
3999
|
const { type, connectionCommand, remoteCwd, localCwdBeforeRemote } = chat.remoteContext;
|
|
3446
|
-
// Store local CWD for when user exits remote
|
|
3447
|
-
this.
|
|
4000
|
+
// Store local CWD for when user exits remote (as base of stack)
|
|
4001
|
+
this.cwdStack = [localCwdBeforeRemote];
|
|
3448
4002
|
this.lastConnectionCommand = connectionCommand;
|
|
3449
4003
|
// Show reconnection notification
|
|
3450
4004
|
if (this.onDirectMessageCallback) {
|
|
@@ -3489,7 +4043,7 @@ Then try /models local again.`;
|
|
|
3489
4043
|
}
|
|
3490
4044
|
catch (error) {
|
|
3491
4045
|
// Connection failed - fall back to local mode
|
|
3492
|
-
this.
|
|
4046
|
+
this.cwdStack = [];
|
|
3493
4047
|
this.lastConnectionCommand = null;
|
|
3494
4048
|
if (this.onConnectionStatusUpdate) {
|
|
3495
4049
|
this.onConnectionStatusUpdate({
|
|
@@ -3552,7 +4106,7 @@ Then try /models local again.`;
|
|
|
3552
4106
|
this.currentChatId = null;
|
|
3553
4107
|
this.conversationStarted = false;
|
|
3554
4108
|
this.uiMessageHistory = [];
|
|
3555
|
-
this.
|
|
4109
|
+
this.cwdStack = [];
|
|
3556
4110
|
this.lastConnectionCommand = null;
|
|
3557
4111
|
// Reset context limit state
|
|
3558
4112
|
if (this.contextLimitReached) {
|
|
@@ -3689,45 +4243,6 @@ Once the user approves the plan:
|
|
|
3689
4243
|
}
|
|
3690
4244
|
}
|
|
3691
4245
|
}
|
|
3692
|
-
/**
|
|
3693
|
-
* Handle unexpected remote session disconnect
|
|
3694
|
-
* Called when SSH/WSL/Docker connection is lost unexpectedly
|
|
3695
|
-
*/
|
|
3696
|
-
handleRemoteDisconnect(connectionString, type, error) {
|
|
3697
|
-
// Pop the remote context
|
|
3698
|
-
this.contextManager.popContext();
|
|
3699
|
-
// Restore local CWD
|
|
3700
|
-
if (this.localCwdBeforeRemote) {
|
|
3701
|
-
this.cwd = this.localCwdBeforeRemote;
|
|
3702
|
-
this.contextManager.updateWorkingDirectory(this.localCwdBeforeRemote);
|
|
3703
|
-
if (this.onCwdChange) {
|
|
3704
|
-
this.onCwdChange(this.localCwdBeforeRemote);
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
// Clear remote context tracking
|
|
3708
|
-
this.localCwdBeforeRemote = null;
|
|
3709
|
-
this.lastConnectionCommand = null;
|
|
3710
|
-
// Save chat with no remote context
|
|
3711
|
-
this.saveCurrentChat();
|
|
3712
|
-
// Notify UI of disconnection via connection status update
|
|
3713
|
-
if (this.onConnectionStatusUpdate) {
|
|
3714
|
-
this.onConnectionStatusUpdate({
|
|
3715
|
-
type: type,
|
|
3716
|
-
status: 'disconnected',
|
|
3717
|
-
connectionString,
|
|
3718
|
-
error: error
|
|
3719
|
-
});
|
|
3720
|
-
}
|
|
3721
|
-
// If there's an active tool execution (shell running), mark it as error
|
|
3722
|
-
if (this.onToolExecutionUpdate && this.currentInteractiveProcess) {
|
|
3723
|
-
this.onToolExecutionUpdate({
|
|
3724
|
-
toolName: 'execute_command',
|
|
3725
|
-
status: 'error',
|
|
3726
|
-
error: `Disconnected from ${type}: ${error || 'Connection lost'}`
|
|
3727
|
-
});
|
|
3728
|
-
this.currentInteractiveProcess = undefined;
|
|
3729
|
-
}
|
|
3730
|
-
}
|
|
3731
4246
|
/**
|
|
3732
4247
|
* Set the current interactive process
|
|
3733
4248
|
* This is called by the execute_command tool when starting an interactive command
|
|
@@ -3769,20 +4284,32 @@ Once the user approves the plan:
|
|
|
3769
4284
|
if (currentContext.handler) {
|
|
3770
4285
|
await currentContext.handler.disconnect();
|
|
3771
4286
|
}
|
|
3772
|
-
// Pop context
|
|
4287
|
+
// Pop context - this returns us to the parent context
|
|
3773
4288
|
this.contextManager.popContext();
|
|
3774
|
-
//
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
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;
|
|
3778
4304
|
if (this.onCwdChange) {
|
|
3779
|
-
this.onCwdChange(
|
|
4305
|
+
this.onCwdChange(parentCwd);
|
|
3780
4306
|
}
|
|
3781
4307
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
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
|
|
3786
4313
|
this.saveCurrentChat();
|
|
3787
4314
|
if (this.onResponseCallback) {
|
|
3788
4315
|
this.onResponseCallback('✅ Exited subshell');
|
|
@@ -3790,80 +4317,19 @@ Once the user approves the plan:
|
|
|
3790
4317
|
return;
|
|
3791
4318
|
}
|
|
3792
4319
|
}
|
|
3793
|
-
//
|
|
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)
|
|
3794
4327
|
const detection = this.commandDetector.detect(command);
|
|
3795
4328
|
if (detection) {
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
if (detection.handler.type === 'ssh') {
|
|
3799
|
-
// Parse SSH command to get user@host
|
|
3800
|
-
const sshMatch = command.match(/ssh\s+(?:(?:-\w+\s+)+)?(?:(\S+)@)?(\S+)/);
|
|
3801
|
-
if (sshMatch) {
|
|
3802
|
-
const user = sshMatch[1] || 'user';
|
|
3803
|
-
const host = sshMatch[2] || 'remote';
|
|
3804
|
-
connectionString = `${user}@${host}`;
|
|
3805
|
-
}
|
|
3806
|
-
}
|
|
3807
|
-
else if (detection.handler.type === 'wsl') {
|
|
3808
|
-
// Parse WSL command to get distribution name
|
|
3809
|
-
const wslMatch = command.match(/wsl(?:\s+(?:-d|--distribution)\s+(\S+))?/);
|
|
3810
|
-
connectionString = wslMatch?.[1] || 'Ubuntu';
|
|
3811
|
-
}
|
|
3812
|
-
else if (detection.handler.type === 'docker') {
|
|
3813
|
-
// Parse Docker command to get container
|
|
3814
|
-
const dockerMatch = command.match(/docker\s+exec\s+(?:(?:-\w+\s+)+)?(\S+)/);
|
|
3815
|
-
connectionString = dockerMatch?.[1]?.substring(0, 12) || 'container';
|
|
3816
|
-
}
|
|
3817
|
-
// Show connecting message with spinner (dynamic)
|
|
3818
|
-
if (this.onConnectionStatusUpdate) {
|
|
3819
|
-
this.onConnectionStatusUpdate({
|
|
3820
|
-
type: detection.handler.type,
|
|
3821
|
-
status: 'connecting',
|
|
3822
|
-
connectionString
|
|
3823
|
-
});
|
|
3824
|
-
}
|
|
3825
|
-
// Update connection state
|
|
3826
|
-
this.contextManager.updateConnectionState('connecting');
|
|
3827
|
-
try {
|
|
3828
|
-
// Save local CWD before entering remote session (for restoration when resuming chat)
|
|
3829
|
-
if (this.contextManager.getCurrentContext().type === 'local') {
|
|
3830
|
-
this.localCwdBeforeRemote = this.cwd;
|
|
3831
|
-
}
|
|
3832
|
-
this.lastConnectionCommand = command;
|
|
3833
|
-
// Connect to subshell
|
|
3834
|
-
const context = await detection.handler.connect(command, this.cwd);
|
|
3835
|
-
this.contextManager.pushContext(context);
|
|
3836
|
-
// Set up disconnect callback for SSH connections
|
|
3837
|
-
if (detection.handler.type === 'ssh' && context.handler?.setDisconnectCallback) {
|
|
3838
|
-
context.handler.setDisconnectCallback((error) => {
|
|
3839
|
-
// Handle unexpected disconnect
|
|
3840
|
-
this.handleRemoteDisconnect(connectionString, detection.handler.type, error);
|
|
3841
|
-
});
|
|
3842
|
-
}
|
|
3843
|
-
// Show success message (replaces the spinner with static message)
|
|
3844
|
-
if (this.onConnectionStatusUpdate) {
|
|
3845
|
-
this.onConnectionStatusUpdate({
|
|
3846
|
-
type: detection.handler.type,
|
|
3847
|
-
status: 'connected',
|
|
3848
|
-
connectionString
|
|
3849
|
-
});
|
|
3850
|
-
}
|
|
3851
|
-
return;
|
|
3852
|
-
}
|
|
3853
|
-
catch (error) {
|
|
3854
|
-
// Connection failed
|
|
3855
|
-
this.contextManager.updateConnectionState('error');
|
|
3856
|
-
if (this.onConnectionStatusUpdate) {
|
|
3857
|
-
this.onConnectionStatusUpdate({
|
|
3858
|
-
type: detection.handler.type,
|
|
3859
|
-
status: 'error',
|
|
3860
|
-
connectionString,
|
|
3861
|
-
error: error.message
|
|
3862
|
-
});
|
|
3863
|
-
}
|
|
3864
|
-
return;
|
|
3865
|
-
}
|
|
4329
|
+
// ... old interception logic ...
|
|
4330
|
+
// This triggered password prompt BEFORE running the command
|
|
3866
4331
|
}
|
|
4332
|
+
*/
|
|
3867
4333
|
// Special handling for cd command - change the actual working directory
|
|
3868
4334
|
const cdMatch = command.match(/^cd\s+(.+)$/);
|
|
3869
4335
|
if (cdMatch) {
|