All notable changes to the iOS app are documented here. Format follows Keep a Changelog.
Complete implementation of all remaining web app features that were missing from the iOS version.
- ForwardMessageSheet — Forward messages to other chat sessions with preview.
- RedPacketSheet — Send virtual red packets with custom amount and blessing text.
- TriviaQuizView — In-chat trivia quiz mini-game with score tracking.
- IdiomChainView — Chinese idiom chain (成语接龙) word game.
- PollComposerView + ChatPoll model — Create and vote on polls in group chats.
- GroupDetailsView — Group chat management (announcements, permissions, members, polls).
- MessageBubble now renders
[FORWARDED:],[RED_PACKET:],[GAME:],[POLL:]special message types.
- GlobalMessageSearchView — Full-text search across all chat sessions.
- KnowledgeBaseView — Document import, management, and toggle for knowledge base.
- ModelSwitcherView — Quick switch between AI model configurations with token estimation.
- KnowledgeGraphView — Knowledge graph node management and visualization.
- LearningReportView — Usage analytics summary with export capability.
- FriendsView — Friend list with search, starring, group filtering, custom friend CRUD.
- FriendGroupsView — Create and manage friend groups with color coding.
- FriendService — Persistence layer for friend groups and metadata.
- LeaderboardView — Multi-tab ranking (points, intimacy, check-in, achievements).
- AgentsView — Agent list with search and custom agent management.
- AgentWorkspaceView — Multi-topic workspace for task-oriented agent conversations.
- CustomPersonaEditorSheet — Shared editor for creating and editing custom AI friends and agents.
- PersonaStore — Added
upsertCustomPersona()for unified create/edit with persistence. - FriendsView and AgentsView now support full context-menu create/edit/delete for custom personas.
- StorageService — Added
allowedImportKeyswhitelist covering all new storage keys. - DataImporter — Per-key type validation, size limits, and
reloadAllStoresrefresh chain. - Import whitelist now covers:
friends.groups,friends.meta,knowledgeBase,knowledgeGraph.custom,personas.custom.
- ChatStore — Added group chat management (
createGroupSession,updateGroupName,updateAnnouncement,updatePermissions,addMembers), poll lifecycle (createPoll,votePoll), message forwarding (forwardMessage), topic sessions (createTopicSession), and global search (searchMessages). - ChatSession model — Extended with
announcement,permissions,polls,parentSessionId,topicTitlefields. - DashboardView — QuickActions widget now includes Friends navigation entry.
- SettingsView — Added NavigationLinks to all 5 Advanced panels.
- Chat_Buddy_iOSApp — Injected
FriendServiceas environment object. - AGENTS.md / CLAUDE.md — Updated directory structure, data storage keys, and development status.
- Created
docs/WebToiOSMigration/PARITY_REPORT_2026-03-30.md— Formal Web↔iOS parity status report.
- Project test targets and shared scheme
- Added
Chat_Buddy_iOSTestsandChat_Buddy_iOSUITests. - Added shared scheme at
Chat_Buddy_iOS.xcodeproj/xcshareddata/xcschemes/Chat_Buddy_iOS.xcscheme.
- Added
- Background animation selection in settings
BackgroundPickerViewnow supports choosing animated effects (AnimatedBackground) in both global mode and per-chat mode.
- Backup/restore now covers full app storage
DataExporternow includes rawstorageDatafromStorageService.exportAll().DataImporterrestoresstorageDatafirst, then refreshes in-memory API settings viaAPIConfigStore.reloadFromStorage().- Backup settings now include
hasCompletedOnboarding.
- Chat tool execution flow
AIPipeline.Resultnow returnstoolOutputs.ChatViewModelappends.toolmessages before assistant replies so users can see tool results in chat.- Group chat path now passes
toolExecutorintoAIPipeline.run(...)consistently.
- Dashboard interaction wiring
- Quick actions now route users directly to target tabs (new chat/friends).
- Today's Pick card can navigate to chats.
- Animation intensity now affects real-time UI behavior
- Chat background animation is disabled when
AnimationIntensity == .none. - Typing indicator animation speed/enable state now follows
ThemeManager.animationIntensity.
- Chat background animation is disabled when
- Moments/Social integration gaps
- Moment likes now trigger social progress (
onMomentLiked) on new likes. - User posting now updates social task progress (
onMomentsPosted). - Intimacy reaching 100 now triggers social hook (
onIntimacyMaxed).
- Moment likes now trigger social progress (
- Localization cleanup
- Replaced remaining hardcoded strings in Moments/Settings with
localization.t(...). - Added corresponding
Localizable.xcstringskeys for background animation labels and settings copy.
- Replaced remaining hardcoded strings in Moments/Settings with
- Background task registration timing
MomentsBackgroundScheduler.register()moved to app init and made idempotent.- Background task handlers now instantiate store dependencies within task execution.
- Code cleanup
- Removed deprecated
personaIdusage in affected call sites. - Deleted obsolete
Features/Moments/MomentsPlaceholderView.swift.
- Removed deprecated
Per-persona long-term memory: the AI learns facts about the user during conversation, persists them across sessions, and recalls them to give each relationship a sense of continuity.
Models/CharacterMemory.swift— Two new types:MemoryCategoryenum (preference/fact/event). Each case exposeslabel/labelZh, aColor(blue / green / purple), andimportanceLabel(_ n: Int)which maps importance 1–10 to emoji indicators (⬜ 🟨 🟧 🟥 ⭐).CharacterMemorystruct (Identifiable, Codable):id(UUID),personaId,fact,category,importance(1–10),createdAt,lastRecalledAt,isForgotten(soft-delete flag).MemoriesData: Codable— persistence wrapper at keychat-buddy:memories.
-
Services/Memory/MemoryService.swift—@Observable final class:memories(for:)— active (non-forgotten) records for a persona, newest-first.relevantMemories(for:limit:)— top-N records sorted by importance desc then recency; updateslastRecalledAton each returned record and persists.addMemory(personaId:fact:category:importance:)— deduplication guard: computes Jaccard word-overlap between the new fact and all existing active facts; if any similarity score exceeds 0.85, the insertion is skipped.forgetMemory(id:personaId:)— soft-deletes a single record.forgetAll(for:)— hard-deletes all records for a persona (used by Clear All).applyDecay()— runs on init; soft-deletes any memory whose(now − lastRecalledAt)exceedsimportance × 3 days(e.g. importance 5 → 15-day survival window).- Persisted as
MemoriesDataatchat-buddy:memoriesviaStorageService.
-
Services/Memory/MemoryInjector.swift— Static enum with two helpers:memoryBlock(for:service:isZh:)— callsrelevantMemories(limit: 10), formats them as a• factbullet list under an "WHAT YOU REMEMBER ABOUT THE USER:" / "你对用户的记忆:" header. Returns""when no memories exist ormemoryServiceisnil.memorySaveHint(isZh:)— returns a system-prompt instruction block telling the AI to emit[MEMORY_SAVE: category=preference importance=7]fact[/MEMORY_SAVE]tags when the user reveals important personal information.
-
Services/Chat/AIPipeline.swift:run()gainsmemoryService: MemoryService? = nil(optional, no impact when nil).ResultgainsnewMemories: [ExtractedMemory](empty array when no tags found).ExtractedMemorycarriesfact,category,importance.buildSystemPromptinjectsMemoryInjector.memoryBlock(...)after the affinity hint and appendsMemoryInjector.memorySaveHint(isZh:)at the end of the prompt.parseResponse(_:isZh:)callsextractMemories(from:)first: scans for all[MEMORY_SAVE: category=X importance=N]...[/MEMORY_SAVE]blocks, strips them from the displayed message text, parses attributes viaparseMemoryAttributes(_:), and populatesResult.newMemories.
-
Features/Chats/ChatViewModel.swift:sendMessage()gainsmemoryService: MemoryService? = nil.fetchResponse()gainsmemoryService: MemoryService? = nil; passes it toAIPipeline.run().- After successful response: iterates
result.newMemoriesand callsmemoryService?.addMemory(...)for each extracted fact.
Features/Chats/MemoriesView.swift— Sheet presented from theChatViewtoolbar menu:NavigationStackwith title "Memories" / "角色记忆".- List of active memories sorted
lastRecalledAtdesc. Each row: category color pill badge + importance emoji, fact text (.headlineweight), created date · relative recalled time. - Swipe-to-delete →
memoryService.forgetMemory(id:personaId:). - Toolbar leading "Add Memory" button → expands an inline form section (free-text
TextField,Pickerfor category,Stepperfor importance 1–10); Add commits viaaddMemory. - Toolbar trailing "Clear All" (destructive, with confirmation alert) →
forgetAll(for:). - Empty state:
brain.head.profileSF Symbol icon + localized description.
ChatView—@Environment(MemoryService.self),@State private var showMemories = false. Toolbar···menu gains a "Memories" item (brain.head.profileicon)..sheet(isPresented: $showMemories)→MemoriesView(personaId:).presentationDetents([.medium, .large]).send()passesmemoryService: memoryServicetoChatViewModel.sendMessage.Chat_Buddy_iOSApp—@State private var memoryService = MemoryService()added; injected via.environment(memoryService).Localizable.xcstrings— 11 new keys (en + zh-Hans):memories_title,memories_empty,memories_empty_desc,memories_add,memories_delete,memories_clear,memories_clear_confirm,memories_clear_message,memories_category_preference,memories_category_fact,memories_category_event.
** BUILD SUCCEEDED ** — 0 errors, 0 warnings on iPhone 17 Pro simulator.
Gradient wallpaper layer for the chat view, with per-chat overrides and a global default picker.
Models/ChatBackground.swift—ChatBackgroundPresetCodable struct withid,name,nameZh,startHex,endHex. Ten presets: Default, Aurora, Sunset, Ocean, Rose, Forest, Midnight, Sakura, Golden, Cosmos.preset.gradient(opacity:)returns aLinearGradient?(nil for Default).Services/Background/BackgroundStore.swift—@Observable final class.globalPresetId: String(default"none"),chatPresets: [String: String](sessionId → presetId).resolvedPreset(for:)applies chat override → global fallback. Persisted atchat-buddy:backgrounds.
Features/Settings/Appearance/BackgroundPickerView.swift— Grid of 90-pt gradient preview cards with checkmark overlay. Accepts optionalsessionIdparameter: if provided, sets per-chat override and shows a "Reset to Default" row; otherwise sets global theme.
ChatViewwraps its body in aZStack. The bottom layer renderspreset.gradient()full-bleed behind the message list and input bar. The glass materials on bubbles and the input tray let the gradient show through.- Chat toolbar
···menu gains a "Chat Background"NavigationLink→BackgroundPickerView(sessionId:). - Settings → Appearance gains a "Chat Background" row → global
BackgroundPickerView().
User profile, points economy, achievements, daily check-in, gifts, and mini-games.
Models/UserProfile.swift—UserProfileCodable struct:nickName,avatarEmoji(one of 12 predefined emojis),signature.Models/Achievement.swift— Four types:AchievementDefinition(10 static entries): id, bilingual name/description, SF Symbol icon, points, category (social / streak / gifts).AchievementRecordCodable: id +unlockedAt: Date.GiftDefinition(6 static entries): emoji, bilingual name,cost(points),intimacyBoost.DailyTaskDefinition(6 static entries) +DailyTaskStateCodable: date string,completed: [String],progress: [String: Int],chatPersonasToday: [String].
Services/Social/UserProfileStore.swift—@Observable.update(nickName:avatarEmoji:signature:). Persisted atchat-buddy:userProfile.Services/Social/SocialService.swift—@Observable final class. Core social state machine:- Points:
addPoints(_:),spendPoints(_:) → Bool. - Check-in:
checkIn() → Int(base 10 pts + 2×streak bonus capped at 20, streak auto-calculated from date history). - Achievements:
unlockAchievement(_:) → Bool(idempotent, auto-awards points). - Daily tasks:
updateTaskProgress(_:increment:)(auto-completes + awards on hitting target); special handling fortask_chat3viachatPersonasToday. - Hooks:
onMessageSent(personaId:chatStore:),onGiftSent(intimacyAfter:),onGamePlayed(),onMomentLiked(),onMomentsPosted(total:),onIntimacyMaxed(). Persisted atchat-buddy:social.
- Points:
Services/Chat/AffinityService— AddedaddBoost(_ amount: Int, for personaId: String)for direct intimacy boosts from gifts (bypasses 5-minute cooldown).
Features/Settings/Profile/UserProfileView.swift— Horizontal avatar emoji grid (scrollable), nickname and signature text fields, save button with animated "Saved! ✓" confirmation.Features/Achievements/AchievementsView.swift— Stats header (unlocked/points/streak), category filter pills (All / Social / Streak / Gifts), achievement grid (locked = grayscale 0.7), daily tasks section withProgressViewbars.Features/Achievements/DailyCheckInView.swift— Stat cards (streak / points / achievements), 7-day calendar with filled circles + weekday labels, daily task list with per-task progress, animated check-in button (shows "Checked in! +N pts 🎉" on success).Features/Chats/Components/GiftPanelView.swift— Points balance header, intimacy progress bar, 3-column gift grid (grayed-out if unaffordable), sends a gift chat message on confirm, callssocialService.onGiftSent,affinityService.addBoost.Features/Chats/Components/RockPaperScissorsView.swift— Score board, 3-second countdown, result display (+10 pts per win), Play Again / Finish buttons.Features/Chats/Components/NumberGuessView.swift— 7-attempt pill indicator, number pad input, Too High / Too Low / Correct feedback, +30 pts on win.Features/Dashboard/Widgets/SocialWidget.swift— Full-widthGlassCardshowing points / streak / achievements stats + Quick Check-in button + Achievements sheet button.
Chat_Buddy_iOSApp— injectsbackgroundStore,userProfileStore,socialService.ChatViewtoolbar menu — Send a Gift →GiftPanelViewsheet; Play a Game submenu → RPS or Number Guess.ChatViewModel.sendMessage— gains optionalsocialService: SocialService?parameter; callsonMessageSent(personaId:chatStore:)after each user message.DashboardView—SocialWidgetplaced belowTodaysPickWidget.SettingsView— Profile section (UserProfileView) + Social & Achievements section (DailyCheckInView, AchievementsView) + Chat Background row under Appearance.- 21 new xcstrings keys (en + zh-Hans):
achievements_title,achievements_points,background_title,background_reset,checkin_title,checkin_button,checkin_done,done,game_title,game_rps,game_number,gift_title,gift_no_points,profile_title,profile_avatar,profile_nickname,profile_signature,profile_save,profile_saved,settings_profile,settings_social.
A full WeChat-style 朋友圈 social feed powering the Moments tab, replacing the former placeholder.
-
Models/MomentPost.swift— Three Codable types:MomentComment— comment withauthorId,content,createdAt, and an optionalReplyReference(commentId + authorId + authorName) for threaded replies.MomentPost— post withauthorId,content,imagePaths(filenames), optionallocation,createdAt,likes: [String],reactions: [String: [String]](emoji → user IDs), andcomments: [MomentComment].MomentsData— top-level persisted blob:posts,lastAIPostTime,draftText,draftLocation,lastStoryEventDate.
-
Services/Moments/MomentsStore.swift—@Observable final class MomentsStore:- CRUD:
createPost(content:imageData:location:authorId:),deletePost(id:),toggleLike(postId:userId:),addReaction(postId:emoji:userId:),addComment(postId:content:authorId:replyTo:),deleteComment(postId:commentId:). - Draft:
saveDraft(text:location:),clearDraft(). - Orchestrator support:
recordAIPost(personaId:),recordStoryEvent(date:),addHistoricalPost(_:)(inserts + re-sorts newest-first for seeding). - Image helpers:
saveImage(_:)— compresses to max 600 px JPEG 0.7, saves toDocuments/moments/<UUID>.jpg;deleteImage(_:)removes the file;imageURL(for:)returns the full URL. - Persists a single
MomentsDatablob atchat-buddy:momentsviaStorageService.
- CRUD:
-
Services/Moments/MomentsService.swift— Static helpers:reactionEmojis: [String]— 6 supported reactions: 😂 ❤️ 👍 🔥 😮 😢.aiLocations: [String: [String]]— per-persona location pools matching each character's personality.SeasonalEvent— 13 holidays/events (New Year, Valentine's, Halloween, Christmas, Miku Day, etc.).personaBirthdays: [String: String]— compiled fromPersonaStore(id → "MM-dd").timeContext() → String— maps current hour to morning/afternoon/evening/night.todayEvents() → TodayEvents— returns which persona birthdays fall today + any matching seasonal event.- AI prompt builders:
generatePostPrompt,generateCommentPrompt,generateBirthdayPrompt,generateHolidayPrompt.
-
Services/Moments/MomentsOrchestrator.swift— Static enum driving all AI activity:run(store:configStore:)— called from.taskinMomentsView: seeds 4 posts when feed is empty, checks story events for today (birthday + holiday posts), then enters a 5-minute periodic loop where each social companion posts after a random 30–120 min cooldown.reactToUserPost(postId:store:configStore:)— after user posts: waits 15–40 s then has 2–4 personas like/react; waits 30–90 s then has 1–2 personas comment.
-
Features/Moments/MomentsView.swift— Main feed replacingMomentsPlaceholderView:- Quick-compose tap row (avatar + placeholder text → opens composer).
- Hashtag filter banner (shown when a tag is active;
✕to clear). LazyVStackofMomentCardViewitems, paginated at 10 per page with a "Load More" button.- Sheets for composer, comments, and repost driven by
PostID: Identifiablewrapper (avoids stale struct copies). .tasklaunchesMomentsOrchestrator.run.
-
Features/Moments/MomentCardView.swift— Single post card:- Header: persona avatar circle (initial + accent color), name, relative timestamp, optional location badge.
- Content:
ParsedTextViewrenders plain text with tappable#hashtagpills below. - Photo grid: 1 photo = full-width; 2+ photos = 2-column grid (up to 4 shown).
- Likes strip:
❤️ Name1, Name2 +N(up to 3 names). - Reaction pills: toggleable per-emoji counts; hidden when count is zero.
- Action row: Like toggle ❤️ | Reaction picker
Menu| Comment 💬 | Share ↗. - Last 3 comments inline; "View all N comments" button when more exist.
- Context menu "Delete Moment" for the current user's own posts.
- iOS 26 note: comment text uses
VStacklayout instead of deprecatedText + Textconcatenation.
-
Features/Moments/PostComposerView.swift— Full-height sheet:- Auto-focuses
TextEditoron appear. PhotosPicker(max 4 images); selected photos displayed in a 2-column preview grid with per-image✕remove buttons.- Location picker sub-sheet with 10 preset locations + free-text custom entry.
- Draft restore banner (auto-hides after 3 s) with "Discard" button.
.task(id: text)500 ms debounce →store.saveDraft(...).- Calls
MomentsOrchestrator.reactToUserPostasynchronously after posting.
- Auto-focuses
-
Features/Moments/CommentsView.swift— Half-height sheet:- Accepts
postId: String; reads live fromMomentsStoreso new comments appear instantly. - Comment list with swipe-to-delete for own comments.
- Reply mode: tap any comment to set
replyToComment; a "Replying to Name ✕" strip appears above the input. - Bottom input bar (pill
TextField+ send button).
- Accepts
-
Features/Moments/RepostSheet.swift— Half-height sheet:- Lists all
ChatStore.sessionswith persona avatar and name. - Tap → prepends
"[Shared Moment · Name]\nContent"as a.usermessage viachatStore.appendMessage. - Shows
✓check mark on the tapped row; auto-dismisses after 1.2 s.
- Lists all
Chat_Buddy_iOSApp.swift—MomentsStoreadded as@Stateand injected via.environment(momentsStore).Navigation/RootTabView.swift— Moments tab now rendersMomentsView().Localizable.xcstrings— 25 new keys (moments_*) in bothenandzh-Hans.
** BUILD SUCCEEDED ** — 0 errors, 0 warnings on iPhone 17 Pro simulator.
Models/ChatMessage.swift— Addedtimestamp: Date(auto-assigned at init; backward-compatible customCodable) andquotedMessageId: String?for reply threading.Models/ChatSession.swift— AddedisPinned: Bool(defaultfalse; backward-compatible customCodable).Models/Bookmark.swift— NewIdentifiable, Codablestruct:messageId,sessionId,content,personaId,bookmarkedAt: Date.
Services/Chat/BookmarkService.swift—@Observable final class.isBookmarked(_:),toggleBookmark(_:sessionId:personaId:),removeBookmark(messageId:),clear(). Persisted atchat-buddy:bookmarks.Services/Chat/DraftService.swift—@Observable final class. Stores[String: DraftEntry](text +savedAt+ optionalquotedMessageId) atchat-buddy:drafts. 7-day expiry purge on init.save(text:quotedMessageId:for:),clear(for:),draft(for:).
pinSession(_:)/unpinSession(_:)— updatesisPinnedand re-sorts pinned sessions to the top (stable sort).deleteMessage(id:in:)— removes a single message by ID from a session.searchMessages(query:in:)— case-insensitive filter of visible messages.
Features/Chats/Components/QuotedMessageView.swift— Compact reply-preview strip: left accent border, sender name, 1-line content preview, optional✕dismiss button (omitted when shown inside bubbles).Features/Chats/BookmarksSheet.swift— Session-scoped bookmark list. Swipe-to-delete; tap →onSelect(Bookmark)callback for scroll-to.
MessageBubble— Timestamp (Text(msg.timestamp, style: .time)) below bubble. Long-press context menu: Copy / Bookmark / Reply / Delete. Quoted-message preview strip inside bubble. New params:messages:[ChatMessage],sessionId:String,onQuote:,onDelete:.MessageInputView— OptionalquotedMessage: ChatMessage?showsQuotedMessageViewstrip above the input;onClearQuote:callback dismisses it.ChatView— Bookmarks toolbar button (leading nav bar). Search toggle + search bar.ShareLinkexport. Draft loaded on.onAppear..task(id: viewModel.inputText)debounce-saves draft.scrollToMessageIdstate for bookmark-triggered scroll.ChatsView— Two sections (Pinned / All) with section headers. Context menu adds Pin/Unpin; pin badge (pin.fill) shown in session row.
Chat_Buddy_iOSApp.swift—BookmarkServiceandDraftServiceadded as@Stateand injected via.environment(...).
** BUILD SUCCEEDED ** — 0 errors, 0 warnings on iPhone 17 Pro simulator.
Performed a full implementation audit across all completed phases. The following real functionality gaps were identified and resolved:
Features/Dashboard/Widgets/RecentChatsWidget.swift— Replaced "Coming Soon" stub with a live widget that readsChatStorefrom environment and displays up to 3 recently updated sessions, each with persona avatar (initial + accent color), localized name, and last message preview.Features/Dashboard/Widgets/StatsWidget.swift— Replaced hardcoded0placeholders with real computed values:totalMessages(sum of all visible messages across sessions),totalChats(sessions with at least one message), andstreakDays(consecutive calendar days with activity, computed fromsession.updatedAt). CachedDateFormatterasprivate static letper CLAUDE.md.Features/Dashboard/Widgets/QuickActionsWidget.swift— "New Chat" button now calls anonNewChat: () -> Voidcallback (passed fromDashboardView) that setsAppState.selectedTab = .chats, switching the tab programmatically.Features/Dashboard/DashboardView.swift— Added@Environment(ChatStore.self)and@Environment(AppState.self). PassesonNewChatclosure toQuickActionsWidget.StatsWidgetandRecentChatsWidgetnow read from environment directly (no viewModel pass-through).Features/Dashboard/DashboardViewModel.swift— Removed faketotalMessages: Int { 0 },totalChats: Int { 0 },streakDays: Int { 0 }properties. KeptgreetingKey,dateString,todaysPick.App/AppState.swift— Addedvar selectedTab: AppTab = .dashboardfor app-wide tab switching.Navigation/RootTabView.swift— ReadsAppStatefrom environment; bindsTabView(selection:)to$appState.selectedTabvia@Bindable.Services/API/AIClient.swift— Removed deadconfigure(with:)method andprivate var client: APIClient?property that were never used (every call always created a newAPIClient(config:)directly).
** BUILD SUCCEEDED ** — 0 errors, 0 warnings on iPhone 17 Pro simulator.
Models/AffinityLevel.swift—AffinityLevelenum (5 tiers:acquaintance→friend→goodFriend→closeFriend→soulmate, score 0–100). Each tier exposes:label/labelZh,color(silver → sky → teal → pink → gold),promptHint/promptHintZhfor system-prompt injection.AffinityLevel.level(for score:)maps a raw score to the correct tier.localizedLabel(isZh:)selects EN/ZH at runtime.Services/Chat/AffinityService.swift—@Observable final class AffinityService: stores per-persona affinity scores ([String: Int], 0–100) in UserDefaults underchat-buddy:intimacy.addChatIntimacy(for:)adds +1 per persona per 5-minute cooldown window (in-memory[String: Date]tracker, not persisted).score(for:)andlevel(for:)are O(1) reads. Capped at 100.
Services/Chat/AIPipeline.swift—run(session:persona:config:aiLanguageCode:intimacyLevel:)gains anintimacyLevel: Intparameter (default1).buildSystemPromptinjects aRELATIONSHIP:hint derived fromAffinityLevel(rawValue: intimacyLevel), placed after the mood hint in both EN and ZH prompts. The hint tells the AI how intimately to respond (formal acquaintance → affectionate soulmate).Features/Chats/ChatViewModel.swift—sendMessage(...)gainsaffinityService: AffinityServiceparameter. After appending the user message, callsaffinityService.addChatIntimacy(for: persona.id)then reads the current level and passesintimacyLevelintofetchResponse → AIPipeline.run.Features/Chats/ChatView.swift— ReadsAffinityServicefrom environment. Toolbar principal item appends· [Level Name](color-coded) after the mood label when score > 0. Empty-state hint shows aheart.fillbadge with level name +score/100when affinity has been earned.Chat_Buddy_iOSApp.swift—AffinityServiceadded as@Stateand injected via.environment(affinityService).
Services/Chat/MoodService.swift—MoodServiceenum: five moods (happy,calm,excited,tired,melancholy).currentMood(for:)maps the current hour to a mood candidate pool and selects deterministically using a persona ID hash, so every persona has a unique but stable mood within the same hour. Each mood exposesemoji,localizedLabel(isZh:),promptHint, andpromptHintZh.Services/Chat/AIPipeline.swift—AIPipelineenum: replaces the inline API call inChatViewModel. Responsibilities: (1) context compression — whendisplayMessages.count > 15, only the last 8 messages are passed to the API; (2) minimum response delay — after the API returns, sleeps formax(0, persona.minimumResponseDelay − elapsed)seconds so fast responses feel natural rather than instant; (3) enhanced system prompt — injects persona traits + current mood hint; (4)[SILENCE]parsing — returnswasSilent: true, causingChatViewModelto skip appending any message; (5)[MULTI:msg1|msg2]parsing — splits the response into multiple strings delivered with an 0.8 s inter-message pause.
Models/Persona.swift— AddedminimumResponseDelay: Doublecomputed property: social companions draw from1.0–2.5 s, task agents from0.4–1.2 s, usingDouble.random(in:)for natural variation per turn.Features/Chats/ChatViewModel.swift—fetchResponse(...)now delegates fully toAIPipeline.run(session:persona:config:aiLanguageCode:). Removed the inlinebuildSystemPromptmethod.CancellationErroris caught separately and discarded silently.isTypingis set tofalseexplicitly rather than viadefer, ensuring it staystrueduring multi-message inter-message pauses.Features/Chats/ChatView.swift— Navigation bar now uses aToolbarItem(placement: .principal)showing persona name + mood emoji and label (replaces plain.navigationTitle). The empty-state hint also displays the current mood below the persona name.
Models/ChatSession.swift— NewChatSessionstruct: persists a conversation (id, personaId, messages array, createdAt, updatedAt).displayMessagesfilters out system prompts for UI rendering;lastMessagedrives list previews.Services/Chat/ChatStore.swift—@Observable final class ChatStore: single source of truth for all conversations.getOrCreateSession(for:)returns an existing session or creates one;appendMessage(_:to:)auto-promotes the chat to top of list;clearMessages(in:)wipes user/AI messages while preserving system state. Persisted viaStorageService(chatSessionskey).Features/Chats/ChatViewModel.swift—@ObservableViewModel: ownsinputText,isTyping, anderrorMessagestate.sendMessage(...)appends the user message, setsisTyping, and fires an async task that callsAIClient.shared.sendChatCompletion. Builds a per-persona system prompt (bilingual: respectsresolvedAILanguage). Keeps a rolling 20-message context window.Features/Chats/ChatView.swift— Full chat screen:ScrollViewReaderauto-scrolls to newest message on send and on typing state change. Shows persona avatar + personality hint when conversation is empty. Error banner slides in from bottom. "Clear Messages" alert via toolbarMenu.Features/Chats/ChatsView.swift— ReplacesChatsPlaceholderView.NavigationStackwith path-based navigation toChatView. Session rows areGlassCard-styled with persona avatar, last message preview, and relative timestamp. Context-menu swipe-to-delete. Empty state with CTA toPersonaPickerSheet.PersonaPickerSheet— Bottom sheet with 3-column grid of all 19 personas grouped into Social Friends / Task Agents. Tapping a cell callsgetOrCreateSessionthen navigates intoChatViewafter sheet dismissal.Features/Chats/Components/MessageBubble.swift— User messages: trailing accent-color bubble. AI messages: leading glass-card bubble with 30 pt persona initial avatar. Both support.textSelection(.enabled).Features/Chats/Components/MessageInputView.swift— MultilineTextField(1–5 lines) + send button. Send disabled when text is empty or AI is typing. Top divider overlay separates input from message list.Features/Chats/Components/TypingIndicator.swift— Three-dot bouncing animation shown whileisTyping. Each dot staggers by 130 ms usingrepeatForevereasing.Localizable.xcstrings— 12 new keys:chat_clear,chat_clear_confirm,chat_clear_message,chat_input_placeholder,chats_delete,chats_empty_desc,chats_empty_title,chats_new_chat,chats_no_messages,chats_yesterday,personas_social,personas_task.
Chat_Buddy_iOSApp.swift—ChatStoreadded as@Stateand injected via.environment(chatStore).RootTabView.swift— Chats tab now rendersChatsView()instead of the former placeholder.
ChatsPlaceholderView.swift— Deleted; superseded byChatsView.
DashboardView— OLED black background now correctly activates when mode is.systemand the system appearance is dark; previously only triggered for explicit.darkmode. Added@Environment(\.colorScheme)+isEffectivelyDarkcomputed property.AccentColorManager—emeraldandamberpresets referenced the wrong localization key ("accent_default"); corrected to"accent_emerald"/"accent_amber".AccentColorPickerView— CustomColorPickernow initializes its@Statefrom the saved hex value on.onAppear; was always defaulting to.blueregardless of saved state.ThemeModePickerView— Removed redundant Section footer that re-displayed the view title as body text.AnimationIntensityView— Same redundant footer pattern removed.DashboardViewModel—dateStringcomputed property was creating a newDateFormatterinstance on every read; replaced with aprivate static letcached instance.
Localizable.xcstrings—accent_emerald(Emerald / 翡翠) andaccent_amber(Amber / 琥珀) translation keys added for bothenandzh-Hans.
DashboardView— Restructured layout:TodaysPickWidgetmoved out of theLazyVGridinto a standalone full-width hero card below the 2×2 bento grid. Greeting header gains a tinted profile icon (.person.crop.circle.fill).TodaysPickWidget— Redesigned as a horizontal hero card: layered avatar circle withstrokeBorderaccent ring, persona name intitle3weight, prominent CTA chevron (chevron.right.circle.fill).FriendsWidget— Stacked avatars now carrysystemBackgroundseparation rings (standard iOS overlap treatment); trimmed to 4 visible avatars + overflow counter; persona total displayed below.BentoCardView— Added.frame(minHeight: 120)to ensure visual consistency across paired grid rows regardless of content height.GlassCard— Added subtle0.12opacity whitestrokeBorderoverlay for card edge definition in light and dark modes.OnboardingView— Feature icons now rendered on layered concentric circle backdrops for visual depth. Page indicator upgraded from plainCircledots to animated pill-shapedCapsuleindicators withspringanimation (active pip expands to 24 pt wide).
Localizable.xcstrings— String Catalog with ~80 core keys in English and 简体中文LocalizationManager—@Observablemanager for runtime language switching without app restartAppLanguageenum:system(auto-detect fromLocale.preferredLanguages),en,zh-HansAILanguageenum:auto(follows UI language),en,zhLocalizationManager.t(_:params:)— translation helper with{param}interpolationString+Interpolation.swift—String.interpolating(_:)extension
APIConfig— Codable model:baseURL,apiKey,model,temperature,timeout,maxRetriesAPIClient—actorwith URLSession async/await, exponential backoff retry (429 + 5xx)AIClient.shared— singleton for OpenAI-compatible/chat/completionsAPIConfigStore—@Observablestore with profile CRUD, persisted to UserDefaultsAPIConfigValidator— measures connection latency, returnsResult<Int, Error>ChatMessage/ChatCompletionRequest/ChatCompletionResponse— OpenAI-compatible Codable typesAPIProfile— named, saveable API configuration snapshotStorageService—UserDefaultswrapper withchat-buddy:namespace prefixDataExporter/DataImporter— JSON backup/restore via SwiftUIfileExporter/fileImporter
DesignTokens—DSTypography,DSSpacing,DSRadius,DSShadow,DSIconSizeconstantsThemeManager—@Observable:ThemeMode(system/light/dark), OLED pure black,AnimationIntensityAccentColorManager—@Observable: 10 preset colors + customColorPicker, persistedLiquidGlassModifiers—.liquidGlass()ViewModifier (iOS 26.glassEffect, fallback.ultraThinMaterial)Color(hex:)— hex string initializer (3/6/8 digit)AppTab— 4-tab enum: Dashboard, Chats, Moments, SettingsRootTabView— iOS 26 Liquid Glass tab barOnboardingView— 4-page.pagestyle TabView tutorial with skip/back/nextAppState—@Observableonboarding completion stateGlassCard,BentoCardView,SettingRow— shared reusable card componentsDashboardView— 2-columnLazyVGridbento layout with time-based greeting- Dashboard widgets: RecentChats, Stats, QuickActions, TodaysPick, Friends
DashboardViewModel— time-based greeting, today's pick persona selectionPersonaStore— static data: 13 social companions + 6 task agents (ported from web)SettingsView— grouped Form (Appearance, Language, API, Data, About)ThemeModePickerView,OLEDToggleView,AccentColorPickerView,AnimationIntensityViewLanguagePickerView,AILanguagePickerViewAPIConfigView— Form with URL/key/model/temperature + test buttonAPIProfileListView— list with swipe-to-delete + loadConnectivityTestView— live latency test result displayExportImportView— fileExporter/fileImporter integrationAboutView— version info, app taglineChatsPlaceholderView,MomentsPlaceholderView— coming-soon tabs
- Targets iOS 26.2, compiled with Xcode 26.2
- Build passes with 0 errors, 0 warnings
- File System Synchronized project — new Swift files auto-included
- Default global actor isolation (
@MainActor) enabled via Xcode 26 project settings