Skip to content

Commit 57d5fd8

Browse files
committed
Release v0.0.58
## Features - **Context Menu for Images** — Copy and save options added to fullscreen image viewer - **Graduated from Beta** — Rollback, Kanban, and Tasks features now available to all users - **Version Tags in History** — Show version tags on commits in history view ## Improvements & Fixes - **Optimized Archive Popover** — Improved archive popover with UnarchiveIcon - **Web Search Simplification** — Simplified web search results to single-line without icons - **Pasted Text Label** — Show "Using pasted text" label instead of "selected text" for pasted content - **Theme-Consistent Toasts** — Ensure toasts follow user-selected theme colors - **Auto-Collapse Sub-Agent** — Auto-collapse sub-agent tool when task completes - **Auto-Scroll on Send** — Scroll to bottom when queued message is auto-sent - **Thinking Tool UX** — Auto-expand/collapse thinking tool and fix exploring group collapse ## Downloads - **macOS ARM64 (Apple Silicon)**: Download the `-arm64.dmg` file - **macOS Intel**: Download the `.dmg` file (without arm64) Auto-updates are enabled. Existing users will be notified automatically.
1 parent 783fe43 commit 57d5fd8

29 files changed

+817
-310
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.57",
3+
"version": "0.0.58",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { and, desc, eq, inArray, isNotNull, isNull } from "drizzle-orm"
1+
import { and, desc, eq, inArray, isNotNull, isNull, sql } from "drizzle-orm"
22
import { BrowserWindow } from "electron"
33
import * as fs from "fs/promises"
44
import * as path from "path"
@@ -1453,14 +1453,20 @@ export const chatsRouter = router({
14531453

14541454
if (input.chatIds && input.chatIds.length > 0) {
14551455
// Archive mode: query all sub-chats for given chat IDs
1456+
// Pre-filter with LIKE to skip sub-chats without file edits (avoids loading/parsing large JSON)
14561457
allChats = db
14571458
.select({
14581459
chatId: subChats.chatId,
14591460
subChatId: subChats.id,
14601461
messages: subChats.messages,
14611462
})
14621463
.from(subChats)
1463-
.where(inArray(subChats.chatId, input.chatIds))
1464+
.where(
1465+
and(
1466+
inArray(subChats.chatId, input.chatIds),
1467+
sql`(${subChats.messages} LIKE '%tool-Edit%' OR ${subChats.messages} LIKE '%tool-Write%')`
1468+
)
1469+
)
14641470
.all()
14651471
} else {
14661472
// Main sidebar mode: query specific sub-chats

src/main/windows/main.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
clipboard,
88
session,
99
nativeImage,
10+
dialog,
1011
} from "electron"
1112
import { join } from "path"
1213
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs"
@@ -245,6 +246,43 @@ function registerIpcHandlers(): void {
245246
)
246247
ipcMain.handle("clipboard:read", () => clipboard.readText())
247248

249+
// Save file with native dialog
250+
ipcMain.handle(
251+
"dialog:save-file",
252+
async (
253+
event,
254+
options: { base64Data: string; filename: string; filters?: { name: string; extensions: string[] }[] },
255+
) => {
256+
const win = getWindowFromEvent(event)
257+
if (!win) return { success: false }
258+
259+
// Ensure window is focused before showing dialog (required on macOS)
260+
if (!win.isFocused()) {
261+
win.focus()
262+
await new Promise((resolve) => setTimeout(resolve, 100))
263+
}
264+
265+
const result = await dialog.showSaveDialog(win, {
266+
defaultPath: options.filename,
267+
filters: options.filters || [
268+
{ name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "gif"] },
269+
{ name: "All Files", extensions: ["*"] },
270+
],
271+
})
272+
273+
if (result.canceled || !result.filePath) return { success: false }
274+
275+
try {
276+
const buffer = Buffer.from(options.base64Data, "base64")
277+
writeFileSync(result.filePath, buffer)
278+
return { success: true, filePath: result.filePath }
279+
} catch (err) {
280+
console.error("[dialog:save-file] Failed to write file:", err)
281+
return { success: false }
282+
}
283+
},
284+
)
285+
248286
// Auth IPC handlers
249287
const validateSender = (event: Electron.IpcMainInvokeEvent): boolean => {
250288
const senderUrl = event.sender.getURL()

src/preload/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ contextBridge.exposeInMainWorld("desktopApi", {
131131
clipboardWrite: (text: string) => ipcRenderer.invoke("clipboard:write", text),
132132
clipboardRead: () => ipcRenderer.invoke("clipboard:read"),
133133

134+
// Save file with native dialog
135+
saveFile: (options: { base64Data: string; filename: string; filters?: { name: string; extensions: string[] }[] }) =>
136+
ipcRenderer.invoke("dialog:save-file", options) as Promise<{ success: boolean; filePath?: string }>,
137+
134138
// Auth methods
135139
getUser: () => ipcRenderer.invoke("auth:get-user"),
136140
isAuthenticated: () => ipcRenderer.invoke("auth:is-authenticated"),
@@ -315,6 +319,7 @@ export interface DesktopApi {
315319
getApiBaseUrl: () => Promise<string>
316320
clipboardWrite: (text: string) => Promise<void>
317321
clipboardRead: () => Promise<string>
322+
saveFile: (options: { base64Data: string; filename: string; filters?: { name: string; extensions: string[] }[] }) => Promise<{ success: boolean; filePath?: string }>
318323
// Auth
319324
getUser: () => Promise<{
320325
id: string

src/renderer/components/dialogs/settings-tabs/agents-beta-tab.tsx

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { useQuery } from "@tanstack/react-query"
55
import {
66
autoOfflineModeAtom,
77
betaAutomationsEnabledAtom,
8-
betaKanbanEnabledAtom,
98
betaUpdatesEnabledAtom,
10-
enableTasksAtom,
119
historyEnabledAtom,
1210
selectedOllamaModelAtom,
1311
showOfflineModeFeaturesAtom,
@@ -52,9 +50,7 @@ export function AgentsBetaTab() {
5250
const [showOfflineFeatures, setShowOfflineFeatures] = useAtom(showOfflineModeFeaturesAtom)
5351
const [autoOffline, setAutoOffline] = useAtom(autoOfflineModeAtom)
5452
const [selectedOllamaModel, setSelectedOllamaModel] = useAtom(selectedOllamaModelAtom)
55-
const [kanbanEnabled, setKanbanEnabled] = useAtom(betaKanbanEnabledAtom)
5653
const [automationsEnabled, setAutomationsEnabled] = useAtom(betaAutomationsEnabledAtom)
57-
const [enableTasks, setEnableTasks] = useAtom(enableTasksAtom)
5854
const [betaUpdatesEnabled, setBetaUpdatesEnabled] = useAtom(betaUpdatesEnabledAtom)
5955

6056
// Check subscription to gate automations behind paid plan
@@ -162,22 +158,6 @@ export function AgentsBetaTab() {
162158
/>
163159
</div>
164160

165-
{/* Kanban Board Toggle */}
166-
<div className="flex items-center justify-between p-4 border-t border-border">
167-
<div className="flex flex-col space-y-1">
168-
<span className="text-sm font-medium text-foreground">
169-
Kanban Board
170-
</span>
171-
<span className="text-xs text-muted-foreground">
172-
View workspaces as a Kanban board organized by status.
173-
</span>
174-
</div>
175-
<Switch
176-
checked={kanbanEnabled}
177-
onCheckedChange={setKanbanEnabled}
178-
/>
179-
</div>
180-
181161
{/* Automations & Inbox Toggle */}
182162
<div className="flex items-center justify-between p-4 border-t border-border">
183163
<div className="flex flex-col space-y-1">
@@ -201,21 +181,6 @@ export function AgentsBetaTab() {
201181
/>
202182
</div>
203183

204-
{/* Agent Tasks Toggle */}
205-
<div className="flex items-center justify-between p-4 border-t border-border">
206-
<div className="flex flex-col space-y-1">
207-
<span className="text-sm font-medium text-foreground">
208-
Agent Tasks
209-
</span>
210-
<span className="text-xs text-muted-foreground">
211-
Enable Task instead of legacy Todo system.
212-
</span>
213-
</div>
214-
<Switch
215-
checked={enableTasks}
216-
onCheckedChange={setEnableTasks}
217-
/>
218-
</div>
219184
</div>
220185

221186
{/* Offline Mode Settings - only show when feature is enabled */}

src/renderer/components/ui/icons.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,28 @@ export function IconAlignEnd(props: IconProps) {
695695
)
696696
}
697697

698+
export function UnarchiveIcon(props: IconProps) {
699+
return (
700+
<svg
701+
viewBox="0 0 24 24"
702+
width="16"
703+
height="16"
704+
fill="none"
705+
stroke="currentColor"
706+
strokeWidth="2"
707+
strokeLinecap="round"
708+
strokeLinejoin="round"
709+
{...props}
710+
>
711+
<g transform="scale(1.1) translate(-1.1 -1.1)">
712+
<path d="M18 20C19.1046 20 20 19.1046 20 18V7H4V18C4 19.1046 4.89543 20 6 20H18Z" />
713+
<path d="M20 7L15 3M4 7L9 3" />
714+
<path d="M10 11H14" />
715+
</g>
716+
</svg>
717+
)
718+
}
719+
698720
// Text alignment icons (5 options)
699721
export function IconTextUndo(props: IconProps) {
700722
return (

src/renderer/features/agents/components/queue-processor.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ export function QueueProcessor() {
119119
)
120120
}
121121

122+
// Signal active-chat to scroll to bottom BEFORE sending so that
123+
// shouldAutoScrollRef is true for the entire streaming duration.
124+
// (sendMessage awaits the full stream, so placing this after would
125+
// only scroll after the response is complete.)
126+
useMessageQueueStore.getState().triggerQueueSent(subChatId)
127+
122128
// Send message using Chat's sendMessage method
123129
await chat.sendMessage({ role: "user", parts })
124130

src/renderer/features/agents/hooks/use-pasted-text-files.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface UsePastedTextFilesReturn {
1616
removePastedText: (id: string) => void
1717
clearPastedTexts: () => void
1818
pastedTextsRef: React.RefObject<PastedTextFile[]>
19+
setPastedTextsFromDraft: (texts: PastedTextFile[]) => void
1920
}
2021

2122
export function usePastedTextFiles(subChatId: string): UsePastedTextFilesReturn {
@@ -62,11 +63,18 @@ export function usePastedTextFiles(subChatId: string): UsePastedTextFilesReturn
6263
setPastedTexts([])
6364
}, [])
6465

66+
// Direct state setter for restoring from draft/rollback
67+
const setPastedTextsFromDraft = useCallback((texts: PastedTextFile[]) => {
68+
setPastedTexts(texts)
69+
pastedTextsRef.current = texts
70+
}, [])
71+
6572
return {
6673
pastedTexts,
6774
addPastedText,
6875
removePastedText,
6976
clearPastedTexts,
7077
pastedTextsRef,
78+
setPastedTextsFromDraft,
7179
}
7280
}

0 commit comments

Comments
 (0)