Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ struct CommandPaletteView: View {
)
}
)
actions.append(
CommandPaletteAction(title: "Go to AWS Settings", subtitle: "AWS provider + bootstrap controls") {
NotificationCenter.default.post(
name: .hackSettingsRequested,
object: nil,
userInfo: [SettingsNavigationRequest.paneKey: SettingsSidebarItem.aws.rawValue]
)
}
)
actions.append(
CommandPaletteAction(title: "Go to Permissions Settings", subtitle: "Automation + local network access") {
NotificationCenter.default.post(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,25 @@ public final class DashboardModel {
}
}

public func bootstrapAwsNode(
request: AwsBootstrapRequest
) async -> AwsBootstrapResponse? {
await runActionResult(message: "Bootstrapping AWS node…") {
try await self.client.bootstrapAwsNode(request: request)
}
}

public func inspectAws(
request: AwsBootstrapRequest? = nil
) async -> AwsInspectResponse? {
do {
return try await client.inspectAws(request: request)
} catch {
errorMessage = error.localizedDescription
return nil
}
}

public func listNodes() async -> NodeRegistryListResponse? {
do {
return try await client.listNodes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ struct NodeTopologySettingsView: View {
Spacer()
if isControllerHost {
Button {
openRailwaySettings()
openAwsSettings()
} label: {
Label("Add remote node", systemImage: "plus.circle")
}
Expand Down Expand Up @@ -201,6 +201,9 @@ struct NodeTopologySettingsView: View {
)
if isControllerHost {
Menu {
Button("Add via AWS") {
openAwsSettings()
}
Button("Add via Railway") {
openRailwaySettings()
}
Expand Down Expand Up @@ -248,7 +251,7 @@ struct NodeTopologySettingsView: View {
.overlay(alignment: .topTrailing) {
if isControllerHost {
Button {
openRailwaySettings()
openAwsSettings()
} label: {
Image(systemName: "plus")
.font(.system(size: 12, weight: .semibold))
Expand Down Expand Up @@ -293,6 +296,10 @@ struct NodeTopologySettingsView: View {
Spacer()
if isControllerHost {
Menu {
Button("Add via AWS") {
topologyExpanded = false
openAwsSettings()
}
Button("Add via Railway") {
topologyExpanded = false
openRailwaySettings()
Expand Down Expand Up @@ -1151,11 +1158,20 @@ struct NodeTopologySettingsView: View {
StatusPill(text: "Unknown", tone: .neutral)
}
StatusPill(text: "Authorized", tone: .neutral)
if let source = node.source, !source.isEmpty {
StatusPill(text: "Source-backed", tone: .good)
}
}
Text(node.endpoint)
.font(.mono(.caption))
.foregroundStyle(.secondary)
.textSelection(.enabled)
if let source = node.source, !source.isEmpty {
Text(source)
.font(.mono(.caption2))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
Text(node.id)
.font(.mono(.caption2))
.foregroundStyle(.tertiary)
Expand Down Expand Up @@ -1436,6 +1452,14 @@ struct NodeTopologySettingsView: View {
)
}

private func openAwsSettings() {
NotificationCenter.default.post(
name: .hackSettingsRequested,
object: nil,
userInfo: [SettingsNavigationRequest.paneKey: SettingsSidebarItem.aws.rawValue]
)
}

private func openTailscaleSettings() {
NotificationCenter.default.post(
name: .hackSettingsRequested,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,8 @@ struct ProjectDetailView: View {
.padding(16)
.background(projectSettingsCardBackground)

explicitRemoteActionsCard

linearRoutingSummaryCard

if let projectSystemGitIdentity {
Expand Down Expand Up @@ -975,6 +977,83 @@ struct ProjectDetailView: View {
}
}

private var explicitRemoteActionsCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Explicit remote actions")
.font(.system(size: 15, weight: .semibold))
Text("Project settings above are defaults. These actions let you use a remote node without changing the saved default behavior.")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
}
Spacer()
}

HStack(alignment: .center, spacing: 8) {
StatusPill(
text: executionMode == .local ? "Default: local" : "Default: \(executionMode.title)",
tone: executionMode == .local ? .neutral : .good
)
StatusPill(
text: explicitRemoteActionNode?.name ?? "No remote node selected",
tone: explicitRemoteActionNode == nil ? .warn : .good
)
StatusPill(
text: explicitRemoteActionHasSource ? "Source-backed" : "No SSH source",
tone: explicitRemoteActionHasSource ? .good : .warn
)
Spacer()
}

HStack(spacing: 10) {
Button {
openExplicitRemoteRun()
} label: {
Label("Run on remote once", systemImage: "play.fill")
}
.adaptiveToolbarButtonProminent()
.disabled(explicitRemoteActionNode == nil)

Button {
openExplicitRemoteDevcontainer()
} label: {
Label("Start devcontainer", systemImage: "shippingbox.circle")
}
.adaptiveToolbarButton()
.disabled(!explicitRemoteActionHasSource)

Button {
openExplicitRemoteNodeStatus()
} label: {
Label("Inspect node", systemImage: "stethoscope")
}
.adaptiveToolbarButton()
.disabled(explicitRemoteActionNode == nil)

Button {
NotificationCenter.default.post(
name: .hackSettingsRequested,
object: nil,
userInfo: [SettingsNavigationRequest.paneKey: SettingsSidebarItem.topology.rawValue]
)
} label: {
Label("Open topology", systemImage: "point.3.connected.trianglepath.dotted")
}
.adaptiveToolbarButton()

Spacer()
}

Text(explicitRemoteActionFootnote)
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
.padding(16)
.background(projectSettingsCardBackground)
}

private var defaultLinearRouteTarget: LinearProjectBindingTarget? {
guard !linearBoundProjectId.isEmpty else {
return nil
Expand Down Expand Up @@ -1712,6 +1791,41 @@ struct ProjectDetailView: View {
return "Inherited (\(inheritedDefaultNodeId))"
}

private var dispatchProjectSelector: String {
let candidate = (project.projectId ?? project.name).trimmingCharacters(in: .whitespacesAndNewlines)
if !candidate.isEmpty {
return candidate
}
return project.name
}

private var explicitRemoteActionNode: NodeRegistryRecord? {
let selectedNodeId = executionTargetNodeId.trimmingCharacters(in: .whitespacesAndNewlines)
if !selectedNodeId.isEmpty {
return executionTargetNodes.first(where: { $0.id == selectedNodeId })
}
let defaultNodeId = executionDefaultNodeId.trimmingCharacters(in: .whitespacesAndNewlines)
if !defaultNodeId.isEmpty {
return executionTargetNodes.first(where: { $0.id == defaultNodeId })
}
return executionTargetNodes.first(where: { $0.isDefault == true })
}

private var explicitRemoteActionHasSource: Bool {
let source = explicitRemoteActionNode?.source?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return !source.isEmpty
}

private var explicitRemoteActionFootnote: String {
guard let node = explicitRemoteActionNode else {
return "Pick a remote node above or from Topology to enable one-off remote actions."
}
if explicitRemoteActionHasSource {
return "Using \(node.name) with SSH source \(node.source ?? "")."
}
return "Using \(node.name). Add SSH source metadata if you want source-backed flows like devcontainer attach."
}

private var selectedGitSummary: String {
let selectedProfileId = githubProjectProfile.trimmingCharacters(in: .whitespacesAndNewlines)
guard !selectedProfileId.isEmpty else {
Expand Down Expand Up @@ -3854,4 +3968,40 @@ struct ProjectDetailView: View {
return "'\(value.replacingOccurrences(of: "'", with: "'\"'\"'"))'"
}

private func openExplicitRemoteRun() {
guard let node = explicitRemoteActionNode else {
return
}
let command = [
"hack dispatch run",
"--project \(shellQuote(dispatchProjectSelector))",
"--node \(shellQuote(node.id))",
"--target remote",
"--runner generic",
"--",
"sh -lc \(shellQuote("pwd && uname -a"))",
].joined(separator: " ")
openTerminal(kind: .shell, command: command, title: "remote run")
Comment on lines +3975 to +3984

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Drop the unsupported --target remote flag from the dispatch command.

hack dispatch run in this PR's CLI reference doesn't expose a --target option, so this button currently opens a command that will fail with a usage error instead of dispatching anything. Selecting --node already makes the run explicit.

Suggested fix
   let command = [
     "hack dispatch run",
     "--project \(shellQuote(dispatchProjectSelector))",
     "--node \(shellQuote(node.id))",
-    "--target remote",
     "--runner generic",
     "--",
     "sh -lc \(shellQuote("pwd && uname -a"))",
   ].joined(separator: " ")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/macos/Packages/Features/DashboardFeature/Sources/DashboardFeature/ProjectDetailView.swift`
around lines 3975 - 3984, The generated dispatch command incorrectly includes
the unsupported "--target remote" flag; update the command construction in
ProjectDetailView (the array building the local variable command that calls
openTerminal(kind: .shell, command: command, title: "remote run")) to remove the
"--target remote" element while keeping the other flags (including "--project
\(shellQuote(dispatchProjectSelector))" and "--node \(shellQuote(node.id))") and
the final shell invocation; ensure the title and call to openTerminal remain
unchanged.

}

private func openExplicitRemoteDevcontainer() {
guard let node = explicitRemoteActionNode, explicitRemoteActionHasSource else {
return
}
let command = [
"hack node devcontainer up",
"--node \(shellQuote(node.id))",
"--project \(shellQuote(dispatchProjectSelector))",
].joined(separator: " ")
openTerminal(kind: .shell, command: command, title: "remote devcontainer")
}

private func openExplicitRemoteNodeStatus() {
guard let node = explicitRemoteActionNode else {
return
}
let command = "hack node status --node \(shellQuote(node.id)) --json"
openTerminal(kind: .shell, command: command, title: "node status")
}

}
Loading
Loading