- go.mod replace directive for git.tukangketik.id/swanadiva/hostkeeper → ../../v1
- store/ package: JSON file persistence for hosts, snippets, keys, settings
(~/Library/Application Support/hostkeeper/v2/)
- model SSH fields: Hostname, Port, Username, AuthType, Password, KeyID
- sshconn/ package: real SSH client via golang.org/x/crypto/ssh
- Terminal WS: real SSH connection with fallback to mock shell
- Dynamic host list in terminal (server-rendered JSON script tag)
- All mock data defaults removed from model packages
- Settings persist via store/settings.go
- New ARCHITECTURE_HTMX.md — comprehensive HTMX architecture doc
- ARCHITECTURE.md — updated to point to HTMX version
- UI_COMPONENTS.md — replaced React tree with HTMX partials
- SPRINT_PLAN.md — all sprint tasks updated for HTMX
- BUILD_SYSTEM.md — simplified (no Vite, no npm)
- TIMELINE.md — 3.5 week HTMX timeline
- PROGRESS.md — updated status + HTMX
- AGENTS.md — updated checkpoint with HTMX decision
- git mv docs/plans/ (V1 planning docs) into v1/docs/plans/
- Create AGENTS.md as AI session checkpoint for seamless continuation
- Root now clean: only V2-related files remain
- Add Theme struct with 3 predefined themes (dark/light/druntime)
- Add Profile struct for named configuration profiles
- Add ErrorBanner with severity levels and auto-dismiss
- Add unit tests for theme, error banner, and models
- Update CHANGELOG.md and PROJECT_STATE.md
- Storage.IsDataEncrypted() checks if hosts.json is encrypted
- TUI auto-detects encrypted files, prompts password automatically
- Root command: --password flag for all CLI commands
- newStorage() helper applies password flag to storage
- add/list/edit/delete commands now support encrypted storage
- passwordSetMsg loads hosts after password is set
- M key enters rename mode with current name pre-filled
- Type new name, Enter to confirm, Esc to cancel
- renameItem: client.Rename() for remote, os.Rename() for local
- Clear cache + refresh after rename
- Footer updated: M:rename
- Filter mode allows M to pass through
- Clear dirCache before refresh after transfer/delete/mkdir
- Cache was stale: old entries returned instead of fresh data
- Filter mode: allow commands (c,d,n,r) and nav (↑↓,Tab) to pass through
- Filter mode no longer blocks transfer/delete/mkdir/pane-switch
- Delete: clear cache + program.Send() for re-render
- Mkdir: clear cache + program.Send() for re-render
- Added program *tea.Program to SFTPBrowserTab and Model
- SetProgram() allows goroutines to send messages to Bubble Tea
- After transfer: program.Send(sftpRefreshMsg{}) triggers re-render
- Model stores program reference, passes to SFTP tab on creation
- Progress bar: [████████░░░░] 67% 12.3MB/18.5MB
- progressWriter wraps io.Writer, reports bytes via callback
- Footer shows live progress during upload/download
- Auto-refresh destination pane after transfer completes
- Success message shown for 1 second before clearing
- Added dirCache map to sftpPane for caching directory listings
- Cache hit: instant navigation (no disk/network I/O)
- Cache miss: read from disk/SFTP, store in cache per path
- R key clears current directory cache and forces fresh read
- Significant speedup when navigating back to previously visited folders
- Default active pane changed from Remote to Local on SFTP init
- renderPane now takes maxH parameter for fixed border height
- paneStyle.Height(maxH) prevents border from shrinking/growing on scroll
- Stacked mode: maxH = t.height - 6; Side-by-side: t.height - 5
Stacked mode (<50 cols) now shows only the active pane (Local/Remote)
with a visual indicator bar. Tab switches between panes. Side-by-side
mode (>=50 cols) unchanged.
Root cause: lipgloss Width(maxW) = content area inside border, border adds 2 chars extra.
- halfW was t.width/2, should be t.width/2-2 (reserve border per pane)
- innerW was maxW-4, should be maxW-2 (padding only, not border)
- Each pane total = maxW + 2 (content + border)
- NEW responsive.go: wrapFooter, adaptiveSidePad, clampWidth, truncateStr
- Footer auto-wraps to multi-line on narrow terminals (full labels preserved)
- Box width clamped to terminal width across all tabs
- Host/key/snippet rows truncated with ellipsis; compact format <35 cols
- SFTP panes stack vertically when terminal < 50 cols
- Tab bar truncates names on overflow
- Form contentW minimum lowered 50->30 for mobile
- Fixed: key_list_tab & snippet_list_tab dropped last data row (off-by-one bug)
- tea.Batch(tea.ClearScreen, sshConnectCmd) before SSH starts so TUI
output doesn't bleed into SSH terminal
- tea.ClearScreen after SSH exits so SSH output doesn't ghost in TUI
- Host list now paginates: only (term_height - 8) / 5 cards shown,
viewport follows selectedIndex with centered scrolling
- Shows 'N of M hosts' indicator when content is clipped
- Added 'e' key alias for editing host (Ctrl+E or E)
- Status bar footer centered within terminal width using lipgloss.Width
- Auth Type field replaced with toggle pills (password/key)
Space/Enter/←/→ cycle between options on the focused field
Selected type shown with SelectedStyle/TagStyle (green),
inactive type shown in muted SubtitleStyle
Help hint 'Space/←/→ to toggle' shown when focused
- Added toggleAuthType() and cycleAuthType() helpers
Bug: t.inputs[t.focus].Update(msg) discarded the returned textinput.Model,
so typed characters were never stored in the input state.
Fix: capture both updated model and cmd from textinput.Update():
t.inputs[t.focus], cmd = t.inputs[t.focus].Update(msg)
return t, cmd
Same fix applied to all 3 forms (host, key, snippet).
Also moved ctrl+s out of default to its own case for clarity.
CRITICAL BUG: sshConnectCmd wrapped tea.ExecProcess inside an extra
closure, so Bubble Tea never executed the SSH process — Enter appeared
to do nothing.
Fix:
- sshConnectCmd now returns tea.ExecProcess directly (no extra closure)
- tea.ExecProcess handles all process lifecycle (start, PTY, exit)
- sshExitMsg errors are shown in host list view (ErrorStyle)
- Error routed via Model.Error → HostListTab.err → View()