Skip to content

Commit 8c86d39

Browse files
committed
Release v0.0.60-beta.2
## What's New ### Improvements & Fixes - **Attachment card styling** — Unified attachment card styling and removed hover cards - **Split view streaming** — Fixed concurrent streaming interference in split view - **Notifications** — Fixed notification handling
1 parent a2b0184 commit 8c86d39

19 files changed

Lines changed: 185 additions & 191 deletions

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

src/main/windows/main.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BrowserWindow,
3+
Notification,
34
shell,
45
nativeTheme,
56
ipcMain,
@@ -100,26 +101,36 @@ function registerIpcHandlers(): void {
100101
"app:show-notification",
101102
(event, options: { title: string; body: string }) => {
102103
try {
103-
const { Notification } = require("electron")
104-
const iconPath = join(__dirname, "../../../build/icon.ico")
105-
const icon = existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : undefined
104+
if (!Notification.isSupported()) {
105+
console.warn("[Main] Notifications not supported on this system")
106+
return
107+
}
108+
109+
// On macOS, the app icon is used automatically — no custom icon needed.
110+
// On Windows, use .ico; on Linux, use .png.
111+
let icon: Electron.NativeImage | undefined
112+
if (process.platform !== "darwin") {
113+
const ext = process.platform === "win32" ? "icon.ico" : "icon.png"
114+
const iconPath = join(__dirname, "../../build", ext)
115+
icon = existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : undefined
116+
}
106117

107118
const notification = new Notification({
108119
title: options.title,
109120
body: options.body,
110-
icon,
121+
...(icon && { icon }),
111122
...(process.platform === "win32" && { silent: false }),
112123
})
113124

114-
notification.show()
115-
116125
notification.on("click", () => {
117126
const win = getWindowFromEvent(event)
118127
if (win) {
119128
if (win.isMinimized()) win.restore()
120129
win.focus()
121130
}
122131
})
132+
133+
notification.show()
123134
} catch (error) {
124135
console.error("[Main] Failed to show notification:", error)
125136
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,6 @@ export function AgentsBetaTab() {
332332
</p>
333333
</div>
334334

335-
{/* Early Access section hidden until beta-mac.yml is published to CDN
336335
<div className="bg-background rounded-lg border border-border overflow-hidden">
337336
<div className="flex items-center justify-between p-4">
338337
<div className="flex flex-col space-y-1">
@@ -378,7 +377,6 @@ export function AgentsBetaTab() {
378377
</div>
379378
</div>
380379
</div>
381-
*/}
382380
</div>
383381
</div>
384382
)

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
defaultAgentModeAtom,
88
desktopNotificationsEnabledAtom,
99
extendedThinkingEnabledAtom,
10+
notifyWhenFocusedAtom,
1011
soundNotificationsEnabledAtom,
1112
preferredEditorAtom,
1213
type AgentMode,
@@ -146,6 +147,7 @@ export function AgentsPreferencesTab() {
146147
)
147148
const [soundEnabled, setSoundEnabled] = useAtom(soundNotificationsEnabledAtom)
148149
const [desktopNotificationsEnabled, setDesktopNotificationsEnabled] = useAtom(desktopNotificationsEnabledAtom)
150+
const [notifyWhenFocused, setNotifyWhenFocused] = useAtom(notifyWhenFocusedAtom)
149151
const [analyticsOptOut, setAnalyticsOptOut] = useAtom(analyticsOptOutAtom)
150152
const [ctrlTabTarget, setCtrlTabTarget] = useAtom(ctrlTabTargetAtom)
151153
const [autoAdvanceTarget, setAutoAdvanceTarget] = useAtom(autoAdvanceTargetAtom)
@@ -273,6 +275,21 @@ export function AgentsPreferencesTab() {
273275
</div>
274276
<Switch checked={soundEnabled} onCheckedChange={setSoundEnabled} />
275277
</div>
278+
<div className="flex items-center justify-between p-4 border-t border-border">
279+
<div className="flex flex-col space-y-1">
280+
<span className="text-sm font-medium text-foreground">
281+
Notify When Focused
282+
</span>
283+
<span className="text-xs text-muted-foreground">
284+
Show notifications even when the app window is active
285+
</span>
286+
</div>
287+
<Switch
288+
checked={notifyWhenFocused}
289+
onCheckedChange={setNotifyWhenFocused}
290+
disabled={!desktopNotificationsEnabled}
291+
/>
292+
</div>
276293
</div>
277294

278295
{/* Navigation */}

src/renderer/features/agents/hooks/use-desktop-notifications.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { useCallback, useRef, useEffect } from "react"
77
import { useAtomValue } from "jotai"
88
import { isDesktopApp } from "../../../lib/utils/platform"
9-
import { desktopNotificationsEnabledAtom } from "../../../lib/atoms"
9+
import { desktopNotificationsEnabledAtom, notifyWhenFocusedAtom } from "../../../lib/atoms"
1010

1111
// throttle interval to prevent notification spam (ms)
1212
const NOTIFICATION_THROTTLE_MS = 3000
@@ -30,6 +30,7 @@ export interface NotificationOptions {
3030

3131
export function useDesktopNotifications() {
3232
const notificationsEnabled = useAtomValue(desktopNotificationsEnabledAtom)
33+
const notifyWhenFocused = useAtomValue(notifyWhenFocusedAtom)
3334

3435
// track last notification time to throttle rapid-fire notifications
3536
const lastNotificationTime = useRef<number>(0)
@@ -98,15 +99,15 @@ export function useDesktopNotifications() {
9899
}, [notificationsEnabled])
99100

100101
const notifyAgentComplete = useCallback((chatName: string) => {
101-
// don't notify if window is focused - user is already watching
102-
if (document.hasFocus()) {
102+
// Skip if window is focused and user hasn't opted into focused notifications
103+
if (!notifyWhenFocused && document.hasFocus()) {
103104
return
104105
}
105106

106107
const title = "Agent Complete"
107108
const body = chatName ? `Finished working on "${chatName}"` : "Agent has completed its task"
108109
showNotification(title, body, { priority: "complete" })
109-
}, [showNotification])
110+
}, [showNotification, notifyWhenFocused])
110111

111112
const notifyAgentError = useCallback((errorMessage: string) => {
112113
// always notify on errors, even if window is focused
@@ -116,26 +117,24 @@ export function useDesktopNotifications() {
116117
}, [showNotification])
117118

118119
const notifyAgentNeedsInput = useCallback((chatName: string) => {
119-
// don't notify if window is focused
120-
if (document.hasFocus()) {
120+
if (!notifyWhenFocused && document.hasFocus()) {
121121
return
122122
}
123123

124124
const title = "Input Required"
125125
const body = chatName ? `"${chatName}" is waiting for your input` : "Agent is waiting for your input"
126126
showNotification(title, body, { priority: "input" })
127-
}, [showNotification])
127+
}, [showNotification, notifyWhenFocused])
128128

129129
const notifyPlanReady = useCallback((chatName: string) => {
130-
// don't notify if window is focused
131-
if (document.hasFocus()) {
130+
if (!notifyWhenFocused && document.hasFocus()) {
132131
return
133132
}
134133

135134
const title = "Plan Ready"
136135
const body = chatName ? `"${chatName}" has a plan ready for approval` : "A plan is ready for your approval"
137136
showNotification(title, body, { priority: "plan" })
138-
}, [showNotification])
137+
}, [showNotification, notifyWhenFocused])
139138

140139
const requestPermission = useCallback(async (): Promise<NotificationPermission> => {
141140
if (isDesktopApp()) {

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6113,12 +6113,15 @@ Make sure to preserve all functionality from both branches when resolving confli
61136113
// Ignore audio errors
61146114
}
61156115
}
6116-
6117-
// Show native notification (desktop app, when window not focused)
6118-
notifyAgentComplete(agentChat?.name || "Agent")
61196116
}
61206117
}
61216118

6119+
// Show native notification if not manually aborted
6120+
// (the hook handles focus/preference checks internally)
6121+
if (!wasManuallyAborted) {
6122+
notifyAgentComplete(agentChat?.name || "Agent")
6123+
}
6124+
61226125
// Refresh diff stats after agent finishes making changes
61236126
fetchDiffStatsRef.current()
61246127

@@ -6309,12 +6312,15 @@ Make sure to preserve all functionality from both branches when resolving confli
63096312
// Ignore audio errors
63106313
}
63116314
}
6312-
6313-
// Show native notification (desktop app, when window not focused)
6314-
notifyAgentComplete(agentChat?.name || "Agent")
63156315
}
63166316
}
63176317

6318+
// Show native notification if not manually aborted
6319+
// (the hook handles focus/preference checks internally)
6320+
if (!wasManuallyAborted) {
6321+
notifyAgentComplete(agentChat?.name || "Agent")
6322+
}
6323+
63186324
// Refresh diff stats after agent finishes making changes
63196325
fetchDiffStatsRef.current()
63206326

src/renderer/features/agents/main/chat-input-area.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,9 @@ export const ChatInputArea = memo(function ChatInputArea({
526526
updateMode(getNextMode(subChatMode))
527527
}, [subChatMode, updateMode])
528528

529+
// Track narrow width for compact layout (hide labels when < 310px)
530+
const [isNarrow, setIsNarrow] = useState(false)
531+
529532
// Voice input state
530533
const {
531534
isRecording: isVoiceRecording,
@@ -1041,6 +1044,7 @@ export const ChatInputArea = memo(function ChatInputArea({
10411044
el.style.setProperty("--chat-input-width", `${width}px`)
10421045
parent?.style.setProperty("--chat-input-height", `${height}px`)
10431046
parent?.style.setProperty("--chat-input-width", `${width}px`)
1047+
setIsNarrow(width < 310)
10441048
})
10451049
observer.observe(el)
10461050
}}
@@ -1067,7 +1071,7 @@ export const ChatInputArea = memo(function ChatInputArea({
10671071
onSubmit={onSend}
10681072
contextItems={
10691073
images.length > 0 || files.length > 0 || textContexts.length > 0 || (diffTextContexts?.length ?? 0) > 0 || pastedTexts.length > 0 ? (
1070-
<div className="flex flex-wrap gap-[6px]">
1074+
<div className="flex flex-wrap items-center gap-[6px]">
10711075
{(() => {
10721076
// Build allImages array for gallery navigation
10731077
const allImages = images
@@ -1195,7 +1199,7 @@ export const ChatInputArea = memo(function ChatInputArea({
11951199
) : (
11961200
<AgentIcon className="h-3.5 w-3.5 shrink-0" />
11971201
)}
1198-
<span className="truncate">{subChatMode === "plan" ? "Plan" : "Agent"}</span>
1202+
{!isNarrow && <span className="truncate">{subChatMode === "plan" ? "Plan" : "Agent"}</span>}
11991203
<ChevronDown className="h-3 w-3 shrink-0 opacity-50" />
12001204
</button>
12011205
</DropdownMenuTrigger>
@@ -1407,10 +1411,12 @@ export const ChatInputArea = memo(function ChatInputArea({
14071411
: "hover:text-foreground hover:bg-muted/50",
14081412
)}
14091413
>
1410-
<ClaudeCodeIcon className="h-3.5 w-3.5 shrink-0" />
1414+
{!isNarrow && <ClaudeCodeIcon className="h-3.5 w-3.5 shrink-0" />}
14111415
<span className="truncate">
14121416
{hasCustomClaudeConfig ? (
14131417
"Custom Model"
1418+
) : isNarrow ? (
1419+
selectedModel?.name
14141420
) : (
14151421
<>
14161422
{selectedModel?.name}{" "}

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

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

3-
import { memo, useMemo } from "react"
3+
import { memo, useCallback, useMemo } from "react"
44
import { useAtomValue } from "jotai"
55
import {
66
messageAtomFamily,
77
assistantIdsPerChatAtomFamily,
88
isLastUserMessagePerChatAtomFamily,
99
rollbackTargetPerChatAtomFamily,
10-
isStreamingAtom,
1110
isRollingBackAtom,
1211
} from "../stores/message-store"
12+
import { useStreamingStatusStore } from "../stores/streaming-status-store"
1313
import { MemoizedAssistantMessages } from "./messages-list"
1414
import { extractTextMentions, TextMentionBlocks, TextMentionBlock } from "../mentions/render-file-mentions"
1515
import { AgentImageItem } from "../ui/agent-image-item"
@@ -104,7 +104,10 @@ export const IsolatedMessageGroup = memo(function IsolatedMessageGroup({
104104
const assistantIds = useAtomValue(assistantIdsPerChatAtomFamily(perChatKey))
105105
const isLastGroup = useAtomValue(isLastUserMessagePerChatAtomFamily(perChatKey))
106106
const rollbackTargetSdkUuid = useAtomValue(rollbackTargetPerChatAtomFamily(perChatKey))
107-
const isStreaming = useAtomValue(isStreamingAtom)
107+
const subChatStatus = useStreamingStatusStore(
108+
useCallback((s) => s.statuses[subChatId] ?? "ready", [subChatId])
109+
)
110+
const isStreaming = subChatStatus === "streaming" || subChatStatus === "submitted"
108111
const isRollingBack = useAtomValue(isRollingBackAtom)
109112

110113
// Show rollback button only when this user turn has a valid rollback target.
@@ -148,7 +151,7 @@ export const IsolatedMessageGroup = memo(function IsolatedMessageGroup({
148151
<MessageGroupWrapper isLastGroup={isLastGroup}>
149152
{/* All attachments in one row - NOT sticky (only when there's also text) */}
150153
{((!isImageOnlyMessage && imageParts.length > 0) || textMentions.length > 0) && (
151-
<div className="mb-2 pointer-events-auto flex flex-wrap items-end gap-1.5">
154+
<div className="mb-2 pointer-events-auto flex flex-wrap items-center gap-1.5">
152155
{imageParts.length > 0 && !isImageOnlyMessage && (() => {
153156
const resolveImgUrl = (img: any) =>
154157
img.data?.base64Data && img.data?.mediaType

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { createContext, memo, useCallback, useContext, useLayoutEffect, useMemo,
55
import { showMessageJsonAtom } from "../atoms"
66
import { extractTextMentions, TextMentionBlocks } from "../mentions/render-file-mentions"
77
import {
8-
chatStatusAtom,
98
isLastMessageAtomFamily,
10-
isStreamingAtom,
9+
isLastMessagePerChatAtomFamily,
1110
messageAtomFamily,
1211
} from "../stores/message-store"
12+
import { useStreamingStatusStore } from "../stores/streaming-status-store"
1313
import { MessageJsonDisplay } from "../ui/message-json-display"
1414
import { AssistantMessageItem } from "./assistant-message-item"
1515

@@ -407,9 +407,11 @@ const StreamingMessageItem = memo(function StreamingMessageItem({
407407
// Subscribe to this specific message via Jotai - only re-renders when THIS message changes
408408
const message = useAtomValue(messageAtomFamily(messageId))
409409

410-
// Subscribe to streaming status
411-
const isStreaming = useAtomValue(isStreamingAtom)
412-
const status = useAtomValue(chatStatusAtom)
410+
// Subscribe to per-subchat streaming status (fixes split view concurrent streaming)
411+
const status = useStreamingStatusStore(
412+
useCallback((s) => s.statuses[subChatId] ?? "ready", [subChatId])
413+
)
414+
const isStreaming = status === "streaming" || status === "submitted"
413415

414416
if (!message) return null
415417

@@ -484,7 +486,9 @@ export const MessageItemWrapper = memo(function MessageItemWrapper({
484486

485487
// Only subscribe to isLast - NOT to message content!
486488
// StreamingMessageItem and NonStreamingMessageItem will subscribe to message themselves
487-
const isLast = useAtomValue(isLastMessageAtomFamily(messageId))
489+
// Use per-subchat atom to avoid cross-pane interference in split view
490+
const perChatKey = `${subChatId}:${messageId}`
491+
const isLast = useAtomValue(isLastMessagePerChatAtomFamily(perChatKey))
488492

489493
// Only the last message subscribes to streaming status
490494
if (isLast) {

src/renderer/features/agents/main/new-chat-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1423,7 +1423,7 @@ export function NewChatForm({
14231423
// Context items for images, files, and pasted text files
14241424
const contextItems =
14251425
images.length > 0 || files.length > 0 || pastedTexts.length > 0 ? (
1426-
<div className="flex flex-wrap gap-[6px]">
1426+
<div className="flex flex-wrap items-center gap-[6px]">
14271427
{(() => {
14281428
// Build allImages array for gallery navigation
14291429
const allImages = images

0 commit comments

Comments
 (0)