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
9 changes: 8 additions & 1 deletion panel/Bootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,17 @@ enum Bootstrap {
// benefit, not the daemon's).
private static func writeDaemonPlistIfVenvPresent() throws {
guard let venvURL = bundledVenvURL() else { return }
// Invoke via python3 with stackvox as a script argument. pip
// stamps an absolute build-time shebang into the stackvox script
// — CI-built bundles end up with /Users/runner/... which doesn't
// exist on user machines, so running the script directly fails
// with "bad interpreter". python3 is a real Mach-O binary that's
// relocatable; calling it directly bypasses the shebang entirely.
let python = venvURL.appendingPathComponent("bin/python3").path
let stackvox = venvURL.appendingPathComponent("bin/stackvox").path
let logPath = "\(installDir)/daemon.log"
try writePlist(label: daemonLabel,
programArgs: [stackvox, "serve"],
programArgs: [python, stackvox, "serve"],
logPath: logPath,
env: stackvoxEnv(venvURL: venvURL))
}
Expand Down
18 changes: 14 additions & 4 deletions panel/PanelNav.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,21 @@ final class PanelNav: ObservableObject {
}

private nonisolated static func runStackvoxVoices() -> [String] {
let stackvox = "\(NSHomeDirectory())/.stack-nudge/venv/bin/stackvox"
guard FileManager.default.isExecutableFile(atPath: stackvox) else { return [] }
// Invoke python3 with stackvox as a script argument rather than
// executing the stackvox script directly. pip stamps an absolute
// shebang at install time pointing at the build machine's python3
// path — CI-built bundles end up with /Users/runner/... shebangs
// that don't exist on user machines, so direct execution fails
// with "bad interpreter". Calling python3 directly bypasses it.
let venvBin = "\(NSHomeDirectory())/.stack-nudge/venv/bin"
let python = "\(venvBin)/python3"
let stackvox = "\(venvBin)/stackvox"
guard FileManager.default.isExecutableFile(atPath: python),
FileManager.default.isReadableFile(atPath: stackvox)
else { return [] }
let task = Process()
task.executableURL = URL(fileURLWithPath: stackvox)
task.arguments = ["voices"]
task.executableURL = URL(fileURLWithPath: python)
task.arguments = [stackvox, "voices"]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = Pipe()
Expand Down
7 changes: 7 additions & 0 deletions panel/SessionUsage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ struct UsageView: View {
.padding(.vertical, 14)
.background(ThinScrollers())
}
// Explicit max-height claim so the ScrollView reliably
// bounds itself to the panel's available area instead of
// expanding to fit its content (which clipped behind the
// PageFooter without a visible scrollbar hint). Force the
// indicator visible so users can tell there's more to see.
.frame(maxHeight: .infinity)
.scrollIndicators(.visible)
} else {
emptyState
}
Expand Down
35 changes: 21 additions & 14 deletions panel/Speaker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@ import AppKit
// binary anymore; speech goes through `stackvox say <text>` as a subcommand.
enum Speaker {

// Never invoke the `stackvox` script directly — pip stamps an absolute
// shebang at install time, so a venv built on the CI runner has a
// shebang pointing at /Users/runner/... that doesn't exist on user
// machines. Always go through `python3 <script-path>` so the shebang
// is bypassed and python3's own binary (a real Mach-O) handles loading.
static func speak(_ text: String, voice: String? = nil, speed: String? = nil) {
let venvBin = "\(NSHomeDirectory())/.stack-nudge/venv/bin"
let stackvox = "\(venvBin)/stackvox"
let socketPath = "\(NSHomeDirectory())/.cache/stackvox/daemon.sock"
guard FileManager.default.isExecutableFile(atPath: stackvox) else { return }
let python = "\(venvBin)/python3"
let stackvox = "\(venvBin)/stackvox"
let socketPath = "\(NSHomeDirectory())/.cache/stackvox/daemon.sock"
guard FileManager.default.isExecutableFile(atPath: python),
FileManager.default.isReadableFile(atPath: stackvox)
else { return }

let venvURL = URL(fileURLWithPath: "\(NSHomeDirectory())/.stack-nudge/venv")
.resolvingSymlinksInPath()
var env = ProcessInfo.processInfo.environment
env.merge(Bootstrap.stackvoxEnv(venvURL: venvURL)) { _, new in new }

if !FileManager.default.fileExists(atPath: socketPath) {
let serve = Process()
serve.executableURL = URL(fileURLWithPath: stackvox)
serve.arguments = ["serve"]
// Same espeak-ng data-path workaround as the launchd plist —
// the wheel's libespeak-ng.dylib has a CI-build phontab path
// baked in; ESPEAK_DATA_PATH overrides it at runtime.
let venvURL = URL(fileURLWithPath: "\(NSHomeDirectory())/.stack-nudge/venv")
.resolvingSymlinksInPath()
var env = ProcessInfo.processInfo.environment
env.merge(Bootstrap.stackvoxEnv(venvURL: venvURL)) { _, new in new }
serve.executableURL = URL(fileURLWithPath: python)
serve.arguments = [stackvox, "serve"]
serve.environment = env
try? serve.run()
}
Expand All @@ -34,8 +40,9 @@ enum Speaker {
let resolvedVoice = voice ?? config["STACKNUDGE_VOICE_NAME"] ?? "af_aoede"
let resolvedSpeed = speed ?? config["STACKNUDGE_VOICE_SPEED"] ?? "1.1"
let say = Process()
say.executableURL = URL(fileURLWithPath: stackvox)
say.arguments = ["say", "--voice", resolvedVoice, "--speed", resolvedSpeed, text]
say.executableURL = URL(fileURLWithPath: python)
say.arguments = [stackvox, "say", "--voice", resolvedVoice, "--speed", resolvedSpeed, text]
say.environment = env
say.standardOutput = FileHandle.nullDevice
say.standardError = FileHandle.nullDevice
say.terminationHandler = { ended in
Expand Down