chore: fix lint baseline

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 23:43:10 +02:00
parent 1840ab3d7e
commit ef63a7a063
12 changed files with 66 additions and 34 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# 01. Fix lint and add CI scripts # 01. Fix lint and add CI scripts
Status: pending Status: completed
Make the hygiene baseline green and easy to run locally/CI: Make the hygiene baseline green and easy to run locally/CI:
+1
View File
@@ -12,6 +12,7 @@
"build": "tsc && vite build", "build": "tsc && vite build",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "eslint \"src/**/*.ts\" vite.config.ts", "lint": "eslint \"src/**/*.ts\" vite.config.ts",
"verify": "npm run typecheck && npm run lint",
"start": "tsx src/server/index.ts", "start": "tsx src/server/index.ts",
"start:sessiond": "tsx src/server/sessiond.ts" "start:sessiond": "tsx src/server/sessiond.ts"
}, },
+17 -4
View File
@@ -356,7 +356,9 @@ function parseFileTreeEntry(value: unknown): FileTreeEntry {
function parseFileContentResponse(value: unknown): FileContentResponse { function parseFileContentResponse(value: unknown): FileContentResponse {
const record = requireRecord(value); const record = requireRecord(value);
return { path: requireString(record, "path"), ...optionalField("language", optionalString(record, "language")), encoding: requireString(record, "encoding") as "utf8", size: requireNumber(record, "size"), modifiedAt: requireString(record, "modifiedAt"), content: requireString(record, "content"), truncated: requireBoolean(record, "truncated"), binary: requireBoolean(record, "binary") }; const encoding = requireString(record, "encoding");
if (encoding !== "utf8") throw new Error("Invalid file encoding");
return { path: requireString(record, "path"), ...optionalField("language", optionalString(record, "language")), encoding, size: requireNumber(record, "size"), modifiedAt: requireString(record, "modifiedAt"), content: requireString(record, "content"), truncated: requireBoolean(record, "truncated"), binary: requireBoolean(record, "binary") };
} }
function parseGitStatusResponse(value: unknown): GitStatusResponse { function parseGitStatusResponse(value: unknown): GitStatusResponse {
@@ -370,9 +372,20 @@ function parseGitStatusFile(value: unknown): GitStatusFile {
} }
function parseGitFileState(value: unknown): GitFileState { function parseGitFileState(value: unknown): GitFileState {
if (typeof value !== "string") throw new Error("Expected git file state"); switch (value) {
if (!["unmodified", "modified", "added", "deleted", "renamed", "copied", "untracked", "ignored", "conflicted"].includes(value)) throw new Error("Invalid git file state"); case "unmodified":
return value as GitFileState; case "modified":
case "added":
case "deleted":
case "renamed":
case "copied":
case "untracked":
case "ignored":
case "conflicted":
return value;
default:
throw new Error("Invalid git file state");
}
} }
function parseGitDiffResponse(value: unknown): GitDiffResponse { function parseGitDiffResponse(value: unknown): GitDiffResponse {
+1 -1
View File
@@ -36,7 +36,7 @@ export function writeChatHistoryCache(sessionId: string, page: RawMessagePage):
} }
export function mergeChatHistory(existing: RawMessagePage | undefined, incoming: RawMessagePage): RawMessagePage { export function mergeChatHistory(existing: RawMessagePage | undefined, incoming: RawMessagePage): RawMessagePage {
if (!existing || existing.total !== incoming.total) return incoming; if (existing?.total !== incoming.total) return incoming;
const start = Math.min(existing.start, incoming.start); const start = Math.min(existing.start, incoming.start);
const end = Math.max(existing.start + existing.messages.length, incoming.start + incoming.messages.length); const end = Math.max(existing.start + existing.messages.length, incoming.start + incoming.messages.length);
+3 -3
View File
@@ -26,7 +26,7 @@ export function normalizeMessage(message: unknown): ChatLine[] {
const parts = normalizeContent(getProperty(message, "content"), message); const parts = normalizeContent(getProperty(message, "content"), message);
const skillLines = role === "user" ? normalizeSkillInvocation(parts) : undefined; const skillLines = role === "user" ? normalizeSkillInvocation(parts) : undefined;
if (skillLines !== undefined) return skillLines; if (skillLines !== undefined) return skillLines;
const source = normalizeSource(message, parts); const source = normalizeSource(message);
if (role === "tool") return [{ role, parts, ...(source === undefined ? {} : { source }) }]; if (role === "tool") return [{ role, parts, ...(source === undefined ? {} : { source }) }];
const visible = parts.filter((part) => part.type !== "empty"); const visible = parts.filter((part) => part.type !== "empty");
@@ -44,7 +44,7 @@ function normalizeSkillInvocation(parts: ChatPart[]): ChatLine[] | undefined {
} }
function parseSkillBlock(text: string): { name: string; location: string; content: string; userMessage?: string } | undefined { function parseSkillBlock(text: string): { name: string; location: string; content: string; userMessage?: string } | undefined {
const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/); const match = /^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/.exec(text);
if (match === null) return undefined; if (match === null) return undefined;
const userMessage = match[4]?.trim(); const userMessage = match[4]?.trim();
return { return {
@@ -55,7 +55,7 @@ function parseSkillBlock(text: string): { name: string; location: string; conten
}; };
} }
function normalizeSource(message: unknown, _parts: ChatPart[]): ChatLine["source"] | undefined { function normalizeSource(message: unknown): ChatLine["source"] | undefined {
const source = getString(message, "source"); const source = getString(message, "source");
if (source === "compaction" || source === "branch_summary") return source; if (source === "compaction" || source === "branch_summary") return source;
return undefined; return undefined;
+2 -2
View File
@@ -86,11 +86,11 @@ export class ChatView extends LitElement {
const olderCount = this.messageStart; const olderCount = this.messageStart;
const fullHistory = olderCount <= 0 const fullHistory = olderCount <= 0
? "full history loaded" ? "full history loaded"
: `${olderCount} older not loaded · ${loadedPercent}% loaded`; : `${String(olderCount)} older not loaded · ${String(loadedPercent)}% loaded`;
return html` return html`
<div class="history-indicator"> <div class="history-indicator">
<div>${fullHistory}</div> <div>${fullHistory}</div>
<div>loaded scroll: ${this.loadedScrollPercent}% from top</div> <div>loaded scroll: ${String(this.loadedScrollPercent)}% from top</div>
</div> </div>
`; `;
} }
+1
View File
@@ -97,6 +97,7 @@ const viewerTheme = EditorView.theme({
}); });
function languageExtensions(language: string | undefined): Extension[] { function languageExtensions(language: string | undefined): Extension[] {
if (language === undefined) return [];
switch (language) { switch (language) {
case "typescript": return [javascript({ typescript: true })]; case "typescript": return [javascript({ typescript: true })];
case "javascript": return [javascript()]; case "javascript": return [javascript()];
+9 -7
View File
@@ -149,9 +149,7 @@ export class PiWebApp extends LitElement {
const workspace = this.state.selectedWorkspace; const workspace = this.state.selectedWorkspace;
if (!project || !workspace) return; if (!project || !workspace) return;
if (this.state.expandedDirs[path] !== undefined) { if (this.state.expandedDirs[path] !== undefined) {
const next = { ...this.state.expandedDirs }; this.setState({ expandedDirs: omitKey(this.state.expandedDirs, path) });
delete next[path];
this.setState({ expandedDirs: next });
return; return;
} }
try { try {
@@ -231,7 +229,7 @@ export class PiWebApp extends LitElement {
} }
private renderWorkspacePanel() { private renderWorkspacePanel() {
return html`<workspace-panel .workspace=${this.state.selectedWorkspace} .tool=${this.state.workspaceTool} .fileTree=${this.state.fileTree} .expandedDirs=${this.state.expandedDirs} .selectedFilePath=${this.state.selectedFilePath} .selectedFileContent=${this.state.selectedFileContent} .fileTreeStale=${this.state.fileTreeStale} .gitStatus=${this.state.gitStatus} .selectedDiffPath=${this.state.selectedDiffPath} .selectedDiff=${this.state.selectedDiff} .gitStale=${this.state.gitStale} .onSelectTool=${(tool: "files" | "git") => this.selectWorkspaceTool(tool)} .onRefreshFiles=${() => this.refreshFiles()} .onExpandDir=${(path: string) => this.expandDir(path)} .onSelectFile=${(path: string) => this.selectFile(path)} .onRefreshGit=${() => this.refreshGit()} .onSelectDiff=${(path: string) => this.selectDiff(path)}></workspace-panel>`; return html`<workspace-panel .workspace=${this.state.selectedWorkspace} .tool=${this.state.workspaceTool} .fileTree=${this.state.fileTree} .expandedDirs=${this.state.expandedDirs} .selectedFilePath=${this.state.selectedFilePath} .selectedFileContent=${this.state.selectedFileContent} .fileTreeStale=${this.state.fileTreeStale} .gitStatus=${this.state.gitStatus} .selectedDiffPath=${this.state.selectedDiffPath} .selectedDiff=${this.state.selectedDiff} .gitStale=${this.state.gitStale} .onSelectTool=${(tool: "files" | "git") => { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.refreshFiles()} .onExpandDir=${(path: string) => this.expandDir(path)} .onSelectFile=${(path: string) => this.selectFile(path)} .onRefreshGit=${() => this.refreshGit()} .onSelectDiff=${(path: string) => this.selectDiff(path)}></workspace-panel>`;
} }
override render() { override render() {
@@ -249,9 +247,9 @@ export class PiWebApp extends LitElement {
</aside> </aside>
<main class=${`${state.mainView}-view`}> <main class=${`${state.mainView}-view`}>
<div class="mobile-tabs"> <div class="mobile-tabs">
<button class=${state.mainView === "chat" ? "selected" : ""} @click=${() => this.selectMainView("chat")}>Chat</button> <button class=${state.mainView === "chat" ? "selected" : ""} @click=${() => { this.selectMainView("chat"); }}>Chat</button>
<button class=${state.mainView === "files" ? "selected" : ""} @click=${() => this.selectMainView("files")}>Files</button> <button class=${state.mainView === "files" ? "selected" : ""} @click=${() => { this.selectMainView("files"); }}>Files</button>
<button class=${state.mainView === "git" ? "selected" : ""} @click=${() => this.selectMainView("git")}>Git</button> <button class=${state.mainView === "git" ? "selected" : ""} @click=${() => { this.selectMainView("git"); }}>Git</button>
</div> </div>
${state.error ? html`<div class="error">${state.error}</div>` : null} ${state.error ? html`<div class="error">${state.error}</div>` : null}
${state.selectedSession ? html` ${state.selectedSession ? html`
@@ -274,6 +272,10 @@ function isActive(status: AppState["status"]): boolean {
return status?.isStreaming === true || status?.isBashRunning === true || status?.isCompacting === true; return status?.isStreaming === true || status?.isBashRunning === true || status?.isCompacting === true;
} }
function omitKey<T>(record: Record<string, T>, keyToOmit: string): Record<string, T> {
return Object.fromEntries(Object.entries(record).filter(([key]) => key !== keyToOmit));
}
function nextFrame(): Promise<void> { function nextFrame(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => { resolve(); })); return new Promise((resolve) => requestAnimationFrame(() => { resolve(); }));
} }
+1 -1
View File
@@ -208,7 +208,7 @@ export class PromptEditor extends LitElement {
function fileInsertText(path: string, pathMode: boolean, quoted: boolean): string { function fileInsertText(path: string, pathMode: boolean, quoted: boolean): string {
const prefix = pathMode ? "" : "@"; const prefix = pathMode ? "" : "@";
if (!quoted && !path.includes(" ")) return `${prefix}${path}`; if (!quoted && !path.includes(" ")) return `${prefix}${path}`;
return `${prefix}\"${path}\"`; return `${prefix}"${path}"`;
} }
function emptySlashCommands(): SlashCommand[] { function emptySlashCommands(): SlashCommand[] {
+26 -13
View File
@@ -29,8 +29,8 @@ export class WorkspacePanel extends LitElement {
return html` return html`
<header> <header>
<div class="tabs"> <div class="tabs">
<button class=${this.tool === "files" ? "selected" : ""} @click=${() => this.onSelectTool("files")}>Files</button> <button class=${this.tool === "files" ? "selected" : ""} @click=${() => { this.onSelectTool("files"); }}>Files</button>
<button class=${this.tool === "git" ? "selected" : ""} @click=${() => this.onSelectTool("git")}>Git</button> <button class=${this.tool === "git" ? "selected" : ""} @click=${() => { this.onSelectTool("git"); }}>Git</button>
</div> </div>
<small title=${this.workspace.path}>${this.workspace.label}</small> <small title=${this.workspace.path}>${this.workspace.label}</small>
</header> </header>
@@ -58,19 +58,25 @@ export class WorkspacePanel extends LitElement {
private renderTreeEntry(entry: FileTreeEntry, depth: number): TemplateResult { private renderTreeEntry(entry: FileTreeEntry, depth: number): TemplateResult {
const children = this.expandedDirs[entry.path]; const children = this.expandedDirs[entry.path];
const hasChildren = children !== undefined;
return html` return html`
<button class="row" style=${`--depth:${depth}`} @click=${() => entry.type === "directory" ? this.onExpandDir(entry.path) : this.onSelectFile(entry.path)}> <button class="row" style=${`--depth:${String(depth)}`} @click=${() => { this.selectTreeEntry(entry); }}>
<span>${entry.type === "directory" ? (children ? "▾" : "▸") : "·"}</span> <span>${entry.type === "directory" ? (hasChildren ? "▾" : "▸") : "·"}</span>
<span>${entry.name}</span> <span>${entry.name}</span>
</button> </button>
${children ? children.map((child) => this.renderTreeEntry(child, depth + 1)) : null} ${hasChildren ? children.map((child) => this.renderTreeEntry(child, depth + 1)) : null}
`; `;
} }
private selectTreeEntry(entry: FileTreeEntry): void {
if (entry.type === "directory") this.onExpandDir(entry.path);
else this.onSelectFile(entry.path);
}
private renderFileViewer() { private renderFileViewer() {
const file = this.selectedFileContent; const file = this.selectedFileContent;
if (!this.selectedFilePath) return html`<p class="muted">Select a file.</p>`; if (this.selectedFilePath === undefined || this.selectedFilePath === "") return html`<p class="muted">Select a file.</p>`;
if (!file) return html`<p class="muted">Loading ${this.selectedFilePath}…</p>`; if (file === undefined) return html`<p class="muted">Loading ${this.selectedFilePath}…</p>`;
if (file.binary) return html`<p class="muted">Binary file: ${file.path}</p>`; if (file.binary) return html`<p class="muted">Binary file: ${file.path}</p>`;
return html` return html`
<div class="viewer-header"><strong>${file.path}</strong><small>${file.language ?? "text"}${file.truncated ? " · truncated" : ""}</small></div> <div class="viewer-header"><strong>${file.path}</strong><small>${file.language ?? "text"}${file.truncated ? " · truncated" : ""}</small></div>
@@ -88,10 +94,10 @@ export class WorkspacePanel extends LitElement {
</section> </section>
<section class="split"> <section class="split">
<div class="list"> <div class="list">
${status === undefined ? html`<p class="muted">No status loaded.</p>` : status.isGitRepo === false ? html`<p class="muted">Not a git repository.</p>` : html` ${status === undefined ? html`<p class="muted">No status loaded.</p>` : !status.isGitRepo ? html`<p class="muted">Not a git repository.</p>` : html`
<p class="summary">${status.branch ?? "detached"}${status.ahead || status.behind ? ` · ↑${status.ahead ?? 0}${status.behind ?? 0}` : ""}</p> <p class="summary">${this.gitSummary(status)}</p>
${status.files.length === 0 ? html`<p class="muted">No changes.</p>` : status.files.map((file) => html` ${status.files.length === 0 ? html`<p class="muted">No changes.</p>` : status.files.map((file) => html`
<button class="row ${this.selectedDiffPath === file.path ? "selected" : ""}" @click=${() => this.onSelectDiff(file.path)}> <button class="row ${this.selectedDiffPath === file.path ? "selected" : ""}" @click=${() => { this.onSelectDiff(file.path); }}>
<span>${stateLabel(file.index, file.workingTree)}</span> <span>${stateLabel(file.index, file.workingTree)}</span>
<span>${file.path}</span> <span>${file.path}</span>
</button> </button>
@@ -106,15 +112,22 @@ export class WorkspacePanel extends LitElement {
} }
private renderDiffViewer() { private renderDiffViewer() {
if (!this.selectedDiffPath) return html`<p class="muted">Select a changed file.</p>`; if (this.selectedDiffPath === undefined || this.selectedDiffPath === "") return html`<p class="muted">Select a changed file.</p>`;
const diff = this.selectedDiff; const diff = this.selectedDiff;
if (!diff) return html`<p class="muted">Loading diff…</p>`; if (diff === undefined) return html`<p class="muted">Loading diff…</p>`;
return html` return html`
<div class="viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div> <div class="viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div>
<code-viewer .content=${diff.diff || "No unstaged diff."}></code-viewer> <code-viewer .content=${diff.diff !== "" ? diff.diff : "No unstaged diff."}></code-viewer>
`; `;
} }
private gitSummary(status: GitStatusResponse): string {
const branch = status.branch ?? "detached";
const ahead = status.ahead ?? 0;
const behind = status.behind ?? 0;
return ahead === 0 && behind === 0 ? branch : `${branch} · ↑${String(ahead)}${String(behind)}`;
}
static override styles = workspacePanelStyles; static override styles = workspacePanelStyles;
} }
+1
View File
@@ -88,6 +88,7 @@ function parseStatus(raw: string): GitStatusResponse {
} }
function stateFor(code: string | undefined): GitFileState { function stateFor(code: string | undefined): GitFileState {
if (code === undefined) return "unmodified";
switch (code) { switch (code) {
case ".": return "unmodified"; case ".": return "unmodified";
case "M": return "modified"; case "M": return "modified";
+3 -2
View File
@@ -41,7 +41,7 @@ function isProbablyBinary(buffer: Buffer): boolean {
function languageForPath(path: string): { language?: string } { function languageForPath(path: string): { language?: string } {
const ext = path.split(".").pop()?.toLowerCase(); const ext = path.split(".").pop()?.toLowerCase();
const language = ext === undefined ? undefined : ({ const languages: Record<string, string | undefined> = {
ts: "typescript", ts: "typescript",
tsx: "typescript", tsx: "typescript",
js: "javascript", js: "javascript",
@@ -56,6 +56,7 @@ function languageForPath(path: string): { language?: string } {
sh: "shell", sh: "shell",
yml: "yaml", yml: "yaml",
yaml: "yaml", yaml: "yaml",
} as Record<string, string | undefined>)[ext]; };
const language = ext === undefined ? undefined : languages[ext];
return language === undefined ? {} : { language }; return language === undefined ? {} : { language };
} }