@@ -849,6 +849,30 @@ export default function ChatInput({
849849 return match [ 2 ] && match [ 2 ] . length > 0 ? hint : ` ${ hint } ` ;
850850 } , [ buffer . text ] ) ;
851851
852+ // 当输入为以 `/cmd` 开头且 cmd 命中已注册指令时,计算高亮长度(含开头的 `/`)。
853+ // 用于在输入框中以主题色高亮“完整指令”片段,方便用户确认命令已被识别。
854+ const completedCommandLength = useMemo ( ( ) => {
855+ const text = buffer . text ;
856+ if ( ! text . startsWith ( '/' ) ) return 0 ;
857+ const match = text . match ( / ^ \/ ( [ a - z A - Z 0 - 9 _ - ] + ) (?: \s | $ ) / ) ;
858+ if ( ! match ) return 0 ;
859+ const cmd = match [ 1 ] ?? '' ;
860+ if ( ! cmd ) return 0 ;
861+ const allCommands = getAllCommands ( ) ;
862+ // 精确匹配(如 /help、/clear、/branch ...)
863+ const exact = allCommands . some ( c => c . name === cmd ) ;
864+ if ( exact ) return 1 + cmd . length ;
865+ // 前缀型指令(如 agent-、todo-、skills-):cmd 以 `name` 开头且长度更长
866+ const prefixHit = allCommands . some (
867+ c =>
868+ c . name . endsWith ( '-' ) &&
869+ cmd . length > c . name . length &&
870+ cmd . startsWith ( c . name ) ,
871+ ) ;
872+ if ( prefixHit ) return 1 + cmd . length ;
873+ return 0 ;
874+ } , [ buffer . text , getAllCommands ] ) ;
875+
852876 const renderContent = ( ) => {
853877 if ( buffer . text . length > 0 ) {
854878 // Use visual lines for proper wrapping and multi-line support
@@ -889,13 +913,114 @@ export default function ChatInput({
889913 ) ;
890914 }
891915
916+ // 渲染单行内容的辅助函数:同时高亮
917+ // 1. 首行已识别的完整指令 `/cmd`
918+ // 2. 各类 `[...]` 占位符标签(Paste / image / Skill / GitLine / » running-agent)
919+ // 3. `#agent_xxx` 裸文本子代理标签(词边界上)
920+ const renderLineSegments = ( line : string , isFirstLine : boolean ) => {
921+ type Token = { text : string ; highlight : boolean } ;
922+ const tokens : Token [ ] = [ ] ;
923+ let plainBuf = '' ;
924+ const flushPlain = ( ) => {
925+ if ( plainBuf ) {
926+ tokens . push ( { text : plainBuf , highlight : false } ) ;
927+ plainBuf = '' ;
928+ }
929+ } ;
930+
931+ let i = 0 ;
932+
933+ // 1) 首行完整指令高亮
934+ if (
935+ isFirstLine &&
936+ completedCommandLength > 0 &&
937+ line . length >= completedCommandLength
938+ ) {
939+ tokens . push ( {
940+ text : line . slice ( 0 , completedCommandLength ) ,
941+ highlight : true ,
942+ } ) ;
943+ i = completedCommandLength ;
944+ }
945+
946+ const isPlaceholderTag = ( tag : string ) =>
947+ / ^ \[ P a s t e \d + l i n e s # \d + \] $ / . test ( tag ) ||
948+ / ^ \[ i m a g e # \d + \] $ / . test ( tag ) ||
949+ / ^ \[ S k i l l : [ ^ \] ] + \] $ / . test ( tag ) ||
950+ / ^ \[ G i t L i n e : [ ^ \] ] + \] $ / . test ( tag ) ||
951+ / ^ \[ » [ ^ \] ] * \] $ / . test ( tag ) ;
952+
953+ while ( i < line . length ) {
954+ const ch = line [ i ] ;
955+
956+ // 2) [...] 占位符标签
957+ if ( ch === '[' ) {
958+ const closeIdx = line . indexOf ( ']' , i + 1 ) ;
959+ if ( closeIdx !== - 1 ) {
960+ const tagText = line . slice ( i , closeIdx + 1 ) ;
961+ if ( isPlaceholderTag ( tagText ) ) {
962+ flushPlain ( ) ;
963+ // 标签本身高亮;可能紧跟一个分隔空格,空格不高亮
964+ tokens . push ( { text : tagText , highlight : true } ) ;
965+ i = closeIdx + 1 ;
966+ continue ;
967+ }
968+ }
969+ }
970+
971+ // 3) #agent 裸文本标签:需要词边界(前面是行首或空白,后面是行末或空白)
972+ if ( ch === '#' ) {
973+ const prevCh = i === 0 ? '' : line [ i - 1 ] ?? '' ;
974+ const leftBoundary = i === 0 || / \s / . test ( prevCh ) ;
975+ if ( leftBoundary ) {
976+ const rest = line . slice ( i ) ;
977+ const m = rest . match ( / ^ # [ A - Z a - z ] [ \w - ] * / ) ;
978+ if ( m ) {
979+ const nextIdx = i + m [ 0 ] . length ;
980+ const nextCh = line [ nextIdx ] ;
981+ const rightBoundary = ! nextCh || / \s / . test ( nextCh ) ;
982+ if ( rightBoundary ) {
983+ flushPlain ( ) ;
984+ tokens . push ( { text : m [ 0 ] , highlight : true } ) ;
985+ i = nextIdx ;
986+ continue ;
987+ }
988+ }
989+ }
990+ }
991+
992+ plainBuf += ch ;
993+ i ++ ;
994+ }
995+ flushPlain ( ) ;
996+
997+ if ( tokens . length === 0 ) {
998+ return < Text > { line || ' ' } </ Text > ;
999+ }
1000+
1001+ return (
1002+ < >
1003+ { tokens . map ( ( tok , idx ) =>
1004+ tok . highlight ? (
1005+ < Text key = { idx } color = { theme . colors . menuInfo } bold >
1006+ { tok . text }
1007+ </ Text >
1008+ ) : (
1009+ < Text key = { idx } > { tok . text } </ Text >
1010+ ) ,
1011+ ) }
1012+ </ >
1013+ ) ;
1014+ } ;
1015+
8921016 for ( let i = startLine ; i < endLine ; i ++ ) {
8931017 const line = visualLines [ i ] || '' ;
1018+ const isFirstLine = i === 0 ;
8941019
8951020 if ( i === cursorRow ) {
8961021 renderedLines . push (
8971022 < Box key = { i } flexDirection = "row" >
898- < Text > { line || ' ' } </ Text >
1023+ { renderLineSegments ( line , isFirstLine ) }
8991024 { commandArgsHint && i === visualLines . length - 1 ? (
9001025 < Text color = { theme . colors . menuSecondary } dimColor >
9011026 { commandArgsHint }
@@ -904,7 +1029,11 @@ export default function ChatInput({
9041029 </ Box > ,
9051030 ) ;
9061031 } else {
907- renderedLines . push ( < Text key = { i } > { line || ' ' } </ Text > ) ;
1032+ renderedLines . push (
1033+ < Box key = { i } flexDirection = "row" >
1034+ { renderLineSegments ( line , isFirstLine ) }
1035+ </ Box > ,
1036+ ) ;
9081037 }
9091038 }
9101039
0 commit comments