clauderipple 0.9.5 → 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 (112) hide show
  1. package/CHANGELOG.md +67 -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/providers/anthropic-usage.js +33 -1
  94. package/dist/router/src/providers/google/index.js +26 -0
  95. package/dist/router/src/providers/google/prompt-refusal.js +29 -0
  96. package/dist/router/src/providers/google/usage.js +257 -0
  97. package/dist/router/src/providers/grok/index.js +18 -0
  98. package/dist/router/src/providers/grok/usage.js +77 -0
  99. package/dist/router/src/providers/openai/translate.js +13 -2
  100. package/dist/router/src/providers/opencode-usage.js +116 -0
  101. package/dist/router/src/proxy.js +339 -258
  102. package/dist/router/src/remote-host.js +320 -0
  103. package/dist/router/src/remote-tunnel.js +320 -0
  104. package/dist/router/src/requestlog.js +48 -39
  105. package/dist/router/src/usage-history.js +227 -0
  106. package/dist/router/src/version.js +1 -1
  107. package/dist/ui/app.js +436 -59
  108. package/dist/ui/i18n.js +184 -0
  109. package/dist/ui/index.html +24 -5
  110. package/dist/ui/style.css +39 -0
  111. package/docs/ARCHITECTURE.md +108 -8
  112. package/package.json +7 -1
package/dist/ui/i18n.js CHANGED
@@ -14,8 +14,11 @@ const I18N = {
14
14
  "providerStatus.connected": "연결됨", "providerStatus.keyNeeded": "API 키 확인 필요", "providerStatus.loginNeeded": "로그인 필요", "providerStatus.disconnected": "연결 안 됨", "providerStatus.checking": "확인 중",
15
15
  "clients.title": "클라이언트", "clients.subtitle": "ClaudeRipple을 사용하는 애플리케이션을 설정합니다.", "clients.desktop.title": "Claude Desktop", "clients.desktop.help": "모델 피커에 프로바이더 모델을 실제 이름으로 표시할지 설정합니다.", "clients.codex.title": "Codex (앱 및 CLI)", "clients.codex.help": "Codex 설정(~/.codex/config.toml)에 ClaudeRipple 프로바이더를 추가합니다. 터미널은 `codex --profile clauderipple -m <모델>`, Codex 앱은 설정에서 clauderipple 프로바이더를 기본값으로 선택하세요.", "clients.codex.note": "활성화하면 Codex의 GPT 요청도 ClaudeRipple을 경유합니다. Codex 자체 로그인은 유지되며, ChatGPT 계정을 여러 개 추가해 두면 한 계정이 한도에 걸릴 때 로그아웃 없이 다음 계정이 이어받습니다. 아래 목록은 Codex에서 추가로 사용할 수 있는 Claude 및 Anthropic 호환 모델입니다.", "clients.codex.turnOn": "Codex에서 ClaudeRipple 사용","clients.claudeCode.title": "터미널 Claude Code", "clients.claudeCode.help": "터미널 `claude`도 동일한 라우터를 사용합니다. `/model <이름>` 명령어로 선택하세요.",
16
16
  "logs.title": "로그", "logs.requests": "요청", "logs.raw": "원본 로그", "logs.lastHour": "최근 1시간", "logs.autoscroll": "자동 스크롤", "logs.polling": "3초마다 갱신", "logs.rawPolling": "3초마다 갱신 · 최근 200줄", "logs.provider": "프로바이더", "logs.all": "전체","logs.empty": "아직 요청이 없습니다.", "logs.summary.requests": "요청 수", "logs.summary.success": "성공 / 실패", "logs.summary.input": "입력 토큰", "logs.summary.output": "출력 토큰", "logs.summary.duration": "평균 응답 시간", "logs.cacheHit": "캐시 {percent}%", "logs.th.time": "시간", "logs.th.models": "모델", "logs.th.effort": "effort", "logs.th.input": "입력", "logs.th.output": "출력", "logs.th.duration": "소요", "logs.th.status": "상태", "logs.status.ok": "성공", "logs.status.error": "오류", "logs.status.resent": "재전송", "logs.status.resentHelp": "오류가 아닙니다. 클라이언트가 해당 요청을 자체적으로 재전송했습니다.", "logs.detail.id": "요청 ID", "logs.detail.kind": "종류", "logs.detail.stop": "종료 사유", "logs.detail.uncached": "미캐시 입력", "logs.detail.cacheRead": "캐시 읽기", "logs.detail.cacheWrite": "캐시 기록", "logs.detail.note": "메모", "logs.none": "없음", "logs.seconds": "{value}초",
17
+ "logs.usage": "사용량", "logs.period": "기간", "logs.period.hour": "1시간", "logs.period.today": "오늘", "logs.period.7d": "7일", "logs.period.30d": "30일", "logs.period.month": "이번 달", "logs.period.all": "전체", "logs.period.custom": "직접 지정", "logs.customFrom": "시작일", "logs.customTo": "종료일", "logs.since": "기록은 {date}부터", "logs.byModel": "모델별 사용량", "logs.modelsEmpty": "이 기간에는 사용 기록이 없습니다.", "logs.th.count": "요청 수", "logs.th.avg": "평균 소요", "logs.recent": "최근 요청", "logs.recentNote": "최근 200건",
17
18
  "about.title": "정보", "about.body": "ClaudeRipple은 독립 오픈소스 프로젝트이며 Anthropic·OpenAI와 제휴·보증·후원 관계가 없습니다. Claude와 Claude Code는 Anthropic, PBC의 상표입니다.", "about.license": "라이선스: GPL-3.0",
18
19
  "providers.identityHelp": "시스템 프롬프트 시작 부분에 '너는 <모델명>이고 Claude Code를 통해 답한다'를 추가합니다. 비활성화하면 모델이 자신을 Claude로 인식할 수 있습니다.",
20
+ "providers.limits": "상태 바에 한도 표시",
21
+ "providers.limitsHelp": "ClaudeRipple 상태 바 둘째 줄에 이 프로바이더의 사용 한도를 보여 줍니다. 한도를 읽을 수 없는 프로바이더(OpenCode Go·Zen, Grok 등)는 켜도 표시되지 않습니다.",
19
22
  "picker.models": "피커 등록 모델 {count}개: {names}",
20
23
  "picker.lastAt": "Claude Desktop 최근 목록 수신 시각: {at}",
21
24
  "picker.never": "아직 Claude Desktop이 목록을 수신하지 않았습니다. 앱을 완전히 종료한 후 다시 실행하세요.",
@@ -138,6 +141,9 @@ const I18N = {
138
141
  "mod.appearance": "테마",
139
142
  "mod.dark": "다크",
140
143
  "mod.lightAppearance": "라이트",
144
+ "mod.skin": "스킨",
145
+ "mod.skinDeep": "딥",
146
+ "mod.skinFlat": "플랫",
141
147
  "mod.language": "언어",
142
148
  "mod.show": "표시 항목",
143
149
  "mod.part.timer": "캐시 타이머",
@@ -220,8 +226,11 @@ const I18N = {
220
226
  "providerStatus.connected": "Connected", "providerStatus.keyNeeded": "Key needed", "providerStatus.loginNeeded": "Sign-in needed", "providerStatus.disconnected": "Not connected", "providerStatus.checking": "Checking",
221
227
  "clients.title": "Clients", "clients.subtitle": "Set up the apps that use ClaudeRipple.", "clients.desktop.title": "Claude Desktop", "clients.desktop.help": "Choose whether provider models appear by real name in the model picker.", "clients.codex.title": "Codex (app and CLI)", "clients.codex.help": "Adds ClaudeRipple as a provider to Codex's config (~/.codex/config.toml). Terminal: `codex --profile clauderipple -m <model>`; Codex app: make clauderipple the default provider in that config.", "clients.codex.note": "When on, Codex's GPT requests go through ClaudeRipple too. Codex keeps its own sign-in, and with several ChatGPT accounts added, the next one takes over when one hits its limit — no sign-out. The list below is the Claude and Anthropic-compatible models Codex gains.", "clients.codex.turnOn": "Use ClaudeRipple in Codex","clients.claudeCode.title": "Terminal Claude Code", "clients.claudeCode.help": "Terminal `claude` uses the same router. Choose with `/model <name>`.",
222
228
  "logs.title": "Logs", "logs.requests": "Requests", "logs.raw": "Raw logs", "logs.lastHour": "Last hour", "logs.autoscroll": "Auto-scroll", "logs.polling": "Updates every 3s", "logs.rawPolling": "Updates every 3s · last 200 lines", "logs.provider": "Provider", "logs.all": "All","logs.empty": "No requests yet.", "logs.summary.requests": "Requests", "logs.summary.success": "Succeeded / failed", "logs.summary.input": "Input tokens", "logs.summary.output": "Output tokens", "logs.summary.duration": "Average response", "logs.cacheHit": "Cache {percent}%", "logs.th.time": "Time", "logs.th.models": "Model", "logs.th.effort": "effort", "logs.th.input": "Input", "logs.th.output": "Output", "logs.th.duration": "Duration", "logs.th.status": "Status", "logs.status.ok": "ok", "logs.status.error": "error", "logs.status.resent": "resent", "logs.status.resentHelp": "Not a failure: the client sent this request again on its own.", "logs.detail.id": "Request ID", "logs.detail.kind": "Kind", "logs.detail.stop": "Stop reason", "logs.detail.uncached": "Uncached input", "logs.detail.cacheRead": "Cache read", "logs.detail.cacheWrite": "Cache write", "logs.detail.note": "Note", "logs.none": "None", "logs.seconds": "{value}s",
229
+ "logs.usage": "Usage", "logs.period": "Period", "logs.period.hour": "1 hour", "logs.period.today": "Today", "logs.period.7d": "7 days", "logs.period.30d": "30 days", "logs.period.month": "This month", "logs.period.all": "All time", "logs.period.custom": "Custom", "logs.customFrom": "Start date", "logs.customTo": "End date", "logs.since": "Records start {date}", "logs.byModel": "Usage by model", "logs.modelsEmpty": "No usage in this period.", "logs.th.count": "Requests", "logs.th.avg": "Avg. duration", "logs.recent": "Recent requests", "logs.recentNote": "Last 200",
223
230
  "about.title": "About", "about.body": "ClaudeRipple is an independent open-source project, not affiliated with Anthropic or OpenAI. Claude and Claude Code are trademarks of Anthropic, PBC.", "about.license": "License: GPL-3.0",
224
231
  "providers.identityHelp": "Prepends 'You are <model>, answering through Claude Code' to the system prompt. Off, the model may believe it is Claude.",
232
+ "providers.limits": "Show limits in the status bar",
233
+ "providers.limitsHelp": "Shows this provider's usage limits on the second row of the ClaudeRipple status bar. Providers whose limits cannot be read (OpenCode Go/Zen, Grok, etc.) stay hidden even when this is on.",
225
234
  "picker.models": "{count} models in the picker: {names}",
226
235
  "picker.lastAt": "Claude Desktop last fetched the list at {at}",
227
236
  "picker.never": "Claude Desktop has not fetched the list yet. Quit it completely and open it again.",
@@ -344,6 +353,9 @@ const I18N = {
344
353
  "mod.appearance": "Appearance",
345
354
  "mod.dark": "Dark",
346
355
  "mod.lightAppearance": "Light",
356
+ "mod.skin": "Skin",
357
+ "mod.skinDeep": "Deep",
358
+ "mod.skinFlat": "Flat",
347
359
  "mod.language": "Language",
348
360
  "mod.show": "Show",
349
361
  "mod.part.timer": "Cache timer",
@@ -708,10 +720,30 @@ const I18N = {
708
720
  "logs.detail.note": "备注",
709
721
  "logs.none": "无",
710
722
  "logs.seconds": "{value} 秒",
723
+ "logs.usage": "用量",
724
+ "logs.period": "时间范围",
725
+ "logs.period.hour": "1 小时",
726
+ "logs.period.today": "今天",
727
+ "logs.period.7d": "7 天",
728
+ "logs.period.30d": "30 天",
729
+ "logs.period.month": "本月",
730
+ "logs.period.all": "全部",
731
+ "logs.period.custom": "自定义",
732
+ "logs.customFrom": "开始日期",
733
+ "logs.customTo": "结束日期",
734
+ "logs.since": "记录始于 {date}",
735
+ "logs.byModel": "按模型统计用量",
736
+ "logs.modelsEmpty": "此期间没有用量记录。",
737
+ "logs.th.count": "请求数",
738
+ "logs.th.avg": "平均耗时",
739
+ "logs.recent": "最近的请求",
740
+ "logs.recentNote": "最近 200 条",
711
741
  "about.title": "关于",
712
742
  "about.body": "ClaudeRipple 是一个独立的开源项目,与 Anthropic 和 OpenAI 均无关联。Claude 和 Claude Code 是 Anthropic, PBC 的商标。",
713
743
  "about.license": "许可证:GPL-3.0",
714
744
  "providers.identityHelp": "在系统提示词开头加上 'You are <model>, answering through Claude Code'。关闭时,模型可能会以为自己是 Claude。",
745
+ "providers.limits": "在状态栏显示额度",
746
+ "providers.limitsHelp": "在 ClaudeRipple 状态栏第二行显示此服务商的使用额度。无法读取额度的服务商(OpenCode Go/Zen、Grok 等)即使开启也不会显示。",
715
747
  "picker.models": "选择器中的 {count} 个模型:{names}",
716
748
  "picker.lastAt": "Claude Desktop 最后一次获取列表的时间:{at}",
717
749
  "picker.never": "Claude Desktop 尚未获取过列表。请完全退出后重新打开。",
@@ -834,6 +866,9 @@ const I18N = {
834
866
  "mod.appearance": "外观",
835
867
  "mod.dark": "深色",
836
868
  "mod.lightAppearance": "浅色",
869
+ "mod.skin": "皮肤",
870
+ "mod.skinDeep": "深海",
871
+ "mod.skinFlat": "平面",
837
872
  "mod.language": "语言",
838
873
  "mod.show": "显示",
839
874
  "mod.part.timer": "缓存计时",
@@ -1198,10 +1233,30 @@ const I18N = {
1198
1233
  "logs.detail.note": "備考",
1199
1234
  "logs.none": "なし",
1200
1235
  "logs.seconds": "{value}秒",
1236
+ "logs.usage": "使用量",
1237
+ "logs.period": "期間",
1238
+ "logs.period.hour": "1時間",
1239
+ "logs.period.today": "今日",
1240
+ "logs.period.7d": "7日間",
1241
+ "logs.period.30d": "30日間",
1242
+ "logs.period.month": "今月",
1243
+ "logs.period.all": "全期間",
1244
+ "logs.period.custom": "期間を指定",
1245
+ "logs.customFrom": "開始日",
1246
+ "logs.customTo": "終了日",
1247
+ "logs.since": "記録は{date}から",
1248
+ "logs.byModel": "モデル別の使用量",
1249
+ "logs.modelsEmpty": "この期間の使用記録はありません。",
1250
+ "logs.th.count": "リクエスト数",
1251
+ "logs.th.avg": "平均所要時間",
1252
+ "logs.recent": "最近のリクエスト",
1253
+ "logs.recentNote": "直近200件",
1201
1254
  "about.title": "このアプリについて",
1202
1255
  "about.body": "ClaudeRippleは独立したオープンソースプロジェクトであり、AnthropicおよびOpenAIとは提携関係にありません。ClaudeおよびClaude Codeは、Anthropic, PBCの商標です。",
1203
1256
  "about.license": "ライセンス:GPL-3.0",
1204
1257
  "providers.identityHelp": "システムプロンプトの先頭に「You are <model>, answering through Claude Code」を追加します。オフの場合、モデルが自分をClaudeだと思い込むことがあります。",
1258
+ "providers.limits": "ステータスバーに上限を表示",
1259
+ "providers.limitsHelp": "ClaudeRipple ステータスバーの2行目にこのプロバイダーの使用上限を表示します。上限を読み取れないプロバイダー(OpenCode Go/Zen、Grok など)はオンにしても表示されません。",
1205
1260
  "picker.models": "ピッカーのモデル {count} 個:{names}",
1206
1261
  "picker.lastAt": "Claude Desktopが最後に一覧を取得した日時:{at}",
1207
1262
  "picker.never": "Claude Desktopはまだ一覧を取得していません。完全に終了してから開き直してください。",
@@ -1324,6 +1379,9 @@ const I18N = {
1324
1379
  "mod.appearance": "テーマ",
1325
1380
  "mod.dark": "ダーク",
1326
1381
  "mod.lightAppearance": "ライト",
1382
+ "mod.skin": "スキン",
1383
+ "mod.skinDeep": "ディープ",
1384
+ "mod.skinFlat": "フラット",
1327
1385
  "mod.language": "言語",
1328
1386
  "mod.show": "表示項目",
1329
1387
  "mod.part.timer": "キャッシュタイマー",
@@ -1688,10 +1746,30 @@ const I18N = {
1688
1746
  "logs.detail.note": "Nota",
1689
1747
  "logs.none": "Ninguno",
1690
1748
  "logs.seconds": "{value} s",
1749
+ "logs.usage": "Uso",
1750
+ "logs.period": "Período",
1751
+ "logs.period.hour": "1 hora",
1752
+ "logs.period.today": "Hoy",
1753
+ "logs.period.7d": "7 días",
1754
+ "logs.period.30d": "30 días",
1755
+ "logs.period.month": "Este mes",
1756
+ "logs.period.all": "Todo",
1757
+ "logs.period.custom": "Personalizado",
1758
+ "logs.customFrom": "Fecha de inicio",
1759
+ "logs.customTo": "Fecha de fin",
1760
+ "logs.since": "Registros desde el {date}",
1761
+ "logs.byModel": "Uso por modelo",
1762
+ "logs.modelsEmpty": "No hay uso en este período.",
1763
+ "logs.th.count": "Solicitudes",
1764
+ "logs.th.avg": "Duración media",
1765
+ "logs.recent": "Solicitudes recientes",
1766
+ "logs.recentNote": "Últimas 200",
1691
1767
  "about.title": "Acerca de",
1692
1768
  "about.body": "ClaudeRipple es un proyecto independiente de código abierto, sin afiliación con Anthropic ni con OpenAI. Claude y Claude Code son marcas comerciales de Anthropic, PBC.",
1693
1769
  "about.license": "Licencia: GPL-3.0",
1694
1770
  "providers.identityHelp": "Antepone 'You are <model>, answering through Claude Code' al prompt del sistema. Si está desactivado, el modelo puede creer que es Claude.",
1771
+ "providers.limits": "Mostrar límites en la barra de estado",
1772
+ "providers.limitsHelp": "Muestra los límites de uso de este proveedor en la segunda fila de la barra de estado de ClaudeRipple. Los proveedores cuyos límites no se pueden leer (OpenCode Go/Zen, Grok, etc.) no se muestran aunque esté activado.",
1695
1773
  "picker.models": "{count} modelos en el selector: {names}",
1696
1774
  "picker.lastAt": "Claude Desktop obtuvo la lista por última vez el {at}",
1697
1775
  "picker.never": "Claude Desktop aún no ha obtenido la lista. Ciérralo por completo y vuelve a abrirlo.",
@@ -1814,6 +1892,9 @@ const I18N = {
1814
1892
  "mod.appearance": "Apariencia",
1815
1893
  "mod.dark": "Oscuro",
1816
1894
  "mod.lightAppearance": "Claro",
1895
+ "mod.skin": "Estilo",
1896
+ "mod.skinDeep": "Profundo",
1897
+ "mod.skinFlat": "Plano",
1817
1898
  "mod.language": "Idioma",
1818
1899
  "mod.show": "Mostrar",
1819
1900
  "mod.part.timer": "Temporizador de caché",
@@ -2178,10 +2259,30 @@ const I18N = {
2178
2259
  "logs.detail.note": "Observação",
2179
2260
  "logs.none": "Nenhum",
2180
2261
  "logs.seconds": "{value}s",
2262
+ "logs.usage": "Uso",
2263
+ "logs.period": "Período",
2264
+ "logs.period.hour": "1 hora",
2265
+ "logs.period.today": "Hoje",
2266
+ "logs.period.7d": "7 dias",
2267
+ "logs.period.30d": "30 dias",
2268
+ "logs.period.month": "Este mês",
2269
+ "logs.period.all": "Tudo",
2270
+ "logs.period.custom": "Personalizado",
2271
+ "logs.customFrom": "Data inicial",
2272
+ "logs.customTo": "Data final",
2273
+ "logs.since": "Registros desde {date}",
2274
+ "logs.byModel": "Uso por modelo",
2275
+ "logs.modelsEmpty": "Nenhum uso neste período.",
2276
+ "logs.th.count": "Requisições",
2277
+ "logs.th.avg": "Duração média",
2278
+ "logs.recent": "Requisições recentes",
2279
+ "logs.recentNote": "Últimas 200",
2181
2280
  "about.title": "Sobre",
2182
2281
  "about.body": "O ClaudeRipple é um projeto open source independente, sem afiliação com a Anthropic ou a OpenAI. Claude e Claude Code são marcas comerciais da Anthropic, PBC.",
2183
2282
  "about.license": "Licença: GPL-3.0",
2184
2283
  "providers.identityHelp": "Insere 'You are <model>, answering through Claude Code' no início do prompt de sistema. Desativado, o modelo pode achar que é o Claude.",
2284
+ "providers.limits": "Mostrar limites na barra de status",
2285
+ "providers.limitsHelp": "Mostra os limites de uso deste provedor na segunda linha da barra de status do ClaudeRipple. Provedores cujos limites não podem ser lidos (OpenCode Go/Zen, Grok, etc.) não aparecem mesmo com isto ativado.",
2185
2286
  "picker.models": "{count} modelos no seletor: {names}",
2186
2287
  "picker.lastAt": "O Claude Desktop buscou a lista pela última vez em {at}",
2187
2288
  "picker.never": "O Claude Desktop ainda não buscou a lista. Encerre-o por completo e abra-o de novo.",
@@ -2304,6 +2405,9 @@ const I18N = {
2304
2405
  "mod.appearance": "Aparência",
2305
2406
  "mod.dark": "Escuro",
2306
2407
  "mod.lightAppearance": "Claro",
2408
+ "mod.skin": "Estilo",
2409
+ "mod.skinDeep": "Profundo",
2410
+ "mod.skinFlat": "Plano",
2307
2411
  "mod.language": "Idioma",
2308
2412
  "mod.show": "Mostrar",
2309
2413
  "mod.part.timer": "Timer do cache",
@@ -2375,6 +2479,86 @@ const I18N = {
2375
2479
  },
2376
2480
  };
2377
2481
 
2482
+ // Phone control vocabulary stays together across the dashboard's six languages.
2483
+ Object.assign(I18N.ko, {
2484
+ "nav.phone": "휴대폰", "phone.subtitle": "Ripple Remote 앱을 연결하고 휴대폰 연결과 알림을 관리합니다.",
2485
+ "phone.refresh": "새로고침", "phone.status": "연결 상태", "phone.host": "원격 호스트", "phone.running": "실행 중", "phone.stopped": "중지됨",
2486
+ "phone.mode": "연결 방식", "phone.relayStatus": "Relay — 어디서나 + 알림", "phone.directStatus": "같은 네트워크", "phone.localStatus": "꺼짐",
2487
+ "phone.notifications": "알림", "phone.ready": "준비됨", "phone.needsRelay": "Relay 필요", "phone.install": "저장소 폴더의 터미널에서 호스트를 설치·시작하세요.",
2488
+ "phone.pairing": "휴대폰 연결 (QR)", "phone.scan": "휴대폰 카메라로 스캔", "phone.showQr": "QR 코드 보기", "phone.copyLink": "링크 복사", "phone.copied": "링크를 복사했습니다.", "phone.copyFailed": "링크를 복사하지 못했습니다. 브라우저의 클립보드 권한을 확인하세요.", "phone.qrAlt": "Ripple Remote 연결 QR 코드",
2489
+ "phone.qrWarning": "코드와 링크에는 페어링 키가 들어 있습니다. 본인 휴대폰에만 보여 주세요.", "phone.unreachable": "현재 이 컴퓨터에서만 연결됩니다. 휴대폰 연결을 위해 아래에서 Relay 또는 네트워크 연결을 설정하세요.",
2490
+ "phone.settings": "휴대폰 접속", "phone.choice.relay": "켜기 — Relay (권장)", "phone.help.relay": "어디서나 연결되고 알림도 받습니다. 대화는 종단간 암호화됩니다.", "phone.choice.local": "끄기", "phone.help.local": "휴대폰에서 이 컴퓨터에 연결할 수 없습니다.",
2491
+ "phone.apply": "변경 사항 적용",
2492
+ "phone.restartRequired": "호스트를 다시 시작해야 적용됩니다", "phone.restart": "재시작", "phone.restartConfirm": "호스트를 다시 시작하시겠습니까? 휴대폰 연결과 진행 중인 원격 작업이 중단될 수 있습니다.", "phone.restarting": "호스트를 재시작했습니다. 잠시 후 상태를 확인합니다.", "phone.manualRestart": "이 운영체제에서는 터미널에서 실행하세요: {command}",
2493
+ "phone.phones": "연결된 휴대폰", "phone.connected": "연결됨", "phone.offline": "오프라인", "phone.lastSeen": "마지막 접속: {time}", "phone.unknownSeen": "마지막 접속 기록 없음", "phone.pushOn": "알림 켜짐", "phone.pushOff": "알림 꺼짐", "phone.empty": "아직 휴대폰이 없습니다. QR 코드로 연결하세요.",
2494
+ "phone.rotate": "모든 휴대폰 연결 끊기 (새 키)", "phone.rotateConfirm": "모든 휴대폰의 연결과 알림을 해제하고 새 키를 발급하시겠습니까? 기존 QR과 링크는 무효화되며 다시 페어링해야 합니다.", "phone.rotated": "모든 휴대폰 연결을 해제하고 키를 교체했습니다.", "phone.sharedKey": "모든 휴대폰은 하나의 페어링 키를 공유합니다. 개별 연결 해제는 지원하지 않습니다.",
2495
+ });
2496
+ Object.assign(I18N.en, {
2497
+ "nav.phone": "Phone", "phone.subtitle": "Pair Ripple Remote and manage phone connections and notifications.",
2498
+ "phone.refresh": "Refresh", "phone.status": "Connection status", "phone.host": "Remote host", "phone.running": "Running", "phone.stopped": "Stopped",
2499
+ "phone.mode": "Connection mode", "phone.relayStatus": "Relay — anywhere + notifications", "phone.directStatus": "Same network", "phone.localStatus": "Off",
2500
+ "phone.notifications": "Notifications", "phone.ready": "Ready", "phone.needsRelay": "Relay required", "phone.install": "Install and start the host from a terminal in the repository folder.",
2501
+ "phone.pairing": "Pair a phone (QR)", "phone.scan": "Scan with your phone camera", "phone.showQr": "Show QR code", "phone.copyLink": "Copy link", "phone.copied": "Link copied.", "phone.copyFailed": "Could not copy the link. Check the browser's clipboard permission.", "phone.qrAlt": "Ripple Remote pairing QR code",
2502
+ "phone.qrWarning": "The code and link carry the pairing key. Show them only to your own phone.", "phone.unreachable": "Only this computer can connect right now. Choose Relay or a network connection below to reach it from your phone.",
2503
+ "phone.settings": "Phone access", "phone.choice.relay": "On — Relay (recommended)", "phone.help.relay": "Connect from anywhere, with notifications. Conversations are end-to-end encrypted.", "phone.choice.local": "Off", "phone.help.local": "Phones cannot connect to this computer.",
2504
+ "phone.apply": "Apply changes",
2505
+ "phone.restartRequired": "Restart the host to apply changes", "phone.restart": "Restart", "phone.restartConfirm": "Restart the host? Phone connections and active remote work may be interrupted.", "phone.restarting": "Host restarted. Checking its status shortly.", "phone.manualRestart": "On this operating system, run in a terminal: {command}",
2506
+ "phone.phones": "Connected phones", "phone.connected": "Connected", "phone.offline": "Offline", "phone.lastSeen": "Last seen: {time}", "phone.unknownSeen": "Last seen unknown", "phone.pushOn": "Notifications on", "phone.pushOff": "Notifications off", "phone.empty": "No phones yet. Pair one with the QR code.",
2507
+ "phone.rotate": "Disconnect all phones (new key)", "phone.rotateConfirm": "Disconnect every phone, remove notifications and issue a new key? Existing QR codes and links will stop working. Each phone must pair again.", "phone.rotated": "All phones disconnected and pairing key replaced.", "phone.sharedKey": "All phones share one pairing key. Individual unpairing is not supported.",
2508
+ });
2509
+ Object.assign(I18N.zh, {
2510
+ "nav.phone": "手机", "phone.subtitle": "配对 Ripple Remote,管理手机连接和通知。",
2511
+ "phone.refresh": "刷新", "phone.status": "连接状态", "phone.host": "远程主机", "phone.running": "运行中", "phone.stopped": "已停止",
2512
+ "phone.mode": "连接方式", "phone.relayStatus": "Relay — 随处连接 + 通知", "phone.directStatus": "同一网络", "phone.localStatus": "仅此电脑",
2513
+ "phone.notifications": "通知", "phone.ready": "已就绪", "phone.needsRelay": "需要 Relay", "phone.install": "在仓库文件夹中打开终端,安装并启动主机。",
2514
+ "phone.pairing": "配对手机 (QR)", "phone.scan": "用手机相机扫描", "phone.showQr": "显示 QR 码", "phone.copyLink": "复制链接", "phone.copied": "已复制链接。", "phone.copyFailed": "无法复制链接,请检查浏览器剪贴板权限。", "phone.qrAlt": "Ripple Remote 配对 QR 码",
2515
+ "phone.qrWarning": "QR 码和链接包含配对密钥,仅向您自己的手机展示。", "phone.unreachable": "当前仅此电脑可连接。请在下方选择 Relay 或网络连接以连接手机。",
2516
+ "phone.settings": "手机访问", "phone.choice.relay": "开启 — Relay(推荐)", "phone.help.relay": "随处连接并接收通知。对话端到端加密。", "phone.choice.local": "关闭", "phone.help.local": "手机无法连接到此电脑。",
2517
+ "phone.apply": "应用更改",
2518
+ "phone.restartRequired": "重启主机后更改生效", "phone.restart": "重启", "phone.restartConfirm": "重启主机?手机连接和正在进行的远程任务可能会中断。", "phone.restarting": "主机已重启,稍后检查状态。", "phone.manualRestart": "请在此操作系统的终端中运行:{command}",
2519
+ "phone.phones": "已连接的手机", "phone.connected": "已连接", "phone.offline": "离线", "phone.lastSeen": "最后连接:{time}", "phone.unknownSeen": "无最后连接记录", "phone.pushOn": "通知已开启", "phone.pushOff": "通知已关闭", "phone.empty": "尚无手机,请使用 QR 码配对。",
2520
+ "phone.rotate": "断开所有手机(新密钥)", "phone.rotateConfirm": "断开所有手机、移除通知订阅并生成新密钥?现有 QR 码和链接将失效,所有手机都需要重新配对。", "phone.rotated": "已断开所有手机并更换配对密钥。", "phone.sharedKey": "所有手机共用一个配对密钥,不支持单独解除配对。",
2521
+ });
2522
+ Object.assign(I18N.ja, {
2523
+ "nav.phone": "スマートフォン", "phone.subtitle": "Ripple Remote をペアリングし、接続と通知を管理します。",
2524
+ "phone.refresh": "更新", "phone.status": "接続状況", "phone.host": "リモートホスト", "phone.running": "実行中", "phone.stopped": "停止中",
2525
+ "phone.mode": "接続方式", "phone.relayStatus": "Relay — どこからでも + 通知", "phone.directStatus": "同じネットワーク", "phone.localStatus": "このコンピュータのみ",
2526
+ "phone.notifications": "通知", "phone.ready": "準備完了", "phone.needsRelay": "Relay が必要", "phone.install": "リポジトリのフォルダ内でターミナルを開き、ホストをインストールして起動してください。",
2527
+ "phone.pairing": "スマートフォンの接続 (QR)", "phone.scan": "スマートフォンのカメラでスキャン", "phone.showQr": "QR コードを表示", "phone.copyLink": "リンクをコピー", "phone.copied": "リンクをコピーしました。", "phone.copyFailed": "コピーできませんでした。ブラウザのクリップボード権限を確認してください。", "phone.qrAlt": "Ripple Remote ペアリング QR コード",
2528
+ "phone.qrWarning": "コードとリンクにはペアリングキーが含まれています。ご自身のスマートフォンにのみ見せてください。", "phone.unreachable": "現在はこのコンピュータのみ接続できます。スマートフォンから接続するには、下で Relay またはネットワーク接続を選んでください。",
2529
+ "phone.settings": "スマートフォンからのアクセス", "phone.choice.relay": "オン — Relay(推奨)", "phone.help.relay": "どこからでも接続でき、通知も届きます。会話はエンドツーエンドで暗号化されます。", "phone.choice.local": "オフ", "phone.help.local": "スマートフォンからこのコンピュータに接続できません。",
2530
+ "phone.apply": "変更を適用",
2531
+ "phone.restartRequired": "適用するにはホストの再起動が必要です", "phone.restart": "再起動", "phone.restartConfirm": "ホストを再起動しますか?スマートフォンの接続と実行中のリモート作業が中断する場合があります。", "phone.restarting": "ホストを再起動しました。まもなく状態を確認します。", "phone.manualRestart": "この OS ではターミナルで実行してください:{command}",
2532
+ "phone.phones": "接続済みスマートフォン", "phone.connected": "接続中", "phone.offline": "オフライン", "phone.lastSeen": "最終接続:{time}", "phone.unknownSeen": "最終接続の記録なし", "phone.pushOn": "通知オン", "phone.pushOff": "通知オフ", "phone.empty": "スマートフォンはまだありません。QR コードで接続してください。",
2533
+ "phone.rotate": "すべての接続を解除(新しいキー)", "phone.rotateConfirm": "すべての接続と通知を解除し、新しいキーを発行しますか?既存の QR コードとリンクは無効になり、再ペアリングが必要です。", "phone.rotated": "すべての接続を解除し、キーを更新しました。", "phone.sharedKey": "すべての端末で一つのキーを共有します。個別のペアリング解除はできません。",
2534
+ });
2535
+ Object.assign(I18N.es, {
2536
+ "nav.phone": "Teléfono", "phone.subtitle": "Vincula Ripple Remote y gestiona las conexiones y notificaciones del teléfono.",
2537
+ "phone.refresh": "Actualizar", "phone.status": "Estado de conexión", "phone.host": "Host remoto", "phone.running": "En ejecución", "phone.stopped": "Detenido",
2538
+ "phone.mode": "Modo de conexión", "phone.relayStatus": "Relay — desde cualquier lugar + avisos", "phone.directStatus": "Misma red", "phone.localStatus": "Solo este equipo",
2539
+ "phone.notifications": "Notificaciones", "phone.ready": "Listas", "phone.needsRelay": "Requiere Relay", "phone.install": "Instala e inicia el host desde una terminal en la carpeta del repositorio.",
2540
+ "phone.pairing": "Vincular teléfono (QR)", "phone.scan": "Escanea con la cámara del teléfono", "phone.showQr": "Mostrar código QR", "phone.copyLink": "Copiar enlace", "phone.copied": "Enlace copiado.", "phone.copyFailed": "No se pudo copiar. Revisa el permiso del portapapeles del navegador.", "phone.qrAlt": "Código QR de vinculación de Ripple Remote",
2541
+ "phone.qrWarning": "El código y el enlace contienen la clave de vinculación. Muéstralos solo a tu propio teléfono.", "phone.unreachable": "Ahora solo puede conectarse este equipo. Elige Relay o una conexión de red para conectar tu teléfono.",
2542
+ "phone.settings": "Acceso desde el teléfono", "phone.choice.relay": "Activado — Relay (recomendado)", "phone.help.relay": "Conéctate desde cualquier lugar, con avisos. Las conversaciones se cifran de extremo a extremo.", "phone.choice.local": "Desactivado", "phone.help.local": "Los teléfonos no pueden conectarse a este equipo.",
2543
+ "phone.apply": "Aplicar cambios",
2544
+ "phone.restartRequired": "Reinicia el host para aplicar los cambios", "phone.restart": "Reiniciar", "phone.restartConfirm": "¿Reiniciar el host? Las conexiones y tareas remotas activas podrían interrumpirse.", "phone.restarting": "Host reiniciado. Comprobaremos su estado en breve.", "phone.manualRestart": "En este sistema operativo, ejecuta en una terminal: {command}",
2545
+ "phone.phones": "Teléfonos conectados", "phone.connected": "Conectado", "phone.offline": "Sin conexión", "phone.lastSeen": "Última conexión: {time}", "phone.unknownSeen": "Última conexión desconocida", "phone.pushOn": "Notificaciones activadas", "phone.pushOff": "Notificaciones desactivadas", "phone.empty": "Aún no hay teléfonos. Vincula uno con el QR.",
2546
+ "phone.rotate": "Desconectar todos (nueva clave)", "phone.rotateConfirm": "¿Desconectar todos los teléfonos, eliminar sus avisos y emitir una nueva clave? Los QR y enlaces actuales dejarán de funcionar. Habrá que vincular cada teléfono de nuevo.", "phone.rotated": "Todos los teléfonos desconectados y clave reemplazada.", "phone.sharedKey": "Todos comparten una clave. No se admite desvincular teléfonos individualmente.",
2547
+ });
2548
+ Object.assign(I18N.pt, {
2549
+ "nav.phone": "Celular", "phone.subtitle": "Pareie o Ripple Remote e gerencie conexões e notificações do celular.",
2550
+ "phone.refresh": "Atualizar", "phone.status": "Status da conexão", "phone.host": "Host remoto", "phone.running": "Em execução", "phone.stopped": "Parado",
2551
+ "phone.mode": "Modo de conexão", "phone.relayStatus": "Relay — de qualquer lugar + avisos", "phone.directStatus": "Mesma rede", "phone.localStatus": "Somente este computador",
2552
+ "phone.notifications": "Notificações", "phone.ready": "Prontas", "phone.needsRelay": "Requer Relay", "phone.install": "Instale e inicie o host em um terminal na pasta do repositório.",
2553
+ "phone.pairing": "Parear celular (QR)", "phone.scan": "Escaneie com a câmera do celular", "phone.showQr": "Mostrar QR code", "phone.copyLink": "Copiar link", "phone.copied": "Link copiado.", "phone.copyFailed": "Não foi possível copiar. Verifique a permissão da área de transferência do navegador.", "phone.qrAlt": "QR code de pareamento do Ripple Remote",
2554
+ "phone.qrWarning": "O código e o link contêm a chave de pareamento. Mostre-os apenas ao seu próprio celular.", "phone.unreachable": "Por enquanto, só este computador pode se conectar. Escolha Relay ou uma conexão de rede abaixo para usar o celular.",
2555
+ "phone.settings": "Acesso pelo celular", "phone.choice.relay": "Ativado — Relay (recomendado)", "phone.help.relay": "Conecte de qualquer lugar, com notificações. As conversas têm criptografia de ponta a ponta.", "phone.choice.local": "Desativado", "phone.help.local": "Celulares não podem se conectar a este computador.",
2556
+ "phone.apply": "Aplicar alterações",
2557
+ "phone.restartRequired": "Reinicie o host para aplicar as alterações", "phone.restart": "Reiniciar", "phone.restartConfirm": "Reiniciar o host? As conexões e tarefas remotas em andamento podem ser interrompidas.", "phone.restarting": "Host reiniciado. Verificaremos o status em breve.", "phone.manualRestart": "Neste sistema operacional, execute em um terminal: {command}",
2558
+ "phone.phones": "Celulares conectados", "phone.connected": "Conectado", "phone.offline": "Offline", "phone.lastSeen": "Última conexão: {time}", "phone.unknownSeen": "Última conexão desconhecida", "phone.pushOn": "Notificações ativadas", "phone.pushOff": "Notificações desativadas", "phone.empty": "Nenhum celular ainda. Pareie um com o QR code.",
2559
+ "phone.rotate": "Desconectar todos (nova chave)", "phone.rotateConfirm": "Desconectar todos os celulares, remover as notificações e gerar uma nova chave? Os QR codes e links atuais deixarão de funcionar. Será necessário parear cada celular novamente.", "phone.rotated": "Todos os celulares desconectados e chave substituída.", "phone.sharedKey": "Todos compartilham uma chave. Não há suporte para desemparelhar celulares individualmente.",
2560
+ });
2561
+
2378
2562
  /** The languages offered, each named in itself, in the order the selector lists them. */
2379
2563
  const LANGUAGES = [
2380
2564
  ["en", "English"], ["ko", "한국어"], ["zh", "简体中文"], ["ja", "日本語"], ["es", "Español"], ["pt", "Português (BR)"],
@@ -14,6 +14,7 @@
14
14
  <button data-view="slots" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7h13M13 4l3 3-3 3M17 13H4M7 10l-3 3 3 3"/></svg><span data-i18n="nav.slots">Model mapping</span></button>
15
15
  <button data-view="providers" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.2 15.5h8.1a3.3 3.3 0 0 0 .4-6.6 4.8 4.8 0 0 0-9.2-.8 3.7 3.7 0 0 0 .7 7.4z"/></svg><span data-i18n="nav.providers">Providers</span></button>
16
16
  <button data-view="clients" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="4" width="13" height="9.5" rx="1.6"/><path d="M1.8 16.4h16.4"/></svg><span data-i18n="nav.clients">Clients</span></button>
17
+ <button data-view="phone" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="2" width="10" height="16" rx="2"/><path d="M8.5 14.8h3"/></svg><span data-i18n="nav.phone">Phone</span></button>
17
18
  <button data-view="mod" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 6.5h8M14.5 6.5h3M2.5 13.5h3M9.5 13.5h8"/><circle cx="12.5" cy="6.5" r="2"/><circle cx="7.5" cy="13.5" r="2"/></svg><span class="nav-long" data-i18n="nav.mod">ClaudeRipple mod</span><span class="nav-short" data-i18n="nav.modShort">Mod</span></button>
18
19
  <button data-view="logs" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 5.5H17M7.5 10H17M7.5 14.5H17"/><path d="M3.5 5.5h.01M3.5 10h.01M3.5 14.5h.01" stroke-width="2.4"/></svg><span data-i18n="nav.logs">Logs</span></button>
19
20
  <button data-view="about" class="nav-btn" type="button"><svg class="nav-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="7.6"/><path d="M10 9.2v4.6"/><path d="M10 6.3h.01" stroke-width="2.2"/></svg><span data-i18n="nav.about">About</span></button>
@@ -134,6 +135,11 @@
134
135
  </section>
135
136
  </section>
136
137
 
138
+ <section id="view-phone" class="view">
139
+ <header class="page-header"><div><h1 data-i18n="nav.phone">Phone</h1><p class="page-subtitle" data-i18n="phone.subtitle"></p></div><button id="phone-refresh" class="btn secondary" type="button" data-i18n="phone.refresh">Refresh</button></header>
140
+ <div id="phone-content"></div>
141
+ </section>
142
+
137
143
  <section id="view-mod" class="view">
138
144
  <header class="page-header">
139
145
  <div><h1 data-i18n="nav.mod">ClaudeRipple mod</h1><p class="page-subtitle" data-i18n="mod.subtitle"></p></div>
@@ -152,15 +158,28 @@
152
158
  </div>
153
159
  </header>
154
160
  <div id="logs-requests-panel" role="tabpanel">
155
- <section class="section">
156
- <div class="logs-header-row"><div><h2 class="section-title" data-i18n="logs.lastHour">Last hour</h2><span class="small" data-i18n="logs.polling">Updates every 3s</span></div></div>
157
- <div class="group logs-summary" id="logs-summary"></div>
158
- </section>
159
161
  <section class="section">
160
162
  <div class="logs-header-row">
161
- <h2 class="section-title" data-i18n="logs.requests">Requests</h2>
163
+ <div><h2 class="section-title" data-i18n="logs.usage">Usage</h2><span class="small" id="logs-range-caption"></span></div>
162
164
  <label class="logs-filter"><span data-i18n="logs.provider">Provider</span><select id="logs-provider"><option value="" data-i18n="logs.all">All</option></select></label>
163
165
  </div>
166
+ <div class="logs-period-row">
167
+ <div class="segmented logs-period" id="logs-period" role="radiogroup" data-i18n-title="logs.period"></div>
168
+ <div class="logs-custom" id="logs-custom" hidden>
169
+ <input type="date" id="logs-from" data-i18n-title="logs.customFrom" />
170
+ <span class="logs-custom-dash" aria-hidden="true">–</span>
171
+ <input type="date" id="logs-to" data-i18n-title="logs.customTo" />
172
+ </div>
173
+ </div>
174
+ <div class="group logs-summary" id="logs-summary"></div>
175
+ </section>
176
+ <section class="section">
177
+ <div class="logs-header-row"><h2 class="section-title" data-i18n="logs.byModel">Usage by model</h2></div>
178
+ <div class="group logs-table-card"><table id="models-table"><thead><tr><th data-i18n="logs.th.models">Model</th><th data-i18n="logs.th.count">Requests</th><th data-i18n="logs.th.input">Input</th><th data-i18n="logs.th.output">Output</th><th data-i18n="logs.th.avg">Avg. duration</th></tr></thead><tbody></tbody></table></div>
179
+ <div class="group empty-card" id="models-empty" hidden data-i18n="logs.modelsEmpty">No usage in this period.</div>
180
+ </section>
181
+ <section class="section">
182
+ <div class="logs-header-row"><div><h2 class="section-title" data-i18n="logs.recent">Recent requests</h2><span class="small" data-i18n="logs.recentNote">Last 200</span></div></div>
164
183
  <div class="group logs-table-card"><table id="requests-table"><thead><tr><th data-i18n="logs.th.time">Time</th><th data-i18n="logs.th.models">Model</th><th data-i18n="logs.th.effort">effort</th><th data-i18n="logs.th.input">Input</th><th data-i18n="logs.th.output">Output</th><th data-i18n="logs.th.duration">Duration</th><th data-i18n="logs.th.status">Status</th></tr></thead><tbody></tbody></table></div>
165
184
  <div class="group empty-card" id="requests-empty" hidden data-i18n="logs.empty">No requests yet.</div>
166
185
  </section>
package/dist/ui/style.css CHANGED
@@ -277,6 +277,11 @@ select {
277
277
  text-overflow: ellipsis;
278
278
  cursor: pointer;
279
279
  }
280
+ /* Native popups need an opaque surface independent of the translucent field. */
281
+ select option, select optgroup {
282
+ background-color: var(--group);
283
+ color: var(--label);
284
+ }
280
285
  select:disabled { opacity: 0.5; cursor: default; }
281
286
  input[type="checkbox"], input[type="radio"] { width: auto; min-height: 0; padding: 0; accent-color: var(--accent-fill); }
282
287
  input[type="checkbox"] { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
@@ -450,6 +455,16 @@ label.check input { margin-top: 2px; }
450
455
  .picker-model > .model-window::-webkit-inner-spin-button { display: none; }
451
456
  .picker-model > .model-window:hover, .picker-model > .model-window:focus, .picker-model > .model-window:not(:placeholder-shown) { color: var(--label); background: var(--field); border-color: var(--field-edge); }
452
457
 
458
+ /* ---- Phone --------------------------------------------------------------------------- */
459
+
460
+ .phone-choice { cursor: pointer; }
461
+ .phone-choice input { flex: 0 0 auto; }
462
+ .phone-qr:not(:empty) { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 12px; padding: 16px; }
463
+ .phone-qr-image { display: block; flex: 0 0 240px; width: 240px; height: 240px; padding: 8px; background: #fff; border-radius: 8px; }
464
+ #phone-content .cell-value input { width: 280px; max-width: 100%; }
465
+ #phone-content code { display: block; margin-top: 6px; overflow-wrap: anywhere; }
466
+ #phone-content > .section:first-child { margin-top: 0; }
467
+
453
468
  /* ---- Providers ----------------------------------------------------------------------- */
454
469
 
455
470
  .provider-workspace { display: grid; grid-template-columns: 264px minmax(0, 1fr); gap: 28px; align-items: start; }
@@ -531,6 +546,12 @@ label.check input { margin-top: 2px; }
531
546
  .logs-filter select { width: auto; min-width: 130px; min-height: 28px; }
532
547
  .logs-table-card { overflow-x: auto; }
533
548
  .logs-table-card table { min-width: 860px; }
549
+ /* The usage period: a segmented control, and two date fields beside it for a custom range. */
550
+ .logs-period-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: 0 0 10px; }
551
+ .logs-custom { display: inline-flex; align-items: center; gap: 6px; }
552
+ .logs-custom input { width: auto; min-height: 28px; padding: 3px 8px; font-size: var(--fs-foot); font-variant-numeric: tabular-nums; }
553
+ .logs-custom-dash { color: var(--label-3); }
554
+ #models-table { min-width: 600px; }
534
555
 
535
556
  table { width: 100%; border-collapse: collapse; font-size: var(--fs-sub); }
536
557
  th { padding: 10px 12px 8px; color: var(--label-2); font-size: var(--fs-foot); font-weight: 500; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--separator); }
@@ -854,6 +875,24 @@ a.cell { color: inherit; text-decoration: none; }
854
875
  #requests-table tr.request-detail td { display: block; }
855
876
  .request-detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
856
877
 
878
+ /* Seven periods do not fit one line on a phone: the control wraps and spans the width. */
879
+ .logs-period { display: flex; flex-wrap: wrap; width: 100%; }
880
+ .logs-period button { flex: 1 0 auto; padding: 3px 10px; }
881
+ .logs-custom { display: flex; width: 100%; }
882
+ .logs-custom input { flex: 1 1 0; min-width: 0; }
883
+ /* Model usage reads like the request list, each number carrying its column's name. */
884
+ #models-table { min-width: 0; }
885
+ #models-table thead { display: none; }
886
+ #models-table, #models-table tbody { display: block; }
887
+ #models-table tr { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 0; padding: 10px 16px; }
888
+ #models-table tr + tr::before { content: ""; position: absolute; top: 0; left: 16px; right: 0; height: 1px; background: var(--separator); }
889
+ #models-table td { display: block; padding: 0; border: 0; }
890
+ #models-table td.c-model { flex: 1 1 100%; min-width: 0; }
891
+ #models-table td.c-count, #models-table td.c-in, #models-table td.c-out, #models-table td.c-dur { color: var(--label-2); font-size: var(--fs-cap); white-space: nowrap; }
892
+ #models-table td[data-label]::before { content: attr(data-label) " "; color: var(--label-3); }
893
+ #models-table td.c-count::after, #models-table td.c-in::after, #models-table td.c-out::after { content: "·"; margin: 0 6px; color: var(--label-3); }
894
+ #models-table .request-models .model { max-width: 60vw; }
895
+
857
896
  .modal-backdrop { place-items: end center; padding: 0; }
858
897
  .modal { width: 100%; max-height: calc(100vh - 28px); border-radius: 14px 14px 0 0; padding-bottom: env(safe-area-inset-bottom); }
859
898
  #modal-content { padding: 20px 16px 20px; }