webviewMessageHandler.ts 124 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794
  1. import { safeWriteJson } from "../../utils/safeWriteJson"
  2. import * as path from "path"
  3. import * as os from "os"
  4. import * as fs from "fs/promises"
  5. import pWaitFor from "p-wait-for"
  6. import * as vscode from "vscode"
  7. // kilocode_change start
  8. import axios from "axios"
  9. import { getKiloBaseUriFromToken, isGlobalStateKey } from "@roo-code/types"
  10. import {
  11. MaybeTypedWebviewMessage,
  12. ProfileData,
  13. SeeNewChangesPayload,
  14. TaskHistoryRequestPayload,
  15. TasksByIdRequestPayload,
  16. UpdateGlobalStateMessage,
  17. } from "../../shared/WebviewMessage"
  18. // kilocode_change end
  19. import {
  20. type Language,
  21. type GlobalState,
  22. type ClineMessage,
  23. type TelemetrySetting,
  24. TelemetryEventName,
  25. // kilocode_change start
  26. ghostServiceSettingsSchema,
  27. fastApplyModelSchema,
  28. // kilocode_change end
  29. UserSettingsConfig,
  30. } from "@roo-code/types"
  31. import { CloudService } from "@roo-code/cloud"
  32. import { TelemetryService } from "@roo-code/telemetry"
  33. import { type ApiMessage } from "../task-persistence/apiMessages"
  34. import { saveTaskMessages } from "../task-persistence"
  35. import { ClineProvider } from "./ClineProvider"
  36. import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
  37. import { changeLanguage, t } from "../../i18n"
  38. import { Package } from "../../shared/package"
  39. import { type RouterName, type ModelRecord, toRouterName } from "../../shared/api"
  40. import { MessageEnhancer } from "./messageEnhancer"
  41. import {
  42. type WebviewMessage,
  43. type EditQueuedMessagePayload,
  44. checkoutDiffPayloadSchema,
  45. checkoutRestorePayloadSchema,
  46. } from "../../shared/WebviewMessage"
  47. import { checkExistKey } from "../../shared/checkExistApiConfig"
  48. import { experimentDefault } from "../../shared/experiments"
  49. import { Terminal } from "../../integrations/terminal/Terminal"
  50. import { openFile } from "../../integrations/misc/open-file"
  51. import { openImage, saveImage } from "../../integrations/misc/image-handler"
  52. import { selectImages } from "../../integrations/misc/process-images"
  53. import { getTheme } from "../../integrations/theme/getTheme"
  54. import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
  55. import { searchWorkspaceFiles } from "../../services/search/file-search"
  56. import { fileExistsAtPath } from "../../utils/fs"
  57. import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
  58. import { showSystemNotification } from "../../integrations/notifications" // kilocode_change
  59. import { singleCompletionHandler } from "../../utils/single-completion-handler" // kilocode_change
  60. import { searchCommits } from "../../utils/git"
  61. import { exportSettings, importSettingsWithFeedback } from "../config/importExport"
  62. import { getOpenAiModels } from "../../api/providers/openai"
  63. import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
  64. import { openMention } from "../mentions"
  65. import { getWorkspacePath } from "../../utils/path"
  66. import { Mode, defaultModeSlug } from "../../shared/modes"
  67. import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
  68. import { GetModelsOptions } from "../../shared/api"
  69. import { generateSystemPrompt } from "./generateSystemPrompt"
  70. import { getCommand } from "../../utils/commands"
  71. import { toggleWorkflow, toggleRule, createRuleFile, deleteRuleFile } from "./kilorules"
  72. import { mermaidFixPrompt } from "../prompts/utilities/mermaid" // kilocode_change
  73. import { editMessageHandler, fetchKilocodeNotificationsHandler } from "../kilocode/webview/webviewMessageHandlerUtils" // kilocode_change
  74. const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
  75. import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
  76. import { setPendingTodoList } from "../tools/updateTodoListTool"
  77. import { UsageTracker } from "../../utils/usage-tracker"
  78. import { seeNewChanges } from "../checkpoints/kilocode/seeNewChanges" // kilocode_change
  79. import { getTaskHistory } from "../../shared/kilocode/getTaskHistory" // kilocode_change
  80. import { fetchAndRefreshOrganizationModesOnStartup, refreshOrganizationModes } from "./kiloWebviewMessgeHandlerHelpers"
  81. export const webviewMessageHandler = async (
  82. provider: ClineProvider,
  83. message: MaybeTypedWebviewMessage, // kilocode_change switch to MaybeTypedWebviewMessage for better type-safety
  84. marketplaceManager?: MarketplaceManager,
  85. ) => {
  86. // Utility functions provided for concise get/update of global state via contextProxy API.
  87. const getGlobalState = <K extends keyof GlobalState>(key: K) => provider.contextProxy.getValue(key)
  88. const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
  89. await provider.contextProxy.setValue(key, value)
  90. const getCurrentCwd = () => {
  91. return provider.getCurrentTask()?.cwd || provider.cwd
  92. }
  93. /**
  94. * Shared utility to find message indices based on timestamp
  95. */
  96. const findMessageIndices = (messageTs: number, currentCline: any) => {
  97. // Find the exact message by timestamp, not the first one after a cutoff
  98. const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts === messageTs)
  99. const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex(
  100. (msg: ApiMessage) => msg.ts === messageTs,
  101. )
  102. return { messageIndex, apiConversationHistoryIndex }
  103. }
  104. /**
  105. * Fallback: find first API history index at or after a timestamp.
  106. * Used when the exact user message isn't present in apiConversationHistory (e.g., after condense).
  107. */
  108. const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => {
  109. if (typeof ts !== "number") return -1
  110. return currentCline.apiConversationHistory.findIndex(
  111. (msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts,
  112. )
  113. }
  114. /**
  115. * Removes the target message and all subsequent messages
  116. */
  117. const removeMessagesThisAndSubsequent = async (
  118. currentCline: any,
  119. messageIndex: number,
  120. apiConversationHistoryIndex: number,
  121. ) => {
  122. // Delete this message and all that follow
  123. await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
  124. if (apiConversationHistoryIndex !== -1) {
  125. await currentCline.overwriteApiConversationHistory(
  126. currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
  127. )
  128. }
  129. }
  130. /**
  131. * Handles message deletion operations with user confirmation
  132. */
  133. const handleDeleteOperation = async (messageTs: number): Promise<void> => {
  134. // Check if there's a checkpoint before this message
  135. const currentCline = provider.getCurrentTask()
  136. let hasCheckpoint = false
  137. if (!currentCline) {
  138. await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
  139. return
  140. }
  141. const { messageIndex } = findMessageIndices(messageTs, currentCline)
  142. if (messageIndex !== -1) {
  143. // Find the last checkpoint before this message
  144. const checkpoints = currentCline.clineMessages.filter(
  145. (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
  146. )
  147. hasCheckpoint = checkpoints.length > 0
  148. }
  149. // Send message to webview to show delete confirmation dialog
  150. await provider.postMessageToWebview({
  151. type: "showDeleteMessageDialog",
  152. messageTs,
  153. hasCheckpoint,
  154. })
  155. }
  156. /**
  157. * Handles confirmed message deletion from webview dialog
  158. */
  159. const handleDeleteMessageConfirm = async (messageTs: number, restoreCheckpoint?: boolean): Promise<void> => {
  160. const currentCline = provider.getCurrentTask()
  161. if (!currentCline) {
  162. console.error("[handleDeleteMessageConfirm] No current cline available")
  163. return
  164. }
  165. const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
  166. // Determine API truncation index with timestamp fallback if exact match not found
  167. let apiIndexToUse = apiConversationHistoryIndex
  168. const tsThreshold = currentCline.clineMessages[messageIndex]?.ts
  169. if (apiIndexToUse === -1 && typeof tsThreshold === "number") {
  170. apiIndexToUse = findFirstApiIndexAtOrAfter(tsThreshold, currentCline)
  171. }
  172. if (messageIndex === -1) {
  173. await vscode.window.showErrorMessage(t("common:errors.message.message_not_found", { messageTs }))
  174. return
  175. }
  176. try {
  177. const targetMessage = currentCline.clineMessages[messageIndex]
  178. // If checkpoint restoration is requested, find and restore to the last checkpoint before this message
  179. if (restoreCheckpoint) {
  180. // Find the last checkpoint before this message
  181. const checkpoints = currentCline.clineMessages.filter(
  182. (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
  183. )
  184. const nextCheckpoint = checkpoints[0]
  185. if (nextCheckpoint && nextCheckpoint.text) {
  186. await handleCheckpointRestoreOperation({
  187. provider,
  188. currentCline,
  189. messageTs: targetMessage.ts!,
  190. messageIndex,
  191. checkpoint: { hash: nextCheckpoint.text },
  192. operation: "delete",
  193. })
  194. } else {
  195. // No checkpoint found before this message
  196. console.log("[handleDeleteMessageConfirm] No checkpoint found before message")
  197. vscode.window.showWarningMessage("No checkpoint found before this message")
  198. }
  199. } else {
  200. // For non-checkpoint deletes, preserve checkpoint associations for remaining messages
  201. // Store checkpoints from messages that will be preserved
  202. const preservedCheckpoints = new Map<number, any>()
  203. for (let i = 0; i < messageIndex; i++) {
  204. const msg = currentCline.clineMessages[i]
  205. if (msg?.checkpoint && msg.ts) {
  206. preservedCheckpoints.set(msg.ts, msg.checkpoint)
  207. }
  208. }
  209. // Delete this message and all subsequent messages
  210. await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiIndexToUse)
  211. // Restore checkpoint associations for preserved messages
  212. for (const [ts, checkpoint] of preservedCheckpoints) {
  213. const msgIndex = currentCline.clineMessages.findIndex((msg) => msg.ts === ts)
  214. if (msgIndex !== -1) {
  215. currentCline.clineMessages[msgIndex].checkpoint = checkpoint
  216. }
  217. }
  218. // Save the updated messages with restored checkpoints
  219. await saveTaskMessages({
  220. messages: currentCline.clineMessages,
  221. taskId: currentCline.taskId,
  222. globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
  223. })
  224. // Update the UI to reflect the deletion
  225. await provider.postStateToWebview()
  226. }
  227. } catch (error) {
  228. console.error("Error in delete message:", error)
  229. vscode.window.showErrorMessage(
  230. t("common:errors.message.error_deleting_message", {
  231. error: error instanceof Error ? error.message : String(error),
  232. }),
  233. )
  234. }
  235. }
  236. /**
  237. * Handles message editing operations with user confirmation
  238. */
  239. const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise<void> => {
  240. // Check if there's a checkpoint before this message
  241. const currentCline = provider.getCurrentTask()
  242. let hasCheckpoint = false
  243. if (currentCline) {
  244. const { messageIndex } = findMessageIndices(messageTs, currentCline)
  245. if (messageIndex !== -1) {
  246. // Find the last checkpoint before this message
  247. const checkpoints = currentCline.clineMessages.filter(
  248. (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
  249. )
  250. hasCheckpoint = checkpoints.length > 0
  251. } else {
  252. console.log("[webviewMessageHandler] Edit - Message not found in clineMessages!")
  253. }
  254. } else {
  255. console.log("[webviewMessageHandler] Edit - No currentCline available!")
  256. }
  257. // Send message to webview to show edit confirmation dialog
  258. await provider.postMessageToWebview({
  259. type: "showEditMessageDialog",
  260. messageTs,
  261. text: editedContent,
  262. hasCheckpoint,
  263. images,
  264. })
  265. }
  266. /**
  267. * Handles confirmed message editing from webview dialog
  268. */
  269. const handleEditMessageConfirm = async (
  270. messageTs: number,
  271. editedContent: string,
  272. restoreCheckpoint?: boolean,
  273. images?: string[],
  274. ): Promise<void> => {
  275. const currentCline = provider.getCurrentTask()
  276. if (!currentCline) {
  277. console.error("[handleEditMessageConfirm] No current cline available")
  278. return
  279. }
  280. // Use findMessageIndices to find messages based on timestamp
  281. const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
  282. if (messageIndex === -1) {
  283. const errorMessage = t("common:errors.message.message_not_found", { messageTs })
  284. console.error("[handleEditMessageConfirm]", errorMessage)
  285. await vscode.window.showErrorMessage(errorMessage)
  286. return
  287. }
  288. try {
  289. const targetMessage = currentCline.clineMessages[messageIndex]
  290. // If checkpoint restoration is requested, find and restore to the last checkpoint before this message
  291. if (restoreCheckpoint) {
  292. // Find the last checkpoint before this message
  293. const checkpoints = currentCline.clineMessages.filter(
  294. (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
  295. )
  296. const nextCheckpoint = checkpoints[0]
  297. if (nextCheckpoint && nextCheckpoint.text) {
  298. await handleCheckpointRestoreOperation({
  299. provider,
  300. currentCline,
  301. messageTs: targetMessage.ts!,
  302. messageIndex,
  303. checkpoint: { hash: nextCheckpoint.text },
  304. operation: "edit",
  305. editData: {
  306. editedContent,
  307. images,
  308. apiConversationHistoryIndex,
  309. },
  310. })
  311. // The task will be cancelled and reinitialized by checkpointRestore
  312. // The pending edit will be processed in the reinitialized task
  313. return
  314. } else {
  315. // No checkpoint found before this message
  316. console.log("[handleEditMessageConfirm] No checkpoint found before message")
  317. vscode.window.showWarningMessage("No checkpoint found before this message")
  318. // Continue with non-checkpoint edit
  319. }
  320. }
  321. // For non-checkpoint edits, remove the ORIGINAL user message being edited and all subsequent messages
  322. // Determine the correct starting index to delete from (prefer the last preceding user_feedback message)
  323. let deleteFromMessageIndex = messageIndex
  324. let deleteFromApiIndex = apiConversationHistoryIndex
  325. // Find the nearest preceding user message to ensure we replace the original, not just the assistant reply
  326. for (let i = messageIndex; i >= 0; i--) {
  327. const m = currentCline.clineMessages[i]
  328. if (m?.say === "user_feedback") {
  329. deleteFromMessageIndex = i
  330. // Align API history truncation to the same user message timestamp if present
  331. const userTs = m.ts
  332. if (typeof userTs === "number") {
  333. const apiIdx = currentCline.apiConversationHistory.findIndex(
  334. (am: ApiMessage) => am.ts === userTs,
  335. )
  336. if (apiIdx !== -1) {
  337. deleteFromApiIndex = apiIdx
  338. }
  339. }
  340. break
  341. }
  342. }
  343. // Timestamp fallback for API history when exact user message isn't present
  344. if (deleteFromApiIndex === -1) {
  345. const tsThresholdForEdit = currentCline.clineMessages[deleteFromMessageIndex]?.ts
  346. if (typeof tsThresholdForEdit === "number") {
  347. deleteFromApiIndex = findFirstApiIndexAtOrAfter(tsThresholdForEdit, currentCline)
  348. }
  349. }
  350. // Store checkpoints from messages that will be preserved
  351. const preservedCheckpoints = new Map<number, any>()
  352. for (let i = 0; i < deleteFromMessageIndex; i++) {
  353. const msg = currentCline.clineMessages[i]
  354. if (msg?.checkpoint && msg.ts) {
  355. preservedCheckpoints.set(msg.ts, msg.checkpoint)
  356. }
  357. }
  358. // Delete the original (user) message and all subsequent messages
  359. await removeMessagesThisAndSubsequent(currentCline, deleteFromMessageIndex, deleteFromApiIndex)
  360. // Restore checkpoint associations for preserved messages
  361. for (const [ts, checkpoint] of preservedCheckpoints) {
  362. const msgIndex = currentCline.clineMessages.findIndex((msg) => msg.ts === ts)
  363. if (msgIndex !== -1) {
  364. currentCline.clineMessages[msgIndex].checkpoint = checkpoint
  365. }
  366. }
  367. // Save the updated messages with restored checkpoints
  368. await saveTaskMessages({
  369. messages: currentCline.clineMessages,
  370. taskId: currentCline.taskId,
  371. globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
  372. })
  373. // Update the UI to reflect the deletion
  374. await provider.postStateToWebview()
  375. await currentCline.submitUserMessage(editedContent, images)
  376. } catch (error) {
  377. console.error("Error in edit message:", error)
  378. vscode.window.showErrorMessage(
  379. t("common:errors.message.error_editing_message", {
  380. error: error instanceof Error ? error.message : String(error),
  381. }),
  382. )
  383. }
  384. }
  385. /**
  386. * Handles message modification operations (delete or edit) with confirmation dialog
  387. * @param messageTs Timestamp of the message to operate on
  388. * @param operation Type of operation ('delete' or 'edit')
  389. * @param editedContent New content for edit operations
  390. * @returns Promise<void>
  391. */
  392. const handleMessageModificationsOperation = async (
  393. messageTs: number,
  394. operation: "delete" | "edit",
  395. editedContent?: string,
  396. images?: string[],
  397. ): Promise<void> => {
  398. if (operation === "delete") {
  399. await handleDeleteOperation(messageTs)
  400. } else if (operation === "edit" && editedContent) {
  401. await handleEditOperation(messageTs, editedContent, images)
  402. }
  403. }
  404. switch (message.type) {
  405. case "webviewDidLaunch":
  406. // Load custom modes first
  407. const customModes = await provider.customModesManager.getCustomModes()
  408. await updateGlobalState("customModes", customModes)
  409. // kilocode_change start: Fetch organization modes on startup
  410. // Fetch organization modes on startup if an organization is selected
  411. await fetchAndRefreshOrganizationModesOnStartup(provider, updateGlobalState)
  412. // kilocode_change end
  413. // Refresh workflow toggles
  414. const { refreshWorkflowToggles } = await import("../context/instructions/workflows") // kilocode_change
  415. await refreshWorkflowToggles(provider.context, provider.cwd) // kilocode_change
  416. provider.postStateToWebview()
  417. provider.postRulesDataToWebview() // kilocode_change: send workflows and rules immediately
  418. provider.workspaceTracker?.initializeFilePaths() // Don't await.
  419. getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }))
  420. // If MCP Hub is already initialized, update the webview with
  421. // current server list.
  422. const mcpHub = provider.getMcpHub()
  423. if (mcpHub) {
  424. provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() })
  425. }
  426. provider.providerSettingsManager
  427. .listConfig()
  428. .then(async (listApiConfig) => {
  429. if (!listApiConfig) {
  430. return
  431. }
  432. if (listApiConfig.length === 1) {
  433. // Check if first time init then sync with exist config.
  434. if (!checkExistKey(listApiConfig[0])) {
  435. const { apiConfiguration } = await provider.getState()
  436. await provider.providerSettingsManager.saveConfig(
  437. listApiConfig[0].name ?? "default",
  438. apiConfiguration,
  439. )
  440. listApiConfig[0].apiProvider = apiConfiguration.apiProvider
  441. }
  442. }
  443. const currentConfigName = getGlobalState("currentApiConfigName")
  444. if (currentConfigName) {
  445. if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {
  446. // Current config name not valid, get first config in list.
  447. const name = listApiConfig[0]?.name
  448. await updateGlobalState("currentApiConfigName", name)
  449. if (name) {
  450. await provider.activateProviderProfile({ name })
  451. return
  452. }
  453. }
  454. }
  455. await Promise.all([
  456. await updateGlobalState("listApiConfigMeta", listApiConfig),
  457. await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
  458. ])
  459. })
  460. .catch((error) =>
  461. provider.log(
  462. `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  463. ),
  464. )
  465. // If user already opted in to telemetry, enable telemetry service
  466. provider.getStateToPostToWebview().then(async (/*kilocode_change*/ state) => {
  467. const { telemetrySetting } = state
  468. const isOptedIn = telemetrySetting !== "disabled"
  469. TelemetryService.instance.updateTelemetryState(isOptedIn)
  470. await TelemetryService.instance.updateIdentity(state.apiConfiguration.kilocodeToken ?? "") // kilocode_change
  471. })
  472. provider.isViewLaunched = true
  473. break
  474. case "newTask":
  475. // Initializing new instance of Cline will make sure that any
  476. // agentically running promises in old instance don't affect our new
  477. // task. This essentially creates a fresh slate for the new task.
  478. try {
  479. await provider.createTask(message.text, message.images)
  480. // Task created successfully - notify the UI to reset
  481. await provider.postMessageToWebview({
  482. type: "invoke",
  483. invoke: "newChat",
  484. })
  485. } catch (error) {
  486. // For all errors, reset the UI and show error
  487. await provider.postMessageToWebview({
  488. type: "invoke",
  489. invoke: "newChat",
  490. })
  491. // Show error to user
  492. vscode.window.showErrorMessage(
  493. `Failed to create task: ${error instanceof Error ? error.message : String(error)}`,
  494. )
  495. }
  496. break
  497. // kilocode_change start
  498. case "condense":
  499. provider.getCurrentTask()?.handleWebviewAskResponse("yesButtonClicked")
  500. break
  501. // kilocode_change end
  502. case "customInstructions":
  503. await provider.updateCustomInstructions(message.text)
  504. break
  505. case "alwaysAllowReadOnly":
  506. await updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined)
  507. await provider.postStateToWebview()
  508. break
  509. case "alwaysAllowReadOnlyOutsideWorkspace":
  510. await updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined)
  511. await provider.postStateToWebview()
  512. break
  513. case "alwaysAllowWrite":
  514. await updateGlobalState("alwaysAllowWrite", message.bool ?? undefined)
  515. await provider.postStateToWebview()
  516. break
  517. case "alwaysAllowWriteOutsideWorkspace":
  518. await updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined)
  519. await provider.postStateToWebview()
  520. break
  521. case "alwaysAllowWriteProtected":
  522. await updateGlobalState("alwaysAllowWriteProtected", message.bool ?? undefined)
  523. await provider.postStateToWebview()
  524. break
  525. case "alwaysAllowExecute":
  526. await updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
  527. await provider.postStateToWebview()
  528. break
  529. case "alwaysAllowBrowser":
  530. await updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined)
  531. await provider.postStateToWebview()
  532. break
  533. case "alwaysAllowMcp":
  534. await updateGlobalState("alwaysAllowMcp", message.bool)
  535. await provider.postStateToWebview()
  536. break
  537. case "alwaysAllowModeSwitch":
  538. await updateGlobalState("alwaysAllowModeSwitch", message.bool)
  539. await provider.postStateToWebview()
  540. break
  541. case "allowedMaxRequests":
  542. await updateGlobalState("allowedMaxRequests", message.value)
  543. await provider.postStateToWebview()
  544. break
  545. case "allowedMaxCost":
  546. await updateGlobalState("allowedMaxCost", message.value)
  547. await provider.postStateToWebview()
  548. break
  549. case "alwaysAllowSubtasks":
  550. await updateGlobalState("alwaysAllowSubtasks", message.bool)
  551. await provider.postStateToWebview()
  552. break
  553. case "alwaysAllowUpdateTodoList":
  554. await updateGlobalState("alwaysAllowUpdateTodoList", message.bool)
  555. await provider.postStateToWebview()
  556. break
  557. case "askResponse":
  558. provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
  559. break
  560. case "autoCondenseContext":
  561. await updateGlobalState("autoCondenseContext", message.bool)
  562. await provider.postStateToWebview()
  563. break
  564. case "autoCondenseContextPercent":
  565. await updateGlobalState("autoCondenseContextPercent", message.value)
  566. await provider.postStateToWebview()
  567. break
  568. case "terminalOperation":
  569. if (message.terminalOperation) {
  570. provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation)
  571. }
  572. break
  573. case "clearTask":
  574. // Clear task resets the current session and allows for a new task
  575. // to be started, if this session is a subtask - it allows the
  576. // parent task to be resumed.
  577. // Check if the current task actually has a parent task.
  578. const currentTask = provider.getCurrentTask()
  579. if (currentTask && currentTask.parentTask) {
  580. await provider.finishSubTask(t("common:tasks.canceled"))
  581. } else {
  582. // Regular task - just clear it
  583. await provider.clearTask()
  584. }
  585. await provider.postStateToWebview()
  586. break
  587. case "didShowAnnouncement":
  588. await updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId)
  589. await provider.postStateToWebview()
  590. break
  591. case "selectImages":
  592. const images = await selectImages()
  593. await provider.postMessageToWebview({
  594. type: "selectedImages",
  595. images,
  596. context: message.context,
  597. messageTs: message.messageTs,
  598. })
  599. break
  600. case "exportCurrentTask":
  601. const currentTaskId = provider.getCurrentTask()?.taskId
  602. if (currentTaskId) {
  603. provider.exportTaskWithId(currentTaskId)
  604. }
  605. break
  606. case "shareCurrentTask":
  607. const shareTaskId = provider.getCurrentTask()?.taskId
  608. const clineMessages = provider.getCurrentTask()?.clineMessages
  609. if (!shareTaskId) {
  610. vscode.window.showErrorMessage(t("common:errors.share_no_active_task"))
  611. break
  612. }
  613. try {
  614. const visibility = message.visibility || "organization"
  615. const result = await CloudService.instance.shareTask(shareTaskId, visibility)
  616. if (result.success && result.shareUrl) {
  617. // Show success notification
  618. const messageKey =
  619. visibility === "public"
  620. ? "common:info.public_share_link_copied"
  621. : "common:info.organization_share_link_copied"
  622. vscode.window.showInformationMessage(t(messageKey))
  623. // Send success feedback to webview for inline display
  624. await provider.postMessageToWebview({
  625. type: "shareTaskSuccess",
  626. visibility,
  627. text: result.shareUrl,
  628. })
  629. } else {
  630. // Handle error
  631. const errorMessage = result.error || "Failed to create share link"
  632. if (errorMessage.includes("Authentication")) {
  633. vscode.window.showErrorMessage(t("common:errors.share_auth_required"))
  634. } else if (errorMessage.includes("sharing is not enabled")) {
  635. vscode.window.showErrorMessage(t("common:errors.share_not_enabled"))
  636. } else if (errorMessage.includes("not found")) {
  637. vscode.window.showErrorMessage(t("common:errors.share_task_not_found"))
  638. } else {
  639. vscode.window.showErrorMessage(errorMessage)
  640. }
  641. }
  642. } catch (error) {
  643. provider.log(`[shareCurrentTask] Unexpected error: ${error}`)
  644. vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
  645. }
  646. break
  647. case "showTaskWithId":
  648. provider.showTaskWithId(message.text!)
  649. break
  650. case "condenseTaskContextRequest":
  651. provider.condenseTaskContext(message.text!)
  652. break
  653. case "deleteTaskWithId":
  654. provider.deleteTaskWithId(message.text!)
  655. break
  656. case "deleteMultipleTasksWithIds": {
  657. const ids = message.ids
  658. if (Array.isArray(ids)) {
  659. // Process in batches of 20 (or another reasonable number)
  660. const batchSize = 20
  661. const results = []
  662. // Only log start and end of the operation
  663. console.log(`Batch deletion started: ${ids.length} tasks total`)
  664. for (let i = 0; i < ids.length; i += batchSize) {
  665. const batch = ids.slice(i, i + batchSize)
  666. const batchPromises = batch.map(async (id) => {
  667. try {
  668. await provider.deleteTaskWithId(id)
  669. return { id, success: true }
  670. } catch (error) {
  671. // Keep error logging for debugging purposes
  672. console.log(
  673. `Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`,
  674. )
  675. return { id, success: false }
  676. }
  677. })
  678. // Process each batch in parallel but wait for completion before starting the next batch
  679. const batchResults = await Promise.all(batchPromises)
  680. results.push(...batchResults)
  681. // Update the UI after each batch to show progress
  682. await provider.postStateToWebview()
  683. }
  684. // Log final results
  685. const successCount = results.filter((r) => r.success).length
  686. const failCount = results.length - successCount
  687. console.log(
  688. `Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`,
  689. )
  690. }
  691. break
  692. }
  693. case "exportTaskWithId":
  694. provider.exportTaskWithId(message.text!)
  695. break
  696. case "importSettings": {
  697. await importSettingsWithFeedback({
  698. providerSettingsManager: provider.providerSettingsManager,
  699. contextProxy: provider.contextProxy,
  700. customModesManager: provider.customModesManager,
  701. provider: provider,
  702. })
  703. break
  704. }
  705. case "exportSettings":
  706. await exportSettings({
  707. providerSettingsManager: provider.providerSettingsManager,
  708. contextProxy: provider.contextProxy,
  709. })
  710. break
  711. case "resetState":
  712. await provider.resetState()
  713. break
  714. case "flushRouterModels":
  715. const routerNameFlush: RouterName = toRouterName(message.text)
  716. await flushModels(routerNameFlush)
  717. break
  718. case "requestRouterModels":
  719. const { apiConfiguration } = await provider.getState()
  720. const routerModels: Record<RouterName, ModelRecord> = {
  721. openrouter: {},
  722. gemini: {}, // kilocode_change
  723. "vercel-ai-gateway": {},
  724. huggingface: {},
  725. litellm: {},
  726. "kilocode-openrouter": {}, // kilocode_change
  727. deepinfra: {},
  728. "io-intelligence": {},
  729. requesty: {},
  730. unbound: {},
  731. glama: {},
  732. chutes: {}, // kilocode_change
  733. ollama: {},
  734. lmstudio: {},
  735. ovhcloud: {}, // kilocode_change
  736. }
  737. const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
  738. try {
  739. return await getModels(options)
  740. } catch (error) {
  741. console.error(
  742. `Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
  743. error,
  744. )
  745. throw error // Re-throw to be caught by Promise.allSettled.
  746. }
  747. }
  748. // kilocode_change start: openrouter auth, kilocode provider
  749. const openRouterApiKey = apiConfiguration.openRouterApiKey || message?.values?.openRouterApiKey
  750. const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || message?.values?.openRouterBaseUrl
  751. const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
  752. {
  753. key: "openrouter",
  754. options: { provider: "openrouter", apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl },
  755. },
  756. // kilocode_change start
  757. {
  758. key: "gemini",
  759. options: {
  760. provider: "gemini",
  761. apiKey: apiConfiguration.geminiApiKey,
  762. baseUrl: apiConfiguration.googleGeminiBaseUrl,
  763. },
  764. },
  765. // kilocode_change end
  766. {
  767. key: "requesty",
  768. options: {
  769. provider: "requesty",
  770. apiKey: apiConfiguration.requestyApiKey,
  771. baseUrl: apiConfiguration.requestyBaseUrl,
  772. },
  773. },
  774. { key: "glama", options: { provider: "glama" } },
  775. { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
  776. { key: "chutes", options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey } }, // kilocode_change
  777. {
  778. key: "kilocode-openrouter",
  779. options: {
  780. provider: "kilocode-openrouter",
  781. kilocodeToken: apiConfiguration.kilocodeToken,
  782. kilocodeOrganizationId: apiConfiguration.kilocodeOrganizationId,
  783. },
  784. },
  785. { key: "ollama", options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl } },
  786. { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
  787. {
  788. key: "deepinfra",
  789. options: {
  790. provider: "deepinfra",
  791. apiKey: apiConfiguration.deepInfraApiKey,
  792. baseUrl: apiConfiguration.deepInfraBaseUrl,
  793. },
  794. },
  795. // kilocode_change start
  796. {
  797. key: "ovhcloud",
  798. options: {
  799. provider: "ovhcloud",
  800. apiKey: apiConfiguration.ovhCloudAiEndpointsApiKey,
  801. baseUrl: apiConfiguration.ovhCloudAiEndpointsBaseUrl,
  802. },
  803. },
  804. // kilocode_change end
  805. ]
  806. // kilocode_change end
  807. // Add IO Intelligence if API key is provided.
  808. const ioIntelligenceApiKey = apiConfiguration.ioIntelligenceApiKey
  809. if (ioIntelligenceApiKey) {
  810. modelFetchPromises.push({
  811. key: "io-intelligence",
  812. options: { provider: "io-intelligence", apiKey: ioIntelligenceApiKey },
  813. })
  814. }
  815. // Don't fetch Ollama and LM Studio models by default anymore.
  816. // They have their own specific handlers: requestOllamaModels and requestLmStudioModels.
  817. const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
  818. const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
  819. if (litellmApiKey && litellmBaseUrl) {
  820. modelFetchPromises.push({
  821. key: "litellm",
  822. options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },
  823. })
  824. }
  825. const results = await Promise.allSettled(
  826. modelFetchPromises.map(async ({ key, options }) => {
  827. const models = await safeGetModels(options)
  828. return { key, models } // The key is `ProviderName` here.
  829. }),
  830. )
  831. results.forEach((result, index) => {
  832. const routerName = modelFetchPromises[index].key
  833. if (result.status === "fulfilled") {
  834. routerModels[routerName] = result.value.models
  835. // Ollama and LM Studio settings pages still need these events.
  836. if (routerName === "ollama" && Object.keys(result.value.models).length > 0) {
  837. provider.postMessageToWebview({
  838. type: "ollamaModels",
  839. ollamaModels: result.value.models,
  840. })
  841. } else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) {
  842. provider.postMessageToWebview({
  843. type: "lmStudioModels",
  844. lmStudioModels: result.value.models,
  845. })
  846. }
  847. } else {
  848. // Handle rejection: Post a specific error message for this provider.
  849. const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
  850. console.error(`Error fetching models for ${routerName}:`, result.reason)
  851. routerModels[routerName] = {} // Ensure it's an empty object in the main routerModels message.
  852. provider.postMessageToWebview({
  853. type: "singleRouterModelFetchResponse",
  854. success: false,
  855. error: errorMessage,
  856. values: { provider: routerName },
  857. })
  858. }
  859. })
  860. provider.postMessageToWebview({ type: "routerModels", routerModels })
  861. break
  862. case "requestOllamaModels": {
  863. // Specific handler for Ollama models only.
  864. const { apiConfiguration: ollamaApiConfig } = await provider.getState()
  865. try {
  866. // Flush cache first to ensure fresh models.
  867. await flushModels("ollama")
  868. const ollamaModels = await getModels({
  869. provider: "ollama",
  870. baseUrl: ollamaApiConfig.ollamaBaseUrl,
  871. apiKey: ollamaApiConfig.ollamaApiKey,
  872. numCtx: ollamaApiConfig.ollamaNumCtx, // kilocode_change
  873. })
  874. if (Object.keys(ollamaModels).length > 0) {
  875. provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels })
  876. }
  877. } catch (error) {
  878. // Silently fail - user hasn't configured Ollama yet
  879. console.debug("Ollama models fetch failed:", error)
  880. }
  881. break
  882. }
  883. case "requestLmStudioModels": {
  884. // Specific handler for LM Studio models only.
  885. const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
  886. try {
  887. // Flush cache first to ensure fresh models.
  888. await flushModels("lmstudio")
  889. const lmStudioModels = await getModels({
  890. provider: "lmstudio",
  891. baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
  892. })
  893. if (Object.keys(lmStudioModels).length > 0) {
  894. provider.postMessageToWebview({
  895. type: "lmStudioModels",
  896. lmStudioModels: lmStudioModels,
  897. })
  898. }
  899. } catch (error) {
  900. // Silently fail - user hasn't configured LM Studio yet.
  901. console.debug("LM Studio models fetch failed:", error)
  902. }
  903. break
  904. }
  905. case "requestOpenAiModels":
  906. if (message?.values?.baseUrl && message?.values?.apiKey) {
  907. const openAiModels = await getOpenAiModels(
  908. message?.values?.baseUrl,
  909. message?.values?.apiKey,
  910. message?.values?.openAiHeaders,
  911. )
  912. provider.postMessageToWebview({ type: "openAiModels", openAiModels })
  913. }
  914. break
  915. case "requestVsCodeLmModels":
  916. const vsCodeLmModels = await getVsCodeLmModels()
  917. // TODO: Cache like we do for OpenRouter, etc?
  918. provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
  919. break
  920. case "requestHuggingFaceModels":
  921. // TODO: Why isn't this handled by `requestRouterModels` above?
  922. try {
  923. const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface")
  924. const huggingFaceModelsResponse = await getHuggingFaceModelsWithMetadata()
  925. provider.postMessageToWebview({
  926. type: "huggingFaceModels",
  927. huggingFaceModels: huggingFaceModelsResponse.models,
  928. })
  929. } catch (error) {
  930. console.error("Failed to fetch Hugging Face models:", error)
  931. provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] })
  932. }
  933. break
  934. case "openImage":
  935. openImage(message.text!, { values: message.values })
  936. break
  937. case "saveImage":
  938. saveImage(message.dataUri!)
  939. break
  940. case "openFile":
  941. let filePath: string = message.text!
  942. if (!path.isAbsolute(filePath)) {
  943. filePath = path.join(getCurrentCwd(), filePath)
  944. }
  945. openFile(filePath, message.values as { create?: boolean; content?: string; line?: number })
  946. break
  947. case "openMention":
  948. openMention(getCurrentCwd(), message.text)
  949. break
  950. case "openExternal":
  951. if (message.url) {
  952. vscode.env.openExternal(vscode.Uri.parse(message.url))
  953. }
  954. break
  955. case "checkpointDiff":
  956. const result = checkoutDiffPayloadSchema.safeParse(message.payload)
  957. if (result.success) {
  958. await provider.getCurrentTask()?.checkpointDiff(result.data)
  959. }
  960. break
  961. // kilocode_change start
  962. case "seeNewChanges":
  963. const task = provider.getCurrentTask()
  964. if (task && message.payload && message.payload) {
  965. await seeNewChanges(task, (message.payload as SeeNewChangesPayload).commitRange)
  966. }
  967. break
  968. case "tasksByIdRequest": {
  969. const request = message.payload as TasksByIdRequestPayload
  970. await provider.postMessageToWebview({
  971. type: "tasksByIdResponse",
  972. payload: {
  973. requestId: request.requestId,
  974. tasks: provider.getTaskHistory().filter((task) => request.taskIds.includes(task.id)),
  975. },
  976. })
  977. break
  978. }
  979. case "taskHistoryRequest": {
  980. await provider.postMessageToWebview({
  981. type: "taskHistoryResponse",
  982. payload: getTaskHistory(
  983. provider.getTaskHistory(),
  984. provider.cwd,
  985. message.payload as TaskHistoryRequestPayload,
  986. ),
  987. })
  988. break
  989. }
  990. // kilocode_change end
  991. case "checkpointRestore": {
  992. const result = checkoutRestorePayloadSchema.safeParse(message.payload)
  993. if (result.success) {
  994. await provider.cancelTask()
  995. try {
  996. await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 })
  997. } catch (error) {
  998. vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout"))
  999. }
  1000. try {
  1001. await provider.getCurrentTask()?.checkpointRestore(result.data)
  1002. } catch (error) {
  1003. vscode.window.showErrorMessage(t("common:errors.checkpoint_failed"))
  1004. }
  1005. }
  1006. break
  1007. }
  1008. case "cancelTask":
  1009. await provider.cancelTask()
  1010. break
  1011. case "allowedCommands": {
  1012. // Validate and sanitize the commands array
  1013. const commands = message.commands ?? []
  1014. const validCommands = Array.isArray(commands)
  1015. ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
  1016. : []
  1017. await updateGlobalState("allowedCommands", validCommands)
  1018. // Also update workspace settings.
  1019. await vscode.workspace
  1020. .getConfiguration(Package.name)
  1021. .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global)
  1022. break
  1023. }
  1024. case "deniedCommands": {
  1025. // Validate and sanitize the commands array
  1026. const commands = message.commands ?? []
  1027. const validCommands = Array.isArray(commands)
  1028. ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
  1029. : []
  1030. await updateGlobalState("deniedCommands", validCommands)
  1031. // Also update workspace settings.
  1032. await vscode.workspace
  1033. .getConfiguration(Package.name)
  1034. .update("deniedCommands", validCommands, vscode.ConfigurationTarget.Global)
  1035. break
  1036. }
  1037. case "openCustomModesSettings": {
  1038. const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()
  1039. if (customModesFilePath) {
  1040. openFile(customModesFilePath)
  1041. }
  1042. break
  1043. }
  1044. case "openKeyboardShortcuts": {
  1045. // Open VSCode keyboard shortcuts settings and optionally filter to show the Roo Code commands
  1046. const searchQuery = message.text || ""
  1047. if (searchQuery) {
  1048. // Open with a search query pre-filled
  1049. await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", searchQuery)
  1050. } else {
  1051. // Just open the keyboard shortcuts settings
  1052. await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings")
  1053. }
  1054. break
  1055. }
  1056. case "openMcpSettings": {
  1057. const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()
  1058. if (mcpSettingsFilePath) {
  1059. openFile(mcpSettingsFilePath)
  1060. }
  1061. break
  1062. }
  1063. case "openProjectMcpSettings": {
  1064. if (!vscode.workspace.workspaceFolders?.length) {
  1065. vscode.window.showErrorMessage(t("common:errors.no_workspace"))
  1066. return
  1067. }
  1068. const workspaceFolder = getCurrentCwd()
  1069. const rooDir = path.join(workspaceFolder, ".kilocode")
  1070. const mcpPath = path.join(rooDir, "mcp.json")
  1071. try {
  1072. await fs.mkdir(rooDir, { recursive: true })
  1073. const exists = await fileExistsAtPath(mcpPath)
  1074. if (!exists) {
  1075. await safeWriteJson(mcpPath, { mcpServers: {} })
  1076. }
  1077. openFile(mcpPath)
  1078. } catch (error) {
  1079. vscode.window.showErrorMessage(t("mcp:errors.create_json", { error: `${error}` }))
  1080. }
  1081. break
  1082. }
  1083. case "deleteMcpServer": {
  1084. if (!message.serverName) {
  1085. break
  1086. }
  1087. try {
  1088. provider.log(`Attempting to delete MCP server: ${message.serverName}`)
  1089. await provider.getMcpHub()?.deleteServer(message.serverName, message.source as "global" | "project")
  1090. provider.log(`Successfully deleted MCP server: ${message.serverName}`)
  1091. // Refresh the webview state
  1092. await provider.postStateToWebview()
  1093. } catch (error) {
  1094. const errorMessage = error instanceof Error ? error.message : String(error)
  1095. provider.log(`Failed to delete MCP server: ${errorMessage}`)
  1096. // Error messages are already handled by McpHub.deleteServer
  1097. }
  1098. break
  1099. }
  1100. case "restartMcpServer": {
  1101. try {
  1102. await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project")
  1103. } catch (error) {
  1104. provider.log(
  1105. `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1106. )
  1107. }
  1108. break
  1109. }
  1110. case "toggleToolAlwaysAllow": {
  1111. try {
  1112. await provider
  1113. .getMcpHub()
  1114. ?.toggleToolAlwaysAllow(
  1115. message.serverName!,
  1116. message.source as "global" | "project",
  1117. message.toolName!,
  1118. Boolean(message.alwaysAllow),
  1119. )
  1120. } catch (error) {
  1121. provider.log(
  1122. `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1123. )
  1124. }
  1125. break
  1126. }
  1127. case "toggleToolEnabledForPrompt": {
  1128. try {
  1129. await provider
  1130. .getMcpHub()
  1131. ?.toggleToolEnabledForPrompt(
  1132. message.serverName!,
  1133. message.source as "global" | "project",
  1134. message.toolName!,
  1135. Boolean(message.isEnabled),
  1136. )
  1137. } catch (error) {
  1138. provider.log(
  1139. `Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1140. )
  1141. }
  1142. break
  1143. }
  1144. case "toggleMcpServer": {
  1145. try {
  1146. await provider
  1147. .getMcpHub()
  1148. ?.toggleServerDisabled(
  1149. message.serverName!,
  1150. message.disabled!,
  1151. message.source as "global" | "project",
  1152. )
  1153. } catch (error) {
  1154. provider.log(
  1155. `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1156. )
  1157. }
  1158. break
  1159. }
  1160. case "mcpEnabled":
  1161. const mcpEnabled = message.bool ?? true
  1162. await updateGlobalState("mcpEnabled", mcpEnabled)
  1163. const mcpHubInstance = provider.getMcpHub()
  1164. if (mcpHubInstance) {
  1165. await mcpHubInstance.handleMcpEnabledChange(mcpEnabled)
  1166. }
  1167. await provider.postStateToWebview()
  1168. break
  1169. case "enableMcpServerCreation":
  1170. await updateGlobalState("enableMcpServerCreation", message.bool ?? true)
  1171. await provider.postStateToWebview()
  1172. break
  1173. // kilocode_change begin
  1174. case "openGlobalKeybindings":
  1175. vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", message.text ?? "kilo-code.")
  1176. break
  1177. case "showSystemNotification":
  1178. const isSystemNotificationsEnabled = getGlobalState("systemNotificationsEnabled") ?? true
  1179. if (!isSystemNotificationsEnabled && !message.alwaysAllow) {
  1180. break
  1181. }
  1182. if (message.notificationOptions) {
  1183. showSystemNotification(message.notificationOptions)
  1184. }
  1185. break
  1186. case "systemNotificationsEnabled":
  1187. const systemNotificationsEnabled = message.bool ?? true
  1188. await updateGlobalState("systemNotificationsEnabled", systemNotificationsEnabled)
  1189. await provider.postStateToWebview()
  1190. break
  1191. case "openInBrowser":
  1192. if (message.url) {
  1193. vscode.env.openExternal(vscode.Uri.parse(message.url))
  1194. }
  1195. break
  1196. // kilocode_change end
  1197. case "remoteControlEnabled":
  1198. try {
  1199. await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false })
  1200. } catch (error) {
  1201. provider.log(
  1202. `CloudService#updateUserSettings failed: ${error instanceof Error ? error.message : String(error)}`,
  1203. )
  1204. }
  1205. break
  1206. case "taskSyncEnabled":
  1207. const enabled = message.bool ?? false
  1208. const updatedSettings: Partial<UserSettingsConfig> = {
  1209. taskSyncEnabled: enabled,
  1210. }
  1211. // If disabling task sync, also disable remote control
  1212. if (!enabled) {
  1213. updatedSettings.extensionBridgeEnabled = false
  1214. }
  1215. try {
  1216. await CloudService.instance.updateUserSettings(updatedSettings)
  1217. } catch (error) {
  1218. provider.log(`Failed to update cloud settings for task sync: ${error}`)
  1219. }
  1220. break
  1221. case "refreshAllMcpServers": {
  1222. const mcpHub = provider.getMcpHub()
  1223. if (mcpHub) {
  1224. await mcpHub.refreshAllConnections()
  1225. }
  1226. break
  1227. }
  1228. case "soundEnabled":
  1229. const soundEnabled = message.bool ?? true
  1230. await updateGlobalState("soundEnabled", soundEnabled)
  1231. await provider.postStateToWebview()
  1232. break
  1233. case "soundVolume":
  1234. const soundVolume = message.value ?? 0.5
  1235. await updateGlobalState("soundVolume", soundVolume)
  1236. await provider.postStateToWebview()
  1237. break
  1238. case "ttsEnabled":
  1239. const ttsEnabled = message.bool ?? true
  1240. await updateGlobalState("ttsEnabled", ttsEnabled)
  1241. setTtsEnabled(ttsEnabled)
  1242. await provider.postStateToWebview()
  1243. break
  1244. case "ttsSpeed":
  1245. const ttsSpeed = message.value ?? 1.0
  1246. await updateGlobalState("ttsSpeed", ttsSpeed)
  1247. setTtsSpeed(ttsSpeed)
  1248. await provider.postStateToWebview()
  1249. break
  1250. case "playTts":
  1251. if (message.text) {
  1252. playTts(message.text, {
  1253. onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }),
  1254. onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }),
  1255. })
  1256. }
  1257. break
  1258. case "stopTts":
  1259. stopTts()
  1260. break
  1261. case "diffEnabled":
  1262. const diffEnabled = message.bool ?? true
  1263. await updateGlobalState("diffEnabled", diffEnabled)
  1264. await provider.postStateToWebview()
  1265. break
  1266. case "enableCheckpoints":
  1267. const enableCheckpoints = message.bool ?? true
  1268. await updateGlobalState("enableCheckpoints", enableCheckpoints)
  1269. await provider.postStateToWebview()
  1270. break
  1271. case "browserViewportSize":
  1272. const browserViewportSize = message.text ?? "900x600"
  1273. await updateGlobalState("browserViewportSize", browserViewportSize)
  1274. await provider.postStateToWebview()
  1275. break
  1276. case "remoteBrowserHost":
  1277. await updateGlobalState("remoteBrowserHost", message.text)
  1278. await provider.postStateToWebview()
  1279. break
  1280. case "remoteBrowserEnabled":
  1281. // Store the preference in global state
  1282. // remoteBrowserEnabled now means "enable remote browser connection"
  1283. await updateGlobalState("remoteBrowserEnabled", message.bool ?? false)
  1284. // If disabling remote browser connection, clear the remoteBrowserHost
  1285. if (!message.bool) {
  1286. await updateGlobalState("remoteBrowserHost", undefined)
  1287. }
  1288. await provider.postStateToWebview()
  1289. break
  1290. case "testBrowserConnection":
  1291. // If no text is provided, try auto-discovery
  1292. if (!message.text) {
  1293. // Use testBrowserConnection for auto-discovery
  1294. const chromeHostUrl = await discoverChromeHostUrl()
  1295. if (chromeHostUrl) {
  1296. // Send the result back to the webview
  1297. await provider.postMessageToWebview({
  1298. type: "browserConnectionResult",
  1299. success: !!chromeHostUrl,
  1300. text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,
  1301. values: { endpoint: chromeHostUrl },
  1302. })
  1303. } else {
  1304. await provider.postMessageToWebview({
  1305. type: "browserConnectionResult",
  1306. success: false,
  1307. text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
  1308. })
  1309. }
  1310. } else {
  1311. // Test the provided URL
  1312. const customHostUrl = message.text
  1313. const hostIsValid = await tryChromeHostUrl(message.text)
  1314. // Send the result back to the webview
  1315. await provider.postMessageToWebview({
  1316. type: "browserConnectionResult",
  1317. success: hostIsValid,
  1318. text: hostIsValid
  1319. ? `Successfully connected to Chrome: ${customHostUrl}`
  1320. : "Failed to connect to Chrome",
  1321. })
  1322. }
  1323. break
  1324. case "fuzzyMatchThreshold":
  1325. await updateGlobalState("fuzzyMatchThreshold", message.value)
  1326. await provider.postStateToWebview()
  1327. break
  1328. // kilocode_change start
  1329. case "morphApiKey":
  1330. await updateGlobalState("morphApiKey", message.text)
  1331. await provider.postStateToWebview()
  1332. break
  1333. case "fastApplyModel": {
  1334. const nextModel = fastApplyModelSchema.safeParse(message.text).data ?? "auto"
  1335. await updateGlobalState("fastApplyModel", nextModel)
  1336. await provider.postStateToWebview()
  1337. break
  1338. }
  1339. // kilocode_change end
  1340. case "updateVSCodeSetting": {
  1341. const { setting, value } = message
  1342. if (setting !== undefined && value !== undefined) {
  1343. if (ALLOWED_VSCODE_SETTINGS.has(setting)) {
  1344. await vscode.workspace.getConfiguration().update(setting, value, true)
  1345. } else {
  1346. vscode.window.showErrorMessage(`Cannot update restricted VSCode setting: ${setting}`)
  1347. }
  1348. }
  1349. break
  1350. }
  1351. case "getVSCodeSetting":
  1352. const { setting } = message
  1353. if (setting) {
  1354. try {
  1355. await provider.postMessageToWebview({
  1356. type: "vsCodeSetting",
  1357. setting,
  1358. value: vscode.workspace.getConfiguration().get(setting),
  1359. })
  1360. } catch (error) {
  1361. console.error(`Failed to get VSCode setting ${message.setting}:`, error)
  1362. await provider.postMessageToWebview({
  1363. type: "vsCodeSetting",
  1364. setting,
  1365. error: `Failed to get setting: ${error.message}`,
  1366. value: undefined,
  1367. })
  1368. }
  1369. }
  1370. break
  1371. case "alwaysApproveResubmit":
  1372. await updateGlobalState("alwaysApproveResubmit", message.bool ?? false)
  1373. await provider.postStateToWebview()
  1374. break
  1375. case "requestDelaySeconds":
  1376. await updateGlobalState("requestDelaySeconds", message.value ?? 5)
  1377. await provider.postStateToWebview()
  1378. break
  1379. case "writeDelayMs":
  1380. await updateGlobalState("writeDelayMs", message.value)
  1381. await provider.postStateToWebview()
  1382. break
  1383. case "diagnosticsEnabled":
  1384. await updateGlobalState("diagnosticsEnabled", message.bool ?? true)
  1385. await provider.postStateToWebview()
  1386. break
  1387. case "terminalOutputLineLimit":
  1388. // Validate that the line limit is a positive number
  1389. const lineLimit = message.value
  1390. if (typeof lineLimit === "number" && lineLimit > 0) {
  1391. await updateGlobalState("terminalOutputLineLimit", lineLimit)
  1392. await provider.postStateToWebview()
  1393. } else {
  1394. vscode.window.showErrorMessage(
  1395. t("common:errors.invalid_line_limit") || "Terminal output line limit must be a positive number",
  1396. )
  1397. }
  1398. break
  1399. case "terminalOutputCharacterLimit":
  1400. // Validate that the character limit is a positive number
  1401. const charLimit = message.value
  1402. if (typeof charLimit === "number" && charLimit > 0) {
  1403. await updateGlobalState("terminalOutputCharacterLimit", charLimit)
  1404. await provider.postStateToWebview()
  1405. } else {
  1406. vscode.window.showErrorMessage(
  1407. t("common:errors.invalid_character_limit") ||
  1408. "Terminal output character limit must be a positive number",
  1409. )
  1410. }
  1411. break
  1412. case "terminalShellIntegrationTimeout":
  1413. await updateGlobalState("terminalShellIntegrationTimeout", message.value)
  1414. await provider.postStateToWebview()
  1415. if (message.value !== undefined) {
  1416. Terminal.setShellIntegrationTimeout(message.value)
  1417. }
  1418. break
  1419. case "terminalShellIntegrationDisabled":
  1420. await updateGlobalState("terminalShellIntegrationDisabled", message.bool)
  1421. await provider.postStateToWebview()
  1422. if (message.bool !== undefined) {
  1423. Terminal.setShellIntegrationDisabled(message.bool)
  1424. }
  1425. break
  1426. case "terminalCommandDelay":
  1427. await updateGlobalState("terminalCommandDelay", message.value)
  1428. await provider.postStateToWebview()
  1429. if (message.value !== undefined) {
  1430. Terminal.setCommandDelay(message.value)
  1431. }
  1432. break
  1433. case "terminalPowershellCounter":
  1434. await updateGlobalState("terminalPowershellCounter", message.bool)
  1435. await provider.postStateToWebview()
  1436. if (message.bool !== undefined) {
  1437. Terminal.setPowershellCounter(message.bool)
  1438. }
  1439. break
  1440. case "terminalZshClearEolMark":
  1441. await updateGlobalState("terminalZshClearEolMark", message.bool)
  1442. await provider.postStateToWebview()
  1443. if (message.bool !== undefined) {
  1444. Terminal.setTerminalZshClearEolMark(message.bool)
  1445. }
  1446. break
  1447. case "terminalZshOhMy":
  1448. await updateGlobalState("terminalZshOhMy", message.bool)
  1449. await provider.postStateToWebview()
  1450. if (message.bool !== undefined) {
  1451. Terminal.setTerminalZshOhMy(message.bool)
  1452. }
  1453. break
  1454. case "terminalZshP10k":
  1455. await updateGlobalState("terminalZshP10k", message.bool)
  1456. await provider.postStateToWebview()
  1457. if (message.bool !== undefined) {
  1458. Terminal.setTerminalZshP10k(message.bool)
  1459. }
  1460. break
  1461. case "terminalZdotdir":
  1462. await updateGlobalState("terminalZdotdir", message.bool)
  1463. await provider.postStateToWebview()
  1464. if (message.bool !== undefined) {
  1465. Terminal.setTerminalZdotdir(message.bool)
  1466. }
  1467. break
  1468. case "terminalCompressProgressBar":
  1469. await updateGlobalState("terminalCompressProgressBar", message.bool)
  1470. await provider.postStateToWebview()
  1471. if (message.bool !== undefined) {
  1472. Terminal.setCompressProgressBar(message.bool)
  1473. }
  1474. break
  1475. case "mode":
  1476. await provider.handleModeSwitch(message.text as Mode)
  1477. break
  1478. case "updateSupportPrompt":
  1479. try {
  1480. if (!message?.values) {
  1481. return
  1482. }
  1483. // Replace all prompts with the new values from the cached state
  1484. await updateGlobalState("customSupportPrompts", message.values)
  1485. await provider.postStateToWebview()
  1486. } catch (error) {
  1487. provider.log(
  1488. `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1489. )
  1490. vscode.window.showErrorMessage(t("common:errors.update_support_prompt"))
  1491. }
  1492. break
  1493. case "updatePrompt":
  1494. if (message.promptMode && message.customPrompt !== undefined) {
  1495. const existingPrompts = getGlobalState("customModePrompts") ?? {}
  1496. const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt }
  1497. await updateGlobalState("customModePrompts", updatedPrompts)
  1498. const currentState = await provider.getStateToPostToWebview()
  1499. const stateWithPrompts = {
  1500. ...currentState,
  1501. customModePrompts: updatedPrompts,
  1502. hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false,
  1503. }
  1504. provider.postMessageToWebview({ type: "state", state: stateWithPrompts })
  1505. if (TelemetryService.hasInstance()) {
  1506. // Determine which setting was changed by comparing objects
  1507. const oldPrompt = existingPrompts[message.promptMode] || {}
  1508. const newPrompt = message.customPrompt
  1509. const changedSettings = Object.keys(newPrompt).filter(
  1510. (key) =>
  1511. JSON.stringify((oldPrompt as Record<string, unknown>)[key]) !==
  1512. JSON.stringify((newPrompt as Record<string, unknown>)[key]),
  1513. )
  1514. if (changedSettings.length > 0) {
  1515. TelemetryService.instance.captureModeSettingChanged(changedSettings[0])
  1516. }
  1517. }
  1518. }
  1519. break
  1520. case "deleteMessage": {
  1521. if (!provider.getCurrentTask()) {
  1522. await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
  1523. break
  1524. }
  1525. if (typeof message.value !== "number" || !message.value) {
  1526. await vscode.window.showErrorMessage(t("common:errors.message.invalid_timestamp_for_deletion"))
  1527. break
  1528. }
  1529. await handleMessageModificationsOperation(message.value, "delete")
  1530. break
  1531. }
  1532. case "submitEditedMessage": {
  1533. if (
  1534. provider.getCurrentTask() &&
  1535. typeof message.value === "number" &&
  1536. message.value &&
  1537. message.editedMessageContent
  1538. ) {
  1539. await handleMessageModificationsOperation(
  1540. message.value,
  1541. "edit",
  1542. message.editedMessageContent,
  1543. message.images,
  1544. )
  1545. }
  1546. break
  1547. }
  1548. case "screenshotQuality":
  1549. await updateGlobalState("screenshotQuality", message.value)
  1550. await provider.postStateToWebview()
  1551. break
  1552. case "maxOpenTabsContext":
  1553. const tabCount = Math.min(Math.max(0, message.value ?? 20), 500)
  1554. await updateGlobalState("maxOpenTabsContext", tabCount)
  1555. await provider.postStateToWebview()
  1556. break
  1557. case "maxWorkspaceFiles":
  1558. const fileCount = Math.min(Math.max(0, message.value ?? 200), 500)
  1559. await updateGlobalState("maxWorkspaceFiles", fileCount)
  1560. await provider.postStateToWebview()
  1561. break
  1562. case "alwaysAllowFollowupQuestions":
  1563. await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false)
  1564. await provider.postStateToWebview()
  1565. break
  1566. case "followupAutoApproveTimeoutMs":
  1567. await updateGlobalState("followupAutoApproveTimeoutMs", message.value)
  1568. await provider.postStateToWebview()
  1569. break
  1570. case "browserToolEnabled":
  1571. await updateGlobalState("browserToolEnabled", message.bool ?? true)
  1572. await provider.postStateToWebview()
  1573. break
  1574. case "language":
  1575. changeLanguage(message.text ?? "en")
  1576. await updateGlobalState("language", message.text as Language)
  1577. await provider.postStateToWebview()
  1578. break
  1579. case "openRouterImageApiKey":
  1580. await provider.contextProxy.setValue("openRouterImageApiKey", message.text)
  1581. await provider.postStateToWebview()
  1582. break
  1583. case "openRouterImageGenerationSelectedModel":
  1584. await provider.contextProxy.setValue("openRouterImageGenerationSelectedModel", message.text)
  1585. await provider.postStateToWebview()
  1586. break
  1587. case "showRooIgnoredFiles":
  1588. await updateGlobalState("showRooIgnoredFiles", message.bool ?? false)
  1589. await provider.postStateToWebview()
  1590. break
  1591. case "hasOpenedModeSelector":
  1592. await updateGlobalState("hasOpenedModeSelector", message.bool ?? true)
  1593. await provider.postStateToWebview()
  1594. break
  1595. case "maxReadFileLine":
  1596. await updateGlobalState("maxReadFileLine", message.value)
  1597. await provider.postStateToWebview()
  1598. break
  1599. // kilocode_change start
  1600. case "kiloCodeImageApiKey":
  1601. await provider.contextProxy.setValue("kiloCodeImageApiKey", message.text)
  1602. await provider.postStateToWebview()
  1603. break
  1604. case "showAutoApproveMenu":
  1605. await updateGlobalState("showAutoApproveMenu", message.bool ?? true)
  1606. await provider.postStateToWebview()
  1607. break
  1608. case "showTaskTimeline":
  1609. await updateGlobalState("showTaskTimeline", message.bool ?? false)
  1610. await provider.postStateToWebview()
  1611. break
  1612. // kilocode_change start
  1613. case "sendMessageOnEnter":
  1614. await updateGlobalState("sendMessageOnEnter", message.bool ?? false)
  1615. await provider.postStateToWebview()
  1616. break
  1617. // kilocode_change end
  1618. case "showTimestamps":
  1619. await updateGlobalState("showTimestamps", message.bool ?? false)
  1620. await provider.postStateToWebview()
  1621. break
  1622. case "hideCostBelowThreshold":
  1623. await updateGlobalState("hideCostBelowThreshold", message.value)
  1624. await provider.postStateToWebview()
  1625. break
  1626. case "allowVeryLargeReads":
  1627. await updateGlobalState("allowVeryLargeReads", message.bool ?? false)
  1628. await provider.postStateToWebview()
  1629. break
  1630. // kilocode_change end
  1631. case "maxImageFileSize":
  1632. await updateGlobalState("maxImageFileSize", message.value)
  1633. await provider.postStateToWebview()
  1634. break
  1635. case "maxTotalImageSize":
  1636. await updateGlobalState("maxTotalImageSize", message.value)
  1637. await provider.postStateToWebview()
  1638. break
  1639. case "maxConcurrentFileReads":
  1640. const valueToSave = message.value // Capture the value intended for saving
  1641. await updateGlobalState("maxConcurrentFileReads", valueToSave)
  1642. await provider.postStateToWebview()
  1643. break
  1644. case "includeDiagnosticMessages":
  1645. // Only apply default if the value is truly undefined (not false)
  1646. const includeValue = message.bool !== undefined ? message.bool : true
  1647. await updateGlobalState("includeDiagnosticMessages", includeValue)
  1648. await provider.postStateToWebview()
  1649. break
  1650. case "maxDiagnosticMessages":
  1651. await updateGlobalState("maxDiagnosticMessages", message.value ?? 50)
  1652. await provider.postStateToWebview()
  1653. break
  1654. case "setHistoryPreviewCollapsed": // Add the new case handler
  1655. await updateGlobalState("historyPreviewCollapsed", message.bool ?? false)
  1656. // No need to call postStateToWebview here as the UI already updated optimistically
  1657. break
  1658. case "setReasoningBlockCollapsed":
  1659. await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
  1660. // No need to call postStateToWebview here as the UI already updated optimistically
  1661. break
  1662. case "toggleApiConfigPin":
  1663. if (message.text) {
  1664. const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
  1665. const updatedPinned: Record<string, boolean> = { ...currentPinned }
  1666. if (currentPinned[message.text]) {
  1667. delete updatedPinned[message.text]
  1668. } else {
  1669. updatedPinned[message.text] = true
  1670. }
  1671. await updateGlobalState("pinnedApiConfigs", updatedPinned)
  1672. await provider.postStateToWebview()
  1673. }
  1674. break
  1675. case "enhancementApiConfigId":
  1676. await updateGlobalState("enhancementApiConfigId", message.text)
  1677. await provider.postStateToWebview()
  1678. break
  1679. // kilocode_change start - commitMessageApiConfigId
  1680. case "commitMessageApiConfigId":
  1681. await updateGlobalState("commitMessageApiConfigId", message.text)
  1682. await provider.postStateToWebview()
  1683. break
  1684. // kilocode_change end - commitMessageApiConfigId
  1685. // kilocode_change start - terminalCommandApiConfigId
  1686. case "terminalCommandApiConfigId":
  1687. await updateGlobalState("terminalCommandApiConfigId", message.text)
  1688. await provider.postStateToWebview()
  1689. break
  1690. // kilocode_change end - terminalCommandApiConfigId
  1691. // kilocode_change start - ghostServiceSettings
  1692. case "ghostServiceSettings":
  1693. if (!message.values) {
  1694. return
  1695. }
  1696. // Validate ghostServiceSettings structure
  1697. const ghostServiceSettings = ghostServiceSettingsSchema.parse(message.values)
  1698. await updateGlobalState("ghostServiceSettings", ghostServiceSettings)
  1699. await provider.postStateToWebview()
  1700. vscode.commands.executeCommand("kilo-code.ghost.reload")
  1701. break
  1702. // kilocode_change end
  1703. case "includeTaskHistoryInEnhance":
  1704. await updateGlobalState("includeTaskHistoryInEnhance", message.bool ?? true)
  1705. await provider.postStateToWebview()
  1706. break
  1707. case "condensingApiConfigId":
  1708. await updateGlobalState("condensingApiConfigId", message.text)
  1709. await provider.postStateToWebview()
  1710. break
  1711. case "updateCondensingPrompt":
  1712. // Store the condensing prompt in customSupportPrompts["CONDENSE"] instead of customCondensingPrompt
  1713. const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {}
  1714. const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text }
  1715. await updateGlobalState("customSupportPrompts", updatedSupportPrompts)
  1716. // Also update the old field for backward compatibility during migration
  1717. await updateGlobalState("customCondensingPrompt", message.text)
  1718. await provider.postStateToWebview()
  1719. break
  1720. case "profileThresholds":
  1721. await updateGlobalState("profileThresholds", message.values)
  1722. await provider.postStateToWebview()
  1723. break
  1724. case "autoApprovalEnabled":
  1725. await updateGlobalState("autoApprovalEnabled", message.bool ?? false)
  1726. await provider.postStateToWebview()
  1727. break
  1728. case "enhancePrompt":
  1729. if (message.text) {
  1730. try {
  1731. const state = await provider.getState()
  1732. const {
  1733. apiConfiguration,
  1734. customSupportPrompts,
  1735. listApiConfigMeta = [],
  1736. enhancementApiConfigId,
  1737. includeTaskHistoryInEnhance,
  1738. } = state
  1739. const currentCline = provider.getCurrentTask()
  1740. const result = await MessageEnhancer.enhanceMessage({
  1741. text: message.text,
  1742. apiConfiguration,
  1743. customSupportPrompts,
  1744. listApiConfigMeta,
  1745. enhancementApiConfigId,
  1746. includeTaskHistoryInEnhance,
  1747. currentClineMessages: currentCline?.clineMessages,
  1748. providerSettingsManager: provider.providerSettingsManager,
  1749. })
  1750. if (result.success && result.enhancedText) {
  1751. // Capture telemetry for prompt enhancement
  1752. MessageEnhancer.captureTelemetry(currentCline?.taskId, includeTaskHistoryInEnhance)
  1753. await provider.postMessageToWebview({ type: "enhancedPrompt", text: result.enhancedText })
  1754. } else {
  1755. throw new Error(result.error || "Unknown error")
  1756. }
  1757. } catch (error) {
  1758. provider.log(
  1759. `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1760. )
  1761. TelemetryService.instance.captureException(error, { context: "enhance_prompt" }) // kilocode_change
  1762. vscode.window.showErrorMessage(t("common:errors.enhance_prompt"))
  1763. await provider.postMessageToWebview({ type: "enhancedPrompt" })
  1764. }
  1765. }
  1766. break
  1767. case "getSystemPrompt":
  1768. try {
  1769. const systemPrompt = await generateSystemPrompt(provider, message)
  1770. await provider.postMessageToWebview({
  1771. type: "systemPrompt",
  1772. text: systemPrompt,
  1773. mode: message.mode,
  1774. })
  1775. } catch (error) {
  1776. provider.log(
  1777. `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1778. )
  1779. vscode.window.showErrorMessage(t("common:errors.get_system_prompt"))
  1780. }
  1781. break
  1782. case "copySystemPrompt":
  1783. try {
  1784. const systemPrompt = await generateSystemPrompt(provider, message)
  1785. await vscode.env.clipboard.writeText(systemPrompt)
  1786. await vscode.window.showInformationMessage(t("common:info.clipboard_copy"))
  1787. } catch (error) {
  1788. provider.log(
  1789. `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1790. )
  1791. vscode.window.showErrorMessage(t("common:errors.get_system_prompt"))
  1792. }
  1793. break
  1794. case "searchCommits": {
  1795. const cwd = getCurrentCwd()
  1796. if (cwd) {
  1797. try {
  1798. const commits = await searchCommits(message.query || "", cwd)
  1799. await provider.postMessageToWebview({
  1800. type: "commitSearchResults",
  1801. commits,
  1802. })
  1803. } catch (error) {
  1804. provider.log(
  1805. `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1806. )
  1807. vscode.window.showErrorMessage(t("common:errors.search_commits"))
  1808. }
  1809. }
  1810. break
  1811. }
  1812. // kilocode_change start
  1813. case "showFeedbackOptions": {
  1814. const githubIssuesText = t("common:feedback.githubIssues")
  1815. const discordText = t("common:feedback.discord")
  1816. const customerSupport = t("common:feedback.customerSupport")
  1817. const answer = await vscode.window.showInformationMessage(
  1818. t("common:feedback.description"),
  1819. { modal: true },
  1820. githubIssuesText,
  1821. discordText,
  1822. customerSupport,
  1823. )
  1824. if (answer === githubIssuesText) {
  1825. await vscode.env.openExternal(vscode.Uri.parse("https://github.com/Kilo-Org/kilocode/issues"))
  1826. } else if (answer === discordText) {
  1827. await vscode.env.openExternal(vscode.Uri.parse("https://discord.gg/fxrhCFGhkP"))
  1828. } else if (answer === customerSupport) {
  1829. await vscode.env.openExternal(vscode.Uri.parse("https://kilocode.ai/support"))
  1830. }
  1831. break
  1832. }
  1833. // kilocode_change end
  1834. case "searchFiles": {
  1835. const workspacePath = getCurrentCwd()
  1836. if (!workspacePath) {
  1837. // Handle case where workspace path is not available
  1838. await provider.postMessageToWebview({
  1839. type: "fileSearchResults",
  1840. results: [],
  1841. requestId: message.requestId,
  1842. error: "No workspace path available",
  1843. })
  1844. break
  1845. }
  1846. try {
  1847. // Call file search service with query from message
  1848. const results = await searchWorkspaceFiles(
  1849. message.query || "",
  1850. workspacePath,
  1851. 20, // Use default limit, as filtering is now done in the backend
  1852. )
  1853. // Send results back to webview
  1854. await provider.postMessageToWebview({
  1855. type: "fileSearchResults",
  1856. results,
  1857. requestId: message.requestId,
  1858. })
  1859. } catch (error) {
  1860. const errorMessage = error instanceof Error ? error.message : String(error)
  1861. // Send error response to webview
  1862. await provider.postMessageToWebview({
  1863. type: "fileSearchResults",
  1864. results: [],
  1865. error: errorMessage,
  1866. requestId: message.requestId,
  1867. })
  1868. }
  1869. break
  1870. }
  1871. case "updateTodoList": {
  1872. const payload = message.payload as { todos?: any[] }
  1873. const todos = payload?.todos
  1874. if (Array.isArray(todos)) {
  1875. await setPendingTodoList(todos)
  1876. }
  1877. break
  1878. }
  1879. case "saveApiConfiguration":
  1880. if (message.text && message.apiConfiguration) {
  1881. try {
  1882. await provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)
  1883. const listApiConfig = await provider.providerSettingsManager.listConfig()
  1884. await updateGlobalState("listApiConfigMeta", listApiConfig)
  1885. vscode.commands.executeCommand("kilo-code.ghost.reload") // kilocode_change: Reload ghost model when API provider settings change
  1886. } catch (error) {
  1887. provider.log(
  1888. `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1889. )
  1890. vscode.window.showErrorMessage(t("common:errors.save_api_config"))
  1891. }
  1892. }
  1893. break
  1894. case "upsertApiConfiguration":
  1895. // kilocode_change start: check for kilocodeToken change to remove organizationId and fetch organization modes
  1896. if (message.text && message.apiConfiguration) {
  1897. let configToSave = message.apiConfiguration
  1898. let organizationChanged = false
  1899. try {
  1900. const { ...currentConfig } = await provider.providerSettingsManager.getProfile({
  1901. name: message.text,
  1902. })
  1903. // Only clear organization ID if we actually had a kilocode token before and it's different now
  1904. const hadPreviousToken = currentConfig.kilocodeToken !== undefined
  1905. const hasNewToken = message.apiConfiguration.kilocodeToken !== undefined
  1906. const tokensAreDifferent = currentConfig.kilocodeToken !== message.apiConfiguration.kilocodeToken
  1907. if (hadPreviousToken && hasNewToken && tokensAreDifferent) {
  1908. configToSave = { ...message.apiConfiguration, kilocodeOrganizationId: undefined }
  1909. await updateGlobalState("hasPerformedOrganizationAutoSwitch", undefined)
  1910. }
  1911. organizationChanged =
  1912. currentConfig.kilocodeOrganizationId !== message.apiConfiguration.kilocodeOrganizationId
  1913. if (organizationChanged) {
  1914. // Fetch organization-specific custom modes
  1915. await refreshOrganizationModes(message, provider, updateGlobalState)
  1916. // Flush and refetch models
  1917. await flushModels("kilocode-openrouter")
  1918. const models = await getModels({
  1919. provider: "kilocode-openrouter",
  1920. kilocodeOrganizationId: message.apiConfiguration.kilocodeOrganizationId,
  1921. kilocodeToken: message.apiConfiguration.kilocodeToken,
  1922. })
  1923. provider.postMessageToWebview({
  1924. type: "routerModels",
  1925. routerModels: { "kilocode-openrouter": models } as Record<RouterName, ModelRecord>,
  1926. })
  1927. }
  1928. } catch (error) {
  1929. // Config might not exist yet, that's fine
  1930. }
  1931. await provider.upsertProviderProfile(message.text, configToSave)
  1932. // Ensure state is posted to webview after profile update to reflect organization mode changes
  1933. if (organizationChanged) {
  1934. await provider.postStateToWebview()
  1935. }
  1936. }
  1937. // kilocode_change end: check for kilocodeToken change to remove organizationId and fetch organization modes
  1938. break
  1939. case "renameApiConfiguration":
  1940. if (message.values && message.apiConfiguration) {
  1941. try {
  1942. const { oldName, newName } = message.values
  1943. if (oldName === newName) {
  1944. break
  1945. }
  1946. // Load the old configuration to get its ID.
  1947. const { id } = await provider.providerSettingsManager.getProfile({ name: oldName })
  1948. // Create a new configuration with the new name and old ID.
  1949. await provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id })
  1950. // Delete the old configuration.
  1951. await provider.providerSettingsManager.deleteConfig(oldName)
  1952. // Re-activate to update the global settings related to the
  1953. // currently activated provider profile.
  1954. await provider.activateProviderProfile({ name: newName })
  1955. } catch (error) {
  1956. provider.log(
  1957. `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1958. )
  1959. vscode.window.showErrorMessage(t("common:errors.rename_api_config"))
  1960. }
  1961. }
  1962. break
  1963. case "loadApiConfiguration":
  1964. if (message.text) {
  1965. try {
  1966. await provider.activateProviderProfile({ name: message.text })
  1967. } catch (error) {
  1968. provider.log(
  1969. `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1970. )
  1971. vscode.window.showErrorMessage(t("common:errors.load_api_config"))
  1972. }
  1973. }
  1974. break
  1975. case "loadApiConfigurationById":
  1976. if (message.text) {
  1977. try {
  1978. await provider.activateProviderProfile({ id: message.text })
  1979. } catch (error) {
  1980. provider.log(
  1981. `Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  1982. )
  1983. vscode.window.showErrorMessage(t("common:errors.load_api_config"))
  1984. }
  1985. }
  1986. break
  1987. case "deleteApiConfiguration":
  1988. if (message.text) {
  1989. const answer = await vscode.window.showInformationMessage(
  1990. t("common:confirmation.delete_config_profile"),
  1991. { modal: true },
  1992. t("common:answers.yes"),
  1993. )
  1994. if (answer !== t("common:answers.yes")) {
  1995. break
  1996. }
  1997. const oldName = message.text
  1998. const newName = (await provider.providerSettingsManager.listConfig()).filter(
  1999. (c) => c.name !== oldName,
  2000. )[0]?.name
  2001. if (!newName) {
  2002. vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
  2003. return
  2004. }
  2005. try {
  2006. await provider.providerSettingsManager.deleteConfig(oldName)
  2007. await provider.activateProviderProfile({ name: newName })
  2008. } catch (error) {
  2009. provider.log(
  2010. `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  2011. )
  2012. vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
  2013. }
  2014. }
  2015. break
  2016. case "deleteMessageConfirm":
  2017. if (!message.messageTs) {
  2018. await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_missing_timestamp"))
  2019. break
  2020. }
  2021. if (typeof message.messageTs !== "number") {
  2022. await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_invalid_timestamp"))
  2023. break
  2024. }
  2025. await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint)
  2026. break
  2027. case "editMessageConfirm":
  2028. if (message.messageTs && message.text) {
  2029. await handleEditMessageConfirm(
  2030. message.messageTs,
  2031. message.text,
  2032. message.restoreCheckpoint,
  2033. message.images,
  2034. )
  2035. }
  2036. break
  2037. case "getListApiConfiguration":
  2038. try {
  2039. const listApiConfig = await provider.providerSettingsManager.listConfig()
  2040. await updateGlobalState("listApiConfigMeta", listApiConfig)
  2041. provider.postMessageToWebview({ type: "listApiConfig", listApiConfig })
  2042. } catch (error) {
  2043. provider.log(
  2044. `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  2045. )
  2046. vscode.window.showErrorMessage(t("common:errors.list_api_config"))
  2047. }
  2048. break
  2049. case "updateExperimental": {
  2050. if (!message.values) {
  2051. break
  2052. }
  2053. const updatedExperiments = {
  2054. ...(getGlobalState("experiments") ?? experimentDefault),
  2055. ...message.values,
  2056. }
  2057. await updateGlobalState("experiments", updatedExperiments)
  2058. await provider.postStateToWebview()
  2059. break
  2060. }
  2061. case "updateMcpTimeout":
  2062. if (message.serverName && typeof message.timeout === "number") {
  2063. try {
  2064. await provider
  2065. .getMcpHub()
  2066. ?.updateServerTimeout(
  2067. message.serverName,
  2068. message.timeout,
  2069. message.source as "global" | "project",
  2070. )
  2071. } catch (error) {
  2072. provider.log(
  2073. `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  2074. )
  2075. vscode.window.showErrorMessage(t("common:errors.update_server_timeout"))
  2076. }
  2077. }
  2078. break
  2079. case "updateCustomMode":
  2080. if (message.modeConfig) {
  2081. // Check if this is a new mode or an update to an existing mode
  2082. const existingModes = await provider.customModesManager.getCustomModes()
  2083. const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug)
  2084. await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)
  2085. // Update state after saving the mode
  2086. const customModes = await provider.customModesManager.getCustomModes()
  2087. await updateGlobalState("customModes", customModes)
  2088. await updateGlobalState("mode", message.modeConfig.slug)
  2089. await provider.postStateToWebview()
  2090. // Track telemetry for custom mode creation or update
  2091. if (TelemetryService.hasInstance()) {
  2092. if (isNewMode) {
  2093. // This is a new custom mode
  2094. TelemetryService.instance.captureCustomModeCreated(
  2095. message.modeConfig.slug,
  2096. message.modeConfig.name,
  2097. )
  2098. } else {
  2099. // Determine which setting was changed by comparing objects
  2100. const existingMode = existingModes.find((mode) => mode.slug === message.modeConfig?.slug)
  2101. const changedSettings = existingMode
  2102. ? Object.keys(message.modeConfig).filter(
  2103. (key) =>
  2104. JSON.stringify((existingMode as Record<string, unknown>)[key]) !==
  2105. JSON.stringify((message.modeConfig as Record<string, unknown>)[key]),
  2106. )
  2107. : []
  2108. if (changedSettings.length > 0) {
  2109. TelemetryService.instance.captureModeSettingChanged(changedSettings[0])
  2110. }
  2111. }
  2112. }
  2113. }
  2114. break
  2115. case "deleteCustomMode":
  2116. if (message.slug) {
  2117. // Get the mode details to determine source and rules folder path
  2118. const customModes = await provider.customModesManager.getCustomModes()
  2119. const modeToDelete = customModes.find((mode) => mode.slug === message.slug)
  2120. if (!modeToDelete) {
  2121. break
  2122. }
  2123. // Determine the scope based on source (project or global)
  2124. const scope = modeToDelete.source || "global"
  2125. // Determine the rules folder path
  2126. let rulesFolderPath: string
  2127. if (scope === "project") {
  2128. const workspacePath = getWorkspacePath()
  2129. if (workspacePath) {
  2130. rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`)
  2131. } else {
  2132. rulesFolderPath = path.join(".roo", `rules-${message.slug}`)
  2133. }
  2134. } else {
  2135. // Global scope - use OS home directory
  2136. const homeDir = os.homedir()
  2137. rulesFolderPath = path.join(homeDir, ".roo", `rules-${message.slug}`)
  2138. }
  2139. // Check if the rules folder exists
  2140. const rulesFolderExists = await fileExistsAtPath(rulesFolderPath)
  2141. // If this is a check request, send back the folder info
  2142. if (message.checkOnly) {
  2143. await provider.postMessageToWebview({
  2144. type: "deleteCustomModeCheck",
  2145. slug: message.slug,
  2146. rulesFolderPath: rulesFolderExists ? rulesFolderPath : undefined,
  2147. })
  2148. break
  2149. }
  2150. // Delete the mode
  2151. await provider.customModesManager.deleteCustomMode(message.slug)
  2152. // Delete the rules folder if it exists
  2153. if (rulesFolderExists) {
  2154. try {
  2155. await fs.rm(rulesFolderPath, { recursive: true, force: true })
  2156. provider.log(`Deleted rules folder for mode ${message.slug}: ${rulesFolderPath}`)
  2157. } catch (error) {
  2158. provider.log(`Failed to delete rules folder for mode ${message.slug}: ${error}`)
  2159. // Notify the user about the failure
  2160. vscode.window.showErrorMessage(
  2161. t("common:errors.delete_rules_folder_failed", {
  2162. rulesFolderPath,
  2163. error: error instanceof Error ? error.message : String(error),
  2164. }),
  2165. )
  2166. // Continue with mode deletion even if folder deletion fails
  2167. }
  2168. }
  2169. // Switch back to default mode after deletion
  2170. await updateGlobalState("mode", defaultModeSlug)
  2171. await provider.postStateToWebview()
  2172. }
  2173. break
  2174. case "exportMode":
  2175. if (message.slug) {
  2176. try {
  2177. // Get custom mode prompts to check if built-in mode has been customized
  2178. const customModePrompts = getGlobalState("customModePrompts") || {}
  2179. const customPrompt = customModePrompts[message.slug]
  2180. // Export the mode with any customizations merged directly
  2181. const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt)
  2182. if (result.success && result.yaml) {
  2183. // Get last used directory for export
  2184. const lastExportPath = getGlobalState("lastModeExportPath")
  2185. let defaultUri: vscode.Uri
  2186. if (lastExportPath) {
  2187. // Use the directory from the last export
  2188. const lastDir = path.dirname(lastExportPath)
  2189. defaultUri = vscode.Uri.file(path.join(lastDir, `${message.slug}-export.yaml`))
  2190. } else {
  2191. // Default to workspace or home directory
  2192. const workspaceFolders = vscode.workspace.workspaceFolders
  2193. if (workspaceFolders && workspaceFolders.length > 0) {
  2194. defaultUri = vscode.Uri.file(
  2195. path.join(workspaceFolders[0].uri.fsPath, `${message.slug}-export.yaml`),
  2196. )
  2197. } else {
  2198. defaultUri = vscode.Uri.file(`${message.slug}-export.yaml`)
  2199. }
  2200. }
  2201. // Show save dialog
  2202. const saveUri = await vscode.window.showSaveDialog({
  2203. defaultUri,
  2204. filters: {
  2205. "YAML files": ["yaml", "yml"],
  2206. },
  2207. title: "Save mode export",
  2208. })
  2209. if (saveUri && result.yaml) {
  2210. // Save the directory for next time
  2211. await updateGlobalState("lastModeExportPath", saveUri.fsPath)
  2212. // Write the file to the selected location
  2213. await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8")
  2214. // Send success message to webview
  2215. provider.postMessageToWebview({
  2216. type: "exportModeResult",
  2217. success: true,
  2218. slug: message.slug,
  2219. })
  2220. // Show info message
  2221. vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug }))
  2222. } else {
  2223. // User cancelled the save dialog
  2224. provider.postMessageToWebview({
  2225. type: "exportModeResult",
  2226. success: false,
  2227. error: "Export cancelled",
  2228. slug: message.slug,
  2229. })
  2230. }
  2231. } else {
  2232. // Send error message to webview
  2233. provider.postMessageToWebview({
  2234. type: "exportModeResult",
  2235. success: false,
  2236. error: result.error,
  2237. slug: message.slug,
  2238. })
  2239. }
  2240. } catch (error) {
  2241. const errorMessage = error instanceof Error ? error.message : String(error)
  2242. provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`)
  2243. // Send error message to webview
  2244. provider.postMessageToWebview({
  2245. type: "exportModeResult",
  2246. success: false,
  2247. error: errorMessage,
  2248. slug: message.slug,
  2249. })
  2250. }
  2251. }
  2252. break
  2253. case "importMode":
  2254. try {
  2255. // Get last used directory for import
  2256. const lastImportPath = getGlobalState("lastModeImportPath")
  2257. let defaultUri: vscode.Uri | undefined
  2258. if (lastImportPath) {
  2259. // Use the directory from the last import
  2260. const lastDir = path.dirname(lastImportPath)
  2261. defaultUri = vscode.Uri.file(lastDir)
  2262. } else {
  2263. // Default to workspace or home directory
  2264. const workspaceFolders = vscode.workspace.workspaceFolders
  2265. if (workspaceFolders && workspaceFolders.length > 0) {
  2266. defaultUri = vscode.Uri.file(workspaceFolders[0].uri.fsPath)
  2267. }
  2268. }
  2269. // Show file picker to select YAML file
  2270. const fileUri = await vscode.window.showOpenDialog({
  2271. canSelectFiles: true,
  2272. canSelectFolders: false,
  2273. canSelectMany: false,
  2274. defaultUri,
  2275. filters: {
  2276. "YAML files": ["yaml", "yml"],
  2277. },
  2278. title: "Select mode export file to import",
  2279. })
  2280. if (fileUri && fileUri[0]) {
  2281. // Save the directory for next time
  2282. await updateGlobalState("lastModeImportPath", fileUri[0].fsPath)
  2283. // Read the file content
  2284. const yamlContent = await fs.readFile(fileUri[0].fsPath, "utf-8")
  2285. // Import the mode with the specified source level
  2286. const result = await provider.customModesManager.importModeWithRules(
  2287. yamlContent,
  2288. message.source || "project", // Default to project if not specified
  2289. )
  2290. if (result.success) {
  2291. // Update state after importing
  2292. const customModes = await provider.customModesManager.getCustomModes()
  2293. await updateGlobalState("customModes", customModes)
  2294. await provider.postStateToWebview()
  2295. // Send success message to webview
  2296. provider.postMessageToWebview({
  2297. type: "importModeResult",
  2298. success: true,
  2299. })
  2300. // Show success message
  2301. vscode.window.showInformationMessage(t("common:info.mode_imported"))
  2302. } else {
  2303. // Send error message to webview
  2304. provider.postMessageToWebview({
  2305. type: "importModeResult",
  2306. success: false,
  2307. error: result.error,
  2308. })
  2309. // Show error message
  2310. vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: result.error }))
  2311. }
  2312. } else {
  2313. // User cancelled the file dialog - reset the importing state
  2314. provider.postMessageToWebview({
  2315. type: "importModeResult",
  2316. success: false,
  2317. error: "cancelled",
  2318. })
  2319. }
  2320. } catch (error) {
  2321. const errorMessage = error instanceof Error ? error.message : String(error)
  2322. provider.log(`Failed to import mode: ${errorMessage}`)
  2323. // Send error message to webview
  2324. provider.postMessageToWebview({
  2325. type: "importModeResult",
  2326. success: false,
  2327. error: errorMessage,
  2328. })
  2329. // Show error message
  2330. vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: errorMessage }))
  2331. }
  2332. break
  2333. case "checkRulesDirectory":
  2334. if (message.slug) {
  2335. const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug)
  2336. provider.postMessageToWebview({
  2337. type: "checkRulesDirectoryResult",
  2338. slug: message.slug,
  2339. hasContent: hasContent,
  2340. })
  2341. }
  2342. break
  2343. case "humanRelayResponse":
  2344. if (message.requestId && message.text) {
  2345. vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), {
  2346. requestId: message.requestId,
  2347. text: message.text,
  2348. cancelled: false,
  2349. })
  2350. }
  2351. break
  2352. case "humanRelayCancel":
  2353. if (message.requestId) {
  2354. vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), {
  2355. requestId: message.requestId,
  2356. cancelled: true,
  2357. })
  2358. }
  2359. break
  2360. // kilocode_change_start
  2361. case "fetchProfileDataRequest":
  2362. try {
  2363. const { apiConfiguration, currentApiConfigName } = await provider.getState()
  2364. const kilocodeToken = apiConfiguration?.kilocodeToken
  2365. if (!kilocodeToken) {
  2366. provider.log("KiloCode token not found in extension state.")
  2367. provider.postMessageToWebview({
  2368. type: "profileDataResponse",
  2369. payload: { success: false, error: "KiloCode API token not configured." },
  2370. })
  2371. break
  2372. }
  2373. // Changed to /api/profile
  2374. const headers: Record<string, string> = {
  2375. Authorization: `Bearer ${kilocodeToken}`,
  2376. "Content-Type": "application/json",
  2377. }
  2378. // Add X-KILOCODE-TESTER: SUPPRESS header if the setting is enabled
  2379. if (
  2380. apiConfiguration.kilocodeTesterWarningsDisabledUntil &&
  2381. apiConfiguration.kilocodeTesterWarningsDisabledUntil > Date.now()
  2382. ) {
  2383. headers["X-KILOCODE-TESTER"] = "SUPPRESS"
  2384. }
  2385. const response = await axios.get<Omit<ProfileData, "kilocodeToken">>(
  2386. `${getKiloBaseUriFromToken(kilocodeToken)}/api/profile`,
  2387. {
  2388. headers,
  2389. },
  2390. )
  2391. // Go back to Personal when no longer part of the current set organization
  2392. const organizationExists = (response.data.organizations ?? []).some(
  2393. ({ id }) => id === apiConfiguration?.kilocodeOrganizationId,
  2394. )
  2395. if (apiConfiguration?.kilocodeOrganizationId && !organizationExists) {
  2396. provider.upsertProviderProfile(currentApiConfigName ?? "default", {
  2397. ...apiConfiguration,
  2398. kilocodeOrganizationId: undefined,
  2399. })
  2400. }
  2401. try {
  2402. const shouldAutoSwitch =
  2403. response.data.organizations &&
  2404. response.data.organizations.length > 0 &&
  2405. !apiConfiguration.kilocodeOrganizationId &&
  2406. !getGlobalState("hasPerformedOrganizationAutoSwitch")
  2407. if (shouldAutoSwitch) {
  2408. const firstOrg = response.data.organizations![0]
  2409. provider.log(
  2410. `[Auto-switch] Performing automatic organization switch to: ${firstOrg.name} (${firstOrg.id})`,
  2411. )
  2412. const upsertMessage: WebviewMessage = {
  2413. type: "upsertApiConfiguration",
  2414. text: currentApiConfigName ?? "default",
  2415. apiConfiguration: {
  2416. ...apiConfiguration,
  2417. kilocodeOrganizationId: firstOrg.id,
  2418. },
  2419. }
  2420. await webviewMessageHandler(provider, upsertMessage)
  2421. await updateGlobalState("hasPerformedOrganizationAutoSwitch", true)
  2422. vscode.window.showInformationMessage(`Automatically switched to organization: ${firstOrg.name}`)
  2423. provider.log(`[Auto-switch] Successfully switched to organization: ${firstOrg.name}`)
  2424. }
  2425. } catch (error) {
  2426. provider.log(
  2427. `[Auto-switch] Error during automatic organization switch: ${error instanceof Error ? error.message : String(error)}`,
  2428. )
  2429. }
  2430. provider.postMessageToWebview({
  2431. type: "profileDataResponse",
  2432. payload: { success: true, data: { kilocodeToken, ...response.data } },
  2433. })
  2434. } catch (error: any) {
  2435. const errorMessage =
  2436. error.response?.data?.message ||
  2437. error.message ||
  2438. "Failed to fetch general profile data from backend."
  2439. provider.log(`Error fetching general profile data: ${errorMessage}`)
  2440. provider.postMessageToWebview({
  2441. type: "profileDataResponse",
  2442. payload: { success: false, error: errorMessage },
  2443. })
  2444. }
  2445. break
  2446. case "fetchBalanceDataRequest": // New handler
  2447. try {
  2448. const { apiConfiguration } = await provider.getState()
  2449. const { kilocodeToken, kilocodeOrganizationId } = apiConfiguration ?? {}
  2450. if (!kilocodeToken) {
  2451. provider.log("KiloCode token not found in extension state for balance data.")
  2452. provider.postMessageToWebview({
  2453. type: "balanceDataResponse", // New response type
  2454. payload: { success: false, error: "KiloCode API token not configured." },
  2455. })
  2456. break
  2457. }
  2458. const headers: Record<string, string> = {
  2459. Authorization: `Bearer ${kilocodeToken}`,
  2460. "Content-Type": "application/json",
  2461. }
  2462. if (kilocodeOrganizationId) {
  2463. headers["X-KiloCode-OrganizationId"] = kilocodeOrganizationId
  2464. }
  2465. // Add X-KILOCODE-TESTER: SUPPRESS header if the setting is enabled
  2466. if (
  2467. apiConfiguration.kilocodeTesterWarningsDisabledUntil &&
  2468. apiConfiguration.kilocodeTesterWarningsDisabledUntil > Date.now()
  2469. ) {
  2470. headers["X-KILOCODE-TESTER"] = "SUPPRESS"
  2471. }
  2472. const response = await axios.get(`${getKiloBaseUriFromToken(kilocodeToken)}/api/profile/balance`, {
  2473. // Original path for balance
  2474. headers,
  2475. })
  2476. provider.postMessageToWebview({
  2477. type: "balanceDataResponse", // New response type
  2478. payload: { success: true, data: response.data },
  2479. })
  2480. } catch (error: any) {
  2481. const errorMessage =
  2482. error.response?.data?.message || error.message || "Failed to fetch balance data from backend."
  2483. provider.log(`Error fetching balance data: ${errorMessage}`)
  2484. provider.postMessageToWebview({
  2485. type: "balanceDataResponse", // New response type
  2486. payload: { success: false, error: errorMessage },
  2487. })
  2488. }
  2489. break
  2490. case "shopBuyCredits": // New handler
  2491. try {
  2492. const { apiConfiguration } = await provider.getState()
  2493. const kilocodeToken = apiConfiguration?.kilocodeToken
  2494. if (!kilocodeToken) {
  2495. provider.log("KiloCode token not found in extension state for buy credits.")
  2496. break
  2497. }
  2498. const credits = message.values?.credits || 50
  2499. const uriScheme = message.values?.uriScheme || "vscode"
  2500. const uiKind = message.values?.uiKind || "Desktop"
  2501. const source = uiKind === "Web" ? "web" : uriScheme
  2502. const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
  2503. const response = await axios.post(
  2504. `${baseUrl}/payments/topup?origin=extension&source=${source}&amount=${credits}`,
  2505. {},
  2506. {
  2507. headers: {
  2508. Authorization: `Bearer ${kilocodeToken}`,
  2509. "Content-Type": "application/json",
  2510. },
  2511. maxRedirects: 0, // Prevent axios from following redirects automatically
  2512. validateStatus: (status) => status < 400, // Accept 3xx status codes
  2513. },
  2514. )
  2515. if (response.status !== 303 || !response.headers.location) {
  2516. return
  2517. }
  2518. await vscode.env.openExternal(vscode.Uri.parse(response.headers.location))
  2519. } catch (error: any) {
  2520. const errorMessage = error?.message || "Unknown error"
  2521. const errorStack = error?.stack ? ` Stack: ${error.stack}` : ""
  2522. provider.log(`Error redirecting to payment page: ${errorMessage}.${errorStack}`)
  2523. provider.postMessageToWebview({
  2524. type: "updateProfileData",
  2525. })
  2526. }
  2527. break
  2528. case "fetchMcpMarketplace": {
  2529. await provider.fetchMcpMarketplace(message.bool)
  2530. break
  2531. }
  2532. case "downloadMcp": {
  2533. if (message.mcpId) {
  2534. await provider.downloadMcp(message.mcpId)
  2535. }
  2536. break
  2537. }
  2538. case "silentlyRefreshMcpMarketplace": {
  2539. await provider.silentlyRefreshMcpMarketplace()
  2540. break
  2541. }
  2542. case "toggleWorkflow": {
  2543. if (message.workflowPath && typeof message.enabled === "boolean" && typeof message.isGlobal === "boolean") {
  2544. await toggleWorkflow(
  2545. message.workflowPath,
  2546. message.enabled,
  2547. message.isGlobal,
  2548. provider.contextProxy,
  2549. provider.context,
  2550. )
  2551. await provider.postRulesDataToWebview()
  2552. }
  2553. break
  2554. }
  2555. case "refreshRules": {
  2556. await provider.postRulesDataToWebview()
  2557. break
  2558. }
  2559. case "toggleRule": {
  2560. if (message.rulePath && typeof message.enabled === "boolean" && typeof message.isGlobal === "boolean") {
  2561. await toggleRule(
  2562. message.rulePath,
  2563. message.enabled,
  2564. message.isGlobal,
  2565. provider.contextProxy,
  2566. provider.context,
  2567. )
  2568. await provider.postRulesDataToWebview()
  2569. }
  2570. break
  2571. }
  2572. case "createRuleFile": {
  2573. if (
  2574. message.filename &&
  2575. typeof message.isGlobal === "boolean" &&
  2576. (message.ruleType === "rule" || message.ruleType === "workflow")
  2577. ) {
  2578. try {
  2579. await createRuleFile(message.filename, message.isGlobal, message.ruleType)
  2580. } catch (error) {
  2581. console.error("Error creating rule file:", error)
  2582. vscode.window.showErrorMessage(t("kilocode:rules.errors.failedToCreateRuleFile"))
  2583. }
  2584. await provider.postRulesDataToWebview()
  2585. }
  2586. break
  2587. }
  2588. case "deleteRuleFile": {
  2589. if (message.rulePath) {
  2590. try {
  2591. await deleteRuleFile(message.rulePath)
  2592. } catch (error) {
  2593. console.error("Error deleting rule file:", error)
  2594. vscode.window.showErrorMessage(t("kilocode:rules.errors.failedToDeleteRuleFile"))
  2595. }
  2596. await provider.postRulesDataToWebview()
  2597. }
  2598. break
  2599. }
  2600. case "reportBug":
  2601. provider.getCurrentTask()?.handleWebviewAskResponse("yesButtonClicked")
  2602. break
  2603. // end kilocode_change
  2604. case "telemetrySetting": {
  2605. const telemetrySetting = message.text as TelemetrySetting
  2606. await updateGlobalState("telemetrySetting", telemetrySetting)
  2607. const isOptedIn = telemetrySetting === "enabled"
  2608. TelemetryService.instance.updateTelemetryState(isOptedIn)
  2609. await provider.postStateToWebview()
  2610. break
  2611. }
  2612. case "cloudButtonClicked": {
  2613. // Navigate to the cloud tab.
  2614. provider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
  2615. break
  2616. }
  2617. case "rooCloudSignIn": {
  2618. try {
  2619. TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
  2620. await CloudService.instance.login()
  2621. } catch (error) {
  2622. provider.log(`AuthService#login failed: ${error}`)
  2623. vscode.window.showErrorMessage("Sign in failed.")
  2624. }
  2625. break
  2626. }
  2627. case "cloudLandingPageSignIn": {
  2628. try {
  2629. const landingPageSlug = message.text || "supernova"
  2630. TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
  2631. await CloudService.instance.login(landingPageSlug)
  2632. } catch (error) {
  2633. provider.log(`CloudService#login failed: ${error}`)
  2634. vscode.window.showErrorMessage("Sign in failed.")
  2635. }
  2636. break
  2637. }
  2638. case "rooCloudSignOut": {
  2639. try {
  2640. await CloudService.instance.logout()
  2641. await provider.postStateToWebview()
  2642. provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined })
  2643. } catch (error) {
  2644. provider.log(`AuthService#logout failed: ${error}`)
  2645. vscode.window.showErrorMessage("Sign out failed.")
  2646. }
  2647. break
  2648. }
  2649. case "rooCloudManualUrl": {
  2650. try {
  2651. if (!message.text) {
  2652. vscode.window.showErrorMessage(t("common:errors.manual_url_empty"))
  2653. break
  2654. }
  2655. // Parse the callback URL to extract parameters
  2656. const callbackUrl = message.text.trim()
  2657. const uri = vscode.Uri.parse(callbackUrl)
  2658. if (!uri.query) {
  2659. throw new Error(t("common:errors.manual_url_no_query"))
  2660. }
  2661. const query = new URLSearchParams(uri.query)
  2662. const code = query.get("code")
  2663. const state = query.get("state")
  2664. const organizationId = query.get("organizationId")
  2665. if (!code || !state) {
  2666. throw new Error(t("common:errors.manual_url_missing_params"))
  2667. }
  2668. // Reuse the existing authentication flow
  2669. await CloudService.instance.handleAuthCallback(
  2670. code,
  2671. state,
  2672. organizationId === "null" ? null : organizationId,
  2673. )
  2674. await provider.postStateToWebview()
  2675. } catch (error) {
  2676. provider.log(`ManualUrl#handleAuthCallback failed: ${error}`)
  2677. const errorMessage = error instanceof Error ? error.message : t("common:errors.manual_url_auth_failed")
  2678. // Show error message through VS Code UI
  2679. vscode.window.showErrorMessage(`${t("common:errors.manual_url_auth_error")}: ${errorMessage}`)
  2680. }
  2681. break
  2682. }
  2683. case "switchOrganization": {
  2684. try {
  2685. const organizationId = message.organizationId ?? null
  2686. // Switch to the new organization context
  2687. await CloudService.instance.switchOrganization(organizationId)
  2688. // Refresh the state to update UI
  2689. await provider.postStateToWebview()
  2690. // Send success response back to webview
  2691. await provider.postMessageToWebview({
  2692. type: "organizationSwitchResult",
  2693. success: true,
  2694. organizationId: organizationId,
  2695. })
  2696. } catch (error) {
  2697. provider.log(`Organization switch failed: ${error}`)
  2698. const errorMessage = error instanceof Error ? error.message : String(error)
  2699. // Send error response back to webview
  2700. await provider.postMessageToWebview({
  2701. type: "organizationSwitchResult",
  2702. success: false,
  2703. error: errorMessage,
  2704. organizationId: message.organizationId ?? null,
  2705. })
  2706. vscode.window.showErrorMessage(`Failed to switch organization: ${errorMessage}`)
  2707. }
  2708. break
  2709. }
  2710. case "saveCodeIndexSettingsAtomic": {
  2711. if (!message.codeIndexSettings) {
  2712. break
  2713. }
  2714. const settings = message.codeIndexSettings
  2715. try {
  2716. // Check if embedder provider has changed
  2717. const currentConfig = getGlobalState("codebaseIndexConfig") || {}
  2718. const embedderProviderChanged =
  2719. currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
  2720. // Save global state settings atomically
  2721. const globalStateConfig = {
  2722. ...currentConfig,
  2723. codebaseIndexEnabled: settings.codebaseIndexEnabled,
  2724. codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
  2725. codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
  2726. codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl,
  2727. codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId,
  2728. codebaseIndexEmbedderModelDimension: settings.codebaseIndexEmbedderModelDimension, // Generic dimension
  2729. codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl,
  2730. codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
  2731. codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
  2732. }
  2733. // Save global state first
  2734. await updateGlobalState("codebaseIndexConfig", globalStateConfig)
  2735. // Save secrets directly using context proxy
  2736. if (settings.codeIndexOpenAiKey !== undefined) {
  2737. await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey)
  2738. }
  2739. if (settings.codeIndexQdrantApiKey !== undefined) {
  2740. await provider.contextProxy.storeSecret("codeIndexQdrantApiKey", settings.codeIndexQdrantApiKey)
  2741. }
  2742. if (settings.codebaseIndexOpenAiCompatibleApiKey !== undefined) {
  2743. await provider.contextProxy.storeSecret(
  2744. "codebaseIndexOpenAiCompatibleApiKey",
  2745. settings.codebaseIndexOpenAiCompatibleApiKey,
  2746. )
  2747. }
  2748. if (settings.codebaseIndexGeminiApiKey !== undefined) {
  2749. await provider.contextProxy.storeSecret(
  2750. "codebaseIndexGeminiApiKey",
  2751. settings.codebaseIndexGeminiApiKey,
  2752. )
  2753. }
  2754. if (settings.codebaseIndexMistralApiKey !== undefined) {
  2755. await provider.contextProxy.storeSecret(
  2756. "codebaseIndexMistralApiKey",
  2757. settings.codebaseIndexMistralApiKey,
  2758. )
  2759. }
  2760. if (settings.codebaseIndexVercelAiGatewayApiKey !== undefined) {
  2761. await provider.contextProxy.storeSecret(
  2762. "codebaseIndexVercelAiGatewayApiKey",
  2763. settings.codebaseIndexVercelAiGatewayApiKey,
  2764. )
  2765. }
  2766. // Send success response first - settings are saved regardless of validation
  2767. await provider.postMessageToWebview({
  2768. type: "codeIndexSettingsSaved",
  2769. success: true,
  2770. settings: globalStateConfig,
  2771. })
  2772. // Update webview state
  2773. await provider.postStateToWebview()
  2774. // Then handle validation and initialization for the current workspace
  2775. const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
  2776. if (currentCodeIndexManager) {
  2777. // If embedder provider changed, perform proactive validation
  2778. if (embedderProviderChanged) {
  2779. try {
  2780. // Force handleSettingsChange which will trigger validation
  2781. await currentCodeIndexManager.handleSettingsChange()
  2782. } catch (error) {
  2783. // Validation failed - the error state is already set by handleSettingsChange
  2784. provider.log(
  2785. `Embedder validation failed after provider change: ${error instanceof Error ? error.message : String(error)}`,
  2786. )
  2787. // Send validation error to webview
  2788. await provider.postMessageToWebview({
  2789. type: "indexingStatusUpdate",
  2790. values: currentCodeIndexManager.getCurrentStatus(),
  2791. })
  2792. // Exit early - don't try to start indexing with invalid configuration
  2793. break
  2794. }
  2795. } else {
  2796. // No provider change, just handle settings normally
  2797. try {
  2798. await currentCodeIndexManager.handleSettingsChange()
  2799. } catch (error) {
  2800. // Log but don't fail - settings are saved
  2801. provider.log(
  2802. `Settings change handling error: ${error instanceof Error ? error.message : String(error)}`,
  2803. )
  2804. }
  2805. }
  2806. // Wait a bit more to ensure everything is ready
  2807. await new Promise((resolve) => setTimeout(resolve, 200))
  2808. // Auto-start indexing if now enabled and configured
  2809. if (currentCodeIndexManager.isFeatureEnabled && currentCodeIndexManager.isFeatureConfigured) {
  2810. if (!currentCodeIndexManager.isInitialized) {
  2811. try {
  2812. await currentCodeIndexManager.initialize(provider.contextProxy)
  2813. provider.log(`Code index manager initialized after settings save`)
  2814. } catch (error) {
  2815. provider.log(
  2816. `Code index initialization failed: ${error instanceof Error ? error.message : String(error)}`,
  2817. )
  2818. // Send error status to webview
  2819. await provider.postMessageToWebview({
  2820. type: "indexingStatusUpdate",
  2821. values: currentCodeIndexManager.getCurrentStatus(),
  2822. })
  2823. }
  2824. }
  2825. }
  2826. } else {
  2827. // No workspace open - send error status
  2828. provider.log("Cannot save code index settings: No workspace folder open")
  2829. await provider.postMessageToWebview({
  2830. type: "indexingStatusUpdate",
  2831. values: {
  2832. systemStatus: "Error",
  2833. message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
  2834. processedItems: 0,
  2835. totalItems: 0,
  2836. currentItemUnit: "items",
  2837. },
  2838. })
  2839. }
  2840. } catch (error) {
  2841. provider.log(`Error saving code index settings: ${error.message || error}`)
  2842. await provider.postMessageToWebview({
  2843. type: "codeIndexSettingsSaved",
  2844. success: false,
  2845. error: error.message || "Failed to save settings",
  2846. })
  2847. }
  2848. break
  2849. }
  2850. case "requestIndexingStatus": {
  2851. const manager = provider.getCurrentWorkspaceCodeIndexManager()
  2852. if (!manager) {
  2853. // No workspace open - send error status
  2854. provider.postMessageToWebview({
  2855. type: "indexingStatusUpdate",
  2856. values: {
  2857. systemStatus: "Error",
  2858. message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
  2859. processedItems: 0,
  2860. totalItems: 0,
  2861. currentItemUnit: "items",
  2862. workerspacePath: undefined,
  2863. },
  2864. })
  2865. return
  2866. }
  2867. const status = manager
  2868. ? manager.getCurrentStatus()
  2869. : {
  2870. systemStatus: "Standby",
  2871. message: "No workspace folder open",
  2872. processedItems: 0,
  2873. totalItems: 0,
  2874. currentItemUnit: "items",
  2875. workspacePath: undefined,
  2876. }
  2877. provider.postMessageToWebview({
  2878. type: "indexingStatusUpdate",
  2879. values: status,
  2880. })
  2881. break
  2882. }
  2883. case "requestCodeIndexSecretStatus": {
  2884. // Check if secrets are set using the VSCode context directly for async access
  2885. const hasOpenAiKey = !!(await provider.context.secrets.get("codeIndexOpenAiKey"))
  2886. const hasQdrantApiKey = !!(await provider.context.secrets.get("codeIndexQdrantApiKey"))
  2887. const hasOpenAiCompatibleApiKey = !!(await provider.context.secrets.get(
  2888. "codebaseIndexOpenAiCompatibleApiKey",
  2889. ))
  2890. const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey"))
  2891. const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey"))
  2892. const hasVercelAiGatewayApiKey = !!(await provider.context.secrets.get(
  2893. "codebaseIndexVercelAiGatewayApiKey",
  2894. ))
  2895. provider.postMessageToWebview({
  2896. type: "codeIndexSecretStatus",
  2897. values: {
  2898. hasOpenAiKey,
  2899. hasQdrantApiKey,
  2900. hasOpenAiCompatibleApiKey,
  2901. hasGeminiApiKey,
  2902. hasMistralApiKey,
  2903. hasVercelAiGatewayApiKey,
  2904. },
  2905. })
  2906. break
  2907. }
  2908. case "startIndexing": {
  2909. try {
  2910. const manager = provider.getCurrentWorkspaceCodeIndexManager()
  2911. if (!manager) {
  2912. // No workspace open - send error status
  2913. provider.postMessageToWebview({
  2914. type: "indexingStatusUpdate",
  2915. values: {
  2916. systemStatus: "Error",
  2917. message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
  2918. processedItems: 0,
  2919. totalItems: 0,
  2920. currentItemUnit: "items",
  2921. },
  2922. })
  2923. provider.log("Cannot start indexing: No workspace folder open")
  2924. return
  2925. }
  2926. if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
  2927. if (!manager.isInitialized) {
  2928. await manager.initialize(provider.contextProxy)
  2929. }
  2930. // startIndexing now handles error recovery internally
  2931. manager.startIndexing()
  2932. // If startIndexing recovered from error, we need to reinitialize
  2933. if (!manager.isInitialized) {
  2934. await manager.initialize(provider.contextProxy)
  2935. // Try starting again after initialization
  2936. manager.startIndexing()
  2937. }
  2938. }
  2939. } catch (error) {
  2940. provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)
  2941. }
  2942. break
  2943. }
  2944. // kilocode_change start
  2945. case "cancelIndexing": {
  2946. try {
  2947. const manager = provider.getCurrentWorkspaceCodeIndexManager()
  2948. if (!manager) {
  2949. provider.postMessageToWebview({
  2950. type: "indexingStatusUpdate",
  2951. values: {
  2952. systemStatus: "Error",
  2953. message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
  2954. processedItems: 0,
  2955. totalItems: 0,
  2956. currentItemUnit: "items",
  2957. },
  2958. })
  2959. provider.log("Cannot cancel indexing: No workspace folder open")
  2960. return
  2961. }
  2962. if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
  2963. manager.cancelIndexing()
  2964. // Immediately reflect updated status to UI
  2965. provider.postMessageToWebview({
  2966. type: "indexingStatusUpdate",
  2967. values: manager.getCurrentStatus(),
  2968. })
  2969. }
  2970. } catch (error) {
  2971. provider.log(`Error canceling indexing: ${error instanceof Error ? error.message : String(error)}`)
  2972. }
  2973. break
  2974. }
  2975. // kilocode_change end
  2976. case "clearIndexData": {
  2977. try {
  2978. const manager = provider.getCurrentWorkspaceCodeIndexManager()
  2979. if (!manager) {
  2980. provider.log("Cannot clear index data: No workspace folder open")
  2981. provider.postMessageToWebview({
  2982. type: "indexCleared",
  2983. values: {
  2984. success: false,
  2985. error: t("embeddings:orchestrator.indexingRequiresWorkspace"),
  2986. },
  2987. })
  2988. return
  2989. }
  2990. await manager.clearIndexData()
  2991. provider.postMessageToWebview({ type: "indexCleared", values: { success: true } })
  2992. } catch (error) {
  2993. provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`)
  2994. provider.postMessageToWebview({
  2995. type: "indexCleared",
  2996. values: {
  2997. success: false,
  2998. error: error instanceof Error ? error.message : String(error),
  2999. },
  3000. })
  3001. }
  3002. break
  3003. }
  3004. // kilocode_change start - add clearUsageData
  3005. case "clearUsageData": {
  3006. try {
  3007. const usageTracker = UsageTracker.getInstance()
  3008. await usageTracker.clearAllUsageData()
  3009. vscode.window.showInformationMessage("Usage data has been successfully cleared.")
  3010. } catch (error) {
  3011. const errorMessage = error instanceof Error ? error.message : String(error)
  3012. provider.log(`Error clearing usage data: ${errorMessage}`)
  3013. vscode.window.showErrorMessage(`Failed to clear usage data: ${errorMessage}`)
  3014. }
  3015. break
  3016. }
  3017. // kilocode_change start - add getUsageData
  3018. case "getUsageData": {
  3019. if (message.text) {
  3020. try {
  3021. const usageTracker = UsageTracker.getInstance()
  3022. const usageData = usageTracker.getAllUsage(message.text)
  3023. await provider.postMessageToWebview({
  3024. type: "usageDataResponse",
  3025. text: message.text,
  3026. values: usageData,
  3027. })
  3028. } catch (error) {
  3029. const errorMessage = error instanceof Error ? error.message : String(error)
  3030. provider.log(`Error getting usage data: ${errorMessage}`)
  3031. }
  3032. }
  3033. break
  3034. }
  3035. // kilocode_change end - add getUsageData
  3036. // kilocode_change start - add toggleTaskFavorite
  3037. case "toggleTaskFavorite":
  3038. if (message.text) {
  3039. await provider.toggleTaskFavorite(message.text)
  3040. }
  3041. break
  3042. // kilocode_change start - add fixMermaidSyntax
  3043. case "fixMermaidSyntax":
  3044. if (message.text && message.requestId) {
  3045. try {
  3046. const { apiConfiguration } = await provider.getState()
  3047. const prompt = mermaidFixPrompt(message.values?.error || "Unknown syntax error", message.text)
  3048. const fixedCode = await singleCompletionHandler(apiConfiguration, prompt)
  3049. provider.postMessageToWebview({
  3050. type: "mermaidFixResponse",
  3051. requestId: message.requestId,
  3052. success: true,
  3053. fixedCode: fixedCode?.trim() || null,
  3054. })
  3055. } catch (error) {
  3056. const errorMessage = error instanceof Error ? error.message : "Failed to fix Mermaid syntax"
  3057. provider.log(`Error fixing Mermaid syntax: ${errorMessage}`)
  3058. provider.postMessageToWebview({
  3059. type: "mermaidFixResponse",
  3060. requestId: message.requestId,
  3061. success: false,
  3062. error: errorMessage,
  3063. })
  3064. }
  3065. }
  3066. break
  3067. // kilocode_change end
  3068. case "focusPanelRequest": {
  3069. // Execute the focusPanel command to focus the WebView
  3070. await vscode.commands.executeCommand(getCommand("focusPanel"))
  3071. break
  3072. }
  3073. case "filterMarketplaceItems": {
  3074. if (marketplaceManager && message.filters) {
  3075. try {
  3076. await marketplaceManager.updateWithFilteredItems({
  3077. type: message.filters.type as MarketplaceItemType | undefined,
  3078. search: message.filters.search,
  3079. tags: message.filters.tags,
  3080. })
  3081. await provider.postStateToWebview()
  3082. } catch (error) {
  3083. console.error("Marketplace: Error filtering items:", error)
  3084. vscode.window.showErrorMessage("Failed to filter marketplace items")
  3085. }
  3086. }
  3087. break
  3088. }
  3089. case "fetchMarketplaceData": {
  3090. // Fetch marketplace data on demand
  3091. await provider.fetchMarketplaceData()
  3092. break
  3093. }
  3094. case "installMarketplaceItem": {
  3095. if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
  3096. try {
  3097. const configFilePath = await marketplaceManager.installMarketplaceItem(
  3098. message.mpItem,
  3099. message.mpInstallOptions,
  3100. )
  3101. await provider.postStateToWebview()
  3102. console.log(`Marketplace item installed and config file opened: ${configFilePath}`)
  3103. // Send success message to webview
  3104. provider.postMessageToWebview({
  3105. type: "marketplaceInstallResult",
  3106. success: true,
  3107. slug: message.mpItem.id,
  3108. })
  3109. } catch (error) {
  3110. console.error(`Error installing marketplace item: ${error}`)
  3111. // Send error message to webview
  3112. provider.postMessageToWebview({
  3113. type: "marketplaceInstallResult",
  3114. success: false,
  3115. error: error instanceof Error ? error.message : String(error),
  3116. slug: message.mpItem.id,
  3117. })
  3118. }
  3119. }
  3120. break
  3121. }
  3122. case "removeInstalledMarketplaceItem": {
  3123. if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
  3124. try {
  3125. await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
  3126. await provider.postStateToWebview()
  3127. // Send success message to webview
  3128. provider.postMessageToWebview({
  3129. type: "marketplaceRemoveResult",
  3130. success: true,
  3131. slug: message.mpItem.id,
  3132. })
  3133. } catch (error) {
  3134. console.error(`Error removing marketplace item: ${error}`)
  3135. // Show error message to user
  3136. vscode.window.showErrorMessage(
  3137. `Failed to remove marketplace item: ${error instanceof Error ? error.message : String(error)}`,
  3138. )
  3139. // Send error message to webview
  3140. provider.postMessageToWebview({
  3141. type: "marketplaceRemoveResult",
  3142. success: false,
  3143. error: error instanceof Error ? error.message : String(error),
  3144. slug: message.mpItem.id,
  3145. })
  3146. }
  3147. } else {
  3148. // MarketplaceManager not available or missing required parameters
  3149. const errorMessage = !marketplaceManager
  3150. ? "Marketplace manager is not available"
  3151. : "Missing required parameters for marketplace item removal"
  3152. console.error(errorMessage)
  3153. vscode.window.showErrorMessage(errorMessage)
  3154. if (message.mpItem?.id) {
  3155. provider.postMessageToWebview({
  3156. type: "marketplaceRemoveResult",
  3157. success: false,
  3158. error: errorMessage,
  3159. slug: message.mpItem.id,
  3160. })
  3161. }
  3162. }
  3163. break
  3164. }
  3165. case "installMarketplaceItemWithParameters": {
  3166. if (marketplaceManager && message.payload && "item" in message.payload && "parameters" in message.payload) {
  3167. try {
  3168. const configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {
  3169. parameters: message.payload.parameters,
  3170. })
  3171. await provider.postStateToWebview()
  3172. console.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`)
  3173. } catch (error) {
  3174. console.error(`Error installing marketplace item with parameters: ${error}`)
  3175. vscode.window.showErrorMessage(
  3176. `Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`,
  3177. )
  3178. }
  3179. }
  3180. break
  3181. }
  3182. case "switchTab": {
  3183. if (message.tab) {
  3184. // Capture tab shown event for all switchTab messages (which are user-initiated)
  3185. if (TelemetryService.hasInstance()) {
  3186. TelemetryService.instance.captureTabShown(message.tab)
  3187. }
  3188. await provider.postMessageToWebview({
  3189. type: "action",
  3190. action: "switchTab",
  3191. tab: message.tab,
  3192. values: message.values,
  3193. })
  3194. }
  3195. break
  3196. }
  3197. // kilocode_change start
  3198. case "editMessage": {
  3199. await editMessageHandler(provider, message)
  3200. break
  3201. }
  3202. case "fetchKilocodeNotifications": {
  3203. await fetchKilocodeNotificationsHandler(provider)
  3204. break
  3205. }
  3206. case "dismissNotificationId": {
  3207. if (!message.notificationId) {
  3208. break
  3209. }
  3210. const dismissedNotificationIds = getGlobalState("dismissedNotificationIds") || []
  3211. await updateGlobalState("dismissedNotificationIds", [...dismissedNotificationIds, message.notificationId])
  3212. await provider.postStateToWebview()
  3213. break
  3214. }
  3215. // kilocode_change end
  3216. // kilocode_change start: Type-safe global state handler
  3217. case "updateGlobalState": {
  3218. const { stateKey, stateValue } = message as UpdateGlobalStateMessage
  3219. if (stateKey !== undefined && stateValue !== undefined && isGlobalStateKey(stateKey)) {
  3220. await updateGlobalState(stateKey, stateValue)
  3221. await provider.postStateToWebview()
  3222. }
  3223. break
  3224. }
  3225. // kilocode_change end: Type-safe global state handler
  3226. case "insertTextToChatArea":
  3227. provider.postMessageToWebview({ type: "insertTextToChatArea", text: message.text })
  3228. break
  3229. case "requestCommands": {
  3230. try {
  3231. const { getCommands } = await import("../../services/command/commands")
  3232. const commands = await getCommands(getCurrentCwd())
  3233. // Convert to the format expected by the frontend
  3234. const commandList = commands.map((command) => ({
  3235. name: command.name,
  3236. source: command.source,
  3237. filePath: command.filePath,
  3238. description: command.description,
  3239. argumentHint: command.argumentHint,
  3240. }))
  3241. await provider.postMessageToWebview({
  3242. type: "commands",
  3243. commands: commandList,
  3244. })
  3245. } catch (error) {
  3246. provider.log(`Error fetching commands: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
  3247. // Send empty array on error
  3248. await provider.postMessageToWebview({
  3249. type: "commands",
  3250. commands: [],
  3251. })
  3252. }
  3253. break
  3254. }
  3255. case "getKeybindings": {
  3256. try {
  3257. const { getKeybindingsForCommands } = await import("../../utils/keybindings")
  3258. const keybindings = await getKeybindingsForCommands(message.commandIds ?? [])
  3259. await provider.postMessageToWebview({ type: "keybindingsResponse", keybindings })
  3260. } catch (error) {
  3261. await provider.postMessageToWebview({ type: "keybindingsResponse", keybindings: {} })
  3262. }
  3263. break
  3264. }
  3265. case "openCommandFile": {
  3266. try {
  3267. if (message.text) {
  3268. const { getCommand } = await import("../../services/command/commands")
  3269. const command = await getCommand(getCurrentCwd(), message.text)
  3270. if (command && command.filePath) {
  3271. openFile(command.filePath)
  3272. } else {
  3273. vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text }))
  3274. }
  3275. }
  3276. } catch (error) {
  3277. provider.log(
  3278. `Error opening command file: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
  3279. )
  3280. vscode.window.showErrorMessage(t("common:errors.open_command_file"))
  3281. }
  3282. break
  3283. }
  3284. case "deleteCommand": {
  3285. try {
  3286. if (message.text && message.values?.source) {
  3287. const { getCommand } = await import("../../services/command/commands")
  3288. const command = await getCommand(getCurrentCwd(), message.text)
  3289. if (command && command.filePath) {
  3290. // Delete the command file
  3291. await fs.unlink(command.filePath)
  3292. provider.log(`Deleted command file: ${command.filePath}`)
  3293. } else {
  3294. vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text }))
  3295. }
  3296. }
  3297. } catch (error) {
  3298. provider.log(`Error deleting command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
  3299. vscode.window.showErrorMessage(t("common:errors.delete_command"))
  3300. }
  3301. break
  3302. }
  3303. case "createCommand": {
  3304. try {
  3305. const source = message.values?.source as "global" | "project"
  3306. const fileName = message.text // Custom filename from user input
  3307. if (!source) {
  3308. provider.log("Missing source for createCommand")
  3309. break
  3310. }
  3311. // Determine the commands directory based on source
  3312. let commandsDir: string
  3313. if (source === "global") {
  3314. const globalConfigDir = path.join(os.homedir(), ".roo")
  3315. commandsDir = path.join(globalConfigDir, "commands")
  3316. } else {
  3317. if (!vscode.workspace.workspaceFolders?.length) {
  3318. vscode.window.showErrorMessage(t("common:errors.no_workspace"))
  3319. return
  3320. }
  3321. // Project commands
  3322. const workspaceRoot = getCurrentCwd()
  3323. if (!workspaceRoot) {
  3324. vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command"))
  3325. break
  3326. }
  3327. commandsDir = path.join(workspaceRoot, ".roo", "commands")
  3328. }
  3329. // Ensure the commands directory exists
  3330. await fs.mkdir(commandsDir, { recursive: true })
  3331. // Use provided filename or generate a unique one
  3332. let commandName: string
  3333. if (fileName && fileName.trim()) {
  3334. let cleanFileName = fileName.trim()
  3335. // Strip leading slash if present
  3336. if (cleanFileName.startsWith("/")) {
  3337. cleanFileName = cleanFileName.substring(1)
  3338. }
  3339. // Remove .md extension if present BEFORE slugification
  3340. if (cleanFileName.toLowerCase().endsWith(".md")) {
  3341. cleanFileName = cleanFileName.slice(0, -3)
  3342. }
  3343. // Slugify the command name: lowercase, replace spaces with dashes, remove special characters
  3344. commandName = cleanFileName
  3345. .toLowerCase()
  3346. .replace(/\s+/g, "-") // Replace spaces with dashes
  3347. .replace(/[^a-z0-9-]/g, "") // Remove special characters except dashes
  3348. .replace(/-+/g, "-") // Replace multiple dashes with single dash
  3349. .replace(/^-|-$/g, "") // Remove leading/trailing dashes
  3350. // Ensure we have a valid command name
  3351. if (!commandName || commandName.length === 0) {
  3352. commandName = "new-command"
  3353. }
  3354. } else {
  3355. // Generate a unique command name
  3356. commandName = "new-command"
  3357. let counter = 1
  3358. let filePath = path.join(commandsDir, `${commandName}.md`)
  3359. while (
  3360. await fs
  3361. .access(filePath)
  3362. .then(() => true)
  3363. .catch(() => false)
  3364. ) {
  3365. commandName = `new-command-${counter}`
  3366. filePath = path.join(commandsDir, `${commandName}.md`)
  3367. counter++
  3368. }
  3369. }
  3370. const filePath = path.join(commandsDir, `${commandName}.md`)
  3371. // Check if file already exists
  3372. if (
  3373. await fs
  3374. .access(filePath)
  3375. .then(() => true)
  3376. .catch(() => false)
  3377. ) {
  3378. vscode.window.showErrorMessage(t("common:errors.command_already_exists", { commandName }))
  3379. break
  3380. }
  3381. // Create the command file with template content
  3382. const templateContent = t("common:errors.command_template_content")
  3383. await fs.writeFile(filePath, templateContent, "utf8")
  3384. provider.log(`Created new command file: ${filePath}`)
  3385. // Open the new file in the editor
  3386. openFile(filePath)
  3387. // Refresh commands list
  3388. const { getCommands } = await import("../../services/command/commands")
  3389. const commands = await getCommands(getCurrentCwd() || "")
  3390. const commandList = commands.map((command) => ({
  3391. name: command.name,
  3392. source: command.source,
  3393. filePath: command.filePath,
  3394. description: command.description,
  3395. argumentHint: command.argumentHint,
  3396. }))
  3397. await provider.postMessageToWebview({
  3398. type: "commands",
  3399. commands: commandList,
  3400. })
  3401. } catch (error) {
  3402. provider.log(`Error creating command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
  3403. vscode.window.showErrorMessage(t("common:errors.create_command_failed"))
  3404. }
  3405. break
  3406. }
  3407. case "insertTextIntoTextarea": {
  3408. const text = message.text
  3409. if (text) {
  3410. // Send message to insert text into the chat textarea
  3411. await provider.postMessageToWebview({
  3412. type: "insertTextIntoTextarea",
  3413. text: text,
  3414. })
  3415. }
  3416. break
  3417. }
  3418. case "showMdmAuthRequiredNotification": {
  3419. // Show notification that organization requires authentication
  3420. vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))
  3421. break
  3422. }
  3423. /**
  3424. * Chat Message Queue
  3425. */
  3426. case "queueMessage": {
  3427. provider.getCurrentTask()?.messageQueueService.addMessage(message.text ?? "", message.images)
  3428. break
  3429. }
  3430. case "removeQueuedMessage": {
  3431. provider.getCurrentTask()?.messageQueueService.removeMessage(message.text ?? "")
  3432. break
  3433. }
  3434. case "editQueuedMessage": {
  3435. if (message.payload) {
  3436. const { id, text, images } = message.payload as EditQueuedMessagePayload
  3437. provider.getCurrentTask()?.messageQueueService.updateMessage(id, text, images)
  3438. }
  3439. break
  3440. }
  3441. case "dismissUpsell": {
  3442. if (message.upsellId) {
  3443. try {
  3444. // Get current list of dismissed upsells
  3445. const dismissedUpsells = getGlobalState("dismissedUpsells") || []
  3446. // Add the new upsell ID if not already present
  3447. let updatedList = dismissedUpsells
  3448. if (!dismissedUpsells.includes(message.upsellId)) {
  3449. updatedList = [...dismissedUpsells, message.upsellId]
  3450. await updateGlobalState("dismissedUpsells", updatedList)
  3451. }
  3452. // Send updated list back to webview (use the already computed updatedList)
  3453. await provider.postMessageToWebview({
  3454. type: "dismissedUpsells",
  3455. list: updatedList,
  3456. })
  3457. } catch (error) {
  3458. // Fail silently as per Bruno's comment - it's OK to fail silently in this case
  3459. provider.log(`Failed to dismiss upsell: ${error instanceof Error ? error.message : String(error)}`)
  3460. }
  3461. }
  3462. break
  3463. }
  3464. case "getDismissedUpsells": {
  3465. // Send the current list of dismissed upsells to the webview
  3466. const dismissedUpsells = getGlobalState("dismissedUpsells") || []
  3467. await provider.postMessageToWebview({
  3468. type: "dismissedUpsells",
  3469. list: dismissedUpsells,
  3470. })
  3471. break
  3472. }
  3473. }
  3474. }