Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions electron/services/ConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class ConnectionManagerImpl {
private cwds = new Map<string, string>()
/** Recent output per pane, for clients that attach after a pane is already running. */
private scrollback = new Map<string, string>()
/** Geometry reported for a tab whose backend is still being spawned (see resize). */
private pendingGeometry = new Map<string, { cols: number; rows: number }>()
private taps = new Set<(e: TapEvent) => void>()

/** Subscribe to terminal traffic from outside the renderer. Returns an unsubscribe fn. */
Expand Down Expand Up @@ -103,14 +105,22 @@ class ConnectionManagerImpl {

register(tabId: string, backend: TerminalBackend, sessionName: string, host: string | null, sessionId: number | null, owner: number | null = null): void {
const logId = logRepo.start(sessionId, sessionName, host)
// Placeholder geometry — the first client to fit its terminal overwrites it via resize().
this.entries.set(tabId, { backend, logId, sessionName, host, sessionId, owner, cols: 80, rows: 24 })
// Geometry the client already reported while we were still connecting, else a placeholder.
const geo = this.pendingGeometry.get(tabId)
this.pendingGeometry.delete(tabId)
this.entries.set(tabId, { backend, logId, sessionName, host, sessionId, owner, cols: geo?.cols ?? 80, rows: geo?.rows ?? 24 })
if (geo) this.resize(tabId, geo.cols, geo.rows)
}

/** Resize a backend and remember the geometry so other clients can match it. */
resize(tabId: string, cols: number, rows: number): void {
const e = this.entries.get(tabId)
if (!e) return
// Spawning is async, so the client's real geometry usually lands before the backend does.
// Dropping it leaves the shell wrapping at the cols we guessed, which corrupts redraws.
if (!e) {
this.pendingGeometry.set(tabId, { cols, rows })
return
}
e.cols = cols
e.rows = rows
e.backend.resize(cols, rows)
Expand Down Expand Up @@ -206,6 +216,8 @@ class ConnectionManagerImpl {
this.recorders.delete(tabId)
this.cwds.delete(tabId)
this.scrollback.delete(tabId)
// A connection that failed to spawn never consumed its pending geometry.
this.pendingGeometry.delete(tabId)
}

private finishLog(tabId: string, reason: string): void {
Expand Down
52 changes: 40 additions & 12 deletions src/components/settings/UpdateSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,44 @@ import { useUiStore } from '@/store/useUiStore'
import { formatSpeed } from '@/utils/formatBytes'

type UpdateState = 'idle' | 'checking' | 'available' | 'downloading' | 'ready'
function parseNotes(raw: unknown): string[] {

/** One line of a release body: a section label, or a bullet under the label above it. */
export interface ReleaseNote {
text: string
heading: boolean
}

/**
* Release bodies arrive as GitHub-rendered HTML, where a `**Section**` line is a bare <p> and
* only bullets are <li>. Walking both in document order keeps each label with its bullets.
*/
export function parseNotes(raw: unknown): ReleaseNote[] {
const html = Array.isArray(raw)
? raw.map((n: any) => n?.note ?? '').join('\n')
: typeof raw === 'string' ? raw : ''
if (!html.trim()) return []
const body = new DOMParser().parseFromString(html, 'text/html').body
const items = [...body.querySelectorAll('li')].map((li) => li.textContent?.trim() ?? '')
const lines = items.length ? items : (body.textContent ?? '').split('\n')
return lines.map((l) => l.replace(/^[-*•]\s*/, '').trim()).filter(Boolean)
const notes = [...body.querySelectorAll('li, p, h1, h2, h3, h4')]
// A <p> inside an <li> is that bullet's own text (loose lists), else it lands twice.
.filter((el) => el.tagName === 'LI' || !el.closest('li'))
.map((el) => ({
text: (el.textContent ?? '').replace(/^[-*•]\s*/, '').trim(),
heading: el.tagName !== 'LI'
}))
.filter((n) => n.text)
if (notes.some((n) => !n.heading)) return notes
// Nothing list-shaped in there: fall back to plain lines, as before.
return (body.textContent ?? '')
.split('\n')
.map((l) => ({ text: l.replace(/^[-*•]\s*/, '').trim(), heading: false }))
.filter((n) => n.text)
}

export function UpdateSettings() {
const [version, setVersion] = useState('')
const [state, setState] = useState<UpdateState>('idle')
const [newVersion, setNewVersion] = useState<string>('')
const [notes, setNotes] = useState<string[]>([])
const [notes, setNotes] = useState<ReleaseNote[]>([])
const [progress, setProgress] = useState({ percent: 0, speed: 0 })
const notify = useUiStore((s) => s.notify)

Expand Down Expand Up @@ -104,13 +126,19 @@ export function UpdateSettings() {
{notes.length > 0 && state !== 'idle' && (
<div className="rounded-md border border-border bg-surface px-3 py-2.5">
<div className="text-[11px] font-semibold text-text uppercase tracking-wide mb-1.5">{`What's new in v${newVersion}`}</div>
<ul className="space-y-1">
{notes.map((n, i) => (
<li key={i} className="text-[11px] text-muted flex gap-2">
<span className="text-accent">•</span>
<span className="min-w-0">{n}</span>
</li>
))}
<ul className="space-y-1 max-h-52 overflow-y-auto">
{notes.map((n, i) =>
n.heading ? (
<li key={i} className="text-[11px] font-semibold text-text pt-2 first:pt-0">
{n.text}
</li>
) : (
<li key={i} className="text-[11px] text-muted flex gap-2">
<span className="text-accent">•</span>
<span className="min-w-0">{n.text}</span>
</li>
)
)}
</ul>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export function useTerminal(pane: Pane): TerminalController {
return () => {
cancelled = true
}
}, [fontFamily, fontSize, lineHeight, letterSpacing])
}, [fontFamily, fontSize, lineHeight, letterSpacing, pane.id])

return { containerRef, terminal, search, fit, focus, paste, clear }
}
Loading