clauddy 1.27.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/pet.js +286 -150
- 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,31 +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
806
|
// the highest session % across connected services, and which ones are at or
|
|
784
|
-
// past `from`; an expired Codex reading is null, so it never counts
|
|
807
|
+
// past `from`; an expired Codex or Cursor reading is null, so it never counts
|
|
785
808
|
function sessionHeat(from) {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
const known = Object.entries(pcts).filter(([, p]) => p != null)
|
|
809
|
+
const known = connected()
|
|
810
|
+
.map((p) => [p, sessionOf(p)?.pct])
|
|
811
|
+
.filter(([, pct]) => pct != null)
|
|
791
812
|
return {
|
|
792
|
-
pct: Math.max(0, ...known.map(([,
|
|
793
|
-
providers: known.filter(([,
|
|
813
|
+
pct: Math.max(0, ...known.map(([, pct]) => pct)),
|
|
814
|
+
providers: known.filter(([, pct]) => pct >= from).map(([p]) => p),
|
|
794
815
|
}
|
|
795
816
|
}
|
|
796
817
|
|
|
797
|
-
// Codex's numbers in the shape the panel draws
|
|
798
|
-
|
|
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) {
|
|
799
821
|
const idleFor = c.lastSeen ? Date.now() - c.lastSeen : null
|
|
800
822
|
return {
|
|
801
823
|
d: {
|
|
802
824
|
active: c.active,
|
|
803
|
-
activity: null,
|
|
825
|
+
activity: p === 'cursor' ? c.activity : null,
|
|
804
826
|
// no data is not the same as asleep
|
|
805
827
|
sleeping: !c.active && idleFor != null && idleFor >= CODEX_SLEEP_MS,
|
|
806
828
|
tokensPerMin: 0,
|
|
@@ -813,82 +835,83 @@ function codexView(c) {
|
|
|
813
835
|
monthTokens: c.monthTokens ?? null,
|
|
814
836
|
},
|
|
815
837
|
sess: c.session,
|
|
816
|
-
week: c.weekly,
|
|
838
|
+
week: p === 'cursor' ? c.api : c.weekly,
|
|
817
839
|
stale: c.limitsAt != null && Date.now() - c.limitsAt > STALE_MS ? c.limitsAt : null,
|
|
818
840
|
}
|
|
819
841
|
}
|
|
820
842
|
|
|
821
|
-
// a Codex meter caption: its reset, or how old the reading is when
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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)
|
|
826
850
|
if (stale) {
|
|
827
|
-
const at = w.resetMs != null ? `resets ${fmtResetClock(w.resetMs)}
|
|
828
|
-
return
|
|
851
|
+
const at = w.resetMs != null ? `resets ${fmtResetClock(w.resetMs)}` : ''
|
|
852
|
+
return join(at, `read ${fmtReset(Date.now() - stale)} ago`)
|
|
829
853
|
}
|
|
830
|
-
|
|
831
|
-
return `${reset}${t}`
|
|
854
|
+
return join(w.resetMs != null ? `resets in ${fmtResetIn(w.resetMs)}` : '', t) || '—'
|
|
832
855
|
}
|
|
833
856
|
|
|
834
857
|
const pctText = (v) => (v == null ? '—' : `${Math.round(v)}%`)
|
|
835
858
|
const warnAt = () => Math.min(...(currentConfig.alertThresholds || [80, 95]))
|
|
836
859
|
|
|
837
|
-
// tabs + the collapsed
|
|
860
|
+
// tabs + the collapsed dock: every service's session %, whichever is on screen
|
|
838
861
|
function paintServices(view) {
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
document.body.classList.toggle('
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
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
|
|
845
870
|
el('mini-reset').textContent = resetMs == null ? '' : `resets ${fmtResetClock(resetMs)}`
|
|
846
|
-
el('service-panel').setAttribute('role',
|
|
847
|
-
if (
|
|
871
|
+
el('service-panel').setAttribute('role', many ? 'tabpanel' : 'region')
|
|
872
|
+
if (many) {
|
|
848
873
|
el('service-panel').setAttribute('aria-labelledby', `tab-${view}`)
|
|
849
874
|
el('service-panel').removeAttribute('aria-label')
|
|
850
875
|
} else {
|
|
851
876
|
el('service-panel').removeAttribute('aria-labelledby')
|
|
852
|
-
el('service-panel').setAttribute('aria-label', `${view
|
|
877
|
+
el('service-panel').setAttribute('aria-label', `${NAMES[view]} usage`)
|
|
853
878
|
return
|
|
854
879
|
}
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
week: cl?.week?.pct ?? null,
|
|
861
|
-
active: !!lastData?.active,
|
|
862
|
-
},
|
|
863
|
-
codex: {
|
|
864
|
-
pct: cx?.session?.pct ?? null,
|
|
865
|
-
week: cx?.weekly?.pct ?? null,
|
|
866
|
-
active: !!cx?.active,
|
|
867
|
-
stale: codexStale(cx),
|
|
868
|
-
},
|
|
880
|
+
// what each service calls its two meters
|
|
881
|
+
const labels = {
|
|
882
|
+
claude: ['session', 'weekly'],
|
|
883
|
+
codex: ['session', 'weekly'],
|
|
884
|
+
cursor: ['month', 'API'],
|
|
869
885
|
}
|
|
870
|
-
for (const
|
|
871
|
-
const on = p === view
|
|
872
|
-
const urgent = Math.max(r.pct ?? 0, r.week ?? 0) >= warnAt()
|
|
886
|
+
for (const p of PROVIDERS) {
|
|
873
887
|
const tab = el(`tab-${p}`)
|
|
874
|
-
|
|
875
|
-
tab.
|
|
876
|
-
|
|
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)
|
|
877
901
|
tab.classList.toggle('urgent', urgent)
|
|
878
902
|
tab.querySelector('.tab-alert').hidden = !urgent
|
|
879
|
-
el(`tab-${p}-value`).textContent = pctText(
|
|
880
|
-
const
|
|
881
|
-
tab.title = `${
|
|
882
|
-
|
|
883
|
-
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))
|
|
884
907
|
line.title = tab.title
|
|
885
908
|
line.setAttribute('aria-label', `Show ${tab.title}`)
|
|
886
909
|
line.classList.toggle('urgent', urgent)
|
|
887
910
|
tab.setAttribute('aria-label', `Show ${tab.title}`)
|
|
888
|
-
el(`mini-${p}-value`).textContent = pctText(
|
|
911
|
+
el(`mini-${p}-value`).textContent = pctText(pct)
|
|
889
912
|
const bar = el(`mini-${p}-bar`)
|
|
890
|
-
bar.style.width = `${Math.min(
|
|
891
|
-
bar.classList.toggle('hot', (
|
|
913
|
+
bar.style.width = `${Math.min(pct ?? 0, 100)}%`
|
|
914
|
+
bar.classList.toggle('hot', (pct ?? 0) >= 80)
|
|
892
915
|
}
|
|
893
916
|
}
|
|
894
917
|
|
|
@@ -899,7 +922,7 @@ for (const b of document.querySelectorAll('#harness-tabs button, .mini-source'))
|
|
|
899
922
|
})
|
|
900
923
|
}
|
|
901
924
|
el('harness-tabs').addEventListener('keydown', (e) => {
|
|
902
|
-
const providers =
|
|
925
|
+
const providers = connected()
|
|
903
926
|
const current = providers.indexOf(e.target.dataset.provider)
|
|
904
927
|
if (current < 0) return
|
|
905
928
|
let next
|
|
@@ -919,7 +942,7 @@ let sizeLoaded = false
|
|
|
919
942
|
let peekTimer = null
|
|
920
943
|
let nativePetInside = null
|
|
921
944
|
let noticeTimer = null
|
|
922
|
-
let reminders = { claude: null, codex: null }
|
|
945
|
+
let reminders = { claude: null, codex: null, cursor: null }
|
|
923
946
|
const activityTracker = Companion.createActivityTracker()
|
|
924
947
|
|
|
925
948
|
function setPetPeek(on) {
|
|
@@ -994,37 +1017,44 @@ for (const button of document.querySelectorAll('#pet-glance button[data-provider
|
|
|
994
1017
|
button.addEventListener('click', () => pickProvider(button.dataset.provider))
|
|
995
1018
|
}
|
|
996
1019
|
function paintCompanion(view) {
|
|
997
|
-
for (const provider of
|
|
998
|
-
const
|
|
999
|
-
const session = provider === 'claude' ? realUsage?.session : codexData?.session
|
|
1020
|
+
for (const provider of PROVIDERS) {
|
|
1021
|
+
const session = sessionOf(provider)
|
|
1000
1022
|
const button = el(`glance-${provider}`)
|
|
1001
|
-
button.hidden = !
|
|
1023
|
+
button.hidden = !isOn(provider)
|
|
1002
1024
|
button.setAttribute('aria-pressed', String(provider === view))
|
|
1003
1025
|
button.classList.toggle('urgent', session?.pct >= warnAt())
|
|
1004
1026
|
el(`glance-${provider}-value`).textContent = pctText(session?.pct)
|
|
1005
|
-
const stale = provider
|
|
1027
|
+
const stale = provider !== 'claude' && readingStale(dataOf(provider))
|
|
1006
1028
|
button.title = stale
|
|
1007
1029
|
? 'Last recorded usage · waiting for a fresh reading'
|
|
1008
1030
|
: `Show ${provider} usage`
|
|
1009
1031
|
button.classList.toggle('stale-reading', !!stale)
|
|
1010
1032
|
}
|
|
1011
|
-
el('glance-empty').hidden =
|
|
1012
|
-
const session = view
|
|
1033
|
+
el('glance-empty').hidden = connected().length > 0
|
|
1034
|
+
const session = sessionOf(view)
|
|
1013
1035
|
const reminder = reminders[view]
|
|
1036
|
+
const ext = dataOf(view)
|
|
1014
1037
|
const stale =
|
|
1015
|
-
view === '
|
|
1016
|
-
? !
|
|
1017
|
-
: !
|
|
1038
|
+
view === 'claude'
|
|
1039
|
+
? !realUsageReadAt || Date.now() - realUsageReadAt > STALE_MS
|
|
1040
|
+
: !ext?.limitsAt || readingStale(ext)
|
|
1018
1041
|
const available = session?.pct != null && session.resetMs > 0 && !stale
|
|
1019
|
-
const name = view
|
|
1042
|
+
const name = NAMES[view]
|
|
1020
1043
|
for (const id of ['reminder-toggle', 'mini-reminder']) {
|
|
1021
1044
|
const button = el(id)
|
|
1022
1045
|
button.hidden = !reminder && (!available || session.pct < warnAt())
|
|
1023
1046
|
button.setAttribute('aria-pressed', String(!!reminder))
|
|
1024
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
|
+
: ''
|
|
1025
1055
|
button.title = reminder
|
|
1026
|
-
? `Cancel ${name} reminder for ${
|
|
1027
|
-
: `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`
|
|
1028
1058
|
}
|
|
1029
1059
|
}
|
|
1030
1060
|
function companionNotice(text, provider = null) {
|
|
@@ -1047,14 +1077,14 @@ for (const id of ['reminder-toggle', 'mini-reminder']) {
|
|
|
1047
1077
|
})
|
|
1048
1078
|
}
|
|
1049
1079
|
window.api.onReminders((state) => {
|
|
1050
|
-
reminders = state || { claude: null, codex: null }
|
|
1080
|
+
reminders = state || { claude: null, codex: null, cursor: null }
|
|
1051
1081
|
if (state?.error) companionNotice(state.error)
|
|
1052
1082
|
paintCompanion(viewProvider())
|
|
1053
1083
|
fitSize()
|
|
1054
1084
|
})
|
|
1055
1085
|
window.api.onReminderDue((event) => {
|
|
1056
1086
|
if (!event.isCurrent) return
|
|
1057
|
-
const name = event.provider
|
|
1087
|
+
const name = NAMES[event.provider] || 'Claude'
|
|
1058
1088
|
companionNotice(
|
|
1059
1089
|
event.confirmed ? `${name} · budget is back!` : `${name} · reset time reached`,
|
|
1060
1090
|
event.provider,
|
|
@@ -1066,13 +1096,14 @@ function reactToProvider(provider, data) {
|
|
|
1066
1096
|
const cue = activityTracker.observe(provider, data)
|
|
1067
1097
|
if (!cue || document.body.classList.contains('settings-open')) return
|
|
1068
1098
|
if (!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
1069
|
-
|
|
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]
|
|
1070
1101
|
el('eyes').animate(
|
|
1071
1102
|
[
|
|
1072
|
-
{ transform: '
|
|
1073
|
-
{ transform: `
|
|
1074
|
-
{ transform: `
|
|
1075
|
-
{ 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)' },
|
|
1076
1107
|
],
|
|
1077
1108
|
{ duration: 1100, easing: 'ease-in-out' },
|
|
1078
1109
|
)
|
|
@@ -1080,17 +1111,18 @@ function reactToProvider(provider, data) {
|
|
|
1080
1111
|
}
|
|
1081
1112
|
|
|
1082
1113
|
function paintActivity() {
|
|
1083
|
-
const activity = Companion.currentActivity(
|
|
1084
|
-
claudeOn() ? claudeActivityData : null,
|
|
1085
|
-
codexOn() ? codexData : null,
|
|
1086
|
-
|
|
1114
|
+
const activity = Companion.currentActivity({
|
|
1115
|
+
claude: claudeOn() ? claudeActivityData : null,
|
|
1116
|
+
codex: codexOn() ? codexData : null,
|
|
1117
|
+
cursor: cursorOn() ? cursorData : null,
|
|
1118
|
+
})
|
|
1087
1119
|
paintLogos(activity.providers, activity.activity)
|
|
1088
1120
|
return activity
|
|
1089
1121
|
}
|
|
1090
1122
|
|
|
1091
1123
|
// the logos under the pet name who it's about: the workers, or the service on fire
|
|
1092
1124
|
function paintLogos(providers, caption) {
|
|
1093
|
-
const names = providers.map((p) =>
|
|
1125
|
+
const names = providers.map((p) => NAMES[p]).join(' + ')
|
|
1094
1126
|
const label = names ? `${names} · ${caption}` : ''
|
|
1095
1127
|
for (const id of ['pet-activity', 'mini-workers']) {
|
|
1096
1128
|
const badge = el(id)
|
|
@@ -1123,22 +1155,27 @@ function render(d) {
|
|
|
1123
1155
|
|
|
1124
1156
|
function paint() {
|
|
1125
1157
|
setPlan(el('codex-plan'), codexData?.plan)
|
|
1158
|
+
setPlan(el('cursor-plan'), cursorData?.plan)
|
|
1159
|
+
paintConnections()
|
|
1126
1160
|
const d0 = lastData
|
|
1127
1161
|
if (!d0) return
|
|
1128
1162
|
const view = viewProvider()
|
|
1129
1163
|
paintServices(view)
|
|
1130
1164
|
paintCompanion(view)
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
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
|
|
1136
1173
|
document.body.classList.toggle('live', liveOn)
|
|
1137
|
-
const sessPct =
|
|
1138
|
-
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
|
|
1139
1176
|
const sessActive = liveOn && sessReset != null
|
|
1140
|
-
const wkPct =
|
|
1141
|
-
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
|
|
1142
1179
|
const sp = sessPct ?? 0
|
|
1143
1180
|
const wp = wkPct ?? 0
|
|
1144
1181
|
|
|
@@ -1216,16 +1253,23 @@ function paint() {
|
|
|
1216
1253
|
: 'idle'
|
|
1217
1254
|
el('status-text').textContent = word
|
|
1218
1255
|
el('mini-text').textContent = word
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
|
1221
1265
|
? `${fmtTokens(d.tokensPerMin)} tok/min`
|
|
1222
1266
|
: `${fmtTokens(d.today.tokens)} tokens today`
|
|
1223
1267
|
|
|
1224
1268
|
const was = prevPct[view]
|
|
1225
1269
|
if (liveOn && was != null && sessActive && sessPct != null && was - sessPct > 25) {
|
|
1226
1270
|
celebrate()
|
|
1227
|
-
// Codex's own
|
|
1228
|
-
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 })
|
|
1229
1273
|
}
|
|
1230
1274
|
prevPct[view] = sessPct
|
|
1231
1275
|
el('session-pct').textContent = pctText(liveOn ? sessPct : 0)
|
|
@@ -1244,15 +1288,24 @@ function paint() {
|
|
|
1244
1288
|
sf.style.width = `${sp}%`
|
|
1245
1289
|
sf.classList.toggle('high', sp >= 80)
|
|
1246
1290
|
setLevel(sf, sp)
|
|
1247
|
-
el('session-
|
|
1248
|
-
|
|
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)
|
|
1249
1302
|
: sessActive
|
|
1250
1303
|
? `resets in ${fmtResetIn(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
|
|
1251
1304
|
: 'no active session'
|
|
1252
1305
|
|
|
1253
|
-
// 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
|
|
1254
1307
|
const proj =
|
|
1255
|
-
!
|
|
1308
|
+
!isExt && sessActive && sp < 100 ? burn.project(sp, sessReset, d.session.tokens) : null
|
|
1256
1309
|
const pe = el('session-proj')
|
|
1257
1310
|
pe.hidden = !proj
|
|
1258
1311
|
pe.classList.toggle('tight', proj?.kind === 'eta')
|
|
@@ -1267,13 +1320,15 @@ function paint() {
|
|
|
1267
1320
|
wf.style.width = `${wp}%`
|
|
1268
1321
|
wf.classList.toggle('high', wp >= 80)
|
|
1269
1322
|
setLevel(wf, wp)
|
|
1270
|
-
el('week-sub').textContent =
|
|
1271
|
-
?
|
|
1323
|
+
el('week-sub').textContent = isExt
|
|
1324
|
+
? isCursor
|
|
1325
|
+
? extSub(cv.week, null, cv.stale, '—')
|
|
1326
|
+
: extSub(cv.week, d.week.tokens, cv.stale)
|
|
1272
1327
|
: wkReset != null
|
|
1273
1328
|
? `resets in ${fmtResetIn(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
|
|
1274
1329
|
: `${fmtTokens(d.week.tokens)} tokens · last 7 days`
|
|
1275
1330
|
|
|
1276
|
-
renderScoped(!
|
|
1331
|
+
renderScoped(!isExt && liveOn ? realUsage.scoped || [] : [], d.byModel || [])
|
|
1277
1332
|
renderModels(d.byModel || [])
|
|
1278
1333
|
renderProjects(d.byProject || [])
|
|
1279
1334
|
renderHeat(d.days30 || [])
|
|
@@ -1293,7 +1348,7 @@ function paint() {
|
|
|
1293
1348
|
stopEating()
|
|
1294
1349
|
}
|
|
1295
1350
|
|
|
1296
|
-
listen(before, st, {
|
|
1351
|
+
listen(before, st, { isExt, liveOn, sp, sessReset, proj })
|
|
1297
1352
|
fitSize()
|
|
1298
1353
|
}
|
|
1299
1354
|
|
|
@@ -1304,10 +1359,12 @@ function fitSize() {
|
|
|
1304
1359
|
requestAnimationFrame(() => {
|
|
1305
1360
|
const collapsed = document.body.classList.contains('collapsed')
|
|
1306
1361
|
const zoom = Number.parseFloat(document.body.style.zoom) || 1
|
|
1307
|
-
// The
|
|
1362
|
+
// The multi-service dock trades height for a little horizontal room, and
|
|
1363
|
+
// three readings widen the pet-only glance.
|
|
1308
1364
|
const dual = document.body.classList.contains('dual')
|
|
1365
|
+
const trio = document.body.classList.contains('trio')
|
|
1309
1366
|
const petOnly = document.body.classList.contains('pet-only')
|
|
1310
|
-
const w = (petOnly ? 168 : collapsed ? (dual ? 240 : 192) : 304) * zoom
|
|
1367
|
+
const w = (petOnly ? (trio ? 212 : 168) : collapsed ? (dual ? 240 : 192) : 304) * zoom
|
|
1311
1368
|
// offsetHeight never reflects a CSS zoom applied to an ancestor (confirmed
|
|
1312
1369
|
// empirically against this Electron build) — so measure the unzoomed content
|
|
1313
1370
|
// height, then scale the whole thing (content + margin) ourselves
|
|
@@ -1349,6 +1406,9 @@ window.api.onConfig((cfg) => {
|
|
|
1349
1406
|
document.body.classList.toggle('codex-on', !!currentConfig.codex)
|
|
1350
1407
|
if (!currentConfig.codex) codexData = null
|
|
1351
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')
|
|
1352
1412
|
if (!sizeLoaded) {
|
|
1353
1413
|
sizeLoaded = true
|
|
1354
1414
|
let size = 'expanded'
|
|
@@ -1370,6 +1430,11 @@ window.api.onCodex((c) => {
|
|
|
1370
1430
|
reactToProvider('codex', codexOn() ? c : null)
|
|
1371
1431
|
paint()
|
|
1372
1432
|
})
|
|
1433
|
+
window.api.onCursor((c) => {
|
|
1434
|
+
cursorData = c || null
|
|
1435
|
+
reactToProvider('cursor', cursorOn() ? c : null)
|
|
1436
|
+
paint()
|
|
1437
|
+
})
|
|
1373
1438
|
window.api.onRealUsage((u) => {
|
|
1374
1439
|
realUsage = u || null
|
|
1375
1440
|
realUsageReadAt = u ? Date.now() : 0
|
|
@@ -1388,7 +1453,7 @@ window.api.onAuthState((s) => {
|
|
|
1388
1453
|
endLogin()
|
|
1389
1454
|
showProfile(null)
|
|
1390
1455
|
}
|
|
1391
|
-
paint() // losing Claude can leave Codex as the only service
|
|
1456
|
+
paint() // losing Claude can leave Codex or Cursor as the only service
|
|
1392
1457
|
if (document.body.classList.contains('settings-open')) fitSize()
|
|
1393
1458
|
})
|
|
1394
1459
|
|
|
@@ -1397,6 +1462,7 @@ let lastProfile = null
|
|
|
1397
1462
|
function showProfile(p) {
|
|
1398
1463
|
lastProfile = p
|
|
1399
1464
|
paintChip()
|
|
1465
|
+
paintConnections()
|
|
1400
1466
|
}
|
|
1401
1467
|
|
|
1402
1468
|
// The chip is also the account switcher, so it must not vanish just because the
|
|
@@ -1415,14 +1481,16 @@ function paintChip() {
|
|
|
1415
1481
|
el('acc-ok').title = email || 'Connected'
|
|
1416
1482
|
const chip = el('account-chip')
|
|
1417
1483
|
const mini = el('mini-acct')
|
|
1418
|
-
// the Codex
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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`
|
|
1423
1491
|
setPlan(el('ac-plan'), plan)
|
|
1424
1492
|
chip.hidden = false
|
|
1425
|
-
el('mini-acct-name').textContent =
|
|
1493
|
+
el('mini-acct-name').textContent = name
|
|
1426
1494
|
setPlan(el('mini-acct-plan'), plan)
|
|
1427
1495
|
mini.hidden = false
|
|
1428
1496
|
return
|
|
@@ -1564,8 +1632,8 @@ function toggleAccountMenu() {
|
|
|
1564
1632
|
|
|
1565
1633
|
el('account-chip').addEventListener('click', (e) => {
|
|
1566
1634
|
e.stopPropagation()
|
|
1567
|
-
// Codex
|
|
1568
|
-
if (viewProvider()
|
|
1635
|
+
// Codex and Cursor have no account switcher: their chip leads to their card
|
|
1636
|
+
if (viewProvider() !== 'claude') return openSettings(viewProvider())
|
|
1569
1637
|
toggleAccountMenu()
|
|
1570
1638
|
})
|
|
1571
1639
|
// clicking anywhere else — the pet, the gear, another app — puts it away
|
|
@@ -1745,6 +1813,34 @@ el('codex-setup-cancel').addEventListener('click', () => {
|
|
|
1745
1813
|
})
|
|
1746
1814
|
el('codex-disconnect').addEventListener('click', () => window.api.codexEnable(false))
|
|
1747
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
|
+
|
|
1748
1844
|
// account login (browser flow)
|
|
1749
1845
|
el('acc-connect').addEventListener('click', () => window.api.authStart())
|
|
1750
1846
|
// main opened the browser — the only thing left to do is paste the code back,
|
|
@@ -1752,7 +1848,7 @@ el('acc-connect').addEventListener('click', () => window.api.authStart())
|
|
|
1752
1848
|
window.api.onAuthPending(() => {
|
|
1753
1849
|
// the login can start from the account menu, with the panel closed: the code
|
|
1754
1850
|
// field is where the flow continues, so bring it up
|
|
1755
|
-
openSettings()
|
|
1851
|
+
openSettings('claude')
|
|
1756
1852
|
document.body.classList.add('awaiting')
|
|
1757
1853
|
el('acc-paste').classList.add('show')
|
|
1758
1854
|
el('acc-code').classList.remove('filled')
|
|
@@ -1887,14 +1983,50 @@ function populateSettings() {
|
|
|
1887
1983
|
reflectTalkOn()
|
|
1888
1984
|
clearSaveDirty() // fields now match the saved config
|
|
1889
1985
|
}
|
|
1890
|
-
|
|
1986
|
+
// `provider` opens that service's connection card straight away
|
|
1987
|
+
function openSettings(provider = null) {
|
|
1891
1988
|
hideBubble()
|
|
1892
1989
|
setDisplaySize('expanded', false)
|
|
1893
1990
|
populateSettings()
|
|
1991
|
+
if (typeof provider === 'string') openConnection(provider)
|
|
1894
1992
|
document.body.classList.add('settings-open')
|
|
1895
1993
|
fitSize()
|
|
1896
1994
|
}
|
|
1897
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
|
+
|
|
1898
2030
|
// Leaving settings plays the panel out to the right while home slides back in.
|
|
1899
2031
|
// The panel is lifted out of the flow for those few frames (see .settings-closing
|
|
1900
2032
|
// in the stylesheet), so the card can already be measured at its home height.
|
|
@@ -1903,6 +2035,9 @@ function closeSettings() {
|
|
|
1903
2035
|
if (!document.body.classList.contains('settings-open')) return
|
|
1904
2036
|
abandonLogin()
|
|
1905
2037
|
endCodexSetup()
|
|
2038
|
+
endCursorSetup()
|
|
2039
|
+
openConn = null
|
|
2040
|
+
paintConnections()
|
|
1906
2041
|
document.body.classList.remove('settings-open')
|
|
1907
2042
|
document.body.classList.add('settings-closing')
|
|
1908
2043
|
clearTimeout(closingTimer)
|
|
@@ -1915,8 +2050,9 @@ el('gear').addEventListener('click', () => {
|
|
|
1915
2050
|
if (document.body.classList.contains('settings-open')) closeSettings()
|
|
1916
2051
|
else openSettings()
|
|
1917
2052
|
})
|
|
2053
|
+
|
|
1918
2054
|
// the "connect" placeholder jumps straight to settings
|
|
1919
|
-
el('limits-connect').addEventListener('click', openSettings)
|
|
2055
|
+
el('limits-connect').addEventListener('click', () => openSettings('claude'))
|
|
1920
2056
|
// custom number steppers (▲ / ▼)
|
|
1921
2057
|
for (const b of document.querySelectorAll('.num-btn')) {
|
|
1922
2058
|
b.addEventListener('click', () => {
|