1+ /**
2+ * Thread History Management
3+ *
4+ * Functions for scanning thread history and restoring conversation context
5+ * based on SQL query matching.
6+ */
7+
8+ import { ChatThread , ChatMessage , Action , startNewThread , cloneThreadFromHistory } from '../state/chat/reducer' ;
9+ import { applySQLEdits , SQLEdits , getCurrentQuery } from 'apps' ;
10+ import { dispatch } from '../state/dispatch' ;
11+ import { RootState } from '../state/store' ;
12+ import { queryURL } from '../app/rpc' ;
13+
14+ /**
15+ * Normalizes SQL for comparison by removing extra whitespace,
16+ * converting to lowercase, and standardizing formatting
17+ */
18+ export function normalizeSQL ( sql : string ) : string {
19+ if ( ! sql || typeof sql !== 'string' ) {
20+ return '' ;
21+ }
22+
23+ return sql
24+ . trim ( )
25+ . toLowerCase ( )
26+ . replace ( / \s + / g, ' ' ) // Replace multiple spaces with single space
27+ . replace ( / \( \s + / g, '(' ) // Remove spaces after opening parentheses
28+ . replace ( / \s + \) / g, ')' ) // Remove spaces before closing parentheses
29+ . replace ( / , \s + / g, ',' ) // Normalize comma spacing
30+ . replace ( / ; \s * $ / , '' ) ; // Remove trailing semicolon
31+ }
32+
33+ /**
34+ * Extracts SQL from tool call arguments, handling both ExecuteQuery and EditAndExecuteQuery
35+ */
36+ function extractSQLFromAction ( action : Action ) : string | null {
37+ try {
38+ const functionName = action . function . name ;
39+ const args = JSON . parse ( action . function . arguments ) ;
40+
41+ if ( functionName === 'ExecuteQuery' ) {
42+ return args . sql || null ;
43+ }
44+
45+ if ( functionName === 'EditAndExecuteQuery' ) {
46+ // For EditAndExecuteQuery, we need to reconstruct the final SQL
47+ // This is a simplified approach - in reality we'd need access to the base SQL
48+ const sql_edits = args . sql_edits as SQLEdits ;
49+ // Note: We can't reconstruct without the original SQL, so return null for now
50+ // This could be enhanced to store the reconstructed SQL in the action results
51+ return null ;
52+ }
53+
54+ return null ;
55+ } catch ( error ) {
56+ console . warn ( 'Error extracting SQL from action:' , error ) ;
57+ return null ;
58+ }
59+ }
60+
61+ /**
62+ * Result of scanning threads for matching SQL
63+ */
64+ export interface ThreadScanResult {
65+ threadIndex : number ;
66+ messageIndex : number ;
67+ matchingSQL : string ;
68+ }
69+
70+ /**
71+ * Scans thread history for a matching SQL query
72+ * Returns the first match found (most recent threads searched first)
73+ */
74+ export function scanThreadsForSQL (
75+ threads : ChatThread [ ] ,
76+ currentSQL : string
77+ ) : ThreadScanResult | null {
78+ if ( ! currentSQL || ! threads || threads . length === 0 ) {
79+ return null ;
80+ }
81+
82+ const normalizedCurrentSQL = normalizeSQL ( currentSQL ) ;
83+ if ( ! normalizedCurrentSQL ) {
84+ return null ;
85+ }
86+
87+ // Scan threads backwards (most recent first)
88+ for ( let threadIndex = threads . length - 1 ; threadIndex >= 0 ; threadIndex -- ) {
89+ const thread = threads [ threadIndex ] ;
90+ if ( ! thread . messages ) continue ;
91+
92+ // Scan messages backwards within each thread
93+ for ( let messageIndex = thread . messages . length - 1 ; messageIndex >= 0 ; messageIndex -- ) {
94+ const message = thread . messages [ messageIndex ] ;
95+
96+ // Only check tool messages with ExecuteQuery or EditAndExecuteQuery actions
97+ if ( message . role === 'tool' && message . action ) {
98+ const extractedSQL = extractSQLFromAction ( message . action ) ;
99+ if ( extractedSQL ) {
100+ const normalizedExtractedSQL = normalizeSQL ( extractedSQL ) ;
101+ if ( normalizedExtractedSQL === normalizedCurrentSQL ) {
102+ return {
103+ threadIndex,
104+ messageIndex,
105+ matchingSQL : extractedSQL
106+ } ;
107+ }
108+ }
109+ }
110+ }
111+ }
112+
113+ return null ;
114+ }
115+
116+
117+ /**
118+ * Intelligent thread start function that checks for matching SQL in history
119+ * and restores context if found, otherwise starts a new thread
120+ */
121+ export async function intelligentThreadStart ( getState : ( ) => RootState ) : Promise < {
122+ restored : boolean ;
123+ matchingSQL ?: string ;
124+ } > {
125+ try {
126+ // Get current SQL from the page
127+ const currentURL = await queryURL ( )
128+ let currentSQL = ''
129+ try {
130+ const url = new URL ( currentURL ) ;
131+ const hash = url . hash ;
132+ currentSQL = JSON . parse ( atob ( decodeURIComponent ( hash . slice ( 1 ) ) ) ) . dataset_query . native . query ;
133+ } catch {
134+ console . warn ( 'Failed to extract SQL from URL hash, using getCurrentQuery' ) ;
135+ }
136+ if ( ! currentSQL ) {
137+ // No SQL on page, start new thread normally
138+ dispatch ( startNewThread ( ) ) ;
139+ return { restored : false } ;
140+ }
141+
142+ // Get current thread state
143+ const state = getState ( ) ;
144+ const threads = state . chat . threads ;
145+
146+ if ( ! threads || threads . length === 0 ) {
147+ // No threads to search, start new thread
148+ dispatch ( startNewThread ( ) ) ;
149+ return { restored : false } ;
150+ }
151+
152+ // Scan for matching SQL in thread history
153+ const matchResult = scanThreadsForSQL ( threads , currentSQL ) ;
154+
155+ if ( matchResult ) {
156+ // Found a match! Clone the thread up to that message
157+ dispatch ( cloneThreadFromHistory ( {
158+ sourceThreadIndex : matchResult . threadIndex ,
159+ upToMessageIndex : matchResult . messageIndex
160+ } ) ) ;
161+
162+ return {
163+ restored : true ,
164+ matchingSQL : matchResult . matchingSQL
165+ } ;
166+ } else {
167+ // No match found, start new thread
168+ dispatch ( startNewThread ( ) ) ;
169+ return { restored : false } ;
170+ }
171+
172+ } catch ( error ) {
173+ console . error ( 'Error in intelligentThreadStart:' , error ) ;
174+ // Fallback to normal thread start on any error
175+ dispatch ( startNewThread ( ) ) ;
176+ return { restored : false } ;
177+ }
178+ }
0 commit comments