rippletide-package 0.1.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/PUBLISHING.md +144 -0
- package/README.md +44 -0
- package/bin/rippletide.js +27 -0
- package/counter/README.md +37 -0
- package/counter/frontend/window.jxa +601 -0
- package/counter/src/cli.js +22 -0
- package/counter/src/counter.js +165 -0
- package/counter/src/proxy.js +243 -0
- package/package.json +64 -0
- package/reviewer/.env.example +5 -0
- package/reviewer/README.md +137 -0
- package/reviewer/bin/rippletide-review.js +16 -0
- package/reviewer/bin/rippletide.js +8 -0
- package/reviewer/codex/rippletide-reviewer/README.md +28 -0
- package/reviewer/codex/rippletide-reviewer/scripts/rippletide-codex +16 -0
- package/reviewer/codex/rippletide-reviewer/scripts/rippletide-codex.mjs +24 -0
- package/reviewer/codex/rippletide-reviewer/scripts/rippletide-reviewer +37 -0
- package/reviewer/src/connect/evidence.js +235 -0
- package/reviewer/src/integrations/cli/rippletide.js +1595 -0
- package/reviewer/src/integrations/codex/prepare.js +277 -0
- package/tide/README.md +128 -0
- package/tide/bin/tide.js +4 -0
- package/tide/examples/no-file-writes/POLICY.md +3 -0
- package/tide/src/cli.js +173 -0
- package/tide/src/codex.js +65 -0
- package/tide/src/compile.js +193 -0
- package/tide/src/credentials.js +41 -0
- package/tide/src/engine.js +115 -0
- package/tide/src/expr.js +260 -0
- package/tide/src/hook.js +71 -0
- package/tide/src/ontology.js +28 -0
- package/tide/src/schema.js +87 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
ObjC.import("Cocoa")
|
|
2
|
+
|
|
3
|
+
const args = $.NSProcessInfo.processInfo.arguments
|
|
4
|
+
const statePath = ObjC.unwrap(args.objectAtIndex(args.count - 1))
|
|
5
|
+
const app = $.NSApplication.sharedApplication
|
|
6
|
+
const width = 200
|
|
7
|
+
const height = 178
|
|
8
|
+
const detailWidth = 380
|
|
9
|
+
const detailHeight = 304
|
|
10
|
+
|
|
11
|
+
function readState() {
|
|
12
|
+
const text = $.NSString.stringWithContentsOfFileEncodingError(statePath, $.NSUTF8StringEncoding, null)
|
|
13
|
+
if (!text) return null
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(ObjC.unwrap(text))
|
|
16
|
+
} catch (_) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
app.setActivationPolicy($.NSApplicationActivationPolicyAccessory)
|
|
22
|
+
|
|
23
|
+
const screen = $.NSScreen.mainScreen.visibleFrame
|
|
24
|
+
const rect = $.NSMakeRect(
|
|
25
|
+
screen.origin.x + screen.size.width - width - 24,
|
|
26
|
+
screen.origin.y + screen.size.height - height - 24,
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
)
|
|
30
|
+
const style = $.NSWindowStyleMaskTitled | $.NSWindowStyleMaskClosable | $.NSWindowStyleMaskNonactivatingPanel
|
|
31
|
+
const win = $.NSPanel.alloc.initWithContentRectStyleMaskBackingDefer(rect, style, $.NSBackingStoreBuffered, false)
|
|
32
|
+
|
|
33
|
+
win.setLevel($.NSFloatingWindowLevel)
|
|
34
|
+
win.setCollectionBehavior(
|
|
35
|
+
$.NSWindowCollectionBehaviorCanJoinAllSpaces | $.NSWindowCollectionBehaviorFullScreenAuxiliary,
|
|
36
|
+
)
|
|
37
|
+
win.setMovableByWindowBackground(true)
|
|
38
|
+
win.setTitleVisibility($.NSWindowTitleHidden)
|
|
39
|
+
win.setTitlebarAppearsTransparent(true)
|
|
40
|
+
win.setAppearance($.NSAppearance.appearanceNamed($.NSAppearanceNameDarkAqua))
|
|
41
|
+
win.setOpaque(false)
|
|
42
|
+
|
|
43
|
+
const G = [0.36, 0.86, 0.52] // Codex green
|
|
44
|
+
const CLAUDE = [0.95, 0.55, 0.28]
|
|
45
|
+
const UNKNOWN = [0.72, 0.72, 0.72]
|
|
46
|
+
const RED = [1.0, 0.22, 0.24]
|
|
47
|
+
const GOLD = [1.0, 0.82, 0.3] // milestone celebration accent
|
|
48
|
+
const POPUP_BG = [0.13, 0.13, 0.13]
|
|
49
|
+
const POPUP_RISK_BG = [0.62, 0.03, 0.04]
|
|
50
|
+
const RING_BG = [0.19, 0.19, 0.19]
|
|
51
|
+
const RING_RISK_BG = [0.42, 0.02, 0.03]
|
|
52
|
+
const DIM = [0.55, 0.55, 0.55]
|
|
53
|
+
const rgb = (c, a) => $.NSColor.colorWithCalibratedRedGreenBlueAlpha(c[0], c[1], c[2], a)
|
|
54
|
+
const lerp = (a, b, t) => a + (b - a) * t
|
|
55
|
+
const mixColor = (a, b, t) => [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]
|
|
56
|
+
const dim = rgb(DIM, 1.0)
|
|
57
|
+
|
|
58
|
+
win.setBackgroundColor(rgb(POPUP_BG, 1.0))
|
|
59
|
+
|
|
60
|
+
function providerColor(provider) {
|
|
61
|
+
if (provider === "claude") return CLAUDE
|
|
62
|
+
if (provider === "codex") return G
|
|
63
|
+
return UNKNOWN
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Use NSBox for colored shapes: it takes NSColor directly. (Setting a CGColor on
|
|
67
|
+
// a CALayer through JXA crashes, so layer color setters are off-limits here.)
|
|
68
|
+
function box(frame) {
|
|
69
|
+
const b = $.NSBox.alloc.initWithFrame(frame)
|
|
70
|
+
b.setBoxType($.NSBoxCustom)
|
|
71
|
+
b.setTitlePosition($.NSNoTitle)
|
|
72
|
+
b.setBorderWidth(0)
|
|
73
|
+
return b
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A circular gauge whose green core grows to fill the ring as calls add up.
|
|
77
|
+
const D = 104
|
|
78
|
+
const cx = width / 2
|
|
79
|
+
const cy = 42 + D / 2
|
|
80
|
+
|
|
81
|
+
const ring = box($.NSMakeRect(cx - D / 2, cy - D / 2, D, D))
|
|
82
|
+
ring.setCornerRadius(D / 2)
|
|
83
|
+
ring.setFillColor(rgb(RING_BG, 1))
|
|
84
|
+
ring.setBorderColor(rgb(G, 0.5))
|
|
85
|
+
ring.setBorderWidth(1.5)
|
|
86
|
+
win.contentView.addSubview(ring)
|
|
87
|
+
|
|
88
|
+
const disc = box($.NSMakeRect(cx, cy, 0, 0))
|
|
89
|
+
disc.setFillColor(rgb(G, 1))
|
|
90
|
+
win.contentView.addSubview(disc)
|
|
91
|
+
|
|
92
|
+
// A thin outline that expands past the ring and fades out on every call.
|
|
93
|
+
const rippleRing = box($.NSMakeRect(cx - D / 2, cy - D / 2, D, D))
|
|
94
|
+
rippleRing.setFillColor($.NSColor.clearColor)
|
|
95
|
+
rippleRing.setBorderWidth(2)
|
|
96
|
+
rippleRing.setBorderColor(rgb(G, 0))
|
|
97
|
+
rippleRing.setCornerRadius(D / 2)
|
|
98
|
+
win.contentView.addSubview(rippleRing)
|
|
99
|
+
|
|
100
|
+
function label(y, font, color) {
|
|
101
|
+
const f = $.NSTextField.alloc.initWithFrame($.NSMakeRect(0, y, width, 20))
|
|
102
|
+
f.setBezeled(false)
|
|
103
|
+
f.setDrawsBackground(false)
|
|
104
|
+
f.setEditable(false)
|
|
105
|
+
f.setSelectable(false)
|
|
106
|
+
f.setFont(font)
|
|
107
|
+
f.setTextColor(color)
|
|
108
|
+
win.contentView.addSubview(f)
|
|
109
|
+
return f
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const number = label(76, $.NSFont.monospacedDigitSystemFontOfSizeWeight(30, $.NSFontWeightBold), $.NSColor.whiteColor)
|
|
113
|
+
const caption = label(16, $.NSFont.systemFontOfSizeWeight(10.5, $.NSFontWeightSemibold), dim)
|
|
114
|
+
|
|
115
|
+
const detailRect = $.NSMakeRect(
|
|
116
|
+
screen.origin.x + screen.size.width - detailWidth - 24,
|
|
117
|
+
Math.max(screen.origin.y + 24, rect.origin.y - detailHeight - 12),
|
|
118
|
+
detailWidth,
|
|
119
|
+
detailHeight,
|
|
120
|
+
)
|
|
121
|
+
const detail = $.NSPanel.alloc.initWithContentRectStyleMaskBackingDefer(detailRect, style, $.NSBackingStoreBuffered, false)
|
|
122
|
+
detail.setLevel($.NSFloatingWindowLevel)
|
|
123
|
+
detail.setCollectionBehavior(
|
|
124
|
+
$.NSWindowCollectionBehaviorCanJoinAllSpaces | $.NSWindowCollectionBehaviorFullScreenAuxiliary,
|
|
125
|
+
)
|
|
126
|
+
detail.setMovableByWindowBackground(true)
|
|
127
|
+
detail.setTitle("rippletide counter")
|
|
128
|
+
detail.setAppearance($.NSAppearance.appearanceNamed($.NSAppearanceNameDarkAqua))
|
|
129
|
+
detail.setOpaque(false)
|
|
130
|
+
detail.setBackgroundColor($.NSColor.colorWithCalibratedWhiteAlpha(0.11, 0.98))
|
|
131
|
+
detail.setReleasedWhenClosed(false)
|
|
132
|
+
|
|
133
|
+
function detailField(frame, font, color) {
|
|
134
|
+
const f = $.NSTextField.alloc.initWithFrame(frame)
|
|
135
|
+
f.setBezeled(false)
|
|
136
|
+
f.setDrawsBackground(false)
|
|
137
|
+
f.setEditable(false)
|
|
138
|
+
f.setSelectable(false)
|
|
139
|
+
f.setFont(font)
|
|
140
|
+
f.setTextColor(color)
|
|
141
|
+
detail.contentView.addSubview(f)
|
|
142
|
+
return f
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const detailTitle = detailField(
|
|
146
|
+
$.NSMakeRect(22, detailHeight - 42, detailWidth - 44, 24),
|
|
147
|
+
$.NSFont.systemFontOfSizeWeight(17, $.NSFontWeightBold),
|
|
148
|
+
$.NSColor.whiteColor,
|
|
149
|
+
)
|
|
150
|
+
const detailSub = detailField(
|
|
151
|
+
$.NSMakeRect(22, detailHeight - 63, detailWidth - 44, 18),
|
|
152
|
+
$.NSFont.systemFontOfSizeWeight(11, $.NSFontWeightMedium),
|
|
153
|
+
dim,
|
|
154
|
+
)
|
|
155
|
+
const legendCodex = detailField(
|
|
156
|
+
$.NSMakeRect(22, detailHeight - 84, 54, 16),
|
|
157
|
+
$.NSFont.systemFontOfSizeWeight(10, $.NSFontWeightSemibold),
|
|
158
|
+
rgb(G, 1),
|
|
159
|
+
)
|
|
160
|
+
legendCodex.setStringValue("Codex")
|
|
161
|
+
const legendClaude = detailField(
|
|
162
|
+
$.NSMakeRect(82, detailHeight - 84, 82, 16),
|
|
163
|
+
$.NSFont.systemFontOfSizeWeight(10, $.NSFontWeightSemibold),
|
|
164
|
+
rgb(CLAUDE, 1),
|
|
165
|
+
)
|
|
166
|
+
legendClaude.setStringValue("Claude")
|
|
167
|
+
const legendRisky = detailField(
|
|
168
|
+
$.NSMakeRect(166, detailHeight - 84, 76, 16),
|
|
169
|
+
$.NSFont.systemFontOfSizeWeight(10, $.NSFontWeightSemibold),
|
|
170
|
+
rgb(RED, 1),
|
|
171
|
+
)
|
|
172
|
+
legendRisky.setStringValue("Risky")
|
|
173
|
+
|
|
174
|
+
const rowCount = 8
|
|
175
|
+
const rowTop = detailHeight - 108
|
|
176
|
+
const rowGap = 27
|
|
177
|
+
const barX = 138
|
|
178
|
+
const barW = 168
|
|
179
|
+
const rows = []
|
|
180
|
+
for (let i = 0; i < rowCount; i++) {
|
|
181
|
+
const y = rowTop - i * rowGap
|
|
182
|
+
const name = detailField(
|
|
183
|
+
$.NSMakeRect(22, y - 1, 108, 18),
|
|
184
|
+
$.NSFont.systemFontOfSizeWeight(10.5, $.NSFontWeightMedium),
|
|
185
|
+
$.NSColor.colorWithCalibratedWhiteAlpha(0.82, 1.0),
|
|
186
|
+
)
|
|
187
|
+
const track = box($.NSMakeRect(barX, y + 4, barW, 7))
|
|
188
|
+
track.setCornerRadius(3.5)
|
|
189
|
+
track.setFillColor($.NSColor.colorWithCalibratedWhiteAlpha(0.22, 1.0))
|
|
190
|
+
detail.contentView.addSubview(track)
|
|
191
|
+
const bar = box($.NSMakeRect(barX, y + 4, 0, 7))
|
|
192
|
+
bar.setCornerRadius(3.5)
|
|
193
|
+
bar.setFillColor(rgb(G, 1))
|
|
194
|
+
detail.contentView.addSubview(bar)
|
|
195
|
+
const riskBar = box($.NSMakeRect(barX, y + 4, 0, 7))
|
|
196
|
+
riskBar.setCornerRadius(3.5)
|
|
197
|
+
riskBar.setFillColor(rgb(RED, 1))
|
|
198
|
+
detail.contentView.addSubview(riskBar)
|
|
199
|
+
const value = detailField(
|
|
200
|
+
$.NSMakeRect(barX + barW + 10, y - 1, 48, 18),
|
|
201
|
+
$.NSFont.monospacedDigitSystemFontOfSizeWeight(10.5, $.NSFontWeightSemibold),
|
|
202
|
+
$.NSColor.whiteColor,
|
|
203
|
+
)
|
|
204
|
+
rows.push({ name, track, bar, riskBar, value })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function toggleDetails() {
|
|
208
|
+
const now = Date.now()
|
|
209
|
+
if (now - lastToggleAt < 250) return
|
|
210
|
+
lastToggleAt = now
|
|
211
|
+
if (detail.isVisible) {
|
|
212
|
+
detail.orderOut(null)
|
|
213
|
+
} else {
|
|
214
|
+
detail.makeKeyAndOrderFront(null)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toggleRiskDistribution() {
|
|
219
|
+
const now = Date.now()
|
|
220
|
+
if (now - lastRiskToggleAt < 250) return
|
|
221
|
+
lastRiskToggleAt = now
|
|
222
|
+
detailMode = detailMode === "models" ? "risks" : detailMode === "risks" ? "reroutes" : "models"
|
|
223
|
+
shownModelsKey = null
|
|
224
|
+
const state = readState()
|
|
225
|
+
const calls = state && typeof state.calls === "number" ? state.calls : 0
|
|
226
|
+
updateDetails(state, calls)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ObjC.registerSubclass({
|
|
230
|
+
name: "XrayClickView",
|
|
231
|
+
superclass: "NSView",
|
|
232
|
+
methods: {
|
|
233
|
+
"acceptsFirstMouse:": {
|
|
234
|
+
types: ["bool", ["id"]],
|
|
235
|
+
implementation: () => true,
|
|
236
|
+
},
|
|
237
|
+
"mouseDown:": {
|
|
238
|
+
types: ["void", ["id"]],
|
|
239
|
+
implementation: () => toggleDetails(),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
ObjC.registerSubclass({
|
|
245
|
+
name: "XrayRiskClickView",
|
|
246
|
+
superclass: "NSView",
|
|
247
|
+
methods: {
|
|
248
|
+
"acceptsFirstMouse:": {
|
|
249
|
+
types: ["bool", ["id"]],
|
|
250
|
+
implementation: () => true,
|
|
251
|
+
},
|
|
252
|
+
"mouseDown:": {
|
|
253
|
+
types: ["void", ["id"]],
|
|
254
|
+
implementation: () => toggleRiskDistribution(),
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const clickView = $.XrayClickView.alloc.initWithFrame($.NSMakeRect(0, 0, width, height))
|
|
260
|
+
clickView.setAutoresizingMask($.NSViewWidthSizable | $.NSViewHeightSizable)
|
|
261
|
+
win.contentView.addSubview(clickView)
|
|
262
|
+
|
|
263
|
+
const riskClickView = $.XrayRiskClickView.alloc.initWithFrame($.NSMakeRect(158, detailHeight - 92, 94, 30))
|
|
264
|
+
riskClickView.setToolTip("Cycle risk categories and rerouted calls")
|
|
265
|
+
detail.contentView.addSubview(riskClickView)
|
|
266
|
+
|
|
267
|
+
// Center by measuring the text and sizing the field to it.
|
|
268
|
+
function place(f, text, y, h) {
|
|
269
|
+
f.setStringValue(text)
|
|
270
|
+
const attrs = $.NSDictionary.dictionaryWithObjectForKey(f.font, $.NSFontAttributeName)
|
|
271
|
+
const w = Math.ceil($(text).sizeWithAttributes(attrs).width) + 6
|
|
272
|
+
f.setFrame($.NSMakeRect((width - w) / 2, y, w, h))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const K = 30 // the core approaches full as calls grow (about half-full around K calls)
|
|
276
|
+
|
|
277
|
+
let shownCalls = null
|
|
278
|
+
let shownRisky = null
|
|
279
|
+
let shownPrefix = null
|
|
280
|
+
let shownModelsKey = null
|
|
281
|
+
let shownDetailCalls = null
|
|
282
|
+
let shownDetailRisky = null
|
|
283
|
+
let shownDetailRerouted = null
|
|
284
|
+
let lastToggleAt = 0
|
|
285
|
+
let lastRiskToggleAt = 0
|
|
286
|
+
let detailMode = "models"
|
|
287
|
+
let displayFrac = 0
|
|
288
|
+
let displayCalls = 0 // the number rolls toward the true count like an odometer
|
|
289
|
+
let renderedInt = -1
|
|
290
|
+
let renderedCaption = null
|
|
291
|
+
let flash = 0 // 1 on a new call, decays each frame
|
|
292
|
+
let riskyFlash = 0 // risky responses briefly override the green pulse with red
|
|
293
|
+
let milestoneFlash = 0 // 1 when a milestone is crossed, tints everything gold
|
|
294
|
+
let milestoneValue = 0
|
|
295
|
+
let ripplePulse = 0 // 1 on a new call, drives the expanding outline
|
|
296
|
+
let rippleColor = G
|
|
297
|
+
let breathT = 0 // frame counter for the idle breathing sine
|
|
298
|
+
|
|
299
|
+
function truncate(s, n) {
|
|
300
|
+
s = String(s)
|
|
301
|
+
return s.length <= n ? s : s.slice(0, Math.max(0, n - 3)) + "..."
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Compact token count (1234 -> 1.2k, 3_400_000 -> 3.4M) and USD cost for the caption.
|
|
305
|
+
function formatTokens(n) {
|
|
306
|
+
n = Number(n) || 0
|
|
307
|
+
if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + "M"
|
|
308
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k"
|
|
309
|
+
return String(Math.round(n))
|
|
310
|
+
}
|
|
311
|
+
function formatUsd(n) {
|
|
312
|
+
n = Number(n) || 0
|
|
313
|
+
if (n >= 100) return "$" + n.toFixed(0)
|
|
314
|
+
if (n >= 1) return "$" + n.toFixed(2)
|
|
315
|
+
if (n > 0) return "$" + n.toFixed(3)
|
|
316
|
+
return "$0"
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Highest milestone crossed between two call counts (0 if none). Fixed stops
|
|
320
|
+
// early on, then every 1000 so the celebration keeps recurring on long runs.
|
|
321
|
+
function milestoneCrossed(prev, cur) {
|
|
322
|
+
const stops = [10, 25, 50, 100, 250, 500, 1000]
|
|
323
|
+
let hit = 0
|
|
324
|
+
for (let i = 0; i < stops.length; i++) {
|
|
325
|
+
if (prev < stops[i] && cur >= stops[i]) hit = stops[i]
|
|
326
|
+
}
|
|
327
|
+
if (cur >= 1000 && Math.floor(cur / 1000) > Math.floor(prev / 1000)) {
|
|
328
|
+
hit = Math.floor(cur / 1000) * 1000
|
|
329
|
+
}
|
|
330
|
+
return hit
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function modelProvider(name, providers) {
|
|
334
|
+
const explicit = providers && providers[name]
|
|
335
|
+
if (explicit) return String(explicit)
|
|
336
|
+
const lower = String(name).toLowerCase()
|
|
337
|
+
if (lower.includes("claude")) return "claude"
|
|
338
|
+
if (lower.includes("gpt") || lower.includes("codex") || lower.includes("o3") || lower.includes("o4")) return "codex"
|
|
339
|
+
return "unknown"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function modelEntries(state) {
|
|
343
|
+
const models = state && state.models && typeof state.models === "object" ? state.models : {}
|
|
344
|
+
const providers = state && state.providers && typeof state.providers === "object" ? state.providers : {}
|
|
345
|
+
const riskyModels = state && state.riskyModels && typeof state.riskyModels === "object" ? state.riskyModels : {}
|
|
346
|
+
return Object.keys(Object.assign({}, models, riskyModels))
|
|
347
|
+
.map((name) => {
|
|
348
|
+
const value = Number(models[name]) || 0
|
|
349
|
+
const risky = Math.max(0, Math.min(value, Number(riskyModels[name]) || 0))
|
|
350
|
+
return [name, value, modelProvider(name, providers), risky]
|
|
351
|
+
})
|
|
352
|
+
.filter((entry) => entry[1] > 0)
|
|
353
|
+
.sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0])))
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function riskActionEntries(state) {
|
|
357
|
+
const actions = state && state.riskyActions && typeof state.riskyActions === "object" ? state.riskyActions : {}
|
|
358
|
+
return Object.keys(actions)
|
|
359
|
+
.map((name) => [name, Number(actions[name]) || 0])
|
|
360
|
+
.filter((entry) => entry[1] > 0)
|
|
361
|
+
.sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0])))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function rerouteEntries(state) {
|
|
365
|
+
const reroutes = state && state.reroutes && typeof state.reroutes === "object" ? state.reroutes : {}
|
|
366
|
+
return Object.keys(reroutes)
|
|
367
|
+
.map((name) => [name.split("claude-").join(""), Number(reroutes[name]) || 0])
|
|
368
|
+
.filter((entry) => entry[1] > 0)
|
|
369
|
+
.sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0])))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function updateDetails(state, calls) {
|
|
373
|
+
const risky = state && typeof state.risky === "number" ? state.risky : 0
|
|
374
|
+
const rerouted = state && typeof state.rerouted === "number" ? state.rerouted : 0
|
|
375
|
+
const entries =
|
|
376
|
+
detailMode === "risks" ? riskActionEntries(state) : detailMode === "reroutes" ? rerouteEntries(state) : modelEntries(state)
|
|
377
|
+
const key = `${detailMode}:${JSON.stringify(entries)}`
|
|
378
|
+
if (key === shownModelsKey && calls === shownDetailCalls && risky === shownDetailRisky && rerouted === shownDetailRerouted) return
|
|
379
|
+
shownModelsKey = key
|
|
380
|
+
shownDetailCalls = calls
|
|
381
|
+
shownDetailRisky = risky
|
|
382
|
+
shownDetailRerouted = rerouted
|
|
383
|
+
|
|
384
|
+
if (detailMode === "risks") {
|
|
385
|
+
updateRiskDetails(entries, risky)
|
|
386
|
+
} else if (detailMode === "reroutes") {
|
|
387
|
+
updateRerouteDetails(entries, rerouted)
|
|
388
|
+
} else {
|
|
389
|
+
updateModelDetails(entries, calls, risky, rerouted)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function clearRow(i) {
|
|
394
|
+
rows[i].name.setStringValue("")
|
|
395
|
+
rows[i].value.setStringValue("")
|
|
396
|
+
rows[i].bar.setFrame($.NSMakeRect(barX, rowTop - i * rowGap + 4, 0, 7))
|
|
397
|
+
rows[i].riskBar.setFrame($.NSMakeRect(barX, rowTop - i * rowGap + 4, 0, 7))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function updateModelDetails(entries, calls, risky, rerouted) {
|
|
401
|
+
detailTitle.setStringValue("LLM calls by model")
|
|
402
|
+
const reroutedNote = rerouted > 0 ? ` · ${rerouted} rerouted` : ""
|
|
403
|
+
detailSub.setStringValue(entries.length ? `${calls} total · ${risky} risky${reroutedNote} · click Risky for more` : `${risky} risky${reroutedNote} · no model calls yet`)
|
|
404
|
+
legendCodex.setFrame($.NSMakeRect(22, detailHeight - 84, 54, 16))
|
|
405
|
+
legendCodex.setStringValue("Codex")
|
|
406
|
+
legendCodex.setTextColor(rgb(G, 1))
|
|
407
|
+
legendClaude.setStringValue("Claude")
|
|
408
|
+
legendClaude.setTextColor(rgb(CLAUDE, 1))
|
|
409
|
+
legendRisky.setStringValue("Risky")
|
|
410
|
+
legendRisky.setTextColor(rgb(RED, 1))
|
|
411
|
+
|
|
412
|
+
const max = entries.length ? entries[0][1] : 1
|
|
413
|
+
for (let i = 0; i < rowCount; i++) {
|
|
414
|
+
const entry = entries[i]
|
|
415
|
+
if (!entry) {
|
|
416
|
+
clearRow(i)
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
const [name, value, provider, riskyValue] = entry
|
|
420
|
+
const y = rowTop - i * rowGap
|
|
421
|
+
const w = Math.max(4, Math.round((value / max) * barW))
|
|
422
|
+
const riskW = riskyValue > 0 ? Math.max(4, Math.round((riskyValue / value) * w)) : 0
|
|
423
|
+
rows[i].name.setStringValue(truncate(name, 18))
|
|
424
|
+
rows[i].value.setStringValue(riskyValue > 0 ? `${riskyValue}/${value}` : String(value))
|
|
425
|
+
rows[i].bar.setFillColor(rgb(providerColor(provider), 1))
|
|
426
|
+
rows[i].bar.setFrame($.NSMakeRect(barX, y + 4, w, 7))
|
|
427
|
+
rows[i].riskBar.setFrame($.NSMakeRect(barX + w - riskW, y + 4, riskW, 7))
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function updateRiskDetails(entries, risky) {
|
|
432
|
+
// Header sums the ACTION bars (a call can flag several), which differs from the
|
|
433
|
+
// risky-CALL count shown on the compact window; label it as actions to avoid confusion.
|
|
434
|
+
const actionTotal = entries.reduce((sum, entry) => sum + (Number(entry[1]) || 0), 0)
|
|
435
|
+
detailTitle.setStringValue("Risky actions by category")
|
|
436
|
+
detailSub.setStringValue(entries.length ? `${actionTotal} risky actions · ${risky} risky calls · click Risky for reroutes` : "No risk categories yet · click Risky for reroutes")
|
|
437
|
+
legendCodex.setFrame($.NSMakeRect(22, detailHeight - 84, 54, 16))
|
|
438
|
+
legendCodex.setStringValue("Categories")
|
|
439
|
+
legendCodex.setTextColor(rgb(RED, 1))
|
|
440
|
+
legendClaude.setStringValue("")
|
|
441
|
+
legendRisky.setStringValue("Risky")
|
|
442
|
+
legendRisky.setTextColor(rgb(RED, 1))
|
|
443
|
+
|
|
444
|
+
const max = entries.length ? entries[0][1] : 1
|
|
445
|
+
for (let i = 0; i < rowCount; i++) {
|
|
446
|
+
const entry = entries[i]
|
|
447
|
+
if (!entry) {
|
|
448
|
+
clearRow(i)
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
const [name, value] = entry
|
|
452
|
+
const y = rowTop - i * rowGap
|
|
453
|
+
const w = Math.max(4, Math.round((value / max) * barW))
|
|
454
|
+
rows[i].name.setStringValue(truncate(name, 18))
|
|
455
|
+
rows[i].value.setStringValue(String(value))
|
|
456
|
+
rows[i].bar.setFillColor(rgb(RED, 1))
|
|
457
|
+
rows[i].bar.setFrame($.NSMakeRect(barX, y + 4, w, 7))
|
|
458
|
+
rows[i].riskBar.setFrame($.NSMakeRect(barX, y + 4, 0, 7))
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Calls answered by a different model than the one requested. Row names read
|
|
463
|
+
// "requested → served" with the shared claude- prefix dropped to fit.
|
|
464
|
+
function updateRerouteDetails(entries, rerouted) {
|
|
465
|
+
detailTitle.setStringValue("Rerouted calls by model")
|
|
466
|
+
detailSub.setStringValue(entries.length ? `${rerouted} answered by another model · click Risky to return` : "No reroutes detected yet · click Risky to return")
|
|
467
|
+
legendCodex.setFrame($.NSMakeRect(22, detailHeight - 84, 130, 16))
|
|
468
|
+
legendCodex.setStringValue("Requested → served")
|
|
469
|
+
legendCodex.setTextColor(rgb(GOLD, 1))
|
|
470
|
+
legendClaude.setStringValue("")
|
|
471
|
+
legendRisky.setStringValue("Risky")
|
|
472
|
+
legendRisky.setTextColor(rgb(RED, 1))
|
|
473
|
+
|
|
474
|
+
const max = entries.length ? entries[0][1] : 1
|
|
475
|
+
for (let i = 0; i < rowCount; i++) {
|
|
476
|
+
const entry = entries[i]
|
|
477
|
+
if (!entry) {
|
|
478
|
+
clearRow(i)
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
const [name, value] = entry
|
|
482
|
+
const y = rowTop - i * rowGap
|
|
483
|
+
const w = Math.max(4, Math.round((value / max) * barW))
|
|
484
|
+
rows[i].name.setStringValue(truncate(name, 18))
|
|
485
|
+
rows[i].value.setStringValue(String(value))
|
|
486
|
+
rows[i].bar.setFillColor(rgb(GOLD, 1))
|
|
487
|
+
rows[i].bar.setFrame($.NSMakeRect(barX, y + 4, w, 7))
|
|
488
|
+
rows[i].riskBar.setFrame($.NSMakeRect(barX, y + 4, 0, 7))
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function tick() {
|
|
493
|
+
if (!win.isVisible) return app.terminate(null)
|
|
494
|
+
const state = readState()
|
|
495
|
+
const calls = state && typeof state.calls === "number" ? state.calls : 0
|
|
496
|
+
const risky = state && typeof state.risky === "number" ? state.risky : 0
|
|
497
|
+
const prefix = state && state.label ? String(state.label).toUpperCase() + " · " : ""
|
|
498
|
+
if (calls !== shownCalls || risky !== shownRisky || prefix !== shownPrefix) {
|
|
499
|
+
const prevCalls = shownCalls
|
|
500
|
+
const gainedCall = prevCalls !== null && calls > prevCalls
|
|
501
|
+
const gainedRisky = shownRisky !== null && risky > shownRisky
|
|
502
|
+
const milestone = gainedCall ? milestoneCrossed(prevCalls, calls) : 0
|
|
503
|
+
if (gainedCall) {
|
|
504
|
+
flash = 1
|
|
505
|
+
ripplePulse = 1
|
|
506
|
+
rippleColor = G
|
|
507
|
+
}
|
|
508
|
+
if (milestone) {
|
|
509
|
+
milestoneFlash = 1
|
|
510
|
+
milestoneValue = milestone
|
|
511
|
+
rippleColor = GOLD
|
|
512
|
+
}
|
|
513
|
+
if (gainedRisky) {
|
|
514
|
+
riskyFlash = 1
|
|
515
|
+
ripplePulse = 1
|
|
516
|
+
if (!milestone) rippleColor = RED
|
|
517
|
+
}
|
|
518
|
+
shownCalls = calls
|
|
519
|
+
shownRisky = risky
|
|
520
|
+
shownPrefix = prefix
|
|
521
|
+
}
|
|
522
|
+
updateDetails(state, calls)
|
|
523
|
+
|
|
524
|
+
// 1. Odometer: roll the shown number toward the true count.
|
|
525
|
+
displayCalls += (calls - displayCalls) * 0.16
|
|
526
|
+
if (Math.abs(calls - displayCalls) < 0.5) displayCalls = calls
|
|
527
|
+
const shownInt = Math.round(displayCalls)
|
|
528
|
+
if (shownInt !== renderedInt) {
|
|
529
|
+
place(number, String(shownInt), 76, 40)
|
|
530
|
+
renderedInt = shownInt
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 3. Milestone banner briefly replaces the caption, then reverts on decay.
|
|
534
|
+
const totalTokens = state && state.tokens && typeof state.tokens.totalTokens === "number" ? state.tokens.totalTokens : 0
|
|
535
|
+
const costUsd = state && typeof state.costUsd === "number" ? state.costUsd : 0
|
|
536
|
+
const captionText =
|
|
537
|
+
milestoneFlash > 0.3 ? `MILESTONE · ${milestoneValue} CALLS` : `${prefix}${formatTokens(totalTokens)} TOK · ${formatUsd(costUsd)}`
|
|
538
|
+
if (captionText !== renderedCaption) {
|
|
539
|
+
place(caption, captionText, 16, 15)
|
|
540
|
+
renderedCaption = captionText
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const pulse = Math.max(flash, riskyFlash, milestoneFlash)
|
|
544
|
+
|
|
545
|
+
// 2. Idle breathing: a slow sine on the core (and ring glow) when nothing is
|
|
546
|
+
// happening, so the popup stays alive between calls. It fades out under pulses.
|
|
547
|
+
breathT += 1
|
|
548
|
+
const idle = Math.max(0, 1 - pulse * 4)
|
|
549
|
+
const breath = Math.sin(breathT * 0.07) * 0.014 * idle
|
|
550
|
+
|
|
551
|
+
const target = calls / (calls + K)
|
|
552
|
+
displayFrac += (target - displayFrac) * 0.18 // ease the core toward its size
|
|
553
|
+
const s = Math.max(0, Math.min(1, displayFrac + pulse * 0.06 + breath)) * D // surge on a new call
|
|
554
|
+
disc.setFrame($.NSMakeRect(cx - s / 2, cy - s / 2, s, s))
|
|
555
|
+
disc.setCornerRadius(s / 2)
|
|
556
|
+
const callFill = [lerp(G[0], 0.7, flash), lerp(G[1], 1, flash), lerp(G[2], 0.8, flash)]
|
|
557
|
+
const fill = mixColor(mixColor(callFill, RED, riskyFlash), GOLD, milestoneFlash)
|
|
558
|
+
const popupBg = mixColor(POPUP_BG, POPUP_RISK_BG, riskyFlash)
|
|
559
|
+
const ringBg = mixColor(RING_BG, RING_RISK_BG, riskyFlash)
|
|
560
|
+
// Tint the number on call/risky pulses but not on the gold milestone, so it
|
|
561
|
+
// stays legible over the gold core during the burst.
|
|
562
|
+
const numPulse = Math.max(flash, riskyFlash)
|
|
563
|
+
const numTint = mixColor(callFill, RED, riskyFlash)
|
|
564
|
+
const textColor = [lerp(1, numTint[0], numPulse), lerp(1, numTint[1], numPulse), lerp(1, numTint[2], numPulse)]
|
|
565
|
+
win.setBackgroundColor(rgb(popupBg, 1.0))
|
|
566
|
+
ring.setFillColor(rgb(ringBg, 1))
|
|
567
|
+
disc.setFillColor(rgb(fill, 1))
|
|
568
|
+
ring.setBorderColor(rgb(fill, Math.min(1, lerp(0.45, 1, pulse) + breath * 6)))
|
|
569
|
+
number.setTextColor(rgb(textColor, 1))
|
|
570
|
+
caption.setTextColor(rgb(mixColor(mixColor(DIM, [1, 0.88, 0.88], riskyFlash), GOLD, milestoneFlash), 1))
|
|
571
|
+
|
|
572
|
+
// 4. Ring ripple: expand the outline past the ring and fade it as it decays.
|
|
573
|
+
if (ripplePulse > 0.02) {
|
|
574
|
+
const rs = lerp(s, D * 1.28, 1 - ripplePulse)
|
|
575
|
+
rippleRing.setFrame($.NSMakeRect(cx - rs / 2, cy - rs / 2, rs, rs))
|
|
576
|
+
rippleRing.setCornerRadius(rs / 2)
|
|
577
|
+
rippleRing.setBorderColor(rgb(rippleColor, ripplePulse * 0.55))
|
|
578
|
+
} else {
|
|
579
|
+
rippleRing.setBorderColor(rgb(rippleColor, 0))
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
flash *= 0.88
|
|
583
|
+
riskyFlash *= 0.84
|
|
584
|
+
milestoneFlash *= 0.94
|
|
585
|
+
ripplePulse *= 0.9
|
|
586
|
+
if (flash < 0.02) flash = 0
|
|
587
|
+
if (riskyFlash < 0.02) riskyFlash = 0
|
|
588
|
+
if (milestoneFlash < 0.02) milestoneFlash = 0
|
|
589
|
+
if (ripplePulse < 0.02) ripplePulse = 0
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
win.makeKeyAndOrderFront(null)
|
|
593
|
+
tick()
|
|
594
|
+
|
|
595
|
+
$.NSTimer.scheduledTimerWithTimeIntervalRepeatsBlock(0.033, true, () => {
|
|
596
|
+
try {
|
|
597
|
+
tick()
|
|
598
|
+
} catch (_) {}
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
app.run()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The `counter` feature entry point — `rippletide counter`.
|
|
2
|
+
// Starts the Codex live counter: a floating sticky window that tallies LLM calls, tokens,
|
|
3
|
+
// and estimated cost in real time. macOS for the window; counting works anywhere.
|
|
4
|
+
import { run } from "./counter.js";
|
|
5
|
+
|
|
6
|
+
const HELP = `counter — a live Codex sticky window: LLM calls, tokens, and estimated cost.
|
|
7
|
+
|
|
8
|
+
Usage: rippletide counter
|
|
9
|
+
|
|
10
|
+
Run it, then use Codex (the desktop app or the CLI). A floating window shows the running
|
|
11
|
+
call count, total tokens, and estimated USD cost. It points Codex's openai_base_url at a
|
|
12
|
+
local proxy while running and restores your Codex config on exit. Ctrl-C (or close the
|
|
13
|
+
window) to stop. macOS only for the window.`;
|
|
14
|
+
|
|
15
|
+
export async function main(argv = []) {
|
|
16
|
+
const sub = argv[0];
|
|
17
|
+
if (sub === "-h" || sub === "--help" || sub === "help") {
|
|
18
|
+
console.log(HELP);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
return await run();
|
|
22
|
+
}
|