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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### 🎨 Design

- Redesign the Mori sidebar as an attention inbox with Needs You and Running agent sections, filter pills, unified status dots, idle project clustering, and a single-tile collapsed rail.

## [0.4.7] - 2026-06-07

### ✨ Features
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

## [Unreleased]

### 🎨 界面优化

- 将 Mori 侧边栏重设计为注意力收件箱:新增 Needs You 与 Running agent 区块、筛选胶囊、统一状态圆点、空闲项目收纳,以及单 tile 折叠 rail。

## [0.4.7] - 2026-06-07

### ✨ 新功能
Expand Down
9 changes: 6 additions & 3 deletions Packages/MoriUI/Sources/MoriUI/ProjectLetterTile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import MoriCore
/// colour-match rather than a text-read.
struct ProjectLetterTile: View {
let project: Project
var size: CGFloat = MoriTokens.Size.projectTile
var cornerRadius: CGFloat = MoriTokens.Radius.projectTile
var fontSize: CGFloat = 10

var body: some View {
let pair = MoriTokens.ProjectPalette.pair(for: project.id)
ZStack {
RoundedRectangle(cornerRadius: MoriTokens.Radius.projectTile)
RoundedRectangle(cornerRadius: cornerRadius)
.fill(pair.background)
Text(letter)
.font(MoriTokens.Font.projectTile)
.font(.system(size: fontSize, weight: .semibold, design: .monospaced))
.foregroundStyle(pair.foreground)
}
.frame(width: MoriTokens.Size.projectTile, height: MoriTokens.Size.projectTile)
.frame(width: size, height: size)
.accessibilityHidden(true)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,12 @@
"No agents running" = "No agents running";
"Pin Project" = "Pin Project";
"Unpin Project" = "Unpin Project";

/* Sidebar attention inbox */
"Needs You" = "Needs You";
"All" = "All";
"%lld idle projects" = "%lld idle projects";
"reply sent — resuming…" = "reply sent — resuming…";
"sent" = "sent";
"Waiting for input" = "Waiting for input";
"now" = "now";
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,12 @@
"No agents running" = "没有运行中的代理";
"Pin Project" = "置顶项目";
"Unpin Project" = "取消置顶";

/* Sidebar attention inbox */
"Needs You" = "需要你处理";
"All" = "全部";
"%lld idle projects" = "%lld 个空闲项目";
"reply sent — resuming…" = "回复已发送 — 正在继续…";
"sent" = "已发送";
"Waiting for input" = "等待输入";
"now" = "现在";
51 changes: 30 additions & 21 deletions Packages/MoriUI/Sources/MoriUI/WorktreeRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,19 @@ public struct WorktreeRowView: View {
.fill(statusDotColor)
.frame(width: MoriTokens.Icon.dot, height: MoriTokens.Icon.dot)

Text(worktree.name)
.font(MoriTokens.Font.rowTitle)
.foregroundStyle(isSelected ? Color.primary : Color.primary.opacity(0.9))
Text(branchDisplayText)
.font(.system(size: 12, weight: isSelected ? .semibold : .regular, design: .monospaced))
.foregroundStyle(isSelected ? Color.primary : MoriTokens.Color.muted)
.lineLimit(1)

if isSelected {
selectedAgentLabel
}

Spacer(minLength: 0)

primaryBadge
if let timeText = relativeTimeText {
Text(timeText)
.font(MoriTokens.Font.monoSmall)
.foregroundStyle(MoriTokens.Color.inactive)
.lineLimit(1)
}

if isHovered {
overflowMenu
Expand All @@ -67,6 +68,14 @@ public struct WorktreeRowView: View {
RoundedRectangle(cornerRadius: MoriTokens.Radius.small)
.strokeBorder(rowOutlineColor, lineWidth: rowOutlineColor == .clear ? 0 : 1)
}
.overlay(alignment: .leading) {
if worktree.agentState == .none && worktree.status != .active {
Circle()
.strokeBorder(MoriTokens.Color.inactive.opacity(0.55), lineWidth: 1.5)
.frame(width: MoriTokens.Icon.dot, height: MoriTokens.Icon.dot)
.padding(.leading, MoriTokens.Spacing.lg)
}
}
.overlay(alignment: .leading) {
if isSelected {
// 2pt accent bar, inset 6pt top/bottom, rounded on the trailing edge.
Expand All @@ -87,10 +96,13 @@ public struct WorktreeRowView: View {
}

private var statusDotColor: Color {
if worktree.agentState == .error { return MoriTokens.Color.error }
if worktree.agentState == .waitingForInput { return MoriTokens.Color.attention }
if worktree.agentState == .running { return MoriTokens.Color.success }
if worktree.status == .active { return MoriTokens.Color.success }
return MoriTokens.Color.inactive
// Live session with no agent: solid muted dot ("warm"), so semantic
// colors stay reserved for agent activity.
if worktree.status == .active { return MoriTokens.Color.inactive }
return .clear
}

private var iconView: some View {
Expand Down Expand Up @@ -126,13 +138,6 @@ public struct WorktreeRowView: View {
.lineLimit(1)
}

if worktree.status == .active {
Circle()
.fill(MoriTokens.Color.success)
.frame(width: MoriTokens.Icon.dot, height: MoriTokens.Icon.dot)
.accessibilityLabel(String.localized("Active"))
}

if let timeText = relativeTimeText {
Text(timeText)
.font(MoriTokens.Font.caption)
Expand Down Expand Up @@ -251,13 +256,17 @@ public struct WorktreeRowView: View {
private var relativeTimeText: String? {
guard let date = worktree.lastActiveAt else { return nil }
let seconds = Int(-date.timeIntervalSinceNow)
if seconds < 60 { return String.localized("just now") }
if seconds < 3600 { return String.localized("\(seconds / 60)m ago") }
if seconds < 86400 { return String.localized("\(seconds / 3600)h ago") }
if seconds < 604_800 { return String.localized("\(seconds / 86400)d ago") }
if seconds < 60 { return String.localized("now") }
if seconds < 3600 { return "\(seconds / 60)m" }
if seconds < 86400 { return "\(seconds / 3600)h" }
if seconds < 604_800 { return "\(seconds / 86400)d" }
return nil
}

private var branchDisplayText: String {
worktree.branch ?? worktree.name
}

private var gitSummaryText: String? {
var parts: [String] = []

Expand Down
Loading
Loading