优化目录候选文件名显示并重新打包
This commit is contained in:
Binary file not shown.
@@ -11556,6 +11556,30 @@
|
||||
};
|
||||
}
|
||||
|
||||
function getComposerFileSuggestionDisplay(item) {
|
||||
const rawName = String(item?.name || item?.label || item?.insertion || '').trim();
|
||||
const normalizedName = rawName
|
||||
.replace(/^@/, '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\/+$/, '');
|
||||
const segments = normalizedName.split('/').filter(Boolean);
|
||||
const basename = segments.pop() || normalizedName || rawName;
|
||||
const parentPath = segments.join('/');
|
||||
const isDirectory = item?.itemType === 'directory';
|
||||
const primaryText = `${basename}${isDirectory ? '/' : ''}`;
|
||||
const parentLabel = parentPath ? `@${parentPath}/` : '';
|
||||
const fallbackDescription = String(item?.description || '').trim();
|
||||
const secondaryText = [parentLabel, parentLabel ? '' : fallbackDescription]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
|| fallbackDescription;
|
||||
return {
|
||||
primaryText,
|
||||
secondaryText,
|
||||
fullPath: String(item?.label || item?.insertion || rawName).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function showCmdMenu(token, items) {
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
if (!token || safeItems.length === 0) {
|
||||
@@ -11565,6 +11589,8 @@
|
||||
activeComposerToken = token;
|
||||
cmdMenuIndex = 0;
|
||||
cmdMenu.innerHTML = safeItems.map((item, i) => {
|
||||
const isFileSuggestion = item.kind === 'file';
|
||||
const fileDisplay = isFileSuggestion ? getComposerFileSuggestionDisplay(item) : null;
|
||||
const kindLabel = item.kind === 'skill'
|
||||
? 'Skill'
|
||||
: item.kind === 'prompt'
|
||||
@@ -11574,11 +11600,14 @@
|
||||
: item.kind === 'mcp'
|
||||
? 'MCP'
|
||||
: 'Cmd';
|
||||
return `<div class="cmd-item${i === 0 ? ' active' : ''}" data-index="${i}">
|
||||
const primaryText = fileDisplay?.primaryText || item.label || item.name || item.insertion || '';
|
||||
const secondaryText = fileDisplay?.secondaryText || item.description || item.title || '';
|
||||
const fullPathTitle = fileDisplay?.fullPath ? ` title="${escapeHtmlAttr(fileDisplay.fullPath)}"` : '';
|
||||
return `<div class="cmd-item${i === 0 ? ' active' : ''}${isFileSuggestion ? ' file-suggestion' : ''}" data-index="${i}"${fullPathTitle}>
|
||||
<span class="cmd-item-kind">${kindLabel}</span>
|
||||
<span class="cmd-item-main">
|
||||
<span class="cmd-item-cmd">${escapeHtml(item.label || item.name || item.insertion || '')}</span>
|
||||
<span class="cmd-item-desc">${escapeHtml(item.description || item.title || '')}</span>
|
||||
<span class="cmd-item-cmd">${escapeHtml(primaryText)}</span>
|
||||
<span class="cmd-item-desc">${escapeHtml(secondaryText)}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
document.documentElement.dataset.dividerTime = dividerTime;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="style.css?v=20260818-goal-mode-label">
|
||||
<link rel="stylesheet" href="style.css?v=20260911-file-suggestion">
|
||||
<link rel="stylesheet" href="task-board.css?v=__CC_WEB_FRONTEND_ASSET_VERSION__">
|
||||
<link rel="stylesheet" href="gitea-workflow.css?v=__CC_WEB_GITEA_WORKFLOW_ASSET_VERSION__">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||
|
||||
@@ -4425,8 +4425,9 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(45, 31, 20, 0.12);
|
||||
padding: 6px;
|
||||
min-width: 240px;
|
||||
max-width: 320px;
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
min-width: min(240px, calc(100vw - 32px));
|
||||
max-width: none;
|
||||
max-height: min(52vh, 360px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
@@ -4486,6 +4487,23 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cmd-item.file-suggestion .cmd-item-cmd {
|
||||
color: var(--text-primary);
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.cmd-item.file-suggestion .cmd-item-desc {
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.35;
|
||||
max-height: 2.7em;
|
||||
}
|
||||
|
||||
/* === Input Area === */
|
||||
.input-area {
|
||||
@@ -4932,7 +4950,7 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
gap: 10px;
|
||||
}
|
||||
.session-item-actions { display: flex; }
|
||||
.cmd-menu { left: 10px; right: 10px; transform: none; min-width: auto; bottom: 72px; }
|
||||
.cmd-menu { left: 10px; right: 10px; transform: none; width: auto; min-width: auto; bottom: 72px; }
|
||||
.option-picker { left: 10px; right: 10px; transform: none; min-width: auto; bottom: 72px; }
|
||||
.chat-header {
|
||||
padding: 0 10px;
|
||||
|
||||
234
scripts/composer-file-display-browser.js
Normal file
234
scripts/composer-file-display-browser.js
Normal file
@@ -0,0 +1,234 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const net = require('node:net');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawn } = require('node:child_process');
|
||||
|
||||
const REPO_DIR = path.resolve(__dirname, '..');
|
||||
const STYLE_PATH = path.join(REPO_DIR, 'public', 'style.css');
|
||||
const GECKODRIVER = '/snap/firefox/current/usr/lib/firefox/geckodriver';
|
||||
const FIREFOX = '/snap/firefox/current/usr/lib/firefox/firefox';
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function freePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
server.close(() => resolve(address.port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForPort(port, timeoutMs = 10_000) {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const ready = await new Promise((resolve) => {
|
||||
const socket = net.createConnection({ host: '127.0.0.1', port });
|
||||
socket.once('connect', () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.once('error', () => resolve(false));
|
||||
});
|
||||
if (ready) return;
|
||||
await sleep(50);
|
||||
}
|
||||
throw new Error(`等待 geckodriver 端口 ${port} 超时`);
|
||||
}
|
||||
|
||||
async function stopChild(child) {
|
||||
if (!child || child.exitCode !== null) return;
|
||||
child.kill('SIGTERM');
|
||||
await Promise.race([
|
||||
new Promise((resolve) => child.once('exit', resolve)),
|
||||
sleep(1_000),
|
||||
]);
|
||||
if (child.exitCode === null) child.kill('SIGKILL');
|
||||
}
|
||||
|
||||
class FirefoxDriver {
|
||||
constructor(port) {
|
||||
this.port = port;
|
||||
this.sessionId = '';
|
||||
}
|
||||
|
||||
async request(method, endpoint, body) {
|
||||
const response = await fetch(`http://127.0.0.1:${this.port}${endpoint}`, {
|
||||
method,
|
||||
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok || payload?.value?.error) {
|
||||
throw new Error(`WebDriver ${method} ${endpoint}: ${JSON.stringify(payload.value || payload)}`);
|
||||
}
|
||||
return payload.value;
|
||||
}
|
||||
|
||||
async start() {
|
||||
const value = await this.request('POST', '/session', {
|
||||
capabilities: {
|
||||
alwaysMatch: {
|
||||
browserName: 'firefox',
|
||||
'moz:firefoxOptions': {
|
||||
binary: FIREFOX,
|
||||
args: ['-headless'],
|
||||
prefs: {
|
||||
'browser.shell.checkDefaultBrowser': false,
|
||||
'browser.startup.homepage_override.mstone': 'ignore',
|
||||
'media.prefers-reduced-motion': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
this.sessionId = value.sessionId;
|
||||
}
|
||||
|
||||
async execute(script, args = []) {
|
||||
return this.request('POST', `/session/${this.sessionId}/execute/sync`, { script, args });
|
||||
}
|
||||
|
||||
async navigate(url) {
|
||||
return this.request('POST', `/session/${this.sessionId}/url`, { url });
|
||||
}
|
||||
|
||||
async setWindow(width, height) {
|
||||
await this.request('POST', `/session/${this.sessionId}/window/rect`, { width, height });
|
||||
return this.request('GET', `/session/${this.sessionId}/window/rect`);
|
||||
}
|
||||
|
||||
async screenshot(targetPath) {
|
||||
const base64 = await this.request('GET', `/session/${this.sessionId}/screenshot`);
|
||||
fs.writeFileSync(targetPath, Buffer.from(base64, 'base64'));
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.sessionId) return;
|
||||
try {
|
||||
await this.request('DELETE', `/session/${this.sessionId}`);
|
||||
} finally {
|
||||
this.sessionId = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fixtureHtml() {
|
||||
const style = fs.readFileSync(STYLE_PATH, 'utf8');
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>${style}</style>
|
||||
<style>
|
||||
:root { --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
html, body { min-height: 100%; }
|
||||
body { margin: 0; padding: 24px; background: var(--bg-primary); color: var(--text-primary); }
|
||||
.fixture-stage { min-height: 460px; position: relative; }
|
||||
#cmd-menu { position: relative; inset: auto; transform: none; display: block; margin: 0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="fixture-stage">
|
||||
<div id="cmd-menu" class="cmd-menu">
|
||||
<div class="cmd-item file-suggestion active" data-index="0" title="@01_设计书说明文档/wayfinder/README.md">
|
||||
<span class="cmd-item-kind">File</span>
|
||||
<span class="cmd-item-main">
|
||||
<span class="cmd-item-cmd">README.md</span>
|
||||
<span class="cmd-item-desc">@01_设计书说明文档/wayfinder/</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="cmd-item file-suggestion" data-index="1" title="@01_设计书说明文档/wayfinder/hooks/">
|
||||
<span class="cmd-item-kind">Dir</span>
|
||||
<span class="cmd-item-main">
|
||||
<span class="cmd-item-cmd">hooks/</span>
|
||||
<span class="cmd-item-desc">@01_设计书说明文档/wayfinder/</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
assert(fs.existsSync(GECKODRIVER), `缺少 geckodriver: ${GECKODRIVER}`);
|
||||
assert(fs.existsSync(FIREFOX), `缺少 Firefox: ${FIREFOX}`);
|
||||
|
||||
const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-composer-file-browser-'));
|
||||
const evidenceRoot = path.join(runtimeRoot, 'evidence');
|
||||
fs.mkdirSync(evidenceRoot, { recursive: true });
|
||||
const fixturePath = path.join(runtimeRoot, 'fixture.html');
|
||||
fs.writeFileSync(fixturePath, fixtureHtml());
|
||||
const driverPort = await freePort();
|
||||
const geckodriver = spawn(GECKODRIVER, ['--port', String(driverPort), '-b', FIREFOX], {
|
||||
cwd: runtimeRoot,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let driverStderr = '';
|
||||
geckodriver.stderr.on('data', (chunk) => { driverStderr += chunk.toString(); });
|
||||
const driver = new FirefoxDriver(driverPort);
|
||||
const evidence = { screenshots: {}, audits: {} };
|
||||
|
||||
try {
|
||||
await waitForPort(driverPort);
|
||||
await driver.start();
|
||||
await driver.navigate(`file://${fixturePath}`);
|
||||
for (const viewport of [{ name: 'desktop', width: 1440, height: 900 }, { name: 'narrow', width: 375, height: 667 }]) {
|
||||
const rect = await driver.setWindow(viewport.width, viewport.height);
|
||||
const audit = await driver.execute(`
|
||||
const menu = document.querySelector('#cmd-menu');
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const items = Array.from(menu.querySelectorAll('.file-suggestion')).map((item) => {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const main = item.querySelector('.cmd-item-main');
|
||||
return {
|
||||
primary: item.querySelector('.cmd-item-cmd')?.textContent.trim() || '',
|
||||
secondary: item.querySelector('.cmd-item-desc')?.textContent.trim() || '',
|
||||
right: itemRect.right,
|
||||
fullyInsideMenu: itemRect.left >= menuRect.left && itemRect.right <= menuRect.right,
|
||||
mainOverflow: main ? main.scrollWidth > main.clientWidth + 1 : false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
viewport: { width: innerWidth, height: innerHeight },
|
||||
window: arguments[0],
|
||||
menu: { left: menuRect.left, right: menuRect.right, width: menuRect.width, scrollWidth: menu.scrollWidth },
|
||||
items,
|
||||
};
|
||||
`, [rect]);
|
||||
assert(audit.menu.right <= audit.viewport.width + 1, `${viewport.name} 菜单超出视口:${JSON.stringify(audit)}`);
|
||||
assert(audit.menu.scrollWidth <= audit.menu.width + 1, `${viewport.name} 菜单出现横向溢出:${JSON.stringify(audit)}`);
|
||||
assert.deepEqual(audit.items.map((item) => item.primary), ['README.md', 'hooks/']);
|
||||
assert(audit.items.every((item) => item.secondary.includes('@01_设计书说明文档/wayfinder/')), `${viewport.name} 父路径不可见`);
|
||||
assert(audit.items.every((item) => item.fullyInsideMenu && !item.mainOverflow), `${viewport.name} 候选内容溢出`);
|
||||
evidence.audits[viewport.name] = audit;
|
||||
const screenshotPath = path.join(evidenceRoot, `${viewport.name}-1440x900-or-375x667.png`);
|
||||
await driver.screenshot(screenshotPath);
|
||||
evidence.screenshots[viewport.name] = screenshotPath;
|
||||
}
|
||||
fs.writeFileSync(path.join(evidenceRoot, 'evidence.json'), JSON.stringify(evidence, null, 2));
|
||||
console.log(JSON.stringify({ ok: true, evidenceRoot, ...evidence }, null, 2));
|
||||
} catch (error) {
|
||||
error.message += `\ngeckodriver stderr:\n${driverStderr.slice(-4_000)}`;
|
||||
throw error;
|
||||
} finally {
|
||||
try { await driver.close(); } catch {}
|
||||
await stopChild(geckodriver);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message || String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -671,7 +671,7 @@ function assertFrontendSidebarCollapseContract() {
|
||||
'Rich themes should provide isolated rail treatments on top of the shared semantic fallback'
|
||||
);
|
||||
assert(
|
||||
indexSource.includes('style.css?v=20260818-goal-mode-label')
|
||||
indexSource.includes('style.css?v=20260911-file-suggestion')
|
||||
&& indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'),
|
||||
'Sidebar interaction assets should share the reviewed cache-busting version'
|
||||
);
|
||||
@@ -900,6 +900,42 @@ function assertFrontendComposerMcpContract() {
|
||||
assert(menuStart >= 0 && menuEnd > menuStart, 'Frontend should define showCmdMenu before requestComposerSuggestions');
|
||||
const menuBlock = source.slice(menuStart, menuEnd);
|
||||
assert(/item\.kind\s*===\s*'mcp'/.test(menuBlock) && menuBlock.includes("'MCP'"), 'Composer menu should render MCP item labels');
|
||||
assert(
|
||||
source.includes('function getComposerFileSuggestionDisplay(item)')
|
||||
&& menuBlock.includes('getComposerFileSuggestionDisplay(item)')
|
||||
&& menuBlock.includes("' file-suggestion'")
|
||||
&& menuBlock.includes('escapeHtmlAttr(fileDisplay.fullPath)'),
|
||||
'Composer file suggestions should show the basename first while retaining the full path as a tooltip'
|
||||
);
|
||||
const fileDisplaySource = extractFunctionSource(source, 'getComposerFileSuggestionDisplay');
|
||||
const fileDisplayApi = new Function(`${fileDisplaySource}; return getComposerFileSuggestionDisplay;`)();
|
||||
const nestedFileDisplay = fileDisplayApi({
|
||||
kind: 'file',
|
||||
itemType: 'file',
|
||||
name: '01_设计书说明文档/wayfinder/README.md',
|
||||
label: '@01_设计书说明文档/wayfinder/README.md',
|
||||
insertion: '@01_设计书说明文档/wayfinder/README.md',
|
||||
});
|
||||
assert(nestedFileDisplay.primaryText === 'README.md', 'Nested file suggestions should keep the final filename as the primary text');
|
||||
assert(nestedFileDisplay.secondaryText === '@01_设计书说明文档/wayfinder/', 'Nested file suggestions should show the parent path as secondary text');
|
||||
assert(nestedFileDisplay.fullPath === '@01_设计书说明文档/wayfinder/README.md', 'Nested file suggestions should retain the complete insertion path');
|
||||
const nestedDirectoryDisplay = fileDisplayApi({
|
||||
kind: 'file',
|
||||
itemType: 'directory',
|
||||
name: '01_设计书说明文档/wayfinder/hooks',
|
||||
label: '@01_设计书说明文档/wayfinder/hooks/',
|
||||
insertion: '@01_设计书说明文档/wayfinder/hooks/',
|
||||
});
|
||||
assert(nestedDirectoryDisplay.primaryText === 'hooks/', 'Nested directory suggestions should keep the final directory name and slash visible');
|
||||
assert(nestedDirectoryDisplay.secondaryText === '@01_设计书说明文档/wayfinder/', 'Nested directory suggestions should show the parent path as secondary text');
|
||||
const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
|
||||
assert(
|
||||
/\.cmd-menu\s*\{[\s\S]*?width:\s*min\(560px,\s*calc\(100vw\s*-\s*32px\)\)/.test(styleSource)
|
||||
&& styleSource.includes('.cmd-item.file-suggestion .cmd-item-cmd')
|
||||
&& styleSource.includes('.cmd-item.file-suggestion .cmd-item-desc')
|
||||
&& /\.cmd-menu\s*\{[^}]*left:\s*10px;[^}]*right:\s*10px;[^}]*width:\s*auto;/.test(styleSource),
|
||||
'Composer menu should reserve enough width and style file path hierarchy'
|
||||
);
|
||||
const selectStart = source.indexOf('function selectComposerItemByIndex(index)');
|
||||
const selectEnd = source.indexOf('\n function selectCmdMenuItem()', selectStart);
|
||||
assert(selectStart >= 0 && selectEnd > selectStart, 'Frontend should define selectComposerItemByIndex before selectCmdMenuItem');
|
||||
@@ -1154,7 +1190,7 @@ function assertPlanListProgressContract() {
|
||||
assert(extractorSource.includes('references/source-assets/wasteland-icon-sheet.webp'), 'Plan progress extractor should read the archived source sheet');
|
||||
assert(!extractorSource.includes('sessions/_attachments'), 'Plan progress extractor should not depend on temporary session attachments');
|
||||
|
||||
assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Plan progress CSS should use the current cache-busted URL');
|
||||
assert(indexSource.includes('style.css?v=20260911-file-suggestion'), 'Plan progress CSS should use the current cache-busted URL');
|
||||
assert(indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'), 'Plan progress frontend logic should use the dynamic cache-busted URL');
|
||||
}
|
||||
|
||||
@@ -1270,7 +1306,7 @@ function assertFrontendGildedThemeContract() {
|
||||
assert(contrast('#655446', '#fff7ea') >= 4.5, 'Gilded muted text should remain readable on ivory panels');
|
||||
assert(contrast('#fff7ea', '#7a3f20') >= 7, 'Gilded primary action text should reach AAA contrast on copper');
|
||||
assert(themeStyle.includes('@media (prefers-reduced-motion: reduce)'), 'Gilded theme motion should respect reduced-motion preferences');
|
||||
assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
|
||||
assert(indexSource.includes('style.css?v=20260911-file-suggestion'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
|
||||
assert(indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'), 'Theme bundle app script should use the dynamic cache-busted asset URL');
|
||||
}
|
||||
|
||||
@@ -1524,7 +1560,7 @@ function assertFrontendWastelandThemeContract() {
|
||||
assert(contrast('#c9bda6', backgroundColor) >= 4.5, `Wasteland muted text should reach AA contrast on ${backgroundColor}`);
|
||||
});
|
||||
|
||||
assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
|
||||
assert(indexSource.includes('style.css?v=20260911-file-suggestion'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
|
||||
assert(indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'), 'Wasteland registration should share the dynamic cache-busted theme bundle URL');
|
||||
}
|
||||
|
||||
@@ -2554,7 +2590,7 @@ function assertGoalModeTitleContract() {
|
||||
assert(styleSource.includes('.msg.user.goal-message .msg-bubble'), 'Goal label styling should remain scoped to user Goal bubbles');
|
||||
assert(styleSource.includes('.goal-message-label'), 'Goal mode label should have a dedicated style');
|
||||
assert(styleSource.includes("html[data-theme='wasteland'] .msg.user.goal-message .goal-message-label"), 'Wasteland should provide a dedicated Goal label treatment');
|
||||
assert(indexSource.includes('style.css?v=20260818-goal-mode-label'), 'Goal label CSS should use the current cache-busted URL');
|
||||
assert(indexSource.includes('style.css?v=20260911-file-suggestion'), 'Goal label CSS should use the current cache-busted URL');
|
||||
assert(serverSource.includes('GOAL_DERIVED_TITLE_MAX_CHARS = 60'), 'Goal-derived titles should keep the 60-character limit');
|
||||
assert(serverSource.includes('function isDefaultConversationTitle(session)'), 'Goal title updates should use a default-title guard');
|
||||
assert(serverSource.includes('function applyDerivedGoalConversationTitle(session, objective)'), 'Goal title updates should stay in the /goal-specific server path');
|
||||
@@ -5568,7 +5604,7 @@ function assertAdvancedSessionSearchContract() {
|
||||
'Existing sidebar search clear control contract should remain unchanged');
|
||||
assert(indexSource.includes('id="advanced-search-panel"') && indexSource.includes('id="advanced-search-results"'),
|
||||
'Advanced search workspace should expose stable panel and result hooks');
|
||||
assert(indexSource.includes('style.css?v=20260818-goal-mode-label')
|
||||
assert(indexSource.includes('style.css?v=20260911-file-suggestion')
|
||||
&& indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'),
|
||||
'Advanced search CSS and frontend script should share the reviewed cache-bust');
|
||||
|
||||
@@ -5685,7 +5721,7 @@ function assertUsageStatisticsContract() {
|
||||
'Usage dashboard should be a chat-main-local workspace');
|
||||
assert(indexSource.includes('class="usage-dashboard-open"') && !indexSource.includes('class="settings-btn usage-dashboard-open"'),
|
||||
'Usage entry must use its own class so theme-specific settings pseudo-elements cannot leak');
|
||||
assert(indexSource.includes('style.css?v=20260818-goal-mode-label')
|
||||
assert(indexSource.includes('style.css?v=20260911-file-suggestion')
|
||||
&& indexSource.includes('vendor/echarts.min.js?v=5.6.0')
|
||||
&& indexSource.includes('app.js?v=__CC_WEB_FRONTEND_ASSET_VERSION__'),
|
||||
'Usage dashboard CSS, local ECharts runtime, and frontend script should share the current asset contract');
|
||||
@@ -6433,6 +6469,11 @@ async function main() {
|
||||
console.log('Composer slash routing regression checks passed.');
|
||||
return;
|
||||
}
|
||||
if (regressionTarget === 'composer-file-display') {
|
||||
assertFrontendComposerMcpContract();
|
||||
console.log('Composer file display regression checks passed.');
|
||||
return;
|
||||
}
|
||||
if (regressionTarget === 'codexapp-unrouted-routing') {
|
||||
assertCodexAppUnroutedNotificationRoutingContract();
|
||||
console.log('Codex App unrouted routing regression checks passed.');
|
||||
@@ -6847,6 +6888,12 @@ async function main() {
|
||||
'command = "definitely-missing-mcp-command"',
|
||||
].join('\n'));
|
||||
fs.writeFileSync(path.join(codexInitCwd, 'context.txt'), 'Composer file context body.');
|
||||
const nestedComposerRoot = path.join(codexInitCwd, '01_设计书说明文档', 'wayfinder');
|
||||
const nestedComposerHooks = path.join(nestedComposerRoot, 'hooks', 'deep');
|
||||
mkdirp(nestedComposerHooks);
|
||||
fs.writeFileSync(path.join(nestedComposerRoot, 'README.md'), 'Nested composer file context body.');
|
||||
fs.writeFileSync(path.join(nestedComposerRoot, 'hooks', 'index.md'), 'Nested composer directory file.');
|
||||
fs.writeFileSync(path.join(nestedComposerHooks, 'notes.txt'), 'Three-level composer file context body.');
|
||||
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: codexInitCwd, mode: 'plan' }));
|
||||
const codexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === codexInitCwd);
|
||||
assert(codexSession.mode === 'plan', 'Codex new_session should follow requested mode');
|
||||
@@ -6964,6 +7011,35 @@ async function main() {
|
||||
assert(fileComposer.items.some((item) => item.kind === 'file' && item.name === 'context.txt'), 'Composer file suggestions should include cwd file');
|
||||
assert(!fileComposer.items.some((item) => item.kind === 'mcp'), 'Composer file suggestions should not include MCP tools');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'composer_suggestions',
|
||||
requestId: 'reg-file-nested',
|
||||
trigger: '@',
|
||||
query: '01_设计书说明文档/wayfinder/',
|
||||
sessionId: codexSession.sessionId,
|
||||
agent: 'codex',
|
||||
}));
|
||||
const nestedFileComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-file-nested');
|
||||
const nestedFileSuggestion = nestedFileComposer.items.find((item) => item.kind === 'file' && item.name === '01_设计书说明文档/wayfinder/README.md');
|
||||
const nestedDirectorySuggestion = nestedFileComposer.items.find((item) => item.kind === 'file' && item.itemType === 'directory' && item.name === '01_设计书说明文档/wayfinder/hooks');
|
||||
assert(nestedFileSuggestion, 'Composer file suggestions should include a two-level nested file');
|
||||
assert(nestedDirectorySuggestion, 'Composer file suggestions should include a two-level nested directory');
|
||||
assert(nestedFileSuggestion.label === '@01_设计书说明文档/wayfinder/README.md' && nestedFileSuggestion.insertion === nestedFileSuggestion.label, 'Nested file suggestions should retain the complete insertion path');
|
||||
assert(nestedDirectorySuggestion.label === '@01_设计书说明文档/wayfinder/hooks/' && nestedDirectorySuggestion.insertion === nestedDirectorySuggestion.label && nestedDirectorySuggestion.appendSpace === false, 'Nested directory suggestions should retain the trailing slash and continue completion');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'composer_suggestions',
|
||||
requestId: 'reg-file-deep',
|
||||
trigger: '@',
|
||||
query: '01_设计书说明文档/wayfinder/hooks/deep/',
|
||||
sessionId: codexSession.sessionId,
|
||||
agent: 'codex',
|
||||
}));
|
||||
const deepFileComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-file-deep');
|
||||
const deepFileSuggestion = deepFileComposer.items.find((item) => item.kind === 'file' && item.name === '01_设计书说明文档/wayfinder/hooks/deep/notes.txt');
|
||||
assert(deepFileSuggestion, 'Composer file suggestions should include a three-level nested file');
|
||||
assert(deepFileSuggestion.label === '@01_设计书说明文档/wayfinder/hooks/deep/notes.txt' && deepFileSuggestion.insertion === deepFileSuggestion.label, 'Three-level file suggestions should retain the complete insertion path');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
text: '@shipit @quick-note @context.txt $regression-skill $project-skill run composer regression',
|
||||
|
||||
Reference in New Issue
Block a user