11import { createStore } from "solid-js/store"
22import { dirname } from "node:path"
33import { createMemo , For , Match , Show , Switch } from "solid-js"
4- import { Portal , useRenderer , useTerminalDimensions , type JSX } from "@opentui/solid"
4+ import { Portal , useRenderer , useTerminalDimensions , useKeyboard , type JSX } from "@opentui/solid"
55import type { TextareaRenderable } from "@opentui/core"
66import { useTheme , selectedForeground } from "../../context/theme"
77import type { PermissionRequest } from "@opencode-ai/sdk/v2"
@@ -16,21 +16,51 @@ import { getScrollAcceleration } from "../../util/scroll"
1616import { useTuiConfig } from "../../config"
1717import { OPENCODE_BASE_MODE , useBindings , useCommandShortcut } from "../../keymap"
1818import { usePathFormatter } from "../../context/path-format"
19+ import { useDialog } from "../../ui/dialog"
1920
2021type PermissionStage = "permission" | "always" | "reject"
2122
23+ interface FileEntry {
24+ relativePath : string
25+ patch : string
26+ filePath : string
27+ type : string
28+ additions : number
29+ deletions : number
30+ }
31+
32+ function fileTitle ( file ?: FileEntry ) {
33+ if ( ! file ) return ""
34+ if ( file . type === "delete" ) return "Deleted " + file . relativePath
35+ if ( file . type === "add" ) return "Created " + file . relativePath
36+ return "Patched " + file . relativePath
37+ }
38+
2239function EditBody ( props : { request : PermissionRequest } ) {
2340const themeState = useTheme ( )
2441const theme = themeState . theme
2542const syntax = themeState . syntax
2643const config = useTuiConfig ( )
2744const dimensions = useTerminalDimensions ( )
45+ const dialog = useDialog ( )
46+
47+ const files = createMemo ( ( ) => {
48+ const raw = props . request . metadata ?. files
49+ return Array . isArray ( raw ) ? ( raw as FileEntry [ ] ) : [ ]
50+ } )
51+
52+ const [ store , setStore ] = createStore ( { fileIndex : 0 } )
53+
54+ const isMultiFile = createMemo ( ( ) => files ( ) . length > 1 )
55+ const currentFile = createMemo ( ( ) => files ( ) [ store . fileIndex ] )
2856
2957const filepath = createMemo ( ( ) => {
58+ if ( isMultiFile ( ) ) return currentFile ( ) ?. filePath ?? ""
3059const value = props . request . metadata ?. filepath
3160return typeof value === "string" ? value : ""
3261} )
3362const diff = createMemo ( ( ) => {
63+ if ( isMultiFile ( ) ) return currentFile ( ) ?. patch ?? ""
3464const value = props . request . metadata ?. diff
3565return typeof value === "string" ? value : ""
3666} )
@@ -44,8 +74,30 @@ function EditBody(props: { request: PermissionRequest }) {
4474const ft = createMemo ( ( ) => filetype ( filepath ( ) ) )
4575const scrollAcceleration = createMemo ( ( ) => getScrollAcceleration ( config ) )
4676
77+ useKeyboard ( ( evt ) => {
78+ if ( dialog . stack . length > 0 ) return
79+ if ( ! isMultiFile ( ) ) return
80+
81+ if ( evt . name === "]" ) {
82+ evt . preventDefault ( )
83+ setStore ( "fileIndex" , ( i ) => ( i + 1 ) % files ( ) . length )
84+ }
85+ if ( evt . name === "[" ) {
86+ evt . preventDefault ( )
87+ setStore ( "fileIndex" , ( i ) => ( i - 1 + files ( ) . length ) % files ( ) . length )
88+ }
89+ } )
90+
4791return (
4892< box flexDirection = "column" gap = { 1 } >
93+ < Show when = { isMultiFile ( ) } >
94+ < box flexDirection = "row" justifyContent = "space-between" paddingLeft = { 1 } paddingRight = { 1 } >
95+ < text fg = { theme . text } > { fileTitle ( currentFile ( ) ) } </ text >
96+ < text fg = { theme . textMuted } >
97+ { store . fileIndex + 1 } /{ files ( ) . length }
98+ </ text >
99+ </ box >
100+ </ Show >
49101< Show when = { diff ( ) } >
50102< scrollbox
51103height = "100%"
@@ -199,10 +251,17 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
199251if ( permission === "edit" ) {
200252const raw = props . request . metadata ?. filepath
201253const filepath = typeof raw === "string" ? raw : ""
254+ const filesArray = Array . isArray ( props . request . metadata ?. files ) ? props . request . metadata . files : [ ]
255+ const hasMultipleFiles = filesArray . length > 1
202256return {
203257icon : "→" ,
204258title : `Edit ${ pathFormatter . format ( filepath ) } ` ,
205259body : < EditBody request = { props . request } /> ,
260+ hints : hasMultipleFiles ? (
261+ < text fg = { theme . text } >
262+ { "[ ]" } < span style = { { fg : theme . textMuted } } > files</ span >
263+ </ text >
264+ ) : undefined ,
206265}
207266}
208267
@@ -405,6 +464,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
405464options = { { once : "Allow once" , always : "Allow always" , reject : "Reject" } }
406465escapeKey = "reject"
407466fullscreen
467+ hints = { current . hints }
408468onSelect = { ( option ) => {
409469if ( option === "always" ) {
410470setStore ( "stage" , "always" )
@@ -528,6 +588,7 @@ function Prompt<const T extends Record<string, string>>(props: {
528588options : T
529589escapeKey ?: keyof T
530590fullscreen ?: boolean
591+ hints ?: JSX . Element
531592onSelect : ( option : keyof T ) => void
532593} ) {
533594const { theme } = useTheme ( )
@@ -694,6 +755,7 @@ function Prompt<const T extends Record<string, string>>(props: {
694755</ For >
695756</ box >
696757< box flexDirection = "row" gap = { 2 } flexShrink = { 0 } >
758+ < Show when = { props . hints } > { props . hints } </ Show >
697759< Show when = { props . fullscreen } >
698760< text fg = { theme . text } >
699761{ fullscreenHint ( ) } < span style = { { fg : theme . textMuted } } > { hint ( ) } </ span >
0 commit comments