Skip to content
Draft
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
10 changes: 10 additions & 0 deletions Nativ.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
0B5170A046C3C587DE077072 /* PDFDocumentTextExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8513AE482EB6C3113080826A /* PDFDocumentTextExtractorTests.swift */; };
0C9F43690B51C4F3012E9DD2 /* ControlPanelSidebarDragAndDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 087534BC45A233134507ED19 /* ControlPanelSidebarDragAndDrop.swift */; };
0CEEC986F8617E501223DA3E /* NativExtensionMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941186B3A4BA46FB90201DE /* NativExtensionMessaging.swift */; };
0D047002DDEE16DDEA5D947B /* NativExtensionPackageInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA21CD9722CBCB2CF7905AA /* NativExtensionPackageInstaller.swift */; };
0DF842E2BE07D345918F3D54 /* Manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 5AF8F6359E4523814FB0EFDA /* Manifest.json */; };
0E326825AE5A1A5EEA33D928 /* NativExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0DA5C4D0B6B5A094E13CCC8 /* NativExtensionTests.swift */; };
0F60E87BC41F29381BD81F73 /* TextDocumentTextExtractors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91D4332044DCC5DCE4B27F6 /* TextDocumentTextExtractors.swift */; };
Expand Down Expand Up @@ -335,6 +336,7 @@
AEF81776985AD1104B189BFE /* SearXNGBrowsingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552A0926132AC026ED6F530 /* SearXNGBrowsingProvider.swift */; };
AFBC36570418275354CE129B /* ExaBrowsingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769A36AA0753AB7F4CB1E371 /* ExaBrowsingProvider.swift */; };
B15353CDE6204176664DB3B2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0E2EE61C1652C49ECC69658 /* AppDelegate.swift */; };
B2464ADB1853CEAEBBA5F9B6 /* NativExtensionPackageInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA21CD9722CBCB2CF7905AA /* NativExtensionPackageInstaller.swift */; };
B25D81298D9DB66967A5FB84 /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50DB6DEB5C434C27FE9AF66 /* MarkdownText.swift */; };
B2B154008527C6291C17AC93 /* MLXImageModelResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61126D4235AFF77D82F1F80B /* MLXImageModelResolver.swift */; };
B38B5B7EFE4D34BBB26198AA /* ChatTranscriptScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318F0B7627C7BB80B04FF747 /* ChatTranscriptScroller.swift */; };
Expand Down Expand Up @@ -466,6 +468,7 @@
F7F573F15C7DC5CD17061EB9 /* ChatProjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11A25CB7C798C39A206FACC /* ChatProjectStore.swift */; };
F945C21FDDC8AE619860D222 /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = B47FB8D68E53A933ACF71A58 /* Highlightr */; };
F9B4AAA3C467240C00807753 /* ChatReadFileToolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56181DA223153BACC750E96 /* ChatReadFileToolTests.swift */; };
FB50CA1DB30A9FAD59C8ACFF /* NativExtensionPackageInstallerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989C43F61A70769AC90B5CF3 /* NativExtensionPackageInstallerTests.swift */; };
FBF7379DD55C920F6D5C4197 /* ControlPanelDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90494E1AE30EF515482DDBE7 /* ControlPanelDependencies.swift */; };
FC65EA7675E8441D11372F9C /* ChatDocumentContextBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F56569EDB0E77FAF6269F577 /* ChatDocumentContextBuilderTests.swift */; };
FD18166B4B3FAEB77E0D8C69 /* CodexCLIProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5BF52CED5DE873DD6BF02B /* CodexCLIProfile.swift */; };
Expand Down Expand Up @@ -689,6 +692,7 @@
6BF298015C5DDB04B72C29A9 /* ControlPanelSidebarRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlPanelSidebarRows.swift; sourceTree = "<group>"; };
6CDD9C3FE179B2E8D76FC769 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
6D4ED7BDFBC000148E13EA19 /* PowerPointDocumentTextExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerPointDocumentTextExtractor.swift; sourceTree = "<group>"; };
6DA21CD9722CBCB2CF7905AA /* NativExtensionPackageInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativExtensionPackageInstaller.swift; sourceTree = "<group>"; };
7069F17AAC82BF3F1FD1C95E /* NativBulkSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativBulkSelection.swift; sourceTree = "<group>"; };
711EAE65183E78E000DD3253 /* GitHubOAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubOAuthTests.swift; sourceTree = "<group>"; };
7405BB62742DE08EB640A2C5 /* VoiceShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceShortcut.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -748,6 +752,7 @@
96185929F990EA1470079AAB /* NativEmbeddingsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativEmbeddingsClient.swift; sourceTree = "<group>"; };
97D987724FB28974BA95CF29 /* ChatTerminalToolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTerminalToolTests.swift; sourceTree = "<group>"; };
982F3BB563023885587DC860 /* NativNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativNotificationTests.swift; sourceTree = "<group>"; };
989C43F61A70769AC90B5CF3 /* NativExtensionPackageInstallerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativExtensionPackageInstallerTests.swift; sourceTree = "<group>"; };
995095FAF89C9D1DEC4158FB /* Exa.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Exa.png; sourceTree = "<group>"; };
9A7A7ACB288953FD476C47E8 /* MCPServerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPServerConfig.swift; sourceTree = "<group>"; };
9AF7F0D894C4EB6521F21D4D /* SystemMonitorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMonitorView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1407,6 +1412,7 @@
A05824815E9047B7042D766D /* NativExtensionCatalogClient.swift */,
586D3A7976823159DE5C00F9 /* NativExtensionHostBroker.swift */,
1DC0B3C7BCF8243FF1A3CA56 /* NativExtensionMarketplace.swift */,
6DA21CD9722CBCB2CF7905AA /* NativExtensionPackageInstaller.swift */,
7BD28C5E1607C8E6025B7240 /* NativExtensionPlatform.swift */,
27F42CEC1B39477AC13352E3 /* NativExtensionStateStore.swift */,
AA7F610FC8739BA80227DB26 /* NativKit.swift */,
Expand Down Expand Up @@ -1583,6 +1589,7 @@
2BDBC269531FA6C6DA9F5DDC /* ModelPrimaryTaskResolverTests.swift */,
6964F9653F9E59196C2EDACB /* NativBulkSelectionTests.swift */,
2336F0C9DD984723FB930AD4 /* NativExtensionCatalogTests.swift */,
989C43F61A70769AC90B5CF3 /* NativExtensionPackageInstallerTests.swift */,
E0DA5C4D0B6B5A094E13CCC8 /* NativExtensionTests.swift */,
780A9582305FC184F6C68D12 /* NativKitTests.swift */,
982F3BB563023885587DC860 /* NativNotificationTests.swift */,
Expand Down Expand Up @@ -2022,6 +2029,7 @@
584E2B79AC7BCD949CC53070 /* NativExtensionCatalogClient.swift in Sources */,
39ADE4524CF22ED36D505546 /* NativExtensionHostBroker.swift in Sources */,
F4F5A265970521BCC2273D23 /* NativExtensionMarketplace.swift in Sources */,
B2464ADB1853CEAEBBA5F9B6 /* NativExtensionPackageInstaller.swift in Sources */,
F6BFEEE99336BB34E3194214 /* NativExtensionPlatform.swift in Sources */,
2B988CEEF0FD7FCCD3862AF6 /* NativExtensionStateStore.swift in Sources */,
C03E948D1C7E27B27838C4C6 /* NativKit.swift in Sources */,
Expand Down Expand Up @@ -2240,6 +2248,8 @@
83E74199C4F9A50350E3E354 /* NativComponents.swift in Sources */,
385B524770D5084E48B80F41 /* NativExtensionCatalogClient.swift in Sources */,
3D2458AA39D0AE10CCACE7CD /* NativExtensionCatalogTests.swift in Sources */,
0D047002DDEE16DDEA5D947B /* NativExtensionPackageInstaller.swift in Sources */,
FB50CA1DB30A9FAD59C8ACFF /* NativExtensionPackageInstallerTests.swift in Sources */,
ABB0814F3990772A1BEF7D0C /* NativExtensionStateStore.swift in Sources */,
0E326825AE5A1A5EEA33D928 /* NativExtensionTests.swift in Sources */,
D849D6A8E4B6C43284891EA9 /* NativKit.swift in Sources */,
Expand Down
16 changes: 1 addition & 15 deletions Sources/Nativ/Features/ControlPanel/ControlPanelState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ final class ControlPanelContentState: ObservableObject {
)

extensionManager.$records
.map(Self.enabledSidebarContributions)
.map(NativExtensionSidebarOrdering.contributions(from:))
.removeDuplicates()
.sink { [weak self] value in
self?.update { $0.extensionSidebarContributions = value }
Expand All @@ -190,18 +190,4 @@ final class ControlPanelContentState: ObservableObject {
guard next != snapshot else { return }
snapshot = next
}

private static func enabledSidebarContributions(
records: [NativExtensionRecord]
) -> [NativSidebarContribution] {
records
.filter { $0.isEnabled && $0.hasRuntime }
.flatMap(\.manifest.contributions.sidebar)
.sorted {
if $0.order == $1.order {
return $0.title.localizedStandardCompare($1.title) == .orderedAscending
}
return $0.order < $1.order
}
}
}
177 changes: 149 additions & 28 deletions Sources/Nativ/Features/Extensions/ExtensionsHubView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AppKit
import NativExtensionSDK
import NativServerKit
import SwiftUI
import UniformTypeIdentifiers

struct ExtensionsHubView: View {
@ObservedObject var manager: NativExtensionManager
Expand Down Expand Up @@ -168,6 +169,47 @@ struct HubEmptyHint: View {

// MARK: - Extensions section

struct NoticeRow: View {
let tone: NativStatusTone
let title: String
var detail: String?
var onDismiss: (() -> Void)?

private var symbol: String {
switch tone {
case .danger: "exclamationmark.triangle.fill"
case .warning: "exclamationmark.circle.fill"
default: "info.circle.fill"
}
}

var body: some View {
HStack(alignment: .top, spacing: 10) {
Image(systemName: symbol)
.foregroundStyle(tone.color)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.legacyTextStyle(.supporting)
if let detail {
Text(detail)
.legacyTextStyle(.metadata)
.foregroundStyle(.secondary)
}
}
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 8)
if let onDismiss {
NativHoverCloseButton(action: onDismiss, help: "Dismiss")
}
}
.padding(12)
.background(
tone.color.opacity(0.08),
in: RoundedRectangle(cornerRadius: 10, style: .continuous)
)
}
}

private struct ExtensionsSectionView: View {
@ObservedObject var manager: NativExtensionManager
@State private var selection: Selection = .discover
Expand Down Expand Up @@ -197,26 +239,70 @@ private struct ExtensionsSectionView: View {
case .discover:
NativExtensionMarketplaceView()
case .installed:
if manager.records.isEmpty {
HubEmptyHint(
icon: "square.stack.3d.up.slash",
text: "No extensions installed."
)
} else {
VStack(spacing: 12) {
ForEach(manager.records) { record in
ExtensionRow(record: record, manager: manager)
}
}
}
installedTab
}
}
.onAppear {
manager.refreshPermissionStatuses()
}
}
}

private var installedTab: some View {
VStack(alignment: .leading, spacing: 12) {
if let message = manager.lastErrorMessage {
NoticeRow(tone: .danger, title: message) {
manager.lastErrorMessage = nil
}
}
ForEach(manager.packageIssues) { issue in
NoticeRow(
tone: .warning,
title: "\(issue.packageName) was not loaded.",
detail: issue.message
)
}
if manager.records.isEmpty {
HubEmptyHint(
icon: "square.stack.3d.up.slash",
text: "No extensions installed."
)
} else {
ForEach(manager.records) { record in
ExtensionRow(record: record, manager: manager)
}
}
installFromFolderButton
}
}

private var installFromFolderButton: some View {
HStack(spacing: 8) {
Button("Install from Folder…") {
installFromFolder()
}
.controlSize(.small)
Text("Load a .nativextension package you are developing.")
.legacyTextStyle(.metadata)
.foregroundStyle(.tertiary)
}
.padding(.top, 2)
}

private func installFromFolder() {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.treatsFilePackagesAsDirectories = false
panel.allowedContentTypes = [UTType(exportedAs: "com.nativ.extension-package", conformingTo: .package)]
panel.allowsMultipleSelection = false
panel.prompt = "Install"
panel.message = "Choose a .nativextension package."
guard panel.runModal() == .OK, let url = panel.url else {
return
}
manager.installPackage(at: url)
}
}
private struct ExtensionRow: View {
let record: NativExtensionRecord
@ObservedObject var manager: NativExtensionManager
Expand All @@ -229,7 +315,12 @@ private struct ExtensionRow: View {
HStack(spacing: 8) {
Text(record.manifest.displayName)
.legacyTextStyle(.compactCardTitle)
if record.isIncluded { includedBadge }
if record.isIncluded {
NativStatusBadge(text: "Included", tone: .active)
}
if record.isRemoved {
NativStatusBadge(text: "Removed", tone: .neutral)
}
}
Text(record.manifest.summary)
.legacyTextStyle(.supporting)
Expand All @@ -241,20 +332,31 @@ private struct ExtensionRow: View {
.padding(.top, 1)
}
Spacer(minLength: 12)
Toggle(
"",
isOn: Binding(
get: { record.isEnabled },
set: { manager.setEnabled($0, extensionID: record.id) }
if record.isRemoved {
Button("Restore") {
manager.restore(extensionID: record.id)
}
.controlSize(.small)
} else {
Toggle(
"",
isOn: Binding(
get: { record.isEnabled },
set: { manager.setEnabled($0, extensionID: record.id) }
)
)
)
.labelsHidden()
.toggleStyle(.switch)
.controlSize(.small)
.labelsHidden()
.toggleStyle(.switch)
.controlSize(.small)
removeMenu
}
}
if let errorMessage = record.errorMessage {
sectionDivider
NoticeRow(tone: .warning, title: errorMessage)
}
if !record.manifest.permissions.isEmpty {
Divider()
.padding(.vertical, 14)
sectionDivider
permissions
}
}
Expand All @@ -269,8 +371,27 @@ private struct ExtensionRow: View {
)
}

private var includedBadge: some View {
NativStatusBadge(text: "Included", tone: .active)
private var sectionDivider: some View {
Divider()
.padding(.vertical, 14)
}

private var removeMenu: some View {
Menu {
Button("Remove Extension", role: .destructive) {
manager.remove(extensionID: record.id)
}
} label: {
Image(systemName: "ellipsis")
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.frame(width: 18)
.help(
record.isIncluded
? "Hide this included extension"
: "Delete this extension package"
)
}

private var permissions: some View {
Expand All @@ -292,7 +413,7 @@ private struct ExtensionRow: View {
) -> some View {
let status = manager.permissionStatus(permission)
let actionTitle = extensionIsEnabled
? manager.permissionActionTitle(permission)
? manager.permissionActionTitle(permission, status: status)
: nil
if let actionTitle {
Button {
Expand Down
Loading
Loading