@m13v/s4l 1.7.5-rc.2 → 1.7.5-rc.21
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/mcp/dist/index.js +571 -79
- package/mcp/dist/panel.html +2 -3
- package/mcp/dist/redditAuth.js +115 -0
- package/mcp/dist/runtime.js +83 -2
- package/mcp/dist/setup.js +142 -5
- package/mcp/dist/telemetry.js +13 -3
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_card.py +32 -6
- package/mcp/menubar/s4l_menubar.py +42 -111
- package/mcp/menubar/s4l_state.py +54 -187
- package/mcp/package.json +1 -1
- package/mcp/shared/onboarding-ledger.cjs +63 -4
- package/package.json +1 -1
- package/scripts/autopilot_stall_watch.py +3 -6
- package/scripts/browser_mutex.py +316 -0
- package/scripts/claude_job.py +21 -0
- package/scripts/config.py +118 -0
- package/scripts/counterparty_history.py +44 -6
- package/scripts/engage_github.py +7 -3
- package/scripts/engage_reddit.py +9 -3
- package/scripts/engagement_signal_feedback.py +256 -0
- package/scripts/learned_preferences.py +37 -14
- package/scripts/memory_snapshot.py +45 -0
- package/scripts/merge_review_queue.py +67 -3
- package/scripts/post_reddit.py +289 -24
- package/scripts/reddit_browser.py +62 -63
- package/scripts/reddit_browser_fetch.py +32 -0
- package/scripts/release-mcpb.sh +17 -0
- package/scripts/reset-test-machine.sh +13 -7
- package/scripts/scan_twitter_thread_followups.py +7 -1
- package/scripts/schedule_state.py +1 -1
- package/scripts/setup_reddit_auth.py +925 -0
- package/scripts/snapshot.py +96 -4
- package/scripts/stats.py +37 -13
- package/scripts/test_no_silent_fallbacks.py +281 -0
- package/scripts/trigger_promo_lane_test.sh +91 -0
- package/scripts/twitter_browser.py +183 -342
- package/scripts/twitter_scan.py +75 -10
- package/scripts/watchdog_hung_runs.py +15 -9
- package/skill/lib/harness-common.sh +344 -0
- package/skill/lib/linkedin-backend.sh +61 -161
- package/skill/lib/reddit-backend.sh +39 -125
- package/skill/lib/twitter-backend.sh +51 -273
- package/skill/lock.sh +57 -0
- package/skill/run-reddit-search-launchd.sh +4 -1
- package/skill/run-reddit-search.sh +51 -2
- package/skill/run-twitter-cycle.sh +44 -21
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.5-rc.
|
|
5
|
+
"version": "1.7.5-rc.21",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
7
|
"long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
|
|
8
8
|
"author": {
|
package/mcp/menubar/s4l_card.py
CHANGED
|
@@ -1180,20 +1180,45 @@ class _ReviewController(NSObject):
|
|
|
1180
1180
|
age_label.setAlignment_(NSTextAlignmentRight)
|
|
1181
1181
|
content.addSubview_(age_label)
|
|
1182
1182
|
right_x -= age_w + 4
|
|
1183
|
-
|
|
1183
|
+
# Platform mark (brand identification, inline with the author row):
|
|
1184
|
+
# Reddit's orange "r/" vs X's glyph, so a mixed-platform review queue
|
|
1185
|
+
# reads at a glance which network each card posts to.
|
|
1186
|
+
_plat = (d.get("platform") or "twitter").lower()
|
|
1187
|
+
_mark = "r/" if _plat == "reddit" else "\U0001d54f"
|
|
1188
|
+
_mark_w = 18
|
|
1189
|
+
_mark_label = _label(
|
|
1190
|
+
NSMakeRect(M + 78, H - 70, _mark_w, 18), _mark, size=12, bold=True
|
|
1191
|
+
)
|
|
1192
|
+
if _plat == "reddit":
|
|
1193
|
+
try:
|
|
1194
|
+
# Reddit orangered (#FF4500); best effort, falls back to the
|
|
1195
|
+
# default label color on any AppKit hiccup.
|
|
1196
|
+
_mark_label.setTextColor_(
|
|
1197
|
+
NSColor.colorWithSRGBRed_green_blue_alpha_(1.0, 0.27, 0.0, 1.0)
|
|
1198
|
+
)
|
|
1199
|
+
except Exception:
|
|
1200
|
+
pass
|
|
1201
|
+
content.addSubview_(_mark_label)
|
|
1202
|
+
_author_x = M + 78 + _mark_w
|
|
1203
|
+
handle_w = right_x - _author_x - 4
|
|
1184
1204
|
if handle:
|
|
1185
1205
|
# Size the link to its text so the follower count can sit right
|
|
1186
1206
|
# after the handle instead of at a fixed column.
|
|
1187
|
-
text = f"@{handle}"
|
|
1207
|
+
text = f"u/{handle}" if _plat == "reddit" else f"@{handle}"
|
|
1188
1208
|
measured = NSAttributedString.alloc().initWithString_attributes_(
|
|
1189
1209
|
text, {NSFontAttributeName: _font(12, True)}
|
|
1190
1210
|
).size().width
|
|
1191
1211
|
link_w = min(int(measured) + 8, handle_w)
|
|
1212
|
+
_prof_url = (
|
|
1213
|
+
f"https://www.reddit.com/user/{handle}"
|
|
1214
|
+
if _plat == "reddit"
|
|
1215
|
+
else f"https://x.com/{handle}"
|
|
1216
|
+
)
|
|
1192
1217
|
self._add_link(
|
|
1193
1218
|
content,
|
|
1194
|
-
NSMakeRect(
|
|
1219
|
+
NSMakeRect(_author_x, H - 70, link_w, 18),
|
|
1195
1220
|
text,
|
|
1196
|
-
|
|
1221
|
+
_prof_url,
|
|
1197
1222
|
bold=True,
|
|
1198
1223
|
kind="profile_click",
|
|
1199
1224
|
)
|
|
@@ -1202,7 +1227,7 @@ class _ReviewController(NSObject):
|
|
|
1202
1227
|
if followers and fol_w > 20:
|
|
1203
1228
|
content.addSubview_(
|
|
1204
1229
|
_label(
|
|
1205
|
-
NSMakeRect(
|
|
1230
|
+
NSMakeRect(_author_x + link_w, H - 70, fol_w, 18),
|
|
1206
1231
|
f"· {followers}",
|
|
1207
1232
|
size=11,
|
|
1208
1233
|
muted=True,
|
|
@@ -1210,7 +1235,7 @@ class _ReviewController(NSObject):
|
|
|
1210
1235
|
)
|
|
1211
1236
|
else:
|
|
1212
1237
|
content.addSubview_(
|
|
1213
|
-
_label(NSMakeRect(
|
|
1238
|
+
_label(NSMakeRect(_author_x, H - 70, handle_w, 18), "thread", size=12, bold=True, muted=True)
|
|
1214
1239
|
)
|
|
1215
1240
|
# Non-English drafts: the prep step stamps display-only English
|
|
1216
1241
|
# translations (thread_text_en / reply_text_en) on the candidate. The
|
|
@@ -1859,6 +1884,7 @@ class _ReviewController(NSObject):
|
|
|
1859
1884
|
# decision can be shipped to /api/v1/review-events without
|
|
1860
1885
|
# re-reading the plan.
|
|
1861
1886
|
"candidate_id": d.get("candidate_id"),
|
|
1887
|
+
"platform": d.get("platform"),
|
|
1862
1888
|
"project": d.get("project"),
|
|
1863
1889
|
"thread_url": d.get("thread_url"),
|
|
1864
1890
|
"thread_author": d.get("thread_author"),
|
|
@@ -334,7 +334,6 @@ SETUP_PROMPT = (
|
|
|
334
334
|
"Keep going without asking me to approve each safe setup step. Ask only if I "
|
|
335
335
|
"must interactively sign in or no product can be identified."
|
|
336
336
|
)
|
|
337
|
-
UPDATE_PROMPT = "Update the S4L plugin to the latest version."
|
|
338
337
|
# Re-arm goes through the HOST create_scheduled_task path (the same one onboarding
|
|
339
338
|
# uses) — it registers the routines under whatever account is logged in and shows
|
|
340
339
|
# up in Routines. The host tool only runs inside an agent chat, so the menu bar
|
|
@@ -494,42 +493,6 @@ def _glyph(status):
|
|
|
494
493
|
return GLYPH.get(status, "·")
|
|
495
494
|
|
|
496
495
|
|
|
497
|
-
def _osa_quote(s):
|
|
498
|
-
"""Escape a Python string for an AppleScript double-quoted literal."""
|
|
499
|
-
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
def _claude_send_script(prompt):
|
|
503
|
-
"""AppleScript that focuses Claude, pastes `prompt` into the focused composer,
|
|
504
|
-
and presses Return. Uses the clipboard (saved + restored) rather than slow
|
|
505
|
-
per-character keystrokes, and waits longer on a cold launch so the window is
|
|
506
|
-
ready before pasting."""
|
|
507
|
-
p = _osa_quote(prompt)
|
|
508
|
-
return "\n".join(
|
|
509
|
-
[
|
|
510
|
-
'set prevClip to ""',
|
|
511
|
-
"try",
|
|
512
|
-
" set prevClip to (the clipboard as text)",
|
|
513
|
-
"end try",
|
|
514
|
-
f'set the clipboard to "{p}"',
|
|
515
|
-
'tell application "System Events" to set wasRunning to (exists process "Claude")',
|
|
516
|
-
'tell application "Claude" to activate',
|
|
517
|
-
"if wasRunning then",
|
|
518
|
-
" delay 0.5",
|
|
519
|
-
"else",
|
|
520
|
-
" delay 2.5",
|
|
521
|
-
"end if",
|
|
522
|
-
'tell application "System Events"',
|
|
523
|
-
' keystroke "v" using {command down}',
|
|
524
|
-
" delay 0.15",
|
|
525
|
-
" key code 36",
|
|
526
|
-
"end tell",
|
|
527
|
-
"delay 0.3",
|
|
528
|
-
'if prevClip is not "" then set the clipboard to prevClip',
|
|
529
|
-
]
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
|
|
533
496
|
class S4LMenuBar(rumps.App):
|
|
534
497
|
def __init__(self):
|
|
535
498
|
super().__init__("S4L", quit_button=None)
|
|
@@ -710,23 +673,6 @@ class S4LMenuBar(rumps.App):
|
|
|
710
673
|
except Exception:
|
|
711
674
|
return False
|
|
712
675
|
|
|
713
|
-
def _manual_paste_fallback(self, prompt, reason):
|
|
714
|
-
"""Automation couldn't paste (no Accessibility, or osascript failed). Don't
|
|
715
|
-
dead-end: drop the prompt on the clipboard and open Claude so the user can
|
|
716
|
-
paste it themselves (Cmd+V, Enter). This is what makes re-arm/setup usable
|
|
717
|
-
even when the TCC grant is stale (granted but the running process still
|
|
718
|
-
reads untrusted until restart)."""
|
|
719
|
-
copied = self._copy_to_clipboard(prompt)
|
|
720
|
-
self._open_claude()
|
|
721
|
-
if copied:
|
|
722
|
-
self._notify(
|
|
723
|
-
"S4L · prompt copied to clipboard",
|
|
724
|
-
f"{reason} Paste it into Claude (⌘V) and press Enter to continue.",
|
|
725
|
-
)
|
|
726
|
-
else:
|
|
727
|
-
self._notify("S4L", f"{reason} Open Claude and type your request there.")
|
|
728
|
-
return False
|
|
729
|
-
|
|
730
676
|
def _clipboard_prompt(self, prompt, title, action_desc):
|
|
731
677
|
"""The menu bar's UNIVERSAL way to hand an agent-driven action to Claude
|
|
732
678
|
without depending on the MCP/loopback being up (the menu bar can't inject
|
|
@@ -751,20 +697,13 @@ class S4LMenuBar(rumps.App):
|
|
|
751
697
|
self._open_claude()
|
|
752
698
|
return True
|
|
753
699
|
|
|
754
|
-
def _send_to_claude(self, prompt):
|
|
755
|
-
"""Back-compat shim: every agent-driven menu action now uses the reliable
|
|
756
|
-
clipboard-prompt model (no flaky auto-type). Delegates to _clipboard_prompt."""
|
|
757
|
-
return self._clipboard_prompt(
|
|
758
|
-
prompt, "Send to Claude", "Claude will take it from there"
|
|
759
|
-
)
|
|
760
|
-
|
|
761
700
|
# Agent-driven action: hand the full setup prompt to Claude via the clipboard.
|
|
762
701
|
def _setup(self, _=None):
|
|
763
702
|
self._clipboard_prompt(
|
|
764
703
|
SETUP_PROMPT,
|
|
765
704
|
"Set up S4L in Claude",
|
|
766
|
-
"Claude will set up your runtime, connect X, configure your
|
|
767
|
-
"schedule the autopilot",
|
|
705
|
+
"Claude will set up your runtime, connect X (or Reddit), configure your "
|
|
706
|
+
"project, and schedule the autopilot",
|
|
768
707
|
)
|
|
769
708
|
|
|
770
709
|
|
|
@@ -1151,13 +1090,14 @@ class S4LMenuBar(rumps.App):
|
|
|
1151
1090
|
)
|
|
1152
1091
|
|
|
1153
1092
|
# Posting volume (2026-07-13): server-side per-install throttle for the
|
|
1154
|
-
# twitter cycle's virality bar.
|
|
1155
|
-
#
|
|
1156
|
-
#
|
|
1157
|
-
#
|
|
1158
|
-
# (
|
|
1093
|
+
# twitter cycle's virality bar. The labels carry ballpark rates; exact
|
|
1094
|
+
# per-install estimates live on the dashboard's Posting Volume section
|
|
1095
|
+
# (Stats tab). Internal values stay high|medium|low (DB CHECK + API enum);
|
|
1096
|
+
# Aggressive/Steady/Chill are display names only (renamed 2026-07-14).
|
|
1097
|
+
# There is NO separate "default" state (removed 2026-07-14 per user): an
|
|
1098
|
+
# install that never chose resolves to Steady server-side, so the menu
|
|
1099
|
+
# shows Steady checked.
|
|
1159
1100
|
POSTING_MODE_PRESETS = (
|
|
1160
|
-
(None, "Default (cycle setting)"),
|
|
1161
1101
|
("high", "Aggressive (100+ posts/day)"),
|
|
1162
1102
|
("medium", "Steady (~30 posts/day)"),
|
|
1163
1103
|
("low", "Chill (~5 posts/day)"),
|
|
@@ -1188,11 +1128,11 @@ class S4LMenuBar(rumps.App):
|
|
|
1188
1128
|
sys.stderr.flush()
|
|
1189
1129
|
|
|
1190
1130
|
def _posting_mode_label(self, mode):
|
|
1191
|
-
"""Menu copy for a posting-volume mode value."""
|
|
1131
|
+
"""Menu copy for a posting-volume mode value (unknown/unset = Steady)."""
|
|
1192
1132
|
for preset, label in self.POSTING_MODE_PRESETS:
|
|
1193
1133
|
if preset == mode:
|
|
1194
1134
|
return label
|
|
1195
|
-
return "
|
|
1135
|
+
return "Steady (~30 posts/day)"
|
|
1196
1136
|
|
|
1197
1137
|
def _on_posting_mode_preset(self, mode, _sender=None):
|
|
1198
1138
|
"""Set the server-side posting-volume mode (network POST; a click may
|
|
@@ -1304,26 +1244,6 @@ class S4LMenuBar(rumps.App):
|
|
|
1304
1244
|
except Exception:
|
|
1305
1245
|
pass
|
|
1306
1246
|
|
|
1307
|
-
# ---- disable scheduled tasks (menu-bar driven) ------------------------
|
|
1308
|
-
def _has_scheduled_tasks(self):
|
|
1309
|
-
"""Read-only: True if any S4L worker/autopilot task is registered in any
|
|
1310
|
-
scheduled-tasks.json. Gates whether the 'Disable scheduled tasks' item is
|
|
1311
|
-
worth showing."""
|
|
1312
|
-
try:
|
|
1313
|
-
wanted = set(WORKER_TASK_IDS) | set(DEPRECATED_TASK_IDS)
|
|
1314
|
-
for f in glob.glob(SCHED_REGISTRY_GLOB):
|
|
1315
|
-
try:
|
|
1316
|
-
with open(f) as fh:
|
|
1317
|
-
d = json.load(fh)
|
|
1318
|
-
except Exception:
|
|
1319
|
-
continue
|
|
1320
|
-
for t in d.get("scheduledTasks", []):
|
|
1321
|
-
if t.get("id") in wanted:
|
|
1322
|
-
return True
|
|
1323
|
-
except Exception:
|
|
1324
|
-
pass
|
|
1325
|
-
return False
|
|
1326
|
-
|
|
1327
1247
|
def _pause_toggle(self, _=None):
|
|
1328
1248
|
"""Pause/Resume: the lighter, fully reversible alternative to Quit. Stops
|
|
1329
1249
|
S4L's own scan/draft/post pipeline (the launchd kicker + its support
|
|
@@ -1475,9 +1395,6 @@ class S4LMenuBar(rumps.App):
|
|
|
1475
1395
|
# exit as final. os._exit because we're on a background thread.
|
|
1476
1396
|
os._exit(0)
|
|
1477
1397
|
|
|
1478
|
-
def _update(self, _=None):
|
|
1479
|
-
self._send_to_claude(UPDATE_PROMPT)
|
|
1480
|
-
|
|
1481
1398
|
# ---- .mcpb self-update (menu-bar driven) ------------------------------
|
|
1482
1399
|
@staticmethod
|
|
1483
1400
|
def _ext_dir():
|
|
@@ -1594,13 +1511,6 @@ class S4LMenuBar(rumps.App):
|
|
|
1594
1511
|
pass
|
|
1595
1512
|
return dirs
|
|
1596
1513
|
|
|
1597
|
-
@classmethod
|
|
1598
|
-
def _claude_user_data_dir(cls):
|
|
1599
|
-
"""First custom --user-data-dir among running instances, or None.
|
|
1600
|
-
Prefer _claude_user_data_dirs when relaunching after a kill — the
|
|
1601
|
-
kill takes every profile down, not just this one."""
|
|
1602
|
-
return next((d for d in cls._claude_user_data_dirs() if d), None)
|
|
1603
|
-
|
|
1604
1514
|
@staticmethod
|
|
1605
1515
|
def _claude_running():
|
|
1606
1516
|
"""True while a Claude Desktop main process is alive. pgrep -x matches
|
|
@@ -2213,6 +2123,14 @@ class S4LMenuBar(rumps.App):
|
|
|
2213
2123
|
):
|
|
2214
2124
|
label = self._posting_label
|
|
2215
2125
|
if label:
|
|
2126
|
+
# Menu bar stays CONCISE (user rule 2026-07-14): strip the trailing
|
|
2127
|
+
# elapsed-duration token the producer encodes in the label ("draft
|
|
2128
|
+
# 2m", "draft ⧖18m") from the TITLE only. The full label (with the
|
|
2129
|
+
# duration the stall detector's _label_elapsed_secs parses) still
|
|
2130
|
+
# lives in activity.json — this is display-side trimming, never a
|
|
2131
|
+
# change to the data.
|
|
2132
|
+
import re as _re
|
|
2133
|
+
label = _re.sub(r"\s*⧖?\s*\d+\s*[sm]\s*$", "", str(label)).strip() or label
|
|
2216
2134
|
# The update arrow must stay visible even while a tool runs, so the
|
|
2217
2135
|
# "update available" signal is never masked by activity. _tick skips the
|
|
2218
2136
|
# title repaint while the spinner owns it, so the arrow is injected here.
|
|
@@ -3056,7 +2974,11 @@ class S4LMenuBar(rumps.App):
|
|
|
3056
2974
|
cid = None
|
|
3057
2975
|
st.review_event_add(
|
|
3058
2976
|
{
|
|
3059
|
-
|
|
2977
|
+
# reddit cards ship platform=reddit (2026-07-14); their
|
|
2978
|
+
# candidate_id is rd-prefixed and int()s to None above, so
|
|
2979
|
+
# the server's twitter_candidates row flip never fires for
|
|
2980
|
+
# them (by design — see merge_review_queue.py).
|
|
2981
|
+
"platform": decision.get("platform") or "twitter",
|
|
3060
2982
|
"project": decision.get("project"),
|
|
3061
2983
|
"candidate_id": cid,
|
|
3062
2984
|
"batch_id": batch,
|
|
@@ -3135,17 +3057,19 @@ class S4LMenuBar(rumps.App):
|
|
|
3135
3057
|
self._ship_review_event(batch, decision)
|
|
3136
3058
|
if not decision.get("approved"):
|
|
3137
3059
|
n = decision.get("n")
|
|
3138
|
-
# Durable local record FIRST, mirroring
|
|
3060
|
+
# Durable local record FIRST, mirroring the approve branch below.
|
|
3139
3061
|
# review_drafts() consults this, so the rejected card is suppressed from
|
|
3140
3062
|
# re-review IMMEDIATELY and even if the loopback is down when the
|
|
3141
3063
|
# background plan-flag write below runs. Without this, a reject was a
|
|
3142
3064
|
# fire-and-forget loopback call with a swallowed exception, so rejects
|
|
3143
3065
|
# silently vanished and the card "came back" — unlike durable approvals.
|
|
3066
|
+
# A failure here is captured (not silently swallowed) so a broken
|
|
3067
|
+
# durable write is visible instead of looking identical to success.
|
|
3144
3068
|
try:
|
|
3145
3069
|
st.store_stamp_decision(batch, decision)
|
|
3146
3070
|
self._session_decisions.append(dict(decision))
|
|
3147
|
-
except Exception:
|
|
3148
|
-
|
|
3071
|
+
except Exception as e:
|
|
3072
|
+
_capture(e, phase="reject_stamp_decision")
|
|
3149
3073
|
|
|
3150
3074
|
def _persist_reject():
|
|
3151
3075
|
try:
|
|
@@ -3160,8 +3084,15 @@ class S4LMenuBar(rumps.App):
|
|
|
3160
3084
|
# restart resumes the drain instead of stranding it and re-presenting the
|
|
3161
3085
|
# card. The in-memory _post_q below is just the fast path; this file is the
|
|
3162
3086
|
# source of truth review_drafts() consults to avoid re-showing it.
|
|
3163
|
-
|
|
3164
|
-
|
|
3087
|
+
# Same try/except shape as the reject branch above (was previously
|
|
3088
|
+
# unguarded here, an asymmetry with no intentional reason): a failure is
|
|
3089
|
+
# captured, not silently swallowed, and the post still gets enqueued below
|
|
3090
|
+
# either way — this write is a resume/dedup aid, not a gate on posting.
|
|
3091
|
+
try:
|
|
3092
|
+
st.store_stamp_decision(batch, decision)
|
|
3093
|
+
self._session_decisions.append(dict(decision))
|
|
3094
|
+
except Exception as e:
|
|
3095
|
+
_capture(e, phase="approve_stamp_decision")
|
|
3165
3096
|
with self._review_lock:
|
|
3166
3097
|
self._posts_outstanding += 1
|
|
3167
3098
|
self._posting_batch_total += 1
|
|
@@ -3571,10 +3502,10 @@ class S4LMenuBar(rumps.App):
|
|
|
3571
3502
|
items.append(cadence_menu)
|
|
3572
3503
|
|
|
3573
3504
|
# Posting volume: server-side per-install throttle (virality bar).
|
|
3574
|
-
# read_posting_mode() is cached and non-blocking;
|
|
3575
|
-
#
|
|
3576
|
-
# the
|
|
3577
|
-
pv_mode = st.read_posting_mode()
|
|
3505
|
+
# read_posting_mode() is cached and non-blocking; unknown/unset shows
|
|
3506
|
+
# as Steady (the server resolves unset the same way), corrected on the
|
|
3507
|
+
# rebuild after the first refresh lands.
|
|
3508
|
+
pv_mode = st.read_posting_mode() or "medium"
|
|
3578
3509
|
pv_menu = rumps.MenuItem(
|
|
3579
3510
|
f"Posting volume: {self._posting_mode_label(pv_mode).split(' (')[0].lower()}"
|
|
3580
3511
|
)
|