Skip to content

Commit a8c54fa

Browse files
committed
Release v0.0.17
1 parent 91fffda commit a8c54fa

31 files changed

Lines changed: 1695 additions & 656 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Best UI for Claude Code with local and remote agent execution.
66

77
By [21st.dev](https://21st.dev) team
88

9+
> **Note:** Currently tested on macOS and Linux. Windows support is experimental and may have issues.
10+
911
## Features
1012

1113
- **Plan & Agent Modes** - Read-only analysis or full code execution permissions

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.16",
3+
"version": "0.0.17",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": "21st.dev",

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

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,4 +707,216 @@ export const chatsRouter = router({
707707
)
708708
}
709709
}),
710+
711+
/**
712+
* Get file change stats for all workspaces
713+
* Parses messages from all sub-chats and aggregates Edit/Write tool calls
714+
* If openSubChatIds provided, only count stats from those sub-chats
715+
*/
716+
getFileStats: publicProcedure
717+
.input(z.object({ openSubChatIds: z.array(z.string()).optional() }).optional())
718+
.query(({ input }) => {
719+
const db = getDatabase()
720+
const openSubChatIdsSet = input?.openSubChatIds ? new Set(input.openSubChatIds) : null
721+
722+
// Get all non-archived chats with their sub-chats
723+
const allChats = db
724+
.select({
725+
chatId: chats.id,
726+
subChatId: subChats.id,
727+
messages: subChats.messages,
728+
})
729+
.from(chats)
730+
.leftJoin(subChats, eq(subChats.chatId, chats.id))
731+
.where(isNull(chats.archivedAt))
732+
.all()
733+
// Filter by open sub-chats if provided
734+
.filter(row => !openSubChatIdsSet || !row.subChatId || openSubChatIdsSet.has(row.subChatId))
735+
736+
// Aggregate stats per workspace (chatId)
737+
const statsMap = new Map<
738+
string,
739+
{ additions: number; deletions: number; fileCount: number }
740+
>()
741+
742+
for (const row of allChats) {
743+
if (!row.messages || !row.chatId) continue
744+
745+
try {
746+
const messages = JSON.parse(row.messages) as Array<{
747+
role: string
748+
parts?: Array<{
749+
type: string
750+
input?: {
751+
file_path?: string
752+
old_string?: string
753+
new_string?: string
754+
content?: string
755+
}
756+
}>
757+
}>
758+
759+
// Track file states for this sub-chat
760+
const fileStates = new Map<
761+
string,
762+
{ originalContent: string | null; currentContent: string }
763+
>()
764+
765+
for (const msg of messages) {
766+
if (msg.role !== "assistant") continue
767+
for (const part of msg.parts || []) {
768+
if (part.type === "tool-Edit" || part.type === "tool-Write") {
769+
const filePath = part.input?.file_path
770+
if (!filePath) continue
771+
// Skip session files
772+
if (
773+
filePath.includes("claude-sessions") ||
774+
filePath.includes("Application Support")
775+
)
776+
continue
777+
778+
const oldString = part.input?.old_string || ""
779+
const newString =
780+
part.input?.new_string || part.input?.content || ""
781+
782+
const existing = fileStates.get(filePath)
783+
if (existing) {
784+
existing.currentContent = newString
785+
} else {
786+
fileStates.set(filePath, {
787+
originalContent: part.type === "tool-Write" ? null : oldString,
788+
currentContent: newString,
789+
})
790+
}
791+
}
792+
}
793+
}
794+
795+
// Calculate stats for this sub-chat and add to workspace total
796+
let subChatAdditions = 0
797+
let subChatDeletions = 0
798+
let subChatFileCount = 0
799+
800+
for (const [, state] of fileStates) {
801+
const original = state.originalContent || ""
802+
if (original === state.currentContent) continue
803+
804+
const oldLines = original ? original.split("\n").length : 0
805+
const newLines = state.currentContent
806+
? state.currentContent.split("\n").length
807+
: 0
808+
809+
if (!original) {
810+
// New file
811+
subChatAdditions += newLines
812+
} else {
813+
subChatAdditions += newLines
814+
subChatDeletions += oldLines
815+
}
816+
subChatFileCount += 1
817+
}
818+
819+
// Add to workspace total
820+
const existing = statsMap.get(row.chatId) || {
821+
additions: 0,
822+
deletions: 0,
823+
fileCount: 0,
824+
}
825+
existing.additions += subChatAdditions
826+
existing.deletions += subChatDeletions
827+
existing.fileCount += subChatFileCount
828+
statsMap.set(row.chatId, existing)
829+
} catch {
830+
// Skip invalid JSON
831+
}
832+
}
833+
834+
// Convert to array for easier consumption
835+
return Array.from(statsMap.entries()).map(([chatId, stats]) => ({
836+
chatId,
837+
...stats,
838+
}))
839+
}),
840+
841+
/**
842+
* Get sub-chats with pending plan approvals
843+
* Parses messages to find ExitPlanMode tool calls without subsequent "Implement plan" user message
844+
* Logic must match active-chat.tsx hasUnapprovedPlan
845+
* If openSubChatIds provided, only check those sub-chats
846+
*/
847+
getPendingPlanApprovals: publicProcedure
848+
.input(z.object({ openSubChatIds: z.array(z.string()).optional() }).optional())
849+
.query(({ input }) => {
850+
const db = getDatabase()
851+
const openSubChatIdsSet = input?.openSubChatIds ? new Set(input.openSubChatIds) : null
852+
853+
// Get all non-archived chats with their sub-chats
854+
const allSubChats = db
855+
.select({
856+
chatId: chats.id,
857+
subChatId: subChats.id,
858+
messages: subChats.messages,
859+
})
860+
.from(chats)
861+
.leftJoin(subChats, eq(subChats.chatId, chats.id))
862+
.where(isNull(chats.archivedAt))
863+
.all()
864+
// Filter by open sub-chats if provided
865+
.filter(row => !openSubChatIdsSet || !row.subChatId || openSubChatIdsSet.has(row.subChatId))
866+
867+
const pendingApprovals: Array<{ subChatId: string; chatId: string }> = []
868+
869+
for (const row of allSubChats) {
870+
if (!row.messages || !row.subChatId || !row.chatId) continue
871+
872+
try {
873+
const messages = JSON.parse(row.messages) as Array<{
874+
role: string
875+
content?: string
876+
parts?: Array<{
877+
type: string
878+
text?: string
879+
}>
880+
}>
881+
882+
// Traverse messages from end to find unapproved ExitPlanMode
883+
// Logic matches active-chat.tsx hasUnapprovedPlan
884+
let hasUnapprovedPlan = false
885+
886+
for (let i = messages.length - 1; i >= 0; i--) {
887+
const msg = messages[i]
888+
if (!msg) continue
889+
890+
// If user message says "Implement plan" (exact match), plan is already approved
891+
if (msg.role === "user") {
892+
const textPart = msg.parts?.find((p) => p.type === "text")
893+
const text = textPart?.text || ""
894+
if (text.trim().toLowerCase() === "implement plan") {
895+
break // Plan was approved, stop searching
896+
}
897+
}
898+
899+
// If assistant message with ExitPlanMode, we found an unapproved plan
900+
if (msg.role === "assistant" && msg.parts) {
901+
const exitPlanPart = msg.parts.find((p) => p.type === "tool-ExitPlanMode")
902+
if (exitPlanPart) {
903+
hasUnapprovedPlan = true
904+
break
905+
}
906+
}
907+
}
908+
909+
if (hasUnapprovedPlan) {
910+
pendingApprovals.push({
911+
subChatId: row.subChatId,
912+
chatId: row.chatId,
913+
})
914+
}
915+
} catch {
916+
// Skip invalid JSON
917+
}
918+
}
919+
920+
return pendingApprovals
921+
}),
710922
})

src/renderer/components/chat-markdown-renderer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ const sizeStyles: Record<
212212
"bg-foreground/[0.06] dark:bg-foreground/[0.1] font-mono text-[85%] rounded px-[0.4em] py-[0.2em] break-all",
213213
blockquote:
214214
"border-l-2 border-foreground/20 pl-3 text-foreground/70 mb-px text-sm",
215-
hr: "mt-6 mb-4 border-t border-border",
215+
hr: "mt-8 mb-4 border-t border-border",
216216
table: "w-full text-sm",
217217
thead: "border-b border-border",
218218
tbody: "",
@@ -236,7 +236,7 @@ const sizeStyles: Record<
236236
"bg-foreground/[0.06] dark:bg-foreground/[0.1] font-mono text-[85%] rounded px-[0.4em] py-[0.2em] break-all",
237237
blockquote:
238238
"border-l-2 border-foreground/20 pl-4 text-foreground/70 mb-px",
239-
hr: "mt-6 mb-4 border-t border-border",
239+
hr: "mt-8 mb-4 border-t border-border",
240240
table: "w-full text-sm",
241241
thead: "border-b border-border",
242242
tbody: "",
@@ -260,7 +260,7 @@ const sizeStyles: Record<
260260
"bg-foreground/[0.06] dark:bg-foreground/[0.1] font-mono text-[85%] rounded px-[0.4em] py-[0.2em] break-all",
261261
blockquote:
262262
"border-l-2 border-foreground/20 pl-4 text-foreground/70 mb-px",
263-
hr: "mt-6 mb-4 border-t border-border",
263+
hr: "mt-8 mb-4 border-t border-border",
264264
table: "w-full text-sm",
265265
thead: "border-b border-border",
266266
tbody: "",

src/renderer/components/dialogs/agents-settings-dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
EyeOpenFilledIcon,
1111
SlidersFilledIcon,
1212
} from "../../icons"
13-
import { SkillIcon, AgentIcon } from "../ui/icons"
13+
import { SkillIconFilled, CustomAgentIconFilled } from "../ui/icons"
1414
import { AgentsAppearanceTab } from "./settings-tabs/agents-appearance-tab"
1515
import { AgentsProfileTab } from "./settings-tabs/agents-profile-tab"
1616
import { AgentsPreferencesTab } from "./settings-tabs/agents-preferences-tab"
@@ -65,14 +65,14 @@ const ALL_TABS = [
6565
{
6666
id: "skills" as SettingsTab,
6767
label: "Skills",
68-
icon: SkillIcon,
68+
icon: SkillIconFilled,
6969
description: "Custom Claude skills",
7070
beta: true,
7171
},
7272
{
7373
id: "agents" as SettingsTab,
7474
label: "Custom Agents",
75-
icon: AgentIcon,
75+
icon: CustomAgentIconFilled,
7676
description: "Manage custom Claude agents",
7777
beta: true,
7878
},

src/renderer/components/dialogs/agents-shortcuts-dialog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const GENERAL_SHORTCUTS: Shortcut[] = [
6666
{ label: "Show shortcuts", keys: ["?"] },
6767
{ label: "Settings", keys: ["cmd", ","] },
6868
{ label: "Toggle sidebar", keys: ["cmd", "\\"] },
69+
{ label: "Undo archive", keys: ["cmd", "Z"] },
6970
]
7071

7172
// Dynamic shortcuts based on ctrlTabTarget preference

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useTheme } from "next-themes"
22
import { useState, useEffect, useCallback, useMemo } from "react"
3+
import { IconSpinner } from "../../../icons"
34
import { useAtom, useSetAtom } from "jotai"
45
import { motion, AnimatePresence } from "motion/react"
56
import { cn } from "../../../lib/utils"
@@ -291,7 +292,7 @@ export function AgentsAppearanceTab() {
291292
return (
292293
<div className="p-6 space-y-6">
293294
<div className="h-48 flex items-center justify-center">
294-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-foreground" />
295+
<IconSpinner className="h-8 w-8 text-foreground" />
295296
</div>
296297
</div>
297298
)

0 commit comments

Comments
 (0)