2026-07-04 21:47:42 +08:00
// == Pi Lab Page AI Runtime ==
// MNote-native adapter for Pi-first Page AI Lab.
2026-07-10 10:54:34 +08:00
// Renders the MNote-owned Pi Rust Page AI surface. OpenHub is kept as a
// compatibility/admin boundary during migration, not as the default chat owner.
// NO setInterval polling.
2026-07-04 21:47:42 +08:00
//
// UI note: @earendil-works/pi-web-ui@0.75.3 was checked as the mature upstream UI.
// It expects browser-side pi-agent-core, IndexedDB storage, API-key dialogs, model
// selection and builtin tools/artifacts. MNote's lab backend owns Pi RPC, access
// scope and tools, so this file keeps a host-owned adapter while matching the
// upstream ChatPanel/AgentInterface layout: message list, streaming composer,
// tool timeline, model/runtime controls and artifacts-style right rail.
//
// Default model: Omniroute/freefirst (consumed from backend status/default fields).
( function () {
'use strict' ;
var STATE_IDLE = 'idle' ;
var STATE_STARTING = 'starting' ;
var STATE_STARTED = 'started' ;
var STATE_STREAMING = 'streaming' ;
var STATE_ABORTED = 'aborted' ;
var STATE_ERROR = 'error' ;
var API = {
STATUS : '/api/page-ai/pi/status' ,
START : '/api/page-ai/pi/start' ,
SEND : '/api/page-ai/pi/send' ,
ABORT : '/api/page-ai/pi/abort' ,
2026-07-10 10:54:34 +08:00
UI_RESPONSE : '/api/page-ai/pi/ui-response' ,
2026-07-04 21:47:42 +08:00
EVENTS : '/api/page-ai/pi/events' ,
BOOTSTRAP : '/api/page-ai/pi/bootstrap' ,
SESSIONS : '/api/page-ai/pi/sessions' ,
EFFECTIVE : '/api/ai-settings/effective' ,
};
var DEFAULT_MODEL_PROVIDER = 'omniroute' ;
var DEFAULT_MODEL_ID = 'freefirst' ;
2026-07-10 10:54:34 +08:00
var DEFAULT_THINKING_LEVEL = 'medium' ;
var DEFAULT_PERMISSION_MODE = 'confirm' ;
2026-07-04 21:47:42 +08:00
var piLabInstalled = false ;
var piLabPanelEl = null ;
var piLabDrawerEl = null ;
var piLabLauncherEl = null ;
var piLabEventSource = null ;
2026-07-10 10:54:34 +08:00
var piLabStartPromise = null ;
2026-07-04 21:47:42 +08:00
var piLabState = {
status : STATE_IDLE ,
enabled : false ,
active : false ,
standalone : false ,
sessionId : null ,
providerSessionId : null ,
defaultModelProvider : DEFAULT_MODEL_PROVIDER ,
defaultModelId : DEFAULT_MODEL_ID ,
2026-07-10 10:54:34 +08:00
defaultThinkingLevel : DEFAULT_THINKING_LEVEL ,
2026-07-04 21:47:42 +08:00
modelProvider : null ,
modelId : null ,
2026-07-10 10:54:34 +08:00
thinkingLevel : DEFAULT_THINKING_LEVEL ,
permissionMode : DEFAULT_PERMISSION_MODE ,
permissionMenuOpen : false ,
pendingPermissionModeApply : false ,
applyingPermissionMode : false ,
actionMenuOpen : false ,
2026-07-04 21:47:42 +08:00
runtimeMode : null ,
2026-07-10 10:54:34 +08:00
runtimeImplementation : null ,
runtimeBinary : null ,
runtimeAvailable : true ,
runtimeInstallHint : '' ,
runtimeError : '' ,
2026-07-04 21:47:42 +08:00
runtimePid : null ,
2026-07-10 10:54:34 +08:00
managedBuiltinTools : [],
piExtensionSources : [],
piExtensionToolNames : [],
2026-07-04 21:47:42 +08:00
session : null ,
allowedRootsSummary : '' ,
selectionSummary : '' ,
selectionText : '' ,
changedFiles : [],
messages : [],
diagnostics : '' ,
receipts : [],
modelOptions : [],
history : [],
2026-07-10 10:54:34 +08:00
pendingQueue : { steering : [], followUp : [] },
viewingHistorySessionId : null ,
contextSelection : {
currentPage : false ,
currentFolder : false ,
selection : false ,
lightrag : false ,
},
2026-07-04 21:47:42 +08:00
};
var streamingAssistantMsg = null ;
2026-07-10 10:54:34 +08:00
var piRunViewModel = createPiRunViewModel ();
var activePiUiDialog = null ;
var piCustomUiState = {
panel : null ,
widgetKey : '' ,
title : '' ,
lines : [],
selectedOptionIndex : 0 ,
keyQueue : [],
submitted : false ,
pendingCustomRequest : null ,
};
var piToastSeq = 0 ;
function createPiRunViewModel ( seed ) {
seed = seed || {};
return {
turns : Array . isArray ( seed . turns ) ? seed . turns : [],
messages : Array . isArray ( seed . messages ) ? seed . messages : [],
toolCallsById : seed . toolCallsById || {},
pendingQueue : seed . pendingQueue || { steering : [], followUp : [] },
currentAssistant : seed . currentAssistant || null ,
runtimeStatus : seed . runtimeStatus || STATE_IDLE ,
abortResponse : seed . abortResponse || null ,
lastEventType : seed . lastEventType || '' ,
lastToolCall : null ,
};
}
function normalizeQueueItems ( value ) {
if ( ! Array . isArray ( value )) return [];
return value . map ( function ( item ) {
if ( item == null ) return '' ;
if ( typeof item === 'string' ) return item ;
return {
id : item . id || item . messageId || item . queueId || '' ,
text : item . text || item . message || item . prompt || '' ,
createdAt : item . createdAt || item . timestamp || '' ,
};
}). filter ( function ( item ) {
return typeof item === 'string' ? item . trim () : ( item . text || item . id );
});
}
function rebuildPiRunToolIndex ( vm ) {
vm . toolCallsById = {};
( vm . messages || []). forEach ( function ( msg ) {
( msg && Array . isArray ( msg . toolCalls ) ? msg . toolCalls : []). forEach ( function ( tc ) {
if ( ! tc ) return ;
var key = tc . id || tc . name || ( 'tool-' + Object . keys ( vm . toolCallsById ). length );
vm . toolCallsById [ key ] = tc ;
});
});
return vm . toolCallsById ;
}
function syncPiRunViewModelFromState () {
piRunViewModel . messages = piLabState . messages ;
piRunViewModel . pendingQueue = piLabState . pendingQueue || { steering : [], followUp : [] };
piRunViewModel . currentAssistant = streamingAssistantMsg ;
piRunViewModel . runtimeStatus = piLabState . status ;
rebuildPiRunToolIndex ( piRunViewModel );
return piRunViewModel ;
}
function syncPiRunViewModelToState () {
piLabState . messages = piRunViewModel . messages ;
piLabState . pendingQueue = piRunViewModel . pendingQueue || { steering : [], followUp : [] };
streamingAssistantMsg = piRunViewModel . currentAssistant || null ;
return piRunViewModel ;
}
function resetPiRunViewModel ( options ) {
piRunViewModel = createPiRunViewModel ({ runtimeStatus : piLabState . status });
if ( ! options || options . sync !== false ) syncPiRunViewModelToState ();
return piRunViewModel ;
}
function enterHistoryReplayMode ( sessionId ) {
piLabState . viewingHistorySessionId = sessionId || piLabState . sessionId || null ;
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
}
function exitHistoryReplayMode () {
piLabState . viewingHistorySessionId = null ;
}
function ensurePiRunAssistant ( vm ) {
if ( ! vm . currentAssistant ) {
vm . currentAssistant = { role : 'assistant' , text : '' , toolCalls : [], citations : [], diffSummary : null , status : 'streaming' };
vm . messages . push ( vm . currentAssistant );
}
vm . currentAssistant . status = vm . currentAssistant . status || 'streaming' ;
vm . runtimeStatus = STATE_STREAMING ;
return vm . currentAssistant ;
}
function findLastAssistant ( vm ) {
return ( vm . messages || []). slice (). reverse (). find ( function ( message ) {
return message && message . role === 'assistant' ;
}) || null ;
}
function piRunUpsertToolCall ( vm , raw , status , patch ) {
var msg = vm . currentAssistant || findLastAssistant ( vm ) || ensurePiRunAssistant ( vm );
var payload = raw || {};
var calls = ensureToolCalls ( msg );
var id = normalizeToolCallId ( payload );
var name = toolDisplayName ( payload );
var found = null ;
if ( id ) found = calls . find ( function ( item ) { return item && item . id === id ; });
if ( ! found && name ) {
found = calls . slice (). reverse (). find ( function ( item ) {
return item && item . name === name && ( item . status === 'running' || item . status === 'pending' );
});
}
if ( ! found ) {
found = { id : id || ( 'tool-' + ( calls . length + 1 )), name : name , args : {}, status : status || 'running' };
calls . push ( found );
}
found . id = id || found . id ;
found . name = name || found . name ;
found . status = status || found . status || 'running' ;
if ( payload . args || payload . input || payload . params ) found . args = payload . args || payload . input || payload . params ;
if ( payload . details ) found . details = payload . details ;
Object . keys ( patch || {}). forEach ( function ( key ) { found [ key ] = patch [ key ]; });
vm . lastToolCall = found ;
rebuildPiRunToolIndex ( vm );
return found ;
}
function reducePiRunViewModel ( vm , event ) {
vm = vm || createPiRunViewModel ();
event = event || {};
var payload = event . payload || {};
vm . lastEventType = event . type || '' ;
if ( event . type === 'clear' ) {
vm . messages = [];
vm . toolCallsById = {};
vm . pendingQueue = { steering : [], followUp : [] };
vm . currentAssistant = null ;
vm . abortResponse = null ;
return vm ;
}
if ( event . type === 'queue_update' ) {
vm . pendingQueue = {
steering : normalizeQueueItems ( payload . steering ),
followUp : normalizeQueueItems ( payload . followUp ),
};
return vm ;
}
if ( event . type === 'user_prompt' ) {
vm . messages . push ({ role : 'user' , text : event . text || payload . text || payload . message || '' , meta : event . meta || payload . meta || '' });
return vm ;
}
if ( event . type === 'assistant_begin' ) {
ensurePiRunAssistant ( vm );
return vm ;
}
if ( event . type === 'assistant_delta' ) {
var deltaMsg = ensurePiRunAssistant ( vm );
if ( event . delta || payload . delta ) deltaMsg . text += event . delta || payload . delta ;
if (( event . text || payload . text ) && ! ( event . delta || payload . delta )) deltaMsg . text = event . text || payload . text ;
return vm ;
}
if ( event . type === 'assistant_text_end' ) {
var textEndMsg = ensurePiRunAssistant ( vm );
textEndMsg . text = event . text || payload . text || payload . content || textEndMsg . text ;
return vm ;
}
if ( event . type === 'assistant_done' ) {
var doneMsg = ensurePiRunAssistant ( vm );
if ( event . text || payload . text ) doneMsg . text = event . text || payload . text ;
if ( event . toolCalls || payload . toolCalls ) doneMsg . toolCalls = event . toolCalls || payload . toolCalls ;
if ( event . citations || payload . citations ) doneMsg . citations = event . citations || payload . citations ;
if ( event . diffSummary || payload . diffSummary ) doneMsg . diffSummary = event . diffSummary || payload . diffSummary ;
doneMsg . status = 'done' ;
vm . currentAssistant = null ;
vm . runtimeStatus = STATE_STARTED ;
rebuildPiRunToolIndex ( vm );
return vm ;
}
if ( event . type === 'assistant_abort' ) {
var abortMsg = vm . currentAssistant || findLastAssistant ( vm ) || ensurePiRunAssistant ( vm );
abortMsg . status = 'aborted' ;
abortMsg . abortReason = event . reason || payload . reason || payload . stopReason || payload . error || payload . message || '已中止' ;
abortMsg . abortResponse = payload || { reason : abortMsg . abortReason };
vm . abortResponse = payload || { reason : abortMsg . abortReason };
vm . currentAssistant = null ;
vm . runtimeStatus = STATE_ABORTED ;
return vm ;
}
if ( event . type === 'assistant_error' ) {
var errMsg = ensurePiRunAssistant ( vm );
errMsg . text += ( errMsg . text ? '\n' : '' ) + '错误: ' + ( event . message || payload . error || payload . message || 'Pi runtime error' );
errMsg . status = 'done' ;
vm . currentAssistant = null ;
vm . runtimeStatus = STATE_STARTED ;
return vm ;
}
if ( event . type === 'tool_upsert' ) {
piRunUpsertToolCall ( vm , payload . raw || payload , payload . status || event . status , payload . patch || event . patch || {});
return vm ;
}
if ( event . type === 'citation' ) {
var citMsg = ensurePiRunAssistant ( vm );
citMsg . citations . push ({ source : payload . source || '' , title : payload . title || '' , url : payload . url || '#' });
return vm ;
}
if ( event . type === 'diff' ) {
var diffMsg = ensurePiRunAssistant ( vm );
diffMsg . diffSummary = diffMsg . diffSummary || { files : [] };
diffMsg . diffSummary . files = payload . files || [];
return vm ;
}
return vm ;
}
function applyPiRunEvent ( event ) {
syncPiRunViewModelFromState ();
reducePiRunViewModel ( piRunViewModel , event );
syncPiRunViewModelToState ();
return piRunViewModel ;
}
2026-07-04 21:47:42 +08:00
function injectStyles () {
var styleId = 'pi-lab-runtime-style' ;
if ( document . getElementById ( styleId )) return ;
var style = document . createElement ( 'style' );
style . id = styleId ;
style . textContent =
'.wolai-page-ai-pi-lab-launcher{position:fixed;right:26px;bottom:92px;z-index:2600;width:36px;height:36px;border-radius:18px;border:1px solid rgba(37,37,33,.14);background:#fff;color:#252521;box-shadow:0 8px 24px rgba(15,23,42,.16);display:grid;place-items:center;font:600 15px/1 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;cursor:pointer}' +
'.wolai-page-ai-pi-lab-launcher:hover{background:#f7f7f5;box-shadow:0 10px 28px rgba(15,23,42,.20)}' +
'.wolai-page-ai-pi-lab-launcher[data-state="streaming"]::after{content:"";position:absolute;right:4px;top:4px;width:8px;height:8px;border-radius:50%;background:#10a37f;box-shadow:0 0 0 3px rgba(16,163,127,.14)}' +
'.wolai-page-ai-pi-lab-drawer{position:fixed;right:18px;bottom:18px;top:64px;z-index:2590;width:min(760px,calc(100vw - 36px));border:1px solid rgba(35,35,30,.10);border-radius:20px;background:#f5f6fa;box-shadow:0 24px 70px rgba(15,23,42,.22);overflow:hidden;display:flex;flex-direction:column}' +
'.wolai-page-ai-pi-lab-drawer[hidden]{display:none!important}' +
'.wolai-page-ai-pi-lab-drawer[data-minimized="true"]{top:auto;height:54px;width:min(420px,calc(100vw - 36px))}' +
'.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-config,.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-context-strip,.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-body,.wolai-page-ai-pi-lab-drawer[data-minimized="true"] .wolai-page-ai-pi-lab-diagnostics{display:none!important}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-shell{position:relative;display:flex;flex-direction:column;height:100%;min-height:0;background:#f5f6fa;color:#242424;font:13px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-shell[hidden]{display:none!important}' +
'.wolai-page-ai-pi-lab-topbar{height:58px;display:flex;align-items:center;gap:10px;padding:0 18px;background:#f5f6fa;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-brand{display:flex;align-items:center;gap:8px;min-width:0;flex:1}' +
'.wolai-page-ai-pi-lab-logo{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;background:#1f6feb;color:#fff;font-weight:700}' +
'.wolai-page-ai-pi-lab-title{display:flex;flex-direction:column;gap:1px;min-width:0}' +
'.wolai-page-ai-pi-lab-title strong{font-size:22px;font-weight:760;letter-spacing:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-title span{font-size:11px;color:#6f6a60;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-icon-btn{width:30px;height:30px;border:1px solid #e2ded6;border-radius:8px;background:#fff;color:#514f49;display:grid;place-items:center;cursor:pointer}' +
'.wolai-page-ai-pi-lab-icon-btn:hover{background:#f7f6f2}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-commandbar{height:50px;margin:0 14px 12px;padding:0 12px;border:1px solid #e4e7ef;border-radius:14px;background:#fff;display:flex;align-items:center;gap:8px;box-shadow:0 2px 8px rgba(15,23,42,.04);flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon{width:32px;height:32px;border:0;background:transparent;color:#3c424c;display:grid;place-items:center;cursor:pointer;border-radius:9px}' +
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon svg,.wolai-page-ai-pi-lab-square svg,.wolai-page-ai-pi-lab-icon-btn svg{width:17px;height:17px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}' +
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon:hover,.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon[data-active="true"]{background:#f3f5f8;color:#111827}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-commandbar .wolai-page-ai-pi-lab-tool-icon[disabled]{opacity:.35;cursor:default}' +
'.wolai-page-ai-pi-lab-commandbar-spacer{flex:1}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-config{display:none;grid-template-columns:minmax(0,1fr) auto;gap:10px;margin:0 14px 12px;padding:10px 12px;border:1px solid #e5e8ef;border-radius:12px;background:#fff;box-shadow:0 4px 14px rgba(15,23,42,.05);flex:0 0 auto}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-drawer[data-config-open="true"] .wolai-page-ai-pi-lab-config{display:grid}' +
'.wolai-page-ai-pi-lab-model-row{display:flex;flex-wrap:wrap;gap:6px;min-width:0}' +
'.wolai-page-ai-pi-lab-settings{border:0;min-width:0}' +
'.wolai-page-ai-pi-lab-settings>summary{display:inline-flex;align-items:center;gap:6px;min-height:28px;cursor:pointer;list-style:none;color:#514f49;font-size:12px}' +
'.wolai-page-ai-pi-lab-settings>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-settings>summary::before{content:"▸";font-size:10px;color:#8a8378}' +
'.wolai-page-ai-pi-lab-settings[open]>summary::before{content:"▾"}' +
'.wolai-page-ai-pi-lab-settings-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:8px;max-width:560px}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-history-layer{position:absolute;inset:0;z-index:28;display:block;pointer-events:auto}' +
'.wolai-page-ai-pi-lab-history-layer[hidden]{display:none!important}' +
'.wolai-page-ai-pi-lab-history-scrim{position:absolute;inset:0;background:rgba(15,23,42,.22);backdrop-filter:blur(1px);border-radius:18px}' +
'.wolai-page-ai-pi-lab-history-panel{position:absolute;left:0;top:0;bottom:0;width:min(340px,calc(100vw - 34px));border-right:1px solid #dfe4ec;background:#fff;box-shadow:18px 0 42px rgba(15,23,42,.18);display:flex;flex-direction:column;overflow:hidden}' +
'.wolai-page-ai-pi-lab-history-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:0 14px;border-bottom:1px solid #edf0f5;background:#fff;color:#1f2937;font-size:14px;font-weight:750}' +
'.wolai-page-ai-pi-lab-history-close{position:relative;width:26px;height:26px;border:1px solid #e1e4eb;border-radius:8px;background:#fff;color:#4a4f58;display:grid;place-items:center;cursor:pointer;font-size:0;line-height:1}' +
'.wolai-page-ai-pi-lab-history-close::before,.wolai-page-ai-pi-lab-history-close::after{content:"";position:absolute;left:7px;right:7px;top:12px;height:1.5px;background:currentColor;border-radius:999px}' +
'.wolai-page-ai-pi-lab-history-close::before{transform:rotate(45deg)}.wolai-page-ai-pi-lab-history-close::after{transform:rotate(-45deg)}' +
'.wolai-page-ai-pi-lab-history-close svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}' +
'.wolai-page-ai-pi-lab-history-toolbar{display:flex;align-items:center;gap:8px;padding:12px 14px 10px;border-bottom:1px solid #f0f2f6;background:#fafbfc}' +
'.wolai-page-ai-pi-lab-history-new{height:32px;flex:1;border:1px solid #1f6feb;border-radius:8px;background:#1f6feb;color:#fff;font:12px/1 inherit;font-weight:700;cursor:pointer}' +
'.wolai-page-ai-pi-lab-history-refresh,.wolai-page-ai-pi-lab-history-clear{height:32px;border:1px solid #d9dee8;border-radius:8px;background:#fff;color:#4b5563;font:12px/1 inherit;padding:0 9px;cursor:pointer}' +
'.wolai-page-ai-pi-lab-history-clear{color:#b42318;border-color:#f2c4bd}' +
'.wolai-page-ai-pi-lab-history-meta{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 14px;color:#6b7280;font-size:11px;border-bottom:1px solid #f0f2f6}' +
'.wolai-page-ai-pi-lab-history-list{min-height:0;overflow:auto;flex:1}' +
'.wolai-page-ai-pi-lab-history-row{border-bottom:1px solid #f0eee9;background:#fff;padding:11px 12px;display:grid;grid-template-columns:10px minmax(0,1fr) auto;gap:10px;align-items:center}' +
'.wolai-page-ai-pi-lab-history-row:hover{background:#f8faff}' +
'.wolai-page-ai-pi-lab-history-dot{width:7px;height:7px;border-radius:999px;background:#1f6feb;opacity:.35}' +
'.wolai-page-ai-pi-lab-history-row[data-active="true"] .wolai-page-ai-pi-lab-history-dot{opacity:1}' +
'.wolai-page-ai-pi-lab-history-row strong{display:block;font-size:12px;color:#1f2937}' +
'.wolai-page-ai-pi-lab-history-row span{font-size:11px;color:#6b7280}' +
'.wolai-page-ai-pi-lab-history-main{min-width:0;border:0;background:transparent;padding:0;text-align:left;cursor:pointer}' +
'.wolai-page-ai-pi-lab-history-main strong,.wolai-page-ai-pi-lab-history-main span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.wolai-page-ai-pi-lab-history-actions{display:flex;gap:4px;align-items:center}' +
'.wolai-page-ai-pi-lab-history-action{border:1px solid #d8dee8;border-radius:6px;background:#fff;padding:4px 6px;font-size:11px;color:#4b5563;cursor:pointer;white-space:nowrap}' +
'.wolai-page-ai-pi-lab-history-action:hover{background:#f3f6fb}' +
'.wolai-page-ai-pi-lab-history-danger{color:#b42318;border-color:#f2c4bd}' +
'.wolai-page-ai-pi-lab-history-row[data-active="true"]{background:#f4f7ff}.wolai-page-ai-pi-lab-history-row[data-active="true"] .wolai-page-ai-pi-lab-history-main strong{color:#1f3f8f}' +
'.wolai-page-ai-pi-lab-replay-banner{margin:0 18px 10px;padding:8px 10px;border:1px solid #dbe5ff;border-radius:8px;background:#f6f8ff;color:#36507d;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:10px}' +
'.wolai-page-ai-pi-lab-replay-banner[hidden]{display:none!important}.wolai-page-ai-pi-lab-replay-banner button{border:1px solid #cfd9f5;border-radius:7px;background:#fff;color:#24406d;height:26px;padding:0 9px;font:12px/1 inherit;cursor:pointer}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-field{display:flex;flex-direction:column;gap:4px;font-size:11px;color:#6f6a60}' +
'.wolai-page-ai-pi-lab-field select,.wolai-page-ai-pi-lab-field input{height:30px;border:1px solid #ddd8cf;border-radius:8px;background:#fff;color:#242424;padding:0 8px;font:12px/1.3 inherit;min-width:0}' +
'.wolai-page-ai-pi-lab-context-strip{display:none;padding:0;margin:-6px 14px 12px;border:1px solid #ebe7de;border-radius:12px;background:#faf9f6;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-drawer[data-config-open="true"] .wolai-page-ai-pi-lab-context-strip{display:block}' +
'.wolai-page-ai-pi-lab-context-strip>summary{display:flex;align-items:center;gap:8px;min-height:34px;padding:0 12px;cursor:pointer;list-style:none;color:#5d594f;font-size:12px}' +
'.wolai-page-ai-pi-lab-context-strip>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-context-strip>summary::before{content:"▸";font-size:10px;color:#8a8378}' +
'.wolai-page-ai-pi-lab-context-strip[open]>summary::before{content:"▾"}' +
'.wolai-page-ai-pi-lab-context-title{font-weight:650;color:#42403a}' +
'.wolai-page-ai-pi-lab-context-current{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#242424;font-weight:650}' +
'.wolai-page-ai-pi-lab-context-count{margin-left:auto;color:#8a8378;font-size:11px}' +
'.wolai-page-ai-pi-lab-context-content{display:flex;align-items:center;gap:6px;padding:0 12px 8px;overflow-x:auto}' +
'.wolai-page-ai-pi-lab-chip{height:24px;display:inline-flex;align-items:center;gap:5px;border:1px solid #e4e0d8;border-radius:999px;background:#f8f7f3;color:#514f49;padding:0 8px;font-size:11px;max-width:100%}' +
'.wolai-page-ai-pi-lab-chip strong{font-weight:650;color:#242424;min-width:0;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-context-strip .wolai-page-ai-pi-lab-chip{flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-chip[data-tone="ok"]{background:#eef8f3;border-color:#ccebdc;color:#176b4b}' +
'.wolai-page-ai-pi-lab-chip[data-tone="warn"]{background:#fff6e6;border-color:#f2dfb5;color:#865b13}' +
'.wolai-page-ai-pi-lab-chip[data-tone="danger"]{background:#fff0ee;border-color:#f2c4bd;color:#a73b2f}' +
'.wolai-page-ai-pi-lab-actions{display:flex;align-items:center;gap:6px;justify-content:flex-end}' +
'.wolai-page-ai-pi-lab-btn{height:28px;border:1px solid #d9d4ca;border-radius:7px;background:#fff;color:#383631;font-size:12px;padding:0 10px;cursor:pointer;white-space:nowrap}' +
'.wolai-page-ai-pi-lab-btn:hover{background:#f7f6f2}' +
'.wolai-page-ai-pi-lab-btn:disabled{opacity:.45;cursor:default}' +
'.wolai-page-ai-pi-lab-btn-primary{background:#1f6feb;border-color:#1f6feb;color:#fff}' +
'.wolai-page-ai-pi-lab-btn-primary:hover{background:#1b61cf}' +
'.wolai-page-ai-pi-lab-btn-danger{background:#d93025;border-color:#d93025;color:#fff}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-body{position:relative;display:block;min-height:0;flex:1;background:#f5f6fa;padding:0 14px 12px}' +
'.wolai-page-ai-pi-lab-main{position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;height:100%;overflow:hidden}' +
'.wolai-page-ai-pi-lab-messages{flex:1;min-height:0;overflow-y:auto;padding:22px 18px 20px;display:flex;flex-direction:column;gap:12px;overscroll-behavior:contain;background:#fff;border:1px solid #e4e7ef;border-radius:16px 16px 0 0;box-shadow:0 10px 28px rgba(15,23,42,.07)}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-empty{margin:auto;max-width:460px;text-align:center;color:#8a9099;background:transparent;border:0;border-radius:0;padding:10px 0 4px;box-shadow:none}' +
'.wolai-page-ai-pi-lab-empty-icon{width:76px;height:76px;margin:0 auto 18px;border-radius:50%;background:#f0f2f6;color:#8a93a2;display:grid;place-items:center;font-size:42px;font-weight:400}' +
'.wolai-page-ai-pi-lab-empty strong{display:block;color:#242833;margin-bottom:8px;font-size:25px;font-weight:760;letter-spacing:0}' +
'.wolai-page-ai-pi-lab-empty p{margin:0 0 20px;color:#8a9099;font-size:16px}' +
'.wolai-page-ai-pi-lab-examples{display:flex;flex-direction:column;align-items:center;gap:10px}' +
'.wolai-page-ai-pi-lab-example{height:42px;min-width:min(390px,100%);border:1px solid #e1e4eb;border-radius:12px;background:#fff;color:#4a4f58;font-size:16px;display:flex;align-items:center;justify-content:flex-start;gap:12px;padding:0 18px;box-shadow:0 3px 9px rgba(15,23,42,.05);cursor:pointer}' +
'.wolai-page-ai-pi-lab-example:hover{background:#f8fafc}' +
'.wolai-page-ai-pi-lab-example span{font-size:18px;color:#5c6470}' +
'.wolai-page-ai-pi-lab-message{display:flex;flex-direction:column;gap:6px;max-width:92%}' +
'.wolai-page-ai-pi-lab-message[data-role="user"]{align-self:flex-end;align-items:flex-end}' +
'.wolai-page-ai-pi-lab-message[data-role="assistant"],.wolai-page-ai-pi-lab-message[data-role="system"]{align-self:stretch}' +
'.wolai-page-ai-pi-lab-role{display:flex;align-items:center;gap:6px;color:#777166;font-size:11px;font-weight:650}' +
'.wolai-page-ai-pi-lab-message[data-role="user"] .wolai-page-ai-pi-lab-role{justify-content:flex-end}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-bubble{position:relative;border:1px solid #e6e2da;border-radius:12px;background:#fff;padding:10px 12px;color:#272521;word-break:break-word;box-shadow:0 4px 14px rgba(15,23,42,.035)}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-message[data-role="user"] .wolai-page-ai-pi-lab-bubble{background:#f3f7ff;border-color:#d9e6ff}' +
'.wolai-page-ai-pi-lab-text{white-space:pre-wrap}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-markdown{font-size:14px;line-height:1.62;color:#272521}' +
'.wolai-page-ai-pi-lab-markdown>*:first-child{margin-top:0}' +
'.wolai-page-ai-pi-lab-markdown>*:last-child{margin-bottom:0}' +
'.wolai-page-ai-pi-lab-markdown p{margin:0 0 10px}' +
'.wolai-page-ai-pi-lab-markdown h1,.wolai-page-ai-pi-lab-markdown h2,.wolai-page-ai-pi-lab-markdown h3{margin:14px 0 8px;font-weight:720;letter-spacing:0;line-height:1.28;color:#202124}' +
'.wolai-page-ai-pi-lab-markdown h1{font-size:20px}.wolai-page-ai-pi-lab-markdown h2{font-size:17px}.wolai-page-ai-pi-lab-markdown h3{font-size:15px}' +
'.wolai-page-ai-pi-lab-markdown ul,.wolai-page-ai-pi-lab-markdown ol{margin:0 0 10px 20px;padding:0}' +
'.wolai-page-ai-pi-lab-markdown li{margin:3px 0;padding-left:2px}' +
'.wolai-page-ai-pi-lab-markdown blockquote{margin:0 0 10px;padding:2px 0 2px 12px;border-left:3px solid #d8dce6;color:#5f6368}' +
'.wolai-page-ai-pi-lab-markdown pre{margin:8px 0 12px;padding:10px 12px;border:1px solid #e4e7ef;border-radius:8px;background:#f7f8fb;overflow:auto;white-space:pre;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#242833}' +
'.wolai-page-ai-pi-lab-markdown code{border:1px solid #e5e7eb;border-radius:5px;background:#f7f8fb;padding:1px 4px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#1f2937}' +
'.wolai-page-ai-pi-lab-markdown pre code{border:0;background:transparent;padding:0;font:inherit;color:inherit}' +
'.wolai-page-ai-pi-lab-markdown table{width:100%;border-collapse:collapse;margin:8px 0 12px;font-size:13px}' +
'.wolai-page-ai-pi-lab-markdown th,.wolai-page-ai-pi-lab-markdown td{border:1px solid #e4e7ef;padding:6px 8px;text-align:left;vertical-align:top}' +
'.wolai-page-ai-pi-lab-markdown th{background:#f7f8fb;font-weight:650}' +
'.wolai-page-ai-pi-lab-markdown a{color:#1f6feb;text-decoration:none}.wolai-page-ai-pi-lab-markdown a:hover{text-decoration:underline}' +
'.wolai-page-ai-pi-lab-text.streaming-cursor::after,.wolai-page-ai-pi-lab-markdown.streaming-cursor::after{content:"";display:inline-block;width:7px;height:14px;margin-left:3px;vertical-align:-2px;background:#1f6feb;animation:pi-blink .85s steps(2,start) infinite}' +
2026-07-04 21:47:42 +08:00
'@keyframes pi-blink{50%{opacity:.18}}' +
'.wolai-page-ai-pi-lab-tool-calls,.wolai-page-ai-pi-lab-citations{display:flex;flex-direction:column;gap:6px;margin-top:8px}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-tool-timeline{margin-top:10px;border:1px solid #e4e7ef;border-radius:8px;background:#fafbfc;overflow:hidden}' +
'.wolai-page-ai-pi-lab-tool-timeline>summary{min-height:32px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 10px;cursor:pointer;list-style:none;color:#41464f;font-size:12px;font-weight:650}' +
'.wolai-page-ai-pi-lab-tool-timeline>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-tool-timeline>summary::after{content:"▸";font-size:10px;color:#7c8593}' +
'.wolai-page-ai-pi-lab-tool-timeline[open]>summary::after{content:"▾"}' +
'.wolai-page-ai-pi-lab-tool-timeline-body{display:flex;flex-direction:column;gap:6px;padding:8px;border-top:1px solid #e4e7ef}' +
'.wolai-page-ai-pi-lab-tool-call{border:1px solid #e5e0d8;border-radius:9px;background:#fff;overflow:hidden}' +
'.wolai-page-ai-pi-lab-tool-call[data-status="running"]{border-color:#bcd7ff;background:#f7fbff}' +
'.wolai-page-ai-pi-lab-tool-call[data-status="error"]{border-color:#f2c4bd;background:#fff8f7}' +
'.wolai-page-ai-pi-lab-tool-call-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 9px;font-size:12px;font-weight:650;color:#42403a}' +
'.wolai-page-ai-pi-lab-tool-call-header span{font-size:11px;color:#777166;font-weight:500}' +
'.wolai-page-ai-pi-lab-tool-call-summary-line{padding:0 9px 7px;color:#6f6a60;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.wolai-page-ai-pi-lab-tool-call-details{border-top:1px solid #ebe7de;background:#fff}' +
'.wolai-page-ai-pi-lab-tool-call-details>summary{min-height:28px;display:flex;align-items:center;gap:6px;padding:0 9px;cursor:pointer;list-style:none;color:#5d594f;font-size:11px}' +
'.wolai-page-ai-pi-lab-tool-call-details>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-tool-call-details>summary::before{content:"▸";font-size:10px;color:#8a8378}.wolai-page-ai-pi-lab-tool-call-details[open]>summary::before{content:"▾"}' +
'.wolai-page-ai-pi-lab-tool-call pre{margin:0;border-top:1px solid #ebe7de;background:#fff;padding:8px;max-height:140px;overflow:auto;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#42403a;white-space:pre-wrap}' +
'.wolai-page-ai-pi-lab-tool-policy{display:inline-flex;align-items:center;border-radius:999px;border:1px solid #ded7ca;background:#fff;padding:1px 6px;font-size:10px;color:#6f6557;margin-left:6px}' +
'.wolai-page-ai-pi-lab-tool-policy[data-policy="ask"]{border-color:#e1b870;background:#fff8e8;color:#8a5c13}' +
'.wolai-page-ai-pi-lab-tool-policy[data-approved="true"]{border-color:#83b99a;background:#eef9f1;color:#27613b}' +
'.wolai-page-ai-pi-lab-tool-policy[data-approved="false"]{border-color:#e0a39a;background:#fff0ee;color:#9f352c}' +
'.wolai-page-ai-pi-lab-abort-note{margin-top:8px;border:1px solid #f2c4bd;border-radius:8px;background:#fff8f7;color:#9f352c;padding:7px 9px;font-size:12px}' +
'.wolai-page-ai-pi-lab-message-actions{position:absolute;right:8px;top:8px;display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s ease}' +
'.wolai-page-ai-pi-lab-bubble:hover .wolai-page-ai-pi-lab-message-actions,.wolai-page-ai-pi-lab-bubble:focus-within .wolai-page-ai-pi-lab-message-actions{opacity:1;pointer-events:auto}' +
'.wolai-page-ai-pi-lab-message-actions button{width:28px;height:28px;border:1px solid #e1e4eb;border-radius:8px;background:#fff;color:#4a4f58;padding:0;display:grid;place-items:center;cursor:pointer;box-shadow:0 4px 14px rgba(15,23,42,.08)}' +
'.wolai-page-ai-pi-lab-message-actions button:hover{background:#f8fafc}' +
'.wolai-page-ai-pi-lab-message-actions svg{width:15px;height:15px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-citation{display:inline-flex;align-items:center;gap:6px;width:max-content;max-width:100%;border:1px solid #d9e6ff;border-radius:999px;background:#f3f7ff;color:#2456a6;text-decoration:none;padding:4px 8px;font-size:11px}' +
'.wolai-page-ai-pi-lab-diff{display:inline-flex;align-items:center;width:max-content;max-width:100%;margin-top:8px;border:1px solid #f0dcb7;border-radius:999px;background:#fff7e8;color:#865b13;padding:4px 8px;font-size:11px}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-composer{position:relative;flex:0 0 auto;border:1px solid #e4e7ef;border-top:0;border-radius:0 0 16px 16px;background:#fff;box-shadow:0 14px 34px rgba(15,23,42,.09);overflow:visible}' +
'.wolai-page-ai-pi-lab-input-wrap{position:relative;background:#f7f8fb}' +
'.wolai-page-ai-pi-lab-input{width:100%;min-height:98px;max-height:190px;box-sizing:border-box;border:0;resize:none;padding:18px 62px 12px 22px;font:18px/1.45 inherit;color:#242424;background:transparent;outline:none}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-input::placeholder{color:#b6bbc5}' +
'.wolai-page-ai-pi-lab-quick{height:24px;border:1px solid #e5e0d8;border-radius:999px;background:#f8f7f3;color:#5d594f;font-size:11px;padding:0 8px;cursor:pointer}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-modebar{min-height:46px;display:flex;align-items:center;gap:8px;padding:6px 12px;border-top:1px solid #eef0f5;background:#fff;box-sizing:border-box}' +
'.wolai-page-ai-pi-lab-square{width:34px;height:34px;flex:0 0 34px;border:1px solid #e1e4eb;border-radius:9px;background:#fff;color:#242833;display:grid;place-items:center;cursor:pointer}' +
'.wolai-page-ai-pi-lab-square:hover{background:#f8fafc}' +
'.wolai-page-ai-pi-lab-square[data-active="true"],.wolai-page-ai-pi-lab-chip-button[data-active="true"]{border-color:#9ec5fe;background:#eef6ff;color:#1558a6}' +
'.wolai-page-ai-pi-lab-chip-button{height:34px;display:inline-flex;align-items:center;gap:6px;border:1px solid #e1e4eb;border-radius:10px;background:#fff;color:#4a4f58;padding:0 10px;font:12px/1 inherit;white-space:nowrap;cursor:pointer}' +
'.wolai-page-ai-pi-lab-chip-button svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}' +
'.wolai-page-ai-pi-lab-chip-button[data-tone="warn"]{border-color:#f0d8aa;background:#fff9ed;color:#865b13}.wolai-page-ai-pi-lab-chip-button[data-tone="danger"]{border-color:#f2c4bd;background:#fff8f7;color:#9f352c}.wolai-page-ai-pi-lab-chip-button[data-tone="ok"]{border-color:#bfe6ac;background:#f4fff0;color:#27613b}' +
'.wolai-page-ai-pi-lab-thinking-control{height:34px;border:1px solid #e1e4eb;border-radius:10px;background:#fff;color:#4a4f58;padding:0 8px;font:12px/1 inherit;max-width:104px}' +
'.wolai-page-ai-pi-lab-permission-wrap{position:relative;display:inline-flex;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-permission-menu{position:absolute;left:0;bottom:40px;z-index:18;width:min(306px,calc(100vw - 28px));border:1px solid #dfe3ea;border-radius:12px;background:#fff;box-shadow:0 18px 46px rgba(15,23,42,.22);padding:6px;display:none;color:#242424}' +
'.wolai-page-ai-pi-lab-permission-wrap[data-open="true"] .wolai-page-ai-pi-lab-permission-menu{display:block}' +
'.wolai-page-ai-pi-lab-permission-option{width:100%;min-height:58px;border:0;border-radius:9px;background:#fff;color:#242424;display:grid;grid-template-columns:22px 1fr 18px;gap:10px;align-items:center;text-align:left;padding:8px 9px;font:13px/1.25 inherit;cursor:pointer}' +
'.wolai-page-ai-pi-lab-permission-option:hover{background:#f7f8fb}.wolai-page-ai-pi-lab-permission-option[data-active="true"]{background:#f3f6fb}' +
'.wolai-page-ai-pi-lab-permission-option svg{width:17px;height:17px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round;color:#5d6470}' +
'.wolai-page-ai-pi-lab-permission-option strong{display:block;font-size:13px;font-weight:650}.wolai-page-ai-pi-lab-permission-option small{display:block;margin-top:4px;color:#6b7280;font-size:12px;line-height:1.25}.wolai-page-ai-pi-lab-permission-check{color:#6b7280;text-align:right}' +
'.wolai-page-ai-pi-lab-action-menu{position:absolute;left:12px;bottom:52px;z-index:16;width:min(300px,calc(100% - 24px));border:1px solid #dfe3ea;border-radius:12px;background:#fff;box-shadow:0 18px 46px rgba(15,23,42,.22);padding:6px;display:none;color:#242424}' +
'.wolai-page-ai-pi-lab-composer[data-menu-open="true"] .wolai-page-ai-pi-lab-action-menu{display:block}' +
'.wolai-page-ai-pi-lab-menu-section{padding:5px 7px 4px;color:#8a9099;font-size:11px;font-weight:650}' +
'.wolai-page-ai-pi-lab-menu-item{width:100%;min-height:34px;border:0;border-radius:8px;background:#fff;color:#242424;display:flex;align-items:center;gap:9px;text-align:left;padding:0 9px;font:13px/1.2 inherit;cursor:pointer}' +
'.wolai-page-ai-pi-lab-menu-item:hover{background:#f7f8fb}.wolai-page-ai-pi-lab-menu-item[data-active="true"]{background:#eef6ff;color:#1558a6}.wolai-page-ai-pi-lab-menu-item[disabled]{opacity:.45;cursor:default}.wolai-page-ai-pi-lab-menu-item[disabled]:hover{background:#fff}' +
'.wolai-page-ai-pi-lab-menu-item svg{width:15px;height:15px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-menu-item strong{margin-left:auto;color:#1558a6;font-size:13px}' +
'.wolai-page-ai-pi-lab-menu-sep{height:1px;background:#edf0f5;margin:6px 3px}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-segment{display:inline-flex;border:1px solid #e1e4eb;border-radius:10px;overflow:hidden;background:#fff}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-segment button{height:36px;border:0;background:#fff;color:#4a4f58;padding:0 16px;font:14px/1 inherit;cursor:pointer;white-space:nowrap}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-segment button[data-active=\"true\"]{background:#111827;color:#fff}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-stream-controls{display:inline-flex;align-items:center;gap:8px}' +
'.wolai-page-ai-pi-lab-queue-count{height:22px;min-width:22px;padding:0 6px;border-radius:999px;background:#eef2ff;color:#334155;display:inline-flex;align-items:center;justify-content:center;font-size:11px}' +
'.wolai-page-ai-pi-lab-model-control{height:34px;min-width:0;max-width:210px;flex:1 1 160px;border:1px solid #e1e4eb;border-radius:10px;background:#fff;color:#4a4f58;padding:0 10px;font:13px/1 inherit}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-spacer{flex:1}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-send{position:absolute;right:12px;bottom:11px;width:38px;height:38px;border:0;border-radius:12px;background:#1d9bf0;color:#fff;display:grid;place-items:center;cursor:pointer;box-shadow:0 10px 24px rgba(29,155,240,.25)}' +
'.wolai-page-ai-pi-lab-send svg{width:18px;height:18px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-send:disabled{opacity:.45;cursor:default}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-rail{position:absolute;right:14px;top:0;bottom:104px;width:260px;z-index:4;display:none;flex-direction:column;gap:8px;min-height:0;overflow:auto;padding:8px;border:1px solid #e4e7ef;border-radius:14px;background:#f7f8fb;box-shadow:0 18px 46px rgba(15,23,42,.18)}' +
'.wolai-page-ai-pi-lab-body[data-rail-open="true"] .wolai-page-ai-pi-lab-rail{display:flex}' +
'.wolai-page-ai-pi-lab-rail-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 2px 2px;color:#42403a;font-size:12px;font-weight:650}' +
'.wolai-page-ai-pi-lab-rail-close{width:24px;height:24px;border:1px solid #e1e4eb;border-radius:7px;background:#fff;color:#4a4f58;display:grid;place-items:center;cursor:pointer}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-rail-section{border:1px solid #e5e0d8;border-radius:10px;background:#fff;overflow:hidden}' +
'.wolai-page-ai-pi-lab-rail-section>summary{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;list-style:none;padding:8px 9px;font-size:11px;font-weight:650;color:#5d594f;background:#faf9f6}' +
'.wolai-page-ai-pi-lab-rail-section>summary::-webkit-details-marker{display:none}' +
'.wolai-page-ai-pi-lab-rail-section>summary::after{content:"▸";font-size:10px;color:#8a8378}' +
'.wolai-page-ai-pi-lab-rail-section[open]>summary::after{content:"▾"}' +
'.wolai-page-ai-pi-lab-rail-count{font-weight:600;color:#8a8378}' +
'.wolai-page-ai-pi-lab-rail-title{padding:8px 9px;border-bottom:1px solid #eee9e0;font-size:11px;font-weight:650;color:#5d594f;background:#faf9f6}' +
'.wolai-page-ai-pi-lab-tool-list{display:flex;flex-direction:column;gap:5px;padding:8px}' +
'.wolai-page-ai-pi-lab-tool-pill{border:1px solid #e6e2da;border-radius:7px;background:#fff;color:#42403a;padding:5px 7px;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-artifacts{display:flex;flex-direction:column;gap:6px;padding:8px;max-height:250px;overflow:auto}' +
'.wolai-page-ai-pi-lab-artifact{border:1px solid #e6e2da;border-radius:8px;background:#fff;color:#42403a;padding:7px;font-size:11px;line-height:1.42;overflow:hidden}' +
'.wolai-page-ai-pi-lab-artifact[data-kind="citation"]{border-color:#d9e6ff;background:#f7faff}.wolai-page-ai-pi-lab-artifact[data-kind="diff"]{border-color:#f0dcb7;background:#fff9ed}.wolai-page-ai-pi-lab-artifact[data-kind="mcp"]{border-color:#dfe6f2;background:#fafcff}' +
'.wolai-page-ai-pi-lab-artifact strong{display:block;font-size:11px;font-weight:700;color:#2f343b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wolai-page-ai-pi-lab-artifact span{display:block;margin-top:3px;color:#6f6a60;word-break:break-word}.wolai-page-ai-pi-lab-artifact-actions{display:flex;flex-wrap:wrap;gap:5px}.wolai-page-ai-pi-lab-artifact button,.wolai-page-ai-pi-lab-artifact a,.wolai-page-ai-pi-lab-rail-action{display:inline-flex;align-items:center;height:24px;margin-top:6px;border:1px solid #e1e4eb;border-radius:7px;background:#fff;color:#315d9f;text-decoration:none;padding:0 7px;font-size:11px;cursor:pointer}' +
'.wolai-page-ai-pi-lab-rail-action{margin:8px 8px 0}.wolai-page-ai-pi-lab-rail-action:hover,.wolai-page-ai-pi-lab-artifact button:hover,.wolai-page-ai-pi-lab-artifact a:hover{background:#f8fafc}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-receipts{display:flex;flex-direction:column;gap:5px;padding:8px;max-height:170px;overflow:auto}' +
'.wolai-page-ai-pi-lab-receipt{border-radius:7px;background:#f8f7f3;border:1px solid #e6e2da;padding:6px;font-size:11px;color:#4d4941}' +
'.wolai-page-ai-pi-lab-receipt[data-allowed="false"]{background:#fff0ee;border-color:#f2c4bd;color:#9f352c}' +
2026-07-10 10:54:34 +08:00
'.wolai-page-ai-pi-lab-ui-backdrop{position:relative;z-index:12;display:block;padding:10px 12px 0;background:#fff;pointer-events:none}' +
'.wolai-page-ai-pi-lab-ui-dialog{width:100%;border:1px solid #dfe4ec;border-radius:12px;background:#fff;box-shadow:0 10px 28px rgba(15,23,42,.12);overflow:hidden;color:#242424;pointer-events:auto}' +
'.wolai-page-ai-pi-lab-ui-dialog[data-collapsed="true"] .wolai-page-ai-pi-lab-ui-dialog-body,.wolai-page-ai-pi-lab-ui-dialog[data-collapsed="true"] .wolai-page-ai-pi-lab-ui-dialog-actions{display:none}' +
'.wolai-page-ai-pi-lab-ui-dialog-header{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:start;padding:11px 12px 9px;border-bottom:1px solid #edf0f5;background:#fafbfc}.wolai-page-ai-pi-lab-ui-dialog-header strong{display:block;font-size:13px;font-weight:720;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wolai-page-ai-pi-lab-ui-dialog-header span{display:block;margin-top:3px;font-size:12px;color:#6f6a60;white-space:pre-wrap}.wolai-page-ai-pi-lab-ui-collapse{width:28px;height:28px;border:1px solid #e1e4eb;border-radius:8px;background:#fff;color:#4a4f58;cursor:pointer}' +
'.wolai-page-ai-pi-lab-ui-dialog-body{padding:10px 12px;display:flex;flex-direction:column;gap:8px;max-height:min(320px,38vh);overflow:auto}.wolai-page-ai-pi-lab-ui-dialog-body input,.wolai-page-ai-pi-lab-ui-dialog-body textarea{width:100%;box-sizing:border-box;border:1px solid #ddd8cf;border-radius:8px;background:#fff;color:#242424;padding:8px 9px;font:13px/1.45 inherit;outline:none}.wolai-page-ai-pi-lab-ui-dialog-body textarea{min-height:76px;resize:vertical}' +
'.wolai-page-ai-pi-lab-ui-question{border:1px solid #edf0f5;border-radius:10px;background:#fff;padding:9px}.wolai-page-ai-pi-lab-ui-question-title{font-size:12px;font-weight:700;color:#2f343b;margin-bottom:3px}.wolai-page-ai-pi-lab-ui-question-prompt{font-size:12px;color:#6f6a60;white-space:pre-wrap;margin-bottom:7px}.wolai-page-ai-pi-lab-ui-option{width:100%;min-height:34px;border:1px solid #e1e4eb;border-radius:8px;background:#fff;color:#242424;text-align:left;padding:7px 9px;cursor:pointer;display:flex;align-items:flex-start;gap:8px}.wolai-page-ai-pi-lab-ui-option:hover,.wolai-page-ai-pi-lab-ui-option[data-selected="true"]{background:#f3f7ff;border-color:#bcd7ff}.wolai-page-ai-pi-lab-ui-option small{display:block;margin-top:2px;color:#737a86;font-size:11px;line-height:1.35}.wolai-page-ai-pi-lab-ui-option-mark{flex:0 0 auto;color:#5d6675}.wolai-page-ai-pi-lab-ui-note{border-top:1px solid #edf0f5;padding-top:8px}.wolai-page-ai-pi-lab-ui-note label{display:block;margin-bottom:4px;font-size:11px;color:#737a86}' +
'.wolai-page-ai-pi-lab-ui-dialog-actions{display:flex;justify-content:flex-end;gap:8px;padding:9px 12px 11px;border-top:1px solid #edf0f5}.wolai-page-ai-pi-lab-ui-dialog-actions button{height:30px;border:1px solid #ddd8cf;border-radius:8px;background:#fff;color:#4a4f58;padding:0 11px;cursor:pointer}.wolai-page-ai-pi-lab-ui-dialog-actions button[data-primary="true"]{border-color:#111827;background:#111827;color:#fff}' +
'.wolai-page-ai-pi-lab-ui-tui{margin:0;padding:8px 9px;border:1px solid #edf0f5;border-radius:8px;background:#fafbfc;color:#242833;white-space:pre-wrap;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:auto}.wolai-page-ai-pi-lab-ui-tui-options{display:flex;flex-direction:column;gap:6px}.wolai-page-ai-pi-lab-ui-option[data-custom-selected="true"]{background:#eef6ff;border-color:#9ec5fe}' +
'.wolai-page-ai-pi-lab-toast-stack{position:absolute;left:18px;right:18px;bottom:166px;z-index:13;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none}.wolai-page-ai-pi-lab-toast{width:min(340px,100%);border:1px solid #dbe3ef;border-radius:10px;background:#fff;color:#242424;box-shadow:0 12px 34px rgba(15,23,42,.18);padding:10px 12px;font-size:13px;line-height:1.45;pointer-events:auto}.wolai-page-ai-pi-lab-toast[data-type="warning"]{border-color:#f0dcb7;background:#fff9ed}.wolai-page-ai-pi-lab-toast[data-type="error"]{border-color:#f2c4bd;background:#fff8f7}.wolai-page-ai-pi-lab-toast[data-type="success"]{border-color:#bfe6ac;background:#f4fff0}' +
2026-07-04 21:47:42 +08:00
'.wolai-page-ai-pi-lab-diagnostics{display:none;border-top:1px solid #e6e2da;background:#fff;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-drawer[data-config-open="true"] .wolai-page-ai-pi-lab-diagnostics{display:block}' +
'.wolai-page-ai-pi-lab-diagnostics>summary{cursor:pointer;padding:8px 12px;font-size:11px;color:#6f6a60}' +
'.wolai-page-ai-pi-lab-diagnostics pre{margin:0;padding:0 12px 10px;white-space:pre-wrap;word-break:break-word;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#5d594f}' +
2026-07-10 10:54:34 +08:00
'@media (max-width: 760px){.wolai-page-ai-pi-lab-drawer{left:10px;right:10px;top:58px;bottom:10px;width:auto}.wolai-page-ai-pi-lab-history-panel{width:min(320px,calc(100vw - 34px))}.wolai-page-ai-pi-lab-rail{left:14px;right:14px;top:auto;bottom:132px;width:auto;max-height:min(360px,calc(100% - 164px))}.wolai-page-ai-pi-lab-config{grid-template-columns:1fr}}' +
'@media (max-width: 520px){.wolai-page-ai-pi-lab-topbar{height:54px;padding:0 14px}.wolai-page-ai-pi-lab-title strong{font-size:21px}.wolai-page-ai-pi-lab-commandbar{height:50px;margin:0 14px 12px;gap:6px;overflow:hidden}.wolai-page-ai-pi-lab-history-panel{top:116px;left:12px;right:12px;max-height:min(300px,calc(100% - 300px))}.wolai-page-ai-pi-lab-history-row{grid-template-columns:1fr}.wolai-page-ai-pi-lab-history-actions{justify-content:flex-start}.wolai-page-ai-pi-lab-input{min-height:88px;padding:16px 58px 10px 18px;font-size:16px}.wolai-page-ai-pi-lab-modebar{min-height:48px;flex-wrap:nowrap;gap:6px;padding:6px 10px}.wolai-page-ai-pi-lab-model-control{flex:1 1 130px;min-width:92px;max-width:none}.wolai-page-ai-pi-lab-toast-stack{left:14px;right:14px;bottom:154px}.wolai-page-ai-pi-lab-toast{width:100%}}' ;
2026-07-04 21:47:42 +08:00
document . head . appendChild ( style );
}
function escapeHtml ( str ) {
if ( str == null ) return '' ;
return String ( str )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' );
}
2026-07-10 10:54:34 +08:00
function escapeAttr ( str ) {
return escapeHtml ( str ). replace ( /`/g , '`' );
}
function renderMarkdownInline ( text ) {
var placeholders = [];
function stash ( html ) {
var key = '\u0000MD' + placeholders . length + '\u0000' ;
placeholders . push ( html );
return key ;
}
var value = escapeHtml ( text || '' );
value = value . replace ( /`([^`]+)`/g , function ( _ , code ) {
return stash ( '<code>' + code + '</code>' );
});
value = value . replace ( /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g , function ( _ , label , href ) {
return stash ( '<a href="' + escapeAttr ( href ) + '" target="_blank" rel="noopener">' + label + '</a>' );
});
value = value . replace ( /\*\*([^*]+)\*\*/g , '<strong>$1</strong>' );
value = value . replace ( /(^|[^*])\*([^*\n]+)\*/g , '$1<em>$2</em>' );
placeholders . forEach ( function ( html , index ) {
value = value . replace ( new RegExp ( '\u0000MD' + index + '\u0000' , 'g' ), html );
});
return value ;
}
function isTableDivider ( line ) {
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/ . test ( line || '' );
}
function splitTableRow ( line ) {
var trimmed = String ( line || '' ). trim ();
if ( trimmed . charAt ( 0 ) === '|' ) trimmed = trimmed . slice ( 1 );
if ( trimmed . charAt ( trimmed . length - 1 ) === '|' ) trimmed = trimmed . slice ( 0 , - 1 );
return trimmed . split ( '|' ). map ( function ( cell ) { return cell . trim (); });
}
function renderMarkdownTable ( lines , start ) {
var header = splitTableRow ( lines [ start ]);
var rows = [];
var i = start + 2 ;
while ( i < lines . length && /\|/ . test ( lines [ i ] || '' ) && String ( lines [ i ] || '' ). trim ()) {
rows . push ( splitTableRow ( lines [ i ]));
i += 1 ;
}
return {
html : '<table><thead><tr>' + header . map ( function ( cell ) {
return '<th>' + renderMarkdownInline ( cell ) + '</th>' ;
}). join ( '' ) + '</tr></thead><tbody>' + rows . map ( function ( row ) {
return '<tr>' + header . map ( function ( _ , index ) {
return '<td>' + renderMarkdownInline ( row [ index ] || '' ) + '</td>' ;
}). join ( '' ) + '</tr>' ;
}). join ( '' ) + '</tbody></table>' ,
next : i ,
};
}
function renderMarkdownBlock ( text ) {
var lines = String ( text || '' ). replace ( /\r\n/g , '\n' ). split ( '\n' );
var html = [];
var paragraph = [];
var list = null ;
var inCode = false ;
var codeLines = [];
var codeLang = '' ;
function flushParagraph () {
if ( ! paragraph . length ) return ;
html . push ( '<p>' + renderMarkdownInline ( paragraph . join ( ' ' )) + '</p>' );
paragraph = [];
}
function flushList () {
if ( ! list ) return ;
html . push ( '<' + list . type + '>' + list . items . map ( function ( item ) {
return '<li>' + renderMarkdownInline ( item ) + '</li>' ;
}). join ( '' ) + '</' + list . type + '>' );
list = null ;
}
for ( var i = 0 ; i < lines . length ; i += 1 ) {
var line = lines [ i ];
var trimmed = line . trim ();
var fence = trimmed . match ( /^```([A-Za-z0-9_-]*)\s*$/ );
if ( fence ) {
if ( inCode ) {
html . push ( '<pre data-lang="' + escapeAttr ( codeLang ) + '"><code>' + escapeHtml ( codeLines . join ( '\n' )) + '</code></pre>' );
inCode = false ;
codeLines = [];
codeLang = '' ;
} else {
flushParagraph ();
flushList ();
inCode = true ;
codeLang = fence [ 1 ] || '' ;
}
continue ;
}
if ( inCode ) {
codeLines . push ( line );
continue ;
}
if ( ! trimmed ) {
flushParagraph ();
flushList ();
continue ;
}
if ( i + 1 < lines . length && /\|/ . test ( line ) && isTableDivider ( lines [ i + 1 ])) {
flushParagraph ();
flushList ();
var renderedTable = renderMarkdownTable ( lines , i );
html . push ( renderedTable . html );
i = renderedTable . next - 1 ;
continue ;
}
var heading = trimmed . match ( /^(#{1,3})\s+(.+)$/ );
if ( heading ) {
flushParagraph ();
flushList ();
html . push ( '<h' + heading [ 1 ]. length + '>' + renderMarkdownInline ( heading [ 2 ]) + '</h' + heading [ 1 ]. length + '>' );
continue ;
}
var quote = trimmed . match ( /^>\s?(.*)$/ );
if ( quote ) {
flushParagraph ();
flushList ();
html . push ( '<blockquote>' + renderMarkdownInline ( quote [ 1 ]) + '</blockquote>' );
continue ;
}
var unordered = trimmed . match ( /^[-*+]\s+(.+)$/ );
var ordered = trimmed . match ( /^\d+\.\s+(.+)$/ );
if ( unordered || ordered ) {
flushParagraph ();
var type = ordered ? 'ol' : 'ul' ;
if ( ! list || list . type !== type ) flushList ();
if ( ! list ) list = { type : type , items : [] };
list . items . push (( unordered || ordered )[ 1 ]);
continue ;
}
paragraph . push ( trimmed );
}
if ( inCode ) html . push ( '<pre data-lang="' + escapeAttr ( codeLang ) + '"><code>' + escapeHtml ( codeLines . join ( '\n' )) + '</code></pre>' );
flushParagraph ();
flushList ();
return html . join ( '' );
}
2026-07-04 21:47:42 +08:00
function el ( tag , attrs , children ) {
var elem = document . createElement ( tag );
if ( attrs ) {
Object . keys ( attrs ). forEach ( function ( key ) {
if ( key === 'className' ) elem . className = attrs [ key ];
else if ( key === 'textContent' ) elem . textContent = attrs [ key ];
else if ( key === 'innerHTML' ) elem . innerHTML = attrs [ key ];
else if ( key === 'style' && attrs [ key ] && typeof attrs [ key ] === 'object' ) Object . keys ( attrs [ key ]). forEach ( function ( styleKey ) { elem . style [ styleKey ] = attrs [ key ][ styleKey ]; });
else elem . setAttribute ( key , attrs [ key ]);
});
}
if ( children ) {
( Array . isArray ( children ) ? children : [ children ]). forEach ( function ( child ) {
if ( typeof child === 'string' ) elem . appendChild ( document . createTextNode ( child ));
else if ( child instanceof Node ) elem . appendChild ( child );
});
}
return elem ;
}
2026-07-10 10:54:34 +08:00
function compactJson ( value ) {
if ( value == null || value === '' ) return '' ;
if ( typeof value === 'string' ) return value ;
try {
return JSON . stringify ( value , null , 2 );
} catch ( _ ) {
return String ( value );
}
}
function extractResultText ( value ) {
if ( value == null ) return '' ;
if ( typeof value === 'string' ) return value ;
var content = value . content ;
if ( Array . isArray ( content )) {
var text = content . map ( function ( part ) {
if ( ! part ) return '' ;
if ( typeof part === 'string' ) return part ;
if ( typeof part . text === 'string' ) return part . text ;
return '' ;
}). filter ( Boolean ). join ( '' );
if ( text ) return text ;
}
return compactJson ( value );
}
function truncateSingleLine ( value , limit ) {
var text = String ( value || '' ). replace ( /\s+/g , ' ' ). trim ();
var max = limit || 120 ;
return text . length > max ? text . slice ( 0 , max - 1 ) + '…' : text ;
}
function copyTextToClipboard ( text ) {
text = String ( text || '' );
if ( ! text ) return Promise . resolve ( false );
if ( navigator . clipboard && navigator . clipboard . writeText ) {
return navigator . clipboard . writeText ( text ). then ( function () { return true ; }). catch ( function () { return false ; });
}
return Promise . resolve ( false );
}
function messageToMarkdown ( msg ) {
if ( ! msg ) return '' ;
var lines = [];
if ( msg . text ) lines . push ( String ( msg . text ));
if ( Array . isArray ( msg . citations ) && msg . citations . length ) {
lines . push ( '' , 'References:' );
msg . citations . forEach ( function ( cit , index ) {
lines . push (( index + 1 ) + '. ' + ( cit . title || cit . source || 'Citation' ) + ( cit . url ? ' - ' + cit . url : '' ));
});
}
if ( msg . diffSummary && Array . isArray ( msg . diffSummary . files ) && msg . diffSummary . files . length ) {
lines . push ( '' , 'Changed files:' );
msg . diffSummary . files . forEach ( function ( file ) { lines . push ( '- ' + file ); });
}
if ( Array . isArray ( msg . toolCalls ) && msg . toolCalls . length ) {
lines . push ( '' , 'Tool calls:' );
msg . toolCalls . forEach ( function ( tc ) {
lines . push ( '- ' + toolDisplayName ({ toolName : tc . name , args : tc . args , details : tc . details }) + ' · ' + ( tc . status || '' ));
});
}
return lines . join ( '\n' ). trim ();
}
function currentSessionEventsUrl () {
if ( ! piLabState . sessionId ) return '' ;
return API . SESSIONS + '/' + encodeURIComponent ( piLabState . sessionId ) + '/events?limit=1000' ;
}
function openCurrentSessionJsonl () {
var url = currentSessionEventsUrl ();
if ( ! url ) {
showPiToast ( 'No Pi session to open' , 'warning' );
return Promise . resolve ( false );
}
return fetch ( url , { credentials : 'same-origin' , cache : 'no-store' })
. then ( function ( response ) {
if ( ! response . ok ) throw new Error ( 'Session events failed: ' + response . status );
return response . json ();
})
. then ( function ( payload ) {
var lines = ( Array . isArray ( payload . events ) ? payload . events : []). map ( function ( event ) {
return JSON . stringify ({
eventType : event . eventType ,
createdAt : event . createdAt ,
payload : event . payload || {},
});
}). join ( '\n' );
var blob = new Blob ([ lines + ( lines ? '\n' : '' )], { type : 'application/x-ndjson;charset=utf-8' });
var objectUrl = URL . createObjectURL ( blob );
window . open ( objectUrl , '_blank' , 'noopener' );
setTimeout ( function () { URL . revokeObjectURL ( objectUrl ); }, 30000 );
showPiToast ( 'Session JSONL opened' , 'success' );
return true ;
})
. catch ( function ( error ) {
updateDiagnostics ( error . message || '打开 session JSONL 失败' );
showPiToast ( 'Open session JSONL failed' , 'error' );
return false ;
});
}
function normalizeToolCallId ( payload ) {
payload = payload || {};
return String ( payload . toolCallId || payload . tool_call_id || payload . id || payload . callId || '' ). trim ();
}
function normalizeToolName ( payload ) {
payload = payload || {};
return String ( payload . toolName || payload . tool_name || payload . name || payload . tool || 'tool' ). trim ();
}
function parseMcpArgs ( args ) {
if ( ! args ) return {};
if ( typeof args === 'string' ) {
try {
return JSON . parse ( args );
} catch ( _ ) {
return { raw : args };
}
}
return args ;
}
function toolDisplayName ( payload ) {
var name = normalizeToolName ( payload );
var args = parseMcpArgs ( payload && payload . args );
var details = payload && payload . details ? payload . details : {};
var server = String ( args . server || details . server || '' ). trim ();
var tool = String ( args . tool || details . tool || '' ). trim ();
if ( name === 'mcp' && server ) return 'mcp:' + server + ( tool ? '/' + tool : '/list' );
return name ;
}
function assistantToolCallPayload ( msgData ) {
if ( ! msgData ) return null ;
var toolCall = msgData . toolCall || null ;
if ( ! toolCall && msgData . partial && msgData . partial . type === 'toolCall' ) toolCall = msgData . partial ;
if ( ! toolCall && msgData . toolName ) toolCall = msgData ;
if ( ! toolCall && msgData . name ) toolCall = msgData ;
if ( ! toolCall || ! ( toolCall . name || toolCall . toolName || toolCall . tool_name || toolCall . id || toolCall . toolCallId )) return null ;
return {
toolCallId : toolCall . toolCallId || toolCall . id || msgData . toolCallId || msgData . id || '' ,
toolName : toolCall . toolName || toolCall . tool_name || toolCall . name || msgData . toolName || msgData . name || '' ,
args : toolCall . arguments || toolCall . args || msgData . arguments || msgData . args || {},
details : msgData . details || null ,
result : msgData . result || null ,
};
}
function ensureToolCalls ( msg ) {
if ( ! msg . toolCalls ) msg . toolCalls = [];
return msg . toolCalls ;
}
function upsertToolCall ( raw , status , patch ) {
var viewModel = applyPiRunEvent ({
type : 'tool_upsert' ,
payload : {
raw : raw || {},
status : status || 'running' ,
patch : patch || {},
},
});
updateMessages ();
return viewModel . lastToolCall ;
}
function renderToolCallCard ( tc ) {
var displayName = toolDisplayName ({ toolName : tc . name , args : tc . args , details : tc . details });
var resultText = extractResultText ( tc . result || tc . partialResult || '' );
var summaryParts = [];
if ( tc . args && Object . keys ( tc . args ). length ) {
var args = parseMcpArgs ( tc . args );
if ( args . server ) summaryParts . push ( 'server=' + args . server );
if ( args . tool ) summaryParts . push ( 'tool=' + args . tool );
}
if ( resultText ) summaryParts . push ( truncateSingleLine ( resultText , 140 ));
var item = el ( 'div' , {
className : 'wolai-page-ai-pi-lab-tool-call' ,
'data-page-ai-pi-lab-tool-card' : tc . id || displayName || 'tool' ,
'data-page-ai-pi-lab-tool-call' : displayName || 'tool' ,
'data-status' : tc . status || '' ,
});
var policy = tc . toolPolicy || ( tc . approvalRequired ? 'ask' : '' );
var policyBadge = policy ? '<em class="wolai-page-ai-pi-lab-tool-policy" data-policy="' + escapeHtml ( policy ) + '" data-approved="' +
( tc . approvalRequired ? String ( tc . approvalConfirmed === true ) : '' ) + '">' +
escapeHtml ( tc . approvalRequired ? ( tc . approvalConfirmed ? 'approved' : 'approval required' ) : policy ) + '</em>' : '' ;
item . appendChild ( el ( 'div' , {
className : 'wolai-page-ai-pi-lab-tool-call-header' ,
'data-page-ai-pi-lab-tool-summary' : 'true' ,
innerHTML : '<strong>' + escapeHtml ( displayName || 'tool' ) + policyBadge + '</strong><span data-page-ai-pi-lab-tool-status>' + escapeHtml ( tc . status || '' ) + '</span>' ,
}));
if ( summaryParts . length ) {
item . appendChild ( el ( 'div' , {
className : 'wolai-page-ai-pi-lab-tool-call-summary-line' ,
'data-page-ai-pi-lab-tool-compact-summary' : 'true' ,
}, summaryParts . join ( ' · ' )));
}
var hasDetails = ( tc . args && Object . keys ( tc . args ). length ) || ( tc . details && Object . keys ( tc . details ). length ) || tc . partialResult || tc . result ;
if ( hasDetails ) {
var details = el ( 'details' , {
className : 'wolai-page-ai-pi-lab-tool-call-details' ,
'data-page-ai-pi-lab-tool-details' : displayName || 'tool' ,
});
details . appendChild ( el ( 'summary' , {}, 'Input / Output / Raw' ));
if ( tc . args && Object . keys ( tc . args ). length ) details . appendChild ( el ( 'pre' , { textContent : 'Input\n' + compactJson ( tc . args ) }));
if ( tc . details && Object . keys ( tc . details ). length ) details . appendChild ( el ( 'pre' , { textContent : 'Details\n' + compactJson ( tc . details ) }));
if ( tc . partialResult && tc . status === 'running' ) details . appendChild ( el ( 'pre' , { textContent : 'Partial output\n' + extractResultText ( tc . partialResult ) }));
if ( tc . result ) details . appendChild ( el ( 'pre' , { textContent : 'Output\n' + resultText }));
item . appendChild ( details );
}
return item ;
}
2026-07-04 21:47:42 +08:00
function normalizeSlashes ( value ) {
return String ( value || '' ). trim (). replace ( /\\/g , '/' );
}
function currentUrl () {
try {
return new URL ( window . location . href );
} catch ( _ ) {
return new URL ( '/' , window . location . origin );
}
}
function currentDocumentId () {
var fromBody = document . body && document . body . dataset ? String ( document . body . dataset . documentId || '' ). trim () : '' ;
if ( fromBody ) return fromBody ;
var pageTab = document . querySelector ( '[data-mnote-main-tab="page"]' );
if ( pageTab instanceof HTMLElement ) {
var tabDocumentId = String ( pageTab . getAttribute ( 'data-document-id' ) || '' ). trim ();
if ( tabDocumentId ) return tabDocumentId ;
}
var match = currentUrl (). pathname . match ( /^\/documents\/([^/]+)/ );
2026-07-10 10:54:34 +08:00
if ( match ) return decodeURIComponent ( match [ 1 ]);
var missingPage = String ( currentUrl (). searchParams . get ( 'missingPage' ) || currentUrl (). searchParams . get ( 'pageId' ) || '' ). trim ();
return missingPage ? missingPage : '' ;
2026-07-04 21:47:42 +08:00
}
function currentWorkspaceId () {
return currentUrl (). searchParams . get ( 'workspaceId' ) || ( document . body && document . body . dataset ? String ( document . body . dataset . workspaceId || '' ). trim () : '' );
}
function currentRootUri () {
2026-07-10 10:54:34 +08:00
var fromUrl = String ( currentUrl (). searchParams . get ( 'rootUri' ) || '' ). trim ();
if ( fromUrl ) return fromUrl ;
var fromBody = document . body instanceof HTMLElement ? String ( document . body . getAttribute ( 'data-mnote-root-uri' ) || '' ). trim () : '' ;
if ( fromBody ) return fromBody ;
return String (( piLabState . session && piLabState . session . rootUri ) || '' ). trim ();
2026-07-04 21:47:42 +08:00
}
function localMarkdownRelativePathFromDocumentId ( documentId ) {
var value = String ( documentId || '' ). trim ();
if ( ! value . startsWith ( 'local-md:' )) return '' ;
2026-07-10 10:54:34 +08:00
var encoded = value . slice ( 'local-md:' . length );
try {
return decodeURIComponent ( encoded . replace ( /~([0-9A-Fa-f]{2})/g , '%$1' ));
} catch ( _ ) {
return encoded . replace ( /~2F/gi , '/' );
}
}
function cssEscape ( value ) {
var text = String ( value || '' );
if ( window . CSS && typeof window . CSS . escape === 'function' ) return window . CSS . escape ( text );
return text . replace ( /["\\]/g , '\\$&' );
2026-07-04 21:47:42 +08:00
}
function currentSelectionText () {
try {
var selection = window . getSelection ? window . getSelection () : null ;
return selection ? String ( selection . toString () || '' ). trim () : '' ;
} catch ( _ ) {
return '' ;
}
}
function activeEditorSnapshot () {
try {
var snapshot = window . __mnoteDocumentPaneRuntime
&& typeof window . __mnoteDocumentPaneRuntime . getOpenEditorsSnapshot === 'function'
? window . __mnoteDocumentPaneRuntime . getOpenEditorsSnapshot ()
: null ;
if ( ! snapshot || typeof snapshot !== 'object' ) return null ;
if ( snapshot . activeEditor ) return snapshot . activeEditor ;
var entries = []
. concat ( Array . isArray ( snapshot . editors ) ? snapshot . editors : [])
. concat ( Array . isArray ( snapshot . resourceEditors ) ? snapshot . resourceEditors : []);
return entries . find ( function ( entry ) { return entry && entry . active === true ; }) || entries [ 0 ] || null ;
} catch ( _ ) {
return null ;
}
}
2026-07-10 10:54:34 +08:00
function activeFileTreeWorkspacePath ( documentId ) {
try {
var relativePath = localMarkdownRelativePathFromDocumentId ( documentId );
var selector = '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]' ;
if ( relativePath ) selector += '[data-local-relative-path="' + cssEscape ( relativePath ) + '"]' ;
var row = document . querySelector ( selector );
var reader = window . __mnoteFileTreeRuntime && typeof window . __mnoteFileTreeRuntime . readWorkspacePathFromRow === 'function'
? window . __mnoteFileTreeRuntime . readWorkspacePathFromRow
: null ;
if ( row instanceof HTMLElement && reader ) {
var workspacePath = reader ( row );
if ( workspacePath && typeof workspacePath === 'object' ) {
return Object . assign ({}, workspacePath , {
relativePath : workspacePath . relativePath || row . getAttribute ( 'data-local-relative-path' ) || '' ,
rootUri : workspacePath . rootUri || row . getAttribute ( 'data-root-uri' ) || '' ,
sourceKind : workspacePath . sourceKind || row . getAttribute ( 'data-source-kind' ) || '' ,
documentId : workspacePath . documentId || row . getAttribute ( 'data-document-id' ) || documentId || '' ,
});
}
}
if ( row instanceof HTMLElement ) {
return {
relativePath : row . getAttribute ( 'data-local-relative-path' ) || '' ,
rootUri : row . getAttribute ( 'data-root-uri' ) || '' ,
sourceKind : row . getAttribute ( 'data-source-kind' ) || '' ,
documentId : row . getAttribute ( 'data-document-id' ) || documentId || '' ,
};
}
} catch ( _ ) {}
return null ;
}
2026-07-04 21:47:42 +08:00
function currentPageContext () {
var active = activeEditorSnapshot () || {};
2026-07-10 10:54:34 +08:00
var activeWorkspacePath = active . workspacePath && typeof active . workspacePath === 'object' ? active . workspacePath : {};
var documentId = String ( activeWorkspacePath . documentId || active . documentId || currentDocumentId () || '' ). trim ();
var fileTreeWorkspacePath = activeFileTreeWorkspacePath ( documentId ) || {};
var workspacePath = Object . assign ({}, activeWorkspacePath , fileTreeWorkspacePath );
var session = piLabState . session || {};
var pagePath = normalizeSlashes ( workspacePath . relativePath || workspacePath . path || active . relativePath || active . path || localMarkdownRelativePathFromDocumentId ( documentId ) || session . pagePath || '' );
var rootUri = String ( workspacePath . rootUri || active . rootUri || currentRootUri () || session . rootUri || '' ). trim ();
var workspaceId = String ( workspacePath . workspaceId || active . workspaceId || currentWorkspaceId () || session . workspaceId || '' ). trim ();
2026-07-04 21:47:42 +08:00
var titleNode = document . querySelector ( '[data-page-title-current="true"]' ) || document . querySelector ( '.wolai-breadcrumb-current' );
var title = String ( active . title || workspacePath . title || ( titleNode && titleNode . textContent ) || document . title || '' ). trim ();
var selection = currentSelectionText ();
return {
pageId : documentId ,
pagePath : pagePath ,
pageTitle : title ,
rootUri : rootUri ,
workspaceId : workspaceId ,
selection : selection ,
};
}
2026-07-10 10:54:34 +08:00
function currentFolderPathFromPagePath ( pagePath ) {
var normalized = normalizeSlashes ( pagePath || '' ). replace ( /^\/+/ , '' );
if ( ! normalized ) return null ;
var parts = normalized . split ( '/' ). filter ( Boolean );
if ( parts . length <= 1 ) return '' ;
parts . pop ();
return parts . join ( '/' );
}
function contextSelectionState () {
if ( ! piLabState . contextSelection || typeof piLabState . contextSelection !== 'object' ) {
piLabState . contextSelection = {};
}
if ( typeof piLabState . contextSelection . currentPage !== 'boolean' ) piLabState . contextSelection . currentPage = false ;
if ( typeof piLabState . contextSelection . currentFolder !== 'boolean' ) piLabState . contextSelection . currentFolder = false ;
if ( typeof piLabState . contextSelection . selection !== 'boolean' ) piLabState . contextSelection . selection = false ;
if ( typeof piLabState . contextSelection . lightrag !== 'boolean' ) piLabState . contextSelection . lightrag = false ;
return piLabState . contextSelection ;
}
function selectedContextRefs () {
var selection = contextSelectionState ();
var refs = [];
if ( selection . currentPage ) refs . push ( 'current_page' );
if ( selection . currentFolder ) refs . push ( 'folder' );
if ( selection . selection ) refs . push ( 'selection' );
if ( selection . lightrag ) refs . push ( 'lightrag' );
return refs ;
}
function selectedContextPayload ( context ) {
context = context || currentPageContext ();
var selection = contextSelectionState ();
var folderPath = currentFolderPathFromPagePath ( context . pagePath );
return {
refs : selectedContextRefs (),
currentPage : selection . currentPage ? {
rootUri : context . rootUri || null ,
workspaceId : context . workspaceId || null ,
pagePath : context . pagePath || null ,
pageTitle : context . pageTitle || null ,
} : null ,
currentFolder : selection . currentFolder ? {
rootUri : context . rootUri || null ,
workspaceId : context . workspaceId || null ,
folderPath : folderPath === null ? null : folderPath ,
} : null ,
selection : selection . selection ? {
text : context . selection || piLabState . selectionText || '' ,
source : 'mnote_sidebar_host' ,
} : null ,
lightrag : selection . lightrag ? {
enabled : true ,
provider : 'lightrag' ,
} : null ,
};
}
2026-07-04 21:47:42 +08:00
function refreshSelectionSummary () {
var selection = currentSelectionText ();
if ( selection ) piLabState . selectionText = selection ;
var visibleSelection = selection || piLabState . selectionText ;
piLabState . selectionSummary = visibleSelection ? ( '已选中 ' + visibleSelection . length + ' 字' ) : '' ;
return visibleSelection ;
}
function applyCurrentContextToState () {
var context = currentPageContext ();
refreshSelectionSummary ();
if ( ! piLabState . session ) piLabState . session = {};
2026-07-10 10:54:34 +08:00
if ( context . pagePath ) piLabState . session . pagePath = context . pagePath ;
if ( context . pageTitle ) piLabState . session . pageTitle = context . pageTitle ;
if ( context . rootUri ) piLabState . session . rootUri = context . rootUri ;
if ( context . workspaceId ) piLabState . session . workspaceId = context . workspaceId ;
var folderPath = currentFolderPathFromPagePath ( context . pagePath );
if ( folderPath !== null ) piLabState . session . folderPath = folderPath ;
2026-07-04 21:47:42 +08:00
updateContextStrip ();
return context ;
}
2026-07-10 10:54:34 +08:00
function setContextQuickActive ( kind , active ) {
var selection = contextSelectionState ();
if ( kind === 'read-page' ) selection . currentPage = active !== false ;
if ( kind === 'current-folder' ) selection . currentFolder = active !== false ;
if ( kind === 'selection' ) selection . selection = active !== false ;
if ( kind === 'rag' ) selection . lightrag = active !== false ;
updateContextStrip ();
updateButtons ();
}
function toggleContextQuick ( kind ) {
var selection = contextSelectionState ();
var current = false ;
if ( kind === 'read-page' ) current = selection . currentPage ;
else if ( kind === 'current-folder' ) current = selection . currentFolder ;
else if ( kind === 'selection' ) current = selection . selection ;
else if ( kind === 'rag' ) current = selection . lightrag ;
setContextQuickActive ( kind , ! current );
}
2026-07-04 21:47:42 +08:00
function currentModelLabel () {
var provider = piLabState . modelProvider || piLabState . defaultModelProvider || DEFAULT_MODEL_PROVIDER ;
var modelId = piLabState . modelId || piLabState . defaultModelId || DEFAULT_MODEL_ID ;
if ( String ( modelId ). indexOf ( provider + '/' ) === 0 ) modelId = String ( modelId ). slice ( provider . length + 1 );
return [ provider , modelId ]. filter ( Boolean ). join ( '/' );
}
2026-07-10 10:54:34 +08:00
function normalizeThinkingLevel ( value ) {
var level = String ( value || '' ). trim (). toLowerCase ();
return [ 'off' , 'minimal' , 'low' , 'medium' , 'high' , 'xhigh' ]. indexOf ( level ) >= 0 ? level : DEFAULT_THINKING_LEVEL ;
}
function currentThinkingLabel () {
var labels = {
off : '关' ,
minimal : '极低' ,
low : '低' ,
medium : '中' ,
high : '高' ,
xhigh : '最高' ,
};
return labels [ normalizeThinkingLevel ( piLabState . thinkingLevel || piLabState . defaultThinkingLevel )] || '中' ;
}
function normalizePermissionMode ( value ) {
var mode = String ( value || '' ). trim (). toLowerCase ();
if ( mode === 'ask' ) return 'confirm' ;
return [ 'confirm' , 'auto_edit' , 'plan' , 'full_access' ]. indexOf ( mode ) >= 0 ? mode : DEFAULT_PERMISSION_MODE ;
}
function permissionModeOptions () {
return [
{ mode : 'confirm' , icon : 'status' , label : '变更前确认' , description : '改文件、执行命令前先问我。' },
{ mode : 'auto_edit' , icon : 'zap' , label : '自动编辑' , description : '授权目录内读写自动放行。' },
{ mode : 'plan' , icon : 'settings' , label : '计划模式' , description : '只读分析,禁止写入和命令。' },
{ mode : 'full_access' , icon : 'shield' , label : '完全访问' , description : '本会话内尽量不再确认。' },
];
}
function permissionModeLabel ( mode ) {
mode = normalizePermissionMode ( mode );
var found = permissionModeOptions (). filter ( function ( item ) { return item . mode === mode ; })[ 0 ];
return found ? found . label : '变更前确认' ;
}
function currentRuntimePolicy () {
return ( piLabState . session && ( piLabState . session . runtimePolicySnapshot || piLabState . session . runtimePolicy )) || {};
}
function toolPolicyValues () {
var policy = currentRuntimePolicy ();
var policies = policy . mnoteToolPolicies || policy . mnote_tool_policies || {};
return Object . keys ( policies ). map ( function ( key ) { return String ( policies [ key ] || '' ). toLowerCase (); }). filter ( Boolean );
}
function permissionSummary () {
var selectedMode = normalizePermissionMode ( piLabState . permissionMode );
if ( selectedMode === 'plan' ) return { label : '计划模式' , tone : 'warn' };
if ( selectedMode === 'auto_edit' ) return { label : '自动编辑' , tone : 'ok' };
if ( selectedMode === 'full_access' ) return { label : '完全访问' , tone : 'ok' };
var values = toolPolicyValues ();
var hasAsk = values . indexOf ( 'ask' ) >= 0 ;
var hasDeny = values . indexOf ( 'deny' ) >= 0 ;
var rootsText = String ( piLabState . allowedRootsSummary || '' ). toLowerCase ();
var readonly = rootsText . indexOf ( 'read' ) >= 0 || rootsText . indexOf ( '只读' ) >= 0 ;
if ( hasDeny || readonly ) return { label : hasAsk ? '受限/审批' : '受限' , tone : 'danger' };
if ( hasAsk || ! values . length ) return { label : '变更前确认' , tone : 'warn' };
return { label : '自动工具' , tone : 'ok' };
}
function updateThinkingControl () {
if ( ! piLabPanelEl ) return ;
var thinkingSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-thinking]' );
var level = normalizeThinkingLevel ( piLabState . thinkingLevel || piLabState . defaultThinkingLevel );
piLabState . thinkingLevel = level ;
if ( thinkingSelect ) {
if ( thinkingSelect . value !== level ) thinkingSelect . value = level ;
thinkingSelect . title = '思考深度:' + currentThinkingLabel () + ';启动本次 Pi 会话时应用' ;
}
}
function updatePermissionControl () {
if ( ! piLabPanelEl ) return ;
var control = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-permission]' );
var label = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-permission-label]' );
var wrap = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-permission-wrap]' );
var summary = permissionSummary ();
if ( label ) label . textContent = summary . label ;
if ( control ) {
control . setAttribute ( 'data-tone' , summary . tone || '' );
control . setAttribute ( 'data-active' , piLabState . permissionMenuOpen ? 'true' : 'false' );
control . title = 'Pi 模式:' + permissionModeLabel ( piLabState . permissionMode ) + ';目录范围:' + ( piLabState . allowedRootsSummary || 'pending' );
}
if ( wrap ) wrap . setAttribute ( 'data-open' , piLabState . permissionMenuOpen ? 'true' : 'false' );
Array . prototype . slice . call ( piLabPanelEl . querySelectorAll ( '[data-page-ai-pi-lab-permission-mode]' )). forEach ( function ( node ) {
node . setAttribute ( 'data-active' , node . getAttribute ( 'data-page-ai-pi-lab-permission-mode' ) === normalizePermissionMode ( piLabState . permissionMode ) ? 'true' : 'false' );
var check = node . querySelector ( '[data-page-ai-pi-lab-permission-check]' );
if ( check ) check . textContent = node . getAttribute ( 'data-active' ) === 'true' ? '✓' : '' ;
});
}
function setPermissionMenuOpen ( open ) {
piLabState . permissionMenuOpen = !! open ;
updatePermissionControl ();
}
function selectPermissionMode ( mode ) {
piLabState . permissionMode = normalizePermissionMode ( mode );
setPermissionMenuOpen ( false );
updatePermissionControl ();
if ( piLabState . sessionId && piLabState . status !== STATE_STREAMING && piLabState . status !== STATE_STARTING ) {
return applyPermissionModeToRuntime ();
}
if ( piLabState . status === STATE_STREAMING ) {
piLabState . pendingPermissionModeApply = true ;
showPiToast ( '当前回复结束后自动应用:' + permissionModeLabel ( piLabState . permissionMode ), 'warning' );
return Promise . resolve ({ queued : true });
}
showPiToast ( 'Pi 模式已切换为:' + permissionModeLabel ( piLabState . permissionMode ), 'info' );
return Promise . resolve ({ queued : false });
}
function maybeApplyPendingPermissionMode () {
if ( ! piLabState . pendingPermissionModeApply || piLabState . applyingPermissionMode ) return ;
if ( ! piLabState . sessionId || ! piLabState . enabled ) return ;
if ( piLabState . status === STATE_STREAMING || piLabState . status === STATE_STARTING ) return ;
window . setTimeout ( function () {
if ( ! piLabState . pendingPermissionModeApply || piLabState . applyingPermissionMode ) return ;
if ( piLabState . status === STATE_STREAMING || piLabState . status === STATE_STARTING ) return ;
applyPermissionModeToRuntime (). catch ( function () {});
}, 0 );
}
function applyPermissionModeToRuntime () {
if ( ! piLabState . sessionId || ! piLabState . enabled ) return Promise . resolve ({ skipped : true });
if ( piLabState . applyingPermissionMode ) {
piLabState . pendingPermissionModeApply = true ;
return Promise . resolve ({ queued : true });
}
var context = applyCurrentContextToState ();
var nextMode = normalizePermissionMode ( piLabState . permissionMode );
piLabState . pendingPermissionModeApply = false ;
piLabState . applyingPermissionMode = true ;
setState ( STATE_STARTING );
updateDiagnostics ( 'Applying Pi mode' );
return fetch ( API . START , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({
sessionId : piLabState . sessionId ,
rootUri : context . rootUri || ( piLabState . session && piLabState . session . rootUri ) || undefined ,
workspaceId : context . workspaceId || ( piLabState . session && piLabState . session . workspaceId ) || undefined ,
pagePath : context . pagePath || ( piLabState . session && piLabState . session . pagePath ) || undefined ,
pageTitle : context . pageTitle || ( piLabState . session && piLabState . session . pageTitle ) || undefined ,
modelProvider : piLabState . modelProvider || piLabState . defaultModelProvider ,
modelId : piLabState . modelId || piLabState . defaultModelId ,
thinkingLevel : normalizeThinkingLevel ( piLabState . thinkingLevel || piLabState . defaultThinkingLevel ),
permissionMode : nextMode ,
}),
})
. then ( function ( r ) {
if ( ! r . ok ) throw new Error ( 'Mode switch failed: ' + r . status );
return r . json ();
})
. then ( function ( data ) {
var session = data . session || {};
piLabState . sessionId = session . sessionId || data . sessionId || piLabState . sessionId ;
piLabState . providerSessionId = session . providerSessionId || data . providerSessionId || piLabState . providerSessionId ;
piLabState . permissionMode = normalizePermissionMode ( session . permissionMode || data . permissionMode || ( session . runtimePolicySnapshot && session . runtimePolicySnapshot . permissionMode ) || nextMode );
piLabState . session = session || piLabState . session || null ;
piLabState . runtimeMode = session . runtimeMode || data . runtimeMode || piLabState . runtimeMode || null ;
piLabState . runtimePid = session . runtimePid || data . pid || piLabState . runtimePid || null ;
piLabState . piExtensionSources = data . piExtensionSources || piLabState . piExtensionSources || [];
piLabState . piExtensionToolNames = data . piExtensionToolNames || piLabState . piExtensionToolNames || [];
connectEventSource ();
piLabState . applyingPermissionMode = false ;
setState ( STATE_STARTED );
updateDiagnostics ( 'Pi mode applied' );
updatePermissionControl ();
showPiToast ( 'Pi 模式已应用:' + permissionModeLabel ( piLabState . permissionMode ), 'success' );
maybeApplyPendingPermissionMode ();
return data ;
})
. catch ( function ( error ) {
piLabState . applyingPermissionMode = false ;
setState ( STATE_ERROR );
updateDiagnostics ( error && error . message ? error . message : 'Pi mode switch failed' );
showPiToast ( 'Pi 模式切换失败' , 'error' );
throw error ;
});
}
function setActionMenuOpen ( open ) {
piLabState . actionMenuOpen = !! open ;
if ( ! piLabPanelEl ) return ;
var composer = piLabPanelEl . querySelector ( '.wolai-page-ai-pi-lab-composer' );
var toggle = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-action-menu-toggle]' );
if ( composer ) composer . setAttribute ( 'data-menu-open' , piLabState . actionMenuOpen ? 'true' : 'false' );
if ( toggle ) toggle . setAttribute ( 'data-active' , piLabState . actionMenuOpen ? 'true' : 'false' );
}
function closeActionMenu () {
setActionMenuOpen ( false );
}
function permissionModeMenuHtml () {
return permissionModeOptions (). map ( function ( item ) {
var active = item . mode === normalizePermissionMode ( piLabState . permissionMode );
return '<button type="button" class="wolai-page-ai-pi-lab-permission-option" data-page-ai-pi-lab-permission-mode="' + escapeHtml ( item . mode ) + '" data-active="' + String ( active ) + '">' +
piIcon ( item . icon ) +
'<span><strong>' + escapeHtml ( item . label ) + '</strong><small>' + escapeHtml ( item . description ) + '</small></span>' +
'<span class="wolai-page-ai-pi-lab-permission-check" data-page-ai-pi-lab-permission-check>' + ( active ? '✓' : '' ) + '</span>' +
'</button>' ;
}). join ( '' );
}
function handleActionMenuAction ( action ) {
closeActionMenu ();
if ( action === 'read-page' || action === 'current-folder' || action === 'selection' || action === 'rag' ) {
handleQuickAction ( action );
return ;
}
if ( action === 'directory-permission' ) {
window . location . assign ( '/user/ai#ai-admin-access' );
return ;
}
if ( action === 'send-steer' ) {
sendCurrentInput ({ streamingBehavior : 'steer' });
return ;
}
if ( action === 'send-followup' ) {
sendCurrentInput ({ streamingBehavior : 'followUp' });
return ;
}
if ( action === 'plan-mode' ) {
var input = piLabPanelEl && piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-input]' );
var current = input ? input . value . trim () : '' ;
piLabState . permissionMode = 'plan' ;
updatePermissionControl ();
if ( piLabState . sessionId && piLabState . status !== STATE_STREAMING && piLabState . status !== STATE_STARTING ) {
applyPermissionModeToRuntime (). catch ( function () {});
} else {
piLabState . pendingPermissionModeApply = true ;
}
var planPrompt = current || '请先只做计划评审,不要修改文件。' ;
if ( input ) {
input . value = planPrompt ;
input . focus ();
updateButtons ();
}
showPiToast ( '已切换到 MNote 计划评审模式;发送后由后端只读 wrapper 处理' , 'info' );
}
}
2026-07-04 21:47:42 +08:00
function applyModelControls () {
if ( ! piLabPanelEl ) return ;
var providerSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-provider]' );
var modelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id]' );
var bottomModelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id-bottom]' );
2026-07-10 10:54:34 +08:00
var thinkingSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-thinking]' );
2026-07-04 21:47:42 +08:00
var customInput = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-custom]' );
if ( providerSelect && providerSelect . value ) piLabState . modelProvider = providerSelect . value ;
if ( customInput && customInput . value . trim ()) piLabState . modelId = customInput . value . trim ();
else if ( modelSelect && modelSelect . value ) piLabState . modelId = modelSelect . value ;
else if ( bottomModelSelect && bottomModelSelect . value ) piLabState . modelId = bottomModelSelect . value ;
2026-07-10 10:54:34 +08:00
if ( thinkingSelect && thinkingSelect . value ) piLabState . thinkingLevel = normalizeThinkingLevel ( thinkingSelect . value );
piLabState . permissionMode = normalizePermissionMode ( piLabState . permissionMode );
2026-07-04 21:47:42 +08:00
updateModelLabel ();
2026-07-10 10:54:34 +08:00
updateThinkingControl ();
updatePermissionControl ();
2026-07-04 21:47:42 +08:00
}
function splitModelRef ( value ) {
var raw = String ( value || '' ). trim ();
var slash = raw . indexOf ( '/' );
if ( slash < 0 ) return { provider : DEFAULT_MODEL_PROVIDER , modelId : raw || DEFAULT_MODEL_ID };
return { provider : raw . slice ( 0 , slash ), modelId : raw . slice ( slash + 1 ) };
}
function applyEffectiveConfig ( data ) {
data = data || {};
var defaultRef = splitModelRef ( data . defaultModel || data . default_model || '' );
piLabState . defaultModelProvider = defaultRef . provider || DEFAULT_MODEL_PROVIDER ;
piLabState . defaultModelId = defaultRef . modelId || DEFAULT_MODEL_ID ;
2026-07-10 10:54:34 +08:00
piLabState . defaultThinkingLevel = normalizeThinkingLevel ( data . defaultThinkingLevel || data . default_thinking_level || piLabState . defaultThinkingLevel );
if ( ! piLabState . thinkingLevel ) piLabState . thinkingLevel = piLabState . defaultThinkingLevel ;
2026-07-04 21:47:42 +08:00
piLabState . modelOptions = Array . isArray ( data . models ) ? data . models : [];
if ( Array . isArray ( data . allowedRoots )) {
piLabState . allowedRootsSummary = data . allowedRoots . length + ' roots' ;
}
if ( ! piLabPanelEl ) return ;
var providerSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-provider]' );
var modelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id]' );
var bottomModelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id-bottom]' );
var providers = [];
piLabState . modelOptions . forEach ( function ( item ) {
var provider = String ( item . provider || DEFAULT_MODEL_PROVIDER );
if ( providers . indexOf ( provider ) < 0 ) providers . push ( provider );
});
if ( ! providers . length ) providers . push ( piLabState . defaultModelProvider );
if ( providerSelect ) {
providerSelect . innerHTML = providers . map ( function ( provider ) {
return '<option value="' + escapeHtml ( provider ) + '">' + escapeHtml ( provider ) + '</option>' ;
}). join ( '' );
providerSelect . value = piLabState . modelProvider || piLabState . defaultModelProvider ;
}
var modelOptions = piLabState . modelOptions . length ? piLabState . modelOptions : [{
provider : piLabState . defaultModelProvider ,
id : piLabState . defaultModelId ,
name : piLabState . defaultModelId ,
}];
var optionHtml = modelOptions . map ( function ( item ) {
var id = String ( item . id || item . modelId || item . name || DEFAULT_MODEL_ID );
var label = String ( item . name || id );
return '<option value="' + escapeHtml ( id ) + '">' + escapeHtml ( label ) + '</option>' ;
}). join ( '' );
if ( modelSelect ) modelSelect . innerHTML = optionHtml ;
if ( bottomModelSelect ) bottomModelSelect . innerHTML = optionHtml ;
updateModelLabel ();
2026-07-10 10:54:34 +08:00
updateThinkingControl ();
2026-07-04 21:47:42 +08:00
updateContextStrip ();
}
function loadEffectiveConfig () {
return fetch ( API . EFFECTIVE , { credentials : 'same-origin' , cache : 'no-store' })
. then ( function ( response ) {
if ( ! response . ok ) throw new Error ( 'Effective config failed: ' + response . status );
return response . json ();
})
. then ( function ( data ) {
applyEffectiveConfig ( data );
return data ;
});
}
function currentRuntimeLabel () {
var parts = [];
if ( piLabState . runtimeMode ) parts . push ( piLabState . runtimeMode );
if ( piLabState . runtimePid ) parts . push ( 'pid ' + piLabState . runtimePid );
return parts . join ( ' · ' ) || 'runtime pending' ;
}
function statusTone () {
if ( ! piLabState . enabled || piLabState . status === STATE_ERROR ) return 'danger' ;
if ( piLabState . status === STATE_STREAMING || piLabState . status === STATE_STARTED ) return 'ok' ;
if ( piLabState . status === STATE_STARTING || piLabState . status === STATE_ABORTED ) return 'warn' ;
return '' ;
}
function statusText () {
if ( ! piLabState . enabled ) return 'disabled' ;
if ( piLabState . status === STATE_IDLE ) return 'idle' ;
if ( piLabState . status === STATE_STARTING ) return 'starting' ;
if ( piLabState . status === STATE_STARTED ) return 'ready' ;
if ( piLabState . status === STATE_STREAMING ) return 'streaming' ;
if ( piLabState . status === STATE_ABORTED ) return 'aborted' ;
return 'error' ;
}
2026-07-10 10:54:34 +08:00
function piIcon ( name ) {
var icons = {
plus : '<path d="M12 5v14M5 12h14"/>' ,
history : '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/><path d="M12 7v5l3 2"/>' ,
external : '<path d="M14 3h7v7"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/>' ,
artifacts : '<path d="M4 5h16v14H4z"/><path d="M9 5v14"/><path d="M15 5v14"/>' ,
status : '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>' ,
bell : '<path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M10 21h4"/>' ,
play : '<path d="m8 5 11 7-11 7z"/>' ,
stop : '<path d="M6 6h12v12H6z"/>' ,
trash : '<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/>' ,
settings : '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6V21h-4v-.1a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1-2.8-2.8.1-.1A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.6-1H3v-4h.1a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1 2.8-2.8.1.1A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.6V3h4v.1a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1 2.8 2.8-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.1v4H21a1.7 1.7 0 0 0-1.6 1z"/>' ,
zap : '<path d="M13 2 3 14h8l-1 8 11-13h-8z"/>' ,
file : '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>' ,
selection : '<path d="M4 4h6v6H4z"/><path d="M14 4h6v6h-6z"/><path d="M4 14h6v6H4z"/><path d="M14 14h6v6h-6z"/>' ,
database : '<ellipse cx="12" cy="5" rx="7" ry="3"/><path d="M5 5v6c0 1.7 3.1 3 7 3s7-1.3 7-3V5"/><path d="M5 11v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6"/>' ,
shield : '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>' ,
brain : '<path d="M9 3a3 3 0 0 0-3 3v1a3 3 0 0 0-2 5 3 3 0 0 0 2 5v1a3 3 0 0 0 5 2"/><path d="M15 3a3 3 0 0 1 3 3v1a3 3 0 0 1 2 5 3 3 0 0 1-2 5v1a3 3 0 0 1-5 2"/><path d="M12 4v16"/>' ,
folder : '<path d="M3 6a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>' ,
image : '<rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8" cy="10" r="2"/><path d="m21 15-5-5L5 21"/>' ,
camera : '<path d="M14.5 4 16 7h3a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3l1.5-3z"/><circle cx="12" cy="13" r="3"/>' ,
queue : '<path d="M4 6h16"/><path d="M4 12h10"/><path d="M4 18h7"/><path d="m16 15 3 3-3 3"/>' ,
copy : '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>' ,
send : '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>' ,
close : '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>' ,
minus : '<path d="M5 12h14"/>' ,
};
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + ( icons [ name ] || icons . status ) + '</svg>' ;
}
2026-07-04 21:47:42 +08:00
function piLabPanelHTML () {
return '' +
'<div class="wolai-page-ai-pi-lab-shell" data-page-ai-pi-lab="panel">' +
'<div class="wolai-page-ai-pi-lab-topbar">' +
'<div class="wolai-page-ai-pi-lab-brand">' +
'<div class="wolai-page-ai-pi-lab-logo">π</div>' +
'<div class="wolai-page-ai-pi-lab-title" data-page-ai-pi-lab-title><strong>Pi Lab 平台</strong><span data-page-ai-pi-lab-model>默认模型:omniroute/freefirst</span></div>' +
'</div>' +
2026-07-10 10:54:34 +08:00
'<button type="button" class="wolai-page-ai-pi-lab-icon-btn" data-page-ai-pi-lab-minimize aria-label="最小化 Pi Lab" title="最小化">' + piIcon ( 'minus' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-icon-btn" data-page-ai-pi-lab-close aria-label="关闭 Pi Lab" title="关闭">' + piIcon ( 'close' ) + '</button>' +
2026-07-04 21:47:42 +08:00
'</div>' +
'<div class="wolai-page-ai-pi-lab-commandbar" data-page-ai-pi-lab-commandbar>' +
2026-07-10 10:54:34 +08:00
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-new title="新对话" aria-label="新对话">' + piIcon ( 'plus' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-history title="历史" aria-label="历史">' + piIcon ( 'history' ) + '</button>' +
'<span class="wolai-page-ai-pi-lab-commandbar-spacer"></span>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-btn-abort title="中止" aria-label="中止" hidden>' + piIcon ( 'stop' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-btn-clear title="清空" aria-label="清空">' + piIcon ( 'trash' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-tool-icon" data-page-ai-pi-lab-open-settings title="AI 管理" aria-label="AI 管理">' + piIcon ( 'settings' ) + '</button>' +
2026-07-04 21:47:42 +08:00
'</div>' +
'<div class="wolai-page-ai-pi-lab-config">' +
'<details class="wolai-page-ai-pi-lab-settings" data-page-ai-pi-lab-settings>' +
'<summary><span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-status-chip><strong data-page-ai-pi-lab-status-text>idle</strong></span><span>设置</span><strong data-page-ai-pi-lab-model-chip>omniroute/freefirst</strong></summary>' +
'<div class="wolai-page-ai-pi-lab-settings-grid">' +
'<label class="wolai-page-ai-pi-lab-field">Provider<select data-page-ai-pi-lab-model-provider><option value="omniroute">Omniroute</option></select></label>' +
'<label class="wolai-page-ai-pi-lab-field">Model<select data-page-ai-pi-lab-model-id><option value="freefirst">freefirst</option></select></label>' +
2026-07-10 10:54:34 +08:00
'<label class="wolai-page-ai-pi-lab-field">自定义模型<input data-page-ai-pi-lab-model-custom placeholder="可选,启动时应用"></label>' +
2026-07-04 21:47:42 +08:00
'<span class="wolai-page-ai-pi-lab-chip"><span>runtime</span><strong data-page-ai-pi-lab-runtime-chip>runtime pending</strong></span>' +
2026-07-10 10:54:34 +08:00
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-builtin-managed><span>builtin managed</span><strong>read/write/edit/bash</strong></span>' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-extension-chip><span>Pi 扩展</span><strong data-page-ai-pi-lab-extension-count>pending</strong></span>' +
2026-07-04 21:47:42 +08:00
'</div>' +
'</details>' +
'<div class="wolai-page-ai-pi-lab-actions">' +
'<span class="wolai-page-ai-pi-lab-chip"><span>操作</span><strong>见顶部工具栏</strong></span>' +
'</div>' +
'</div>' +
2026-07-10 10:54:34 +08:00
'<div class="wolai-page-ai-pi-lab-history-layer" data-page-ai-pi-lab-history-layer hidden>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-scrim" data-page-ai-pi-lab-history-scrim aria-label="关闭历史"></button>' +
'<aside class="wolai-page-ai-pi-lab-history-panel" data-page-ai-pi-lab-history-panel aria-label="历史对话">' +
'<div class="wolai-page-ai-pi-lab-history-head"><span>历史对话</span><button type="button" class="wolai-page-ai-pi-lab-history-close" data-page-ai-pi-lab-history-close aria-label="关闭历史">× </button></div><div class="wolai-page-ai-pi-lab-empty">加载历史...</div>' +
'</aside>' +
2026-07-04 21:47:42 +08:00
'</div>' +
'<details class="wolai-page-ai-pi-lab-context-strip" data-page-ai-pi-lab-context-strip>' +
'<summary><span class="wolai-page-ai-pi-lab-context-title">上下文</span><span class="wolai-page-ai-pi-lab-context-current" data-page-ai-pi-lab-current-page>未绑定</span><span class="wolai-page-ai-pi-lab-context-count"><strong data-page-ai-pi-lab-changed-files>0</strong> changed</span></summary>' +
'<div class="wolai-page-ai-pi-lab-context-content">' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-context="selection"><span>选区</span><strong data-page-ai-pi-lab-selection>无选区</strong></span>' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-context="allowed-roots"><span>allowed roots</span><strong data-page-ai-pi-lab-allowed-roots>pending</strong></span>' +
'<span class="wolai-page-ai-pi-lab-chip" data-page-ai-pi-lab-context="lightrag"><span>LightRAG</span><strong data-page-ai-pi-lab-lightrag>default</strong></span>' +
'</div>' +
'</details>' +
2026-07-10 10:54:34 +08:00
'<div class="wolai-page-ai-pi-lab-body" data-page-ai-pi-lab-body data-rail-open="false">' +
2026-07-04 21:47:42 +08:00
'<div class="wolai-page-ai-pi-lab-main">' +
2026-07-10 10:54:34 +08:00
'<div class="wolai-page-ai-pi-lab-replay-banner" data-page-ai-pi-lab-replay-banner hidden><span>正在查看历史,只读</span><button type="button" data-page-ai-pi-lab-history-fork-current>Fork 后继续</button></div>' +
2026-07-04 21:47:42 +08:00
'<div class="wolai-page-ai-pi-lab-messages" data-page-ai-pi-lab-messages></div>' +
'<div class="wolai-page-ai-pi-lab-composer">' +
2026-07-10 10:54:34 +08:00
'<div class="wolai-page-ai-pi-lab-input-wrap">' +
'<textarea class="wolai-page-ai-pi-lab-input" data-page-ai-pi-lab-input placeholder="问 Pi..." rows="3"></textarea>' +
'<button type="button" class="wolai-page-ai-pi-lab-send" data-page-ai-pi-lab-btn-send disabled aria-label="发送" title="发送">' + piIcon ( 'send' ) + '</button>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-action-menu" data-page-ai-pi-lab-action-menu>' +
'<div class="wolai-page-ai-pi-lab-menu-section">上下文</div>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="read-page">' + piIcon ( 'file' ) + '<span>使用当前页</span><strong data-page-ai-pi-lab-context-mark="read-page"></strong></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="current-folder">' + piIcon ( 'folder' ) + '<span>使用当前文件夹</span><strong data-page-ai-pi-lab-context-mark="current-folder"></strong></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="selection">' + piIcon ( 'selection' ) + '<span>使用选区</span><strong data-page-ai-pi-lab-context-mark="selection"></strong></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="rag">' + piIcon ( 'database' ) + '<span>启用 LightRAG</span><strong data-page-ai-pi-lab-context-mark="rag"></strong></button>' +
'<div class="wolai-page-ai-pi-lab-menu-sep"></div>' +
'<div class="wolai-page-ai-pi-lab-menu-section">权限与附件</div>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="directory-permission">' + piIcon ( 'folder' ) + '<span>目录权限</span></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="attach-file" disabled title="Pi RPC 附件上下文尚未接入">' + piIcon ( 'image' ) + '<span>添加图片和文件</span></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="camera" disabled title="Pi RPC 附件上下文尚未接入">' + piIcon ( 'camera' ) + '<span>拍照</span></button>' +
'<div class="wolai-page-ai-pi-lab-menu-sep"></div>' +
'<div class="wolai-page-ai-pi-lab-menu-section">执行中发送</div>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="send-steer">' + piIcon ( 'zap' ) + '<span>作为引导发送</span></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="send-followup">' + piIcon ( 'queue' ) + '<span>作为排队追问发送</span></button>' +
'<button type="button" class="wolai-page-ai-pi-lab-menu-item" data-page-ai-pi-lab-menu-action="plan-mode" title="通过 Pi Rust 官方 plan-mode 扩展进入计划评审">' + piIcon ( 'settings' ) + '<span>计划评审</span></button>' +
2026-07-04 21:47:42 +08:00
'</div>' +
'<div class="wolai-page-ai-pi-lab-modebar" data-page-ai-pi-lab-modebar>' +
2026-07-10 10:54:34 +08:00
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-action-menu-toggle title="添加上下文和操作" aria-label="添加上下文和操作">' + piIcon ( 'plus' ) + '</button>' +
2026-07-04 21:47:42 +08:00
'<select class="wolai-page-ai-pi-lab-model-control" data-page-ai-pi-lab-model-id-bottom><option value="freefirst">omniroute/freefirst</option></select>' +
2026-07-10 10:54:34 +08:00
'<select class="wolai-page-ai-pi-lab-thinking-control" data-page-ai-pi-lab-thinking title="思考深度" aria-label="思考深度"><option value="off">思考 关</option><option value="minimal">思考 极低</option><option value="low">思考 低</option><option value="medium" selected>思考 中</option><option value="high">思考 高</option><option value="xhigh">思考 最高</option></select>' +
'<div class="wolai-page-ai-pi-lab-permission-wrap" data-page-ai-pi-lab-permission-wrap data-open="false">' +
'<button type="button" class="wolai-page-ai-pi-lab-chip-button" data-page-ai-pi-lab-permission title="Pi 模式" aria-label="Pi 模式">' + piIcon ( 'shield' ) + '<span data-page-ai-pi-lab-permission-label>变更前确认</span></button>' +
'<div class="wolai-page-ai-pi-lab-permission-menu" data-page-ai-pi-lab-permission-menu>' + permissionModeMenuHtml () + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-quick="read-page" title="使用当前页" aria-label="使用当前页">' + piIcon ( 'file' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-quick="current-folder" title="使用当前文件夹" aria-label="使用当前文件夹">' + piIcon ( 'folder' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-quick="selection" title="使用选区" aria-label="使用选区">' + piIcon ( 'selection' ) + '</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-quick="rag" title="启用 LightRAG" aria-label="启用 LightRAG">' + piIcon ( 'database' ) + '</button>' +
2026-07-04 21:47:42 +08:00
'</div>' +
'</div>' +
'</div>' +
'<aside class="wolai-page-ai-pi-lab-rail" data-page-ai-pi-lab-rail>' +
2026-07-10 10:54:34 +08:00
'<div class="wolai-page-ai-pi-lab-rail-head"><span>Artifacts</span><button type="button" class="wolai-page-ai-pi-lab-rail-close" data-page-ai-pi-lab-close-artifacts aria-label="关闭 Artifacts">× </button></div>' +
'<details class="wolai-page-ai-pi-lab-rail-section" data-page-ai-pi-lab-artifacts-section open>' +
'<summary>Artifacts <span class="wolai-page-ai-pi-lab-rail-count" data-page-ai-pi-lab-artifact-count>0</span></summary>' +
'<div class="wolai-page-ai-pi-lab-artifacts" data-page-ai-pi-lab-artifacts></div>' +
'</details>' +
2026-07-04 21:47:42 +08:00
'<details class="wolai-page-ai-pi-lab-rail-section">' +
'<summary>MNote tools <span class="wolai-page-ai-pi-lab-rail-count">5</span></summary>' +
'<div class="wolai-page-ai-pi-lab-tool-list" data-page-ai-pi-lab-tool-list>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.current_page.read</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.selection.read</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.local_file.patch</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.knowledge_rag.query</span>' +
'<span class="wolai-page-ai-pi-lab-tool-pill">mnote.reference.open</span>' +
'</div>' +
'</details>' +
2026-07-10 10:54:34 +08:00
'<details class="wolai-page-ai-pi-lab-rail-section" data-page-ai-pi-lab-receipts-section>' +
'<summary>Receipts <span class="wolai-page-ai-pi-lab-rail-count" data-page-ai-pi-lab-receipt-count>0</span></summary>' +
'<button type="button" class="wolai-page-ai-pi-lab-rail-action" data-page-ai-pi-lab-open-session-jsonl>打开 session JSONL</button>' +
'<div class="wolai-page-ai-pi-lab-receipts" data-page-ai-pi-lab-receipts></div>' +
'</details>' +
2026-07-04 21:47:42 +08:00
'</aside>' +
'</div>' +
'<details class="wolai-page-ai-pi-lab-diagnostics" data-page-ai-pi-lab-diagnostics>' +
'<summary>诊断</summary>' +
'<pre data-page-ai-pi-lab-diag-text></pre>' +
'</details>' +
'</div>' ;
}
function renderEmptyState ( container ) {
container . innerHTML = '' +
'<div class="wolai-page-ai-pi-lab-empty">' +
'<div class="wolai-page-ai-pi-lab-empty-icon">?</div>' +
'<strong>开始对话</strong>' +
'<p>试试以下示例,或直接输入您的指令</p>' +
'<div class="wolai-page-ai-pi-lab-examples">' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="帮我解释当前页面的逻辑"><span>▤</span>帮我解释当前页面的逻辑</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="如何优化这段内容的结构?"><span>?</span>如何优化这段内容的结构?</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="用 LightRAG 查询当前页面相关资料"><span>▥</span>用 LightRAG 查询当前页面相关资料</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="帮我把选区改写成更清晰的版本"><span>↗</span>帮我把选区改写成更清晰的版本</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-example" data-page-ai-pi-lab-example="检查当前 Markdown 有什么问题"><span>⌫</span>检查当前 Markdown 有什么问题</button>' +
'</div>' +
'</div>' ;
}
2026-07-10 10:54:34 +08:00
function historyTitle ( session ) {
return ( session && ( session . title || session . preview || session . pageTitle || session . pagePath )) || '未命名会话' ;
}
function formatHistoryTime ( value ) {
if ( ! value ) return '刚刚' ;
var time = Date . parse ( value );
if ( ! Number . isFinite ( time )) return String ( value );
var diff = Date . now () - time ;
if ( diff < 60 * 1000 ) return '刚刚' ;
if ( diff < 60 * 60 * 1000 ) return Math . floor ( diff / ( 60 * 1000 )) + ' 分钟前' ;
if ( diff < 24 * 60 * 60 * 1000 ) return Math . floor ( diff / ( 60 * 60 * 1000 )) + ' 小时前' ;
if ( diff < 7 * 24 * 60 * 60 * 1000 ) return Math . floor ( diff / ( 24 * 60 * 60 * 1000 )) + ' 天前' ;
return new Date ( time ). toLocaleDateString ();
}
function historyShell ( innerHtml ) {
var count = Array . isArray ( piLabState . history ) ? piLabState . history . length : 0 ;
return '<div class="wolai-page-ai-pi-lab-history-head"><span>历史对话</span><button type="button" class="wolai-page-ai-pi-lab-history-close" data-page-ai-pi-lab-history-close aria-label="关闭历史">× </button></div>' +
'<div class="wolai-page-ai-pi-lab-history-toolbar">' +
'<button type="button" class="wolai-page-ai-pi-lab-history-new" data-page-ai-pi-lab-new-small>新建对话</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-refresh" data-page-ai-pi-lab-history-refresh>刷新</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-clear" data-page-ai-pi-lab-history-clear' + ( count ? '' : ' disabled' ) + '>清空</button>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-history-meta"><span>共 ' + count + ' 条历史记录</span><span>Pi Rust</span></div>' +
'<div class="wolai-page-ai-pi-lab-history-list">' + innerHtml + '</div>' ;
}
2026-07-04 21:47:42 +08:00
function renderHistory () {
if ( ! piLabPanelEl ) return ;
var panel = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-history-panel]' );
if ( ! panel ) return ;
if ( ! piLabState . history . length ) {
2026-07-10 10:54:34 +08:00
panel . innerHTML = historyShell ( '<div class="wolai-page-ai-pi-lab-empty">暂无历史对话</div>' );
2026-07-04 21:47:42 +08:00
return ;
}
2026-07-10 10:54:34 +08:00
panel . innerHTML = historyShell ( piLabState . history . map ( function ( session ) {
var title = escapeHtml ( historyTitle ( session ));
var meta = escapeHtml ([ formatHistoryTime ( session . updatedAt || session . createdAt ), session . modelId || session . status ]. filter ( Boolean ). join ( ' · ' ));
var sessionId = escapeHtml ( session . sessionId || '' );
var active = piLabState . sessionId === ( session . sessionId || '' );
return '<div class="wolai-page-ai-pi-lab-history-row" data-active="' + ( active ? 'true' : 'false' ) + '" data-page-ai-pi-lab-history-row="' + sessionId + '">' +
'<span class="wolai-page-ai-pi-lab-history-dot" aria-hidden="true"></span>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-main" data-page-ai-pi-lab-history-session="' + sessionId + '">' +
'<strong>' + title + '</strong><span>' + meta + '</span></button>' +
'<span class="wolai-page-ai-pi-lab-history-actions">' +
'<button type="button" class="wolai-page-ai-pi-lab-history-action" data-page-ai-pi-lab-history-export="' + sessionId + '">导出</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-action" data-page-ai-pi-lab-history-rename="' + sessionId + '">重命名</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-action" data-page-ai-pi-lab-history-fork="' + sessionId + '">Fork</button>' +
'<button type="button" class="wolai-page-ai-pi-lab-history-action wolai-page-ai-pi-lab-history-danger" data-page-ai-pi-lab-history-delete="' + sessionId + '">删除</button>' +
'</span></div>' ;
}). join ( '' ));
}
function setHistoryPanelVisible ( visible ) {
if ( ! piLabPanelEl ) return Promise . resolve ();
var layer = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-history-layer]' );
var panel = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-history-panel]' );
var historyBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-history]' );
if ( ! layer || ! panel ) return Promise . resolve ();
layer . hidden = ! visible ;
if ( historyBtn ) historyBtn . setAttribute ( 'data-active' , visible ? 'true' : 'false' );
if ( visible ) {
var body = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-body]' );
if ( body ) body . setAttribute ( 'data-rail-open' , 'false' );
return loadHistory (). catch ( function ( error ) {
panel . innerHTML = historyShell ( '<div class="wolai-page-ai-pi-lab-empty">' + escapeHtml ( error . message || '历史加载失败' ) + '</div>' );
});
}
return Promise . resolve ();
2026-07-04 21:47:42 +08:00
}
function loadHistory () {
return fetch ( API . SESSIONS + '?limit=50' , { credentials : 'same-origin' , cache : 'no-store' })
. then ( function ( response ) {
if ( ! response . ok ) throw new Error ( 'History failed: ' + response . status );
return response . json ();
})
. then ( function ( data ) {
piLabState . history = Array . isArray ( data . sessions ) ? data . sessions : [];
renderHistory ();
return data ;
});
}
function openHistorySession ( sessionId ) {
if ( ! sessionId ) return Promise . resolve ();
2026-07-10 10:54:34 +08:00
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
2026-07-04 21:47:42 +08:00
return Promise . all ([
fetch ( API . SESSIONS + '/' + encodeURIComponent ( sessionId ), { credentials : 'same-origin' , cache : 'no-store' }). then ( function ( r ) { return r . json (); }),
fetch ( API . SESSIONS + '/' + encodeURIComponent ( sessionId ) + '/events?limit=500' , { credentials : 'same-origin' , cache : 'no-store' }). then ( function ( r ) { return r . json (); }),
]). then ( function ( results ) {
var detail = results [ 0 ] && results [ 0 ]. session || {};
2026-07-10 10:54:34 +08:00
var runtime = detail . runtime || {};
var runtimePolicy = runtime . runtimePolicy || runtime . runtime_policy || {};
2026-07-04 21:47:42 +08:00
var events = results [ 1 ] && Array . isArray ( results [ 1 ]. events ) ? results [ 1 ]. events : [];
piLabState . sessionId = detail . sessionId || sessionId ;
2026-07-10 10:54:34 +08:00
piLabState . providerSessionId = runtime . providerSessionId || detail . providerSessionId || null ;
piLabState . session = Object . assign ({}, runtime , detail , {
sessionId : piLabState . sessionId ,
providerSessionId : piLabState . providerSessionId ,
pagePath : runtime . pagePath || detail . pagePath ,
pageTitle : runtime . pageTitle || detail . pageTitle || detail . title ,
rootUri : runtime . rootUri || detail . rootUri ,
workspaceId : runtime . workspaceId || detail . workspaceId ,
modelProvider : runtime . modelProvider || detail . modelProvider ,
modelId : runtime . modelId || detail . modelId ,
thinkingLevel : runtime . thinkingLevel || detail . thinkingLevel ,
runtimePolicySnapshot : runtimePolicy ,
});
piLabState . modelProvider = piLabState . session . modelProvider || piLabState . modelProvider || null ;
piLabState . modelId = piLabState . session . modelId || piLabState . modelId || null ;
piLabState . thinkingLevel = normalizeThinkingLevel ( piLabState . session . thinkingLevel || piLabState . thinkingLevel || piLabState . defaultThinkingLevel );
piLabState . permissionMode = normalizePermissionMode ( runtimePolicy . permissionMode || runtimePolicy . permission_mode || detail . permissionMode || piLabState . permissionMode );
piLabState . allowedRootsSummary = summarizeAllowedRoots ( runtime . allowedRootsSnapshot || piLabState . session . allowedRootsSnapshot );
enterHistoryReplayMode ( sessionId );
resetPiRunViewModel ();
2026-07-04 21:47:42 +08:00
events . forEach ( function ( event ) {
var payload = event . payload || {};
if ( event . eventType === 'user_prompt' && payload . message ) {
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'user_prompt' , text : payload . message });
return ;
2026-07-04 21:47:42 +08:00
}
if ( event . eventType === 'pi_rpc_event' ) {
2026-07-10 10:54:34 +08:00
handlePiRpcEvent ( payload );
return ;
}
if ( event . eventType === 'runtime_aborted' ) {
applyPiRunEvent ({ type : 'assistant_abort' , payload : payload });
2026-07-04 21:47:42 +08:00
}
});
2026-07-10 10:54:34 +08:00
if ( streamingAssistantMsg && String ( streamingAssistantMsg . text || '' ). trim ()) {
applyPiRunEvent ({ type : 'assistant_done' , text : streamingAssistantMsg . text });
}
setState ( STATE_IDLE );
updateDiagnostics ( '已打开历史 session,可直接继续发送。' );
2026-07-04 21:47:42 +08:00
updateMessages ();
updateConfig ();
2026-07-10 10:54:34 +08:00
renderHistory ();
setHistoryPanelVisible ( false );
2026-07-04 21:47:42 +08:00
});
}
2026-07-10 10:54:34 +08:00
function currentHistorySession ( sessionId ) {
return ( piLabState . history || []). find ( function ( item ) {
return item && item . sessionId === sessionId ;
}) || null ;
}
function renameHistorySession ( sessionId ) {
if ( ! sessionId ) return Promise . resolve ();
var existing = currentHistorySession ( sessionId );
var nextTitle = window . prompt ( '重命名 Pi session' , ( existing && ( existing . title || existing . preview || existing . pageTitle )) || '' );
if ( nextTitle == null ) return Promise . resolve ();
nextTitle = String ( nextTitle || '' ). trim ();
if ( ! nextTitle ) {
showPiToast ( '标题不能为空' , 'warning' );
return Promise . resolve ();
}
return fetch ( API . SESSIONS + '/' + encodeURIComponent ( sessionId ), {
method : 'PATCH' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' , accept : 'application/json' },
body : JSON . stringify ({
title : nextTitle ,
workspaceId : ( piLabState . session && piLabState . session . workspaceId ) || currentWorkspaceId () || undefined ,
}),
}). then ( function ( response ) {
return response . json (). catch ( function () { return {}; }). then ( function ( payload ) {
if ( ! response . ok ) throw new Error ( payload . message || ( 'Rename failed: ' + response . status ));
showPiToast ( 'Session renamed' , 'success' );
return loadHistory ();
});
});
}
function resetCurrentSessionAfterHistoryDelete ( sessionId ) {
if ( piLabState . sessionId !== sessionId && piLabState . viewingHistorySessionId !== sessionId ) return ;
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
exitHistoryReplayMode ();
piLabState . sessionId = null ;
piLabState . providerSessionId = null ;
piLabState . status = STATE_IDLE ;
piLabState . session = null ;
resetPiRunViewModel ();
piLabState . receipts = [];
updateMessages ();
updateReceiptDisplay ();
updateQueueDisplay ();
updateConfig ();
}
function deleteHistorySession ( sessionId ) {
if ( ! sessionId ) return Promise . resolve ();
var existing = currentHistorySession ( sessionId );
var title = existing ? historyTitle ( existing ) : sessionId ;
if ( ! window . confirm ( '删除历史对话“' + title + '”?' )) return Promise . resolve ();
return fetch ( API . SESSIONS + '/' + encodeURIComponent ( sessionId ), {
method : 'DELETE' ,
credentials : 'same-origin' ,
headers : { accept : 'application/json' },
}). then ( function ( response ) {
return response . json (). catch ( function () { return {}; }). then ( function ( payload ) {
if ( ! response . ok ) throw new Error ( payload . message || ( 'Delete failed: ' + response . status ));
resetCurrentSessionAfterHistoryDelete ( sessionId );
showPiToast ( '历史对话已删除' , 'success' );
return loadHistory ();
});
});
}
function clearHistorySessions () {
var count = Array . isArray ( piLabState . history ) ? piLabState . history . length : 0 ;
if ( ! count ) return Promise . resolve ();
if ( ! window . confirm ( '清空全部 ' + count + ' 条 Pi 历史对话?' )) return Promise . resolve ();
return fetch ( API . SESSIONS + '?limit=1000' , {
method : 'DELETE' ,
credentials : 'same-origin' ,
headers : { accept : 'application/json' },
}). then ( function ( response ) {
return response . json (). catch ( function () { return {}; }). then ( function ( payload ) {
if ( ! response . ok ) throw new Error ( payload . message || ( 'Clear failed: ' + response . status ));
resetCurrentSessionAfterHistoryDelete ( piLabState . sessionId || piLabState . viewingHistorySessionId );
piLabState . history = [];
renderHistory ();
showPiToast ( '历史对话已清空' , 'success' );
});
});
}
function exportHistorySession ( sessionId ) {
if ( ! sessionId ) return Promise . resolve ();
return Promise . all ([
fetch ( API . SESSIONS + '/' + encodeURIComponent ( sessionId ), { credentials : 'same-origin' , cache : 'no-store' }). then ( function ( r ) { return r . json (); }),
fetch ( API . SESSIONS + '/' + encodeURIComponent ( sessionId ) + '/events?limit=1000' , { credentials : 'same-origin' , cache : 'no-store' }). then ( function ( r ) { return r . json (); }),
]). then ( function ( results ) {
var payload = {
schema : 'mnote.page_ai_pi.export_session.v1' ,
exportedAt : new Date (). toISOString (),
session : results [ 0 ] && results [ 0 ]. session || null ,
events : results [ 1 ] && Array . isArray ( results [ 1 ]. events ) ? results [ 1 ]. events : [],
};
var blob = new Blob ([ JSON . stringify ( payload , null , 2 )], { type : 'application/json;charset=utf-8' });
var objectUrl = URL . createObjectURL ( blob );
var link = document . createElement ( 'a' );
link . href = objectUrl ;
link . download = 'pi-session-' + sessionId + '.json' ;
document . body . appendChild ( link );
link . click ();
link . remove ();
window . setTimeout ( function () { URL . revokeObjectURL ( objectUrl ); }, 1000 );
showPiToast ( '历史对话已导出' , 'success' );
});
}
function forkHistorySession ( sessionId ) {
return openHistorySession ( sessionId ). then ( function () {
var oldSessionId = piLabState . sessionId ;
var source = piLabState . session || {};
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
piLabState . sessionId = null ;
piLabState . providerSessionId = null ;
piLabState . status = STATE_IDLE ;
exitHistoryReplayMode ();
resetPiRunViewModel ();
piLabState . receipts = [];
piLabState . session = Object . assign ({}, source , {
sessionId : null ,
providerSessionId : null ,
pageTitle : (( source . pageTitle || source . title || 'Pi session' ) + ' fork' ). slice ( 0 , 120 ),
});
updateMessages ();
updateReceiptDisplay ();
updateConfig ();
updateDiagnostics ( 'Forked from ' + oldSessionId + '; start creates a new Pi runtime' );
showPiToast ( 'Fork prepared' , 'success' );
return startRuntime ();
}). then ( function ( data ) {
return loadHistory (). then ( function () { return data ; }). catch ( function () { return data ; });
});
}
function renderMessage ( msg ) {
2026-07-04 21:47:42 +08:00
var card = el ( 'article' , {
className : 'wolai-page-ai-pi-lab-message' ,
'data-role' : msg . role || 'assistant' ,
'data-page-ai-pi-lab-message-role' : msg . role || 'assistant' ,
});
if ( msg . status === 'streaming' ) card . setAttribute ( 'data-page-ai-pi-lab-streaming' , 'true' );
var role = msg . role === 'assistant' ? 'Pi' : ( msg . role === 'user' ? 'You' : 'System' );
2026-07-10 10:54:34 +08:00
var meta = msg . status === 'streaming' ? 'streaming' : ( msg . status === 'aborted' ? 'aborted' : ( msg . meta || '' ));
2026-07-04 21:47:42 +08:00
card . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-role' }, role + ( meta ? ' · ' + meta : '' )));
var bubble = el ( 'div' , { className : 'wolai-page-ai-pi-lab-bubble' });
if ( msg . text ) {
2026-07-10 10:54:34 +08:00
var rendersMarkdown = msg . role === 'assistant' || msg . role === 'system' ;
2026-07-04 21:47:42 +08:00
bubble . appendChild ( el ( 'div' , {
2026-07-10 10:54:34 +08:00
className : ( rendersMarkdown ? 'wolai-page-ai-pi-lab-markdown' : 'wolai-page-ai-pi-lab-text' ) + ( msg . status === 'streaming' ? ' streaming-cursor' : '' ),
innerHTML : rendersMarkdown
? renderMarkdownBlock ( msg . text )
: escapeHtml ( msg . text ). replace ( /\n/g , '<br>' ),
2026-07-04 21:47:42 +08:00
}));
}
if ( msg . toolCalls && msg . toolCalls . length ) {
2026-07-10 10:54:34 +08:00
var tools = el ( 'details' , {
className : 'wolai-page-ai-pi-lab-tool-timeline' ,
'data-page-ai-pi-lab-tool-timeline' : 'true' ,
'data-page-ai-pi-tool-timeline' : 'true' ,
2026-07-04 21:47:42 +08:00
});
2026-07-10 10:54:34 +08:00
tools . appendChild ( el ( 'summary' , {}, [
el ( 'span' , {}, '工具调用' ),
el ( 'span' , { className : 'wolai-page-ai-pi-lab-rail-count' }, String ( msg . toolCalls . length )),
]));
var toolsBody = el ( 'div' , { className : 'wolai-page-ai-pi-lab-tool-timeline-body' });
msg . toolCalls . forEach ( function ( tc ) { toolsBody . appendChild ( renderToolCallCard ( tc )); });
tools . appendChild ( toolsBody );
2026-07-04 21:47:42 +08:00
bubble . appendChild ( tools );
}
if ( msg . citations && msg . citations . length ) {
var citations = el ( 'div' , { className : 'wolai-page-ai-pi-lab-citations' });
msg . citations . forEach ( function ( cit , i ) {
citations . appendChild ( el ( 'a' , {
className : 'wolai-page-ai-pi-lab-citation' ,
href : cit . url || '#' ,
target : '_blank' ,
rel : 'noopener' ,
'data-page-ai-pi-lab-citation' : cit . source || '' ,
}, '[' + ( i + 1 ) + '] ' + ( cit . title || cit . source || 'source' )));
});
bubble . appendChild ( citations );
}
2026-07-10 10:54:34 +08:00
if ( msg . diffSummary ) {
var files = msg . diffSummary . files || [];
bubble . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-diff' , 'data-page-ai-pi-lab-diff' : 'true' }, 'patch · ' + files . join ( ', ' )));
}
if ( msg . status === 'aborted' || msg . abortReason ) {
var abortPayload = msg . abortResponse || {};
var abortLines = [ msg . abortReason || '已中止' ];
if ( abortPayload . schema ) abortLines . push ( 'schema=' + abortPayload . schema );
if ( abortPayload . stopReason ) abortLines . push ( 'stopReason=' + abortPayload . stopReason );
if ( abortPayload . command ) abortLines . push ( 'command=' + abortPayload . command );
var queue = piLabState . pendingQueue || {};
var steering = Array . isArray ( queue . steering ) ? queue . steering . length : 0 ;
var followUp = Array . isArray ( queue . followUp ) ? queue . followUp . length : 0 ;
abortLines . push ( 'queued messages=' + ( steering + followUp ));
bubble . appendChild ( el ( 'div' , {
className : 'wolai-page-ai-pi-lab-abort-note' ,
'data-page-ai-pi-lab-abort-note' : 'true' ,
'data-page-ai-pi-lab-stop-reason' : abortPayload . stopReason || 'aborted' ,
}, abortLines . join ( ' · ' )));
}
if ( msg . role === 'assistant' && ( msg . text || msg . toolCalls || msg . citations || msg . diffSummary )) {
bubble . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-message-actions' }, [
el ( 'button' , {
type : 'button' ,
title : '复制 Markdown' ,
'aria-label' : '复制 Markdown' ,
'data-page-ai-pi-lab-copy-markdown' : messageToMarkdown ( msg ),
innerHTML : piIcon ( 'copy' ),
}),
]));
}
2026-07-04 21:47:42 +08:00
card . appendChild ( bubble );
return card ;
}
function isVisibleMessage ( msg ) {
if ( ! msg ) return false ;
2026-07-10 10:54:34 +08:00
if ( msg . role === 'system' && /^Pi session started:/ . test ( String ( msg . text || '' ))) return false ;
2026-07-04 21:47:42 +08:00
if ( msg . status === 'streaming' ) return true ;
2026-07-10 10:54:34 +08:00
if ( msg . status === 'aborted' || msg . abortReason ) return true ;
2026-07-04 21:47:42 +08:00
if ( String ( msg . text || '' ). trim ()) return true ;
if ( msg . toolCalls && msg . toolCalls . length ) return true ;
if ( msg . citations && msg . citations . length ) return true ;
if ( msg . diffSummary ) return true ;
return msg . role !== 'assistant' ;
}
function updateStatusBadge () {
updateConfig ();
}
function updateModelLabel () {
if ( ! piLabPanelEl ) return ;
var titleLabel = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model]' );
var modelChip = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-chip]' );
var providerSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-provider]' );
var modelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id]' );
var bottomModelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id-bottom]' );
2026-07-10 10:54:34 +08:00
var thinkingSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-thinking]' );
2026-07-04 21:47:42 +08:00
var customInput = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-custom]' );
var provider = piLabState . modelProvider || piLabState . defaultModelProvider || DEFAULT_MODEL_PROVIDER ;
var modelId = piLabState . modelId || piLabState . defaultModelId || DEFAULT_MODEL_ID ;
if ( String ( modelId ). indexOf ( provider + '/' ) === 0 ) {
modelId = String ( modelId ). slice ( provider . length + 1 );
piLabState . modelId = modelId ;
}
if ( titleLabel ) titleLabel . textContent = '默认模型:' + currentModelLabel ();
if ( modelChip ) modelChip . textContent = currentModelLabel ();
if ( providerSelect && providerSelect . value !== provider ) providerSelect . value = provider ;
if ( modelSelect && modelSelect . value !== modelId && Array . prototype . some . call ( modelSelect . options , function ( option ) { return option . value === modelId ; })) {
modelSelect . value = modelId ;
}
if ( bottomModelSelect && bottomModelSelect . value !== modelId && Array . prototype . some . call ( bottomModelSelect . options , function ( option ) { return option . value === modelId ; })) {
bottomModelSelect . value = modelId ;
}
if ( customInput && modelId !== DEFAULT_MODEL_ID && modelId !== 'freefirst' ) customInput . value = modelId ;
}
function updateConfig () {
if ( ! piLabPanelEl ) return ;
updateModelLabel ();
2026-07-10 10:54:34 +08:00
updateThinkingControl ();
2026-07-04 21:47:42 +08:00
var statusChip = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-status-chip]' );
var statusNode = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-status-text]' );
var runtimeNode = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-runtime-chip]' );
2026-07-10 10:54:34 +08:00
var builtinNode = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-builtin-managed]' );
var extensionCountNode = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-extension-count]' );
var replayBanner = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-replay-banner]' );
2026-07-04 21:47:42 +08:00
if ( statusChip ) {
var tone = statusTone ();
if ( tone ) statusChip . setAttribute ( 'data-tone' , tone );
else statusChip . removeAttribute ( 'data-tone' );
}
if ( statusNode ) statusNode . textContent = statusText ();
if ( runtimeNode ) runtimeNode . textContent = currentRuntimeLabel ();
2026-07-10 10:54:34 +08:00
if ( builtinNode ) {
var managed = Array . isArray ( piLabState . managedBuiltinTools ) ? piLabState . managedBuiltinTools : [];
builtinNode . style . display = managed . length ? '' : 'none' ;
var strong = builtinNode . querySelector ( 'strong' );
if ( strong ) strong . textContent = managed . length ? managed . join ( '/' ) : 'pending' ;
}
if ( extensionCountNode ) {
var extensions = Array . isArray ( piLabState . piExtensionSources ) ? piLabState . piExtensionSources : [];
extensionCountNode . textContent = extensions . length ? String ( extensions . length ) + ' packages' : 'pending' ;
extensionCountNode . title = extensions . join ( '\n' );
}
if ( replayBanner ) {
replayBanner . hidden = ! piLabState . viewingHistorySessionId ;
var replayText = replayBanner . querySelector ( 'span' );
if ( replayText ) replayText . textContent = piLabState . viewingHistorySessionId ? '正在查看历史,只读' : '' ;
}
2026-07-04 21:47:42 +08:00
updateContextStrip ();
2026-07-10 10:54:34 +08:00
updatePermissionControl ();
2026-07-04 21:47:42 +08:00
updateButtons ();
2026-07-10 10:54:34 +08:00
updateQueueDisplay ();
2026-07-04 21:47:42 +08:00
updateLauncherState ();
}
function updateContextStrip () {
if ( ! piLabPanelEl ) return ;
var currentPage = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-current-page]' );
var selection = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-selection]' );
var allowedRoots = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-allowed-roots]' );
var lightrag = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-lightrag]' );
var changedFiles = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-changed-files]' );
var session = piLabState . session || {};
if ( currentPage ) currentPage . textContent = session . pageTitle || session . pagePath || document . title || '当前页面' ;
if ( selection ) selection . textContent = piLabState . selectionSummary || '无选区' ;
if ( allowedRoots ) allowedRoots . textContent = piLabState . allowedRootsSummary || 'pending' ;
if ( lightrag ) lightrag . textContent = '唯一默认' ;
if ( changedFiles ) {
var files = {};
piLabState . changedFiles . forEach ( function ( file ) { if ( file ) files [ file ] = true ; });
piLabState . messages . forEach ( function ( msg ) {
if ( msg && msg . diffSummary && Array . isArray ( msg . diffSummary . files )) {
msg . diffSummary . files . forEach ( function ( file ) { if ( file ) files [ file ] = true ; });
}
});
changedFiles . textContent = String ( Object . keys ( files ). length );
}
2026-07-10 10:54:34 +08:00
var selected = contextSelectionState ();
Array . prototype . slice . call ( piLabPanelEl . querySelectorAll ( '[data-page-ai-pi-lab-quick]' )). forEach ( function ( btn ) {
var kind = btn . getAttribute ( 'data-page-ai-pi-lab-quick' ) || '' ;
var active = ( kind === 'read-page' && selected . currentPage )
|| ( kind === 'current-folder' && selected . currentFolder )
|| ( kind === 'selection' && selected . selection )
|| ( kind === 'rag' && selected . lightrag );
btn . setAttribute ( 'data-active' , active ? 'true' : 'false' );
btn . setAttribute ( 'aria-pressed' , active ? 'true' : 'false' );
});
Array . prototype . slice . call ( piLabPanelEl . querySelectorAll ( '[data-page-ai-pi-lab-menu-action]' )). forEach ( function ( btn ) {
var action = btn . getAttribute ( 'data-page-ai-pi-lab-menu-action' ) || '' ;
var active = ( action === 'read-page' && selected . currentPage )
|| ( action === 'current-folder' && selected . currentFolder )
|| ( action === 'selection' && selected . selection )
|| ( action === 'rag' && selected . lightrag );
if ([ 'read-page' , 'current-folder' , 'selection' , 'rag' ]. indexOf ( action ) >= 0 ) {
btn . setAttribute ( 'data-active' , active ? 'true' : 'false' );
btn . setAttribute ( 'aria-pressed' , active ? 'true' : 'false' );
}
});
Array . prototype . slice . call ( piLabPanelEl . querySelectorAll ( '[data-page-ai-pi-lab-context-mark]' )). forEach ( function ( mark ) {
var action = mark . getAttribute ( 'data-page-ai-pi-lab-context-mark' ) || '' ;
var active = ( action === 'read-page' && selected . currentPage )
|| ( action === 'current-folder' && selected . currentFolder )
|| ( action === 'selection' && selected . selection )
|| ( action === 'rag' && selected . lightrag );
mark . textContent = active ? '✓' : '' ;
});
updatePermissionControl ();
2026-07-04 21:47:42 +08:00
}
function updateButtons () {
if ( ! piLabPanelEl ) return ;
var sendBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-btn-send]' );
var abortBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-btn-abort]' );
var input = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-input]' );
var providerSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-provider]' );
var modelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id]' );
var bottomModelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id-bottom]' );
2026-07-10 10:54:34 +08:00
var thinkingSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-thinking]' );
2026-07-04 21:47:42 +08:00
var customInput = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-custom]' );
2026-07-10 10:54:34 +08:00
var isReplay = !! piLabState . viewingHistorySessionId ;
2026-07-04 21:47:42 +08:00
var isIdle = piLabState . status === STATE_IDLE || piLabState . status === STATE_ABORTED || piLabState . status === STATE_ERROR ;
var isStarted = piLabState . status === STATE_STARTED ;
2026-07-10 10:54:34 +08:00
var isStreaming = piLabState . status === STATE_STREAMING ;
2026-07-04 21:47:42 +08:00
if ( sendBtn ) {
2026-07-10 10:54:34 +08:00
sendBtn . disabled = isReplay || ! piLabState . enabled || ! input || ! input . value . trim ();
2026-07-04 21:47:42 +08:00
}
2026-07-10 10:54:34 +08:00
if ( abortBtn ) abortBtn . hidden = ! ( isStreaming || piLabState . status === STATE_STARTING );
if ( input ) {
input . disabled = isReplay ;
input . placeholder = isReplay ? '历史对话为只读,Fork 后继续' : '问 Pi...' ;
}
[ providerSelect , modelSelect , bottomModelSelect , thinkingSelect , customInput ]. forEach ( function ( node ) {
if ( node ) node . disabled = isReplay || ! isIdle ;
2026-07-04 21:47:42 +08:00
});
}
2026-07-10 10:54:34 +08:00
function updateQueueDisplay () {
if ( ! piLabPanelEl ) return ;
var queue = piLabState . pendingQueue || {};
var steering = Array . isArray ( queue . steering ) ? queue . steering . length : 0 ;
var followUp = Array . isArray ( queue . followUp ) ? queue . followUp . length : 0 ;
var count = steering + followUp ;
var node = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-queue-count]' );
if ( node ) {
node . textContent = String ( count );
node . title = '排队消息 ' + count ;
}
}
2026-07-04 21:47:42 +08:00
function updateMessages () {
var container = piLabPanelEl && piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-messages]' );
if ( ! container ) return ;
container . innerHTML = '' ;
var visibleMessages = piLabState . messages . filter ( isVisibleMessage );
if ( ! visibleMessages . length ) {
renderEmptyState ( container );
2026-07-10 10:54:34 +08:00
updateArtifactRail ();
2026-07-04 21:47:42 +08:00
return ;
}
visibleMessages . forEach ( function ( msg ) { container . appendChild ( renderMessage ( msg )); });
container . scrollTop = container . scrollHeight ;
updateContextStrip ();
2026-07-10 10:54:34 +08:00
updateQueueDisplay ();
updateArtifactRail ();
2026-07-04 21:47:42 +08:00
}
function updateDiagnostics ( text ) {
piLabState . diagnostics = text || piLabState . diagnostics || '' ;
if ( ! piLabPanelEl ) return ;
var pre = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-diag-text]' );
if ( ! pre ) return ;
var lines = [];
lines . push ( 'session=' + ( piLabState . sessionId || 'none' ));
lines . push ( 'providerSession=' + ( piLabState . providerSessionId || 'none' ));
lines . push ( 'model=' + currentModelLabel ());
lines . push ( 'runtime=' + currentRuntimeLabel ());
2026-07-10 10:54:34 +08:00
lines . push ( 'managedBuiltinTools=' + (( Array . isArray ( piLabState . managedBuiltinTools ) && piLabState . managedBuiltinTools . length ) ? piLabState . managedBuiltinTools . join ( '/' ) : 'pending' ));
if ( Array . isArray ( piLabState . piExtensionSources ) && piLabState . piExtensionSources . length ) {
lines . push ( 'piExtensions=' + piLabState . piExtensionSources . join ( ',' ));
}
2026-07-04 21:47:42 +08:00
if ( piLabState . allowedRootsSummary ) lines . push ( 'allowedRoots=' + piLabState . allowedRootsSummary );
if ( piLabState . diagnostics ) lines . push ( 'message=' + piLabState . diagnostics );
pre . textContent = lines . join ( '\n' );
}
2026-07-10 10:54:34 +08:00
function updatePiFloatingLayerPosition ( layer ) {
if ( ! layer || ! piLabPanelEl ) return ;
var composer = piLabPanelEl . querySelector ( '.wolai-page-ai-pi-lab-composer' );
if ( ! composer ) return ;
layer . style . bottom = Math . max ( 0 , Math . ceil ( composer . offsetHeight ) + 6 ) + 'px' ;
}
function ensureToastStack () {
var stack = document . querySelector ( '[data-page-ai-pi-lab-toast-stack]' );
if ( stack ) {
updatePiFloatingLayerPosition ( stack );
return stack ;
}
var host = piLabPanelEl && piLabPanelEl . querySelector ( '.wolai-page-ai-pi-lab-main' );
stack = el ( 'div' , {
className : 'wolai-page-ai-pi-lab-toast-stack' ,
'data-page-ai-pi-lab-toast-stack' : 'true' ,
});
( host || document . body ). appendChild ( stack );
updatePiFloatingLayerPosition ( stack );
return stack ;
}
function showPiToast ( message , type ) {
var stack = ensureToastStack ();
var toast = el ( 'div' , {
className : 'wolai-page-ai-pi-lab-toast' ,
'data-page-ai-pi-lab-toast' : String ( ++ piToastSeq ),
'data-type' : type || 'info' ,
textContent : message || '' ,
});
stack . appendChild ( toast );
setTimeout ( function () {
if ( toast . parentNode ) toast . parentNode . removeChild ( toast );
}, 4200 );
return toast ;
}
function closePiUiDialog () {
if ( activePiUiDialog && activePiUiDialog . parentNode ) activePiUiDialog . parentNode . removeChild ( activePiUiDialog );
activePiUiDialog = null ;
}
function closePiCustomUiDialog () {
if ( piCustomUiState . panel && piCustomUiState . panel . parentNode ) piCustomUiState . panel . parentNode . removeChild ( piCustomUiState . panel );
piCustomUiState . panel = null ;
piCustomUiState . lines = [];
piCustomUiState . title = '' ;
piCustomUiState . selectedOptionIndex = 0 ;
piCustomUiState . submitted = false ;
piCustomUiState . pendingCustomRequest = null ;
}
function sendPiUiResponse ( request , response ) {
if ( ! request || ! request . id || ! piLabState . sessionId ) return Promise . resolve ( null );
var body = {
sessionId : piLabState . sessionId ,
requestId : request . id ,
method : request . method || '' ,
};
if ( request . mnoteApproval ) body . mnoteApproval = request . mnoteApproval ;
Object . keys ( response || {}). forEach ( function ( key ) { body [ key ] = response [ key ]; });
return fetch ( API . UI_RESPONSE , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( body ),
}). catch ( function ( error ) {
updateDiagnostics ( 'Extension UI response failed: ' + ( error && error . message ? error . message : String ( error )));
});
}
function stripPiAnsi ( text ) {
return String ( text == null ? '' : text )
. replace ( /\x1b\][^\x07]*(?:\x07|\x1b\\)/g , '' )
. replace ( /\x1b\[[0-?]*[ -/]*[@-~]/g , '' )
. replace ( /\x1b[@-Z\\-_]/g , '' );
}
function piCustomUiWidth () {
var panel = piCustomUiState . panel ;
var body = panel && panel . querySelector ( '.wolai-page-ai-pi-lab-ui-dialog-body' );
var px = ( body && body . clientWidth ) || ( panel && panel . clientWidth ) || 640 ;
return Math . max ( 40 , Math . min ( 120 , Math . floor ( px / 8 )));
}
function parsePiCustomUiOptions ( lines ) {
var options = [];
( Array . isArray ( lines ) ? lines : []). forEach ( function ( line ) {
var clean = stripPiAnsi ( line ). replace ( /[│┃║]/g , '' ). trim ();
var match = clean . match ( /^(?:>\s*)?(\d+)\.\s+(.+?)\s*(?:[✎*])?$/ );
if ( ! match ) return ;
var label = match [ 2 ]. trim ();
if ( ! label ) return ;
options . push ({ index : Number ( match [ 1 ]) - 1 , label : label });
});
return options ;
}
function renderPiCustomUiDialog ( request ) {
request = request || {};
var widgetKey = String ( request . widgetKey || request . widget_key || piCustomUiState . widgetKey || '__pi_custom_overlay' );
if ( request . clear || request . close ) {
closePiCustomUiDialog ();
return null ;
}
if ( piCustomUiState . submitted ) return null ;
var lines = Array . isArray ( request . lines )
? request . lines
: ( Array . isArray ( request . widgetLines ) ? request . widgetLines : String ( request . content || '' ). split ( '\n' ));
piCustomUiState . widgetKey = widgetKey ;
piCustomUiState . lines = lines . map ( stripPiAnsi );
piCustomUiState . title = String ( request . title || piCustomUiState . title || 'Pi 需要输入' ). trim ();
var options = parsePiCustomUiOptions ( piCustomUiState . lines );
if ( piCustomUiState . selectedOptionIndex >= options . length ) piCustomUiState . selectedOptionIndex = 0 ;
var panel = piCustomUiState . panel ;
if ( ! panel ) {
panel = el ( 'div' , {
className : 'wolai-page-ai-pi-lab-ui-backdrop' ,
'data-page-ai-pi-lab-ui-dialog' : 'custom' ,
'data-page-ai-pi-lab-ui-widget-key' : widgetKey ,
});
var composer = piLabPanelEl && piLabPanelEl . querySelector ( '.wolai-page-ai-pi-lab-composer' );
if ( composer ) composer . insertBefore ( panel , composer . firstChild );
else document . body . appendChild ( panel );
piCustomUiState . panel = panel ;
}
panel . setAttribute ( 'data-page-ai-pi-lab-ui-widget-key' , widgetKey );
panel . innerHTML = '' ;
var dialog = el ( 'section' , { className : 'wolai-page-ai-pi-lab-ui-dialog' , role : 'dialog' , 'aria-modal' : 'false' });
dialog . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-dialog-header' }, [
el ( 'div' , {}, [
el ( 'strong' , {}, piCustomUiState . title || 'Pi 需要输入' ),
el ( 'span' , {}, '官方 Pi Rust custom UI' ),
]),
el ( 'button' , { type : 'button' , className : 'wolai-page-ai-pi-lab-ui-collapse' , 'data-page-ai-pi-lab-ui-collapse' : 'true' , title : '折叠/展开' }, '▾' ),
]));
var body = el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-dialog-body' });
body . appendChild ( el ( 'pre' , { className : 'wolai-page-ai-pi-lab-ui-tui' }, piCustomUiState . lines . join ( '\n' )));
if ( options . length ) {
var optionList = el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-tui-options' });
options . forEach ( function ( option , visibleIndex ) {
var selected = visibleIndex === piCustomUiState . selectedOptionIndex ;
optionList . appendChild ( el ( 'button' , {
type : 'button' ,
className : 'wolai-page-ai-pi-lab-ui-option' ,
'data-page-ai-pi-lab-ui-option' : option . label ,
'data-custom-option-index' : String ( visibleIndex ),
'data-custom-selected' : selected ? 'true' : 'false' ,
}, [
el ( 'span' , { className : 'wolai-page-ai-pi-lab-ui-option-mark' }, selected ? '◉' : '○' ),
el ( 'span' , {}, option . label ),
]));
});
body . appendChild ( optionList );
}
dialog . appendChild ( body );
var actions = el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-dialog-actions' });
actions . appendChild ( el ( 'button' , { type : 'button' , 'data-page-ai-pi-lab-ui-cancel' : 'true' }, '取消' ));
actions . appendChild ( el ( 'button' , { type : 'button' , 'data-page-ai-pi-lab-ui-submit' : 'true' , 'data-primary' : 'true' }, '提交' ));
dialog . appendChild ( actions );
panel . appendChild ( dialog );
panel . onclick = function ( event ) {
if ( event . target && event . target . closest && event . target . closest ( '[data-page-ai-pi-lab-ui-collapse]' )) {
var collapsed = dialog . getAttribute ( 'data-collapsed' ) === 'true' ;
dialog . setAttribute ( 'data-collapsed' , collapsed ? 'false' : 'true' );
event . target . textContent = collapsed ? '▾' : '▸' ;
return ;
}
var option = event . target && event . target . closest ? event . target . closest ( '[data-custom-option-index]' ) : null ;
if ( option ) {
piCustomUiState . selectedOptionIndex = Number ( option . getAttribute ( 'data-custom-option-index' )) || 0 ;
renderPiCustomUiDialog ( request );
return ;
}
if ( event . target && event . target . closest && event . target . closest ( '[data-page-ai-pi-lab-ui-cancel]' )) {
var cancelRequest = piCustomUiState . pendingCustomRequest ;
piCustomUiState . pendingCustomRequest = null ;
piCustomUiState . submitted = true ;
if ( panel . parentNode ) panel . parentNode . removeChild ( panel );
piCustomUiState . panel = null ;
if ( cancelRequest ) sendPiUiResponse ( cancelRequest , { value : { width : piCustomUiWidth (), key : '\x1b' } });
else piCustomUiState . keyQueue . push ( '\x1b' );
return ;
}
if ( event . target && event . target . closest && event . target . closest ( '[data-page-ai-pi-lab-ui-submit]' )) {
var selectedIndex = Math . max ( 0 , piCustomUiState . selectedOptionIndex || 0 );
var submitKeys = [];
for ( var i = 0 ; i < selectedIndex ; i += 1 ) submitKeys . push ( '\x1b[B' );
submitKeys . push ( '\r' );
var submitRequest = piCustomUiState . pendingCustomRequest ;
piCustomUiState . pendingCustomRequest = null ;
piCustomUiState . submitted = true ;
if ( panel . parentNode ) panel . parentNode . removeChild ( panel );
piCustomUiState . panel = null ;
if ( submitRequest ) {
var firstKey = submitKeys . shift ();
sendPiUiResponse ( submitRequest , { value : { width : piCustomUiWidth (), key : firstKey } });
}
submitKeys . forEach ( function ( key ) { piCustomUiState . keyQueue . push ( key ); });
}
};
return panel ;
}
function respondToPiCustomUiPoll ( request ) {
var value = { width : piCustomUiWidth () };
var key = piCustomUiState . keyQueue . shift ();
if ( key ) value . key = key ;
return sendPiUiResponse ( request , { value : value });
}
function normalizePiAskQuestions ( request ) {
var source = Array . isArray ( request . questions ) && request . questions . length ? request . questions : [{
header : request . header || request . question || request . title || 'Pi 需要选择' ,
tab : request . tab || 'answer' ,
prompt : request . prompt || request . message || request . context || '' ,
options : request . options || [],
multiSelect : request . allowMultiple === true || request . multiSelect === true ,
allowSkip : request . allowSkip !== false ,
allowFreeform : request . allowFreeform !== false ,
}];
return source . map ( function ( question , index ) {
return {
header : String ( question . header || question . title || question . question || ( '问题 ' + ( index + 1 ))). trim (),
tab : String ( question . tab || question . id || ( 'q' + ( index + 1 ))). trim (),
prompt : String ( question . prompt || question . description || '' ). trim (),
options : Array . isArray ( question . options ) ? question . options : [],
multiSelect : question . multiSelect === true || question . allowMultiple === true ,
allowSkip : question . allowSkip !== false ,
allowFreeform : question . allowFreeform !== false ,
};
});
}
function renderPiAskQuestion ( body , question , index ) {
var section = el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-question' , 'data-page-ai-pi-lab-ui-question' : String ( index ), 'data-tab' : question . tab });
section . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-question-title' }, question . header ));
if ( question . prompt ) section . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-question-prompt' }, question . prompt ));
question . options . forEach ( function ( option , optionIndex ) {
var label = typeof option === 'string' ? option : String (( option && ( option . label || option . title || option . value )) || '' );
var description = typeof option === 'string' ? '' : String (( option && option . description ) || '' );
var value = typeof option === 'string' ? option : String (( option && ( option . value || option . label || option . title )) || label );
var row = el ( 'button' , {
type : 'button' ,
className : 'wolai-page-ai-pi-lab-ui-option' ,
'data-page-ai-pi-lab-ui-option' : value ,
'data-question-index' : String ( index ),
'data-option-index' : String ( optionIndex ),
'data-selected' : 'false' ,
}, [
el ( 'span' , { className : 'wolai-page-ai-pi-lab-ui-option-mark' }, question . multiSelect ? '□' : '○' ),
el ( 'span' , {}, [
el ( 'span' , {}, label || value ),
description ? el ( 'small' , {}, description ) : null ,
]),
]);
section . appendChild ( row );
});
if ( question . allowFreeform ) {
section . appendChild ( el ( 'textarea' , {
'data-page-ai-pi-lab-ui-custom' : String ( index ),
placeholder : 'Type something...' ,
}));
}
body . appendChild ( section );
}
function collectPiAskResponse ( panel , questions ) {
var answers = questions . map ( function ( question , index ) {
var section = panel . querySelector ( '[data-page-ai-pi-lab-ui-question="' + index + '"]' );
var selected = Array . from ( section ? section . querySelectorAll ( '[data-page-ai-pi-lab-ui-option][data-selected="true"]' ) : [])
. map ( function ( node ) { return node . getAttribute ( 'data-page-ai-pi-lab-ui-option' ) || '' ; })
. filter ( Boolean );
var customNode = section ? section . querySelector ( '[data-page-ai-pi-lab-ui-custom="' + index + '"]' ) : null ;
var custom = customNode ? customNode . value . trim () : '' ;
var answer = { tab : question . tab };
if ( question . multiSelect ) {
if ( selected . length ) answer . answers = selected ;
if ( custom ) answer . custom = custom ;
if ( ! selected . length && ! custom ) answer . answers = [];
} else if ( selected [ 0 ]) {
answer . answer = selected [ 0 ];
} else if ( custom ) {
answer . custom = custom ;
} else {
answer . skipped = true ;
}
return answer ;
});
var noteNode = panel . querySelector ( '[data-page-ai-pi-lab-ui-note]' );
var value = { cancelled : false , answers : answers };
if ( noteNode && noteNode . value . trim ()) value . message = noteNode . value . trim ();
return { value : value };
}
function showPiUiDialog ( request ) {
closePiUiDialog ();
request = request || {};
var method = String ( request . method || '' ). trim ();
var isAskUser = method === 'ask_user' || method === 'ask-user' || method === 'questionnaire' || Array . isArray ( request . questions );
var title = String ( request . title || request . question || ( method === 'confirm' ? '确认操作' : 'Pi 需要输入' )). trim ();
var message = String ( request . message || request . context || '' ). trim ();
var panel = el ( 'div' , {
className : 'wolai-page-ai-pi-lab-ui-backdrop' ,
'data-page-ai-pi-lab-ui-dialog' : method || 'dialog' ,
});
var dialog = el ( 'section' , { className : 'wolai-page-ai-pi-lab-ui-dialog' , role : 'dialog' , 'aria-modal' : 'false' });
dialog . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-dialog-header' }, [
el ( 'div' , {}, [
el ( 'strong' , {}, title ),
el ( 'span' , {}, message || ( method === 'select' ? '请选择一个选项' : '' )),
]),
el ( 'button' , { type : 'button' , className : 'wolai-page-ai-pi-lab-ui-collapse' , 'data-page-ai-pi-lab-ui-collapse' : 'true' , title : '折叠/展开' }, '▾' ),
]));
var body = el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-dialog-body' });
var inputNode = null ;
var askQuestions = [];
var finish = function ( response ) {
closePiUiDialog ();
sendPiUiResponse ( request , response || {});
};
if ( isAskUser ) {
askQuestions = normalizePiAskQuestions ( request );
askQuestions . forEach ( function ( question , index ) { renderPiAskQuestion ( body , question , index ); });
body . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-note' }, [
el ( 'label' , {}, 'Note to assistant' ),
el ( 'textarea' , { 'data-page-ai-pi-lab-ui-note' : 'true' , placeholder : '可选补充说明' }),
]));
} else if ( method === 'select' ) {
( Array . isArray ( request . options ) ? request . options : []). forEach ( function ( option ) {
var value = typeof option === 'string' ? option : String (( option && ( option . value || option . label )) || '' );
var label = typeof option === 'string' ? option : String (( option && ( option . label || option . value )) || '' );
body . appendChild ( el ( 'button' , {
type : 'button' ,
className : 'wolai-page-ai-pi-lab-ui-option' ,
'data-page-ai-pi-lab-ui-option' : value ,
textContent : label || value ,
}));
});
} else if ( method === 'input' ) {
inputNode = el ( 'input' , {
'data-page-ai-pi-lab-ui-input' : 'true' ,
placeholder : request . placeholder || '' ,
value : request . prefill || '' ,
});
body . appendChild ( inputNode );
} else if ( method === 'editor' ) {
inputNode = el ( 'textarea' , {
'data-page-ai-pi-lab-ui-input' : 'true' ,
placeholder : request . placeholder || '' ,
textContent : request . prefill || '' ,
});
body . appendChild ( inputNode );
}
if ( ! body . childNodes . length && method !== 'confirm' ) {
body . appendChild ( el ( 'div' , { textContent : message || 'Pi extension UI request' }));
}
dialog . appendChild ( body );
var actions = el ( 'div' , { className : 'wolai-page-ai-pi-lab-ui-dialog-actions' });
actions . appendChild ( el ( 'button' , { type : 'button' , 'data-page-ai-pi-lab-ui-cancel' : 'true' }, '取消' ));
if ( method !== 'select' ) actions . appendChild ( el ( 'button' , { type : 'button' , 'data-page-ai-pi-lab-ui-submit' : 'true' , 'data-primary' : 'true' }, method === 'confirm' ? '确认' : '提交' ));
dialog . appendChild ( actions );
panel . appendChild ( dialog );
var composer = piLabPanelEl && piLabPanelEl . querySelector ( '.wolai-page-ai-pi-lab-composer' );
if ( composer ) composer . insertBefore ( panel , composer . firstChild );
else document . body . appendChild ( panel );
activePiUiDialog = panel ;
var timeout = Number ( request . timeout || request . timeoutMs || 0 );
var timer = timeout > 0 ? setTimeout ( function () { finish ({ cancelled : true }); }, timeout ) : null ;
var finishOnce = function ( response ) {
if ( timer ) clearTimeout ( timer );
finish ( response );
};
panel . addEventListener ( 'click' , function ( event ) {
if ( event . target && event . target . closest && event . target . closest ( '[data-page-ai-pi-lab-ui-collapse]' )) {
var collapsed = dialog . getAttribute ( 'data-collapsed' ) === 'true' ;
dialog . setAttribute ( 'data-collapsed' , collapsed ? 'false' : 'true' );
event . target . textContent = collapsed ? '▾' : '▸' ;
return ;
}
var option = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-ui-option]' ) : null ;
if ( option && isAskUser ) {
var questionIndex = option . getAttribute ( 'data-question-index' );
var question = askQuestions [ Number ( questionIndex )];
if ( question && question . multiSelect ) {
var nextSelected = option . getAttribute ( 'data-selected' ) !== 'true' ;
option . setAttribute ( 'data-selected' , nextSelected ? 'true' : 'false' );
var mark = option . querySelector ( '.wolai-page-ai-pi-lab-ui-option-mark' );
if ( mark ) mark . textContent = nextSelected ? '▣' : '□' ;
} else {
panel . querySelectorAll ( '[data-question-index="' + questionIndex + '"][data-selected="true"]' ). forEach ( function ( node ) {
node . setAttribute ( 'data-selected' , 'false' );
var oldMark = node . querySelector ( '.wolai-page-ai-pi-lab-ui-option-mark' );
if ( oldMark ) oldMark . textContent = '○' ;
});
option . setAttribute ( 'data-selected' , 'true' );
var selectedMark = option . querySelector ( '.wolai-page-ai-pi-lab-ui-option-mark' );
if ( selectedMark ) selectedMark . textContent = '◉' ;
}
return ;
}
if ( option ) {
finishOnce ({ value : option . getAttribute ( 'data-page-ai-pi-lab-ui-option' ) || option . textContent || '' });
return ;
}
if ( event . target && event . target . closest && event . target . closest ( '[data-page-ai-pi-lab-ui-cancel]' )) {
finishOnce ( isAskUser ? { value : { cancelled : true , answers : [] }, cancelled : true } : { cancelled : true });
return ;
}
if ( event . target && event . target . closest && event . target . closest ( '[data-page-ai-pi-lab-ui-submit]' )) {
if ( method === 'confirm' ) finishOnce ({ confirmed : true });
else if ( isAskUser ) finishOnce ( collectPiAskResponse ( panel , askQuestions ));
else finishOnce ({ value : inputNode ? inputNode . value : '' });
}
});
panel . addEventListener ( 'keydown' , function ( event ) {
if ( event . key === 'Escape' ) {
event . preventDefault ();
finishOnce ( isAskUser ? { value : { cancelled : true , answers : [] }, cancelled : true } : { cancelled : true });
}
if ( event . key === 'Enter' && method === 'input' ) {
event . preventDefault ();
finishOnce ({ value : inputNode ? inputNode . value : '' });
}
});
setTimeout ( function () {
var focusTarget = inputNode || panel . querySelector ( '[data-page-ai-pi-lab-ui-option], [data-page-ai-pi-lab-ui-submit]' );
if ( focusTarget && typeof focusTarget . focus === 'function' ) focusTarget . focus ();
}, 20 );
return panel ;
}
function handlePiExtensionUiRequest ( payload ) {
if ( ! payload || payload . type !== 'extension_ui_request' ) return false ;
if ( payload . method === 'notify' ) {
showPiToast ( payload . message || payload . title || 'Pi notification' , payload . notifyType || 'info' );
return true ;
}
if ( payload . method === 'setWidget' || payload . method === 'set_widget' ) {
renderPiCustomUiDialog ( payload );
return true ;
}
if ( payload . method === 'custom' ) {
if ( payload . close || payload . mode === 'close' ) {
closePiCustomUiDialog ();
sendPiUiResponse ( payload , { value : { closed : true } });
} else {
if ( Array . isArray ( payload . lines ) || Array . isArray ( payload . widgetLines ) || payload . content ) renderPiCustomUiDialog ( payload );
if ( piCustomUiState . keyQueue . length > 0 || piCustomUiState . submitted || ! piCustomUiState . panel ) {
respondToPiCustomUiPoll ( payload );
} else {
piCustomUiState . pendingCustomRequest = payload ;
}
}
return true ;
}
if ( payload . method === 'confirm' || payload . method === 'select' || payload . method === 'input' || payload . method === 'editor' || payload . method === 'ask_user' || payload . method === 'ask-user' || payload . method === 'questionnaire' || Array . isArray ( payload . questions )) {
showPiUiDialog ( payload );
return true ;
}
return false ;
}
function updateBuiltinManaged () {
2026-07-04 21:47:42 +08:00
updateConfig ();
}
function updateReceiptDisplay () {
if ( ! piLabPanelEl ) return ;
var receipts = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-receipts]' );
var count = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-receipt-count]' );
if ( ! receipts ) return ;
if ( count ) count . textContent = String ( piLabState . receipts . length );
receipts . innerHTML = '' ;
if ( ! piLabState . receipts . length ) {
receipts . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-receipt' }, 'No tool receipt yet' ));
return ;
}
piLabState . receipts . slice ( - 8 ). reverse (). forEach ( function ( receipt ) {
2026-07-10 10:54:34 +08:00
var label = ( receipt . toolName || 'tool' ) + ' · ' + ( receipt . allowed === false ? 'denied' : 'allowed' );
if ( receipt . approvalRequired ) label += receipt . approvalConfirmed ? ' · approved' : ' · approval required' ;
if ( receipt . denyReason ) label += ' · ' + receipt . denyReason ;
2026-07-04 21:47:42 +08:00
if ( receipt . diffSummary ) label += ' · diff ' + receipt . diffSummary ;
if ( receipt . citationCount ) label += ' · citations ' + receipt . citationCount ;
if ( receipt . afterFileVersion ) label += ' · v ' + String ( receipt . afterFileVersion ). slice ( 0 , 8 );
receipts . appendChild ( el ( 'div' , {
className : 'wolai-page-ai-pi-lab-receipt' ,
'data-allowed' : receipt . allowed === false ? 'false' : 'true' ,
}, label ));
});
}
2026-07-10 10:54:34 +08:00
function collectPiArtifacts () {
var artifacts = [];
var seen = {};
piLabState . messages . forEach ( function ( msg ) {
if ( ! msg ) return ;
( Array . isArray ( msg . citations ) ? msg . citations : []). forEach ( function ( cit , index ) {
var key = 'citation:' + ( cit . url || cit . source || cit . title || index );
if ( seen [ key ]) return ;
seen [ key ] = true ;
artifacts . push ({
kind : 'citation' ,
title : cit . title || cit . source || 'Citation' ,
summary : cit . source || cit . url || '' ,
url : cit . url || '#' ,
});
});
if ( msg . diffSummary && Array . isArray ( msg . diffSummary . files )) {
msg . diffSummary . files . forEach ( function ( file ) {
if ( ! file ) return ;
var key = 'diff:' + file ;
if ( seen [ key ]) return ;
seen [ key ] = true ;
artifacts . push ({ kind : 'diff' , title : file , summary : '文件变更' });
});
}
( Array . isArray ( msg . toolCalls ) ? msg . toolCalls : []). forEach ( function ( tc ) {
if ( ! tc ) return ;
var name = toolDisplayName ({ toolName : tc . name , args : tc . args , details : tc . details });
if ( String ( name || '' ). indexOf ( 'mcp:' ) !== 0 ) return ;
var key = 'mcp:' + ( tc . id || name );
if ( seen [ key ]) return ;
seen [ key ] = true ;
artifacts . push ({
kind : 'mcp' ,
title : name ,
summary : extractResultText ( tc . result || tc . partialResult || tc . details || tc . args ). slice ( 0 , 280 ),
raw : tc . result || tc . details || tc . args || {},
});
});
});
piLabState . changedFiles . forEach ( function ( file ) {
if ( ! file || seen [ 'diff:' + file ]) return ;
seen [ 'diff:' + file ] = true ;
artifacts . push ({ kind : 'diff' , title : file , summary : '文件变更' });
});
return artifacts ;
}
function renderArtifactItem ( artifact ) {
var item = el ( 'div' , {
className : 'wolai-page-ai-pi-lab-artifact' ,
'data-page-ai-pi-lab-artifact' : artifact . kind || 'artifact' ,
'data-kind' : artifact . kind || 'artifact' ,
});
item . appendChild ( el ( 'strong' , {}, artifact . title || 'Artifact' ));
if ( artifact . summary ) item . appendChild ( el ( 'span' , {}, artifact . summary ));
var actions = el ( 'div' , { className : 'wolai-page-ai-pi-lab-artifact-actions' });
var markdown = '- ' + ( artifact . title || 'Artifact' ) + ( artifact . summary ? ': ' + artifact . summary : '' ) + ( artifact . url ? ' (' + artifact . url + ')' : '' );
if ( artifact . kind === 'citation' ) {
actions . appendChild ( el ( 'a' , {
href : artifact . url || '#' ,
target : '_blank' ,
rel : 'noopener' ,
'data-page-ai-pi-lab-artifact-open' : artifact . url || '#' ,
'data-page-ai-pi-lab-artifact-title' : artifact . title || 'Citation' ,
}, '打开引用' ));
}
actions . appendChild ( el ( 'button' , {
type : 'button' ,
'data-page-ai-pi-lab-artifact-copy-markdown' : markdown ,
}, '复制 MD' ));
if ( artifact . raw ) {
actions . appendChild ( el ( 'button' , {
type : 'button' ,
'data-page-ai-pi-lab-artifact-copy' : compactJson ( artifact . raw ),
}, '复制 raw' ));
}
item . appendChild ( actions );
return item ;
}
function updateArtifactRail () {
if ( ! piLabPanelEl ) return ;
var container = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-artifacts]' );
var count = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-artifact-count]' );
var body = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-body]' );
if ( ! container ) return ;
var artifacts = collectPiArtifacts ();
if ( count ) count . textContent = String ( artifacts . length );
if ( ! artifacts . length && body ) body . setAttribute ( 'data-rail-open' , 'false' );
container . innerHTML = '' ;
if ( ! artifacts . length ) {
container . appendChild ( el ( 'div' , { className : 'wolai-page-ai-pi-lab-receipt' }, 'No artifact yet' ));
return ;
}
artifacts . slice ( - 12 ). reverse (). forEach ( function ( artifact ) {
container . appendChild ( renderArtifactItem ( artifact ));
});
}
2026-07-04 21:47:42 +08:00
function setState ( newState ) {
piLabState . status = newState ;
2026-07-10 10:54:34 +08:00
if ( piRunViewModel ) piRunViewModel . runtimeStatus = newState ;
2026-07-04 21:47:42 +08:00
updateConfig ();
2026-07-10 10:54:34 +08:00
maybeApplyPendingPermissionMode ();
}
function responseJsonOrError ( response , fallbackMessage ) {
return response . json (). catch ( function () { return {}; }). then ( function ( payload ) {
if ( ! response . ok ) {
var message = ( payload && ( payload . message || payload . error )) || ( fallbackMessage + ': ' + response . status );
var error = new Error ( message );
error . payload = payload ;
throw error ;
}
return payload ;
});
2026-07-04 21:47:42 +08:00
}
function checkStatus () {
return Promise . all ([
fetch ( API . STATUS , { credentials : 'same-origin' , cache : 'no-store' }). then ( function ( r ) { return r . json (); }),
loadEffectiveConfig (). catch ( function () { return null ; }),
])
. then ( function ( results ) {
var data = results [ 0 ] || {};
piLabState . enabled = !! data . enabled ;
piLabState . sessionId = data . sessionId || piLabState . sessionId || null ;
piLabState . providerSessionId = data . providerSessionId || piLabState . providerSessionId || null ;
// Consume backend status/default fields when present.
if ( data . defaultModelProvider ) piLabState . defaultModelProvider = data . defaultModelProvider ;
if ( data . defaultModelId ) piLabState . defaultModelId = data . defaultModelId ;
2026-07-10 10:54:34 +08:00
if ( data . defaultThinkingLevel ) piLabState . defaultThinkingLevel = normalizeThinkingLevel ( data . defaultThinkingLevel );
2026-07-04 21:47:42 +08:00
piLabState . modelProvider = ( data . session && data . session . modelProvider ) || piLabState . modelProvider || null ;
piLabState . modelId = ( data . session && data . session . modelId ) || piLabState . modelId || null ;
2026-07-10 10:54:34 +08:00
var sessionThinkingLevel = data . session && data . session . thinkingLevel ;
if ( sessionThinkingLevel && data . session && data . session . status === 'runtime_running' ) {
piLabState . thinkingLevel = normalizeThinkingLevel ( sessionThinkingLevel );
} else if ( ! piLabState . thinkingLevel ) {
piLabState . thinkingLevel = piLabState . defaultThinkingLevel ;
}
piLabState . permissionMode = normalizePermissionMode (( data . session && ( data . session . permissionMode || ( data . session . runtimePolicySnapshot && data . session . runtimePolicySnapshot . permissionMode ))) || data . permissionMode || piLabState . permissionMode );
2026-07-04 21:47:42 +08:00
piLabState . session = data . session || piLabState . session || null ;
piLabState . allowedRootsSummary = summarizeAllowedRoots ( data . session && data . session . allowedRootsSnapshot );
piLabState . runtimeMode = data . runtimeMode || ( data . session && data . session . runtimeMode ) || piLabState . runtimeMode || null ;
2026-07-10 10:54:34 +08:00
piLabState . runtimeImplementation = data . runtimeImplementation || data . runtime_implementation || piLabState . runtimeImplementation || null ;
piLabState . runtimeBinary = data . runtimeBinary || data . runtime_binary || piLabState . runtimeBinary || null ;
piLabState . runtimeAvailable = data . runtimeAvailable !== false ;
piLabState . runtimeInstallHint = data . runtimeInstallHint || data . runtime_install_hint || '' ;
piLabState . runtimeError = data . runtimeError || ( data . session && data . session . runtimeError ) || '' ;
2026-07-04 21:47:42 +08:00
piLabState . runtimePid = data . pid || ( data . session && data . session . runtimePid ) || piLabState . runtimePid || null ;
2026-07-10 10:54:34 +08:00
piLabState . managedBuiltinTools = data . managedPiBuiltinTools || data . managedBuiltinTools || piLabState . managedBuiltinTools || [];
piLabState . piExtensionSources = data . piExtensionSources || ( data . session && data . session . piExtensionSources ) || piLabState . piExtensionSources || [];
piLabState . piExtensionToolNames = data . piExtensionToolNames || piLabState . piExtensionToolNames || [];
2026-07-04 21:47:42 +08:00
if ( data . session && data . session . status === 'runtime_running' ) {
piLabState . sessionId = data . session . sessionId ;
setState ( STATE_STARTED );
if ( ! piLabEventSource || piLabEventSource . readyState === EventSource . CLOSED ) connectEventSource ( data . session . sessionId );
}
2026-07-10 10:54:34 +08:00
updateBuiltinManaged ();
2026-07-04 21:47:42 +08:00
updateReceiptDisplay ();
updateDiagnostics ( '' );
updateMessages ();
return data ;
})
. catch ( function ( error ) {
piLabState . enabled = false ;
setState ( STATE_ERROR );
updateDiagnostics ( error && error . message ? error . message : 'status failed' );
});
}
function startRuntime () {
2026-07-10 10:54:34 +08:00
exitHistoryReplayMode ();
if ( piLabStartPromise ) return piLabStartPromise ;
if ( piLabState . status === STATE_STREAMING ) return Promise . resolve ({ skipped : true });
if ( piLabState . status === STATE_STARTED && piLabState . sessionId ) return Promise . resolve ({ ok : true , session : piLabState . session });
piLabStartPromise = Promise . resolve ()
. then ( function () {
return piLabState . enabled ? null : checkStatus ();
})
. then ( function () {
if ( ! piLabState . enabled ) throw new Error ( 'Pi Rust 服务未启用' );
if ( piLabState . runtimeAvailable === false ) throw new Error ( piLabState . runtimeInstallHint || 'Pi Rust runtime 不可用' );
var context = applyCurrentContextToState ();
applyModelControls ();
setState ( STATE_STARTING );
updateDiagnostics ( 'Starting Pi Rust runtime' );
return fetch ( API . START , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({
sessionId : piLabState . sessionId || undefined ,
rootUri : context . rootUri || ( piLabState . session && piLabState . session . rootUri ) || undefined ,
workspaceId : context . workspaceId || ( piLabState . session && piLabState . session . workspaceId ) || undefined ,
pagePath : context . pagePath || ( piLabState . session && piLabState . session . pagePath ) || undefined ,
pageTitle : context . pageTitle || ( piLabState . session && piLabState . session . pageTitle ) || undefined ,
modelProvider : piLabState . modelProvider || piLabState . defaultModelProvider ,
modelId : piLabState . modelId || piLabState . defaultModelId ,
thinkingLevel : normalizeThinkingLevel ( piLabState . thinkingLevel || piLabState . defaultThinkingLevel ),
permissionMode : normalizePermissionMode ( piLabState . permissionMode ),
}),
}). then ( function ( response ) {
return responseJsonOrError ( response , 'Start failed' );
});
2026-07-04 21:47:42 +08:00
})
. then ( function ( data ) {
var session = data . session || {};
piLabState . sessionId = session . sessionId || data . sessionId || piLabState . sessionId ;
piLabState . providerSessionId = session . providerSessionId || data . providerSessionId || piLabState . providerSessionId ;
piLabState . modelProvider = session . modelProvider || piLabState . modelProvider || null ;
piLabState . modelId = session . modelId || piLabState . modelId || null ;
2026-07-10 10:54:34 +08:00
piLabState . thinkingLevel = normalizeThinkingLevel ( session . thinkingLevel || data . thinkingLevel || piLabState . thinkingLevel );
piLabState . permissionMode = normalizePermissionMode ( session . permissionMode || data . permissionMode || ( session . runtimePolicySnapshot && session . runtimePolicySnapshot . permissionMode ) || piLabState . permissionMode );
piLabState . session = session || piLabState . session || null ;
2026-07-04 21:47:42 +08:00
piLabState . runtimeMode = session . runtimeMode || data . runtimeMode || piLabState . runtimeMode || null ;
2026-07-10 10:54:34 +08:00
piLabState . runtimeImplementation = data . runtimeImplementation || data . runtime_implementation || piLabState . runtimeImplementation || null ;
piLabState . runtimeBinary = data . runtimeBinary || data . runtime_binary || piLabState . runtimeBinary || null ;
piLabState . runtimeAvailable = data . runtimeAvailable !== false ;
piLabState . runtimeInstallHint = data . runtimeInstallHint || data . runtime_install_hint || '' ;
piLabState . runtimeError = session . runtimeError || data . runtimeError || '' ;
2026-07-04 21:47:42 +08:00
piLabState . runtimePid = session . runtimePid || data . pid || piLabState . runtimePid || null ;
2026-07-10 10:54:34 +08:00
piLabState . managedBuiltinTools = data . managedPiBuiltinTools || data . managedBuiltinTools || piLabState . managedBuiltinTools || [];
piLabState . piExtensionSources = data . piExtensionSources || piLabState . piExtensionSources || [];
piLabState . piExtensionToolNames = data . piExtensionToolNames || piLabState . piExtensionToolNames || [];
2026-07-04 21:47:42 +08:00
connectEventSource ();
setState ( STATE_STARTED );
updateDiagnostics ( 'Pi runtime ready' );
updateMessages ();
return data ;
})
. catch ( function ( err ) {
setState ( STATE_ERROR );
updateDiagnostics ( 'Start error: ' + err . message );
throw err ;
2026-07-10 10:54:34 +08:00
})
. finally ( function () {
piLabStartPromise = null ;
2026-07-04 21:47:42 +08:00
});
2026-07-10 10:54:34 +08:00
return piLabStartPromise ;
}
2026-07-04 21:47:42 +08:00
2026-07-10 10:54:34 +08:00
function handleToolCallEventPayload ( payload ) {
payload = payload || {};
applyToolCallSideEffects ({
ok : payload . allowed !== false ,
toolName : payload . toolName ,
result : {
rootUri : payload . rootUri ,
relativePath : payload . relativePath ,
diffSummary : payload . diffSummary ,
},
});
piLabState . receipts . push ({
toolName : payload . toolName ,
allowed : payload . allowed !== false ,
denyReason : payload . denyReason || null ,
diffSummary : payload . diffSummary || null ,
citationCount : payload . citationCount || 0 ,
beforeFileVersion : payload . beforeFileVersion || null ,
afterFileVersion : payload . afterFileVersion || null ,
approvalRequired : payload . approvalRequired === true ,
approvalConfirmed : payload . approvalConfirmed === true ,
});
upsertToolCall ({
toolCallId : payload . toolCallId || payload . id || ( 'receipt:' + ( payload . toolName || 'tool' ) + ':' + piLabState . receipts . length ),
toolName : payload . toolName || 'tool' ,
args : payload . params || payload . args || {},
}, payload . allowed === false ? 'denied' : 'done' , {
result : {
allowed : payload . allowed !== false ,
denyReason : payload . denyReason || null ,
diffSummary : payload . diffSummary || null ,
citationCount : payload . citationCount || 0 ,
},
approvalRequired : payload . approvalRequired === true ,
approvalConfirmed : payload . approvalConfirmed === true ,
toolPolicy : payload . toolPolicy || '' ,
});
if ( payload . normalizedFilePath && payload . diffSummary ) piLabState . changedFiles . push ( payload . normalizedFilePath );
updateReceiptDisplay ();
updateContextStrip ();
}
function connectEventSource ( sessionId ) {
2026-07-04 21:47:42 +08:00
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
var sid = sessionId || piLabState . sessionId ;
if ( ! sid ) return ;
var es = new EventSource ( API . EVENTS + '?sessionId=' + encodeURIComponent ( sid ), { withCredentials : true });
es . addEventListener ( 'connected' , function () { updateDiagnostics ( 'SSE connected' ); });
es . addEventListener ( 'runtime_started' , function ( e ) {
try {
var data = JSON . parse ( e . data );
var payload = data . payload || data ;
piLabState . runtimePid = payload . pid || piLabState . runtimePid ;
piLabState . runtimeMode = payload . runtimeMode || piLabState . runtimeMode ;
2026-07-10 10:54:34 +08:00
piLabState . managedBuiltinTools = payload . managedBuiltinTools || payload . managedPiBuiltinTools || piLabState . managedBuiltinTools ;
piLabState . piExtensionSources = payload . piExtensionSources || piLabState . piExtensionSources || [];
piLabState . piExtensionToolNames = payload . piExtensionToolNames || piLabState . piExtensionToolNames || [];
2026-07-04 21:47:42 +08:00
setState ( STATE_STARTED );
updateDiagnostics ( 'runtime_started' );
} catch ( _ ) {}
});
es . addEventListener ( 'pi_rpc_event' , function ( e ) {
try {
var data = JSON . parse ( e . data );
handlePiRpcEvent ( data . payload || data );
} catch ( _ ) {}
});
2026-07-10 10:54:34 +08:00
es . addEventListener ( 'tool_call' , function ( e ) {
try {
var data = JSON . parse ( e . data );
var payload = data . payload || data ;
handleToolCallEventPayload ( payload );
} catch ( _ ) {}
});
es . addEventListener ( 'runtime_aborted' , function ( e ) {
var payload = { reason : '已中止' , stopReason : 'aborted' };
try {
var data = JSON . parse ( e . data );
payload = Object . assign ( payload , data . payload || data || {});
} catch ( _ ) {}
applyPiRunEvent ({ type : 'assistant_abort' , payload : payload });
setState ( STATE_ABORTED );
updateMessages ();
});
es . addEventListener ( 'runtime_stdout_closed' , function ( e ) {
var payload = { message : 'Pi Rust runtime stdout closed' };
try {
var data = JSON . parse ( e . data );
payload = Object . assign ( payload , data . payload || data || {});
} catch ( _ ) {}
piLabState . runtimePid = null ;
if ( payload . duringTurn || piLabState . status === STATE_STREAMING ) {
applyPiRunEvent ({
type : 'assistant_error' ,
payload : { error : payload . message || 'Pi Rust runtime 已退出,未返回回复' },
});
setState ( STATE_ERROR );
updateMessages ();
} else {
setState ( STATE_IDLE );
}
updateDiagnostics ( payload . message || 'runtime_stdout_closed' );
});
2026-07-04 21:47:42 +08:00
es . addEventListener ( 'error' , function () {
if ( piLabEventSource && piLabEventSource . readyState === EventSource . CLOSED ) updateDiagnostics ( 'SSE connection closed' );
});
piLabEventSource = es ;
}
function ensureStreamingAssistant () {
2026-07-10 10:54:34 +08:00
if ( ! streamingAssistantMsg ) applyPiRunEvent ({ type : 'assistant_begin' });
setState ( STATE_STREAMING );
2026-07-04 21:47:42 +08:00
return streamingAssistantMsg ;
}
function extractVisibleAssistantText ( message ) {
if ( ! message || ! Array . isArray ( message . content )) return '' ;
return message . content
. filter ( function ( part ) { return part && part . type === 'text' && typeof part . text === 'string' ; })
. map ( function ( part ) { return part . text ; })
. join ( '' );
}
2026-07-10 10:54:34 +08:00
function handlePiMessageEvent ( payload ) {
var message = payload && payload . message ;
if ( ! message || ! message . role ) return false ;
if ( message . role === 'assistant' ) {
var content = Array . isArray ( message . content ) ? message . content : [];
var toolParts = content . filter ( function ( part ) { return part && part . type === 'toolCall' ; });
toolParts . forEach ( function ( part ) {
upsertToolCall ({
toolCallId : part . id || part . toolCallId || '' ,
toolName : part . name || part . toolName || 'tool' ,
args : part . arguments || part . args || {},
}, 'running' );
});
var visibleText = extractVisibleAssistantText ( message );
if ( visibleText ) {
applyPiRunEvent ({ type : 'assistant_done' , text : visibleText });
setState ( STATE_STARTED );
updateMessages ();
}
return toolParts . length > 0 || !! visibleText ;
}
if ( message . role === 'toolResult' ) {
upsertToolCall ({
toolCallId : message . toolCallId || message . tool_call_id || '' ,
toolName : message . toolName || message . tool_name || 'tool' ,
details : message . details || {},
}, message . isError ? 'error' : 'done' , {
result : {
content : message . content || [],
details : message . details || {},
},
isError : message . isError === true ,
});
return true ;
}
return false ;
}
2026-07-04 21:47:42 +08:00
function handlePiRpcEvent ( payload ) {
if ( ! payload ) return ;
var eventType = payload . type ;
var msgData = payload . assistantMessageEvent || payload ;
var msgDataType = msgData && msgData . type ;
2026-07-10 10:54:34 +08:00
if ( handlePiExtensionUiRequest ( payload )) return ;
2026-07-04 21:47:42 +08:00
if ( msgDataType === 'thinking_start' || msgDataType === 'thinking_delta' || msgDataType === 'thinking_end' ) {
updateDiagnostics ( 'Pi thinking event hidden from visible reply' );
return ;
}
2026-07-10 10:54:34 +08:00
if ( eventType === 'queue_update' ) {
applyPiRunEvent ({ type : 'queue_update' , payload : payload });
updateQueueDisplay ();
return ;
}
if ( eventType === 'message' && handlePiMessageEvent ( payload )) return ;
2026-07-04 21:47:42 +08:00
if ( msgDataType === 'text_start' ) {
2026-07-10 10:54:34 +08:00
setState ( STATE_STREAMING );
applyPiRunEvent ({ type : 'assistant_begin' });
2026-07-04 21:47:42 +08:00
updateMessages ();
return ;
}
if ( msgDataType === 'text_delta' ) {
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'assistant_delta' , payload : msgData });
2026-07-04 21:47:42 +08:00
updateMessages ();
} else if ( msgDataType === 'text_end' ) {
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'assistant_text_end' , payload : msgData });
2026-07-04 21:47:42 +08:00
updateMessages ();
} else if ( eventType === 'message_end' && payload . message && payload . message . role === 'assistant' ) {
var visibleText = extractVisibleAssistantText ( payload . message );
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'assistant_done' , text : visibleText });
2026-07-04 21:47:42 +08:00
setState ( STATE_STARTED );
updateMessages ();
} else if ( eventType === 'agent_end' ) {
var finalMessages = Array . isArray ( payload . messages ) ? payload . messages : [];
var finalAssistant = finalMessages . slice (). reverse (). find ( function ( message ) { return message && message . role === 'assistant' ; });
var finalText = extractVisibleAssistantText ( finalAssistant );
var lastVisibleAssistant = piLabState . messages . slice (). reverse (). find ( function ( message ) { return message && message . role === 'assistant' ; });
if ( ! streamingAssistantMsg && lastVisibleAssistant && lastVisibleAssistant . status === 'done' ) {
if ( finalText && ! lastVisibleAssistant . text ) {
lastVisibleAssistant . text = finalText ;
updateMessages ();
}
setState ( STATE_STARTED );
return ;
}
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'assistant_done' , text : finalText });
2026-07-04 21:47:42 +08:00
setState ( STATE_STARTED );
updateMessages ();
2026-07-10 10:54:34 +08:00
} else if ( eventType === 'tool_execution_start' || eventType === 'tool_call_start' || msgDataType === 'tool_call_start' || msgDataType === 'toolcall_start' ) {
var startToolPayload = eventType === 'tool_execution_start' || eventType === 'tool_call_start' ? payload : assistantToolCallPayload ( msgData );
if ( startToolPayload ) upsertToolCall ( startToolPayload , 'running' );
} else if ( eventType === 'tool_execution_update' ) {
upsertToolCall ( payload , 'running' , { partialResult : payload . partialResult || payload . result || null });
} else if ( eventType === 'tool_execution_end' || eventType === 'tool_call_end' || msgDataType === 'tool_call_end' || msgDataType === 'toolcall_end' ) {
var endToolPayload = eventType === 'tool_execution_end' || eventType === 'tool_call_end' ? payload : assistantToolCallPayload ( msgData );
if ( endToolPayload ) {
upsertToolCall ( endToolPayload , payload . isError ? 'error' : 'done' , {
result : payload . result || msgData . result || endToolPayload . result || {},
isError : payload . isError === true ,
});
2026-07-04 21:47:42 +08:00
}
2026-07-10 10:54:34 +08:00
} else if ( msgDataType === 'toolcall_delta' ) {
var deltaToolPayload = assistantToolCallPayload ( msgData );
if ( deltaToolPayload ) upsertToolCall ( deltaToolPayload , 'running' , { args : deltaToolPayload . args || {} });
} else if ( eventType === 'response' && payload . command === 'abort' ) {
applyPiRunEvent ({ type : 'assistant_abort' , payload : payload });
setState ( STATE_ABORTED );
2026-07-04 21:47:42 +08:00
updateMessages ();
} else if ( eventType === 'citation' || msgDataType === 'citation' ) {
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'citation' , payload : { source : payload . source || msgData . source || '' , title : payload . title || msgData . title || '' , url : payload . url || msgData . url || '#' } });
2026-07-04 21:47:42 +08:00
updateMessages ();
} else if ( eventType === 'diff' || msgDataType === 'diff' ) {
2026-07-10 10:54:34 +08:00
var diffFiles = payload . files || msgData . files || [];
applyPiRunEvent ({ type : 'diff' , payload : { files : diffFiles } });
diffFiles . forEach ( function ( file ) { if ( file ) piLabState . changedFiles . push ( file ); });
2026-07-04 21:47:42 +08:00
updateMessages ();
} else if ( eventType === 'done' || msgDataType === 'done' ) {
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({
type : 'assistant_done' ,
payload : {
text : msgData . text ,
toolCalls : msgData . toolCalls ,
citations : msgData . citations ,
diffSummary : msgData . diffSummary ,
},
});
2026-07-04 21:47:42 +08:00
setState ( STATE_STARTED );
updateMessages ();
} else if ( eventType === 'error' || msgDataType === 'error' ) {
2026-07-10 10:54:34 +08:00
applyPiRunEvent ({ type : 'assistant_error' , payload : { error : payload . error || msgData . message || 'Pi runtime error' } });
2026-07-04 21:47:42 +08:00
setState ( STATE_STARTED );
updateMessages ();
}
}
2026-07-10 10:54:34 +08:00
function sendPrompt ( text , options ) {
2026-07-04 21:47:42 +08:00
var prompt = String ( text || '' ). trim ();
if ( ! prompt ) return ;
2026-07-10 10:54:34 +08:00
var streamingBehavior = options && options . streamingBehavior ;
var midStream = piLabState . status === STATE_STREAMING && streamingAssistantMsg ;
applyPiRunEvent ({ type : 'user_prompt' , text : prompt , meta : midStream ? ( streamingBehavior === 'followUp' ? 'follow-up queued' : 'steer' ) : '' });
if ( ! midStream ) applyPiRunEvent ({ type : 'assistant_begin' });
2026-07-04 21:47:42 +08:00
setState ( STATE_STREAMING );
updateMessages ();
2026-07-10 10:54:34 +08:00
var context = applyCurrentContextToState ();
var contextSelection = selectedContextPayload ( context );
var refs = contextSelection . refs || [];
var selectedFolderPath = currentFolderPathFromPagePath ( context . pagePath );
var includeAnyContext = refs . length > 0 ;
var includePageContext = refs . indexOf ( 'current_page' ) >= 0 || refs . indexOf ( 'selection' ) >= 0 || refs . indexOf ( 'lightrag' ) >= 0 ;
var includeFolderContext = refs . indexOf ( 'folder' ) >= 0 ;
var body = {
sessionId : piLabState . sessionId ,
message : prompt ,
rootUri : includeAnyContext ? ( context . rootUri || undefined ) : undefined ,
workspaceId : includeAnyContext ? ( context . workspaceId || undefined ) : undefined ,
pagePath : includePageContext ? ( context . pagePath || undefined ) : undefined ,
pageTitle : includePageContext ? ( context . pageTitle || undefined ) : undefined ,
folderPath : includeFolderContext && selectedFolderPath !== null ? selectedFolderPath : undefined ,
contextRefs : contextSelection . refs ,
selectedContext : contextSelection ,
};
if ( streamingBehavior ) body . streamingBehavior = streamingBehavior ;
2026-07-04 21:47:42 +08:00
return fetch ( API . SEND , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' },
2026-07-10 10:54:34 +08:00
body : JSON . stringify ( body ),
2026-07-04 21:47:42 +08:00
})
. then ( function ( r ) {
2026-07-10 10:54:34 +08:00
return responseJsonOrError ( r , 'Send failed' );
2026-07-04 21:47:42 +08:00
})
. then ( function ( data ) {
if ( ! data . accepted ) throw new Error ( 'Pi Lab rejected prompt' );
if ( ! piLabEventSource || piLabEventSource . readyState === EventSource . CLOSED ) connectEventSource ( data . sessionId || piLabState . sessionId );
})
. catch ( function ( err ) {
2026-07-10 10:54:34 +08:00
if ( streamingAssistantMsg ) applyPiRunEvent ({ type : 'assistant_error' , message : err . message });
setState ( STATE_STARTED );
updateMessages ();
2026-07-04 21:47:42 +08:00
});
}
function ensureRuntimeReady () {
2026-07-10 10:54:34 +08:00
if ( piLabState . status === STATE_STARTED && piLabState . sessionId ) {
return Promise . resolve ({ ok : true , session : piLabState . session });
}
if ( piLabState . status === STATE_STREAMING && piLabState . sessionId ) {
return Promise . resolve ({ ok : true , session : piLabState . session });
}
if ( piLabState . status === STATE_STARTING || piLabState . status === STATE_IDLE || piLabState . status === STATE_ABORTED || piLabState . status === STATE_ERROR || ! piLabState . sessionId ) {
2026-07-04 21:47:42 +08:00
return startRuntime ();
}
return Promise . resolve ({ ok : true });
}
function callMNoteTool ( toolName , params ) {
return ensureRuntimeReady (). then ( function () {
return fetch ( '/api/page-ai/pi/tool-call' , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' , accept : 'application/json' },
body : JSON . stringify ({
sessionId : piLabState . sessionId ,
toolName : toolName ,
params : params || {},
}),
});
}). then ( function ( response ) {
return response . json (). catch ( function () { return {}; }). then ( function ( payload ) {
if ( ! response . ok ) throw new Error ( payload . message || ( 'Tool call failed: ' + response . status ));
2026-07-10 10:54:34 +08:00
if ( payload && payload . ok === false ) {
var result = payload . result || {};
throw new Error ( result . message || payload . message || result . code || 'Tool call failed' );
}
2026-07-04 21:47:42 +08:00
return payload ;
});
});
}
function emitLocalFolderRefresh ( rootUri , relativePath , source ) {
var normalizedPath = normalizeSlashes ( relativePath ). replace ( /^\/+/ , '' );
if ( ! rootUri || ! normalizedPath ) return false ;
var documentId = /\.md(?:own)?$/i . test ( normalizedPath ) ? 'local-md:' + normalizedPath . replace ( /\//g , '~2F' ) : '' ;
if ( window . __mnoteLocalFolderEventBus && typeof window . __mnoteLocalFolderEventBus . emitChangedFiles === 'function' ) {
window . __mnoteLocalFolderEventBus . emitChangedFiles ({
source : source || 'pi_lab_tool_call' ,
reason : source || 'pi_lab_tool_call' ,
rootUri : rootUri ,
workspaceId : ( piLabState . session && piLabState . session . workspaceId ) || currentWorkspaceId (),
changedFiles : [{
relativePath : normalizedPath ,
documentId : documentId ,
changeType : 'modified' ,
}],
affectedParents : [{
relativePath : normalizedPath . indexOf ( '/' ) >= 0 ? normalizedPath . split ( '/' ). slice ( 0 , - 1 ). join ( '/' ) : '' ,
reason : source || 'pi_lab_tool_call' ,
}],
});
return true ;
}
window . dispatchEvent ( new CustomEvent ( 'tree:local-folder-watch-batch' , {
detail : {
payload : {
schema : 'mnote.local_folder.watch_batch.v1' ,
source : source || 'pi_lab_tool_call' ,
rootUri : rootUri ,
changedPaths : [{ relativePath : normalizedPath , documentId : documentId , changeType : 'modified' }],
},
},
}));
return true ;
}
function refreshPatchedCurrentDocument ( rootUri , relativePath , source ) {
emitLocalFolderRefresh ( rootUri , relativePath , source || 'pi_lab_patch' );
var documentId = /\.md(?:own)?$/i . test ( String ( relativePath || '' )) ? 'local-md:' + normalizeSlashes ( relativePath ). replace ( /^\/+/ , '' ). replace ( /\//g , '~2F' ) : currentDocumentId ();
var api = window . __mnoteDocumentPaneRuntime ;
if ( api && typeof api . refreshDocument === 'function' ) {
api . refreshDocument ({
documentId : documentId ,
workspaceId : ( piLabState . session && piLabState . session . workspaceId ) || currentWorkspaceId (),
source : source || 'pi_lab_patch' ,
}). catch ( function () {});
}
}
function applyToolCallSideEffects ( payload ) {
if ( ! payload || payload . ok !== true ) return ;
var result = payload . result || {};
if ( payload . toolName === 'mnote.local_file.patch' || result . diffSummary ) {
var rootUri = String ( result . rootUri || ( piLabState . session && piLabState . session . rootUri ) || currentRootUri () || '' ). trim ();
var relativePath = String ( result . relativePath || '' ). trim ();
if ( ! relativePath && result . path ) relativePath = result . path ;
if ( relativePath ) refreshPatchedCurrentDocument ( rootUri , relativePath , 'pi_lab_local_file_patch' );
}
}
function setComposerText ( text ) {
if ( ! piLabPanelEl ) return ;
var input = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-input]' );
if ( ! input ) return ;
input . value = text || '' ;
updateButtons ();
input . focus ();
}
function handleQuickAction ( kind ) {
var context = applyCurrentContextToState ();
if ( kind === 'read-page' ) {
if ( ! context . rootUri || ! context . pagePath ) {
2026-07-10 10:54:34 +08:00
showPiToast ( '当前页未绑定文件上下文' , 'warning' );
updateDiagnostics ( '当前页缺少 rootUri/pagePath,无法作为 Pi 上下文' );
2026-07-04 21:47:42 +08:00
return ;
}
2026-07-10 10:54:34 +08:00
toggleContextQuick ( kind );
showPiToast (( contextSelectionState (). currentPage ? '已选择' : '已取消' ) + '当前页上下文' , 'info' );
return ;
}
if ( kind === 'current-folder' ) {
if ( ! context . rootUri || currentFolderPathFromPagePath ( context . pagePath ) === null ) {
showPiToast ( '当前页没有可用文件夹上下文' , 'warning' );
updateDiagnostics ( '当前页缺少 rootUri/folderPath,无法作为 Pi 文件夹上下文' );
return ;
}
toggleContextQuick ( kind );
showPiToast (( contextSelectionState (). currentFolder ? '已选择' : '已取消' ) + '当前文件夹上下文' , 'info' );
2026-07-04 21:47:42 +08:00
return ;
}
if ( kind === 'selection' ) {
var selection = refreshSelectionSummary ();
2026-07-10 10:54:34 +08:00
if ( ! selection && ! contextSelectionState (). selection ) showPiToast ( '当前没有选区' , 'warning' );
toggleContextQuick ( kind );
2026-07-04 21:47:42 +08:00
return ;
}
2026-07-10 10:54:34 +08:00
toggleContextQuick ( 'rag' );
showPiToast (( contextSelectionState (). lightrag ? '已启用' : '已关闭' ) + ' LightRAG 上下文' , 'info' );
2026-07-04 21:47:42 +08:00
}
2026-07-10 10:54:34 +08:00
function sendCurrentInput ( options ) {
2026-07-04 21:47:42 +08:00
if ( ! piLabPanelEl ) return ;
var input = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-input]' );
var text = input ? input . value . trim () : '' ;
if ( ! text ) return ;
2026-07-10 10:54:34 +08:00
if ( piLabState . status === STATE_IDLE || piLabState . status === STATE_ABORTED || piLabState . status === STATE_ERROR || ! piLabState . sessionId ) {
showPiToast ( 'Pi Rust 正在启动,启动后自动发送' , 'info' );
ensureRuntimeReady (). then ( function () {
if ( ! piLabState . sessionId ) throw new Error ( 'Pi Rust 会话尚未就绪' );
if ( input ) input . value = '' ;
updateButtons ();
return sendPrompt ( text , { streamingBehavior : undefined });
}). catch ( function ( err ) {
showPiToast ( err && err . message ? err . message : 'Pi Rust 启动失败' , 'warning' );
updateDiagnostics ( err && err . message ? err . message : 'Pi Rust 启动失败' );
updateButtons ();
});
return ;
}
if ( piLabState . status === STATE_STARTING ) {
showPiToast ( 'Pi Rust 正在启动,启动后自动发送' , 'info' );
ensureRuntimeReady (). then ( function () {
if ( ! piLabState . sessionId ) throw new Error ( 'Pi Rust 会话尚未就绪' );
if ( input ) input . value = '' ;
updateButtons ();
return sendPrompt ( text , { streamingBehavior : undefined });
}). catch ( function ( err ) {
showPiToast ( err && err . message ? err . message : 'Pi Rust 启动失败' , 'warning' );
updateDiagnostics ( err && err . message ? err . message : 'Pi Rust 启动失败' );
updateButtons ();
});
return ;
}
2026-07-04 21:47:42 +08:00
if ( input ) input . value = '' ;
updateButtons ();
2026-07-10 10:54:34 +08:00
var isMidStream = piLabState . status === STATE_STREAMING ;
var streamingBehavior = isMidStream ? (( options && options . streamingBehavior ) || 'steer' ) : undefined ;
sendPrompt ( text , { streamingBehavior : streamingBehavior }). catch ( function () {});
2026-07-04 21:47:42 +08:00
}
function abortPrompt () {
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
2026-07-10 10:54:34 +08:00
if ( piLabState . sessionId ) {
fetch ( API . ABORT , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({ sessionId : piLabState . sessionId }),
}). then ( function ( response ) {
return response . json (). catch ( function () { return {}; });
}). then ( function ( payload ) {
applyPiRunEvent ({ type : 'assistant_abort' , payload : Object . assign ({ stopReason : 'aborted' }, payload || {}) });
updateMessages ();
}). catch ( function () {});
}
applyPiRunEvent ({ type : 'assistant_abort' , payload : { reason : '已中止' , stopReason : 'aborted' } });
setState ( STATE_ABORTED );
updateMessages ();
}
function startNewConversation () {
var previousSessionId = piLabState . sessionId ;
if ( piLabEventSource ) {
piLabEventSource . close ();
piLabEventSource = null ;
}
if ( previousSessionId && ! piLabState . viewingHistorySessionId && ( piLabState . status === STATE_STARTED || piLabState . status === STATE_STREAMING || piLabState . status === STATE_STARTING )) {
2026-07-04 21:47:42 +08:00
fetch ( API . ABORT , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' },
2026-07-10 10:54:34 +08:00
body : JSON . stringify ({ sessionId : previousSessionId }),
2026-07-04 21:47:42 +08:00
}). catch ( function () {});
}
2026-07-10 10:54:34 +08:00
closeActionMenu ();
setHistoryPanelVisible ( false );
exitHistoryReplayMode ();
piLabState . sessionId = null ;
piLabState . providerSessionId = null ;
piLabState . session = null ;
piLabState . thinkingLevel = normalizeThinkingLevel ( piLabState . defaultThinkingLevel || DEFAULT_THINKING_LEVEL );
piLabState . status = STATE_IDLE ;
resetPiRunViewModel ();
piLabState . receipts = [];
2026-07-04 21:47:42 +08:00
updateMessages ();
2026-07-10 10:54:34 +08:00
updateReceiptDisplay ();
updateQueueDisplay ();
updateThinkingControl ();
updateConfig ();
if ( piLabState . active ) {
startRuntime (). catch ( function () {});
}
2026-07-04 21:47:42 +08:00
}
function ensurePanel () {
if ( piLabPanelEl ) return piLabPanelEl ;
var container = document . createElement ( 'div' );
container . innerHTML = piLabPanelHTML ();
piLabPanelEl = container . firstElementChild ;
wireEvents ();
updateReceiptDisplay ();
2026-07-10 10:54:34 +08:00
updateArtifactRail ();
2026-07-04 21:47:42 +08:00
updateMessages ();
return piLabPanelEl ;
}
function summarizeAllowedRoots ( snapshot ) {
if ( ! snapshot ) return piLabState . allowedRootsSummary || 'pending' ;
var roots = Array . isArray ( snapshot . roots ) ? snapshot . roots : ( Array . isArray ( snapshot ) ? snapshot : []);
if ( ! roots . length ) return '0 roots' ;
return roots . length + ' root' + ( roots . length === 1 ? '' : 's' );
}
function ensureDrawer () {
if ( piLabDrawerEl ) return piLabDrawerEl ;
piLabDrawerEl = el ( 'section' , {
className : 'wolai-page-ai-pi-lab-drawer' ,
'data-page-ai-pi-lab' : 'drawer' ,
'data-page-ai-pi-lab-drawer' : 'true' ,
'aria-label' : 'Pi Lab' ,
hidden : 'true' ,
});
piLabDrawerEl . appendChild ( ensurePanel ());
document . body . appendChild ( piLabDrawerEl );
return piLabDrawerEl ;
}
function setDrawerVisible ( visible ) {
var drawer = ensureDrawer ();
drawer . hidden = ! visible ;
drawer . setAttribute ( 'data-open' , visible ? 'true' : 'false' );
if ( visible ) drawer . setAttribute ( 'data-minimized' , 'false' );
}
function showPiLab () {
piLabState . active = true ;
setDrawerVisible ( true );
applyCurrentContextToState ();
updateConfig ();
2026-07-10 10:54:34 +08:00
checkStatus (). then ( function () {
if ( ! piLabState . active ) return ;
if ( piLabState . status === STATE_IDLE || piLabState . status === STATE_ABORTED || piLabState . status === STATE_ERROR || ! piLabState . sessionId ) {
return startRuntime ();
}
}). catch ( function () {});
2026-07-04 21:47:42 +08:00
var input = piLabPanelEl && piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-input]' );
if ( input ) setTimeout ( function () { input . focus (); }, 60 );
}
function hidePiLab () {
piLabState . active = false ;
if ( piLabDrawerEl ) setDrawerVisible ( false );
updateLauncherState ();
}
function updateLauncherState () {
if ( ! piLabLauncherEl ) return ;
piLabLauncherEl . setAttribute ( 'data-state' , piLabState . status );
piLabLauncherEl . hidden = false ;
}
function installLauncher () {
if ( piLabLauncherEl ) return ;
piLabLauncherEl = el ( 'button' , {
type : 'button' ,
className : 'wolai-page-ai-pi-lab-launcher' ,
'data-page-ai-pi-lab-launcher' : 'true' ,
title : 'Pi Lab' ,
'aria-label' : '打开 Pi Lab' ,
}, 'π' );
piLabLauncherEl . addEventListener ( 'click' , showPiLab );
document . body . appendChild ( piLabLauncherEl );
}
function wireEvents () {
if ( ! piLabPanelEl || piLabPanelEl . getAttribute ( 'data-wired' ) === 'true' ) return ;
piLabPanelEl . setAttribute ( 'data-wired' , 'true' );
var input = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-input]' );
var sendBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-btn-send]' );
var abortBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-btn-abort]' );
var clearBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-btn-clear]' );
var closeBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-close]' );
var minimizeBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-minimize]' );
var providerSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-provider]' );
var modelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id]' );
var bottomModelSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-id-bottom]' );
2026-07-10 10:54:34 +08:00
var thinkingSelect = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-thinking]' );
2026-07-04 21:47:42 +08:00
var customInput = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-model-custom]' );
var settingsBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-open-settings]' );
var historyBtn = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-history]' );
2026-07-10 10:54:34 +08:00
var newBtns = piLabPanelEl . querySelectorAll ( '[data-page-ai-pi-lab-new]' );
2026-07-04 21:47:42 +08:00
if ( sendBtn ) sendBtn . addEventListener ( 'click' , sendCurrentInput );
if ( abortBtn ) abortBtn . addEventListener ( 'click' , abortPrompt );
if ( clearBtn ) clearBtn . addEventListener ( 'click' , function () {
2026-07-10 10:54:34 +08:00
resetPiRunViewModel ();
2026-07-04 21:47:42 +08:00
piLabState . receipts = [];
updateMessages ();
updateReceiptDisplay ();
2026-07-10 10:54:34 +08:00
updateQueueDisplay ();
2026-07-04 21:47:42 +08:00
});
if ( closeBtn ) closeBtn . addEventListener ( 'click' , hidePiLab );
if ( minimizeBtn ) {
minimizeBtn . addEventListener ( 'click' , function () {
var drawer = ensureDrawer ();
var minimized = drawer . getAttribute ( 'data-minimized' ) === 'true' ;
drawer . setAttribute ( 'data-minimized' , minimized ? 'false' : 'true' );
});
}
if ( input ) {
input . addEventListener ( 'input' , updateButtons );
input . addEventListener ( 'keydown' , function ( e ) {
if ( e . isComposing || e . key === 'Process' ) return ;
if ( e . key === 'Enter' && ! e . shiftKey ) {
e . preventDefault ();
2026-07-10 10:54:34 +08:00
sendCurrentInput ( e . altKey ? { streamingBehavior : 'followUp' } : undefined );
2026-07-04 21:47:42 +08:00
}
if ( e . key === 'Escape' && piLabState . status === STATE_STREAMING ) {
e . preventDefault ();
abortPrompt ();
}
});
}
2026-07-10 10:54:34 +08:00
[ providerSelect , modelSelect , bottomModelSelect , thinkingSelect ]. forEach ( function ( node ) {
2026-07-04 21:47:42 +08:00
if ( node ) node . addEventListener ( 'change' , applyModelControls );
});
if ( customInput ) customInput . addEventListener ( 'input' , applyModelControls );
2026-07-10 10:54:34 +08:00
if ( settingsBtn ) {
settingsBtn . addEventListener ( 'click' , function () {
window . location . assign ( '/user/ai#ai-admin-access' );
2026-07-04 21:47:42 +08:00
});
}
2026-07-10 10:54:34 +08:00
if ( historyBtn ) {
historyBtn . addEventListener ( 'click' , function () {
var layer = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-history-layer]' );
if ( ! layer ) return ;
setHistoryPanelVisible ( layer . hidden );
});
}
2026-07-04 21:47:42 +08:00
Array . prototype . slice . call ( newBtns || []). forEach ( function ( btn ) {
2026-07-10 10:54:34 +08:00
btn . addEventListener ( 'click' , startNewConversation );
2026-07-04 21:47:42 +08:00
});
2026-07-10 10:54:34 +08:00
piLabPanelEl . addEventListener ( 'click' , function ( event ) {
var historyNew = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-new-small]' ) : null ;
if ( historyNew ) {
startNewConversation ();
return ;
}
var actionMenuToggle = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-action-menu-toggle]' ) : null ;
if ( actionMenuToggle ) {
setActionMenuOpen ( ! piLabState . actionMenuOpen );
return ;
}
var menuAction = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-menu-action]' ) : null ;
if ( menuAction && ! menuAction . disabled ) {
handleActionMenuAction ( menuAction . getAttribute ( 'data-page-ai-pi-lab-menu-action' ));
return ;
}
var permissionBtn = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-permission]' ) : null ;
if ( permissionBtn ) {
closeActionMenu ();
setPermissionMenuOpen ( ! piLabState . permissionMenuOpen );
return ;
}
var permissionMode = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-permission-mode]' ) : null ;
if ( permissionMode ) {
selectPermissionMode ( permissionMode . getAttribute ( 'data-page-ai-pi-lab-permission-mode' ));
return ;
}
var closeHistory = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-close], [data-page-ai-pi-lab-history-scrim]' ) : null ;
if ( closeHistory ) {
setHistoryPanelVisible ( false );
return ;
}
var refreshHistory = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-refresh]' ) : null ;
if ( refreshHistory ) {
loadHistory (). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史刷新失败' );
showPiToast ( '历史刷新失败' , 'error' );
});
return ;
}
var clearHistory = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-clear]' ) : null ;
if ( clearHistory && ! clearHistory . disabled ) {
clearHistorySessions (). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史清空失败' );
showPiToast ( '历史清空失败' , 'error' );
});
return ;
}
var forkCurrent = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-fork-current]' ) : null ;
if ( forkCurrent ) {
forkHistorySession ( piLabState . viewingHistorySessionId || piLabState . sessionId ). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史会话 fork 失败' );
showPiToast ( 'Fork failed' , 'error' );
});
return ;
}
var closeArtifacts = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-close-artifacts]' ) : null ;
if ( closeArtifacts ) {
var closeBody = piLabPanelEl . querySelector ( '[data-page-ai-pi-lab-body]' );
if ( closeBody ) closeBody . setAttribute ( 'data-rail-open' , 'false' );
return ;
}
var copyMarkdown = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-copy-markdown]' ) : null ;
if ( copyMarkdown ) {
copyTextToClipboard ( copyMarkdown . getAttribute ( 'data-page-ai-pi-lab-copy-markdown' ) || '' ). then ( function () {
showPiToast ( 'Markdown copied' , 'success' );
});
return ;
}
var openSessionJsonl = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-open-session-jsonl]' ) : null ;
if ( openSessionJsonl ) {
openCurrentSessionJsonl ();
return ;
}
var openArtifact = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-artifact-open]' ) : null ;
if ( openArtifact ) {
window . dispatchEvent ( new CustomEvent ( 'mnote:open-reference' , {
detail : {
source : 'page_ai_pi_lab_artifact' ,
title : openArtifact . getAttribute ( 'data-page-ai-pi-lab-artifact-title' ) || openArtifact . textContent || '' ,
url : openArtifact . getAttribute ( 'data-page-ai-pi-lab-artifact-open' ) || openArtifact . getAttribute ( 'href' ) || '' ,
},
}));
}
var copyArtifactMarkdown = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-artifact-copy-markdown]' ) : null ;
if ( copyArtifactMarkdown ) {
copyTextToClipboard ( copyArtifactMarkdown . getAttribute ( 'data-page-ai-pi-lab-artifact-copy-markdown' ) || '' ). then ( function () {
showPiToast ( 'Artifact markdown copied' , 'success' );
});
return ;
}
var copyArtifact = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-artifact-copy]' ) : null ;
if ( copyArtifact ) {
var raw = copyArtifact . getAttribute ( 'data-page-ai-pi-lab-artifact-copy' ) || '' ;
copyTextToClipboard ( raw ). then ( function () { showPiToast ( 'Artifact raw copied' , 'success' ); });
return ;
}
var deleteTarget = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-delete]' ) : null ;
if ( deleteTarget ) {
deleteHistorySession ( deleteTarget . getAttribute ( 'data-page-ai-pi-lab-history-delete' )). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史会话删除失败' );
showPiToast ( 'Delete failed' , 'error' );
});
return ;
}
var exportTarget = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-export]' ) : null ;
if ( exportTarget ) {
exportHistorySession ( exportTarget . getAttribute ( 'data-page-ai-pi-lab-history-export' )). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史会话导出失败' );
showPiToast ( 'Export failed' , 'error' );
});
return ;
}
var renameTarget = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-rename]' ) : null ;
if ( renameTarget ) {
renameHistorySession ( renameTarget . getAttribute ( 'data-page-ai-pi-lab-history-rename' )). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史会话重命名失败' );
showPiToast ( 'Rename failed' , 'error' );
});
return ;
}
var forkTarget = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-fork]' ) : null ;
if ( forkTarget ) {
forkHistorySession ( forkTarget . getAttribute ( 'data-page-ai-pi-lab-history-fork' )). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史会话 fork 失败' );
showPiToast ( 'Fork failed' , 'error' );
});
return ;
}
2026-07-04 21:47:42 +08:00
var historyTarget = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-history-session]' ) : null ;
if ( historyTarget ) {
openHistorySession ( historyTarget . getAttribute ( 'data-page-ai-pi-lab-history-session' )). catch ( function ( error ) {
updateDiagnostics ( error . message || '历史会话加载失败' );
});
return ;
}
var target = event . target && event . target . closest ? event . target . closest ( '[data-page-ai-pi-lab-example]' ) : null ;
if ( ! target ) return ;
setComposerText ( target . getAttribute ( 'data-page-ai-pi-lab-example' ) || target . textContent || '' );
});
Array . prototype . slice . call ( piLabPanelEl . querySelectorAll ( '[data-page-ai-pi-lab-quick]' )). forEach ( function ( btn ) {
btn . addEventListener ( 'click' , function () {
handleQuickAction ( btn . getAttribute ( 'data-page-ai-pi-lab-quick' ));
});
});
}
function setupPostMessageListener () {
window . addEventListener ( 'message' , function ( event ) {
if ( ! event . data || event . data . source !== 'mnote-sidebar' ) return ;
if ( event . data . type === 'mnote:pi-lab-status' ) checkStatus ();
if ( event . data . type === 'mnote:pi-lab-show' ) showPiLab ();
if ( event . data . type === 'mnote:pi-lab-hide' ) hidePiLab ();
});
}
function setupSelectionListener () {
document . addEventListener ( 'selectionchange' , function () {
if ( ! piLabState . active ) return ;
refreshSelectionSummary ();
updateContextStrip ();
}, true );
window . addEventListener ( 'mnote:leptos-tiptap-spike:selection' , function () {
if ( ! piLabState . active ) return ;
refreshSelectionSummary ();
updateContextStrip ();
}, true );
}
function createSidebarPageAiPiLabRuntime ( context ) {
if ( piLabInstalled ) return piLabState ;
piLabInstalled = true ;
injectStyles ();
piLabState . standalone = !! ( context && context . standalone );
ensureDrawer ();
if ( piLabState . standalone ) {
setDrawerVisible ( true );
piLabState . active = true ;
} else {
installLauncher ();
}
setupPostMessageListener ();
setupSelectionListener ();
updateConfig ();
updateReceiptDisplay ();
checkStatus ();
return piLabState ;
}
if ( typeof window !== 'undefined' ) {
window . createSidebarPageAiPiLabRuntime = createSidebarPageAiPiLabRuntime ;
2026-07-10 10:54:34 +08:00
if ( window . __MNOTE_PI_LAB_TEST__ === true ) window . __mnotePiLabReducerTest = {
createPiRunViewModel : createPiRunViewModel ,
reducePiRunViewModel : reducePiRunViewModel ,
rebuildPiRunToolIndex : rebuildPiRunToolIndex ,
};
if ( window . __MNOTE_PI_LAB_TEST__ === true ) window . __mnotePiLabTest = {
emitRpcEvent : handlePiRpcEvent ,
emitToolCall : handleToolCallEventPayload ,
showToast : showPiToast ,
closeDialog : closePiUiDialog ,
};
2026-07-04 21:47:42 +08:00
}
})();