@oar-sh/oar 0.9.16 → 0.9.17
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/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
- package/CHANGELOG.md +220 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +127 -29
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +71 -5
- package/server/db-schema.mjs +63 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/public/app/api-client.js +53 -0
- package/server/public/app/bootstrap.js +101 -13
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/journal-view.js +31 -3
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +10 -0
- package/server/public/index.html +408 -33
- package/server/repositories/conversation-group-repository.mjs +13 -6
- package/server/routes/conversation-group-routes.mjs +2 -1
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +192 -15
- package/server/routes/sessions-routes.mjs +364 -55
- package/server/server-runtime.mjs +123 -10
- package/server/services/about-service.mjs +1 -0
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/remote-relay-dispatcher.mjs +2 -1
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +2 -2
- package/shared/remote-relay-contract.mjs +1 -1
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
package/server/public/index.html
CHANGED
|
@@ -295,6 +295,16 @@
|
|
|
295
295
|
--provider-cursor: #a78bfa;
|
|
296
296
|
--provider-grok: #e879f9;
|
|
297
297
|
--provider-claude-cloud: #7dd3fc;
|
|
298
|
+
/* DeepSeek's blue, toward indigo so it reads apart from Copilot's. */
|
|
299
|
+
--provider-deepseek: #8b9dff;
|
|
300
|
+
/* The usage charts' series, in a fixed order (blue, orange, green,
|
|
301
|
+
amber; grey for "other"), stepped for the dark surface and checked
|
|
302
|
+
for colour-vision deficiency and contrast against it. */
|
|
303
|
+
--plan-usage-series-1: #3987e5;
|
|
304
|
+
--plan-usage-series-2: #d95926;
|
|
305
|
+
--plan-usage-series-3: #199e70;
|
|
306
|
+
--plan-usage-series-4: #c98500;
|
|
307
|
+
--plan-usage-series-5: #8b949e;
|
|
298
308
|
--viewport-top-offset: 0px;
|
|
299
309
|
--header-safe-top: max(env(safe-area-inset-top, 0px), var(--viewport-top-offset, 0px));
|
|
300
310
|
--chat-header-top-base-desktop: 14px;
|
|
@@ -359,6 +369,13 @@
|
|
|
359
369
|
--provider-cursor: #6b3fd4;
|
|
360
370
|
--provider-grok: #a21caf;
|
|
361
371
|
--provider-claude-cloud: #0369a1;
|
|
372
|
+
--provider-deepseek: #3b5bdb;
|
|
373
|
+
/* The same series order, stepped for the white surface. */
|
|
374
|
+
--plan-usage-series-1: #2a78d6;
|
|
375
|
+
--plan-usage-series-2: #eb6834;
|
|
376
|
+
--plan-usage-series-3: #1baf7a;
|
|
377
|
+
--plan-usage-series-4: #eda100;
|
|
378
|
+
--plan-usage-series-5: #656d76;
|
|
362
379
|
--inline-code-bg: #eff1f3;
|
|
363
380
|
}
|
|
364
381
|
:root {
|
|
@@ -987,6 +1004,7 @@
|
|
|
987
1004
|
.conv-provider-indicator[data-provider="claude"] { color: var(--provider-claude); }
|
|
988
1005
|
.conv-provider-indicator[data-provider="cursor"] { color: var(--provider-cursor); }
|
|
989
1006
|
.conv-provider-indicator[data-provider="grok"] { color: var(--provider-grok); }
|
|
1007
|
+
.conv-provider-indicator[data-provider="deepseek"] { color: var(--provider-deepseek); }
|
|
990
1008
|
.conv-provider-indicator[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
|
|
991
1009
|
.conv-item.worker-ui-offline .conv-title,
|
|
992
1010
|
#chat-title[data-ui-state="offline"] { color: var(--conversation-title-offline); }
|
|
@@ -1455,6 +1473,74 @@
|
|
|
1455
1473
|
font-size: var(--fs-meta-md);
|
|
1456
1474
|
width: min(260px, 100%);
|
|
1457
1475
|
}
|
|
1476
|
+
/* The DeepSeek tab's key, base URL and monthly budget take the text
|
|
1477
|
+
inputs' look (an input left to the browser is a white box in dark). */
|
|
1478
|
+
#settings-provider-panel-deepseek input[type="password"],
|
|
1479
|
+
#settings-provider-panel-deepseek input[type="url"],
|
|
1480
|
+
#settings-provider-panel-deepseek input[type="number"] {
|
|
1481
|
+
background: var(--bg);
|
|
1482
|
+
color: var(--text);
|
|
1483
|
+
border: 1px solid var(--border);
|
|
1484
|
+
border-radius: 6px;
|
|
1485
|
+
padding: 6px 12px;
|
|
1486
|
+
outline: none;
|
|
1487
|
+
font-size: var(--fs-meta-md);
|
|
1488
|
+
width: min(360px, 100%);
|
|
1489
|
+
}
|
|
1490
|
+
/* A select is as wide as its longest option (the commit attribution's
|
|
1491
|
+
runs to ~290 px): kept inside the panel on a narrow phone. */
|
|
1492
|
+
#settings-provider-panel-deepseek select {
|
|
1493
|
+
max-width: 100%;
|
|
1494
|
+
}
|
|
1495
|
+
/* "No longer used" under the starting balance: the warning look of the
|
|
1496
|
+
composer's model banner, apart from the help text above it. */
|
|
1497
|
+
#deepseek-starting-balance-status {
|
|
1498
|
+
background: var(--bg2);
|
|
1499
|
+
background: color-mix(in srgb, var(--warn) 14%, var(--bg2));
|
|
1500
|
+
border: 1px solid var(--warn);
|
|
1501
|
+
color: var(--warn);
|
|
1502
|
+
border-radius: 8px;
|
|
1503
|
+
padding: 6px 10px;
|
|
1504
|
+
margin-top: 6px;
|
|
1505
|
+
}
|
|
1506
|
+
/* Import usage export: the two file pickers in the panel's colours, their
|
|
1507
|
+
button drawn like the panel's other buttons. */
|
|
1508
|
+
.deepseek-usage-import {
|
|
1509
|
+
margin-top: 14px;
|
|
1510
|
+
padding-top: 12px;
|
|
1511
|
+
border-top: 1px solid var(--border);
|
|
1512
|
+
display: flex;
|
|
1513
|
+
flex-direction: column;
|
|
1514
|
+
gap: 6px;
|
|
1515
|
+
min-width: 0;
|
|
1516
|
+
}
|
|
1517
|
+
.deepseek-usage-import-files {
|
|
1518
|
+
display: grid;
|
|
1519
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1520
|
+
gap: 6px 10px;
|
|
1521
|
+
align-items: center;
|
|
1522
|
+
}
|
|
1523
|
+
.deepseek-usage-import input[type="file"] {
|
|
1524
|
+
color: var(--text);
|
|
1525
|
+
font-size: var(--fs-meta-md);
|
|
1526
|
+
min-width: 0;
|
|
1527
|
+
max-width: 100%;
|
|
1528
|
+
}
|
|
1529
|
+
.deepseek-usage-import input[type="file"]::file-selector-button {
|
|
1530
|
+
border: 1px solid var(--border);
|
|
1531
|
+
background: var(--bg2);
|
|
1532
|
+
color: var(--text);
|
|
1533
|
+
border-radius: 8px;
|
|
1534
|
+
height: 30px;
|
|
1535
|
+
padding: 0 10px;
|
|
1536
|
+
margin-right: 8px;
|
|
1537
|
+
font: inherit;
|
|
1538
|
+
cursor: pointer;
|
|
1539
|
+
}
|
|
1540
|
+
.deepseek-usage-import-status:empty { display: none; }
|
|
1541
|
+
@media (max-width: 520px) {
|
|
1542
|
+
.deepseek-usage-import-files { grid-template-columns: minmax(0, 1fr); }
|
|
1543
|
+
}
|
|
1458
1544
|
.settings-row input[type="checkbox"] {
|
|
1459
1545
|
width: 18px;
|
|
1460
1546
|
height: 18px;
|
|
@@ -2863,6 +2949,25 @@
|
|
|
2863
2949
|
padding: 2px 0;
|
|
2864
2950
|
}
|
|
2865
2951
|
.plan-usage-details summary:hover { color: var(--text); }
|
|
2952
|
+
/* A card's figures (plan-usage-view.mjs renderFact): label and hint on the
|
|
2953
|
+
left, the value on the right, one row each. */
|
|
2954
|
+
.plan-usage-facts {
|
|
2955
|
+
display: grid;
|
|
2956
|
+
gap: 8px;
|
|
2957
|
+
margin: 12px 0 0;
|
|
2958
|
+
padding-top: 10px;
|
|
2959
|
+
border-top: 1px solid var(--border);
|
|
2960
|
+
}
|
|
2961
|
+
.plan-usage-fact {
|
|
2962
|
+
display: flex;
|
|
2963
|
+
align-items: baseline;
|
|
2964
|
+
justify-content: space-between;
|
|
2965
|
+
gap: 12px;
|
|
2966
|
+
font-size: 0.84rem;
|
|
2967
|
+
}
|
|
2968
|
+
.plan-usage-fact dt { display: flex; flex-direction: column; gap: 1px; min-width: 0; font-weight: 500; }
|
|
2969
|
+
.plan-usage-fact-hint { font-size: 0.72rem; font-weight: 400; color: var(--muted); }
|
|
2970
|
+
.plan-usage-fact dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
2866
2971
|
.plan-usage-detail-note {
|
|
2867
2972
|
font-size: 0.74rem;
|
|
2868
2973
|
color: var(--muted);
|
|
@@ -2872,14 +2977,22 @@
|
|
|
2872
2977
|
.plan-usage-detail-table { width: 100%; border-collapse: collapse; margin-top: 6px; }
|
|
2873
2978
|
.plan-usage-detail-table td { padding: 5px 2px; border-bottom: 1px solid var(--border); }
|
|
2874
2979
|
.plan-usage-detail-table tr:last-child td { border-bottom: none; }
|
|
2875
|
-
|
|
2876
|
-
|
|
2980
|
+
/* The label cell stays a table cell (a flex cell does not stretch to the
|
|
2981
|
+
row, and its border then steps against the value's): the hint goes
|
|
2982
|
+
under the label as a block. */
|
|
2983
|
+
.plan-usage-detail-hint { display: block; margin-top: 2px; font-size: 0.7rem; color: var(--muted); }
|
|
2877
2984
|
.plan-usage-detail-value {
|
|
2878
2985
|
text-align: right;
|
|
2879
2986
|
font-variant-numeric: tabular-nums;
|
|
2880
2987
|
white-space: nowrap;
|
|
2881
2988
|
vertical-align: top;
|
|
2882
2989
|
}
|
|
2990
|
+
/* On a phone a long value (tokens by model) wraps instead of pushing the
|
|
2991
|
+
card wider than the screen. */
|
|
2992
|
+
@media (max-width: 520px) {
|
|
2993
|
+
.plan-usage-details .plan-usage-detail-value { white-space: normal; }
|
|
2994
|
+
}
|
|
2995
|
+
.plan-usage-detail-part { white-space: nowrap; }
|
|
2883
2996
|
.plan-usage-card-foot {
|
|
2884
2997
|
display: flex;
|
|
2885
2998
|
align-items: center;
|
|
@@ -2892,6 +3005,145 @@
|
|
|
2892
3005
|
}
|
|
2893
3006
|
.plan-usage-link { color: var(--accent); text-decoration: none; }
|
|
2894
3007
|
.plan-usage-link:hover { text-decoration: underline; }
|
|
3008
|
+
/* Usage charts (plan-usage-view.mjs renderChart): stacked columns and
|
|
3009
|
+
bars from the theme's series colours; text stays in the text colours. */
|
|
3010
|
+
.plan-usage-chart {
|
|
3011
|
+
margin: 12px 0 0;
|
|
3012
|
+
padding-top: 10px;
|
|
3013
|
+
border-top: 1px solid var(--border);
|
|
3014
|
+
}
|
|
3015
|
+
.plan-usage-chart-head {
|
|
3016
|
+
display: flex;
|
|
3017
|
+
align-items: baseline;
|
|
3018
|
+
justify-content: space-between;
|
|
3019
|
+
gap: 10px;
|
|
3020
|
+
flex-wrap: wrap;
|
|
3021
|
+
margin-bottom: 8px;
|
|
3022
|
+
}
|
|
3023
|
+
.plan-usage-chart-label { font-size: 0.88rem; font-weight: 600; }
|
|
3024
|
+
.plan-usage-chart-summary { font-size: 0.82rem; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
3025
|
+
.plan-usage-chart-columns {
|
|
3026
|
+
position: relative;
|
|
3027
|
+
display: grid;
|
|
3028
|
+
grid-template-columns: repeat(var(--plan-usage-columns, 30), minmax(0, 1fr));
|
|
3029
|
+
gap: 2px;
|
|
3030
|
+
height: 72px;
|
|
3031
|
+
padding-bottom: 18px;
|
|
3032
|
+
border-bottom: 1px solid var(--border);
|
|
3033
|
+
}
|
|
3034
|
+
.plan-usage-chart-col {
|
|
3035
|
+
position: relative;
|
|
3036
|
+
display: flex;
|
|
3037
|
+
align-items: flex-end;
|
|
3038
|
+
min-width: 0;
|
|
3039
|
+
height: 100%;
|
|
3040
|
+
border-radius: 2px;
|
|
3041
|
+
outline: none;
|
|
3042
|
+
}
|
|
3043
|
+
.plan-usage-chart-col:hover, .plan-usage-chart-col:focus-visible { background: var(--bg3); }
|
|
3044
|
+
.plan-usage-chart-col:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
|
|
3045
|
+
.plan-usage-chart-stack {
|
|
3046
|
+
display: flex;
|
|
3047
|
+
flex-direction: column-reverse;
|
|
3048
|
+
width: 100%;
|
|
3049
|
+
height: 100%;
|
|
3050
|
+
justify-content: flex-start;
|
|
3051
|
+
}
|
|
3052
|
+
.plan-usage-chart-col-empty .plan-usage-chart-stack::before {
|
|
3053
|
+
content: '';
|
|
3054
|
+
display: block;
|
|
3055
|
+
height: 2px;
|
|
3056
|
+
background: var(--border);
|
|
3057
|
+
}
|
|
3058
|
+
.plan-usage-chart-columns .plan-usage-chart-seg {
|
|
3059
|
+
display: block;
|
|
3060
|
+
width: 100%;
|
|
3061
|
+
min-height: 2px;
|
|
3062
|
+
margin-top: 2px;
|
|
3063
|
+
}
|
|
3064
|
+
.plan-usage-chart-columns .plan-usage-chart-seg:last-child { border-radius: 4px 4px 0 0; }
|
|
3065
|
+
.plan-usage-chart-axis {
|
|
3066
|
+
position: absolute;
|
|
3067
|
+
left: 0;
|
|
3068
|
+
right: 0;
|
|
3069
|
+
bottom: 0;
|
|
3070
|
+
display: flex;
|
|
3071
|
+
justify-content: space-between;
|
|
3072
|
+
font-size: 0.68rem;
|
|
3073
|
+
color: var(--muted);
|
|
3074
|
+
}
|
|
3075
|
+
.plan-usage-chart-bars { display: flex; flex-direction: column; gap: 8px; }
|
|
3076
|
+
.plan-usage-chart-row {
|
|
3077
|
+
position: relative;
|
|
3078
|
+
display: grid;
|
|
3079
|
+
grid-template-columns: minmax(80px, 1fr) 3fr auto;
|
|
3080
|
+
align-items: center;
|
|
3081
|
+
gap: 10px;
|
|
3082
|
+
font-size: 0.82rem;
|
|
3083
|
+
border-radius: 4px;
|
|
3084
|
+
outline: none;
|
|
3085
|
+
}
|
|
3086
|
+
.plan-usage-chart-row:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
|
|
3087
|
+
/* A model's name wraps on a narrow screen rather than being cut. */
|
|
3088
|
+
.plan-usage-chart-row-label { min-width: 0; line-height: 1.25; overflow-wrap: anywhere; }
|
|
3089
|
+
.plan-usage-chart-row-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
3090
|
+
.plan-usage-chart-bar {
|
|
3091
|
+
display: flex;
|
|
3092
|
+
gap: 2px;
|
|
3093
|
+
height: 14px;
|
|
3094
|
+
border-radius: 4px;
|
|
3095
|
+
overflow: hidden;
|
|
3096
|
+
background: var(--bg3);
|
|
3097
|
+
}
|
|
3098
|
+
.plan-usage-chart-bar .plan-usage-chart-seg { display: block; height: 100%; min-width: 2px; }
|
|
3099
|
+
.plan-usage-chart-bar .plan-usage-chart-seg:first-child { border-radius: 4px 0 0 4px; }
|
|
3100
|
+
.plan-usage-chart-bar .plan-usage-chart-seg:last-child { border-radius: 0 4px 4px 0; }
|
|
3101
|
+
.plan-usage-chart-tip {
|
|
3102
|
+
display: none;
|
|
3103
|
+
position: absolute;
|
|
3104
|
+
bottom: calc(100% + 4px);
|
|
3105
|
+
left: 50%;
|
|
3106
|
+
transform: translateX(-50%);
|
|
3107
|
+
z-index: 2;
|
|
3108
|
+
max-width: 260px;
|
|
3109
|
+
padding: 4px 8px;
|
|
3110
|
+
border: 1px solid var(--border);
|
|
3111
|
+
border-radius: 6px;
|
|
3112
|
+
background: var(--bg2);
|
|
3113
|
+
color: var(--text);
|
|
3114
|
+
font-size: 0.74rem;
|
|
3115
|
+
line-height: 1.4;
|
|
3116
|
+
white-space: normal;
|
|
3117
|
+
text-align: center;
|
|
3118
|
+
pointer-events: none;
|
|
3119
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
3120
|
+
}
|
|
3121
|
+
.plan-usage-chart-col:hover .plan-usage-chart-tip,
|
|
3122
|
+
.plan-usage-chart-col:focus .plan-usage-chart-tip,
|
|
3123
|
+
.plan-usage-chart-row:hover .plan-usage-chart-tip,
|
|
3124
|
+
.plan-usage-chart-row:focus .plan-usage-chart-tip { display: block; }
|
|
3125
|
+
.plan-usage-chart-col .plan-usage-chart-tip { width: 180px; }
|
|
3126
|
+
.plan-usage-chart-col:first-child .plan-usage-chart-tip,
|
|
3127
|
+
.plan-usage-chart-col:nth-child(-n+6) .plan-usage-chart-tip { left: 0; transform: none; text-align: left; }
|
|
3128
|
+
.plan-usage-chart-col:nth-last-child(-n+7):not(.plan-usage-chart-axis) .plan-usage-chart-tip { left: auto; right: 0; transform: none; text-align: right; }
|
|
3129
|
+
.plan-usage-chart-legend {
|
|
3130
|
+
display: flex;
|
|
3131
|
+
flex-wrap: wrap;
|
|
3132
|
+
gap: 4px 14px;
|
|
3133
|
+
margin: 8px 0 0;
|
|
3134
|
+
padding: 0;
|
|
3135
|
+
list-style: none;
|
|
3136
|
+
font-size: 0.74rem;
|
|
3137
|
+
color: var(--muted);
|
|
3138
|
+
}
|
|
3139
|
+
.plan-usage-chart-legend li { display: inline-flex; align-items: center; gap: 5px; }
|
|
3140
|
+
.plan-usage-chart-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 3px; }
|
|
3141
|
+
/* A table wider than a narrow screen scrolls inside the card. */
|
|
3142
|
+
.plan-usage-chart-table { margin-top: 6px; font-size: 0.78rem; max-width: 100%; overflow-x: auto; }
|
|
3143
|
+
.plan-usage-chart-table summary { cursor: pointer; color: var(--muted); padding: 2px 0; }
|
|
3144
|
+
.plan-usage-chart-table summary:hover { color: var(--text); }
|
|
3145
|
+
.plan-usage-chart-table th { text-align: left; font-weight: 500; color: var(--muted); padding: 4px 2px; border-bottom: 1px solid var(--border); }
|
|
3146
|
+
.plan-usage-chart-table thead th:not(:first-child) { text-align: right; }
|
|
2895
3147
|
/* About OAR: the plan-usage cards and tables, stacked. */
|
|
2896
3148
|
.about-modal { display: flex; flex-direction: column; gap: 12px; }
|
|
2897
3149
|
.about-section .plan-usage-card-head { margin-bottom: 4px; }
|
|
@@ -3186,6 +3438,7 @@
|
|
|
3186
3438
|
#model-lock-note[data-provider="claude"] { color: var(--provider-claude); }
|
|
3187
3439
|
#model-lock-note[data-provider="cursor"] { color: var(--provider-cursor); }
|
|
3188
3440
|
#model-lock-note[data-provider="grok"] { color: var(--provider-grok); }
|
|
3441
|
+
#model-lock-note[data-provider="deepseek"] { color: var(--provider-deepseek); }
|
|
3189
3442
|
#model-lock-note[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
|
|
3190
3443
|
/* Claude Cloud conversations (claude-cloud-conversation-ui.js): where the
|
|
3191
3444
|
session runs and what it pushed, above the composer like the lock note.
|
|
@@ -6389,37 +6642,51 @@
|
|
|
6389
6642
|
.conv-item.conv-drag-ghost { margin: -18px 0 0; }
|
|
6390
6643
|
.conv-item.conv-drop-before::before,
|
|
6391
6644
|
.conv-item.conv-drop-after::after { height: 4px; }
|
|
6645
|
+
/* Two rows on a phone: ☰, the title and the buttons on the first; the
|
|
6646
|
+
origin line, the chips and the folder on a second row across the whole
|
|
6647
|
+
header. The title column dissolves (display: contents) so its lines
|
|
6648
|
+
become the header's own flex items; ::after breaks the row after the
|
|
6649
|
+
buttons. The row gap is 0 and the second row's items carry their own
|
|
6650
|
+
top margin, so a header with nothing below the title stays one row. */
|
|
6392
6651
|
#chat-header {
|
|
6393
6652
|
padding: var(--chat-header-top-pad-mobile) 10px 10px;
|
|
6394
|
-
gap:
|
|
6395
|
-
flex-wrap:
|
|
6653
|
+
gap: 0 8px;
|
|
6654
|
+
flex-wrap: wrap;
|
|
6396
6655
|
align-items: center;
|
|
6397
6656
|
position: relative;
|
|
6398
6657
|
z-index: 30;
|
|
6399
6658
|
}
|
|
6400
|
-
#chat-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
align-items: flex-start;
|
|
6406
|
-
gap: 6px;
|
|
6407
|
-
}
|
|
6408
|
-
#chat-title-slot {
|
|
6409
|
-
display: flex;
|
|
6410
|
-
flex-direction: column;
|
|
6411
|
-
flex: 1 1 auto;
|
|
6412
|
-
width: auto;
|
|
6413
|
-
margin: 0;
|
|
6414
|
-
min-width: 0;
|
|
6659
|
+
#chat-header::after {
|
|
6660
|
+
content: '';
|
|
6661
|
+
order: 4;
|
|
6662
|
+
flex: 0 0 100%;
|
|
6663
|
+
height: 0;
|
|
6415
6664
|
}
|
|
6665
|
+
#chat-title-wrap,
|
|
6666
|
+
#chat-title-slot { display: contents; }
|
|
6416
6667
|
#chat-title-primary {
|
|
6417
|
-
|
|
6668
|
+
order: 2;
|
|
6669
|
+
flex: 1 1 0;
|
|
6418
6670
|
min-width: 0;
|
|
6419
6671
|
}
|
|
6672
|
+
#chat-title-origin,
|
|
6673
|
+
#chat-title-relay-events,
|
|
6674
|
+
#chat-title-relay-schedules,
|
|
6675
|
+
#chat-title-cwd,
|
|
6676
|
+
#chat-title-session-usage {
|
|
6677
|
+
order: 5;
|
|
6678
|
+
flex: 0 1 auto;
|
|
6679
|
+
max-width: 100%;
|
|
6680
|
+
margin-top: 4px;
|
|
6681
|
+
}
|
|
6682
|
+
#chat-title-relay-events,
|
|
6683
|
+
#chat-title-relay-schedules { flex-shrink: 0; }
|
|
6684
|
+
#chat-title-relay-events .relay-watch-chip,
|
|
6685
|
+
#chat-title-relay-events .conv-watched-badge,
|
|
6686
|
+
#chat-title-relay-schedules .relay-watch-chip { white-space: nowrap; }
|
|
6687
|
+
#chat-title-primary { display: block; }
|
|
6420
6688
|
#chat-title-cwd {
|
|
6421
6689
|
display: block;
|
|
6422
|
-
margin-top: 1px;
|
|
6423
6690
|
font-size: clamp(0.62rem, 2vw, 0.7rem);
|
|
6424
6691
|
line-height: 1.2;
|
|
6425
6692
|
min-width: 0;
|
|
@@ -6429,7 +6696,6 @@
|
|
|
6429
6696
|
}
|
|
6430
6697
|
#chat-title-session-usage {
|
|
6431
6698
|
display: block;
|
|
6432
|
-
margin-top: 1px;
|
|
6433
6699
|
font-size: clamp(0.62rem, 2vw, 0.7rem);
|
|
6434
6700
|
line-height: 1.2;
|
|
6435
6701
|
min-width: 0;
|
|
@@ -6437,10 +6703,7 @@
|
|
|
6437
6703
|
overflow: hidden;
|
|
6438
6704
|
text-overflow: ellipsis;
|
|
6439
6705
|
}
|
|
6440
|
-
#chat-title-origin {
|
|
6441
|
-
margin-top: 1px;
|
|
6442
|
-
font-size: clamp(0.62rem, 2vw, 0.7rem);
|
|
6443
|
-
}
|
|
6706
|
+
#chat-title-origin { font-size: clamp(0.62rem, 2vw, 0.7rem); }
|
|
6444
6707
|
#chat-header-actions {
|
|
6445
6708
|
margin-left: auto;
|
|
6446
6709
|
}
|
|
@@ -6449,6 +6712,7 @@
|
|
|
6449
6712
|
flex: 0 0 auto;
|
|
6450
6713
|
}
|
|
6451
6714
|
#chat-title-editor {
|
|
6715
|
+
order: 2;
|
|
6452
6716
|
flex: 1 0 100%;
|
|
6453
6717
|
width: 100%;
|
|
6454
6718
|
flex-wrap: wrap;
|
|
@@ -6457,9 +6721,6 @@
|
|
|
6457
6721
|
body.chat-title-editing #chat-header-actions {
|
|
6458
6722
|
align-self: flex-start;
|
|
6459
6723
|
}
|
|
6460
|
-
body.chat-title-editing #chat-title-wrap {
|
|
6461
|
-
z-index: 40;
|
|
6462
|
-
}
|
|
6463
6724
|
body.chat-title-editing #chat-title-editor {
|
|
6464
6725
|
position: fixed;
|
|
6465
6726
|
left: 10px;
|
|
@@ -6504,7 +6765,6 @@
|
|
|
6504
6765
|
color: var(--relay-tone-tunnelled);
|
|
6505
6766
|
text-shadow: 0 0 8px rgba(210, 153, 34, 0.35);
|
|
6506
6767
|
}
|
|
6507
|
-
#chat-title-wrap { order: 2; }
|
|
6508
6768
|
#chat-header-actions { order: 3; }
|
|
6509
6769
|
#chat-title { font-size: clamp(0.92rem, 3vw, 0.98rem); }
|
|
6510
6770
|
#chat-title-input,
|
|
@@ -6777,13 +7037,20 @@
|
|
|
6777
7037
|
flex: 0 0 auto;
|
|
6778
7038
|
min-width: 40px;
|
|
6779
7039
|
}
|
|
6780
|
-
/* About OAR: the same, with Refresh
|
|
6781
|
-
|
|
7040
|
+
/* About OAR, Plan usage and Settings: the same, with Refresh (where
|
|
7041
|
+
there is one) beside the close button, and the title cut with an
|
|
7042
|
+
ellipsis if it must be. */
|
|
7043
|
+
#summary-modal[data-kind="about"] .summary-header-actions,
|
|
7044
|
+
#summary-modal[data-kind="usage"] .summary-header-actions,
|
|
7045
|
+
#settings-modal .summary-header-actions {
|
|
6782
7046
|
width: auto;
|
|
6783
7047
|
flex: 0 0 auto;
|
|
6784
7048
|
}
|
|
6785
7049
|
#summary-modal[data-kind="about"] .summary-header-actions .summary-btn,
|
|
6786
|
-
#summary-modal[data-kind="about"] .summary-header-actions .summary-close
|
|
7050
|
+
#summary-modal[data-kind="about"] .summary-header-actions .summary-close,
|
|
7051
|
+
#summary-modal[data-kind="usage"] .summary-header-actions .summary-btn,
|
|
7052
|
+
#summary-modal[data-kind="usage"] .summary-header-actions .summary-close,
|
|
7053
|
+
#settings-modal .summary-header-actions .summary-close {
|
|
6787
7054
|
flex: 0 0 auto;
|
|
6788
7055
|
min-width: 40px;
|
|
6789
7056
|
}
|
|
@@ -7610,6 +7877,16 @@
|
|
|
7610
7877
|
tabindex="-1"
|
|
7611
7878
|
data-settings-provider-tab="cursor"
|
|
7612
7879
|
>Cursor</button>
|
|
7880
|
+
<button
|
|
7881
|
+
id="settings-provider-tab-deepseek"
|
|
7882
|
+
class="settings-tab settings-subtab"
|
|
7883
|
+
type="button"
|
|
7884
|
+
role="tab"
|
|
7885
|
+
aria-selected="false"
|
|
7886
|
+
aria-controls="settings-provider-panel-deepseek"
|
|
7887
|
+
tabindex="-1"
|
|
7888
|
+
data-settings-provider-tab="deepseek"
|
|
7889
|
+
>DeepSeek</button>
|
|
7613
7890
|
</div>
|
|
7614
7891
|
<div
|
|
7615
7892
|
id="settings-provider-panel-copilot"
|
|
@@ -8047,6 +8324,104 @@
|
|
|
8047
8324
|
<div class="settings-help">Unlocks the live percentage bars on Check Usage: "Plan usage (total)", "Auto + Composer" and "API". Detected automatically from this machine's Cursor IDE login when one exists; paste the <code>WorkosCursorSessionToken</code> cookie from a logged-in cursor.com tab (DevTools → Application → Cookies) only for headless relay hosts or to use a different account. Headless hosts can also set <code>CURSOR_SESSION_TOKEN</code> in the relay environment. Tokens stay on the relay.</div>
|
|
8048
8325
|
</div>
|
|
8049
8326
|
</div>
|
|
8327
|
+
<div
|
|
8328
|
+
id="settings-provider-panel-deepseek"
|
|
8329
|
+
class="settings-panel"
|
|
8330
|
+
role="tabpanel"
|
|
8331
|
+
tabindex="0"
|
|
8332
|
+
aria-labelledby="settings-provider-tab-deepseek"
|
|
8333
|
+
data-settings-provider-panel="deepseek"
|
|
8334
|
+
hidden
|
|
8335
|
+
>
|
|
8336
|
+
<div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
|
|
8337
|
+
<label for="deepseek-api-key-input">DeepSeek</label>
|
|
8338
|
+
<div class="settings-row" style="width: 100%; margin-bottom: 8px;">
|
|
8339
|
+
<label for="deepseek-enabled-toggle">Enable DeepSeek for New Chat model selection</label>
|
|
8340
|
+
<input id="deepseek-enabled-toggle" type="checkbox" onchange="toggleDeepSeekProvider(this.checked)" disabled>
|
|
8341
|
+
</div>
|
|
8342
|
+
<input
|
|
8343
|
+
id="deepseek-api-key-input"
|
|
8344
|
+
type="password"
|
|
8345
|
+
autocomplete="off"
|
|
8346
|
+
placeholder="DeepSeek API key…"
|
|
8347
|
+
aria-label="DeepSeek API key"
|
|
8348
|
+
>
|
|
8349
|
+
<label for="deepseek-model-select" style="margin-top: 8px;">Default model</label>
|
|
8350
|
+
<select id="deepseek-model-select" aria-label="DeepSeek default model">
|
|
8351
|
+
<option value="deepseek-flash">deepseek-flash</option>
|
|
8352
|
+
<option value="deepseek-v4-pro">deepseek-v4-pro</option>
|
|
8353
|
+
</select>
|
|
8354
|
+
<label for="deepseek-attribution-select" style="margin-top: 8px;">Commit attribution</label>
|
|
8355
|
+
<select id="deepseek-attribution-select" aria-label="DeepSeek commit attribution">
|
|
8356
|
+
<option value="oar">OAR: Co-authored-by: Open Agent Relay (DeepSeek …)</option>
|
|
8357
|
+
<option value="off">Off: no attribution</option>
|
|
8358
|
+
</select>
|
|
8359
|
+
<label for="deepseek-monthly-budget-input" style="margin-top: 8px;">Monthly budget (optional)</label>
|
|
8360
|
+
<input
|
|
8361
|
+
id="deepseek-monthly-budget-input"
|
|
8362
|
+
type="number"
|
|
8363
|
+
inputmode="decimal"
|
|
8364
|
+
min="0"
|
|
8365
|
+
step="0.01"
|
|
8366
|
+
autocomplete="off"
|
|
8367
|
+
placeholder="No budget bar"
|
|
8368
|
+
aria-label="DeepSeek monthly budget"
|
|
8369
|
+
>
|
|
8370
|
+
<label for="deepseek-monthly-budget-currency-select" style="margin-top: 8px;">Budget currency</label>
|
|
8371
|
+
<select id="deepseek-monthly-budget-currency-select" aria-label="DeepSeek monthly budget currency">
|
|
8372
|
+
<option value="USD">USD ($)</option>
|
|
8373
|
+
<option value="CNY">CNY (¥)</option>
|
|
8374
|
+
<option value="">The balance's currency (may change)</option>
|
|
8375
|
+
</select>
|
|
8376
|
+
<div class="settings-help">The usage card shows this relay's estimated DeepSeek spend of the calendar month (UTC) against the budget, in the budget's currency, amber near the budget and red over it. Leave the amount empty for no budget bar.</div>
|
|
8377
|
+
<label for="deepseek-starting-balance-input" style="margin-top: 8px;">Starting balance (optional)</label>
|
|
8378
|
+
<input
|
|
8379
|
+
id="deepseek-starting-balance-input"
|
|
8380
|
+
type="number"
|
|
8381
|
+
inputmode="decimal"
|
|
8382
|
+
min="0"
|
|
8383
|
+
step="0.01"
|
|
8384
|
+
autocomplete="off"
|
|
8385
|
+
placeholder="The balance after the last top-up"
|
|
8386
|
+
aria-label="DeepSeek starting balance"
|
|
8387
|
+
aria-describedby="deepseek-starting-balance-help"
|
|
8388
|
+
>
|
|
8389
|
+
<div id="deepseek-starting-balance-help" class="settings-help">The usage card's balance bar measures what is left against this amount, in the balance's currency, for example your first top-up. Leave it empty to measure from the balance after the last top-up this relay has seen (before it has seen one, the highest balance it has read).</div>
|
|
8390
|
+
<div id="deepseek-starting-balance-status" class="settings-help" hidden></div>
|
|
8391
|
+
<details id="deepseek-advanced" style="margin-top: 8px;">
|
|
8392
|
+
<summary>Advanced</summary>
|
|
8393
|
+
<label for="deepseek-base-url-input" style="margin-top: 8px;">Base URL</label>
|
|
8394
|
+
<input
|
|
8395
|
+
id="deepseek-base-url-input"
|
|
8396
|
+
type="url"
|
|
8397
|
+
autocomplete="off"
|
|
8398
|
+
placeholder="https://api.deepseek.com"
|
|
8399
|
+
aria-label="DeepSeek base URL"
|
|
8400
|
+
>
|
|
8401
|
+
<div class="settings-help">Leave empty for DeepSeek's own API. Another address must use https (http only on this machine, for a local proxy); the key is sent there and nowhere else.</div>
|
|
8402
|
+
</details>
|
|
8403
|
+
<div id="deepseek-settings-status" class="settings-help settings-provider-status">Loading DeepSeek settings…</div>
|
|
8404
|
+
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
|
8405
|
+
<button id="deepseek-save-btn" type="button" class="summary-btn" onclick="saveDeepSeekSettings()">Save DeepSeek settings</button>
|
|
8406
|
+
<button id="deepseek-remove-btn" type="button" class="summary-btn" onclick="removeDeepSeekSettings()">Remove saved key</button>
|
|
8407
|
+
</div>
|
|
8408
|
+
<div class="settings-help">DeepSeek chats run Claude Code on the relay host against DeepSeek's Anthropic-compatible API, with all four modes, tools and question cards. The key is stored on the relay host and never sent to browsers; saving it lists DeepSeek's models. Turns are billed to the DeepSeek account the key belongs to.</div>
|
|
8409
|
+
<div id="deepseek-usage-import" class="deepseek-usage-import" role="group" aria-labelledby="deepseek-usage-import-title">
|
|
8410
|
+
<label id="deepseek-usage-import-title">Import usage export</label>
|
|
8411
|
+
<div class="settings-help">DeepSeek's usage page exports two files for a period, amount and cost. Imported here, their figures for the saved key replace the estimate on the usage card for the days they cover; the other days stay estimated.</div>
|
|
8412
|
+
<div class="deepseek-usage-import-files">
|
|
8413
|
+
<label for="deepseek-usage-amount-input" class="settings-help">Amount file</label>
|
|
8414
|
+
<input id="deepseek-usage-amount-input" type="file" accept=".csv,text/csv" aria-label="DeepSeek usage export: amount file">
|
|
8415
|
+
<label for="deepseek-usage-cost-input" class="settings-help">Cost file</label>
|
|
8416
|
+
<input id="deepseek-usage-cost-input" type="file" accept=".csv,text/csv" aria-label="DeepSeek usage export: cost file">
|
|
8417
|
+
</div>
|
|
8418
|
+
<div>
|
|
8419
|
+
<button id="deepseek-usage-import-btn" type="button" class="summary-btn" onclick="importDeepSeekUsageExport()">Import usage export</button>
|
|
8420
|
+
</div>
|
|
8421
|
+
<div id="deepseek-usage-import-status" class="settings-help settings-provider-status deepseek-usage-import-status" role="status" aria-live="polite"></div>
|
|
8422
|
+
</div>
|
|
8423
|
+
</div>
|
|
8424
|
+
</div>
|
|
8050
8425
|
</div>
|
|
8051
8426
|
<div
|
|
8052
8427
|
id="settings-panel-relays"
|
|
@@ -18,7 +18,9 @@ import { compareGroupMembers } from '../public/app/conversation-group-order.mjs'
|
|
|
18
18
|
//
|
|
19
19
|
// A group can have a hand order (migration 0015): once the owner drags a row
|
|
20
20
|
// into place, every member after the head gets a `group_position` (10, 20,
|
|
21
|
-
// 30, ...) and keeps it; later
|
|
21
|
+
// 30, ...) and keeps it; a later arrival (a session created in the group, a
|
|
22
|
+
// row the menu moves in) goes first, right under the head, and the next
|
|
23
|
+
// drag renumbers from 10 again. A group nobody
|
|
22
24
|
// reordered keeps NULL positions and the activity order. Without the column
|
|
23
25
|
// the hand order is off and moves only change the membership.
|
|
24
26
|
//
|
|
@@ -141,8 +143,8 @@ export function createConversationGroupRepository(db) {
|
|
|
141
143
|
WHERE group_id = ? AND status != 'deleted'
|
|
142
144
|
ORDER BY updated_at DESC
|
|
143
145
|
`);
|
|
144
|
-
const
|
|
145
|
-
? db.prepare(`SELECT
|
|
146
|
+
const minPosition = hasPosition
|
|
147
|
+
? db.prepare(`SELECT MIN(group_position) AS n FROM conversations WHERE group_id = ? AND status != 'deleted'`)
|
|
146
148
|
: null;
|
|
147
149
|
const countMembers = db.prepare(`SELECT COUNT(*) AS n FROM conversations WHERE group_id = ? AND status != 'deleted'`);
|
|
148
150
|
// Groups with a live member, the one with the latest activity first, then
|
|
@@ -188,11 +190,16 @@ export function createConversationGroupRepository(db) {
|
|
|
188
190
|
|
|
189
191
|
const positionValue = (row) => (row?.group_position == null ? null : Number(row.group_position));
|
|
190
192
|
|
|
191
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Where a row joining `groupId` goes: before the first placed member (the
|
|
195
|
+
* owner sees a new session at the top of its group, under the head), or
|
|
196
|
+
* NULL when nobody ordered the group, where newest-first puts it on top
|
|
197
|
+
* anyway. Positions may go to zero and below; the next drag renumbers.
|
|
198
|
+
*/
|
|
192
199
|
function joiningPosition(groupId) {
|
|
193
200
|
if (!hasPosition) return null;
|
|
194
|
-
const
|
|
195
|
-
return
|
|
201
|
+
const min = minPosition.get(toText(groupId))?.n;
|
|
202
|
+
return min == null ? null : Number(min) - POSITION_STEP;
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
const liveConversation = (id) => {
|
|
@@ -129,7 +129,8 @@ export function registerConversationGroupRoutes(app, deps) {
|
|
|
129
129
|
// a conversation into a group. `groupId: null` takes it out (as DELETE).
|
|
130
130
|
// With `beforeConversationId` in the body (null: to the end) it is a drag:
|
|
131
131
|
// the row lands before that one and the group gets a hand order; without
|
|
132
|
-
// it the row joins
|
|
132
|
+
// it the row joins a hand-ordered group first, under the head, as the menu
|
|
133
|
+
// does (and the next drag renumbers).
|
|
133
134
|
app.put('/api/conversation/:id/group', auth, (req, res) => {
|
|
134
135
|
if (refuseRemote(req, res)) return;
|
|
135
136
|
if (!conversationGroups?.enabled) return unavailable(res);
|