From c4c856a4508b3cba0fe3d8bfc73cb1c83fd8266a Mon Sep 17 00:00:00 2001 From: HuanLinOTO Date: Wed, 9 Sep 2026 13:37:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(sidebar):=20agent=20=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E7=AD=89=E5=BE=85=20banner=EF=BC=88needle=20+=20=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E7=AD=89=E5=BE=85=E6=8C=89=E9=92=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/TerminalView.tsx | 28 ++++++++++++++ src/client/TerminalWaitBanner.tsx | 32 +++++++++++++++ src/client/api.ts | 4 ++ src/client/locales-ar.ts | 2 + src/client/locales-de.ts | 2 + src/client/locales-fr.ts | 2 + src/client/locales-hi.ts | 2 + src/client/locales-id.ts | 2 + src/client/locales-it.ts | 2 + src/client/locales-ja.ts | 2 + src/client/locales-ko.ts | 2 + src/client/locales-nl.ts | 2 + src/client/locales-pl.ts | 2 + src/client/locales-pt.ts | 2 + src/client/locales-ru.ts | 2 + src/client/locales-sv.ts | 2 + src/client/locales-th.ts | 2 + src/client/locales-tr.ts | 2 + src/client/locales-vi.ts | 2 + src/client/locales-zh-HK.ts | 2 + src/client/locales-zh-MO.ts | 2 + src/client/locales-zh-TW.ts | 2 + src/client/locales.ts | 4 ++ src/client/sidebar.module.css | 23 +++++++++++ tests/terminal-wait-banner.spec.tsx | 60 +++++++++++++++++++++++++++++ 25 files changed, 189 insertions(+) create mode 100644 src/client/TerminalWaitBanner.tsx create mode 100644 tests/terminal-wait-banner.spec.tsx diff --git a/src/client/TerminalView.tsx b/src/client/TerminalView.tsx index ab03083..ff99278 100644 --- a/src/client/TerminalView.tsx +++ b/src/client/TerminalView.tsx @@ -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(null) const [depsFatal, setDepsFatal] = useState(null) const [lastUrl, setLastUrl] = useState(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 (
+ {agentUuid !== null && waiting !== undefined && ( + { void api.agentSkipWait(agentUuid).catch(() => { /* 跳过失败时 banner 留存,可重试 */ }) }} + /> + )} {depsFatal !== null && ( { setDepsFatal(null); connectRef.current?.() }} /> )} diff --git a/src/client/TerminalWaitBanner.tsx b/src/client/TerminalWaitBanner.tsx new file mode 100644 index 0000000..6d0c0fa --- /dev/null +++ b/src/client/TerminalWaitBanner.tsx @@ -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 ( +
+ + {t('terminalWaitBanner', { needle: truncateNeedle(needle) })} + + +
+ ) +} diff --git a/src/client/api.ts b/src/client/api.ts index cf0cacb..677c44b 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -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). */ diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts index cc66eec..de1d0f4 100644 --- a/src/client/locales-ar.ts +++ b/src/client/locales-ar.ts @@ -104,6 +104,8 @@ export const ar: Record = { terminalError: 'فشل اتصال الطرفية', terminalConnectFailed: 'فشل اتصال الطرفية عدة مرات', terminalRetry: 'إعادة المحاولة', + terminalWaitBanner: 'الوكيل ينتظر {needle}', + terminalSkipWait: 'تخطي الانتظار', terminalDepsFailed: 'فشل تحميل تبعية الطرفية node-pty', terminalDepsHint: 'شغّل الأمر التالي في طرفية أو cmd على جهاز DSH للإصلاح، ثم أعد المحاولة (يبقى node-pty متزامناً مع إصدار نواة DSH):', terminalDepsProfile: ' (الملف الشخصي المكتشف: {profile})', diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts index d34b382..2b5b2a9 100644 --- a/src/client/locales-de.ts +++ b/src/client/locales-de.ts @@ -89,6 +89,8 @@ export const de: Record = { 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})', diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts index 567fffd..f1f240c 100644 --- a/src/client/locales-fr.ts +++ b/src/client/locales-fr.ts @@ -96,6 +96,8 @@ export const fr: Record = { 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})', diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts index 3c0ee0b..1710368 100644 --- a/src/client/locales-hi.ts +++ b/src/client/locales-hi.ts @@ -103,6 +103,8 @@ export const hi: Record = { terminalError: 'टर्मिनल कनेक्शन विफल', terminalConnectFailed: 'टर्मिनल बार-बार कनेक्ट विफल', terminalRetry: 'पुनः प्रयास', + terminalWaitBanner: 'एजेंट {needle} की प्रतीक्षा कर रहा है', + terminalSkipWait: 'प्रतीक्षा छोड़ें', terminalDepsFailed: 'टर्मिनल निर्भरता node-pty लोड विफल', terminalDepsHint: 'DSH मशीन पर एक टर्मिनल या cmd में नीचे दिया गया कमांड चलाएँ, फिर पुनः प्रयास करें (node-pty DSH कोर संस्करण के साथ सिंक रहता है):', terminalDepsProfile: ' (पहचाना गया प्रोफ़ाइल: {profile})', diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts index 45a1002..d61b886 100644 --- a/src/client/locales-id.ts +++ b/src/client/locales-id.ts @@ -101,6 +101,8 @@ export const id: Record = { 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})', diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts index a7ab143..8635e2d 100644 --- a/src/client/locales-it.ts +++ b/src/client/locales-it.ts @@ -94,6 +94,8 @@ export const it: Record = { 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})', diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts index a967059..a20f693 100644 --- a/src/client/locales-ja.ts +++ b/src/client/locales-ja.ts @@ -103,6 +103,8 @@ export const ja: Record = { terminalError: 'ターミナル接続に失敗', terminalConnectFailed: 'ターミナル接続が繰り返し失敗しました', terminalRetry: '再試行', + terminalWaitBanner: 'エージェントが {needle} を待機中', + terminalSkipWait: '待機をスキップ', terminalDepsFailed: 'ターミナル依存関係 node-pty の読み込みに失敗', terminalDepsHint: 'DSH 環境のターミナルまたは cmd で以下のコマンドを実行して修復し、再試行してください(node-pty は DSH コアと同じバージョンを維持):', terminalDepsProfile: '(検出された profile:{profile})', diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts index 4e8b4f1..a0907be 100644 --- a/src/client/locales-ko.ts +++ b/src/client/locales-ko.ts @@ -95,6 +95,8 @@ export const ko: Record = { terminalError: '터미널 연결 실패', terminalConnectFailed: '터미널 연결이 여러 번 실패했습니다', terminalRetry: '다시 시도', + terminalWaitBanner: '에이전트가 {needle} 대기 중', + terminalSkipWait: '대기 건너뛰기', terminalDepsFailed: '터미널 의존성 node-pty 로드 실패', terminalDepsHint: 'DSH가 설치된 환경의 터미널 또는 cmd에서 아래 명령을 실행하여 복구한 후 다시 시도를 클릭하세요(node-pty는 DSH 코어와 동일한 버전을 유지합니다):', terminalDepsProfile: ' (profile 감지됨: {profile})', diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts index 8106e22..736fac0 100644 --- a/src/client/locales-nl.ts +++ b/src/client/locales-nl.ts @@ -101,6 +101,8 @@ export const nl: Record = { 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})', diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts index dd62423..4eef5eb 100644 --- a/src/client/locales-pl.ts +++ b/src/client/locales-pl.ts @@ -105,6 +105,8 @@ export const pl: Record = { 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})', diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts index 08c0d25..7197677 100644 --- a/src/client/locales-pt.ts +++ b/src/client/locales-pt.ts @@ -86,6 +86,8 @@ export const pt: Record = { 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})', diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts index 10711b8..86b3b7d 100644 --- a/src/client/locales-ru.ts +++ b/src/client/locales-ru.ts @@ -99,6 +99,8 @@ export const ru: Record = { terminalError: 'Сбой подключения терминала', terminalConnectFailed: 'Терминал не смог подключиться после нескольких попыток', terminalRetry: 'Повторить', + terminalWaitBanner: 'Агент ожидает {needle}', + terminalSkipWait: 'Пропустить ожидание', terminalDepsFailed: 'Не удалось загрузить зависимость терминала node-pty', terminalDepsHint: 'Выполните приведённую ниже команду в терминале или cmd на машине DSH, затем нажмите «Повторить» (node-pty остаётся синхронизированным с версией ядра DSH):', terminalDepsProfile: ' (обнаружен profile: {profile})', diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts index 8c1262f..b8ad6a4 100644 --- a/src/client/locales-sv.ts +++ b/src/client/locales-sv.ts @@ -86,6 +86,8 @@ export const sv: Record = { 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})', diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts index fcdfa5d..820baa8 100644 --- a/src/client/locales-th.ts +++ b/src/client/locales-th.ts @@ -103,6 +103,8 @@ export const th: Record = { terminalError: 'การเชื่อมต่อเทอร์มินัลล้มเหลว', terminalConnectFailed: 'เทอร์มินัลเชื่อมต่อล้มเหลวหลายครั้ง', terminalRetry: 'ลองอีกครั้ง', + terminalWaitBanner: 'เอเจนต์กำลังรอ {needle}', + terminalSkipWait: 'ข้ามการรอ', terminalDepsFailed: 'การอ้างอิงเทอร์มินัล node-pty โหลดล้มเหลว', terminalDepsHint: 'เรียกใช้คำสั่งด้านล่างในเทอร์มินัลหรือ cmd บนเครื่อง DSH เพื่อซ่อมแซม แล้วกดลองอีกครั้ง (node-pty ต้องซิงค์กับเวอร์ชันหลักของ DSH):', terminalDepsProfile: ' (ตรวจพบ profile: {profile})', diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts index f7d015e..843dd67 100644 --- a/src/client/locales-tr.ts +++ b/src/client/locales-tr.ts @@ -103,6 +103,8 @@ export const tr: Record = { 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})', diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts index 11044a8..34e9570 100644 --- a/src/client/locales-vi.ts +++ b/src/client/locales-vi.ts @@ -103,6 +103,8 @@ export const vi: Record = { 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})', diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts index b898c5a..f21ac38 100644 --- a/src/client/locales-zh-HK.ts +++ b/src/client/locales-zh-HK.ts @@ -118,6 +118,8 @@ export const zhHK: Record = { terminalError: '終端連線失敗', terminalConnectFailed: '終端多次連線失敗', terminalRetry: '重試', + terminalWaitBanner: 'Agent 正在等待 {needle}', + terminalSkipWait: '跳過等待', terminalDepsFailed: '終端依賴 node-pty 載入失敗', terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):', terminalDepsProfile: '(偵測到 profile:{profile})', diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts index 52d3e8d..8270392 100644 --- a/src/client/locales-zh-MO.ts +++ b/src/client/locales-zh-MO.ts @@ -118,6 +118,8 @@ export const zhMO: Record = { terminalError: '終端連線失敗', terminalConnectFailed: '終端多次連線失敗', terminalRetry: '重試', + terminalWaitBanner: 'Agent 正在等待 {needle}', + terminalSkipWait: '跳過等待', terminalDepsFailed: '終端依賴 node-pty 載入失敗', terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):', terminalDepsProfile: '(偵測到 profile:{profile})', diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts index 0601580..1acab65 100644 --- a/src/client/locales-zh-TW.ts +++ b/src/client/locales-zh-TW.ts @@ -118,6 +118,8 @@ export const zhTW: Record = { terminalError: '終端連線失敗', terminalConnectFailed: '終端多次連線失敗', terminalRetry: '重試', + terminalWaitBanner: 'Agent 正在等待 {needle}', + terminalSkipWait: '跳過等待', terminalDepsFailed: '終端依賴 node-pty 載入失敗', terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):', terminalDepsProfile: '(偵測到 profile:{profile})', diff --git a/src/client/locales.ts b/src/client/locales.ts index 2c0083e..94f223c 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -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 = { 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})', diff --git a/src/client/sidebar.module.css b/src/client/sidebar.module.css index 3694a6b..eb70711 100644 --- a/src/client/sidebar.module.css +++ b/src/client/sidebar.module.css @@ -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; diff --git a/tests/terminal-wait-banner.spec.tsx b/tests/terminal-wait-banner.spec.tsx new file mode 100644 index 0000000..a34094e --- /dev/null +++ b/tests/terminal-wait-banner.spec.tsx @@ -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) + }) +})