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:
klopez4212
2026-07-07 07:52:00 +01:00
co-authored by Claude Fable 5
parent 6047a11537
commit ed160c6869
5 changed files with 94 additions and 65 deletions
+21 -20
View File
@@ -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
+9 -2
View File
@@ -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