Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,6 @@ const DYNAMIC_STYLE_HOOKS = new Set([
'maka-sidebar-row',
'maka-sidebar-section',
'maka-titlebar',
'maka-tool-error-body',
'maka-tool-error-icon',
'maka-tool-error-title',
'scrollbar-hide',
'scrollbar-hover',
'shadow-medium',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -553,22 +553,39 @@ describe('tool error copy feedback contract', () => {
});

it('styles tool-error copy pending and failure states', async () => {
const src = await readRendererContractCss();
// `.maka-tool-error-copy[…]` retired onto the `@maka/ui` Alert primitive
// (issue #332 PR3c): the pending / copy-feedback chrome — which lived UNLAYERED
// in tool-output.css so it out-ranked the ghost button — now lives as literal
// arbitrary utilities inlined on the copy button's `className`. We slice the
// `ToolErrorBanner` block and require the utilities to appear on an actual
// `className="maka-button …"` so the assertion proves BOTH that the state
// utilities exist AND that the banner's copy button wears them — a whole-file
// scan would false-pass if the string drifted to another component. These are
// arbitrary-value utilities (source == computed), so this source contract is the
// proof; the computed-style harness only re-diffs the non-trivial container box.
const componentsPath = resolve(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'components.tsx');
const src = await readFile(componentsPath, 'utf8');
const block = src.match(/function ToolErrorBanner[\s\S]*?export function OverlayHost/)?.[0] ?? '';

assert.match(
src,
/\.maka-tool-error-copy\[data-pending="true"\]\s*\{[\s\S]*cursor:\s*progress;/,
block,
/className="maka-button \[align-self:start\] data-\[pending=true\]:cursor-progress/,
'The tool-error copy button must wear the leaf state utilities inline on its className.',
);
assert.match(
block,
/data-\[pending=true\]:cursor-progress/,
'Tool-error pending copy should visibly indicate in-progress work.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="copied"\]/,
'Tool-error copied state should have a stable CSS selector.',
block,
/data-\[copy-feedback=copied\]:text-\[color:var\(--accent\)\] data-\[copy-feedback=copied\]:border-\[oklch\(from_var\(--accent\)_l_c_h_\/_0\.35\)\]/,
'Tool-error copied state should have a stable color + border styling hook.',
);
assert.match(
src,
/\.maka-tool-error-copy\[data-copy-feedback="failed"\]/,
'Tool-error failed copy state should have a stable CSS selector.',
block,
/data-\[copy-feedback=failed\]:text-\[color:var\(--destructive\)\] data-\[copy-feedback=failed\]:border-\[oklch\(from_var\(--destructive\)_l_c_h_\/_0\.35\)\]/,
'Tool-error failed copy state should have a stable color + border styling hook.',
);
});
});
Expand Down
49 changes: 0 additions & 49 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1496,55 +1496,6 @@
50% { opacity: 1; transform: scale(1.1); }
}

/* Inline failure banner shown at the top of an errored tool body so the
* user sees what failed without expanding the result blob. The wrapper
* already paints a red border via [data-status="errored"]; this banner
* gives the failure a heading + copy-button affordance. */
.maka-tool-error {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
padding: 10px 12px;
margin: 0 0 10px;
border: 1px solid oklch(from var(--destructive) l c h / 0.30);
border-radius: 10px;
background: oklch(from var(--destructive) l c h / 0.08);
}
.maka-tool-error-icon {
display: grid;
place-items: center;
color: var(--destructive);
margin-top: 1px;
}
.maka-tool-error-body {
display: grid;
gap: 4px;
min-width: 0;
}
.maka-tool-error-title {
color: var(--destructive);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}
.maka-tool-error-text {
margin: 0;
color: var(--foreground-80);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.maka-tool-error-copy {
align-self: start;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
}

/* ---- Composer --------------------------------------------------- */

.maka-composer {
Expand Down
14 changes: 0 additions & 14 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -334,20 +334,6 @@
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-tool-error-copy[data-pending="true"] {
cursor: progress;
}

.maka-tool-error-copy[data-copy-feedback="copied"] {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 0.35);
}

.maka-tool-error-copy[data-copy-feedback="failed"] {
color: var(--destructive);
border-color: oklch(from var(--destructive) l c h / 0.35);
}

.maka-office-document-preview {
display: grid;
gap: 8px;
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6325,6 +6325,9 @@ function ToolOutputStream(props: {
);
}

// Preserve the retired `.maka-tool-error*` leaf utilities onto Alert (#332 PR3c) —
// Alert owns the shell; these are the few declarations it doesn't set, kept arbitrary
// so they map 1:1 to the old CSS (`[align-self:start]`, not Tailwind's `flex-start`).
function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
// Tool stderr / raw provider errors occasionally slip credential paths,
// bearer tokens, or API keys through main-side redaction. Apply a
Expand All@@ -6348,11 +6351,11 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
}

return (
<Alert variant="error" className="maka-tool-error">
<Alert variant="error" className="mb-[10px]">
<AlertOctagon size={16} strokeWidth={2} aria-hidden="true" />
<AlertTitle>工具调用失败</AlertTitle>
{errorText && (
<AlertDescription className="maka-tool-error-text">
<AlertDescription className="[font-family:var(--font-mono)] text-[12px] leading-[1.5] whitespace-pre-wrap [word-break:break-word]">
{errorText.length > 240 ? `${errorText.slice(0, 240)}…` : errorText}
</AlertDescription>
)}
Expand All@@ -6362,7 +6365,7 @@ function ToolErrorBanner(props: { result: ToolActivityItem['result'] }) {
type="button"
variant="ghost"
size="sm"
className="maka-button maka-tool-error-copy"
className="maka-button [align-self:start] data-[pending=true]:cursor-progress data-[copy-feedback=copied]:text-[color:var(--accent)] data-[copy-feedback=copied]:border-[oklch(from_var(--accent)_l_c_h_/_0.35)] data-[copy-feedback=failed]:text-[color:var(--destructive)] data-[copy-feedback=failed]:border-[oklch(from_var(--destructive)_l_c_h_/_0.35)]"
data-pending={copyPending ? 'true' : undefined}
data-copy-feedback={copyPhase ?? undefined}
aria-label={`${copyLabel}错误信息`}
Expand Down
40 changes: 39 additions & 1 deletion scripts/check-chat-marker-computed-style.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
const REPO_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
const { buttonVariants, cn } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/ui.js')).href);
const { markerVariants, streamVariants, toolVariants } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/chat.js')).href);
const { Alert, AlertTitle, AlertDescription, AlertAction } = await import(pathToFileURL(resolve(REPO_ROOT, 'packages/ui/dist/primitives/alert.js')).href);

const mainCssPath = process.argv[2] && resolve(process.argv[2]);
const headCssPath = process.argv[3] && resolve(process.argv[3]);
Expand DownExpand Up@@ -206,6 +207,36 @@ const toolCardSection = (el) => {
+ STAT.map(card).join('\n'));
};

// PR3c — the tool-error banner CONTAINER. The ONE thing this harness uniquely proves
// is that the retired `.maka-tool-error` CONTAINER declarations were INERT:
// `.maka-tool-error*` sat in `@layer components` while Alert's slot utilities sit in
// `@layer utilities` (which win regardless of specificity), so its bespoke 18px grid /
// 10px radius / padding / border / background never rendered. The main side (real Alert
// error class + `.maka-tool-error`) and the head side (same Alert class + `mb-[10px]`)
// must therefore compute identically; a wrongly-surviving `.maka-tool-error`
// declaration would surface as a real DIFF here, not a false green. The Alert slot
// classes come from CALLING the real primitive components — single source of truth, no
// hand-copy, no production-API change (a function component is a plain function, so
// `.props.className` is exactly what it renders). The description / copy-button LEAF
// utilities are NOT re-diffed here: they are arbitrary-value Tailwind (source ==
// computed by construction) pinned by visible-copy-hygiene-contract instead.
const ALERT_ERR = Alert({ variant: 'error' }).props.className;
const ALERT_DESC = AlertDescription({}).props.className;
const ALERT_ACTION = AlertAction({}).props.className;
const ALERT_TITLE = AlertTitle({}).props.className;
const errorBanner = (el) => {
const cont = pair(cn(ALERT_ERR, 'maka-tool-error'), cn(ALERT_ERR, 'mb-[10px]'));
// The svg + title / description / action slot children carry their real Alert slot
// classes only so Alert's `has-[>svg]:has-data-[slot=alert-action]` 3-col grid
// resolves on the container exactly as in production; only `err-banner` is diffed.
return el('div', 'err-banner', cont, 'data-slot="alert" role="alert"',
'<svg width="16" height="16" aria-hidden="true"></svg>'
+ el('div', 'err-title', pair(ALERT_TITLE, ALERT_TITLE), 'data-slot="alert-title"', '工具调用失败')
+ el('div', 'err-text', pair(ALERT_DESC, ALERT_DESC), 'data-slot="alert-description"', 'boom: command not found')
+ el('div', 'err-action', pair(ALERT_ACTION, ALERT_ACTION), 'data-slot="alert-action"',
el('button', 'err-copy', pair(bv('ghost', 'sm'), bv('ghost', 'sm')), 'type="button"', '<svg width="11" height="11"></svg><span>复制</span>')));
};

// DOM tree mirroring TurnView nesting.
const TREE = (side) => {
const C = (p) => p[side];
Expand DownExpand Up@@ -251,6 +282,8 @@ const TREE = (side) => {
streamPanel(el, 'stream-live', 'data-live="true"'),
// The PR3b tool-activity card shell.
toolCardSection(el),
// The PR3c tool-error banner (Alert primitive).
errorBanner(el),
].join('\n');
};

Expand All@@ -272,7 +305,12 @@ const IDS = ['summary', 'summary-chip-1', 'summary-chip-2', 'summary-chip-tools'
'tool-summary-collapsed', 'tool-name-collapsed', 'tool-body-collapsed',
// …and every static dot, EXCEPT running's (its `maka-tool-pulse` ring is
// animated → phase-dependent `getComputedStyle`; pinned by the keyframe contract).
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`)];
...STAT.filter((s) => s !== 'running').map((s) => `tool-dot-${s}`),
// PR3c tool-error banner: the CONTAINER box only — proves `.maka-tool-error` was
// inert (shadowed by Alert's `@layer utilities`). The description / copy-button leaf
// utilities are arbitrary-value (source == computed) and pinned by
// visible-copy-hygiene-contract, so they are not re-diffed here.
'err-banner'];
// `::before` middot separators are now diffed for real (they render once the
// CSS is inlined — the old `<link>` build couldn't apply them, masking this).
// summary-chip-2 is a non-first chip (`[&:not(:first-child)]:before:…`);
Expand Down