@@ -45,7 +45,7 @@ let contextFiles = [];
4545/** @type {AbortController | null } */
4646let abort = null ;
4747/** Agent mode: sending runs the autonomous tool loop instead of plain chat. */
48- let agentMode = false ;
48+ let agentMode = true ;
4949/** Apply-then-review session (Keep/Undo for applied agent edits). Set in activate(). */
5050let review ;
5151/** Persistent agent transcript for the session (tool calls + results), so follow-up goals
@@ -56,6 +56,9 @@ function post(msg) { if (activeWebview) { activeWebview.postMessage(msg); } }
5656
5757function aiConfig ( ) { return vscode . workspace . getConfiguration ( 'atompp.ai' ) ; }
5858
59+ /** Inline debug trace — prints a 🐛 DEBUG line in the chat (gated by atompp.ai.debug). */
60+ function dbg ( label , data ) { if ( aiConfig ( ) . get ( 'debug' , true ) ) { post ( { type : 'debug' , label, data : data != null ? data : null } ) ; } }
61+
5962async function promptForKey ( ) {
6063 const key = await vscode . window . showInputBox ( {
6164 title : 'Atom++ AI — Anthropic API Key' ,
@@ -370,12 +373,16 @@ let approvalSeq = 0;
370373/** Ask the webview to approve an action; resolves true/false. */
371374function requestApproval ( req ) {
372375 const id = String ( ++ approvalSeq ) ;
376+ const wf = vscode . workspace . workspaceFolders ;
377+ const cwd = ( wf && wf [ 0 ] ) ? wf [ 0 ] . name : '~' ;
378+ dbg ( 'approval.request' , { id, kind : req . kind , cmd : req . command } ) ;
373379 return new Promise ( ( resolve ) => {
374380 pendingApprovals . set ( id , resolve ) ;
375- post ( Object . assign ( { type : 'agentApproval' , id } , req ) ) ;
381+ post ( Object . assign ( { type : 'agentApproval' , id, cwd } , req ) ) ;
376382 } ) ;
377383}
378384function resolveApproval ( id , approved ) {
385+ dbg ( 'approval.response' , { id, approved : ! ! approved } ) ;
379386 const r = pendingApprovals . get ( id ) ;
380387 if ( r ) { pendingApprovals . delete ( id ) ; r ( ! ! approved ) ; }
381388}
@@ -384,6 +391,29 @@ function clearApprovals() {
384391 pendingApprovals . clear ( ) ;
385392}
386393
394+ /** Pending in-chat clarifying-question prompts (ask_user), keyed by id, resolved by the webview. */
395+ const pendingQuestions = new Map ( ) ;
396+ let questionSeq = 0 ;
397+
398+ /** Ask the webview to present clickable multiple-choice questions; resolves with {answers, notes} or null. */
399+ function requestQuestions ( req ) {
400+ const id = String ( ++ questionSeq ) ;
401+ dbg ( 'ask.request' , { id, questions : ( req . questions || [ ] ) . length } ) ;
402+ return new Promise ( ( resolve ) => {
403+ pendingQuestions . set ( id , resolve ) ;
404+ post ( { type : 'agentQuestions' , id, questions : req . questions || [ ] } ) ;
405+ } ) ;
406+ }
407+ function resolveQuestions ( id , answers , notes ) {
408+ dbg ( 'ask.response' , { id, answered : Array . isArray ( answers ) ? answers . length : 0 , notes : ! ! notes } ) ;
409+ const r = pendingQuestions . get ( id ) ;
410+ if ( r ) { pendingQuestions . delete ( id ) ; r ( { answers : answers || [ ] , notes : notes || '' } ) ; }
411+ }
412+ function clearQuestions ( ) {
413+ for ( const [ , r ] of pendingQuestions ) { r ( null ) ; }
414+ pendingQuestions . clear ( ) ;
415+ }
416+
387417/** Heal any dangling tool_use (e.g. from a run the user stopped mid-tool) by inserting the
388418 * missing tool_result blocks, so the next API call doesn't 400. */
389419function repairAgentMemory ( ) {
@@ -412,7 +442,9 @@ function trimAgentMemory() {
412442 if ( cut > 0 && cut < agentMessages . length ) { agentMessages . splice ( 0 , cut ) ; }
413443}
414444
445+ let lastAgentGoal = null ; // remembered so the response bar's Retry can re-run it
415446async function agentFlow ( text ) {
447+ if ( text && text . trim ( ) ) { lastAgentGoal = text ; }
416448 const cfg = aiConfig ( ) ;
417449 if ( cfg . get ( 'provider' , 'claude' ) !== 'claude' ) {
418450 post ( { type : 'agentError' , message : 'Agent mode currently requires the Claude provider.' } ) ;
@@ -428,19 +460,23 @@ async function agentFlow(text) {
428460 repairAgentMemory ( ) ;
429461 agentMessages . push ( { role : 'user' , content : text } ) ;
430462 trimAgentMemory ( ) ;
463+ dbg ( 'agent.start' , { model : cfg . get ( 'claude.model' , 'claude-sonnet-4-6' ) , maxSteps : cfg . get ( 'agent.maxSteps' , 25 ) , transcriptMsgs : agentMessages . length , goalChars : text . length } ) ;
431464 try {
432465 await runAgent ( {
433466 messages : agentMessages , // persists across runs → the agent remembers the session
434467 apiKey : key ,
435468 model : cfg . get ( 'claude.model' , 'claude-sonnet-4-6' ) ,
436469 maxSteps : Math . max ( 1 , cfg . get ( 'agent.maxSteps' , 25 ) ) ,
437- post,
470+ post, dbg ,
438471 approve : requestApproval , // run_command only
472+ ask : requestQuestions , // ask_user — clickable clarifying questions
473+ contextLimit : cfg . get ( 'contextWindow' , 200000 ) , // model context window → drives the usage meter
439474 applyEdit : ( req ) => review . applyEdit ( req ) , // file edits: apply-then-review
440475 signal : abort . signal
441476 } ) ;
442477 } finally {
443478 clearApprovals ( ) ;
479+ clearQuestions ( ) ;
444480 abort = null ;
445481 }
446482}
@@ -450,6 +486,7 @@ async function handleSend(text) {
450486 if ( agentMode ) { await agentFlow ( text ) ; return ; }
451487 const cfg = aiConfig ( ) ;
452488 const provider = cfg . get ( 'provider' , 'claude' ) ;
489+ dbg ( 'chat.send' , { provider, model : provider === 'claude' ? cfg . get ( 'claude.model' ) : cfg . get ( 'ollama.model' ) , chars : text . length , history : conversation . length } ) ;
453490
454491 const blocks = [ ] ;
455492
@@ -569,13 +606,21 @@ class ChatViewProvider {
569606 view . webview . html = getHtml ( ) ;
570607 view . webview . onDidReceiveMessage ( async ( msg ) => {
571608 switch ( msg . type ) {
572- case 'ready' : sendConfigToWebview ( ) ; postActiveFile ( ) ; postContextFiles ( ) ; post ( { type : 'mode' , agent : agentMode } ) ; break ;
609+ case 'ready' : sendConfigToWebview ( ) ; postActiveFile ( ) ; postContextFiles ( ) ; post ( { type : 'mode' , agent : agentMode } ) ; postAccount ( ) ; buildFileIndex ( ) ; post ( { type : 'contextUsage' , input : 0 , limit : aiConfig ( ) . get ( 'contextWindow' , 200000 ) } ) ; if ( review ) { review . resync ( ) ; } break ;
573610 case 'setMode' : agentMode = ! ! msg . agent ; post ( { type : 'mode' , agent : agentMode } ) ; break ;
574611 case 'send' : await handleSend ( msg . text ) ; break ;
575- case 'stop' : if ( abort ) { abort . abort ( ) ; } clearApprovals ( ) ; break ;
612+ case 'stop' : dbg ( 'stop.clicked' ) ; if ( abort ) { abort . abort ( ) ; } clearApprovals ( ) ; clearQuestions ( ) ; break ;
576613 case 'approvalResponse' : resolveApproval ( msg . id , msg . approved ) ; break ;
577- case 'reviewKeepFile' : review . keepFile ( msg . id , 'kept' ) ; break ;
578- case 'reviewUndoFile' : await review . undoFile ( msg . id ) ; break ;
614+ case 'questionsResponse' : resolveQuestions ( msg . id , msg . answers , msg . notes ) ; break ;
615+ case 'accountSignIn' : await accountSignIn ( msg . provider , msg . create ) ; break ;
616+ case 'accountSignOut' : await accountSignOut ( ) ; break ;
617+ case 'accountManage' : await accountManage ( ) ; break ;
618+ case 'copy' : try { await vscode . env . clipboard . writeText ( String ( msg . text || '' ) ) ; } catch ( e ) { /* clipboard unavailable */ } break ;
619+ case 'retry' : if ( lastAgentGoal && ! abort ) { dbg ( 'retry' , { goalChars : lastAgentGoal . length } ) ; await agentFlow ( lastAgentGoal ) ; } break ;
620+ case 'feedback' : dbg ( 'feedback' , { value : msg . value } ) ; break ;
621+ case 'openFile' : await openWorkspaceFile ( msg . path ) ; break ;
622+ case 'reviewKeepFile' : dbg ( 'review.keep' , { id : msg . id } ) ; review . keepFile ( msg . id , 'kept' ) ; break ;
623+ case 'reviewUndoFile' : dbg ( 'review.undo' , { id : msg . id } ) ; await review . undoFile ( msg . id ) ; break ;
579624 case 'reviewKeepAll' : review . keepAll ( ) ; break ;
580625 case 'reviewUndoAll' : await review . undoAll ( ) ; break ;
581626 case 'reviewOpenDiff' : await review . openDiff ( msg . id ) ; break ;
@@ -602,6 +647,100 @@ function getHtml() {
602647 return html . replace ( / _ _ C S P _ _ / g, csp ) . replace ( / _ _ N O N C E _ _ / g, nonce ) ;
603648}
604649
650+ // ---- Atom++ Cloud account (sign-in + sync) -------------------------------------------------
651+ // IMPORTANT: this is a SEPARATE, optional layer. The AI path stays BYO-key / no-backend — the
652+ // account only syncs settings/skills/profile, never proxies AI or sees your provider keys.
653+ // It is gated on a configured `atompp.cloud.endpoint`; with none set, sign-in is honestly inert.
654+ const ACCOUNT_TOKEN_KEY = 'atompp.cloud.token' ; // session token → SecretStorage
655+ const ACCOUNT_PROFILE_KEY = 'atompp.cloud.profile' ; // {name,email,plan} → globalState
656+ function cloudEndpoint ( ) { return String ( vscode . workspace . getConfiguration ( 'atompp.cloud' ) . get ( 'endpoint' , '' ) || '' ) . replace ( / \/ + $ / , '' ) ; }
657+ async function currentAccount ( ) {
658+ const token = ctx ? await ctx . secrets . get ( ACCOUNT_TOKEN_KEY ) : null ;
659+ if ( token ) {
660+ const p = ( ctx && ctx . globalState . get ( ACCOUNT_PROFILE_KEY ) ) || { } ;
661+ return { signedIn : true , name : p . name || p . email || 'Atom++ user' , email : p . email || '' , plan : p . plan || '' } ;
662+ }
663+ return { signedIn : false , status : cloudEndpoint ( ) ? 'signedout' : 'unconfigured' } ;
664+ }
665+ async function postAccount ( open ) {
666+ const a = await currentAccount ( ) ;
667+ post ( Object . assign ( { type : 'account' , open : ! ! open } , a ) ) ;
668+ }
669+ async function accountSignIn ( provider , create ) {
670+ const endpoint = cloudEndpoint ( ) ;
671+ if ( ! endpoint ) {
672+ dbg ( 'account.signin' , { unconfigured : true } ) ;
673+ await postAccount ( true ) ;
674+ vscode . window . showInformationMessage ( 'Atom++ Cloud isn’t connected yet. Set "atompp.cloud.endpoint" to your account server to sign in. (Accounts + sync are on the roadmap — M9.)' ) ;
675+ return ;
676+ }
677+ // Canonical editor OAuth callback: open the server's auth page with a redirect back to us.
678+ const cb = await vscode . env . asExternalUri ( vscode . Uri . parse ( `${ vscode . env . uriScheme } ://atompp.atom-ai/auth/callback` ) ) ;
679+ const path = create ? 'signup' : ( provider ? 'oauth/' + encodeURIComponent ( provider ) : 'login' ) ;
680+ const url = `${ endpoint } /auth/${ path } ?redirect_uri=${ encodeURIComponent ( cb . toString ( ) ) } ` ;
681+ dbg ( 'account.signin' , { provider : provider || 'browser' , create : ! ! create } ) ;
682+ await vscode . env . openExternal ( vscode . Uri . parse ( url ) ) ;
683+ }
684+ async function accountSignOut ( ) {
685+ if ( ctx ) { await ctx . secrets . delete ( ACCOUNT_TOKEN_KEY ) ; await ctx . globalState . update ( ACCOUNT_PROFILE_KEY , undefined ) ; }
686+ dbg ( 'account.signout' ) ;
687+ await postAccount ( ) ;
688+ }
689+ async function accountManage ( ) {
690+ const endpoint = cloudEndpoint ( ) ;
691+ if ( endpoint ) { await vscode . env . openExternal ( vscode . Uri . parse ( endpoint + '/account' ) ) ; }
692+ else { await postAccount ( true ) ; }
693+ }
694+ // The browser redirects to atom-plus-plus://atompp.atom-ai/auth/callback?token=…&name=…&email=…
695+ async function handleAuthCallback ( uri ) {
696+ try {
697+ const q = new URLSearchParams ( uri . query || '' ) ;
698+ const token = q . get ( 'token' ) ;
699+ if ( ! token ) { return ; }
700+ if ( ctx ) {
701+ await ctx . secrets . store ( ACCOUNT_TOKEN_KEY , token ) ;
702+ await ctx . globalState . update ( ACCOUNT_PROFILE_KEY , { name : q . get ( 'name' ) || '' , email : q . get ( 'email' ) || '' , plan : q . get ( 'plan' ) || '' } ) ;
703+ }
704+ dbg ( 'account.callback' , { ok : true } ) ;
705+ await postAccount ( true ) ;
706+ vscode . window . showInformationMessage ( 'Signed in to Atom++.' ) ;
707+ } catch ( e ) { dbg ( 'account.callback' , { error : String ( ( e && e . message ) || e ) } ) ; }
708+ }
709+
710+ // ---- Workspace file index (powers clickable file chips in chat) -------------------------------
711+ let workspaceFiles = new Map ( ) ; // relPath -> vscode.Uri
712+ let fileIndexTimer = null ;
713+ async function buildFileIndex ( ) {
714+ try {
715+ const map = new Map ( ) ;
716+ const exclude = '{**/node_modules/**,**/.git/**,**/out/**,**/dist/**,**/build/**,**/.DS_Store}' ;
717+ const uris = await vscode . workspace . findFiles ( '**/*' , exclude , 8000 ) ;
718+ const rels = [ ] ;
719+ for ( const u of uris ) { const rel = vscode . workspace . asRelativePath ( u , false ) ; map . set ( rel , u ) ; rels . push ( rel ) ; }
720+ workspaceFiles = map ;
721+ post ( { type : 'fileIndex' , files : rels } ) ;
722+ dbg ( 'fileIndex' , { count : rels . length } ) ;
723+ } catch ( e ) { /* ignore index build errors */ }
724+ }
725+ function scheduleFileIndex ( ) { if ( fileIndexTimer ) { clearTimeout ( fileIndexTimer ) ; } fileIndexTimer = setTimeout ( buildFileIndex , 400 ) ; }
726+ async function openWorkspaceFile ( rel ) {
727+ if ( ! rel ) { return ; }
728+ let uri = workspaceFiles . get ( rel ) ;
729+ if ( ! uri ) {
730+ for ( const f of ( vscode . workspace . workspaceFolders || [ ] ) ) {
731+ const cand = vscode . Uri . joinPath ( f . uri , rel ) ;
732+ try { await vscode . workspace . fs . stat ( cand ) ; uri = cand ; break ; } catch ( e ) { /* not here */ }
733+ }
734+ if ( ! uri ) { const found = await vscode . workspace . findFiles ( '**/' + rel . split ( '/' ) . pop ( ) , '**/node_modules/**' , 1 ) ; if ( found . length ) { uri = found [ 0 ] ; } }
735+ }
736+ if ( ! uri ) { vscode . window . showWarningMessage ( 'Atom++: could not locate ' + rel ) ; return ; }
737+ try {
738+ await vscode . commands . executeCommand ( 'revealInExplorer' , uri ) ; // reveal in the (nested) Explorer tree
739+ await vscode . window . showTextDocument ( uri , { preview : false } ) ; // open in the main editor
740+ dbg ( 'openFile' , { rel : rel } ) ;
741+ } catch ( e ) { dbg ( 'openFile.error' , { msg : String ( ( e && e . message ) || e ) } ) ; }
742+ }
743+
605744function activate ( context ) {
606745 ctx = context ;
607746 context . subscriptions . push (
@@ -619,8 +758,15 @@ function activate(context) {
619758 await context . secrets . delete ( SECRET_KEY ) ;
620759 vscode . window . showInformationMessage ( 'Atom++ AI: Anthropic API key cleared.' ) ;
621760 } ) ,
761+ vscode . commands . registerCommand ( 'atompp.ai.account' , ( ) => postAccount ( true ) ) ,
762+ vscode . window . registerUriHandler ( { handleUri ( uri ) { if ( uri . path === '/auth/callback' ) { handleAuthCallback ( uri ) ; } } } ) ,
763+ vscode . workspace . onDidCreateFiles ( scheduleFileIndex ) ,
764+ vscode . workspace . onDidDeleteFiles ( scheduleFileIndex ) ,
765+ vscode . workspace . onDidRenameFiles ( scheduleFileIndex ) ,
766+ vscode . workspace . onDidChangeWorkspaceFolders ( scheduleFileIndex ) ,
622767 vscode . workspace . onDidChangeConfiguration ( ( e ) => {
623768 if ( e . affectsConfiguration ( 'atompp.ai' ) ) { sendConfigToWebview ( ) ; }
769+ if ( e . affectsConfiguration ( 'atompp.cloud' ) ) { postAccount ( ) ; }
624770 } )
625771 ) ;
626772
@@ -644,7 +790,7 @@ function activate(context) {
644790 } ) ;
645791
646792 // Apply-then-review: agent edits land immediately and are reviewed with Keep/Undo (editor + panel).
647- review = registerReview ( context , post ) ;
793+ review = registerReview ( context , post , dbg ) ;
648794
649795 // Inline (ghost-text) tab-completion. Silent key lookup — it must never prompt mid-typing.
650796 registerInlineComplete ( context , {
0 commit comments