clauderipple 0.9.4 → 0.10.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +31 -0
  3. package/dist/app/package.json +1 -1
  4. package/dist/cli/src/hooks/agent-title.js +7 -4
  5. package/dist/cli/src/index.js +43 -0
  6. package/dist/cli/src/remote.js +263 -0
  7. package/dist/cli/src/schtasks.js +48 -40
  8. package/dist/cli/src/settings.js +2 -1
  9. package/dist/mod/hooks/i18n.ts +252 -6
  10. package/dist/mod/hooks/judge.ts +35 -17
  11. package/dist/mod/hooks/modes.ts +56 -1
  12. package/dist/mod/hooks/register.tsx +238 -32
  13. package/dist/mod/hooks/session.ts +98 -1
  14. package/dist/mod/hooks/settings.js +7 -3
  15. package/dist/mod/hooks/settings.ts +9 -4
  16. package/dist/mod/hooks/stats.ts +25 -1
  17. package/dist/mod/hooks/ui/agents.tsx +858 -79
  18. package/dist/mod/hooks/ui/alerts.tsx +20 -17
  19. package/dist/mod/hooks/ui/art.ts +33 -7
  20. package/dist/mod/hooks/ui/band.tsx +262 -81
  21. package/dist/mod/hooks/ui/flat.ts +265 -0
  22. package/dist/mod/hooks/ui/kit.tsx +70 -55
  23. package/dist/mod/hooks/ui/look.ts +129 -0
  24. package/dist/mod/hooks/ui/settings.tsx +71 -46
  25. package/dist/mod/hooks/ui/setup.tsx +15 -13
  26. package/dist/mod/hooks/ui/theme.ts +3 -0
  27. package/dist/mod/hooks/ui/tools.ts +124 -0
  28. package/dist/mod/hooks/ui/view.ts +116 -3
  29. package/dist/mod/hooks/workers.ts +33 -6
  30. package/dist/mod/tests/register.test.tsx +155 -11
  31. package/dist/mod/tests/ui.test.tsx +375 -27
  32. package/dist/mod/types/index.d.ts +68 -2
  33. package/dist/remote/package.json +28 -0
  34. package/dist/remote/src/away.js +85 -0
  35. package/dist/remote/src/bridge-client.js +295 -0
  36. package/dist/remote/src/composer-menu.js +11 -0
  37. package/dist/remote/src/config.js +102 -0
  38. package/dist/remote/src/desktop.js +1018 -0
  39. package/dist/remote/src/e2ee.js +126 -0
  40. package/dist/remote/src/e2ee.ts +126 -0
  41. package/dist/remote/src/eventlog.js +298 -0
  42. package/dist/remote/src/expo-push.js +127 -0
  43. package/dist/remote/src/folders.js +79 -0
  44. package/dist/remote/src/fsutil.js +39 -0
  45. package/dist/remote/src/handoff.js +38 -0
  46. package/dist/remote/src/host-service.js +168 -0
  47. package/dist/remote/src/images.js +215 -0
  48. package/dist/remote/src/local-control.js +138 -0
  49. package/dist/remote/src/main.js +264 -0
  50. package/dist/remote/src/permissions.js +124 -0
  51. package/dist/remote/src/power.js +76 -0
  52. package/dist/remote/src/processes.js +24 -0
  53. package/dist/remote/src/protocol.js +32 -0
  54. package/dist/remote/src/protocol.ts +284 -0
  55. package/dist/remote/src/push-content.js +32 -0
  56. package/dist/remote/src/push.js +134 -0
  57. package/dist/remote/src/receipts.js +111 -0
  58. package/dist/remote/src/relay.js +438 -0
  59. package/dist/remote/src/router.js +208 -0
  60. package/dist/remote/src/sdk-launcher.js +18 -0
  61. package/dist/remote/src/server.js +706 -0
  62. package/dist/remote/src/service.js +157 -0
  63. package/dist/remote/src/sessions.js +1800 -0
  64. package/dist/remote/src/sidebar-storage.js +211 -0
  65. package/dist/remote/src/sidebar.js +173 -0
  66. package/dist/remote/src/tool-details.js +205 -0
  67. package/dist/remote/src/tool-details.ts +153 -0
  68. package/dist/remote/src/worktree.js +99 -0
  69. package/dist/remote/web/app.ts +746 -0
  70. package/dist/remote/web/conn.ts +193 -0
  71. package/dist/remote/web/demo.ts +134 -0
  72. package/dist/remote/web/dom.ts +124 -0
  73. package/dist/remote/web/esc.ts +4 -0
  74. package/dist/remote/web/i18n.ts +374 -0
  75. package/dist/remote/web/icon.svg +1 -0
  76. package/dist/remote/web/index.html +24 -0
  77. package/dist/remote/web/manifest.webmanifest +10 -0
  78. package/dist/remote/web/mascot.ts +60 -0
  79. package/dist/remote/web/md.ts +78 -0
  80. package/dist/remote/web/notify.ts +55 -0
  81. package/dist/remote/web/pushstate.ts +27 -0
  82. package/dist/remote/web/secure.ts +109 -0
  83. package/dist/remote/web/sidebar.ts +106 -0
  84. package/dist/remote/web/style.css +311 -0
  85. package/dist/remote/web/sw.ts +48 -0
  86. package/dist/remote/web/timeline.ts +224 -0
  87. package/dist/remote/web/views.ts +323 -0
  88. package/dist/router/src/admin.js +227 -4
  89. package/dist/router/src/config.js +30 -0
  90. package/dist/router/src/index.js +39 -0
  91. package/dist/router/src/ingress/translate.js +6 -2
  92. package/dist/router/src/phone.js +82 -0
  93. package/dist/router/src/presets.js +3 -0
  94. package/dist/router/src/providers/anthropic-usage.js +33 -1
  95. package/dist/router/src/providers/chatgpt/index.js +41 -11
  96. package/dist/router/src/providers/chatgpt/translate.js +147 -7
  97. package/dist/router/src/providers/google/index.js +26 -0
  98. package/dist/router/src/providers/google/prompt-refusal.js +29 -0
  99. package/dist/router/src/providers/google/usage.js +257 -0
  100. package/dist/router/src/providers/grok/index.js +18 -0
  101. package/dist/router/src/providers/grok/usage.js +77 -0
  102. package/dist/router/src/providers/openai/index.js +29 -5
  103. package/dist/router/src/providers/openai/translate.js +260 -27
  104. package/dist/router/src/providers/opencode-usage.js +116 -0
  105. package/dist/router/src/proxy.js +339 -258
  106. package/dist/router/src/reasoning.js +157 -0
  107. package/dist/router/src/remote-host.js +320 -0
  108. package/dist/router/src/remote-tunnel.js +320 -0
  109. package/dist/router/src/requestlog.js +48 -39
  110. package/dist/router/src/thinking.js +20 -9
  111. package/dist/router/src/usage-history.js +227 -0
  112. package/dist/router/src/version.js +1 -1
  113. package/dist/ui/app.js +436 -59
  114. package/dist/ui/i18n.js +184 -0
  115. package/dist/ui/index.html +24 -5
  116. package/dist/ui/style.css +39 -0
  117. package/docs/ARCHITECTURE.md +164 -12
  118. package/package.json +7 -1
@@ -12,7 +12,8 @@ import { t } from '../i18n'
12
12
  import type { RenderNode } from 'claude-code'
13
13
 
14
14
  import type { HandoffKind, Settings } from '../../types'
15
- import { CELL_PX, MUTE, ROW_PX, SUB, TONE, pillArt } from './art'
15
+ import { CELL_PX, ROW_PX } from './art'
16
+ import { lookOf } from './look'
16
17
  import type { Sea, Tone } from './art'
17
18
  import { capsule, closeButton, pill, ripple, segmented as drawnSegmented, well } from './kit'
18
19
  import type { SegmentOption } from './kit'
@@ -26,7 +27,7 @@ import { compactNumber, isRich, limitName, resetsLabel } from './view'
26
27
 
27
28
  type Choice = { key: string; label: string; hotkey: string; onPress: () => void }
28
29
 
29
- type Card = {
30
+ export type Card = {
30
31
  key: string
31
32
  /** 리플이's face; on the terminal its tone colours the title's mark. */
32
33
  face: Face
@@ -44,7 +45,7 @@ type Card = {
44
45
 
45
46
  const JUDGE_NAME = { haiku: 'settings.haiku', jev: 'settings.jev', model: 'alerts.routerModel' } as const
46
47
 
47
- function cardOf(frame: Frame, alert: AlertView, a: AlertActions): Card {
48
+ export function cardOf(frame: Frame, alert: AlertView, a: AlertActions): Card {
48
49
  const L = frame.lang
49
50
  const notNow: Choice = { key: 'dismiss', label: t(L, 'alerts.notNow'), hotkey: 'n', onPress: a.dismiss }
50
51
  const cancel: Choice = { key: 'dismiss', label: t(L, 'alerts.cancel'), hotkey: 'x', onPress: a.dismiss }
@@ -249,6 +250,7 @@ const CARD_FACE = 34
249
250
 
250
251
  function drawnExtras(el: Els, frame: Frame, alert: AlertView, a: AlertActions, tone: Tone): RenderNode[] {
251
252
  const { Box, Text } = el
253
+ const k = lookOf(frame)
252
254
  if (alert.kind === 'compactNote' && 'Input' in el) {
253
255
  const Field = el.Input
254
256
  const name = t(frame.lang, 'alerts.keep').trim()
@@ -257,8 +259,8 @@ function drawnExtras(el: Els, frame: Frame, alert: AlertView, a: AlertActions, t
257
259
  const cells = Math.max(20, frame.columns - Math.ceil(CARD_FACE / CELL_PX) - Math.ceil(chOf(name)) - 10)
258
260
  return [
259
261
  <Box key="note-row" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
260
- <Text color={SUB}>{name}</Text>
261
- {well(el, 'note', cells, (
262
+ <Text {...k.sub}>{name}</Text>
263
+ {well(el, frame, 'note', cells, (
262
264
  <Field
263
265
  key="note"
264
266
  placeholder={placeholder}
@@ -287,29 +289,30 @@ function drawnExtras(el: Els, frame: Frame, alert: AlertView, a: AlertActions, t
287
289
  const line = (key: string, name: string, control: RenderNode) => (
288
290
  <Box key={key} flexDirection="row" alignItems="center" columnGap={2}>
289
291
  <Box width={nameCells} flexShrink={0}>
290
- <Text color={MUTE}>{name}</Text>
292
+ <Text {...k.mute}>{name}</Text>
291
293
  </Box>
292
294
  {control}
293
295
  </Box>
294
296
  )
295
297
  return [
296
298
  <Box key="choices" flexDirection="column" rowGap={1}>
297
- {line('kind', names[0]!, drawnSegmented(el, 'kind', kinds, alert.handoffKind, k => a.setHandoffKind(k), tone))}
298
- {line('after', names[1]!, drawnSegmented(el, 'after', afters, alert.after, k => a.setHandoffAfter(k), tone))}
299
+ {line('kind', names[0]!, drawnSegmented(el, frame, 'kind', kinds, alert.handoffKind, kind => a.setHandoffKind(kind), tone))}
300
+ {line('after', names[1]!, drawnSegmented(el, frame, 'after', afters, alert.after, after => a.setHandoffAfter(after), tone))}
299
301
  <Box paddingLeft={nameCells + 2}>
300
- <Text color={MUTE} wrap="wrap">{afterHint(frame, alert.after)}</Text>
302
+ <Text {...k.mute} wrap="wrap">{afterHint(frame, alert.after)}</Text>
301
303
  </Box>
302
304
  </Box>,
303
305
  ]
304
306
  }
305
307
 
306
308
  /** What the chosen "then" does, in a line: the names alone were read the wrong way (2026-10-13). */
307
- function afterHint(frame: Frame, after: Settings['handoffAfter']): string {
309
+ export function afterHint(frame: Frame, after: Settings['handoffAfter']): string {
308
310
  return t(frame.lang, after === 'continue' ? 'alerts.continueHint' : after === 'confirm' ? 'alerts.confirmHint' : 'alerts.copyHint')
309
311
  }
310
312
 
311
313
  function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): RenderNode {
312
314
  const { Box, Text } = el
315
+ const k = lookOf(frame)
313
316
  const card = cardOf(frame, alert, a)
314
317
  const sea = seaOf(card, alert)
315
318
  const tone = sea.tone
@@ -319,7 +322,7 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
319
322
  const main = card.main && !fieldSubmits ? card.main : undefined
320
323
  const more = drawnExtras(el, frame, alert, a, tone)
321
324
  // The actions' cells, as drawn (each pill its own whole cells, a cell between, the way out last).
322
- const actionCells = quiet.reduce((n, c) => n + pillArt(c.label, 'ghost', tone).cells + 1, 0) + (main ? pillArt(main.label, 'primary', tone).cells + 1 : 0) + (card.close ? 4 : 0)
325
+ const actionCells = quiet.reduce((n, c) => n + k.art.pill(c.label, 'ghost', tone).cells + 1, 0) + (main ? k.art.pill(main.label, 'primary', tone).cells + 1 : 0) + (card.close ? 4 : 0)
323
326
  const faceCells = Math.ceil(CARD_FACE / CELL_PX) + 2
324
327
  const room = frame.columns - 4
325
328
  const oneRow = !more.length && faceCells + chOf(card.title) * 1.12 + 2 + chOf(card.body) + 2 + actionCells <= room
@@ -330,14 +333,14 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
330
333
  // where a way out is looked for.
331
334
  const buttons = (
332
335
  <Box key="actions" flexDirection="row" alignItems="center" alignSelf={more.length ? 'flex-start' : 'center'} columnGap={1} flexShrink={0} flexWrap="wrap" rowGap={1}>
333
- {quiet.map(c => pill(el, c.key, c.label, 'ghost', c.onPress, tone))}
334
- {main ? pill(el, main.key, main.label, 'primary', main.onPress, tone) : null}
335
- {card.close ? closeButton(el, card.close.key, card.close.label, card.close.onPress) : null}
336
+ {quiet.map(c => pill(el, frame, c.key, c.label, 'ghost', c.onPress, tone))}
337
+ {main ? pill(el, frame, main.key, main.label, 'primary', main.onPress, tone) : null}
338
+ {card.close ? closeButton(el, frame, card.close.key, card.close.label, card.close.onPress) : null}
336
339
  </Box>
337
340
  )
338
- const face = ripple(el, card.key, card.face, card.title, CARD_FACE, card.percent)
339
- const title = <Text key="title" bold color={TONE[tone].text} wrap="wrap">{card.title}</Text>
340
- const body = <Text key="body" color={SUB} wrap="wrap">{card.body}</Text>
341
+ const face = ripple(el, frame, card.key, card.face, card.title, CARD_FACE, card.percent)
342
+ const title = <Text key="title" bold color={k.tone(tone)} wrap="wrap">{card.title}</Text>
343
+ const body = <Text key="body" {...k.sub} wrap="wrap">{card.body}</Text>
341
344
  const top = oneRow ? (
342
345
  <Box key="top" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
343
346
  {face}
@@ -13,7 +13,8 @@
13
13
  // - The desktop sets text at 13 px with 19 px lines; a cell is 1ch, 7.917 px. Controls are sized in
14
14
  // whole cells so a picture and the presses laid over it share one grid.
15
15
 
16
- import { chOf } from './theme'
16
+ import type { Skin } from '../../types/index.d.ts'
17
+ import { chOf } from './theme.ts'
17
18
 
18
19
  /** One desktop cell (1ch of the host's 13 px sans), px. */
19
20
  export const CELL_PX = 7.917
@@ -58,7 +59,7 @@ export const DEPTH = ['#67e8f9', '#7dd3fc', '#a5b4fc', '#a78bfa', '#c084fc'] as
58
59
 
59
60
  const FONT = "-apple-system,BlinkMacSystemFont,'SF Pro Text','Apple SD Gothic Neo','Segoe UI',system-ui,sans-serif"
60
61
 
61
- const r = (x: number): string => String(Math.round(x * 100) / 100)
62
+ export const r = (x: number): string => String(Math.round(x * 100) / 100)
62
63
 
63
64
  /** One SVG document, `width` × `height` px; nothing in it moves when less motion is asked for. */
64
65
  export function svg(width: number, height: number, body: string, css = '', extra = ''): string {
@@ -66,7 +67,7 @@ export function svg(width: number, height: number, body: string, css = '', extra
66
67
  return '<svg xmlns="http://www.w3.org/2000/svg" width="' + r(width) + '" height="' + r(height) + '" viewBox="0 0 ' + r(width) + ' ' + r(height) + '"' + extra + '><style>' + base + css + '</style>' + body + '</svg>'
67
68
  }
68
69
 
69
- function esc(text: string): string {
70
+ export function esc(text: string): string {
70
71
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
71
72
  }
72
73
 
@@ -320,7 +321,7 @@ export type PillLook = 'primary' | 'ghost'
320
321
  /** Glyphs a button may lead with, drawn on a 12 px square. */
321
322
  export type Glyph = 'open' | 'close' | 'gear' | 'back' | 'next' | 'copy' | 'save' | 'compact' | 'handoff'
322
323
 
323
- const GLYPH: Record<Glyph, string> = {
324
+ export const GLYPH: Record<Glyph, string> = {
324
325
  open: '<path d="M7 2.2h2.8V5M9.8 2.2L5.6 6.4M8.8 7.4v2.4H2.2V3.2h2.4"/>',
325
326
  close: '<path d="M3 3l6 6M9 3L3 9"/>',
326
327
  gear: '<circle cx="6" cy="6" r="1.7"/><path d="M6 1.3v1.3M6 9.4v1.3M1.3 6h1.3M9.4 6h1.3M2.7 2.7l.9.9M8.4 8.4l.9.9M2.7 9.3l.9-.9M8.4 3.6l.9-.9"/><circle cx="6" cy="6" r="3.5"/>',
@@ -555,14 +556,30 @@ export function gaugeArt(kind: string): string {
555
556
  return svg(16, 16, arc + '<path d="' + needle + '" fill="none" stroke="' + stroke + '" stroke-width="1.4" stroke-linecap="round"/><circle cx="8" cy="11.4" r="1.2" fill="' + stroke + '"/>')
556
557
  }
557
558
 
559
+ /** A limit window's share as a small horizontal bar, 36 × 6 px with round ends: the track white at
560
+ * low strength, the fill the ring's own teal-to-violet while all is well, amber, then red. Whole
561
+ * percents only, like the ring: the same figure is the same picture. */
562
+ export function barArt(percent: number, tone: Tone | 'ink'): string {
563
+ const W = 36
564
+ const H = 6
565
+ // Rounded to the integer: neighbouring readings draw byte-identical pictures.
566
+ const share = Math.max(0, Math.min(1, Math.round(percent) / 100))
567
+ const fill = tone === 'ink' ? 'url(#g)' : TONE[tone].hi
568
+ return svg(W, H,
569
+ '<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#67e8f9"/><stop offset="1" stop-color="#a78bfa"/></linearGradient>' +
570
+ '<clipPath id="c"><rect x="0" y="0" width="' + W + '" height="' + H + '" rx="3"/></clipPath></defs>' +
571
+ '<rect x="0" y="0" width="' + W + '" height="' + H + '" rx="3" fill="#fff" fill-opacity=".14"/>' +
572
+ '<g clip-path="url(#c)"><rect x="0" y="0" width="' + r(W * share) + '" height="' + H + '" fill="' + fill + '"/></g>')
573
+ }
574
+
558
575
  export const ICON_PX = 16
559
576
 
560
577
  /** Line icons of the band's controls, 16 px. */
561
578
  export type Icon = 'flame' | 'gear' | 'agents' | 'log' | 'compact' | 'clear'
562
579
 
563
- const FLAME = 'M8.1 14.3c2.8 0 4.5-1.9 4.5-4.4 0-2.8-2.4-4.2-2.8-7.2-1.7 1.1-2.6 2.8-2.5 4.3-1-.4-1.5-1.4-1.6-2.3-1.4 1.3-2 2.9-2 5.2 0 2.5 1.6 4.4 4.4 4.4z'
580
+ export const FLAME = 'M8.1 14.3c2.8 0 4.5-1.9 4.5-4.4 0-2.8-2.4-4.2-2.8-7.2-1.7 1.1-2.6 2.8-2.5 4.3-1-.4-1.5-1.4-1.6-2.3-1.4 1.3-2 2.9-2 5.2 0 2.5 1.6 4.4 4.4 4.4z'
564
581
 
565
- const ICON: Record<Icon, string> = {
582
+ export const ICON: Record<Icon, string> = {
566
583
  flame: '<path d="' + FLAME + '"/>',
567
584
  gear: '<circle cx="8" cy="8" r="2.3"/><path d="M8 1.7v1.8M8 12.5v1.8M1.7 8h1.8M12.5 8h1.8M3.5 3.5l1.3 1.3M11.2 11.2l1.3 1.3M3.5 12.5l1.3-1.3M11.2 4.8l1.3-1.3"/><circle cx="8" cy="8" r="4.8"/>',
568
585
  agents: '<circle cx="4.8" cy="5" r="2.1"/><circle cx="11.4" cy="5" r="2.1"/><path d="M1.6 13.4c.4-2.4 1.7-3.6 3.2-3.6s2.8 1.2 3.2 3.6M8.2 13.4c.4-2.4 1.7-3.6 3.2-3.6s2.8 1.2 3.2 3.6"/>',
@@ -617,11 +634,12 @@ export type TileArt =
617
634
  | { kind: 'judge'; judge: 'haiku' | 'jev' | 'model' }
618
635
  | { kind: 'mode'; on: boolean }
619
636
  | { kind: 'warm'; on: boolean }
637
+ | { kind: 'skin'; skin: Skin }
620
638
 
621
639
  /**
622
640
  * A tile's picture, 44 px: a rounded square of its own colour with the setting drawn as it stands
623
641
  * (rising bars sounding by themselves for automatic effort, the judge's letter, a stack of worker
624
- * cards, a flame or a snowflake for keep-warm), alive while it is on.
642
+ * cards, a flame or a snowflake for keep-warm, a small panel for the skin), alive while it is on.
625
643
  */
626
644
  export function tileArt(art: TileArt): string {
627
645
  const s = TILE_PX
@@ -662,6 +680,14 @@ export function tileArt(art: TileArt): string {
662
680
  return svg(s, s, square('#4a3410', '#24180a', '#fbbf24') + card(28, 0.18) + card(21.5, 0.32) + '<g class="fl">' + card(15, 0.95) + '<circle cx="15.5" cy="19.5" r="1.4" fill="#3a2604"/><rect x="19" y="18.6" width="10" height="1.8" rx=".9" fill="#3a2604" fill-opacity=".7"/></g>',
663
681
  '.fl{animation:fl 4.4s ease-in-out infinite}@keyframes fl{0%,100%{transform:none}50%{transform:translateY(-1.2px)}}')
664
682
  }
683
+ if (art.kind === 'skin') {
684
+ // A small panel in lavender: its head's dot, a title line, two lines of words.
685
+ const ink = '#c4b5fd'
686
+ return svg(s, s, square('#33227a', '#191146', '#a78bfa') +
687
+ '<rect x="10.5" y="12.5" width="23" height="19" rx="3.5" fill="' + ink + '" fill-opacity=".12" stroke="' + ink + '" stroke-width="1.6"/>' +
688
+ '<circle cx="16" cy="18.5" r="2" fill="' + ink + '"/>' +
689
+ '<path d="M20.5 18.5h8M15 24h12M15 27.5h8" fill="none" stroke="' + ink + '" stroke-width="1.6" stroke-linecap="round"/>')
690
+ }
665
691
  if (art.on) {
666
692
  const flame = 'M22 35.5c6.4 0 10-4.2 10-9.6 0-6-5.2-9-6.1-15.4-3.6 2.4-5.6 6-5.4 9.3-2.2-.9-3.3-3-3.5-4.9-3 2.9-4.4 6.4-4.4 11.3 0 5.4 3.6 9.3 9.4 9.3z'
667
693
  return svg(s, s,