accounts: a magpie just started no longer holds every page and request for cursor-agent and devin — who their CLIs say is signed in is kept on disk (cli-identity.json), served at once on the next start and asked again behind it; only a first look ever, or one after a sign-in or out, waits for the CLI. The window's first read went from ~4s to ~0.15s here with both signed in, and the gateway page shows its skeleton, not blank cards, while the state loads (#123)

This commit is contained in:
yetone committed 2026-09-28 09:21:24 +08:00
1 parent 316803e260
commit 0686bdffa7
5 files changed
+186 -66

No files matched your search

+3
View File
@@ -701,7 +701,10 @@ function fit(extra = 0, glide) {
async function load() {
if (!load.done) renderAgentsLoading();
// the gateway page too waits for the state first: its skeleton, not a
// blank page, until then (#123)
if (view === "providers" && !providers) renderProvidersLoading();
if (view === "gateway" && !providers) renderGatewayLoading();
try {
state = await api("state");
load.done = true;
+100
View File
@@ -0,0 +1,100 @@
package provider
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
)
// cliIdentity is who an agent's CLI says is signed in (`cursor-agent about`,
// `devin auth status`). Asking takes the CLI seconds, ten at worst, and
// every request of the gateway and every page of the window reads the
// accounts, so after the first answer a stale one is served while a fresh
// one is fetched behind it. The answer is kept on disk too: a magpie just
// started serves the last one at once rather than holding everything for
// the CLIs (#123), and asks them again behind it.
type cliIdentity struct {
sync.Mutex
name string
exe func() string
ask func() (user, plan string, ok bool)
at time.Time
refreshing bool
read bool // the one kept on disk was looked for
user, plan string
ok bool
}
type keptIdentity struct {
User string `json:"user,omitempty"`
Plan string `json:"plan,omitempty"`
OK bool `json:"ok"`
}
var identityFile sync.Mutex
func identityPath() string { return filepath.Join(filepath.Dir(Path()), "cli-identity.json") }
func readIdentities() map[string]keptIdentity {
m := map[string]keptIdentity{}
if b, err := os.ReadFile(identityPath()); err == nil {
_ = json.Unmarshal(b, &m)
}
return m
}
func (c *cliIdentity) keep() {
identityFile.Lock()
defer identityFile.Unlock()
m := readIdentities()
k := keptIdentity{User: c.user, Plan: c.plan, OK: c.ok}
if m[c.name] == k {
return
}
m[c.name] = k
if b, err := json.MarshalIndent(m, "", " "); err == nil {
_ = writePrivate(identityPath(), append(b, '\n'))
}
}
func (c *cliIdentity) get() (user, plan string, ok bool) {
c.Lock()
defer c.Unlock()
if c.at.IsZero() && !c.read {
c.read = true
identityFile.Lock()
k, found := readIdentities()[c.name]
identityFile.Unlock()
// served now, asked again behind it; a CLI since removed has no one
if found && c.exe() != "" {
c.user, c.plan, c.ok = k.User, k.Plan, k.OK
c.at = time.Unix(1, 0)
}
}
if c.at.IsZero() {
c.user, c.plan, c.ok = c.ask()
c.at = time.Now()
c.keep()
} else if time.Since(c.at) > time.Minute && !c.refreshing {
c.refreshing = true
go func() {
u, p, ok := c.ask()
c.Lock()
c.user, c.plan, c.ok = u, p, ok
c.at, c.refreshing = time.Now(), false
c.keep()
c.Unlock()
}()
}
return c.user, c.plan, c.ok
}
// forget has the next look ask the CLI and wait for it: after a sign-in or
// a sign-out, the last answer is the wrong one.
func (c *cliIdentity) forget() {
c.Lock()
c.at, c.read = time.Time{}, true
c.Unlock()
}
+75
View File
@@ -0,0 +1,75 @@
package provider
import (
"os"
"sync/atomic"
"testing"
"time"
)
func TestCLIIdentityKeptAcrossStarts(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
var asked atomic.Int32
answer := func() (string, string, bool) { asked.Add(1); return "me@example.com", "Pro", true }
exe := func() string { return "/bin/sh" }
// never asked before: the first look waits for the CLI, and keeps it
first := &cliIdentity{name: "x", exe: exe, ask: answer}
if u, p, ok := first.get(); !ok || u != "me@example.com" || p != "Pro" || asked.Load() != 1 {
t.Fatalf("first: %q %q %v, asked %d", u, p, ok, asked.Load())
}
if _, err := os.Stat(identityPath()); err != nil {
t.Fatal(err)
}
// a magpie started again serves the kept one at once, however long the
// CLI takes, and asks it behind that
release := make(chan struct{})
slow := func() (string, string, bool) { <-release; return "other@example.com", "Ultra", true }
again := &cliIdentity{name: "x", exe: exe, ask: slow}
done := make(chan struct{})
go func() {
defer close(done)
if u, p, ok := again.get(); !ok || u != "me@example.com" || p != "Pro" {
t.Errorf("kept: %q %q %v", u, p, ok)
}
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("a start with a kept answer waited for the CLI")
}
close(release)
deadline := time.Now().Add(2 * time.Second)
for {
if u, _, _ := again.get(); u == "other@example.com" {
break
}
if time.Now().After(deadline) {
t.Fatal("the answer behind the kept one never came")
}
time.Sleep(10 * time.Millisecond)
}
if k := readIdentities()["x"]; k.User != "other@example.com" || k.Plan != "Ultra" {
t.Fatalf("kept after the refresh: %+v", k)
}
// after a sign-in or out, the next look waits for the CLI again
again.forget()
again.ask = func() (string, string, bool) { return "", "", false }
if _, _, ok := again.get(); ok {
t.Fatal("forget served the old answer")
}
if k := readIdentities()["x"]; k.OK {
t.Fatalf("signed out, still kept: %+v", k)
}
}
func TestCLIIdentityKeptIgnoredWithoutTheCLI(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
(&cliIdentity{name: "x", exe: func() string { return "/bin/sh" }, ask: func() (string, string, bool) { return "me@example.com", "", true }}).get()
gone := &cliIdentity{name: "x", exe: func() string { return "" }, ask: func() (string, string, bool) { return "", "", false }}
if u, _, ok := gone.get(); ok || u != "" {
t.Fatalf("a removed CLI still served %q", u)
}
}
+4 -33
View File
@@ -48,41 +48,12 @@ func isCursorAgent(path string) bool {
return err == nil && strings.Contains(real, "cursor-agent")
}
var cursorStatus struct {
sync.Mutex
at time.Time
refreshing bool
user, plan string
ok bool
}
var cursorStatus = &cliIdentity{name: "cursor", exe: func() string { return CursorExecutable() }, ask: func() (string, string, bool) { return askCursorIdentity() }}
// cursorIdentity asks cursor-agent who is signed in. `about` takes a few
// seconds, so after the first answer a stale one is served while a fresh one
// is fetched behind it.
func cursorIdentity() (user, plan string, ok bool) {
cursorStatus.Lock()
defer cursorStatus.Unlock()
if cursorStatus.at.IsZero() {
cursorStatus.user, cursorStatus.plan, cursorStatus.ok = askCursorIdentity()
cursorStatus.at = time.Now()
} else if time.Since(cursorStatus.at) > time.Minute && !cursorStatus.refreshing {
cursorStatus.refreshing = true
go func() {
u, p, ok := askCursorIdentity()
cursorStatus.Lock()
cursorStatus.user, cursorStatus.plan, cursorStatus.ok = u, p, ok
cursorStatus.at, cursorStatus.refreshing = time.Now(), false
cursorStatus.Unlock()
}()
}
return cursorStatus.user, cursorStatus.plan, cursorStatus.ok
}
// cursorIdentity is who Cursor's CLI says is signed in; see cliIdentity.
func cursorIdentity() (user, plan string, ok bool) { return cursorStatus.get() }
func forgetCursorStatus() {
cursorStatus.Lock()
cursorStatus.at = time.Time{}
cursorStatus.Unlock()
}
func forgetCursorStatus() { cursorStatus.forget() }
func askCursorIdentity() (user, plan string, ok bool) {
path := CursorExecutable()
+4 -33
View File
@@ -53,41 +53,12 @@ func DevinCredentialsPath() string {
return filepath.Join(base, "devin", "credentials.toml")
}
var devinStatus struct {
sync.Mutex
at time.Time
refreshing bool
user, plan string
ok bool
}
var devinStatus = &cliIdentity{name: "devin", exe: func() string { return DevinExecutable() }, ask: func() (string, string, bool) { return askDevinIdentity() }}
// devinIdentity asks the CLI who is signed in. `devin auth status` takes a
// moment, so after the first answer a stale one is served while a fresh one
// is fetched behind it.
func devinIdentity() (user, plan string, ok bool) {
devinStatus.Lock()
defer devinStatus.Unlock()
if devinStatus.at.IsZero() {
devinStatus.user, devinStatus.plan, devinStatus.ok = askDevinIdentity()
devinStatus.at = time.Now()
} else if time.Since(devinStatus.at) > time.Minute && !devinStatus.refreshing {
devinStatus.refreshing = true
go func() {
u, p, ok := askDevinIdentity()
devinStatus.Lock()
devinStatus.user, devinStatus.plan, devinStatus.ok = u, p, ok
devinStatus.at, devinStatus.refreshing = time.Now(), false
devinStatus.Unlock()
}()
}
return devinStatus.user, devinStatus.plan, devinStatus.ok
}
// devinIdentity is who Devin's CLI says is signed in; see cliIdentity.
func devinIdentity() (user, plan string, ok bool) { return devinStatus.get() }
func forgetDevinStatus() {
devinStatus.Lock()
devinStatus.at = time.Time{}
devinStatus.Unlock()
}
func forgetDevinStatus() { devinStatus.forget() }
func askDevinIdentity() (user, plan string, ok bool) {
path := DevinExecutable()