Skip to content

[SM PROTOTYPE - DO NOT MERGE] Group Desktop - #1237

Draft
shanemac wants to merge 5 commits into
devfrom
codex/design-system-foundation
Draft

[SM PROTOTYPE - DO NOT MERGE] Group Desktop#1237
shanemac wants to merge 5 commits into
devfrom
codex/design-system-foundation

Conversation

@shanemac

@shanemac shanemac commented Jul 28, 2026

Copy link
Copy Markdown

Summary

Builds the new Group Desktop vision on top of a reusable Convos design-system foundation.

  • adds the action-first Group Desktop as the native Things experience
  • keeps people, shared context, work, and next actions visible behind group chat
  • routes desktop actions into group chat or a private side chat with the group agent
  • adds native message actions for research, reminders, Things, connected services, and private agent work
  • adds the group Agents directory, shared credits/limits, and bring-your-own-AI entry points
  • exposes inbound group email and SMS when an agent publishes them
  • retains the broader typography, spacing, surface, component, accessibility, and design-catalog foundation already on this branch

Why

The first experience should make a Convo feel useful before anyone understands agents. Messages become work, the work returns to a shared place, and every result invites the group to add context, edit, research, or keep improving it. Agents remain visible and attributable without turning the product into an agent-management interface.

User impact

A group now has three connected spaces:

  1. group chat for natural conversation
  2. Things as the shared group desktop and memory
  3. a private side chat for doing work quietly with the group agent

The desktop is action-oriented, shows who and what can help, and makes connected services part of the group rather than a separate settings concept.

Validation

  • native signed iPhone build succeeded
  • corrected preview launched and remained running on a physical iPhone
  • SwiftFormat passed on all changed Swift files
  • SwiftLint pre-commit and pre-push checks passed
  • git diff --check passed
  • Firebase PR distribution workflow runs on this branch update

Review notes

This remains a draft product exploration. The new screens intentionally use the existing iOS visual language and shared design tokens while rethinking the product model around the Group Desktop.

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown

Claude finished @shanemac's task —— View job


PR Review Complete

I've completed a comprehensive review of PR #1237. This is a significant prototype that introduces the Group Desktop experience alongside a well-designed design system foundation. Below are my findings organized by severity.


🔴 Critical Issues

1. Work Menu Layout Broken on Standard Devices

Location: MessageContextMenuOverlay.swift:337

The endBubbleRect function double-counts C.verticalBreathingRoom, producing negative maxContentHeight on typical iPhones:

  • iPhone with 852pt screen: maxContentHeight = -13pt
  • iPhone with 667pt screen: maxContentHeight = -151pt

This causes the bubble preview to scale by a negative ratio, breaking the entire work menu layout. This is already flagged by Macroscope.

Fix: Remove the duplicate verticalBreathingRoom subtraction and clamp maxContentHeight to a positive minimum.

2. Things Page Shows Group UI for 1-on-1 Conversations

Location: ConversationView.swift:991

The thingsPage property unconditionally returns GroupDesktopView for all conversations, including 1-on-1s. This means:

  • Users lose their prior Things page
  • 1-on-1 conversations show group-only content (member lists, agent setup, etc.)
  • UI implies group features that don't exist for that conversation

Fix: Gate GroupDesktopView to conversation.isGroupConversation or preserve the existing files/links page for non-groups. Macroscope also identified this.

3. Third Pager Page Always Present

Location: ConversationView.swift:573

Every ConversationView mounts a third pager page for the group-agent side chat, even when there's no agent. Users can swipe/tap into a placeholder "Mountain Guide" chat surface that's not backed by a real agent. The nil guard in openAgentSideChat only blocks sending, not direct pager navigation.

Fix: Conditionally include the agent page only when groupAgent != nil. Macroscope identified this too.


🟡 High-Priority Issues

4. Hard-Coded Demo Content in Production Views

Location: GroupDesktopView.swift (lines 3, 348-424, 454-465)

The sharedWork and activity sections render fixed Tahoe demo content for every group:

  • "Cabins under $250" with Google Docs status
  • Fixed "Ask Zoe/Jimmy" prompts
  • Activity rows for Shane, Brent, Julie, and Kai

Any group opening Things sees this fabricated content as if it's theirs. These rows are tappable and route into real flows (outputs, notes, reminders).

Fix: Populate from actual conversation data or show an explicit empty/onboarding state. Macroscope identified this.

5. Work Menu Button Shows X but Doesn't Close

Location: MessageContextMenuWrapper.swift:110-119

When isSourceBubble is true, the work button shows an xmark icon but tapping it calls contextMenuState.present(...) again instead of dismiss(). The apparent close button re-presents the menu.

Fix: Call contextMenuState.dismiss() when isSourceBubble is true. Macroscope identified this.

6. Work Menu Not Updated on Membership Changes

Location: ConversationView.swift:600

contextMenuState.isWorkMenuEnabled is set only in .onAppear from viewModel.conversation.isGroupConversation. If membership changes while the conversation stays open:

  • A conversation becoming a group keeps work actions disabled
  • A conversation dropping below group threshold retains them enabled

Fix: Update isWorkMenuEnabled via .onChange(of: viewModel.conversation.members). Macroscope identified this.

7. Type-Check Performance Risk

Location: GroupDesktopView.swift (entire body)

The view body is 729 lines with:

  • Multiple deeply nested VStack/HStack/LazyVStack hierarchies
  • Inline conditional expressions in modifier arguments
  • Repeated button handlers with inline closures

This structure is extremely close to triggering the 100ms type-check budget (repository's -warn-long-expression-type-checking 100 setting). According to CLAUDE.md, the threshold is a cliff — chains at 80-95ms trip over with minor additions.

Recommendation: Extract sections like sharedWork, activity, directory, and inbound into @ViewBuilder computed properties or separate view components.


🟠 Medium-Priority Issues

8. Start Chip Titles Ignored

Location: GroupDesktopView.swift:162

All startChip buttons ("A trip", "My family", "Book club", etc.) emit the same .describeGroup action, discarding the chip title. The selected purpose never reaches the handler, so distinct choices produce identical behavior.

Fix: Pass the title through Action (e.g., via an associated value like .describeGroup(String)). Macroscope identified this.

9. "Tahoe Weekend" Fallback for Unnamed Groups

Location: GroupDesktopView.swift:47-49

The groupName property uses the hardcoded literal "Tahoe Weekend" for any unnamed/whitespace-only group name. This mislabels all unnamed groups as Tahoe trips, even though unnamed groups are a supported state (the codebase uses "this group" elsewhere).

Fix: Use a neutral placeholder like "This group" consistent with the rest of the app. Macroscope identified this.

10. "Ask Zoe" Text Not a Separate Button

Location: GroupDesktopView.swift:388-395

The Ask Zoe text in sharedWork is accent-colored and reads as an interactive action, but it's plain text inside the card Button whose handler always calls onAction(.openOutputs). Tapping it opens the work log instead.

Fix: Make Ask Zoe a separately wired Button or remove the accent treatment. Macroscope identified this.

11. Lost Draft When No Agent

Location: ConversationView.swift:1072

When the work menu's .askAgentPrivately action is used in a group with no agent, openAgentSideChat presents the agent builder but discards the composed draft. After the user adds an agent, the selected message and requested action are lost.

Fix: Preserve the draft or show a prompt before presenting the builder. Macroscope identified this.

12. agentPhone Fallback Logic Bug

Location: Profile.swift:276-283

agentPhone reads phone and falls back to phone_number via ?? before trimming. When phone is whitespace-only and phone_number holds a valid number, the whitespace value wins and is then coerced to nil — the valid fallback is never checked.

Fix: Move trim/empty-coercion inside each branch so the fallback triggers when the first key is actually absent. Macroscope identified this with a suggested fix.


🟢 Low-Priority Issues

13. Duplicate Backlog Item

Location: design-system-audit.md:153

The migration backlog lists "A debug-only route to the catalog for device review" under "Add later when build tooling is available," but line 122 already records that route as introduced. Macroscope identified this.


✅ Design System Foundation — Strong Implementation

The design system foundation is well-architected and follows best practices:

Strengths:

  • Clean separation of concerns (foundations, styles, components)
  • Semantic naming throughout (colors, typography, surfaces)
  • Excellent documentation in design-system.md and ADR 015
  • Light/dark mode support baked in
  • Accessibility considerations (Dynamic Type, minimum tap targets, semantic labels)
  • Visual catalog for review (DesignSystemCatalogView)
  • Gradual adoption strategy documented

Architecture:

  • Foundations in DesignConstants (spacing, typography, layout metrics)
  • Semantic colors in asset catalog
  • Styles as view modifiers (convosTextStyle, convosSurface, convosButtonStyle)
  • Components as reusable views (ConvosBadge, ConvosIconTile, ConvosSettingsRow, etc.)

This is a solid foundation that will improve consistency and maintainability going forward.


🔍 Test Coverage Assessment

Concerns:

  • No test files changed in this PR
  • 3000+ lines of new UI code with zero accompanying tests
  • Group Desktop has complex state management (agents, members, connections) but no unit tests
  • Message work menu integration has no tests for the new work actions
  • Design system components have no snapshot tests

Recommendations:

  1. Add unit tests for GroupDesktopView.Action routing logic
  2. Add tests for work menu presentation and dismissal flows
  3. Add tests for the agent side chat draft preservation
  4. Consider snapshot tests for the design system catalog
  5. Add integration tests for the three-pager (chat, things, agent) navigation

🔒 Security Assessment

No major security concerns identified. The PR:

  • Uses existing authentication/authorization patterns
  • Doesn't introduce new data persistence mechanisms
  • Doesn't add new network endpoints
  • Properly uses the existing XMTP SDK abstraction

Minor note: The inbound email/SMS addresses in GroupDesktopView are read from agent profile metadata, which is already validated by the backend.


⚡ Performance Considerations

  1. Type-check budget risk (Onboarding Auth Flow #7 above) — the 729-line view body is dangerously close to the 100ms threshold
  2. LazyVStack usage in GroupDesktopView is appropriate for the long scrolling content
  3. Image/avatar rendering uses existing cached infrastructure (AvatarView)
  4. No observable performance regressions in the changed code itself

Recommendation: Extract large sections into computed properties/subviews before adding more features to GroupDesktopView.


📋 Code Quality

Positives:

  • Follows SwiftUI best practices (extracting button actions, using @ViewBuilder)
  • Consistent naming conventions
  • Good use of semantic design tokens
  • Clean component composition

Areas for improvement:

  • Hard-coded demo content should be removed before merge (even for a prototype)
  • Some view methods are long but not yet problematic
  • Missing doc comments on public components (though the design-system.md compensates)

Summary

This PR introduces a well-designed Group Desktop experience and a strong design system foundation. However, it has 3 critical bugs that break core functionality:

  1. Work menu layout broken on standard phones
  2. Group UI shown for 1-on-1 conversations
  3. Agent side chat page always present

Additionally, all production groups show fixed Tahoe demo content, which is confusing and misleading.

The design system itself is excellent and ready to adopt incrementally. The Macroscope review identified most of these issues, and I've provided context and prioritization.

Recommendation: Fix the 3 critical issues and remove hard-coded demo content before merging, even as a prototype. The rest can be addressed in follow-up work.


Todo List:

  • Read repository guidelines (CLAUDE.md)
  • Review Macroscope feedback for context
  • Analyze main conversation view changes
  • Review new GroupDesktopView implementation
  • Review message context menu work integration
  • Review design system foundation
  • Check for performance issues
  • Check for security concerns
  • Assess test coverage
  • Synthesize findings and post review

- Preview snapshots for the catalog in light/dark and accessibility sizes
- A lint rule that flags new raw RGB colors and arbitrary layout values
- Accessibility UI checks for tap targets and clipped text
- A debug-only route to the catalog for device review, if the team wants it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low docs/design-system-audit.md:153

The migration backlog lists "A debug-only route to the catalog for device review" under "Add later when build tooling is available," but the same document already records that route as introduced in the framework inventory on line 122. This leaves maintainers with a false outstanding task that contradicts the documented current state. Remove the duplicate backlog item or update it to reflect that the route already exists.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @docs/design-system-audit.md around line 153:

The migration backlog lists "A debug-only route to the catalog for device review" under "Add later when build tooling is available," but the same document already records that route as introduced in the framework inventory on line 122. This leaves maintainers with a false outstanding task that contradicts the documented current state. Remove the duplicate backlog item or update it to reflect that the route already exists.

@shanemac shanemac changed the title feat(design-system): add app-wide foundation and catalog feat: establish design system and group desktop experience Jul 28, 2026
"Connect this group to the service needed for this message from \(sender): \(context)\n\n"
+ "Explain why the connection helps and ask for the minimum access needed."
)
case .askAgentPrivately:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/ConversationView.swift:1072

When the work menu's .askAgentPrivately action is used in a group with no agent, openAgentSideChat presents the agent builder but discards the composed draft, so after the user adds an agent the selected message and requested private-agent action are lost. The work menu is enabled for any qualifying group regardless of whether an agent exists, so this path is reachable.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/ConversationView.swift around line 1072:

When the work menu's `.askAgentPrivately` action is used in a group with no agent, `openAgentSideChat` presents the agent builder but discards the composed draft, so after the user adds an agent the selected message and requested private-agent action are lost. The work menu is enabled for any qualifying group regardless of whether an agent exists, so this path is reachable.

)
}

var thingsPage: some View {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Conversation Detail/ConversationView.swift:991

thingsPage now unconditionally renders GroupDesktopView for every conversation, so one-on-one conversations lose their prior Things page and instead show group-only content and controls (member lists, agent-side-chat, group setup actions). The property returns GroupDesktopView without checking isGroupConversation. Gate this replacement to group conversations or preserve the existing files/links page for non-groups.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/ConversationView.swift around line 991:

`thingsPage` now unconditionally renders `GroupDesktopView` for every conversation, so one-on-one conversations lose their prior Things page and instead show group-only content and controls (member lists, agent-side-chat, group setup actions). The property returns `GroupDesktopView` without checking `isGroupConversation`. Gate this replacement to group conversations or preserve the existing files/links page for non-groups.

@@ -622,10 +600,12 @@ struct ConversationView<MessagesBottomBar: View>: View {
.onAppear {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/ConversationView.swift:600

contextMenuState.isWorkMenuEnabled is set only in .onAppear from viewModel.conversation.isGroupConversation, so it never updates when membership changes while the conversation stays open. A conversation that becomes a group after appearing keeps group work menu actions disabled, and one that drops below the group threshold retains them enabled, until the view is left and re-entered. Consider recomputing isWorkMenuEnabled on membership changes (e.g., via .onChange(of: viewModel.conversation.members)).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/ConversationView.swift around line 600:

`contextMenuState.isWorkMenuEnabled` is set only in `.onAppear` from `viewModel.conversation.isGroupConversation`, so it never updates when membership changes while the conversation stays open. A conversation that becomes a group after appearing keeps group work menu actions disabled, and one that drops below the group threshold retains them enabled, until the view is left and re-entered. Consider recomputing `isWorkMenuEnabled` on membership changes (e.g., via `.onChange(of: viewModel.conversation.members)`).

Comment on lines +276 to +283
public var agentPhone: String? {
let value = metadata?[Constant.phoneKey]?.stringValue
?? metadata?[Constant.phoneNumberKey]?.stringValue
return value.flatMap { rawValue in
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Models/Profile.swift:276

agentPhone reads phone and falls back to phone_number via ?? before trimming, so when phone is present but whitespace-only and phone_number holds a valid number, the whitespace value wins and is then coerced to nil — the valid fallback is never checked. During a rollout where both keys coexist, this hides a valid SMS address. Move the trim/empty-coercion inside each branch so the fallback only triggers when the first key is actually absent rather than whitespace-only.

Suggested change
public var agentPhone: String? {
let value = metadata?[Constant.phoneKey]?.stringValue
?? metadata?[Constant.phoneNumberKey]?.stringValue
return value.flatMap { rawValue in
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
public var agentPhone: String? {
let trim: (String?) -> String? = { rawValue in
rawValue.flatMap { value in
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
return trim(metadata?[Constant.phoneKey]?.stringValue)
?? trim(metadata?[Constant.phoneNumberKey]?.stringValue)
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @ConvosCore/Sources/ConvosCore/Storage/Models/Profile.swift around lines 276-283:

`agentPhone` reads `phone` and falls back to `phone_number` via `??` *before* trimming, so when `phone` is present but whitespace-only and `phone_number` holds a valid number, the whitespace value wins and is then coerced to `nil` — the valid fallback is never checked. During a rollout where both keys coexist, this hides a valid SMS address. Move the trim/empty-coercion inside each branch so the fallback only triggers when the first key is actually absent rather than whitespace-only.

@saulmc saulmc changed the title feat: establish design system and group desktop experience [SM PROTOTYPE - DO NOT MERGE] Group Desktop Jul 28, 2026
@@ -0,0 +1,698 @@
import ConvosCore
import SwiftUI
import UIKit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupDesktopView.swift:3

sharedWork and activity render hard-coded Tahoe demo content for every conversation. sharedWork always shows a fixed cabin comparison with Google Docs status, Zoe/Jimmy prompts, and suggestions, while activity always shows fixed rows for Shane, Brent, Julie, and Kai. Any real or newly created group opening the Things page will present this fabricated work and activity as if it belonged to that group, and these rows are tappable, routing users into the group's real outputs/notes/reminders/agents flows as if that content existed. These sections should be populated from the actual conversation data or replaced with an explicit empty/onboarding state.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupDesktopView.swift around line 3:

`sharedWork` and `activity` render hard-coded Tahoe demo content for every conversation. `sharedWork` always shows a fixed cabin comparison with Google Docs status, Zoe/Jimmy prompts, and suggestions, while `activity` always shows fixed rows for Shane, Brent, Julie, and Kai. Any real or newly created group opening the Things page will present this fabricated work and activity as if it belonged to that group, and these rows are tappable, routing users into the group's real outputs/notes/reminders/agents flows as if that content existed. These sections should be populated from the actual conversation data or replaced with an explicit empty/onboarding state.

.foregroundStyle(.secondary)
}

HStack {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupDesktopView.swift:388

The Ask Zoe text in sharedWork is not a separate button — it is plain text inside the card Button whose handler always calls onAction(.openOutputs). Tapping the Ask Zoe text therefore opens the work log instead of performing the ask action that the label implies. Consider making Ask Zoe a separately wired Button (e.g., with .askEveryone) or removing the accent-colored action treatment so it does not read as a distinct interactive action.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupDesktopView.swift around line 388:

The `Ask Zoe` text in `sharedWork` is not a separate button — it is plain text inside the card `Button` whose handler always calls `onAction(.openOutputs)`. Tapping the `Ask Zoe` text therefore opens the work log instead of performing the ask action that the label implies. Consider making `Ask Zoe` a separately wired `Button` (e.g., with `.askEveryone`) or removing the accent-colored action treatment so it does not read as a distinct interactive action.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

https://github.com/xmtplabs/convos-ios/blob/9b26cfeaf11c24d74312e918b6350559b5fbf0d0/Convos/Conversation Detail/ConversationView.swift#L573

Every ConversationView mounts a third pager page for a group-agent side chat, including one-to-one conversations and conversations with no agent. When groupAgent is nil the agentPage closure still builds a side-chat view with a placeholder "Mountain Guide" agent, and ConversationPager unconditionally renders all three pages with three page dots, so users can swipe or tap into a group-agent surface backed by no real agent. The nil guard in openAgentSideChat only blocks the send button, not direct pager navigation. Consider suppressing the agent page (and its dot) for conversations that don't have a group agent, falling back to the prior two-page pager.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/ConversationView.swift around line 573:

Every `ConversationView` mounts a third pager page for a group-agent side chat, including one-to-one conversations and conversations with no agent. When `groupAgent` is nil the `agentPage` closure still builds a side-chat view with a placeholder "Mountain Guide" agent, and `ConversationPager` unconditionally renders all three pages with three page dots, so users can swipe or tap into a group-agent surface backed by no real agent. The nil guard in `openAgentSideChat` only blocks the send button, not direct pager navigation. Consider suppressing the `agent` page (and its dot) for conversations that don't have a group agent, falling back to the prior two-page pager.

Comment on lines +110 to +119
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
contextMenuState.present(
message: message,
bubbleFrame: currentBubbleFrame,
bubbleStyle: bubbleStyle,
isExpanded: isExpanded,
segment: segment,
presentation: .work
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium MessagesListView/MessageContextMenuWrapper.swift:110

The work button shows an xmark icon when isSourceBubble is true, but tapping it calls contextMenuState.present(...) again instead of dismiss(). Tapping the apparent close control re-presents the same work menu rather than closing it, so the menu can't be dismissed via the button. Consider calling contextMenuState.dismiss() when isSourceBubble is true.

Suggested change
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
contextMenuState.present(
message: message,
bubbleFrame: currentBubbleFrame,
bubbleStyle: bubbleStyle,
isExpanded: isExpanded,
segment: segment,
presentation: .work
)
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
if isSourceBubble {
contextMenuState.dismiss()
} else {
contextMenuState.present(
message: message,
bubbleFrame: currentBubbleFrame,
bubbleStyle: bubbleStyle,
isExpanded: isExpanded,
segment: segment,
presentation: .work
)
}
} label: {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @ConvosCore/Sources/ConvosComposer/MessagesListView/MessageContextMenuWrapper.swift around lines 110-119:

The work button shows an `xmark` icon when `isSourceBubble` is true, but tapping it calls `contextMenuState.present(...)` again instead of `dismiss()`. Tapping the apparent close control re-presents the same work menu rather than closing it, so the menu can't be dismissed via the button. Consider calling `contextMenuState.dismiss()` when `isSourceBubble` is true.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High

let bottomPadding: CGFloat = C.sectionSpacing + menuHeight + C.verticalBreathingRoom

endBubbleRect double-counts the work menu height, producing a negative maxContentHeight and an invalid preview on standard devices. When state.presentation == .work, menuHeight is set to C.workMenuEstimatedHeight (510) and passed in. Inside endBubbleRect, bottomPadding includes menuHeight plus C.verticalBreathingRoom (80), and then maxContentHeight subtracts an additional C.verticalBreathingRoom:

maxContentHeight = screenSize.height - minY - (sectionSpacing + menuHeight + verticalBreathingRoom) - verticalBreathingRoom

On an 852pt iPhone with a 59pt safe top this is 852 - 187 - 598 - 80 = -13; on a 667pt device it is -151. The endHeight > maxContentHeight branch then scales endWidth/endHeight by a negative ratio, and the work menu is positioned from that invalid endBubble. The work menu layout is therefore broken on normal-sized phones. Remove the duplicate C.verticalBreathingRoom subtraction from maxContentHeight, and clamp maxContentHeight to a positive minimum so the preview never receives negative geometry on compact screens.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @ConvosCore/Sources/ConvosComposer/MessagesListView/MessageContextMenu/MessageContextMenuOverlay.swift around line 337:

`endBubbleRect` double-counts the work menu height, producing a negative `maxContentHeight` and an invalid preview on standard devices. When `state.presentation == .work`, `menuHeight` is set to `C.workMenuEstimatedHeight` (510) and passed in. Inside `endBubbleRect`, `bottomPadding` includes `menuHeight` plus `C.verticalBreathingRoom` (80), and then `maxContentHeight` subtracts an additional `C.verticalBreathingRoom`:

`maxContentHeight = screenSize.height - minY - (sectionSpacing + menuHeight + verticalBreathingRoom) - verticalBreathingRoom`

On an 852pt iPhone with a 59pt safe top this is `852 - 187 - 598 - 80 = -13`; on a 667pt device it is `-151`. The `endHeight > maxContentHeight` branch then scales `endWidth`/`endHeight` by a negative ratio, and the work menu is positioned from that invalid `endBubble`. The work menu layout is therefore broken on normal-sized phones. Remove the duplicate `C.verticalBreathingRoom` subtraction from `maxContentHeight`, and clamp `maxContentHeight` to a positive minimum so the preview never receives negative geometry on compact screens.

) {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
startChip("A trip")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupDesktopView.swift:162

The startChip("A trip"), startChip("My family"), and other chips all emit the same .describeGroup action, discarding the chip title. Tapping any starting point produces identical behavior — the selected group purpose never reaches the handler, so these distinct onboarding choices do not perform their advertised selection.

startChip ignores its title parameter and always calls onAction(.describeGroup). Consider passing the title through Action (e.g., via an associated value like .describeGroup(String)) so the handler receives the user's selection.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupDesktopView.swift around line 162:

The `startChip("A trip")`, `startChip("My family")`, and other chips all emit the same `.describeGroup` action, discarding the chip title. Tapping any starting point produces identical behavior — the selected group purpose never reaches the handler, so these distinct onboarding choices do not perform their advertised selection.

`startChip` ignores its `title` parameter and always calls `onAction(.describeGroup)`. Consider passing the title through `Action` (e.g., via an associated value like `.describeGroup(String)`) so the handler receives the user's selection.

Comment on lines +47 to +49
let name = conversation.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return name.isEmpty ? "Tahoe Weekend" : name
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupDesktopView.swift:47

The groupName fallback uses the hardcoded literal "Tahoe Weekend" whenever the conversation has no name or a whitespace-only name. This makes the people header mislabel any unnamed group as a Tahoe trip, even though unnamed groups are a supported state (the codebase uses "this group" elsewhere for the same condition). Consider using a neutral placeholder consistent with the rest of the app instead of the hardcoded demo string.

Suggested change
let name = conversation.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return name.isEmpty ? "Tahoe Weekend" : name
}
let name = conversation.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return name.isEmpty ? "This group" : name
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupDesktopView.swift around lines 47-49:

The `groupName` fallback uses the hardcoded literal `"Tahoe Weekend"` whenever the conversation has no name or a whitespace-only name. This makes the people header mislabel any unnamed group as a Tahoe trip, even though unnamed groups are a supported state (the codebase uses `"this group"` elsewhere for the same condition). Consider using a neutral placeholder consistent with the rest of the app instead of the hardcoded demo string.

agentAvatar(agent, isGroupAgent: index == 0)

VStack(alignment: .leading, spacing: 3) {
Text(agent.profile.displayName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupAgentsView.swift:88

The directory row renders agent.profile.displayName directly instead of agent.displayName, so an unverified agent can display its raw profile-supplied name instead of the intentional "Agent" fallback. This bypasses the ConversationMember.displayName guard that prevents unverified agents from presenting arbitrary names, allowing impersonation in the agent directory. The same raw name is also used for the avatar initial (agentAvatar) and groupAgentName. Consider using agent.displayName in all three places.

Suggested change
Text(agent.profile.displayName)
Text(agent.displayName)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupAgentsView.swift around line 88:

The directory row renders `agent.profile.displayName` directly instead of `agent.displayName`, so an unverified agent can display its raw profile-supplied name instead of the intentional "Agent" fallback. This bypasses the `ConversationMember.displayName` guard that prevents unverified agents from presenting arbitrary names, allowing impersonation in the agent directory. The same raw name is also used for the avatar initial (`agentAvatar`) and `groupAgentName`. Consider using `agent.displayName` in all three places.

Comment on lines +42 to +44
private var agentName: String {
agents.first?.profile.displayName ?? "Mountain Guide"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupDesktopView.swift:42

agentName falls back to the hardcoded string "Mountain Guide" when the group has no agent, so the entire screen refers to a nonexistent agent — e.g. "tell Mountain Guide anything", "Mountain Guide can privately ask each person", and "ask Mountain Guide to send something" — and surfaces agent-dependent actions like "Research it" and "Ask everyone" as if an agent exists. A group with only human members still reaches this view, so the fallback presents a fabricated agent name and agent-dependent CTAs instead of a no-agent/setup state. Consider falling back to an empty or neutral label (or gating the agent-dependent sections) when agents is empty, so the UI doesn't reference an agent that doesn't exist.

Suggested change
private var agentName: String {
agents.first?.profile.displayName ?? "Mountain Guide"
}
private var agentName: String {
agents.first?.profile.displayName ?? ""
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupDesktopView.swift around lines 42-44:

`agentName` falls back to the hardcoded string `"Mountain Guide"` when the group has no agent, so the entire screen refers to a nonexistent agent — e.g. "tell Mountain Guide anything", "Mountain Guide can privately ask each person", and "ask Mountain Guide to send something" — and surfaces agent-dependent actions like "Research it" and "Ask everyone" as if an agent exists. A group with only human members still reaches this view, so the fallback presents a fabricated agent name and agent-dependent CTAs instead of a no-agent/setup state. Consider falling back to an empty or neutral label (or gating the agent-dependent sections) when `agents` is empty, so the UI doesn't reference an agent that doesn't exist.

.font(.headline)
.foregroundStyle(.colorTextPrimary)

Text(index == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Conversation Detail/GroupAgentsView.swift:92

The agentDirectory section displays hardcoded ownership text ("Shane's Convos agent · Group use allowed" / "Shane's agent · Private power") regardless of which member actually owns each agent, and determines group-agent status solely from array position (index == 0). For any conversation whose agents are owned by other members, or whose first agent is private, the directory shows false ownership and access-permission information. ConversationMember.invitedBy is available but never consulted; the per-row label and the isGroupAgent flag should derive from member/verification/ownership data instead of a fixed name and index.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @Convos/Conversation Detail/GroupAgentsView.swift around line 92:

The `agentDirectory` section displays hardcoded ownership text ("Shane's Convos agent · Group use allowed" / "Shane's agent · Private power") regardless of which member actually owns each agent, and determines group-agent status solely from array position (`index == 0`). For any conversation whose agents are owned by other members, or whose first agent is private, the directory shows false ownership and access-permission information. `ConversationMember.invitedBy` is available but never consulted; the per-row label and the `isGroupAgent` flag should derive from member/verification/ownership data instead of a fixed name and index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant