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 RxCode/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,10 @@
/// Set when a hook/context-menu action wants to open the global docs-capable
/// search overlay in the current window.
var docsSearchRequest: UUID?
/// Non-nil while the bundled User Guide should be presented, optionally at a
/// specific section. Set from the Help menu; `MainView` consumes it to show
/// `UserManualView`.
var userGuideRequest: UserGuideRequest?
/// One-shot: when a docs-setup chat is kicked off, this holds the project so
/// `DocsHook.onSessionStart` injects the docs skill into exactly that chat's
/// system prompt, then clears it.
Expand Down Expand Up @@ -1300,4 +1304,4 @@
return message
}
}
}

Check warning on line 1307 in RxCode/App/AppState.swift

View workflow job for this annotation

GitHub Actions / swiftlint

File should contain 600 lines or less excluding comments and whitespaces: currently contains 751 (file_length)
26 changes: 26 additions & 0 deletions RxCode/App/DocumentationCommands.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftUI

/// Replaces the default macOS **Help** menu, which otherwise looks for a
/// (nonexistent) Help Book and reports "Help isn't available for RxCode"
/// (e.g. 未找到"RxCode"的帮助). Instead it opens the bundled, offline in-app
/// **User Guide** (`UserManualView`), including a direct entry that opens the
/// guide straight to its Custom Context Menus section.
///
/// Selecting an item sets `appState.userGuideRequest`; `MainView` consumes it
/// and presents the guide as a sheet.
struct DocumentationCommands: Commands {
let appState: AppState

var body: some Commands {
CommandGroup(replacing: .help) {
Button("RxCode User Guide") {
appState.userGuideRequest = UserGuideRequest()
}
.keyboardShortcut("?", modifiers: .command)

Button("Custom Context Menus Guide") {
appState.userGuideRequest = UserGuideRequest(section: "custom_context_menus")
}
}
}
}
1 change: 1 addition & 0 deletions RxCode/App/RxCodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
}
}
AutomationCommands()
DocumentationCommands(appState: appState)
}

// Dedicated project window — opened on double-click
Expand Down Expand Up @@ -937,4 +938,4 @@
.frame(minWidth: 600, idealWidth: 900, minHeight: 400, idealHeight: 600)
.background(ClaudeTheme.surfaceElevated)
}
}

Check warning on line 941 in RxCode/App/RxCodeApp.swift

View workflow job for this annotation

GitHub Actions / swiftlint

File should contain 600 lines or less excluding comments and whitespaces: currently contains 792 (file_length)
37 changes: 36 additions & 1 deletion RxCode/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -4498,6 +4498,9 @@
}
}
}
},
"Copy device token" : {

},
"Copy install command" : {
"localizations" : {
Expand Down Expand Up @@ -4983,6 +4986,22 @@
}
}
},
"Custom Context Menus Guide" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "사용자 지정 컨텍스트 메뉴 가이드"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "自定义上下文菜单指南"
}
}
}
},
"Custom script" : {
"localizations" : {
"ko" : {
Expand Down Expand Up @@ -13547,6 +13566,22 @@
}
}
},
"RxCode User Guide" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "RxCode 사용 가이드"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "RxCode 用户指南"
}
}
}
},
"RxCode.xcodeproj" : {
"localizations" : {
"ko" : {
Expand Down Expand Up @@ -17403,4 +17438,4 @@
}
},
"version" : "1.1"
}
}
99 changes: 99 additions & 0 deletions RxCode/Resources/user_manual_custom_context_menus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Custom Context Menus

Custom context menus let you add your own items to RxCode's right-click menus.
Each item runs an action — an HTTP request, or seeding a new / existing chat
thread — and can be scoped to specific projects, attached to one or more menu
surfaces, and gated by a Swift "show condition" that decides whether it appears.

You configure items on the Mac (the single source of truth). The desktop renders
them natively, and the mobile app fetches the same items over the relay and
renders them identically — so there's no separate mobile setup.

## Where to configure them

Open the editor from either:

- **Automation menu → Custom Context Menus** (top menu bar), or
- **Settings → General → Custom Context Menus**.

From there you can add, edit, reorder, enable/disable, and remove items. Each row
shows the item's title, the surfaces it attaches to, and whether it has a Swift
show-condition.

## Surfaces

A surface is *where* the item appears. An item can attach to any combination of
the three:

| Surface | Where it shows |
|---------|----------------|
| **Project** | The project row's context menu in the sidebar (right-click a project, or its `…` menu). |
| **Thread** | A chat thread's context menu in the sidebar. |
| **Briefing** | A branch-scoped briefing card's context menu. |

## Scope

- **All projects** — leave the project unset; the item appears in every project.
- **One project** — scope the item to a single project, and it appears only there.

## Actions

Each item performs exactly one action when tapped:

| Action | What it does |
|--------|--------------|
| **Call API** | Sends an HTTP request. Configure the method, URL, headers, and an optional body. A non-2xx response surfaces as an error. |
| **Create Thread** | Spawns a new chat thread in the project, seeded with your message template. The spawned thread is a utility run — it does **not** trigger lifecycle hooks such as code review or auto-continue. |
| **Continue Thread** | Adds your message to an existing thread (by target thread id). |

## Placeholders

URLs, headers, request bodies, and message templates can use placeholders that
are substituted with the current context when the menu is built:

| Placeholder | Value |
|-------------|-------|
| `{{projectName}}` | The project's display name |
| `{{projectPath}}` | Absolute path to the project on disk |
| `{{gitHubRepo}}` | The project's `owner/repo` (empty if none) |
| `{{branch}}` | The current branch (on briefing-card menus, the card's branch) |
| `{{sessionId}}` | The thread id (on thread menus) |

Example URL:

```
https://api.example.com/projects/{{projectName}}/build?branch={{branch}}
```

## Show conditions

By default every item is **always shown**. You can instead gate an item on a
Swift script that returns whether it should appear. It runs on the desktop when
the menu is built (and is warmed ahead of time for mobile).

Write a single async function:

```swift
func checkShowMenu(context: Context) async throws -> Bool {
// Return true to show the menu item, false to hide it.
let status = try await context.git("status", "--porcelain")
return !status.isEmpty // only show when there are uncommitted changes
}
```

`context` exposes the current project plus two helpers that run inside the
project directory:

| Member | Description |
|--------|-------------|
| `projectName`, `projectPath`, `gitHubRepo`, `branch`, `sessionId` | Same values as the placeholders above. |
| `shell(_:)` | Runs a shell command in the project directory and returns its trimmed stdout. |
| `git(_:)` | Convenience wrapper that runs `git` with the given arguments in the project directory. |

The editor refuses to save a script-gated item until it compiles, offers an
**AI generate** helper that writes the function from a plain-language
requirement, and an inline **Compile** button that surfaces diagnostics.

> **Fail-open by design.** Every failure mode — no toolchain, compile error,
> runtime crash, a timeout, or any non-`true` output — resolves to **show** the
> item. A broken condition never silently hides a menu entry you configured.
3 changes: 3 additions & 0 deletions RxCode/Views/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ struct MainView: View {
)
.environment(appState)
}
.sheet(item: Bindable(appState).userGuideRequest) { request in
UserManualView(initialSection: request.section)
}
.onChange(of: appState.docsSearchRequest) { _, request in
guard request != nil else { return }
windowState.showGlobalSearch = true
Expand Down
21 changes: 21 additions & 0 deletions RxCode/Views/Settings/MobileSettingsTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct MobileSettingsTab: View {
@State private var showPairingSheet = false
@State private var showAddRelaySheet = false
@State private var testNotificationDeviceID: String?
@State private var copiedTokenDeviceID: String?
@State private var testNotificationAlert: TestNotificationAlert?
@State private var deviceBeingRenamed: PairedDevice?
@State private var renameText: String = ""
Expand Down Expand Up @@ -294,6 +295,7 @@ struct MobileSettingsTab: View {
}
}
Spacer()
copyTokenButton(for: device)
testNotificationButton(for: device)
Button {
renameText = device.displayName
Expand Down Expand Up @@ -332,6 +334,25 @@ struct MobileSettingsTab: View {
}
}

@ViewBuilder
private func copyTokenButton(for device: PairedDevice) -> some View {
let token = MobileSyncService.pushToken(for: device)
let copied = copiedTokenDeviceID == device.id
Button {
guard let token, !token.isEmpty else { return }
copyToClipboard(token, feedback: Binding(
get: { copiedTokenDeviceID == device.id },
set: { copiedTokenDeviceID = $0 ? device.id : nil }
))
} label: {
Image(systemName: copied ? "checkmark" : "doc.on.doc")
.foregroundStyle(copied ? Color.green : Color.secondary)
}
.buttonStyle(.borderless)
.disabled(token?.isEmpty ?? true)
.help(copied ? "Copied" : "Copy device token")
}

@ViewBuilder
private func testNotificationButton(for device: PairedDevice) -> some View {
if testNotificationDeviceID == device.id {
Expand Down
17 changes: 15 additions & 2 deletions RxCode/Views/UserManualView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import RxCodeChatKit
import RxCodeCore
import SwiftUI

/// One-shot request to present the bundled User Guide, optionally at a given
/// section (a `UserManualMenu` raw value). Identifiable so it drives a `.sheet`.
struct UserGuideRequest: Identifiable, Hashable {
let id = UUID()
var section: String?
}

struct UserManualView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedMenu: UserManualMenu? = .overview
@State private var selectedMenu: UserManualMenu?

private let documents: [UserManualMenu: BundledMarkdownDocument]

init() {
/// `initialSection` is a `UserManualMenu` raw value (e.g.
/// `"custom_context_menus"`); unknown/nil values open the overview.
init(initialSection: String? = nil) {
_selectedMenu = State(initialValue: UserManualMenu(rawValue: initialSection ?? "") ?? .overview)
self.documents = Dictionary(uniqueKeysWithValues: UserManualMenu.allCases.map { menu in
(
menu,
Expand Down Expand Up @@ -125,6 +135,7 @@ private enum UserManualMenu: String, CaseIterable, Identifiable {
case integrations
case worktrees
case hooks
case customContextMenus = "custom_context_menus"

var id: String { rawValue }

Expand All @@ -143,6 +154,7 @@ private enum UserManualMenu: String, CaseIterable, Identifiable {
case .integrations: "MCP and ACP Clients"
case .worktrees: "Git Worktrees"
case .hooks: "Project Hooks"
case .customContextMenus: "Custom Context Menus"
}
}

Expand All @@ -157,6 +169,7 @@ private enum UserManualMenu: String, CaseIterable, Identifiable {
case .integrations: "puzzlepiece.extension"
case .worktrees: "arrow.triangle.branch"
case .hooks: "bolt.horizontal.circle"
case .customContextMenus: "filemenu.and.cursorarrow"
}
}
}
Expand Down
Loading
Loading