Add file badges, menu icons, LSP diagnostics, robust compaction

- Files in chat: preview with type/size badges and file name; attachment chips
- Menu and settings tabs with icons
- Heatmap: distinct colour for empty hours, hourly labels
- Context chip shows used/total tokens (e.g. 221k/240k) plus percent
- LSP client: diagnostics appended to edit/write results and a diagnostics tool
  (typescript-language-server, pyright, rust-analyzer, gopls, clangd; sandboxed)
- Compaction summarizes a plain transcript and retries once on empty output
- files/stat endpoint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 19:50:12 +01:00
1 parent 5b569b6835
commit ec1b9b4925
22 files changed
+1036 -48

No files matched your search

+46 -5
View File
@@ -1,3 +1,4 @@
import { base64Size, FileChip, FileFigure, fileType } from "./FileFigure";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { del, get, patch, post } from "../api";
import { useModels } from "../hooks";
@@ -12,7 +13,7 @@ import { ShareDialog } from "./ShareDialog";
import { QueueList } from "./QueueList";
import { BudgetChip } from "./Budget";
import { PermissionCard, QuestionCard } from "./Interaction";
import { ImageRefs, Markdown } from "./Markdown";
import { ImageRefs, Markdown, projectFileUrl } from "./Markdown";
import { UsageOverlay } from "./UsageOverlay";
import { RewindDialog } from "./RewindDialog";
import { ShellsChip } from "./ShellsPanel";
@@ -238,7 +239,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
</div>
{ctxShare !== undefined && (
<span className={`chip hide-mobile ${ctxShare > 0.7 ? "warn" : ""}`} title={t("chat.contextHint", { tokens: fmtNumber(ctxTokens) })}>
{t("chat.context")} {fmtPercent(ctxShare)}
{t("chat.context")} {fmtK(ctxTokens)}/{fmtK(modelInfo!.context!)} · {fmtPercent(ctxShare)}
</span>
)}
{stats && (
@@ -325,11 +326,13 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
</button>
</div>
)}
{texts.map((p) => p.text).join("\n\n")}
<UserText text={texts.map((p) => p.text).join("\n\n")} />
{imgs.length > 0 && (
<div>
<div className="user-files">
{imgs.map((p, i) => (
<img key={i} src={`data:${p.image.media_type};base64,${p.image.data}`} alt="" />
<FileFigure key={i} name={`image-${i + 1}.${fileType(undefined, p.image.media_type).toLowerCase()}`} mime={p.image.media_type} size={base64Size(p.image.data)}>
<img src={`data:${p.image.media_type};base64,${p.image.data}`} alt="" />
</FileFigure>
))}
</div>
)}
@@ -399,3 +402,41 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
</>
);
}
/** Attached-file lines ("- /path (12.3 KB)") of a user message become file chips. */
const ATTACHED_RE = /^- (\/\S+) \(([\d.]+ (?:B|KB|MB))\)$/;
function UserText({ text }: { text: string }) {
const lines = text.split("\n");
const files: { path: string; size: string }[] = [];
const rest: string[] = [];
for (const l of lines) {
const m = l.match(ATTACHED_RE);
if (m) files.push({ path: m[1], size: m[2] });
else rest.push(l);
}
// Drop the heading line ("Attached files …:") right before the list.
while (files.length && rest.length && (rest[rest.length - 1].trim() === "" || rest[rest.length - 1].trim().endsWith(":"))) {
const last = rest.pop()!;
if (last.trim().endsWith(":")) break;
}
return (
<>
{rest.join("\n").trimEnd()}
{files.length > 0 && (
<div className="user-files">
{files.map((f) => (
<FileChip key={f.path} url={projectFileUrl(f.path)} name={f.path} sizeText={f.size} />
))}
</div>
)}
</>
);
}
/** Token counts like "221k" / "1.2M" (same in every language). */
function fmtK(n: number) {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1).replace(/\.0$/, "")}M`;
if (n >= 1000) return `${Math.round(n / 1000)}k`;
return String(n);
}