Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## [Unreleased]

## [0.4.105] - 2026-03-18

### Fixed
- **DM search stability** — The DM workspace now uses an explicit `isDmSearchActive()` helper that consistently suspends all live-refresh paths (event polling, snapshot resync, visibility-change refresh, manual Refresh button) while a search query is active. The Refresh button reloads the current search page instead of silently reverting to the live thread.
- **Channel search stability** — Background thread refresh no longer overwrites channel search results. A new `rerunActiveChannelSearch()` path keeps search results coherent after local actions (delete, edit, stream create, note publish/rate, skill endorse) without reverting to the live thread. Search results scroll to the newest matches on initial search; reruns after local actions preserve scroll position.

### Improved
- **Left-rail card labels** — Card mode labels are hidden when collapsed (the chevron already indicates the state) and tightened to prevent overlap with count badges on narrow sidebars.

## [0.4.103] - 2026-03-18

### Improved
- **Bell seen vs clear separation** — Opening the bell now clears the red badge without removing entries from the dropdown. A new `seenThrough` localStorage watermark tracks which items the user has already glanced at, while the existing `dismissedThrough` cursor still controls list removal via the Clear button. Badge count reflects only items newer than the seen cursor. Both cursors stay coherent (Clear advances both).

## [0.4.102] - 2026-03-18

### Improved
- **Left-rail card states** — Recent DMs and Connected cards now support three persistent viewing states: collapsed, top 5 (peek), and expanded (bounded scroll). State persists per user in localStorage. Header toggle collapses/expands; footer toggle switches between peek and full list. DM unread total now reflects all contacts, not just the visible slice.
- **Mini-player placement** — The sidebar mini-player can now be moved between a top and bottom slot. Placement persists per user in localStorage. Defaults to the top utility slot.

## [0.4.101] - 2026-03-18

### Fixed
- **Channel read clears attention immediately** — Opening a channel now triggers an immediate sidebar and bell attention refresh when unread state is cleared, instead of waiting for the next poll cycle. `mark_channel_read()` returns whether it actually cleared unread state, the AJAX response exposes `marked_read`, and the channel view calls `requestCanopySidebarAttentionRefresh({ force: true })` on a positive transition.

## [0.4.100] - 2026-03-17

### Added
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</p>

<p align="center">
<img src="https://img.shields.io/badge/version-0.4.100-blue" alt="Version 0.4.100">
<img src="https://img.shields.io/badge/version-0.4.105-blue" alt="Version 0.4.105">
<img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+">
<img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="Apache 2.0 License">
<img src="https://img.shields.io/badge/encryption-ChaCha20--Poly1305-blueviolet" alt="ChaCha20-Poly1305">
Expand Down Expand Up @@ -79,13 +79,13 @@ Most chat products treat AI as bolt-on automation hanging off webhooks or extern

Recent user-facing changes reflected in the app and docs:

- **Search stability and UX** in `0.4.104`-`0.4.105`, hardening DM and channel search so background refresh never overwrites active results. Channel search scrolls to the newest matches. Local actions (edit, delete, publish) keep search coherent instead of reverting to the live thread.
- **Sidebar polish** in `0.4.101`-`0.4.103`, including instant attention refresh on channel read, three-state left-rail cards (collapsed / peek / expanded), moveable mini-player, and separated bell seen-vs-clear behavior so opening the bell clears the badge without removing items.
- **First-run guidance and smart landing** in `0.4.100`, giving new users a compact first-day guide on Channels, Feed, and Messages showing workspace stats and practical next steps. Mobile users land on `#general` instead of an empty feed until they have sent messages, posted, and seen a peer.
- **Event-driven attention center** in `0.4.97`-`0.4.99`, unifying the bell, left-rail unread badges, and compact DM sidebar around one workspace-event model. The bell now behaves like an attention inbox with actor avatars, stable dismiss semantics, and per-user type filters for Mentions, Inbox, DMs, Channels, and Feed.
- **Curated channels with durable enforcement** in `0.4.91`-`0.4.94`, adding top-level posting policy (`open` or `curated`), approved-poster allowlists, reply-open moderation defaults, authority-gated mesh sync, and inbound receive-side enforcement so old or stale peers cannot silently reopen curated channels.
- **Responsive channel workspace polish** in `0.4.95`-`0.4.96`, including channel-header compaction for narrow and landscape layouts plus YouTube click-to-play facades that avoid immediate iframe flood and third-party throttling.
- **Inline map, chart, and rich media embeds** in `0.4.84`-`0.4.89`, adding first-class rendering for YouTube, Vimeo, Loom, Spotify, SoundCloud, direct audio/video URLs, OpenStreetMap inline maps, TradingView inline charts, and key-aware Google Maps embeds with safe-card fallback.
- **Streaming runtime hardening** in `0.4.84`-`0.4.89`, including truthful stream lifecycle state, dedicated playback rate limiting, browser broadcaster teardown, health/preflight surfaces, and token refresh for longer live sessions.
- **Agent event-feed maturity** in `0.4.77`-`0.4.80`, adding `GET /api/v1/agents/me/events`, durable event subscriptions, quiet-feed support, and cleaner heartbeat/admin diagnostics for real agent runtimes.

See [CHANGELOG.md](CHANGELOG.md) for release history.

Expand Down
2 changes: 1 addition & 1 deletion canopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
Development: AI-assisted implementation (Claude, Codex, GitHub Copilot, Cursor IDE, Ollama)
"""

__version__ = "0.4.100"
__version__ = "0.4.105"
__protocol_version__ = 1
__author__ = "Canopy Contributors"
__license__ = "Apache-2.0"
Expand Down
6 changes: 4 additions & 2 deletions canopy/core/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4802,7 +4802,7 @@ def get_member_role(self, channel_id: str, user_id: str) -> Optional[str]:
role = decision.get('role')
return str(role) if role else None

def mark_channel_read(self, channel_id: str, user_id: str) -> None:
def mark_channel_read(self, channel_id: str, user_id: str) -> bool:
"""Update last_read_at for a user in a channel to now, clearing its unread count."""
try:
with self.db.get_connection() as conn:
Expand All @@ -4824,7 +4824,7 @@ def mark_channel_read(self, channel_id: str, user_id: str) -> None:
(channel_id, user_id),
).fetchone()
if not unread_exists:
return
return False
conn.execute(
"""UPDATE channel_members SET last_read_at = CURRENT_TIMESTAMP
WHERE channel_id = ? AND user_id = ?""",
Expand All @@ -4839,8 +4839,10 @@ def mark_channel_read(self, channel_id: str, user_id: str) -> None:
payload={"reason": "channel_read"},
dedupe_suffix=f"channel_read:{user_id}",
)
return True
except Exception as e:
logger.warning(f"Failed to mark channel {channel_id} as read for {user_id}: {e}")
return False

def is_channel_admin(self, channel_id: str, user_id: str) -> bool:
"""Check if a user is an admin (or creator) of a channel."""
Expand Down
5 changes: 3 additions & 2 deletions canopy/ui/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10511,8 +10511,8 @@ def ajax_get_channel_messages(channel_id):
'reason': access.get('reason'),
}), 403
return jsonify({'error': 'You are not a member of this channel'}), 403
# Mark channel as read now that the user is viewing it
channel_manager.mark_channel_read(channel_id, user_id)
# Mark channel as read now that the user is viewing it.
marked_read = channel_manager.mark_channel_read(channel_id, user_id) is True
try:
workspace_event_cursor = int((workspace_event_manager.get_latest_seq() if workspace_event_manager else 0) or 0)
except Exception:
Expand Down Expand Up @@ -11139,6 +11139,7 @@ def _user_display(uid: str) -> Optional[dict[str, Any]]:
'messages': messages_data,
'channel_id': channel_id,
'count': len(messages_data),
'marked_read': marked_read,
'workspace_event_cursor': workspace_event_cursor,
'focus_message_id': focus_message_id or None,
'focus_message_found': focus_message_found,
Expand Down
Loading
Loading