diff --git a/web/src/components/terminal.test.tsx b/web/src/components/terminal.test.tsx
index a8a9b53..042248f 100644
--- a/web/src/components/terminal.test.tsx
+++ b/web/src/components/terminal.test.tsx
@@ -348,6 +348,25 @@ describe('Terminal', () => {
vi.useRealTimers()
})
+ it('holds the connecting pill behind a wordless skeleton until the connect drags', async () => {
+ vi.useFakeTimers({ shouldAdvanceTime: true })
+ mountTerminal((e) => )
+ // A fresh mount connects in a blink on a local daemon; words would flash.
+ expect(screen.getByRole('status').textContent).not.toContain('Connecting')
+
+ await act(() => vi.advanceTimersByTimeAsync(2100))
+ expect(screen.getByRole('status').textContent).toContain('Connecting')
+ vi.useRealTimers()
+ })
+
+ it('drops the skeleton the moment the attach lands', () => {
+ const { sock } = mountTerminal((e) => )
+ expect(screen.getByRole('status')).toBeTruthy()
+
+ act(() => sock.emitControl(attached({ ref: 1, id: 's1' })))
+ expect(screen.queryByRole('status')).toBeNull()
+ })
+
it('reports the process exiting, with its code, once', () => {
const { sock, em } = mountTerminal((e) => )
act(() => sock.emitControl(attached({ ref: 1, id: 's1' })))
@@ -1312,9 +1331,14 @@ describe('Terminal', () => {
expect(pane().getAttribute('data-flue-mode')).toBe('tab')
})
- it('offers the shortcut where there is already chrome to put it on', () => {
+ it('offers the shortcut where there is already chrome to put it on', async () => {
+ vi.useFakeTimers({ shouldAdvanceTime: true })
mountTerminal((e) => )
+ // The hint rides the worded pill, which a young connect holds behind
+ // the skeleton — so it, too, waits out the grace.
+ await act(() => vi.advanceTimersByTimeAsync(2100))
expect(screen.getByRole('status').textContent).toContain(TERMINAL_SHORTCUT_HINT)
+ vi.useRealTimers()
})
it('enters focus mode on the shortcut, and takes the key before the terminal sees it', async () => {
diff --git a/web/src/components/terminal.tsx b/web/src/components/terminal.tsx
index cfb3e48..bee1594 100644
--- a/web/src/components/terminal.tsx
+++ b/web/src/components/terminal.tsx
@@ -135,6 +135,9 @@ export const TERMINAL_SHORTCUT_HINT = 'Ctrl+Shift+Enter'
*/
const RESET = new TextEncoder().encode('\x1bc')
+/** How long a connect stays a skeleton before the pill spells it out. */
+const SLOW_CONNECT_MS = 2000
+
const EXIT_NOTICE = (code: number) =>
new TextEncoder().encode(`\r\n\x1b[90m[process exited: ${code}]\x1b[0m\r\n`)
@@ -237,6 +240,20 @@ export function Terminal({
// The daemon's reason, for the pill's second line. Seeded alongside the
// phase and replaced by the event so both mount orders read the same.
const [revokedWhy, setRevokedWhy] = useState(() => client.revoked)
+ // A fresh connect resolves in a blink on a local daemon, and a worded pill
+ // for that blink reads as a flash on every tab switch — the group view
+ // remounts the pane. The pill waits this long behind a wordless skeleton;
+ // a connect still pending by then is genuinely slow and worth the words.
+ // Reconnecting is exempt: an outage is news however briefly it lasts.
+ const [slowConnect, setSlowConnect] = useState(false)
+ useEffect(() => {
+ if (phase !== 'connecting') {
+ setSlowConnect(false)
+ return
+ }
+ const t = setTimeout(() => setSlowConnect(true), SLOW_CONNECT_MS)
+ return () => clearTimeout(t)
+ }, [phase])
const [mode, setMode] = useState('tab')
// Coarse pointer once per mount: whether this device's primary pointer is a
// finger decides the key bar's existence, and a pointer does not change
@@ -1330,7 +1347,17 @@ export function Terminal({
>
)}
- {phase !== 'live' && (
+ {phase === 'connecting' && !slowConnect && (
+ // The young connect's stand-in: a chip-shaped shimmer, sized like
+ // the pill so nothing shifts when the words do arrive. See
+ // slowConnect for why the words wait.
+
+ )}
+ {phase !== 'live' && !(phase === 'connecting' && !slowConnect) && (
// Dark in both themes, like the pane it floats over usually is; the
// translucent ground and backdrop-blur keep it legible over whatever
// the screen underneath was showing. The dot is the phase at a