Skip to content

Commit 5a7f051

Browse files
committed
feat(mistral): add session resuming and status footer improvements
add support for resuming mistral sessions with session IDs and custom prompts improve status footer by removing conditional rendering and combining text enhance tool handling and error formatting in mistral execution
1 parent 06f84cc commit 5a7f051

File tree

3 files changed

+74
-22
lines changed

3 files changed

+74
-22
lines changed

src/cli/tui/routes/workflow/components/output/status-footer.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* Show keyboard shortcuts at bottom of screen
77
*/
88

9-
import { Show } from "solid-js"
109
import { useTheme } from "@tui/shared/context/theme"
1110

1211
export interface StatusFooterProps {
@@ -22,11 +21,8 @@ export function StatusFooter(props: StatusFooterProps) {
2221
return (
2322
<box paddingLeft={1} paddingRight={1}>
2423
<text fg={themeCtx.theme.textMuted}>
25-
[↑↓] Navigate [ENTER] Expand/View [Tab] Toggle Panel [H] History [P] Pause [Ctrl+S] Skip [Esc] Stop
24+
[↑↓] Navigate [ENTER] Expand/View [Tab] Toggle Panel [H] History [P] Pause [Ctrl+S] Skip [Esc] Stop{props.autonomousMode ? ' [Shift+Tab] Disable Auto' : ''}
2625
</text>
27-
<Show when={props.autonomousMode}>
28-
<text fg={themeCtx.theme.primary}> [Shift+Tab] Disable Auto</text>
29-
</Show>
3026
</box>
3127
)
3228
}

src/infra/engines/providers/mistral/execution/commands.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface MistralCommandOptions {
22
workingDir: string;
33
prompt: string;
4+
resumeSessionId?: string;
45
model?: string;
56
}
67

@@ -49,7 +50,7 @@ export function buildMistralExecCommand(options: MistralCommandOptions): Mistral
4950
// Mistral Vibe CLI doesn't support --model flag
5051
// Model selection is done via agent configuration files at ~/.vibe/agents/
5152
// For now, we'll use the default model configured in Vibe
52-
53+
5354
// Base args for Mistral Vibe CLI in programmatic mode
5455
// -p: programmatic mode (send prompt, auto-approve tools, output response, exit)
5556
// The prompt will be passed as an argument to -p
@@ -63,6 +64,11 @@ export function buildMistralExecCommand(options: MistralCommandOptions): Mistral
6364
'streaming',
6465
];
6566

67+
// Add resume flag if resuming a session
68+
if (options.resumeSessionId?.trim()) {
69+
args.push('--resume', options.resumeSessionId.trim());
70+
}
71+
6672
// Note: Model selection is not supported via CLI flags in Mistral Vibe
6773
// Users need to configure models via agent config files at ~/.vibe/agents/NAME.toml
6874
// or use the default model configured in ~/.vibe/config.toml

src/infra/engines/providers/mistral/execution/runner.ts

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ import {
2222
export interface RunMistralOptions {
2323
prompt: string;
2424
workingDir: string;
25+
resumeSessionId?: string;
26+
resumePrompt?: string;
2527
model?: string;
2628
env?: NodeJS.ProcessEnv;
2729
onData?: (chunk: string) => void;
2830
onErrorData?: (chunk: string) => void;
2931
onTelemetry?: (telemetry: ParsedTelemetry) => void;
32+
onSessionId?: (sessionId: string) => void;
3033
abortSignal?: AbortSignal;
3134
timeout?: number; // Timeout in milliseconds (default: 1800000ms = 30 minutes)
3235
}
@@ -38,6 +41,20 @@ export interface RunMistralResult {
3841

3942
const ANSI_ESCAPE_SEQUENCE = new RegExp(String.raw`\u001B\[[0-9;?]*[ -/]*[@-~]`, 'g');
4043

44+
/**
45+
* Build the final resume prompt combining steering instruction with user message
46+
*/
47+
function buildResumePrompt(userPrompt?: string): string {
48+
const defaultPrompt = 'Continue from where you left off.';
49+
50+
if (!userPrompt) {
51+
return defaultPrompt;
52+
}
53+
54+
// Combine steering instruction with user's message
55+
return `[USER STEERING] The user paused this session to give you new direction. Continue from where you left off, but prioritize the user's request: "${userPrompt}"`;
56+
}
57+
4158
// Track tool names for associating with results
4259
const toolNameMap = new Map<string, string>();
4360

@@ -48,51 +65,75 @@ function formatStreamJsonLine(line: string): string | null {
4865
try {
4966
const json = JSON.parse(line);
5067

51-
// Mistral Vibe uses role/content format instead of type/message format
52-
// Handle both formats for compatibility
5368
const role = json.role || json.type;
5469
const content = json.content || json.message?.content;
5570

5671
if (role === 'assistant') {
57-
// Handle assistant messages
58-
if (typeof content === 'string') {
59-
// Simple string content
72+
// Handle tool_calls array
73+
if (Array.isArray(json.tool_calls) && json.tool_calls.length > 0) {
74+
const results: string[] = [];
75+
for (const toolCall of json.tool_calls) {
76+
const toolName = toolCall.function?.name || toolCall.name || 'tool';
77+
const toolId = toolCall.id;
78+
if (toolId && toolName) {
79+
toolNameMap.set(toolId, toolName);
80+
}
81+
results.push(formatCommand(toolName, 'started'));
82+
}
83+
return results.join('\n');
84+
}
85+
86+
// Handle text content
87+
if (typeof content === 'string' && content.trim()) {
6088
return content;
6189
} else if (Array.isArray(content)) {
62-
// Array of content blocks (like Claude/Gemini format)
6390
for (const block of content) {
6491
if (block.type === 'text' && block.text) {
6592
return block.text;
6693
} else if (block.type === 'thinking' && block.text) {
6794
return formatThinking(block.text);
6895
} else if (block.type === 'tool_use') {
69-
// Track tool name for later use with result
7096
if (block.id && block.name) {
7197
toolNameMap.set(block.id, block.name);
7298
}
73-
const commandName = block.name || 'tool';
74-
return formatCommand(commandName, 'started');
99+
return formatCommand(block.name || 'tool', 'started');
75100
}
76101
}
77102
}
103+
} else if (role === 'tool') {
104+
// Handle tool results
105+
const toolName = json.name || (json.tool_call_id ? toolNameMap.get(json.tool_call_id) : undefined) || 'tool';
106+
107+
if (json.tool_call_id) {
108+
toolNameMap.delete(json.tool_call_id);
109+
}
110+
111+
let preview: string;
112+
const toolContent = json.content;
113+
if (typeof toolContent === 'string') {
114+
const trimmed = toolContent.trim();
115+
preview = trimmed
116+
? (trimmed.length > 100 ? trimmed.substring(0, 100) + '...' : trimmed)
117+
: 'empty';
118+
} else {
119+
preview = JSON.stringify(toolContent);
120+
}
121+
return formatCommand(toolName, 'success') + '\n' + formatResult(preview, false);
78122
} else if (role === 'user') {
79-
// Handle user messages (tool results)
123+
// Handle user messages with tool results
80124
if (Array.isArray(content)) {
81125
for (const block of content) {
82126
if (block.type === 'tool_result') {
83-
// Get tool name from map
84127
const toolName = block.tool_use_id ? toolNameMap.get(block.tool_use_id) : undefined;
85128
const commandName = toolName || 'tool';
86129

87-
// Clean up the map entry
88130
if (block.tool_use_id) {
89131
toolNameMap.delete(block.tool_use_id);
90132
}
91133

92134
let preview: string;
93135
if (block.is_error) {
94136
preview = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
95-
// Show command in red with nested error
96137
return formatCommand(commandName, 'error') + '\n' + formatResult(preview, true);
97138
} else {
98139
if (typeof block.content === 'string') {
@@ -103,7 +144,6 @@ function formatStreamJsonLine(line: string): string | null {
103144
} else {
104145
preview = JSON.stringify(block.content);
105146
}
106-
// Show command in green with nested result
107147
return formatCommand(commandName, 'success') + '\n' + formatResult(preview, false);
108148
}
109149
}
@@ -134,7 +174,7 @@ function formatStreamJsonLine(line: string): string | null {
134174
}
135175

136176
export async function runMistral(options: RunMistralOptions): Promise<RunMistralResult> {
137-
const { prompt, workingDir, model, env, onData, onErrorData, onTelemetry, abortSignal, timeout = 1800000 } = options;
177+
const { prompt, workingDir, resumeSessionId, resumePrompt, model, env, onData, onErrorData, onTelemetry, onSessionId, abortSignal, timeout = 1800000 } = options;
138178

139179
if (!prompt) {
140180
throw new Error('runMistral requires a prompt.');
@@ -185,13 +225,16 @@ export async function runMistral(options: RunMistralOptions): Promise<RunMistral
185225
return result;
186226
};
187227

188-
const { command, args } = buildMistralExecCommand({ workingDir, prompt, model });
228+
// When resuming, use the resume prompt instead of the original prompt
229+
const effectivePrompt = resumeSessionId ? buildResumePrompt(resumePrompt) : prompt;
230+
const { command, args } = buildMistralExecCommand({ workingDir, prompt: effectivePrompt, resumeSessionId, model });
189231

190232
// Create telemetry capture instance
191233
const telemetryCapture = createTelemetryCapture('mistral', model, prompt, workingDir);
192234

193235
// Track JSON error events (Mistral may exit 0 even on errors)
194236
let capturedError: string | null = null;
237+
let sessionIdCaptured = false;
195238

196239
let result;
197240
try {
@@ -218,6 +261,13 @@ export async function runMistral(options: RunMistralOptions): Promise<RunMistral
218261
// Check for error events (Mistral may exit 0 even on errors like invalid model)
219262
try {
220263
const json = JSON.parse(line);
264+
265+
// Capture session ID from first event that contains it
266+
if (!sessionIdCaptured && json.session_id && onSessionId) {
267+
sessionIdCaptured = true;
268+
onSessionId(json.session_id);
269+
}
270+
221271
// Check for error in result type
222272
if (json.type === 'result' && json.is_error && json.result && !capturedError) {
223273
capturedError = json.result;

0 commit comments

Comments
 (0)