Skip to content

Commit b192570

Browse files
committed
Release v0.0.52
## What's New ### Features - **File Viewer Sidebar** — Monaco Editor-powered file viewer with theme support, custom context menu, and find widget - **Cmd+P File Search** — Quick file search dialog with recently opened files - **Terminal Display Modes** — Switch between sidebar and bottom panel terminal layout - **AGENTS.md Support** — Project-level instructions via AGENTS.md files - **Task Tools in Sidebar** — Show task tools in details sidebar todo widget ### Improvements & Fixes - **Reply to Wrong Sub-Chat Fix** — Fixed pending messages being sent to inactive sub-chat tab — thanks @jjjrmy! (#127) - **Project-Level Skills Discovery** — Pass project path to skills and agents queries — thanks @amal-irgashev! (#132) - **Relative Paths in Tools** — Show relative paths instead of absolute in tool UI - **Attachments Layout** — Render all attachment blocks in a single row above message bubble - **Any File Attachments** — Allow attaching any file type in chat - **Cmd+J Terminal Toggle** — Toggle terminal with keyboard shortcut - **Rollback Fix** — Handle checkpoint not found gracefully - **Multi-Account Fix** — Stop deleting active account when adding new one - **File Icons** — Added icons for .gitignore, .npmrc, .prettierrc, and .txt files - **Sub Agent Labels** — Renamed Task tool display labels to Sub agent
1 parent 4c67545 commit b192570

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+4858
-207
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ By [21st.dev](https://21st.dev) team
1919
| **Integrated Terminal** |||
2020
| **Plan Mode** |||
2121
| **MCP Support** |||
22-
| **Memory (CLAUDE.md & AGENTS.md)** |||
22+
| **Memory (CLAUDE.md)** |||
2323
| **Skills & Slash Commands** |||
2424
| **Custom Subagents** |||
2525
| **Subscription & API Key Support** |||

bun.lockb

-1.83 KB
Binary file not shown.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.51",
3+
"version": "0.0.52",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {
@@ -37,6 +37,7 @@
3737
"@git-diff-view/react": "^0.0.35",
3838
"@git-diff-view/shiki": "^0.0.36",
3939
"@modelcontextprotocol/sdk": "^1.25.3",
40+
"@monaco-editor/react": "^4.7.0",
4041
"@radix-ui/react-accordion": "^1.2.12",
4142
"@radix-ui/react-alert-dialog": "^1.1.15",
4243
"@radix-ui/react-checkbox": "^1.3.3",
@@ -82,6 +83,7 @@
8283
"jsonc-parser": "^3.3.1",
8384
"lucide-react": "^0.468.0",
8485
"mermaid": "^11.12.2",
86+
"monaco-editor": "^0.55.1",
8587
"motion": "^11.15.0",
8688
"next-themes": "^0.4.4",
8789
"node-pty": "^1.1.0",
@@ -92,7 +94,6 @@
9294
"react-dom": "19.2.1",
9395
"react-hotkeys-hook": "^4.6.1",
9496
"react-icons": "^5.5.0",
95-
"react-syntax-highlighter": "^16.1.0",
9697
"react-zoom-pan-pinch": "^3.7.0",
9798
"remark-breaks": "^4.0.0",
9899
"remark-gfm": "^4.0.1",
@@ -116,7 +117,6 @@
116117
"@types/node": "^20.17.50",
117118
"@types/react": "^19.0.7",
118119
"@types/react-dom": "^19.0.3",
119-
"@types/react-syntax-highlighter": "^15.5.13",
120120
"@vitejs/plugin-react": "^4.3.4",
121121
"@welldone-software/why-did-you-render": "^10.0.1",
122122
"autoprefixer": "^10.4.20",

src/main/lib/trpc/routers/files.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { z } from "zod"
22
import { router, publicProcedure } from "../index"
33
import { readdir, stat, readFile, writeFile, mkdir } from "node:fs/promises"
4-
import { join, relative, basename } from "node:path"
4+
import { join, relative, basename, extname } from "node:path"
55
import { app } from "electron"
6+
import { watch } from "node:fs"
7+
import { observable } from "@trpc/server/observable"
68

79
// Directories to ignore when scanning
810
const IGNORED_DIRS = new Set([
@@ -280,6 +282,109 @@ export const filesRouter = router({
280282
}
281283
}),
282284

285+
/**
286+
* Read a text file with size/binary validation
287+
* Returns structured result with error reasons
288+
*/
289+
readTextFile: publicProcedure
290+
.input(z.object({ filePath: z.string() }))
291+
.query(async ({ input }) => {
292+
const { filePath } = input
293+
const MAX_SIZE = 2 * 1024 * 1024 // 2 MB
294+
295+
try {
296+
const fileStat = await stat(filePath)
297+
298+
if (fileStat.size > MAX_SIZE) {
299+
return { ok: false as const, reason: "too-large" as const, byteLength: fileStat.size }
300+
}
301+
302+
const buffer = await readFile(filePath)
303+
304+
// Check if binary by looking for null bytes in first 8KB
305+
const sample = buffer.subarray(0, 8192)
306+
if (sample.includes(0)) {
307+
return { ok: false as const, reason: "binary" as const, byteLength: fileStat.size }
308+
}
309+
310+
const content = buffer.toString("utf-8")
311+
return { ok: true as const, content, byteLength: fileStat.size }
312+
} catch (error) {
313+
const msg = error instanceof Error ? error.message : "Unknown error"
314+
if (msg.includes("ENOENT") || msg.includes("no such file")) {
315+
return { ok: false as const, reason: "not-found" as const, byteLength: 0 }
316+
}
317+
throw new Error(`Failed to read file: ${msg}`)
318+
}
319+
}),
320+
321+
/**
322+
* Read a binary file as base64 (for images)
323+
*/
324+
readBinaryFile: publicProcedure
325+
.input(z.object({ filePath: z.string() }))
326+
.query(async ({ input }) => {
327+
const { filePath } = input
328+
const MAX_SIZE = 20 * 1024 * 1024 // 20 MB
329+
330+
try {
331+
const fileStat = await stat(filePath)
332+
333+
if (fileStat.size > MAX_SIZE) {
334+
return { ok: false as const, reason: "too-large" as const, byteLength: fileStat.size }
335+
}
336+
337+
const buffer = await readFile(filePath)
338+
const ext = extname(filePath).toLowerCase()
339+
340+
// Determine MIME type
341+
const mimeMap: Record<string, string> = {
342+
".png": "image/png",
343+
".jpg": "image/jpeg",
344+
".jpeg": "image/jpeg",
345+
".gif": "image/gif",
346+
".svg": "image/svg+xml",
347+
".webp": "image/webp",
348+
".ico": "image/x-icon",
349+
".bmp": "image/bmp",
350+
}
351+
const mimeType = mimeMap[ext] || "application/octet-stream"
352+
353+
return {
354+
ok: true as const,
355+
data: buffer.toString("base64"),
356+
mimeType,
357+
byteLength: fileStat.size,
358+
}
359+
} catch (error) {
360+
const msg = error instanceof Error ? error.message : "Unknown error"
361+
if (msg.includes("ENOENT") || msg.includes("no such file")) {
362+
return { ok: false as const, reason: "not-found" as const, byteLength: 0 }
363+
}
364+
throw new Error(`Failed to read binary file: ${msg}`)
365+
}
366+
}),
367+
368+
/**
369+
* Watch for file changes in a project directory
370+
* Emits events when files are modified
371+
*/
372+
watchChanges: publicProcedure
373+
.input(z.object({ projectPath: z.string() }))
374+
.subscription(({ input }) => {
375+
return observable<{ filename: string; eventType: string }>((emit) => {
376+
const watcher = watch(input.projectPath, { recursive: true }, (eventType, filename) => {
377+
if (filename) {
378+
emit.next({ filename, eventType })
379+
}
380+
})
381+
382+
return () => {
383+
watcher.close()
384+
}
385+
})
386+
}),
387+
283388
/**
284389
* Write pasted text to a file in the session's pasted directory
285390
* Used for large text pastes that shouldn't be embedded inline
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Component, type ReactNode } from "react"
2+
import { AlertCircle } from "lucide-react"
3+
import { Button } from "./button"
4+
5+
interface ErrorBoundaryProps {
6+
children: ReactNode
7+
viewerType?: string
8+
onReset?: () => void
9+
}
10+
11+
interface ErrorBoundaryState {
12+
hasError: boolean
13+
error: Error | null
14+
}
15+
16+
export class ViewerErrorBoundary extends Component<
17+
ErrorBoundaryProps,
18+
ErrorBoundaryState
19+
> {
20+
constructor(props: ErrorBoundaryProps) {
21+
super(props)
22+
this.state = { hasError: false, error: null }
23+
}
24+
25+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
26+
return { hasError: true, error }
27+
}
28+
29+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
30+
console.error(
31+
`[ViewerErrorBoundary] ${this.props.viewerType || "viewer"} crashed:`,
32+
error,
33+
errorInfo,
34+
)
35+
}
36+
37+
handleReset = () => {
38+
this.setState({ hasError: false, error: null })
39+
this.props.onReset?.()
40+
}
41+
42+
render() {
43+
if (this.state.hasError) {
44+
return (
45+
<div className="flex flex-col items-center justify-center h-full gap-3 p-4 text-center">
46+
<AlertCircle className="h-10 w-10 text-muted-foreground" />
47+
<p className="font-medium text-foreground">
48+
Failed to render {this.props.viewerType || "file"}
49+
</p>
50+
<p className="text-sm text-muted-foreground max-w-[300px]">
51+
{this.state.error?.message || "An unexpected error occurred."}
52+
</p>
53+
<Button variant="outline" size="sm" onClick={this.handleReset}>
54+
Try again
55+
</Button>
56+
</div>
57+
)
58+
}
59+
60+
return this.props.children
61+
}
62+
}

src/renderer/components/ui/icons.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5681,6 +5681,26 @@ export function IconFullPage(props: IconProps) {
56815681
)
56825682
}
56835683

5684+
export function IconBottomPanel(props: IconProps) {
5685+
return (
5686+
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" {...props}>
5687+
<path fillRule="evenodd" clipRule="evenodd" d="M8.72505 3H14.2749C15.4129 2.99999 16.3307 2.99998 17.074 3.06071C17.8394 3.12324 18.5116 3.25534 19.1335 3.57222C20.1213 4.07555 20.9245 4.8787 21.4278 5.86655C21.7446 6.48846 21.8767 7.16066 21.9393 7.92595C22 8.66921 22 9.58706 22 10.7251V14.1749C22 15.3129 22 16.2308 21.9393 16.974C21.8767 17.7394 21.7446 18.4116 21.4278 19.0335C20.9245 20.0213 20.1213 20.8245 19.1335 21.3277C18.5116 21.6446 17.8394 21.7767 17.074 21.8393C16.3308 21.9 15.4129 21.9 14.2749 21.9H8.72503C7.58706 21.9 6.66921 21.9 5.92595 21.8393C5.16066 21.7767 4.48846 21.6446 3.86655 21.3277C2.8787 20.8245 2.07555 20.0213 1.57222 19.0335C1.25534 18.4116 1.12324 17.7394 1.06071 16.974C0.999979 16.2307 0.99999 15.3129 1 14.1749V10.7251C0.99999 9.58708 0.999979 8.66922 1.06071 7.92595C1.12324 7.16066 1.25534 6.48846 1.57222 5.86655C2.07555 4.8787 2.8787 4.07555 3.86655 3.57222C4.48846 3.25534 5.16066 3.12324 5.92595 3.06071C6.66922 2.99998 7.58708 2.99999 8.72505 3ZM6.09695 5.15374C5.46152 5.20565 5.09645 5.30243 4.81993 5.44333C4.22722 5.74533 3.74533 6.22722 3.44333 6.81993C3.30243 7.09645 3.20565 7.46152 3.15374 8.09695C3.10082 8.74463 3.1 9.57656 3.1 10.77V14.13C3.1 15.3234 3.10082 16.1553 3.15374 16.8031C3.20565 17.4384 3.30243 17.8035 3.44333 18.0801C3.74533 18.6728 4.22722 19.1547 4.81993 19.4566C5.09645 19.5976 5.46152 19.6944 6.09695 19.7462C6.74464 19.7992 7.57656 19.8 8.77 19.8H14.23C15.4234 19.8 16.2553 19.7992 16.9031 19.7462C17.5384 19.6944 17.9035 19.5976 18.1801 19.4566C18.7728 19.1547 19.2547 18.6728 19.5567 18.0801C19.6976 17.8035 19.7944 17.4384 19.8462 16.8031C19.8992 16.1553 19.9 15.3234 19.9 14.13V10.77C19.9 9.57656 19.8992 8.74463 19.8462 8.09695C19.7944 7.46152 19.6976 7.09645 19.5567 6.81993C19.2547 6.22722 18.7728 5.74533 18.1801 5.44333C17.9035 5.30243 17.5384 5.20565 16.9031 5.15374C16.2553 5.10082 15.4234 5.1 14.23 5.1H8.77C7.57656 5.1 6.74464 5.10082 6.09695 5.15374Z" />
5688+
<path d="M16.7783 11.501C17.3447 11.5164 17.7987 11.9797 17.7988 12.5498V16.6504C17.7987 17.2302 17.3289 17.7002 16.749 17.7002C16.706 17.7002 16.6637 17.6964 16.6221 17.6914H6.65625C6.6131 17.6968 6.56903 17.7002 6.52441 17.7002C5.94462 17.7001 5.47466 17.2302 5.47461 16.6504V12.5498C5.47471 11.9868 5.91799 11.5281 6.47461 11.502V11.5H16.7783V11.501Z" />
5689+
</svg>
5690+
)
5691+
}
5692+
5693+
export function IconLineNumbers(props: IconProps) {
5694+
return (
5695+
<svg viewBox="0 0 24 24" fill="none" width="24" height="24" {...props}>
5696+
<path d="M12 17H20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
5697+
<path d="M12 7H20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
5698+
<path d="M6 9.5V4.5L4 5.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
5699+
<path d="M4.25 15C4.25 15 4.9 14.5 5.61102 14.5C6.37813 14.5 7 15.1219 7 15.889C7 17.6885 4 18 4 19.5H7.25" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
5700+
</svg>
5701+
)
5702+
}
5703+
56845704
export function AIPenIcon(props: IconProps) {
56855705
return (
56865706
<svg

0 commit comments

Comments
 (0)