Skip to content

Approval Flow#1

Merged
jamesrochabrun merged 15 commits intomainfrom
jroch-main
Jun 2, 2025
Merged

Approval Flow#1
jamesrochabrun merged 15 commits intomainfrom
jroch-main

Conversation

@jamesrochabrun
Copy link
Copy Markdown
Collaborator

Added approval flow.

  • Saving Always approved tools in user defaults
  • Showing UI for approval flow
  • Handling denied tool sending message to chat history with error description
  • Added new button to show label + icons

TODO:

  • Address feedback
  • Tests

Screenshot 2025-05-30 at 2 25 17 AM

@jamesrochabrun
Copy link
Copy Markdown
Collaborator Author

I was having troubles withe push flow, so I just pushed all as no verify to get initial feedback, also the app suddenly looses the ability to access Xcode workspace which makes a bit hard to test this reliable. Maybe we can have a quick chat to debug.

Copy link
Copy Markdown
Collaborator

@gsabran gsabran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

Left a few comments.

For the UI, it seems that we'll have to use a vertical layout as it's getting compacted in the sidebar

Screenshot 2025-05-30 at 8 44 59 AM

What do you think of some layout like:

List files
{ args... }
Do you want to proceeed?
Yes, always allow (key shortcut)
Yes (key shortcut)
No (key shortcut)


do {
// Request approval before executing the tool
try await context.requestToolApproval(toolUse)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to put this before startExecuting (it's called in two different places). Here the tool call has already been started, and we're waiting for its result.
Screenshot 2025-05-30 at 8 15 28 AM

Comment on lines +44 to +57
public enum LLMServiceError: Error {
case toolUsageDenied
}

extension LLMServiceError: LocalizedError {
public var errorDescription: String? {
switch self {
case .toolUsageDenied:
return "User denied permission to execute this tool. Please suggest an alternative approach or ask for clarification."
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if you don't care about having a specific error type and just want a localized error, you can use AppFoundation.AppError

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is relevant as we are sending this message in to the chat conversation history so the LLM knows what to do next, the LLM has no concept of our UX so we need to give it hints about what is happening and how to recover

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that AppError can preserve your message without requiring a dedicated type, but you would not be able to do something like

catch error as LLMServiceError {

} catch {

}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh so you already have a custom error? yeah I can use that I guess

enum ApprovalResult {
case approved
case denied
case alwaysApprove(toolName: String)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also "toolUseCancel" ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of denied?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in addition


// MARK: - ToolApprovalRequest

// TODO: james - should move this `ToolFoundation`?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we're not using this outside of Chat I'd say no, but if we do yes

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deal

}

private func storeAlwaysApprovePreference(for toolName: String) {
userDefaults.set(true, forKey: "\(Self.userDefaultsAlwaysApproveKey)\(toolName)")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 using user default is ok for now. I think eventually we'll want to move this to the Setting, so that we can display this in the settings and users can also update them from there.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I can se this moving there eventually.

@jamesrochabrun
Copy link
Copy Markdown
Collaborator Author

I added the changes based on the feedback, I am not quite sure why yet but if I denied the usage of a tool e,g the command tool, the tool heeader message still shows "Tool use: execute_command" any pointer?
Screenshot 2025-05-31 at 12 46 45 AM

@jamesrochabrun jamesrochabrun requested a review from gsabran May 31, 2025 07:47
@gsabran
Copy link
Copy Markdown
Collaborator

gsabran commented May 31, 2025

(just pushed a merge commit to make changes easier to review)

Task { [weak self] in
guard let self else { return }
do {
try await self.context.requestToolApproval(toolUse)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you made a good point about the tool use showing up in the UI. If we don't want to show it, we could ask for approval before inserting the tool use in the chat history (here and here).

But I can see this being an issue because we likely want to keep the tool use in the history, even if it is rejected, so that the next message has the rejection and the LLM can continue based of that.

What do you think of instead modifying the Tool protocol to better describe the pending approval / rejected status? We could then have a standard UI for a rejected tool use. We can do this in a follow up PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of instead modifying the Tool protocol to better describe the pending approval / rejected status? We could then have a standard UI for a rejected tool use. We can do this in a follow up PR.

I think this is the correct move yes.

@gsabran
Copy link
Copy Markdown
Collaborator

gsabran commented May 31, 2025

Not for now, but we can think about whether we need to give a reason when rejecting tool to help the LLM make progress

Screenshot 2025-05-30 at 8 49 45 AM

@gsabran
Copy link
Copy Markdown
Collaborator

gsabran commented Jun 1, 2025

Looks great overall. Nit: can we update this to:
"cmd wants to use the list files tool"
always allow: ⌘↵⇧, allow once: ⌘↵, reject: ⌘⌫
Screenshot 2025-05-31 at 8 47 32 PM

(clarifying what is being ask, putting the ⌘ key first to be on brand, slight simplification for reject, starting with the most favorable option)

@jamesrochabrun
Copy link
Copy Markdown
Collaborator Author

2 pending jobs Failing

Run cd app && ./cmd.sh clean && ./cmd.sh test
Command not found: test
Error: Process completed with exit code 1.

I am also getting this error locally. cc @gsabran

@gsabran
Copy link
Copy Markdown
Collaborator

gsabran commented Jun 1, 2025

oh I got some CI credits back. Yeah! No worries about CI. Could you actually comment out

pull_request:
branches: [ "main" ]

here https://github.com/gsabran/cmd/blob/e9b18b952262c519c0232aa9b71d2c20eb0cbd06/.github/workflows/main.yml#L9-L10

So that I keep my CI credits to test automating releasing the app?

push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @gsabran like this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes perfect

}

close_xcode() {
if pgrep -x "Xcode" > /dev/null; then
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @gsabran changes made by linter

# So we need to exit with a non-zero exit code to indicate that the build needs to be restarted with the correct files.
echo "The local server has been updated. Please restart the build." >&2
exit 1
fi
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @gsabran changes mad by linter

name: "Bash",
parameters: {
command: "cd /Users/me/command/app/modules && swift test 2>&1 | head -100",
command: "cd /Users/guigui/dev/Xcompanion/app/modules && swift test 2>&1 | head -100",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @gsabran changes made by linter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh really? Weird I'll look into it

@jamesrochabrun
Copy link
Copy Markdown
Collaborator Author

I pushed some changes made by the linter, tagged you on those in case you want to take a look. Thanks!

Comment on lines +43 to +48
let action: () -> Void
let systemNames: [String]
let label: String
let onHoverColor: Color
let padding: CGFloat
let cornerRadius: CGFloat
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: private? 🤷‍♂️

push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes perfect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants