Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
3c3d259
chore(go-deps:backend)(deps): bump the go-dependencies group with 3 u…
dependabot[bot] Aug 24, 2026
465210a
chore(npm-deps:website)(deps-dev): bump vite
dependabot[bot] Aug 24, 2026
b08169d
chore(npm-deps:frontend)(deps-dev): bump the frontend-dependencies group
dependabot[bot] Aug 24, 2026
361be04
fix(search): explain AI results and open selected articles
marcomarcogd Aug 24, 2026
15b413f
fix(settings): update AI usage limit display immediately
marcomarcogd Aug 24, 2026
52e0a68
fix(database): retry locked article batch writes
marcomarcogd Aug 24, 2026
80340b6
feat(macos): add native SwiftUI client
YunFy26 Aug 24, 2026
eede0b8
fix(translation): make the free Google provider more reliable
YunFy26 Aug 24, 2026
298bf01
Merge remote-tracking branch 'upstream/main' into codex/swiftui-macos
YunFy26 Aug 24, 2026
b519a1b
fix(macos): show the app in the Dock and enable the Edit shortcuts
YunFy26 Aug 24, 2026
983fd64
fix(macos): give sidebar rows a visible selection
YunFy26 Aug 24, 2026
d47d2fa
fix(macos): lay out the Add Feed sheet without scrolling or crowding
YunFy26 Aug 24, 2026
c0f6068
feat(macos): group subscriptions into folders and colour their icons
YunFy26 Aug 24, 2026
c87d8fb
feat(macos): create a folder from the empty area of the sidebar
YunFy26 Aug 24, 2026
5a65b0f
feat(macos): drag a subscription onto a folder to file it
YunFy26 Aug 24, 2026
9df13da
fix(macos): make the drop half of folder filing work
YunFy26 Aug 24, 2026
5cd426c
feat(macos): reorder subscriptions by dropping between rows
YunFy26 Aug 24, 2026
70a1cab
release: Upgrade version to v1.3.28
WCY-dt Aug 24, 2026
e982951
Merge pull request #1024 from DevXDojo/dependabot/go_modules/go-depen…
WCY-dt Aug 24, 2026
01c494d
Merge pull request #1025 from DevXDojo/dependabot/npm_and_yarn/websit…
WCY-dt Aug 24, 2026
f2fd3f8
Merge pull request #1026 from DevXDojo/dependabot/npm_and_yarn/fronte…
WCY-dt Aug 24, 2026
ee185d2
feat(macos): open a gap under the drag instead of drawing a bar
YunFy26 Aug 24, 2026
9d0f334
fix(ui): prevent long toast messages from overflowing
marcomarcogd Aug 25, 2026
b97de0f
fix(feeds): restore feed management table layout
marcomarcogd Aug 25, 2026
fe4b56f
perf(macos): stop the sidebar stalling on drags and thumbnails
YunFy26 Aug 25, 2026
f3da9e7
refactor(macos): build the sidebar on NSOutlineView
YunFy26 Aug 25, 2026
d22fa74
doc: describe how to run the macOS client
YunFy26 Aug 25, 2026
0749c8b
test(macos): wait for work to finish instead of guessing how long it …
YunFy26 Aug 25, 2026
a304679
Merge pull request #1030 from marcomarcogd/codex/fix-ai-search-releva…
WCY-dt Aug 26, 2026
a67708d
Merge pull request #1031 from marcomarcogd/codex/fix-ai-usage-limit-l…
WCY-dt Aug 26, 2026
69453da
Merge pull request #1032 from marcomarcogd/codex/fix-article-batch-lo…
WCY-dt Aug 26, 2026
fcd35db
Merge pull request #1037 from marcomarcogd/codex/fix-toast-overflow
WCY-dt Aug 26, 2026
50153af
doc: Update changelog
WCY-dt Aug 26, 2026
d1f9199
Merge branch 'release/v1.3.28' of https://github.com/WCY-dt/MrRSS int…
WCY-dt Aug 26, 2026
085e9ff
Merge pull request #1040 from marcomarcogd/codex/fix-feed-management-…
WCY-dt Aug 26, 2026
12a00f1
fix(ai): return user-friendly errors across AI features
marcomarcogd Aug 25, 2026
89a76b0
fix(chat): persist article conversations
marcomarcogd Aug 25, 2026
2ade02a
Merge pull request #1039 from marcomarcogd/codex/fix-chat-persistence
WCY-dt Aug 27, 2026
4263eae
Merge pull request #1035 from marcomarcogd/codex/fix-ai-friendly-errors
WCY-dt Aug 27, 2026
92c3934
Merge pull request #1033 from DevXDojo/release/v1.3.28
WCY-dt Aug 27, 2026
773561b
Merge branch 'codex/swiftui-macos' into feature/macos-client
YunFy26 Aug 29, 2026
0704834
refactor: make the macOS SwiftUI client the only frontend
YunFy26 Aug 29, 2026
d04ce10
feat(macos): add the translation catalogue and the full data model
YunFy26 Aug 29, 2026
24af348
feat(macos): cover the whole backend API from the client
YunFy26 Aug 29, 2026
6b295d5
feat(macos): bring the article list up to the previous interface
YunFy26 Aug 29, 2026
5b3fac3
feat(macos): complete the reading view
YunFy26 Aug 29, 2026
7e3c992
feat(macos): add the keyboard shortcuts from the previous interface
YunFy26 Aug 29, 2026
f52fde7
feat(macos): manage subscriptions the way the previous interface did
YunFy26 Aug 29, 2026
7ec2e29
feat(macos): build the settings window from the backend schema
YunFy26 Aug 29, 2026
c3b5f2b
feat(macos): add AI chat, AI search, tag management and status messages
YunFy26 Aug 29, 2026
d9da42b
fix(macos): correct the refresh progress shape and date language
YunFy26 Aug 29, 2026
2e928ac
feat(macos): remember the window, style articles, and step between them
YunFy26 Aug 29, 2026
e959377
docs: describe the macOS client instead of the Vue frontend
YunFy26 Aug 29, 2026
e42970f
feat(macos): add the manage-feeds pane
YunFy26 Aug 29, 2026
82b2659
feat(macos): play media in place and offer the article outline
YunFy26 Aug 29, 2026
210b4b2
refactor(macos): name the two mark-as-read calls apart
YunFy26 Aug 29, 2026
a0a832b
fix(macos): correct paging under the unread filter and mark-above scope
YunFy26 Aug 29, 2026
8847eaa
fix: keep IMAP passwords, and read feed tags from the listing
YunFy26 Aug 29, 2026
f3f28da
perf(macos): stop sending a request per keystroke and per subscription
YunFy26 Aug 29, 2026
c0b06a6
feat(macos): save settings as they are changed
YunFy26 Aug 29, 2026
57c9e1e
fix(macos): correct two request shapes the backend rejected
YunFy26 Aug 29, 2026
11c76a8
perf(macos): reload once when removing several feeds
YunFy26 Aug 29, 2026
1e50444
fix(macos): pin the development launcher to one data directory
YunFy26 Aug 29, 2026
34af336
fix: drop two Wails binding files that came back with the frontend re…
YunFy26 Aug 29, 2026
00602dd
refactor: rename frontend-swift to frontend
YunFy26 Aug 29, 2026
e8b6709
feat(macos): describe every toolbar button and follow the system lang…
YunFy26 Aug 29, 2026
3a09132
fix(macos): give every toolbar button a hover description
YunFy26 Aug 30, 2026
eb79729
fix(macos): keep toolbar descriptions correct and show them sooner
YunFy26 Aug 30, 2026
d19b389
fix(macos): leave one description per toolbar button
YunFy26 Aug 30, 2026
0ee6c68
docs: say where each way of running keeps its data
YunFy26 Aug 30, 2026
f5274ab
build(macos): name the release image MrRSS-{version}-macos.dmg
YunFy26 Aug 30, 2026
995d291
build(macos): name the application bundle MrRSS.app
YunFy26 Aug 30, 2026
7f58440
build(macos): keep the bundled backend's data under Application Suppo…
YunFy26 Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
343 changes: 56 additions & 287 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

## Project Context

MrRSS is a modern, privacy-focused, cross-platform desktop RSS reader built with Wails (Go + Vue.js).
MrRSS is a privacy-focused RSS reader. On this branch the interface is a native macOS client in SwiftUI, talking to a Go backend over its HTTP API.

**Core Principles**: Privacy-first, cross-platform, modern UI, high performance, accessible

## Tech Stack

- **Backend**: Go 1.27+ with Wails v3 (beta) framework, SQLite with `modernc.org/sqlite`
- **Frontend**: Vue 3.5+ Composition API, Pinia, Tailwind CSS 3.3+, Vite 5+
- **Tools**: Wails CLI v3, npm, Go modules
- **Backend**: Go 1.27+, SQLite with `modernc.org/sqlite`, serving only `/api`
- **Client**: SwiftUI for macOS 14+, a SwiftPM package with no third-party dependencies
- **Tools**: Swift toolchain from Xcode, Go modules
- **Icons**: Phosphor Icons | **I18n**: vue-i18n (English/Chinese)

## Quick Patterns Reference
Expand All @@ -29,302 +29,70 @@ MrRSS is a modern, privacy-focused, cross-platform desktop RSS reader built with

πŸ“š **Full Patterns**: See [CODE_PATTERNS.md](../docs/CODE_PATTERNS.md#backend-patterns-go)

### Frontend (Vue 3)
### Client (SwiftUI)

When writing Vue components, follow this pattern:
When writing a view, follow this shape:

```vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useAppStore } from '@/stores/app';
import { useI18n } from 'vue-i18n';
```swift
struct ArticleRowView: View {
let article: Article
@ObservedObject var viewModel: AppViewModel

// Props with proper typing
interface Props {
item: Article;
isActive?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});

// Store and i18n
const store = useAppStore();
const { t } = useI18n();

// Reactive state
const isLoading = ref(false);

// Async operations with error handling
async function loadData() {
isLoading.value = true;
try {
const data = await fetch('/api/articles').then(r => r.json());
// Process data...
} catch (error) {
console.error('Failed to load:', error);
window.showToast(t('error'), 'error');
} finally {
isLoading.value = false;
}
}

onMounted(() => loadData());
</script>

<template>
<div class="component-container">
<!-- Content with proper i18n -->
</div>
</template>
```

πŸ“š **Full Patterns**: See [CODE_PATTERNS.md](../docs/CODE_PATTERNS.md#frontend-patterns-vue)

## Internationalization

Always use i18n for user-facing strings:

```vue
<!-- Template -->
<h1>{{ t('welcome') }}</h1>
<button :title="t('clickToOpen')">{{ t('open') }}</button>

<!-- Script -->
window.showToast(t('successMessage'), 'success');
```

## UI Components

### Common Patterns

**Card Container**:

```html
<div class="bg-bg-primary border border-border rounded-lg p-4">
<h3 class="text-text-primary font-semibold">{{ t('title') }}</h3>
<p class="text-text-secondary text-sm">{{ t('description') }}</p>
</div>
```

**Modal/Dialog**:

```html
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div class="bg-bg-primary w-full max-w-2xl rounded-2xl shadow-2xl border border-border">
<div class="modal-header">
<h2 class="text-xl font-bold">{{ t('modalTitle') }}</h2>
<button @click="close" class="btn-icon">
<i class="ph ph-x"></i>
</button>
</div>
</div>
</div>
```

### Toast Notifications

```javascript
// Success message
window.showToast(message, 'success');

// Error message
window.showToast(t('operationFailed'), 'error');

// Info message with custom duration
window.showToast(t('updateAvailable'), 'info', 5000);
```

### Confirm Dialogs

```javascript
const confirmed = await window.showConfirm(
t('confirmDelete'),
t('deleteWarning'),
true // isDanger - shows red confirmation button
);

if (confirmed) {
// Proceed with dangerous operation
}
```

### Context Menu Pattern

```vue
<script setup>
import { useContextMenu } from '@/composables/ui/useContextMenu';

const { contextMenu, openContextMenu, closeContextMenu } = useContextMenu();

// Define menu items
const menuItems = [
{ label: t('edit'), action: 'edit', icon: 'ph-pencil' },
{ label: t('delete'), action: 'delete', icon: 'ph-trash', danger: true },
{ type: 'divider' },
{ label: t('markAsRead'), action: 'mark-read' }
];

// Handle right-click
function handleRightClick(event: MouseEvent, item: Article) {
event.preventDefault();
openContextMenu(event, menuItems, item);
}

// Handle menu action
function handleMenuAction(action: string, item: Article) {
switch (action) {
case 'edit':
// Handle edit
break;
case 'delete':
// Handle delete
break;
case 'mark-read':
// Handle mark as read
break;
}
}
</script>

<template>
<div @contextmenu="handleRightClick($event, item)">
<!-- Item content -->
</div>
</template>
```

## Settings Management (OPTIMIZED)

βœ… **The settings system has been optimized with schema-driven code generation!**

### Quick Method (3 Steps)

**Step 1**: Edit `internal/config/settings_schema.json`

```json
"new_setting_key": {
"type": "bool",
"default": false,
"category": "general",
"encrypted": false,
"frontend_key": "new_setting_key"
}
```

**Step 2**: Generate all code

```bash
go run tools/settings-generator/main.go
```

**Step 3**: Add UI (optional)

```vue
<SettingItem :title="t('newSettingKey')">
<Toggle
:model-value="settings.new_setting_key"
@update:model-value="updateSetting('new_setting_key', $event)"
/>
</SettingItem>
```

### What Gets Generated Automatically

- βœ… Backend types and handlers
- βœ… Frontend types and composables
- βœ… Database initialization keys
- βœ… Default values

### Old Method (Deprecated)

The manual 8-file checklist is **no longer needed**. All new settings should use the schema-driven approach.

πŸ“š **Complete Guide**: See [docs/SETTINGS.md](../docs/SETTINGS.md)

## Security Best Practices

### Input Validation

Always validate user inputs, especially URLs and file paths:

```go
// Validate URL format and scheme
func validateFeedURL(urlStr string) error {
u, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}

if u.Scheme != "http" && u.Scheme != "https" {
return errors.New("URL must use HTTP or HTTPS")
var body: some View {
HStack(alignment: .top, spacing: 10) {
unreadDot
VStack(alignment: .leading, spacing: 4) {
// Every interface string comes from the catalogue.
Text(article.displayTitle(preferTranslation: showsTranslation))
.fontWeight(article.isRead ? .regular : .semibold)
metadata
}
}
.contextMenu { contextMenu }
}

return nil
}
private var unreadDot: some View { /* ... */ }
private var metadata: some View { /* ... */ }

// Validate file path to prevent directory traversal
func validateFilePath(baseDir, filePath string) error {
cleanPath := filepath.Clean(filePath)
if !strings.HasPrefix(cleanPath, filepath.Clean(baseDir)) {
return errors.New("invalid file path: path traversal detected")
@ViewBuilder
private var contextMenu: some View {
Button {
viewModel.setArticleRead(article, read: !article.isRead)
} label: {
Label(t("article.action.markAsRead"), systemImage: "checkmark.circle")
}
}
return nil
}
```

### Safe Command Execution
Behaviour belongs on `AppViewModel` or one of its extensions, not in the view:

**NEVER** use shell command concatenation:

```go
// ❌ BAD: Command injection vulnerability
cmd := exec.Command("sh", "-c", "rm " + filePath)

// βœ… GOOD: Use Go standard library
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("remove file: %w", err)
}

// βœ… GOOD: If external command is necessary, use separate args
cmd := exec.Command("installer.exe", "/S") // No concatenation
```

### File Operations

Always clean up temporary files and use proper error handling:

```go
// Schedule cleanup with timeout
scheduleCleanup := func(filePath string, delay time.Duration) {
go func() {
time.Sleep(delay)
if err := os.Remove(filePath); err != nil {
log.Printf("Failed to cleanup %s: %v", filePath, err)
} else {
log.Printf("Cleaned up temporary file: %s", filePath)
```swift
extension AppViewModel {
func toggleReadLater(_ article: Article) {
mutateArticle(article.id, apply: { $0.isReadLater.toggle() }) { [weak self] in
try await self?.api.toggleReadLater(id: article.id)
}
}()
}
}
```

## Don'ts

❌ **Don't**:

- Use `var` declarations in Vue (use `ref` or `reactive`)
- Hardcode user-facing strings (always use i18n `t()`)
- Use inline styles (use Tailwind classes or scoped styles)
- Hardcode user-facing strings (always use `t("some.key")`)
- Write long SwiftUI bodies (type checking stalls; split into computed properties)
- Force unwrap outside tests
- Forget error handling in async operations
- Use `any` type without strong justification
- Commit API keys, secrets, or sensitive data
- Use `v-html` for user content (XSS risk)
- Make breaking changes without migration path
- Render untrusted markup without sanitising it (see `HTMLDocument.sanitize`)
- Make breaking changes without a migration path
- Use shell command concatenation (security risk)
- Create multiple deep watchers when one suffices
- Forget to clean up timers/intervals on component unmount
- Let a superseded request overwrite newer state (check the request identifier)
- Forget to remove time observers and event monitors when a view disappears
- Delete favorited articles during cleanup operations
- Use synchronous operations in UI thread for long tasks
- Block the main actor with long-running work

## Do's

Expand Down Expand Up @@ -354,21 +122,22 @@ scheduleCleanup := func(filePath string, delay time.Duration) {

**Build Commands**:

- Development: `wails3 dev`
- Production Build: `wails3 build`
- Important: MrRSS uses HTTP API, not Wails bindings
- Development: `./frontend/run.sh`
- Client only: `swift build --package-path frontend`
- Release bundle: `make build-app VERSION=1.3.28`

**Store Access**:
**State Access**:

- `const store = useAppStore()`
- `const { t } = useI18n()`
- Theme: `store.theme` returns `'light'` or `'dark'`
- Language: `store.i18n.locale.value` returns `'en'` or `'zh'`
- `@ObservedObject var viewModel: AppViewModel`
- Strings: `t("some.key")`, `t("some.key", ["count": n])`
- Settings: `viewModel.setting("key")`, `viewModel.boolSetting("key")`
- Theme follows the `theme` setting through `viewModel.preferredColorScheme`

**UI Helpers**:

- Toast: `window.showToast(message, type)`
- Confirm: `await window.showConfirm(title, message, isDanger)`
- Confirmation: `viewModel.statusMessage = t("...")` shows a short message
- Errors: `viewModel.errorMessage = ...` raises an alert
- Confirmations use `.confirmationDialog`

**API Endpoints**:

Expand Down
Loading