@@ -92,6 +92,7 @@ const PI_CATALOG_REQUEST_TIMEOUT_MS = 120_000;
9292const PASEO_PI_TREE_EXTENSION_COMMAND = "paseo_tree" ;
9393const PASEO_PI_CAPTURE_EXTENSION_COMMAND = "paseo_capture_entries" ;
9494const PASEO_PI_ENTRY_CAPTURE_MARKER = "PASEO_ENTRY_CAPTURE" ;
95+ const PASEO_PI_SUBMITTED_USER_ENTRY_MARKER = "PASEO_SUBMITTED_USER_ENTRY" ;
9596const PASEO_PI_COMMAND_RESULT_MARKER = "PASEO_COMMAND_RESULT" ;
9697const DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS = 30_000 ;
9798const QUESTION_RESPONSE_HEADER = "Response" ;
@@ -256,12 +257,6 @@ interface PiCapturedEntry extends PiCapturedUserMessageEntry {
256257 parentId : string | null ;
257258}
258259
259- interface PendingPiUserMessage {
260- text : string ;
261- turnId : string | undefined ;
262- clientMessageId ?: string ;
263- }
264-
265260interface PendingExtensionResult {
266261 resolve : ( value : unknown ) => void ;
267262 reject : ( error : Error ) => void ;
@@ -653,11 +648,15 @@ function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile {
653648 return ctx.sessionManager
654649 .getEntries()
655650 .filter((entry) => entry.type === "message" && entry.message?.role === "user")
656- .map((entry) => ({
651+ .map(toCapturedUserEntry);
652+ }
653+
654+ function toCapturedUserEntry(entry) {
655+ return {
657656 id: entry.id,
658657 parentId: entry.parentId ?? null,
659658 text: readTextContent(entry.message.content),
660- })) ;
659+ };
661660 }
662661
663662 function emitEntryCapture(ctx, reason, requestId) {
@@ -676,6 +675,30 @@ function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile {
676675 }
677676
678677 export default function paseoIntegration(pi) {
678+ const submittedUserMessages = [];
679+
680+ function emitSubmittedUserEntries(ctx) {
681+ const entries = ctx.sessionManager.getEntries();
682+ for (let index = 0; index < submittedUserMessages.length; index += 1) {
683+ const message = submittedUserMessages[index];
684+ // Pi assigns the entry ID after message_end, then persists this same message object.
685+ // Reference equality preserves the exact association even when another extension edits it.
686+ const entry = entries.find(
687+ (candidate) => candidate.type === "message" && candidate.message === message,
688+ );
689+ if (!entry) {
690+ continue;
691+ }
692+ submittedUserMessages.splice(index, 1);
693+ index -= 1;
694+ ctx.ui.notify(
695+ "${ PASEO_PI_SUBMITTED_USER_ENTRY_MARKER } " +
696+ JSON.stringify({ entry: toCapturedUserEntry(entry) }),
697+ "info",
698+ );
699+ }
700+ }
701+
679702 ${
680703 systemPrompt
681704 ? `pi.on("before_agent_start", async (event) => ({
@@ -688,7 +711,20 @@ function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile {
688711 emitEntryCapture(ctx, "session_start");
689712 });
690713
714+ pi.on("message_end", async (event) => {
715+ if (event.message?.role === "user") {
716+ submittedUserMessages.push(event.message);
717+ }
718+ });
719+
720+ pi.on("message_start", async (event, ctx) => {
721+ if (event.message?.role === "assistant") {
722+ emitSubmittedUserEntries(ctx);
723+ }
724+ });
725+
691726 pi.on("turn_end", async (_event, ctx) => {
727+ emitSubmittedUserEntries(ctx);
692728 emitEntryCapture(ctx, "turn_end");
693729 });
694730
@@ -1197,8 +1233,6 @@ export class PiRpcAgentSession implements AgentSession {
11971233 currentLeafOverrideId : string | null | undefined ;
11981234 private readonly capturedUserEntries : PiCapturedEntry [ ] = [ ] ;
11991235 private readonly capturedUserEntriesById = new Map < string , PiCapturedEntry > ( ) ;
1200- private readonly seenUserEntryIds = new Set < string > ( ) ;
1201- private readonly pendingUserMessages : PendingPiUserMessage [ ] = [ ] ;
12021236 private readonly pendingExtensionResults = new Map < string , PendingExtensionResult > ( ) ;
12031237 private outOfBandCompactionEmit : ( ( event : AgentStreamEvent ) => void ) | null = null ;
12041238 private outOfBandCompactionStarted = false ;
@@ -1776,42 +1810,34 @@ export class PiRpcAgentSession implements AgentSession {
17761810 }
17771811
17781812 private recordCapturedUserEntries ( entries : PiCapturedEntry [ ] ) : void {
1779- const previouslySeenEntryIds = new Set ( this . seenUserEntryIds ) ;
17801813 this . capturedUserEntries . splice ( 0 , this . capturedUserEntries . length , ...entries ) ;
17811814 this . capturedUserEntriesById . clear ( ) ;
17821815 for ( const entry of entries ) {
17831816 this . capturedUserEntriesById . set ( entry . id , entry ) ;
17841817 }
1785- this . flushPendingUserMessages ( previouslySeenEntryIds ) ;
1786- for ( const entry of entries ) {
1787- this . seenUserEntryIds . add ( entry . id ) ;
1788- }
17891818 }
17901819
1791- private flushPendingUserMessages ( previouslySeenEntryIds : Set < string > ) : void {
1792- for ( let index = 0 ; index < this . pendingUserMessages . length ; index += 1 ) {
1793- const pending = this . pendingUserMessages [ index ] ! ;
1794- const entry = this . capturedUserEntries . find (
1795- ( candidate ) => ! previouslySeenEntryIds . has ( candidate . id ) ,
1796- ) ;
1797- if ( ! entry ) {
1798- continue ;
1799- }
1800- previouslySeenEntryIds . add ( entry . id ) ;
1801- this . pendingUserMessages . splice ( index , 1 ) ;
1802- index -= 1 ;
1803- this . emit ( {
1804- type : "timeline" ,
1805- provider : this . provider ,
1806- turnId : pending . turnId ,
1807- item : {
1808- type : "user_message" ,
1809- text : pending . text ,
1810- messageId : entry . id ,
1811- ...( pending . clientMessageId ? { clientMessageId : pending . clientMessageId } : { } ) ,
1812- } ,
1813- } ) ;
1820+ private handleSubmittedUserEntryMarker ( message : string ) : boolean {
1821+ const payload = parseExtensionMarkerPayload ( message , PASEO_PI_SUBMITTED_USER_ENTRY_MARKER ) ;
1822+ if ( ! payload ) {
1823+ return false ;
1824+ }
1825+ const [ entry ] = parseCapturedEntries ( [ payload . entry ] ) ;
1826+ if ( ! entry ) {
1827+ return true ;
18141828 }
1829+ this . emit ( {
1830+ type : "timeline" ,
1831+ provider : this . provider ,
1832+ turnId : this . currentTurnIdForEvent ( ) ,
1833+ item : {
1834+ type : "user_message" ,
1835+ text : entry . text ,
1836+ messageId : entry . id ,
1837+ ...( this . activeClientMessageId ? { clientMessageId : this . activeClientMessageId } : { } ) ,
1838+ } ,
1839+ } ) ;
1840+ return true ;
18151841 }
18161842
18171843 private handleEntryCaptureMarker ( message : string ) : boolean {
@@ -1849,7 +1875,11 @@ export class PiRpcAgentSession implements AgentSession {
18491875 ) : void {
18501876 const message = optionalString ( event . message ) ;
18511877 if ( event . method === "notify" && message ) {
1852- if ( this . handleEntryCaptureMarker ( message ) || this . handleCommandResultMarker ( message ) ) {
1878+ if (
1879+ this . handleSubmittedUserEntryMarker ( message ) ||
1880+ this . handleEntryCaptureMarker ( message ) ||
1881+ this . handleCommandResultMarker ( message )
1882+ ) {
18531883 return ;
18541884 }
18551885 this . bufferNoTurnOutput ( message ) ;
@@ -2176,28 +2206,6 @@ export class PiRpcAgentSession implements AgentSession {
21762206 this . completeTurn ( turnId , [ ] ) ;
21772207 return ;
21782208 }
2179-
2180- if ( event . message . role !== "user" ) {
2181- return ;
2182- }
2183- const text = getUserMessageText ( event . message . content ) ;
2184- if ( ! text ) {
2185- return ;
2186- }
2187- this . pendingUserMessages . push ( {
2188- text,
2189- turnId,
2190- ...( this . activeClientMessageId ? { clientMessageId : this . activeClientMessageId } : { } ) ,
2191- } ) ;
2192- void this . requestEntryCapture ( "message_end" ) . catch ( ( error : unknown ) => {
2193- const message = error instanceof Error ? error . message : String ( error ) ;
2194- this . emit ( {
2195- type : "turn_failed" ,
2196- provider : this . provider ,
2197- turnId,
2198- error : message ,
2199- } ) ;
2200- } ) ;
22012209 }
22022210
22032211 private emitToolCallEvent (
0 commit comments