clauderipple 0.10.0 → 0.11.1
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 +59 -0
- package/README.es.md +45 -0
- package/README.ja.md +17 -0
- package/README.ko.md +31 -0
- package/README.md +39 -0
- package/README.pt-BR.md +45 -0
- package/README.zh-CN.md +17 -0
- package/dist/app/assets/icon.icns +0 -0
- package/dist/app/assets/icon.ico +0 -0
- package/dist/app/assets/icon.png +0 -0
- package/dist/app/assets/trayDirectTemplate.png +0 -0
- package/dist/app/assets/trayDirectTemplate@2x.png +0 -0
- package/dist/app/assets/trayDownTemplate.png +0 -0
- package/dist/app/assets/trayDownTemplate@2x.png +0 -0
- package/dist/app/assets/trayTemplate.png +0 -0
- package/dist/app/assets/trayTemplate@2x.png +0 -0
- package/dist/app/assets/trayWarnTemplate.png +0 -0
- package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
- package/dist/app/assets/trayWin.png +0 -0
- package/dist/app/assets/trayWin@2x.png +0 -0
- package/dist/app/assets/trayWinDirect.png +0 -0
- package/dist/app/assets/trayWinDirect@2x.png +0 -0
- package/dist/app/assets/trayWinDown.png +0 -0
- package/dist/app/assets/trayWinDown@2x.png +0 -0
- package/dist/app/assets/trayWinWarn.png +0 -0
- package/dist/app/assets/trayWinWarn@2x.png +0 -0
- package/dist/app/dist/claude-desktop.js +67 -0
- package/dist/app/dist/main.js +423 -110
- package/dist/app/dist/preload.js +21 -0
- package/dist/app/dist/protocol.js +121 -0
- package/dist/app/dist/status-model.js +87 -0
- package/dist/app/dist/window.js +140 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/awake-setup.js +314 -0
- package/dist/cli/src/bundle.js +9 -5
- package/dist/cli/src/direct.js +159 -0
- package/dist/cli/src/doctor.js +205 -0
- package/dist/cli/src/index.js +184 -31
- package/dist/cli/src/launchd.js +29 -20
- package/dist/cli/src/launcher.js +215 -0
- package/dist/cli/src/power.js +182 -0
- package/dist/cli/src/registration.js +157 -0
- package/dist/cli/src/schtasks.js +2 -0
- package/dist/cli/src/settings.js +24 -0
- package/dist/cli/src/tray.js +5 -2
- package/dist/mod/hooks/i18n.ts +237 -237
- package/dist/mod/hooks/register.tsx +4 -5
- package/dist/mod/hooks/session.ts +6 -2
- package/dist/mod/hooks/ui/agents.tsx +81 -36
- package/dist/mod/hooks/ui/view.ts +8 -5
- package/dist/mod/tests/ui.test.tsx +46 -13
- package/dist/mod/types/index.d.ts +0 -2
- package/dist/remote/src/bridge-client.js +45 -4
- package/dist/router/src/admin.js +105 -2
- package/dist/router/src/awake-win.js +590 -0
- package/dist/router/src/awake.js +432 -0
- package/dist/router/src/config.js +64 -0
- package/dist/router/src/health.js +86 -3
- package/dist/router/src/hold.js +107 -0
- package/dist/router/src/index.js +69 -6
- package/dist/router/src/ingress/server.js +26 -4
- package/dist/router/src/power.js +107 -0
- package/dist/router/src/providers/google/antigravity.js +73 -10
- package/dist/router/src/providers/google/index.js +168 -17
- package/dist/router/src/providers/google/thoughtSignatures.js +13 -0
- package/dist/router/src/providers/google/transport.js +1 -1
- package/dist/router/src/providers/google/usage.js +71 -26
- package/dist/router/src/providers/retry.js +4 -1
- package/dist/router/src/proxy.js +50 -21
- package/dist/router/src/tether-win.js +211 -0
- package/dist/router/src/tether.js +712 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +649 -2
- package/dist/ui/i18n.js +494 -77
- package/dist/ui/index.html +9 -1
- package/dist/ui/style.css +138 -0
- package/docs/ARCHITECTURE.md +294 -6
- package/package.json +2 -2
package/dist/app/dist/main.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// ClaudeRipple menu-bar app.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// ClaudeRipple menu-bar app. The router owns the logic and the admin API on 127.0.0.1; this shell
|
|
3
|
+
// adds a tray icon with live health, the dashboard window, and shortcuts for restart / logs / login.
|
|
4
|
+
//
|
|
5
|
+
// The window draws the dashboard from the app's own copy of packages/ui (protocol.ts) and only asks
|
|
6
|
+
// the router for data, so it opens with the router dead — which is when it is needed: on
|
|
7
|
+
// 2026-10-20 a broken launch entry took the router down, and the router-served dashboard with it,
|
|
8
|
+
// and nothing was left to fix it from. A preload bridge lets the dashboard repair the router or
|
|
9
|
+
// switch Claude to its default connection through the CLI (`doctor` / `repair` / `direct`).
|
|
5
10
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
11
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
12
|
};
|
|
@@ -11,6 +16,10 @@ const node_child_process_1 = require("node:child_process");
|
|
|
11
16
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
17
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
18
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const claude_desktop_1 = require("./claude-desktop");
|
|
20
|
+
const protocol_1 = require("./protocol");
|
|
21
|
+
const status_model_1 = require("./status-model");
|
|
22
|
+
const window_1 = require("./window");
|
|
14
23
|
const home = process.env.CLAUDERIPPLE_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), ".clauderipple");
|
|
15
24
|
const POLL_MS = 5000;
|
|
16
25
|
// ---- i18n: tray menu, dialogs, tooltips -------------------------------------------------
|
|
@@ -65,11 +74,17 @@ const STRINGS = {
|
|
|
65
74
|
proxyLine: (host, port, slots, flight) => `Proxy ${host}:${port} · ${slots} · ${flight}`,
|
|
66
75
|
startAtLogin: "Start at login",
|
|
67
76
|
downNotifyTitle: "Claude Desktop cannot connect",
|
|
68
|
-
downNotifyBody: "The ClaudeRipple router is not running, so Claude Desktop has no way out. Click to start it.",
|
|
69
77
|
starting: "starting the router…",
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
open: "Open ClaudeRipple…",
|
|
79
|
+
directHeadline: "Claude's default connection (Ripple off)",
|
|
80
|
+
tooltipDirect: "ClaudeRipple: using Claude's default connection",
|
|
81
|
+
notifyDownTitle: "Ripple is not responding",
|
|
82
|
+
notifyDownBody: "Claude cannot reach AI right now. Repair Ripple, or switch to Claude's default connection.",
|
|
83
|
+
actRepair: "Repair",
|
|
84
|
+
actDirectOn: "Use Claude's default connection",
|
|
85
|
+
restartConfirm: "Quit and reopen the Claude app?",
|
|
86
|
+
restartConfirmDetail: "Open conversations and any work in progress in the Claude app will stop.",
|
|
87
|
+
restartNow: "Reopen",
|
|
73
88
|
setupNeeded: "setup not finished",
|
|
74
89
|
setupNeededDetail: "ClaudeRipple is installed but not set up yet",
|
|
75
90
|
runSetup: "Finish setting up ClaudeRipple…",
|
|
@@ -94,78 +109,84 @@ const STRINGS = {
|
|
|
94
109
|
ko: {
|
|
95
110
|
healthy: "정상",
|
|
96
111
|
attentionNeeded: "확인 필요",
|
|
97
|
-
routerNotRunning: "
|
|
98
|
-
slotsMapped: (n) => `슬롯 ${n}개
|
|
112
|
+
routerNotRunning: "리플 꺼짐",
|
|
113
|
+
slotsMapped: (n) => `슬롯 ${n}개 연결`,
|
|
99
114
|
inFlight: "진행 중",
|
|
100
115
|
connected: "Claude Desktop 연결됨",
|
|
101
116
|
notConnected: "Claude Desktop 연결 안 됨",
|
|
102
|
-
percentOfWeekUsed: (percent, plan) => `ChatGPT ${plan}
|
|
117
|
+
percentOfWeekUsed: (percent, plan) => `ChatGPT ${plan} 이번 주 ${percent}% 사용`,
|
|
103
118
|
cliVersion: (v) => `Claude Code CLI ${v}`,
|
|
104
|
-
restartRouter: "
|
|
105
|
-
startRouter: "
|
|
106
|
-
pickerOn: "Code 탭 피커에 GPT 모델
|
|
107
|
-
pickerOff: "
|
|
108
|
-
pickerState: (on) => (on ? "피커: GPT
|
|
109
|
-
pickerOnConfirm: "ClaudeRipple 인증서를
|
|
110
|
-
pickerDone: "
|
|
111
|
-
pickerOffDone: "
|
|
119
|
+
restartRouter: "리플 재시작",
|
|
120
|
+
startRouter: "리플 시작",
|
|
121
|
+
pickerOn: "Code 탭 피커에 GPT 모델 표시…",
|
|
122
|
+
pickerOff: "피커에서 GPT 모델 숨기기…",
|
|
123
|
+
pickerState: (on) => (on ? "피커: GPT 모델 이름 표시 중 ✓" : "피커: Claude 이름으로 GPT 사용 (별칭 모드)"),
|
|
124
|
+
pickerOnConfirm: "ClaudeRipple 인증서를 신뢰하려면 macOS 로그인 암호를 입력해야 합니다(ClaudeRipple은 암호를 보지 않습니다). 완료 후 Claude Desktop을 완전히 종료했다가 다시 열어 주십시오.",
|
|
125
|
+
pickerDone: "완료되었습니다. Claude Desktop을 완전히 종료했다가 다시 연 뒤 Code 탭 피커를 확인해 주십시오.",
|
|
126
|
+
pickerOffDone: "완료되었습니다. Claude Desktop을 완전히 종료했다가 다시 열면 적용됩니다.",
|
|
112
127
|
cancel: "취소",
|
|
113
128
|
agentTitleOn: "서브에이전트에 모델 이름 표시",
|
|
114
|
-
agentTitleOff: "서브에이전트 모델 이름
|
|
115
|
-
agentTitleDone: (on) => (on ? "켰습니다. 새로
|
|
129
|
+
agentTitleOff: "서브에이전트 모델 이름 숨기기",
|
|
130
|
+
agentTitleDone: (on) => (on ? "켰습니다. 새로 시작하는 서브에이전트 제목이 \"Terra·high · …\"처럼 표시됩니다." : "껐습니다."),
|
|
116
131
|
signInChatgpt: "ChatGPT 로그인…",
|
|
117
132
|
connectClaudeSubscription: "Claude 구독 연결…",
|
|
118
133
|
copyStatus: "상태 복사",
|
|
119
134
|
showLogs: "로그 보기",
|
|
120
135
|
rerunSetup: "ClaudeRipple 설정 다시 실행…",
|
|
121
|
-
setupTitle: "ClaudeRipple
|
|
122
|
-
setupPrompt: "로컬
|
|
136
|
+
setupTitle: "ClaudeRipple 설정",
|
|
137
|
+
setupPrompt: "로컬 인증서를 만들고, Claude Code 설정(~/.claude/settings.json)을 연결하고, 리플이 백그라운드에서 돌도록 등록합니다. 계속하시겠습니까?",
|
|
123
138
|
setupLater: "나중에",
|
|
124
139
|
setupContinue: "계속",
|
|
125
140
|
setupDone: "ClaudeRipple 설정이 완료되었습니다.",
|
|
126
|
-
setupFailed: "설정을
|
|
141
|
+
setupFailed: "ClaudeRipple 설정을 완료하지 못했습니다",
|
|
127
142
|
updatedTitle: (v) => `ClaudeRipple ${v}`,
|
|
128
|
-
updatedRestarting: (from) =>
|
|
129
|
-
updatedFailed: "
|
|
130
|
-
problem: (code) => code === "settings" ? "Claude Code가 ClaudeRipple
|
|
131
|
-
: code === "upstream" ? "Anthropic
|
|
132
|
-
: code === "picker-ca" ? "피커 모드: 인증서
|
|
133
|
-
: code === "picker-proxy" ? "피커 모드: Claude Desktop 프록시
|
|
143
|
+
updatedRestarting: (from) => `현재 실행 중인 리플은 ${from} 버전입니다. 새 버전으로 전환합니다…`,
|
|
144
|
+
updatedFailed: "리플을 새 버전으로 전환하지 못했습니다",
|
|
145
|
+
problem: (code) => code === "settings" ? "Claude Code가 ClaudeRipple에 연결되지 않음"
|
|
146
|
+
: code === "upstream" ? "Anthropic 연결 안 됨"
|
|
147
|
+
: code === "picker-ca" ? "피커 모드: 인증서 신뢰 필요"
|
|
148
|
+
: code === "picker-proxy" ? "피커 모드: Claude Desktop 프록시 설정 필요"
|
|
134
149
|
: code.startsWith("provider:") ? `프로바이더 ${code.slice(9)} 연결 안 됨`
|
|
135
150
|
: code,
|
|
136
151
|
about: "ClaudeRipple 정보",
|
|
137
|
-
aboutDetail: "Claude Desktop을 끄지 않고 그 안에서 GPT 등
|
|
152
|
+
aboutDetail: "Claude Desktop을 끄지 않고 그 안에서 GPT 등 다양한 모델을 함께 씁니다.\n\n독립 오픈소스 프로젝트(GPL-3.0)이며 Anthropic·OpenAI와 제휴·보증·후원 관계가 없습니다. Claude와 Claude Code는 Anthropic, PBC의 상표입니다.",
|
|
138
153
|
quit: "종료",
|
|
139
154
|
tooltip: (state) => `ClaudeRipple ${state}`,
|
|
140
|
-
tooltipDown: "ClaudeRipple:
|
|
155
|
+
tooltipDown: "ClaudeRipple: 리플이 꺼져 있어 Claude Desktop이 연결되지 않습니다",
|
|
141
156
|
proxyLine: (host, port, slots, flight) => `프록시 ${host}:${port} · ${slots} · ${flight}`,
|
|
142
157
|
startAtLogin: "로그인 시 자동 시작",
|
|
143
158
|
downNotifyTitle: "Claude Desktop이 연결되지 않습니다",
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
159
|
+
starting: "리플을 시작하는 중…",
|
|
160
|
+
open: "ClaudeRipple 열기…",
|
|
161
|
+
directHeadline: "Claude 기본 연결 사용 중 (리플 꺼짐)",
|
|
162
|
+
tooltipDirect: "ClaudeRipple: Claude 기본 연결 사용 중",
|
|
163
|
+
notifyDownTitle: "리플이 응답하지 않습니다",
|
|
164
|
+
notifyDownBody: "지금 Claude가 AI에 연결되지 않습니다. 리플을 복구하거나 Claude 기본 연결로 전환할 수 있습니다.",
|
|
165
|
+
actRepair: "복구",
|
|
166
|
+
actDirectOn: "Claude 기본 연결로 전환",
|
|
167
|
+
restartConfirm: "Claude 앱을 종료했다가 다시 여시겠습니까?",
|
|
168
|
+
restartConfirmDetail: "Claude 앱의 열린 대화와 진행 중인 작업이 중단됩니다.",
|
|
169
|
+
restartNow: "다시 열기",
|
|
170
|
+
setupNeeded: "설정 필요",
|
|
171
|
+
setupNeededDetail: "ClaudeRipple이 설치되었지만 아직 설정이 완료되지 않았습니다.",
|
|
172
|
+
runSetup: "ClaudeRipple 설정 완료하기…",
|
|
152
173
|
claudeCurrent: "현재 로그인",
|
|
153
174
|
claudeUsage: (label, windows) => `Claude ${label} · ${windows}`,
|
|
154
175
|
window5h: "5시간",
|
|
155
176
|
windowWeek: "주간",
|
|
156
177
|
checkUpdates: "업데이트 확인…",
|
|
157
|
-
updateAvailableItem: (v) =>
|
|
158
|
-
upToDate: (v) => `최신
|
|
159
|
-
updateAvailable: (latest, current) => `새 버전이 나왔습니다: ClaudeRipple ${latest} (
|
|
160
|
-
updateDetail: "진행 중인 요청이 끝나면
|
|
178
|
+
updateAvailableItem: (v) => `ClaudeRipple ${v} 버전으로 업데이트…`,
|
|
179
|
+
upToDate: (v) => `최신 버전입니다 (ClaudeRipple ${v})`,
|
|
180
|
+
updateAvailable: (latest, current) => `새 버전이 나왔습니다: ClaudeRipple ${latest} (현재 ${current})`,
|
|
181
|
+
updateDetail: "진행 중인 요청이 끝나면 리플이 새 버전으로 다시 시작됩니다. 트레이 메뉴는 자동으로 다시 열립니다.",
|
|
161
182
|
updateNow: "업데이트",
|
|
162
183
|
later: "나중에",
|
|
163
184
|
updating: (v) => `ClaudeRipple ${v} 업데이트 중…`,
|
|
164
185
|
updateDone: (v) => `ClaudeRipple ${v} 업데이트 완료`,
|
|
165
|
-
updateFailed: "업데이트를
|
|
166
|
-
updateCheckFailed: "
|
|
167
|
-
updatePackaged: "
|
|
168
|
-
updateCheckout: "소스
|
|
186
|
+
updateFailed: "업데이트를 완료하지 못했습니다",
|
|
187
|
+
updateCheckFailed: "업데이트 정보를 확인하지 못했습니다",
|
|
188
|
+
updatePackaged: "설치형 앱입니다. 릴리스 페이지에서 최신 버전을 다운로드해 주십시오.",
|
|
189
|
+
updateCheckout: "소스 코드로 실행 중입니다. git pull 명령으로 업데이트해 주십시오.",
|
|
169
190
|
},
|
|
170
191
|
zh: {
|
|
171
192
|
healthy: "运行正常",
|
|
@@ -217,11 +238,17 @@ const STRINGS = {
|
|
|
217
238
|
proxyLine: (host, port, slots, flight) => `代理 ${host}:${port} · ${slots} · ${flight}`,
|
|
218
239
|
startAtLogin: "登录时启动",
|
|
219
240
|
downNotifyTitle: "Claude Desktop 无法连接",
|
|
220
|
-
downNotifyBody: "ClaudeRipple 路由服务未运行,因此 Claude Desktop 无法向外发送请求。点击即可启动路由服务。",
|
|
221
241
|
starting: "正在启动路由服务…",
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
open: "打开 ClaudeRipple…",
|
|
243
|
+
directHeadline: "正在使用 Claude 默认连接(Ripple 已关闭)",
|
|
244
|
+
tooltipDirect: "ClaudeRipple:正在使用 Claude 默认连接",
|
|
245
|
+
notifyDownTitle: "Ripple 没有响应",
|
|
246
|
+
notifyDownBody: "Claude 现在无法连接 AI。可以修复 Ripple,或切换到 Claude 默认连接。",
|
|
247
|
+
actRepair: "修复",
|
|
248
|
+
actDirectOn: "切换到 Claude 默认连接",
|
|
249
|
+
restartConfirm: "要退出并重新打开 Claude 应用吗?",
|
|
250
|
+
restartConfirmDetail: "Claude 应用中打开的对话和进行中的工作会停止。",
|
|
251
|
+
restartNow: "重新打开",
|
|
225
252
|
setupNeeded: "设置未完成",
|
|
226
253
|
setupNeededDetail: "ClaudeRipple 已安装,但尚未完成设置",
|
|
227
254
|
runSetup: "完成 ClaudeRipple 设置…",
|
|
@@ -293,11 +320,17 @@ const STRINGS = {
|
|
|
293
320
|
proxyLine: (host, port, slots, flight) => `プロキシ ${host}:${port} · ${slots} · ${flight}`,
|
|
294
321
|
startAtLogin: "ログイン時に起動",
|
|
295
322
|
downNotifyTitle: "Claude Desktopが接続できません",
|
|
296
|
-
downNotifyBody: "ClaudeRippleのルーターが停止しているため、Claude Desktopは外部と通信できません。クリックして起動してください。",
|
|
297
323
|
starting: "ルーターを起動中…",
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
324
|
+
open: "ClaudeRippleを開く…",
|
|
325
|
+
directHeadline: "Claude の標準接続を使用中(Ripple はオフ)",
|
|
326
|
+
tooltipDirect: "ClaudeRipple:Claude の標準接続を使用中",
|
|
327
|
+
notifyDownTitle: "Ripple が応答しません",
|
|
328
|
+
notifyDownBody: "現在 Claude は AI に接続できません。Ripple を修復するか、Claude の標準接続に切り替えられます。",
|
|
329
|
+
actRepair: "修復",
|
|
330
|
+
actDirectOn: "Claude の標準接続に切り替え",
|
|
331
|
+
restartConfirm: "Claude アプリを終了して開き直しますか?",
|
|
332
|
+
restartConfirmDetail: "Claude アプリで開いている会話や進行中の作業は止まります。",
|
|
333
|
+
restartNow: "開き直す",
|
|
301
334
|
setupNeeded: "セットアップ未完了",
|
|
302
335
|
setupNeededDetail: "ClaudeRippleはインストール済みですが、まだセットアップされていません",
|
|
303
336
|
runSetup: "ClaudeRippleのセットアップを完了…",
|
|
@@ -369,11 +402,17 @@ const STRINGS = {
|
|
|
369
402
|
proxyLine: (host, port, slots, flight) => `Proxy ${host}:${port} · ${slots} · ${flight}`,
|
|
370
403
|
startAtLogin: "Abrir al iniciar sesión",
|
|
371
404
|
downNotifyTitle: "Claude Desktop no puede conectarse",
|
|
372
|
-
downNotifyBody: "El router de ClaudeRipple no está en ejecución, así que Claude Desktop no puede conectarse. Haz clic para iniciarlo.",
|
|
373
405
|
starting: "iniciando el router…",
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
406
|
+
open: "Abrir ClaudeRipple…",
|
|
407
|
+
directHeadline: "Conexión predeterminada de Claude (Ripple apagado)",
|
|
408
|
+
tooltipDirect: "ClaudeRipple: usando la conexión predeterminada de Claude",
|
|
409
|
+
notifyDownTitle: "Ripple no responde",
|
|
410
|
+
notifyDownBody: "Claude no puede conectarse a la IA ahora mismo. Repara Ripple o usa la conexión predeterminada de Claude.",
|
|
411
|
+
actRepair: "Reparar",
|
|
412
|
+
actDirectOn: "Usar la conexión predeterminada de Claude",
|
|
413
|
+
restartConfirm: "¿Cerrar y volver a abrir la app de Claude?",
|
|
414
|
+
restartConfirmDetail: "Las conversaciones abiertas y el trabajo en curso en la app de Claude se detendrán.",
|
|
415
|
+
restartNow: "Reabrir",
|
|
377
416
|
setupNeeded: "configuración pendiente",
|
|
378
417
|
setupNeededDetail: "ClaudeRipple está instalado, pero aún no está configurado",
|
|
379
418
|
runSetup: "Terminar de configurar ClaudeRipple…",
|
|
@@ -445,11 +484,17 @@ const STRINGS = {
|
|
|
445
484
|
proxyLine: (host, port, slots, flight) => `Proxy ${host}:${port} · ${slots} · ${flight}`,
|
|
446
485
|
startAtLogin: "Iniciar ao fazer login",
|
|
447
486
|
downNotifyTitle: "O Claude Desktop não consegue se conectar",
|
|
448
|
-
downNotifyBody: "O roteador do ClaudeRipple não está rodando, então o Claude Desktop não consegue acessar a rede. Clique para iniciá-lo.",
|
|
449
487
|
starting: "iniciando o roteador…",
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
488
|
+
open: "Abrir o ClaudeRipple…",
|
|
489
|
+
directHeadline: "Conexão padrão do Claude (Ripple desligado)",
|
|
490
|
+
tooltipDirect: "ClaudeRipple: usando a conexão padrão do Claude",
|
|
491
|
+
notifyDownTitle: "O Ripple não está respondendo",
|
|
492
|
+
notifyDownBody: "O Claude não consegue acessar a IA agora. Repare o Ripple ou use a conexão padrão do Claude.",
|
|
493
|
+
actRepair: "Reparar",
|
|
494
|
+
actDirectOn: "Usar a conexão padrão do Claude",
|
|
495
|
+
restartConfirm: "Encerrar e reabrir o app do Claude?",
|
|
496
|
+
restartConfirmDetail: "As conversas abertas e o trabalho em andamento no app do Claude vão parar.",
|
|
497
|
+
restartNow: "Reabrir",
|
|
453
498
|
setupNeeded: "configuração não concluída",
|
|
454
499
|
setupNeededDetail: "O ClaudeRipple está instalado, mas ainda não foi configurado",
|
|
455
500
|
runSetup: "Concluir a configuração do ClaudeRipple…",
|
|
@@ -489,6 +534,8 @@ let lastError = null;
|
|
|
489
534
|
let starting = false;
|
|
490
535
|
let downSince = null;
|
|
491
536
|
let downNotified = false;
|
|
537
|
+
/** Whether the last health poll got an answer; null before the first poll. */
|
|
538
|
+
let answered = null;
|
|
492
539
|
// Long enough that a router still coming up after login does not fire a notification.
|
|
493
540
|
const DOWN_NOTIFY_AFTER_MS = 20_000;
|
|
494
541
|
// ---- login item ------------------------------------------------------------------------
|
|
@@ -539,7 +586,16 @@ function setLoginItem(on) {
|
|
|
539
586
|
}
|
|
540
587
|
writeAppState({ loginItemInitialized: true });
|
|
541
588
|
}
|
|
589
|
+
// Overrides for running this app against stand-ins instead of the real installation (tests, and
|
|
590
|
+
// trying a change without touching the router someone depends on):
|
|
591
|
+
// CLAUDERIPPLE_APP_ADMIN_URL the admin API base the health poll and the dashboard use
|
|
592
|
+
// CLAUDERIPPLE_APP_CLI a CLI script to run instead of the installed one
|
|
593
|
+
// CLAUDERIPPLE_APP_USER_DATA Electron's data folder, which also keys the one-tray lock
|
|
594
|
+
// CLAUDERIPPLE_HOME (logs, app.json, config.json) is honoured as everywhere else.
|
|
542
595
|
function adminUrl() {
|
|
596
|
+
const override = process.env.CLAUDERIPPLE_APP_ADMIN_URL;
|
|
597
|
+
if (override)
|
|
598
|
+
return override.endsWith("/") ? override : `${override}/`;
|
|
543
599
|
return `http://127.0.0.1:${readConfigPorts().admin}/`;
|
|
544
600
|
}
|
|
545
601
|
/**
|
|
@@ -554,11 +610,8 @@ function uiLang() {
|
|
|
554
610
|
const base = first.toLowerCase().split(/[-_]/)[0];
|
|
555
611
|
return base in STRINGS ? base : "en";
|
|
556
612
|
}
|
|
557
|
-
/** The GUI follows the OS language by default; a language the user picked in the GUI itself wins (it is stored in the page). */
|
|
558
|
-
function guiUrl() {
|
|
559
|
-
return `${adminUrl()}?lang=${uiLang()}`;
|
|
560
|
-
}
|
|
561
613
|
async function poll() {
|
|
614
|
+
const before = answered;
|
|
562
615
|
try {
|
|
563
616
|
const res = await fetch(`${adminUrl()}api/status`, { signal: AbortSignal.timeout(3000) });
|
|
564
617
|
if (!res.ok)
|
|
@@ -567,27 +620,57 @@ async function poll() {
|
|
|
567
620
|
lastError = null;
|
|
568
621
|
downSince = null;
|
|
569
622
|
downNotified = false;
|
|
623
|
+
answered = true;
|
|
570
624
|
void reconcileRouter(last);
|
|
571
625
|
}
|
|
572
626
|
catch (e) {
|
|
573
627
|
last = null;
|
|
574
628
|
lastError = e.message;
|
|
629
|
+
answered = false;
|
|
575
630
|
downSince ??= Date.now();
|
|
576
|
-
// An update restarts the router on purpose; that is not
|
|
577
|
-
|
|
631
|
+
// An update or one of the window's own actions restarts the router on purpose; that is not
|
|
632
|
+
// the outage the notification is for. Neither is the direct mode, where the router being off
|
|
633
|
+
// is the point — so doctor is asked first, and once per outage. With the window on screen the
|
|
634
|
+
// user is already looking at the same news.
|
|
635
|
+
if ((0, status_model_1.shouldNotifyDown)({ downForMs: Date.now() - downSince, graceMs: DOWN_NOTIFY_AFTER_MS, alreadyNotified: downNotified, updating, busy: !!rec.busy || (0, window_1.windowVisible)(), mode: rec.doctor?.mode ?? null })) {
|
|
578
636
|
downNotified = true;
|
|
579
|
-
|
|
637
|
+
void refreshDoctor().then(() => {
|
|
638
|
+
if (!last && rec.doctor?.mode !== "direct")
|
|
639
|
+
notifyDown();
|
|
640
|
+
});
|
|
580
641
|
}
|
|
581
642
|
}
|
|
643
|
+
// Up or down changed: what doctor said about the rest (direct, mixed) may have changed with it.
|
|
644
|
+
if (before !== null && before !== answered && !rec.busy)
|
|
645
|
+
void refreshDoctor();
|
|
582
646
|
render();
|
|
583
647
|
}
|
|
584
|
-
|
|
648
|
+
// Held so the handlers are not collected with it before the user gets to the notification.
|
|
649
|
+
let downNotification = null;
|
|
650
|
+
/** The router being down means Claude is silent, so say so out loud, with the two ways out. */
|
|
585
651
|
function notifyDown() {
|
|
586
|
-
if (!electron_1.Notification.isSupported())
|
|
652
|
+
if (!electron_1.Notification.isSupported()) {
|
|
653
|
+
// Nothing else would tell the user; the window does.
|
|
654
|
+
showWindow("health");
|
|
587
655
|
return;
|
|
588
|
-
|
|
589
|
-
n
|
|
656
|
+
}
|
|
657
|
+
const n = new electron_1.Notification({
|
|
658
|
+
title: L.notifyDownTitle,
|
|
659
|
+
body: L.notifyDownBody,
|
|
660
|
+
icon: notifyIcon(),
|
|
661
|
+
// macOS shows these as buttons on the notification (an Options menu when there are two).
|
|
662
|
+
actions: [
|
|
663
|
+
{ type: "button", text: L.actRepair },
|
|
664
|
+
{ type: "button", text: L.actDirectOn },
|
|
665
|
+
],
|
|
666
|
+
});
|
|
667
|
+
n.on("click", () => showWindow("health"));
|
|
668
|
+
n.on("action", (_event, index) => {
|
|
669
|
+
showWindow("health");
|
|
670
|
+
void act(index === 0 ? "repair" : "direct-on");
|
|
671
|
+
});
|
|
590
672
|
n.show();
|
|
673
|
+
downNotification = n;
|
|
591
674
|
}
|
|
592
675
|
/** Idempotent: `clauderipple start` leaves a router that is already coming up alone. */
|
|
593
676
|
async function startRouter() {
|
|
@@ -618,33 +701,28 @@ const isMac = process.platform === "darwin";
|
|
|
618
701
|
* state read as a colour rather than only a shape.
|
|
619
702
|
*/
|
|
620
703
|
function icon(state) {
|
|
621
|
-
const mac = { ok: "trayTemplate.png", warn: "trayWarnTemplate.png", down: "trayDownTemplate.png" };
|
|
622
|
-
const win = { ok: "trayWin.png", warn: "trayWinWarn.png", down: "trayWinDown.png" };
|
|
704
|
+
const mac = { ok: "trayTemplate.png", warn: "trayWarnTemplate.png", down: "trayDownTemplate.png", direct: "trayDirectTemplate.png" };
|
|
705
|
+
const win = { ok: "trayWin.png", warn: "trayWinWarn.png", down: "trayWinDown.png", direct: "trayWinDirect.png" };
|
|
623
706
|
const img = electron_1.nativeImage.createFromPath(node_path_1.default.join(__dirname, "..", "assets", (isMac ? mac : win)[state]));
|
|
624
707
|
if (isMac)
|
|
625
708
|
img.setTemplateImage(true);
|
|
626
709
|
return img;
|
|
627
710
|
}
|
|
711
|
+
/** The app icon on notifications: macOS uses the bundle's own; elsewhere Electron would show its default. */
|
|
712
|
+
function notifyIcon() {
|
|
713
|
+
return isMac ? undefined : node_path_1.default.join(__dirname, "..", "assets", "icon.png");
|
|
714
|
+
}
|
|
628
715
|
/**
|
|
629
|
-
* The dashboard
|
|
630
|
-
* the
|
|
631
|
-
*
|
|
716
|
+
* The dashboard window. It opens with the router down too (its page is the app's own), and then on
|
|
717
|
+
* Status, where the way back is. Before setup there is no router to ask anything of, so the step
|
|
718
|
+
* that is actually missing comes first.
|
|
632
719
|
*/
|
|
633
720
|
async function openDashboard() {
|
|
634
|
-
if (last) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
// Before setup there is no router to start, so offer the step that is actually missing.
|
|
639
|
-
const message = needsSetup() ? (await setup(), "") : await startRouter();
|
|
640
|
-
// startRouter() returns once the CLI has finished; ask the router itself rather than waiting
|
|
641
|
-
// for the next tick, since the user is standing in front of the menu.
|
|
642
|
-
await poll();
|
|
643
|
-
if (last) {
|
|
644
|
-
void electron_1.shell.openExternal(guiUrl());
|
|
645
|
-
return;
|
|
721
|
+
if (!last && needsSetup()) {
|
|
722
|
+
await setup();
|
|
723
|
+
await poll();
|
|
646
724
|
}
|
|
647
|
-
|
|
725
|
+
showWindow(last ? undefined : "health");
|
|
648
726
|
}
|
|
649
727
|
// ---- update handling -------------------------------------------------------------------
|
|
650
728
|
// Installing a new version replaces files, not the process: the supervisor started the router at
|
|
@@ -690,7 +768,7 @@ async function reconcileRouter(s) {
|
|
|
690
768
|
reconciled = true;
|
|
691
769
|
const from = `${s.version}${s.runtime?.router ? ` (${s.runtime.router})` : ""}`;
|
|
692
770
|
if (electron_1.Notification.isSupported())
|
|
693
|
-
new electron_1.Notification({ title: L.updatedTitle(electron_1.app.getVersion()), body: L.updatedRestarting(from) }).show();
|
|
771
|
+
new electron_1.Notification({ title: L.updatedTitle(electron_1.app.getVersion()), body: L.updatedRestarting(from), icon: notifyIcon() }).show();
|
|
694
772
|
const outputs = [];
|
|
695
773
|
if (installedElsewhere || runningElsewhere)
|
|
696
774
|
outputs.push(await runCli(["install"]));
|
|
@@ -721,6 +799,9 @@ function packagedRuntime() {
|
|
|
721
799
|
}
|
|
722
800
|
/** Prefer this app's bundled Electron runtime; existing installations retain their recorded runtime. */
|
|
723
801
|
function cliPaths() {
|
|
802
|
+
const override = process.env.CLAUDERIPPLE_APP_CLI;
|
|
803
|
+
if (override)
|
|
804
|
+
return { node: process.execPath, env: { ELECTRON_RUN_AS_NODE: "1" }, cli: override };
|
|
724
805
|
const packaged = packagedRuntime();
|
|
725
806
|
if (packaged && node_fs_1.default.existsSync(packaged.cli))
|
|
726
807
|
return packaged;
|
|
@@ -743,15 +824,204 @@ function cliPaths() {
|
|
|
743
824
|
function runCli(args) {
|
|
744
825
|
return runCliResult(args).then((r) => r.output);
|
|
745
826
|
}
|
|
746
|
-
|
|
827
|
+
/**
|
|
828
|
+
* Runs the CLI without blocking anything. `stdout` is kept apart for the `--json` commands, whose
|
|
829
|
+
* one object must not be mixed with warnings on stderr. With `timeoutMs` a CLI that hangs (a
|
|
830
|
+
* supervisor that never answers) is ended rather than left holding a button down.
|
|
831
|
+
*/
|
|
832
|
+
function runCliResult(args, timeoutMs = 0) {
|
|
747
833
|
const { node, env, cli } = cliPaths();
|
|
748
834
|
return new Promise((resolve) => {
|
|
749
835
|
// An update prints npm's and the installer's own lines; the default 1MB buffer is not for that.
|
|
750
|
-
(0, node_child_process_1.execFile)(node, [cli, ...args], { env: { ...process.env, ...env, CLAUDERIPPLE_HOME: home }, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
751
|
-
|
|
836
|
+
(0, node_child_process_1.execFile)(node, [cli, ...args], { env: { ...process.env, ...env, CLAUDERIPPLE_HOME: home }, maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) => {
|
|
837
|
+
const timedOut = !!err && timeoutMs > 0 && err.killed === true;
|
|
838
|
+
resolve({ ok: !err, output: `${stdout}${stderr}${err ? `\n${err.message}` : ""}`.trim(), stdout: String(stdout), timedOut });
|
|
752
839
|
});
|
|
753
840
|
});
|
|
754
841
|
}
|
|
842
|
+
// ---- recovery: doctor, repair, direct -----------------------------------------------------
|
|
843
|
+
// The CLI does the work and answers in JSON; this keeps what it last said and what is running,
|
|
844
|
+
// and hands both to the dashboard (window.rippleApp) to draw its connection card.
|
|
845
|
+
const ACTION_TIMEOUT_MS = 40_000;
|
|
846
|
+
const DOCTOR_TIMEOUT_MS = 15_000;
|
|
847
|
+
/** Background check, mainly to notice the direct mode while the router is still answering. */
|
|
848
|
+
const DOCTOR_EVERY_MS = 5 * 60_000;
|
|
849
|
+
/** While the window is open it should not show something a minute old. */
|
|
850
|
+
const DOCTOR_WHILE_OPEN_MS = 10_000;
|
|
851
|
+
const LOG_LINES = 40;
|
|
852
|
+
const rec = {
|
|
853
|
+
doctor: null,
|
|
854
|
+
doctorFailed: false,
|
|
855
|
+
busy: null,
|
|
856
|
+
result: null,
|
|
857
|
+
restartClaude: false,
|
|
858
|
+
};
|
|
859
|
+
let doctorRun = null;
|
|
860
|
+
/** One `doctor` at a time; callers that arrive while one runs share it. */
|
|
861
|
+
function refreshDoctor() {
|
|
862
|
+
doctorRun ??= (async () => {
|
|
863
|
+
const r = await runCliResult(["doctor", "--json"], DOCTOR_TIMEOUT_MS);
|
|
864
|
+
const d = (0, status_model_1.parseJsonOutput)(r.stdout);
|
|
865
|
+
rec.doctor = d;
|
|
866
|
+
rec.doctorFailed = !d;
|
|
867
|
+
})().finally(() => {
|
|
868
|
+
doctorRun = null;
|
|
869
|
+
render();
|
|
870
|
+
});
|
|
871
|
+
return doctorRun;
|
|
872
|
+
}
|
|
873
|
+
/** The end of router.log, read from disk: it is there to look at precisely when the router is not. */
|
|
874
|
+
function logTail() {
|
|
875
|
+
const file = node_path_1.default.join(home, "logs", "router.log");
|
|
876
|
+
try {
|
|
877
|
+
const size = node_fs_1.default.statSync(file).size;
|
|
878
|
+
const length = Math.min(size, 16 * 1024);
|
|
879
|
+
const fd = node_fs_1.default.openSync(file, "r");
|
|
880
|
+
try {
|
|
881
|
+
const buf = Buffer.alloc(length);
|
|
882
|
+
node_fs_1.default.readSync(fd, buf, 0, length, size - length);
|
|
883
|
+
return (0, status_model_1.tailLines)(buf.toString("utf8"), LOG_LINES).map((l) => (l.length > 400 ? `${l.slice(0, 400)}…` : l));
|
|
884
|
+
}
|
|
885
|
+
finally {
|
|
886
|
+
node_fs_1.default.closeSync(fd);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
return [];
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/** What the dashboard's connection card is drawn from. */
|
|
894
|
+
function bridgeState() {
|
|
895
|
+
return {
|
|
896
|
+
doctor: rec.doctor,
|
|
897
|
+
doctorFailed: rec.doctorFailed,
|
|
898
|
+
routerAnswering: answered,
|
|
899
|
+
busy: rec.busy,
|
|
900
|
+
result: rec.result,
|
|
901
|
+
restartClaude: rec.restartClaude,
|
|
902
|
+
loginItem: loginItemOn(),
|
|
903
|
+
appVersion: electron_1.app.getVersion(),
|
|
904
|
+
platform: process.platform,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
let lastSent = "";
|
|
908
|
+
/** Tells the open window, if any, when the state changed (render() runs on every poll). */
|
|
909
|
+
function pushState() {
|
|
910
|
+
if (!(0, window_1.mainWindow)())
|
|
911
|
+
return;
|
|
912
|
+
const state = bridgeState();
|
|
913
|
+
const key = JSON.stringify(state);
|
|
914
|
+
if (key === lastSent)
|
|
915
|
+
return;
|
|
916
|
+
lastSent = key;
|
|
917
|
+
(0, window_1.sendToWindow)("ripple-app:state", state);
|
|
918
|
+
}
|
|
919
|
+
function showWindow(view) {
|
|
920
|
+
(0, window_1.openWindow)({
|
|
921
|
+
lang: uiLang(),
|
|
922
|
+
savedBounds: () => appState().window ?? null,
|
|
923
|
+
saveBounds: (bounds) => writeAppState({ window: bounds }),
|
|
924
|
+
shown: () => {
|
|
925
|
+
lastSent = "";
|
|
926
|
+
if (!rec.busy)
|
|
927
|
+
void refreshDoctor();
|
|
928
|
+
},
|
|
929
|
+
}, view);
|
|
930
|
+
}
|
|
931
|
+
const BRIDGE_ACTIONS = new Set(["repair", "direct-on", "direct-off", "restart-claude", "refresh", "login-item", "logs"]);
|
|
932
|
+
/** Only our own page may drive the bridge, never something it navigated to. */
|
|
933
|
+
function fromOurPage(event) {
|
|
934
|
+
return (event.senderFrame?.url ?? "").startsWith(`${protocol_1.ORIGIN}/`);
|
|
935
|
+
}
|
|
936
|
+
function wireBridge() {
|
|
937
|
+
electron_1.ipcMain.handle("ripple-app:state", (event) => (fromOurPage(event) ? bridgeState() : null));
|
|
938
|
+
electron_1.ipcMain.handle("ripple-app:log", (event) => (fromOurPage(event) ? logTail() : []));
|
|
939
|
+
electron_1.ipcMain.handle("ripple-app:act", async (event, id) => {
|
|
940
|
+
if (!fromOurPage(event) || typeof id !== "string" || !BRIDGE_ACTIONS.has(id))
|
|
941
|
+
return null;
|
|
942
|
+
await act(id);
|
|
943
|
+
return bridgeState();
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
async function act(id) {
|
|
947
|
+
switch (id) {
|
|
948
|
+
case "repair":
|
|
949
|
+
return runAction("repair", ["repair", "--json"]);
|
|
950
|
+
case "direct-on":
|
|
951
|
+
return runAction("direct-on", ["direct", "on", "--json"]);
|
|
952
|
+
case "direct-off":
|
|
953
|
+
return runAction("direct-off", ["direct", "off", "--json"]);
|
|
954
|
+
case "restart-claude":
|
|
955
|
+
return restartClaude();
|
|
956
|
+
case "logs":
|
|
957
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(home, "logs"), { recursive: true });
|
|
958
|
+
void electron_1.shell.openPath(node_path_1.default.join(home, "logs"));
|
|
959
|
+
return;
|
|
960
|
+
case "refresh":
|
|
961
|
+
if (rec.busy)
|
|
962
|
+
return;
|
|
963
|
+
rec.busy = "checking";
|
|
964
|
+
render();
|
|
965
|
+
await Promise.all([poll(), refreshDoctor()]);
|
|
966
|
+
rec.busy = null;
|
|
967
|
+
render();
|
|
968
|
+
return;
|
|
969
|
+
case "login-item":
|
|
970
|
+
setLoginItem(true);
|
|
971
|
+
render();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function runAction(action, args) {
|
|
976
|
+
if (rec.busy)
|
|
977
|
+
return;
|
|
978
|
+
rec.busy = action;
|
|
979
|
+
rec.result = null;
|
|
980
|
+
render();
|
|
981
|
+
const r = await runCliResult(args, ACTION_TIMEOUT_MS);
|
|
982
|
+
const out = (0, status_model_1.parseJsonOutput)(r.stdout);
|
|
983
|
+
const ok = !r.timedOut && out?.ok === true;
|
|
984
|
+
const details = action === "repair"
|
|
985
|
+
? (0, status_model_1.repairDetails)(out, r.output)
|
|
986
|
+
: [out?.detail ?? "", ...(ok ? [] : (0, status_model_1.tailLines)(out ? "" : r.output, 8))].filter(Boolean);
|
|
987
|
+
rec.result = { action, ok, timedOut: r.timedOut, details, at: Date.now() };
|
|
988
|
+
if (ok && out?.restartClaude)
|
|
989
|
+
rec.restartClaude = true;
|
|
990
|
+
if (out?.doctor) {
|
|
991
|
+
rec.doctor = out.doctor;
|
|
992
|
+
rec.doctorFailed = false;
|
|
993
|
+
}
|
|
994
|
+
rec.busy = null;
|
|
995
|
+
await poll();
|
|
996
|
+
if (!out?.doctor)
|
|
997
|
+
await refreshDoctor();
|
|
998
|
+
render();
|
|
999
|
+
}
|
|
1000
|
+
async function restartClaude() {
|
|
1001
|
+
if (rec.busy)
|
|
1002
|
+
return;
|
|
1003
|
+
const options = {
|
|
1004
|
+
type: "question",
|
|
1005
|
+
message: L.restartConfirm,
|
|
1006
|
+
detail: L.restartConfirmDetail,
|
|
1007
|
+
buttons: [L.restartNow, L.cancel],
|
|
1008
|
+
defaultId: 0,
|
|
1009
|
+
cancelId: 1,
|
|
1010
|
+
};
|
|
1011
|
+
const parent = (0, window_1.mainWindow)();
|
|
1012
|
+
const choice = parent ? await electron_1.dialog.showMessageBox(parent, options) : await electron_1.dialog.showMessageBox(options);
|
|
1013
|
+
if (choice.response !== 0)
|
|
1014
|
+
return;
|
|
1015
|
+
rec.busy = "restart-claude";
|
|
1016
|
+
rec.result = null;
|
|
1017
|
+
render();
|
|
1018
|
+
const ok = await (0, claude_desktop_1.reopenClaude)();
|
|
1019
|
+
rec.busy = null;
|
|
1020
|
+
rec.result = { action: "restart-claude", ok, details: [], at: Date.now() };
|
|
1021
|
+
if (ok)
|
|
1022
|
+
rec.restartClaude = false;
|
|
1023
|
+
render();
|
|
1024
|
+
}
|
|
755
1025
|
let availableUpdate = null;
|
|
756
1026
|
let updating = false;
|
|
757
1027
|
const UPDATE_CHECK_MS = 12 * 60 * 60 * 1000;
|
|
@@ -797,7 +1067,7 @@ async function offerUpdate(check) {
|
|
|
797
1067
|
render();
|
|
798
1068
|
const version = check.latest ?? "";
|
|
799
1069
|
if (electron_1.Notification.isSupported())
|
|
800
|
-
new electron_1.Notification({ title: "ClaudeRipple", body: L.updating(version) }).show();
|
|
1070
|
+
new electron_1.Notification({ title: "ClaudeRipple", body: L.updating(version), icon: notifyIcon() }).show();
|
|
801
1071
|
const r = await runCliResult(["update"]);
|
|
802
1072
|
updating = false;
|
|
803
1073
|
if (!r.ok) {
|
|
@@ -807,7 +1077,7 @@ async function offerUpdate(check) {
|
|
|
807
1077
|
}
|
|
808
1078
|
availableUpdate = null;
|
|
809
1079
|
if (electron_1.Notification.isSupported())
|
|
810
|
-
new electron_1.Notification({ title: "ClaudeRipple", body: L.updateDone(version) }).show();
|
|
1080
|
+
new electron_1.Notification({ title: "ClaudeRipple", body: L.updateDone(version), icon: notifyIcon() }).show();
|
|
811
1081
|
// A new tray from the new files, then this one goes. The lock is let go first, or the new tray
|
|
812
1082
|
// would see this one and leave.
|
|
813
1083
|
electron_1.app.releaseSingleInstanceLock();
|
|
@@ -862,10 +1132,14 @@ function windowsText(quota) {
|
|
|
862
1132
|
.join(" · ");
|
|
863
1133
|
}
|
|
864
1134
|
function render() {
|
|
1135
|
+
pushState();
|
|
865
1136
|
if (!tray)
|
|
866
1137
|
return;
|
|
867
1138
|
const state = healthy(last);
|
|
868
|
-
|
|
1139
|
+
// The direct mode is chosen, not broken: its own mark, and no "cannot connect" line.
|
|
1140
|
+
const shown = (0, status_model_1.trayState)(state, rec.doctor);
|
|
1141
|
+
const direct = shown === "direct";
|
|
1142
|
+
tray.setImage(icon(shown));
|
|
869
1143
|
const s = last;
|
|
870
1144
|
const quota = Object.values(s?.chatgpt?.quota ?? {}).find((q) => q);
|
|
871
1145
|
const quotaLine = quota?.rate_limits?.primary
|
|
@@ -877,22 +1151,29 @@ function render() {
|
|
|
877
1151
|
// "Router not running" is useless advice to someone who has never set it up — that reads as a
|
|
878
1152
|
// fault when the truth is there is one step left. Separate the two states everywhere.
|
|
879
1153
|
const unconfigured = !s && needsSetup();
|
|
880
|
-
const headline =
|
|
881
|
-
? `ClaudeRipple · ${
|
|
882
|
-
:
|
|
1154
|
+
const headline = direct
|
|
1155
|
+
? `ClaudeRipple · ${L.directHeadline}`
|
|
1156
|
+
: s
|
|
1157
|
+
? `ClaudeRipple · ${state === "ok" ? L.healthy : L.attentionNeeded}`
|
|
1158
|
+
: `ClaudeRipple · ${unconfigured ? L.setupNeeded : L.routerNotRunning}`;
|
|
883
1159
|
// What is actually in the way, not only that something is: the router's readiness list names it.
|
|
884
1160
|
const problems = (s?.readiness?.problems ?? []).map((code) => L.problem(code));
|
|
885
|
-
const detail =
|
|
886
|
-
?
|
|
887
|
-
:
|
|
888
|
-
? L.
|
|
889
|
-
:
|
|
1161
|
+
const detail = direct
|
|
1162
|
+
? null
|
|
1163
|
+
: s
|
|
1164
|
+
? [connected ? L.connected : L.notConnected, ...problems, quotaLine].filter(Boolean).join(" · ")
|
|
1165
|
+
: unconfigured
|
|
1166
|
+
? L.setupNeededDetail
|
|
1167
|
+
: L.downNotifyTitle;
|
|
890
1168
|
// One line per Claude subscription account, current login first (#40).
|
|
891
1169
|
const claudeLines = (s?.claude?.accounts ?? [])
|
|
892
1170
|
.map((account) => ({ account, windows: windowsText(account.quota) }))
|
|
893
1171
|
.filter(({ windows }) => windows)
|
|
894
1172
|
.map(({ account, windows }) => ({ label: L.claudeUsage(account.id === "current" ? L.claudeCurrent : account.label, windows), click: () => void openDashboard() }));
|
|
895
1173
|
const template = [
|
|
1174
|
+
// First, because it is the way back when nothing else works: the window needs no router.
|
|
1175
|
+
{ label: L.open, click: () => void openDashboard() },
|
|
1176
|
+
{ type: "separator" },
|
|
896
1177
|
{ label: headline, click: () => void openDashboard() },
|
|
897
1178
|
...(detail ? [{ label: detail, click: () => void openDashboard() }] : []),
|
|
898
1179
|
...claudeLines,
|
|
@@ -900,8 +1181,6 @@ function render() {
|
|
|
900
1181
|
? [{ label: updating ? L.updating(availableUpdate.latest ?? "") : L.updateAvailableItem(availableUpdate.latest ?? ""), enabled: !updating, click: () => void offerUpdate(availableUpdate) }]
|
|
901
1182
|
: []),
|
|
902
1183
|
{ type: "separator" },
|
|
903
|
-
// Enabled even when the router is down: it is started first, then the page is opened.
|
|
904
|
-
{ label: L.openDashboard, click: () => void openDashboard() },
|
|
905
1184
|
{ label: L.startAtLogin, type: "checkbox", checked: loginItemOn(), click: (item) => setLoginItem(item.checked) },
|
|
906
1185
|
{ type: "separator" },
|
|
907
1186
|
{ label: unconfigured ? L.runSetup : L.rerunSetup, click: () => void setup() },
|
|
@@ -924,14 +1203,38 @@ function render() {
|
|
|
924
1203
|
{ label: L.quit, role: "quit" },
|
|
925
1204
|
];
|
|
926
1205
|
tray.setContextMenu(electron_1.Menu.buildFromTemplate(template));
|
|
927
|
-
tray.setToolTip(s ? L.tooltip(state) : L.tooltipDown);
|
|
1206
|
+
tray.setToolTip(direct ? L.tooltipDirect : s ? L.tooltip(state) : L.tooltipDown);
|
|
928
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* The dashboard this app draws: the copy of packages/ui inside a packaged build, else the one next
|
|
1210
|
+
* to the tray's own folder (a checkout's packages/ui, an npm install's dist/ui — the same files the
|
|
1211
|
+
* router serves).
|
|
1212
|
+
*/
|
|
1213
|
+
function uiRoot() {
|
|
1214
|
+
const resources = process.resourcesPath;
|
|
1215
|
+
const candidates = [
|
|
1216
|
+
...(resources ? [node_path_1.default.join(resources, "clauderipple", "packages", "ui")] : []),
|
|
1217
|
+
node_path_1.default.resolve(__dirname, "..", "..", "ui"),
|
|
1218
|
+
];
|
|
1219
|
+
return candidates.find((dir) => node_fs_1.default.existsSync(node_path_1.default.join(dir, "index.html"))) ?? candidates[candidates.length - 1];
|
|
1220
|
+
}
|
|
1221
|
+
(0, protocol_1.registerScheme)();
|
|
929
1222
|
// One tray. `clauderipple tray` run while one is up started a second icon (Windows VM,
|
|
930
1223
|
// 2026-10-10); the newcomer now leaves at once with code 0, which the CLI reads as "already running".
|
|
1224
|
+
if (process.env.CLAUDERIPPLE_APP_USER_DATA)
|
|
1225
|
+
electron_1.app.setPath("userData", process.env.CLAUDERIPPLE_APP_USER_DATA);
|
|
931
1226
|
if (!electron_1.app.requestSingleInstanceLock())
|
|
932
1227
|
electron_1.app.exit(0);
|
|
1228
|
+
// Opening the app again (Finder, Start menu, `clauderipple tray`) while it already runs in the
|
|
1229
|
+
// tray is someone looking for it: bring the window forward rather than nothing.
|
|
1230
|
+
electron_1.app.on("second-instance", () => {
|
|
1231
|
+
if (electron_1.app.isReady())
|
|
1232
|
+
showWindow(last ? undefined : "health");
|
|
1233
|
+
});
|
|
933
1234
|
electron_1.app.whenReady().then(() => {
|
|
934
1235
|
L = STRINGS[uiLang()];
|
|
1236
|
+
(0, protocol_1.handleScheme)(uiRoot(), adminUrl);
|
|
1237
|
+
wireBridge();
|
|
935
1238
|
// Without this, Windows attributes our notifications to "Electron" instead of ClaudeRipple.
|
|
936
1239
|
if (process.platform === "win32")
|
|
937
1240
|
electron_1.app.setAppUserModelId("com.clauderipple.app");
|
|
@@ -950,8 +1253,18 @@ electron_1.app.whenReady().then(() => {
|
|
|
950
1253
|
tray = new electron_1.Tray(icon("down"));
|
|
951
1254
|
tray.on("click", () => tray?.popUpContextMenu());
|
|
952
1255
|
render();
|
|
1256
|
+
// Opened from its icon (the CLI sets this; a login launch does not): show the window, not just a
|
|
1257
|
+
// tray icon the person may never notice. Taken out so an update's relaunch does not inherit it.
|
|
1258
|
+
if (process.env.CLAUDERIPPLE_APP_SHOW === "1") {
|
|
1259
|
+
delete process.env.CLAUDERIPPLE_APP_SHOW;
|
|
1260
|
+
showWindow("health");
|
|
1261
|
+
}
|
|
953
1262
|
void poll();
|
|
954
1263
|
setInterval(() => void poll(), POLL_MS);
|
|
1264
|
+
// What the health poll cannot see (the direct mode, a supervisor gone missing) comes from doctor.
|
|
1265
|
+
setTimeout(() => void refreshDoctor(), 3_000);
|
|
1266
|
+
setInterval(() => void (!rec.busy && refreshDoctor()), DOCTOR_EVERY_MS);
|
|
1267
|
+
setInterval(() => void ((0, window_1.windowVisible)() && !rec.busy && refreshDoctor()), DOCTOR_WHILE_OPEN_MS);
|
|
955
1268
|
if (needsSetup())
|
|
956
1269
|
void promptForSetup();
|
|
957
1270
|
// Not at the very start: a tray opened at login shares the first minute with everything else.
|