Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
082d173
feat(clipboard-history): add private multimedia clipboard history
xcv58 Aug 23, 2026
427ee09
Merge PR #324 Window Layouts into Clipboard History branch
xcv58 Aug 23, 2026
ef29fae
feat(clipboard-history): scale encrypted history storage
xcv58 Aug 23, 2026
f030c44
Merge latest PR #324 Window Layouts changes
xcv58 Aug 23, 2026
b40fab4
Merge remote-tracking branch 'origin/main' into codex/issue-306-clipb…
xcv58 Aug 23, 2026
10b848a
fix(clipboard-history): harden storage and workflows
xcv58 Aug 23, 2026
0ae223f
perf(clipboard-history): reduce result page size
xcv58 Aug 23, 2026
822d906
Merge remote-tracking branch 'origin/main' into codex/issue-306-clipb…
xcv58 Aug 23, 2026
cf82b93
fix(clipboard-history): satisfy merged validation guards
xcv58 Aug 23, 2026
d4c9935
feat: improve clipboard history setup and storage
xcv58 Aug 23, 2026
1daa522
feat: refine clipboard history onboarding
xcv58 Aug 23, 2026
85df956
fix(clipboard): address review findings
xcv58 Aug 23, 2026
0c11217
fix(clipboard): resolve final review findings
xcv58 Aug 23, 2026
711e013
fix(clipboard): close final review gaps
xcv58 Aug 23, 2026
3d01093
test(automation): stabilize cancellation synchronization
xcv58 Aug 23, 2026
11097cc
fix(clipboard): harden paste and panel lifecycle
xcv58 Aug 24, 2026
6d83808
Merge remote-tracking branch 'origin/main' into codex/issue-306-clipb…
xcv58 Aug 24, 2026
e7e4195
Improve Clipboard History window shortcuts
xcv58 Aug 24, 2026
76544ce
feat: add clipboard history export workflows
xcv58 Aug 24, 2026
ecbe853
Merge latest main and resolve clipboard integration conflicts
xcv58 Aug 27, 2026
fe4358d
feat(clipboard): unify saved clips, snippets, and sequential paste
xcv58 Aug 27, 2026
48eaed2
fix(clipboard): harden snippet expansion, queues, and save state
xcv58 Aug 28, 2026
932f399
Merge latest main and resolve modifier-drag startup conflict
xcv58 Aug 28, 2026
877a036
fix(clipboard): address privacy, paste, and snippet review findings
xcv58 Aug 28, 2026
3ce1946
fix(clipboard): match two-character search fragments
xcv58 Aug 28, 2026
0964b9c
fix(clipboard): guard paste and storage lifecycle and reveal shortcut…
xcv58 Aug 28, 2026
51dd217
Polish Clipboard advanced settings divider
xcv58 Aug 28, 2026
dc9346e
Make CLI cancellation regression tests deterministic
xcv58 Aug 28, 2026
8934b60
fix(clipboard): reconcile concurrent updates and speed up panel inter…
xcv58 Aug 28, 2026
dfc2af2
fix(clipboard): preserve shares and switch accessibility
xcv58 Aug 29, 2026
2dad5fe
fix(clipboard): limit window dragging to handle
xcv58 Aug 29, 2026
723c211
fix(clipboard): make rows focusable and identify panel
xcv58 Aug 29, 2026
c6ff4e4
test(clipboard): stabilize settings accessibility checks
xcv58 Aug 29, 2026
2da1271
test(clipboard): inspect ordered accessibility tree
xcv58 Aug 29, 2026
3271859
test(clipboard): order accessibility window normally
xcv58 Aug 29, 2026
b81a3b3
fix(clipboard): stabilize settings accessibility CI
xcv58 Aug 29, 2026
01dfae5
fix(clipboard): harden concurrency and panel performance
xcv58 Aug 29, 2026
c1e6a55
fix(clipboard): address final review findings
xcv58 Aug 29, 2026
54aff71
fix: harden clipboard readers shortcuts and cache
xcv58 Aug 29, 2026
c6c1c2c
fix: harden clipboard privacy and updates
xcv58 Aug 30, 2026
369d7ac
fix: keep snippet usage updates responsive
xcv58 Aug 30, 2026
3efa2f7
fix: harden clipboard queues and snippet expansion
xcv58 Aug 30, 2026
87e9576
Merge origin/main into codex/issue-306-clipboard-history
xcv58 Aug 30, 2026
c4c5f77
test: stabilize Cloudflare upload completion wait
xcv58 Aug 30, 2026
61ffc0b
feat(clipboard): paste visible rich text as plain text
xcv58 Aug 30, 2026
04da61d
fix(clipboard): keep history in capture order
xcv58 Aug 30, 2026
83afbab
docs(changelog): shorten clipboard entry
xcv58 Aug 30, 2026
c0fe76e
refactor(clipboard): simplify search toolbar
xcv58 Sep 1, 2026
d78a4e6
feat(clipboard): delete multiple selected items
xcv58 Sep 1, 2026
6187b39
Merge origin/main into codex/issue-306-clipboard-history
xcv58 Sep 1, 2026
be93a2e
fix(clipboard): align plugin license metadata
xcv58 Sep 1, 2026
2f6b92a
fix(clipboard): harden queues and library operations
xcv58 Sep 2, 2026
be33701
fix(clipboard): close final review gaps
xcv58 Sep 2, 2026
acdb289
perf(clipboard): improve panel responsiveness
xcv58 Sep 2, 2026
5832ae8
fix(clipboard): restore selection contrast and consolidate docs
xcv58 Sep 2, 2026
7f3af93
fix(clipboard): remove redundant toolbar identity
xcv58 Sep 3, 2026
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Unless a file is clearly identified as third-party material under separate terms
- Icon gallery assets must explicitly declare `renderingMode`; use `template` only for black artwork on transparency, and use `original` for color or grayscale detail. Third-party static assets must pin their upstream revision and catalog mapping in `docs/icon-gallery/sources/manifest.json`, with the corresponding license under `Sources/Resources/ThirdPartyNotices/`. Run the gallery generation and related tests after catalog changes.
- Plugins implement `MacToolsPlugin`; menu panel plugins implement `PluginPrimaryPanel`, and component panel plugins implement `PluginComponentPanel`.
- `plugin.json.id` must be stable, readable, and exactly match the runtime `PluginMetadata.id`; each plugin package should return exactly one plugin instance.
- Plugin data is preserved by default on uninstall. A plugin that stores sensitive payloads and must crypto-shred them should declare `uninstallDataPolicy: removePrivateData`, use `PluginPrivateDataKeychainIdentity` for its encryption key, and leave cleanup to the host; failed cleanup must finish before the same plugin can be reinstalled, and lifecycle changes must test both recovery failures and successful cleanup.
- Register newly consumed PluginKit APIs in `scripts/tests/test_plugin_minimum_host_compatibility.py`, including optional protocols. Dashboard presentation and component-detail APIs require host 1.2.1; keeping an older protocol's witness table unchanged does not make new symbols loadable by older hosts.
- Plugin display state should be expressed through `PluginPanelState`, `PluginPanelDetail`, `PluginPanelControl`, and related models. Do not bypass the existing panel framework.
- Prefer `PluginSettingsPage.form` with declarative sections and typed controls. Use a custom form section for a complex region and `PluginSettingsPage.workspace` only for task-oriented managers or editors that need the full content area. Permissions, shortcuts, page chrome, search, validation, and backgrounds remain host-owned.
Expand All @@ -58,6 +59,7 @@ Unless a file is clearly identified as third-party material under separate terms
- User-facing copy is primarily Chinese. Keep it concise, clear, and close to native macOS wording.
- Localize user-facing copy with `.xcstrings`. App/Core copy belongs under `Sources/Resources/Localization`, PluginKit copy under `Sources/MacToolsPluginKit/Resources`, and plugin copy under `Plugins/<PluginName>/Resources`. Plugin `plugin.json` files should keep `displayName`/`summary` as fallbacks and add `localizedMetadata` for marketplace and unloaded-plugin presentation. Pre-install product, capability, privacy, setup, and relationship metadata belongs in the same `plugin.json`; follow `docs/plugins/plugin-manifest.schema.json`. Declare localized product copy once under the source-only `productStrings` table, using `@displayName`, `@summary`, `@localizable.<key>`, `@standardAction.<key>`, `@standardSetup.requirements.<key>`, or all 11 locale values, and make every localized product field reference `@productStrings.<key>`. Keep referenced screenshots under `MarketplaceAssets/`, and never add a parallel marketplace manifest or machine-local dynamic action entries.
- Keep current `plugin.json` runtime envelopes complete. Generated package manifests must contain expanded localization values and match their source metadata; do not edit package copies independently. Legacy manifests must still include runtime-decodable `capabilities` and `permissions`; omitting newer product fields is supported only for PluginKit versions below 5 through the explicit local-debug compatibility flag and must never be used for release catalog generation.
- When merging new plugins with product-metadata changes, register their factories in `PluginRuntimeActionSnapshotTests` and preserve independent runtime policies such as `uninstallDataPolicy` alongside the product fields.
- New plugins should provide localization whenever practical, at minimum for panel copy, settings copy, permission text, and plugin metadata.
- Prefer Apple native frameworks. When adding system frameworks, private include paths, or helper executables inside a plugin bundle, declare the smallest necessary differences in the plugin's own `project.yml`. Bundle resource executables that need separate signing should be listed in `plugin.json.package.signPaths`.
- System power and session-ending actions must use native macOS confirmation flows, remain foreground-only, and avoid immediate restart or shutdown events that can discard unsaved work.
Expand Down
2 changes: 1 addition & 1 deletion Configs/AppVersion.xcconfig
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
MARKETING_VERSION = 1.2.1
MARKETING_VERSION = 1.3.0
CURRENT_PROJECT_VERSION = 69
14 changes: 5 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,9 @@ package-plugins-release: generate
# only when a deliberately isolated bundle needs to coexist.
stop-debug-app:
@if [[ "$(ALLOW_MULTIPLE_DEBUG_APPS)" == "1" ]]; then exit 0; fi
@PIDS=(); \
while read -r PID COMMAND; do \
if [[ "$$COMMAND" == "$(INSTALLED_APP_EXECUTABLE)" \
|| "$$COMMAND" == "$(INSTALLED_APP_EXECUTABLE) "* ]]; then \
PIDS+=("$$PID"); \
fi; \
done < <(/bin/ps -axo pid=,command=); \
@PIDS=($$(/usr/bin/pgrep -f -x \
".*/$(APP_PRODUCT_NAME)[.]app/Contents/MacOS/$(APP_PRODUCT_NAME)( .*)?" \
2>/dev/null || true)); \
if (( $${#PIDS[@]} == 0 )); then \
exit 0; \
fi; \
Expand Down Expand Up @@ -233,10 +229,10 @@ run: stop-debug-app install-debug-app
for ENVIRONMENT_VARIABLE in "$${RUN_ENV[@]}"; do \
OPEN_ENV+=(--env "$$ENVIRONMENT_VARIABLE"); \
done; \
open "$${OPEN_ENV[@]}" -W "$(INSTALLED_APP_PATH)"
open -n "$${OPEN_ENV[@]}" -W "$(INSTALLED_APP_PATH)"

run-open: stop-debug-app install-debug-app
@open -W "$(INSTALLED_APP_PATH)"
@open -n -W "$(INSTALLED_APP_PATH)"

e2e-preflight:
@$(E2E_SCRIPT) preflight
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ClipboardHistoryPlugin

private let clipboardHistoryPluginFactoryAnchor: Any.Type = ClipboardHistoryPluginFactory.self
268 changes: 268 additions & 0 deletions Plugins/ClipboardHistory/PasteboardReaderHelper/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import AppKit
import Darwin
import Foundation

private final class StalledPlainTextProvider: NSObject, NSPasteboardItemDataProvider {
func pasteboard(
_ pasteboard: NSPasteboard?,
item: NSPasteboardItem,
provideDataForType type: NSPasteboard.PasteboardType
) {
while true { sleep(60) }
}
}

private final class LargeDataProvider: NSObject, NSPasteboardItemDataProvider {
let byteCount: Int

init(byteCount: Int) {
self.byteCount = byteCount
}

func pasteboard(
_ pasteboard: NSPasteboard?,
item: NSPasteboardItem,
provideDataForType type: NSPasteboard.PasteboardType
) {
item.setData(Data(repeating: 0x41, count: byteCount), forType: type)
}
}

private func argumentValue(after option: String) -> String? {
guard let optionIndex = CommandLine.arguments.firstIndex(of: option),
CommandLine.arguments.indices.contains(optionIndex + 1) else {
return nil
}
return CommandLine.arguments[optionIndex + 1]
}

private func currentVirtualMemorySize() -> UInt64? {
var info = mach_task_basic_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<mach_task_basic_info_data_t>.size / MemoryLayout<natural_t>.size
)
let result = withUnsafeMutablePointer(to: &info) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(
mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count
)
}
}
return result == KERN_SUCCESS ? info.virtual_size : nil
}

private func currentPhysicalFootprint() -> UInt64? {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size
)
let result = withUnsafeMutablePointer(to: &info) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(
mach_task_self_,
task_flavor_t(TASK_VM_INFO),
$0,
&count
)
}
}
return result == KERN_SUCCESS ? info.phys_footprint : nil
}

private func installAddressSpaceGrowthLimit(additionalByteCount: UInt64) -> Bool {
guard let currentVirtualMemorySize = currentVirtualMemorySize(),
additionalByteCount <= UInt64.max - currentVirtualMemorySize else {
return false
}
let maximumAddressSpace = currentVirtualMemorySize + additionalByteCount
var limit = rlimit(
rlim_cur: rlim_t(maximumAddressSpace),
rlim_max: rlim_t(maximumAddressSpace)
)
return setrlimit(RLIMIT_AS, &limit) == 0
}

private func maximumResidentMemoryFootprint(additionalByteCount: UInt64) -> UInt64? {
guard let baseline = currentPhysicalFootprint(),
additionalByteCount <= UInt64.max - baseline else {
return nil
}
return baseline + additionalByteCount
}

private func recordWatchdogState(_ state: String, at path: String?) {
guard let path else { return }
let url = URL(fileURLWithPath: path)
if !FileManager.default.fileExists(atPath: path) {
FileManager.default.createFile(atPath: path, contents: nil)
}
guard let handle = try? FileHandle(forWritingTo: url) else { return }
defer { try? handle.close() }
do {
try handle.seekToEnd()
try handle.write(contentsOf: Data("\(state)\n".utf8))
} catch {
return
}
}

private final class ResidentMemoryWatchdog: @unchecked Sendable {
private let maximumPhysicalFootprint: UInt64
private let stateFilePath: String?
private let timer = DispatchSource.makeTimerSource(
queue: DispatchQueue(
label: "clipboard-pasteboard-memory-watchdog",
qos: .userInitiated
)
)
private var isRunning = false

init(maximumPhysicalFootprint: UInt64, stateFilePath: String?) {
self.maximumPhysicalFootprint = maximumPhysicalFootprint
self.stateFilePath = stateFilePath
}

func start() {
guard !isRunning else { return }
isRunning = true
let maximumPhysicalFootprint = maximumPhysicalFootprint
timer.setEventHandler {
if let physicalFootprint = currentPhysicalFootprint(),
physicalFootprint > maximumPhysicalFootprint {
_exit(4)
}
}
timer.schedule(deadline: .now(), repeating: .milliseconds(20))
recordWatchdogState("active", at: stateFilePath)
timer.resume()
}

func stop() {
guard isRunning else { return }
isRunning = false
timer.setEventHandler {}
timer.cancel()
recordWatchdogState("idle", at: stateFilePath)
}
}

if let optionIndex = CommandLine.arguments.firstIndex(of: "--stall-plain-text-owner"),
CommandLine.arguments.indices.contains(optionIndex + 1) {
let pasteboardName = NSPasteboard.Name(CommandLine.arguments[optionIndex + 1])
let pasteboard = NSPasteboard(name: pasteboardName)
let provider = StalledPlainTextProvider()
let item = NSPasteboardItem()
item.setDataProvider(provider, forTypes: [.string])
pasteboard.clearContents()
guard pasteboard.writeObjects([item]) else { exit(2) }
FileHandle.standardOutput.write(Data("ready\n".utf8))
RunLoop.current.run()
withExtendedLifetime(provider) {}
exit(0)
}

if let pasteboardNameValue = argumentValue(after: "--large-data-owner"),
let byteCountValue = argumentValue(after: "--large-data-byte-count"),
let byteCount = Int(byteCountValue), byteCount > 0 {
let pasteboard = NSPasteboard(name: .init(pasteboardNameValue))
let provider = LargeDataProvider(byteCount: byteCount)
let item = NSPasteboardItem()
item.setDataProvider(provider, forTypes: [.png])
pasteboard.clearContents()
guard pasteboard.writeObjects([item]) else { exit(2) }
FileHandle.standardOutput.write(Data("ready\n".utf8))
RunLoop.current.run()
withExtendedLifetime(provider) {}
exit(0)
}

let defaultMemoryHeadroom = UInt64(384 * 1_024 * 1_024)
let memoryHeadroom = argumentValue(after: "--memory-headroom-byte-count")
.flatMap(UInt64.init) ?? defaultMemoryHeadroom
guard memoryHeadroom >= 8 * 1_024 * 1_024,
memoryHeadroom <= 1_024 * 1_024 * 1_024,
installAddressSpaceGrowthLimit(additionalByteCount: memoryHeadroom),
let maximumPhysicalFootprint = maximumResidentMemoryFootprint(
additionalByteCount: memoryHeadroom
) else {
exit(3)
}
let watchdogStateFilePath = argumentValue(after: "--watchdog-state-file")

let responseDelayMicroseconds: useconds_t = {
guard let milliseconds = argumentValue(after: "--response-delay-milliseconds").flatMap(UInt32.init),
milliseconds <= 5_000 else { return 0 }
return milliseconds * 1_000
}()
let allocationTestPasteboardName = argumentValue(after: "--allocate-when-pasteboard-name")
let allocationTestByteCount = argumentValue(after: "--allocate-byte-count").flatMap(Int.init)

let input = FileHandle.standardInput
let output = FileHandle.standardOutput
let maximumRequests: Int? = {
guard let optionIndex = CommandLine.arguments.firstIndex(of: "--maximum-requests"),
CommandLine.arguments.indices.contains(optionIndex + 1),
let value = Int(CommandLine.arguments[optionIndex + 1]),
value > 0 else {
return nil
}
return value
}()
let lingerAfterMaximumRequestsMicroseconds: useconds_t = {
guard let milliseconds = argumentValue(after: "--linger-after-maximum-requests-milliseconds")
.flatMap(UInt32.init),
milliseconds <= 5_000 else { return 0 }
return milliseconds * 1_000
}()
var completedRequestCount = 0

while maximumRequests.map({ completedRequestCount < $0 }) ?? true {
do {
let requestData = try ClipboardPasteboardReaderWire.readFrame(
from: input,
maximumByteCount: ClipboardPasteboardReaderWire.maximumRequestFrameByteCount
)
let request = try ClipboardPasteboardReaderWire.decode(
ClipboardPasteboardReaderRequest.self,
from: requestData
)
let watchdog = ResidentMemoryWatchdog(
maximumPhysicalFootprint: maximumPhysicalFootprint,
stateFilePath: watchdogStateFilePath
)
watchdog.start()
defer { watchdog.stop() }
if request.pasteboardName == allocationTestPasteboardName,
let allocationTestByteCount,
allocationTestByteCount > 0,
allocationTestByteCount <= 512 * 1_024 * 1_024 {
let allocation = Data(repeating: 0x41, count: allocationTestByteCount)
allocation.withUnsafeBytes { bytes in
_ = bytes.last
usleep(100_000)
}
withExtendedLifetime(allocation) {}
}
let response = ClipboardPasteboardReaderWire.read(request)
if responseDelayMicroseconds > 0 { usleep(responseDelayMicroseconds) }
try ClipboardPasteboardReaderWire.writeFrame(
ClipboardPasteboardReaderWire.encode(response),
to: output
)
completedRequestCount += 1
} catch ClipboardPasteboardReaderWireError.unexpectedEndOfFile {
break
} catch {
break
}
}

if maximumRequests != nil, lingerAfterMaximumRequestsMicroseconds > 0 {
// Keep the process alive after closing stdin so the parent deterministically exercises the
// stale-session write boundary instead of depending on Process.isRunning propagation timing.
try? input.close()
usleep(lingerAfterMaximumRequestsMicroseconds)
}
Loading
Loading