feat: Story 8 - Collapsible tool outputs + autonomous coding improvements

Implemented Story 8: Collapsible Tool Outputs
- Tool outputs now render in <details>/<summary> elements, collapsed by default
- Summary shows tool name with key argument (e.g., ▶ read_file(src/main.rs))
- Added arrow rotation animation and scrollable content (max 300px)
- Enhanced tool_calls display to show arguments inline
- Added CSS styling for dark theme consistency

Fixed: LLM autonomous coding behavior
- Strengthened system prompt with explicit examples and directives
- Implemented triple-reinforcement system (primary prompt + reminder + message prefixes)
- Improved tool descriptions to be more explicit and action-oriented
- Increased MAX_TURNS from 10 to 30 for complex agentic workflows
- Added debug logging for Ollama requests/responses
- Result: GPT-OSS (gpt-oss:20b) now successfully uses write_file autonomously

Documentation improvements
- Created MODEL_SELECTION.md guide with recommendations
- Updated PERSONA.md spec to emphasize autonomous agent behavior
- Updated UI_UX.md spec with collapsible tool output requirements
- Updated SDSW workflow: LLM archives stories and performs squash merge

Cleanup
- Removed unused ToolTester.tsx component
This commit is contained in:
Dave
2025-12-25 15:18:12 +00:00
parent c493da2f2a
commit 990441dfc1
17 changed files with 471 additions and 172 deletions

View File

@@ -17,7 +17,7 @@ pub struct ProviderConfig {
pub enable_tools: Option<bool>,
}
const MAX_TURNS: usize = 10;
const MAX_TURNS: usize = 30;
#[tauri::command]
pub async fn get_ollama_models(base_url: Option<String>) -> Result<Vec<String>, String> {
@@ -53,6 +53,16 @@ pub async fn chat(
// 3. Agent Loop
let mut current_history = messages.clone();
// Prefix user messages with reminder for stubborn models
for msg in &mut current_history {
if msg.role == Role::User && !msg.content.starts_with("[AGENT DIRECTIVE]") {
msg.content = format!(
"[AGENT DIRECTIVE: You must use write_file tool to implement changes. Never suggest code.]\n\n{}",
msg.content
);
}
}
// Inject System Prompt
current_history.insert(
0,
@@ -64,6 +74,17 @@ pub async fn chat(
},
);
// Inject aggressive reminder as a second system message
current_history.insert(
1,
Message {
role: Role::System,
content: "CRITICAL REMINDER: When the user asks you to create, modify, or implement code, you MUST call the write_file tool with the complete file content. DO NOT output code in markdown blocks. DO NOT suggest what the user should do. TAKE ACTION IMMEDIATELY using tools.".to_string(),
tool_calls: None,
tool_call_id: None,
},
);
let mut new_messages: Vec<Message> = Vec::new();
let mut turn_count = 0;
@@ -91,8 +112,8 @@ pub async fn chat(
current_history.push(assistant_msg.clone());
new_messages.push(assistant_msg);
// Emit history excluding system prompt (index 0)
app.emit("chat:update", &current_history[1..])
// Emit history excluding system prompts (indices 0 and 1)
app.emit("chat:update", &current_history[2..])
.map_err(|e| e.to_string())?;
// Execute Tools
@@ -110,8 +131,8 @@ pub async fn chat(
current_history.push(tool_msg.clone());
new_messages.push(tool_msg);
// Emit history excluding system prompt (index 0)
app.emit("chat:update", &current_history[1..])
// Emit history excluding system prompts (indices 0 and 1)
app.emit("chat:update", &current_history[2..])
.map_err(|e| e.to_string())?;
}
} else {
@@ -126,8 +147,8 @@ pub async fn chat(
// We don't push to current_history needed for next loop, because we are done.
new_messages.push(assistant_msg.clone());
current_history.push(assistant_msg);
// Emit history excluding system prompt (index 0)
app.emit("chat:update", &current_history[1..])
// Emit history excluding system prompts (indices 0 and 1)
app.emit("chat:update", &current_history[2..])
.map_err(|e| e.to_string())?;
break;
}
@@ -200,11 +221,11 @@ fn get_tool_definitions() -> Vec<ToolDefinition> {
kind: "function".to_string(),
function: ToolFunctionDefinition {
name: "read_file".to_string(),
description: "Reads the content of a file in the project.".to_string(),
description: "Reads the complete content of a file from the project. Use this to understand existing code before making changes.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Relative path to the file" }
"path": { "type": "string", "description": "Relative path to the file from project root" }
},
"required": ["path"]
}),
@@ -214,12 +235,12 @@ fn get_tool_definitions() -> Vec<ToolDefinition> {
kind: "function".to_string(),
function: ToolFunctionDefinition {
name: "write_file".to_string(),
description: "Writes content to a file. Overwrites if exists.".to_string(),
description: "Creates or completely overwrites a file with new content. YOU MUST USE THIS to implement code changes - do not suggest code to the user. The content parameter must contain the COMPLETE file including all imports, functions, and unchanged code.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Relative path to the file" },
"content": { "type": "string", "description": "The full content to write" }
"path": { "type": "string", "description": "Relative path to the file from project root" },
"content": { "type": "string", "description": "The complete file content to write (not a diff or partial code)" }
},
"required": ["path", "content"]
}),
@@ -229,11 +250,11 @@ fn get_tool_definitions() -> Vec<ToolDefinition> {
kind: "function".to_string(),
function: ToolFunctionDefinition {
name: "list_directory".to_string(),
description: "Lists files and directories at a path.".to_string(),
description: "Lists all files and directories at a given path. Use this to explore the project structure.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Relative path to list (use '.' for root)" }
"path": { "type": "string", "description": "Relative path to list (use '.' for project root)" }
},
"required": ["path"]
}),
@@ -243,12 +264,12 @@ fn get_tool_definitions() -> Vec<ToolDefinition> {
kind: "function".to_string(),
function: ToolFunctionDefinition {
name: "search_files".to_string(),
description: "Searches for text content across all files in the project."
description: "Searches for text patterns across all files in the project. Use this to find functions, variables, or code patterns when you don't know which file they're in."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "The string to search for" }
"query": { "type": "string", "description": "The text pattern to search for across all files" }
},
"required": ["query"]
}),
@@ -258,18 +279,18 @@ fn get_tool_definitions() -> Vec<ToolDefinition> {
kind: "function".to_string(),
function: ToolFunctionDefinition {
name: "exec_shell".to_string(),
description: "Executes a shell command in the project root.".to_string(),
description: "Executes a shell command in the project root directory. Use this to run tests, build commands, git operations, or any command-line tool. Examples: cargo check, npm test, git status.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The command to run (e.g., 'git', 'cargo', 'ls')"
"description": "The command binary to execute (e.g., 'git', 'cargo', 'npm', 'ls')"
},
"args": {
"type": "array",
"items": { "type": "string" },
"description": "Arguments for the command"
"description": "Array of arguments to pass to the command (e.g., ['status'] for git status)"
}
},
"required": ["command", "args"]