fix: show one compact theme-aware discovery loader

Remove the duplicate progress bar and pulsing status dot. Reduce wave geometry to 190 by 72 and inherit existing light/dark accent colors. Record the provider-agnostic native CLI boundary in AGENTS.md.

Checks: frontend 375 tests; web and desktop builds; desktop pack; Electron first-run and returning-user checks with animated loader, light/dark colors and a 560px window.
This commit is contained in:
Ryder Sun committed 2026-10-30 11:01:49 +08:00
1 parent 05ec2bfbf0
commit d2f6171ddd
5 files changed
+21 -83

No files matched your search

+1
View File
@@ -11,6 +11,7 @@
- Meldwork is a local-first desktop application. Do not introduce a required server, container, or cluster dependency unless the task explicitly calls for it. - Meldwork is a local-first desktop application. Do not introduce a required server, container, or cluster dependency unless the task explicitly calls for it.
- Keep Agent execution in the Electron main process. Renderer access must go through narrow, validated preload APIs. - Keep Agent execution in the Electron main process. Renderer access must go through narrow, validated preload APIs.
- Respect native Agent configuration and keep CLI integration provider-agnostic. Do not add gateway-specific adapters, provider overrides, or bundled patches for external tools such as Magpie.
- Never expose executable paths, tokens, credentials, or unrestricted shell execution to renderer code. - Never expose executable paths, tokens, credentials, or unrestricted shell execution to renderer code.
- Store secrets only through operating-system-backed secure storage. Do not add plaintext fallbacks. - Store secrets only through operating-system-backed secure storage. Do not add plaintext fallbacks.
- Keep workspace write access opt-in. Conversations and groups stay local; do not add remote participants, cloud channels, or remote conversation storage. - Keep workspace write access opt-in. Conversations and groups stay local; do not add remote participants, cloud channels, or remote conversation storage.
-3
View File
@@ -41,9 +41,6 @@
<p class="agent-discovery-kicker">{{ t('agentDiscovery.kicker') }}</p> <p class="agent-discovery-kicker">{{ t('agentDiscovery.kicker') }}</p>
<h1>{{ t('agentDiscovery.title') }}</h1> <h1>{{ t('agentDiscovery.title') }}</h1>
<p>{{ t('agentDiscovery.body') }}</p> <p>{{ t('agentDiscovery.body') }}</p>
<div class="agent-discovery-progress" aria-hidden="true">
<span />
</div>
<div class="agent-discovery-status"> <div class="agent-discovery-status">
<span class="agent-discovery-status-dot" aria-hidden="true" /> <span class="agent-discovery-status-dot" aria-hidden="true" />
<span>{{ t('agentDiscovery.status') }}</span> <span>{{ t('agentDiscovery.status') }}</span>
@@ -59,6 +59,8 @@ describe('Meldwork workbench', () => {
expect(wrapper.get('.agent-discovery-overlay').text()).toContain('Agent CLI') expect(wrapper.get('.agent-discovery-overlay').text()).toContain('Agent CLI')
expect(wrapper.find('.pixel-blast-container').exists()).toBe(true) expect(wrapper.find('.pixel-blast-container').exists()).toBe(true)
expect(wrapper.get('.agent-discovery-overlay').find('.wave-physics-loader').exists()).toBe(true) expect(wrapper.get('.agent-discovery-overlay').find('.wave-physics-loader').exists()).toBe(true)
expect(wrapper.get('.agent-discovery-overlay').findAll('.wave-physics-loader')).toHaveLength(1)
expect(wrapper.find('.agent-discovery-progress').exists()).toBe(false)
expect(wrapper.get('.workspace-pane').attributes()).toHaveProperty('inert') expect(wrapper.get('.workspace-pane').attributes()).toHaveProperty('inert')
expect(wrapper.get('.sidebar').attributes()).toHaveProperty('inert') expect(wrapper.get('.sidebar').attributes()).toHaveProperty('inert')
+18 -37
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="wave-physics-loader" aria-hidden="true"> <div class="wave-physics-loader" :data-theme="theme" aria-hidden="true">
<div class="wave-physics-track"> <div class="wave-physics-track">
<span <span
v-for="bar in bars" v-for="bar in bars"
@@ -21,34 +21,23 @@
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps({ defineProps({
theme: { type: String, default: 'light' }, theme: { type: String, default: 'light' },
}) })
const numBars = 15 const numBars = 15
const barWidth = 12 const barWidth = 8
const barGap = 8 const barGap = 5
const barTotalWidth = barWidth + barGap const barTotalWidth = barWidth + barGap
const B = 4 const B = 4
const maxBounce = 60 const maxBounce = 24
const baseBarH = 16 const baseBarH = 8
const wavePeakH = 48 const wavePeakH = 24
const duration = 4000 const duration = 4000
const progress = ref(0.5) const progress = ref(0.5)
let animationFrame = 0 let animationFrame = 0
function interpolate(start, end, value) {
return Math.round(start + (end - start) * value)
}
function barColor(theme, waveValue) {
if (theme === 'dark') {
return `rgb(${interpolate(39, 228, waveValue)} ${interpolate(39, 228, waveValue)} ${interpolate(42, 231, waveValue)})`
}
return `rgb(${interpolate(228, 39, waveValue)} ${interpolate(228, 39, waveValue)} ${interpolate(231, 42, waveValue)})`
}
function frameAt(t) { function frameAt(t) {
const xFrac = t < 0.5 ? t / 0.5 : (1 - t) / 0.5 const xFrac = t < 0.5 ? t / 0.5 : (1 - t) / 0.5
const ballIndex = xFrac * (numBars - 1) const ballIndex = xFrac * (numBars - 1)
@@ -57,7 +46,7 @@ function frameAt(t) {
const bounceH = 4 * bounceF * (1 - bounceF) const bounceH = 4 * bounceF * (1 - bounceF)
const heightFactor = Math.max(0, 1 - bounceH * 2) const heightFactor = Math.max(0, 1 - bounceH * 2)
const ballIndent = heightFactor * 20 const ballIndent = heightFactor * 10
const ballY = (baseBarH + wavePeakH - ballIndent) + bounceH * maxBounce const ballY = (baseBarH + wavePeakH - ballIndent) + bounceH * maxBounce
const bars = Array.from({ length: numBars }, (_unused, index) => { const bars = Array.from({ length: numBars }, (_unused, index) => {
@@ -68,13 +57,13 @@ function frameAt(t) {
let indent = 0 let indent = 0
if (dist < 1.5) { if (dist < 1.5) {
const indentDist = Math.cos((dist / 1.5) * (Math.PI / 2)) const indentDist = Math.cos((dist / 1.5) * (Math.PI / 2))
indent = indentDist * heightFactor * 20 indent = indentDist * heightFactor * 10
} }
const barHeight = Math.max(4, baseBarH + waveValue * wavePeakH - indent) const barHeight = Math.max(3, baseBarH + waveValue * wavePeakH - indent)
return { return {
color: barColor(props.theme, waveValue), color: `color-mix(in srgb, var(--accent) ${Math.round(waveValue * 100)}%, var(--accent-soft))`,
height: `${barHeight}px`, height: `${barHeight}px`,
index, index,
} }
@@ -83,7 +72,7 @@ function frameAt(t) {
return { return {
bars, bars,
ball: { ball: {
color: props.theme === 'dark' ? 'rgb(244, 244, 245)' : 'rgb(24, 24, 27)', color: 'var(--accent)',
scaleX: 1 + heightFactor * 0.25, scaleX: 1 + heightFactor * 0.25,
scaleY: 1 - heightFactor * 0.3, scaleY: 1 - heightFactor * 0.3,
x: `${ballIndex * barTotalWidth}px`, x: `${ballIndex * barTotalWidth}px`,
@@ -126,16 +115,16 @@ onBeforeUnmount(() => {
.wave-physics-track { .wave-physics-track {
position: relative; position: relative;
width: 292px; width: 190px;
height: 192px; height: 72px;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: flex-start; justify-content: flex-start;
gap: 8px; gap: 5px;
} }
.wave-physics-bar { .wave-physics-bar {
width: 12px; width: 8px;
border-radius: 999px; border-radius: 999px;
transform-origin: bottom center; transform-origin: bottom center;
will-change: height, background-color; will-change: height, background-color;
@@ -145,22 +134,14 @@ onBeforeUnmount(() => {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 12px; width: 8px;
height: 12px; height: 8px;
border-radius: 999px; border-radius: 999px;
transform-origin: bottom center; transform-origin: bottom center;
box-shadow: 0 1px 3px color-mix(in srgb, currentColor 24%, transparent); box-shadow: 0 1px 3px color-mix(in srgb, currentColor 24%, transparent);
will-change: transform, background-color; will-change: transform, background-color;
} }
@media (max-width: 640px) {
.wave-physics-track {
width: 260px;
transform: scale(0.9);
transform-origin: center;
}
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.wave-physics-bar, .wave-physics-bar,
.wave-physics-ball { .wave-physics-ball {
-43
View File
@@ -67,25 +67,6 @@
line-height: 1.55; line-height: 1.55;
} }
.agent-discovery-progress {
width: min(260px, 100%);
height: 3px;
margin-top: 26px;
overflow: hidden;
border-radius: 999px;
background: color-mix(in srgb, var(--border) 68%, transparent);
}
.agent-discovery-progress span {
display: block;
width: 46%;
height: 100%;
border-radius: inherit;
background: var(--onboarding-frame-gradient);
box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 34%, transparent);
animation: agent-discovery-progress 1.35s ease-in-out infinite;
}
.agent-discovery-status { .agent-discovery-status {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -101,7 +82,6 @@
border-radius: 50%; border-radius: 50%;
background: var(--accent); background: var(--accent);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent);
animation: agent-discovery-dot 1.25s ease-in-out infinite;
} }
.agent-discovery-enter-active, .agent-discovery-enter-active,
@@ -125,29 +105,6 @@
transform: translateY(8px) scale(0.985); transform: translateY(8px) scale(0.985);
} }
@keyframes agent-discovery-progress {
0% { transform: translateX(-125%); opacity: 0.4; }
55% { opacity: 1; }
100% { transform: translateX(300%); opacity: 0.4; }
}
@keyframes agent-discovery-dot {
0%, 100% { opacity: 0.42; transform: scale(0.86); }
50% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.agent-discovery-progress span,
.agent-discovery-status-dot {
animation: none;
}
.agent-discovery-progress span {
width: 58%;
transform: translateX(36%);
}
}
@media (max-width: 480px) { @media (max-width: 480px) {
.agent-discovery-overlay { padding: 24px; } .agent-discovery-overlay { padding: 24px; }
.agent-discovery-copy h1 { font-size: 26px; } .agent-discovery-copy h1 { font-size: 26px; }