clauderipple 0.8.10 → 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 +36 -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/package.json +1 -1
- package/dist/cli/src/codex.js +3 -1
- package/dist/cli/src/settings.js +5 -2
- 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 +170 -52
- package/dist/router/src/agents.js +24 -5
- package/dist/router/src/bootstrap.js +4 -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/chatgpt/index.js +6 -1
- package/dist/router/src/providers/chatgpt/translate.js +1 -1
- 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 +908 -209
- package/dist/ui/favicon-32.png +0 -0
- package/dist/ui/favicon.svg +12 -0
- package/dist/ui/i18n.js +919 -233
- package/dist/ui/index.html +159 -87
- package/dist/ui/style.css +789 -410
- package/docs/ARCHITECTURE.md +128 -0
- package/package.json +7 -3
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// Quick settings in the band. Each control applies immediately; the router dashboard owns
|
|
2
|
+
// the full settings page. Setup reuses the judge and keep-warm controls below.
|
|
3
|
+
//
|
|
4
|
+
// The terminal draws 리플이's column (parts.tsx `speak`): one row per setting, its options as ●/○
|
|
5
|
+
// words, then the way out to the dashboard. The desktop draws a capsule of the deep (kit.tsx): 리플이
|
|
6
|
+
// at the head saying in one line what the settings do right now, the two ways out (all settings,
|
|
7
|
+
// close) as drawn buttons at the top right, and four tiles, each a setting with its own picture of
|
|
8
|
+
// how it stands and its control right there, so nothing is a tab away.
|
|
9
|
+
|
|
10
|
+
import { t } from '../i18n'
|
|
11
|
+
import type { RenderNode } from 'claude-code'
|
|
12
|
+
|
|
13
|
+
import type { Settings } from '../../types'
|
|
14
|
+
import { CELL_PX, INK, MUTE, SUB, TILE_PX, TONE, tileArt } from './art'
|
|
15
|
+
import type { TileArt } from './art'
|
|
16
|
+
import { capsule, pill, pic, ripple, segmented, slider, toggle, well } from './kit'
|
|
17
|
+
import { action, actions, canDraw, dismiss, segmented as textSegmented, speak, toggle as textToggle } from './parts'
|
|
18
|
+
import type { Els } from './parts'
|
|
19
|
+
import { chOf, displayWidth, textTone } from './theme'
|
|
20
|
+
import type { Frame, SettingsActions, SettingsView } from './view'
|
|
21
|
+
import { isRich } from './view'
|
|
22
|
+
|
|
23
|
+
/** Every name a row of quick settings or setup shows: the name column is the widest of them. */
|
|
24
|
+
const ROW_NAMES = ['settings.effortAuto', 'settings.lean', 'settings.judge', 'settings.key', 'settings.keepWarm', 'settings.workers', 'settings.mode', 'band.effort', 'band.cache',
|
|
25
|
+
'workers.rung.light', 'workers.rung.standard', 'workers.rung.hard', 'workers.rung.plan'] as const
|
|
26
|
+
|
|
27
|
+
/** The name column's width in cells: snug to this language's names, so controls sit close to them. */
|
|
28
|
+
function nameCells(frame: Frame): number {
|
|
29
|
+
const names = ROW_NAMES.map(key => t(frame.lang, key))
|
|
30
|
+
return isRich(frame) ? Math.ceil(Math.max(...names.map(chOf))) + 1 : Math.max(...names.map(displayWidth)) + 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A labelled row: name column, the control, an optional quiet hint. */
|
|
34
|
+
export function row(el: Els, frame: Frame, key: string, label: string, control: RenderNode, hint?: string): RenderNode {
|
|
35
|
+
const { Box, Text } = el
|
|
36
|
+
return (
|
|
37
|
+
<Box key={'row-' + key} flexDirection="row" alignItems="center" columnGap={2} flexWrap="wrap">
|
|
38
|
+
<Box width={nameCells(frame)} flexShrink={0}>
|
|
39
|
+
<Text dimColor>{label}</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
{control}
|
|
42
|
+
{hint ? <Text dimColor wrap="wrap">{hint}</Text> : null}
|
|
43
|
+
</Box>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- The lean: five stops between cheaper and smarter -------------------------------------------
|
|
48
|
+
|
|
49
|
+
const BIAS: readonly Settings['bias'][] = [-2, -1, 0, 1, 2]
|
|
50
|
+
|
|
51
|
+
/** Cheaper ○ ○ ● ○ ○ Smarter on the editor; the terminal names the stops (−2 … +2). */
|
|
52
|
+
function biasStops(el: Els, frame: Frame, bias: Settings['bias'], onPick: (bias: Settings['bias']) => void): RenderNode {
|
|
53
|
+
const { Box, Button, Text } = el
|
|
54
|
+
const name = (b: number) => (b > 0 ? '+' + b : b < 0 ? '−' + -b : '0')
|
|
55
|
+
if (!isRich(frame)) return textSegmented(el, frame, 'bias', BIAS.map(b => ({ value: String(b), label: name(b) })), String(bias), picked => onPick(Number(picked) as Settings['bias']))
|
|
56
|
+
return (
|
|
57
|
+
<Box flexDirection="row" alignItems="center" columnGap={1}>
|
|
58
|
+
<Text dimColor>{t(frame.lang, 'settings.cheaper')}</Text>
|
|
59
|
+
<Box flexDirection="row">
|
|
60
|
+
{BIAS.map(b => (b === bias ? <Button key={'bias-' + b} plain label="●" onPress={() => onPick(b)} /> : <Button key={'bias-' + b} plain dimColor label="○" onPress={() => onPick(b)} />))}
|
|
61
|
+
</Box>
|
|
62
|
+
<Text dimColor>{t(frame.lang, 'settings.smarter')}</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- The terminal's rows (setup on the terminal lends them too) ---------------------------------
|
|
68
|
+
|
|
69
|
+
export function effortRows(el: Els, frame: Frame, d: Settings, a: SettingsActions): RenderNode[] {
|
|
70
|
+
const out = [row(el, frame, 'auto', t(frame.lang, 'settings.effortAuto'), textToggle(el, frame, 'set-auto', d.effortAuto ? t(frame.lang, 'band.on') : t(frame.lang, 'band.off'), d.effortAuto, () => a.patch({ effortAuto: !d.effortAuto })), t(frame.lang, 'settings.autoHint'))]
|
|
71
|
+
if (d.effortAuto) out.push(row(el, frame, 'bias', t(frame.lang, 'settings.lean'), biasStops(el, frame, d.bias, bias => a.patch({ bias })), t(frame.lang, 'settings.leanHint')))
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The Jev key field and its test, when TypeSafe's Jev judges and no key is known. */
|
|
76
|
+
function needsKey(view: SettingsView): boolean {
|
|
77
|
+
return view.settings.judge === 'jev' && view.settings.jevSource === 'typesafe' && !['env', 'file', 'pasted'].includes(view.jev.source ?? '')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function judgeRows(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode[] {
|
|
81
|
+
const d = view.settings
|
|
82
|
+
const tone = textTone(frame.appearance)
|
|
83
|
+
const { Box, Text } = el
|
|
84
|
+
const out: RenderNode[] = [
|
|
85
|
+
row(
|
|
86
|
+
el,
|
|
87
|
+
frame,
|
|
88
|
+
'judge',
|
|
89
|
+
t(frame.lang, 'settings.judge'),
|
|
90
|
+
textSegmented(
|
|
91
|
+
el,
|
|
92
|
+
frame,
|
|
93
|
+
'judge',
|
|
94
|
+
[
|
|
95
|
+
{ value: 'haiku', label: t(frame.lang, 'settings.haiku') },
|
|
96
|
+
{ value: 'jev', label: t(frame.lang, 'settings.jev') },
|
|
97
|
+
{ value: 'model', label: t(frame.lang, 'settings.routerModel') },
|
|
98
|
+
] as const,
|
|
99
|
+
d.judge,
|
|
100
|
+
judge => a.patch({ judge }),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
if (needsKey(view)) {
|
|
105
|
+
const jev = view.jev
|
|
106
|
+
const placeholder = t(frame.lang, 'settings.keyPaste')
|
|
107
|
+
const field = 'Input' in el ? (
|
|
108
|
+
<el.Input key="jev-key" placeholder={placeholder} value="" submitLabel={t(frame.lang, 'settings.saveKey')} onSubmit={(key: string) => a.jevKey(key)} />
|
|
109
|
+
) : (
|
|
110
|
+
<Text dimColor>{placeholder}</Text>
|
|
111
|
+
)
|
|
112
|
+
const result =
|
|
113
|
+
jev.test.state === 'testing' ? (
|
|
114
|
+
<Text dimColor>{t(frame.lang, 'settings.testing')}</Text>
|
|
115
|
+
) : jev.test.state === 'ok' ? (
|
|
116
|
+
<Text color={tone.ok} wrap="wrap">{'✓ ' + (jev.test.message ?? t(frame.lang, 'settings.keyWorks'))}</Text>
|
|
117
|
+
) : jev.test.state === 'fail' ? (
|
|
118
|
+
<Text color={tone.bad} wrap="wrap">{'✕ ' + (jev.test.message ?? t(frame.lang, 'settings.keyRejected'))}</Text>
|
|
119
|
+
) : null
|
|
120
|
+
out.push(
|
|
121
|
+
row(
|
|
122
|
+
el,
|
|
123
|
+
frame,
|
|
124
|
+
'jev-key',
|
|
125
|
+
t(frame.lang, 'settings.key'),
|
|
126
|
+
<Box flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={1} rowGap={1}>
|
|
127
|
+
{field}
|
|
128
|
+
{action(el, frame, 'jev-test', t(frame.lang, 'settings.test'), a.testJev, { hotkey: 't' })}
|
|
129
|
+
{result}
|
|
130
|
+
</Box>,
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The active saved worker set: Base followed by user-named modes. Rungs are edited on the dashboard.
|
|
139
|
+
*/
|
|
140
|
+
export function modeRows(el: Els, frame: Frame, d: Settings, a: SettingsActions): RenderNode[] {
|
|
141
|
+
const { Text } = el
|
|
142
|
+
const items = modeItems(frame, d)
|
|
143
|
+
const current = d.activeMode ?? ''
|
|
144
|
+
const control = 'Select' in el && a.setMode
|
|
145
|
+
? <el.Select key="mode" options={items} value={current} onSelect={(value: string) => a.setMode!(value)} />
|
|
146
|
+
: <Text>{items.find(item => item.value === current)?.label ?? '—'}</Text>
|
|
147
|
+
return [row(el, frame, 'mode', t(frame.lang, 'settings.mode'), control)]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function keepWarmRow(el: Els, frame: Frame, d: Settings, a: SettingsActions): RenderNode {
|
|
151
|
+
return row(el, frame, 'warm', t(frame.lang, 'settings.keepWarm'), textToggle(el, frame, 'set-warm', d.keepWarm ? t(frame.lang, 'band.on') : t(frame.lang, 'band.off'), d.keepWarm, () => a.patch({ keepWarm: !d.keepWarm })), t(frame.lang, 'settings.keepWarmHint'))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function modeItems(frame: Frame, d: Settings): { value: string; label: string }[] {
|
|
155
|
+
return [{ value: '', label: t(frame.lang, 'settings.modeNone') }, ...d.modes.map(m => ({ value: m.id, label: m.name }))]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function judgeName(frame: Frame, judge: Settings['judge']): string {
|
|
159
|
+
return t(frame.lang, judge === 'haiku' ? 'settings.haiku' : judge === 'jev' ? 'settings.jev' : 'settings.routerModel')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- Desktop: the deep's tiles -------------------------------------------------------------------
|
|
163
|
+
//
|
|
164
|
+
// A tile is a setting: its picture (art.ts `tileArt`, how the setting stands, alive while on), its
|
|
165
|
+
// name in bright ink with its switch at the right when it has one, one line on what it does, then
|
|
166
|
+
// its control. The pictures replace the old line icons, which said nothing at 15 px.
|
|
167
|
+
|
|
168
|
+
/** A tile's edge and glass: light on the water, brighter under the pointer. */
|
|
169
|
+
const TILE_EDGE = 'rgba(255,255,255,0.1)'
|
|
170
|
+
const TILE_EDGE_HOT = 'rgba(255,255,255,0.26)'
|
|
171
|
+
const TILE_GLASS = 'rgba(255,255,255,0.045)'
|
|
172
|
+
const TILE_GLASS_HOT = 'rgba(255,255,255,0.075)'
|
|
173
|
+
/** The least a tile takes, in cells: two side by side from about 84 cells, one a row below. */
|
|
174
|
+
const TILE_MIN = 40
|
|
175
|
+
|
|
176
|
+
/** Tiles side by side: two when the frame holds two at their least, else one a row. */
|
|
177
|
+
function tilesAcross(frame: Frame): 1 | 2 {
|
|
178
|
+
return frame.columns >= 2 * TILE_MIN + 6 ? 2 : 1
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Cells a tile's words and controls have beside its picture, about. */
|
|
182
|
+
function tileInner(frame: Frame): number {
|
|
183
|
+
const tile = tilesAcross(frame) === 2 ? Math.floor((frame.columns - 5) / 2) : frame.columns - 4
|
|
184
|
+
return Math.max(18, tile - 3 - Math.ceil(TILE_PX / CELL_PX) - 3)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
type Tile = { id: string; art: TileArt; title: string; head?: RenderNode; line: string; body: RenderNode[] }
|
|
188
|
+
|
|
189
|
+
function tile(el: Els, x: Tile): RenderNode {
|
|
190
|
+
const { Box, Text } = el
|
|
191
|
+
return (
|
|
192
|
+
<Box key={'tile-' + x.id} position="relative" flexDirection="row" alignItems="flex-start" columnGap={2} flexGrow={1} width={0} minWidth={TILE_MIN} paddingX={1.5} paddingY={1} borderStyle="round" borderColor={TILE_EDGE} backgroundColor={TILE_GLASS} hover={{ borderColor: TILE_EDGE_HOT, backgroundColor: TILE_GLASS_HOT }}>
|
|
193
|
+
{pic(el, 'tile-art-' + x.id, tileArt(x.art), x.title, TILE_PX, TILE_PX)}
|
|
194
|
+
<Box key={'tile-body-' + x.id} flexDirection="column" rowGap={1} flexGrow={1} flexShrink={1} minWidth={0}>
|
|
195
|
+
<Box flexDirection="row" alignItems="center" columnGap={1} width="100%" minWidth={0}>
|
|
196
|
+
<Box flexGrow={1} flexShrink={1} minWidth={0}>
|
|
197
|
+
<Text bold color={INK} wrap="wrap">{x.title}</Text>
|
|
198
|
+
</Box>
|
|
199
|
+
{x.head ?? null}
|
|
200
|
+
</Box>
|
|
201
|
+
<Text color={SUB} wrap="wrap">{x.line}</Text>
|
|
202
|
+
{x.body}
|
|
203
|
+
</Box>
|
|
204
|
+
</Box>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A quiet line under a control. */
|
|
209
|
+
function note(el: Els, key: string, text: string, color = MUTE): RenderNode {
|
|
210
|
+
const { Text } = el
|
|
211
|
+
return <Text key={key} color={color} wrap="wrap">{text}</Text>
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Automatic effort: its switch, and while on the lean between cheaper and smarter. */
|
|
215
|
+
export function effortTile(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
216
|
+
const { Box, Text } = el
|
|
217
|
+
const L = frame.lang
|
|
218
|
+
const d = view.settings
|
|
219
|
+
const body = d.effortAuto
|
|
220
|
+
? [
|
|
221
|
+
<Box key="lean" flexDirection="row" alignItems="center" columnGap={1} flexWrap="wrap">
|
|
222
|
+
<Text color={MUTE}>{t(L, 'settings.cheaper')}</Text>
|
|
223
|
+
{slider(el, 'bias', BIAS, d.bias, bias => a.patch({ bias }), 3)}
|
|
224
|
+
<Text color={MUTE}>{t(L, 'settings.smarter')}</Text>
|
|
225
|
+
</Box>,
|
|
226
|
+
note(el, 'lean-note', t(L, 'settings.leanHint')),
|
|
227
|
+
]
|
|
228
|
+
: [note(el, 'auto-off', t(L, 'settings.autoOffNote'))]
|
|
229
|
+
return tile(el, {
|
|
230
|
+
id: 'effort',
|
|
231
|
+
art: { kind: 'effort', on: d.effortAuto },
|
|
232
|
+
title: t(L, 'settings.effortAuto'),
|
|
233
|
+
head: toggle(el, 'set-auto', d.effortAuto, t(L, d.effortAuto ? 'band.on' : 'band.off'), () => a.patch({ effortAuto: !d.effortAuto })),
|
|
234
|
+
line: t(L, 'settings.autoLine'),
|
|
235
|
+
body,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Who judges: Haiku, Jev or a router model; Jev's key field when it needs one. */
|
|
240
|
+
export function judgeTile(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
241
|
+
const { Box, Text } = el
|
|
242
|
+
const L = frame.lang
|
|
243
|
+
const d = view.settings
|
|
244
|
+
const body: RenderNode[] = [
|
|
245
|
+
segmented(el, 'judge', [
|
|
246
|
+
{ value: 'haiku', label: t(L, 'settings.haiku') },
|
|
247
|
+
{ value: 'jev', label: t(L, 'settings.jev') },
|
|
248
|
+
{ value: 'model', label: t(L, 'settings.routerModel') },
|
|
249
|
+
] as const, d.judge, judge => a.patch({ judge }), 'teal'),
|
|
250
|
+
]
|
|
251
|
+
if (d.judge === 'model' && d.judgeModel) body.push(note(el, 'judge-model-name', t(L, 'settings.routerModel') + ' · ' + d.judgeModel))
|
|
252
|
+
if (needsKey(view)) {
|
|
253
|
+
const jev = view.jev
|
|
254
|
+
const placeholder = t(L, 'settings.keyPaste')
|
|
255
|
+
const field = 'Input' in el
|
|
256
|
+
? well(el, 'jev-key', tileInner(frame), <el.Input key="jev-key" placeholder={placeholder} value="" submitLabel={t(L, 'settings.saveKey')} onSubmit={(key: string) => a.jevKey(key)} />, placeholder)
|
|
257
|
+
: <Text color={MUTE}>{placeholder}</Text>
|
|
258
|
+
const result =
|
|
259
|
+
jev.test.state === 'testing' ? <Text color={SUB}>{t(L, 'settings.testing')}</Text>
|
|
260
|
+
: jev.test.state === 'ok' ? <Text color={TONE.green.text} wrap="wrap">{'✓ ' + (jev.test.message ?? t(L, 'settings.keyWorks'))}</Text>
|
|
261
|
+
: jev.test.state === 'fail' ? <Text color={TONE.red.text} wrap="wrap">{'✕ ' + (jev.test.message ?? t(L, 'settings.keyRejected'))}</Text>
|
|
262
|
+
: null
|
|
263
|
+
body.push(
|
|
264
|
+
field,
|
|
265
|
+
<Box key="jev-row" flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={1} rowGap={1}>
|
|
266
|
+
{pill(el, 'jev-test', t(L, 'settings.test'), 'ghost', a.testJev, 'teal')}
|
|
267
|
+
{result}
|
|
268
|
+
</Box>,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
return tile(el, { id: 'judge', art: { kind: 'judge', judge: d.judge }, title: t(L, 'settings.judge'), line: t(L, 'settings.judgeLine'), body })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The worker set that is on: Base or a named mode. */
|
|
275
|
+
export function modeTile(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
276
|
+
const L = frame.lang
|
|
277
|
+
const d = view.settings
|
|
278
|
+
const items = modeItems(frame, d)
|
|
279
|
+
const current = d.activeMode ?? ''
|
|
280
|
+
const control = items.length <= 5 || !('Select' in el)
|
|
281
|
+
? segmented(el, 'mode', items, current, id => a.setMode?.(id), 'amber')
|
|
282
|
+
: <el.Select key="mode" options={items} value={current} onSelect={(value: string) => a.setMode?.(value)} />
|
|
283
|
+
return tile(el, { id: 'mode', art: { kind: 'mode', on: current !== '' }, title: t(L, 'settings.mode'), line: t(L, 'settings.modeLine'), body: [control] })
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Keep warm: its switch, and what each ping costs. */
|
|
287
|
+
export function warmTile(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
288
|
+
const L = frame.lang
|
|
289
|
+
const d = view.settings
|
|
290
|
+
return tile(el, {
|
|
291
|
+
id: 'warm',
|
|
292
|
+
art: { kind: 'warm', on: d.keepWarm },
|
|
293
|
+
title: t(L, 'settings.keepWarm'),
|
|
294
|
+
head: toggle(el, 'set-warm', d.keepWarm, t(L, d.keepWarm ? 'band.on' : 'band.off'), () => a.patch({ keepWarm: !d.keepWarm })),
|
|
295
|
+
line: t(L, 'settings.warmLine'),
|
|
296
|
+
body: [note(el, 'warm-cost', t(L, 'settings.warmCostShort'))],
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** What 리플이 says the settings do right now, in a sentence or two. */
|
|
301
|
+
export function settingsSay(frame: Frame, d: Settings): string {
|
|
302
|
+
const L = frame.lang
|
|
303
|
+
const effort = d.effortAuto ? t(L, 'settings.sayAuto', { judge: judgeName(frame, d.judge) }) : t(L, 'settings.sayManual')
|
|
304
|
+
return d.keepWarm ? effort + ' ' + t(L, 'settings.sayWarm') : effort
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Rows the quick settings take on the desktop, about: for the sea's height. */
|
|
308
|
+
function panelRows(frame: Frame, view: SettingsView): number {
|
|
309
|
+
return (tilesAcross(frame) === 2 ? 20 : 36) + (needsKey(view) ? 4 : 0)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function settingsDrawn(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
313
|
+
const { Box, Text } = el
|
|
314
|
+
const L = frame.lang
|
|
315
|
+
const d = view.settings
|
|
316
|
+
const face = 48
|
|
317
|
+
const head = (
|
|
318
|
+
<Box key="settings-head" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
|
|
319
|
+
{ripple(el, 'settings', d.effortAuto ? 'calm' : 'drowsy', t(L, 'band.brand'), face)}
|
|
320
|
+
<Box key="settings-say" flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
321
|
+
<Box flexDirection="row" alignItems="center" columnGap={1} flexWrap="wrap">
|
|
322
|
+
<Text bold color={INK}>{t(L, 'settings.quick')}</Text>
|
|
323
|
+
<Text color={MUTE}>{t(L, 'band.brand')}</Text>
|
|
324
|
+
</Box>
|
|
325
|
+
<Text color={SUB} wrap="wrap">{settingsSay(frame, d)}</Text>
|
|
326
|
+
</Box>
|
|
327
|
+
<Box key="settings-exits" flexDirection="row" alignItems="center" columnGap={1} flexShrink={0}>
|
|
328
|
+
{pill(el, 'all-settings', t(L, 'settings.openAll'), 'ghost', a.openAllSettings, 'violet', 'open')}
|
|
329
|
+
{pill(el, 'close', t(L, 'settings.close'), 'ghost', a.close, 'violet', 'close')}
|
|
330
|
+
</Box>
|
|
331
|
+
</Box>
|
|
332
|
+
)
|
|
333
|
+
const tiles = (
|
|
334
|
+
<Box key="settings-tiles" flexDirection="row" flexWrap="wrap" columnGap={1} rowGap={1} width="100%" minWidth={0}>
|
|
335
|
+
{effortTile(el, frame, view, a)}
|
|
336
|
+
{judgeTile(el, frame, view, a)}
|
|
337
|
+
{modeTile(el, frame, view, a)}
|
|
338
|
+
{warmTile(el, frame, view, a)}
|
|
339
|
+
</Box>
|
|
340
|
+
)
|
|
341
|
+
return capsule(el, frame, {
|
|
342
|
+
key: 'settings',
|
|
343
|
+
// 리플이 floats on the surface: the wave runs just under the head, the tiles below it.
|
|
344
|
+
sea: { tone: 'violet', ripples: 'calm', reach: 170, motes: true, wave: 19 * 0.5 + face + 9, at: { x: 1.5 * CELL_PX + face / 2, y: 19 * 0.5 + face / 2 } },
|
|
345
|
+
rows: panelRows(frame, view),
|
|
346
|
+
paddingY: 1,
|
|
347
|
+
gap: 1,
|
|
348
|
+
children: [head, tiles],
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- The panel ---------------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
/** Session-time controls only; the full settings live on the router dashboard. */
|
|
355
|
+
export function renderSettings(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
356
|
+
if (canDraw(el, frame)) return settingsDrawn(el, frame, view, a)
|
|
357
|
+
const { Box } = el
|
|
358
|
+
const rich = isRich(frame)
|
|
359
|
+
const close = dismiss(el, frame, 'close', t(frame.lang, 'settings.close'), a.close, 'x')
|
|
360
|
+
return speak(el, frame, {
|
|
361
|
+
key: 'settings',
|
|
362
|
+
face: 'calm',
|
|
363
|
+
title: t(frame.lang, 'band.brand'),
|
|
364
|
+
aside: t(frame.lang, 'settings.quick'),
|
|
365
|
+
blocks: [
|
|
366
|
+
<Box key="controls" flexDirection="column" rowGap={rich ? 1 : 0}>
|
|
367
|
+
{effortRows(el, frame, view.settings, a)}
|
|
368
|
+
{judgeRows(el, frame, view, a)}
|
|
369
|
+
{modeRows(el, frame, view.settings, a)}
|
|
370
|
+
{keepWarmRow(el, frame, view.settings, a)}
|
|
371
|
+
</Box>,
|
|
372
|
+
actions(el, frame, [action(el, frame, 'all-settings', t(frame.lang, 'settings.openAll'), a.openAllSettings, { hotkey: 's', primary: true }), rich ? '' : close]),
|
|
373
|
+
],
|
|
374
|
+
...(rich ? { dismiss: close } : {}),
|
|
375
|
+
})
|
|
376
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// First run: three short steps (who judges, workers on the dashboard, keep warm) and a
|
|
2
|
+
// done card. Shown once, in the band; detailed settings live on the router dashboard.
|
|
3
|
+
//
|
|
4
|
+
// 리플이 asks each step as a card asks: the step's question is the title, the steps beside it, one
|
|
5
|
+
// line of why, the step's control, then Next and the quiet ways back or out. It asks who should
|
|
6
|
+
// judge, waits calmly, and is pleased when all is set. The terminal draws 리플이's column (parts.tsx
|
|
7
|
+
// `speak`); the desktop a capsule of the deep, its controls the quick settings' own tiles.
|
|
8
|
+
|
|
9
|
+
import { t } from '../i18n'
|
|
10
|
+
import type { RenderNode } from 'claude-code'
|
|
11
|
+
import { RUNGS } from '../settings'
|
|
12
|
+
|
|
13
|
+
import { CELL_PX, INK, MUTE, SUB, TONE, beadsArt } from './art'
|
|
14
|
+
import { capsule, pic, pill, ripple } from './kit'
|
|
15
|
+
import { GREET_SIZE, action, actions, canDraw, speak } from './parts'
|
|
16
|
+
import type { Els } from './parts'
|
|
17
|
+
import { judgeRows, judgeTile, keepWarmRow, row, warmTile } from './settings'
|
|
18
|
+
import { chOf } from './theme'
|
|
19
|
+
import type { Frame, SettingsActions, SettingsView, SetupActions, SetupView } from './view'
|
|
20
|
+
import { targetLabel } from './view'
|
|
21
|
+
|
|
22
|
+
const STEPS = [
|
|
23
|
+
{ title: 'setup.judgeTitle', blurb: 'setup.judgeBlurb' },
|
|
24
|
+
{ title: 'setup.workersTitle', blurb: 'setup.workersNote' },
|
|
25
|
+
{ title: 'setup.cacheTitle', blurb: 'setup.cacheBlurb' },
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
/** `Sol · Astra · …`: the four workers' models, lowest first. */
|
|
29
|
+
function workersSummary(lang: Frame['lang'], d: SetupView['draft']): string {
|
|
30
|
+
const general = d.rules.general
|
|
31
|
+
return RUNGS.map(rung => general.rungs[rung]?.map(step => targetLabel(step.target, lang)).join(' → ') || '—').join(' · ')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The steps as rings, the current one filled: ● ○ ○. */
|
|
35
|
+
function dots(step: number): string {
|
|
36
|
+
return [1, 2, 3].map(i => (i <= step ? '●' : '○')).join(' ')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The first-run setup in the band. */
|
|
40
|
+
export function renderSetup(el: Els, frame: Frame, view: SetupView, a: SetupActions): RenderNode {
|
|
41
|
+
const { Text } = el
|
|
42
|
+
// Setup lends the same small controls its draft and handlers.
|
|
43
|
+
const asSettings: SettingsView = { settings: view.draft, jev: view.jev }
|
|
44
|
+
const asActions: SettingsActions = { ...a, close: () => undefined }
|
|
45
|
+
const d = view.draft
|
|
46
|
+
const done = view.step === 4
|
|
47
|
+
const step = done ? null : STEPS[view.step - 1]!
|
|
48
|
+
const body: RenderNode[] = done
|
|
49
|
+
? [
|
|
50
|
+
row(el, frame, 'sum-effort', t(frame.lang, 'band.effort'), <Text wrap="wrap">{t(frame.lang, 'setup.effortSummary', { state: t(frame.lang, d.effortAuto ? 'band.on' : 'band.off').toLowerCase(), judge: d.judge === 'haiku' ? t(frame.lang, 'settings.haiku') : d.judge === 'jev' ? t(frame.lang, 'settings.jev') : d.judgeModel })}</Text>),
|
|
51
|
+
row(el, frame, 'sum-workers', t(frame.lang, 'settings.workers'), <Text wrap="wrap">{workersSummary(frame.lang, d)}</Text>),
|
|
52
|
+
row(el, frame, 'sum-cache', t(frame.lang, 'band.cache'), <Text wrap="wrap">{d.keepWarm ? t(frame.lang, 'setup.cacheSummary', { hours: d.keepWarmHours }) : t(frame.lang, 'setup.cacheOff')}</Text>),
|
|
53
|
+
]
|
|
54
|
+
: view.step === 1
|
|
55
|
+
? judgeRows(el, frame, asSettings, asActions)
|
|
56
|
+
: view.step === 2
|
|
57
|
+
? [] // Its line says it all: workers start on Auto, the rest is on the dashboard.
|
|
58
|
+
: [keepWarmRow(el, frame, view.draft, asActions)]
|
|
59
|
+
const { Box } = el
|
|
60
|
+
const buttons = done
|
|
61
|
+
? [action(el, frame, 'finish', t(frame.lang, 'setup.start'), a.finish, { hotkey: 'g', primary: true }), action(el, frame, 'back', t(frame.lang, 'setup.back'), a.back, { hotkey: 'b' })]
|
|
62
|
+
: [
|
|
63
|
+
action(el, frame, 'next', view.step === 3 ? t(frame.lang, 'setup.finish') : t(frame.lang, 'setup.next'), a.next, { hotkey: 'n', primary: true }),
|
|
64
|
+
view.step === 2 ? action(el, frame, 'all-settings', t(frame.lang, 'settings.openAll'), a.openAllSettings, { hotkey: 's' }) : '',
|
|
65
|
+
view.step > 1 ? action(el, frame, 'back', t(frame.lang, 'setup.back'), a.back, { hotkey: 'b' }) : '',
|
|
66
|
+
action(el, frame, 'skip', t(frame.lang, 'setup.skip'), a.skip, { hotkey: 'k' }),
|
|
67
|
+
]
|
|
68
|
+
const face = done ? 'pleased' : view.step === 1 ? 'asking' : 'calm'
|
|
69
|
+
// It greets first; the first step's question is its one row (who judges).
|
|
70
|
+
const title = done ? t(frame.lang, 'setup.ready') : view.step === 1 ? t(frame.lang, 'setup.welcome') : t(frame.lang, step!.title)
|
|
71
|
+
const line = done ? t(frame.lang, 'setup.changeDashboard') : t(frame.lang, step!.blurb)
|
|
72
|
+
if (canDraw(el, frame)) {
|
|
73
|
+
// The desktop: a capsule of the deep, the step's control the quick settings' own tile (who
|
|
74
|
+
// judges, keep warm), what was chosen when done, Next lit and the rest ghost buttons.
|
|
75
|
+
const L = frame.lang
|
|
76
|
+
const beads = beadsArt(view.step, 3)
|
|
77
|
+
const summary = done
|
|
78
|
+
? [
|
|
79
|
+
[t(L, 'band.effort'), t(L, 'setup.effortSummary', { state: t(L, d.effortAuto ? 'band.on' : 'band.off').toLowerCase(), judge: d.judge === 'haiku' ? t(L, 'settings.haiku') : d.judge === 'jev' ? t(L, 'settings.jev') : d.judgeModel })],
|
|
80
|
+
[t(L, 'settings.workers'), workersSummary(L, d)],
|
|
81
|
+
[t(L, 'band.cache'), d.keepWarm ? t(L, 'setup.cacheSummary', { hours: d.keepWarmHours }) : t(L, 'setup.cacheOff')],
|
|
82
|
+
]
|
|
83
|
+
: []
|
|
84
|
+
const nameCells = Math.ceil(Math.max(0, ...summary.map(([name]) => chOf(name!)))) + 1
|
|
85
|
+
const content = done
|
|
86
|
+
? [
|
|
87
|
+
<Box key="summary" flexDirection="column" rowGap={1} paddingLeft={Math.ceil(GREET_SIZE / CELL_PX) + 2}>
|
|
88
|
+
{summary.map(([name, value], i) => (
|
|
89
|
+
<Box key={'sum-' + i} flexDirection="row" columnGap={2} alignItems="flex-start">
|
|
90
|
+
<Box width={nameCells} flexShrink={0}><Text color={MUTE}>{name}</Text></Box>
|
|
91
|
+
<Text color={INK} wrap="wrap">{value}</Text>
|
|
92
|
+
</Box>
|
|
93
|
+
))}
|
|
94
|
+
</Box>,
|
|
95
|
+
]
|
|
96
|
+
: view.step === 1 || view.step === 3
|
|
97
|
+
? [
|
|
98
|
+
<Box key="step-tile" flexDirection="row" width="100%" minWidth={0}>
|
|
99
|
+
{view.step === 1 ? judgeTile(el, frame, asSettings, asActions) : warmTile(el, frame, asSettings, asActions)}
|
|
100
|
+
</Box>,
|
|
101
|
+
]
|
|
102
|
+
: []
|
|
103
|
+
const next = done ? pill(el, 'finish', t(L, 'setup.start'), 'primary', a.finish, 'green', 'next') : pill(el, 'next', view.step === 3 ? t(L, 'setup.finish') : t(L, 'setup.next'), 'primary', a.next, 'violet', 'next')
|
|
104
|
+
return capsule(el, frame, {
|
|
105
|
+
key: 'setup',
|
|
106
|
+
sea: { tone: done ? 'green' : 'violet', ripples: 'calm', reach: 170, motes: true, wave: 19 * 0.5 + GREET_SIZE + 9, ...(done ? { tick: true, weather: 'sparkles' as const } : {}), at: { x: 1.5 * CELL_PX + GREET_SIZE / 2, y: 19 * 0.5 + GREET_SIZE / 2 } },
|
|
107
|
+
rows: 8 + (done ? 6 : view.step === 1 ? 12 : view.step === 3 ? 8 : 0) + (frame.columns < 70 ? 4 : 0),
|
|
108
|
+
paddingY: 1,
|
|
109
|
+
gap: 1,
|
|
110
|
+
children: [
|
|
111
|
+
<Box key="setup-head" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
|
|
112
|
+
{ripple(el, 'setup', face, title, GREET_SIZE)}
|
|
113
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
114
|
+
<Box flexDirection="row" alignItems="center" columnGap={2} flexWrap="wrap">
|
|
115
|
+
<Text bold color={done ? TONE.green.text : INK} wrap="wrap">{title}</Text>
|
|
116
|
+
{done ? null : pic(el, 'setup-beads', beads.source, view.step + ' / 3', beads.width, 16)}
|
|
117
|
+
</Box>
|
|
118
|
+
<Text color={SUB} wrap="wrap">{line}</Text>
|
|
119
|
+
</Box>
|
|
120
|
+
</Box>,
|
|
121
|
+
...content,
|
|
122
|
+
<Box key="actions" flexDirection="row" alignItems="center" columnGap={1} flexWrap="wrap" rowGap={1}>
|
|
123
|
+
{next}
|
|
124
|
+
{!done && view.step === 2 ? pill(el, 'all-settings', t(L, 'settings.openAll'), 'ghost', a.openAllSettings, 'violet', 'open') : null}
|
|
125
|
+
{done || view.step > 1 ? pill(el, 'back', t(L, 'setup.back'), 'ghost', a.back, 'violet', 'back') : null}
|
|
126
|
+
{done ? null : pill(el, 'skip', t(L, 'setup.skip'), 'ghost', a.skip)}
|
|
127
|
+
</Box>,
|
|
128
|
+
],
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
return speak(el, frame, {
|
|
132
|
+
key: 'setup',
|
|
133
|
+
face,
|
|
134
|
+
// It greets here, the first thing a new session shows: at its greeting size.
|
|
135
|
+
size: GREET_SIZE,
|
|
136
|
+
title,
|
|
137
|
+
aside: done ? '' : dots(view.step),
|
|
138
|
+
line,
|
|
139
|
+
blocks: [
|
|
140
|
+
body.length ? (
|
|
141
|
+
<Box key="body" flexDirection="column">
|
|
142
|
+
{body}
|
|
143
|
+
</Box>
|
|
144
|
+
) : '',
|
|
145
|
+
actions(el, frame, buttons),
|
|
146
|
+
],
|
|
147
|
+
})
|
|
148
|
+
}
|