Fix experimental keep-alive, wire it into "Other" tab, add Dynamic Island Live Activity

Keep-alive for any app:
- On TXM devices (iOS 26+), keep-alive now falls back to the bundled universal
  JIT script for apps with no assigned/name-matched script. Previously only known
  apps got a script callback, so a generic app's JIT brk syscalls went unserviced
  and the app hung / was killed in the background — the reason keep-alive "didn't
  work for any app". The fallback is read straight from the app bundle so the
  current, cancellation-aware script is always used.
- Stopping a hold now reliably stops the script loop: the hold and its JIT script
  share one cancellation token, exposed to JS as should_continue() and honored by
  send_command(). universal.js checks should_continue() each iteration. On stop we
  interrupt + detach so the held app keeps running instead of being torn down. The
  cancellation flag is only read from the script's own thread, so there is no
  unsafe concurrent access to the (non-thread-safe) debug proxy.

Banner from the "Other" tab:
- Selecting an app in the "Other" (launch) tab while keep-alive is enabled now
  holds it in the background and shows the banner, instead of silently launching
  without a debugger. The launch rows show a green "Hold" action and a footer note
  in this mode.

Dynamic Island / Live Activity:
- New StikDebugWidgets widget-extension target hosting a keep-alive Live Activity,
  with compact/expanded/minimal Dynamic Island presentations and a Lock Screen
  view. Shared KeepAliveActivityAttributes lives in a StikDebugShared group used by
  both the app and the extension.
- BackgroundAliveManager starts the activity when a hold begins and ends it when
  the hold stops; stale activities from a previous run are cleared at launch.
- Added NSSupportsLiveActivities to the app Info.plist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EpCxAimrBo9RA8v6BxSEXT
This commit is contained in:
Claude 2026-07-15 10:47:40 +00:00
parent 5c3c37a1e4
commit 23e8052c7b
No known key found for this signature in database
16 changed files with 579 additions and 29 deletions

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,88 @@
//
// KeepAliveLiveActivityWidget.swift
// StikDebugWidgets
//
// Live Activity presentation for the background keep-alive session. Shows the
// held app in the Dynamic Island (compact, expanded, and minimal) and on the
// Lock Screen.
//
import WidgetKit
import SwiftUI
import ActivityKit
@available(iOS 16.1, *)
struct KeepAliveLiveActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: KeepAliveActivityAttributes.self) { context in
// Lock Screen / banner presentation.
KeepAliveLockScreenView(appName: context.state.appName)
.activityBackgroundTint(Color.black.opacity(0.55))
.activitySystemActionForegroundColor(.green)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "bolt.circle.fill")
.font(.title2)
.foregroundStyle(.green)
.padding(.leading, 4)
}
DynamicIslandExpandedRegion(.trailing) {
Text("JIT")
.font(.caption.weight(.bold))
.foregroundStyle(.green)
.padding(.trailing, 4)
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.appName)
.font(.headline)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
DynamicIslandExpandedRegion(.bottom) {
Text("Held alive in the background")
.font(.caption)
.foregroundStyle(.secondary)
}
} compactLeading: {
Image(systemName: "bolt.circle.fill")
.foregroundStyle(.green)
} compactTrailing: {
Text(context.state.appName)
.font(.caption2)
.lineLimit(1)
.frame(maxWidth: 64)
} minimal: {
Image(systemName: "bolt.circle.fill")
.foregroundStyle(.green)
}
.keylineTint(.green)
}
}
}
@available(iOS 16.1, *)
private struct KeepAliveLockScreenView: View {
let appName: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: "bolt.circle.fill")
.font(.title)
.foregroundStyle(.green)
VStack(alignment: .leading, spacing: 2) {
Text("Keeping \(appName) alive")
.font(.headline)
.lineLimit(1)
.minimumScaleFactor(0.7)
Text("Held in the background")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
.padding()
}
}

View file

@ -0,0 +1,17 @@
//
// StikDebugWidgetsBundle.swift
// StikDebugWidgets
//
// Widget extension bundle. Hosts the keep-alive Live Activity so it can be
// rendered in the Dynamic Island and on the Lock Screen.
//
import WidgetKit
import SwiftUI
@main
struct StikDebugWidgetsBundle: WidgetBundle {
var body: some Widget {
KeepAliveLiveActivityWidget()
}
}