feat(sidebar): agent 终端等待 banner(needle + 跳过等待按钮)
This commit is contained in:
@@ -48,6 +48,7 @@ import {
|
||||
shouldActivateTerminalLink,
|
||||
openTerminalUrl,
|
||||
} from './terminal-links.ts'
|
||||
import { TerminalWaitBanner } from './TerminalWaitBanner.tsx'
|
||||
import css from './sidebar.module.css'
|
||||
|
||||
/** How many consecutive unreasoned failures before showing the error banner. */
|
||||
@@ -123,6 +124,27 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
|
||||
const [fatal, setFatal] = useState<string | null>(null)
|
||||
const [depsFatal, setDepsFatal] = useState<TerminalDepsInfo | null>(null)
|
||||
const [lastUrl, setLastUrl] = useState<string | null>(null)
|
||||
// Agent terminals only: the model's active terminal_wait_for (mirrored
|
||||
// from the host's agent-terminals push into the store) drives the wait
|
||||
// banner. Read + subscribe like the font prefs above; the banner vanishes
|
||||
// when the host's push drops the waiting field (skip / exit / abort all
|
||||
// converge through the same push). getSnapshot() is {sessionId, state?,
|
||||
// prefs} — the state may be briefly undefined around session switches.
|
||||
const agentUuid = isAgentTabId(tabId) ? agentUuidOf(tabId) : null
|
||||
const [waiting, setWaiting] = useState<{ needle: string; since: number } | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
if (agentUuid === null) return
|
||||
const read = (): void => {
|
||||
const next = store.getSnapshot().state?.agentWaits?.[agentUuid]
|
||||
setWaiting(prev => {
|
||||
const nextValue = next === undefined ? undefined : { needle: next.needle, since: next.since }
|
||||
if (prev?.needle === nextValue?.needle && prev?.since === nextValue?.since) return prev
|
||||
return nextValue
|
||||
})
|
||||
}
|
||||
read()
|
||||
return store.subscribe(read)
|
||||
}, [agentUuid, store])
|
||||
const connectRef = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -386,6 +408,12 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
|
||||
|
||||
return (
|
||||
<div className={css.terminalWrap}>
|
||||
{agentUuid !== null && waiting !== undefined && (
|
||||
<TerminalWaitBanner
|
||||
needle={waiting.needle}
|
||||
onSkip={() => { void api.agentSkipWait(agentUuid).catch(() => { /* 跳过失败时 banner 留存,可重试 */ }) }}
|
||||
/>
|
||||
)}
|
||||
{depsFatal !== null && (
|
||||
<TerminalDepsBanner deps={depsFatal} onRetry={() => { setDepsFatal(null); connectRef.current?.() }} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* The "Agent 正在等待 {needle}" wait banner: rendered at the top of an
|
||||
* agent-owned terminal's view while the model blocks in terminal_wait_for.
|
||||
* The skip button asks the host to abort every active wait on the terminal
|
||||
* (`agent-pty.skip-wait`); the banner disappears when the host's next
|
||||
* agent-terminals push drops the waiting field — no optimistic UI. Kept in
|
||||
* its own module (no xterm imports) so jsdom tests can render it directly.
|
||||
*/
|
||||
import { t } from './locales.ts'
|
||||
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). */
|
||||
export function truncateNeedle(needle: string): string {
|
||||
return needle.length > NEEDLE_DISPLAY_CAP ? `${needle.slice(0, NEEDLE_DISPLAY_CAP - 1)}…` : needle
|
||||
}
|
||||
|
||||
export function TerminalWaitBanner(props: { needle: string; onSkip: () => void }) {
|
||||
const { needle, onSkip } = props
|
||||
return (
|
||||
<div className={css.terminalWaitBanner}>
|
||||
<span className={css.terminalWaitNeedle} title={needle}>
|
||||
{t('terminalWaitBanner', { needle: truncateNeedle(needle) })}
|
||||
</span>
|
||||
<button type="button" className={css.terminalRetry} onClick={onSkip}>
|
||||
{t('terminalSkipWait')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -346,6 +346,10 @@ export const api = {
|
||||
/** Release an agent terminal by uuid (tab closed while WS was down). */
|
||||
agentPtyClose: (uuid: string) =>
|
||||
call<{ ok: true }>('agent-pty.close', { uuid }),
|
||||
/** Skip every active terminal_wait_for on one agent terminal (the wait
|
||||
* banner's skip button). Idempotent: {skipped:0} when none is active. */
|
||||
agentSkipWait: (uuid: string) =>
|
||||
call<{ ok: true; skipped: number }>('agent-pty.skip-wait', { uuid }),
|
||||
/** Terminal dependency status (issue #140): after a WS close 1011 with
|
||||
* reason `pty-deps-missing` the view fetches the full repair details here
|
||||
* (the close reason itself is capped at 123 bytes). */
|
||||
|
||||
@@ -104,6 +104,8 @@ export const ar: Record<string, string> = {
|
||||
terminalError: 'فشل اتصال الطرفية',
|
||||
terminalConnectFailed: 'فشل اتصال الطرفية عدة مرات',
|
||||
terminalRetry: 'إعادة المحاولة',
|
||||
terminalWaitBanner: 'الوكيل ينتظر {needle}',
|
||||
terminalSkipWait: 'تخطي الانتظار',
|
||||
terminalDepsFailed: 'فشل تحميل تبعية الطرفية node-pty',
|
||||
terminalDepsHint: 'شغّل الأمر التالي في طرفية أو cmd على جهاز DSH للإصلاح، ثم أعد المحاولة (يبقى node-pty متزامناً مع إصدار نواة DSH):',
|
||||
terminalDepsProfile: ' (الملف الشخصي المكتشف: {profile})',
|
||||
|
||||
@@ -89,6 +89,8 @@ export const de: Record<string, string> = {
|
||||
terminalError: 'Terminalverbindung fehlgeschlagen',
|
||||
terminalConnectFailed: 'Terminal konnte wiederholt keine Verbindung herstellen',
|
||||
terminalRetry: 'Erneut versuchen',
|
||||
terminalWaitBanner: 'Agent wartet auf {needle}',
|
||||
terminalSkipWait: 'Warten überspringen',
|
||||
terminalDepsFailed: 'Die Terminal-Abhängigkeit node-pty konnte nicht geladen werden',
|
||||
terminalDepsHint: 'Führen Sie den folgenden Befehl in einem Terminal oder in cmd auf dem DSH-System aus, um dies zu beheben, und klicken Sie dann auf „Erneut versuchen“ (node-pty bleibt mit der DSH-Core-Version synchron):',
|
||||
terminalDepsProfile: ' (erkanntes Profil: {profile})',
|
||||
|
||||
@@ -96,6 +96,8 @@ export const fr: Record<string, string> = {
|
||||
terminalError: 'Échec de la connexion au terminal',
|
||||
terminalConnectFailed: 'Le terminal a échoué à se connecter à plusieurs reprises',
|
||||
terminalRetry: 'Réessayer',
|
||||
terminalWaitBanner: 'L’agent attend {needle}',
|
||||
terminalSkipWait: 'Ignorer l’attente',
|
||||
terminalDepsFailed: 'Échec du chargement de la dépendance terminal node-pty',
|
||||
terminalDepsHint: 'Exécutez la commande suivante dans un terminal ou cmd de l’environnement DSH pour réparer, puis cliquez sur Réessayer (node-pty reste à la même version que le cœur DSH) :',
|
||||
terminalDepsProfile: ' (profil détecté : {profile})',
|
||||
|
||||
@@ -103,6 +103,8 @@ export const hi: Record<string, string> = {
|
||||
terminalError: 'टर्मिनल कनेक्शन विफल',
|
||||
terminalConnectFailed: 'टर्मिनल बार-बार कनेक्ट विफल',
|
||||
terminalRetry: 'पुनः प्रयास',
|
||||
terminalWaitBanner: 'एजेंट {needle} की प्रतीक्षा कर रहा है',
|
||||
terminalSkipWait: 'प्रतीक्षा छोड़ें',
|
||||
terminalDepsFailed: 'टर्मिनल निर्भरता node-pty लोड विफल',
|
||||
terminalDepsHint: 'DSH मशीन पर एक टर्मिनल या cmd में नीचे दिया गया कमांड चलाएँ, फिर पुनः प्रयास करें (node-pty DSH कोर संस्करण के साथ सिंक रहता है):',
|
||||
terminalDepsProfile: ' (पहचाना गया प्रोफ़ाइल: {profile})',
|
||||
|
||||
@@ -101,6 +101,8 @@ export const id: Record<string, string> = {
|
||||
terminalError: 'Koneksi terminal gagal',
|
||||
terminalConnectFailed: 'Terminal gagal terhubung berulang kali',
|
||||
terminalRetry: 'Coba lagi',
|
||||
terminalWaitBanner: 'Agen menunggu {needle}',
|
||||
terminalSkipWait: 'Lewati penungguan',
|
||||
terminalDepsFailed: 'Dependensi terminal node-pty gagal dimuat',
|
||||
terminalDepsHint: 'Jalankan perintah di bawah ini di terminal atau cmd pada mesin DSH untuk memperbaikinya, lalu coba lagi (node-pty tetap sinkron dengan versi inti DSH):',
|
||||
terminalDepsProfile: ' (profil terdeteksi: {profile})',
|
||||
|
||||
@@ -94,6 +94,8 @@ export const it: Record<string, string> = {
|
||||
terminalError: 'Connessione del terminale non riuscita',
|
||||
terminalConnectFailed: 'Il terminale non è riuscito a connettersi più volte',
|
||||
terminalRetry: 'Riprova',
|
||||
terminalWaitBanner: 'L’agente è in attesa di {needle}',
|
||||
terminalSkipWait: 'Salta attesa',
|
||||
terminalDepsFailed: 'Caricamento della dipendenza del terminale node-pty non riuscito',
|
||||
terminalDepsHint: 'Esegua il comando seguente in un terminale o cmd sul computer DSH per ripristinarlo, poi riprovi (node-pty resta sincronizzato con la versione del core DSH):',
|
||||
terminalDepsProfile: ' (rilevato profilo: {profile})',
|
||||
|
||||
@@ -103,6 +103,8 @@ export const ja: Record<string, string> = {
|
||||
terminalError: 'ターミナル接続に失敗',
|
||||
terminalConnectFailed: 'ターミナル接続が繰り返し失敗しました',
|
||||
terminalRetry: '再試行',
|
||||
terminalWaitBanner: 'エージェントが {needle} を待機中',
|
||||
terminalSkipWait: '待機をスキップ',
|
||||
terminalDepsFailed: 'ターミナル依存関係 node-pty の読み込みに失敗',
|
||||
terminalDepsHint: 'DSH 環境のターミナルまたは cmd で以下のコマンドを実行して修復し、再試行してください(node-pty は DSH コアと同じバージョンを維持):',
|
||||
terminalDepsProfile: '(検出された profile:{profile})',
|
||||
|
||||
@@ -95,6 +95,8 @@ export const ko: Record<string, string> = {
|
||||
terminalError: '터미널 연결 실패',
|
||||
terminalConnectFailed: '터미널 연결이 여러 번 실패했습니다',
|
||||
terminalRetry: '다시 시도',
|
||||
terminalWaitBanner: '에이전트가 {needle} 대기 중',
|
||||
terminalSkipWait: '대기 건너뛰기',
|
||||
terminalDepsFailed: '터미널 의존성 node-pty 로드 실패',
|
||||
terminalDepsHint: 'DSH가 설치된 환경의 터미널 또는 cmd에서 아래 명령을 실행하여 복구한 후 다시 시도를 클릭하세요(node-pty는 DSH 코어와 동일한 버전을 유지합니다):',
|
||||
terminalDepsProfile: ' (profile 감지됨: {profile})',
|
||||
|
||||
@@ -101,6 +101,8 @@ export const nl: Record<string, string> = {
|
||||
terminalError: 'Terminalverbinding mislukt',
|
||||
terminalConnectFailed: 'Terminalverbinding herhaaldelijk mislukt',
|
||||
terminalRetry: 'Opnieuw proberen',
|
||||
terminalWaitBanner: 'Agent wacht op {needle}',
|
||||
terminalSkipWait: 'Wachten overslaan',
|
||||
terminalDepsFailed: 'Terminalafhankelijkheid node-pty kon niet worden geladen',
|
||||
terminalDepsHint: 'Voer het onderstaande commando uit in een terminal of cmd op de DSH-machine om het te herstellen en probeer het opnieuw (node-pty blijft in sync met de DSH-coreversie):',
|
||||
terminalDepsProfile: ' (gedetecteerde profile: {profile})',
|
||||
|
||||
@@ -105,6 +105,8 @@ export const pl: Record<string, string> = {
|
||||
terminalError: 'Połączenie z terminalem nie powiodło się',
|
||||
terminalConnectFailed: 'Terminal nie mógł połączyć się wielokrotnie',
|
||||
terminalRetry: 'Ponów',
|
||||
terminalWaitBanner: 'Agent czeka na {needle}',
|
||||
terminalSkipWait: 'Pomiń oczekiwanie',
|
||||
terminalDepsFailed: 'Zależność terminala node-pty nie mogła się załadować',
|
||||
terminalDepsHint: 'Uruchom poniższe polecenie w terminalu lub cmd na maszynie DSH, aby to naprawić, a następnie ponów (node-pty pozostaje w synchronizacji z wersją rdzenia DSH):',
|
||||
terminalDepsProfile: ' (wykryty profil: {profile})',
|
||||
|
||||
@@ -86,6 +86,8 @@ export const pt: Record<string, string> = {
|
||||
terminalError: 'Falha na conexão do terminal',
|
||||
terminalConnectFailed: 'O terminal falhou ao conectar repetidamente',
|
||||
terminalRetry: 'Tentar novamente',
|
||||
terminalWaitBanner: 'O agente está aguardando {needle}',
|
||||
terminalSkipWait: 'Pular espera',
|
||||
terminalDepsFailed: 'A dependência do terminal node-pty falhou ao carregar',
|
||||
terminalDepsHint: 'Execute o comando abaixo em um terminal ou cmd na máquina do DSH para reparar e tente novamente (node-pty permanece em sincronia com a versão do núcleo do DSH):',
|
||||
terminalDepsProfile: ' (perfil detectado: {profile})',
|
||||
|
||||
@@ -99,6 +99,8 @@ export const ru: Record<string, string> = {
|
||||
terminalError: 'Сбой подключения терминала',
|
||||
terminalConnectFailed: 'Терминал не смог подключиться после нескольких попыток',
|
||||
terminalRetry: 'Повторить',
|
||||
terminalWaitBanner: 'Агент ожидает {needle}',
|
||||
terminalSkipWait: 'Пропустить ожидание',
|
||||
terminalDepsFailed: 'Не удалось загрузить зависимость терминала node-pty',
|
||||
terminalDepsHint: 'Выполните приведённую ниже команду в терминале или cmd на машине DSH, затем нажмите «Повторить» (node-pty остаётся синхронизированным с версией ядра DSH):',
|
||||
terminalDepsProfile: ' (обнаружен profile: {profile})',
|
||||
|
||||
@@ -86,6 +86,8 @@ export const sv: Record<string, string> = {
|
||||
terminalError: 'Terminalanslutning misslyckades',
|
||||
terminalConnectFailed: 'Terminal misslyckades att ansluta upprepade gånger',
|
||||
terminalRetry: 'Försök igen',
|
||||
terminalWaitBanner: 'Agenten väntar på {needle}',
|
||||
terminalSkipWait: 'Hoppa över väntan',
|
||||
terminalDepsFailed: 'Terminalberoendet node-pty kunde inte laddas',
|
||||
terminalDepsHint: 'Kör kommandot nedan i en terminal eller cmd på DSH-maskinen för att reparera det, försök sedan igen (node-pty hålls synkad med DSH-kärnversionen):',
|
||||
terminalDepsProfile: ' (upptäckt profil: {profile})',
|
||||
|
||||
@@ -103,6 +103,8 @@ export const th: Record<string, string> = {
|
||||
terminalError: 'การเชื่อมต่อเทอร์มินัลล้มเหลว',
|
||||
terminalConnectFailed: 'เทอร์มินัลเชื่อมต่อล้มเหลวหลายครั้ง',
|
||||
terminalRetry: 'ลองอีกครั้ง',
|
||||
terminalWaitBanner: 'เอเจนต์กำลังรอ {needle}',
|
||||
terminalSkipWait: 'ข้ามการรอ',
|
||||
terminalDepsFailed: 'การอ้างอิงเทอร์มินัล node-pty โหลดล้มเหลว',
|
||||
terminalDepsHint: 'เรียกใช้คำสั่งด้านล่างในเทอร์มินัลหรือ cmd บนเครื่อง DSH เพื่อซ่อมแซม แล้วกดลองอีกครั้ง (node-pty ต้องซิงค์กับเวอร์ชันหลักของ DSH):',
|
||||
terminalDepsProfile: ' (ตรวจพบ profile: {profile})',
|
||||
|
||||
@@ -103,6 +103,8 @@ export const tr: Record<string, string> = {
|
||||
terminalError: 'Terminal bağlantısı başarısız',
|
||||
terminalConnectFailed: 'Terminal art arda bağlanamadı',
|
||||
terminalRetry: 'Yeniden dene',
|
||||
terminalWaitBanner: 'Ajan {needle} bekliyor',
|
||||
terminalSkipWait: 'Beklemeyi atla',
|
||||
terminalDepsFailed: 'Terminal bağımlılığı node-pty yüklenemedi',
|
||||
terminalDepsHint: 'Onarmak için DSH makinesindeki bir terminalde veya cmd’de aşağıdaki komutu çalıştırın, sonra yeniden deneyin (node-pty, DSH çekirdek sürümüyle senkron kalır):',
|
||||
terminalDepsProfile: ' (algılanan profil: {profile})',
|
||||
|
||||
@@ -103,6 +103,8 @@ export const vi: Record<string, string> = {
|
||||
terminalError: 'Kết nối terminal thất bại',
|
||||
terminalConnectFailed: 'Terminal kết nối thất bại nhiều lần',
|
||||
terminalRetry: 'Thử lại',
|
||||
terminalWaitBanner: 'Agent đang chờ {needle}',
|
||||
terminalSkipWait: 'Bỏ qua thời gian chờ',
|
||||
terminalDepsFailed: 'Dependency terminal node-pty tải thất bại',
|
||||
terminalDepsHint: 'Chạy lệnh sau trong terminal hoặc cmd trên máy DSH để sửa, rồi nhấn thử lại (node-pty đồng bộ phiên bản với lõi DSH):',
|
||||
terminalDepsProfile: ' (phát hiện profile: {profile})',
|
||||
|
||||
@@ -118,6 +118,8 @@ export const zhHK: Record<string, string> = {
|
||||
terminalError: '終端連線失敗',
|
||||
terminalConnectFailed: '終端多次連線失敗',
|
||||
terminalRetry: '重試',
|
||||
terminalWaitBanner: 'Agent 正在等待 {needle}',
|
||||
terminalSkipWait: '跳過等待',
|
||||
terminalDepsFailed: '終端依賴 node-pty 載入失敗',
|
||||
terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):',
|
||||
terminalDepsProfile: '(偵測到 profile:{profile})',
|
||||
|
||||
@@ -118,6 +118,8 @@ export const zhMO: Record<string, string> = {
|
||||
terminalError: '終端連線失敗',
|
||||
terminalConnectFailed: '終端多次連線失敗',
|
||||
terminalRetry: '重試',
|
||||
terminalWaitBanner: 'Agent 正在等待 {needle}',
|
||||
terminalSkipWait: '跳過等待',
|
||||
terminalDepsFailed: '終端依賴 node-pty 載入失敗',
|
||||
terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):',
|
||||
terminalDepsProfile: '(偵測到 profile:{profile})',
|
||||
|
||||
@@ -118,6 +118,8 @@ export const zhTW: Record<string, string> = {
|
||||
terminalError: '終端連線失敗',
|
||||
terminalConnectFailed: '終端多次連線失敗',
|
||||
terminalRetry: '重試',
|
||||
terminalWaitBanner: 'Agent 正在等待 {needle}',
|
||||
terminalSkipWait: '跳過等待',
|
||||
terminalDepsFailed: '終端依賴 node-pty 載入失敗',
|
||||
terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):',
|
||||
terminalDepsProfile: '(偵測到 profile:{profile})',
|
||||
|
||||
@@ -115,6 +115,8 @@ export const zh = {
|
||||
terminalError: '终端连接失败',
|
||||
terminalConnectFailed: '终端多次连接失败',
|
||||
terminalRetry: '重试',
|
||||
terminalWaitBanner: 'Agent 正在等待 {needle}',
|
||||
terminalSkipWait: '跳过等待',
|
||||
terminalDepsFailed: '终端依赖 node-pty 加载失败',
|
||||
terminalDepsHint: '在 DSH 所在环境的终端或 cmd 中执行以下命令修复,然后点重试(node-pty 与 DSH 核心保持同一版本):',
|
||||
terminalDepsProfile: '(检测到 profile:{profile})',
|
||||
@@ -565,6 +567,8 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
terminalError: 'Terminal connection failed',
|
||||
terminalConnectFailed: 'Terminal failed to connect repeatedly',
|
||||
terminalRetry: 'Retry',
|
||||
terminalWaitBanner: 'Agent is waiting for {needle}',
|
||||
terminalSkipWait: 'Skip wait',
|
||||
terminalDepsFailed: 'Terminal dependency node-pty failed to load',
|
||||
terminalDepsHint: 'Run the command below in a terminal or cmd on the DSH machine to repair it, then retry (node-pty stays in sync with the DSH core version):',
|
||||
terminalDepsProfile: ' (detected profile: {profile})',
|
||||
|
||||
@@ -2201,6 +2201,29 @@ body[data-dsh-sidebar-dragging] .bottomPanel {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* The "Agent 正在等待 {needle}" wait banner: a warn-toned strip above the
|
||||
terminal surface while the model blocks in terminal_wait_for; the needle
|
||||
ellipsizes inline and the full text rides the title tooltip. */
|
||||
.terminalWaitBanner {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 3px 10px;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-state-warn-label);
|
||||
background: var(--dsw-alias-state-warn-tertiary);
|
||||
}
|
||||
|
||||
.terminalWaitNeedle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.boundaryError {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* TerminalWaitBanner unit tests: localized text with the needle, the skip
|
||||
* button wiring, and the long-needle display cap (full text stays on the
|
||||
* title attribute). The component imports no xterm — renderable in plain
|
||||
* jsdom (the repo's jsdom pattern, see bottom-auto-terminal.spec.tsx head).
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { TerminalWaitBanner } from '../src/client/TerminalWaitBanner.tsx'
|
||||
|
||||
describe('TerminalWaitBanner', () => {
|
||||
const mounted: Array<{ root: Root; container: HTMLDivElement }> = []
|
||||
function mount(needle: string, onSkip: () => void): HTMLDivElement {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root = createRoot(container)
|
||||
act(() => { root.render(createElement(TerminalWaitBanner, { needle, onSkip })) })
|
||||
mounted.push({ root, container })
|
||||
return container
|
||||
}
|
||||
afterEach(() => {
|
||||
for (const { root, container } of mounted.splice(0)) {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows the localized waiting text with the needle and a working skip button', () => {
|
||||
let skipped = 0
|
||||
const container = mount('BUILD_OK', () => { skipped += 1 })
|
||||
// jsdom navigator.language is en-US → the en dictionary applies.
|
||||
expect(container.textContent).toContain('Agent is waiting for')
|
||||
expect(container.textContent).toContain('BUILD_OK')
|
||||
expect(container.textContent).toContain('Skip wait')
|
||||
const button = container.querySelector('button')
|
||||
expect(button).not.toBeNull()
|
||||
act(() => { button!.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
|
||||
expect(skipped).toBe(1)
|
||||
})
|
||||
|
||||
it('truncates a long needle for display but keeps the full text on the title', () => {
|
||||
const long = 'X'.repeat(200)
|
||||
const container = mount(long, () => {})
|
||||
const span = container.querySelector('span[title]')
|
||||
expect(span).not.toBeNull()
|
||||
expect(span!.getAttribute('title')).toBe(long)
|
||||
// The needle's inline contribution is capped (NEEDLE_DISPLAY_CAP): at
|
||||
// most 80 X's are rendered — the full 200-char run only rides the title
|
||||
// tooltip. The localized sentence around the needle is not measured;
|
||||
// its length varies per language, so counting the needle's own chars is
|
||||
// the language-agnostic form of the display-cap assertion.
|
||||
expect((span!.textContent?.match(/X/g) ?? []).length).toBeLessThanOrEqual(80)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user