Skip to content

Commit d9ba03a

Browse files
committed
Release v0.0.46
## What's New ### Improvements & Fixes - **Chat Stability** — Improved chat remounting and migration idempotency - **Selected Text Indicator** — Show "Using selected text" bubble for pasted text attachments - **UI Polish** — Updated spinner icons and selectors to match desktop style
1 parent 4870843 commit d9ba03a

10 files changed

Lines changed: 109 additions & 43 deletions

drizzle/0005_add_subchat_stats.sql

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- Create anthropic_accounts table for multi-account support
2-
CREATE TABLE `anthropic_accounts` (
2+
CREATE TABLE IF NOT EXISTS `anthropic_accounts` (
33
`id` text PRIMARY KEY NOT NULL,
44
`email` text,
55
`display_name` text,
@@ -10,14 +10,14 @@ CREATE TABLE `anthropic_accounts` (
1010
);
1111
--> statement-breakpoint
1212
-- Create anthropic_settings table to track active account
13-
CREATE TABLE `anthropic_settings` (
13+
CREATE TABLE IF NOT EXISTS `anthropic_settings` (
1414
`id` text PRIMARY KEY DEFAULT 'singleton' NOT NULL,
1515
`active_account_id` text,
1616
`updated_at` integer
1717
);
1818
--> statement-breakpoint
19-
-- Migrate existing credential from claude_code_credentials to anthropic_accounts
20-
INSERT INTO `anthropic_accounts` (`id`, `oauth_token`, `connected_at`, `desktop_user_id`, `display_name`)
19+
-- Migrate existing credential from claude_code_credentials to anthropic_accounts (skip if already migrated)
20+
INSERT OR IGNORE INTO `anthropic_accounts` (`id`, `oauth_token`, `connected_at`, `desktop_user_id`, `display_name`)
2121
SELECT
2222
'migrated-default',
2323
`oauth_token`,
@@ -27,7 +27,7 @@ SELECT
2727
FROM `claude_code_credentials`
2828
WHERE `id` = 'default' AND `oauth_token` IS NOT NULL;
2929
--> statement-breakpoint
30-
-- Set migrated account as active (only if migration inserted a row)
31-
INSERT INTO `anthropic_settings` (`id`, `active_account_id`, `updated_at`)
30+
-- Set migrated account as active (only if migration inserted a row and settings don't exist)
31+
INSERT OR IGNORE INTO `anthropic_settings` (`id`, `active_account_id`, `updated_at`)
3232
SELECT 'singleton', 'migrated-default', strftime('%s', 'now') * 1000
3333
WHERE EXISTS (SELECT 1 FROM `anthropic_accounts` WHERE `id` = 'migrated-default');

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

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4676,10 +4676,18 @@ export function ChatView({
46764676
const tabsToRender = useMemo(() => {
46774677
if (!activeSubChatId) return []
46784678

4679-
// Use allSubChats from Zustand store for validation (not agentSubChats from tRPC)
4680-
// allSubChats is updated optimistically when creating new sub-chats,
4681-
// while agentSubChats from tRPC query may be stale during race conditions
4682-
const validSubChatIds = new Set(allSubChats.map(sc => sc.id))
4679+
// Use agentSubChats from server (tRPC/remote API) as the authoritative source for validation.
4680+
// This fixes the race condition where:
4681+
// 1. setChatId resets allSubChats to [] but loads activeSubChatId from localStorage
4682+
// 2. tabsToRender was checking activeSubChatId against empty allSubChats → always failing
4683+
//
4684+
// agentSubChats comes from the server and is the "truth" about which sub-chats exist.
4685+
// allSubChats in Zustand is only populated AFTER the init useEffect runs.
4686+
//
4687+
// For optimistic updates when creating new sub-chats, we fall back to allSubChats
4688+
// since the new sub-chat won't be in agentSubChats yet (tRPC query is stale).
4689+
const sourceForValidation = agentSubChats.length > 0 ? agentSubChats : allSubChats
4690+
const validSubChatIds = new Set(sourceForValidation.map(sc => sc.id))
46834691

46844692
// If active sub-chat doesn't belong to this workspace → return []
46854693
// This prevents rendering sub-chats from another workspace during race condition
@@ -4711,9 +4719,15 @@ export function ChatView({
47114719
}
47124720
}
47134721

4714-
// Return in validOpenIds order for consistent rendering
4715-
return validOpenIds.filter(id => mustRender.has(id))
4716-
}, [activeSubChatId, pinnedSubChatIds, openSubChatIds, allSubChats])
4722+
// Return tabs to render
4723+
// Always include activeSubChatId even if not in validOpenIds (handles race condition
4724+
// where openSubChatIds from localStorage doesn't include the active tab yet)
4725+
const result = validOpenIds.filter(id => mustRender.has(id))
4726+
if (!result.includes(activeSubChatId)) {
4727+
result.unshift(activeSubChatId)
4728+
}
4729+
return result
4730+
}, [activeSubChatId, pinnedSubChatIds, openSubChatIds, allSubChats, agentSubChats])
47174731

47184732
// Get PR status when PR exists (for checking if it's open/merged/closed)
47194733
const hasPrNumber = !!agentChat?.prNumber
@@ -6414,15 +6428,16 @@ Make sure to preserve all functionality from both branches when resolving confli
64146428
<IconSpinner className="h-6 w-6 animate-spin" />
64156429
</div>
64166430
) : (
6417-
tabsToRender.map(subChatId => {
6431+
tabsToRender.map(subChatId => {
64186432
const chat = getOrCreateChat(subChatId)
64196433
const isActive = subChatId === activeSubChatId
64206434
const isFirstSubChat = getFirstSubChatId(agentSubChats) === subChatId
64216435

64226436
// Defense in depth: double-check workspace ownership
6423-
// Use allSubChats (Zustand) instead of agentSubChats (tRPC) because
6424-
// new sub-chats are added to Zustand immediately but tRPC query may be stale
6425-
const belongsToWorkspace = allSubChats.some(sc => sc.id === subChatId)
6437+
// Use agentSubChats (server data) as primary source, fall back to allSubChats for optimistic updates
6438+
// This fixes the race condition where allSubChats is empty after setChatId but before setAllSubChats
6439+
const belongsToWorkspace = agentSubChats.some(sc => sc.id === subChatId) ||
6440+
allSubChats.some(sc => sc.id === subChatId)
64266441

64276442
if (!chat || !belongsToWorkspace) return null
64286443

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

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,12 @@ export const IsolatedMessageGroup = memo(function IsolatedMessageGroup({
122122
const shouldShowSetupError =
123123
sandboxSetupStatus === "error" && isLastGroup && assistantIds.length === 0
124124

125-
// Check if this is an image-only message (no text content)
125+
// Check if this is an image-only message (no text content and no text mentions)
126126
const isImageOnlyMessage = imageParts.length > 0 && !textContent.trim() && textMentions.length === 0
127127

128+
// Check if this is an attachment-only message (no text but has images or text mentions)
129+
const isAttachmentOnlyMessage = !textContent.trim() && (imageParts.length > 0 || textMentions.length > 0)
130+
128131
return (
129132
<MessageGroupWrapper isLastGroup={isLastGroup}>
130133
{/* Attachments - NOT sticky (only when there's also text) */}
@@ -139,24 +142,49 @@ export const IsolatedMessageGroup = memo(function IsolatedMessageGroup({
139142
</div>
140143
)}
141144

142-
{/* Text mentions (quote/diff) - NOT sticky */}
145+
{/* Text mentions (quote/diff/pasted) - NOT sticky */}
143146
{textMentions.length > 0 && (
144147
<div className="mb-2 pointer-events-auto">
145148
<TextMentionBlocks mentions={textMentions} />
146149
</div>
147150
)}
148151

149-
{/* User message text - sticky (or image-only bubble) */}
152+
{/* User message text - sticky (or attachment-only summary bubble) */}
150153
<div
151154
data-user-message-id={userMsgId}
152155
className={`[&>div]:!mb-4 pointer-events-auto sticky z-10 ${stickyTopClass}`}
153156
>
154-
<UserBubbleComponent
155-
messageId={userMsgId}
156-
textContent={textContent}
157-
imageParts={isImageOnlyMessage ? imageParts : []}
158-
skipTextMentionBlocks={!isImageOnlyMessage}
159-
/>
157+
{/* Show "Using X" summary when no text but have attachments */}
158+
{isAttachmentOnlyMessage && !isImageOnlyMessage ? (
159+
<div className="flex justify-start drop-shadow-[0_10px_20px_hsl(var(--background))]" data-user-bubble>
160+
<div className="space-y-2 w-full">
161+
<div className="bg-input-background border px-3 py-2 rounded-xl text-sm text-muted-foreground italic">
162+
{(() => {
163+
const parts: string[] = []
164+
if (imageParts.length > 0) {
165+
parts.push(imageParts.length === 1 ? "image" : `${imageParts.length} images`)
166+
}
167+
const quoteCount = textMentions.filter(m => m.type === "quote" || m.type === "pasted").length
168+
const codeCount = textMentions.filter(m => m.type === "diff").length
169+
if (quoteCount > 0) {
170+
parts.push(quoteCount === 1 ? "selected text" : `${quoteCount} text selections`)
171+
}
172+
if (codeCount > 0) {
173+
parts.push(codeCount === 1 ? "code selection" : `${codeCount} code selections`)
174+
}
175+
return `Using ${parts.join(", ")}`
176+
})()}
177+
</div>
178+
</div>
179+
</div>
180+
) : (
181+
<UserBubbleComponent
182+
messageId={userMsgId}
183+
textContent={textContent}
184+
imageParts={isImageOnlyMessage ? imageParts : []}
185+
skipTextMentionBlocks={!isImageOnlyMessage}
186+
/>
187+
)}
160188

161189
{/* Cloning indicator */}
162190
{shouldShowCloning && (

src/renderer/features/agents/main/messages-list.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ export const SimpleIsolatedGroup = memo(function SimpleIsolatedGroup({
10151015
</div>
10161016
)}
10171017

1018-
{/* Text mentions (quote/diff) - NOT sticky */}
1018+
{/* Text mentions (quote/diff/pasted) - NOT sticky */}
10191019
{textMentions.length > 0 && (
10201020
<div className="mb-2 pointer-events-auto">
10211021
<TextMentionBlocks mentions={textMentions} />
@@ -1027,12 +1027,37 @@ export const SimpleIsolatedGroup = memo(function SimpleIsolatedGroup({
10271027
data-user-message-id={userMsg.id}
10281028
className={`[&>div]:!mb-4 pointer-events-auto sticky z-10 ${stickyTopClass}`}
10291029
>
1030-
<UserBubbleComponent
1031-
messageId={userMsg.id}
1032-
textContent={textContent}
1033-
imageParts={[]}
1034-
skipTextMentionBlocks
1035-
/>
1030+
{/* Show "Using X" summary when no text but have attachments */}
1031+
{!textContent.trim() && (imageParts.length > 0 || textMentions.length > 0) ? (
1032+
<div className="flex justify-start drop-shadow-[0_10px_20px_hsl(var(--background))]" data-user-bubble>
1033+
<div className="space-y-2 w-full">
1034+
<div className="bg-input-background border px-3 py-2 rounded-xl text-sm text-muted-foreground italic">
1035+
{(() => {
1036+
const parts: string[] = []
1037+
if (imageParts.length > 0) {
1038+
parts.push(imageParts.length === 1 ? "image" : `${imageParts.length} images`)
1039+
}
1040+
const quoteCount = textMentions.filter(m => m.type === "quote" || m.type === "pasted").length
1041+
const codeCount = textMentions.filter(m => m.type === "diff").length
1042+
if (quoteCount > 0) {
1043+
parts.push(quoteCount === 1 ? "selected text" : `${quoteCount} text selections`)
1044+
}
1045+
if (codeCount > 0) {
1046+
parts.push(codeCount === 1 ? "code selection" : `${codeCount} code selections`)
1047+
}
1048+
return `Using ${parts.join(", ")}`
1049+
})()}
1050+
</div>
1051+
</div>
1052+
</div>
1053+
) : (
1054+
<UserBubbleComponent
1055+
messageId={userMsg.id}
1056+
textContent={textContent}
1057+
imageParts={[]}
1058+
skipTextMentionBlocks
1059+
/>
1060+
)}
10361061

10371062
{/* Cloning indicator */}
10381063
{shouldShowCloning && (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const AgentBashTool = memo(function AgentBashTool({
121121
<div
122122
onClick={() => hasMoreOutput && !isPending && setIsOutputExpanded(!isOutputExpanded)}
123123
className={cn(
124-
"flex items-center justify-between pl-2.5 pr-2 h-7",
124+
"flex items-center justify-between pl-2.5 pr-0.5 h-7",
125125
hasMoreOutput && !isPending && "cursor-pointer hover:bg-muted/50 transition-colors duration-150",
126126
)}
127127
>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ export const AgentEditTool = memo(function AgentEditTool({
522522
<div
523523
onClick={hasVisibleContent ? handleHeaderClick : undefined}
524524
className={cn(
525-
"flex items-center justify-between pl-2.5 pr-2 h-7",
525+
"flex items-center justify-between pl-2.5 pr-0.5 h-7",
526526
hasVisibleContent && !isPending && !isInputStreaming && "cursor-pointer hover:bg-muted/50 transition-colors duration-150",
527527
)}
528528
>

src/renderer/features/agents/ui/agents-content.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function AgentsContent() {
6666
const [selectedChatId, setSelectedChatId] = useAtom(selectedAgentChatIdAtom)
6767
const setSelectedChatIsRemote = useSetAtom(selectedChatIsRemoteAtom)
6868
const setChatSourceMode = useSetAtom(chatSourceModeAtom)
69+
const chatSourceMode = useAtomValue(chatSourceModeAtom)
6970
const selectedDraftId = useAtomValue(selectedDraftIdAtom)
7071
const showNewChatForm = useAtomValue(showNewChatFormAtom)
7172
const betaKanbanEnabled = useAtomValue(betaKanbanEnabledAtom)
@@ -858,7 +859,7 @@ export function AgentsContent() {
858859
>
859860
{selectedChatId ? (
860861
<ChatView
861-
key={selectedChatId}
862+
key={`${chatSourceMode}-${selectedChatId}`}
862863
chatId={selectedChatId}
863864
isSidebarOpen={false}
864865
onToggleSidebar={() => {}}
@@ -942,7 +943,7 @@ export function AgentsContent() {
942943
{selectedChatId ? (
943944
<div className="h-full flex flex-col relative overflow-hidden">
944945
<ChatView
945-
key={selectedChatId}
946+
key={`${chatSourceMode}-${selectedChatId}`}
946947
chatId={selectedChatId}
947948
isSidebarOpen={sidebarOpen}
948949
onToggleSidebar={() => setSidebarOpen((prev) => !prev)}

src/renderer/features/sidebar/agents-sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3061,8 +3061,8 @@ export function AgentsSidebar({
30613061
isMultiSelectMode ? "px-0" : "px-2",
30623062
)}
30633063
>
3064-
{/* Drafts Section - only show for local chats */}
3065-
{chatSourceMode === "local" && drafts.length > 0 && !searchQuery && (
3064+
{/* Drafts Section - always show regardless of chat source mode */}
3065+
{drafts.length > 0 && !searchQuery && (
30663066
<div className={cn("mb-4", isMultiSelectMode ? "px-0" : "-mx-1")}>
30673067
<div
30683068
className={cn(

0 commit comments

Comments
 (0)