Files
mnote/rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js
T

4800 lines
254 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// == Pi Lab Page AI Runtime ==
// MNote-native adapter for Pi-first Page AI Lab.
// 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.
//
// 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/gpt-5.4-mini (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',
UI_RESPONSE: '/api/page-ai/pi/ui-response',
EVENTS: '/api/page-ai/pi/events',
BOOTSTRAP: '/api/page-ai/pi/bootstrap',
SESSIONS: '/api/page-ai/pi/sessions',
DIFF: '/api/page-ai/pi/artifacts',
FORK: '/api/page-ai/pi/fork',
STATE: '/api/page-ai/pi/state',
COMPACT: '/api/page-ai/pi/compact',
CONFIGURE: '/api/page-ai/pi/configure',
QUEUE_CONFIG: '/api/page-ai/pi/queue-config',
EFFECTIVE: '/api/ai-settings/effective',
};
var DEFAULT_MODEL_PROVIDER = 'omniroute';
var DEFAULT_MODEL_ID = 'gpt-5.4-mini';
var DEFAULT_THINKING_LEVEL = 'medium';
var DEFAULT_PERMISSION_MODE = 'confirm';
var piLabInstalled = false;
var piLabPanelEl = null;
var piLabDrawerEl = null;
var piLabLauncherEl = null;
var piLabEventSource = null;
var piLabStartPromise = null;
var piLabState = {
status: STATE_IDLE,
enabled: false,
active: false,
standalone: false,
sessionId: null,
providerSessionId: null,
defaultModelProvider: DEFAULT_MODEL_PROVIDER,
defaultModelId: DEFAULT_MODEL_ID,
defaultThinkingLevel: DEFAULT_THINKING_LEVEL,
modelProvider: null,
modelId: null,
thinkingLevel: DEFAULT_THINKING_LEVEL,
modelMenuOpen: false,
thinkingMenuOpen: false,
permissionMode: DEFAULT_PERMISSION_MODE,
permissionMenuOpen: false,
pendingPermissionModeApply: false,
pendingPermissionMode: null,
applyingPermissionMode: false,
actionMenuOpen: false,
runtimeMode: null,
runtimeImplementation: null,
runtimeBinary: null,
runtimeAvailable: true,
runtimeInstallHint: '',
runtimeError: '',
runtimePid: null,
warmupRunning: false,
warmupSessionId: null,
managedBuiltinTools: [],
piExtensionSources: [],
piExtensionToolNames: [],
session: null,
currentToolEventId: null,
allowedRootsSummary: '',
selectionSummary: '',
selectionText: '',
changedFiles: [],
messages: [],
diagnostics: '',
receipts: [],
modelOptions: [],
history: [],
pendingQueue: { steering: [], followUp: [] },
viewingHistorySessionId: null,
contextSelection: {
currentPage: false,
currentFolder: false,
selection: false,
lightrag: false,
},
queuedMessages: [],
pendingMessageCount: 0,
isCompacting: false,
contextUsage: {},
};
var streamingAssistantMsg = null;
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_ERROR;
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: [], toolEventId: '' };
diffMsg.diffSummary.files = payload.files || [];
if (payload.toolEventId) diffMsg.diffSummary.toolEventId = payload.toolEventId;
return vm;
}
return vm;
}
function applyPiRunEvent(event) {
syncPiRunViewModelFromState();
reducePiRunViewModel(piRunViewModel, event);
syncPiRunViewModelToState();
return piRunViewModel;
}
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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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)}' +
'.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}' +
'.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)}' +
'.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}' +
'.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}' +
'@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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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-control-wrap{position:relative;display:inline-flex;min-width:0;flex:0 0 auto}' +
'.wolai-page-ai-pi-lab-model-control-wrap{flex:1 1 160px;max-width:210px}' +
'.wolai-page-ai-pi-lab-control-button{height:34px;min-width:0;width:100%;border:1px solid #e1e4eb;border-radius:10px;background:#fff;color:#4a4f58;padding:0 9px;display:inline-flex;align-items:center;justify-content:space-between;gap:7px;font:12px/1 inherit;white-space:nowrap;cursor:pointer}' +
'.wolai-page-ai-pi-lab-model-control{font-size:13px}' +
'.wolai-page-ai-pi-lab-thinking-control{max-width:118px}' +
'.wolai-page-ai-pi-lab-control-button:hover{background:#f8fafc}.wolai-page-ai-pi-lab-control-button[data-active="true"]{border-color:#9ec5fe;background:#eef6ff;color:#1558a6}.wolai-page-ai-pi-lab-control-button:disabled{opacity:.45;cursor:default}' +
'.wolai-page-ai-pi-lab-control-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.wolai-page-ai-pi-lab-control-caret{flex:0 0 auto;color:#8a9099;font-size:11px;line-height:1;transform:translateY(-1px)}' +
'.wolai-page-ai-pi-lab-control-menu{position:absolute;left:0;bottom:40px;z-index:20;width:min(280px,calc(100vw - 28px));max-height:min(280px,calc(100vh - 160px));overflow:auto;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-thinking-control-wrap .wolai-page-ai-pi-lab-control-menu{width:180px}' +
'.wolai-page-ai-pi-lab-control-wrap[data-open="true"] .wolai-page-ai-pi-lab-control-menu{display:block}' +
'.wolai-page-ai-pi-lab-control-option{width:100%;min-height:34px;border:0;border-radius:8px;background:#fff;color:#242424;display:grid;grid-template-columns:minmax(0,1fr) 18px;gap:8px;align-items:center;text-align:left;padding:7px 9px;font:13px/1.25 inherit;cursor:pointer}' +
'.wolai-page-ai-pi-lab-control-option:hover{background:#f7f8fb}.wolai-page-ai-pi-lab-control-option[data-active="true"]{background:#f3f6fb;color:#1558a6}' +
'.wolai-page-ai-pi-lab-control-option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wolai-page-ai-pi-lab-control-option-check{text-align:right;color:#6b7280}' +
'.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}' +
'.wolai-page-ai-pi-lab-segment{display:inline-flex;border:1px solid #e1e4eb;border-radius:10px;overflow:hidden;background:#fff}' +
'.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}' +
'.wolai-page-ai-pi-lab-segment button[data-active=\"true\"]{background:#111827;color:#fff}' +
'.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-spacer{flex:1}' +
'.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}' +
'.wolai-page-ai-pi-lab-send:disabled{opacity:.45;cursor:default}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'.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}' +
'@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-wrap{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%}}';
/* Split diff overlay styles */
var diffStyles = '.wolai-page-ai-pi-lab-diff-overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;background:rgba(0,0,0,0.4);display:flex;flex-direction:column;padding:40px 60px}' +
'.wolai-page-ai-pi-lab-diff-header{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;background:#f0f0ed;border-radius:8px 8px 0 0;font:600 14px/1.4 system-ui}' +
'.wolai-page-ai-pi-lab-diff-close{background:none;border:none;font:18px/1 sans-serif;cursor:pointer;padding:4px 8px;border-radius:4px}' +
'.wolai-page-ai-pi-lab-diff-close:hover{background:rgba(0,0,0,0.08)}' +
'.wolai-page-ai-pi-lab-diff-body{flex:1;overflow:auto;background:#fff;border-radius:0 0 8px 8px;padding:0;font:12px/1.5 Monaco,Consolas,monospace}' +
'.wolai-page-ai-pi-lab-diff-file-header{background:#f8f8f5;font-weight:600;padding:6px 12px;border-bottom:1px solid #e5e5e0}' +
'.wolai-page-ai-pi-lab-diff-hunk-header{background:#f0f0ed;color:#666;padding:4px 12px;border-bottom:1px solid #e5e5e0;font-size:11px}' +
'.wolai-page-ai-pi-lab-diff-line{padding:1px 12px;border-bottom:1px solid #f0f0ed;white-space:pre}' +
'.wolai-page-ai-pi-lab-diff-line-add{background:#e6ffec}' +
'.wolai-page-ai-pi-lab-diff-line-del{background:#ffebe9}' +
'.wolai-page-ai-pi-lab-diff-line-ctx{background:#f8f8f5;color:#666}' +
'.wolai-page-ai-pi-lab-diff-line-num{display:inline-block;width:40px;text-align:right;color:#999;padding-right:12px;user-select:none}' +
'.wolai-page-ai-pi-lab-diff-collapse{cursor:pointer;text-align:center;color:#888;background:#f5f5f2;padding:2px;font-size:11px;border-bottom:1px solid #e5e5e0}' +
'.wolai-page-ai-pi-lab-diff-collapse:hover{background:#eee}';
style.textContent += diffStyles;
document.head.appendChild(style);
}
function escapeHtml(str) {
if (str == null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function escapeAttr(str) {
return escapeHtml(str).replace(/`/g, '&#96;');
}
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('');
}
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;
}
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 citationFromMarkdown(markdown, fallbackTitle) {
var value = String(markdown || '').trim();
if (!value || value[0] !== '[' || value[value.length - 1] !== ')') return null;
var split = value.lastIndexOf('](');
if (split <= 0) return null;
var title = value.slice(1, split).replace(/\\([\[\]])/g, '$1').trim();
var url = value.slice(split + 2, -1).trim();
if (!url) return null;
return {
source: String(fallbackTitle || title || url),
title: title || String(fallbackTitle || '资料库引用'),
url: url,
};
}
function collectKnowledgeRagCitations(value) {
var citations = [];
var seen = {};
function add(markdown, fallbackTitle) {
var citation = citationFromMarkdown(markdown, fallbackTitle);
if (!citation || seen[citation.url] || citations.length >= 8) return;
seen[citation.url] = true;
citations.push(citation);
}
function visit(node) {
if (node == null || citations.length >= 8) return;
if (Array.isArray(node)) {
node.forEach(visit);
return;
}
if (typeof node !== 'object') return;
var title = String(node.sourceRootRelativePath || node.citationLabel || node.title || '').trim();
if (typeof node.citationMarkdown === 'string') add(node.citationMarkdown, title);
Object.keys(node).forEach(function (key) {
if (key !== 'citationMarkdown') visit(node[key]);
});
}
visit(value);
return citations;
}
function appendKnowledgeRagCitations(value) {
var citations = collectKnowledgeRagCitations(value);
if (!citations.length) return;
syncPiRunViewModelFromState();
var current = piRunViewModel.currentAssistant || findLastAssistant(piRunViewModel);
if (!current) current = ensurePiRunAssistant(piRunViewModel);
if (!Array.isArray(current.citations)) current.citations = [];
var existing = {};
current.citations.forEach(function (citation) {
existing[String(citation && citation.url || '')] = true;
});
citations.forEach(function (citation) {
if (existing[citation.url]) return;
existing[citation.url] = true;
current.citations.push(citation);
});
syncPiRunViewModelToState();
}
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;
}
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\/([^/]+)/);
if (match) return decodeURIComponent(match[1]);
var missingPage = String(currentUrl().searchParams.get('missingPage') || currentUrl().searchParams.get('pageId') || '').trim();
return missingPage ? missingPage : '';
}
function currentWorkspaceId() {
return currentUrl().searchParams.get('workspaceId') || (document.body && document.body.dataset ? String(document.body.dataset.workspaceId || '').trim() : '');
}
function currentRootUri() {
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();
}
function localMarkdownRelativePathFromDocumentId(documentId) {
var value = String(documentId || '').trim();
if (!value.startsWith('local-md:')) return '';
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, '\\$&');
}
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;
}
}
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;
}
function currentPageContext() {
var active = activeEditorSnapshot() || {};
var activeWorkspacePath = active.workspacePath && typeof active.workspacePath === 'object' ? active.workspacePath : {};
var urlDocumentId = currentDocumentId();
var urlPagePath = localMarkdownRelativePathFromDocumentId(urlDocumentId);
var documentId = String(urlDocumentId || activeWorkspacePath.documentId || active.documentId || '').trim();
var fileTreeWorkspacePath = activeFileTreeWorkspacePath(documentId) || {};
var workspacePath = Object.assign({}, activeWorkspacePath, fileTreeWorkspacePath);
var session = piLabState.session || {};
var pagePath = normalizeSlashes(urlPagePath || workspacePath.relativePath || workspacePath.path || active.relativePath || active.path || localMarkdownRelativePathFromDocumentId(documentId) || session.pagePath || '');
var resourceKind = String(workspacePath.resourceKind || workspacePath.kind || active.resourceKind || active.kind || '').trim().toLowerCase();
var isDirectory = resourceKind === 'directory' || resourceKind === 'folder' || resourceKind === 'workspace';
var isStandalonePiPage = currentUrl().pathname === '/page-ai/pi';
var isLocalMarkdownPage = documentId.indexOf('local-md:') === 0 || /\.md$/i.test(pagePath);
if (isDirectory || (isStandalonePiPage && pagePath && !isLocalMarkdownPage)) pagePath = '';
var rootUri = String(currentRootUri() || workspacePath.rootUri || active.rootUri || session.rootUri || '').trim();
var workspaceId = String(currentWorkspaceId() || workspacePath.workspaceId || active.workspaceId || session.workspaceId || '').trim();
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,
};
}
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,
};
}
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 = {};
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;
updateContextStrip();
return context;
}
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);
}
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('/');
}
function modelControlOptions() {
var source = piLabState.modelOptions.length ? piLabState.modelOptions : [{
provider: piLabState.defaultModelProvider || DEFAULT_MODEL_PROVIDER,
id: piLabState.defaultModelId || DEFAULT_MODEL_ID,
name: piLabState.defaultModelId || DEFAULT_MODEL_ID,
}];
source = source.concat([{
provider: DEFAULT_MODEL_PROVIDER,
id: 'freefirst',
name: DEFAULT_MODEL_PROVIDER + '/freefirst',
}, {
provider: DEFAULT_MODEL_PROVIDER,
id: DEFAULT_MODEL_ID,
name: DEFAULT_MODEL_ID,
}]);
var seen = {};
return source.map(function (item) {
var provider = String(item.provider || piLabState.defaultModelProvider || DEFAULT_MODEL_PROVIDER);
var modelId = String(item.id || item.modelId || item.name || DEFAULT_MODEL_ID);
if (modelId.indexOf(provider + '/') === 0) modelId = modelId.slice(provider.length + 1);
var key = provider + '/' + modelId;
if (seen[key]) return null;
seen[key] = true;
return {
provider: provider,
modelId: modelId,
label: String(item.name || key),
key: key,
};
}).filter(Boolean);
}
function normalizeThinkingLevel(value) {
var level = String(value || '').trim().toLowerCase();
return ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'].indexOf(level) >= 0 ? level : DEFAULT_THINKING_LEVEL;
}
function thinkingLevelOptions() {
return [
{ level: 'off', label: '思考 关' },
{ level: 'minimal', label: '思考 极低' },
{ level: 'low', label: '思考 低' },
{ level: 'medium', label: '思考 中' },
{ level: 'high', label: '思考 高' },
{ level: 'xhigh', label: '思考 最高' },
];
}
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 wrap = piLabPanelEl.querySelector('[data-page-ai-pi-lab-thinking-menu-wrap]');
var control = piLabPanelEl.querySelector('[data-page-ai-pi-lab-thinking-menu-toggle]');
var label = piLabPanelEl.querySelector('[data-page-ai-pi-lab-thinking-label]');
var menu = piLabPanelEl.querySelector('[data-page-ai-pi-lab-thinking-menu]');
var level = normalizeThinkingLevel(piLabState.thinkingLevel || piLabState.defaultThinkingLevel);
piLabState.thinkingLevel = level;
if (wrap) wrap.setAttribute('data-open', piLabState.thinkingMenuOpen ? 'true' : 'false');
if (control) {
control.setAttribute('data-active', piLabState.thinkingMenuOpen ? 'true' : 'false');
control.setAttribute('aria-expanded', piLabState.thinkingMenuOpen ? 'true' : 'false');
control.title = '思考深度:' + currentThinkingLabel() + ';启动本次 Pi 会话时应用';
}
if (label) label.textContent = '思考 ' + currentThinkingLabel();
if (menu) menu.innerHTML = thinkingLevelOptions().map(function (item) {
var active = item.level === level;
return '<button type="button" class="wolai-page-ai-pi-lab-control-option" role="menuitemradio" aria-checked="' + String(active) + '" data-page-ai-pi-lab-thinking-option="' + escapeHtml(item.level) + '" data-active="' + String(active) + '">' +
'<span class="wolai-page-ai-pi-lab-control-option-label">' + escapeHtml(item.label) + '</span>' +
'<span class="wolai-page-ai-pi-lab-control-option-check">' + (active ? '✓' : '') + '</span>' +
'</button>';
}).join('');
}
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) {
if (open) {
setActionMenuOpen(false);
setModelMenuOpen(false);
setThinkingMenuOpen(false);
}
piLabState.permissionMenuOpen = !!open;
updatePermissionControl();
}
function setModelMenuOpen(open) {
if (open) {
setActionMenuOpen(false);
setPermissionMenuOpen(false);
setThinkingMenuOpen(false);
}
piLabState.modelMenuOpen = !!open;
updateModelLabel();
}
function setThinkingMenuOpen(open) {
if (open) {
setActionMenuOpen(false);
setPermissionMenuOpen(false);
setModelMenuOpen(false);
}
piLabState.thinkingMenuOpen = !!open;
updateThinkingControl();
}
function closeComposerControlMenus() {
if (!piLabState.actionMenuOpen && !piLabState.permissionMenuOpen && !piLabState.modelMenuOpen && !piLabState.thinkingMenuOpen) return;
setActionMenuOpen(false);
setPermissionMenuOpen(false);
setModelMenuOpen(false);
setThinkingMenuOpen(false);
}
function selectModelControl(provider, modelId) {
piLabState.modelProvider = String(provider || piLabState.defaultModelProvider || DEFAULT_MODEL_PROVIDER);
piLabState.modelId = String(modelId || piLabState.defaultModelId || DEFAULT_MODEL_ID);
setModelMenuOpen(false);
updateModelLabel();
applyModelConfigToRuntime();
}
function selectThinkingControl(level) {
piLabState.thinkingLevel = normalizeThinkingLevel(level);
setThinkingMenuOpen(false);
updateThinkingControl();
applyModelConfigToRuntime();
}
function selectPermissionMode(mode) {
piLabState.permissionMode = normalizePermissionMode(mode);
setPermissionMenuOpen(false);
updatePermissionControl();
if (piLabState.sessionId
&& piLabState.status !== STATE_STREAMING
&& piLabState.status !== STATE_STARTING
&& !piLabState.applyingPermissionMode) {
return applyPermissionModeToRuntime();
}
if (piLabState.sessionId
&& (piLabState.status === STATE_STREAMING
|| piLabState.status === STATE_STARTING
|| piLabState.applyingPermissionMode)) {
piLabState.pendingPermissionModeApply = true;
piLabState.pendingPermissionMode = piLabState.permissionMode;
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;
if (piLabState.pendingPermissionMode) {
piLabState.permissionMode = normalizePermissionMode(piLabState.pendingPermissionMode);
piLabState.pendingPermissionMode = null;
updatePermissionControl();
}
applyPermissionModeToRuntime().catch(function () {});
}, 0);
}
function applyPermissionModeToRuntime() {
if (!piLabState.sessionId || !piLabState.enabled) return Promise.resolve({ skipped: true });
if (piLabState.applyingPermissionMode) {
piLabState.pendingPermissionModeApply = true;
piLabState.pendingPermissionMode = normalizePermissionMode(piLabState.permissionMode);
return Promise.resolve({ queued: true });
}
var context = applyCurrentContextToState();
var nextMode = normalizePermissionMode(piLabState.permissionMode);
ensurePiToolCapableModel();
piLabState.pendingPermissionModeApply = false;
piLabState.pendingPermissionMode = null;
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) {
if (open) {
setPermissionMenuOpen(false);
setModelMenuOpen(false);
setThinkingMenuOpen(false);
}
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');
}
}
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 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;
ensurePiToolCapableModel();
piLabState.permissionMode = normalizePermissionMode(piLabState.permissionMode);
updateModelLabel();
updateThinkingControl();
updatePermissionControl();
}
function applyModelConfigToRuntime() {
if (!piLabState.sessionId) return Promise.resolve();
if (piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTING) {
piLabState.pendingModelConfigApply = true;
return Promise.resolve();
}
piLabState.pendingModelConfigApply = false;
return fetch(API.CONFIGURE, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: piLabState.sessionId,
modelProvider: piLabState.modelProvider,
modelId: piLabState.modelId,
thinkingLevel: piLabState.thinkingLevel,
}),
}).then(function (r) { return r.json(); }).then(function (data) {
if (data && (data.message || data.error)) {
showPiToast(data.message || data.error, 'warning');
}
return data;
}).catch(function (err) {
showPiToast('配置应用失败:' + (err.message || ''), 'error');
});
}
function maybeApplyPendingModelConfig() {
if (!piLabState.pendingModelConfigApply || !piLabState.sessionId) return;
if (piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTING) return;
piLabState.pendingModelConfigApply = false;
setTimeout(function () {
applyModelConfigToRuntime().catch(function () {});
}, 0);
}
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 stripModelProviderPrefix(provider, modelId) {
var normalizedProvider = String(provider || DEFAULT_MODEL_PROVIDER).trim();
var normalizedModelId = String(modelId || '').trim();
if (normalizedProvider && normalizedModelId.indexOf(normalizedProvider + '/') === 0) {
return normalizedModelId.slice(normalizedProvider.length + 1);
}
return normalizedModelId;
}
function ensurePiToolCapableModel() {
var provider = piLabState.modelProvider || piLabState.defaultModelProvider || DEFAULT_MODEL_PROVIDER;
var modelId = piLabState.modelId || piLabState.defaultModelId || DEFAULT_MODEL_ID;
modelId = stripModelProviderPrefix(provider, modelId);
piLabState.modelProvider = provider || DEFAULT_MODEL_PROVIDER;
piLabState.modelId = modelId || DEFAULT_MODEL_ID;
return true;
}
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;
if (!piLabState.modelProvider && !piLabState.modelId) {
piLabState.modelProvider = piLabState.defaultModelProvider;
piLabState.modelId = piLabState.defaultModelId;
}
ensurePiToolCapableModel();
piLabState.defaultThinkingLevel = normalizeThinkingLevel(data.defaultThinkingLevel || data.default_thinking_level || piLabState.defaultThinkingLevel);
if (!piLabState.thinkingLevel) piLabState.thinkingLevel = piLabState.defaultThinkingLevel;
piLabState.modelOptions = Array.isArray(data.models) ? data.models : [];
piLabState.modelOptions = piLabState.modelOptions.concat([{
provider: DEFAULT_MODEL_PROVIDER,
id: 'freefirst',
name: DEFAULT_MODEL_PROVIDER + '/freefirst',
}, {
provider: DEFAULT_MODEL_PROVIDER,
id: DEFAULT_MODEL_ID,
name: DEFAULT_MODEL_ID,
}]);
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 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;
updateModelLabel();
updateThinkingControl();
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';
}
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"/>',
compact: '<path d="M4 8h16M4 16h16"/><path d="M8 4 12 8 16 4M8 20l4-4 4 4"/>',
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>';
}
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/gpt-5.4-mini</span></div>' +
'</div>' +
'<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>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-commandbar" data-page-ai-pi-lab-commandbar>' +
'<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-btn-compact title="压缩上下文" aria-label="压缩上下文" disabled>' + piIcon('compact') + '</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>' +
'</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/gpt-5.4-mini</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="gpt-5.4-mini">gpt-5.4-mini</option></select></label>' +
'<label class="wolai-page-ai-pi-lab-field">自定义模型<input data-page-ai-pi-lab-model-custom placeholder="可选,启动时应用"></label>' +
'<span class="wolai-page-ai-pi-lab-chip"><span>runtime</span><strong data-page-ai-pi-lab-runtime-chip>runtime pending</strong></span>' +
'<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>' +
'</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>' +
'<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>' +
'</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>' +
'<div class="wolai-page-ai-pi-lab-body" data-page-ai-pi-lab-body data-rail-open="false">' +
'<div class="wolai-page-ai-pi-lab-main">' +
'<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>' +
'<div class="wolai-page-ai-pi-lab-messages" data-page-ai-pi-lab-messages></div>' +
'<div class="wolai-page-ai-pi-lab-composer">' +
'<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>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-modebar" data-page-ai-pi-lab-modebar>' +
'<button type="button" class="wolai-page-ai-pi-lab-square" data-page-ai-pi-lab-action-menu-toggle title="添加上下文和操作" aria-label="添加上下文和操作">' + piIcon('plus') + '</button>' +
'<div class="wolai-page-ai-pi-lab-control-wrap wolai-page-ai-pi-lab-model-control-wrap" data-page-ai-pi-lab-model-menu-wrap data-open="false">' +
'<button type="button" class="wolai-page-ai-pi-lab-control-button wolai-page-ai-pi-lab-model-control" data-page-ai-pi-lab-model-menu-toggle aria-haspopup="menu" aria-expanded="false" title="模型">' +
'<span class="wolai-page-ai-pi-lab-control-label" data-page-ai-pi-lab-model-label>omniroute/gpt-5.4-mini</span>' +
'<span class="wolai-page-ai-pi-lab-control-caret" aria-hidden="true">⌄</span>' +
'</button>' +
'<div class="wolai-page-ai-pi-lab-control-menu" data-page-ai-pi-lab-model-menu role="menu"></div>' +
'</div>' +
'<div class="wolai-page-ai-pi-lab-control-wrap wolai-page-ai-pi-lab-thinking-control-wrap" data-page-ai-pi-lab-thinking-menu-wrap data-open="false">' +
'<button type="button" class="wolai-page-ai-pi-lab-control-button wolai-page-ai-pi-lab-thinking-control" data-page-ai-pi-lab-thinking-menu-toggle aria-haspopup="menu" aria-expanded="false" title="思考深度" aria-label="思考深度">' +
'<span class="wolai-page-ai-pi-lab-control-label" data-page-ai-pi-lab-thinking-label>思考 中</span>' +
'<span class="wolai-page-ai-pi-lab-control-caret" aria-hidden="true">⌄</span>' +
'</button>' +
'<div class="wolai-page-ai-pi-lab-control-menu" data-page-ai-pi-lab-thinking-menu role="menu"></div>' +
'</div>' +
'<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>' +
'</div>' +
'</div>' +
'</div>' +
'<aside class="wolai-page-ai-pi-lab-rail" data-page-ai-pi-lab-rail>' +
'<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>' +
'<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>' +
'<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>' +
'</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>';
}
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>';
}
function renderHistory() {
if (!piLabPanelEl) return;
var panel = piLabPanelEl.querySelector('[data-page-ai-pi-lab-history-panel]');
if (!panel) return;
if (!piLabState.history.length) {
panel.innerHTML = historyShell('<div class="wolai-page-ai-pi-lab-empty">暂无历史对话</div>');
return;
}
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();
}
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 loadSessionTree(sessionId) {
if (!sessionId) return Promise.resolve(null);
return fetch(API.SESSIONS + '/' + encodeURIComponent(sessionId) + '/tree', {
credentials: 'same-origin',
cache: 'no-store'
}).then(function (r) {
if (!r.ok) return null;
return r.json();
}).then(function (data) {
if (data && data.schema === 'mnote.page_ai_pi.session_tree.v1' && Array.isArray(data.entries)) {
return data;
}
if (data && Array.isArray(data.entries)) return data;
return null;
}).catch(function () { return null; });
}
function treeEntriesToMessages(entries) {
if (!Array.isArray(entries)) return [];
return entries.map(function (entry) {
var msg = {
entryId: entry.id || '',
role: entry.role === 'user' ? 'user' : 'assistant',
text: entry.text || entry.content || entry.preview || '',
meta: entry.meta || '',
toolCalls: Array.isArray(entry.toolCalls) ? entry.toolCalls : [],
citations: Array.isArray(entry.citations) ? entry.citations : [],
diffSummary: entry.diffSummary || null,
};
return msg;
}).filter(function (m) { return m.text || m.toolCalls.length || m.citations.length || m.diffSummary; });
}
function applyReplayMessages(messages) {
(Array.isArray(messages) ? messages : []).forEach(function (msg) {
if (msg.role === 'user') {
applyPiRunEvent({ type: 'user_prompt', text: msg.text, meta: msg.meta });
} else if (msg.abortReason) {
applyPiRunEvent({ type: 'assistant_abort', payload: { reason: msg.abortReason } });
} else {
applyPiRunEvent({ type: 'assistant_done', text: msg.text, meta: msg.meta, toolCalls: msg.toolCalls, citations: msg.citations, diffSummary: msg.diffSummary });
}
});
}
function openHistorySession(sessionId) {
if (!sessionId) return Promise.resolve();
if (piLabEventSource) {
piLabEventSource.close();
piLabEventSource = null;
}
return fetch(API.SESSIONS + '/' + encodeURIComponent(sessionId), { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (detail) {
var detailSession = detail.session || detail;
var runtime = detailSession.runtime || {};
var runtimePolicy = runtime.runtimePolicy || runtime.runtime_policy || {};
piLabState.sessionId = detailSession.sessionId || sessionId;
piLabState.providerSessionId = runtime.providerSessionId || detailSession.providerSessionId || null;
piLabState.session = Object.assign({}, runtime, detailSession, {
sessionId: piLabState.sessionId,
providerSessionId: piLabState.providerSessionId,
pagePath: runtime.pagePath || detailSession.pagePath,
pageTitle: runtime.pageTitle || detailSession.pageTitle || detailSession.title,
rootUri: runtime.rootUri || detailSession.rootUri,
workspaceId: runtime.workspaceId || detailSession.workspaceId,
modelProvider: runtime.modelProvider || detailSession.modelProvider,
modelId: runtime.modelId || detailSession.modelId,
thinkingLevel: runtime.thinkingLevel || detailSession.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 || detailSession.permissionMode || piLabState.permissionMode);
piLabState.allowedRootsSummary = summarizeAllowedRoots(runtime.allowedRootsSnapshot || piLabState.session.allowedRootsSnapshot);
exitHistoryReplayMode();
resetPiRunViewModel();
return loadSessionTree(sessionId).then(function (treeData) {
var treeMessages = treeData && Array.isArray(treeData.messages) && treeData.messages.length
? treeData.messages
: (treeData && treeData.entries && treeData.entries.length ? treeEntriesToMessages(treeData.entries) : []);
applyReplayMessages(treeMessages);
}).then(function () {
if (streamingAssistantMsg && String(streamingAssistantMsg.text || '').trim()) {
applyPiRunEvent({ type: 'assistant_done', text: streamingAssistantMsg.text });
}
setState(STATE_IDLE);
updateDiagnostics('已打开历史 session,可直接继续发送。');
updateMessages();
updateButtons();
updateArtifactRail();
updateReceiptDisplay();
updateQueueDisplay();
setHistoryPanelVisible(false);
});
});
}
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 forkHistoryEntry(sessionId, entryId) {
if (!sessionId) return Promise.reject(new Error('sessionId required'));
if (!entryId) return forkHistorySession(sessionId);
return fetch(API.FORK, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sessionId, entryId: entryId }),
}).then(function (r) { return r.json(); }).then(function (data) {
if (data && data.sessionId) {
showPiToast('从 entry 创建的新会话已就绪', 'success');
return openHistorySession(data.sessionId);
}
throw new Error((data && (data.message || data.error)) || 'fork failed');
});
}
function renderMessage(msg) {
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');
var meta = msg.status === 'streaming' ? 'streaming' : (msg.status === 'aborted' ? 'aborted' : (msg.meta || ''));
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) {
var rendersMarkdown = msg.role === 'assistant' || msg.role === 'system';
bubble.appendChild(el('div', {
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>'),
}));
}
if (msg.toolCalls && msg.toolCalls.length) {
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',
});
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);
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);
}
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'),
}),
]));
}
if (piLabState.viewingHistorySessionId && msg.role !== 'system') {
var forkEntryBtn = el('div', { className: 'wolai-page-ai-pi-lab-message-actions wolai-page-ai-pi-lab-fork-entry' }, [
el('button', {
type: 'button',
title: '从该条消息继续',
'aria-label': '从该条消息继续',
'data-page-ai-pi-lab-fork-entry': msg.entryId || msg._entryIndex || '',
innerHTML: '从这里继续',
}),
]);
bubble.appendChild(forkEntryBtn);
}
card.appendChild(bubble);
return card;
}
function isVisibleMessage(msg) {
if (!msg) return false;
if (msg.role === 'system' && /^Pi session started:/.test(String(msg.text || ''))) return false;
if (msg.status === 'streaming') return true;
if (msg.status === 'aborted' || msg.abortReason) return true;
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 modelWrap = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-menu-wrap]');
var modelControl = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-menu-toggle]');
var modelLabel = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-label]');
var modelMenu = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-menu]');
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;
modelId = stripModelProviderPrefix(provider, modelId);
piLabState.modelProvider = provider;
piLabState.modelId = modelId;
if (titleLabel) titleLabel.textContent = '默认模型:' + currentModelLabel();
if (modelChip) modelChip.textContent = currentModelLabel();
if (modelWrap) modelWrap.setAttribute('data-open', piLabState.modelMenuOpen ? 'true' : 'false');
if (modelControl) {
modelControl.setAttribute('data-active', piLabState.modelMenuOpen ? 'true' : 'false');
modelControl.setAttribute('aria-expanded', piLabState.modelMenuOpen ? 'true' : 'false');
modelControl.title = '模型:' + currentModelLabel();
}
if (modelLabel) modelLabel.textContent = currentModelLabel();
if (modelMenu) {
modelMenu.innerHTML = modelControlOptions().map(function (item) {
var active = item.provider === provider && item.modelId === modelId;
return '<button type="button" class="wolai-page-ai-pi-lab-control-option" role="menuitemradio" aria-checked="' + String(active) + '" data-page-ai-pi-lab-model-option data-provider="' + escapeHtml(item.provider) + '" data-model-id="' + escapeHtml(item.modelId) + '" data-active="' + String(active) + '" title="' + escapeHtml(item.key) + '">' +
'<span class="wolai-page-ai-pi-lab-control-option-label">' + escapeHtml(item.label) + '</span>' +
'<span class="wolai-page-ai-pi-lab-control-option-check">' + (active ? '✓' : '') + '</span>' +
'</button>';
}).join('');
}
if (providerSelect && providerSelect.value !== provider) providerSelect.value = provider;
if (modelSelect && modelSelect.value !== modelId) {
if (Array.prototype.some.call(modelSelect.options, function (option) { return option.value === modelId; })) {
modelSelect.value = modelId;
} else if (Array.prototype.some.call(modelSelect.options, function (option) { return option.value === provider + '/' + modelId; })) {
modelSelect.value = provider + '/' + modelId;
}
}
if (customInput) customInput.value = modelId !== DEFAULT_MODEL_ID && modelId !== 'freefirst' ? modelId : '';
}
function updateConfig() {
if (!piLabPanelEl) return;
updateModelLabel();
updateThinkingControl();
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]');
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]');
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();
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 ? '正在查看历史,只读' : '';
}
updateContextStrip();
updatePermissionControl();
updateButtons();
updateQueueDisplay();
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);
}
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();
}
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 modelControl = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-menu-toggle]');
var thinkingControl = piLabPanelEl.querySelector('[data-page-ai-pi-lab-thinking-menu-toggle]');
var customInput = piLabPanelEl.querySelector('[data-page-ai-pi-lab-model-custom]');
var isReplay = !!piLabState.viewingHistorySessionId;
var isIdle = piLabState.status === STATE_IDLE || piLabState.status === STATE_ABORTED || piLabState.status === STATE_ERROR;
var isStarted = piLabState.status === STATE_STARTED;
var isStreaming = piLabState.status === STATE_STREAMING;
var compactBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-compact]');
if (sendBtn) {
sendBtn.disabled = isReplay || !piLabState.enabled || !input || !input.value.trim();
}
if (abortBtn) abortBtn.hidden = !(isStreaming || piLabState.status === STATE_STARTING);
if (compactBtn) compactBtn.disabled = isReplay || piLabState.isCompacting;
if (input) {
input.disabled = isReplay;
input.placeholder = isReplay ? '历史对话为只读,Fork 后继续' : '问 Pi...';
}
var controlsDisabled = isReplay || piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTING;
[providerSelect, modelSelect, modelControl, thinkingControl, customInput].forEach(function (node) {
if (node) node.disabled = isReplay || piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTING;
});
if (controlsDisabled && (piLabState.modelMenuOpen || piLabState.thinkingMenuOpen)) {
piLabState.modelMenuOpen = false;
piLabState.thinkingMenuOpen = false;
updateModelLabel();
updateThinkingControl();
}
}
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;
}
}
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);
updateArtifactRail();
return;
}
visibleMessages.forEach(function (msg) { container.appendChild(renderMessage(msg)); });
container.scrollTop = container.scrollHeight;
updateContextStrip();
updateQueueDisplay();
updateArtifactRail();
}
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());
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(','));
}
if (piLabState.allowedRootsSummary) lines.push('allowedRoots=' + piLabState.allowedRootsSummary);
if (piLabState.diagnostics) lines.push('message=' + piLabState.diagnostics);
pre.textContent = lines.join('\n');
}
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() {
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) {
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;
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));
});
}
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: '文件变更',
toolEventId: msg.diffSummary.toolEventId || piLabState.currentToolEventId || '',
sessionId: piLabState.sessionId || '',
});
});
}
(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',
}, '打开引用'));
}
if (artifact.kind === 'diff' && artifact.toolEventId) {
actions.appendChild(el('button', {
type: 'button',
'data-page-ai-pi-lab-artifact-view-diff': artifact.toolEventId,
'data-page-ai-pi-lab-artifact-diff-sid': artifact.sessionId || '',
}, '查看 diff'));
}
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));
});
}
function setState(newState) {
piLabState.status = newState;
if (piRunViewModel) piRunViewModel.runtimeStatus = newState;
updateConfig();
maybeApplyPendingPermissionMode();
maybeApplyPendingModelConfig();
}
function piLabDraftKey() {
var wid = piLabState.workspaceId || currentWorkspaceId() || 'default';
var root = piLabState.rootUri || currentRootUri() || '';
var page = piLabState.pagePath || '';
var sid = piLabState.sessionId || piLabState.viewingHistorySessionId || 'new';
return 'pi-lab:' + wid + ':' + root + ':' + page + ':' + sid;
}
function savePiLabDraft() {
if (!piLabPanelEl) return;
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
if (!input) return;
var text = input.value || '';
if (text.trim()) {
try { localStorage.setItem(piLabDraftKey(), text); } catch (e) {}
} else {
clearPiLabDraft();
}
}
function restorePiLabDraft() {
if (!piLabPanelEl) return;
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
if (!input) return;
try {
var saved = localStorage.getItem(piLabDraftKey());
if (saved) {
input.value = saved;
updateButtons();
}
} catch (e) {}
}
function clearPiLabDraft() {
try {
localStorage.removeItem(piLabDraftKey());
} catch (e) {}
}
function syncRuntimeState() {
if (!piLabState.sessionId && !piLabState.viewingHistorySessionId) return;
var sid = piLabState.sessionId || piLabState.viewingHistorySessionId;
fetch(API.STATE, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid }),
}).then(function (r) { return r.json(); }).then(function (data) {
if (!data) return;
if (data.queuedMessages !== undefined) piLabState.queuedMessages = data.queuedMessages;
if (data.pendingMessageCount !== undefined) piLabState.pendingMessageCount = data.pendingMessageCount;
if (data.isCompacting !== undefined) piLabState.isCompacting = data.isCompacting;
if (data.contextUsage !== undefined) piLabState.contextUsage = data.contextUsage;
if (data.modelProvider) piLabState.modelProvider = data.modelProvider;
if (data.modelId) piLabState.modelId = data.modelId;
if (data.thinkingLevel) piLabState.thinkingLevel = normalizeThinkingLevel(data.thinkingLevel);
updateQueueDisplay();
updateConfig();
}).catch(function () {});
}
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;
});
}
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;
if (data.defaultThinkingLevel) piLabState.defaultThinkingLevel = normalizeThinkingLevel(data.defaultThinkingLevel);
piLabState.modelProvider = (data.session && data.session.modelProvider) || piLabState.modelProvider || null;
piLabState.modelId = (data.session && data.session.modelId) || piLabState.modelId || null;
var sessionThinkingLevel = data.session && data.session.thinkingLevel;
if (sessionThinkingLevel) {
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);
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;
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) || '';
piLabState.runtimePid = data.pid || (data.session && data.session.runtimePid) || piLabState.runtimePid || null;
piLabState.warmupRunning = data.warmupRunning === true;
piLabState.warmupSessionId = data.warmupSessionId || null;
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 || [];
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);
}
updateBuiltinManaged();
updateModelLabel();
updateThinkingControl();
updatePermissionControl();
updateReceiptDisplay();
updateDiagnostics('');
updateMessages();
return data;
})
.catch(function (error) {
piLabState.enabled = false;
setState(STATE_ERROR);
updateDiagnostics(error && error.message ? error.message : 'status failed');
});
}
function startRuntime() {
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();
ensurePiToolCapableModel();
setState(STATE_STARTING);
updateDiagnostics(piLabState.warmupRunning ? 'Binding current page Pi session' : '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');
});
})
.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;
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;
piLabState.runtimeMode = session.runtimeMode || data.runtimeMode || piLabState.runtimeMode || null;
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 || '';
piLabState.runtimePid = session.runtimePid || data.pid || piLabState.runtimePid || null;
piLabState.managedBuiltinTools = data.managedPiBuiltinTools || data.managedBuiltinTools || piLabState.managedBuiltinTools || [];
piLabState.piExtensionSources = data.piExtensionSources || piLabState.piExtensionSources || [];
piLabState.piExtensionToolNames = data.piExtensionToolNames || piLabState.piExtensionToolNames || [];
connectEventSource();
setState(STATE_STARTED);
updateDiagnostics('Pi runtime ready');
updateMessages();
return data;
})
.catch(function (err) {
setState(STATE_ERROR);
updateDiagnostics('Start error: ' + err.message);
throw err;
})
.finally(function () {
piLabStartPromise = null;
});
return piLabStartPromise;
}
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 fetchQueueConfig() {
if (!piLabState.sessionId) return Promise.resolve(null);
return fetch(API.QUEUE_CONFIG, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: piLabState.sessionId }),
}).then(function (r) { return r.json(); }).then(function (data) {
if (data && data.steering !== undefined) piLabState.pendingQueue.steering = data.steering;
if (data && data.followUp !== undefined) piLabState.pendingQueue.followUp = data.followUp;
updateQueueDisplay();
return data;
}).catch(function () { return null; });
}
function handleAtMention(inputEl) {
if (!inputEl) return;
var text = inputEl.value || '';
var atIndex = text.lastIndexOf('@');
if (atIndex < 0 || (atIndex > 0 && text[atIndex - 1] !== ' ' && text[atIndex - 1] !== '\n' && text[atIndex - 1] !== '\t')) {
var existingMenu = document.querySelector('.wolai-page-ai-pi-lab-mention-menu');
if (existingMenu) existingMenu.remove();
return;
}
var query = text.slice(atIndex + 1).split(/\s/)[0] || '';
var candidates = [];
if (piLabState.allowedRootsSummary && piLabState.allowedRootsSummary !== 'pending') {
candidates.push({ label: '@' + piLabState.allowedRootsSummary, value: piLabState.allowedRootsSummary, type: 'root' });
}
var pagePath = piLabState.pagePath || currentFolderPathFromPagePath(currentPageContext().pagePath) || '';
if (pagePath) {
var parts = pagePath.split('/');
var folderName = parts[parts.length - 1] || pagePath;
candidates.push({ label: '@' + folderName, value: folderName, type: 'folder' });
}
if (query) {
candidates = candidates.filter(function (c) { return c.label.toLowerCase().indexOf(query.toLowerCase()) >= 0; });
}
var existingMenu = document.querySelector('.wolai-page-ai-pi-lab-mention-menu');
if (existingMenu) existingMenu.remove();
if (!candidates.length) return;
var menu = document.createElement('div');
menu.className = 'wolai-page-ai-pi-lab-mention-menu';
menu.style.cssText = 'position:fixed;bottom:100px;left:50%;transform:translateX(-50%);background:#fff;border:1px solid #e0e0dc;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);padding:4px 0;z-index:10000;max-height:160px;overflow-y:auto;min-width:200px;font:13px/1.5 system-ui';
candidates.forEach(function (cand) {
var item = document.createElement('button');
item.type = 'button';
item.style.cssText = 'display:block;width:100%;text-align:left;padding:6px 14px;border:none;background:none;cursor:pointer;color:#333';
item.textContent = cand.label;
item.addEventListener('click', function () {
var before = text.slice(0, atIndex);
var rest = text.slice(atIndex + 1 + query.length);
inputEl.value = before + '@' + cand.value + ' ' + rest;
inputEl.focus();
menu.remove();
updateButtons();
savePiLabDraft();
});
item.addEventListener('mouseenter', function () { item.style.background = '#f0f0ed'; });
item.addEventListener('mouseleave', function () { item.style.background = 'none'; });
menu.appendChild(item);
});
document.body.appendChild(menu);
}
function connectEventSource(sessionId) {
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;
piLabState.managedBuiltinTools = payload.managedBuiltinTools || payload.managedPiBuiltinTools || piLabState.managedBuiltinTools;
piLabState.piExtensionSources = payload.piExtensionSources || piLabState.piExtensionSources || [];
piLabState.piExtensionToolNames = payload.piExtensionToolNames || piLabState.piExtensionToolNames || [];
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 (_) {}
});
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 (_) {}
if (piLabEventSource === es) {
es.close();
piLabEventSource = null;
}
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');
});
es.addEventListener('error', function () {
if (piLabEventSource && piLabEventSource.readyState === EventSource.CLOSED) updateDiagnostics('SSE connection closed');
});
piLabEventSource = es;
}
function ensureStreamingAssistant() {
if (!streamingAssistantMsg) applyPiRunEvent({ type: 'assistant_begin' });
setState(STATE_STREAMING);
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('');
}
function currentTurnToolCalls() {
var current = streamingAssistantMsg || (piRunViewModel && piRunViewModel.currentAssistant);
return current && Array.isArray(current.toolCalls) ? current.toolCalls : [];
}
function finishEmptyAssistantTurn(message) {
if (!message || message.role !== 'assistant') return false;
var content = Array.isArray(message.content) ? message.content : [];
if (content.some(function (part) { return part && (part.type === 'text' || part.type === 'toolCall'); })) return false;
var toolCalls = currentTurnToolCalls();
if (toolCalls.length > 0) {
var failed = toolCalls.filter(function (tool) { return tool && (tool.status === 'error' || tool.isError === true); });
var labels = toolCalls.map(function (tool) {
var name = String((tool && (tool.toolName || tool.name)) || 'tool');
return name + (tool && (tool.status === 'error' || tool.isError === true) ? '(失败)' : '(完成)');
});
var summary = (failed.length > 0 ? '工具检查已结束,其中有失败项:' : '工具检查已完成:') + labels.join('、') + '。';
applyPiRunEvent({ type: 'assistant_done', text: summary });
setState(STATE_STARTED);
updateMessages();
updateDiagnostics('Pi assistant completed with tool-only result');
return true;
}
applyPiRunEvent({
type: 'assistant_error',
payload: {
error: 'Pi runtime 返回了空回复;请新建会话后重试。',
},
});
setState(STATE_ERROR);
updateMessages();
updateDiagnostics('Pi assistant turn ended without text or tool calls');
return true;
}
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();
}
if (toolParts.length > 0 || !!visibleText) return true;
updateDiagnostics('Pi assistant message is empty; waiting for agent_end');
return true;
}
if (message.role === 'toolResult') {
var toolResultPayload = {
toolCallId: message.toolCallId || message.tool_call_id || '',
toolName: message.toolName || message.tool_name || 'tool',
details: message.details || {},
};
upsertToolCall(toolResultPayload, message.isError ? 'error' : 'done', {
result: {
content: message.content || [],
details: message.details || {},
},
isError: message.isError === true,
});
if (/knowledge_rag|knowledge\.rag/i.test(String(toolResultPayload.toolName || ''))) {
appendKnowledgeRagCitations(message.details || {});
updateMessages();
}
return true;
}
return false;
}
function handlePiRpcEvent(payload) {
if (!payload) return;
var eventType = payload.type;
var msgData = payload.assistantMessageEvent || payload;
var msgDataType = msgData && msgData.type;
if (handlePiExtensionUiRequest(payload)) return;
if (msgDataType === 'thinking_start' || msgDataType === 'thinking_delta' || msgDataType === 'thinking_end') {
updateDiagnostics('Pi thinking event hidden from visible reply');
return;
}
if (eventType === 'queue_update') {
applyPiRunEvent({ type: 'queue_update', payload: payload });
updateQueueDisplay();
return;
}
if (eventType === 'message' && handlePiMessageEvent(payload)) return;
if (msgDataType === 'text_start') {
setState(STATE_STREAMING);
applyPiRunEvent({ type: 'assistant_begin' });
updateMessages();
return;
}
if (msgDataType === 'text_delta') {
applyPiRunEvent({ type: 'assistant_delta', payload: msgData });
updateMessages();
} else if (msgDataType === 'text_end') {
applyPiRunEvent({ type: 'assistant_text_end', payload: msgData });
updateMessages();
} else if (eventType === 'message_end' && payload.message && payload.message.role === 'toolResult') {
handlePiMessageEvent(payload);
} else if (eventType === 'message_end' && payload.message && payload.message.role === 'assistant') {
var visibleText = extractVisibleAssistantText(payload.message);
if (!visibleText) {
updateDiagnostics('Pi assistant message_end has no text; waiting for agent_end');
return;
}
applyPiRunEvent({ type: 'assistant_done', text: visibleText });
setState(STATE_STARTED);
updateMessages();
} else if (eventType === 'agent_end') {
var finalMessages = Array.isArray(payload.messages) ? payload.messages : [];
finalMessages.forEach(function (message) {
if (
message
&& message.role === 'toolResult'
&& /knowledge_rag|knowledge\.rag/i.test(String(message.toolName || message.tool_name || ''))
) {
appendKnowledgeRagCitations(message.details || {});
}
});
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;
}
if (!finalText && finalAssistant && finishEmptyAssistantTurn(finalAssistant)) return;
applyPiRunEvent({ type: 'assistant_done', text: finalText });
setState(STATE_STARTED);
updateMessages();
} 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,
});
}
} 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);
updateMessages();
} else if (eventType === 'citation' || msgDataType === 'citation') {
applyPiRunEvent({ type: 'citation', payload: { source: payload.source || msgData.source || '', title: payload.title || msgData.title || '', url: payload.url || msgData.url || '#' } });
updateMessages();
} else if (eventType === 'diff' || msgDataType === 'diff') {
var diffFiles = payload.files || msgData.files || [];
var diffToolEventId = payload.toolEventId || msgData.toolEventId || '';
applyPiRunEvent({ type: 'diff', payload: { files: diffFiles, toolEventId: diffToolEventId } });
diffFiles.forEach(function (file) { if (file) piLabState.changedFiles.push(file); });
if (diffToolEventId) { piLabState.currentToolEventId = diffToolEventId; }
updateMessages();
} else if (eventType === 'done' || msgDataType === 'done') {
applyPiRunEvent({
type: 'assistant_done',
payload: {
text: msgData.text,
toolCalls: msgData.toolCalls,
citations: msgData.citations,
diffSummary: msgData.diffSummary,
},
});
setState(STATE_STARTED);
updateMessages();
} else if (eventType === 'error' || msgDataType === 'error') {
applyPiRunEvent({ type: 'assistant_error', payload: { error: payload.error || msgData.message || 'Pi runtime error' } });
setState(STATE_STARTED);
updateMessages();
}
}
function sendPrompt(text, options) {
var prompt = String(text || '').trim();
if (!prompt) return;
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' });
setState(STATE_STREAMING);
updateMessages();
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;
return fetch(API.SEND, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then(function (r) {
return responseJsonOrError(r, 'Send failed');
})
.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) {
if (streamingAssistantMsg) applyPiRunEvent({ type: 'assistant_error', message: err.message });
setState(STATE_STARTED);
updateMessages();
});
}
function ensureRuntimeReady() {
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) {
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));
if (payload && payload.ok === false) {
var result = payload.result || {};
throw new Error(result.message || payload.message || result.code || 'Tool call failed');
}
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) {
showPiToast('当前页未绑定文件上下文', 'warning');
updateDiagnostics('当前页缺少 rootUri/pagePath,无法作为 Pi 上下文');
return;
}
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');
return;
}
if (kind === 'selection') {
var selection = refreshSelectionSummary();
if (!selection && !contextSelectionState().selection) showPiToast('当前没有选区', 'warning');
toggleContextQuick(kind);
return;
}
toggleContextQuick('rag');
showPiToast((contextSelectionState().lightrag ? '已启用' : '已关闭') + ' LightRAG 上下文', 'info');
}
function sendCurrentInput(options) {
if (!piLabPanelEl) return;
var input = piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
var text = input ? input.value.trim() : '';
if (!text) return;
var startToast = piLabState.warmupRunning
? '正在绑定当前页 Pi 会话,完成后自动发送'
: 'Pi Rust 正在启动,启动后自动发送';
if (piLabState.status === STATE_IDLE || piLabState.status === STATE_ABORTED || piLabState.status === STATE_ERROR || !piLabState.sessionId) {
showPiToast(startToast, '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(startToast, '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 (input) {
input.value = '';
clearPiLabDraft();
}
updateButtons();
var isMidStream = piLabState.status === STATE_STREAMING;
var streamingBehavior = isMidStream ? ((options && options.streamingBehavior) || 'steer') : undefined;
sendPrompt(text, { streamingBehavior: streamingBehavior }).catch(function () {});
}
function abortPrompt() {
var piInput = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
if (piInput && piInput.value.trim()) savePiLabDraft();
if (piLabEventSource) {
piLabEventSource.close();
piLabEventSource = null;
}
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;
var preservedContext = currentPageContext();
if (piLabEventSource) {
piLabEventSource.close();
piLabEventSource = null;
}
if (previousSessionId && !piLabState.viewingHistorySessionId && (piLabState.status === STATE_STARTED || piLabState.status === STATE_STREAMING || piLabState.status === STATE_STARTING)) {
fetch(API.ABORT, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: previousSessionId }),
}).catch(function () {});
}
closeActionMenu();
setHistoryPanelVisible(false);
exitHistoryReplayMode();
piLabState.sessionId = null;
piLabState.providerSessionId = null;
piLabState.session = {
rootUri: preservedContext.rootUri || undefined,
workspaceId: preservedContext.workspaceId || undefined,
pagePath: preservedContext.pagePath || undefined,
pageTitle: preservedContext.pageTitle || undefined,
};
piLabState.contextSelection = {
currentPage: false,
currentFolder: false,
selection: false,
lightrag: false,
};
piLabState.thinkingLevel = normalizeThinkingLevel(piLabState.defaultThinkingLevel || DEFAULT_THINKING_LEVEL);
piLabState.status = STATE_IDLE;
resetPiRunViewModel();
piLabState.receipts = [];
updateMessages();
updateReceiptDisplay();
updateQueueDisplay();
updateThinkingControl();
updateConfig();
}
function triggerCompact() {
if (piLabState.status === STATE_STREAMING) return;
if (piLabState.viewingHistorySessionId) return;
if (!piLabState.sessionId) return;
piLabState.isCompacting = true;
updateQueueDisplay();
fetch(API.COMPACT, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: piLabState.sessionId }),
}).then(function (r) { return r.json(); }).then(function (data) {
piLabState.isCompacting = false;
if (data && data.summary) {
updateDiagnostics('Compaction: ' + data.summary);
showPiToast('会话压缩完成', 'success');
} else if (data && data.message) {
showPiToast(data.message, 'info');
} else {
showPiToast('会话压缩完成', 'success');
}
if (data && data.messages && data.messages.length) {
var compactCard = { role: 'system', text: '会话压缩已完成,共 ' + data.messages.length + ' 条消息', type: 'compaction', 'data-page-ai-pi-lab-compaction-result': 'true' };
piLabState.messages.push(compactCard);
updateMessages();
}
syncRuntimeState();
}).catch(function (err) {
piLabState.isCompacting = false;
showPiToast('压缩失败:' + (err.message || ''), 'error');
syncRuntimeState();
});
}
function parseUnifiedPatch(patchText) {
if (!patchText) return [];
var files = [];
var fileLines = patchText.split('\\n');
var currentFile = null;
var currentHunk = null;
for (var i = 0; i < fileLines.length; i++) {
var line = fileLines[i];
if (line.slice(0, 4) === '--- ') {
if (currentFile) files.push(currentFile);
currentFile = { oldPath: line.slice(4).trim(), newPath: '', hunks: [] };
if (i + 1 < fileLines.length && fileLines[i + 1].slice(0, 4) === '+++ ') {
currentFile.newPath = fileLines[i + 1].slice(4).trim();
i++;
}
} else if (line.slice(0, 2) === '@@' && currentFile) {
currentHunk = { header: line, lines: [] };
currentFile.hunks.push(currentHunk);
} else if (currentHunk) {
var type = 'ctx';
if (line[0] === '+') type = 'add';
else if (line[0] === '-') type = 'del';
else if (line[0] === '\\') continue;
currentHunk.lines.push({ type: type, text: line });
}
}
if (currentFile) files.push(currentFile);
return files;
}
function showDiffOverlay(title, html) {
var overlay = document.createElement('div');
overlay.className = 'wolai-page-ai-pi-lab-diff-overlay';
overlay.innerHTML = '<div class="wolai-page-ai-pi-lab-diff-header"><span>' + escapeHtml(title) + '</span><button type="button" class="wolai-page-ai-pi-lab-diff-close" aria-label="关闭 diff">×</button></div><div class="wolai-page-ai-pi-lab-diff-body">' + html + '</div>';
overlay.querySelector('.wolai-page-ai-pi-lab-diff-close').addEventListener('click', function () { overlay.remove(); });
overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
}
function renderSplitDiff(files) {
if (!files || !files.length) return '<div style="padding:20px;color:#888;text-align:center">无 diff 数据</div>';
var html = '';
files.forEach(function (file) {
html += '<div class="wolai-page-ai-pi-lab-diff-file-header">' + escapeHtml(file.newPath || file.oldPath || 'unknown') + '</div>';
(file.hunks || []).forEach(function (hunk, hi) {
if (hi > 0) html += '<div class="wolai-page-ai-pi-lab-diff-hunk-header">' + escapeHtml(hunk.header) + '</div>';
var ctxCount = 0;
var ctxLines = [];
hunk.lines.forEach(function (ln) {
if (ln.type === 'ctx') {
ctxCount++;
ctxLines.push(ln);
if (ctxCount >= 6) {
if (ctxLines.length > 6) {
html += '<div class="wolai-page-ai-pi-lab-diff-collapse" onclick="var n=this.nextElementSibling;if(n)n.hidden=!n.hidden;this.hidden=true">⋯ 展开 ' + (ctxLines.length - 6) + ' 行上下文</div>';
html += '<div hidden>';
ctxLines.slice(0, ctxLines.length - 6).forEach(function (cl) {
html += '<div class="wolai-page-ai-pi-lab-diff-line wolai-page-ai-pi-lab-diff-line-ctx"><span class="wolai-page-ai-pi-lab-diff-line-num"></span>' + escapeHtml(cl.text) + '</div>';
});
html += '</div>';
}
ctxLines.slice(-6).forEach(function (cl) {
html += '<div class="wolai-page-ai-pi-lab-diff-line wolai-page-ai-pi-lab-diff-line-ctx"><span class="wolai-page-ai-pi-lab-diff-line-num"></span>' + escapeHtml(cl.text) + '</div>';
});
ctxCount = 0;
ctxLines = [];
return;
}
} else {
if (ctxLines.length) {
ctxLines.forEach(function (cl) {
html += '<div class="wolai-page-ai-pi-lab-diff-line wolai-page-ai-pi-lab-diff-line-ctx"><span class="wolai-page-ai-pi-lab-diff-line-num"></span>' + escapeHtml(cl.text) + '</div>';
});
ctxLines = [];
ctxCount = 0;
}
html += '<div class="wolai-page-ai-pi-lab-diff-line wolai-page-ai-pi-lab-diff-line-' + ln.type + '"><span class="wolai-page-ai-pi-lab-diff-line-num"></span>' + escapeHtml(ln.text) + '</div>';
}
});
if (ctxLines.length) {
ctxLines.forEach(function (cl) {
html += '<div class="wolai-page-ai-pi-lab-diff-line wolai-page-ai-pi-lab-diff-line-ctx"><span class="wolai-page-ai-pi-lab-diff-line-num"></span>' + escapeHtml(cl.text) + '</div>';
});
}
});
});
return html;
}
function fetchAndShowDiff(toolEventId, sessionId) {
if (!toolEventId) { showPiToast('无 diff 数据', 'warning'); return; }
var sid = sessionId || piLabState.sessionId || piLabState.viewingHistorySessionId;
if (!sid) { showPiToast('缺少 session ID', 'warning'); return; }
fetch(API.DIFF + '/' + encodeURIComponent(toolEventId) + '/diff?sessionId=' + encodeURIComponent(sid), {
credentials: 'same-origin',
cache: 'no-store'
}).then(function (r) {
if (!r.ok) throw new Error('Diff fetch failed: ' + r.status);
return r.json();
}).then(function (data) {
var patchText = data.patch || data.diff || data.text || '';
if (!patchText) {
showDiffOverlay('Diff: ' + toolEventId, '<div style="padding:20px;color:#888;text-align:center">无 diff 内容' + (data.message ? ': ' + escapeHtml(data.message) : '') + '</div>');
return;
}
var files = parseUnifiedPatch(patchText);
var title = 'Diff: ' + (data.toolEventId || toolEventId);
showDiffOverlay(title, renderSplitDiff(files));
}).catch(function (err) {
showPiToast('加载 diff 失败:' + (err.message || ''), 'error');
});
}
function ensurePanel() {
if (piLabPanelEl) return piLabPanelEl;
var container = document.createElement('div');
container.innerHTML = piLabPanelHTML();
piLabPanelEl = container.firstElementChild;
wireEvents();
updateReceiptDisplay();
updateArtifactRail();
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();
checkStatus().then(function () {
if (piLabState.enabled && piLabState.runtimeAvailable !== false && !piLabState.sessionId && piLabState.status !== STATE_STARTING) {
return startRuntime().then(function () {
syncRuntimeState();
});
}
syncRuntimeState();
updateButtons();
return null;
}).then(function () {
updateButtons();
}).catch(function (error) {
updateDiagnostics(error && error.message ? error.message : 'Pi current page session prepare failed');
updateButtons();
});
var input = piLabPanelEl && piLabPanelEl.querySelector('[data-page-ai-pi-lab-input]');
if (input) {
restorePiLabDraft();
setTimeout(function () { input.focus(); }, 60);
}
}
function hidePiLab() {
piLabState.active = false;
closeComposerControlMenus();
if (piLabDrawerEl) setDrawerVisible(false);
updateLauncherState();
}
function updateLauncherState() {
if (!piLabLauncherEl) return;
piLabLauncherEl.setAttribute('data-state', piLabState.status);
piLabLauncherEl.hidden = !!piLabState.active;
}
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 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]');
var compactBtn = piLabPanelEl.querySelector('[data-page-ai-pi-lab-btn-compact]');
var newBtns = piLabPanelEl.querySelectorAll('[data-page-ai-pi-lab-new]');
if (sendBtn) sendBtn.addEventListener('click', sendCurrentInput);
if (abortBtn) abortBtn.addEventListener('click', abortPrompt);
if (compactBtn) compactBtn.addEventListener('click', triggerCompact);
if (clearBtn) clearBtn.addEventListener('click', function () {
resetPiRunViewModel();
piLabState.receipts = [];
updateMessages();
updateReceiptDisplay();
updateQueueDisplay();
});
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', function () {
updateButtons();
handleAtMention(input);
savePiLabDraft();
});
input.addEventListener('keydown', function (e) {
if (e.isComposing || e.key === 'Process') return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendCurrentInput(e.altKey ? { streamingBehavior: 'followUp' } : undefined);
}
if (e.key === 'Escape' && piLabState.status === STATE_STREAMING) {
e.preventDefault();
abortPrompt();
}
});
}
[providerSelect, modelSelect].forEach(function (node) {
if (node) node.addEventListener('change', function () {
applyModelControls();
applyModelConfigToRuntime();
});
});
if (customInput) customInput.addEventListener('input', applyModelControls);
if (settingsBtn) {
settingsBtn.addEventListener('click', function () {
window.location.assign('/user/ai#ai-admin-access');
});
}
if (historyBtn) {
historyBtn.addEventListener('click', function () {
var layer = piLabPanelEl.querySelector('[data-page-ai-pi-lab-history-layer]');
if (!layer) return;
setHistoryPanelVisible(layer.hidden);
});
}
Array.prototype.slice.call(newBtns || []).forEach(function (btn) {
btn.addEventListener('click', startNewConversation);
});
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 modelMenuToggle = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-model-menu-toggle]') : null;
if (modelMenuToggle && !modelMenuToggle.disabled) {
setModelMenuOpen(!piLabState.modelMenuOpen);
return;
}
var modelOption = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-model-option]') : null;
if (modelOption && !modelOption.disabled) {
selectModelControl(
modelOption.getAttribute('data-provider'),
modelOption.getAttribute('data-model-id')
);
return;
}
var thinkingMenuToggle = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-thinking-menu-toggle]') : null;
if (thinkingMenuToggle && !thinkingMenuToggle.disabled) {
setThinkingMenuOpen(!piLabState.thinkingMenuOpen);
return;
}
var thinkingOption = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-thinking-option]') : null;
if (thinkingOption && !thinkingOption.disabled) {
selectThinkingControl(thinkingOption.getAttribute('data-page-ai-pi-lab-thinking-option'));
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 forkEntry = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-fork-entry]') : null;
if (forkEntry) {
var forkEntryId = forkEntry.getAttribute('data-page-ai-pi-lab-fork-entry');
forkHistoryEntry(piLabState.viewingHistorySessionId, forkEntryId).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) {
var artUrl = openArtifact.getAttribute('data-page-ai-pi-lab-artifact-open') || openArtifact.getAttribute('href') || '';
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: artUrl,
},
}));
if (!artUrl || artUrl === '#') {
showPiToast('引用无效或无法访问', 'warning');
}
}
var viewDiff = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-artifact-view-diff]') : null;
if (viewDiff) {
var diffTeid = viewDiff.getAttribute('data-page-ai-pi-lab-artifact-view-diff');
var diffSid = viewDiff.getAttribute('data-page-ai-pi-lab-artifact-diff-sid');
fetchAndShowDiff(diffTeid, diffSid);
return;
}
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;
}
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'));
});
});
document.addEventListener('pointerdown', function (event) {
if (!piLabState.active || !piLabPanelEl) return;
var target = event.target;
var controlSurface = target && target.closest ? target.closest(
'[data-page-ai-pi-lab-action-menu-toggle], [data-page-ai-pi-lab-action-menu], ' +
'[data-page-ai-pi-lab-model-menu-wrap], [data-page-ai-pi-lab-thinking-menu-wrap], ' +
'[data-page-ai-pi-lab-permission-wrap]'
) : null;
if (!controlSurface) closeComposerControlMenus();
}, true);
}
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;
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,
getCurrentContext: currentPageContext,
getState: function () { return piLabState; },
};
}
})();