Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112121131211412115121161211712118121191212012121121221212312124121251212612127121281212912130121311213212133121341213512136121371213812139121401214112142121431214412145121461214712148121491215012151121521215312154121551215612157121581215912160121611216212163121641216512166121671216812169121701217112172121731217412175121761217712178121791218012181121821218312184121851218612187121881218912190121911219212193121941219512196121971219812199122001220112202122031220412205122061220712208122091221012211122121221312214122151221612217122181221912220122211222212223122241222512226122271222812229122301223112232122331223412235122361223712238122391224012241122421224312244122451224612247122481224912250122511225212253122541225512256122571225812259122601226112262122631226412265122661226712268122691227012271122721227312274122751227612277122781227912280122811228212283122841228512286122871228812289122901229112292122931229412295122961229712298122991230012301123021230312304123051230612307123081230912310123111231212313123141231512316123171231812319123201232112322123231232412325123261232712328123291233012331123321233312334123351233612337123381233912340123411234212343123441234512346123471234812349123501235112352123531235412355123561235712358123591236012361123621236312364123651236612367123681236912370123711237212373123741237512376123771237812379123801238112382123831238412385123861238712388123891239012391123921239312394123951239612397123981239912400124011240212403124041240512406124071240812409124101241112412124131241412415124161241712418124191242012421124221242312424124251242612427124281242912430124311243212433124341243512436124371243812439124401244112442124431244412445124461244712448124491245012451124521245312454124551245612457124581245912460124611246212463124641246512466124671246812469124701247112472124731247412475124761247712478124791248012481124821248312484124851248612487124881248912490124911249212493124941249512496124971249812499125001250112502125031250412505125061250712508125091251012511125121251312514125151251612517125181251912520125211252212523125241252512526125271252812529125301253112532125331253412535125361253712538125391254012541125421254312544125451254612547125481254912550125511255212553125541255512556125571255812559125601256112562125631256412565125661256712568125691257012571125721257312574125751257612577125781257912580125811258212583125841258512586125871258812589125901259112592125931259412595125961259712598125991260012601126021260312604126051260612607126081260912610126111261212613126141261512616126171261812619126201262112622126231262412625126261262712628126291263012631126321263312634126351263612637126381263912640126411264212643126441264512646126471264812649126501265112652126531265412655126561265712658126591266012661126621266312664126651266612667126681266912670126711267212673126741267512676126771267812679126801268112682126831268412685126861268712688126891269012691126921269312694126951269612697126981269912700127011270212703127041270512706127071270812709127101271112712127131271412715127161271712718127191272012721127221272312724127251272612727127281272912730127311273212733127341273512736127371273812739127401274112742127431274412745127461274712748127491275012751127521275312754127551275612757127581275912760127611276212763127641276512766127671276812769127701277112772127731277412775127761277712778127791278012781127821278312784127851278612787127881278912790127911279212793127941279512796127971279812799128001280112802128031280412805128061280712808128091281012811128121281312814128151281612817128181281912820128211282212823128241282512826128271282812829128301283112832128331283412835128361283712838128391284012841128421284312844128451284612847128481284912850128511285212853128541285512856128571285812859128601286112862128631286412865128661286712868128691287012871128721287312874128751287612877128781287912880128811288212883128841288512886128871288812889128901289112892128931289412895128961289712898128991290012901129021290312904129051290612907129081290912910129111291212913129141291512916129171291812919129201292112922129231292412925129261292712928129291293012931129321293312934129351293612937129381293912940129411294212943129441294512946129471294812949129501295112952129531295412955129561295712958129591296012961129621296312964129651296612967129681296912970129711297212973129741297512976129771297812979129801298112982129831298412985129861298712988129891299012991129921299312994129951299612997129981299913000130011300213003130041300513006130071300813009130101301113012130131301413015130161301713018130191302013021130221302313024130251302613027130281302913030130311303213033130341303513036130371303813039130401304113042130431304413045130461304713048130491305013051130521305313054130551305613057130581305913060130611306213063130641306513066130671306813069130701307113072130731307413075130761307713078130791308013081130821308313084130851308613087130881308913090130911309213093130941309513096130971309813099131001310113102131031310413105131061310713108131091311013111131121311313114131151311613117131181311913120131211312213123131241312513126131271312813129131301313113132131331313413135131361313713138131391314013141131421314313144131451314613147131481314913150131511315213153131541315513156131571315813159131601316113162131631316413165131661316713168131691317013171131721317313174131751317613177131781317913180131811318213183131841318513186131871318813189131901319113192131931319413195131961319713198131991320013201132021320313204132051320613207132081320913210132111321213213132141321513216132171321813219132201322113222132231322413225132261322713228132291323013231132321323313234132351323613237132381323913240132411324213243132441324513246132471324813249132501325113252132531325413255132561325713258132591326013261132621326313264132651326613267132681326913270132711327213273132741327513276132771327813279132801328113282132831328413285132861328713288132891329013291132921329313294132951329613297132981329913300133011330213303133041330513306133071330813309133101331113312133131331413315133161331713318133191332013321133221332313324133251332613327133281332913330133311333213333133341333513336133371333813339133401334113342133431334413345133461334713348133491335013351133521335313354133551335613357133581335913360133611336213363133641336513366133671336813369133701337113372133731337413375133761337713378133791338013381133821338313384133851338613387133881338913390133911339213393133941339513396133971339813399134001340113402134031340413405134061340713408134091341013411134121341313414134151341613417134181341913420134211342213423134241342513426134271342813429134301343113432134331343413435134361343713438134391344013441134421344313444134451344613447134481344913450134511345213453134541345513456134571345813459134601346113462134631346413465134661346713468134691347013471134721347313474134751347613477134781347913480134811348213483134841348513486134871348813489134901349113492134931349413495134961349713498134991350013501135021350313504135051350613507135081350913510135111351213513135141351513516135171351813519135201352113522135231352413525135261352713528135291353013531135321353313534135351353613537135381353913540135411354213543135441354513546135471354813549135501355113552135531355413555135561355713558135591356013561135621356313564135651356613567135681356913570135711357213573135741357513576135771357813579135801358113582135831358413585135861358713588135891359013591135921359313594135951359613597135981359913600136011360213603136041360513606136071360813609136101361113612136131361413615136161361713618136191362013621136221362313624136251362613627136281362913630136311363213633136341363513636136371363813639136401364113642136431364413645136461364713648136491365013651136521365313654136551365613657136581365913660136611366213663136641366513666136671366813669136701367113672136731367413675136761367713678136791368013681136821368313684136851368613687136881368913690136911369213693136941369513696136971369813699137001370113702137031370413705137061370713708137091371013711137121371313714137151371613717137181371913720137211372213723137241372513726137271372813729137301373113732137331373413735137361373713738137391374013741137421374313744137451374613747137481374913750137511375213753137541375513756137571375813759137601376113762137631376413765137661376713768137691377013771137721377313774137751377613777137781377913780137811378213783137841378513786137871378813789137901379113792137931379413795137961379713798137991380013801138021380313804138051380613807138081380913810138111381213813138141381513816138171381813819138201382113822138231382413825138261382713828138291383013831138321383313834138351383613837138381383913840138411384213843138441384513846138471384813849138501385113852138531385413855138561385713858138591386013861138621386313864138651386613867138681386913870138711387213873138741387513876138771387813879138801388113882138831388413885138861388713888138891389013891138921389313894138951389613897138981389913900139011390213903139041390513906139071390813909139101391113912139131391413915139161391713918139191392013921139221392313924139251392613927139281392913930139311393213933139341393513936139371393813939139401394113942139431394413945139461394713948139491395013951139521395313954139551395613957139581395913960139611396213963139641396513966139671396813969139701397113972139731397413975139761397713978139791398013981139821398313984139851398613987139881398913990139911399213993139941399513996139971399813999140001400114002140031400414005140061400714008140091401014011140121401314014140151401614017140181401914020140211402214023140241402514026140271402814029140301403114032140331403414035140361403714038140391404014041140421404314044140451404614047140481404914050140511405214053140541405514056140571405814059140601406114062140631406414065140661406714068140691407014071140721407314074140751407614077140781407914080140811408214083140841408514086140871408814089140901409114092140931409414095140961409714098140991410014101141021410314104141051410614107141081410914110141111411214113141141411514116141171411814119141201412114122141231412414125141261412714128141291413014131141321413314134141351413614137141381413914140141411414214143141441414514146141471414814149141501415114152141531415414155141561415714158141591416014161141621416314164141651416614167141681416914170141711417214173141741417514176141771417814179141801418114182141831418414185141861418714188141891419014191141921419314194141951419614197141981419914200142011420214203142041420514206142071420814209142101421114212142131421414215142161421714218142191422014221142221422314224142251422614227142281422914230142311423214233142341423514236142371423814239142401424114242142431424414245142461424714248142491425014251142521425314254142551425614257142581425914260142611426214263142641426514266142671426814269142701427114272142731427414275142761427714278142791428014281142821428314284142851428614287142881428914290142911429214293142941429514296142971429814299143001430114302143031430414305143061430714308143091431014311143121431314314143151431614317143181431914320143211432214323143241432514326143271432814329143301433114332143331433414335143361433714338143391434014341143421434314344143451434614347143481434914350143511435214353143541435514356143571435814359143601436114362143631436414365143661436714368143691437014371143721437314374143751437614377143781437914380143811438214383143841438514386143871438814389143901439114392143931439414395143961439714398143991440014401144021440314404144051440614407144081440914410144111441214413144141441514416144171441814419144201442114422144231442414425144261442714428144291443014431144321443314434144351443614437144381443914440144411444214443144441444514446144471444814449144501445114452144531445414455144561445714458144591446014461144621446314464144651446614467144681446914470144711447214473144741447514476144771447814479144801448114482144831448414485144861448714488144891449014491144921449314494144951449614497144981449914500145011450214503145041450514506145071450814509145101451114512145131451414515145161451714518145191452014521145221452314524145251452614527145281452914530145311453214533145341453514536145371453814539145401454114542145431454414545145461454714548145491455014551145521455314554145551455614557145581455914560145611456214563145641456514566145671456814569145701457114572145731457414575145761457714578145791458014581145821458314584145851458614587145881458914590145911459214593145941459514596145971459814599146001460114602146031460414605146061460714608146091461014611146121461314614146151461614617146181461914620146211462214623146241462514626146271462814629146301463114632146331463414635146361463714638146391464014641146421464314644146451464614647146481464914650146511465214653146541465514656146571465814659146601466114662146631466414665146661466714668146691467014671146721467314674146751467614677146781467914680146811468214683146841468514686146871468814689146901469114692146931469414695146961469714698146991470014701147021470314704147051470614707147081470914710147111471214713147141471514716147171471814719147201472114722147231472414725147261472714728147291473014731147321473314734147351473614737147381473914740147411474214743147441474514746147471474814749147501475114752147531475414755147561475714758147591476014761147621476314764147651476614767147681476914770147711477214773147741477514776147771477814779147801478114782147831478414785147861478714788147891479014791147921479314794147951479614797147981479914800148011480214803148041480514806148071480814809148101481114812148131481414815148161481714818148191482014821148221482314824148251482614827148281482914830148311483214833148341483514836148371483814839148401484114842148431484414845148461484714848148491485014851148521485314854148551485614857148581485914860148611486214863148641486514866148671486814869148701487114872148731487414875148761487714878148791488014881148821488314884148851488614887148881488914890148911489214893148941489514896148971489814899149001490114902149031490414905149061490714908149091491014911149121491314914149151491614917149181491914920149211492214923149241492514926149271492814929149301493114932149331493414935149361493714938149391494014941149421494314944149451494614947149481494914950149511495214953149541495514956149571495814959149601496114962149631496414965149661496714968149691497014971149721497314974149751497614977149781497914980149811498214983149841498514986149871498814989149901499114992149931499414995149961499714998149991500015001150021500315004150051500615007150081500915010150111501215013150141501515016150171501815019150201502115022150231502415025150261502715028150291503015031150321503315034150351503615037150381503915040150411504215043150441504515046150471504815049150501505115052150531505415055150561505715058150591506015061150621506315064150651506615067150681506915070150711507215073150741507515076150771507815079150801508115082150831508415085150861508715088150891509015091150921509315094150951509615097150981509915100151011510215103151041510515106151071510815109151101511115112151131511415115151161511715118151191512015121151221512315124151251512615127151281512915130151311513215133151341513515136151371513815139151401514115142151431514415145151461514715148151491515015151151521515315154151551515615157151581515915160151611516215163151641516515166151671516815169151701517115172151731517415175151761517715178151791518015181151821518315184151851518615187151881518915190151911519215193151941519515196151971519815199152001520115202152031520415205152061520715208152091521015211152121521315214152151521615217152181521915220152211522215223152241522515226152271522815229152301523115232152331523415235152361523715238152391524015241152421524315244152451524615247152481524915250152511525215253152541525515256152571525815259152601526115262152631526415265152661526715268152691527015271152721527315274152751527615277152781527915280152811528215283152841528515286152871528815289152901529115292152931529415295152961529715298152991530015301153021530315304153051530615307153081530915310153111531215313153141531515316153171531815319153201532115322153231532415325153261532715328153291533015331153321533315334153351533615337153381533915340153411534215343153441534515346153471534815349153501535115352153531535415355153561535715358153591536015361153621536315364153651536615367153681536915370153711537215373153741537515376153771537815379153801538115382153831538415385153861538715388153891539015391153921539315394153951539615397153981539915400154011540215403154041540515406154071540815409154101541115412154131541415415154161541715418154191542015421154221542315424154251542615427154281542915430154311543215433154341543515436154371543815439154401544115442154431544415445154461544715448154491545015451154521545315454154551545615457154581545915460154611546215463154641546515466154671546815469154701547115472154731547415475154761547715478154791548015481154821548315484154851548615487154881548915490154911549215493154941549515496154971549815499155001550115502155031550415505155061550715508155091551015511155121551315514155151551615517155181551915520155211552215523155241552515526155271552815529155301553115532155331553415535155361553715538155391554015541155421554315544155451554615547155481554915550155511555215553155541555515556155571555815559155601556115562155631556415565155661556715568155691557015571155721557315574155751557615577const std = @import("std");const file_picker_path = @import("../input/file_picker_path.zig");const question_prompt = @import("../agent/question_prompt.zig");const app_auth_runtime = @import("app_auth_runtime.zig");const app_permission_runtime = @import("app_permission_runtime.zig");const app_session_runtime = @import("app_session_runtime.zig");const app_workspace_runtime = @import("app_workspace_runtime.zig");const app_commands = @import("app_commands.zig");const project_config = @import("../mcp/project_config.zig");const mcp_menu_state = @import("../mcp/menu_state.zig");const app_worker_runtime = @import("app_worker_runtime.zig");const app_render_runtime = @import("app_render_runtime.zig");const auth_runtime = @import("../auth/auth_runtime.zig");const host = @import("../hosts/host.zig");const runtime_profile = @import("../hosts/runtime_profile.zig");const composer_insertion = @import("../input/composer_insertion.zig");const composer_stash = @import("../input/composer_stash.zig");const gesture_state = @import("../input/gesture_state.zig");const horizontal_navigation = @import("../input/horizontal_navigation.zig");const input_action = @import("../input/input_action.zig");const transcript_presentation = @import("../output/transcript_presentation.zig");const core_input_runtime = @import("../input/runtime.zig");const input_limit_rejection = @import("../input/input_limit_rejection.zig");const input_reset = @import("../input/input_reset.zig");const picker_state = @import("../input/picker_state.zig");const paste_framing = @import("../input/paste_framing.zig");const text_scalar = @import("../input/text_scalar.zig");const io_mod = @import("../shared/io.zig");const debug_trace = @import("../shared/debug_trace.zig");const text_utils = @import("../shared/text_utils.zig");const image_attachments = @import("../images/image_attachments.zig");const image_commands = @import("../images/image_commands.zig");const model_capabilities = @import("../config/model_capabilities.zig");const model_cache_runtime = @import("model_cache_runtime.zig");const permission_request = @import("../permissions/permission_request.zig");const approval_decision = @import("../permissions/approval_decision.zig");const permissions = @import("../permissions/permissions.zig");const session_commands = @import("../session/session_commands.zig");const session_catalog = @import("../session/session_catalog.zig");const session_runtime = @import("../session/session.zig");const session_store = @import("../session/session_store.zig");const usage_report = @import("../session/usage_report.zig");const skill_runtime = @import("../skills/skill_runtime.zig");const file_index = @import("../workspace/file_index.zig");const command_specs = @import("../slash_commands/command_specs.zig");const types = @import("../shared/types.zig");const worker_runtime = @import("../agent/worker_runtime.zig");const auto_upgrade = @import("../upgrade/auto_upgrade.zig");const upgrade_helpers = @import("../upgrade/upgrade_helpers.zig");const test_ui_input = @import("../../ui/input/runtime.zig");const visual_layout = @import("../../ui/input/visual_layout.zig");const approval_ui = @import("../../ui/footer/approval_ui.zig");const interaction_state = @import("../../ui/footer/interaction_state.zig");const approval_prompt = @import("../permissions/approval_prompt.zig");const picker_presentation = @import("../../ui/footer/picker_presentation.zig");const compact_command_menu_presentation = @import("../../ui/footer/compact_command_menu_presentation.zig");const mcp_menu_presentation = @import("../../ui/footer/mcp_menu_presentation.zig");const render_input = @import("../../ui/footer/render_input.zig");const paste_blocks = @import("../input/pasted_blocks.zig");const registered_entities = @import("../input/registered_entities.zig");const prompt_history_runtime = @import("prompt_history_runtime.zig");const prompt_history_store = @import("../session/prompt_history_store.zig");const shell_runtime = @import("../../ui/shell_runtime.zig");const render_request = @import("../../ui/render_request.zig");const transcript_runtime = @import("../../ui/transcript/runtime.zig");const input_interrupt_runtime = @import("input_interrupt_runtime.zig");const input_history_runtime = @import("input_history_runtime.zig");const input_completion_runtime = @import("input_completion_runtime.zig");const provider_picker_runtime = @import("provider_picker_runtime.zig");const input_paste_runtime = @import("input_paste_runtime.zig");const input_submit_runtime = @import("input_submit_runtime.zig");const input_approval_runtime = @import("input_approval_runtime.zig");const input_question_runtime = @import("input_question_runtime.zig");const input_full_transcript_runtime = @import("input_full_transcript_runtime.zig");const input_selection_runtime = @import("input_selection_runtime.zig");const input_limit_feedback = @import("input_limit_feedback.zig");const app_upgrade_runtime = @import("app_upgrade_runtime.zig");
test { _ = file_picker_path; _ = picker_state; _ = input_completion_runtime;}
const ModelPickerStage = picker_state.ModelPickerStage;const ToolPermissionDecision = types.ToolPermissionDecision;
pub const file_picker_completion_cap = input_completion_runtime.file_picker_completion_cap;const ctrl_g_upgrade_byte: u8 = 7;
fn classifyResumeFailure(err: anyerror) session_catalog.ResumeFailure { return switch (err) { error.SessionBusy => .open_elsewhere, error.SessionAuthorityBoundaryUnavailable, error.SessionCommitBoundaryUnavailable, => .being_updated, else => .unavailable, };}
const ExplicitModelSelection = struct { model: []const u8, effort: types.ReasoningEffort, fast_mode: bool, has_fast_mode_token: bool = false,};
const ExplicitModelSelectionParse = union(enum) { none, invalid, selection: ExplicitModelSelection,};
const ProjectMcpPromptInputState = struct { active: bool, question_active: bool, approval_active: bool, subagent_active: bool, menu_active: bool, authentication_active: bool,};
fn projectMcpPromptMayOwnInput(state: ProjectMcpPromptInputState) bool { return state.active and !state.question_active and !state.approval_active and !state.subagent_active and !state.menu_active and !state.authentication_active;}
fn parseExplicitModelSelection(input: []const u8) ExplicitModelSelectionParse { const trimmed = std.mem.trim(u8, input, " \t\r\n"); if (!std.ascii.startsWithIgnoreCase(trimmed, "/model")) return .none; if (trimmed.len == "/model".len) return .none; if (trimmed["/model".len] != ' ' and trimmed["/model".len] != '\t') return .none;
var tokens: [4][]const u8 = undefined; var token_count: usize = 0; var iterator = std.mem.tokenizeAny(u8, trimmed["/model".len..], " \t\r\n"); while (iterator.next()) |token| { if (token_count == tokens.len) return .invalid; tokens[token_count] = token; token_count += 1; } if (token_count < 2) return .none;
const effort = types.ReasoningEffort.parse(tokens[1]) orelse return .none;
if (token_count == 2) { return .{ .selection = .{ .model = tokens[0], .effort = effort, .fast_mode = false, .has_fast_mode_token = false, } }; }
if (token_count != 3) return .invalid; const fast_mode = if (std.ascii.eqlIgnoreCase(tokens[2], "fast")) true else if (std.ascii.eqlIgnoreCase(tokens[2], "normal")) false else return .invalid; return .{ .selection = .{ .model = tokens[0], .effort = effort, .fast_mode = fast_mode, .has_fast_mode_token = true, } };}
fn validateExplicitModelSelection(selection: ExplicitModelSelection, capabilities: model_capabilities.Capabilities) ExplicitModelSelectionParse { if (!model_capabilities.reasoningEffortSupported(capabilities, selection.effort)) return .invalid; if (capabilities.supports_fast_mode != selection.has_fast_mode_token) return .invalid; return .{ .selection = selection };}
pub fn Runtime(comptime App: type) type { return struct { const history_rt = input_history_runtime.HistoryRuntime(App); const completion_rt = input_completion_runtime.CompletionRuntime(App); const provider_picker_rt = provider_picker_runtime.Runtime(App); const paste_rt = input_paste_runtime.PasteEditRuntime(App); const submit_rt = input_submit_runtime.SubmitRuntime(App); const approval_rt = input_approval_runtime.ApprovalRuntime(App); const question_rt = input_question_runtime.QuestionRuntime(App); const interrupt_rt = input_interrupt_runtime.InterruptRuntime(App); const full_transcript_rt = input_full_transcript_runtime.Runtime(App);
const ctrl_c_exit_window_ms = gesture_state.ctrl_c_exit_window_ms; const ResolvedEscapeRoute = union(enum) { done, remapped_byte: u8, };
const routeSlashCompletionMove = completion_rt.routeSlashCompletionMove; const advanceModelPickerOnSpace = completion_rt.advanceModelPickerOnSpace; const routeModifiedHistory = completion_rt.routeModifiedHistory; const routePlainVertical = completion_rt.routePlainVertical; const routeVisiblePickerMove = completion_rt.routeVisiblePickerMove; const navigateModelPicker = completion_rt.navigateModelPicker; const cancelApprovalOperation = approval_rt.cancelApprovalOperation;
const routeApprovalEscapeAction = approval_rt.routeApprovalEscapeAction; const routeQuestionEscapeAction = question_rt.routeQuestionEscapeAction; const submitQuestionBatch = question_rt.submitQuestionBatch; pub const rerenderQuestionBlock = question_rt.rerenderQuestionBlock; const finishPaste = paste_rt.finishPaste; const finalizePastedBlock = paste_rt.finalizePastedBlock; const handlePastedBytes = paste_rt.handlePastedBytes; const submit = submit_rt.submit; pub const installPromptHistory = history_rt.installPromptHistory; pub const loadAndInstallPromptHistory = history_rt.loadAndInstallPromptHistory;
fn insertComposerSliceBounded( app: *App, bytes: []const u8, max_input_len: usize, preserve_model_picker: bool, ) !composer_insertion.InsertResult { const picker_policy: composer_insertion.PickerPolicy = if (preserve_model_picker) .preserve else .clear; return app.input_runtime.replacementState(&app.pending_images).replaceSelectionOrInsertSliceBounded( app.alloc, bytes, max_input_len, picker_policy, ); }
pub fn clearPendingImages(app: *App) void { app.input_runtime.vertical_navigation.reset(); app.input_runtime.entities.image_tokens.clearRetainingCapacity(); image_attachments.discardImageSnapshots(app.alloc, app.pending_images.items); for (app.pending_images.items) |image| { types.freeImageAttachment(app.alloc, image); } app.pending_images.clearRetainingCapacity(); }
pub fn releasePendingImages(app: *App) void { app.input_runtime.vertical_navigation.reset(); app.input_runtime.entities.image_tokens.clearRetainingCapacity(); for (app.pending_images.items) |image| { types.freeImageAttachment(app.alloc, image); } app.pending_images.clearRetainingCapacity(); }
fn routeComposerShortcutAction(app: *App, action: input_action.ShortcutAction, max_input_len: usize) !void { switch (action) { .move => |intent| { switch (intent.kind) { .character_left => { app.input_runtime.vertical_navigation.reset(); if (!intent.extend_selection and app.input_runtime.edit_state.selectionRange() == null) { if (!app.stream.active and try provider_picker_rt.stepBack(app)) { app.shell.render_requests.request(.footer); return; } if (try completion_rt.stepBackModelPicker(app)) { app.shell.render_requests.request(.footer); return; } } _ = app.input_runtime.moveInputCursor(intent); }, .character_right => { app.input_runtime.vertical_navigation.reset(); // In the provider picker the arrows walk columns: // Left reopens the previous one, Right acts as // Enter on the highlighted row. Only from the end // of the text, so Right keeps moving the cursor // while editing. if (!intent.extend_selection and app.input_runtime.edit_state.selectionRange() == null and !app.stream.active and app.input_runtime.edit_state.cursor == app.input_runtime.edit_state.input.items.len and try provider_picker_rt.submit(app)) { app.shell.render_requests.request(.footer); return; } if (!intent.extend_selection and app.input_runtime.edit_state.selectionRange() == null) { switch (try completion_rt.autocompleteInlineCompletion(app, max_input_len)) { .inserted => { app.shell.render_requests.request(.footer); return; }, .inactive => {}, .limit_exceeded => { try input_limit_feedback.report(App, app, .composer, 1); app.shell.render_requests.request(.footer); return; }, } } _ = app.input_runtime.moveInputCursor(intent); }, .word_left, .word_right, .line_start, .line_end, .draft_start, .draft_end, .paragraph_up, .paragraph_down, => { completion_rt.abandonModelPickerOptions(app); _ = app.input_runtime.moveInputCursor(intent); }, .visual_up, .visual_down, .page_up, .page_down, => { const direction: visual_layout.Direction = switch (intent.kind) { .visual_up, .page_up => .up, .visual_down, .page_down => .down, else => unreachable, }; const delta: i32 = if (direction == .up) -1 else 1; try completion_rt.routeComposerVertical( app, direction, delta, intent.extend_selection, intent.kind == .page_up or intent.kind == .page_down, ); }, } app.shell.render_requests.request(.footer); }, .select_all => { dismissActiveMenusThenRedraw(app); if (app.input_runtime.selectionState().selectAll()) { app.shell.render_requests.request(.footer); } }, .copy_selection => { if (comptime @hasDecl(App, "clipboard")) { if (input_selection_runtime.copySelection( &app.input_runtime.edit_state, app.clipboard(), ) == .copy_failed) { debug_trace.logf( "input", "composer selection copy preserved reason=clipboard_copy_failed", .{}, ); } } }, .cut_selection => { if (comptime @hasDecl(App, "clipboard")) { switch (input_selection_runtime.cutSelection( app.alloc, app.input_runtime.selectionState(), &app.pending_images, app.clipboard(), )) { .cut => { dismissActiveMenusThenRedraw(app); syncCatalogMenus(app); app.shell.render_requests.request(.footer); }, .copy_failed => debug_trace.logf( "input", "composer selection cut preserved reason=clipboard_copy_failed", .{}, ), .delete_failed => debug_trace.logf( "input", "composer selection cut delete skipped reason=delete_failed", .{}, ), .inactive, .copied => {}, } } }, .undo => { dismissActiveMenusThenRedraw(app); if (try app.input_runtime.undoState().undo(app.alloc)) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .redo => { dismissActiveMenusThenRedraw(app); if (try app.input_runtime.undoState().redo(app.alloc)) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .redraw => { if (!composerPickerSurfaceVisible(app) or activeCatalogMenuOwnsByte(app, 12)) return; if (comptime @hasField(App, "pacer") and @hasField(App, "metrics") and @hasDecl(@TypeOf(app.pacer), "discardDeferredPresentationForVisualEpoch")) { _ = try app_render_runtime.Runtime(App).resetVisualEpoch(app, .ctrl_l); } }, .history_next => { try completion_rt.navigatePromptHistory(app, 1); syncCatalogMenus(app); app.shell.render_requests.request(.footer); }, .delete_backward => { if (app.input_runtime.edit_state.input.items.len > 0) { const picker_policy: composer_insertion.PickerPolicy = if (completion_rt.shouldPreserveModelPickerBackspace(app)) .preserve else .clear; const changed = app.input_runtime.deletionState( &app.pending_images, ).delete(app.alloc, .character_left, picker_policy); if (changed) { app.input_runtime.picker.resetFilePickerIndex(); syncCatalogMenus(app); app.shell.render_requests.request(.footer); } } }, .delete_forward => { dismissActiveMenusThenRedraw(app); if (app.input_runtime.deletionState(&app.pending_images).delete( app.alloc, .character_right, .clear, )) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .delete_word_left => { dismissActiveMenusThenRedraw(app); completion_rt.abandonModelPickerOptions(app); if (app.input_runtime.deletionState(&app.pending_images).delete( app.alloc, .word_left, .clear, )) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .delete_whitespace_word_left => { if (try app.input_runtime.killRingState(&app.pending_images).delete( app.alloc, .whitespace_word_left, )) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .delete_word_right => { dismissActiveMenusThenRedraw(app); completion_rt.abandonModelPickerOptions(app); if (app.input_runtime.deletionState(&app.pending_images).delete( app.alloc, .word_right, .clear, )) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .delete_to_line_start => { dismissActiveMenusThenRedraw(app); if (try app.input_runtime.killRingState(&app.pending_images).delete( app.alloc, .line_start, )) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .delete_to_line_end => { dismissActiveMenusThenRedraw(app); if (try app.input_runtime.killRingState(&app.pending_images).delete( app.alloc, .line_end, )) { syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, .yank => { const first_image_id = app.peekNextImageId(); switch (try app.input_runtime.killRingState(&app.pending_images).yank( app.alloc, first_image_id, max_input_len, )) { .inserted => |image_count| { for (0..image_count) |offset| { const committed_id = app.nextImageId(); std.debug.assert(committed_id == first_image_id + offset); } syncCatalogMenus(app); app.shell.render_requests.request(.footer); }, .inactive => {}, .limit_exceeded => |attempted_bytes| try input_limit_feedback.report( App, app, .composer, attempted_bytes, ), } }, .insert_newline => { if (commandSkillsMenuActive(app) or modelMenuActive(app) or sessionMenuActive(app)) return; dismissActiveMenusThenRedraw(app); switch (try insertComposerSliceBounded(app, "\n", max_input_len, false)) { .inserted => app.shell.render_requests.request(.footer), .inactive => unreachable, .limit_exceeded => try input_limit_feedback.report(App, app, .composer, 1), } }, } }
fn routePrimaryComposerPointerAction( app: *App, pointer: input_action.MousePointer, ) bool { if (pointer.kind != .press or pointer.shift or pointer.alt or pointer.ctrl) return false; if (!app.shell.footer_viewport.has_frame) return false;
const prefix_cells: u16 = @intCast(visual_layout.inputPrefix(0).cell_width); const position = app.shell.footer_viewport.geometry.inputPointerPosition( pointer.row, pointer.column, prefix_cells, ) orelse return false; const point = visual_layout.cursorPointAtPosition(.{ .input = app.input_runtime.edit_state.input.items, .cursor = app.input_runtime.edit_state.cursor, .terminal_cols = app.shell.layout.cols, .pasted_blocks = app.input_runtime.entities.pasted_blocks.items, .skill_tokens = app.input_runtime.entities.skill_tokens.items, }, position.row_index, position.content_column) orelse return false;
dismissActiveMenusThenRedraw(app); app.input_runtime.vertical_navigation.reset(); if (app.input_runtime.edit_state.setCursor(point.raw_offset)) { app.shell.render_requests.request(.footer); } return true; }
pub fn expireTerminalInputGestures(app: *App, now: i64) void { expireCtrlCExitArm(app, now); expireEscClearArm(app, now); expireEscapeInterruptArm(app, now); }
fn terminalDecodeContext( app: *App, paste_active: bool, ) input_action.TerminalDecodeContext { if (paste_active) { return .{ .now_ms = 0, .paste_active = true, .cancel_pending = false, .question_freeform_selected = false, }; } return .{ .now_ms = io_mod.milliTimestamp(), .paste_active = false, .cancel_pending = interrupt_rt.hasActiveOperation(app), .question_freeform_selected = app.question_prompt.isFreeformSelected(), }; }
pub fn prepareTerminalDecode(app: *App) !?input_action.TerminalDecodeContext { if (!try app_worker_runtime.Runtime(App).authorizeInteractiveAdmission(app)) return null; const paste_active = terminalPasteActive(app); return terminalDecodeContext(app, paste_active); }
pub fn handleTerminalInputIngressWithLimits( app: *App, ingress: input_action.TerminalInputIngress, input_limits: paste_framing.InputLimits, max_prompt_history: usize, ) !?u8 { if (!ingress.has_routing_work()) return null; if (terminalIngressCancelsPendingFullTranscriptOpen(ingress)) { _ = full_transcript_rt.cancelPendingOpenForInput(app); }
defer completion_rt.reconcileFilePicker(app);
const paste_was_active = terminalPasteActive(app); defer { const paste_is_active = terminalPasteActive(app); if (!paste_was_active and paste_is_active) { shell_runtime.suspendResizeCursorProbeForPaste(&app.terminal_input_runtime.terminal_cursor_probe); } else if (paste_was_active and !paste_is_active) { shell_runtime.resumeResizeCursorProbeAfterPaste( &app.terminal_input_runtime.terminal_cursor_probe, io_mod.milliTimestamp(), ); } }
if (ingress.interrupts_pending_text and text_scalar.hasPending(app.input_runtime.text_scalar)) { input_reset.resetPendingTextScalarWithTrace( &app.input_runtime.text_scalar, "interrupted_by_non_text_input", ); }
var replay_byte = ingress.replay_byte_after_routing; if (replay_byte) |byte| { if (byte != 0x1b and try routeProjectMcpPromptByte(app, byte)) { replay_byte = null; } } if (ingress.event) |event| { switch (event) { .paste_byte => |byte| { const routed = try routeActivePasteByte(app, byte); std.debug.assert(routed); }, .raw => |raw| try handleRawTerminalInputWithLimits( app, raw, input_limits, max_prompt_history, ), .action => |decoded| { switch (try routeResolvedEscapeAction( app, decoded.action, decoded.composer_shortcut, decoded.approval_focused_edit, decoded.question_action, decoded.cancel_pending, input_limits.composer_bytes, )) { .done => {}, .remapped_byte => |byte| { std.debug.assert(replay_byte == null); replay_byte = byte; }, } }, } } return replay_byte; }
pub fn flushPendingEscape(app: *App, input_escape_timeout_ms: i64) !void { const now = io_mod.milliTimestamp(); expireTerminalInputGestures(app, now); const ingress = app.terminal_input_runtime.flushTerminalAction( now, input_escape_timeout_ms, terminalPasteActive(app), ); if (ingress.event) |event| { switch (event) { .action => |decoded| { std.debug.assert(decoded.action == .escape); disarmCtrlCExit(app, "semantic_action"); try resolveEscape(app, decoded.cancel_pending, now); }, .paste_byte, .raw => unreachable, } } }
pub fn handleTerminalByte(app: *App, byte: u8, max_input_len: usize, max_prompt_history: usize) !void { return handleTerminalByteWithLimits( app, byte, paste_framing.InputLimits.single(max_input_len), max_prompt_history, ); }
pub fn handleTerminalByteWithLimits( app: *App, byte: u8, input_limits: paste_framing.InputLimits, max_prompt_history: usize, ) !void { var context = try prepareTerminalDecode(app) orelse return; var ingress = app.terminal_input_runtime.decodeTerminalByte( byte, context, ); while (true) { const replay_byte = try handleTerminalInputIngressWithLimits( app, ingress, input_limits, max_prompt_history, ); const replay = replay_byte orelse return; context = try prepareTerminalDecode(app) orelse return; ingress = app.terminal_input_runtime.decodeTerminalByte(replay, context); } }
pub fn terminalPasteActive(app: *const App) bool { return app.input_runtime.paste.active(); }
fn terminalIngressCancelsPendingFullTranscriptOpen( ingress: input_action.TerminalInputIngress, ) bool { const event = ingress.event orelse return false; return switch (event) { .paste_byte, .raw => true, .action => |decoded| decoded.action != .toggle_full_transcript, }; }
pub fn routeActivePasteIngressByteWithLimits( app: *App, byte: u8, input_limits: paste_framing.InputLimits, max_prompt_history: usize, ) !bool { if (!terminalPasteActive(app)) return false; const replay_byte = try handleTerminalInputIngressWithLimits( app, .{ .event = .{ .paste_byte = byte } }, input_limits, max_prompt_history, ); std.debug.assert(replay_byte == null); return true; }
pub fn settleTerminalPasteDeliveryEpochWithLimits( app: *App, input_limits: paste_framing.InputLimits, ) !void { const paste_was_active = terminalPasteActive(app); if (!paste_was_active) return; defer if (!terminalPasteActive(app)) { shell_runtime.resumeResizeCursorProbeAfterPaste( &app.terminal_input_runtime.terminal_cursor_probe, io_mod.milliTimestamp(), ); };
if (app.input_runtime.paste.active()) { const settled = try paste_rt.settlePasteDeliveryEpoch( app, input_limits.forOwner(app.input_runtime.paste.owner), ); if (settled and !app.input_runtime.paste.active()) syncCatalogMenus(app); } }
pub const prepareFilePicker = completion_rt.prepareFilePicker; pub const collectFilePickerFacts = completion_rt.collectFilePickerFacts;
pub fn handleByte(app: *App, byte: u8, max_input_len: usize, max_prompt_history: usize) !void { return handleTerminalByteWithLimits( app, byte, paste_framing.InputLimits.single(max_input_len), max_prompt_history, ); }
fn routeActivePasteByte( app: *App, byte: u8, ) !bool { if (app.input_runtime.paste.active()) { try paste_rt.handleActivePasteByte(app, byte); return true; } return false; }
fn projectMcpPromptOwnsInput(app: *App) bool { if (comptime !@hasDecl(App, "projectMcpPromptActive")) return false; const menu_active = activeCompactCommandMenu(app) != null or settingsMenuActive(app) or skillsMenuActive(app) or modelMenuActive(app) or sessionMenuActive(app) or mcpMenuActive(app) or helpMenuActive(app); var authentication_active = false; if (comptime runtime_profile.allows(App, .native_auth)) { authentication_active = app.auth.apiKeyEntryActive(); } return projectMcpPromptMayOwnInput(.{ .active = app.projectMcpPromptActive(), .question_active = app.question_prompt.isActive(), .approval_active = app.approval_prompt.isActive(), .subagent_active = false, .menu_active = menu_active, .authentication_active = authentication_active, }); }
fn routeProjectMcpPromptByte(app: *App, byte: u8) !bool { if (comptime !@hasDecl(App, "projectMcpPromptName")) return false; const owns_input = projectMcpPromptOwnsInput(app); debug_trace.logf( "mcp", "project prompt input byte={d} owns_input={s}", .{ byte, if (owns_input) "true" else "false" }, ); if (!owns_input) return false; const action: project_config.ProjectMcpAction = switch (byte) { '1' => { const name = (try app.projectMcpPromptName(app.alloc)) orelse return true; defer app.alloc.free(name); const display = try text_utils.encodeTerminalSafe(app.alloc, name, 256); defer app.alloc.free(display.bytes); const body = try std.fmt.allocPrint( app.alloc, "Approving project MCP server '{s}'.", .{display.bytes}, ); defer app.alloc.free(body); try app_commands.Handlers(App).applyProjectMcpAction( app, .{ .approve = name }, body, ); return true; }, '2' => .approve_all, '3' => { const name = (try app.projectMcpPromptName(app.alloc)) orelse return true; defer app.alloc.free(name); const display = try text_utils.encodeTerminalSafe(app.alloc, name, 256); defer app.alloc.free(display.bytes); const body = try std.fmt.allocPrint( app.alloc, "Rejecting project MCP server '{s}'.", .{display.bytes}, ); defer app.alloc.free(body); try app_commands.Handlers(App).applyProjectMcpAction( app, .{ .reject = name }, body, ); return true; }, else => return true, }; try app_commands.Handlers(App).applyProjectMcpAction( app, action, "Approving all project MCP servers for this workspace.", ); return true; }
fn handleRawTerminalInputWithLimits( app: *App, raw: input_action.RawTerminalInput, input_limits: paste_framing.InputLimits, max_prompt_history: usize, ) !void { const byte = raw.byte; const max_input_len = input_limits.composer_bytes; // Fire the max-level cue only when the slash menu becomes visible. const slash_menu_was_visible = slashMenuVisibleForCue(app); defer announceSlashMenuOpened(app, slash_menu_was_visible); if (try routeActivePasteByte(app, byte)) return;
if (byte != 3 and byte != 0x1b) disarmCtrlCExit(app, "raw_input");
// Non-escape input disarms the pending double-Escape clear. if (byte != 0x1b) { if (disarmEscapeClear(app)) { app.shell.render_requests.request(.footer); } _ = disarmEscapeInterrupt(app, "raw_input"); }
// Ctrl-Z arrives as raw byte 26 (ISIG disabled); kitty remaps here too. if (byte == 26) { try app.suspendToJobControl(); return; }
if (comptime runtime_profile.allows(App, .native_auth)) { if (try app_auth_runtime.Runtime(App).routeAuthPickerByte(app, byte)) return; } if (try full_transcript_rt.routeByte(app, byte)) return; if (try routeProjectMcpPromptByte(app, byte)) return; if (try routeActiveModalInput(app, raw, input_limits.decision_bytes)) return; if (byte >= 0x80) { try handleTextByte(app, .composer, byte, max_input_len); return; } if (try routeUpgradeShortcut(app, byte)) return; if (try routePickerControlByte(app, byte)) return;
if (isComposerEditingByte(byte, raw.composer_shortcut) and !activeCatalogMenuOwnsByte(app, byte) and dismissActiveMenusForComposerEdit(app)) { app.shell.render_requests.request(.footer); } try handleComposerByte( app, byte, raw.composer_shortcut, max_input_len, max_prompt_history, ); }
fn routeResolvedEscapeAction( app: *App, resolved: input_action.Action, composer_shortcut: ?input_action.ShortcutAction, approval_focused_edit: ?approval_decision.DraftAction, question_action: ?question_prompt.Action, was_cancel_pending: bool, max_input_len: usize, ) !ResolvedEscapeRoute { switch (resolved) { .remapped_byte, .paste_start, .paste_end, .ignore => {}, else => disarmCtrlCExit(app, "semantic_action"), } // Any other semantic key stands down an armed interrupt. switch (resolved) { .remapped_byte, .paste_start, .paste_end, .ignore, .escape => {}, else => _ = disarmEscapeInterrupt(app, "semantic_action"), }
if (resolved == .escape) { if (try full_transcript_rt.routeAction(app, resolved)) { _ = disarmEscapeInterrupt(app, "full_transcript"); return .done; } if (comptime @hasDecl(App, "suppressProjectMcpPrompts")) { if (projectMcpPromptOwnsInput(app)) { _ = disarmEscapeInterrupt(app, "mcp_prompt_suppressed"); app.suppressProjectMcpPrompts(); try app.writeDomainNotice(.{ .topic = "mcp", .tone = .neutral, .body = "Project MCP approval prompts dismissed for this process.", }, true); return .done; } } const now = io_mod.milliTimestamp(); expireEscClearArm(app, now); try resolveEscape(app, was_cancel_pending, now); return .done; }
if (app_auth_runtime.Runtime(App).routeAuthPickerEscapeAction(app, resolved)) { return .done; }
if (try full_transcript_rt.routeAction(app, resolved)) return .done;
if (resolved == .paste_start) { if (comptime runtime_profile.allows(App, .native_auth) and @hasDecl(@TypeOf(app.auth), "signInCodeEntryActive")) { if (app.auth.signInCodeEntryActive()) { paste_rt.beginPaste(app, max_input_len); return .done; } } dismissActiveMenusThenRedraw(app); paste_rt.beginPaste(app, max_input_len); return .done; }
if (resolved == .paste_end) { return .done; }
switch (resolved) { .remapped_byte => |byte| return .{ .remapped_byte = byte }, else => {}, }
if (app.question_prompt.isActive()) { try question_rt.routeQuestionEscapeAction( app, resolved, question_action, ); return .done; }
if (app.approval_prompt.isActive()) { try approval_rt.routeApprovalEscapeAction( app, resolved, approval_focused_edit, ); return .done; }
if (resolved == .mouse_pointer and routePrimaryComposerPointerAction(app, resolved.mouse_pointer)) { return .done; }
if (activeCompactCommandMenu(app)) |menu| { try routeCompactCommandMenuEscapeAction(app, menu, resolved); return .done; }
if (mcpMenuActive(app)) { switch (resolved) { .cursor_up => _ = moveMcpMenu(app, -1), .cursor_down => _ = moveMcpMenu(app, 1), .cursor_left => _ = try cycleMcpMenuSection(app, -1), .cursor_right => _ = try cycleMcpMenuSection(app, 1), else => {}, } if (resolved == .cursor_up or resolved == .cursor_down or resolved == .cursor_left or resolved == .cursor_right) { return .done; } }
if (settingsMenuActive(app) and (resolved == .cursor_left or resolved == .cursor_right)) { try changeSettingsMenuSelection( app, if (resolved == .cursor_left) -1 else 1, ); return .done; }
if (try full_transcript_rt.routeComposerAction(app, resolved)) return .done;
if (composer_shortcut) |action| { if (routeMcpFilterShortcut(app, action)) return .done; try routeComposerShortcutAction(app, action, max_input_len); return .done; }
switch (resolved) { .history_up => { try completion_rt.routeModifiedHistory(app, .up, -1); app.shell.render_requests.request(.footer); }, .history_down => { try completion_rt.routeModifiedHistory(app, .down, 1); app.shell.render_requests.request(.footer); }, .cursor_up => { try completion_rt.routePlainVertical(app, .up, -1); app.shell.render_requests.request(.footer); }, .cursor_down => { try completion_rt.routePlainVertical(app, .down, 1); app.shell.render_requests.request(.footer); }, .cursor_left, .cursor_right, .word_left, .word_right, .home, .end, .delete_next, .delete_word_left, .delete_word_right, .delete_to_line_start, .delete_to_line_end, .insert_newline, .composer_shortcut, .toggle_full_transcript, => unreachable, .page_up, .page_down, .mouse_wheel, .mouse_pointer, => {}, .clear_line => { dismissActiveMenusThenRedraw(app); if (draftHasState(app)) { clearDraftState(app, "clear_line"); app.shell.render_requests.request(.footer); } }, .toggle_permission_mode => { if (cycleHelpMenuCategory(app, -1) or cycleSettingsMenuCategory(app, -1)) { app.shell.render_requests.request(.footer); } else if (try toggleSessionPickerScopeIfActive(app)) { app.shell.render_requests.request(.footer); } else if (cycleModelMenuProvider(app, -1) or cycleSkillsMenuSource(app, -1)) { app.shell.render_requests.request(.footer); } else { try app_permission_runtime.Runtime(App).toggleMode(app); } }, .open_all_sessions => { if (comptime @hasField(App, "session_persistence") and runtime_profile.allows(App, .durable_sessions)) { if (settingsMenuActive(app) or helpMenuActive(app) or skillsMenuActive(app) or modelMenuActive(app)) return .done; if (try refuseSessionActionWithDraft(app, "submit or clear the draft before switching sessions")) return .done; dismissActiveMenusThenRedraw(app); try app_session_runtime.Runtime(App).openAllSessionPicker(app); app.shell.render_requests.request(.footer); } }, .open_model_catalog => { if (comptime @hasField(App, "model_cache")) { if (modelMenuActive(app)) { _ = closeModelMenu(app, true); app.shell.render_requests.request(.footer); return .done; } if (modelPickerShortcutBlocked(app)) return .done; try openModelPickerShortcut(app); } }, .paste_start => dismissActiveMenusThenRedraw(app), .paste_end => {}, .remapped_byte => unreachable, .escape => unreachable, .ignore => {}, } return .done; }
fn routePickerControlByte(app: *App, byte: u8) !bool { if (!composerPickerSurfaceVisible(app)) return false; const delta = pickerControlDelta(byte) orelse return false; if (!try routeVisiblePickerMove(app, delta)) return false; app.input_runtime.vertical_navigation.reset(); app.shell.render_requests.request(.footer); return true; }
fn pickerControlDelta(byte: u8) ?i32 { return switch (byte) { 10 => 1, 11 => -1, else => null, }; }
fn composerPickerSurfaceVisible(app: *App) bool { if (app.question_prompt.isActive() or approvalOwnsCurrentSurface(app)) return false; if (comptime @hasField(App, "terminal")) { if (app.terminal.fullTranscriptScreenActive()) return false; } return true; }
fn routeUpgradeShortcut(app: *App, byte: u8) !bool { if (byte != ctrl_g_upgrade_byte) return false; if (comptime @hasDecl(App, "applyReadyUpgradeShortcut")) { try app.applyReadyUpgradeShortcut(); } return true; }
fn routeActiveModalInput( app: *App, raw: input_action.RawTerminalInput, max_input_len: usize, ) !bool { const byte = raw.byte; if ((app.question_prompt.isActive() or approvalOwnsCurrentSurface(app)) and byte == ctrl_g_upgrade_byte) { _ = try routeUpgradeShortcut(app, byte); return true; } if (app.question_prompt.isActive()) { if (byte >= 0x80) { if (app.question_prompt.isFreeformSelected()) { try handleTextByte(app, .question_freeform, byte, max_input_len); } else { input_reset.resetPendingTextScalarWithTrace( &app.input_runtime.text_scalar, "question_decision_active", ); } return true; } if (raw.question_action) |action| { switch (try question_rt.handleQuestionActionBounded( app, action, max_input_len, )) { .consumed => app.input_runtime.input_limit_rejection = input_limit_rejection.clear(), .ignored => {}, .limit_exceeded => try input_limit_feedback.report(App, app, .question_freeform, 1), } } return true; } if (approvalOwnsCurrentSurface(app)) { if (byte >= 0x80) { if (app.approval_prompt.isAmending()) { try handleTextByte(app, .approval_amendment, byte, max_input_len); } else { input_reset.resetPendingTextScalarWithTrace( &app.input_runtime.text_scalar, "approval_decision_active", ); } return true; } if (raw.approval_action) |action| { switch (try approval_rt.handlePermissionActionBounded( app, action, max_input_len, )) { .consumed => app.input_runtime.input_limit_rejection = input_limit_rejection.clear(), .ignored => {}, .limit_exceeded => try input_limit_feedback.report(App, app, .approval_amendment, 1), } } return true; } if (mcpMenuActive(app)) { if (comptime @hasField(App, "mcp")) { if (app.mcp.menu.screen == .add) { return try handleMcpAddInput(app, byte, max_input_len); } if (app.mcp.menu.screen == .arguments) { return try handleMcpArgumentInput(app, byte, max_input_len); } if (app.mcp.menu.filter_active) { if (byte == 0x7f) { _ = applyMcpMenuEvent(app, .delete_filter_byte); return true; } if (byte >= 0x20 and byte < 0x7f and byte != '\r') { _ = applyMcpMenuEvent(app, .{ .append_filter_byte = byte }); return true; } } } if (byte >= 0x80) { input_reset.resetPendingTextScalarWithTrace( &app.input_runtime.text_scalar, "mcp_menu_active", ); return true; } switch (byte) { '\t' => _ = try cycleMcpMenuSection(app, 1), '\r' => _ = try submitMcpMenuSelection(app), 'a', 'A' => _ = try handleMcpMenuPrimaryAction(app), 'd', 'D' => _ = confirmMcpMenuAction(app, .remove), 'l', 'L' => _ = confirmMcpMenuAction(app, .logout), 'x', 'X' => _ = confirmMcpMenuAction(app, .trust_reject), 'p', 'P' => _ = confirmMcpMenuAction(app, .trust_approve_all), 'z', 'Z' => _ = confirmMcpMenuAction(app, .trust_reset), 'c', 'C' => _ = applyMcpMenuEvent(app, .show_info), 'r', 'R' => _ = try refreshMcpMenu(app), 'i', 'I' => _ = try insertMcpMenuPreview(app, max_input_len), '/' => _ = applyMcpMenuEvent(app, .begin_filter), 10 => _ = moveMcpMenu(app, 1), 11 => _ = moveMcpMenu(app, -1), else => {}, } return true; } if (activeCompactCommandMenu(app)) |menu| { if (byte >= 0x80) { input_reset.resetPendingTextScalarWithTrace( &app.input_runtime.text_scalar, "compact_command_menu_active", ); return true; } switch (byte) { '\t' => if (menu == .usage) { try cycleUsageMenuScope(app, 1); }, '\r' => try submitCompactCommandMenuSelection(app, menu, max_input_len), 'r', 'R' => if (menu == .usage) { if (comptime runtime_profile.allows(App, .profile_usage)) { try reloadUsageMenu( app, app.input_runtime.usage_menu.navigationScope(), ); } }, 10 => { _ = moveCompactCommandMenu(app, menu, 1); app.shell.render_requests.request(.footer); }, 11 => { _ = moveCompactCommandMenu(app, menu, -1); app.shell.render_requests.request(.footer); }, else => {}, } return true; } return false; }
fn presentedSubagentApproval(app: *const App) bool { _ = app; return false; }
fn approvalOwnsCurrentSurface(app: *const App) bool { return app.approval_prompt.isActive(); }
fn handleTextByte(app: *App, owner: text_scalar.Owner, byte: u8, max_input_len: usize) !void { const transition = text_scalar.advance(app.input_runtime.text_scalar, owner, byte); app.input_runtime.text_scalar = transition.next; const step = transition.step; if (step.dropped) |drop| { debug_trace.logf( "input", "text scalar dropped owner={s} bytes={d} reason={s}", .{ @tagName(drop.owner), drop.bytes, @tagName(drop.reason) }, ); } const scalar = step.scalar orelse return; const bytes = scalar.slice(); switch (scalar.owner) { .question_freeform => switch (try app.question_prompt.insertFreeformSlice(app.alloc, bytes, max_input_len)) { .inserted => { app.input_runtime.input_limit_rejection = input_limit_rejection.clear(); app.shell.render_requests.request(.modal); }, .inactive => {}, .limit_exceeded => try input_limit_feedback.report(App, app, .question_freeform, bytes.len), }, .approval_amendment => switch (try app.approval_prompt.decision.insertAmendmentSlice(app.alloc, bytes, max_input_len)) { .inserted => { app.input_runtime.input_limit_rejection = input_limit_rejection.clear(); app.shell.render_requests.request(.modal); }, .inactive => {}, .limit_exceeded => try input_limit_feedback.report(App, app, .approval_amendment, bytes.len), }, .composer => { if (!app.input_runtime.replacementState(&app.pending_images).canReplaceSelectionOrInsert( bytes.len, max_input_len, )) { try input_limit_feedback.report(App, app, .composer, bytes.len); return; } if (dismissActiveMenusForComposerEdit(app)) { app.shell.render_requests.request(.footer); } switch (try insertComposerSliceBounded( app, bytes, max_input_len, completion_rt.shouldPreserveModelPickerInsert(app), )) { .inserted => {}, .inactive => unreachable, .limit_exceeded => { try input_limit_feedback.report(App, app, .composer, bytes.len); return; }, } app.input_runtime.picker.resetFilePickerIndex(); syncCatalogMenus(app); app.shell.render_requests.request(.footer); }, } }
fn handleComposerByte( app: *App, byte: u8, composer_shortcut: ?input_action.ShortcutAction, max_input_len: usize, max_prompt_history: usize, ) !void { switch (byte) { 3 => { try handleSemanticCtrlC(app); }, 4 => { try handleSemanticCtrlD(app, max_input_len); }, '\t' => { if (cycleHelpMenuCategory(app, 1) or cycleSettingsMenuCategory(app, 1)) { app.shell.render_requests.request(.footer); } else if (moveAuthPickerIfActive(app, 1)) { app.shell.render_requests.request(.footer); } else if (try toggleSessionPickerScopeIfActive(app)) { app.shell.render_requests.request(.footer); } else if (cycleModelMenuProvider(app, 1) or cycleSkillsMenuSource(app, 1)) { app.shell.render_requests.request(.footer); } else if (picker_state.isBareModelCommandAtCursor(&app.input_runtime.edit_state)) { try completion_rt.openCurrentModelPicker(app); } else if (completion_rt.hasFileQuery(app)) { if ((try completion_rt.autocompleteFilePickerSelection(app, max_input_len)) == .limit_exceeded) { try input_limit_feedback.report(App, app, .composer, 1); } app.shell.render_requests.request(.footer); } else if (!commandSkillsMenuActive(app) and provider_picker_rt.hasQuery(app)) { if (!app.stream.active) try provider_picker_rt.autocomplete(app); } else if (!commandSkillsMenuActive(app) and completion_rt.hasModelQuery(app)) { try completion_rt.autocompleteModelPickerSelection(app); } else if (completion_rt.visibleInlineCompletion(app) != null) { if ((try completion_rt.autocompleteInlineCompletion(app, max_input_len)) == .limit_exceeded) { try input_limit_feedback.report(App, app, .composer, 1); } app.shell.render_requests.request(.footer); } else if (!(app.stream.active and app.input_runtime.picker.isModelShapedInput(&app.input_runtime.edit_state))) { const count = completion_rt.visibleSlashCompletionCount(app); if (count > 0) { const items = app.input_runtime.edit_state.input.items; const prefix = command_specs.slashCompletionPrefix( app.slashRegistry(), items, ) orelse return; const idx = app.input_runtime.picker.slash_completion_index % count; if (try bindSelectedSlashSkill(app, idx)) { app.shell.render_requests.request(.footer); } else if (command_specs.nthSlashCompletion(app.slashRegistry(), prefix, idx)) |completion| { if (command_specs.slashCompletionHasArgs(app.slashRegistry(), completion)) { var buf: [128]u8 = undefined; const with_space = std.fmt.bufPrint(&buf, "{s} ", .{completion}) catch completion; try app.input_runtime.textReplacementState().replace(app.alloc, with_space); } else { try app.input_runtime.textReplacementState().replace(app.alloc, completion); } app.shell.render_requests.request(.footer); } } } }, ' ' => { dismissMentionSkillsMenuForSpace(app); if (app.input_runtime.edit_state.selectionRange() == null and !commandSkillsMenuActive(app) and completion_rt.hasModelQuery(app)) { if (try completion_rt.advanceModelPickerOnSpace(app)) { app.shell.render_requests.request(.footer); } } else if (app.input_runtime.edit_state.selectionRange() == null and !app.stream.active and !commandSkillsMenuActive(app) and provider_picker_rt.hasQuery(app) and try provider_picker_rt.advanceOnSpace(app)) { app.shell.render_requests.request(.footer); } else { switch (try insertComposerSliceBounded(app, " ", max_input_len, false)) { .inserted => { syncCatalogMenus(app); app.shell.render_requests.request(.footer); }, .inactive => unreachable, .limit_exceeded => try input_limit_feedback.report(App, app, .composer, 1), } } }, 22 => { // The catalog menu borrows the composer as its query box: // attaching an image there would orphan the payload. if (modelMenuActive(app)) { debug_trace.logf("input", "image attach skipped reason=model_menu_active", .{}); return; } try image_commands.Commands(App).attachClipboard(app); }, 24 => { return; }, '\r' => { if (try submitSettingsMenuSelection(app)) return; if (try submitHelpMenuSelection(app, max_input_len, max_prompt_history)) return; if (try submitAuthPickerSelection(app)) return; if (try submitModelMenuSelection(app)) return; if (try submitSkillsMenuSelection(app, max_input_len)) return; if (try submitSlashPickerSelection(app)) return; if (comptime runtime_profile.allows(App, .durable_sessions)) { if (try submitSessionPickerSelection(app)) return; } if (try completion_rt.submitFilePickerOnEnter(app, max_input_len)) |result| { if (result == .limit_exceeded) { try input_limit_feedback.report(App, app, .composer, 1); } app.shell.render_requests.request(.footer); return; } if (picker_state.isBareModelCommandAtCursor(&app.input_runtime.edit_state)) { try openModelBrowseCatalog(app); return; } if (provider_picker_rt.hasQuery(app)) { if (try app_auth_runtime.Runtime(App).reject_provider_picker_if_busy(app)) return; if (try provider_picker_rt.submit(app)) return; } if (completion_rt.hasModelQuery(app)) { if (try completion_rt.submitModelPicker(app)) return; } if (try submitExplicitModelSelection( app, resolveExplicitModelSelection(app, app.input_runtime.edit_state.input.items), )) return; if (app.input_runtime.lineContinuationState().replaceBackslashBeforeCursorWithNewline(app.alloc)) { app.shell.render_requests.request(.footer); return; } debug_trace.logf("input", "submit requested stream_active={s} queued={d} input_bytes={d}", .{ if (app.stream.active) "true" else "false", app.worker.queuedPromptCount(), app.input_runtime.edit_state.input.items.len }); try submit_rt.submit(app, max_prompt_history); }, else => { if (composer_shortcut) |action| { try routeComposerShortcutAction(app, action, max_input_len); return; } if (byte >= 32 and byte != 127) { const insertion_start = if (app.input_runtime.edit_state.selectionRange()) |selection| selection.start else app.input_runtime.edit_state.cursor; if (byte == '$' and !file_picker_path.contains_position(app.input_runtime.edit_state.input.items, insertion_start) and !helpMenuActive(app) and !commandSkillsMenuActive(app) and !modelMenuActive(app)) { if ((try insertComposerSliceBounded(app, &.{byte}, max_input_len, false)) == .limit_exceeded) { try input_limit_feedback.report(App, app, .composer, 1); return; } app.input_runtime.picker.resetInlinePickerEpisode(); if (comptime @hasField(App, "skills")) { app.skills.openMenuWithQuery(.dollar, .{ .start = insertion_start, .end = insertion_start + 1 }, ""); } app.input_runtime.picker.resetFilePickerIndex(); app.shell.render_requests.request(.footer); return; } const result = try insertComposerSliceBounded( app, &.{byte}, max_input_len, completion_rt.shouldPreserveModelPickerInsert(app), ); if (result == .limit_exceeded) { try input_limit_feedback.report(App, app, .composer, 1); return; } app.input_runtime.picker.resetFilePickerIndex(); syncCatalogMenus(app); app.shell.render_requests.request(.footer); } }, } }
/// While the picker borrows the composer, destructive global gestures /// back out of the picker and restore the draft instead of acting on /// the empty borrowed composer (clearing draft state would free the /// stashed draft's pending image payloads; exiting would hand off an /// empty composer). The restore fallback covers a stash stranded with /// the menu already closed. fn exitModelPickerShortcutIfActive(app: *App) bool { if (comptime !@hasField(App, "model_cache")) return false; if (app.input_runtime.model_picker_draft == null) return false; _ = closeModelMenu(app, true); restoreModelPickerDraft(app); app.shell.render_requests.request(.footer); return true; }
fn handleSemanticCtrlC(app: *App) !void { if (exitModelPickerShortcutIfActive(app)) return; if (app.stream.active and draftHasState(app)) { clearDraftState(app, "ctrl_c"); app.shell.render_requests.request(.footer); return; }
const now = io_mod.milliTimestamp(); const transition = gesture_state.pressCtrlCExit( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.result == .activated) { debug_trace.logf("shutdown", "ctrl_c_exit_activated", .{}); app_session_runtime.Runtime(App).requestResumeHandoff(app); app.should_exit = true; return; }
debug_trace.logf("input", "ctrl_c_exit_hint_armed", .{});
if (interrupt_rt.hasActiveOperation(app)) { try interrupt_rt.cancelActiveOperation(app); app.shell.render_requests.request(.footer); return; }
if (comptime @hasDecl(App, "cancelPendingSubmission")) { if (App.cancelPendingSubmission(app)) return; }
if (draftHasState(app)) clearDraftState(app, "ctrl_c"); app.shell.render_requests.request(.footer); }
fn draftHasState(app: *App) bool { return app.input_runtime.edit_state.input.items.len > 0 or app.input_runtime.entities.pasted_blocks.items.len > 0 or app.pending_images.items.len > 0; }
fn refuseSessionActionWithDraft(app: *App, body: []const u8) !bool { if (!draftHasState(app)) return false; try app.writeDomainNotice(.{ .topic = "session", .tone = .neutral, .body = body, }, true); return true; }
fn clearDraftState(app: *App, reason: []const u8) void { debug_trace.logf( "input", "draft cleared reason={s} input_bytes={d} pasted_blocks={d} images={d}", .{ reason, app.input_runtime.edit_state.input.items.len, app.input_runtime.entities.pasted_blocks.items.len, app.pending_images.items.len, }, ); app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); app.clearPendingImages(); syncCatalogMenus(app); }
fn handleSemanticCtrlD(app: *App, max_input_len: usize) !void { if (exitModelPickerShortcutIfActive(app)) return; if (app.input_runtime.edit_state.input.items.len > 0) { try routeComposerShortcutAction(app, .delete_forward, max_input_len); return; } if (app.stream.active) return; app_session_runtime.Runtime(App).requestResumeHandoff(app); app.should_exit = true; }
fn isComposerEditingByte( byte: u8, composer_shortcut: ?input_action.ShortcutAction, ) bool { if (composer_shortcut) |shortcut| { return switch (shortcut) { .redraw => false, else => true, }; } return switch (byte) { ' ', 22 => true, else => byte >= 32 and byte != 127, }; }
fn activeCatalogMenuOwnsByte(app: *App, byte: u8) bool { if (settingsMenuActive(app)) return true; if (helpMenuActive(app)) return true; if (modelMenuActive(app)) return true; if (sessionMenuActive(app)) return true; if (comptime !@hasField(App, "skills")) return false; if (!app.skills.menu.active) return false; return byte == ' ' or commandSkillsMenuActive(app); }
fn moveAuthPickerIfActive(app: *App, delta: i32) bool { if (comptime !@hasField(App, "auth")) return false; if (app.stream.active) return false; return app.auth.movePicker(delta); }
fn submitSessionPickerSelection(app: *App) !bool { if (comptime !@hasField(App, "session_persistence")) return false; if (!app.session_persistence.session_picker.active) return false;
if (app.loadMoreSessionPicker() catch |err| { const notice = try std.fmt.allocPrint( app.alloc, "unable to load more saved sessions: {s}", .{@errorName(err)}, ); defer app.alloc.free(notice); try app.writeDomainNotice(.{ .topic = "session", .tone = .@"error", .body = notice }, true); app.shell.render_requests.request(.footer); return true; }) return true;
if (app.session_persistence.session_picker.selectedId() == null) return true;
_ = app.resumeSelectedSession() catch |err| { const picker = &app.session_persistence.session_picker; debug_trace.logf( "session", "picker resume failed err={s} picker_active={s}", .{ @errorName(err), if (picker.active) "true" else "false" }, ); if (picker.active) { picker.selection_failure = classifyResumeFailure(err); } else { try app.writeDomainNotice(.{ .topic = "session", .tone = .@"error", .body = "Unable to resume selected session.", }, true); } app.shell.render_requests.request(.footer); return true; }; return true; }
fn dismissAuthPickerForComposerEdit(app: *App) bool { if (comptime !@hasField(App, "auth")) return false; if (!app.auth.pickerView().active) return false; cancelPromptRetryAfterAuth(app); app.auth.closePicker(app.alloc); return true; }
fn popOrCloseAuthPicker(app: *App) bool { if (comptime !@hasField(App, "auth")) return false; const picker = app.auth.pickerView(); if (!picker.active) return false; cancelPromptRetryAfterAuth(app); if (picker.stage == .root and picker.include_skip) app.auth.skipOnboarding(); return app.auth.popPickerStage(app.alloc); }
fn cancelPromptRetryAfterAuth(app: *App) void { if (comptime @hasDecl(App, "cancelPromptRetryAfterAuth")) { app.cancelPromptRetryAfterAuth(); } }
fn commandSkillsMenuActive(app: *App) bool { if (comptime !@hasField(App, "skills")) return false; return app.skills.menu.active and !app.skills.menu.origin.isMention(); }
fn skillsMenuActive(app: *App) bool { if (comptime !@hasField(App, "skills")) return false; return app.skills.menuVisible(); }
fn modelMenuActive(app: *App) bool { if (comptime !@hasField(App, "model_cache")) return false; return app.model_cache.menu.active; }
fn sessionMenuActive(app: *App) bool { if (comptime !@hasField(App, "session_persistence")) return false; return app.session_persistence.session_picker.active; }
fn helpMenuActive(app: *App) bool { return app.input_runtime.help_menu.active; }
fn settingsMenuActive(app: *App) bool { return app.input_runtime.settings_menu.active; }
fn mcpMenuActive(app: *App) bool { if (comptime @hasField(App, "mcp")) { return app.mcp.menu.active; } return false; }
fn mcpMenuProjection(app: *App) render_input.McpMenuProjection { if (comptime @hasField(App, "mcp")) { const view = app.mcp.menuView(); return .{ .state = view.state, .servers = if (view.health) |health| health.servers else &.{}, .tools = view.tools, .resources = if (view.resources) |catalog| catalog.resources.items else &.{}, .resource_templates = if (view.resources) |catalog| catalog.templates.items else &.{}, .prompts = if (view.prompts) |catalog| catalog.items else &.{}, .configuration_issue_count = if (view.health) |health| health.configuration_issues.len else 0, .preview = view.preview, .feedback = view.feedback, .add_name = view.add_form.name.items, .add_target = view.add_form.target.items, .add_arguments = view.add_form.arguments.items, .add_draft = app.input_runtime.edit_state.input.items, .arguments = view.argument_fields, .argument_draft = app.input_runtime.edit_state.input.items, }; } return .{}; }
fn applyMcpMenuEvent(app: *App, event: mcp_menu_state.Event) ?mcp_menu_state.Effect { const effect = applyMcpMenuEventDeferred(app, event); app.shell.render_requests.request(.footer); return effect; }
fn applyMcpMenuEventDeferred(app: *App, event: mcp_menu_state.Event) ?mcp_menu_state.Effect { if (comptime @hasField(App, "mcp")) { return mcp_menu_state.apply(&app.mcp.menu, event); } return null; }
fn routeMcpFilterShortcut(app: *App, action: input_action.ShortcutAction) bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { if (!app.mcp.menu.filter_active) return false; switch (action) { .delete_backward => _ = applyMcpMenuEvent(app, .delete_filter_byte), .delete_word_left, .delete_whitespace_word_left, .delete_to_line_start => _ = applyMcpMenuEvent(app, .clear_filter_text), else => return false, } return true; } return false; }
fn startMcpMenuEffect(app: *App, effect: mcp_menu_state.Effect) !void { if (comptime !@hasDecl(App, "beginMcpMenuEffect")) return; app.beginMcpMenuEffect(effect) catch |err| { const generation = switch (effect) { .load_catalog => |request| request.generation, .load_preview => |request| request.generation, .complete_argument => |request| request.generation, .action => |request| request.generation, .cancel => |value| value, }; if (comptime @hasDecl(App, "recordMcpMenuEffectFailure")) { try app.recordMcpMenuEffectFailure(generation, err); } }; }
fn moveMcpMenu(app: *App, delta: i8) bool { if (!mcpMenuActive(app)) return false; const projection = mcpMenuProjection(app); if (projection.state.screen == .preview) { _ = applyMcpMenuEvent(app, .{ .scroll_preview = .{ .delta = delta, .row_count = mcp_menu_presentation.previewVisualRowCount( projection.preview, app.shell.layout.cols, ), } }); return true; } _ = applyMcpMenuEvent(app, .{ .move = .{ .delta = delta, .item_count = projection.itemCount(), .visible_count = mcp_menu_presentation.visibleItemsForBudget( mcp_menu_presentation.max_inline_rows, ), } }); return true; }
fn cycleMcpMenuSection(app: *App, delta: i8) !bool { if (!mcpMenuActive(app)) return false; if (applyMcpMenuEventDeferred(app, .{ .cycle_section = delta })) |effect| { try startMcpMenuEffect(app, effect); } if (comptime @hasField(App, "mcp")) { if (app.mcp.menu.load_state == .loading) return true; } app.shell.render_requests.request(.footer); return true; }
fn submitMcpMenuSelection(app: *App) !bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { const state = app.mcp.menu; if (state.screen == .browse and state.section == .servers and mcpMenuProjection(app).itemCount() > 0) { _ = applyMcpMenuEvent(app, .show_details); } else if (state.screen == .browse and state.section != .servers and mcpMenuProjection(app).itemCount() > 0) { if (try app.mcp.prepareMenuArguments(app.alloc)) { app.input_runtime.inputResetState().clearCurrent(app.alloc); app.shell.render_requests.request(.footer); } else { if (applyMcpMenuEvent(app, .begin_preview)) |effect| { try startMcpMenuEffect(app, effect); } } } else if (state.screen == .details) { const server = mcpMenuProjection(app).selectedServer() orelse return true; if (mcp_menu_state.serverActionAvailable(.authenticate, server)) { try authenticateMcpMenuServer(app); } } else if (state.screen == .confirm) { if (state.confirmation_action) |action| { const effect = applyMcpMenuEvent( app, .{ .request_action = action }, ) orelse return true; switch (effect) { .action => |request| try executeMcpMenuAction(app, request), .load_catalog, .load_preview, .complete_argument, .cancel => {}, } } } } return true; }
fn insertMcpMenuPreview(app: *App, max_input_len: usize) !bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { const view = app.mcp.menuView(); const insert = view.insert orelse return true; const effect = applyMcpMenuEvent( app, .{ .request_action = .insert_preview }, ) orelse return true; switch (effect) { .action => |request| { if (request.action != .insert_preview) return true; switch (try insertComposerSliceBounded( app, insert, max_input_len, false, )) { .inserted => { if (comptime @hasDecl(App, "closeMcpMenu")) app.closeMcpMenu(); if (comptime @hasDecl(App, "presentProjectMcpPrompt")) { if (projectMcpPromptOwnsInput(app)) try app.presentProjectMcpPrompt(); } }, .limit_exceeded => try input_limit_feedback.report( App, app, .composer, insert.len, ), .inactive => {}, } app.shell.render_requests.request(.footer); }, .load_catalog, .load_preview, .complete_argument, .cancel => {}, } } return true; }
fn refreshMcpMenu(app: *App) !bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { if (app.mcp.menu.screen != .browse or app.mcp.menu.section != .servers) return true; } const effect = applyMcpMenuEvent( app, .{ .request_action = .refresh }, ) orelse return true; switch (effect) { .action => |request| { if (request.action != .refresh) return true; if (comptime @hasDecl(App, "beginMcpMenuReload")) { app.beginMcpMenuReload(request.generation) catch |err| { if (comptime @hasDecl(App, "recordMcpMenuEffectFailure")) { try app.recordMcpMenuEffectFailure(request.generation, err); } }; } }, .load_catalog, .load_preview, .complete_argument, .cancel => {}, } return true; }
fn authenticateMcpMenuServer(app: *App) !void { const effect = applyMcpMenuEvent( app, .{ .request_action = .authenticate }, ) orelse return; switch (effect) { .action => |request| { if (request.action == .authenticate) try executeMcpMenuAction(app, request); }, .load_catalog, .load_preview, .complete_argument, .cancel => {}, } }
fn confirmMcpMenuAction(app: *App, action: mcp_menu_state.Action) bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { if (app.mcp.menu.screen != .details and !(app.mcp.menu.screen == .browse and app.mcp.menu.section == .servers and (action == .trust_approve_all or action == .trust_reset))) return true; if (app.mcp.menu.screen == .details) { const server = mcpMenuProjection(app).selectedServer() orelse return true; if (!mcp_menu_state.serverActionAvailable(action, server)) return true; } _ = applyMcpMenuEvent(app, .{ .show_confirmation = action }); } return true; }
fn executeMcpMenuAction( app: *App, request: mcp_menu_state.ActionRequest, ) !void { const result = switch (request.action) { .authenticate => if (comptime @hasDecl(App, "beginMcpMenuAuthentication")) app.beginMcpMenuAuthentication(request.generation) else error.McpMenuActionUnavailable, .remove => if (comptime @hasDecl(App, "removeMcpMenuServer")) app.removeMcpMenuServer(request.generation) else error.McpMenuActionUnavailable, .logout => if (comptime @hasDecl(App, "beginMcpMenuEffect")) app.beginMcpMenuEffect(.{ .action = request }) else error.McpMenuActionUnavailable, .trust_approve, .trust_reject, .trust_approve_all, .trust_reset => if (comptime @hasDecl(App, "applyMcpMenuTrustAction")) app.applyMcpMenuTrustAction(request.generation, request.action) else error.McpMenuActionUnavailable, else => error.McpMenuActionUnavailable, }; result catch |err| { if (comptime @hasDecl(App, "recordMcpMenuEffectFailure")) { try app.recordMcpMenuEffectFailure(request.generation, err); } }; }
fn handleMcpMenuPrimaryAction(app: *App) !bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { if (app.mcp.menu.screen == .details) { const server = mcpMenuProjection(app).selectedServer() orelse return true; if (mcp_menu_state.serverActionAvailable(.trust_approve, server)) { const effect = applyMcpMenuEvent( app, .{ .request_action = .trust_approve }, ) orelse return true; switch (effect) { .action => |request| try executeMcpMenuAction(app, request), .load_catalog, .load_preview, .complete_argument, .cancel => {}, } } return true; } if (app.mcp.menu.screen == .browse and app.mcp.menu.section == .servers) { _ = applyMcpMenuEvent(app, .show_add); } } return true; }
fn handleMcpAddInput(app: *App, byte: u8, max_input_len: usize) !bool { if (comptime !@hasField(App, "mcp")) return false; switch (byte) { '\t' => { try commitMcpAddDraft(app); _ = applyMcpMenuEvent(app, .cycle_add_transport); app.input_runtime.inputResetState().clearCurrent(app.alloc); }, '\r' => { try commitMcpAddDraft(app); app.input_runtime.inputResetState().clearCurrent(app.alloc); const field_count: usize = if (app.mcp.menu.add_transport == .local) 3 else 2; if (app.mcp.menu.add_field_index + 1 < field_count) { _ = applyMcpMenuEvent(app, .{ .move_add_field = .{ .delta = 1, .field_count = field_count, } }); } else { const action: mcp_menu_state.Action = if (app.mcp.menu.add_transport == .local) .add_local else .add_http; const effect = applyMcpMenuEvent( app, .{ .request_action = action }, ) orelse return true; switch (effect) { .action => |request| { if (comptime @hasDecl(App, "saveMcpMenuAdd")) { app.saveMcpMenuAdd( request.generation, app.mcp.menu.add_transport, ) catch |err| { if (comptime @hasDecl(App, "recordMcpMenuEffectFailure")) { try app.recordMcpMenuEffectFailure(request.generation, err); } }; } }, .load_catalog, .load_preview, .complete_argument, .cancel => {}, } } app.shell.render_requests.request(.footer); }, else => try handleTextByte(app, .composer, byte, max_input_len), } return true; }
fn commitMcpAddDraft(app: *App) !void { if (comptime @hasField(App, "mcp")) { try app.mcp.setMenuAddField( app.alloc, app.mcp.menu.add_field_index, app.input_runtime.edit_state.input.items, ); } }
fn handleMcpArgumentInput(app: *App, byte: u8, max_input_len: usize) !bool { if (comptime !@hasField(App, "mcp")) return false; if (app.mcp.menu.pending_generation != null) return true; switch (byte) { '\t' => { try commitMcpArgumentDraft(app); app.input_runtime.inputResetState().clearCurrent(app.alloc); const effect = applyMcpMenuEvent(app, .request_completion) orelse return true; try startMcpMenuEffect(app, effect); }, '\r' => { try commitMcpArgumentDraft(app); app.input_runtime.inputResetState().clearCurrent(app.alloc); const field_count = mcpMenuProjection(app).arguments.len; if (app.mcp.menu.argument_index + 1 < field_count) { _ = applyMcpMenuEvent(app, .{ .move_argument = .{ .delta = 1, .field_count = field_count, } }); } else if (!app.mcp.menuArgumentsValid()) { try app.mcp.setMenuFeedback(app.alloc, "Complete every required MCP argument."); app.shell.render_requests.request(.footer); } else if (applyMcpMenuEvent(app, .begin_preview)) |effect| { try startMcpMenuEffect(app, effect); } }, else => try handleTextByte(app, .composer, byte, max_input_len), } return true; }
fn commitMcpArgumentDraft(app: *App) !void { if (comptime @hasField(App, "mcp")) { const draft = app.input_runtime.edit_state.input.items; const arguments = mcpMenuProjection(app).arguments; if (draft.len == 0 and app.mcp.menu.argument_index < arguments.len and arguments[app.mcp.menu.argument_index].value.items.len > 0) { return; } try app.mcp.setMenuArgumentField( app.alloc, app.mcp.menu.argument_index, draft, ); } }
const CompactCommandMenuKind = enum { statusline, usage, workspace, };
fn activeCompactCommandMenu(app: *App) ?CompactCommandMenuKind { if (app.input_runtime.statusline_menu.active) return .statusline; if (app.input_runtime.usage_menu.active) return .usage; if (app.input_runtime.workspace_menu.active) return .workspace; return null; }
// A space never extends a mention token, so it ends the mention and // inserts through the normal composer path. fn dismissMentionSkillsMenuForSpace(app: *App) void { if (comptime !@hasField(App, "skills")) return; if (!app.skills.menu.active) return; if (!app.skills.menu.origin.isMention()) return; app.skills.closeMenu(); app.shell.render_requests.request(.footer); }
fn closeSkillsMenu(app: *App) bool { if (comptime !@hasField(App, "skills")) return false; if (!app.skills.menu.active) return false; app.skills.closeMenu(); return true; }
fn closeHelpMenu(app: *App, clear_query: bool) bool { if (!app.input_runtime.help_menu.active) return false; app.input_runtime.help_menu.close(); if (clear_query) { app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); } return true; }
fn closeSettingsMenu(app: *App, clear_query: bool) bool { if (!app.input_runtime.settings_menu.active) return false; app.input_runtime.settings_menu.close(); if (clear_query) { app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); } return true; }
fn closeModelMenu(app: *App, clear_query: bool) bool { if (comptime !@hasField(App, "model_cache")) return false; if (!app.model_cache.menu.active) return false; app.model_cache.closeMenu(); if (clear_query) { app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); } restoreModelPickerDraft(app); return true; }
fn openModelBrowseCatalog(app: *App) !void { if (comptime !@hasField(App, "model_cache")) return; if (comptime @hasDecl(App, "ensureModelCache")) app.ensureModelCache(); if (comptime @hasField(App, "skills")) app.skills.closeMenu(); try app.model_cache.openMenu(); app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); app.shell.render_requests.request(.footer); }
/// Ctrl+P opens the same catalog `/model` opens, but the composer is /// only lent to the menu: the draft moves into a stash and comes back /// verbatim when the picker closes, whether a model was picked or not. fn openModelPickerShortcut(app: *App) !void { if (comptime !@hasField(App, "model_cache")) return; if (app.input_runtime.model_picker_draft != null) { // Unreachable through the keyboard (the stash implies the menu // is open); recover a stranded draft rather than overwrite it. debug_trace.logf("input", "model picker draft stashed with menu closed; restoring before reopen", .{}); restoreModelPickerDraft(app); } app.input_runtime.model_picker_draft = composer_stash.State.capture( app.input_runtime.composerStashView(), ); openModelBrowseCatalog(app) catch |err| { restoreModelPickerDraft(app); return err; }; }
fn restoreModelPickerDraft(app: *App) void { if (app.input_runtime.model_picker_draft) |*draft| { draft.restore(app.alloc, app.input_runtime.composerStashView()); app.input_runtime.model_picker_draft = null; app.shell.render_requests.request(.footer); } }
/// Surfaces that own the keyboard make Ctrl+P a no-op instead of /// borrowing a composer they are already using. fn modelPickerShortcutBlocked(app: *App) bool { if (settingsMenuActive(app) or helpMenuActive(app) or skillsMenuActive(app) or sessionMenuActive(app) or mcpMenuActive(app)) return true; if (comptime @hasField(App, "auth")) { if (app.auth.pickerView().active) return true; } if (comptime @hasField(App, "terminal")) { if (app.terminal.fullTranscriptScreenActive()) return true; } return false; }
fn toggleSessionPickerScopeIfActive(app: *App) !bool { if (comptime !runtime_profile.allows(App, .durable_sessions)) return false; if (comptime !@hasField(App, "session_persistence")) return false; if (!app.session_persistence.session_picker.active) return false; return try app_session_runtime.Runtime(App).toggleSessionPickerScope(app); }
fn dismissActiveMenusForComposerEdit(app: *App) bool { return dismissAuthPickerForComposerEdit(app); }
fn dismissActiveMenusThenRedraw(app: *App) void { if (dismissActiveMenusForComposerEdit(app)) { app.shell.render_requests.request(.footer); } }
fn submitSkillsMenuSelection(app: *App, max_input_len: usize) !bool { if (comptime !@hasField(App, "skills")) return false; if (!app.skills.menu.active) return false; // Captured before the sync: a stale $ anchor makes the sync close // the menu, and close() resets origin to .command. const origin = app.skills.menu.origin; syncSkillsMenu(app); const skill = app.skills.selectedMenuSkill() orelse { if (origin.isMention()) { app.skills.closeMenu(); app.shell.render_requests.request(.footer); return false; } return true; }; const target = if (!origin.isMention()) skill_runtime.SkillMenuTarget{ .start = 0, .end = app.input_runtime.edit_state.input.items.len } else app.skills.menu.target orelse skill_runtime.SkillMenuTarget{ .start = app.input_runtime.edit_state.cursor, .end = app.input_runtime.edit_state.cursor, }; const raw_len = app.input_runtime.entities.skillTokenInsertedLen( app.input_runtime.edit_state.input.items, target.end, skill.name, ); if (!app.input_runtime.insertionState().canReplaceRange( target.start, target.end, raw_len, max_input_len, )) { app.skills.closeMenu(); app.shell.render_requests.request(.footer); try input_limit_feedback.report(App, app, .composer, raw_len); return true; }
try app.input_runtime.skillBindingState().bindSkillToken( app.alloc, target.start, target.end, skill.name, skill.path, skill_runtime.skillDisplaySource(app.skills.items, skill), ); app.skills.closeMenu(); app.shell.render_requests.request(.footer); return true; }
fn changeSettingsMenuSelection(app: *App, delta: i32) !void { const menu = &app.input_runtime.settings_menu; if (comptime @hasField(App, "model_cache")) { if (app.model_cache.menu.active) { if (delta < 0) try applyInlineSettingsModelSelection(app); return; } }
const snapshot = app_commands.settingsCatalogSnapshot(app); const query = app.input_runtime.edit_state.input.items; const item = menu.selectedItem(&snapshot, query) orelse return; if (item.id == .model) { if (delta > 0 and comptime @hasField(App, "model_cache")) { if (comptime @hasDecl(App, "ensureModelCache")) app.ensureModelCache(); try app.model_cache.openMenu(); app.shell.render_requests.request(.footer); } return; }
const change = menu.changeSelectedOption(&snapshot, query, delta) orelse return; if (comptime @hasDecl(App, "notificationPreferences")) { try app_commands.applySettingsCatalogChange(app, change); } app.shell.render_requests.request(.footer); }
fn applyInlineSettingsModelSelection(app: *App) !void { const selected = (try app.model_cache.menu.selectedModelAlloc(app.alloc)) orelse return; defer app.alloc.free(selected); try session_commands.Commands(App).selectModelFromPicker( app, selected, app.effort, app.fast_mode, ); app.model_cache.closeMenu(); app.shell.render_requests.request(.footer); }
fn submitSettingsMenuSelection(app: *App) !bool { if (!settingsMenuActive(app)) return false; if (comptime @hasField(App, "model_cache")) { if (app.model_cache.menu.active) { try applyInlineSettingsModelSelection(app); return true; }
const snapshot = app_commands.settingsCatalogSnapshot(app); const selected = app.input_runtime.settings_menu.selectedItem( &snapshot, app.input_runtime.edit_state.input.items, ) orelse return true; if (selected.id == .model) { if (comptime @hasDecl(App, "ensureModelCache")) app.ensureModelCache(); try app.model_cache.openMenu(); app.shell.render_requests.request(.footer); } } return true; }
fn submitCompactCommandMenuSelection( app: *App, menu: CompactCommandMenuKind, max_input_len: usize, ) !void { if (menu == .workspace) { try prepareWorkspaceMenuCommand(app, max_input_len); return; } if (menu == .usage) { _ = app.input_runtime.usage_menu.toggleExpanded( usageMenuVisibleModelItems(app), ); app.shell.render_requests.request(.footer); return; } const snapshot = app_commands.settingsCatalogSnapshot(app); const change = switch (menu) { .statusline => app.input_runtime.statusline_menu.selectedChange(snapshot), .usage, .workspace => unreachable, } orelse return; if (comptime @hasDecl(App, "notificationPreferences")) { try app_commands.applySettingsCatalogMenuChange(app, change); } app.shell.render_requests.request(.footer); }
fn prepareWorkspaceMenuCommand(app: *App, max_input_len: usize) !void { if (comptime !@hasDecl(App, "workspaceAccess")) return; const entries = app.workspaceAccess().entries; const action = app.input_runtime.workspace_menu.selectedAction(entries) orelse return;
var command: std.Io.Writer.Allocating = .init(app.alloc); defer command.deinit(); switch (action) { .add => try command.writer.writeAll("/workspace add "), .remove => |index| { if (index >= entries.len) return; try command.writer.print("/workspace remove {s}", .{entries[index].path}); }, .clear => try command.writer.writeAll("/workspace clear"), } const text = command.written(); if (text.len > max_input_len) return; var prepared_input = try app.input_runtime.textReplacementState().prepare( app.alloc, text, ); defer prepared_input.deinit(app.alloc);
app.input_runtime.workspace_menu.close(); app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); app.input_runtime.textReplacementState().commit(app.alloc, &prepared_input); app.input_runtime.picker.dismissInlinePicker(.slash); app.shell.render_requests.request(.footer); }
fn moveCompactCommandMenu(app: *App, menu: CompactCommandMenuKind, delta: i32) bool { return switch (menu) { .statusline => app.input_runtime.statusline_menu.move(delta), .usage => app.input_runtime.usage_menu.moveModel( delta, usageMenuVisibleModelItems(app), ), .workspace => if (comptime @hasDecl(App, "workspaceAccess")) app.input_runtime.workspace_menu.move( delta, app.workspaceAccess().entries, ) else false, }; }
fn usageMenuVisibleModelItems(app: *App) u16 { const projection = render_input.usageMenuProjection( &app.input_runtime.usage_menu, ); const menu: render_input.CompactCommandMenuProjection = .{ .usage = projection, }; const visible_rows = @min( compact_command_menu_presentation.desiredRowCount( menu, app.shell.layout.cols, ), app.shell.layout.rows -| 3, ); return compact_command_menu_presentation.usageVisibleModelItems( projection, visible_rows, app.shell.layout.cols, ); }
fn routeCompactCommandMenuEscapeAction( app: *App, menu: CompactCommandMenuKind, resolved: input_action.Action, ) !void { if (menu == .usage and (resolved == .cursor_left or resolved == .cursor_right)) { const current = app.input_runtime.usage_menu.navigationScope(); const next = if (resolved == .cursor_left) current.previous() else current.next(); if (next != current) { try refreshUsageMenu(app, next); } return; } if (menu == .usage and resolved == .toggle_permission_mode) { try cycleUsageMenuScope(app, -1); return; } if (menu == .statusline and (resolved == .cursor_left or resolved == .cursor_right)) { const snapshot = app_commands.settingsCatalogSnapshot(app); const delta: i32 = if (resolved == .cursor_left) -1 else 1; const change = switch (menu) { .statusline => app.input_runtime.statusline_menu.changeSelectedOption(&snapshot, delta), .usage, .workspace => unreachable, } orelse return; if (comptime @hasDecl(App, "notificationPreferences")) { try app_commands.applySettingsCatalogMenuChange(app, change); } return; } const delta: i32 = switch (resolved) { .cursor_up => -1, .cursor_down => 1, else => return, }; _ = moveCompactCommandMenu(app, menu, delta); app.shell.render_requests.request(.footer); }
fn refreshUsageMenu(app: *App, scope: usage_report.Scope) !void { if (comptime !runtime_profile.allows(App, .profile_usage)) return; if (comptime @hasDecl(App, "refreshUsageMenu")) { try app.refreshUsageMenu(scope); } else { try app_commands.Handlers(App).refreshUsageMenu(app, scope); } }
fn reloadUsageMenu(app: *App, scope: usage_report.Scope) !void { if (comptime !runtime_profile.allows(App, .profile_usage)) return; if (comptime @hasDecl(App, "reloadUsageMenu")) { try app.reloadUsageMenu(scope); } else if (comptime @hasDecl(App, "refreshUsageMenu")) { try app.refreshUsageMenu(scope); } else { try app_commands.Handlers(App).reloadUsageMenu(app, scope); } }
fn cycleUsageMenuScope(app: *App, delta: i32) !void { const current = app.input_runtime.usage_menu.navigationScope(); const next: usage_report.Scope = if (delta < 0) switch (current) { .days_30 => .session, .days_7 => .days_30, .hours_24 => .days_7, .session => .hours_24, } else switch (current) { .days_30 => .days_7, .days_7 => .hours_24, .hours_24 => .session, .session => .days_30, }; try refreshUsageMenu(app, next); }
fn submitHelpMenuSelection(app: *App, max_input_len: usize, max_prompt_history: usize) !bool { if (!app.input_runtime.help_menu.active) return false; if (app.slashRegistry().matchExact(app.input_runtime.edit_state.input.items) != null) { app.input_runtime.help_menu.close(); return false; } const selected = app.input_runtime.help_menu.selectedSpec( app.slashRegistry(), app.input_runtime.edit_state.input.items, ) orelse { app.shell.render_requests.request(.footer); return true; }; const inserted_len = selected.command.len + @intFromBool(selected.has_args); if (inserted_len > max_input_len) return true; var prepared_input = try app.input_runtime.textReplacementState().prepare( app.alloc, selected.command, ); defer prepared_input.deinit(app.alloc);
app.input_runtime.help_menu.close(); app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); app.input_runtime.textReplacementState().commit(app.alloc, &prepared_input); if (selected.has_args) { try app.input_runtime.insertionState().insertByte(app.alloc, ' ', .clear); app.shell.render_requests.request(.footer); return true; } try submit_rt.submit(app, max_prompt_history); return true; }
fn submitModelMenuSelection(app: *App) !bool { if (comptime !@hasField(App, "model_cache")) return false; if (!app.model_cache.menu.active) return false; const selected = (try app.model_cache.menu.selectedModelAlloc(app.alloc)) orelse { app.shell.render_requests.request(.footer); return true; }; defer app.alloc.free(selected);
if (app.input_runtime.model_picker_draft != null) { // Opened via Ctrl+P: Enter uses the model as-is (current effort // and fast mode, clamped to the model's capabilities) and hands // the composer back to the draft instead of chaining into the // inline effort and fast stages. closeModelMenu owns the close, // composer cleanup, and draft restore, including when applying // the model fails. session_commands.Commands(App).selectModelFromPicker( app, selected, app.effort, app.fast_mode, ) catch |err| { _ = closeModelMenu(app, true); return err; }; _ = closeModelMenu(app, true); return true; }
// Without a stashed draft the restore inside closeModelMenu is a // no-op, so the /model flow shares the same close policy. _ = closeModelMenu(app, true); try completion_rt.beginExactModelSelection(app, selected); return true; }
fn submitAuthPickerSelection(app: *App) !bool { if (comptime !@hasField(App, "auth")) return false; if (app.stream.active) return false; if (!app.auth.pickerView().active) return false; const choice = app.auth.takePickerChoice(app.alloc) orelse { app.shell.render_requests.request(.footer); return true; }; app.applyAuthPickerChoice(choice) catch |err| { debug_trace.logf("auth", "picker choice failed choice={s} err={s}", .{ @tagName(choice), @errorName(err) }); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Could not load that credential. The current source is unchanged.", }, true); app.shell.render_requests.request(.footer); return true; }; app.shell.render_requests.request(.footer); return true; }
fn submitSlashPickerSelection(app: *App) !bool { if (comptime !@hasField(App, "skills")) return false; if (nonSlashPickerOwnsEnter(app)) return false; const items = app.input_runtime.edit_state.input.items; const prefix = command_specs.slashCompletionPrefix( app.slashRegistry(), items, ) orelse return false; if (app.slashRegistry().matchExact(prefix) != null) return false; const count = completion_rt.visibleSlashCompletionCount(app); if (count == 0) return false; const idx = app.input_runtime.picker.slash_completion_index % count; if (!picker_presentation.mixedSlashCompletionIsSkill( app.slashRegistry(), prefix, app.skills.items, idx, )) { const selected = picker_presentation.nthMixedSlashCompletionText( app.slashRegistry(), prefix, app.skills.items, idx, ) orelse return false; const spec = app.slashRegistry().matchExact(selected) orelse return false; if (spec.command.kind != .model) return false; try openModelBrowseCatalog(app); return true; } return try bindSelectedSlashSkill(app, idx); }
fn nonSlashPickerOwnsEnter(app: *App) bool { if (completion_rt.filePickerOwnsSurface(app)) return true; // Bare `/model` and `/model …` own Enter over slash/skill bind. Mid-turn // commit is gated separately when the model list stays hidden. if (app.input_runtime.picker.isModelShapedInput(&app.input_runtime.edit_state)) return true; if (comptime @hasField(App, "stream")) { if (app.stream.active) return false; } if (comptime @hasField(App, "session_persistence")) { if (app.session_persistence.session_picker.active) return true; } if (comptime @hasField(App, "auth")) { if (app.auth.pickerView().active) return true; } return false; }
fn resolveExplicitModelSelection(app: *App, input: []const u8) ExplicitModelSelectionParse { return switch (parseExplicitModelSelection(input)) { .selection => |selection| validateExplicitModelSelection(selection, model_capabilities.resolveForApp(App, app, selection.model)), .none => .none, .invalid => .invalid, }; }
fn submitExplicitModelSelection( app: *App, parsed: ExplicitModelSelectionParse, ) !bool { switch (parsed) { .none => return false, .invalid => { try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = "usage: /model <id> <effort> [normal|fast]", }, true); }, .selection => |selection| { try session_commands.Commands(App).selectModelFromPicker( app, selection.model, selection.effort, selection.fast_mode, ); app.input_runtime.inputResetState().clearCurrent(app.alloc); }, } app.shell.render_requests.request(.footer); return true; }
fn bindSelectedSlashSkill(app: *App, selected_index: usize) !bool { if (comptime !@hasField(App, "skills")) return false; const items = app.input_runtime.edit_state.input.items; const prefix = command_specs.slashCompletionPrefix( app.slashRegistry(), items, ) orelse return false; const skill = picker_presentation.nthMixedSlashCompletionSkill( app.slashRegistry(), prefix, app.skills.items, selected_index, ) orelse return false; const leading_ws = items.len - prefix.len; try app.input_runtime.skillBindingState().bindSkillToken( app.alloc, leading_ws, leading_ws + prefix.len, skill.name, skill.path, skill_runtime.skillDisplaySource(app.skills.items, skill), ); app.input_runtime.picker.resetInlinePickerEpisode(); app.shell.render_requests.request(.footer); return true; }
fn syncCatalogMenus(app: *App) void { syncSettingsMenu(app); syncHelpMenu(app); syncSkillsMenu(app); syncModelMenu(app); syncSessionMenu(app); }
fn syncSettingsMenu(app: *App) void { if (!app.input_runtime.settings_menu.active) return; app.input_runtime.settings_menu.resetForQuery(); }
fn syncHelpMenu(app: *App) void { if (!app.input_runtime.help_menu.active) return; app.input_runtime.help_menu.resetForQuery(); }
fn syncSkillsMenu(app: *App) void { if (comptime !@hasField(App, "skills")) return; if (!app.skills.menu.active) return; if (!app.skills.menu.origin.isMention()) { app.skills.setMenuQuery( app.alloc, app.input_runtime.edit_state.input.items, ); return; } const target = app.skills.menu.target orelse return; const items = app.input_runtime.edit_state.input.items; if (target.start >= items.len or items[target.start] != '$') { app.skills.closeMenu(); return; } const end = skillTokenEnd(items, target.start + 1); app.skills.menu.target = .{ .start = target.start, .end = end }; app.skills.setMenuQuery(app.alloc, items[target.start + 1 .. end]); }
fn syncModelMenu(app: *App) void { if (comptime !@hasField(App, "model_cache")) return; if (!app.model_cache.menu.active) return; app.model_cache.setMenuQuery(app.input_runtime.edit_state.input.items); }
fn syncSessionMenu(app: *App) void { if (comptime !@hasField(App, "session_persistence")) return; const picker = &app.session_persistence.session_picker; if (!picker.active) return; picker.setQuery(app.input_runtime.edit_state.input.items); }
fn skillTokenEnd(items: []const u8, start: usize) usize { var end = start; while (end < items.len) : (end += 1) { const byte = items[end]; if (!(std.ascii.isAlphanumeric(byte) or byte == '_' or byte == '-' or byte == '.' or byte == ':')) break; } return end; }
fn cycleSkillsMenuSource(app: *App, delta: i32) bool { if (comptime !@hasField(App, "skills")) return false; if (!app.skills.menuVisible()) return false; return app.skills.moveMenuSourceFilter(delta); }
fn slashMenuVisibleForCue(app: *App) bool { if (comptime !@hasDecl(App, "playSlashMenuSound")) return false; if (comptime @hasDecl(App, "soundMaxEnabled")) { if (!app.soundMaxEnabled()) return false; } return completion_rt.slashCompletionCount(app) > 0; }
fn announceSlashMenuOpened(app: *App, was_visible: bool) void { if (comptime !@hasDecl(App, "playSlashMenuSound")) return; if (comptime @hasDecl(App, "soundMaxEnabled")) { if (!app.soundMaxEnabled()) return; } if (was_visible) return; if (completion_rt.slashCompletionCount(app) == 0) return; app.playSlashMenuSound(); }
fn cycleModelMenuProvider(app: *App, delta: i32) bool { if (comptime !@hasField(App, "model_cache")) return false; if (!app.model_cache.menu.active) return false; _ = app.model_cache.menu.moveProvider(delta); return true; }
fn cycleSettingsMenuCategory(app: *App, delta: i32) bool { if (!app.input_runtime.settings_menu.cycleCategory(delta)) return false; if (comptime @hasField(App, "model_cache")) app.model_cache.closeMenu(); return true; }
fn cycleHelpMenuCategory(app: *App, delta: i32) bool { return app.input_runtime.help_menu.cycleCategory(delta); }
fn expireEscClearArm(app: *App, now: i64) void { const transition = gesture_state.expireEscapeClear( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.cleared) { app.shell.render_requests.request(.footer); } }
fn expireEscapeInterruptArm(app: *App, now: i64) void { // An armed interrupt guards one active operation. When that // operation settles on its own, the arm must not carry into // whatever work starts next. if (app.input_runtime.gestures.escapeInterruptArmed() and !interrupt_rt.hasActiveOperation(app)) { _ = disarmEscapeInterrupt(app, "operation_settled"); return; } const transition = gesture_state.expireEscapeInterrupt( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.cleared) { debug_trace.logf( "input", "event=esc_interrupt_disarmed reason=timeout", .{}, ); app.shell.render_requests.request(.footer); } }
fn expireCtrlCExitArm(app: *App, now: i64) void { const transition = gesture_state.expireCtrlCExit( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.cleared) { debug_trace.logf( "input", "event=ctrl_c_exit_disarmed reason=timeout", .{}, ); app.shell.render_requests.request(.footer); } }
fn disarmCtrlCExit(app: *App, reason: []const u8) void { const transition = gesture_state.disarmCtrlCExit( app.input_runtime.gestures, ); app.input_runtime.gestures = transition.next; if (transition.cleared) { debug_trace.logf( "input", "event=ctrl_c_exit_disarmed reason={s}", .{reason}, ); app.shell.render_requests.request(.footer); } }
fn disarmEscapeClear(app: *App) bool { const transition = gesture_state.disarmEscapeClear( app.input_runtime.gestures, ); app.input_runtime.gestures = transition.next; return transition.cleared; }
fn disarmEscapeInterrupt(app: *App, reason: []const u8) bool { const transition = gesture_state.disarmEscapeInterrupt( app.input_runtime.gestures, ); app.input_runtime.gestures = transition.next; if (transition.cleared) { debug_trace.logf( "input", "event=esc_interrupt_disarmed reason={s}", .{reason}, ); app.shell.render_requests.request(.footer); } return transition.cleared; }
fn resolveEscape(app: *App, was_cancel_pending: bool, now: i64) !void { if (try full_transcript_rt.routeAction(app, .escape)) return; if (was_cancel_pending) { if (app.question_prompt.isActive()) { // Freeform answers mirror the composer's Esc contract: // Esc on a non-empty draft arms, a second press within // the window clears it, and only an empty field lets // Esc cancel the batch. _ = disarmEscapeInterrupt(app, "question_prompt"); if (app.question_prompt.freeformDraftLen() > 0) { const transition = gesture_state.pressEscapeClear( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.result == .activated) { app.question_prompt.clearFreeformDraft("escape_clear"); } app.shell.render_requests.request(.modal); app.shell.render_requests.request(.footer); return; } try question_rt.cancelQuestionPrompt(app); return; } if (app.approval_prompt.isActive()) { try approval_rt.cancelApprovalOperation(app); _ = disarmEscapeClear(app); _ = disarmEscapeInterrupt(app, "approval_prompt"); return; } if (cancelCompactCommandMenu(app) or (try cancelMcpMenu(app)) or cancelSettingsMenu(app) or cancelHelpMenu(app) or cancelModelMenu(app) or cancelSkillsMenu(app) or cancelSessionMenu(app)) { _ = disarmEscapeClear(app); _ = disarmEscapeInterrupt(app, "menu"); app.shell.render_requests.request(.footer); return; } if (completion_rt.dismissVisibleInlinePicker(app)) { _ = disarmEscapeClear(app); _ = disarmEscapeInterrupt(app, "inline_picker"); app.shell.render_requests.request(.footer); return; } if (interrupt_rt.dismissCompactionFeedback(app)) { _ = disarmEscapeClear(app); _ = disarmEscapeInterrupt(app, "compaction_feedback"); return; } if (interrupt_rt.pauseActiveRecovery(app)) { _ = disarmEscapeClear(app); _ = disarmEscapeInterrupt(app, "recovery_pause"); return; } // Interrupting active work requires a confirming second Esc // within the gesture window. The first press only arms. const transition = gesture_state.pressEscapeInterrupt( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.result == .activated) { try interrupt_rt.cancelActiveOperation(app); } else { debug_trace.logf( "input", "event=esc_interrupt_armed target=active_operation", .{}, ); } _ = disarmEscapeClear(app); app.shell.render_requests.request(.footer); return; }
if (app.approval_prompt.isActive()) { _ = disarmEscapeClear(app); return; } if (cancelCompactCommandMenu(app) or (try cancelMcpMenu(app)) or cancelSettingsMenu(app) or cancelHelpMenu(app) or cancelModelMenu(app) or cancelSkillsMenu(app) or cancelSessionMenu(app)) { _ = disarmEscapeClear(app); app.shell.render_requests.request(.footer); return; } if (popOrCloseAuthPicker(app)) { _ = disarmEscapeClear(app); app.shell.render_requests.request(.footer); return; } if (completion_rt.dismissVisibleInlinePicker(app)) { _ = disarmEscapeClear(app); app.shell.render_requests.request(.footer); return; } if (interrupt_rt.dismissCompactionFeedback(app)) { _ = disarmEscapeClear(app); return; } if (!draftHasState(app)) { _ = disarmEscapeClear(app); return; } const transition = gesture_state.pressEscapeClear( app.input_runtime.gestures, now, ); app.input_runtime.gestures = transition.next; if (transition.result == .activated) { clearDraftState(app, "double_escape"); app.shell.render_requests.request(.footer); if (comptime @hasDecl(App, "playInputClearedSound")) app.playInputClearedSound(); } else { app.shell.render_requests.request(.footer); } }
fn cancelSkillsMenu(app: *App) bool { if (comptime !@hasField(App, "skills")) return false; if (!app.skills.menuVisible()) return false; const mention = app.skills.menu.origin.isMention(); const clear_query = !mention; app.skills.closeMenu(); if (mention) app.input_runtime.picker.dismissInlinePicker(.skill); if (clear_query) { app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); } return true; }
fn cancelCompactCommandMenu(app: *App) bool { if (app.input_runtime.statusline_menu.active) { app.input_runtime.statusline_menu.close(); return true; } if (app.input_runtime.usage_menu.active) { app.input_runtime.usage_menu.close(app.alloc); return true; } if (app.input_runtime.workspace_menu.active) { app.input_runtime.workspace_menu.close(); return true; } return false; }
fn cancelHelpMenu(app: *App) bool { return closeHelpMenu(app, true); }
fn cancelMcpMenu(app: *App) !bool { if (!mcpMenuActive(app)) return false; if (comptime @hasField(App, "mcp")) { debug_trace.logf("mcp", "MCP menu escape screen={s} filter={}", .{ @tagName(app.mcp.menu.screen), app.mcp.menu.filter_active }); if (app.mcp.menu.filter_active) { _ = applyMcpMenuEvent(app, .clear_filter); return true; } if (app.mcp.menu.screen != .browse) { if (app.mcp.menu.screen == .add or app.mcp.menu.screen == .arguments) { app.input_runtime.inputResetState().clearCurrent(app.alloc); } if (applyMcpMenuEvent(app, .back)) |effect| switch (effect) { .cancel => app.mcp.cancelMenuOperation(), .load_catalog, .load_preview, .complete_argument, .action => {}, }; return true; } } if (comptime @hasDecl(App, "closeMcpMenu")) app.closeMcpMenu(); app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); if (comptime @hasDecl(App, "presentProjectMcpPrompt")) { if (projectMcpPromptOwnsInput(app)) try app.presentProjectMcpPrompt(); } return true; }
fn cancelSettingsMenu(app: *App) bool { return closeSettingsMenu(app, true); }
fn cancelModelMenu(app: *App) bool { return closeModelMenu(app, true); }
fn cancelSessionMenu(app: *App) bool { if (comptime !@hasField(App, "session_persistence")) return false; if (!app.session_persistence.session_picker.active) return false; app_session_runtime.Runtime(App).cancelSessionPickerToComposer(app); app.input_runtime.inputResetState().clearCurrent(app.alloc); paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks); return true; } };}const ApprovalCancelEvent = enum { request_cancel,};
const FakeApprovalCancelWorker = struct { cancel_requested: bool = false, order: [1]ApprovalCancelEvent = undefined, order_len: usize = 0,
fn record(self: *FakeApprovalCancelWorker, event: ApprovalCancelEvent) void { if (self.order_len < self.order.len) { self.order[self.order_len] = event; self.order_len += 1; } }
pub fn isCancelRequested(self: *const FakeApprovalCancelWorker) bool { return self.cancel_requested; }
pub fn queuedPromptCount(_: *const FakeApprovalCancelWorker) usize { return 0; }
pub fn requestCancel(self: *FakeApprovalCancelWorker) void { self.cancel_requested = true; self.record(.request_cancel); }
pub fn cancelApprovalTurn(self: *FakeApprovalCancelWorker) void { self.requestCancel(); }
fn submitPermissionResponse( _: *FakeApprovalCancelWorker, _: u64, response: permission_request.OwnedPermissionResponse, ) worker_runtime.PermissionSubmissionResult { var owned = response; owned.deinit(); return .accepted; }
pub fn submitQuestionBatchAnswer(_: *FakeApprovalCancelWorker, _: std.mem.Allocator, _: ?[]const []const u8) !void {}};
const FakeApprovalPacer = struct { cleared: bool = false,
pub fn clear(self: *FakeApprovalPacer, alloc: std.mem.Allocator) void { _ = alloc; self.cleared = true; }};
const FakeInactiveSubagents = struct { fn isViewActive(_: *const FakeInactiveSubagents) bool { return false; }};
const FakeApprovalShell = struct { render_requests: render_request.RenderRequestState = .{}, layout: types.Layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, transcript: std.ArrayList(u8) = .empty,
fn deinit(self: *FakeApprovalShell, alloc: std.mem.Allocator) void { self.transcript.deinit(alloc); }
fn updateRawBytesEntry(_: *FakeApprovalShell, _: std.mem.Allocator, _: u32, _: []const u8) !bool { return false; }
fn appendRawTranscriptEntry(_: *FakeApprovalShell, _: std.mem.Allocator, _: []const u8) !u32 { return 1; }
pub fn scrollFullTranscript( _: *FakeApprovalShell, _: input_action.MouseWheel, _: transcript_runtime.TranscriptRuntime.FullTranscriptScrollUnit, ) void {}};
const routing_test_slash_specs = [_]command_specs.SlashSpec{ .{ .kind = .help, .command = "/help", .help_entry = "/help", .completion_description = "show available slash commands", .presentation_category = .general }, .{ .kind = .clear_screen, .command = "/clear", .help_entry = "/clear", .completion_description = "clear the terminal transcript", .presentation_category = .general }, .{ .kind = .image, .command = "/image", .aliases = &.{"/img"}, .help_entry = "/image <path> (/img)", .completion_description = "attach an image by path", .presentation_category = .media, .has_args = true, .accepts_payload = true }, .{ .kind = .images, .command = "/images", .help_entry = "/images [clear]", .completion_description = "manage pending image attachments", .presentation_category = .media, .has_args = true, .accepts_payload = true }, .{ .kind = .model, .command = "/model", .help_entry = "/model <id-or-query>", .completion_description = "choose a model", .presentation_category = .model, .has_args = true, .accepts_payload = true, .requires_prompt_credential = true }, .{ .kind = .skills, .command = "/skills", .help_entry = "/skills", .completion_description = "browse and manage skills", .presentation_category = .extensions, .has_args = true, .accepts_payload = true }, .{ .kind = .workspace, .command = "/workspace", .help_entry = "/workspace [list|add PATH|remove PATH|clear]", .completion_description = "manage additional workspace directories", .presentation_category = .workspace, .has_args = true, .accepts_payload = true },};const routing_test_slash_registry = command_specs.SlashRegistry{ .commands = routing_test_slash_specs[0..] };
const FakeApprovalCancelApp = struct { alloc: std.mem.Allocator, worker: FakeApprovalCancelWorker = .{}, approval_prompt: approval_prompt.ApprovalPrompt = .{}, approval_screen: interaction_state.ApprovalScreenState = .{}, question_prompt: question_prompt.QuestionPrompt = .{}, subagents: FakeInactiveSubagents = .{}, input_runtime: core_input_runtime.Runtime = .{}, terminal_input_runtime: test_ui_input.Runtime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, shell: FakeApprovalShell = .{}, stream: types.StreamState = .{ .active = true }, pacer: FakeApprovalPacer = .{}, transcript: std.ArrayList(u8) = .empty, last_class: transcript_runtime.RawEntryClass = .unknown_raw, notice_topic: std.ArrayList(u8) = .empty, notice_body: std.ArrayList(u8) = .empty, notice_tone: types.NoticeTone = .information,
pub fn slashRegistry(_: *const FakeApprovalCancelApp) command_specs.SlashRegistry { return routing_test_slash_registry; }
fn deinit(self: *FakeApprovalCancelApp) void { self.approval_prompt.deinit(self.alloc); self.question_prompt.deinit(self.alloc); self.clearPendingImages(); self.pending_images.deinit(self.alloc); self.input_runtime.deinit(self.alloc); self.terminal_input_runtime.deinit(self.alloc); self.shell.deinit(self.alloc); self.transcript.deinit(self.alloc); self.notice_topic.deinit(self.alloc); self.notice_body.deinit(self.alloc); }
fn clearPendingImages(self: *FakeApprovalCancelApp) void { self.input_runtime.entities.image_tokens.clearRetainingCapacity(); for (self.pending_images.items) |image| types.freeImageAttachment(self.alloc, image); self.pending_images.clearRetainingCapacity(); }
fn ensureTrailingBlankRow(self: *FakeApprovalCancelApp) !void { try self.transcript.appendSlice(self.alloc, "\n\n"); }
fn writeTranscript(self: *FakeApprovalCancelApp, content: []const u8, redraw: bool) !void { _ = redraw; try self.transcript.appendSlice(self.alloc, content); }
pub fn writeTranscriptClassified(self: *FakeApprovalCancelApp, content: []const u8, redraw: bool, class: transcript_runtime.RawEntryClass) !void { self.last_class = class; try self.writeTranscript(content, redraw); }
pub fn writeDomainNotice(self: *FakeApprovalCancelApp, notice: types.SemanticNotice, _: bool) !void { self.notice_topic.clearRetainingCapacity(); self.notice_body.clearRetainingCapacity(); try self.notice_topic.appendSlice(self.alloc, notice.topic); try self.notice_body.appendSlice(self.alloc, notice.body); self.notice_tone = notice.tone; }
pub fn stopStream(self: *FakeApprovalCancelApp) void { self.stream = .{}; }
pub fn appendReplaceableTranscriptLine(self: *FakeApprovalCancelApp, content: []const u8) !u32 { try self.shell.transcript.appendSlice(self.alloc, content); return 1; }
pub fn appendReplaceableTranscriptLineClassified(self: *FakeApprovalCancelApp, content: []const u8, _: transcript_runtime.RawEntryClass) !u32 { return self.appendReplaceableTranscriptLine(content); }
pub fn fileCompletions( _: *FakeApprovalCancelApp, _: []const u8, out: []file_index.SearchResult, _: []file_index.MatchSpan, _: []u8, ) file_index.SearchError!usize { if (out.len == 0) return 0; out[0] = .{ .path = "file.zig", .kind = .file, .matched_spans = &.{} }; return 1; }
pub fn modelCompletions(_: *FakeApprovalCancelApp, _: []const u8, out: [][]const u8) usize { if (out.len == 0) return 0; out[0] = "openai/gpt-5"; return 1; }
pub fn transitionFullTranscriptProjection( _: *FakeApprovalCancelApp, event: transcript_presentation.Event, ) !transcript_presentation.Depth { return transcript_presentation.Depth.inline_mode.transition( event, ); }};
const RoutingSubagents = struct { active: bool = false, main_approval_presented: bool = false, handled_keys: usize = 0, toggle_view_calls: usize = 0, manager_paste: core_input_runtime.Runtime = .{},
fn deinit(self: *RoutingSubagents, alloc: std.mem.Allocator) void { self.manager_paste.deinit(alloc); }
pub fn isViewActive(self: *const RoutingSubagents) bool { return self.active; }
pub fn mainApprovalPresented(self: *const RoutingSubagents) bool { return self.main_approval_presented; }
pub fn count(_: *const RoutingSubagents) usize { return 0; }
pub fn panelText(_: *RoutingSubagents, alloc: std.mem.Allocator, _: u16, _: u16, _: transcript_runtime.Styles) ![]u8 { return alloc.dupe(u8, ""); }
pub fn managerPasteActive(self: *const RoutingSubagents) bool { return self.active and self.manager_paste.paste.active(); }
pub fn beginManagerPaste(self: *RoutingSubagents) void { self.manager_paste.paste.begin(.decision_prompt, std.math.maxInt(usize)); }
pub fn consumeManagerPasteByte( self: *RoutingSubagents, alloc: std.mem.Allocator, byte: u8, ) !bool { if (!self.managerPasteActive()) return false; try self.manager_paste.paste.consumeByte(alloc, byte); return true; }
pub fn settleManagerPasteDeliveryEpoch( self: *RoutingSubagents, _: std.mem.Allocator, ) bool { switch (self.manager_paste.paste.settleDeliveryEpoch()) { .none => return false, .finish => self.manager_paste.paste.resetWithTrace(.decision_prompt_active), .reject => self.manager_paste.paste.resetWithTrace(.unsafe_suffix), } return true; }};
const RoutingUpgradeStatus = struct { state: auto_upgrade.State = .ready, stop_count: usize = 0,
pub fn getState(self: *const RoutingUpgradeStatus) auto_upgrade.State { return self.state; }
pub fn stop(self: *RoutingUpgradeStatus) void { self.stop_count += 1; }
pub fn statusLabel(_: *const RoutingUpgradeStatus, _: []u8) []const u8 { return ""; }};
const RoutingWorker = struct { compaction: @import("../output/compaction_activity.zig").State = .{},
submitted_permission: ?ToolPermissionDecision = null, submitted_permission_feedback: [64]u8 = undefined, submitted_permission_feedback_len: usize = 0, active_permission_request_id: ?u64 = null, permission_response: ?ToolPermissionDecision = null, resize_signal_on_submit: ?*shell_runtime.ResizeApprovalInterlock = null, question_cancelled: bool = false, cancel_requested_when_question_cancelled: bool = false, submitted_question_answer: [64]u8 = undefined, submitted_question_answer_len: usize = 0, submitted_question_answers: [2][64]u8 = undefined, submitted_question_answer_lens: [2]usize = .{ 0, 0 }, submitted_question_count: usize = 0, cancel_requested: bool = false, question_source: worker_runtime.QuestionPromptSource = .agent_question, admission_snapshot: worker_runtime.InteractiveAdmissionSnapshot = .open, queued_count: usize = 0, queued_steer_text: ?[]const u8 = null, synced_permission_mode: ?types.PermissionMode = null, permission_mode_sync_count: usize = 0,
pub fn compactionActivitySnapshot(self: *RoutingWorker) @import("../output/compaction_activity.zig").Snapshot { return self.compaction.snapshot; }
pub fn dismissCompactionActivity(self: *RoutingWorker, id: @import("../output/compaction_activity.zig").OperationId, revision: u64) bool { return self.compaction.dismiss(id, revision); }
pub fn queuedPromptCount(self: *const RoutingWorker) usize { return self.queued_count; }
pub fn popQueuedSteerForEdit(self: *RoutingWorker, alloc: std.mem.Allocator) !?[]u8 { const text = self.queued_steer_text orelse return null; self.queued_steer_text = null; return try alloc.dupe(u8, text); }
pub fn activeTurnId(_: *const RoutingWorker) u64 { return 1; }
pub fn isCancelRequested(self: *const RoutingWorker) bool { return self.cancel_requested; }
pub fn requestCancel(self: *RoutingWorker) void { self.cancel_requested = true; }
pub fn requestInteractiveCancel(self: *RoutingWorker) void { self.cancel_requested = true; }
pub fn interactiveAdmissionSnapshot(self: *RoutingWorker) worker_runtime.InteractiveAdmissionSnapshot { return self.admission_snapshot; }
pub fn cancelApprovalTurn(self: *RoutingWorker) void { self.cancel_requested = true; if (self.active_permission_request_id != null and self.permission_response == null) { self.permission_response = .deny; } }
pub fn submitPermissionResponse( self: *RoutingWorker, request_id: u64, response: permission_request.OwnedPermissionResponse, ) worker_runtime.PermissionSubmissionResult { var owned = response; defer owned.deinit(); const decision = owned.decision; if (owned.feedback) |feedback| { self.submitted_permission_feedback_len = @min( feedback.len, self.submitted_permission_feedback.len, ); @memcpy( self.submitted_permission_feedback[0..self.submitted_permission_feedback_len], feedback[0..self.submitted_permission_feedback_len], ); } if (self.resize_signal_on_submit) |interlock| { interlock.noteResizeSignal(); } if (self.active_permission_request_id) |active_id| { if (self.permission_response != null) return .no_pending; if (request_id != active_id) return .stale; self.permission_response = decision; } self.submitted_permission = decision; return .accepted; }
pub fn submitQuestionBatchAnswer(self: *RoutingWorker, _: std.mem.Allocator, answers: ?[]const []const u8) !void { if (answers == null) { self.cancel_requested_when_question_cancelled = self.cancel_requested; self.question_cancelled = true; return; } const submitted = answers.?; self.submitted_question_count = submitted.len; for (submitted[0..@min(submitted.len, self.submitted_question_answers.len)], 0..) |answer, index| { const len = @min(answer.len, self.submitted_question_answers[index].len); @memcpy(self.submitted_question_answers[index][0..len], answer[0..len]); self.submitted_question_answer_lens[index] = len; if (index == 0) { self.submitted_question_answer_len = len; @memcpy(self.submitted_question_answer[0..len], answer[0..len]); } } }
pub fn pendingQuestionBatchSource(self: *RoutingWorker) worker_runtime.QuestionPromptSource { return self.question_source; }
pub fn syncQueuedPromptModel(_: *RoutingWorker, _: std.mem.Allocator, _: []const u8) !void {}
pub fn syncQueuedPromptPermissionSnapshot(self: *RoutingWorker, snapshot: worker_runtime.PermissionSnapshot) void { self.synced_permission_mode = snapshot.mode; self.permission_mode_sync_count += 1; }
pub fn syncQueuedPromptFastMode(_: *RoutingWorker, _: bool) void {}
pub fn syncQueuedPromptEffort(_: *RoutingWorker, _: types.ReasoningEffort) void {}
pub fn clearQueuedPrompts( self: *RoutingWorker, _: std.mem.Allocator, _: []const types.ImageAttachment, ) void { self.queued_count = 0; }
pub fn removeQueuedPrompt( self: *RoutingWorker, _: std.mem.Allocator, _: u64, _: []const types.ImageAttachment, ) bool { if (self.queued_count == 0) return false; self.queued_count -= 1; return true; }};
const RoutingPacer = struct { pub fn clear(_: *RoutingPacer, _: std.mem.Allocator) void {}};
fn routingFullTranscriptStyles() transcript_runtime.Styles { return .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", };}
fn armCtrlCExitForTest(input_runtime: *core_input_runtime.Runtime, armed_ms: i64) void { input_runtime.gestures = gesture_state.disarmCtrlCExit( input_runtime.gestures, ).next; input_runtime.gestures = gesture_state.pressCtrlCExit( input_runtime.gestures, armed_ms, ).next;}
fn armEscapeClearForTest(input_runtime: *core_input_runtime.Runtime, armed_ms: i64) void { input_runtime.gestures = gesture_state.disarmEscapeClear( input_runtime.gestures, ).next; input_runtime.gestures = gesture_state.pressEscapeClear( input_runtime.gestures, armed_ms, ).next;}
test "project MCP prompt waits for every existing modal owner" { const base = ProjectMcpPromptInputState{ .active = true, .question_active = false, .approval_active = false, .subagent_active = false, .menu_active = false, .authentication_active = false, }; try std.testing.expect(projectMcpPromptMayOwnInput(base)); var blocked = base; blocked.question_active = true; try std.testing.expect(!projectMcpPromptMayOwnInput(blocked)); blocked = base; blocked.approval_active = true; try std.testing.expect(!projectMcpPromptMayOwnInput(blocked)); blocked = base; blocked.subagent_active = true; try std.testing.expect(!projectMcpPromptMayOwnInput(blocked)); blocked = base; blocked.menu_active = true; try std.testing.expect(!projectMcpPromptMayOwnInput(blocked)); blocked = base; blocked.authentication_active = true; try std.testing.expect(!projectMcpPromptMayOwnInput(blocked)); var inactive = base; inactive.active = false; try std.testing.expect(!projectMcpPromptMayOwnInput(inactive));}
const RoutingFakeApp = struct { pub const input_byte_limit: usize = 4096;
alloc: std.mem.Allocator, metrics: types.Metrics = .{}, approval_prompt: approval_prompt.ApprovalPrompt = .{}, approval_screen: interaction_state.ApprovalScreenState = .{}, question_prompt: question_prompt.QuestionPrompt = .{}, input_runtime: core_input_runtime.Runtime = .{}, terminal_input_runtime: test_ui_input.Runtime = .{}, shell: transcript_runtime.TranscriptRuntime = .{}, terminal: shell_runtime.TerminalState = .{}, worker: RoutingWorker = .{}, subagents: RoutingSubagents = .{}, permission_engine: permissions.PermissionEngine = .{}, upgrader: RoutingUpgradeStatus = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, next_image_id_counter: usize = 1, selected_model: std.ArrayList(u8) = .empty, workspace_root: []const u8 = "", stream: types.StreamState = .{}, session_persistence: app_session_runtime.Persistence = .{}, skills: skill_runtime.Runtime = .{}, pacer: RoutingPacer = .{}, transcript: std.ArrayList(u8) = .empty, notice_topic: std.ArrayList(u8) = .empty, notice_body: std.ArrayList(u8) = .empty, notice_tone: types.NoticeTone = .information, notice_visibility: types.NoticeVisibility = .compact_and_full, notice_write_count: usize = 0, last_replaceable_class: transcript_runtime.RawEntryClass = .unknown_raw, auth: auth_runtime.Runtime = .{}, fast_mode: bool = false, effort: types.ReasoningEffort = .auto, should_exit: bool = false, statusline_context: bool = false, permission_state: app_permission_runtime.State = .{}, total_input_tokens: u64 = 0, last_command: ?[]u8 = null, input_len_at_command: usize = 0, pasted_block_count_at_command: usize = 0, preference_commit_count: usize = 0, last_preference_model: std.ArrayList(u8) = .empty, last_preference_effort: ?types.ReasoningEffort = null, last_preference_fast_mode: ?bool = null, permission_mode_preference_commit_count: usize = 0, last_preference_permission_mode: ?types.PermissionMode = null, model_completion_values: []const []const u8 = &.{}, gateway_metadata_model: ?[]const u8 = null, gateway_metadata: model_capabilities.GatewayMetadata = .{}, model_cache_loading: bool = false, model_cache: model_cache_runtime.Runtime, file_completion_values: []const file_index.Candidate = &.{}, file_completion_current: bool = true, expected_file_completion_kind: ?file_index.CandidateKind = null, validated_file_completion_kind: ?file_index.CandidateKind = null, file_completion_error: ?file_index.SearchError = null, file_completions_depend_on_index: bool = true, file_index_refresh_count: usize = 0, file_index_loading: bool = false, file_index_failed: bool = false, submitted_prompt_count: usize = 0, submitted_prompt: [128]u8 = undefined, submitted_prompt_len: usize = 0, approval_resize_interlock: shell_runtime.ResizeApprovalInterlock = .{}, inject_resize_before_affirmative_claim: bool = false, affirmative_claim_count: usize = 0, affirmative_release_count: usize = 0, resume_selected_count: usize = 0, resume_selected_error: anyerror = error.SessionStoreUnavailable, resume_selected_closes_picker: bool = false, load_more_session_count: usize = 0, selected_credential_source: ?types.CredentialSource = null, selected_auth_action: ?auth_runtime.AcquisitionAction = null, selected_auth_team: ?usize = null, prompt_retry_after_auth: bool = false, upgrade_apply_count: usize = 0, upgrade_denied_count: usize = 0, suspend_count: usize = 0, workspace_access: app_workspace_runtime.Access = .{},
pub fn slashRegistry(_: *const RoutingFakeApp) command_specs.SlashRegistry { return routing_test_slash_registry; }
pub fn urlOpener(_: *const RoutingFakeApp) host.UrlOpener { return .{ .open_fn = routingOpenUrl }; }
fn init(alloc: std.mem.Allocator) !RoutingFakeApp { var app = RoutingFakeApp{ .alloc = alloc, .model_cache = model_cache_runtime.Runtime.init(alloc, "/v1/models"), }; try app.selected_model.appendSlice(alloc, "test/model"); app.shell.layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }; app.shell.owned_top_row = 1; app.shell.viewport_top_row = 1; app.shell.cursor_row = 10; app.shell.cursor_col = 1; return app; }
fn deinit(self: *RoutingFakeApp) void { self.auth.deinit(self.alloc); self.approval_prompt.deinit(self.alloc); self.question_prompt.deinit(self.alloc); self.input_runtime.deinit(self.alloc); self.terminal_input_runtime.deinit(self.alloc); self.subagents.deinit(self.alloc); self.shell.deinit(self.alloc); self.pending_images.deinit(self.alloc); self.selected_model.deinit(self.alloc); self.session_persistence.deinit(self.alloc); self.transcript.deinit(self.alloc); self.notice_topic.deinit(self.alloc); self.notice_body.deinit(self.alloc); self.last_preference_model.deinit(self.alloc); self.permission_engine.deinit(self.alloc); self.model_cache.deinit(); self.workspace_access.deinit(self.alloc); if (self.last_command) |command| self.alloc.free(command); }
pub fn workspaceAccess(self: *RoutingFakeApp) *app_workspace_runtime.Access { return &self.workspace_access; }
pub fn flushBeforeBlockingExternalWork(_: *RoutingFakeApp) !void {}
pub fn cancelPromptRetryAfterAuth(self: *RoutingFakeApp) void { self.prompt_retry_after_auth = false; }
pub fn suspendToJobControl(self: *RoutingFakeApp) !void { self.suspend_count += 1; }
pub fn modelCompletions(self: *RoutingFakeApp, query: []const u8, out: *[32][]const u8) usize { if (self.model_cache_loading) return 0; var count: usize = 0; for (self.model_completion_values) |value| { if (count == out.len) break; if (query.len > 0 and std.ascii.indexOfIgnoreCase(value, query) == null) continue; out[count] = value; count += 1; } return count; }
pub fn catalogModelCompletion(self: *RoutingFakeApp, model: []const u8) ?[]const u8 { if (self.model_cache_loading) return null; for (self.model_completion_values) |value| { if (std.mem.eql(u8, value, model)) return value; } return null; }
pub fn fileCompletions( self: *RoutingFakeApp, _: []const u8, out: []file_index.SearchResult, _: []file_index.MatchSpan, _: []u8, ) file_index.SearchError!usize { if (self.file_completion_error) |err| return err; const count = @min(self.file_completion_values.len, out.len); for (self.file_completion_values[0..count], 0..) |value, index| { out[index] = .{ .path = value.path, .kind = value.kind, .matched_spans = &.{} }; } return count; }
pub fn prepareDirectoryCompletion(self: *RoutingFakeApp) void { const completion = @import("../input/file_completion_state.zig"); const state = &self.input_runtime.picker.file_completion; if (self.file_completion_error != null) { state.stage(self.alloc, .{ .state = .ready }, .unavailable, null); return; } var results: [completion.capacity]file_index.SearchResult = undefined; for (self.file_completion_values, 0..) |value, i| results[i] = .{ .path = value.path, .kind = value.kind, .matched_spans = &.{} }; const rows = completion.Rows.copy(self.alloc, results[0..self.file_completion_values.len]) catch { state.stage(self.alloc, .{ .state = .ready }, .unavailable, null); return; }; state.stage(self.alloc, .{ .state = .ready }, if (rows.results.len == 0) .empty else .ready, rows); }
pub fn refreshFileIndex(self: *RoutingFakeApp) void { self.file_index_refresh_count += 1; }
pub fn fileCompletionsDependOnIndex(self: *const RoutingFakeApp, _: []const u8) bool { return self.file_completions_depend_on_index; }
pub fn refreshUsageMenu( self: *RoutingFakeApp, scope: usage_report.Scope, ) !void { self.input_runtime.usage_menu.requested_scope = scope; }
pub fn isCurrentFileCompletion(self: *RoutingFakeApp, _: []const u8, _: []const u8, kind: file_index.CandidateKind) bool { self.validated_file_completion_kind = kind; return self.file_completion_current and (self.expected_file_completion_kind == null or self.expected_file_completion_kind.? == kind); }
pub fn isModelCacheLoading(self: *const RoutingFakeApp) bool { return self.model_cache_loading; }
pub fn isModelCacheFailed(_: *const RoutingFakeApp) bool { return false; }
pub fn resolvedModelCapabilities(self: *RoutingFakeApp, model: []const u8) model_capabilities.Capabilities { if (self.gateway_metadata_model) |metadata_model| { if (std.mem.eql(u8, metadata_model, model)) { return model_capabilities.resolveCapabilities(model, self.gateway_metadata); } } return model_capabilities.capabilitiesForModel(model); }
fn setGatewayControls( self: *RoutingFakeApp, model: []const u8, efforts: []const types.ReasoningEffort, supports_fast_mode: bool, ) void { self.gateway_metadata_model = model; self.gateway_metadata = .{ .reasoning_efforts = .fromSlice(efforts), .supports_fast_mode = supports_fast_mode, }; }
pub fn isFileIndexLoading(self: *const RoutingFakeApp) bool { return self.file_index_loading; }
pub fn isFileIndexFailed(self: *const RoutingFakeApp) bool { return self.file_index_failed; }
pub fn appendReplaceableTranscriptLine(self: *RoutingFakeApp, content: []const u8) !u32 { try self.transcript.appendSlice(self.alloc, content); return 1; }
pub fn appendReplaceableTranscriptLineClassified(self: *RoutingFakeApp, content: []const u8, class: transcript_runtime.RawEntryClass) !u32 { self.last_replaceable_class = class; return self.appendReplaceableTranscriptLine(content); }
pub fn writeTranscriptClassified(self: *RoutingFakeApp, content: []const u8, _: bool, _: transcript_runtime.RawEntryClass) !void { try self.transcript.appendSlice(self.alloc, content); }
pub fn writeDomainNotice(self: *RoutingFakeApp, notice: types.SemanticNotice, _: bool) !void { self.notice_write_count += 1; self.notice_topic.clearRetainingCapacity(); self.notice_body.clearRetainingCapacity(); try self.notice_topic.appendSlice(self.alloc, notice.topic); try self.notice_body.appendSlice(self.alloc, notice.body); self.notice_tone = notice.tone; self.notice_visibility = notice.visibility; const rendered = if (notice.topic.len > 0) try std.fmt.allocPrint(self.alloc, "{s} {s}: {s}", .{ types.noticeGlyph(notice.tone), notice.topic, notice.body, }) else try std.fmt.allocPrint(self.alloc, "{s} {s}", .{ types.noticeGlyph(notice.tone), notice.body }); defer self.alloc.free(rendered); try self.transcript.appendSlice(self.alloc, rendered); }
pub fn appendDomainNotice(self: *RoutingFakeApp, notice: types.SemanticNotice) !u32 { return self.shell.appendSemanticNotice(self.alloc, notice); }
pub fn appendReplaceableDomainNotice(self: *RoutingFakeApp, notice: types.SemanticNotice) !u32 { return self.shell.appendReplaceableSemanticNotice(self.alloc, notice); }
pub fn replaceDomainNotice(self: *RoutingFakeApp, entry_id: u32, notice: types.SemanticNotice) !bool { return self.shell.replaceSemanticNotice(self.alloc, entry_id, notice); }
pub fn writeUserPromptCard(self: *RoutingFakeApp, user: types.UserTurn) !void { try self.transcript.appendSlice(self.alloc, user.text); }
pub fn stopStream(self: *RoutingFakeApp) void { self.stream = .{}; }
pub fn prepareLiveSessionResume(_: *RoutingFakeApp) !void {}
pub fn finishLiveSessionResume(_: *RoutingFakeApp) !void {}
pub fn resumeSelectedSession(self: *RoutingFakeApp) !bool { self.resume_selected_count += 1; if (self.resume_selected_closes_picker) { self.session_persistence.session_picker.active = false; } return self.resume_selected_error; }
pub fn loadMoreSessionPicker(self: *RoutingFakeApp) !bool { const picker = &self.session_persistence.session_picker; if (!picker.active or picker.load_state != .ready or !picker.has_more or picker.selected != picker.filteredItemCount()) return false; self.load_more_session_count += 1; return true; }
pub fn applyAuthPickerChoice(self: *RoutingFakeApp, choice: auth_runtime.Choice) !void { switch (choice) { .provider => {}, .source => |source| _ = try self.selectCredentialSource(source), .action => |action| self.selected_auth_action = action, .team => |index| self.selected_auth_team = index, } }
pub fn selectCredentialSource(self: *RoutingFakeApp, source: types.CredentialSource) !bool { self.selected_credential_source = source; return true; }
pub fn prepareResumeHandoffForUpgrade(_: *RoutingFakeApp) !void {}
pub fn requestUpgradeRelaunch( self: *RoutingFakeApp, _: []const u8, ) !void { self.upgrade_apply_count += 1; }
pub fn requestResumeHandoffForUpgrade(_: *RoutingFakeApp) void {}
pub fn applyReadyUpgradeShortcut(self: *RoutingFakeApp) !void { const outcome = try app_upgrade_runtime.Runtime(RoutingFakeApp).applyReadyUpgradeWithDeps( self, .{ .ctx = self, .current_executable_path = currentExecutablePathForRouting, }, ); if (outcome != .relaunch_requested) { self.upgrade_denied_count += 1; } }
pub fn nextImageId(self: *RoutingFakeApp) usize { return image_attachments.allocateImageId(&self.next_image_id_counter); }
pub fn captureImageAttachment(_: *RoutingFakeApp, _: *types.ImageAttachment) !void { return error.MissingImageSnapshot; }
pub fn peekNextImageId(self: *const RoutingFakeApp) usize { return self.next_image_id_counter; }
pub fn transitionFullTranscriptProjection( self: *RoutingFakeApp, event: transcript_presentation.Event, ) !transcript_presentation.Depth { self.shell.setCommandOutputRenderPolicy( routingFullTranscriptStyles(), ); const depth = self.shell.transcriptPresentationDepth().transition(event); _ = try self.shell.setTranscriptPresentationDepth(self.alloc, depth); return depth; }
pub fn writeSubagentSnapshot(self: *RoutingFakeApp) !void { self.subagents.toggle_view_calls += 1; }
pub fn admitPendingApprovalResize(self: *RoutingFakeApp) bool { if (!self.approval_resize_interlock.takeResizePending()) return false; self.shell.render_requests.observeResizeSignal(100, 80); return true; }
pub fn beforeApprovalAffirmativeClaim(self: *RoutingFakeApp) void { if (!self.inject_resize_before_affirmative_claim) return; self.inject_resize_before_affirmative_claim = false; self.approval_resize_interlock.noteResizeSignal(); }
pub fn claimApprovalAffirmative(self: *RoutingFakeApp) bool { self.affirmative_claim_count += 1; return self.approval_resize_interlock.claimAffirmative(); }
pub fn releaseApprovalAffirmative(self: *RoutingFakeApp) void { self.affirmative_release_count += 1; self.approval_resize_interlock.releaseAffirmative(); }
pub fn enqueuePrompt(self: *RoutingFakeApp, text: []const u8) !bool { self.submitted_prompt_len = @min(text.len, self.submitted_prompt.len); @memcpy(self.submitted_prompt[0..self.submitted_prompt_len], text[0..self.submitted_prompt_len]); self.submitted_prompt_count += 1; return true; }
pub fn handleCommand(self: *RoutingFakeApp, command: []const u8) !void { if (self.last_command) |previous| self.alloc.free(previous); self.last_command = try self.alloc.dupe(u8, command); self.input_len_at_command = self.input_runtime.edit_state.input.items.len; self.pasted_block_count_at_command = self.input_runtime.entities.pasted_blocks.items.len; }
pub fn persistRuntimePreferences( self: *RoutingFakeApp, patch: app_session_runtime.SessionPreferencePatch, ) app_session_runtime.PreferenceCommitResult { self.preference_commit_count += 1; self.last_preference_model.clearRetainingCapacity(); if (patch.model) |model| { self.last_preference_model.appendSlice(self.alloc, model) catch return .{ .settings_error = error.OutOfMemory }; } self.last_preference_effort = patch.effort; self.last_preference_fast_mode = patch.fast_mode; return .{}; }
pub fn persistPermissionModePreference( self: *RoutingFakeApp, mode: types.PermissionMode, ) !void { self.permission_mode_preference_commit_count += 1; self.last_preference_permission_mode = mode; }
pub fn clearPendingImages(self: *RoutingFakeApp) void { self.input_runtime.entities.image_tokens.clearRetainingCapacity(); image_attachments.discardImageSnapshots(self.alloc, self.pending_images.items); for (self.pending_images.items) |image| types.freeImageAttachment(self.alloc, image); self.pending_images.clearRetainingCapacity(); }};
fn routingOpenUrl( _: ?*anyopaque, _: std.mem.Allocator, _: []const u8,) host.UrlOpenError!bool { return false;}
fn currentExecutablePathForRouting( _: ?*anyopaque, executable_buf: []u8,) upgrade_helpers.ExecutablePathError![]const u8 { const executable_path = "/tmp/fx-routing-upgraded"; if (executable_path.len > executable_buf.len) return error.PathTooLong; @memcpy(executable_buf[0..executable_path.len], executable_path); return executable_buf[0..executable_path.len];}
fn readTraceFileForTest(alloc: std.mem.Allocator, path: []const u8) ![]u8 { var file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}); defer file.close(io_mod.getIo()); return io_mod.readFileToEnd(alloc, &file, 8192);}
fn activateFullTranscriptForRoutingTest(app: *RoutingFakeApp) void { app.terminal.alternate_screen_owner = .full_transcript; app.terminal.alternate_mouse_tracking_active = false; app.shell.full_transcript = .{ .depth = .full, .follow_tail = true };}
fn appendRoutingSessionPickerSummary( alloc: std.mem.Allocator, picker: *app_session_runtime.SessionPicker, index: usize,) !void { const id = try std.fmt.allocPrint(alloc, "session-{d}", .{index}); errdefer alloc.free(id); const workspace_root = try std.fmt.allocPrint(alloc, "/tmp/workspace-{d}", .{index}); errdefer alloc.free(workspace_root); const title = try std.fmt.allocPrint(alloc, "session {d}", .{index}); errdefer alloc.free(title); const preview = try std.fmt.allocPrint(alloc, "session {d} preview", .{index}); errdefer alloc.free(preview); try picker.summaries.append(alloc, .{ .id = id, .workspace_root = workspace_root, .title = title, .preview = preview, .display_metadata_present = true, .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, });}
test "app_input_runtime slash completion movement works while stream is active" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
const count = command_specs.slashCompletionCount(app.slashRegistry(), app.input_runtime.edit_state.input.items); try std.testing.expect(count > 1); try std.testing.expect(app.stream.active);
try std.testing.expect(Runtime(FakeApprovalCancelApp).routeSlashCompletionMove(&app, 1));
try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.slash_completion_index); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor);
try std.testing.expect(Runtime(FakeApprovalCancelApp).routeSlashCompletionMove(&app, -1));
try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.slash_completion_index); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor);
app.input_runtime.picker.dismissInlinePicker(.slash); try std.testing.expect(!Runtime(FakeApprovalCancelApp).routeSlashCompletionMove(&app, 1)); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.slash_completion_index);}
test "app_input_runtime slash completion window moves up before reverse scrolling" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
var down: usize = 0; while (down < 6) : (down += 1) { try std.testing.expect(Runtime(FakeApprovalCancelApp).routeSlashCompletionMove(&app, 1)); } try std.testing.expectEqual(@as(usize, 6), app.input_runtime.picker.slash_completion_index); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.slash_completion_window_start);
try std.testing.expect(Runtime(FakeApprovalCancelApp).routeSlashCompletionMove(&app, -1)); try std.testing.expectEqual(@as(usize, 5), app.input_runtime.picker.slash_completion_index); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.slash_completion_window_start);}
test "app_input_runtime one-row session picker keeps reverse scrolling sticky" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.layout.rows = 10; const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; for (0..8) |index| try appendRoutingSessionPickerSummary(alloc, picker, index);
var down: usize = 0; while (down < 5) : (down += 1) { try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); } try std.testing.expectEqual(@as(usize, 5), picker.selected); try std.testing.expectEqual(@as(usize, 5), picker.window_start);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .up, -1); try std.testing.expectEqual(@as(usize, 4), picker.selected); try std.testing.expectEqual(@as(usize, 4), picker.window_start);}
test "app_input_runtime roomy session picker scrolls after twenty inline rows" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.layout.rows = 32; app.shell.layout.content_bottom = 28; app.shell.layout.divider_top_row = 29; app.shell.layout.input_row = 30; app.shell.layout.divider_bottom_row = 31; app.shell.layout.hint_row = 32;
const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; for (0..25) |index| try appendRoutingSessionPickerSummary(alloc, picker, index);
var down: usize = 0; while (down < 20) : (down += 1) { try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); }
try std.testing.expectEqual(@as(usize, 20), picker.selected); try std.testing.expectEqual(@as(usize, 1), picker.window_start);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .up, -1); try std.testing.expectEqual(@as(usize, 19), picker.selected); try std.testing.expectEqual(@as(usize, 1), picker.window_start);}
test "app_input_runtime help menu navigation skips headings and Enter executes selection" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.input_runtime.help_menu.open();
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.help_menu.selected_index);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', RoutingFakeApp.input_byte_limit, 100);
try std.testing.expect(!app.input_runtime.help_menu.active); try std.testing.expectEqualStrings("/clear", app.last_command.?); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime help menu inserts argument commands with a trailing space" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "additional directories"); app.input_runtime.help_menu.open();
try Runtime(RoutingFakeApp).handleByte(&app, '\r', RoutingFakeApp.input_byte_limit, 100);
try std.testing.expect(!app.input_runtime.help_menu.active); try std.testing.expectEqualStrings("/workspace ", app.input_runtime.edit_state.input.items); try std.testing.expect(app.last_command == null);}
test "app_input_runtime help menu submits an exact slash command on first Enter" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/clear"); app.input_runtime.help_menu.open();
try Runtime(RoutingFakeApp).handleByte( &app, '\r', RoutingFakeApp.input_byte_limit, 100, );
try std.testing.expect(!app.input_runtime.help_menu.active); try std.testing.expectEqualStrings("/clear", app.last_command.?); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime Escape closes help and clears its composer search" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "model"); app.input_runtime.help_menu.open();
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.input_runtime.help_menu.active); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime session picker uses the expanded responsive window before scrolling" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.layout.rows = 28; app.shell.layout.content_bottom = 24; app.shell.layout.divider_top_row = 25; app.shell.layout.input_row = 26; app.shell.layout.divider_bottom_row = 27; app.shell.layout.hint_row = 28;
const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; for (0..10) |index| try appendRoutingSessionPickerSummary(alloc, picker, index);
var down: usize = 0; while (down < 6) : (down += 1) { try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); }
try std.testing.expectEqual(@as(usize, 6), picker.selected); try std.testing.expectEqual(@as(usize, 0), picker.window_start);}
test "app_input_runtime session picker navigation keeps the inline window stable" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.layout.rows = 28; app.shell.layout.content_bottom = 24; app.shell.layout.divider_top_row = 25; app.shell.layout.input_row = 26; app.shell.layout.divider_bottom_row = 27; app.shell.layout.hint_row = 28;
const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; var index: usize = 0; while (index < 10) : (index += 1) { try appendRoutingSessionPickerSummary(alloc, picker, index); }
var down: usize = 0; while (down < 4) : (down += 1) { try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); }
try std.testing.expectEqual(@as(usize, 4), picker.selected); try std.testing.expectEqual(@as(usize, 0), picker.window_start);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1);
try std.testing.expectEqual(@as(usize, 5), picker.selected); try std.testing.expectEqual(@as(usize, 0), picker.window_start);}
test "app_input_runtime routes auth picker navigation before composer history" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initMany(&.{ .ai_gateway_api_key, .fx_login }); app.auth.openPicker(alloc);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1);
try std.testing.expect((auth_runtime.Choice{ .action = .switch_provider }).eql(app.auth.pickerView().selected_choice.?)); try std.testing.expectEqual(@as(?usize, null), app.input_runtime.composer_history.activeIndex());}
test "app_input_runtime Tab cycles the active auth picker" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initMany(&.{ .ai_gateway_api_key, .fx_login }); app.auth.openPicker(alloc);
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expect((auth_runtime.Choice{ .action = .switch_provider }).eql(app.auth.pickerView().selected_choice.?));}
test "app_input_runtime Tab leaves a dismissed slash query unchanged" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/he"); app.input_runtime.picker.dismissInlinePicker(.slash);
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqualStrings("/he", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash));}
test "app_input_runtime workspace add handoff dismisses argument completion" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.input_runtime.workspace_menu.open();
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.input_runtime.workspace_menu.active); try std.testing.expectEqualStrings( "/workspace add ", app.input_runtime.edit_state.input.items, ); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash));}
test "help menu whole replacement preserves the prior draft when staging fails" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const alloc = failing.allocator(); var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace( alloc, "additional directories", ); app.input_runtime.help_menu.open(); failing.fail_index = failing.alloc_index;
try std.testing.expectError( error.OutOfMemory, Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100), );
try std.testing.expect(app.input_runtime.help_menu.active); try std.testing.expectEqualStrings( "additional directories", app.input_runtime.edit_state.input.items, );}
test "workspace menu whole replacement preserves the prior draft when staging fails" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const alloc = failing.allocator(); var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "existing draft"); app.input_runtime.workspace_menu.open(); // The command writer owns the first allocation; fail replacement staging. failing.fail_index = failing.alloc_index + 1;
try std.testing.expectError( error.OutOfMemory, Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100), );
try std.testing.expect(app.input_runtime.workspace_menu.active); try std.testing.expectEqualStrings( "existing draft", app.input_runtime.edit_state.input.items, );}
test "app_input_runtime auth picker enter closes before selecting a switched source" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initMany(&.{ .ai_gateway_api_key, .fx_login }); app.auth.openPicker(alloc); app.auth.openSwitchCredentialPicker(alloc); _ = app.auth.movePicker(1);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.auth.pickerView().active); try std.testing.expectEqual(types.CredentialSource.fx_login, app.selected_credential_source.?);}
test "app_input_runtime connections picker delegates typed acquisition actions" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initMany(&.{ .ai_gateway_api_key, .fx_login }); app.auth.openPicker(alloc);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(auth_runtime.PickerStage.connections, app.auth.pickerView().stage);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.auth.pickerView().active); try std.testing.expectEqual(auth_runtime.AcquisitionAction.login, app.selected_auth_action.?);}
test "app_input_runtime Escape closes auth picker without arming composer clear" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initOne(.ai_gateway_api_key); app.auth.openPicker(alloc); app.prompt_retry_after_auth = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.auth.pickerView().active); try std.testing.expect(!app.prompt_retry_after_auth); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());}
test "app_input_runtime onboarding Escape skips setup for the session" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.openOnboardingPicker(alloc);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.auth.pickerView().active); try std.testing.expect(app.auth.view().onboarding_skipped); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());}
test "app_input_runtime auth stage Escape pops before closing the picker" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initOne(.stored_key); app.auth.openPicker(alloc);
_ = app.auth.movePicker(-1); try std.testing.expect((auth_runtime.Choice{ .action = .switch_credential }).eql( app.auth.pickerView().selected_choice.?, ));
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(auth_runtime.PickerStage.switch_credential, app.auth.pickerView().stage); try std.testing.expect(app.auth.pickerView().active);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1); try std.testing.expectEqual(auth_runtime.PickerStage.root, app.auth.pickerView().stage); try std.testing.expect(app.auth.pickerView().active); try std.testing.expect((auth_runtime.Choice{ .action = .switch_credential }).eql( app.auth.pickerView().selected_choice.?, ));
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 2); try std.testing.expect(!app.auth.pickerView().active); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len);}
test "app_input_runtime navigation bypasses disabled change team action" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initOne(.stored_key); app.auth.openPicker(alloc);
for (0..5) |_| _ = app.auth.movePicker(1); try std.testing.expect((auth_runtime.Choice{ .action = .switch_credential }).eql( app.auth.pickerView().selected_choice.?, ));
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.auth.pickerView().active); try std.testing.expectEqual(auth_runtime.PickerStage.switch_credential, app.auth.pickerView().stage); try std.testing.expect(app.selected_auth_action == null); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len);}
test "app_input_runtime Escape dismisses visible slash completions without arming composer clear" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/"); const cursor_before = app.input_runtime.edit_state.cursor;
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expectEqualStrings("/", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(cursor_before, app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash)); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).resolveEscape(&app, false, 2);
try std.testing.expectEqualStrings("/", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(cursor_before, app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash)); try std.testing.expectEqual( @as(?i64, 2), app.input_runtime.gestures.escapeClearArmedAt(), ); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime Escape leaves unmatched slash input with the composer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/hezzzzz");
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expectEqualStrings("/hezzzzz", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.input_runtime.picker.isInlinePickerDismissed(.slash)); try std.testing.expectEqual(@as(?i64, 1), app.input_runtime.gestures.escapeClearArmedAt()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime Escape dismisses slash matches with wrapped input" { const alloc = std.testing.allocator;
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.layout.cols = 8; app.shell.layout.content_bottom = 10; try app.input_runtime.textReplacementState().replace(alloc, " /mo"); try std.testing.expect(command_specs.slashCompletionCount(app.slashRegistry(), "/mo") > 0);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 11);
try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash)); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());}
test "app_input_runtime Escape dismisses model and file pickers through one contract" { const alloc = std.testing.allocator; const cases = [_]struct { input: []const u8, kind: picker_state.InlinePickerKind, }{ .{ .input = "/model ", .kind = .model }, .{ .input = "@not-present", .kind = .file }, };
for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, case.input); const cursor_before = app.input_runtime.edit_state.cursor;
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 12);
try std.testing.expectEqualStrings(case.input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(cursor_before, app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(case.kind)); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());
try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(case.kind)); }}
test "app_input_runtime Escape dismisses an idle inline skill completion" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "explain $man"); try std.testing.expect(input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleInlineSkillCompletion(&app) != null);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expectEqualStrings("explain $man", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.skill)); try std.testing.expect(input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleInlineSkillCompletion(&app) == null); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());}
test "app_input_runtime Escape dismisses an idle inline slash completion" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "explain /he"); const completion_rt = input_completion_runtime.CompletionRuntime(RoutingFakeApp); try std.testing.expect(completion_rt.visibleInlineSlashCompletion(&app) != null);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expectEqualStrings("explain /he", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash)); try std.testing.expect(completion_rt.visibleInlineSlashCompletion(&app) == null); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());}
test "app_input_runtime double Escape interrupts an active operation with arm, expiry, and disarm" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
// First press arms the gesture without cancelling. try Runtime(RoutingFakeApp).resolveEscape(&app, true, 100); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active);
// A press outside the window re-arms instead of cancelling. Runtime(RoutingFakeApp).expireTerminalInputGestures( &app, 101 + gesture_state.escape_interrupt_window_ms, ); try std.testing.expect(!app.input_runtime.gestures.escapeInterruptArmed()); try Runtime(RoutingFakeApp).resolveEscape(&app, true, 102 + gesture_state.escape_interrupt_window_ms); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.worker.cancel_requested);
// Non-escape input disarms the pending interrupt. try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 103 + gesture_state.escape_interrupt_window_ms); try std.testing.expect(!app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.worker.cancel_requested);
// Two presses inside the window cancel the active operation. try Runtime(RoutingFakeApp).resolveEscape(&app, true, 200); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed()); try Runtime(RoutingFakeApp).resolveEscape(&app, true, 201); try std.testing.expect(!app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(!app.stream.active);}
test "app_input_runtime semantic action stands down an armed Escape interrupt" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 100); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed());
try feedRoutingBytes(&app, "\x1b[A");
try std.testing.expect(!app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active);}
test "app_input_runtime armed interrupt disarms when the active operation settles" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 100); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed());
app.stream.active = false; Runtime(RoutingFakeApp).expireTerminalInputGestures(&app, 101); try std.testing.expect(!app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.worker.cancel_requested);}
test "app_input_runtime active operation Escape keeps precedence over inline skill dismissal" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.stream.active = true; try app.input_runtime.textReplacementState().replace(alloc, "explain $man"); try std.testing.expect(input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleInlineSkillCompletion(&app) != null);
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 1);
try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.input_runtime.picker.isInlinePickerDismissed(.skill));
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 2);
try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(!app.stream.active); try std.testing.expect(!app.input_runtime.gestures.escapeInterruptArmed()); try std.testing.expect(!app.input_runtime.picker.isInlinePickerDismissed(.skill));}
test "app_input_runtime composer editing dismisses auth picker before inserting" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.auth.source_inventory = auth_runtime.SourceSet.initOne(.ai_gateway_api_key); app.auth.openPicker(alloc); app.prompt_retry_after_auth = true;
try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100);
try std.testing.expect(!app.auth.pickerView().active); try std.testing.expect(!app.prompt_retry_after_auth); try std.testing.expectEqualStrings("x", app.input_runtime.edit_state.input.items);}
test "app_input_runtime skills menu navigation clamps before prompt history" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{ .{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx }, .{ .name = "workspace", .description = "", .path = "/tmp/workspace/SKILL.md", .source = .workspace_shared }, }; app.skills.items = @constCast(&skills); app.skills.openMenu();
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 1), app.skills.menu.selected_index);
// At the last skill, down clamps (stays on the last) and does not fall // through to prompt history. try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 1), app.skills.menu.selected_index);}
test "app_input_runtime at path dollars do not enter typed or pasted skill menus" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "review", .description = "", .path = "/tmp/review/SKILL.md", .source = .workspace_shared }}; for ([_][]const u8{ "@./", "@\"./space ", "@\"./escaped\\" }) |prefix| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, prefix); try Runtime(RoutingFakeApp).handleByte(&app, '$', 4096, 100); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqual(prefix.len + 1, app.input_runtime.edit_state.input.items.len); } for ([_][]const u8{ "@./$review", "@\"./$review\"", "@\"./a\\\"$review\"" }) |text| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try input_paste_runtime.PasteEditRuntime(RoutingFakeApp).handlePastedBytes(&app, text, 4096); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings(text, app.input_runtime.edit_state.input.items); } var control = try RoutingFakeApp.init(alloc); defer control.deinit(); control.skills.items = @constCast(&skills); try input_paste_runtime.PasteEditRuntime(RoutingFakeApp).handlePastedBytes(&control, "$review", 4096); try std.testing.expect(control.skills.menu.active);}
test "app_input_runtime skills menu navigation remains interactive while streaming" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{ .{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx }, .{ .name = "workspace", .description = "", .path = "/tmp/workspace/SKILL.md", .source = .workspace_shared }, }; app.skills.items = @constCast(&skills); app.skills.openMenu(); app.stream.active = true;
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1);
try std.testing.expectEqual(@as(usize, 1), app.skills.menu.selected_index);}
test "app_input_runtime command skills navigation uses the inline composer window" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{ .{ .name = "one", .description = "", .path = "/tmp/one/SKILL.md", .source = .global_fx }, .{ .name = "two", .description = "", .path = "/tmp/two/SKILL.md", .source = .global_fx }, .{ .name = "three", .description = "", .path = "/tmp/three/SKILL.md", .source = .global_fx }, .{ .name = "four", .description = "", .path = "/tmp/four/SKILL.md", .source = .global_fx }, }; app.shell.layout = .{ .rows = 16, .cols = 80, .content_bottom = 12, .divider_top_row = 13, .input_row = 14, .divider_bottom_row = 15, .hint_row = 16, }; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "one\ntwo\nthree\nfour\nfive\nsix\nseven"); app.skills.openMenuWithQuery(.command, null, "");
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1);
try std.testing.expectEqual(@as(usize, 1), app.skills.menu.selected_index); try std.testing.expectEqual(@as(usize, 1), app.skills.menu.window_start);}
test "app_input_runtime Escape closes an idle skills menu before empty-composer handling" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu();
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.skills.menu.active); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.worker.cancel_requested);}
test "api key entry bypasses composer paste and zeroes on cancellation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const sentinel = "FX_API_KEY_HISTORY_SENTINEL"; app.auth.openApiKeyPicker(alloc);
try feedRoutingBytes(&app, "\x1b[200~"); try feedRoutingBytes(&app, sentinel); try feedRoutingBytes(&app, "\x1b[201~");
try std.testing.expect(app.auth.apiKeyEntryActive()); try std.testing.expectEqual(sentinel.len, app.auth.pickerView().api_key_mask_count); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.composer_history.count());
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1); try std.testing.expect(!app.auth.apiKeyEntryActive()); try std.testing.expectEqual(@as(usize, 0), app.auth.pickerView().api_key_mask_count); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime command skills menu reuses composer input as its query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu();
try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100);
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("x", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("x", app.skills.menu.query());}
test "app_input_runtime command skills search owns dollar and model-shaped text" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "model-helper", .description = "", .path = "/tmp/model-helper/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu();
try feedRoutingBytes(&app, "/model $");
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuOrigin.command, app.skills.menu.origin); try std.testing.expect(app.skills.menu.target == null); try std.testing.expectEqualStrings("/model $", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("/model $", app.skills.menu.query());}
test "app_input_runtime command skills menu keeps its query on ctrl+p and ctrl-c clear" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "older", .description = "", .path = "/tmp/older/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.composer_history.installTextEntries(alloc, &.{"older"}); app.skills.openMenu();
// Ctrl+P owns the model picker now; while the skills menu owns the // composer it is a no-op instead of a history recall. try feedRoutingBytes(&app, "\x10"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("", app.skills.menu.query()); try std.testing.expect(app.input_runtime.model_picker_draft == null);
try feedRoutingBytes(&app, "\x03"); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("", app.skills.menu.query());}
test "app_input_runtime Escape closes a command skills menu and clears its temporary query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "man"); app.skills.openMenuWithQuery(.command, null, "man");
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor);}
test "app_input_runtime Space edits and filters a command skills query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "managed description", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu();
try Runtime(RoutingFakeApp).handleByte(&app, ' ', 4096, 100);
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings(" ", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings(" ", app.skills.menu.query());}
test "app_input_runtime command skills search owns Space in model-shaped queries" { const model = "anthropic/claude-sonnet-4.6"; const completions = [_][]const u8{model}; const cases = [_]struct { query: []const u8, exact: bool, }{ .{ .query = "/model nonsense", .exact = false }, .{ .query = "/model " ++ model, .exact = true }, };
for (cases) |case| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_completion_values = &completions; app.skills.openMenu(); try feedRoutingBytes(&app, case.query);
const model_query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return error.TestExpectedEqual; try std.testing.expectEqual(case.exact, std.ascii.eqlIgnoreCase(model_query.query, model));
try Runtime(RoutingFakeApp).handleByte(&app, ' ', 4096, 100);
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(case.query.len + 1, app.input_runtime.edit_state.input.items.len); try std.testing.expectEqualStrings(case.query, app.input_runtime.edit_state.input.items[0..case.query.len]); try std.testing.expectEqual(@as(u8, ' '), app.input_runtime.edit_state.input.items[case.query.len]); try std.testing.expectEqualStrings(app.input_runtime.edit_state.input.items, app.skills.menu.query()); try std.testing.expect(!app.input_runtime.picker.hasPendingModelPickerSelection()); }}
test "app_input_runtime Tab cycles skills menu sources before autocomplete" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{ .{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx }, .{ .name = "codex", .description = "", .path = "/tmp/codex/SKILL.md", .source = .global_codex }, }; app.skills.items = @constCast(&skills); app.skills.openMenu(); try app.input_runtime.textReplacementState().replace(alloc, "/sk");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuSourceFilter.fx, app.skills.menu.source_filter); try std.testing.expectEqualStrings("/sk", app.input_runtime.edit_state.input.items);}
test "app_input_runtime Tab cycles skills menu sources while streaming" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{ .{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx }, .{ .name = "codex", .description = "", .path = "/tmp/codex/SKILL.md", .source = .global_codex }, }; app.skills.items = @constCast(&skills); app.skills.openMenu(); app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqual(skill_runtime.SkillMenuSourceFilter.fx, app.skills.menu.source_filter);}
test "app_input_runtime Tab advances settings categories before autocomplete" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.input_runtime.settings_menu.open(); app.input_runtime.settings_menu.selected_index = 2; app.input_runtime.settings_menu.window_start = 1;
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqual(@import("../config/settings_catalog.zig").Category.interface, app.input_runtime.settings_menu.category); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.settings_menu.selected_index); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.settings_menu.window_start); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.input_runtime.settings_menu.selected_index = 2; app.input_runtime.settings_menu.window_start = 1; try feedRoutingBytes(&app, "\x1b[Z"); try std.testing.expectEqual(@import("../config/settings_catalog.zig").Category.all, app.input_runtime.settings_menu.category); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.settings_menu.selected_index); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.settings_menu.window_start);}
test "app_input_runtime Tab advances help categories before autocomplete" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.input_runtime.help_menu.open(); app.input_runtime.help_menu.selected_index = 2; app.input_runtime.help_menu.window_start = 1;
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqual(@as(?command_specs.SlashPresentationCategory, .general), app.input_runtime.help_menu.category); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.help_menu.selected_index); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.help_menu.window_start); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.input_runtime.help_menu.selected_index = 2; app.input_runtime.help_menu.window_start = 1; try feedRoutingBytes(&app, "\x1b[Z"); try std.testing.expect(app.input_runtime.help_menu.category == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.help_menu.selected_index); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.help_menu.window_start);}
test "app_input_runtime Tab cycles usage scopes in both directions" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.usage_menu.openError( alloc, .days_30, "usage is unavailable", );
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expectEqual(usage_report.Scope.days_7, app.input_runtime.usage_menu.navigationScope());
try feedRoutingBytes(&app, "\x1b[Z"); try std.testing.expectEqual(usage_report.Scope.days_30, app.input_runtime.usage_menu.navigationScope());}
test "app_input_runtime Tab toggles session picker scope before autocomplete" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace);
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.workspace_root = workspace; app.session_persistence.store = try session_store.Store.initFromHome(alloc, home, workspace); app.session_persistence.session_picker.active = true; app.session_persistence.session_picker.scope = .current_workspace; try app.input_runtime.textReplacementState().replace(alloc, "/sk");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expect(app.session_persistence.session_picker.active); try std.testing.expectEqual(app_session_runtime.SessionPickerScope.all_workspaces, app.session_persistence.session_picker.scope); try std.testing.expectEqualStrings("/sk", app.input_runtime.edit_state.input.items);}
test "app_input_runtime Shift+Tab cycles permission mode and queued prompts" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try std.testing.expectEqual(types.PermissionMode.ask, app.permission_engine.mode); app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '[', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'Z', 4096, 100);
try std.testing.expectEqual(types.PermissionMode.auto, app.permission_engine.mode); try std.testing.expectEqual(@as(?types.PermissionMode, .auto), app.worker.synced_permission_mode); try std.testing.expectEqual(@as(usize, 1), app.worker.permission_mode_sync_count); try std.testing.expectEqual(@as(?types.PermissionMode, .auto), app.last_preference_permission_mode); try std.testing.expectEqual(@as(usize, 1), app.permission_mode_preference_commit_count); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '[', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'Z', 4096, 100);
try std.testing.expectEqual(types.PermissionMode.yolo, app.permission_engine.mode); try std.testing.expectEqual(@as(?types.PermissionMode, .yolo), app.worker.synced_permission_mode); try std.testing.expectEqual(@as(usize, 2), app.worker.permission_mode_sync_count); try std.testing.expectEqual(@as(?types.PermissionMode, .yolo), app.last_preference_permission_mode); try std.testing.expectEqual(@as(usize, 2), app.permission_mode_preference_commit_count); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '[', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'Z', 4096, 100);
try std.testing.expectEqual(types.PermissionMode.ask, app.permission_engine.mode); try std.testing.expectEqual(@as(?types.PermissionMode, .ask), app.worker.synced_permission_mode); try std.testing.expectEqual(@as(usize, 3), app.worker.permission_mode_sync_count); try std.testing.expectEqual(@as(?types.PermissionMode, .ask), app.last_preference_permission_mode); try std.testing.expectEqual(@as(usize, 3), app.permission_mode_preference_commit_count); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime Shift+Tab reverses skills source without changing permission mode" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{ .{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx }, .{ .name = "claw", .description = "", .path = "/tmp/claw/SKILL.md", .source = .global_claw }, }; app.skills.items = @constCast(&skills); app.skills.openMenu();
try feedRoutingBytes(&app, "\x1b[Z");
try std.testing.expectEqual(skill_runtime.SkillMenuSourceFilter.claw, app.skills.menu.source_filter); try std.testing.expectEqual(types.PermissionMode.ask, app.permission_engine.mode); try std.testing.expectEqual(@as(usize, 0), app.worker.permission_mode_sync_count);}
test "app_input_runtime routes horizontal arrows to inline settings" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "permission"); app.input_runtime.settings_menu.open(); app.input_runtime.settings_menu.category = .agent; const cursor_before = app.input_runtime.edit_state.cursor;
try feedRoutingBytes(&app, "\x1b[D");
try std.testing.expect(app.input_runtime.settings_menu.active); try std.testing.expectEqual(cursor_before, app.input_runtime.edit_state.cursor); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime Enter stays inside the inline settings list" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.input_runtime.settings_menu.open();
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.input_runtime.settings_menu.active); try std.testing.expectEqual(types.PermissionMode.ask, app.permission_engine.mode); try std.testing.expectEqual(@as(usize, 0), app.worker.permission_mode_sync_count);}
test "app_input_runtime Space edits the session catalog query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.session_persistence.session_picker.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, ' ', 4096, 100);
try std.testing.expect(app.session_persistence.session_picker.active); try std.testing.expectEqualStrings(" ", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings(" ", app.session_persistence.session_picker.query());
try Runtime(RoutingFakeApp).handleByte(&app, ' ', 4096, 100);
try std.testing.expect(app.session_persistence.session_picker.active); try std.testing.expectEqualStrings(" ", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings(" ", app.session_persistence.session_picker.query());}
test "app_input_runtime Enter binds a selected skill token" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "review", .description = "", .path = "/tmp/review/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu();
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("$review ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, "$review ".len), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("review", app.input_runtime.entities.skill_tokens.items[0].name); try std.testing.expectEqualStrings("/tmp/review/SKILL.md", app.input_runtime.entities.skill_tokens.items[0].path); try std.testing.expectEqual(@as(usize, "$review".len), app.input_runtime.entities.skill_tokens.items[0].raw_end); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(app.last_command == null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Unknown command") == null);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expect(app.last_command == null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Unknown command") == null);}
test "app_input_runtime Enter reports a skill binding rejected by the input limit" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "review", .description = "", .path = "/tmp/review/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "$r suffix"); app.input_runtime.edit_state.cursor = "$r".len; app.skills.openMenuWithQuery(.dollar, .{ .start = 0, .end = "$r".len }, "r");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', app.input_runtime.edit_state.input.items.len, 100);
try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("$r suffix", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings( "That edit exceeds the local prompt safety limit and was not applied.", app.notice_body.items, );}
test "app_input_runtime redraws a closed skill picker when the input limit notice is coalesced" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "review", .description = "", .path = "/tmp/review/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "$r suffix"); app.input_runtime.edit_state.cursor = "$r".len; app.skills.openMenuWithQuery(.dollar, .{ .start = 0, .end = "$r".len }, "r"); const limit_transition = input_limit_rejection.begin( app.input_runtime.input_limit_rejection, .composer, ); app.input_runtime.input_limit_rejection = limit_transition.next; try std.testing.expect(limit_transition.should_report); app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', app.input_runtime.edit_state.input.items.len, 100);
try std.testing.expect(!app.skills.menu.active); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqualStrings("$r suffix", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);}
test "app_input_runtime keeps the skill picker open when binding allocation fails" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const alloc = failing.allocator(); var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "review", .description = "", .path = "/tmp/review/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu(); failing.fail_index = failing.alloc_index;
try std.testing.expectError( error.OutOfMemory, Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100), );
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuOrigin.command, app.skills.menu.origin); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);}
test "app_input_runtime Enter replaces the full command skills query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "man"); app.skills.openMenuWithQuery(.command, null, "man");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("$managed ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("/tmp/managed/SKILL.md", app.input_runtime.entities.skill_tokens.items[0].path);}
test "app_input_runtime command skills query tracks bracketed paste" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu();
try feedRoutingBytes(&app, "\x1b[200~man\x1b[201~");
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("man", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("man", app.skills.menu.query()); try std.testing.expectEqualStrings("managed", app.skills.selectedMenuSkill().?.name);}
test "app_input_runtime dollar opens skills menu and Escape preserves raw text" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "scale", .description = "", .path = "/tmp/scale/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try Runtime(RoutingFakeApp).handleByte(&app, '$', 4096, 100); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuOrigin.dollar, app.skills.menu.origin); try std.testing.expectEqualStrings("", app.skills.menu.query()); try std.testing.expectEqualStrings("$", app.input_runtime.edit_state.input.items);
try Runtime(RoutingFakeApp).handleByte(&app, 's', 4096, 101); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("s", app.skills.menu.query()); try std.testing.expectEqual(@as(usize, 0), app.skills.menu.target.?.start); try std.testing.expectEqual(@as(usize, 2), app.skills.menu.target.?.end);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 102); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("$s", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.skill));
try Runtime(RoutingFakeApp).handleByte(&app, 'c', 4096, 103); try std.testing.expect(!app.skills.menu.active); try std.testing.expect(input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleInlineSkillCompletion(&app) == null);
try Runtime(RoutingFakeApp).handleByte(&app, '$', 4096, 104); try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.input_runtime.picker.isInlinePickerDismissed(.skill)); try std.testing.expectEqual(@as(usize, "$sc".len), app.skills.menu.target.?.start);}
test "app_input_runtime typed dollar opens an anchored skills menu at every composer position" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "scale", .description = "", .path = "/tmp/scale/SKILL.md", .source = .global_fx, }}; const inputs = [_][]const u8{ " $", "hello $", "price$" };
for (inputs) |input| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, input);
try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuOrigin.dollar, app.skills.menu.origin); try std.testing.expectEqual(input.len - 1, app.skills.menu.target.?.start); try std.testing.expectEqual(input.len, app.skills.menu.target.?.end); try std.testing.expectEqualStrings("", app.skills.menu.query()); }}
test "app_input_runtime Tab and Right Arrow accept visible inline skill completions" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }};
for ([_]enum { tab, right }{ .tab, .right }) |key| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "explain $man");
const completion = input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleInlineSkillCompletion(&app).?; try std.testing.expectEqualStrings("aged-menu", completion.suffix); try std.testing.expectEqualStrings("explain $man", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);
switch (key) { .tab => try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100), .right => try Runtime(RoutingFakeApp).routeComposerShortcutAction( &app, .{ .move = .{ .kind = .character_right } }, 4096, ), }
try std.testing.expectEqualStrings("explain $managed-menu ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("managed-menu", app.input_runtime.entities.skill_tokens.items[0].name); try std.testing.expectEqualStrings("/tmp/managed-menu/SKILL.md", app.input_runtime.entities.skill_tokens.items[0].path); try std.testing.expectEqual("explain ".len, app.input_runtime.entities.skill_tokens.items[0].raw_start); }}
test "app_input_runtime Tab and Right Arrow accept visible inline slash completions" { const alloc = std.testing.allocator;
for ([_]enum { tab, right }{ .tab, .right }) |key| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try feedRoutingBytes(&app, "explain /he");
const completion_rt = input_completion_runtime.CompletionRuntime(RoutingFakeApp); try std.testing.expectEqualStrings( "lp", completion_rt.visibleInlineSlashCompletion(&app).?.suffix, ); try std.testing.expectEqualStrings("explain /he", app.input_runtime.edit_state.input.items);
switch (key) { .tab => try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100), .right => try Runtime(RoutingFakeApp).routeComposerShortcutAction( &app, .{ .move = .{ .kind = .character_right } }, 4096, ), }
try std.testing.expectEqualStrings("explain /help", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len); }}
test "app_input_runtime Right Arrow collapses selection before inline completion" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "explain $man"); const end = app.input_runtime.edit_state.input.items.len; _ = app.input_runtime.selectionState().begin(end - 1); _ = app.input_runtime.selectionState().extend(end);
try Runtime(RoutingFakeApp).routeComposerShortcutAction( &app, .{ .move = .{ .kind = .character_right } }, 4096, );
try std.testing.expectEqualStrings("explain $man", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.edit_state.selectionRange() == null); try std.testing.expectEqual(end, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);}
test "app_input_runtime ctrl-l preserves an active inline picker" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try feedRoutingBytes(&app, "$man"); try std.testing.expect(app.skills.menu.active);
try Runtime(RoutingFakeApp).handleByte(&app, 12, 4096, 100); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("$man", app.input_runtime.edit_state.input.items);
_ = try Runtime(RoutingFakeApp).routeResolvedEscapeAction( &app, .{ .composer_shortcut = .redraw }, .redraw, null, null, false, 4096, ); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("$man", app.input_runtime.edit_state.input.items);}
test "app_input_runtime Enter keeps backslash newline semantics when history allocation fails" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "draft\\"); var failing = std.testing.FailingAllocator.init( alloc, .{ .fail_index = 0 }, ); app.alloc = failing.allocator();
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); app.alloc = alloc;
try std.testing.expectEqualStrings("draft\n", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime Tab accepts a root slash completion after multiline whitespace" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "\n /he");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqualStrings("/help", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor);}
test "app_input_runtime inline skill acceptance preserves input on limit rejection" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "explain $man"); const original_len = app.input_runtime.edit_state.input.items.len;
try Runtime(RoutingFakeApp).handleByte(&app, '\t', original_len, 100);
try std.testing.expectEqualStrings("explain $man", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings( "That edit exceeds the local prompt safety limit and was not applied.", app.notice_body.items, );}
test "app_input_runtime no-match dollar text keeps spaces and submits raw" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "Explain echo $HOME"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.skills.menuVisible()); try std.testing.expectEqualStrings("Explain echo $HOME", app.input_runtime.edit_state.input.items);
try feedRoutingBytes(&app, " please"); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("Explain echo $HOME please", app.input_runtime.edit_state.input.items);
try feedRoutingBytes(&app, "\r"); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings( "Explain echo $HOME please", app.submitted_prompt[0..app.submitted_prompt_len], );}
test "app_input_runtime space after matched dollar token inserts and closes menu" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "$man"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(app.skills.selectedMenuSkill() != null);
try feedRoutingBytes(&app, " x"); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("$man x", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);
try feedRoutingBytes(&app, "\r"); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("$man x", app.submitted_prompt[0..app.submitted_prompt_len]);}
test "app_input_runtime matched dollar token still binds on enter" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "$man"); try feedRoutingBytes(&app, "\r");
try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("managed", app.input_runtime.entities.skill_tokens.items[0].name); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime enter on tab-filtered empty dollar menu submits raw" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_codex, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "$man"); try std.testing.expect(app.skills.selectedMenuSkill() != null);
// .all -> .fx: the codex-sourced skill vanishes, nothing is selectable. try feedRoutingBytes(&app, "\t"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(app.skills.selectedMenuSkill() == null);
try feedRoutingBytes(&app, "\r"); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("$man", app.submitted_prompt[0..app.submitted_prompt_len]);}
test "app_input_runtime enter submits after delete-forward removes the dollar anchor" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "$x"); try std.testing.expect(app.skills.menu.active);
// Delete-forward removes the $ and synchronizes the tracked menu // immediately, so Enter proceeds through normal prompt submission. app.input_runtime.edit_state.cursor = 0; try feedRoutingBytes(&app, "\x04"); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("x", app.input_runtime.edit_state.input.items);
try feedRoutingBytes(&app, "\r"); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("x", app.submitted_prompt[0..app.submitted_prompt_len]);}
test "app_input_runtime zero-match dollar query recovers matches on backspace" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "$mzz"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.skills.menuVisible()); try std.testing.expect(app.skills.selectedMenuSkill() == null);
try feedRoutingBytes(&app, "\x7f\x7f"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(app.skills.menuVisible()); try std.testing.expectEqualStrings("m", app.skills.menu.query()); try std.testing.expect(app.skills.selectedMenuSkill() != null);
try feedRoutingBytes(&app, "\r"); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("managed", app.input_runtime.entities.skill_tokens.items[0].name);}
test "app_input_runtime hidden zero-match dollar menu yields navigation to the composer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "managed", .description = "", .path = "/tmp/managed/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills);
try feedRoutingBytes(&app, "$missing"); try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.skills.menuVisible()); try std.testing.expectEqual("$missing".len, app.input_runtime.edit_state.cursor);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .up, 1);
try std.testing.expectEqualStrings("$missing", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.skills.menuVisible());}
test "app_input_runtime Escape cancels an idle session picker before empty-composer handling" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.session_persistence.session_picker.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.session_persistence.session_picker.active); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.worker.cancel_requested);}
test "app_input_runtime stream Escape retains cancellation precedence over session picker state" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 1); try Runtime(RoutingFakeApp).resolveEscape(&app, true, 2);
try std.testing.expect(!app.session_persistence.session_picker.active); try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(!app.stream.active);}
test "app_input_runtime stream Escape closes the skills menu without cancelling the active operation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.openMenu(); app.stream.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 1);
try std.testing.expect(!app.skills.menu.active); try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime stream Escape dismisses the visible slash picker before cancelling" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/he"); app.stream.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 1);
try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash)); try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime model catalog owns search navigation and provider tabs" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try openRoutingModelMenu(&app, &.{ "anthropic/one", "openai/two", "private/three" });
try Runtime(RoutingFakeApp).handleByte(&app, '$', 4096, 100); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings("$", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("$", app.model_cache.menu.query());
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 101); try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.model_cache.menu.query());
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 1), app.model_cache.menu.selected_index);
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 102); try std.testing.expectEqual(@as(usize, 1), app.model_cache.menu.provider_index); try std.testing.expectEqual(@as(usize, 0), app.model_cache.menu.selected_index); try std.testing.expectEqual(model_cache_runtime.ModelProviderFilter.anthropic, app.model_cache.menu.providerFilter()); try std.testing.expectEqual(@as(usize, 1), app.model_cache.menu.filteredItemCount());}
test "app_input_runtime model catalog selection reuses immediate model persistence" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try openRoutingModelMenu(&app, &.{ "alpha/one", "beta/two" });
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("alpha/one", app.selected_model.items); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings("alpha/one", app.last_preference_model.items); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "app_input_runtime model catalog selection preserves effort and fast picker stages" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const model = "anthropic/claude-opus-4.8"; const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("future-tier")}; app.setGatewayControls(model, &efforts, true); try openRoutingModelMenu(&app, &.{model});
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("test/model", app.selected_model.items); try std.testing.expectEqualStrings("/model " ++ model ++ " ", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.hasPendingModelPickerSelection()); try std.testing.expectEqual(picker_state.ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count);}
test "app_input_runtime stream Escape closes the model catalog without cancelling the active operation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try openRoutingModelMenu(&app, &.{"alpha/one"}); app.stream.active = true;
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 1);
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime unselectable model catalog states consume Enter without submitting" { const Case = enum { loading, failed, no_match }; for ([_]Case{ .loading, .failed, .no_match }) |case| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); switch (case) { .loading, .failed => { app.model_cache.menu.active = true; app.model_cache.menu.load_state = if (case == .loading) .loading else .failed; }, .no_match => { try openRoutingModelMenu(&app, &.{"alpha/one"}); app.model_cache.menu.setQuery("missing"); }, }
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(app.last_command == null); }}
test "app_input_runtime typing filters the active session catalog" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.session_persistence.session_picker.active = true;
try std.testing.expect(Runtime(RoutingFakeApp).isComposerEditingByte(22, null));
try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100);
try std.testing.expect(app.session_persistence.session_picker.active); try std.testing.expectEqualStrings("x", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("x", app.session_persistence.session_picker.query());}
test "app_input_runtime session catalog keeps Shift Enter out of the search query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.session_persistence.session_picker.active = true; try app.input_runtime.textReplacementState().replace(alloc, "session"); app.session_persistence.session_picker.setQuery(app.input_runtime.edit_state.input.items);
try Runtime(RoutingFakeApp).routeComposerShortcutAction(&app, .insert_newline, 4096);
try std.testing.expectEqualStrings("session", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("session", app.session_persistence.session_picker.query());}
test "app_input_runtime session picker navigation clamps before prompt history" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; for (0..2) |index| try appendRoutingSessionPickerSummary(alloc, picker, index);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 1), picker.selected);
// At the last session, down clamps (stays on the last) and does not fall // through to prompt history. try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 1), picker.selected);}
test "app_input_runtime Enter consumes an active session picker on target failure" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; try picker.summaries.append(alloc, .{ .id = try alloc.dupe(u8, "missing-session"), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 0, });
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(picker.active); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expectEqual(session_catalog.ResumeFailure.unavailable, picker.selection_failure.?); try std.testing.expect(std.mem.find(u8, app.transcript.items, "unable to resume") == null);}
test "app_input_runtime reports resume failure after picker transition" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; try picker.summaries.append(alloc, .{ .id = try alloc.dupe(u8, "broken-session"), .created_at_ms = 0, .updated_at_ms = 0, .conversation_language = session_runtime.ConversationLanguage.literal("en"), .history_len = 1, }); app.resume_selected_closes_picker = true; app.resume_selected_error = error.SessionCommitBoundaryUnavailable;
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!picker.active); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings("Unable to resume selected session.", app.notice_body.items); try std.testing.expect(std.mem.find(u8, app.transcript.items, "SessionCommitBoundaryUnavailable") == null);}
test "app_input_runtime Enter loads the next session page from the catalog action" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; picker.has_more = true; for (0..2) |index| try appendRoutingSessionPickerSummary(alloc, picker, index); picker.selected = picker.filteredItemCount();
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.load_more_session_count); try std.testing.expectEqual(@as(usize, 0), app.resume_selected_count); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(picker.active);}
test "app_input_runtime Enter consumes an empty session catalog result" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const picker = &app.session_persistence.session_picker; picker.active = true; picker.load_state = .ready; try appendRoutingSessionPickerSummary(alloc, picker, 0); picker.setQuery("missing");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.resume_selected_count); try std.testing.expect(picker.active); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime approval escape arrows move choices directly" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
app.approval_prompt.decision.choice_index = 1; try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .history_up, null); try std.testing.expectEqual(@as(u8, 0), app.approval_prompt.decision.choice_index); try std.testing.expectEqual(@as(usize, 0), app.approval_screen.document_scroll_rows); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); app.approval_prompt.decision.choice_index = 1; try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .cursor_left, null); try std.testing.expectEqual(@as(u8, 0), app.approval_prompt.decision.choice_index); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); app.approval_prompt.decision.choice_index = 1; try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .history_down, null); try std.testing.expectEqual(@as(u8, 2), app.approval_prompt.decision.choice_index); try std.testing.expectEqual(@as(usize, 0), app.approval_screen.document_scroll_rows); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); app.approval_prompt.decision.choice_index = 1; try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .cursor_right, null); try std.testing.expectEqual(@as(u8, 2), app.approval_prompt.decision.choice_index); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime approval amendment arrows edit the selected draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", })); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'a', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'b', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'c', 4096, 100);
try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .cursor_left, null); try std.testing.expectEqual(@as(usize, 3), app.approval_prompt.draftCursorForChoice(0)); try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .cursor_left, .cursor_left); try std.testing.expectEqual(@as(u8, 0), app.approval_prompt.decision.choice_index); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expectEqual(@as(usize, 2), app.approval_prompt.draftCursorForChoice(0));}
test "app_input_runtime routes approval amendment home end delete and word movement" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", })); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); for ("alpha beta") |byte| try Runtime(RoutingFakeApp).handleByte(&app, byte, 4096, 100);
try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .home, .cursor_home); try std.testing.expectEqual(@as(usize, 0), app.approval_prompt.draftCursorForChoice(0)); try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .word_right, .cursor_word_right); try std.testing.expectEqual(@as(usize, "alpha".len), app.approval_prompt.draftCursorForChoice(0)); try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .delete_next, .delete_next); try std.testing.expectEqualStrings("alphabeta", app.approval_prompt.decision.selectedDraft()); try Runtime(RoutingFakeApp).routeApprovalEscapeAction(&app, .end, .cursor_end); try std.testing.expectEqual(app.approval_prompt.decision.selectedDraft().len, app.approval_prompt.draftCursorForChoice(0));}
test "app_input_runtime routes focused editor aliases into approval amendment only" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "hidden composer"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", }));
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try feedRoutingBytes(&app, "alpha beta gamma");
try Runtime(RoutingFakeApp).handleByte(&app, 1, 4096, 100); try std.testing.expectEqual(@as(usize, 0), app.approval_prompt.draftCursorForChoice(0)); try Runtime(RoutingFakeApp).handleByte(&app, 5, 4096, 100); try std.testing.expectEqual(app.approval_prompt.decision.selectedDraft().len, app.approval_prompt.draftCursorForChoice(0)); try Runtime(RoutingFakeApp).handleByte(&app, 2, 4096, 100); try std.testing.expectEqual(app.approval_prompt.decision.selectedDraft().len - 1, app.approval_prompt.draftCursorForChoice(0)); try Runtime(RoutingFakeApp).handleByte(&app, 6, 4096, 100); try std.testing.expectEqual(app.approval_prompt.decision.selectedDraft().len, app.approval_prompt.draftCursorForChoice(0));
try Runtime(RoutingFakeApp).handleByte(&app, 23, 4096, 100); try std.testing.expectEqualStrings("alpha beta ", app.approval_prompt.decision.selectedDraft()); try Runtime(RoutingFakeApp).handleByte(&app, 21, 4096, 100); try std.testing.expectEqualStrings("", app.approval_prompt.decision.selectedDraft());
try feedRoutingBytes(&app, "alpha beta"); try Runtime(RoutingFakeApp).handleByte(&app, 1, 4096, 100); try feedRoutingBytes(&app, "\x1bd"); try std.testing.expectEqualStrings("beta", app.approval_prompt.decision.selectedDraft()); try Runtime(RoutingFakeApp).handleByte(&app, 5, 4096, 100); try feedRoutingBytes(&app, "\x1b\x7f"); try std.testing.expectEqualStrings("", app.approval_prompt.decision.selectedDraft());
try feedRoutingBytes(&app, "left right"); try Runtime(RoutingFakeApp).handleByte(&app, 1, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 6, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 11, 4096, 100); try std.testing.expectEqualStrings("l", app.approval_prompt.decision.selectedDraft()); try std.testing.expectEqualStrings("hidden composer", app.input_runtime.edit_state.input.items); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime approval wheel scrolls only a committed overflowing file document" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest( alloc, routingFileApprovalRequest(41), ));
app.approval_screen.recordScreenCommit(41, .{ .request_id = 41, .rows = 24, .cols = 80, .file_identity_visible = true, .all_decision_controls_visible = true, .changed_or_notice_visible = true, .document_scrollable = true, }); app.approval_screen.document_scroll_rows = 3; try Runtime(RoutingFakeApp).routeApprovalEscapeAction( &app, .{ .mouse_wheel = .up }, null, ); try std.testing.expectEqual(@as(usize, 6), app.approval_screen.document_scroll_rows); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); app.approval_screen.recordScreenCommit(41, .{ .request_id = 41, .rows = 24, .cols = 80, .file_identity_visible = true, .all_decision_controls_visible = true, .changed_or_notice_visible = true, .document_scrollable = false, }); try Runtime(RoutingFakeApp).routeApprovalEscapeAction( &app, .{ .mouse_wheel = .down }, null, ); try std.testing.expectEqual(@as(usize, 6), app.approval_screen.document_scroll_rows); try std.testing.expect(!app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime submits raw model command when picker has no selection" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "/model custom/provider-model\r");
try std.testing.expect(app.last_command != null); try std.testing.expectEqualStrings("/model custom/provider-model", app.last_command.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "explicit model selection parser accepts complete picker syntax" { const selection = switch (parseExplicitModelSelection( "/model anthropic/claude-opus-4.7 auto normal", )) { .selection => |selection| selection, else => return error.TestExpectedEqual, };
try std.testing.expectEqualStrings( "anthropic/claude-opus-4.7", selection.model, ); try std.testing.expectEqual(types.ReasoningEffort.auto, selection.effort); try std.testing.expect(!selection.fast_mode); try std.testing.expect(selection.has_fast_mode_token);}
test "explicit model selection semantic validation uses resolved capabilities" { const high_efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; const xhigh_efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("xhigh")}; const unsupported_fast = switch (parseExplicitModelSelection( "/model anthropic/claude-sonnet-4.6 auto normal", )) { .selection => |selection| selection, else => return error.TestExpectedEqual, }; try std.testing.expect(validateExplicitModelSelection( unsupported_fast, model_capabilities.capabilitiesForModel(unsupported_fast.model), ) == .invalid);
const gateway_reasoning_only = switch (parseExplicitModelSelection( "/model provider/new-reasoning-model high", )) { .selection => |selection| selection, else => return error.TestExpectedEqual, }; try std.testing.expect(validateExplicitModelSelection( gateway_reasoning_only, model_capabilities.resolveCapabilities(gateway_reasoning_only.model, .{ .reasoning_efforts = .fromSlice(&high_efforts) }), ) != .invalid);
const unsupported_portable = switch (parseExplicitModelSelection( "/model provider/new-reasoning-model max", )) { .selection => |selection| selection, else => return error.TestExpectedEqual, }; try std.testing.expect(validateExplicitModelSelection( unsupported_portable, model_capabilities.resolveCapabilities(unsupported_portable.model, .{ .reasoning_efforts = .fromSlice(&high_efforts) }), ) == .invalid);
const verified = switch (parseExplicitModelSelection( "/model anthropic/claude-opus-4.8 xhigh fast", )) { .selection => |selection| selection, else => return error.TestExpectedEqual, }; try std.testing.expect(validateExplicitModelSelection( verified, model_capabilities.resolveCapabilities(verified.model, .{ .reasoning_efforts = .fromSlice(&xhigh_efforts), .supports_fast_mode = true, }), ) != .invalid);}
test "model picker spaces do not commit before enter" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace( alloc, "/model anthropic/claude-sonnet-4.6 auto", ); try app.input_runtime.picker.beginModelPickerFlow( alloc, "anthropic/claude-sonnet-4.6", 0, false, .effort, );
try std.testing.expect( !try Runtime(RoutingFakeApp).advanceModelPickerOnSpace(&app), ); try std.testing.expectEqualStrings( "/model anthropic/claude-sonnet-4.6 auto", app.input_runtime.edit_state.input.items, );}
test "model picker enter commits selected fast option from fast stage" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls("anthropic/claude-opus-4.7", &efforts, true);
try app.input_runtime.textReplacementState().replace( alloc, "/model anthropic/claude-opus-4.7 auto ", ); try app.input_runtime.picker.beginModelPickerFlow( alloc, "anthropic/claude-opus-4.7", 0, false, .fast, );
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings( "anthropic/claude-opus-4.7", app.selected_model.items, ); try std.testing.expectEqualStrings( "anthropic/claude-opus-4.7", app.last_preference_model.items, ); try std.testing.expectEqual(types.ReasoningEffort.auto, app.effort); try std.testing.expectEqual(types.ReasoningEffort.auto, app.last_preference_effort.?); try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(false, app.last_preference_fast_mode.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings( "Switched to anthropic/claude-opus-4.7", app.notice_body.items, ); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Invalid /model selection") == null);}
test "active stream Enter commits a complete model choice for the next turn" { const alloc = std.testing.allocator; const model = "openai/gpt-5"; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls(model, &efforts, false);
try app.input_runtime.textReplacementState().replace(alloc, "/model " ++ model ++ " auto"); try app.input_runtime.picker.beginModelPickerFlow( alloc, model, 0, false, .effort, ); app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.stream.active); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings(model, app.selected_model.items); try std.testing.expectEqualStrings(model, app.last_preference_model.items); try std.testing.expectEqual(types.ReasoningEffort.auto, app.effort); try std.testing.expectEqual(types.ReasoningEffort.auto, app.last_preference_effort.?); try std.testing.expectEqual(false, app.last_preference_fast_mode.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expectEqualStrings( "Next turn will use " ++ model, app.notice_body.items, );}
test "active stream Enter selects the visible model choice" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const model = "provider/plain-model"; app.model_completion_values = &.{model};
try app.input_runtime.textReplacementState().replace(alloc, "/model provider/plain"); app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.stream.active); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings(model, app.selected_model.items); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings( "Next turn will use " ++ model, app.notice_body.items, ); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime staged model picker Enter ignores hidden slash skill matches" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "model-helper", .description = "model helper", .path = "/tmp/model-helper/SKILL.md", .source = .global_fx, }}; const completions = [_][]const u8{"xai/grok-build-1"};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); app.model_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expectEqualStrings("/model ", app.input_runtime.edit_state.input.items);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings("xai/grok-build-1", app.selected_model.items); try std.testing.expectEqualStrings("xai/grok-build-1", app.last_preference_model.items); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Invalid /model selection") == null);}
test "app_input_runtime Tab on bare model opens the staged picker" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_completion_values = &.{"openai/gpt-5"}; try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqualStrings("/model ", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) != null); try std.testing.expect(!app.model_cache.menu.active);}
test "app_input_runtime Enter on bare model opens the browse catalog" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) == null);}
test "app_input_runtime Enter on selected model slash completion opens browse on first try" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/mode");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len);}
test "app_input_runtime bare model Tab opens on current scrolled selection before staging effort" { const alloc = std.testing.allocator; const current_model = "anthropic/claude-opus-4.8"; const completions = [_][]const u8{ "provider/model-0", "provider/model-1", "provider/model-2", "provider/model-3", "provider/model-4", "provider/model-5", current_model, };
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls(current_model, &efforts, true); app.model_completion_values = &completions; app.selected_model.clearRetainingCapacity(); try app.selected_model.appendSlice(alloc, current_model); app.effort = types.ReasoningEffort.literal("high"); app.fast_mode = false; try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqualStrings("/model ", app.input_runtime.edit_state.input.items); var projected: [32][]const u8 = undefined; const projected_count = input_completion_runtime.CompletionRuntime(RoutingFakeApp).modelPickerCompletions(&app, "", &projected); const projected_index = input_completion_runtime.CompletionRuntime(RoutingFakeApp).modelPickerIndex(&app, projected[0..projected_count]); try std.testing.expectEqual(@as(usize, 6), projected_index); try std.testing.expectEqual(@as(usize, 1), input_completion_runtime.CompletionRuntime(RoutingFakeApp).modelPickerWindowStart(&app, projected_count, projected_index)); try std.testing.expectEqualStrings(current_model, app.selected_model.items); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.effort); try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count); try std.testing.expect(!app.input_runtime.picker.hasPendingModelPickerSelection());
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings(current_model, app.input_runtime.picker.model_picker_pending_model.items); // The effort stage lands on the product default rather // than the session's current effort. try std.testing.expectEqual( 0, app.input_runtime.picker.model_picker_effort_index, ); try std.testing.expect(app.input_runtime.picker.selectedModelPickerFast()); try std.testing.expectEqualStrings(current_model, app.selected_model.items); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.effort); try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count);}
test "app_input_runtime bare model Tab keeps current selection across catalog readiness" { const alloc = std.testing.allocator; const current_model = "anthropic/claude-opus-4.8"; const completions = [_][]const u8{ "provider/model-0", current_model, };
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls(current_model, &efforts, false); app.model_completion_values = &completions; app.model_cache_loading = true; app.selected_model.clearRetainingCapacity(); try app.selected_model.appendSlice(alloc, current_model); try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); app.model_cache_loading = false; try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings(current_model, app.input_runtime.picker.model_picker_pending_model.items); try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count);}
test "app_input_runtime bare model Tab keeps current selection beyond completion window" { const alloc = std.testing.allocator; const current_model = "anthropic/claude-opus-4.8"; var completions = [_][]const u8{"anthropic/claude-opus-4.8-preview"} ** 33; completions[completions.len - 1] = current_model;
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls(current_model, &efforts, false); app.model_completion_values = &completions; app.selected_model.clearRetainingCapacity(); try app.selected_model.appendSlice(alloc, current_model); try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings(current_model, app.input_runtime.picker.model_picker_pending_model.items); try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count);}
test "app_input_runtime stream model picker opens navigates and selects" { const alloc = std.testing.allocator; const models = [_][]const u8{ "provider/first-model", "provider/second-model", }; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_completion_values = ⊧ app.stream.active = true; try app.input_runtime.textReplacementState().replace(alloc, "/model");
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try Runtime(RoutingFakeApp).routePlainVertical(&app, .down, 1); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.stream.active); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings(models[1], app.selected_model.items); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime fast-only model opens the Fast stage while streaming" { const alloc = std.testing.allocator; const model = "provider/fast-only-model"; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.setGatewayControls(model, &.{}, true); app.model_completion_values = &.{model}; app.stream.active = true; try app.input_runtime.textReplacementState().replace(alloc, "/model provider/fast");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return error.TestExpectedEqual; try std.testing.expectEqual(ModelPickerStage.fast, query.stage); try std.testing.expectEqualStrings("", query.query);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.stream.active); try std.testing.expectEqualStrings(model, app.selected_model.items); try std.testing.expect(app.fast_mode); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings("Next turn will use " ++ model, app.notice_body.items);}
test "app_input_runtime Enter submits a dismissed slash skill query as text" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "custom-skill", .description = "custom skill", .path = "/tmp/custom-skill/SKILL.md", .source = .global_fx, }};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "/custom"); app.input_runtime.picker.dismissInlinePicker(.slash);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings( "/custom", app.submitted_prompt[0..app.submitted_prompt_len], ); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);}
test "app_input_runtime Enter binds a slash skill after multiline whitespace" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "custom-skill", .description = "custom skill", .path = "/tmp/custom-skill/SKILL.md", .source = .global_fx, }};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "\n /custom");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("\n $custom-skill ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("custom-skill", app.input_runtime.entities.skill_tokens.items[0].name); try std.testing.expectEqual(@as(usize, 4), app.input_runtime.entities.skill_tokens.items[0].raw_start); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime retired slash alias no longer shadows a matching skill" { const alloc = std.testing.allocator; const skills = [_]skill_runtime.Skill{.{ .name = "input-helper", .description = "input helper", .path = "/tmp/input-helper/SKILL.md", .source = .global_fx, }};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.items = @constCast(&skills); try app.input_runtime.textReplacementState().replace(alloc, "/input");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings("$input-helper ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len);}
fn presentRoutingFilePicker(app: *RoutingFakeApp) void { const rt = input_completion_runtime.CompletionRuntime(RoutingFakeApp); rt.prepareFilePicker(app); if (rt.filePickerView(app).receipt) |receipt| rt.acknowledgeFilePicker(app, receipt);}
test "app_input_runtime file picker replaces only the active query for Tab and Enter" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "src/main.zig", .kind = .file }}; const before_suffix = "prefix @mai"; const input = before_suffix ++ "/suffix"; const expected = "prefix @src/main.zig /suffix"; const expected_cursor = "prefix @src/main.zig ".len;
for ([_]u8{ '\t', '\r' }) |byte| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, input); app.input_runtime.edit_state.cursor = before_suffix.len;
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, byte, 4096, 100);
try std.testing.expectEqualStrings(expected, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(expected_cursor, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); }}
test "app_input_runtime file completion preserves a selected skill binding" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const completions = [_]file_index.Candidate{.{ .path = "target.txt", .kind = .file }}; app.file_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, "review @tar"); try app.input_runtime.skillBindingState().bindSkillToken( alloc, 0, "review".len, "review", "/tmp/review/SKILL.md", null, ); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqualStrings("$review @target.txt ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("review", app.input_runtime.entities.skill_tokens.items[0].name); try std.testing.expectEqualStrings( "/tmp/review/SKILL.md", app.input_runtime.entities.skill_tokens.items[0].path, ); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items[0].raw_start); try std.testing.expectEqual(@as(usize, "$review".len), app.input_runtime.entities.skill_tokens.items[0].raw_end);}
test "app_input_runtime file picker reuses one existing terminator and preserves the tail" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "src/main.zig", .kind = .file }}; const prefix = "prefix @mai"; const cases = [_]struct { tail: []const u8, expected_tail: []const u8, }{ .{ .tail = "", .expected_tail = " " }, .{ .tail = "/suffix", .expected_tail = " /suffix" }, .{ .tail = " suffix", .expected_tail = " suffix" }, .{ .tail = "\tsuffix", .expected_tail = "\tsuffix" }, .{ .tail = "\rsuffix", .expected_tail = "\rsuffix" }, .{ .tail = "\nsuffix", .expected_tail = "\nsuffix" }, .{ .tail = " suffix", .expected_tail = " suffix" }, };
for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; const input = try std.mem.concat(alloc, u8, &.{ prefix, case.tail }); defer alloc.free(input); try app.input_runtime.textReplacementState().replace(alloc, input); app.input_runtime.edit_state.cursor = prefix.len;
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
const expected = try std.mem.concat(alloc, u8, &.{ "prefix @src/main.zig", case.expected_tail }); defer alloc.free(expected); try std.testing.expectEqualStrings(expected, app.input_runtime.edit_state.input.items); try std.testing.expectEqual("prefix @src/main.zig".len + 1, app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) == null); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); }}
test "app_input_runtime accepts typed directories with one synthetic slash" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "src/nested", .kind = .directory }}; const cases = [_]struct { input: []const u8, cursor: usize, expected: []const u8, expected_cursor: usize, }{ .{ .input = "@srcn", .cursor = "@srcn".len, .expected = "@src/nested/", .expected_cursor = "@src/nested/".len, }, .{ .input = "before @srcn tail", .cursor = "before @srcn".len, .expected = "before @src/nested/ tail", .expected_cursor = "before @src/nested/".len, }, };
for ([_]u8{ '\t', '\r' }) |byte| { for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; app.expected_file_completion_kind = .directory; try app.input_runtime.textReplacementState().replace(alloc, case.input); app.input_runtime.edit_state.cursor = case.cursor;
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, byte, 4096, 100);
try std.testing.expectEqualStrings(case.expected, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(case.expected_cursor, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(file_index.CandidateKind.directory, app.validated_file_completion_kind.?); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) != null); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); } }}
test "app_input_runtime counts the directory slash against the input limit" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "src", .kind = .directory }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, "@s");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\t', "@src/".len - 1, 100);
try std.testing.expectEqualStrings("@s", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime quotes whitespace paths only while they are active" { const alloc = std.testing.allocator; const directory = [_]file_index.Candidate{.{ .path = "space dir", .kind = .directory }}; const file = [_]file_index.Candidate{.{ .path = "space dir/item.txt", .kind = .file }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &directory; try app.input_runtime.textReplacementState().replace(alloc, "@space");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expectEqualStrings("@\"space dir/", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) != null);
app.file_completion_values = &file; presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expectEqualStrings("@\"space dir/item.txt\" ", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) == null);}
test "app_input_runtime file completion escapes quotes inside whitespace paths" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "space\" dir", .kind = .directory }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, "@space");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expectEqualStrings("@\"space\\\" dir/", app.input_runtime.edit_state.input.items);}
test "app_input_runtime rejects kind-changed selections and preserves the query" { const alloc = std.testing.allocator; const cases = [_]file_index.Candidate{ .{ .path = "file-became-directory", .kind = .file }, .{ .path = "directory-became-file", .kind = .directory }, }; for (cases) |candidate| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &.{candidate}; app.expected_file_completion_kind = switch (candidate.kind) { .file => .directory, .directory => .file, }; try app.input_runtime.textReplacementState().replace(alloc, "@changed");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("@changed", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(candidate.kind, app.validated_file_completion_kind.?); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refresh_count); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); }}
test "app_input_runtime consumes file search errors without stale insertion" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &.{.{ .path = "stale.txt", .kind = .file }}; app.file_completion_error = error.NoSpaceLeft; try app.input_runtime.textReplacementState().replace(alloc, "@stale");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("@stale", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime file query submits only an acknowledged successful empty result" { const alloc = std.testing.allocator; const states = [_]struct { loading: bool, failed: bool }{ .{ .loading = false, .failed = false }, .{ .loading = true, .failed = false }, .{ .loading = false, .failed = true }, };
for (states) |state| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_index_loading = state.loading; app.file_index_failed = state.failed; try app.input_runtime.textReplacementState().replace(alloc, "@not-present");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
if (state.loading or state.failed) { try std.testing.expectEqualStrings("@not-present", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); } else { try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("@not-present", app.submitted_prompt[0..app.submitted_prompt_len]); } }}
test "app_input_runtime dismissed file query submits as ordinary prompt text" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "@not-present");
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1); presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("@not-present", app.submitted_prompt[0..app.submitted_prompt_len]);}
test "app_input_runtime stale file selection is rejected and refreshed" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "deleted.txt", .kind = .file }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; app.file_completion_current = false; try app.input_runtime.textReplacementState().replace(alloc, "@deleted");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("@deleted", app.input_runtime.edit_state.input.items); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refresh_count); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime stale explicit selection does not refresh the workspace index" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "../deleted.txt", .kind = .file }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; app.file_completion_current = false; app.file_completions_depend_on_index = false; try app.input_runtime.textReplacementState().replace(alloc, "@../deleted");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("@../deleted", app.input_runtime.edit_state.input.items); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 0), app.file_index_refresh_count);}
test "app_input_runtime stale Tab selection requests a footer repaint" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{.{ .path = "deleted.txt", .kind = .file }}; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; app.file_completion_current = false; try app.input_runtime.textReplacementState().replace(alloc, "@deleted");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try std.testing.expectEqualStrings("@deleted", app.input_runtime.edit_state.input.items); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refresh_count); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime stream file picker navigates and selects without submitting" { const alloc = std.testing.allocator; const completions = [_]file_index.Candidate{ .{ .path = "first.txt", .kind = .file }, .{ .path = "second.txt", .kind = .file }, }; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_completion_values = &completions; app.stream.active = true; try app.input_runtime.textReplacementState().replace(alloc, "@file");
presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).routePlainVertical(&app, .down, 1); presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(app.stream.active); try std.testing.expectEqualStrings("@second.txt ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime terminated and unmatched file tokens submit as prompt text" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "@literal "); presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("@literal ", app.submitted_prompt[0..app.submitted_prompt_len]); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try app.input_runtime.textReplacementState().replace(alloc, "@queued"); try std.testing.expect(Runtime(RoutingFakeApp).nonSlashPickerOwnsEnter(&app)); presentRoutingFilePicker(&app); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("@queued", app.submitted_prompt[0..app.submitted_prompt_len]); }}
test "app_input_runtime refreshes each file picker episode after startup loading" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
for ("@first") |byte| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, byte, 4096, 100); } Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expect(app.input_runtime.picker.file_picker_episode_seen); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refresh_count);
try Runtime(RoutingFakeApp).handleTerminalByte(&app, ' ', 4096, 100); try Runtime(RoutingFakeApp).handleTerminalByte(&app, '@', 4096, 100); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 2), app.file_index_refresh_count);
for ("second") |byte| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, byte, 4096, 100); } Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 2), app.file_index_refresh_count);
try Runtime(RoutingFakeApp).handleTerminalByte(&app, ' ', 4096, 100); try Runtime(RoutingFakeApp).handleTerminalByte(&app, '@', 4096, 100); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 3), app.file_index_refresh_count);}
test "app_input_runtime preserves exact refresh accounting across one thousand file picker episodes" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
for (0..1_000) |episode| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, '@', 4_096, 100); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) != null); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(episode + 1, app.file_index_refresh_count);
try Runtime(RoutingFakeApp).handleTerminalByte(&app, 'x', 4_096, 100); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(episode + 1, app.file_index_refresh_count); try Runtime(RoutingFakeApp).handleTerminalByte(&app, ' ', 4_096, 100); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) == null); }
Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expect(app.input_runtime.picker.file_picker_episode_seen); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1_000), app.file_index_refresh_count); try std.testing.expectEqual(@as(usize, 3_000), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime does not duplicate the startup file index load" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.file_index_loading = true;
for ("@first") |byte| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, byte, 4096, 100); } Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expect(app.input_runtime.picker.file_picker_episode_seen); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 0), app.file_index_refresh_count);
app.file_index_loading = false; try Runtime(RoutingFakeApp).handleTerminalByte(&app, ' ', 4096, 100); try Runtime(RoutingFakeApp).handleTerminalByte(&app, '@', 4096, 100); Runtime(RoutingFakeApp).prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refresh_count);}
test "app_input_runtime file picker navigation respects completion cap" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
var values: [file_picker_completion_cap + 1]file_index.Candidate = undefined; for (&values) |*value| value.* = .{ .path = "main.zig", .kind = .file }; app.file_completion_values = values[0..]; try app.input_runtime.textReplacementState().replace(alloc, "prefix @mai /suffix"); app.input_runtime.edit_state.cursor = "prefix @mai".len; presentRoutingFilePicker(&app); app.input_runtime.picker.file_completion_index = file_picker_completion_cap - 1;
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.file_completion_index);}
test "app_input_runtime file picker window moves up before reverse scrolling" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const completions: []const file_index.Candidate = &.{ .{ .path = "main0.zig", .kind = .file }, .{ .path = "main1.zig", .kind = .file }, .{ .path = "main2.zig", .kind = .file }, .{ .path = "main3.zig", .kind = .file }, .{ .path = "main4.zig", .kind = .file }, .{ .path = "main5.zig", .kind = .file }, .{ .path = "main6.zig", .kind = .file }, .{ .path = "main7.zig", .kind = .file }, }; app.file_completion_values = completions; try app.input_runtime.textReplacementState().replace(alloc, "prefix @mai /suffix"); app.input_runtime.edit_state.cursor = "prefix @mai".len;
presentRoutingFilePicker(&app); var down: usize = 0; while (down < 6) : (down += 1) { try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .down, 1); } try std.testing.expectEqual(@as(usize, 6), app.input_runtime.picker.file_completion_index); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.file_completion_window_start);
try Runtime(RoutingFakeApp).routeModifiedHistory(&app, .up, -1); try std.testing.expectEqual(@as(usize, 5), app.input_runtime.picker.file_completion_index); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.file_completion_window_start);}
test "app_input_runtime model picker window moves up before reverse scrolling" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const completions: []const []const u8 = &.{ "provider/model-0", "provider/model-1", "provider/model-2", "provider/model-3", "provider/model-4", "provider/model-5", "provider/model-6", "provider/model-7", }; app.model_completion_values = completions; try app.input_runtime.textReplacementState().replace(alloc, "/model ");
var down: usize = 0; while (down < 6) : (down += 1) { Runtime(RoutingFakeApp).navigateModelPicker(&app, 1); } try std.testing.expectEqual(@as(usize, 6), app.input_runtime.picker.model_completion_index); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.model_completion_window_start);
Runtime(RoutingFakeApp).navigateModelPicker(&app, -1); try std.testing.expectEqual(@as(usize, 5), app.input_runtime.picker.model_completion_index); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.model_completion_window_start);}
test "app_input_runtime model picker consumes Enter while cache is loading for an empty query" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_cache_loading = true; try app.input_runtime.textReplacementState().replace(alloc, "/model "); app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("/model ", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime model picker preserves staged selections and backs out safely" { const alloc = std.testing.allocator; const completions = [_][]const u8{ "openai/gpt-4o", "anthropic/claude-opus-4.7", };
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const efforts = [_]types.ReasoningEffort{ types.ReasoningEffort.literal("low"), types.ReasoningEffort.literal("medium"), types.ReasoningEffort.literal("high"), }; app.setGatewayControls("anthropic/claude-opus-4.7", &efforts, true); app.model_completion_values = &completions; app.fast_mode = true; try app.input_runtime.textReplacementState().replace(alloc, "/model "); app.input_runtime.picker.model_completion_index = 1;
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings( "anthropic/claude-opus-4.7", app.input_runtime.picker.model_picker_pending_model.items, );
const effort_input_end = app.input_runtime.edit_state.input.items.len; _ = app.input_runtime.selectionState().begin(effort_input_end - 1); _ = app.input_runtime.selectionState().extend(effort_input_end); try feedRoutingBytes(&app, "\x1b[D"); try std.testing.expectEqual(ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage); try std.testing.expect(app.input_runtime.edit_state.selectionRange() == null); try std.testing.expectEqual(effort_input_end - 1, app.input_runtime.edit_state.cursor);
try feedRoutingBytes(&app, "\x1b[D"); try std.testing.expectEqual(ModelPickerStage.model, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings("/model anthropic/claude-opus-4.7", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.model_completion_index);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); Runtime(RoutingFakeApp).navigateModelPicker(&app, 2); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(ModelPickerStage.fast, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings( "/model anthropic/claude-opus-4.7 medium ", app.input_runtime.edit_state.input.items, ); // The fast stage lands on the recommended fast default; navigation // still reaches normal and back. try std.testing.expect(app.input_runtime.picker.selectedModelPickerFast()); Runtime(RoutingFakeApp).navigateModelPicker(&app, 1); try std.testing.expect(!app.input_runtime.picker.selectedModelPickerFast()); Runtime(RoutingFakeApp).navigateModelPicker(&app, 1); try std.testing.expect(app.input_runtime.picker.selectedModelPickerFast());
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings("anthropic/claude-opus-4.7", app.selected_model.items); try std.testing.expectEqual(types.ReasoningEffort.literal("medium"), app.effort); try std.testing.expect(app.fast_mode); try std.testing.expectEqual(types.ReasoningEffort.literal("medium"), app.last_preference_effort.?); try std.testing.expectEqual(true, app.last_preference_fast_mode.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "app_input_runtime model picker commits a model without options directly" { const alloc = std.testing.allocator; const completions = [_][]const u8{"openai/gpt-4o"};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, "/model ");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings("openai/gpt-4o", app.selected_model.items); try std.testing.expect(app.last_preference_effort == null); try std.testing.expectEqual(false, app.last_preference_fast_mode.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "app_input_runtime model picker skips effort stage for reasoning model without tiers" { const alloc = std.testing.allocator; const model = "deepseek/deepseek-v4-pro-0813"; const completions = [_][]const u8{model};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_completion_values = &completions; app.gateway_metadata_model = model; app.gateway_metadata = .{ .supports_reasoning = true }; try app.input_runtime.textReplacementState().replace(alloc, "/model ");
const capabilities = app.resolvedModelCapabilities(model); try std.testing.expect(capabilities.supports_reasoning); try std.testing.expectEqual(@as(usize, 0), capabilities.reasoning_efforts.len);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings(model, app.selected_model.items); try std.testing.expectEqual(ModelPickerStage.model, app.input_runtime.picker.model_picker_stage); try std.testing.expect(!app.input_runtime.picker.hasPendingModelPickerSelection()); try std.testing.expect(app.last_preference_effort == null); try std.testing.expectEqual(false, app.last_preference_fast_mode.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "app_input_runtime model picker exposes opaque Gateway reasoning effort" { const alloc = std.testing.allocator; const completions = [_][]const u8{"provider/new-reasoning-model"};
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.model_completion_values = &completions; const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("future-tier")}; app.setGatewayControls("provider/new-reasoning-model", &efforts, false); try app.input_runtime.textReplacementState().replace(alloc, "/model ");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count); try std.testing.expectEqual(ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings( "provider/new-reasoning-model", app.input_runtime.picker.model_picker_pending_model.items, );
Runtime(RoutingFakeApp).navigateModelPicker(&app, 1); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings("provider/new-reasoning-model", app.selected_model.items); try std.testing.expectEqual(types.ReasoningEffort.literal("future-tier"), app.effort); try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(types.ReasoningEffort.literal("future-tier"), app.last_preference_effort.?); try std.testing.expectEqual(false, app.last_preference_fast_mode.?); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "app_input_runtime fatal admission rejects byte before input mutation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "kept"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try app.transcript.appendSlice(alloc, "kept transcript"); app.worker.queued_count = 2; app.shell.render_requests.request(.footer); app.shell.transcript_band_dirty = true; app.metrics = .{ .ansi_bytes = 11, .full_redraws = 12, .debounced_resizes = 13, .footer_line_updates = 14, .stream_chunks = 15, }; app.worker.admission_snapshot = .{ .finalization_failed = .{ .turn_id = 44, .outcome = .failed, } }; const metrics_before = app.metrics;
try std.testing.expectError( error.TurnFinalizationDeliveryFailed, Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100), );
try std.testing.expect(app.should_exit); try std.testing.expectEqualStrings("kept", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.worker.queuedPromptCount()); try std.testing.expectEqualStrings("kept transcript", app.transcript.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(app.shell.transcript_band_dirty); try std.testing.expectEqualDeep(metrics_before, app.metrics);}
test "app_input_runtime question escape arrows preserve modal contracts" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, .{ .label = "Gamma", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "First?", .options = &opts }, .{ .question = "Second?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
app.question_prompt.entries.items[0].choice_index = 1; try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .history_up, null); try std.testing.expectEqual(@as(u8, 0), currentQuestionChoiceForRoutingTest(&app.question_prompt)); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); app.question_prompt.entries.items[0].choice_index = 1; try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .history_down, null); try std.testing.expectEqual(@as(u8, 2), currentQuestionChoiceForRoutingTest(&app.question_prompt)); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); app.question_prompt.current_index = 1; try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .cursor_left, null); try std.testing.expectEqual(@as(u8, 0), app.question_prompt.current_index); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .cursor_right, null); try std.testing.expectEqual(@as(u8, 0), app.question_prompt.current_index); try std.testing.expect(!app.shell.render_requests.hasReason(.modal));
app.question_prompt.entries.items[0].choice_index = 3; try app.question_prompt.entries.items[0].freeform_buffer.appendSlice(alloc, "hi"); app.question_prompt.entries.items[0].freeform_cursor = 2;
try Runtime(RoutingFakeApp).routeQuestionEscapeAction( &app, .cursor_left, .{ .edit_freeform = .cursor_left }, ); try std.testing.expectEqual(@as(usize, 1), app.question_prompt.entries.items[0].freeform_cursor); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.render_requests.clearReason(.modal); try Runtime(RoutingFakeApp).routeQuestionEscapeAction( &app, .cursor_right, .{ .edit_freeform = .cursor_right }, ); try std.testing.expectEqual(@as(usize, 2), app.question_prompt.entries.items[0].freeform_cursor); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime routes question freeform home end delete and word movement" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1); try feedRoutingBytes(&app, "alpha beta");
try Runtime(RoutingFakeApp).routeQuestionEscapeAction( &app, .home, .{ .edit_freeform = .cursor_home }, ); try std.testing.expectEqual(@as(usize, 0), app.question_prompt.entries.items[0].freeform_cursor); try Runtime(RoutingFakeApp).routeQuestionEscapeAction( &app, .word_right, .{ .edit_freeform = .cursor_word_right }, ); try std.testing.expectEqual(@as(usize, "alpha".len), app.question_prompt.entries.items[0].freeform_cursor); try Runtime(RoutingFakeApp).routeQuestionEscapeAction( &app, .delete_next, .{ .edit_freeform = .delete_next }, ); try std.testing.expectEqualStrings("alphabeta", app.question_prompt.entries.items[0].freeform_buffer.items); try Runtime(RoutingFakeApp).routeQuestionEscapeAction( &app, .end, .{ .edit_freeform = .cursor_end }, ); try std.testing.expectEqual( app.question_prompt.entries.items[0].freeform_buffer.items.len, app.question_prompt.entries.items[0].freeform_cursor, );}
test "app_input_runtime routes focused editor aliases into question freeform only" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "hidden composer"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try Runtime(RoutingFakeApp).handleByte(&app, '2', 4096, 100); try feedRoutingBytes(&app, "alpha beta gamma");
try Runtime(RoutingFakeApp).handleByte(&app, 1, 4096, 100); try std.testing.expectEqual(@as(usize, 0), app.question_prompt.entries.items[0].freeform_cursor); try Runtime(RoutingFakeApp).handleByte(&app, 5, 4096, 100); try std.testing.expectEqual( app.question_prompt.entries.items[0].freeform_buffer.items.len, app.question_prompt.entries.items[0].freeform_cursor, ); try Runtime(RoutingFakeApp).handleByte(&app, 2, 4096, 100); try std.testing.expectEqual( app.question_prompt.entries.items[0].freeform_buffer.items.len - 1, app.question_prompt.entries.items[0].freeform_cursor, ); try Runtime(RoutingFakeApp).handleByte(&app, 6, 4096, 100); try std.testing.expectEqual( app.question_prompt.entries.items[0].freeform_buffer.items.len, app.question_prompt.entries.items[0].freeform_cursor, );
try Runtime(RoutingFakeApp).handleByte(&app, 23, 4096, 100); try std.testing.expectEqualStrings("alpha beta ", app.question_prompt.entries.items[0].freeform_buffer.items); try Runtime(RoutingFakeApp).handleByte(&app, 21, 4096, 100); try std.testing.expectEqualStrings("", app.question_prompt.entries.items[0].freeform_buffer.items);
try feedRoutingBytes(&app, "alpha beta"); try Runtime(RoutingFakeApp).handleByte(&app, 1, 4096, 100); try feedRoutingBytes(&app, "\x1bd"); try std.testing.expectEqualStrings("beta", app.question_prompt.entries.items[0].freeform_buffer.items); try Runtime(RoutingFakeApp).handleByte(&app, 5, 4096, 100); try feedRoutingBytes(&app, "\x1b\x7f"); try std.testing.expectEqualStrings("", app.question_prompt.entries.items[0].freeform_buffer.items);
try feedRoutingBytes(&app, "left right"); try Runtime(RoutingFakeApp).handleByte(&app, 1, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 6, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 11, 4096, 100); try std.testing.expectEqualStrings("l", app.question_prompt.entries.items[0].freeform_buffer.items); try std.testing.expectEqualStrings("hidden composer", app.input_runtime.edit_state.input.items); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime plain question arrows stay within multiline freeform answers" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1); try std.testing.expectEqual( question_prompt.FreeformInsertResult.inserted, try app.question_prompt.insertFreeformSlice(alloc, "first\nxy\nthird", 4096), );
try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .cursor_up, null); try std.testing.expect(app.question_prompt.isFreeformSelected()); try std.testing.expectEqual(@as(usize, 8), app.question_prompt.entries.items[0].freeform_cursor);
try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .cursor_up, null); try std.testing.expectEqual(@as(usize, 5), app.question_prompt.entries.items[0].freeform_cursor);
try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .cursor_down, null); try std.testing.expectEqual(@as(usize, 8), app.question_prompt.entries.items[0].freeform_cursor);
try Runtime(RoutingFakeApp).routeQuestionEscapeAction(&app, .history_up, null); try std.testing.expect(!app.question_prompt.isFreeformSelected()); try std.testing.expectEqual(@as(?usize, null), app.question_prompt.entries.items[0].freeform_preferred_column);}
test "app_input_runtime question modal ignores non-modal text" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try feedRoutingBytes(&app, "compose sentinel text");
try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expectEqual(@as(usize, 0), app.worker.submitted_question_count);}
test "app_input_runtime approval modal ignores non-modal text" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "queue approval sentinel text");
try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null);}
test "app_input_runtime modal controls win before ordinary modal input" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try Runtime(RoutingFakeApp).handleByte(&app, '2', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.worker.submitted_question_count); try std.testing.expectEqualStrings( "Beta", app.worker.submitted_question_answer[0..app.worker.submitted_question_answer_len], ); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", .amendment_allowed = true, }));
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100);
try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expectEqualStrings("x", app.approval_prompt.decision.selectedDraft()); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(app.worker.submitted_permission == null); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try Runtime(RoutingFakeApp).handleByte(&app, '3', 4096, 100);
try std.testing.expectEqual(ToolPermissionDecision.deny, app.worker.submitted_permission.?); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); }}
test "app_input_runtime raw CSI-u digits stay isolated from approval selection" { const alloc = std.testing.allocator; const sequences = [_][]const u8{ "\x1b[49;5u", "\x1b[\x1b[49;5u", };
for (sequences) |sequence| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); app.approval_prompt.decision.choice_index = 2;
try feedRoutingBytes(&app, sequence); try std.testing.expectEqual(@as(u8, 2), app.approval_prompt.decision.choice_index); try std.testing.expect(app.worker.submitted_permission == null);
try feedRoutingBytes(&app, "\t"); try std.testing.expectEqual(@as(u8, 2), app.approval_prompt.decision.choice_index); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expect(app.worker.submitted_permission == null); }}
test "app_input_runtime raw CSI-u digits stay isolated from question selection" { const alloc = std.testing.allocator; const sequences = [_][]const u8{ "\x1b[49;5u", "\x1b[\x1b[49;5u", }; const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, .{ .label = "Gamma", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, };
for (sequences) |sequence| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.entries.items[0].choice_index = 1;
try feedRoutingBytes(&app, sequence); try std.testing.expectEqual(@as(u8, 1), currentQuestionChoiceForRoutingTest(&app.question_prompt)); try std.testing.expectEqual(@as(usize, 0), app.worker.submitted_question_answer_len);
try feedRoutingBytes(&app, "\t"); try std.testing.expectEqual(@as(u8, 1), currentQuestionChoiceForRoutingTest(&app.question_prompt)); try std.testing.expectEqual(@as(usize, 0), app.worker.submitted_question_answer_len); }}
test "app_input_runtime raw CSI-u digits stay isolated from question freeform text" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.entries.items[0].choice_index = 2;
try feedRoutingBytes(&app, "\x1b[49;5u"); try std.testing.expectEqual(@as(usize, 0), app.question_prompt.entries.items[0].freeform_buffer.items.len); try std.testing.expectEqual(@as(usize, 0), app.worker.submitted_question_answer_len);
try feedRoutingBytes(&app, "x\r"); try std.testing.expectEqualStrings("x", app.worker.submitted_question_answer[0..app.worker.submitted_question_answer_len]);}
test "app_input_runtime decoded kitty Escape follows the raw Escape policy" { const alloc = std.testing.allocator; const question_sequences = [_][]const u8{ "\x1b[27u", "\x1b[27;1u" }; const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, };
for (question_sequences) |sequence| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try app.question_prompt.syncFrom(alloc, &entries);
try feedRoutingBytes(&app, sequence);
try std.testing.expect(app.worker.question_cancelled); try std.testing.expect(!app.question_prompt.isActive()); try std.testing.expect(!app.stream.active); try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(app.worker.cancel_requested_when_question_cancelled); try std.testing.expectEqual(transcript_runtime.RawEntryClass.question_resolution, app.last_replaceable_class); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try app.question_prompt.syncFrom(alloc, &entries); app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0;
try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0);
try std.testing.expect(app.worker.question_cancelled); try std.testing.expect(!app.question_prompt.isActive()); try std.testing.expect(!app.stream.active); try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(app.worker.cancel_requested_when_question_cancelled); try std.testing.expectEqual(transcript_runtime.RawEntryClass.question_resolution, app.last_replaceable_class); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\x1b[27u");
try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(!app.approval_prompt.isActive()); try std.testing.expect(!app.stream.active); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try feedRoutingBytes(&app, "\x1b[27u");
try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed());
try feedRoutingBytes(&app, "\x1b[27u");
try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(!app.stream.active); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "draft");
try feedRoutingBytes(&app, "\x1b[27u"); try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.gestures.escapeClearArmed());
try feedRoutingBytes(&app, "\x1b[27u"); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); }}
test "pending full transcript open cancels after complete escape input" { const alloc = std.testing.allocator; const sequences = [_][]const u8{ "\x1b[A", "\x1b[B", "\x1b[5~", "\x1b[6~", };
for (sequences) |sequence| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); _ = try app.shell.appendRawTranscriptEntryClassified( alloc, "pending transcript\n", .unknown_raw, ); try std.testing.expect(!app.shell.requestFullTranscriptOpen());
try feedRoutingBytes(&app, sequence);
try std.testing.expectEqualStrings( "", app.input_runtime.edit_state.input.items, ); try std.testing.expect(!app.shell.cancelPendingFullTranscriptOpen()); }}
test "app_input_runtime Ghostty Escape press closes the usage dashboard" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.usage_menu.openError( alloc, .days_30, "usage is unavailable", );
try feedRoutingBytes(&app, "\x1b[27;1:1u");
try std.testing.expect(!app.input_runtime.usage_menu.active);}
test "app_input_runtime Ghostty Escape release leaves the usage dashboard open" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.usage_menu.openError( alloc, .days_30, "usage is unavailable", );
try feedRoutingBytes(&app, "\x1b[27;1:3u");
try std.testing.expect(app.input_runtime.usage_menu.active);}
test "app_input_runtime decoded kitty Backspace edits the draft without cancelling" { const alloc = std.testing.allocator; const sequences = [_][]const u8{ "\x1b[127u", "\x1b[127;1u" };
for (sequences) |sequence| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try feedRoutingBytes(&app, "draft");
try feedRoutingBytes(&app, sequence);
try std.testing.expectEqualStrings("draf", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active); try std.testing.expect(!app.terminal_input_runtime.terminal_action_decoder.cancel_pending); }}
test "app_input_runtime idle terminal ingress stays fast with a maximum composer draft" { const alloc = std.testing.allocator; const input_limits: paste_framing.InputLimits = .{}; const idle_poll_count: usize = 4096; const max_elapsed_ms: i64 = 1000;
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendNTimes( alloc, 'x', input_limits.composer_bytes, ); try std.testing.expect(app.input_runtime.edit_state.moveEnd());
try std.testing.expect( app.input_runtime.picker.activeFilePickerQuery( &app.input_runtime.edit_state, ) == null, );
var ingress = input_action.TerminalInputIngress{}; std.mem.doNotOptimizeAway(&ingress); const started = std.Io.Clock.Timestamp.now(std.testing.io, .awake); for (0..idle_poll_count) |_| { const replay = try Runtime(RoutingFakeApp).handleTerminalInputIngressWithLimits( &app, ingress, input_limits, 100, ); try std.testing.expect(replay == null); } const elapsed_ms = started.durationTo( std.Io.Clock.Timestamp.now(std.testing.io, .awake), ).raw.toMilliseconds();
try std.testing.expect(elapsed_ms < max_elapsed_ms);}
test "app_input_runtime remapped ctrl+c reaches prompt cancellation" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\x1b[99;5u");
try std.testing.expectEqual(ToolPermissionDecision.deny, app.worker.submitted_permission.?); try std.testing.expect(!app.approval_prompt.isActive()); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try feedRoutingBytes(&app, "\x1b[99;5u");
try std.testing.expect(app.worker.question_cancelled); try std.testing.expect(!app.question_prompt.isActive()); try std.testing.expect(!app.stream.active); try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(app.worker.cancel_requested_when_question_cancelled); try std.testing.expectEqual(transcript_runtime.RawEntryClass.question_resolution, app.last_replaceable_class); }}
test "app_input_runtime ctrl+g invokes upgrade shortcut without composer mutation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try Runtime(RoutingFakeApp).handleByte(&app, ctrl_g_upgrade_byte, 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.upgrade_apply_count); try std.testing.expectEqual(@as(usize, 0), app.upgrade_denied_count); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime immediate ctrl+g follows bare Escape" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x1b\x07");
try std.testing.expectEqual(@as(usize, 1), app.upgrade_apply_count); try std.testing.expectEqual(@as(usize, 0), app.upgrade_denied_count); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage);}
test "app_input_runtime ctrl+r no longer invokes the ready upgrade shortcut" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try Runtime(RoutingFakeApp).handleByte(&app, 18, 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.upgrade_apply_count); try std.testing.expectEqual(@as(usize, 0), app.upgrade_denied_count);}
test "app_input_runtime ctrl+z suspends to job control without composer mutation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "draft"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(RoutingFakeApp).handleByte(&app, 26, 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.suspend_count); try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime ctrl+z is inert mid-escape sequence" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 26, 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.suspend_count);}
test "app_input_runtime immediate Ctrl-A follows bare Escape" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abc");
try feedRoutingBytes(&app, "\x1b\x01X");
try std.testing.expectEqualStrings("Xabc", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage);}
test "app_input_runtime immediate Ctrl-U follows slash completion Escape" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/");
try feedRoutingBytes(&app, "\x1b\x15after");
try std.testing.expectEqualStrings("after", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 5), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage);}
test "app_input_runtime Ctrl-X is inert after bare Escape" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x1b\x18");
try std.testing.expectEqual(@as(usize, 0), app.subagents.toggle_view_calls); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage);}
test "app_input_runtime ctrl+d exits on an empty idle composer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try std.testing.expect(!app.should_exit);
try Runtime(RoutingFakeApp).handleByte(&app, 4, 4096, 100);
try std.testing.expect(app.should_exit); try std.testing.expectEqual( app_session_runtime.ResumeHandoffIntent.requested, app.session_persistence.resume_handoff_intent, );}
test "app_input_runtime ctrl+d deletes forward when the composer holds a draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "draft"); app.input_runtime.edit_state.cursor = 0;
try Runtime(RoutingFakeApp).handleByte(&app, 4, 4096, 100);
try std.testing.expect(!app.should_exit); try std.testing.expectEqualStrings("raft", app.input_runtime.edit_state.input.items);}
test "app_input_runtime ctrl+d does not exit while a response is streaming" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, 4, 4096, 100);
try std.testing.expect(!app.should_exit);}
test "app_input_runtime ctrl+g is denied while stream or modal owns state" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, ctrl_g_upgrade_byte, 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.upgrade_apply_count); try std.testing.expectEqual(@as(usize, 1), app.upgrade_denied_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "response finishes") != null); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try Runtime(RoutingFakeApp).handleByte(&app, ctrl_g_upgrade_byte, 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.upgrade_apply_count); try std.testing.expectEqual(@as(usize, 1), app.upgrade_denied_count); try std.testing.expect(app.question_prompt.isActive()); }}
test "app_input_runtime ctrl+c drops an active approval amendment" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", }));
try feedRoutingBytes(&app, "\tsummarize the result"); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expectEqualStrings( "summarize the result", app.approval_prompt.decision.selectedDraft(), );
try feedRoutingBytes(&app, "\x1b[99;5u");
try std.testing.expectEqual(ToolPermissionDecision.deny, app.worker.submitted_permission.?); try std.testing.expectEqual(@as(usize, 0), app.worker.submitted_permission_feedback_len);}
test "app_input_runtime ctrl+c clears an image-only draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.pending_images.append(alloc, .{ .id = 1, .path = try alloc.dupe(u8, "/tmp/image.png"), .media_type = try alloc.dupe(u8, "image/png"), }); try app.input_runtime.textReplacementState().replace(alloc, "[Image #1]");
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100);
try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "Escape dismisses idle compaction feedback without clearing the draft or cancelling a turn" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "keep this draft"); const id = app.worker.compaction.begin(.manual, null, 1); app.worker.compaction.settle(id, .{ .outcome = .failed }, 2); try Runtime(RoutingFakeApp).resolveEscape(&app, false, 3); try std.testing.expect(app.worker.compaction.snapshot.operation.?.dismissed); try std.testing.expectEqualStrings("keep this draft", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime double escape clears an image-only draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.pending_images.append(alloc, .{ .id = 1, .path = try alloc.dupe(u8, "/tmp/image.png"), .media_type = try alloc.dupe(u8, "image/png"), }); try app.input_runtime.textReplacementState().replace(alloc, "[Image #1]");
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1); try std.testing.expectEqual( @as(?i64, 1), app.input_runtime.gestures.escapeClearArmedAt(), ); try Runtime(RoutingFakeApp).resolveEscape(&app, false, 2);
try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed());}
test "app_input_runtime owns pending image discard and release" { const alloc = std.testing.allocator; const PendingImageLifecycleApp = struct { alloc: std.mem.Allocator, input_runtime: core_input_runtime.Runtime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, };
var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var source = try tmp.dir.createFile(std.testing.io, "pending.png", .{}); defer source.close(std.testing.io); try source.writeStreamingAll(std.testing.io, "\x89PNG\r\n\x1a\npending"); } const source_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "pending.png"); defer alloc.free(source_path); const snapshot_root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(snapshot_root); const snapshot_dir = try std.fs.path.join(alloc, &.{ snapshot_root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = PendingImageLifecycleApp{ .alloc = alloc }; defer app.pending_images.deinit(alloc); app.input_runtime.vertical_navigation.preferred_column = 6; try app.pending_images.append(alloc, .{ .id = 1, .path = try alloc.dupe(u8, source_path), .media_type = try alloc.dupe(u8, "image/png"), }); try image_attachments.captureImageSnapshot( alloc, &app.pending_images.items[0], snapshot_dir, ); const discarded_path = try alloc.dupe( u8, app.pending_images.items[0].snapshot_path.?, ); defer alloc.free(discarded_path);
Runtime(PendingImageLifecycleApp).clearPendingImages(&app);
try std.testing.expect(app.input_runtime.vertical_navigation.preferredColumn() == null); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectError( error.FileNotFound, std.Io.Dir.openFileAbsolute(std.testing.io, discarded_path, .{}), );
try app.pending_images.append(alloc, .{ .id = 2, .path = try alloc.dupe(u8, source_path), .media_type = try alloc.dupe(u8, "image/png"), }); try image_attachments.captureImageSnapshot( alloc, &app.pending_images.items[0], snapshot_dir, ); const released_path = try alloc.dupe( u8, app.pending_images.items[0].snapshot_path.?, ); defer alloc.free(released_path);
Runtime(PendingImageLifecycleApp).releasePendingImages(&app);
try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); var released = try std.Io.Dir.openFileAbsolute(std.testing.io, released_path, .{}); released.close(std.testing.io);}
fn currentQuestionChoiceForRoutingTest(prompt: *const question_prompt.QuestionPrompt) u8 { return prompt.entries.items[prompt.current_index].choice_index;}
fn feedRoutingBytes(app: *RoutingFakeApp, bytes: []const u8) !void { return feedRoutingBytesWithLimit(app, bytes, RoutingFakeApp.input_byte_limit);}
fn feedRoutingBytesWithLimit(app: *RoutingFakeApp, bytes: []const u8, max_input_len: usize) !void { for (bytes) |byte| { try Runtime(RoutingFakeApp).handleByte(app, byte, max_input_len, 100); } try Runtime(RoutingFakeApp).settleTerminalPasteDeliveryEpochWithLimits( app, paste_framing.InputLimits.single(max_input_len), );}
fn primeComposerHistoryForTest(comptime App: type, app: *App, draft: []const u8) !void { try app.input_runtime.composer_history.installTextEntries( app.alloc, &.{"history entry"}, ); try app.input_runtime.textReplacementState().replace(app.alloc, draft); try input_completion_runtime.CompletionRuntime(App).navigatePromptHistory(app, -1);}
test "prompt history movement starts a new text undo episode" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try app.input_runtime.composer_history.installTextEntries( alloc, &.{"history entry"}, ); try app.input_runtime.insertionState().insertSlice(alloc, "draft", .preserve); try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1);
try std.testing.expectEqualStrings("history entry", app.input_runtime.edit_state.input.items); try std.testing.expect(!(try app.input_runtime.undoState().undo(alloc)));}
fn openRoutingModelMenu(app: *RoutingFakeApp, model_ids: []const []const u8) !void { const menu = &app.model_cache.menu; menu.active = true; menu.load_state = .ready; for (model_ids) |model_id| { const owned_id = try app.alloc.dupe(u8, model_id); var item_owns_id = false; errdefer if (!item_owns_id) app.alloc.free(owned_id); const provider_end = std.mem.indexOfScalar(u8, owned_id, '/') orelse owned_id.len; const provider = owned_id[0..provider_end]; try menu.items.append(app.alloc, .{ .id = owned_id, .provider = provider, .capabilities = app.resolvedModelCapabilities(owned_id), }); item_owns_id = true; }}
fn openRoutingAuthPicker(app: *RoutingFakeApp) !void { app.auth.source_inventory.insert(.vercel_oidc_token); app.auth.source_inventory.insert(.ai_gateway_api_key); app.auth.openPicker(app.alloc); try std.testing.expect(app.auth.movePicker(1)); try std.testing.expectEqual(@as(usize, 4), app.auth.pickerView().choiceCount()); try std.testing.expectEqual(@as(usize, 1), app.auth.pickerView().selectedIndex());}
const RoutingDecisionKind = enum { question, approval };
fn activateRoutingDecision(app: *RoutingFakeApp, kind: RoutingDecisionKind) !void { switch (kind) { .question => { const options = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &options }, }; try app.question_prompt.syncFrom(app.alloc, &entries); }, .approval => try std.testing.expect(try app.approval_prompt.syncRequest(app.alloc, .{ .label = "shell.run npm test", })), }}
test "app_input_runtime ctrl+w kills whitespace-delimited text and redraws only when changed" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try primeComposerHistoryForTest(RoutingFakeApp, &app, "draft"); try app.input_runtime.textReplacementState().replace(alloc, "foo-bar"); app.input_runtime.vertical_navigation.preferred_column = 7; app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x17"); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("foo-bar", app.input_runtime.kill_ring.text.items); try std.testing.expectEqual(@as(?usize, 0), app.input_runtime.composer_history.activeIndex()); try std.testing.expectEqualStrings("draft", app.input_runtime.composer_history.draftText().?); try std.testing.expectEqual(@as(?usize, null), app.input_runtime.vertical_navigation.preferredColumn()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.shell.render_requests.clearReason(.footer); try feedRoutingBytes(&app, "\x19"); try std.testing.expectEqualStrings("foo-bar", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(?usize, 0), app.input_runtime.composer_history.activeIndex()); try std.testing.expectEqualStrings("draft", app.input_runtime.composer_history.draftText().?); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "unchanged"); try app.input_runtime.kill_ring.text.appendSlice(alloc, "existing kill"); app.input_runtime.vertical_navigation.preferred_column = 7; app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x17"); try std.testing.expectEqualStrings("unchanged", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("existing kill", app.input_runtime.kill_ring.text.items); try std.testing.expectEqual(@as(?usize, null), app.input_runtime.vertical_navigation.preferredColumn()); try std.testing.expect(!app.shell.render_requests.hasReason(.footer)); }}
test "app_input_runtime composer control aliases move, delete, and preserve process state" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abcd");
try feedRoutingBytes(&app, "\x02"); try std.testing.expectEqual(@as(usize, 3), app.input_runtime.edit_state.cursor);
try feedRoutingBytes(&app, "\x02"); try feedRoutingBytes(&app, "\x06"); try Runtime(RoutingFakeApp).handleByte(&app, 'X', 4096, 100); try std.testing.expectEqualStrings("abcXd", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 4), app.input_runtime.edit_state.cursor);
try feedRoutingBytes(&app, "\x04"); try std.testing.expectEqualStrings("abcX", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 4), app.input_runtime.edit_state.cursor); try std.testing.expect(!app.should_exit);
app.input_runtime.inputResetState().clearCurrent(alloc); app.shell.render_requests.clearReason(.footer); try feedRoutingBytes(&app, "\x04"); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.should_exit); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime ctrl+n navigates prompt history directly" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{ "older", "newer" }); try app.input_runtime.textReplacementState().replace(alloc, "draft"); armCtrlCExitForTest(&app.input_runtime, 123);
// Enter history at the oldest entry, then ctrl+n steps newer and past the // end back to the draft. try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items);
try feedRoutingBytes(&app, "\x0e"); try std.testing.expectEqualStrings("newer", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed());
try feedRoutingBytes(&app, "\x0e"); try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items);}
fn setRoutingModelMenuReady(app: *RoutingFakeApp, model_ids: []const []const u8) !void { app.model_cache.menu.load_state = .ready; for (model_ids) |model_id| { const owned_id = try app.alloc.dupe(u8, model_id); errdefer app.alloc.free(owned_id); const provider_end = std.mem.indexOfScalar(u8, owned_id, '/') orelse owned_id.len; try app.model_cache.menu.items.append(app.alloc, .{ .id = owned_id, .provider = owned_id[0..provider_end], .capabilities = app.resolvedModelCapabilities(owned_id), }); }}
test "app_input_runtime ctrl+p opens the model catalog and escape restores the draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "keep this draft"); app.input_runtime.edit_state.cursor = 4;
try feedRoutingBytes(&app, "\x10");
try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.model_picker_draft != null);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("keep this draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 4), app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.model_picker_draft == null); try std.testing.expectEqual(@as(usize, 0), app.preference_commit_count);}
test "app_input_runtime ctrl+p catalog enter applies the model and restores the draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const model = "anthropic/claude-opus-4.8"; const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("future-tier")}; app.setGatewayControls(model, &efforts, true); try app.input_runtime.textReplacementState().replace(alloc, "draft survives"); app.input_runtime.edit_state.cursor = 3;
try feedRoutingBytes(&app, "\x10"); try setRoutingModelMenuReady(&app, &.{model}); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
// The shortcut applies the model directly instead of chaining into the // inline effort and fast stages. try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings(model, app.selected_model.items); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqual(picker_state.ModelPickerStage.model, app.input_runtime.picker.model_picker_stage); try std.testing.expect(!app.input_runtime.picker.hasPendingModelPickerSelection()); try std.testing.expectEqualStrings("draft survives", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 3), app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.model_picker_draft == null);}
test "app_input_runtime ctrl+p toggles the model catalog closed" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "toggle draft");
try feedRoutingBytes(&app, "\x10"); try std.testing.expect(app.model_cache.menu.active);
try feedRoutingBytes(&app, "\x10"); try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("toggle draft", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.model_picker_draft == null);}
test "app_input_runtime ctrl+p model catalog keeps history navigation position" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try primeComposerHistoryForTest(RoutingFakeApp, &app, "draft"); try std.testing.expectEqualStrings("history entry", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.composer_history.activeIndex() != null);
try feedRoutingBytes(&app, "\x10"); try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1);
try std.testing.expectEqualStrings("history entry", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(?usize, 0), app.input_runtime.composer_history.activeIndex());
// Navigation resumes where it left off: down returns to the pending draft. try feedRoutingBytes(&app, "\x0e"); try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items);}
test "app_input_runtime ctrl+p leaves the draft alone when a decision owns input" { const alloc = std.testing.allocator; for ([_]RoutingDecisionKind{ .question, .approval }) |kind| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "undisturbed"); try activateRoutingDecision(&app, kind);
try feedRoutingBytes(&app, "\x10");
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("undisturbed", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.model_picker_draft == null); }}
test "app_input_runtime ctrl+c while the picker borrows the composer keeps the stashed draft and its image" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); defer app.clearPendingImages(); try app.pending_images.append(alloc, .{ .id = 1, .path = try alloc.dupe(u8, "/tmp/image.png"), .media_type = try alloc.dupe(u8, "image/png"), }); try app.input_runtime.textReplacementState().replace(alloc, "draft [Image #1]");
try feedRoutingBytes(&app, "\x10"); try std.testing.expect(app.model_cache.menu.active); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100);
// The picker backs out instead of clearing draft state: the pending image // payload and the draft text both survive, and no exit gesture was armed. try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("draft [Image #1]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expect(app.input_runtime.model_picker_draft == null); try std.testing.expect(!app.should_exit); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed());}
test "app_input_runtime ctrl+d while the picker borrows the composer closes instead of exiting" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "keep me");
try feedRoutingBytes(&app, "\x10"); try std.testing.expect(app.model_cache.menu.active);
try Runtime(RoutingFakeApp).handleByte(&app, 4, 4096, 100);
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("keep me", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.model_picker_draft == null); try std.testing.expect(!app.should_exit);}
test "app_input_runtime ctrl+p catalog enter failure still closes and restores the draft" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const alloc = failing.allocator(); var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "fragile draft");
try feedRoutingBytes(&app, "\x10"); try setRoutingModelMenuReady(&app, &.{"alpha/one"}); // The selection copy owns the first allocation; fail the apply itself. failing.fail_index = failing.alloc_index + 1;
try std.testing.expectError( error.OutOfMemory, Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100), );
try std.testing.expect(!app.model_cache.menu.active); try std.testing.expectEqualStrings("fragile draft", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.model_picker_draft == null);}
test "app_input_runtime image attach is skipped while the model catalog borrows the composer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "draft");
try feedRoutingBytes(&app, "\x10"); try std.testing.expect(app.model_cache.menu.active);
try Runtime(RoutingFakeApp).handleByte(&app, 22, 4096, 100);
try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expect(app.model_cache.menu.active);}
test "app_input_runtime plain arrows keep history ownership across recalled slash commands" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{ "older", "/help" });
try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); try std.testing.expectEqualStrings("/help", app.input_runtime.edit_state.input.items); try std.testing.expectEqual( @as(usize, 0), input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleSlashCompletionCount(&app), );
try feedRoutingBytes(&app, "\x1b[A"); try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items);
try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqualStrings("/help", app.input_runtime.edit_state.input.items); try feedRoutingBytes(&app, "\x7f"); try std.testing.expectEqualStrings("/hel", app.input_runtime.edit_state.input.items); try std.testing.expect( input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleSlashCompletionCount(&app) > 0, );
app.input_runtime.inputResetState().clearCurrent(alloc); try Runtime(RoutingFakeApp).handleByte(&app, '/', 4096, 100); try std.testing.expect( input_completion_runtime.CompletionRuntime(RoutingFakeApp).visibleSlashCompletionCount(&app) > 0, );}
test "app_input_runtime up arrow retracts a queued steer into an empty composer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{ "older", "newer" }); app.worker.queued_steer_text = "steer text";
try feedRoutingBytes(&app, "\x1b[A"); // The restored text matches no history entry, proving restore over recall. try std.testing.expectEqualStrings("steer text", app.input_runtime.edit_state.input.items); try std.testing.expect(app.worker.queued_steer_text == null); try std.testing.expect(app.input_runtime.composer_history.activeIndex() == null);
// With the steer back in the composer, up and down cycle history normally: // up jumps to the draft start, then recalls the newest history entry. try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("newer", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.composer_history.activeIndex() != null); try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqualStrings("steer text", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.composer_history.activeIndex() == null);}
test "app_input_runtime up arrow during a history episode keeps the queued steer and draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{ "older", "newer" }); app.worker.queued_steer_text = "steer text";
// Enter history with a stashed draft, then clear the recalled entry to empty. try app.input_runtime.textReplacementState().replace(alloc, "unsent draft"); try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); try std.testing.expectEqualStrings("newer", app.input_runtime.edit_state.input.items); try app.input_runtime.textReplacementState().replace(alloc, ""); try std.testing.expect(app.input_runtime.composer_history.activeIndex() != null);
// Up must not retract the steer or drop the stashed draft; it navigates history. try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items); try std.testing.expect(app.worker.queued_steer_text != null);
// Down past the newest entry restores the stashed draft intact. try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqualStrings("newer", app.input_runtime.edit_state.input.items); try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqualStrings("unsent draft", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.composer_history.activeIndex() == null);}
test "app_input_runtime up arrow with a non-empty draft leaves queued steers queued" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{"older"}); app.worker.queued_steer_text = "steer text"; try app.input_runtime.insertionState().insertSlice(alloc, "draft", .preserve);
try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items); try std.testing.expect(app.worker.queued_steer_text != null);
try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items); try std.testing.expect(app.worker.queued_steer_text != null);}
test "app_input_runtime decoded history recall disarms pending Ctrl-C exit" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{"history sentinel"});
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); armEscapeClearForTest(&app.input_runtime, io_mod.milliTimestamp()); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x1b[A");
try std.testing.expectEqualStrings("history sentinel", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(app.input_runtime.gestures.escapeClearArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.should_exit);
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime bare Escape disarms pending Ctrl-C exit" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); app.terminal_input_runtime.terminal_action_decoder.started_ms = io_mod.milliTimestamp() - 31; app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).flushPendingEscape(&app, 30);
try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit);}
test "app_input_runtime ctrl+j and ctrl+k navigate visible composer pickers" { const alloc = std.testing.allocator; const cases = [_]struct { bytes: []const u8, expected_index: usize, }{ .{ .bytes = "\x0a", .expected_index = 3 }, .{ .bytes = "\x0b", .expected_index = 0 }, .{ .bytes = "\x1b[106;5u", .expected_index = 3 }, .{ .bytes = "\x1b[107;5u", .expected_index = 0 }, };
for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try openRoutingAuthPicker(&app); try app.input_runtime.textReplacementState().replace(alloc, "/"); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, case.bytes);
try std.testing.expect(app.auth.pickerView().active); try std.testing.expectEqual(case.expected_index, app.auth.pickerView().selectedIndex()); try std.testing.expectEqualStrings("/", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.edit_state.cursor); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); }}
test "app_input_runtime ctrl+j and ctrl+k preserve editor fallback behind full transcript" { const alloc = std.testing.allocator; const cases = [_]struct { bytes: []const u8, expected_input: []const u8, }{ .{ .bytes = "\x0a", .expected_input = "a\nb" }, .{ .bytes = "\x0b", .expected_input = "a" }, .{ .bytes = "\x1b[106;5u", .expected_input = "a\nb" }, .{ .bytes = "\x1b[107;5u", .expected_input = "a" }, };
for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); try openRoutingAuthPicker(&app); try app.input_runtime.textReplacementState().replace(alloc, "ab"); app.input_runtime.edit_state.cursor = 1;
try feedRoutingBytes(&app, case.bytes);
try std.testing.expectEqualStrings(case.expected_input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.auth.pickerView().selectedIndex()); try std.testing.expect(!app.auth.pickerView().active); try std.testing.expect(app.terminal.fullTranscriptScreenActive()); }}
test "app_input_runtime modal-only controls do not reach the composer" { const alloc = std.testing.allocator; const cases = [_]struct { kind: RoutingDecisionKind, bytes: []const u8, }{ .{ .kind = .question, .bytes = "\x0b" }, .{ .kind = .question, .bytes = "\x1b[107;5u" }, .{ .kind = .approval, .bytes = "\x0b" }, .{ .kind = .approval, .bytes = "\x1b[107;5u" }, };
for ([_]bool{ false, true }) |stacked_subagent| { for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try activateRoutingDecision(&app, case.kind); app.subagents.active = stacked_subagent; try openRoutingAuthPicker(&app); try app.input_runtime.textReplacementState().replace(alloc, "ab"); app.input_runtime.edit_state.cursor = 1;
try feedRoutingBytes(&app, case.bytes);
try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.auth.pickerView().selectedIndex()); try std.testing.expect(app.auth.pickerView().active); try std.testing.expect(app.question_prompt.isActive() or app.approval_prompt.isActive()); try std.testing.expectEqual(@as(usize, 0), app.worker.submitted_question_count); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 0), app.subagents.handled_keys);
try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); } }}
const RoutingDraftKind = enum { text, paste, image,};
fn seedRoutingDraftForGuard(app: *RoutingFakeApp, kind: RoutingDraftKind) !void { switch (kind) { .text => try app.input_runtime.textReplacementState().replace(app.alloc, "guarded draft"), .paste => try app.input_runtime.entities.pasted_blocks.append(app.alloc, .{ .id = 1, .text = try app.alloc.dupe(u8, "guarded pasted text"), .line_count = 1, }), .image => try app.pending_images.append(app.alloc, .{ .id = 1, .path = try app.alloc.dupe(u8, "/tmp/guarded-image.png"), .media_type = try app.alloc.dupe(u8, "image/png"), }), }}
fn expectRoutingDraftForGuard(app: *const RoutingFakeApp, kind: RoutingDraftKind) !void { switch (kind) { .text => try std.testing.expectEqualStrings("guarded draft", app.input_runtime.edit_state.input.items), .paste => { try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqualStrings("guarded pasted text", app.input_runtime.entities.pasted_blocks.items[0].text); }, .image => { try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqualStrings("/tmp/guarded-image.png", app.pending_images.items[0].path); }, }}
test "app_input_runtime ctrl+l preserves text paste and image drafts" { const alloc = std.testing.allocator; const control_sequences = [_][]const u8{ "\x0c", "\x1b[108;5u", }; const draft_kinds = [_]RoutingDraftKind{ .text, .paste, .image };
for (control_sequences) |bytes| { for (draft_kinds) |kind| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); defer app.clearPendingImages(); try seedRoutingDraftForGuard(&app, kind);
try feedRoutingBytes(&app, bytes);
try std.testing.expect(app.last_command == null); try expectRoutingDraftForGuard(&app, kind); } }}
test "app_input_runtime ctrl+l does not invoke the clear command" { const alloc = std.testing.allocator; const control_sequences = [_][]const u8{ "\x0c", "\x1b[108;5u", };
for (control_sequences) |bytes| { var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, bytes);
try std.testing.expect(app.last_command == null); }}
test "app_input_runtime cmd+r refuses to open sessions over text paste and image drafts" { const alloc = std.testing.allocator; const draft_kinds = [_]RoutingDraftKind{ .text, .paste, .image };
for (draft_kinds) |kind| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); defer app.clearPendingImages(); try seedRoutingDraftForGuard(&app, kind);
try feedRoutingBytes(&app, "\x1b[114;9u");
try std.testing.expect(!app.session_persistence.session_picker.active); try expectRoutingDraftForGuard(&app, kind); try std.testing.expectEqualStrings("session", app.notice_topic.items); try std.testing.expectEqual(types.NoticeTone.neutral, app.notice_tone); try std.testing.expectEqualStrings( "submit or clear the draft before switching sessions", app.notice_body.items, ); }}
test "app_input_runtime cmd+r keeps empty composer picker behavior" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x1b[114;9u");
try std.testing.expect(app.session_persistence.session_picker.active);}
test "app_input_runtime ctrl+l leaves the session catalog open" { const alloc = std.testing.allocator; const cases = [_][]const u8{ "\x0c", "\x1b[108;5u", };
for (cases) |bytes| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "draft with paste"); try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 1, .text = try alloc.dupe(u8, "owned pasted text"), .line_count = 1, }); app.session_persistence.session_picker.active = true;
try feedRoutingBytes(&app, bytes);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings("draft with paste", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expect(app.session_persistence.session_picker.active); }}
test "app_input_runtime decision prompts consume raw and kitty ctrl+l" { const alloc = std.testing.allocator; const cases = [_]struct { kind: RoutingDecisionKind, bytes: []const u8, }{ .{ .kind = .question, .bytes = "\x0c" }, .{ .kind = .question, .bytes = "\x1b[108;5u" }, .{ .kind = .approval, .bytes = "\x0c" }, .{ .kind = .approval, .bytes = "\x1b[108;5u" }, };
for ([_]bool{ false, true }) |stacked_subagent| { for (cases) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try activateRoutingDecision(&app, case.kind); app.subagents.active = stacked_subagent; try app.input_runtime.textReplacementState().replace(alloc, "draft");
try feedRoutingBytes(&app, case.bytes);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items); try std.testing.expect(app.question_prompt.isActive() or app.approval_prompt.isActive()); try std.testing.expectEqual(@as(usize, 0), app.subagents.handled_keys); } }}
test "app_input_runtime active multiline history moves vertically before advancing" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{ "older", "newer top\nnewer bottom" }); try app.input_runtime.textReplacementState().replace(alloc, "draft top\ndraft bottom");
try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); try std.testing.expectEqualStrings("newer top\nnewer bottom", app.input_runtime.edit_state.input.items); _ = horizontal_navigation.move( .character_left, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("newer top\nnewer bottom", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, "newer top".len), app.input_runtime.edit_state.cursor); try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("newer top\nnewer bottom", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items); try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqualStrings("newer top\nnewer bottom", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor); try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqualStrings("draft top\ndraft bottom", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor);
try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); _ = try Runtime(RoutingFakeApp).routeResolvedEscapeAction( &app, .history_up, null, null, null, false, 4096, ); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items);
try openRoutingAuthPicker(&app); try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqual(@as(usize, 0), app.auth.pickerView().selectedIndex()); try std.testing.expectEqualStrings("older", app.input_runtime.edit_state.input.items);}
test "app_input_runtime editing recalled history keeps the original draft reachable" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.composer_history.installTextEntries(alloc, &.{"historical prompt"}); try app.input_runtime.textReplacementState().replace(alloc, "unsent draft");
try input_completion_runtime.CompletionRuntime(RoutingFakeApp).navigatePromptHistory(&app, -1); try Runtime(RoutingFakeApp).handleByte(&app, '!', 4096, 100);
try std.testing.expectEqualStrings("historical prompt!", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(?usize, 0), app.input_runtime.composer_history.activeIndex()); try std.testing.expectEqualStrings("unsent draft", app.input_runtime.composer_history.draftText().?);
try Runtime(RoutingFakeApp).routeComposerShortcutAction( &app, .history_next, 4096, ); try std.testing.expectEqualStrings("unsent draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(?usize, null), app.input_runtime.composer_history.activeIndex()); try std.testing.expect(app.input_runtime.composer_history.draftView() == null);}
test "app_input_runtime alt+d deletes the next composer word" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "alpha beta"); app.input_runtime.edit_state.cursor = 0;
try feedRoutingBytes(&app, "\x1bd");
try std.testing.expectEqualStrings("beta", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor);}
test "app_input_runtime backslash enter inserts newline after enter-owned handlers decline" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "one\\");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqualStrings("one\n", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);}
test "app_input_runtime backslash enter is consumed by an active command skills menu" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "review", .description = "", .path = "/tmp/review/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); app.skills.openMenu(); try app.input_runtime.textReplacementState().replace(alloc, "\\");
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(!std.mem.eql(u8, app.input_runtime.edit_state.input.items, "\n")); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expect(app.skills.menu.active);}
test "app_input_runtime decision prompt owns and discards bracketed paste" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "decision-paste.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "input");
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
const payload = "typing\t\r\n" ++ "\x03" ++ "\x1b[99;5u" ++ "\x1b[A" ++ "\x1b[200~"; try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try feedRoutingBytes(&app, payload); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len);
try feedRoutingBytes(&app, "\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.approval_prompt.isActive()); debug_trace.shutdown();
const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); const needle = try std.fmt.allocPrint(alloc, "decision prompt paste dropped bytes={d} reason=decision_prompt_active", .{payload.len}); defer alloc.free(needle); try std.testing.expect(std.mem.find(u8, trace, needle) != null);}
test "app_input_runtime rejects same-epoch paste suffix without submitting it" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "keep");
try feedRoutingBytes( &app, "\x1b[200~\x1b[201~/version\r\x1b[201~", );
try std.testing.expectEqualStrings("keep", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner);
try feedRoutingBytes(&app, "\r"); try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings("keep", app.submitted_prompt[0..app.submitted_prompt_len]);}
test "app_input_runtime rejects a same-epoch enter after an otherwise valid paste" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "prior");
try feedRoutingBytes(&app, "\x1b[200~new\x1b[201~\r");
try std.testing.expectEqualStrings("prior", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count);}
test "app_input_runtime approval amendment accepts bracketed paste" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\t"); try std.testing.expect(app.approval_prompt.isAmending());
const payload = "pasted amendment marker " ++ "\\x1b[200~" ++ "\x1b[200~" ++ "\x1b[201;1~" ++ "\t\r\x03"; try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.approval_amendment, app.input_runtime.paste.owner); try feedRoutingBytes(&app, payload);
try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqualStrings("", app.approval_prompt.decision.selectedDraft());
try feedRoutingBytes(&app, "\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqualStrings( "pasted amendment marker \\x1b[200~[200~[201;1~ ", app.approval_prompt.decision.selectedDraft(), );}
test "app_input_runtime approval amendment rejects oversized paste visibly" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", })); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 2, 100);
try feedRoutingBytesWithLimit(&app, "\x1b[200~abc\x1b[201~", 2);
try std.testing.expectEqualStrings("", app.approval_prompt.decision.selectedDraft()); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings("input", app.notice_topic.items); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime keeps composer and decision input limits independent" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const limits = paste_framing.InputLimits{ .composer_bytes = 4, .decision_bytes = 2, };
for ("abcd") |byte| { try Runtime(RoutingFakeApp).handleTerminalByteWithLimits( &app, byte, limits, 100, ); } try std.testing.expectEqualStrings("abcd", app.input_runtime.edit_state.input.items);
try std.testing.expect(try app.approval_prompt.syncRequest( alloc, .{ .label = "shell.run npm test" }, )); try Runtime(RoutingFakeApp).handleTerminalByteWithLimits(&app, '\t', limits, 100); for ("xyz") |byte| { try Runtime(RoutingFakeApp).handleTerminalByteWithLimits( &app, byte, limits, 100, ); }
try std.testing.expectEqualStrings("xy", app.approval_prompt.decision.selectedDraft()); try std.testing.expectEqualStrings("abcd", app.input_runtime.edit_state.input.items);}
test "app_input_runtime approval amendment paste finalization resets state after allocation failure" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); try feedRoutingBytes(&app, "\t"); try std.testing.expect(app.approval_prompt.isAmending()); try app.input_runtime.paste.buffer.ensureTotalCapacity(alloc, 1); try feedRoutingBytes(&app, "\x1b[200~");
var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 }); app.alloc = failing.allocator(); try std.testing.expectError( error.OutOfMemory, feedRoutingBytes(&app, "x\x1b[201~"), );
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.approval_prompt.isAmending());}
test "app_input_runtime paste start drops pending gesture arms before capture" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x03"); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); armEscapeClearForTest(&app.input_runtime, 777); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x1b[200~");
try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expectEqual(@as(u16, 0), app.terminal_input_runtime.terminal_action_decoder.param); try std.testing.expectEqual(@as(u16, 0), app.terminal_input_runtime.terminal_action_decoder.param2); try std.testing.expectEqual(@as(i64, 0), app.terminal_input_runtime.terminal_action_decoder.started_ms); try std.testing.expect(!app.terminal_input_runtime.terminal_action_decoder.cancel_pending); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
try feedRoutingBytes(&app, "\x1b[201~");}
test "app_input_runtime paste start without pending gestures skips footer redraw" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
app.shell.render_requests.clearReason(.footer); try feedRoutingBytes(&app, "\x1b[200~");
try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); try std.testing.expect(!app.shell.render_requests.hasReason(.footer));
try feedRoutingBytes(&app, "\x1b[201~");}
test "app_input_runtime zero-content decision paste logs once" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "zero-decision-paste.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "input");
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\x1b[200~\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.approval_prompt.isActive()); debug_trace.shutdown();
const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, trace, "decision prompt paste dropped bytes=0 reason=decision_prompt_active"));}
test "app_input_runtime decision paste start clears pending ctrl-c and esc gestures" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); armCtrlCExitForTest(&app.input_runtime, io_mod.milliTimestamp()); armEscapeClearForTest(&app.input_runtime, io_mod.milliTimestamp()); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x1b[200~");
try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime question prompt owns and discards bracketed paste" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
const payload = "typing\t\r\n" ++ "\x03" ++ "\x1b[99;5u" ++ "\x1b[A" ++ "\x1b[200~"; try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try feedRoutingBytes(&app, payload); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expectEqual(@as(u8, 0), currentQuestionChoiceForRoutingTest(&app.question_prompt)); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expectEqual(@as(usize, 0), app.question_prompt.entries.items[0].freeform_buffer.items.len);
try feedRoutingBytes(&app, "\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.question_prompt.isActive());}
test "app_input_runtime question freeform accepts bracketed paste atomically" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1);
try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.question_freeform, app.input_runtime.paste.owner); try feedRoutingBytes(&app, "first\rsecond\tvalue\x03"); try std.testing.expectEqual(@as(usize, 0), app.question_prompt.entries.items[0].freeform_buffer.items.len); try std.testing.expectEqualStrings("first\rsecond value", app.input_runtime.paste.buffer.items);
try feedRoutingBytes(&app, "\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqualStrings("first\nsecond value", app.question_prompt.entries.items[0].freeform_buffer.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime rejects unsafe suffixes for every root modal paste owner" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", }));
try feedRoutingBytes(&app, "\x1b[200~\x1b[201~1\r");
try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", })); try feedRoutingBytes(&app, "\tkeep");
try feedRoutingBytes(&app, "\x1b[200~new\x1b[201~\r");
try std.testing.expectEqualStrings("keep", app.approval_prompt.decision.selectedDraft()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Other", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1); try feedRoutingBytes(&app, "keep");
try feedRoutingBytes(&app, "\x1b[200~new\x1b[201~\r");
try std.testing.expectEqualStrings( "keep", app.question_prompt.entries.items[0].freeform_buffer.items, ); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); }}
test "app_input_runtime question freeform rejects paste beyond input limit" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1);
try feedRoutingBytes(&app, "\x1b[200~"); try feedRoutingBytes(&app, "x" ** 4097); try feedRoutingBytes(&app, "\x1b[201~");
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.question_prompt.entries.items[0].freeform_buffer.items.len); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings("input", app.notice_topic.items); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "app_input_runtime rejects invalid utf8 paste without changing the draft" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "safe draft");
try feedRoutingBytes(&app, "\x1b[200~"); try Runtime(RoutingFakeApp).handleByte(&app, 0xC3, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '(', 4096, 100); try feedRoutingBytes(&app, "\x1b[201~");
try std.testing.expectEqualStrings("safe draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expectEqualStrings("input", app.notice_topic.items); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone);}
test "app_input_runtime admits a typed utf8 scalar only when the whole scalar fits" { const alloc = std.testing.allocator;
for ([_]struct { limit: usize, expected: []const u8, }{ .{ .limit = 2, .expected = "x" }, .{ .limit = 3, .expected = "xé" }, }) |case| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "x");
try Runtime(RoutingFakeApp).handleByte(&app, 0xC3, case.limit, 100); try std.testing.expectEqualStrings("x", app.input_runtime.edit_state.input.items); try Runtime(RoutingFakeApp).handleByte(&app, 0xA9, case.limit, 100);
try std.testing.expectEqualStrings(case.expected, app.input_runtime.edit_state.input.items); try std.testing.expect(std.unicode.utf8ValidateSlice(app.input_runtime.edit_state.input.items)); }}
test "app_input_runtime reports one notice for a continuous composer limit rejection" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abcd");
try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'y', 4, 100);
try std.testing.expectEqualStrings("abcd", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings("input", app.notice_topic.items); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone);
try Runtime(RoutingFakeApp).handleByte(&app, 0x7F, 4, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'z', 4, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'q', 4, 100);
try std.testing.expectEqualStrings("abcz", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.notice_write_count);}
test "app_input_runtime bounds typed question and approval drafts with visible feedback" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1);
try feedRoutingBytesWithLimit(&app, "abcc", 2);
try std.testing.expectEqualStrings("ab", app.question_prompt.entries.items[0].freeform_buffer.items); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expect(app.shell.render_requests.hasReason(.modal)); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test", })); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 2, 100);
try feedRoutingBytesWithLimit(&app, "abcc", 2);
try std.testing.expectEqualStrings("ab", app.approval_prompt.decision.selectedDraft()); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expect(app.shell.render_requests.hasReason(.modal)); }}
test "app_input_runtime rejects yank and file completion before owner mutation" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abc"); try app.input_runtime.kill_ring.text.appendSlice(alloc, "XY");
try Runtime(RoutingFakeApp).routeComposerShortcutAction(&app, .yank, 4);
try std.testing.expectEqualStrings("abc", app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("XY", app.input_runtime.kill_ring.text.items); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const completions = [_]file_index.Candidate{.{ .path = "long/path.zig", .kind = .file }}; app.file_completion_values = &completions; try app.input_runtime.textReplacementState().replace(alloc, "@l"); presentRoutingFilePicker(&app);
try Runtime(RoutingFakeApp).handleByte(&app, '\t', 2, 100);
try std.testing.expectEqualStrings("@l", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) != null); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); }}
test "app_input_runtime routes typed utf8 scalars to modal editors" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Other", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.entries.items[0].choice_index = @intCast(app.question_prompt.entries.items[0].options.items.len - 1);
try feedRoutingBytes(&app, "🙂");
try std.testing.expectEqualStrings("🙂", app.question_prompt.entries.items[0].freeform_buffer.items); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100);
try feedRoutingBytes(&app, "🙂");
try std.testing.expectEqualStrings("🙂", app.approval_prompt.decision.selectedDraft()); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); }}
test "app_input_runtime question prompt owns paste over an approval amendment" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); try feedRoutingBytes(&app, "\tkeep"); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expectEqualStrings("keep", app.approval_prompt.decision.selectedDraft());
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try feedRoutingBytes(&app, "question paste\x1b[201~");
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expectEqual(@as(u8, 0), currentQuestionChoiceForRoutingTest(&app.question_prompt)); try std.testing.expectEqualStrings("keep", app.approval_prompt.decision.selectedDraft()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime routes input to the innermost active modal" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
const opts = [_]types.QuestionOption{ .{ .label = "Alpha", .description = null }, .{ .label = "Beta", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Continue?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expect(!app.approval_prompt.isAmending()); try std.testing.expectEqual(@as(usize, 0), app.subagents.handled_keys); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);
app.question_prompt.discard(alloc, "test_cleanup"); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expectEqual(@as(usize, 0), app.subagents.handled_keys);
app.approval_prompt.clear(alloc); try Runtime(RoutingFakeApp).handleByte(&app, 'x', 4096, 100); try std.testing.expectEqualStrings("x", app.input_runtime.edit_state.input.items);}
test "app_input_runtime active paste shields prompts from escape timeout cancellation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.paste.end_match_len);
app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0);
try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.paste.end_match_len); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expect(!app.terminal_input_runtime.terminal_action_decoder.cancel_pending);}
test "app_input_runtime terminal paste pauses and resumes the resize cursor probe only at exact boundaries" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.terminal_input_runtime.terminal_cursor_probe.begin(.ansi_tagged, 0);
for ("\x1b[200~") |byte| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, byte, 4096, 100); } try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); switch (app.terminal_input_runtime.terminal_cursor_probe.poll(std.math.maxInt(i64))) { .none => {}, else => return error.TestExpectedEqual, }
for ("\x1b[201;1~") |byte| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, byte, 4096, 100); } try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); switch (app.terminal_input_runtime.terminal_cursor_probe.poll(std.math.maxInt(i64))) { .none => {}, else => return error.TestExpectedEqual, }
for ("\x1b[201~") |byte| { try Runtime(RoutingFakeApp).handleTerminalByte(&app, byte, 4096, 100); } try Runtime(RoutingFakeApp).settleTerminalPasteDeliveryEpochWithLimits( &app, paste_framing.InputLimits.single(4096), ); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); switch (app.terminal_input_runtime.terminal_cursor_probe.poll(std.math.maxInt(i64))) { .probe_timed_out => {}, else => return error.TestExpectedEqual, }}
test "app_input_runtime false paste starts leave decision prompt controls usable" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\x1b[0200~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null);
try feedRoutingBytes(&app, "\x1b[200;1~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null);
try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, '[', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'C', 4096, 100); try std.testing.expectEqual(@as(u8, 1), app.approval_prompt.decision.choice_index);
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100); try std.testing.expectEqual(ToolPermissionDecision.always, app.worker.submitted_permission.?);}
test "app_input_runtime false paste starts preserve stale paste and gestures" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); app.input_runtime.paste.decision_bytes = 7; try app.input_runtime.paste.buffer.appendSlice(alloc, "old"); armCtrlCExitForTest(&app.input_runtime, 123); armEscapeClearForTest(&app.input_runtime, 456); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x1b[0200~"); try feedRoutingBytes(&app, "\x1b[200;1~");
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 7), app.input_runtime.paste.decision_bytes); try std.testing.expectEqualStrings("old", app.input_runtime.paste.buffer.items); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expectEqual( @as(?i64, 123), app.input_runtime.gestures.ctrlCExitArmedAt(), ); try std.testing.expectEqual( @as(?i64, 456), app.input_runtime.gestures.escapeClearArmedAt(), ); try std.testing.expect(!app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime inline scroll actions do not open the transcript viewer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
const cases = [_][]const u8{ "\x1b[<64;1;1M", "\x1b[<65;1;1M", "\x1b[5~", "\x1b[6~", }; for (cases) |bytes| { try feedRoutingBytes(&app, bytes); try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqual( transcript_presentation.Depth.inline_mode, app.shell.transcriptPresentationDepth(), ); try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); }}
test "app_input_runtime ctrl-o toggles full transcript while arrows preserve detail" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const out_path = "ctrl-o-routing-harness.out"; var sink = try tmp.dir.createFile(std.testing.io, out_path, .{ .read = true }); var sink_open = true; defer if (sink_open) sink.close(io_mod.getIo());
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.stdout_file = sink;
app.shell.render_requests.clearReason(.modal); try Runtime(RoutingFakeApp).handleByte(&app, 15, 4096, 100); try std.testing.expect(app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.terminal.alternate_mouse_tracking_active); try std.testing.expect(app.shell.fullTranscriptActive()); try std.testing.expect(app.shell.full_transcript.follow_tail); try std.testing.expect(app.shell.render_requests.hasReason(.modal)); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), );
try feedRoutingBytes(&app, "\x1b[C"); try std.testing.expect(app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), );
try feedRoutingBytes(&app, "\x1b[D"); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), );
try Runtime(RoutingFakeApp).handleByte(&app, 15, 4096, 100); try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.terminal.alternate_mouse_tracking_active); try std.testing.expect(!app.shell.fullTranscriptActive());
try feedRoutingBytes(&app, "\x1b[111;5u"); try std.testing.expect(app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.terminal.alternate_mouse_tracking_active); try std.testing.expect(app.shell.fullTranscriptActive());
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 'a', 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 'b', 4096, 100); try feedRoutingBytes(&app, "\x1b[D"); try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), ); try feedRoutingBytes(&app, "\x1b[C"); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), ); try std.testing.expect(app.terminal.fullTranscriptScreenActive());
try Runtime(RoutingFakeApp).handleByte(&app, 24, 4096, 100); try feedRoutingBytes(&app, "\x1b[120;5u"); try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.subagents.toggle_view_calls); try std.testing.expect(app.terminal.fullTranscriptScreenActive());
app.shell.render_requests.clearReason(.modal); app.shell.full_transcript.scroll_rows = 1; app.shell.full_transcript.follow_tail = true; try feedRoutingBytes(&app, "\x1b[5~"); try std.testing.expectEqual(@as(u32, 0), app.shell.full_transcript.scroll_rows); try std.testing.expect(!app.shell.full_transcript.follow_tail); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
app.shell.full_transcript.scroll_rows = std.math.maxInt(u32) - 1; try feedRoutingBytes(&app, "\x1b[6~"); try std.testing.expectEqual(std.math.maxInt(u32), app.shell.full_transcript.scroll_rows); try feedRoutingBytes(&app, "\x1b[<64;1;1M"); try std.testing.expectEqual(std.math.maxInt(u32) - 3, app.shell.full_transcript.scroll_rows); try feedRoutingBytes(&app, "\x1b[<65;1;1M"); try std.testing.expectEqual(std.math.maxInt(u32), app.shell.full_transcript.scroll_rows); try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items);
try feedRoutingBytes(&app, "\x1b[99;5u"); try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.terminal.alternate_mouse_tracking_active); try std.testing.expect(!app.shell.fullTranscriptActive()); try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expectEqual(@as(usize, 0), app.subagents.toggle_view_calls); }
{ var approval_app = try RoutingFakeApp.init(alloc); defer approval_app.deinit(); approval_app.shell.stdout_file = sink; try std.testing.expect(try approval_app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try Runtime(RoutingFakeApp).handleByte(&approval_app, 15, 4096, 100); try std.testing.expect(!approval_app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!approval_app.shell.fullTranscriptActive());
activateFullTranscriptForRoutingTest(&approval_app); approval_app.shell.full_transcript.scroll_rows = 10; try feedRoutingBytes(&approval_app, "\x1b[5~"); try feedRoutingBytes(&approval_app, "\x1b[<64;1;1M"); try std.testing.expectEqual(@as(u32, 10), approval_app.shell.full_transcript.scroll_rows); try std.testing.expect(approval_app.terminal.fullTranscriptScreenActive()); }
sink.close(io_mod.getIo()); sink_open = false;
var read_file = try tmp.dir.openFile(io_mod.getIo(), out_path, .{}); defer read_file.close(io_mod.getIo()); const bytes = try io_mod.readFileToEnd(alloc, &read_file, 1024); defer alloc.free(bytes); try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, bytes, "\x1b[?1049h")); try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, bytes, "\x1b[?1049l")); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, bytes, "\x1b[?1000h")); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, bytes, "\x1b[?1006h")); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, bytes, "\x1b[?1000l")); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, bytes, "\x1b[?1006l"));}
test "app_input_runtime skills catalog owns ctrl-o without opening another screen" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.openMenu(); app.terminal.alternate_screen_owner = .catalog_menu;
try Runtime(RoutingFakeApp).handleByte(&app, 15, 4096, 100);
try std.testing.expect(app.skills.menu.active); try std.testing.expect(app.terminal.catalogMenuScreenActive()); try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.shell.fullTranscriptActive());}
test "app_input_runtime skills catalog ignores ctrl-x" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.openMenu();
try Runtime(RoutingFakeApp).handleByte(&app, 24, 4096, 100);
try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.subagents.isViewActive()); try std.testing.expectEqual(@as(usize, 0), app.subagents.toggle_view_calls);}
test "app_input_runtime skills catalog owns the all-sessions shortcut" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.skills.openMenu();
try feedRoutingBytes(&app, "\x1b[114;9u");
try std.testing.expect(app.skills.menu.active); try std.testing.expect(!app.session_persistence.session_picker.active); try std.testing.expectEqual(@as(usize, 0), app.notice_body.items.len);}
test "app_input_runtime full transcript retains ownership while ctrl-x is inert" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app);
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 'a', 4096, 100); try std.testing.expectEqualStrings("a", app.input_runtime.edit_state.input.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(app.terminal.fullTranscriptScreenActive());
try Runtime(RoutingFakeApp).handleByte(&app, 'b', 4096, 100); try feedRoutingBytes(&app, "\x1b[D"); try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), );
try Runtime(RoutingFakeApp).handleByte(&app, 24, 4096, 100); try feedRoutingBytes(&app, "\x1b[120;5u"); try std.testing.expectEqualStrings("ab", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.subagents.toggle_view_calls); try std.testing.expect(!app.subagents.isViewActive()); try std.testing.expect(app.terminal.fullTranscriptScreenActive());}
test "app_input_runtime full transcript page wheel and alternate-scroll keys scroll the projection" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.full_transcript.scroll_rows = 10; app.shell.full_transcript.follow_tail = true;
app.shell.render_requests.clearReason(.modal); try feedRoutingBytes(&app, "\x1b[5~"); try std.testing.expectEqual(@as(u32, 0), app.shell.full_transcript.scroll_rows); try std.testing.expect(!app.shell.full_transcript.follow_tail); try std.testing.expect(app.shell.render_requests.hasReason(.modal));
try feedRoutingBytes(&app, "\x1b[6~"); try std.testing.expectEqual(@as(u32, 20), app.shell.full_transcript.scroll_rows);
try feedRoutingBytes(&app, "\x1b[<64;1;1M"); try std.testing.expectEqual(@as(u32, 17), app.shell.full_transcript.scroll_rows); try feedRoutingBytes(&app, "\x1b[<65;1;1M"); try std.testing.expectEqual(@as(u32, 20), app.shell.full_transcript.scroll_rows);
try feedRoutingBytes(&app, "\x1b[A"); try std.testing.expectEqual(@as(u32, 17), app.shell.full_transcript.scroll_rows); try feedRoutingBytes(&app, "\x1b[B"); try std.testing.expectEqual(@as(u32, 20), app.shell.full_transcript.scroll_rows);
try std.testing.expect(app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime ctrl-o opens full detail and closes it from each encoding" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var sink = try tmp.dir.createFile(std.testing.io, "full-transcript-toggle.log", .{ .read = true }); defer sink.close(io_mod.getIo());
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.stdout_file = sink;
try feedRoutingBytes(&app, "\x1b[111;5u");
try std.testing.expect(app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.terminal.alternate_mouse_tracking_active); try std.testing.expect(app.shell.fullTranscriptActive()); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.stdout_file = sink;
try Runtime(RoutingFakeApp).handleByte(&app, 15, 4096, 100); try feedRoutingBytes(&app, "\x1b[C"); try std.testing.expectEqual( transcript_presentation.Depth.full, app.shell.transcriptPresentationDepth(), ); try std.testing.expect(app.terminal.fullTranscriptScreenActive());
try Runtime(RoutingFakeApp).handleByte(&app, 15, 4096, 100); try std.testing.expectEqual( transcript_presentation.Depth.inline_mode, app.shell.transcriptPresentationDepth(), ); try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.stdout_file = sink; try feedRoutingBytes(&app, "hi");
try feedRoutingBytes(&app, "\x1b[C"); try Runtime(RoutingFakeApp).handleByte(&app, 15, 4096, 100);
try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.stdout_file = sink; try feedRoutingBytes(&app, "hi");
try feedRoutingBytes(&app, "\x1b[C"); try feedRoutingBytes(&app, "\x1b[111;5u");
try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.stdout_file = sink; try feedRoutingBytes(&app, "hi");
try feedRoutingBytes(&app, "\x1b[C"); try feedRoutingBytes(&app, "\x1b[27;5;111~");
try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.shell.stdout_file = sink; try feedRoutingBytes(&app, "hi");
try feedRoutingBytes(&app, "\x1b[27;5;111~");
try std.testing.expect(app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); }}
test "app_input_runtime full transcript ctrl-c closes the screen and preserves the composer" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var sink = try tmp.dir.createFile(std.testing.io, "full-transcript-interrupt.log", .{ .read = true }); defer sink.close(io_mod.getIo());
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.stdout_file = sink; try feedRoutingBytes(&app, "hi");
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100);
try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.stdout_file = sink; try feedRoutingBytes(&app, "hi");
try feedRoutingBytes(&app, "\x1b[99;5u");
try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit); }}
test "app_input_runtime raw escape closes full transcript without cancelling active work" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var sink = try tmp.dir.createFile(std.testing.io, "full-transcript-escape.log", .{ .read = true }); defer sink.close(io_mod.getIo());
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); app.shell.stdout_file = sink; app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0);
try std.testing.expect(!app.terminal.fullTranscriptScreenActive()); try std.testing.expect(!app.shell.fullTranscriptActive()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active);}
test "app_input_runtime approval prompt blocks full transcript key interception" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); app.shell.full_transcript.scroll_rows = 10;
try feedRoutingBytes(&app, "\x1b[5~");
try std.testing.expectEqual(@as(u32, 10), app.shell.full_transcript.scroll_rows); try std.testing.expect(app.terminal.fullTranscriptScreenActive());}
test "app_input_runtime full transcript malformed private CSI does not leak digits into composer" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); activateFullTranscriptForRoutingTest(&app);
try feedRoutingBytes(&app, "\x1b[?12"); app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0);
const draft = "CTRL_O_MALFORMED_ESCAPE_DRAFT"; try feedRoutingBytes(&app, draft); try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expectEqual(@as(usize, 1), app.submitted_prompt_count); try std.testing.expectEqualStrings(draft, app.submitted_prompt[0..app.submitted_prompt_len]);}
test "app_input_runtime stray paste end cannot finalize stale composer state" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.paste.buffer.appendSlice(alloc, "stale");
try feedRoutingBytes(&app, "\x1b[201~");
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqualStrings("stale", app.input_runtime.paste.buffer.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime false paste ends cannot finalize stale composer state" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.paste.buffer.appendSlice(alloc, "stale");
try feedRoutingBytes(&app, "\x1b[0201~"); try feedRoutingBytes(&app, "\x1b[201;1~");
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqualStrings("stale", app.input_runtime.paste.buffer.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime new paste traces and clears stale inactive state" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "stale-paste.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "input");
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); app.input_runtime.paste.decision_bytes = 7; try app.input_runtime.paste.buffer.appendSlice(alloc, "old");
try feedRoutingBytes(&app, "\x1b[200~");
try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.decision_bytes); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); debug_trace.shutdown();
const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); try std.testing.expect(std.mem.find(u8, trace, "decision prompt paste dropped bytes=7 reason=new_paste_stale_state") != null); try std.testing.expect(std.mem.find(u8, trace, "composer paste dropped bytes=3 reason=new_paste_stale_state") != null);}
test "app_input_runtime composer paste stays composer owned when prompt appears" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); try feedRoutingBytes(&app, "hi"); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); try feedRoutingBytes(&app, "\x1b[201~");
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqualStrings("hi", app.input_runtime.edit_state.input.items); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.worker.submitted_permission == null);}
test "app_input_runtime malformed escape bytes stay inert under composer-owned paste" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); try feedRoutingBytes(&app, "\x1b[9999999999999999999q\x03\x1b[A");
try std.testing.expectEqual(paste_framing.Owner.composer, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit);
try feedRoutingBytes(&app, "\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqualStrings("[9999999999999999999q[A", app.input_runtime.edit_state.input.items);}
test "app_input_runtime modal disappearance keeps decision-owned paste pinned" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "modal-disappear-paste.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "input");
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, "\x1b[200~"); app.approval_prompt.clear(app.alloc); try feedRoutingBytes(&app, "remaining" ++ "\x1b[200~"); try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);
try feedRoutingBytes(&app, "\x1b[201~"); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); debug_trace.shutdown();
const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); try std.testing.expect(std.mem.find(u8, trace, "decision prompt paste dropped bytes=15 reason=decision_prompt_active") != null);}
test "app_input_runtime exact paste start clears pending ctrl-c and esc gestures" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); armCtrlCExitForTest(&app.input_runtime, io_mod.milliTimestamp()); armEscapeClearForTest(&app.input_runtime, io_mod.milliTimestamp()); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, "\x1b[200~\x1b[201~"); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit);}
test "app_input_runtime idle Ctrl-C clears composer before second press exits" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "draft"); app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expect(app.should_exit); try std.testing.expectEqual( app_session_runtime.ResumeHandoffIntent.requested, app.session_persistence.resume_handoff_intent, );}
test "app_input_runtime pending Ctrl-C expires from state and rendering" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); armCtrlCExitForTest( &app.input_runtime, io_mod.milliTimestamp() - Runtime(RoutingFakeApp).ctrl_c_exit_window_ms, ); app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).flushPendingEscape(&app, 30);
try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
app.shell.render_requests.clearReason(.footer); try Runtime(RoutingFakeApp).flushPendingEscape(&app, 30); try std.testing.expect(!app.shell.render_requests.hasReason(.footer));
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit);}
test "app_input_runtime raw and Kitty Ctrl-C preserve double-press exit" { const sequences = [_][]const u8{ "\x03", "\x1b[99;5u" };
for (sequences) |sequence| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, sequence); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit);
try feedRoutingBytes(&app, sequence); try std.testing.expect(app.should_exit); }}
test "app_input_runtime active Ctrl-C clears drafts before cancelling work" { const sequences = [_][]const u8{ "\x03", "\x1b[99;5u" }; const drafts = [_]RoutingDraftKind{ .text, .paste, .image }; for (sequences) |sequence| { for (drafts) |draft| { for ([_]bool{ false, true }) |tool_active| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; if (tool_active) { _ = try app.shell.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = .{ .turn_id = 1, .call_id = "command" }, .reconciles_provisional_call_id = null, .tool_name = "shell", .activity_kind = .command, } }); } try seedRoutingDraftForGuard(&app, draft); app.shell.render_requests.clearReason(.footer);
try feedRoutingBytes(&app, sequence);
try std.testing.expect(!Runtime(RoutingFakeApp).draftHasState(&app)); try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(!app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
try feedRoutingBytes(&app, sequence);
try std.testing.expect(app.worker.cancel_requested); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(!app.should_exit); } } }}
test "app_input_runtime active Ctrl-C cancels stream and arms exit window" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100);
try std.testing.expect(!app.stream.active); try std.testing.expect(app.worker.cancel_requested); var rendered = try app.shell.prepareTranscriptSource(alloc, null); defer rendered.deinit(alloc); try std.testing.expect(std.mem.find(u8, rendered.bytes, "Cancelled") != null); try std.testing.expect(std.mem.find(u8, rendered.bytes, "What can fx do differently?") != null); try std.testing.expect(std.mem.find(u8, rendered.bytes, "System:") == null); try std.testing.expect(std.mem.find(u8, rendered.bytes, "Cancelling") == null); try std.testing.expectEqual(@as(usize, 0), app.notice_write_count); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmed()); try std.testing.expect(app.input_runtime.gestures.ctrlCExitArmedAt() != null); try std.testing.expect(!app.should_exit); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "automatic compaction cancellation is silent only while compaction is active" { const alloc = std.testing.allocator; for ([_]bool{ false, true }) |settled| { var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; const id = app.worker.compaction.begin(.automatic, 1, 1); app.worker.compaction.running(id, .summary); if (settled) app.worker.compaction.settle(id, .{ .outcome = .succeeded }, 2); try Runtime(RoutingFakeApp).resolveEscape(&app, true, 100); try std.testing.expect(!app.worker.cancel_requested); try Runtime(RoutingFakeApp).resolveEscape(&app, true, 101); try std.testing.expect(app.worker.cancel_requested); var rendered = try app.shell.prepareTranscriptSource(alloc, null); defer rendered.deinit(alloc); try std.testing.expectEqual(settled, std.mem.find(u8, rendered.bytes, "What can fx do differently?") != null); try std.testing.expectEqualStrings("", app.notice_body.items); }}
test "app_input_runtime active tool Escape presents final cancellation immediately" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; _ = try app.shell.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = .{ .turn_id = 1, .call_id = "command" }, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, } }); try std.testing.expectEqual(@as(usize, 1), app.shell.activeToolActivityCount()); app.shell.render_requests.clearReason(.footer);
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 100);
try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.input_runtime.gestures.escapeInterruptArmed());
try Runtime(RoutingFakeApp).resolveEscape(&app, true, 101);
try std.testing.expect(app.stream.active); try std.testing.expect(app.worker.cancel_requested); var rendered = try app.shell.prepareTranscriptSource(alloc, null); defer rendered.deinit(alloc); try std.testing.expect(std.mem.find(u8, rendered.bytes, "Cancelled") != null); try std.testing.expect(std.mem.find(u8, rendered.bytes, "What can fx do differently?") != null); try std.testing.expect(std.mem.find(u8, rendered.bytes, "System:") == null); try std.testing.expect(std.mem.find(u8, rendered.bytes, "Cancelling") == null); try std.testing.expectEqual(@as(usize, 1), app.shell.activeToolActivityCount()); try std.testing.expectEqualStrings("", app.notice_topic.items); try std.testing.expectEqualStrings("", app.notice_body.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime second Ctrl-C after active cancellation exits without duplicate notice" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100); try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100);
try std.testing.expect(app.should_exit); var rendered = try app.shell.prepareTranscriptSource(alloc, null); defer rendered.deinit(alloc); try std.testing.expectEqual( @as(usize, 1), std.mem.count(u8, rendered.bytes, "What can fx do differently?"), ); try std.testing.expect(std.mem.find(u8, rendered.bytes, "System:") == null);}
test "app_input_runtime pending Ctrl-C exits before repeating active cancellation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; app.worker.cancel_requested = true; armCtrlCExitForTest(&app.input_runtime, io_mod.milliTimestamp());
try Runtime(RoutingFakeApp).handleByte(&app, 3, 4096, 100);
try std.testing.expect(app.should_exit); try std.testing.expectEqualStrings("", app.transcript.items);}
test "app_input_runtime expired incomplete control sequence preserves approval" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); app.terminal_input_runtime.terminal_action_decoder.stage = 3; app.terminal_input_runtime.terminal_action_decoder.param = 20; app.terminal_input_runtime.terminal_action_decoder.param2 = 2; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true;
try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0);
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.worker.submitted_permission == null);}
test "app_input_runtime complete unknown control sequence preserves active operation" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true;
try feedRoutingBytes(&app, "\x1b[>0q");
try std.testing.expect(app.stream.active); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);}
test "app_input_runtime refreshes the pending escape deadline on decoder progress" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const old_started_ms = io_mod.milliTimestamp() - 1; app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.started_ms = old_started_ms; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true;
try Runtime(RoutingFakeApp).handleByte(&app, '[', 4096, 100);
try std.testing.expectEqual(@as(u8, 2), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expect(app.terminal_input_runtime.terminal_action_decoder.started_ms > old_started_ms); try std.testing.expect(app.terminal_input_runtime.terminal_action_decoder.cancel_pending);}
test "app_input_runtime expired mouse prefixes discard their tails without cancelling approval" { const Case = struct { prefix: []const u8, tail: []const u8, }; const cases = [_]Case{ .{ .prefix = "\x1b[<64;79", .tail = ";12M" }, .{ .prefix = "\x1b[M", .tail = "\x60\x2a\x25" }, };
for (cases) |case| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, case.prefix); app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0);
try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); try feedRoutingBytes(&app, case.tail); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); }}
test "app_input_runtime tail-less expired mouse recovery releases quietly" { const prefixes = [_][]const u8{ "\x1b[<64", "\x1b[M" };
for (prefixes) |prefix| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, prefix); app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested);
try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); }}
test "app_input_runtime fresh Escape restarts expired mouse recovery before cancelling" { const prefixes = [_][]const u8{ "\x1b[<64", "\x1b[M" };
for (prefixes) |prefix| { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.stream.active = true; try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" }));
try feedRoutingBytes(&app, prefix); app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested);
try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try std.testing.expectEqual(@as(u8, 1), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expect(app.terminal_input_runtime.terminal_action_decoder.cancel_pending); app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(RoutingFakeApp).flushPendingEscape(&app, 0); try std.testing.expect(!app.approval_prompt.isActive()); try std.testing.expect(app.worker.cancel_requested); }}
test "app_input_runtime fresh escape rearms generic decoder before paste start" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run npm test" })); app.terminal_input_runtime.terminal_action_decoder.stage = 3; app.terminal_input_runtime.terminal_action_decoder.param = 20; app.terminal_input_runtime.terminal_action_decoder.param2 = 2; const old_started_ms = io_mod.milliTimestamp() - 1; app.terminal_input_runtime.terminal_action_decoder.started_ms = old_started_ms; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = false; app.stream.active = true;
try Runtime(RoutingFakeApp).handleByte(&app, 0x1b, 4096, 100); try std.testing.expectEqual(@as(u8, 1), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expect(app.terminal_input_runtime.terminal_action_decoder.started_ms > old_started_ms); try std.testing.expectEqual(app.stream.active, app.terminal_input_runtime.terminal_action_decoder.cancel_pending);
try feedRoutingBytes(&app, "[200~"); try std.testing.expectEqual(paste_framing.Owner.decision_prompt, app.input_runtime.paste.owner);}
test "app_input_runtime plain vertical routes visible slash picker while stream is active" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; app.input_runtime.vertical_navigation.preferred_column = 9; try Runtime(FakeApprovalCancelApp).routePlainVertical(&app, .down, 1);
try std.testing.expectEqual(@as(usize, 1), app.input_runtime.picker.slash_completion_index); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.vertical_navigation.preferredColumn() == null);}
test "app_input_runtime plain vertical keeps composer precedence over wrapped slash picker" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit(); app.stream.active = false; app.shell.layout.cols = 8; app.shell.layout.content_bottom = 10; try app.input_runtime.edit_state.input.appendSlice(alloc, " /mo"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeApprovalCancelApp).routePlainVertical(&app, .up, -1); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.slash_completion_index); try std.testing.expect(app.input_runtime.edit_state.cursor < app.input_runtime.edit_state.input.items.len);
const cursor_after_plain = app.input_runtime.edit_state.cursor; app.input_runtime.picker.slash_completion_index = 7; try std.testing.expect(Runtime(FakeApprovalCancelApp).routeSlashCompletionMove(&app, 1)); try std.testing.expectEqual(cursor_after_plain, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.slash_completion_index);}
test "app_input_runtime tiny footer keeps wrapped slash picker visible" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit(); app.stream.active = false; app.shell.layout.cols = 8; app.shell.layout.content_bottom = 4; try app.input_runtime.edit_state.input.appendSlice(alloc, " /mo"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; app.input_runtime.picker.slash_completion_index = 7;
try Runtime(FakeApprovalCancelApp).routePlainVertical(&app, .down, 1); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.slash_completion_index);}
test "app_input_runtime plain vertical edge history resets preferred column exactly" { const alloc = std.testing.allocator; var top = FakeApprovalCancelApp{ .alloc = alloc }; defer top.deinit(); top.stream.active = false; try top.input_runtime.edit_state.input.appendSlice(alloc, "\nabcdef"); top.input_runtime.edit_state.cursor = top.input_runtime.edit_state.input.items.len; try Runtime(FakeApprovalCancelApp).routePlainVertical(&top, .up, -1); try std.testing.expectEqual(@as(usize, 0), top.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(?usize, 6), top.input_runtime.vertical_navigation.preferredColumn()); try Runtime(FakeApprovalCancelApp).routeModifiedHistory(&top, .up, -1); try std.testing.expectEqual(@as(usize, 0), top.input_runtime.edit_state.cursor); try std.testing.expect(top.input_runtime.vertical_navigation.preferredColumn() == null); try Runtime(FakeApprovalCancelApp).routePlainVertical(&top, .down, 1); try std.testing.expectEqual(@as(usize, 1), top.input_runtime.edit_state.cursor);
var bottom = FakeApprovalCancelApp{ .alloc = alloc }; defer bottom.deinit(); bottom.stream.active = false; try bottom.input_runtime.edit_state.input.appendSlice(alloc, "abcdef\n"); bottom.input_runtime.edit_state.cursor = 6; try Runtime(FakeApprovalCancelApp).routePlainVertical(&bottom, .down, 1); try std.testing.expectEqual(bottom.input_runtime.edit_state.input.items.len, bottom.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(?usize, 6), bottom.input_runtime.vertical_navigation.preferredColumn()); try Runtime(FakeApprovalCancelApp).routeModifiedHistory(&bottom, .down, 1); try std.testing.expectEqual(bottom.input_runtime.edit_state.input.items.len, bottom.input_runtime.edit_state.cursor); try std.testing.expect(bottom.input_runtime.vertical_navigation.preferredColumn() == null); try Runtime(FakeApprovalCancelApp).routePlainVertical(&bottom, .up, -1); try std.testing.expectEqual(@as(usize, 0), bottom.input_runtime.edit_state.cursor);}
test "approval cancellation uses one worker-owned terminal transition" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest( alloc, .{ .label = "shell.run npm test" }, ));
try Runtime(FakeApprovalCancelApp).cancelApprovalOperation(&app);
try std.testing.expect(app.worker.cancel_requested); try std.testing.expectEqual(@as(usize, 1), app.worker.order_len); try std.testing.expectEqual(ApprovalCancelEvent.request_cancel, app.worker.order[0]); try std.testing.expect(!app.approval_prompt.isActive()); try std.testing.expect(!app.stream.active); try std.testing.expect(app.pacer.cleared); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqual(transcript_runtime.RawEntryClass.unknown_raw, app.last_class); try std.testing.expect(app.shell.render_requests.hasReason(.modal));}
test "escape on a freeform draft arms then clears before cancelling the batch" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Inspect repo state", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "What next?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); app.question_prompt.moveChoice(-1); try std.testing.expect(app.question_prompt.isFreeformSelected()); _ = try app.question_prompt.apply(alloc, .{ .insert_ascii = 'k' });
app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(FakeApprovalCancelApp).flushPendingEscape(&app, 0);
try std.testing.expect(app.input_runtime.gestures.escapeClearArmed()); try std.testing.expectEqual(@as(usize, 1), app.question_prompt.freeformDraftLen()); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested);
app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(FakeApprovalCancelApp).flushPendingEscape(&app, 0);
try std.testing.expectEqual(@as(usize, 0), app.question_prompt.freeformDraftLen()); try std.testing.expect(app.question_prompt.isFreeformSelected()); try std.testing.expect(app.question_prompt.isActive()); try std.testing.expect(!app.input_runtime.gestures.escapeClearArmed()); try std.testing.expect(!app.worker.cancel_requested);
app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0; try Runtime(FakeApprovalCancelApp).flushPendingEscape(&app, 0);
try std.testing.expect(!app.question_prompt.isActive());}
test "bare escape during approval uses worker-owned cancellation" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest( alloc, .{ .label = "shell.run npm test" }, )); app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0;
try Runtime(FakeApprovalCancelApp).flushPendingEscape(&app, 0);
try std.testing.expect(app.worker.cancel_requested); try std.testing.expectEqual(ApprovalCancelEvent.request_cancel, app.worker.order[0]); try std.testing.expect(!app.approval_prompt.isActive()); try std.testing.expect(!app.stream.active);}
test "stale approval escape cancels and wakes the worker current request" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest( alloc, .{ .id = 41, .label = "stale A" }, )); app.worker.active_permission_request_id = 42; app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0;
try Runtime(RoutingFakeApp).flushPendingEscape( &app, 0, );
try std.testing.expect(app.worker.cancel_requested); try std.testing.expectEqual( ToolPermissionDecision.deny, app.worker.permission_response.?, ); try std.testing.expect(!app.approval_prompt.isActive()); try std.testing.expectEqual( worker_runtime.PermissionSubmissionResult.no_pending, app.worker.submitPermissionResponse( 41, permission_request.OwnedPermissionResponse.init(alloc, .always, null), ), ); try std.testing.expectEqual( ToolPermissionDecision.deny, app.worker.permission_response.?, ); try std.testing.expect(app.worker.submitted_permission == null);}
const routing_file_approval_preview_lines = [_]@import("../output/diff.zig").PreviewLine{ .{ .op = .deletion, .old_line = 2, .text = "before", }, .{ .op = .addition, .new_line = 2, .text = "after", }, };
fn routingFileApprovalRequest( id: u64,) permission_request.PermissionRequest { return .{ .id = id, .label = "file_mutation", .file = .{ .kind = .edit, .intent = .mutation, .preview = .{ .path = "note.txt", .lines = &routing_file_approval_preview_lines, .additions = 1, .deletions = 1, .truncated = false, }, .scope = .workspace_files, }, };}
fn installReadyRoutingFileApproval(app: *RoutingFakeApp) !void { try std.testing.expect(try app.approval_prompt.syncRequest( app.alloc, routingFileApprovalRequest(41), )); app.worker.active_permission_request_id = 41; app.shell.layout.rows = 24; app.shell.layout.cols = 96; app.shell.footer_viewport.has_frame = true; app.shell.footer_viewport.geometry = .{ .top = 1, .top_divider = 1, .input_base = 2, .bottom_divider = 13, .hint = 13, }; app.approval_screen.recordScreenCommit(41, .{ .request_id = 41, .rows = app.shell.layout.rows, .cols = app.shell.layout.cols, .file_identity_visible = true, .all_decision_controls_visible = true, .changed_or_notice_visible = true, .document_scrollable = true, });}
const ApprovalOwnershipBinding = struct { child_id: []const u8,};
const ApprovalOwnershipSubagents = struct { view_active: bool = true, child_id: []const u8 = "selected-child", presented_binding: ?ApprovalOwnershipBinding = null, card_binding: ?ApprovalOwnershipBinding = null,
pub fn isViewActive(self: *const ApprovalOwnershipSubagents) bool { return self.view_active; }
pub fn childRouteId(self: *const ApprovalOwnershipSubagents) ?[]const u8 { return self.child_id; }
pub fn mainApprovalBinding( self: *const ApprovalOwnershipSubagents, _: u64, ) ?ApprovalOwnershipBinding { return self.presented_binding; }
pub fn mainApprovalCardBinding( self: *const ApprovalOwnershipSubagents, _: u64, ) ?ApprovalOwnershipBinding { return self.card_binding; }};
test "app_input_runtime consumes legacy X10 reports during active file approval" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app); app.approval_screen.recordScreenCommit(41, .{ .request_id = 41, .rows = 24, .cols = 96, .file_identity_visible = true, .all_decision_controls_visible = true, .changed_or_notice_visible = true, .document_scrollable = true, }); app.approval_screen.document_scroll_rows = 3; app.stream.active = true;
try feedRoutingBytes(&app, "\x1b[M\x60\x2a\x25");
try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active); try std.testing.expectEqual(@as(usize, 6), app.approval_screen.document_scroll_rows); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app); app.stream.active = true;
try feedRoutingBytes(&app, "\x1b[M\x1b\x2a\x25");
try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); }}
test "app_input_runtime keeps in-progress SGR mouse reports past bare escape timeout" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app); app.approval_screen.document_scroll_rows = 3; app.stream.active = true;
try feedRoutingBytes(&app, "\x1b[<65;79;"); app.terminal_input_runtime.terminal_action_decoder.started_ms = io_mod.milliTimestamp() - 31;
try Runtime(RoutingFakeApp).flushPendingEscape(&app, 30); try feedRoutingBytes(&app, "12M");
try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(app.stream.active); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.stage); try std.testing.expectEqual(@as(u8, 0), app.terminal_input_runtime.terminal_action_decoder.mouse.sgr_bytes); try std.testing.expectEqual(@as(usize, 0), app.approval_screen.document_scroll_rows);}
test "primary composer pointer press places the caret without starting a drag selection" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.setText(alloc, "alpha beta"); _ = app.input_runtime.edit_state.selectAll(); app.shell.footer_viewport.has_frame = true; app.shell.footer_viewport.geometry = .{ .top = 21, .top_divider = 21, .input_base = 22, .input_first = 22, .input_window_first = 0, .bottom_divider = 23, .hint = 24, };
const prefix_cells: u16 = @intCast(visual_layout.inputPrefix(0).cell_width); var report: [48]u8 = undefined; const click = try std.fmt.bufPrint( &report, "\x1b[<0;{d};22M\x1b[<0;{d};22m", .{ prefix_cells + 4, prefix_cells + 4 }, ); try feedRoutingBytes(&app, click);
try std.testing.expectEqual(@as(usize, 3), app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.edit_state.selection_anchor == null); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "file approval enter waits for the matching committed modal frame" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app);
app.shell.render_requests.request(.modal); try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expect(app.approval_prompt.isActive());
app.shell.render_requests.clearReason(.modal); try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ); try std.testing.expectEqual( ToolPermissionDecision.once, app.worker.submitted_permission.?, ); try std.testing.expect(!app.approval_prompt.isActive());}
test "file approval digit submits the mapped choice when the modal is ready" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app);
try Runtime(RoutingFakeApp).handleByte( &app, '2', 4096, 100, ); try std.testing.expectEqual( ToolPermissionDecision.always, app.worker.submitted_permission.?, ); try std.testing.expect(!app.approval_prompt.isActive());}
test "file approval keeps the active review when feedback materialization fails" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app);
_ = try app.approval_prompt.decision.apply(alloc, .tab, true, null); _ = try app.approval_prompt.decision.apply( alloc, .{ .insert_ascii = 's' }, true, null, ); _ = try app.approval_prompt.decision.apply( alloc, .{ .insert_ascii = 'u' }, true, null, ); _ = try app.approval_prompt.decision.apply( alloc, .{ .insert_ascii = 'm' }, true, null, );
var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 }); app.alloc = failing.allocator(); defer app.alloc = alloc;
try std.testing.expectError( error.OutOfMemory, Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ), ); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expect(app.approval_prompt.isAmending()); try std.testing.expectEqualStrings("sum", app.approval_prompt.decision.selectedDraft()); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expectEqual(@as(usize, 1), app.affirmative_claim_count); try std.testing.expectEqual(@as(usize, 1), app.affirmative_release_count);}
test "approval submission transfers feedback to the worker without a local card" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest(alloc, .{ .label = "shell.run printf done", })); try Runtime(RoutingFakeApp).handleByte(&app, '\t', 4096, 100); for ("summarize the output") |byte| { try Runtime(RoutingFakeApp).handleByte(&app, byte, 4096, 100); }
try Runtime(RoutingFakeApp).handleByte(&app, '\r', 4096, 100);
try std.testing.expect(std.mem.find(u8, app.transcript.items, "summarize the output") == null); try std.testing.expectEqualStrings( "summarize the output", app.worker.submitted_permission_feedback[0..app.worker.submitted_permission_feedback_len], );}
test "byte-identical replacement waits for commit and rejects stale request input" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app);
app.worker.active_permission_request_id = 42; try std.testing.expect(try app.approval_prompt.syncRequest( alloc, routingFileApprovalRequest(42), )); app.shell.render_requests.request(.modal);
try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expectEqual( worker_runtime.PermissionSubmissionResult.stale, app.worker.submitPermissionResponse( 41, permission_request.OwnedPermissionResponse.init(alloc, .once, null), ), ); try std.testing.expect(app.worker.permission_response == null);
var attempt = (try app.shell.render_requests.beginAttempt()).?; try std.testing.expect(!approval_ui.fileApprovalAffirmativeReady( &app.shell, app.approval_prompt.projection().?, )); attempt.commit( 1_000, render_request.animation_interval_ms, false, ); app.approval_screen.recordScreenCommit(42, .{ .request_id = 42, .rows = app.shell.layout.rows, .cols = app.shell.layout.cols, .file_identity_visible = true, .all_decision_controls_visible = true, .changed_or_notice_visible = true, .document_scrollable = true, });
try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ); try std.testing.expectEqual( ToolPermissionDecision.once, app.worker.submitted_permission.?, ); try std.testing.expectEqual( ToolPermissionDecision.once, app.worker.permission_response.?, ); try std.testing.expect(!app.approval_prompt.isActive());}
test "file approval affirmative enter admits a pending raw resize" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app); app.approval_resize_interlock.noteResizeSignal();
try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ); try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expect(app.shell.render_requests.resize_dirty); try std.testing.expect(app.shell.render_requests.hasReason(.resize));}
test "resize signal after readiness blocks affirmative before worker submission" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app); app.inject_resize_before_affirmative_claim = true;
try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, );
try std.testing.expect(app.worker.submitted_permission == null); try std.testing.expect(app.approval_prompt.isActive()); try std.testing.expectEqual(@as(usize, 1), app.affirmative_claim_count); try std.testing.expectEqual(@as(usize, 0), app.affirmative_release_count); try std.testing.expect(app.approval_resize_interlock.resizePending());
try std.testing.expect(app.admitPendingApprovalResize()); app.shell.render_requests.completeResizeGeometry(false); var attempt = (try app.shell.render_requests.beginAttempt()).?; attempt.commit( 1_000, render_request.animation_interval_ms, false, ); try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, ); try std.testing.expectEqual( ToolPermissionDecision.once, app.worker.submitted_permission.?, ); try std.testing.expectEqual(@as(usize, 2), app.affirmative_claim_count); try std.testing.expectEqual(@as(usize, 1), app.affirmative_release_count);}
test "affirmative claim preserves resize raised during worker submission" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app); app.worker.resize_signal_on_submit = &app.approval_resize_interlock;
try Runtime(RoutingFakeApp).handleByte( &app, '\r', 4096, 100, );
try std.testing.expectEqual( ToolPermissionDecision.once, app.worker.submitted_permission.?, ); try std.testing.expectEqual(@as(usize, 1), app.affirmative_claim_count); try std.testing.expectEqual(@as(usize, 1), app.affirmative_release_count); try std.testing.expect(app.approval_resize_interlock.resizePending()); try std.testing.expect(app.admitPendingApprovalResize()); try std.testing.expect(app.shell.render_requests.hasReason(.resize));}
test "file approval denial remains actionable while modal work is pending" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try installReadyRoutingFileApproval(&app);
try Runtime(RoutingFakeApp).handleByte( &app, '3', 4096, 100, ); try std.testing.expectEqual( ToolPermissionDecision.deny, app.worker.submitted_permission.?, ); try std.testing.expect(!app.approval_prompt.isActive());}
test "bare escape trace captures approval interrupt context" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "escape-trace.log" }); defer alloc.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "interrupt");
var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit(); try std.testing.expect(try app.approval_prompt.syncRequest( alloc, .{ .label = "shell.run npm test" }, )); app.terminal_input_runtime.terminal_action_decoder.stage = 1; app.terminal_input_runtime.terminal_action_decoder.cancel_pending = true; app.terminal_input_runtime.terminal_action_decoder.started_ms = 0;
try Runtime(FakeApprovalCancelApp).flushPendingEscape(&app, 0); debug_trace.shutdown();
const trace = try readTraceFileForTest(alloc, trace_path); defer alloc.free(trace); try std.testing.expect(std.mem.find(u8, trace, "event=cancel_requested") != null); try std.testing.expect(std.mem.find(u8, trace, "source=input_approval") != null); try std.testing.expect(std.mem.find(u8, trace, "stream_active=true") != null); try std.testing.expect(std.mem.find(u8, trace, "active_streamed_text_response=false") != null); try std.testing.expect(std.mem.find(u8, trace, "pending_tool_call=true") != null); try std.testing.expect(std.mem.find(u8, trace, "approval_prompt=true") != null); try std.testing.expect(std.mem.find(u8, trace, "running_tool=false") != null);}
test "question resolution append does not add a second gap after prompt card" { const alloc = std.testing.allocator; var app = FakeApprovalCancelApp{ .alloc = alloc }; defer app.deinit();
const opts = [_]types.QuestionOption{ .{ .label = "Inspect repo state", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "What would you like to do next?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); try std.testing.expectEqual( question_prompt.QuestionInputEvent.all_decided, try app.question_prompt.apply(alloc, .submit), );
try Runtime(FakeApprovalCancelApp).submitQuestionBatch(&app);
try std.testing.expect(!std.mem.startsWith(u8, app.shell.transcript.items, "\n")); try std.testing.expect(std.mem.find(u8, app.shell.transcript.items, " 1) What would you like to do next?\n") != null); try std.testing.expect(std.mem.find(u8, app.shell.transcript.items, " Inspect repo state") != null);}
test "route recovery question submit does not write agent question transcript" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.worker.question_source = .route_recovery;
const opts = [_]types.QuestionOption{ .{ .label = "Change model", .description = null }, .{ .label = "Try again later", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Route failed after 3 attempts. What should fx do?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries); try std.testing.expectEqual( question_prompt.QuestionInputEvent.all_decided, try app.question_prompt.apply(alloc, .submit), );
try Runtime(RoutingFakeApp).submitQuestionBatch(&app);
try std.testing.expectEqual(@as(usize, 1), app.worker.submitted_question_count); try std.testing.expectEqualStrings( "Change model", app.worker.submitted_question_answers[0][0..app.worker.submitted_question_answer_lens[0]], ); try std.testing.expect(!app.question_prompt.isActive()); try std.testing.expectEqual(@as(usize, 0), countOccurrences(app.transcript.items, "Route failed after 3 attempts. What should fx do?"));}
test "route recovery question cancel stays local" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); app.worker.question_source = .route_recovery; app.stream.active = true;
const opts = [_]types.QuestionOption{ .{ .label = "Try again later", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Route failed. What should fx do?", .options = &opts }, }; try app.question_prompt.syncFrom(alloc, &entries);
try input_question_runtime.QuestionRuntime(RoutingFakeApp).cancelQuestionPrompt(&app);
try std.testing.expect(app.worker.question_cancelled); try std.testing.expect(!app.worker.cancel_requested_when_question_cancelled); try std.testing.expect(!app.worker.cancel_requested); try std.testing.expect(!app.question_prompt.isActive()); try std.testing.expect(!app.stream.active); try std.testing.expectEqual(@as(usize, 0), countOccurrences(app.transcript.items, "Route failed. What should fx do?"));}
test "app_input_runtime submits multi-question answers in entry order" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
const first_options = [_]types.QuestionOption{ .{ .label = "First", .description = null }, .{ .label = "Second", .description = null }, }; const second_options = [_]types.QuestionOption{ .{ .label = "Third", .description = null }, .{ .label = "Fourth", .description = null }, }; const entries = [_]types.QuestionBatchEntry{ .{ .question = "Choose the first answer", .options = &first_options }, .{ .question = "Choose the second answer", .options = &second_options }, }; try app.question_prompt.syncFrom(alloc, &entries);
try feedRoutingBytes(&app, "\r2");
try std.testing.expectEqual(@as(usize, 2), app.worker.submitted_question_count); try std.testing.expectEqualStrings( "First", app.worker.submitted_question_answers[0][0..app.worker.submitted_question_answer_lens[0]], ); try std.testing.expectEqualStrings( "Fourth", app.worker.submitted_question_answers[1][0..app.worker.submitted_question_answer_lens[1]], ); try std.testing.expect(!app.question_prompt.isActive()); try std.testing.expectEqual( @as(usize, 1), countOccurrences(app.transcript.items, "Choose the first answer"), ); try std.testing.expectEqual( @as(usize, 1), countOccurrences(app.transcript.items, " Fourth"), );}
const FakeSubmitApp = struct { pub const input_byte_limit: usize = 4096;
alloc: std.mem.Allocator, workspace_root: []const u8 = "/tmp/workspace", input_runtime: core_input_runtime.Runtime = .{}, prompt_history: prompt_history_runtime.PromptHistoryRuntime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, stream: types.StreamState = .{}, pacer: struct { pending: bool = false, completed_assistant_presentation_tail: bool = false,
pub fn hasPending(self: *const @This()) bool { return self.pending; }
pub fn hasCompletedAssistantPresentationTail(self: *const @This()) bool { return self.completed_assistant_presentation_tail; } } = .{}, worker: struct { hold_available: bool = false, held: bool = false, queued_turn_id: ?u64 = null, queued_images: []types.ImageAttachment = &.{}, queued_images_alloc: ?std.mem.Allocator = null,
pub fn queuedPromptCount(_: *@This()) usize { return 0; }
pub fn tryHoldTurnStart(self: *@This()) bool { if (!self.hold_available or self.held) return false; self.held = true; return true; }
pub fn releaseTurnStartHold(self: *@This()) void { self.held = false; }
pub fn removeQueuedPrompt( self: *@This(), _: std.mem.Allocator, turn_id: u64, retained_images: []const types.ImageAttachment, ) bool { if (self.queued_turn_id != turn_id) return false; image_attachments.deleteUnreferencedImageSnapshots( self.queued_images, retained_images, ); self.freeQueuedImageMetadata(); self.queued_turn_id = null; return true; }
pub fn clearQueuedPrompts( self: *@This(), _: std.mem.Allocator, retained_images: []const types.ImageAttachment, ) void { image_attachments.deleteUnreferencedImageSnapshots( self.queued_images, retained_images, ); self.freeQueuedImageMetadata(); self.queued_turn_id = null; }
fn freeQueuedImageMetadata(self: *@This()) void { if (self.queued_images_alloc) |alloc| { types.freeImageAttachmentSlice(alloc, self.queued_images); } self.queued_images = &.{}; self.queued_images_alloc = null; }
pub fn clearQueuedPromptsForSessionTransition( self: *@This(), alloc: std.mem.Allocator, _: u64, retained_images: []const types.ImageAttachment, ) void { self.clearQueuedPrompts(alloc, retained_images); }
pub fn activeTurnId(_: *const @This()) u64 { return 0; }
pub fn requestCancel(_: *@This()) void {} } = .{}, submission: input_submit_runtime.State = .{}, shell: struct { render_requests: render_request.RenderRequestState = .{}, full_transcript_active: bool = false, layout: types.Layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, },
pub fn fullTranscriptActive(self: *const @This()) bool { return self.full_transcript_active; } } = .{}, next_image_id_counter: usize = 1, transcript: std.ArrayList(u8) = .empty, last_command: ?[]u8 = null, last_prompt: ?[]u8 = null, last_images: []types.ImageAttachment = &.{}, last_skill_tokens: std.ArrayList(registered_entities.SkillTokenSpan) = .empty, notice_topic: std.ArrayList(u8) = .empty, notice_body: std.ArrayList(u8) = .empty, notice_tone: types.NoticeTone = .information, notice_visibility: types.NoticeVisibility = .compact_and_full, input_len_at_command: usize = 0, command_count: usize = 0, fail_notice: bool = false, prompt_admitted: bool = true, queue_admitted: bool = true, queue_accept_count: usize = 0, preflight_count: usize = 0, notice_count: usize = 0, capture_count: usize = 0, capture_error_id: ?usize = null, capture_error: ?anyerror = null, fail_enqueue_after_snapshot: bool = false, fail_pending_finalization: bool = false, fail_command_after_pending_clear: bool = false, snapshot_dir: ?[]const u8 = null,
pub fn slashRegistry(_: *const FakeSubmitApp) command_specs.SlashRegistry { return routing_test_slash_registry; }
fn deinit(self: *FakeSubmitApp) void { if (self.submission.pending) |*pending| pending.deinit(self.alloc); self.submission.pending = null; self.prompt_history.deinit(self.alloc); self.clearPendingImages(); self.pending_images.deinit(self.alloc); self.input_runtime.deinit(self.alloc); self.transcript.deinit(self.alloc); self.notice_topic.deinit(self.alloc); self.notice_body.deinit(self.alloc); if (self.last_command) |text| self.alloc.free(text); if (self.last_prompt) |text| self.alloc.free(text); types.freeImageAttachmentSlice(self.alloc, self.last_images); self.clearLastSkillTokens(); self.last_skill_tokens.deinit(self.alloc); self.worker.freeQueuedImageMetadata(); }
pub fn writeDomainNotice(self: *FakeSubmitApp, notice: types.SemanticNotice, _: bool) !void { if (self.fail_notice) return error.InjectedTranscriptFailure; self.notice_count += 1; self.notice_topic.clearRetainingCapacity(); self.notice_body.clearRetainingCapacity(); try self.notice_topic.appendSlice(self.alloc, notice.topic); try self.notice_body.appendSlice(self.alloc, notice.body); self.notice_tone = notice.tone; self.notice_visibility = notice.visibility; }
pub fn clearPendingImages(self: *FakeSubmitApp) void { self.input_runtime.entities.image_tokens.clearRetainingCapacity(); image_attachments.discardImageSnapshots(self.alloc, self.pending_images.items); for (self.pending_images.items) |image| types.freeImageAttachment(self.alloc, image); self.pending_images.clearRetainingCapacity(); }
pub fn releasePendingImages(self: *FakeSubmitApp) void { self.input_runtime.entities.image_tokens.clearRetainingCapacity(); for (self.pending_images.items) |image| types.freeImageAttachment(self.alloc, image); self.pending_images.clearRetainingCapacity(); }
pub fn captureImageAttachment(self: *FakeSubmitApp, attachment: *types.ImageAttachment) !void { self.capture_count += 1; if (self.capture_error_id == attachment.id) { return self.capture_error orelse error.ImageTooLarge; } const snapshot_dir = self.snapshot_dir orelse return; try image_attachments.captureImageSnapshot(self.alloc, attachment, snapshot_dir); }
pub fn handleCommand(self: *FakeSubmitApp, text: []const u8) !void { if (self.last_command) |old| self.alloc.free(old); self.last_command = try self.alloc.dupe(u8, text); self.command_count += 1; self.input_len_at_command = self.input_runtime.edit_state.input.items.len; if (std.mem.eql(u8, text, "/images clear")) { self.clearPendingImages(); if (self.fail_command_after_pending_clear) return error.InjectedCommandFailure; } // Attach through the production handler so routing tests observe real placeholder // insertion, id allocation, and snapshot capture rather than a stub. const attach_prefix: ?[]const u8 = if (std.mem.startsWith(u8, text, "/image ")) "/image " else if (std.mem.startsWith(u8, text, "/img ")) "/img " else null; if (attach_prefix) |prefix| { try image_commands.Commands(FakeSubmitApp).attachPath(self, text[prefix.len..]); } }
pub fn ensurePromptCredential(self: *FakeSubmitApp) !bool { self.preflight_count += 1; return self.prompt_admitted; }
pub fn adoptPendingUserPrompt( _: *FakeSubmitApp, _: *const input_submit_runtime.PendingPromptDraft, ) !void {}
pub fn finalizePendingSubmission( self: *FakeSubmitApp, draft: *const input_submit_runtime.PendingPromptDraft, ) !void { if (self.fail_pending_finalization) { return error.InjectedPendingFinalizationFailure; } self.worker.freeQueuedImageMetadata(); self.worker.queued_images = try types.dupeImageAttachmentSlice( self.alloc, draft.images, ); self.worker.queued_images_alloc = self.alloc; self.worker.queued_turn_id = draft.turn_id; }
pub fn enqueuePrompt(self: *FakeSubmitApp, text: []const u8) !bool { return self.enqueuePromptWithSkillBindings(text, &.{}); }
pub fn enqueuePromptWithSkillBindings( self: *FakeSubmitApp, text: []const u8, skill_tokens: []const registered_entities.SkillTokenSpan, ) !bool { if (!self.queue_admitted) return false; const prompt_copy = try self.alloc.dupe(u8, text); errdefer self.alloc.free(prompt_copy); const images_copy = try types.dupeImageAttachmentSlice(self.alloc, self.pending_images.items); errdefer types.freeImageAttachmentSlice(self.alloc, images_copy); if (self.fail_enqueue_after_snapshot) return error.InjectedEnqueueFailure; if (self.last_prompt) |old| self.alloc.free(old); self.last_prompt = prompt_copy; types.freeImageAttachmentSlice(self.alloc, self.last_images); self.last_images = images_copy; self.clearLastSkillTokens(); errdefer self.clearLastSkillTokens(); try self.last_skill_tokens.ensureTotalCapacity(self.alloc, skill_tokens.len); for (skill_tokens) |token| { const name = try self.alloc.dupe(u8, token.name); var appended = false; errdefer if (!appended) self.alloc.free(name); const path = try self.alloc.dupe(u8, token.path); errdefer if (!appended) self.alloc.free(path); self.last_skill_tokens.appendAssumeCapacity(.{ .raw_start = token.raw_start, .raw_end = token.raw_end, .name = name, .path = path, }); appended = true; } self.queue_accept_count += 1; return true; }
pub fn nextImageId(self: *FakeSubmitApp) usize { return image_attachments.allocateImageId(&self.next_image_id_counter); }
pub fn peekNextImageId(self: *const FakeSubmitApp) usize { return self.next_image_id_counter; }
fn clearLastSkillTokens(self: *FakeSubmitApp) void { for (self.last_skill_tokens.items) |token| { self.alloc.free(@constCast(token.name)); self.alloc.free(@constCast(token.path)); } self.last_skill_tokens.clearRetainingCapacity(); }};
const FrameSubmitApp = struct { alloc: std.mem.Allocator, workspace_root: []const u8 = "/tmp/workspace", input_runtime: core_input_runtime.Runtime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, next_image_id_counter: usize = 1, shell: transcript_runtime.TranscriptRuntime, metrics: types.Metrics = .{}, command_seen: bool = false,
pub fn slashRegistry(_: *const FrameSubmitApp) command_specs.SlashRegistry { return routing_test_slash_registry; }
fn deinit(self: *FrameSubmitApp) void { self.clearPendingImages(); self.pending_images.deinit(self.alloc); self.input_runtime.deinit(self.alloc); self.shell.deinit(self.alloc); }
pub fn clearPendingImages(self: *FrameSubmitApp) void { self.input_runtime.entities.image_tokens.clearRetainingCapacity(); for (self.pending_images.items) |image| types.freeImageAttachment(self.alloc, image); self.pending_images.clearRetainingCapacity(); }
pub fn handleCommand(self: *FrameSubmitApp, _: []const u8) !void { self.command_seen = true; try self.writeDomainNotice(.{ .topic = "system", .tone = .information, .body = "command result", }, true); }
pub fn writeDomainNotice(self: *FrameSubmitApp, notice: types.SemanticNotice, record: bool) !void { try self.shell.writeNotice(self.alloc, &self.metrics, notice, record); }
pub fn enqueuePrompt(_: *FrameSubmitApp, _: []const u8) !bool { return true; }
pub fn nextImageId(self: *FrameSubmitApp) usize { return image_attachments.allocateImageId(&self.next_image_id_counter); }
pub fn peekNextImageId(self: *const FrameSubmitApp) usize { return self.next_image_id_counter; }
pub fn captureImageAttachment(_: *FrameSubmitApp, _: *types.ImageAttachment) !void { return error.MissingImageSnapshot; }};
fn writeTestImage(tmp: *std.testing.TmpDir, name: []const u8) !void { var file = try tmp.dir.createFile(std.testing.io, name, .{}); defer file.close(std.testing.io); try file.writeStreamingAll(std.testing.io, "\x89PNG\r\n\x1a\nrest");}
fn realTmpPath(alloc: std.mem.Allocator, tmp: *std.testing.TmpDir, name: []const u8) ![]u8 { return io_mod.dirRealpathAlloc(alloc, tmp.dir, name);}
fn countTestSnapshotFiles(path: []const u8) !usize { var dir = std.Io.Dir.openDirAbsolute(std.testing.io, path, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => return 0, else => return err, }; defer dir.close(std.testing.io); var iterator = dir.iterate(); var count: usize = 0; while (try iterator.next(std.testing.io)) |entry| { if (entry.kind == .file or entry.kind == .sym_link) count += 1; } return count;}
fn appendOwnedPendingImage(app: *FakeSubmitApp, id: usize, path: []const u8) !void { const path_copy = try app.alloc.dupe(u8, path); errdefer app.alloc.free(path_copy); const media_type = try app.alloc.dupe(u8, "image/png"); errdefer app.alloc.free(media_type); try app.pending_images.append(app.alloc, .{ .id = id, .path = path_copy, .media_type = media_type, });}
fn appendImageTokenForPlaceholderAt( app: *FakeSubmitApp, id: usize, raw_start: usize,) !void { var placeholder_buf: [32]u8 = undefined; const placeholder = try image_attachments.formatImagePlaceholder( &placeholder_buf, id, ); try app.input_runtime.entities.image_tokens.append(app.alloc, .{ .id = id, .span = .{ .raw_start = raw_start, .raw_end = raw_start + placeholder.len, }, });}
fn countOccurrences(haystack: []const u8, needle: []const u8) usize { if (needle.len == 0) return 0; var count: usize = 0; var start: usize = 0; while (std.mem.find(u8, haystack[start..], needle)) |relative| { count += 1; start += relative + needle.len; } return count;}
test "composer shortcut line delete handles decoded and raw mutations" { const alloc = std.testing.allocator;
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try primeComposerHistoryForTest(RoutingFakeApp, &app, "draft"); try app.input_runtime.textReplacementState().replace(alloc, "alpha\nleftMIDright\ngamma"); app.input_runtime.edit_state.cursor = "alpha\nleftMID".len; try app.input_runtime.picker.beginModelPickerFlow(alloc, "openai/gpt-5", 3, true, .fast); app.input_runtime.picker.file_completion_index = 4;
_ = try Runtime(RoutingFakeApp).routeResolvedEscapeAction( &app, .delete_to_line_start, .delete_to_line_start, null, null, false, 4096, ); try std.testing.expectEqualStrings("alpha\nright\ngamma", app.input_runtime.edit_state.input.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqual(picker_state.ModelPickerStage.model, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.picker.file_completion_index); try std.testing.expectEqual(@as(?usize, 0), app.input_runtime.composer_history.activeIndex()); try std.testing.expectEqualStrings("draft", app.input_runtime.composer_history.draftText().?); }
{ var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "alpha\nleftMIDright\ngamma"); app.input_runtime.edit_state.cursor = "alpha\nleftMID".len;
const action = test_ui_input.shortcutFromControlByte(11); try std.testing.expectEqual(@as(?input_action.ShortcutAction, .delete_to_line_end), action); try Runtime(RoutingFakeApp).handleByte(&app, 11, 4096, 100); try std.testing.expectEqualStrings("alpha\nleftMID\ngamma", app.input_runtime.edit_state.input.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); }}
test "typed composer byte replaces a semantic selection and remains undoable" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try app.input_runtime.textReplacementState().replace(alloc, "(alpha beta)"); try Runtime(RoutingFakeApp).routeComposerShortcutAction( &app, .{ .move = .{ .kind = .word_left, .extend_selection = true } }, 4096, ); try Runtime(RoutingFakeApp).handleByte(&app, 'X', 4096, 100); try std.testing.expectEqualStrings("(alpha X", app.input_runtime.edit_state.input.items);
try Runtime(RoutingFakeApp).routeComposerShortcutAction(&app, .undo, 4096); try std.testing.expectEqualStrings("(alpha beta)", app.input_runtime.edit_state.input.items);}
test "composer shortcut line delete preserves no-op picker redraw and metadata state" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); defer app.clearPendingImages();
try app.input_runtime.edit_state.input.appendSlice(alloc, "[Pasted text #7, 1 line]\n[Image #8]"); try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "pasted"), .line_count = 1, }); try app.pending_images.append(alloc, .{ .id = 8, .path = try alloc.dupe(u8, "/tmp/8.png"), .media_type = try alloc.dupe(u8, "image/png"), }); try app.input_runtime.picker.beginModelPickerFlow(alloc, "openai/gpt-5", 3, true, .effort); app.input_runtime.edit_state.cursor = 0;
const line_start_action = test_ui_input.shortcutFromControlByte(21); try std.testing.expectEqual(@as(?input_action.ShortcutAction, .delete_to_line_start), line_start_action); try Runtime(RoutingFakeApp).handleByte(&app, 21, 4096, 100); try std.testing.expect(!app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqual(picker_state.ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings("openai/gpt-5", app.input_runtime.picker.model_picker_pending_model.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len);
app.shell.render_requests.request(.footer); _ = try Runtime(RoutingFakeApp).routeResolvedEscapeAction( &app, .delete_to_line_start, .delete_to_line_start, null, null, false, 4096, ); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqual(picker_state.ModelPickerStage.effort, app.input_runtime.picker.model_picker_stage);
app.shell.render_requests.clearReason(.footer); try app.input_runtime.picker.beginModelPickerFlow(alloc, "openai/gpt-5", 4, true, .fast); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; _ = try Runtime(RoutingFakeApp).routeResolvedEscapeAction( &app, .delete_to_line_end, .delete_to_line_end, null, null, false, 4096, ); try std.testing.expect(!app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqual(picker_state.ModelPickerStage.fast, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqualStrings("openai/gpt-5", app.input_runtime.picker.model_picker_pending_model.items);}
test "composer shortcut line delete joins at non-final line end and rejects unknown bytes" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "alpha\nbeta"); try app.input_runtime.kill_ring.text.appendSlice(alloc, "existing kill"); app.input_runtime.edit_state.cursor = "alpha".len;
const action = test_ui_input.shortcutFromControlByte(11); try std.testing.expectEqual(@as(?input_action.ShortcutAction, .delete_to_line_end), action); try Runtime(RoutingFakeApp).handleByte(&app, 11, 4096, 100); try std.testing.expectEqualStrings("\n", app.input_runtime.kill_ring.text.items); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expectEqual(@as(?input_action.ShortcutAction, null), test_ui_input.shortcutFromControlByte('x')); try std.testing.expectEqualStrings("alphabeta", app.input_runtime.edit_state.input.items);}
test "composer shortcut yank commits fresh image ids only after insertion" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "source.png"); const source_path = try realTmpPath(alloc, &tmp, "source.png"); defer alloc.free(source_path); const snapshot_dir = try realTmpPath(alloc, &tmp, "."); defer alloc.free(snapshot_dir);
var app = try RoutingFakeApp.init(alloc); defer app.deinit(); defer app.clearPendingImages();
try app.input_runtime.edit_state.input.appendSlice(alloc, "[Image #8]"); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 8, .span = .{ .raw_start = 0, .raw_end = "[Image #8]".len }, }); var attachment = try image_attachments.loadImageAttachment(alloc, source_path); var attachment_owned = true; errdefer if (attachment_owned) { image_attachments.discardImageAttachment(alloc, attachment); }; attachment.id = 8; try image_attachments.captureImageSnapshot(alloc, &attachment, snapshot_dir); try app.pending_images.append(alloc, attachment); attachment_owned = false; app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; app.next_image_id_counter = 9;
try Runtime(RoutingFakeApp).handleByte(&app, 21, 4096, 100); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 9), app.next_image_id_counter);
try Runtime(RoutingFakeApp).handleByte(&app, 25, 4096, 100); try std.testing.expectEqualStrings("[Image #9]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 9), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 10), app.next_image_id_counter);
try Runtime(RoutingFakeApp).handleByte(&app, 25, 4096, 100); try std.testing.expectEqualStrings( "[Image #9][Image #10]", app.input_runtime.edit_state.input.items, ); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 9), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 10), app.pending_images.items[1].id); try std.testing.expectEqual(@as(usize, 11), app.next_image_id_counter); try std.testing.expect( image_attachments.imageAttachmentsSortedById(app.pending_images.items), );
try Runtime(RoutingFakeApp).handleByte(&app, 21, 4096, 100); try app.input_runtime.textReplacementState().replace(alloc, "full"); try Runtime(RoutingFakeApp).routeComposerShortcutAction(&app, .yank, 4); try std.testing.expectEqualStrings("full", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 11), app.next_image_id_counter);}
test "app_input_runtime submit resolves slash completion through core command specs" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/he"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/help", app.last_command.?); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime idle submit commits its frame before credential preflight" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.worker.hold_available = true; try app.input_runtime.edit_state.input.appendSlice(alloc, "pending first"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.submission.pending != null); try std.testing.expectEqualStrings("pending first", app.submission.pending.?.draft.prompt); try std.testing.expectEqual( input_submit_runtime.PendingPhase.awaiting_frame, app.submission.pending.?.phase, ); try std.testing.expect(app.worker.held); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expect(app.shell.render_requests.hasReason(.transcript)); try std.testing.expect(app.shell.render_requests.hasReason(.footer));
input_submit_runtime.SubmitRuntime(FakeSubmitApp).noteCommittedFrame(&app); input_submit_runtime.SubmitRuntime(FakeSubmitApp).collectPendingSubmissionFacts(&app);
try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expectEqual( input_submit_runtime.PendingPhase.queued, app.submission.pending.?.phase, );}
test "app_input_runtime second Enter preserves the newer draft until pending acknowledgement" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.worker.hold_available = true; try app.input_runtime.edit_state.input.appendSlice(alloc, "pending first"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try Runtime(FakeSubmitApp).submit(&app, 100);
try app.input_runtime.edit_state.input.appendSlice(alloc, "newer draft"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("pending first", app.submission.pending.?.draft.prompt); try std.testing.expectEqualStrings("newer draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.command_count); try std.testing.expectEqual(@as(usize, 0), app.capture_count); try std.testing.expect(app.worker.held);
app.input_runtime.inputResetState().clearCurrent(alloc); try app.input_runtime.edit_state.input.appendSlice(alloc, "/help"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/help", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.command_count);}
test "app_input_runtime recalls a submitted slash command with history previous" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/he"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings( "/help", app.input_runtime.composer_history.entryText(0).?, );
try input_completion_runtime.CompletionRuntime(FakeSubmitApp).navigatePromptHistory( &app, -1, );
try std.testing.expectEqualStrings("/help", app.input_runtime.edit_state.input.items);}
test "app_input_runtime submit preserves a dismissed slash query" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/he"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; app.input_runtime.picker.dismissInlinePicker(.slash);
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings("/he", app.last_prompt.?);}
test "app_input_runtime routes an unmatched visible slash query as an unknown command" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/foo"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/foo", app.last_command.?); try std.testing.expect(app.last_prompt == null);}
test "app_input_runtime preserves an unmatched slash prompt with arguments" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/output quiet"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/output quiet", app.last_prompt.?); try std.testing.expect(app.last_command == null);}
test "app_input_runtime submits a slash selection with wrapped input" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.shell.layout.cols = 8; app.shell.layout.content_bottom = 10;
try app.input_runtime.edit_state.input.appendSlice(alloc, " /he"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/help", app.last_command.?); try std.testing.expect(app.last_prompt == null);}
test "app_input_runtime clears slash input before command output" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/sk"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/skills", app.last_command.?); try std.testing.expectEqual(@as(usize, 0), app.input_len_at_command); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime local commands clear unrelated pending images" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, "/help"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/help", app.last_command.?); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len);}
test "app_input_runtime routes pending image list locally and preserves its placeholder" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, "[Image #1]/images"); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 1, .span = .{ .raw_start = 0, .raw_end = "[Image #1]".len }, }); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const command = app.last_command orelse return error.TestExpectedImageCommand; try std.testing.expectEqualStrings("/images", command); try std.testing.expectEqual(@as(usize, "[Image #1]".len), app.input_len_at_command); try std.testing.expectEqualStrings("[Image #1]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, "[Image #1]".len), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings( "/images", app.input_runtime.composer_history.entryText(0).?, ); try std.testing.expect(app.last_prompt == null);}
test "app_input_runtime routes image-prefixed slash command with reversed pending image order" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try appendOwnedPendingImage(&app, 2, "/tmp/two.png"); try appendOwnedPendingImage(&app, 1, "/tmp/one.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, "[Image #2] [Image #1] /images"); try appendImageTokenForPlaceholderAt(&app, 2, 0); try appendImageTokenForPlaceholderAt(&app, 1, "[Image #2] ".len); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/images", app.last_command.?); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items[1].id);}
test "app_input_runtime clears pending image placeholders after image command state clears" { const alloc = std.testing.allocator;
for ([_]bool{ false, true }) |fail_after_clear| { var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false, .fail_command_after_pending_clear = fail_after_clear, }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, "[Image #1]/images clear"); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 1, .span = .{ .raw_start = 0, .raw_end = "[Image #1]".len }, }); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
if (fail_after_clear) { try std.testing.expectError( error.InjectedCommandFailure, Runtime(FakeSubmitApp).submit(&app, 100), ); } else { try Runtime(FakeSubmitApp).submit(&app, 100); }
const command = app.last_command orelse return error.TestExpectedImageCommand; try std.testing.expectEqualStrings("/images clear", command); try std.testing.expectEqual(@as(usize, "[Image #1]".len), app.input_len_at_command); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expectEqual( @as(usize, 1), app.input_runtime.composer_history.count(), ); try std.testing.expectEqualStrings( "/images clear", app.input_runtime.composer_history.entryText(0).?, ); try std.testing.expect(app.last_prompt == null); }}
test "app_input_runtime keeps mixed unknown and non-images attachment suffixes as prompts" { const alloc = std.testing.allocator; const cases = [_][]const u8{ "describe [Image #1]/images", "[Image #2]/images", };
for (cases) |input| { var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings(input, app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expectEqual(@as(usize, 1), app.queue_accept_count); }}
test "app_input_runtime routes registered commands behind pending images" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, "[Image #1]/help"); try appendImageTokenForPlaceholderAt(&app, 1, 0); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("/help", app.last_command.?); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqualStrings("[Image #1]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count);}
test "app_input_runtime preserves pending image draft when model selection preflight rejects" { const alloc = std.testing.allocator; const input = "[Image #1]/model claude"; var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); app.next_image_id_counter = 2; try app.input_runtime.edit_state.input.appendSlice(alloc, input); try appendImageTokenForPlaceholderAt(&app, 1, 0); app.input_runtime.edit_state.cursor = input.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expect(app.last_command == null); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(input.len, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.image_tokens.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items[0].id); try std.testing.expectEqualStrings("/tmp/image.png", app.pending_images.items[0].path); try std.testing.expectEqual(@as(usize, 2), app.next_image_id_counter); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count);}
test "app_input_runtime routes image slash commands before inline attachment extraction" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "slash-image.png"); const image_path = try realTmpPath(alloc, &tmp, "slash-image.png"); defer alloc.free(image_path);
for ([_][]const u8{ "/image", "/img" }) |command| { var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit(); const input = try std.fmt.allocPrint(alloc, "{s} {s}", .{ command, image_path }); defer alloc.free(input); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const last_command = app.last_command orelse return error.TestExpectedImageCommand; try std.testing.expectEqualStrings(input, last_command); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 0), app.last_images.len); }}
test "app_input_runtime keeps image-command near misses on the prompt path" { const alloc = std.testing.allocator; const cases = [_][]const u8{ "describe [Image #1]/image", "[Image #2]/image", "[Image #1]/imagex", "[Image #1]/imagine a logo", "[Image #1] tell me about /image", };
for (cases) |input| { var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/first.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqual(@as(usize, 0), app.command_count); try std.testing.expectEqualStrings(input, app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expectEqual(@as(usize, 1), app.queue_accept_count); }}
test "app_input_runtime keeps a repeated image command local while an image is pending" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "second.png"); const second_path = try realTmpPath(alloc, &tmp, "second.png"); defer alloc.free(second_path);
for ([_][]const u8{ "/image", "/img" }) |command| { var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false, .queue_admitted = false }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/first.png"); app.next_image_id_counter = 2; const input = try std.fmt.allocPrint(alloc, "[Image #1]{s} {s}", .{ command, second_path }); defer alloc.free(input); try app.input_runtime.edit_state.input.appendSlice(alloc, input); try appendImageTokenForPlaceholderAt(&app, 1, 0); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const last_command = app.last_command orelse return error.TestExpectedImageCommand; try std.testing.expectEqualStrings(input["[Image #1]".len..], last_command); try std.testing.expectEqual(@as(usize, 1), app.command_count); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items[1].id); try std.testing.expectEqualStrings(second_path, app.pending_images.items[1].path); try std.testing.expectEqualStrings("[Image #1][Image #2]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings( last_command, app.input_runtime.composer_history.entryText(0).?, ); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 0), app.last_images.len); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); }}
test "app_input_runtime attaches three sequential image commands in appearance order" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const names = [_][]const u8{ "one.png", "two.png", "three.png" }; for (names) |name| try writeTestImage(&tmp, name);
var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false, .queue_admitted = false }; defer app.deinit();
for (names, 1..) |name, expected_id| { const path = try realTmpPath(alloc, &tmp, name); defer alloc.free(path); const suffix = try std.fmt.allocPrint(alloc, "/image {s}", .{path}); defer alloc.free(suffix); try app.input_runtime.insertionState().insertSlice(alloc, suffix, .preserve);
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(expected_id, app.pending_images.items.len); try std.testing.expectEqual(expected_id, app.pending_images.items[expected_id - 1].id); }
try std.testing.expectEqualStrings("[Image #1][Image #2][Image #3]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 3), app.command_count); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expect(app.last_prompt == null);}
test "app_input_runtime submits one prompt carrying every image attached by repeated commands" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "second.png"); const second_path = try realTmpPath(alloc, &tmp, "second.png"); defer alloc.free(second_path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/first.png"); app.next_image_id_counter = 2; const attach = try std.fmt.allocPrint(alloc, "[Image #1]/image {s}", .{second_path}); defer alloc.free(attach); try app.input_runtime.edit_state.input.appendSlice(alloc, attach); try appendImageTokenForPlaceholderAt(&app, 1, 0); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count);
try app.input_runtime.insertionState().insertSlice(alloc, " describe both", .preserve); try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 1), app.queue_accept_count); try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expectEqualStrings("[Image #1][Image #2] describe both", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 2), app.last_images.len); try std.testing.expectEqual(@as(usize, 1), app.last_images[0].id); try std.testing.expectEqual(@as(usize, 2), app.last_images[1].id); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime rejected second image keeps the first pending image and its snapshot" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "first.png"); const first_path = try realTmpPath(alloc, &tmp, "first.png"); defer alloc.free(first_path); const snapshot_dir = try realTmpPath(alloc, &tmp, "."); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false, .queue_admitted = false, .snapshot_dir = snapshot_dir, }; defer app.deinit(); const attach_first = try std.fmt.allocPrint(alloc, "/image {s}", .{first_path}); defer alloc.free(attach_first); try app.input_runtime.insertionState().insertSlice(alloc, attach_first, .preserve); try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); const first_snapshot = try alloc.dupe(u8, app.pending_images.items[0].snapshot_path.?); defer alloc.free(first_snapshot);
const missing = try realTmpPath(alloc, &tmp, "."); defer alloc.free(missing); const attach_missing = try std.fmt.allocPrint(alloc, "/image {s}/absent.png", .{missing}); defer alloc.free(attach_missing); try app.input_runtime.insertionState().insertSlice(alloc, attach_missing, .preserve);
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("images", app.notice_topic.items); try std.testing.expectEqualStrings("image file not found", app.notice_body.items); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items[0].id); try std.testing.expectEqualStrings(first_snapshot, app.pending_images.items[0].snapshot_path.?); try std.testing.expectEqualStrings("[Image #1]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expect(app.last_prompt == null);
var probe = try std.Io.Dir.openDirAbsolute(std.testing.io, first_snapshot[0..std.mem.lastIndexOfScalar(u8, first_snapshot, '/').?], .{}); defer probe.close(std.testing.io); var snapshot_file = try probe.openFile(std.testing.io, std.fs.path.basename(first_snapshot), .{}); snapshot_file.close(std.testing.io);}
test "app_input_runtime repeated image command releases every allocation on failure" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "second.png"); const second_path = try realTmpPath(alloc, &tmp, "second.png"); defer alloc.free(second_path); const input = try std.fmt.allocPrint(alloc, "[Image #1]/image {s}", .{second_path}); defer alloc.free(input);
var counting = std.testing.FailingAllocator.init(alloc, .{}); try runRepeatedImageCommand(counting.allocator(), input);
var fail_index: usize = 0; while (fail_index < counting.alloc_index) : (fail_index += 1) { var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = fail_index }); runRepeatedImageCommand(failing.allocator(), input) catch {}; try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); }}
fn runRepeatedImageCommand(alloc: std.mem.Allocator, input: []const u8) !void { var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false, .queue_admitted = false }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/first.png"); app.next_image_id_counter = 2; try app.input_runtime.edit_state.input.appendSlice(alloc, input); try appendImageTokenForPlaceholderAt(&app, 1, 0); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try Runtime(FakeSubmitApp).submit(&app, 100);}
test "app_input_runtime slash command records output without a pre-frame terminal write" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var sink = try tmp.dir.createFile(std.testing.io, "slash-command-frame.log", .{ .read = true }); defer sink.close(io_mod.getIo());
var app = FrameSubmitApp{ .alloc = alloc, .shell = .{ .stdout_file = sink, .layout = .{ .rows = 24, .cols = 80, .content_bottom = 21, .divider_top_row = 22, .input_row = 23, .divider_bottom_row = 24, .hint_row = 22, }, }, }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "/help"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FrameSubmitApp).submit(&app, 100);
try std.testing.expect(app.command_seen); try std.testing.expectEqual(@as(usize, 1), app.shell.entries.items.len); try std.testing.expect(std.mem.find(u8, app.shell.transcript.items, "command result") != null); try std.testing.expectEqual(@as(u64, 0), try sink.length(io_mod.getIo()));}
test "app_input_runtime idle pasted prompt enters the pending acknowledgement frame" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.worker.hold_available = true;
try app.input_runtime.edit_state.input.appendSlice(alloc, "before [Pasted text #7, 2 lines] after"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "alpha\nbeta"), .line_count = 2, .span = .{ .raw_start = "before ".len, .raw_end = "before [Pasted text #7, 2 lines]".len, }, });
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqualStrings( "before alpha\nbeta after", app.submission.pending.?.draft.prompt, ); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expect(app.shell.render_requests.hasReason(.transcript)); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.shell.render_requests.blocksFrameCommit());}
test "app_input_runtime submit rejects oversized registered paste backing" { const alloc = std.testing.allocator; const placeholder = "[Pasted text #1, 1 line]"; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, placeholder); try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 1, .text = try alloc.alloc(u8, FakeSubmitApp.input_byte_limit + 1), .line_count = 1, .span = .{ .raw_start = 0, .raw_end = placeholder.len }, }); @memset(app.input_runtime.entities.pasted_blocks.items[0].text, 'x'); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(?[]u8, null), app.last_prompt); try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqualStrings( "That edit exceeds the local prompt safety limit and was not applied.", app.notice_body.items, ); try std.testing.expectEqualStrings(placeholder, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len);}
test "app_input_runtime accepted prompt repaints while a turn is active" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .stream = .{ .active = true } }; defer app.deinit(); app.worker.hold_available = true;
try app.input_runtime.edit_state.input.appendSlice(alloc, "queued follow-up"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("queued follow-up", app.last_prompt.?); try std.testing.expect(app.submission.pending == null); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.shell.render_requests.blocksFrameCommit());}
test "app_input_runtime completed presentation tail keeps the active queue path unblocked" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .stream = .{ .active = true }, .pacer = .{ .completed_assistant_presentation_tail = true }, }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, "paced-tail follow-up"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("paced-tail follow-up", app.last_prompt.?); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.shell.render_requests.blocksFrameCommit());}
test "app_input_runtime pending paced output keeps the active queue path" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .pacer = .{ .pending = true }, }; defer app.deinit(); app.worker.hold_available = true;
try app.input_runtime.edit_state.input.appendSlice(alloc, "paced-output follow-up"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("paced-output follow-up", app.last_prompt.?); try std.testing.expect(app.submission.pending == null); try std.testing.expect(!app.worker.held);}
test "app_input_runtime full transcript keeps the active queue path" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.worker.hold_available = true; app.shell.full_transcript_active = true;
try app.input_runtime.edit_state.input.appendSlice(alloc, "full-transcript follow-up"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("full-transcript follow-up", app.last_prompt.?); try std.testing.expect(app.submission.pending == null); try std.testing.expect(!app.worker.held);}
test "app_input_runtime blank submit repaints without a worker event" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, " \t "); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expect(app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime idle image-only prompt enters the pending acknowledgement frame" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.worker.hold_available = true; try appendOwnedPendingImage(&app, 1, "/tmp/image.png");
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_prompt == null); try std.testing.expectEqualStrings("", app.submission.pending.?.draft.prompt); try std.testing.expectEqual(@as(usize, 1), app.submission.pending.?.draft.images.len); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expect(app.shell.render_requests.hasReason(.transcript)); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(!app.shell.render_requests.blocksFrameCommit());}
test "app_input_runtime submit preserves direct prompt boundary whitespace" { const alloc = std.testing.allocator; const input = " if True:\n print(\"x\")\n"; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = input.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings(input, app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings( input, app.input_runtime.composer_history.entryText(0).?, );}
test "app_input_runtime disabled prompt history skips composer recall recording" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
app.prompt_history.disable(); try app.input_runtime.edit_state.input.appendSlice(alloc, "disabled prompt"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("disabled prompt", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.composer_history.count());
app.prompt_history.enable(); try app.input_runtime.edit_state.input.appendSlice(alloc, "enabled prompt"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("enabled prompt", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings( "enabled prompt", app.input_runtime.composer_history.entryText(0).?, );}
test "app_input_runtime accepted large paste recalls compact and editable" { const alloc = std.testing.allocator; const placeholder = "[Pasted text #7, 1 line]"; const backing = "x" ** (paste_blocks.large_paste_char_threshold + 1); var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice( alloc, " " ++ placeholder ++ " ", ); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, backing), .line_count = 1, .span = .{ .raw_start = 2, .raw_end = 2 + placeholder.len }, });
try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqualStrings(" " ++ backing ++ " ", app.last_prompt.?);
try input_completion_runtime.CompletionRuntime(FakeSubmitApp).navigatePromptHistory( &app, -1, );
try std.testing.expectEqualStrings( " " ++ placeholder ++ " ", app.input_runtime.edit_state.input.items, ); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqualStrings( backing, app.input_runtime.entities.pasted_blocks.items[0].text, ); try std.testing.expectEqual( composer_insertion.InsertResult.inserted, try app.input_runtime.insertionState().insertSliceBounded( alloc, " edit", 4096, .preserve, ), );}
test "app_input_runtime recalled image and skill resubmit with fresh provenance" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "history-image.png"); const image_path = try realTmpPath(alloc, &tmp, "history-image.png"); defer alloc.free(image_path); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .snapshot_dir = snapshot_dir, }; defer app.deinit(); const input = try std.fmt.allocPrint(alloc, "{s} $review", .{image_path}); defer alloc.free(input); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = input.len; const skill_start = image_path.len + 1; try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = skill_start, .raw_end = skill_start + "$review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/claude/review/SKILL.md"), });
try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqualStrings("[Image #1] $review", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len);
try input_completion_runtime.CompletionRuntime(FakeSubmitApp).navigatePromptHistory( &app, -1, ); try std.testing.expectEqualStrings("[Image #2] $review", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings( "/tmp/claude/review/SKILL.md", app.input_runtime.entities.skill_tokens.items[0].path, ); try std.testing.expectEqual(@as(usize, 3), app.next_image_id_counter);
try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqualStrings("[Image #2] $review", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqual(@as(usize, 2), app.last_images[0].id); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len); try std.testing.expectEqualStrings( "/tmp/claude/review/SKILL.md", app.last_skill_tokens.items[0].path, );
app.next_image_id_counter = std.math.maxInt(usize); try std.testing.expectError( error.Overflow, input_completion_runtime.CompletionRuntime(FakeSubmitApp).navigatePromptHistory( &app, -1, ), ); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(?usize, null), app.input_runtime.composer_history.activeIndex()); try std.testing.expect(app.input_runtime.composer_history.draftView() == null); try std.testing.expectEqual( std.math.maxInt(usize), app.next_image_id_counter, );}
test "app_input_runtime recalls image after post-ack finalization failure" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "failed-image.png"); const image_path = try realTmpPath(alloc, &tmp, "failed-image.png"); defer alloc.free(image_path); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .snapshot_dir = snapshot_dir, .fail_pending_finalization = true, }; defer app.deinit(); app.worker.hold_available = true; try app.input_runtime.edit_state.input.appendSlice(alloc, image_path); app.input_runtime.edit_state.cursor = image_path.len;
try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expect(app.submission.pending != null); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count());
input_submit_runtime.SubmitRuntime(FakeSubmitApp).noteCommittedFrame(&app); input_submit_runtime.SubmitRuntime(FakeSubmitApp).collectPendingSubmissionFacts(&app);
try std.testing.expect(app.submission.pending == null); try std.testing.expectEqual(@as(usize, 1), app.notice_count); try input_completion_runtime.CompletionRuntime(FakeSubmitApp).navigatePromptHistory( &app, -1, ); try std.testing.expectEqualStrings("[Image #2]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 2), try countTestSnapshotFiles(snapshot_dir));
app.clearPendingImages(); try std.testing.expectEqual(@as(usize, 1), try countTestSnapshotFiles(snapshot_dir)); app.input_runtime.composer_history.clear(alloc); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir));}
test "app_input_runtime terminal pending cleanup preserves image history" { const alloc = std.testing.allocator; const PendingRuntime = input_submit_runtime.SubmitRuntime(FakeSubmitApp); const cases = [_]struct { name: []const u8, queued: bool, session_transition: bool, }{ .{ .name = "ctrl-c-awaiting.png", .queued = false, .session_transition = false }, .{ .name = "ctrl-c-queued.png", .queued = true, .session_transition = false }, .{ .name = "reset-awaiting.png", .queued = false, .session_transition = true }, .{ .name = "reset-queued.png", .queued = true, .session_transition = true }, };
for (cases) |case| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, case.name); const image_path = try realTmpPath(alloc, &tmp, case.name); defer alloc.free(image_path); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .snapshot_dir = snapshot_dir }; defer app.deinit(); app.worker.hold_available = true; try app.input_runtime.edit_state.input.appendSlice(alloc, image_path); app.input_runtime.edit_state.cursor = image_path.len; try Runtime(FakeSubmitApp).submit(&app, 100); if (case.queued) { PendingRuntime.noteCommittedFrame(&app); PendingRuntime.collectPendingSubmissionFacts(&app); try std.testing.expectEqual( input_submit_runtime.PendingPhase.queued, app.submission.pending.?.phase, ); }
if (case.session_transition) { PendingRuntime.clearPendingSubmissionForSessionTransition(&app); app.input_runtime.inputResetState().resetForSession(alloc); } else { try std.testing.expect(PendingRuntime.cancelPendingSubmission(&app)); }
try std.testing.expect(app.submission.pending == null); try std.testing.expectEqual(@as(usize, 1), try countTestSnapshotFiles(snapshot_dir)); try input_completion_runtime.CompletionRuntime(FakeSubmitApp).navigatePromptHistory( &app, -1, ); try std.testing.expectEqualStrings("[Image #2]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), try countTestSnapshotFiles(snapshot_dir));
app.clearPendingImages(); try std.testing.expectEqual(@as(usize, 1), try countTestSnapshotFiles(snapshot_dir)); app.input_runtime.composer_history.clear(alloc); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir)); }}
test "app_input_runtime missing auth preserves the complete composer before prompt admission" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit();
try app.input_runtime.edit_state.input.appendSlice( alloc, "before [Pasted text #7, 1 line] $review [Image #1]", ); app.input_runtime.edit_state.cursor = "before ".len; try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "pasted"), .line_count = 1, .span = .{ .raw_start = "before ".len, .raw_end = "before [Pasted text #7, 1 line]".len, }, }); try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = "before [Pasted text #7, 1 line] ".len, .raw_end = "before [Pasted text #7, 1 line] $review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/review/SKILL.md"), }); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 1, .span = .{ .raw_start = "before [Pasted text #7, 1 line] $review ".len, .raw_end = "before [Pasted text #7, 1 line] $review [Image #1]".len, }, });
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expectEqualStrings( "before [Pasted text #7, 1 line] $review [Image #1]", app.input_runtime.edit_state.input.items, ); try std.testing.expectEqual(@as(usize, "before ".len), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.composer_history.count()); try std.testing.expect(app.last_prompt == null);}
test "app_input_runtime preflight rejection removes captured inline snapshot" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "preflight.png"); const image_path = try realTmpPath(alloc, &tmp, "preflight.png"); defer alloc.free(image_path); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false, .snapshot_dir = snapshot_dir, }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, image_path); app.input_runtime.edit_state.cursor = image_path.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings(image_path, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir));}
test "app_input_runtime gates direct model selection but not bare model browse" { const alloc = std.testing.allocator; const cases = [_]struct { input: []const u8, preflight_count: usize, command: ?[]const u8, remaining_input: []const u8, }{ .{ .input = "/model", .preflight_count = 0, .command = "/model", .remaining_input = "" }, .{ .input = "/model claude", .preflight_count = 1, .command = null, .remaining_input = "/model claude" }, };
for (cases) |case| { var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, case.input); app.input_runtime.edit_state.cursor = case.input.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(case.preflight_count, app.preflight_count); try std.testing.expectEqualStrings(case.remaining_input, app.input_runtime.edit_state.input.items); if (case.command) |command| { try std.testing.expectEqualStrings(command, app.last_command.?); } else { try std.testing.expect(app.last_command == null); } }}
test "app_input_runtime keeps bare model status local without auth" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .prompt_admitted = false }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "/model"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 0), app.preflight_count); try std.testing.expectEqualStrings("/model", app.last_command.?);}
test "app_input_runtime queue rejection preserves complete composer state" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc, .queue_admitted = false }; defer app.deinit(); const input = "before [Pasted text #7, 1 line] $review [Image #1]"; try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = "before ".len; try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "pasted"), .line_count = 1, .span = .{ .raw_start = "before ".len, .raw_end = "before [Pasted text #7, 1 line]".len, }, }); try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = "before [Pasted text #7, 1 line] ".len, .raw_end = "before [Pasted text #7, 1 line] $review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/review/SKILL.md"), }); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 1, .span = .{ .raw_start = "before [Pasted text #7, 1 line] $review ".len, .raw_end = "before [Pasted text #7, 1 line] $review [Image #1]".len, }, });
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 1), app.preflight_count); try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, "before ".len), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqualStrings("pasted", app.input_runtime.entities.pasted_blocks.items[0].text); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqualStrings("review", app.input_runtime.entities.skill_tokens.items[0].name); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqualStrings("/tmp/image.png", app.pending_images.items[0].path); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.composer_history.count()); try std.testing.expect(app.last_prompt == null); try std.testing.expect(!app.shell.render_requests.hasReason(.footer));}
test "app_input_runtime queue rejection removes captured inline snapshot" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "rejected.png"); const image_path = try realTmpPath(alloc, &tmp, "rejected.png"); defer alloc.free(image_path); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .queue_admitted = false, .snapshot_dir = snapshot_dir, }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, image_path); app.input_runtime.edit_state.cursor = image_path.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings(image_path, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir));}
fn checkAcceptedPromptCleanupAcrossAllocationFailures(failing: *std.testing.FailingAllocator) !void { const alloc = failing.allocator(); var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "accepted prompt"); app.input_runtime.edit_state.cursor = "accepted prompt".len;
Runtime(FakeSubmitApp).submit(&app, 100) catch |err| { if (app.queue_accept_count > 0) return error.AcceptedPromptReturnedError; return err; };
if (app.queue_accept_count > 0) { try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); }}
test "app_input_runtime accepted prompt cleanup survives allocation failures" { var probe = std.testing.FailingAllocator.init(std.testing.allocator, .{}); try checkAcceptedPromptCleanupAcrossAllocationFailures(&probe); const allocation_count = probe.alloc_index; try std.testing.expectEqual(probe.allocated_bytes, probe.freed_bytes);
for (0..allocation_count) |fail_index| { var failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = fail_index }, ); checkAcceptedPromptCleanupAcrossAllocationFailures(&failing) catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); }; try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); }}
test "app_input_runtime inline image edits compose stable collision and history spans" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "first.png"); try writeTestImage(&tmp, "second.png"); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const cases = [_]struct { input: []const u8, expected: []const u8, first_id: usize, image_count: usize }{ .{ .input = " \t[Pasted text #7, 2 lines]\n@./first.png,\n[Image #8]\t$review @./second.png \r\n", .expected = " \talpha\n beta\n[Image #9],\n[Image #8]\t$review [Image #10] \r\n", .first_id = 9, .image_count = 3, }, .{ .input = " \t[Pasted text #7, 2 lines]\n[Image #9] @./first.png,\n[Image #8]\t$review \r\n", .expected = " \talpha\n beta\n[Image #9] [Image #10],\n[Image #8]\t$review \r\n", .first_id = 10, .image_count = 2, }, }; for (cases) |case| { var app = FakeSubmitApp{ .alloc = alloc, .workspace_root = root, .next_image_id_counter = 9 }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, case.input); app.input_runtime.edit_state.cursor = case.input.len; try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "alpha\n beta"), .line_count = 2, .span = .{ .raw_start = 2, .raw_end = 2 + "[Pasted text #7, 2 lines]".len }, }); try appendOwnedPendingImage(&app, 8, "/tmp/existing.png"); try appendImageTokenForPlaceholderAt(&app, 8, std.mem.find(u8, case.input, "[Image #8]").?); const skill_start = std.mem.find(u8, case.input, "$review").?; try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = skill_start, .raw_end = skill_start + "$review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/review/SKILL.md"), }); try Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqualStrings(case.expected, app.last_prompt.?); try std.testing.expectEqual(case.image_count, app.last_images.len); try std.testing.expectEqual(case.first_id, app.last_images[0].id); try std.testing.expectEqual(@as(usize, 8), app.last_images[1].id); if (case.image_count == 3) try std.testing.expectEqual(@as(usize, 10), app.last_images[2].id); try std.testing.expectEqual(@as(usize, 11), app.next_image_id_counter); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len); const skill = app.last_skill_tokens.items[0]; try std.testing.expectEqualStrings("$review", app.last_prompt.?[skill.raw_start..skill.raw_end]); const history = app.input_runtime.composer_history.viewEntry(0).?; try std.testing.expectEqualStrings(case.expected, history.text); try std.testing.expectEqual(case.image_count, history.image_tokens.len); for (history.image_tokens) |token| { const parsed = image_attachments.matchImagePlaceholder(history.text, token.span.raw_start).?; try std.testing.expectEqual(token.id, parsed.id); try std.testing.expectEqual(token.span.raw_end, token.span.raw_start + parsed.length); } const stored_skill = history.skill_tokens[0]; try std.testing.expectEqualStrings("$review", history.text[stored_skill.raw_start..stored_skill.raw_end]); }}
test "app_input_runtime inline image edits retain draft IDs and snapshots on failed admission" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "existing.png"); try writeTestImage(&tmp, "first.png"); try writeTestImage(&tmp, "second.png"); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const existing = try realTmpPath(alloc, &tmp, "existing.png"); defer alloc.free(existing); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir); const input = " \t[Image #8]\n@./first.png @./second.png \r\n"; const cases = [_]struct { capture_error: ?anyerror = null, queue_admitted: bool = true, enqueue_failure: bool = false, first_id: usize = 9, expected_error: ?anyerror = null }{ .{ .capture_error = error.Cancelled, .expected_error = error.Cancelled }, .{ .capture_error = error.OutOfMemory, .expected_error = error.OutOfMemory }, .{ .capture_error = error.ImageTooLarge }, .{ .queue_admitted = false }, .{ .enqueue_failure = true, .expected_error = error.InjectedEnqueueFailure }, .{ .first_id = 8, .expected_error = error.DuplicateImageId }, .{ .first_id = std.math.maxInt(usize), .expected_error = error.ImageIdOverflow }, }; for (cases) |case| { var app = FakeSubmitApp{ .alloc = alloc, .workspace_root = root, .snapshot_dir = snapshot_dir, .next_image_id_counter = case.first_id, .capture_error_id = if (case.capture_error != null) 10 else null, .capture_error = case.capture_error, .queue_admitted = case.queue_admitted, .fail_enqueue_after_snapshot = case.enqueue_failure, }; defer app.deinit(); try appendOwnedPendingImage(&app, 8, existing); try image_attachments.captureImageSnapshot(alloc, &app.pending_images.items[0], snapshot_dir); const digest = try alloc.dupe(u8, app.pending_images.items[0].snapshot_sha256.?); defer alloc.free(digest); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = input.len; try appendImageTokenForPlaceholderAt(&app, 8, 2); const result = Runtime(FakeSubmitApp).submit(&app, 100); if (case.expected_error) |err| try std.testing.expectError(err, result) else try result; try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(input.len, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(case.first_id, app.next_image_id_counter); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 8), app.pending_images.items[0].id); try std.testing.expectEqualStrings(digest, app.pending_images.items[0].snapshot_sha256.?); try std.testing.expectEqual(@as(usize, 1), try countTestSnapshotFiles(snapshot_dir)); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.image_tokens.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.composer_history.count()); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expect(app.last_prompt == null); }}
fn check_inline_image_edit_allocation_failure(failing: *std.testing.FailingAllocator, root: []const u8, snapshot_dir: []const u8) !void { const alloc = failing.allocator(); const input = " \t[Image #8]\n@./first.png $review \r\n"; var app = FakeSubmitApp{ .alloc = alloc, .workspace_root = root, .snapshot_dir = snapshot_dir, .next_image_id_counter = 9, .queue_admitted = false, }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = input.len; try appendOwnedPendingImage(&app, 8, "/tmp/retained.png"); try appendImageTokenForPlaceholderAt(&app, 8, 2); const skill_start = std.mem.find(u8, input, "$review").?; try app.input_runtime.entities.skill_tokens.ensureUnusedCapacity(alloc, 1); const name = try alloc.dupe(u8, "review"); const path = alloc.dupe(u8, "/tmp/review/SKILL.md") catch |err| { alloc.free(name); return err; }; app.input_runtime.entities.skill_tokens.appendAssumeCapacity(.{ .raw_start = skill_start, .raw_end = skill_start + "$review".len, .name = name, .path = path }); const result = Runtime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(input.len, app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 9), app.next_image_id_counter); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 8), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.image_tokens.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.skill_tokens.items.len); try std.testing.expectEqual(skill_start, app.input_runtime.entities.skill_tokens.items[0].raw_start); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.composer_history.count()); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir)); try result;}
test "app_input_runtime inline image edits roll back across allocation failures" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "first.png"); const root = try realTmpPath(std.testing.allocator, &tmp, "."); defer std.testing.allocator.free(root); const snapshot_dir = try std.fs.path.join(std.testing.allocator, &.{ root, "snapshots" }); defer std.testing.allocator.free(snapshot_dir); var probe = std.testing.FailingAllocator.init(std.testing.allocator, .{}); try check_inline_image_edit_allocation_failure(&probe, root, snapshot_dir); try std.testing.expectEqual(probe.allocated_bytes, probe.freed_bytes); for (0..probe.alloc_index) |index| { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = index }); check_inline_image_edit_allocation_failure(&failing, root, snapshot_dir) catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); }; try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir)); }}
test "app_input_runtime submit projects selected skill spans onto transformed prompt text" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "selected-skill.png");
const image_path = try realTmpPath(alloc, &tmp, "selected-skill.png"); defer alloc.free(image_path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "pasted"), .line_count = 1, .span = .{ .raw_start = " ".len, .raw_end = " [Pasted text #7, 1 line]".len, }, });
const prefix = try std.fmt.allocPrint( alloc, " [Pasted text #7, 1 line] {s}, ", .{image_path}, ); defer alloc.free(prefix); try app.input_runtime.edit_state.input.appendSlice(alloc, prefix); try app.input_runtime.edit_state.input.appendSlice(alloc, "$review"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = prefix.len, .raw_end = prefix.len + "$review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/.codex/skills/review/SKILL.md"), });
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings(" pasted [Image #1], $review", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqualStrings(image_path, app.last_images[0].path); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len); try std.testing.expectEqualStrings("review", app.last_skill_tokens.items[0].name); try std.testing.expectEqualStrings("/tmp/.codex/skills/review/SKILL.md", app.last_skill_tokens.items[0].path); try std.testing.expectEqual(@as(usize, " pasted [Image #1], ".len), app.last_skill_tokens.items[0].raw_start); try std.testing.expectEqual(@as(usize, " pasted [Image #1], $review".len), app.last_skill_tokens.items[0].raw_end);}
test "app_input_runtime submit preserves side whitespace around semantic entities" { const alloc = std.testing.allocator; const input = " [Image #1] $review\n"; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, input); try appendImageTokenForPlaceholderAt(&app, 1, " ".len); const skill_start = " [Image #1] ".len; try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = skill_start, .raw_end = skill_start + "$review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/review/SKILL.md"), }); app.input_runtime.edit_state.cursor = input.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings(input, app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len); try std.testing.expectEqual(skill_start, app.last_skill_tokens.items[0].raw_start); try std.testing.expectEqual( skill_start + "$review".len, app.last_skill_tokens.items[0].raw_end, );}
test "app_input_runtime submit keeps pasted dollar text separate from selected skill span" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "raw $review inside"), .line_count = 1, .span = .{ .raw_start = 0, .raw_end = "[Pasted text #7, 1 line]".len, }, });
const prefix = "[Pasted text #7, 1 line] "; try app.input_runtime.edit_state.input.appendSlice(alloc, prefix); try app.input_runtime.edit_state.input.appendSlice(alloc, "$review"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = prefix.len, .raw_end = prefix.len + "$review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/.codex/skills/review/SKILL.md"), });
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("raw $review inside $review", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len); try std.testing.expectEqual(@as(usize, "raw $review inside ".len), app.last_skill_tokens.items[0].raw_start); try std.testing.expectEqual(@as(usize, "raw $review inside $review".len), app.last_skill_tokens.items[0].raw_end);}
test "app_input_runtime submit keeps selected skill on its exact marker after image renumbering" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
const image_ids = [_]usize{ 10, 11, 12, 13 }; const placeholders = [_][]const u8{ "[Image #10]", "[Image #11]", "[Image #12]", "[Image #13]", }; for (image_ids, placeholders, 0..) |id, placeholder, index| { if (index > 0) try app.input_runtime.edit_state.input.append(alloc, ' '); const raw_start = app.input_runtime.edit_state.input.items.len; try app.input_runtime.edit_state.input.appendSlice(alloc, placeholder); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = id, .span = .{ .raw_start = raw_start, .raw_end = raw_start + placeholder.len, }, }); var path_buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&path_buf, "/tmp/image-{d}.png", .{id}); try appendOwnedPendingImage(&app, id, path); }
try app.input_runtime.edit_state.input.appendSlice(alloc, " $review$review"); const selected_start = app.input_runtime.edit_state.input.items.len - "$review$review".len; try app.input_runtime.entities.skill_tokens.append(alloc, .{ .raw_start = selected_start, .raw_end = selected_start + "$review".len, .name = try alloc.dupe(u8, "review"), .path = try alloc.dupe(u8, "/tmp/review/SKILL.md"), }); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const expected_prefix = "[Image #10] [Image #11] [Image #12] [Image #13] "; try std.testing.expectEqualStrings( expected_prefix ++ "$review$review", app.last_prompt.?, ); try std.testing.expectEqual(@as(usize, 1), app.last_skill_tokens.items.len); try std.testing.expectEqual(expected_prefix.len, app.last_skill_tokens.items[0].raw_start); try std.testing.expectEqual( expected_prefix.len + "$review".len, app.last_skill_tokens.items[0].raw_end, );}
test "app_input_runtime persists accepted model prompt bytes after enqueue" { const alloc = std.testing.allocator; const input = " accepted prompt\n"; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const entries = try app.prompt_history.loadRecent( alloc, app.workspace_root, 100, ); defer prompt_history_store.freeLoadedEntries(alloc, entries); try std.testing.expectEqualStrings(input, app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), entries.len); try std.testing.expectEqualStrings(input, entries[0].text);}
test "app_input_runtime persists expanded pasted prompt bytes" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true); try app.input_runtime.edit_state.input.appendSlice( alloc, "before [Pasted text #7, 2 lines] after", ); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len; try app.input_runtime.entities.pasted_blocks.append(alloc, .{ .id = 7, .text = try alloc.dupe(u8, "alpha\nbeta"), .line_count = 2, .span = .{ .raw_start = "before ".len, .raw_end = "before [Pasted text #7, 2 lines]".len, }, });
try Runtime(FakeSubmitApp).submit(&app, 100);
const entries = try app.prompt_history.loadRecent( alloc, app.workspace_root, 100, ); defer prompt_history_store.freeLoadedEntries(alloc, entries); try std.testing.expectEqual(@as(usize, 1), entries.len); try std.testing.expectEqualStrings( "before alpha\nbeta after", entries[0].text, );}
test "app_input_runtime persists slash commands in durable prompt history" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true); try app.input_runtime.edit_state.input.appendSlice(alloc, "/help"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const entries = try app.prompt_history.loadRecent( alloc, app.workspace_root, 100, ); defer prompt_history_store.freeLoadedEntries(alloc, entries); try std.testing.expectEqual(@as(usize, 1), entries.len); try std.testing.expectEqualStrings("/help", entries[0].text); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings( "/help", app.input_runtime.composer_history.entryText(0).?, );}
test "app_input_runtime excludes image-bearing prompts from durable history" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true); try appendOwnedPendingImage(&app, 1, "/tmp/image.png"); try app.input_runtime.edit_state.input.appendSlice( alloc, "describe [Image #1]", ); try appendImageTokenForPlaceholderAt(&app, 1, "describe ".len); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
const entries = try app.prompt_history.loadRecent( alloc, app.workspace_root, 100, ); defer prompt_history_store.freeLoadedEntries(alloc, entries); try std.testing.expectEqual(@as(usize, 0), entries.len);}
test "app_input_runtime submits nonexistent relative and absolute image paths as ordinary text" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const absolute_path = try std.fs.path.join(alloc, &.{ root, "missing.png" }); defer alloc.free(absolute_path);
for ([_][]const u8{ "fx-missing-image.png", absolute_path }) |missing_path| { var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, missing_path); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings(missing_path, app.last_prompt.?); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); }}
test "app_input_runtime submits an absolute non-image path as ordinary text" { const alloc = std.testing.allocator; const input = "/Users/example/project/notes.txt please inspect"; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = input.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings(input, app.last_prompt.?);}
test "app_input_runtime submits an absolute image path instead of treating it as a command" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "absolute.png");
const image_path = try realTmpPath(alloc, &tmp, "absolute.png"); defer alloc.free(image_path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, image_path); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expect(app.last_command == null); try std.testing.expectEqualStrings("[Image #1]", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqualStrings(image_path, app.last_images[0].path);}
test "app_input_runtime preserves punctuation after a submitted image path" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "punctuated.png"); const image_path = try realTmpPath(alloc, &tmp, "punctuated.png"); defer alloc.free(image_path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); const input = try std.fmt.allocPrint(alloc, "inspect {s},", .{image_path}); defer alloc.free(input); try app.input_runtime.edit_state.input.appendSlice(alloc, input); app.input_runtime.edit_state.cursor = input.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("inspect [Image #1],", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqualStrings(image_path, app.last_images[0].path);}
test "app_input_runtime keeps a typed image lookalike literal beside a real image" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "real.png"); const image_path = try realTmpPath(alloc, &tmp, "real.png"); defer alloc.free(image_path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try Runtime(FakeSubmitApp).handlePastedBytes( &app, image_path, FakeSubmitApp.input_byte_limit, ); try app.input_runtime.insertionState().insertSlice(alloc, " [Image #1]", .preserve);
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("[Image #2] [Image #1]", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 1), app.last_images.len); try std.testing.expectEqual(@as(usize, 2), app.last_images[0].id); try std.testing.expectEqualStrings(image_path, app.last_images[0].path); try std.testing.expectEqual(@as(usize, 3), app.next_image_id_counter);}
test "app_input_runtime queues existing and typed images in placeholder order" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "typed.png");
const typed_path = try realTmpPath(alloc, &tmp, "typed.png"); defer alloc.free(typed_path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try appendOwnedPendingImage(&app, 7, "/tmp/existing.png"); app.next_image_id_counter = 8; const input = try std.fmt.allocPrint(alloc, "before [Image #7] then {s}", .{typed_path}); defer alloc.free(input); try app.input_runtime.edit_state.input.appendSlice(alloc, input); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 7, .span = .{ .raw_start = "before ".len, .raw_end = "before [Image #7]".len, }, }); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("before [Image #7] then [Image #8]", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 2), app.last_images.len); try std.testing.expectEqual(@as(usize, 7), app.last_images[0].id); try std.testing.expectEqualStrings("/tmp/existing.png", app.last_images[0].path); try std.testing.expectEqual(@as(usize, 8), app.last_images[1].id); try std.testing.expectEqualStrings(typed_path, app.last_images[1].path); try std.testing.expectEqual(@as(usize, 9), app.next_image_id_counter); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime restores composer and pending images after late enqueue failure" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "typed.png");
const typed_path = try realTmpPath(alloc, &tmp, "typed.png"); defer alloc.free(typed_path);
var app = FakeSubmitApp{ .alloc = alloc, .fail_enqueue_after_snapshot = true }; defer app.deinit(); try appendOwnedPendingImage(&app, 7, "/tmp/original-a.png"); try appendOwnedPendingImage(&app, 11, "/tmp/original-b.png"); app.next_image_id_counter = 12; const input = try std.fmt.allocPrint(alloc, "[Image #11] {s} [Image #7]", .{typed_path}); defer alloc.free(input); try app.input_runtime.edit_state.input.appendSlice(alloc, input); try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 11, .span = .{ .raw_start = 0, .raw_end = "[Image #11]".len }, }); const last_image_start = input.len - "[Image #7]".len; try app.input_runtime.entities.image_tokens.append(alloc, .{ .id = 7, .span = .{ .raw_start = last_image_start, .raw_end = input.len }, }); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try std.testing.expectError(error.InjectedEnqueueFailure, Runtime(FakeSubmitApp).submit(&app, 100));
try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 7), app.pending_images.items[0].id); try std.testing.expectEqualStrings("/tmp/original-a.png", app.pending_images.items[0].path); try std.testing.expectEqualStrings("image/png", app.pending_images.items[0].media_type); try std.testing.expectEqual(@as(usize, 11), app.pending_images.items[1].id); try std.testing.expectEqualStrings("/tmp/original-b.png", app.pending_images.items[1].path); try std.testing.expectEqualStrings("image/png", app.pending_images.items[1].media_type); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 0), app.last_images.len);}
test "app_input_runtime inline image rejection preserves the draft and rolls back only attempted snapshots" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "existing.png"); try writeTestImage(&tmp, "first.png"); try writeTestImage(&tmp, "second.png");
const existing_path = try realTmpPath(alloc, &tmp, "existing.png"); defer alloc.free(existing_path); const first_path = try realTmpPath(alloc, &tmp, "first.png"); defer alloc.free(first_path); const second_path = try realTmpPath(alloc, &tmp, "second.png"); defer alloc.free(second_path); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir); const input = try std.fmt.allocPrint( alloc, "draft [Image #7] {s} {s}", .{ first_path, second_path }, ); defer alloc.free(input);
var app = FakeSubmitApp{ .alloc = alloc, .next_image_id_counter = 8, .capture_error_id = 9, .snapshot_dir = snapshot_dir, }; defer app.deinit(); try appendOwnedPendingImage(&app, 7, existing_path); try image_attachments.captureImageSnapshot( alloc, &app.pending_images.items[0], snapshot_dir, ); const existing_snapshot_path = try alloc.dupe(u8, app.pending_images.items[0].snapshot_path.?); defer alloc.free(existing_snapshot_path); const existing_snapshot_digest = try alloc.dupe(u8, app.pending_images.items[0].snapshot_sha256.?); defer alloc.free(existing_snapshot_digest); try app.input_runtime.edit_state.input.appendSlice(alloc, input); try appendImageTokenForPlaceholderAt(&app, 7, "draft ".len); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings(input, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, input.len), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 7), app.pending_images.items[0].id); try std.testing.expectEqualStrings(existing_path, app.pending_images.items[0].path); try std.testing.expectEqualStrings(existing_snapshot_path, app.pending_images.items[0].snapshot_path.?); try std.testing.expectEqualStrings(existing_snapshot_digest, app.pending_images.items[0].snapshot_sha256.?); try std.testing.expectEqual(@as(usize, 2), app.capture_count); try std.testing.expectEqual(@as(usize, 1), app.notice_count); try std.testing.expectEqualStrings("images", app.notice_topic.items); try std.testing.expectEqualStrings( "image exceeds the 20 MiB limit", app.notice_body.items, ); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count); try std.testing.expect(app.last_prompt == null); try std.testing.expectEqual(@as(usize, 1), try countTestSnapshotFiles(snapshot_dir));
app.capture_error_id = null; try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqual(@as(usize, 1), app.queue_accept_count); try std.testing.expectEqual(@as(usize, 3), app.last_images.len); try std.testing.expectEqual(@as(usize, 1), app.notice_count); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime pasted image rejection preserves the raw paste and existing images" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "rejected.png");
const path = try realTmpPath(alloc, &tmp, "rejected.png"); defer alloc.free(path); const expected = try std.fmt.allocPrint(alloc, "draft {s}", .{path}); defer alloc.free(expected);
var app = FakeSubmitApp{ .alloc = alloc, .next_image_id_counter = 8, .capture_error_id = 8, }; defer app.deinit(); try appendOwnedPendingImage(&app, 7, "/tmp/existing.png"); try app.input_runtime.edit_state.input.appendSlice(alloc, "draft "); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).handlePastedBytes( &app, path, FakeSubmitApp.input_byte_limit, );
try std.testing.expectEqualStrings(expected, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 7), app.pending_images.items[0].id); try std.testing.expectEqualStrings("/tmp/existing.png", app.pending_images.items[0].path); try std.testing.expectEqual(@as(usize, 1), app.notice_count); try std.testing.expectEqualStrings("images", app.notice_topic.items); try std.testing.expectEqualStrings( "image exceeds the 20 MiB limit", app.notice_body.items, );
try Runtime(FakeSubmitApp).handlePastedBytes( &app, " next", FakeSubmitApp.input_byte_limit, ); const expected_after = try std.fmt.allocPrint(alloc, "{s} next", .{expected}); defer alloc.free(expected_after); try std.testing.expectEqualStrings( expected_after, app.input_runtime.edit_state.input.items, );}
test "app_input_runtime inline capture out of memory propagates without notice or mutation" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "oom-inline.png");
const path = try realTmpPath(alloc, &tmp, "oom-inline.png"); defer alloc.free(path);
var app = FakeSubmitApp{ .alloc = alloc, .capture_error_id = 1, .capture_error = error.OutOfMemory, }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, path); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try std.testing.expectError( error.OutOfMemory, Runtime(FakeSubmitApp).submit(&app, 100), );
try std.testing.expectEqualStrings(path, app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), app.notice_count); try std.testing.expectEqual(@as(usize, 0), app.queue_accept_count);}
const PromptHistoryBusyLockState = struct { now_ms: i64 = 0,
fn tryLock(_: ?*anyopaque, _: std.Io.File) anyerror!bool { return false; }
fn now(ctx: ?*anyopaque) i64 { const self: *PromptHistoryBusyLockState = @ptrCast(@alignCast(ctx.?)); return self.now_ms; }
fn sleep(ctx: ?*anyopaque, millis: u64) void { const self: *PromptHistoryBusyLockState = @ptrCast(@alignCast(ctx.?)); self.now_ms += @intCast(millis); }};
test "app_input_runtime history warning failures never block accepted submission" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true); var lock_state = PromptHistoryBusyLockState{}; app.prompt_history.setStoreLockOpsForTest(.{ .ctx = &lock_state, .try_lock = PromptHistoryBusyLockState.tryLock, .now_ms = PromptHistoryBusyLockState.now, .sleep_ms = PromptHistoryBusyLockState.sleep, }); app.fail_notice = true; try app.input_runtime.edit_state.input.appendSlice(alloc, "still accepted"); app.input_runtime.edit_state.cursor = app.input_runtime.edit_state.input.items.len;
try Runtime(FakeSubmitApp).submit(&app, 100);
try std.testing.expectEqualStrings("still accepted", app.last_prompt.?); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len);}
test "app_input_runtime finalizePastedBlock keeps small text visible" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.paste.buffer.appendSlice(alloc, "one\ntwo\n");
try Runtime(FakeSubmitApp).finalizePastedBlock(&app, 4096);
try std.testing.expectEqualStrings("one\ntwo\n", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.next_paste_id); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len);}
test "app_input_runtime small paste replaces selection within the existing byte budget" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abcde"); _ = app.input_runtime.selectionState().begin(1); _ = app.input_runtime.selectionState().extend(4); try app.input_runtime.paste.buffer.appendSlice(alloc, "XYZ");
try Runtime(FakeSubmitApp).finalizePastedBlock(&app, 5);
try std.testing.expectEqualStrings("aXYZe", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.edit_state.selectionRange() == null); try std.testing.expect(try app.input_runtime.undoState().undo(alloc)); try std.testing.expectEqualStrings("abcde", app.input_runtime.edit_state.input.items);}
test "app_input_runtime bracketed composer paste normalizes CRLF once" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit();
try feedRoutingBytes(&app, "\x1b[200~a\r\nb\rc\n\x1b[201~");
try std.testing.expectEqualStrings("a\nb\nc\n", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len);}
test "app_input_runtime rejects a small paste atomically when it does not fit" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abc"); try app.input_runtime.paste.buffer.appendSlice(alloc, "XY");
try Runtime(FakeSubmitApp).finalizePastedBlock(&app, 4);
try std.testing.expectEqualStrings("abc", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 3), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expectEqualStrings("input", app.notice_topic.items); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone);}
test "app_input_runtime paste edit keeps the original history draft reachable" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); try primeComposerHistoryForTest(RoutingFakeApp, &app, "unsent draft"); try app.input_runtime.textReplacementState().replace(alloc, "recalled prompt");
input_paste_runtime.PasteEditRuntime(RoutingFakeApp).beginPaste( &app, RoutingFakeApp.input_byte_limit, ); try app.input_runtime.paste.buffer.appendSlice(alloc, " pasted edit"); try Runtime(RoutingFakeApp).finishPaste(&app, 4096);
try std.testing.expectEqualStrings("recalled prompt pasted edit", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(?usize, 0), app.input_runtime.composer_history.activeIndex()); try std.testing.expectEqualStrings("unsent draft", app.input_runtime.composer_history.draftText().?);}
test "ordinary submit uses one interactive prompt path" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.stream.active = true;
try app.input_runtime.edit_state.input.appendSlice(alloc, "steer now"); try input_submit_runtime.SubmitRuntime(FakeSubmitApp).submit(&app, 100); try std.testing.expectEqualStrings("steer now", app.last_prompt.?);}
test "app_input_runtime small paste opens skills menu for matching dollar token" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "scale", .description = "", .path = "/tmp/scale/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); try app.input_runtime.paste.buffer.appendSlice(alloc, "use $sca now");
try Runtime(RoutingFakeApp).finalizePastedBlock(&app, 4096);
try std.testing.expectEqualStrings("use $sca now", app.input_runtime.edit_state.input.items); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuOrigin.paste, app.skills.menu.origin); try std.testing.expectEqualStrings("sca", app.skills.menu.query()); try std.testing.expectEqual(@as(usize, 4), app.skills.menu.target.?.start); try std.testing.expectEqual(@as(usize, 8), app.skills.menu.target.?.end);}
test "app_input_runtime multi dollar paste preserves spaces and opens first matching skill" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "fx-test-strategy", .description = "", .path = "/tmp/fx-test-strategy/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); const pasted = "Need $fx-test and $notaskill in pasted text"; try app.input_runtime.paste.buffer.appendSlice(alloc, pasted);
try Runtime(RoutingFakeApp).finalizePastedBlock(&app, 4096);
try std.testing.expectEqualStrings(pasted, app.input_runtime.edit_state.input.items); try std.testing.expect(app.skills.menu.active); try std.testing.expectEqual(skill_runtime.SkillMenuOrigin.paste, app.skills.menu.origin); try std.testing.expectEqualStrings("fx-test", app.skills.menu.query()); try std.testing.expectEqual(@as(usize, "Need ".len), app.skills.menu.target.?.start); try std.testing.expectEqual(@as(usize, "Need $fx-test".len), app.skills.menu.target.?.end);
try Runtime(RoutingFakeApp).resolveEscape(&app, false, 1); try std.testing.expect(!app.skills.menu.active); try std.testing.expectEqualStrings(pasted, app.input_runtime.edit_state.input.items);}
test "app_input_runtime no-match dollar paste remains raw without opening skills menu" { const alloc = std.testing.allocator; var app = try RoutingFakeApp.init(alloc); defer app.deinit(); const skills = [_]skill_runtime.Skill{.{ .name = "scale", .description = "", .path = "/tmp/scale/SKILL.md", .source = .global_fx, }}; app.skills.items = @constCast(&skills); const pasted = "Need $notaskill in pasted text"; try app.input_runtime.paste.buffer.appendSlice(alloc, pasted);
try Runtime(RoutingFakeApp).finalizePastedBlock(&app, 4096);
try std.testing.expectEqualStrings(pasted, app.input_runtime.edit_state.input.items); try std.testing.expect(!app.skills.menu.active);}
test "app_input_runtime image path paste rejects the complete edit without changing draft state" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "a.png"); try writeTestImage(&tmp, "b.png");
const path_a = try realTmpPath(alloc, &tmp, "a.png"); defer alloc.free(path_a); const path_b = try realTmpPath(alloc, &tmp, "b.png"); defer alloc.free(path_b); const paste = try std.fmt.allocPrint(alloc, "{s} {s}", .{ path_a, path_b }); defer alloc.free(paste);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.edit_state.input.appendSlice(alloc, "draft"); app.input_runtime.edit_state.cursor = 2; try app.input_runtime.picker.beginModelPickerFlow(alloc, "test/model", 2, true, .fast); app.input_runtime.picker.file_completion_index = 4; try appendOwnedPendingImage(&app, 3, "/tmp/original.png"); app.next_image_id_counter = 7;
var placeholder_buf: [32]u8 = undefined; const first_placeholder = try image_attachments.formatImagePlaceholder(&placeholder_buf, 7); const limit = app.input_runtime.edit_state.input.items.len + first_placeholder.len + 1; try Runtime(FakeSubmitApp).handlePastedBytes(&app, paste, limit);
try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.hasPendingModelPickerSelection()); try std.testing.expectEqual(picker_state.ModelPickerStage.fast, app.input_runtime.picker.model_picker_stage); try std.testing.expectEqual(@as(usize, 4), app.input_runtime.picker.file_completion_index); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 3), app.pending_images.items[0].id); try std.testing.expectEqualStrings("/tmp/original.png", app.pending_images.items[0].path); try std.testing.expectEqual(@as(usize, 7), app.next_image_id_counter); try std.testing.expectEqualStrings( "That edit exceeds the local prompt safety limit and was not applied.", app.notice_body.items, );}
test "app_input_runtime image path paste commits complete text and attachments in order" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "a.png"); try writeTestImage(&tmp, "b.png");
const path_a = try realTmpPath(alloc, &tmp, "a.png"); defer alloc.free(path_a); const path_b = try realTmpPath(alloc, &tmp, "b.png"); defer alloc.free(path_b);
const paste = try std.fmt.allocPrint( alloc, "before {s} middle {s} after", .{ path_a, path_b }, ); defer alloc.free(paste); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .snapshot_dir = snapshot_dir }; defer app.deinit();
try Runtime(FakeSubmitApp).handlePastedBytes( &app, paste, FakeSubmitApp.input_byte_limit, );
try std.testing.expectEqualStrings( "before [Image #1] middle [Image #2] after", app.input_runtime.edit_state.input.items, ); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items[0].id); try std.testing.expectEqualStrings(path_a, app.pending_images.items[0].path); try std.testing.expectEqual(@as(usize, 2), app.pending_images.items[1].id); try std.testing.expectEqualStrings(path_b, app.pending_images.items[1].path); try std.testing.expectEqual(@as(usize, 3), app.next_image_id_counter); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.entities.image_tokens.items.len); try std.testing.expectEqual(@as(usize, 2), try countTestSnapshotFiles(snapshot_dir));}
test "app_input_runtime image path paste resolves paths from the workspace" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "photo.png");
const workspace_root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(workspace_root); const expected_path = try realTmpPath(alloc, &tmp, "photo.png"); defer alloc.free(expected_path);
var app = FakeSubmitApp{ .alloc = alloc, .workspace_root = workspace_root, }; defer app.deinit();
try Runtime(FakeSubmitApp).handlePastedBytes( &app, "photo.png", FakeSubmitApp.input_byte_limit, );
try std.testing.expectEqualStrings("[Image #1]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqualStrings(expected_path, app.pending_images.items[0].path);}
test "app_input_runtime image path paste replaces selection as one structured edit" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "selection.png"); const path = try realTmpPath(alloc, &tmp, "selection.png"); defer alloc.free(path);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abcde"); _ = app.input_runtime.selectionState().begin(1); _ = app.input_runtime.selectionState().extend(4); var placeholder_buf: [32]u8 = undefined; const placeholder = try image_attachments.formatImagePlaceholder(&placeholder_buf, 1);
try Runtime(FakeSubmitApp).handlePastedBytes( &app, path, 2 + placeholder.len, );
try std.testing.expectEqualStrings("a[Image #1]e", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.image_tokens.items.len); try std.testing.expect(app.input_runtime.edit_state.selectionRange() == null); try std.testing.expect(!(try app.input_runtime.undoState().undo(alloc)));}
test "app_input_runtime image path paste keeps trailing punctuation outside the badge" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "punctuated.png"); const path = try realTmpPath(alloc, &tmp, "punctuated.png"); defer alloc.free(path); const paste = try std.fmt.allocPrint(alloc, "{s},", .{path}); defer alloc.free(paste);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try Runtime(FakeSubmitApp).handlePastedBytes( &app, paste, FakeSubmitApp.input_byte_limit, );
try std.testing.expectEqualStrings("[Image #1],", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.image_tokens.items.len); try std.testing.expectEqual( @as(usize, "[Image #1]".len), app.input_runtime.entities.image_tokens.items[0].span.raw_end, );}
fn checkImagePathPasteAllocationFailureIsAtomic( failing: *std.testing.FailingAllocator, path: []const u8,) !void { const alloc = failing.allocator(); var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); app.next_image_id_counter = 7;
Runtime(FakeSubmitApp).handlePastedBytes( &app, path, FakeSubmitApp.input_byte_limit, ) catch |err| { try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.input.items.len); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.edit_state.cursor); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 7), app.next_image_id_counter); return err; };
try std.testing.expectEqualStrings("[Image #7]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 7), app.pending_images.items[0].id); try std.testing.expectEqual(@as(usize, 8), app.next_image_id_counter);}
test "app_input_runtime image path paste stays atomic across allocation failures" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "oom.png");
const path = try realTmpPath(std.testing.allocator, &tmp, "oom.png"); defer std.testing.allocator.free(path);
var probe = std.testing.FailingAllocator.init(std.testing.allocator, .{}); try checkImagePathPasteAllocationFailureIsAtomic(&probe, path); const allocation_count = probe.alloc_index; try std.testing.expectEqual(probe.allocated_bytes, probe.freed_bytes);
for (0..allocation_count) |fail_index| { var failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = fail_index }, ); checkImagePathPasteAllocationFailureIsAtomic(&failing, path) catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); }; try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); }}
test "app_input_runtime oversized image path paste reports rejection without a snapshot" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var file = try tmp.dir.createFile(std.testing.io, "oversized.png", .{}); defer file.close(std.testing.io); try file.writeStreamingAll(std.testing.io, "\x89PNG\r\n\x1a\n"); try file.setLength(std.testing.io, image_attachments.max_image_bytes + 1); }
const path = try realTmpPath(alloc, &tmp, "oversized.png"); defer alloc.free(path); const root = try realTmpPath(alloc, &tmp, "."); defer alloc.free(root); const snapshot_dir = try std.fs.path.join(alloc, &.{ root, "snapshots" }); defer alloc.free(snapshot_dir);
var app = FakeSubmitApp{ .alloc = alloc, .snapshot_dir = snapshot_dir }; defer app.deinit();
try Runtime(FakeSubmitApp).handlePastedBytes(&app, path, FakeSubmitApp.input_byte_limit);
try std.testing.expectEqualStrings(path, app.input_runtime.edit_state.input.items); try std.testing.expectEqualStrings("images", app.notice_topic.items); try std.testing.expectEqualStrings("image exceeds the 20 MiB limit", app.notice_body.items); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone); try std.testing.expectEqual(@as(usize, 0), app.pending_images.items.len); try std.testing.expectEqual(@as(usize, 0), try countTestSnapshotFiles(snapshot_dir));}
test "app_input_runtime repeated image path paste appends keep pending images sorted" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try writeTestImage(&tmp, "a.png"); try writeTestImage(&tmp, "b.png");
const path_a = try realTmpPath(alloc, &tmp, "a.png"); defer alloc.free(path_a); const path_b = try realTmpPath(alloc, &tmp, "b.png"); defer alloc.free(path_b); const paste = try std.fmt.allocPrint(alloc, "{s} {s}", .{ path_a, path_b }); defer alloc.free(paste);
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.insertionState().insertByte(alloc, 'a', .clear); try Runtime(FakeSubmitApp).handlePastedBytes(&app, paste, FakeSubmitApp.input_byte_limit);
try std.testing.expectEqual(@as(usize, 2), app.pending_images.items.len); try std.testing.expect(image_attachments.imageAttachmentsSortedById(app.pending_images.items)); try std.testing.expect(!(try app.input_runtime.undoState().undo(alloc)));}
test "app_input_runtime finalizePastedBlock registers large text placeholder atomically" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
const large_paste = "x" ** (paste_blocks.large_paste_char_threshold + 1);
try app.input_runtime.insertionState().insertByte(alloc, 'a', .clear); try app.input_runtime.paste.buffer.appendSlice(alloc, large_paste);
try Runtime(FakeSubmitApp).finalizePastedBlock(&app, large_paste.len + 1);
try std.testing.expectEqualStrings("a[Pasted text #1, 1 line]", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqualStrings(large_paste, app.input_runtime.entities.pasted_blocks.items[0].text); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items[0].line_count); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.entities.next_paste_id); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expect(!(try app.input_runtime.undoState().undo(alloc)));}
test "app_input_runtime large paste replaces selection within its expanded byte budget" { const alloc = std.testing.allocator; const large_paste = "x" ** (paste_blocks.large_paste_char_threshold + 1); var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "abcde"); _ = app.input_runtime.selectionState().begin(1); _ = app.input_runtime.selectionState().extend(4); try app.input_runtime.paste.buffer.appendSlice(alloc, large_paste);
try Runtime(FakeSubmitApp).finalizePastedBlock(&app, 2 + large_paste.len);
try std.testing.expectEqualStrings("a[Pasted text #1, 1 line]e", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqualStrings(large_paste, app.input_runtime.entities.pasted_blocks.items[0].text); try std.testing.expect(app.input_runtime.edit_state.selectionRange() == null); try std.testing.expect(!(try app.input_runtime.undoState().undo(alloc)));}
test "app_input_runtime rejects a large paste when its backing text does not fit" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); const large_paste = "x" ** (paste_blocks.large_paste_char_threshold + 1); try app.input_runtime.textReplacementState().replace(alloc, "draft"); try app.input_runtime.paste.buffer.appendSlice(alloc, large_paste);
const limit = "draft".len + large_paste.len - 1; try Runtime(FakeSubmitApp).finalizePastedBlock(&app, limit);
try std.testing.expectEqualStrings("draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.next_paste_id); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expectEqualStrings("input", app.notice_topic.items); try std.testing.expectEqual(types.NoticeTone.@"error", app.notice_tone);}
test "app_input_runtime multiple large pastes share one expanded byte budget" { const alloc = std.testing.allocator; const large_paste = "x" ** (paste_blocks.large_paste_char_threshold + 1); var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit();
try app.input_runtime.paste.buffer.appendSlice(alloc, large_paste); try Runtime(FakeSubmitApp).finalizePastedBlock(&app, large_paste.len * 2 - 1); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len);
try app.input_runtime.paste.buffer.appendSlice(alloc, large_paste); try Runtime(FakeSubmitApp).finalizePastedBlock(&app, large_paste.len * 2 - 1);
try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 2), app.input_runtime.entities.next_paste_id); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try std.testing.expectEqualStrings("input", app.notice_topic.items);}
test "app_input_runtime finalizePastedBlock leaves large paste unchanged on allocation failure" { const base = std.testing.allocator; const large_paste = "x" ** (paste_blocks.large_paste_char_threshold + 1);
var saw_failure = false; var fail_index: usize = 0; while (fail_index < 8) : (fail_index += 1) { var app = FakeSubmitApp{ .alloc = base }; defer app.deinit(); try app.input_runtime.paste.buffer.appendSlice(base, large_paste);
var failing = std.testing.FailingAllocator.init(base, .{ .fail_index = fail_index }); app.alloc = failing.allocator(); if (Runtime(FakeSubmitApp).finalizePastedBlock(&app, 4096)) |_| { app.alloc = base; if (saw_failure) break; continue; } else |err| { app.alloc = base; try std.testing.expectEqual(error.OutOfMemory, err); saw_failure = true; try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.next_paste_id); try std.testing.expectEqualStrings(large_paste, app.input_runtime.paste.buffer.items); } } try std.testing.expect(saw_failure);}
test "app_input_runtime finishPaste leaves no orphan placeholder after late finalization failure" { const base = std.testing.allocator; const large_paste = "x" ** (paste_blocks.large_paste_char_threshold + 1);
var saw_failure = false; var fail_index: usize = 0; while (fail_index < 8) : (fail_index += 1) { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(base, tmp.dir, "."); defer base.free(root); const trace_path = try std.fs.path.join(base, &.{ root, "late-finalize-failed.log" }); defer base.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(base, trace_path, "input");
var app = FakeSubmitApp{ .alloc = base }; defer app.deinit(); app.input_runtime.paste.owner = .composer; try app.input_runtime.paste.buffer.appendSlice(base, large_paste);
var failing = std.testing.FailingAllocator.init(base, .{ .fail_index = fail_index }); app.alloc = failing.allocator(); if (Runtime(FakeSubmitApp).finishPaste(&app, 4096)) |_| { app.alloc = base; debug_trace.shutdown(); if (saw_failure) break; continue; } else |err| { app.alloc = base; try std.testing.expectEqual(error.OutOfMemory, err); saw_failure = true; try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.pasted_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), app.input_runtime.entities.next_paste_id); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); debug_trace.shutdown();
const trace = try readTraceFileForTest(base, trace_path); defer base.free(trace); const needle = try std.fmt.allocPrint( base, "composer paste dropped bytes={d} reason=finalize_failed error=OutOfMemory", .{large_paste.len}, ); defer base.free(needle); try std.testing.expect(std.mem.find(u8, trace, needle) != null); } } try std.testing.expect(saw_failure);}
test "app_input_runtime composer paste finalization failure clears inactive state" { const base = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(base, tmp.dir, "."); defer base.free(root); const trace_path = try std.fs.path.join(base, &.{ root, "finalize-failed-paste.log" }); defer base.free(trace_path);
debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(base, trace_path, "input");
var app = FakeSubmitApp{ .alloc = base }; defer app.deinit(); app.input_runtime.paste.owner = .composer; try app.input_runtime.paste.buffer.appendSlice(base, "pending");
var failing = std.testing.FailingAllocator.init(base, .{ .fail_index = 0 }); app.alloc = failing.allocator(); try std.testing.expectError(error.OutOfMemory, Runtime(FakeSubmitApp).finishPaste(&app, 4096)); app.alloc = base;
try std.testing.expectEqual(paste_framing.Owner.none, app.input_runtime.paste.owner); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.paste.buffer.items.len); try Runtime(FakeSubmitApp).finishPaste(&app, 4096); debug_trace.shutdown();
const trace = try readTraceFileForTest(base, trace_path); defer base.free(trace); try std.testing.expect(std.mem.find(u8, trace, "composer paste dropped bytes=7 reason=finalize_failed error=OutOfMemory") != null);}
test "app input bridge installs durable history into the unified input owner" { const alloc = std.testing.allocator; var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); const Entry = @import("../session/prompt_history_store.zig").LoadedPromptHistoryEntry; var entries = [_]Entry{ .{ .text = try alloc.dupe(u8, "older") }, .{ .text = try alloc.dupe(u8, "newer") }, }; defer { alloc.free(entries[0].text); alloc.free(entries[1].text); }
try Runtime(FakeSubmitApp).installPromptHistory(&app, &entries);
try std.testing.expectEqual(@as(usize, 2), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings("older", app.input_runtime.composer_history.entryText(0).?); try std.testing.expectEqualStrings("newer", app.input_runtime.composer_history.entryText(1).?);}
test "app input bridge loads the most recent durable history in chronological order" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var seed = try prompt_history_store.Store.initFromHome(alloc, home); defer seed.deinit(alloc); for (0..101) |index| { var text_buf: [32]u8 = undefined; const text = try std.fmt.bufPrint(&text_buf, "prompt-{d}", .{index}); _ = try seed.append(alloc, @intCast(index), "/tmp/workspace", text); }
var app = FakeSubmitApp{ .alloc = alloc }; defer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true);
try Runtime(FakeSubmitApp).loadAndInstallPromptHistory(&app);
try std.testing.expectEqual(@as(usize, 100), app.prompt_history.loaded_count); try std.testing.expectEqual(@as(usize, 100), app.input_runtime.composer_history.count()); try std.testing.expectEqualStrings("prompt-1", app.input_runtime.composer_history.entryText(0).?); try std.testing.expectEqualStrings("prompt-100", app.input_runtime.composer_history.entryText(99).?);}