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
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ hosts cannot populate the sidebar.
Expand your Mac in the sidebar and select an existing tmux session. Use the
host's **+** menu to create a named session. Closing its Ghosthub window or tab
only detaches. To end a standalone session, hover its sidebar row and click the
**×**; worktree-backed sessions keep **Kill Session…** in their action menu.
**×**; for a worktree-backed session, Control-click its worktree row and choose
**Kill Session…**.
Both paths confirm the exact host and session before terminating it. If a bare
session exits on its own, **Reopen** creates the same named session again.

Expand Down Expand Up @@ -190,7 +191,14 @@ local tracking branch when needed; unmatched input creates a new branch.
Selecting the primary checkout or a linked worktree creates or repairs its
canonical tmux session when needed, then attaches an ordinary tmux client.

To remove a non-primary worktree, hover its sidebar row and click the **×**.
Use the disclosure chevron beside a worktree to expand its staged, working-tree,
and untracked files without selecting or opening the worktree. Expanded panels
refresh automatically and can be refreshed or collapsed independently. Ghosthub
displays Kwt's semantic status only; it does not show diffs or provide file
actions.

To remove a non-primary worktree, hover over its row and select the **×**, or
Control-click the row and choose **Remove Worktree…**.
After confirmation, Ghosthub terminates that worktree's verified live tmux
session if needed and asks kwt to remove the checkout. The Git branch is kept.

Expand Down
33 changes: 24 additions & 9 deletions Sources/App/AccountCommandRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct AccountCommandRunner: Sendable {
static let timedOutStatus: Int32 = -124
static let outputExceededStatus: Int32 = -125
static let cancelledStatus: Int32 = -130
private static let maximumOutputBytes = 1 * 1_024 * 1_024
static let defaultMaximumOutputBytes = 1 * 1_024 * 1_024
private static let sessionOpenRetryDelays: [useconds_t] = [100_000, 250_000]

typealias ProcessRunner = @Sendable (
Expand All @@ -101,10 +101,20 @@ struct AccountCommandRunner: Sendable {
private let loginShellProvider: @Sendable () -> String

init(
processRunner: @escaping ProcessRunner = Self.runProcess,
processRunner: ProcessRunner? = nil,
maximumOutputBytes: Int = Self.defaultMaximumOutputBytes,
loginShellProvider: @escaping @Sendable () -> String = Self.loginShell
) {
self.processRunner = processRunner
self.processRunner = processRunner ?? {
executable, arguments, timeout, environmentOverrides in
Self.runProcess(
executable: executable,
arguments: arguments,
timeout: timeout,
environmentOverrides: environmentOverrides,
maximumOutputBytes: maximumOutputBytes
)
}
self.loginShellProvider = loginShellProvider
}

Expand Down Expand Up @@ -161,9 +171,11 @@ struct AccountCommandRunner: Sendable {
command: String,
timeout: TimeInterval,
captureStandardError: Bool = false,
environmentOverrides: [String: String] = [:]
environmentOverrides: [String: String] = [:],
maximumOutputBytes: Int = defaultMaximumOutputBytes
) -> (status: Int32, stdout: String) {
let output = AccountCommandRunner(
maximumOutputBytes: maximumOutputBytes,
loginShellProvider: { shell }
).runLocalLoginShell(
command: command,
Expand All @@ -184,7 +196,8 @@ struct AccountCommandRunner: Sendable {
timeout: TimeInterval,
captureStandardError: Bool = false,
accountShell: String = loginShell(),
environmentOverrides: [String: String] = [:]
environmentOverrides: [String: String] = [:],
maximumOutputBytes: Int = defaultMaximumOutputBytes
) -> (status: Int32, stdout: String) {
let command = ([executable] + arguments)
.map(shellQuotedCommandArgument)
Expand All @@ -194,7 +207,8 @@ struct AccountCommandRunner: Sendable {
command: command,
timeout: timeout,
captureStandardError: captureStandardError,
environmentOverrides: environmentOverrides
environmentOverrides: environmentOverrides,
maximumOutputBytes: maximumOutputBytes
)
}

Expand Down Expand Up @@ -223,7 +237,8 @@ struct AccountCommandRunner: Sendable {
executable: String,
arguments: [String],
timeout: TimeInterval,
environmentOverrides: [String: String] = [:]
environmentOverrides: [String: String] = [:],
maximumOutputBytes: Int = defaultMaximumOutputBytes
) -> AccountCommandOutput {
var outputDescriptors = [Int32](repeating: -1, count: 2)
guard outputDescriptors.withUnsafeMutableBufferPointer({ descriptors in
Expand Down Expand Up @@ -321,10 +336,10 @@ struct AccountCommandRunner: Sendable {
return AccountCommandOutput(status: 127, stdout: "", stderr: "")
}
let output = AccountCommandOutputCollector(
limit: maximumOutputBytes
limit: max(0, maximumOutputBytes)
)
let errorOutput = AccountCommandOutputCollector(
limit: maximumOutputBytes
limit: max(0, maximumOutputBytes)
)
var readBuffer = [UInt8](repeating: 0, count: 64 * 1_024)
let deadline = Date().addingTimeInterval(timeout)
Expand Down
14 changes: 10 additions & 4 deletions Sources/App/KwtSSHCommandClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ struct KwtSSHCommandClient: Sendable {
private let runner: Runner
private let binaryPath: String?
private let environment: [String: String]
private let maximumOutputBytes: Int

init(
runner: Runner? = nil,
binaryPath: String? = KwtBinaryLocator.bundledPath(),
environment: [String: String]? = nil
environment: [String: String]? = nil,
maximumOutputBytes: Int = AccountCommandRunner.defaultMaximumOutputBytes
) {
let processEnvironment = environment
?? KwtSSHRuntimeEnvironment.resolved()
Expand All @@ -29,11 +31,13 @@ struct KwtSSHCommandClient: Sendable {
executable: executable,
arguments: arguments,
timeout: timeout,
environmentOverrides: environmentOverrides
environmentOverrides: environmentOverrides,
maximumOutputBytes: maximumOutputBytes
)
}
self.binaryPath = binaryPath
self.environment = processEnvironment
self.maximumOutputBytes = maximumOutputBytes
}

func run(
Expand All @@ -45,7 +49,9 @@ struct KwtSSHCommandClient: Sendable {
let demoArguments = demoSSHIsolationArguments(environment: environment)
if !demoArguments.isEmpty {
return await Self.runDetached {
AccountCommandRunner().runRemoteLoginShell(
AccountCommandRunner(
maximumOutputBytes: maximumOutputBytes
).runRemoteLoginShell(
host: host,
connectionArguments: demoArguments,
command: command,
Expand Down Expand Up @@ -124,7 +130,7 @@ struct KwtSSHCommandClient: Sendable {
: output.stderr
return AccountCommandOutput(
status: 255,
stdout: "",
stdout: output.stdout,
stderr: marker + diagnostic
)
}
Expand Down
Loading
Loading