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
5 changes: 5 additions & 0 deletions Sources/Domain/AppNotifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ import Foundation
extension Notification.Name {
static let pluginCredentialsDidChange = Notification.Name("jin.pluginCredentialsDidChange")
static let codexWorkingDirectoryPresetsDidChange = Notification.Name("jin.codexWorkingDirectoryPresetsDidChange")
static let settingsNavigateToPlugin = Notification.Name("jin.settingsNavigateToPlugin")
}

enum SettingsNavigationUserInfoKey {
static let pluginID = "pluginID"
}
11 changes: 11 additions & 0 deletions Sources/Domain/WebSearchAndToolControls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ enum SearchPluginProvider: String, Codable, CaseIterable, Identifiable, Sendable
case .perplexity: return "PPLX"
}
}

var signupURL: URL? {
switch self {
case .exa: return URL(string: "https://dashboard.exa.ai/")
case .brave: return URL(string: "https://api-dashboard.search.brave.com/")
case .jina: return URL(string: "https://jina.ai/api-dashboard/")
case .firecrawl: return URL(string: "https://www.firecrawl.dev/")
case .tavily: return URL(string: "https://app.tavily.com/")
case .perplexity: return URL(string: "https://docs.perplexity.ai/")
}
}
Comment on lines +111 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check HTTP status of all signup URLs

urls=(
  "https://dashboard.exa.ai/"
  "https://api-dashboard.search.brave.com/"
  "https://jina.ai/api-dashboard/"
  "https://www.firecrawl.dev/"
  "https://app.tavily.com/"
  "https://www.perplexity.ai/settings/api"
)

for url in "${urls[@]}"; do
  status=$(curl -o /dev/null -s -w "%{http_code}" -L "$url")
  echo "$url -> HTTP $status"
done

Repository: hrayleung/Jin

Length of output: 316


Verify signup URLs for Exa and Perplexity; most other URLs are accessible.

Accessibility check reveals most provider signup URLs are reachable, but two need attention:

  • Exa dashboard (https://dashboard.exa.ai/) returns HTTP 429 (Rate Limited)
  • Perplexity settings (https://www.perplexity.ai/settings/api) returns HTTP 403 (Forbidden)

Confirm these are the correct signup/dashboard URLs, or update them if they should point elsewhere. The remaining URLs (Brave, Jina, Firecrawl, Tavily) are confirmed accessible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Domain/WebSearchAndToolControls.swift` around lines 111 - 120, The
signupURL computed property contains potentially incorrect endpoints for the
.exa and .perplexity cases; verify the canonical dashboard/signup URLs and
replace "https://dashboard.exa.ai/" and "https://www.perplexity.ai/settings/api"
with the correct targets (or a documented help/support URL) inside the signupURL
switch branch for .exa and .perplexity in WebSearchAndToolControls so the links
no longer return 429/403; if no direct dashboard URL exists, point to the
provider’s public signup or account/settings landing page and add a short
comment above the .exa/.perplexity cases noting the reason for the chosen
fallback.

}

enum ExaSearchType: String, Codable, CaseIterable, Sendable {
Expand Down
6 changes: 1 addition & 5 deletions Sources/UI/AddMCPServerSections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ struct AddMCPServerQuickSetupSection: View {
.jinInlineErrorText()
} else {
JinDetailsDisclosure(title: "Import Details") {
Text("Supports Claude Desktop-style `mcpServers` configs plus single-server payloads.")
.font(.caption)
.foregroundStyle(.secondary)
Text("HTTP imports are mapped to native HTTP transport.")
Text("Accepts Claude Desktop `mcpServers` configs and single-server payloads. HTTP entries map to native HTTP transport.")
.font(.caption)
.foregroundStyle(.secondary)
}
Expand Down Expand Up @@ -83,7 +80,6 @@ struct AddMCPServerIdentitySection: View {
text: $id,
usesMonospacedFont: true
)
.help("Short identifier (e.g. 'git').")

JinSettingsTextFieldRow("Name", fieldTitle: "Exa", text: $name)

Expand Down
4 changes: 1 addition & 3 deletions Sources/UI/AddProviderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ struct AddProviderView: View {
text: $baseURL,
usesMonospacedFont: true
)
.help("Default endpoint is pre-filled.")
}

if let providerSetupCallout {
Expand All @@ -97,8 +96,7 @@ struct AddProviderView: View {
case .optionalAPIKey:
JinSettingsSecureFieldRow(
"API Key",
fieldTitle: "API Key (Optional)",
supportingText: "Optional. Leave blank to use ChatGPT account login in provider settings.",
supportingText: "Leave blank to use ChatGPT account login.",
text: $apiKey,
isRevealed: $isKeyVisible,
revealHelp: "Show API key",
Expand Down
11 changes: 4 additions & 7 deletions Sources/UI/AgentModeSettingsView+CommandPrefixes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ extension AgentModeSettingsView {
var safePrefixesSection: some View {
JinSettingsSection(
"Safe Commands",
detail: "Commands starting with these prefixes are auto-approved when RTK can rewrite them."
detail: "Auto-approved when RTK can rewrite them."
) {
DisclosureGroup("Safe commands (\(safePrefixes.count))") {
FlowLayout(spacing: 4) {
Expand All @@ -18,7 +18,7 @@ extension AgentModeSettingsView {

JinSettingsControlRow(
"Add safe prefix",
supportingText: "Matches the beginning of a command, for example python3."
supportingText: "Matches command start, e.g., python3."
) {
AgentModeCommandPrefixAddRow(
title: "Add safe prefix",
Expand Down Expand Up @@ -47,10 +47,7 @@ extension AgentModeSettingsView {
}

var allowedPrefixesSection: some View {
JinSettingsSection(
"Additional Allowed Prefixes",
detail: "Extend the auto-approved list with prefixes that make sense for your workflow."
) {
JinSettingsSection("Additional Allowed Prefixes") {
if allowedPrefixes.isEmpty {
Text("No custom prefixes added.")
.font(.caption)
Expand All @@ -67,7 +64,7 @@ extension AgentModeSettingsView {

JinSettingsControlRow(
"Add prefix",
supportingText: "Use the full prefix you want auto-approved, for example npm run."
supportingText: "Full prefix to auto-approve, e.g., npm run."
) {
AgentModeCommandPrefixAddRow(
title: "Add prefix",
Expand Down
14 changes: 6 additions & 8 deletions Sources/UI/AgentModeSettingsView+ToolsAndSafety.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,18 @@ extension AgentModeSettingsView {
var detailsSection: some View {
JinSettingsSection("Details") {
JinDetailsDisclosure(title: "How Agent Mode Works") {
AgentModeDetailsText("Agent Mode can run shell commands, search codebases, and edit local files.")
AgentModeDetailsText("Shell, grep, and glob run through RTK. File reads and edits stay local.")
AgentModeDetailsText("Runs shell commands, searches codebases, edits files.")
AgentModeDetailsText("Shell/grep/glob run through RTK. Reads and edits stay local.")
}

JinDetailsDisclosure(title: "Approval Rules") {
AgentModeDetailsText("Safe commands are auto-approved by prefix, but RTK still rejects commands it cannot rewrite.")
AgentModeDetailsText("Additional allowed prefixes extend that auto-approval list.")
AgentModeDetailsText("Auto-approve file reads skips approval prompts for reads only.")
AgentModeDetailsText("Auto-approval matches prefix; RTK rejects commands it can't rewrite.")
AgentModeDetailsText("Allowed prefixes extend the auto-approval list.")
}

JinDetailsDisclosure(title: "RTK") {
AgentModeDetailsText("Agent shell commands must be rewriteable by RTK.")
AgentModeDetailsText("Jin manages RTK tee output so you can reopen the full raw logs later.")
AgentModeDetailsText("Command timeout is the maximum runtime before a shell command is terminated.")
AgentModeDetailsText("Shell commands must be rewriteable by RTK.")
AgentModeDetailsText("RTK output is logged for later replay.")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/UI/AppearanceSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ struct AppearanceSettingsView: View {

JinSettingsToggleRow(
"Overlay Scrollbars",
supportingText: "When enabled, scrollbars fade in during scrolling and hide when idle. Turn off to follow the system preference.",
supportingText: "Fade in during scroll, hide when idle.",
isOn: $useOverlayScrollbars
)
}
Expand Down
7 changes: 1 addition & 6 deletions Sources/UI/ChatNamingPluginSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@ struct ChatNamingPluginSettingsView: View {

var body: some View {
JinSettingsPage {
JinSettingsSection("Behavior") {
JinSettingsSection("Chat Naming") {
Picker("Rename Mode", selection: $chatNamingMode) {
ForEach(ChatNamingMode.allCases) { mode in
Text(mode.label).tag(mode)
}
}
}

JinSettingsSection(
"Naming Model",
detail: "Choose the provider and model used when Jin suggests chat titles."
) {
if allProviderModelPairs.isEmpty {
Text("No providers with chat-capable models found. Add or enable a chat model under Settings → Providers.")
.jinInfoCallout()
Expand Down
18 changes: 8 additions & 10 deletions Sources/UI/CloudflareR2UploadPluginSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ struct CloudflareR2UploadPluginSettingsView: View {

var body: some View {
JinSettingsPage {
JinSettingsSection(
"Credentials",
detail: "These keys are used for temporary public uploads before OCR or remote processing."
) {
JinSettingsSection("Credentials") {
JinSettingsTextFieldRow("Account ID", text: $accountID, usesMonospacedFont: true)

if let dashboardURL = URL(string: "https://dash.cloudflare.com/?to=/:account/r2/overview") {
Link("Open Cloudflare R2 dashboard", destination: dashboardURL)
.font(.caption)
}

JinSettingsTextFieldRow("Access Key ID", text: $accessKeyID, usesMonospacedFont: true)

JinSettingsSecureFieldRow(
Expand All @@ -63,21 +65,17 @@ struct CloudflareR2UploadPluginSettingsView: View {

JinSettingsTextFieldRow(
"Public Base URL",
supportingText: "Optional public URL, for example https://pub-xxx.r2.dev.",
fieldTitle: "https://pub-xxx.r2.dev",
text: $publicBaseURL,
usesMonospacedFont: true
)

JinSettingsTextFieldRow(
"Key Prefix",
fieldTitle: "Key Prefix (optional)",
supportingText: "Optional.",
text: $keyPrefix,
usesMonospacedFont: true
)
}

JinSettingsSection("Actions") {
PluginCredentialActionsView(
canTestConnection: canTest,
canClear: true,
Expand Down Expand Up @@ -207,7 +205,7 @@ struct CloudflareR2UploadPluginSettingsView: View {

lastPersistedConfiguration = configuration
if showSavedStatus {
statusMessage = configurationIsEmpty(configuration) ? "Cleared." : "Saved automatically."
statusMessage = configurationIsEmpty(configuration) ? "Cleared." : nil
statusIsError = false
}
NotificationCenter.default.post(name: .pluginCredentialsDidChange, object: nil)
Expand Down
2 changes: 1 addition & 1 deletion Sources/UI/DeepSeekOCRPluginSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct DeepSeekOCRPluginSettingsView: View {
PluginAPIKeySettingsView(
title: "DeepSeek OCR (DeepInfra)",
preferenceKey: AppPreferenceKeys.pluginDeepSeekOCRAPIKey,
apiKeyHint: "Use your DeepInfra API key here.",
apiKeyHint: "Uses your DeepInfra API key.",
testConnection: { apiKey in
let client = DeepInfraDeepSeekOCRClient(apiKey: apiKey)
try await client.validateAPIKey()
Expand Down
79 changes: 24 additions & 55 deletions Sources/UI/FirecrawlOCRPluginSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ struct FirecrawlOCRPluginSettingsView: View {
JinSettingsPage(maxWidth: 620) {
JinSettingsSection(
"Shared Firecrawl API Key",
detail: "This key is used by Firecrawl OCR and the Web Search plugin."
detail: "Used by Firecrawl OCR and the Web Search plugin."
) {
JinSettingsSecureFieldRow(
"API Key",
supportingText: "Changes save automatically.",
text: $apiKey,
isRevealed: $isKeyVisible,
usesMonospacedFont: true,
Expand All @@ -41,19 +40,15 @@ struct FirecrawlOCRPluginSettingsView: View {
Spacer()
}

Text(statusMessage ?? "Shared with Web Search.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
if let statusMessage {
Text(statusMessage)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}

JinSettingsSection(
"Before You Use Firecrawl OCR",
detail: "Firecrawl OCR needs one shared API key and a configured Cloudflare R2 upload target.",
style: .plain
) {
guidanceSection
}
r2RequirementCallout
}
.navigationTitle("Firecrawl OCR")
.task {
Expand All @@ -69,49 +64,23 @@ struct FirecrawlOCRPluginSettingsView: View {
}
}

private var guidanceSection: some View {
VStack(alignment: .leading, spacing: JinSpacing.medium) {
guidanceRow(
title: "Cloudflare R2 is required",
detail: "Firecrawl OCR uploads each local PDF to your configured public R2 bucket before calling Firecrawl.",
systemImage: "externaldrive.badge.icloud"
)

Divider()

guidanceRow(
title: "Configure the upload target first",
detail: "Open Settings → Plugins → Cloudflare R2 Upload and add your bucket details there.",
systemImage: "gearshape.2"
)

Divider()

guidanceRow(
title: "Choose a parser per chat",
detail: "Use the PDF menu to switch between Fast, Auto, and OCR for each conversation.",
systemImage: "doc.text.magnifyingglass"
)
}
}
private var r2RequirementCallout: some View {
HStack(alignment: .firstTextBaseline, spacing: JinSpacing.medium) {
Text("Requires Cloudflare R2 to be configured for uploads.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)

private func guidanceRow(title: String, detail: String, systemImage: String) -> some View {
HStack(alignment: .top, spacing: JinSpacing.medium) {
Image(systemName: systemImage)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.tertiary)
.frame(width: 16)
.padding(.top, 2)

VStack(alignment: .leading, spacing: JinSpacing.xSmall) {
Text(title)
.font(.subheadline.weight(.semibold))

Text(detail)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: JinSpacing.small)

Button("Configure R2 Upload…") {
NotificationCenter.default.post(
name: .settingsNavigateToPlugin,
object: nil,
userInfo: [SettingsNavigationUserInfoKey.pluginID: "cloudflare_r2_upload"]
)
}
.buttonStyle(.link)
}
}

Expand Down Expand Up @@ -155,7 +124,7 @@ struct FirecrawlOCRPluginSettingsView: View {
UserDefaults.standard.set(key, forKey: AppPreferenceKeys.pluginWebSearchFirecrawlAPIKey)
}
lastPersistedAPIKey = key
statusMessage = key.isEmpty ? "Cleared." : "Saved automatically."
statusMessage = key.isEmpty ? "Cleared." : nil
NotificationCenter.default.post(name: .pluginCredentialsDidChange, object: nil)
}
}
5 changes: 1 addition & 4 deletions Sources/UI/MCPServerConfigFormSections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,7 @@ struct MCPServerStdioTransportSections: View {

if showsNodeIsolationNote {
JinDetailsDisclosure(title: "Launcher Details") {
Text("For Node launchers (`npx`, `npm`, `pnpm`, `yarn`, `bunx`, `bun`), Jin isolates npm HOME/cache under Application Support.")
.font(.caption)
.foregroundStyle(.secondary)
Text("This avoids `~/.npmrc` permission and prefix conflicts.")
Text("Node launchers (`npx`, `npm`, `pnpm`, `yarn`, `bunx`, `bun`) run with an isolated HOME/cache to avoid `~/.npmrc` conflicts.")
.font(.caption)
.foregroundStyle(.secondary)
}
Expand Down
17 changes: 9 additions & 8 deletions Sources/UI/MinerUOCRPluginSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,25 @@ struct MinerUOCRPluginSettingsView: View {

var body: some View {
JinSettingsPage {
JinSettingsSection(
"API Token",
detail: "MinerU uses a token plus an optional user header. Changes save automatically."
) {
JinSettingsSection("API Key") {
JinSettingsSecureFieldRow(
"API Token",
fieldTitle: "MinerU API Token",
supportingText: "Stored locally on this Mac. Changes save automatically.",
text: $apiToken,
isRevealed: $isTokenVisible,
revealHelp: "Show API token",
concealHelp: "Hide API token"
)

if let signupURL = URL(string: "https://mineru.net/") {
Link("Get a MinerU API token", destination: signupURL)
.font(.caption)
}

JinSettingsTextFieldRow(
"User Header",
fieldTitle: "Optional user header",
supportingText: "Optional. Sends an extra user identifier with requests.",
fieldTitle: "e.g. team-jin",
supportingText: "Sent as an extra user identifier.",
text: $userIdentifier
)

Expand Down Expand Up @@ -165,7 +166,7 @@ struct MinerUOCRPluginSettingsView: View {
lastPersistedToken = token
lastPersistedUserIdentifier = userIdentifier
lastPersistedLanguage = language
statusMessage = token.isEmpty ? "Cleared." : "Saved automatically."
statusMessage = token.isEmpty ? "Cleared." : nil
statusIsError = false
NotificationCenter.default.post(name: .pluginCredentialsDidChange, object: nil)
}
Expand Down
Loading
Loading