mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Efficiency pass over the chat features
Four fixes from a branch-wide review of the hot paths: - useChatWorkAutomation content-compares its snapshot: a fresh object per storage event re-ran every consumer effect and re-rendered the panel even when nothing changed. - ChatMessageRow and ChatActivityTranscript are memoized: ChatDetail re-renders on every observer frame during a live turn, and every persisted message re-rendered its whole Markdown tree each time. - The active-agent pubkey list is key-stabilized: the managed-agents 30s refetch minted a fresh array identity that resubscribed the transcript store with an unchanged set. - GitHub API commands share one reqwest client: the PR monitor polls on short intervals and built a new connection pool + TLS session cache per call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6047a11537
commit
ed160c6869
@@ -11,6 +11,23 @@ const MAX_TITLE_FETCH_BYTES: usize = 256 * 1024;
|
||||
const TITLE_FETCH_TIMEOUT: Duration = Duration::from_secs(4);
|
||||
const GITHUB_API_TIMEOUT: Duration = Duration::from_secs(8);
|
||||
|
||||
/// Shared HTTP client for GitHub API commands: the PR monitor polls on
|
||||
/// short intervals, and building a fresh client (connection pool + TLS
|
||||
/// session cache) per call threw away keep-alive reuse on every tick.
|
||||
fn github_client() -> Result<&'static reqwest::Client, String> {
|
||||
static CLIENT: std::sync::OnceLock<Option<reqwest::Client>> = std::sync::OnceLock::new();
|
||||
CLIENT
|
||||
.get_or_init(|| {
|
||||
reqwest::Client::builder()
|
||||
.pool_idle_timeout(Duration::from_secs(90))
|
||||
.pool_max_idle_per_host(2)
|
||||
.build()
|
||||
.ok()
|
||||
})
|
||||
.as_ref()
|
||||
.ok_or_else(|| "github client failed to initialize".to_string())
|
||||
}
|
||||
|
||||
/// Live pull-request details for the rich GitHub PR link card.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -82,11 +99,7 @@ pub async fn fetch_github_pull_request(
|
||||
return Err("invalid GitHub repository reference".to_string());
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.pool_idle_timeout(Duration::from_secs(10))
|
||||
.pool_max_idle_per_host(1)
|
||||
.build()
|
||||
.map_err(|error| format!("github client failed: {error}"))?;
|
||||
let client = github_client()?;
|
||||
|
||||
let url = format!("https://api.github.com/repos/{owner}/{repo}/pulls/{number}");
|
||||
let mut request = client
|
||||
@@ -149,11 +162,7 @@ pub async fn fetch_github_check_summary(
|
||||
return Err("invalid GitHub check reference".to_string());
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.pool_idle_timeout(Duration::from_secs(10))
|
||||
.pool_max_idle_per_host(1)
|
||||
.build()
|
||||
.map_err(|error| format!("github client failed: {error}"))?;
|
||||
let client = github_client()?;
|
||||
|
||||
let url = format!(
|
||||
"https://api.github.com/repos/{owner}/{repo}/commits/{sha}/check-runs?per_page=100"
|
||||
@@ -235,11 +244,7 @@ pub async fn fetch_github_pr_comment_state(
|
||||
return Err("invalid GitHub repository reference".to_string());
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.pool_idle_timeout(Duration::from_secs(10))
|
||||
.pool_max_idle_per_host(1)
|
||||
.build()
|
||||
.map_err(|error| format!("github client failed: {error}"))?;
|
||||
let client = github_client()?;
|
||||
|
||||
let base = format!("https://api.github.com/repos/{owner}/{repo}");
|
||||
let build = |url: String| {
|
||||
@@ -357,11 +362,7 @@ pub async fn find_github_pr_for_branch(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.pool_idle_timeout(Duration::from_secs(10))
|
||||
.pool_max_idle_per_host(1)
|
||||
.build()
|
||||
.map_err(|error| format!("github client failed: {error}"))?;
|
||||
let client = github_client()?;
|
||||
let build = |url: String| {
|
||||
let mut request = client
|
||||
.get(url)
|
||||
|
||||
@@ -99,7 +99,20 @@ export function useChatWorkAutomation(chatId: string): ChatWorkAutomation {
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const refresh = () => setState(readChatWorkAutomation(chatId));
|
||||
// Content-compare: a fresh object per storage event would re-run every
|
||||
// consumer effect (and re-render every panel) even when nothing changed.
|
||||
const refresh = () =>
|
||||
setState((current) => {
|
||||
const next = readChatWorkAutomation(chatId);
|
||||
return current.autoFixCi === next.autoFixCi &&
|
||||
current.addressComments === next.addressComments &&
|
||||
current.lastCiNudgeSha === next.lastCiNudgeSha &&
|
||||
current.lastCommentNudgeCount === next.lastCommentNudgeCount &&
|
||||
current.lastCiNudgeAt === next.lastCiNudgeAt &&
|
||||
current.lastCommentNudgeAt === next.lastCommentNudgeAt
|
||||
? current
|
||||
: next;
|
||||
});
|
||||
refresh();
|
||||
window.addEventListener(STORAGE_EVENT, refresh);
|
||||
window.addEventListener("storage", refresh);
|
||||
|
||||
@@ -87,47 +87,52 @@ function isSetupLifecycleItem(item: TranscriptItem) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatActivityTranscript({
|
||||
activeTurnIds,
|
||||
agent,
|
||||
blocks,
|
||||
identityPubkey,
|
||||
profiles,
|
||||
showAgentIdentity = true,
|
||||
}: {
|
||||
/** Turn ids currently live in this channel — drives per-turn rendering. */
|
||||
activeTurnIds?: ReadonlySet<string>;
|
||||
agent: ManagedAgent | null;
|
||||
blocks: ChatActivityRenderBlock[];
|
||||
identityPubkey?: string;
|
||||
profiles?: UserProfileLookup;
|
||||
/** Hidden in solo chats so agent replies read as part of the stream. */
|
||||
showAgentIdentity?: boolean;
|
||||
}) {
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// Memoized: rendered once per anchored message; placement recomputes on
|
||||
// every transcript event, but rows whose props didn't change must not
|
||||
// re-render their Markdown trees.
|
||||
export const ChatActivityTranscript = React.memo(
|
||||
function ChatActivityTranscript({
|
||||
activeTurnIds,
|
||||
agent,
|
||||
blocks,
|
||||
identityPubkey,
|
||||
profiles,
|
||||
showAgentIdentity = true,
|
||||
}: {
|
||||
/** Turn ids currently live in this channel — drives per-turn rendering. */
|
||||
activeTurnIds?: ReadonlySet<string>;
|
||||
agent: ManagedAgent | null;
|
||||
blocks: ChatActivityRenderBlock[];
|
||||
identityPubkey?: string;
|
||||
profiles?: UserProfileLookup;
|
||||
/** Hidden in solo chats so agent replies read as part of the stream. */
|
||||
showAgentIdentity?: boolean;
|
||||
}) {
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{blocks.map((renderBlock) => (
|
||||
<ChatActivityBlockView
|
||||
agent={agent}
|
||||
block={renderBlock.block}
|
||||
identityPubkey={identityPubkey}
|
||||
isTurnActive={
|
||||
renderBlock.block.kind === "turn" &&
|
||||
(activeTurnIds?.has(renderBlock.block.turnId) ?? false)
|
||||
}
|
||||
key={renderBlock.id}
|
||||
profiles={profiles}
|
||||
showAgentIdentity={showAgentIdentity}
|
||||
suppressPromptMessage={renderBlock.suppressPromptMessage}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{blocks.map((renderBlock) => (
|
||||
<ChatActivityBlockView
|
||||
agent={agent}
|
||||
block={renderBlock.block}
|
||||
identityPubkey={identityPubkey}
|
||||
isTurnActive={
|
||||
renderBlock.block.kind === "turn" &&
|
||||
(activeTurnIds?.has(renderBlock.block.turnId) ?? false)
|
||||
}
|
||||
key={renderBlock.id}
|
||||
profiles={profiles}
|
||||
showAgentIdentity={showAgentIdentity}
|
||||
suppressPromptMessage={renderBlock.suppressPromptMessage}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function ChatActivityBlockView({
|
||||
agent,
|
||||
|
||||
@@ -35,7 +35,10 @@ function profileName(
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatMessageRow({
|
||||
// Memoized: ChatDetail re-renders on every observer frame during a live
|
||||
// turn, and without this every persisted message re-renders its whole
|
||||
// Markdown tree each time. All props are identity-stable between events.
|
||||
export const ChatMessageRow = React.memo(function ChatMessageRow({
|
||||
event,
|
||||
isAgent,
|
||||
isOwn,
|
||||
@@ -138,7 +141,7 @@ export function ChatMessageRow({
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Following the stream is owned entirely by the MessageScroller's built-in
|
||||
// autoScroll (content mutation + resize observers). This anchor only handles
|
||||
|
||||
@@ -125,15 +125,22 @@ export function ChatDetail({
|
||||
// Every active managed agent, not just the default: a chat can have
|
||||
// several agents working and all of their activity must render.
|
||||
const managedAgentsQuery = useManagedAgentsQuery();
|
||||
const activeAgentPubkeys = React.useMemo(() => {
|
||||
// Key-stabilized: the managed-agents query refetches on a 30s interval and
|
||||
// a fresh array identity would resubscribe the transcript store each time
|
||||
// even when the active set is unchanged.
|
||||
const activeAgentPubkeysKey = React.useMemo(() => {
|
||||
const pubkeys = (managedAgentsQuery.data ?? [])
|
||||
.filter(isManagedAgentActive)
|
||||
.map((agent) => normalizePubkey(agent.pubkey));
|
||||
if (defaultAgent && isManagedAgentActive(defaultAgent)) {
|
||||
pubkeys.push(normalizePubkey(defaultAgent.pubkey));
|
||||
}
|
||||
return [...new Set(pubkeys)].sort();
|
||||
return [...new Set(pubkeys)].sort().join(",");
|
||||
}, [defaultAgent, managedAgentsQuery.data]);
|
||||
const activeAgentPubkeys = React.useMemo(
|
||||
() => activeAgentPubkeysKey.split(",").filter(Boolean),
|
||||
[activeAgentPubkeysKey],
|
||||
);
|
||||
const hasObserver = activeAgentPubkeys.length > 0;
|
||||
const activeChannelTurns = useActiveAgentTurnsByChannel();
|
||||
// Per-turn ids, not a channel-wide boolean: while a new turn runs, older
|
||||
|
||||
Reference in New Issue
Block a user