clauderipple 0.8.9 → 0.9.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/CHANGELOG.md +62 -0
- package/README.es.md +0 -4
- package/README.ja.md +0 -4
- package/README.ko.md +5 -1
- package/README.md +5 -1
- package/README.pt-BR.md +0 -4
- package/README.zh-CN.md +0 -4
- package/dist/app/dist/main.js +23 -3
- package/dist/app/package.json +1 -1
- package/dist/cli/src/codex.js +3 -1
- package/dist/cli/src/index.js +1 -1
- package/dist/cli/src/settings.js +5 -2
- package/dist/cli/src/tray.js +41 -5
- package/dist/mod/.claude-plugin/plugin.json +9 -1
- package/dist/mod/hooks/cache.ts +60 -0
- package/dist/mod/hooks/compact.ts +40 -0
- package/dist/mod/hooks/handoff.ts +43 -0
- package/dist/mod/hooks/i18n.ts +2716 -0
- package/dist/mod/hooks/judge.ts +226 -0
- package/dist/mod/hooks/ladder.js +179 -0
- package/dist/mod/hooks/ladder.ts +174 -0
- package/dist/mod/hooks/modes.ts +93 -0
- package/dist/mod/hooks/register.tsx +1458 -256
- package/dist/mod/hooks/session.ts +62 -0
- package/dist/mod/hooks/settings.js +342 -0
- package/dist/mod/hooks/settings.ts +321 -0
- package/dist/mod/hooks/stats.ts +59 -0
- package/dist/mod/hooks/ui/agents.tsx +138 -0
- package/dist/mod/hooks/ui/alerts.tsx +370 -0
- package/dist/mod/hooks/ui/art.ts +628 -0
- package/dist/mod/hooks/ui/band.tsx +836 -0
- package/dist/mod/hooks/ui/kit.tsx +258 -0
- package/dist/mod/hooks/ui/log.tsx +109 -0
- package/dist/mod/hooks/ui/parts.tsx +217 -0
- package/dist/mod/hooks/ui/ripple.ts +614 -0
- package/dist/mod/hooks/ui/settings.tsx +376 -0
- package/dist/mod/hooks/ui/setup.tsx +148 -0
- package/dist/mod/hooks/ui/theme.ts +217 -0
- package/dist/mod/hooks/ui/view.ts +371 -0
- package/dist/mod/hooks/worker.ts +1 -1
- package/dist/mod/hooks/workers.ts +217 -0
- package/dist/mod/tests/register.test.tsx +1730 -0
- package/dist/mod/tests/ui.test.tsx +853 -0
- package/dist/mod/tsconfig.json +2 -1
- package/dist/mod/types/index.d.ts +320 -1
- package/dist/router/src/admin.js +195 -53
- package/dist/router/src/agents.js +24 -5
- package/dist/router/src/bootstrap.js +4 -2
- package/dist/router/src/compat.js +10 -2
- package/dist/router/src/config.js +3 -0
- package/dist/router/src/health.js +33 -0
- package/dist/router/src/index.js +1 -0
- package/dist/router/src/ingress/models.js +4 -1
- package/dist/router/src/mod-settings.js +119 -0
- package/dist/router/src/model-policy.js +6 -0
- package/dist/router/src/model-scores.js +159 -0
- package/dist/router/src/picker.js +7 -0
- package/dist/router/src/providers/anthropic-account-pool.js +10 -4
- package/dist/router/src/providers/anthropic-accounts.js +26 -1
- package/dist/router/src/providers/chatgpt/catalog.js +45 -14
- package/dist/router/src/providers/chatgpt/index.js +9 -3
- package/dist/router/src/providers/chatgpt/translate.js +9 -5
- package/dist/router/src/providers/google/index.js +6 -2
- package/dist/router/src/providers/openai/index.js +4 -1
- package/dist/router/src/providers/openai/translate.js +1 -1
- package/dist/router/src/proxy.js +14 -3
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +932 -209
- package/dist/ui/favicon-32.png +0 -0
- package/dist/ui/favicon.svg +12 -0
- package/dist/ui/i18n.js +923 -237
- package/dist/ui/index.html +159 -87
- package/dist/ui/style.css +789 -408
- package/docs/ARCHITECTURE.md +134 -2
- package/package.json +7 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Colours and SVG plumbing shared by every drawing of the mod. Pure: no engine calls.
|
|
2
|
+
//
|
|
3
|
+
// The look is calm at rest: neutral ink on the host's own background, the ClaudeRipple accent
|
|
4
|
+
// (#6a56d6, packages/ui/style.css) only where something wants attention, and green/amber/red kept
|
|
5
|
+
// for results. SVG chips do not know the host's background, so they are drawn in translucent ink
|
|
6
|
+
// (a few percent of the text colour) and sit on the Code tab's warm or cool greys alike.
|
|
7
|
+
|
|
8
|
+
export type Appearance = 'auto' | 'dark' | 'light'
|
|
9
|
+
|
|
10
|
+
export type Palette = {
|
|
11
|
+
/** Body text. */
|
|
12
|
+
ink: string
|
|
13
|
+
/** Secondary text. */
|
|
14
|
+
muted: string
|
|
15
|
+
/** Hairlines, unlit glyph parts. */
|
|
16
|
+
faint: string
|
|
17
|
+
/** Chip fill and stroke: ink at a few percent. */
|
|
18
|
+
fill: string
|
|
19
|
+
stroke: string
|
|
20
|
+
accent: string
|
|
21
|
+
/** The accent as a wash behind a lit control. */
|
|
22
|
+
accentWash: string
|
|
23
|
+
accentLine: string
|
|
24
|
+
/** Fill of a primary action, white text on it. */
|
|
25
|
+
primary: string
|
|
26
|
+
ok: string
|
|
27
|
+
warn: string
|
|
28
|
+
bad: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const ACCENT = '#6a56d6'
|
|
32
|
+
|
|
33
|
+
export const DARK: Palette = {
|
|
34
|
+
ink: '#ecebf2',
|
|
35
|
+
muted: '#a19fae',
|
|
36
|
+
faint: '#55535f',
|
|
37
|
+
fill: 'rgba(236,235,242,0.045)',
|
|
38
|
+
stroke: 'rgba(236,235,242,0.13)',
|
|
39
|
+
accent: '#a594f5',
|
|
40
|
+
accentWash: 'rgba(150,130,245,0.17)',
|
|
41
|
+
accentLine: 'rgba(165,148,245,0.62)',
|
|
42
|
+
primary: '#6f5bdc',
|
|
43
|
+
ok: '#3ecb7d',
|
|
44
|
+
warn: '#e0b436',
|
|
45
|
+
bad: '#ef5a5a',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const LIGHT: Palette = {
|
|
49
|
+
ink: '#1d1b26',
|
|
50
|
+
muted: '#64616f',
|
|
51
|
+
faint: '#c9c6d2',
|
|
52
|
+
fill: 'rgba(29,27,38,0.035)',
|
|
53
|
+
stroke: 'rgba(29,27,38,0.13)',
|
|
54
|
+
accent: '#6a56d6',
|
|
55
|
+
accentWash: 'rgba(106,86,214,0.10)',
|
|
56
|
+
accentLine: 'rgba(106,86,214,0.55)',
|
|
57
|
+
primary: '#6a56d6',
|
|
58
|
+
ok: '#1a8a4a',
|
|
59
|
+
warn: '#b5820a',
|
|
60
|
+
bad: '#c73434',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Colours for native Text, which the host draws on its own background. With appearance auto the
|
|
65
|
+
* mod cannot know that background, so these are mid tones that read on both.
|
|
66
|
+
*/
|
|
67
|
+
export const TEXT = {
|
|
68
|
+
accent: '#7c69e0',
|
|
69
|
+
ok: '#16a34a',
|
|
70
|
+
warn: '#d97706',
|
|
71
|
+
bad: '#dc2626',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function textTone(appearance: Appearance): typeof TEXT {
|
|
75
|
+
if (appearance === 'dark') return { accent: DARK.accent, ok: DARK.ok, warn: DARK.warn, bad: DARK.bad }
|
|
76
|
+
if (appearance === 'light') return { accent: LIGHT.accent, ok: LIGHT.ok, warn: LIGHT.warn, bad: LIGHT.bad }
|
|
77
|
+
return TEXT
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A provider is named by whoever added it, so most names are not known here. The presets' default
|
|
81
|
+
// names keep their vendor's hue; any other name gets one from PALETTE picked by a hash of the name,
|
|
82
|
+
// so it keeps that colour from one session to the next. The palette leaves out the status
|
|
83
|
+
// red/green/amber, so a provider is never read as a result.
|
|
84
|
+
const KNOWN: Record<string, string> = {
|
|
85
|
+
anthropic: '#d97757',
|
|
86
|
+
chatgpt: '#10a37f',
|
|
87
|
+
'google-gemini': '#4285f4',
|
|
88
|
+
deepseek: '#4d6bfe',
|
|
89
|
+
'opencode-go': '#9333ea',
|
|
90
|
+
'opencode-zen': '#9333ea',
|
|
91
|
+
}
|
|
92
|
+
const PALETTE = ['#0891b2', '#db2777', '#7c3aed', '#0d9488', '#c026d3', '#4f46e5', '#0284c7', '#be185d']
|
|
93
|
+
|
|
94
|
+
export function providerColor(provider: string): string {
|
|
95
|
+
if (provider === 'refused') return TEXT.bad
|
|
96
|
+
const known = KNOWN[provider]
|
|
97
|
+
if (known) return known
|
|
98
|
+
let hash = 0
|
|
99
|
+
for (const char of provider) hash = (hash * 31 + char.charCodeAt(0)) >>> 0
|
|
100
|
+
return PALETTE[hash % PALETTE.length]!
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Worker tiers keep one hue each, away from the status colours and from each other. */
|
|
104
|
+
export const TIER_HUE: Record<'light' | 'standard' | 'deep' | 'design', string> = {
|
|
105
|
+
light: '#0891b2',
|
|
106
|
+
standard: '#6a56d6',
|
|
107
|
+
deep: '#be185d',
|
|
108
|
+
design: '#d97757',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The acceptance bar for translated providers is a 90% cache hit.
|
|
112
|
+
export function cacheTone(percent: number): 'ok' | 'warn' | 'bad' {
|
|
113
|
+
return percent >= 90 ? 'ok' : percent >= 50 ? 'warn' : 'bad'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Context fill: neutral until the swamp line, amber past it, red from 85%. */
|
|
117
|
+
export function contextTone(percent: number, swampAt: number): 'ink' | 'warn' | 'bad' {
|
|
118
|
+
return percent >= 85 ? 'bad' : percent >= swampAt ? 'warn' : 'ink'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Plan window: neutral under 80%, amber to 95%, red above. */
|
|
122
|
+
export function limitTone(percent: number): 'ink' | 'warn' | 'bad' {
|
|
123
|
+
return percent >= 95 ? 'bad' : percent >= 80 ? 'warn' : 'ink'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- SVG ------------------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
const VARS: (keyof Palette)[] = ['ink', 'muted', 'faint', 'fill', 'stroke', 'accent', 'accentWash', 'accentLine', 'primary', 'ok', 'warn', 'bad']
|
|
129
|
+
|
|
130
|
+
function varsOf(p: Palette): string {
|
|
131
|
+
return VARS.map(k => '--' + k + ':' + p[k]).join(';')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `var(--name)` for a palette key: what SVG markup paints with. */
|
|
135
|
+
export function v(key: keyof Palette): string {
|
|
136
|
+
return 'var(--' + key + ')'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const FONT = "-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif"
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One SVG document. The palette is set as CSS variables on the root: fixed for dark or light, and
|
|
143
|
+
* for auto switched by the frame's `prefers-color-scheme`. `viewBox` draws a grid of its own at
|
|
144
|
+
* this size (the mascot's 56 units); absent, one unit is one pixel.
|
|
145
|
+
*/
|
|
146
|
+
export function svgDoc(width: number, height: number, appearance: Appearance, body: string, css = '', viewBox = '0 0 ' + width + ' ' + height): string {
|
|
147
|
+
const vars =
|
|
148
|
+
appearance === 'dark'
|
|
149
|
+
? 'svg{' + varsOf(DARK) + '}'
|
|
150
|
+
: appearance === 'light'
|
|
151
|
+
? 'svg{' + varsOf(LIGHT) + '}'
|
|
152
|
+
: 'svg{' + varsOf(LIGHT) + '}@media (prefers-color-scheme:dark){svg{' + varsOf(DARK) + '}}'
|
|
153
|
+
const base =
|
|
154
|
+
'text{font-family:' + FONT + ';font-variant-numeric:tabular-nums;fill:var(--ink)}' +
|
|
155
|
+
'.cap{font-size:8.5px;letter-spacing:.09em;font-weight:600;fill:var(--muted)}' +
|
|
156
|
+
'.val{font-size:12.5px;font-weight:600}' +
|
|
157
|
+
'.sub{font-size:11.5px;fill:var(--muted)}' +
|
|
158
|
+
'.lbl{font-size:12px;font-weight:500}'
|
|
159
|
+
return (
|
|
160
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height +
|
|
161
|
+
'" viewBox="' + viewBox + '"><style>' + vars + base + css + '</style>' + body + '</svg>'
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// East Asian Wide and Fullwidth ranges (UAX #11), and the emoji planes terminals draw two wide.
|
|
166
|
+
const WIDE = /[ᄀ-ᅟ⌚⌛〈〉⏩-⏬⏰⏳◽◾☔☕♈-♓♿⚓⚡⚪⚫⚽⚾⛄⛅⛎⛔⛪⛲⛳⛵⛺⛽✅✊✋✨❌❎❓-❕❗➕-➗➰➿⬛⬜⭐⭕⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-ꥠ-가-힣豈-︐-︙︰--⦆¢-₩\u{16FE0}-\u{16FE4}\u{17000}-\u{18CFF}\u{1B000}-\u{1B2FF}\u{1F004}\u{1F0CF}\u{1F18E}\u{1F191}-\u{1F19A}\u{1F200}-\u{1F251}\u{1F300}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F7E0}-\u{1F7EB}\u{1F90C}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u
|
|
167
|
+
// Combining marks, zero-width spaces and joiners, variation selectors: no cell of their own.
|
|
168
|
+
const ZERO = /[\p{Mn}\p{Me}--︀-️]/u
|
|
169
|
+
|
|
170
|
+
/** Terminal cells a code point takes: 2 for East Asian Wide/Fullwidth, 0 for combining and controls, else 1. */
|
|
171
|
+
function cellWidth(char: string): number {
|
|
172
|
+
const code = char.codePointAt(0)!
|
|
173
|
+
if (code < 0x20 || (code >= 0x7f && code < 0xa0)) return 0
|
|
174
|
+
if (ZERO.test(char)) return 0
|
|
175
|
+
return WIDE.test(char) ? 2 : 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Terminal columns `text` takes: East Asian Wide/Fullwidth count 2, combining marks 0. */
|
|
179
|
+
export function displayWidth(text: string): number {
|
|
180
|
+
let width = 0
|
|
181
|
+
for (const char of text) width += cellWidth(char)
|
|
182
|
+
return width
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The desktop width of `text` in `ch`, unrounded: the host's `ch` is the width of "0" in its 14 px
|
|
187
|
+
* sans. Measured 2026-10-10 in Chromium on macOS: ch 8.8 px, Hangul and kana ~12 px (1.36 ch),
|
|
188
|
+
* "Medium" 5.9 ch, "X-High" 5.1 ch. Each class of character is taken a little wide of that.
|
|
189
|
+
*/
|
|
190
|
+
export function chOf(text: string): number {
|
|
191
|
+
let ch = 0
|
|
192
|
+
for (const char of text) {
|
|
193
|
+
if (displayWidth(char) === 2) ch += 1.45
|
|
194
|
+
else if (char === ' ') ch += 0.45
|
|
195
|
+
else if (/[ilIj.,:;|'!]/.test(char)) ch += 0.4
|
|
196
|
+
else if (/[mwMW@%]/.test(char)) ch += 1.35
|
|
197
|
+
else if (/[A-Z]/.test(char)) ch += 1.1
|
|
198
|
+
else if (/[0-9]/.test(char)) ch += 1
|
|
199
|
+
else ch += 0.85
|
|
200
|
+
}
|
|
201
|
+
return ch
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Cuts text to at most `max` terminal columns, the ellipsis included. */
|
|
205
|
+
export function clip(text: string, max: number): string {
|
|
206
|
+
if (displayWidth(text) <= max) return text
|
|
207
|
+
const room = Math.max(1, max - 1)
|
|
208
|
+
let out = ''
|
|
209
|
+
let used = 0
|
|
210
|
+
for (const char of text) {
|
|
211
|
+
const w = cellWidth(char)
|
|
212
|
+
if (used + w > room) break
|
|
213
|
+
out += char
|
|
214
|
+
used += w
|
|
215
|
+
}
|
|
216
|
+
return out + '…'
|
|
217
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// What the drawings take: view models register.tsx fills from the mod's state atoms, and the
|
|
2
|
+
// callbacks it hands in. Every renderer in ./ is a pure function of (elements, view, actions);
|
|
3
|
+
// none of them calls the engine, so they can be drawn and tested anywhere.
|
|
4
|
+
|
|
5
|
+
import type { AgentRec, Effort, HandoffKind, JudgeKind, JudgeRun, LadderOptions, Pick, Settings, StatusRequest, TierTarget } from '../../types'
|
|
6
|
+
import type { Appearance } from './theme'
|
|
7
|
+
import { t } from '../i18n'
|
|
8
|
+
import type { Lang } from '../i18n'
|
|
9
|
+
|
|
10
|
+
export type Surface = 'terminal' | 'desktop' | 'vscode' | 'mobile'
|
|
11
|
+
|
|
12
|
+
/** Where and how big: from `e.surface`, `e.props.bodyColumns` / `maxRows`, the clock and settings. */
|
|
13
|
+
export type Frame = {
|
|
14
|
+
surface: Surface
|
|
15
|
+
/** Cells across (`bodyColumns`). */
|
|
16
|
+
columns: number
|
|
17
|
+
/** Rows the band may take (`maxRows`); a pane passes its `scroll.bodyRows`. */
|
|
18
|
+
maxRows: number
|
|
19
|
+
/** Epoch ms when drawn; countdowns are computed from it. */
|
|
20
|
+
now: number
|
|
21
|
+
appearance: Appearance
|
|
22
|
+
lang: Lang
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Desktop and the editor draw SVG; the terminal and mobile draw text. */
|
|
26
|
+
export function isRich(frame: Frame): boolean {
|
|
27
|
+
return frame.surface === 'desktop' || frame.surface === 'vscode'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---- Dashboard -------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* auto: Auto picked it · deciding: the judge is thinking · paused: Auto cannot run on this model ·
|
|
34
|
+
* off: Auto is off (the level is the app's) · manual: the person picked the level.
|
|
35
|
+
*/
|
|
36
|
+
export type EffortMode = 'auto' | 'deciding' | 'paused' | 'off' | 'manual'
|
|
37
|
+
|
|
38
|
+
export type RouteView = {
|
|
39
|
+
/** `target`, or `target (requested)` when the router swapped it. */
|
|
40
|
+
model: string
|
|
41
|
+
provider: string
|
|
42
|
+
ok: boolean
|
|
43
|
+
resent: boolean
|
|
44
|
+
status: string
|
|
45
|
+
effort?: string
|
|
46
|
+
/** Prompt tokens read from cache, in percent. */
|
|
47
|
+
cachePct?: number
|
|
48
|
+
seconds: number
|
|
49
|
+
note?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type DashboardView = {
|
|
53
|
+
effort: {
|
|
54
|
+
level: Effort | null
|
|
55
|
+
mode: EffortMode
|
|
56
|
+
/** The judge's reason, a few words. */
|
|
57
|
+
why?: string
|
|
58
|
+
/** Who gave it. */
|
|
59
|
+
by?: Pick['by']
|
|
60
|
+
/** When the judge started (deciding): keeps the glow's phase across redraws. */
|
|
61
|
+
since?: number
|
|
62
|
+
}
|
|
63
|
+
/** Display name of the main chat's model ("Opus 5.5"); see modelName. */
|
|
64
|
+
model: string | null
|
|
65
|
+
context: { percent: number; tokens: number; window: number } | null
|
|
66
|
+
/** The last main reply's cache; null when unknown. */
|
|
67
|
+
cache: { expiresAt: number; ttlMs: number } | null
|
|
68
|
+
warm: { on: boolean; pings: number; cost: number; stopped?: string }
|
|
69
|
+
/** The hottest plan window, shown when present. */
|
|
70
|
+
limit: { kind: string; percent: number; resetsAt?: string; save: boolean } | null
|
|
71
|
+
route: RouteView | null
|
|
72
|
+
/** A line from the router side ("router not answering"), shown in place of the route. */
|
|
73
|
+
notice: string | null
|
|
74
|
+
auto: boolean
|
|
75
|
+
/** Compact turns into a lit Handoff when advice says so, or always with handoffButton 'always'. */
|
|
76
|
+
handoff: 'off' | 'advised' | 'always'
|
|
77
|
+
agents: { live: number; total: number }
|
|
78
|
+
logOpen: boolean
|
|
79
|
+
agentsOpen: boolean
|
|
80
|
+
/** The manual effort row is open. */
|
|
81
|
+
effortMenu: boolean
|
|
82
|
+
/** Context percent at which the swamp colour starts (settings.swampAt). */
|
|
83
|
+
swampAt: number
|
|
84
|
+
/** Band parts switched off: 'timer' | 'reason' | 'route'. */
|
|
85
|
+
hide: readonly string[]
|
|
86
|
+
/** Optional chips switched on: 'context' | 'limits'. */
|
|
87
|
+
extras: readonly string[]
|
|
88
|
+
isWorking: boolean
|
|
89
|
+
/** The active operating mode's name; absent when none is on. */
|
|
90
|
+
mode?: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type BandActions = {
|
|
94
|
+
toggleAuto: () => void
|
|
95
|
+
toggleWarm: () => void
|
|
96
|
+
compact: () => void
|
|
97
|
+
handoff: () => void
|
|
98
|
+
toggleLog: () => void
|
|
99
|
+
toggleAgents: () => void
|
|
100
|
+
openSettings: () => void
|
|
101
|
+
/** Opens or closes the manual effort row. */
|
|
102
|
+
toggleEffortMenu: () => void
|
|
103
|
+
/** A manual pick: Auto steps aside for it (spec §1 "Person wins"). */
|
|
104
|
+
pickEffort: (effort: Effort) => void
|
|
105
|
+
/** Hides the band (desktop: its × is drawn inside the band). */
|
|
106
|
+
hide?: () => void
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- Alerts and cards ------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export type ResultStage = 'handingOff' | 'handoffReady' | 'handoffDone' | 'compacting' | 'compacted' | 'failed'
|
|
112
|
+
|
|
113
|
+
export type AlertView =
|
|
114
|
+
/** The cache expired on a big chat: the next prompt re-reads it all. */
|
|
115
|
+
| { kind: 'cold'; tokens: number; idleMinutes: number }
|
|
116
|
+
/** Context is heavy. */
|
|
117
|
+
| { kind: 'swamp'; percent: number; tokens: number }
|
|
118
|
+
/** A plan window at or above 80%. */
|
|
119
|
+
| { kind: 'hot'; limitKind: string; percent: number; resetsAt?: string; save: boolean }
|
|
120
|
+
/** The judge failed and Auto fell back. */
|
|
121
|
+
| { kind: 'judgeDown'; judge: JudgeKind; reason: string; network?: boolean }
|
|
122
|
+
| { kind: 'result'; stage: ResultStage; handoffKind?: HandoffKind; detail?: string }
|
|
123
|
+
/** Compact with an optional note of what to keep. */
|
|
124
|
+
| { kind: 'compactNote'; draft: string }
|
|
125
|
+
/** Quick or full handoff, and what happens after. */
|
|
126
|
+
| { kind: 'handoffChoice'; handoffKind: HandoffKind; after: Settings['handoffAfter'] }
|
|
127
|
+
|
|
128
|
+
export type AlertActions = {
|
|
129
|
+
compact: (note?: string) => void
|
|
130
|
+
/** Opens the handoff choice bar. */
|
|
131
|
+
handoff: () => void
|
|
132
|
+
dismiss: () => void
|
|
133
|
+
toggleSave: () => void
|
|
134
|
+
openSettings: () => void
|
|
135
|
+
/** Each change of the compact note's field. */
|
|
136
|
+
noteInput: (text: string) => void
|
|
137
|
+
setHandoffKind: (kind: HandoffKind) => void
|
|
138
|
+
setHandoffAfter: (after: Settings['handoffAfter']) => void
|
|
139
|
+
go: () => void
|
|
140
|
+
/** Copies a handoff's text again (result card, after: copy). */
|
|
141
|
+
copy: () => void
|
|
142
|
+
/** Opens a new session in this folder with the written handoff (Confirm first's card). */
|
|
143
|
+
openNew: () => void
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- Settings and setup ----------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export type JevState = {
|
|
149
|
+
/** Where the key comes from; null when there is none. */
|
|
150
|
+
source: 'env' | 'file' | 'pasted' | 'zen' | null
|
|
151
|
+
test: { state: 'idle' | 'testing' | 'ok' | 'fail'; message?: string }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type SettingsView = {
|
|
155
|
+
settings: Settings
|
|
156
|
+
jev: JevState
|
|
157
|
+
/** What each rung can run (the ticked models); null until the router has answered. */
|
|
158
|
+
ladder?: LadderOptions | null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type SettingsActions = {
|
|
162
|
+
/** Applies and saves each change immediately. */
|
|
163
|
+
patch: (change: Partial<Settings>) => void
|
|
164
|
+
/** Turns a mode on by id, or every mode off with ''. */
|
|
165
|
+
setMode?: (id: string) => void
|
|
166
|
+
/** The Jev key typed into the field (Enter). */
|
|
167
|
+
jevKey: (key: string) => void
|
|
168
|
+
testJev: () => void
|
|
169
|
+
openAllSettings: () => void
|
|
170
|
+
close: () => void
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type SetupStep = 1 | 2 | 3 | 4
|
|
174
|
+
|
|
175
|
+
export type SetupView = {
|
|
176
|
+
step: SetupStep
|
|
177
|
+
draft: Settings
|
|
178
|
+
jev: JevState
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type SetupActions = {
|
|
182
|
+
patch: (change: Partial<Settings>) => void
|
|
183
|
+
jevKey: (key: string) => void
|
|
184
|
+
testJev: () => void
|
|
185
|
+
openAllSettings: () => void
|
|
186
|
+
back: () => void
|
|
187
|
+
next: () => void
|
|
188
|
+
/** Saves the draft with setupDone and closes. */
|
|
189
|
+
finish: () => void
|
|
190
|
+
/** Closes with the defaults, setupDone. */
|
|
191
|
+
skip: () => void
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- Panes -----------------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
export type AgentsView = {
|
|
197
|
+
agents: readonly AgentRec[]
|
|
198
|
+
now: number
|
|
199
|
+
showDone: boolean
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type AgentsActions = {
|
|
203
|
+
toggleDone: () => void
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type LogView = {
|
|
207
|
+
rows: readonly StatusRequest[]
|
|
208
|
+
mineOnly: boolean
|
|
209
|
+
/** This session's verdicts, listed among its requests. */
|
|
210
|
+
judges?: readonly JudgeRun[]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type LogActions = {
|
|
214
|
+
toggleScope: () => void
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- Helpers register.tsx and the drawings share ----------------------------------------------
|
|
218
|
+
|
|
219
|
+
export const EFFORTS: readonly Effort[] = ['low', 'medium', 'high', 'xhigh', 'max']
|
|
220
|
+
|
|
221
|
+
export function effortIndex(effort: Effort | null | undefined): number {
|
|
222
|
+
return effort ? EFFORTS.indexOf(effort) : -1
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function effortName(effort: Effort, lang: Lang): string {
|
|
226
|
+
return t(lang, 'band.' + effort)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Route records can contain unknown effort ids; translate only the known display values. */
|
|
230
|
+
export function effortLabel(value: string, lang: Lang): string {
|
|
231
|
+
return EFFORTS.includes(value as Effort) ? effortName(value as Effort, lang).toLowerCase() : value
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** "claude-opus-5-5[1m]" → "Opus 5.5"; ids it does not know pass through, trimmed. */
|
|
235
|
+
export function modelName(id: string): string {
|
|
236
|
+
const bare = id.replace(/\[.*?\]/g, '').replace(/^claude-/, '')
|
|
237
|
+
const m = /^(opus|sonnet|haiku|fable)-(\d+)(?:-(\d{1,2}))?/.exec(bare)
|
|
238
|
+
if (m) return m[1]!.charAt(0).toUpperCase() + m[1]!.slice(1) + ' ' + m[2] + (m[3] ? '.' + m[3] : '')
|
|
239
|
+
// A bare alias ("sonnet") reads as its family's name.
|
|
240
|
+
if (/^(opus|sonnet|haiku|fable)\b/.test(bare) && !bare.includes('-')) return bare.charAt(0).toUpperCase() + bare.slice(1)
|
|
241
|
+
return bare
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A TierTarget as one Select value: `agent` or `agent+model`. */
|
|
245
|
+
export function targetValue(target: TierTarget): string {
|
|
246
|
+
return target.model ? target.agent + '+' + target.model : target.agent
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function targetOf(value: string): TierTarget {
|
|
250
|
+
const plus = value.indexOf('+')
|
|
251
|
+
return plus < 0 ? { agent: value } : { agent: value.slice(0, plus), model: value.slice(plus + 1) }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A target as a person reads it: the model for Claude's own agents, else the agent; Claude's agent
|
|
256
|
+
* with no model runs on the session's model and says so.
|
|
257
|
+
*/
|
|
258
|
+
export function targetLabel(target: TierTarget, lang?: Lang): string {
|
|
259
|
+
if (target.model) return 'Claude ' + modelName(target.model)
|
|
260
|
+
return lang && target.agent === 'general-purpose' ? t(lang, 'workers.sessionModel') : target.agent
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
type Usage = NonNullable<StatusRequest['usage']>
|
|
264
|
+
|
|
265
|
+
export function promptTokens(usage: Usage): number {
|
|
266
|
+
return usage.input + usage.cached + (usage.cacheWrite ?? 0)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function cachePercent(usage: Usage | undefined): number | undefined {
|
|
270
|
+
if (!usage) return undefined
|
|
271
|
+
const prompt = promptTokens(usage)
|
|
272
|
+
return prompt > 0 ? Math.round((usage.cached / prompt) * 100) : undefined
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// The requested name's `@effort` is the agent file's default, not what was sent; the sent effort is
|
|
276
|
+
// its own field.
|
|
277
|
+
export function modelOf(record: StatusRequest): string {
|
|
278
|
+
const at = record.source.lastIndexOf('@')
|
|
279
|
+
const source = at < 0 ? record.source : record.source.slice(0, at)
|
|
280
|
+
// A dated id of the same model (claude-haiku-5-5-20260815 for claude-haiku-5-5) says nothing new.
|
|
281
|
+
return source.startsWith(record.target) || record.target.startsWith(source) ? record.target : record.target + ' (' + source + ')'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function routeOf(record: StatusRequest): RouteView {
|
|
285
|
+
const route: RouteView = {
|
|
286
|
+
model: modelOf(record),
|
|
287
|
+
provider: record.provider,
|
|
288
|
+
ok: record.ok,
|
|
289
|
+
resent: record.resent === true,
|
|
290
|
+
status: String(record.status),
|
|
291
|
+
seconds: record.ms / 1000,
|
|
292
|
+
}
|
|
293
|
+
const cache = cachePercent(record.usage)
|
|
294
|
+
if (record.effort) route.effort = record.effort
|
|
295
|
+
if (cache !== undefined) route.cachePct = cache
|
|
296
|
+
if (record.note) route.note = record.note
|
|
297
|
+
return route
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 1234 → "1.2k", 1_200_000 → "1.2M". */
|
|
301
|
+
export function compactNumber(n: number): string {
|
|
302
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
|
|
303
|
+
if (n >= 10_000) return Math.round(n / 1000) + 'k'
|
|
304
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
|
|
305
|
+
return String(Math.round(n))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** A countdown as the band shows it: "58m" above two minutes, "1:45" below, "cold" at zero. */
|
|
309
|
+
export function countdown(ms: number, lang: Lang): string {
|
|
310
|
+
if (ms <= 0) return t(lang, 'band.cold')
|
|
311
|
+
const s = Math.ceil(ms / 1000)
|
|
312
|
+
if (s >= 120) return t(lang, 'units.minutes', { n: Math.ceil(s / 60) })
|
|
313
|
+
return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The cache countdown as the band and the footer show it: whole minutes rounded up ("42m" … "1m"),
|
|
318
|
+
* "cold" at zero. It changes once a minute, so the band redraws at most once a minute for it.
|
|
319
|
+
*/
|
|
320
|
+
export function minutesLeft(ms: number, lang: Lang): string {
|
|
321
|
+
if (ms <= 0) return t(lang, 'band.cold')
|
|
322
|
+
return t(lang, 'units.minutes', { n: Math.ceil(ms / 60_000) })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** When minutesLeft next changes for a cache expiring at `expiresAt` (epoch ms); null once cold. */
|
|
326
|
+
export function nextMinuteChange(expiresAt: number, now: number): number | null {
|
|
327
|
+
const left = expiresAt - now
|
|
328
|
+
if (left <= 0) return null
|
|
329
|
+
return expiresAt - (Math.ceil(left / 60_000) - 1) * 60_000
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** A duration the Agents pane shows: "12s", "4m 05s", "1h 02m". */
|
|
333
|
+
export function elapsed(ms: number, lang: Lang): string {
|
|
334
|
+
const s = Math.max(0, Math.floor(ms / 1000))
|
|
335
|
+
if (s < 60) return t(lang, 'units.seconds', { n: s })
|
|
336
|
+
const m = Math.floor(s / 60)
|
|
337
|
+
if (m < 60) return t(lang, 'units.minSec', { m, s: String(s % 60).padStart(2, '0') })
|
|
338
|
+
return t(lang, 'units.hourMin', { h: Math.floor(m / 60), m: String(m % 60).padStart(2, '0') })
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Seconds with one decimal, as routes and log entries show them: "1.2s". */
|
|
342
|
+
export function seconds(value: number, lang: Lang): string {
|
|
343
|
+
return t(lang, 'units.seconds', { n: value.toFixed(1) })
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function clockTime(at: string | number): string {
|
|
347
|
+
const date = new Date(at)
|
|
348
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()].map(n => String(n).padStart(2, '0')).join(':')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** "resets 14:20" or "resets in 3h" from an ISO time. */
|
|
352
|
+
export function resetsLabel(resetsAt: string | undefined, now: number, lang: Lang): string | undefined {
|
|
353
|
+
if (!resetsAt) return undefined
|
|
354
|
+
const at = Date.parse(resetsAt)
|
|
355
|
+
if (!Number.isFinite(at)) return undefined
|
|
356
|
+
const left = at - now
|
|
357
|
+
if (left <= 0) return t(lang, 'band.resetting')
|
|
358
|
+
if (left < 3_600_000) return t(lang, 'band.resetsIn', { time: t(lang, 'units.minutes', { n: Math.max(1, Math.round(left / 60_000)) }) })
|
|
359
|
+
if (left < 86_400_000) {
|
|
360
|
+
const d = new Date(at)
|
|
361
|
+
return t(lang, 'band.resetsAt', { time: String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') })
|
|
362
|
+
}
|
|
363
|
+
return t(lang, 'band.resetsIn', { time: t(lang, 'units.days', { n: Math.round(left / 86_400_000) }) })
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** "five_hour" → "5-hour", "seven_day" → "Weekly". */
|
|
367
|
+
export function limitName(kind: string, lang: Lang): string {
|
|
368
|
+
if (kind === 'five_hour') return t(lang, 'band.fiveHour')
|
|
369
|
+
if (kind === 'seven_day') return t(lang, 'band.weekly')
|
|
370
|
+
return kind.replace(/_/g, ' ')
|
|
371
|
+
}
|
package/dist/mod/hooks/worker.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// `deepseek-v4.1-flash@low` runs the worker on that id, and the router records effort low.
|
|
12
12
|
|
|
13
13
|
/** The router's marker, found anywhere in the prompt rather than only at its top. */
|
|
14
|
-
const MARKER = /\[\[\s*(?:ripple|gpt)\s*:\s*([A-Za-z0-9.\-]+)\s*(?:@\s*([A-Za-z]+))?\s*\]\]/
|
|
14
|
+
export const MARKER = /\[\[\s*(?:ripple|gpt)\s*:\s*([A-Za-z0-9.\-]+)\s*(?:@\s*([A-Za-z]+))?\s*\]\]/
|
|
15
15
|
|
|
16
16
|
export type Spawn = { subagentType: string; prompt: string; model?: string }
|
|
17
17
|
export type WorkerFix = { prompt: string; model: string; dropped?: string }
|