From e5d0e1e4e616abd516892051c618ef0742964845 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 7 Jun 2026 16:02:42 +0900 Subject: [PATCH] Bundle docs into the app + add 'Ask Agent About Prowl' help Ship the agent-facing manual (docs/) inside the app bundle at Contents/Resources/docs, and surface a one-click way to point an AI agent at it. - Bundling: stage docs/ into Resources/docs (Makefile embed-docs, wired into build-app/archive/test; gitignored) and add it as a Resources folder reference, mirroring how ghostty/prowl-cli are bundled. SupacodePaths gains bundledDocs* helpers for the on-disk path. - Ask Agent help: a localized, copyable prompt (en/zh-Hans/zh-Hant/ja, fallback en; every variant asks the agent to reply in the user's language) that points the agent at the bundled docs and asks it to introduce Prowl and suggest features tailored to the user. Reachable from the sidebar Help menu and the macOS Help menu via a shared @Observable presenter + a sheet on the main window. - README: an English prompt pointing agents at docs/README.md on GitHub. - Tests for the locale->prompt logic. --- .gitignore | 1 + Makefile | 16 +- README.md | 20 ++ supacode.xcodeproj/project.pbxproj | 4 + supacode/App/supacodeApp.swift | 15 ++ .../Features/Help/AskAgentHelpPresenter.swift | 20 ++ .../Features/Help/AskAgentHelpPrompt.swift | 193 ++++++++++++++++++ supacode/Features/Help/AskAgentHelpView.swift | 84 ++++++++ .../Views/SidebarFooterView.swift | 6 + supacode/Support/SupacodePaths.swift | 21 ++ supacodeTests/AskAgentHelpPromptTests.swift | 48 +++++ 11 files changed, 424 insertions(+), 4 deletions(-) create mode 100644 supacode/Features/Help/AskAgentHelpPresenter.swift create mode 100644 supacode/Features/Help/AskAgentHelpPrompt.swift create mode 100644 supacode/Features/Help/AskAgentHelpView.swift create mode 100644 supacodeTests/AskAgentHelpPromptTests.swift diff --git a/.gitignore b/.gitignore index 454ad20e..449e18de 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ fastlane/test_output Frameworks/GhosttyKit.xcframework Resources/ghostty Resources/terminfo +Resources/docs .ghostty_hash .ghostty_build_stamp *.profraw diff --git a/Makefile b/Makefile index 419f64e9..08bcda32 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ PROWL_POSTHOG_API_KEY ?= PROWL_POSTHOG_HOST ?= .DEFAULT_GOAL := help -.PHONY: build-ghostty-xcframework ensure-ghostty sync-ghostty _check-ghostty-hash _record-ghostty-hash build-app build-cli build-cli-release embed-cli-debug embed-cli run-app install-dev-build install-release archive export-archive format format-changed format-lint lint check test test-app test-cli-smoke test-cli-integration bump-version bump-and-release log-stream +.PHONY: build-ghostty-xcframework ensure-ghostty sync-ghostty _check-ghostty-hash _record-ghostty-hash build-app build-cli build-cli-release embed-cli-debug embed-cli embed-docs run-app install-dev-build install-release archive export-archive format format-changed format-lint lint check test test-app test-cli-smoke test-cli-integration bump-version bump-and-release log-stream help: # Display this help. @-+echo "Run make with one of the following targets:" @@ -91,7 +91,15 @@ sync-ghostty: # Force sync GhosttyKit to current submodule HEAD (always rebuilds rm -rf ~/Library/Developer/Xcode/DerivedData/supacode-* @echo "Done. Xcode module cache cleared for fresh compilation." -build-app: ensure-ghostty embed-cli-debug # Build the macOS app (Debug) +embed-docs: # Stage docs/ into Resources for bundling into the app (.app/Contents/Resources/docs) + @set -euo pipefail; \ + src="$(CURRENT_MAKEFILE_DIR)/docs"; \ + dst="$(CURRENT_MAKEFILE_DIR)/Resources/docs"; \ + mkdir -p "$$dst"; \ + rsync -a --delete --exclude '.sync-meta.json' "$$src/" "$$dst/"; \ + echo "embedded docs at $$dst" + +build-app: ensure-ghostty embed-cli-debug embed-docs # Build the macOS app (Debug) bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Debug build -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) 2>&1 | mise exec -- xcsift -w --format toon' sync-cli-version: # Sync app MARKETING_VERSION into ProwlCLIShared/ProwlVersion.swift @@ -271,13 +279,13 @@ install-release: build-ghostty-xcframework # Build Release, sign locally, instal ditto "$$APP_PATH" "$$DST"; \ echo "installed $$DST (Release build, locally signed)" -archive: build-ghostty-xcframework embed-cli # Archive Release build for distribution +archive: build-ghostty-xcframework embed-cli embed-docs # Archive Release build for distribution bash -o pipefail -c 'xcodebuild -project supacode.xcodeproj -scheme supacode -configuration Release -archivePath build/supacode.xcarchive archive CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM="$$APPLE_TEAM_ID" CODE_SIGN_IDENTITY="$$DEVELOPER_ID_IDENTITY_SHA" OTHER_CODE_SIGN_FLAGS="--timestamp" PROWL_SENTRY_DSN="$(PROWL_SENTRY_DSN)" PROWL_POSTHOG_API_KEY="$(PROWL_POSTHOG_API_KEY)" PROWL_POSTHOG_HOST="$(PROWL_POSTHOG_HOST)" -skipMacroValidation -clonedSourcePackagesDirPath $(SPM_CACHE_DIR) $(XCODEBUILD_FLAGS) 2>&1 | mise exec -- xcsift -qw --format toon' export-archive: # Export xarchive bash -o pipefail -c 'xcodebuild -exportArchive -archivePath build/supacode.xcarchive -exportPath build/export -exportOptionsPlist build/ExportOptions.plist 2>&1 | mise exec -- xcsift -qw --format toon' -test: ensure-ghostty embed-cli-debug test-app +test: ensure-ghostty embed-cli-debug embed-docs test-app test-app: ensure-ghostty # Run app/unit tests via xcodebuild @set -euo pipefail; \ diff --git a/README.md b/README.md index 159d8229..8c7d10a9 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,26 @@ --- +## 🐾 Meet Prowl through your agent + +Prefer to learn the agentic way instead of scrolling? Hand this prompt to your coding agent (Claude Code, Codex, …) or any AI assistant — it reads Prowl's full documentation and gives you a tailored introduction: + +```text +Read Prowl's documentation and introduce it to me. + +Prowl is a native macOS command center for running many AI coding agents in parallel. Its full manual lives here: +https://github.com/onevcat/Prowl/blob/main/docs/README.md + +Fetch that index and read it (it links to an overview and per-feature manuals in the same docs/ folder — read the relevant ones). Then: +1. Briefly tell me what Prowl is and why it's worth my time. +2. Based on what you know about how I work, suggest 3–4 Prowl features that would genuinely help me, each with a one-line "how". +3. Then answer my follow-up questions, consulting the matching doc. + +Reply in my preferred language. +``` + +> Already installed Prowl? Your agent can read the same docs straight from the app bundle — choose **Help → Ask Agent About Prowl** in the app to copy a version localized to your language. + ## Why Prowl? You're not just typing commands anymore — you're orchestrating Claude Code, Codex, and friends across repos, branches, and ideas. Prowl is the terminal built for that. diff --git a/supacode.xcodeproj/project.pbxproj b/supacode.xcodeproj/project.pbxproj index cc0933ef..d00dfc2a 100644 --- a/supacode.xcodeproj/project.pbxproj +++ b/supacode.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ D6A1CB312F1F4ABF004FDABD /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A1CB2F2F1F4A48004FDABD /* GhosttyKit.xcframework */; }; D6A1CB362F1F4ACB004FDABD /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A1CB342F1F4AC2004FDABD /* Carbon.framework */; }; D6CLI0012F99000100000001 /* prowl-cli in Resources */ = {isa = PBXBuildFile; fileRef = D6CLI0022F99000100000001 /* prowl-cli */; }; + D6DOC0012F99000300000001 /* docs in Resources */ = {isa = PBXBuildFile; fileRef = D6DOC0022F99000300000001 /* docs */; }; D6D054282F2E3EBF00D70ED5 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = D6D054272F2E3EBF00D70ED5 /* PostHog */; }; DFE570C142184CC387C1BC78 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = B5698573F7354B14B940EA60 /* Sparkle */; }; /* End PBXBuildFile section */ @@ -41,6 +42,7 @@ D6A1CB2F2F1F4A48004FDABD /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; D6A1CB342F1F4AC2004FDABD /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; D6CLI0022F99000100000001 /* prowl-cli */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "prowl-cli"; path = "Resources/prowl-cli"; sourceTree = ""; }; + D6DOC0022F99000300000001 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; name = docs; path = "Resources/docs"; sourceTree = ""; }; D6F821012F1FCBC1004B4174 /* supacodeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = supacodeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -101,6 +103,7 @@ D69CE0412F1F378200584C57 = { isa = PBXGroup; children = ( + D6DOC0022F99000300000001 /* docs */, D64162AD2F23CAF100260CA3 /* ghostty */, D64162AF2F23CAF100260CA3 /* git-wt */, D6CLI0022F99000100000001 /* prowl-cli */, @@ -241,6 +244,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D6DOC0012F99000300000001 /* docs in Resources */, D64162B02F23CAF100260CA3 /* ghostty in Resources */, D64162B12F23CAF100260CA3 /* terminfo in Resources */, D64162B22F23CAF100260CA3 /* git-wt in Resources */, diff --git a/supacode/App/supacodeApp.swift b/supacode/App/supacodeApp.swift index 5394eb16..ac782bd4 100644 --- a/supacode/App/supacodeApp.swift +++ b/supacode/App/supacodeApp.swift @@ -106,6 +106,7 @@ struct SupacodeApp: App { @State private var cliSocketServer: CLISocketServer @State private var store: StoreOf @State private var memoryWatchdog: MemoryWatchdog + @State private var askAgentHelp = AskAgentHelpPresenter() private static func cliLaunchOpenPath() -> String? { let args = ProcessInfo.processInfo.arguments @@ -749,6 +750,15 @@ struct SupacodeApp: App { .environment(ghosttyShortcuts) .environment(commandKeyObserver) .environment(\.resolvedKeybindings, store.resolvedKeybindings) + .environment(askAgentHelp) + .sheet( + isPresented: Binding( + get: { askAgentHelp.isPresented }, + set: { askAgentHelp.isPresented = $0 } + ) + ) { + AskAgentHelpView { askAgentHelp.dismiss() } + } } .registersMainWindowOpener() .onAppear { @@ -822,6 +832,11 @@ struct SupacodeApp: App { } #endif CommandGroup(replacing: .help) { + Button("Ask Agent About Prowl…", systemImage: "sparkles") { + askAgentHelp.present() + } + .help("Copy a prompt that points your AI agent at Prowl's bundled docs") + Divider() Button("Homepage", systemImage: "house") { if let url = URL(string: "https://prowl.onev.cat/") { NSWorkspace.shared.open(url) diff --git a/supacode/Features/Help/AskAgentHelpPresenter.swift b/supacode/Features/Help/AskAgentHelpPresenter.swift new file mode 100644 index 00000000..319f0ff4 --- /dev/null +++ b/supacode/Features/Help/AskAgentHelpPresenter.swift @@ -0,0 +1,20 @@ +import Observation + +/// Shared trigger for the "Ask an agent about Prowl" help dialog. +/// +/// Both the sidebar Help menu and the macOS Help menu set `isPresented`; the +/// main window observes it and presents the sheet. It's transient UI state, so +/// it lives in a lightweight `@Observable` store rather than the TCA tree. +@MainActor +@Observable +final class AskAgentHelpPresenter { + var isPresented = false + + func present() { + isPresented = true + } + + func dismiss() { + isPresented = false + } +} diff --git a/supacode/Features/Help/AskAgentHelpPrompt.swift b/supacode/Features/Help/AskAgentHelpPrompt.swift new file mode 100644 index 00000000..3e1d6c2a --- /dev/null +++ b/supacode/Features/Help/AskAgentHelpPrompt.swift @@ -0,0 +1,193 @@ +import Foundation + +/// User-facing strings for the "Ask an agent about Prowl" help dialog. +/// +/// `prompt` is the copyable text the user hands to their AI agent; the rest is +/// chrome for the dialog itself. Everything is localized to the user's device +/// language so the prompt is transparent at a glance, and every prompt also +/// asks the agent to answer in the user's preferred language. +nonisolated struct AskAgentHelpStrings: Equatable, Sendable { + var title: String + var explanation: String + var prompt: String + var copyButtonTitle: String + var copiedButtonTitle: String + var doneButtonTitle: String +} + +/// Builds the localized "Ask an agent" help content. Pure and locale-driven so +/// it can be unit-tested without a running app. +nonisolated enum AskAgentHelpPrompt { + enum LanguageKey: Equatable, Sendable { + case english + case simplifiedChinese + case traditionalChinese + case japanese + } + + /// Resolve which prompt language to use from a locale. Chinese disambiguates + /// Hans/Hant by script, falling back to region (TW/HK/MO → Traditional). + /// Anything we don't translate falls back to English (which still asks the + /// agent to reply in the user's preferred language). + static func languageKey(for locale: Locale) -> LanguageKey { + switch locale.language.languageCode?.identifier { + case "ja": + return .japanese + case "zh": + if let script = locale.language.script?.identifier { + return script == "Hant" ? .traditionalChinese : .simplifiedChinese + } + switch locale.region?.identifier { + case "TW", "HK", "MO": + return .traditionalChinese + default: + return .simplifiedChinese + } + default: + return .english + } + } + + static func strings(docsDirectoryPath: String, locale: Locale = .current) -> AskAgentHelpStrings { + let readme = "\(docsDirectoryPath)/README.md" + let overview = "\(docsDirectoryPath)/overview.md" + switch languageKey(for: locale) { + case .english: + return english(readme: readme, overview: overview) + case .simplifiedChinese: + return simplifiedChinese(readme: readme, overview: overview) + case .traditionalChinese: + return traditionalChinese(readme: readme, overview: overview) + case .japanese: + return japanese(readme: readme, overview: overview) + } + } + + // MARK: - English + + private static func english(readme: String, overview: String) -> AskAgentHelpStrings { + AskAgentHelpStrings( + title: "Ask an agent about Prowl", + explanation: + "Copy this prompt and paste it into your coding agent (Claude Code, Codex, …) " + + "in a terminal, or any AI assistant. It points the agent at the documentation " + + "bundled inside Prowl and asks it to introduce Prowl and suggest features useful to you.", + prompt: """ + Read Prowl's bundled documentation and introduce it to me. + + Prowl is a native macOS command center for running many AI coding agents in parallel. \ + Its full manual ships inside the app: + + - Index: \(readme) + - Highlights: \(overview) + + Read those two first — the index links to per-feature manuals in the same folder; \ + open whichever are relevant. Then: + + 1. Briefly tell me what Prowl is and why it's worth my time. + 2. Based on what you know about how I work (my projects, tools, and habits), suggest \ + 3–4 Prowl features or workflows that would genuinely help me — one line of "how" each. + 3. Then let me ask follow-up questions, consulting the matching file in that folder as needed. + + Reply in my preferred language. + """, + copyButtonTitle: "Copy Prompt", + copiedButtonTitle: "Copied!", + doneButtonTitle: "Done" + ) + } + + // MARK: - Simplified Chinese + + private static func simplifiedChinese(readme: String, overview: String) -> AskAgentHelpStrings { + AskAgentHelpStrings( + title: "让 agent 介绍 Prowl", + explanation: + "复制这段提示词,粘贴给你在终端里的编码 agent(Claude Code、Codex…)或任意 AI 助手。" + + "它会让 agent 读取 Prowl 内置的文档,介绍 Prowl 并推荐对你有用的功能。", + prompt: """ + 阅读 Prowl 自带的文档,并向我介绍它。 + + Prowl 是一款原生 macOS 指挥中心,用来并行运行多个 AI 编码 agent。它的完整说明书就打包在 app 内: + + - 索引: \(readme) + - 亮点: \(overview) + + 请先读这两份——索引里链接了各功能的分册(在同一文件夹下),按需打开相关的。然后: + + 1. 简要告诉我 Prowl 是什么、为什么值得我花时间。 + 2. 结合你对我工作方式的了解(我的项目、工具和习惯),推荐 3–4 个真正能帮到我的 Prowl 功能或用法,每个配一句“怎么用”。 + 3. 之后让我继续追问,按需查阅该文件夹下对应的文档。 + + 请用我的首选语言回答。 + """, + copyButtonTitle: "复制提示词", + copiedButtonTitle: "已复制!", + doneButtonTitle: "完成" + ) + } + + // MARK: - Traditional Chinese + + private static func traditionalChinese(readme: String, overview: String) -> AskAgentHelpStrings { + AskAgentHelpStrings( + title: "讓 agent 介紹 Prowl", + explanation: + "複製這段提示詞,貼給你在終端機裡的編碼 agent(Claude Code、Codex…)或任意 AI 助手。" + + "它會讓 agent 讀取 Prowl 內建的文件,介紹 Prowl 並推薦對你有用的功能。", + prompt: """ + 閱讀 Prowl 內建的文件,並向我介紹它。 + + Prowl 是一款原生 macOS 指揮中心,用來並行執行多個 AI 編碼 agent。它的完整說明書就打包在 app 內: + + - 索引: \(readme) + - 亮點: \(overview) + + 請先讀這兩份——索引裡連結了各功能的分冊(在同一資料夾下),按需開啟相關的。然後: + + 1. 簡要告訴我 Prowl 是什麼、為什麼值得我花時間。 + 2. 結合你對我工作方式的了解(我的專案、工具和習慣),推薦 3–4 個真正能幫到我的 Prowl 功能或用法,每個配一句「怎麼用」。 + 3. 之後讓我繼續追問,按需查閱該資料夾下對應的文件。 + + 請用我的首選語言回答。 + """, + copyButtonTitle: "複製提示詞", + copiedButtonTitle: "已複製!", + doneButtonTitle: "完成" + ) + } + + // MARK: - Japanese + + private static func japanese(readme: String, overview: String) -> AskAgentHelpStrings { + AskAgentHelpStrings( + title: "エージェントに Prowl を尋ねる", + explanation: + "このプロンプトをコピーして、ターミナルのコーディングエージェント(Claude Code、Codex など)" + + "や任意の AI アシスタントに貼り付けてください。Prowl に同梱されたドキュメントを読ませ、" + + "Prowl の紹介とあなたに役立つ機能の提案をしてもらえます。", + prompt: """ + Prowl に同梱されているドキュメントを読んで、私に紹介してください。 + + Prowl は、複数の AI コーディングエージェントを並行して動かすためのネイティブ macOS \ + コマンドセンターです。完全なマニュアルはアプリ内に同梱されています: + + - 索引: \(readme) + - ハイライト: \(overview) + + まずこの2つを読んでください。索引から各機能のマニュアル(同じフォルダ内)にリンクしているので、\ + 関連するものを開いてください。そのうえで: + + 1. Prowl が何で、なぜ使う価値があるのかを簡潔に教えてください。 + 2. 私の働き方(プロジェクト・ツール・習慣)について知っていることを踏まえて、本当に役立つ \ + Prowl の機能や使い方を3〜4個、それぞれ「どう使うか」を一言添えて提案してください。 + 3. その後の私の質問にも、同じフォルダ内の該当ドキュメントを参照しながら答えてください。 + + 私の優先言語で回答してください。 + """, + copyButtonTitle: "プロンプトをコピー", + copiedButtonTitle: "コピーしました!", + doneButtonTitle: "完了" + ) + } +} diff --git a/supacode/Features/Help/AskAgentHelpView.swift b/supacode/Features/Help/AskAgentHelpView.swift new file mode 100644 index 00000000..c5061e7b --- /dev/null +++ b/supacode/Features/Help/AskAgentHelpView.swift @@ -0,0 +1,84 @@ +import AppKit +import SwiftUI + +/// A small, self-contained help sheet that hands the user a ready-to-paste +/// prompt for their AI agent. The prompt points the agent at the documentation +/// bundled inside the app (`Contents/Resources/docs`). Localized to the device +/// language; purely presentational (no store). +struct AskAgentHelpView: View { + private let strings: AskAgentHelpStrings + private let onDone: () -> Void + @State private var didCopy = false + + init( + docsDirectoryPath: String = AskAgentHelpView.resolvedDocsDirectoryPath, + locale: Locale = .current, + onDone: @escaping () -> Void + ) { + self.strings = AskAgentHelpPrompt.strings(docsDirectoryPath: docsDirectoryPath, locale: locale) + self.onDone = onDone + } + + /// The bundled docs directory, falling back to the standard install path so + /// the prompt always shows a sensible location even if the bundle lookup fails. + static var resolvedDocsDirectoryPath: String { + SupacodePaths.bundledDocsDirectoryPath ?? "/Applications/Prowl.app/Contents/Resources/docs" + } + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Label(strings.title, systemImage: "sparkles") + .font(.headline) + + Text(strings.explanation) + .font(.callout) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + ScrollView { + Text(strings.prompt) + .font(.system(.callout, design: .monospaced)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + } + .frame(minHeight: 200, maxHeight: 300) + .background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder(.separator, lineWidth: 1) + ) + + HStack { + Button { + copyPrompt() + } label: { + Label( + didCopy ? strings.copiedButtonTitle : strings.copyButtonTitle, + systemImage: didCopy ? "checkmark" : "doc.on.doc" + ) + } + .buttonStyle(.borderedProminent) + + Spacer() + + Button(strings.doneButtonTitle) { + onDone() + } + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 540) + } + + private func copyPrompt() { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(strings.prompt, forType: .string) + didCopy = true + Task { + try? await Task.sleep(for: .seconds(1.5)) + didCopy = false + } + } +} diff --git a/supacode/Features/Repositories/Views/SidebarFooterView.swift b/supacode/Features/Repositories/Views/SidebarFooterView.swift index 24664889..52919568 100644 --- a/supacode/Features/Repositories/Views/SidebarFooterView.swift +++ b/supacode/Features/Repositories/Views/SidebarFooterView.swift @@ -6,10 +6,16 @@ struct SidebarFooterView: View { @Environment(\.surfaceBottomChromeBackgroundOpacity) private var surfaceBottomChromeBackgroundOpacity @Environment(\.openURL) private var openURL @Environment(\.resolvedKeybindings) private var resolvedKeybindings + @Environment(AskAgentHelpPresenter.self) private var askAgentHelp var body: some View { HStack { Menu { + Button("Ask Agent About Prowl", systemImage: "sparkles") { + askAgentHelp.present() + } + .help("Copy a prompt that points your AI agent at Prowl's bundled docs") + Divider() Button("Homepage", systemImage: "house") { if let url = URL(string: "https://prowl.onev.cat/") { openURL(url) diff --git a/supacode/Support/SupacodePaths.swift b/supacode/Support/SupacodePaths.swift index 9b9fc0d1..f70b0667 100644 --- a/supacode/Support/SupacodePaths.swift +++ b/supacode/Support/SupacodePaths.swift @@ -32,6 +32,27 @@ nonisolated enum SupacodePaths { appSupportDirectory.appending(path: "cache", directoryHint: .isDirectory) } + /// The agent-facing documentation set bundled inside the app + /// (`Prowl.app/Contents/Resources/docs`). Shipped so users and their + /// AI agents can read the manual straight from the installed app. + static var bundledDocsURL: URL? { + Bundle.main.resourceURL?.appending(path: "docs", directoryHint: .isDirectory) + } + + /// On-disk path to the bundled docs index (`docs/README.md`), e.g. + /// `/Applications/Prowl.app/Contents/Resources/docs/README.md`. `nil` only + /// if the bundle has no resource directory (should not happen at runtime). + static var bundledDocsReadmePath: String? { + bundledDocsURL? + .appending(path: "README.md", directoryHint: .notDirectory) + .path(percentEncoded: false) + } + + /// On-disk path to the bundled docs directory itself. + static var bundledDocsDirectoryPath: String? { + bundledDocsURL?.path(percentEncoded: false) + } + static var reposDirectory: URL { baseDirectory.appending(path: "repos", directoryHint: .isDirectory) } diff --git a/supacodeTests/AskAgentHelpPromptTests.swift b/supacodeTests/AskAgentHelpPromptTests.swift new file mode 100644 index 00000000..74ed62f7 --- /dev/null +++ b/supacodeTests/AskAgentHelpPromptTests.swift @@ -0,0 +1,48 @@ +import Foundation +import Testing + +@testable import supacode + +struct AskAgentHelpPromptTests { + @Test func languageKeyMapsCommonLocales() { + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "en_US")) == .english) + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "ja_JP")) == .japanese) + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "fr_FR")) == .english) + } + + @Test func chineseDisambiguatesByScriptThenRegion() { + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "zh_CN")) == .simplifiedChinese) + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "zh_TW")) == .traditionalChinese) + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "zh_HK")) == .traditionalChinese) + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "zh-Hant")) == .traditionalChinese) + #expect(AskAgentHelpPrompt.languageKey(for: Locale(identifier: "zh-Hans")) == .simplifiedChinese) + } + + @Test func promptEmbedsResolvedDocPaths() { + let docs = "/Applications/Prowl.app/Contents/Resources/docs" + let strings = AskAgentHelpPrompt.strings(docsDirectoryPath: docs, locale: Locale(identifier: "en_US")) + #expect(strings.prompt.contains("\(docs)/README.md")) + #expect(strings.prompt.contains("\(docs)/overview.md")) + } + + @Test func everyLanguageAsksToReplyInPreferredLanguage() { + let docs = "/Applications/Prowl.app/Contents/Resources/docs" + let locales = ["en_US", "zh_CN", "zh_TW", "ja_JP"] + for identifier in locales { + let strings = AskAgentHelpPrompt.strings(docsDirectoryPath: docs, locale: Locale(identifier: identifier)) + #expect(!strings.prompt.isEmpty) + #expect(!strings.title.isEmpty) + #expect(strings.prompt.contains(docs)) + } + } + + @Test func localizedTitlesDiffer() { + let docs = "/tmp/docs" + let english = AskAgentHelpPrompt.strings(docsDirectoryPath: docs, locale: Locale(identifier: "en_US")) + let japanese = AskAgentHelpPrompt.strings(docsDirectoryPath: docs, locale: Locale(identifier: "ja_JP")) + let simplified = AskAgentHelpPrompt.strings(docsDirectoryPath: docs, locale: Locale(identifier: "zh_CN")) + #expect(english.prompt != japanese.prompt) + #expect(english.prompt != simplified.prompt) + #expect(english.copyButtonTitle == "Copy Prompt") + } +} -- 2.51.2