Skip to content

Commit 4cc6b86

Browse files
chphchclaudehappy-otter
committed
feat(app): gate bold-unread-title layout behind an experimental setting
Put the bold-unread-title session-list layout behind a new `expUnreadBoldTitle` setting (off by default) so it ships as an opt-in experiment rather than changing every user's unread presentation. When off, the upstream behavior is preserved exactly: unread is a solid blue status dot plus the "unread" status text, and titles keep their default weight. When on, unread shows as a bold title (once the agent has stopped) and the status dot reflects true liveness only. - settings: add `expUnreadBoldTitle` (default false) - SessionsList / ActiveSessionsGroupCompact: branch status color, leading indicator, status text, and title weight on the setting - settings/features: add the toggle + i18n strings for all languages Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent 98b2468 commit 4cc6b86

15 files changed

Lines changed: 92 additions & 25 deletions

File tree

packages/happy-app/sources/app/(app)/settings/features.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default function FeaturesSettingsScreen() {
1919
const [groupToolCalls, setGroupToolCalls] = useSettingMutable('groupToolCalls');
2020
const [expImageUpload, setExpImageUpload] = useSettingMutable('expImageUpload');
2121
const [sortSessionsByActivity, setSortSessionsByActivity] = useSettingMutable('sortSessionsByActivity');
22+
const [expUnreadBoldTitle, setExpUnreadBoldTitle] = useSettingMutable('expUnreadBoldTitle');
2223

2324
return (
2425
<ItemList style={{ paddingTop: 0 }}>
@@ -130,6 +131,18 @@ export default function FeaturesSettingsScreen() {
130131
}
131132
showChevron={false}
132133
/>
134+
<Item
135+
title={t('settingsFeatures.unreadBoldTitle')}
136+
subtitle={t('settingsFeatures.unreadBoldTitleSubtitle')}
137+
icon={<Ionicons name="ellipse-outline" size={29} color="#007AFF" />}
138+
rightElement={
139+
<Switch
140+
value={expUnreadBoldTitle}
141+
onValueChange={setExpUnreadBoldTitle}
142+
/>
143+
}
144+
showChevron={false}
145+
/>
133146
</ItemGroup>
134147

135148
{/* Privacy */}

packages/happy-app/sources/components/ActiveSessionsGroupCompact.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { type SessionState, formatPathRelativeToHome, vibingMessages, formatLast
99
import { Avatar } from './Avatar';
1010
import { Typography } from '@/constants/Typography';
1111
import { StatusDot } from './StatusDot';
12-
import { useAllMachines, useSessionGitStatus } from '@/sync/storage';
12+
import { useAllMachines, useSessionGitStatus, useSetting } from '@/sync/storage';
1313
import { StyleSheet, useUnistyles } from 'react-native-unistyles';
1414
import { t } from '@/text';
1515
import { useNavigateToSession } from '@/hooks/useNavigateToSession';
@@ -274,13 +274,19 @@ export function ActiveSessionsGroupCompact({ sessions, selectedSessionId }: Acti
274274
const CompactSessionRow = React.memo(({ session, selected, showBorder }: { session: SessionRowData; selected?: boolean; showBorder?: boolean }) => {
275275
const styles = stylesheet;
276276
const { theme } = useUnistyles();
277-
// Status dot reflects true liveness only, never reusing the blue
278-
// "thinking/running" color for unread.
279-
const status = STATUS_CONFIG[session.state];
280-
// Unread is shown as a bold title, but only once the agent has stopped —
281-
// never while it's still running (thinking), so a re-activated session
282-
// doesn't read as unread mid-turn.
283-
const showUnreadTitle = session.hasUnread && session.state !== 'thinking';
277+
// Experimental: show unread as a bold title instead of the blue status dot.
278+
const expUnreadBoldTitle = useSetting('expUnreadBoldTitle');
279+
const baseStatus = STATUS_CONFIG[session.state];
280+
// With the experimental layout off, keep the upstream behavior: reuse the
281+
// blue "thinking/running" color for the unread status dot. With it on, the
282+
// dot reflects true liveness only.
283+
const status = (!expUnreadBoldTitle && session.hasUnread)
284+
? { ...baseStatus, color: '#007AFF', dotColor: '#007AFF', isPulsing: false, isConnected: baseStatus.isConnected }
285+
: baseStatus;
286+
// Bold-title unread (experimental) is shown only once the agent has
287+
// stopped — never while it's still running (thinking), so a re-activated
288+
// session doesn't read as unread mid-turn.
289+
const showUnreadTitle = expUnreadBoldTitle && session.hasUnread && session.state !== 'thinking';
284290
const navigateToSession = useNavigateToSession();
285291
const swipeableRef = React.useRef<Swipeable | null>(null);
286292
const swipeEnabled = Platform.OS !== 'web';
@@ -322,7 +328,11 @@ const CompactSessionRow = React.memo(({ session, selected, showBorder }: { sessi
322328
const renderLeadingIndicator = () => {
323329
let indicator: React.ReactNode = null;
324330

325-
if (session.state === 'waiting' && session.hasDraft) {
331+
if (!expUnreadBoldTitle && session.hasUnread) {
332+
// Upstream behavior when the experimental layout is off: unread is a
333+
// solid blue dot.
334+
indicator = <StatusDot color={status.dotColor} isPulsing={false} />;
335+
} else if (session.state === 'waiting' && session.hasDraft) {
326336
indicator = (
327337
<Ionicons
328338
name="create-outline"

packages/happy-app/sources/components/SessionsList.tsx

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { layout } from './layout';
1919
import { useNavigateToSession } from '@/hooks/useNavigateToSession';
2020
import { SessionActionsAnchor, SessionActionsPopover } from './SessionActionsPopover';
2121
import { useSessionActionAlert } from '@/hooks/useSessionQuickActions';
22-
import { useSettingMutable } from '@/sync/storage';
22+
import { useSettingMutable, useSetting } from '@/sync/storage';
2323
import { t } from '@/text';
2424

2525
const stylesheet = StyleSheet.create((theme) => ({
@@ -116,6 +116,16 @@ const stylesheet = StyleSheet.create((theme) => ({
116116
sessionTitle: {
117117
fontSize: 15,
118118
flex: 1,
119+
},
120+
// Default title weight (upstream look) — used when the experimental
121+
// bold-unread-title layout is off.
122+
sessionTitleWeightDefault: {
123+
fontWeight: '500',
124+
...Typography.default('semiBold'),
125+
},
126+
// Lighter title weight for the experimental layout's read (non-unread) rows,
127+
// so unread rows stand out by contrast.
128+
sessionTitleWeightRegular: {
119129
...Typography.default('regular'),
120130
},
121131
sessionTitleConnected: {
@@ -358,25 +368,33 @@ const SessionItem = React.memo(({ session, selected, isFirst, isLast, isSingle }
358368
const styles = stylesheet;
359369
const navigateToSession = useNavigateToSession();
360370
const [actionsAnchor, setActionsAnchor] = React.useState<SessionActionsAnchor | null>(null);
361-
// Status dot reflects true liveness only, never reusing the blue
362-
// "thinking/running" color for unread.
363-
const status = STATUS_CONFIG[session.state];
364-
// Unread is shown as a bold title, but only once the agent has stopped —
365-
// never while it's still running (thinking), so a re-activated session
366-
// doesn't read as unread mid-turn.
367-
const showUnreadTitle = session.hasUnread && session.state !== 'thinking';
371+
// Experimental: show unread as a bold title instead of the blue status dot.
372+
const expUnreadBoldTitle = useSetting('expUnreadBoldTitle');
373+
const baseStatus = STATUS_CONFIG[session.state];
374+
// With the experimental layout off, keep the upstream behavior: reuse the
375+
// blue "thinking/running" color for the unread status dot. With it on, the
376+
// dot reflects true liveness only.
377+
const status = (!expUnreadBoldTitle && session.hasUnread)
378+
? { ...baseStatus, color: '#007AFF', dotColor: '#007AFF', isPulsing: false, isConnected: baseStatus.isConnected }
379+
: baseStatus;
380+
// Bold-title unread (experimental) is shown only once the agent has
381+
// stopped — never while it's still running (thinking), so a re-activated
382+
// session doesn't read as unread mid-turn.
383+
const showUnreadTitle = expUnreadBoldTitle && session.hasUnread && session.state !== 'thinking';
368384

369385
const vibingMessage = React.useMemo(() => {
370386
return vibingMessages[Math.floor(Math.random() * vibingMessages.length)].toLowerCase() + '…';
371387
}, [session.state]);
372388

373-
const statusText = session.state === 'thinking'
374-
? vibingMessage
375-
: session.state === 'disconnected'
376-
? t('status.lastSeen', { time: formatLastSeen(session.activeAt!, false) })
377-
: session.state === 'permission_required'
378-
? t('status.permissionRequired')
379-
: t('status.online');
389+
const statusText = (!expUnreadBoldTitle && session.hasUnread)
390+
? t('status.unread')
391+
: session.state === 'thinking'
392+
? vibingMessage
393+
: session.state === 'disconnected'
394+
? t('status.lastSeen', { time: formatLastSeen(session.activeAt!, false) })
395+
: session.state === 'permission_required'
396+
? t('status.permissionRequired')
397+
: t('status.online');
380398

381399
const handlePress = React.useCallback(() => {
382400
navigateToSession(session.id);
@@ -434,7 +452,9 @@ const SessionItem = React.memo(({ session, selected, isFirst, isLast, isSingle }
434452
<Text style={[
435453
styles.sessionTitle,
436454
status.isConnected ? styles.sessionTitleConnected : styles.sessionTitleDisconnected,
437-
showUnreadTitle && styles.sessionTitleUnread
455+
expUnreadBoldTitle
456+
? (showUnreadTitle ? styles.sessionTitleUnread : styles.sessionTitleWeightRegular)
457+
: styles.sessionTitleWeightDefault,
438458
]} numberOfLines={1}>
439459
{session.name}
440460
</Text>

packages/happy-app/sources/sync/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const SettingsSchema = z.object({
4141
fileDiffsSidebar: z.boolean().describe('Show the file diffs sidebar next to the chat on desktop'),
4242
groupToolCalls: z.boolean().describe('Collapse consecutive tool calls into grouped containers in chat'),
4343
expImageUpload: z.boolean().describe('Enable experimental image upload in chat'),
44+
expUnreadBoldTitle: z.boolean().describe('Show unread sessions as a bold title instead of a blue status dot (experimental)'),
4445
reviewPromptAnswered: z.boolean().describe('Whether the review prompt has been answered'),
4546
reviewPromptLikedApp: z.boolean().nullish().describe('Whether user liked the app when asked'),
4647
voiceAssistantLanguage: z.string().nullable().describe('Preferred language for voice assistant (null for auto-detect)'),
@@ -117,6 +118,7 @@ export const settingsDefaults: Settings = {
117118
fileDiffsSidebar: false,
118119
groupToolCalls: false,
119120
expImageUpload: false,
121+
expUnreadBoldTitle: false,
120122
reviewPromptAnswered: false,
121123
reviewPromptLikedApp: null,
122124
voiceAssistantLanguage: null,

packages/happy-app/sources/text/_default.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ export const en = {
238238
analyticsEnabled: 'Anonymous usage analytics active',
239239
imageUpload: 'Image Upload',
240240
imageUploadSubtitle: 'Attach images to messages for supported agents to analyze',
241+
unreadBoldTitle: 'Bold Unread Titles',
242+
unreadBoldTitleSubtitle: 'Show unread sessions as a bold title instead of a blue dot',
241243
},
242244

243245
imageUpload: {

packages/happy-app/sources/text/translations/ca.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export const ca: TranslationStructure = {
240240
analyticsEnabled: 'Analítica anònima d\'ús activa',
241241
imageUpload: 'Pujada d\'imatges',
242242
imageUploadSubtitle: 'Adjunta imatges als missatges perquè els agents compatibles les analitzin',
243+
unreadBoldTitle: 'Títols en negreta per a no llegides',
244+
unreadBoldTitleSubtitle: 'Mostra les sessions no llegides amb un títol en negreta en lloc d\'un punt blau',
243245
},
244246

245247
errors: {

packages/happy-app/sources/text/translations/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ export const en: TranslationStructure = {
254254
analyticsEnabled: 'Anonymous usage analytics active',
255255
imageUpload: 'Image Upload',
256256
imageUploadSubtitle: 'Attach images to messages for supported agents to analyze',
257+
unreadBoldTitle: 'Bold Unread Titles',
258+
unreadBoldTitleSubtitle: 'Show unread sessions as a bold title instead of a blue dot',
257259
},
258260

259261
errors: {

packages/happy-app/sources/text/translations/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export const es: TranslationStructure = {
240240
analyticsEnabled: 'Analítica anónima de uso activa',
241241
imageUpload: 'Subida de imágenes',
242242
imageUploadSubtitle: 'Adjunta imágenes a los mensajes para que los agentes compatibles las analicen',
243+
unreadBoldTitle: 'Títulos en negrita para no leídas',
244+
unreadBoldTitleSubtitle: 'Muestra las sesiones no leídas con un título en negrita en lugar de un punto azul',
243245
},
244246

245247
errors: {

packages/happy-app/sources/text/translations/it.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ export const it: TranslationStructure = {
238238
analyticsEnabled: 'Analisi anonime di utilizzo attive',
239239
imageUpload: 'Caricamento immagini',
240240
imageUploadSubtitle: 'Allega immagini ai messaggi per farle analizzare dagli agenti supportati',
241+
unreadBoldTitle: 'Titoli in grassetto per le non lette',
242+
unreadBoldTitleSubtitle: 'Mostra le sessioni non lette con un titolo in grassetto invece di un punto blu',
241243
},
242244

243245
errors: {

packages/happy-app/sources/text/translations/ja.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ export const ja: TranslationStructure = {
241241
analyticsEnabled: '匿名の使用状況分析がアクティブ',
242242
imageUpload: '画像アップロード',
243243
imageUploadSubtitle: '対応エージェントに分析させるため、メッセージに画像を添付する',
244+
unreadBoldTitle: '未読タイトルを太字に',
245+
unreadBoldTitleSubtitle: '未読セッションを青いドットではなく太字のタイトルで表示',
244246
},
245247

246248
errors: {

0 commit comments

Comments
 (0)