1111import type { ExtensionAPI , ExtensionCommandContext } from "@mariozechner/pi-coding-agent" ;
1212
1313function getLastAssistantText ( ctx : ExtensionCommandContext ) : string | null {
14- const branch = ctx . sessionManager . getBranch ( ) ;
15- for ( let i = branch . length - 1 ; i >= 0 ; i -- ) {
16- const entry = branch [ i ] ;
17- if ( entry . type !== "message" ) continue ;
18- const msg = entry . message ;
19- if ( ! ( "role" in msg ) || msg . role !== "assistant" ) continue ;
20- const textParts = msg . content
21- . filter ( ( c ) : c is { type : "text" ; text : string } => c . type === "text" )
22- . map ( ( c ) => c . text ) ;
23- if ( textParts . length > 0 ) return textParts . join ( "\n" ) ;
24- }
25- return null ;
14+ const branch = ctx . sessionManager . getBranch ( ) ;
15+ for ( let i = branch . length - 1 ; i >= 0 ; i -- ) {
16+ const entry = branch [ i ] ;
17+ if ( entry . type !== "message" ) continue ;
18+ const msg = entry . message ;
19+ if ( ! ( "role" in msg ) || msg . role !== "assistant" ) continue ;
20+ const textParts = msg . content
21+ . filter ( ( c ) : c is { type : "text" ; text : string } => c . type === "text" )
22+ . map ( ( c ) => c . text ) ;
23+ if ( textParts . length > 0 ) return textParts . join ( "\n" ) ;
24+ }
25+ return null ;
2626}
2727
2828/**
@@ -33,98 +33,98 @@ function getLastAssistantText(ctx: ExtensionCommandContext): string | null {
3333 * common markdown emphasis so the prompt reads cleanly.
3434 */
3535function extractQuestions ( text : string ) : string [ ] {
36- const questions : string [ ] = [ ] ;
37- const seen = new Set < string > ( ) ;
38-
39- // Split into "sentences" by terminal punctuation while keeping the '?' tokens.
40- // Regex captures text up to and including ? . or ! (or end-of-string).
41- const sentenceRe = / [ ^ . ! ? \n ] * \? / g;
42- let match : RegExpExecArray | null ;
43- while ( ( match = sentenceRe . exec ( text ) ) !== null ) {
44- let q = match [ 0 ] . trim ( ) ;
45- if ( ! q . endsWith ( "?" ) ) continue ;
46-
47- // Strip leading list/number markers: "- ", "* ", "1. ", "1) ", "**1.** "
48- q = q . replace ( / ^ [ - * + • ] \s + / , "" ) ;
49- q = q . replace ( / ^ \* + \s * / , "" ) ;
50- q = q . replace ( / ^ \d + [ . ) ] \s + / , "" ) ;
51- q = q . replace ( / ^ \* \* [ ^ * ] + \* \* \s * [: \- ] ? \s * / , "" ) ;
52-
53- // Strip surrounding markdown emphasis
54- q = q . replace ( / \* \* / g, "" ) . replace ( / _ _ / g, "" ) . replace ( / ` / g, "" ) ;
55-
56- // Collapse whitespace
57- q = q . replace ( / \s + / g, " " ) . trim ( ) ;
58-
59- if ( q . length < 3 ) continue ;
60- if ( seen . has ( q ) ) continue ;
61- seen . add ( q ) ;
62- questions . push ( q ) ;
63- }
64-
65- return questions ;
36+ const questions : string [ ] = [ ] ;
37+ const seen = new Set < string > ( ) ;
38+
39+ // Split into "sentences" by terminal punctuation while keeping the '?' tokens.
40+ // Regex captures text up to and including ? . or ! (or end-of-string).
41+ const sentenceRe = / [ ^ . ! ? \n ] * \? / g;
42+ let match : RegExpExecArray | null ;
43+ while ( ( match = sentenceRe . exec ( text ) ) !== null ) {
44+ let q = match [ 0 ] . trim ( ) ;
45+ if ( ! q . endsWith ( "?" ) ) continue ;
46+
47+ // Strip leading list/number markers: "- ", "* ", "1. ", "1) ", "**1.** "
48+ q = q . replace ( / ^ [ - * + • ] \s + / , "" ) ;
49+ q = q . replace ( / ^ \* + \s * / , "" ) ;
50+ q = q . replace ( / ^ \d + [ . ) ] \s + / , "" ) ;
51+ q = q . replace ( / ^ \* \* [ ^ * ] + \* \* \s * [: \- ] ? \s * / , "" ) ;
52+
53+ // Strip surrounding markdown emphasis
54+ q = q . replace ( / \* \* / g, "" ) . replace ( / _ _ / g, "" ) . replace ( / ` / g, "" ) ;
55+
56+ // Collapse whitespace
57+ q = q . replace ( / \s + / g, " " ) . trim ( ) ;
58+
59+ if ( q . length < 3 ) continue ;
60+ if ( seen . has ( q ) ) continue ;
61+ seen . add ( q ) ;
62+ questions . push ( q ) ;
63+ }
64+
65+ return questions ;
6666}
6767
6868function formatQA ( pairs : Array < { question : string ; answer : string } > ) : string {
69- const lines : string [ ] = [
70- "Here are my answers to the questions you asked:" ,
71- "" ,
72- ] ;
73- pairs . forEach ( ( p , i ) => {
74- lines . push ( `${ i + 1 } . ${ p . question } ` ) ;
75- lines . push ( ` ${ p . answer } ` ) ;
76- lines . push ( "" ) ;
77- } ) ;
78- lines . push ( "Please continue based on these answers." ) ;
79- return lines . join ( "\n" ) ;
69+ const lines : string [ ] = [
70+ "Here are my answers to the questions you asked:" ,
71+ "" ,
72+ ] ;
73+ pairs . forEach ( ( p , i ) => {
74+ lines . push ( `${ i + 1 } . ${ p . question } ` ) ;
75+ lines . push ( ` ${ p . answer } ` ) ;
76+ lines . push ( "" ) ;
77+ } ) ;
78+ lines . push ( "Please continue based on these answers." ) ;
79+ return lines . join ( "\n" ) ;
8080}
8181
8282export default function ( pi : ExtensionAPI ) {
83- pi . registerCommand ( "answer" , {
84- description : "Extract questions from the last AI response and answer them interactively" ,
85- handler : async ( _args , ctx ) => {
86- if ( ! ctx . hasUI ) {
87- ctx . ui . notify ( "/answer requires interactive mode" , "error" ) ;
88- return ;
89- }
90-
91- const lastText = getLastAssistantText ( ctx ) ;
92- if ( ! lastText ) {
93- ctx . ui . notify ( "No previous assistant message found" , "error" ) ;
94- return ;
95- }
96-
97- const questions = extractQuestions ( lastText ) ;
98- if ( questions . length === 0 ) {
99- ctx . ui . notify ( "No questions found in the last AI response" , "warning" ) ;
100- return ;
101- }
102-
103- ctx . ui . notify ( `Found ${ questions . length } question${ questions . length === 1 ? "" : "s" } ` , "info" ) ;
104-
105- const pairs : Array < { question : string ; answer : string } > = [ ] ;
106- for ( let i = 0 ; i < questions . length ; i ++ ) {
107- const q = questions [ i ] ;
108- const title = `Question ${ i + 1 } /${ questions . length } : ${ q } ` ;
109- const answer = await ctx . ui . input ( title , "Type your answer..." ) ;
110-
111- if ( answer === null || answer === undefined ) {
112- ctx . ui . notify ( "Cancelled" , "info" ) ;
113- return ;
114- }
115-
116- pairs . push ( { question : q , answer : answer . trim ( ) || "(no answer)" } ) ;
117- }
118-
119- const reply = formatQA ( pairs ) ;
120-
121- if ( ctx . isIdle ( ) ) {
122- pi . sendUserMessage ( reply ) ;
123- } else {
124- pi . sendUserMessage ( reply , { deliverAs : "followUp" } ) ;
125- }
126-
127- ctx . ui . notify ( "Answers sent to AI" , "info" ) ;
128- } ,
129- } ) ;
83+ pi . registerCommand ( "answer" , {
84+ description : "Extract questions from the last AI response and answer them interactively" ,
85+ handler : async ( _args , ctx ) => {
86+ if ( ! ctx . hasUI ) {
87+ ctx . ui . notify ( "/answer requires interactive mode" , "error" ) ;
88+ return ;
89+ }
90+
91+ const lastText = getLastAssistantText ( ctx ) ;
92+ if ( ! lastText ) {
93+ ctx . ui . notify ( "No previous assistant message found" , "error" ) ;
94+ return ;
95+ }
96+
97+ const questions = extractQuestions ( lastText ) ;
98+ if ( questions . length === 0 ) {
99+ ctx . ui . notify ( "No questions found in the last AI response" , "warning" ) ;
100+ return ;
101+ }
102+
103+ ctx . ui . notify ( `Found ${ questions . length } question${ questions . length === 1 ? "" : "s" } ` , "info" ) ;
104+
105+ const pairs : Array < { question : string ; answer : string } > = [ ] ;
106+ for ( let i = 0 ; i < questions . length ; i ++ ) {
107+ const q = questions [ i ] ;
108+ const title = `Question ${ i + 1 } /${ questions . length } : ${ q } ` ;
109+ const answer = await ctx . ui . input ( title , "Type your answer..." ) ;
110+
111+ if ( answer === null || answer === undefined ) {
112+ ctx . ui . notify ( "Cancelled" , "info" ) ;
113+ return ;
114+ }
115+
116+ pairs . push ( { question : q , answer : answer . trim ( ) || "(no answer)" } ) ;
117+ }
118+
119+ const reply = formatQA ( pairs ) ;
120+
121+ if ( ctx . isIdle ( ) ) {
122+ pi . sendUserMessage ( reply ) ;
123+ } else {
124+ pi . sendUserMessage ( reply , { deliverAs : "followUp" } ) ;
125+ }
126+
127+ ctx . ui . notify ( "Answers sent to AI" , "info" ) ;
128+ } ,
129+ } ) ;
130130}
0 commit comments