@@ -13,6 +13,7 @@ import {
1313 CliRenderEvents ,
1414 MarkdownRenderable ,
1515 ScrollBoxRenderable ,
16+ SyntaxStyle ,
1617 TextRenderable ,
1718 TextTableRenderable ,
1819 StyledText ,
@@ -47,6 +48,11 @@ import {
4748 type SentHistoryBrowse ,
4849} from "../tui/sent-message-history.js"
4950import { spliceMentionCompletion } from "./prompt-attachments.js"
51+ import {
52+ resolvePromptHighlightSpans ,
53+ resolvePromptRecognitionMatcher ,
54+ type PromptRecognitionSource ,
55+ } from "./prompt-recognition.js"
5056import {
5157 createPromptInput ,
5258 promptCaretAtFirstRow ,
@@ -354,6 +360,17 @@ export function setMentionSuggestionSource(
354360 else shellMentionSource . delete ( shell )
355361}
356362
363+ /** Names the prompt is allowed to highlight as recognized skills/agents. */
364+ const shellRecognitionSource = new WeakMap < AppShell , PromptRecognitionSource > ( )
365+
366+ export function setPromptRecognitionSource (
367+ shell : AppShell ,
368+ source : PromptRecognitionSource | undefined ,
369+ ) : void {
370+ if ( source ) shellRecognitionSource . set ( shell , source )
371+ else shellRecognitionSource . delete ( shell )
372+ }
373+
357374/**
358375 * Injectable handler for the palette "observe" action. Host resolves a live
359376 * `ObserveSession` (or `null` when no subagent is running). Demo/smoke keep
@@ -1704,6 +1721,50 @@ export function syncPromptRows(shell: AppShell): void {
17041721 relayout ( shell , { promptContentRows : rows } )
17051722}
17061723
1724+ let cachedPromptSyntaxStyle : SyntaxStyle | null = null
1725+ let cachedPromptRecognizedStyleId : number | null = null
1726+
1727+ /**
1728+ * The style registry backing the prompt's highlights, plus the one style id
1729+ * this feature uses. Lazy for the same reason as `transcriptSyntaxStyle`:
1730+ * construction reaches into the native render lib.
1731+ */
1732+ function promptRecognizedStyleId ( ) : number {
1733+ if ( cachedPromptSyntaxStyle === null ) {
1734+ cachedPromptSyntaxStyle = SyntaxStyle . fromStyles ( {
1735+ recognized : { fg : UI . action } ,
1736+ } )
1737+ }
1738+ if ( cachedPromptRecognizedStyleId === null ) {
1739+ cachedPromptRecognizedStyleId = cachedPromptSyntaxStyle . resolveStyleId ( "recognized" ) ?? 0
1740+ }
1741+ return cachedPromptRecognizedStyleId
1742+ }
1743+
1744+ const promptHighlightedValue = new WeakMap < AppShell , string > ( )
1745+
1746+ /**
1747+ * Re-mark recognized skill/agent tokens in the prompt. Runs once per frame
1748+ * (see `onFrame` in `createShell`), and only does anything when the prompt's
1749+ * text actually changed since the last frame — typing that doesn't touch a
1750+ * token, and every non-typing frame, is a no-op string comparison.
1751+ */
1752+ export function syncPromptHighlights ( shell : AppShell ) : void {
1753+ const source = shellRecognitionSource . get ( shell )
1754+ if ( source === undefined ) return
1755+ const value = shell . prompt . value
1756+ if ( promptHighlightedValue . get ( shell ) === value ) return
1757+ promptHighlightedValue . set ( shell , value )
1758+
1759+ const styleId = promptRecognizedStyleId ( )
1760+ shell . prompt . syntaxStyle = cachedPromptSyntaxStyle
1761+ shell . prompt . clearAllHighlights ( )
1762+ const matcher = resolvePromptRecognitionMatcher ( source )
1763+ for ( const span of resolvePromptHighlightSpans ( value , matcher ) ) {
1764+ shell . prompt . addHighlightByCharRange ( { start : span . start , end : span . end , styleId } )
1765+ }
1766+ }
1767+
17071768export type RelayoutOpts = {
17081769 readonly columns ?: number
17091770 readonly rows ?: number
@@ -5513,6 +5574,7 @@ export function createAppShell(
55135574 const onFrame = ( ) : void => {
55145575 if ( disposed ) return
55155576 syncPromptRows ( shell )
5577+ syncPromptHighlights ( shell )
55165578 // Applied after a natural render, not at mutation time: a row's own box
55175579 // needs a layout pass to size itself, and claiming the padding first
55185580 // starves that pass of room to lay the row out in.
0 commit comments