Skip to content

Commit 2e8f4c0

Browse files
devantlerclaude
andauthored
feat(chat): richer permission prompt details (#5338)
Populate the TUI permission modal's "Arguments:" line with concise, kind-specific context so the user sees more when approving an operation: - Shell: the command warning, if present (⚠ <warning>). - MCP: the server name, plus "(read-only)" when the tool is read-only. - Write: "New file" when creating a new file (NewFileContents set). Adds a permissionArguments helper (type switch over the v1.0.0 concrete pointer request types) and wires it into CreateTUIPermissionHandler in place of the hardcoded empty arguments string. Covered by table-driven tests. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6af93a9 commit 2e8f4c0

3 files changed

Lines changed: 130 additions & 1 deletion

File tree

pkg/cli/ui/chat/export_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ var ExportSetLastUsageModel = func(m *Model, model string) {
147147
// ExportExtractPermissionDetails exposes extractPermissionDetails for testing.
148148
var ExportExtractPermissionDetails = extractPermissionDetails
149149

150+
// ExportPermissionArguments exposes permissionArguments for testing.
151+
var ExportPermissionArguments = permissionArguments
152+
150153
// ExportFormatPermissionKind exposes formatPermissionKind for testing.
151154
var ExportFormatPermissionKind = formatPermissionKind
152155

pkg/cli/ui/chat/permission.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func CreateTUIPermissionHandler(
289289
toolCallID: toolCallID,
290290
toolName: toolName,
291291
command: command,
292-
arguments: "",
292+
arguments: permissionArguments(request),
293293
response: responseChan,
294294
}
295295

@@ -370,6 +370,35 @@ func extensionPermissionDetail(request copilot.PermissionRequest) string {
370370
return ""
371371
}
372372

373+
// permissionArguments returns a short, kind-specific line of extra context for a
374+
// permission request, shown to the user under the "Arguments:" label in the modal.
375+
// It returns "" when there's nothing useful to add for the request's kind.
376+
func permissionArguments(request copilot.PermissionRequest) string {
377+
switch req := request.(type) {
378+
case *copilot.PermissionRequestShell:
379+
if warning := derefString(req.Warning); warning != "" {
380+
return "⚠ " + warning
381+
}
382+
case *copilot.PermissionRequestMCP:
383+
if req.ServerName == "" {
384+
return ""
385+
}
386+
387+
detail := "Server: " + req.ServerName
388+
if req.ReadOnly {
389+
detail += " (read-only)"
390+
}
391+
392+
return detail
393+
case *copilot.PermissionRequestWrite:
394+
if req.NewFileContents != nil {
395+
return "New file"
396+
}
397+
}
398+
399+
return ""
400+
}
401+
373402
// permissionToolCallID extracts the tool-call ID from a permission request, if present.
374403
// Each concrete request variant carries its own optional ToolCallID field.
375404
func permissionToolCallID(request copilot.PermissionRequest) string {

pkg/cli/ui/chat/permission_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,103 @@ func TestPermissionModal_AllowThisOperation(t *testing.T) {
449449
}
450450
}
451451

452+
// assertPermissionArguments runs a table of permissionArguments cases.
453+
func assertPermissionArguments(t *testing.T, tests []struct {
454+
name string
455+
request copilot.PermissionRequest
456+
want string
457+
},
458+
) {
459+
t.Helper()
460+
461+
for _, testCase := range tests {
462+
t.Run(testCase.name, func(t *testing.T) {
463+
t.Parallel()
464+
465+
got := chat.ExportPermissionArguments(testCase.request)
466+
if got != testCase.want {
467+
t.Errorf("permissionArguments() = %q, want %q", got, testCase.want)
468+
}
469+
})
470+
}
471+
}
472+
473+
// TestPermissionArguments_Shell tests shell-warning extraction for permission requests.
474+
func TestPermissionArguments_Shell(t *testing.T) {
475+
t.Parallel()
476+
477+
assertPermissionArguments(t, []struct {
478+
name string
479+
request copilot.PermissionRequest
480+
want string
481+
}{
482+
{
483+
name: "shell with warning",
484+
request: &copilot.PermissionRequestShell{
485+
FullCommandText: "rm -rf /",
486+
Warning: new("dangerous"),
487+
},
488+
want: "⚠ dangerous",
489+
},
490+
{
491+
name: "shell without warning",
492+
request: &copilot.PermissionRequestShell{
493+
FullCommandText: "ls",
494+
},
495+
want: "",
496+
},
497+
{
498+
name: "read has no extra context",
499+
request: &copilot.PermissionRequestRead{Path: "/etc/config.yaml"},
500+
want: "",
501+
},
502+
})
503+
}
504+
505+
// TestPermissionArguments_MCPAndWrite tests MCP server and write new-file extraction.
506+
func TestPermissionArguments_MCPAndWrite(t *testing.T) {
507+
t.Parallel()
508+
509+
assertPermissionArguments(t, []struct {
510+
name string
511+
request copilot.PermissionRequest
512+
want string
513+
}{
514+
{
515+
name: "mcp with server name",
516+
request: &copilot.PermissionRequestMCP{
517+
ServerName: "ksail",
518+
ToolName: "cluster_create",
519+
},
520+
want: "Server: ksail",
521+
},
522+
{
523+
name: "mcp read-only",
524+
request: &copilot.PermissionRequestMCP{
525+
ServerName: "ksail",
526+
ToolName: "cluster_list",
527+
ReadOnly: true,
528+
},
529+
want: "Server: ksail (read-only)",
530+
},
531+
{
532+
name: "write new file",
533+
request: &copilot.PermissionRequestWrite{
534+
FileName: "/tmp/output.txt",
535+
NewFileContents: new("hello"),
536+
},
537+
want: "New file",
538+
},
539+
{
540+
name: "write existing file",
541+
request: &copilot.PermissionRequestWrite{
542+
FileName: "/tmp/output.txt",
543+
},
544+
want: "",
545+
},
546+
})
547+
}
548+
452549
// TestPermissionModal_AllowAlwaysSwitchesToAutopilot tests that pressing 'a' on the
453550
// permission prompt switches to Autopilot mode and approves the request.
454551
func TestPermissionModal_AllowAlwaysSwitchesToAutopilot(t *testing.T) {

0 commit comments

Comments
 (0)