char-slop/ai-dots
ai dotfiles
git clone https://git.t4t.associates/char-slop/ai-dots
c535dce
main
1// raw thinking notepad (based on github:lyramakesmusic/neuralese-leaker) 2 3import { Type } from "@earendil-works/pi-ai" ; 4import { type ExtensionAPI , getMarkdownTheme } from "@earendil-works/pi-coding-agent" ; 5import { Container , Markdown } from "@earendil-works/pi-tui" ; 6import { loadSettings } from "./_char_common/settings" ; 7 8const TOOL_NAME = "notepad" ; 9const PARAMETER_IDLE_TIMEOUT_MS = 10_000 ; 10 11const SCRATCH_REPLY = "Continue thinking, call a tool, or respond to the user." ; 12 13const SYSTEM_PROMPT_ADDENDUM = `# notepad 14 15- Call notepad first, before anything else, on every new task. 16- Re-enter the notepad after every tool result, before your next action.` ; 17 18export default function ( pi :ExtensionAPI ) { 19let enabled = true ; 20let watchedCallId :string | undefined ; 21let idleTimer :ReturnType < typeof setTimeout > | undefined ; 22let resumeTimer :ReturnType < typeof setTimeout > | undefined ; 23let resumeAfterAgentEnd = false ; 24const truncatedCalls = new Set < string >(); 25 26function clearIdleTimer () { 27if ( idleTimer ) clearTimeout ( idleTimer ); 28idleTimer = undefined ; 29watchedCallId = undefined ; 30} 31 32function isActive () { 33return enabled && pi . getActiveTools (). includes ( TOOL_NAME ); 34} 35 36function setActive ( on :boolean ) { 37enabled = on ; 38if ( ! on ) clearIdleTimer (); 39const active = pi . getActiveTools (); 40if ( on && ! active . includes ( TOOL_NAME )) { 41pi . setActiveTools ([ ...active , TOOL_NAME ]); 42} else if ( ! on && active . includes ( TOOL_NAME )) { 43pi . setActiveTools ( active . filter (( t ) => t !== TOOL_NAME )); 44} 45} 46 47pi . registerTool ({ 48name :TOOL_NAME , 49label :"Notepad" , 50description : 51"Notepad: Unlike typical scratchpads, it has no budget cap. " + 52"Use it before and between actions." , 53promptSnippet :"notepad; think here before and between actions" , 54promptGuidelines :[ 55"Use notepad to plan before acting, and re-enter it after every tool result." , 56], 57parameters :Type . Object ({ 58text :Type . String ({ 59description :"Working notes" , 60}), 61}), 62renderShell :"self" , 63async execute ( toolCallId ) { 64const terminate = truncatedCalls . delete ( toolCallId ); 65return { 66content :[{ type :"text" , text :SCRATCH_REPLY }], 67details :{}, 68 ...( terminate ?{ terminate :true } :{}), 69}; 70}, 71renderCall ( args , theme , context ) { 72const text = typeof args ?. text === "string" ?args . text :"" ; 73const md = 74( context . lastComponent as Markdown | undefined ) ?? 75new Markdown ( "" , 1 , 0 , getMarkdownTheme (), { 76color :( t ) => theme . fg ( "thinkingText" , t ), 77italic :true , 78}); 79md . setText ( text ); 80return md ; 81}, 82renderResult :() => new Container (), 83}); 84 85pi . on ( "session_start" , async ( _event , ctx ) => { 86const tools = loadSettings ( ctx . cwd ). tools ; 87const fromSettings = 88tools && typeof tools === "object" 89 ?( tools as Record < string , unknown >). notepad 90 :undefined ; 91enabled = fromSettings !== false ; 92if ( enabled ) setActive ( true ); 93}); 94 95pi . on ( "message_update" , ( event , ctx ) => { 96if ( ! isActive ()) return ; 97const update = event . assistantMessageEvent ; 98if ( 99update . type !== "toolcall_start" && 100update . type !== "toolcall_delta" && 101update . type !== "toolcall_end" 102) { 103return ; 104} 105 106const call = update . partial . content [ update . contentIndex ]; 107if ( call ?. type !== "toolCall" || call . name !== TOOL_NAME ) return ; 108if ( update . type === "toolcall_end" ) { 109clearIdleTimer (); 110return ; 111} 112 113clearIdleTimer (); 114watchedCallId = call . id ; 115idleTimer = setTimeout (() => { 116if ( watchedCallId !== call . id ) return ; 117clearIdleTimer (); 118truncatedCalls . add ( call . id ); 119resumeAfterAgentEnd = true ; 120ctx . abort (); 121}, PARAMETER_IDLE_TIMEOUT_MS ); 122}); 123 124// An aborted response normally skips tool execution. Reclassify only our 125// timed-out call so its partial arguments become an ordinary tool result. 126pi . on ( "message_end" , ( event ) => { 127clearIdleTimer (); 128if ( event . message . role !== "assistant" || event . message . stopReason !== "aborted" ) return ; 129 130const truncated = event . message . content . some ( 131( part ) => part . type === "toolCall" && truncatedCalls . has ( part . id ), 132); 133if ( ! truncated ) return ; 134 135const { errorMessage :_errorMessage , ...message } = event . message ; 136return { 137message :{ 138 ...message , 139stopReason :"toolUse" as const , 140content :message . content . map (( part ) => 141part . type === "toolCall" && truncatedCalls . has ( part . id ) 142 ?{ 143 ...part , 144arguments :{ 145 ...part . arguments , 146work :typeof part . arguments . work === "string" ?part . arguments . work :"" , 147}, 148} 149 :part , 150), 151}, 152}; 153}); 154 155// The salvaged result terminates the aborted run; resume once its signal is 156// gone so the next provider request gets a fresh one. 157pi . on ( "agent_end" , ( _event , ctx ) => { 158if ( ! resumeAfterAgentEnd ) return ; 159resumeAfterAgentEnd = false ; 160 161const resume = () => { 162if ( ! ctx . isIdle ()) { 163resumeTimer = setTimeout ( resume , 10 ); 164return ; 165} 166resumeTimer = undefined ; 167pi . sendMessage ( 168{ 169customType :"notepad-timeout" , 170content : 171"The previous notepad call was truncated after its arguments stopped streaming. Continue, calling notepad again if more reasoning is needed." , 172display :false , 173}, 174{ triggerTurn :true }, 175); 176}; 177resumeTimer = setTimeout ( resume , 0 ); 178}); 179 180pi . on ( "session_shutdown" , () => { 181clearIdleTimer (); 182if ( resumeTimer ) clearTimeout ( resumeTimer ); 183resumeTimer = undefined ; 184resumeAfterAgentEnd = false ; 185truncatedCalls . clear (); 186}); 187 188pi . on ( "before_agent_start" , async ( event ) => { 189if ( ! isActive ()) return ; 190pi . setThinkingLevel ( "off" ); 191if ( event . systemPrompt . includes ( "# raw thinking (notepad)" )) return ; 192return { systemPrompt :event . systemPrompt + "\n\n" + SYSTEM_PROMPT_ADDENDUM }; 193}); 194 195pi . registerCommand ( "notepad" , { 196description :"Toggle raw-thinking notepad: /notepad [on|off|status]" , 197getArgumentCompletions :( prefix ) => { 198const normalized = ( prefix || "" ). trim (). toLowerCase (); 199const items = [ "on" , "off" , "status" ]. filter (( v ) => v . startsWith ( normalized )); 200return items . length > 0 ?items . map (( value ) => ({ value, label :value })) :null ; 201}, 202handler :async ( args , ctx ) => { 203const arg = ( args || "" ). trim (). toLowerCase (); 204if ( arg === "" || arg === "status" ) { 205ctx . ui . notify ( `Notepad raw thinking: ${ isActive () ? "on" : "off" } ` , "info" ); 206return ; 207} 208if ( arg !== "on" && arg !== "off" ) { 209ctx . ui . notify ( "Usage: /notepad [on|off|status]" , "warning" ); 210return ; 211} 212setActive ( arg === "on" ); 213ctx . ui . notify ( `Notepad raw thinking: ${ arg } (this session)` , "info" ); 214}, 215}); 216}