clauderipple 0.9.0 → 0.9.2
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 +49 -0
- package/README.es.md +22 -11
- package/README.ja.md +14 -5
- package/README.ko.md +19 -8
- package/README.md +22 -10
- package/README.pt-BR.md +22 -11
- package/README.zh-CN.md +14 -5
- package/dist/app/package.json +1 -1
- package/dist/cli/src/index.js +2 -1
- package/dist/mod/hooks/i18n.ts +36 -12
- package/dist/mod/hooks/judge.ts +17 -1
- package/dist/mod/hooks/register.tsx +39 -9
- package/dist/mod/hooks/session.ts +11 -3
- package/dist/mod/hooks/ui/alerts.tsx +21 -5
- package/dist/mod/hooks/ui/band.tsx +1 -1
- package/dist/mod/hooks/ui/view.ts +4 -1
- package/dist/mod/tests/register.test.tsx +53 -0
- package/dist/mod/tests/ui.test.tsx +10 -6
- package/dist/mod/types/index.d.ts +4 -1
- package/dist/router/src/admin.js +72 -2
- package/dist/router/src/agents.js +4 -0
- package/dist/router/src/config.js +24 -0
- package/dist/router/src/ingress/server.js +2 -2
- package/dist/router/src/mod-settings.js +2 -1
- package/dist/router/src/providers/chatgpt/index.js +19 -6
- package/dist/router/src/providers/chatgpt/translate.js +3 -0
- package/dist/router/src/providers/google/index.js +4 -3
- package/dist/router/src/providers/grok/auth.js +289 -0
- package/dist/router/src/providers/grok/catalog.js +52 -0
- package/dist/router/src/providers/grok/index.js +94 -0
- package/dist/router/src/providers/openai/index.js +71 -17
- package/dist/router/src/providers/openai/translate.js +11 -1
- package/dist/router/src/proxy.js +91 -21
- package/dist/router/src/redact.js +7 -2
- package/dist/router/src/requestlog.js +78 -20
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +105 -0
- package/dist/ui/i18n.js +6 -6
- package/docs/ARCHITECTURE.md +91 -0
- package/package.json +1 -1
package/dist/mod/hooks/i18n.ts
CHANGED
|
@@ -70,6 +70,7 @@ const EN = {
|
|
|
70
70
|
'band.hit': 'hit {percent}%',
|
|
71
71
|
'band.effortAutoAlt': 'Effort Auto {state}',
|
|
72
72
|
'band.warmAlt': 'Keep cache warm {state}',
|
|
73
|
+
'band.warmHere': 'this chat only; new chats follow Settings',
|
|
73
74
|
'band.handoffAdvised': 'Handoff (advised: a fresh chat pays now)',
|
|
74
75
|
'band.closeLog': 'Close the request log',
|
|
75
76
|
'band.openLog': 'Open the request log',
|
|
@@ -90,6 +91,9 @@ const EN = {
|
|
|
90
91
|
// Alerts.
|
|
91
92
|
'alerts.coldTitle': 'The cache went cold',
|
|
92
93
|
'alerts.coldBody': 'It sat {minutes} min, so the next prompt re-reads all {tokens} tokens at full price.',
|
|
94
|
+
'alerts.keptTitle': 'Cache kept warm while you were away',
|
|
95
|
+
'alerts.keptBody': '{pings} ping(s) held {tokens} tokens in cache, so your next prompt reads them at the cached rate. {minutes} min left.',
|
|
96
|
+
'alerts.gotIt': 'Got it',
|
|
93
97
|
'alerts.swampTitle': 'Context {percent}% full',
|
|
94
98
|
'alerts.swampBody': '{tokens} tokens ride along with every prompt: slower, dearer, blurrier.',
|
|
95
99
|
'alerts.hotTitle': '{window} limit at {percent}%',
|
|
@@ -347,12 +351,12 @@ const EN = {
|
|
|
347
351
|
'toasts.workerMissing': '{name} is not installed',
|
|
348
352
|
// Commands (stats and debug labels included).
|
|
349
353
|
'commands.description': 'ClaudeRipple effort, workers, cache and session tools',
|
|
350
|
-
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down',
|
|
354
|
+
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down|kept',
|
|
351
355
|
'commands.logDescription': 'Open or close the ClaudeRipple request log',
|
|
352
356
|
'commands.barDescription': 'Show or hide the ClaudeRipple band',
|
|
353
357
|
'commands.logTitle': 'ClaudeRipple',
|
|
354
358
|
'commands.agentsTitle': 'ClaudeRipple Agents',
|
|
355
|
-
'commands.help': 'ClaudeRipple: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down.',
|
|
359
|
+
'commands.help': 'ClaudeRipple: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down, kept.',
|
|
356
360
|
'commands.done': 'ClaudeRipple: {command}.',
|
|
357
361
|
'commands.logOpened': 'ClaudeRipple log opened.',
|
|
358
362
|
'commands.logClosed': 'ClaudeRipple log closed.',
|
|
@@ -515,6 +519,7 @@ const KO: Record<Key, string> = {
|
|
|
515
519
|
'band.hit': '적중 {percent}%',
|
|
516
520
|
'band.effortAutoAlt': '추론 강도 자동 {state}',
|
|
517
521
|
'band.warmAlt': '캐시 유지 {state}',
|
|
522
|
+
'band.warmHere': '이 세션만 적용, 새 세션은 설정을 따릅니다',
|
|
518
523
|
'band.handoffAdvised': '인수인계 (권장: 지금은 새 대화가 더 효율적입니다)',
|
|
519
524
|
'band.closeLog': '요청 로그 닫기',
|
|
520
525
|
'band.openLog': '요청 로그 열기',
|
|
@@ -535,6 +540,9 @@ const KO: Record<Key, string> = {
|
|
|
535
540
|
// Alerts.
|
|
536
541
|
'alerts.coldTitle': '캐시가 만료되었습니다',
|
|
537
542
|
'alerts.coldBody': '{minutes}분 동안 대기하여 캐시가 만료되었습니다. 다음 프롬프트는 {tokens} 토큰 전체를 새로 읽어 비용이 발생합니다.',
|
|
543
|
+
'alerts.keptTitle': '자리를 비운 동안 캐시를 유지했습니다',
|
|
544
|
+
'alerts.keptBody': '핑 {pings}회로 {tokens} 토큰을 캐시에 붙잡아 두었습니다. 다음 프롬프트도 캐시로 읽습니다. 만료까지 {minutes}분 남았습니다.',
|
|
545
|
+
'alerts.gotIt': '확인',
|
|
538
546
|
'alerts.swampTitle': '컨텍스트 {percent}% 사용',
|
|
539
547
|
'alerts.swampBody': '프롬프트마다 {tokens} 토큰이 함께 전송되어 응답 속도와 정확도, 비용에 영향을 줍니다.',
|
|
540
548
|
'alerts.hotTitle': '{window} 한도 {percent}% 도달',
|
|
@@ -792,12 +800,12 @@ const KO: Record<Key, string> = {
|
|
|
792
800
|
'toasts.workerMissing': '{name} 에이전트 미설치',
|
|
793
801
|
// Commands (stats and debug labels included).
|
|
794
802
|
'commands.description': 'ClaudeRipple 추론 강도, 워커, 캐시 및 세션 도구',
|
|
795
|
-
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down',
|
|
803
|
+
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down|kept',
|
|
796
804
|
'commands.logDescription': 'ClaudeRipple 요청 로그 열기/닫기',
|
|
797
805
|
'commands.barDescription': 'ClaudeRipple 상태 바 표시/숨기기',
|
|
798
806
|
'commands.logTitle': 'ClaudeRipple',
|
|
799
807
|
'commands.agentsTitle': 'ClaudeRipple 에이전트',
|
|
800
|
-
'commands.help': 'ClaudeRipple 하위 명령: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down.',
|
|
808
|
+
'commands.help': 'ClaudeRipple 하위 명령: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down, kept.',
|
|
801
809
|
'commands.done': 'ClaudeRipple: {command} 완료.',
|
|
802
810
|
'commands.logOpened': 'ClaudeRipple 로그를 열었습니다.',
|
|
803
811
|
'commands.logClosed': 'ClaudeRipple 로그를 닫았습니다.',
|
|
@@ -958,6 +966,7 @@ const ZH: Record<Key, string> = {
|
|
|
958
966
|
'band.hit': '命中 {percent}%',
|
|
959
967
|
'band.effortAutoAlt': '推理强度自动{state}',
|
|
960
968
|
'band.warmAlt': '缓存保温{state}',
|
|
969
|
+
'band.warmHere': '仅限本会话,新会话按设置',
|
|
961
970
|
'band.handoffAdvised': '交接(建议:现在换新对话更划算)',
|
|
962
971
|
'band.closeLog': '关闭请求日志',
|
|
963
972
|
'band.openLog': '打开请求日志',
|
|
@@ -978,6 +987,9 @@ const ZH: Record<Key, string> = {
|
|
|
978
987
|
// Alerts.
|
|
979
988
|
'alerts.coldTitle': '缓存已过期',
|
|
980
989
|
'alerts.coldBody': '闲置了 {minutes} 分钟,下一条提示词会按全价重读全部 {tokens} token。',
|
|
990
|
+
'alerts.keptTitle': '您离开期间缓存已保持温热',
|
|
991
|
+
'alerts.keptBody': '保温 ping {pings} 次,把 {tokens} token 留在了缓存里,下一条提示词仍按缓存价读取。还有 {minutes} 分钟过期。',
|
|
992
|
+
'alerts.gotIt': '知道了',
|
|
981
993
|
'alerts.swampTitle': '上下文已用 {percent}%',
|
|
982
994
|
'alerts.swampBody': '每条提示词都带着 {tokens} token:更慢、更贵、更模糊。',
|
|
983
995
|
'alerts.hotTitle': '{window}限额已达 {percent}%',
|
|
@@ -1235,12 +1247,12 @@ const ZH: Record<Key, string> = {
|
|
|
1235
1247
|
'toasts.workerMissing': '未安装 {name}',
|
|
1236
1248
|
// Commands (stats and debug labels included).
|
|
1237
1249
|
'commands.description': 'ClaudeRipple 推理强度、Worker、缓存与会话工具',
|
|
1238
|
-
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down',
|
|
1250
|
+
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down|kept',
|
|
1239
1251
|
'commands.logDescription': '打开或关闭 ClaudeRipple 请求日志',
|
|
1240
1252
|
'commands.barDescription': '显示或隐藏 ClaudeRipple 状态栏',
|
|
1241
1253
|
'commands.logTitle': 'ClaudeRipple',
|
|
1242
1254
|
'commands.agentsTitle': 'ClaudeRipple 代理',
|
|
1243
|
-
'commands.help': 'ClaudeRipple 子命令:settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down.',
|
|
1255
|
+
'commands.help': 'ClaudeRipple 子命令:settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down, kept.',
|
|
1244
1256
|
'commands.done': 'ClaudeRipple: {command}.',
|
|
1245
1257
|
'commands.logOpened': '已打开 ClaudeRipple 日志。',
|
|
1246
1258
|
'commands.logClosed': '已关闭 ClaudeRipple 日志。',
|
|
@@ -1401,6 +1413,7 @@ const JA: Record<Key, string> = {
|
|
|
1401
1413
|
'band.hit': 'ヒット {percent}%',
|
|
1402
1414
|
'band.effortAutoAlt': '推論強度の自動 {state}',
|
|
1403
1415
|
'band.warmAlt': 'キャッシュ保持 {state}',
|
|
1416
|
+
'band.warmHere': 'このチャットだけ。新しいチャットは設定に従います',
|
|
1404
1417
|
'band.handoffAdvised': '引き継ぎ(推奨: 今は新しい会話のほうが得です)',
|
|
1405
1418
|
'band.closeLog': 'リクエストログを閉じる',
|
|
1406
1419
|
'band.openLog': 'リクエストログを開く',
|
|
@@ -1421,6 +1434,9 @@ const JA: Record<Key, string> = {
|
|
|
1421
1434
|
// Alerts.
|
|
1422
1435
|
'alerts.coldTitle': 'キャッシュの期限が切れました',
|
|
1423
1436
|
'alerts.coldBody': '{minutes} 分止まっていたので、次のプロンプトで {tokens} トークンを定価で読み直します。',
|
|
1437
|
+
'alerts.keptTitle': '不在中もキャッシュを温かいまま保ちました',
|
|
1438
|
+
'alerts.keptBody': 'キープウォームが {pings} 回、{tokens} トークンをキャッシュに保持しました。次のプロンプトもキャッシュで読めます。期限まで {minutes} 分です。',
|
|
1439
|
+
'alerts.gotIt': '了解',
|
|
1424
1440
|
'alerts.swampTitle': 'コンテキスト {percent}% 使用中',
|
|
1425
1441
|
'alerts.swampBody': 'プロンプトごとに {tokens} トークンが付いていき、遅く・高く・曖昧になります。',
|
|
1426
1442
|
'alerts.hotTitle': '{window}上限の {percent}% に到達',
|
|
@@ -1678,12 +1694,12 @@ const JA: Record<Key, string> = {
|
|
|
1678
1694
|
'toasts.workerMissing': '{name} がインストールされていません',
|
|
1679
1695
|
// Commands (stats and debug labels included).
|
|
1680
1696
|
'commands.description': 'ClaudeRipple の推論強度、ワーカー、キャッシュ、セッションツール',
|
|
1681
|
-
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down',
|
|
1697
|
+
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down|kept',
|
|
1682
1698
|
'commands.logDescription': 'ClaudeRipple のリクエストログを開閉',
|
|
1683
1699
|
'commands.barDescription': 'ClaudeRipple のバーを表示・非表示',
|
|
1684
1700
|
'commands.logTitle': 'ClaudeRipple',
|
|
1685
1701
|
'commands.agentsTitle': 'ClaudeRipple エージェント',
|
|
1686
|
-
'commands.help': 'ClaudeRipple のサブコマンド: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down.',
|
|
1702
|
+
'commands.help': 'ClaudeRipple のサブコマンド: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down, kept.',
|
|
1687
1703
|
'commands.done': 'ClaudeRipple: {command}.',
|
|
1688
1704
|
'commands.logOpened': 'ClaudeRipple のログを開きました。',
|
|
1689
1705
|
'commands.logClosed': 'ClaudeRipple のログを閉じました。',
|
|
@@ -1844,6 +1860,7 @@ const ES: Record<Key, string> = {
|
|
|
1844
1860
|
'band.hit': 'acierto {percent}%',
|
|
1845
1861
|
'band.effortAutoAlt': 'Esfuerzo auto {state}',
|
|
1846
1862
|
'band.warmAlt': 'Mantener la caché caliente {state}',
|
|
1863
|
+
'band.warmHere': 'solo este chat; los nuevos siguen los Ajustes',
|
|
1847
1864
|
'band.handoffAdvised': 'Traspaso (recomendado: ahora conviene una conversación nueva)',
|
|
1848
1865
|
'band.closeLog': 'Cerrar el registro de solicitudes',
|
|
1849
1866
|
'band.openLog': 'Abrir el registro de solicitudes',
|
|
@@ -1864,6 +1881,9 @@ const ES: Record<Key, string> = {
|
|
|
1864
1881
|
// Alerts.
|
|
1865
1882
|
'alerts.coldTitle': 'La caché se enfrió',
|
|
1866
1883
|
'alerts.coldBody': 'Estuvo {minutes} min parada, así que el próximo prompt relee sus {tokens} tokens a precio completo.',
|
|
1884
|
+
'alerts.keptTitle': 'La caché se mantuvo caliente mientras no estabas',
|
|
1885
|
+
'alerts.keptBody': '{pings} ping(s) mantuvieron {tokens} tokens en caché, así que tu próximo prompt los lee al precio en caché. Quedan {minutes} min.',
|
|
1886
|
+
'alerts.gotIt': 'Entendido',
|
|
1867
1887
|
'alerts.swampTitle': 'Contexto al {percent}%',
|
|
1868
1888
|
'alerts.swampBody': '{tokens} tokens viajan con cada prompt: más lento, más caro, más difuso.',
|
|
1869
1889
|
'alerts.hotTitle': 'Límite {window} al {percent}%',
|
|
@@ -2121,12 +2141,12 @@ const ES: Record<Key, string> = {
|
|
|
2121
2141
|
'toasts.workerMissing': '{name} no está instalado',
|
|
2122
2142
|
// Commands (stats and debug labels included).
|
|
2123
2143
|
'commands.description': 'Esfuerzo, workers, caché y herramientas de sesión de ClaudeRipple',
|
|
2124
|
-
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down',
|
|
2144
|
+
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down|kept',
|
|
2125
2145
|
'commands.logDescription': 'Abre o cierra el registro de solicitudes de ClaudeRipple',
|
|
2126
2146
|
'commands.barDescription': 'Muestra u oculta la barra de ClaudeRipple',
|
|
2127
2147
|
'commands.logTitle': 'ClaudeRipple',
|
|
2128
2148
|
'commands.agentsTitle': 'Agentes de ClaudeRipple',
|
|
2129
|
-
'commands.help': 'Subcomandos de ClaudeRipple: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down.',
|
|
2149
|
+
'commands.help': 'Subcomandos de ClaudeRipple: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down, kept.',
|
|
2130
2150
|
'commands.done': 'ClaudeRipple: {command}.',
|
|
2131
2151
|
'commands.logOpened': 'Registro de ClaudeRipple abierto.',
|
|
2132
2152
|
'commands.logClosed': 'Registro de ClaudeRipple cerrado.',
|
|
@@ -2287,6 +2307,7 @@ const PT: Record<Key, string> = {
|
|
|
2287
2307
|
'band.hit': 'acerto {percent}%',
|
|
2288
2308
|
'band.effortAutoAlt': 'Esforço auto {state}',
|
|
2289
2309
|
'band.warmAlt': 'Manter o cache quente {state}',
|
|
2310
|
+
'band.warmHere': 'só esta conversa; as novas seguem as Configurações',
|
|
2290
2311
|
'band.handoffAdvised': 'Repasse (recomendado: agora uma conversa nova compensa)',
|
|
2291
2312
|
'band.closeLog': 'Fechar o log de requisições',
|
|
2292
2313
|
'band.openLog': 'Abrir o log de requisições',
|
|
@@ -2307,6 +2328,9 @@ const PT: Record<Key, string> = {
|
|
|
2307
2328
|
// Alerts.
|
|
2308
2329
|
'alerts.coldTitle': 'O cache esfriou',
|
|
2309
2330
|
'alerts.coldBody': 'Ficou parada {minutes} min, então o próximo prompt relê os {tokens} tokens pelo preço cheio.',
|
|
2331
|
+
'alerts.keptTitle': 'O cache ficou quente enquanto você esteve fora',
|
|
2332
|
+
'alerts.keptBody': '{pings} ping(s) seguraram {tokens} tokens no cache, então o próximo prompt os lê pelo preço em cache. Faltam {minutes} min.',
|
|
2333
|
+
'alerts.gotIt': 'Entendi',
|
|
2310
2334
|
'alerts.swampTitle': 'Contexto {percent}% cheio',
|
|
2311
2335
|
'alerts.swampBody': '{tokens} tokens vão junto com cada prompt: mais lento, mais caro, mais confuso.',
|
|
2312
2336
|
'alerts.hotTitle': 'Limite {window} em {percent}%',
|
|
@@ -2564,12 +2588,12 @@ const PT: Record<Key, string> = {
|
|
|
2564
2588
|
'toasts.workerMissing': '{name} não está instalado',
|
|
2565
2589
|
// Commands (stats and debug labels included).
|
|
2566
2590
|
'commands.description': 'Esforço, workers, cache e ferramentas de sessão do ClaudeRipple',
|
|
2567
|
-
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down',
|
|
2591
|
+
'commands.argumentHint': 'settings|setup|auto|warm|save|handoff [full]|stats|debug|agents|log|bar|cold|swamp|hot|down|kept',
|
|
2568
2592
|
'commands.logDescription': 'Abre ou fecha o log de requisições do ClaudeRipple',
|
|
2569
2593
|
'commands.barDescription': 'Mostra ou oculta a barra do ClaudeRipple',
|
|
2570
2594
|
'commands.logTitle': 'ClaudeRipple',
|
|
2571
2595
|
'commands.agentsTitle': 'Agentes do ClaudeRipple',
|
|
2572
|
-
'commands.help': 'Subcomandos do ClaudeRipple: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down.',
|
|
2596
|
+
'commands.help': 'Subcomandos do ClaudeRipple: settings, setup, auto, warm, save, handoff [full], stats, debug, agents, log, bar, cold, swamp, hot, down, kept.',
|
|
2573
2597
|
'commands.done': 'ClaudeRipple: {command}.',
|
|
2574
2598
|
'commands.logOpened': 'Log do ClaudeRipple aberto.',
|
|
2575
2599
|
'commands.logClosed': 'Log do ClaudeRipple fechado.',
|
package/dist/mod/hooks/judge.ts
CHANGED
|
@@ -90,7 +90,23 @@ export function verdictObject(text: string): Record<string, unknown> | undefined
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
return
|
|
93
|
+
return looseVerdictObject(text)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A small judge sometimes breaks its JSON (Haiku answered `…"why":"…","}`, #53). Its complete
|
|
98
|
+
* `"key": value` pairs still carry the verdict, so they are read one by one; the parsers above
|
|
99
|
+
* check every field they use, so nothing here is trusted beyond a JSON scalar.
|
|
100
|
+
*/
|
|
101
|
+
function looseVerdictObject(text: string): Record<string, unknown> | undefined {
|
|
102
|
+
const start = text.indexOf('{')
|
|
103
|
+
if (start === -1) return undefined
|
|
104
|
+
const value: Record<string, unknown> = {}
|
|
105
|
+
const pair = /"(\w+)"\s*:\s*("(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?|true|false|null)/g
|
|
106
|
+
for (const match of text.slice(start).matchAll(pair)) {
|
|
107
|
+
try { value[match[1]!] ??= JSON.parse(match[2]!) } catch { /* a malformed scalar adds nothing */ }
|
|
108
|
+
}
|
|
109
|
+
return Object.keys(value).length ? value : undefined
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
const effortCriteria = {
|
|
@@ -13,7 +13,7 @@ import { activeModeOf, claudeWindow, eagerNote, modeKey, modeOver, quotaWindows,
|
|
|
13
13
|
import type { QuotaWindow } from './modes'
|
|
14
14
|
import type { Spawn, WorkerFix } from './worker'
|
|
15
15
|
import { afterPing, nextMemo, restoreFromResume, shouldPing } from './cache'
|
|
16
|
-
import { bandPriority, coldAlert, hotAlert, hottest, saveUntil as limitSaveUntil, swampAlert, swamped } from './session'
|
|
16
|
+
import { bandPriority, coldAlert, hotAlert, hottest, keptAlert, saveUntil as limitSaveUntil, swampAlert, swamped } from './session'
|
|
17
17
|
import { COMPACT_SYSTEM, compactResult, serializeTranscript } from './compact'
|
|
18
18
|
import { ADVICE_SYSTEM, FULL_PROMPT, QUICK_PROMPT, adviceDue, adviceInput, parseAdvice, wrapForNewChat } from './handoff'
|
|
19
19
|
import { addJudge, addPrompt, addRequest, addWarm, addWorker, emptyStats, formatStats, weighted } from './stats'
|
|
@@ -48,6 +48,7 @@ const lastSpawn = atom({ plugin: 'ripple', key: 'lastSpawn' } as const, null)
|
|
|
48
48
|
const agents = atom({ plugin: 'ripple', key: 'agents' } as const, [])
|
|
49
49
|
const cache = atom({ plugin: 'ripple', key: 'cache' } as const, null)
|
|
50
50
|
const warm = atom({ plugin: 'ripple', key: 'warm' } as const, { pings: 0, cost: 0 })
|
|
51
|
+
const warmHere = atom({ plugin: 'ripple', key: 'warmHere' } as const, null)
|
|
51
52
|
const lastRealReplyAt = atom({ plugin: 'ripple', key: 'lastRealReplyAt' } as const, null)
|
|
52
53
|
const context = atom({ plugin: 'ripple', key: 'context' } as const, null)
|
|
53
54
|
const hot = atom({ plugin: 'ripple', key: 'hot' } as const, null)
|
|
@@ -56,6 +57,7 @@ const hotHidden = atom({ plugin: 'ripple', key: 'hotHidden' } as const, null)
|
|
|
56
57
|
const saveUntil = atom({ plugin: 'ripple', key: 'saveUntil' } as const, null)
|
|
57
58
|
const swampHidden = atom({ plugin: 'ripple', key: 'swampHidden' } as const, null)
|
|
58
59
|
const coldHidden = atom({ plugin: 'ripple', key: 'coldHidden' } as const, false)
|
|
60
|
+
const kept = atom({ plugin: 'ripple', key: 'kept' } as const, null)
|
|
59
61
|
const judgeDown = atom({ plugin: 'ripple', key: 'judgeDown' } as const, null)
|
|
60
62
|
const judgeDownHidden = atom({ plugin: 'ripple', key: 'judgeDownHidden' } as const, null)
|
|
61
63
|
const judgeSeen = atom({ plugin: 'ripple', key: 'judgeSeen' } as const, [])
|
|
@@ -233,6 +235,8 @@ async function installSettings($: EngineInterface, s: Settings): Promise<void> {
|
|
|
233
235
|
try { await registerAuto($, s) } catch (error) { await report($, 'agent register', error) }
|
|
234
236
|
if (!s.effortAuto) await update($, heldPick, () => null)
|
|
235
237
|
if (s.keepWarm && !previous.keepWarm) await update($, warm, value => ({ ...value, stopped: undefined }))
|
|
238
|
+
// Settings caught up with this chat's own choice: it follows Settings again.
|
|
239
|
+
if (await read($, warmHere) === s.keepWarm) await update($, warmHere, () => null)
|
|
236
240
|
}
|
|
237
241
|
function settingsSnapshot(text: string): ModSettingsSnapshot {
|
|
238
242
|
const value = object(JSON.parse(text))
|
|
@@ -712,9 +716,15 @@ async function toggleAuto($: EngineInterface): Promise<void> {
|
|
|
712
716
|
const s = await currentSettings($)
|
|
713
717
|
await saveSettings($, { ...s, effortAuto: !s.effortAuto })
|
|
714
718
|
}
|
|
719
|
+
/** The band's flame turns keep-warm on or off for this chat only; Settings is what a new chat starts with. */
|
|
720
|
+
async function keepWarmOn($: EngineInterface, s: Settings): Promise<boolean> {
|
|
721
|
+
return await read($, warmHere) ?? s.keepWarm
|
|
722
|
+
}
|
|
715
723
|
async function toggleWarm($: EngineInterface): Promise<void> {
|
|
716
724
|
const s = await currentSettings($)
|
|
717
|
-
await
|
|
725
|
+
const on = !await keepWarmOn($, s)
|
|
726
|
+
await update($, warmHere, () => on === s.keepWarm ? null : on)
|
|
727
|
+
if (on) await update($, warm, value => ({ ...value, stopped: undefined }))
|
|
718
728
|
}
|
|
719
729
|
async function toggleSave($: EngineInterface): Promise<void> {
|
|
720
730
|
const now = await $.clock.now()
|
|
@@ -986,7 +996,8 @@ async function tick($: EngineInterface): Promise<void> {
|
|
|
986
996
|
const answer = await $.model.complete({ model: HAIKU, system: ADVICE_SYSTEM, prompt: queued, effort: 'low', maxTokens: 60, timeoutMs: 8000 })
|
|
987
997
|
if (answer.isAnswered) await update($, advice, () => parseAdvice(answer.text))
|
|
988
998
|
}
|
|
989
|
-
const s = await currentSettings($)
|
|
999
|
+
const s = { ...await currentSettings($) }
|
|
1000
|
+
s.keepWarm = await keepWarmOn($, s)
|
|
990
1001
|
const decision = shouldPing({ settings: s, memo: await read($, cache), warm: await read($, warm), now: await $.clock.now(), idle: !await read($, busy) && !await read($, deciding) && !forking,
|
|
991
1002
|
contextTokens: (await read($, context))?.tokens ?? 0, lastRealReplyAt: await read($, lastRealReplyAt) })
|
|
992
1003
|
if (decision.ping) {
|
|
@@ -997,6 +1008,15 @@ async function tick($: EngineInterface): Promise<void> {
|
|
|
997
1008
|
const after = afterPing(before, answer, await $.clock.now(), language)
|
|
998
1009
|
await update($, warm, () => after)
|
|
999
1010
|
if (after.stopped && !before.stopped) await $.ui.toast(t(language, 'toasts.warmStopped', { reason: after.stopped }))
|
|
1011
|
+
// A ping read cache tokens: the cache held while the person was away. One card counts them all
|
|
1012
|
+
// (a real reply or the expiry clears it; register.tsx kept), until then each ping adds up.
|
|
1013
|
+
const usage = answer && 'usage' in answer ? answer.usage : null
|
|
1014
|
+
if ((usage?.cache_read_input_tokens ?? 0) > 0) {
|
|
1015
|
+
await update($, kept, prev => ({
|
|
1016
|
+
pings: (prev?.pings ?? 0) + 1,
|
|
1017
|
+
tokens: usage!.cache_read_input_tokens,
|
|
1018
|
+
}))
|
|
1019
|
+
}
|
|
1000
1020
|
} else if (s.keepWarm && !await read($, busy) && decision.reason === 'keep warm time bound reached' && !(await read($, warm)).stopped) {
|
|
1001
1021
|
await update($, warm, value => ({ ...value, stopped: t(language, 'toasts.warmBoundReason') }))
|
|
1002
1022
|
await $.ui.toast(t(language, 'toasts.warmBound'))
|
|
@@ -1025,7 +1045,7 @@ async function dashboard($: EngineInterface, working: boolean): Promise<Dashboar
|
|
|
1025
1045
|
mode: await read($, deciding) ? 'deciding' : await read($, paused) ? 'paused' : p?.by === 'manual' ? 'manual' : s.effortAuto ? 'auto' : 'off',
|
|
1026
1046
|
...(p?.why ? { why: p.why, by: p.by } : {}), ...(await read($, decidingSince) !== null ? { since: (await read($, decidingSince))! } : {}) },
|
|
1027
1047
|
model: await read($, modelIs) ? modelName((await read($, modelIs))!) : null,
|
|
1028
|
-
context: await read($, context), cache: await read($, cache), warm: { ...w, on: s
|
|
1048
|
+
context: await read($, context), cache: await read($, cache), warm: { ...w, on: await keepWarmOn($, s), ...(await read($, warmHere) !== null ? { here: true } : {}) },
|
|
1029
1049
|
limit: l ? { ...l, save: saving } : null, route: record ? routeOf(record) : null, notice: await read($, line),
|
|
1030
1050
|
auto: s.effortAuto, handoff: await read($, advice) ? 'advised' : s.handoffButton === 'always' ? 'always' : 'off',
|
|
1031
1051
|
agents: { live: list.filter(a => ['running', 'waiting'].includes(a.state)).length, total: list.length },
|
|
@@ -1056,6 +1076,7 @@ async function alertView($: EngineInterface, now: number): Promise<AlertView | n
|
|
|
1056
1076
|
const which = bandPriority({ panel: await read($, panel), judgeDown: d === 'down' || (!!downReason && downReason !== await read($, judgeDownHidden)),
|
|
1057
1077
|
hot: d === 'hot' || hotAlert(h, await read($, hotHidden)), result: !!writing || !!result,
|
|
1058
1078
|
cold: d === 'cold' || coldAlert(m, c?.tokens ?? 0, now, await read($, coldHidden)),
|
|
1079
|
+
kept: d === 'kept' || keptAlert(await read($, kept), m, now),
|
|
1059
1080
|
swamp: d === 'swamp' || (s.swampAlert && !await read($, busy) && swampAlert(swamped(c, s.swampAt), await read($, swampHidden))) })
|
|
1060
1081
|
if (which === 'compact') return { kind: 'compactNote', draft: await read($, compactNote) }
|
|
1061
1082
|
if (which === 'handoff') return { kind: 'handoffChoice', handoffKind: await read($, handoffChoiceKind), after: await read($, handoffChoiceAfter) }
|
|
@@ -1068,22 +1089,27 @@ async function alertView($: EngineInterface, now: number): Promise<AlertView | n
|
|
|
1068
1089
|
return { kind: 'result', stage, handoffKind: writing.kind, ...(detail ? { detail } : {}) }
|
|
1069
1090
|
}
|
|
1070
1091
|
if (which === 'cold') return { kind: 'cold', tokens: d === 'cold' ? 180000 : c!.tokens, idleMinutes: m ? Math.round((now - m.lastReplyAt) / 60000) : 61 }
|
|
1092
|
+
if (which === 'kept') {
|
|
1093
|
+
const k = d === 'kept' ? { pings: 2, tokens: 312000 } : (await read($, kept)) ?? { pings: 0, tokens: 0 }
|
|
1094
|
+
return { kind: 'kept', pings: k.pings, tokens: k.tokens, minutesLeft: Math.ceil(((d === 'kept' ? now + 47 * 60000 : m!.expiresAt) - now) / 60000) }
|
|
1095
|
+
}
|
|
1071
1096
|
if (which === 'swamp') return { kind: 'swamp', percent: d === 'swamp' ? 65 : c!.percent, tokens: d === 'swamp' ? 160000 : c!.tokens }
|
|
1072
1097
|
return null
|
|
1073
1098
|
}
|
|
1074
1099
|
/**
|
|
1075
|
-
* A prompt sent while the cold, heavy or hot band shows counts as having seen it: it comes
|
|
1076
|
-
* only on its own next occasion (the next expiry, 15 more points of context,
|
|
1077
|
-
* window), never after every message.
|
|
1100
|
+
* A prompt sent while the cold, kept, heavy or hot band shows counts as having seen it: it comes
|
|
1101
|
+
* back only on its own next occasion (the next expiry, one more ping, 15 more points of context,
|
|
1102
|
+
* 10 more of the plan window), never after every message.
|
|
1078
1103
|
*/
|
|
1079
1104
|
async function markSeen($: EngineInterface): Promise<void> {
|
|
1080
1105
|
const view = await alertView($, await $.clock.now())
|
|
1081
|
-
if (view && (view.kind === 'cold' || view.kind === 'swamp' || view.kind === 'hot')) await dismiss($, view)
|
|
1106
|
+
if (view && (view.kind === 'cold' || view.kind === 'kept' || view.kind === 'swamp' || view.kind === 'hot')) await dismiss($, view)
|
|
1082
1107
|
}
|
|
1083
1108
|
async function dismiss($: EngineInterface, view: AlertView): Promise<void> {
|
|
1084
1109
|
await update($, demo, () => null)
|
|
1085
1110
|
if (view.kind === 'hot') await update($, hotHidden, () => view.percent)
|
|
1086
1111
|
else if (view.kind === 'cold') await update($, coldHidden, () => true)
|
|
1112
|
+
else if (view.kind === 'kept') await update($, kept, () => null)
|
|
1087
1113
|
else if (view.kind === 'swamp') await update($, swampHidden, () => view.percent)
|
|
1088
1114
|
else if (view.kind === 'judgeDown') await update($, judgeDownHidden, () => view.reason)
|
|
1089
1115
|
else if (view.kind === 'result') { await update($, resultCard, () => null); await update($, handoff, () => null) }
|
|
@@ -1133,6 +1159,7 @@ async function prepareStep($: EngineInterface, e: TurnStepInput): Promise<TurnSt
|
|
|
1133
1159
|
if (previousModel !== null) {
|
|
1134
1160
|
await update($, cache, () => null)
|
|
1135
1161
|
await update($, lastRealReplyAt, () => null)
|
|
1162
|
+
if (await read($, kept)) await update($, kept, () => null)
|
|
1136
1163
|
}
|
|
1137
1164
|
}
|
|
1138
1165
|
const safe = isCacheSafeModel(e.model)
|
|
@@ -1200,6 +1227,7 @@ export const register: Register = on => {
|
|
|
1200
1227
|
await update($, cache, () => null)
|
|
1201
1228
|
await update($, context, () => null)
|
|
1202
1229
|
await update($, lastRealReplyAt, () => null)
|
|
1230
|
+
if (await read($, kept)) await update($, kept, () => null)
|
|
1203
1231
|
}
|
|
1204
1232
|
if (e.model) await update($, modelIs, () => e.model!)
|
|
1205
1233
|
await update($, paused, () => e.model ? !isCacheSafeModel(e.model) : false)
|
|
@@ -1291,6 +1319,7 @@ export const register: Register = on => {
|
|
|
1291
1319
|
const memo = nextMemo(await read($, cache), answer.usage, now)
|
|
1292
1320
|
await update($, cache, () => memo)
|
|
1293
1321
|
if (await read($, coldHidden)) await update($, coldHidden, () => false)
|
|
1322
|
+
if (await read($, kept)) await update($, kept, () => null)
|
|
1294
1323
|
await update($, lastRealReplyAt, () => now)
|
|
1295
1324
|
if ((await read($, warm)).stopped !== undefined) await update($, warm, value => ({ ...value, stopped: undefined }))
|
|
1296
1325
|
}
|
|
@@ -1403,6 +1432,7 @@ export const register: Register = on => {
|
|
|
1403
1432
|
if (!answer.isAnswered || answer.text.trim().length <= 200) throw new Error(t(language, 'toasts.summaryEmpty'))
|
|
1404
1433
|
await update($, cache, () => null)
|
|
1405
1434
|
await update($, context, () => null)
|
|
1435
|
+
if (await read($, kept)) await update($, kept, () => null)
|
|
1406
1436
|
await update($, resultCard, () => ({ stage: 'compacted' }))
|
|
1407
1437
|
return compactResult(answer.text)
|
|
1408
1438
|
} catch (error) {
|
|
@@ -1425,7 +1455,7 @@ export const register: Register = on => {
|
|
|
1425
1455
|
else if (sub === 'log') await toggleLog($)
|
|
1426
1456
|
else if (sub === 'agents') await toggleAgents($)
|
|
1427
1457
|
else if (sub === 'bar') await toggleBar($)
|
|
1428
|
-
else if (['cold', 'swamp', 'hot', 'down'].includes(sub)) { await update($, panel, () => null); await update($, demo, () => sub as 'cold' | 'swamp' | 'hot' | 'down') }
|
|
1458
|
+
else if (['cold', 'swamp', 'hot', 'down', 'kept'].includes(sub)) { await update($, panel, () => null); await update($, demo, () => sub as 'cold' | 'swamp' | 'hot' | 'down' | 'kept') }
|
|
1429
1459
|
else return { text: t(language, 'commands.help') }
|
|
1430
1460
|
return { text: t(language, 'commands.done', { command: sub }) }
|
|
1431
1461
|
} catch (error) { await report($, 'ripple command', error); return next(e) }
|
|
@@ -37,6 +37,14 @@ export function coldAlert(memo: CacheMemo | null, contextTokens: number, now: nu
|
|
|
37
37
|
return !hidden && memo !== null && memo.expiresAt <= now && contextTokens >= 150000
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Keep-warm held the cache while the person was away: at least one ping read cache tokens and the
|
|
42
|
+
* memo says the cache is still warm. Once it has expired the cold card is the truth, never this.
|
|
43
|
+
*/
|
|
44
|
+
export function keptAlert(kept: { pings: number; tokens: number } | null, memo: CacheMemo | null, now: number): boolean {
|
|
45
|
+
return kept !== null && kept.pings > 0 && memo !== null && memo.expiresAt > now
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
export function hotAlert(hot: Hot | null, hidden: number | null): boolean {
|
|
41
49
|
return hot !== null && hot.percent >= 80 && (hidden === null || hot.percent >= hidden + 10)
|
|
42
50
|
}
|
|
@@ -54,9 +62,9 @@ export function swampAlert(percent: number | null, hidden: number | null): boole
|
|
|
54
62
|
/** Rendering consumes eligibility, keeping dismissal and idle decisions outside drawing code. */
|
|
55
63
|
export function bandPriority(state: {
|
|
56
64
|
panel?: 'settings' | 'compact' | 'handoff' | 'setup' | null
|
|
57
|
-
judgeDown?: boolean; hot?: boolean; result?: boolean; cold?: boolean; swamp?: boolean
|
|
58
|
-
}): 'settings' | 'compact' | 'handoff' | 'setup' | 'judgeDown' | 'hot' | 'result' | 'cold' | 'swamp' | 'dashboard' {
|
|
65
|
+
judgeDown?: boolean; hot?: boolean; result?: boolean; cold?: boolean; swamp?: boolean; kept?: boolean
|
|
66
|
+
}): 'settings' | 'compact' | 'handoff' | 'setup' | 'judgeDown' | 'hot' | 'result' | 'cold' | 'swamp' | 'kept' | 'dashboard' {
|
|
59
67
|
if (state.panel) return state.panel
|
|
60
|
-
for (const band of ['judgeDown', 'hot', 'result', 'cold', 'swamp'] as const) if (state[band]) return band
|
|
68
|
+
for (const band of ['judgeDown', 'hot', 'result', 'cold', 'swamp', 'kept'] as const) if (state[band]) return band
|
|
61
69
|
return 'dashboard'
|
|
62
70
|
}
|
|
@@ -12,7 +12,7 @@ import { t } from '../i18n'
|
|
|
12
12
|
import type { RenderNode } from 'claude-code'
|
|
13
13
|
|
|
14
14
|
import type { HandoffKind, Settings } from '../../types'
|
|
15
|
-
import { CELL_PX, MUTE, SUB, TONE, pillArt } from './art'
|
|
15
|
+
import { CELL_PX, MUTE, ROW_PX, SUB, TONE, pillArt } from './art'
|
|
16
16
|
import type { Sea, Tone } from './art'
|
|
17
17
|
import { capsule, closeButton, pill, ripple, segmented as drawnSegmented, well } from './kit'
|
|
18
18
|
import type { SegmentOption } from './kit'
|
|
@@ -61,6 +61,14 @@ function cardOf(frame: Frame, alert: AlertView, a: AlertActions): Card {
|
|
|
61
61
|
quiet: [handoff],
|
|
62
62
|
close: notNow,
|
|
63
63
|
}
|
|
64
|
+
case 'kept':
|
|
65
|
+
return {
|
|
66
|
+
key: 'kept',
|
|
67
|
+
face: 'pleased',
|
|
68
|
+
title: t(L, 'alerts.keptTitle'),
|
|
69
|
+
body: t(L, 'alerts.keptBody', { pings: alert.pings, tokens: compactNumber(alert.tokens), minutes: alert.minutesLeft }),
|
|
70
|
+
close: { key: 'dismiss', label: t(L, 'alerts.gotIt'), hotkey: 'x', onPress: a.dismiss },
|
|
71
|
+
}
|
|
64
72
|
case 'swamp':
|
|
65
73
|
return {
|
|
66
74
|
key: 'swamp',
|
|
@@ -205,6 +213,8 @@ function seaOf(card: Card, alert: AlertView): Omit<Sea, 'at'> {
|
|
|
205
213
|
switch (card.key) {
|
|
206
214
|
case 'cold':
|
|
207
215
|
return { tone: 'ice', weather: 'snow', motes: true }
|
|
216
|
+
case 'kept':
|
|
217
|
+
return { tone: 'amber', weather: 'sparkles', ripples: 'calm' }
|
|
208
218
|
case 'swamp':
|
|
209
219
|
return { tone: 'amber', weather: 'bubbles', ripples: 'calm' }
|
|
210
220
|
case 'hot':
|
|
@@ -305,8 +315,11 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
|
|
|
305
315
|
const oneRow = !more.length && faceCells + chOf(card.title) * 1.12 + 2 + chOf(card.body) + 2 + actionCells <= room
|
|
306
316
|
// Too narrow to keep the actions beside the words: they go under them.
|
|
307
317
|
const actionsBelow = !oneRow && room - faceCells - actionCells < 30
|
|
318
|
+
// Beside a title and a why of two lines, the actions sit at the card's middle, not on the title's
|
|
319
|
+
// line (2026-10-13: they rode high). Beside the handoff's options they stay by the title, the ×
|
|
320
|
+
// where a way out is looked for.
|
|
308
321
|
const buttons = (
|
|
309
|
-
<Box key="actions" flexDirection="row" alignItems="center" columnGap={1} flexShrink={0} flexWrap="wrap" rowGap={1}>
|
|
322
|
+
<Box key="actions" flexDirection="row" alignItems="center" alignSelf={more.length ? 'flex-start' : 'center'} columnGap={1} flexShrink={0} flexWrap="wrap" rowGap={1}>
|
|
310
323
|
{quiet.map(c => pill(el, c.key, c.label, 'ghost', c.onPress, tone))}
|
|
311
324
|
{main ? pill(el, main.key, main.label, 'primary', main.onPress, tone) : null}
|
|
312
325
|
{card.close ? closeButton(el, card.close.key, card.close.label, card.close.onPress) : null}
|
|
@@ -326,7 +339,8 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
|
|
|
326
339
|
</Box>
|
|
327
340
|
) : (
|
|
328
341
|
<Box key="top" flexDirection="row" alignItems="flex-start" columnGap={2} width="100%" minWidth={0}>
|
|
329
|
-
{
|
|
342
|
+
{/* Beside two lines 리플이 sits at their middle, not on the title's line (2026-10-14: it rode high). */}
|
|
343
|
+
<Box key="face" alignSelf="center" flexShrink={0}>{face}</Box>
|
|
330
344
|
<Box key="say" flexDirection="column" rowGap={1} flexGrow={1} flexShrink={1} minWidth={0}>
|
|
331
345
|
{title}
|
|
332
346
|
{body}
|
|
@@ -336,10 +350,12 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
|
|
|
336
350
|
{actionsBelow ? null : buttons}
|
|
337
351
|
</Box>
|
|
338
352
|
)
|
|
353
|
+
const rows = oneRow ? 2 : 4 + (more.length ? 3.2 : 0) + (actionsBelow ? 2 : 0) + (frame.columns < 60 ? 3 : 0)
|
|
339
354
|
return capsule(el, frame, {
|
|
340
355
|
key: 'alert-' + card.key,
|
|
341
|
-
|
|
342
|
-
|
|
356
|
+
// The light comes from 리플이: on the title's line in one row, at the card's middle beside more.
|
|
357
|
+
sea: { ...sea, reach: 130, at: { x: 1.5 * CELL_PX + CARD_FACE / 2, y: oneRow ? 0.5 * ROW_PX * 0.5 + CARD_FACE / 2 : rows * ROW_PX / 2 } },
|
|
358
|
+
rows,
|
|
343
359
|
children: [top],
|
|
344
360
|
})
|
|
345
361
|
}
|
|
@@ -596,7 +596,7 @@ function controlsDrawn(el: Els, frame: Frame, view: DashboardView, actions: Band
|
|
|
596
596
|
? { key: 'auto', cells: toggleCells(autoLabel), node: toggle(el, 'auto', view.auto, autoLabel, actions.toggleAuto, view.effort.mode === 'deciding', tipScope('auto')), tip: autoTip }
|
|
597
597
|
: { key: 'auto', cells: SWITCH_ONLY, node: lit(el, 'auto', SWITCH_ONLY, [pic(el, 'auto', switchArt(view.auto, view.effort.mode === 'deciding'), autoLabel, 36, 22), press(el, 'auto', SWITCH_ONLY * CELL_PX, actions.toggleAuto)], 0.25, tipScope('auto')), tip: autoTip })
|
|
598
598
|
const warmLabel = t(L, 'band.warm')
|
|
599
|
-
out.push(iconControl(el, 'warm', iconArt('flame', view.warm.on, 'amber'), ICON_PX, warmLabel, t(L, 'band.warmAlt', { state: state(view.warm.on) }) + ' · ' + t(L, 'settings.warmLine'), actions.toggleWarm, words.includes('warm') ? warmLabel : undefined, view.warm.on ? TONE.amber.text : SUB))
|
|
599
|
+
out.push(iconControl(el, 'warm', iconArt('flame', view.warm.on, 'amber'), ICON_PX, warmLabel, t(L, 'band.warmAlt', { state: state(view.warm.on) }) + ' · ' + t(L, view.warm.here ? 'band.warmHere' : 'settings.warmLine'), actions.toggleWarm, words.includes('warm') ? warmLabel : undefined, view.warm.on ? TONE.amber.text : SUB))
|
|
600
600
|
const agentsLabel = t(L, 'band.agents')
|
|
601
601
|
const live = view.agents.live
|
|
602
602
|
// The live count sits on the icon's shoulder, in one picture of one width: no width change.
|
|
@@ -65,7 +65,8 @@ export type DashboardView = {
|
|
|
65
65
|
context: { percent: number; tokens: number; window: number } | null
|
|
66
66
|
/** The last main reply's cache; null when unknown. */
|
|
67
67
|
cache: { expiresAt: number; ttlMs: number } | null
|
|
68
|
-
|
|
68
|
+
/** `here`: this chat differs from Settings, set by the band's flame. */
|
|
69
|
+
warm: { on: boolean; pings: number; cost: number; stopped?: string; here?: boolean }
|
|
69
70
|
/** The hottest plan window, shown when present. */
|
|
70
71
|
limit: { kind: string; percent: number; resetsAt?: string; save: boolean } | null
|
|
71
72
|
route: RouteView | null
|
|
@@ -113,6 +114,8 @@ export type ResultStage = 'handingOff' | 'handoffReady' | 'handoffDone' | 'compa
|
|
|
113
114
|
export type AlertView =
|
|
114
115
|
/** The cache expired on a big chat: the next prompt re-reads it all. */
|
|
115
116
|
| { kind: 'cold'; tokens: number; idleMinutes: number }
|
|
117
|
+
/** Keep-warm held the cache while the person was away. */
|
|
118
|
+
| { kind: 'kept'; pings: number; tokens: number; minutesLeft: number }
|
|
116
119
|
/** Context is heavy. */
|
|
117
120
|
| { kind: 'swamp'; percent: number; tokens: number }
|
|
118
121
|
/** A plan window at or above 80%. */
|
|
@@ -918,6 +918,42 @@ test('keep-warm forks near expiry, refreshes the memo and stops once on a null f
|
|
|
918
918
|
expect(stats.text).toContain('Warm: 1 pings')
|
|
919
919
|
})
|
|
920
920
|
|
|
921
|
+
test('a cache-reading ping keeps the card, a real reply and /clear drop it, and the demo draws it', async ($, on) => {
|
|
922
|
+
const b = boundaries(on, { keepWarm: true })
|
|
923
|
+
http(on)
|
|
924
|
+
judgeStub(on)
|
|
925
|
+
stepStub(on)
|
|
926
|
+
on('model.fork', () => ({ value: { isAnswered: true, text: 'ok', usage: USAGE } }))
|
|
927
|
+
await start($)
|
|
928
|
+
await step($)
|
|
929
|
+
await complete($)
|
|
930
|
+
await b.clock.advance(3375000)
|
|
931
|
+
// The ping read 40000 cache tokens: the card tells it, with the memo's minutes left.
|
|
932
|
+
const ui = await band($)
|
|
933
|
+
expect(await ui.find({ type: 'Text', text: /held 40k tokens in cache/ })).toBeDefined()
|
|
934
|
+
await ui.unmount()
|
|
935
|
+
// A prompt counts as having seen the card; the next is a real reply, which clears the count.
|
|
936
|
+
await submit($)
|
|
937
|
+
await step($)
|
|
938
|
+
await complete($)
|
|
939
|
+
const after = await band($)
|
|
940
|
+
expect(await after.find({ type: 'Text', text: /held 40k tokens in cache/ })).toBeUndefined()
|
|
941
|
+
await after.unmount()
|
|
942
|
+
await b.clock.advance(3375000)
|
|
943
|
+
await b.clock.settle()
|
|
944
|
+
const again = await band($)
|
|
945
|
+
expect(await again.find({ type: 'Text', text: /held 40k tokens in cache/ })).toBeDefined()
|
|
946
|
+
await again.unmount()
|
|
947
|
+
await $.classic.SessionStart({ source: 'clear' })
|
|
948
|
+
const cleared = await band($)
|
|
949
|
+
expect(await cleared.find({ type: 'Text', text: /held 40k tokens in cache/ })).toBeUndefined()
|
|
950
|
+
await cleared.unmount()
|
|
951
|
+
// The demo shows the card with sample values, whatever the state.
|
|
952
|
+
await command($, 'kept')
|
|
953
|
+
const demo = await band($)
|
|
954
|
+
expect(await demo.find({ type: 'Text', text: /held 312k tokens in cache/ })).toBeDefined()
|
|
955
|
+
})
|
|
956
|
+
|
|
921
957
|
test('fork turn.step is not effort-steered, busy or main-request cost', async ($, on) => {
|
|
922
958
|
const b = boundaries(on, { keepWarm: true })
|
|
923
959
|
http(on)
|
|
@@ -941,6 +977,23 @@ test('fork turn.step is not effort-steered, busy or main-request cost', async ($
|
|
|
941
977
|
expect(stats.text).toContain('Warm: 1 pings')
|
|
942
978
|
})
|
|
943
979
|
|
|
980
|
+
test('the flame turns keep-warm off for this chat only; Settings stay as they are', async ($, on) => {
|
|
981
|
+
const b = boundaries(on, { keepWarm: true })
|
|
982
|
+
http(on)
|
|
983
|
+
judgeStub(on, '{"effort":"high","why":"scope"}')
|
|
984
|
+
stepStub(on)
|
|
985
|
+
let forks = 0
|
|
986
|
+
on('model.fork', async () => { forks++; return { value: { isAnswered: true, text: 'ok', usage: USAGE } } })
|
|
987
|
+
await start($)
|
|
988
|
+
await submit($)
|
|
989
|
+
await step($)
|
|
990
|
+
await complete($)
|
|
991
|
+
await $.command.run({ command: 'ripple', origin: { kind: 'composer' }, presentation: { isFullscreen: true, columns: 140 }, args: 'warm' })
|
|
992
|
+
await b.clock.advance(3375000)
|
|
993
|
+
expect(forks).toBe(0)
|
|
994
|
+
expect(b.router().settings.keepWarm).toBe(true)
|
|
995
|
+
})
|
|
996
|
+
|
|
944
997
|
test('Haiku compaction returns replacement messages and forwards the keep note', async ($, on) => {
|
|
945
998
|
boundaries(on)
|
|
946
999
|
http(on)
|