authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-10 12:25:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-10 12:25:03-07:00
log343061a0538bde5aa79e6d2c4aca0d42aa7992cb
tree13b303e193457b27bed0c05fe1e978997ffa0349
parent0b40ea62e12ea061bbc8e954cf8dee288e9faf56
signature Signed by PGP key B5690EEEBB952194

Misc chrome polish: header badges, plan sidebar, diff panel, empty state (#3027)

Co-authored-by: codex <codex@users.noreply.github.com>

13 files changed, 746 insertions(+), 232 deletions(-)

apps/server/package.json+1-1
......@@ -22,7 +22,7 @@
2222 "test": "vp test run"
2323 },
2424 "dependencies": {
25 "@anthropic-ai/claude-agent-sdk": "^0.3.154",
25 "@anthropic-ai/claude-agent-sdk": "^0.3.170",
2626 "@effect/platform-bun": "catalog:",
2727 "@effect/platform-node": "catalog:",
2828 "@effect/platform-node-shared": "catalog:",
apps/server/src/provider/Layers/ClaudeAdapter.ts+579-135
......@@ -14,6 +14,7 @@ import {
1414 type PermissionResult,
1515 type PermissionUpdate,
1616 type SDKMessage,
17 type SDKControlGetContextUsageResponse,
1718 type SDKResultMessage,
1819 type SettingSource,
1920 type SDKUserMessage,
......@@ -29,6 +30,7 @@ import {
2930 type ProviderApprovalDecision,
3031 ProviderDriverKind,
3132 ProviderInstanceId,
33 type ModelSelection,
3234 ProviderItemId,
3335 type ProviderRuntimeEvent,
3436 type ProviderRuntimeTurnStatus,
......@@ -159,6 +161,13 @@ interface ToolInFlight {
159161 readonly lastEmittedInputFingerprint?: string;
160162}
161163
164interface ClaudeTaskState {
165 readonly id: string;
166 subject: string;
167 status: PlanStep["status"];
168 readonly blockedBy: Set<string>;
169}
170
162171interface ClaudeSessionContext {
163172 session: ProviderSession;
164173 readonly promptQueue: Queue.Queue<PromptQueueItem>;
......@@ -175,9 +184,11 @@ interface ClaudeSessionContext {
175184 items: Array<unknown>;
176185 }>;
177186 readonly inFlightTools: Map<number, ToolInFlight>;
187 readonly claudeTasks: Map<string, ClaudeTaskState>;
178188 turnState: ClaudeTurnState | undefined;
179189 lastKnownContextWindow: number | undefined;
180190 lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined;
191 lastKnownTotalProcessedTokens: number | undefined;
181192 lastAssistantUuid: string | undefined;
182193 lastThreadStartedId: string | undefined;
183194 stopped: boolean;
......@@ -188,6 +199,7 @@ interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
188199 readonly setModel: (model?: string) => Promise<void>;
189200 readonly setPermissionMode: (mode: PermissionMode) => Promise<void>;
190201 readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>;
202 readonly getContextUsage?: () => Promise<SDKControlGetContextUsageResponse>;
191203 readonly close: () => void;
192204}
193205
......@@ -337,60 +349,220 @@ function maxClaudeContextWindowFromModelUsage(
337349 return maxContextWindow;
338350}
339351
340function normalizeClaudeTokenUsage(
341 value: unknown,
342 contextWindow?: number,
343): ThreadTokenUsageSnapshot | undefined {
344 if (!value || typeof value !== "object") {
352function selectedClaudeContextWindow(
353 modelSelection: ModelSelection | undefined,
354): number | undefined {
355 switch (modelSelection?.model) {
356 case "claude-opus-4-8":
357 case "claude-opus-4-7":
358 return 1_000_000;
359 }
360
361 const optionValue = getModelSelectionStringOptionValue(modelSelection, "contextWindow");
362 if (optionValue === "1m") {
363 return 1_000_000;
364 }
365 if (optionValue === "200k") {
366 return 200_000;
367 }
368 const caps = getClaudeModelCapabilities(modelSelection?.model);
369 const hasContextWindowOption = getProviderOptionDescriptors({ caps }).some(
370 (descriptor) => descriptor.type === "select" && descriptor.id === "contextWindow",
371 );
372 if (hasContextWindowOption) {
373 return 200_000;
374 }
375 return undefined;
376}
377
378function finiteNonNegativeInteger(value: unknown): number | undefined {
379 return typeof value === "number" && Number.isFinite(value) && value >= 0
380 ? Math.round(value)
381 : undefined;
382}
383
384function finitePositiveInteger(value: unknown): number | undefined {
385 return typeof value === "number" && Number.isFinite(value) && value > 0
386 ? Math.round(value)
387 : undefined;
388}
389
390function claudeUsageInputTokens(usage: Record<string, unknown>): number {
391 return (
392 (finiteNonNegativeInteger(usage.input_tokens) ?? 0) +
393 (finiteNonNegativeInteger(usage.cache_creation_input_tokens) ?? 0) +
394 (finiteNonNegativeInteger(usage.cache_read_input_tokens) ?? 0)
395 );
396}
397
398function claudeUsageOutputTokens(usage: Record<string, unknown>): number {
399 return finiteNonNegativeInteger(usage.output_tokens) ?? 0;
400}
401
402function lastClaudeUsageIteration(
403 value: Record<string, unknown>,
404): Record<string, unknown> | undefined {
405 const iterations = Array.isArray(value.iterations) ? value.iterations : [];
406 return iterations.findLast(
407 (iteration): iteration is Record<string, unknown> =>
408 iteration !== null && typeof iteration === "object" && !Array.isArray(iteration),
409 );
410}
411
412function claudeTotalProcessedTokens(value: unknown): number | undefined {
413 if (!value || typeof value !== "object" || Array.isArray(value)) {
345414 return undefined;
346415 }
347416
348417 const usage = value as Record<string, unknown>;
349 const inputTokens =
350 (typeof usage.input_tokens === "number" && Number.isFinite(usage.input_tokens)
351 ? usage.input_tokens
352 : 0) +
353 (typeof usage.cache_creation_input_tokens === "number" &&
354 Number.isFinite(usage.cache_creation_input_tokens)
355 ? usage.cache_creation_input_tokens
356 : 0) +
357 (typeof usage.cache_read_input_tokens === "number" &&
358 Number.isFinite(usage.cache_read_input_tokens)
359 ? usage.cache_read_input_tokens
360 : 0);
361 const outputTokens =
362 typeof usage.output_tokens === "number" && Number.isFinite(usage.output_tokens)
363 ? usage.output_tokens
364 : 0;
365 const derivedTotalProcessedTokens = inputTokens + outputTokens;
366 const totalProcessedTokens =
367 (typeof usage.total_tokens === "number" && Number.isFinite(usage.total_tokens)
368 ? usage.total_tokens
369 : undefined) ?? (derivedTotalProcessedTokens > 0 ? derivedTotalProcessedTokens : undefined);
370 if (totalProcessedTokens === undefined || totalProcessedTokens <= 0) {
418 const explicitTotal = finiteNonNegativeInteger(usage.total_tokens);
419 if (explicitTotal !== undefined && explicitTotal > 0) {
420 return explicitTotal;
421 }
422
423 const total = claudeUsageInputTokens(usage) + claudeUsageOutputTokens(usage);
424 return total > 0 ? total : undefined;
425}
426
427function makeClaudeTokenUsageSnapshot(input: {
428 readonly activeTokens: number;
429 readonly inputTokens?: number;
430 readonly outputTokens?: number;
431 readonly contextWindow?: number;
432 readonly totalProcessedTokens?: number;
433 readonly lastUsedTokens?: number;
434 readonly compactsAutomatically?: boolean;
435}): ThreadTokenUsageSnapshot | undefined {
436 const activeTokens = finiteNonNegativeInteger(input.activeTokens);
437 if (activeTokens === undefined || activeTokens <= 0) {
371438 return undefined;
372439 }
373440
374 const maxTokens =
375 typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0
376 ? contextWindow
377 : undefined;
378 const usedTokens =
379 maxTokens !== undefined ? Math.min(totalProcessedTokens, maxTokens) : totalProcessedTokens;
441 const maxTokens = finitePositiveInteger(input.contextWindow);
442 const usedTokens = maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens;
443 const lastUsedTokens =
444 finiteNonNegativeInteger(input.lastUsedTokens) ??
445 (maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens);
446 const totalProcessedTokens = finiteNonNegativeInteger(input.totalProcessedTokens);
447 const inputTokens = finiteNonNegativeInteger(input.inputTokens);
448 const outputTokens = finiteNonNegativeInteger(input.outputTokens);
380449
381450 return {
382451 usedTokens,
383 lastUsedTokens: usedTokens,
384 ...(totalProcessedTokens > usedTokens ? { totalProcessedTokens } : {}),
385 ...(inputTokens > 0 ? { inputTokens } : {}),
386 ...(outputTokens > 0 ? { outputTokens } : {}),
452 lastUsedTokens,
453 ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens
454 ? { totalProcessedTokens }
455 : {}),
456 ...(inputTokens !== undefined && inputTokens > 0 ? { inputTokens } : {}),
457 ...(outputTokens !== undefined && outputTokens > 0 ? { outputTokens } : {}),
387458 ...(maxTokens !== undefined ? { maxTokens } : {}),
388 ...(typeof usage.tool_uses === "number" && Number.isFinite(usage.tool_uses)
389 ? { toolUses: usage.tool_uses }
459 ...(input.compactsAutomatically !== undefined
460 ? { compactsAutomatically: input.compactsAutomatically }
390461 : {}),
391 ...(typeof usage.duration_ms === "number" && Number.isFinite(usage.duration_ms)
392 ? { durationMs: usage.duration_ms }
462 };
463}
464
465function normalizeClaudeActiveTokenUsage(
466 value: unknown,
467 contextWindow?: number,
468 totalProcessedTokens?: number,
469): ThreadTokenUsageSnapshot | undefined {
470 if (!value || typeof value !== "object") {
471 return undefined;
472 }
473
474 const usage = value as Record<string, unknown>;
475 const activeUsage = lastClaudeUsageIteration(usage) ?? usage;
476 const inputTokens = claudeUsageInputTokens(activeUsage);
477 const outputTokens = claudeUsageOutputTokens(activeUsage);
478 const activeTokens = claudeTotalProcessedTokens(activeUsage) ?? inputTokens + outputTokens;
479 if (activeTokens <= 0) {
480 return undefined;
481 }
482
483 return makeClaudeTokenUsageSnapshot({
484 activeTokens,
485 inputTokens,
486 outputTokens,
487 ...(contextWindow !== undefined ? { contextWindow } : {}),
488 ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}),
489 });
490}
491
492function normalizeClaudeContextUsageApiSnapshot(
493 value: SDKControlGetContextUsageResponse,
494 totalProcessedTokens?: number,
495): ThreadTokenUsageSnapshot | undefined {
496 return makeClaudeTokenUsageSnapshot({
497 activeTokens: value.totalTokens,
498 contextWindow: value.maxTokens,
499 ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}),
500 compactsAutomatically: value.isAutoCompactEnabled,
501 });
502}
503
504function compactBoundaryTokenUsageSnapshot(
505 message: Record<string, unknown>,
506 contextWindow?: number,
507 totalProcessedTokens?: number,
508): ThreadTokenUsageSnapshot | undefined {
509 const metadata = message.compact_metadata;
510 if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
511 return undefined;
512 }
513
514 const compactMetadata = metadata as Record<string, unknown>;
515 const postTokens = finiteNonNegativeInteger(compactMetadata.post_tokens);
516 if (postTokens === undefined || postTokens <= 0) {
517 return undefined;
518 }
519
520 const preTokens = finiteNonNegativeInteger(compactMetadata.pre_tokens);
521 return makeClaudeTokenUsageSnapshot({
522 activeTokens: postTokens,
523 ...(preTokens !== undefined ? { lastUsedTokens: preTokens } : {}),
524 ...(contextWindow !== undefined ? { contextWindow } : {}),
525 ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}),
526 });
527}
528
529function normalizeClaudeTaskProgressTokenUsage(
530 value: unknown,
531 context: ClaudeSessionContext,
532): ThreadTokenUsageSnapshot | undefined {
533 const totalTokens = claudeTotalProcessedTokens(value);
534 if (totalTokens === undefined || totalTokens <= 0) {
535 return undefined;
536 }
537
538 const lastUsedTokens = context.lastKnownTokenUsage?.usedTokens;
539 const activeTokens =
540 lastUsedTokens !== undefined ? Math.max(totalTokens, lastUsedTokens) : totalTokens;
541 if (lastUsedTokens !== undefined && activeTokens === lastUsedTokens) {
542 return undefined;
543 }
544
545 const usage = value as Record<string, unknown>;
546 const snapshot = makeClaudeTokenUsageSnapshot({
547 activeTokens,
548 ...(context.lastKnownContextWindow !== undefined
549 ? { contextWindow: context.lastKnownContextWindow }
393550 : {}),
551 totalProcessedTokens: Math.max(
552 totalTokens,
553 context.lastKnownTotalProcessedTokens ?? totalTokens,
554 ),
555 });
556 if (!snapshot) {
557 return undefined;
558 }
559
560 const toolUses = finiteNonNegativeInteger(usage.tool_uses);
561 const durationMs = finiteNonNegativeInteger(usage.duration_ms);
562 return {
563 ...snapshot,
564 ...(toolUses !== undefined ? { toolUses } : {}),
565 ...(durationMs !== undefined ? { durationMs } : {}),
394566 };
395567}
396568
......@@ -539,6 +711,140 @@ function extractPlanStepsFromTodoInput(input: Record<string, unknown>): PlanStep
539711 }));
540712}
541713
714function isClaudeTaskTool(toolName: string): boolean {
715 return toolName === "TaskCreate" || toolName === "TaskUpdate" || toolName === "TaskList";
716}
717
718function normalizeClaudeTaskStatus(value: unknown): PlanStep["status"] {
719 return value === "completed" ? "completed" : value === "in_progress" ? "inProgress" : "pending";
720}
721
722function readString(value: unknown): string | undefined {
723 return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
724}
725
726function readStringArray(value: unknown): Array<string> {
727 return Array.isArray(value)
728 ? value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
729 : [];
730}
731
732function readClaudeToolUseResult(message: SDKMessage): Record<string, unknown> | undefined {
733 if (message.type !== "user") {
734 return undefined;
735 }
736 const result = (message as { readonly tool_use_result?: unknown }).tool_use_result;
737 return result !== null && typeof result === "object" && !Array.isArray(result)
738 ? (result as Record<string, unknown>)
739 : undefined;
740}
741
742function readClaudeTaskFromResult(
743 result: Record<string, unknown> | undefined,
744): Record<string, unknown> | undefined {
745 const task = result?.task;
746 return task !== null && typeof task === "object" && !Array.isArray(task)
747 ? (task as Record<string, unknown>)
748 : undefined;
749}
750
751function applyClaudeTaskToolResult(
752 tasks: Map<string, ClaudeTaskState>,
753 tool: ToolInFlight,
754 result: Record<string, unknown> | undefined,
755): boolean {
756 if (!isClaudeTaskTool(tool.toolName)) {
757 return false;
758 }
759
760 let changed = false;
761 if (tool.toolName === "TaskList") {
762 const resultTasks = result?.tasks;
763 if (!Array.isArray(resultTasks)) {
764 return false;
765 }
766 tasks.clear();
767 for (const entry of resultTasks) {
768 if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
769 continue;
770 }
771 const task = entry as Record<string, unknown>;
772 const id = readString(task.id);
773 const subject = readString(task.subject);
774 if (!id || !subject) {
775 continue;
776 }
777 tasks.set(id, {
778 id,
779 subject,
780 status: normalizeClaudeTaskStatus(task.status),
781 blockedBy: new Set(readStringArray(task.blockedBy)),
782 });
783 }
784 return tasks.size > 0;
785 }
786
787 if (tool.toolName === "TaskCreate") {
788 const resultTask = readClaudeTaskFromResult(result);
789 const id = readString(resultTask?.id);
790 const subject = readString(resultTask?.subject) ?? readString(tool.input.subject);
791 if (!id || !subject) {
792 return false;
793 }
794 tasks.set(id, {
795 id,
796 subject,
797 status: normalizeClaudeTaskStatus(tool.input.status),
798 blockedBy: new Set(readStringArray(tool.input.blockedBy)),
799 });
800 return true;
801 }
802
803 const taskId = readString(tool.input.taskId) ?? readString(result?.taskId);
804 if (!taskId) {
805 return false;
806 }
807 const task = tasks.get(taskId);
808 if (!task) {
809 return false;
810 }
811 const subject = readString(tool.input.subject);
812 if (subject && task.subject !== subject) {
813 task.subject = subject;
814 changed = true;
815 }
816 if (typeof tool.input.status === "string") {
817 const status = normalizeClaudeTaskStatus(tool.input.status);
818 if (task.status !== status) {
819 task.status = status;
820 changed = true;
821 }
822 }
823 for (const dependency of readStringArray(tool.input.addBlockedBy)) {
824 if (!task.blockedBy.has(dependency)) {
825 task.blockedBy.add(dependency);
826 changed = true;
827 }
828 }
829 for (const dependency of readStringArray(tool.input.removeBlockedBy)) {
830 if (task.blockedBy.delete(dependency)) {
831 changed = true;
832 }
833 }
834 return changed;
835}
836
837function planStepsFromClaudeTasks(tasks: Map<string, ClaudeTaskState>): PlanStep[] {
838 return Array.from(tasks.values()).map((task) => {
839 const blockedBy = Array.from(task.blockedBy);
840 const blockedSuffix = blockedBy.length > 0 ? ` (blocked by #${blockedBy.join(", #")})` : "";
841 return {
842 step: `${task.subject}${blockedSuffix}`,
843 status: task.status,
844 };
845 });
846}
847
542848function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {
543849 const commandValue = input.command ?? input.cmd;
544850 const command = typeof commandValue === "string" ? commandValue : undefined;
......@@ -1438,6 +1744,70 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
14381744 });
14391745 });
14401746
1747 const emitThreadTokenUsage = Effect.fn("emitThreadTokenUsage")(function* (
1748 context: ClaudeSessionContext,
1749 usage: ThreadTokenUsageSnapshot | undefined,
1750 options?: {
1751 readonly rawMethod?: string;
1752 readonly rawPayload?: unknown;
1753 },
1754 ) {
1755 if (!usage) {
1756 return;
1757 }
1758
1759 context.lastKnownTokenUsage = usage;
1760 context.lastKnownTotalProcessedTokens =
1761 usage.totalProcessedTokens ?? context.lastKnownTotalProcessedTokens;
1762
1763 const turnState = context.turnState;
1764 const stamp = yield* makeEventStamp();
1765 yield* offerRuntimeEvent({
1766 type: "thread.token-usage.updated",
1767 eventId: stamp.eventId,
1768 provider: PROVIDER,
1769 createdAt: stamp.createdAt,
1770 threadId: context.session.threadId,
1771 ...(turnState ? { turnId: turnState.turnId } : {}),
1772 payload: {
1773 usage,
1774 },
1775 providerRefs: nativeProviderRefs(context),
1776 ...(options?.rawMethod || options?.rawPayload
1777 ? {
1778 raw: {
1779 source: "claude.sdk.message" as const,
1780 ...(options.rawMethod ? { method: options.rawMethod } : {}),
1781 payload: options.rawPayload,
1782 },
1783 }
1784 : {}),
1785 });
1786 });
1787
1788 const queryCurrentContextUsage = Effect.fn("queryCurrentContextUsage")(function* (
1789 context: ClaudeSessionContext,
1790 totalProcessedTokens?: number,
1791 ) {
1792 if (!context.query.getContextUsage) {
1793 return undefined;
1794 }
1795
1796 const usage = yield* Effect.promise(async () => {
1797 try {
1798 return await context.query.getContextUsage?.();
1799 } catch {
1800 return undefined;
1801 }
1802 });
1803 if (!usage) {
1804 return undefined;
1805 }
1806
1807 context.lastKnownContextWindow = usage.maxTokens;
1808 return normalizeClaudeContextUsageApiSnapshot(usage, totalProcessedTokens);
1809 });
1810
14411811 const emitProposedPlanCompleted = Effect.fn("emitProposedPlanCompleted")(function* (
14421812 context: ClaudeSessionContext,
14431813 input: {
......@@ -1485,6 +1855,42 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
14851855 });
14861856 });
14871857
1858 const emitClaudeTaskPlanUpdated = Effect.fn("emitClaudeTaskPlanUpdated")(function* (
1859 context: ClaudeSessionContext,
1860 input: {
1861 readonly toolUseId: string;
1862 readonly rawMethod: string;
1863 readonly rawPayload: unknown;
1864 },
1865 ) {
1866 const plan = planStepsFromClaudeTasks(context.claudeTasks);
1867 if (plan.length === 0) {
1868 return;
1869 }
1870
1871 const stamp = yield* makeEventStamp();
1872 yield* offerRuntimeEvent({
1873 type: "turn.plan.updated",
1874 eventId: stamp.eventId,
1875 provider: PROVIDER,
1876 createdAt: stamp.createdAt,
1877 threadId: context.session.threadId,
1878 ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
1879 payload: {
1880 explanation: "Claude Tasks",
1881 plan,
1882 },
1883 providerRefs: nativeProviderRefs(context, {
1884 providerItemId: input.toolUseId,
1885 }),
1886 raw: {
1887 source: "claude.sdk.message",
1888 method: input.rawMethod,
1889 payload: input.rawPayload,
1890 },
1891 });
1892 });
1893
14881894 const completeTurn = Effect.fn("completeTurn")(function* (
14891895 context: ClaudeSessionContext,
14901896 status: ProviderRuntimeTurnStatus,
......@@ -1496,51 +1902,77 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
14961902 context.lastKnownContextWindow = resultContextWindow;
14971903 }
14981904
1499 // The SDK result.usage contains *accumulated* totals across all API calls
1500 // (input_tokens, cache_read_input_tokens, etc. summed over every request).
1501 // This does NOT represent the current context window size.
1502 // Instead, use the last known context-window-accurate usage from task_progress
1503 // events and treat the accumulated total as totalProcessedTokens.
1504 const accumulatedSnapshot = normalizeClaudeTokenUsage(
1505 result?.usage,
1506 resultContextWindow ?? context.lastKnownContextWindow,
1905 const maxTokens = resultContextWindow ?? context.lastKnownContextWindow;
1906 const accumulatedTotalProcessedTokens = claudeTotalProcessedTokens(result?.usage);
1907 if (accumulatedTotalProcessedTokens !== undefined) {
1908 context.lastKnownTotalProcessedTokens = accumulatedTotalProcessedTokens;
1909 }
1910
1911 const contextUsageSnapshot = yield* queryCurrentContextUsage(
1912 context,
1913 accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens,
15071914 );
1508 const accumulatedTotalProcessedTokens =
1509 accumulatedSnapshot?.totalProcessedTokens ?? accumulatedSnapshot?.usedTokens;
1915 const resultUsageRecord =
1916 result?.usage && typeof result.usage === "object" && !Array.isArray(result.usage)
1917 ? (result.usage as Record<string, unknown>)
1918 : undefined;
1919 const hasResultUsageIteration =
1920 resultUsageRecord !== undefined && lastClaudeUsageIteration(resultUsageRecord) !== undefined;
1921 const resultHasActiveUsage =
1922 resultUsageRecord !== undefined &&
1923 (hasResultUsageIteration ||
1924 claudeUsageInputTokens(resultUsageRecord) + claudeUsageOutputTokens(resultUsageRecord) > 0);
1925 const resultTotalOnly =
1926 resultUsageRecord !== undefined &&
1927 !resultHasActiveUsage &&
1928 claudeTotalProcessedTokens(resultUsageRecord) !== undefined;
1929 const resultIterationSnapshot = resultUsageRecord
1930 ? normalizeClaudeActiveTokenUsage(
1931 resultUsageRecord,
1932 maxTokens,
1933 accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens,
1934 )
1935 : undefined;
15101936 const lastGoodUsage = context.lastKnownTokenUsage;
1511 const maxTokens = resultContextWindow ?? context.lastKnownContextWindow;
1512 const usageSnapshot: ThreadTokenUsageSnapshot | undefined = lastGoodUsage
1513 ? {
1514 ...lastGoodUsage,
1515 ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0
1516 ? { maxTokens }
1517 : {}),
1518 ...(typeof accumulatedTotalProcessedTokens === "number" &&
1519 Number.isFinite(accumulatedTotalProcessedTokens) &&
1520 accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens
1521 ? {
1522 totalProcessedTokens: accumulatedTotalProcessedTokens,
1523 }
1524 : {}),
1525 }
1526 : accumulatedSnapshot;
1937 const usageSnapshot: ThreadTokenUsageSnapshot | undefined =
1938 contextUsageSnapshot ??
1939 (resultTotalOnly && lastGoodUsage
1940 ? {
1941 ...lastGoodUsage,
1942 ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0
1943 ? { maxTokens }
1944 : {}),
1945 ...(typeof accumulatedTotalProcessedTokens === "number" &&
1946 Number.isFinite(accumulatedTotalProcessedTokens) &&
1947 accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens
1948 ? {
1949 totalProcessedTokens: accumulatedTotalProcessedTokens,
1950 }
1951 : {}),
1952 }
1953 : resultIterationSnapshot) ??
1954 (lastGoodUsage
1955 ? {
1956 ...lastGoodUsage,
1957 ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0
1958 ? { maxTokens }
1959 : {}),
1960 ...(typeof accumulatedTotalProcessedTokens === "number" &&
1961 Number.isFinite(accumulatedTotalProcessedTokens) &&
1962 accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens
1963 ? {
1964 totalProcessedTokens: accumulatedTotalProcessedTokens,
1965 }
1966 : {}),
1967 }
1968 : undefined);
15271969
15281970 const turnState = context.turnState;
15291971 if (!turnState) {
1530 if (usageSnapshot) {
1531 const usageStamp = yield* makeEventStamp();
1532 yield* offerRuntimeEvent({
1533 type: "thread.token-usage.updated",
1534 eventId: usageStamp.eventId,
1535 provider: PROVIDER,
1536 createdAt: usageStamp.createdAt,
1537 threadId: context.session.threadId,
1538 payload: {
1539 usage: usageSnapshot,
1540 },
1541 providerRefs: {},
1542 });
1543 }
1972 yield* emitThreadTokenUsage(context, usageSnapshot, {
1973 rawMethod: "claude/result",
1974 rawPayload: result ?? { status },
1975 });
15441976
15451977 const stamp = yield* makeEventStamp();
15461978 yield* offerRuntimeEvent({
......@@ -1611,21 +2043,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
16112043 items: [...turnState.items],
16122044 });
16132045
1614 if (usageSnapshot) {
1615 const usageStamp = yield* makeEventStamp();
1616 yield* offerRuntimeEvent({
1617 type: "thread.token-usage.updated",
1618 eventId: usageStamp.eventId,
1619 provider: PROVIDER,
1620 createdAt: usageStamp.createdAt,
1621 threadId: context.session.threadId,
1622 turnId: turnState.turnId,
1623 payload: {
1624 usage: usageSnapshot,
1625 },
1626 providerRefs: nativeProviderRefs(context),
1627 });
1628 }
2046 yield* emitThreadTokenUsage(context, usageSnapshot, {
2047 rawMethod: "claude/result",
2048 rawPayload: result ?? { status },
2049 });
16292050
16302051 const stamp = yield* makeEventStamp();
16312052 yield* offerRuntimeEvent({
......@@ -1670,6 +2091,23 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
16702091
16712092 const { event } = message;
16722093
2094 if (event.type === "message_delta") {
2095 if (message.parent_tool_use_id !== null && message.parent_tool_use_id !== undefined) {
2096 return;
2097 }
2098
2099 const snapshot = normalizeClaudeActiveTokenUsage(
2100 event.usage,
2101 context.lastKnownContextWindow,
2102 context.lastKnownTotalProcessedTokens,
2103 );
2104 yield* emitThreadTokenUsage(context, snapshot, {
2105 rawMethod: "claude/stream_event/message_delta",
2106 rawPayload: message,
2107 });
2108 return;
2109 }
2110
16732111 if (event.type === "content_block_delta") {
16742112 if (
16752113 (event.delta.type === "text_delta" || event.delta.type === "thinking_delta") &&
......@@ -1932,6 +2370,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
19322370
19332371 const [index, tool] = toolEntry;
19342372 const itemStatus = toolResult.isError ? "failed" : "completed";
2373 const toolUseResult = readClaudeToolUseResult(message);
19352374 const toolData = {
19362375 toolName: tool.toolName,
19372376 input: tool.input,
......@@ -2016,6 +2455,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
20162455 },
20172456 });
20182457
2458 if (
2459 !toolResult.isError &&
2460 applyClaudeTaskToolResult(context.claudeTasks, tool, toolUseResult)
2461 ) {
2462 yield* emitClaudeTaskPlanUpdated(context, {
2463 toolUseId: tool.itemId,
2464 rawMethod: "claude/user",
2465 rawPayload: message,
2466 });
2467 }
2468
20192469 context.inFlightTools.delete(index);
20202470 }
20212471 });
......@@ -2171,6 +2621,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
21712621 });
21722622 return;
21732623 case "compact_boundary":
2624 yield* emitThreadTokenUsage(
2625 context,
2626 compactBoundaryTokenUsageSnapshot(
2627 message as unknown as Record<string, unknown>,
2628 context.lastKnownContextWindow,
2629 context.lastKnownTotalProcessedTokens,
2630 ),
2631 {
2632 rawMethod: "claude/system/compact_boundary",
2633 rawPayload: message,
2634 },
2635 );
21742636 yield* offerRuntimeEvent({
21752637 ...base,
21762638 type: "thread.state.changed",
......@@ -2229,25 +2691,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
22292691 });
22302692 return;
22312693 case "task_progress":
2232 if (message.usage) {
2233 const normalizedUsage = normalizeClaudeTokenUsage(
2234 message.usage,
2235 context.lastKnownContextWindow,
2236 );
2237 if (normalizedUsage) {
2238 context.lastKnownTokenUsage = normalizedUsage;
2239 const usageStamp = yield* makeEventStamp();
2240 yield* offerRuntimeEvent({
2241 ...base,
2242 eventId: usageStamp.eventId,
2243 createdAt: usageStamp.createdAt,
2244 type: "thread.token-usage.updated",
2245 payload: {
2246 usage: normalizedUsage,
2247 },
2248 });
2249 }
2250 }
2694 yield* emitThreadTokenUsage(
2695 context,
2696 normalizeClaudeTaskProgressTokenUsage(message.usage, context),
2697 {
2698 rawMethod: "claude/system/task_progress",
2699 rawPayload: message,
2700 },
2701 );
22512702 yield* offerRuntimeEvent({
22522703 ...base,
22532704 type: "task.progress",
......@@ -2261,25 +2712,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
22612712 });
22622713 return;
22632714 case "task_notification":
2264 if (message.usage) {
2265 const normalizedUsage = normalizeClaudeTokenUsage(
2266 message.usage,
2267 context.lastKnownContextWindow,
2268 );
2269 if (normalizedUsage) {
2270 context.lastKnownTokenUsage = normalizedUsage;
2271 const usageStamp = yield* makeEventStamp();
2272 yield* offerRuntimeEvent({
2273 ...base,
2274 eventId: usageStamp.eventId,
2275 createdAt: usageStamp.createdAt,
2276 type: "thread.token-usage.updated",
2277 payload: {
2278 usage: normalizedUsage,
2279 },
2280 });
2281 }
2282 }
2715 yield* emitThreadTokenUsage(
2716 context,
2717 normalizeClaudeTaskProgressTokenUsage(message.usage, context),
2718 {
2719 rawMethod: "claude/system/task_notification",
2720 rawPayload: message,
2721 },
2722 );
22832723 yield* offerRuntimeEvent({
22842724 ...base,
22852725 type: "task.completed",
......@@ -2669,6 +3109,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
26693109 const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();
26703110 const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>();
26713111 const inFlightTools = new Map<number, ToolInFlight>();
3112 const claudeTasks = new Map<string, ClaudeTaskState>();
26723113
26733114 const contextRef = yield* Ref.make<ClaudeSessionContext | undefined>(undefined);
26743115
......@@ -2969,6 +3410,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
29693410 const caps = getClaudeModelCapabilities(modelSelection?.model);
29703411 const descriptors = getProviderOptionDescriptors({ caps });
29713412 const apiModelId = modelSelection ? resolveClaudeApiModelId(modelSelection) : undefined;
3413 const initialContextWindow = selectedClaudeContextWindow(modelSelection);
29723414 const rawEffort = getModelSelectionStringOptionValue(modelSelection, "effort");
29733415 const effort = resolveClaudeEffort(caps, rawEffort) ?? null;
29743416 const fastModeSupported = descriptors.some(
......@@ -3094,9 +3536,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
30943536 pendingUserInputs,
30953537 turns: [],
30963538 inFlightTools,
3539 claudeTasks,
30973540 turnState: undefined,
3098 lastKnownContextWindow: undefined,
3541 lastKnownContextWindow: initialContextWindow,
30993542 lastKnownTokenUsage: undefined,
3543 lastKnownTotalProcessedTokens: undefined,
31003544 lastAssistantUuid: resumeState?.resumeSessionAt,
31013545 lastThreadStartedId: undefined,
31023546 stopped: false,
apps/server/src/provider/Layers/ClaudeProvider.ts-16
......@@ -108,14 +108,6 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
108108 id: "fastMode",
109109 label: "Fast Mode",
110110 }),
111 buildSelectOptionDescriptor({
112 id: "contextWindow",
113 label: "Context Window",
114 options: [
115 { value: "200k", label: "200k", isDefault: true },
116 { value: "1m", label: "1M" },
117 ],
118 }),
119111 ],
120112 }),
121113 },
......@@ -142,14 +134,6 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
142134 id: "fastMode",
143135 label: "Fast Mode",
144136 }),
145 buildSelectOptionDescriptor({
146 id: "contextWindow",
147 label: "Context Window",
148 options: [
149 { value: "200k", label: "200k", isDefault: true },
150 { value: "1m", label: "1M" },
151 ],
152 }),
153137 ],
154138 }),
155139 },
apps/web/src/components/ChatMarkdown.tsx+5-1
......@@ -665,6 +665,7 @@ function UncachedShikiCodeBlock({
665665interface MarkdownFileLinkProps {
666666 href: string;
667667 targetPath: string;
668 iconPath: string;
668669 displayPath: string;
669670 label: string;
670671 copyMarkdown: string;
......@@ -938,6 +939,7 @@ function MarkdownExternalLinkContent({
938939const MarkdownFileLink = memo(function MarkdownFileLink({
939940 href,
940941 targetPath,
942 iconPath,
941943 displayPath,
942944 label,
943945 copyMarkdown,
......@@ -1044,7 +1046,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
10441046 }}
10451047 onContextMenu={handleContextMenu}
10461048 >
1047 <FileTagChipContent path={targetPath} label={label} theme={theme} selectable />
1049 <FileTagChipContent path={iconPath} label={label} theme={theme} selectable />
10481050 </a>
10491051 }
10501052 />
......@@ -1067,6 +1069,7 @@ function areMarkdownFileLinkPropsEqual(
10671069 return (
10681070 previous.href === next.href &&
10691071 previous.targetPath === next.targetPath &&
1072 previous.iconPath === next.iconPath &&
10701073 previous.displayPath === next.displayPath &&
10711074 previous.label === next.label &&
10721075 previous.copyMarkdown === next.copyMarkdown &&
......@@ -1186,6 +1189,7 @@ function ChatMarkdown({
11861189 <MarkdownFileLink
11871190 href={fileLinkMeta.targetPath}
11881191 targetPath={fileLinkMeta.targetPath}
1192 iconPath={fileLinkMeta.filePath}
11891193 displayPath={fileLinkMeta.displayPath}
11901194 label={labelParts.join(" · ")}
11911195 copyMarkdown={`[${fileLinkMeta.basename}](${normalizedHref})`}
apps/web/src/components/DiffPanel.tsx+35-1
......@@ -53,6 +53,8 @@ const DIFF_PANEL_UNSAFE_CSS = `
5353[data-file],
5454[data-error-wrapper],
5555[data-virtualizer-buffer] {
56 --diffs-header-font-family: var(--font-sans) !important;
57 --diffs-font-family: var(--font-mono) !important;
5658 --diffs-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;
5759 --diffs-light-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;
5860 --diffs-dark-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;
......@@ -93,6 +95,37 @@ const DIFF_PANEL_UNSAFE_CSS = `
9395 z-index: 4;
9496 background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;
9597 border-bottom: 1px solid var(--border) !important;
98 align-items: center !important;
99 font-family: var(--font-sans) !important;
100 font-size: 12px !important;
101 line-height: 1 !important;
102 min-height: 32px !important;
103 padding-block: 6px !important;
104}
105
106[data-diffs-header] [data-header-content] {
107 align-items: center !important;
108 line-height: 1 !important;
109}
110
111[data-diffs-header] [data-metadata] {
112 align-items: center !important;
113 line-height: 1 !important;
114 font-variant-numeric: tabular-nums;
115}
116
117[data-diffs-header] [data-additions-count],
118[data-diffs-header] [data-deletions-count] {
119 font-family: var(--font-mono) !important;
120 font-size: 11px !important;
121 font-variant-numeric: tabular-nums;
122 line-height: 1 !important;
123}
124
125[data-diffs-header] [data-change-icon],
126[data-diffs-header] [data-rename-icon] {
127 display: block;
128 flex-shrink: 0;
96129}
97130
98131[data-title] {
......@@ -103,6 +136,7 @@ const DIFF_PANEL_UNSAFE_CSS = `
103136 text-decoration: underline;
104137 text-decoration-color: transparent;
105138 text-underline-offset: 2px;
139 font-family: var(--font-sans) !important;
106140}
107141
108142[data-title]:hover {
......@@ -488,7 +522,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
488522 inferredCheckpointTurnCountByTurnId[summary.turnId] ??
489523 "?"}
490524 </span>
491 <span className="text-[9px] leading-tight opacity-70">
525 <span className="text-[9px] leading-tight opacity-70 tabular-nums">
492526 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
493527 </span>
494528 </div>
apps/web/src/components/GitActionsControl.tsx+5-1
......@@ -19,6 +19,7 @@ import {
1919 ChevronDownIcon,
2020 CloudUploadIcon,
2121 ExternalLinkIcon,
22 GitBranchPlusIcon,
2223 GitCommitIcon,
2324 InfoIcon,
2425 LockIcon,
......@@ -1611,7 +1612,10 @@ export default function GitActionsControl({
16111612 void initAction.run();
16121613 }}
16131614 >
1614 {initAction.isPending ? "Initializing..." : "Initialize Git"}
1615 <GitBranchPlusIcon className="size-3.5" aria-hidden />
1616 <span className="ml-0.5">
1617 {initAction.isPending ? "Initializing..." : "Initialize Git"}
1618 </span>
16151619 </Button>
16161620 ) : (
16171621 <Group aria-label="Git actions" className="shrink-0">
apps/web/src/components/NoActiveThreadState.tsx+1-1
......@@ -30,7 +30,7 @@ export function NoActiveThreadState() {
3030 </header>
3131
3232 <Empty className="flex-1">
33 <div className="w-full max-w-lg rounded-3xl border border-border/55 bg-card/20 px-8 py-12 shadow-sm/5">
33 <div className="w-full max-w-lg px-8 py-12">
3434 <EmptyHeader className="max-w-none">
3535 <EmptyTitle className="text-foreground text-xl">Pick a thread to continue</EmptyTitle>
3636 <EmptyDescription className="mt-2 text-sm text-muted-foreground/78">
apps/web/src/components/PlanSidebar.tsx+8-7
......@@ -32,14 +32,14 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
3232function stepStatusIcon(status: string): React.ReactNode {
3333 if (status === "completed") {
3434 return (
35 <span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-500">
35 <span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-success/10 text-success-foreground">
3636 <CheckIcon className="size-3" />
3737 </span>
3838 );
3939 }
4040 if (status === "inProgress") {
4141 return (
42 <span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-blue-500/15 text-blue-400">
42 <span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
4343 <LoaderIcon className="size-3 animate-spin" />
4444 </span>
4545 );
......@@ -139,13 +139,14 @@ const PlanSidebar = memo(function PlanSidebar({
139139 <div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3">
140140 <div className="flex items-center gap-2">
141141 <Badge
142 variant="secondary"
143 className="rounded-md bg-blue-500/10 px-1.5 py-0 text-[10px] font-semibold tracking-wide text-blue-400 uppercase"
142 variant="info"
143 size="sm"
144 className="rounded-md px-1.5 py-0 font-semibold tracking-wide uppercase"
144145 >
145146 {label}
146147 </Badge>
147148 {activePlan ? (
148 <span className="text-[11px] text-muted-foreground/60">
149 <span className="text-[11px] text-muted-foreground/60 tabular-nums">
149150 {formatTimestamp(activePlan.createdAt, timestampFormat)}
150151 </span>
151152 ) : null}
......@@ -211,12 +212,12 @@ const PlanSidebar = memo(function PlanSidebar({
211212 <div
212213 key={`${step.status}:${step.step}`}
213214 className={cn(
214 "flex items-start gap-2.5 rounded-lg px-2.5 py-2 transition-colors duration-200",
215 "flex items-center gap-2.5 rounded-lg px-2.5 py-2 transition-colors duration-200",
215216 step.status === "inProgress" && "bg-blue-500/5",
216217 step.status === "completed" && "bg-emerald-500/5",
217218 )}
218219 >
219 <div className="mt-0.5">{stepStatusIcon(step.status)}</div>
220 {stepStatusIcon(step.status)}
220221 <p
221222 className={cn(
222223 "text-[13px] leading-snug",
apps/web/src/components/chat/ChatHeader.tsx+2-16
......@@ -10,7 +10,6 @@ import { memo } from "react";
1010import GitActionsControl from "../GitActionsControl";
1111import { type DraftId } from "~/composerDraftStore";
1212import { DiffIcon, TerminalSquareIcon } from "lucide-react";
13import { Badge } from "../ui/badge";
1413import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1514import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl";
1615import { Toggle } from "../ui/toggle";
......@@ -105,19 +104,6 @@ export const ChatHeader = memo(function ChatHeader({
105104 />
106105 <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup>
107106 </Tooltip>
108 {activeProjectName && (
109 <Badge
110 variant="outline"
111 className="min-w-0 max-w-full shrink overflow-hidden sm:max-w-56"
112 >
113 <span className="min-w-0 truncate">{activeProjectName}</span>
114 </Badge>
115 )}
116 {activeProjectName && !isGitRepo && (
117 <Badge variant="outline" className="shrink-0 text-[10px] text-amber-700">
118 No Git
119 </Badge>
120 )}
121107 </div>
122108 <div className="flex min-w-0 flex-wrap items-center justify-start gap-2 sm:shrink-0 sm:justify-end @3xl/header-actions:gap-3">
123109 {activeProjectScripts && (
......@@ -153,7 +139,7 @@ export const ChatHeader = memo(function ChatHeader({
153139 pressed={terminalOpen}
154140 onPressedChange={onToggleTerminal}
155141 aria-label="Toggle terminal drawer"
156 variant="outline"
142 variant="ghost"
157143 size="xs"
158144 disabled={!terminalAvailable}
159145 >
......@@ -177,7 +163,7 @@ export const ChatHeader = memo(function ChatHeader({
177163 pressed={diffOpen}
178164 onPressedChange={onToggleDiff}
179165 aria-label="Toggle diff panel"
180 variant="outline"
166 variant="ghost"
181167 size="xs"
182168 disabled={!isGitRepo && !diffOpen}
183169 >
apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx+8-1
......@@ -1,4 +1,5 @@
11import { memo } from "react";
2import { Badge } from "../ui/badge";
23
34export const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBanner({
45 planTitle,
......@@ -8,7 +9,13 @@ export const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBann
89 return (
910 <div className="px-4 py-3.5 sm:px-5 sm:py-4">
1011 <div className="flex flex-wrap items-center gap-2">
11 <span className="uppercase text-sm tracking-[0.2em]">Plan ready</span>
12 <Badge
13 variant="info"
14 size="sm"
15 className="rounded-md px-1.5 py-0 font-semibold tracking-wide uppercase"
16 >
17 Plan Ready
18 </Badge>
1219 {planTitle ? (
1320 <span className="min-w-0 flex-1 truncate text-sm font-medium">{planTitle}</span>
1421 ) : null}
apps/web/src/components/chat/MessagesTimeline.browser.tsx+29
......@@ -7,6 +7,8 @@ import { page } from "vite-plus/test/browser";
77import { afterEach, describe, expect, it, vi } from "vite-plus/test";
88import { render } from "vitest-browser-react";
99
10import { getVscodeIconUrlForEntry } from "../../vscode-icons";
11
1012const scrollToEndSpy = vi.fn();
1113const getStateSpy = vi.fn(() => ({ isAtEnd: true }));
1214
......@@ -386,4 +388,31 @@ describe("MessagesTimeline", () => {
386388 await screen.unmount();
387389 }
388390 });
391
392 it("uses the file path without line suffix for markdown file tag icons", async () => {
393 const fileLink = "[package.json](path/to/package.json:25)";
394 const screen = await render(
395 <MessagesTimeline
396 {...buildProps()}
397 markdownCwd="/repo/project"
398 timelineEntries={[buildAssistantTimelineEntry(`Updated ${fileLink}`)]}
399 />,
400 );
401
402 try {
403 const assistantFileLink = document.querySelector(
404 '[data-message-role="assistant"] .chat-markdown-file-link',
405 );
406 const icon = assistantFileLink?.querySelector("img");
407
408 expect(assistantFileLink?.textContent).toContain("package.json");
409 expect(assistantFileLink?.textContent).toContain("L25");
410 expect(assistantFileLink?.getAttribute("href")).toBe("/repo/project/path/to/package.json:25");
411 expect(icon?.getAttribute("src")).toBe(
412 getVscodeIconUrlForEntry("/repo/project/path/to/package.json", "file", "dark"),
413 );
414 } finally {
415 await screen.unmount();
416 }
417 });
389418});
apps/web/src/components/ui/toggle.tsx+2
......@@ -21,6 +21,8 @@ const toggleVariants = cva(
2121 },
2222 variant: {
2323 default: "border-transparent",
24 ghost:
25 "border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-accent data-pressed:text-accent-foreground disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",
2426 outline:
2527 "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:data-pressed:bg-input dark:hover:bg-input/64 dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] dark:not-disabled:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/2%)] [:disabled,:active,[data-pressed]]:shadow-none",
2628 },
pnpm-lock.yaml+71-52
......@@ -371,8 +371,8 @@ importers:
371371 apps/server:
372372 dependencies:
373373 '@anthropic-ai/claude-agent-sdk':
374 specifier: ^0.3.154
375 version: 0.3.159(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)
374 specifier: ^0.3.170
375 version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)
376376 '@effect/platform-bun':
377377 specifier: 4.0.0-beta.78
378378 version: 4.0.0-beta.78(bufferutil@4.1.0)(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754))(utf-8-validate@6.0.6)
......@@ -854,48 +854,48 @@ packages:
854854 '@alchemy.run/node-utils@0.0.4':
855855 resolution: {integrity: sha512-TiIhPXCTCi3tk0zmdYJJ14CNSesSfsJxXdIOP0HTSItQ1mZWLocrF7qCuEWKyW/IEFzp6kaiOf19aIA/mbCp1g==}
856856
857 '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.159':
858 resolution: {integrity: sha512-3nnH4yUNJVSyaU5DBlGw2yxc4zlnVvAnc9UOe+La47QVG7/dN+rWAgn4zCbqKk9bWFLDQ1Ek0r56EZE1Qo4UKQ==}
857 '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170':
858 resolution: {integrity: sha512-rwfgArIa5WI0QPNqFsRBgvtSI0mrtpynUm0oK6+l6/KX4hcgnYGEzciZR1bOeD9/7sSZlTdIgt+T9alKeZmXcg==}
859859 cpu: [arm64]
860860 os: [darwin]
861861
862 '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.159':
863 resolution: {integrity: sha512-iv+NRjz+t4Q1R2+kLdDbccSo3b0wedVJ9jMT3noznOVojZHzgkxVpTvt36/XkXV0rqIDQ5H18bBYIZZzOXu7mg==}
862 '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170':
863 resolution: {integrity: sha512-0e58h8UQMtsQxLGIv9r4foxfBFWKZ7NeDtoplLhuD7EwQonehomw1sBXCch77t/IfUS+q5vQ5zv+fOGmap5nLQ==}
864864 cpu: [x64]
865865 os: [darwin]
866866
867 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.159':
868 resolution: {integrity: sha512-WvwiQBWt3tdu5EwqjpDZszI6p2uetYsw4Cxc6ptO/SmLIYXcDienP8nmirZdsZrS+Gzk6imgY0IY5mmNaRhelQ==}
867 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170':
868 resolution: {integrity: sha512-SRYfQcsXlOq+CD/FqkQBTSHbaD++w73GnnO+NUV9adLYrca3kfetRwWT1iguY1cNS0l34dCR3rlzCPq78vg1Jg==}
869869 cpu: [arm64]
870870 os: [linux]
871871
872 '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.159':
873 resolution: {integrity: sha512-FlsS5M4GCpzsQVaNDFF8dRgFGR3QwyAHZFl/xM/2Y2BqVBH+NH17RpKQSJxr1qr41QnsNkinMnu2iSKoc33hKg==}
872 '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170':
873 resolution: {integrity: sha512-gLbaFqcGppFJQd4DLNV4IXoeahejT/p2/M8bSSvRDbla9GOsBr1AxV5XLRyBn1e7xFGozZIAIQr3+1chp7NJgQ==}
874874 cpu: [arm64]
875875 os: [linux]
876876
877 '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.159':
878 resolution: {integrity: sha512-kFH6RC2YJbPc8XWRNy/wL4YU7LzdJjSwAdH488sVzIif3q+TrvVrV5y/IW0+MLmta+CKIqtFYpGaucsJYvj7Eg==}
877 '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170':
878 resolution: {integrity: sha512-m4+I0qBEk7cxRKS+pL+eoWXbXTFOAo83fQ0tQvap4z/mDMm06IWJtEPoYTaMBwsp32GJWLkHWKbZSBCHZnp2DQ==}
879879 cpu: [x64]
880880 os: [linux]
881881
882 '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.159':
883 resolution: {integrity: sha512-uNPEC/iRzVb4bEdzs0KAz1zV7i1PVGEZZnJTQyi1OtgVa81sAoH/H0CbbzDiTsquKdaESf+1DSSEkUlfZmMUEw==}
882 '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170':
883 resolution: {integrity: sha512-Xl/m7TaSC3T5IDBdHrZQ9fCQYyDmPELN34CL+MoyPIf7uSmuZnjE9fUOqDh2Rv26JxWssi1M6X+BBvVuKd6Cpg==}
884884 cpu: [x64]
885885 os: [linux]
886886
887 '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.159':
888 resolution: {integrity: sha512-WN1QEZGgWXz9GMl61QU6j9E+LEF5plki87bL2xsGwuCPzK+OeVPQU55pabuP8P+vFBFHUo3Y9OlTVyZHnUzmAQ==}
887 '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170':
888 resolution: {integrity: sha512-IG+8isJNNJKbnnhO7m+PGhfVCg+XoQ/MDxGde5eigFI0WsEfitjuWSWwx82bT9ghxI1aa6qNvI+UPgPcZuo5Fg==}
889889 cpu: [arm64]
890890 os: [win32]
891891
892 '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.159':
893 resolution: {integrity: sha512-Ty4seccD+dTDX5hhj89IUELZd/LkxO5O43Uiz5Mo8ZJktoX38SK4XMZlBS935QdqTFLRvPL0hvK4Lt4dTOqzPw==}
892 '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170':
893 resolution: {integrity: sha512-7cuqSKbHVItPGVwRbd3A0BEJwcNtc7Fhoh6qHN4C6yrmjSrvdYYx3MLvq/VI768/RoG7mAMDxb+j7WfEfoP9BA==}
894894 cpu: [x64]
895895 os: [win32]
896896
897 '@anthropic-ai/claude-agent-sdk@0.3.159':
898 resolution: {integrity: sha512-Xh1oVMIK6N3KsiNIhqNH8ZK90zjRmAEL9d1Md8ZlGdHJE+HhdMYdBadujc3KEkV0uufsEUvYp+A3fDenfypGSA==}
897 '@anthropic-ai/claude-agent-sdk@0.3.170':
898 resolution: {integrity: sha512-pAvhfk+iTodXZ6RF18Kz7BEUWFjL7EcR3tKuhUNdPpE1NAYCR3mSHGbafi72JsrNwKEDIs7FU31z3fqhwy8QzA==}
899899 engines: {node: '>=18.0.0'}
900900 peerDependencies:
901901 '@anthropic-ai/sdk': '>=0.93.0'
......@@ -2809,6 +2809,12 @@ packages:
28092809 '@emnapi/core': ^1.7.1
28102810 '@emnapi/runtime': ^1.7.1
28112811
2812 '@napi-rs/wasm-runtime@1.1.5':
2813 resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
2814 peerDependencies:
2815 '@emnapi/core': ^1.7.1
2816 '@emnapi/runtime': ^1.7.1
2817
28122818 '@neon-rs/load@0.0.4':
28132819 resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
28142820
......@@ -6534,8 +6540,8 @@ packages:
65346540 hoist-non-react-statics@3.3.2:
65356541 resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
65366542
6537 hono@4.12.23:
6538 resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==}
6543 hono@4.12.25:
6544 resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==}
65396545 engines: {node: '>=16.9.0'}
65406546
65416547 hosted-git-info@4.1.0:
......@@ -7689,6 +7695,10 @@ packages:
76897695 obug@2.1.1:
76907696 resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
76917697
7698 obug@2.1.2:
7699 resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==}
7700 engines: {node: '>=12.20.0'}
7701
76927702 ofetch@1.5.1:
76937703 resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
76947704
......@@ -8627,8 +8637,8 @@ packages:
86278637 resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
86288638 engines: {node: '>= 0.4'}
86298639
8630 side-channel@1.1.0:
8631 resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
8640 side-channel@1.1.1:
8641 resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
86328642 engines: {node: '>= 0.4'}
86338643
86348644 siginfo@2.0.0:
......@@ -9673,44 +9683,44 @@ snapshots:
96739683
96749684 '@alchemy.run/node-utils@0.0.4': {}
96759685
9676 '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.159':
9686 '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170':
96779687 optional: true
96789688
9679 '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.159':
9689 '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170':
96809690 optional: true
96819691
9682 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.159':
9692 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170':
96839693 optional: true
96849694
9685 '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.159':
9695 '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170':
96869696 optional: true
96879697
9688 '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.159':
9698 '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170':
96899699 optional: true
96909700
9691 '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.159':
9701 '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170':
96929702 optional: true
96939703
9694 '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.159':
9704 '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170':
96959705 optional: true
96969706
9697 '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.159':
9707 '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170':
96989708 optional: true
96999709
9700 '@anthropic-ai/claude-agent-sdk@0.3.159(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)':
9710 '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)':
97019711 dependencies:
97029712 '@anthropic-ai/sdk': 0.93.0(zod@4.4.3)
97039713 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
97049714 zod: 4.4.3
97059715 optionalDependencies:
9706 '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.159
9707 '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.159
9708 '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.159
9709 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.159
9710 '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.159
9711 '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.159
9712 '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.159
9713 '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.159
9716 '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.170
9717 '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.170
9718 '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.170
9719 '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.170
9720 '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.170
9721 '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.170
9722 '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.170
9723 '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.170
97149724
97159725 '@anthropic-ai/sdk@0.93.0(zod@4.4.3)':
97169726 dependencies:
......@@ -11606,9 +11616,9 @@ snapshots:
1160611616
1160711617 '@formkit/auto-animate@0.9.0': {}
1160811618
11609 '@hono/node-server@1.19.14(hono@4.12.23)':
11619 '@hono/node-server@1.19.14(hono@4.12.25)':
1161011620 dependencies:
11611 hono: 4.12.23
11621 hono: 4.12.25
1161211622
1161311623 '@img/colour@1.1.0':
1161411624 optional: true
......@@ -12031,7 +12041,7 @@ snapshots:
1203112041
1203212042 '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)':
1203312043 dependencies:
12034 '@hono/node-server': 1.19.14(hono@4.12.23)
12044 '@hono/node-server': 1.19.14(hono@4.12.25)
1203512045 ajv: 8.20.0
1203612046 ajv-formats: 3.0.1(ajv@8.20.0)
1203712047 content-type: 1.0.5
......@@ -12041,7 +12051,7 @@ snapshots:
1204112051 eventsource-parser: 3.1.0
1204212052 express: 5.2.1
1204312053 express-rate-limit: 8.5.2(express@5.2.1)
12044 hono: 4.12.23
12054 hono: 4.12.25
1204512055 jose: 6.2.3
1204612056 json-schema-typed: 8.0.2
1204712057 pkce-challenge: 5.0.1
......@@ -12085,6 +12095,13 @@ snapshots:
1208512095 '@tybys/wasm-util': 0.10.2
1208612096 optional: true
1208712097
12098 '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
12099 dependencies:
12100 '@emnapi/core': 1.10.0
12101 '@emnapi/runtime': 1.10.0
12102 '@tybys/wasm-util': 0.10.2
12103 optional: true
12104
1208812105 '@neon-rs/load@0.0.4': {}
1208912106
1209012107 '@noble/curves@1.9.1':
......@@ -12603,7 +12620,7 @@ snapshots:
1260312620 '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7)
1260412621 '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7)
1260512622 '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7)
12606 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.7)
12623 '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7)
1260712624 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
1260812625 '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
1260912626 '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7)
......@@ -12818,7 +12835,7 @@ snapshots:
1281812835 dependencies:
1281912836 '@emnapi/core': 1.10.0
1282012837 '@emnapi/runtime': 1.10.0
12821 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
12838 '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
1282212839 optional: true
1282312840
1282412841 '@rolldown/binding-wasm32-wasi@1.0.1':
......@@ -12832,7 +12849,7 @@ snapshots:
1283212849 dependencies:
1283312850 '@emnapi/core': 1.10.0
1283412851 '@emnapi/runtime': 1.10.0
12835 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
12852 '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
1283612853 optional: true
1283712854
1283812855 '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
......@@ -15896,7 +15913,7 @@ snapshots:
1589615913 dependencies:
1589715914 react-is: 16.13.1
1589815915
15899 hono@4.12.23: {}
15916 hono@4.12.25: {}
1590015917
1590115918 hosted-git-info@4.1.0:
1590215919 dependencies:
......@@ -17258,6 +17275,8 @@ snapshots:
1725817275
1725917276 obug@2.1.1: {}
1726017277
17278 obug@2.1.2: {}
17279
1726117280 ofetch@1.5.1:
1726217281 dependencies:
1726317282 destr: 2.0.5
......@@ -17645,7 +17664,7 @@ snapshots:
1764517664
1764617665 qs@6.15.2:
1764717666 dependencies:
17648 side-channel: 1.1.0
17667 side-channel: 1.1.1
1764917668
1765017669 query-string@7.1.3:
1765117670 dependencies:
......@@ -18439,7 +18458,7 @@ snapshots:
1843918458 object-inspect: 1.13.4
1844018459 side-channel-map: 1.0.1
1844118460
18442 side-channel@1.1.0:
18461 side-channel@1.1.1:
1844318462 dependencies:
1844418463 es-errors: 1.3.0
1844518464 object-inspect: 1.13.4
......@@ -19071,7 +19090,7 @@ snapshots:
1907119090 es-module-lexer: 2.1.0
1907219091 expect-type: 1.3.0
1907319092 magic-string: 0.30.21
19074 obug: 2.1.1
19093 obug: 2.1.2
1907519094 pathe: 2.0.3
1907619095 picomatch: 4.0.4
1907719096 std-env: 4.1.0