mirror of
https://github.com/patriceckhart/zot.git
synced 2026-07-06 01:51:04 +02:00
feat(tui): /login shows current status for each provider
Running /login used to drop straight into the method / provider
pickers with no indication of what's already logged in. Easy to
accidentally log out a working subscription because you
forgot which provider you'd authenticated with last.
The dialog now takes a snapshot of auth.json when Open() runs
and renders it as a two-line header above both the method step
and the provider step:
login
\u2713 anthropic: subscription
\u2713 openai: api key
choose login method (\u2191/\u2193, enter, esc to cancel):
api key
subscription (claude pro/max \u00b7 chatgpt plus/pro)
Logged-in providers get a green check + their method
("api key" or "subscription"); providers with no credentials
get a muted \u2013 dash + "not logged in". When NEITHER provider is
logged in (first-run, fresh box) the status block is
suppressed entirely \u2014 a pair of "not logged in" rows there is
just noise when the user is already seconds away from picking
a method.
On the provider step each row also gets an inline method tag
so picking it implicitly replaces the existing credential:
login \u00b7 oauth
\u2713 anthropic: subscription
\u2713 openai: api key
choose provider:
anthropic (subscription)
openai (api key)
Implementation:
- New d.status map on loginDialog, populated by Open() from
auth.Credentials.Method(provider) which already returns
exactly the three states we care about: "apikey", "oauth",
or "".
- Open() gained a zotHome string arg so the dialog can
compute auth.json's path without importing the agent
package (which would be a cyclic import; modes is inside
agent). Both callers in interactive.go now pass
i.cfg.ZotHome.
- renderStatusLines() centralises the two-row block so
loginStepMethod and loginStepProvider share it.
Tests: the whole package compiles and vet-clean; go test
./... still passes. No new tests because the dialog is pure
rendering off captured state and the captured-state path is a
one-liner.
This commit is contained in:
parent
cb8526c061
commit
755ca7ccdf
2 changed files with 84 additions and 5 deletions
|
|
@ -353,7 +353,7 @@ func (i *Interactive) Run(ctx context.Context) error {
|
|||
// want to dismiss it (e.g. to check /help or /exit first).
|
||||
if i.agent == nil {
|
||||
i.statusErr = "not logged in. pick a login method below or press esc to dismiss."
|
||||
i.dialog.Open()
|
||||
i.dialog.Open(i.cfg.ZotHome)
|
||||
}
|
||||
|
||||
// Input goroutine.
|
||||
|
|
@ -1420,7 +1420,7 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
|
|||
i.scrollOffset = 0
|
||||
i.mu.Unlock()
|
||||
case "/login":
|
||||
i.dialog.Open()
|
||||
i.dialog.Open(i.cfg.ZotHome)
|
||||
case "/logout":
|
||||
if len(parts) >= 2 {
|
||||
// Explicit target: /logout anthropic | openai | all
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package modes
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/patriceckhart/zot/internal/auth"
|
||||
"github.com/patriceckhart/zot/internal/tui"
|
||||
)
|
||||
|
||||
|
|
@ -30,6 +32,14 @@ type loginDialog struct {
|
|||
success bool
|
||||
url string
|
||||
cursor int
|
||||
|
||||
// status is a snapshot of the current login state for each
|
||||
// provider, captured when Open() runs. Rendered above the
|
||||
// method picker so the user can see whether they're already
|
||||
// logged in (and how) before starting a new flow. Keys:
|
||||
// "anthropic", "openai". Value is "apikey", "oauth", or ""
|
||||
// (not logged in).
|
||||
status map[string]string
|
||||
}
|
||||
|
||||
func newLoginDialog() *loginDialog {
|
||||
|
|
@ -39,8 +49,13 @@ func newLoginDialog() *loginDialog {
|
|||
// Active reports whether the dialog consumes input.
|
||||
func (d *loginDialog) Active() bool { return d != nil && d.step != loginStepClosed }
|
||||
|
||||
// Open starts the dialog from scratch.
|
||||
func (d *loginDialog) Open() {
|
||||
// Open starts the dialog from scratch and captures the current
|
||||
// login status for each provider so the picker can show it.
|
||||
// zotHome is the zot state directory ($ZOT_HOME); auth.json
|
||||
// lives inside it. Passing the path in (instead of importing
|
||||
// the agent package to call AuthPath()) avoids a cyclic import
|
||||
// between agent and agent/modes.
|
||||
func (d *loginDialog) Open(zotHome string) {
|
||||
d.step = loginStepMethod
|
||||
d.method = ""
|
||||
d.provider = ""
|
||||
|
|
@ -48,6 +63,16 @@ func (d *loginDialog) Open() {
|
|||
d.success = false
|
||||
d.url = ""
|
||||
d.cursor = 0
|
||||
d.status = map[string]string{"anthropic": "", "openai": ""}
|
||||
// Best-effort: if the auth file can't be read, treat every
|
||||
// provider as not-logged-in. The status line just won't show
|
||||
// anything useful in that case, which is fine — the user
|
||||
// was about to log in anyway.
|
||||
path := filepath.Join(zotHome, "auth.json")
|
||||
if creds, err := auth.NewStore(path).Load(); err == nil {
|
||||
d.status["anthropic"] = creds.Method("anthropic")
|
||||
d.status["openai"] = creds.Method("openai")
|
||||
}
|
||||
}
|
||||
|
||||
// Close hides the dialog.
|
||||
|
|
@ -69,6 +94,9 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string {
|
|||
"subscription (claude pro/max · chatgpt plus/pro)",
|
||||
}
|
||||
lines = append(lines, frameHeader(th, "login", width))
|
||||
for _, l := range d.renderStatusLines(th) {
|
||||
lines = append(lines, l)
|
||||
}
|
||||
lines = append(lines, th.FG256(th.Muted, "choose login method (↑/↓, enter, esc to cancel):"))
|
||||
for i, o := range opts {
|
||||
plain := " " + o
|
||||
|
|
@ -82,9 +110,22 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string {
|
|||
case loginStepProvider:
|
||||
opts := []string{"anthropic", "openai"}
|
||||
lines = append(lines, frameHeader(th, "login · "+d.method, width))
|
||||
for _, l := range d.renderStatusLines(th) {
|
||||
lines = append(lines, l)
|
||||
}
|
||||
lines = append(lines, th.FG256(th.Muted, "choose provider:"))
|
||||
for i, o := range opts {
|
||||
plain := " " + o
|
||||
// Annotate each provider with its current login
|
||||
// state so the user can see at a glance which will
|
||||
// be replaced if they pick it.
|
||||
tag := ""
|
||||
switch d.status[o] {
|
||||
case "apikey":
|
||||
tag = " (api key)"
|
||||
case "oauth":
|
||||
tag = " (subscription)"
|
||||
}
|
||||
plain := " " + o + tag
|
||||
if i == d.cursor {
|
||||
lines = append(lines, th.PadHighlight(plain, width))
|
||||
} else {
|
||||
|
|
@ -114,6 +155,44 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string {
|
|||
return lines
|
||||
}
|
||||
|
||||
// renderStatusLines returns an overview of the current login
|
||||
// state for each provider, one row per provider, suitable to
|
||||
// insert between the frame header and the picker body. Logged-
|
||||
// in providers get a green checkmark in front; providers with
|
||||
// no credentials render as a muted dash so the list layout
|
||||
// stays aligned across first-run and re-login cases.
|
||||
//
|
||||
// Returns nil when neither provider is logged in (first-run
|
||||
// case — a pair of "not logged in" rows is just noise when the
|
||||
// user is about to pick a method anyway).
|
||||
func (d *loginDialog) renderStatusLines(th tui.Theme) []string {
|
||||
anth := d.status["anthropic"]
|
||||
op := d.status["openai"]
|
||||
if anth == "" && op == "" {
|
||||
return nil
|
||||
}
|
||||
row := func(name, method string) string {
|
||||
var mark, body string
|
||||
switch method {
|
||||
case "apikey":
|
||||
mark = th.FG256(th.Tool, "✓")
|
||||
body = th.FG256(th.Muted, name+": api key")
|
||||
case "oauth":
|
||||
mark = th.FG256(th.Tool, "✓")
|
||||
body = th.FG256(th.Muted, name+": subscription")
|
||||
default:
|
||||
mark = th.FG256(th.Muted, "–")
|
||||
body = th.FG256(th.Muted, name+": not logged in")
|
||||
}
|
||||
return " " + mark + " " + body
|
||||
}
|
||||
return []string{
|
||||
row("anthropic", anth),
|
||||
row("openai", op),
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
// Key is the result of handling a key press.
|
||||
type loginDialogAction struct {
|
||||
StartAPIKey bool
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue