Skip to content

Commit 0d773a1

Browse files
committed
Release v0.0.59
## What's New ### Features - Open pushed commits on GitHub - Enable extended thinking by default ### Improvements & Fixes - Fix history view remote link - Handle rebase when resolving commit hash for GitHub URL - Show thinking gradient only when content overflows - Improve thinking tool content visibility - Hide scrollbar in thinking tool during streaming - Display model version separately in model selector - Move rollback button to user message bubble and restore input content ## 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 57d5fd8 commit 0d773a1

File tree

11 files changed

+273
-41
lines changed

11 files changed

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

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,11 +1776,13 @@ ${prompt}
17761776
let policyRetryCount = 0
17771777
let policyRetryNeeded = false
17781778
let messageCount = 0
1779+
let pendingFinishChunk: UIMessageChunk | null = null
17791780

17801781
// eslint-disable-next-line no-constant-condition
17811782
while (true) {
17821783
policyRetryNeeded = false
17831784
messageCount = 0
1785+
pendingFinishChunk = null
17841786

17851787
// 5. Run Claude SDK
17861788
let stream
@@ -2088,6 +2090,14 @@ ${prompt}
20882090
}
20892091
}
20902092

2093+
// IMPORTANT: Defer the protocol "finish" chunk until after DB persistence.
2094+
// If we emit finish early, the UI can send the next user message before
2095+
// this assistant message is written, and the next save overwrites it.
2096+
if (chunk.type === "finish") {
2097+
pendingFinishChunk = chunk
2098+
continue
2099+
}
2100+
20912101
// Use safeEmit to prevent throws when observer is closed
20922102
if (!safeEmit(chunk)) {
20932103
// Observer closed (user clicked Stop), break out of loop
@@ -2466,6 +2476,12 @@ ${prompt}
24662476
console.log(
24672477
`[SD] M:END sub=${subId} reason=ok n=${chunkCount} last=${lastChunkType} t=${duration}s`,
24682478
)
2479+
if (pendingFinishChunk) {
2480+
safeEmit(pendingFinishChunk)
2481+
} else {
2482+
// Keep protocol invariant for consumers that wait for finish.
2483+
safeEmit({ type: "finish" } as UIMessageChunk)
2484+
}
24692485
safeComplete()
24702486
} catch (error) {
24712487
const duration = ((Date.now() - streamStart) / 1000).toFixed(1)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ async function getUserPlan(): Promise<{ plan: string; status: string | null } |
9696
}
9797

9898
/**
99-
* Check if user has paid subscription (onecode_pro or onecode_max with active status)
99+
* Check if user has paid subscription (onecode_pro, onecode_max_100, or onecode_max with active status)
100100
*/
101101
async function hasPaidSubscription(): Promise<boolean> {
102102
const planData = await getUserPlan()
103103
if (!planData) return false
104104

105-
const paidPlans = ["onecode_pro", "onecode_max"]
105+
const paidPlans = ["onecode_pro", "onecode_max_100", "onecode_max"]
106106
return paidPlans.includes(planData.plan) && planData.status === "active"
107107
}
108108

src/renderer/features/agents/components/agents-help-popover.tsx

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
11
"use client"
22

3-
import { useState } from "react"
3+
import { useState, useEffect } from "react"
44
import {
55
DropdownMenu,
66
DropdownMenuContent,
77
DropdownMenuItem,
88
DropdownMenuTrigger,
9+
DropdownMenuSeparator,
10+
DropdownMenuLabel,
911
} from "../../../components/ui/dropdown-menu"
12+
import { ArrowUpRight } from "lucide-react"
1013
import { KeyboardIcon } from "../../../components/ui/icons"
1114
import { DiscordIcon } from "../../../icons"
1215
import { useSetAtom } from "jotai"
1316
import { agentsSettingsDialogOpenAtom, agentsSettingsDialogActiveTabAtom } from "../../../lib/atoms"
1417

18+
interface ReleaseHighlight {
19+
version: string
20+
title: string
21+
}
22+
23+
function parseFirstHighlight(content: string): string {
24+
const lines = content.split("\n")
25+
let inFeatures = false
26+
for (const line of lines) {
27+
if (/^###\s+Features/i.test(line)) {
28+
inFeatures = true
29+
continue
30+
}
31+
if (inFeatures && /^###?\s+/.test(line)) break
32+
if (inFeatures) {
33+
const bold = line.match(/^[-*]\s+\*\*(.+?)\*\*/)
34+
if (bold) return bold[1]
35+
const plain = line.match(/^[-*]\s+(.+)/)
36+
if (plain) return plain[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim()
37+
}
38+
}
39+
return "Bug fixes & improvements"
40+
}
41+
1542
interface AgentsHelpPopoverProps {
1643
children: React.ReactNode
1744
open?: boolean
@@ -28,13 +55,48 @@ export function AgentsHelpPopover({
2855
const [internalOpen, setInternalOpen] = useState(false)
2956
const setSettingsDialogOpen = useSetAtom(agentsSettingsDialogOpenAtom)
3057
const setSettingsActiveTab = useSetAtom(agentsSettingsDialogActiveTabAtom)
58+
const [highlights, setHighlights] = useState<ReleaseHighlight[]>([])
3159

32-
// Use controlled state if provided, otherwise use internal state
3360
const open = controlledOpen ?? internalOpen
3461
const setOpen = controlledOnOpenChange ?? setInternalOpen
3562

63+
useEffect(() => {
64+
let cancelled = false
65+
window.desktopApi
66+
.signedFetch("https://21st.dev/api/changelog/desktop?per_page=3")
67+
.then((result) => {
68+
if (cancelled) return
69+
const data = result.data as {
70+
releases?: Array<{ version?: string; content?: string }>
71+
}
72+
if (data?.releases) {
73+
const items: ReleaseHighlight[] = []
74+
for (const release of data.releases) {
75+
if (release.version) {
76+
items.push({ version: release.version, title: parseFirstHighlight(release.content || "") })
77+
}
78+
}
79+
setHighlights(items)
80+
}
81+
})
82+
.catch(() => {})
83+
return () => {
84+
cancelled = true
85+
}
86+
}, [])
87+
3688
const handleCommunityClick = () => {
37-
window.open("https://discord.gg/8ektTZGnj4", "_blank")
89+
window.desktopApi.openExternal("https://discord.gg/8ektTZGnj4")
90+
}
91+
92+
const handleChangelogClick = () => {
93+
window.desktopApi.openExternal("https://1code.dev/agents/changelog")
94+
}
95+
96+
const handleReleaseClick = (version: string) => {
97+
window.desktopApi.openExternal(
98+
`https://1code.dev/agents/changelog#${version}`,
99+
)
38100
}
39101

40102
const handleKeyboardShortcutsClick = () => {
@@ -46,7 +108,7 @@ export function AgentsHelpPopover({
46108
return (
47109
<DropdownMenu open={open} onOpenChange={setOpen}>
48110
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
49-
<DropdownMenuContent side="top" align="start" className="w-36">
111+
<DropdownMenuContent side="top" align="start" className="w-56">
50112
<DropdownMenuItem onClick={handleCommunityClick} className="gap-2">
51113
<DiscordIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
52114
<span className="flex-1">Discord</span>
@@ -61,6 +123,40 @@ export function AgentsHelpPopover({
61123
<span className="flex-1">Shortcuts</span>
62124
</DropdownMenuItem>
63125
)}
126+
127+
{highlights.length > 0 && (
128+
<>
129+
<DropdownMenuSeparator />
130+
<div className="mx-1 px-1.5 pt-1.5 pb-0.5 text-xs text-muted-foreground">
131+
What's new
132+
</div>
133+
{highlights.map((item, i) => (
134+
<DropdownMenuItem
135+
key={item.version}
136+
onClick={() => handleReleaseClick(item.version)}
137+
className="gap-0 items-stretch min-h-0 px-2 py-0"
138+
>
139+
<div className="flex flex-col items-center w-3 shrink-0">
140+
{i === 0 ? <div className="h-[11px]" /> : <div className="w-px h-[11px] border-l border-dashed border-muted-foreground/30" />}
141+
<div className="w-1.5 h-1.5 rounded-full border border-muted-foreground/40 shrink-0" />
142+
<div className="w-px flex-1 border-l border-dashed border-muted-foreground/30" />
143+
</div>
144+
<span className="text-xs text-muted-foreground leading-tight py-1.5 pl-2 line-clamp-2">
145+
{item.title}
146+
</span>
147+
</DropdownMenuItem>
148+
))}
149+
<DropdownMenuItem onClick={handleChangelogClick} className="gap-0 items-stretch min-h-0 px-2 py-0">
150+
<div className="flex flex-col items-center w-3 shrink-0">
151+
<div className="w-px h-[11px] border-l border-dashed border-muted-foreground/30" />
152+
<div className="w-1.5 h-1.5 rounded-full bg-foreground shrink-0" />
153+
<div className="w-px flex-1 border-l border-dashed border-muted-foreground/30" />
154+
</div>
155+
<span className="flex-1 text-xs pl-2 py-1.5">Full changelog</span>
156+
<ArrowUpRight className="h-3 w-3 text-muted-foreground shrink-0 self-center" />
157+
</DropdownMenuItem>
158+
</>
159+
)}
64160
</DropdownMenuContent>
65161
</DropdownMenu>
66162
)

src/renderer/features/agents/main/active-chat.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,12 @@ import {
188188
} from "../search"
189189
import { agentChatStore } from "../stores/agent-chat-store"
190190
import { EMPTY_QUEUE, useMessageQueueStore } from "../stores/message-queue-store"
191-
import { clearSubChatCaches, isRollingBackAtom, syncMessagesWithStatusAtom } from "../stores/message-store"
191+
import {
192+
clearSubChatCaches,
193+
findRollbackTargetSdkUuidForUserIndex,
194+
isRollingBackAtom,
195+
syncMessagesWithStatusAtom
196+
} from "../stores/message-store"
192197
import { useStreamingStatusStore } from "../stores/streaming-status-store"
193198
import {
194199
useAgentSubChatStore,
@@ -3131,23 +3136,14 @@ const ChatViewInner = memo(function ChatViewInner({
31313136
return
31323137
}
31333138

3134-
// Find the last assistant message BEFORE this user message
3135-
let targetAssistantMsg: (typeof messages)[0] | null = null
3136-
for (let i = userMsgIndex - 1; i >= 0; i--) {
3137-
if (messages[i].role === "assistant") {
3138-
targetAssistantMsg = messages[i]
3139-
break
3140-
}
3141-
}
3142-
3143-
if (!targetAssistantMsg) {
3144-
toast.error("Cannot rollback: no previous assistant message found")
3145-
return
3146-
}
3139+
const sdkUuid = findRollbackTargetSdkUuidForUserIndex(
3140+
userMsgIndex,
3141+
messages.length,
3142+
(index) => messages[index] as any,
3143+
)
31473144

3148-
const sdkUuid = (targetAssistantMsg.metadata as any)?.sdkMessageUuid
31493145
if (!sdkUuid) {
3150-
toast.error("Cannot rollback: message has no SDK UUID")
3146+
toast.error("Cannot rollback: this turn is not rollbackable")
31513147
return
31523148
}
31533149

src/renderer/features/agents/main/isolated-message-group.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
messageAtomFamily,
77
assistantIdsForUserMsgAtomFamily,
88
isLastUserMessageAtomFamily,
9-
isFirstUserMessageAtomFamily,
9+
rollbackTargetSdkUuidForUserMsgAtomFamily,
1010
isStreamingAtom,
1111
isRollingBackAtom,
1212
} from "../stores/message-store"
@@ -101,12 +101,12 @@ export const IsolatedMessageGroup = memo(function IsolatedMessageGroup({
101101
const userMsg = useAtomValue(messageAtomFamily(userMsgId))
102102
const assistantIds = useAtomValue(assistantIdsForUserMsgAtomFamily(userMsgId))
103103
const isLastGroup = useAtomValue(isLastUserMessageAtomFamily(userMsgId))
104-
const isFirstUserMessage = useAtomValue(isFirstUserMessageAtomFamily(userMsgId))
104+
const rollbackTargetSdkUuid = useAtomValue(rollbackTargetSdkUuidForUserMsgAtomFamily(userMsgId))
105105
const isStreaming = useAtomValue(isStreamingAtom)
106106
const isRollingBack = useAtomValue(isRollingBackAtom)
107107

108-
// Show rollback button on non-first user messages (first has no preceding assistant to roll back to)
109-
const canRollback = onRollback && !isFirstUserMessage && !isStreaming
108+
// Show rollback button only when this user turn has a valid rollback target.
109+
const canRollback = onRollback && !!rollbackTargetSdkUuid && !isStreaming
110110

111111
// Extract user message content
112112
// Note: file-content parts are hidden from UI but sent to agent

src/renderer/features/agents/stores/message-store.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,78 @@ export const isFirstUserMessageAtomFamily = atomFamily((userMsgId: string) =>
358358
})
359359
)
360360

361+
type RollbackLookupMessage = {
362+
role: "user" | "assistant" | "system"
363+
metadata?: any
364+
parts?: MessagePart[]
365+
}
366+
367+
function hasCompactToolUsePart(parts?: MessagePart[]): boolean {
368+
return !!parts?.some((part) => part.type === "tool-Compact")
369+
}
370+
371+
// Shared rollback target lookup used by both UI visibility and rollback action.
372+
export function findRollbackTargetSdkUuidForUserIndex(
373+
userMsgIndex: number,
374+
totalMessageCount: number,
375+
getMessageAt: (index: number) => RollbackLookupMessage | null | undefined,
376+
): string | null {
377+
if (userMsgIndex <= 0 || totalMessageCount <= 0) return null
378+
379+
// 1) Pick the first assistant before this user message.
380+
let targetAssistantIndex = -1
381+
let targetAssistantMessage: RollbackLookupMessage | null | undefined = null
382+
for (let i = userMsgIndex - 1; i >= 0; i--) {
383+
const message = getMessageAt(i)
384+
if (!message || message.role !== "assistant") continue
385+
targetAssistantIndex = i
386+
targetAssistantMessage = message
387+
break
388+
}
389+
390+
if (targetAssistantIndex === -1 || !targetAssistantMessage) return null
391+
392+
// 2) Any compact after that assistant (up to the end of the dialog) means
393+
// this assistant is already behind compact and cannot be a rollback target.
394+
for (let i = targetAssistantIndex; i < totalMessageCount; i++) {
395+
const message = getMessageAt(i)
396+
if (!message || message.role !== "assistant") continue
397+
if (hasCompactToolUsePart(message.parts)) {
398+
return null
399+
}
400+
}
401+
402+
// 3) No compact after target assistant: allow rollback only if target has SDK UUID.
403+
const sdkUuid = (targetAssistantMessage.metadata as any)?.sdkMessageUuid
404+
return typeof sdkUuid === "string" && sdkUuid.length > 0 ? sdkUuid : null
405+
}
406+
407+
// SDK UUID of the assistant message that rollback should target for this user message.
408+
// Returns null when this turn cannot be rolled back.
409+
export const rollbackTargetSdkUuidForUserMsgAtomFamily = atomFamily((userMsgId: string) =>
410+
atom((get) => {
411+
const ids = get(messageIdsAtom)
412+
const roles = get(messageRolesAtom)
413+
const userMsgIndex = ids.indexOf(userMsgId)
414+
415+
if (userMsgIndex <= 0) return null
416+
417+
return findRollbackTargetSdkUuidForUserIndex(userMsgIndex, ids.length, (index) => {
418+
const messageId = ids[index]
419+
if (!messageId) return null
420+
421+
const role = roles.get(messageId)
422+
if (!role) return null
423+
424+
if (role !== "assistant") {
425+
return { role }
426+
}
427+
428+
return get(messageAtomFamily(messageId))
429+
})
430+
})
431+
)
432+
361433
// ============================================================================
362434
// STREAMING STATUS
363435
// ============================================================================

src/renderer/features/agents/ui/agent-thinking-tool.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,15 @@ export const AgentThinkingTool = memo(function AgentThinkingTool({
7272
return () => clearInterval(interval)
7373
}, [isStreaming])
7474

75-
// Auto-scroll when expanded during streaming
75+
// Track whether content overflows the scroll container
76+
const [isOverflowing, setIsOverflowing] = useState(false)
77+
78+
// Auto-scroll when expanded during streaming + check overflow
7679
useEffect(() => {
7780
if (isStreaming && isExpanded && scrollRef.current) {
78-
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
81+
const el = scrollRef.current
82+
setIsOverflowing(el.scrollHeight > el.clientHeight)
83+
el.scrollTop = el.scrollHeight
7984
}
8085
}, [part.input?.text, isStreaming, isExpanded])
8186

@@ -142,14 +147,14 @@ export const AgentThinkingTool = memo(function AgentThinkingTool({
142147
<div
143148
className={cn(
144149
"absolute inset-x-0 top-0 h-8 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none transition-opacity duration-200",
145-
isStreaming ? "opacity-100" : "opacity-0",
150+
isStreaming && isOverflowing ? "opacity-100" : "opacity-0",
146151
)}
147152
/>
148153
<div
149154
ref={scrollRef}
150155
className={cn(
151-
"px-2 opacity-50",
152-
isStreaming && "overflow-y-auto scrollbar-none max-h-24",
156+
"px-2",
157+
isStreaming && "overflow-y-auto scrollbar-hide max-h-36",
153158
)}
154159
>
155160
<ChatMarkdownRenderer content={thinkingText} size="sm" isStreaming={isStreaming} />

0 commit comments

Comments
 (0)