feat(auth,tui): dark login pages + /logout picker
Two unrelated UX improvements bundled:
1. Login pages (all of them) now use the TUI-matching dark style.
Swapped the shared monoStyle from white/black to:
- background #0a0a0a
- white body text
- Geist Mono via Google Fonts @import
- accent #7ed3fc on every occurrence of the word "zot"
Applies to: /apikey index, /apikey form, api-key success, oauth
success, oauth error. The three pages that were still white
(index, form, error) now match the TUI's dark look end-to-end.
Input focus ring and button hover flipped to white-on-dark.
2. /logout without an argument opens a picker.
New logout_dialog.go modelled on the existing small-list dialogs
(model picker shape, session picker size). Lists only the
providers the user is actually logged into, each with an
(apikey) or (oauth) tag. When both are logged in, an extra
"all" row is appended. When nothing is stored, /logout reports
"no credentials stored; already logged out" and doesn't open
an empty dialog.
/logout anthropic, /logout openai, /logout all still work
exactly as before (direct, no dialog).
Also includes the user's earlier edit to defaultIdentity:
"operating inside zot, a coding agent harness" rewording.- commit
- f8c309e3ca6187e5b0d4729556ee8fd533a8957d
- author
- patriceckhart · · 2026-04-19 18:14 UTC
- parent
- e1c1e0e
- signature
- unsigned
- refs
- tag: v0.0.49
5 files · +219 -32
M internal/agent/modes/interactive.go +81 -4
| @@ -176,6 +176,7 @@ type Interactive struct { | |||
| 176 | 176 | skillsDialog *skillsDialog | |
| 177 | 177 | changelogDialog *changelogDialog | |
| 178 | 178 | confirmDialog *confirmDialog | |
| 179 | + | logoutDialog *logoutDialog | |
| 179 | 180 | suggest *slashSuggester | |
| 180 | 181 | spin *spinner | |
| 181 | 182 | ||
| @@ -227,6 +228,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 227 | 228 | skillsDialog: newSkillsDialog(), | |
| 228 | 229 | changelogDialog: newChangelogDialog(), | |
| 229 | 230 | confirmDialog: newConfirmDialog(), | |
| 231 | + | logoutDialog: newLogoutDialog(), | |
| 230 | 232 | suggest: newSlashSuggester(), | |
| 231 | 233 | spin: newSpinner(), | |
| 232 | 234 | } | |
| @@ -416,7 +418,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||
| 416 | 418 | // and the AfterFunc-driven invalidate got dropped on a | |
| 417 | 419 | // full channel. | |
| 418 | 420 | drainPending() | |
| 419 | - | if i.busy || i.dialog.Active() || i.modelDialog.Active() || i.sessionDialog.Active() || i.jumpDialog.Active() || i.btwDialog.Active() || i.skillsDialog.Active() || i.changelogDialog.Active() || i.confirmDialog.Active() { | |
| 421 | + | if i.busy || i.dialog.Active() || i.modelDialog.Active() || i.sessionDialog.Active() || i.jumpDialog.Active() || i.btwDialog.Active() || i.skillsDialog.Active() || i.changelogDialog.Active() || i.confirmDialog.Active() || i.logoutDialog.Active() { | |
| 420 | 422 | requestRedraw() // keep the spinner / dialog animation moving | |
| 421 | 423 | } | |
| 422 | 424 | } | |
| @@ -576,6 +578,8 @@ func (i *Interactive) redraw() { | |||
| 576 | 578 | dialog = i.changelogDialog.Render(i.cfg.Theme, cols) | |
| 577 | 579 | case i.confirmDialog.Active(): | |
| 578 | 580 | dialog = i.confirmDialog.Render(i.cfg.Theme, cols) | |
| 581 | + | case i.logoutDialog.Active(): | |
| 582 | + | dialog = i.logoutDialog.Render(i.cfg.Theme, cols) | |
| 579 | 583 | } | |
| 580 | 584 | ||
| 581 | 585 | // Slash-command autocomplete: popup above the status line, only | |
| @@ -822,6 +826,19 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||
| 822 | 826 | } | |
| 823 | 827 | return false | |
| 824 | 828 | } | |
| 829 | + | if i.logoutDialog.Active() { | |
| 830 | + | if k.Kind == tui.KeyCtrlC { | |
| 831 | + | i.logoutDialog.Close() | |
| 832 | + | i.invalidate() | |
| 833 | + | return false | |
| 834 | + | } | |
| 835 | + | act := i.logoutDialog.HandleKey(k) | |
| 836 | + | if act.Select { | |
| 837 | + | i.doLogout(act.Target) | |
| 838 | + | } | |
| 839 | + | i.invalidate() | |
| 840 | + | return false | |
| 841 | + | } | |
| 825 | 842 | if i.jumpDialog.Active() { | |
| 826 | 843 | if k.Kind == tui.KeyCtrlC { | |
| 827 | 844 | i.jumpDialog.Close() | |
| @@ -1192,11 +1209,15 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) { | |||
| 1192 | 1209 | case "/login": | |
| 1193 | 1210 | i.dialog.Open() | |
| 1194 | 1211 | case "/logout": | |
| 1195 | - | target := "all" | |
| 1196 | 1212 | if len(parts) >= 2 { | |
| 1197 | - | target = parts[1] | |
| 1213 | + | // Explicit target: /logout anthropic | openai | all | |
| 1214 | + | i.doLogout(parts[1]) | |
| 1215 | + | break | |
| 1198 | 1216 | } | |
| 1199 | - | i.doLogout(target) | |
| 1217 | + | // No arg: open the picker over whichever providers are | |
| 1218 | + | // currently logged in. If nothing's logged in, bail with a | |
| 1219 | + | // status line. | |
| 1220 | + | i.openLogoutDialog() | |
| 1200 | 1221 | case "/model": | |
| 1201 | 1222 | if len(parts) >= 2 { | |
| 1202 | 1223 | i.applyModelSelection("", parts[1]) | |
| @@ -1263,6 +1284,62 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) { | |||
| 1263 | 1284 | return false | |
| 1264 | 1285 | } | |
| 1265 | 1286 | ||
| 1287 | + | // openLogoutDialog shows the provider picker for `/logout` with no | |
| 1288 | + | // argument. Only providers the user is currently logged into are | |
| 1289 | + | // listed, plus an "all" entry when more than one is present. If | |
| 1290 | + | // nothing's logged in, writes a status line instead of opening an | |
| 1291 | + | // empty dialog. | |
| 1292 | + | func (i *Interactive) openLogoutDialog() { | |
| 1293 | + | if i.cfg.AuthManager == nil { | |
| 1294 | + | i.mu.Lock() | |
| 1295 | + | i.statusErr = "no auth manager configured" | |
| 1296 | + | i.mu.Unlock() | |
| 1297 | + | i.invalidate() | |
| 1298 | + | return | |
| 1299 | + | } | |
| 1300 | + | store := i.cfg.AuthManager.Store() | |
| 1301 | + | if store == nil { | |
| 1302 | + | i.mu.Lock() | |
| 1303 | + | i.statusErr = "auth store is not available" | |
| 1304 | + | i.mu.Unlock() | |
| 1305 | + | i.invalidate() | |
| 1306 | + | return | |
| 1307 | + | } | |
| 1308 | + | creds, err := store.Load() | |
| 1309 | + | if err != nil { | |
| 1310 | + | i.mu.Lock() | |
| 1311 | + | i.statusErr = "read auth store: " + err.Error() | |
| 1312 | + | i.mu.Unlock() | |
| 1313 | + | i.invalidate() | |
| 1314 | + | return | |
| 1315 | + | } | |
| 1316 | + | ||
| 1317 | + | var items []logoutItem | |
| 1318 | + | for _, p := range []string{"anthropic", "openai"} { | |
| 1319 | + | if creds.Has(p) { | |
| 1320 | + | items = append(items, logoutItem{ | |
| 1321 | + | label: p, | |
| 1322 | + | target: p, | |
| 1323 | + | method: creds.Method(p), | |
| 1324 | + | }) | |
| 1325 | + | } | |
| 1326 | + | } | |
| 1327 | + | if len(items) == 0 { | |
| 1328 | + | i.mu.Lock() | |
| 1329 | + | i.statusOK = "no credentials stored; already logged out" | |
| 1330 | + | i.statusErr = "" | |
| 1331 | + | i.mu.Unlock() | |
| 1332 | + | i.invalidate() | |
| 1333 | + | return | |
| 1334 | + | } | |
| 1335 | + | if len(items) > 1 { | |
| 1336 | + | items = append(items, logoutItem{label: "all", target: "all"}) | |
| 1337 | + | } | |
| 1338 | + | ||
| 1339 | + | i.logoutDialog.Open(items) | |
| 1340 | + | i.invalidate() | |
| 1341 | + | } | |
| 1342 | + | ||
| 1266 | 1343 | // doLogout clears credentials for the given provider (or all providers) | |
| 1267 | 1344 | // from auth.json. If the active agent was using those credentials, it | |
| 1268 | 1345 | // is torn down so the user is forced through /login before their next | |
| @@ -176,6 +176,7 @@ type Interactive struct { | |||||
| 176 | skillsDialog *skillsDialog | 176 | skillsDialog *skillsDialog | ||
| 177 | changelogDialog *changelogDialog | 177 | changelogDialog *changelogDialog | ||
| 178 | confirmDialog *confirmDialog | 178 | confirmDialog *confirmDialog | ||
| 179 | + | logoutDialog *logoutDialog | |||
| 179 | suggest *slashSuggester | 180 | suggest *slashSuggester | ||
| 180 | spin *spinner | 181 | spin *spinner | ||
| 181 | 182 | ||||
| @@ -227,6 +228,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 227 | skillsDialog: newSkillsDialog(), | 228 | skillsDialog: newSkillsDialog(), | ||
| 228 | changelogDialog: newChangelogDialog(), | 229 | changelogDialog: newChangelogDialog(), | ||
| 229 | confirmDialog: newConfirmDialog(), | 230 | confirmDialog: newConfirmDialog(), | ||
| 231 | + | logoutDialog: newLogoutDialog(), | |||
| 230 | suggest: newSlashSuggester(), | 232 | suggest: newSlashSuggester(), | ||
| 231 | spin: newSpinner(), | 233 | spin: newSpinner(), | ||
| 232 | } | 234 | } | ||
| @@ -416,7 +418,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||||
| 416 | // and the AfterFunc-driven invalidate got dropped on a | 418 | // and the AfterFunc-driven invalidate got dropped on a | ||
| 417 | // full channel. | 419 | // full channel. | ||
| 418 | drainPending() | 420 | drainPending() | ||
| 419 | - | if i.busy || i.dialog.Active() || i.modelDialog.Active() || i.sessionDialog.Active() || i.jumpDialog.Active() || i.btwDialog.Active() || i.skillsDialog.Active() || i.changelogDialog.Active() || i.confirmDialog.Active() { | 421 | + | if i.busy || i.dialog.Active() || i.modelDialog.Active() || i.sessionDialog.Active() || i.jumpDialog.Active() || i.btwDialog.Active() || i.skillsDialog.Active() || i.changelogDialog.Active() || i.confirmDialog.Active() || i.logoutDialog.Active() { |
| 420 | requestRedraw() // keep the spinner / dialog animation moving | 422 | requestRedraw() // keep the spinner / dialog animation moving | ||
| 421 | } | 423 | } | ||
| 422 | } | 424 | } | ||
| @@ -576,6 +578,8 @@ func (i *Interactive) redraw() { | |||||
| 576 | dialog = i.changelogDialog.Render(i.cfg.Theme, cols) | 578 | dialog = i.changelogDialog.Render(i.cfg.Theme, cols) | ||
| 577 | case i.confirmDialog.Active(): | 579 | case i.confirmDialog.Active(): | ||
| 578 | dialog = i.confirmDialog.Render(i.cfg.Theme, cols) | 580 | dialog = i.confirmDialog.Render(i.cfg.Theme, cols) | ||
| 581 | + | case i.logoutDialog.Active(): | |||
| 582 | + | dialog = i.logoutDialog.Render(i.cfg.Theme, cols) | |||
| 579 | } | 583 | } | ||
| 580 | 584 | ||||
| 581 | // Slash-command autocomplete: popup above the status line, only | 585 | // Slash-command autocomplete: popup above the status line, only | ||
| @@ -822,6 +826,19 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||||
| 822 | } | 826 | } | ||
| 823 | return false | 827 | return false | ||
| 824 | } | 828 | } | ||
| 829 | + | if i.logoutDialog.Active() { | |||
| 830 | + | if k.Kind == tui.KeyCtrlC { | |||
| 831 | + | i.logoutDialog.Close() | |||
| 832 | + | i.invalidate() | |||
| 833 | + | return false | |||
| 834 | + | } | |||
| 835 | + | act := i.logoutDialog.HandleKey(k) | |||
| 836 | + | if act.Select { | |||
| 837 | + | i.doLogout(act.Target) | |||
| 838 | + | } | |||
| 839 | + | i.invalidate() | |||
| 840 | + | return false | |||
| 841 | + | } | |||
| 825 | if i.jumpDialog.Active() { | 842 | if i.jumpDialog.Active() { | ||
| 826 | if k.Kind == tui.KeyCtrlC { | 843 | if k.Kind == tui.KeyCtrlC { | ||
| 827 | i.jumpDialog.Close() | 844 | i.jumpDialog.Close() | ||
| @@ -1192,11 +1209,15 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) { | |||||
| 1192 | case "/login": | 1209 | case "/login": | ||
| 1193 | i.dialog.Open() | 1210 | i.dialog.Open() | ||
| 1194 | case "/logout": | 1211 | case "/logout": | ||
| 1195 | - | target := "all" | |||
| 1196 | if len(parts) >= 2 { | 1212 | if len(parts) >= 2 { | ||
| 1197 | - | target = parts[1] | 1213 | + | // Explicit target: /logout anthropic | openai | all |
| 1214 | + | i.doLogout(parts[1]) | |||
| 1215 | + | break | |||
| 1198 | } | 1216 | } | ||
| 1199 | - | i.doLogout(target) | 1217 | + | // No arg: open the picker over whichever providers are |
| 1218 | + | // currently logged in. If nothing's logged in, bail with a | |||
| 1219 | + | // status line. | |||
| 1220 | + | i.openLogoutDialog() | |||
| 1200 | case "/model": | 1221 | case "/model": | ||
| 1201 | if len(parts) >= 2 { | 1222 | if len(parts) >= 2 { | ||
| 1202 | i.applyModelSelection("", parts[1]) | 1223 | i.applyModelSelection("", parts[1]) | ||
| @@ -1263,6 +1284,62 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) { | |||||
| 1263 | return false | 1284 | return false | ||
| 1264 | } | 1285 | } | ||
| 1265 | 1286 | ||||
| 1287 | + | // openLogoutDialog shows the provider picker for `/logout` with no | |||
| 1288 | + | // argument. Only providers the user is currently logged into are | |||
| 1289 | + | // listed, plus an "all" entry when more than one is present. If | |||
| 1290 | + | // nothing's logged in, writes a status line instead of opening an | |||
| 1291 | + | // empty dialog. | |||
| 1292 | + | func (i *Interactive) openLogoutDialog() { | |||
| 1293 | + | if i.cfg.AuthManager == nil { | |||
| 1294 | + | i.mu.Lock() | |||
| 1295 | + | i.statusErr = "no auth manager configured" | |||
| 1296 | + | i.mu.Unlock() | |||
| 1297 | + | i.invalidate() | |||
| 1298 | + | return | |||
| 1299 | + | } | |||
| 1300 | + | store := i.cfg.AuthManager.Store() | |||
| 1301 | + | if store == nil { | |||
| 1302 | + | i.mu.Lock() | |||
| 1303 | + | i.statusErr = "auth store is not available" | |||
| 1304 | + | i.mu.Unlock() | |||
| 1305 | + | i.invalidate() | |||
| 1306 | + | return | |||
| 1307 | + | } | |||
| 1308 | + | creds, err := store.Load() | |||
| 1309 | + | if err != nil { | |||
| 1310 | + | i.mu.Lock() | |||
| 1311 | + | i.statusErr = "read auth store: " + err.Error() | |||
| 1312 | + | i.mu.Unlock() | |||
| 1313 | + | i.invalidate() | |||
| 1314 | + | return | |||
| 1315 | + | } | |||
| 1316 | + | ||||
| 1317 | + | var items []logoutItem | |||
| 1318 | + | for _, p := range []string{"anthropic", "openai"} { | |||
| 1319 | + | if creds.Has(p) { | |||
| 1320 | + | items = append(items, logoutItem{ | |||
| 1321 | + | label: p, | |||
| 1322 | + | target: p, | |||
| 1323 | + | method: creds.Method(p), | |||
| 1324 | + | }) | |||
| 1325 | + | } | |||
| 1326 | + | } | |||
| 1327 | + | if len(items) == 0 { | |||
| 1328 | + | i.mu.Lock() | |||
| 1329 | + | i.statusOK = "no credentials stored; already logged out" | |||
| 1330 | + | i.statusErr = "" | |||
| 1331 | + | i.mu.Unlock() | |||
| 1332 | + | i.invalidate() | |||
| 1333 | + | return | |||
| 1334 | + | } | |||
| 1335 | + | if len(items) > 1 { | |||
| 1336 | + | items = append(items, logoutItem{label: "all", target: "all"}) | |||
| 1337 | + | } | |||
| 1338 | + | ||||
| 1339 | + | i.logoutDialog.Open(items) | |||
| 1340 | + | i.invalidate() | |||
| 1341 | + | } | |||
| 1342 | + | ||||
| 1266 | // doLogout clears credentials for the given provider (or all providers) | 1343 | // doLogout clears credentials for the given provider (or all providers) | ||
| 1267 | // from auth.json. If the active agent was using those credentials, it | 1344 | // from auth.json. If the active agent was using those credentials, it | ||
| 1268 | // is torn down so the user is forced through /login before their next | 1345 | // is torn down so the user is forced through /login before their next | ||
A internal/agent/modes/logout_dialog.go +103 -0
| @@ -0,0 +1,103 @@ | |||
| 1 | + | package modes | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/patriceckhart/zot/internal/tui" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | // logoutDialog is the picker shown when the user runs `/logout` | |
| 8 | + | // without an argument. Lists each provider the user is currently | |
| 9 | + | // logged into (apikey or oauth), plus an "all" entry and a cancel. | |
| 10 | + | // Up/down to move, enter to pick, esc to cancel. | |
| 11 | + | type logoutDialog struct { | |
| 12 | + | active bool | |
| 13 | + | items []logoutItem | |
| 14 | + | cursor int | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // logoutItem is one row in the picker. target is what gets passed | |
| 18 | + | // to doLogout ("anthropic", "openai", or "all"); method is the | |
| 19 | + | // short tag shown in muted text next to the provider ("apikey" or | |
| 20 | + | // "oauth"), empty for the "all" row. | |
| 21 | + | type logoutItem struct { | |
| 22 | + | label string | |
| 23 | + | target string | |
| 24 | + | method string | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // logoutAction is the outcome of a key press. | |
| 28 | + | type logoutAction struct { | |
| 29 | + | Select bool | |
| 30 | + | Target string // one of: "anthropic", "openai", "all" | |
| 31 | + | Close bool | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func newLogoutDialog() *logoutDialog { return &logoutDialog{} } | |
| 35 | + | ||
| 36 | + | // Open populates the picker with whichever providers are currently | |
| 37 | + | // logged in. Returns false if nothing to log out of; the caller | |
| 38 | + | // should fall back to showing a status message instead of opening | |
| 39 | + | // an empty dialog. | |
| 40 | + | func (d *logoutDialog) Open(items []logoutItem) bool { | |
| 41 | + | if len(items) == 0 { | |
| 42 | + | return false | |
| 43 | + | } | |
| 44 | + | d.items = items | |
| 45 | + | d.cursor = 0 | |
| 46 | + | d.active = true | |
| 47 | + | return true | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Close hides the dialog. | |
| 51 | + | func (d *logoutDialog) Close() { d.active = false } | |
| 52 | + | ||
| 53 | + | // Active reports whether the dialog is consuming input. | |
| 54 | + | func (d *logoutDialog) Active() bool { return d != nil && d.active } | |
| 55 | + | ||
| 56 | + | // HandleKey advances the selection or resolves the dialog. | |
| 57 | + | func (d *logoutDialog) HandleKey(k tui.Key) logoutAction { | |
| 58 | + | switch k.Kind { | |
| 59 | + | case tui.KeyUp: | |
| 60 | + | if d.cursor > 0 { | |
| 61 | + | d.cursor-- | |
| 62 | + | } | |
| 63 | + | case tui.KeyDown: | |
| 64 | + | if d.cursor < len(d.items)-1 { | |
| 65 | + | d.cursor++ | |
| 66 | + | } | |
| 67 | + | case tui.KeyEsc: | |
| 68 | + | d.Close() | |
| 69 | + | return logoutAction{Close: true} | |
| 70 | + | case tui.KeyEnter: | |
| 71 | + | if len(d.items) == 0 { | |
| 72 | + | d.Close() | |
| 73 | + | return logoutAction{Close: true} | |
| 74 | + | } | |
| 75 | + | it := d.items[d.cursor] | |
| 76 | + | d.Close() | |
| 77 | + | return logoutAction{Select: true, Target: it.target} | |
| 78 | + | } | |
| 79 | + | return logoutAction{} | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Render returns the dialog lines. | |
| 83 | + | func (d *logoutDialog) Render(th tui.Theme, width int) []string { | |
| 84 | + | if !d.Active() { | |
| 85 | + | return nil | |
| 86 | + | } | |
| 87 | + | var lines []string | |
| 88 | + | lines = append(lines, frameHeader(th, "logout", width)) | |
| 89 | + | lines = append(lines, th.FG256(th.Muted, "choose what to log out of (\u2191/\u2193, enter, esc to cancel):")) | |
| 90 | + | for i, it := range d.items { | |
| 91 | + | plain := " " + it.label | |
| 92 | + | if it.method != "" { | |
| 93 | + | plain += " " + th.FG256(th.Muted, "("+it.method+")") | |
| 94 | + | } | |
| 95 | + | if i == d.cursor { | |
| 96 | + | lines = append(lines, th.PadHighlight(plain, width)) | |
| 97 | + | } else { | |
| 98 | + | lines = append(lines, th.FG256(th.Muted, plain)) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | lines = append(lines, frameRule(th, width)) | |
| 102 | + | return lines | |
| 103 | + | } | |
| @@ -0,0 +1,103 @@ | |||||
| 1 | + | package modes | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/patriceckhart/zot/internal/tui" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | // logoutDialog is the picker shown when the user runs `/logout` | |||
| 8 | + | // without an argument. Lists each provider the user is currently | |||
| 9 | + | // logged into (apikey or oauth), plus an "all" entry and a cancel. | |||
| 10 | + | // Up/down to move, enter to pick, esc to cancel. | |||
| 11 | + | type logoutDialog struct { | |||
| 12 | + | active bool | |||
| 13 | + | items []logoutItem | |||
| 14 | + | cursor int | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // logoutItem is one row in the picker. target is what gets passed | |||
| 18 | + | // to doLogout ("anthropic", "openai", or "all"); method is the | |||
| 19 | + | // short tag shown in muted text next to the provider ("apikey" or | |||
| 20 | + | // "oauth"), empty for the "all" row. | |||
| 21 | + | type logoutItem struct { | |||
| 22 | + | label string | |||
| 23 | + | target string | |||
| 24 | + | method string | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // logoutAction is the outcome of a key press. | |||
| 28 | + | type logoutAction struct { | |||
| 29 | + | Select bool | |||
| 30 | + | Target string // one of: "anthropic", "openai", "all" | |||
| 31 | + | Close bool | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func newLogoutDialog() *logoutDialog { return &logoutDialog{} } | |||
| 35 | + | ||||
| 36 | + | // Open populates the picker with whichever providers are currently | |||
| 37 | + | // logged in. Returns false if nothing to log out of; the caller | |||
| 38 | + | // should fall back to showing a status message instead of opening | |||
| 39 | + | // an empty dialog. | |||
| 40 | + | func (d *logoutDialog) Open(items []logoutItem) bool { | |||
| 41 | + | if len(items) == 0 { | |||
| 42 | + | return false | |||
| 43 | + | } | |||
| 44 | + | d.items = items | |||
| 45 | + | d.cursor = 0 | |||
| 46 | + | d.active = true | |||
| 47 | + | return true | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Close hides the dialog. | |||
| 51 | + | func (d *logoutDialog) Close() { d.active = false } | |||
| 52 | + | ||||
| 53 | + | // Active reports whether the dialog is consuming input. | |||
| 54 | + | func (d *logoutDialog) Active() bool { return d != nil && d.active } | |||
| 55 | + | ||||
| 56 | + | // HandleKey advances the selection or resolves the dialog. | |||
| 57 | + | func (d *logoutDialog) HandleKey(k tui.Key) logoutAction { | |||
| 58 | + | switch k.Kind { | |||
| 59 | + | case tui.KeyUp: | |||
| 60 | + | if d.cursor > 0 { | |||
| 61 | + | d.cursor-- | |||
| 62 | + | } | |||
| 63 | + | case tui.KeyDown: | |||
| 64 | + | if d.cursor < len(d.items)-1 { | |||
| 65 | + | d.cursor++ | |||
| 66 | + | } | |||
| 67 | + | case tui.KeyEsc: | |||
| 68 | + | d.Close() | |||
| 69 | + | return logoutAction{Close: true} | |||
| 70 | + | case tui.KeyEnter: | |||
| 71 | + | if len(d.items) == 0 { | |||
| 72 | + | d.Close() | |||
| 73 | + | return logoutAction{Close: true} | |||
| 74 | + | } | |||
| 75 | + | it := d.items[d.cursor] | |||
| 76 | + | d.Close() | |||
| 77 | + | return logoutAction{Select: true, Target: it.target} | |||
| 78 | + | } | |||
| 79 | + | return logoutAction{} | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Render returns the dialog lines. | |||
| 83 | + | func (d *logoutDialog) Render(th tui.Theme, width int) []string { | |||
| 84 | + | if !d.Active() { | |||
| 85 | + | return nil | |||
| 86 | + | } | |||
| 87 | + | var lines []string | |||
| 88 | + | lines = append(lines, frameHeader(th, "logout", width)) | |||
| 89 | + | lines = append(lines, th.FG256(th.Muted, "choose what to log out of (\u2191/\u2193, enter, esc to cancel):")) | |||
| 90 | + | for i, it := range d.items { | |||
| 91 | + | plain := " " + it.label | |||
| 92 | + | if it.method != "" { | |||
| 93 | + | plain += " " + th.FG256(th.Muted, "("+it.method+")") | |||
| 94 | + | } | |||
| 95 | + | if i == d.cursor { | |||
| 96 | + | lines = append(lines, th.PadHighlight(plain, width)) | |||
| 97 | + | } else { | |||
| 98 | + | lines = append(lines, th.FG256(th.Muted, plain)) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | lines = append(lines, frameRule(th, width)) | |||
| 102 | + | return lines | |||
| 103 | + | } | |||
M internal/agent/systemprompt.go +1 -1
| @@ -75,6 +75,6 @@ func BuildSystemPrompt(o SystemPromptOpts) string { | |||
| 75 | 75 | return sb.String() | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | const defaultIdentity = `You are zot, a lightweight terminal coding agent. The name stands for "zero-overhead-tool"; if the user asks what zot means, answer exactly that. | |
| 78 | + | const defaultIdentity = `You are an expert coding assistant operating inside zot, a coding agent harness. The name "zot" stands for "zero-overhead-tool"; if the user asks what zot means, answer exactly that. | |
| 79 | 79 | ||
| 80 | 80 | Your output renders in a TUI that understands markdown for prose and plain text for tool output. Use markdown freely, keep answers concise, and let tool calls speak for themselves rather than narrating them in prose before you invoke them. Act first, then summarise what you did.` | |
| @@ -75,6 +75,6 @@ func BuildSystemPrompt(o SystemPromptOpts) string { | |||||
| 75 | return sb.String() | 75 | return sb.String() | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | - | const defaultIdentity = `You are zot, a lightweight terminal coding agent. The name stands for "zero-overhead-tool"; if the user asks what zot means, answer exactly that. | 78 | + | const defaultIdentity = `You are an expert coding assistant operating inside zot, a coding agent harness. The name "zot" stands for "zero-overhead-tool"; if the user asks what zot means, answer exactly that. |
| 79 | 79 | ||||
| 80 | Your output renders in a TUI that understands markdown for prose and plain text for tool output. Use markdown freely, keep answers concise, and let tool calls speak for themselves rather than narrating them in prose before you invoke them. Act first, then summarise what you did.` | 80 | Your output renders in a TUI that understands markdown for prose and plain text for tool output. Use markdown freely, keep answers concise, and let tool calls speak for themselves rather than narrating them in prose before you invoke them. Act first, then summarise what you did.` | ||
M internal/auth/callback.go +25 -18
| @@ -122,16 +122,18 @@ func (cs *CallbackServer) deliver(r CallbackResult) { | |||
| 122 | 122 | ||
| 123 | 123 | // ---- static HTML for the callback tab ---- | |
| 124 | 124 | ||
| 125 | - | // All zot-served pages share a single style: white background, black | |
| 126 | - | // text, the cyan pixel-art `z` logo at the top, thin black rules, | |
| 127 | - | // monospace type. | |
| 125 | + | // All zot-served pages share a single dark style matching the TUI: | |
| 126 | + | // near-black background (#0a0a0a), white text, Geist Mono type, cyan | |
| 127 | + | // accent on the word "zot". Serves every step of both the api-key | |
| 128 | + | // flow and the oauth callback flow. | |
| 128 | 129 | const monoStyle = `<style> | |
| 129 | - | :root { color-scheme: light; } | |
| 130 | + | @import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;600&display=swap'); | |
| 131 | + | :root { color-scheme: dark; } | |
| 130 | 132 | * { box-sizing: border-box; } | |
| 131 | 133 | body { | |
| 132 | - | font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| 133 | - | background: #ffffff; | |
| 134 | - | color: #000000; | |
| 134 | + | font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| 135 | + | background: #0a0a0a; | |
| 136 | + | color: #ffffff; | |
| 135 | 137 | max-width: 44rem; | |
| 136 | 138 | margin: 0 auto; | |
| 137 | 139 | padding: 3rem 1.5rem; | |
| @@ -152,23 +154,28 @@ const monoStyle = `<style> | |||
| 152 | 154 | letter-spacing: 0.01em; | |
| 153 | 155 | } | |
| 154 | 156 | h1 .mark { display: inline-block; width: 1.25rem; } | |
| 155 | - | .rule { border: 0; border-top: 1px solid #000000; margin: 1.5rem 0; } | |
| 156 | - | .muted { color: #595959; } | |
| 157 | - | .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; } | |
| 157 | + | .zot { color: #7ed3fc; } | |
| 158 | + | .rule { border: 0; border-top: 1px solid #ffffff; margin: 1.5rem 0; } | |
| 159 | + | .muted { color: #9ca3af; } | |
| 160 | + | .mono { font-family: inherit; word-break: break-all; } | |
| 158 | 161 | .msg { padding: 0.75rem 0; } | |
| 159 | - | a { color: inherit; } | |
| 162 | + | a { color: #7ed3fc; } | |
| 160 | 163 | input[type=password], input[type=text] { | |
| 161 | 164 | width: 100%; padding: 0.5rem 0.6rem; | |
| 162 | - | border: 1px solid #000000; background: #ffffff; color: #000000; | |
| 165 | + | background: #0a0a0a; color: #ffffff; | |
| 166 | + | border: 1px solid #ffffff; | |
| 163 | 167 | font-family: inherit; font-size: 0.95rem; | |
| 164 | 168 | } | |
| 169 | + | input[type=password]:focus, input[type=text]:focus { | |
| 170 | + | outline: none; border-color: #7ed3fc; | |
| 171 | + | } | |
| 165 | 172 | button { | |
| 166 | 173 | padding: 0.5rem 1.25rem; | |
| 167 | - | background: #000000; color: #ffffff; | |
| 168 | - | border: 1px solid #000000; font-family: inherit; font-size: 0.95rem; | |
| 174 | + | background: #ffffff; color: #0a0a0a; | |
| 175 | + | border: 1px solid #ffffff; font-family: inherit; font-size: 0.95rem; | |
| 169 | 176 | cursor: pointer; | |
| 170 | 177 | } | |
| 171 | - | button:hover { background: #ffffff; color: #000000; } | |
| 178 | + | button:hover { background: #0a0a0a; color: #ffffff; } | |
| 172 | 179 | </style>` | |
| 173 | 180 | ||
| 174 | 181 | // logoTag is the <img> element used at the top of every zot-served | |
| @@ -189,17 +196,17 @@ func oauthSuccessHTML(provider string) string { | |||
| 189 | 196 | ` + logoTag + ` | |
| 190 | 197 | <h1><span class="mark">✓</span> logged in to ` + p + `</h1> | |
| 191 | 198 | <hr class="rule"> | |
| 192 | - | <p class="msg">zot received the callback. you can close this tab.</p> | |
| 199 | + | <p class="msg"><span class="zot">zot</span> received the callback. you can close this tab.</p> | |
| 193 | 200 | </body></html>` | |
| 194 | 201 | } | |
| 195 | 202 | ||
| 196 | 203 | func oauthErrorHTML(msg string) string { | |
| 197 | - | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | |
| 204 | + | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot - error</title>` + monoStyle + `</head><body> | |
| 198 | 205 | ` + logoTag + ` | |
| 199 | 206 | <h1><span class="mark">✗</span> login failed</h1> | |
| 200 | 207 | <hr class="rule"> | |
| 201 | 208 | <p class="msg mono">` + htmlEscape(msg) + `</p> | |
| 202 | - | <p class="muted">go back to zot and try again.</p> | |
| 209 | + | <p class="muted">go back to <span class="zot">zot</span> and try again.</p> | |
| 203 | 210 | </body></html>` | |
| 204 | 211 | } | |
| 205 | 212 | ||
| @@ -122,16 +122,18 @@ func (cs *CallbackServer) deliver(r CallbackResult) { | |||||
| 122 | 122 | ||||
| 123 | // ---- static HTML for the callback tab ---- | 123 | // ---- static HTML for the callback tab ---- | ||
| 124 | 124 | ||||
| 125 | - | // All zot-served pages share a single style: white background, black | 125 | + | // All zot-served pages share a single dark style matching the TUI: |
| 126 | - | // text, the cyan pixel-art `z` logo at the top, thin black rules, | 126 | + | // near-black background (#0a0a0a), white text, Geist Mono type, cyan |
| 127 | - | // monospace type. | 127 | + | // accent on the word "zot". Serves every step of both the api-key |
| 128 | + | // flow and the oauth callback flow. | |||
| 128 | const monoStyle = `<style> | 129 | const monoStyle = `<style> | ||
| 129 | - | :root { color-scheme: light; } | 130 | + | @import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;600&display=swap'); |
| 131 | + | :root { color-scheme: dark; } | |||
| 130 | * { box-sizing: border-box; } | 132 | * { box-sizing: border-box; } | ||
| 131 | body { | 133 | body { | ||
| 132 | - | font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | 134 | + | font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; |
| 133 | - | background: #ffffff; | 135 | + | background: #0a0a0a; |
| 134 | - | color: #000000; | 136 | + | color: #ffffff; |
| 135 | max-width: 44rem; | 137 | max-width: 44rem; | ||
| 136 | margin: 0 auto; | 138 | margin: 0 auto; | ||
| 137 | padding: 3rem 1.5rem; | 139 | padding: 3rem 1.5rem; | ||
| @@ -152,23 +154,28 @@ const monoStyle = `<style> | |||||
| 152 | letter-spacing: 0.01em; | 154 | letter-spacing: 0.01em; | ||
| 153 | } | 155 | } | ||
| 154 | h1 .mark { display: inline-block; width: 1.25rem; } | 156 | h1 .mark { display: inline-block; width: 1.25rem; } | ||
| 155 | - | .rule { border: 0; border-top: 1px solid #000000; margin: 1.5rem 0; } | 157 | + | .zot { color: #7ed3fc; } |
| 156 | - | .muted { color: #595959; } | 158 | + | .rule { border: 0; border-top: 1px solid #ffffff; margin: 1.5rem 0; } |
| 157 | - | .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; } | 159 | + | .muted { color: #9ca3af; } |
| 160 | + | .mono { font-family: inherit; word-break: break-all; } | |||
| 158 | .msg { padding: 0.75rem 0; } | 161 | .msg { padding: 0.75rem 0; } | ||
| 159 | - | a { color: inherit; } | 162 | + | a { color: #7ed3fc; } |
| 160 | input[type=password], input[type=text] { | 163 | input[type=password], input[type=text] { | ||
| 161 | width: 100%; padding: 0.5rem 0.6rem; | 164 | width: 100%; padding: 0.5rem 0.6rem; | ||
| 162 | - | border: 1px solid #000000; background: #ffffff; color: #000000; | 165 | + | background: #0a0a0a; color: #ffffff; |
| 166 | + | border: 1px solid #ffffff; | |||
| 163 | font-family: inherit; font-size: 0.95rem; | 167 | font-family: inherit; font-size: 0.95rem; | ||
| 164 | } | 168 | } | ||
| 169 | + | input[type=password]:focus, input[type=text]:focus { | |||
| 170 | + | outline: none; border-color: #7ed3fc; | |||
| 171 | + | } | |||
| 165 | button { | 172 | button { | ||
| 166 | padding: 0.5rem 1.25rem; | 173 | padding: 0.5rem 1.25rem; | ||
| 167 | - | background: #000000; color: #ffffff; | 174 | + | background: #ffffff; color: #0a0a0a; |
| 168 | - | border: 1px solid #000000; font-family: inherit; font-size: 0.95rem; | 175 | + | border: 1px solid #ffffff; font-family: inherit; font-size: 0.95rem; |
| 169 | cursor: pointer; | 176 | cursor: pointer; | ||
| 170 | } | 177 | } | ||
| 171 | - | button:hover { background: #ffffff; color: #000000; } | 178 | + | button:hover { background: #0a0a0a; color: #ffffff; } |
| 172 | </style>` | 179 | </style>` | ||
| 173 | 180 | ||||
| 174 | // logoTag is the <img> element used at the top of every zot-served | 181 | // logoTag is the <img> element used at the top of every zot-served | ||
| @@ -189,17 +196,17 @@ func oauthSuccessHTML(provider string) string { | |||||
| 189 | ` + logoTag + ` | 196 | ` + logoTag + ` | ||
| 190 | <h1><span class="mark">✓</span> logged in to ` + p + `</h1> | 197 | <h1><span class="mark">✓</span> logged in to ` + p + `</h1> | ||
| 191 | <hr class="rule"> | 198 | <hr class="rule"> | ||
| 192 | - | <p class="msg">zot received the callback. you can close this tab.</p> | 199 | + | <p class="msg"><span class="zot">zot</span> received the callback. you can close this tab.</p> |
| 193 | </body></html>` | 200 | </body></html>` | ||
| 194 | } | 201 | } | ||
| 195 | 202 | ||||
| 196 | func oauthErrorHTML(msg string) string { | 203 | func oauthErrorHTML(msg string) string { | ||
| 197 | - | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | 204 | + | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot - error</title>` + monoStyle + `</head><body> |
| 198 | ` + logoTag + ` | 205 | ` + logoTag + ` | ||
| 199 | <h1><span class="mark">✗</span> login failed</h1> | 206 | <h1><span class="mark">✗</span> login failed</h1> | ||
| 200 | <hr class="rule"> | 207 | <hr class="rule"> | ||
| 201 | <p class="msg mono">` + htmlEscape(msg) + `</p> | 208 | <p class="msg mono">` + htmlEscape(msg) + `</p> | ||
| 202 | - | <p class="muted">go back to zot and try again.</p> | 209 | + | <p class="muted">go back to <span class="zot">zot</span> and try again.</p> |
| 203 | </body></html>` | 210 | </body></html>` | ||
| 204 | } | 211 | } | ||
| 205 | 212 | ||||
M internal/auth/server.go +9 -9
| @@ -221,15 +221,15 @@ func Redirect(w http.ResponseWriter, r *http.Request, u *url.URL) { | |||
| 221 | 221 | ||
| 222 | 222 | var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body> | |
| 223 | 223 | ` + logoTag + ` | |
| 224 | - | <h1>zot login</h1> | |
| 224 | + | <h1><span class="zot">zot</span> login</h1> | |
| 225 | 225 | <hr class="rule"> | |
| 226 | - | <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> | |
| 226 | + | <p>paste an api key for anthropic or openai. <span class="zot">zot</span> probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> | |
| 227 | 227 | <p> | |
| 228 | 228 | <a href="/apikey?provider=anthropic">anthropic api key →</a><br> | |
| 229 | 229 | <a href="/apikey?provider=openai">openai api key →</a> | |
| 230 | 230 | </p> | |
| 231 | 231 | <hr class="rule"> | |
| 232 | - | <p class="muted">for a subscription login (claude pro/max · chatgpt plus/pro), close this tab and run /login inside zot.</p> | |
| 232 | + | <p class="muted">for a subscription login (claude pro/max - chatgpt plus/pro), close this tab and run /login inside <span class="zot">zot</span>.</p> | |
| 233 | 233 | </body></html>`)) | |
| 234 | 234 | ||
| 235 | 235 | func init() { | |
| @@ -238,9 +238,9 @@ func init() { | |||
| 238 | 238 | label { font-size: 0.875rem; } | |
| 239 | 239 | </style></head><body> | |
| 240 | 240 | ` + logoTag + ` | |
| 241 | - | <h1>zot login · {{.Provider}} api key</h1> | |
| 241 | + | <h1><span class="zot">zot</span> login - {{.Provider}} api key</h1> | |
| 242 | 242 | <hr class="rule"> | |
| 243 | - | <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p> | |
| 243 | + | <p>paste your {{.Provider}} api key. <span class="zot">zot</span> will probe the provider with it once, then save it if the key is accepted.</p> | |
| 244 | 244 | <form method="POST" action="/apikey"> | |
| 245 | 245 | <input type="hidden" name="provider" value="{{.Provider}}" /> | |
| 246 | 246 | <label for="api_key">api key</label> | |
| @@ -249,19 +249,19 @@ func init() { | |||
| 249 | 249 | </form> | |
| 250 | 250 | </body></html>`)) | |
| 251 | 251 | ||
| 252 | - | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | |
| 252 | + | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot - logged in</title>` + monoStyle + `</head><body> | |
| 253 | 253 | ` + logoTag + ` | |
| 254 | 254 | <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1> | |
| 255 | 255 | <hr class="rule"> | |
| 256 | 256 | <p class="msg">method: {{.Method}}</p> | |
| 257 | - | <p class="muted">zot received the callback. you can close this tab and return to the terminal.</p> | |
| 257 | + | <p class="muted"><span class="zot">zot</span> received the callback. you can close this tab and return to the terminal.</p> | |
| 258 | 258 | </body></html>`)) | |
| 259 | 259 | ||
| 260 | - | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | |
| 260 | + | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot - error</title>` + monoStyle + `</head><body> | |
| 261 | 261 | ` + logoTag + ` | |
| 262 | 262 | <h1><span class="mark">✗</span> login failed</h1> | |
| 263 | 263 | <hr class="rule"> | |
| 264 | 264 | <p class="msg mono">{{.Message}}</p> | |
| 265 | - | <p class="muted">go back to zot and try again.</p> | |
| 265 | + | <p class="muted">go back to <span class="zot">zot</span> and try again.</p> | |
| 266 | 266 | </body></html>`)) | |
| 267 | 267 | } | |
| @@ -221,15 +221,15 @@ func Redirect(w http.ResponseWriter, r *http.Request, u *url.URL) { | |||||
| 221 | 221 | ||||
| 222 | var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body> | 222 | var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body> | ||
| 223 | ` + logoTag + ` | 223 | ` + logoTag + ` | ||
| 224 | - | <h1>zot login</h1> | 224 | + | <h1><span class="zot">zot</span> login</h1> |
| 225 | <hr class="rule"> | 225 | <hr class="rule"> | ||
| 226 | - | <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> | 226 | + | <p>paste an api key for anthropic or openai. <span class="zot">zot</span> probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> |
| 227 | <p> | 227 | <p> | ||
| 228 | <a href="/apikey?provider=anthropic">anthropic api key →</a><br> | 228 | <a href="/apikey?provider=anthropic">anthropic api key →</a><br> | ||
| 229 | <a href="/apikey?provider=openai">openai api key →</a> | 229 | <a href="/apikey?provider=openai">openai api key →</a> | ||
| 230 | </p> | 230 | </p> | ||
| 231 | <hr class="rule"> | 231 | <hr class="rule"> | ||
| 232 | - | <p class="muted">for a subscription login (claude pro/max · chatgpt plus/pro), close this tab and run /login inside zot.</p> | 232 | + | <p class="muted">for a subscription login (claude pro/max - chatgpt plus/pro), close this tab and run /login inside <span class="zot">zot</span>.</p> |
| 233 | </body></html>`)) | 233 | </body></html>`)) | ||
| 234 | 234 | ||||
| 235 | func init() { | 235 | func init() { | ||
| @@ -238,9 +238,9 @@ func init() { | |||||
| 238 | label { font-size: 0.875rem; } | 238 | label { font-size: 0.875rem; } | ||
| 239 | </style></head><body> | 239 | </style></head><body> | ||
| 240 | ` + logoTag + ` | 240 | ` + logoTag + ` | ||
| 241 | - | <h1>zot login · {{.Provider}} api key</h1> | 241 | + | <h1><span class="zot">zot</span> login - {{.Provider}} api key</h1> |
| 242 | <hr class="rule"> | 242 | <hr class="rule"> | ||
| 243 | - | <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p> | 243 | + | <p>paste your {{.Provider}} api key. <span class="zot">zot</span> will probe the provider with it once, then save it if the key is accepted.</p> |
| 244 | <form method="POST" action="/apikey"> | 244 | <form method="POST" action="/apikey"> | ||
| 245 | <input type="hidden" name="provider" value="{{.Provider}}" /> | 245 | <input type="hidden" name="provider" value="{{.Provider}}" /> | ||
| 246 | <label for="api_key">api key</label> | 246 | <label for="api_key">api key</label> | ||
| @@ -249,19 +249,19 @@ func init() { | |||||
| 249 | </form> | 249 | </form> | ||
| 250 | </body></html>`)) | 250 | </body></html>`)) | ||
| 251 | 251 | ||||
| 252 | - | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | 252 | + | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot - logged in</title>` + monoStyle + `</head><body> |
| 253 | ` + logoTag + ` | 253 | ` + logoTag + ` | ||
| 254 | <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1> | 254 | <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1> | ||
| 255 | <hr class="rule"> | 255 | <hr class="rule"> | ||
| 256 | <p class="msg">method: {{.Method}}</p> | 256 | <p class="msg">method: {{.Method}}</p> | ||
| 257 | - | <p class="muted">zot received the callback. you can close this tab and return to the terminal.</p> | 257 | + | <p class="muted"><span class="zot">zot</span> received the callback. you can close this tab and return to the terminal.</p> |
| 258 | </body></html>`)) | 258 | </body></html>`)) | ||
| 259 | 259 | ||||
| 260 | - | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | 260 | + | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot - error</title>` + monoStyle + `</head><body> |
| 261 | ` + logoTag + ` | 261 | ` + logoTag + ` | ||
| 262 | <h1><span class="mark">✗</span> login failed</h1> | 262 | <h1><span class="mark">✗</span> login failed</h1> | ||
| 263 | <hr class="rule"> | 263 | <hr class="rule"> | ||
| 264 | <p class="msg mono">{{.Message}}</p> | 264 | <p class="msg mono">{{.Message}}</p> | ||
| 265 | - | <p class="muted">go back to zot and try again.</p> | 265 | + | <p class="muted">go back to <span class="zot">zot</span> and try again.</p> |
| 266 | </body></html>`)) | 266 | </body></html>`)) | ||
| 267 | } | 267 | } | ||