mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
Merge remote-tracking branch 'origin/main' into duncan/workspace-scoped-agent-store
* origin/main: fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198) Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> # Conflicts: # desktop/src-tauri/src/commands/agents.rs
This commit is contained in:
@@ -18,11 +18,11 @@ use super::agent_update_rollback::{rollback_failed_agent_update, AgentUpdateRoll
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
managed_agents::{
|
||||
build_managed_agent_summary, current_instance_id, discovery_env_with_baked_floor,
|
||||
find_managed_agent_mut, known_acp_runtime, load_global_agent_config, load_managed_agents,
|
||||
load_personas, managed_agent_avatar_url, missing_command_message, normalize_agent_args,
|
||||
resolve_command, save_managed_agents, sync_managed_agent_processes, try_regenerate_nest,
|
||||
AgentModelInfo, AgentModelsResponse, ManagedAgentRecord, UpdateManagedAgentRequest,
|
||||
current_instance_id, discovery_env_with_baked_floor, find_managed_agent_mut,
|
||||
known_acp_runtime, load_global_agent_config, load_managed_agents, load_personas,
|
||||
managed_agent_avatar_url, missing_command_message, normalize_agent_args, resolve_command,
|
||||
save_managed_agents, sync_managed_agent_processes, try_regenerate_nest, AgentModelInfo,
|
||||
AgentModelsResponse, ManagedAgentRecord, UpdateManagedAgentRequest,
|
||||
UpdateManagedAgentResponse, DEFAULT_ACP_COMMAND,
|
||||
},
|
||||
relay::{relay_ws_url_with_override, sync_managed_agent_profile},
|
||||
|
||||
@@ -247,16 +247,7 @@ pub async fn update_managed_agent(
|
||||
None
|
||||
};
|
||||
|
||||
let summary = {
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)?
|
||||
};
|
||||
let summary = { super::super::agents::summarize_from_disk(&app, record, &runtimes)? };
|
||||
let rollback = name_changed
|
||||
.then(|| AgentUpdateRollback::new(previous_record, record, access_policy_changed));
|
||||
(
|
||||
|
||||
@@ -4,9 +4,8 @@ use tauri::{AppHandle, Manager, State};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
managed_agents::{
|
||||
build_managed_agent_summary, current_instance_id, find_managed_agent_mut,
|
||||
load_managed_agents, load_personas, save_managed_agents, sync_managed_agent_processes,
|
||||
ManagedAgentSummary,
|
||||
current_instance_id, find_managed_agent_mut, load_managed_agents, save_managed_agents,
|
||||
sync_managed_agent_processes, ManagedAgentSummary,
|
||||
},
|
||||
util::now_iso,
|
||||
};
|
||||
@@ -53,14 +52,7 @@ pub async fn set_managed_agent_start_on_app_launch(
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)
|
||||
super::agents::summarize_from_disk(&app, record, &runtimes)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
@@ -101,14 +93,7 @@ pub async fn set_managed_agent_auto_restart(
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)
|
||||
super::agents::summarize_from_disk(&app, record, &runtimes)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
|
||||
@@ -25,6 +25,28 @@ pub(super) fn workspace_owner_hex(state: &AppState) -> Result<String, String> {
|
||||
Ok(state.current_pubkey()?.to_hex())
|
||||
}
|
||||
|
||||
/// Build a summary from fresh disk state (personas, teams, global config).
|
||||
/// For one-shot command paths only — the 5s list poll calls
|
||||
/// `build_managed_agent_summary` directly with stores loaded once per call,
|
||||
/// not once per record.
|
||||
pub(super) fn summarize_from_disk(
|
||||
app: &AppHandle,
|
||||
record: &ManagedAgentRecord,
|
||||
runtimes: &std::collections::HashMap<
|
||||
crate::managed_agents::ManagedAgentRuntimeKey,
|
||||
crate::managed_agents::ManagedAgentPairRuntime,
|
||||
>,
|
||||
) -> Result<ManagedAgentSummary, String> {
|
||||
build_managed_agent_summary(
|
||||
app,
|
||||
record,
|
||||
runtimes,
|
||||
&load_personas(app).unwrap_or_default(),
|
||||
&load_teams(app).unwrap_or_default(),
|
||||
&crate::managed_agents::load_global_agent_config(app).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Retain a freshly authored managed-agent event in the local store, flagged
|
||||
/// for relay sync. MUST be called inside the `managed_agents_store_lock`-held
|
||||
/// body after `save_managed_agents`, NEVER across an `.await`: it acquires
|
||||
@@ -357,6 +379,7 @@ pub(super) async fn start_local_agent_with_preflight(
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&load_teams(app).unwrap_or_default(),
|
||||
&crate::managed_agents::load_global_agent_config(app).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
@@ -392,14 +415,22 @@ pub async fn list_managed_agents(app: AppHandle) -> Result<Vec<ManagedAgentSumma
|
||||
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
// One disk read for the whole list — build_managed_agent_summary takes
|
||||
// the config as a parameter precisely so this poll-every-5s call does
|
||||
// not re-read it per record.
|
||||
// teams and config as parameters precisely so this poll-every-5s call
|
||||
// does not re-read them per record.
|
||||
let teams = load_teams(&app).unwrap_or_default();
|
||||
let global_config =
|
||||
crate::managed_agents::load_global_agent_config(&app).unwrap_or_default();
|
||||
records
|
||||
.iter()
|
||||
.map(|record| {
|
||||
build_managed_agent_summary(&app, record, &runtimes, &personas, &global_config)
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&teams,
|
||||
&global_config,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
@@ -768,15 +799,8 @@ pub async fn create_managed_agent(
|
||||
// before any .await — owner-authored, every agent (Will's ruling: no
|
||||
// is_builtin/persona-membership gate).
|
||||
retain_managed_agent_pending(&app, &state, record);
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
(
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)?,
|
||||
summarize_from_disk(&app, record, &runtimes)?,
|
||||
resolved_avatar_url,
|
||||
)
|
||||
};
|
||||
@@ -805,14 +829,7 @@ pub async fn create_managed_agent(
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| "created agent disappeared unexpectedly".to_string())?;
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)?
|
||||
summarize_from_disk(&app, record, &runtimes)?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -881,14 +898,7 @@ pub async fn create_managed_agent(
|
||||
.iter()
|
||||
.find(|r| r.pubkey == pubkey)
|
||||
.ok_or_else(|| "agent disappeared".to_string())?;
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)?
|
||||
summarize_from_disk(&app, record, &runtimes)?
|
||||
} else {
|
||||
agent
|
||||
};
|
||||
@@ -994,14 +1004,7 @@ pub async fn start_managed_agent(
|
||||
.iter()
|
||||
.find(|r| r.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)
|
||||
summarize_from_disk(&app, record, &runtimes)
|
||||
}
|
||||
StartTarget::Provider { backend, .. } => Err(format!(
|
||||
"agent {pubkey} has unsupported backend kind: {backend:?}"
|
||||
@@ -1079,14 +1082,7 @@ pub async fn stop_managed_agent(
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
build_managed_agent_summary(
|
||||
&app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
|
||||
)
|
||||
summarize_from_disk(&app, record, &runtimes)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
|
||||
@@ -84,16 +84,9 @@ pub(crate) async fn start_local_agent_pairs_with_preflight(
|
||||
.managed_agent_processes
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let personas = super::load_personas(app).unwrap_or_default();
|
||||
let record = records
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
super::build_managed_agent_summary(
|
||||
app,
|
||||
record,
|
||||
&runtimes,
|
||||
&personas,
|
||||
&crate::managed_agents::load_global_agent_config(app).unwrap_or_default(),
|
||||
)
|
||||
super::summarize_from_disk(app, record, &runtimes)
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ pub fn build_managed_agent_summary(
|
||||
record: &ManagedAgentRecord,
|
||||
runtimes: &HashMap<ManagedAgentRuntimeKey, ManagedAgentPairRuntime>,
|
||||
personas: &[crate::managed_agents::types::AgentDefinition],
|
||||
teams: &[crate::managed_agents::TeamRecord],
|
||||
global_config: &crate::managed_agents::GlobalAgentConfig,
|
||||
) -> Result<ManagedAgentSummary, String> {
|
||||
use crate::managed_agents::BackendKind;
|
||||
@@ -195,12 +196,10 @@ pub fn build_managed_agent_summary(
|
||||
|
||||
let (persona_out_of_date, persona_orphaned) = persona_drift_state(record, personas);
|
||||
|
||||
let global_for_summary =
|
||||
crate::managed_agents::load_global_agent_config(app).unwrap_or_default();
|
||||
let effective_cfg = crate::managed_agents::effective_config::resolve_effective_config(
|
||||
record,
|
||||
personas,
|
||||
&global_for_summary,
|
||||
global_config,
|
||||
);
|
||||
let (effective_model, effective_provider, effective_prompt, model_source) = match effective_cfg
|
||||
{
|
||||
@@ -242,14 +241,13 @@ pub fn build_managed_agent_summary(
|
||||
// env layering below — the caller loads it once and passes it in, so
|
||||
// list-style callers pay one disk read per call rather than one per record.
|
||||
|
||||
// The prospective side is computed only for a tracked pair: it costs a
|
||||
// teams-store read, and an unstamped agent has nothing to compare against.
|
||||
// The prospective side is computed only for a tracked pair: an unstamped
|
||||
// agent has nothing to compare against.
|
||||
let tracked_spawn = pair_key.as_ref().zip(pair_runtime).map(|(key, runtime)| {
|
||||
let teams = crate::managed_agents::load_teams(app).unwrap_or_default();
|
||||
let current = crate::managed_agents::spawn_snapshot::prospective_spawn_config_snapshot(
|
||||
record,
|
||||
personas,
|
||||
&teams,
|
||||
teams,
|
||||
&key.relay_url,
|
||||
global_config,
|
||||
super::owner_only_access_build(),
|
||||
|
||||
@@ -243,11 +243,26 @@ function appendAgentEvents(
|
||||
: events;
|
||||
if (admissible.length === 0) return null;
|
||||
|
||||
const seen = new Set(
|
||||
current.map(
|
||||
(event) => `${event.timestamp.length}:${event.timestamp}:${event.seq}`,
|
||||
),
|
||||
);
|
||||
// Ordinary live path: the harness publishes frames in order once per
|
||||
// second, so the whole batch lands strictly after the retained tail. In
|
||||
// that case no admissible event can collide with a retained one (the
|
||||
// journal is sorted), so dedup only needs to look inside the batch and the
|
||||
// merged journal is a plain concat — no Set over the full journal and no
|
||||
// whole-journal re-sort (whose comparator Date.parses per comparison).
|
||||
// Out-of-order or replayed arrivals take the full dedup + re-sort path.
|
||||
const currentLast = current.at(-1);
|
||||
const allAtEnd =
|
||||
!currentLast ||
|
||||
admissible.every((event) => isObserverEventAfter(event, currentLast));
|
||||
|
||||
const seen = allAtEnd
|
||||
? new Set<string>()
|
||||
: new Set(
|
||||
current.map(
|
||||
(event) =>
|
||||
`${event.timestamp.length}:${event.timestamp}:${event.seq}`,
|
||||
),
|
||||
);
|
||||
const added: ObserverEvent[] = [];
|
||||
for (const event of admissible) {
|
||||
const eventKey = `${event.timestamp.length}:${event.timestamp}:${event.seq}`;
|
||||
@@ -258,7 +273,9 @@ function appendAgentEvents(
|
||||
if (added.length === 0) return null;
|
||||
|
||||
const sortedAdded = added.sort(compareObserverEvents);
|
||||
const sorted = [...current, ...sortedAdded].sort(compareObserverEvents);
|
||||
const sorted = allAtEnd
|
||||
? [...current, ...sortedAdded]
|
||||
: [...current, ...sortedAdded].sort(compareObserverEvents);
|
||||
const trimmed = sorted.length > MAX_OBSERVER_EVENTS;
|
||||
const final = trimmed
|
||||
? sorted.slice(sorted.length - OBSERVER_EVENTS_LOW_WATER)
|
||||
@@ -276,14 +293,11 @@ function appendAgentEvents(
|
||||
});
|
||||
}
|
||||
|
||||
// The common live path appends a sorted batch after the retained window. Fold
|
||||
// The common live path appends a sorted batch after the retained window
|
||||
// (the same `allAtEnd` that authorized the concat fast-path above). Fold
|
||||
// that batch through the transcript state once without rebuilding history.
|
||||
// Out-of-order arrivals and cap eviction rebuild from the final window so
|
||||
// stateful tool/permission relationships remain correct.
|
||||
const currentLast = current.at(-1);
|
||||
const allAtEnd =
|
||||
!currentLast ||
|
||||
sortedAdded.every((event) => compareObserverEvents(event, currentLast) > 0);
|
||||
if (allAtEnd && !trimmed) {
|
||||
let transcriptState =
|
||||
transcriptByAgent.get(key) ?? createEmptyTranscriptState();
|
||||
|
||||
@@ -303,3 +303,116 @@ describe("live observer journal retention — eviction floor (reconnect replay)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("live observer journal — in-order append fast path ordering/dedup", () => {
|
||||
// The common live path (every batch strictly after the retained tail) skips
|
||||
// the whole-journal dedup Set and re-sort. These pin the observable
|
||||
// invariants that authorize that skip: identical ordering, dedup, and
|
||||
// transcript against the general path.
|
||||
beforeEach(() => {
|
||||
resetAgentObserverStore();
|
||||
});
|
||||
|
||||
/** An event with an explicit timestamp, for equal-timestamp tie-breaks. */
|
||||
function makeEventAt(seq, timestampMs) {
|
||||
return {
|
||||
...makeEvent(seq),
|
||||
timestamp: new Date(timestampMs).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
it("test_equal_timestamp_batch_orders_by_seq", () => {
|
||||
// A one-second harness frame batches several events sharing a timestamp;
|
||||
// the tie-break is seq. Deliver them out of seq order in one batch.
|
||||
const t = 1_760_000_100_000;
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, [makeEventAt(1, t - 1000)]);
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, [
|
||||
makeEventAt(4, t),
|
||||
makeEventAt(2, t),
|
||||
makeEventAt(3, t),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
getAgentObserverSnapshot(AGENT_PUBKEY).events.map((event) => event.seq),
|
||||
[1, 2, 3, 4],
|
||||
"equal-timestamp events are retained in seq order",
|
||||
);
|
||||
});
|
||||
|
||||
it("test_duplicate_batch_redelivery_is_ignored", () => {
|
||||
// Relay redelivery of the newest batch: every event duplicates the tail,
|
||||
// so nothing is admitted, nothing is notified.
|
||||
const batch = [makeEvent(1), makeEvent(2), makeEvent(3)];
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, batch);
|
||||
let notifications = 0;
|
||||
const unsubscribe = subscribeAgentObserverStore(() => {
|
||||
notifications += 1;
|
||||
});
|
||||
try {
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, batch);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
assert.deepEqual(
|
||||
getAgentObserverSnapshot(AGENT_PUBKEY).events.map((event) => event.seq),
|
||||
[1, 2, 3],
|
||||
"a redelivered batch adds nothing",
|
||||
);
|
||||
assert.equal(notifications, 0, "a pure-duplicate batch notifies no one");
|
||||
});
|
||||
|
||||
it("test_batch_with_intra_batch_duplicate_admits_once", () => {
|
||||
// A batch strictly after the tail still dedups within itself.
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, [makeEvent(1)]);
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, [
|
||||
makeEvent(2),
|
||||
makeEvent(3),
|
||||
makeEvent(2),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
getAgentObserverSnapshot(AGENT_PUBKEY).events.map((event) => event.seq),
|
||||
[1, 2, 3],
|
||||
"an intra-batch duplicate is admitted exactly once",
|
||||
);
|
||||
});
|
||||
|
||||
it("test_late_arrival_overlapping_tail_takes_slow_path_and_dedups", () => {
|
||||
// A replayed window straddling the tail: partly duplicate, partly new,
|
||||
// partly older-than-tail. Not all-after, so the general path must dedup
|
||||
// against the whole journal and re-sort.
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, [
|
||||
makeEvent(1),
|
||||
makeEvent(2),
|
||||
makeEvent(4),
|
||||
]);
|
||||
syncAgentObserverEvents(AGENT_PUBKEY, [
|
||||
makeEvent(2),
|
||||
makeEvent(3),
|
||||
makeEvent(4),
|
||||
makeEvent(5),
|
||||
]);
|
||||
const events = getAgentObserverSnapshot(AGENT_PUBKEY).events;
|
||||
assert.deepEqual(
|
||||
events.map((event) => event.seq),
|
||||
[1, 2, 3, 4, 5],
|
||||
"overlapping late arrival dedups against the journal and sorts into place",
|
||||
);
|
||||
assert.deepEqual(
|
||||
getAgentTranscript(AGENT_PUBKEY),
|
||||
buildTranscript(events),
|
||||
"the transcript equals a full replay after a mixed-path sequence",
|
||||
);
|
||||
});
|
||||
|
||||
it("test_fast_path_transcript_equals_full_replay", () => {
|
||||
// Pure in-order streaming (fast path every time) must produce the same
|
||||
// derived transcript as a replay of the retained window.
|
||||
fillSequential(50);
|
||||
const events = getAgentObserverSnapshot(AGENT_PUBKEY).events;
|
||||
assert.equal(events.length, 50);
|
||||
assert.deepEqual(
|
||||
getAgentTranscript(AGENT_PUBKEY),
|
||||
buildTranscript(events),
|
||||
"in-order fast-path appends derive the same transcript as a replay",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,7 +194,6 @@
|
||||
|
||||
.buzz-shimmer {
|
||||
--buzz-shimmer-duration: 2600ms;
|
||||
--buzz-shimmer-band: 250%;
|
||||
--buzz-shimmer-highlight: color-mix(
|
||||
in srgb,
|
||||
hsl(var(--background)) 60%,
|
||||
@@ -202,49 +201,58 @@
|
||||
);
|
||||
--buzz-shimmer-spread: 2rem;
|
||||
|
||||
animation: buzz-shimmer var(--buzz-shimmer-duration) linear infinite;
|
||||
background-clip: text;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
transparent calc(50% - var(--buzz-shimmer-spread)),
|
||||
var(--buzz-shimmer-highlight) 50%,
|
||||
transparent calc(50% + var(--buzz-shimmer-spread))
|
||||
),
|
||||
linear-gradient(hsl(var(--muted-foreground)), hsl(var(--muted-foreground)));
|
||||
background-position:
|
||||
100% 0,
|
||||
0 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size:
|
||||
var(--buzz-shimmer-band) 100%,
|
||||
100% 100%;
|
||||
color: transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*
|
||||
* The highlight lives on an aria-hidden overlay child that duplicates the
|
||||
* label text and animates ONLY opacity. The previous implementation animated
|
||||
* background-position under -webkit-background-clip: text, which WebKit
|
||||
* cannot run on the compositor: every animation frame forced a full document
|
||||
* style resolve + compositing-hierarchy walk, burning ~20% CPU at rest
|
||||
* whenever a large timeline was mounted with a working agent. Opacity is
|
||||
* compositor-accelerated: the layer paints once and the pulse runs outside
|
||||
* the web process's main thread. A real element (not ::after generated
|
||||
* content) is used so the duplicate text can be aria-hidden — pseudo-element
|
||||
* text is inconsistently exposed to screen readers and cannot be hidden.
|
||||
*/
|
||||
.buzz-shimmer > .buzz-shimmer-overlay {
|
||||
animation: buzz-shimmer var(--buzz-shimmer-duration) ease-in-out infinite;
|
||||
background-clip: text;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent calc(50% - var(--buzz-shimmer-spread)),
|
||||
var(--buzz-shimmer-highlight) 50%,
|
||||
transparent calc(50% + var(--buzz-shimmer-spread))
|
||||
);
|
||||
color: transparent;
|
||||
inset: 0;
|
||||
overflow: inherit;
|
||||
padding: inherit;
|
||||
position: absolute;
|
||||
text-overflow: inherit;
|
||||
white-space: inherit;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
@keyframes buzz-shimmer {
|
||||
0% {
|
||||
background-position:
|
||||
100% 0,
|
||||
0 0;
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position:
|
||||
0 0,
|
||||
0 0;
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.buzz-shimmer {
|
||||
.buzz-shimmer > .buzz-shimmer-overlay {
|
||||
animation: none;
|
||||
background: none;
|
||||
color: hsl(var(--muted-foreground));
|
||||
-webkit-text-fill-color: currentcolor;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ export function Shimmer({ children, className }: ShimmerProps) {
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{/* Visual-only highlight copy; the sibling text node above is the sole
|
||||
accessible content. */}
|
||||
<span aria-hidden="true" className="buzz-shimmer-overlay">
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user