@@ -14,6 +14,7 @@ import {closeHost} from './host.js';
1414import {
1515 getInputText ,
1616 setInputText ,
17+ focusInput ,
1718 getSelectedModelId ,
1819 getAttachmentType ,
1920 renderAttachmentUI ,
@@ -199,9 +200,25 @@ function buildAttachmentChip (name) {
199200 return chip ;
200201}
201202
202- export function appendUserMessage ( pane , text , attachmentName ) {
203+ /**
204+ * @param {number } turnIndex index this message has (or will have) in `session.chatHistory`;
205+ * editing the message rolls the chat back to it.
206+ */
207+ export function appendUserMessage ( pane , text , attachmentName , turnIndex ) {
203208 const userMessage = document . createElement ( 'div' ) ;
204209 userMessage . className = 'user-message' ;
210+ userMessage . dataset . turnIndex = String ( turnIndex ) ;
211+
212+ const edit = document . createElement ( 'button' ) ;
213+ edit . type = 'button' ;
214+ edit . className = 'torchy-edit-message' ;
215+ edit . title = 'Edit this message' ;
216+ edit . innerHTML =
217+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" ' +
218+ 'stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" ' +
219+ 'd="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Z" /></svg>' ;
220+ userMessage . appendChild ( edit ) ;
221+
205222 const message = document . createElement ( 'div' ) ;
206223 message . className = 'message' ;
207224
@@ -213,9 +230,73 @@ export function appendUserMessage (pane, text, attachmentName) {
213230
214231 userMessage . appendChild ( message ) ;
215232 pane . appendChild ( userMessage ) ;
233+ attachEditButtonListeners ( userMessage ) ;
216234 return userMessage ;
217235}
218236
237+ const ROLLBACK_WARNING_HTML =
238+ '<p>Editing goes back to this point in the chat: this message and everything after it, ' +
239+ "including Torchy's answers, will be deleted. The message is put back in the text box so you can change it " +
240+ 'and send it again.</p><br><p>Code you already added to your workspace is not touched.</p>' ;
241+
242+ function countCodeChunks ( history ) {
243+ return history . reduce ( ( count , turn ) => {
244+ if ( turn . role !== 'assistant' ) return count ;
245+ return count + ( String ( turn . message || '' ) . match ( FENCE_REGEX ) || [ ] ) . length ;
246+ } , 0 ) ;
247+ }
248+
249+ function rollbackToTurn ( session , turnIndex , text ) {
250+ const pane = getPaneForSession ( session . id ) ;
251+ const kept = session . chatHistory . slice ( 0 , Math . max ( 0 , Math . min ( turnIndex , session . chatHistory . length ) ) ) ;
252+ session . chatHistory = kept ;
253+ session . allCodeChunksEverAdded = session . allCodeChunksEverAdded . slice ( 0 , countCodeChunks ( kept ) ) ;
254+ session . domCache = null ;
255+ session . inputText = text ;
256+ if ( pane ) rehydrateChatPane ( session , pane ) ;
257+ if ( session . id === store . activeSessionId ) {
258+ setInputText ( text ) ;
259+ focusInput ( ) ;
260+ }
261+ renderChatUI ( ) ;
262+ updateAndSaveSession ( session ) ;
263+ }
264+
265+ function attachEditButtonListeners ( parentElement ) {
266+ if ( ! parentElement ) return ;
267+ parentElement . querySelectorAll ( '.torchy-edit-message' ) . forEach ( button => {
268+ if ( button . dataset . listenerAttached ) return ;
269+ button . dataset . listenerAttached = 'true' ;
270+ button . addEventListener ( 'click' , ( ) => {
271+ const session = getActiveSession ( ) ;
272+ const userMessage = button . closest ( '.user-message' ) ;
273+ if ( ! session || ! userMessage ) return ;
274+ if ( session . isBlabbering ) {
275+ helpers . showAlert ( 'TorchyWaitForAIToFinishWarning' ) ;
276+ return ;
277+ }
278+ const turnIndex = Number ( userMessage . dataset . turnIndex ) ;
279+ if ( ! Number . isInteger ( turnIndex ) ) return ;
280+ const textEl = userMessage . querySelector ( '.message > span' ) ;
281+ const text = textEl ? textEl . textContent : '' ;
282+
283+ const ScratchBlocks = window . ScratchBlocks ;
284+ if ( ! ScratchBlocks || typeof ScratchBlocks . prompt !== 'function' ) return ;
285+ ScratchBlocks . prompt (
286+ ROLLBACK_WARNING_HTML ,
287+ null ,
288+ ( ) => {
289+ if ( session . isBlabbering ) return ;
290+ rollbackToTurn ( session , turnIndex , text ) ;
291+ } ,
292+ 'Edit this message?' ,
293+ ScratchBlocks . BROADCAST_MESSAGE_VARIABLE_TYPE ,
294+ true
295+ ) ;
296+ } ) ;
297+ } ) ;
298+ }
299+
219300export function removeWelcomeHeader ( pane ) {
220301 const header = pane . querySelector ( '.Popup_Header' ) ;
221302 if ( header ) header . remove ( ) ;
@@ -446,12 +527,12 @@ export function rehydrateChatPane (session, pane) {
446527
447528 let codeChunkCounter = 0 ;
448529
449- session . chatHistory . forEach ( turn => {
530+ session . chatHistory . forEach ( ( turn , turnIndex ) => {
450531 if ( turn . role === 'user' ) {
451532 const text = String ( turn . message || '' ) . split ( '\n\n\nContext:\n' ) [ 0 ] ;
452533 const attachmentName =
453534 turn . attachment && turn . attachment . type !== '0' ? turn . attachment . name : null ;
454- appendUserMessage ( pane , text , attachmentName ) ;
535+ appendUserMessage ( pane , text , attachmentName , turnIndex ) ;
455536 return ;
456537 }
457538 if ( turn . role !== 'assistant' ) return ;
@@ -582,10 +663,11 @@ function insertChunkIntoWorkspace (chunkData, workspace, replacingBlockIds) {
582663
583664export function restoreInteractivity ( pane ) {
584665 if ( ! pane ) return ;
585- pane . querySelectorAll ( '.insert_button' ) . forEach ( button => {
666+ pane . querySelectorAll ( '.insert_button, .torchy-edit-message ' ) . forEach ( button => {
586667 delete button . dataset . listenerAttached ;
587668 } ) ;
588669 attachInsertButtonListeners ( pane ) ;
670+ attachEditButtonListeners ( pane ) ;
589671 pane . querySelectorAll ( '.torchy-warnings' ) . forEach ( wrap => {
590672 const toggle = wrap . querySelector ( '.torchy-warnings-toggle' ) ;
591673 const list = wrap . querySelector ( '.torchy-warnings-list' ) ;
0 commit comments