Skip to content

Commit e74ec3a

Browse files
committed
feat(tui): add file cycling in multi-file permission prompt (anomalyco#23262)
1 parent cc77377 commit e74ec3a

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎FORK.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ git push -u origin <your-custom-branch>
4747

4848
### 2026-07-17
4949

50+
-**Ported PR #23262 (File Cycling in Permission Prompt):** Ported the file cycling feature from upstream PR #23262 to [packages/tui/src/routes/session/permission.tsx](file:///Users/mmcdonnell/code/opencode/packages/tui/src/routes/session/permission.tsx). This enables cycling through multiple files inside the TUI permission dialog using `[` and `]` keys.
5051
-**Added [FORK.md](file:///Users/mmcdonnell/code/opencode/FORK.md):** Outlines branching, syncing, and custom workspace strategy.
5152
-**Added [Makefile](file:///Users/mmcdonnell/code/opencode/Makefile):** Standard checkmake-compliant targets for building, cleaning, testing, and syncing with automatic Bun dependency management.
5253
-**Updated [Makefile](file:///Users/mmcdonnell/code/opencode/Makefile):** Added `push` target to automate pushing the active custom branch to your fork.

‎packages/tui/src/routes/session/permission.tsx‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import{createStore}from"solid-js/store"
22
import{dirname}from"node:path"
33
import{createMemo,For,Match,Show,Switch}from"solid-js"
4-
import{Portal,useRenderer,useTerminalDimensions,typeJSX}from"@opentui/solid"
4+
import{Portal,useRenderer,useTerminalDimensions,useKeyboard,typeJSX}from"@opentui/solid"
55
importtype{TextareaRenderable}from"@opentui/core"
66
import{useTheme,selectedForeground}from"../../context/theme"
77
importtype{PermissionRequest}from"@opencode-ai/sdk/v2"
@@ -16,21 +16,51 @@ import { getScrollAcceleration } from "../../util/scroll"
1616
import{useTuiConfig}from"../../config"
1717
import{OPENCODE_BASE_MODE,useBindings,useCommandShortcut}from"../../keymap"
1818
import{usePathFormatter}from"../../context/path-format"
19+
import{useDialog}from"../../ui/dialog"
1920

2021
typePermissionStage="permission"|"always"|"reject"
2122

23+
interfaceFileEntry{
24+
relativePath: string
25+
patch: string
26+
filePath: string
27+
type: string
28+
additions: number
29+
deletions: number
30+
}
31+
32+
functionfileTitle(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+
2239
functionEditBody(props: {request: PermissionRequest}){
2340
constthemeState=useTheme()
2441
consttheme=themeState.theme
2542
constsyntax=themeState.syntax
2643
constconfig=useTuiConfig()
2744
constdimensions=useTerminalDimensions()
45+
constdialog=useDialog()
46+
47+
constfiles=createMemo(()=>{
48+
constraw=props.request.metadata?.files
49+
returnArray.isArray(raw) ? (rawasFileEntry[]) : []
50+
})
51+
52+
const[store,setStore]=createStore({fileIndex: 0})
53+
54+
constisMultiFile=createMemo(()=>files().length>1)
55+
constcurrentFile=createMemo(()=>files()[store.fileIndex])
2856

2957
constfilepath=createMemo(()=>{
58+
if(isMultiFile())returncurrentFile()?.filePath??""
3059
constvalue=props.request.metadata?.filepath
3160
returntypeofvalue==="string" ? value : ""
3261
})
3362
constdiff=createMemo(()=>{
63+
if(isMultiFile())returncurrentFile()?.patch??""
3464
constvalue=props.request.metadata?.diff
3565
returntypeofvalue==="string" ? value : ""
3666
})
@@ -44,8 +74,30 @@ function EditBody(props: { request: PermissionRequest }) {
4474
constft=createMemo(()=>filetype(filepath()))
4575
constscrollAcceleration=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+
4791
return(
4892
<boxflexDirection="column"gap={1}>
93+
<Showwhen={isMultiFile()}>
94+
<boxflexDirection="row"justifyContent="space-between"paddingLeft={1}paddingRight={1}>
95+
<textfg={theme.text}>{fileTitle(currentFile())}</text>
96+
<textfg={theme.textMuted}>
97+
{store.fileIndex+1}/{files().length}
98+
</text>
99+
</box>
100+
</Show>
49101
<Showwhen={diff()}>
50102
<scrollbox
51103
height="100%"
@@ -199,10 +251,17 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
199251
if(permission==="edit"){
200252
constraw=props.request.metadata?.filepath
201253
constfilepath=typeofraw==="string" ? raw : ""
254+
constfilesArray=Array.isArray(props.request.metadata?.files) ? props.request.metadata.files : []
255+
consthasMultipleFiles=filesArray.length>1
202256
return{
203257
icon: "→",
204258
title: `Edit ${pathFormatter.format(filepath)}`,
205259
body: <EditBodyrequest={props.request}/>,
260+
hints: hasMultipleFiles ? (
261+
<textfg={theme.text}>
262+
{"[ ]"}<spanstyle={{fg: theme.textMuted}}>files</span>
263+
</text>
264+
) : undefined,
206265
}
207266
}
208267

@@ -405,6 +464,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
405464
options={{once: "Allow once",always: "Allow always",reject: "Reject"}}
406465
escapeKey="reject"
407466
fullscreen
467+
hints={current.hints}
408468
onSelect={(option)=>{
409469
if(option==="always"){
410470
setStore("stage","always")
@@ -528,6 +588,7 @@ function Prompt<const T extends Record<string, string>>(props: {
528588
options: T
529589
escapeKey?: keyofT
530590
fullscreen?: boolean
591+
hints?: JSX.Element
531592
onSelect: (option: keyofT)=>void
532593
}){
533594
const{ theme }=useTheme()
@@ -694,6 +755,7 @@ function Prompt<const T extends Record<string, string>>(props: {
694755
</For>
695756
</box>
696757
<boxflexDirection="row"gap={2}flexShrink={0}>
758+
<Showwhen={props.hints}>{props.hints}</Show>
697759
<Showwhen={props.fullscreen}>
698760
<textfg={theme.text}>
699761
{fullscreenHint()}<spanstyle={{fg: theme.textMuted}}>{hint()}</span>

0 commit comments

Comments
 (0)