From 9887b258645103761fad4d7f16197740c6614046 Mon Sep 17 00:00:00 2001 From: aptdnfapt <197602950+aptdnfapt@users.noreply.github.com> Date: Sat, 3 Jan 2026 16:07:08 -0500 Subject: [PATCH] Add permission asking UI and subAgent session navigation footer (#101) * feat: add support for OpenCode permission.asked event Implement UI handling for the new permission.asked event from OpenCode's PermissionNext system, allowing tools configured with "ask" in opencode.json to prompt user for approval. Changes: - Add permission.asked event handler in useEventStream - Add patterns[] and always[] fields to Permission type - Display patterns being requested in PermissionCard - Show what "Always Allow" will auto-approve This enables the permission asking feature where tools can require user approval based on opencode.json configuration. * feat: add button to open subAgent session from task tool When an agent uses the task tool to create a subagent session, a new "Open subAgent session" button is now displayed next to the task tool output. Clicking this button navigates to the child session in the chat view. The button shows only when a sessionId is available in the task metadata and uses the external link icon to indicate it opens a separate session context. --- .../ui/src/components/chat/PermissionCard.tsx | 94 ++++++++++++++----- .../chat/message/parts/ToolPart.tsx | 42 +++++++-- packages/ui/src/hooks/useEventStream.ts | 38 +++++++- packages/ui/src/types/permission.ts | 6 ++ 4 files changed, 151 insertions(+), 29 deletions(-) diff --git a/packages/ui/src/components/chat/PermissionCard.tsx b/packages/ui/src/components/chat/PermissionCard.tsx index c882f48f..5e9cefce 100644 --- a/packages/ui/src/components/chat/PermissionCard.tsx +++ b/packages/ui/src/components/chat/PermissionCard.tsx @@ -329,7 +329,26 @@ export const PermissionCard: React.FC = ({ {}
- {} + {/* Show patterns being requested */} + {(permission.patterns as string[]) && (permission.patterns as string[]).length > 0 && ( +
+
Patterns:
+ + {(permission.patterns as string[]).join(", ")} + +
+ )} + + {!((permission.patterns as string[]) && (permission.patterns as string[]).length > 0) && + (permission.pattern as string | string[]) && +
+
Pattern:
+ + {Array.isArray(permission.pattern) ? permission.pattern.join(", ") : permission.pattern} + +
+ } + {(() => { let primaryContent = ''; @@ -441,27 +460,58 @@ export const PermissionCard: React.FC = ({ Allow Once - + {(permission.always as string[]) && (permission.always as string[]).length > 0 ? ( + + ) : ( + + )} + )} + {hasOutput ? ( -
0 && 'pt-1')} +
0 || sessionId) && 'pt-1')} > - {isOutputExpanded ? (
@@ -895,6 +916,14 @@ const ToolPart: React.FC = ({ part, isExpanded, onToggle, syntaxT const effectiveTimeStart = isTaskTool ? (pinnedTaskTimeRef.current.start ?? time?.start) : time?.start; const effectiveTimeEnd = isTaskTool ? (pinnedTaskTimeRef.current.end ?? time?.end) : time?.end; + const taskSessionId = React.useMemo(() => { + if (!isTaskTool) { + return undefined; + } + const candidate = metadata as { sessionId?: string } | undefined; + return typeof candidate?.sessionId === 'string' ? candidate.sessionId : undefined; + }, [isTaskTool, metadata]); + const taskSummaryEntries = React.useMemo(() => { if (!isTaskTool) { return []; @@ -1023,13 +1052,14 @@ const ToolPart: React.FC = ({ part, isExpanded, onToggle, syntaxT
{} - {isTaskTool && (taskSummaryEntries.length > 0 || isActive || isFinalized) ? ( + {isTaskTool && (taskSummaryEntries.length > 0 || isActive || isFinalized || taskSessionId) ? ( ) : null} diff --git a/packages/ui/src/hooks/useEventStream.ts b/packages/ui/src/hooks/useEventStream.ts index e6d596fc..545961af 100644 --- a/packages/ui/src/hooks/useEventStream.ts +++ b/packages/ui/src/hooks/useEventStream.ts @@ -955,8 +955,44 @@ export const useEventStream = () => { } break; - case 'permission.replied': + case 'permission.asked': + // New permission system from OpenCode's PermissionNext + if ('sessionID' in props && props.sessionID === currentSessionId) { + const askedProps = props as { + id: string; + permission: string; + sessionID: string; + patterns?: string[]; + always?: string[]; + metadata: Record; + tool?: { + messageID: string; + callID: string; + }; + }; + // Convert new permission.asked event format to Permission type + const permission = { + id: askedProps.id, + type: askedProps.permission, + pattern: askedProps.patterns, // Map patterns to pattern field for compatibility + sessionID: askedProps.sessionID, + messageID: askedProps.tool?.messageID || askedProps.sessionID, + callID: askedProps.tool?.callID, + title: `${askedProps.permission} permission required`, + metadata: { + ...askedProps.metadata, + always: askedProps.always, // Store always in metadata for UI access + patterns: askedProps.patterns, + }, + time: { created: Date.now() }, + } as unknown as Permission; + addPermission(permission); + } + break; + + case 'permission.replied': + // Permission was responded to - UI will update via permissionStore break; case 'todo.updated': { diff --git a/packages/ui/src/types/permission.ts b/packages/ui/src/types/permission.ts index 9d2a650a..1f157cb6 100644 --- a/packages/ui/src/types/permission.ts +++ b/packages/ui/src/types/permission.ts @@ -2,6 +2,8 @@ export interface Permission { id: string; type: string; pattern?: string | string[]; + patterns?: string[]; // New system: array of specific patterns requesting approval + always?: string[]; // New system: what will be auto-approved on "always" click sessionID: string; messageID: string; callID?: string; @@ -10,6 +12,10 @@ export interface Permission { time: { created: number; }; + tool?: { + messageID: string; + callID: string; + }; } export interface PermissionEvent {