Skip to content

Commit 7cf0dc0

Browse files
committed
Release v0.0.40
## What's New ### Features - **Details Sidebar** — New unified Details sidebar with configurable widgets
1 parent b4f9086 commit 7cf0dc0

29 files changed

+3588
-94
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.39",
3+
"version": "0.0.40",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { z } from "zod"
22
import { router, publicProcedure } from "../index"
3-
import { readdir, stat, readFile } from "node:fs/promises"
3+
import { readdir, stat, readFile, writeFile, mkdir } from "node:fs/promises"
44
import { join, relative, basename } from "node:path"
5+
import { app } from "electron"
56

67
// Directories to ignore when scanning
78
const IGNORED_DIRS = new Set([
@@ -278,4 +279,40 @@ export const filesRouter = router({
278279
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`)
279280
}
280281
}),
282+
283+
/**
284+
* Write pasted text to a file in the session's pasted directory
285+
* Used for large text pastes that shouldn't be embedded inline
286+
*/
287+
writePastedText: publicProcedure
288+
.input(
289+
z.object({
290+
subChatId: z.string(),
291+
text: z.string(),
292+
filename: z.string().optional(),
293+
})
294+
)
295+
.mutation(async ({ input }) => {
296+
const { subChatId, text, filename } = input
297+
298+
// Create pasted directory in session folder
299+
const sessionDir = join(app.getPath("userData"), "claude-sessions", subChatId)
300+
const pastedDir = join(sessionDir, "pasted")
301+
await mkdir(pastedDir, { recursive: true })
302+
303+
// Generate filename with timestamp
304+
const finalFilename = filename || `pasted_${Date.now()}.txt`
305+
const filePath = join(pastedDir, finalFilename)
306+
307+
// Write file
308+
await writeFile(filePath, text, "utf-8")
309+
310+
console.log(`[files] Wrote pasted text to ${filePath} (${text.length} bytes)`)
311+
312+
return {
313+
filePath,
314+
filename: finalFilename,
315+
size: text.length,
316+
}
317+
}),
281318
})

src/renderer/components/ui/icons.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,31 @@ export function ClockIcon(props: IconProps) {
193193
)
194194
}
195195

196+
export function GitBranchFilledIcon(props: IconProps) {
197+
return (
198+
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" {...props}>
199+
<path d="M6.5 3C4.84315 3 3.5 4.34315 3.5 6C3.5 7.30622 4.33481 8.41746 5.5 8.82929V15.1707C4.33481 15.5825 3.5 16.6938 3.5 18C3.5 19.6569 4.84315 21 6.5 21C8.15685 21 9.5 19.6569 9.5 18C9.5 16.6938 8.66519 15.5825 7.5 15.1707V14C7.5 13.4477 7.94772 13 8.5 13H15.5C17.1569 13 18.5 11.6569 18.5 10V8.82929C19.6652 8.41746 20.5 7.30622 20.5 6C20.5 4.34315 19.1569 3 17.5 3C15.8431 3 14.5 4.34315 14.5 6C14.5 7.30622 15.3348 8.41746 16.5 8.82929V10C16.5 10.5523 16.0523 11 15.5 11H8.5C8.14936 11 7.81278 11.0602 7.5 11.1707V8.82929C8.66519 8.41746 9.5 7.30622 9.5 6C9.5 4.34315 8.15685 3 6.5 3Z" />
200+
</svg>
201+
)
202+
}
203+
204+
export function FolderFilledIcon(props: IconProps) {
205+
return (
206+
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" {...props}>
207+
<path d="M5 3C3.34315 3 2 4.34315 2 6V17C2 18.6569 3.34315 20 5 20H19C20.6569 20 22 18.6569 22 17V9C22 7.34315 20.6569 6 19 6L12.5352 6L11.4258 4.3359C10.8694 3.5013 9.93269 3 8.92963 3H5Z" />
208+
</svg>
209+
)
210+
}
211+
212+
export function GitPullRequestFilledIcon(props: IconProps) {
213+
return (
214+
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" {...props}>
215+
<path d="M6 3C4.34315 3 3 4.34315 3 6C3 7.30622 3.83481 8.41746 5 8.82929V15.1707C3.83481 15.5825 3 16.6938 3 18C3 19.6569 4.34315 21 6 21C7.65685 21 9 19.6569 9 18C9 16.6938 8.16519 15.5825 7 15.1707V8.82929C8.16519 8.41746 9 7.30622 9 6C9 4.34315 7.65685 3 6 3Z" />
216+
<path d="M14.4142 5L14.7071 4.70711C15.0976 4.31658 15.0976 3.68342 14.7071 3.29289C14.3166 2.90237 13.6834 2.90237 13.2929 3.29289L11.2929 5.29289C10.9024 5.68342 10.9024 6.31658 11.2929 6.70711L13.2929 8.70711C13.6834 9.09763 14.3166 9.09763 14.7071 8.70711C15.0976 8.31658 15.0976 7.68342 14.7071 7.29289L14.4142 7H16C16.5523 7 17 7.44772 17 8V15.1707C15.8348 15.5825 15 16.6938 15 18C15 19.6569 16.3431 21 18 21C19.6569 21 21 19.6569 21 18C21 16.6938 20.1652 15.5825 19 15.1707V8C19 6.34315 17.6569 5 16 5H14.4142Z" />
217+
</svg>
218+
)
219+
}
220+
196221
export function IconChatBubble(props: IconProps) {
197222
return (
198223
<svg
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useState, useCallback, useRef } from "react"
2+
import { trpc } from "../../../lib/trpc"
3+
import { toast } from "sonner"
4+
5+
export interface PastedTextFile {
6+
id: string
7+
filePath: string
8+
filename: string
9+
size: number
10+
preview: string
11+
createdAt: Date
12+
}
13+
14+
export interface UsePastedTextFilesReturn {
15+
pastedTexts: PastedTextFile[]
16+
addPastedText: (text: string) => Promise<void>
17+
removePastedText: (id: string) => void
18+
clearPastedTexts: () => void
19+
pastedTextsRef: React.RefObject<PastedTextFile[]>
20+
}
21+
22+
export function usePastedTextFiles(subChatId: string): UsePastedTextFilesReturn {
23+
const [pastedTexts, setPastedTexts] = useState<PastedTextFile[]>([])
24+
const pastedTextsRef = useRef<PastedTextFile[]>([])
25+
26+
// Keep ref in sync with state
27+
pastedTextsRef.current = pastedTexts
28+
29+
const writePastedTextMutation = trpc.files.writePastedText.useMutation()
30+
31+
const addPastedText = useCallback(
32+
async (text: string) => {
33+
try {
34+
const result = await writePastedTextMutation.mutateAsync({
35+
subChatId,
36+
text,
37+
})
38+
39+
// Create preview from first 50 chars, replace newlines with spaces
40+
const preview = text.slice(0, 50).replace(/\n/g, " ")
41+
const newPasted: PastedTextFile = {
42+
id: `pasted_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
43+
filePath: result.filePath,
44+
filename: result.filename,
45+
size: result.size,
46+
preview: preview.length < text.length ? `${preview}...` : preview,
47+
createdAt: new Date(),
48+
}
49+
50+
setPastedTexts((prev) => [...prev, newPasted])
51+
52+
const sizeKB = Math.round(result.size / 1024)
53+
toast.info(`Text saved as file (${sizeKB}KB)`, {
54+
description: "Large text will be sent as a file attachment.",
55+
})
56+
} catch (error) {
57+
console.error("[usePastedTextFiles] Failed to write:", error)
58+
toast.error("Failed to save pasted text")
59+
}
60+
},
61+
[subChatId, writePastedTextMutation]
62+
)
63+
64+
const removePastedText = useCallback((id: string) => {
65+
setPastedTexts((prev) => prev.filter((p) => p.id !== id))
66+
}, [])
67+
68+
const clearPastedTexts = useCallback(() => {
69+
setPastedTexts([])
70+
}, [])
71+
72+
return {
73+
pastedTexts,
74+
addPastedText,
75+
removePastedText,
76+
clearPastedTexts,
77+
pastedTextsRef,
78+
}
79+
}

0 commit comments

Comments
 (0)