clauddy 1.26.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/bin/clauddy.js +11 -1
- package/cursor.js +267 -0
- package/main.js +107 -28
- package/package.json +3 -1
- package/preload.js +4 -0
- package/reminders.js +3 -2
- package/renderer/companion.js +12 -8
- package/renderer/index.html +56 -8
- package/renderer/motion.css +0 -3
- package/renderer/pet.js +315 -148
- package/renderer/style.css +206 -6
- package/renderer/voice.js +12 -11
package/renderer/pet.js
CHANGED
|
@@ -148,6 +148,8 @@ function fmtReset(ms) {
|
|
|
148
148
|
function fmtResetClock(ms) {
|
|
149
149
|
if (!ms || ms <= 0) return null
|
|
150
150
|
const at = new Date(Date.now() + ms)
|
|
151
|
+
// a week or more out (Cursor's month), a weekday would be ambiguous
|
|
152
|
+
if (ms >= 6 * 86400000) return at.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
151
153
|
const time = at.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
152
154
|
return ms >= 86400000 ? `${at.toLocaleDateString([], { weekday: 'short' })} ${time}` : time
|
|
153
155
|
}
|
|
@@ -603,33 +605,35 @@ function takeRecap(was) {
|
|
|
603
605
|
return r
|
|
604
606
|
}
|
|
605
607
|
|
|
606
|
-
// Codex, followed whichever tab is on screen.
|
|
607
|
-
// runs, so a stale reading keeps the baseline: the next fresh one is
|
|
608
|
-
// against the last number actually seen.
|
|
609
|
-
|
|
610
|
-
function
|
|
611
|
-
const c =
|
|
612
|
-
if (!
|
|
613
|
-
|
|
608
|
+
// Codex and Cursor, followed whichever tab is on screen. Codex's logs only move
|
|
609
|
+
// while it runs, so a stale reading keeps the baseline: the next fresh one is
|
|
610
|
+
// compared against the last number actually seen.
|
|
611
|
+
const lastExtPct = { codex: null, cursor: null }
|
|
612
|
+
function listenExt(p) {
|
|
613
|
+
const c = dataOf(p)
|
|
614
|
+
if (!isOn(p) || !c) {
|
|
615
|
+
lastExtPct[p] = null
|
|
614
616
|
return
|
|
615
617
|
}
|
|
616
618
|
const pct = c.session?.pct
|
|
617
|
-
if (pct == null ||
|
|
618
|
-
const was =
|
|
619
|
-
|
|
619
|
+
if (pct == null || readingStale(c)) return
|
|
620
|
+
const was = lastExtPct[p]
|
|
621
|
+
lastExtPct[p] = pct
|
|
620
622
|
if (was == null) return
|
|
621
623
|
const fireAt = currentConfig.fireThreshold ?? 90
|
|
622
624
|
const reset = c.session.resetMs
|
|
623
|
-
|
|
625
|
+
const name = NAMES[p]
|
|
626
|
+
if (was - pct > 25)
|
|
627
|
+
say(Voice.codexResetLine(was, name, p === 'cursor' ? 'month' : 'window'), { headline: true })
|
|
624
628
|
else if (was < 100 && pct >= 100)
|
|
625
|
-
say(Voice.codexMaxedLine(fmtResetClock(reset)), { mood: 'sleepy' })
|
|
629
|
+
say(Voice.codexMaxedLine(fmtResetClock(reset), name), { mood: 'sleepy' })
|
|
626
630
|
else if (was < fireAt && pct >= fireAt && pct < 100)
|
|
627
|
-
say(Voice.codexFireLine(pct, reset, fmtReset), { headline: true, mood: 'fire' })
|
|
631
|
+
say(Voice.codexFireLine(pct, reset, fmtReset, name), { headline: true, mood: 'fire' })
|
|
628
632
|
}
|
|
629
633
|
|
|
630
634
|
// called by paint() with the state it just drew. The Claude remarks follow the
|
|
631
|
-
// Claude view; Codex
|
|
632
|
-
function listen(before, st, {
|
|
635
|
+
// Claude view; Codex and Cursor have their own, above.
|
|
636
|
+
function listen(before, st, { isExt, liveOn, sp, sessReset, proj }) {
|
|
633
637
|
// the saved config decides whether it talks at all, and it lands just after
|
|
634
638
|
// the first usage tick: speaking before it would ignore a "talk: false"
|
|
635
639
|
if (!heardConfig) return
|
|
@@ -643,8 +647,9 @@ function listen(before, st, { isCodex, liveOn, sp, sessReset, proj }) {
|
|
|
643
647
|
if (say(line, { headline: true, mood })) return
|
|
644
648
|
}
|
|
645
649
|
}
|
|
646
|
-
|
|
647
|
-
|
|
650
|
+
listenExt('codex')
|
|
651
|
+
listenExt('cursor')
|
|
652
|
+
if (isExt) return
|
|
648
653
|
const now = Date.now()
|
|
649
654
|
|
|
650
655
|
if (liveOn) {
|
|
@@ -701,6 +706,8 @@ function sampleLine(kind) {
|
|
|
701
706
|
}
|
|
702
707
|
if (kind === 'codex')
|
|
703
708
|
return Voice.codexFireLine(91, codexData?.session?.resetMs ?? reset, fmtReset)
|
|
709
|
+
if (kind === 'cursor')
|
|
710
|
+
return Voice.codexFireLine(91, cursorData?.session?.resetMs ?? reset, fmtReset, 'Cursor')
|
|
704
711
|
if (kind === 'maxed') return Voice.maxedLine(fmtResetClock(reset))
|
|
705
712
|
if (kind === 'welcome') return Voice.welcomeLine(3 * 3600000 + 12 * 60000, pct ?? null, fmtReset)
|
|
706
713
|
if (kind === 'streak') return Voice.streakLine(95 * 60000, fmtReset)
|
|
@@ -714,7 +721,11 @@ function sampleLine(kind) {
|
|
|
714
721
|
function sayDebug(kind) {
|
|
715
722
|
const st = [...document.body.classList].find((c) => c.startsWith('state-'))?.slice(6)
|
|
716
723
|
const mood =
|
|
717
|
-
kind === 'fire' || kind === 'codex'
|
|
724
|
+
kind === 'fire' || kind === 'codex' || kind === 'cursor'
|
|
725
|
+
? 'fire'
|
|
726
|
+
: kind === 'maxed'
|
|
727
|
+
? 'sleepy'
|
|
728
|
+
: moodOf(st)
|
|
718
729
|
return say(sampleLine(kind), { force: true, mood })
|
|
719
730
|
}
|
|
720
731
|
|
|
@@ -742,14 +753,18 @@ el('mute').addEventListener('click', (e) => {
|
|
|
742
753
|
})
|
|
743
754
|
|
|
744
755
|
// ---- services ---------------------------------------------------------------
|
|
745
|
-
// Claude and
|
|
746
|
-
// time: tabs own the chip, meters, breakdowns and Usage arrow. The expanded
|
|
747
|
-
// scene follows that view; compact pets follow activity across
|
|
756
|
+
// Claude, Codex and Cursor are monitored side by side, but the panel shows one
|
|
757
|
+
// at a time: tabs own the chip, meters, breakdowns and Usage arrow. The expanded
|
|
758
|
+
// scene follows that view; compact pets follow activity across every service.
|
|
748
759
|
const PROVIDER_KEY = 'clauddy.provider'
|
|
760
|
+
const PROVIDERS = ['claude', 'codex', 'cursor']
|
|
761
|
+
const NAMES = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor' }
|
|
749
762
|
let codexData = null // last Codex payload; null while Codex isn't enabled
|
|
763
|
+
let cursorData = null // last Cursor payload; null while Cursor isn't enabled
|
|
750
764
|
let pickedProvider = (() => {
|
|
751
765
|
try {
|
|
752
|
-
|
|
766
|
+
const p = localStorage.getItem(PROVIDER_KEY)
|
|
767
|
+
return PROVIDERS.includes(p) ? p : 'claude'
|
|
753
768
|
} catch {
|
|
754
769
|
return 'claude'
|
|
755
770
|
}
|
|
@@ -757,12 +772,19 @@ let pickedProvider = (() => {
|
|
|
757
772
|
|
|
758
773
|
const claudeOn = () => document.body.classList.contains('auth-on')
|
|
759
774
|
const codexOn = () => !!currentConfig.codex
|
|
760
|
-
const
|
|
775
|
+
const cursorOn = () => !!currentConfig.cursor
|
|
776
|
+
const isOn = (p) => (p === 'claude' ? claudeOn() : p === 'codex' ? codexOn() : cursorOn())
|
|
777
|
+
const connected = () => PROVIDERS.filter(isOn)
|
|
778
|
+
// Codex and Cursor payloads; Claude's live in lastData + realUsage
|
|
779
|
+
const dataOf = (p) => (p === 'codex' ? codexData : p === 'cursor' ? cursorData : null)
|
|
780
|
+
// each service's two meters: the window that resets, and the one beside it
|
|
781
|
+
const sessionOf = (p) => (p === 'claude' ? realUsage?.session : dataOf(p)?.session)
|
|
782
|
+
const secondOf = (p) =>
|
|
783
|
+
p === 'claude' ? realUsage?.week : p === 'codex' ? codexData?.weekly : cursorData?.api
|
|
761
784
|
// the service on screen: the pick, unless that one isn't there any more
|
|
762
785
|
function viewProvider() {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
return pickedProvider
|
|
786
|
+
const on = connected()
|
|
787
|
+
return on.includes(pickedProvider) ? pickedProvider : on[0] || 'claude'
|
|
766
788
|
}
|
|
767
789
|
|
|
768
790
|
function pickProvider(p) {
|
|
@@ -776,17 +798,31 @@ function pickProvider(p) {
|
|
|
776
798
|
|
|
777
799
|
const STALE_MS = 15 * 60000
|
|
778
800
|
const CODEX_SLEEP_MS = 5 * 60000 // main's default sleepThresholdMs
|
|
779
|
-
// Codex logs are only written while it runs, so a reading can be hours old
|
|
780
|
-
|
|
801
|
+
// Codex logs are only written while it runs, so a reading can be hours old;
|
|
802
|
+
// Cursor's comes from a fetch that can fail. Either way: say how old it is.
|
|
803
|
+
const readingStale = (c) =>
|
|
781
804
|
!!c && (c.session?.expired || (c.limitsAt != null && Date.now() - c.limitsAt > STALE_MS))
|
|
782
805
|
|
|
783
|
-
//
|
|
784
|
-
|
|
806
|
+
// the highest session % across connected services, and which ones are at or
|
|
807
|
+
// past `from`; an expired Codex or Cursor reading is null, so it never counts
|
|
808
|
+
function sessionHeat(from) {
|
|
809
|
+
const known = connected()
|
|
810
|
+
.map((p) => [p, sessionOf(p)?.pct])
|
|
811
|
+
.filter(([, pct]) => pct != null)
|
|
812
|
+
return {
|
|
813
|
+
pct: Math.max(0, ...known.map(([, pct]) => pct)),
|
|
814
|
+
providers: known.filter(([, pct]) => pct >= from).map(([p]) => p),
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Codex's or Cursor's numbers in the shape the panel draws. Codex's logs carry
|
|
819
|
+
// tokens but no activity; Cursor's transcripts carry activity but no tokens.
|
|
820
|
+
function extView(p, c) {
|
|
785
821
|
const idleFor = c.lastSeen ? Date.now() - c.lastSeen : null
|
|
786
822
|
return {
|
|
787
823
|
d: {
|
|
788
824
|
active: c.active,
|
|
789
|
-
activity: null,
|
|
825
|
+
activity: p === 'cursor' ? c.activity : null,
|
|
790
826
|
// no data is not the same as asleep
|
|
791
827
|
sleeping: !c.active && idleFor != null && idleFor >= CODEX_SLEEP_MS,
|
|
792
828
|
tokensPerMin: 0,
|
|
@@ -799,82 +835,83 @@ function codexView(c) {
|
|
|
799
835
|
monthTokens: c.monthTokens ?? null,
|
|
800
836
|
},
|
|
801
837
|
sess: c.session,
|
|
802
|
-
week: c.weekly,
|
|
838
|
+
week: p === 'cursor' ? c.api : c.weekly,
|
|
803
839
|
stale: c.limitsAt != null && Date.now() - c.limitsAt > STALE_MS ? c.limitsAt : null,
|
|
804
840
|
}
|
|
805
841
|
}
|
|
806
842
|
|
|
807
|
-
// a Codex meter caption: its reset, or how old the reading is when
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
843
|
+
// a Codex or Cursor meter caption: its reset, or how old the reading is when
|
|
844
|
+
// that's what matters. Cursor has no token counts, so `tokens` is null there.
|
|
845
|
+
function extSub(w, tokens, stale, empty = 'limits not recorded yet') {
|
|
846
|
+
const t = tokens == null ? '' : `${fmtTokens(tokens)} tokens`
|
|
847
|
+
const join = (...parts) => parts.filter(Boolean).join(' · ')
|
|
848
|
+
if (!w) return join(empty, t)
|
|
849
|
+
if (w.pct == null) return join('waiting for a fresh reading', t)
|
|
812
850
|
if (stale) {
|
|
813
|
-
const at = w.resetMs != null ? `resets ${fmtResetClock(w.resetMs)}
|
|
814
|
-
return
|
|
851
|
+
const at = w.resetMs != null ? `resets ${fmtResetClock(w.resetMs)}` : ''
|
|
852
|
+
return join(at, `read ${fmtReset(Date.now() - stale)} ago`)
|
|
815
853
|
}
|
|
816
|
-
|
|
817
|
-
return `${reset}${t}`
|
|
854
|
+
return join(w.resetMs != null ? `resets in ${fmtResetIn(w.resetMs)}` : '', t) || '—'
|
|
818
855
|
}
|
|
819
856
|
|
|
820
857
|
const pctText = (v) => (v == null ? '—' : `${Math.round(v)}%`)
|
|
821
858
|
const warnAt = () => Math.min(...(currentConfig.alertThresholds || [80, 95]))
|
|
822
859
|
|
|
823
|
-
// tabs + the collapsed
|
|
860
|
+
// tabs + the collapsed dock: every service's session %, whichever is on screen
|
|
824
861
|
function paintServices(view) {
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
document.body.classList.toggle('
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
862
|
+
const on = connected()
|
|
863
|
+
const many = on.length > 1
|
|
864
|
+
document.body.classList.toggle('dual', many)
|
|
865
|
+
document.body.classList.toggle('trio', on.length > 2)
|
|
866
|
+
for (const p of ['codex', 'cursor']) document.body.classList.toggle(`view-${p}`, view === p)
|
|
867
|
+
// collapsed with several: when the selected service's window resets
|
|
868
|
+
const resetMs = sessionOf(view)?.resetMs
|
|
869
|
+
el('mini-reset').hidden = !many || resetMs == null
|
|
831
870
|
el('mini-reset').textContent = resetMs == null ? '' : `resets ${fmtResetClock(resetMs)}`
|
|
832
|
-
el('service-panel').setAttribute('role',
|
|
833
|
-
if (
|
|
871
|
+
el('service-panel').setAttribute('role', many ? 'tabpanel' : 'region')
|
|
872
|
+
if (many) {
|
|
834
873
|
el('service-panel').setAttribute('aria-labelledby', `tab-${view}`)
|
|
835
874
|
el('service-panel').removeAttribute('aria-label')
|
|
836
875
|
} else {
|
|
837
876
|
el('service-panel').removeAttribute('aria-labelledby')
|
|
838
|
-
el('service-panel').setAttribute('aria-label', `${view
|
|
877
|
+
el('service-panel').setAttribute('aria-label', `${NAMES[view]} usage`)
|
|
839
878
|
return
|
|
840
879
|
}
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
week: cl?.week?.pct ?? null,
|
|
847
|
-
active: !!lastData?.active,
|
|
848
|
-
},
|
|
849
|
-
codex: {
|
|
850
|
-
pct: cx?.session?.pct ?? null,
|
|
851
|
-
week: cx?.weekly?.pct ?? null,
|
|
852
|
-
active: !!cx?.active,
|
|
853
|
-
stale: codexStale(cx),
|
|
854
|
-
},
|
|
880
|
+
// what each service calls its two meters
|
|
881
|
+
const labels = {
|
|
882
|
+
claude: ['session', 'weekly'],
|
|
883
|
+
codex: ['session', 'weekly'],
|
|
884
|
+
cursor: ['month', 'API'],
|
|
855
885
|
}
|
|
856
|
-
for (const
|
|
857
|
-
const on = p === view
|
|
858
|
-
const urgent = Math.max(r.pct ?? 0, r.week ?? 0) >= warnAt()
|
|
886
|
+
for (const p of PROVIDERS) {
|
|
859
887
|
const tab = el(`tab-${p}`)
|
|
860
|
-
|
|
861
|
-
tab.
|
|
862
|
-
|
|
888
|
+
const line = document.querySelector(`.mini-source[data-provider="${p}"]`)
|
|
889
|
+
tab.hidden = !on.includes(p)
|
|
890
|
+
line.hidden = tab.hidden
|
|
891
|
+
if (tab.hidden) continue
|
|
892
|
+
const pct = sessionOf(p)?.pct ?? null
|
|
893
|
+
const second = secondOf(p)?.pct ?? null
|
|
894
|
+
const active = p === 'claude' ? !!lastData?.active : !!dataOf(p)?.active
|
|
895
|
+
const stale = p !== 'claude' && readingStale(dataOf(p))
|
|
896
|
+
const selected = p === view
|
|
897
|
+
const urgent = Math.max(pct ?? 0, second ?? 0) >= warnAt()
|
|
898
|
+
tab.setAttribute('aria-selected', String(selected))
|
|
899
|
+
tab.tabIndex = selected ? 0 : -1
|
|
900
|
+
tab.classList.toggle('active', active)
|
|
863
901
|
tab.classList.toggle('urgent', urgent)
|
|
864
902
|
tab.querySelector('.tab-alert').hidden = !urgent
|
|
865
|
-
el(`tab-${p}-value`).textContent = pctText(
|
|
866
|
-
const
|
|
867
|
-
tab.title = `${
|
|
868
|
-
|
|
869
|
-
line.setAttribute('aria-pressed', String(on))
|
|
903
|
+
el(`tab-${p}-value`).textContent = pctText(pct)
|
|
904
|
+
const [first, next] = labels[p]
|
|
905
|
+
tab.title = `${NAMES[p]} ${first} ${pctText(pct)} · ${next} ${pctText(second)}${active ? ' · working' : ''}${stale ? ' · not updated recently' : ''}`
|
|
906
|
+
line.setAttribute('aria-pressed', String(selected))
|
|
870
907
|
line.title = tab.title
|
|
871
908
|
line.setAttribute('aria-label', `Show ${tab.title}`)
|
|
872
909
|
line.classList.toggle('urgent', urgent)
|
|
873
910
|
tab.setAttribute('aria-label', `Show ${tab.title}`)
|
|
874
|
-
el(`mini-${p}-value`).textContent = pctText(
|
|
911
|
+
el(`mini-${p}-value`).textContent = pctText(pct)
|
|
875
912
|
const bar = el(`mini-${p}-bar`)
|
|
876
|
-
bar.style.width = `${Math.min(
|
|
877
|
-
bar.classList.toggle('hot', (
|
|
913
|
+
bar.style.width = `${Math.min(pct ?? 0, 100)}%`
|
|
914
|
+
bar.classList.toggle('hot', (pct ?? 0) >= 80)
|
|
878
915
|
}
|
|
879
916
|
}
|
|
880
917
|
|
|
@@ -885,7 +922,7 @@ for (const b of document.querySelectorAll('#harness-tabs button, .mini-source'))
|
|
|
885
922
|
})
|
|
886
923
|
}
|
|
887
924
|
el('harness-tabs').addEventListener('keydown', (e) => {
|
|
888
|
-
const providers =
|
|
925
|
+
const providers = connected()
|
|
889
926
|
const current = providers.indexOf(e.target.dataset.provider)
|
|
890
927
|
if (current < 0) return
|
|
891
928
|
let next
|
|
@@ -905,7 +942,7 @@ let sizeLoaded = false
|
|
|
905
942
|
let peekTimer = null
|
|
906
943
|
let nativePetInside = null
|
|
907
944
|
let noticeTimer = null
|
|
908
|
-
let reminders = { claude: null, codex: null }
|
|
945
|
+
let reminders = { claude: null, codex: null, cursor: null }
|
|
909
946
|
const activityTracker = Companion.createActivityTracker()
|
|
910
947
|
|
|
911
948
|
function setPetPeek(on) {
|
|
@@ -980,37 +1017,44 @@ for (const button of document.querySelectorAll('#pet-glance button[data-provider
|
|
|
980
1017
|
button.addEventListener('click', () => pickProvider(button.dataset.provider))
|
|
981
1018
|
}
|
|
982
1019
|
function paintCompanion(view) {
|
|
983
|
-
for (const provider of
|
|
984
|
-
const
|
|
985
|
-
const session = provider === 'claude' ? realUsage?.session : codexData?.session
|
|
1020
|
+
for (const provider of PROVIDERS) {
|
|
1021
|
+
const session = sessionOf(provider)
|
|
986
1022
|
const button = el(`glance-${provider}`)
|
|
987
|
-
button.hidden = !
|
|
1023
|
+
button.hidden = !isOn(provider)
|
|
988
1024
|
button.setAttribute('aria-pressed', String(provider === view))
|
|
989
1025
|
button.classList.toggle('urgent', session?.pct >= warnAt())
|
|
990
1026
|
el(`glance-${provider}-value`).textContent = pctText(session?.pct)
|
|
991
|
-
const stale = provider
|
|
1027
|
+
const stale = provider !== 'claude' && readingStale(dataOf(provider))
|
|
992
1028
|
button.title = stale
|
|
993
1029
|
? 'Last recorded usage · waiting for a fresh reading'
|
|
994
1030
|
: `Show ${provider} usage`
|
|
995
1031
|
button.classList.toggle('stale-reading', !!stale)
|
|
996
1032
|
}
|
|
997
|
-
el('glance-empty').hidden =
|
|
998
|
-
const session = view
|
|
1033
|
+
el('glance-empty').hidden = connected().length > 0
|
|
1034
|
+
const session = sessionOf(view)
|
|
999
1035
|
const reminder = reminders[view]
|
|
1036
|
+
const ext = dataOf(view)
|
|
1000
1037
|
const stale =
|
|
1001
|
-
view === '
|
|
1002
|
-
? !
|
|
1003
|
-
: !
|
|
1038
|
+
view === 'claude'
|
|
1039
|
+
? !realUsageReadAt || Date.now() - realUsageReadAt > STALE_MS
|
|
1040
|
+
: !ext?.limitsAt || readingStale(ext)
|
|
1004
1041
|
const available = session?.pct != null && session.resetMs > 0 && !stale
|
|
1005
|
-
const name = view
|
|
1042
|
+
const name = NAMES[view]
|
|
1006
1043
|
for (const id of ['reminder-toggle', 'mini-reminder']) {
|
|
1007
1044
|
const button = el(id)
|
|
1008
1045
|
button.hidden = !reminder && (!available || session.pct < warnAt())
|
|
1009
1046
|
button.setAttribute('aria-pressed', String(!!reminder))
|
|
1010
1047
|
button.textContent = reminder ? '✓ Reminder on' : 'Notify at reset'
|
|
1048
|
+
const at = reminder ? new Date(reminder.at) : null
|
|
1049
|
+
// a month-out reset needs its date, not just a time
|
|
1050
|
+
const when = at
|
|
1051
|
+
? at.getTime() - Date.now() >= 86400000
|
|
1052
|
+
? at.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
1053
|
+
: at.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
1054
|
+
: ''
|
|
1011
1055
|
button.title = reminder
|
|
1012
|
-
? `Cancel ${name} reminder for ${
|
|
1013
|
-
: `Remind me when ${name}'s session reset is due`
|
|
1056
|
+
? `Cancel ${name} reminder for ${when}`
|
|
1057
|
+
: `Remind me when ${name}'s ${view === 'cursor' ? 'monthly' : 'session'} reset is due`
|
|
1014
1058
|
}
|
|
1015
1059
|
}
|
|
1016
1060
|
function companionNotice(text, provider = null) {
|
|
@@ -1033,14 +1077,14 @@ for (const id of ['reminder-toggle', 'mini-reminder']) {
|
|
|
1033
1077
|
})
|
|
1034
1078
|
}
|
|
1035
1079
|
window.api.onReminders((state) => {
|
|
1036
|
-
reminders = state || { claude: null, codex: null }
|
|
1080
|
+
reminders = state || { claude: null, codex: null, cursor: null }
|
|
1037
1081
|
if (state?.error) companionNotice(state.error)
|
|
1038
1082
|
paintCompanion(viewProvider())
|
|
1039
1083
|
fitSize()
|
|
1040
1084
|
})
|
|
1041
1085
|
window.api.onReminderDue((event) => {
|
|
1042
1086
|
if (!event.isCurrent) return
|
|
1043
|
-
const name = event.provider
|
|
1087
|
+
const name = NAMES[event.provider] || 'Claude'
|
|
1044
1088
|
companionNotice(
|
|
1045
1089
|
event.confirmed ? `${name} · budget is back!` : `${name} · reset time reached`,
|
|
1046
1090
|
event.provider,
|
|
@@ -1052,13 +1096,14 @@ function reactToProvider(provider, data) {
|
|
|
1052
1096
|
const cue = activityTracker.observe(provider, data)
|
|
1053
1097
|
if (!cue || document.body.classList.contains('settings-open')) return
|
|
1054
1098
|
if (!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
1055
|
-
|
|
1099
|
+
// in tab order: Claude to the left, Codex to the right, Cursor down-right
|
|
1100
|
+
const [x, y] = { claude: [-3, 0], codex: [3, 0], cursor: [3, 1] }[provider] || [0, 0]
|
|
1056
1101
|
el('eyes').animate(
|
|
1057
1102
|
[
|
|
1058
|
-
{ transform: '
|
|
1059
|
-
{ transform: `
|
|
1060
|
-
{ transform: `
|
|
1061
|
-
{ transform: '
|
|
1103
|
+
{ transform: 'translate(0, 0)' },
|
|
1104
|
+
{ transform: `translate(${x}px, ${y}px)`, offset: 0.3 },
|
|
1105
|
+
{ transform: `translate(${x}px, ${y}px)`, offset: 0.7 },
|
|
1106
|
+
{ transform: 'translate(0, 0)' },
|
|
1062
1107
|
],
|
|
1063
1108
|
{ duration: 1100, easing: 'ease-in-out' },
|
|
1064
1109
|
)
|
|
@@ -1066,23 +1111,28 @@ function reactToProvider(provider, data) {
|
|
|
1066
1111
|
}
|
|
1067
1112
|
|
|
1068
1113
|
function paintActivity() {
|
|
1069
|
-
const activity = Companion.currentActivity(
|
|
1070
|
-
claudeOn() ? claudeActivityData : null,
|
|
1071
|
-
codexOn() ? codexData : null,
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1114
|
+
const activity = Companion.currentActivity({
|
|
1115
|
+
claude: claudeOn() ? claudeActivityData : null,
|
|
1116
|
+
codex: codexOn() ? codexData : null,
|
|
1117
|
+
cursor: cursorOn() ? cursorData : null,
|
|
1118
|
+
})
|
|
1119
|
+
paintLogos(activity.providers, activity.activity)
|
|
1120
|
+
return activity
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// the logos under the pet name who it's about: the workers, or the service on fire
|
|
1124
|
+
function paintLogos(providers, caption) {
|
|
1125
|
+
const names = providers.map((p) => NAMES[p]).join(' + ')
|
|
1126
|
+
const label = names ? `${names} · ${caption}` : ''
|
|
1075
1127
|
for (const id of ['pet-activity', 'mini-workers']) {
|
|
1076
1128
|
const badge = el(id)
|
|
1077
1129
|
badge.hidden = !names
|
|
1078
1130
|
badge.setAttribute('aria-label', label)
|
|
1079
1131
|
for (const logo of badge.querySelectorAll('[data-provider]')) {
|
|
1080
|
-
logo.hidden = !
|
|
1132
|
+
logo.hidden = !providers.includes(logo.dataset.provider)
|
|
1081
1133
|
}
|
|
1082
1134
|
}
|
|
1083
|
-
el('mini-dot').hidden =
|
|
1084
|
-
activity.providers.length > 0 && document.body.classList.contains('collapsed')
|
|
1085
|
-
return activity
|
|
1135
|
+
el('mini-dot').hidden = providers.length > 0 && document.body.classList.contains('collapsed')
|
|
1086
1136
|
}
|
|
1087
1137
|
|
|
1088
1138
|
// main render
|
|
@@ -1105,22 +1155,27 @@ function render(d) {
|
|
|
1105
1155
|
|
|
1106
1156
|
function paint() {
|
|
1107
1157
|
setPlan(el('codex-plan'), codexData?.plan)
|
|
1158
|
+
setPlan(el('cursor-plan'), cursorData?.plan)
|
|
1159
|
+
paintConnections()
|
|
1108
1160
|
const d0 = lastData
|
|
1109
1161
|
if (!d0) return
|
|
1110
1162
|
const view = viewProvider()
|
|
1111
1163
|
paintServices(view)
|
|
1112
1164
|
paintCompanion(view)
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
const
|
|
1165
|
+
// Codex or Cursor on screen: their own numbers, never Claude's
|
|
1166
|
+
const ext = view !== 'claude' ? dataOf(view) : null
|
|
1167
|
+
const isExt = !!ext
|
|
1168
|
+
const isCursor = view === 'cursor' && isExt
|
|
1169
|
+
const cv = isExt ? extView(view, ext) : null
|
|
1170
|
+
const d = isExt ? cv.d : d0
|
|
1171
|
+
// % comes only from the connected account (or the service's own data) — no estimates
|
|
1172
|
+
const liveOn = isExt || !!realUsage
|
|
1118
1173
|
document.body.classList.toggle('live', liveOn)
|
|
1119
|
-
const sessPct =
|
|
1120
|
-
const sessReset =
|
|
1174
|
+
const sessPct = isExt ? (cv.sess?.pct ?? null) : liveOn ? realUsage.session.pct : 0
|
|
1175
|
+
const sessReset = isExt ? (cv.sess?.resetMs ?? null) : liveOn ? realUsage.session.resetMs : null
|
|
1121
1176
|
const sessActive = liveOn && sessReset != null
|
|
1122
|
-
const wkPct =
|
|
1123
|
-
const wkReset =
|
|
1177
|
+
const wkPct = isExt ? (cv.week?.pct ?? null) : liveOn ? realUsage.week.pct : 0
|
|
1178
|
+
const wkReset = isExt ? (cv.week?.resetMs ?? null) : liveOn ? realUsage.week.resetMs : null
|
|
1124
1179
|
const sp = sessPct ?? 0
|
|
1125
1180
|
const wp = wkPct ?? 0
|
|
1126
1181
|
|
|
@@ -1140,7 +1195,20 @@ function paint() {
|
|
|
1140
1195
|
const activity = paintActivity()
|
|
1141
1196
|
const collapsedNow = document.body.classList.contains('collapsed')
|
|
1142
1197
|
if (collapsedNow) {
|
|
1143
|
-
|
|
1198
|
+
// work wins; at rest, the hottest connected session still sets the mood,
|
|
1199
|
+
// and its logo takes the status dot's place
|
|
1200
|
+
const heat = sessionHeat(fireAt)
|
|
1201
|
+
st = activity.providers.length
|
|
1202
|
+
? 'working'
|
|
1203
|
+
: heat.pct >= 100
|
|
1204
|
+
? 'tired'
|
|
1205
|
+
: heat.pct >= fireAt
|
|
1206
|
+
? 'stressed'
|
|
1207
|
+
: activity.sleeping
|
|
1208
|
+
? 'sleeping'
|
|
1209
|
+
: 'idle'
|
|
1210
|
+
if (st === 'tired') paintLogos(sessionHeat(100).providers, 'maxed out')
|
|
1211
|
+
else if (st === 'stressed') paintLogos(heat.providers, 'on fire')
|
|
1144
1212
|
curActivity = activity.providers.length ? activity.activity : null
|
|
1145
1213
|
}
|
|
1146
1214
|
// dev override from `./pet <state>` (base states or an activity name)
|
|
@@ -1185,16 +1253,23 @@ function paint() {
|
|
|
1185
1253
|
: 'idle'
|
|
1186
1254
|
el('status-text').textContent = word
|
|
1187
1255
|
el('mini-text').textContent = word
|
|
1188
|
-
|
|
1189
|
-
|
|
1256
|
+
// Cursor's transcripts carry no tokens: when it last ran is what there is to say
|
|
1257
|
+
const ranAgo = isCursor && ext.lastSeen ? Date.now() - ext.lastSeen : null
|
|
1258
|
+
el('rate').textContent = isCursor
|
|
1259
|
+
? ranAgo == null
|
|
1260
|
+
? 'no agent runs yet'
|
|
1261
|
+
: ranAgo < 60000
|
|
1262
|
+
? 'agent ran just now'
|
|
1263
|
+
: `agent ran ${fmtReset(ranAgo)} ago`
|
|
1264
|
+
: d.active && d.tokensPerMin > 0
|
|
1190
1265
|
? `${fmtTokens(d.tokensPerMin)} tok/min`
|
|
1191
1266
|
: `${fmtTokens(d.today.tokens)} tokens today`
|
|
1192
1267
|
|
|
1193
1268
|
const was = prevPct[view]
|
|
1194
1269
|
if (liveOn && was != null && sessActive && sessPct != null && was - sessPct > 25) {
|
|
1195
1270
|
celebrate()
|
|
1196
|
-
// Codex's own
|
|
1197
|
-
if (!
|
|
1271
|
+
// Codex's and Cursor's own resets are called out by listenExt, on screen or not
|
|
1272
|
+
if (!isExt) say(Voice.recapLine(takeRecap(was), fmtTokens, fmtReset), { headline: true })
|
|
1198
1273
|
}
|
|
1199
1274
|
prevPct[view] = sessPct
|
|
1200
1275
|
el('session-pct').textContent = pctText(liveOn ? sessPct : 0)
|
|
@@ -1213,15 +1288,24 @@ function paint() {
|
|
|
1213
1288
|
sf.style.width = `${sp}%`
|
|
1214
1289
|
sf.classList.toggle('high', sp >= 80)
|
|
1215
1290
|
setLevel(sf, sp)
|
|
1216
|
-
el('session-
|
|
1217
|
-
|
|
1291
|
+
el('session-label').textContent = isCursor ? 'this month · included' : 'current session'
|
|
1292
|
+
el('week-label').textContent = isCursor ? 'API models · this month' : 'weekly · all models'
|
|
1293
|
+
el('session-sub').textContent = isExt
|
|
1294
|
+
? isCursor
|
|
1295
|
+
? extSub(
|
|
1296
|
+
cv.sess,
|
|
1297
|
+
null,
|
|
1298
|
+
cv.stale,
|
|
1299
|
+
ext.signedOut ? 'sign in to the Cursor app' : 'no reading yet',
|
|
1300
|
+
)
|
|
1301
|
+
: extSub(cv.sess, d.session.tokens, cv.stale)
|
|
1218
1302
|
: sessActive
|
|
1219
1303
|
? `resets in ${fmtResetIn(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
|
|
1220
1304
|
: 'no active session'
|
|
1221
1305
|
|
|
1222
|
-
// where this pace is taking you — Claude's only:
|
|
1306
|
+
// where this pace is taking you — Claude's only: the others have no trail to read
|
|
1223
1307
|
const proj =
|
|
1224
|
-
!
|
|
1308
|
+
!isExt && sessActive && sp < 100 ? burn.project(sp, sessReset, d.session.tokens) : null
|
|
1225
1309
|
const pe = el('session-proj')
|
|
1226
1310
|
pe.hidden = !proj
|
|
1227
1311
|
pe.classList.toggle('tight', proj?.kind === 'eta')
|
|
@@ -1236,13 +1320,15 @@ function paint() {
|
|
|
1236
1320
|
wf.style.width = `${wp}%`
|
|
1237
1321
|
wf.classList.toggle('high', wp >= 80)
|
|
1238
1322
|
setLevel(wf, wp)
|
|
1239
|
-
el('week-sub').textContent =
|
|
1240
|
-
?
|
|
1323
|
+
el('week-sub').textContent = isExt
|
|
1324
|
+
? isCursor
|
|
1325
|
+
? extSub(cv.week, null, cv.stale, '—')
|
|
1326
|
+
: extSub(cv.week, d.week.tokens, cv.stale)
|
|
1241
1327
|
: wkReset != null
|
|
1242
1328
|
? `resets in ${fmtResetIn(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
|
|
1243
1329
|
: `${fmtTokens(d.week.tokens)} tokens · last 7 days`
|
|
1244
1330
|
|
|
1245
|
-
renderScoped(!
|
|
1331
|
+
renderScoped(!isExt && liveOn ? realUsage.scoped || [] : [], d.byModel || [])
|
|
1246
1332
|
renderModels(d.byModel || [])
|
|
1247
1333
|
renderProjects(d.byProject || [])
|
|
1248
1334
|
renderHeat(d.days30 || [])
|
|
@@ -1262,7 +1348,7 @@ function paint() {
|
|
|
1262
1348
|
stopEating()
|
|
1263
1349
|
}
|
|
1264
1350
|
|
|
1265
|
-
listen(before, st, {
|
|
1351
|
+
listen(before, st, { isExt, liveOn, sp, sessReset, proj })
|
|
1266
1352
|
fitSize()
|
|
1267
1353
|
}
|
|
1268
1354
|
|
|
@@ -1273,10 +1359,12 @@ function fitSize() {
|
|
|
1273
1359
|
requestAnimationFrame(() => {
|
|
1274
1360
|
const collapsed = document.body.classList.contains('collapsed')
|
|
1275
1361
|
const zoom = Number.parseFloat(document.body.style.zoom) || 1
|
|
1276
|
-
// The
|
|
1362
|
+
// The multi-service dock trades height for a little horizontal room, and
|
|
1363
|
+
// three readings widen the pet-only glance.
|
|
1277
1364
|
const dual = document.body.classList.contains('dual')
|
|
1365
|
+
const trio = document.body.classList.contains('trio')
|
|
1278
1366
|
const petOnly = document.body.classList.contains('pet-only')
|
|
1279
|
-
const w = (petOnly ? 168 : collapsed ? (dual ? 240 : 192) : 304) * zoom
|
|
1367
|
+
const w = (petOnly ? (trio ? 212 : 168) : collapsed ? (dual ? 240 : 192) : 304) * zoom
|
|
1280
1368
|
// offsetHeight never reflects a CSS zoom applied to an ancestor (confirmed
|
|
1281
1369
|
// empirically against this Electron build) — so measure the unzoomed content
|
|
1282
1370
|
// height, then scale the whole thing (content + margin) ourselves
|
|
@@ -1318,6 +1406,9 @@ window.api.onConfig((cfg) => {
|
|
|
1318
1406
|
document.body.classList.toggle('codex-on', !!currentConfig.codex)
|
|
1319
1407
|
if (!currentConfig.codex) codexData = null
|
|
1320
1408
|
if (!currentConfig.codex) activityTracker.forget('codex')
|
|
1409
|
+
document.body.classList.toggle('cursor-on', !!currentConfig.cursor)
|
|
1410
|
+
if (!currentConfig.cursor) cursorData = null
|
|
1411
|
+
if (!currentConfig.cursor) activityTracker.forget('cursor')
|
|
1321
1412
|
if (!sizeLoaded) {
|
|
1322
1413
|
sizeLoaded = true
|
|
1323
1414
|
let size = 'expanded'
|
|
@@ -1339,6 +1430,11 @@ window.api.onCodex((c) => {
|
|
|
1339
1430
|
reactToProvider('codex', codexOn() ? c : null)
|
|
1340
1431
|
paint()
|
|
1341
1432
|
})
|
|
1433
|
+
window.api.onCursor((c) => {
|
|
1434
|
+
cursorData = c || null
|
|
1435
|
+
reactToProvider('cursor', cursorOn() ? c : null)
|
|
1436
|
+
paint()
|
|
1437
|
+
})
|
|
1342
1438
|
window.api.onRealUsage((u) => {
|
|
1343
1439
|
realUsage = u || null
|
|
1344
1440
|
realUsageReadAt = u ? Date.now() : 0
|
|
@@ -1357,7 +1453,7 @@ window.api.onAuthState((s) => {
|
|
|
1357
1453
|
endLogin()
|
|
1358
1454
|
showProfile(null)
|
|
1359
1455
|
}
|
|
1360
|
-
paint() // losing Claude can leave Codex as the only service
|
|
1456
|
+
paint() // losing Claude can leave Codex or Cursor as the only service
|
|
1361
1457
|
if (document.body.classList.contains('settings-open')) fitSize()
|
|
1362
1458
|
})
|
|
1363
1459
|
|
|
@@ -1366,6 +1462,7 @@ let lastProfile = null
|
|
|
1366
1462
|
function showProfile(p) {
|
|
1367
1463
|
lastProfile = p
|
|
1368
1464
|
paintChip()
|
|
1465
|
+
paintConnections()
|
|
1369
1466
|
}
|
|
1370
1467
|
|
|
1371
1468
|
// The chip is also the account switcher, so it must not vanish just because the
|
|
@@ -1384,14 +1481,16 @@ function paintChip() {
|
|
|
1384
1481
|
el('acc-ok').title = email || 'Connected'
|
|
1385
1482
|
const chip = el('account-chip')
|
|
1386
1483
|
const mini = el('mini-acct')
|
|
1387
|
-
// the Codex
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1484
|
+
// the Codex and Cursor views never borrow Claude's identity
|
|
1485
|
+
const view = viewProvider()
|
|
1486
|
+
if (view !== 'claude') {
|
|
1487
|
+
const plan = dataOf(view)?.plan || null
|
|
1488
|
+
const name = NAMES[view]
|
|
1489
|
+
el('ac-email').textContent = name
|
|
1490
|
+
chip.title = `${name} on this computer`
|
|
1392
1491
|
setPlan(el('ac-plan'), plan)
|
|
1393
1492
|
chip.hidden = false
|
|
1394
|
-
el('mini-acct-name').textContent =
|
|
1493
|
+
el('mini-acct-name').textContent = name
|
|
1395
1494
|
setPlan(el('mini-acct-plan'), plan)
|
|
1396
1495
|
mini.hidden = false
|
|
1397
1496
|
return
|
|
@@ -1533,8 +1632,8 @@ function toggleAccountMenu() {
|
|
|
1533
1632
|
|
|
1534
1633
|
el('account-chip').addEventListener('click', (e) => {
|
|
1535
1634
|
e.stopPropagation()
|
|
1536
|
-
// Codex
|
|
1537
|
-
if (viewProvider()
|
|
1635
|
+
// Codex and Cursor have no account switcher: their chip leads to their card
|
|
1636
|
+
if (viewProvider() !== 'claude') return openSettings(viewProvider())
|
|
1538
1637
|
toggleAccountMenu()
|
|
1539
1638
|
})
|
|
1540
1639
|
// clicking anywhere else — the pet, the gear, another app — puts it away
|
|
@@ -1714,6 +1813,34 @@ el('codex-setup-cancel').addEventListener('click', () => {
|
|
|
1714
1813
|
})
|
|
1715
1814
|
el('codex-disconnect').addEventListener('click', () => window.api.codexEnable(false))
|
|
1716
1815
|
|
|
1816
|
+
// Settings → Cursor: the same, but what it looks for is the Cursor app's login
|
|
1817
|
+
function endCursorSetup() {
|
|
1818
|
+
document.body.classList.remove('cursor-setup')
|
|
1819
|
+
el('cursor-setup').hidden = true
|
|
1820
|
+
el('cursor-hint').textContent = 'Track Cursor alongside Claude, in this pet.'
|
|
1821
|
+
}
|
|
1822
|
+
el('cursor-connect').addEventListener('click', () => {
|
|
1823
|
+
el('cursor-hint').textContent = 'Looking…'
|
|
1824
|
+
window.api.cursorDetect()
|
|
1825
|
+
})
|
|
1826
|
+
el('cursor-retry').addEventListener('click', () => window.api.cursorDetect())
|
|
1827
|
+
window.api.onCursorDetected((r) => {
|
|
1828
|
+
if (r?.found) {
|
|
1829
|
+
endCursorSetup()
|
|
1830
|
+
window.api.cursorEnable(true)
|
|
1831
|
+
return
|
|
1832
|
+
}
|
|
1833
|
+
document.body.classList.add('cursor-setup')
|
|
1834
|
+
el('cursor-setup').hidden = false
|
|
1835
|
+
el('cursor-hint').textContent = 'Not signed in — open the Cursor app and log in'
|
|
1836
|
+
fitSize()
|
|
1837
|
+
})
|
|
1838
|
+
el('cursor-setup-cancel').addEventListener('click', () => {
|
|
1839
|
+
endCursorSetup()
|
|
1840
|
+
fitSize()
|
|
1841
|
+
})
|
|
1842
|
+
el('cursor-disconnect').addEventListener('click', () => window.api.cursorEnable(false))
|
|
1843
|
+
|
|
1717
1844
|
// account login (browser flow)
|
|
1718
1845
|
el('acc-connect').addEventListener('click', () => window.api.authStart())
|
|
1719
1846
|
// main opened the browser — the only thing left to do is paste the code back,
|
|
@@ -1721,7 +1848,7 @@ el('acc-connect').addEventListener('click', () => window.api.authStart())
|
|
|
1721
1848
|
window.api.onAuthPending(() => {
|
|
1722
1849
|
// the login can start from the account menu, with the panel closed: the code
|
|
1723
1850
|
// field is where the flow continues, so bring it up
|
|
1724
|
-
openSettings()
|
|
1851
|
+
openSettings('claude')
|
|
1725
1852
|
document.body.classList.add('awaiting')
|
|
1726
1853
|
el('acc-paste').classList.add('show')
|
|
1727
1854
|
el('acc-code').classList.remove('filled')
|
|
@@ -1856,14 +1983,50 @@ function populateSettings() {
|
|
|
1856
1983
|
reflectTalkOn()
|
|
1857
1984
|
clearSaveDirty() // fields now match the saved config
|
|
1858
1985
|
}
|
|
1859
|
-
|
|
1986
|
+
// `provider` opens that service's connection card straight away
|
|
1987
|
+
function openSettings(provider = null) {
|
|
1860
1988
|
hideBubble()
|
|
1861
1989
|
setDisplaySize('expanded', false)
|
|
1862
1990
|
populateSettings()
|
|
1991
|
+
if (typeof provider === 'string') openConnection(provider)
|
|
1863
1992
|
document.body.classList.add('settings-open')
|
|
1864
1993
|
fitSize()
|
|
1865
1994
|
}
|
|
1866
1995
|
|
|
1996
|
+
// Settings → Connections: a tile per service says whether it's on (a check and
|
|
1997
|
+
// its plan); a click opens its card underneath, one at a time
|
|
1998
|
+
let openConn = null
|
|
1999
|
+
function openConnection(provider) {
|
|
2000
|
+
openConn = provider
|
|
2001
|
+
paintConnections()
|
|
2002
|
+
fitSize()
|
|
2003
|
+
}
|
|
2004
|
+
function paintConnections() {
|
|
2005
|
+
const plans = { claude: lastProfile?.plan, codex: codexData?.plan, cursor: cursorData?.plan }
|
|
2006
|
+
for (const p of PROVIDERS) {
|
|
2007
|
+
const tile = el(`conn-${p}`)
|
|
2008
|
+
const on = isOn(p)
|
|
2009
|
+
const plan = on ? plans[p] || null : null
|
|
2010
|
+
const open = openConn === p
|
|
2011
|
+
tile.setAttribute('aria-expanded', String(open))
|
|
2012
|
+
tile.classList.toggle('on', on)
|
|
2013
|
+
tile.querySelector('.conn-check').hidden = !on
|
|
2014
|
+
const state = el(`conn-${p}-state`)
|
|
2015
|
+
state.textContent = on ? plan || 'Connected' : 'Connect'
|
|
2016
|
+
state.classList.toggle('plan-badge', !!plan)
|
|
2017
|
+
tile.setAttribute(
|
|
2018
|
+
'aria-label',
|
|
2019
|
+
`${NAMES[p]} · ${on ? `connected${plan ? `, ${plan}` : ''}` : 'not connected'}`,
|
|
2020
|
+
)
|
|
2021
|
+
el(p === 'claude' ? 'account' : `${p}-settings`).classList.toggle('open', open)
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
for (const tile of document.querySelectorAll('.conn-tile')) {
|
|
2025
|
+
tile.addEventListener('click', () =>
|
|
2026
|
+
openConnection(openConn === tile.dataset.conn ? null : tile.dataset.conn),
|
|
2027
|
+
)
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1867
2030
|
// Leaving settings plays the panel out to the right while home slides back in.
|
|
1868
2031
|
// The panel is lifted out of the flow for those few frames (see .settings-closing
|
|
1869
2032
|
// in the stylesheet), so the card can already be measured at its home height.
|
|
@@ -1872,6 +2035,9 @@ function closeSettings() {
|
|
|
1872
2035
|
if (!document.body.classList.contains('settings-open')) return
|
|
1873
2036
|
abandonLogin()
|
|
1874
2037
|
endCodexSetup()
|
|
2038
|
+
endCursorSetup()
|
|
2039
|
+
openConn = null
|
|
2040
|
+
paintConnections()
|
|
1875
2041
|
document.body.classList.remove('settings-open')
|
|
1876
2042
|
document.body.classList.add('settings-closing')
|
|
1877
2043
|
clearTimeout(closingTimer)
|
|
@@ -1884,8 +2050,9 @@ el('gear').addEventListener('click', () => {
|
|
|
1884
2050
|
if (document.body.classList.contains('settings-open')) closeSettings()
|
|
1885
2051
|
else openSettings()
|
|
1886
2052
|
})
|
|
2053
|
+
|
|
1887
2054
|
// the "connect" placeholder jumps straight to settings
|
|
1888
|
-
el('limits-connect').addEventListener('click', openSettings)
|
|
2055
|
+
el('limits-connect').addEventListener('click', () => openSettings('claude'))
|
|
1889
2056
|
// custom number steppers (▲ / ▼)
|
|
1890
2057
|
for (const b of document.querySelectorAll('.num-btn')) {
|
|
1891
2058
|
b.addEventListener('click', () => {
|