Skip to content

Commit 79124db

Browse files
committed
fix(provider): use native upgrade commands and recalc usage status
- switch Claude and OpenCode update commands to their native CLIs - derive provider usage status from current windows and drop stale warnings - use the latest user message for thread activity and restyle quoted context labels
1 parent 43866b2 commit 79124db

7 files changed

Lines changed: 95 additions & 36 deletions

File tree

apps/server/src/provider/providerVersionLifecycle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("providerVersionLifecycle", () => {
4545
status: "behind_latest",
4646
currentVersion: "2.1.110",
4747
latestVersion: "2.1.117",
48-
updateCommand: "npm install -g @anthropic-ai/claude-code@latest",
48+
updateCommand: "claude upgrade",
4949
canUpdate: true,
5050
message: "Install the update now or review provider settings.",
5151
});

apps/server/src/provider/providerVersionLifecycle.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ const PROVIDER_VERSION_LIFECYCLES = {
3131
claudeAgent: {
3232
provider: "claudeAgent",
3333
packageName: "@anthropic-ai/claude-code",
34-
updateCommand: "npm install -g @anthropic-ai/claude-code@latest",
35-
updateExecutable: "npm",
36-
updateArgs: ["install", "-g", "@anthropic-ai/claude-code@latest"],
37-
updateLockKey: "npm-global",
34+
updateCommand: "claude upgrade",
35+
updateExecutable: "claude",
36+
updateArgs: ["upgrade"],
37+
updateLockKey: "claude",
3838
},
3939
cursor: {
4040
provider: "cursor",
@@ -47,10 +47,10 @@ const PROVIDER_VERSION_LIFECYCLES = {
4747
opencode: {
4848
provider: "opencode",
4949
packageName: "opencode-ai",
50-
updateCommand: "npm install -g opencode-ai@latest",
51-
updateExecutable: "npm",
52-
updateArgs: ["install", "-g", "opencode-ai@latest"],
53-
updateLockKey: "npm-global",
50+
updateCommand: "opencode upgrade",
51+
updateExecutable: "opencode",
52+
updateArgs: ["upgrade"],
53+
updateLockKey: "opencode",
5454
},
5555
} as const satisfies Record<ProviderKind, ProviderVersionLifecycle>;
5656

apps/web/src/components/ChatView.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import {
106106
useStore,
107107
} from "../store";
108108
import {
109+
createSidebarThreadSummarySelectorByRef,
109110
createProjectSelectorByRef,
110111
createThreadSelectorByRef,
111112
createThreadSelectorAcrossEnvironments,
@@ -320,12 +321,16 @@ const threadPlanCatalogCache = new LRUCache<{
320321
entry: ThreadPlanCatalogEntry;
321322
}>(MAX_THREAD_PLAN_CATALOG_CACHE_ENTRIES, MAX_THREAD_PLAN_CATALOG_CACHE_MEMORY_BYTES);
322323

323-
function deriveActiveThreadActivityIso(thread: Thread): string {
324+
function deriveActiveThreadActivityIso(
325+
thread: Thread,
326+
latestUserMessageAt: string | null | undefined,
327+
): string | undefined {
328+
if (latestUserMessageAt) return latestUserMessageAt;
324329
for (let i = thread.messages.length - 1; i >= 0; i--) {
325330
const message = thread.messages[i];
326331
if (message?.role === "user") return message.createdAt;
327332
}
328-
return thread.updatedAt ?? thread.createdAt;
333+
return undefined;
329334
}
330335

331336
function estimateThreadPlanCatalogEntrySize(thread: Thread): number {
@@ -767,6 +772,9 @@ export default function ChatView({
767772
: scopeThreadRef("" as EnvironmentId, threadId),
768773
[activeThreadEnvironmentId, threadId],
769774
);
775+
const activeSidebarThreadSummary = useStore(
776+
useMemo(() => createSidebarThreadSummarySelectorByRef(threadRef), [threadRef]),
777+
);
770778
const setStoreThreadError = useStore((store) => store.setError);
771779
const markThreadVisited = useUiStateStore((store) => store.markThreadVisited);
772780
const activeThreadLastVisitedAt = useUiStateStore(
@@ -4790,7 +4798,10 @@ export default function ChatView({
47904798
activeThreadId={activeThread.id}
47914799
activeThreadEnvironmentId={activeThreadEnvironmentId!}
47924800
activeThreadTitle={activeThread.title}
4793-
activeThreadActivityAt={deriveActiveThreadActivityIso(activeThread)}
4801+
activeThreadActivityAt={deriveActiveThreadActivityIso(
4802+
activeThread,
4803+
activeSidebarThreadSummary?.latestUserMessageAt,
4804+
)}
47944805
activeProjectName={activeProject?.name}
47954806
activeProjectCwd={activeProject?.cwd ?? null}
47964807
isGitRepo={isGitRepo}

apps/web/src/components/chat/UserMessageQuotedContextLabel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ function QuotedContextEntry({ ctx, panelId }: { ctx: ParsedQuotedContextEntry; p
4242
"rounded-md rounded-l-none border-l-2 transition-colors",
4343
isDiff
4444
? "border-emerald-400/70 bg-emerald-500/8 hover:bg-emerald-500/12 dark:border-emerald-400/70 dark:bg-emerald-400/8 dark:hover:bg-emerald-400/12"
45-
: "border-violet-400/70 bg-violet-500/8 hover:bg-violet-500/12 dark:border-violet-400/70 dark:bg-violet-400/8 dark:hover:bg-violet-400/12",
45+
: "border-primary/60 bg-primary/15 hover:bg-primary/20",
4646
);
4747

4848
const headerClass = cn(
4949
"flex w-full items-center gap-1.5 rounded-md rounded-l-none px-2.5 py-1.5 text-left text-xs font-medium",
50-
isDiff ? "text-emerald-700 dark:text-emerald-300" : "text-violet-700 dark:text-violet-300",
50+
isDiff ? "text-emerald-700 dark:text-emerald-300" : "text-primary",
5151
);
5252

5353
const headerInner = (

apps/web/src/components/settings/SettingsPanels.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const PROVIDER_SETTINGS: readonly InstallProviderSettings[] = [
147147
title: "Claude",
148148
binaryPlaceholder: "Claude binary path",
149149
binaryDescription: "Path to the Claude binary",
150-
defaultUpdateCommand: "npm install -g @anthropic-ai/claude-code@latest",
150+
defaultUpdateCommand: "claude upgrade",
151151
},
152152
{
153153
provider: "cursor",
@@ -162,7 +162,7 @@ const PROVIDER_SETTINGS: readonly InstallProviderSettings[] = [
162162
title: "OpenCode",
163163
binaryPlaceholder: "OpenCode binary path",
164164
binaryDescription: "Path to the OpenCode binary",
165-
defaultUpdateCommand: "npm install -g opencode-ai@latest",
165+
defaultUpdateCommand: "opencode upgrade",
166166
serverUrlPlaceholder: "http://127.0.0.1:4096",
167167
serverUrlDescription: "Leave blank to let MarCode spawn the server when needed",
168168
serverPasswordPlaceholder: "Server password (optional)",

apps/web/src/lib/providerUsage.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,55 @@ describe("providerUsage", () => {
4040
]);
4141
});
4242

43+
it("does not keep an old Claude warning after the same window resets", () => {
44+
const snapshot = deriveLatestProviderUsageSnapshot([
45+
makeActivity("activity-1", {
46+
type: "rate_limit_event",
47+
rate_limit_info: {
48+
status: "allowed_warning",
49+
resetsAt: 1776582000,
50+
rateLimitType: "five_hour",
51+
utilization: 0.97,
52+
},
53+
}),
54+
makeActivity("activity-2", {
55+
type: "rate_limit_event",
56+
rate_limit_info: {
57+
status: "allowed",
58+
resetsAt: 1776600000,
59+
rateLimitType: "five_hour",
60+
utilization: 0,
61+
},
62+
}),
63+
]);
64+
65+
expect(snapshot?.providerLabel).toBe("Claude");
66+
expect(snapshot?.status).toBe("ok");
67+
expect(snapshot?.windows).toEqual([
68+
{
69+
label: "Session (5 hrs)",
70+
usedPercent: 0,
71+
resetsAt: 1776600000,
72+
},
73+
]);
74+
});
75+
76+
it("marks current provider usage as rejected at 90 percent or higher", () => {
77+
const snapshot = deriveLatestProviderUsageSnapshot([
78+
makeActivity("activity-1", {
79+
type: "rate_limit_event",
80+
rate_limit_info: {
81+
status: "allowed_warning",
82+
resetsAt: 1776582000,
83+
rateLimitType: "five_hour",
84+
utilization: 0.97,
85+
},
86+
}),
87+
]);
88+
89+
expect(snapshot?.status).toBe("rejected");
90+
});
91+
4392
it("derives Codex primary and secondary rate-limit windows", () => {
4493
const snapshot = deriveLatestProviderUsageSnapshot([
4594
makeActivity("activity-1", {

apps/web/src/lib/providerUsage.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ const CLAUDE_WINDOW_LABELS: Record<string, string> = {
6060
overage: "Overage",
6161
};
6262

63+
function statusFromWindows(
64+
windows: ReadonlyArray<RateLimitWindow>,
65+
): ProviderUsageSnapshot["status"] {
66+
const maxPercent = Math.max(...windows.map((window) => window.usedPercent), 0);
67+
if (maxPercent >= 90) {
68+
return "rejected";
69+
}
70+
if (maxPercent >= 70) {
71+
return "warning";
72+
}
73+
return "ok";
74+
}
75+
6376
function normalizeClaudeRateLimitEvent(
6477
payload: Record<string, unknown>,
6578
): Omit<ProviderUsageSnapshot, "updatedAt"> | null {
@@ -103,13 +116,10 @@ function normalizeClaudeRateLimitEvent(
103116
return null;
104117
}
105118

106-
const status: ProviderUsageSnapshot["status"] =
107-
statusRaw === "rejected" ? "rejected" : statusRaw === "allowed_warning" ? "warning" : "ok";
108-
109119
return {
110120
providerLabel: "Claude",
111121
windows,
112-
status,
122+
status: statusFromWindows(windows),
113123
};
114124
}
115125

@@ -189,14 +199,10 @@ function normalizeCodexRateLimits(
189199
return null;
190200
}
191201

192-
const maxPercent = Math.max(...windows.map((w) => w.usedPercent));
193-
const status: ProviderUsageSnapshot["status"] =
194-
maxPercent >= 100 ? "rejected" : maxPercent >= 80 ? "warning" : "ok";
195-
196202
return {
197203
providerLabel: "Codex",
198204
windows,
199-
status,
205+
status: statusFromWindows(windows),
200206
};
201207
}
202208

@@ -257,7 +263,6 @@ export function deriveLatestProviderUsageSnapshot(
257263
): ProviderUsageSnapshot | null {
258264
const windowsByLabel = new Map<string, RateLimitWindow>();
259265
let providerLabel: string | null = null;
260-
let latestStatus: ProviderUsageSnapshot["status"] = "ok";
261266
let latestUpdatedAt: string | null = null;
262267

263268
// Walk backwards so the first match for each label wins (most recent).
@@ -274,7 +279,6 @@ export function deriveLatestProviderUsageSnapshot(
274279

275280
if (providerLabel === null) {
276281
providerLabel = result.providerLabel;
277-
latestStatus = result.status;
278282
latestUpdatedAt = activity.createdAt;
279283
}
280284

@@ -288,23 +292,18 @@ export function deriveLatestProviderUsageSnapshot(
288292
windowsByLabel.set(window.label, window);
289293
}
290294
}
291-
292-
// Escalate status if a worse status was seen in an older event.
293-
if (result.status === "rejected") {
294-
latestStatus = "rejected";
295-
} else if (result.status === "warning" && latestStatus === "ok") {
296-
latestStatus = "warning";
297-
}
298295
}
299296

300297
if (providerLabel === null || windowsByLabel.size === 0 || latestUpdatedAt === null) {
301298
return null;
302299
}
303300

301+
const windows = Array.from(windowsByLabel.values());
302+
304303
return {
305304
providerLabel,
306-
windows: Array.from(windowsByLabel.values()),
307-
status: latestStatus,
305+
windows,
306+
status: statusFromWindows(windows),
308307
updatedAt: latestUpdatedAt,
309308
};
310309
}

0 commit comments

Comments
 (0)