diff --git a/Launch RepoPrompt CE.command b/Launch RepoPrompt CE.command index e4e25dada..baaa43c67 100755 --- a/Launch RepoPrompt CE.command +++ b/Launch RepoPrompt CE.command @@ -4,6 +4,7 @@ set -uo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONDUCTOR="$ROOT_DIR/conductor" APP_ARGS=("$@") +LAUNCHER_ADHOC_SIGNING=0 if ! command -v python3 >/dev/null 2>&1; then echo "RepoPrompt CE's safe coordinated launcher requires Python 3." @@ -21,23 +22,39 @@ elif [[ ! -x "$CONDUCTOR" ]]; then exit 1 fi +configure_debug_signing() { + if [[ -n "${SIGN_IDENTITY:-}" || "${ALLOW_ADHOC_SIGNING:-0}" == "1" || "${ALLOW_ADHOC_SIGNING:-0}" == "true" ]]; then + return 0 + fi + + local apple_development_identity + apple_development_identity="$(security find-identity -v -p codesigning 2>/dev/null | awk -F'"' '/"Apple Development: / { print $2; exit }' || true)" + if [[ -n "$apple_development_identity" ]]; then + return 0 + fi + + export ALLOW_ADHOC_SIGNING=1 + LAUNCHER_ADHOC_SIGNING=1 +} + launch_app() { echo echo "Building and relaunching RepoPrompt CE..." echo "This run becomes the active launch; any older build or launch jobs still in flight are canceled." + if (( LAUNCHER_ADHOC_SIGNING )); then + echo "No Apple Development signing identity was found, so this launcher is using explicit ad-hoc debug signing." + echo "Debug secure storage will be in-memory; saved API keys and secure permission changes will not persist across app launches." + fi echo + local launch_log + launch_log="$(mktemp -t repoprompt-ce-launch)" + local relaunch_rc=0 if (( ${#APP_ARGS[@]} > 0 )); then - if "$CONDUCTOR" app relaunch -- "${APP_ARGS[@]}"; then - echo - echo "RepoPrompt CE has been relaunched." - else - echo - echo "RepoPrompt CE was not relaunched." - echo "Check the result above to see whether the build failed or this run was canceled/replaced." - echo "If the build failed, fix the errors (or let in-flight edits settle), then press r to retry." - echo "Press s to check the current app and job state." - fi - elif "$CONDUCTOR" app relaunch; then + "$CONDUCTOR" app relaunch -- "${APP_ARGS[@]}" 2>&1 | tee "$launch_log" || relaunch_rc=${PIPESTATUS[0]} + else + "$CONDUCTOR" app relaunch 2>&1 | tee "$launch_log" || relaunch_rc=${PIPESTATUS[0]} + fi + if (( relaunch_rc == 0 )); then echo echo "RepoPrompt CE has been relaunched." else @@ -46,7 +63,21 @@ launch_app() { echo "Check the result above to see whether the build failed or this run was canceled/replaced." echo "If the build failed, fix the errors (or let in-flight edits settle), then press r to retry." echo "Press s to check the current app and job state." + if grep -q "Debug ad-hoc signing is disabled by default" "$launch_log"; then + echo + echo "Debug signing was refused even though this launcher tried to configure it automatically." + echo "Run the same debug app from Terminal with explicit ad-hoc signing:" + echo + echo " ALLOW_ADHOC_SIGNING=1 ./conductor app relaunch" + echo + echo "Ad-hoc debug builds use in-memory secure storage, so saved API keys and secure" + echo "permission changes do not persist across launches. For persistent debug" + echo "Keychain storage, pass a stable Apple Development identity explicitly:" + echo + echo " SIGN_IDENTITY=\"Apple Development: Your Name (TEAMID)\" ./conductor app relaunch" + fi fi + rm -f "$launch_log" } show_status() { @@ -112,6 +143,7 @@ echo "Project: $ROOT_DIR" echo "Mode: coordinated (builds and launches run through the dev daemon)" cd "$ROOT_DIR" || exit 1 +configure_debug_signing launch_app while true; do diff --git a/Scripts/patch_keyboard_shortcuts_resource_lookup.sh b/Scripts/patch_keyboard_shortcuts_resource_lookup.sh index 0354c7362..7cf5173b6 100755 --- a/Scripts/patch_keyboard_shortcuts_resource_lookup.sh +++ b/Scripts/patch_keyboard_shortcuts_resource_lookup.sh @@ -11,7 +11,9 @@ RUN_WITHOUT_GITHUB_TOKENS="${REPOPROMPT_RUN_WITHOUT_GITHUB_TOKENS:-$SCRIPT_DIR/r SWIFTPM_SCRATCH_PATH="${REPOPROMPT_SWIFTPM_SCRATCH_PATH:-$ROOT_DIR/.build}" CHECKOUT_DIR="$SWIFTPM_SCRATCH_PATH/checkouts/KeyboardShortcuts" UTILITIES_FILE="$CHECKOUT_DIR/Sources/KeyboardShortcuts/Utilities.swift" -PATCH_FILE="$SCRIPT_DIR/patches/keyboardshortcuts-2.3.0-resource-lookup.patch" +RECORDER_FILE="$CHECKOUT_DIR/Sources/KeyboardShortcuts/Recorder.swift" +RESOURCE_PATCH_FILE="$SCRIPT_DIR/patches/keyboardshortcuts-2.3.0-resource-lookup.patch" +PREVIEW_PATCH_FILE="$SCRIPT_DIR/patches/keyboardshortcuts-2.3.0-remove-previews.patch" EXPECTED_VERSION="2.3.0" EXPECTED_REVISION="045cf174010beb335fa1d2567d18c057b8787165" PATCH_MARKER="RepoPromptKeyboardShortcutsResourceLookupV1" @@ -29,7 +31,8 @@ run() { } [[ -n "$ROOT_DIR" ]] || fail "Usage: $0 " -[[ -f "$PATCH_FILE" ]] || fail "Missing KeyboardShortcuts resource lookup patch: $PATCH_FILE" +[[ -f "$RESOURCE_PATCH_FILE" ]] || fail "Missing KeyboardShortcuts resource lookup patch: $RESOURCE_PATCH_FILE" +[[ -f "$PREVIEW_PATCH_FILE" ]] || fail "Missing KeyboardShortcuts preview patch: $PREVIEW_PATCH_FILE" if [[ ! -f "$UTILITIES_FILE" ]]; then run "$RUN_WITHOUT_GITHUB_TOKENS" swift package \ @@ -38,6 +41,7 @@ if [[ ! -f "$UTILITIES_FILE" ]]; then resolve fi [[ -f "$UTILITIES_FILE" ]] || fail "Could not locate KeyboardShortcuts Utilities.swift after package resolution: $UTILITIES_FILE" +[[ -f "$RECORDER_FILE" ]] || fail "Could not locate KeyboardShortcuts Recorder.swift after package resolution: $RECORDER_FILE" python3 - "$ROOT_DIR/Package.resolved" "$EXPECTED_VERSION" "$EXPECTED_REVISION" <<'PY' import json @@ -68,14 +72,35 @@ else: raise SystemExit("ERROR: KeyboardShortcuts dependency pin is missing from Package.resolved") PY +RESOURCE_PATCH_NEEDED=1 if grep -Fq "$PATCH_MARKER" "$UTILITIES_FILE"; then - printf 'KeyboardShortcuts resource lookup patch already applied: %s\n' "$UTILITIES_FILE" + RESOURCE_PATCH_NEEDED=0 +fi + +PREVIEW_PATCH_NEEDED=1 +if ! grep -Fq "#Preview {" "$RECORDER_FILE"; then + PREVIEW_PATCH_NEEDED=0 +fi + +if (( ! RESOURCE_PATCH_NEEDED )) && (( ! PREVIEW_PATCH_NEEDED )); then + printf 'KeyboardShortcuts patches already applied: %s, %s\n' "$UTILITIES_FILE" "$RECORDER_FILE" exit 0 fi -run chmod u+w "$UTILITIES_FILE" -if ! (cd "$CHECKOUT_DIR" && git apply --unidiff-zero --check "$PATCH_FILE"); then - fail "KeyboardShortcuts resource lookup patch no longer applies cleanly. Review $PATCH_FILE against $UTILITIES_FILE." +if (( RESOURCE_PATCH_NEEDED )); then + run chmod u+w "$UTILITIES_FILE" + if ! (cd "$CHECKOUT_DIR" && git apply --unidiff-zero --check "$RESOURCE_PATCH_FILE"); then + fail "KeyboardShortcuts resource lookup patch no longer applies cleanly. Review $RESOURCE_PATCH_FILE against $UTILITIES_FILE." + fi + run bash -c 'cd "$1" && git apply --unidiff-zero "$2"' bash "$CHECKOUT_DIR" "$RESOURCE_PATCH_FILE" + printf 'Applied KeyboardShortcuts resource lookup patch: %s\n' "$RESOURCE_PATCH_FILE" +fi + +if (( PREVIEW_PATCH_NEEDED )); then + run chmod u+w "$RECORDER_FILE" + if ! (cd "$CHECKOUT_DIR" && git apply --unidiff-zero --check "$PREVIEW_PATCH_FILE"); then + fail "KeyboardShortcuts preview patch no longer applies cleanly. Review $PREVIEW_PATCH_FILE against $RECORDER_FILE." + fi + run bash -c 'cd "$1" && git apply --unidiff-zero "$2"' bash "$CHECKOUT_DIR" "$PREVIEW_PATCH_FILE" + printf 'Applied KeyboardShortcuts preview patch: %s\n' "$PREVIEW_PATCH_FILE" fi -run bash -c 'cd "$1" && git apply --unidiff-zero "$2"' bash "$CHECKOUT_DIR" "$PATCH_FILE" -printf 'Applied KeyboardShortcuts resource lookup patch: %s\n' "$PATCH_FILE" diff --git a/Scripts/patches/keyboardshortcuts-2.3.0-remove-previews.patch b/Scripts/patches/keyboardshortcuts-2.3.0-remove-previews.patch new file mode 100644 index 000000000..4149e3b44 --- /dev/null +++ b/Scripts/patches/keyboardshortcuts-2.3.0-remove-previews.patch @@ -0,0 +1,19 @@ +diff --git a/Sources/KeyboardShortcuts/Recorder.swift b/Sources/KeyboardShortcuts/Recorder.swift +index 7e7869b..d0dc3b9 100644 +--- a/Sources/KeyboardShortcuts/Recorder.swift ++++ b/Sources/KeyboardShortcuts/Recorder.swift +@@ -172,14 +171,0 @@ +-#Preview { +- KeyboardShortcuts.Recorder("record_shortcut", name: .init("xcodePreview")) +- .environment(\.locale, .init(identifier: "en")) +-} +- +-#Preview { +- KeyboardShortcuts.Recorder("record_shortcut", name: .init("xcodePreview")) +- .environment(\.locale, .init(identifier: "zh-Hans")) +-} +- +-#Preview { +- KeyboardShortcuts.Recorder("record_shortcut", name: .init("xcodePreview")) +- .environment(\.locale, .init(identifier: "ru")) +-} diff --git a/Scripts/test_debug_app_process.py b/Scripts/test_debug_app_process.py index 965da9f2c..fb937ebf9 100644 --- a/Scripts/test_debug_app_process.py +++ b/Scripts/test_debug_app_process.py @@ -177,6 +177,97 @@ def test_finder_launcher_without_python_exits_before_any_lifecycle_action(self) self.assertIn("No uncoordinated fallback is provided", result.stdout) self.assertNotIn("Building and relaunching", result.stdout) + def test_finder_launcher_uses_ad_hoc_signing_when_no_identity_exists(self) -> None: + dirname = shutil.which("dirname") + self.assertIsNotNone(dirname) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + launcher = root / "Launch RepoPrompt CE.command" + launcher.write_text((SCRIPT_DIR.parent / launcher.name).read_text(encoding="utf-8"), encoding="utf-8") + bin_dir = root / "bin" + bin_dir.mkdir() + (bin_dir / "dirname").symlink_to(dirname) + python = bin_dir / "python3" + python.write_text("binary", encoding="utf-8") + python.chmod(0o755) + conductor_log = root / "conductor-env.log" + conductor = root / "conductor" + conductor.write_text( + "#!/bin/bash\n" + "printf 'ALLOW_ADHOC_SIGNING=%s\\n' \"${ALLOW_ADHOC_SIGNING:-}\" >> conductor-env.log\n" + "exit 0\n", + encoding="utf-8", + ) + conductor.chmod(0o755) + security = bin_dir / "security" + security.write_text("#!/bin/bash\nprintf ' 0 valid identities found\\n'\n", encoding="utf-8") + security.chmod(0o755) + env = os.environ.copy() + env["PATH"] = str(bin_dir) + env.pop("SIGN_IDENTITY", None) + env.pop("ALLOW_ADHOC_SIGNING", None) + + result = subprocess.run( + ["/bin/bash", str(launcher)], + env=env, + input="q", + text=True, + capture_output=True, + timeout=2, + ) + conductor_log_text = conductor_log.read_text(encoding="utf-8") + + self.assertEqual(result.returncode, 0) + self.assertIn("using explicit ad-hoc debug signing", result.stdout) + self.assertIn("Debug secure storage will be in-memory", result.stdout) + self.assertIn("ALLOW_ADHOC_SIGNING=1", conductor_log_text) + + def test_finder_launcher_shows_fallback_message_when_signing_still_refused(self) -> None: + dirname = shutil.which("dirname") + self.assertIsNotNone(dirname) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + launcher = root / "Launch RepoPrompt CE.command" + launcher.write_text((SCRIPT_DIR.parent / launcher.name).read_text(encoding="utf-8"), encoding="utf-8") + bin_dir = root / "bin" + bin_dir.mkdir() + (bin_dir / "dirname").symlink_to(dirname) + python = bin_dir / "python3" + python.write_text("binary", encoding="utf-8") + python.chmod(0o755) + conductor = root / "conductor" + conductor.write_text( + "#!/bin/bash\n" + "echo 'ERROR: Debug ad-hoc signing is disabled by default. Set ALLOW_ADHOC_SIGNING=1 to build an ad-hoc package, or set SIGN_IDENTITY for stable signing.'\n" + "exit 1\n", + encoding="utf-8", + ) + conductor.chmod(0o755) + security = bin_dir / "security" + security.write_text("#!/bin/bash\nprintf ' 0 valid identities found\\n'\n", encoding="utf-8") + security.chmod(0o755) + env = os.environ.copy() + # Include system paths so tee, mktemp, grep, and rm are available + # for the launcher's reactive fallback log capture and grep. + env["PATH"] = f"{bin_dir}:/usr/bin:/bin" + env.pop("SIGN_IDENTITY", None) + env.pop("ALLOW_ADHOC_SIGNING", None) + + result = subprocess.run( + ["/bin/bash", str(launcher)], + env=env, + input="q", + text=True, + capture_output=True, + timeout=5, + ) + + self.assertEqual(result.returncode, 0) + self.assertIn("RepoPrompt CE was not relaunched", result.stdout) + self.assertIn("Debug signing was refused even though this launcher tried to configure it automatically", result.stdout) + self.assertIn("ALLOW_ADHOC_SIGNING=1 ./conductor app relaunch", result.stdout) + self.assertIn('SIGN_IDENTITY="Apple Development: Your Name (TEAMID)" ./conductor app relaunch', result.stdout) + if __name__ == "__main__": unittest.main() diff --git a/Scripts/test_release_tooling.py b/Scripts/test_release_tooling.py index 2673cdfc1..0cecce7f2 100644 --- a/Scripts/test_release_tooling.py +++ b/Scripts/test_release_tooling.py @@ -1799,6 +1799,8 @@ def make_keyboard_shortcuts_patch_fixture(self, source: str | None = None) -> tu utilities = root / ".build" / "checkouts" / "KeyboardShortcuts" / "Sources" / "KeyboardShortcuts" / "Utilities.swift" utilities.parent.mkdir(parents=True) utilities.write_text(source if source is not None else self.keyboard_shortcuts_upstream_utilities(), encoding="utf-8") + recorder = utilities.parent / "Recorder.swift" + recorder.write_text(self.keyboard_shortcuts_upstream_recorder(), encoding="utf-8") self.write_package_resolved(root, "2.3.0") return root, utilities @@ -1824,6 +1826,29 @@ def keyboard_shortcuts_upstream_utilities() -> str: extension Data { \tvar toString: String? { String(data: self, encoding: .utf8) } } +""" + + @staticmethod + def keyboard_shortcuts_upstream_recorder() -> str: + # The preview-removal patch expects #Preview blocks at line 172. + # Pad with 171 lines so the hunk header @@ -172,14 +171,0 @@ applies. + padding = "\n".join(f"// recorder line {i + 1}" for i in range(171)) + return f"""\ +{padding} +#Preview {{ +\tKeyboardShortcuts.Recorder("record_shortcut", name: .init("xcodePreview")) +\t\t.environment(\\.locale, .init(identifier: "en")) +}} + +#Preview {{ +\tKeyboardShortcuts.Recorder("record_shortcut", name: .init("xcodePreview")) +\t\t.environment(\\.locale, .init(identifier: "zh-Hans")) +}} + +#Preview {{ +\tKeyboardShortcuts.Recorder("record_shortcut", name: .init("xcodePreview")) +\t\t.environment(\\.locale, .init(identifier: "ru")) +}} """ @staticmethod diff --git a/Sources/RepoPrompt/App/FontPreset.swift b/Sources/RepoPrompt/App/FontPreset.swift index e6c43671d..cd704a389 100644 --- a/Sources/RepoPrompt/App/FontPreset.swift +++ b/Sources/RepoPrompt/App/FontPreset.swift @@ -36,10 +36,20 @@ enum FontScalePreset: Double, CaseIterable, Identifiable { } } +// swiftformat:disable environmentEntry +private struct RepoPromptFontScalePresetKey: EnvironmentKey { + static let defaultValue: FontScalePreset = .current +} + extension EnvironmentValues { - @Entry var repoPromptFontScalePreset: FontScalePreset = .current + var repoPromptFontScalePreset: FontScalePreset { + get { self[RepoPromptFontScalePresetKey.self] } + set { self[RepoPromptFontScalePresetKey.self] = newValue } + } } +// swiftformat:enable environmentEntry + extension FontScalePreset { var standardFont: Font { #if DEBUG diff --git a/Sources/RepoPrompt/Features/AgentMode/Views/AgentMessageBubble.swift b/Sources/RepoPrompt/Features/AgentMode/Views/AgentMessageBubble.swift index 904d05ed4..f3f310af2 100644 --- a/Sources/RepoPrompt/Features/AgentMode/Views/AgentMessageBubble.swift +++ b/Sources/RepoPrompt/Features/AgentMode/Views/AgentMessageBubble.swift @@ -1,10 +1,20 @@ import AppKit import SwiftUI +// swiftformat:disable environmentEntry +private struct AgentWindowIsFocusedKey: EnvironmentKey { + static let defaultValue = true +} + extension EnvironmentValues { - @Entry var agentWindowIsFocused: Bool = true + var agentWindowIsFocused: Bool { + get { self[AgentWindowIsFocusedKey.self] } + set { self[AgentWindowIsFocusedKey.self] = newValue } + } } +// swiftformat:enable environmentEntry + // MARK: - Message Footer Strip /// An inline footer strip with timestamp and a subtle copy button. diff --git a/Sources/RepoPrompt/Features/AgentMode/Views/ToolCards/ToolCardContainer.swift b/Sources/RepoPrompt/Features/AgentMode/Views/ToolCards/ToolCardContainer.swift index 796b6a5ef..f573bbb76 100644 --- a/Sources/RepoPrompt/Features/AgentMode/Views/ToolCards/ToolCardContainer.swift +++ b/Sources/RepoPrompt/Features/AgentMode/Views/ToolCards/ToolCardContainer.swift @@ -8,18 +8,56 @@ func performAgentToolCardExpansionStateUpdateWithoutAnimation(_ update: () -> Vo } } +// swiftformat:disable environmentEntry +private struct AgentToolCardAutoExpandEnabledKey: EnvironmentKey { + static let defaultValue = true +} + +private struct AgentLiveBashExecutionByItemIDKey: EnvironmentKey { + static let defaultValue: [UUID: AgentModeViewModel.BashLiveExecutionState] = [:] +} + +private struct AgentRecentAssistantItemIDsKey: EnvironmentKey { + static let defaultValue: Set = [] +} + +private struct AgentMessageRuntimeFooterByItemIDKey: EnvironmentKey { + static let defaultValue: [UUID: AgentMessageRuntimeFooter] = [:] +} + +private struct AgentApprovalVisibleKey: EnvironmentKey { + static let defaultValue = false +} + extension EnvironmentValues { - @Entry var agentToolCardAutoExpandEnabled: Bool = true + var agentToolCardAutoExpandEnabled: Bool { + get { self[AgentToolCardAutoExpandEnabledKey.self] } + set { self[AgentToolCardAutoExpandEnabledKey.self] = newValue } + } - @Entry var agentLiveBashExecutionByItemID: [UUID: AgentModeViewModel.BashLiveExecutionState] = [:] + var agentLiveBashExecutionByItemID: [UUID: AgentModeViewModel.BashLiveExecutionState] { + get { self[AgentLiveBashExecutionByItemIDKey.self] } + set { self[AgentLiveBashExecutionByItemIDKey.self] = newValue } + } - @Entry var agentRecentAssistantItemIDs: Set = [] + var agentRecentAssistantItemIDs: Set { + get { self[AgentRecentAssistantItemIDsKey.self] } + set { self[AgentRecentAssistantItemIDsKey.self] = newValue } + } - @Entry var agentMessageRuntimeFooterByItemID: [UUID: AgentMessageRuntimeFooter] = [:] + var agentMessageRuntimeFooterByItemID: [UUID: AgentMessageRuntimeFooter] { + get { self[AgentMessageRuntimeFooterByItemIDKey.self] } + set { self[AgentMessageRuntimeFooterByItemIDKey.self] = newValue } + } - @Entry var agentApprovalVisible: Bool = false + var agentApprovalVisible: Bool { + get { self[AgentApprovalVisibleKey.self] } + set { self[AgentApprovalVisibleKey.self] = newValue } + } } +// swiftformat:enable environmentEntry + enum AgentToolCardRenderedBashPhase: String, Equatable { case live case completed diff --git a/Sources/RepoPrompt/Infrastructure/UI/Markdown/MarkdownFileLinkInteraction.swift b/Sources/RepoPrompt/Infrastructure/UI/Markdown/MarkdownFileLinkInteraction.swift index 7d26c14e8..da4974d34 100644 --- a/Sources/RepoPrompt/Infrastructure/UI/Markdown/MarkdownFileLinkInteraction.swift +++ b/Sources/RepoPrompt/Infrastructure/UI/Markdown/MarkdownFileLinkInteraction.swift @@ -94,10 +94,20 @@ final class MarkdownFileLinkOpener { } } +// swiftformat:disable environmentEntry +private struct MarkdownFileLinkOpenerKey: EnvironmentKey { + static let defaultValue: MarkdownFileLinkOpener? = nil +} + extension EnvironmentValues { - @Entry var markdownFileLinkOpener: MarkdownFileLinkOpener? + var markdownFileLinkOpener: MarkdownFileLinkOpener? { + get { self[MarkdownFileLinkOpenerKey.self] } + set { self[MarkdownFileLinkOpenerKey.self] = newValue } + } } +// swiftformat:enable environmentEntry + extension NSAttributedString.Key { static let markdownRawLink = NSAttributedString.Key("markdownRawLink") }