@@ -27,7 +27,10 @@ import { StreamingStatus } from './components/StreamingStatus';
2727import { TodoPanel } from './components/panels/TodoPanel' ;
2828import { ActiveAgentsPanel } from './components/panels/ActiveAgentsPanel' ;
2929import { WelcomeHeader } from './components/WelcomeHeader' ;
30- import { ApprovalModeDialog } from './components/dialogs/ApprovalModeDialog' ;
30+ import {
31+ APPROVAL_MODE_ACTIVE_EVENT ,
32+ ApprovalModeMessage ,
33+ } from './components/messages/ApprovalModeMessage' ;
3134import { ResumeDialog } from './components/dialogs/ResumeDialog' ;
3235import {
3336 AGENTS_ACTIVE_EVENT ,
@@ -97,13 +100,42 @@ import type {
97100} from './adapters/types' ;
98101import { extractTodosFromToolCall , hasActiveTodos } from './utils/todos' ;
99102import { ThemeProvider } from './themeContext' ;
103+ import {
104+ WebShellCustomizationProvider ,
105+ type WebShellMarkdownCustomization ,
106+ type ToolHeaderExtraRenderer ,
107+ } from './customization' ;
100108import styles from './App.module.css' ;
101109
102110export const CompactModeContext = createContext ( false ) ;
103111
104112const MODES_CYCLE = DAEMON_APPROVAL_MODES ;
105113const MAX_DISPLAYED_QUEUED_PROMPTS = 3 ;
106114const MAX_QUEUED_PROMPT_PREVIEW_CHARS = 240 ;
115+ const COMPACT_MODE_STORAGE_KEY = 'web-shell:compact-mode' ;
116+
117+ function loadCompactMode ( ) : boolean {
118+ try {
119+ return window . localStorage . getItem ( COMPACT_MODE_STORAGE_KEY ) === 'true' ;
120+ } catch {
121+ return false ;
122+ }
123+ }
124+
125+ function saveCompactMode ( enabled : boolean ) {
126+ try {
127+ window . localStorage . setItem (
128+ COMPACT_MODE_STORAGE_KEY ,
129+ enabled ? 'true' : 'false' ,
130+ ) ;
131+ } catch {
132+ // Ignore storage failures in private browsing or restricted contexts.
133+ }
134+ }
135+
136+ function normalizeHiddenCommand ( command : string ) : string {
137+ return command . trim ( ) . replace ( / ^ \/ + / , '' ) . toLowerCase ( ) ;
138+ }
107139
108140interface QueuedPrompt {
109141 id : number ;
@@ -153,6 +185,12 @@ export interface WebShellProps {
153185 onError ?: ( error : Error ) => void ;
154186 /** Called when `/bug` is invoked. Receives system info. If omitted, web-shell opens the report URL itself. */
155187 onBugReport ?: ( info : BugReportInfo ) => void ;
188+ /** Slash command names to hide from completion/help, for example `['approval-mode']`. */
189+ hiddenSlashCommands ?: string [ ] ;
190+ /** Custom renderer for the tool-card header content after the status icon and tool name. */
191+ renderToolHeaderExtra ?: ToolHeaderExtraRenderer ;
192+ /** Custom Markdown behavior for assistant content only. */
193+ markdown ?: WebShellMarkdownCustomization ;
156194}
157195
158196function replaceSessionUrl ( sessionId : string ) : void {
@@ -438,6 +476,9 @@ export function App({
438476 onStreamingStateChange,
439477 onError,
440478 onBugReport,
479+ hiddenSlashCommands,
480+ renderToolHeaderExtra,
481+ markdown,
441482} : WebShellProps = { } ) {
442483 const [ selectedLanguage , setSelectedLanguage ] = useState < WebShellLanguage > (
443484 ( ) =>
@@ -446,6 +487,10 @@ export function App({
446487 : normalizeLanguage ( providedLanguage ) ,
447488 ) ;
448489 const t = useMemo ( ( ) => getTranslator ( selectedLanguage ) , [ selectedLanguage ] ) ;
490+ const customization = useMemo (
491+ ( ) => ( { renderToolHeaderExtra, markdown } ) ,
492+ [ renderToolHeaderExtra , markdown ] ,
493+ ) ;
449494 const store = useTranscriptStore ( ) ;
450495 const blocks = useTranscriptBlocks ( ) ;
451496 const connection = useConnection ( ) ;
@@ -568,7 +613,7 @@ export function App({
568613
569614 const [ modelInlineMode , setModelInlineMode ] =
570615 useState < ModelInlineMode | null > ( null ) ;
571- const [ showModeDialog , setShowModeDialog ] = useState ( false ) ;
616+ const [ approvalModeInlineOpen , setApprovalModeInlineOpen ] = useState ( false ) ;
572617 const [ showResumeDialog , setShowResumeDialog ] = useState ( false ) ;
573618 const [ showDeleteDialog , setShowDeleteDialog ] = useState ( false ) ;
574619 const [ showReleaseDialog , setShowReleaseDialog ] = useState ( false ) ;
@@ -586,6 +631,7 @@ export function App({
586631 const [ memoryPortalHost , setMemoryPortalHost ] =
587632 useState < HTMLDivElement | null > ( null ) ;
588633 const [ showShortcuts , setShowShortcuts ] = useState ( false ) ;
634+ const approvalModePanelActive = usePanelActive ( APPROVAL_MODE_ACTIVE_EVENT ) ;
589635 const mcpPanelActive = usePanelActive ( MCP_STATUS_ACTIVE_EVENT ) ;
590636 const agentsPanelActive = usePanelActive ( AGENTS_ACTIVE_EVENT ) ;
591637 const memoryPanelActive = usePanelActive ( MEMORY_ACTIVE_EVENT ) ;
@@ -603,7 +649,6 @@ export function App({
603649 const nextQueuedPromptIdRef = useRef ( 1 ) ;
604650 const drainingQueueRef = useRef ( false ) ;
605651 const dialogOpen =
606- showModeDialog ||
607652 showResumeDialog ||
608653 showDeleteDialog ||
609654 showReleaseDialog ||
@@ -612,6 +657,7 @@ export function App({
612657 showToolsDialog ;
613658 const bottomHidden =
614659 dialogOpen ||
660+ approvalModePanelActive ||
615661 mcpPanelActive ||
616662 agentsPanelActive ||
617663 memoryPanelActive ||
@@ -854,7 +900,7 @@ export function App({
854900 setShowShortcuts ( ( prev ) => ! prev ) ;
855901 } , [ ] ) ;
856902
857- const [ compactMode , setCompactMode ] = useState ( false ) ;
903+ const [ compactMode , setCompactMode ] = useState ( loadCompactMode ) ;
858904 const compactModeRef = useRef ( compactMode ) ;
859905 compactModeRef . current = compactMode ;
860906
@@ -869,13 +915,8 @@ export function App({
869915 const handleToggleCompact = useCallback ( ( ) => {
870916 const next = ! compactModeRef . current ;
871917 setCompactMode ( next ) ;
872- store . dispatch ( [
873- {
874- type : 'status' ,
875- text : next ? t ( 'compact.enabled' ) : t ( 'compact.disabled' ) ,
876- } ,
877- ] ) ;
878- } , [ store , t ] ) ;
918+ saveCompactMode ( next ) ;
919+ } , [ ] ) ;
879920
880921 const handleSetMode = useCallback (
881922 ( modeId : string ) => {
@@ -1192,12 +1233,13 @@ export function App({
11921233 } ) ;
11931234 return true ;
11941235 }
1195- if ( cmd === 'approval-mode' || cmd === 'mode' ) {
1236+ if ( cmd === 'approval-mode' ) {
11961237 const modeArg = text . slice ( match [ 0 ] . length ) . trim ( ) ;
11971238 if ( modeArg ) {
11981239 handleSetMode ( modeArg ) ;
11991240 } else {
1200- setShowModeDialog ( true ) ;
1241+ store . appendLocalUserMessage ( text ) ;
1242+ setApprovalModeInlineOpen ( true ) ;
12011243 }
12021244 return true ;
12031245 }
@@ -1374,7 +1416,9 @@ export function App({
13741416 return true ;
13751417 }
13761418 if ( cmd === 'clear' ) {
1377- store . reset ( ) ;
1419+ sessionActions . newSession ( ) . catch ( ( error : unknown ) => {
1420+ reportError ( error , 'Failed to create a new session' ) ;
1421+ } ) ;
13781422 return true ;
13791423 }
13801424 if ( cmd === 'new' || cmd === 'reset' ) {
@@ -1791,13 +1835,17 @@ export function App({
17911835
17921836 const commands = useMemo ( ( ) => {
17931837 const skillNames = new Set ( connection . skills ?? [ ] ) ;
1794- return mergeCommands ( connection . commands ?? [ ] , getLocalCommands ( t ) ) . map (
1795- ( command ) =>
1838+ const hidden = new Set (
1839+ ( hiddenSlashCommands ?? [ ] ) . map ( normalizeHiddenCommand ) . filter ( Boolean ) ,
1840+ ) ;
1841+ return mergeCommands ( connection . commands ?? [ ] , getLocalCommands ( t ) )
1842+ . filter ( ( command ) => ! hidden . has ( normalizeHiddenCommand ( command . name ) ) )
1843+ . map ( ( command ) =>
17961844 skillNames . has ( command . name )
17971845 ? { ...command , description : t ( 'skills.run' ) }
17981846 : command ,
1799- ) ;
1800- } , [ connection . commands , connection . skills , t ] ) ;
1847+ ) ;
1848+ } , [ connection . commands , connection . skills , hiddenSlashCommands , t ] ) ;
18011849
18021850 const welcomeHeader = useMemo (
18031851 ( ) => (
@@ -1893,13 +1941,6 @@ export function App({
18931941 onClose = { ( ) => setShowReleaseDialog ( false ) }
18941942 />
18951943 ) }
1896- { showModeDialog && (
1897- < ApprovalModeDialog
1898- currentMode = { currentMode }
1899- onSelect = { handleSetMode }
1900- onClose = { ( ) => setShowModeDialog ( false ) }
1901- />
1902- ) }
19031944 { showHelpDialog && (
19041945 < HelpDialog
19051946 commands = { commands }
@@ -1919,81 +1960,96 @@ export function App({
19191960 </ div >
19201961 ) }
19211962
1922- < CompactModeContext . Provider value = { compactMode } >
1923- < div
1924- className = {
1925- floatingTodos . length > 0 || floatingAgents . length > 0
1926- ? `${ styles . content } ${ styles . contentHasMessages } `
1927- : styles . content
1928- }
1929- style = { dialogOpen ? { visibility : 'hidden' } : undefined }
1930- >
1931- < MessageList
1932- messages = { displayMessages }
1933- pendingApproval = { pendingApproval }
1934- onConfirm = { handleConfirm }
1935- catchingUp = { connection . catchingUp }
1936- workspaceCwd = { connection . workspaceCwd || '' }
1937- welcomeHeader = { welcomeHeader }
1938- tailContent = {
1939- agentsInlineMode || memoryInlineOpen || modelInlineMode ? (
1940- < >
1941- { modelInlineMode && (
1942- < ModelMessage
1943- mode = { modelInlineMode }
1944- onSelect = {
1945- modelInlineMode === 'fast'
1946- ? handleFastModelSelect
1947- : handleModelSelect
1948- }
1949- onClose = { ( ) => setModelInlineMode ( null ) }
1950- />
1951- ) }
1952- { agentsInlineMode && (
1953- < AgentsMessage
1954- mode = { agentsInlineMode }
1955- onMessage = { ( text ) =>
1956- store . dispatch ( [ { type : 'status' , text } ] )
1957- }
1958- onClose = { ( ) => setAgentsInlineMode ( null ) }
1959- />
1960- ) }
1961- { memoryInlineOpen && (
1962- < MemoryMessage
1963- refreshSignal = { memoryRefreshSignal }
1964- addSignal = { memoryAddSignal }
1965- addScope = { memoryAddScope }
1966- portalHost = { memoryPortalHost }
1967- onMessage = { ( text , type = 'status' ) => {
1968- store . dispatch ( [ { type, text } ] ) ;
1969- } }
1970- onClose = { ( ) => setMemoryInlineOpen ( false ) }
1971- />
1972- ) }
1973- </ >
1974- ) : undefined
1975- }
1976- tailKey = {
1977- agentsInlineMode || memoryInlineOpen || modelInlineMode
1978- ? `inline-${ modelInlineMode ?? 'none' } -${ agentsInlineMode ?? 'none' } -${ memoryInlineOpen ? 'memory' : 'none' } `
1979- : undefined
1963+ < WebShellCustomizationProvider value = { customization } >
1964+ < CompactModeContext . Provider value = { compactMode } >
1965+ < div
1966+ className = {
1967+ floatingTodos . length > 0 || floatingAgents . length > 0
1968+ ? `${ styles . content } ${ styles . contentHasMessages } `
1969+ : styles . content
19801970 }
1981- />
1982-
1983- { btwMessage ?. role === 'btw' && (
1984- < div className = { styles . btwPanel } >
1985- < BtwMessage
1986- question = { btwMessage . question }
1987- answer = { btwMessage . answer }
1988- isPending = { btwMessage . isPending }
1989- />
1990- </ div >
1991- ) }
1971+ style = { dialogOpen ? { visibility : 'hidden' } : undefined }
1972+ >
1973+ < MessageList
1974+ messages = { displayMessages }
1975+ pendingApproval = { pendingApproval }
1976+ onConfirm = { handleConfirm }
1977+ catchingUp = { connection . catchingUp }
1978+ workspaceCwd = { connection . workspaceCwd || '' }
1979+ welcomeHeader = { welcomeHeader }
1980+ tailContent = {
1981+ agentsInlineMode ||
1982+ memoryInlineOpen ||
1983+ modelInlineMode ||
1984+ approvalModeInlineOpen ? (
1985+ < >
1986+ { approvalModeInlineOpen && (
1987+ < ApprovalModeMessage
1988+ currentMode = { currentMode }
1989+ onSelect = { handleSetMode }
1990+ onClose = { ( ) => setApprovalModeInlineOpen ( false ) }
1991+ />
1992+ ) }
1993+ { modelInlineMode && (
1994+ < ModelMessage
1995+ mode = { modelInlineMode }
1996+ onSelect = {
1997+ modelInlineMode === 'fast'
1998+ ? handleFastModelSelect
1999+ : handleModelSelect
2000+ }
2001+ onClose = { ( ) => setModelInlineMode ( null ) }
2002+ />
2003+ ) }
2004+ { agentsInlineMode && (
2005+ < AgentsMessage
2006+ mode = { agentsInlineMode }
2007+ onMessage = { ( text ) =>
2008+ store . dispatch ( [ { type : 'status' , text } ] )
2009+ }
2010+ onClose = { ( ) => setAgentsInlineMode ( null ) }
2011+ />
2012+ ) }
2013+ { memoryInlineOpen && (
2014+ < MemoryMessage
2015+ refreshSignal = { memoryRefreshSignal }
2016+ addSignal = { memoryAddSignal }
2017+ addScope = { memoryAddScope }
2018+ portalHost = { memoryPortalHost }
2019+ onMessage = { ( text , type = 'status' ) => {
2020+ store . dispatch ( [ { type, text } ] ) ;
2021+ } }
2022+ onClose = { ( ) => setMemoryInlineOpen ( false ) }
2023+ />
2024+ ) }
2025+ </ >
2026+ ) : undefined
2027+ }
2028+ tailKey = {
2029+ agentsInlineMode ||
2030+ memoryInlineOpen ||
2031+ modelInlineMode ||
2032+ approvalModeInlineOpen
2033+ ? `inline-${ modelInlineMode ?? 'none' } -${ agentsInlineMode ?? 'none' } -${ memoryInlineOpen ? 'memory' : 'none' } -${ approvalModeInlineOpen ? 'approval' : 'none' } `
2034+ : undefined
2035+ }
2036+ />
19922037
1993- < StreamingStatus />
1994- </ div >
1995- < div ref = { setMemoryPortalHost } data-web-shell-overlay-root />
1996- </ CompactModeContext . Provider >
2038+ { btwMessage ?. role === 'btw' && (
2039+ < div className = { styles . btwPanel } >
2040+ < BtwMessage
2041+ question = { btwMessage . question }
2042+ answer = { btwMessage . answer }
2043+ isPending = { btwMessage . isPending }
2044+ />
2045+ </ div >
2046+ ) }
2047+
2048+ < StreamingStatus />
2049+ </ div >
2050+ < div ref = { setMemoryPortalHost } data-web-shell-overlay-root />
2051+ </ CompactModeContext . Provider >
2052+ </ WebShellCustomizationProvider >
19972053
19982054 < div
19992055 className = {
0 commit comments