fix: address Copilot review round 3 (code-point truncation, a11y live status, disabled-tab wait mirroring, test unmount, concurrent-wait test)

This commit is contained in:
HuanLinOTO
2026-09-10 21:28:22 +08:00
parent ccb89fd91b
commit 70a150794b
6 changed files with 106 additions and 19 deletions
+14 -3
View File
@@ -12,16 +12,27 @@ import css from './sidebar.module.css'
/** Cap the needle shown inline; the full text rides the title tooltip. */
const NEEDLE_DISPLAY_CAP = 80
/** Truncate one needle for inline display (title attr carries the full text). */
/** Truncate one needle for inline display (title attr carries the full text).
* Cut by Unicode code points, not UTF-16 code units — a naive slice can split
* a surrogate pair at the cutoff and render a dangling replacement char. */
export function truncateNeedle(needle: string): string {
return needle.length > NEEDLE_DISPLAY_CAP ? `${needle.slice(0, NEEDLE_DISPLAY_CAP - 1)}…` : needle
const points = Array.from(needle)
return points.length > NEEDLE_DISPLAY_CAP
? `${points.slice(0, NEEDLE_DISPLAY_CAP - 1).join('')}…`
: needle
}
export function TerminalWaitBanner(props: { needle: string; onSkip: () => void }) {
const { needle, onSkip } = props
return (
<div className={css.terminalWaitBanner}>
<span className={css.terminalWaitNeedle} title={needle}>
<span
className={css.terminalWaitNeedle}
title={needle}
role="status"
aria-live="polite"
aria-label={t('terminalWaitBanner', { needle })}
>
{t('terminalWaitBanner', { needle: truncateNeedle(needle) })}
</span>
<button type="button" className={css.terminalRetry} onClick={onSkip}>
+7 -2
View File
@@ -7,7 +7,7 @@
*/
import { useEffect, useRef } from 'react'
import type { Context, SidebarSessionList } from '../../context-types.ts'
import { reconcileAgentTerminals, type SidebarStore } from '../state.ts'
import { mirrorAgentWaits, reconcileAgentTerminals, type SidebarStore } from '../state.ts'
import { detectNewDirectSubagent } from '../subagent-detect.ts'
import { detectNewJob } from '../subagent-jobs.ts'
import { t } from '../locales.ts'
@@ -65,7 +65,12 @@ export function useHostFeeds(feeds: {
const list = JSON.parse(event.data) as Array<{ uuid: string; title: string; command: string; exited: boolean; waiting?: { needle: string; since: number } | null }>
if (!Array.isArray(list)) return
store.reduce(s => ctx.get('betterSidebar')?.isTabEnabled('terminal') === false
? s
// Terminal tabs are disabled: skip tab add/remove reconciliation,
// but STILL mirror the authoritative wait map — a wait resolving
// during the disabled window must clear its banner state, or a
// re-enabled terminal keeps a stale banner until the next
// unrelated push.
? mirrorAgentWaits(s, list)
: reconcileAgentTerminals(s, list))
} catch {
// Malformed push: ignore (the next push will reconcile).
+31 -6
View File
@@ -687,6 +687,19 @@ function sameAgentWaits(
return true
}
/** Fold the pushed terminal snapshots into the authoritative wait map. */
function serverWaitsOf(
agentTerminals: ReadonlyArray<{ uuid: string; title: string; waiting?: { needle: string; since: number } | null }>,
): Record<string, { needle: string; since: number }> {
const serverWaits: Record<string, { needle: string; since: number }> = {}
for (const terminal of agentTerminals) {
if (terminal.waiting !== undefined && terminal.waiting !== null) {
serverWaits[terminal.uuid] = { needle: terminal.waiting.needle, since: terminal.waiting.since }
}
}
return serverWaits
}
/**
* Reconcile the sidebar's agent-terminal tabs with the host's live list.
* The host pushes the current list of agent terminals (created by the model
@@ -719,12 +732,7 @@ export function reconcileAgentTerminals(
// waiting field simply drops the entry). A waits-only change must still
// produce a new state — the tab add/remove no-change check alone would
// swallow banner updates.
const serverWaits: Record<string, { needle: string; since: number }> = {}
for (const terminal of agentTerminals) {
if (terminal.waiting !== undefined && terminal.waiting !== null) {
serverWaits[terminal.uuid] = { needle: terminal.waiting.needle, since: terminal.waiting.since }
}
}
const serverWaits = serverWaitsOf(agentTerminals)
if (toAdd.length === 0 && toRemove.length === 0 && sameAgentWaits(state.agentWaits, serverWaits)) return state
// Remove tabs whose uuids vanished from the server list (the agent closed
// them, or the pty exited and was reaped). Reuse closeTab's leaf cleanup.
@@ -750,6 +758,23 @@ export function reconcileAgentTerminals(
return { ...next, agentWaits: serverWaits }
}
/**
* Mirror ONLY the authoritative agent-wait map from a push — no tab
* add/remove reconciliation. Used while the `terminal` tab type is disabled:
* the tab surface is frozen, but a wait that resolves during that window
* must still clear its banner state, or a re-enabled terminal keeps a stale
* banner/⏳ until some unrelated host event fires the next full reconcile.
* Idempotent: a no-op when the map already matches.
*/
export function mirrorAgentWaits(
state: SidebarState,
agentTerminals: ReadonlyArray<{ uuid: string; title: string; waiting?: { needle: string; since: number } | null }>,
): SidebarState {
const serverWaits = serverWaitsOf(agentTerminals)
if (sameAgentWaits(state.agentWaits, serverWaits)) return state
return { ...state, agentWaits: serverWaits }
}
// ── The per-session store ──────────────────────────────────────────────────
const STORAGE_PREFIX = 'dsh-sidebar:v1'