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;
|
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 = {}) {
|
async function fetchAuthJson(url, options = {}) {
|
||||||
await ensureAuthenticatedWs();
|
await ensureAuthenticatedWs();
|
||||||
const response = await fetch(url, {
|
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;
|
if (!fileBrowserState) return;
|
||||||
const state = fileBrowserState;
|
const state = fileBrowserState;
|
||||||
const normalizedPath = normalizeBrowserPath(targetPath);
|
const normalizedPath = normalizeBrowserPath(targetPath);
|
||||||
|
const targetLine = Math.max(0, Number(options.line || 0) || 0);
|
||||||
const requestId = ++state.previewRequestId;
|
const requestId = ++state.previewRequestId;
|
||||||
state.selectedFilePath = normalizedPath;
|
state.selectedFilePath = normalizedPath;
|
||||||
syncFileBrowserSelection();
|
syncFileBrowserSelection();
|
||||||
@@ -3040,11 +3116,13 @@
|
|||||||
const metaParts = [formatFileSize(data.size || 0)];
|
const metaParts = [formatFileSize(data.size || 0)];
|
||||||
if (data.updatedAt) metaParts.push(timeAgo(data.updatedAt));
|
if (data.updatedAt) metaParts.push(timeAgo(data.updatedAt));
|
||||||
if (data.truncated) metaParts.push(`仅显示前 ${formatFileSize(data.previewBytes || 0)}`);
|
if (data.truncated) metaParts.push(`仅显示前 ${formatFileSize(data.previewBytes || 0)}`);
|
||||||
|
if (targetLine) metaParts.push(`第 ${targetLine} 行`);
|
||||||
state.previewMetaEl.textContent = metaParts.join(' · ');
|
state.previewMetaEl.textContent = metaParts.join(' · ');
|
||||||
state.previewEmptyEl.hidden = true;
|
state.previewEmptyEl.hidden = true;
|
||||||
state.previewCodeEl.hidden = false;
|
state.previewCodeEl.hidden = false;
|
||||||
state.previewCodeEl.textContent = data.content || '';
|
state.previewCodeEl.textContent = data.content || '';
|
||||||
setFileBrowserStatus(`已打开 ${data.name || '文件'}`);
|
scrollFileBrowserPreviewToLine(targetLine);
|
||||||
|
setFileBrowserStatus(`已打开 ${data.name || '文件'}${targetLine ? `:${targetLine}` : ''}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!fileBrowserState || state !== fileBrowserState || requestId !== state.previewRequestId) return;
|
if (!fileBrowserState || state !== fileBrowserState || requestId !== state.previewRequestId) return;
|
||||||
state.selectedFilePath = '';
|
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) {
|
function hydrateRenderedMarkdown(root) {
|
||||||
|
hydrateLocalFileLinks(root);
|
||||||
hydrateMermaidBlocks(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 h3 { font-size: 1.05em; }
|
||||||
.msg-bubble a { color: var(--info); text-decoration: none; }
|
.msg-bubble a { color: var(--info); text-decoration: none; }
|
||||||
.msg-bubble a:hover { text-decoration: underline; }
|
.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 {
|
.msg-bubble blockquote {
|
||||||
border-left: 3px solid var(--border-color);
|
border-left: 3px solid var(--border-color);
|
||||||
padding-left: 12px;
|
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');
|
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() {
|
function assertFrontendMcpReloadContract() {
|
||||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||||
assert(source.includes('function mcpStartupStatusToastText(status)'), 'Frontend should format MCP startup status toast text');
|
assert(source.includes('function mcpStartupStatusToastText(status)'), 'Frontend should format MCP startup status toast text');
|
||||||
@@ -582,6 +592,7 @@ async function main() {
|
|||||||
assertFrontendGenerationControlsContract();
|
assertFrontendGenerationControlsContract();
|
||||||
assertFrontendComposerMcpContract();
|
assertFrontendComposerMcpContract();
|
||||||
assertFrontendCcwebPromptContract();
|
assertFrontendCcwebPromptContract();
|
||||||
|
assertFrontendMarkdownLinkContract();
|
||||||
assertMockCodexAppPromptUserNotTextTriggered();
|
assertMockCodexAppPromptUserNotTextTriggered();
|
||||||
assertFrontendMcpReloadContract();
|
assertFrontendMcpReloadContract();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user