Skip to content

Commit 0c2b4b3

Browse files
committed
feat: add /tooltest slash command and bump iOS deployment target to 26.0
- Add `toolTest(suite:)` case to `SlashCommand` enum for running chat-driven V2 tool test suites - Implement `/tooltest [suite|list]` parsing in `SlashCommandParser` supporting optional suite ID argument - Add `executeToolTestCommand` to dispatch to `ToolTestSuiteService`, listing suites or running a specified suite with missing-tool validation - Update help text and extended documentation to document the new `/tooltest` command - Set `IPHONEOS_DEPLOYMENT_TARGET` to 26.0 in both Debug and Release build configurations
1 parent e99cb90 commit 0c2b4b3

8 files changed

Lines changed: 1117 additions & 39 deletions

Axon.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,7 @@
717717
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Axon provides a local API server to allow developer tools like Cline to access your conversations via OpenAI-compatible endpoints.";
718718
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Axon needs access to your photos to attach images to messages.";
719719
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
720+
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
720721
LD_RUNPATH_SEARCH_PATHS = (
721722
"$(inherited)",
722723
"@executable_path/Frameworks",
@@ -783,6 +784,7 @@
783784
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Axon provides a local API server to allow developer tools like Cline to access your conversations via OpenAI-compatible endpoints.";
784785
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Axon needs access to your photos to attach images to messages.";
785786
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
787+
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
786788
LD_RUNPATH_SEARCH_PATHS = (
787789
"$(inherited)",
788790
"@executable_path/Frameworks",

Axon/Services/Chat/SlashCommandParser.swift

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Foundation
1414
enum SlashCommand {
1515
case tool(toolId: String) // Show tool definition to AI
1616
case use(toolId: String) // User directly invokes a tool
17+
case toolTest(suite: String?) // Run chat-driven tool suite
1718
case listTools
1819
case help
1920
case privateThread // Mark thread as private (no AI)
@@ -110,6 +111,13 @@ class SlashCommandParser {
110111
}
111112
return .use(toolId: toolId)
112113

114+
case "tooltest":
115+
if let args {
116+
let trimmedArgs = args.trimmingCharacters(in: .whitespacesAndNewlines)
117+
return .toolTest(suite: trimmedArgs.isEmpty ? nil : trimmedArgs)
118+
}
119+
return .toolTest(suite: nil)
120+
113121
case "tools", "listtools", "list_tools":
114122
return .listTools
115123

@@ -151,6 +159,9 @@ class SlashCommandParser {
151159
success: true
152160
)
153161

162+
case .toolTest(let suite):
163+
return await executeToolTestCommand(suite: suite)
164+
154165
case .listTools:
155166
return await executeListToolsCommand()
156167

@@ -179,6 +190,7 @@ class SlashCommandParser {
179190
**Available commands:**
180191
- `/tool <tool_id>` - Show tool definition to AI
181192
- `/use <tool_id>` - Directly invoke a tool yourself
193+
- `/tooltest [suite|list]` - Run chat-driven V2 tool tests
182194
- `/tools` - List all available tools
183195
- `/private` - Start a private thread (no AI)
184196
- `/sync` - Enable temporal symmetry (time + turns)
@@ -303,6 +315,10 @@ class SlashCommandParser {
303315
### /tools
304316
List all available tools organized by category.
305317
318+
### /tooltest [suite_id]
319+
Run chat-driven V2 tool test suites through normal `tool_request` blocks.
320+
Examples: `/tooltest`, `/tooltest core_v2_safe`, `/tooltest list`
321+
306322
### /private
307323
Start a private thread where Axon will not respond.
308324
Must be the first message in a new conversation.
@@ -431,6 +447,103 @@ class SlashCommandParser {
431447
)
432448
}
433449

450+
private func executeToolTestCommand(suite: String?) async -> SlashCommandResult {
451+
let service = ToolTestSuiteService.shared
452+
let normalized = suite?
453+
.trimmingCharacters(in: .whitespacesAndNewlines)
454+
.lowercased()
455+
let requestedSuite = (normalized?.isEmpty ?? true) ? ToolTestSuiteService.defaultSuiteId : normalized!
456+
457+
if requestedSuite == "list" {
458+
return SlashCommandResult(
459+
command: .toolTest(suite: suite),
460+
displayText: "/tooltest list",
461+
resultText: service.renderSuiteListMarkdown(),
462+
success: true
463+
)
464+
}
465+
466+
guard service.hasSuite(requestedSuite) else {
467+
return SlashCommandResult(
468+
command: .toolTest(suite: suite),
469+
displayText: "/tooltest \(requestedSuite)",
470+
resultText: """
471+
Unknown tool test suite: `\(requestedSuite)`.
472+
473+
\(service.renderSuiteListMarkdown())
474+
""",
475+
success: false
476+
)
477+
}
478+
479+
guard ToolsV2Toggle.shared.isV2Active else {
480+
return SlashCommandResult(
481+
command: .toolTest(suite: suite),
482+
displayText: suite == nil ? "/tooltest" : "/tooltest \(requestedSuite)",
483+
resultText: """
484+
Cannot run `\(requestedSuite)` while ToolsV2 is inactive.
485+
486+
Enable **Plugin-Based (V2)** in Settings > Tools, then retry:
487+
- `/tooltest \(requestedSuite)`
488+
""",
489+
success: false
490+
)
491+
}
492+
493+
if ToolPluginLoader.shared.loadedTools.isEmpty {
494+
await ToolPluginLoader.shared.loadAllTools()
495+
}
496+
497+
let requiredTools = Set(service.requiredTools(for: requestedSuite))
498+
let loadedById = Dictionary(uniqueKeysWithValues: ToolPluginLoader.shared.loadedTools.map { ($0.id, $0) })
499+
500+
let missingTools = requiredTools.filter { loadedById[$0] == nil }.sorted()
501+
if !missingTools.isEmpty {
502+
return SlashCommandResult(
503+
command: .toolTest(suite: suite),
504+
displayText: suite == nil ? "/tooltest" : "/tooltest \(requestedSuite)",
505+
resultText: """
506+
Cannot run `\(requestedSuite)` because required tools are not loaded:
507+
\(missingTools.map { "- `\($0)`" }.joined(separator: "\n"))
508+
509+
Open Settings > Tools (V2) and reload tool manifests, then retry.
510+
""",
511+
success: false
512+
)
513+
}
514+
515+
let disabledTools = requiredTools.filter { loadedById[$0]?.isEnabled == false }.sorted()
516+
if !disabledTools.isEmpty {
517+
return SlashCommandResult(
518+
command: .toolTest(suite: suite),
519+
displayText: suite == nil ? "/tooltest" : "/tooltest \(requestedSuite)",
520+
resultText: """
521+
Cannot run `\(requestedSuite)` because required tools are disabled:
522+
\(disabledTools.map { "- `\($0)`" }.joined(separator: "\n"))
523+
524+
Enable these tools in Settings > Tools (V2), then retry.
525+
""",
526+
success: false
527+
)
528+
}
529+
530+
guard let markdown = service.renderSuiteMarkdown(suiteId: requestedSuite) else {
531+
return SlashCommandResult(
532+
command: .toolTest(suite: suite),
533+
displayText: suite == nil ? "/tooltest" : "/tooltest \(requestedSuite)",
534+
resultText: "Failed to render suite `\(requestedSuite)`.",
535+
success: false
536+
)
537+
}
538+
539+
return SlashCommandResult(
540+
command: .toolTest(suite: suite),
541+
displayText: suite == nil ? "/tooltest" : "/tooltest \(requestedSuite)",
542+
resultText: markdown,
543+
success: true
544+
)
545+
}
546+
434547
// MARK: - Helpers
435548

436549
private func generateToolDetails(for tool: ToolId) -> String {
@@ -563,6 +676,14 @@ class SlashCommandParser {
563676
icon: "list.bullet",
564677
hasSubmenu: false
565678
),
679+
SlashCommandSuggestion(
680+
id: "tooltest",
681+
command: "tooltest",
682+
displayName: "Tool Test",
683+
description: "Run chat-driven V2 tool suites",
684+
icon: "checklist",
685+
hasSubmenu: false
686+
),
566687
SlashCommandSuggestion(
567688
id: "help",
568689
command: "help",

0 commit comments

Comments
 (0)