chore: rebuild release package

This commit is contained in:
shiyue
2026-07-27 17:06:32 +08:00
parent 85f2f2d420
commit efe7c02ec3
12 changed files with 548 additions and 25 deletions

View File

@@ -226,6 +226,7 @@
let loginPasswordValue = ''; // store login password for force-change flow
let currentCwd = null;
let currentSessionMessageCount = 0;
let currentOutlineTitleHistory = [];
let currentSessionRunning = false;
let fileBrowserState = null;
let directoryPickerState = null;
@@ -1452,15 +1453,97 @@
userMessageIndex.clear();
}
function registerUserMessage(messageId, element, content) {
function registerUserMessage(messageId, element, content, timestamp = '') {
if (!messageId || !element) return;
const previous = userMessageIndex.get(messageId);
userMessageIndex.set(messageId, {
id: messageId,
element,
content: String(content || ''),
timestamp: String(timestamp || previous?.timestamp || ''),
});
}
function normalizeOutlineTitleHistory(history) {
if (!Array.isArray(history)) return [];
const normalized = [];
history.forEach((event) => {
if (!event || typeof event !== 'object') return;
const title = String(event.title || '').replace(/\s+/g, ' ').trim().slice(0, 120);
const changedAtMs = Date.parse(String(event.changedAt || ''));
const messageIndex = Number(event.messageIndex);
const source = String(event.source || '').trim().toLowerCase();
if (!title || !Number.isFinite(changedAtMs) || !Number.isFinite(messageIndex) || messageIndex < 0 || source !== 'llm') return;
normalized.push({
title,
changedAt: new Date(changedAtMs).toISOString(),
messageIndex: Math.trunc(messageIndex),
source: 'llm',
});
});
return normalized.slice(-100);
}
function formatUserOutlineDate(timestamp) {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return '';
const pad = (value) => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
function buildUserOutlineTimelineItems(messageItems, titleHistory = []) {
const timeline = [];
(Array.isArray(messageItems) ? messageItems : []).forEach((message, order) => {
const messageIndex = Number(message.messageIndex);
const timestampMs = Date.parse(String(message.timestamp || ''));
timeline.push({
...message,
type: 'message',
messageNumber: order + 1,
timestamp: String(message.timestamp || ''),
sortPosition: Number.isFinite(messageIndex) && messageIndex >= 0 ? messageIndex * 2 : null,
sortTime: Number.isFinite(timestampMs) ? timestampMs : null,
sortOrder: order,
});
});
normalizeOutlineTitleHistory(titleHistory).forEach((event, order) => {
const timestampMs = Date.parse(event.changedAt);
timeline.push({
type: 'title',
label: event.title,
timestamp: event.changedAt,
messageIndex: event.messageIndex,
source: event.source,
sortPosition: event.messageIndex * 2 - 1,
sortTime: Number.isFinite(timestampMs) ? timestampMs : null,
sortOrder: order,
});
});
timeline.sort((left, right) => {
if (Number.isFinite(left.sortPosition) && Number.isFinite(right.sortPosition) && left.sortPosition !== right.sortPosition) {
return left.sortPosition - right.sortPosition;
}
if (Number.isFinite(left.sortTime) && Number.isFinite(right.sortTime) && left.sortTime !== right.sortTime) {
return left.sortTime - right.sortTime;
}
if (left.type !== right.type) return left.type === 'message' ? -1 : 1;
return left.sortOrder - right.sortOrder;
});
const result = [];
let previousDate = '';
timeline.forEach((entry) => {
const dateLabel = formatUserOutlineDate(entry.timestamp);
if (dateLabel && dateLabel !== previousDate) {
result.push({ type: 'date', label: dateLabel });
previousDate = dateLabel;
}
const { sortPosition, sortTime, sortOrder, ...item } = entry;
result.push(item);
});
return result;
}
function buildUserOutlineItems() {
const seen = new Set();
return Array.from(messagesDiv.querySelectorAll('.msg.user[data-message-id]')).map((element) => {
@@ -1469,27 +1552,47 @@
seen.add(id);
const indexed = userMessageIndex.get(id);
const content = indexed?.content || element.querySelector('.msg-text')?.textContent || '';
const rawMessageIndex = String(element.dataset.messageIndex || '').trim();
const messageIndex = rawMessageIndex ? Number(rawMessageIndex) : Number.NaN;
return {
id,
targetMessageId: element.id || '',
label: shortMessagePreview(content, 64),
timestamp: indexed?.timestamp || '',
messageIndex: Number.isFinite(messageIndex) ? messageIndex : null,
};
}).filter((entry) => entry && entry.targetMessageId);
}
function updateUserOutlinePanel() {
if (!userOutlinePanel || !userOutlineBtn) return;
const items = buildUserOutlineItems();
const items = buildUserOutlineTimelineItems(buildUserOutlineItems(), currentOutlineTitleHistory);
if (items.length === 0) {
userOutlinePanel.innerHTML = '<div class="user-outline-empty">暂无用户消息</div>';
userOutlineBtn.disabled = true;
} else {
userOutlinePanel.innerHTML = items.map((item, index) => `
<button type="button" class="user-outline-item" data-target="${escapeHtml(item.targetMessageId)}">
<span class="user-outline-index">${index + 1}</span>
<span class="user-outline-text">${escapeHtml(item.label)}</span>
</button>
`).join('');
userOutlinePanel.innerHTML = items.map((item) => {
if (item.type === 'date') {
return `<div class="user-outline-date" role="separator" aria-label="${escapeHtml(item.label)}"><span>${escapeHtml(item.label)}</span></div>`;
}
if (item.type === 'title') {
return `
<div class="user-outline-title-event" role="note" aria-label="标题变更:${escapeHtml(item.label)}">
<span class="user-outline-title-label">标题</span>
<span class="user-outline-title-text">${escapeHtml(item.label)}</span>
</div>
`;
}
if (item.type === 'message') {
return `
<button type="button" class="user-outline-item" data-target="${escapeHtml(item.targetMessageId)}">
<span class="user-outline-index">${item.messageNumber}</span>
<span class="user-outline-text">${escapeHtml(item.label)}</span>
</button>
`;
}
return '';
}).join('');
userOutlineBtn.disabled = false;
}
}
@@ -1848,6 +1951,7 @@
agent: snapshot.agent || '',
cwd: snapshot.cwd || '',
updated: snapshot.updated || '',
titleHistory: normalizeOutlineTitleHistory(snapshot.titleHistory),
}).length;
return base + (snapshot.messages || []).reduce((sum, message) => sum + estimateSessionMessageWeight(message), 0);
}
@@ -1866,6 +1970,7 @@
id: sessionId,
messages,
title: payload.title || '新会话',
titleHistory: normalizeOutlineTitleHistory(payload.titleHistory),
mode: payload.mode || 'yolo',
model: payload.model || '',
agent: normalizeAgent(payload.agent),
@@ -3903,6 +4008,15 @@
return cwd ? (getPathLeaf(cwd) || cwd) : '';
}
function buildSessionItemTooltip(projectName, title) {
const normalizedProjectName = String(projectName || '').trim();
const normalizedTitle = String(title || '').trim() || 'Untitled';
return [
normalizedProjectName ? `项目:${normalizedProjectName}` : '',
`标题:${normalizedTitle}`,
].filter(Boolean).join('\n');
}
function groupSessionsByProject(sessionItems) {
const groups = [];
const groupMap = new Map();
@@ -4059,8 +4173,8 @@
const waitingLabel = readyReplyCount > 0 ? `子对话已返回 ${readyReplyCount}` : `等待子对话 ${Number(session.pendingReplyCount || 0) || ''}`.trim();
item.className = `session-item${session.id === currentSessionId ? ' active' : ''}${isPinned ? ' pinned' : ''}${isLlmCreated ? ' llm-created' : ''}${waitingOnChildren ? ' waiting-children' : ''}`;
item.dataset.id = session.id;
const sessionCwd = getSessionEffectiveCwd(session);
if (sessionCwd) item.title = sessionCwd;
const sessionProjectName = getSessionProjectName(session);
item.title = buildSessionItemTooltip(sessionProjectName, session.title);
item.innerHTML = `
<div class="session-item-main">
<span class="session-item-title">${escapeHtml(session.title || 'Untitled')}</span>
@@ -4254,6 +4368,7 @@
currentSessionId = null;
loadedHistorySessionId = null;
currentSessionMessageCount = 0;
currentOutlineTitleHistory = [];
clearSessionLoading();
setCurrentSessionRunningState(false);
currentCwd = null;
@@ -4311,6 +4426,7 @@
}
currentSessionId = snapshot.sessionId;
loadedHistorySessionId = snapshot.sessionId;
currentOutlineTitleHistory = normalizeOutlineTitleHistory(snapshot.titleHistory);
currentSessionMessageCount = Math.max(
snapshot.historyTotal || 0,
snapshot.historyBaseIndex + (snapshot.messages || []).length,
@@ -5462,9 +5578,16 @@
snapshot.title = msg.title;
if (msg.titleSource !== undefined) snapshot.titleSource = msg.titleSource || null;
if (msg.createdFromKind !== undefined) snapshot.createdFromKind = msg.createdFromKind || null;
if (msg.titleEvent) {
snapshot.titleHistory = normalizeOutlineTitleHistory([...(snapshot.titleHistory || []), msg.titleEvent]);
}
});
if (msg.sessionId === currentSessionId) {
chatTitle.textContent = msg.title;
if (msg.titleEvent) {
currentOutlineTitleHistory = normalizeOutlineTitleHistory([...currentOutlineTitleHistory, msg.titleEvent]);
updateUserOutlinePanel();
}
}
renderSessionList();
break;
@@ -6315,7 +6438,7 @@
setCodexAppSteerStatusElement(div, meta.codexAppSteerStatus, meta.codexAppSteerMessage);
}
if (role === 'user') {
registerUserMessage(resolvedMessageId, div, content);
registerUserMessage(resolvedMessageId, div, content, meta.timestamp);
}
return div;
}
@@ -9086,14 +9209,15 @@
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const messageId = createLocalId('user');
const element = createMsgElement('user', text, attachments, { messageId });
const timestamp = new Date().toISOString();
const element = createMsgElement('user', text, attachments, { messageId, timestamp });
messagesDiv.appendChild(element);
if (currentSessionId) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
}
registerUserMessage(messageId, element, text);
registerUserMessage(messageId, element, text, timestamp);
updateUserOutlinePanel();
scrollToBottom();
@@ -9131,7 +9255,8 @@
}
const attachments = pendingAttachments.map((attachment) => ({ ...attachment }));
const messageId = createLocalId('user');
const element = createMsgElement('user', text, attachments, { messageId, codexAppSteerStatus: 'pending' });
const timestamp = new Date().toISOString();
const element = createMsgElement('user', text, attachments, { messageId, timestamp, codexAppSteerStatus: 'pending' });
const streamEl = document.getElementById('streaming-msg');
const shouldFollow = isNearBottom();
if (streamEl && streamEl.parentNode === messagesDiv) {
@@ -9144,7 +9269,7 @@
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
}
registerUserMessage(messageId, element, text);
registerUserMessage(messageId, element, text, timestamp);
updateUserOutlinePanel();
if (shouldFollow) {
scrollToBottom();

View File

@@ -24,7 +24,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
</script>
<link rel="stylesheet" href="style.css?v=20260720-plan-progress-final-state">
<link rel="stylesheet" href="style.css?v=20260727-session-item-tooltip">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head>
<body>
@@ -183,6 +183,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
<script src="app.js?v=20260720-plan-progress-final-state"></script>
<script src="app.js?v=20260727-session-item-tooltip"></script>
</body>
</html>

View File

@@ -1955,6 +1955,62 @@ body.session-loading-active {
text-overflow: ellipsis;
white-space: nowrap;
}
.user-outline-date {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 4px 5px;
color: var(--text-muted);
font-size: 10px;
line-height: 1;
white-space: nowrap;
}
.user-outline-date::before,
.user-outline-date::after {
content: '';
flex: 1 1 auto;
height: 1px;
background: var(--border-color);
}
.user-outline-date span {
flex: 0 0 auto;
padding: 2px 7px;
border: 1px solid var(--border-color);
border-radius: 999px;
background: var(--bg-primary);
color: var(--text-muted);
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-variant-numeric: tabular-nums;
}
.user-outline-title-event {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
align-items: center;
gap: 8px;
margin: 3px 2px;
padding: 7px 8px;
border-left: 2px solid var(--accent);
border-radius: 8px;
background: var(--accent-light);
color: var(--text-secondary);
cursor: default;
}
.user-outline-title-label {
color: var(--accent);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
}
.user-outline-title-text {
min-width: 0;
overflow: hidden;
color: var(--text-primary);
font-size: 12px;
font-weight: 650;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Mode selector */
.mode-select {