chore: rebuild CentOS7 release package
This commit is contained in:
Binary file not shown.
125
public/app.js
125
public/app.js
@@ -1949,6 +1949,70 @@
|
||||
return current ? `${root}/${current}` : root;
|
||||
}
|
||||
|
||||
function normalizeAbsoluteDisplayPath(input) {
|
||||
return String(input || '').trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function isAbsoluteDisplayPath(input) {
|
||||
const value = normalizeAbsoluteDisplayPath(input);
|
||||
return value.startsWith('/') || /^[A-Za-z]:\//.test(value);
|
||||
}
|
||||
|
||||
function getWorkspaceRelativePath(input) {
|
||||
const filePath = normalizeAbsoluteDisplayPath(input);
|
||||
if (!filePath) return '';
|
||||
if (!isAbsoluteDisplayPath(filePath)) return normalizeBrowserPath(filePath);
|
||||
|
||||
const rootPath = normalizeAbsoluteDisplayPath(currentCwd);
|
||||
if (!rootPath) return '';
|
||||
if (filePath === rootPath) return '';
|
||||
if (!filePath.startsWith(`${rootPath}/`)) return '';
|
||||
return normalizeBrowserPath(filePath.slice(rootPath.length + 1));
|
||||
}
|
||||
|
||||
function parseLocalFileLinkHref(rawHref) {
|
||||
const raw = String(rawHref || '').trim();
|
||||
if (!raw || raw.startsWith('#')) return null;
|
||||
|
||||
let decoded = raw;
|
||||
try { decoded = decodeURI(raw); } catch {}
|
||||
|
||||
if (/^file:\/\//i.test(decoded)) {
|
||||
try { decoded = decodeURI(new URL(decoded).pathname || decoded); } catch {}
|
||||
} else if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(decoded) && !/^[A-Za-z]:[\\/]/.test(decoded)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let line = 0;
|
||||
let hasLineSuffix = false;
|
||||
const hashMatch = decoded.match(/#L?(\d+)(?:\b|$)/i);
|
||||
if (hashMatch) {
|
||||
line = Number(hashMatch[1]) || 0;
|
||||
decoded = decoded.slice(0, hashMatch.index);
|
||||
}
|
||||
|
||||
decoded = decoded.replace(/\\/g, '/');
|
||||
const lineMatch = decoded.match(/^(.+?):(\d+)(?::\d+)?$/);
|
||||
if (lineMatch) {
|
||||
decoded = lineMatch[1];
|
||||
line = Number(lineMatch[2]) || line;
|
||||
hasLineSuffix = true;
|
||||
}
|
||||
|
||||
const filePath = normalizeAbsoluteDisplayPath(decoded);
|
||||
if (!filePath) return null;
|
||||
if (!isAbsoluteDisplayPath(filePath) && !/^\.{1,2}\//.test(filePath) && !filePath.includes('/') && !hasLineSuffix) return null;
|
||||
|
||||
return { filePath, line };
|
||||
}
|
||||
|
||||
function formatLocalFileLinkText(link, fallbackText) {
|
||||
const rawText = String(fallbackText || '').trim();
|
||||
const baseText = rawText && rawText !== link.filePath ? rawText : (getPathLeaf(link.filePath) || link.filePath);
|
||||
if (!link.line || new RegExp(`:${link.line}$`).test(baseText)) return baseText;
|
||||
return `${baseText}:${link.line}`;
|
||||
}
|
||||
|
||||
async function fetchAuthJson(url, options = {}) {
|
||||
await ensureAuthenticatedWs();
|
||||
const response = await fetch(url, {
|
||||
@@ -3021,10 +3085,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function openFileBrowserFile(targetPath) {
|
||||
function scrollFileBrowserPreviewToLine(lineNumber) {
|
||||
const line = Number(lineNumber || 0);
|
||||
const codeEl = fileBrowserState?.previewCodeEl;
|
||||
if (!codeEl || line <= 0) return;
|
||||
requestAnimationFrame(() => {
|
||||
const styles = window.getComputedStyle(codeEl);
|
||||
const lineHeight = parseFloat(styles.lineHeight) || (parseFloat(styles.fontSize) * 1.4) || 18;
|
||||
codeEl.scrollTop = Math.max(0, (line - 4) * lineHeight);
|
||||
});
|
||||
}
|
||||
|
||||
async function openFileBrowserFile(targetPath, options = {}) {
|
||||
if (!fileBrowserState) return;
|
||||
const state = fileBrowserState;
|
||||
const normalizedPath = normalizeBrowserPath(targetPath);
|
||||
const targetLine = Math.max(0, Number(options.line || 0) || 0);
|
||||
const requestId = ++state.previewRequestId;
|
||||
state.selectedFilePath = normalizedPath;
|
||||
syncFileBrowserSelection();
|
||||
@@ -3040,11 +3116,13 @@
|
||||
const metaParts = [formatFileSize(data.size || 0)];
|
||||
if (data.updatedAt) metaParts.push(timeAgo(data.updatedAt));
|
||||
if (data.truncated) metaParts.push(`仅显示前 ${formatFileSize(data.previewBytes || 0)}`);
|
||||
if (targetLine) metaParts.push(`第 ${targetLine} 行`);
|
||||
state.previewMetaEl.textContent = metaParts.join(' · ');
|
||||
state.previewEmptyEl.hidden = true;
|
||||
state.previewCodeEl.hidden = false;
|
||||
state.previewCodeEl.textContent = data.content || '';
|
||||
setFileBrowserStatus(`已打开 ${data.name || '文件'}`);
|
||||
scrollFileBrowserPreviewToLine(targetLine);
|
||||
setFileBrowserStatus(`已打开 ${data.name || '文件'}${targetLine ? `:${targetLine}` : ''}`);
|
||||
} catch (err) {
|
||||
if (!fileBrowserState || state !== fileBrowserState || requestId !== state.previewRequestId) return;
|
||||
state.selectedFilePath = '';
|
||||
@@ -4552,7 +4630,50 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleLocalFileLinkClick(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const link = event.currentTarget;
|
||||
const filePath = link?.dataset?.filePath || '';
|
||||
const line = Number(link?.dataset?.fileLine || 0) || 0;
|
||||
const relativePath = getWorkspaceRelativePath(filePath);
|
||||
if (!relativePath) {
|
||||
showToast(currentCwd ? '文件不在当前会话工作目录内' : '当前会话没有可浏览的工作目录');
|
||||
return;
|
||||
}
|
||||
|
||||
showFileBrowser();
|
||||
if (!fileBrowserState) return;
|
||||
|
||||
loadFileBrowserDirectory(getBrowserParentPath(relativePath), { preservePreview: true });
|
||||
openFileBrowserFile(relativePath, { line });
|
||||
}
|
||||
|
||||
function hydrateLocalFileLinks(root) {
|
||||
if (!root) return;
|
||||
const links = root.matches?.('a')
|
||||
? [root]
|
||||
: Array.from(root.querySelectorAll('a'));
|
||||
|
||||
links.forEach((link) => {
|
||||
if (link.dataset.localFileHydrated === '1') return;
|
||||
const parsed = parseLocalFileLinkHref(link.getAttribute('href') || '');
|
||||
if (!parsed) return;
|
||||
|
||||
link.dataset.localFileHydrated = '1';
|
||||
link.dataset.localFileLink = 'true';
|
||||
link.dataset.filePath = parsed.filePath;
|
||||
link.dataset.fileLine = parsed.line ? String(parsed.line) : '';
|
||||
link.classList.add('local-file-link');
|
||||
link.title = parsed.line ? `${parsed.filePath}:${parsed.line}` : parsed.filePath;
|
||||
link.textContent = formatLocalFileLinkText(parsed, link.textContent);
|
||||
link.addEventListener('click', handleLocalFileLinkClick);
|
||||
});
|
||||
}
|
||||
|
||||
function hydrateRenderedMarkdown(root) {
|
||||
hydrateLocalFileLinks(root);
|
||||
hydrateMermaidBlocks(root);
|
||||
}
|
||||
|
||||
|
||||
@@ -2681,6 +2681,27 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
.msg-bubble h3 { font-size: 1.05em; }
|
||||
.msg-bubble a { color: var(--info); text-decoration: none; }
|
||||
.msg-bubble a:hover { text-decoration: underline; }
|
||||
.msg-bubble a.local-file-link {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: 0.95em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.msg-bubble a.local-file-link::before {
|
||||
content: 'file';
|
||||
margin-right: 4px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
font-family: inherit;
|
||||
font-size: 0.76em;
|
||||
line-height: 1.35;
|
||||
text-decoration: none;
|
||||
}
|
||||
.msg-bubble blockquote {
|
||||
border-left: 3px solid var(--border-color);
|
||||
padding-left: 12px;
|
||||
|
||||
@@ -565,6 +565,16 @@ function assertFrontendCcwebPromptContract() {
|
||||
assert(source.includes("className = 'ccweb-prompt-answer'"), 'Each ccweb prompt question should expose an editable answer textarea');
|
||||
}
|
||||
|
||||
function assertFrontendMarkdownLinkContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const styleSource = fs.readFileSync(path.join(REPO_DIR, 'public', 'style.css'), 'utf8');
|
||||
assert(source.includes('function parseLocalFileLinkHref(rawHref)'), 'Frontend should parse local file hrefs separately from web links');
|
||||
assert(source.includes("link.dataset.localFileLink = 'true';"), 'Frontend should mark local file links with data-local-file-link');
|
||||
assert(source.includes('hydrateLocalFileLinks(root);'), 'Rendered markdown should hydrate local file links');
|
||||
assert(source.includes('openFileBrowserFile(relativePath, { line });'), 'Local file links should open the file browser with line metadata');
|
||||
assert(styleSource.includes('.msg-bubble a.local-file-link'), 'Local file links should have a distinct message style');
|
||||
}
|
||||
|
||||
function assertFrontendMcpReloadContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
assert(source.includes('function mcpStartupStatusToastText(status)'), 'Frontend should format MCP startup status toast text');
|
||||
@@ -582,6 +592,7 @@ async function main() {
|
||||
assertFrontendGenerationControlsContract();
|
||||
assertFrontendComposerMcpContract();
|
||||
assertFrontendCcwebPromptContract();
|
||||
assertFrontendMarkdownLinkContract();
|
||||
assertMockCodexAppPromptUserNotTextTriggered();
|
||||
assertFrontendMcpReloadContract();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user